Concrete perf metrics: live counters in --profile + benchmark suite
Live metrics (--profile): - New metricsTracker instruments OnPTYOut, viewport renderer, stdout writes, libghostty-vt Write/Title CGO calls, sidebar / tabbar / status draws (with cache-hit accounting), snapshot replays, and the chrome ticker (so we can see ticker fires that did nothing). - Writes metrics.jsonl (one snapshot per second) and metrics.json + summary.txt on exit, alongside the existing pprof files. - All record* methods are nil-safe so disabled paths pay only a cheap nil check; counters are atomic so the per-PTY-chunk hot path stays lock-free. Benchmark suite (go test -bench=.): - Three workload fixtures — plain ASCII, SGR-styled lines, and a ratatui-style cursor-shuffling burst — plus a containsOSC microbenchmark. Reports ns/op, MB/s, allocs/op, B/op. - Initial baseline numbers added to TODO under the perf-audit section, alongside two new findings (renderer allocs ~1 per 4 bytes on styled chunks; styled throughput tops out near 90 MB/s) those benchmarks surfaced.
This commit is contained in:
@@ -50,6 +50,11 @@ type Session struct {
|
||||
// JSON file so they can be re-spawned after patterm restarts.
|
||||
// Optional; nil means "no persistence" (used by unit tests).
|
||||
persistStore *persist.Store
|
||||
|
||||
// metrics is the optional performance tracker. nil when --profile
|
||||
// is off. The pump goroutine reads it via atomic Load so installing
|
||||
// metrics post-construction doesn't race with running children.
|
||||
metrics atomic.Pointer[metricsTracker]
|
||||
}
|
||||
|
||||
// SetPersistStore attaches a process-persistence store. Future Spawn /
|
||||
@@ -61,6 +66,18 @@ func (s *Session) SetPersistStore(p *persist.Store) {
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetMetrics installs the per-session performance tracker. Safe to
|
||||
// call with nil to disable (the default). Reads on the hot path go
|
||||
// through atomic.Pointer.Load() with no lock; SetMetrics swaps the
|
||||
// pointer once at startup.
|
||||
func (s *Session) SetMetrics(m *metricsTracker) {
|
||||
s.metrics.Store(m)
|
||||
}
|
||||
|
||||
func (s *Session) loadMetrics() *metricsTracker {
|
||||
return s.metrics.Load()
|
||||
}
|
||||
|
||||
// ChildEventListener is implemented by the TUI to react to lifecycle
|
||||
// events without polling.
|
||||
type ChildEventListener interface {
|
||||
@@ -392,9 +409,17 @@ func (s *Session) pumpChild(c *Child, runID uint64) {
|
||||
}
|
||||
chunk := buf[:n]
|
||||
if em := c.Emulator(); em != nil {
|
||||
m := s.loadMetrics()
|
||||
wstart := time.Time{}
|
||||
if m != nil {
|
||||
wstart = time.Now()
|
||||
}
|
||||
if _, werr := em.Write(chunk); werr != nil {
|
||||
logf("emulator.Write(child %s): %v", c.ID, werr)
|
||||
}
|
||||
if m != nil {
|
||||
m.recordEmuWrite(time.Since(wstart))
|
||||
}
|
||||
// OSC 0/2 title updates ride on the same byte stream as
|
||||
// the rest of the output. Polling the emulator after each
|
||||
// chunk is cheap on its own (one CGO call) but codex/
|
||||
@@ -403,9 +428,18 @@ func (s *Session) pumpChild(c *Child, runID uint64) {
|
||||
// the chunk doesn't carry an OSC start byte at all; the
|
||||
// title can only change on chunks that include one.
|
||||
if containsOSC(chunk) {
|
||||
tstart := time.Time{}
|
||||
if m != nil {
|
||||
tstart = time.Now()
|
||||
}
|
||||
if t, terr := em.Title(); terr == nil {
|
||||
c.recordTitle(t)
|
||||
}
|
||||
if m != nil {
|
||||
m.recordEmuTitle(time.Since(tstart), false)
|
||||
}
|
||||
} else if m != nil {
|
||||
m.recordEmuTitle(0, true)
|
||||
}
|
||||
}
|
||||
c.recordWrite(chunk)
|
||||
|
||||
Reference in New Issue
Block a user