diff --git a/src/ui/INDEX.md b/src/ui/INDEX.md index 868c66b..528d9d5 100644 --- a/src/ui/INDEX.md +++ b/src/ui/INDEX.md @@ -62,6 +62,7 @@ void navigate_to_event(int32_t ev_idx, const TraceEvent& ev, double pad_factor = ``` void render(const TraceModel&, ViewState&); DiagStats diag_stats; // written each frame, read by DiagnosticsPanel +static int32_t select_best_candidate(const std::vector& candidates, const std::vector& events, const std::unordered_set& hidden_cats, int clicked_depth, double click_time, double tolerance); ``` ## detail_panel.h / detail_panel.cpp — selected-event details: timing, args, call stack, children table, range summary diff --git a/src/ui/timeline_view.cpp b/src/ui/timeline_view.cpp index a6fd87d..a17a828 100644 --- a/src/ui/timeline_view.cpp +++ b/src/ui/timeline_view.cpp @@ -359,6 +359,28 @@ void TimelineView::render_tracks(ImDrawList* dl, ImVec2 area_min, ImVec2 area_ma total_content_height_ = (y + scroll_y_) - (area_min.y + ruler_height); } +int32_t TimelineView::select_best_candidate(const std::vector& candidates, + const std::vector& events, + const std::unordered_set& hidden_cats, int clicked_depth, + double click_time, double tolerance) { + int32_t best = -1; + double best_dur = 1e18; + const bool has_hidden_cats = !hidden_cats.empty(); + for (uint32_t idx : candidates) { + const auto& ev = events[idx]; + if (ev.is_end_event) continue; + if (has_hidden_cats && hidden_cats.count(ev.cat_idx)) continue; + if (ev.depth != clicked_depth) continue; + if (click_time >= ev.ts - tolerance && click_time <= ev.end_ts() + tolerance) { + if (ev.dur < best_dur) { + best_dur = ev.dur; + best = (int32_t)idx; + } + } + } + return best; +} + int32_t TimelineView::hit_test(float click_x, float click_y, ImVec2 area_min, ImVec2 area_max, const TraceModel& model, const ViewState& view) { TRACE_FUNCTION_CAT("ui"); @@ -387,26 +409,11 @@ int32_t TimelineView::hit_test(float click_x, float click_y, ImVec2 area_min, Im double time_per_px = (view.view_end_ts() - view.view_start_ts()) / (double)track_width; double tolerance = px_tolerance * time_per_px; - // Find the best matching event at this depth and time - int32_t best = -1; - double best_dur = 1e18; - std::vector candidates; model.query_visible(thread, click_time - tolerance, click_time + tolerance, candidates); - for (uint32_t idx : candidates) { - const auto& ev = model.events()[idx]; - if (ev.is_end_event) continue; - if (ev.depth != clicked_depth) continue; - if (click_time >= ev.ts - tolerance && click_time <= ev.end_ts() + tolerance) { - if (ev.dur < best_dur) { - best_dur = ev.dur; - best = (int32_t)idx; - } - } - } - - return best; + return select_best_candidate(candidates, model.events(), view.hidden_cats(), clicked_depth, click_time, + tolerance); } return -1; } diff --git a/src/ui/timeline_view.h b/src/ui/timeline_view.h index 1dca47e..b8db4ab 100644 --- a/src/ui/timeline_view.h +++ b/src/ui/timeline_view.h @@ -33,6 +33,14 @@ class TimelineView { int32_t hit_test(float click_x, float click_y, ImVec2 area_min, ImVec2 area_max, const TraceModel& model, const ViewState& view); +public: + // Select the best (shortest-duration) event from candidates at the given depth/time. + // Skips end events and events whose category is hidden. Exposed for testing. + static int32_t select_best_candidate(const std::vector& candidates, const std::vector& events, + const std::unordered_set& hidden_cats, int clicked_depth, + double click_time, double tolerance); + +private: struct TrackLayout { uint32_t pid; uint32_t tid; diff --git a/tests/test_timeline_hit_test.cpp b/tests/test_timeline_hit_test.cpp new file mode 100644 index 0000000..69b004c --- /dev/null +++ b/tests/test_timeline_hit_test.cpp @@ -0,0 +1,129 @@ +#include +#include "ui/timeline_view.h" + +// Helper to build a minimal set of events for hit-test candidate selection. +static std::vector make_events(uint32_t visible_cat, uint32_t hidden_cat) { + std::vector events; + + // Event 0: visible category, depth 0, ts=100 dur=200 + TraceEvent e0; + e0.ts = 100.0; + e0.dur = 200.0; + e0.depth = 0; + e0.cat_idx = visible_cat; + e0.ph = Phase::Complete; + e0.is_end_event = false; + events.push_back(e0); + + // Event 1: hidden category, depth 0, ts=100 dur=50 (shorter — would win without filter) + TraceEvent e1; + e1.ts = 100.0; + e1.dur = 50.0; + e1.depth = 0; + e1.cat_idx = hidden_cat; + e1.ph = Phase::Complete; + e1.is_end_event = false; + events.push_back(e1); + + // Event 2: end event (should always be skipped) + TraceEvent e2; + e2.ts = 100.0; + e2.dur = 10.0; + e2.depth = 0; + e2.cat_idx = visible_cat; + e2.ph = Phase::DurationEnd; + e2.is_end_event = true; + events.push_back(e2); + + return events; +} + +TEST(TimelineHitTest, HiddenCategoryFilteredOut) { + const uint32_t visible_cat = 1; + const uint32_t hidden_cat = 2; + auto events = make_events(visible_cat, hidden_cat); + + std::vector candidates = {0, 1}; + std::unordered_set hidden_cats = {hidden_cat}; + + // Event 1 has shorter duration but is hidden — should select event 0 + int32_t result = TimelineView::select_best_candidate(candidates, events, hidden_cats, /*clicked_depth=*/0, + /*click_time=*/150.0, /*tolerance=*/5.0); + EXPECT_EQ(result, 0); +} + +TEST(TimelineHitTest, AllCandidatesHiddenReturnsNone) { + const uint32_t hidden_cat = 2; + auto events = make_events(/*visible_cat=*/1, hidden_cat); + + // Only event 1 (hidden category) as candidate + std::vector candidates = {1}; + std::unordered_set hidden_cats = {hidden_cat}; + + int32_t result = TimelineView::select_best_candidate(candidates, events, hidden_cats, /*clicked_depth=*/0, + /*click_time=*/120.0, /*tolerance=*/5.0); + EXPECT_EQ(result, -1); +} + +TEST(TimelineHitTest, NoCategoryFilterSelectsShortest) { + const uint32_t cat_a = 1; + const uint32_t cat_b = 2; + auto events = make_events(cat_a, cat_b); + + std::vector candidates = {0, 1}; + std::unordered_set hidden_cats; // empty — no filtering + + // Event 1 (dur=50) is shorter than event 0 (dur=200) — should win + int32_t result = TimelineView::select_best_candidate(candidates, events, hidden_cats, /*clicked_depth=*/0, + /*click_time=*/120.0, /*tolerance=*/5.0); + EXPECT_EQ(result, 1); +} + +TEST(TimelineHitTest, EndEventsSkipped) { + const uint32_t cat = 1; + auto events = make_events(cat, /*hidden_cat=*/99); + + // Only the end event (index 2) as candidate + std::vector candidates = {2}; + std::unordered_set hidden_cats; + + int32_t result = TimelineView::select_best_candidate(candidates, events, hidden_cats, /*clicked_depth=*/0, + /*click_time=*/105.0, /*tolerance=*/5.0); + EXPECT_EQ(result, -1); +} + +TEST(TimelineHitTest, WrongDepthSkipped) { + const uint32_t cat = 1; + auto events = make_events(cat, /*hidden_cat=*/99); + + std::vector candidates = {0}; + std::unordered_set hidden_cats; + + // Event 0 is at depth 0, but we click depth 1 + int32_t result = TimelineView::select_best_candidate(candidates, events, hidden_cats, /*clicked_depth=*/1, + /*click_time=*/150.0, /*tolerance=*/5.0); + EXPECT_EQ(result, -1); +} + +TEST(TimelineHitTest, OutOfTimeRangeSkipped) { + const uint32_t cat = 1; + auto events = make_events(cat, /*hidden_cat=*/99); + + std::vector candidates = {0}; + std::unordered_set hidden_cats; + + // Event 0 spans [100, 300]. Click at 400 with 5px tolerance — out of range. + int32_t result = TimelineView::select_best_candidate(candidates, events, hidden_cats, /*clicked_depth=*/0, + /*click_time=*/400.0, /*tolerance=*/5.0); + EXPECT_EQ(result, -1); +} + +TEST(TimelineHitTest, EmptyCandidatesReturnsNone) { + std::vector events; + std::vector candidates; + std::unordered_set hidden_cats; + + int32_t result = TimelineView::select_best_candidate(candidates, events, hidden_cats, /*clicked_depth=*/0, + /*click_time=*/100.0, /*tolerance=*/5.0); + EXPECT_EQ(result, -1); +}