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