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