1 | // SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. |
2 | // |
3 | // SPDX-License-Identifier: GPL-3.0-or-later |
4 | |
5 | #include "timelinewidget.h" |
6 | #include "event_man.h" |
7 | #include "reversedebuggerconstants.h" |
8 | #include "taskwindow.h" |
9 | |
10 | #include <QPainter> |
11 | #include <QMenu> |
12 | #include <QTime> |
13 | #include <QDebug> |
14 | #include <QMouseEvent> |
15 | |
16 | #include <assert.h> |
17 | #include <math.h> |
18 | |
19 | #define SCALE_WIDTH (100) |
20 | #define SCALE_HEIGHT (10) |
21 | #define MIN_TIME_PER_SCALE (1) |
22 | #define SCROLL_BAR_HEIGHT (10) |
23 | |
24 | namespace ReverseDebugger { |
25 | namespace Internal { |
26 | |
27 | const int g_colors[] = { |
28 | Qt::cyan, // syscall |
29 | Qt::red, // signal |
30 | Qt::blue, // dbus |
31 | Qt::magenta, // x11 |
32 | }; |
33 | |
34 | static QString formatTime(int curtime /*unit is ms*/) |
35 | { |
36 | int h = 0; |
37 | int m = 0; |
38 | int s = 0; |
39 | int ms = 0; |
40 | |
41 | // hh:mm:ss.zzz |
42 | s = curtime/1000; |
43 | ms = curtime - s*1000; |
44 | m = s/60; |
45 | s -= m*60; |
46 | h = m/60; |
47 | m -= h*60; |
48 | |
49 | return QString::asprintf("%02d:%02d:%02d.%03d" , h, m, s, ms); |
50 | } |
51 | |
52 | class TimelineWidgetPrivate |
53 | { |
54 | public: |
55 | int visibleX = 0; |
56 | int currentX = 0; |
57 | int timePerScale = 50; //unit is ms |
58 | double firstTime = 0; |
59 | double duration = 60.0 * 1000; //unit is ms |
60 | void *timeline = nullptr; |
61 | int count = 0; |
62 | int visibleBegin = 0; |
63 | int visibleEnd = 0; |
64 | int tid = -1; |
65 | int eventBegin = -1; |
66 | int eventEnd = -1; |
67 | int eventIndexBegin = -1; |
68 | int eventIndexEnd = -1; |
69 | // bit 0:disable syscall |
70 | // bit 1:disable signal; |
71 | // bit 2:disable dbus; |
72 | // bit 3:disable x11; |
73 | int categoryIds = 0; |
74 | |
75 | QScrollBar *scroll = nullptr; |
76 | TaskWindow *window = nullptr; |
77 | |
78 | QMenu * = nullptr; |
79 | QAction *zoomIn = nullptr; |
80 | QAction *zoomOut = nullptr; |
81 | QAction *zoomFit = nullptr; |
82 | }; |
83 | |
84 | TimelineWidget::TimelineWidget(QWidget *parent) |
85 | : QWidget(parent), |
86 | d(new TimelineWidgetPrivate()) |
87 | { |
88 | |
89 | d->scroll = new QScrollBar(Qt::Horizontal, this); |
90 | d->scroll->setRange(0, d->duration/d->timePerScale*SCALE_WIDTH); |
91 | d->scroll->setSingleStep(SCALE_WIDTH); |
92 | d->scroll->setMinimumHeight(SCROLL_BAR_HEIGHT); |
93 | connect(d->scroll, &QScrollBar::valueChanged, this, &TimelineWidget::valueChanged); |
94 | |
95 | d->zoomIn = new QAction(tr("Zoom in" ), this); |
96 | connect(d->zoomIn, &QAction::triggered, this, &TimelineWidget::zoomIn); |
97 | |
98 | d->zoomOut = new QAction(tr("Zoom out" ), this); |
99 | connect(d->zoomOut, &QAction::triggered, this, &TimelineWidget::zoomOut); |
100 | |
101 | d->zoomFit = new QAction(tr("Fit view" ), this); |
102 | connect(d->zoomFit, &QAction::triggered, this, &TimelineWidget::zoomFit); |
103 | } |
104 | |
105 | void TimelineWidget::paintEvent(QPaintEvent *) |
106 | { |
107 | QPainter painter(this); |
108 | painter.fillRect(0, 0, width(), height(), QColor(0x40,0x42,0x44,255)); |
109 | |
110 | int times = d->timePerScale; |
111 | for (; times >= 10; times /= 10); |
112 | |
113 | // draw timeline |
114 | // painter.setPen(Qt::black); |
115 | painter.setPen(Qt::white); |
116 | painter.setFont(QFont(QLatin1String("Arial" ), SCALE_HEIGHT)); |
117 | painter.drawLine(0, SCALE_HEIGHT*2, width(), SCALE_HEIGHT*2); |
118 | |
119 | int step = SCALE_WIDTH/times; |
120 | int begin = d->visibleX/SCALE_WIDTH*SCALE_WIDTH; |
121 | for (int x = begin - d->visibleX, i = begin / SCALE_WIDTH * d->timePerScale; |
122 | x < width(); |
123 | i += d->timePerScale) { |
124 | QString val = formatTime(i); |
125 | painter.drawText(x+2, SCALE_HEIGHT, val); |
126 | |
127 | painter.drawLine(x, SCALE_HEIGHT*2, x, SCALE_HEIGHT); |
128 | |
129 | for (int j = x + step; j < x + SCALE_WIDTH; j += step) { |
130 | painter.drawLine(j, SCALE_HEIGHT*2, j, SCALE_HEIGHT*3/2); |
131 | } |
132 | x += SCALE_WIDTH; |
133 | } |
134 | |
135 | // draw event graph |
136 | int prev_x = -1; |
137 | const EventEntry* entry = (d->timeline != nullptr) ? |
138 | get_event_pointer(d->timeline) + d->visibleBegin : nullptr; |
139 | |
140 | int cur_color = Qt::black; |
141 | |
142 | for (int i = d->visibleBegin; i < d->visibleEnd; ++i, ++entry) { |
143 | assert(entry->time >= d->firstTime); |
144 | |
145 | if ((d->categoryIds > 0) && |
146 | (d->categoryIds & (1 << (entry->type/1000 - __NR_Linux/1000)))) { |
147 | // disable this category |
148 | continue; |
149 | } |
150 | |
151 | if (d->tid > 0 && entry->tid != d->tid) |
152 | continue; |
153 | |
154 | if (d->eventIndexBegin >= 0 && |
155 | d->eventIndexEnd >= d->eventIndexBegin && |
156 | (i < d->eventIndexBegin || i > d->eventIndexEnd)){ |
157 | // not in index range! |
158 | continue; |
159 | } |
160 | |
161 | if (d->eventBegin >= 0 && |
162 | d->eventEnd >= d->eventBegin && |
163 | (entry->type < d->eventBegin || entry->type > d->eventEnd)){ |
164 | // not in event range! |
165 | continue; |
166 | } |
167 | |
168 | int x = (entry->time - d->firstTime)/d->timePerScale*SCALE_WIDTH - d->visibleX; |
169 | if (x > prev_x) { |
170 | int event_color = g_colors[entry->type/1000 - __NR_Linux/1000]; |
171 | if (event_color != cur_color) { |
172 | painter.setPen(QColor(Qt::GlobalColor(event_color))); |
173 | cur_color = event_color; |
174 | } |
175 | painter.drawLine(x, SCALE_HEIGHT*2 + 2, x, height() - SCROLL_BAR_HEIGHT); |
176 | prev_x = x; |
177 | } |
178 | else if (Qt::darkYellow != cur_color) { |
179 | // multiple events overlap |
180 | cur_color = Qt::darkYellow; |
181 | painter.setPen(QColor(Qt::GlobalColor(Qt::darkYellow))); |
182 | painter.drawLine(prev_x, SCALE_HEIGHT*2 + 2, |
183 | prev_x, height() - SCROLL_BAR_HEIGHT); |
184 | } |
185 | } |
186 | |
187 | // TODO: how to update d->currentX if zoomed ? |
188 | // draw current position if visible |
189 | painter.setPen(Qt::yellow); |
190 | if (d->currentX >= d->visibleX && d->currentX <= d->visibleX + width()) { |
191 | int x = d->currentX - d->visibleX; |
192 | int h = height() - SCROLL_BAR_HEIGHT; |
193 | painter.drawLine(x, SCALE_HEIGHT*2, x, h); |
194 | } |
195 | } |
196 | |
197 | void TimelineWidget::mousePressEvent(QMouseEvent* event) |
198 | { |
199 | if (Qt::LeftButton == event->button()) { |
200 | d->currentX = event->x() + d->visibleX; |
201 | update(); |
202 | } |
203 | } |
204 | |
205 | void TimelineWidget::mouseReleaseEvent(QMouseEvent* event) |
206 | { |
207 | Q_UNUSED(event); |
208 | } |
209 | |
210 | void TimelineWidget::mouseMoveEvent(QMouseEvent* event) |
211 | { |
212 | Q_UNUSED(event); |
213 | // check if left button is pressed |
214 | // d->currentX = event->x; |
215 | } |
216 | |
217 | void TimelineWidget::mouseDoubleClickEvent(QMouseEvent *event) |
218 | { |
219 | double x = event->x() + d->visibleX; |
220 | double time = x/SCALE_WIDTH*d->timePerScale + d->firstTime; |
221 | |
222 | qDebug() << "double click at :" << x << ", " << time; |
223 | |
224 | // NOTE: index is relative to the full event list, not to the filter list; |
225 | if (Qt::LeftButton == event->button() && d->timeline && d->window) { |
226 | const EventEntry* entry = get_event_pointer(d->timeline) + d->visibleBegin; |
227 | |
228 | for (int i = d->visibleBegin; i<d->visibleEnd; ++i, ++entry) { |
229 | if ((d->categoryIds > 0) && |
230 | (d->categoryIds & (1 << (entry->type/1000 - __NR_Linux/1000)))) { |
231 | // disable this category |
232 | continue; |
233 | } |
234 | |
235 | if (d->eventIndexBegin >= 0 && |
236 | d->eventIndexEnd >= d->eventIndexBegin && |
237 | (i < d->eventIndexBegin || i > d->eventIndexEnd)){ |
238 | // not in index range! |
239 | continue; |
240 | } |
241 | |
242 | if (d->eventBegin >= 0 && |
243 | d->eventEnd >= d->eventBegin && |
244 | (entry->type < d->eventBegin || entry->type > d->eventEnd)){ |
245 | // not in event range! |
246 | continue; |
247 | } |
248 | |
249 | // select the index range in 5. |
250 | if (fabs(entry->time - time) < 5) { |
251 | qDebug() << "double click event :" << i; |
252 | d->window->goTo(i); |
253 | break; |
254 | } |
255 | } |
256 | } |
257 | } |
258 | |
259 | void TimelineWidget::(QContextMenuEvent* event) |
260 | { |
261 | if (nullptr == d->menu) { |
262 | d->menu = new QMenu; |
263 | d->menu->setParent(this); |
264 | d->menu->addAction(d->zoomIn); |
265 | d->menu->addAction(d->zoomOut); |
266 | d->menu->addAction(d->zoomFit); |
267 | } |
268 | d->menu->exec(event->globalPos()); |
269 | } |
270 | |
271 | void TimelineWidget::zoomIn() |
272 | { |
273 | if (d->timePerScale <= MIN_TIME_PER_SCALE) { |
274 | qDebug() << "reach minimum zoom level" ; |
275 | return; |
276 | } |
277 | |
278 | int scale = 1; |
279 | int times = d->timePerScale; |
280 | double cent_time = (d->visibleX + width()/2.0)/SCALE_WIDTH * d->timePerScale; |
281 | for (; times >= 10; times /= 10, scale *= 10); |
282 | if (5 == times) { |
283 | d->timePerScale = 2 * scale; |
284 | } |
285 | else { |
286 | d->timePerScale /= 2; |
287 | } |
288 | |
289 | double cent_x = cent_time/d->timePerScale*SCALE_WIDTH; |
290 | if (cent_x > width()/2.0) { |
291 | d->visibleX = cent_x - width()/2.0; |
292 | } |
293 | else { |
294 | d->visibleX = 0; |
295 | } |
296 | updateVisibleEvent(); |
297 | d->scroll->setValue(d->visibleX); |
298 | d->scroll->setRange(0, d->duration/d->timePerScale*SCALE_WIDTH); |
299 | update(); |
300 | |
301 | qDebug() << "new unit:" << d->timePerScale << "ms, scroll range:" << d->scroll->maximum(); |
302 | } |
303 | |
304 | void TimelineWidget::zoomOut() |
305 | { |
306 | int max = d->duration/d->timePerScale*SCALE_WIDTH; |
307 | if (max <= width()) { |
308 | qDebug() << "reach maximum zoom level " << max << "<=" << width(); |
309 | return; |
310 | } |
311 | |
312 | int scale = 1; |
313 | int times = d->timePerScale; |
314 | double cent_time = (d->visibleX + width()/2.0)/SCALE_WIDTH * d->timePerScale; |
315 | for (; times >= 10; times /= 10, scale *= 10); |
316 | if (2 == times) { |
317 | d->timePerScale = 5 * scale; |
318 | } |
319 | else { |
320 | d->timePerScale *= 2; |
321 | } |
322 | |
323 | double cent_x = cent_time/d->timePerScale*SCALE_WIDTH; |
324 | if (cent_x > width()/2.0) { |
325 | d->visibleX = cent_x - width()/2.0; |
326 | } |
327 | else { |
328 | d->visibleX = 0; |
329 | } |
330 | updateVisibleEvent(); |
331 | d->scroll->setValue(d->visibleX); |
332 | d->scroll->setRange(0, d->duration/d->timePerScale*SCALE_WIDTH); |
333 | update(); |
334 | |
335 | qDebug() << "new unit:" << d->timePerScale << "ms, scroll range:" << d->scroll->maximum(); |
336 | } |
337 | |
338 | void TimelineWidget::zoomFit() |
339 | { |
340 | d->timePerScale = MIN_TIME_PER_SCALE; |
341 | |
342 | for (;;) { |
343 | int max = d->duration/d->timePerScale*SCALE_WIDTH; |
344 | qDebug() << "zoomFit try " << d->timePerScale |
345 | << "ms, max:" << max << ", width:" << width(); |
346 | if (max < width() + 100) { |
347 | break; |
348 | } |
349 | |
350 | int scale = 1; |
351 | int times = d->timePerScale; |
352 | for (; times >= 10; times /= 10, scale *= 10); |
353 | if (2 == times) { |
354 | d->timePerScale = 5 * scale; |
355 | } |
356 | else { |
357 | d->timePerScale *= 2; |
358 | } |
359 | } |
360 | |
361 | int max = d->duration/d->timePerScale*SCALE_WIDTH; |
362 | d->visibleX = 0; |
363 | updateVisibleEvent(); |
364 | d->scroll->setValue(0); |
365 | d->scroll->setRange(0, max); |
366 | d->scroll->setPageStep(max); |
367 | update(); |
368 | |
369 | qDebug() << "new unit:" << d->timePerScale << "ms, scroll range:" << d->scroll->maximum(); |
370 | } |
371 | |
372 | void TimelineWidget::resizeEvent(QResizeEvent *e) |
373 | { |
374 | QSize size = e->size(); |
375 | d->scroll->setGeometry(0, size.height() - SCROLL_BAR_HEIGHT, size.width(), SCROLL_BAR_HEIGHT); |
376 | d->scroll->setPageStep(size.width()); |
377 | } |
378 | |
379 | void TimelineWidget::valueChanged(int value) |
380 | { |
381 | if (d->visibleX != value) { |
382 | d->visibleX = value; |
383 | updateVisibleEvent(); |
384 | update(); |
385 | qDebug() << "new pos:" << value; |
386 | } |
387 | } |
388 | |
389 | void TimelineWidget::setData(TaskWindow* window, void* timeline, int count) |
390 | { |
391 | EventEntry entry; |
392 | |
393 | d->window = window; |
394 | if (nullptr == timeline) { |
395 | d->timeline = nullptr; |
396 | d->count = 0; |
397 | d->visibleBegin = 0; |
398 | d->visibleEnd = 0; |
399 | |
400 | update(); |
401 | |
402 | return; |
403 | } |
404 | |
405 | d->timeline = timeline; |
406 | d->count = count; |
407 | get_event(d->timeline, 0, &entry); |
408 | d->firstTime = entry.time; |
409 | assert(d->firstTime > 0); |
410 | get_event(d->timeline, count - 1, &entry); |
411 | assert(entry.time > d->firstTime); |
412 | d->duration = entry.time - d->firstTime; |
413 | qDebug() << "set Duration:" << d->duration |
414 | << ", first:" << d->firstTime << ", count:" << count; |
415 | |
416 | zoomFit(); |
417 | } |
418 | |
419 | void TimelineWidget::setEventTid(int tid) |
420 | { |
421 | //NOTE: tid filter can combine with event type filter |
422 | d->tid = tid; |
423 | invalidateFilter(); |
424 | } |
425 | void TimelineWidget::setEventRange(int begin, int end) |
426 | { |
427 | d->eventBegin = begin; |
428 | d->eventEnd = end; |
429 | d->eventIndexBegin = -1; |
430 | d->eventIndexEnd = -1; |
431 | invalidateFilter(); |
432 | } |
433 | void TimelineWidget::setEventIndexRange(int begin, int end) |
434 | { |
435 | d->eventBegin = -1; |
436 | d->eventEnd = -1; |
437 | d->eventIndexBegin = begin; |
438 | d->eventIndexEnd = end; |
439 | invalidateFilter(); |
440 | } |
441 | |
442 | void TimelineWidget::updateVisibleEvent(void) |
443 | { |
444 | if (!d->timeline) return; |
445 | |
446 | double begin_time = d->visibleX/SCALE_WIDTH*d->timePerScale; |
447 | double end_time = (d->visibleX + width())/SCALE_WIDTH*d->timePerScale; |
448 | |
449 | // TODO: Should merge multiple event for best draw performance if |
450 | // d->timePerScale >= 1s && d->count > 10K |
451 | |
452 | begin_time += d->firstTime; |
453 | end_time += d->firstTime; |
454 | |
455 | const EventEntry* entry = get_event_pointer(d->timeline); |
456 | for (int i = 0; i<d->count; ++i, ++entry) { |
457 | if (entry->time >= begin_time) { |
458 | d->visibleBegin = i; |
459 | break; |
460 | } |
461 | } |
462 | |
463 | if (d->firstTime + d->duration < end_time) { |
464 | d->visibleEnd = d->count; |
465 | qDebug() << "visible event range:" << d->visibleBegin << "," << d->visibleEnd; |
466 | return; |
467 | } |
468 | |
469 | for (int i = d->visibleBegin + 1; i<d->count; ++i, ++entry) { |
470 | if (entry->time > end_time) { |
471 | d->visibleEnd = i; |
472 | break; |
473 | } |
474 | } |
475 | |
476 | qDebug() << "visible event range:" << d->visibleBegin << "," << d->visibleEnd; |
477 | } |
478 | |
479 | void TimelineWidget::setFilteredCategories(const QList<QString> &categoryIds) |
480 | { |
481 | long mask = 0; |
482 | QString ids[4] = { |
483 | Constants::EVENT_CATEGORY_SYSCALL, |
484 | Constants::EVENT_CATEGORY_SIGNAL, |
485 | Constants::EVENT_CATEGORY_DBUS, |
486 | Constants::EVENT_CATEGORY_X11, |
487 | }; |
488 | |
489 | for (int i = 0; i < categoryIds.size(); ++i) { |
490 | if (categoryIds.at(i) == ids[0]) |
491 | mask |= 1; |
492 | else if (categoryIds.at(i) == ids[1]) |
493 | mask |= 1<<1; |
494 | else if (categoryIds.at(i) == ids[2]) |
495 | mask |= 1<<2; |
496 | else if (categoryIds.at(i) == ids[3]) |
497 | mask |= 1<<3; |
498 | } |
499 | |
500 | qDebug() << "d->categoryIds:" << (void*)mask; |
501 | if (mask != d->categoryIds) { |
502 | d->categoryIds = mask; |
503 | invalidateFilter(); |
504 | } |
505 | } |
506 | |
507 | void TimelineWidget::invalidateFilter() |
508 | { |
509 | update(); |
510 | } |
511 | |
512 | } // namespace Internal |
513 | } // namespace ReverseDebugger |
514 | |
515 | |