diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-06 09:05:36 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-06 09:05:36 +0300 |
| commit | 33dfe4ee3cf948444571554aa35508605fee0474 (patch) | |
| tree | 90e462bb697e55814b644e61d55d10ef155571af | |
| parent | 1b4202ac84acd129e274112a8293b4b319af9eb0 (diff) | |
test(retbytes): assert getdents64 READ_CLASSIFIED byte count end-to-end
getdents64 was listed in retbytesTraceArgs but the retbytes workload
never issued it and TestRetbytesPhaseA never asserted its exit byte
count, leaving the READ_CLASSIFIED ctx->ret path for getdents/getdents64
unverified end-to-end (unlike sibling read/recvfrom/getxattrat).
Add retbytesGetdents to the phase-A workload: it populates a temp
directory with several files, opens it O_DIRECTORY, and re-issues
getdents64 in a short window (lseek-rewind each iteration) so ior can
attach and capture an enter/exit pair under parallel load. A non-empty
directory guarantees ctx->ret > 0.
Assert enter_getdents64 presence (MinCount) plus bytes>=1 via
assertEventBytesAtLeast and a positive duration, mirroring the existing
READ-classified siblings. Bytes>=1 (not an exact payload length) because
dirent size varies with filename length and alignment.
Coverage hardening only; classify.go getdents/getdents64=ReadClassified
is correct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| -rw-r--r-- | cmd/ioworkload/scenario_retbytes.go | 53 | ||||
| -rw-r--r-- | integrationtests/retbytes_test.go | 9 |
2 files changed, 62 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_retbytes.go b/cmd/ioworkload/scenario_retbytes.go index d9b2984..0382db2 100644 --- a/cmd/ioworkload/scenario_retbytes.go +++ b/cmd/ioworkload/scenario_retbytes.go @@ -6,6 +6,7 @@ import ( "path/filepath" "runtime" "syscall" + "time" "unsafe" ) @@ -39,9 +40,61 @@ func retbytesPhaseA() error { if err := retbytesTee(); err != nil { return err } + if err := retbytesGetdents(); err != nil { + return err + } return retbytesProcessVM() } +// retbytesGetdents opens a non-empty directory and reads its entries via +// getdents64(2). getdents/getdents64 are READ_CLASSIFIED, so a successful call +// on a populated directory returns ctx->ret > 0 (bytes filled into the dirent +// buffer). This drives the exit byte-count assertion in TestRetbytesPhaseA. +func retbytesGetdents() error { + dir, cleanup, err := makeTempDir("retbytes-getdents") + if err != nil { + return err + } + defer cleanup() + + // Create several files so getdents64 has substantial dirent data to return, + // guaranteeing a strictly positive byte count. + for i := 0; i < 4; i++ { + filePath := filepath.Join(dir, fmt.Sprintf("getdents-file-%d.txt", i)) + fd, openErr := syscall.Open(filePath, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if openErr != nil { + return fmt.Errorf("open file: %w", openErr) + } + syscall.Close(fd) + } + + dirFD, err := syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0) + if err != nil { + return fmt.Errorf("open dir: %w", err) + } + defer syscall.Close(dirFD) + + // Re-issue getdents64 in a short window so ior has enough time to attach and + // capture an enter/exit pair under high parallel integration load. Each call + // rewinds via lseek so the directory keeps returning entries. + buf := make([]byte, 4096) + for i := 0; i < 40; i++ { + if _, err := syscall.Seek(dirFD, 0, 0); err != nil { + return fmt.Errorf("seek dir: %w", err) + } + n, _, errno := syscall.Syscall(syscall.SYS_GETDENTS64, uintptr(dirFD), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) + runtime.KeepAlive(buf) + if errno != 0 { + return fmt.Errorf("getdents64: %w", errno) + } + if n == 0 { + return fmt.Errorf("getdents64 returned 0 bytes on a non-empty directory") + } + time.Sleep(25 * time.Millisecond) + } + return nil +} + func retbytesSocketIO() error { fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) if err != nil { diff --git a/integrationtests/retbytes_test.go b/integrationtests/retbytes_test.go index 4baed9e..37eb45b 100644 --- a/integrationtests/retbytes_test.go +++ b/integrationtests/retbytes_test.go @@ -19,6 +19,7 @@ func TestRetbytesPhaseA(t *testing.T) { {Tracepoint: "enter_tee", Comm: "ioworkload", MinCount: 1}, {Tracepoint: "enter_process_vm_writev", Comm: "ioworkload", MinCount: 1}, {Tracepoint: "enter_process_vm_readv", Comm: "ioworkload", MinCount: 1}, + {Tracepoint: "enter_getdents64", Comm: "ioworkload", MinCount: 1}, }, retbytesTraceArgs) for _, tracepoint := range []string{ @@ -42,4 +43,12 @@ func TestRetbytesPhaseA(t *testing.T) { assertEventBytesEqual(t, result, exp, 0) assertEventDurationPositive(t, result, exp) } + + // getdents64 is READ_CLASSIFIED: a successful call on a non-empty directory + // fills the dirent buffer and reports ctx->ret > 0. Dirent size varies with + // filename length and alignment, so assert a conservative bytes>=1 minimum + // rather than an exact payload length. + getdentsExp := ExpectedEvent{Tracepoint: "enter_getdents64", Comm: "ioworkload"} + assertEventBytesAtLeast(t, result, getdentsExp, 1) + assertEventDurationPositive(t, result, getdentsExp) } |
