#!/usr/bin/env python # SPDX-License-Identifier: GPL-2.0 # exported-sql-viewer.py: view data from sql database # Copyright (c) 2014-2018, Intel Corporation.
# To use this script you will need to have exported data using either the # export-to-sqlite.py or the export-to-postgresql.py script. Refer to those # scripts for details. # # Following on from the example in the export scripts, a # call-graph can be displayed for the pt_example database like this: # # python tools/perf/scripts/python/exported-sql-viewer.py pt_example # # Note that for PostgreSQL, this script supports connecting to remote databases # by setting hostname, port, username, password, and dbname e.g. # # python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example" # # The result is a GUI window with a tree representing a context-sensitive # call-graph. Expanding a couple of levels of the tree and adjusting column # widths to suit will display something like: # # Call Graph: pt_example # Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%) # v- ls # v- 2638:2638 # v- _start ld-2.19.so 1 10074071 100.0 211135 100.0 # |- unknown unknown 1 13198 0.1 1 0.0 # >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3 # >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3 # v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4 # >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1 # >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0 # >- __libc_csu_init ls 1 10354 0.1 10 0.0 # |- _setjmp libc-2.19.so 1 0 0.0 4 0.0 # v- main ls 1 8182043 99.6 180254 99.9 # # Points to note: # The top level is a command name (comm) # The next level is a thread (pid:tid) # Subsequent levels are functions # 'Count' is the number of calls # 'Time' is the elapsed time until the function returns # Percentages are relative to the level above # 'Branch Count' is the total number of branches for that function and all # functions that it calls
# There is also a "All branches" report, which displays branches and # possibly disassembly. However, presently, the only supported disassembler is # Intel XED, and additionally the object code must be present in perf build ID # cache. To use Intel XED, libxed.so must be present. To build and install # libxed.so: # git clone https://github.com/intelxed/mbuild.git mbuild # git clone https://github.com/intelxed/xed # cd xed # ./mfile.py --share # sudo ./mfile.py --prefix=/usr/local install # sudo ldconfig # # Example report: # # Time CPU Command PID TID Branch Type In Tx Branch # 8107675239590 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so) # 7fab593ea260 48 89 e7 mov %rsp, %rdi # 8107675239899 2 ls 22011 22011 hardware interrupt No 7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) # 8107675241900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so) # 7fab593ea260 48 89 e7 mov %rsp, %rdi # 7fab593ea263 e8 c8 06 00 00 callq 0x7fab593ea930 # 8107675241900 2 ls 22011 22011 call No 7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so) # 7fab593ea930 55 pushq %rbp # 7fab593ea931 48 89 e5 mov %rsp, %rbp # 7fab593ea934 41 57 pushq %r15 # 7fab593ea936 41 56 pushq %r14 # 7fab593ea938 41 55 pushq %r13 # 7fab593ea93a 41 54 pushq %r12 # 7fab593ea93c 53 pushq %rbx # 7fab593ea93d 48 89 fb mov %rdi, %rbx # 7fab593ea940 48 83 ec 68 sub $0x68, %rsp # 7fab593ea944 0f 31 rdtsc # 7fab593ea946 48 c1 e2 20 shl $0x20, %rdx # 7fab593ea94a 89 c0 mov %eax, %eax # 7fab593ea94c 48 09 c2 or %rax, %rdx # 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax # 8107675242232 2 ls 22011 22011 hardware interrupt No 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) # 8107675242900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so) # 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax # 7fab593ea956 48 89 15 3b 13 22 00 movq %rdx, 0x22133b(%rip) # 8107675243232 2 ls 22011 22011 hardware interrupt No 7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
from __future__ import print_function
import sys # Only change warnings if the python -W option was not used ifnot sys.warnoptions: import warnings # PySide2 causes deprecation warnings, ignore them.
warnings.filterwarnings("ignore", category=DeprecationWarning) import argparse import weakref import threading import string try: # Python2 import cPickle as pickle # size of pickled integer big enough for record size
glb_nsz = 8 except ImportError: import pickle
glb_nsz = 16 import re import os import random import copy import math from libxed import LibXED
pyside_version_1 = True ifnot"--pyside-version-1"in sys.argv: try: from PySide2.QtCore import * from PySide2.QtGui import * from PySide2.QtSql import * from PySide2.QtWidgets import *
pyside_version_1 = False except: pass
if pyside_version_1: from PySide.QtCore import * from PySide.QtGui import * from PySide.QtSql import *
from decimal import Decimal, ROUND_HALF_UP from ctypes import CDLL, Structure, create_string_buffer, addressof, sizeof, \
c_void_p, c_bool, c_byte, c_char, c_int, c_uint, c_longlong, c_ulonglong from multiprocessing import Process, Array, Value, Event
# xrange is range in Python3 try:
xrange except NameError:
xrange = range
def run(self): whileTrue: if self.param isNone:
done, result = self.task() else:
done, result = self.task(self.param)
self.done.emit(result) if done: break
def headerData(self, section, orientation, role): if role == Qt.TextAlignmentRole: return self.columnAlignment(section) if role != Qt.DisplayRole: returnNone if orientation != Qt.Horizontal: returnNone return self.columnHeader(section)
def parent(self, child):
child_item = child.internalPointer() if child_item is self.root: return QModelIndex()
parent_item = child_item.getParentItem() return self.createIndex(parent_item.getRow(), 0, parent_item)
def data(self, index, role): if role == Qt.TextAlignmentRole: return self.columnAlignment(index.column()) if role == Qt.FontRole: return self.columnFont(index.column()) if role != Qt.DisplayRole: returnNone
item = index.internalPointer() return self.DisplayData(item, index)
def headerData(self, section, orientation, role): if role == Qt.TextAlignmentRole: return self.columnAlignment(section) if role != Qt.DisplayRole: returnNone if orientation != Qt.Horizontal: returnNone return self.columnHeader(section)
def data(self, index, role): if role == Qt.TextAlignmentRole: return self.columnAlignment(index.column()) if role == Qt.FontRole: return self.columnFont(index.column()) if role != Qt.DisplayRole: returnNone
item = index.internalPointer() return self.DisplayData(item, index)
def LookupCreateModel(model_name, create_fn):
model_cache_lock.acquire() try:
model = model_cache[model_name] except:
model = None if model isNone:
model = create_fn()
model_cache[model_name] = model
model_cache_lock.release() return model
def LookupModel(model_name):
model_cache_lock.acquire() try:
model = model_cache[model_name] except:
model = None
model_cache_lock.release() return model
def Find(self, direction):
value = self.textbox.currentText()
pattern = self.pattern.isChecked()
self.last_value = value
self.last_pattern = pattern
self.finder.Find(value, direction, pattern, self.context)
def ValueChanged(self):
value = self.textbox.currentText()
pattern = self.pattern.isChecked()
index = self.textbox.currentIndex()
data = self.textbox.itemData(index) # Store the pattern in the combo box to keep it with the text value if data == None:
self.textbox.setItemData(index, pattern) else:
self.pattern.setChecked(data)
self.Find(0)
def NextPrev(self, direction):
value = self.textbox.currentText()
pattern = self.pattern.isChecked() if value != self.last_value:
index = self.textbox.findText(value) # Allow for a button press before the value has been added to the combo box if index < 0:
index = self.textbox.count()
self.textbox.addItem(value, pattern)
self.textbox.setCurrentIndex(index) return else:
self.textbox.setItemData(index, pattern) elif pattern != self.last_pattern: # Keep the pattern recorded in the combo box up to date
index = self.textbox.currentIndex()
self.textbox.setItemData(index, pattern)
self.Find(direction)
def FindSelect(self, value, pattern, query): if pattern: # postgresql and sqlite pattern patching differences: # postgresql LIKE is case sensitive but sqlite LIKE is not # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not # postgresql supports ILIKE which is case insensitive # sqlite supports GLOB (text only) which uses * and ? and is case sensitive ifnot self.glb.dbref.is_sqlite3: # Escape % and _
s = value.replace("%", "\\%")
s = s.replace("_", "\\_") # Translate * and ? into SQL LIKE pattern characters % and _ if sys.version_info[0] == 3:
trans = str.maketrans("*?", "%_") else:
trans = string.maketrans("*?", "%_")
match = " LIKE '" + str(s).translate(trans) + "'" else:
match = " GLOB '" + str(value) + "'" else:
match = " = '" + str(value) + "'"
self.DoFindSelect(query, match)
def Found(self, query, found): if found: return self.FindPath(query) return []
def FindValue(self, value, pattern, query, last_value, last_pattern): if last_value == value and pattern == last_pattern:
found = query.first() else:
self.FindSelect(value, pattern, query)
found = query.next() return self.Found(query, found)
def FindNext(self, query):
found = query.next() ifnot found:
found = query.first() return self.Found(query, found)
def FindPrev(self, query):
found = query.previous() ifnot found:
found = query.last() return self.Found(query, found)
def DoFindSelect(self, query, match):
QueryExec(query, "SELECT call_path_id, comm_id, thread_id" " FROM calls" " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" " WHERE calls.id <> 0" " AND symbols.name" + match + " GROUP BY comm_id, thread_id, call_path_id" " ORDER BY comm_id, thread_id, call_path_id")
def FindPath(self, query): # Turn the query result into a list of ids that the tree view can walk # to open the tree at the right place.
ids = []
parent_id = query.value(0) while parent_id:
ids.insert(0, parent_id)
q2 = QSqlQuery(self.glb.db)
QueryExec(q2, "SELECT parent_id" " FROM call_paths" " WHERE id = " + str(parent_id)) ifnot q2.next(): break
parent_id = q2.value(0) # The call path root is not used if ids[0] == 1: del ids[0]
ids.insert(0, query.value(2))
ids.insert(0, query.value(1)) return ids
# Call tree data model level 2+ item base
class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
def DoFindSelect(self, query, match):
QueryExec(query, "SELECT calls.id, comm_id, thread_id" " FROM calls" " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" " WHERE calls.id <> 0" " AND symbols.name" + match + " ORDER BY comm_id, thread_id, call_time, calls.id")
def FindPath(self, query): # Turn the query result into a list of ids that the tree view can walk # to open the tree at the right place.
ids = []
parent_id = query.value(0) while parent_id:
ids.insert(0, parent_id)
q2 = QSqlQuery(self.glb.db)
QueryExec(q2, "SELECT parent_id" " FROM calls" " WHERE id = " + str(parent_id)) ifnot q2.next(): break
parent_id = q2.value(0)
ids.insert(0, query.value(2))
ids.insert(0, query.value(1)) return ids
self.layout().setContentsMargins(0, 0, 0, 0) for child in children: if child.isWidgetType():
self.layout().addWidget(child) else:
self.layout().addLayout(child)
self.layout().setContentsMargins(0, 0, 0, 0) for child in children: if child.isWidgetType():
self.layout().addWidget(child) else:
self.layout().addLayout(child)
if thread_at_time:
self.DisplayThreadAtTime(*thread_at_time)
def DisplayThreadAtTime(self, comm_id, thread_id, time):
parent = QModelIndex() for dbid in (comm_id, thread_id):
found = False
n = self.model.rowCount(parent) for row in xrange(n):
child = self.model.index(row, 0, parent) if child.internalPointer().dbid == dbid:
found = True
self.view.setExpanded(parent, True)
self.view.setCurrentIndex(child)
parent = child break ifnot found: return
found = False whileTrue:
n = self.model.rowCount(parent) ifnot n: return
last_child = None for row in xrange(n):
self.view.setExpanded(parent, True)
child = self.model.index(row, 0, parent)
child_call_time = child.internalPointer().call_time if child_call_time < time:
last_child = child elif child_call_time == time:
self.view.setCurrentIndex(child) return elif child_call_time > time: break ifnot last_child: ifnot found:
child = self.model.index(0, 0, parent)
self.view.setExpanded(parent, True)
self.view.setCurrentIndex(child) return
found = True
self.view.setExpanded(parent, True)
self.view.setCurrentIndex(last_child)
parent = last_child
# ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name
def ExecComm(db, thread_id, time):
query = QSqlQuery(db)
QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag" " FROM comm_threads" " INNER JOIN comms ON comms.id = comm_threads.comm_id" " WHERE comm_threads.thread_id = " + str(thread_id) + " ORDER BY comms.c_time, comms.id")
first = None
last = None while query.next(): if first isNone:
first = query.value(0) if query.value(2) and Decimal(query.value(1)) <= Decimal(time):
last = query.value(0) ifnot(last isNone): return last return first
# Container for (x, y) data
class XY(): def __init__(self, x=0, y=0):
self.x = x
self.y = y
def paint(self, painter, option, widget):
last = None for point in self.data.points:
self.PaintPoint(painter, last, point.x) if point.x > self.attrs.subrange.x.hi: break;
last = point
self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1)
def Step(self):
attrs = self.parentItem().attrs
subrange = attrs.subrange.x
t = subrange.hi - subrange.lo
s = (3.0 * t) / self.width
n = 1.0 while s > n:
n = n * 10.0 return n
def PaintMarks(self, painter, at_y, lo, hi, step, i):
attrs = self.parentItem().attrs
x = lo while x <= hi:
xp = attrs.XToPixel(x) if i % 10: if i % 5:
sz = 1 else:
sz = 2 else:
sz = self.max_mark_sz
i = 0
painter.drawLine(xp, at_y, xp, at_y + sz)
x += step
i += 1
def paint(self, painter, option, widget): # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1
painter.drawLine(0, 0, self.width - 1, 0)
n = self.Step()
attrs = self.parentItem().attrs
subrange = attrs.subrange.x if subrange.lo:
x_offset = n - (subrange.lo % n) else:
x_offset = 0.0
x = subrange.lo + x_offset
i = (x / n) % 10
self.PaintMarks(painter, 0, x, subrange.hi, n, i)
def ScaleDimensions(self):
n = self.Step()
attrs = self.parentItem().attrs
lo = attrs.subrange.x.lo
hi = (n * 10.0) + lo
width = attrs.XToPixel(hi) if width > 500:
width = 0 return (n, lo, hi, width)
def Text(self):
unit = self.Unit() if unit >= 1000000000:
unit = int(unit / 1000000000)
us = "s" elif unit >= 1000000:
unit = int(unit / 1000000)
us = "ms" elif unit >= 1000:
unit = int(unit / 1000)
us = "us" else:
unit = int(unit)
us = "ns" return" = " + str(unit) + " " + us
# Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data
def RubberBandX(self, event):
x = event.pos().toPoint().x() if x < self.rb_xlo:
x = self.rb_xlo elif x > self.rb_xhi:
x = self.rb_xhi else:
self.rb_in_view = True return x
def RubberBandRect(self, x): if self.rb_origin.x() <= x:
width = x - self.rb_origin.x()
rect = QRect(self.rb_origin, QSize(width, self.height)) else:
width = self.rb_origin.x() - x
top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y())
rect = QRect(top_left, QSize(width, self.height)) return rect
def MousePressEvent(self, event):
self.rb_in_view = False
x = self.RubberBandX(event)
self.rb_origin = QPoint(x, self.top) if self.rubber_band isNone:
self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent())
self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height))) if self.rb_in_view:
self.rubber_band.show()
self.rb_event_handler.RBMoveEvent(x, x) else:
self.rubber_band.hide()
def MouseMoveEvent(self, event):
x = self.RubberBandX(event)
rect = self.RubberBandRect(x)
self.RubberBandSetGeometry(rect) if self.rb_in_view:
self.rubber_band.show()
self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x)
def MouseReleaseEvent(self, event):
x = self.RubberBandX(event) if self.rb_in_view:
selection_state = self.RubberBandRect(x) else:
selection_state = None
self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state)
# Switch graph legend data model
class SwitchGraphLegendModel(QAbstractTableModel):
def data(self, index, role): if role == Qt.BackgroundRole:
child = self.child_items[index.row()] if child in self.highlight_set: return self.region_attributes[child.key].colour returnNone if role == Qt.ForegroundRole:
child = self.child_items[index.row()] if child in self.highlight_set: return QColor(255, 255, 255) return self.region_attributes[child.key].colour if role != Qt.DisplayRole: returnNone
hregion = self.child_items[index.row()]
col = index.column() if col == 0: return hregion.pid if col == 1: return hregion.tid if col == 2: return hregion.comm returnNone
def Highlight(self, highlight_set): for row in xrange(self.child_count):
child = self.child_items[row] if child in self.highlight_set: if child notin highlight_set:
self.SetHighlight(row, False) elif child in highlight_set:
self.SetHighlight(row, True)
self.highlight_set = highlight_set
def changeEvent(self, event): if event.type() == QEvent.FontChange:
self.view.resizeRowsToContents()
self.view.resizeColumnsToContents() # Need to resize rows again after column resize
self.view.resizeRowsToContents()
super(SwitchGraphLegend, self).changeEvent(event)
# Random colour generation
def RGBColourTooLight(r, g, b): if g > 230: returnTrue if g <= 160: returnFalse if r <= 180 and g <= 180: returnFalse if r < 60: returnFalse returnTrue
def GenerateColours(x):
cs = [0] for i in xrange(1, x):
cs.append(int((255.0 / i) + 0.5))
colours = [] for r in cs: for g in cs: for b in cs: # Exclude black and colours that look too light against a white background if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b): continue
colours.append(QColor(r, g, b)) return colours
def GenerateNColours(n): for x in xrange(2, n + 2):
colours = GenerateColours(x) if len(colours) >= n: return colours return []
def PixelToX(self, px):
x = self.PixelToXRounded(px) if self.pdp.x == 0:
rt = self.XToPixel(x) if rt > px: return x - 1 return x
def PixelToY(self, py):
y = self.PixelToYRounded(py) if self.pdp.y == 0:
rt = self.YToPixel(y) if rt > py: return y - 1 return y
def ToPDP(self, dp, scale): # Calculate pixel decimal places: # (10 ** dp) is the minimum delta in the data # scale it to get the minimum delta in pixels # log10 gives the number of decimals places negatively # subtrace 1 to divide by 10 # round to the lower negative number # change the sign to get the number of decimals positively
x = math.log10((10 ** dp) * scale) if x < 0:
x -= 1
x = -int(math.floor(x) - 0.1) else:
x = 0 return x
def Update(self):
x = self.ToPDP(self.dp.x, self.scale.x)
y = self.ToPDP(self.dp.y, self.scale.y)
self.pdp = XY(x, y) # pixel decimal places
# Switch graph splitter which divides the CPU graphs from the legend
def ToTimeStr(val):
val = Decimal(val) if val >= 1000000000: return"{} s".format((val / 1000000000).quantize(Decimal("0.000000001"))) if val >= 1000000: return"{} ms".format((val / 1000000).quantize(Decimal("0.000001"))) if val >= 1000: return"{} us".format((val / 1000).quantize(Decimal("0.001"))) return"{} ns".format(val.quantize(Decimal("1")))
# Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons
def FindSelect(self):
self.rows = [] if self.pattern:
pattern = re.compile(self.value) for child in self.root.child_items: for column_data in child.data: if re.search(pattern, str(column_data)) isnotNone:
self.rows.append(child.row) break else: for child in self.root.child_items: for column_data in child.data: if self.value in str(column_data):
self.rows.append(child.row) break
def FindValue(self):
self.pos = 0 if self.last_value != self.value or self.pattern != self.last_pattern:
self.FindSelect() ifnot len(self.rows): return -1 return self.rows[self.pos]
def FindThread(self): if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
row = self.FindValue() elif len(self.rows): if self.direction > 0:
self.pos += 1 if self.pos >= len(self.rows):
self.pos = 0 else:
self.pos -= 1 if self.pos < 0:
self.pos = len(self.rows) - 1
row = self.rows[self.pos] else:
row = -1 return (True, row)
def Find(self, value, direction, pattern, context, callback):
self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern) # Use a thread so the UI is not blocked
thread = Thread(self.FindThread)
thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
thread.start()
def WaitForTarget(self): whileTrue:
self.wait_event.clear()
target = self.process_target.value if target > self.fetched or target < 0: break
self.wait_event.wait() return target
def HasSpace(self, sz): if self.local_tail <= self.local_head:
space = len(self.buffer) - self.local_head if space > sz: returnTrue if space >= glb_nsz: # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
self.buffer[self.local_head : self.local_head + len(nd)] = nd
self.local_head = 0 if self.local_tail - self.local_head > sz: returnTrue returnFalse
def WaitForSpace(self, sz): if self.HasSpace(sz): return whileTrue:
self.wait_event.clear()
self.local_tail = self.tail.value if self.HasSpace(sz): return
self.wait_event.wait()
def Fetch(self, nr): ifnot self.more: # -1 inidcates there are no more return -1
result = self.fetched
extra = result + nr - self.target if extra > 0:
self.target += extra # process_target < 0 indicates shutting down if self.process_target.value >= 0:
self.process_target.value = self.target
self.wait_event.set() return result
def RemoveFromBuffer(self):
pos = self.local_tail if len(self.buffer) - pos < glb_nsz:
pos = 0
n = pickle.loads(self.buffer[pos : pos + glb_nsz]) if n == 0:
pos = 0
n = pickle.loads(self.buffer[0 : glb_nsz])
pos += glb_nsz
obj = pickle.loads(self.buffer[pos : pos + n])
self.local_tail = pos + n return obj
def ProcessData(self, count): for i in xrange(count):
obj = self.RemoveFromBuffer()
self.process_data(obj)
self.tail.value = self.local_tail
self.wait_event.set()
self.done.emit(count)
# Fetch more records bar
class FetchMoreRecordsBar():
def __init__(self, model, parent):
self.model = model
self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
def Progress(self, count): if self.in_progress: if count:
percent = ((count - self.start) * 100) / self.Target() if percent >= 100:
self.Idle() else:
self.progress.setValue(percent) ifnot count: # Count value of zero means no more records
self.Done()
QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip" " FROM samples" " INNER JOIN dsos ON samples.to_dso_id = dsos.id" " INNER JOIN symbols ON samples.to_symbol_id = symbols.id" " WHERE samples.id = " + str(self.dbid)) ifnot query.next(): return
cpu = query.value(0)
dso = query.value(1)
sym = query.value(2) if dso == 0 or sym == 0: return
off = query.value(3)
short_name = query.value(4)
long_name = query.value(5)
build_id = query.value(6)
sym_start = query.value(7)
ip = query.value(8)
QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start" " FROM samples" " INNER JOIN symbols ON samples.symbol_id = symbols.id" " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) + " ORDER BY samples.id" " LIMIT 1") ifnot query.next(): return if query.value(0) != dso: # Cannot disassemble from one dso to another return
bsym = query.value(1)
boff = query.value(2)
bsym_start = query.value(3) if bsym == 0: return
tot = bsym_start + boff + 1 - sym_start - off if tot <= 0 or tot > 16384: return
inst = self.glb.disassembler.Instruction()
f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id) ifnot f: return
mode = 0 if Is64Bit(f) else 1
self.glb.disassembler.SetMode(inst, mode)
buf_sz = tot + 16
buf = create_string_buffer(tot + 16)
f.seek(sym_start + off)
buf.value = f.read(buf_sz)
buf_ptr = addressof(buf)
i = 0 while tot > 0:
cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip) if cnt:
byte_str = tohex(ip).rjust(16) for k in xrange(cnt):
byte_str += " %02x" % ord(buf[i])
i += 1 while k < 15:
byte_str += " "
k += 1
self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self))
self.child_count += 1 else: return
buf_ptr += cnt
tot -= cnt
buf_sz -= cnt
ip += cnt
def BranchDataPrep(query):
data = [] for i in xrange(0, 8):
data.append(query.value(i))
BranchDataPrepBr(query, data) return data
def BranchDataPrepWA(query):
data = []
data.append(query.value(0)) # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
data.append("{:>19}".format(query.value(1))) for i in xrange(2, 8):
data.append(query.value(i))
BranchDataPrepBr(query, data) return data
def BranchDataWithIPCPrep(query):
data = [] for i in xrange(0, 8):
data.append(query.value(i))
BranchDataPrepIPC(query, data)
BranchDataPrepBr(query, data) return data
def BranchDataWithIPCPrepWA(query):
data = []
data.append(query.value(0)) # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
data.append("{:>19}".format(query.value(1))) for i in xrange(2, 8):
data.append(query.value(i))
BranchDataPrepIPC(query, data)
BranchDataPrepBr(query, data) return data
# Branch data model
class BranchModel(TreeModel):
progress = Signal(object)
def __init__(self, glb, event_id, where_clause, parent=None):
super(BranchModel, self).__init__(glb, None, parent)
self.event_id = event_id
self.more = True
self.populated = 0
self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count") if self.have_ipc:
select_ipc = ", insn_count, cyc_count"
prep_fn = BranchDataWithIPCPrep
prep_wa_fn = BranchDataWithIPCPrepWA else:
select_ipc = ""
prep_fn = BranchDataPrep
prep_wa_fn = BranchDataPrepWA
sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name," " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END," " ip, symbols.name, sym_offset, dsos.short_name," " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
+ select_ipc + " FROM samples" " INNER JOIN comms ON comm_id = comms.id" " INNER JOIN threads ON thread_id = threads.id" " INNER JOIN branch_types ON branch_type = branch_types.id" " INNER JOIN symbols ON symbol_id = symbols.id" " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id" " INNER JOIN dsos ON samples.dso_id = dsos.id" " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id" " WHERE samples.id > $$last_id$$" + where_clause + " AND evsel_id = " + str(self.event_id) + " ORDER BY samples.id" " LIMIT " + str(glb_chunk_sz)) if pyside_version_1 and sys.version_info[0] == 3:
prep = prep_fn else:
prep = prep_wa_fn
self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
self.fetcher.done.connect(self.Update)
self.fetcher.Fetch(glb_chunk_sz)
def GetRoot(self): return BranchRootItem()
def columnCount(self, parent=None): if self.have_ipc: return 11 else: return 8
def ResizeColumnToContents(self, column, n): # Using the view's resizeColumnToContents() here is extrememly slow # so implement a crude alternative
mm = "MM"if column else"MMMM"
font = self.view.font()
metrics = QFontMetrics(font)
max = 0 for row in xrange(n):
val = self.model.root.child_items[row].data[column]
len = metrics.width(str(val) + mm)
max = len if len > max else max
val = self.model.columnHeader(column)
len = metrics.width(str(val) + mm)
max = len if len > max else max
self.view.setColumnWidth(column, max)
def ResizeColumnsToContents(self):
n = min(self.model.root.child_count, 100) if n < 1: # No data yet, so connect a signal to notify when there is
self.model.rowsInserted.connect(self.UpdateColumnWidths) return
columns = self.model.columnCount() for i in xrange(columns):
self.ResizeColumnToContents(i, n)
def UpdateColumnWidths(self, *x): # This only needs to be done once, so disconnect the signal now
self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
self.ResizeColumnsToContents()
query = QSqlQuery(glb.db)
QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1") if query.next():
self.last_id = int(query.value(0))
self.first_time = int(glb.HostStartTime())
self.last_time = int(glb.HostFinishTime()) if placeholder_text:
placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
def IdBetween(self, query, lower_id, higher_id, order):
QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1") if query.next(): returnTrue, int(query.value(0)) else: returnFalse, 0
def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
query = QSqlQuery(self.glb.db) whileTrue:
next_id = int((lower_id + higher_id) / 2)
QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) ifnot query.next():
ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC") ifnot ok:
ok, dbid = self.IdBetween(query, next_id, higher_id, "") ifnot ok: return str(higher_id)
next_id = dbid
QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
next_time = int(query.value(0)) if get_floor: if target_time > next_time:
lower_id = next_id else:
higher_id = next_id if higher_id <= lower_id + 1: return str(higher_id) else: if target_time >= next_time:
lower_id = next_id else:
higher_id = next_id if higher_id <= lower_id + 1: return str(lower_id)
def ConvertRelativeTime(self, val):
mult = 1
suffix = val[-2:] if suffix == "ms":
mult = 1000000 elif suffix == "us":
mult = 1000 elif suffix == "ns":
mult = 1 else: return val
val = val[:-2].strip() ifnot self.IsNumber(val): return val
val = int(val) * mult if val >= 0:
val += self.first_time else:
val += self.last_time return str(val)
def AddTimeRange(self, value, ranges):
n = value.count("-") if n == 1: pass elif n == 2: if value.split("-")[1].strip() == "":
n = 1 elif n == 3:
n = 2 else: returnFalse
pos = findnth(value, "-", n)
vrange = [value[:pos].strip() ,value[pos+1:].strip()] if self.ConvertTimeRange(vrange):
ranges.append(vrange) returnTrue returnFalse
def DoValidate(self, input_string):
ranges = [] for value in [x.strip() for x in input_string.split(",")]: ifnot self.AddTimeRange(value, ranges): return self.InvalidValue(value)
ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
self.value = " OR ".join(ranges)
for row in xrange(len(self.data_items)):
self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
self.grid.addWidget(self.data_items[row].widget, row, 1)
def Ok(self):
vars = self.report_vars for d in self.data_items: if d.id == "REPORTNAME":
vars.name = d.value ifnot vars.name:
self.ShowMessage("Report name is required") return for d in self.data_items: ifnot d.IsValid(): return for d in self.data_items[1:]: if d.id == "LIMIT":
vars.limit = d.value elif len(d.value): if len(vars.where_clause):
vars.where_clause += " AND "
vars.where_clause += d.value if len(vars.where_clause): if self.partial:
vars.where_clause = " AND ( " + vars.where_clause + " ) " else:
vars.where_clause = " WHERE " + vars.where_clause + " "
self.accept()
def __init__(self, glb, parent=None):
title = "Selected Branches"
items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p), lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p), lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p), lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p), lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p), lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p), lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p), lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
# Event list
def GetEventList(db):
events = []
query = QSqlQuery(db)
QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id") while query.next():
events.append(query.value(0)) return events
def SQLTableDataPrep(self, query, count):
data = [] for i in xrange(count):
data.append(query.value(i)) return data
# SQL automatic table data model
class SQLAutoTableModel(SQLTableModel):
def __init__(self, glb, table_name, parent=None):
sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz) if table_name == "comm_threads_view": # For now, comm_threads_view has no id column
sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
column_headers = []
query = QSqlQuery(glb.db) if glb.dbref.is_sqlite3:
QueryExec(query, "PRAGMA table_info(" + table_name + ")") while query.next():
column_headers.append(query.value(1)) if table_name == "sqlite_master":
sql = "SELECT * FROM " + table_name else: if table_name[:19] == "information_schema.":
sql = "SELECT * FROM " + table_name
select_table_name = table_name[19:]
schema = "information_schema" else:
select_table_name = table_name
schema = "public"
QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'") while query.next():
column_headers.append(query.value(0)) if pyside_version_1 and sys.version_info[0] == 3: if table_name == "samples_view":
self.SQLTableDataPrep = self.samples_view_DataPrep if table_name == "samples":
self.SQLTableDataPrep = self.samples_DataPrep
super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
def samples_view_DataPrep(self, query, count):
data = []
data.append(query.value(0)) # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
data.append("{:>19}".format(query.value(1))) for i in xrange(2, count):
data.append(query.value(i)) return data
def samples_DataPrep(self, query, count):
data = [] for i in xrange(9):
data.append(query.value(i)) # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
data.append("{:>19}".format(query.value(9))) for i in xrange(10, count):
data.append(query.value(i)) return data
def ResizeColumnToContents(self, column, n): # Using the view's resizeColumnToContents() here is extrememly slow # so implement a crude alternative
font = self.view.font()
metrics = QFontMetrics(font)
max = 0 for row in xrange(n):
val = self.data_model.child_items[row].data[column]
len = metrics.width(str(val) + "MM")
max = len if len > max else max
val = self.data_model.columnHeader(column)
len = metrics.width(str(val) + "MM")
max = len if len > max else max
self.view.setColumnWidth(column, max)
def ResizeColumnsToContents(self):
n = min(self.data_model.child_count, 100) if n < 1: # No data yet, so connect a signal to notify when there is
self.data_model.rowsInserted.connect(self.UpdateColumnWidths) return
columns = self.data_model.columnCount() for i in xrange(columns):
self.ResizeColumnToContents(i, n)
def UpdateColumnWidths(self, *x): # This only needs to be done once, so disconnect the signal now
self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
self.ResizeColumnsToContents()
# Convert value to CSV
def ToCSValue(val): if'"'in val:
val = val.replace('"', '""') if","in val or'"'in val:
val = '"' + val + '"' return val
# Key to sort table model indexes by row / column, assuming fewer than 1000 columns
ifnot as_csv:
pos = first whileTrue:
row_cnt += 1
row = pos.row() for c in range(col_cnt):
i = pos.sibling(row, c) if c:
n = len(str(i.data())) else:
n = len(str(i.data()).strip())
n += (i.internalPointer().level - 1) * indent_sz
n += expanded_mark_sz
max_width[c] = max(max_width[c], n)
pos = view.indexBelow(pos) ifnot selection.isSelected(pos): break
text = ""
pad = ""
sep = "" if with_hdr: for c in range(col_cnt):
val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip() if as_csv:
text += sep + ToCSValue(val)
sep = "," else:
max_width[c] = max(max_width[c], len(val))
width = max_width[c]
align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole) if align & Qt.AlignRight:
val = val.rjust(width)
text += pad + sep + val
pad = " " * (width - len(val))
sep = " "
text += "\n"
pad = ""
sep = ""
pos = first whileTrue:
row = pos.row() for c in range(col_cnt):
i = pos.sibling(row, c)
val = str(i.data()) ifnot c: if model.hasChildren(i): if view.isExpanded(i):
mark = expanded_mark else:
mark = not_expanded_mark else:
mark = leaf_mark
val = indent_str * (i.internalPointer().level - 1) + mark + val.strip() if as_csv:
text += sep + ToCSValue(val)
sep = "," else:
width = max_width[c] if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
val = val.rjust(width)
text += pad + sep + val
pad = " " * (width - len(val))
sep = " "
pos = view.indexBelow(pos) ifnot selection.isSelected(pos): break
text = text.rstrip() + "\n"
pad = ""
sep = ""
def AddActions(self, menu):
i = self.view.currentIndex()
text = str(i.data()).strip() if len(text):
menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view))
self.AddCopy(menu)
# Table window
class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
def GetTableList(glb):
tables = []
query = QSqlQuery(glb.db) if glb.dbref.is_sqlite3:
QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name") else:
QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name") while query.next():
tables.append(query.value(0)) if glb.dbref.is_sqlite3:
tables.append("sqlite_master") else:
tables.append("information_schema.tables")
tables.append("information_schema.views")
tables.append("information_schema.columns") return tables
# Top Calls data model
class TopCallsModel(SQLTableModel):
def __init__(self, glb, report_vars, parent=None):
text = "" ifnot glb.dbref.is_sqlite3:
text = "::text"
limit = "" if len(report_vars.limit):
limit = " LIMIT " + report_vars.limit
sql = ("SELECT comm, pid, tid, name," " CASE" " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text + " ELSE short_name" " END AS dso," " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, " " CASE" " WHEN (calls.flags = 1) THEN 'no call'" + text + " WHEN (calls.flags = 2) THEN 'no return'" + text + " WHEN (calls.flags = 3) THEN 'no call/return'" + text + " ELSE ''" + text + " END AS flags" " FROM calls" " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" " INNER JOIN dsos ON symbols.dso_id = dsos.id" " INNER JOIN comms ON calls.comm_id = comms.id" " INNER JOIN threads ON calls.thread_id = threads.id" +
report_vars.where_clause + " ORDER BY elapsed_time DESC" +
limit
)
column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
def __init__(self, glb, parent=None):
title = "Top Calls by Elapsed Time"
items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p), lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p), lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p), lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p), lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p), lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p), lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
# Top Calls window
class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
def CreateCloseActiveWindowAction(mdi_area): return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
def CreateCloseAllWindowsAction(mdi_area): return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
def CreateTileWindowsAction(mdi_area): return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
def CreateCascadeWindowsAction(mdi_area): return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
def CreateNextWindowAction(mdi_area): return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
def CreatePreviousWindowAction(mdi_area): return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
glb_help_text = """
<h1>Contents</h1>
<style>
p.c1 {
text-indent: 40px;
}
p.c2 {
text-indent: 80px;
}
}
</style>
<p class=c1><a href=#reports>1. Reports</a></p>
<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
<p class=c2><a href=#calltree>1.2 Call Tree</a></p>
<p class=c2><a href=#allbranches>1.3 All branches</a></p>
<p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
<p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
<p class=c1><a href=#charts>2. Charts</a></p>
<p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p>
<p class=c1><a href=#tables>3. Tables</a></p>
<h1 id=reports>1. Reports</h1>
<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
The result is a GUI window with a tree representing a context-sensitive
call-graph. Expanding a couple of levels of the tree and adjusting column
widths to suit will display something like:
<pre>
Call Graph: pt_example
Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
v- ls
v- 2638:2638
v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
|- unknown unknown 1 13198 0.1 1 0.0
>- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
>- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
>- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
>- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
>- __libc_csu_init ls 1 10354 0.1 10 0.0
|- _setjmp libc-2.19.so 1 0 0.0 4 0.0
v- main ls 1 8182043 99.6 180254 99.9
</pre>
<h3>Points to note:</h3>
<ul>
<li>The top level is a command name (comm)</li>
<li>The next level is a thread (pid:tid)</li>
<li>Subsequent levels are functions</li>
<li>'Count'is the number of calls</li>
<li>'Time'is the elapsed time until the function returns</li>
<li>Percentages are relative to the level above</li>
<li>'Branch Count'is the total number of branches for that function and all functions that it calls
</ul>
<h3>Find</h3>
Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
The pattern matching symbols are ? for any character and * for zero or more characters.
<h2 id=calltree>1.2 Call Tree</h2>
The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data isnot aggregated.
Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
<h2 id=allbranches>1.3 All branches</h2>
The All branches report displays all branches in chronological order. Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
<h3>Disassembly</h3>
Open a branch to display disassembly. This only works if:
<ol>
<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
<li>The object code is available. Currently, only the perf build ID cache is searched for object code.
The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu), or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
</ol>
<h4 id=xed>Intel XED Setup</h4>
To use Intel XED, libxed.so must be present. To build and install libxed.so:
<pre>
git clone https://github.com/intelxed/mbuild.git mbuild
git clone https://github.com/intelxed/xed
cd xed
./mfile.py --share
sudo ./mfile.py --prefix=/usr/local install
sudo ldconfig
</pre>
<h3>Instructions per Cycle (IPC)</h3> If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt'and'IPC'.
<p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch.
Due to the granularity of timing information, the number of cycles for some code blocks will notbe known. In that case, 'insn_cnt', 'cyc_cnt'and'IPC' are zero, but when 'IPC'is displayed it covers the period
since the previous displayed 'IPC'.
<h3>Find</h3>
Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
Refer to Python documentation for the regular expression syntax.
All columns are searched, but only currently fetched rows are searched.
<h2 id=selectedbranches>1.4 Selected branches</h2>
This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
by various selection criteria. A dialog box displays available criteria which are AND'ed together.
<h3>1.4.1 Time ranges</h3>
The time ranges hint text shows the total time range. Relative time ranges can also be entered in
ms, us or ns. Also, negative values are relative to the end of trace. Examples:
<pre>
81073085947329-81073085958238 From 81073085947329 to 81073085958238
100us-200us From 100us to 200us
10ms- From 10ms to the end
-100ns The first 100ns
-10ms- The last 10ms
</pre>
N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
<h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned.
The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together. Ifnot all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
<h1 id=charts>2. Charts</h1>
<h2 id=timechartbycpu>2.1 Time chart by CPU</h2>
This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu.
<h3>Features</h3>
<ol>
<li>Mouse over to highight the task and show the time</li>
<li>Drag the mouse to select a region and zoom by pushing the Zoom button</li>
<li>Go back and forward by pressing the arrow buttons</li>
<li>If call information is available, right-click to show a call tree opened to that task and time.
Note, the call tree may take some time to appear, and there may not be call information for the task or time selected.
</li>
</ol>
<h3>Important</h3>
The graph can be misleading in the following respects:
<ol>
<li>The graph shows the first task on each CPU as running from the beginning of the time range.
Because tracing might start on different CPUs at different times, that isnot necessarily the case.
Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
<li>Similarly, the last task on each CPU can be showing running longer than it really was.
Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
<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>
</ol>
<h1 id=tables>3. Tables</h1>
The Tables menu shows all tables and views in the database. Most tables have an associated view
which displays the information in a more friendly way. Not all data for large tables is fetched
immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
but that can be slow for large tables.
<p>There are also tables of database meta-information. For SQLite3 databases, the sqlite_master table is included. For PostgreSQL databases, information_schema.tables/views/columns are included.
<h3>Find</h3>
Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
Refer to Python documentation for the regular expression syntax.
All columns are searched, but only currently fetched rows are searched.
<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
will go to the next/previous result in id order, instead of display order. """
def NumberedWindowName(name, nr): if nr > 1:
name += " <" + str(nr) + ">" return name
def UniqueSubWindowName(mdi_area, name):
nr = 1 whileTrue:
unique_name = NumberedWindowName(name, nr)
ok = True for sub_window in mdi_area.subWindowList(): if sub_window.name == unique_name:
ok = False break if ok: return unique_name
nr += 1
edit_menu = menu.addMenu("&Edit")
edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
reports_menu = menu.addMenu("&Reports") if IsSelectable(glb.db, "calls"):
reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
if IsSelectable(glb.db, "calls"):
reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
if IsSelectable(glb.db, "context_switches"):
charts_menu = menu.addMenu("&Charts")
charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self))
def TableMenu(self, tables, menu):
table_menu = menu.addMenu("&Tables") for table in tables:
table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self))
def FileFromNamesAndBuildId(self, short_name, long_name, build_id): # Assume current machine i.e. no support for virtualization if short_name[0:7] == "[kernel"and os.path.basename(long_name) == "kcore":
file_name = os.getenv("PERF_KCORE")
f = TryOpen(file_name) if file_name elseNone if f: return f # For now, no special handling if long_name is /proc/kcore
f = TryOpen(long_name) if f: return f
f = self.FileFromBuildId(build_id) if f: return f returnNone
# Shutdown any background processes or threads def ShutdownInstances(self): for x in self.instances_to_shutdown_on_exit: try:
x.Shutdown() except: pass
def GetHostMachineId(self):
query = QSqlQuery(self.db)
QueryExec(query, "SELECT id FROM machines WHERE pid = -1") if query.next():
self.host_machine_id = query.value(0) else:
self.host_machine_id = 0 return self.host_machine_id
def HostMachineId(self): if self.host_machine_id: return self.host_machine_id return self.GetHostMachineId()
def SwitchesMinTime(self, machine_id): return self.SelectValue("SELECT time" " FROM context_switches" " WHERE time != 0 AND machine_id = " + str(machine_id) + " ORDER BY id LIMIT 1")
def SwitchesMaxTime(self, machine_id): return self.SelectValue("SELECT time" " FROM context_switches" " WHERE time != 0 AND machine_id = " + str(machine_id) + " ORDER BY id DESC LIMIT 1")
def SamplesMinTime(self, machine_id): return self.SelectValue("SELECT time" " FROM samples" " WHERE time != 0 AND machine_id = " + str(machine_id) + " ORDER BY id LIMIT 1")
def SamplesMaxTime(self, machine_id): return self.SelectValue("SELECT time" " FROM samples" " WHERE time != 0 AND machine_id = " + str(machine_id) + " ORDER BY id DESC LIMIT 1")
def CallsMinTime(self, machine_id): return self.SelectValue("SELECT calls.call_time" " FROM calls" " INNER JOIN threads ON threads.thread_id = calls.thread_id" " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) + " ORDER BY calls.id LIMIT 1")
def CallsMaxTime(self, machine_id): return self.SelectValue("SELECT calls.return_time" " FROM calls" " INNER JOIN threads ON threads.thread_id = calls.thread_id" " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) + " ORDER BY calls.return_time DESC LIMIT 1")
def GetStartTime(self, machine_id):
t0 = self.SwitchesMinTime(machine_id)
t1 = self.SamplesMinTime(machine_id)
t2 = self.CallsMinTime(machine_id) if t0 isNoneor (not(t1 isNone) and t1 < t0):
t0 = t1 if t0 isNoneor (not(t2 isNone) and t2 < t0):
t0 = t2 return t0
def GetFinishTime(self, machine_id):
t0 = self.SwitchesMaxTime(machine_id)
t1 = self.SamplesMaxTime(machine_id)
t2 = self.CallsMaxTime(machine_id) if t0 isNoneor (not(t1 isNone) and t1 > t0):
t0 = t1 if t0 isNoneor (not(t2 isNone) and t2 > t0):
t0 = t2 return t0
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.