summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--integrationtests/cmd/ioworkload/main.go6
-rw-r--r--integrationtests/harness.go23
-rw-r--r--integrationtests/readwrite_test.go35
-rw-r--r--internal/flags/flags_test.go4
-rw-r--r--internal/flamegraph/liveserver.go18
5 files changed, 67 insertions, 19 deletions
diff --git a/integrationtests/cmd/ioworkload/main.go b/integrationtests/cmd/ioworkload/main.go
index 1261c9f..0276a9c 100644
--- a/integrationtests/cmd/ioworkload/main.go
+++ b/integrationtests/cmd/ioworkload/main.go
@@ -12,9 +12,9 @@ import (
)
// Give ior enough time to attach tracepoints before scenarios emit syscalls.
-// Under parallel integration load, 2s can be too short and cause missed
-// first-call events for single-shot scenarios.
-const startupDelay = 5 * time.Second
+// Under slower CI or locally saturated systems, 5s can still miss first-call
+// events for single-shot scenarios. Use a slightly larger delay for stability.
+const startupDelay = 8 * time.Second
func main() {
scenario := flag.String("scenario", "", "I/O scenario to execute")
diff --git a/integrationtests/harness.go b/integrationtests/harness.go
index a8a73d0..17ae994 100644
--- a/integrationtests/harness.go
+++ b/integrationtests/harness.go
@@ -102,6 +102,9 @@ func (h *TestHarness) startWorkload(scenario string) (*exec.Cmd, int, error) {
io.Copy(io.Discard, stdout) //nolint:errcheck
}()
+ startupTimer := time.NewTimer(workloadStartupTimeout)
+ defer stopAndDrainTimer(startupTimer)
+
select {
case pid := <-pidCh:
return cmd, pid, nil
@@ -109,7 +112,7 @@ func (h *TestHarness) startWorkload(scenario string) (*exec.Cmd, int, error) {
cmd.Process.Kill()
cmd.Wait()
return nil, 0, err
- case <-time.After(workloadStartupTimeout):
+ case <-startupTimer.C:
cmd.Process.Kill()
cmd.Wait()
return nil, 0, fmt.Errorf("timeout waiting for workload PID")
@@ -151,7 +154,8 @@ func waitBoth(workloadCmd, iorCmd *exec.Cmd, duration int, grace time.Duration)
go func(ch chan error) { ch <- workloadCmd.Wait() }(workloadDone)
go func(ch chan error) { ch <- iorCmd.Wait() }(iorDone)
- timeout := time.After(time.Duration(duration)*time.Second + grace)
+ timeout := time.NewTimer(time.Duration(duration)*time.Second + grace)
+ defer stopAndDrainTimer(timeout)
for workloadDone != nil || iorDone != nil {
select {
@@ -161,7 +165,7 @@ func waitBoth(workloadCmd, iorCmd *exec.Cmd, duration int, grace time.Duration)
case err := <-iorDone:
iorErr = err
iorDone = nil
- case <-timeout:
+ case <-timeout.C:
if iorDone != nil {
iorCmd.Process.Kill()
iorErr = fmt.Errorf("ior timed out")
@@ -178,6 +182,19 @@ func waitBoth(workloadCmd, iorCmd *exec.Cmd, duration int, grace time.Duration)
return
}
+func stopAndDrainTimer(timer *time.Timer) {
+ if timer == nil {
+ return
+ }
+ if timer.Stop() {
+ return
+ }
+ select {
+ case <-timer.C:
+ default:
+ }
+}
+
// findIorZstFile locates the .ior.zst file matching the scenario name in the output directory.
func findIorZstFile(dir, scenario string) (string, error) {
entries, err := os.ReadDir(dir)
diff --git a/integrationtests/readwrite_test.go b/integrationtests/readwrite_test.go
index a55bf78..4e8cbef 100644
--- a/integrationtests/readwrite_test.go
+++ b/integrationtests/readwrite_test.go
@@ -98,11 +98,11 @@ func TestReadwriteWronlyRead(t *testing.T) {
MinCount: 1,
},
})
- assertEventBytesAtLeast(t, result, ExpectedEvent{
+ assertEventBytesEqual(t, result, ExpectedEvent{
PathContains: "wronlyfile.txt",
Tracepoint: "enter_read",
Comm: "ioworkload",
- }, 1)
+ }, 0)
assertEventBytesReasonable(t, result, ExpectedEvent{
PathContains: "wronlyfile.txt",
Tracepoint: "enter_read",
@@ -119,11 +119,11 @@ func TestReadwriteRdonlyWrite(t *testing.T) {
MinCount: 1,
},
})
- assertEventBytesAtLeast(t, result, ExpectedEvent{
+ assertEventBytesEqual(t, result, ExpectedEvent{
PathContains: "rdonlywritefile.txt",
Tracepoint: "enter_write",
Comm: "ioworkload",
- }, 1)
+ }, 0)
assertEventBytesReasonable(t, result, ExpectedEvent{
PathContains: "rdonlywritefile.txt",
Tracepoint: "enter_write",
@@ -140,11 +140,11 @@ func TestReadwritePreadInvalid(t *testing.T) {
MinCount: 1,
},
})
- assertEventBytesAtLeast(t, result, ExpectedEvent{
+ assertEventBytesEqual(t, result, ExpectedEvent{
PathContains: "preadinvalid.txt",
Tracepoint: "enter_pread64",
Comm: "ioworkload",
- }, 1)
+ }, 0)
assertEventBytesReasonable(t, result, ExpectedEvent{
PathContains: "preadinvalid.txt",
Tracepoint: "enter_pread64",
@@ -161,11 +161,11 @@ func TestReadwritePwriteInvalid(t *testing.T) {
MinCount: 1,
},
})
- assertEventBytesAtLeast(t, result, ExpectedEvent{
+ assertEventBytesEqual(t, result, ExpectedEvent{
PathContains: "pwriteinvalid.txt",
Tracepoint: "enter_pwrite64",
Comm: "ioworkload",
- }, 1)
+ }, 0)
assertEventBytesReasonable(t, result, ExpectedEvent{
PathContains: "pwriteinvalid.txt",
Tracepoint: "enter_pwrite64",
@@ -192,6 +192,25 @@ func assertEventBytesAtLeast(t *testing.T, result TestResult, exp ExpectedEvent,
}
}
+func assertEventBytesEqual(t *testing.T, result TestResult, exp ExpectedEvent, wantBytes uint64) {
+ t.Helper()
+ var matched bool
+ var totalBytes uint64
+ for _, rec := range result.Records {
+ if !matchesExpectation(rec, exp) {
+ continue
+ }
+ matched = true
+ totalBytes += rec.Cnt.Bytes
+ }
+ if !matched {
+ t.Fatalf("expected event not found while asserting bytes: %+v", exp)
+ }
+ if totalBytes != wantBytes {
+ t.Fatalf("bytes for %+v mismatch: got=%d want=%d", exp, totalBytes, wantBytes)
+ }
+}
+
func assertEventBytesReasonable(t *testing.T, result TestResult, exp ExpectedEvent) {
t.Helper()
const maxReasonableBytes = 1 << 20 // 1MiB is far above any bytes used in these scenarios.
diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go
index 81d01ad..54c65b8 100644
--- a/internal/flags/flags_test.go
+++ b/internal/flags/flags_test.go
@@ -5,7 +5,6 @@ import (
"io"
"os"
"strings"
- "sync"
"testing"
"time"
)
@@ -16,7 +15,6 @@ func parseForTest(t *testing.T, args ...string) (Flags, error) {
oldCommandLine := flag.CommandLine
oldArgs := os.Args
oldSingleton := singleton
- oldOnce := once
oldParseErr := parseErr
oldPID := pidFilter.Load()
oldTID := tidFilter.Load()
@@ -28,7 +26,6 @@ func parseForTest(t *testing.T, args ...string) (Flags, error) {
os.Args = append([]string{"ior"}, args...)
singleton = Flags{TUIExportEnable: true}
- once = sync.Once{}
parseErr = nil
pidFilter.Store(-1)
tidFilter.Store(-1)
@@ -41,7 +38,6 @@ func parseForTest(t *testing.T, args ...string) (Flags, error) {
flag.CommandLine = oldCommandLine
os.Args = oldArgs
singleton = oldSingleton
- once = oldOnce
parseErr = oldParseErr
pidFilter.Store(oldPID)
tidFilter.Store(oldTID)
diff --git a/internal/flamegraph/liveserver.go b/internal/flamegraph/liveserver.go
index 4cc5629..8ae2b82 100644
--- a/internal/flamegraph/liveserver.go
+++ b/internal/flamegraph/liveserver.go
@@ -71,16 +71,32 @@ func openBrowserURL(url, openCommand string) error {
waitCh := make(chan error, 1)
go func() { waitCh <- cmd.Wait() }()
+ timer := time.NewTimer(750 * time.Millisecond)
+ defer stopAndDrainTimer(timer)
+
select {
case waitErr := <-waitCh:
if waitErr != nil {
return fmt.Errorf("browser command exited early: %w", waitErr)
}
- case <-time.After(750 * time.Millisecond):
+ case <-timer.C:
}
return nil
}
+func stopAndDrainTimer(timer *time.Timer) {
+ if timer == nil {
+ return
+ }
+ if timer.Stop() {
+ return
+ }
+ select {
+ case <-timer.C:
+ default:
+ }
+}
+
func notifyLiveWarning(warningCb func(string), message string) {
if message == "" {
return