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