1#!/usr/bin/env python 2# SPDX-License-Identifier: GPL-2.0 3# exported-sql-viewer.py: view data from sql database 4# Copyright (c) 2014-2018, Intel Corporation. 5 6# 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. 9# 10# Following on from the example in the export scripts, a 11# call-graph can be displayed for the pt_example database like this: 12# 13# python tools/perf/scripts/python/exported-sql-viewer.py pt_example 14# 15# Note that for PostgreSQL, this script supports connecting to remote databases 16# by setting hostname, port, username, password, and dbname e.g. 17# 18# python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example" 19# 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 49# 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 91from __future__ import print_function 92 93import sys 94import argparse 95import weakref 96import threading 97import string 98try: 99 # Python2 100 import cPickle as pickle 101 # size of pickled integer big enough for record size 102 glb_nsz = 8 103except ImportError: 104 import pickle 105 glb_nsz = 16 106import re 107import os 108 109pyside_version_1 = True 110if not "--pyside-version-1" in sys.argv: 111 try: 112 from PySide2.QtCore import * 113 from PySide2.QtGui import * 114 from PySide2.QtSql import * 115 from PySide2.QtWidgets import * 116 pyside_version_1 = False 117 except: 118 pass 119 120if pyside_version_1: 121 from PySide.QtCore import * 122 from PySide.QtGui import * 123 from PySide.QtSql import * 124 125from decimal import * 126from ctypes import * 127from multiprocessing import Process, Array, Value, Event 128 129# xrange is range in Python3 130try: 131 xrange 132except NameError: 133 xrange = range 134 135def printerr(*args, **keyword_args): 136 print(*args, file=sys.stderr, **keyword_args) 137 138# Data formatting helpers 139 140def tohex(ip): 141 if ip < 0: 142 ip += 1 << 64 143 return "%x" % ip 144 145def offstr(offset): 146 if offset: 147 return "+0x%x" % offset 148 return "" 149 150def dsoname(name): 151 if name == "[kernel.kallsyms]": 152 return "[kernel]" 153 return name 154 155def findnth(s, sub, n, offs=0): 156 pos = s.find(sub) 157 if pos < 0: 158 return pos 159 if n <= 1: 160 return offs + pos 161 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1) 162 163# Percent to one decimal place 164 165def PercentToOneDP(n, d): 166 if not d: 167 return "0.0" 168 x = (n * Decimal(100)) / d 169 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP)) 170 171# Helper for queries that must not fail 172 173def QueryExec(query, stmt): 174 ret = query.exec_(stmt) 175 if not ret: 176 raise Exception("Query failed: " + query.lastError().text()) 177 178# Background thread 179 180class Thread(QThread): 181 182 done = Signal(object) 183 184 def __init__(self, task, param=None, parent=None): 185 super(Thread, self).__init__(parent) 186 self.task = task 187 self.param = param 188 189 def run(self): 190 while True: 191 if self.param is None: 192 done, result = self.task() 193 else: 194 done, result = self.task(self.param) 195 self.done.emit(result) 196 if done: 197 break 198 199# Tree data model 200 201class TreeModel(QAbstractItemModel): 202 203 def __init__(self, glb, params, parent=None): 204 super(TreeModel, self).__init__(parent) 205 self.glb = glb 206 self.params = params 207 self.root = self.GetRoot() 208 self.last_row_read = 0 209 210 def Item(self, parent): 211 if parent.isValid(): 212 return parent.internalPointer() 213 else: 214 return self.root 215 216 def rowCount(self, parent): 217 result = self.Item(parent).childCount() 218 if result < 0: 219 result = 0 220 self.dataChanged.emit(parent, parent) 221 return result 222 223 def hasChildren(self, parent): 224 return self.Item(parent).hasChildren() 225 226 def headerData(self, section, orientation, role): 227 if role == Qt.TextAlignmentRole: 228 return self.columnAlignment(section) 229 if role != Qt.DisplayRole: 230 return None 231 if orientation != Qt.Horizontal: 232 return None 233 return self.columnHeader(section) 234 235 def parent(self, child): 236 child_item = child.internalPointer() 237 if child_item is self.root: 238 return QModelIndex() 239 parent_item = child_item.getParentItem() 240 return self.createIndex(parent_item.getRow(), 0, parent_item) 241 242 def index(self, row, column, parent): 243 child_item = self.Item(parent).getChildItem(row) 244 return self.createIndex(row, column, child_item) 245 246 def DisplayData(self, item, index): 247 return item.getData(index.column()) 248 249 def FetchIfNeeded(self, row): 250 if row > self.last_row_read: 251 self.last_row_read = row 252 if row + 10 >= self.root.child_count: 253 self.fetcher.Fetch(glb_chunk_sz) 254 255 def columnAlignment(self, column): 256 return Qt.AlignLeft 257 258 def columnFont(self, column): 259 return None 260 261 def data(self, index, role): 262 if role == Qt.TextAlignmentRole: 263 return self.columnAlignment(index.column()) 264 if role == Qt.FontRole: 265 return self.columnFont(index.column()) 266 if role != Qt.DisplayRole: 267 return None 268 item = index.internalPointer() 269 return self.DisplayData(item, index) 270 271# Table data model 272 273class TableModel(QAbstractTableModel): 274 275 def __init__(self, parent=None): 276 super(TableModel, self).__init__(parent) 277 self.child_count = 0 278 self.child_items = [] 279 self.last_row_read = 0 280 281 def Item(self, parent): 282 if parent.isValid(): 283 return parent.internalPointer() 284 else: 285 return self 286 287 def rowCount(self, parent): 288 return self.child_count 289 290 def headerData(self, section, orientation, role): 291 if role == Qt.TextAlignmentRole: 292 return self.columnAlignment(section) 293 if role != Qt.DisplayRole: 294 return None 295 if orientation != Qt.Horizontal: 296 return None 297 return self.columnHeader(section) 298 299 def index(self, row, column, parent): 300 return self.createIndex(row, column, self.child_items[row]) 301 302 def DisplayData(self, item, index): 303 return item.getData(index.column()) 304 305 def FetchIfNeeded(self, row): 306 if row > self.last_row_read: 307 self.last_row_read = row 308 if row + 10 >= self.child_count: 309 self.fetcher.Fetch(glb_chunk_sz) 310 311 def columnAlignment(self, column): 312 return Qt.AlignLeft 313 314 def columnFont(self, column): 315 return None 316 317 def data(self, index, role): 318 if role == Qt.TextAlignmentRole: 319 return self.columnAlignment(index.column()) 320 if role == Qt.FontRole: 321 return self.columnFont(index.column()) 322 if role != Qt.DisplayRole: 323 return None 324 item = index.internalPointer() 325 return self.DisplayData(item, index) 326 327# Model cache 328 329model_cache = weakref.WeakValueDictionary() 330model_cache_lock = threading.Lock() 331 332def LookupCreateModel(model_name, create_fn): 333 model_cache_lock.acquire() 334 try: 335 model = model_cache[model_name] 336 except: 337 model = None 338 if model is None: 339 model = create_fn() 340 model_cache[model_name] = model 341 model_cache_lock.release() 342 return model 343 344# Find bar 345 346class FindBar(): 347 348 def __init__(self, parent, finder, is_reg_expr=False): 349 self.finder = finder 350 self.context = [] 351 self.last_value = None 352 self.last_pattern = None 353 354 label = QLabel("Find:") 355 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 356 357 self.textbox = QComboBox() 358 self.textbox.setEditable(True) 359 self.textbox.currentIndexChanged.connect(self.ValueChanged) 360 361 self.progress = QProgressBar() 362 self.progress.setRange(0, 0) 363 self.progress.hide() 364 365 if is_reg_expr: 366 self.pattern = QCheckBox("Regular Expression") 367 else: 368 self.pattern = QCheckBox("Pattern") 369 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 370 371 self.next_button = QToolButton() 372 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown)) 373 self.next_button.released.connect(lambda: self.NextPrev(1)) 374 375 self.prev_button = QToolButton() 376 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp)) 377 self.prev_button.released.connect(lambda: self.NextPrev(-1)) 378 379 self.close_button = QToolButton() 380 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) 381 self.close_button.released.connect(self.Deactivate) 382 383 self.hbox = QHBoxLayout() 384 self.hbox.setContentsMargins(0, 0, 0, 0) 385 386 self.hbox.addWidget(label) 387 self.hbox.addWidget(self.textbox) 388 self.hbox.addWidget(self.progress) 389 self.hbox.addWidget(self.pattern) 390 self.hbox.addWidget(self.next_button) 391 self.hbox.addWidget(self.prev_button) 392 self.hbox.addWidget(self.close_button) 393 394 self.bar = QWidget() 395 self.bar.setLayout(self.hbox); 396 self.bar.hide() 397 398 def Widget(self): 399 return self.bar 400 401 def Activate(self): 402 self.bar.show() 403 self.textbox.lineEdit().selectAll() 404 self.textbox.setFocus() 405 406 def Deactivate(self): 407 self.bar.hide() 408 409 def Busy(self): 410 self.textbox.setEnabled(False) 411 self.pattern.hide() 412 self.next_button.hide() 413 self.prev_button.hide() 414 self.progress.show() 415 416 def Idle(self): 417 self.textbox.setEnabled(True) 418 self.progress.hide() 419 self.pattern.show() 420 self.next_button.show() 421 self.prev_button.show() 422 423 def Find(self, direction): 424 value = self.textbox.currentText() 425 pattern = self.pattern.isChecked() 426 self.last_value = value 427 self.last_pattern = pattern 428 self.finder.Find(value, direction, pattern, self.context) 429 430 def ValueChanged(self): 431 value = self.textbox.currentText() 432 pattern = self.pattern.isChecked() 433 index = self.textbox.currentIndex() 434 data = self.textbox.itemData(index) 435 # Store the pattern in the combo box to keep it with the text value 436 if data == None: 437 self.textbox.setItemData(index, pattern) 438 else: 439 self.pattern.setChecked(data) 440 self.Find(0) 441 442 def NextPrev(self, direction): 443 value = self.textbox.currentText() 444 pattern = self.pattern.isChecked() 445 if value != self.last_value: 446 index = self.textbox.findText(value) 447 # Allow for a button press before the value has been added to the combo box 448 if index < 0: 449 index = self.textbox.count() 450 self.textbox.addItem(value, pattern) 451 self.textbox.setCurrentIndex(index) 452 return 453 else: 454 self.textbox.setItemData(index, pattern) 455 elif pattern != self.last_pattern: 456 # Keep the pattern recorded in the combo box up to date 457 index = self.textbox.currentIndex() 458 self.textbox.setItemData(index, pattern) 459 self.Find(direction) 460 461 def NotFound(self): 462 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found") 463 464# Context-sensitive call graph data model item base 465 466class CallGraphLevelItemBase(object): 467 468 def __init__(self, glb, params, row, parent_item): 469 self.glb = glb 470 self.params = params 471 self.row = row 472 self.parent_item = parent_item 473 self.query_done = False; 474 self.child_count = 0 475 self.child_items = [] 476 if parent_item: 477 self.level = parent_item.level + 1 478 else: 479 self.level = 0 480 481 def getChildItem(self, row): 482 return self.child_items[row] 483 484 def getParentItem(self): 485 return self.parent_item 486 487 def getRow(self): 488 return self.row 489 490 def childCount(self): 491 if not self.query_done: 492 self.Select() 493 if not self.child_count: 494 return -1 495 return self.child_count 496 497 def hasChildren(self): 498 if not self.query_done: 499 return True 500 return self.child_count > 0 501 502 def getData(self, column): 503 return self.data[column] 504 505# Context-sensitive call graph data model level 2+ item base 506 507class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase): 508 509 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item): 510 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item) 511 self.comm_id = comm_id 512 self.thread_id = thread_id 513 self.call_path_id = call_path_id 514 self.insn_cnt = insn_cnt 515 self.cyc_cnt = cyc_cnt 516 self.branch_count = branch_count 517 self.time = time 518 519 def Select(self): 520 self.query_done = True; 521 query = QSqlQuery(self.glb.db) 522 if self.params.have_ipc: 523 ipc_str = ", SUM(insn_count), SUM(cyc_count)" 524 else: 525 ipc_str = "" 526 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)" 527 " FROM calls" 528 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 529 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 530 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 531 " WHERE parent_call_path_id = " + str(self.call_path_id) + 532 " AND comm_id = " + str(self.comm_id) + 533 " AND thread_id = " + str(self.thread_id) + 534 " GROUP BY call_path_id, name, short_name" 535 " ORDER BY call_path_id") 536 while query.next(): 537 if self.params.have_ipc: 538 insn_cnt = int(query.value(5)) 539 cyc_cnt = int(query.value(6)) 540 branch_count = int(query.value(7)) 541 else: 542 insn_cnt = 0 543 cyc_cnt = 0 544 branch_count = int(query.value(5)) 545 child_item = CallGraphLevelThreeItem(self.glb, self.params, 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)), insn_cnt, cyc_cnt, branch_count, self) 546 self.child_items.append(child_item) 547 self.child_count += 1 548 549# Context-sensitive call graph data model level three item 550 551class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase): 552 553 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, name, dso, count, time, insn_cnt, cyc_cnt, branch_count, parent_item): 554 super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item) 555 dso = dsoname(dso) 556 if self.params.have_ipc: 557 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt) 558 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt) 559 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count) 560 ipc = CalcIPC(cyc_cnt, insn_cnt) 561 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ] 562 else: 563 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] 564 self.dbid = call_path_id 565 566# Context-sensitive call graph data model level two item 567 568class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase): 569 570 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item): 571 super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item) 572 if self.params.have_ipc: 573 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""] 574 else: 575 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] 576 self.dbid = thread_id 577 578 def Select(self): 579 super(CallGraphLevelTwoItem, self).Select() 580 for child_item in self.child_items: 581 self.time += child_item.time 582 self.insn_cnt += child_item.insn_cnt 583 self.cyc_cnt += child_item.cyc_cnt 584 self.branch_count += child_item.branch_count 585 for child_item in self.child_items: 586 child_item.data[4] = PercentToOneDP(child_item.time, self.time) 587 if self.params.have_ipc: 588 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt) 589 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt) 590 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count) 591 else: 592 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) 593 594# Context-sensitive call graph data model level one item 595 596class CallGraphLevelOneItem(CallGraphLevelItemBase): 597 598 def __init__(self, glb, params, row, comm_id, comm, parent_item): 599 super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item) 600 if self.params.have_ipc: 601 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""] 602 else: 603 self.data = [comm, "", "", "", "", "", ""] 604 self.dbid = comm_id 605 606 def Select(self): 607 self.query_done = True; 608 query = QSqlQuery(self.glb.db) 609 QueryExec(query, "SELECT thread_id, pid, tid" 610 " FROM comm_threads" 611 " INNER JOIN threads ON thread_id = threads.id" 612 " WHERE comm_id = " + str(self.dbid)) 613 while query.next(): 614 child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) 615 self.child_items.append(child_item) 616 self.child_count += 1 617 618# Context-sensitive call graph data model root item 619 620class CallGraphRootItem(CallGraphLevelItemBase): 621 622 def __init__(self, glb, params): 623 super(CallGraphRootItem, self).__init__(glb, params, 0, None) 624 self.dbid = 0 625 self.query_done = True; 626 query = QSqlQuery(glb.db) 627 QueryExec(query, "SELECT id, comm FROM comms") 628 while query.next(): 629 if not query.value(0): 630 continue 631 child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self) 632 self.child_items.append(child_item) 633 self.child_count += 1 634 635# Call graph model parameters 636 637class CallGraphModelParams(): 638 639 def __init__(self, glb, parent=None): 640 self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count") 641 642# Context-sensitive call graph data model base 643 644class CallGraphModelBase(TreeModel): 645 646 def __init__(self, glb, parent=None): 647 super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent) 648 649 def FindSelect(self, value, pattern, query): 650 if pattern: 651 # postgresql and sqlite pattern patching differences: 652 # postgresql LIKE is case sensitive but sqlite LIKE is not 653 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not 654 # postgresql supports ILIKE which is case insensitive 655 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive 656 if not self.glb.dbref.is_sqlite3: 657 # Escape % and _ 658 s = value.replace("%", "\%") 659 s = s.replace("_", "\_") 660 # Translate * and ? into SQL LIKE pattern characters % and _ 661 trans = string.maketrans("*?", "%_") 662 match = " LIKE '" + str(s).translate(trans) + "'" 663 else: 664 match = " GLOB '" + str(value) + "'" 665 else: 666 match = " = '" + str(value) + "'" 667 self.DoFindSelect(query, match) 668 669 def Found(self, query, found): 670 if found: 671 return self.FindPath(query) 672 return [] 673 674 def FindValue(self, value, pattern, query, last_value, last_pattern): 675 if last_value == value and pattern == last_pattern: 676 found = query.first() 677 else: 678 self.FindSelect(value, pattern, query) 679 found = query.next() 680 return self.Found(query, found) 681 682 def FindNext(self, query): 683 found = query.next() 684 if not found: 685 found = query.first() 686 return self.Found(query, found) 687 688 def FindPrev(self, query): 689 found = query.previous() 690 if not found: 691 found = query.last() 692 return self.Found(query, found) 693 694 def FindThread(self, c): 695 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern: 696 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern) 697 elif c.direction > 0: 698 ids = self.FindNext(c.query) 699 else: 700 ids = self.FindPrev(c.query) 701 return (True, ids) 702 703 def Find(self, value, direction, pattern, context, callback): 704 class Context(): 705 def __init__(self, *x): 706 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x 707 def Update(self, *x): 708 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern) 709 if len(context): 710 context[0].Update(value, direction, pattern) 711 else: 712 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None)) 713 # Use a thread so the UI is not blocked during the SELECT 714 thread = Thread(self.FindThread, context[0]) 715 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection) 716 thread.start() 717 718 def FindDone(self, thread, callback, ids): 719 callback(ids) 720 721# Context-sensitive call graph data model 722 723class CallGraphModel(CallGraphModelBase): 724 725 def __init__(self, glb, parent=None): 726 super(CallGraphModel, self).__init__(glb, parent) 727 728 def GetRoot(self): 729 return CallGraphRootItem(self.glb, self.params) 730 731 def columnCount(self, parent=None): 732 if self.params.have_ipc: 733 return 12 734 else: 735 return 7 736 737 def columnHeader(self, column): 738 if self.params.have_ipc: 739 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "] 740 else: 741 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] 742 return headers[column] 743 744 def columnAlignment(self, column): 745 if self.params.have_ipc: 746 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] 747 else: 748 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] 749 return alignment[column] 750 751 def DoFindSelect(self, query, match): 752 QueryExec(query, "SELECT call_path_id, comm_id, thread_id" 753 " FROM calls" 754 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 755 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 756 " WHERE symbols.name" + match + 757 " GROUP BY comm_id, thread_id, call_path_id" 758 " ORDER BY comm_id, thread_id, call_path_id") 759 760 def FindPath(self, query): 761 # Turn the query result into a list of ids that the tree view can walk 762 # to open the tree at the right place. 763 ids = [] 764 parent_id = query.value(0) 765 while parent_id: 766 ids.insert(0, parent_id) 767 q2 = QSqlQuery(self.glb.db) 768 QueryExec(q2, "SELECT parent_id" 769 " FROM call_paths" 770 " WHERE id = " + str(parent_id)) 771 if not q2.next(): 772 break 773 parent_id = q2.value(0) 774 # The call path root is not used 775 if ids[0] == 1: 776 del ids[0] 777 ids.insert(0, query.value(2)) 778 ids.insert(0, query.value(1)) 779 return ids 780 781# Call tree data model level 2+ item base 782 783class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase): 784 785 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, time, insn_cnt, cyc_cnt, branch_count, parent_item): 786 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item) 787 self.comm_id = comm_id 788 self.thread_id = thread_id 789 self.calls_id = calls_id 790 self.insn_cnt = insn_cnt 791 self.cyc_cnt = cyc_cnt 792 self.branch_count = branch_count 793 self.time = time 794 795 def Select(self): 796 self.query_done = True; 797 if self.calls_id == 0: 798 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id) 799 else: 800 comm_thread = "" 801 if self.params.have_ipc: 802 ipc_str = ", insn_count, cyc_count" 803 else: 804 ipc_str = "" 805 query = QSqlQuery(self.glb.db) 806 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count" 807 " FROM calls" 808 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 809 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 810 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 811 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread + 812 " ORDER BY call_time, calls.id") 813 while query.next(): 814 if self.params.have_ipc: 815 insn_cnt = int(query.value(5)) 816 cyc_cnt = int(query.value(6)) 817 branch_count = int(query.value(7)) 818 else: 819 insn_cnt = 0 820 cyc_cnt = 0 821 branch_count = int(query.value(5)) 822 child_item = CallTreeLevelThreeItem(self.glb, self.params, 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)), insn_cnt, cyc_cnt, branch_count, self) 823 self.child_items.append(child_item) 824 self.child_count += 1 825 826# Call tree data model level three item 827 828class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase): 829 830 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, name, dso, count, time, insn_cnt, cyc_cnt, branch_count, parent_item): 831 super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, time, insn_cnt, cyc_cnt, branch_count, parent_item) 832 dso = dsoname(dso) 833 if self.params.have_ipc: 834 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt) 835 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt) 836 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count) 837 ipc = CalcIPC(cyc_cnt, insn_cnt) 838 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ] 839 else: 840 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] 841 self.dbid = calls_id 842 843# Call tree data model level two item 844 845class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase): 846 847 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item): 848 super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, parent_item) 849 if self.params.have_ipc: 850 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""] 851 else: 852 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] 853 self.dbid = thread_id 854 855 def Select(self): 856 super(CallTreeLevelTwoItem, self).Select() 857 for child_item in self.child_items: 858 self.time += child_item.time 859 self.insn_cnt += child_item.insn_cnt 860 self.cyc_cnt += child_item.cyc_cnt 861 self.branch_count += child_item.branch_count 862 for child_item in self.child_items: 863 child_item.data[4] = PercentToOneDP(child_item.time, self.time) 864 if self.params.have_ipc: 865 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt) 866 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt) 867 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count) 868 else: 869 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) 870 871# Call tree data model level one item 872 873class CallTreeLevelOneItem(CallGraphLevelItemBase): 874 875 def __init__(self, glb, params, row, comm_id, comm, parent_item): 876 super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item) 877 if self.params.have_ipc: 878 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""] 879 else: 880 self.data = [comm, "", "", "", "", "", ""] 881 self.dbid = comm_id 882 883 def Select(self): 884 self.query_done = True; 885 query = QSqlQuery(self.glb.db) 886 QueryExec(query, "SELECT thread_id, pid, tid" 887 " FROM comm_threads" 888 " INNER JOIN threads ON thread_id = threads.id" 889 " WHERE comm_id = " + str(self.dbid)) 890 while query.next(): 891 child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) 892 self.child_items.append(child_item) 893 self.child_count += 1 894 895# Call tree data model root item 896 897class CallTreeRootItem(CallGraphLevelItemBase): 898 899 def __init__(self, glb, params): 900 super(CallTreeRootItem, self).__init__(glb, params, 0, None) 901 self.dbid = 0 902 self.query_done = True; 903 query = QSqlQuery(glb.db) 904 QueryExec(query, "SELECT id, comm FROM comms") 905 while query.next(): 906 if not query.value(0): 907 continue 908 child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self) 909 self.child_items.append(child_item) 910 self.child_count += 1 911 912# Call Tree data model 913 914class CallTreeModel(CallGraphModelBase): 915 916 def __init__(self, glb, parent=None): 917 super(CallTreeModel, self).__init__(glb, parent) 918 919 def GetRoot(self): 920 return CallTreeRootItem(self.glb, self.params) 921 922 def columnCount(self, parent=None): 923 if self.params.have_ipc: 924 return 12 925 else: 926 return 7 927 928 def columnHeader(self, column): 929 if self.params.have_ipc: 930 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "] 931 else: 932 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] 933 return headers[column] 934 935 def columnAlignment(self, column): 936 if self.params.have_ipc: 937 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] 938 else: 939 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] 940 return alignment[column] 941 942 def DoFindSelect(self, query, match): 943 QueryExec(query, "SELECT calls.id, comm_id, thread_id" 944 " FROM calls" 945 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 946 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 947 " WHERE symbols.name" + match + 948 " ORDER BY comm_id, thread_id, call_time, calls.id") 949 950 def FindPath(self, query): 951 # Turn the query result into a list of ids that the tree view can walk 952 # to open the tree at the right place. 953 ids = [] 954 parent_id = query.value(0) 955 while parent_id: 956 ids.insert(0, parent_id) 957 q2 = QSqlQuery(self.glb.db) 958 QueryExec(q2, "SELECT parent_id" 959 " FROM calls" 960 " WHERE id = " + str(parent_id)) 961 if not q2.next(): 962 break 963 parent_id = q2.value(0) 964 ids.insert(0, query.value(2)) 965 ids.insert(0, query.value(1)) 966 return ids 967 968# Vertical widget layout 969 970class VBox(): 971 972 def __init__(self, w1, w2, w3=None): 973 self.vbox = QWidget() 974 self.vbox.setLayout(QVBoxLayout()); 975 976 self.vbox.layout().setContentsMargins(0, 0, 0, 0) 977 978 self.vbox.layout().addWidget(w1) 979 self.vbox.layout().addWidget(w2) 980 if w3: 981 self.vbox.layout().addWidget(w3) 982 983 def Widget(self): 984 return self.vbox 985 986# Tree window base 987 988class TreeWindowBase(QMdiSubWindow): 989 990 def __init__(self, parent=None): 991 super(TreeWindowBase, self).__init__(parent) 992 993 self.model = None 994 self.find_bar = None 995 996 self.view = QTreeView() 997 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) 998 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard 999 1000 self.context_menu = TreeContextMenu(self.view) 1001 1002 def DisplayFound(self, ids): 1003 if not len(ids): 1004 return False 1005 parent = QModelIndex() 1006 for dbid in ids: 1007 found = False 1008 n = self.model.rowCount(parent) 1009 for row in xrange(n): 1010 child = self.model.index(row, 0, parent) 1011 if child.internalPointer().dbid == dbid: 1012 found = True 1013 self.view.setCurrentIndex(child) 1014 parent = child 1015 break 1016 if not found: 1017 break 1018 return found 1019 1020 def Find(self, value, direction, pattern, context): 1021 self.view.setFocus() 1022 self.find_bar.Busy() 1023 self.model.Find(value, direction, pattern, context, self.FindDone) 1024 1025 def FindDone(self, ids): 1026 found = True 1027 if not self.DisplayFound(ids): 1028 found = False 1029 self.find_bar.Idle() 1030 if not found: 1031 self.find_bar.NotFound() 1032 1033 1034# Context-sensitive call graph window 1035 1036class CallGraphWindow(TreeWindowBase): 1037 1038 def __init__(self, glb, parent=None): 1039 super(CallGraphWindow, self).__init__(parent) 1040 1041 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x)) 1042 1043 self.view.setModel(self.model) 1044 1045 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): 1046 self.view.setColumnWidth(c, w) 1047 1048 self.find_bar = FindBar(self, self) 1049 1050 self.vbox = VBox(self.view, self.find_bar.Widget()) 1051 1052 self.setWidget(self.vbox.Widget()) 1053 1054 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph") 1055 1056# Call tree window 1057 1058class CallTreeWindow(TreeWindowBase): 1059 1060 def __init__(self, glb, parent=None): 1061 super(CallTreeWindow, self).__init__(parent) 1062 1063 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x)) 1064 1065 self.view.setModel(self.model) 1066 1067 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)): 1068 self.view.setColumnWidth(c, w) 1069 1070 self.find_bar = FindBar(self, self) 1071 1072 self.vbox = VBox(self.view, self.find_bar.Widget()) 1073 1074 self.setWidget(self.vbox.Widget()) 1075 1076 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree") 1077 1078# Child data item finder 1079 1080class ChildDataItemFinder(): 1081 1082 def __init__(self, root): 1083 self.root = root 1084 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5 1085 self.rows = [] 1086 self.pos = 0 1087 1088 def FindSelect(self): 1089 self.rows = [] 1090 if self.pattern: 1091 pattern = re.compile(self.value) 1092 for child in self.root.child_items: 1093 for column_data in child.data: 1094 if re.search(pattern, str(column_data)) is not None: 1095 self.rows.append(child.row) 1096 break 1097 else: 1098 for child in self.root.child_items: 1099 for column_data in child.data: 1100 if self.value in str(column_data): 1101 self.rows.append(child.row) 1102 break 1103 1104 def FindValue(self): 1105 self.pos = 0 1106 if self.last_value != self.value or self.pattern != self.last_pattern: 1107 self.FindSelect() 1108 if not len(self.rows): 1109 return -1 1110 return self.rows[self.pos] 1111 1112 def FindThread(self): 1113 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern: 1114 row = self.FindValue() 1115 elif len(self.rows): 1116 if self.direction > 0: 1117 self.pos += 1 1118 if self.pos >= len(self.rows): 1119 self.pos = 0 1120 else: 1121 self.pos -= 1 1122 if self.pos < 0: 1123 self.pos = len(self.rows) - 1 1124 row = self.rows[self.pos] 1125 else: 1126 row = -1 1127 return (True, row) 1128 1129 def Find(self, value, direction, pattern, context, callback): 1130 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern) 1131 # Use a thread so the UI is not blocked 1132 thread = Thread(self.FindThread) 1133 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection) 1134 thread.start() 1135 1136 def FindDone(self, thread, callback, row): 1137 callback(row) 1138 1139# Number of database records to fetch in one go 1140 1141glb_chunk_sz = 10000 1142 1143# Background process for SQL data fetcher 1144 1145class SQLFetcherProcess(): 1146 1147 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep): 1148 # Need a unique connection name 1149 conn_name = "SQLFetcher" + str(os.getpid()) 1150 self.db, dbname = dbref.Open(conn_name) 1151 self.sql = sql 1152 self.buffer = buffer 1153 self.head = head 1154 self.tail = tail 1155 self.fetch_count = fetch_count 1156 self.fetching_done = fetching_done 1157 self.process_target = process_target 1158 self.wait_event = wait_event 1159 self.fetched_event = fetched_event 1160 self.prep = prep 1161 self.query = QSqlQuery(self.db) 1162 self.query_limit = 0 if "$$last_id$$" in sql else 2 1163 self.last_id = -1 1164 self.fetched = 0 1165 self.more = True 1166 self.local_head = self.head.value 1167 self.local_tail = self.tail.value 1168 1169 def Select(self): 1170 if self.query_limit: 1171 if self.query_limit == 1: 1172 return 1173 self.query_limit -= 1 1174 stmt = self.sql.replace("$$last_id$$", str(self.last_id)) 1175 QueryExec(self.query, stmt) 1176 1177 def Next(self): 1178 if not self.query.next(): 1179 self.Select() 1180 if not self.query.next(): 1181 return None 1182 self.last_id = self.query.value(0) 1183 return self.prep(self.query) 1184 1185 def WaitForTarget(self): 1186 while True: 1187 self.wait_event.clear() 1188 target = self.process_target.value 1189 if target > self.fetched or target < 0: 1190 break 1191 self.wait_event.wait() 1192 return target 1193 1194 def HasSpace(self, sz): 1195 if self.local_tail <= self.local_head: 1196 space = len(self.buffer) - self.local_head 1197 if space > sz: 1198 return True 1199 if space >= glb_nsz: 1200 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer 1201 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL) 1202 self.buffer[self.local_head : self.local_head + len(nd)] = nd 1203 self.local_head = 0 1204 if self.local_tail - self.local_head > sz: 1205 return True 1206 return False 1207 1208 def WaitForSpace(self, sz): 1209 if self.HasSpace(sz): 1210 return 1211 while True: 1212 self.wait_event.clear() 1213 self.local_tail = self.tail.value 1214 if self.HasSpace(sz): 1215 return 1216 self.wait_event.wait() 1217 1218 def AddToBuffer(self, obj): 1219 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL) 1220 n = len(d) 1221 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL) 1222 sz = n + glb_nsz 1223 self.WaitForSpace(sz) 1224 pos = self.local_head 1225 self.buffer[pos : pos + len(nd)] = nd 1226 self.buffer[pos + glb_nsz : pos + sz] = d 1227 self.local_head += sz 1228 1229 def FetchBatch(self, batch_size): 1230 fetched = 0 1231 while batch_size > fetched: 1232 obj = self.Next() 1233 if obj is None: 1234 self.more = False 1235 break 1236 self.AddToBuffer(obj) 1237 fetched += 1 1238 if fetched: 1239 self.fetched += fetched 1240 with self.fetch_count.get_lock(): 1241 self.fetch_count.value += fetched 1242 self.head.value = self.local_head 1243 self.fetched_event.set() 1244 1245 def Run(self): 1246 while self.more: 1247 target = self.WaitForTarget() 1248 if target < 0: 1249 break 1250 batch_size = min(glb_chunk_sz, target - self.fetched) 1251 self.FetchBatch(batch_size) 1252 self.fetching_done.value = True 1253 self.fetched_event.set() 1254 1255def SQLFetcherFn(*x): 1256 process = SQLFetcherProcess(*x) 1257 process.Run() 1258 1259# SQL data fetcher 1260 1261class SQLFetcher(QObject): 1262 1263 done = Signal(object) 1264 1265 def __init__(self, glb, sql, prep, process_data, parent=None): 1266 super(SQLFetcher, self).__init__(parent) 1267 self.process_data = process_data 1268 self.more = True 1269 self.target = 0 1270 self.last_target = 0 1271 self.fetched = 0 1272 self.buffer_size = 16 * 1024 * 1024 1273 self.buffer = Array(c_char, self.buffer_size, lock=False) 1274 self.head = Value(c_longlong) 1275 self.tail = Value(c_longlong) 1276 self.local_tail = 0 1277 self.fetch_count = Value(c_longlong) 1278 self.fetching_done = Value(c_bool) 1279 self.last_count = 0 1280 self.process_target = Value(c_longlong) 1281 self.wait_event = Event() 1282 self.fetched_event = Event() 1283 glb.AddInstanceToShutdownOnExit(self) 1284 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)) 1285 self.process.start() 1286 self.thread = Thread(self.Thread) 1287 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection) 1288 self.thread.start() 1289 1290 def Shutdown(self): 1291 # Tell the thread and process to exit 1292 self.process_target.value = -1 1293 self.wait_event.set() 1294 self.more = False 1295 self.fetching_done.value = True 1296 self.fetched_event.set() 1297 1298 def Thread(self): 1299 if not self.more: 1300 return True, 0 1301 while True: 1302 self.fetched_event.clear() 1303 fetch_count = self.fetch_count.value 1304 if fetch_count != self.last_count: 1305 break 1306 if self.fetching_done.value: 1307 self.more = False 1308 return True, 0 1309 self.fetched_event.wait() 1310 count = fetch_count - self.last_count 1311 self.last_count = fetch_count 1312 self.fetched += count 1313 return False, count 1314 1315 def Fetch(self, nr): 1316 if not self.more: 1317 # -1 inidcates there are no more 1318 return -1 1319 result = self.fetched 1320 extra = result + nr - self.target 1321 if extra > 0: 1322 self.target += extra 1323 # process_target < 0 indicates shutting down 1324 if self.process_target.value >= 0: 1325 self.process_target.value = self.target 1326 self.wait_event.set() 1327 return result 1328 1329 def RemoveFromBuffer(self): 1330 pos = self.local_tail 1331 if len(self.buffer) - pos < glb_nsz: 1332 pos = 0 1333 n = pickle.loads(self.buffer[pos : pos + glb_nsz]) 1334 if n == 0: 1335 pos = 0 1336 n = pickle.loads(self.buffer[0 : glb_nsz]) 1337 pos += glb_nsz 1338 obj = pickle.loads(self.buffer[pos : pos + n]) 1339 self.local_tail = pos + n 1340 return obj 1341 1342 def ProcessData(self, count): 1343 for i in xrange(count): 1344 obj = self.RemoveFromBuffer() 1345 self.process_data(obj) 1346 self.tail.value = self.local_tail 1347 self.wait_event.set() 1348 self.done.emit(count) 1349 1350# Fetch more records bar 1351 1352class FetchMoreRecordsBar(): 1353 1354 def __init__(self, model, parent): 1355 self.model = model 1356 1357 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:") 1358 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 1359 1360 self.fetch_count = QSpinBox() 1361 self.fetch_count.setRange(1, 1000000) 1362 self.fetch_count.setValue(10) 1363 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 1364 1365 self.fetch = QPushButton("Go!") 1366 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 1367 self.fetch.released.connect(self.FetchMoreRecords) 1368 1369 self.progress = QProgressBar() 1370 self.progress.setRange(0, 100) 1371 self.progress.hide() 1372 1373 self.done_label = QLabel("All records fetched") 1374 self.done_label.hide() 1375 1376 self.spacer = QLabel("") 1377 1378 self.close_button = QToolButton() 1379 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) 1380 self.close_button.released.connect(self.Deactivate) 1381 1382 self.hbox = QHBoxLayout() 1383 self.hbox.setContentsMargins(0, 0, 0, 0) 1384 1385 self.hbox.addWidget(self.label) 1386 self.hbox.addWidget(self.fetch_count) 1387 self.hbox.addWidget(self.fetch) 1388 self.hbox.addWidget(self.spacer) 1389 self.hbox.addWidget(self.progress) 1390 self.hbox.addWidget(self.done_label) 1391 self.hbox.addWidget(self.close_button) 1392 1393 self.bar = QWidget() 1394 self.bar.setLayout(self.hbox); 1395 self.bar.show() 1396 1397 self.in_progress = False 1398 self.model.progress.connect(self.Progress) 1399 1400 self.done = False 1401 1402 if not model.HasMoreRecords(): 1403 self.Done() 1404 1405 def Widget(self): 1406 return self.bar 1407 1408 def Activate(self): 1409 self.bar.show() 1410 self.fetch.setFocus() 1411 1412 def Deactivate(self): 1413 self.bar.hide() 1414 1415 def Enable(self, enable): 1416 self.fetch.setEnabled(enable) 1417 self.fetch_count.setEnabled(enable) 1418 1419 def Busy(self): 1420 self.Enable(False) 1421 self.fetch.hide() 1422 self.spacer.hide() 1423 self.progress.show() 1424 1425 def Idle(self): 1426 self.in_progress = False 1427 self.Enable(True) 1428 self.progress.hide() 1429 self.fetch.show() 1430 self.spacer.show() 1431 1432 def Target(self): 1433 return self.fetch_count.value() * glb_chunk_sz 1434 1435 def Done(self): 1436 self.done = True 1437 self.Idle() 1438 self.label.hide() 1439 self.fetch_count.hide() 1440 self.fetch.hide() 1441 self.spacer.hide() 1442 self.done_label.show() 1443 1444 def Progress(self, count): 1445 if self.in_progress: 1446 if count: 1447 percent = ((count - self.start) * 100) / self.Target() 1448 if percent >= 100: 1449 self.Idle() 1450 else: 1451 self.progress.setValue(percent) 1452 if not count: 1453 # Count value of zero means no more records 1454 self.Done() 1455 1456 def FetchMoreRecords(self): 1457 if self.done: 1458 return 1459 self.progress.setValue(0) 1460 self.Busy() 1461 self.in_progress = True 1462 self.start = self.model.FetchMoreRecords(self.Target()) 1463 1464# Brance data model level two item 1465 1466class BranchLevelTwoItem(): 1467 1468 def __init__(self, row, col, text, parent_item): 1469 self.row = row 1470 self.parent_item = parent_item 1471 self.data = [""] * (col + 1) 1472 self.data[col] = text 1473 self.level = 2 1474 1475 def getParentItem(self): 1476 return self.parent_item 1477 1478 def getRow(self): 1479 return self.row 1480 1481 def childCount(self): 1482 return 0 1483 1484 def hasChildren(self): 1485 return False 1486 1487 def getData(self, column): 1488 return self.data[column] 1489 1490# Brance data model level one item 1491 1492class BranchLevelOneItem(): 1493 1494 def __init__(self, glb, row, data, parent_item): 1495 self.glb = glb 1496 self.row = row 1497 self.parent_item = parent_item 1498 self.child_count = 0 1499 self.child_items = [] 1500 self.data = data[1:] 1501 self.dbid = data[0] 1502 self.level = 1 1503 self.query_done = False 1504 self.br_col = len(self.data) - 1 1505 1506 def getChildItem(self, row): 1507 return self.child_items[row] 1508 1509 def getParentItem(self): 1510 return self.parent_item 1511 1512 def getRow(self): 1513 return self.row 1514 1515 def Select(self): 1516 self.query_done = True 1517 1518 if not self.glb.have_disassembler: 1519 return 1520 1521 query = QSqlQuery(self.glb.db) 1522 1523 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip" 1524 " FROM samples" 1525 " INNER JOIN dsos ON samples.to_dso_id = dsos.id" 1526 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id" 1527 " WHERE samples.id = " + str(self.dbid)) 1528 if not query.next(): 1529 return 1530 cpu = query.value(0) 1531 dso = query.value(1) 1532 sym = query.value(2) 1533 if dso == 0 or sym == 0: 1534 return 1535 off = query.value(3) 1536 short_name = query.value(4) 1537 long_name = query.value(5) 1538 build_id = query.value(6) 1539 sym_start = query.value(7) 1540 ip = query.value(8) 1541 1542 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start" 1543 " FROM samples" 1544 " INNER JOIN symbols ON samples.symbol_id = symbols.id" 1545 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) + 1546 " ORDER BY samples.id" 1547 " LIMIT 1") 1548 if not query.next(): 1549 return 1550 if query.value(0) != dso: 1551 # Cannot disassemble from one dso to another 1552 return 1553 bsym = query.value(1) 1554 boff = query.value(2) 1555 bsym_start = query.value(3) 1556 if bsym == 0: 1557 return 1558 tot = bsym_start + boff + 1 - sym_start - off 1559 if tot <= 0 or tot > 16384: 1560 return 1561 1562 inst = self.glb.disassembler.Instruction() 1563 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id) 1564 if not f: 1565 return 1566 mode = 0 if Is64Bit(f) else 1 1567 self.glb.disassembler.SetMode(inst, mode) 1568 1569 buf_sz = tot + 16 1570 buf = create_string_buffer(tot + 16) 1571 f.seek(sym_start + off) 1572 buf.value = f.read(buf_sz) 1573 buf_ptr = addressof(buf) 1574 i = 0 1575 while tot > 0: 1576 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip) 1577 if cnt: 1578 byte_str = tohex(ip).rjust(16) 1579 for k in xrange(cnt): 1580 byte_str += " %02x" % ord(buf[i]) 1581 i += 1 1582 while k < 15: 1583 byte_str += " " 1584 k += 1 1585 self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self)) 1586 self.child_count += 1 1587 else: 1588 return 1589 buf_ptr += cnt 1590 tot -= cnt 1591 buf_sz -= cnt 1592 ip += cnt 1593 1594 def childCount(self): 1595 if not self.query_done: 1596 self.Select() 1597 if not self.child_count: 1598 return -1 1599 return self.child_count 1600 1601 def hasChildren(self): 1602 if not self.query_done: 1603 return True 1604 return self.child_count > 0 1605 1606 def getData(self, column): 1607 return self.data[column] 1608 1609# Brance data model root item 1610 1611class BranchRootItem(): 1612 1613 def __init__(self): 1614 self.child_count = 0 1615 self.child_items = [] 1616 self.level = 0 1617 1618 def getChildItem(self, row): 1619 return self.child_items[row] 1620 1621 def getParentItem(self): 1622 return None 1623 1624 def getRow(self): 1625 return 0 1626 1627 def childCount(self): 1628 return self.child_count 1629 1630 def hasChildren(self): 1631 return self.child_count > 0 1632 1633 def getData(self, column): 1634 return "" 1635 1636# Calculate instructions per cycle 1637 1638def CalcIPC(cyc_cnt, insn_cnt): 1639 if cyc_cnt and insn_cnt: 1640 ipc = Decimal(float(insn_cnt) / cyc_cnt) 1641 ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP)) 1642 else: 1643 ipc = "0" 1644 return ipc 1645 1646# Branch data preparation 1647 1648def BranchDataPrepBr(query, data): 1649 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) + 1650 " (" + dsoname(query.value(11)) + ")" + " -> " + 1651 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) + 1652 " (" + dsoname(query.value(15)) + ")") 1653 1654def BranchDataPrepIPC(query, data): 1655 insn_cnt = query.value(16) 1656 cyc_cnt = query.value(17) 1657 ipc = CalcIPC(cyc_cnt, insn_cnt) 1658 data.append(insn_cnt) 1659 data.append(cyc_cnt) 1660 data.append(ipc) 1661 1662def BranchDataPrep(query): 1663 data = [] 1664 for i in xrange(0, 8): 1665 data.append(query.value(i)) 1666 BranchDataPrepBr(query, data) 1667 return data 1668 1669def BranchDataPrepWA(query): 1670 data = [] 1671 data.append(query.value(0)) 1672 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string 1673 data.append("{:>19}".format(query.value(1))) 1674 for i in xrange(2, 8): 1675 data.append(query.value(i)) 1676 BranchDataPrepBr(query, data) 1677 return data 1678 1679def BranchDataWithIPCPrep(query): 1680 data = [] 1681 for i in xrange(0, 8): 1682 data.append(query.value(i)) 1683 BranchDataPrepIPC(query, data) 1684 BranchDataPrepBr(query, data) 1685 return data 1686 1687def BranchDataWithIPCPrepWA(query): 1688 data = [] 1689 data.append(query.value(0)) 1690 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string 1691 data.append("{:>19}".format(query.value(1))) 1692 for i in xrange(2, 8): 1693 data.append(query.value(i)) 1694 BranchDataPrepIPC(query, data) 1695 BranchDataPrepBr(query, data) 1696 return data 1697 1698# Branch data model 1699 1700class BranchModel(TreeModel): 1701 1702 progress = Signal(object) 1703 1704 def __init__(self, glb, event_id, where_clause, parent=None): 1705 super(BranchModel, self).__init__(glb, None, parent) 1706 self.event_id = event_id 1707 self.more = True 1708 self.populated = 0 1709 self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count") 1710 if self.have_ipc: 1711 select_ipc = ", insn_count, cyc_count" 1712 prep_fn = BranchDataWithIPCPrep 1713 prep_wa_fn = BranchDataWithIPCPrepWA 1714 else: 1715 select_ipc = "" 1716 prep_fn = BranchDataPrep 1717 prep_wa_fn = BranchDataPrepWA 1718 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name," 1719 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END," 1720 " ip, symbols.name, sym_offset, dsos.short_name," 1721 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name" 1722 + select_ipc + 1723 " FROM samples" 1724 " INNER JOIN comms ON comm_id = comms.id" 1725 " INNER JOIN threads ON thread_id = threads.id" 1726 " INNER JOIN branch_types ON branch_type = branch_types.id" 1727 " INNER JOIN symbols ON symbol_id = symbols.id" 1728 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id" 1729 " INNER JOIN dsos ON samples.dso_id = dsos.id" 1730 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id" 1731 " WHERE samples.id > $$last_id$$" + where_clause + 1732 " AND evsel_id = " + str(self.event_id) + 1733 " ORDER BY samples.id" 1734 " LIMIT " + str(glb_chunk_sz)) 1735 if pyside_version_1 and sys.version_info[0] == 3: 1736 prep = prep_fn 1737 else: 1738 prep = prep_wa_fn 1739 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample) 1740 self.fetcher.done.connect(self.Update) 1741 self.fetcher.Fetch(glb_chunk_sz) 1742 1743 def GetRoot(self): 1744 return BranchRootItem() 1745 1746 def columnCount(self, parent=None): 1747 if self.have_ipc: 1748 return 11 1749 else: 1750 return 8 1751 1752 def columnHeader(self, column): 1753 if self.have_ipc: 1754 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column] 1755 else: 1756 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column] 1757 1758 def columnFont(self, column): 1759 if self.have_ipc: 1760 br_col = 10 1761 else: 1762 br_col = 7 1763 if column != br_col: 1764 return None 1765 return QFont("Monospace") 1766 1767 def DisplayData(self, item, index): 1768 if item.level == 1: 1769 self.FetchIfNeeded(item.row) 1770 return item.getData(index.column()) 1771 1772 def AddSample(self, data): 1773 child = BranchLevelOneItem(self.glb, self.populated, data, self.root) 1774 self.root.child_items.append(child) 1775 self.populated += 1 1776 1777 def Update(self, fetched): 1778 if not fetched: 1779 self.more = False 1780 self.progress.emit(0) 1781 child_count = self.root.child_count 1782 count = self.populated - child_count 1783 if count > 0: 1784 parent = QModelIndex() 1785 self.beginInsertRows(parent, child_count, child_count + count - 1) 1786 self.insertRows(child_count, count, parent) 1787 self.root.child_count += count 1788 self.endInsertRows() 1789 self.progress.emit(self.root.child_count) 1790 1791 def FetchMoreRecords(self, count): 1792 current = self.root.child_count 1793 if self.more: 1794 self.fetcher.Fetch(count) 1795 else: 1796 self.progress.emit(0) 1797 return current 1798 1799 def HasMoreRecords(self): 1800 return self.more 1801 1802# Report Variables 1803 1804class ReportVars(): 1805 1806 def __init__(self, name = "", where_clause = "", limit = ""): 1807 self.name = name 1808 self.where_clause = where_clause 1809 self.limit = limit 1810 1811 def UniqueId(self): 1812 return str(self.where_clause + ";" + self.limit) 1813 1814# Branch window 1815 1816class BranchWindow(QMdiSubWindow): 1817 1818 def __init__(self, glb, event_id, report_vars, parent=None): 1819 super(BranchWindow, self).__init__(parent) 1820 1821 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId() 1822 1823 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause)) 1824 1825 self.view = QTreeView() 1826 self.view.setUniformRowHeights(True) 1827 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) 1828 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard 1829 self.view.setModel(self.model) 1830 1831 self.ResizeColumnsToContents() 1832 1833 self.context_menu = TreeContextMenu(self.view) 1834 1835 self.find_bar = FindBar(self, self, True) 1836 1837 self.finder = ChildDataItemFinder(self.model.root) 1838 1839 self.fetch_bar = FetchMoreRecordsBar(self.model, self) 1840 1841 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) 1842 1843 self.setWidget(self.vbox.Widget()) 1844 1845 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events") 1846 1847 def ResizeColumnToContents(self, column, n): 1848 # Using the view's resizeColumnToContents() here is extrememly slow 1849 # so implement a crude alternative 1850 mm = "MM" if column else "MMMM" 1851 font = self.view.font() 1852 metrics = QFontMetrics(font) 1853 max = 0 1854 for row in xrange(n): 1855 val = self.model.root.child_items[row].data[column] 1856 len = metrics.width(str(val) + mm) 1857 max = len if len > max else max 1858 val = self.model.columnHeader(column) 1859 len = metrics.width(str(val) + mm) 1860 max = len if len > max else max 1861 self.view.setColumnWidth(column, max) 1862 1863 def ResizeColumnsToContents(self): 1864 n = min(self.model.root.child_count, 100) 1865 if n < 1: 1866 # No data yet, so connect a signal to notify when there is 1867 self.model.rowsInserted.connect(self.UpdateColumnWidths) 1868 return 1869 columns = self.model.columnCount() 1870 for i in xrange(columns): 1871 self.ResizeColumnToContents(i, n) 1872 1873 def UpdateColumnWidths(self, *x): 1874 # This only needs to be done once, so disconnect the signal now 1875 self.model.rowsInserted.disconnect(self.UpdateColumnWidths) 1876 self.ResizeColumnsToContents() 1877 1878 def Find(self, value, direction, pattern, context): 1879 self.view.setFocus() 1880 self.find_bar.Busy() 1881 self.finder.Find(value, direction, pattern, context, self.FindDone) 1882 1883 def FindDone(self, row): 1884 self.find_bar.Idle() 1885 if row >= 0: 1886 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) 1887 else: 1888 self.find_bar.NotFound() 1889 1890# Line edit data item 1891 1892class LineEditDataItem(object): 1893 1894 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""): 1895 self.glb = glb 1896 self.label = label 1897 self.placeholder_text = placeholder_text 1898 self.parent = parent 1899 self.id = id 1900 1901 self.value = default 1902 1903 self.widget = QLineEdit(default) 1904 self.widget.editingFinished.connect(self.Validate) 1905 self.widget.textChanged.connect(self.Invalidate) 1906 self.red = False 1907 self.error = "" 1908 self.validated = True 1909 1910 if placeholder_text: 1911 self.widget.setPlaceholderText(placeholder_text) 1912 1913 def TurnTextRed(self): 1914 if not self.red: 1915 palette = QPalette() 1916 palette.setColor(QPalette.Text,Qt.red) 1917 self.widget.setPalette(palette) 1918 self.red = True 1919 1920 def TurnTextNormal(self): 1921 if self.red: 1922 palette = QPalette() 1923 self.widget.setPalette(palette) 1924 self.red = False 1925 1926 def InvalidValue(self, value): 1927 self.value = "" 1928 self.TurnTextRed() 1929 self.error = self.label + " invalid value '" + value + "'" 1930 self.parent.ShowMessage(self.error) 1931 1932 def Invalidate(self): 1933 self.validated = False 1934 1935 def DoValidate(self, input_string): 1936 self.value = input_string.strip() 1937 1938 def Validate(self): 1939 self.validated = True 1940 self.error = "" 1941 self.TurnTextNormal() 1942 self.parent.ClearMessage() 1943 input_string = self.widget.text() 1944 if not len(input_string.strip()): 1945 self.value = "" 1946 return 1947 self.DoValidate(input_string) 1948 1949 def IsValid(self): 1950 if not self.validated: 1951 self.Validate() 1952 if len(self.error): 1953 self.parent.ShowMessage(self.error) 1954 return False 1955 return True 1956 1957 def IsNumber(self, value): 1958 try: 1959 x = int(value) 1960 except: 1961 x = 0 1962 return str(x) == value 1963 1964# Non-negative integer ranges dialog data item 1965 1966class NonNegativeIntegerRangesDataItem(LineEditDataItem): 1967 1968 def __init__(self, glb, label, placeholder_text, column_name, parent): 1969 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent) 1970 1971 self.column_name = column_name 1972 1973 def DoValidate(self, input_string): 1974 singles = [] 1975 ranges = [] 1976 for value in [x.strip() for x in input_string.split(",")]: 1977 if "-" in value: 1978 vrange = value.split("-") 1979 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): 1980 return self.InvalidValue(value) 1981 ranges.append(vrange) 1982 else: 1983 if not self.IsNumber(value): 1984 return self.InvalidValue(value) 1985 singles.append(value) 1986 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges] 1987 if len(singles): 1988 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")") 1989 self.value = " OR ".join(ranges) 1990 1991# Positive integer dialog data item 1992 1993class PositiveIntegerDataItem(LineEditDataItem): 1994 1995 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""): 1996 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default) 1997 1998 def DoValidate(self, input_string): 1999 if not self.IsNumber(input_string.strip()): 2000 return self.InvalidValue(input_string) 2001 value = int(input_string.strip()) 2002 if value <= 0: 2003 return self.InvalidValue(input_string) 2004 self.value = str(value) 2005 2006# Dialog data item converted and validated using a SQL table 2007 2008class SQLTableDataItem(LineEditDataItem): 2009 2010 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent): 2011 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent) 2012 2013 self.table_name = table_name 2014 self.match_column = match_column 2015 self.column_name1 = column_name1 2016 self.column_name2 = column_name2 2017 2018 def ValueToIds(self, value): 2019 ids = [] 2020 query = QSqlQuery(self.glb.db) 2021 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'" 2022 ret = query.exec_(stmt) 2023 if ret: 2024 while query.next(): 2025 ids.append(str(query.value(0))) 2026 return ids 2027 2028 def DoValidate(self, input_string): 2029 all_ids = [] 2030 for value in [x.strip() for x in input_string.split(",")]: 2031 ids = self.ValueToIds(value) 2032 if len(ids): 2033 all_ids.extend(ids) 2034 else: 2035 return self.InvalidValue(value) 2036 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")" 2037 if self.column_name2: 2038 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )" 2039 2040# Sample time ranges dialog data item converted and validated using 'samples' SQL table 2041 2042class SampleTimeRangesDataItem(LineEditDataItem): 2043 2044 def __init__(self, glb, label, placeholder_text, column_name, parent): 2045 self.column_name = column_name 2046 2047 self.last_id = 0 2048 self.first_time = 0 2049 self.last_time = 2 ** 64 2050 2051 query = QSqlQuery(glb.db) 2052 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1") 2053 if query.next(): 2054 self.last_id = int(query.value(0)) 2055 self.last_time = int(query.value(1)) 2056 QueryExec(query, "SELECT time FROM samples WHERE time != 0 ORDER BY id LIMIT 1") 2057 if query.next(): 2058 self.first_time = int(query.value(0)) 2059 if placeholder_text: 2060 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time) 2061 2062 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent) 2063 2064 def IdBetween(self, query, lower_id, higher_id, order): 2065 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1") 2066 if query.next(): 2067 return True, int(query.value(0)) 2068 else: 2069 return False, 0 2070 2071 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor): 2072 query = QSqlQuery(self.glb.db) 2073 while True: 2074 next_id = int((lower_id + higher_id) / 2) 2075 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) 2076 if not query.next(): 2077 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC") 2078 if not ok: 2079 ok, dbid = self.IdBetween(query, next_id, higher_id, "") 2080 if not ok: 2081 return str(higher_id) 2082 next_id = dbid 2083 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) 2084 next_time = int(query.value(0)) 2085 if get_floor: 2086 if target_time > next_time: 2087 lower_id = next_id 2088 else: 2089 higher_id = next_id 2090 if higher_id <= lower_id + 1: 2091 return str(higher_id) 2092 else: 2093 if target_time >= next_time: 2094 lower_id = next_id 2095 else: 2096 higher_id = next_id 2097 if higher_id <= lower_id + 1: 2098 return str(lower_id) 2099 2100 def ConvertRelativeTime(self, val): 2101 mult = 1 2102 suffix = val[-2:] 2103 if suffix == "ms": 2104 mult = 1000000 2105 elif suffix == "us": 2106 mult = 1000 2107 elif suffix == "ns": 2108 mult = 1 2109 else: 2110 return val 2111 val = val[:-2].strip() 2112 if not self.IsNumber(val): 2113 return val 2114 val = int(val) * mult 2115 if val >= 0: 2116 val += self.first_time 2117 else: 2118 val += self.last_time 2119 return str(val) 2120 2121 def ConvertTimeRange(self, vrange): 2122 if vrange[0] == "": 2123 vrange[0] = str(self.first_time) 2124 if vrange[1] == "": 2125 vrange[1] = str(self.last_time) 2126 vrange[0] = self.ConvertRelativeTime(vrange[0]) 2127 vrange[1] = self.ConvertRelativeTime(vrange[1]) 2128 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): 2129 return False 2130 beg_range = max(int(vrange[0]), self.first_time) 2131 end_range = min(int(vrange[1]), self.last_time) 2132 if beg_range > self.last_time or end_range < self.first_time: 2133 return False 2134 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True) 2135 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False) 2136 return True 2137 2138 def AddTimeRange(self, value, ranges): 2139 n = value.count("-") 2140 if n == 1: 2141 pass 2142 elif n == 2: 2143 if value.split("-")[1].strip() == "": 2144 n = 1 2145 elif n == 3: 2146 n = 2 2147 else: 2148 return False 2149 pos = findnth(value, "-", n) 2150 vrange = [value[:pos].strip() ,value[pos+1:].strip()] 2151 if self.ConvertTimeRange(vrange): 2152 ranges.append(vrange) 2153 return True 2154 return False 2155 2156 def DoValidate(self, input_string): 2157 ranges = [] 2158 for value in [x.strip() for x in input_string.split(",")]: 2159 if not self.AddTimeRange(value, ranges): 2160 return self.InvalidValue(value) 2161 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges] 2162 self.value = " OR ".join(ranges) 2163 2164# Report Dialog Base 2165 2166class ReportDialogBase(QDialog): 2167 2168 def __init__(self, glb, title, items, partial, parent=None): 2169 super(ReportDialogBase, self).__init__(parent) 2170 2171 self.glb = glb 2172 2173 self.report_vars = ReportVars() 2174 2175 self.setWindowTitle(title) 2176 self.setMinimumWidth(600) 2177 2178 self.data_items = [x(glb, self) for x in items] 2179 2180 self.partial = partial 2181 2182 self.grid = QGridLayout() 2183 2184 for row in xrange(len(self.data_items)): 2185 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0) 2186 self.grid.addWidget(self.data_items[row].widget, row, 1) 2187 2188 self.status = QLabel() 2189 2190 self.ok_button = QPushButton("Ok", self) 2191 self.ok_button.setDefault(True) 2192 self.ok_button.released.connect(self.Ok) 2193 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 2194 2195 self.cancel_button = QPushButton("Cancel", self) 2196 self.cancel_button.released.connect(self.reject) 2197 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 2198 2199 self.hbox = QHBoxLayout() 2200 #self.hbox.addStretch() 2201 self.hbox.addWidget(self.status) 2202 self.hbox.addWidget(self.ok_button) 2203 self.hbox.addWidget(self.cancel_button) 2204 2205 self.vbox = QVBoxLayout() 2206 self.vbox.addLayout(self.grid) 2207 self.vbox.addLayout(self.hbox) 2208 2209 self.setLayout(self.vbox); 2210 2211 def Ok(self): 2212 vars = self.report_vars 2213 for d in self.data_items: 2214 if d.id == "REPORTNAME": 2215 vars.name = d.value 2216 if not vars.name: 2217 self.ShowMessage("Report name is required") 2218 return 2219 for d in self.data_items: 2220 if not d.IsValid(): 2221 return 2222 for d in self.data_items[1:]: 2223 if d.id == "LIMIT": 2224 vars.limit = d.value 2225 elif len(d.value): 2226 if len(vars.where_clause): 2227 vars.where_clause += " AND " 2228 vars.where_clause += d.value 2229 if len(vars.where_clause): 2230 if self.partial: 2231 vars.where_clause = " AND ( " + vars.where_clause + " ) " 2232 else: 2233 vars.where_clause = " WHERE " + vars.where_clause + " " 2234 self.accept() 2235 2236 def ShowMessage(self, msg): 2237 self.status.setText("<font color=#FF0000>" + msg) 2238 2239 def ClearMessage(self): 2240 self.status.setText("") 2241 2242# Selected branch report creation dialog 2243 2244class SelectedBranchDialog(ReportDialogBase): 2245 2246 def __init__(self, glb, parent=None): 2247 title = "Selected Branches" 2248 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), 2249 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p), 2250 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p), 2251 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p), 2252 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p), 2253 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p), 2254 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p), 2255 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p), 2256 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p)) 2257 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent) 2258 2259# Event list 2260 2261def GetEventList(db): 2262 events = [] 2263 query = QSqlQuery(db) 2264 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id") 2265 while query.next(): 2266 events.append(query.value(0)) 2267 return events 2268 2269# Is a table selectable 2270 2271def IsSelectable(db, table, sql = "", columns = "*"): 2272 query = QSqlQuery(db) 2273 try: 2274 QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1") 2275 except: 2276 return False 2277 return True 2278 2279# SQL table data model item 2280 2281class SQLTableItem(): 2282 2283 def __init__(self, row, data): 2284 self.row = row 2285 self.data = data 2286 2287 def getData(self, column): 2288 return self.data[column] 2289 2290# SQL table data model 2291 2292class SQLTableModel(TableModel): 2293 2294 progress = Signal(object) 2295 2296 def __init__(self, glb, sql, column_headers, parent=None): 2297 super(SQLTableModel, self).__init__(parent) 2298 self.glb = glb 2299 self.more = True 2300 self.populated = 0 2301 self.column_headers = column_headers 2302 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample) 2303 self.fetcher.done.connect(self.Update) 2304 self.fetcher.Fetch(glb_chunk_sz) 2305 2306 def DisplayData(self, item, index): 2307 self.FetchIfNeeded(item.row) 2308 return item.getData(index.column()) 2309 2310 def AddSample(self, data): 2311 child = SQLTableItem(self.populated, data) 2312 self.child_items.append(child) 2313 self.populated += 1 2314 2315 def Update(self, fetched): 2316 if not fetched: 2317 self.more = False 2318 self.progress.emit(0) 2319 child_count = self.child_count 2320 count = self.populated - child_count 2321 if count > 0: 2322 parent = QModelIndex() 2323 self.beginInsertRows(parent, child_count, child_count + count - 1) 2324 self.insertRows(child_count, count, parent) 2325 self.child_count += count 2326 self.endInsertRows() 2327 self.progress.emit(self.child_count) 2328 2329 def FetchMoreRecords(self, count): 2330 current = self.child_count 2331 if self.more: 2332 self.fetcher.Fetch(count) 2333 else: 2334 self.progress.emit(0) 2335 return current 2336 2337 def HasMoreRecords(self): 2338 return self.more 2339 2340 def columnCount(self, parent=None): 2341 return len(self.column_headers) 2342 2343 def columnHeader(self, column): 2344 return self.column_headers[column] 2345 2346 def SQLTableDataPrep(self, query, count): 2347 data = [] 2348 for i in xrange(count): 2349 data.append(query.value(i)) 2350 return data 2351 2352# SQL automatic table data model 2353 2354class SQLAutoTableModel(SQLTableModel): 2355 2356 def __init__(self, glb, table_name, parent=None): 2357 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz) 2358 if table_name == "comm_threads_view": 2359 # For now, comm_threads_view has no id column 2360 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz) 2361 column_headers = [] 2362 query = QSqlQuery(glb.db) 2363 if glb.dbref.is_sqlite3: 2364 QueryExec(query, "PRAGMA table_info(" + table_name + ")") 2365 while query.next(): 2366 column_headers.append(query.value(1)) 2367 if table_name == "sqlite_master": 2368 sql = "SELECT * FROM " + table_name 2369 else: 2370 if table_name[:19] == "information_schema.": 2371 sql = "SELECT * FROM " + table_name 2372 select_table_name = table_name[19:] 2373 schema = "information_schema" 2374 else: 2375 select_table_name = table_name 2376 schema = "public" 2377 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'") 2378 while query.next(): 2379 column_headers.append(query.value(0)) 2380 if pyside_version_1 and sys.version_info[0] == 3: 2381 if table_name == "samples_view": 2382 self.SQLTableDataPrep = self.samples_view_DataPrep 2383 if table_name == "samples": 2384 self.SQLTableDataPrep = self.samples_DataPrep 2385 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent) 2386 2387 def samples_view_DataPrep(self, query, count): 2388 data = [] 2389 data.append(query.value(0)) 2390 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string 2391 data.append("{:>19}".format(query.value(1))) 2392 for i in xrange(2, count): 2393 data.append(query.value(i)) 2394 return data 2395 2396 def samples_DataPrep(self, query, count): 2397 data = [] 2398 for i in xrange(9): 2399 data.append(query.value(i)) 2400 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string 2401 data.append("{:>19}".format(query.value(9))) 2402 for i in xrange(10, count): 2403 data.append(query.value(i)) 2404 return data 2405 2406# Base class for custom ResizeColumnsToContents 2407 2408class ResizeColumnsToContentsBase(QObject): 2409 2410 def __init__(self, parent=None): 2411 super(ResizeColumnsToContentsBase, self).__init__(parent) 2412 2413 def ResizeColumnToContents(self, column, n): 2414 # Using the view's resizeColumnToContents() here is extrememly slow 2415 # so implement a crude alternative 2416 font = self.view.font() 2417 metrics = QFontMetrics(font) 2418 max = 0 2419 for row in xrange(n): 2420 val = self.data_model.child_items[row].data[column] 2421 len = metrics.width(str(val) + "MM") 2422 max = len if len > max else max 2423 val = self.data_model.columnHeader(column) 2424 len = metrics.width(str(val) + "MM") 2425 max = len if len > max else max 2426 self.view.setColumnWidth(column, max) 2427 2428 def ResizeColumnsToContents(self): 2429 n = min(self.data_model.child_count, 100) 2430 if n < 1: 2431 # No data yet, so connect a signal to notify when there is 2432 self.data_model.rowsInserted.connect(self.UpdateColumnWidths) 2433 return 2434 columns = self.data_model.columnCount() 2435 for i in xrange(columns): 2436 self.ResizeColumnToContents(i, n) 2437 2438 def UpdateColumnWidths(self, *x): 2439 # This only needs to be done once, so disconnect the signal now 2440 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths) 2441 self.ResizeColumnsToContents() 2442 2443# Convert value to CSV 2444 2445def ToCSValue(val): 2446 if '"' in val: 2447 val = val.replace('"', '""') 2448 if "," in val or '"' in val: 2449 val = '"' + val + '"' 2450 return val 2451 2452# Key to sort table model indexes by row / column, assuming fewer than 1000 columns 2453 2454glb_max_cols = 1000 2455 2456def RowColumnKey(a): 2457 return a.row() * glb_max_cols + a.column() 2458 2459# Copy selected table cells to clipboard 2460 2461def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False): 2462 indexes = sorted(view.selectedIndexes(), key=RowColumnKey) 2463 idx_cnt = len(indexes) 2464 if not idx_cnt: 2465 return 2466 if idx_cnt == 1: 2467 with_hdr=False 2468 min_row = indexes[0].row() 2469 max_row = indexes[0].row() 2470 min_col = indexes[0].column() 2471 max_col = indexes[0].column() 2472 for i in indexes: 2473 min_row = min(min_row, i.row()) 2474 max_row = max(max_row, i.row()) 2475 min_col = min(min_col, i.column()) 2476 max_col = max(max_col, i.column()) 2477 if max_col > glb_max_cols: 2478 raise RuntimeError("glb_max_cols is too low") 2479 max_width = [0] * (1 + max_col - min_col) 2480 for i in indexes: 2481 c = i.column() - min_col 2482 max_width[c] = max(max_width[c], len(str(i.data()))) 2483 text = "" 2484 pad = "" 2485 sep = "" 2486 if with_hdr: 2487 model = indexes[0].model() 2488 for col in range(min_col, max_col + 1): 2489 val = model.headerData(col, Qt.Horizontal) 2490 if as_csv: 2491 text += sep + ToCSValue(val) 2492 sep = "," 2493 else: 2494 c = col - min_col 2495 max_width[c] = max(max_width[c], len(val)) 2496 width = max_width[c] 2497 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole) 2498 if align & Qt.AlignRight: 2499 val = val.rjust(width) 2500 text += pad + sep + val 2501 pad = " " * (width - len(val)) 2502 sep = " " 2503 text += "\n" 2504 pad = "" 2505 sep = "" 2506 last_row = min_row 2507 for i in indexes: 2508 if i.row() > last_row: 2509 last_row = i.row() 2510 text += "\n" 2511 pad = "" 2512 sep = "" 2513 if as_csv: 2514 text += sep + ToCSValue(str(i.data())) 2515 sep = "," 2516 else: 2517 width = max_width[i.column() - min_col] 2518 if i.data(Qt.TextAlignmentRole) & Qt.AlignRight: 2519 val = str(i.data()).rjust(width) 2520 else: 2521 val = str(i.data()) 2522 text += pad + sep + val 2523 pad = " " * (width - len(val)) 2524 sep = " " 2525 QApplication.clipboard().setText(text) 2526 2527def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False): 2528 indexes = view.selectedIndexes() 2529 if not len(indexes): 2530 return 2531 2532 selection = view.selectionModel() 2533 2534 first = None 2535 for i in indexes: 2536 above = view.indexAbove(i) 2537 if not selection.isSelected(above): 2538 first = i 2539 break 2540 2541 if first is None: 2542 raise RuntimeError("CopyTreeCellsToClipboard internal error") 2543 2544 model = first.model() 2545 row_cnt = 0 2546 col_cnt = model.columnCount(first) 2547 max_width = [0] * col_cnt 2548 2549 indent_sz = 2 2550 indent_str = " " * indent_sz 2551 2552 expanded_mark_sz = 2 2553 if sys.version_info[0] == 3: 2554 expanded_mark = "\u25BC " 2555 not_expanded_mark = "\u25B6 " 2556 else: 2557 expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8") 2558 not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8") 2559 leaf_mark = " " 2560 2561 if not as_csv: 2562 pos = first 2563 while True: 2564 row_cnt += 1 2565 row = pos.row() 2566 for c in range(col_cnt): 2567 i = pos.sibling(row, c) 2568 if c: 2569 n = len(str(i.data())) 2570 else: 2571 n = len(str(i.data()).strip()) 2572 n += (i.internalPointer().level - 1) * indent_sz 2573 n += expanded_mark_sz 2574 max_width[c] = max(max_width[c], n) 2575 pos = view.indexBelow(pos) 2576 if not selection.isSelected(pos): 2577 break 2578 2579 text = "" 2580 pad = "" 2581 sep = "" 2582 if with_hdr: 2583 for c in range(col_cnt): 2584 val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip() 2585 if as_csv: 2586 text += sep + ToCSValue(val) 2587 sep = "," 2588 else: 2589 max_width[c] = max(max_width[c], len(val)) 2590 width = max_width[c] 2591 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole) 2592 if align & Qt.AlignRight: 2593 val = val.rjust(width) 2594 text += pad + sep + val 2595 pad = " " * (width - len(val)) 2596 sep = " " 2597 text += "\n" 2598 pad = "" 2599 sep = "" 2600 2601 pos = first 2602 while True: 2603 row = pos.row() 2604 for c in range(col_cnt): 2605 i = pos.sibling(row, c) 2606 val = str(i.data()) 2607 if not c: 2608 if model.hasChildren(i): 2609 if view.isExpanded(i): 2610 mark = expanded_mark 2611 else: 2612 mark = not_expanded_mark 2613 else: 2614 mark = leaf_mark 2615 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip() 2616 if as_csv: 2617 text += sep + ToCSValue(val) 2618 sep = "," 2619 else: 2620 width = max_width[c] 2621 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight: 2622 val = val.rjust(width) 2623 text += pad + sep + val 2624 pad = " " * (width - len(val)) 2625 sep = " " 2626 pos = view.indexBelow(pos) 2627 if not selection.isSelected(pos): 2628 break 2629 text = text.rstrip() + "\n" 2630 pad = "" 2631 sep = "" 2632 2633 QApplication.clipboard().setText(text) 2634 2635def CopyCellsToClipboard(view, as_csv=False, with_hdr=False): 2636 view.CopyCellsToClipboard(view, as_csv, with_hdr) 2637 2638def CopyCellsToClipboardHdr(view): 2639 CopyCellsToClipboard(view, False, True) 2640 2641def CopyCellsToClipboardCSV(view): 2642 CopyCellsToClipboard(view, True, True) 2643 2644# Context menu 2645 2646class ContextMenu(object): 2647 2648 def __init__(self, view): 2649 self.view = view 2650 self.view.setContextMenuPolicy(Qt.CustomContextMenu) 2651 self.view.customContextMenuRequested.connect(self.ShowContextMenu) 2652 2653 def ShowContextMenu(self, pos): 2654 menu = QMenu(self.view) 2655 self.AddActions(menu) 2656 menu.exec_(self.view.mapToGlobal(pos)) 2657 2658 def AddCopy(self, menu): 2659 menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view)) 2660 menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view)) 2661 2662 def AddActions(self, menu): 2663 self.AddCopy(menu) 2664 2665class TreeContextMenu(ContextMenu): 2666 2667 def __init__(self, view): 2668 super(TreeContextMenu, self).__init__(view) 2669 2670 def AddActions(self, menu): 2671 i = self.view.currentIndex() 2672 text = str(i.data()).strip() 2673 if len(text): 2674 menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view)) 2675 self.AddCopy(menu) 2676 2677# Table window 2678 2679class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase): 2680 2681 def __init__(self, glb, table_name, parent=None): 2682 super(TableWindow, self).__init__(parent) 2683 2684 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name)) 2685 2686 self.model = QSortFilterProxyModel() 2687 self.model.setSourceModel(self.data_model) 2688 2689 self.view = QTableView() 2690 self.view.setModel(self.model) 2691 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) 2692 self.view.verticalHeader().setVisible(False) 2693 self.view.sortByColumn(-1, Qt.AscendingOrder) 2694 self.view.setSortingEnabled(True) 2695 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) 2696 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard 2697 2698 self.ResizeColumnsToContents() 2699 2700 self.context_menu = ContextMenu(self.view) 2701 2702 self.find_bar = FindBar(self, self, True) 2703 2704 self.finder = ChildDataItemFinder(self.data_model) 2705 2706 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) 2707 2708 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) 2709 2710 self.setWidget(self.vbox.Widget()) 2711 2712 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table") 2713 2714 def Find(self, value, direction, pattern, context): 2715 self.view.setFocus() 2716 self.find_bar.Busy() 2717 self.finder.Find(value, direction, pattern, context, self.FindDone) 2718 2719 def FindDone(self, row): 2720 self.find_bar.Idle() 2721 if row >= 0: 2722 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex()))) 2723 else: 2724 self.find_bar.NotFound() 2725 2726# Table list 2727 2728def GetTableList(glb): 2729 tables = [] 2730 query = QSqlQuery(glb.db) 2731 if glb.dbref.is_sqlite3: 2732 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name") 2733 else: 2734 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name") 2735 while query.next(): 2736 tables.append(query.value(0)) 2737 if glb.dbref.is_sqlite3: 2738 tables.append("sqlite_master") 2739 else: 2740 tables.append("information_schema.tables") 2741 tables.append("information_schema.views") 2742 tables.append("information_schema.columns") 2743 return tables 2744 2745# Top Calls data model 2746 2747class TopCallsModel(SQLTableModel): 2748 2749 def __init__(self, glb, report_vars, parent=None): 2750 text = "" 2751 if not glb.dbref.is_sqlite3: 2752 text = "::text" 2753 limit = "" 2754 if len(report_vars.limit): 2755 limit = " LIMIT " + report_vars.limit 2756 sql = ("SELECT comm, pid, tid, name," 2757 " CASE" 2758 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text + 2759 " ELSE short_name" 2760 " END AS dso," 2761 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, " 2762 " CASE" 2763 " WHEN (calls.flags = 1) THEN 'no call'" + text + 2764 " WHEN (calls.flags = 2) THEN 'no return'" + text + 2765 " WHEN (calls.flags = 3) THEN 'no call/return'" + text + 2766 " ELSE ''" + text + 2767 " END AS flags" 2768 " FROM calls" 2769 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 2770 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 2771 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 2772 " INNER JOIN comms ON calls.comm_id = comms.id" 2773 " INNER JOIN threads ON calls.thread_id = threads.id" + 2774 report_vars.where_clause + 2775 " ORDER BY elapsed_time DESC" + 2776 limit 2777 ) 2778 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags") 2779 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft) 2780 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent) 2781 2782 def columnAlignment(self, column): 2783 return self.alignment[column] 2784 2785# Top Calls report creation dialog 2786 2787class TopCallsDialog(ReportDialogBase): 2788 2789 def __init__(self, glb, parent=None): 2790 title = "Top Calls by Elapsed Time" 2791 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), 2792 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p), 2793 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p), 2794 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p), 2795 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p), 2796 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p), 2797 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p), 2798 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100")) 2799 super(TopCallsDialog, self).__init__(glb, title, items, False, parent) 2800 2801# Top Calls window 2802 2803class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase): 2804 2805 def __init__(self, glb, report_vars, parent=None): 2806 super(TopCallsWindow, self).__init__(parent) 2807 2808 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars)) 2809 self.model = self.data_model 2810 2811 self.view = QTableView() 2812 self.view.setModel(self.model) 2813 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) 2814 self.view.verticalHeader().setVisible(False) 2815 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) 2816 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard 2817 2818 self.context_menu = ContextMenu(self.view) 2819 2820 self.ResizeColumnsToContents() 2821 2822 self.find_bar = FindBar(self, self, True) 2823 2824 self.finder = ChildDataItemFinder(self.model) 2825 2826 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) 2827 2828 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) 2829 2830 self.setWidget(self.vbox.Widget()) 2831 2832 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name) 2833 2834 def Find(self, value, direction, pattern, context): 2835 self.view.setFocus() 2836 self.find_bar.Busy() 2837 self.finder.Find(value, direction, pattern, context, self.FindDone) 2838 2839 def FindDone(self, row): 2840 self.find_bar.Idle() 2841 if row >= 0: 2842 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) 2843 else: 2844 self.find_bar.NotFound() 2845 2846# Action Definition 2847 2848def CreateAction(label, tip, callback, parent=None, shortcut=None): 2849 action = QAction(label, parent) 2850 if shortcut != None: 2851 action.setShortcuts(shortcut) 2852 action.setStatusTip(tip) 2853 action.triggered.connect(callback) 2854 return action 2855 2856# Typical application actions 2857 2858def CreateExitAction(app, parent=None): 2859 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit) 2860 2861# Typical MDI actions 2862 2863def CreateCloseActiveWindowAction(mdi_area): 2864 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area) 2865 2866def CreateCloseAllWindowsAction(mdi_area): 2867 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area) 2868 2869def CreateTileWindowsAction(mdi_area): 2870 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area) 2871 2872def CreateCascadeWindowsAction(mdi_area): 2873 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area) 2874 2875def CreateNextWindowAction(mdi_area): 2876 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild) 2877 2878def CreatePreviousWindowAction(mdi_area): 2879 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild) 2880 2881# Typical MDI window menu 2882 2883class WindowMenu(): 2884 2885 def __init__(self, mdi_area, menu): 2886 self.mdi_area = mdi_area 2887 self.window_menu = menu.addMenu("&Windows") 2888 self.close_active_window = CreateCloseActiveWindowAction(mdi_area) 2889 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area) 2890 self.tile_windows = CreateTileWindowsAction(mdi_area) 2891 self.cascade_windows = CreateCascadeWindowsAction(mdi_area) 2892 self.next_window = CreateNextWindowAction(mdi_area) 2893 self.previous_window = CreatePreviousWindowAction(mdi_area) 2894 self.window_menu.aboutToShow.connect(self.Update) 2895 2896 def Update(self): 2897 self.window_menu.clear() 2898 sub_window_count = len(self.mdi_area.subWindowList()) 2899 have_sub_windows = sub_window_count != 0 2900 self.close_active_window.setEnabled(have_sub_windows) 2901 self.close_all_windows.setEnabled(have_sub_windows) 2902 self.tile_windows.setEnabled(have_sub_windows) 2903 self.cascade_windows.setEnabled(have_sub_windows) 2904 self.next_window.setEnabled(have_sub_windows) 2905 self.previous_window.setEnabled(have_sub_windows) 2906 self.window_menu.addAction(self.close_active_window) 2907 self.window_menu.addAction(self.close_all_windows) 2908 self.window_menu.addSeparator() 2909 self.window_menu.addAction(self.tile_windows) 2910 self.window_menu.addAction(self.cascade_windows) 2911 self.window_menu.addSeparator() 2912 self.window_menu.addAction(self.next_window) 2913 self.window_menu.addAction(self.previous_window) 2914 if sub_window_count == 0: 2915 return 2916 self.window_menu.addSeparator() 2917 nr = 1 2918 for sub_window in self.mdi_area.subWindowList(): 2919 label = str(nr) + " " + sub_window.name 2920 if nr < 10: 2921 label = "&" + label 2922 action = self.window_menu.addAction(label) 2923 action.setCheckable(True) 2924 action.setChecked(sub_window == self.mdi_area.activeSubWindow()) 2925 action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x)) 2926 self.window_menu.addAction(action) 2927 nr += 1 2928 2929 def setActiveSubWindow(self, nr): 2930 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1]) 2931 2932# Help text 2933 2934glb_help_text = """ 2935<h1>Contents</h1> 2936<style> 2937p.c1 { 2938 text-indent: 40px; 2939} 2940p.c2 { 2941 text-indent: 80px; 2942} 2943} 2944</style> 2945<p class=c1><a href=#reports>1. Reports</a></p> 2946<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p> 2947<p class=c2><a href=#calltree>1.2 Call Tree</a></p> 2948<p class=c2><a href=#allbranches>1.3 All branches</a></p> 2949<p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p> 2950<p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p> 2951<p class=c1><a href=#tables>2. Tables</a></p> 2952<h1 id=reports>1. Reports</h1> 2953<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2> 2954The result is a GUI window with a tree representing a context-sensitive 2955call-graph. Expanding a couple of levels of the tree and adjusting column 2956widths to suit will display something like: 2957<pre> 2958 Call Graph: pt_example 2959Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%) 2960v- ls 2961 v- 2638:2638 2962 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0 2963 |- unknown unknown 1 13198 0.1 1 0.0 2964 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3 2965 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3 2966 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4 2967 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1 2968 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0 2969 >- __libc_csu_init ls 1 10354 0.1 10 0.0 2970 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0 2971 v- main ls 1 8182043 99.6 180254 99.9 2972</pre> 2973<h3>Points to note:</h3> 2974<ul> 2975<li>The top level is a command name (comm)</li> 2976<li>The next level is a thread (pid:tid)</li> 2977<li>Subsequent levels are functions</li> 2978<li>'Count' is the number of calls</li> 2979<li>'Time' is the elapsed time until the function returns</li> 2980<li>Percentages are relative to the level above</li> 2981<li>'Branch Count' is the total number of branches for that function and all functions that it calls 2982</ul> 2983<h3>Find</h3> 2984Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match. 2985The pattern matching symbols are ? for any character and * for zero or more characters. 2986<h2 id=calltree>1.2 Call Tree</h2> 2987The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated. 2988Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'. 2989<h2 id=allbranches>1.3 All branches</h2> 2990The All branches report displays all branches in chronological order. 2991Not all data is fetched immediately. More records can be fetched using the Fetch bar provided. 2992<h3>Disassembly</h3> 2993Open a branch to display disassembly. This only works if: 2994<ol> 2995<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li> 2996<li>The object code is available. Currently, only the perf build ID cache is searched for object code. 2997The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR. 2998One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu), 2999or alternatively, set environment variable PERF_KCORE to the kcore file name.</li> 3000</ol> 3001<h4 id=xed>Intel XED Setup</h4> 3002To use Intel XED, libxed.so must be present. To build and install libxed.so: 3003<pre> 3004git clone https://github.com/intelxed/mbuild.git mbuild 3005git clone https://github.com/intelxed/xed 3006cd xed 3007./mfile.py --share 3008sudo ./mfile.py --prefix=/usr/local install 3009sudo ldconfig 3010</pre> 3011<h3>Instructions per Cycle (IPC)</h3> 3012If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'. 3013<p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch. 3014Due to the granularity of timing information, the number of cycles for some code blocks will not be known. 3015In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period 3016since the previous displayed 'IPC'. 3017<h3>Find</h3> 3018Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. 3019Refer to Python documentation for the regular expression syntax. 3020All columns are searched, but only currently fetched rows are searched. 3021<h2 id=selectedbranches>1.4 Selected branches</h2> 3022This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced 3023by various selection criteria. A dialog box displays available criteria which are AND'ed together. 3024<h3>1.4.1 Time ranges</h3> 3025The time ranges hint text shows the total time range. Relative time ranges can also be entered in 3026ms, us or ns. Also, negative values are relative to the end of trace. Examples: 3027<pre> 3028 81073085947329-81073085958238 From 81073085947329 to 81073085958238 3029 100us-200us From 100us to 200us 3030 10ms- From 10ms to the end 3031 -100ns The first 100ns 3032 -10ms- The last 10ms 3033</pre> 3034N.B. Due to the granularity of timestamps, there could be no branches in any given time range. 3035<h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2> 3036The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned. 3037The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together. 3038If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar. 3039<h1 id=tables>2. Tables</h1> 3040The Tables menu shows all tables and views in the database. Most tables have an associated view 3041which displays the information in a more friendly way. Not all data for large tables is fetched 3042immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted, 3043but that can be slow for large tables. 3044<p>There are also tables of database meta-information. 3045For SQLite3 databases, the sqlite_master table is included. 3046For PostgreSQL databases, information_schema.tables/views/columns are included. 3047<h3>Find</h3> 3048Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. 3049Refer to Python documentation for the regular expression syntax. 3050All columns are searched, but only currently fetched rows are searched. 3051<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous 3052will go to the next/previous result in id order, instead of display order. 3053""" 3054 3055# Help window 3056 3057class HelpWindow(QMdiSubWindow): 3058 3059 def __init__(self, glb, parent=None): 3060 super(HelpWindow, self).__init__(parent) 3061 3062 self.text = QTextBrowser() 3063 self.text.setHtml(glb_help_text) 3064 self.text.setReadOnly(True) 3065 self.text.setOpenExternalLinks(True) 3066 3067 self.setWidget(self.text) 3068 3069 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help") 3070 3071# Main window that only displays the help text 3072 3073class HelpOnlyWindow(QMainWindow): 3074 3075 def __init__(self, parent=None): 3076 super(HelpOnlyWindow, self).__init__(parent) 3077 3078 self.setMinimumSize(200, 100) 3079 self.resize(800, 600) 3080 self.setWindowTitle("Exported SQL Viewer Help") 3081 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation)) 3082 3083 self.text = QTextBrowser() 3084 self.text.setHtml(glb_help_text) 3085 self.text.setReadOnly(True) 3086 self.text.setOpenExternalLinks(True) 3087 3088 self.setCentralWidget(self.text) 3089 3090# PostqreSQL server version 3091 3092def PostqreSQLServerVersion(db): 3093 query = QSqlQuery(db) 3094 QueryExec(query, "SELECT VERSION()") 3095 if query.next(): 3096 v_str = query.value(0) 3097 v_list = v_str.strip().split(" ") 3098 if v_list[0] == "PostgreSQL" and v_list[2] == "on": 3099 return v_list[1] 3100 return v_str 3101 return "Unknown" 3102 3103# SQLite version 3104 3105def SQLiteVersion(db): 3106 query = QSqlQuery(db) 3107 QueryExec(query, "SELECT sqlite_version()") 3108 if query.next(): 3109 return query.value(0) 3110 return "Unknown" 3111 3112# About dialog 3113 3114class AboutDialog(QDialog): 3115 3116 def __init__(self, glb, parent=None): 3117 super(AboutDialog, self).__init__(parent) 3118 3119 self.setWindowTitle("About Exported SQL Viewer") 3120 self.setMinimumWidth(300) 3121 3122 pyside_version = "1" if pyside_version_1 else "2" 3123 3124 text = "<pre>" 3125 text += "Python version: " + sys.version.split(" ")[0] + "\n" 3126 text += "PySide version: " + pyside_version + "\n" 3127 text += "Qt version: " + qVersion() + "\n" 3128 if glb.dbref.is_sqlite3: 3129 text += "SQLite version: " + SQLiteVersion(glb.db) + "\n" 3130 else: 3131 text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n" 3132 text += "</pre>" 3133 3134 self.text = QTextBrowser() 3135 self.text.setHtml(text) 3136 self.text.setReadOnly(True) 3137 self.text.setOpenExternalLinks(True) 3138 3139 self.vbox = QVBoxLayout() 3140 self.vbox.addWidget(self.text) 3141 3142 self.setLayout(self.vbox); 3143 3144# Font resize 3145 3146def ResizeFont(widget, diff): 3147 font = widget.font() 3148 sz = font.pointSize() 3149 font.setPointSize(sz + diff) 3150 widget.setFont(font) 3151 3152def ShrinkFont(widget): 3153 ResizeFont(widget, -1) 3154 3155def EnlargeFont(widget): 3156 ResizeFont(widget, 1) 3157 3158# Unique name for sub-windows 3159 3160def NumberedWindowName(name, nr): 3161 if nr > 1: 3162 name += " <" + str(nr) + ">" 3163 return name 3164 3165def UniqueSubWindowName(mdi_area, name): 3166 nr = 1 3167 while True: 3168 unique_name = NumberedWindowName(name, nr) 3169 ok = True 3170 for sub_window in mdi_area.subWindowList(): 3171 if sub_window.name == unique_name: 3172 ok = False 3173 break 3174 if ok: 3175 return unique_name 3176 nr += 1 3177 3178# Add a sub-window 3179 3180def AddSubWindow(mdi_area, sub_window, name): 3181 unique_name = UniqueSubWindowName(mdi_area, name) 3182 sub_window.setMinimumSize(200, 100) 3183 sub_window.resize(800, 600) 3184 sub_window.setWindowTitle(unique_name) 3185 sub_window.setAttribute(Qt.WA_DeleteOnClose) 3186 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon)) 3187 sub_window.name = unique_name 3188 mdi_area.addSubWindow(sub_window) 3189 sub_window.show() 3190 3191# Main window 3192 3193class MainWindow(QMainWindow): 3194 3195 def __init__(self, glb, parent=None): 3196 super(MainWindow, self).__init__(parent) 3197 3198 self.glb = glb 3199 3200 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname) 3201 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) 3202 self.setMinimumSize(200, 100) 3203 3204 self.mdi_area = QMdiArea() 3205 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) 3206 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) 3207 3208 self.setCentralWidget(self.mdi_area) 3209 3210 menu = self.menuBar() 3211 3212 file_menu = menu.addMenu("&File") 3213 file_menu.addAction(CreateExitAction(glb.app, self)) 3214 3215 edit_menu = menu.addMenu("&Edit") 3216 edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy)) 3217 edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self)) 3218 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find)) 3219 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)])) 3220 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")])) 3221 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")])) 3222 3223 reports_menu = menu.addMenu("&Reports") 3224 if IsSelectable(glb.db, "calls"): 3225 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self)) 3226 3227 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"): 3228 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self)) 3229 3230 self.EventMenu(GetEventList(glb.db), reports_menu) 3231 3232 if IsSelectable(glb.db, "calls"): 3233 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self)) 3234 3235 self.TableMenu(GetTableList(glb), menu) 3236 3237 self.window_menu = WindowMenu(self.mdi_area, menu) 3238 3239 help_menu = menu.addMenu("&Help") 3240 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents)) 3241 help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self)) 3242 3243 def Try(self, fn): 3244 win = self.mdi_area.activeSubWindow() 3245 if win: 3246 try: 3247 fn(win.view) 3248 except: 3249 pass 3250 3251 def CopyToClipboard(self): 3252 self.Try(CopyCellsToClipboardHdr) 3253 3254 def CopyToClipboardCSV(self): 3255 self.Try(CopyCellsToClipboardCSV) 3256 3257 def Find(self): 3258 win = self.mdi_area.activeSubWindow() 3259 if win: 3260 try: 3261 win.find_bar.Activate() 3262 except: 3263 pass 3264 3265 def FetchMoreRecords(self): 3266 win = self.mdi_area.activeSubWindow() 3267 if win: 3268 try: 3269 win.fetch_bar.Activate() 3270 except: 3271 pass 3272 3273 def ShrinkFont(self): 3274 self.Try(ShrinkFont) 3275 3276 def EnlargeFont(self): 3277 self.Try(EnlargeFont) 3278 3279 def EventMenu(self, events, reports_menu): 3280 branches_events = 0 3281 for event in events: 3282 event = event.split(":")[0] 3283 if event == "branches": 3284 branches_events += 1 3285 dbid = 0 3286 for event in events: 3287 dbid += 1 3288 event = event.split(":")[0] 3289 if event == "branches": 3290 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")" 3291 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self)) 3292 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")" 3293 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self)) 3294 3295 def TableMenu(self, tables, menu): 3296 table_menu = menu.addMenu("&Tables") 3297 for table in tables: 3298 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self)) 3299 3300 def NewCallGraph(self): 3301 CallGraphWindow(self.glb, self) 3302 3303 def NewCallTree(self): 3304 CallTreeWindow(self.glb, self) 3305 3306 def NewTopCalls(self): 3307 dialog = TopCallsDialog(self.glb, self) 3308 ret = dialog.exec_() 3309 if ret: 3310 TopCallsWindow(self.glb, dialog.report_vars, self) 3311 3312 def NewBranchView(self, event_id): 3313 BranchWindow(self.glb, event_id, ReportVars(), self) 3314 3315 def NewSelectedBranchView(self, event_id): 3316 dialog = SelectedBranchDialog(self.glb, self) 3317 ret = dialog.exec_() 3318 if ret: 3319 BranchWindow(self.glb, event_id, dialog.report_vars, self) 3320 3321 def NewTableView(self, table_name): 3322 TableWindow(self.glb, table_name, self) 3323 3324 def Help(self): 3325 HelpWindow(self.glb, self) 3326 3327 def About(self): 3328 dialog = AboutDialog(self.glb, self) 3329 dialog.exec_() 3330 3331# XED Disassembler 3332 3333class xed_state_t(Structure): 3334 3335 _fields_ = [ 3336 ("mode", c_int), 3337 ("width", c_int) 3338 ] 3339 3340class XEDInstruction(): 3341 3342 def __init__(self, libxed): 3343 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion 3344 xedd_t = c_byte * 512 3345 self.xedd = xedd_t() 3346 self.xedp = addressof(self.xedd) 3347 libxed.xed_decoded_inst_zero(self.xedp) 3348 self.state = xed_state_t() 3349 self.statep = addressof(self.state) 3350 # Buffer for disassembled instruction text 3351 self.buffer = create_string_buffer(256) 3352 self.bufferp = addressof(self.buffer) 3353 3354class LibXED(): 3355 3356 def __init__(self): 3357 try: 3358 self.libxed = CDLL("libxed.so") 3359 except: 3360 self.libxed = None 3361 if not self.libxed: 3362 self.libxed = CDLL("/usr/local/lib/libxed.so") 3363 3364 self.xed_tables_init = self.libxed.xed_tables_init 3365 self.xed_tables_init.restype = None 3366 self.xed_tables_init.argtypes = [] 3367 3368 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero 3369 self.xed_decoded_inst_zero.restype = None 3370 self.xed_decoded_inst_zero.argtypes = [ c_void_p ] 3371 3372 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode 3373 self.xed_operand_values_set_mode.restype = None 3374 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ] 3375 3376 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode 3377 self.xed_decoded_inst_zero_keep_mode.restype = None 3378 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ] 3379 3380 self.xed_decode = self.libxed.xed_decode 3381 self.xed_decode.restype = c_int 3382 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ] 3383 3384 self.xed_format_context = self.libxed.xed_format_context 3385 self.xed_format_context.restype = c_uint 3386 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ] 3387 3388 self.xed_tables_init() 3389 3390 def Instruction(self): 3391 return XEDInstruction(self) 3392 3393 def SetMode(self, inst, mode): 3394 if mode: 3395 inst.state.mode = 4 # 32-bit 3396 inst.state.width = 4 # 4 bytes 3397 else: 3398 inst.state.mode = 1 # 64-bit 3399 inst.state.width = 8 # 8 bytes 3400 self.xed_operand_values_set_mode(inst.xedp, inst.statep) 3401 3402 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip): 3403 self.xed_decoded_inst_zero_keep_mode(inst.xedp) 3404 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt) 3405 if err: 3406 return 0, "" 3407 # Use AT&T mode (2), alternative is Intel (3) 3408 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0) 3409 if not ok: 3410 return 0, "" 3411 if sys.version_info[0] == 2: 3412 result = inst.buffer.value 3413 else: 3414 result = inst.buffer.value.decode() 3415 # Return instruction length and the disassembled instruction text 3416 # For now, assume the length is in byte 166 3417 return inst.xedd[166], result 3418 3419def TryOpen(file_name): 3420 try: 3421 return open(file_name, "rb") 3422 except: 3423 return None 3424 3425def Is64Bit(f): 3426 result = sizeof(c_void_p) 3427 # ELF support only 3428 pos = f.tell() 3429 f.seek(0) 3430 header = f.read(7) 3431 f.seek(pos) 3432 magic = header[0:4] 3433 if sys.version_info[0] == 2: 3434 eclass = ord(header[4]) 3435 encoding = ord(header[5]) 3436 version = ord(header[6]) 3437 else: 3438 eclass = header[4] 3439 encoding = header[5] 3440 version = header[6] 3441 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1: 3442 result = True if eclass == 2 else False 3443 return result 3444 3445# Global data 3446 3447class Glb(): 3448 3449 def __init__(self, dbref, db, dbname): 3450 self.dbref = dbref 3451 self.db = db 3452 self.dbname = dbname 3453 self.home_dir = os.path.expanduser("~") 3454 self.buildid_dir = os.getenv("PERF_BUILDID_DIR") 3455 if self.buildid_dir: 3456 self.buildid_dir += "/.build-id/" 3457 else: 3458 self.buildid_dir = self.home_dir + "/.debug/.build-id/" 3459 self.app = None 3460 self.mainwindow = None 3461 self.instances_to_shutdown_on_exit = weakref.WeakSet() 3462 try: 3463 self.disassembler = LibXED() 3464 self.have_disassembler = True 3465 except: 3466 self.have_disassembler = False 3467 3468 def FileFromBuildId(self, build_id): 3469 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf" 3470 return TryOpen(file_name) 3471 3472 def FileFromNamesAndBuildId(self, short_name, long_name, build_id): 3473 # Assume current machine i.e. no support for virtualization 3474 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore": 3475 file_name = os.getenv("PERF_KCORE") 3476 f = TryOpen(file_name) if file_name else None 3477 if f: 3478 return f 3479 # For now, no special handling if long_name is /proc/kcore 3480 f = TryOpen(long_name) 3481 if f: 3482 return f 3483 f = self.FileFromBuildId(build_id) 3484 if f: 3485 return f 3486 return None 3487 3488 def AddInstanceToShutdownOnExit(self, instance): 3489 self.instances_to_shutdown_on_exit.add(instance) 3490 3491 # Shutdown any background processes or threads 3492 def ShutdownInstances(self): 3493 for x in self.instances_to_shutdown_on_exit: 3494 try: 3495 x.Shutdown() 3496 except: 3497 pass 3498 3499# Database reference 3500 3501class DBRef(): 3502 3503 def __init__(self, is_sqlite3, dbname): 3504 self.is_sqlite3 = is_sqlite3 3505 self.dbname = dbname 3506 3507 def Open(self, connection_name): 3508 dbname = self.dbname 3509 if self.is_sqlite3: 3510 db = QSqlDatabase.addDatabase("QSQLITE", connection_name) 3511 else: 3512 db = QSqlDatabase.addDatabase("QPSQL", connection_name) 3513 opts = dbname.split() 3514 for opt in opts: 3515 if "=" in opt: 3516 opt = opt.split("=") 3517 if opt[0] == "hostname": 3518 db.setHostName(opt[1]) 3519 elif opt[0] == "port": 3520 db.setPort(int(opt[1])) 3521 elif opt[0] == "username": 3522 db.setUserName(opt[1]) 3523 elif opt[0] == "password": 3524 db.setPassword(opt[1]) 3525 elif opt[0] == "dbname": 3526 dbname = opt[1] 3527 else: 3528 dbname = opt 3529 3530 db.setDatabaseName(dbname) 3531 if not db.open(): 3532 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text()) 3533 return db, dbname 3534 3535# Main 3536 3537def Main(): 3538 usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \ 3539 " or: exported-sql-viewer.py --help-only" 3540 ap = argparse.ArgumentParser(usage = usage_str, add_help = False) 3541 ap.add_argument("--pyside-version-1", action='store_true') 3542 ap.add_argument("dbname", nargs="?") 3543 ap.add_argument("--help-only", action='store_true') 3544 args = ap.parse_args() 3545 3546 if args.help_only: 3547 app = QApplication(sys.argv) 3548 mainwindow = HelpOnlyWindow() 3549 mainwindow.show() 3550 err = app.exec_() 3551 sys.exit(err) 3552 3553 dbname = args.dbname 3554 if dbname is None: 3555 ap.print_usage() 3556 print("Too few arguments") 3557 sys.exit(1) 3558 3559 is_sqlite3 = False 3560 try: 3561 f = open(dbname, "rb") 3562 if f.read(15) == b'SQLite format 3': 3563 is_sqlite3 = True 3564 f.close() 3565 except: 3566 pass 3567 3568 dbref = DBRef(is_sqlite3, dbname) 3569 db, dbname = dbref.Open("main") 3570 glb = Glb(dbref, db, dbname) 3571 app = QApplication(sys.argv) 3572 glb.app = app 3573 mainwindow = MainWindow(glb) 3574 glb.mainwindow = mainwindow 3575 mainwindow.show() 3576 err = app.exec_() 3577 glb.ShutdownInstances() 3578 db.close() 3579 sys.exit(err) 3580 3581if __name__ == "__main__": 3582 Main() 3583