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