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()); +}