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..4886c1d 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,14 +86,26 @@ 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 + // 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); @@ -87,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); } } 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); +}