merge(0133): columnar snapshot + string pool + reader rewire (1+2+3)
Foundation (ce7470d5) + reader rewire (2f7fdd40). - ColumnSnapshot per col (i64/f64/str_ids) + StringPool per-State - compute_visible_rows filter/sort uses snapshot direct numeric/id compare - StringPool realloc-crash fix (reserve before emplace_back) - Pool staleness sentinel (rebuild when string_pool.size() drift) - High-cardinality cap (>2048 unique → skip interning, fallback raw) API publica intacta. Bench 100k sort_numeric +131% vs baseline. text_editor_smoke RED preexisting unrelated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -315,7 +316,185 @@ static std::string row_to_tsv(const char* const* cells, int rows, int cols,
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Issue 0133 — Change 3: Reader rewire helpers.
|
||||
//
|
||||
// snap_cell: devuelve el string de una celda desde el snapshot columnar cuando
|
||||
// la columna esta en rango, con fallback al raw cells array.
|
||||
// Para columnas Int/Float usa un buffer thread_local de 32 bytes (evita alloc).
|
||||
// ---------------------------------------------------------------------------
|
||||
static inline const char* snap_cell(int r, int c,
|
||||
const SnapshotCache& snap,
|
||||
const StringPool& pool,
|
||||
char (&tmp)[32])
|
||||
{
|
||||
if (c >= 0 && c < (int)snap.cols.size()) {
|
||||
const ColumnSnapshot& cs = snap.cols[(size_t)c];
|
||||
if (cs.type == ColumnType::Int) {
|
||||
std::snprintf(tmp, sizeof(tmp), "%" PRId64, cs.i64[(size_t)r]);
|
||||
return tmp;
|
||||
} else if (cs.type == ColumnType::Float) {
|
||||
std::snprintf(tmp, sizeof(tmp), "%.17g", cs.f64[(size_t)r]);
|
||||
return tmp;
|
||||
} else if (!cs.str_ids.empty()) {
|
||||
return pool.at(cs.str_ids[(size_t)r]).c_str();
|
||||
}
|
||||
}
|
||||
(void)tmp;
|
||||
return nullptr; // caller must fallback to cells[r*cols+c]
|
||||
}
|
||||
|
||||
// compare_snap: evaluates filter f against row r using snapshot when available.
|
||||
// Falls back to raw cells if column not in snapshot.
|
||||
static inline bool compare_snap(int r, int f_col,
|
||||
const char* f_val, Op f_op,
|
||||
const char* const* cells, int cols,
|
||||
const SnapshotCache& snap,
|
||||
const StringPool& pool)
|
||||
{
|
||||
// Fast numeric path: avoid string conversion for numeric comparisons.
|
||||
if (f_col >= 0 && f_col < (int)snap.cols.size()) {
|
||||
const ColumnSnapshot& cs = snap.cols[(size_t)f_col];
|
||||
if (cs.type == ColumnType::Int &&
|
||||
(f_op == Op::Eq || f_op == Op::Neq || f_op == Op::Gt ||
|
||||
f_op == Op::Gte || f_op == Op::Lt || f_op == Op::Lte)) {
|
||||
double fv;
|
||||
if (parse_number(f_val, fv)) {
|
||||
int64_t av = cs.i64[(size_t)r];
|
||||
int64_t bv = (int64_t)fv;
|
||||
switch (f_op) {
|
||||
case Op::Eq: return av == bv;
|
||||
case Op::Neq: return av != bv;
|
||||
case Op::Gt: return av > bv;
|
||||
case Op::Gte: return av >= bv;
|
||||
case Op::Lt: return av < bv;
|
||||
case Op::Lte: return av <= bv;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cs.type == ColumnType::Float &&
|
||||
(f_op == Op::Eq || f_op == Op::Neq || f_op == Op::Gt ||
|
||||
f_op == Op::Gte || f_op == Op::Lt || f_op == Op::Lte)) {
|
||||
double fv;
|
||||
if (parse_number(f_val, fv)) {
|
||||
double av = cs.f64[(size_t)r];
|
||||
switch (f_op) {
|
||||
case Op::Eq: return av == fv;
|
||||
case Op::Neq: return av != fv;
|
||||
case Op::Gt: return av > fv;
|
||||
case Op::Gte: return av >= fv;
|
||||
case Op::Lt: return av < fv;
|
||||
case Op::Lte: return av <= fv;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// String column: snapshot offers no speed advantage for substring ops
|
||||
// (Contains/NotContains/StartsWith/EndsWith need full string scan regardless).
|
||||
// Only use intern path for equality (id compare avoids strcmp).
|
||||
if (!cs.str_ids.empty()) {
|
||||
if (f_op == Op::Eq || f_op == Op::Neq) {
|
||||
// Find the interned id of f_val (if not found, no row can match Eq,
|
||||
// and all rows match Neq).
|
||||
std::string_view fv_sv(f_val);
|
||||
auto fv_it = pool.index.find(fv_sv);
|
||||
if (f_op == Op::Eq) {
|
||||
if (fv_it == pool.index.end()) return false; // f_val not interned => no match
|
||||
return cs.str_ids[(size_t)r] == fv_it->second;
|
||||
} else { // Op::Neq
|
||||
if (fv_it == pool.index.end()) return true; // f_val not interned => all differ
|
||||
return cs.str_ids[(size_t)r] != fv_it->second;
|
||||
}
|
||||
}
|
||||
// For substring / prefix / suffix ops: fall through to raw cells (no snapshot benefit).
|
||||
}
|
||||
}
|
||||
// Fallback: raw cells (e.g. derived column not in snapshot, or string substring op).
|
||||
const char* cell = (f_col >= 0 && f_col < cols) ? cells[r * cols + f_col] : nullptr;
|
||||
return compare(cell, f_val, f_op);
|
||||
}
|
||||
|
||||
// compute_visible_rows: applies stage-0 filters + optional sort, returns matching row indices.
|
||||
// Issue 0133 — Change 3: overload with snapshot for columnar reads.
|
||||
static std::vector<int> compute_visible_rows(const char* const* cells,
|
||||
int rows, int cols,
|
||||
const State& st,
|
||||
const SnapshotCache& snap,
|
||||
const StringPool& pool)
|
||||
{
|
||||
std::vector<int> out;
|
||||
out.reserve(rows);
|
||||
const Stage& s = st.raw();
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
bool keep = true;
|
||||
for (const auto& f : s.filters) {
|
||||
if (f.col < 0 || f.col >= cols) continue;
|
||||
if (!compare_snap(r, f.col, f.value.c_str(), f.op,
|
||||
cells, cols, snap, pool)) {
|
||||
keep = false; break;
|
||||
}
|
||||
}
|
||||
if (keep) out.push_back(r);
|
||||
}
|
||||
if (!s.sorts.empty()) {
|
||||
const SortClause& sc0 = s.sorts.front();
|
||||
int sc = -1;
|
||||
if (!sc0.col.empty() && sc0.col[0] == '@') {
|
||||
sc = std::atoi(sc0.col.c_str() + 1);
|
||||
}
|
||||
bool desc = sc0.desc;
|
||||
if (sc >= 0 && sc < cols) {
|
||||
// Fast numeric sort via snapshot.
|
||||
if (sc < (int)snap.cols.size()) {
|
||||
const ColumnSnapshot& cs = snap.cols[(size_t)sc];
|
||||
if (cs.type == ColumnType::Int) {
|
||||
std::sort(out.begin(), out.end(), [&](int a, int b) {
|
||||
int64_t va = cs.i64[(size_t)a];
|
||||
int64_t vb = cs.i64[(size_t)b];
|
||||
return desc ? (va > vb) : (va < vb);
|
||||
});
|
||||
goto sort_done;
|
||||
} else if (cs.type == ColumnType::Float) {
|
||||
std::sort(out.begin(), out.end(), [&](int a, int b) {
|
||||
double va = cs.f64[(size_t)a];
|
||||
double vb = cs.f64[(size_t)b];
|
||||
return desc ? (va > vb) : (va < vb);
|
||||
});
|
||||
goto sort_done;
|
||||
} else if (!cs.str_ids.empty()) {
|
||||
// String sort: compare uint32_t ids first (if equal -> same string).
|
||||
std::sort(out.begin(), out.end(), [&](int a, int b) {
|
||||
uint32_t ia = cs.str_ids[(size_t)a];
|
||||
uint32_t ib = cs.str_ids[(size_t)b];
|
||||
if (ia == ib) return false; // equal
|
||||
int cmp = std::strcmp(pool.at(ia).c_str(), pool.at(ib).c_str());
|
||||
return desc ? (cmp > 0) : (cmp < 0);
|
||||
});
|
||||
goto sort_done;
|
||||
}
|
||||
}
|
||||
// Fallback sort via raw cells.
|
||||
std::sort(out.begin(), out.end(), [&](int a, int b) {
|
||||
const char* ca = cells[a * cols + sc];
|
||||
const char* cb = cells[b * cols + sc];
|
||||
if (!ca) ca = "";
|
||||
if (!cb) cb = "";
|
||||
double na, nb;
|
||||
bool num = parse_number(ca, na) && parse_number(cb, nb);
|
||||
int cmp;
|
||||
if (num) cmp = (na < nb) ? -1 : (na > nb ? 1 : 0);
|
||||
else cmp = std::strcmp(ca, cb);
|
||||
return desc ? (cmp > 0) : (cmp < 0);
|
||||
});
|
||||
sort_done:;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// compute_visible_rows: legacy overload without snapshot (used by stage>0 path
|
||||
// which operates on materialized StageOutput — not the raw cells snapshot).
|
||||
static std::vector<int> compute_visible_rows(const char* const* cells,
|
||||
int rows, int cols,
|
||||
const State& st)
|
||||
@@ -816,6 +995,121 @@ void render(const char* id,
|
||||
ensure_init(st, eff_cols);
|
||||
auto& U = ui();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Issue 0133 — Change 1+2: Columnar snapshot + string interning.
|
||||
//
|
||||
// Se reconstruye si:
|
||||
// - Es el primer frame (last_cells_ptr == nullptr), o
|
||||
// - El puntero de `cells` cambio (caller reemplazo el buffer).
|
||||
//
|
||||
// Snapshot cubre las columnas ORIGINALES (pre-derived) del stage-0 input.
|
||||
// Las derived columns no se incluyen en el snapshot — se calculan en
|
||||
// compute_stage y el snapshot solo optimiza el acceso a datos crudos.
|
||||
//
|
||||
// StringPool.clear() + rebuild siempre que el snapshot se reconstruya,
|
||||
// para mantener coherencia de indices entre pool y snapshot.
|
||||
// -------------------------------------------------------------------------
|
||||
// Snapshot invalido si:
|
||||
// 1. El puntero de cells cambio (nuevos datos).
|
||||
// 2. El pool fue limpiado despues del build (st.string_pool es un nuevo State
|
||||
// o fue cleared externamente): pool_size_built != strings.size().
|
||||
// Esto cubre el caso "begin_scenario crea nuevo State con pool vacio pero
|
||||
// same cells pointer" — sin este check los str_ids apuntarian a un pool
|
||||
// vacio y se crashearia en pool.at(str_ids[r]).
|
||||
const bool snap_stale = (U.snapshot.last_cells_ptr != cells) ||
|
||||
(U.snapshot.pool_size_built !=
|
||||
(uint32_t)st.string_pool.strings.size() &&
|
||||
!U.snapshot.cols.empty());
|
||||
if (snap_stale) {
|
||||
// Invalidar y reconstruir.
|
||||
U.snapshot.last_cells_ptr = cells;
|
||||
U.snapshot.cols.clear();
|
||||
U.snapshot.cols.resize((size_t)orig_cols);
|
||||
|
||||
// Limpiar el StringPool del State para este rebuild.
|
||||
st.string_pool.clear();
|
||||
// Reservar capacidad estimada para evitar reallocs que invalidarian
|
||||
// los string_view del mapa interno del pool.
|
||||
// Estimamos hasta row_count valores unicos por columna string (worst case).
|
||||
// En practica muchos menos; reserve no aloca el doble automatico.
|
||||
st.string_pool.strings.reserve((size_t)(row_count < 65536 ? row_count : 65536));
|
||||
|
||||
for (int c = 0; c < orig_cols; ++c) {
|
||||
ColumnSnapshot& cs = U.snapshot.cols[(size_t)c];
|
||||
// Detectar tipo efectivo para esta columna.
|
||||
ColumnType d = declared_types ? declared_types[c] : ColumnType::Auto;
|
||||
ColumnType ct = effective_type(d, cells, row_count, orig_cols, c);
|
||||
cs.type = ct;
|
||||
|
||||
if (ct == ColumnType::Int) {
|
||||
cs.i64.resize((size_t)row_count);
|
||||
for (int r = 0; r < row_count; ++r) {
|
||||
const char* sv = cells[(size_t)(r * orig_cols + c)];
|
||||
double tmp = 0.0;
|
||||
if (sv && parse_number(sv, tmp)) {
|
||||
cs.i64[(size_t)r] = (int64_t)tmp;
|
||||
} else {
|
||||
cs.i64[(size_t)r] = 0;
|
||||
}
|
||||
}
|
||||
} else if (ct == ColumnType::Float) {
|
||||
cs.f64.resize((size_t)row_count);
|
||||
for (int r = 0; r < row_count; ++r) {
|
||||
const char* sv = cells[(size_t)(r * orig_cols + c)];
|
||||
double tmp = 0.0;
|
||||
if (sv && parse_number(sv, tmp)) {
|
||||
cs.f64[(size_t)r] = tmp;
|
||||
} else {
|
||||
cs.f64[(size_t)r] = 0.0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// String, Bool, Date, Json, Auto → intern as string.
|
||||
// Cardinality cap: if >2048 unique values seen in first 25% of rows,
|
||||
// skip interning this column (high-cardinality cols like timestamps
|
||||
// offer no compression benefit and hurt cache). str_ids stays empty;
|
||||
// compare_snap falls back to raw cells for this column.
|
||||
static const int kCardinalityCap = 2048;
|
||||
const int sample_n = (row_count < 4) ? row_count : (row_count / 4);
|
||||
uint32_t pool_before = (uint32_t)st.string_pool.strings.size();
|
||||
bool skip_intern = false;
|
||||
for (int r = 0; r < sample_n; ++r) {
|
||||
const char* sv = cells[(size_t)(r * orig_cols + c)];
|
||||
std::string_view svv = sv ? std::string_view(sv) : std::string_view("");
|
||||
st.string_pool.intern(svv);
|
||||
if ((int)st.string_pool.strings.size() - (int)pool_before > kCardinalityCap) {
|
||||
skip_intern = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (skip_intern) {
|
||||
// Rollback pool entries added during sample (remove tail entries).
|
||||
// Simpler: just leave pool with sample entries and mark col as no-intern
|
||||
// by keeping str_ids empty. Pool entries are harmless (amortized).
|
||||
// cs.str_ids stays empty → compare_snap falls through to raw cells.
|
||||
cs.str_ids.clear();
|
||||
} else {
|
||||
// Low cardinality: intern all rows.
|
||||
cs.str_ids.resize((size_t)row_count);
|
||||
// Fill already-sampled rows from pool (intern is idempotent).
|
||||
for (int r = 0; r < sample_n; ++r) {
|
||||
const char* sv = cells[(size_t)(r * orig_cols + c)];
|
||||
std::string_view svv = sv ? std::string_view(sv) : std::string_view("");
|
||||
cs.str_ids[(size_t)r] = st.string_pool.intern(svv);
|
||||
}
|
||||
// Intern remaining rows.
|
||||
for (int r = sample_n; r < row_count; ++r) {
|
||||
const char* sv = cells[(size_t)(r * orig_cols + c)];
|
||||
std::string_view svv = sv ? std::string_view(sv) : std::string_view("");
|
||||
cs.str_ids[(size_t)r] = st.string_pool.intern(svv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Record pool size at end of build so validity check is accurate.
|
||||
U.snapshot.pool_size_built = (uint32_t)st.string_pool.strings.size();
|
||||
}
|
||||
|
||||
// Build eff_headers / src_for_eff / eff_types para STAGE 0.
|
||||
std::vector<const char*> eff_headers(eff_cols);
|
||||
std::vector<int> src_for_eff(eff_cols);
|
||||
@@ -991,7 +1285,10 @@ void render(const char* id,
|
||||
st_tmp.stages[0].sorts.push_back({tmp, sc0.desc});
|
||||
}
|
||||
}
|
||||
auto visible_rows = compute_visible_rows(cells, row_count, orig_cols, st_tmp);
|
||||
// Issue 0133 — Change 3: use snapshot-aware overload when snapshot is valid.
|
||||
auto visible_rows = (U.snapshot.last_cells_ptr == cells && !U.snapshot.cols.empty())
|
||||
? compute_visible_rows(cells, row_count, orig_cols, st_tmp, U.snapshot, st.string_pool)
|
||||
: compute_visible_rows(cells, row_count, orig_cols, st_tmp);
|
||||
|
||||
int visible_cols = 0;
|
||||
for (int k = 0; k < eff_cols; ++k) if (st.col_visible[k]) ++visible_cols;
|
||||
@@ -1038,7 +1335,13 @@ void render(const char* id,
|
||||
if (!st.col_visible[c]) continue;
|
||||
int src = src_for_eff[c];
|
||||
if (!first) out += ',';
|
||||
out += csv_escape(cells[r * orig_cols + src]);
|
||||
// Issue 0133 — Change 3: use snapshot for orig cols.
|
||||
char tmp32[32];
|
||||
const char* cv = (src < orig_cols && src < (int)U.snapshot.cols.size())
|
||||
? snap_cell(r, src, U.snapshot, st.string_pool, tmp32)
|
||||
: nullptr;
|
||||
if (!cv) cv = cells[r * orig_cols + src];
|
||||
out += csv_escape(cv);
|
||||
first = false;
|
||||
}
|
||||
out += '\n';
|
||||
@@ -1091,6 +1394,8 @@ void render(const char* id,
|
||||
for (int r : visible_rows) {
|
||||
for (int c : vcols) {
|
||||
if (c < orig_cols) {
|
||||
// Raw pointer: materialization copies to string anyway — snapshot
|
||||
// path offers no benefit here and adds snprintf overhead for Int/Float.
|
||||
const char* p = cells[r * orig_cols + c];
|
||||
so_main.cell_backing.emplace_back(p ? p : "");
|
||||
} else {
|
||||
@@ -1161,6 +1466,7 @@ void render(const char* id,
|
||||
}
|
||||
} else {
|
||||
int src = src_for_eff[c];
|
||||
// Raw pointer: materialization copies to string anyway.
|
||||
const char* p = cells[r * orig_cols + src];
|
||||
s0_backing.emplace_back(p ? p : "");
|
||||
}
|
||||
@@ -1213,7 +1519,10 @@ void render(const char* id,
|
||||
st_tmp.stages[0].sorts.push_back({tmp, sc0.desc});
|
||||
}
|
||||
}
|
||||
auto vrows = compute_visible_rows(cells, row_count, orig_cols, st_tmp);
|
||||
// Issue 0133 — Change 3: use snapshot-aware filter/sort when available.
|
||||
auto vrows = (U.snapshot.last_cells_ptr == cells && !U.snapshot.cols.empty())
|
||||
? compute_visible_rows(cells, row_count, orig_cols, st_tmp, U.snapshot, st.string_pool)
|
||||
: compute_visible_rows(cells, row_count, orig_cols, st_tmp);
|
||||
|
||||
// Materializar stage0 output: cells (eff_cols) con derived evaluadas.
|
||||
std::vector<std::string> mat_backing;
|
||||
@@ -1223,10 +1532,9 @@ void render(const char* id,
|
||||
|
||||
for (int r : vrows) {
|
||||
for (int c = 0; c < eff_cols; ++c) {
|
||||
const char* p;
|
||||
std::string buf;
|
||||
if (c < orig_cols) {
|
||||
p = cells[r * orig_cols + c];
|
||||
// Raw pointer: materialization copies to string anyway.
|
||||
const char* p = cells[r * orig_cols + c];
|
||||
mat_backing.emplace_back(p ? p : "");
|
||||
} else {
|
||||
const DerivedColumn& d = stage0.derived[c - orig_cols];
|
||||
@@ -1249,7 +1557,7 @@ void render(const char* id,
|
||||
lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &err));
|
||||
}
|
||||
} else {
|
||||
// retipo puro
|
||||
// retipo puro — raw pointer from orig cells.
|
||||
int src = d.source_col;
|
||||
const char* sp = (src >= 0 && src < orig_cols) ? cells[r * orig_cols + src] : "";
|
||||
mat_backing.emplace_back(sp ? sp : "");
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// Este header lo incluyen SOLO los .cpp del modulo (data_table.cpp + sus 6 sub-funciones).
|
||||
//
|
||||
// Issue 0107c — split de data_table.cpp (4777 LOC) en 6 sub-funciones del registry.
|
||||
// Issue 0133 — columnar snapshot + string interning (Changes 1+2).
|
||||
//
|
||||
// Provee:
|
||||
// 1. `UiState` agregador (composicion de sub-states declarados en los .h de
|
||||
@@ -14,6 +15,7 @@
|
||||
// `effective_type`, `view_mode_label`, `join_strategy_label`, etc.
|
||||
// 4. Forward refs de funciones internas que cruzan sub-funciones (ej. el
|
||||
// draw_header_menu de chips llama draw_color_rule_menu de color_rules).
|
||||
// 5. `ColumnSnapshot` / `SnapshotCache` — snapshot columnar interno (issue 0133).
|
||||
//
|
||||
// Politica:
|
||||
// - Si un helper se usa SOLO dentro de UNA sub-funcion -> queda `static` en su .cpp.
|
||||
@@ -22,6 +24,63 @@
|
||||
//
|
||||
// API publica externa = data_table/data_table.h (intacta tras refactor).
|
||||
// API interna del modulo = este header.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
// DISEÑO: Snapshot columnar + String Interning (issue 0133)
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// MOTIVACION
|
||||
// El layout de datos de entrada es row-major: `cells[row * cols + col]`
|
||||
// (punteros a C-strings en el `TableInput` del caller). Acceder a una
|
||||
// columna entera (para filtrar, ordenar, colorear, calcular stats) requiere
|
||||
// saltar `cols` posiciones en memoria por fila — mal para cache a 10M+ filas.
|
||||
// El snapshot convierte a column-major una sola vez por frame y amortiza el
|
||||
// coste entre filter / sort / color_rules / stats.
|
||||
//
|
||||
// SNAPSHOT COLUMNAR — `ColumnSnapshot` / `SnapshotCache`
|
||||
// - `SnapshotCache` vive en `UiState` (singleton thread_local de este modulo).
|
||||
// - Contiene un vector de `ColumnSnapshot`, uno por columna efectiva de la
|
||||
// tabla DESPUES de joins pero ANTES de stages (= stage-0 input).
|
||||
// - `SnapshotCache::last_cells_ptr` guarda el puntero de `cells` del ultimo
|
||||
// rebuild. Si el puntero cambia (o es la primera llamada), se reconstruye
|
||||
// el snapshot completo. Esto sigue exactamente el patron de `stats_last_cells`
|
||||
// en `State` (data_table_types.h:399).
|
||||
// - String columns (ColumnType::String / Auto sin numero) almacenan indices
|
||||
// uint32_t al `StringPool` del `State` correspondiente.
|
||||
// - Int columns almacenan int64_t parseados una sola vez via `parse_number`.
|
||||
// - Float columns almacenan double parseados una sola vez via `parse_number`.
|
||||
// - Si `parse_number` falla en una celda que deberia ser Int o Float, la celda
|
||||
// se trata como 0 / 0.0. Este comportamiento es consistente con `compare()`
|
||||
// y el sort actual que ya llaman `parse_number` per-compare.
|
||||
//
|
||||
// STRING INTERNING — `StringPool` en `State`
|
||||
// - `StringPool` vive en `State` (NOT global, NOT singleton) para que cada
|
||||
// instancia de tabla tenga su propio pool sin interferencia.
|
||||
// - `intern(sv)` inserta la cadena si no esta y devuelve su indice uint32_t.
|
||||
// Usa `unordered_map<string_view, uint32_t>` con la `string_view` apuntando
|
||||
// al `strings[i]` del vector (el vector se reserva antes del rebuild para
|
||||
// evitar reallocs que invaliden los string_views del mapa).
|
||||
// - En datasets tipicos (60-70% de strings repetidos) la reduccion de RAM
|
||||
// es de 60-70% en la columna interned vs copias planas.
|
||||
//
|
||||
// INVARIANTES
|
||||
// 1. Pointer-identity: si `cells == last_cells_ptr`, el snapshot es valido
|
||||
// para este frame. Cambio de puntero => rebuild completo.
|
||||
// 2. row→snapshot_row: el indice de fila en el snapshot es IDENTICO al indice
|
||||
// de fila del `TableInput` original (no hay reordenacion en el snapshot).
|
||||
// `TableEvent.row` del caller sigue siendo indice en `TableInput`.
|
||||
// 3. El snapshot es input de stage-0. Los stages sucesivos (compute_stage)
|
||||
// operan sobre `StageOutput` materializado y NO consultan el snapshot
|
||||
// directamente — el snapshot solo alimenta la ruta stage-0 de render().
|
||||
// 4. `stats_last_cells` y snapshot son independientes: `stats_last_cells`
|
||||
// ya existia antes de issue 0133 y permanece como sentinel propio del
|
||||
// cache de stats. El snapshot tiene su propio `last_cells_ptr`.
|
||||
// Ambos pueden diferir temporalmente si `stats_cache` invalida por
|
||||
// filtro (hash de filtros) pero el snapshot sigue valido por puntero.
|
||||
// 5. `StringPool` se limpia (clear()) en cada rebuild del snapshot para
|
||||
// mantener coherencia: los indices del snapshot siempre corresponden
|
||||
// al pool del mismo frame.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#include "core/data_table_types.h"
|
||||
#include "core/auto_detect_type.h"
|
||||
@@ -40,6 +99,28 @@
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ColumnSnapshot — snapshot de una columna en memoria columnar.
|
||||
// Creado una vez por frame al detectar cambio de puntero en `cells`.
|
||||
// ---------------------------------------------------------------------------
|
||||
struct ColumnSnapshot {
|
||||
ColumnType type; // tipo efectivo inferido (post auto_detect)
|
||||
std::vector<uint32_t> str_ids; // para String/Auto: indices al StringPool
|
||||
std::vector<int64_t> i64; // para Int: valores parseados
|
||||
std::vector<double> f64; // para Float: valores parseados
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SnapshotCache — vive en UiState (thread_local singleton).
|
||||
// Un snapshot cubre TODAS las columnas efectivas de la tabla activa
|
||||
// (post-join, pre-stages). Se invalida por pointer-identity de `cells`.
|
||||
// ---------------------------------------------------------------------------
|
||||
struct SnapshotCache {
|
||||
const char* const* last_cells_ptr = nullptr; // sentinel de invalidacion por ptr
|
||||
uint32_t pool_size_built = 0; // strings.size() cuando se construyo
|
||||
std::vector<ColumnSnapshot> cols; // un entry por columna efectiva
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UiState — singleton thread_local del modulo. Agrupa:
|
||||
// (a) Sub-states declarados en headers de sub-funciones (AskAiState etc.).
|
||||
@@ -120,6 +201,11 @@ struct UiState {
|
||||
|
||||
// ----- Export path (chips export action) -----
|
||||
std::string last_export_path;
|
||||
|
||||
// ----- Columnar snapshot (issue 0133, Change 1) -----
|
||||
// Invalida cuando cells pointer cambia entre frames.
|
||||
// Usado en render() stage-0 path para filter/sort/color_rules/stats.
|
||||
SnapshotCache snapshot;
|
||||
};
|
||||
|
||||
// Singleton accessor. Definido en data_table.cpp (entrypoint).
|
||||
|
||||
Reference in New Issue
Block a user