// perf_tests — stress data_table con N filas (default 1M). // // Patron: el boton "Performance Tests" del QA panel llama run_perf_test(rows, frames), // que prepara el backing storage sync (lento si N grande) y marca el state como // "perf mode ON". Durante los siguientes `frames` renders, render_perf_tab() // mide latency por frame y al terminar deja resultados en qa::counters(). #include "perf_tests.h" #include "qa_state.h" #include "data_table/data_table.h" #include "core/logger.h" #include #include #include #include #include #include namespace tables_qa { namespace { // Estado estatico del perf test. Sobrevive entre frames hasta nueva run. struct PerfState { bool active = false; long long rows = 0; int frames_target = 0; int frames_measured = 0; std::vector frame_times_ms; std::vector back; // backing — puede ser N millones std::vector ptrs; data_table::State dt; }; PerfState& perf() { static PerfState s; return s; } // Generador deterministico — mismo seed produce mismas filas. void seed_synthetic(long long rows) { auto& p = perf(); p.back.clear(); // Para N filas con 6 columnas, necesitamos rows*6 strings. // Reservamos para evitar reallocs durante el push_back. p.back.reserve(static_cast(rows * 6)); char buf[64]; const char* statuses[] = {"ok", "error", "running", "pending"}; const char* langs[] = {"go", "py", "cpp"}; const int n_statuses = 4; const int n_langs = 3; for (long long i = 0; i < rows; ++i) { // id std::snprintf(buf, sizeof(buf), "%lld", i + 1); p.back.emplace_back(buf); // name std::snprintf(buf, sizeof(buf), "item_%lld", i); p.back.emplace_back(buf); // status p.back.emplace_back(statuses[i % n_statuses]); // duration_ms — varia 50..7000 std::snprintf(buf, sizeof(buf), "%lld", 50 + (i * 137) % 6950); p.back.emplace_back(buf); // lang p.back.emplace_back(langs[i % n_langs]); // value 0..10000 std::snprintf(buf, sizeof(buf), "%lld", (i * 71) % 10000); p.back.emplace_back(buf); } qa::TabState dummy; p.ptrs.clear(); p.ptrs.reserve(p.back.size()); for (auto& s : p.back) p.ptrs.push_back(s.c_str()); } } // anon void run_perf_test(long long rows, int frames) { fn_log::log_info("perf_test: starting rows=%lld frames=%d", rows, frames); auto& p = perf(); p.active = true; p.rows = rows; p.frames_target = frames; p.frames_measured = 0; p.frame_times_ms.clear(); p.frame_times_ms.reserve(static_cast(frames)); // Seed sync. Para 10M filas tarda varios segundos. auto t0 = std::chrono::steady_clock::now(); seed_synthetic(rows); auto t1 = std::chrono::steady_clock::now(); double seed_ms = std::chrono::duration(t1 - t0).count(); auto& c = qa::counters(); c.last_perf_rows = rows; c.last_perf_seed_ms = seed_ms; c.last_perf_frames = 0; c.last_perf_render_ms_p50 = 0.0; c.last_perf_render_ms_p95 = 0.0; fn_log::log_info("perf_test: seeded rows=%lld in %.2f ms", rows, seed_ms); } void render_perf_tab() { auto& p = perf(); if (!p.active) { ImGui::TextDisabled("No perf test active."); ImGui::TextDisabled("Open QA Control Panel and click " "Performance Tests" " to start."); return; } // KPI header auto& c = qa::counters(); ImGui::Text("Active perf: rows=%lld seed=%.1fms frames_done=%d/%d", p.rows, c.last_perf_seed_ms, p.frames_measured, p.frames_target); if (p.frames_measured > 0) { ImGui::SameLine(); ImGui::Text(" p50=%.2fms p95=%.2fms", c.last_perf_render_ms_p50, c.last_perf_render_ms_p95); } ImGui::Separator(); // Construir TableInput data_table::TableInput tbl; tbl.name = "perf"; tbl.headers = {"id", "name", "status", "duration_ms", "lang", "value"}; tbl.types = { data_table::ColumnType::Int, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::Float, data_table::ColumnType::String, data_table::ColumnType::Float, }; tbl.cells = p.ptrs.data(); tbl.rows = static_cast(p.rows); tbl.cols = 6; // Renderers — ejercemos los caros (CategoricalChip + ColorScale + Duration). tbl.column_specs.resize(tbl.cols); for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i]; tbl.column_specs[2].renderer = data_table::CellRenderer::CategoricalChip; tbl.column_specs[2].chips = { {"ok", "#22c55e"}, {"error", "#ef4444"}, {"running", "#f59e0b"}, {"pending", "#a3a3a3"}, }; tbl.column_specs[3].renderer = data_table::CellRenderer::Duration; tbl.column_specs[3].duration_warn_ms = 1000.0f; tbl.column_specs[3].duration_error_ms = 5000.0f; tbl.column_specs[4].renderer = data_table::CellRenderer::CategoricalChip; tbl.column_specs[4].chips = { {"go", "#3b82f6"}, {"py", "#22c55e"}, {"cpp", "#a855f7"}, }; tbl.column_specs[5].renderer = data_table::CellRenderer::ColorScale; tbl.column_specs[5].range_min = 0.0; tbl.column_specs[5].range_max = 10000.0; tbl.column_specs[5].range_alpha = 0.30f; // Medir frame time ImGui::BeginChild("##perf_host", ImVec2(-1, -1)); auto t0 = std::chrono::steady_clock::now(); data_table::render("##perf_tbl", {tbl}, p.dt, nullptr, true); auto t1 = std::chrono::steady_clock::now(); ImGui::EndChild(); if (p.frames_measured < p.frames_target) { double ms = std::chrono::duration(t1 - t0).count(); p.frame_times_ms.push_back(ms); p.frames_measured++; if (p.frames_measured == p.frames_target) { // Compute p50 / p95 std::vector sorted = p.frame_times_ms; std::sort(sorted.begin(), sorted.end()); size_t n = sorted.size(); double p50 = sorted[n / 2]; double p95 = sorted[(n * 95) / 100]; c.last_perf_render_ms_p50 = p50; c.last_perf_render_ms_p95 = p95; c.last_perf_frames = p.frames_measured; fn_log::log_info("perf_test DONE rows=%lld frames=%d p50=%.2fms p95=%.2fms", p.rows, p.frames_measured, p50, p95); } } } } // namespace tables_qa