summaryrefslogtreecommitdiff
path: root/cmd/ioworkload
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-02 21:32:47 +0300
committerPaul Buetow <paul@buetow.org>2026-06-02 21:32:47 +0300
commit4f44de8ee0ec51ee5c934048405030e362cc197f (patch)
tree3babca11e491761484238782944cd65933dede97 /cmd/ioworkload
parent8e8cad2f1a085366eff89f6299d49637cb4d425f (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.go72
-rw-r--r--cmd/ioworkload/scenarios.go1
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,