Skip to content

passthrough: test the readdir/readdirplus contracts and benchmark directory enumeration - #261

Merged
jiangliu merged 2 commits into
cloud-hypervisor:masterfrom
jiangliu:test/passthrough-readdir
Oct 9, 2026
Merged

jiangliu merged 2 commits into
cloud-hypervisor:masterfrom
jiangliu:test/passthrough-readdir

Conversation

@jiangliu

@jiangliu jiangliu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two commits closing the readdir coverage gap in both the test suite and
the benchmark harness:

  • passthrough: test the readdir and readdirplus contracts — a new
    readdir_tests module (10 tests) in fuse-backend-passthrough that
    drives the READDIR/READDIRPLUS handlers end to end through a
    persistent opendir() handle over a real backing directory.
  • benchmark: add a readdir workload — a readdir workload for the
    fio comparison in bench_sync_async.sh, driven by the new
    bench_readdir.py helper (fio has no readdir engine).

Why

The existing readdir coverage stops at getdents64 buffer parsing
(readdir_cookie_tests), the no_opendir re-seek regression (xfstests
generic/676), and the shallow error paths of test_dir_operations. The
flow the kernel actually drives — persistent directory handles,
cookie-based resume, reply-batch splitting, the readdirplus attribute
and ownership contracts — had no coverage at all.

Likewise, the benchmark comparison covers data IO and create/delete
metadata, but not directory enumeration: changes to the
READDIR/READDIRPLUS handlers had no performance signal (e.g. for
evaluating an offload of these ops to a blocking pool).

What the tests pin

Contract Test
Multi-batch enumeration: complete and exactly-once delivery via cookie resume, strictly increasing non-zero cookies, stable order across repeated enumerations readdir_enumerates_all_entries_in_batches
readdirplus attrs: dirent.ino == attr.st_ino; the accompanying Entry matches a plain lookup() of the name (inode/mode/size) readdirplus_attrs_match_lookup
Ownership: plain readdir is reference-neutral; readdirplus keeps the kernel-owned reference until FORGET readdir_releases_lookup_references
An entry rejected by a full reply buffer (add_entry -> 0) has its readdirplus reference released immediately instead of leaking readdirplus_buffer_full_forgets_undelivered
Error contract: the callback's error propagates before the first stored entry (exercised on a resumed stream — a fresh one begins with the internally filtered ./..); after a delivered entry it returns Ok(()) with the partial delivery readdir_error_contract
Cookie resume: mid-stream repositioning via lseek; a stale cached cookie is discarded (the cached cookie is a one-shot token); a > i64::MAX cookie terminates at EOF through the linear scan fallback readdir_resume_from_mid_stream_cookie
d_type passthrough for REG/DIR/LNK entries readdir_reports_entry_types
Empty directory hits EOF immediately; size == 0 does not touch the directory readdir_empty_directory
no_readdir kill switch reports success without enumerating readdir_disabled_returns_empty
releasedir drops the handle together with the cached cookie; the released handle stops working releasedir_clears_the_cookie_cache

The setup is path-based (no inode file handles), so the whole suite runs
unprivileged.

Benchmark workload

  • bench_readdir.py walks the mountpoint with os.scandir()
    (getdents64): a 2 s ramp whose counts are discarded, then a
    ${RUNTIME} s window measured by the wall clock spanning the whole
    run. It emits fio-shaped JSON (entries/s as iops, approximated
    reply bytes as bw/io_bytes), so bench_compare.py and the CI
    collect/compare steps treat it like any other workload — no special
    casing anywhere.
  • It runs between filecreate and filedelete, when the directory
    holds ${NRFILES} entries.
  • Classified with the metadata workloads for the looser (25 %)
    regression threshold.
  • CI picks the workload up automatically via the existing
    ${mode}-*.json glob; python3 was already a CI dependency.

Results

  • cargo test -p fuse-backend-passthrough --lib (unprivileged Linux
    host): 49 passed / 12 failed — the 12 are the known pre-existing
    environmental EPERM tests that require root, unchanged by this PR.
    All 10 new tests pass.
  • cargo fmt / cargo clippy clean in the new code.
  • End-to-end benchmark run (RUNTIME=5, NRFILES=20000):
    sync 213k / async 212k entries/s — parity, as expected: one
    enumeration is a strictly sequential request chain (each
    getdents64 resumes from the cookie of the previous reply), so the
    rate is latency-bound and does not scale with the worker count. The
    workload is the measurement baseline for future
    READDIR/READDIRPLUS changes rather than a sync-vs-async finding.

The existing coverage stops at buffer parsing (readdir_cookie_tests),
the no_opendir re-seek regression (generic/676), and shallow error
paths of test_dir_operations. The persistent-opendir flow that the
kernel actually drives -- opendir handle, cookie resume, batch
splitting, the readdirplus attribute and ownership contracts -- had
no coverage at all.

Add a readdir_tests module exercising that flow against a real backing
directory through a persistent handle:

- enumeration: a stream that does not fit one reply batch is drained
  completely and exactly once by resuming from the cookie of the last
  delivered entry, with strictly increasing non-zero cookies and a
  stable order across repeated enumerations of the same handle;
- readdirplus contract: the dirent ino equals the accompanying
  Entry's attr.st_ino, the Entry matches a plain lookup of the name
  (inode, mode, size), and delivered entries stay mapped with exactly
  the kernel-owned reference until forget() drops them;
- ownership: plain readdir is reference-neutral (transient per-entry
  lookups are forgotten again), and an entry rejected by a full reply
  buffer (add_entry -> 0) has its readdirplus reference released
  immediately instead of leaking;
- error contract: an error before anything was stored propagates
  (exercised on a resumed stream, since a fresh one begins with the
  internally filtered "." and ".."), an error after a delivered
  entry returns Ok(()) with the partial delivery;
- cookies: resuming from a mid-stream cookie repositions via lseek,
  a cookie beyond i64::MAX terminates at EOF through the linear scan
  fallback, and releasedir drops the cached cookie together with the
  handle (the cached cookie is a one-shot token consumed by the
  matching resume, which the drain helper's final EOF probe does);
- plus the d_type passthrough of REG/DIR/LNK entries, the empty
  directory, size == 0, and the no_readdir kill switch.

The setup uses a plain path-based config without inode file handles,
so the suite runs unprivileged like the rest of the crate's
non-privileged tests.

Signed-off-by: Jiang Liu <gerry@linux.alibaba.com>
The fio comparison covers data IO and create/delete metadata, but not
directory enumeration -- the READDIR/READDIRPLUS handlers, whose sync
vs async cost profile is exactly the open question of the offload
discussion. fio has no readdir engine, so drive it with a helper
instead: bench_readdir.py walks the mountpoint with os.scandir()
(getdents64) over and over, first for a 2 second ramp whose counts are
discarded, then for ${RUNTIME} seconds measured by the wall clock that
spans the whole window. It writes fio-shaped JSON (entries/s as iops
on the read side) so bench_compare.py and the CI collect/compare steps
treat it like any other workload without any special casing.

Run it between filecreate and filedelete, when the directory holds
${NRFILES} entries: the workload is self-contained in the create/
delete pair and sees a realistic directory size. Enumeration is one
strictly sequential request chain per stream -- each getdents64
resumes from the cookie of the previous reply -- so the rate is
latency-bound and does not scale with THREADS.

Classify readdir with the metadata workloads for the looser regression
threshold: it is a per-entry metadata operation like create/delete,
and its byte metrics are only approximated reply traffic anyway. On
an unprivileged host the run reports ~210k entries/s with sync and
async at parity (a 5 second smoke run on a 20k-entry directory).

Signed-off-by: Jiang Liu <gerry@linux.alibaba.com>
@jiangliu jiangliu added the run-benchmark Run performance benchmark for the PR label Oct 9, 2026
@jiangliu
jiangliu merged commit 05f971c into cloud-hypervisor:master Oct 9, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-benchmark Run performance benchmark for the PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant