Skip to content

⚡ Bolt: Stop rebuilding the HTML-escape replacers on every ASCII screen capture - #114

Merged
jnnngs merged 1 commit into
mainfrom
claude/funny-mccarthy-2wyn4m
Sep 27, 2026
Merged

jnnngs merged 1 commit into
mainfrom
claude/funny-mccarthy-2wyn4m

Conversation

@jnnngs

@jnnngs jnnngs commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

💡 What

escapeAttr and escapeText in connect3270/emulator.go each built a brand new *strings.Replacer on every single call, even though their replacement tables (&→&amp;, <→&lt;, >→&gt;, and the attribute variant adding "→&quot;) are constant. Both replacers are now hoisted to package-level vars (htmlAttrEscaper, htmlTextEscaper) built once at init.

🎯 Why

  • escapeText runs once per non-API AsciiScreenGrab call, over the entire captured screen's text — and AsciiScreenGrab is a workflow step that runs once per step, per worker, in a concurrent load test (-concurrent N).
  • escapeAttr runs up to a handful of times per capture too (once per non-empty data-port / data-host / data-type attribute via captureAttrs).
  • *strings.Replacer is documented as safe for concurrent use, so there's no correctness reason to rebuild it per call — it was just wasted allocation on a table that never changes, repeated on a per-workflow-step hot path.

📊 Impact

Confirmed with a synthetic benchmark (~2000-byte screen, representative of an 80×24 capture):

old (per-call NewReplacer) new (hoisted)
allocs/op 9 2
B/op ~14952 ~8192 (~45% less)
ns/op ~23165 ~20032 (~13% faster)

No algorithmic complexity change — this is a pure allocation/GC-pressure reduction on a per-call basis, which compounds under -concurrent load testing where many workers hit AsciiScreenGrab repeatedly.

🔬 Measurement

// connect3270/zzbench_test.go (not committed — for local verification only)
func BenchmarkEscapeTextOld(b *testing.B) {
	s := strings.Repeat("data <foo> & <bar> ", 100)
	for i := 0; i < b.N; i++ {
		replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
		_ = replacer.Replace(s)
	}
}

func BenchmarkEscapeTextNew(b *testing.B) {
	s := strings.Repeat("data <foo> & <bar> ", 100)
	for i := 0; i < b.N; i++ {
		_ = escapeText(s)
	}
}

Run with go test -bench=BenchmarkEscapeText -benchmem -run=^$ ./connect3270/.

Verified locally:

  • go vet ./... — clean
  • go test -short ./... — all packages pass
  • go test -run TestCompat -v ./connect3270/ — all 17 compat tests pass (embedded emulator was available in this environment)

No behavior change: output of escapeAttr/escapeText is byte-for-byte identical to before.


🤖 Generated with Claude Code

https://claude.ai/code/session_015CPbfGS3iGtSU6YU2ZLtgP


Generated by Claude Code

escapeAttr and escapeText each built a fresh *strings.Replacer on
every call, even though their replacement tables never change.
escapeAttr runs up to a handful of times per AsciiScreenGrab call
(once per non-empty attribute), and escapeText runs once per capture
over the whole screen's text — both in the concurrent load-test hot
path (per step, per worker). *strings.Replacer is safe for concurrent
use, so the tables are hoisted to package-level vars instead.

Benchmarked with a synthetic ~2000-byte screen: allocations drop from
9 to 2 per call and bytes/op from ~14952 to ~8192 (~45% less), with a
~13% wall-clock improvement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CPbfGS3iGtSU6YU2ZLtgP
@jnnngs
jnnngs marked this pull request as ready for review September 27, 2026 07:40
@jnnngs
jnnngs merged commit 1e4e7aa into main Sep 27, 2026
3 checks passed
@jnnngs
jnnngs deleted the claude/funny-mccarthy-2wyn4m branch September 27, 2026 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants