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