diff --git a/frontend/app/components/trace_viewer_v2/application.cc b/frontend/app/components/trace_viewer_v2/application.cc index e0eba5b7..8b9f21b9 100644 --- a/frontend/app/components/trace_viewer_v2/application.cc +++ b/frontend/app/components/trace_viewer_v2/application.cc @@ -7,6 +7,7 @@ #include #include +#include #include "absl/time/clock.h" #include "absl/time/time.h" @@ -27,6 +28,15 @@ const char* const kWindowTarget = EMSCRIPTEN_EVENT_TARGET_WINDOW; const char* const kCanvasTarget = "#canvas"; constexpr float kScrollbarSize = 10.0f; +void ApplyDefaultTraceViewerStyles() { + ImGuiStyle& style = ImGui::GetStyle(); + style.ScrollbarSize = kScrollbarSize; + style.WindowRounding = 0.0f; + style.WindowPadding = ImVec2(0.0f, 0.0f); + style.CellPadding = ImVec2(0.0f, 0.0f); + style.ItemSpacing = ImVec2(0.0f, 0.0f); +} + void ApplyLightTheme() { ImGui::StyleColorsLight(); ImGuiStyle& style = ImGui::GetStyle(); @@ -57,7 +67,7 @@ void Application::Initialize() { fonts::LoadFonts(initial_canvas_state.device_pixel_ratio()); // TODO: b/450584482 - Add a dark theme for the timeline. ApplyLightTheme(); - ImGui::GetStyle().ScrollbarSize = kScrollbarSize; + ApplyDefaultTraceViewerStyles(); // Initialize the platform platform_ = std::make_unique(); diff --git a/frontend/app/components/trace_viewer_v2/color/colors.h b/frontend/app/components/trace_viewer_v2/color/colors.h index dbfbd206..7d1fd61b 100644 --- a/frontend/app/components/trace_viewer_v2/color/colors.h +++ b/frontend/app/components/trace_viewer_v2/color/colors.h @@ -24,14 +24,6 @@ inline constexpr ImU32 kPurple70 = 0xFFFF97C5; inline constexpr ImU32 kYellow90 = 0xFF7CE0FF; // go/keep-sorted end -// Baseline palette: -// go/keep-sorted start -// Baseline Primary: #0B57D0 -inline constexpr ImU32 kPrimary40 = 0xFFD0570B; -// Baseline Secondary: #C2E7FF -inline constexpr ImU32 kSecondary90 = 0xFFFFE7C2; -// go/keep-sorted end - } // namespace traceviewer #endif // THIRD_PARTY_XPROF_FRONTEND_APP_COMPONENTS_TRACE_VIEWER_V2_COLOR_COLORS_H_ diff --git a/frontend/app/components/trace_viewer_v2/timeline/BUILD b/frontend/app/components/trace_viewer_v2/timeline/BUILD index 3ba1e3da..bcc4bd49 100644 --- a/frontend/app/components/trace_viewer_v2/timeline/BUILD +++ b/frontend/app/components/trace_viewer_v2/timeline/BUILD @@ -44,7 +44,6 @@ cc_library( ], deps = [ ":constants", - ":draw_utils", ":time_range", "//third_party/dear_imgui", "@com_google_absl//absl/base:nullability", @@ -94,18 +93,6 @@ cc_library( ], ) -cc_library( - name = "draw_utils", - srcs = ["draw_utils.cc"], - hdrs = ["draw_utils.h"], - deps = [ - ":constants", - "//third_party/dear_imgui", - "@com_google_absl//absl/strings:string_view", - "@org_xprof//frontend/app/components/trace_viewer_v2/color:colors", - ], -) - cc_test( name = "data_provider_test", srcs = ["data_provider_test.cc"], diff --git a/frontend/app/components/trace_viewer_v2/timeline/data_provider.cc b/frontend/app/components/trace_viewer_v2/timeline/data_provider.cc index 53c90d3b..38c0eef2 100644 --- a/frontend/app/components/trace_viewer_v2/timeline/data_provider.cc +++ b/frontend/app/components/trace_viewer_v2/timeline/data_provider.cc @@ -303,7 +303,7 @@ void DataProvider::ProcessTraceEvents(const ParsedTraceEvents& parsed_events, Timeline& timeline) { if (parsed_events.flame_events.empty() && parsed_events.counter_events.empty()) { - timeline.set_timeline_data({}); + timeline.SetTimelineData({}); timeline.set_data_time_range(TimeRange::Zero()); timeline.SetVisibleRange(TimeRange::Zero()); return; @@ -372,7 +372,7 @@ void DataProvider::ProcessTraceEvents(const ParsedTraceEvents& parsed_events, } } - timeline.set_timeline_data(CreateTimelineData(trace_info, time_bounds)); + timeline.SetTimelineData(CreateTimelineData(trace_info, time_bounds)); // Don't need to check for max_time because the TimeRange constructor will // handle any potential issues with max_time. diff --git a/frontend/app/components/trace_viewer_v2/timeline/draw_utils.cc b/frontend/app/components/trace_viewer_v2/timeline/draw_utils.cc deleted file mode 100644 index 3fb206b9..00000000 --- a/frontend/app/components/trace_viewer_v2/timeline/draw_utils.cc +++ /dev/null @@ -1,138 +0,0 @@ -#include "xprof/frontend/app/components/trace_viewer_v2/timeline/draw_utils.h" - -#include -#include -#include - -#include "xprof/frontend/app/components/trace_viewer_v2/color/colors.h" -#include "xprof/frontend/app/components/trace_viewer_v2/timeline/constants.h" -#include "absl/strings/string_view.h" -#include "third_party/dear_imgui/imgui.h" - -namespace traceviewer { - -namespace { - -// GM3-style indefinite linear progress indicator constants. -// The width of the progress bar as a ratio of the viewport width. -constexpr float kProgressBarWidthRatio = 0.4f; -constexpr Pixel kProgressBarHeight = 4.0f; -// The duration in seconds for one full animation cycle of the progress bar. -constexpr float kAnimationDuration = 3.0f; -// The gap in pixels between the primary and secondary indicators. -constexpr Pixel kProgressBarGap = 4.0f; -// A scaling factor used in easing calculations for animation. -constexpr float kProgressScale = 2.0f; -constexpr Pixel kTextOffsetY = 8.0f; - -// Tutorial messages displayed in the loading indicator. -constexpr absl::string_view kTutorials[] = { - "Pan: A/D or Shift+Scroll", - "Zoom: W/S or Ctrl+Scroll", - "Scroll: Up/Down Arrow or Scroll", -}; -// The interval in seconds to display each tutorial message. -constexpr float kTutorialInterval = 1.5f; -// The padding adding a gap between the progress indicator and the tutorial text -constexpr float kTutorialTextPaddingTop = 32.0f; - -void DrawTutorialText(ImDrawList* draw_list, ImVec2 center, float time) { - int num_tutorials = std::size(kTutorials); - int tutorial_index = - static_cast(time / kTutorialInterval) % num_tutorials; - absl::string_view tutorial = kTutorials[tutorial_index]; - - ImVec2 text_size = - ImGui::CalcTextSize(tutorial.data(), tutorial.data() + tutorial.size()); - ImVec2 text_pos = - ImVec2(center.x - text_size.x / 2.0f, center.y + kTutorialTextPaddingTop); - draw_list->AddText(text_pos, kBlackColor, tutorial.data(), - tutorial.data() + tutorial.size()); -} - -// Draws a "Loading data..." text message centered below the progress bar. -void DrawLoadingText(const ImGuiViewport* viewport) { - ImDrawList* draw_list = ImGui::GetForegroundDrawList(); - if (!draw_list) return; - - const char* text = "Loading data..."; - const ImVec2 text_size = ImGui::CalcTextSize(text); - const ImVec2 center = viewport->GetCenter(); - - const float text_x = center.x - text_size.x / 2.0f; - // Position text below the progress bar. The progress bar's center is at - // viewport->GetCenter().y. - const float text_y = center.y + kProgressBarHeight / 2.0f + kTextOffsetY; - - draw_list->AddText(ImVec2(text_x, text_y), kBlackColor, text); -} - -} // namespace - -// Draws a loading indicator in the center of the viewport. -void DrawLoadingIndicator(const ImGuiViewport* viewport) { - ImDrawList* draw_list = ImGui::GetForegroundDrawList(); - if (!draw_list) return; - - const Pixel kProgressBarCornerRadius = kProgressBarHeight / 2.0f; - const Pixel progress_bar_width = viewport->Size.x * kProgressBarWidthRatio; - - const ImVec2 center = viewport->GetCenter(); - const Pixel start_x = center.x - progress_bar_width / 2.0f; - const Pixel end_x = center.x + progress_bar_width / 2.0f; - const Pixel y = center.y - kProgressBarHeight / 2.0f; - - const float time = static_cast(ImGui::GetTime()); - const float cycle_progress = - fmod(time, kAnimationDuration) / kAnimationDuration; - - // The animation of the primary progress bar is defined by the movement of its - // head (right side) and tail (left side). The head moves with an ease-out - // curve, starting fast and slowing down, while the tail moves linearly. This - // creates the effect of the bar first growing in width and then shrinking as - // the tail catches up to the head. - - // Quadratic ease-out for the head of the bar. - const float head_progress = - kProgressScale * cycle_progress * (kProgressScale - cycle_progress); - // Linear progress for the tail of the bar. - const float tail_progress = kProgressScale * cycle_progress; - - const Pixel primary_start = start_x + progress_bar_width * tail_progress; - const Pixel primary_end = start_x + progress_bar_width * head_progress; - - // Draw the two secondary segments and one primary segment, with gaps. - // All segments are clipped to the progress bar's bounds. - // Something like this: - // (-----s1-----) (--p--) (-----s2-----) - // ^ gap ^ - const Pixel s1_start = start_x; - const Pixel s1_end = std::min(primary_start - kProgressBarGap, end_x); - if (s1_start < s1_end) { - draw_list->AddRectFilled( - ImVec2(s1_start, y), ImVec2(s1_end, y + kProgressBarHeight), - kSecondary90, kProgressBarCornerRadius, ImDrawFlags_RoundCornersAll); - } - - const Pixel p_start = std::max(primary_start, start_x); - const Pixel p_end = std::min(primary_end, end_x); - if (p_start < p_end) { - draw_list->AddRectFilled( - ImVec2(p_start, y), ImVec2(p_end, y + kProgressBarHeight), kPrimary40, - kProgressBarCornerRadius, ImDrawFlags_RoundCornersAll); - } - - const Pixel s2_start = std::max(primary_end + kProgressBarGap, start_x); - const Pixel s2_end = end_x; - if (s2_start < s2_end) { - draw_list->AddRectFilled( - ImVec2(s2_start, y), ImVec2(s2_end, y + kProgressBarHeight), - kSecondary90, kProgressBarCornerRadius, ImDrawFlags_RoundCornersAll); - } - - DrawLoadingText(viewport); - - DrawTutorialText(draw_list, center, time); -} - -} // namespace traceviewer diff --git a/frontend/app/components/trace_viewer_v2/timeline/draw_utils.h b/frontend/app/components/trace_viewer_v2/timeline/draw_utils.h deleted file mode 100644 index 0f738cbd..00000000 --- a/frontend/app/components/trace_viewer_v2/timeline/draw_utils.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef THIRD_PARTY_XPROF_FRONTEND_APP_COMPONENTS_TRACE_VIEWER_V2_TIMELINE_DRAW_UTILS_H_ -#define THIRD_PARTY_XPROF_FRONTEND_APP_COMPONENTS_TRACE_VIEWER_V2_TIMELINE_DRAW_UTILS_H_ - -#include "third_party/dear_imgui/imgui.h" - -namespace traceviewer { - -// Draws a loading indicator in the center of the viewport. -void DrawLoadingIndicator(const ImGuiViewport* viewport); - -} // namespace traceviewer - -#endif // THIRD_PARTY_XPROF_FRONTEND_APP_COMPONENTS_TRACE_VIEWER_V2_TIMELINE_DRAW_UTILS_H_ diff --git a/frontend/app/components/trace_viewer_v2/timeline/timeline.cc b/frontend/app/components/trace_viewer_v2/timeline/timeline.cc index 32ef8ed8..f22d83ce 100644 --- a/frontend/app/components/trace_viewer_v2/timeline/timeline.cc +++ b/frontend/app/components/trace_viewer_v2/timeline/timeline.cc @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include "absl/base/nullability.h" #include "absl/log/log.h" @@ -19,7 +21,6 @@ #include "xprof/frontend/app/components/trace_viewer_v2/event_data.h" #include "xprof/frontend/app/components/trace_viewer_v2/helper/time_formatter.h" #include "xprof/frontend/app/components/trace_viewer_v2/timeline/constants.h" -#include "xprof/frontend/app/components/trace_viewer_v2/timeline/draw_utils.h" #include "xprof/frontend/app/components/trace_viewer_v2/timeline/time_range.h" #include "xprof/frontend/app/components/trace_viewer_v2/trace_helper/trace_event.h" @@ -51,6 +52,14 @@ void Timeline::SetVisibleRange(const TimeRange& range, bool animate) { } } +void Timeline::SetTimelineData(FlameChartTimelineData data) { + // Calculate offsets first to avoid partial state in group_offsets_ member. + Offsets new_offsets = CalculateOffsets(data); + timeline_data_ = std::move(data); + group_offsets_ = std::move(new_offsets.group_offsets); + visible_level_offsets_ = std::move(new_offsets.visible_level_offsets); +} + void Timeline::Draw() { event_clicked_this_frame_ = false; @@ -59,16 +68,8 @@ void Timeline::Draw() { ImGui::SetNextWindowSize(viewport->Size); ImGui::SetNextWindowViewport(viewport->ID); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.f); - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); - ImGui::Begin("Timeline viewer", nullptr, kImGuiWindowFlags); - if (timeline_data_.groups.empty()) { - DrawLoadingIndicator(viewport); - } - const Pixel timeline_width = ImGui::GetContentRegionAvail().x - label_width_ - kTimelinePaddingRight; const double px_per_time_unit_val = px_per_time_unit(timeline_width); @@ -88,8 +89,43 @@ void Timeline::Draw() { label_width_); ImGui::TableSetupColumn("Timeline", ImGuiTableColumnFlags_WidthStretch); - for (int group_index = 0; group_index < timeline_data_.groups.size(); - ++group_index) { + // If the group offsets are empty, we don't need to draw any tracks/events + // Return early to avoid unnecessary operations and crashes. + if (group_offsets_.empty()) { + ImGui::EndTable(); + ImGui::EndChild(); // Tracks + ImGui::End(); // Timeline viewer + return; + } + + // Manual culling for smooth scrolling with variable height groups. + // ImGuiListClipper can cause jumping with variable height items because it + // estimates the height of unseen items. By pre-calculating the exact height + // of each group, we can implement a custom culling loop that provides + // perfectly smooth scrolling. + + // 1. Determine which groups are visible based on scroll position using binary + // search on the pre-calculated group offsets. + // Note: scroll_y is relative to the top of the "Tracks" child window. Since + // the ruler is drawn outside this child window, scroll_y starts at 0 for the + // first group and does not include the ruler height. + const float scroll_y = ImGui::GetScrollY(); + const float visible_height = ImGui::GetContentRegionAvail().y; + + auto [start_index, end_index] = + GetVisibleGroupRange(scroll_y, visible_height); + + // 2. Add top padding for all invisible items above the viewport. This is + // simply the offset of the first visible group. + const float top_padding_height = group_offsets_[start_index]; + if (top_padding_height > 0.0f) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Dummy(ImVec2(0.0f, top_padding_height)); + } + + // 3. Draw only the visible items. + for (int group_index = start_index; group_index <= end_index; ++group_index) { const Group& group = timeline_data_.groups[group_index]; ImGui::TableNextRow(); ImGui::TableNextColumn(); @@ -107,6 +143,15 @@ void Timeline::Draw() { DrawGroup(group_index, px_per_time_unit_val); } + // 4. Add bottom padding for all invisible items below the viewport. + const float bottom_padding_height = + group_offsets_.back() - group_offsets_[end_index + 1]; + if (bottom_padding_height > 0.0f) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Dummy(ImVec2(0.0f, bottom_padding_height)); + } + ImGui::EndTable(); HandleEventDeselection(); @@ -127,10 +172,7 @@ void Timeline::Draw() { // window, without affecting global foreground elements like tooltips. DrawSelectedTimeRanges(timeline_width, px_per_time_unit_val); - ImGui::EndChild(); - ImGui::PopStyleVar(); // ItemSpacing - ImGui::PopStyleVar(); // CellPadding - ImGui::PopStyleVar(); // WindowRounding + ImGui::EndChild(); // Tracks ImGui::End(); // Timeline viewer } @@ -138,7 +180,7 @@ EventRect Timeline::CalculateEventRect(Microseconds start, Microseconds end, Pixel screen_x_offset, Pixel screen_y_offset, double px_per_time_unit, - int level_in_group, + Pixel relative_y_pos, Pixel timeline_width) const { const Pixel left = TimeToScreenX(start, screen_x_offset, px_per_time_unit); Pixel right = TimeToScreenX(end, screen_x_offset, px_per_time_unit); @@ -152,8 +194,7 @@ EventRect Timeline::CalculateEventRect(Microseconds start, Microseconds end, // event's start time. right -= kEventPaddingRight; - const Pixel top = - screen_y_offset + level_in_group * (kEventHeight + kEventPaddingBottom); + const Pixel top = screen_y_offset + relative_y_pos; const Pixel bottom = top + kEventHeight; const Pixel timeline_right_boundary = screen_x_offset + timeline_width; @@ -345,6 +386,73 @@ double Timeline::px_per_time_unit(Pixel timeline_width) const { } } +Timeline::Offsets Timeline::CalculateOffsets( + const FlameChartTimelineData& data) const { + Offsets offsets; + // There is one more element in offsets than in groups to store the total + // height. + offsets.group_offsets.reserve(data.groups.size() + 1); + offsets.visible_level_offsets.resize(data.events_by_level.size() + 1); + + float current_offset = 0.0f; + offsets.group_offsets.push_back(current_offset); + + for (int i = 0; i < data.groups.size(); ++i) { + const Group& group = data.groups[i]; + const int start_level = group.start_level; + int end_level = (i + 1 < data.groups.size()) + ? data.groups[i + 1].start_level + : data.events_by_level.size(); + + if (group.type == Group::Type::kCounter) { + if (start_level < offsets.visible_level_offsets.size()) { + offsets.visible_level_offsets[start_level] = current_offset; + current_offset += kCounterTrackHeight; + } + } else if (group.type == Group::Type::kFlame) { + // kFlame group. + // Ensure a minimum height of one level to prevent ImGui::BeginChild from + // auto-resizing, even if a group contains no levels. This is important + // for parent groups (e.g., a process) that might not contain any event + // levels directly. + if (end_level <= start_level) { + current_offset += (kEventHeight + kEventPaddingBottom); + } else { + for (int level = start_level; level < end_level; ++level) { + if (level < offsets.visible_level_offsets.size()) { + offsets.visible_level_offsets[level] = current_offset; + } + current_offset += (kEventHeight + kEventPaddingBottom); + } + } + } + offsets.group_offsets.push_back(current_offset); + } + // The last element stores the total height of all levels. + if (!offsets.visible_level_offsets.empty()) { + offsets.visible_level_offsets.back() = current_offset; + } + return offsets; +} + +std::pair Timeline::GetVisibleGroupRange(float scroll_y, + float visible_height) const { + if (group_offsets_.empty()) return {0, -1}; + + auto it_start = + std::upper_bound(group_offsets_.begin(), group_offsets_.end(), scroll_y); + int start_index = std::distance(group_offsets_.begin(), it_start) - 1; + start_index = std::max(0, start_index); + + auto it_end = std::lower_bound(group_offsets_.begin(), group_offsets_.end(), + scroll_y + visible_height); + int end_index = std::distance(group_offsets_.begin(), it_end) - 1; + end_index = + std::min(end_index, static_cast(timeline_data_.groups.size() - 1)); + + return {start_index, end_index}; +} + // Draws the timeline ruler. This includes the main horizontal line, // vertical tick marks indicating time intervals, and their corresponding time // labels. @@ -531,14 +639,49 @@ void Timeline::DrawEvent(int group_index, int event_index, void Timeline::DrawEventsForLevel(int group_index, absl::Span event_indices, - double px_per_time_unit, int level_in_group, + double px_per_time_unit, int level, const ImVec2& pos, const ImVec2& max) { ImDrawList* const draw_list = ImGui::GetWindowDrawList(); if (!draw_list) { return; } - for (int event_index : event_indices) { + const Microseconds visible_start_time = PixelToTime(pos.x, px_per_time_unit); + const Microseconds visible_end_time = PixelToTime(max.x, px_per_time_unit); + + // Since we are drawing events for a single level, the events are guaranteed + // not to overlap. This implies that if the events are sorted by start time + // (which is a requirement for event_indices), they are effectively sorted by + // end time as well. Specifically, if event A comes before event B, then + // start_A < end_A <= start_B < end_B. Thus, we can use binary search for + // both start and end times. + + // Find the first event that ends after the visible start time. + // lower_bound with this comparator finds the first element where + // (end_time < visible_start_time) is false, i.e., end_time >= + // visible_start_time. + auto first_visible_it = std::lower_bound( + event_indices.begin(), event_indices.end(), visible_start_time, + [&](int event_index, Microseconds time) { + return timeline_data_.entry_start_times[event_index] + + timeline_data_.entry_total_times[event_index] < + time; + }); + + // Find the first event that starts after the visible end time. + // upper_bound with this comparator finds the first element where + // (visible_end_time < start_time) is true. + auto last_visible_it = std::upper_bound( + first_visible_it, event_indices.end(), visible_end_time, + [&](Microseconds time, int event_index) { + return time < timeline_data_.entry_start_times[event_index]; + }); + + const Pixel relative_y_pos = + visible_level_offsets_[level] - group_offsets_[group_index]; + + for (auto it = first_visible_it; it != last_visible_it; ++it) { + int event_index = *it; if (event_index < 0 || event_index >= timeline_data_.entry_start_times.size() || event_index >= timeline_data_.entry_total_times.size()) { @@ -550,7 +693,7 @@ void Timeline::DrawEventsForLevel(int group_index, start + timeline_data_.entry_total_times[event_index]; const EventRect rect = CalculateEventRect( - start, end, pos.x, pos.y, px_per_time_unit, level_in_group, max.x); + start, end, pos.x, pos.y, px_per_time_unit, relative_y_pos, max.x); DrawEvent(group_index, event_index, rect, draw_list); } @@ -690,18 +833,8 @@ void Timeline::DrawGroup(int group_index, double px_per_time_unit_val) { // If this is the last group, the end level is the total // number of levels. : timeline_data_.events_by_level.size(); - // Ensure end_level is not less than start_level, to avoid negative height. - end_level = std::max(start_level, end_level); - - // Calculate group height. Ensure a minimum height of one level to prevent - // ImGui::BeginChild from auto-resizing, even if a group contains no levels. - // This is important for parent groups (e.g., a process) that might not - // contain any event levels directly. - // TODO: b/453676716 - Add tests for group height calculation. - const Pixel group_height = group.type == Group::Type::kCounter - ? kCounterTrackHeight - : std::max(1, end_level - start_level) * - (kEventHeight + kEventPaddingBottom); + const Pixel group_height = + group_offsets_[group_index + 1] - group_offsets_[group_index]; // Groups might have the same name. We add the index of the group to the ID // to ensure each ImGui::BeginChild call has a unique ID, otherwise ImGui // might ignore later calls with the same name. @@ -728,7 +861,7 @@ void Timeline::DrawGroup(int group_index, double px_per_time_unit_val) { // TODO: b/453676716 - Add boundary test cases for this function. DrawEventsForLevel(group_index, timeline_data_.events_by_level[level], px_per_time_unit_val, - /*level_in_group=*/level - start_level, pos, max); + /*level=*/level, pos, max); } } } diff --git a/frontend/app/components/trace_viewer_v2/timeline/timeline.h b/frontend/app/components/trace_viewer_v2/timeline/timeline.h index d1347e7b..9e6cbe52 100644 --- a/frontend/app/components/trace_viewer_v2/timeline/timeline.h +++ b/frontend/app/components/trace_viewer_v2/timeline/timeline.h @@ -113,11 +113,15 @@ class Timeline { void set_data_time_range(const TimeRange& range) { data_time_range_ = range; } - void set_timeline_data(FlameChartTimelineData data) { - timeline_data_ = std::move(data); - } + void SetTimelineData(FlameChartTimelineData data); const FlameChartTimelineData& timeline_data() const { return timeline_data_; } + // Returns the cached group offsets. This is for testing only. + const std::vector& group_offsets() const { return group_offsets_; } + const std::vector& visible_level_offsets() const { + return visible_level_offsets_; + } + int selected_event_index() const { return selected_event_index_; } int selected_group_index() const { return selected_group_index_; } int selected_counter_index() const { return selected_counter_index_; } @@ -127,7 +131,7 @@ class Timeline { // Calculates the screen coordinates of the rectangle for an event. EventRect CalculateEventRect(Microseconds start, Microseconds end, Pixel screen_x_offset, Pixel screen_y_offset, - double px_per_time_unit, int level_in_group, + double px_per_time_unit, Pixel relative_y_pos, Pixel timeline_width) const; // Calculates the top-left screen coordinates for the event name text. @@ -176,6 +180,23 @@ class Timeline { // zooming behavior. virtual void Zoom(float zoom_factor); + struct Offsets { + std::vector group_offsets; + std::vector visible_level_offsets; + }; + + // Pre-calculates the offsets of each group row based on the provided data. + // This avoids re-calculating these heights on every frame during the draw + // call. + virtual Offsets CalculateOffsets(const FlameChartTimelineData& data) const; + + // Calculates the range of visible group indices [start, end] based on the + // current scroll position and visible height. The returned range includes + // a buffer of one group before and after the visible area for smooth + // scrolling. + std::pair GetVisibleGroupRange(float scroll_y, + float visible_height) const; + private: double px_per_time_unit() const; double px_per_time_unit(Pixel timeline_width) const; @@ -191,8 +212,8 @@ class Timeline { ImDrawList* absl_nonnull draw_list); void DrawEventsForLevel(int group_index, absl::Span event_indices, - double px_per_time_unit, int level_in_group, - const ImVec2& pos, const ImVec2& max); + double px_per_time_unit, int level, const ImVec2& pos, + const ImVec2& max); void DrawCounterTooltip(int group_index, const CounterData& counter_data, double px_per_time_unit_val, const ImVec2& pos, @@ -238,10 +259,18 @@ class Timeline { static constexpr ImGuiWindowFlags kLaneFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; - FlameChartTimelineData timeline_data_; // TODO - b/444026851: Set the label width based on the real screen width. Pixel label_width_ = 250.0f; + FlameChartTimelineData timeline_data_; + + // Cached offsets of each group row. This is pre-calculated in + // `set_timeline_data` to avoid recalculating on every frame in the `Draw` + // call, which is a significant performance optimization. The last element + // stores the total height. + std::vector group_offsets_; + std::vector visible_level_offsets_; + // The visible time range in microseconds in the timeline. It is initialized // to {0, 0} by the `TimeRange` default constructor. // This range is updated through `SetVisibleRange`. diff --git a/frontend/app/components/trace_viewer_v2/timeline/timeline_test.cc b/frontend/app/components/trace_viewer_v2/timeline/timeline_test.cc index e5f2049a..bf616646 100644 --- a/frontend/app/components/trace_viewer_v2/timeline/timeline_test.cc +++ b/frontend/app/components/trace_viewer_v2/timeline/timeline_test.cc @@ -1,11 +1,14 @@ #include "xprof/frontend/app/components/trace_viewer_v2/timeline/timeline.h" #include +#include #include #include +#include #include "testing/base/public/gmock.h" #include "" +#include "absl/strings/match.h" #include "absl/strings/string_view.h" #include "third_party/dear_imgui/imgui.h" #include "third_party/dear_imgui/imgui_internal.h" @@ -41,13 +44,74 @@ TEST(TimelineTest, SetTimelineData) { data.entry_start_times.push_back(10.0); data.entry_total_times.push_back(5.0); - timeline.set_timeline_data(std::move(data)); + timeline.SetTimelineData(std::move(data)); EXPECT_THAT(timeline.timeline_data().entry_levels, ElementsAre(0)); EXPECT_THAT(timeline.timeline_data().entry_start_times, ElementsAre(10.0)); EXPECT_THAT(timeline.timeline_data().entry_total_times, ElementsAre(5.0)); } +TEST(TimelineTest, CalculateOffsets) { + Timeline timeline; + FlameChartTimelineData data; + + // Group 0: Flame chart with 2 levels (0, 1). + // Height = 2 * (kEventHeight + kEventPaddingBottom) = 2 * 17.0 = 34.0. + data.groups.push_back( + {.type = Group::Type::kFlame, .name = "Group 0", .start_level = 0}); + + // Group 1: Counter track. + // Height = kCounterTrackHeight = 40.0. + data.groups.push_back( + {.type = Group::Type::kCounter, .name = "Group 1", .start_level = 2}); + + // Group 2: Empty flame chart (0 levels). + // Height = 1 * (kEventHeight + kEventPaddingBottom) = 17.0 (min height). + data.groups.push_back( + {.type = Group::Type::kFlame, .name = "Group 2", .start_level = 3}); + + // Simulate 3 levels in total (0, 1, 2). Group 2 starts at 3, so it's empty. + // But since start_level is 3 for Group 2, maybe we should have 4 levels? + // 0, 1 are in Group 0. + // 2 is in Group 1. + // 3 is in Group 2 (empty). + // Let's resize events_by_level to 4 so visible_level_offsets has size 4. + data.events_by_level.resize(4); + + timeline.SetTimelineData(std::move(data)); + + const std::vector& offsets = timeline.group_offsets(); + + // We expect 4 offsets: 0, height(G0), height(G0)+height(G1), total height. + ASSERT_THAT(offsets, ::testing::SizeIs(4)); + + EXPECT_THAT(offsets[0], FloatEq(0.0f)); + EXPECT_THAT(offsets[1], FloatEq(34.0f)); + EXPECT_THAT(offsets[2], FloatEq(34.0f + 40.0f)); // 74.0f + EXPECT_THAT(offsets[3], FloatEq(74.0f + 17.0f)); // 91.0f + + const std::vector& visible_level_offsets = + timeline.visible_level_offsets(); + + // Max level is 3. So size is 3 + 1 + 1 (end offset) = 5. + ASSERT_THAT(visible_level_offsets, ::testing::SizeIs(5)); + + // Group 0 + // Level 0: 0.0 + 0 * 17.0 = 0.0 + EXPECT_THAT(visible_level_offsets[0], FloatEq(0.0f)); + + // Level 1: 0.0 + 1 * 17.0 = 17.0 + EXPECT_THAT(visible_level_offsets[1], FloatEq(17.0f)); + + // Group 1 (Counter) - Level 2 + // Populated by CalculateOffsets for counter groups. + EXPECT_THAT(visible_level_offsets[2], FloatEq(34.0f)); + + // Group 2 (Flame) - Level 3 + // It covers level 3. + EXPECT_THAT(visible_level_offsets[3], FloatEq(74.0f)); +} + TEST(TimelineTest, SetVisibleRange) { Timeline timeline; TimeRange range(10.0, 50.0); @@ -115,7 +179,7 @@ TEST(TimelineTest, TimeToScreenX) { constexpr double kPxPerTimeUnit = 1.0; constexpr Pixel kScreenXOffset = 0.0f; constexpr Pixel kScreenYOffset = 0.0f; -constexpr int kLevelInGroup = 0; +constexpr Pixel kRelativeYPos = 0.0f; constexpr Pixel kTimelineWidth = 100.0f; TEST(TimelineTest, CalculateEventRect_EventFullyWithinView) { @@ -126,7 +190,7 @@ TEST(TimelineTest, CalculateEventRect_EventFullyWithinView) { // Screen range before adjustments: [10.0, 20.0]. EventRect rect = timeline.CalculateEventRect( /*start=*/110.0, /*end=*/120.0, kScreenXOffset, kScreenYOffset, - kPxPerTimeUnit, kLevelInGroup, kTimelineWidth); + kPxPerTimeUnit, kRelativeYPos, kTimelineWidth); EXPECT_FLOAT_EQ(rect.left, 10.0f); EXPECT_FLOAT_EQ(rect.right, 20.0f - kEventPaddingRight); @@ -142,7 +206,7 @@ TEST(TimelineTest, CalculateEventRect_EventPartiallyClippedLeft) { // Screen range after left clipping: [0.0, 10.0]. EventRect rect = timeline.CalculateEventRect( /*start=*/90.0, /*end=*/110.0, kScreenXOffset, kScreenYOffset, - kPxPerTimeUnit, kLevelInGroup, kTimelineWidth); + kPxPerTimeUnit, kRelativeYPos, kTimelineWidth); EXPECT_FLOAT_EQ(rect.left, 0.0f); EXPECT_FLOAT_EQ(rect.right, 10.0f - kEventPaddingRight); @@ -156,7 +220,7 @@ TEST(TimelineTest, CalculateEventRect_EventPartiallyClippedRight) { // Screen range after right clipping: [90.0, 100.0]. EventRect rect = timeline.CalculateEventRect( /*start=*/190.0, /*end=*/210.0, kScreenXOffset, kScreenYOffset, - kPxPerTimeUnit, kLevelInGroup, kTimelineWidth); + kPxPerTimeUnit, kRelativeYPos, kTimelineWidth); EXPECT_FLOAT_EQ(rect.left, 90.0f); EXPECT_FLOAT_EQ(rect.right, 100.0f); @@ -172,7 +236,7 @@ TEST(TimelineTest, CalculateEventRect_EventCompletelyOutsideLeft) { // won't effect here because the event is clipped to the left edge). EventRect rect = timeline.CalculateEventRect( /*start=*/80.0, /*end=*/90.0, kScreenXOffset, kScreenYOffset, - kPxPerTimeUnit, kLevelInGroup, kTimelineWidth); + kPxPerTimeUnit, kRelativeYPos, kTimelineWidth); EXPECT_FLOAT_EQ(rect.left, 0.0f); EXPECT_FLOAT_EQ(rect.right, 0.0f); @@ -188,7 +252,7 @@ TEST(TimelineTest, CalculateEventRect_EventCompletelyOutsideRight) { // right won't effect here because the event is clipped to the right edge). EventRect rect = timeline.CalculateEventRect( /*start=*/210.0, /*end=*/220.0, kScreenXOffset, kScreenYOffset, - kPxPerTimeUnit, kLevelInGroup, kTimelineWidth); + kPxPerTimeUnit, kRelativeYPos, kTimelineWidth); EXPECT_FLOAT_EQ(rect.left, 100.0f); EXPECT_FLOAT_EQ(rect.right, 100.0f); @@ -202,7 +266,7 @@ TEST(TimelineTest, CalculateEventRect_EventSmallerThanMinimumWidth) { // Screen width is expanded to kEventMinimumDrawWidth. EventRect rect = timeline.CalculateEventRect( /*start=*/110.0, /*end=*/110.1, kScreenXOffset, kScreenYOffset, - kPxPerTimeUnit, kLevelInGroup, kTimelineWidth); + kPxPerTimeUnit, kRelativeYPos, kTimelineWidth); EXPECT_FLOAT_EQ(rect.left, 10.0f); EXPECT_FLOAT_EQ(rect.right, @@ -217,7 +281,7 @@ TEST(TimelineTest, CalculateEventRect_ZeroPxPerTimeUnit) { // kEventMinimumDrawWidth. EventRect rect = timeline.CalculateEventRect( /*start=*/110.0, /*end=*/120.0, kScreenXOffset, kScreenYOffset, - /*px_per_time_unit=*/0.0, kLevelInGroup, kTimelineWidth); + /*px_per_time_unit=*/0.0, kRelativeYPos, kTimelineWidth); // left becomes screen_x_offset (0), right becomes max(0, 0 + // kEventMinimumDrawWidth) @@ -527,7 +591,7 @@ TEST(TimelineTest, NavigateToEvent) { data.entry_start_times.push_back(100.0); data.entry_total_times.push_back(10.0); data.entry_total_times.push_back(20.0); - timeline.set_timeline_data(std::move(data)); + timeline.SetTimelineData(std::move(data)); timeline.set_data_time_range({0.0, 200.0}); timeline.SetVisibleRange({0.0, 50.0}); @@ -547,7 +611,7 @@ TEST(TimelineTest, NavigateToEventWithNegativeIndex) { Timeline timeline; FlameChartTimelineData data; data.entry_start_times.push_back(10.0); - timeline.set_timeline_data(std::move(data)); + timeline.SetTimelineData(std::move(data)); TimeRange initial_range(0.0, 50.0); timeline.SetVisibleRange(initial_range); @@ -562,7 +626,7 @@ TEST(TimelineTest, NavigateToEventWithIndexOutOfBounds) { Timeline timeline; FlameChartTimelineData data; data.entry_start_times.push_back(10.0); - timeline.set_timeline_data(std::move(data)); + timeline.SetTimelineData(std::move(data)); TimeRange initial_range(0.0, 50.0); timeline.SetVisibleRange(initial_range); @@ -573,6 +637,185 @@ TEST(TimelineTest, NavigateToEventWithIndexOutOfBounds) { EXPECT_EQ(timeline.visible_range().end(), initial_range.end()); } +// Specialized class for testing internal behavior and state. +class TestableTimeline : public Timeline { + public: + // Expose protected methods for testing. + using Timeline::GetVisibleGroupRange; + + // Intercept CalculateOffsets to verify internal state during updates. + Offsets CalculateOffsets(const FlameChartTimelineData& data) const override { + if (on_calculate_group_offsets) { + on_calculate_group_offsets(); + } + return Timeline::CalculateOffsets(data); + } + + std::function on_calculate_group_offsets; +}; + +TEST(TimelineTest, DataRemainsOldDuringCalculation) { + TestableTimeline timeline; + + // 1. Set initial data + FlameChartTimelineData old_data; + old_data.groups.push_back({.name = "Old Group", .start_level = 0}); + timeline.SetTimelineData(old_data); // Initial set + + ASSERT_EQ(timeline.timeline_data().groups.size(), 1); + ASSERT_EQ(timeline.timeline_data().groups[0].name, "Old Group"); + + // 2. Prepare new data + FlameChartTimelineData new_data; + new_data.groups.push_back({.name = "New Group", .start_level = 0}); + + // 3. Setup verification hook + bool checked = false; + timeline.on_calculate_group_offsets = [&]() { + checked = true; + // CRITICAL CHECK: The timeline should still hold the old data + EXPECT_EQ(timeline.timeline_data().groups.size(), 1); + EXPECT_EQ(timeline.timeline_data().groups[0].name, "Old Group"); + + // And obviously not the new data (though size=1 check above is weak if both + // are size 1) + EXPECT_NE(timeline.timeline_data().groups[0].name, "New Group"); + }; + + // 4. Trigger update + timeline.SetTimelineData(std::move(new_data)); + + EXPECT_TRUE(checked); + + // 5. Verify final state + EXPECT_EQ(timeline.timeline_data().groups.size(), 1); + EXPECT_EQ(timeline.timeline_data().groups[0].name, "New Group"); +} + +class GetVisibleGroupRangeTest : public ::testing::Test { + protected: + void SetUp() override { + // Setup some dummy data with predictable offsets. + // 10 groups, each height 10.0 (simulated). + // group_offsets_ will be {0, 10, 20, ..., 100} + FlameChartTimelineData data; + for (int i = 0; i < 10; ++i) { + data.groups.push_back({.name = "Group " + std::to_string(i)}); + } + // We can't control CalculateGroupOffsets easily to produce exact numbers + // without mocking or setting up complex events. + // Instead, we rely on setting up data such that CalculateGroupOffsets + // produces known values. + // Let's assume kCounterTrackHeight is 40.0f. + // We'll use counter tracks for simplicity. + data.groups.clear(); + for (int i = 0; i < 10; ++i) { + data.groups.push_back({.type = Group::Type::kCounter, + .name = "Group " + std::to_string(i), + .start_level = 0}); + } + // Height of each group will be kCounterTrackHeight (40.0f). + // Offsets: 0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400. + timeline_.SetTimelineData(std::move(data)); + } + + TestableTimeline timeline_; + const float kRowHeight = kCounterTrackHeight; // 40.0f +}; + +TEST_F(GetVisibleGroupRangeTest, EmptyData) { + TestableTimeline empty_timeline; + empty_timeline.SetTimelineData({}); + auto [start, end] = empty_timeline.GetVisibleGroupRange(0.0f, 100.0f); + + // Implementation returns {0, -1} for empty offsets. + EXPECT_EQ(start, 0); + EXPECT_EQ(end, -1); +} + +TEST_F(GetVisibleGroupRangeTest, ScrollAtTop_VisibleOneRow) { + // Scroll 0, Visible 40 (1 row). + // Visible: Group 0 [0, 40). + // Start: 0. + // End: 0. + // Expected: [0, 0]. + auto [start, end] = timeline_.GetVisibleGroupRange(0.0f, kRowHeight); + + EXPECT_EQ(start, 0); + EXPECT_EQ(end, 0); +} + +TEST_F(GetVisibleGroupRangeTest, ScrollAtTop_VisibleTwoRows) { + // Scroll 0, Visible 80 (2 rows). + // Visible: Group 0 [0, 40), Group 1 [40, 80). + // Start: 0. + // End: 1. + auto [start, end] = timeline_.GetVisibleGroupRange(0.0f, kRowHeight * 2); + + EXPECT_EQ(start, 0); + EXPECT_EQ(end, 1); +} + +TEST_F(GetVisibleGroupRangeTest, ScrollInMiddle_ExactAlign) { + // Scroll 40 (start of Group 1), Visible 40. + // Visible: Group 1 [40, 80). + // Start: 1. + // End: 1. + auto [start, end] = timeline_.GetVisibleGroupRange(kRowHeight, kRowHeight); + + EXPECT_EQ(start, 1); + EXPECT_EQ(end, 1); +} + +TEST_F(GetVisibleGroupRangeTest, ScrollInMiddle_PartialAlign) { + // Scroll 60 (middle of Group 1), Visible 40. + // Visible Range: [60, 100). + // Covers Group 1 [40, 80) and Group 2 [80, 120). + // Start: Group 1. + // End: Group 2. + auto [start, end] = + timeline_.GetVisibleGroupRange(kRowHeight * 1.5f, kRowHeight); + + EXPECT_EQ(start, 1); + EXPECT_EQ(end, 2); +} + +TEST_F(GetVisibleGroupRangeTest, ScrollAtBottom) { + // Scroll 360 (start of Group 9, last one), Visible 40. + // Visible: Group 9 [360, 400). + // Start: 9. + // End: 9. + auto [start, end] = + timeline_.GetVisibleGroupRange(kRowHeight * 9, kRowHeight); + + EXPECT_EQ(start, 9); + EXPECT_EQ(end, 9); +} + +TEST_F(GetVisibleGroupRangeTest, ScrollPastBottom) { + // Scroll 500 (beyond 400), Visible 100. + // Visible Range: [500, 600). + // Start: > 400. upper_bound returns end(), distance is 11. start_index 10. + // End: > 400. lower_bound returns end(), distance 11. end_index 10. + // Clamped to 9. + // Result: [10, 9]. (Empty range because start > end) + auto [start, end] = timeline_.GetVisibleGroupRange(500.0f, 100.0f); + + EXPECT_EQ(start, 10); + EXPECT_EQ(end, 9); +} + +TEST_F(GetVisibleGroupRangeTest, VisibleHeightLargerThanContent) { + // Scroll 0, Visible 1000. + // Visible Range [0, 1000). + // Start: 0. + // End: 9 (clamped). + auto [start, end] = timeline_.GetVisibleGroupRange(0.0f, 1000.0f); + + EXPECT_EQ(start, 0); + EXPECT_EQ(end, 9); +} + // Test fixture for tests that require an ImGui context. template class TimelineImGuiTestFixture : public Test { @@ -580,13 +823,21 @@ class TimelineImGuiTestFixture : public Test { void SetUp() override { ImGui::CreateContext(); + // Match the default styles applied in Application::Initialize(). + ImGuiStyle& style = ImGui::GetStyle(); + style.ScrollbarSize = 10.0f; + style.WindowRounding = 0.0f; + style.WindowPadding = ImVec2(0.0f, 0.0f); + style.CellPadding = ImVec2(0.0f, 0.0f); + style.ItemSpacing = ImVec2(0.0f, 0.0f); + ImGuiIO& io = ImGui::GetIO(); // Set dummy display size and delta time, required for ImGui to function. io.DisplaySize = ImVec2(1920, 1080); io.DeltaTime = 0.1f; // The font atlas must be built before ImGui::NewFrame() is called. io.Fonts->Build(); - timeline_.set_timeline_data( + timeline_.SetTimelineData( {{}, {}, {}, @@ -834,24 +1085,24 @@ TEST_F(MockTimelineImGuiFixture, ShiftClickAndReleaseShiftMidDragContinuesSelection) { // Setup similar to TimelineDragSelectionTest to ensure predictable // coordinates. - timeline_.SetVisibleRange({0.0, 165.3}); - timeline_.set_data_time_range({0.0, 165.3}); + timeline_.SetVisibleRange({0.0, 166.9}); + timeline_.set_data_time_range({0.0, 166.9}); ImGuiIO& io = ImGui::GetIO(); // Start with Shift held down. io.AddKeyEvent(ImGuiMod_Shift, true); // Start drag in timeline area. - // X=308 is safely inside the timeline (250 + padding < 308). - io.MousePos = ImVec2(308.0f, 50.0f); + // X=300 is safely inside the timeline (250 + padding < 300). + io.MousePos = ImVec2(300.0f, 50.0f); io.AddMouseButtonEvent(0, true); SimulateFrame(); // Release Shift key. io.AddKeyEvent(ImGuiMod_Shift, false); - // Drag mouse to X=508. - io.MousePos = ImVec2(508.0f, 50.0f); + // Drag mouse to X=500. + io.MousePos = ImVec2(500.0f, 50.0f); SimulateFrame(); // End drag. @@ -863,22 +1114,22 @@ TEST_F(MockTimelineImGuiFixture, const TimeRange& range = timeline_.selected_time_ranges()[0]; // Calculate expected range based on pixel movement. // 10px/us assumption from TimelineDragSelectionTest. - // 308 -> 5.0. 508 -> 25.0. + // 300 -> 5.0. 500 -> 25.0. EXPECT_NEAR(range.start(), 5.0, 1e-5); EXPECT_NEAR(range.end(), 25.0, 1e-5); } TEST_F(MockTimelineImGuiFixture, ClickAndPressShiftMidDragContinuesPanning) { // Setup similar to TimelineDragSelectionTest. - timeline_.SetVisibleRange({0.0, 165.3}); - timeline_.set_data_time_range({0.0, 165.3}); + timeline_.SetVisibleRange({0.0, 166.9}); + timeline_.set_data_time_range({0.0, 166.9}); ImGuiIO& io = ImGui::GetIO(); // Start without Shift. io.AddKeyEvent(ImGuiMod_Shift, false); // Start drag in timeline area. - io.MousePos = ImVec2(308.0f, 50.0f); + io.MousePos = ImVec2(300.0f, 50.0f); io.AddMouseButtonEvent(0, true); EXPECT_CALL(timeline_, Pan(0.0f)); @@ -889,8 +1140,8 @@ TEST_F(MockTimelineImGuiFixture, ClickAndPressShiftMidDragContinuesPanning) { io.AddKeyEvent(ImGuiMod_Shift, true); // Drag mouse to left (simulate pan right). - // Move from 308 to 208 (-100px). - io.MousePos = ImVec2(208.0f, 50.0f); + // Move from 300 to 200 (-100px). + io.MousePos = ImVec2(200.0f, 50.0f); EXPECT_CALL(timeline_, Pan(FloatEq(100.0f))); EXPECT_CALL(timeline_, Scroll(FloatEq(0.0f))); @@ -914,7 +1165,7 @@ TEST_F(MockTimelineImGuiFixture, DrawEventNameTextHiddenWhenTooNarrow) { data.entry_levels.push_back(0); data.entry_start_times.push_back(10.0); data.entry_total_times.push_back(0.001); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); // The event rect width will be kEventMinimumDrawWidth = 2.0f because @@ -939,7 +1190,7 @@ TEST_F(MockTimelineImGuiFixture, data.entry_levels.push_back(0); data.entry_start_times.push_back(10.0); data.entry_total_times.push_back(0.255); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); // The event rect width will be around 4.51f, which is < kMinTextWidth (5.0f). @@ -963,7 +1214,7 @@ TEST_F(RealTimelineImGuiFixture, ClickEventSelectsEvent) { data.entry_levels.push_back(0); data.entry_start_times.push_back(0.0); data.entry_total_times.push_back(100.0); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); bool callback_called = false; @@ -978,7 +1229,7 @@ TEST_F(RealTimelineImGuiFixture, ClickEventSelectsEvent) { // Set a mouse position that is guaranteed to be over the event, since the // event spans the entire timeline. - ImGui::GetIO().MousePos = ImVec2(300.f, 40.f); + ImGui::GetIO().MousePos = ImVec2(300.f, 30.f); ImGui::GetIO().MouseDown[0] = true; SimulateFrame(); @@ -1002,7 +1253,7 @@ TEST_F(RealTimelineImGuiFixture, ClickOutsideEventDoesNotSelectEvent) { data.entry_levels.push_back(0); data.entry_start_times.push_back(0.0); data.entry_total_times.push_back(100.0); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); bool callback_called = false; @@ -1031,7 +1282,7 @@ TEST_F(RealTimelineImGuiFixture, data.entry_levels.push_back(0); data.entry_start_times.push_back(0.0); data.entry_total_times.push_back(100.0); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); int callback_count = 0; @@ -1040,7 +1291,7 @@ TEST_F(RealTimelineImGuiFixture, callback_count++; }); - ImGui::GetIO().MousePos = ImVec2(300.f, 40.f); + ImGui::GetIO().MousePos = ImVec2(300.f, 30.f); // First click. ImGui::GetIO().MouseDown[0] = true; @@ -1070,11 +1321,11 @@ TEST_F(RealTimelineImGuiFixture, ClickEmptyAreaDeselectsEvent) { data.entry_levels.push_back(0); data.entry_start_times.push_back(0.0); data.entry_total_times.push_back(100.0); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); // First, select an event. - ImGui::GetIO().MousePos = ImVec2(300.f, 40.f); // A position over the event. + ImGui::GetIO().MousePos = ImVec2(300.f, 30.f); // A position over the event. ImGui::GetIO().MouseDown[0] = true; SimulateFrame(); ImGui::GetIO().MouseDown[0] = false; // Release the mouse. @@ -1113,11 +1364,11 @@ TEST_F(RealTimelineImGuiFixture, ClickEmptyAreaDeselectsOnlyOnce) { data.entry_levels.push_back(0); data.entry_start_times.push_back(0.0); data.entry_total_times.push_back(100.0); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); // First, select an event. - ImGui::GetIO().MousePos = ImVec2(300.f, 40.f); // A position over the event. + ImGui::GetIO().MousePos = ImVec2(300.f, 30.f); // A position over the event. ImGui::GetIO().MouseDown[0] = true; SimulateFrame(); ImGui::GetIO().MouseDown[0] = false; // Release the mouse. @@ -1164,7 +1415,7 @@ TEST_F(RealTimelineImGuiFixture, ClickEmptyAreaWhenNoEventSelectedDoesNothing) { data.entry_levels.push_back(0); data.entry_start_times.push_back(0.0); data.entry_total_times.push_back(100.0); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); bool callback_called = false; @@ -1183,8 +1434,10 @@ TEST_F(RealTimelineImGuiFixture, ClickEmptyAreaWhenNoEventSelectedDoesNothing) { EXPECT_FALSE(callback_called); } -TEST_F(RealTimelineImGuiFixture, DrawsLoadingAnimationWhenTimelineDataIsEmpty) { - timeline_.set_timeline_data({}); +// This is a test for making sure the wasm is not crashing when timeline data +// is empty. +TEST_F(RealTimelineImGuiFixture, DrawsTimelineWindowWhenTimelineDataIsEmpty) { + timeline_.SetTimelineData({}); // We don't use SimulateFrame() here because we need to inspect the draw list // before ImGui::EndFrame() is called. @@ -1193,12 +1446,6 @@ TEST_F(RealTimelineImGuiFixture, DrawsLoadingAnimationWhenTimelineDataIsEmpty) { EXPECT_NE(ImGui::FindWindowByName("Timeline viewer"), nullptr); - ImDrawList* draw_list = ImGui::GetForegroundDrawList(); - - ASSERT_NE(draw_list, nullptr); - // The loading animation should be drawn to the foreground draw list. - EXPECT_FALSE(draw_list->VtxBuffer.empty()); - ImGui::EndFrame(); } @@ -1211,11 +1458,11 @@ TEST_F(RealTimelineImGuiFixture, ShiftClickEventTogglesCurtain) { data.entry_levels.push_back(0); data.entry_start_times.push_back(10.0); data.entry_total_times.push_back(20.0); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); // Mouse is over the event - ImGui::GetIO().MousePos = ImVec2(500.f, 40.f); + ImGui::GetIO().MousePos = ImVec2(500.f, 30.f); ImGui::GetIO().AddKeyEvent(ImGuiMod_Shift, true); ImGui::GetIO().MouseDown[0] = true; @@ -1255,13 +1502,13 @@ TEST_F(RealTimelineImGuiFixture, data.entry_start_times.push_back(50.0); data.entry_total_times.push_back(20.0); data.entry_total_times.push_back(10.0); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); ImGui::GetIO().AddKeyEvent(ImGuiMod_Shift, true); // First shift-click on event 1. - ImGui::GetIO().MousePos = ImVec2(500.f, 40.f); // Position over event 1. + ImGui::GetIO().MousePos = ImVec2(500.f, 30.f); // Position over event 1. ImGui::GetIO().MouseDown[0] = true; SimulateFrame(); @@ -1274,7 +1521,7 @@ TEST_F(RealTimelineImGuiFixture, SimulateFrame(); // Second shift-click on event 2. - ImGui::GetIO().MousePos = ImVec2(1100.f, 40.f); // Position over event 2. + ImGui::GetIO().MousePos = ImVec2(1100.f, 30.f); // Position over event 2. ImGui::GetIO().MouseDown[0] = true; SimulateFrame(); @@ -1289,7 +1536,7 @@ TEST_F(RealTimelineImGuiFixture, SimulateFrame(); // Third shift-click on event 1 again to deselect. - ImGui::GetIO().MousePos = ImVec2(500.f, 40.f); // Position over event 1. + ImGui::GetIO().MousePos = ImVec2(500.f, 30.f); // Position over event 1. ImGui::GetIO().MouseDown[0] = true; SimulateFrame(); @@ -1350,11 +1597,11 @@ class TimelineDragSelectionTest : public RealTimelineImGuiFixture { void SetUp() override { RealTimelineImGuiFixture::SetUp(); // Set a visible range that results in a round number for px_per_time_unit - // to make test calculations predictable. With a timeline width of 1653px - // (based on 1920px window width and paddings), a duration of 165.3 gives + // to make test calculations predictable. With a timeline width of 1669px + // (based on 1920px window width and no paddings), a duration of 166.9 gives // 10px per microsecond. - timeline_.SetVisibleRange({0.0, 165.3}); - timeline_.set_data_time_range({0.0, 165.3}); + timeline_.SetVisibleRange({0.0, 166.9}); + timeline_.set_data_time_range({0.0, 166.9}); ImGui::GetIO().AddKeyEvent(ImGuiMod_Shift, true); } @@ -1370,12 +1617,12 @@ TEST_F(TimelineDragSelectionTest, ShiftDragCreatesTimeSelection) { // Start drag in timeline area. // The label column is 250px wide, so timeline starts after that. - io.MousePos = ImVec2(308.0f, 50.0f); + io.MousePos = ImVec2(300.0f, 50.0f); io.AddMouseButtonEvent(0, true); SimulateFrame(); // Dragging - io.MousePos = ImVec2(508.0f, 50.0f); + io.MousePos = ImVec2(500.0f, 50.0f); SimulateFrame(); // End drag @@ -1392,10 +1639,10 @@ TEST_F(TimelineDragSelectionTest, ShiftDragCreatesMultipleTimeSelections) { ImGuiIO& io = ImGui::GetIO(); // First drag - io.MousePos = ImVec2(308.0f, 50.0f); + io.MousePos = ImVec2(300.0f, 50.0f); io.AddMouseButtonEvent(0, true); SimulateFrame(); - io.MousePos = ImVec2(408.0f, 50.0f); + io.MousePos = ImVec2(400.0f, 50.0f); SimulateFrame(); io.AddMouseButtonEvent(0, false); SimulateFrame(); @@ -1405,10 +1652,10 @@ TEST_F(TimelineDragSelectionTest, ShiftDragCreatesMultipleTimeSelections) { EXPECT_DOUBLE_EQ(timeline_.selected_time_ranges()[0].end(), 15.0); // Second drag - io.MousePos = ImVec2(508.0f, 50.0f); + io.MousePos = ImVec2(500.0f, 50.0f); io.AddMouseButtonEvent(0, true); SimulateFrame(); - io.MousePos = ImVec2(608.0f, 50.0f); + io.MousePos = ImVec2(600.0f, 50.0f); SimulateFrame(); io.AddMouseButtonEvent(0, false); SimulateFrame(); @@ -1424,12 +1671,12 @@ TEST_F(TimelineDragSelectionTest, DraggingUpdatesCurrentSelectedTimeRange) { ImGuiIO& io = ImGui::GetIO(); // Start drag in timeline area. - io.MousePos = ImVec2(308.0f, 50.0f); + io.MousePos = ImVec2(300.0f, 50.0f); io.AddMouseButtonEvent(0, true); SimulateFrame(); // Dragging - io.MousePos = ImVec2(508.0f, 50.0f); + io.MousePos = ImVec2(500.0f, 50.0f); SimulateFrame(); // During drag, current_selected_time_range_ should be set, but @@ -1465,7 +1712,7 @@ TEST_F(RealTimelineImGuiFixture, DrawCounterTrack) { counter_data.max_value = 10.0; data.counter_data_by_group_index[0] = std::move(counter_data); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); ImGui::NewFrame(); @@ -1504,7 +1751,7 @@ TEST_F(RealTimelineImGuiFixture, HoverCounterTrackShowsTooltip) { counter_data.max_value = 10.0; data.counter_data_by_group_index[0] = std::move(counter_data); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); // Render first frame to layout windows and find the counter track location. @@ -1581,11 +1828,11 @@ TEST_F(RealTimelineImGuiFixture, ClickEventSetsSelectionIndices) { data.entry_levels.push_back(0); data.entry_start_times.push_back(0.0); data.entry_total_times.push_back(100.0); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); // Set a mouse position that is guaranteed to be over the event. - ImGui::GetIO().MousePos = ImVec2(300.f, 40.f); + ImGui::GetIO().MousePos = ImVec2(300.f, 30.f); ImGui::GetIO().MouseDown[0] = true; SimulateFrame(); @@ -1609,7 +1856,7 @@ TEST_F(RealTimelineImGuiFixture, ClickCounterEventSetsSelectionIndices) { counter_data.max_value = 10.0; data.counter_data_by_group_index[0] = std::move(counter_data); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); ImGui::NewFrame(); @@ -1673,11 +1920,11 @@ TEST_F(RealTimelineImGuiFixture, SelectionMutualExclusion) { counter_data.max_value = 10.0; data.counter_data_by_group_index[1] = std::move(counter_data); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); // Step 1: Select Flame Event - ImGui::GetIO().MousePos = ImVec2(300.f, 40.f); // Over flame event + ImGui::GetIO().MousePos = ImVec2(300.f, 30.f); // Over flame event ImGui::GetIO().MouseDown[0] = true; SimulateFrame(); ImGui::GetIO().MouseDown[0] = false; @@ -1716,7 +1963,7 @@ TEST_F(RealTimelineImGuiFixture, SelectionMutualExclusion) { EXPECT_EQ(timeline_.selected_counter_index(), 0); // Step 3: Select Flame Event Again - ImGui::GetIO().MousePos = ImVec2(300.f, 40.f); + ImGui::GetIO().MousePos = ImVec2(300.f, 30.f); ImGui::GetIO().MouseDown[0] = true; SimulateFrame(); @@ -1734,11 +1981,11 @@ TEST_F(RealTimelineImGuiFixture, ClickEmptyAreaClearsSelectionIndices) { data.entry_levels.push_back(0); data.entry_start_times.push_back(0.0); data.entry_total_times.push_back(100.0); - timeline_.set_timeline_data(std::move(data)); + timeline_.SetTimelineData(std::move(data)); timeline_.SetVisibleRange({0.0, 100.0}); // Select event - ImGui::GetIO().MousePos = ImVec2(300.f, 40.f); + ImGui::GetIO().MousePos = ImVec2(300.f, 30.f); ImGui::GetIO().MouseDown[0] = true; SimulateFrame(); ImGui::GetIO().MouseDown[0] = false; @@ -1756,6 +2003,114 @@ TEST_F(RealTimelineImGuiFixture, ClickEmptyAreaClearsSelectionIndices) { EXPECT_EQ(timeline_.selected_counter_index(), -1); } +TEST_F(RealTimelineImGuiFixture, DrawsOnlyVisibleGroups) { + FlameChartTimelineData data; + // Create 100 groups. + for (int i = 0; i < 100; ++i) { + data.groups.push_back( + {.name = "Group " + std::to_string(i), .start_level = 0}); + } + // Ensure we have enough levels so DrawGroup doesn't crash or skip? + // Timeline::CalculateGroupOffsets ensures min height of 1 level (17px). + // So empty groups have height 17px. + + timeline_.SetTimelineData(std::move(data)); + // Total height ~ 1700px. + // Viewport ~1080px. + + ImGui::NewFrame(); + timeline_.Draw(); + + int child_window_count = 0; + for (ImGuiWindow* w : ImGui::GetCurrentContext()->Windows) { + if (absl::StrContains(std::string(w->Name), "TimelineChild_")) { + child_window_count++; + } + } + + // Should verify culling. + // If visible height is 1080 (minus ruler ~20, minus labels...), say 1000. + // 1000/17 ~ 58. + // Plus buffer (1 before, 1 after). ~60. + // Definitely < 100. + EXPECT_LT(child_window_count, 90); + // And > 0. + EXPECT_GT(child_window_count, 10); + + ImGui::EndFrame(); +} + +TEST_F(RealTimelineImGuiFixture, ContentHeightMatchesTotalGroupHeight) { + FlameChartTimelineData data; + // Add enough groups to have some height. + // 10 groups of height 40 (Counter). Total 400. + for (int i = 0; i < 10; ++i) { + data.groups.push_back({.type = Group::Type::kCounter, + .name = "Group " + std::to_string(i), + .start_level = 0}); + } + timeline_.SetTimelineData(std::move(data)); + // Total height should be 400.0f. + + ImGui::NewFrame(); + timeline_.Draw(); + + // Find the "Tracks" child window. + ImGuiWindow* tracks_window = nullptr; + for (ImGuiWindow* w : ImGui::GetCurrentContext()->Windows) { + if (absl::StrContains(w->Name, "Tracks") && + !absl::StrContains(w->Name, "TimelineChild")) { + tracks_window = w; + break; + } + } + ASSERT_NE(tracks_window, nullptr); + + // The expected total height is the last offset. + const float expected_height = timeline_.group_offsets().back(); + + // ContentSize is updated at the end of the frame/child. Since we are + // calling this after Draw() (which calls EndChild()), ContentSize should be + // valid. However, if it's 0, it might be because the table handling defers + // size report. + // Instead, let's look at the cursor position which tracks the layout. + // DC.CursorMaxPos tracks the maximum position reached by the cursor (content + // size). + float content_height = + tracks_window->DC.CursorMaxPos.y - tracks_window->DC.CursorStartPos.y; + + // We expect it to match exactly. + EXPECT_FLOAT_EQ(content_height, expected_height); + + ImGui::EndFrame(); +} + +TEST_F(RealTimelineImGuiFixture, + DrawGroupChildWindowHeightMatchesOffsetDifference) { + FlameChartTimelineData data; + data.groups.push_back( + {.type = Group::Type::kCounter, .name = "TestGroup", .start_level = 0}); + timeline_.SetTimelineData(std::move(data)); + + ImGui::NewFrame(); + timeline_.Draw(); + + ImGuiWindow* group_window = nullptr; + for (ImGuiWindow* w : ImGui::GetCurrentContext()->Windows) { + if (absl::StrContains(w->Name, "TimelineChild_TestGroup_0")) { + group_window = w; + break; + } + } + ASSERT_NE(group_window, nullptr); + + const float expected_height = + timeline_.group_offsets()[1] - timeline_.group_offsets()[0]; + EXPECT_FLOAT_EQ(group_window->Size.y, expected_height); + + ImGui::EndFrame(); +} + } // namespace } // namespace testing } // namespace traceviewer