fix(terminal): stop parent color stack growing unbounded

ParentColors accumulated every segment ever rendered across the whole
process lifetime, never reset - harmless for a one-shot render but
quadratic in the pwsh serve daemon, where StreamPrimary keeps calling
Primary() in the same long-lived process. Each render's SetParentColors
also copied the full history on every call, so cost compounded further.

Push onto a real stack instead of prepend-copying, and clear it once
per block in String() where the rest of the per-block render state
already resets. resolveParentColor's walk order flips to match: nearest
ancestor is now the tail, not the head.

BenchmarkEnginePrimary at 64000 iterations: 670µs/op -> 16µs/op, flat
instead of growing with iteration count; SetParentColors accounted for
99% of all allocated bytes in a mem profile before this change.

Entire-Checkpoint: 7b56670e64a2
This commit is contained in:
Jan De Dobbeleer
2026-07-23 13:03:14 +02:00
parent 2f30a87852
commit cdb4bfe897
2 changed files with 15 additions and 7 deletions
+4 -1
View File
@@ -26,7 +26,10 @@ func (color Ansi) isKeyword() bool {
func (color Ansi) Resolve(current *Set, parents []*Set) Ansi {
resolveParentColor := func(keyword Ansi) Ansi {
for _, parentColor := range parents {
// parents is a stack pushed tail-first (see terminal.SetParentColors):
// the nearest ancestor is the last element, so walk back-to-front.
for i := len(parents) - 1; i >= 0; i-- {
parentColor := parents[i]
if parentColor == nil {
return Transparent
}
+11 -6
View File
@@ -218,15 +218,15 @@ func SetColors(background, foreground color.Ansi) {
}
}
// SetParentColors pushes the completed segment's colors onto the parent
// stack; the most recent entry (nearest ancestor) sits at the tail. Cleared
// per block by String() - see resolveParentColor in color/keywords.go for
// the matching tail-to-head walk.
func SetParentColors(background, foreground color.Ansi) {
if ParentColors == nil {
ParentColors = make([]*color.Set, 0)
}
ParentColors = append([]*color.Set{{
ParentColors = append(ParentColors, &color.Set{
Background: background,
Foreground: foreground,
}}, ParentColors...)
})
}
func ChangeLine(numberOfLines int) string {
@@ -602,6 +602,11 @@ func String() (string, int) {
bgGradientCells, fgGradientCells = nil, nil
cellIndex = 0
// the parent stack is scoped to one block; each new block starts a
// fresh ancestor chain. Slicing to zero keeps the backing array so
// same-size blocks (the common case) push without reallocating.
ParentColors = ParentColors[:0]
}()
return builder.String(), length