linux-perf-users.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Arnaldo Carvalho de Melo <acme@kernel.org>
To: Ingo Molnar <mingo@kernel.org>, Thomas Gleixner <tglx@linutronix.de>
Cc: Jiri Olsa <jolsa@kernel.org>, Namhyung Kim <namhyung@kernel.org>,
	Clark Williams <williams@redhat.com>,
	linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org,
	Adrian Hunter <adrian.hunter@intel.com>,
	Jiri Olsa <jolsa@redhat.com>,
	Arnaldo Carvalho de Melo <acme@redhat.com>
Subject: [PATCH 14/69] perf scripts python: exported-sql-viewer.py: Add Time chart by CPU
Date: Fri, 11 Oct 2019 17:05:04 -0300	[thread overview]
Message-ID: <20191011200559.7156-15-acme@kernel.org> (raw)
In-Reply-To: <20191011200559.7156-1-acme@kernel.org>

From: Adrian Hunter <adrian.hunter@intel.com>

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.

Signed-off-by: Adrian Hunter <adrian.hunter@intel.com>
Cc: Jiri Olsa <jolsa@redhat.com>
Link: http://lore.kernel.org/lkml/20190821083216.1340-7-adrian.hunter@intel.com
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 .../scripts/python/exported-sql-viewer.py     | 1333 ++++++++++++++++-
 1 file changed, 1331 insertions(+), 2 deletions(-)

diff --git a/tools/perf/scripts/python/exported-sql-viewer.py b/tools/perf/scripts/python/exported-sql-viewer.py
index a5af52f422e6..ebc6a2e5eae9 100755
--- a/tools/perf/scripts/python/exported-sql-viewer.py
+++ b/tools/perf/scripts/python/exported-sql-viewer.py
@@ -105,6 +105,9 @@ except ImportError:
 	glb_nsz = 16
 import re
 import os
+import random
+import copy
+import math
 
 pyside_version_1 = True
 if not "--pyside-version-1" in sys.argv:
@@ -1154,6 +1157,1301 @@ class CallTreeWindow(TreeWindowBase):
 			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 is None:
+			first = query.value(0)
+		if query.value(2) and Decimal(query.value(1)) <= Decimal(time):
+			last = query.value(0)
+	if not(last is None):
+		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 __str__(self):
+		return "XY({}, {})".format(str(self.x), str(self.y))
+
+# Container for sub-range data
+
+class Subrange():
+	def __init__(self, lo=0, hi=0):
+		self.lo = lo
+		self.hi = hi
+
+	def __str__(self):
+		return "Subrange({}, {})".format(str(self.lo), str(self.hi))
+
+# Graph data region base class
+
+class GraphDataRegion(object):
+
+	def __init__(self, key, title = "", ordinal = ""):
+		self.key = key
+		self.title = title
+		self.ordinal = ordinal
+
+# Function to sort GraphDataRegion
+
+def GraphDataRegionOrdinal(data_region):
+	return data_region.ordinal
+
+# Attributes for a graph region
+
+class GraphRegionAttribute():
+
+	def __init__(self, colour):
+		self.colour = colour
+
+# Switch graph data region represents a task
+
+class SwitchGraphDataRegion(GraphDataRegion):
+
+	def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id):
+		super(SwitchGraphDataRegion, self).__init__(key)
+
+		self.title = str(pid) + " / " + str(tid) + " " + comm
+		# Order graph legend within exec comm by pid / tid / time
+		self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16)
+		self.exec_comm_id = exec_comm_id
+		self.pid = pid
+		self.tid = tid
+		self.comm = comm
+		self.thread_id = thread_id
+		self.comm_id = comm_id
+
+# Graph data point
+
+class GraphDataPoint():
+
+	def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None):
+		self.data = data
+		self.index = index
+		self.x = x
+		self.y = y
+		self.altx = altx
+		self.alty = alty
+		self.hregion = hregion
+		self.vregion = vregion
+
+# Graph data (single graph) base class
+
+class GraphData(object):
+
+	def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)):
+		self.collection = collection
+		self.points = []
+		self.xbase = xbase
+		self.ybase = ybase
+		self.title = ""
+
+	def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None):
+		index = len(self.points)
+
+		x = float(Decimal(x) - self.xbase)
+		y = float(Decimal(y) - self.ybase)
+
+		self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion))
+
+	def XToData(self, x):
+		return Decimal(x) + self.xbase
+
+	def YToData(self, y):
+		return Decimal(y) + self.ybase
+
+# Switch graph data (for one CPU)
+
+class SwitchGraphData(GraphData):
+
+	def __init__(self, db, collection, cpu, xbase):
+		super(SwitchGraphData, self).__init__(collection, xbase)
+
+		self.cpu = cpu
+		self.title = "CPU " + str(cpu)
+		self.SelectSwitches(db)
+
+	def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time):
+		query = QSqlQuery(db)
+		QueryExec(query, "SELECT id, c_time"
+					" FROM comms"
+					" WHERE c_thread_id = " + str(thread_id) +
+					"   AND exec_flag = TRUE"
+					"   AND c_time >= " + str(start_time) +
+					"   AND c_time <= " + str(end_time) +
+					" ORDER BY c_time, id")
+		while query.next():
+			comm_id = query.value(0)
+			if comm_id == last_comm_id:
+				continue
+			time = query.value(1)
+			hregion = self.HRegion(db, thread_id, comm_id, time)
+			self.AddPoint(time, 1000, None, None, hregion)
+
+	def SelectSwitches(self, db):
+		last_time = None
+		last_comm_id = None
+		last_thread_id = None
+		query = QSqlQuery(db)
+		QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags"
+					" FROM context_switches"
+					" WHERE machine_id = " + str(self.collection.machine_id) +
+					"   AND cpu = " + str(self.cpu) +
+					" ORDER BY time, id")
+		while query.next():
+			flags = int(query.value(5))
+			if flags & 1:
+				# Schedule-out: detect and add exec's
+				if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3):
+					self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0))
+				continue
+			# Schedule-in: add data point
+			if len(self.points) == 0:
+				start_time = self.collection.glb.StartTime(self.collection.machine_id)
+				hregion = self.HRegion(db, query.value(1), query.value(3), start_time)
+				self.AddPoint(start_time, 1000, None, None, hregion)
+			time = query.value(0)
+			comm_id = query.value(4)
+			thread_id = query.value(2)
+			hregion = self.HRegion(db, thread_id, comm_id, time)
+			self.AddPoint(time, 1000, None, None, hregion)
+			last_time = time
+			last_comm_id = comm_id
+			last_thread_id = thread_id
+
+	def NewHRegion(self, db, key, thread_id, comm_id, time):
+		exec_comm_id = ExecComm(db, thread_id, time)
+		query = QSqlQuery(db)
+		QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id))
+		if query.next():
+			pid = query.value(0)
+			tid = query.value(1)
+		else:
+			pid = -1
+			tid = -1
+		query = QSqlQuery(db)
+		QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id))
+		if query.next():
+			comm = query.value(0)
+		else:
+			comm = ""
+		return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id)
+
+	def HRegion(self, db, thread_id, comm_id, time):
+		key = str(thread_id) + ":" + str(comm_id)
+		hregion = self.collection.LookupHRegion(key)
+		if hregion is None:
+			hregion = self.NewHRegion(db, key, thread_id, comm_id, time)
+			self.collection.AddHRegion(key, hregion)
+		return hregion
+
+# Graph data collection (multiple related graphs) base class
+
+class GraphDataCollection(object):
+
+	def __init__(self, glb):
+		self.glb = glb
+		self.data = []
+		self.hregions = {}
+		self.xrangelo = None
+		self.xrangehi = None
+		self.yrangelo = None
+		self.yrangehi = None
+		self.dp = XY(0, 0)
+
+	def AddGraphData(self, data):
+		self.data.append(data)
+
+	def LookupHRegion(self, key):
+		if key in self.hregions:
+			return self.hregions[key]
+		return None
+
+	def AddHRegion(self, key, hregion):
+		self.hregions[key] = hregion
+
+# Switch graph data collection (SwitchGraphData for each CPU)
+
+class SwitchGraphDataCollection(GraphDataCollection):
+
+	def __init__(self, glb, db, machine_id):
+		super(SwitchGraphDataCollection, self).__init__(glb)
+
+		self.machine_id = machine_id
+		self.cpus = self.SelectCPUs(db)
+
+		self.xrangelo = glb.StartTime(machine_id)
+		self.xrangehi = glb.FinishTime(machine_id)
+
+		self.yrangelo = Decimal(0)
+		self.yrangehi = Decimal(1000)
+
+		for cpu in self.cpus:
+			self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo))
+
+	def SelectCPUs(self, db):
+		cpus = []
+		query = QSqlQuery(db)
+		QueryExec(query, "SELECT DISTINCT cpu"
+					" FROM context_switches"
+					" WHERE machine_id = " + str(self.machine_id))
+		while query.next():
+			cpus.append(int(query.value(0)))
+		return sorted(cpus)
+
+# Switch graph data graphics item displays the graphed data
+
+class SwitchGraphDataGraphicsItem(QGraphicsItem):
+
+	def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None):
+		super(SwitchGraphDataGraphicsItem, self).__init__(parent)
+
+		self.data = data
+		self.graph_width = graph_width
+		self.graph_height = graph_height
+		self.attrs = attrs
+		self.event_handler = event_handler
+		self.setAcceptHoverEvents(True)
+
+	def boundingRect(self):
+		return QRectF(0, 0, self.graph_width, self.graph_height)
+
+	def PaintPoint(self, painter, last, x):
+		if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo):
+			if last.x < self.attrs.subrange.x.lo:
+				x0 = self.attrs.subrange.x.lo
+			else:
+				x0 = last.x
+			if x > self.attrs.subrange.x.hi:
+				x1 = self.attrs.subrange.x.hi
+			else:
+				x1 = x - 1
+			x0 = self.attrs.XToPixel(x0)
+			x1 = self.attrs.XToPixel(x1)
+
+			y0 = self.attrs.YToPixel(last.y)
+
+			colour = self.attrs.region_attributes[last.hregion.key].colour
+
+			width = x1 - x0 + 1
+			if width < 2:
+				painter.setPen(colour)
+				painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height)
+			else:
+				painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour)
+
+	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 BinarySearchPoint(self, target):
+		lower_pos = 0
+		higher_pos = len(self.data.points)
+		while True:
+			pos = int((lower_pos + higher_pos) / 2)
+			val = self.data.points[pos].x
+			if target >= val:
+				lower_pos = pos
+			else:
+				higher_pos = pos
+			if higher_pos <= lower_pos + 1:
+				return lower_pos
+
+	def XPixelToData(self, x):
+		x = self.attrs.PixelToX(x)
+		if x < self.data.points[0].x:
+			x = 0
+			pos = 0
+			low = True
+		else:
+			pos = self.BinarySearchPoint(x)
+			low = False
+		return (low, pos, self.data.XToData(x))
+
+	def EventToData(self, event):
+		no_data = (None,) * 4
+		if len(self.data.points) < 1:
+			return no_data
+		x = event.pos().x()
+		if x < 0:
+			return no_data
+		low0, pos0, time_from = self.XPixelToData(x)
+		low1, pos1, time_to = self.XPixelToData(x + 1)
+		hregions = set()
+		hregion_times = []
+		if not low1:
+			for i in xrange(pos0, pos1 + 1):
+				hregion = self.data.points[i].hregion
+				hregions.add(hregion)
+				if i == pos0:
+					time = time_from
+				else:
+					time = self.data.XToData(self.data.points[i].x)
+				hregion_times.append((hregion, time))
+		return (time_from, time_to, hregions, hregion_times)
+
+	def hoverMoveEvent(self, event):
+		time_from, time_to, hregions, hregion_times = self.EventToData(event)
+		if time_from is not None:
+			self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions)
+
+	def hoverLeaveEvent(self, event):
+		self.event_handler.NoPointEvent()
+
+	def mousePressEvent(self, event):
+		if event.button() != Qt.RightButton:
+			super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event)
+			return
+		time_from, time_to, hregions, hregion_times = self.EventToData(event)
+		if hregion_times:
+			self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos())
+
+# X-axis graphics item
+
+class XAxisGraphicsItem(QGraphicsItem):
+
+	def __init__(self, width, parent=None):
+		super(XAxisGraphicsItem, self).__init__(parent)
+
+		self.width = width
+		self.max_mark_sz = 4
+		self.height = self.max_mark_sz + 1
+
+	def boundingRect(self):
+		return QRectF(0, 0, self.width, self.height)
+
+	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 PaintScale(self, painter, at_x, at_y):
+		n, lo, hi, width = self.ScaleDimensions()
+		if not width:
+			return
+		painter.drawLine(at_x, at_y, at_x + width, at_y)
+		self.PaintMarks(painter, at_y, lo, hi, n, 0)
+
+	def ScaleWidth(self):
+		n, lo, hi, width = self.ScaleDimensions()
+		return width
+
+	def ScaleHeight(self):
+		return self.height
+
+	def ScaleUnit(self):
+		return self.Step() * 10
+
+# Scale graphics item base class
+
+class ScaleGraphicsItem(QGraphicsItem):
+
+	def __init__(self, axis, parent=None):
+		super(ScaleGraphicsItem, self).__init__(parent)
+		self.axis = axis
+
+	def boundingRect(self):
+		scale_width = self.axis.ScaleWidth()
+		if not scale_width:
+			return QRectF()
+		return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight())
+
+	def paint(self, painter, option, widget):
+		scale_width = self.axis.ScaleWidth()
+		if not scale_width:
+			return
+		self.axis.PaintScale(painter, 0, 5)
+		x = scale_width + 4
+		painter.drawText(QPointF(x, 10), self.Text())
+
+	def Unit(self):
+		return self.axis.ScaleUnit()
+
+	def Text(self):
+		return ""
+
+# Switch graph scale graphics item
+
+class SwitchScaleGraphicsItem(ScaleGraphicsItem):
+
+	def __init__(self, axis, parent=None):
+		super(SwitchScaleGraphicsItem, self).__init__(axis, parent)
+
+	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
+
+class SwitchGraphGraphicsItem(QGraphicsItem):
+
+	def __init__(self, collection, data, attrs, event_handler, first, parent=None):
+		super(SwitchGraphGraphicsItem, self).__init__(parent)
+		self.collection = collection
+		self.data = data
+		self.attrs = attrs
+		self.event_handler = event_handler
+
+		margin = 20
+		title_width = 50
+
+		self.title_graphics = QGraphicsSimpleTextItem(data.title, self)
+
+		self.title_graphics.setPos(margin, margin)
+		graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1
+		graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1
+
+		self.graph_origin_x = margin + title_width + margin
+		self.graph_origin_y = graph_height + margin
+
+		x_axis_size = 1
+		y_axis_size = 1
+		self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self)
+
+		self.x_axis = XAxisGraphicsItem(graph_width, self)
+		self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1)
+
+		if first:
+			self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self)
+			self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10)
+
+		self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height)
+
+		self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self)
+		self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1)
+
+		self.width = self.graph_origin_x + graph_width + margin
+		self.height = self.graph_origin_y + margin
+
+		self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self)
+		self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height)
+
+		if parent and 'EnableRubberBand' in dir(parent):
+			parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self)
+
+	def boundingRect(self):
+		return QRectF(0, 0, self.width, self.height)
+
+	def paint(self, painter, option, widget):
+		pass
+
+	def RBXToPixel(self, x):
+		return self.attrs.PixelToX(x - self.graph_origin_x)
+
+	def RBXRangeToPixel(self, x0, x1):
+		return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1))
+
+	def RBPixelToTime(self, x):
+		if x < self.data.points[0].x:
+			return self.data.XToData(0)
+		return self.data.XToData(x)
+
+	def RBEventTimes(self, x0, x1):
+		x0, x1 = self.RBXRangeToPixel(x0, x1)
+		time_from = self.RBPixelToTime(x0)
+		time_to = self.RBPixelToTime(x1)
+		return (time_from, time_to)
+
+	def RBEvent(self, x0, x1):
+		time_from, time_to = self.RBEventTimes(x0, x1)
+		self.event_handler.RangeEvent(time_from, time_to)
+
+	def RBMoveEvent(self, x0, x1):
+		if x1 < x0:
+			x0, x1 = x1, x0
+		self.RBEvent(x0, x1)
+
+	def RBReleaseEvent(self, x0, x1, selection_state):
+		if x1 < x0:
+			x0, x1 = x1, x0
+		x0, x1 = self.RBXRangeToPixel(x0, x1)
+		self.event_handler.SelectEvent(x0, x1, selection_state)
+
+# Graphics item to draw a vertical bracket (used to highlight "forward" sub-range)
+
+class VerticalBracketGraphicsItem(QGraphicsItem):
+
+	def __init__(self, parent=None):
+		super(VerticalBracketGraphicsItem, self).__init__(parent)
+
+		self.width = 0
+		self.height = 0
+		self.hide()
+
+	def SetSize(self, width, height):
+		self.width = width + 1
+		self.height = height + 1
+
+	def boundingRect(self):
+		return QRectF(0, 0, self.width, self.height)
+
+	def paint(self, painter, option, widget):
+		colour = QColor(255, 255, 0, 32)
+		painter.fillRect(0, 0, self.width, self.height, colour)
+		x1 = self.width - 1
+		y1 = self.height - 1
+		painter.drawLine(0, 0, x1, 0)
+		painter.drawLine(0, 0, 0, 3)
+		painter.drawLine(x1, 0, x1, 3)
+		painter.drawLine(0, y1, x1, y1)
+		painter.drawLine(0, y1, 0, y1 - 3)
+		painter.drawLine(x1, y1, x1, y1 - 3)
+
+# Graphics item to contain graphs arranged vertically
+
+class VertcalGraphSetGraphicsItem(QGraphicsItem):
+
+	def __init__(self, collection, attrs, event_handler, child_class, parent=None):
+		super(VertcalGraphSetGraphicsItem, self).__init__(parent)
+
+		self.collection = collection
+
+		self.top = 10
+
+		self.width = 0
+		self.height = self.top
+
+		self.rubber_band = None
+		self.rb_enabled = False
+
+		first = True
+		for data in collection.data:
+			child = child_class(collection, data, attrs, event_handler, first, self)
+			child.setPos(0, self.height + 1)
+			rect = child.boundingRect()
+			if rect.right() > self.width:
+				self.width = rect.right()
+			self.height = self.height + rect.bottom() + 1
+			first = False
+
+		self.bracket = VerticalBracketGraphicsItem(self)
+
+	def EnableRubberBand(self, xlo, xhi, rb_event_handler):
+		if self.rb_enabled:
+			return
+		self.rb_enabled = True
+		self.rb_in_view = False
+		self.setAcceptedMouseButtons(Qt.LeftButton)
+		self.rb_xlo = xlo
+		self.rb_xhi = xhi
+		self.rb_event_handler = rb_event_handler
+		self.mousePressEvent = self.MousePressEvent
+		self.mouseMoveEvent = self.MouseMoveEvent
+		self.mouseReleaseEvent = self.MouseReleaseEvent
+
+	def boundingRect(self):
+		return QRectF(0, 0, self.width, self.height)
+
+	def paint(self, painter, option, widget):
+		pass
+
+	def RubberBandParent(self):
+		scene = self.scene()
+		view = scene.views()[0]
+		viewport = view.viewport()
+		return viewport
+
+	def RubberBandSetGeometry(self, rect):
+		scene_rectf = self.mapRectToScene(QRectF(rect))
+		scene = self.scene()
+		view = scene.views()[0]
+		poly = view.mapFromScene(scene_rectf)
+		self.rubber_band.setGeometry(poly.boundingRect())
+
+	def SetSelection(self, selection_state):
+		if self.rubber_band:
+			if selection_state:
+				self.RubberBandSetGeometry(selection_state)
+				self.rubber_band.show()
+			else:
+				self.rubber_band.hide()
+
+	def SetBracket(self, rect):
+		if rect:
+			x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()
+			self.bracket.setPos(x, y)
+			self.bracket.SetSize(width, height)
+			self.bracket.show()
+		else:
+			self.bracket.hide()
+
+	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 is None:
+			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 __init__(self, collection, region_attributes, parent=None):
+		super(SwitchGraphLegendModel, self).__init__(parent)
+
+		self.region_attributes = region_attributes
+
+		self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal)
+		self.child_count = len(self.child_items)
+
+		self.highlight_set = set()
+
+		self.column_headers = ("pid", "tid", "comm")
+
+	def rowCount(self, parent):
+		return self.child_count
+
+	def headerData(self, section, orientation, role):
+		if role != Qt.DisplayRole:
+			return None
+		if orientation != Qt.Horizontal:
+			return None
+		return self.columnHeader(section)
+
+	def index(self, row, column, parent):
+		return self.createIndex(row, column, self.child_items[row])
+
+	def columnCount(self, parent=None):
+		return len(self.column_headers)
+
+	def columnHeader(self, column):
+		return self.column_headers[column]
+
+	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
+			return None
+		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:
+			return None
+		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
+		return None
+
+	def SetHighlight(self, row, set_highlight):
+		child = self.child_items[row]
+		top_left = self.createIndex(row, 0, child)
+		bottom_right = self.createIndex(row, len(self.column_headers) - 1, child)
+		self.dataChanged.emit(top_left, bottom_right)
+
+	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 not in highlight_set:
+					self.SetHighlight(row, False)
+			elif child in highlight_set:
+				self.SetHighlight(row, True)
+		self.highlight_set = highlight_set
+
+# Switch graph legend is a table
+
+class SwitchGraphLegend(QWidget):
+
+	def __init__(self, collection, region_attributes, parent=None):
+		super(SwitchGraphLegend, self).__init__(parent)
+
+		self.data_model = SwitchGraphLegendModel(collection, region_attributes)
+
+		self.model = QSortFilterProxyModel()
+		self.model.setSourceModel(self.data_model)
+
+		self.view = QTableView()
+		self.view.setModel(self.model)
+		self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
+		self.view.verticalHeader().setVisible(False)
+		self.view.sortByColumn(-1, Qt.AscendingOrder)
+		self.view.setSortingEnabled(True)
+		self.view.resizeColumnsToContents()
+		self.view.resizeRowsToContents()
+
+		self.vbox = VBoxLayout(self.view)
+		self.setLayout(self.vbox)
+
+		sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2
+		sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width()
+		self.saved_size = sz1
+
+	def resizeEvent(self, event):
+		self.saved_size = self.size().width()
+		super(SwitchGraphLegend, self).resizeEvent(event)
+
+	def Highlight(self, highlight_set):
+		self.data_model.Highlight(highlight_set)
+		self.update()
+
+	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:
+		return True
+	if g <= 160:
+		return False
+	if r <= 180 and g <= 180:
+		return False
+	if r < 60:
+		return False
+	return True
+
+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 GenerateNRandomColours(n, seed):
+	colours = GenerateNColours(n)
+	random.seed(seed)
+	random.shuffle(colours)
+	return colours
+
+# Graph attributes, in particular the scale and subrange that change when zooming
+
+class GraphAttributes():
+
+	def __init__(self, scale, subrange, region_attributes, dp):
+		self.scale = scale
+		self.subrange = subrange
+		self.region_attributes = region_attributes
+		# Rounding avoids errors due to finite floating point precision
+		self.dp = dp	# data decimal places
+		self.Update()
+
+	def XToPixel(self, x):
+		return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x))
+
+	def YToPixel(self, y):
+		return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y))
+
+	def PixelToXRounded(self, px):
+		return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo
+
+	def PixelToYRounded(self, py):
+		return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo
+
+	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
+
+class SwitchGraphSplitter(QSplitter):
+
+	def __init__(self, parent=None):
+		super(SwitchGraphSplitter, self).__init__(parent)
+
+		self.first_time = False
+
+	def resizeEvent(self, ev):
+		if self.first_time:
+			self.first_time = False
+			sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2
+			sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width()
+			sz0 = self.size().width() - self.handleWidth() - sz1
+			self.setSizes([sz0, sz1])
+		elif not(self.widget(1).saved_size is None):
+			sz1 = self.widget(1).saved_size
+			sz0 = self.size().width() - self.handleWidth() - sz1
+			self.setSizes([sz0, sz1])
+		super(SwitchGraphSplitter, self).resizeEvent(ev)
+
+# Graph widget base class
+
+class GraphWidget(QWidget):
+
+	graph_title_changed = Signal(object)
+
+	def __init__(self, parent=None):
+		super(GraphWidget, self).__init__(parent)
+
+	def GraphTitleChanged(self, title):
+		self.graph_title_changed.emit(title)
+
+	def Title(self):
+		return ""
+
+# Display time in s, ms, us or ns
+
+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
+
+class SwitchGraphWidget(GraphWidget):
+
+	def __init__(self, glb, collection, parent=None):
+		super(SwitchGraphWidget, self).__init__(parent)
+
+		self.glb = glb
+		self.collection = collection
+
+		self.back_state = []
+		self.forward_state = []
+		self.selection_state = (None, None)
+		self.fwd_rect = None
+		self.start_time = self.glb.StartTime(collection.machine_id)
+
+		i = 0
+		hregions = collection.hregions.values()
+		colours = GenerateNRandomColours(len(hregions), 1013)
+		region_attributes = {}
+		for hregion in hregions:
+			if hregion.pid == 0 and hregion.tid == 0:
+				region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0))
+			else:
+				region_attributes[hregion.key] = GraphRegionAttribute(colours[i])
+				i = i + 1
+
+		# Default to entire range
+		xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0)
+		ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0)
+		subrange = XY(xsubrange, ysubrange)
+
+		scale = self.GetScaleForRange(subrange)
+
+		self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp)
+
+		self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem)
+
+		self.scene = QGraphicsScene()
+		self.scene.addItem(self.item)
+
+		self.view = QGraphicsView(self.scene)
+		self.view.centerOn(0, 0)
+		self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop)
+
+		self.legend = SwitchGraphLegend(collection, region_attributes)
+
+		self.splitter = SwitchGraphSplitter()
+		self.splitter.addWidget(self.view)
+		self.splitter.addWidget(self.legend)
+
+		self.point_label = QLabel("")
+		self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
+
+		self.back_button = QToolButton()
+		self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft))
+		self.back_button.setDisabled(True)
+		self.back_button.released.connect(lambda: self.Back())
+
+		self.forward_button = QToolButton()
+		self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight))
+		self.forward_button.setDisabled(True)
+		self.forward_button.released.connect(lambda: self.Forward())
+
+		self.zoom_button = QToolButton()
+		self.zoom_button.setText("Zoom")
+		self.zoom_button.setDisabled(True)
+		self.zoom_button.released.connect(lambda: self.Zoom())
+
+		self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label)
+
+		self.vbox = VBoxLayout(self.splitter, self.hbox)
+
+		self.setLayout(self.vbox)
+
+	def GetScaleForRangeX(self, xsubrange):
+		# Default graph 1000 pixels wide
+		dflt = 1000.0
+		r = xsubrange.hi - xsubrange.lo
+		return dflt / r
+
+	def GetScaleForRangeY(self, ysubrange):
+		# Default graph 50 pixels high
+		dflt = 50.0
+		r = ysubrange.hi - ysubrange.lo
+		return dflt / r
+
+	def GetScaleForRange(self, subrange):
+		# Default graph 1000 pixels wide, 50 pixels high
+		xscale = self.GetScaleForRangeX(subrange.x)
+		yscale = self.GetScaleForRangeY(subrange.y)
+		return XY(xscale, yscale)
+
+	def PointEvent(self, cpu, time_from, time_to, hregions):
+		text = "CPU: " + str(cpu)
+		time_from = time_from.quantize(Decimal(1))
+		rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id)
+		text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")"
+		self.point_label.setText(text)
+		self.legend.Highlight(hregions)
+
+	def RightClickEvent(self, cpu, hregion_times, pos):
+		if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"):
+			return
+		menu = QMenu(self.view)
+		for hregion, time in hregion_times:
+			thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time)
+			menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time)
+			menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view))
+		menu.exec_(pos)
+
+	def RightClickSelect(self, args):
+		CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args)
+
+	def NoPointEvent(self):
+		self.point_label.setText("")
+		self.legend.Highlight({})
+
+	def RangeEvent(self, time_from, time_to):
+		time_from = time_from.quantize(Decimal(1))
+		time_to = time_to.quantize(Decimal(1))
+		if time_to <= time_from:
+			self.point_label.setText("")
+			return
+		rel_time_from = time_from - self.start_time
+		rel_time_to = time_to - self.start_time
+		text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")"
+		text = text + " duration: " + ToTimeStr(time_to - time_from)
+		self.point_label.setText(text)
+
+	def BackState(self):
+		return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect)
+
+	def PushBackState(self):
+		state = copy.deepcopy(self.BackState())
+		self.back_state.append(state)
+		self.back_button.setEnabled(True)
+
+	def PopBackState(self):
+		self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop()
+		self.attrs.Update()
+		if not self.back_state:
+			self.back_button.setDisabled(True)
+
+	def PushForwardState(self):
+		state = copy.deepcopy(self.BackState())
+		self.forward_state.append(state)
+		self.forward_button.setEnabled(True)
+
+	def PopForwardState(self):
+		self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop()
+		self.attrs.Update()
+		if not self.forward_state:
+			self.forward_button.setDisabled(True)
+
+	def Title(self):
+		time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo)
+		time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi)
+		rel_time_from = time_from - self.start_time
+		rel_time_to = time_to - self.start_time
+		title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to)
+		title = title + " (" + ToTimeStr(time_to - time_from) + ")"
+		return title
+
+	def Update(self):
+		selected_subrange, selection_state = self.selection_state
+		self.item.SetSelection(selection_state)
+		self.item.SetBracket(self.fwd_rect)
+		self.zoom_button.setDisabled(selected_subrange is None)
+		self.GraphTitleChanged(self.Title())
+		self.item.update(self.item.boundingRect())
+
+	def Back(self):
+		if not self.back_state:
+			return
+		self.PushForwardState()
+		self.PopBackState()
+		self.Update()
+
+	def Forward(self):
+		if not self.forward_state:
+			return
+		self.PushBackState()
+		self.PopForwardState()
+		self.Update()
+
+	def SelectEvent(self, x0, x1, selection_state):
+		if selection_state is None:
+			selected_subrange = None
+		else:
+			if x1 - x0 < 1.0:
+				x1 += 1.0
+			selected_subrange = Subrange(x0, x1)
+		self.selection_state = (selected_subrange, selection_state)
+		self.zoom_button.setDisabled(selected_subrange is None)
+
+	def Zoom(self):
+		selected_subrange, selection_state = self.selection_state
+		if selected_subrange is None:
+			return
+		self.fwd_rect = selection_state
+		self.item.SetSelection(None)
+		self.PushBackState()
+		self.attrs.subrange.x = selected_subrange
+		self.forward_state = []
+		self.forward_button.setDisabled(True)
+		self.selection_state = (None, None)
+		self.fwd_rect = None
+		self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x)
+		self.attrs.Update()
+		self.Update()
+
+# Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting
+
+class SlowInitClass():
+
+	def __init__(self, glb, title, init_fn):
+		self.init_fn = init_fn
+		self.done = False
+		self.result = None
+
+		self.msg_box = QMessageBox(glb.mainwindow)
+		self.msg_box.setText("Initializing " + title + ". Please wait.")
+		self.msg_box.setWindowTitle("Initializing " + title)
+		self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation))
+
+		self.init_thread = Thread(self.ThreadFn, glb)
+		self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection)
+
+		self.init_thread.start()
+
+	def Done(self):
+		self.msg_box.done(0)
+
+	def ThreadFn(self, glb):
+		conn_name = "SlowInitClass" + str(os.getpid())
+		db, dbname = glb.dbref.Open(conn_name)
+		self.result = self.init_fn(db)
+		self.done = True
+		return (True, 0)
+
+	def Result(self):
+		while not self.done:
+			self.msg_box.exec_()
+		self.init_thread.wait()
+		return self.result
+
+def SlowInit(glb, title, init_fn):
+	init = SlowInitClass(glb, title, init_fn)
+	return init.Result()
+
+# Time chart by CPU window
+
+class TimeChartByCPUWindow(QMdiSubWindow):
+
+	def __init__(self, glb, parent=None):
+		super(TimeChartByCPUWindow, self).__init__(parent)
+
+		self.glb = glb
+		self.machine_id = glb.HostMachineId()
+		self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id)
+
+		collection = LookupModel(self.collection_name)
+		if collection is None:
+			collection = SlowInit(glb, "Time Chart", self.Init)
+
+		self.widget = SwitchGraphWidget(glb, collection, self)
+		self.view = self.widget
+
+		self.base_title = "Time Chart by CPU"
+		self.setWindowTitle(self.base_title + self.widget.Title())
+		self.widget.graph_title_changed.connect(self.GraphTitleChanged)
+
+		self.setWidget(self.widget)
+
+		AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle())
+
+	def Init(self, db):
+		return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id))
+
+	def GraphTitleChanged(self, title):
+		self.setWindowTitle(self.base_title + " : " + title)
+
 # Child data item  finder
 
 class ChildDataItemFinder():
@@ -3025,7 +4323,9 @@ p.c2 {
 <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=#tables>2. Tables</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
@@ -3113,7 +4413,29 @@ N.B. Due to the granularity of timestamps, there could be no branches in any giv
 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.
 If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
-<h1 id=tables>2. Tables</h1>
+<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 is not 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,
@@ -3309,6 +4631,10 @@ class MainWindow(QMainWindow):
 		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))
+
 		self.TableMenu(GetTableList(glb), menu)
 
 		self.window_menu = WindowMenu(self.mdi_area, menu)
@@ -3369,6 +4695,9 @@ class MainWindow(QMainWindow):
 				label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
 				reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))
 
+	def TimeChartByCPU(self):
+		TimeChartByCPUWindow(self.glb, self)
+
 	def TableMenu(self, tables, menu):
 		table_menu = menu.addMenu("&Tables")
 		for table in tables:
-- 
2.21.0

  parent reply	other threads:[~2019-10-11 20:05 UTC|newest]

Thread overview: 71+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2019-10-11 20:04 [GIT PULL] perf/core improvements and fixes Arnaldo Carvalho de Melo
2019-10-11 20:04 ` [PATCH 01/69] perf env: Add routine to read the env->cpuid from the running machine Arnaldo Carvalho de Melo
2019-10-11 20:04 ` [PATCH 02/69] perf top: Initialize perf_env->cpuid, needed by the per arch annotation init routine Arnaldo Carvalho de Melo
2019-10-11 20:04 ` [PATCH 03/69] perf evlist: Adopt __set_tracepoint_handlers method from perf_session Arnaldo Carvalho de Melo
2019-10-11 20:04 ` [PATCH 04/69] perf trace: Make evlist__set_evsel_handler() affect just entries without a handler Arnaldo Carvalho de Melo
2019-10-11 20:04 ` [PATCH 05/69] perf trace: Separate 'struct syscall_fmt' definition from syscall_fmts variable Arnaldo Carvalho de Melo
2019-10-11 20:04 ` [PATCH 06/69] perf trace: Generalize the syscall_fmt find routines Arnaldo Carvalho de Melo
2019-10-11 20:04 ` [PATCH 07/69] perf trace: Postpone parsing .perfconfig trace.add_events to after --verbose is processed Arnaldo Carvalho de Melo
2019-10-11 20:04 ` [PATCH 08/69] perf trace augmented_syscalls: Do not show syscalls when none was asked for Arnaldo Carvalho de Melo
2019-10-11 20:04 ` [PATCH 09/69] perf scripts python: exported-sql-viewer.py: Add LookupModel() Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 10/69] perf scripts python: exported-sql-viewer.py: Add HBoxLayout and VBoxLayout Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 11/69] perf scripts python: exported-sql-viewer.py: Add global time range calculations Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 12/69] perf scripts python: exported-sql-viewer.py: Tidy up Call tree call_time Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 13/69] perf scripts python: exported-sql-viewer.py: Add ability for Call tree to open at a specified task and time Arnaldo Carvalho de Melo
2019-10-11 20:05 ` Arnaldo Carvalho de Melo [this message]
2019-10-11 20:05 ` [PATCH 15/69] perf tools: Make usage of test_attr__* optional for perf-sys.h Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 16/69] samples/bpf: fix build by setting HAVE_ATTR_TEST to zero Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 17/69] perf script: Allow --time with --reltime Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 18/69] perf trace: Factor out the initialization of syscal_arg_fmt->scnprintf Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 19/69] perf trace: Allocate an array of beautifiers for tracepoint args Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 20/69] perf trace: Move some scnprintf methods from syscall to syscall_arg_fmt Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 21/69] perf trace: Add the syscall_arg_fmt pointer to syscall_arg Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 22/69] perf trace: Add array of chars scnprintf beautifier Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 23/69] perf trace: Enclose all events argument lists with () Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 24/69] perf trace: Allow choosing how to augment the tracepoint arguments Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 25/69] tools arch x86: Grab a copy of the file containing the MSR numbers Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 26/69] perf beauty: Make strarray's offset be u64 Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 27/69] perf trace beauty: Add a x86 MSR cmd id->str table generator Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 28/69] perf beauty: Hook up the x86 MSR " Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 29/69] perf trace: Allow associating scnprintf routines with well known arg names Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 30/69] perf trace beauty: Add the glue for the autogenerated MSR arrays Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 31/69] perf trace: Associate the "msr" tracepoint arg name with x86_MSR__scnprintf() Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 32/69] perf evlist: Factor out asprintf routine to build a tracepoint pid filter Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 33/69] perf evlist: Introduce append_tp_filter() method Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 34/69] perf evlist: Introduce append_tp_filter_pid() and append_tp_filter_pids() Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 35/69] perf trace: Introduce --filter for tracepoint events Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 36/69] perf trace: Add a strtoul() method to 'struct syscall_arg_fmt' Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 37/69] perf trace: Introduce a strtoul() method for 'struct strarrays' Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 38/69] perf trace: Expand strings in filters to integers Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 39/69] perf beauty: Introduce strtoul() for x86 MSRs Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 40/69] perf tools: Avoid 'sample_reg_masks' being const + weak Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 41/69] MAINTAINERS: Add entry for perf tool arm64 pmu-events files Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 42/69] libperf: Add perf_mmap__init() function Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 43/69] libperf: Add 'struct perf_mmap_param' Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 44/69] libperf: Adopt perf_mmap__mmap_len() function from tools/perf Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 45/69] libperf: Adopt perf_mmap__mmap() " Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 46/69] libperf: Adopt perf_mmap__get() " Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 47/69] libperf: Adopt perf_mmap__unmap() " Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 48/69] libperf: Adopt perf_mmap__put() " Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 49/69] perf tools: Use perf_mmap way to detect aux mmap Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 50/69] libperf: Adopt perf_mmap__consume() function from tools/perf Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 51/69] libperf: Adopt perf_mmap__read_init() " Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 52/69] libperf: Adopt perf_mmap__read_done() " Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 53/69] libperf: Adopt perf_mmap__read_event() " Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 54/69] libperf: Adopt perf_evlist__mmap()/munmap() " Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 55/69] libperf: Introduce perf_evlist__mmap_ops() Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 56/69] libperf: Introduce perf_evlist_mmap_ops::idx callback Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 57/69] libperf: Add perf_evlist_mmap_ops::get callback Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 58/69] libperf: Introduce perf_evlist_mmap_ops::mmap callback Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 59/69] perf tools: Introduce perf_evlist__mmap_cb_idx() Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 60/69] perf evlist: Introduce perf_evlist__mmap_cb_get() Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 61/69] perf evlist: Introduce perf_evlist__mmap_cb_mmap() Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 62/69] perf evlist: Switch to libperf's mmap interface Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 63/69] libperf: Centralize map refcnt setting Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 64/69] libperf: Move the pollfd allocation from tools/perf to libperf Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 65/69] libperf: Introduce perf_evlist__exit() Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 66/69] libperf: Introduce perf_evlist__purge() Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 67/69] libperf: Adopt perf_evlist__filter_pollfd() from tools/perf Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 68/69] perf tools: Propagate CFLAGS to libperf Arnaldo Carvalho de Melo
2019-10-11 20:05 ` [PATCH 69/69] perf diff: Report noisy for cycles diff Arnaldo Carvalho de Melo
2019-10-15  5:25 ` [GIT PULL] perf/core improvements and fixes Ingo Molnar

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20191011200559.7156-15-acme@kernel.org \
    --to=acme@kernel.org \
    --cc=acme@redhat.com \
    --cc=adrian.hunter@intel.com \
    --cc=jolsa@kernel.org \
    --cc=jolsa@redhat.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-perf-users@vger.kernel.org \
    --cc=mingo@kernel.org \
    --cc=namhyung@kernel.org \
    --cc=tglx@linutronix.de \
    --cc=williams@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).