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