From c87894b771e7a36ffd101111b704770f2859d604 Mon Sep 17 00:00:00 2001 From: Mick Date: Sat, 21 Mar 2026 14:01:18 -0400 Subject: [PATCH 1/2] feat: Search Panel - Unique by name checkbox on by default (#78) Add a "Unique by name" checkbox (default on) to the search panel that filters results to show only one event per unique name. This avoids showing thousands of duplicate events when searching by name. Co-Authored-By: Claude Opus 4.6 --- src/ui/INDEX.md | 5 +++- src/ui/search_panel.cpp | 29 +++++++++++++++++++++- src/ui/search_panel.h | 9 ++++++- tests/test_search_panel.cpp | 48 +++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/ui/INDEX.md b/src/ui/INDEX.md index d068f14..117c661 100644 --- a/src/ui/INDEX.md +++ b/src/ui/INDEX.md @@ -71,12 +71,15 @@ void render(const TraceModel&, ViewState&); void on_model_changed(); ``` -## search_panel.h / search_panel.cpp — text search over event names; populates `ViewState::search_results`; shows per-name Count and Avg duration +## search_panel.h / search_panel.cpp — text search over event names; populates `ViewState::search_results`; shows per-name Count and Avg duration; "Unique by name" checkbox (default on) deduplicates results ``` 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; +bool unique_by_name() const; +void set_unique_by_name(bool); +static std::vector filter_unique_by_name(const TraceModel&, const std::vector& results); // NameStats: count, total_dur, avg_dur ``` diff --git a/src/ui/search_panel.cpp b/src/ui/search_panel.cpp index 47c4634..9dc93b8 100644 --- a/src/ui/search_panel.cpp +++ b/src/ui/search_panel.cpp @@ -6,17 +6,32 @@ #include "imgui.h" #include #include +#include void SearchPanel::reset() { TRACE_FUNCTION_CAT("ui"); search_buf_[0] = '\0'; needs_search_ = false; + unique_by_name_ = true; sorted_results_.clear(); needs_sort_ = false; scroll_to_top_ = false; name_stats_.clear(); } +std::vector SearchPanel::filter_unique_by_name(const TraceModel& model, + const std::vector& results) { + std::unordered_set seen; + std::vector filtered; + for (uint32_t idx : results) { + const auto& ev = model.events()[idx]; + if (seen.insert(ev.name_idx).second) { + filtered.push_back(idx); + } + } + return filtered; +} + void SearchPanel::build_name_stats(const TraceModel& model, const std::vector& results) { name_stats_.clear(); for (uint32_t idx : results) { @@ -71,10 +86,22 @@ void SearchPanel::render(const TraceModel& model, ViewState& view) { } sorted_results_ = view.search_results(); build_name_stats(model, sorted_results_); + if (unique_by_name_) { + sorted_results_ = filter_unique_by_name(model, sorted_results_); + } + needs_sort_ = true; + } + + if (ImGui::Checkbox("Unique by name", &unique_by_name_)) { + // Rebuild sorted_results_ from full results when toggling + sorted_results_ = view.search_results(); + if (unique_by_name_) { + sorted_results_ = filter_unique_by_name(model, sorted_results_); + } needs_sort_ = true; } - ImGui::Text("%zu results", view.search_results().size()); + ImGui::Text("%zu results", sorted_results_.size()); // Navigation bool navigate = false; diff --git a/src/ui/search_panel.h b/src/ui/search_panel.h index 7cf0265..df5646d 100644 --- a/src/ui/search_panel.h +++ b/src/ui/search_panel.h @@ -16,13 +16,20 @@ class SearchPanel { void build_name_stats(const TraceModel& model, const std::vector& results); const std::unordered_map& name_stats() const { return name_stats_; } + bool unique_by_name() const { return unique_by_name_; } + void set_unique_by_name(bool v) { unique_by_name_ = v; } + + // Filter results to keep one event per unique name (first occurrence). + static std::vector filter_unique_by_name(const TraceModel& model, const std::vector& results); + private: // NOTE: update reset() when adding cached fields void reset(); char search_buf_[256] = {}; bool needs_search_ = false; + bool unique_by_name_ = true; - // Sorted view of search results + // Sorted view of search results (may be filtered by unique_by_name_) std::vector sorted_results_; bool needs_sort_ = false; bool scroll_to_top_ = false; diff --git a/tests/test_search_panel.cpp b/tests/test_search_panel.cpp index 63f1c13..86818ab 100644 --- a/tests/test_search_panel.cpp +++ b/tests/test_search_panel.cpp @@ -87,3 +87,51 @@ TEST(SearchPanel, BuildNameStatsEmpty) { EXPECT_TRUE(panel.name_stats().empty()); } + +TEST(SearchPanel, UniqueByNameDefaultOn) { + SearchPanel panel; + EXPECT_TRUE(panel.unique_by_name()); +} + +TEST(SearchPanel, FilterUniqueByNameKeepsOnePerName) { + TraceModel m = make_search_model(); + + // All 4 events: foo(0), foo(1), foo(2), bar(3) + std::vector results = {0, 1, 2, 3}; + auto filtered = SearchPanel::filter_unique_by_name(m, results); + + // Should keep first foo (idx 0) and first bar (idx 3) + ASSERT_EQ(filtered.size(), 2u); + EXPECT_EQ(filtered[0], 0u); + EXPECT_EQ(filtered[1], 3u); +} + +TEST(SearchPanel, FilterUniqueByNameEmpty) { + TraceModel m = make_search_model(); + std::vector results = {}; + auto filtered = SearchPanel::filter_unique_by_name(m, results); + EXPECT_TRUE(filtered.empty()); +} + +TEST(SearchPanel, FilterUniqueByNameAllSameName) { + TraceModel m = make_search_model(); + + // Only foo events + std::vector results = {0, 1, 2}; + auto filtered = SearchPanel::filter_unique_by_name(m, results); + + ASSERT_EQ(filtered.size(), 1u); + EXPECT_EQ(filtered[0], 0u); +} + +TEST(SearchPanel, FilterUniqueByNameAllUnique) { + TraceModel m = make_search_model(); + + // One foo and one bar - already unique + std::vector results = {0, 3}; + auto filtered = SearchPanel::filter_unique_by_name(m, results); + + ASSERT_EQ(filtered.size(), 2u); + EXPECT_EQ(filtered[0], 0u); + EXPECT_EQ(filtered[1], 3u); +} From d3834686275a33a22667432e5f1242ccfd195222 Mon Sep 17 00:00:00 2001 From: Mick Date: Sat, 21 Mar 2026 14:04:17 -0400 Subject: [PATCH 2/2] fix: Search Panel - Navigation buttons respect unique filter (#78) Make < > navigation buttons index into sorted_results_ (which respects the unique-by-name filter) instead of the unfiltered search_results(). Co-Authored-By: Claude Opus 4.6 --- src/ui/search_panel.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/search_panel.cpp b/src/ui/search_panel.cpp index 9dc93b8..4886c1d 100644 --- a/src/ui/search_panel.cpp +++ b/src/ui/search_panel.cpp @@ -103,9 +103,9 @@ void SearchPanel::render(const TraceModel& model, ViewState& view) { ImGui::Text("%zu results", sorted_results_.size()); - // Navigation + // Navigation (indexes into sorted_results_ which respects the unique filter) bool navigate = false; - if (!view.search_results().empty()) { + if (!sorted_results_.empty()) { ImGui::SameLine(); if (ImGui::Button("<") && view.search_current() > 0) { view.set_search_current(view.search_current() - 1); @@ -114,13 +114,13 @@ void SearchPanel::render(const TraceModel& model, ViewState& view) { ImGui::SameLine(); if (ImGui::Button(">")) { view.set_search_current(view.search_current() + 1); - if (view.search_current() >= (int32_t)view.search_results().size()) - view.set_search_current((int32_t)view.search_results().size() - 1); + if (view.search_current() >= (int32_t)sorted_results_.size()) + view.set_search_current((int32_t)sorted_results_.size() - 1); navigate = true; } - if (navigate && view.search_current() >= 0 && view.search_current() < (int32_t)view.search_results().size()) { - uint32_t ev_idx = view.search_results()[view.search_current()]; + if (navigate && view.search_current() >= 0 && view.search_current() < (int32_t)sorted_results_.size()) { + uint32_t ev_idx = sorted_results_[view.search_current()]; view.navigate_to_event(ev_idx, model.events()[ev_idx], 2.0, 1000.0); } }