1 // Copyright 2018 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 #include "tools/render/trace_program.h"
16
17 #include "absl/flags/flag.h"
18 #include "absl/time/clock.h"
19 #include "tools/render/layout_constants.h"
20
21 ABSL_FLAG(bool, show_fps, false, "Show the current framerate of the program");
22 ABSL_FLAG(bool, vsync, true, "Enables vsync");
23
24 ABSL_FLAG(double,
25 mouseover_threshold,
26 3.0,
27 "The minimum size of a single packet (in fractional pixels) that "
28 "causes the packet information box being showed");
29
30 namespace quic_trace {
31 namespace render {
32
TraceProgram()33 TraceProgram::TraceProgram()
34 : window_(SDL_CreateWindow(
35 "QUIC trace viewer",
36 0,
37 0,
38 state_.window.x,
39 state_.window.y,
40 SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI)),
41 context_(*window_) {
42 UpdateWindowSize();
43 state_buffer_ = std::make_unique<ProgramState>(&state_);
44 renderer_ = std::make_unique<TraceRenderer>(state_buffer_.get());
45 text_renderer_ = std::make_unique<TextRenderer>(state_buffer_.get());
46 axis_renderer_ = std::make_unique<AxisRenderer>(text_renderer_.get(),
47 state_buffer_.get());
48 rectangle_renderer_ =
49 std::make_unique<RectangleRenderer>(state_buffer_.get());
50
51 SDL_GL_SetSwapInterval(absl::GetFlag(FLAGS_vsync) ? 1 : 0);
52 SDL_SetWindowMinimumSize(*window_, 640, 480);
53
54 glEnable(GL_BLEND);
55 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
56 }
57
LoadTrace(std::unique_ptr<Trace> trace)58 void TraceProgram::LoadTrace(std::unique_ptr<Trace> trace) {
59 std::stable_sort(
60 trace->mutable_events()->begin(), trace->mutable_events()->end(),
61 [](const Event& a, const Event& b) { return a.time_us() < b.time_us(); });
62 trace_ = std::make_unique<ProcessedTrace>(std::move(trace), renderer_.get());
63 state_.viewport.x = renderer_->max_x();
64 state_.viewport.y = renderer_->max_y();
65 }
66
Loop()67 void TraceProgram::Loop() {
68 while (!quit_) {
69 absl::Time frame_start = absl::Now();
70 PollEvents();
71 PollKeyboard();
72 PollMouse();
73 EnsureBounds();
74
75 state_buffer_->Refresh();
76
77 // Render.
78 glClearColor(1.f, 1.f, 1.f, 1.f);
79 glClear(GL_COLOR_BUFFER_BIT);
80 // Note that the order of calls below determines what is drawn on top of
81 // what.
82 renderer_->Render();
83 axis_renderer_->Render();
84 MaybeShowFramerate();
85 DrawRightSideTables();
86 // The batch object renderers should be called last.
87 rectangle_renderer_->Render();
88 text_renderer_->DrawAll();
89 SDL_GL_SwapWindow(*window_);
90
91 absl::Time frame_end = absl::Now();
92 frame_duration_ = 0.25 * (frame_end - frame_start) + 0.75 * frame_duration_;
93 }
94 }
95
ScaleAdditiveFactor(float x)96 float TraceProgram::ScaleAdditiveFactor(float x) {
97 return x * absl::ToDoubleSeconds(frame_duration_) /
98 absl::ToDoubleSeconds(kReferenceFrameDuration);
99 }
100
ScaleMultiplicativeFactor(float k)101 float TraceProgram::ScaleMultiplicativeFactor(float k) {
102 return std::pow(k, absl::ToDoubleSeconds(frame_duration_) /
103 absl::ToDoubleSeconds(kReferenceFrameDuration));
104 }
105
Zoom(float zoom)106 void TraceProgram::Zoom(float zoom) {
107 float zoom_factor = std::abs(zoom);
108 float sign = std::copysign(1.f, zoom);
109
110 // Ensure that the central point doesn't move.
111 state_.offset.x += sign * (1 - zoom_factor) * state_.viewport.x / 2;
112 state_.offset.y += sign * (1 - zoom_factor) * state_.viewport.y / 2;
113
114 state_.viewport.x *= std::pow(zoom_factor, sign);
115 state_.viewport.y *= std::pow(zoom_factor, sign);
116 }
117
UpdateWindowSize()118 void TraceProgram::UpdateWindowSize() {
119 int width, height;
120 SDL_GL_GetDrawableSize(*window_, &width, &height);
121 state_.window = vec2(width, height);
122 glViewport(0, 0, width, height);
123
124 int input_width, input_height;
125 SDL_GetWindowSize(*window_, &input_width, &input_height);
126 input_scale_ = vec2((float)width / input_width, (float)height / input_height);
127
128 const float kReferenceDpi = 100.f;
129 float dpi;
130 int result = SDL_GetDisplayDPI(SDL_GetWindowDisplayIndex(*window_), &dpi,
131 nullptr, nullptr);
132 if (result < 0) {
133 LOG(WARNING) << "Failed to retrieve window DPI";
134 }
135 state_.dpi_scale = input_scale_.x * dpi / kReferenceDpi;
136 }
137
PollEvents()138 void TraceProgram::PollEvents() {
139 SDL_Event event;
140 while (SDL_PollEvent(&event)) {
141 switch (event.type) {
142 case SDL_QUIT:
143 quit_ = true;
144 break;
145
146 case SDL_WINDOWEVENT:
147 if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
148 UpdateWindowSize();
149 }
150 break;
151
152 case SDL_KEYDOWN:
153 if (event.key.keysym.scancode == SDL_SCANCODE_H) {
154 show_online_help_ = !show_online_help_;
155 }
156 break;
157
158 case SDL_MOUSEWHEEL: {
159 int wheel_offset = event.wheel.y;
160 if (wheel_offset == 0) {
161 break;
162 }
163 if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
164 wheel_offset *= -1;
165 }
166 // Note that this does not need to be scaled with framerate since
167 // mousewheel events are discrete.
168 Zoom(std::copysign(0.95f, wheel_offset));
169 break;
170 }
171 }
172 }
173 }
174
PollKeyboard()175 void TraceProgram::PollKeyboard() {
176 const uint8_t* state = SDL_GetKeyboardState(nullptr);
177 if (state[SDL_SCANCODE_Q]) {
178 quit_ = true;
179 }
180
181 // Zoom handling.
182 const float zoom_factor = ScaleMultiplicativeFactor(0.98);
183 if (state[SDL_SCANCODE_Z]) {
184 Zoom(+zoom_factor);
185 }
186 if (state[SDL_SCANCODE_X]) {
187 Zoom(-zoom_factor);
188 }
189 if (state[SDL_SCANCODE_UP]) {
190 state_.offset.y += ScaleAdditiveFactor(state_.viewport.y * 0.03);
191 }
192 if (state[SDL_SCANCODE_DOWN]) {
193 state_.offset.y -= ScaleAdditiveFactor(state_.viewport.y * 0.03);
194 }
195 if (state[SDL_SCANCODE_LEFT]) {
196 state_.offset.x -= ScaleAdditiveFactor(state_.viewport.x * 0.03);
197 }
198 if (state[SDL_SCANCODE_RIGHT]) {
199 state_.offset.x += ScaleAdditiveFactor(state_.viewport.x * 0.03);
200 }
201 if (state[SDL_SCANCODE_R]) {
202 absl::optional<Box> new_viewport =
203 trace_->BoundContainedPackets(Box{state_.offset, state_.viewport});
204 if (new_viewport) {
205 state_.offset = new_viewport->origin;
206 state_.viewport = new_viewport->size;
207 }
208 }
209 }
210
PollMouse()211 void TraceProgram::PollMouse() {
212 int x, y;
213 uint32_t buttons = SDL_GetMouseState(&x, &y);
214 const uint8_t* state = SDL_GetKeyboardState(nullptr);
215 bool shift = state[SDL_SCANCODE_LSHIFT] || state[SDL_SCANCODE_RSHIFT];
216 x *= input_scale_.x;
217 y *= input_scale_.y;
218
219 HandlePanning((buttons & SDL_BUTTON(SDL_BUTTON_LEFT)) && !shift, x,
220 state_.window.y - y);
221 HandleSummary((buttons & SDL_BUTTON(SDL_BUTTON_LEFT)) && shift, x);
222 HandleZooming(buttons & SDL_BUTTON(SDL_BUTTON_RIGHT), x, state_.window.y - y);
223 HandleMouseover(x, state_.window.y - y);
224 }
225
HandlePanning(bool pressed,int x,int y)226 void TraceProgram::HandlePanning(bool pressed, int x, int y) {
227 if (!pressed) {
228 panning_ = false;
229 return;
230 }
231
232 if (!panning_) {
233 panning_ = true;
234 panning_last_pos_ = vec2(x, y);
235 return;
236 }
237
238 state_.offset += WindowToTraceRelative(panning_last_pos_ - vec2(x, y));
239
240 panning_last_pos_ = vec2(x, y);
241 }
242
HandleZooming(bool pressed,int x,int y)243 void TraceProgram::HandleZooming(bool pressed, int x, int y) {
244 if (!pressed && !zooming_) {
245 return;
246 }
247
248 if (pressed && !zooming_) {
249 zooming_ = true;
250 zoom_start_x_ = x;
251 zoom_start_y_ = y;
252 return;
253 }
254
255 Box window_box = BoundingBox(vec2(x, y), vec2(zoom_start_x_, zoom_start_y_));
256 // Ensure that the selection does not go out of the bounds of the trace view.
257 window_box = IntersectBoxes(window_box, TraceBounds());
258
259 if (pressed && zooming_) {
260 // User is still selecting the area to zoom into. Draw a transparent grey
261 // rectangle to indicate the currently picked area.
262 rectangle_renderer_->AddRectangle(window_box, 0x00000033);
263 return;
264 }
265
266 // Actually zoom in.
267 zooming_ = false;
268 // Discard all attempts to zoom in into something smaller than 16x16 pixels,
269 // as those are more liekly to be accidental.
270 if (window_box.size.x < 16 || window_box.size.y < 16) {
271 return;
272 }
273
274 const Box trace_box = WindowToTraceCoordinates(window_box);
275 state_.viewport = trace_box.size;
276 state_.offset = trace_box.origin;
277 }
278
HandleSummary(bool pressed,int x)279 void TraceProgram::HandleSummary(bool pressed, int x) {
280 if (!pressed) {
281 summary_ = false;
282 return;
283 }
284
285 if (!summary_) {
286 summary_ = true;
287 summary_start_x_ = x;
288 return;
289 }
290
291 Box window_box = BoundingBox(vec2(x, 0), vec2(summary_start_x_, 99999.f));
292 // Ensure that the selection does not go out of the bounds of the trace view.
293 window_box = IntersectBoxes(window_box, TraceBounds());
294 if (window_box.size.x < 1) {
295 return;
296 }
297 rectangle_renderer_->AddRectangle(window_box, 0x00000033);
298
299 const Box selected = WindowToTraceCoordinates(window_box);
300
301 summary_table_.emplace(state_buffer_.get(), text_renderer_.get(),
302 rectangle_renderer_.get());
303 if (!trace_->SummaryTable(&*summary_table_, selected.origin.x,
304 selected.origin.x + selected.size.x)) {
305 summary_table_ = absl::nullopt;
306 }
307 }
308
HandleMouseover(int x,int y)309 void TraceProgram::HandleMouseover(int x, int y) {
310 vec2 window_pos(x, y);
311 if (!IsInside(window_pos, TraceBounds())) {
312 return;
313 }
314
315 vec2 trace_pos = WindowToTraceCoordinates(window_pos);
316
317 float packet_size_in_pixels =
318 kSentPacketDurationMs / state_.viewport.x * state_.window.x;
319 if (packet_size_in_pixels < absl::GetFlag(FLAGS_mouseover_threshold)) {
320 renderer_->set_highlighted_packet(-1);
321 return;
322 }
323
324 constexpr int kPixelMargin = 64;
325 const vec2 margin = WindowToTraceRelative(vec2(kPixelMargin, kPixelMargin));
326 ProcessedTrace::PacketSearchResult hovered_packet =
327 trace_->FindPacketContainingPoint(trace_pos, margin);
328 renderer_->set_highlighted_packet(hovered_packet.index);
329 if (hovered_packet.event == nullptr) {
330 return;
331 }
332
333 constexpr vec2 kMouseoverOffset = vec2(32, 32);
334 Table table(state_buffer_.get(), text_renderer_.get(),
335 rectangle_renderer_.get());
336 trace_->FillTableForPacket(&table, hovered_packet.as_rendered,
337 hovered_packet.event);
338 vec2 table_size = table.Layout();
339 table.Draw(vec2(x, y) + kMouseoverOffset + 0 * table_size);
340 }
341
GenerateOnlineHelp()342 Table TraceProgram::GenerateOnlineHelp() {
343 Table table(state_buffer_.get(), text_renderer_.get(),
344 rectangle_renderer_.get());
345 table.AddHeader("Help");
346 table.AddRow("h", "Toggle help");
347 table.AddRow("z", "Zoom in");
348 table.AddRow("x", "Zoom out");
349 table.AddRow("r", "Rescale");
350 table.AddRow("Arrows", "Move");
351 table.AddRow("LMouse", "Move");
352 table.AddRow("RMouse", "Zoom");
353 table.AddRow("Shift+LM", "Summary");
354 return table;
355 }
356
EnsureBounds()357 void TraceProgram::EnsureBounds() {
358 constexpr float kTimeMargin = 3000.f;
359 constexpr float kOffsetMargin = 10 * 1350.f;
360 state_.viewport.x =
361 std::min(state_.viewport.x, renderer_->max_x() + 2 * kTimeMargin);
362 state_.viewport.y =
363 std::min(state_.viewport.y, renderer_->max_y() + 2 * kOffsetMargin);
364
365 const float min_x = -kTimeMargin;
366 const float min_y = -kOffsetMargin;
367 const float max_x = renderer_->max_x() + kTimeMargin;
368 const float max_y = renderer_->max_y() + kOffsetMargin;
369
370 state_.offset.x = std::max(min_x, state_.offset.x);
371 state_.offset.x = std::min(max_x - state_.viewport.x, state_.offset.x);
372 state_.offset.y = std::max(min_y, state_.offset.y);
373 state_.offset.y = std::min(max_y - state_.viewport.y, state_.offset.y);
374 }
375
WindowToTraceRelative(vec2 vector)376 vec2 TraceProgram::WindowToTraceRelative(vec2 vector) {
377 const vec2 pixel_viewport = state_.window - 2 * TraceMargin(state_.dpi_scale);
378 return vector * state_.viewport / pixel_viewport;
379 }
380
WindowToTraceCoordinates(vec2 point)381 vec2 TraceProgram::WindowToTraceCoordinates(vec2 point) {
382 return state_.offset +
383 WindowToTraceRelative(point - TraceMargin(state_.dpi_scale));
384 }
385
WindowToTraceCoordinates(Box box)386 Box TraceProgram::WindowToTraceCoordinates(Box box) {
387 return BoundingBox(WindowToTraceCoordinates(box.origin),
388 WindowToTraceCoordinates(box.origin + box.size));
389 }
390
TraceBounds()391 Box TraceProgram::TraceBounds() {
392 return BoundingBox(TraceMargin(state_.dpi_scale),
393 state_.window - TraceMargin(state_.dpi_scale));
394 }
395
MaybeShowFramerate()396 void TraceProgram::MaybeShowFramerate() {
397 if (!absl::GetFlag(FLAGS_show_fps)) {
398 return;
399 }
400
401 char buffer[16];
402 snprintf(buffer, sizeof(buffer), "%.2ffps",
403 1. / absl::ToDoubleSeconds(frame_duration_));
404 std::shared_ptr<const Text> framerate = text_renderer_->RenderText(buffer);
405 text_renderer_->AddText(framerate, 0, state_.window.y - framerate->height());
406 }
407
DrawRightSideTables()408 void TraceProgram::DrawRightSideTables() {
409 float distance = 20.f * state_.dpi_scale;
410 vec2 offset = state_.window - vec2(distance, distance);
411
412 if (summary_table_.has_value()) {
413 vec2 table_size = summary_table_->Layout();
414 summary_table_->Draw(offset - table_size);
415 offset.y -= table_size.y + distance;
416 }
417
418 if (show_online_help_) {
419 Table online_help = GenerateOnlineHelp();
420 vec2 table_size = online_help.Layout();
421 online_help.Draw(offset - table_size);
422 offset.y -= table_size.y + distance;
423 }
424
425 summary_table_ = absl::nullopt;
426 }
427
428 } // namespace render
429 } // namespace quic_trace
430