summaryrefslogtreecommitdiff
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
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>
-rw-r--r--cmd/ioworkload/scenario_readwrite.go72
-rw-r--r--cmd/ioworkload/scenarios.go1
-rw-r--r--go.mod4
-rw-r--r--integrationtests/readwrite_test.go26
4 files changed, 101 insertions, 2 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,
diff --git a/go.mod b/go.mod
index 7e68ecd..c9c4f0c 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,8 @@ require (
github.com/charmbracelet/x/term v0.2.2
github.com/magefile/mage v1.15.0
github.com/parquet-go/parquet-go v0.29.0
+ golang.org/x/sync v0.19.0
+ golang.org/x/sys v0.41.0
)
require (
@@ -35,7 +37,5 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/twpayne/go-geom v1.6.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
- golang.org/x/sync v0.19.0 // indirect
- golang.org/x/sys v0.41.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)
diff --git a/integrationtests/readwrite_test.go b/integrationtests/readwrite_test.go
index f4f528a..8bfa539 100644
--- a/integrationtests/readwrite_test.go
+++ b/integrationtests/readwrite_test.go
@@ -302,6 +302,32 @@ func TestReadwriteReadaheadEbadf(t *testing.T) {
}, 0)
}
+func TestReadwriteCachestat(t *testing.T) {
+ // cachestat(2) is KindFd / UNCLASSIFIED: it queries page-cache residency for
+ // a file and returns 0/-1 (no byte count, no I/O bytes to userspace), so the
+ // tracer must attribute zero bytes while still capturing the fd (args[0]) on
+ // enter and timing the syscall. cachestat is Linux 6.5+; the enter tracepoint
+ // fires before the kernel checks availability, so even an ENOSYS kernel would
+ // still record enter_cachestat, but this dev kernel (7.0.9) supports it fully.
+ result, _ := runScenarioResult(t, "readwrite-cachestat", []ExpectedEvent{
+ {
+ PathContains: "cachestatfile.txt",
+ Tracepoint: "enter_cachestat",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ })
+ exp := ExpectedEvent{
+ PathContains: "cachestatfile.txt",
+ Tracepoint: "enter_cachestat",
+ Comm: "ioworkload",
+ }
+ // UNCLASSIFIED: no byte count is attributed for a successful cachestat.
+ assertEventBytesEqual(t, result, exp, 0)
+ // Timing is captured end-to-end (enter/exit paired into a duration).
+ assertEventDurationPositive(t, result, exp)
+}
+
func assertEventBytesAtLeast(t *testing.T, result TestResult, exp ExpectedEvent, minBytes uint64) {
t.Helper()
var matched bool