Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/ui/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint32_t>& candidates, const std::vector<TraceEvent>& events, const std::unordered_set<uint32_t>& 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
Expand Down
41 changes: 24 additions & 17 deletions src/ui/timeline_view.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint32_t>& candidates,
const std::vector<TraceEvent>& events,
const std::unordered_set<uint32_t>& 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");
Expand Down Expand Up @@ -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<uint32_t> 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;
}
Expand Down
8 changes: 8 additions & 0 deletions src/ui/timeline_view.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint32_t>& candidates, const std::vector<TraceEvent>& events,
const std::unordered_set<uint32_t>& hidden_cats, int clicked_depth,
double click_time, double tolerance);

private:
struct TrackLayout {
uint32_t pid;
uint32_t tid;
Expand Down
129 changes: 129 additions & 0 deletions tests/test_timeline_hit_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#include <gtest/gtest.h>
#include "ui/timeline_view.h"

// Helper to build a minimal set of events for hit-test candidate selection.
static std::vector<TraceEvent> make_events(uint32_t visible_cat, uint32_t hidden_cat) {
std::vector<TraceEvent> 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<uint32_t> candidates = {0, 1};
std::unordered_set<uint32_t> 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<uint32_t> candidates = {1};
std::unordered_set<uint32_t> 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<uint32_t> candidates = {0, 1};
std::unordered_set<uint32_t> 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<uint32_t> candidates = {2};
std::unordered_set<uint32_t> 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<uint32_t> candidates = {0};
std::unordered_set<uint32_t> 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<uint32_t> candidates = {0};
std::unordered_set<uint32_t> 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<TraceEvent> events;
std::vector<uint32_t> candidates;
std::unordered_set<uint32_t> 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);
}
Loading