History

perf scripts python: exported-sql-viewer.py: Add Time chart by CPU

Add a time chart based on context switch information. Context switch information was added to the database export fairly recently, so the chart menu option will only appear if context switch information is in the database. Refer to the Exported SQL Viewer Help option for more information about the chart.

Adrian Hunter committed 7 years ago

Commit b3700f21 parent e69d5df1 file · +1331 −2Browse files

Showing the changes to tools/perf/scripts/python (1 of 1 file). Show all

Unified · Split

tools/perf/scripts/python/exported-sql-viewer.py+1331−2
@@ -105,6 +105,9 @@ except ImportError: · 104 unmodified lines
105105 glb_nsz = 16
106106 import re
107107 import os
108+import random
109+import copy
110+import math
108111
109112 pyside_version_1 = True
110113 if not "--pyside-version-1" in sys.argv:
@@ -1154,6 +1157,1301 @@ class CallTreeWindow(TreeWindowBase): · 1043 unmodified lines
11541157 self.view.setCurrentIndex(last_child)
11551158 parent = last_child
11561159
1160+# ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name
1161+
1162+def ExecComm(db, thread_id, time):
1163+ query = QSqlQuery(db)
1164+ QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag"
1165+ " FROM comm_threads"
1166+ " INNER JOIN comms ON comms.id = comm_threads.comm_id"
1167+ " WHERE comm_threads.thread_id = " + str(thread_id) +
1168+ " ORDER BY comms.c_time, comms.id")
1169+ first = None
1170+ last = None
1171+ while query.next():
1172+ if first is None:
1173+ first = query.value(0)
1174+ if query.value(2) and Decimal(query.value(1)) <= Decimal(time):
1175+ last = query.value(0)
1176+ if not(last is None):
1177+ return last
1178+ return first
1179+
1180+# Container for (x, y) data
1181+
1182+class XY():
1183+ def __init__(self, x=0, y=0):
1184+ self.x = x
1185+ self.y = y
1186+
1187+ def __str__(self):
1188+ return "XY({}, {})".format(str(self.x), str(self.y))
1189+
1190+# Container for sub-range data
1191+
1192+class Subrange():
1193+ def __init__(self, lo=0, hi=0):
1194+ self.lo = lo
1195+ self.hi = hi
1196+
1197+ def __str__(self):
1198+ return "Subrange({}, {})".format(str(self.lo), str(self.hi))
1199+
1200+# Graph data region base class
1201+
1202+class GraphDataRegion(object):
1203+
1204+ def __init__(self, key, title = "", ordinal = ""):
1205+ self.key = key
1206+ self.title = title
1207+ self.ordinal = ordinal
1208+
1209+# Function to sort GraphDataRegion
1210+
1211+def GraphDataRegionOrdinal(data_region):
1212+ return data_region.ordinal
1213+
1214+# Attributes for a graph region
1215+
1216+class GraphRegionAttribute():
1217+
1218+ def __init__(self, colour):
1219+ self.colour = colour
1220+
1221+# Switch graph data region represents a task
1222+
1223+class SwitchGraphDataRegion(GraphDataRegion):
1224+
1225+ def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id):
1226+ super(SwitchGraphDataRegion, self).__init__(key)
1227+
1228+ self.title = str(pid) + " / " + str(tid) + " " + comm
1229+ # Order graph legend within exec comm by pid / tid / time
1230+ self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16)
1231+ self.exec_comm_id = exec_comm_id
1232+ self.pid = pid
1233+ self.tid = tid
1234+ self.comm = comm
1235+ self.thread_id = thread_id
1236+ self.comm_id = comm_id
1237+
1238+# Graph data point
1239+
1240+class GraphDataPoint():
1241+
1242+ def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None):
1243+ self.data = data
1244+ self.index = index
1245+ self.x = x
1246+ self.y = y
1247+ self.altx = altx
1248+ self.alty = alty
1249+ self.hregion = hregion
1250+ self.vregion = vregion
1251+
1252+# Graph data (single graph) base class
1253+
1254+class GraphData(object):
1255+
1256+ def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)):
1257+ self.collection = collection
1258+ self.points = []
1259+ self.xbase = xbase
1260+ self.ybase = ybase
1261+ self.title = ""
1262+
1263+ def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None):
1264+ index = len(self.points)
1265+
1266+ x = float(Decimal(x) - self.xbase)
1267+ y = float(Decimal(y) - self.ybase)
1268+
1269+ self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion))
1270+
1271+ def XToData(self, x):
1272+ return Decimal(x) + self.xbase
1273+
1274+ def YToData(self, y):
1275+ return Decimal(y) + self.ybase
1276+
1277+# Switch graph data (for one CPU)
1278+
1279+class SwitchGraphData(GraphData):
1280+
1281+ def __init__(self, db, collection, cpu, xbase):
1282+ super(SwitchGraphData, self).__init__(collection, xbase)
1283+
1284+ self.cpu = cpu
1285+ self.title = "CPU " + str(cpu)
1286+ self.SelectSwitches(db)
1287+
1288+ def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time):
1289+ query = QSqlQuery(db)
1290+ QueryExec(query, "SELECT id, c_time"
1291+ " FROM comms"
1292+ " WHERE c_thread_id = " + str(thread_id) +
1293+ " AND exec_flag = TRUE"
1294+ " AND c_time >= " + str(start_time) +
1295+ " AND c_time <= " + str(end_time) +
1296+ " ORDER BY c_time, id")
1297+ while query.next():
1298+ comm_id = query.value(0)
1299+ if comm_id == last_comm_id:
1300+ continue
1301+ time = query.value(1)
1302+ hregion = self.HRegion(db, thread_id, comm_id, time)
1303+ self.AddPoint(time, 1000, None, None, hregion)
1304+
1305+ def SelectSwitches(self, db):
1306+ last_time = None
1307+ last_comm_id = None
1308+ last_thread_id = None
1309+ query = QSqlQuery(db)
1310+ QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags"
1311+ " FROM context_switches"
1312+ " WHERE machine_id = " + str(self.collection.machine_id) +
1313+ " AND cpu = " + str(self.cpu) +
1314+ " ORDER BY time, id")
1315+ while query.next():
1316+ flags = int(query.value(5))
1317+ if flags & 1:
1318+ # Schedule-out: detect and add exec's
1319+ if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3):
1320+ self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0))
1321+ continue
1322+ # Schedule-in: add data point
1323+ if len(self.points) == 0:
1324+ start_time = self.collection.glb.StartTime(self.collection.machine_id)
1325+ hregion = self.HRegion(db, query.value(1), query.value(3), start_time)
1326+ self.AddPoint(start_time, 1000, None, None, hregion)
1327+ time = query.value(0)
1328+ comm_id = query.value(4)
1329+ thread_id = query.value(2)
1330+ hregion = self.HRegion(db, thread_id, comm_id, time)
1331+ self.AddPoint(time, 1000, None, None, hregion)
1332+ last_time = time
1333+ last_comm_id = comm_id
1334+ last_thread_id = thread_id
1335+
1336+ def NewHRegion(self, db, key, thread_id, comm_id, time):
1337+ exec_comm_id = ExecComm(db, thread_id, time)
1338+ query = QSqlQuery(db)
1339+ QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id))
1340+ if query.next():
1341+ pid = query.value(0)
1342+ tid = query.value(1)
1343+ else:
1344+ pid = -1
1345+ tid = -1
1346+ query = QSqlQuery(db)
1347+ QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id))
1348+ if query.next():
1349+ comm = query.value(0)
1350+ else:
1351+ comm = ""
1352+ return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id)
1353+
1354+ def HRegion(self, db, thread_id, comm_id, time):
1355+ key = str(thread_id) + ":" + str(comm_id)
1356+ hregion = self.collection.LookupHRegion(key)
1357+ if hregion is None:
1358+ hregion = self.NewHRegion(db, key, thread_id, comm_id, time)
1359+ self.collection.AddHRegion(key, hregion)
1360+ return hregion
1361+
1362+# Graph data collection (multiple related graphs) base class
1363+
1364+class GraphDataCollection(object):
1365+
1366+ def __init__(self, glb):
1367+ self.glb = glb
1368+ self.data = []
1369+ self.hregions = {}
1370+ self.xrangelo = None
1371+ self.xrangehi = None
1372+ self.yrangelo = None
1373+ self.yrangehi = None
1374+ self.dp = XY(0, 0)
1375+
1376+ def AddGraphData(self, data):
1377+ self.data.append(data)
1378+
1379+ def LookupHRegion(self, key):
1380+ if key in self.hregions:
1381+ return self.hregions[key]
1382+ return None
1383+
1384+ def AddHRegion(self, key, hregion):
1385+ self.hregions[key] = hregion
1386+
1387+# Switch graph data collection (SwitchGraphData for each CPU)
1388+
1389+class SwitchGraphDataCollection(GraphDataCollection):
1390+
1391+ def __init__(self, glb, db, machine_id):
1392+ super(SwitchGraphDataCollection, self).__init__(glb)
1393+
1394+ self.machine_id = machine_id
1395+ self.cpus = self.SelectCPUs(db)
1396+
1397+ self.xrangelo = glb.StartTime(machine_id)
1398+ self.xrangehi = glb.FinishTime(machine_id)
1399+
1400+ self.yrangelo = Decimal(0)
1401+ self.yrangehi = Decimal(1000)
1402+
1403+ for cpu in self.cpus:
1404+ self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo))
1405+
1406+ def SelectCPUs(self, db):
1407+ cpus = []
1408+ query = QSqlQuery(db)
1409+ QueryExec(query, "SELECT DISTINCT cpu"
1410+ " FROM context_switches"
1411+ " WHERE machine_id = " + str(self.machine_id))
1412+ while query.next():
1413+ cpus.append(int(query.value(0)))
1414+ return sorted(cpus)
1415+
1416+# Switch graph data graphics item displays the graphed data
1417+
1418+class SwitchGraphDataGraphicsItem(QGraphicsItem):
1419+
1420+ def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None):
1421+ super(SwitchGraphDataGraphicsItem, self).__init__(parent)
1422+
1423+ self.data = data
1424+ self.graph_width = graph_width
1425+ self.graph_height = graph_height
1426+ self.attrs = attrs
1427+ self.event_handler = event_handler
1428+ self.setAcceptHoverEvents(True)
1429+
1430+ def boundingRect(self):
1431+ return QRectF(0, 0, self.graph_width, self.graph_height)
1432+
1433+ def PaintPoint(self, painter, last, x):
1434+ if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo):
1435+ if last.x < self.attrs.subrange.x.lo:
1436+ x0 = self.attrs.subrange.x.lo
1437+ else:
1438+ x0 = last.x
1439+ if x > self.attrs.subrange.x.hi:
1440+ x1 = self.attrs.subrange.x.hi
1441+ else:
1442+ x1 = x - 1
1443+ x0 = self.attrs.XToPixel(x0)
1444+ x1 = self.attrs.XToPixel(x1)
1445+
1446+ y0 = self.attrs.YToPixel(last.y)
1447+
1448+ colour = self.attrs.region_attributes[last.hregion.key].colour
1449+
1450+ width = x1 - x0 + 1
1451+ if width < 2:
1452+ painter.setPen(colour)
1453+ painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height)
1454+ else:
1455+ painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour)
1456+
1457+ def paint(self, painter, option, widget):
1458+ last = None
1459+ for point in self.data.points:
1460+ self.PaintPoint(painter, last, point.x)
1461+ if point.x > self.attrs.subrange.x.hi:
1462+ break;
1463+ last = point
1464+ self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1)
1465+
1466+ def BinarySearchPoint(self, target):
1467+ lower_pos = 0
1468+ higher_pos = len(self.data.points)
1469+ while True:
1470+ pos = int((lower_pos + higher_pos) / 2)
1471+ val = self.data.points[pos].x
1472+ if target >= val:
1473+ lower_pos = pos
1474+ else:
1475+ higher_pos = pos
1476+ if higher_pos <= lower_pos + 1:
1477+ return lower_pos
1478+
1479+ def XPixelToData(self, x):
1480+ x = self.attrs.PixelToX(x)
1481+ if x < self.data.points[0].x:
1482+ x = 0
1483+ pos = 0
1484+ low = True
1485+ else:
1486+ pos = self.BinarySearchPoint(x)
1487+ low = False
1488+ return (low, pos, self.data.XToData(x))
1489+
1490+ def EventToData(self, event):
1491+ no_data = (None,) * 4
1492+ if len(self.data.points) < 1:
1493+ return no_data
1494+ x = event.pos().x()
1495+ if x < 0:
1496+ return no_data
1497+ low0, pos0, time_from = self.XPixelToData(x)
1498+ low1, pos1, time_to = self.XPixelToData(x + 1)
1499+ hregions = set()
1500+ hregion_times = []
1501+ if not low1:
1502+ for i in xrange(pos0, pos1 + 1):
1503+ hregion = self.data.points[i].hregion
1504+ hregions.add(hregion)
1505+ if i == pos0:
1506+ time = time_from
1507+ else:
1508+ time = self.data.XToData(self.data.points[i].x)
1509+ hregion_times.append((hregion, time))
1510+ return (time_from, time_to, hregions, hregion_times)
1511+
1512+ def hoverMoveEvent(self, event):
1513+ time_from, time_to, hregions, hregion_times = self.EventToData(event)
1514+ if time_from is not None:
1515+ self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions)
1516+
1517+ def hoverLeaveEvent(self, event):
1518+ self.event_handler.NoPointEvent()
1519+
1520+ def mousePressEvent(self, event):
1521+ if event.button() != Qt.RightButton:
1522+ super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event)
1523+ return
1524+ time_from, time_to, hregions, hregion_times = self.EventToData(event)
1525+ if hregion_times:
1526+ self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos())
1527+
1528+# X-axis graphics item
1529+
1530+class XAxisGraphicsItem(QGraphicsItem):
1531+
1532+ def __init__(self, width, parent=None):
1533+ super(XAxisGraphicsItem, self).__init__(parent)
1534+
1535+ self.width = width
1536+ self.max_mark_sz = 4
1537+ self.height = self.max_mark_sz + 1
1538+
1539+ def boundingRect(self):
1540+ return QRectF(0, 0, self.width, self.height)
1541+
1542+ def Step(self):
1543+ attrs = self.parentItem().attrs
1544+ subrange = attrs.subrange.x
1545+ t = subrange.hi - subrange.lo
1546+ s = (3.0 * t) / self.width
1547+ n = 1.0
1548+ while s > n:
1549+ n = n * 10.0
1550+ return n
1551+
1552+ def PaintMarks(self, painter, at_y, lo, hi, step, i):
1553+ attrs = self.parentItem().attrs
1554+ x = lo
1555+ while x <= hi:
1556+ xp = attrs.XToPixel(x)
1557+ if i % 10:
1558+ if i % 5:
1559+ sz = 1
1560+ else:
1561+ sz = 2
1562+ else:
1563+ sz = self.max_mark_sz
1564+ i = 0
1565+ painter.drawLine(xp, at_y, xp, at_y + sz)
1566+ x += step
1567+ i += 1
1568+
1569+ def paint(self, painter, option, widget):
1570+ # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1
1571+ painter.drawLine(0, 0, self.width - 1, 0)
1572+ n = self.Step()
1573+ attrs = self.parentItem().attrs
1574+ subrange = attrs.subrange.x
1575+ if subrange.lo:
1576+ x_offset = n - (subrange.lo % n)
1577+ else:
1578+ x_offset = 0.0
1579+ x = subrange.lo + x_offset
1580+ i = (x / n) % 10
1581+ self.PaintMarks(painter, 0, x, subrange.hi, n, i)
1582+
1583+ def ScaleDimensions(self):
1584+ n = self.Step()
1585+ attrs = self.parentItem().attrs
1586+ lo = attrs.subrange.x.lo
1587+ hi = (n * 10.0) + lo
1588+ width = attrs.XToPixel(hi)
1589+ if width > 500:
1590+ width = 0
1591+ return (n, lo, hi, width)
1592+
1593+ def PaintScale(self, painter, at_x, at_y):
1594+ n, lo, hi, width = self.ScaleDimensions()
1595+ if not width:
1596+ return
1597+ painter.drawLine(at_x, at_y, at_x + width, at_y)
1598+ self.PaintMarks(painter, at_y, lo, hi, n, 0)
1599+
1600+ def ScaleWidth(self):
1601+ n, lo, hi, width = self.ScaleDimensions()
1602+ return width
1603+
1604+ def ScaleHeight(self):
1605+ return self.height
1606+
1607+ def ScaleUnit(self):
1608+ return self.Step() * 10
1609+
1610+# Scale graphics item base class
1611+
1612+class ScaleGraphicsItem(QGraphicsItem):
1613+
1614+ def __init__(self, axis, parent=None):
1615+ super(ScaleGraphicsItem, self).__init__(parent)
1616+ self.axis = axis
1617+
1618+ def boundingRect(self):
1619+ scale_width = self.axis.ScaleWidth()
1620+ if not scale_width:
1621+ return QRectF()
1622+ return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight())
1623+
1624+ def paint(self, painter, option, widget):
1625+ scale_width = self.axis.ScaleWidth()
1626+ if not scale_width:
1627+ return
1628+ self.axis.PaintScale(painter, 0, 5)
1629+ x = scale_width + 4
1630+ painter.drawText(QPointF(x, 10), self.Text())
1631+
1632+ def Unit(self):
1633+ return self.axis.ScaleUnit()
1634+
1635+ def Text(self):
1636+ return ""
1637+
1638+# Switch graph scale graphics item
1639+
1640+class SwitchScaleGraphicsItem(ScaleGraphicsItem):
1641+
1642+ def __init__(self, axis, parent=None):
1643+ super(SwitchScaleGraphicsItem, self).__init__(axis, parent)
1644+
1645+ def Text(self):
1646+ unit = self.Unit()
1647+ if unit >= 1000000000:
1648+ unit = int(unit / 1000000000)
1649+ us = "s"
1650+ elif unit >= 1000000:
1651+ unit = int(unit / 1000000)
1652+ us = "ms"
1653+ elif unit >= 1000:
1654+ unit = int(unit / 1000)
1655+ us = "us"
1656+ else:
1657+ unit = int(unit)
1658+ us = "ns"
1659+ return " = " + str(unit) + " " + us
1660+
1661+# Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data
1662+
1663+class SwitchGraphGraphicsItem(QGraphicsItem):
1664+
1665+ def __init__(self, collection, data, attrs, event_handler, first, parent=None):
1666+ super(SwitchGraphGraphicsItem, self).__init__(parent)
1667+ self.collection = collection
1668+ self.data = data
1669+ self.attrs = attrs
1670+ self.event_handler = event_handler
1671+
1672+ margin = 20
1673+ title_width = 50
1674+
1675+ self.title_graphics = QGraphicsSimpleTextItem(data.title, self)
1676+
1677+ self.title_graphics.setPos(margin, margin)
1678+ graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1
1679+ graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1
1680+
1681+ self.graph_origin_x = margin + title_width + margin
1682+ self.graph_origin_y = graph_height + margin
1683+
1684+ x_axis_size = 1
1685+ y_axis_size = 1
1686+ self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self)
1687+
1688+ self.x_axis = XAxisGraphicsItem(graph_width, self)
1689+ self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1)
1690+
1691+ if first:
1692+ self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self)
1693+ self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10)
1694+
1695+ self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height)
1696+
1697+ self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self)
1698+ self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1)
1699+
1700+ self.width = self.graph_origin_x + graph_width + margin
1701+ self.height = self.graph_origin_y + margin
1702+
1703+ self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self)
1704+ self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height)
1705+
1706+ if parent and 'EnableRubberBand' in dir(parent):
1707+ parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self)
1708+
1709+ def boundingRect(self):
1710+ return QRectF(0, 0, self.width, self.height)
1711+
1712+ def paint(self, painter, option, widget):
1713+ pass
1714+
1715+ def RBXToPixel(self, x):
1716+ return self.attrs.PixelToX(x - self.graph_origin_x)
1717+
1718+ def RBXRangeToPixel(self, x0, x1):
1719+ return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1))
1720+
1721+ def RBPixelToTime(self, x):
1722+ if x < self.data.points[0].x:
1723+ return self.data.XToData(0)
1724+ return self.data.XToData(x)
1725+
1726+ def RBEventTimes(self, x0, x1):
1727+ x0, x1 = self.RBXRangeToPixel(x0, x1)
1728+ time_from = self.RBPixelToTime(x0)
1729+ time_to = self.RBPixelToTime(x1)
1730+ return (time_from, time_to)
1731+
1732+ def RBEvent(self, x0, x1):
1733+ time_from, time_to = self.RBEventTimes(x0, x1)
1734+ self.event_handler.RangeEvent(time_from, time_to)
1735+
1736+ def RBMoveEvent(self, x0, x1):
1737+ if x1 < x0:
1738+ x0, x1 = x1, x0
1739+ self.RBEvent(x0, x1)
1740+
1741+ def RBReleaseEvent(self, x0, x1, selection_state):
1742+ if x1 < x0:
1743+ x0, x1 = x1, x0
1744+ x0, x1 = self.RBXRangeToPixel(x0, x1)
1745+ self.event_handler.SelectEvent(x0, x1, selection_state)
1746+
1747+# Graphics item to draw a vertical bracket (used to highlight "forward" sub-range)
1748+
1749+class VerticalBracketGraphicsItem(QGraphicsItem):
1750+
1751+ def __init__(self, parent=None):
1752+ super(VerticalBracketGraphicsItem, self).__init__(parent)
1753+
1754+ self.width = 0
1755+ self.height = 0
1756+ self.hide()
1757+
1758+ def SetSize(self, width, height):
1759+ self.width = width + 1
1760+ self.height = height + 1
1761+
1762+ def boundingRect(self):
1763+ return QRectF(0, 0, self.width, self.height)
1764+
1765+ def paint(self, painter, option, widget):
1766+ colour = QColor(255, 255, 0, 32)
1767+ painter.fillRect(0, 0, self.width, self.height, colour)
1768+ x1 = self.width - 1
1769+ y1 = self.height - 1
1770+ painter.drawLine(0, 0, x1, 0)
1771+ painter.drawLine(0, 0, 0, 3)
1772+ painter.drawLine(x1, 0, x1, 3)
1773+ painter.drawLine(0, y1, x1, y1)
1774+ painter.drawLine(0, y1, 0, y1 - 3)
1775+ painter.drawLine(x1, y1, x1, y1 - 3)
1776+
1777+# Graphics item to contain graphs arranged vertically
1778+
1779+class VertcalGraphSetGraphicsItem(QGraphicsItem):
1780+
1781+ def __init__(self, collection, attrs, event_handler, child_class, parent=None):
1782+ super(VertcalGraphSetGraphicsItem, self).__init__(parent)
1783+
1784+ self.collection = collection
1785+
1786+ self.top = 10
1787+
1788+ self.width = 0
1789+ self.height = self.top
1790+
1791+ self.rubber_band = None
1792+ self.rb_enabled = False
1793+
1794+ first = True
1795+ for data in collection.data:
1796+ child = child_class(collection, data, attrs, event_handler, first, self)
1797+ child.setPos(0, self.height + 1)
1798+ rect = child.boundingRect()
1799+ if rect.right() > self.width:
1800+ self.width = rect.right()
1801+ self.height = self.height + rect.bottom() + 1
1802+ first = False
1803+
1804+ self.bracket = VerticalBracketGraphicsItem(self)
1805+
1806+ def EnableRubberBand(self, xlo, xhi, rb_event_handler):
1807+ if self.rb_enabled:
1808+ return
1809+ self.rb_enabled = True
1810+ self.rb_in_view = False
1811+ self.setAcceptedMouseButtons(Qt.LeftButton)
1812+ self.rb_xlo = xlo
1813+ self.rb_xhi = xhi
1814+ self.rb_event_handler = rb_event_handler
1815+ self.mousePressEvent = self.MousePressEvent
1816+ self.mouseMoveEvent = self.MouseMoveEvent
1817+ self.mouseReleaseEvent = self.MouseReleaseEvent
1818+
1819+ def boundingRect(self):
1820+ return QRectF(0, 0, self.width, self.height)
1821+
1822+ def paint(self, painter, option, widget):
1823+ pass
1824+
1825+ def RubberBandParent(self):
1826+ scene = self.scene()
1827+ view = scene.views()[0]
1828+ viewport = view.viewport()
1829+ return viewport
1830+
1831+ def RubberBandSetGeometry(self, rect):
1832+ scene_rectf = self.mapRectToScene(QRectF(rect))
1833+ scene = self.scene()
1834+ view = scene.views()[0]
1835+ poly = view.mapFromScene(scene_rectf)
1836+ self.rubber_band.setGeometry(poly.boundingRect())
1837+
1838+ def SetSelection(self, selection_state):
1839+ if self.rubber_band:
1840+ if selection_state:
1841+ self.RubberBandSetGeometry(selection_state)
1842+ self.rubber_band.show()
1843+ else:
1844+ self.rubber_band.hide()
1845+
1846+ def SetBracket(self, rect):
1847+ if rect:
1848+ x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()
1849+ self.bracket.setPos(x, y)
1850+ self.bracket.SetSize(width, height)
1851+ self.bracket.show()
1852+ else:
1853+ self.bracket.hide()
1854+
1855+ def RubberBandX(self, event):
1856+ x = event.pos().toPoint().x()
1857+ if x < self.rb_xlo:
1858+ x = self.rb_xlo
1859+ elif x > self.rb_xhi:
1860+ x = self.rb_xhi
1861+ else:
1862+ self.rb_in_view = True
1863+ return x
1864+
1865+ def RubberBandRect(self, x):
1866+ if self.rb_origin.x() <= x:
1867+ width = x - self.rb_origin.x()
1868+ rect = QRect(self.rb_origin, QSize(width, self.height))
1869+ else:
1870+ width = self.rb_origin.x() - x
1871+ top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y())
1872+ rect = QRect(top_left, QSize(width, self.height))
1873+ return rect
1874+
1875+ def MousePressEvent(self, event):
1876+ self.rb_in_view = False
1877+ x = self.RubberBandX(event)
1878+ self.rb_origin = QPoint(x, self.top)
1879+ if self.rubber_band is None:
1880+ self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent())
1881+ self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height)))
1882+ if self.rb_in_view:
1883+ self.rubber_band.show()
1884+ self.rb_event_handler.RBMoveEvent(x, x)
1885+ else:
1886+ self.rubber_band.hide()
1887+
1888+ def MouseMoveEvent(self, event):
1889+ x = self.RubberBandX(event)
1890+ rect = self.RubberBandRect(x)
1891+ self.RubberBandSetGeometry(rect)
1892+ if self.rb_in_view:
1893+ self.rubber_band.show()
1894+ self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x)
1895+
1896+ def MouseReleaseEvent(self, event):
1897+ x = self.RubberBandX(event)
1898+ if self.rb_in_view:
1899+ selection_state = self.RubberBandRect(x)
1900+ else:
1901+ selection_state = None
1902+ self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state)
1903+
1904+# Switch graph legend data model
1905+
1906+class SwitchGraphLegendModel(QAbstractTableModel):
1907+
1908+ def __init__(self, collection, region_attributes, parent=None):
1909+ super(SwitchGraphLegendModel, self).__init__(parent)
1910+
1911+ self.region_attributes = region_attributes
1912+
1913+ self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal)
1914+ self.child_count = len(self.child_items)
1915+
1916+ self.highlight_set = set()
1917+
1918+ self.column_headers = ("pid", "tid", "comm")
1919+
1920+ def rowCount(self, parent):
1921+ return self.child_count
1922+
1923+ def headerData(self, section, orientation, role):
1924+ if role != Qt.DisplayRole:
1925+ return None
1926+ if orientation != Qt.Horizontal:
1927+ return None
1928+ return self.columnHeader(section)
1929+
1930+ def index(self, row, column, parent):
1931+ return self.createIndex(row, column, self.child_items[row])
1932+
1933+ def columnCount(self, parent=None):
1934+ return len(self.column_headers)
1935+
1936+ def columnHeader(self, column):
1937+ return self.column_headers[column]
1938+
1939+ def data(self, index, role):
1940+ if role == Qt.BackgroundRole:
1941+ child = self.child_items[index.row()]
1942+ if child in self.highlight_set:
1943+ return self.region_attributes[child.key].colour
1944+ return None
1945+ if role == Qt.ForegroundRole:
1946+ child = self.child_items[index.row()]
1947+ if child in self.highlight_set:
1948+ return QColor(255, 255, 255)
1949+ return self.region_attributes[child.key].colour
1950+ if role != Qt.DisplayRole:
1951+ return None
1952+ hregion = self.child_items[index.row()]
1953+ col = index.column()
1954+ if col == 0:
1955+ return hregion.pid
1956+ if col == 1:
1957+ return hregion.tid
1958+ if col == 2:
1959+ return hregion.comm
1960+ return None
1961+
1962+ def SetHighlight(self, row, set_highlight):
1963+ child = self.child_items[row]
1964+ top_left = self.createIndex(row, 0, child)
1965+ bottom_right = self.createIndex(row, len(self.column_headers) - 1, child)
1966+ self.dataChanged.emit(top_left, bottom_right)
1967+
1968+ def Highlight(self, highlight_set):
1969+ for row in xrange(self.child_count):
1970+ child = self.child_items[row]
1971+ if child in self.highlight_set:
1972+ if child not in highlight_set:
1973+ self.SetHighlight(row, False)
1974+ elif child in highlight_set:
1975+ self.SetHighlight(row, True)
1976+ self.highlight_set = highlight_set
1977+
1978+# Switch graph legend is a table
1979+
1980+class SwitchGraphLegend(QWidget):
1981+
1982+ def __init__(self, collection, region_attributes, parent=None):
1983+ super(SwitchGraphLegend, self).__init__(parent)
1984+
1985+ self.data_model = SwitchGraphLegendModel(collection, region_attributes)
1986+
1987+ self.model = QSortFilterProxyModel()
1988+ self.model.setSourceModel(self.data_model)
1989+
1990+ self.view = QTableView()
1991+ self.view.setModel(self.model)
1992+ self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
1993+ self.view.verticalHeader().setVisible(False)
1994+ self.view.sortByColumn(-1, Qt.AscendingOrder)
1995+ self.view.setSortingEnabled(True)
1996+ self.view.resizeColumnsToContents()
1997+ self.view.resizeRowsToContents()
1998+
1999+ self.vbox = VBoxLayout(self.view)
2000+ self.setLayout(self.vbox)
2001+
2002+ sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2
2003+ sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width()
2004+ self.saved_size = sz1
2005+
2006+ def resizeEvent(self, event):
2007+ self.saved_size = self.size().width()
2008+ super(SwitchGraphLegend, self).resizeEvent(event)
2009+
2010+ def Highlight(self, highlight_set):
2011+ self.data_model.Highlight(highlight_set)
2012+ self.update()
2013+
2014+ def changeEvent(self, event):
2015+ if event.type() == QEvent.FontChange:
2016+ self.view.resizeRowsToContents()
2017+ self.view.resizeColumnsToContents()
2018+ # Need to resize rows again after column resize
2019+ self.view.resizeRowsToContents()
2020+ super(SwitchGraphLegend, self).changeEvent(event)
2021+
2022+# Random colour generation
2023+
2024+def RGBColourTooLight(r, g, b):
2025+ if g > 230:
2026+ return True
2027+ if g <= 160:
2028+ return False
2029+ if r <= 180 and g <= 180:
2030+ return False
2031+ if r < 60:
2032+ return False
2033+ return True
2034+
2035+def GenerateColours(x):
2036+ cs = [0]
2037+ for i in xrange(1, x):
2038+ cs.append(int((255.0 / i) + 0.5))
2039+ colours = []
2040+ for r in cs:
2041+ for g in cs:
2042+ for b in cs:
2043+ # Exclude black and colours that look too light against a white background
2044+ if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b):
2045+ continue
2046+ colours.append(QColor(r, g, b))
2047+ return colours
2048+
2049+def GenerateNColours(n):
2050+ for x in xrange(2, n + 2):
2051+ colours = GenerateColours(x)
2052+ if len(colours) >= n:
2053+ return colours
2054+ return []
2055+
2056+def GenerateNRandomColours(n, seed):
2057+ colours = GenerateNColours(n)
2058+ random.seed(seed)
2059+ random.shuffle(colours)
2060+ return colours
2061+
2062+# Graph attributes, in particular the scale and subrange that change when zooming
2063+
2064+class GraphAttributes():
2065+
2066+ def __init__(self, scale, subrange, region_attributes, dp):
2067+ self.scale = scale
2068+ self.subrange = subrange
2069+ self.region_attributes = region_attributes
2070+ # Rounding avoids errors due to finite floating point precision
2071+ self.dp = dp # data decimal places
2072+ self.Update()
2073+
2074+ def XToPixel(self, x):
2075+ return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x))
2076+
2077+ def YToPixel(self, y):
2078+ return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y))
2079+
2080+ def PixelToXRounded(self, px):
2081+ return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo
2082+
2083+ def PixelToYRounded(self, py):
2084+ return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo
2085+
2086+ def PixelToX(self, px):
2087+ x = self.PixelToXRounded(px)
2088+ if self.pdp.x == 0:
2089+ rt = self.XToPixel(x)
2090+ if rt > px:
2091+ return x - 1
2092+ return x
2093+
2094+ def PixelToY(self, py):
2095+ y = self.PixelToYRounded(py)
2096+ if self.pdp.y == 0:
2097+ rt = self.YToPixel(y)
2098+ if rt > py:
2099+ return y - 1
2100+ return y
2101+
2102+ def ToPDP(self, dp, scale):
2103+ # Calculate pixel decimal places:
2104+ # (10 ** dp) is the minimum delta in the data
2105+ # scale it to get the minimum delta in pixels
2106+ # log10 gives the number of decimals places negatively
2107+ # subtrace 1 to divide by 10
2108+ # round to the lower negative number
2109+ # change the sign to get the number of decimals positively
2110+ x = math.log10((10 ** dp) * scale)
2111+ if x < 0:
2112+ x -= 1
2113+ x = -int(math.floor(x) - 0.1)
2114+ else:
2115+ x = 0
2116+ return x
2117+
2118+ def Update(self):
2119+ x = self.ToPDP(self.dp.x, self.scale.x)
2120+ y = self.ToPDP(self.dp.y, self.scale.y)
2121+ self.pdp = XY(x, y) # pixel decimal places
2122+
2123+# Switch graph splitter which divides the CPU graphs from the legend
2124+
2125+class SwitchGraphSplitter(QSplitter):
2126+
2127+ def __init__(self, parent=None):
2128+ super(SwitchGraphSplitter, self).__init__(parent)
2129+
2130+ self.first_time = False
2131+
2132+ def resizeEvent(self, ev):
2133+ if self.first_time:
2134+ self.first_time = False
2135+ sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2
2136+ sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width()
2137+ sz0 = self.size().width() - self.handleWidth() - sz1
2138+ self.setSizes([sz0, sz1])
2139+ elif not(self.widget(1).saved_size is None):
2140+ sz1 = self.widget(1).saved_size
2141+ sz0 = self.size().width() - self.handleWidth() - sz1
2142+ self.setSizes([sz0, sz1])
2143+ super(SwitchGraphSplitter, self).resizeEvent(ev)
2144+
2145+# Graph widget base class
2146+
2147+class GraphWidget(QWidget):
2148+
2149+ graph_title_changed = Signal(object)
2150+
2151+ def __init__(self, parent=None):
2152+ super(GraphWidget, self).__init__(parent)
2153+
2154+ def GraphTitleChanged(self, title):
2155+ self.graph_title_changed.emit(title)
2156+
2157+ def Title(self):
2158+ return ""
2159+
2160+# Display time in s, ms, us or ns
2161+
2162+def ToTimeStr(val):
2163+ val = Decimal(val)
2164+ if val >= 1000000000:
2165+ return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001")))
2166+ if val >= 1000000:
2167+ return "{} ms".format((val / 1000000).quantize(Decimal("0.000001")))
2168+ if val >= 1000:
2169+ return "{} us".format((val / 1000).quantize(Decimal("0.001")))
2170+ return "{} ns".format(val.quantize(Decimal("1")))
2171+
2172+# Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons
2173+
2174+class SwitchGraphWidget(GraphWidget):
2175+
2176+ def __init__(self, glb, collection, parent=None):
2177+ super(SwitchGraphWidget, self).__init__(parent)
2178+
2179+ self.glb = glb
2180+ self.collection = collection
2181+
2182+ self.back_state = []
2183+ self.forward_state = []
2184+ self.selection_state = (None, None)
2185+ self.fwd_rect = None
2186+ self.start_time = self.glb.StartTime(collection.machine_id)
2187+
2188+ i = 0
2189+ hregions = collection.hregions.values()
2190+ colours = GenerateNRandomColours(len(hregions), 1013)
2191+ region_attributes = {}
2192+ for hregion in hregions:
2193+ if hregion.pid == 0 and hregion.tid == 0:
2194+ region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0))
2195+ else:
2196+ region_attributes[hregion.key] = GraphRegionAttribute(colours[i])
2197+ i = i + 1
2198+
2199+ # Default to entire range
2200+ xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0)
2201+ ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0)
2202+ subrange = XY(xsubrange, ysubrange)
2203+
2204+ scale = self.GetScaleForRange(subrange)
2205+
2206+ self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp)
2207+
2208+ self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem)
2209+
2210+ self.scene = QGraphicsScene()
2211+ self.scene.addItem(self.item)
2212+
2213+ self.view = QGraphicsView(self.scene)
2214+ self.view.centerOn(0, 0)
2215+ self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop)
2216+
2217+ self.legend = SwitchGraphLegend(collection, region_attributes)
2218+
2219+ self.splitter = SwitchGraphSplitter()
2220+ self.splitter.addWidget(self.view)
2221+ self.splitter.addWidget(self.legend)
2222+
2223+ self.point_label = QLabel("")
2224+ self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
2225+
2226+ self.back_button = QToolButton()
2227+ self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft))
2228+ self.back_button.setDisabled(True)
2229+ self.back_button.released.connect(lambda: self.Back())
2230+
2231+ self.forward_button = QToolButton()
2232+ self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight))
2233+ self.forward_button.setDisabled(True)
2234+ self.forward_button.released.connect(lambda: self.Forward())
2235+
2236+ self.zoom_button = QToolButton()
2237+ self.zoom_button.setText("Zoom")
2238+ self.zoom_button.setDisabled(True)
2239+ self.zoom_button.released.connect(lambda: self.Zoom())
2240+
2241+ self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label)
2242+
2243+ self.vbox = VBoxLayout(self.splitter, self.hbox)
2244+
2245+ self.setLayout(self.vbox)
2246+
2247+ def GetScaleForRangeX(self, xsubrange):
2248+ # Default graph 1000 pixels wide
2249+ dflt = 1000.0
2250+ r = xsubrange.hi - xsubrange.lo
2251+ return dflt / r
2252+
2253+ def GetScaleForRangeY(self, ysubrange):
2254+ # Default graph 50 pixels high
2255+ dflt = 50.0
2256+ r = ysubrange.hi - ysubrange.lo
2257+ return dflt / r
2258+
2259+ def GetScaleForRange(self, subrange):
2260+ # Default graph 1000 pixels wide, 50 pixels high
2261+ xscale = self.GetScaleForRangeX(subrange.x)
2262+ yscale = self.GetScaleForRangeY(subrange.y)
2263+ return XY(xscale, yscale)
2264+
2265+ def PointEvent(self, cpu, time_from, time_to, hregions):
2266+ text = "CPU: " + str(cpu)
2267+ time_from = time_from.quantize(Decimal(1))
2268+ rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id)
2269+ text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")"
2270+ self.point_label.setText(text)
2271+ self.legend.Highlight(hregions)
2272+
2273+ def RightClickEvent(self, cpu, hregion_times, pos):
2274+ if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"):
2275+ return
2276+ menu = QMenu(self.view)
2277+ for hregion, time in hregion_times:
2278+ thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time)
2279+ menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time)
2280+ menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view))
2281+ menu.exec_(pos)
2282+
2283+ def RightClickSelect(self, args):
2284+ CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args)
2285+
2286+ def NoPointEvent(self):
2287+ self.point_label.setText("")
2288+ self.legend.Highlight({})
2289+
2290+ def RangeEvent(self, time_from, time_to):
2291+ time_from = time_from.quantize(Decimal(1))
2292+ time_to = time_to.quantize(Decimal(1))
2293+ if time_to <= time_from:
2294+ self.point_label.setText("")
2295+ return
2296+ rel_time_from = time_from - self.start_time
2297+ rel_time_to = time_to - self.start_time
2298+ text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")"
2299+ text = text + " duration: " + ToTimeStr(time_to - time_from)
2300+ self.point_label.setText(text)
2301+
2302+ def BackState(self):
2303+ return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect)
2304+
2305+ def PushBackState(self):
2306+ state = copy.deepcopy(self.BackState())
2307+ self.back_state.append(state)
2308+ self.back_button.setEnabled(True)
2309+
2310+ def PopBackState(self):
2311+ self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop()
2312+ self.attrs.Update()
2313+ if not self.back_state:
2314+ self.back_button.setDisabled(True)
2315+
2316+ def PushForwardState(self):
2317+ state = copy.deepcopy(self.BackState())
2318+ self.forward_state.append(state)
2319+ self.forward_button.setEnabled(True)
2320+
2321+ def PopForwardState(self):
2322+ self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop()
2323+ self.attrs.Update()
2324+ if not self.forward_state:
2325+ self.forward_button.setDisabled(True)
2326+
2327+ def Title(self):
2328+ time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo)
2329+ time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi)
2330+ rel_time_from = time_from - self.start_time
2331+ rel_time_to = time_to - self.start_time
2332+ title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to)
2333+ title = title + " (" + ToTimeStr(time_to - time_from) + ")"
2334+ return title
2335+
2336+ def Update(self):
2337+ selected_subrange, selection_state = self.selection_state
2338+ self.item.SetSelection(selection_state)
2339+ self.item.SetBracket(self.fwd_rect)
2340+ self.zoom_button.setDisabled(selected_subrange is None)
2341+ self.GraphTitleChanged(self.Title())
2342+ self.item.update(self.item.boundingRect())
2343+
2344+ def Back(self):
2345+ if not self.back_state:
2346+ return
2347+ self.PushForwardState()
2348+ self.PopBackState()
2349+ self.Update()
2350+
2351+ def Forward(self):
2352+ if not self.forward_state:
2353+ return
2354+ self.PushBackState()
2355+ self.PopForwardState()
2356+ self.Update()
2357+
2358+ def SelectEvent(self, x0, x1, selection_state):
2359+ if selection_state is None:
2360+ selected_subrange = None
2361+ else:
2362+ if x1 - x0 < 1.0:
2363+ x1 += 1.0
2364+ selected_subrange = Subrange(x0, x1)
2365+ self.selection_state = (selected_subrange, selection_state)
2366+ self.zoom_button.setDisabled(selected_subrange is None)
2367+
2368+ def Zoom(self):
2369+ selected_subrange, selection_state = self.selection_state
2370+ if selected_subrange is None:
2371+ return
2372+ self.fwd_rect = selection_state
2373+ self.item.SetSelection(None)
2374+ self.PushBackState()
2375+ self.attrs.subrange.x = selected_subrange
2376+ self.forward_state = []
2377+ self.forward_button.setDisabled(True)
2378+ self.selection_state = (None, None)
2379+ self.fwd_rect = None
2380+ self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x)
2381+ self.attrs.Update()
2382+ self.Update()
2383+
2384+# Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting
2385+
2386+class SlowInitClass():
2387+
2388+ def __init__(self, glb, title, init_fn):
2389+ self.init_fn = init_fn
2390+ self.done = False
2391+ self.result = None
2392+
2393+ self.msg_box = QMessageBox(glb.mainwindow)
2394+ self.msg_box.setText("Initializing " + title + ". Please wait.")
2395+ self.msg_box.setWindowTitle("Initializing " + title)
2396+ self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation))
2397+
2398+ self.init_thread = Thread(self.ThreadFn, glb)
2399+ self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection)
2400+
2401+ self.init_thread.start()
2402+
2403+ def Done(self):
2404+ self.msg_box.done(0)
2405+
2406+ def ThreadFn(self, glb):
2407+ conn_name = "SlowInitClass" + str(os.getpid())
2408+ db, dbname = glb.dbref.Open(conn_name)
2409+ self.result = self.init_fn(db)
2410+ self.done = True
2411+ return (True, 0)
2412+
2413+ def Result(self):
2414+ while not self.done:
2415+ self.msg_box.exec_()
2416+ self.init_thread.wait()
2417+ return self.result
2418+
2419+def SlowInit(glb, title, init_fn):
2420+ init = SlowInitClass(glb, title, init_fn)
2421+ return init.Result()
2422+
2423+# Time chart by CPU window
2424+
2425+class TimeChartByCPUWindow(QMdiSubWindow):
2426+
2427+ def __init__(self, glb, parent=None):
2428+ super(TimeChartByCPUWindow, self).__init__(parent)
2429+
2430+ self.glb = glb
2431+ self.machine_id = glb.HostMachineId()
2432+ self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id)
2433+
2434+ collection = LookupModel(self.collection_name)
2435+ if collection is None:
2436+ collection = SlowInit(glb, "Time Chart", self.Init)
2437+
2438+ self.widget = SwitchGraphWidget(glb, collection, self)
2439+ self.view = self.widget
2440+
2441+ self.base_title = "Time Chart by CPU"
2442+ self.setWindowTitle(self.base_title + self.widget.Title())
2443+ self.widget.graph_title_changed.connect(self.GraphTitleChanged)
2444+
2445+ self.setWidget(self.widget)
2446+
2447+ AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle())
2448+
2449+ def Init(self, db):
2450+ return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id))
2451+
2452+ def GraphTitleChanged(self, title):
2453+ self.setWindowTitle(self.base_title + " : " + title)
2454+
11572455 # Child data item finder
11582456
11592457 class ChildDataItemFinder():
@@ -3025,7 +4323,9 @@ p.c2 { · 1865 unmodified lines
30254323 <p class=c2><a href=#allbranches>1.3 All branches</a></p>
30264324 <p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
30274325 <p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
3028−<p class=c1><a href=#tables>2. Tables</a></p>
4326+<p class=c1><a href=#charts>2. Charts</a></p>
4327+<p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p>
4328+<p class=c1><a href=#tables>3. Tables</a></p>
30294329 <h1 id=reports>1. Reports</h1>
30304330 <h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
30314331 The result is a GUI window with a tree representing a context-sensitive
@@ -3113,7 +4413,29 @@ N.B. Due to the granularity of timestamps, there could be no branches in any giv · 81 unmodified lines
31134413 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.
31144414 The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
31154415 If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
3116−<h1 id=tables>2. Tables</h1>
4416+<h1 id=charts>2. Charts</h1>
4417+<h2 id=timechartbycpu>2.1 Time chart by CPU</h2>
4418+This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu.
4419+<h3>Features</h3>
4420+<ol>
4421+<li>Mouse over to highight the task and show the time</li>
4422+<li>Drag the mouse to select a region and zoom by pushing the Zoom button</li>
4423+<li>Go back and forward by pressing the arrow buttons</li>
4424+<li>If call information is available, right-click to show a call tree opened to that task and time.
4425+Note, the call tree may take some time to appear, and there may not be call information for the task or time selected.
4426+</li>
4427+</ol>
4428+<h3>Important</h3>
4429+The graph can be misleading in the following respects:
4430+<ol>
4431+<li>The graph shows the first task on each CPU as running from the beginning of the time range.
4432+Because tracing might start on different CPUs at different times, that is not necessarily the case.
4433+Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4434+<li>Similarly, the last task on each CPU can be showing running longer than it really was.
4435+Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4436+<li>When the mouse is over a task, the highlighted task might not be visible on the legend without scrolling if the legend does not fit fully in the window</li>
4437+</ol>
4438+<h1 id=tables>3. Tables</h1>
31174439 The Tables menu shows all tables and views in the database. Most tables have an associated view
31184440 which displays the information in a more friendly way. Not all data for large tables is fetched
31194441 immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
@@ -3309,6 +4631,10 @@ class MainWindow(QMainWindow): · 189 unmodified lines
33094631 if IsSelectable(glb.db, "calls"):
33104632 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
33114633
4634+ if IsSelectable(glb.db, "context_switches"):
4635+ charts_menu = menu.addMenu("&Charts")
4636+ charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self))
4637+
33124638 self.TableMenu(GetTableList(glb), menu)
33134639
33144640 self.window_menu = WindowMenu(self.mdi_area, menu)
@@ -3369,6 +4695,9 @@ class MainWindow(QMainWindow): · 54 unmodified lines
33694695 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
33704696 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))
33714697
4698+ def TimeChartByCPU(self):
4699+ TimeChartByCPUWindow(self.glb, self)
4700+
33724701 def TableMenu(self, tables, menu):
33734702 table_menu = menu.addMenu("&Tables")
33744703 for table in tables: