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