feat: initial scaffold data_table_bench (issue 0133)
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
add_imgui_app(data_table_bench
|
||||||
|
main.cpp
|
||||||
|
bench_runner.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(data_table_bench PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
|
# fn_module_data_table: data_table::render(), TQL engine, Lua.
|
||||||
|
if(TARGET fn_module_data_table)
|
||||||
|
target_link_libraries(data_table_bench PRIVATE fn_module_data_table)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# SQLite3 for operations.db persistence.
|
||||||
|
find_package(SQLite3 REQUIRED)
|
||||||
|
target_link_libraries(data_table_bench PRIVATE SQLite::SQLite3)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
set_target_properties(data_table_bench PROPERTIES WIN32_EXECUTABLE FALSE)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
name: data_table_bench
|
||||||
|
lang: cpp
|
||||||
|
domain: tools
|
||||||
|
version: 0.1.0
|
||||||
|
description: "Headless 10M-row performance gate for data_table::render() (issue 0133). Mide fps_p50/fps_p1/mem/cpu en 4 escenarios: scroll lineal, filter LIKE, sort numerico y color rule."
|
||||||
|
tags: [imgui, bench, headless, data-table, perf]
|
||||||
|
uses_functions:
|
||||||
|
- data_table_cpp_viz
|
||||||
|
uses_types: []
|
||||||
|
framework: "imgui"
|
||||||
|
entry_point: "main.cpp"
|
||||||
|
dir_path: "apps/data_table_bench"
|
||||||
|
repo_url: ""
|
||||||
|
icon:
|
||||||
|
phosphor: "gauge"
|
||||||
|
accent: "#22d3ee"
|
||||||
|
e2e_checks:
|
||||||
|
- id: build
|
||||||
|
cmd: "cd cpp/build/linux && cmake --build . --target data_table_bench -j 4"
|
||||||
|
timeout_s: 300
|
||||||
|
- id: smoke_100k
|
||||||
|
cmd: "cd apps/data_table_bench && xvfb-run -a -s \"-screen 0 1920x1080x24\" env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe ../../cpp/build/linux/apps/data_table_bench/data_table_bench --rows 100000 --duration 5 --no-db"
|
||||||
|
timeout_s: 120
|
||||||
|
expect_stdout_contains: "\"overall_pass\":"
|
||||||
|
severity: warning
|
||||||
|
---
|
||||||
|
|
||||||
|
# data_table_bench
|
||||||
|
|
||||||
|
Headless benchmark app para el modulo `data_table` (issue 0133). Gate de performance entre cada cambio del refactor 10M-row.
|
||||||
|
|
||||||
|
## Uso
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# CI smoke (rapido, 100k filas, 5s por escenario)
|
||||||
|
cd apps/data_table_bench
|
||||||
|
xvfb-run -a -s "-screen 0 1920x1080x24" \
|
||||||
|
env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \
|
||||||
|
../../cpp/build/linux/apps/data_table_bench/data_table_bench \
|
||||||
|
--rows 100000 --duration 5 --no-db
|
||||||
|
|
||||||
|
# Full bench (10M filas, 30s por escenario)
|
||||||
|
xvfb-run -a -s "-screen 0 1920x1080x24" \
|
||||||
|
env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \
|
||||||
|
../../cpp/build/linux/apps/data_table_bench/data_table_bench \
|
||||||
|
--rows 10000000 --duration 30
|
||||||
|
|
||||||
|
# Con persistencia SQLite
|
||||||
|
./data_table_bench --rows 10000000 --duration 30 --db operations.db --sha $(git rev-parse HEAD)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output ejemplo
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rows": 100000,
|
||||||
|
"fps_threshold": 60.0,
|
||||||
|
"scenarios": [
|
||||||
|
{ "scenario": "linear_scroll", "fps_p50": 320.5, "fps_p1": 125.3, "mem_rss_mb": 450.2, "cpu_pct": 85.1, "duration_s": 5.0, "pass": true },
|
||||||
|
{ "scenario": "filter_like", "fps_p50": 210.1, "fps_p1": 95.2, "mem_rss_mb": 450.5, "cpu_pct": 92.3, "duration_s": 5.0, "pass": true },
|
||||||
|
{ "scenario": "sort_numeric", "fps_p50": 180.3, "fps_p1": 70.1, "mem_rss_mb": 451.0, "cpu_pct": 88.7, "duration_s": 5.0, "pass": true },
|
||||||
|
{ "scenario": "color_rule", "fps_p50": 290.7, "fps_p1": 98.4, "mem_rss_mb": 451.2, "cpu_pct": 86.2, "duration_s": 5.0, "pass": true }
|
||||||
|
],
|
||||||
|
"overall_pass": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Assertion DoD
|
||||||
|
|
||||||
|
`fps_p1 >= 60` en cada escenario con 10M filas a 30s.
|
||||||
|
|
||||||
|
## Escenarios
|
||||||
|
|
||||||
|
| ID | Patron | Lo que estresan |
|
||||||
|
|---|---|---|
|
||||||
|
| `linear_scroll` | Avanza el scroll 1 fila/frame | Clipper virtual scroll en O(1) |
|
||||||
|
| `filter_like` | Contains("foo") sobre col name | Scan de 10M strings por frame |
|
||||||
|
| `sort_numeric` | Sort score DESC | Reordenacion del stage pipeline |
|
||||||
|
| `color_rule` | NumericRange gradient en col value | Pintado condicional de 10M celdas |
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cpp/build/linux
|
||||||
|
cmake --build . --target data_table_bench -j 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dataset
|
||||||
|
|
||||||
|
Sintetico y determinista (mismo seed => mismas filas). 20 columnas:
|
||||||
|
- Cols 0-4: int (id, count, score, rank, flags)
|
||||||
|
- Cols 5-9: float (ratio, pct, value, rate, weight)
|
||||||
|
- Cols 10-14: string (name, status, category, tag, label)
|
||||||
|
- Cols 15-19: timestamp como int string (created_at, updated_at, closed_at, due_at, ts)
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- Requiere xvfb + llvmpipe en CI Linux (sin GPU real). Instalar: `sudo apt-get install xvfb libgles2-mesa`.
|
||||||
|
- El seed de 10M filas puede tardar 10-15s antes de arrancar el GL context.
|
||||||
|
- `vsync = false` en AppConfig para que el bench no este limitado por el refresh rate del display virtual.
|
||||||
|
- La ventana se oculta con `glfwHideWindow()` en el primer frame — nunca aparece en pantalla.
|
||||||
|
- Windows: sin cross-compile habilitado el bench es solo Linux. El DoD del issue 0133 es Linux.
|
||||||
|
|
||||||
|
## Capability growth log
|
||||||
|
|
||||||
|
- v0.1.0 (2026-05-22) — baseline para issue 0133.
|
||||||
@@ -0,0 +1,487 @@
|
|||||||
|
// bench_runner.cpp — frame-by-frame benchmark harness for data_table::render().
|
||||||
|
// Issue 0133: perf gate for 10M-row refactor.
|
||||||
|
//
|
||||||
|
// Design: Runner::tick() is called once per fn::run_app frame.
|
||||||
|
// Each frame it renders the table for the current scenario and measures the
|
||||||
|
// frame time (wall clock from tick() entry to exit). After duration_s seconds
|
||||||
|
// it finalizes the scenario, moves to the next one, and eventually sets done_.
|
||||||
|
//
|
||||||
|
// This avoids any nested-frame or out-of-context ImGui calls — tick() runs
|
||||||
|
// inside a valid ImGui frame, after NewFrame() and before Render().
|
||||||
|
|
||||||
|
#include "bench_runner.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include "core/logger.h"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <sqlite3.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
# include <sys/resource.h>
|
||||||
|
#endif
|
||||||
|
#ifdef _WIN32
|
||||||
|
# include <windows.h>
|
||||||
|
# include <psapi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace bench {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// System metrics
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
double measure_rss_mb() {
|
||||||
|
#ifdef __linux__
|
||||||
|
FILE* f = fopen("/proc/self/status", "r");
|
||||||
|
if (!f) return 0.0;
|
||||||
|
char line[256];
|
||||||
|
while (fgets(line, sizeof(line), f)) {
|
||||||
|
long kb = 0;
|
||||||
|
if (sscanf(line, "VmRSS: %ld kB", &kb) == 1) {
|
||||||
|
fclose(f);
|
||||||
|
return static_cast<double>(kb) / 1024.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
return 0.0;
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
PROCESS_MEMORY_COUNTERS pmc;
|
||||||
|
if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc)))
|
||||||
|
return static_cast<double>(pmc.WorkingSetSize) / (1024.0 * 1024.0);
|
||||||
|
return 0.0;
|
||||||
|
#else
|
||||||
|
return 0.0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
double measure_cpu_pct(double wall_s, double user_s, double sys_s) {
|
||||||
|
if (wall_s < 1e-9) return 0.0;
|
||||||
|
return 100.0 * (user_s + sys_s) / wall_s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void get_rusage_times(double& user_s, double& sys_s) {
|
||||||
|
#ifdef __linux__
|
||||||
|
struct rusage ru;
|
||||||
|
getrusage(RUSAGE_SELF, &ru);
|
||||||
|
user_s = ru.ru_utime.tv_sec + ru.ru_utime.tv_usec * 1e-6;
|
||||||
|
sys_s = ru.ru_stime.tv_sec + ru.ru_stime.tv_usec * 1e-6;
|
||||||
|
#else
|
||||||
|
user_s = 0.0; sys_s = 0.0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ScenarioState — holds TableInput + data_table::State for one scenario.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct Runner::ScenarioState {
|
||||||
|
data_table::TableInput ti;
|
||||||
|
data_table::State st;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Dataset generation — deterministic, 20 columns, 5 int / 5 float / 5 str / 5 ts.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Runner::Runner(const Config& cfg) : cfg_(cfg) {
|
||||||
|
seed_dataset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Runner::seed_dataset() {
|
||||||
|
const long long N = cfg_.rows;
|
||||||
|
const int C = cfg_.cols;
|
||||||
|
|
||||||
|
backing_.clear();
|
||||||
|
backing_.reserve(static_cast<size_t>(N) * static_cast<size_t>(C));
|
||||||
|
|
||||||
|
static const char* statuses[] = {"ok", "error", "running", "pending", "blocked"};
|
||||||
|
static const char* categories[] = {"alpha", "beta", "gamma", "delta", "epsilon"};
|
||||||
|
static const char* tags_a[] = {"foo", "bar", "baz", "qux", "quux"};
|
||||||
|
static const char* labels_a[] = {"low", "medium", "high", "critical", "unknown"};
|
||||||
|
|
||||||
|
char buf[64];
|
||||||
|
for (long long i = 0; i < N; ++i) {
|
||||||
|
// 0: id (int)
|
||||||
|
snprintf(buf, sizeof(buf), "%lld", i + 1);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 1: count (int 0..9999)
|
||||||
|
snprintf(buf, sizeof(buf), "%lld", (i * 7 + 3) % 10000);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 2: score (int 0..1000)
|
||||||
|
snprintf(buf, sizeof(buf), "%lld", (i * 131) % 1001);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 3: rank (int 0..499)
|
||||||
|
snprintf(buf, sizeof(buf), "%lld", (i * 17) % 500);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 4: flags (int 0..15)
|
||||||
|
snprintf(buf, sizeof(buf), "%lld", i & 0xF);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 5: ratio (float 0..1)
|
||||||
|
snprintf(buf, sizeof(buf), "%.6f", static_cast<double>(i % 10000) / 10000.0);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 6: pct (float 0..100)
|
||||||
|
snprintf(buf, sizeof(buf), "%.4f", static_cast<double>((i * 37) % 10001) / 100.0);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 7: value (float 0..9999, for ColorRule p95 scenario)
|
||||||
|
snprintf(buf, sizeof(buf), "%.2f", static_cast<double>((i * 71) % 10000));
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 8: rate (float)
|
||||||
|
snprintf(buf, sizeof(buf), "%.4f", static_cast<double>((i * 13) % 5000) / 500.0);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 9: weight (float)
|
||||||
|
snprintf(buf, sizeof(buf), "%.3f", static_cast<double>((i * 29) % 1000) / 100.0);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 10: name (string; "item_foo_N" every 7th row for filter_like)
|
||||||
|
if (i % 7 == 0)
|
||||||
|
snprintf(buf, sizeof(buf), "item_foo_%lld", i);
|
||||||
|
else
|
||||||
|
snprintf(buf, sizeof(buf), "item_%lld", i);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 11: status
|
||||||
|
backing_.emplace_back(statuses[i % 5]);
|
||||||
|
// 12: category
|
||||||
|
backing_.emplace_back(categories[(i * 3) % 5]);
|
||||||
|
// 13: tag
|
||||||
|
backing_.emplace_back(tags_a[(i * 5) % 5]);
|
||||||
|
// 14: label
|
||||||
|
backing_.emplace_back(labels_a[(i * 11) % 5]);
|
||||||
|
// 15: created_at (int as string)
|
||||||
|
snprintf(buf, sizeof(buf), "%lld", 1700000000LL + i * 60);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 16: updated_at
|
||||||
|
snprintf(buf, sizeof(buf), "%lld", 1700000000LL + i * 60 + 30);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 17: closed_at
|
||||||
|
snprintf(buf, sizeof(buf), "%lld", 1700000000LL + i * 60 + (i % 3 == 0 ? 120 : 0));
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 18: due_at
|
||||||
|
snprintf(buf, sizeof(buf), "%lld", 1700000000LL + (i + 1000) * 3600);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
// 19: ts
|
||||||
|
snprintf(buf, sizeof(buf), "%lld", 1700000000LL + i);
|
||||||
|
backing_.emplace_back(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
ptrs_.clear();
|
||||||
|
ptrs_.reserve(backing_.size());
|
||||||
|
for (const auto& s : backing_) ptrs_.push_back(s.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// begin_scenario — sets up TableInput + State for a scenario.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void Runner::begin_scenario(Scenario s) {
|
||||||
|
delete sc_state_;
|
||||||
|
sc_state_ = new ScenarioState();
|
||||||
|
|
||||||
|
// TableInput.
|
||||||
|
sc_state_->ti.name = "bench_data";
|
||||||
|
sc_state_->ti.headers = {
|
||||||
|
"id", "count", "score", "rank", "flags",
|
||||||
|
"ratio", "pct", "value", "rate", "weight",
|
||||||
|
"name", "status", "category", "tag", "label",
|
||||||
|
"created_at", "updated_at", "closed_at", "due_at", "ts"
|
||||||
|
};
|
||||||
|
using CT = data_table::ColumnType;
|
||||||
|
sc_state_->ti.types = {
|
||||||
|
CT::Int, CT::Int, CT::Int, CT::Int, CT::Int,
|
||||||
|
CT::Float, CT::Float, CT::Float, CT::Float, CT::Float,
|
||||||
|
CT::String, CT::String, CT::String, CT::String, CT::String,
|
||||||
|
CT::Int, CT::Int, CT::Int, CT::Int, CT::Int
|
||||||
|
};
|
||||||
|
sc_state_->ti.cells = ptrs_.data();
|
||||||
|
sc_state_->ti.rows = static_cast<int>(cfg_.rows);
|
||||||
|
sc_state_->ti.cols = cfg_.cols;
|
||||||
|
|
||||||
|
// State.
|
||||||
|
sc_state_->st = data_table::State{};
|
||||||
|
sc_state_->st.ensure_stage0();
|
||||||
|
|
||||||
|
switch (s) {
|
||||||
|
case Scenario::FilterLike: {
|
||||||
|
data_table::Filter f;
|
||||||
|
f.col = 10; // name column
|
||||||
|
f.op = data_table::Op::Contains;
|
||||||
|
f.value = "foo";
|
||||||
|
sc_state_->st.raw().filters.push_back(f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Scenario::SortNumeric: {
|
||||||
|
data_table::SortClause sc;
|
||||||
|
sc.col = "score";
|
||||||
|
sc.desc = true;
|
||||||
|
sc_state_->st.raw().sorts.push_back(sc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Scenario::ColorRule: {
|
||||||
|
data_table::ColorRule cr;
|
||||||
|
cr.col = 7; // value column
|
||||||
|
cr.kind = data_table::ColorRuleKind::NumericRange;
|
||||||
|
cr.range_min = 0.0;
|
||||||
|
cr.range_max = 9999.0;
|
||||||
|
cr.range_alpha = 0.3f;
|
||||||
|
cr.range_stops.push_back({0.0f, "#22c55e"});
|
||||||
|
cr.range_stops.push_back({1.0f, "#ef4444"});
|
||||||
|
sc_state_->st.color_rules.push_back(cr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Scenario::LinearScroll:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timing init.
|
||||||
|
frame_fps_.clear();
|
||||||
|
frame_fps_.reserve(4096);
|
||||||
|
scroll_row_ = 0;
|
||||||
|
scenario_wall_start_ = Clock::now();
|
||||||
|
last_frame_start_ = Clock::now();
|
||||||
|
get_rusage_times(user_start_, sys_start_);
|
||||||
|
|
||||||
|
fprintf(stderr, "[bench] scenario: %s (rows=%lld duration=%.0fs)\n",
|
||||||
|
scenario_name(s), cfg_.rows, cfg_.duration_s);
|
||||||
|
fflush(stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// finish_scenario — collects stats from frame_fps_ and stores result.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void Runner::finish_scenario() {
|
||||||
|
Scenario s = kScenarios[current_scenario_];
|
||||||
|
|
||||||
|
double user1 = 0.0, sys1 = 0.0;
|
||||||
|
get_rusage_times(user1, sys1);
|
||||||
|
double wall_s = std::chrono::duration<double>(
|
||||||
|
Clock::now() - scenario_wall_start_).count();
|
||||||
|
|
||||||
|
ScenarioResult res;
|
||||||
|
res.scenario = s;
|
||||||
|
res.duration_s = wall_s;
|
||||||
|
res.mem_rss_mb = measure_rss_mb();
|
||||||
|
res.cpu_pct = measure_cpu_pct(wall_s, user1 - user_start_, sys1 - sys_start_);
|
||||||
|
|
||||||
|
if (!frame_fps_.empty()) {
|
||||||
|
std::sort(frame_fps_.begin(), frame_fps_.end());
|
||||||
|
size_t n = frame_fps_.size();
|
||||||
|
res.fps_p50 = frame_fps_[n / 2];
|
||||||
|
size_t p1i = n * 1 / 100;
|
||||||
|
res.fps_p1 = frame_fps_[p1i];
|
||||||
|
}
|
||||||
|
res.pass = (res.fps_p1 >= cfg_.fps_threshold);
|
||||||
|
|
||||||
|
fn_log::log_info("bench: scenario=%s frames=%zu fps_p50=%.1f fps_p1=%.1f "
|
||||||
|
"mem=%.1fMB cpu=%.1f%% %s",
|
||||||
|
scenario_name(s), frame_fps_.size(),
|
||||||
|
res.fps_p50, res.fps_p1, res.mem_rss_mb, res.cpu_pct,
|
||||||
|
res.pass ? "PASS" : "FAIL");
|
||||||
|
|
||||||
|
fprintf(stderr, "[bench] %s: fps_p50=%.1f fps_p1=%.1f mem=%.1fMB %s\n",
|
||||||
|
scenario_name(s), res.fps_p50, res.fps_p1, res.mem_rss_mb,
|
||||||
|
res.pass ? "PASS" : "FAIL");
|
||||||
|
fflush(stderr);
|
||||||
|
|
||||||
|
results_.push_back(res);
|
||||||
|
|
||||||
|
delete sc_state_;
|
||||||
|
sc_state_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// render_table_frame — render one frame for the current scenario.
|
||||||
|
// Called inside an active ImGui frame (after NewFrame, before Render).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void Runner::render_table_frame(Scenario s) {
|
||||||
|
if (!sc_state_) return;
|
||||||
|
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(1920, 1080), ImGuiCond_Always);
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||||
|
ImGui::Begin("##bench_table", nullptr,
|
||||||
|
ImGuiWindowFlags_NoDecoration |
|
||||||
|
ImGuiWindowFlags_NoNav |
|
||||||
|
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||||
|
ImGuiWindowFlags_NoMove |
|
||||||
|
ImGuiWindowFlags_NoResize);
|
||||||
|
|
||||||
|
if (s == Scenario::LinearScroll && sc_state_->ti.rows > 0) {
|
||||||
|
float row_h = ImGui::GetTextLineHeightWithSpacing();
|
||||||
|
ImGui::SetScrollY(static_cast<float>(scroll_row_) * row_h);
|
||||||
|
scroll_row_ = (scroll_row_ + 1) % sc_state_->ti.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
data_table::render("##bench_tbl",
|
||||||
|
{sc_state_->ti}, sc_state_->st, nullptr, false);
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tick — one frame of the benchmark state machine.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
bool Runner::tick() {
|
||||||
|
if (done_) return true;
|
||||||
|
|
||||||
|
Scenario s = kScenarios[current_scenario_];
|
||||||
|
auto now = Clock::now();
|
||||||
|
|
||||||
|
// Start new scenario on first tick or after transition.
|
||||||
|
if (!scenario_started_) {
|
||||||
|
begin_scenario(s);
|
||||||
|
scenario_started_ = true;
|
||||||
|
last_frame_start_ = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure frame time from last tick entry.
|
||||||
|
double frame_ms = std::chrono::duration<double, std::milli>(
|
||||||
|
now - last_frame_start_).count();
|
||||||
|
last_frame_start_ = now;
|
||||||
|
|
||||||
|
// Record FPS (skip the very first frame — it includes scenario setup time).
|
||||||
|
if (frame_ms > 0.01 && results_.size() == static_cast<size_t>(current_scenario_)) {
|
||||||
|
frame_fps_.push_back(1000.0 / frame_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render table for this frame.
|
||||||
|
render_table_frame(s);
|
||||||
|
|
||||||
|
// Check if scenario duration has elapsed.
|
||||||
|
double elapsed = std::chrono::duration<double>(
|
||||||
|
Clock::now() - scenario_wall_start_).count();
|
||||||
|
|
||||||
|
if (elapsed >= cfg_.duration_s) {
|
||||||
|
finish_scenario();
|
||||||
|
current_scenario_++;
|
||||||
|
scenario_started_ = false;
|
||||||
|
|
||||||
|
if (current_scenario_ >= kNumScenarios) {
|
||||||
|
done_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Accessors
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
bool Runner::all_passed() const {
|
||||||
|
if (results_.empty()) return false;
|
||||||
|
for (const auto& r : results_)
|
||||||
|
if (!r.pass) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JSON output
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void Runner::print_json() const {
|
||||||
|
printf("{\n");
|
||||||
|
printf(" \"rows\": %lld,\n", cfg_.rows);
|
||||||
|
printf(" \"fps_threshold\": %.1f,\n", cfg_.fps_threshold);
|
||||||
|
printf(" \"scenarios\": [\n");
|
||||||
|
for (size_t i = 0; i < results_.size(); ++i) {
|
||||||
|
const auto& r = results_[i];
|
||||||
|
printf(" {\n");
|
||||||
|
printf(" \"scenario\": \"%s\",\n", scenario_name(r.scenario));
|
||||||
|
printf(" \"fps_p50\": %.2f,\n", r.fps_p50);
|
||||||
|
printf(" \"fps_p1\": %.2f,\n", r.fps_p1);
|
||||||
|
printf(" \"mem_rss_mb\": %.1f,\n", r.mem_rss_mb);
|
||||||
|
printf(" \"cpu_pct\": %.1f,\n", r.cpu_pct);
|
||||||
|
printf(" \"duration_s\": %.2f,\n", r.duration_s);
|
||||||
|
printf(" \"pass\": %s\n", r.pass ? "true" : "false");
|
||||||
|
printf(" }%s\n", (i + 1 < results_.size()) ? "," : "");
|
||||||
|
}
|
||||||
|
printf(" ],\n");
|
||||||
|
printf(" \"overall_pass\": %s\n", all_passed() ? "true" : "false");
|
||||||
|
printf("}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SQLite persistence
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static const char* kCreateSQL =
|
||||||
|
"CREATE TABLE IF NOT EXISTS bench_runs ("
|
||||||
|
" id TEXT PRIMARY KEY,"
|
||||||
|
" started_at INTEGER NOT NULL,"
|
||||||
|
" rows INTEGER NOT NULL,"
|
||||||
|
" scenario TEXT NOT NULL,"
|
||||||
|
" fps_p50 REAL NOT NULL,"
|
||||||
|
" fps_p1 REAL NOT NULL,"
|
||||||
|
" mem_rss_mb REAL NOT NULL,"
|
||||||
|
" cpu_pct REAL NOT NULL,"
|
||||||
|
" duration_s REAL NOT NULL,"
|
||||||
|
" commit_sha TEXT NOT NULL DEFAULT '',"
|
||||||
|
" summary_json TEXT NOT NULL DEFAULT '{}'"
|
||||||
|
");";
|
||||||
|
|
||||||
|
void Runner::persist(const std::string& db_path) const {
|
||||||
|
if (db_path.empty()) return;
|
||||||
|
|
||||||
|
sqlite3* db = nullptr;
|
||||||
|
if (sqlite3_open(db_path.c_str(), &db) != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "[bench] sqlite3_open(%s) failed: %s\n",
|
||||||
|
db_path.c_str(), sqlite3_errmsg(db));
|
||||||
|
if (db) sqlite3_close(db);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_exec(db, "PRAGMA journal_mode=WAL;", nullptr, nullptr, nullptr);
|
||||||
|
sqlite3_exec(db, kCreateSQL, nullptr, nullptr, nullptr);
|
||||||
|
|
||||||
|
auto now_ts = static_cast<long long>(
|
||||||
|
std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
|
std::chrono::system_clock::now().time_since_epoch()).count());
|
||||||
|
|
||||||
|
const char* kInsert =
|
||||||
|
"INSERT OR REPLACE INTO bench_runs "
|
||||||
|
"(id, started_at, rows, scenario, fps_p50, fps_p1, mem_rss_mb, cpu_pct, "
|
||||||
|
" duration_s, commit_sha, summary_json) "
|
||||||
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?);";
|
||||||
|
|
||||||
|
for (const auto& r : results_) {
|
||||||
|
char id_buf[128];
|
||||||
|
snprintf(id_buf, sizeof(id_buf), "%lld_%s", now_ts, scenario_name(r.scenario));
|
||||||
|
|
||||||
|
char json_buf[256];
|
||||||
|
snprintf(json_buf, sizeof(json_buf),
|
||||||
|
"{\"fps_p50\":%.2f,\"fps_p1\":%.2f,\"mem_rss_mb\":%.1f,"
|
||||||
|
"\"cpu_pct\":%.1f,\"pass\":%s}",
|
||||||
|
r.fps_p50, r.fps_p1, r.mem_rss_mb, r.cpu_pct,
|
||||||
|
r.pass ? "true" : "false");
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt = nullptr;
|
||||||
|
sqlite3_prepare_v2(db, kInsert, -1, &stmt, nullptr);
|
||||||
|
sqlite3_bind_text (stmt, 1, id_buf, -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_int64 (stmt, 2, now_ts);
|
||||||
|
sqlite3_bind_int64 (stmt, 3, cfg_.rows);
|
||||||
|
sqlite3_bind_text (stmt, 4, scenario_name(r.scenario), -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_double(stmt, 5, r.fps_p50);
|
||||||
|
sqlite3_bind_double(stmt, 6, r.fps_p1);
|
||||||
|
sqlite3_bind_double(stmt, 7, r.mem_rss_mb);
|
||||||
|
sqlite3_bind_double(stmt, 8, r.cpu_pct);
|
||||||
|
sqlite3_bind_double(stmt, 9, r.duration_s);
|
||||||
|
sqlite3_bind_text (stmt, 10, cfg_.commit_sha.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text (stmt, 11, json_buf, -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_close(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace bench
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
#pragma once
|
||||||
|
// bench_runner — headless benchmark harness for data_table::render().
|
||||||
|
// Issue 0133: perf gate for 10M-row refactor.
|
||||||
|
//
|
||||||
|
// Architecture: the Runner is driven frame-by-frame from within fn::run_app's
|
||||||
|
// render callback. Each frame it calls data_table::render() for the current
|
||||||
|
// scenario and measures the frame time. After duration_s seconds it moves to
|
||||||
|
// the next scenario. After all 4 scenarios it signals completion.
|
||||||
|
//
|
||||||
|
// Usage in render callback:
|
||||||
|
// static bench::Runner runner(cfg);
|
||||||
|
// runner.tick(); // called every frame
|
||||||
|
// if (runner.done()) { ... print + persist + close window ... }
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace bench {
|
||||||
|
|
||||||
|
enum class Scenario {
|
||||||
|
LinearScroll, // advance row offset each frame
|
||||||
|
FilterLike, // filter name CONTAINS "foo"
|
||||||
|
SortNumeric, // sort score col descending
|
||||||
|
ColorRule, // NumericRange color rule on value col
|
||||||
|
};
|
||||||
|
|
||||||
|
inline const char* scenario_name(Scenario s) {
|
||||||
|
switch (s) {
|
||||||
|
case Scenario::LinearScroll: return "linear_scroll";
|
||||||
|
case Scenario::FilterLike: return "filter_like";
|
||||||
|
case Scenario::SortNumeric: return "sort_numeric";
|
||||||
|
case Scenario::ColorRule: return "color_rule";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScenarioResult {
|
||||||
|
Scenario scenario = Scenario::LinearScroll;
|
||||||
|
double fps_p50 = 0.0;
|
||||||
|
double fps_p1 = 0.0;
|
||||||
|
double mem_rss_mb = 0.0;
|
||||||
|
double cpu_pct = 0.0;
|
||||||
|
double duration_s = 0.0;
|
||||||
|
bool pass = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
long long rows = 10000000;
|
||||||
|
int cols = 20;
|
||||||
|
double duration_s = 30.0;
|
||||||
|
double fps_threshold = 60.0;
|
||||||
|
std::string db_path = "operations.db";
|
||||||
|
std::string commit_sha;
|
||||||
|
bool verbose = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Runner {
|
||||||
|
public:
|
||||||
|
explicit Runner(const Config& cfg);
|
||||||
|
|
||||||
|
// Called once per render frame. Internally manages scenario transitions.
|
||||||
|
// Returns true when all scenarios are finished.
|
||||||
|
bool tick();
|
||||||
|
|
||||||
|
// Returns true when all scenarios have completed.
|
||||||
|
bool done() const { return done_; }
|
||||||
|
|
||||||
|
// Print JSON summary to stdout.
|
||||||
|
void print_json() const;
|
||||||
|
|
||||||
|
// Persist results into bench_runs SQLite table.
|
||||||
|
void persist(const std::string& db_path) const;
|
||||||
|
|
||||||
|
// Returns false if any scenario failed fps_threshold assertion.
|
||||||
|
bool all_passed() const;
|
||||||
|
|
||||||
|
const std::vector<ScenarioResult>& results() const { return results_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Config cfg_;
|
||||||
|
std::vector<ScenarioResult> results_;
|
||||||
|
bool done_ = false;
|
||||||
|
|
||||||
|
// Backing storage for synthetic dataset (row-major strings).
|
||||||
|
std::vector<std::string> backing_;
|
||||||
|
std::vector<const char*> ptrs_;
|
||||||
|
|
||||||
|
// Per-scenario timing state.
|
||||||
|
int current_scenario_ = 0;
|
||||||
|
bool scenario_started_ = false;
|
||||||
|
|
||||||
|
// Frame timing for current scenario.
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
Clock::time_point scenario_wall_start_;
|
||||||
|
Clock::time_point last_frame_start_;
|
||||||
|
std::vector<double> frame_fps_;
|
||||||
|
double user_start_ = 0.0;
|
||||||
|
double sys_start_ = 0.0;
|
||||||
|
|
||||||
|
// Scenario-specific state (rebuilt each scenario).
|
||||||
|
struct ScenarioState;
|
||||||
|
ScenarioState* sc_state_ = nullptr;
|
||||||
|
|
||||||
|
int scroll_row_ = 0;
|
||||||
|
|
||||||
|
static constexpr Scenario kScenarios[] = {
|
||||||
|
Scenario::LinearScroll,
|
||||||
|
Scenario::FilterLike,
|
||||||
|
Scenario::SortNumeric,
|
||||||
|
Scenario::ColorRule,
|
||||||
|
};
|
||||||
|
static constexpr int kNumScenarios = 4;
|
||||||
|
|
||||||
|
void seed_dataset();
|
||||||
|
void begin_scenario(Scenario s);
|
||||||
|
void finish_scenario();
|
||||||
|
void render_table_frame(Scenario s);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Measure RSS memory in MB (Linux: /proc/self/status).
|
||||||
|
double measure_rss_mb();
|
||||||
|
|
||||||
|
// Measure CPU % from RUSAGE delta over wall time.
|
||||||
|
double measure_cpu_pct(double wall_s, double user_s, double sys_s);
|
||||||
|
|
||||||
|
} // namespace bench
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[2026-05-22 23:37:25.548] [INFO] app start: data_table_bench
|
||||||
|
[2026-05-22 23:37:30.625] [INFO] bench: scenario=linear_scroll frames=512 fps_p50=104.1 fps_p1=84.7 mem=124.8MB cpu=763.8% PASS
|
||||||
|
[2026-05-22 23:37:35.637] [INFO] bench: scenario=filter_like frames=757 fps_p50=153.2 fps_p1=125.1 mem=124.8MB cpu=1096.1% PASS
|
||||||
|
[2026-05-22 23:37:40.654] [INFO] bench: scenario=sort_numeric frames=273 fps_p50=54.9 fps_p1=48.6 mem=125.1MB cpu=452.1% FAIL
|
||||||
|
[2026-05-22 23:37:45.662] [INFO] bench: scenario=color_rule frames=522 fps_p50=105.3 fps_p1=93.0 mem=125.1MB cpu=787.5% PASS
|
||||||
|
[2026-05-22 23:37:45.667] [INFO] app exit
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
// data_table_bench — headless performance gate for data_table::render().
|
||||||
|
// Issue 0133: 10M-row bench for the upcoming data_table refactor.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// ./data_table_bench --rows 10000000 --duration 30
|
||||||
|
// ./data_table_bench --rows 100000 --duration 5 # CI smoke (fast)
|
||||||
|
// ./data_table_bench --rows 1000000 --duration 10 --no-db
|
||||||
|
//
|
||||||
|
// Output: JSON to stdout.
|
||||||
|
// Exit 0 if all scenarios pass (fps_p1 >= threshold), 1 otherwise.
|
||||||
|
//
|
||||||
|
// Headless strategy:
|
||||||
|
// fn::run_app creates the GL context (needed for ImGui clipper to work
|
||||||
|
// on 10M-row virtual scroll). We hide the GLFW window via glfwHideWindow()
|
||||||
|
// on the first frame. The Runner::tick() is called each frame to drive the
|
||||||
|
// benchmark state machine across all 4 scenarios. When tick() returns true
|
||||||
|
// (all done) we close the window and fn::run_app exits.
|
||||||
|
|
||||||
|
#include "app_base.h"
|
||||||
|
#include "core/logger.h"
|
||||||
|
#include "bench_runner.h"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <GLFW/glfw3.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Argument parsing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void print_usage() {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage: data_table_bench [options]\n"
|
||||||
|
" --rows N Number of rows (default: 10000000)\n"
|
||||||
|
" --duration S Seconds per scenario (default: 30)\n"
|
||||||
|
" --threshold F fps_p1 pass threshold (default: 60.0)\n"
|
||||||
|
" --db PATH Persist results to SQLite (default: operations.db)\n"
|
||||||
|
" --no-db Skip persistence\n"
|
||||||
|
" --sha SHA Git commit SHA to tag run\n"
|
||||||
|
" --verbose Extra logging\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Args {
|
||||||
|
long long rows = 10000000;
|
||||||
|
double duration_s = 30.0;
|
||||||
|
double threshold = 60.0;
|
||||||
|
std::string db_path = "operations.db";
|
||||||
|
std::string commit_sha;
|
||||||
|
bool verbose = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static Args parse_args(int argc, char** argv) {
|
||||||
|
Args a;
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
if (strcmp(argv[i], "--rows") == 0 && i + 1 < argc)
|
||||||
|
a.rows = atoll(argv[++i]);
|
||||||
|
else if (strcmp(argv[i], "--duration") == 0 && i + 1 < argc)
|
||||||
|
a.duration_s = atof(argv[++i]);
|
||||||
|
else if (strcmp(argv[i], "--threshold") == 0 && i + 1 < argc)
|
||||||
|
a.threshold = atof(argv[++i]);
|
||||||
|
else if (strcmp(argv[i], "--db") == 0 && i + 1 < argc)
|
||||||
|
a.db_path = argv[++i];
|
||||||
|
else if (strcmp(argv[i], "--no-db") == 0)
|
||||||
|
a.db_path = "";
|
||||||
|
else if (strcmp(argv[i], "--sha") == 0 && i + 1 < argc)
|
||||||
|
a.commit_sha = argv[++i];
|
||||||
|
else if (strcmp(argv[i], "--verbose") == 0)
|
||||||
|
a.verbose = true;
|
||||||
|
else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
||||||
|
print_usage();
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// main
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
Args args = parse_args(argc, argv);
|
||||||
|
|
||||||
|
fprintf(stderr, "[bench] rows=%lld duration=%.0fs threshold=%.0f db=%s\n",
|
||||||
|
args.rows, args.duration_s, args.threshold,
|
||||||
|
args.db_path.empty() ? "(none)" : args.db_path.c_str());
|
||||||
|
|
||||||
|
bench::Config cfg;
|
||||||
|
cfg.rows = args.rows;
|
||||||
|
cfg.duration_s = args.duration_s;
|
||||||
|
cfg.fps_threshold = args.threshold;
|
||||||
|
cfg.db_path = args.db_path;
|
||||||
|
cfg.commit_sha = args.commit_sha;
|
||||||
|
cfg.verbose = args.verbose;
|
||||||
|
|
||||||
|
// Seed dataset before opening GL context — can take several seconds for 10M.
|
||||||
|
fprintf(stderr, "[bench] seeding %lld x %d dataset... ", cfg.rows, cfg.cols);
|
||||||
|
fflush(stderr);
|
||||||
|
bench::Runner runner(cfg);
|
||||||
|
fprintf(stderr, "done\n");
|
||||||
|
fflush(stderr);
|
||||||
|
|
||||||
|
// fn::run_app config — headless visual setup.
|
||||||
|
fn::AppConfig app_cfg;
|
||||||
|
app_cfg.title = "data_table_bench";
|
||||||
|
app_cfg.width = 1920;
|
||||||
|
app_cfg.height = 1080;
|
||||||
|
app_cfg.vsync = false; // uncapped FPS for meaningful bench
|
||||||
|
app_cfg.viewports = false; // no floating panels
|
||||||
|
app_cfg.auto_dockspace = false; // no dockspace overhead
|
||||||
|
app_cfg.auto_layouts = false; // no layout DB
|
||||||
|
|
||||||
|
app_cfg.about.name = "data_table_bench";
|
||||||
|
app_cfg.about.version = "0.1.0";
|
||||||
|
app_cfg.about.description = "Headless 10M-row bench for data_table (issue 0133)";
|
||||||
|
app_cfg.log = {"data_table_bench.log", 1};
|
||||||
|
app_cfg.header_badge.enabled = false; // headless — no badge
|
||||||
|
|
||||||
|
static GLFWwindow* g_window = nullptr;
|
||||||
|
bool window_hidden = false;
|
||||||
|
|
||||||
|
int rc = fn::run_app(app_cfg, [&]() {
|
||||||
|
// First frame: grab window handle and hide.
|
||||||
|
if (!g_window) {
|
||||||
|
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
|
if (vp && vp->PlatformHandle) {
|
||||||
|
g_window = (GLFWwindow*)vp->PlatformHandle;
|
||||||
|
glfwHideWindow(g_window);
|
||||||
|
window_hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tick() drives the benchmark state machine, renders the table,
|
||||||
|
// and returns true when all 4 scenarios are complete.
|
||||||
|
bool finished = runner.tick();
|
||||||
|
if (finished && g_window) {
|
||||||
|
glfwSetWindowShouldClose(g_window, GLFW_TRUE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rc != 0) {
|
||||||
|
fprintf(stderr, "[bench] run_app error: %d\n", rc);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print JSON to stdout.
|
||||||
|
runner.print_json();
|
||||||
|
|
||||||
|
// Persist to SQLite.
|
||||||
|
if (!args.db_path.empty()) {
|
||||||
|
runner.persist(args.db_path);
|
||||||
|
fprintf(stderr, "[bench] results persisted to %s\n", args.db_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool passed = runner.all_passed();
|
||||||
|
fprintf(stderr, "[bench] overall: %s\n", passed ? "PASS" : "FAIL");
|
||||||
|
return passed ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- data_table_bench: schema inicial
|
||||||
|
-- Tabla bench_runs: persiste resultados de cada ejecucion del benchmark.
|
||||||
|
CREATE TABLE IF NOT EXISTS bench_runs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
started_at INTEGER NOT NULL,
|
||||||
|
rows INTEGER NOT NULL,
|
||||||
|
scenario TEXT NOT NULL,
|
||||||
|
fps_p50 REAL NOT NULL,
|
||||||
|
fps_p1 REAL NOT NULL,
|
||||||
|
mem_rss_mb REAL NOT NULL,
|
||||||
|
cpu_pct REAL NOT NULL,
|
||||||
|
duration_s REAL NOT NULL,
|
||||||
|
commit_sha TEXT NOT NULL DEFAULT '',
|
||||||
|
summary_json TEXT NOT NULL DEFAULT '{}'
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user