diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5d1435e..ff0f247 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -89,8 +89,9 @@ flowchart LR `runner.RunJob` builds the platform-specific invocation, executes the command through the platform shell, captures stdout and stderr, writes one timestamped `.log` file, and returns a `domain.RunRecord` containing - `DurationMS` (wall-clock milliseconds from start to finish; 0 for - `StartOnly` fire-and-forget jobs). + `DurationMS` (wall-clock milliseconds from start to finish; for `StartOnly` + fire-and-forget jobs it measures launch latency — the time to spawn the + process — since there is no exit to wait for). 6. History update: When a run goroutine completes, `Service` updates the job's runtime @@ -132,7 +133,7 @@ in flight increments `JobRuntime.PendingRuns`. When the current run finishes, |-------|---------| | `RunCount` | total runs recorded | | `FailCount` | runs that exited non-zero | -| `LastDurationMS` | wall-clock time of the most recent run | +| `LastDurationMS` | wall-clock time of the most recent run (launch latency for `StartOnly`) | | `AvgDurationMS` | mean over all runs with a recorded duration | | `MaxDurationMS` | longest recorded run | diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 819a422..62092a4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,14 @@ All notable GoSentry changes are recorded in this file. +## Unreleased + +**Statistics:** +- `StartOnly` jobs now record launch latency (time to spawn the process) as the + run duration instead of a hard-coded `0`, so the Statistics line shows a real + last/avg/max for fire-and-forget jobs. Sub-millisecond launches still round to + 0 and are excluded from the average, as before. + ## 0.11.3 - 2026-06-29 **Reliability fixes from an internal code review: safer runs, a real overlap diff --git a/src/domain/runtime.go b/src/domain/runtime.go index 874fa28..2d6ed0f 100644 --- a/src/domain/runtime.go +++ b/src/domain/runtime.go @@ -32,7 +32,9 @@ type JobRuntime struct { AvgDurationMS int64 MaxDurationMS int64 // TimedRunCount is the number of runs that contributed to AvgDurationMS. - // StartOnly and legacy duration-less runs increment RunCount but not this. + // Runs with no recorded duration (legacy logs, or sub-millisecond StartOnly + // launches that round to 0) increment RunCount but not this. StartOnly runs + // otherwise contribute their launch latency. TimedRunCount int } diff --git a/src/runner/runner.go b/src/runner/runner.go index 5ef8721..13adb36 100644 --- a/src/runner/runner.go +++ b/src/runner/runner.go @@ -30,9 +30,9 @@ func RunJob(ctx context.Context, job *domain.Job, trigger string, logsDir string var durationMS int64 if job.StartOnly { invocation := jobInvocation(ctx, *job) - state, detail, output = startJobOnly(invocation, *job, started) - // StartOnly jobs don't wait for process exit, so no meaningful duration. - durationMS = 0 + // StartOnly jobs don't wait for process exit, so the duration measures + // launch latency (time to spawn the process) rather than run time. + state, detail, output, durationMS = startJobOnly(invocation, *job, started) } else { var stdoutBuf strings.Builder var stderrBuf strings.Builder @@ -72,21 +72,22 @@ func RunJob(ctx context.Context, job *domain.Job, trigger string, logsDir string }, logErr } -func startJobOnly(invocation commandInvocation, job domain.Job, started time.Time) (string, string, string) { +func startJobOnly(invocation commandInvocation, job domain.Job, started time.Time) (string, string, string, int64) { command := invocation.command if invocation.hideWindow { winproc.ConfigureHiddenWindow(command) } err := command.Start() duration := time.Since(started).Round(time.Millisecond) + durationMS := duration.Milliseconds() if err != nil { - return "Failed", fmt.Sprintf("%T: %v", err, err), startOnlyOutput(job, 0) + return "Failed", fmt.Sprintf("%T: %v", err, err), startOnlyOutput(job, 0), durationMS } pid := command.Process.Pid if releaseErr := command.Process.Release(); releaseErr != nil { - return "Failed", fmt.Sprintf("process started with pid %d, but release failed: %T: %v", pid, releaseErr, releaseErr), startOnlyOutput(job, pid) + return "Failed", fmt.Sprintf("process started with pid %d, but release failed: %T: %v", pid, releaseErr, releaseErr), startOnlyOutput(job, pid), durationMS } - return "OK", fmt.Sprintf("Started in %s (pid %d); not waiting for process exit", duration, pid), startOnlyOutput(job, pid) + return "OK", fmt.Sprintf("Started in %s (pid %d); not waiting for process exit", duration, pid), startOnlyOutput(job, pid), durationMS } func startOnlyOutput(job domain.Job, pid int) string {