kpi

package
v0.0.27 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package kpi is the stdlib-only KPI-capture support for the offline performance scenario harness (perf-tracking.md Phase 2). It owns the per-scenario metric shape (ScenarioResult), the allocation/RSS/wall-clock capture bracket (Capture), the linux /proc-based RSS sampler, and a settle-then-count goroutine probe.

LAYERING: this package imports ONLY the standard library. It must NOT import engine/... or internal/... — the engine-shaped KPIs (tokens, cache-hit-rate) are passed IN by the scenario caller (perf/scenarios), which is the only side that knows about session.Usage. That keeps kpi a leaf the engine never depends on and a scenario can freely consume.

Index

Constants

View Source
const SchemaVersion = 1

SchemaVersion is the on-disk JSON schema version for a ScenarioResult. Bump it when the field set changes shape so a trend-store ingester can branch.

Variables

This section is empty.

Functions

func GoroutineDelta

func GoroutineDelta(baseline int, settle time.Duration) int

GoroutineDelta returns the leaked-goroutine count: the settled end count minus a baseline the caller captured (also via GoroutinesAfterSettle) BEFORE the measured region, clamped at 0. So 0 = no leak; a positive value is the number of goroutines the scenario started and did not join. Clamping at 0 keeps a transient that the baseline happened to catch (a sampler/GC goroutine that has since exited) from reporting a spurious negative. This is the meaningful KPI for the delegation/team/background leak class — the raw process-wide count carries the test runner's own goroutines as noise.

func GoroutinesAfterSettle

func GoroutinesAfterSettle(d time.Duration) int

GoroutinesAfterSettle returns the live goroutine count after a settle delay, so transient run-teardown goroutines (a draining emitter, a parked sampler, the GC) are not miscounted as a leak. It is the tracked-number promotion of goleak's boolean pass/fail (perf-tracking.md Phase 2): the end-of-run goroutine count is a gate-hard KPI for the delegation/team/background leak class.

It sleeps d (clamped to a small floor), runs a GC to release goroutines parked on finalizers/timers, then reads runtime.NumGoroutine. The reading includes the caller's own goroutine and the test runner's, so the harness compares it against a same-process baseline rather than treating it as an absolute — see GoroutineDelta, which is what scenarios actually store.

func WriteJSON

func WriteJSON(path string, results []ScenarioResult) error

WriteJSON marshals the accumulated scenario results to path as a pretty JSON array. The caller decides the path (typically $MECATL_PERF_JSON); an empty slice still writes a valid (empty) array so a downstream ingester never chokes on a missing file vs an empty one.

Types

type Capture

type Capture struct {
	// contains filtered or unexported fields
}

Capture brackets a measured region and reports its allocation, RSS, and wall-clock deltas. Use it as:

c := kpi.NewCapture()
c.Begin()
... run the scenario ...
m := c.End()

Begin runs a GC and snapshots the allocation counters so the captured allocs exclude prior setup garbage; End runs a final GC and reads the deltas. The RSS sampler runs between Begin and End. The numbers are process-wide (Go's MemStats and /proc are per-process), so a Capture must wrap a single scenario run with no concurrent benchmark in the same process — the harness runs scenarios sequentially, which holds.

func NewCapture

func NewCapture() *Capture

NewCapture returns a ready-to-Begin Capture.

func (*Capture) Begin

func (c *Capture) Begin()

Begin snapshots the allocation baseline (after a GC so prior garbage is not counted), starts the RSS sampler, and starts the wall clock.

func (*Capture) End

func (c *Capture) End() Metrics

End stops the wall clock and the RSS sampler, runs a final GC, and returns the allocation/RSS/wall-clock deltas since Begin.

type Metrics

type Metrics struct {
	// Allocs is the number of heap allocations during the measured region.
	Allocs uint64
	// Bytes is the cumulative bytes allocated during the measured region.
	Bytes uint64
	// RSSPeak is the highest sampled resident set size, in bytes (0 off-linux).
	RSSPeak uint64
	// RSSFinal is the resident set size at End, in bytes (0 off-linux).
	RSSFinal uint64
	// WallNs is the elapsed wall-clock time of the measured region, in ns.
	WallNs int64
}

Metrics is the result of one Capture bracket.

type RSSSampler

type RSSSampler struct {
	// contains filtered or unexported fields
}

RSSSampler polls the process resident-set size on a fixed interval and tracks the peak and the most-recent (final) sample. It is platform-pluggable through readRSS (a direct /proc/self/status read on linux, 0 elsewhere — see rss_linux.go / rss_other.go), so the harness takes no new dependency: the "Open decisions" lean in perf-tracking.md is the direct /proc read.

Start launches a sampling goroutine; Stop signals it, waits for it to drain, takes one final sample, and returns (peak, final). A sampler is single-use.

func NewRSSSampler

func NewRSSSampler(interval time.Duration) *RSSSampler

NewRSSSampler returns a sampler that polls every interval. An interval <= 0 is clamped to 10ms.

func (*RSSSampler) Start

func (s *RSSSampler) Start()

Start begins sampling in the background. It records an immediate first sample so a region shorter than one interval still yields a peak.

func (*RSSSampler) Stop

func (s *RSSSampler) Stop() (peak, final uint64)

Stop ends sampling, takes a final reading, and returns (peak, final) in bytes. On a non-linux build both are 0.

type ScenarioResult

type ScenarioResult struct {
	SchemaVersion int    `json:"schema_version"`
	Name          string `json:"name"`
	// Sample is the per-scenario-name ordinal (0,1,2…) of this row within one
	// process run. Under `go test -count=N` a scenario emits N rows with identical
	// Name+GitSHA; Sample is the only thing that distinguishes them, letting Phase 3
	// group by (Name, GitSHA) and aggregate the N samples.
	Sample      int    `json:"sample"`
	GitSHA      string `json:"git_sha"`
	Timestamp   string `json:"timestamp"`
	Iterations  int    `json:"iterations"`
	AllocsPerOp uint64 `json:"allocs_per_op"`
	BytesPerOp  uint64 `json:"bytes_per_op"`
	// GoroutinesEnd is a DELTA: live goroutines at end-of-run MINUS a baseline
	// captured before the measured region, clamped at 0. So 0 = the scenario leaked
	// no goroutines; a positive number is the leak count (the team-fanout /
	// background-subagents leak class). It is NOT the raw process-wide
	// runtime.NumGoroutine (which would carry the test runner's own goroutines as
	// noise). See kpi.GoroutineDelta.
	GoroutinesEnd   int     `json:"goroutines_end"`
	TokensInput     int64   `json:"tokens_input"`
	TokensOutput    int64   `json:"tokens_output"`
	TokensCacheRead int64   `json:"tokens_cache_read"`
	CacheHitRate    float64 `json:"cache_hit_rate"`
	RSSPeakBytes    uint64  `json:"rss_peak_bytes"`
	RSSFinalBytes   uint64  `json:"rss_final_bytes"`
	WallClockNs     int64   `json:"wall_clock_ns"`
}

ScenarioResult is one offline scenario's captured KPIs, serialised to the trend-store JSON. The deterministic, low-noise fields (allocs/op, bytes/op, goroutines, tokens, cache-hit-rate) are the gate-hard signals; the RSS and wall-clock fields are advisory (machine-dependent). Token fields are zero for scenarios with no model usage (e.g. the TUI scrollback render bench).

Field normalization — the contract a Phase 3 ingester reads (group rows by (Name, GitSHA), then aggregate the same-named samples):

  • PER-OP (divided by the benchmark's b.N): AllocsPerOp, BytesPerOp. These are the amortised cost of ONE scenario iteration and are the deterministic, gate-hard signals.
  • PER-RUN (the LAST iteration's cumulative figure, NOT divided): TokensInput, TokensOutput, TokensCacheRead, CacheHitRate. They are read off the final session's (or team outcome's) accumulated session.Usage — a single iteration's whole-run total, which is constant across iterations because the script is fixed. CacheHitRate == 0 is honest for scenarios with no cache-read in their script (e.g. compaction_cycle) — see perf-tracking.md.
  • WHOLE-BRACKET (captured ONCE across all b.N iterations, NOT divided): RSSPeakBytes, RSSFinalBytes, WallClockNs. They describe the whole measured region, so they scale with b.N and are advisory (machine-dependent); a trend gate should compare them ratio-wise on the same runner, not absolutely.
  • DELTA: GoroutinesEnd is end-minus-baseline (see its field comment) — 0 means no goroutine leak, clamped at 0.
  • DISCRIMINATOR: Sample is the per-(Name) ordinal within ONE process run (0,1,2…), so -count=N emits N groupable rows per scenario rather than N indistinguishable ones.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL