From 7a2afaed5930a73092b70ce3c11fd6b86b5388a1 Mon Sep 17 00:00:00 2001 From: Mick Date: Sat, 21 Mar 2026 13:21:33 -0400 Subject: [PATCH] fix: Search Panel - Add Count and Average columns (#70) Add Count and Avg duration columns to the search results table. Each row shows how many times that event name appears in results and the average duration across all occurrences of that name. Co-Authored-By: Claude Opus 4.6 --- src/ui/INDEX.md | 5 ++- src/ui/search_panel.cpp | 48 +++++++++++++++++++- src/ui/search_panel.h | 12 +++++ tests/test_search_panel.cpp | 89 +++++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 tests/test_search_panel.cpp diff --git a/src/ui/INDEX.md b/src/ui/INDEX.md index 868c66b..b218b7b 100644 --- a/src/ui/INDEX.md +++ b/src/ui/INDEX.md @@ -70,10 +70,13 @@ void render(const TraceModel&, ViewState&); void on_model_changed(); ``` -## search_panel.h / search_panel.cpp — text search over event names; populates `ViewState::search_results` +## search_panel.h / search_panel.cpp — text search over event names; populates `ViewState::search_results`; shows per-name Count and Avg duration ``` void render(const TraceModel&, ViewState&); void on_model_changed(); +void build_name_stats(const TraceModel&, const std::vector& results); +const std::unordered_map& name_stats() const; +// NameStats: count, total_dur, avg_dur ``` ## filter_panel.h / filter_panel.cpp — hide/show processes, threads, categories via `ViewState::hidden_*` diff --git a/src/ui/search_panel.cpp b/src/ui/search_panel.cpp index 4b6e42e..47c4634 100644 --- a/src/ui/search_panel.cpp +++ b/src/ui/search_panel.cpp @@ -14,6 +14,20 @@ void SearchPanel::reset() { sorted_results_.clear(); needs_sort_ = false; scroll_to_top_ = false; + name_stats_.clear(); +} + +void SearchPanel::build_name_stats(const TraceModel& model, const std::vector& results) { + name_stats_.clear(); + for (uint32_t idx : results) { + const auto& ev = model.events()[idx]; + auto& stats = name_stats_[ev.name_idx]; + stats.count++; + stats.total_dur += ev.dur; + } + for (auto& [name_idx, stats] : name_stats_) { + stats.avg_dur = stats.count > 0 ? stats.total_dur / stats.count : 0.0; + } } void SearchPanel::on_model_changed() { @@ -56,6 +70,7 @@ void SearchPanel::render(const TraceModel& model, ViewState& view) { } } sorted_results_ = view.search_results(); + build_name_stats(model, sorted_results_); needs_sort_ = true; } @@ -87,7 +102,7 @@ void SearchPanel::render(const TraceModel& model, ViewState& view) { // Results table if (!sorted_results_.empty() && - ImGui::BeginTable("SearchResults", 3, + ImGui::BeginTable("SearchResults", 5, ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable, @@ -95,6 +110,8 @@ void SearchPanel::render(const TraceModel& model, ViewState& view) { ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_DefaultSort, 0.0f, 0); ImGui::TableSetupColumn("Duration", ImGuiTableColumnFlags_None, 0.0f, 1); + ImGui::TableSetupColumn("Count", ImGuiTableColumnFlags_None, 0.0f, 3); + ImGui::TableSetupColumn("Avg", ImGuiTableColumnFlags_None, 0.0f, 4); ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_None, 0.0f, 2); ImGui::TableHeadersRow(); @@ -126,6 +143,22 @@ void SearchPanel::render(const TraceModel& model, ViewState& view) { cmp = na.compare(nb); break; } + case 3: { // Count + auto it_a = name_stats_.find(a.name_idx); + auto it_b = name_stats_.find(b.name_idx); + uint32_t ca = it_a != name_stats_.end() ? it_a->second.count : 0; + uint32_t cb = it_b != name_stats_.end() ? it_b->second.count : 0; + cmp = sort_utils::three_way_cmp(ca, cb); + break; + } + case 4: { // Avg + auto it_a = name_stats_.find(a.name_idx); + auto it_b = name_stats_.find(b.name_idx); + double aa = it_a != name_stats_.end() ? it_a->second.avg_dur : 0.0; + double ab = it_b != name_stats_.end() ? it_b->second.avg_dur : 0.0; + cmp = sort_utils::three_way_cmp(aa, ab); + break; + } } return ascending ? (cmp < 0) : (cmp > 0); }); @@ -171,6 +204,19 @@ void SearchPanel::render(const TraceModel& model, ViewState& view) { ImGui::TableNextColumn(); ImGui::TextUnformatted(dur_buf); + ImGui::TableNextColumn(); + auto stats_it = name_stats_.find(ev.name_idx); + if (stats_it != name_stats_.end()) { + ImGui::Text("%u", stats_it->second.count); + } + + ImGui::TableNextColumn(); + if (stats_it != name_stats_.end()) { + char avg_buf[64]; + format_time(stats_it->second.avg_dur, avg_buf, sizeof(avg_buf)); + ImGui::TextUnformatted(avg_buf); + } + ImGui::TableNextColumn(); ImGui::TextUnformatted(name.c_str()); } diff --git a/src/ui/search_panel.h b/src/ui/search_panel.h index 25b5c95..7cf0265 100644 --- a/src/ui/search_panel.h +++ b/src/ui/search_panel.h @@ -1,11 +1,20 @@ #pragma once #include "model/trace_model.h" #include "ui/view_state.h" +#include + +struct NameStats { + uint32_t count = 0; + double total_dur = 0.0; + double avg_dur = 0.0; +}; class SearchPanel { public: void render(const TraceModel& model, ViewState& view); void on_model_changed(); + void build_name_stats(const TraceModel& model, const std::vector& results); + const std::unordered_map& name_stats() const { return name_stats_; } private: // NOTE: update reset() when adding cached fields @@ -17,4 +26,7 @@ class SearchPanel { std::vector sorted_results_; bool needs_sort_ = false; bool scroll_to_top_ = false; + + // Per-name aggregates (name_idx -> stats) + std::unordered_map name_stats_; }; diff --git a/tests/test_search_panel.cpp b/tests/test_search_panel.cpp new file mode 100644 index 0000000..63f1c13 --- /dev/null +++ b/tests/test_search_panel.cpp @@ -0,0 +1,89 @@ +#include +#include "ui/search_panel.h" + +static TraceModel make_search_model() { + TraceModel m; + uint32_t n_foo = m.intern_string("foo"); + uint32_t n_bar = m.intern_string("bar"); + uint32_t cat = m.intern_string("test"); + + auto& t = m.get_or_create_process(1).get_or_create_thread(1); + + auto push = [&](uint32_t name, double ts, double dur) { + TraceEvent e; + e.name_idx = name; + e.cat_idx = cat; + e.ph = Phase::Complete; + e.ts = ts; + e.dur = dur; + e.pid = 1; + e.tid = 1; + e.depth = 0; + e.parent_idx = -1; + e.self_time = dur; + t.event_indices.push_back(m.add_event(e)); + }; + + // 3 "foo" events with durations 10, 20, 30 => count=3, avg=20 + push(n_foo, 0, 10); + push(n_foo, 100, 20); + push(n_foo, 200, 30); + // 1 "bar" event with duration 50 => count=1, avg=50 + push(n_bar, 300, 50); + + m.build_index(); + return m; +} + +TEST(SearchPanel, BuildNameStatsCountAndAvg) { + TraceModel m = make_search_model(); + SearchPanel panel; + + // All 4 events + std::vector results = {0, 1, 2, 3}; + panel.build_name_stats(m, results); + + uint32_t n_foo = m.intern_string("foo"); + uint32_t n_bar = m.intern_string("bar"); + + const auto& stats = panel.name_stats(); + ASSERT_EQ(stats.size(), 2u); + + auto foo_it = stats.find(n_foo); + ASSERT_NE(foo_it, stats.end()); + EXPECT_EQ(foo_it->second.count, 3u); + EXPECT_DOUBLE_EQ(foo_it->second.avg_dur, 20.0); + + auto bar_it = stats.find(n_bar); + ASSERT_NE(bar_it, stats.end()); + EXPECT_EQ(bar_it->second.count, 1u); + EXPECT_DOUBLE_EQ(bar_it->second.avg_dur, 50.0); +} + +TEST(SearchPanel, BuildNameStatsSubsetOfResults) { + TraceModel m = make_search_model(); + SearchPanel panel; + + // Only first 2 foo events + std::vector results = {0, 1}; + panel.build_name_stats(m, results); + + uint32_t n_foo = m.intern_string("foo"); + const auto& stats = panel.name_stats(); + ASSERT_EQ(stats.size(), 1u); + + auto foo_it = stats.find(n_foo); + ASSERT_NE(foo_it, stats.end()); + EXPECT_EQ(foo_it->second.count, 2u); + EXPECT_DOUBLE_EQ(foo_it->second.avg_dur, 15.0); +} + +TEST(SearchPanel, BuildNameStatsEmpty) { + TraceModel m = make_search_model(); + SearchPanel panel; + + std::vector results = {}; + panel.build_name_stats(m, results); + + EXPECT_TRUE(panel.name_stats().empty()); +}