perf(render): memoize rendered pages, filter PNGs, split request timing - #158
Open
akumrazor wants to merge 2 commits into
Open
perf(render): memoize rendered pages, filter PNGs, split request timing#158akumrazor wants to merge 2 commits into
akumrazor wants to merge 2 commits into
Conversation
The proxy re-rendered identical PNGs on every turn. history.ts freezes old chunks so they stay byte-identical for the upstream prompt cache, which means a long session pays full render cost for pages that provably did not change: measured on a real session, 134 images cost ~10s of single-threaded CPU per request while the payload hash held constant across consecutive turns. Three changes, each measured: * Rendered-page cache (render.ts). Rendering is a pure function of its inputs -- atlases decode once at module init and never mutate -- so identical inputs return identical pages. Bounded LRU over retained bytes (PNG payloads plus the key strings, which embed the source text and are the larger half), 64 MiB default, PXPIPE_RENDER_CACHE_BYTES to tune, 0 to disable. The key embeds the source text verbatim rather than a digest: a digest collision would serve the WRONG image, a silent correctness bug far worse than a miss. 134 images: 14.4s -> 1.0s. * PNG Average filter (png.ts). Residuals of 5x8 bitmap glyphs on a flat background collapse to mostly zeros, which deflate encodes in both less space and less time. Lossless -- verified against skia, an independent decoder, rather than a hand-rolled one that could share a bug with the encoder. 34% smaller IDAT at roughly half the compression cost; the images the model sees are bit-identical. * Request timing split (proxy.ts, node.ts, tracker.ts). durationMs alone could not distinguish our own CPU from a slow provider. tx is the local transform, fb is time to upstream response headers, so fb - tx isolates upload plus provider processing and duration - fb is generation. Persisted as transform_ms in the JSONL so it is analyzable across requests. vitest.config.ts: testTimeout 30s -> 60s. The append-only cache-stability case alone runs ~30s, so it timed out purely from CPU contention whenever another file joined the parallel pool -- a failure that says nothing about the code under test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The render cache added a public env var without listing it alongside the others, so the only way to discover it was reading render.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The proxy re-renders identical PNGs on every turn.
history.tsfreezes old chunks so they stay byte-identical for the upstream prompt cache, which means a long session pays full render cost for pages that provably did not change. Measured on a real session: 134 images cost ~10s of single-threaded CPU per request while the payload hash held constant across consecutive turns.Three changes, each measured.
1. Rendered-page cache (
render.ts)Rendering is a pure function of its inputs — atlases decode once at module init and never mutate — so identical inputs can return identical pages. Bounded LRU over retained bytes (PNG payloads plus the key strings, which embed the source text and are the larger half). 64 MiB default,
PXPIPE_RENDER_CACHE_BYTESto tune,0to disable.The key embeds the source text verbatim rather than a digest: a digest collision would serve the wrong image, a silent correctness bug far worse than a miss.
slotTextis length-prefixed so("a b", "c")and("a", "b c")cannot collide, andundefinedstays distinct from''because the renderer branches on it.134 images: 14.4s → 1.0s.
2. PNG Average filter (
png.ts)Residuals of 5×8 bitmap glyphs on a flat background collapse to mostly zeros, which deflate encodes in both less space and less time.
34% smaller IDAT at roughly half the compression cost. Lossless — verified against skia (
@napi-rs/canvas), an independent decoder, rather than a hand-rolled one that could share a bug with the encoder. The images the model sees are bit-identical; only the bytes on the wire shrink. Filtering is pure JS ahead of the compressor, so it stays portable to Workers unlike a deflate-level change.3. Request timing split (
proxy.ts,node.ts,tracker.ts)durationMsalone could not distinguish our own CPU from a slow provider.txis the local transform,fbis time to upstream response headers, sofb - txisolates upload plus provider processing andduration - fbis generation:Persisted as
transform_msin the JSONL so it is analyzable across many requests, not just readable line by line. The timer deliberately closes before the Googlecount_tokensprobe, so network latency is never charged to local CPU.vitest.config.ts: testTimeout 30s → 60sThe append-only cache-stability case alone runs ~30s against a 30s limit, so it timed out purely from CPU contention whenever another file joined the parallel pool — a failure that says nothing about the code under test. The existing comment already documented this class of problem when it went 5s → 30s.
Verification
tsc --noEmitclean. 933 tests pass, including 11 new cache tests (byte identity, every input that must miss, the slot/text ambiguity, style key ordering,droppedCodepointsisolation, LRU eviction, exact byte accounting, kill switch) and 3 lossless-PNG tests.The 3 failures in
tests/node-security.test.tsare pre-existing and Windows-only — POSIX permission assertions (0o700) that Windows does not apply. Confirmed against a clean checkout of this base; they should pass on Linux CI.🤖 Generated with Claude Code