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
24namespace ReverseDebugger {
25namespace Internal {
26
27const int g_colors[] = {
28 Qt::cyan, // syscall
29 Qt::red, // signal
30 Qt::blue, // dbus
31 Qt::magenta, // x11
32};
33
34static 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
52class TimelineWidgetPrivate
53{
54public:
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 *menu = nullptr;
79 QAction *zoomIn = nullptr;
80 QAction *zoomOut = nullptr;
81 QAction *zoomFit = nullptr;
82};
83
84TimelineWidget::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
105void 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
197void TimelineWidget::mousePressEvent(QMouseEvent* event)
198{
199 if (Qt::LeftButton == event->button()) {
200 d->currentX = event->x() + d->visibleX;
201 update();
202 }
203}
204
205void TimelineWidget::mouseReleaseEvent(QMouseEvent* event)
206{
207 Q_UNUSED(event);
208}
209
210void TimelineWidget::mouseMoveEvent(QMouseEvent* event)
211{
212 Q_UNUSED(event);
213 // check if left button is pressed
214 // d->currentX = event->x;
215}
216
217void 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
259void TimelineWidget::contextMenuEvent(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
271void 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
304void 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
338void 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
372void 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
379void 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
389void 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
419void TimelineWidget::setEventTid(int tid)
420{
421 //NOTE: tid filter can combine with event type filter
422 d->tid = tid;
423 invalidateFilter();
424}
425void 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}
433void 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
442void 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
479void 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
507void TimelineWidget::invalidateFilter()
508{
509 update();
510}
511
512} // namespace Internal
513} // namespace ReverseDebugger
514
515