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