Cache bin-color resolution per trace, not per request#235
Conversation
Greptile SummaryThis PR caches bin-color resolution on each trace and avoids redoing full-column color work during density replies. The main changes are:
Confidence Score: 5/5Safe 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.
What T-Rex did
Important Files Changed
Reviews (7): Last reviewed commit: "Name the aggregate floor on the drilldow..." | Re-trigger Greptile |
Merging this PR will not alter performance
Performance Changes
Comparing Footnotes
|
19b42aa to
64f6b75
Compare
… §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.
64f6b75 to
0234748
Compare
Summary
Resolves a critical performance regression where
density_viewquantized 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 feedbin_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: Addedtrace_bin_colors()function that resolves and caches the full-column bin-color source once per trace. Addedbin_color_cache_bytes()for memory reporting. Modified_ensure_pyramid()anddensity_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_colorsfield 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 useinteraction.trace_bin_colors()instead of resolving inline, sharing the O(N) quantize with pyramid build and later grid replies._figure.py: Updatedmemory_report()to includebin_color_bytes(itemized cache size) and include it inresident_array_bytestotal.interaction.pyappend 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.pycovering:test_density_view_exact_band_resolves_colors_once: Verifies exact re-bins resolve once and reuse cached resulttest_density_view_points_band_never_resolves_colors: Confirms drills never trigger resolutiontest_pyramid_band_reuses_build_time_resolution: Pyramid replies compose prebuilt planestest_append_re_resolves_bin_colors_exactly_once: Append invalidates and re-resolves exactly oncetest_memory_report_itemizes_bin_color_cache_bytes: Memory accountingBenchmarks: Added
colored_drilldown_figurefixture andtest_adaptive_drilldown_cycle_mean_colorregression 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 resolved0: resolved but not applicable (no channel or constant values)dict: theresolve_bin_colorskwargs (idx/rgba planes + LUT)Only the cheap
channels.bins_mean_color()fact is decided up front indensity_view; the expensive full-column source is materialized solely by branches that feedbin_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