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