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 108import random 109import copy 110import math 111 112pyside_version_1 = True 113if not "--pyside-version-1" in sys.argv: 114 try: 115 from PySide2.QtCore import * 116 from PySide2.QtGui import * 117 from PySide2.QtSql import * 118 from PySide2.QtWidgets import * 119 pyside_version_1 = False 120 except: 121 pass 122 123if pyside_version_1: 124 from PySide.QtCore import * 125 from PySide.QtGui import * 126 from PySide.QtSql import * 127 128from decimal import * 129from ctypes import * 130from multiprocessing import Process, Array, Value, Event 131 132# xrange is range in Python3 133try: 134 xrange 135except NameError: 136 xrange = range 137 138def printerr(*args, **keyword_args): 139 print(*args, file=sys.stderr, **keyword_args) 140 141# Data formatting helpers 142 143def tohex(ip): 144 if ip < 0: 145 ip += 1 << 64 146 return "%x" % ip 147 148def offstr(offset): 149 if offset: 150 return "+0x%x" % offset 151 return "" 152 153def dsoname(name): 154 if name == "[kernel.kallsyms]": 155 return "[kernel]" 156 return name 157 158def findnth(s, sub, n, offs=0): 159 pos = s.find(sub) 160 if pos < 0: 161 return pos 162 if n <= 1: 163 return offs + pos 164 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1) 165 166# Percent to one decimal place 167 168def PercentToOneDP(n, d): 169 if not d: 170 return "0.0" 171 x = (n * Decimal(100)) / d 172 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP)) 173 174# Helper for queries that must not fail 175 176def QueryExec(query, stmt): 177 ret = query.exec_(stmt) 178 if not ret: 179 raise Exception("Query failed: " + query.lastError().text()) 180 181# Background thread 182 183class Thread(QThread): 184 185 done = Signal(object) 186 187 def __init__(self, task, param=None, parent=None): 188 super(Thread, self).__init__(parent) 189 self.task = task 190 self.param = param 191 192 def run(self): 193 while True: 194 if self.param is None: 195 done, result = self.task() 196 else: 197 done, result = self.task(self.param) 198 self.done.emit(result) 199 if done: 200 break 201 202# Tree data model 203 204class TreeModel(QAbstractItemModel): 205 206 def __init__(self, glb, params, parent=None): 207 super(TreeModel, self).__init__(parent) 208 self.glb = glb 209 self.params = params 210 self.root = self.GetRoot() 211 self.last_row_read = 0 212 213 def Item(self, parent): 214 if parent.isValid(): 215 return parent.internalPointer() 216 else: 217 return self.root 218 219 def rowCount(self, parent): 220 result = self.Item(parent).childCount() 221 if result < 0: 222 result = 0 223 self.dataChanged.emit(parent, parent) 224 return result 225 226 def hasChildren(self, parent): 227 return self.Item(parent).hasChildren() 228 229 def headerData(self, section, orientation, role): 230 if role == Qt.TextAlignmentRole: 231 return self.columnAlignment(section) 232 if role != Qt.DisplayRole: 233 return None 234 if orientation != Qt.Horizontal: 235 return None 236 return self.columnHeader(section) 237 238 def parent(self, child): 239 child_item = child.internalPointer() 240 if child_item is self.root: 241 return QModelIndex() 242 parent_item = child_item.getParentItem() 243 return self.createIndex(parent_item.getRow(), 0, parent_item) 244 245 def index(self, row, column, parent): 246 child_item = self.Item(parent).getChildItem(row) 247 return self.createIndex(row, column, child_item) 248 249 def DisplayData(self, item, index): 250 return item.getData(index.column()) 251 252 def FetchIfNeeded(self, row): 253 if row > self.last_row_read: 254 self.last_row_read = row 255 if row + 10 >= self.root.child_count: 256 self.fetcher.Fetch(glb_chunk_sz) 257 258 def columnAlignment(self, column): 259 return Qt.AlignLeft 260 261 def columnFont(self, column): 262 return None 263 264 def data(self, index, role): 265 if role == Qt.TextAlignmentRole: 266 return self.columnAlignment(index.column()) 267 if role == Qt.FontRole: 268 return self.columnFont(index.column()) 269 if role != Qt.DisplayRole: 270 return None 271 item = index.internalPointer() 272 return self.DisplayData(item, index) 273 274# Table data model 275 276class TableModel(QAbstractTableModel): 277 278 def __init__(self, parent=None): 279 super(TableModel, self).__init__(parent) 280 self.child_count = 0 281 self.child_items = [] 282 self.last_row_read = 0 283 284 def Item(self, parent): 285 if parent.isValid(): 286 return parent.internalPointer() 287 else: 288 return self 289 290 def rowCount(self, parent): 291 return self.child_count 292 293 def headerData(self, section, orientation, role): 294 if role == Qt.TextAlignmentRole: 295 return self.columnAlignment(section) 296 if role != Qt.DisplayRole: 297 return None 298 if orientation != Qt.Horizontal: 299 return None 300 return self.columnHeader(section) 301 302 def index(self, row, column, parent): 303 return self.createIndex(row, column, self.child_items[row]) 304 305 def DisplayData(self, item, index): 306 return item.getData(index.column()) 307 308 def FetchIfNeeded(self, row): 309 if row > self.last_row_read: 310 self.last_row_read = row 311 if row + 10 >= self.child_count: 312 self.fetcher.Fetch(glb_chunk_sz) 313 314 def columnAlignment(self, column): 315 return Qt.AlignLeft 316 317 def columnFont(self, column): 318 return None 319 320 def data(self, index, role): 321 if role == Qt.TextAlignmentRole: 322 return self.columnAlignment(index.column()) 323 if role == Qt.FontRole: 324 return self.columnFont(index.column()) 325 if role != Qt.DisplayRole: 326 return None 327 item = index.internalPointer() 328 return self.DisplayData(item, index) 329 330# Model cache 331 332model_cache = weakref.WeakValueDictionary() 333model_cache_lock = threading.Lock() 334 335def LookupCreateModel(model_name, create_fn): 336 model_cache_lock.acquire() 337 try: 338 model = model_cache[model_name] 339 except: 340 model = None 341 if model is None: 342 model = create_fn() 343 model_cache[model_name] = model 344 model_cache_lock.release() 345 return model 346 347def LookupModel(model_name): 348 model_cache_lock.acquire() 349 try: 350 model = model_cache[model_name] 351 except: 352 model = None 353 model_cache_lock.release() 354 return model 355 356# Find bar 357 358class FindBar(): 359 360 def __init__(self, parent, finder, is_reg_expr=False): 361 self.finder = finder 362 self.context = [] 363 self.last_value = None 364 self.last_pattern = None 365 366 label = QLabel("Find:") 367 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 368 369 self.textbox = QComboBox() 370 self.textbox.setEditable(True) 371 self.textbox.currentIndexChanged.connect(self.ValueChanged) 372 373 self.progress = QProgressBar() 374 self.progress.setRange(0, 0) 375 self.progress.hide() 376 377 if is_reg_expr: 378 self.pattern = QCheckBox("Regular Expression") 379 else: 380 self.pattern = QCheckBox("Pattern") 381 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 382 383 self.next_button = QToolButton() 384 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown)) 385 self.next_button.released.connect(lambda: self.NextPrev(1)) 386 387 self.prev_button = QToolButton() 388 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp)) 389 self.prev_button.released.connect(lambda: self.NextPrev(-1)) 390 391 self.close_button = QToolButton() 392 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) 393 self.close_button.released.connect(self.Deactivate) 394 395 self.hbox = QHBoxLayout() 396 self.hbox.setContentsMargins(0, 0, 0, 0) 397 398 self.hbox.addWidget(label) 399 self.hbox.addWidget(self.textbox) 400 self.hbox.addWidget(self.progress) 401 self.hbox.addWidget(self.pattern) 402 self.hbox.addWidget(self.next_button) 403 self.hbox.addWidget(self.prev_button) 404 self.hbox.addWidget(self.close_button) 405 406 self.bar = QWidget() 407 self.bar.setLayout(self.hbox) 408 self.bar.hide() 409 410 def Widget(self): 411 return self.bar 412 413 def Activate(self): 414 self.bar.show() 415 self.textbox.lineEdit().selectAll() 416 self.textbox.setFocus() 417 418 def Deactivate(self): 419 self.bar.hide() 420 421 def Busy(self): 422 self.textbox.setEnabled(False) 423 self.pattern.hide() 424 self.next_button.hide() 425 self.prev_button.hide() 426 self.progress.show() 427 428 def Idle(self): 429 self.textbox.setEnabled(True) 430 self.progress.hide() 431 self.pattern.show() 432 self.next_button.show() 433 self.prev_button.show() 434 435 def Find(self, direction): 436 value = self.textbox.currentText() 437 pattern = self.pattern.isChecked() 438 self.last_value = value 439 self.last_pattern = pattern 440 self.finder.Find(value, direction, pattern, self.context) 441 442 def ValueChanged(self): 443 value = self.textbox.currentText() 444 pattern = self.pattern.isChecked() 445 index = self.textbox.currentIndex() 446 data = self.textbox.itemData(index) 447 # Store the pattern in the combo box to keep it with the text value 448 if data == None: 449 self.textbox.setItemData(index, pattern) 450 else: 451 self.pattern.setChecked(data) 452 self.Find(0) 453 454 def NextPrev(self, direction): 455 value = self.textbox.currentText() 456 pattern = self.pattern.isChecked() 457 if value != self.last_value: 458 index = self.textbox.findText(value) 459 # Allow for a button press before the value has been added to the combo box 460 if index < 0: 461 index = self.textbox.count() 462 self.textbox.addItem(value, pattern) 463 self.textbox.setCurrentIndex(index) 464 return 465 else: 466 self.textbox.setItemData(index, pattern) 467 elif pattern != self.last_pattern: 468 # Keep the pattern recorded in the combo box up to date 469 index = self.textbox.currentIndex() 470 self.textbox.setItemData(index, pattern) 471 self.Find(direction) 472 473 def NotFound(self): 474 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found") 475 476# Context-sensitive call graph data model item base 477 478class CallGraphLevelItemBase(object): 479 480 def __init__(self, glb, params, row, parent_item): 481 self.glb = glb 482 self.params = params 483 self.row = row 484 self.parent_item = parent_item 485 self.query_done = False 486 self.child_count = 0 487 self.child_items = [] 488 if parent_item: 489 self.level = parent_item.level + 1 490 else: 491 self.level = 0 492 493 def getChildItem(self, row): 494 return self.child_items[row] 495 496 def getParentItem(self): 497 return self.parent_item 498 499 def getRow(self): 500 return self.row 501 502 def childCount(self): 503 if not self.query_done: 504 self.Select() 505 if not self.child_count: 506 return -1 507 return self.child_count 508 509 def hasChildren(self): 510 if not self.query_done: 511 return True 512 return self.child_count > 0 513 514 def getData(self, column): 515 return self.data[column] 516 517# Context-sensitive call graph data model level 2+ item base 518 519class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase): 520 521 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item): 522 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item) 523 self.comm_id = comm_id 524 self.thread_id = thread_id 525 self.call_path_id = call_path_id 526 self.insn_cnt = insn_cnt 527 self.cyc_cnt = cyc_cnt 528 self.branch_count = branch_count 529 self.time = time 530 531 def Select(self): 532 self.query_done = True 533 query = QSqlQuery(self.glb.db) 534 if self.params.have_ipc: 535 ipc_str = ", SUM(insn_count), SUM(cyc_count)" 536 else: 537 ipc_str = "" 538 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)" 539 " FROM calls" 540 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 541 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 542 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 543 " WHERE parent_call_path_id = " + str(self.call_path_id) + 544 " AND comm_id = " + str(self.comm_id) + 545 " AND thread_id = " + str(self.thread_id) + 546 " GROUP BY call_path_id, name, short_name" 547 " ORDER BY call_path_id") 548 while query.next(): 549 if self.params.have_ipc: 550 insn_cnt = int(query.value(5)) 551 cyc_cnt = int(query.value(6)) 552 branch_count = int(query.value(7)) 553 else: 554 insn_cnt = 0 555 cyc_cnt = 0 556 branch_count = int(query.value(5)) 557 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) 558 self.child_items.append(child_item) 559 self.child_count += 1 560 561# Context-sensitive call graph data model level three item 562 563class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase): 564 565 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): 566 super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item) 567 dso = dsoname(dso) 568 if self.params.have_ipc: 569 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt) 570 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt) 571 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count) 572 ipc = CalcIPC(cyc_cnt, insn_cnt) 573 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 ] 574 else: 575 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] 576 self.dbid = call_path_id 577 578# Context-sensitive call graph data model level two item 579 580class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase): 581 582 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item): 583 super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item) 584 if self.params.have_ipc: 585 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""] 586 else: 587 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] 588 self.dbid = thread_id 589 590 def Select(self): 591 super(CallGraphLevelTwoItem, self).Select() 592 for child_item in self.child_items: 593 self.time += child_item.time 594 self.insn_cnt += child_item.insn_cnt 595 self.cyc_cnt += child_item.cyc_cnt 596 self.branch_count += child_item.branch_count 597 for child_item in self.child_items: 598 child_item.data[4] = PercentToOneDP(child_item.time, self.time) 599 if self.params.have_ipc: 600 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt) 601 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt) 602 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count) 603 else: 604 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) 605 606# Context-sensitive call graph data model level one item 607 608class CallGraphLevelOneItem(CallGraphLevelItemBase): 609 610 def __init__(self, glb, params, row, comm_id, comm, parent_item): 611 super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item) 612 if self.params.have_ipc: 613 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""] 614 else: 615 self.data = [comm, "", "", "", "", "", ""] 616 self.dbid = comm_id 617 618 def Select(self): 619 self.query_done = True 620 query = QSqlQuery(self.glb.db) 621 QueryExec(query, "SELECT thread_id, pid, tid" 622 " FROM comm_threads" 623 " INNER JOIN threads ON thread_id = threads.id" 624 " WHERE comm_id = " + str(self.dbid)) 625 while query.next(): 626 child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) 627 self.child_items.append(child_item) 628 self.child_count += 1 629 630# Context-sensitive call graph data model root item 631 632class CallGraphRootItem(CallGraphLevelItemBase): 633 634 def __init__(self, glb, params): 635 super(CallGraphRootItem, self).__init__(glb, params, 0, None) 636 self.dbid = 0 637 self.query_done = True 638 if_has_calls = "" 639 if IsSelectable(glb.db, "comms", columns = "has_calls"): 640 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE 641 query = QSqlQuery(glb.db) 642 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls) 643 while query.next(): 644 if not query.value(0): 645 continue 646 child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self) 647 self.child_items.append(child_item) 648 self.child_count += 1 649 650# Call graph model parameters 651 652class CallGraphModelParams(): 653 654 def __init__(self, glb, parent=None): 655 self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count") 656 657# Context-sensitive call graph data model base 658 659class CallGraphModelBase(TreeModel): 660 661 def __init__(self, glb, parent=None): 662 super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent) 663 664 def FindSelect(self, value, pattern, query): 665 if pattern: 666 # postgresql and sqlite pattern patching differences: 667 # postgresql LIKE is case sensitive but sqlite LIKE is not 668 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not 669 # postgresql supports ILIKE which is case insensitive 670 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive 671 if not self.glb.dbref.is_sqlite3: 672 # Escape % and _ 673 s = value.replace("%", "\%") 674 s = s.replace("_", "\_") 675 # Translate * and ? into SQL LIKE pattern characters % and _ 676 trans = string.maketrans("*?", "%_") 677 match = " LIKE '" + str(s).translate(trans) + "'" 678 else: 679 match = " GLOB '" + str(value) + "'" 680 else: 681 match = " = '" + str(value) + "'" 682 self.DoFindSelect(query, match) 683 684 def Found(self, query, found): 685 if found: 686 return self.FindPath(query) 687 return [] 688 689 def FindValue(self, value, pattern, query, last_value, last_pattern): 690 if last_value == value and pattern == last_pattern: 691 found = query.first() 692 else: 693 self.FindSelect(value, pattern, query) 694 found = query.next() 695 return self.Found(query, found) 696 697 def FindNext(self, query): 698 found = query.next() 699 if not found: 700 found = query.first() 701 return self.Found(query, found) 702 703 def FindPrev(self, query): 704 found = query.previous() 705 if not found: 706 found = query.last() 707 return self.Found(query, found) 708 709 def FindThread(self, c): 710 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern: 711 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern) 712 elif c.direction > 0: 713 ids = self.FindNext(c.query) 714 else: 715 ids = self.FindPrev(c.query) 716 return (True, ids) 717 718 def Find(self, value, direction, pattern, context, callback): 719 class Context(): 720 def __init__(self, *x): 721 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x 722 def Update(self, *x): 723 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern) 724 if len(context): 725 context[0].Update(value, direction, pattern) 726 else: 727 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None)) 728 # Use a thread so the UI is not blocked during the SELECT 729 thread = Thread(self.FindThread, context[0]) 730 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection) 731 thread.start() 732 733 def FindDone(self, thread, callback, ids): 734 callback(ids) 735 736# Context-sensitive call graph data model 737 738class CallGraphModel(CallGraphModelBase): 739 740 def __init__(self, glb, parent=None): 741 super(CallGraphModel, self).__init__(glb, parent) 742 743 def GetRoot(self): 744 return CallGraphRootItem(self.glb, self.params) 745 746 def columnCount(self, parent=None): 747 if self.params.have_ipc: 748 return 12 749 else: 750 return 7 751 752 def columnHeader(self, column): 753 if self.params.have_ipc: 754 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "] 755 else: 756 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] 757 return headers[column] 758 759 def columnAlignment(self, column): 760 if self.params.have_ipc: 761 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 ] 762 else: 763 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] 764 return alignment[column] 765 766 def DoFindSelect(self, query, match): 767 QueryExec(query, "SELECT call_path_id, comm_id, thread_id" 768 " FROM calls" 769 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 770 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 771 " WHERE calls.id <> 0" 772 " AND symbols.name" + match + 773 " GROUP BY comm_id, thread_id, call_path_id" 774 " ORDER BY comm_id, thread_id, call_path_id") 775 776 def FindPath(self, query): 777 # Turn the query result into a list of ids that the tree view can walk 778 # to open the tree at the right place. 779 ids = [] 780 parent_id = query.value(0) 781 while parent_id: 782 ids.insert(0, parent_id) 783 q2 = QSqlQuery(self.glb.db) 784 QueryExec(q2, "SELECT parent_id" 785 " FROM call_paths" 786 " WHERE id = " + str(parent_id)) 787 if not q2.next(): 788 break 789 parent_id = q2.value(0) 790 # The call path root is not used 791 if ids[0] == 1: 792 del ids[0] 793 ids.insert(0, query.value(2)) 794 ids.insert(0, query.value(1)) 795 return ids 796 797# Call tree data model level 2+ item base 798 799class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase): 800 801 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item): 802 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item) 803 self.comm_id = comm_id 804 self.thread_id = thread_id 805 self.calls_id = calls_id 806 self.call_time = call_time 807 self.time = time 808 self.insn_cnt = insn_cnt 809 self.cyc_cnt = cyc_cnt 810 self.branch_count = branch_count 811 812 def Select(self): 813 self.query_done = True 814 if self.calls_id == 0: 815 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id) 816 else: 817 comm_thread = "" 818 if self.params.have_ipc: 819 ipc_str = ", insn_count, cyc_count" 820 else: 821 ipc_str = "" 822 query = QSqlQuery(self.glb.db) 823 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count" 824 " FROM calls" 825 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 826 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 827 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 828 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread + 829 " ORDER BY call_time, calls.id") 830 while query.next(): 831 if self.params.have_ipc: 832 insn_cnt = int(query.value(5)) 833 cyc_cnt = int(query.value(6)) 834 branch_count = int(query.value(7)) 835 else: 836 insn_cnt = 0 837 cyc_cnt = 0 838 branch_count = int(query.value(5)) 839 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) 840 self.child_items.append(child_item) 841 self.child_count += 1 842 843# Call tree data model level three item 844 845class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase): 846 847 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): 848 super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item) 849 dso = dsoname(dso) 850 if self.params.have_ipc: 851 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt) 852 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt) 853 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count) 854 ipc = CalcIPC(cyc_cnt, insn_cnt) 855 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 ] 856 else: 857 self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] 858 self.dbid = calls_id 859 860# Call tree data model level two item 861 862class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase): 863 864 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item): 865 super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, 0, parent_item) 866 if self.params.have_ipc: 867 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""] 868 else: 869 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] 870 self.dbid = thread_id 871 872 def Select(self): 873 super(CallTreeLevelTwoItem, self).Select() 874 for child_item in self.child_items: 875 self.time += child_item.time 876 self.insn_cnt += child_item.insn_cnt 877 self.cyc_cnt += child_item.cyc_cnt 878 self.branch_count += child_item.branch_count 879 for child_item in self.child_items: 880 child_item.data[4] = PercentToOneDP(child_item.time, self.time) 881 if self.params.have_ipc: 882 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt) 883 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt) 884 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count) 885 else: 886 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) 887 888# Call tree data model level one item 889 890class CallTreeLevelOneItem(CallGraphLevelItemBase): 891 892 def __init__(self, glb, params, row, comm_id, comm, parent_item): 893 super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item) 894 if self.params.have_ipc: 895 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""] 896 else: 897 self.data = [comm, "", "", "", "", "", ""] 898 self.dbid = comm_id 899 900 def Select(self): 901 self.query_done = True 902 query = QSqlQuery(self.glb.db) 903 QueryExec(query, "SELECT thread_id, pid, tid" 904 " FROM comm_threads" 905 " INNER JOIN threads ON thread_id = threads.id" 906 " WHERE comm_id = " + str(self.dbid)) 907 while query.next(): 908 child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) 909 self.child_items.append(child_item) 910 self.child_count += 1 911 912# Call tree data model root item 913 914class CallTreeRootItem(CallGraphLevelItemBase): 915 916 def __init__(self, glb, params): 917 super(CallTreeRootItem, self).__init__(glb, params, 0, None) 918 self.dbid = 0 919 self.query_done = True 920 if_has_calls = "" 921 if IsSelectable(glb.db, "comms", columns = "has_calls"): 922 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE 923 query = QSqlQuery(glb.db) 924 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls) 925 while query.next(): 926 if not query.value(0): 927 continue 928 child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self) 929 self.child_items.append(child_item) 930 self.child_count += 1 931 932# Call Tree data model 933 934class CallTreeModel(CallGraphModelBase): 935 936 def __init__(self, glb, parent=None): 937 super(CallTreeModel, self).__init__(glb, parent) 938 939 def GetRoot(self): 940 return CallTreeRootItem(self.glb, self.params) 941 942 def columnCount(self, parent=None): 943 if self.params.have_ipc: 944 return 12 945 else: 946 return 7 947 948 def columnHeader(self, column): 949 if self.params.have_ipc: 950 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "] 951 else: 952 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] 953 return headers[column] 954 955 def columnAlignment(self, column): 956 if self.params.have_ipc: 957 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 ] 958 else: 959 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] 960 return alignment[column] 961 962 def DoFindSelect(self, query, match): 963 QueryExec(query, "SELECT calls.id, comm_id, thread_id" 964 " FROM calls" 965 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 966 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 967 " WHERE symbols.name" + match + 968 " ORDER BY comm_id, thread_id, call_time, calls.id") 969 970 def FindPath(self, query): 971 # Turn the query result into a list of ids that the tree view can walk 972 # to open the tree at the right place. 973 ids = [] 974 parent_id = query.value(0) 975 while parent_id: 976 ids.insert(0, parent_id) 977 q2 = QSqlQuery(self.glb.db) 978 QueryExec(q2, "SELECT parent_id" 979 " FROM calls" 980 " WHERE id = " + str(parent_id)) 981 if not q2.next(): 982 break 983 parent_id = q2.value(0) 984 ids.insert(0, query.value(2)) 985 ids.insert(0, query.value(1)) 986 return ids 987 988# Vertical layout 989 990class HBoxLayout(QHBoxLayout): 991 992 def __init__(self, *children): 993 super(HBoxLayout, self).__init__() 994 995 self.layout().setContentsMargins(0, 0, 0, 0) 996 for child in children: 997 if child.isWidgetType(): 998 self.layout().addWidget(child) 999 else: 1000 self.layout().addLayout(child) 1001 1002# Horizontal layout 1003 1004class VBoxLayout(QVBoxLayout): 1005 1006 def __init__(self, *children): 1007 super(VBoxLayout, self).__init__() 1008 1009 self.layout().setContentsMargins(0, 0, 0, 0) 1010 for child in children: 1011 if child.isWidgetType(): 1012 self.layout().addWidget(child) 1013 else: 1014 self.layout().addLayout(child) 1015 1016# Vertical layout widget 1017 1018class VBox(): 1019 1020 def __init__(self, *children): 1021 self.vbox = QWidget() 1022 self.vbox.setLayout(VBoxLayout(*children)) 1023 1024 def Widget(self): 1025 return self.vbox 1026 1027# Tree window base 1028 1029class TreeWindowBase(QMdiSubWindow): 1030 1031 def __init__(self, parent=None): 1032 super(TreeWindowBase, self).__init__(parent) 1033 1034 self.model = None 1035 self.find_bar = None 1036 1037 self.view = QTreeView() 1038 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) 1039 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard 1040 1041 self.context_menu = TreeContextMenu(self.view) 1042 1043 def DisplayFound(self, ids): 1044 if not len(ids): 1045 return False 1046 parent = QModelIndex() 1047 for dbid in ids: 1048 found = False 1049 n = self.model.rowCount(parent) 1050 for row in xrange(n): 1051 child = self.model.index(row, 0, parent) 1052 if child.internalPointer().dbid == dbid: 1053 found = True 1054 self.view.setExpanded(parent, True) 1055 self.view.setCurrentIndex(child) 1056 parent = child 1057 break 1058 if not found: 1059 break 1060 return found 1061 1062 def Find(self, value, direction, pattern, context): 1063 self.view.setFocus() 1064 self.find_bar.Busy() 1065 self.model.Find(value, direction, pattern, context, self.FindDone) 1066 1067 def FindDone(self, ids): 1068 found = True 1069 if not self.DisplayFound(ids): 1070 found = False 1071 self.find_bar.Idle() 1072 if not found: 1073 self.find_bar.NotFound() 1074 1075 1076# Context-sensitive call graph window 1077 1078class CallGraphWindow(TreeWindowBase): 1079 1080 def __init__(self, glb, parent=None): 1081 super(CallGraphWindow, self).__init__(parent) 1082 1083 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x)) 1084 1085 self.view.setModel(self.model) 1086 1087 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): 1088 self.view.setColumnWidth(c, w) 1089 1090 self.find_bar = FindBar(self, self) 1091 1092 self.vbox = VBox(self.view, self.find_bar.Widget()) 1093 1094 self.setWidget(self.vbox.Widget()) 1095 1096 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph") 1097 1098# Call tree window 1099 1100class CallTreeWindow(TreeWindowBase): 1101 1102 def __init__(self, glb, parent=None, thread_at_time=None): 1103 super(CallTreeWindow, self).__init__(parent) 1104 1105 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x)) 1106 1107 self.view.setModel(self.model) 1108 1109 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)): 1110 self.view.setColumnWidth(c, w) 1111 1112 self.find_bar = FindBar(self, self) 1113 1114 self.vbox = VBox(self.view, self.find_bar.Widget()) 1115 1116 self.setWidget(self.vbox.Widget()) 1117 1118 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree") 1119 1120 if thread_at_time: 1121 self.DisplayThreadAtTime(*thread_at_time) 1122 1123 def DisplayThreadAtTime(self, comm_id, thread_id, time): 1124 parent = QModelIndex() 1125 for dbid in (comm_id, thread_id): 1126 found = False 1127 n = self.model.rowCount(parent) 1128 for row in xrange(n): 1129 child = self.model.index(row, 0, parent) 1130 if child.internalPointer().dbid == dbid: 1131 found = True 1132 self.view.setCurrentIndex(child) 1133 parent = child 1134 break 1135 if not found: 1136 return 1137 found = False 1138 while True: 1139 n = self.model.rowCount(parent) 1140 if not n: 1141 return 1142 last_child = None 1143 for row in xrange(n): 1144 child = self.model.index(row, 0, parent) 1145 child_call_time = child.internalPointer().call_time 1146 if child_call_time < time: 1147 last_child = child 1148 elif child_call_time == time: 1149 self.view.setCurrentIndex(child) 1150 return 1151 elif child_call_time > time: 1152 break 1153 if not last_child: 1154 if not found: 1155 child = self.model.index(0, 0, parent) 1156 self.view.setCurrentIndex(child) 1157 return 1158 found = True 1159 self.view.setCurrentIndex(last_child) 1160 parent = last_child 1161 1162# ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name 1163 1164def ExecComm(db, thread_id, time): 1165 query = QSqlQuery(db) 1166 QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag" 1167 " FROM comm_threads" 1168 " INNER JOIN comms ON comms.id = comm_threads.comm_id" 1169 " WHERE comm_threads.thread_id = " + str(thread_id) + 1170 " ORDER BY comms.c_time, comms.id") 1171 first = None 1172 last = None 1173 while query.next(): 1174 if first is None: 1175 first = query.value(0) 1176 if query.value(2) and Decimal(query.value(1)) <= Decimal(time): 1177 last = query.value(0) 1178 if not(last is None): 1179 return last 1180 return first 1181 1182# Container for (x, y) data 1183 1184class XY(): 1185 def __init__(self, x=0, y=0): 1186 self.x = x 1187 self.y = y 1188 1189 def __str__(self): 1190 return "XY({}, {})".format(str(self.x), str(self.y)) 1191 1192# Container for sub-range data 1193 1194class Subrange(): 1195 def __init__(self, lo=0, hi=0): 1196 self.lo = lo 1197 self.hi = hi 1198 1199 def __str__(self): 1200 return "Subrange({}, {})".format(str(self.lo), str(self.hi)) 1201 1202# Graph data region base class 1203 1204class GraphDataRegion(object): 1205 1206 def __init__(self, key, title = "", ordinal = ""): 1207 self.key = key 1208 self.title = title 1209 self.ordinal = ordinal 1210 1211# Function to sort GraphDataRegion 1212 1213def GraphDataRegionOrdinal(data_region): 1214 return data_region.ordinal 1215 1216# Attributes for a graph region 1217 1218class GraphRegionAttribute(): 1219 1220 def __init__(self, colour): 1221 self.colour = colour 1222 1223# Switch graph data region represents a task 1224 1225class SwitchGraphDataRegion(GraphDataRegion): 1226 1227 def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id): 1228 super(SwitchGraphDataRegion, self).__init__(key) 1229 1230 self.title = str(pid) + " / " + str(tid) + " " + comm 1231 # Order graph legend within exec comm by pid / tid / time 1232 self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16) 1233 self.exec_comm_id = exec_comm_id 1234 self.pid = pid 1235 self.tid = tid 1236 self.comm = comm 1237 self.thread_id = thread_id 1238 self.comm_id = comm_id 1239 1240# Graph data point 1241 1242class GraphDataPoint(): 1243 1244 def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None): 1245 self.data = data 1246 self.index = index 1247 self.x = x 1248 self.y = y 1249 self.altx = altx 1250 self.alty = alty 1251 self.hregion = hregion 1252 self.vregion = vregion 1253 1254# Graph data (single graph) base class 1255 1256class GraphData(object): 1257 1258 def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)): 1259 self.collection = collection 1260 self.points = [] 1261 self.xbase = xbase 1262 self.ybase = ybase 1263 self.title = "" 1264 1265 def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None): 1266 index = len(self.points) 1267 1268 x = float(Decimal(x) - self.xbase) 1269 y = float(Decimal(y) - self.ybase) 1270 1271 self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion)) 1272 1273 def XToData(self, x): 1274 return Decimal(x) + self.xbase 1275 1276 def YToData(self, y): 1277 return Decimal(y) + self.ybase 1278 1279# Switch graph data (for one CPU) 1280 1281class SwitchGraphData(GraphData): 1282 1283 def __init__(self, db, collection, cpu, xbase): 1284 super(SwitchGraphData, self).__init__(collection, xbase) 1285 1286 self.cpu = cpu 1287 self.title = "CPU " + str(cpu) 1288 self.SelectSwitches(db) 1289 1290 def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time): 1291 query = QSqlQuery(db) 1292 QueryExec(query, "SELECT id, c_time" 1293 " FROM comms" 1294 " WHERE c_thread_id = " + str(thread_id) + 1295 " AND exec_flag = " + self.collection.glb.dbref.TRUE + 1296 " AND c_time >= " + str(start_time) + 1297 " AND c_time <= " + str(end_time) + 1298 " ORDER BY c_time, id") 1299 while query.next(): 1300 comm_id = query.value(0) 1301 if comm_id == last_comm_id: 1302 continue 1303 time = query.value(1) 1304 hregion = self.HRegion(db, thread_id, comm_id, time) 1305 self.AddPoint(time, 1000, None, None, hregion) 1306 1307 def SelectSwitches(self, db): 1308 last_time = None 1309 last_comm_id = None 1310 last_thread_id = None 1311 query = QSqlQuery(db) 1312 QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags" 1313 " FROM context_switches" 1314 " WHERE machine_id = " + str(self.collection.machine_id) + 1315 " AND cpu = " + str(self.cpu) + 1316 " ORDER BY time, id") 1317 while query.next(): 1318 flags = int(query.value(5)) 1319 if flags & 1: 1320 # Schedule-out: detect and add exec's 1321 if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3): 1322 self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0)) 1323 continue 1324 # Schedule-in: add data point 1325 if len(self.points) == 0: 1326 start_time = self.collection.glb.StartTime(self.collection.machine_id) 1327 hregion = self.HRegion(db, query.value(1), query.value(3), start_time) 1328 self.AddPoint(start_time, 1000, None, None, hregion) 1329 time = query.value(0) 1330 comm_id = query.value(4) 1331 thread_id = query.value(2) 1332 hregion = self.HRegion(db, thread_id, comm_id, time) 1333 self.AddPoint(time, 1000, None, None, hregion) 1334 last_time = time 1335 last_comm_id = comm_id 1336 last_thread_id = thread_id 1337 1338 def NewHRegion(self, db, key, thread_id, comm_id, time): 1339 exec_comm_id = ExecComm(db, thread_id, time) 1340 query = QSqlQuery(db) 1341 QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id)) 1342 if query.next(): 1343 pid = query.value(0) 1344 tid = query.value(1) 1345 else: 1346 pid = -1 1347 tid = -1 1348 query = QSqlQuery(db) 1349 QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id)) 1350 if query.next(): 1351 comm = query.value(0) 1352 else: 1353 comm = "" 1354 return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id) 1355 1356 def HRegion(self, db, thread_id, comm_id, time): 1357 key = str(thread_id) + ":" + str(comm_id) 1358 hregion = self.collection.LookupHRegion(key) 1359 if hregion is None: 1360 hregion = self.NewHRegion(db, key, thread_id, comm_id, time) 1361 self.collection.AddHRegion(key, hregion) 1362 return hregion 1363 1364# Graph data collection (multiple related graphs) base class 1365 1366class GraphDataCollection(object): 1367 1368 def __init__(self, glb): 1369 self.glb = glb 1370 self.data = [] 1371 self.hregions = {} 1372 self.xrangelo = None 1373 self.xrangehi = None 1374 self.yrangelo = None 1375 self.yrangehi = None 1376 self.dp = XY(0, 0) 1377 1378 def AddGraphData(self, data): 1379 self.data.append(data) 1380 1381 def LookupHRegion(self, key): 1382 if key in self.hregions: 1383 return self.hregions[key] 1384 return None 1385 1386 def AddHRegion(self, key, hregion): 1387 self.hregions[key] = hregion 1388 1389# Switch graph data collection (SwitchGraphData for each CPU) 1390 1391class SwitchGraphDataCollection(GraphDataCollection): 1392 1393 def __init__(self, glb, db, machine_id): 1394 super(SwitchGraphDataCollection, self).__init__(glb) 1395 1396 self.machine_id = machine_id 1397 self.cpus = self.SelectCPUs(db) 1398 1399 self.xrangelo = glb.StartTime(machine_id) 1400 self.xrangehi = glb.FinishTime(machine_id) 1401 1402 self.yrangelo = Decimal(0) 1403 self.yrangehi = Decimal(1000) 1404 1405 for cpu in self.cpus: 1406 self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo)) 1407 1408 def SelectCPUs(self, db): 1409 cpus = [] 1410 query = QSqlQuery(db) 1411 QueryExec(query, "SELECT DISTINCT cpu" 1412 " FROM context_switches" 1413 " WHERE machine_id = " + str(self.machine_id)) 1414 while query.next(): 1415 cpus.append(int(query.value(0))) 1416 return sorted(cpus) 1417 1418# Switch graph data graphics item displays the graphed data 1419 1420class SwitchGraphDataGraphicsItem(QGraphicsItem): 1421 1422 def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None): 1423 super(SwitchGraphDataGraphicsItem, self).__init__(parent) 1424 1425 self.data = data 1426 self.graph_width = graph_width 1427 self.graph_height = graph_height 1428 self.attrs = attrs 1429 self.event_handler = event_handler 1430 self.setAcceptHoverEvents(True) 1431 1432 def boundingRect(self): 1433 return QRectF(0, 0, self.graph_width, self.graph_height) 1434 1435 def PaintPoint(self, painter, last, x): 1436 if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo): 1437 if last.x < self.attrs.subrange.x.lo: 1438 x0 = self.attrs.subrange.x.lo 1439 else: 1440 x0 = last.x 1441 if x > self.attrs.subrange.x.hi: 1442 x1 = self.attrs.subrange.x.hi 1443 else: 1444 x1 = x - 1 1445 x0 = self.attrs.XToPixel(x0) 1446 x1 = self.attrs.XToPixel(x1) 1447 1448 y0 = self.attrs.YToPixel(last.y) 1449 1450 colour = self.attrs.region_attributes[last.hregion.key].colour 1451 1452 width = x1 - x0 + 1 1453 if width < 2: 1454 painter.setPen(colour) 1455 painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height) 1456 else: 1457 painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour) 1458 1459 def paint(self, painter, option, widget): 1460 last = None 1461 for point in self.data.points: 1462 self.PaintPoint(painter, last, point.x) 1463 if point.x > self.attrs.subrange.x.hi: 1464 break; 1465 last = point 1466 self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1) 1467 1468 def BinarySearchPoint(self, target): 1469 lower_pos = 0 1470 higher_pos = len(self.data.points) 1471 while True: 1472 pos = int((lower_pos + higher_pos) / 2) 1473 val = self.data.points[pos].x 1474 if target >= val: 1475 lower_pos = pos 1476 else: 1477 higher_pos = pos 1478 if higher_pos <= lower_pos + 1: 1479 return lower_pos 1480 1481 def XPixelToData(self, x): 1482 x = self.attrs.PixelToX(x) 1483 if x < self.data.points[0].x: 1484 x = 0 1485 pos = 0 1486 low = True 1487 else: 1488 pos = self.BinarySearchPoint(x) 1489 low = False 1490 return (low, pos, self.data.XToData(x)) 1491 1492 def EventToData(self, event): 1493 no_data = (None,) * 4 1494 if len(self.data.points) < 1: 1495 return no_data 1496 x = event.pos().x() 1497 if x < 0: 1498 return no_data 1499 low0, pos0, time_from = self.XPixelToData(x) 1500 low1, pos1, time_to = self.XPixelToData(x + 1) 1501 hregions = set() 1502 hregion_times = [] 1503 if not low1: 1504 for i in xrange(pos0, pos1 + 1): 1505 hregion = self.data.points[i].hregion 1506 hregions.add(hregion) 1507 if i == pos0: 1508 time = time_from 1509 else: 1510 time = self.data.XToData(self.data.points[i].x) 1511 hregion_times.append((hregion, time)) 1512 return (time_from, time_to, hregions, hregion_times) 1513 1514 def hoverMoveEvent(self, event): 1515 time_from, time_to, hregions, hregion_times = self.EventToData(event) 1516 if time_from is not None: 1517 self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions) 1518 1519 def hoverLeaveEvent(self, event): 1520 self.event_handler.NoPointEvent() 1521 1522 def mousePressEvent(self, event): 1523 if event.button() != Qt.RightButton: 1524 super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event) 1525 return 1526 time_from, time_to, hregions, hregion_times = self.EventToData(event) 1527 if hregion_times: 1528 self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos()) 1529 1530# X-axis graphics item 1531 1532class XAxisGraphicsItem(QGraphicsItem): 1533 1534 def __init__(self, width, parent=None): 1535 super(XAxisGraphicsItem, self).__init__(parent) 1536 1537 self.width = width 1538 self.max_mark_sz = 4 1539 self.height = self.max_mark_sz + 1 1540 1541 def boundingRect(self): 1542 return QRectF(0, 0, self.width, self.height) 1543 1544 def Step(self): 1545 attrs = self.parentItem().attrs 1546 subrange = attrs.subrange.x 1547 t = subrange.hi - subrange.lo 1548 s = (3.0 * t) / self.width 1549 n = 1.0 1550 while s > n: 1551 n = n * 10.0 1552 return n 1553 1554 def PaintMarks(self, painter, at_y, lo, hi, step, i): 1555 attrs = self.parentItem().attrs 1556 x = lo 1557 while x <= hi: 1558 xp = attrs.XToPixel(x) 1559 if i % 10: 1560 if i % 5: 1561 sz = 1 1562 else: 1563 sz = 2 1564 else: 1565 sz = self.max_mark_sz 1566 i = 0 1567 painter.drawLine(xp, at_y, xp, at_y + sz) 1568 x += step 1569 i += 1 1570 1571 def paint(self, painter, option, widget): 1572 # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1 1573 painter.drawLine(0, 0, self.width - 1, 0) 1574 n = self.Step() 1575 attrs = self.parentItem().attrs 1576 subrange = attrs.subrange.x 1577 if subrange.lo: 1578 x_offset = n - (subrange.lo % n) 1579 else: 1580 x_offset = 0.0 1581 x = subrange.lo + x_offset 1582 i = (x / n) % 10 1583 self.PaintMarks(painter, 0, x, subrange.hi, n, i) 1584 1585 def ScaleDimensions(self): 1586 n = self.Step() 1587 attrs = self.parentItem().attrs 1588 lo = attrs.subrange.x.lo 1589 hi = (n * 10.0) + lo 1590 width = attrs.XToPixel(hi) 1591 if width > 500: 1592 width = 0 1593 return (n, lo, hi, width) 1594 1595 def PaintScale(self, painter, at_x, at_y): 1596 n, lo, hi, width = self.ScaleDimensions() 1597 if not width: 1598 return 1599 painter.drawLine(at_x, at_y, at_x + width, at_y) 1600 self.PaintMarks(painter, at_y, lo, hi, n, 0) 1601 1602 def ScaleWidth(self): 1603 n, lo, hi, width = self.ScaleDimensions() 1604 return width 1605 1606 def ScaleHeight(self): 1607 return self.height 1608 1609 def ScaleUnit(self): 1610 return self.Step() * 10 1611 1612# Scale graphics item base class 1613 1614class ScaleGraphicsItem(QGraphicsItem): 1615 1616 def __init__(self, axis, parent=None): 1617 super(ScaleGraphicsItem, self).__init__(parent) 1618 self.axis = axis 1619 1620 def boundingRect(self): 1621 scale_width = self.axis.ScaleWidth() 1622 if not scale_width: 1623 return QRectF() 1624 return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight()) 1625 1626 def paint(self, painter, option, widget): 1627 scale_width = self.axis.ScaleWidth() 1628 if not scale_width: 1629 return 1630 self.axis.PaintScale(painter, 0, 5) 1631 x = scale_width + 4 1632 painter.drawText(QPointF(x, 10), self.Text()) 1633 1634 def Unit(self): 1635 return self.axis.ScaleUnit() 1636 1637 def Text(self): 1638 return "" 1639 1640# Switch graph scale graphics item 1641 1642class SwitchScaleGraphicsItem(ScaleGraphicsItem): 1643 1644 def __init__(self, axis, parent=None): 1645 super(SwitchScaleGraphicsItem, self).__init__(axis, parent) 1646 1647 def Text(self): 1648 unit = self.Unit() 1649 if unit >= 1000000000: 1650 unit = int(unit / 1000000000) 1651 us = "s" 1652 elif unit >= 1000000: 1653 unit = int(unit / 1000000) 1654 us = "ms" 1655 elif unit >= 1000: 1656 unit = int(unit / 1000) 1657 us = "us" 1658 else: 1659 unit = int(unit) 1660 us = "ns" 1661 return " = " + str(unit) + " " + us 1662 1663# Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data 1664 1665class SwitchGraphGraphicsItem(QGraphicsItem): 1666 1667 def __init__(self, collection, data, attrs, event_handler, first, parent=None): 1668 super(SwitchGraphGraphicsItem, self).__init__(parent) 1669 self.collection = collection 1670 self.data = data 1671 self.attrs = attrs 1672 self.event_handler = event_handler 1673 1674 margin = 20 1675 title_width = 50 1676 1677 self.title_graphics = QGraphicsSimpleTextItem(data.title, self) 1678 1679 self.title_graphics.setPos(margin, margin) 1680 graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1 1681 graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1 1682 1683 self.graph_origin_x = margin + title_width + margin 1684 self.graph_origin_y = graph_height + margin 1685 1686 x_axis_size = 1 1687 y_axis_size = 1 1688 self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self) 1689 1690 self.x_axis = XAxisGraphicsItem(graph_width, self) 1691 self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1) 1692 1693 if first: 1694 self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self) 1695 self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10) 1696 1697 self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height) 1698 1699 self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self) 1700 self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1) 1701 1702 self.width = self.graph_origin_x + graph_width + margin 1703 self.height = self.graph_origin_y + margin 1704 1705 self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self) 1706 self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height) 1707 1708 if parent and 'EnableRubberBand' in dir(parent): 1709 parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self) 1710 1711 def boundingRect(self): 1712 return QRectF(0, 0, self.width, self.height) 1713 1714 def paint(self, painter, option, widget): 1715 pass 1716 1717 def RBXToPixel(self, x): 1718 return self.attrs.PixelToX(x - self.graph_origin_x) 1719 1720 def RBXRangeToPixel(self, x0, x1): 1721 return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1)) 1722 1723 def RBPixelToTime(self, x): 1724 if x < self.data.points[0].x: 1725 return self.data.XToData(0) 1726 return self.data.XToData(x) 1727 1728 def RBEventTimes(self, x0, x1): 1729 x0, x1 = self.RBXRangeToPixel(x0, x1) 1730 time_from = self.RBPixelToTime(x0) 1731 time_to = self.RBPixelToTime(x1) 1732 return (time_from, time_to) 1733 1734 def RBEvent(self, x0, x1): 1735 time_from, time_to = self.RBEventTimes(x0, x1) 1736 self.event_handler.RangeEvent(time_from, time_to) 1737 1738 def RBMoveEvent(self, x0, x1): 1739 if x1 < x0: 1740 x0, x1 = x1, x0 1741 self.RBEvent(x0, x1) 1742 1743 def RBReleaseEvent(self, x0, x1, selection_state): 1744 if x1 < x0: 1745 x0, x1 = x1, x0 1746 x0, x1 = self.RBXRangeToPixel(x0, x1) 1747 self.event_handler.SelectEvent(x0, x1, selection_state) 1748 1749# Graphics item to draw a vertical bracket (used to highlight "forward" sub-range) 1750 1751class VerticalBracketGraphicsItem(QGraphicsItem): 1752 1753 def __init__(self, parent=None): 1754 super(VerticalBracketGraphicsItem, self).__init__(parent) 1755 1756 self.width = 0 1757 self.height = 0 1758 self.hide() 1759 1760 def SetSize(self, width, height): 1761 self.width = width + 1 1762 self.height = height + 1 1763 1764 def boundingRect(self): 1765 return QRectF(0, 0, self.width, self.height) 1766 1767 def paint(self, painter, option, widget): 1768 colour = QColor(255, 255, 0, 32) 1769 painter.fillRect(0, 0, self.width, self.height, colour) 1770 x1 = self.width - 1 1771 y1 = self.height - 1 1772 painter.drawLine(0, 0, x1, 0) 1773 painter.drawLine(0, 0, 0, 3) 1774 painter.drawLine(x1, 0, x1, 3) 1775 painter.drawLine(0, y1, x1, y1) 1776 painter.drawLine(0, y1, 0, y1 - 3) 1777 painter.drawLine(x1, y1, x1, y1 - 3) 1778 1779# Graphics item to contain graphs arranged vertically 1780 1781class VertcalGraphSetGraphicsItem(QGraphicsItem): 1782 1783 def __init__(self, collection, attrs, event_handler, child_class, parent=None): 1784 super(VertcalGraphSetGraphicsItem, self).__init__(parent) 1785 1786 self.collection = collection 1787 1788 self.top = 10 1789 1790 self.width = 0 1791 self.height = self.top 1792 1793 self.rubber_band = None 1794 self.rb_enabled = False 1795 1796 first = True 1797 for data in collection.data: 1798 child = child_class(collection, data, attrs, event_handler, first, self) 1799 child.setPos(0, self.height + 1) 1800 rect = child.boundingRect() 1801 if rect.right() > self.width: 1802 self.width = rect.right() 1803 self.height = self.height + rect.bottom() + 1 1804 first = False 1805 1806 self.bracket = VerticalBracketGraphicsItem(self) 1807 1808 def EnableRubberBand(self, xlo, xhi, rb_event_handler): 1809 if self.rb_enabled: 1810 return 1811 self.rb_enabled = True 1812 self.rb_in_view = False 1813 self.setAcceptedMouseButtons(Qt.LeftButton) 1814 self.rb_xlo = xlo 1815 self.rb_xhi = xhi 1816 self.rb_event_handler = rb_event_handler 1817 self.mousePressEvent = self.MousePressEvent 1818 self.mouseMoveEvent = self.MouseMoveEvent 1819 self.mouseReleaseEvent = self.MouseReleaseEvent 1820 1821 def boundingRect(self): 1822 return QRectF(0, 0, self.width, self.height) 1823 1824 def paint(self, painter, option, widget): 1825 pass 1826 1827 def RubberBandParent(self): 1828 scene = self.scene() 1829 view = scene.views()[0] 1830 viewport = view.viewport() 1831 return viewport 1832 1833 def RubberBandSetGeometry(self, rect): 1834 scene_rectf = self.mapRectToScene(QRectF(rect)) 1835 scene = self.scene() 1836 view = scene.views()[0] 1837 poly = view.mapFromScene(scene_rectf) 1838 self.rubber_band.setGeometry(poly.boundingRect()) 1839 1840 def SetSelection(self, selection_state): 1841 if self.rubber_band: 1842 if selection_state: 1843 self.RubberBandSetGeometry(selection_state) 1844 self.rubber_band.show() 1845 else: 1846 self.rubber_band.hide() 1847 1848 def SetBracket(self, rect): 1849 if rect: 1850 x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height() 1851 self.bracket.setPos(x, y) 1852 self.bracket.SetSize(width, height) 1853 self.bracket.show() 1854 else: 1855 self.bracket.hide() 1856 1857 def RubberBandX(self, event): 1858 x = event.pos().toPoint().x() 1859 if x < self.rb_xlo: 1860 x = self.rb_xlo 1861 elif x > self.rb_xhi: 1862 x = self.rb_xhi 1863 else: 1864 self.rb_in_view = True 1865 return x 1866 1867 def RubberBandRect(self, x): 1868 if self.rb_origin.x() <= x: 1869 width = x - self.rb_origin.x() 1870 rect = QRect(self.rb_origin, QSize(width, self.height)) 1871 else: 1872 width = self.rb_origin.x() - x 1873 top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y()) 1874 rect = QRect(top_left, QSize(width, self.height)) 1875 return rect 1876 1877 def MousePressEvent(self, event): 1878 self.rb_in_view = False 1879 x = self.RubberBandX(event) 1880 self.rb_origin = QPoint(x, self.top) 1881 if self.rubber_band is None: 1882 self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent()) 1883 self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height))) 1884 if self.rb_in_view: 1885 self.rubber_band.show() 1886 self.rb_event_handler.RBMoveEvent(x, x) 1887 else: 1888 self.rubber_band.hide() 1889 1890 def MouseMoveEvent(self, event): 1891 x = self.RubberBandX(event) 1892 rect = self.RubberBandRect(x) 1893 self.RubberBandSetGeometry(rect) 1894 if self.rb_in_view: 1895 self.rubber_band.show() 1896 self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x) 1897 1898 def MouseReleaseEvent(self, event): 1899 x = self.RubberBandX(event) 1900 if self.rb_in_view: 1901 selection_state = self.RubberBandRect(x) 1902 else: 1903 selection_state = None 1904 self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state) 1905 1906# Switch graph legend data model 1907 1908class SwitchGraphLegendModel(QAbstractTableModel): 1909 1910 def __init__(self, collection, region_attributes, parent=None): 1911 super(SwitchGraphLegendModel, self).__init__(parent) 1912 1913 self.region_attributes = region_attributes 1914 1915 self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal) 1916 self.child_count = len(self.child_items) 1917 1918 self.highlight_set = set() 1919 1920 self.column_headers = ("pid", "tid", "comm") 1921 1922 def rowCount(self, parent): 1923 return self.child_count 1924 1925 def headerData(self, section, orientation, role): 1926 if role != Qt.DisplayRole: 1927 return None 1928 if orientation != Qt.Horizontal: 1929 return None 1930 return self.columnHeader(section) 1931 1932 def index(self, row, column, parent): 1933 return self.createIndex(row, column, self.child_items[row]) 1934 1935 def columnCount(self, parent=None): 1936 return len(self.column_headers) 1937 1938 def columnHeader(self, column): 1939 return self.column_headers[column] 1940 1941 def data(self, index, role): 1942 if role == Qt.BackgroundRole: 1943 child = self.child_items[index.row()] 1944 if child in self.highlight_set: 1945 return self.region_attributes[child.key].colour 1946 return None 1947 if role == Qt.ForegroundRole: 1948 child = self.child_items[index.row()] 1949 if child in self.highlight_set: 1950 return QColor(255, 255, 255) 1951 return self.region_attributes[child.key].colour 1952 if role != Qt.DisplayRole: 1953 return None 1954 hregion = self.child_items[index.row()] 1955 col = index.column() 1956 if col == 0: 1957 return hregion.pid 1958 if col == 1: 1959 return hregion.tid 1960 if col == 2: 1961 return hregion.comm 1962 return None 1963 1964 def SetHighlight(self, row, set_highlight): 1965 child = self.child_items[row] 1966 top_left = self.createIndex(row, 0, child) 1967 bottom_right = self.createIndex(row, len(self.column_headers) - 1, child) 1968 self.dataChanged.emit(top_left, bottom_right) 1969 1970 def Highlight(self, highlight_set): 1971 for row in xrange(self.child_count): 1972 child = self.child_items[row] 1973 if child in self.highlight_set: 1974 if child not in highlight_set: 1975 self.SetHighlight(row, False) 1976 elif child in highlight_set: 1977 self.SetHighlight(row, True) 1978 self.highlight_set = highlight_set 1979 1980# Switch graph legend is a table 1981 1982class SwitchGraphLegend(QWidget): 1983 1984 def __init__(self, collection, region_attributes, parent=None): 1985 super(SwitchGraphLegend, self).__init__(parent) 1986 1987 self.data_model = SwitchGraphLegendModel(collection, region_attributes) 1988 1989 self.model = QSortFilterProxyModel() 1990 self.model.setSourceModel(self.data_model) 1991 1992 self.view = QTableView() 1993 self.view.setModel(self.model) 1994 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) 1995 self.view.verticalHeader().setVisible(False) 1996 self.view.sortByColumn(-1, Qt.AscendingOrder) 1997 self.view.setSortingEnabled(True) 1998 self.view.resizeColumnsToContents() 1999 self.view.resizeRowsToContents() 2000 2001 self.vbox = VBoxLayout(self.view) 2002 self.setLayout(self.vbox) 2003 2004 sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2 2005 sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width() 2006 self.saved_size = sz1 2007 2008 def resizeEvent(self, event): 2009 self.saved_size = self.size().width() 2010 super(SwitchGraphLegend, self).resizeEvent(event) 2011 2012 def Highlight(self, highlight_set): 2013 self.data_model.Highlight(highlight_set) 2014 self.update() 2015 2016 def changeEvent(self, event): 2017 if event.type() == QEvent.FontChange: 2018 self.view.resizeRowsToContents() 2019 self.view.resizeColumnsToContents() 2020 # Need to resize rows again after column resize 2021 self.view.resizeRowsToContents() 2022 super(SwitchGraphLegend, self).changeEvent(event) 2023 2024# Random colour generation 2025 2026def RGBColourTooLight(r, g, b): 2027 if g > 230: 2028 return True 2029 if g <= 160: 2030 return False 2031 if r <= 180 and g <= 180: 2032 return False 2033 if r < 60: 2034 return False 2035 return True 2036 2037def GenerateColours(x): 2038 cs = [0] 2039 for i in xrange(1, x): 2040 cs.append(int((255.0 / i) + 0.5)) 2041 colours = [] 2042 for r in cs: 2043 for g in cs: 2044 for b in cs: 2045 # Exclude black and colours that look too light against a white background 2046 if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b): 2047 continue 2048 colours.append(QColor(r, g, b)) 2049 return colours 2050 2051def GenerateNColours(n): 2052 for x in xrange(2, n + 2): 2053 colours = GenerateColours(x) 2054 if len(colours) >= n: 2055 return colours 2056 return [] 2057 2058def GenerateNRandomColours(n, seed): 2059 colours = GenerateNColours(n) 2060 random.seed(seed) 2061 random.shuffle(colours) 2062 return colours 2063 2064# Graph attributes, in particular the scale and subrange that change when zooming 2065 2066class GraphAttributes(): 2067 2068 def __init__(self, scale, subrange, region_attributes, dp): 2069 self.scale = scale 2070 self.subrange = subrange 2071 self.region_attributes = region_attributes 2072 # Rounding avoids errors due to finite floating point precision 2073 self.dp = dp # data decimal places 2074 self.Update() 2075 2076 def XToPixel(self, x): 2077 return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x)) 2078 2079 def YToPixel(self, y): 2080 return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y)) 2081 2082 def PixelToXRounded(self, px): 2083 return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo 2084 2085 def PixelToYRounded(self, py): 2086 return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo 2087 2088 def PixelToX(self, px): 2089 x = self.PixelToXRounded(px) 2090 if self.pdp.x == 0: 2091 rt = self.XToPixel(x) 2092 if rt > px: 2093 return x - 1 2094 return x 2095 2096 def PixelToY(self, py): 2097 y = self.PixelToYRounded(py) 2098 if self.pdp.y == 0: 2099 rt = self.YToPixel(y) 2100 if rt > py: 2101 return y - 1 2102 return y 2103 2104 def ToPDP(self, dp, scale): 2105 # Calculate pixel decimal places: 2106 # (10 ** dp) is the minimum delta in the data 2107 # scale it to get the minimum delta in pixels 2108 # log10 gives the number of decimals places negatively 2109 # subtrace 1 to divide by 10 2110 # round to the lower negative number 2111 # change the sign to get the number of decimals positively 2112 x = math.log10((10 ** dp) * scale) 2113 if x < 0: 2114 x -= 1 2115 x = -int(math.floor(x) - 0.1) 2116 else: 2117 x = 0 2118 return x 2119 2120 def Update(self): 2121 x = self.ToPDP(self.dp.x, self.scale.x) 2122 y = self.ToPDP(self.dp.y, self.scale.y) 2123 self.pdp = XY(x, y) # pixel decimal places 2124 2125# Switch graph splitter which divides the CPU graphs from the legend 2126 2127class SwitchGraphSplitter(QSplitter): 2128 2129 def __init__(self, parent=None): 2130 super(SwitchGraphSplitter, self).__init__(parent) 2131 2132 self.first_time = False 2133 2134 def resizeEvent(self, ev): 2135 if self.first_time: 2136 self.first_time = False 2137 sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2 2138 sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width() 2139 sz0 = self.size().width() - self.handleWidth() - sz1 2140 self.setSizes([sz0, sz1]) 2141 elif not(self.widget(1).saved_size is None): 2142 sz1 = self.widget(1).saved_size 2143 sz0 = self.size().width() - self.handleWidth() - sz1 2144 self.setSizes([sz0, sz1]) 2145 super(SwitchGraphSplitter, self).resizeEvent(ev) 2146 2147# Graph widget base class 2148 2149class GraphWidget(QWidget): 2150 2151 graph_title_changed = Signal(object) 2152 2153 def __init__(self, parent=None): 2154 super(GraphWidget, self).__init__(parent) 2155 2156 def GraphTitleChanged(self, title): 2157 self.graph_title_changed.emit(title) 2158 2159 def Title(self): 2160 return "" 2161 2162# Display time in s, ms, us or ns 2163 2164def ToTimeStr(val): 2165 val = Decimal(val) 2166 if val >= 1000000000: 2167 return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001"))) 2168 if val >= 1000000: 2169 return "{} ms".format((val / 1000000).quantize(Decimal("0.000001"))) 2170 if val >= 1000: 2171 return "{} us".format((val / 1000).quantize(Decimal("0.001"))) 2172 return "{} ns".format(val.quantize(Decimal("1"))) 2173 2174# Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons 2175 2176class SwitchGraphWidget(GraphWidget): 2177 2178 def __init__(self, glb, collection, parent=None): 2179 super(SwitchGraphWidget, self).__init__(parent) 2180 2181 self.glb = glb 2182 self.collection = collection 2183 2184 self.back_state = [] 2185 self.forward_state = [] 2186 self.selection_state = (None, None) 2187 self.fwd_rect = None 2188 self.start_time = self.glb.StartTime(collection.machine_id) 2189 2190 i = 0 2191 hregions = collection.hregions.values() 2192 colours = GenerateNRandomColours(len(hregions), 1013) 2193 region_attributes = {} 2194 for hregion in hregions: 2195 if hregion.pid == 0 and hregion.tid == 0: 2196 region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0)) 2197 else: 2198 region_attributes[hregion.key] = GraphRegionAttribute(colours[i]) 2199 i = i + 1 2200 2201 # Default to entire range 2202 xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0) 2203 ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0) 2204 subrange = XY(xsubrange, ysubrange) 2205 2206 scale = self.GetScaleForRange(subrange) 2207 2208 self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp) 2209 2210 self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem) 2211 2212 self.scene = QGraphicsScene() 2213 self.scene.addItem(self.item) 2214 2215 self.view = QGraphicsView(self.scene) 2216 self.view.centerOn(0, 0) 2217 self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop) 2218 2219 self.legend = SwitchGraphLegend(collection, region_attributes) 2220 2221 self.splitter = SwitchGraphSplitter() 2222 self.splitter.addWidget(self.view) 2223 self.splitter.addWidget(self.legend) 2224 2225 self.point_label = QLabel("") 2226 self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) 2227 2228 self.back_button = QToolButton() 2229 self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft)) 2230 self.back_button.setDisabled(True) 2231 self.back_button.released.connect(lambda: self.Back()) 2232 2233 self.forward_button = QToolButton() 2234 self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight)) 2235 self.forward_button.setDisabled(True) 2236 self.forward_button.released.connect(lambda: self.Forward()) 2237 2238 self.zoom_button = QToolButton() 2239 self.zoom_button.setText("Zoom") 2240 self.zoom_button.setDisabled(True) 2241 self.zoom_button.released.connect(lambda: self.Zoom()) 2242 2243 self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label) 2244 2245 self.vbox = VBoxLayout(self.splitter, self.hbox) 2246 2247 self.setLayout(self.vbox) 2248 2249 def GetScaleForRangeX(self, xsubrange): 2250 # Default graph 1000 pixels wide 2251 dflt = 1000.0 2252 r = xsubrange.hi - xsubrange.lo 2253 return dflt / r 2254 2255 def GetScaleForRangeY(self, ysubrange): 2256 # Default graph 50 pixels high 2257 dflt = 50.0 2258 r = ysubrange.hi - ysubrange.lo 2259 return dflt / r 2260 2261 def GetScaleForRange(self, subrange): 2262 # Default graph 1000 pixels wide, 50 pixels high 2263 xscale = self.GetScaleForRangeX(subrange.x) 2264 yscale = self.GetScaleForRangeY(subrange.y) 2265 return XY(xscale, yscale) 2266 2267 def PointEvent(self, cpu, time_from, time_to, hregions): 2268 text = "CPU: " + str(cpu) 2269 time_from = time_from.quantize(Decimal(1)) 2270 rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id) 2271 text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")" 2272 self.point_label.setText(text) 2273 self.legend.Highlight(hregions) 2274 2275 def RightClickEvent(self, cpu, hregion_times, pos): 2276 if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"): 2277 return 2278 menu = QMenu(self.view) 2279 for hregion, time in hregion_times: 2280 thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time) 2281 menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time) 2282 menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view)) 2283 menu.exec_(pos) 2284 2285 def RightClickSelect(self, args): 2286 CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args) 2287 2288 def NoPointEvent(self): 2289 self.point_label.setText("") 2290 self.legend.Highlight({}) 2291 2292 def RangeEvent(self, time_from, time_to): 2293 time_from = time_from.quantize(Decimal(1)) 2294 time_to = time_to.quantize(Decimal(1)) 2295 if time_to <= time_from: 2296 self.point_label.setText("") 2297 return 2298 rel_time_from = time_from - self.start_time 2299 rel_time_to = time_to - self.start_time 2300 text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")" 2301 text = text + " duration: " + ToTimeStr(time_to - time_from) 2302 self.point_label.setText(text) 2303 2304 def BackState(self): 2305 return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect) 2306 2307 def PushBackState(self): 2308 state = copy.deepcopy(self.BackState()) 2309 self.back_state.append(state) 2310 self.back_button.setEnabled(True) 2311 2312 def PopBackState(self): 2313 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop() 2314 self.attrs.Update() 2315 if not self.back_state: 2316 self.back_button.setDisabled(True) 2317 2318 def PushForwardState(self): 2319 state = copy.deepcopy(self.BackState()) 2320 self.forward_state.append(state) 2321 self.forward_button.setEnabled(True) 2322 2323 def PopForwardState(self): 2324 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop() 2325 self.attrs.Update() 2326 if not self.forward_state: 2327 self.forward_button.setDisabled(True) 2328 2329 def Title(self): 2330 time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo) 2331 time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi) 2332 rel_time_from = time_from - self.start_time 2333 rel_time_to = time_to - self.start_time 2334 title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to) 2335 title = title + " (" + ToTimeStr(time_to - time_from) + ")" 2336 return title 2337 2338 def Update(self): 2339 selected_subrange, selection_state = self.selection_state 2340 self.item.SetSelection(selection_state) 2341 self.item.SetBracket(self.fwd_rect) 2342 self.zoom_button.setDisabled(selected_subrange is None) 2343 self.GraphTitleChanged(self.Title()) 2344 self.item.update(self.item.boundingRect()) 2345 2346 def Back(self): 2347 if not self.back_state: 2348 return 2349 self.PushForwardState() 2350 self.PopBackState() 2351 self.Update() 2352 2353 def Forward(self): 2354 if not self.forward_state: 2355 return 2356 self.PushBackState() 2357 self.PopForwardState() 2358 self.Update() 2359 2360 def SelectEvent(self, x0, x1, selection_state): 2361 if selection_state is None: 2362 selected_subrange = None 2363 else: 2364 if x1 - x0 < 1.0: 2365 x1 += 1.0 2366 selected_subrange = Subrange(x0, x1) 2367 self.selection_state = (selected_subrange, selection_state) 2368 self.zoom_button.setDisabled(selected_subrange is None) 2369 2370 def Zoom(self): 2371 selected_subrange, selection_state = self.selection_state 2372 if selected_subrange is None: 2373 return 2374 self.fwd_rect = selection_state 2375 self.item.SetSelection(None) 2376 self.PushBackState() 2377 self.attrs.subrange.x = selected_subrange 2378 self.forward_state = [] 2379 self.forward_button.setDisabled(True) 2380 self.selection_state = (None, None) 2381 self.fwd_rect = None 2382 self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x) 2383 self.attrs.Update() 2384 self.Update() 2385 2386# Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting 2387 2388class SlowInitClass(): 2389 2390 def __init__(self, glb, title, init_fn): 2391 self.init_fn = init_fn 2392 self.done = False 2393 self.result = None 2394 2395 self.msg_box = QMessageBox(glb.mainwindow) 2396 self.msg_box.setText("Initializing " + title + ". Please wait.") 2397 self.msg_box.setWindowTitle("Initializing " + title) 2398 self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation)) 2399 2400 self.init_thread = Thread(self.ThreadFn, glb) 2401 self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection) 2402 2403 self.init_thread.start() 2404 2405 def Done(self): 2406 self.msg_box.done(0) 2407 2408 def ThreadFn(self, glb): 2409 conn_name = "SlowInitClass" + str(os.getpid()) 2410 db, dbname = glb.dbref.Open(conn_name) 2411 self.result = self.init_fn(db) 2412 self.done = True 2413 return (True, 0) 2414 2415 def Result(self): 2416 while not self.done: 2417 self.msg_box.exec_() 2418 self.init_thread.wait() 2419 return self.result 2420 2421def SlowInit(glb, title, init_fn): 2422 init = SlowInitClass(glb, title, init_fn) 2423 return init.Result() 2424 2425# Time chart by CPU window 2426 2427class TimeChartByCPUWindow(QMdiSubWindow): 2428 2429 def __init__(self, glb, parent=None): 2430 super(TimeChartByCPUWindow, self).__init__(parent) 2431 2432 self.glb = glb 2433 self.machine_id = glb.HostMachineId() 2434 self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id) 2435 2436 collection = LookupModel(self.collection_name) 2437 if collection is None: 2438 collection = SlowInit(glb, "Time Chart", self.Init) 2439 2440 self.widget = SwitchGraphWidget(glb, collection, self) 2441 self.view = self.widget 2442 2443 self.base_title = "Time Chart by CPU" 2444 self.setWindowTitle(self.base_title + self.widget.Title()) 2445 self.widget.graph_title_changed.connect(self.GraphTitleChanged) 2446 2447 self.setWidget(self.widget) 2448 2449 AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle()) 2450 2451 def Init(self, db): 2452 return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id)) 2453 2454 def GraphTitleChanged(self, title): 2455 self.setWindowTitle(self.base_title + " : " + title) 2456 2457# Child data item finder 2458 2459class ChildDataItemFinder(): 2460 2461 def __init__(self, root): 2462 self.root = root 2463 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5 2464 self.rows = [] 2465 self.pos = 0 2466 2467 def FindSelect(self): 2468 self.rows = [] 2469 if self.pattern: 2470 pattern = re.compile(self.value) 2471 for child in self.root.child_items: 2472 for column_data in child.data: 2473 if re.search(pattern, str(column_data)) is not None: 2474 self.rows.append(child.row) 2475 break 2476 else: 2477 for child in self.root.child_items: 2478 for column_data in child.data: 2479 if self.value in str(column_data): 2480 self.rows.append(child.row) 2481 break 2482 2483 def FindValue(self): 2484 self.pos = 0 2485 if self.last_value != self.value or self.pattern != self.last_pattern: 2486 self.FindSelect() 2487 if not len(self.rows): 2488 return -1 2489 return self.rows[self.pos] 2490 2491 def FindThread(self): 2492 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern: 2493 row = self.FindValue() 2494 elif len(self.rows): 2495 if self.direction > 0: 2496 self.pos += 1 2497 if self.pos >= len(self.rows): 2498 self.pos = 0 2499 else: 2500 self.pos -= 1 2501 if self.pos < 0: 2502 self.pos = len(self.rows) - 1 2503 row = self.rows[self.pos] 2504 else: 2505 row = -1 2506 return (True, row) 2507 2508 def Find(self, value, direction, pattern, context, callback): 2509 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern) 2510 # Use a thread so the UI is not blocked 2511 thread = Thread(self.FindThread) 2512 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection) 2513 thread.start() 2514 2515 def FindDone(self, thread, callback, row): 2516 callback(row) 2517 2518# Number of database records to fetch in one go 2519 2520glb_chunk_sz = 10000 2521 2522# Background process for SQL data fetcher 2523 2524class SQLFetcherProcess(): 2525 2526 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep): 2527 # Need a unique connection name 2528 conn_name = "SQLFetcher" + str(os.getpid()) 2529 self.db, dbname = dbref.Open(conn_name) 2530 self.sql = sql 2531 self.buffer = buffer 2532 self.head = head 2533 self.tail = tail 2534 self.fetch_count = fetch_count 2535 self.fetching_done = fetching_done 2536 self.process_target = process_target 2537 self.wait_event = wait_event 2538 self.fetched_event = fetched_event 2539 self.prep = prep 2540 self.query = QSqlQuery(self.db) 2541 self.query_limit = 0 if "$$last_id$$" in sql else 2 2542 self.last_id = -1 2543 self.fetched = 0 2544 self.more = True 2545 self.local_head = self.head.value 2546 self.local_tail = self.tail.value 2547 2548 def Select(self): 2549 if self.query_limit: 2550 if self.query_limit == 1: 2551 return 2552 self.query_limit -= 1 2553 stmt = self.sql.replace("$$last_id$$", str(self.last_id)) 2554 QueryExec(self.query, stmt) 2555 2556 def Next(self): 2557 if not self.query.next(): 2558 self.Select() 2559 if not self.query.next(): 2560 return None 2561 self.last_id = self.query.value(0) 2562 return self.prep(self.query) 2563 2564 def WaitForTarget(self): 2565 while True: 2566 self.wait_event.clear() 2567 target = self.process_target.value 2568 if target > self.fetched or target < 0: 2569 break 2570 self.wait_event.wait() 2571 return target 2572 2573 def HasSpace(self, sz): 2574 if self.local_tail <= self.local_head: 2575 space = len(self.buffer) - self.local_head 2576 if space > sz: 2577 return True 2578 if space >= glb_nsz: 2579 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer 2580 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL) 2581 self.buffer[self.local_head : self.local_head + len(nd)] = nd 2582 self.local_head = 0 2583 if self.local_tail - self.local_head > sz: 2584 return True 2585 return False 2586 2587 def WaitForSpace(self, sz): 2588 if self.HasSpace(sz): 2589 return 2590 while True: 2591 self.wait_event.clear() 2592 self.local_tail = self.tail.value 2593 if self.HasSpace(sz): 2594 return 2595 self.wait_event.wait() 2596 2597 def AddToBuffer(self, obj): 2598 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL) 2599 n = len(d) 2600 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL) 2601 sz = n + glb_nsz 2602 self.WaitForSpace(sz) 2603 pos = self.local_head 2604 self.buffer[pos : pos + len(nd)] = nd 2605 self.buffer[pos + glb_nsz : pos + sz] = d 2606 self.local_head += sz 2607 2608 def FetchBatch(self, batch_size): 2609 fetched = 0 2610 while batch_size > fetched: 2611 obj = self.Next() 2612 if obj is None: 2613 self.more = False 2614 break 2615 self.AddToBuffer(obj) 2616 fetched += 1 2617 if fetched: 2618 self.fetched += fetched 2619 with self.fetch_count.get_lock(): 2620 self.fetch_count.value += fetched 2621 self.head.value = self.local_head 2622 self.fetched_event.set() 2623 2624 def Run(self): 2625 while self.more: 2626 target = self.WaitForTarget() 2627 if target < 0: 2628 break 2629 batch_size = min(glb_chunk_sz, target - self.fetched) 2630 self.FetchBatch(batch_size) 2631 self.fetching_done.value = True 2632 self.fetched_event.set() 2633 2634def SQLFetcherFn(*x): 2635 process = SQLFetcherProcess(*x) 2636 process.Run() 2637 2638# SQL data fetcher 2639 2640class SQLFetcher(QObject): 2641 2642 done = Signal(object) 2643 2644 def __init__(self, glb, sql, prep, process_data, parent=None): 2645 super(SQLFetcher, self).__init__(parent) 2646 self.process_data = process_data 2647 self.more = True 2648 self.target = 0 2649 self.last_target = 0 2650 self.fetched = 0 2651 self.buffer_size = 16 * 1024 * 1024 2652 self.buffer = Array(c_char, self.buffer_size, lock=False) 2653 self.head = Value(c_longlong) 2654 self.tail = Value(c_longlong) 2655 self.local_tail = 0 2656 self.fetch_count = Value(c_longlong) 2657 self.fetching_done = Value(c_bool) 2658 self.last_count = 0 2659 self.process_target = Value(c_longlong) 2660 self.wait_event = Event() 2661 self.fetched_event = Event() 2662 glb.AddInstanceToShutdownOnExit(self) 2663 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)) 2664 self.process.start() 2665 self.thread = Thread(self.Thread) 2666 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection) 2667 self.thread.start() 2668 2669 def Shutdown(self): 2670 # Tell the thread and process to exit 2671 self.process_target.value = -1 2672 self.wait_event.set() 2673 self.more = False 2674 self.fetching_done.value = True 2675 self.fetched_event.set() 2676 2677 def Thread(self): 2678 if not self.more: 2679 return True, 0 2680 while True: 2681 self.fetched_event.clear() 2682 fetch_count = self.fetch_count.value 2683 if fetch_count != self.last_count: 2684 break 2685 if self.fetching_done.value: 2686 self.more = False 2687 return True, 0 2688 self.fetched_event.wait() 2689 count = fetch_count - self.last_count 2690 self.last_count = fetch_count 2691 self.fetched += count 2692 return False, count 2693 2694 def Fetch(self, nr): 2695 if not self.more: 2696 # -1 inidcates there are no more 2697 return -1 2698 result = self.fetched 2699 extra = result + nr - self.target 2700 if extra > 0: 2701 self.target += extra 2702 # process_target < 0 indicates shutting down 2703 if self.process_target.value >= 0: 2704 self.process_target.value = self.target 2705 self.wait_event.set() 2706 return result 2707 2708 def RemoveFromBuffer(self): 2709 pos = self.local_tail 2710 if len(self.buffer) - pos < glb_nsz: 2711 pos = 0 2712 n = pickle.loads(self.buffer[pos : pos + glb_nsz]) 2713 if n == 0: 2714 pos = 0 2715 n = pickle.loads(self.buffer[0 : glb_nsz]) 2716 pos += glb_nsz 2717 obj = pickle.loads(self.buffer[pos : pos + n]) 2718 self.local_tail = pos + n 2719 return obj 2720 2721 def ProcessData(self, count): 2722 for i in xrange(count): 2723 obj = self.RemoveFromBuffer() 2724 self.process_data(obj) 2725 self.tail.value = self.local_tail 2726 self.wait_event.set() 2727 self.done.emit(count) 2728 2729# Fetch more records bar 2730 2731class FetchMoreRecordsBar(): 2732 2733 def __init__(self, model, parent): 2734 self.model = model 2735 2736 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:") 2737 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 2738 2739 self.fetch_count = QSpinBox() 2740 self.fetch_count.setRange(1, 1000000) 2741 self.fetch_count.setValue(10) 2742 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 2743 2744 self.fetch = QPushButton("Go!") 2745 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 2746 self.fetch.released.connect(self.FetchMoreRecords) 2747 2748 self.progress = QProgressBar() 2749 self.progress.setRange(0, 100) 2750 self.progress.hide() 2751 2752 self.done_label = QLabel("All records fetched") 2753 self.done_label.hide() 2754 2755 self.spacer = QLabel("") 2756 2757 self.close_button = QToolButton() 2758 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) 2759 self.close_button.released.connect(self.Deactivate) 2760 2761 self.hbox = QHBoxLayout() 2762 self.hbox.setContentsMargins(0, 0, 0, 0) 2763 2764 self.hbox.addWidget(self.label) 2765 self.hbox.addWidget(self.fetch_count) 2766 self.hbox.addWidget(self.fetch) 2767 self.hbox.addWidget(self.spacer) 2768 self.hbox.addWidget(self.progress) 2769 self.hbox.addWidget(self.done_label) 2770 self.hbox.addWidget(self.close_button) 2771 2772 self.bar = QWidget() 2773 self.bar.setLayout(self.hbox) 2774 self.bar.show() 2775 2776 self.in_progress = False 2777 self.model.progress.connect(self.Progress) 2778 2779 self.done = False 2780 2781 if not model.HasMoreRecords(): 2782 self.Done() 2783 2784 def Widget(self): 2785 return self.bar 2786 2787 def Activate(self): 2788 self.bar.show() 2789 self.fetch.setFocus() 2790 2791 def Deactivate(self): 2792 self.bar.hide() 2793 2794 def Enable(self, enable): 2795 self.fetch.setEnabled(enable) 2796 self.fetch_count.setEnabled(enable) 2797 2798 def Busy(self): 2799 self.Enable(False) 2800 self.fetch.hide() 2801 self.spacer.hide() 2802 self.progress.show() 2803 2804 def Idle(self): 2805 self.in_progress = False 2806 self.Enable(True) 2807 self.progress.hide() 2808 self.fetch.show() 2809 self.spacer.show() 2810 2811 def Target(self): 2812 return self.fetch_count.value() * glb_chunk_sz 2813 2814 def Done(self): 2815 self.done = True 2816 self.Idle() 2817 self.label.hide() 2818 self.fetch_count.hide() 2819 self.fetch.hide() 2820 self.spacer.hide() 2821 self.done_label.show() 2822 2823 def Progress(self, count): 2824 if self.in_progress: 2825 if count: 2826 percent = ((count - self.start) * 100) / self.Target() 2827 if percent >= 100: 2828 self.Idle() 2829 else: 2830 self.progress.setValue(percent) 2831 if not count: 2832 # Count value of zero means no more records 2833 self.Done() 2834 2835 def FetchMoreRecords(self): 2836 if self.done: 2837 return 2838 self.progress.setValue(0) 2839 self.Busy() 2840 self.in_progress = True 2841 self.start = self.model.FetchMoreRecords(self.Target()) 2842 2843# Brance data model level two item 2844 2845class BranchLevelTwoItem(): 2846 2847 def __init__(self, row, col, text, parent_item): 2848 self.row = row 2849 self.parent_item = parent_item 2850 self.data = [""] * (col + 1) 2851 self.data[col] = text 2852 self.level = 2 2853 2854 def getParentItem(self): 2855 return self.parent_item 2856 2857 def getRow(self): 2858 return self.row 2859 2860 def childCount(self): 2861 return 0 2862 2863 def hasChildren(self): 2864 return False 2865 2866 def getData(self, column): 2867 return self.data[column] 2868 2869# Brance data model level one item 2870 2871class BranchLevelOneItem(): 2872 2873 def __init__(self, glb, row, data, parent_item): 2874 self.glb = glb 2875 self.row = row 2876 self.parent_item = parent_item 2877 self.child_count = 0 2878 self.child_items = [] 2879 self.data = data[1:] 2880 self.dbid = data[0] 2881 self.level = 1 2882 self.query_done = False 2883 self.br_col = len(self.data) - 1 2884 2885 def getChildItem(self, row): 2886 return self.child_items[row] 2887 2888 def getParentItem(self): 2889 return self.parent_item 2890 2891 def getRow(self): 2892 return self.row 2893 2894 def Select(self): 2895 self.query_done = True 2896 2897 if not self.glb.have_disassembler: 2898 return 2899 2900 query = QSqlQuery(self.glb.db) 2901 2902 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip" 2903 " FROM samples" 2904 " INNER JOIN dsos ON samples.to_dso_id = dsos.id" 2905 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id" 2906 " WHERE samples.id = " + str(self.dbid)) 2907 if not query.next(): 2908 return 2909 cpu = query.value(0) 2910 dso = query.value(1) 2911 sym = query.value(2) 2912 if dso == 0 or sym == 0: 2913 return 2914 off = query.value(3) 2915 short_name = query.value(4) 2916 long_name = query.value(5) 2917 build_id = query.value(6) 2918 sym_start = query.value(7) 2919 ip = query.value(8) 2920 2921 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start" 2922 " FROM samples" 2923 " INNER JOIN symbols ON samples.symbol_id = symbols.id" 2924 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) + 2925 " ORDER BY samples.id" 2926 " LIMIT 1") 2927 if not query.next(): 2928 return 2929 if query.value(0) != dso: 2930 # Cannot disassemble from one dso to another 2931 return 2932 bsym = query.value(1) 2933 boff = query.value(2) 2934 bsym_start = query.value(3) 2935 if bsym == 0: 2936 return 2937 tot = bsym_start + boff + 1 - sym_start - off 2938 if tot <= 0 or tot > 16384: 2939 return 2940 2941 inst = self.glb.disassembler.Instruction() 2942 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id) 2943 if not f: 2944 return 2945 mode = 0 if Is64Bit(f) else 1 2946 self.glb.disassembler.SetMode(inst, mode) 2947 2948 buf_sz = tot + 16 2949 buf = create_string_buffer(tot + 16) 2950 f.seek(sym_start + off) 2951 buf.value = f.read(buf_sz) 2952 buf_ptr = addressof(buf) 2953 i = 0 2954 while tot > 0: 2955 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip) 2956 if cnt: 2957 byte_str = tohex(ip).rjust(16) 2958 for k in xrange(cnt): 2959 byte_str += " %02x" % ord(buf[i]) 2960 i += 1 2961 while k < 15: 2962 byte_str += " " 2963 k += 1 2964 self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self)) 2965 self.child_count += 1 2966 else: 2967 return 2968 buf_ptr += cnt 2969 tot -= cnt 2970 buf_sz -= cnt 2971 ip += cnt 2972 2973 def childCount(self): 2974 if not self.query_done: 2975 self.Select() 2976 if not self.child_count: 2977 return -1 2978 return self.child_count 2979 2980 def hasChildren(self): 2981 if not self.query_done: 2982 return True 2983 return self.child_count > 0 2984 2985 def getData(self, column): 2986 return self.data[column] 2987 2988# Brance data model root item 2989 2990class BranchRootItem(): 2991 2992 def __init__(self): 2993 self.child_count = 0 2994 self.child_items = [] 2995 self.level = 0 2996 2997 def getChildItem(self, row): 2998 return self.child_items[row] 2999 3000 def getParentItem(self): 3001 return None 3002 3003 def getRow(self): 3004 return 0 3005 3006 def childCount(self): 3007 return self.child_count 3008 3009 def hasChildren(self): 3010 return self.child_count > 0 3011 3012 def getData(self, column): 3013 return "" 3014 3015# Calculate instructions per cycle 3016 3017def CalcIPC(cyc_cnt, insn_cnt): 3018 if cyc_cnt and insn_cnt: 3019 ipc = Decimal(float(insn_cnt) / cyc_cnt) 3020 ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP)) 3021 else: 3022 ipc = "0" 3023 return ipc 3024 3025# Branch data preparation 3026 3027def BranchDataPrepBr(query, data): 3028 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) + 3029 " (" + dsoname(query.value(11)) + ")" + " -> " + 3030 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) + 3031 " (" + dsoname(query.value(15)) + ")") 3032 3033def BranchDataPrepIPC(query, data): 3034 insn_cnt = query.value(16) 3035 cyc_cnt = query.value(17) 3036 ipc = CalcIPC(cyc_cnt, insn_cnt) 3037 data.append(insn_cnt) 3038 data.append(cyc_cnt) 3039 data.append(ipc) 3040 3041def BranchDataPrep(query): 3042 data = [] 3043 for i in xrange(0, 8): 3044 data.append(query.value(i)) 3045 BranchDataPrepBr(query, data) 3046 return data 3047 3048def BranchDataPrepWA(query): 3049 data = [] 3050 data.append(query.value(0)) 3051 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string 3052 data.append("{:>19}".format(query.value(1))) 3053 for i in xrange(2, 8): 3054 data.append(query.value(i)) 3055 BranchDataPrepBr(query, data) 3056 return data 3057 3058def BranchDataWithIPCPrep(query): 3059 data = [] 3060 for i in xrange(0, 8): 3061 data.append(query.value(i)) 3062 BranchDataPrepIPC(query, data) 3063 BranchDataPrepBr(query, data) 3064 return data 3065 3066def BranchDataWithIPCPrepWA(query): 3067 data = [] 3068 data.append(query.value(0)) 3069 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string 3070 data.append("{:>19}".format(query.value(1))) 3071 for i in xrange(2, 8): 3072 data.append(query.value(i)) 3073 BranchDataPrepIPC(query, data) 3074 BranchDataPrepBr(query, data) 3075 return data 3076 3077# Branch data model 3078 3079class BranchModel(TreeModel): 3080 3081 progress = Signal(object) 3082 3083 def __init__(self, glb, event_id, where_clause, parent=None): 3084 super(BranchModel, self).__init__(glb, None, parent) 3085 self.event_id = event_id 3086 self.more = True 3087 self.populated = 0 3088 self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count") 3089 if self.have_ipc: 3090 select_ipc = ", insn_count, cyc_count" 3091 prep_fn = BranchDataWithIPCPrep 3092 prep_wa_fn = BranchDataWithIPCPrepWA 3093 else: 3094 select_ipc = "" 3095 prep_fn = BranchDataPrep 3096 prep_wa_fn = BranchDataPrepWA 3097 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name," 3098 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END," 3099 " ip, symbols.name, sym_offset, dsos.short_name," 3100 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name" 3101 + select_ipc + 3102 " FROM samples" 3103 " INNER JOIN comms ON comm_id = comms.id" 3104 " INNER JOIN threads ON thread_id = threads.id" 3105 " INNER JOIN branch_types ON branch_type = branch_types.id" 3106 " INNER JOIN symbols ON symbol_id = symbols.id" 3107 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id" 3108 " INNER JOIN dsos ON samples.dso_id = dsos.id" 3109 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id" 3110 " WHERE samples.id > $$last_id$$" + where_clause + 3111 " AND evsel_id = " + str(self.event_id) + 3112 " ORDER BY samples.id" 3113 " LIMIT " + str(glb_chunk_sz)) 3114 if pyside_version_1 and sys.version_info[0] == 3: 3115 prep = prep_fn 3116 else: 3117 prep = prep_wa_fn 3118 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample) 3119 self.fetcher.done.connect(self.Update) 3120 self.fetcher.Fetch(glb_chunk_sz) 3121 3122 def GetRoot(self): 3123 return BranchRootItem() 3124 3125 def columnCount(self, parent=None): 3126 if self.have_ipc: 3127 return 11 3128 else: 3129 return 8 3130 3131 def columnHeader(self, column): 3132 if self.have_ipc: 3133 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column] 3134 else: 3135 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column] 3136 3137 def columnFont(self, column): 3138 if self.have_ipc: 3139 br_col = 10 3140 else: 3141 br_col = 7 3142 if column != br_col: 3143 return None 3144 return QFont("Monospace") 3145 3146 def DisplayData(self, item, index): 3147 if item.level == 1: 3148 self.FetchIfNeeded(item.row) 3149 return item.getData(index.column()) 3150 3151 def AddSample(self, data): 3152 child = BranchLevelOneItem(self.glb, self.populated, data, self.root) 3153 self.root.child_items.append(child) 3154 self.populated += 1 3155 3156 def Update(self, fetched): 3157 if not fetched: 3158 self.more = False 3159 self.progress.emit(0) 3160 child_count = self.root.child_count 3161 count = self.populated - child_count 3162 if count > 0: 3163 parent = QModelIndex() 3164 self.beginInsertRows(parent, child_count, child_count + count - 1) 3165 self.insertRows(child_count, count, parent) 3166 self.root.child_count += count 3167 self.endInsertRows() 3168 self.progress.emit(self.root.child_count) 3169 3170 def FetchMoreRecords(self, count): 3171 current = self.root.child_count 3172 if self.more: 3173 self.fetcher.Fetch(count) 3174 else: 3175 self.progress.emit(0) 3176 return current 3177 3178 def HasMoreRecords(self): 3179 return self.more 3180 3181# Report Variables 3182 3183class ReportVars(): 3184 3185 def __init__(self, name = "", where_clause = "", limit = ""): 3186 self.name = name 3187 self.where_clause = where_clause 3188 self.limit = limit 3189 3190 def UniqueId(self): 3191 return str(self.where_clause + ";" + self.limit) 3192 3193# Branch window 3194 3195class BranchWindow(QMdiSubWindow): 3196 3197 def __init__(self, glb, event_id, report_vars, parent=None): 3198 super(BranchWindow, self).__init__(parent) 3199 3200 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId() 3201 3202 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause)) 3203 3204 self.view = QTreeView() 3205 self.view.setUniformRowHeights(True) 3206 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) 3207 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard 3208 self.view.setModel(self.model) 3209 3210 self.ResizeColumnsToContents() 3211 3212 self.context_menu = TreeContextMenu(self.view) 3213 3214 self.find_bar = FindBar(self, self, True) 3215 3216 self.finder = ChildDataItemFinder(self.model.root) 3217 3218 self.fetch_bar = FetchMoreRecordsBar(self.model, self) 3219 3220 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) 3221 3222 self.setWidget(self.vbox.Widget()) 3223 3224 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events") 3225 3226 def ResizeColumnToContents(self, column, n): 3227 # Using the view's resizeColumnToContents() here is extrememly slow 3228 # so implement a crude alternative 3229 mm = "MM" if column else "MMMM" 3230 font = self.view.font() 3231 metrics = QFontMetrics(font) 3232 max = 0 3233 for row in xrange(n): 3234 val = self.model.root.child_items[row].data[column] 3235 len = metrics.width(str(val) + mm) 3236 max = len if len > max else max 3237 val = self.model.columnHeader(column) 3238 len = metrics.width(str(val) + mm) 3239 max = len if len > max else max 3240 self.view.setColumnWidth(column, max) 3241 3242 def ResizeColumnsToContents(self): 3243 n = min(self.model.root.child_count, 100) 3244 if n < 1: 3245 # No data yet, so connect a signal to notify when there is 3246 self.model.rowsInserted.connect(self.UpdateColumnWidths) 3247 return 3248 columns = self.model.columnCount() 3249 for i in xrange(columns): 3250 self.ResizeColumnToContents(i, n) 3251 3252 def UpdateColumnWidths(self, *x): 3253 # This only needs to be done once, so disconnect the signal now 3254 self.model.rowsInserted.disconnect(self.UpdateColumnWidths) 3255 self.ResizeColumnsToContents() 3256 3257 def Find(self, value, direction, pattern, context): 3258 self.view.setFocus() 3259 self.find_bar.Busy() 3260 self.finder.Find(value, direction, pattern, context, self.FindDone) 3261 3262 def FindDone(self, row): 3263 self.find_bar.Idle() 3264 if row >= 0: 3265 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) 3266 else: 3267 self.find_bar.NotFound() 3268 3269# Line edit data item 3270 3271class LineEditDataItem(object): 3272 3273 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""): 3274 self.glb = glb 3275 self.label = label 3276 self.placeholder_text = placeholder_text 3277 self.parent = parent 3278 self.id = id 3279 3280 self.value = default 3281 3282 self.widget = QLineEdit(default) 3283 self.widget.editingFinished.connect(self.Validate) 3284 self.widget.textChanged.connect(self.Invalidate) 3285 self.red = False 3286 self.error = "" 3287 self.validated = True 3288 3289 if placeholder_text: 3290 self.widget.setPlaceholderText(placeholder_text) 3291 3292 def TurnTextRed(self): 3293 if not self.red: 3294 palette = QPalette() 3295 palette.setColor(QPalette.Text,Qt.red) 3296 self.widget.setPalette(palette) 3297 self.red = True 3298 3299 def TurnTextNormal(self): 3300 if self.red: 3301 palette = QPalette() 3302 self.widget.setPalette(palette) 3303 self.red = False 3304 3305 def InvalidValue(self, value): 3306 self.value = "" 3307 self.TurnTextRed() 3308 self.error = self.label + " invalid value '" + value + "'" 3309 self.parent.ShowMessage(self.error) 3310 3311 def Invalidate(self): 3312 self.validated = False 3313 3314 def DoValidate(self, input_string): 3315 self.value = input_string.strip() 3316 3317 def Validate(self): 3318 self.validated = True 3319 self.error = "" 3320 self.TurnTextNormal() 3321 self.parent.ClearMessage() 3322 input_string = self.widget.text() 3323 if not len(input_string.strip()): 3324 self.value = "" 3325 return 3326 self.DoValidate(input_string) 3327 3328 def IsValid(self): 3329 if not self.validated: 3330 self.Validate() 3331 if len(self.error): 3332 self.parent.ShowMessage(self.error) 3333 return False 3334 return True 3335 3336 def IsNumber(self, value): 3337 try: 3338 x = int(value) 3339 except: 3340 x = 0 3341 return str(x) == value 3342 3343# Non-negative integer ranges dialog data item 3344 3345class NonNegativeIntegerRangesDataItem(LineEditDataItem): 3346 3347 def __init__(self, glb, label, placeholder_text, column_name, parent): 3348 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent) 3349 3350 self.column_name = column_name 3351 3352 def DoValidate(self, input_string): 3353 singles = [] 3354 ranges = [] 3355 for value in [x.strip() for x in input_string.split(",")]: 3356 if "-" in value: 3357 vrange = value.split("-") 3358 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): 3359 return self.InvalidValue(value) 3360 ranges.append(vrange) 3361 else: 3362 if not self.IsNumber(value): 3363 return self.InvalidValue(value) 3364 singles.append(value) 3365 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges] 3366 if len(singles): 3367 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")") 3368 self.value = " OR ".join(ranges) 3369 3370# Positive integer dialog data item 3371 3372class PositiveIntegerDataItem(LineEditDataItem): 3373 3374 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""): 3375 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default) 3376 3377 def DoValidate(self, input_string): 3378 if not self.IsNumber(input_string.strip()): 3379 return self.InvalidValue(input_string) 3380 value = int(input_string.strip()) 3381 if value <= 0: 3382 return self.InvalidValue(input_string) 3383 self.value = str(value) 3384 3385# Dialog data item converted and validated using a SQL table 3386 3387class SQLTableDataItem(LineEditDataItem): 3388 3389 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent): 3390 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent) 3391 3392 self.table_name = table_name 3393 self.match_column = match_column 3394 self.column_name1 = column_name1 3395 self.column_name2 = column_name2 3396 3397 def ValueToIds(self, value): 3398 ids = [] 3399 query = QSqlQuery(self.glb.db) 3400 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'" 3401 ret = query.exec_(stmt) 3402 if ret: 3403 while query.next(): 3404 ids.append(str(query.value(0))) 3405 return ids 3406 3407 def DoValidate(self, input_string): 3408 all_ids = [] 3409 for value in [x.strip() for x in input_string.split(",")]: 3410 ids = self.ValueToIds(value) 3411 if len(ids): 3412 all_ids.extend(ids) 3413 else: 3414 return self.InvalidValue(value) 3415 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")" 3416 if self.column_name2: 3417 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )" 3418 3419# Sample time ranges dialog data item converted and validated using 'samples' SQL table 3420 3421class SampleTimeRangesDataItem(LineEditDataItem): 3422 3423 def __init__(self, glb, label, placeholder_text, column_name, parent): 3424 self.column_name = column_name 3425 3426 self.last_id = 0 3427 self.first_time = 0 3428 self.last_time = 2 ** 64 3429 3430 query = QSqlQuery(glb.db) 3431 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1") 3432 if query.next(): 3433 self.last_id = int(query.value(0)) 3434 self.first_time = int(glb.HostStartTime()) 3435 self.last_time = int(glb.HostFinishTime()) 3436 if placeholder_text: 3437 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time) 3438 3439 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent) 3440 3441 def IdBetween(self, query, lower_id, higher_id, order): 3442 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1") 3443 if query.next(): 3444 return True, int(query.value(0)) 3445 else: 3446 return False, 0 3447 3448 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor): 3449 query = QSqlQuery(self.glb.db) 3450 while True: 3451 next_id = int((lower_id + higher_id) / 2) 3452 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) 3453 if not query.next(): 3454 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC") 3455 if not ok: 3456 ok, dbid = self.IdBetween(query, next_id, higher_id, "") 3457 if not ok: 3458 return str(higher_id) 3459 next_id = dbid 3460 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) 3461 next_time = int(query.value(0)) 3462 if get_floor: 3463 if target_time > next_time: 3464 lower_id = next_id 3465 else: 3466 higher_id = next_id 3467 if higher_id <= lower_id + 1: 3468 return str(higher_id) 3469 else: 3470 if target_time >= next_time: 3471 lower_id = next_id 3472 else: 3473 higher_id = next_id 3474 if higher_id <= lower_id + 1: 3475 return str(lower_id) 3476 3477 def ConvertRelativeTime(self, val): 3478 mult = 1 3479 suffix = val[-2:] 3480 if suffix == "ms": 3481 mult = 1000000 3482 elif suffix == "us": 3483 mult = 1000 3484 elif suffix == "ns": 3485 mult = 1 3486 else: 3487 return val 3488 val = val[:-2].strip() 3489 if not self.IsNumber(val): 3490 return val 3491 val = int(val) * mult 3492 if val >= 0: 3493 val += self.first_time 3494 else: 3495 val += self.last_time 3496 return str(val) 3497 3498 def ConvertTimeRange(self, vrange): 3499 if vrange[0] == "": 3500 vrange[0] = str(self.first_time) 3501 if vrange[1] == "": 3502 vrange[1] = str(self.last_time) 3503 vrange[0] = self.ConvertRelativeTime(vrange[0]) 3504 vrange[1] = self.ConvertRelativeTime(vrange[1]) 3505 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): 3506 return False 3507 beg_range = max(int(vrange[0]), self.first_time) 3508 end_range = min(int(vrange[1]), self.last_time) 3509 if beg_range > self.last_time or end_range < self.first_time: 3510 return False 3511 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True) 3512 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False) 3513 return True 3514 3515 def AddTimeRange(self, value, ranges): 3516 n = value.count("-") 3517 if n == 1: 3518 pass 3519 elif n == 2: 3520 if value.split("-")[1].strip() == "": 3521 n = 1 3522 elif n == 3: 3523 n = 2 3524 else: 3525 return False 3526 pos = findnth(value, "-", n) 3527 vrange = [value[:pos].strip() ,value[pos+1:].strip()] 3528 if self.ConvertTimeRange(vrange): 3529 ranges.append(vrange) 3530 return True 3531 return False 3532 3533 def DoValidate(self, input_string): 3534 ranges = [] 3535 for value in [x.strip() for x in input_string.split(",")]: 3536 if not self.AddTimeRange(value, ranges): 3537 return self.InvalidValue(value) 3538 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges] 3539 self.value = " OR ".join(ranges) 3540 3541# Report Dialog Base 3542 3543class ReportDialogBase(QDialog): 3544 3545 def __init__(self, glb, title, items, partial, parent=None): 3546 super(ReportDialogBase, self).__init__(parent) 3547 3548 self.glb = glb 3549 3550 self.report_vars = ReportVars() 3551 3552 self.setWindowTitle(title) 3553 self.setMinimumWidth(600) 3554 3555 self.data_items = [x(glb, self) for x in items] 3556 3557 self.partial = partial 3558 3559 self.grid = QGridLayout() 3560 3561 for row in xrange(len(self.data_items)): 3562 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0) 3563 self.grid.addWidget(self.data_items[row].widget, row, 1) 3564 3565 self.status = QLabel() 3566 3567 self.ok_button = QPushButton("Ok", self) 3568 self.ok_button.setDefault(True) 3569 self.ok_button.released.connect(self.Ok) 3570 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 3571 3572 self.cancel_button = QPushButton("Cancel", self) 3573 self.cancel_button.released.connect(self.reject) 3574 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 3575 3576 self.hbox = QHBoxLayout() 3577 #self.hbox.addStretch() 3578 self.hbox.addWidget(self.status) 3579 self.hbox.addWidget(self.ok_button) 3580 self.hbox.addWidget(self.cancel_button) 3581 3582 self.vbox = QVBoxLayout() 3583 self.vbox.addLayout(self.grid) 3584 self.vbox.addLayout(self.hbox) 3585 3586 self.setLayout(self.vbox) 3587 3588 def Ok(self): 3589 vars = self.report_vars 3590 for d in self.data_items: 3591 if d.id == "REPORTNAME": 3592 vars.name = d.value 3593 if not vars.name: 3594 self.ShowMessage("Report name is required") 3595 return 3596 for d in self.data_items: 3597 if not d.IsValid(): 3598 return 3599 for d in self.data_items[1:]: 3600 if d.id == "LIMIT": 3601 vars.limit = d.value 3602 elif len(d.value): 3603 if len(vars.where_clause): 3604 vars.where_clause += " AND " 3605 vars.where_clause += d.value 3606 if len(vars.where_clause): 3607 if self.partial: 3608 vars.where_clause = " AND ( " + vars.where_clause + " ) " 3609 else: 3610 vars.where_clause = " WHERE " + vars.where_clause + " " 3611 self.accept() 3612 3613 def ShowMessage(self, msg): 3614 self.status.setText("<font color=#FF0000>" + msg) 3615 3616 def ClearMessage(self): 3617 self.status.setText("") 3618 3619# Selected branch report creation dialog 3620 3621class SelectedBranchDialog(ReportDialogBase): 3622 3623 def __init__(self, glb, parent=None): 3624 title = "Selected Branches" 3625 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), 3626 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p), 3627 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p), 3628 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p), 3629 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p), 3630 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p), 3631 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p), 3632 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p), 3633 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p)) 3634 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent) 3635 3636# Event list 3637 3638def GetEventList(db): 3639 events = [] 3640 query = QSqlQuery(db) 3641 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id") 3642 while query.next(): 3643 events.append(query.value(0)) 3644 return events 3645 3646# Is a table selectable 3647 3648def IsSelectable(db, table, sql = "", columns = "*"): 3649 query = QSqlQuery(db) 3650 try: 3651 QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1") 3652 except: 3653 return False 3654 return True 3655 3656# SQL table data model item 3657 3658class SQLTableItem(): 3659 3660 def __init__(self, row, data): 3661 self.row = row 3662 self.data = data 3663 3664 def getData(self, column): 3665 return self.data[column] 3666 3667# SQL table data model 3668 3669class SQLTableModel(TableModel): 3670 3671 progress = Signal(object) 3672 3673 def __init__(self, glb, sql, column_headers, parent=None): 3674 super(SQLTableModel, self).__init__(parent) 3675 self.glb = glb 3676 self.more = True 3677 self.populated = 0 3678 self.column_headers = column_headers 3679 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample) 3680 self.fetcher.done.connect(self.Update) 3681 self.fetcher.Fetch(glb_chunk_sz) 3682 3683 def DisplayData(self, item, index): 3684 self.FetchIfNeeded(item.row) 3685 return item.getData(index.column()) 3686 3687 def AddSample(self, data): 3688 child = SQLTableItem(self.populated, data) 3689 self.child_items.append(child) 3690 self.populated += 1 3691 3692 def Update(self, fetched): 3693 if not fetched: 3694 self.more = False 3695 self.progress.emit(0) 3696 child_count = self.child_count 3697 count = self.populated - child_count 3698 if count > 0: 3699 parent = QModelIndex() 3700 self.beginInsertRows(parent, child_count, child_count + count - 1) 3701 self.insertRows(child_count, count, parent) 3702 self.child_count += count 3703 self.endInsertRows() 3704 self.progress.emit(self.child_count) 3705 3706 def FetchMoreRecords(self, count): 3707 current = self.child_count 3708 if self.more: 3709 self.fetcher.Fetch(count) 3710 else: 3711 self.progress.emit(0) 3712 return current 3713 3714 def HasMoreRecords(self): 3715 return self.more 3716 3717 def columnCount(self, parent=None): 3718 return len(self.column_headers) 3719 3720 def columnHeader(self, column): 3721 return self.column_headers[column] 3722 3723 def SQLTableDataPrep(self, query, count): 3724 data = [] 3725 for i in xrange(count): 3726 data.append(query.value(i)) 3727 return data 3728 3729# SQL automatic table data model 3730 3731class SQLAutoTableModel(SQLTableModel): 3732 3733 def __init__(self, glb, table_name, parent=None): 3734 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz) 3735 if table_name == "comm_threads_view": 3736 # For now, comm_threads_view has no id column 3737 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz) 3738 column_headers = [] 3739 query = QSqlQuery(glb.db) 3740 if glb.dbref.is_sqlite3: 3741 QueryExec(query, "PRAGMA table_info(" + table_name + ")") 3742 while query.next(): 3743 column_headers.append(query.value(1)) 3744 if table_name == "sqlite_master": 3745 sql = "SELECT * FROM " + table_name 3746 else: 3747 if table_name[:19] == "information_schema.": 3748 sql = "SELECT * FROM " + table_name 3749 select_table_name = table_name[19:] 3750 schema = "information_schema" 3751 else: 3752 select_table_name = table_name 3753 schema = "public" 3754 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'") 3755 while query.next(): 3756 column_headers.append(query.value(0)) 3757 if pyside_version_1 and sys.version_info[0] == 3: 3758 if table_name == "samples_view": 3759 self.SQLTableDataPrep = self.samples_view_DataPrep 3760 if table_name == "samples": 3761 self.SQLTableDataPrep = self.samples_DataPrep 3762 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent) 3763 3764 def samples_view_DataPrep(self, query, count): 3765 data = [] 3766 data.append(query.value(0)) 3767 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string 3768 data.append("{:>19}".format(query.value(1))) 3769 for i in xrange(2, count): 3770 data.append(query.value(i)) 3771 return data 3772 3773 def samples_DataPrep(self, query, count): 3774 data = [] 3775 for i in xrange(9): 3776 data.append(query.value(i)) 3777 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string 3778 data.append("{:>19}".format(query.value(9))) 3779 for i in xrange(10, count): 3780 data.append(query.value(i)) 3781 return data 3782 3783# Base class for custom ResizeColumnsToContents 3784 3785class ResizeColumnsToContentsBase(QObject): 3786 3787 def __init__(self, parent=None): 3788 super(ResizeColumnsToContentsBase, self).__init__(parent) 3789 3790 def ResizeColumnToContents(self, column, n): 3791 # Using the view's resizeColumnToContents() here is extrememly slow 3792 # so implement a crude alternative 3793 font = self.view.font() 3794 metrics = QFontMetrics(font) 3795 max = 0 3796 for row in xrange(n): 3797 val = self.data_model.child_items[row].data[column] 3798 len = metrics.width(str(val) + "MM") 3799 max = len if len > max else max 3800 val = self.data_model.columnHeader(column) 3801 len = metrics.width(str(val) + "MM") 3802 max = len if len > max else max 3803 self.view.setColumnWidth(column, max) 3804 3805 def ResizeColumnsToContents(self): 3806 n = min(self.data_model.child_count, 100) 3807 if n < 1: 3808 # No data yet, so connect a signal to notify when there is 3809 self.data_model.rowsInserted.connect(self.UpdateColumnWidths) 3810 return 3811 columns = self.data_model.columnCount() 3812 for i in xrange(columns): 3813 self.ResizeColumnToContents(i, n) 3814 3815 def UpdateColumnWidths(self, *x): 3816 # This only needs to be done once, so disconnect the signal now 3817 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths) 3818 self.ResizeColumnsToContents() 3819 3820# Convert value to CSV 3821 3822def ToCSValue(val): 3823 if '"' in val: 3824 val = val.replace('"', '""') 3825 if "," in val or '"' in val: 3826 val = '"' + val + '"' 3827 return val 3828 3829# Key to sort table model indexes by row / column, assuming fewer than 1000 columns 3830 3831glb_max_cols = 1000 3832 3833def RowColumnKey(a): 3834 return a.row() * glb_max_cols + a.column() 3835 3836# Copy selected table cells to clipboard 3837 3838def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False): 3839 indexes = sorted(view.selectedIndexes(), key=RowColumnKey) 3840 idx_cnt = len(indexes) 3841 if not idx_cnt: 3842 return 3843 if idx_cnt == 1: 3844 with_hdr=False 3845 min_row = indexes[0].row() 3846 max_row = indexes[0].row() 3847 min_col = indexes[0].column() 3848 max_col = indexes[0].column() 3849 for i in indexes: 3850 min_row = min(min_row, i.row()) 3851 max_row = max(max_row, i.row()) 3852 min_col = min(min_col, i.column()) 3853 max_col = max(max_col, i.column()) 3854 if max_col > glb_max_cols: 3855 raise RuntimeError("glb_max_cols is too low") 3856 max_width = [0] * (1 + max_col - min_col) 3857 for i in indexes: 3858 c = i.column() - min_col 3859 max_width[c] = max(max_width[c], len(str(i.data()))) 3860 text = "" 3861 pad = "" 3862 sep = "" 3863 if with_hdr: 3864 model = indexes[0].model() 3865 for col in range(min_col, max_col + 1): 3866 val = model.headerData(col, Qt.Horizontal) 3867 if as_csv: 3868 text += sep + ToCSValue(val) 3869 sep = "," 3870 else: 3871 c = col - min_col 3872 max_width[c] = max(max_width[c], len(val)) 3873 width = max_width[c] 3874 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole) 3875 if align & Qt.AlignRight: 3876 val = val.rjust(width) 3877 text += pad + sep + val 3878 pad = " " * (width - len(val)) 3879 sep = " " 3880 text += "\n" 3881 pad = "" 3882 sep = "" 3883 last_row = min_row 3884 for i in indexes: 3885 if i.row() > last_row: 3886 last_row = i.row() 3887 text += "\n" 3888 pad = "" 3889 sep = "" 3890 if as_csv: 3891 text += sep + ToCSValue(str(i.data())) 3892 sep = "," 3893 else: 3894 width = max_width[i.column() - min_col] 3895 if i.data(Qt.TextAlignmentRole) & Qt.AlignRight: 3896 val = str(i.data()).rjust(width) 3897 else: 3898 val = str(i.data()) 3899 text += pad + sep + val 3900 pad = " " * (width - len(val)) 3901 sep = " " 3902 QApplication.clipboard().setText(text) 3903 3904def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False): 3905 indexes = view.selectedIndexes() 3906 if not len(indexes): 3907 return 3908 3909 selection = view.selectionModel() 3910 3911 first = None 3912 for i in indexes: 3913 above = view.indexAbove(i) 3914 if not selection.isSelected(above): 3915 first = i 3916 break 3917 3918 if first is None: 3919 raise RuntimeError("CopyTreeCellsToClipboard internal error") 3920 3921 model = first.model() 3922 row_cnt = 0 3923 col_cnt = model.columnCount(first) 3924 max_width = [0] * col_cnt 3925 3926 indent_sz = 2 3927 indent_str = " " * indent_sz 3928 3929 expanded_mark_sz = 2 3930 if sys.version_info[0] == 3: 3931 expanded_mark = "\u25BC " 3932 not_expanded_mark = "\u25B6 " 3933 else: 3934 expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8") 3935 not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8") 3936 leaf_mark = " " 3937 3938 if not as_csv: 3939 pos = first 3940 while True: 3941 row_cnt += 1 3942 row = pos.row() 3943 for c in range(col_cnt): 3944 i = pos.sibling(row, c) 3945 if c: 3946 n = len(str(i.data())) 3947 else: 3948 n = len(str(i.data()).strip()) 3949 n += (i.internalPointer().level - 1) * indent_sz 3950 n += expanded_mark_sz 3951 max_width[c] = max(max_width[c], n) 3952 pos = view.indexBelow(pos) 3953 if not selection.isSelected(pos): 3954 break 3955 3956 text = "" 3957 pad = "" 3958 sep = "" 3959 if with_hdr: 3960 for c in range(col_cnt): 3961 val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip() 3962 if as_csv: 3963 text += sep + ToCSValue(val) 3964 sep = "," 3965 else: 3966 max_width[c] = max(max_width[c], len(val)) 3967 width = max_width[c] 3968 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole) 3969 if align & Qt.AlignRight: 3970 val = val.rjust(width) 3971 text += pad + sep + val 3972 pad = " " * (width - len(val)) 3973 sep = " " 3974 text += "\n" 3975 pad = "" 3976 sep = "" 3977 3978 pos = first 3979 while True: 3980 row = pos.row() 3981 for c in range(col_cnt): 3982 i = pos.sibling(row, c) 3983 val = str(i.data()) 3984 if not c: 3985 if model.hasChildren(i): 3986 if view.isExpanded(i): 3987 mark = expanded_mark 3988 else: 3989 mark = not_expanded_mark 3990 else: 3991 mark = leaf_mark 3992 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip() 3993 if as_csv: 3994 text += sep + ToCSValue(val) 3995 sep = "," 3996 else: 3997 width = max_width[c] 3998 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight: 3999 val = val.rjust(width) 4000 text += pad + sep + val 4001 pad = " " * (width - len(val)) 4002 sep = " " 4003 pos = view.indexBelow(pos) 4004 if not selection.isSelected(pos): 4005 break 4006 text = text.rstrip() + "\n" 4007 pad = "" 4008 sep = "" 4009 4010 QApplication.clipboard().setText(text) 4011 4012def CopyCellsToClipboard(view, as_csv=False, with_hdr=False): 4013 view.CopyCellsToClipboard(view, as_csv, with_hdr) 4014 4015def CopyCellsToClipboardHdr(view): 4016 CopyCellsToClipboard(view, False, True) 4017 4018def CopyCellsToClipboardCSV(view): 4019 CopyCellsToClipboard(view, True, True) 4020 4021# Context menu 4022 4023class ContextMenu(object): 4024 4025 def __init__(self, view): 4026 self.view = view 4027 self.view.setContextMenuPolicy(Qt.CustomContextMenu) 4028 self.view.customContextMenuRequested.connect(self.ShowContextMenu) 4029 4030 def ShowContextMenu(self, pos): 4031 menu = QMenu(self.view) 4032 self.AddActions(menu) 4033 menu.exec_(self.view.mapToGlobal(pos)) 4034 4035 def AddCopy(self, menu): 4036 menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view)) 4037 menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view)) 4038 4039 def AddActions(self, menu): 4040 self.AddCopy(menu) 4041 4042class TreeContextMenu(ContextMenu): 4043 4044 def __init__(self, view): 4045 super(TreeContextMenu, self).__init__(view) 4046 4047 def AddActions(self, menu): 4048 i = self.view.currentIndex() 4049 text = str(i.data()).strip() 4050 if len(text): 4051 menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view)) 4052 self.AddCopy(menu) 4053 4054# Table window 4055 4056class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase): 4057 4058 def __init__(self, glb, table_name, parent=None): 4059 super(TableWindow, self).__init__(parent) 4060 4061 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name)) 4062 4063 self.model = QSortFilterProxyModel() 4064 self.model.setSourceModel(self.data_model) 4065 4066 self.view = QTableView() 4067 self.view.setModel(self.model) 4068 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) 4069 self.view.verticalHeader().setVisible(False) 4070 self.view.sortByColumn(-1, Qt.AscendingOrder) 4071 self.view.setSortingEnabled(True) 4072 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) 4073 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard 4074 4075 self.ResizeColumnsToContents() 4076 4077 self.context_menu = ContextMenu(self.view) 4078 4079 self.find_bar = FindBar(self, self, True) 4080 4081 self.finder = ChildDataItemFinder(self.data_model) 4082 4083 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) 4084 4085 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) 4086 4087 self.setWidget(self.vbox.Widget()) 4088 4089 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table") 4090 4091 def Find(self, value, direction, pattern, context): 4092 self.view.setFocus() 4093 self.find_bar.Busy() 4094 self.finder.Find(value, direction, pattern, context, self.FindDone) 4095 4096 def FindDone(self, row): 4097 self.find_bar.Idle() 4098 if row >= 0: 4099 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex()))) 4100 else: 4101 self.find_bar.NotFound() 4102 4103# Table list 4104 4105def GetTableList(glb): 4106 tables = [] 4107 query = QSqlQuery(glb.db) 4108 if glb.dbref.is_sqlite3: 4109 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name") 4110 else: 4111 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name") 4112 while query.next(): 4113 tables.append(query.value(0)) 4114 if glb.dbref.is_sqlite3: 4115 tables.append("sqlite_master") 4116 else: 4117 tables.append("information_schema.tables") 4118 tables.append("information_schema.views") 4119 tables.append("information_schema.columns") 4120 return tables 4121 4122# Top Calls data model 4123 4124class TopCallsModel(SQLTableModel): 4125 4126 def __init__(self, glb, report_vars, parent=None): 4127 text = "" 4128 if not glb.dbref.is_sqlite3: 4129 text = "::text" 4130 limit = "" 4131 if len(report_vars.limit): 4132 limit = " LIMIT " + report_vars.limit 4133 sql = ("SELECT comm, pid, tid, name," 4134 " CASE" 4135 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text + 4136 " ELSE short_name" 4137 " END AS dso," 4138 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, " 4139 " CASE" 4140 " WHEN (calls.flags = 1) THEN 'no call'" + text + 4141 " WHEN (calls.flags = 2) THEN 'no return'" + text + 4142 " WHEN (calls.flags = 3) THEN 'no call/return'" + text + 4143 " ELSE ''" + text + 4144 " END AS flags" 4145 " FROM calls" 4146 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 4147 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 4148 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 4149 " INNER JOIN comms ON calls.comm_id = comms.id" 4150 " INNER JOIN threads ON calls.thread_id = threads.id" + 4151 report_vars.where_clause + 4152 " ORDER BY elapsed_time DESC" + 4153 limit 4154 ) 4155 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags") 4156 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft) 4157 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent) 4158 4159 def columnAlignment(self, column): 4160 return self.alignment[column] 4161 4162# Top Calls report creation dialog 4163 4164class TopCallsDialog(ReportDialogBase): 4165 4166 def __init__(self, glb, parent=None): 4167 title = "Top Calls by Elapsed Time" 4168 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), 4169 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p), 4170 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p), 4171 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p), 4172 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p), 4173 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p), 4174 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p), 4175 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100")) 4176 super(TopCallsDialog, self).__init__(glb, title, items, False, parent) 4177 4178# Top Calls window 4179 4180class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase): 4181 4182 def __init__(self, glb, report_vars, parent=None): 4183 super(TopCallsWindow, self).__init__(parent) 4184 4185 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars)) 4186 self.model = self.data_model 4187 4188 self.view = QTableView() 4189 self.view.setModel(self.model) 4190 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) 4191 self.view.verticalHeader().setVisible(False) 4192 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) 4193 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard 4194 4195 self.context_menu = ContextMenu(self.view) 4196 4197 self.ResizeColumnsToContents() 4198 4199 self.find_bar = FindBar(self, self, True) 4200 4201 self.finder = ChildDataItemFinder(self.model) 4202 4203 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) 4204 4205 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) 4206 4207 self.setWidget(self.vbox.Widget()) 4208 4209 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name) 4210 4211 def Find(self, value, direction, pattern, context): 4212 self.view.setFocus() 4213 self.find_bar.Busy() 4214 self.finder.Find(value, direction, pattern, context, self.FindDone) 4215 4216 def FindDone(self, row): 4217 self.find_bar.Idle() 4218 if row >= 0: 4219 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) 4220 else: 4221 self.find_bar.NotFound() 4222 4223# Action Definition 4224 4225def CreateAction(label, tip, callback, parent=None, shortcut=None): 4226 action = QAction(label, parent) 4227 if shortcut != None: 4228 action.setShortcuts(shortcut) 4229 action.setStatusTip(tip) 4230 action.triggered.connect(callback) 4231 return action 4232 4233# Typical application actions 4234 4235def CreateExitAction(app, parent=None): 4236 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit) 4237 4238# Typical MDI actions 4239 4240def CreateCloseActiveWindowAction(mdi_area): 4241 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area) 4242 4243def CreateCloseAllWindowsAction(mdi_area): 4244 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area) 4245 4246def CreateTileWindowsAction(mdi_area): 4247 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area) 4248 4249def CreateCascadeWindowsAction(mdi_area): 4250 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area) 4251 4252def CreateNextWindowAction(mdi_area): 4253 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild) 4254 4255def CreatePreviousWindowAction(mdi_area): 4256 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild) 4257 4258# Typical MDI window menu 4259 4260class WindowMenu(): 4261 4262 def __init__(self, mdi_area, menu): 4263 self.mdi_area = mdi_area 4264 self.window_menu = menu.addMenu("&Windows") 4265 self.close_active_window = CreateCloseActiveWindowAction(mdi_area) 4266 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area) 4267 self.tile_windows = CreateTileWindowsAction(mdi_area) 4268 self.cascade_windows = CreateCascadeWindowsAction(mdi_area) 4269 self.next_window = CreateNextWindowAction(mdi_area) 4270 self.previous_window = CreatePreviousWindowAction(mdi_area) 4271 self.window_menu.aboutToShow.connect(self.Update) 4272 4273 def Update(self): 4274 self.window_menu.clear() 4275 sub_window_count = len(self.mdi_area.subWindowList()) 4276 have_sub_windows = sub_window_count != 0 4277 self.close_active_window.setEnabled(have_sub_windows) 4278 self.close_all_windows.setEnabled(have_sub_windows) 4279 self.tile_windows.setEnabled(have_sub_windows) 4280 self.cascade_windows.setEnabled(have_sub_windows) 4281 self.next_window.setEnabled(have_sub_windows) 4282 self.previous_window.setEnabled(have_sub_windows) 4283 self.window_menu.addAction(self.close_active_window) 4284 self.window_menu.addAction(self.close_all_windows) 4285 self.window_menu.addSeparator() 4286 self.window_menu.addAction(self.tile_windows) 4287 self.window_menu.addAction(self.cascade_windows) 4288 self.window_menu.addSeparator() 4289 self.window_menu.addAction(self.next_window) 4290 self.window_menu.addAction(self.previous_window) 4291 if sub_window_count == 0: 4292 return 4293 self.window_menu.addSeparator() 4294 nr = 1 4295 for sub_window in self.mdi_area.subWindowList(): 4296 label = str(nr) + " " + sub_window.name 4297 if nr < 10: 4298 label = "&" + label 4299 action = self.window_menu.addAction(label) 4300 action.setCheckable(True) 4301 action.setChecked(sub_window == self.mdi_area.activeSubWindow()) 4302 action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x)) 4303 self.window_menu.addAction(action) 4304 nr += 1 4305 4306 def setActiveSubWindow(self, nr): 4307 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1]) 4308 4309# Help text 4310 4311glb_help_text = """ 4312<h1>Contents</h1> 4313<style> 4314p.c1 { 4315 text-indent: 40px; 4316} 4317p.c2 { 4318 text-indent: 80px; 4319} 4320} 4321</style> 4322<p class=c1><a href=#reports>1. Reports</a></p> 4323<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p> 4324<p class=c2><a href=#calltree>1.2 Call Tree</a></p> 4325<p class=c2><a href=#allbranches>1.3 All branches</a></p> 4326<p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p> 4327<p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p> 4328<p class=c1><a href=#charts>2. Charts</a></p> 4329<p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p> 4330<p class=c1><a href=#tables>3. Tables</a></p> 4331<h1 id=reports>1. Reports</h1> 4332<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2> 4333The result is a GUI window with a tree representing a context-sensitive 4334call-graph. Expanding a couple of levels of the tree and adjusting column 4335widths to suit will display something like: 4336<pre> 4337 Call Graph: pt_example 4338Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%) 4339v- ls 4340 v- 2638:2638 4341 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0 4342 |- unknown unknown 1 13198 0.1 1 0.0 4343 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3 4344 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3 4345 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4 4346 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1 4347 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0 4348 >- __libc_csu_init ls 1 10354 0.1 10 0.0 4349 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0 4350 v- main ls 1 8182043 99.6 180254 99.9 4351</pre> 4352<h3>Points to note:</h3> 4353<ul> 4354<li>The top level is a command name (comm)</li> 4355<li>The next level is a thread (pid:tid)</li> 4356<li>Subsequent levels are functions</li> 4357<li>'Count' is the number of calls</li> 4358<li>'Time' is the elapsed time until the function returns</li> 4359<li>Percentages are relative to the level above</li> 4360<li>'Branch Count' is the total number of branches for that function and all functions that it calls 4361</ul> 4362<h3>Find</h3> 4363Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match. 4364The pattern matching symbols are ? for any character and * for zero or more characters. 4365<h2 id=calltree>1.2 Call Tree</h2> 4366The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated. 4367Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'. 4368<h2 id=allbranches>1.3 All branches</h2> 4369The All branches report displays all branches in chronological order. 4370Not all data is fetched immediately. More records can be fetched using the Fetch bar provided. 4371<h3>Disassembly</h3> 4372Open a branch to display disassembly. This only works if: 4373<ol> 4374<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li> 4375<li>The object code is available. Currently, only the perf build ID cache is searched for object code. 4376The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR. 4377One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu), 4378or alternatively, set environment variable PERF_KCORE to the kcore file name.</li> 4379</ol> 4380<h4 id=xed>Intel XED Setup</h4> 4381To use Intel XED, libxed.so must be present. To build and install libxed.so: 4382<pre> 4383git clone https://github.com/intelxed/mbuild.git mbuild 4384git clone https://github.com/intelxed/xed 4385cd xed 4386./mfile.py --share 4387sudo ./mfile.py --prefix=/usr/local install 4388sudo ldconfig 4389</pre> 4390<h3>Instructions per Cycle (IPC)</h3> 4391If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'. 4392<p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch. 4393Due to the granularity of timing information, the number of cycles for some code blocks will not be known. 4394In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period 4395since the previous displayed 'IPC'. 4396<h3>Find</h3> 4397Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. 4398Refer to Python documentation for the regular expression syntax. 4399All columns are searched, but only currently fetched rows are searched. 4400<h2 id=selectedbranches>1.4 Selected branches</h2> 4401This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced 4402by various selection criteria. A dialog box displays available criteria which are AND'ed together. 4403<h3>1.4.1 Time ranges</h3> 4404The time ranges hint text shows the total time range. Relative time ranges can also be entered in 4405ms, us or ns. Also, negative values are relative to the end of trace. Examples: 4406<pre> 4407 81073085947329-81073085958238 From 81073085947329 to 81073085958238 4408 100us-200us From 100us to 200us 4409 10ms- From 10ms to the end 4410 -100ns The first 100ns 4411 -10ms- The last 10ms 4412</pre> 4413N.B. Due to the granularity of timestamps, there could be no branches in any given time range. 4414<h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2> 4415The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned. 4416The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together. 4417If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar. 4418<h1 id=charts>2. Charts</h1> 4419<h2 id=timechartbycpu>2.1 Time chart by CPU</h2> 4420This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu. 4421<h3>Features</h3> 4422<ol> 4423<li>Mouse over to highight the task and show the time</li> 4424<li>Drag the mouse to select a region and zoom by pushing the Zoom button</li> 4425<li>Go back and forward by pressing the arrow buttons</li> 4426<li>If call information is available, right-click to show a call tree opened to that task and time. 4427Note, the call tree may take some time to appear, and there may not be call information for the task or time selected. 4428</li> 4429</ol> 4430<h3>Important</h3> 4431The graph can be misleading in the following respects: 4432<ol> 4433<li>The graph shows the first task on each CPU as running from the beginning of the time range. 4434Because tracing might start on different CPUs at different times, that is not necessarily the case. 4435Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li> 4436<li>Similarly, the last task on each CPU can be showing running longer than it really was. 4437Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li> 4438<li>When the mouse is over a task, the highlighted task might not be visible on the legend without scrolling if the legend does not fit fully in the window</li> 4439</ol> 4440<h1 id=tables>3. Tables</h1> 4441The Tables menu shows all tables and views in the database. Most tables have an associated view 4442which displays the information in a more friendly way. Not all data for large tables is fetched 4443immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted, 4444but that can be slow for large tables. 4445<p>There are also tables of database meta-information. 4446For SQLite3 databases, the sqlite_master table is included. 4447For PostgreSQL databases, information_schema.tables/views/columns are included. 4448<h3>Find</h3> 4449Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. 4450Refer to Python documentation for the regular expression syntax. 4451All columns are searched, but only currently fetched rows are searched. 4452<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous 4453will go to the next/previous result in id order, instead of display order. 4454""" 4455 4456# Help window 4457 4458class HelpWindow(QMdiSubWindow): 4459 4460 def __init__(self, glb, parent=None): 4461 super(HelpWindow, self).__init__(parent) 4462 4463 self.text = QTextBrowser() 4464 self.text.setHtml(glb_help_text) 4465 self.text.setReadOnly(True) 4466 self.text.setOpenExternalLinks(True) 4467 4468 self.setWidget(self.text) 4469 4470 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help") 4471 4472# Main window that only displays the help text 4473 4474class HelpOnlyWindow(QMainWindow): 4475 4476 def __init__(self, parent=None): 4477 super(HelpOnlyWindow, self).__init__(parent) 4478 4479 self.setMinimumSize(200, 100) 4480 self.resize(800, 600) 4481 self.setWindowTitle("Exported SQL Viewer Help") 4482 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation)) 4483 4484 self.text = QTextBrowser() 4485 self.text.setHtml(glb_help_text) 4486 self.text.setReadOnly(True) 4487 self.text.setOpenExternalLinks(True) 4488 4489 self.setCentralWidget(self.text) 4490 4491# PostqreSQL server version 4492 4493def PostqreSQLServerVersion(db): 4494 query = QSqlQuery(db) 4495 QueryExec(query, "SELECT VERSION()") 4496 if query.next(): 4497 v_str = query.value(0) 4498 v_list = v_str.strip().split(" ") 4499 if v_list[0] == "PostgreSQL" and v_list[2] == "on": 4500 return v_list[1] 4501 return v_str 4502 return "Unknown" 4503 4504# SQLite version 4505 4506def SQLiteVersion(db): 4507 query = QSqlQuery(db) 4508 QueryExec(query, "SELECT sqlite_version()") 4509 if query.next(): 4510 return query.value(0) 4511 return "Unknown" 4512 4513# About dialog 4514 4515class AboutDialog(QDialog): 4516 4517 def __init__(self, glb, parent=None): 4518 super(AboutDialog, self).__init__(parent) 4519 4520 self.setWindowTitle("About Exported SQL Viewer") 4521 self.setMinimumWidth(300) 4522 4523 pyside_version = "1" if pyside_version_1 else "2" 4524 4525 text = "<pre>" 4526 text += "Python version: " + sys.version.split(" ")[0] + "\n" 4527 text += "PySide version: " + pyside_version + "\n" 4528 text += "Qt version: " + qVersion() + "\n" 4529 if glb.dbref.is_sqlite3: 4530 text += "SQLite version: " + SQLiteVersion(glb.db) + "\n" 4531 else: 4532 text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n" 4533 text += "</pre>" 4534 4535 self.text = QTextBrowser() 4536 self.text.setHtml(text) 4537 self.text.setReadOnly(True) 4538 self.text.setOpenExternalLinks(True) 4539 4540 self.vbox = QVBoxLayout() 4541 self.vbox.addWidget(self.text) 4542 4543 self.setLayout(self.vbox) 4544 4545# Font resize 4546 4547def ResizeFont(widget, diff): 4548 font = widget.font() 4549 sz = font.pointSize() 4550 font.setPointSize(sz + diff) 4551 widget.setFont(font) 4552 4553def ShrinkFont(widget): 4554 ResizeFont(widget, -1) 4555 4556def EnlargeFont(widget): 4557 ResizeFont(widget, 1) 4558 4559# Unique name for sub-windows 4560 4561def NumberedWindowName(name, nr): 4562 if nr > 1: 4563 name += " <" + str(nr) + ">" 4564 return name 4565 4566def UniqueSubWindowName(mdi_area, name): 4567 nr = 1 4568 while True: 4569 unique_name = NumberedWindowName(name, nr) 4570 ok = True 4571 for sub_window in mdi_area.subWindowList(): 4572 if sub_window.name == unique_name: 4573 ok = False 4574 break 4575 if ok: 4576 return unique_name 4577 nr += 1 4578 4579# Add a sub-window 4580 4581def AddSubWindow(mdi_area, sub_window, name): 4582 unique_name = UniqueSubWindowName(mdi_area, name) 4583 sub_window.setMinimumSize(200, 100) 4584 sub_window.resize(800, 600) 4585 sub_window.setWindowTitle(unique_name) 4586 sub_window.setAttribute(Qt.WA_DeleteOnClose) 4587 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon)) 4588 sub_window.name = unique_name 4589 mdi_area.addSubWindow(sub_window) 4590 sub_window.show() 4591 4592# Main window 4593 4594class MainWindow(QMainWindow): 4595 4596 def __init__(self, glb, parent=None): 4597 super(MainWindow, self).__init__(parent) 4598 4599 self.glb = glb 4600 4601 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname) 4602 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) 4603 self.setMinimumSize(200, 100) 4604 4605 self.mdi_area = QMdiArea() 4606 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) 4607 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) 4608 4609 self.setCentralWidget(self.mdi_area) 4610 4611 menu = self.menuBar() 4612 4613 file_menu = menu.addMenu("&File") 4614 file_menu.addAction(CreateExitAction(glb.app, self)) 4615 4616 edit_menu = menu.addMenu("&Edit") 4617 edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy)) 4618 edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self)) 4619 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find)) 4620 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)])) 4621 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")])) 4622 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")])) 4623 4624 reports_menu = menu.addMenu("&Reports") 4625 if IsSelectable(glb.db, "calls"): 4626 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self)) 4627 4628 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"): 4629 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self)) 4630 4631 self.EventMenu(GetEventList(glb.db), reports_menu) 4632 4633 if IsSelectable(glb.db, "calls"): 4634 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self)) 4635 4636 if IsSelectable(glb.db, "context_switches"): 4637 charts_menu = menu.addMenu("&Charts") 4638 charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self)) 4639 4640 self.TableMenu(GetTableList(glb), menu) 4641 4642 self.window_menu = WindowMenu(self.mdi_area, menu) 4643 4644 help_menu = menu.addMenu("&Help") 4645 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents)) 4646 help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self)) 4647 4648 def Try(self, fn): 4649 win = self.mdi_area.activeSubWindow() 4650 if win: 4651 try: 4652 fn(win.view) 4653 except: 4654 pass 4655 4656 def CopyToClipboard(self): 4657 self.Try(CopyCellsToClipboardHdr) 4658 4659 def CopyToClipboardCSV(self): 4660 self.Try(CopyCellsToClipboardCSV) 4661 4662 def Find(self): 4663 win = self.mdi_area.activeSubWindow() 4664 if win: 4665 try: 4666 win.find_bar.Activate() 4667 except: 4668 pass 4669 4670 def FetchMoreRecords(self): 4671 win = self.mdi_area.activeSubWindow() 4672 if win: 4673 try: 4674 win.fetch_bar.Activate() 4675 except: 4676 pass 4677 4678 def ShrinkFont(self): 4679 self.Try(ShrinkFont) 4680 4681 def EnlargeFont(self): 4682 self.Try(EnlargeFont) 4683 4684 def EventMenu(self, events, reports_menu): 4685 branches_events = 0 4686 for event in events: 4687 event = event.split(":")[0] 4688 if event == "branches": 4689 branches_events += 1 4690 dbid = 0 4691 for event in events: 4692 dbid += 1 4693 event = event.split(":")[0] 4694 if event == "branches": 4695 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")" 4696 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self)) 4697 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")" 4698 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self)) 4699 4700 def TimeChartByCPU(self): 4701 TimeChartByCPUWindow(self.glb, self) 4702 4703 def TableMenu(self, tables, menu): 4704 table_menu = menu.addMenu("&Tables") 4705 for table in tables: 4706 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self)) 4707 4708 def NewCallGraph(self): 4709 CallGraphWindow(self.glb, self) 4710 4711 def NewCallTree(self): 4712 CallTreeWindow(self.glb, self) 4713 4714 def NewTopCalls(self): 4715 dialog = TopCallsDialog(self.glb, self) 4716 ret = dialog.exec_() 4717 if ret: 4718 TopCallsWindow(self.glb, dialog.report_vars, self) 4719 4720 def NewBranchView(self, event_id): 4721 BranchWindow(self.glb, event_id, ReportVars(), self) 4722 4723 def NewSelectedBranchView(self, event_id): 4724 dialog = SelectedBranchDialog(self.glb, self) 4725 ret = dialog.exec_() 4726 if ret: 4727 BranchWindow(self.glb, event_id, dialog.report_vars, self) 4728 4729 def NewTableView(self, table_name): 4730 TableWindow(self.glb, table_name, self) 4731 4732 def Help(self): 4733 HelpWindow(self.glb, self) 4734 4735 def About(self): 4736 dialog = AboutDialog(self.glb, self) 4737 dialog.exec_() 4738 4739# XED Disassembler 4740 4741class xed_state_t(Structure): 4742 4743 _fields_ = [ 4744 ("mode", c_int), 4745 ("width", c_int) 4746 ] 4747 4748class XEDInstruction(): 4749 4750 def __init__(self, libxed): 4751 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion 4752 xedd_t = c_byte * 512 4753 self.xedd = xedd_t() 4754 self.xedp = addressof(self.xedd) 4755 libxed.xed_decoded_inst_zero(self.xedp) 4756 self.state = xed_state_t() 4757 self.statep = addressof(self.state) 4758 # Buffer for disassembled instruction text 4759 self.buffer = create_string_buffer(256) 4760 self.bufferp = addressof(self.buffer) 4761 4762class LibXED(): 4763 4764 def __init__(self): 4765 try: 4766 self.libxed = CDLL("libxed.so") 4767 except: 4768 self.libxed = None 4769 if not self.libxed: 4770 self.libxed = CDLL("/usr/local/lib/libxed.so") 4771 4772 self.xed_tables_init = self.libxed.xed_tables_init 4773 self.xed_tables_init.restype = None 4774 self.xed_tables_init.argtypes = [] 4775 4776 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero 4777 self.xed_decoded_inst_zero.restype = None 4778 self.xed_decoded_inst_zero.argtypes = [ c_void_p ] 4779 4780 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode 4781 self.xed_operand_values_set_mode.restype = None 4782 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ] 4783 4784 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode 4785 self.xed_decoded_inst_zero_keep_mode.restype = None 4786 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ] 4787 4788 self.xed_decode = self.libxed.xed_decode 4789 self.xed_decode.restype = c_int 4790 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ] 4791 4792 self.xed_format_context = self.libxed.xed_format_context 4793 self.xed_format_context.restype = c_uint 4794 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ] 4795 4796 self.xed_tables_init() 4797 4798 def Instruction(self): 4799 return XEDInstruction(self) 4800 4801 def SetMode(self, inst, mode): 4802 if mode: 4803 inst.state.mode = 4 # 32-bit 4804 inst.state.width = 4 # 4 bytes 4805 else: 4806 inst.state.mode = 1 # 64-bit 4807 inst.state.width = 8 # 8 bytes 4808 self.xed_operand_values_set_mode(inst.xedp, inst.statep) 4809 4810 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip): 4811 self.xed_decoded_inst_zero_keep_mode(inst.xedp) 4812 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt) 4813 if err: 4814 return 0, "" 4815 # Use AT&T mode (2), alternative is Intel (3) 4816 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0) 4817 if not ok: 4818 return 0, "" 4819 if sys.version_info[0] == 2: 4820 result = inst.buffer.value 4821 else: 4822 result = inst.buffer.value.decode() 4823 # Return instruction length and the disassembled instruction text 4824 # For now, assume the length is in byte 166 4825 return inst.xedd[166], result 4826 4827def TryOpen(file_name): 4828 try: 4829 return open(file_name, "rb") 4830 except: 4831 return None 4832 4833def Is64Bit(f): 4834 result = sizeof(c_void_p) 4835 # ELF support only 4836 pos = f.tell() 4837 f.seek(0) 4838 header = f.read(7) 4839 f.seek(pos) 4840 magic = header[0:4] 4841 if sys.version_info[0] == 2: 4842 eclass = ord(header[4]) 4843 encoding = ord(header[5]) 4844 version = ord(header[6]) 4845 else: 4846 eclass = header[4] 4847 encoding = header[5] 4848 version = header[6] 4849 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1: 4850 result = True if eclass == 2 else False 4851 return result 4852 4853# Global data 4854 4855class Glb(): 4856 4857 def __init__(self, dbref, db, dbname): 4858 self.dbref = dbref 4859 self.db = db 4860 self.dbname = dbname 4861 self.home_dir = os.path.expanduser("~") 4862 self.buildid_dir = os.getenv("PERF_BUILDID_DIR") 4863 if self.buildid_dir: 4864 self.buildid_dir += "/.build-id/" 4865 else: 4866 self.buildid_dir = self.home_dir + "/.debug/.build-id/" 4867 self.app = None 4868 self.mainwindow = None 4869 self.instances_to_shutdown_on_exit = weakref.WeakSet() 4870 try: 4871 self.disassembler = LibXED() 4872 self.have_disassembler = True 4873 except: 4874 self.have_disassembler = False 4875 self.host_machine_id = 0 4876 self.host_start_time = 0 4877 self.host_finish_time = 0 4878 4879 def FileFromBuildId(self, build_id): 4880 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf" 4881 return TryOpen(file_name) 4882 4883 def FileFromNamesAndBuildId(self, short_name, long_name, build_id): 4884 # Assume current machine i.e. no support for virtualization 4885 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore": 4886 file_name = os.getenv("PERF_KCORE") 4887 f = TryOpen(file_name) if file_name else None 4888 if f: 4889 return f 4890 # For now, no special handling if long_name is /proc/kcore 4891 f = TryOpen(long_name) 4892 if f: 4893 return f 4894 f = self.FileFromBuildId(build_id) 4895 if f: 4896 return f 4897 return None 4898 4899 def AddInstanceToShutdownOnExit(self, instance): 4900 self.instances_to_shutdown_on_exit.add(instance) 4901 4902 # Shutdown any background processes or threads 4903 def ShutdownInstances(self): 4904 for x in self.instances_to_shutdown_on_exit: 4905 try: 4906 x.Shutdown() 4907 except: 4908 pass 4909 4910 def GetHostMachineId(self): 4911 query = QSqlQuery(self.db) 4912 QueryExec(query, "SELECT id FROM machines WHERE pid = -1") 4913 if query.next(): 4914 self.host_machine_id = query.value(0) 4915 else: 4916 self.host_machine_id = 0 4917 return self.host_machine_id 4918 4919 def HostMachineId(self): 4920 if self.host_machine_id: 4921 return self.host_machine_id 4922 return self.GetHostMachineId() 4923 4924 def SelectValue(self, sql): 4925 query = QSqlQuery(self.db) 4926 try: 4927 QueryExec(query, sql) 4928 except: 4929 return None 4930 if query.next(): 4931 return Decimal(query.value(0)) 4932 return None 4933 4934 def SwitchesMinTime(self, machine_id): 4935 return self.SelectValue("SELECT time" 4936 " FROM context_switches" 4937 " WHERE time != 0 AND machine_id = " + str(machine_id) + 4938 " ORDER BY id LIMIT 1") 4939 4940 def SwitchesMaxTime(self, machine_id): 4941 return self.SelectValue("SELECT time" 4942 " FROM context_switches" 4943 " WHERE time != 0 AND machine_id = " + str(machine_id) + 4944 " ORDER BY id DESC LIMIT 1") 4945 4946 def SamplesMinTime(self, machine_id): 4947 return self.SelectValue("SELECT time" 4948 " FROM samples" 4949 " WHERE time != 0 AND machine_id = " + str(machine_id) + 4950 " ORDER BY id LIMIT 1") 4951 4952 def SamplesMaxTime(self, machine_id): 4953 return self.SelectValue("SELECT time" 4954 " FROM samples" 4955 " WHERE time != 0 AND machine_id = " + str(machine_id) + 4956 " ORDER BY id DESC LIMIT 1") 4957 4958 def CallsMinTime(self, machine_id): 4959 return self.SelectValue("SELECT calls.call_time" 4960 " FROM calls" 4961 " INNER JOIN threads ON threads.thread_id = calls.thread_id" 4962 " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) + 4963 " ORDER BY calls.id LIMIT 1") 4964 4965 def CallsMaxTime(self, machine_id): 4966 return self.SelectValue("SELECT calls.return_time" 4967 " FROM calls" 4968 " INNER JOIN threads ON threads.thread_id = calls.thread_id" 4969 " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) + 4970 " ORDER BY calls.return_time DESC LIMIT 1") 4971 4972 def GetStartTime(self, machine_id): 4973 t0 = self.SwitchesMinTime(machine_id) 4974 t1 = self.SamplesMinTime(machine_id) 4975 t2 = self.CallsMinTime(machine_id) 4976 if t0 is None or (not(t1 is None) and t1 < t0): 4977 t0 = t1 4978 if t0 is None or (not(t2 is None) and t2 < t0): 4979 t0 = t2 4980 return t0 4981 4982 def GetFinishTime(self, machine_id): 4983 t0 = self.SwitchesMaxTime(machine_id) 4984 t1 = self.SamplesMaxTime(machine_id) 4985 t2 = self.CallsMaxTime(machine_id) 4986 if t0 is None or (not(t1 is None) and t1 > t0): 4987 t0 = t1 4988 if t0 is None or (not(t2 is None) and t2 > t0): 4989 t0 = t2 4990 return t0 4991 4992 def HostStartTime(self): 4993 if self.host_start_time: 4994 return self.host_start_time 4995 self.host_start_time = self.GetStartTime(self.HostMachineId()) 4996 return self.host_start_time 4997 4998 def HostFinishTime(self): 4999 if self.host_finish_time: 5000 return self.host_finish_time 5001 self.host_finish_time = self.GetFinishTime(self.HostMachineId()) 5002 return self.host_finish_time 5003 5004 def StartTime(self, machine_id): 5005 if machine_id == self.HostMachineId(): 5006 return self.HostStartTime() 5007 return self.GetStartTime(machine_id) 5008 5009 def FinishTime(self, machine_id): 5010 if machine_id == self.HostMachineId(): 5011 return self.HostFinishTime() 5012 return self.GetFinishTime(machine_id) 5013 5014# Database reference 5015 5016class DBRef(): 5017 5018 def __init__(self, is_sqlite3, dbname): 5019 self.is_sqlite3 = is_sqlite3 5020 self.dbname = dbname 5021 self.TRUE = "TRUE" 5022 self.FALSE = "FALSE" 5023 # SQLite prior to version 3.23 does not support TRUE and FALSE 5024 if self.is_sqlite3: 5025 self.TRUE = "1" 5026 self.FALSE = "0" 5027 5028 def Open(self, connection_name): 5029 dbname = self.dbname 5030 if self.is_sqlite3: 5031 db = QSqlDatabase.addDatabase("QSQLITE", connection_name) 5032 else: 5033 db = QSqlDatabase.addDatabase("QPSQL", connection_name) 5034 opts = dbname.split() 5035 for opt in opts: 5036 if "=" in opt: 5037 opt = opt.split("=") 5038 if opt[0] == "hostname": 5039 db.setHostName(opt[1]) 5040 elif opt[0] == "port": 5041 db.setPort(int(opt[1])) 5042 elif opt[0] == "username": 5043 db.setUserName(opt[1]) 5044 elif opt[0] == "password": 5045 db.setPassword(opt[1]) 5046 elif opt[0] == "dbname": 5047 dbname = opt[1] 5048 else: 5049 dbname = opt 5050 5051 db.setDatabaseName(dbname) 5052 if not db.open(): 5053 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text()) 5054 return db, dbname 5055 5056# Main 5057 5058def Main(): 5059 usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \ 5060 " or: exported-sql-viewer.py --help-only" 5061 ap = argparse.ArgumentParser(usage = usage_str, add_help = False) 5062 ap.add_argument("--pyside-version-1", action='store_true') 5063 ap.add_argument("dbname", nargs="?") 5064 ap.add_argument("--help-only", action='store_true') 5065 args = ap.parse_args() 5066 5067 if args.help_only: 5068 app = QApplication(sys.argv) 5069 mainwindow = HelpOnlyWindow() 5070 mainwindow.show() 5071 err = app.exec_() 5072 sys.exit(err) 5073 5074 dbname = args.dbname 5075 if dbname is None: 5076 ap.print_usage() 5077 print("Too few arguments") 5078 sys.exit(1) 5079 5080 is_sqlite3 = False 5081 try: 5082 f = open(dbname, "rb") 5083 if f.read(15) == b'SQLite format 3': 5084 is_sqlite3 = True 5085 f.close() 5086 except: 5087 pass 5088 5089 dbref = DBRef(is_sqlite3, dbname) 5090 db, dbname = dbref.Open("main") 5091 glb = Glb(dbref, db, dbname) 5092 app = QApplication(sys.argv) 5093 glb.app = app 5094 mainwindow = MainWindow(glb) 5095 glb.mainwindow = mainwindow 5096 mainwindow.show() 5097 err = app.exec_() 5098 glb.ShutdownInstances() 5099 db.close() 5100 sys.exit(err) 5101 5102if __name__ == "__main__": 5103 Main() 5104