feat(0133-3): wire filter/sort readers to columnar snapshot

Change 3 of issue 0133 — rewire compute_visible_rows, filter eval,
and sort comparators to read from the SnapshotCache when available.

Hot paths rewired:
- compute_visible_rows (overload with snap): filter eval uses
  compare_snap (fast i64/f64 numeric compare for Int/Float cols;
  id-compare for low-cardinality string Eq/Neq; raw cells fallback
  for Contains/StartsWith/EndsWith).
- Sort comparators: direct i64/f64 array compare for Int/Float cols
  (goto sort_done skips string fallback); string sort uses uint32_t
  id compare with pool lookup only on mismatch.
- Stage>0 filter/sort: same snapshot overload.

Materialization paths (build_so, s0_backing, mat_backing, config popup)
kept on raw cells — they copy into std::string anyway, no benefit from
snapshot and snprintf-per-cell was 2M extra calls per frame.

Bug fixes (required for correctness):
1. StringPool::intern() realloc safety: force reserve before
   emplace_back so string_view keys in the map never go dangling.
2. SnapshotCache::pool_size_built sentinel: detects when a new State
   is created with an empty pool but same cells pointer (begin_scenario
   pattern). Prevents str_ids from indexing into an empty pool (SIGSEGV).
3. Cardinality cap (2048 uniques / 25% sample): high-cardinality string
   cols (timestamps-as-strings, UUIDs, names) skip interning — str_ids
   stays empty and compare_snap falls back to raw cells. Prevents 30MB+
   pool bloat that hurt cache for filter/sort on other cols.

Bench delta vs baseline (100k rows, LIBGL_ALWAYS_SOFTWARE=1):
  linear_scroll: 16.0 -> 15.5 fps p50  (-3%, baseline already FAIL)
  filter_like:   59.7 -> 56.0 fps p50  (-6%, still PASS at 56fps)
  sort_numeric:   3.9 ->  9.0 fps p50 (+131%, snapshot i64 sort)
  color_rule:    15.2 -> 14.8 fps p50  (-3%, baseline already FAIL)

Build: green for all 10 available Linux consumers (text_editor_smoke
linker failure is preexisting, not caused by this change).

API public intact. TableEvent.row indexing TableInput preserved.
Pointer-identity invalidation preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 00:21:09 +02:00
parent ce7470d5f5
commit 2f7fdd407b
3 changed files with 268 additions and 13 deletions
+16 -1
View File
@@ -379,12 +379,27 @@ struct StringPool {
}
// intern: inserta si no existe. Devuelve indice estable.
// INVARIANTE: reserve() ANTES del primer intern() por columna para evitar
// reallocs que invalidarian los string_view del mapa. Si la estimacion fue
// insuficiente, forzamos reserve(size+1) ANTES de emplace_back para que
// la realloc ocurra antes de que cualquier sv del mapa apunte al buffer
// viejo — y reconstruimos el mapa desde cero tras la realloc.
uint32_t intern(std::string_view sv) {
auto it = index.find(sv);
if (it != index.end()) return it->second;
uint32_t id = (uint32_t)strings.size();
if (strings.size() == strings.capacity()) {
// Realloc inminente: hacerlo ANTES de insertar en index para que
// los string_view existentes no queden dangling. Tras el reserve,
// reconstruimos el index desde cero porque los punteros cambiaron.
strings.reserve(strings.capacity() == 0 ? 64 : strings.capacity() * 2);
index.clear();
for (uint32_t i = 0; i < (uint32_t)strings.size(); ++i)
index.emplace(std::string_view(strings[i]), i);
}
strings.emplace_back(sv);
// Re-apuntar el string_view al almacenamiento interno (strings[id]).
// string_view apunta al almacenamiento interno (strings[id]), estable
// porque acabamos de garantizar capacidad suficiente.
index.emplace(std::string_view(strings[id]), id);
return id;
}