Skip to content

Cache bin-color resolution per trace, not per request#235

Merged
masenf merged 5 commits into
mainfrom
claude/fastapi-drilldown-perf-jb3mh6
Jul 24, 2026
Merged

Cache bin-color resolution per trace, not per request#235
masenf merged 5 commits into
mainfrom
claude/fastapi-drilldown-perf-jb3mh6

Conversation

@masenf

@masenf masenf commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Resolves a critical performance regression where density_view quantized the entire color column into kernel LUT indices on every reply — an O(N) NumPy pass with multi-GB temporaries that cost the 100M-point FastAPI drilldown demo 1.3–7 s per request, ~10–100× the actual tier work.

The color resolution is now cached on the trace (interaction.trace_bin_colors) and materialized only by branches that feed bin_2d_mean_color. Pyramid replies compose prebuilt color planes and point drills ship sliced channels, so they never trigger the expensive full-column pass. The cache is invalidated on append and lazily re-resolved by the append's own refresh emit.

Key Changes

  • interaction.py: Added trace_bin_colors() function that resolves and caches the full-column bin-color source once per trace. Added bin_color_cache_bytes() for memory reporting. Modified _ensure_pyramid() and density_view() to use the cached resolution only when needed (exact re-bins and first-paint emit), not for pyramid or drill replies.

  • _trace.py: Added _bin_colors field to cache the resolved color source (None = never resolved, 0 = not applicable, otherwise the resolve_bin_colors kwargs dict). Documented as a rebuildable §27 derived cache.

  • _payload.py: Updated _density_trace_spec() to use interaction.trace_bin_colors() instead of resolving inline, sharing the O(N) quantize with pyramid build and later grid replies.

  • _figure.py: Updated memory_report() to include bin_color_bytes (itemized cache size) and include it in resident_array_bytes total.

  • interaction.py append path: Added cache invalidation (t._bin_colors = None) in the append handler so the next grid reply re-resolves over the post-append column.

  • Tests: Added comprehensive test suite in test_density_mean_color.py covering:

    • test_density_view_exact_band_resolves_colors_once: Verifies exact re-bins resolve once and reuse cached result
    • test_density_view_points_band_never_resolves_colors: Confirms drills never trigger resolution
    • test_pyramid_band_reuses_build_time_resolution: Pyramid replies compose prebuilt planes
    • test_append_re_resolves_bin_colors_exactly_once: Append invalidates and re-resolves exactly once
    • test_memory_report_itemizes_bin_color_cache_bytes: Memory accounting
  • Benchmarks: Added colored_drilldown_figure fixture and test_adaptive_drilldown_cycle_mean_color regression tripwire that measures the drilldown cycle on a channel-bearing trace (the FastAPI demo shape). Ensures per-request cost stays free of full-column channel work.

  • Documentation: Updated LOD architecture spec (§2) with cost note and caching contract. Updated benchmark methodology and results to document the regression guard and measured improvements (0.02–0.45 s per reply on the demo host, 223 ms → 99 ms for the 2.1M benchmark cycle).

Implementation Details

The resolution is a pure function of immutable channel values and their global domain, making it safe to cache for the trace lifetime. The cache is stored as either:

  • None: never resolved
  • 0: resolved but not applicable (no channel or constant values)
  • dict: the resolve_bin_colors kwargs (idx/rgba planes + LUT)

Only the cheap channels.bins_mean_color() fact is decided up front in density_view; the expensive full-column source is materialized solely by branches that feed bin_2d_mean_color (exact re-bins and first-paint emit). This prevents charging pyramid and drill replies for work they never consume.

https://claude.ai/code/session_01NP6yfYGvvkUHu26x8Q2GWb

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown

Greptile Summary

This PR caches bin-color resolution on each trace and avoids redoing full-column color work during density replies. The main changes are:

  • Adds trace_bin_colors() and _bin_colors to cache mean-color kernel inputs per trace.
  • Uses cached color resolution only for paths that build mean-color grids.
  • Invalidates the color cache on append and reports its memory use in memory_report().
  • Bounds quantization and mean-color accumulator temporary memory.
  • Adds benchmark and test coverage for colored drilldown, append refresh, no-rescan behavior, chunked quantization, and memory accounting.
  • Updates LOD and benchmark docs to describe the caching contract.

Confidence Score: 5/5

Safe to merge with low risk.

The changed paths are focused on derived color-cache materialization, memory accounting, and bounded temporary allocations. Tests cover the main behavior changes: exact grids, drills, pyramid reuse, append invalidation, no-rescan retention, chunk equivalence, and memory reports. No verified functional or security issues were found.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • The density regression proof was executed and its log was generated, capturing the command, working directory, pytest output, warnings, a summary, and EXIT_CODE: 0.
  • The benchmark tripwire proof was executed and its log was generated, capturing the command, working directory, benchmark timing table, pytest summary, and EXIT_CODE: 0.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
python/xy/interaction.py Caches full-column bin-color resolution per trace, gates materialization to mean-color grid consumers, and invalidates on append.
python/xy/channels.py Adds chunk-bounded quantization helpers and wires them through bin-color resolution.
python/xy/_payload.py Reuses trace-level bin-color resolution when building density trace specs.
python/xy/_figure.py Includes cached bin-color buffers in memory reporting and resident byte totals.
src/kernels.rs Adds memory-budget-based worker shedding for mean-color accumulation and coverage for thread selection.
tests/test_density_mean_color.py Adds tests for once-per-trace resolution, append invalidation, no-rescan retention, chunk equivalence, and memory accounting.
benchmarks/test_codspeed_kernels.py Adds a colored drilldown benchmark path covering cached color resolution.
spec/design/lod-architecture.md Documents per-trace bin-color caching, no-rescan retention behavior, and chunked transient bounds.

Reviews (7): Last reviewed commit: "Name the aggregate floor on the drilldow..." | Re-trigger Greptile

@codspeed-hq

codspeed-hq Bot commented Jul 23, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 102 untouched benchmarks
🆕 1 new benchmark
⏩ 1 skipped benchmark1

Performance Changes

Benchmark BASE HEAD Efficiency
🆕 test_adaptive_drilldown_cycle_mean_color N/A 314.8 ms N/A

Comparing claude/fastapi-drilldown-perf-jb3mh6 (0234748) with main (960e132)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

Base automatically changed from claude/point-caching-sampling-av37m7 to main July 24, 2026 02:15
@masenf
masenf force-pushed the claude/fastapi-drilldown-perf-jb3mh6 branch from 19b42aa to 64f6b75 Compare July 24, 2026 02:29
masenf added 5 commits July 23, 2026 21:23
… §2)

density_view resolved the kernel's mean-color source — the full-column
LUT-index/RGBA quantize — at the top of every request, before the tier
decision, even though pyramid replies compose prebuilt color planes and
point drills ship sliced channels; only the two bin_2d_mean_color branches
ever consume it. On the 100M-point FastAPI drilldown demo that O(N) NumPy
pass (multi-GB temporaries) cost 1.3-7 s per reply, 10-100x the actual
tier work (pyramid compose 3-34 ms, drill scans 100-230 ms, mid-band
re-bin ~360 ms on a 4-core host).

The resolution is a pure function of the immutable channel values and
their global domain, so cache it on the trace (interaction.trace_bin_colors;
None/0/dict, the _pyr_handle idiom) and share it across every consumer:
pyramid build, first-paint emit, exact and no-rescan re-bins. density_view
now decides only the cheap bins_mean_color fact up front and materializes
the source solely in the branches that feed bin_2d_mean_color. Appends drop
the cache beside the other pre-append derived state and the append's own
refresh emit re-resolves it, once, over the post-append column.

The cache is a rebuildable derived buffer (dossier §27), itemized as
memory_report()['bin_color_bytes'] and counted in resident_array_bytes.
Measured on the demo: every request now completes in 0.02-0.45 s with
byte-identical replies (pyramid band 0-38 ms), and figure warmup no longer
pays the resolve twice.
…lain one

test_adaptive_drilldown_cycle asserted the deep reply's visible count
against the raw 0..10 view window, but a drill ships the widest aligned
window that still fits the budget (T13), so visible counts the padded
window and the row has been failing since padded drills landed (CodSpeed
runs on pull requests only, and this branch has none yet). Derive the
expectation from a warm reply's own x_range/y_range instead — exact
without hardcoding the pad ladder.

Add test_adaptive_drilldown_cycle_mean_color, the channel-bearing twin the
suite was missing: the drilldown cycle on a continuous-color trace (the
FastAPI demo shape) with a mid-band exact re-bin leg, asserting the
mean-color plane rides the pyramid and exact replies and that the drill
restores per-point channels. Every reply must reuse the trace-cached
bin-color resolution; re-resolving the column per request multiplies the
row by the column length (2.25x wall clock at the benchmark's 2.1M rows,
1.3-7 s per reply on the 100M demo), which is exactly the regression the
previous commit removed — with no colored row, CodSpeed could not see it.

Row counts in the methodology doc move 73->74 and 102->103; the category
registry and results tracking list name the new row.
…-out

The mean-color feature made a colored huge scatter's ONE-TIME costs scale
peak RSS with N-sized temporaries, pushing a 1e9-point colored build past
what count-only main needed (main: a stretch; this stack: OOM). Three
bounds restore the envelope without changing a single shipped byte:

- resolve_bin_colors quantizes full columns in _QUANTIZE_CHUNK slices into
  a preallocated u8 result. The per-element math is the identical historical
  chain (normalize f32 -> f64 widen -> x255 -> rint -> u8; bitwise-equal,
  pinned by test), but the one-shot pipeline's several full-length f64
  temporaries — ~20 GB at 1e9 rows, 4 GB at the 205M repro — become
  ~110 MB of chunk scratch. direct_rgba and wide-code folds chunk the same
  way.

- bin_2d_mean_color_cells sheds workers whenever their private 40 B/cell
  accumulators would together exceed MEAN_COLOR_ACCUM_BUDGET_BYTES (1 GiB):
  screen grids and the 2048-square default keep the full 4-way fan-out, a
  no-rescan trace's adaptive 8192-square base level builds serial — one
  2.7 GB accumulator instead of four plus a merge target. The colored
  build's transient now sits at or below the count-only build's own
  fan-out.

- trace_bin_colors resolves but does not retain for no-rescan traces
  (memmapped or past PYRAMID_NO_RESCAN_ROWS): after the pyramid exists,
  every interactive reply composes its prebuilt color planes, so a
  retained per-row idx (1 GB at 1e9 rows) had no remaining consumer. The
  rare correctness-net re-bin re-resolves chunk-bounded. Borrowed
  categorical code arrays also no longer double-count against
  channel_bytes in the memory report.

Measured: the 205M colored first view adds 0.96 GB of peak RSS over the
canonical columns instead of 3.96 GB and builds ~35% faster; a 600M
memmapped colored drilldown now completes inside a 15 GB host (build stage
+1.34 GB anonymous, interactive replies +0.00 GB, correct
pyramid-L0-upsampled deep zoom); the 100M demo's warmup halves to 6.5 s
with steady-state latencies unchanged. LOD doc §2/§4.4 record the bounds
as regressions-to-avoid.
The accumulator-budget commit inserted the budget const and the fan-out
helper between the function's doc comment and its
#[allow(clippy::too_many_arguments)], leaving the attribute (and the doc)
attached to the const and the 9-argument function bare — clippy -D
warnings failed. Restore the doc + allow onto the function and fold the
budget note into its fan-out paragraph.
… gate

Past PYRAMID_NO_RESCAN_ROWS (200M rows, or any disk-backed column) the
engine serves every zoom from the pyramid — upsampled at its floor — and
never ships exact points: the O(N) window rescan a drill needs is
forbidden in that regime (LOD doc §28), however deep the window. Run the
demo with XY_LIVE_POINTS above the bound and that contract reads as a
drilldown that mysteriously refuses to resolve; the reply has recorded the
decision all along (binning: pyramid-L0-upsampled), the page just never
showed it.

The badge now appends 'aggregate floor' (with a tooltip naming the
regime) whenever the reply's binning says the aggregate is served past
its resolution, and the README documents the scale gate next to the
XY_LIVE_POINTS knob it rides on.
@masenf
masenf force-pushed the claude/fastapi-drilldown-perf-jb3mh6 branch from 64f6b75 to 0234748 Compare July 24, 2026 04:23
@masenf
masenf merged commit aa464a8 into main Jul 24, 2026
72 checks passed
@masenf
masenf deleted the claude/fastapi-drilldown-perf-jb3mh6 branch July 24, 2026 18:13
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.

1 participant