blob: baa2b220238af7a46af0ea6ba4e1d890e525476f [file] [log] [blame]
Adrian Hunterb3a675462019-02-22 09:27:18 +02001#!/usr/bin/env python2
Adrian Hunter3e71c702018-10-01 09:28:35 +03002# SPDX-License-Identifier: GPL-2.0
3# exported-sql-viewer.py: view data from sql database
4# Copyright (c) 2014-2018, Intel Corporation.
Adrian Hunter4b715d22015-07-17 19:33:45 +03005
Adrian Hunter1fe03b52017-08-03 11:31:30 +03006# To use this script you will need to have exported data using either the
7# export-to-sqlite.py or the export-to-postgresql.py script. Refer to those
8# scripts for details.
Adrian Hunter4b715d22015-07-17 19:33:45 +03009#
Adrian Hunter1fe03b52017-08-03 11:31:30 +030010# Following on from the example in the export scripts, a
Adrian Hunter4b715d22015-07-17 19:33:45 +030011# call-graph can be displayed for the pt_example database like this:
12#
Adrian Hunter031c2a02018-10-01 09:28:46 +030013# python tools/perf/scripts/python/exported-sql-viewer.py pt_example
Adrian Hunter4b715d22015-07-17 19:33:45 +030014#
Adrian Hunter1fe03b52017-08-03 11:31:30 +030015# Note that for PostgreSQL, this script supports connecting to remote databases
16# by setting hostname, port, username, password, and dbname e.g.
Adrian Hunter4b715d22015-07-17 19:33:45 +030017#
Adrian Hunter031c2a02018-10-01 09:28:46 +030018# python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example"
Adrian Hunter4b715d22015-07-17 19:33:45 +030019#
20# The result is a GUI window with a tree representing a context-sensitive
21# call-graph. Expanding a couple of levels of the tree and adjusting column
22# widths to suit will display something like:
23#
24# Call Graph: pt_example
25# Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
26# v- ls
27# v- 2638:2638
28# v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
29# |- unknown unknown 1 13198 0.1 1 0.0
30# >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
31# >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
32# v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
33# >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
34# >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
35# >- __libc_csu_init ls 1 10354 0.1 10 0.0
36# |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
37# v- main ls 1 8182043 99.6 180254 99.9
38#
39# Points to note:
40# The top level is a command name (comm)
41# The next level is a thread (pid:tid)
42# Subsequent levels are functions
43# 'Count' is the number of calls
44# 'Time' is the elapsed time until the function returns
45# Percentages are relative to the level above
46# 'Branch Count' is the total number of branches for that function and all
47# functions that it calls
48
Adrian Hunter76099f92018-10-23 10:59:49 +030049# There is also a "All branches" report, which displays branches and
50# possibly disassembly. However, presently, the only supported disassembler is
51# Intel XED, and additionally the object code must be present in perf build ID
52# cache. To use Intel XED, libxed.so must be present. To build and install
53# libxed.so:
54# git clone https://github.com/intelxed/mbuild.git mbuild
55# git clone https://github.com/intelxed/xed
56# cd xed
57# ./mfile.py --share
58# sudo ./mfile.py --prefix=/usr/local install
59# sudo ldconfig
60#
61# Example report:
62#
63# Time CPU Command PID TID Branch Type In Tx Branch
64# 8107675239590 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
65# 7fab593ea260 48 89 e7 mov %rsp, %rdi
66# 8107675239899 2 ls 22011 22011 hardware interrupt No 7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
67# 8107675241900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
68# 7fab593ea260 48 89 e7 mov %rsp, %rdi
69# 7fab593ea263 e8 c8 06 00 00 callq 0x7fab593ea930
70# 8107675241900 2 ls 22011 22011 call No 7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so)
71# 7fab593ea930 55 pushq %rbp
72# 7fab593ea931 48 89 e5 mov %rsp, %rbp
73# 7fab593ea934 41 57 pushq %r15
74# 7fab593ea936 41 56 pushq %r14
75# 7fab593ea938 41 55 pushq %r13
76# 7fab593ea93a 41 54 pushq %r12
77# 7fab593ea93c 53 pushq %rbx
78# 7fab593ea93d 48 89 fb mov %rdi, %rbx
79# 7fab593ea940 48 83 ec 68 sub $0x68, %rsp
80# 7fab593ea944 0f 31 rdtsc
81# 7fab593ea946 48 c1 e2 20 shl $0x20, %rdx
82# 7fab593ea94a 89 c0 mov %eax, %eax
83# 7fab593ea94c 48 09 c2 or %rax, %rdx
84# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax
85# 8107675242232 2 ls 22011 22011 hardware interrupt No 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
86# 8107675242900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so)
87# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax
88# 7fab593ea956 48 89 15 3b 13 22 00 movq %rdx, 0x22133b(%rip)
89# 8107675243232 2 ls 22011 22011 hardware interrupt No 7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
90
Tony Jonesbeda0e72019-03-08 16:05:15 -080091from __future__ import print_function
92
Adrian Hunter4b715d22015-07-17 19:33:45 +030093import sys
Adrian Hunter1beb5c72018-10-01 09:28:47 +030094import weakref
95import threading
Adrian Hunterebd70c72018-10-01 09:28:48 +030096import string
Tony Jonesbeda0e72019-03-08 16:05:15 -080097try:
98 # Python2
99 import cPickle as pickle
100 # size of pickled integer big enough for record size
101 glb_nsz = 8
102except ImportError:
103 import pickle
104 glb_nsz = 16
Adrian Hunter8392b742018-10-01 09:28:50 +0300105import re
106import os
Adrian Hunter4b715d22015-07-17 19:33:45 +0300107from PySide.QtCore import *
108from PySide.QtGui import *
109from PySide.QtSql import *
Adrian Hunter8453c932019-03-27 09:28:25 +0200110pyside_version_1 = True
Adrian Hunter4b715d22015-07-17 19:33:45 +0300111from decimal import *
Adrian Hunter8392b742018-10-01 09:28:50 +0300112from ctypes import *
113from multiprocessing import Process, Array, Value, Event
Adrian Hunter4b715d22015-07-17 19:33:45 +0300114
Tony Jonesbeda0e72019-03-08 16:05:15 -0800115# xrange is range in Python3
116try:
117 xrange
118except NameError:
119 xrange = range
120
121def printerr(*args, **keyword_args):
122 print(*args, file=sys.stderr, **keyword_args)
123
Adrian Hunter4be9ace2018-10-01 09:28:44 +0300124# Data formatting helpers
125
Adrian Hunter76099f92018-10-23 10:59:49 +0300126def tohex(ip):
127 if ip < 0:
128 ip += 1 << 64
129 return "%x" % ip
130
131def offstr(offset):
132 if offset:
133 return "+0x%x" % offset
134 return ""
135
Adrian Hunter4be9ace2018-10-01 09:28:44 +0300136def dsoname(name):
137 if name == "[kernel.kallsyms]":
138 return "[kernel]"
139 return name
140
Adrian Hunter210cf1f2018-11-04 17:12:36 +0200141def findnth(s, sub, n, offs=0):
142 pos = s.find(sub)
143 if pos < 0:
144 return pos
145 if n <= 1:
146 return offs + pos
147 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
148
Adrian Hunter4be9ace2018-10-01 09:28:44 +0300149# Percent to one decimal place
150
151def PercentToOneDP(n, d):
152 if not d:
153 return "0.0"
154 x = (n * Decimal(100)) / d
155 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
156
157# Helper for queries that must not fail
158
159def QueryExec(query, stmt):
160 ret = query.exec_(stmt)
161 if not ret:
162 raise Exception("Query failed: " + query.lastError().text())
163
Adrian Hunterebd70c72018-10-01 09:28:48 +0300164# Background thread
165
166class Thread(QThread):
167
168 done = Signal(object)
169
170 def __init__(self, task, param=None, parent=None):
171 super(Thread, self).__init__(parent)
172 self.task = task
173 self.param = param
174
175 def run(self):
176 while True:
177 if self.param is None:
178 done, result = self.task()
179 else:
180 done, result = self.task(self.param)
181 self.done.emit(result)
182 if done:
183 break
184
Adrian Hunter70d831e2018-10-01 09:28:43 +0300185# Tree data model
186
Adrian Hunter4b715d22015-07-17 19:33:45 +0300187class TreeModel(QAbstractItemModel):
188
Adrian Huntera448ba22019-02-28 15:00:29 +0200189 def __init__(self, glb, parent=None):
Adrian Hunter4b715d22015-07-17 19:33:45 +0300190 super(TreeModel, self).__init__(parent)
Adrian Huntera448ba22019-02-28 15:00:29 +0200191 self.glb = glb
192 self.root = self.GetRoot()
Adrian Hunter70d831e2018-10-01 09:28:43 +0300193 self.last_row_read = 0
Adrian Hunter4b715d22015-07-17 19:33:45 +0300194
Adrian Hunter70d831e2018-10-01 09:28:43 +0300195 def Item(self, parent):
196 if parent.isValid():
197 return parent.internalPointer()
198 else:
199 return self.root
Adrian Hunter4b715d22015-07-17 19:33:45 +0300200
201 def rowCount(self, parent):
Adrian Hunter70d831e2018-10-01 09:28:43 +0300202 result = self.Item(parent).childCount()
203 if result < 0:
204 result = 0
205 self.dataChanged.emit(parent, parent)
206 return result
207
208 def hasChildren(self, parent):
209 return self.Item(parent).hasChildren()
Adrian Hunter4b715d22015-07-17 19:33:45 +0300210
211 def headerData(self, section, orientation, role):
212 if role == Qt.TextAlignmentRole:
Adrian Hunter70d831e2018-10-01 09:28:43 +0300213 return self.columnAlignment(section)
Adrian Hunter4b715d22015-07-17 19:33:45 +0300214 if role != Qt.DisplayRole:
215 return None
216 if orientation != Qt.Horizontal:
217 return None
Adrian Hunter70d831e2018-10-01 09:28:43 +0300218 return self.columnHeader(section)
Adrian Hunter4b715d22015-07-17 19:33:45 +0300219
220 def parent(self, child):
221 child_item = child.internalPointer()
222 if child_item is self.root:
223 return QModelIndex()
224 parent_item = child_item.getParentItem()
225 return self.createIndex(parent_item.getRow(), 0, parent_item)
226
227 def index(self, row, column, parent):
Adrian Hunter70d831e2018-10-01 09:28:43 +0300228 child_item = self.Item(parent).getChildItem(row)
Adrian Hunter4b715d22015-07-17 19:33:45 +0300229 return self.createIndex(row, column, child_item)
230
Adrian Hunter70d831e2018-10-01 09:28:43 +0300231 def DisplayData(self, item, index):
232 return item.getData(index.column())
233
Adrian Hunter8392b742018-10-01 09:28:50 +0300234 def FetchIfNeeded(self, row):
235 if row > self.last_row_read:
236 self.last_row_read = row
237 if row + 10 >= self.root.child_count:
238 self.fetcher.Fetch(glb_chunk_sz)
239
240 def columnAlignment(self, column):
241 return Qt.AlignLeft
242
243 def columnFont(self, column):
244 return None
245
246 def data(self, index, role):
247 if role == Qt.TextAlignmentRole:
248 return self.columnAlignment(index.column())
249 if role == Qt.FontRole:
250 return self.columnFont(index.column())
251 if role != Qt.DisplayRole:
252 return None
253 item = index.internalPointer()
254 return self.DisplayData(item, index)
255
256# Table data model
257
258class TableModel(QAbstractTableModel):
259
260 def __init__(self, parent=None):
261 super(TableModel, self).__init__(parent)
262 self.child_count = 0
263 self.child_items = []
264 self.last_row_read = 0
265
266 def Item(self, parent):
267 if parent.isValid():
268 return parent.internalPointer()
269 else:
270 return self
271
272 def rowCount(self, parent):
273 return self.child_count
274
275 def headerData(self, section, orientation, role):
276 if role == Qt.TextAlignmentRole:
277 return self.columnAlignment(section)
278 if role != Qt.DisplayRole:
279 return None
280 if orientation != Qt.Horizontal:
281 return None
282 return self.columnHeader(section)
283
284 def index(self, row, column, parent):
285 return self.createIndex(row, column, self.child_items[row])
286
287 def DisplayData(self, item, index):
288 return item.getData(index.column())
289
290 def FetchIfNeeded(self, row):
291 if row > self.last_row_read:
292 self.last_row_read = row
293 if row + 10 >= self.child_count:
294 self.fetcher.Fetch(glb_chunk_sz)
295
Adrian Hunter70d831e2018-10-01 09:28:43 +0300296 def columnAlignment(self, column):
297 return Qt.AlignLeft
298
299 def columnFont(self, column):
300 return None
301
Adrian Hunter4b715d22015-07-17 19:33:45 +0300302 def data(self, index, role):
303 if role == Qt.TextAlignmentRole:
Adrian Hunter70d831e2018-10-01 09:28:43 +0300304 return self.columnAlignment(index.column())
305 if role == Qt.FontRole:
306 return self.columnFont(index.column())
Adrian Hunter4b715d22015-07-17 19:33:45 +0300307 if role != Qt.DisplayRole:
308 return None
Adrian Hunter70d831e2018-10-01 09:28:43 +0300309 item = index.internalPointer()
310 return self.DisplayData(item, index)
311
Adrian Hunter1beb5c72018-10-01 09:28:47 +0300312# Model cache
313
314model_cache = weakref.WeakValueDictionary()
315model_cache_lock = threading.Lock()
316
317def LookupCreateModel(model_name, create_fn):
318 model_cache_lock.acquire()
319 try:
320 model = model_cache[model_name]
321 except:
322 model = None
323 if model is None:
324 model = create_fn()
325 model_cache[model_name] = model
326 model_cache_lock.release()
327 return model
328
Adrian Hunterebd70c72018-10-01 09:28:48 +0300329# Find bar
330
331class FindBar():
332
333 def __init__(self, parent, finder, is_reg_expr=False):
334 self.finder = finder
335 self.context = []
336 self.last_value = None
337 self.last_pattern = None
338
339 label = QLabel("Find:")
340 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
341
342 self.textbox = QComboBox()
343 self.textbox.setEditable(True)
344 self.textbox.currentIndexChanged.connect(self.ValueChanged)
345
346 self.progress = QProgressBar()
347 self.progress.setRange(0, 0)
348 self.progress.hide()
349
350 if is_reg_expr:
351 self.pattern = QCheckBox("Regular Expression")
352 else:
353 self.pattern = QCheckBox("Pattern")
354 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
355
356 self.next_button = QToolButton()
357 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
358 self.next_button.released.connect(lambda: self.NextPrev(1))
359
360 self.prev_button = QToolButton()
361 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
362 self.prev_button.released.connect(lambda: self.NextPrev(-1))
363
364 self.close_button = QToolButton()
365 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
366 self.close_button.released.connect(self.Deactivate)
367
368 self.hbox = QHBoxLayout()
369 self.hbox.setContentsMargins(0, 0, 0, 0)
370
371 self.hbox.addWidget(label)
372 self.hbox.addWidget(self.textbox)
373 self.hbox.addWidget(self.progress)
374 self.hbox.addWidget(self.pattern)
375 self.hbox.addWidget(self.next_button)
376 self.hbox.addWidget(self.prev_button)
377 self.hbox.addWidget(self.close_button)
378
379 self.bar = QWidget()
380 self.bar.setLayout(self.hbox);
381 self.bar.hide()
382
383 def Widget(self):
384 return self.bar
385
386 def Activate(self):
387 self.bar.show()
388 self.textbox.setFocus()
389
390 def Deactivate(self):
391 self.bar.hide()
392
393 def Busy(self):
394 self.textbox.setEnabled(False)
395 self.pattern.hide()
396 self.next_button.hide()
397 self.prev_button.hide()
398 self.progress.show()
399
400 def Idle(self):
401 self.textbox.setEnabled(True)
402 self.progress.hide()
403 self.pattern.show()
404 self.next_button.show()
405 self.prev_button.show()
406
407 def Find(self, direction):
408 value = self.textbox.currentText()
409 pattern = self.pattern.isChecked()
410 self.last_value = value
411 self.last_pattern = pattern
412 self.finder.Find(value, direction, pattern, self.context)
413
414 def ValueChanged(self):
415 value = self.textbox.currentText()
416 pattern = self.pattern.isChecked()
417 index = self.textbox.currentIndex()
418 data = self.textbox.itemData(index)
419 # Store the pattern in the combo box to keep it with the text value
420 if data == None:
421 self.textbox.setItemData(index, pattern)
422 else:
423 self.pattern.setChecked(data)
424 self.Find(0)
425
426 def NextPrev(self, direction):
427 value = self.textbox.currentText()
428 pattern = self.pattern.isChecked()
429 if value != self.last_value:
430 index = self.textbox.findText(value)
431 # Allow for a button press before the value has been added to the combo box
432 if index < 0:
433 index = self.textbox.count()
434 self.textbox.addItem(value, pattern)
435 self.textbox.setCurrentIndex(index)
436 return
437 else:
438 self.textbox.setItemData(index, pattern)
439 elif pattern != self.last_pattern:
440 # Keep the pattern recorded in the combo box up to date
441 index = self.textbox.currentIndex()
442 self.textbox.setItemData(index, pattern)
443 self.Find(direction)
444
445 def NotFound(self):
446 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
447
Adrian Hunter341e73c2018-10-01 09:28:45 +0300448# Context-sensitive call graph data model item base
449
450class CallGraphLevelItemBase(object):
451
452 def __init__(self, glb, row, parent_item):
453 self.glb = glb
454 self.row = row
455 self.parent_item = parent_item
456 self.query_done = False;
457 self.child_count = 0
458 self.child_items = []
Adrian Hunter3ac641f2019-05-03 15:08:25 +0300459 if parent_item:
460 self.level = parent_item.level + 1
461 else:
462 self.level = 0
Adrian Hunter341e73c2018-10-01 09:28:45 +0300463
464 def getChildItem(self, row):
465 return self.child_items[row]
466
467 def getParentItem(self):
468 return self.parent_item
469
470 def getRow(self):
471 return self.row
472
473 def childCount(self):
474 if not self.query_done:
475 self.Select()
476 if not self.child_count:
477 return -1
478 return self.child_count
479
480 def hasChildren(self):
481 if not self.query_done:
482 return True
483 return self.child_count > 0
484
485 def getData(self, column):
486 return self.data[column]
487
488# Context-sensitive call graph data model level 2+ item base
489
490class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
491
492 def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item):
493 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
494 self.comm_id = comm_id
495 self.thread_id = thread_id
496 self.call_path_id = call_path_id
497 self.branch_count = branch_count
498 self.time = time
499
500 def Select(self):
501 self.query_done = True;
502 query = QSqlQuery(self.glb.db)
503 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)"
504 " FROM calls"
505 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
506 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
507 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
508 " WHERE parent_call_path_id = " + str(self.call_path_id) +
509 " AND comm_id = " + str(self.comm_id) +
510 " AND thread_id = " + str(self.thread_id) +
511 " GROUP BY call_path_id, name, short_name"
512 " ORDER BY call_path_id")
513 while query.next():
514 child_item = CallGraphLevelThreeItem(self.glb, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), int(query.value(5)), self)
515 self.child_items.append(child_item)
516 self.child_count += 1
517
518# Context-sensitive call graph data model level three item
519
520class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
521
522 def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item):
523 super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item)
524 dso = dsoname(dso)
525 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
526 self.dbid = call_path_id
527
528# Context-sensitive call graph data model level two item
529
530class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
531
532 def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
533 super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item)
534 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
535 self.dbid = thread_id
536
537 def Select(self):
538 super(CallGraphLevelTwoItem, self).Select()
539 for child_item in self.child_items:
540 self.time += child_item.time
541 self.branch_count += child_item.branch_count
542 for child_item in self.child_items:
543 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
544 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
545
546# Context-sensitive call graph data model level one item
547
548class CallGraphLevelOneItem(CallGraphLevelItemBase):
549
550 def __init__(self, glb, row, comm_id, comm, parent_item):
551 super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item)
552 self.data = [comm, "", "", "", "", "", ""]
553 self.dbid = comm_id
554
555 def Select(self):
556 self.query_done = True;
557 query = QSqlQuery(self.glb.db)
558 QueryExec(query, "SELECT thread_id, pid, tid"
559 " FROM comm_threads"
560 " INNER JOIN threads ON thread_id = threads.id"
561 " WHERE comm_id = " + str(self.dbid))
562 while query.next():
563 child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
564 self.child_items.append(child_item)
565 self.child_count += 1
566
567# Context-sensitive call graph data model root item
568
569class CallGraphRootItem(CallGraphLevelItemBase):
570
571 def __init__(self, glb):
572 super(CallGraphRootItem, self).__init__(glb, 0, None)
573 self.dbid = 0
574 self.query_done = True;
575 query = QSqlQuery(glb.db)
576 QueryExec(query, "SELECT id, comm FROM comms")
577 while query.next():
578 if not query.value(0):
579 continue
580 child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
581 self.child_items.append(child_item)
582 self.child_count += 1
583
Adrian Hunter254c0d82019-02-28 15:00:30 +0200584# Context-sensitive call graph data model base
Adrian Hunter70d831e2018-10-01 09:28:43 +0300585
Adrian Hunter254c0d82019-02-28 15:00:30 +0200586class CallGraphModelBase(TreeModel):
Adrian Hunter70d831e2018-10-01 09:28:43 +0300587
588 def __init__(self, glb, parent=None):
Adrian Hunter254c0d82019-02-28 15:00:30 +0200589 super(CallGraphModelBase, self).__init__(glb, parent)
Adrian Hunter70d831e2018-10-01 09:28:43 +0300590
Adrian Hunterebd70c72018-10-01 09:28:48 +0300591 def FindSelect(self, value, pattern, query):
592 if pattern:
593 # postgresql and sqlite pattern patching differences:
594 # postgresql LIKE is case sensitive but sqlite LIKE is not
595 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
596 # postgresql supports ILIKE which is case insensitive
597 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive
598 if not self.glb.dbref.is_sqlite3:
599 # Escape % and _
600 s = value.replace("%", "\%")
601 s = s.replace("_", "\_")
602 # Translate * and ? into SQL LIKE pattern characters % and _
603 trans = string.maketrans("*?", "%_")
604 match = " LIKE '" + str(s).translate(trans) + "'"
605 else:
606 match = " GLOB '" + str(value) + "'"
607 else:
608 match = " = '" + str(value) + "'"
Adrian Hunter254c0d82019-02-28 15:00:30 +0200609 self.DoFindSelect(query, match)
Adrian Hunterebd70c72018-10-01 09:28:48 +0300610
611 def Found(self, query, found):
612 if found:
613 return self.FindPath(query)
614 return []
615
616 def FindValue(self, value, pattern, query, last_value, last_pattern):
617 if last_value == value and pattern == last_pattern:
618 found = query.first()
619 else:
620 self.FindSelect(value, pattern, query)
621 found = query.next()
622 return self.Found(query, found)
623
624 def FindNext(self, query):
625 found = query.next()
626 if not found:
627 found = query.first()
628 return self.Found(query, found)
629
630 def FindPrev(self, query):
631 found = query.previous()
632 if not found:
633 found = query.last()
634 return self.Found(query, found)
635
636 def FindThread(self, c):
637 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
638 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
639 elif c.direction > 0:
640 ids = self.FindNext(c.query)
641 else:
642 ids = self.FindPrev(c.query)
643 return (True, ids)
644
645 def Find(self, value, direction, pattern, context, callback):
646 class Context():
647 def __init__(self, *x):
648 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
649 def Update(self, *x):
650 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
651 if len(context):
652 context[0].Update(value, direction, pattern)
653 else:
654 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
655 # Use a thread so the UI is not blocked during the SELECT
656 thread = Thread(self.FindThread, context[0])
657 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
658 thread.start()
659
660 def FindDone(self, thread, callback, ids):
661 callback(ids)
662
Adrian Hunter254c0d82019-02-28 15:00:30 +0200663# Context-sensitive call graph data model
664
665class CallGraphModel(CallGraphModelBase):
666
667 def __init__(self, glb, parent=None):
668 super(CallGraphModel, self).__init__(glb, parent)
669
670 def GetRoot(self):
671 return CallGraphRootItem(self.glb)
672
673 def columnCount(self, parent=None):
674 return 7
675
676 def columnHeader(self, column):
677 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
678 return headers[column]
679
680 def columnAlignment(self, column):
681 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
682 return alignment[column]
683
684 def DoFindSelect(self, query, match):
685 QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
686 " FROM calls"
687 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
688 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
689 " WHERE symbols.name" + match +
690 " GROUP BY comm_id, thread_id, call_path_id"
691 " ORDER BY comm_id, thread_id, call_path_id")
692
693 def FindPath(self, query):
694 # Turn the query result into a list of ids that the tree view can walk
695 # to open the tree at the right place.
696 ids = []
697 parent_id = query.value(0)
698 while parent_id:
699 ids.insert(0, parent_id)
700 q2 = QSqlQuery(self.glb.db)
701 QueryExec(q2, "SELECT parent_id"
702 " FROM call_paths"
703 " WHERE id = " + str(parent_id))
704 if not q2.next():
705 break
706 parent_id = q2.value(0)
707 # The call path root is not used
708 if ids[0] == 1:
709 del ids[0]
710 ids.insert(0, query.value(2))
711 ids.insert(0, query.value(1))
712 return ids
713
Adrian Hunterae8b8872019-02-28 15:00:31 +0200714# Call tree data model level 2+ item base
715
716class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
717
718 def __init__(self, glb, row, comm_id, thread_id, calls_id, time, branch_count, parent_item):
719 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
720 self.comm_id = comm_id
721 self.thread_id = thread_id
722 self.calls_id = calls_id
723 self.branch_count = branch_count
724 self.time = time
725
726 def Select(self):
727 self.query_done = True;
728 if self.calls_id == 0:
729 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
730 else:
731 comm_thread = ""
732 query = QSqlQuery(self.glb.db)
733 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time, branch_count"
734 " FROM calls"
735 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
736 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
737 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
738 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
739 " ORDER BY call_time, calls.id")
740 while query.next():
741 child_item = CallTreeLevelThreeItem(self.glb, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), int(query.value(5)), self)
742 self.child_items.append(child_item)
743 self.child_count += 1
744
745# Call tree data model level three item
746
747class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
748
749 def __init__(self, glb, row, comm_id, thread_id, calls_id, name, dso, count, time, branch_count, parent_item):
750 super(CallTreeLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, calls_id, time, branch_count, parent_item)
751 dso = dsoname(dso)
752 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
753 self.dbid = calls_id
754
755# Call tree data model level two item
756
757class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
758
759 def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
760 super(CallTreeLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 0, 0, 0, parent_item)
761 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
762 self.dbid = thread_id
763
764 def Select(self):
765 super(CallTreeLevelTwoItem, self).Select()
766 for child_item in self.child_items:
767 self.time += child_item.time
768 self.branch_count += child_item.branch_count
769 for child_item in self.child_items:
770 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
771 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
772
773# Call tree data model level one item
774
775class CallTreeLevelOneItem(CallGraphLevelItemBase):
776
777 def __init__(self, glb, row, comm_id, comm, parent_item):
778 super(CallTreeLevelOneItem, self).__init__(glb, row, parent_item)
779 self.data = [comm, "", "", "", "", "", ""]
780 self.dbid = comm_id
781
782 def Select(self):
783 self.query_done = True;
784 query = QSqlQuery(self.glb.db)
785 QueryExec(query, "SELECT thread_id, pid, tid"
786 " FROM comm_threads"
787 " INNER JOIN threads ON thread_id = threads.id"
788 " WHERE comm_id = " + str(self.dbid))
789 while query.next():
790 child_item = CallTreeLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
791 self.child_items.append(child_item)
792 self.child_count += 1
793
794# Call tree data model root item
795
796class CallTreeRootItem(CallGraphLevelItemBase):
797
798 def __init__(self, glb):
799 super(CallTreeRootItem, self).__init__(glb, 0, None)
800 self.dbid = 0
801 self.query_done = True;
802 query = QSqlQuery(glb.db)
803 QueryExec(query, "SELECT id, comm FROM comms")
804 while query.next():
805 if not query.value(0):
806 continue
807 child_item = CallTreeLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
808 self.child_items.append(child_item)
809 self.child_count += 1
810
811# Call Tree data model
812
813class CallTreeModel(CallGraphModelBase):
814
815 def __init__(self, glb, parent=None):
816 super(CallTreeModel, self).__init__(glb, parent)
817
818 def GetRoot(self):
819 return CallTreeRootItem(self.glb)
820
821 def columnCount(self, parent=None):
822 return 7
823
824 def columnHeader(self, column):
825 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
826 return headers[column]
827
828 def columnAlignment(self, column):
829 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
830 return alignment[column]
831
832 def DoFindSelect(self, query, match):
833 QueryExec(query, "SELECT calls.id, comm_id, thread_id"
834 " FROM calls"
835 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
836 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
837 " WHERE symbols.name" + match +
838 " ORDER BY comm_id, thread_id, call_time, calls.id")
839
840 def FindPath(self, query):
841 # Turn the query result into a list of ids that the tree view can walk
842 # to open the tree at the right place.
843 ids = []
844 parent_id = query.value(0)
845 while parent_id:
846 ids.insert(0, parent_id)
847 q2 = QSqlQuery(self.glb.db)
848 QueryExec(q2, "SELECT parent_id"
849 " FROM calls"
850 " WHERE id = " + str(parent_id))
851 if not q2.next():
852 break
853 parent_id = q2.value(0)
854 ids.insert(0, query.value(2))
855 ids.insert(0, query.value(1))
856 return ids
857
Adrian Hunterebd70c72018-10-01 09:28:48 +0300858# Vertical widget layout
859
860class VBox():
861
862 def __init__(self, w1, w2, w3=None):
863 self.vbox = QWidget()
864 self.vbox.setLayout(QVBoxLayout());
865
866 self.vbox.layout().setContentsMargins(0, 0, 0, 0)
867
868 self.vbox.layout().addWidget(w1)
869 self.vbox.layout().addWidget(w2)
870 if w3:
871 self.vbox.layout().addWidget(w3)
872
873 def Widget(self):
874 return self.vbox
875
Adrian Huntera731cc42019-02-28 15:00:28 +0200876# Tree window base
Adrian Hunter1beb5c72018-10-01 09:28:47 +0300877
Adrian Huntera731cc42019-02-28 15:00:28 +0200878class TreeWindowBase(QMdiSubWindow):
Adrian Hunter1beb5c72018-10-01 09:28:47 +0300879
Adrian Huntera731cc42019-02-28 15:00:28 +0200880 def __init__(self, parent=None):
881 super(TreeWindowBase, self).__init__(parent)
Adrian Hunter1beb5c72018-10-01 09:28:47 +0300882
Adrian Huntera731cc42019-02-28 15:00:28 +0200883 self.model = None
Adrian Huntera731cc42019-02-28 15:00:28 +0200884 self.find_bar = None
Adrian Hunter1beb5c72018-10-01 09:28:47 +0300885
Adrian Hunterbe6e7472019-05-03 15:08:24 +0300886 self.view = QTreeView()
Adrian Hunter96c43b92019-05-03 15:08:26 +0300887 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
888 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
Adrian Hunterbe6e7472019-05-03 15:08:24 +0300889
Adrian Hunterebd70c72018-10-01 09:28:48 +0300890 def DisplayFound(self, ids):
891 if not len(ids):
892 return False
893 parent = QModelIndex()
894 for dbid in ids:
895 found = False
896 n = self.model.rowCount(parent)
897 for row in xrange(n):
898 child = self.model.index(row, 0, parent)
899 if child.internalPointer().dbid == dbid:
900 found = True
901 self.view.setCurrentIndex(child)
902 parent = child
903 break
904 if not found:
905 break
906 return found
907
908 def Find(self, value, direction, pattern, context):
909 self.view.setFocus()
910 self.find_bar.Busy()
911 self.model.Find(value, direction, pattern, context, self.FindDone)
912
913 def FindDone(self, ids):
914 found = True
915 if not self.DisplayFound(ids):
916 found = False
917 self.find_bar.Idle()
918 if not found:
919 self.find_bar.NotFound()
920
Adrian Huntera731cc42019-02-28 15:00:28 +0200921
922# Context-sensitive call graph window
923
924class CallGraphWindow(TreeWindowBase):
925
926 def __init__(self, glb, parent=None):
927 super(CallGraphWindow, self).__init__(parent)
928
929 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
930
Adrian Huntera731cc42019-02-28 15:00:28 +0200931 self.view.setModel(self.model)
932
933 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
934 self.view.setColumnWidth(c, w)
935
936 self.find_bar = FindBar(self, self)
937
938 self.vbox = VBox(self.view, self.find_bar.Widget())
939
940 self.setWidget(self.vbox.Widget())
941
942 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
943
Adrian Hunterae8b8872019-02-28 15:00:31 +0200944# Call tree window
945
946class CallTreeWindow(TreeWindowBase):
947
948 def __init__(self, glb, parent=None):
949 super(CallTreeWindow, self).__init__(parent)
950
951 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
952
Adrian Hunterae8b8872019-02-28 15:00:31 +0200953 self.view.setModel(self.model)
954
955 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
956 self.view.setColumnWidth(c, w)
957
958 self.find_bar = FindBar(self, self)
959
960 self.vbox = VBox(self.view, self.find_bar.Widget())
961
962 self.setWidget(self.vbox.Widget())
963
964 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
965
Adrian Hunter8392b742018-10-01 09:28:50 +0300966# Child data item finder
967
968class ChildDataItemFinder():
969
970 def __init__(self, root):
971 self.root = root
972 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
973 self.rows = []
974 self.pos = 0
975
976 def FindSelect(self):
977 self.rows = []
978 if self.pattern:
979 pattern = re.compile(self.value)
980 for child in self.root.child_items:
981 for column_data in child.data:
982 if re.search(pattern, str(column_data)) is not None:
983 self.rows.append(child.row)
984 break
985 else:
986 for child in self.root.child_items:
987 for column_data in child.data:
988 if self.value in str(column_data):
989 self.rows.append(child.row)
990 break
991
992 def FindValue(self):
993 self.pos = 0
994 if self.last_value != self.value or self.pattern != self.last_pattern:
995 self.FindSelect()
996 if not len(self.rows):
997 return -1
998 return self.rows[self.pos]
999
1000 def FindThread(self):
1001 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
1002 row = self.FindValue()
1003 elif len(self.rows):
1004 if self.direction > 0:
1005 self.pos += 1
1006 if self.pos >= len(self.rows):
1007 self.pos = 0
1008 else:
1009 self.pos -= 1
1010 if self.pos < 0:
1011 self.pos = len(self.rows) - 1
1012 row = self.rows[self.pos]
1013 else:
1014 row = -1
1015 return (True, row)
1016
1017 def Find(self, value, direction, pattern, context, callback):
1018 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
1019 # Use a thread so the UI is not blocked
1020 thread = Thread(self.FindThread)
1021 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
1022 thread.start()
1023
1024 def FindDone(self, thread, callback, row):
1025 callback(row)
1026
1027# Number of database records to fetch in one go
1028
1029glb_chunk_sz = 10000
1030
Adrian Hunter8392b742018-10-01 09:28:50 +03001031# Background process for SQL data fetcher
1032
1033class SQLFetcherProcess():
1034
1035 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
1036 # Need a unique connection name
1037 conn_name = "SQLFetcher" + str(os.getpid())
1038 self.db, dbname = dbref.Open(conn_name)
1039 self.sql = sql
1040 self.buffer = buffer
1041 self.head = head
1042 self.tail = tail
1043 self.fetch_count = fetch_count
1044 self.fetching_done = fetching_done
1045 self.process_target = process_target
1046 self.wait_event = wait_event
1047 self.fetched_event = fetched_event
1048 self.prep = prep
1049 self.query = QSqlQuery(self.db)
1050 self.query_limit = 0 if "$$last_id$$" in sql else 2
1051 self.last_id = -1
1052 self.fetched = 0
1053 self.more = True
1054 self.local_head = self.head.value
1055 self.local_tail = self.tail.value
1056
1057 def Select(self):
1058 if self.query_limit:
1059 if self.query_limit == 1:
1060 return
1061 self.query_limit -= 1
1062 stmt = self.sql.replace("$$last_id$$", str(self.last_id))
1063 QueryExec(self.query, stmt)
1064
1065 def Next(self):
1066 if not self.query.next():
1067 self.Select()
1068 if not self.query.next():
1069 return None
1070 self.last_id = self.query.value(0)
1071 return self.prep(self.query)
1072
1073 def WaitForTarget(self):
1074 while True:
1075 self.wait_event.clear()
1076 target = self.process_target.value
1077 if target > self.fetched or target < 0:
1078 break
1079 self.wait_event.wait()
1080 return target
1081
1082 def HasSpace(self, sz):
1083 if self.local_tail <= self.local_head:
1084 space = len(self.buffer) - self.local_head
1085 if space > sz:
1086 return True
1087 if space >= glb_nsz:
1088 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
Tony Jonesbeda0e72019-03-08 16:05:15 -08001089 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
Adrian Hunter8392b742018-10-01 09:28:50 +03001090 self.buffer[self.local_head : self.local_head + len(nd)] = nd
1091 self.local_head = 0
1092 if self.local_tail - self.local_head > sz:
1093 return True
1094 return False
1095
1096 def WaitForSpace(self, sz):
1097 if self.HasSpace(sz):
1098 return
1099 while True:
1100 self.wait_event.clear()
1101 self.local_tail = self.tail.value
1102 if self.HasSpace(sz):
1103 return
1104 self.wait_event.wait()
1105
1106 def AddToBuffer(self, obj):
Tony Jonesbeda0e72019-03-08 16:05:15 -08001107 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
Adrian Hunter8392b742018-10-01 09:28:50 +03001108 n = len(d)
Tony Jonesbeda0e72019-03-08 16:05:15 -08001109 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
Adrian Hunter8392b742018-10-01 09:28:50 +03001110 sz = n + glb_nsz
1111 self.WaitForSpace(sz)
1112 pos = self.local_head
1113 self.buffer[pos : pos + len(nd)] = nd
1114 self.buffer[pos + glb_nsz : pos + sz] = d
1115 self.local_head += sz
1116
1117 def FetchBatch(self, batch_size):
1118 fetched = 0
1119 while batch_size > fetched:
1120 obj = self.Next()
1121 if obj is None:
1122 self.more = False
1123 break
1124 self.AddToBuffer(obj)
1125 fetched += 1
1126 if fetched:
1127 self.fetched += fetched
1128 with self.fetch_count.get_lock():
1129 self.fetch_count.value += fetched
1130 self.head.value = self.local_head
1131 self.fetched_event.set()
1132
1133 def Run(self):
1134 while self.more:
1135 target = self.WaitForTarget()
1136 if target < 0:
1137 break
1138 batch_size = min(glb_chunk_sz, target - self.fetched)
1139 self.FetchBatch(batch_size)
1140 self.fetching_done.value = True
1141 self.fetched_event.set()
1142
1143def SQLFetcherFn(*x):
1144 process = SQLFetcherProcess(*x)
1145 process.Run()
1146
1147# SQL data fetcher
1148
1149class SQLFetcher(QObject):
1150
1151 done = Signal(object)
1152
1153 def __init__(self, glb, sql, prep, process_data, parent=None):
1154 super(SQLFetcher, self).__init__(parent)
1155 self.process_data = process_data
1156 self.more = True
1157 self.target = 0
1158 self.last_target = 0
1159 self.fetched = 0
1160 self.buffer_size = 16 * 1024 * 1024
1161 self.buffer = Array(c_char, self.buffer_size, lock=False)
1162 self.head = Value(c_longlong)
1163 self.tail = Value(c_longlong)
1164 self.local_tail = 0
1165 self.fetch_count = Value(c_longlong)
1166 self.fetching_done = Value(c_bool)
1167 self.last_count = 0
1168 self.process_target = Value(c_longlong)
1169 self.wait_event = Event()
1170 self.fetched_event = Event()
1171 glb.AddInstanceToShutdownOnExit(self)
1172 self.process = Process(target=SQLFetcherFn, args=(glb.dbref, sql, self.buffer, self.head, self.tail, self.fetch_count, self.fetching_done, self.process_target, self.wait_event, self.fetched_event, prep))
1173 self.process.start()
1174 self.thread = Thread(self.Thread)
1175 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
1176 self.thread.start()
1177
1178 def Shutdown(self):
1179 # Tell the thread and process to exit
1180 self.process_target.value = -1
1181 self.wait_event.set()
1182 self.more = False
1183 self.fetching_done.value = True
1184 self.fetched_event.set()
1185
1186 def Thread(self):
1187 if not self.more:
1188 return True, 0
1189 while True:
1190 self.fetched_event.clear()
1191 fetch_count = self.fetch_count.value
1192 if fetch_count != self.last_count:
1193 break
1194 if self.fetching_done.value:
1195 self.more = False
1196 return True, 0
1197 self.fetched_event.wait()
1198 count = fetch_count - self.last_count
1199 self.last_count = fetch_count
1200 self.fetched += count
1201 return False, count
1202
1203 def Fetch(self, nr):
1204 if not self.more:
1205 # -1 inidcates there are no more
1206 return -1
1207 result = self.fetched
1208 extra = result + nr - self.target
1209 if extra > 0:
1210 self.target += extra
1211 # process_target < 0 indicates shutting down
1212 if self.process_target.value >= 0:
1213 self.process_target.value = self.target
1214 self.wait_event.set()
1215 return result
1216
1217 def RemoveFromBuffer(self):
1218 pos = self.local_tail
1219 if len(self.buffer) - pos < glb_nsz:
1220 pos = 0
Tony Jonesbeda0e72019-03-08 16:05:15 -08001221 n = pickle.loads(self.buffer[pos : pos + glb_nsz])
Adrian Hunter8392b742018-10-01 09:28:50 +03001222 if n == 0:
1223 pos = 0
Tony Jonesbeda0e72019-03-08 16:05:15 -08001224 n = pickle.loads(self.buffer[0 : glb_nsz])
Adrian Hunter8392b742018-10-01 09:28:50 +03001225 pos += glb_nsz
Tony Jonesbeda0e72019-03-08 16:05:15 -08001226 obj = pickle.loads(self.buffer[pos : pos + n])
Adrian Hunter8392b742018-10-01 09:28:50 +03001227 self.local_tail = pos + n
1228 return obj
1229
1230 def ProcessData(self, count):
1231 for i in xrange(count):
1232 obj = self.RemoveFromBuffer()
1233 self.process_data(obj)
1234 self.tail.value = self.local_tail
1235 self.wait_event.set()
1236 self.done.emit(count)
1237
1238# Fetch more records bar
1239
1240class FetchMoreRecordsBar():
1241
1242 def __init__(self, model, parent):
1243 self.model = model
1244
1245 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
1246 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1247
1248 self.fetch_count = QSpinBox()
1249 self.fetch_count.setRange(1, 1000000)
1250 self.fetch_count.setValue(10)
1251 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1252
1253 self.fetch = QPushButton("Go!")
1254 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1255 self.fetch.released.connect(self.FetchMoreRecords)
1256
1257 self.progress = QProgressBar()
1258 self.progress.setRange(0, 100)
1259 self.progress.hide()
1260
1261 self.done_label = QLabel("All records fetched")
1262 self.done_label.hide()
1263
1264 self.spacer = QLabel("")
1265
1266 self.close_button = QToolButton()
1267 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
1268 self.close_button.released.connect(self.Deactivate)
1269
1270 self.hbox = QHBoxLayout()
1271 self.hbox.setContentsMargins(0, 0, 0, 0)
1272
1273 self.hbox.addWidget(self.label)
1274 self.hbox.addWidget(self.fetch_count)
1275 self.hbox.addWidget(self.fetch)
1276 self.hbox.addWidget(self.spacer)
1277 self.hbox.addWidget(self.progress)
1278 self.hbox.addWidget(self.done_label)
1279 self.hbox.addWidget(self.close_button)
1280
1281 self.bar = QWidget()
1282 self.bar.setLayout(self.hbox);
1283 self.bar.show()
1284
1285 self.in_progress = False
1286 self.model.progress.connect(self.Progress)
1287
1288 self.done = False
1289
1290 if not model.HasMoreRecords():
1291 self.Done()
1292
1293 def Widget(self):
1294 return self.bar
1295
1296 def Activate(self):
1297 self.bar.show()
1298 self.fetch.setFocus()
1299
1300 def Deactivate(self):
1301 self.bar.hide()
1302
1303 def Enable(self, enable):
1304 self.fetch.setEnabled(enable)
1305 self.fetch_count.setEnabled(enable)
1306
1307 def Busy(self):
1308 self.Enable(False)
1309 self.fetch.hide()
1310 self.spacer.hide()
1311 self.progress.show()
1312
1313 def Idle(self):
1314 self.in_progress = False
1315 self.Enable(True)
1316 self.progress.hide()
1317 self.fetch.show()
1318 self.spacer.show()
1319
1320 def Target(self):
1321 return self.fetch_count.value() * glb_chunk_sz
1322
1323 def Done(self):
1324 self.done = True
1325 self.Idle()
1326 self.label.hide()
1327 self.fetch_count.hide()
1328 self.fetch.hide()
1329 self.spacer.hide()
1330 self.done_label.show()
1331
1332 def Progress(self, count):
1333 if self.in_progress:
1334 if count:
1335 percent = ((count - self.start) * 100) / self.Target()
1336 if percent >= 100:
1337 self.Idle()
1338 else:
1339 self.progress.setValue(percent)
1340 if not count:
1341 # Count value of zero means no more records
1342 self.Done()
1343
1344 def FetchMoreRecords(self):
1345 if self.done:
1346 return
1347 self.progress.setValue(0)
1348 self.Busy()
1349 self.in_progress = True
1350 self.start = self.model.FetchMoreRecords(self.Target())
1351
Adrian Hunter76099f92018-10-23 10:59:49 +03001352# Brance data model level two item
1353
1354class BranchLevelTwoItem():
1355
1356 def __init__(self, row, text, parent_item):
1357 self.row = row
1358 self.parent_item = parent_item
1359 self.data = [""] * 8
1360 self.data[7] = text
1361 self.level = 2
1362
1363 def getParentItem(self):
1364 return self.parent_item
1365
1366 def getRow(self):
1367 return self.row
1368
1369 def childCount(self):
1370 return 0
1371
1372 def hasChildren(self):
1373 return False
1374
1375 def getData(self, column):
1376 return self.data[column]
1377
1378# Brance data model level one item
1379
1380class BranchLevelOneItem():
1381
1382 def __init__(self, glb, row, data, parent_item):
1383 self.glb = glb
1384 self.row = row
1385 self.parent_item = parent_item
1386 self.child_count = 0
1387 self.child_items = []
1388 self.data = data[1:]
1389 self.dbid = data[0]
1390 self.level = 1
1391 self.query_done = False
1392
1393 def getChildItem(self, row):
1394 return self.child_items[row]
1395
1396 def getParentItem(self):
1397 return self.parent_item
1398
1399 def getRow(self):
1400 return self.row
1401
1402 def Select(self):
1403 self.query_done = True
1404
1405 if not self.glb.have_disassembler:
1406 return
1407
1408 query = QSqlQuery(self.glb.db)
1409
1410 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
1411 " FROM samples"
1412 " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
1413 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
1414 " WHERE samples.id = " + str(self.dbid))
1415 if not query.next():
1416 return
1417 cpu = query.value(0)
1418 dso = query.value(1)
1419 sym = query.value(2)
1420 if dso == 0 or sym == 0:
1421 return
1422 off = query.value(3)
1423 short_name = query.value(4)
1424 long_name = query.value(5)
1425 build_id = query.value(6)
1426 sym_start = query.value(7)
1427 ip = query.value(8)
1428
1429 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
1430 " FROM samples"
1431 " INNER JOIN symbols ON samples.symbol_id = symbols.id"
1432 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
1433 " ORDER BY samples.id"
1434 " LIMIT 1")
1435 if not query.next():
1436 return
1437 if query.value(0) != dso:
1438 # Cannot disassemble from one dso to another
1439 return
1440 bsym = query.value(1)
1441 boff = query.value(2)
1442 bsym_start = query.value(3)
1443 if bsym == 0:
1444 return
1445 tot = bsym_start + boff + 1 - sym_start - off
1446 if tot <= 0 or tot > 16384:
1447 return
1448
1449 inst = self.glb.disassembler.Instruction()
1450 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
1451 if not f:
1452 return
1453 mode = 0 if Is64Bit(f) else 1
1454 self.glb.disassembler.SetMode(inst, mode)
1455
1456 buf_sz = tot + 16
1457 buf = create_string_buffer(tot + 16)
1458 f.seek(sym_start + off)
1459 buf.value = f.read(buf_sz)
1460 buf_ptr = addressof(buf)
1461 i = 0
1462 while tot > 0:
1463 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
1464 if cnt:
1465 byte_str = tohex(ip).rjust(16)
1466 for k in xrange(cnt):
1467 byte_str += " %02x" % ord(buf[i])
1468 i += 1
1469 while k < 15:
1470 byte_str += " "
1471 k += 1
1472 self.child_items.append(BranchLevelTwoItem(0, byte_str + " " + text, self))
1473 self.child_count += 1
1474 else:
1475 return
1476 buf_ptr += cnt
1477 tot -= cnt
1478 buf_sz -= cnt
1479 ip += cnt
1480
1481 def childCount(self):
1482 if not self.query_done:
1483 self.Select()
1484 if not self.child_count:
1485 return -1
1486 return self.child_count
1487
1488 def hasChildren(self):
1489 if not self.query_done:
1490 return True
1491 return self.child_count > 0
1492
1493 def getData(self, column):
1494 return self.data[column]
1495
1496# Brance data model root item
1497
1498class BranchRootItem():
1499
1500 def __init__(self):
1501 self.child_count = 0
1502 self.child_items = []
1503 self.level = 0
1504
1505 def getChildItem(self, row):
1506 return self.child_items[row]
1507
1508 def getParentItem(self):
1509 return None
1510
1511 def getRow(self):
1512 return 0
1513
1514 def childCount(self):
1515 return self.child_count
1516
1517 def hasChildren(self):
1518 return self.child_count > 0
1519
1520 def getData(self, column):
1521 return ""
1522
1523# Branch data preparation
1524
1525def BranchDataPrep(query):
1526 data = []
1527 for i in xrange(0, 8):
1528 data.append(query.value(i))
1529 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
1530 " (" + dsoname(query.value(11)) + ")" + " -> " +
1531 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
1532 " (" + dsoname(query.value(15)) + ")")
1533 return data
1534
Adrian Hunter8453c932019-03-27 09:28:25 +02001535def BranchDataPrepWA(query):
1536 data = []
1537 data.append(query.value(0))
1538 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
1539 data.append("{:>19}".format(query.value(1)))
1540 for i in xrange(2, 8):
1541 data.append(query.value(i))
1542 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
1543 " (" + dsoname(query.value(11)) + ")" + " -> " +
1544 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
1545 " (" + dsoname(query.value(15)) + ")")
1546 return data
1547
Adrian Hunter76099f92018-10-23 10:59:49 +03001548# Branch data model
1549
1550class BranchModel(TreeModel):
1551
1552 progress = Signal(object)
1553
1554 def __init__(self, glb, event_id, where_clause, parent=None):
Adrian Huntera448ba22019-02-28 15:00:29 +02001555 super(BranchModel, self).__init__(glb, parent)
Adrian Hunter76099f92018-10-23 10:59:49 +03001556 self.event_id = event_id
1557 self.more = True
1558 self.populated = 0
1559 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
1560 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
1561 " ip, symbols.name, sym_offset, dsos.short_name,"
1562 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
1563 " FROM samples"
1564 " INNER JOIN comms ON comm_id = comms.id"
1565 " INNER JOIN threads ON thread_id = threads.id"
1566 " INNER JOIN branch_types ON branch_type = branch_types.id"
1567 " INNER JOIN symbols ON symbol_id = symbols.id"
1568 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
1569 " INNER JOIN dsos ON samples.dso_id = dsos.id"
1570 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
1571 " WHERE samples.id > $$last_id$$" + where_clause +
1572 " AND evsel_id = " + str(self.event_id) +
1573 " ORDER BY samples.id"
1574 " LIMIT " + str(glb_chunk_sz))
Adrian Hunter8453c932019-03-27 09:28:25 +02001575 if pyside_version_1 and sys.version_info[0] == 3:
1576 prep = BranchDataPrepWA
1577 else:
1578 prep = BranchDataPrep
1579 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
Adrian Hunter76099f92018-10-23 10:59:49 +03001580 self.fetcher.done.connect(self.Update)
1581 self.fetcher.Fetch(glb_chunk_sz)
1582
Adrian Huntera448ba22019-02-28 15:00:29 +02001583 def GetRoot(self):
1584 return BranchRootItem()
1585
Adrian Hunter76099f92018-10-23 10:59:49 +03001586 def columnCount(self, parent=None):
1587 return 8
1588
1589 def columnHeader(self, column):
1590 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
1591
1592 def columnFont(self, column):
1593 if column != 7:
1594 return None
1595 return QFont("Monospace")
1596
1597 def DisplayData(self, item, index):
1598 if item.level == 1:
1599 self.FetchIfNeeded(item.row)
1600 return item.getData(index.column())
1601
1602 def AddSample(self, data):
1603 child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
1604 self.root.child_items.append(child)
1605 self.populated += 1
1606
1607 def Update(self, fetched):
1608 if not fetched:
1609 self.more = False
1610 self.progress.emit(0)
1611 child_count = self.root.child_count
1612 count = self.populated - child_count
1613 if count > 0:
1614 parent = QModelIndex()
1615 self.beginInsertRows(parent, child_count, child_count + count - 1)
1616 self.insertRows(child_count, count, parent)
1617 self.root.child_count += count
1618 self.endInsertRows()
1619 self.progress.emit(self.root.child_count)
1620
1621 def FetchMoreRecords(self, count):
1622 current = self.root.child_count
1623 if self.more:
1624 self.fetcher.Fetch(count)
1625 else:
1626 self.progress.emit(0)
1627 return current
1628
1629 def HasMoreRecords(self):
1630 return self.more
1631
Adrian Hunter0bf09472019-02-22 09:27:23 +02001632# Report Variables
1633
1634class ReportVars():
1635
Adrian Huntercd358012019-02-22 09:27:28 +02001636 def __init__(self, name = "", where_clause = "", limit = ""):
Adrian Hunter947cc382019-02-22 09:27:24 +02001637 self.name = name
Adrian Hunter0bf09472019-02-22 09:27:23 +02001638 self.where_clause = where_clause
Adrian Huntercd358012019-02-22 09:27:28 +02001639 self.limit = limit
Adrian Hunter0bf09472019-02-22 09:27:23 +02001640
1641 def UniqueId(self):
Adrian Huntercd358012019-02-22 09:27:28 +02001642 return str(self.where_clause + ";" + self.limit)
Adrian Hunter0bf09472019-02-22 09:27:23 +02001643
Adrian Hunter76099f92018-10-23 10:59:49 +03001644# Branch window
1645
1646class BranchWindow(QMdiSubWindow):
1647
Adrian Hunter947cc382019-02-22 09:27:24 +02001648 def __init__(self, glb, event_id, report_vars, parent=None):
Adrian Hunter76099f92018-10-23 10:59:49 +03001649 super(BranchWindow, self).__init__(parent)
1650
Adrian Hunter0bf09472019-02-22 09:27:23 +02001651 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId()
Adrian Hunter76099f92018-10-23 10:59:49 +03001652
Adrian Hunter0bf09472019-02-22 09:27:23 +02001653 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
Adrian Hunter76099f92018-10-23 10:59:49 +03001654
1655 self.view = QTreeView()
1656 self.view.setUniformRowHeights(True)
Adrian Hunter96c43b92019-05-03 15:08:26 +03001657 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
1658 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
Adrian Hunter76099f92018-10-23 10:59:49 +03001659 self.view.setModel(self.model)
1660
1661 self.ResizeColumnsToContents()
1662
1663 self.find_bar = FindBar(self, self, True)
1664
1665 self.finder = ChildDataItemFinder(self.model.root)
1666
1667 self.fetch_bar = FetchMoreRecordsBar(self.model, self)
1668
1669 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
1670
1671 self.setWidget(self.vbox.Widget())
1672
Adrian Hunter947cc382019-02-22 09:27:24 +02001673 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
Adrian Hunter76099f92018-10-23 10:59:49 +03001674
1675 def ResizeColumnToContents(self, column, n):
1676 # Using the view's resizeColumnToContents() here is extrememly slow
1677 # so implement a crude alternative
1678 mm = "MM" if column else "MMMM"
1679 font = self.view.font()
1680 metrics = QFontMetrics(font)
1681 max = 0
1682 for row in xrange(n):
1683 val = self.model.root.child_items[row].data[column]
1684 len = metrics.width(str(val) + mm)
1685 max = len if len > max else max
1686 val = self.model.columnHeader(column)
1687 len = metrics.width(str(val) + mm)
1688 max = len if len > max else max
1689 self.view.setColumnWidth(column, max)
1690
1691 def ResizeColumnsToContents(self):
1692 n = min(self.model.root.child_count, 100)
1693 if n < 1:
1694 # No data yet, so connect a signal to notify when there is
1695 self.model.rowsInserted.connect(self.UpdateColumnWidths)
1696 return
1697 columns = self.model.columnCount()
1698 for i in xrange(columns):
1699 self.ResizeColumnToContents(i, n)
1700
1701 def UpdateColumnWidths(self, *x):
1702 # This only needs to be done once, so disconnect the signal now
1703 self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
1704 self.ResizeColumnsToContents()
1705
1706 def Find(self, value, direction, pattern, context):
1707 self.view.setFocus()
1708 self.find_bar.Busy()
1709 self.finder.Find(value, direction, pattern, context, self.FindDone)
1710
1711 def FindDone(self, row):
1712 self.find_bar.Idle()
1713 if row >= 0:
1714 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
1715 else:
1716 self.find_bar.NotFound()
1717
Adrian Hunter1c3ca1b2019-02-22 09:27:25 +02001718# Line edit data item
1719
1720class LineEditDataItem(object):
1721
Adrian Huntercd358012019-02-22 09:27:28 +02001722 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
Adrian Hunter1c3ca1b2019-02-22 09:27:25 +02001723 self.glb = glb
1724 self.label = label
1725 self.placeholder_text = placeholder_text
1726 self.parent = parent
1727 self.id = id
1728
Adrian Huntercd358012019-02-22 09:27:28 +02001729 self.value = default
Adrian Hunter1c3ca1b2019-02-22 09:27:25 +02001730
Adrian Huntercd358012019-02-22 09:27:28 +02001731 self.widget = QLineEdit(default)
Adrian Hunter1c3ca1b2019-02-22 09:27:25 +02001732 self.widget.editingFinished.connect(self.Validate)
1733 self.widget.textChanged.connect(self.Invalidate)
1734 self.red = False
1735 self.error = ""
1736 self.validated = True
1737
1738 if placeholder_text:
1739 self.widget.setPlaceholderText(placeholder_text)
1740
1741 def TurnTextRed(self):
1742 if not self.red:
1743 palette = QPalette()
1744 palette.setColor(QPalette.Text,Qt.red)
1745 self.widget.setPalette(palette)
1746 self.red = True
1747
1748 def TurnTextNormal(self):
1749 if self.red:
1750 palette = QPalette()
1751 self.widget.setPalette(palette)
1752 self.red = False
1753
1754 def InvalidValue(self, value):
1755 self.value = ""
1756 self.TurnTextRed()
1757 self.error = self.label + " invalid value '" + value + "'"
1758 self.parent.ShowMessage(self.error)
1759
1760 def Invalidate(self):
1761 self.validated = False
1762
1763 def DoValidate(self, input_string):
1764 self.value = input_string.strip()
1765
1766 def Validate(self):
1767 self.validated = True
1768 self.error = ""
1769 self.TurnTextNormal()
1770 self.parent.ClearMessage()
1771 input_string = self.widget.text()
1772 if not len(input_string.strip()):
1773 self.value = ""
1774 return
1775 self.DoValidate(input_string)
1776
1777 def IsValid(self):
1778 if not self.validated:
1779 self.Validate()
1780 if len(self.error):
1781 self.parent.ShowMessage(self.error)
1782 return False
1783 return True
1784
1785 def IsNumber(self, value):
1786 try:
1787 x = int(value)
1788 except:
1789 x = 0
1790 return str(x) == value
1791
1792# Non-negative integer ranges dialog data item
1793
1794class NonNegativeIntegerRangesDataItem(LineEditDataItem):
1795
1796 def __init__(self, glb, label, placeholder_text, column_name, parent):
1797 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
1798
1799 self.column_name = column_name
1800
1801 def DoValidate(self, input_string):
1802 singles = []
1803 ranges = []
1804 for value in [x.strip() for x in input_string.split(",")]:
1805 if "-" in value:
1806 vrange = value.split("-")
1807 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
1808 return self.InvalidValue(value)
1809 ranges.append(vrange)
1810 else:
1811 if not self.IsNumber(value):
1812 return self.InvalidValue(value)
1813 singles.append(value)
1814 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
1815 if len(singles):
1816 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
1817 self.value = " OR ".join(ranges)
1818
Adrian Huntercd358012019-02-22 09:27:28 +02001819# Positive integer dialog data item
1820
1821class PositiveIntegerDataItem(LineEditDataItem):
1822
1823 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
1824 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
1825
1826 def DoValidate(self, input_string):
1827 if not self.IsNumber(input_string.strip()):
1828 return self.InvalidValue(input_string)
1829 value = int(input_string.strip())
1830 if value <= 0:
1831 return self.InvalidValue(input_string)
1832 self.value = str(value)
1833
Adrian Hunter1c3ca1b2019-02-22 09:27:25 +02001834# Dialog data item converted and validated using a SQL table
1835
1836class SQLTableDataItem(LineEditDataItem):
1837
1838 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
1839 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
1840
1841 self.table_name = table_name
1842 self.match_column = match_column
1843 self.column_name1 = column_name1
1844 self.column_name2 = column_name2
1845
1846 def ValueToIds(self, value):
1847 ids = []
1848 query = QSqlQuery(self.glb.db)
1849 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
1850 ret = query.exec_(stmt)
1851 if ret:
1852 while query.next():
1853 ids.append(str(query.value(0)))
1854 return ids
1855
1856 def DoValidate(self, input_string):
1857 all_ids = []
1858 for value in [x.strip() for x in input_string.split(",")]:
1859 ids = self.ValueToIds(value)
1860 if len(ids):
1861 all_ids.extend(ids)
1862 else:
1863 return self.InvalidValue(value)
1864 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
1865 if self.column_name2:
1866 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
1867
1868# Sample time ranges dialog data item converted and validated using 'samples' SQL table
1869
1870class SampleTimeRangesDataItem(LineEditDataItem):
1871
1872 def __init__(self, glb, label, placeholder_text, column_name, parent):
1873 self.column_name = column_name
1874
1875 self.last_id = 0
1876 self.first_time = 0
1877 self.last_time = 2 ** 64
1878
1879 query = QSqlQuery(glb.db)
1880 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
1881 if query.next():
1882 self.last_id = int(query.value(0))
1883 self.last_time = int(query.value(1))
1884 QueryExec(query, "SELECT time FROM samples WHERE time != 0 ORDER BY id LIMIT 1")
1885 if query.next():
1886 self.first_time = int(query.value(0))
1887 if placeholder_text:
1888 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
1889
1890 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
1891
1892 def IdBetween(self, query, lower_id, higher_id, order):
1893 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
1894 if query.next():
1895 return True, int(query.value(0))
1896 else:
1897 return False, 0
1898
1899 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
1900 query = QSqlQuery(self.glb.db)
1901 while True:
1902 next_id = int((lower_id + higher_id) / 2)
1903 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
1904 if not query.next():
1905 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
1906 if not ok:
1907 ok, dbid = self.IdBetween(query, next_id, higher_id, "")
1908 if not ok:
1909 return str(higher_id)
1910 next_id = dbid
1911 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
1912 next_time = int(query.value(0))
1913 if get_floor:
1914 if target_time > next_time:
1915 lower_id = next_id
1916 else:
1917 higher_id = next_id
1918 if higher_id <= lower_id + 1:
1919 return str(higher_id)
1920 else:
1921 if target_time >= next_time:
1922 lower_id = next_id
1923 else:
1924 higher_id = next_id
1925 if higher_id <= lower_id + 1:
1926 return str(lower_id)
1927
1928 def ConvertRelativeTime(self, val):
1929 mult = 1
1930 suffix = val[-2:]
1931 if suffix == "ms":
1932 mult = 1000000
1933 elif suffix == "us":
1934 mult = 1000
1935 elif suffix == "ns":
1936 mult = 1
1937 else:
1938 return val
1939 val = val[:-2].strip()
1940 if not self.IsNumber(val):
1941 return val
1942 val = int(val) * mult
1943 if val >= 0:
1944 val += self.first_time
1945 else:
1946 val += self.last_time
1947 return str(val)
1948
1949 def ConvertTimeRange(self, vrange):
1950 if vrange[0] == "":
1951 vrange[0] = str(self.first_time)
1952 if vrange[1] == "":
1953 vrange[1] = str(self.last_time)
1954 vrange[0] = self.ConvertRelativeTime(vrange[0])
1955 vrange[1] = self.ConvertRelativeTime(vrange[1])
1956 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
1957 return False
1958 beg_range = max(int(vrange[0]), self.first_time)
1959 end_range = min(int(vrange[1]), self.last_time)
1960 if beg_range > self.last_time or end_range < self.first_time:
1961 return False
1962 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
1963 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
1964 return True
1965
1966 def AddTimeRange(self, value, ranges):
1967 n = value.count("-")
1968 if n == 1:
1969 pass
1970 elif n == 2:
1971 if value.split("-")[1].strip() == "":
1972 n = 1
1973 elif n == 3:
1974 n = 2
1975 else:
1976 return False
1977 pos = findnth(value, "-", n)
1978 vrange = [value[:pos].strip() ,value[pos+1:].strip()]
1979 if self.ConvertTimeRange(vrange):
1980 ranges.append(vrange)
1981 return True
1982 return False
1983
1984 def DoValidate(self, input_string):
1985 ranges = []
1986 for value in [x.strip() for x in input_string.split(",")]:
1987 if not self.AddTimeRange(value, ranges):
1988 return self.InvalidValue(value)
1989 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
1990 self.value = " OR ".join(ranges)
1991
Adrian Hunter0924cd62019-02-22 09:27:22 +02001992# Report Dialog Base
Adrian Hunter210cf1f2018-11-04 17:12:36 +02001993
Adrian Hunter0924cd62019-02-22 09:27:22 +02001994class ReportDialogBase(QDialog):
Adrian Hunter210cf1f2018-11-04 17:12:36 +02001995
Adrian Hunter0924cd62019-02-22 09:27:22 +02001996 def __init__(self, glb, title, items, partial, parent=None):
1997 super(ReportDialogBase, self).__init__(parent)
Adrian Hunter210cf1f2018-11-04 17:12:36 +02001998
1999 self.glb = glb
2000
Adrian Hunter0bf09472019-02-22 09:27:23 +02002001 self.report_vars = ReportVars()
Adrian Hunter210cf1f2018-11-04 17:12:36 +02002002
Adrian Hunter0924cd62019-02-22 09:27:22 +02002003 self.setWindowTitle(title)
Adrian Hunter210cf1f2018-11-04 17:12:36 +02002004 self.setMinimumWidth(600)
2005
Adrian Hunter1c3ca1b2019-02-22 09:27:25 +02002006 self.data_items = [x(glb, self) for x in items]
Adrian Hunter210cf1f2018-11-04 17:12:36 +02002007
Adrian Hunter0924cd62019-02-22 09:27:22 +02002008 self.partial = partial
2009
Adrian Hunter210cf1f2018-11-04 17:12:36 +02002010 self.grid = QGridLayout()
2011
2012 for row in xrange(len(self.data_items)):
2013 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
2014 self.grid.addWidget(self.data_items[row].widget, row, 1)
2015
2016 self.status = QLabel()
2017
2018 self.ok_button = QPushButton("Ok", self)
2019 self.ok_button.setDefault(True)
2020 self.ok_button.released.connect(self.Ok)
2021 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2022
2023 self.cancel_button = QPushButton("Cancel", self)
2024 self.cancel_button.released.connect(self.reject)
2025 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2026
2027 self.hbox = QHBoxLayout()
2028 #self.hbox.addStretch()
2029 self.hbox.addWidget(self.status)
2030 self.hbox.addWidget(self.ok_button)
2031 self.hbox.addWidget(self.cancel_button)
2032
2033 self.vbox = QVBoxLayout()
2034 self.vbox.addLayout(self.grid)
2035 self.vbox.addLayout(self.hbox)
2036
2037 self.setLayout(self.vbox);
2038
2039 def Ok(self):
Adrian Hunter0bf09472019-02-22 09:27:23 +02002040 vars = self.report_vars
Adrian Hunter1c3ca1b2019-02-22 09:27:25 +02002041 for d in self.data_items:
2042 if d.id == "REPORTNAME":
2043 vars.name = d.value
Adrian Hunter947cc382019-02-22 09:27:24 +02002044 if not vars.name:
Adrian Hunter210cf1f2018-11-04 17:12:36 +02002045 self.ShowMessage("Report name is required")
2046 return
2047 for d in self.data_items:
2048 if not d.IsValid():
2049 return
2050 for d in self.data_items[1:]:
Adrian Huntercd358012019-02-22 09:27:28 +02002051 if d.id == "LIMIT":
2052 vars.limit = d.value
2053 elif len(d.value):
Adrian Hunter0bf09472019-02-22 09:27:23 +02002054 if len(vars.where_clause):
2055 vars.where_clause += " AND "
2056 vars.where_clause += d.value
2057 if len(vars.where_clause):
Adrian Hunter0924cd62019-02-22 09:27:22 +02002058 if self.partial:
Adrian Hunter0bf09472019-02-22 09:27:23 +02002059 vars.where_clause = " AND ( " + vars.where_clause + " ) "
Adrian Hunter0924cd62019-02-22 09:27:22 +02002060 else:
Adrian Hunter0bf09472019-02-22 09:27:23 +02002061 vars.where_clause = " WHERE " + vars.where_clause + " "
Adrian Hunter210cf1f2018-11-04 17:12:36 +02002062 self.accept()
2063
2064 def ShowMessage(self, msg):
2065 self.status.setText("<font color=#FF0000>" + msg)
2066
2067 def ClearMessage(self):
2068 self.status.setText("")
2069
Adrian Hunter0924cd62019-02-22 09:27:22 +02002070# Selected branch report creation dialog
2071
2072class SelectedBranchDialog(ReportDialogBase):
2073
2074 def __init__(self, glb, parent=None):
2075 title = "Selected Branches"
Adrian Hunter1c3ca1b2019-02-22 09:27:25 +02002076 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2077 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
2078 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
2079 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
2080 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2081 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2082 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
2083 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
2084 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
Adrian Hunter0924cd62019-02-22 09:27:22 +02002085 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
2086
Adrian Hunter76099f92018-10-23 10:59:49 +03002087# Event list
2088
2089def GetEventList(db):
2090 events = []
2091 query = QSqlQuery(db)
2092 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
2093 while query.next():
2094 events.append(query.value(0))
2095 return events
2096
Adrian Hunter655cb9522019-02-22 09:27:20 +02002097# Is a table selectable
2098
Adrian Hunterae8b8872019-02-28 15:00:31 +02002099def IsSelectable(db, table, sql = ""):
Adrian Hunter655cb9522019-02-22 09:27:20 +02002100 query = QSqlQuery(db)
2101 try:
Adrian Hunterae8b8872019-02-28 15:00:31 +02002102 QueryExec(query, "SELECT * FROM " + table + " " + sql + " LIMIT 1")
Adrian Hunter655cb9522019-02-22 09:27:20 +02002103 except:
2104 return False
2105 return True
2106
Adrian Hunter8392b742018-10-01 09:28:50 +03002107# SQL table data model item
2108
2109class SQLTableItem():
2110
2111 def __init__(self, row, data):
2112 self.row = row
2113 self.data = data
2114
2115 def getData(self, column):
2116 return self.data[column]
2117
2118# SQL table data model
2119
2120class SQLTableModel(TableModel):
2121
2122 progress = Signal(object)
2123
Adrian Hunter8c90fef2019-02-22 09:27:21 +02002124 def __init__(self, glb, sql, column_headers, parent=None):
Adrian Hunter8392b742018-10-01 09:28:50 +03002125 super(SQLTableModel, self).__init__(parent)
2126 self.glb = glb
2127 self.more = True
2128 self.populated = 0
Adrian Hunter8c90fef2019-02-22 09:27:21 +02002129 self.column_headers = column_headers
Adrian Hunter8453c932019-03-27 09:28:25 +02002130 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
Adrian Hunter8392b742018-10-01 09:28:50 +03002131 self.fetcher.done.connect(self.Update)
2132 self.fetcher.Fetch(glb_chunk_sz)
2133
2134 def DisplayData(self, item, index):
2135 self.FetchIfNeeded(item.row)
2136 return item.getData(index.column())
2137
2138 def AddSample(self, data):
2139 child = SQLTableItem(self.populated, data)
2140 self.child_items.append(child)
2141 self.populated += 1
2142
2143 def Update(self, fetched):
2144 if not fetched:
2145 self.more = False
2146 self.progress.emit(0)
2147 child_count = self.child_count
2148 count = self.populated - child_count
2149 if count > 0:
2150 parent = QModelIndex()
2151 self.beginInsertRows(parent, child_count, child_count + count - 1)
2152 self.insertRows(child_count, count, parent)
2153 self.child_count += count
2154 self.endInsertRows()
2155 self.progress.emit(self.child_count)
2156
2157 def FetchMoreRecords(self, count):
2158 current = self.child_count
2159 if self.more:
2160 self.fetcher.Fetch(count)
2161 else:
2162 self.progress.emit(0)
2163 return current
2164
2165 def HasMoreRecords(self):
2166 return self.more
2167
Adrian Hunter8c90fef2019-02-22 09:27:21 +02002168 def columnCount(self, parent=None):
2169 return len(self.column_headers)
2170
2171 def columnHeader(self, column):
2172 return self.column_headers[column]
2173
Adrian Hunter8453c932019-03-27 09:28:25 +02002174 def SQLTableDataPrep(self, query, count):
2175 data = []
2176 for i in xrange(count):
2177 data.append(query.value(i))
2178 return data
2179
Adrian Hunter8392b742018-10-01 09:28:50 +03002180# SQL automatic table data model
2181
2182class SQLAutoTableModel(SQLTableModel):
2183
2184 def __init__(self, glb, table_name, parent=None):
2185 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
2186 if table_name == "comm_threads_view":
2187 # For now, comm_threads_view has no id column
2188 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
Adrian Hunter8c90fef2019-02-22 09:27:21 +02002189 column_headers = []
Adrian Hunter8392b742018-10-01 09:28:50 +03002190 query = QSqlQuery(glb.db)
2191 if glb.dbref.is_sqlite3:
2192 QueryExec(query, "PRAGMA table_info(" + table_name + ")")
2193 while query.next():
Adrian Hunter8c90fef2019-02-22 09:27:21 +02002194 column_headers.append(query.value(1))
Adrian Hunter8392b742018-10-01 09:28:50 +03002195 if table_name == "sqlite_master":
2196 sql = "SELECT * FROM " + table_name
2197 else:
2198 if table_name[:19] == "information_schema.":
2199 sql = "SELECT * FROM " + table_name
2200 select_table_name = table_name[19:]
2201 schema = "information_schema"
2202 else:
2203 select_table_name = table_name
2204 schema = "public"
2205 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
2206 while query.next():
Adrian Hunter8c90fef2019-02-22 09:27:21 +02002207 column_headers.append(query.value(0))
Adrian Hunter8453c932019-03-27 09:28:25 +02002208 if pyside_version_1 and sys.version_info[0] == 3:
2209 if table_name == "samples_view":
2210 self.SQLTableDataPrep = self.samples_view_DataPrep
2211 if table_name == "samples":
2212 self.SQLTableDataPrep = self.samples_DataPrep
Adrian Hunter8c90fef2019-02-22 09:27:21 +02002213 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
Adrian Hunter8392b742018-10-01 09:28:50 +03002214
Adrian Hunter8453c932019-03-27 09:28:25 +02002215 def samples_view_DataPrep(self, query, count):
2216 data = []
2217 data.append(query.value(0))
2218 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
2219 data.append("{:>19}".format(query.value(1)))
2220 for i in xrange(2, count):
2221 data.append(query.value(i))
2222 return data
2223
2224 def samples_DataPrep(self, query, count):
2225 data = []
2226 for i in xrange(9):
2227 data.append(query.value(i))
2228 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
2229 data.append("{:>19}".format(query.value(9)))
2230 for i in xrange(10, count):
2231 data.append(query.value(i))
2232 return data
2233
Adrian Hunter8392b742018-10-01 09:28:50 +03002234# Base class for custom ResizeColumnsToContents
2235
2236class ResizeColumnsToContentsBase(QObject):
2237
2238 def __init__(self, parent=None):
2239 super(ResizeColumnsToContentsBase, self).__init__(parent)
2240
2241 def ResizeColumnToContents(self, column, n):
2242 # Using the view's resizeColumnToContents() here is extrememly slow
2243 # so implement a crude alternative
2244 font = self.view.font()
2245 metrics = QFontMetrics(font)
2246 max = 0
2247 for row in xrange(n):
2248 val = self.data_model.child_items[row].data[column]
2249 len = metrics.width(str(val) + "MM")
2250 max = len if len > max else max
2251 val = self.data_model.columnHeader(column)
2252 len = metrics.width(str(val) + "MM")
2253 max = len if len > max else max
2254 self.view.setColumnWidth(column, max)
2255
2256 def ResizeColumnsToContents(self):
2257 n = min(self.data_model.child_count, 100)
2258 if n < 1:
2259 # No data yet, so connect a signal to notify when there is
2260 self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
2261 return
2262 columns = self.data_model.columnCount()
2263 for i in xrange(columns):
2264 self.ResizeColumnToContents(i, n)
2265
2266 def UpdateColumnWidths(self, *x):
2267 # This only needs to be done once, so disconnect the signal now
2268 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
2269 self.ResizeColumnsToContents()
2270
Adrian Hunter96c43b92019-05-03 15:08:26 +03002271# Convert value to CSV
2272
2273def ToCSValue(val):
2274 if '"' in val:
2275 val = val.replace('"', '""')
2276 if "," in val or '"' in val:
2277 val = '"' + val + '"'
2278 return val
2279
2280# Key to sort table model indexes by row / column, assuming fewer than 1000 columns
2281
2282glb_max_cols = 1000
2283
2284def RowColumnKey(a):
2285 return a.row() * glb_max_cols + a.column()
2286
2287# Copy selected table cells to clipboard
2288
2289def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
2290 indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
2291 idx_cnt = len(indexes)
2292 if not idx_cnt:
2293 return
2294 if idx_cnt == 1:
2295 with_hdr=False
2296 min_row = indexes[0].row()
2297 max_row = indexes[0].row()
2298 min_col = indexes[0].column()
2299 max_col = indexes[0].column()
2300 for i in indexes:
2301 min_row = min(min_row, i.row())
2302 max_row = max(max_row, i.row())
2303 min_col = min(min_col, i.column())
2304 max_col = max(max_col, i.column())
2305 if max_col > glb_max_cols:
2306 raise RuntimeError("glb_max_cols is too low")
2307 max_width = [0] * (1 + max_col - min_col)
2308 for i in indexes:
2309 c = i.column() - min_col
2310 max_width[c] = max(max_width[c], len(str(i.data())))
2311 text = ""
2312 pad = ""
2313 sep = ""
2314 if with_hdr:
2315 model = indexes[0].model()
2316 for col in range(min_col, max_col + 1):
2317 val = model.headerData(col, Qt.Horizontal)
2318 if as_csv:
2319 text += sep + ToCSValue(val)
2320 sep = ","
2321 else:
2322 c = col - min_col
2323 max_width[c] = max(max_width[c], len(val))
2324 width = max_width[c]
2325 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
2326 if align & Qt.AlignRight:
2327 val = val.rjust(width)
2328 text += pad + sep + val
2329 pad = " " * (width - len(val))
2330 sep = " "
2331 text += "\n"
2332 pad = ""
2333 sep = ""
2334 last_row = min_row
2335 for i in indexes:
2336 if i.row() > last_row:
2337 last_row = i.row()
2338 text += "\n"
2339 pad = ""
2340 sep = ""
2341 if as_csv:
2342 text += sep + ToCSValue(str(i.data()))
2343 sep = ","
2344 else:
2345 width = max_width[i.column() - min_col]
2346 if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
2347 val = str(i.data()).rjust(width)
2348 else:
2349 val = str(i.data())
2350 text += pad + sep + val
2351 pad = " " * (width - len(val))
2352 sep = " "
2353 QApplication.clipboard().setText(text)
2354
2355def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
2356 indexes = view.selectedIndexes()
2357 if not len(indexes):
2358 return
2359
2360 selection = view.selectionModel()
2361
2362 first = None
2363 for i in indexes:
2364 above = view.indexAbove(i)
2365 if not selection.isSelected(above):
2366 first = i
2367 break
2368
2369 if first is None:
2370 raise RuntimeError("CopyTreeCellsToClipboard internal error")
2371
2372 model = first.model()
2373 row_cnt = 0
2374 col_cnt = model.columnCount(first)
2375 max_width = [0] * col_cnt
2376
2377 indent_sz = 2
2378 indent_str = " " * indent_sz
2379
2380 expanded_mark_sz = 2
2381 if sys.version_info[0] == 3:
2382 expanded_mark = "\u25BC "
2383 not_expanded_mark = "\u25B6 "
2384 else:
2385 expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")
2386 not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")
2387 leaf_mark = " "
2388
2389 if not as_csv:
2390 pos = first
2391 while True:
2392 row_cnt += 1
2393 row = pos.row()
2394 for c in range(col_cnt):
2395 i = pos.sibling(row, c)
2396 if c:
2397 n = len(str(i.data()))
2398 else:
2399 n = len(str(i.data()).strip())
2400 n += (i.internalPointer().level - 1) * indent_sz
2401 n += expanded_mark_sz
2402 max_width[c] = max(max_width[c], n)
2403 pos = view.indexBelow(pos)
2404 if not selection.isSelected(pos):
2405 break
2406
2407 text = ""
2408 pad = ""
2409 sep = ""
2410 if with_hdr:
2411 for c in range(col_cnt):
2412 val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
2413 if as_csv:
2414 text += sep + ToCSValue(val)
2415 sep = ","
2416 else:
2417 max_width[c] = max(max_width[c], len(val))
2418 width = max_width[c]
2419 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
2420 if align & Qt.AlignRight:
2421 val = val.rjust(width)
2422 text += pad + sep + val
2423 pad = " " * (width - len(val))
2424 sep = " "
2425 text += "\n"
2426 pad = ""
2427 sep = ""
2428
2429 pos = first
2430 while True:
2431 row = pos.row()
2432 for c in range(col_cnt):
2433 i = pos.sibling(row, c)
2434 val = str(i.data())
2435 if not c:
2436 if model.hasChildren(i):
2437 if view.isExpanded(i):
2438 mark = expanded_mark
2439 else:
2440 mark = not_expanded_mark
2441 else:
2442 mark = leaf_mark
2443 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
2444 if as_csv:
2445 text += sep + ToCSValue(val)
2446 sep = ","
2447 else:
2448 width = max_width[c]
2449 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
2450 val = val.rjust(width)
2451 text += pad + sep + val
2452 pad = " " * (width - len(val))
2453 sep = " "
2454 pos = view.indexBelow(pos)
2455 if not selection.isSelected(pos):
2456 break
2457 text = text.rstrip() + "\n"
2458 pad = ""
2459 sep = ""
2460
2461 QApplication.clipboard().setText(text)
2462
2463def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
2464 view.CopyCellsToClipboard(view, as_csv, with_hdr)
2465
2466def CopyCellsToClipboardHdr(view):
2467 CopyCellsToClipboard(view, False, True)
2468
2469def CopyCellsToClipboardCSV(view):
2470 CopyCellsToClipboard(view, True, True)
2471
Adrian Hunter8392b742018-10-01 09:28:50 +03002472# Table window
2473
2474class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2475
2476 def __init__(self, glb, table_name, parent=None):
2477 super(TableWindow, self).__init__(parent)
2478
2479 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
2480
2481 self.model = QSortFilterProxyModel()
2482 self.model.setSourceModel(self.data_model)
2483
2484 self.view = QTableView()
2485 self.view.setModel(self.model)
2486 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2487 self.view.verticalHeader().setVisible(False)
2488 self.view.sortByColumn(-1, Qt.AscendingOrder)
2489 self.view.setSortingEnabled(True)
Adrian Hunter96c43b92019-05-03 15:08:26 +03002490 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
2491 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
Adrian Hunter8392b742018-10-01 09:28:50 +03002492
2493 self.ResizeColumnsToContents()
2494
2495 self.find_bar = FindBar(self, self, True)
2496
2497 self.finder = ChildDataItemFinder(self.data_model)
2498
2499 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2500
2501 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2502
2503 self.setWidget(self.vbox.Widget())
2504
2505 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
2506
2507 def Find(self, value, direction, pattern, context):
2508 self.view.setFocus()
2509 self.find_bar.Busy()
2510 self.finder.Find(value, direction, pattern, context, self.FindDone)
2511
2512 def FindDone(self, row):
2513 self.find_bar.Idle()
2514 if row >= 0:
Adrian Hunter35fa1ce2018-11-04 17:12:38 +02002515 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
Adrian Hunter8392b742018-10-01 09:28:50 +03002516 else:
2517 self.find_bar.NotFound()
2518
2519# Table list
2520
2521def GetTableList(glb):
2522 tables = []
2523 query = QSqlQuery(glb.db)
2524 if glb.dbref.is_sqlite3:
2525 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
2526 else:
2527 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
2528 while query.next():
2529 tables.append(query.value(0))
2530 if glb.dbref.is_sqlite3:
2531 tables.append("sqlite_master")
2532 else:
2533 tables.append("information_schema.tables")
2534 tables.append("information_schema.views")
2535 tables.append("information_schema.columns")
2536 return tables
2537
Adrian Huntercd358012019-02-22 09:27:28 +02002538# Top Calls data model
2539
2540class TopCallsModel(SQLTableModel):
2541
2542 def __init__(self, glb, report_vars, parent=None):
2543 text = ""
2544 if not glb.dbref.is_sqlite3:
2545 text = "::text"
2546 limit = ""
2547 if len(report_vars.limit):
2548 limit = " LIMIT " + report_vars.limit
2549 sql = ("SELECT comm, pid, tid, name,"
2550 " CASE"
2551 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
2552 " ELSE short_name"
2553 " END AS dso,"
2554 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
2555 " CASE"
2556 " WHEN (calls.flags = 1) THEN 'no call'" + text +
2557 " WHEN (calls.flags = 2) THEN 'no return'" + text +
2558 " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
2559 " ELSE ''" + text +
2560 " END AS flags"
2561 " FROM calls"
2562 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
2563 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
2564 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
2565 " INNER JOIN comms ON calls.comm_id = comms.id"
2566 " INNER JOIN threads ON calls.thread_id = threads.id" +
2567 report_vars.where_clause +
2568 " ORDER BY elapsed_time DESC" +
2569 limit
2570 )
2571 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
2572 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
2573 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
2574
2575 def columnAlignment(self, column):
2576 return self.alignment[column]
2577
2578# Top Calls report creation dialog
2579
2580class TopCallsDialog(ReportDialogBase):
2581
2582 def __init__(self, glb, parent=None):
2583 title = "Top Calls by Elapsed Time"
2584 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2585 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
2586 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2587 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2588 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
2589 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
2590 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
2591 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
2592 super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
2593
2594# Top Calls window
2595
2596class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2597
2598 def __init__(self, glb, report_vars, parent=None):
2599 super(TopCallsWindow, self).__init__(parent)
2600
2601 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
2602 self.model = self.data_model
2603
2604 self.view = QTableView()
2605 self.view.setModel(self.model)
2606 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2607 self.view.verticalHeader().setVisible(False)
Adrian Hunter96c43b92019-05-03 15:08:26 +03002608 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
2609 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
Adrian Huntercd358012019-02-22 09:27:28 +02002610
2611 self.ResizeColumnsToContents()
2612
2613 self.find_bar = FindBar(self, self, True)
2614
2615 self.finder = ChildDataItemFinder(self.model)
2616
2617 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2618
2619 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2620
2621 self.setWidget(self.vbox.Widget())
2622
2623 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
2624
2625 def Find(self, value, direction, pattern, context):
2626 self.view.setFocus()
2627 self.find_bar.Busy()
2628 self.finder.Find(value, direction, pattern, context, self.FindDone)
2629
2630 def FindDone(self, row):
2631 self.find_bar.Idle()
2632 if row >= 0:
2633 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
2634 else:
2635 self.find_bar.NotFound()
2636
Adrian Hunter1beb5c72018-10-01 09:28:47 +03002637# Action Definition
2638
2639def CreateAction(label, tip, callback, parent=None, shortcut=None):
2640 action = QAction(label, parent)
2641 if shortcut != None:
2642 action.setShortcuts(shortcut)
2643 action.setStatusTip(tip)
2644 action.triggered.connect(callback)
2645 return action
2646
2647# Typical application actions
2648
2649def CreateExitAction(app, parent=None):
2650 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
2651
2652# Typical MDI actions
2653
2654def CreateCloseActiveWindowAction(mdi_area):
2655 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
2656
2657def CreateCloseAllWindowsAction(mdi_area):
2658 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
2659
2660def CreateTileWindowsAction(mdi_area):
2661 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
2662
2663def CreateCascadeWindowsAction(mdi_area):
2664 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
2665
2666def CreateNextWindowAction(mdi_area):
2667 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
2668
2669def CreatePreviousWindowAction(mdi_area):
2670 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
2671
2672# Typical MDI window menu
2673
2674class WindowMenu():
2675
2676 def __init__(self, mdi_area, menu):
2677 self.mdi_area = mdi_area
2678 self.window_menu = menu.addMenu("&Windows")
2679 self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
2680 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
2681 self.tile_windows = CreateTileWindowsAction(mdi_area)
2682 self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
2683 self.next_window = CreateNextWindowAction(mdi_area)
2684 self.previous_window = CreatePreviousWindowAction(mdi_area)
2685 self.window_menu.aboutToShow.connect(self.Update)
2686
2687 def Update(self):
2688 self.window_menu.clear()
2689 sub_window_count = len(self.mdi_area.subWindowList())
2690 have_sub_windows = sub_window_count != 0
2691 self.close_active_window.setEnabled(have_sub_windows)
2692 self.close_all_windows.setEnabled(have_sub_windows)
2693 self.tile_windows.setEnabled(have_sub_windows)
2694 self.cascade_windows.setEnabled(have_sub_windows)
2695 self.next_window.setEnabled(have_sub_windows)
2696 self.previous_window.setEnabled(have_sub_windows)
2697 self.window_menu.addAction(self.close_active_window)
2698 self.window_menu.addAction(self.close_all_windows)
2699 self.window_menu.addSeparator()
2700 self.window_menu.addAction(self.tile_windows)
2701 self.window_menu.addAction(self.cascade_windows)
2702 self.window_menu.addSeparator()
2703 self.window_menu.addAction(self.next_window)
2704 self.window_menu.addAction(self.previous_window)
2705 if sub_window_count == 0:
2706 return
2707 self.window_menu.addSeparator()
2708 nr = 1
2709 for sub_window in self.mdi_area.subWindowList():
2710 label = str(nr) + " " + sub_window.name
2711 if nr < 10:
2712 label = "&" + label
2713 action = self.window_menu.addAction(label)
2714 action.setCheckable(True)
2715 action.setChecked(sub_window == self.mdi_area.activeSubWindow())
2716 action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x))
2717 self.window_menu.addAction(action)
2718 nr += 1
2719
2720 def setActiveSubWindow(self, nr):
2721 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
2722
Adrian Hunter65b24292018-11-04 17:12:37 +02002723# Help text
2724
2725glb_help_text = """
2726<h1>Contents</h1>
2727<style>
2728p.c1 {
2729 text-indent: 40px;
2730}
2731p.c2 {
2732 text-indent: 80px;
2733}
2734}
2735</style>
2736<p class=c1><a href=#reports>1. Reports</a></p>
2737<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
Adrian Hunterae8b8872019-02-28 15:00:31 +02002738<p class=c2><a href=#calltree>1.2 Call Tree</a></p>
2739<p class=c2><a href=#allbranches>1.3 All branches</a></p>
2740<p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
2741<p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
Adrian Hunter65b24292018-11-04 17:12:37 +02002742<p class=c1><a href=#tables>2. Tables</a></p>
2743<h1 id=reports>1. Reports</h1>
2744<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
2745The result is a GUI window with a tree representing a context-sensitive
2746call-graph. Expanding a couple of levels of the tree and adjusting column
2747widths to suit will display something like:
2748<pre>
2749 Call Graph: pt_example
2750Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
2751v- ls
2752 v- 2638:2638
2753 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
2754 |- unknown unknown 1 13198 0.1 1 0.0
2755 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
2756 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
2757 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
2758 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
2759 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
2760 >- __libc_csu_init ls 1 10354 0.1 10 0.0
2761 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
2762 v- main ls 1 8182043 99.6 180254 99.9
2763</pre>
2764<h3>Points to note:</h3>
2765<ul>
2766<li>The top level is a command name (comm)</li>
2767<li>The next level is a thread (pid:tid)</li>
2768<li>Subsequent levels are functions</li>
2769<li>'Count' is the number of calls</li>
2770<li>'Time' is the elapsed time until the function returns</li>
2771<li>Percentages are relative to the level above</li>
2772<li>'Branch Count' is the total number of branches for that function and all functions that it calls
2773</ul>
2774<h3>Find</h3>
2775Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
2776The pattern matching symbols are ? for any character and * for zero or more characters.
Adrian Hunterae8b8872019-02-28 15:00:31 +02002777<h2 id=calltree>1.2 Call Tree</h2>
2778The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
2779Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
2780<h2 id=allbranches>1.3 All branches</h2>
Adrian Hunter65b24292018-11-04 17:12:37 +02002781The All branches report displays all branches in chronological order.
2782Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
2783<h3>Disassembly</h3>
2784Open a branch to display disassembly. This only works if:
2785<ol>
2786<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
2787<li>The object code is available. Currently, only the perf build ID cache is searched for object code.
2788The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
2789One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
2790or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
2791</ol>
2792<h4 id=xed>Intel XED Setup</h4>
2793To use Intel XED, libxed.so must be present. To build and install libxed.so:
2794<pre>
2795git clone https://github.com/intelxed/mbuild.git mbuild
2796git clone https://github.com/intelxed/xed
2797cd xed
2798./mfile.py --share
2799sudo ./mfile.py --prefix=/usr/local install
2800sudo ldconfig
2801</pre>
2802<h3>Find</h3>
2803Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
2804Refer to Python documentation for the regular expression syntax.
2805All columns are searched, but only currently fetched rows are searched.
Adrian Hunterae8b8872019-02-28 15:00:31 +02002806<h2 id=selectedbranches>1.4 Selected branches</h2>
Adrian Hunter65b24292018-11-04 17:12:37 +02002807This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
2808by various selection criteria. A dialog box displays available criteria which are AND'ed together.
Adrian Hunterae8b8872019-02-28 15:00:31 +02002809<h3>1.4.1 Time ranges</h3>
Adrian Hunter65b24292018-11-04 17:12:37 +02002810The time ranges hint text shows the total time range. Relative time ranges can also be entered in
2811ms, us or ns. Also, negative values are relative to the end of trace. Examples:
2812<pre>
2813 81073085947329-81073085958238 From 81073085947329 to 81073085958238
2814 100us-200us From 100us to 200us
2815 10ms- From 10ms to the end
2816 -100ns The first 100ns
2817 -10ms- The last 10ms
2818</pre>
2819N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
Adrian Hunterae8b8872019-02-28 15:00:31 +02002820<h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
Adrian Huntercd358012019-02-22 09:27:28 +02002821The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned.
2822The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
2823If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
Adrian Hunter65b24292018-11-04 17:12:37 +02002824<h1 id=tables>2. Tables</h1>
2825The Tables menu shows all tables and views in the database. Most tables have an associated view
2826which displays the information in a more friendly way. Not all data for large tables is fetched
2827immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
2828but that can be slow for large tables.
2829<p>There are also tables of database meta-information.
2830For SQLite3 databases, the sqlite_master table is included.
2831For PostgreSQL databases, information_schema.tables/views/columns are included.
2832<h3>Find</h3>
2833Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
2834Refer to Python documentation for the regular expression syntax.
2835All columns are searched, but only currently fetched rows are searched.
Adrian Hunter35fa1ce2018-11-04 17:12:38 +02002836<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
2837will go to the next/previous result in id order, instead of display order.
Adrian Hunter65b24292018-11-04 17:12:37 +02002838"""
2839
2840# Help window
2841
2842class HelpWindow(QMdiSubWindow):
2843
2844 def __init__(self, glb, parent=None):
2845 super(HelpWindow, self).__init__(parent)
2846
2847 self.text = QTextBrowser()
2848 self.text.setHtml(glb_help_text)
2849 self.text.setReadOnly(True)
2850 self.text.setOpenExternalLinks(True)
2851
2852 self.setWidget(self.text)
2853
2854 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
2855
2856# Main window that only displays the help text
2857
2858class HelpOnlyWindow(QMainWindow):
2859
2860 def __init__(self, parent=None):
2861 super(HelpOnlyWindow, self).__init__(parent)
2862
2863 self.setMinimumSize(200, 100)
2864 self.resize(800, 600)
2865 self.setWindowTitle("Exported SQL Viewer Help")
2866 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
2867
2868 self.text = QTextBrowser()
2869 self.text.setHtml(glb_help_text)
2870 self.text.setReadOnly(True)
2871 self.text.setOpenExternalLinks(True)
2872
2873 self.setCentralWidget(self.text)
2874
Adrian Hunter82f68e22018-10-01 09:28:49 +03002875# Font resize
2876
2877def ResizeFont(widget, diff):
2878 font = widget.font()
2879 sz = font.pointSize()
2880 font.setPointSize(sz + diff)
2881 widget.setFont(font)
2882
2883def ShrinkFont(widget):
2884 ResizeFont(widget, -1)
2885
2886def EnlargeFont(widget):
2887 ResizeFont(widget, 1)
2888
Adrian Hunter1beb5c72018-10-01 09:28:47 +03002889# Unique name for sub-windows
2890
2891def NumberedWindowName(name, nr):
2892 if nr > 1:
2893 name += " <" + str(nr) + ">"
2894 return name
2895
2896def UniqueSubWindowName(mdi_area, name):
2897 nr = 1
2898 while True:
2899 unique_name = NumberedWindowName(name, nr)
2900 ok = True
2901 for sub_window in mdi_area.subWindowList():
2902 if sub_window.name == unique_name:
2903 ok = False
2904 break
2905 if ok:
2906 return unique_name
2907 nr += 1
2908
2909# Add a sub-window
2910
2911def AddSubWindow(mdi_area, sub_window, name):
2912 unique_name = UniqueSubWindowName(mdi_area, name)
2913 sub_window.setMinimumSize(200, 100)
2914 sub_window.resize(800, 600)
2915 sub_window.setWindowTitle(unique_name)
2916 sub_window.setAttribute(Qt.WA_DeleteOnClose)
2917 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
2918 sub_window.name = unique_name
2919 mdi_area.addSubWindow(sub_window)
2920 sub_window.show()
2921
Adrian Hunter70d831e2018-10-01 09:28:43 +03002922# Main window
Adrian Hunter4b715d22015-07-17 19:33:45 +03002923
2924class MainWindow(QMainWindow):
2925
Adrian Hunter5f9dfef2018-10-01 09:28:41 +03002926 def __init__(self, glb, parent=None):
Adrian Hunter4b715d22015-07-17 19:33:45 +03002927 super(MainWindow, self).__init__(parent)
2928
Adrian Hunter5f9dfef2018-10-01 09:28:41 +03002929 self.glb = glb
2930
Adrian Hunter1beb5c72018-10-01 09:28:47 +03002931 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
Adrian Hunter99a097c2018-10-01 09:28:38 +03002932 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
Adrian Hunter3c4ef452018-10-01 09:28:37 +03002933 self.setMinimumSize(200, 100)
Adrian Hunter4b715d22015-07-17 19:33:45 +03002934
Adrian Hunter1beb5c72018-10-01 09:28:47 +03002935 self.mdi_area = QMdiArea()
2936 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
2937 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
Adrian Hunter4b715d22015-07-17 19:33:45 +03002938
Adrian Hunter1beb5c72018-10-01 09:28:47 +03002939 self.setCentralWidget(self.mdi_area)
Adrian Hunter4b715d22015-07-17 19:33:45 +03002940
Adrian Hunter1beb5c72018-10-01 09:28:47 +03002941 menu = self.menuBar()
Adrian Hunter1d865c02018-10-01 09:28:36 +03002942
Adrian Hunter1beb5c72018-10-01 09:28:47 +03002943 file_menu = menu.addMenu("&File")
2944 file_menu.addAction(CreateExitAction(glb.app, self))
2945
Adrian Hunterebd70c72018-10-01 09:28:48 +03002946 edit_menu = menu.addMenu("&Edit")
Adrian Hunter96c43b92019-05-03 15:08:26 +03002947 edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
2948 edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
Adrian Hunterebd70c72018-10-01 09:28:48 +03002949 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
Adrian Hunter8392b742018-10-01 09:28:50 +03002950 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
Adrian Hunter82f68e22018-10-01 09:28:49 +03002951 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
2952 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
Adrian Hunterebd70c72018-10-01 09:28:48 +03002953
Adrian Hunter1beb5c72018-10-01 09:28:47 +03002954 reports_menu = menu.addMenu("&Reports")
Adrian Hunter655cb9522019-02-22 09:27:20 +02002955 if IsSelectable(glb.db, "calls"):
2956 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
Adrian Hunter1beb5c72018-10-01 09:28:47 +03002957
Adrian Hunterae8b8872019-02-28 15:00:31 +02002958 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
2959 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
2960
Adrian Hunter76099f92018-10-23 10:59:49 +03002961 self.EventMenu(GetEventList(glb.db), reports_menu)
2962
Adrian Huntercd358012019-02-22 09:27:28 +02002963 if IsSelectable(glb.db, "calls"):
2964 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
2965
Adrian Hunter8392b742018-10-01 09:28:50 +03002966 self.TableMenu(GetTableList(glb), menu)
2967
Adrian Hunter1beb5c72018-10-01 09:28:47 +03002968 self.window_menu = WindowMenu(self.mdi_area, menu)
2969
Adrian Hunter65b24292018-11-04 17:12:37 +02002970 help_menu = menu.addMenu("&Help")
2971 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
2972
Adrian Hunter4b208452019-05-03 15:08:23 +03002973 def Try(self, fn):
2974 win = self.mdi_area.activeSubWindow()
2975 if win:
2976 try:
2977 fn(win.view)
2978 except:
2979 pass
2980
Adrian Hunter96c43b92019-05-03 15:08:26 +03002981 def CopyToClipboard(self):
2982 self.Try(CopyCellsToClipboardHdr)
2983
2984 def CopyToClipboardCSV(self):
2985 self.Try(CopyCellsToClipboardCSV)
2986
Adrian Hunterebd70c72018-10-01 09:28:48 +03002987 def Find(self):
2988 win = self.mdi_area.activeSubWindow()
2989 if win:
2990 try:
2991 win.find_bar.Activate()
2992 except:
2993 pass
2994
Adrian Hunter8392b742018-10-01 09:28:50 +03002995 def FetchMoreRecords(self):
2996 win = self.mdi_area.activeSubWindow()
2997 if win:
2998 try:
2999 win.fetch_bar.Activate()
3000 except:
3001 pass
3002
Adrian Hunter82f68e22018-10-01 09:28:49 +03003003 def ShrinkFont(self):
Adrian Hunter4b208452019-05-03 15:08:23 +03003004 self.Try(ShrinkFont)
Adrian Hunter82f68e22018-10-01 09:28:49 +03003005
3006 def EnlargeFont(self):
Adrian Hunter4b208452019-05-03 15:08:23 +03003007 self.Try(EnlargeFont)
Adrian Hunter82f68e22018-10-01 09:28:49 +03003008
Adrian Hunter76099f92018-10-23 10:59:49 +03003009 def EventMenu(self, events, reports_menu):
3010 branches_events = 0
3011 for event in events:
3012 event = event.split(":")[0]
3013 if event == "branches":
3014 branches_events += 1
3015 dbid = 0
3016 for event in events:
3017 dbid += 1
3018 event = event.split(":")[0]
3019 if event == "branches":
3020 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
3021 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda x=dbid: self.NewBranchView(x), self))
Adrian Hunter210cf1f2018-11-04 17:12:36 +02003022 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
3023 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda x=dbid: self.NewSelectedBranchView(x), self))
Adrian Hunter76099f92018-10-23 10:59:49 +03003024
Adrian Hunter8392b742018-10-01 09:28:50 +03003025 def TableMenu(self, tables, menu):
3026 table_menu = menu.addMenu("&Tables")
3027 for table in tables:
3028 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda t=table: self.NewTableView(t), self))
3029
Adrian Hunter1beb5c72018-10-01 09:28:47 +03003030 def NewCallGraph(self):
3031 CallGraphWindow(self.glb, self)
Adrian Hunter4b715d22015-07-17 19:33:45 +03003032
Adrian Hunterae8b8872019-02-28 15:00:31 +02003033 def NewCallTree(self):
3034 CallTreeWindow(self.glb, self)
3035
Adrian Huntercd358012019-02-22 09:27:28 +02003036 def NewTopCalls(self):
3037 dialog = TopCallsDialog(self.glb, self)
3038 ret = dialog.exec_()
3039 if ret:
3040 TopCallsWindow(self.glb, dialog.report_vars, self)
3041
Adrian Hunter76099f92018-10-23 10:59:49 +03003042 def NewBranchView(self, event_id):
Adrian Hunter947cc382019-02-22 09:27:24 +02003043 BranchWindow(self.glb, event_id, ReportVars(), self)
Adrian Hunter76099f92018-10-23 10:59:49 +03003044
Adrian Hunter210cf1f2018-11-04 17:12:36 +02003045 def NewSelectedBranchView(self, event_id):
3046 dialog = SelectedBranchDialog(self.glb, self)
3047 ret = dialog.exec_()
3048 if ret:
Adrian Hunter947cc382019-02-22 09:27:24 +02003049 BranchWindow(self.glb, event_id, dialog.report_vars, self)
Adrian Hunter210cf1f2018-11-04 17:12:36 +02003050
Adrian Hunter8392b742018-10-01 09:28:50 +03003051 def NewTableView(self, table_name):
3052 TableWindow(self.glb, table_name, self)
3053
Adrian Hunter65b24292018-11-04 17:12:37 +02003054 def Help(self):
3055 HelpWindow(self.glb, self)
3056
Adrian Hunter76099f92018-10-23 10:59:49 +03003057# XED Disassembler
3058
3059class xed_state_t(Structure):
3060
3061 _fields_ = [
3062 ("mode", c_int),
3063 ("width", c_int)
3064 ]
3065
3066class XEDInstruction():
3067
3068 def __init__(self, libxed):
3069 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
3070 xedd_t = c_byte * 512
3071 self.xedd = xedd_t()
3072 self.xedp = addressof(self.xedd)
3073 libxed.xed_decoded_inst_zero(self.xedp)
3074 self.state = xed_state_t()
3075 self.statep = addressof(self.state)
3076 # Buffer for disassembled instruction text
3077 self.buffer = create_string_buffer(256)
3078 self.bufferp = addressof(self.buffer)
3079
3080class LibXED():
3081
3082 def __init__(self):
Adrian Hunter5ed44192018-11-04 17:12:35 +02003083 try:
3084 self.libxed = CDLL("libxed.so")
3085 except:
3086 self.libxed = None
3087 if not self.libxed:
3088 self.libxed = CDLL("/usr/local/lib/libxed.so")
Adrian Hunter76099f92018-10-23 10:59:49 +03003089
3090 self.xed_tables_init = self.libxed.xed_tables_init
3091 self.xed_tables_init.restype = None
3092 self.xed_tables_init.argtypes = []
3093
3094 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
3095 self.xed_decoded_inst_zero.restype = None
3096 self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
3097
3098 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
3099 self.xed_operand_values_set_mode.restype = None
3100 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
3101
3102 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
3103 self.xed_decoded_inst_zero_keep_mode.restype = None
3104 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
3105
3106 self.xed_decode = self.libxed.xed_decode
3107 self.xed_decode.restype = c_int
3108 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
3109
3110 self.xed_format_context = self.libxed.xed_format_context
3111 self.xed_format_context.restype = c_uint
3112 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
3113
3114 self.xed_tables_init()
3115
3116 def Instruction(self):
3117 return XEDInstruction(self)
3118
3119 def SetMode(self, inst, mode):
3120 if mode:
3121 inst.state.mode = 4 # 32-bit
3122 inst.state.width = 4 # 4 bytes
3123 else:
3124 inst.state.mode = 1 # 64-bit
3125 inst.state.width = 8 # 8 bytes
3126 self.xed_operand_values_set_mode(inst.xedp, inst.statep)
3127
3128 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
3129 self.xed_decoded_inst_zero_keep_mode(inst.xedp)
3130 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
3131 if err:
3132 return 0, ""
3133 # Use AT&T mode (2), alternative is Intel (3)
3134 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
3135 if not ok:
3136 return 0, ""
Adrian Hunter606bd602019-03-27 09:28:26 +02003137 if sys.version_info[0] == 2:
3138 result = inst.buffer.value
3139 else:
3140 result = inst.buffer.value.decode()
Adrian Hunter76099f92018-10-23 10:59:49 +03003141 # Return instruction length and the disassembled instruction text
3142 # For now, assume the length is in byte 166
Adrian Hunter606bd602019-03-27 09:28:26 +02003143 return inst.xedd[166], result
Adrian Hunter76099f92018-10-23 10:59:49 +03003144
3145def TryOpen(file_name):
3146 try:
3147 return open(file_name, "rb")
3148 except:
3149 return None
3150
3151def Is64Bit(f):
3152 result = sizeof(c_void_p)
3153 # ELF support only
3154 pos = f.tell()
3155 f.seek(0)
3156 header = f.read(7)
3157 f.seek(pos)
3158 magic = header[0:4]
Adrian Hunter606bd602019-03-27 09:28:26 +02003159 if sys.version_info[0] == 2:
3160 eclass = ord(header[4])
3161 encoding = ord(header[5])
3162 version = ord(header[6])
3163 else:
3164 eclass = header[4]
3165 encoding = header[5]
3166 version = header[6]
Adrian Hunter76099f92018-10-23 10:59:49 +03003167 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
3168 result = True if eclass == 2 else False
3169 return result
3170
Adrian Hunter5f9dfef2018-10-01 09:28:41 +03003171# Global data
3172
3173class Glb():
3174
3175 def __init__(self, dbref, db, dbname):
3176 self.dbref = dbref
3177 self.db = db
3178 self.dbname = dbname
Adrian Hunter76099f92018-10-23 10:59:49 +03003179 self.home_dir = os.path.expanduser("~")
3180 self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
3181 if self.buildid_dir:
3182 self.buildid_dir += "/.build-id/"
3183 else:
3184 self.buildid_dir = self.home_dir + "/.debug/.build-id/"
Adrian Hunter5f9dfef2018-10-01 09:28:41 +03003185 self.app = None
3186 self.mainwindow = None
Adrian Hunter8392b742018-10-01 09:28:50 +03003187 self.instances_to_shutdown_on_exit = weakref.WeakSet()
Adrian Hunter76099f92018-10-23 10:59:49 +03003188 try:
3189 self.disassembler = LibXED()
3190 self.have_disassembler = True
3191 except:
3192 self.have_disassembler = False
3193
3194 def FileFromBuildId(self, build_id):
3195 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
3196 return TryOpen(file_name)
3197
3198 def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
3199 # Assume current machine i.e. no support for virtualization
3200 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
3201 file_name = os.getenv("PERF_KCORE")
3202 f = TryOpen(file_name) if file_name else None
3203 if f:
3204 return f
3205 # For now, no special handling if long_name is /proc/kcore
3206 f = TryOpen(long_name)
3207 if f:
3208 return f
3209 f = self.FileFromBuildId(build_id)
3210 if f:
3211 return f
3212 return None
Adrian Hunter8392b742018-10-01 09:28:50 +03003213
3214 def AddInstanceToShutdownOnExit(self, instance):
3215 self.instances_to_shutdown_on_exit.add(instance)
3216
3217 # Shutdown any background processes or threads
3218 def ShutdownInstances(self):
3219 for x in self.instances_to_shutdown_on_exit:
3220 try:
3221 x.Shutdown()
3222 except:
3223 pass
Adrian Hunter5f9dfef2018-10-01 09:28:41 +03003224
Adrian Hunterb2556c42018-10-01 09:28:40 +03003225# Database reference
3226
3227class DBRef():
3228
3229 def __init__(self, is_sqlite3, dbname):
3230 self.is_sqlite3 = is_sqlite3
3231 self.dbname = dbname
3232
3233 def Open(self, connection_name):
3234 dbname = self.dbname
3235 if self.is_sqlite3:
3236 db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
3237 else:
3238 db = QSqlDatabase.addDatabase("QPSQL", connection_name)
3239 opts = dbname.split()
3240 for opt in opts:
3241 if "=" in opt:
3242 opt = opt.split("=")
3243 if opt[0] == "hostname":
3244 db.setHostName(opt[1])
3245 elif opt[0] == "port":
3246 db.setPort(int(opt[1]))
3247 elif opt[0] == "username":
3248 db.setUserName(opt[1])
3249 elif opt[0] == "password":
3250 db.setPassword(opt[1])
3251 elif opt[0] == "dbname":
3252 dbname = opt[1]
3253 else:
3254 dbname = opt
3255
3256 db.setDatabaseName(dbname)
3257 if not db.open():
3258 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
3259 return db, dbname
3260
Adrian Hunter7e4fc932018-10-01 09:28:39 +03003261# Main
3262
3263def Main():
Adrian Hunter4b715d22015-07-17 19:33:45 +03003264 if (len(sys.argv) < 2):
Tony Jonesbeda0e72019-03-08 16:05:15 -08003265 printerr("Usage is: exported-sql-viewer.py {<database name> | --help-only}");
Adrian Hunter4b715d22015-07-17 19:33:45 +03003266 raise Exception("Too few arguments")
3267
3268 dbname = sys.argv[1]
Adrian Hunter65b24292018-11-04 17:12:37 +02003269 if dbname == "--help-only":
3270 app = QApplication(sys.argv)
3271 mainwindow = HelpOnlyWindow()
3272 mainwindow.show()
3273 err = app.exec_()
3274 sys.exit(err)
Adrian Hunter4b715d22015-07-17 19:33:45 +03003275
Adrian Hunter1fe03b52017-08-03 11:31:30 +03003276 is_sqlite3 = False
3277 try:
Tony Jonesbeda0e72019-03-08 16:05:15 -08003278 f = open(dbname, "rb")
3279 if f.read(15) == b'SQLite format 3':
Adrian Hunter1fe03b52017-08-03 11:31:30 +03003280 is_sqlite3 = True
3281 f.close()
3282 except:
3283 pass
Adrian Hunter4b715d22015-07-17 19:33:45 +03003284
Adrian Hunterb2556c42018-10-01 09:28:40 +03003285 dbref = DBRef(is_sqlite3, dbname)
3286 db, dbname = dbref.Open("main")
Adrian Hunter5f9dfef2018-10-01 09:28:41 +03003287 glb = Glb(dbref, db, dbname)
Adrian Hunter4b715d22015-07-17 19:33:45 +03003288 app = QApplication(sys.argv)
Adrian Hunter5f9dfef2018-10-01 09:28:41 +03003289 glb.app = app
3290 mainwindow = MainWindow(glb)
3291 glb.mainwindow = mainwindow
3292 mainwindow.show()
Adrian Hunter4b715d22015-07-17 19:33:45 +03003293 err = app.exec_()
Adrian Hunter8392b742018-10-01 09:28:50 +03003294 glb.ShutdownInstances()
Adrian Hunter4b715d22015-07-17 19:33:45 +03003295 db.close()
3296 sys.exit(err)
Adrian Hunter7e4fc932018-10-01 09:28:39 +03003297
3298if __name__ == "__main__":
3299 Main()