diff --git a/cpp/functions/core/data_table_types.h b/cpp/functions/core/data_table_types.h index 67d9dc4e..383aff25 100644 --- a/cpp/functions/core/data_table_types.h +++ b/cpp/functions/core/data_table_types.h @@ -8,6 +8,8 @@ #include "compute_column_stats.h" #include +#include +#include #include #include @@ -353,6 +355,44 @@ struct VizPanel { mutable ViewMode last_non_table = ViewMode::Bar; }; +// ---------------------------------------------------------------------------- +// StringPool — interning de strings para columnas de texto (issue 0133). +// Una instancia por State (NOT global) para aislar tablas independientes. +// +// intern(sv) devuelve un indice uint32_t estable para la vida del rebuild. +// El pool se limpia (clear()) al inicio de cada rebuild de snapshot columnar. +// +// Invariante de invalidacion de string_view: +// - El vector `strings` se reserva con reserve() ANTES del primer intern() +// para evitar reallocs que invalidarian los string_view del mapa. +// Si la estimacion es insuficiente (columna con mas unicos de lo esperado), +// el mapa se reconstruye post-push_back: intern() verifica cap antes de +// insertar en el map para cubrir este caso. +// ---------------------------------------------------------------------------- +struct StringPool { + std::vector strings; // strings unicos, por indice + std::unordered_map index; // sv→id (sv apunta a strings[i]) + + void clear() { + strings.clear(); + index.clear(); + } + + // intern: inserta si no existe. Devuelve indice estable. + 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(); + strings.emplace_back(sv); + // Re-apuntar el string_view al almacenamiento interno (strings[id]). + index.emplace(std::string_view(strings[id]), id); + return id; + } + + const std::string& at(uint32_t id) const { return strings[id]; } + bool empty() const { return strings.empty(); } +}; + // ---------------------------------------------------------------------------- // State: stage pipeline + viz globales. // ---------------------------------------------------------------------------- @@ -419,6 +459,11 @@ struct State { std::vector drill_back; std::vector drill_forward; + // String interning pool (issue 0133, Change 2). + // Limpiado y repoblado en cada rebuild del snapshot columnar. + // NOT global — una instancia por State para aislar tablas independientes. + StringPool string_pool; + // Helpers (definidos en compute_stage.cpp). Stage& raw(); const Stage& raw() const; diff --git a/modules/data_table/data_table.cpp b/modules/data_table/data_table.cpp index 02e6d325..b4d4d8c7 100644 --- a/modules/data_table/data_table.cpp +++ b/modules/data_table/data_table.cpp @@ -816,6 +816,75 @@ 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. + // ------------------------------------------------------------------------- + if (U.snapshot.last_cells_ptr != cells) { + // 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. + cs.str_ids.resize((size_t)row_count); + for (int r = 0; 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); + } + } + } + } + // Build eff_headers / src_for_eff / eff_types para STAGE 0. std::vector eff_headers(eff_cols); std::vector src_for_eff(eff_cols); diff --git a/modules/data_table/data_table_internal.h b/modules/data_table/data_table_internal.h index f6380b40..870aef49 100644 --- a/modules/data_table/data_table_internal.h +++ b/modules/data_table/data_table_internal.h @@ -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` 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,27 @@ 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 str_ids; // para String/Auto: indices al StringPool + std::vector i64; // para Int: valores parseados + std::vector 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 + std::vector 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 +200,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).