diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-02 21:32:47 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-02 21:32:47 +0300 |
| commit | 4f44de8ee0ec51ee5c934048405030e362cc197f (patch) | |
| tree | 3babca11e491761484238782944cd65933dede97 /cmd/ioworkload | |
| parent | 8e8cad2f1a085366eff89f6299d49637cb4d425f (diff) | |
test(integration): add cachestat end-to-end coverage
cachestat(2) had no integration coverage. Add a readwrite-cachestat
ioworkload scenario and TestReadwriteCachestat mirroring the readahead
precedent: open a temp file, write data to populate the page cache, then
issue cachestat via a raw syscall (no glibc/unix wrapper) with a whole-file
cachestat_range and zeroed cachestat output buffer, flags=0. ENOSYS on
kernels < 6.5 is tolerated for portability.
The test asserts enter_cachestat is captured with the fd-resolved file
path, that the UNCLASSIFIED return attributes zero bytes, and that the
syscall duration is positive. golang.org/x/sys is promoted to a direct
dependency. Verified PASS under sudo on kernel 7.0.9.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'cmd/ioworkload')
| -rw-r--r-- | cmd/ioworkload/scenario_readwrite.go | 72 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 1 |
2 files changed, 73 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_readwrite.go b/cmd/ioworkload/scenario_readwrite.go index 04ee86f..21fba8e 100644 --- a/cmd/ioworkload/scenario_readwrite.go +++ b/cmd/ioworkload/scenario_readwrite.go @@ -6,6 +6,8 @@ import ( "runtime" "syscall" "unsafe" + + "golang.org/x/sys/unix" ) // readwriteBasic opens a file, writes data, seeks to start, reads it back. @@ -383,6 +385,76 @@ func readwriteReadaheadEbadf() error { return nil } +// cachestatRange mirrors the kernel's struct cachestat_range, the second +// cachestat(2) argument: { __u64 off; __u64 len; }. off=0/len=0 means "the +// whole file". +type cachestatRange struct { + off uint64 + len uint64 +} + +// cachestatResult mirrors the kernel's struct cachestat output, the third +// cachestat(2) argument. The kernel fills it in; the scenario only needs to +// hand the kernel a correctly-sized buffer, so the fields are not inspected. +type cachestatResult struct { + nrCache uint64 + nrDirty uint64 + nrWriteback uint64 + nrEvicted uint64 + nrRecentlyEvicted uint64 +} + +// readwriteCachestat opens a file, writes data (so the file has pages in the +// page cache), then queries the cache residency of the whole file via +// cachestat(2). cachestat has no glibc/unix wrapper, so it is issued as a raw +// syscall: cachestat(fd, &cachestat_range{0,0}, &cachestat, flags=0). It returns +// 0 on success / -1 on error and transfers no I/O bytes to userspace, so ior +// classifies it KindFd / UNCLASSIFIED. The scenario exercises the enter fd_event +// (fd at args[0]) and the exit ret_event end-to-end. cachestat is Linux 6.5+; +// ENOSYS on older kernels is tolerated so the workload stays portable. +func readwriteCachestat() error { + dir, cleanup, err := makeTempDir("readwrite-cachestat") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "cachestatfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + defer syscall.Close(fd) + + if _, err := syscall.Write(fd, []byte("cachestat test data")); err != nil { + return fmt.Errorf("write: %w", err) + } + + // cachestat(fd, &range{off:0,len:0}=whole file, &cstat output, flags=0). + cr := cachestatRange{off: 0, len: 0} + var cs cachestatResult + _, _, errno := syscall.Syscall6( + unix.SYS_CACHESTAT, + uintptr(fd), + uintptr(unsafe.Pointer(&cr)), + uintptr(unsafe.Pointer(&cs)), + 0, // flags must be 0 + 0, + 0, + ) + runtime.KeepAlive(cr) + runtime.KeepAlive(cs) + if errno == syscall.ENOSYS { + // Kernel < 6.5: cachestat is unavailable. Tolerate gracefully so the + // scenario does not fail on older portable targets. + return nil + } + if errno != 0 { + return fmt.Errorf("cachestat: %w", errno) + } + return nil +} + // readwriteWronlyRead opens a file O_WRONLY, then attempts to read from it. // The read fails with EBADF, but ior should capture the enter_read tracepoint // because arguments are read on syscall entry before the kernel returns an error. diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go index 68dc3d0..7fa3535 100644 --- a/cmd/ioworkload/scenarios.go +++ b/cmd/ioworkload/scenarios.go @@ -31,6 +31,7 @@ var scenarios = map[string]func() error{ "readwrite-pwrite-invalid": readwritePwriteInvalid, "readwrite-readahead": readwriteReadahead, "readwrite-readahead-ebadf": readwriteReadaheadEbadf, + "readwrite-cachestat": readwriteCachestat, "retbytes-phase-a": retbytesPhaseA, "socket-basic": socketBasic, "socketpair-basic": socketpairBasic, |
