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