diff options
| -rw-r--r-- | cmd/ioworkload/scenario_readwrite.go | 72 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 1 | ||||
| -rw-r--r-- | go.mod | 4 | ||||
| -rw-r--r-- | integrationtests/readwrite_test.go | 26 |
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, @@ -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 |
