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