xref: /freebsd/tools/sched/schedgraph.py (revision 05248206f720394d95c2a7475429311df670a2e9)
1#!/usr/local/bin/python
2
3# Copyright (c) 2002-2003, 2009, Jeffrey Roberson <jeff@freebsd.org>
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10#    notice unmodified, this list of conditions, and the following
11#    disclaimer.
12# 2. Redistributions in binary form must reproduce the above copyright
13#    notice, this list of conditions and the following disclaimer in the
14#     documentation and/or other materials provided with the distribution.
15#
16# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
20# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
25# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26#
27
28from __future__ import print_function
29import sys
30import re
31import random
32from operator import attrgetter, itemgetter
33from functools import total_ordering
34from tkinter import *
35
36# To use:
37# - Install the ports/x11-toolkits/py-tkinter package; e.g.
38#	pkg install x11-toolkits/py-tkinter
39# - Add KTR_SCHED to KTR_COMPILE and KTR_MASK in your KERNCONF; e.g.
40#	options 	KTR
41#	options 	KTR_ENTRIES=32768
42#	options 	KTR_COMPILE=(KTR_SCHED)
43#	options 	KTR_MASK=(KTR_SCHED)
44# - It is encouraged to increase KTR_ENTRIES size to gather enough
45#    information for analysis; e.g.
46#	options 	KTR_ENTRIES=262144
47#   as 32768 entries may only correspond to a second or two of profiling
48#   data depending on your workload.
49# - Rebuild kernel with proper changes to KERNCONF and boot new kernel.
50# - Run your workload to be profiled.
51# - While the workload is continuing (i.e. before it finishes), disable
52#   KTR tracing by setting 'sysctl debug.ktr.mask=0'.  This is necessary
53#   to avoid a race condition while running ktrdump, i.e. the KTR ring buffer
54#   will cycle a bit while ktrdump runs, and this confuses schedgraph because
55#   the timestamps appear to go backwards at some point.  Stopping KTR logging
56#   while the workload is still running is to avoid wasting log entries on
57#   "idle" time at the end.
58# - Dump the trace to a file: 'ktrdump -ct > ktr.out'
59# - Alternatively, use schedgraph.d script in this directory for getting
60#   the trace data by means of DTrace.  See the script for details.
61# - Run the python script: 'python schedgraph.py ktr.out' optionally provide
62#   your cpu frequency in ghz: 'python schedgraph.py ktr.out 2.4'
63#
64# To do:
65# Add a per-source summary display
66# "Vertical rule" to help relate data in different rows
67# Mouse-over popup of full thread/event/row label (currently truncated)
68# More visible anchors for popup event windows
69#
70# BUGS: 1) Only 8 CPUs are supported, more CPUs require more choices of
71#          colours to represent them ;-)
72
73eventcolors = [
74	("count",	"red"),
75	("running",	"green"),
76	("idle",	"grey"),
77	("spinning",	"red"),
78	("yielding",	"yellow"),
79	("swapped",	"violet"),
80	("suspended",	"purple"),
81	("iwait",	"grey"),
82	("sleep",	"blue"),
83	("blocked",	"dark red"),
84	("runq add",	"yellow"),
85	("runq rem",	"yellow"),
86	("thread exit",	"grey"),
87	("proc exit",	"grey"),
88	("lock acquire", "blue"),
89	("lock contest", "purple"),
90	("failed lock try", "red"),
91	("lock release", "grey"),
92	("statclock",	"black"),
93	("prio",	"black"),
94	("lend prio",	"black"),
95	("wokeup",	"black")
96]
97
98cpucolors = [
99	("CPU 0",	"light grey"),
100	("CPU 1",	"dark grey"),
101	("CPU 2",	"light blue"),
102	("CPU 3",	"light pink"),
103	("CPU 4",	"blanched almond"),
104	("CPU 5",	"slate grey"),
105	("CPU 6",	"tan"),
106	("CPU 7",	"thistle"),
107	("CPU 8",	"white")
108]
109
110colors = [
111	"white", "thistle", "blanched almond", "tan", "chartreuse",
112	"dark red", "red", "pale violet red", "pink", "light pink",
113	"dark orange", "orange", "coral", "light coral",
114	"goldenrod", "gold", "yellow", "light yellow",
115	"dark green", "green", "light green", "light sea green",
116	"dark blue", "blue", "light blue", "steel blue", "light slate blue",
117	"dark violet", "violet", "purple", "blue violet",
118	"dark grey", "slate grey", "light grey",
119	"black",
120]
121colors.sort()
122
123ticksps = None
124status = None
125colormap = None
126ktrfile = None
127clockfreq = None
128sources = []
129lineno = -1
130
131Y_BORDER = 10
132X_BORDER = 10
133Y_COUNTER = 80
134Y_EVENTSOURCE = 10
135XY_POINT = 4
136
137class Colormap:
138	def __init__(self, table):
139		self.table = table
140		self.map = {}
141		for entry in table:
142			self.map[entry[0]] = entry[1]
143
144	def lookup(self, name):
145		try:
146			color = self.map[name]
147		except:
148			color = colors[random.randrange(0, len(colors))]
149			print("Picking random color", color, "for", name)
150			self.map[name] = color
151			self.table.append((name, color))
152		return (color)
153
154def ticks2sec(ticks):
155	ticks = float(ticks)
156	ns = float(ticksps) / 1000000000
157	ticks /= ns
158	if (ticks < 1000):
159		return ("%.2fns" % ticks)
160	ticks /= 1000
161	if (ticks < 1000):
162		return ("%.2fus" % ticks)
163	ticks /= 1000
164	if (ticks < 1000):
165		return ("%.2fms" % ticks)
166	ticks /= 1000
167	return ("%.2fs" % ticks)
168
169class Scaler(Frame):
170	def __init__(self, master, target):
171		Frame.__init__(self, master)
172		self.scale = None
173		self.target = target
174		self.label = Label(self, text="Ticks per pixel")
175		self.label.pack(side=LEFT)
176		self.resolution = 100
177		self.setmax(10000)
178
179	def scaleset(self, value):
180		self.target.scaleset(int(value))
181
182	def set(self, value):
183		self.scale.set(value)
184
185	def setmax(self, value):
186		#
187		# We can't reconfigure the to_ value so we delete the old
188		# window and make a new one when we resize.
189		#
190		if (self.scale != None):
191			self.scale.pack_forget()
192			self.scale.destroy()
193		self.scale = Scale(self, command=self.scaleset,
194		    from_=100, to_=value, orient=HORIZONTAL,
195		    resolution=self.resolution)
196		self.scale.pack(fill="both", expand=1)
197		self.scale.set(self.target.scaleget())
198
199class Status(Frame):
200	def __init__(self, master):
201		Frame.__init__(self, master)
202		self.label = Label(self, bd=1, relief=SUNKEN, anchor=W)
203		self.label.pack(fill="both", expand=1)
204		self.clear()
205
206	def set(self, str):
207		self.label.config(text=str)
208
209	def clear(self):
210		self.label.config(text="")
211
212	def startup(self, str):
213		self.set(str)
214		root.update()
215
216class ColorConf(Frame):
217	def __init__(self, master, name, color):
218		Frame.__init__(self, master)
219		if (graph.getstate(name) == "hidden"):
220			enabled = 0
221		else:
222			enabled = 1
223		self.name = name
224		self.color = StringVar()
225		self.color_default = color
226		self.color_current = color
227		self.color.set(color)
228		self.enabled = IntVar()
229		self.enabled_default = enabled
230		self.enabled_current = enabled
231		self.enabled.set(enabled)
232		self.draw()
233
234	def draw(self):
235		self.label = Label(self, text=self.name, anchor=W)
236		self.sample = Canvas(self, width=24, height=24,
237		    bg='grey')
238		self.rect = self.sample.create_rectangle(0, 0, 24, 24,
239		    fill=self.color.get())
240		self.list = OptionMenu(self, self.color, command=self.setcolor,
241		    *colors)
242		self.checkbox = Checkbutton(self, text="enabled",
243		    variable=self.enabled)
244		self.label.grid(row=0, column=0, sticky=E+W)
245		self.sample.grid(row=0, column=1)
246		self.list.grid(row=0, column=2, sticky=E+W)
247		self.checkbox.grid(row=0, column=3)
248		self.columnconfigure(0, weight=1)
249		self.columnconfigure(2, minsize=150)
250
251	def setcolor(self, color):
252		self.color.set(color)
253		self.sample.itemconfigure(self.rect, fill=color)
254
255	def apply(self):
256		cchange = 0
257		echange = 0
258		if (self.color_current != self.color.get()):
259			cchange = 1
260		if (self.enabled_current != self.enabled.get()):
261			echange = 1
262		self.color_current = self.color.get()
263		self.enabled_current = self.enabled.get()
264		if (echange != 0):
265			if (self.enabled_current):
266				graph.setcolor(self.name, self.color_current)
267			else:
268				graph.hide(self.name)
269			return
270		if (cchange != 0):
271			graph.setcolor(self.name, self.color_current)
272
273	def revert(self):
274		self.setcolor(self.color_default)
275		self.enabled.set(self.enabled_default)
276
277class ColorConfigure(Toplevel):
278	def __init__(self, table, name):
279		Toplevel.__init__(self)
280		self.resizable(0, 0)
281		self.title(name)
282		self.items = LabelFrame(self, text="Item Type")
283		self.buttons = Frame(self)
284		self.drawbuttons()
285		self.items.grid(row=0, column=0, sticky=E+W)
286		self.columnconfigure(0, weight=1)
287		self.buttons.grid(row=1, column=0, sticky=E+W)
288		self.types = []
289		self.irow = 0
290		for type in table:
291			color = graph.getcolor(type[0])
292			if (color != ""):
293				self.additem(type[0], color)
294		self.bind("<Control-w>", self.destroycb)
295
296	def destroycb(self, event):
297		self.destroy()
298
299	def additem(self, name, color):
300		item = ColorConf(self.items, name, color)
301		self.types.append(item)
302		item.grid(row=self.irow, column=0, sticky=E+W)
303		self.irow += 1
304
305	def drawbuttons(self):
306		self.apply = Button(self.buttons, text="Apply",
307		    command=self.apress)
308		self.default = Button(self.buttons, text="Revert",
309		    command=self.rpress)
310		self.apply.grid(row=0, column=0, sticky=E+W)
311		self.default.grid(row=0, column=1, sticky=E+W)
312		self.buttons.columnconfigure(0, weight=1)
313		self.buttons.columnconfigure(1, weight=1)
314
315	def apress(self):
316		for item in self.types:
317			item.apply()
318
319	def rpress(self):
320		for item in self.types:
321			item.revert()
322
323class SourceConf(Frame):
324	def __init__(self, master, source):
325		Frame.__init__(self, master)
326		if (source.hidden == 1):
327			enabled = 0
328		else:
329			enabled = 1
330		self.source = source
331		self.name = source.name
332		self.enabled = IntVar()
333		self.enabled_default = enabled
334		self.enabled_current = enabled
335		self.enabled.set(enabled)
336		self.draw()
337
338	def draw(self):
339		self.label = Label(self, text=self.name, anchor=W)
340		self.checkbox = Checkbutton(self, text="enabled",
341		    variable=self.enabled)
342		self.label.grid(row=0, column=0, sticky=E+W)
343		self.checkbox.grid(row=0, column=1)
344		self.columnconfigure(0, weight=1)
345
346	def changed(self):
347		if (self.enabled_current != self.enabled.get()):
348			return 1
349		return 0
350
351	def apply(self):
352		self.enabled_current = self.enabled.get()
353
354	def revert(self):
355		self.enabled.set(self.enabled_default)
356
357	def check(self):
358		self.enabled.set(1)
359
360	def uncheck(self):
361		self.enabled.set(0)
362
363class SourceConfigure(Toplevel):
364	def __init__(self):
365		Toplevel.__init__(self)
366		self.resizable(0, 0)
367		self.title("Source Configuration")
368		self.items = []
369		self.iframe = Frame(self)
370		self.iframe.grid(row=0, column=0, sticky=E+W)
371		f = LabelFrame(self.iframe, bd=4, text="Sources")
372		self.items.append(f)
373		self.buttons = Frame(self)
374		self.items[0].grid(row=0, column=0, sticky=E+W)
375		self.columnconfigure(0, weight=1)
376		self.sconfig = []
377		self.irow = 0
378		self.icol = 0
379		for source in sources:
380			self.addsource(source)
381		self.drawbuttons()
382		self.buttons.grid(row=1, column=0, sticky=W)
383		self.bind("<Control-w>", self.destroycb)
384
385	def destroycb(self, event):
386		self.destroy()
387
388	def addsource(self, source):
389		if (self.irow > 30):
390			self.icol += 1
391			self.irow = 0
392			c = self.icol
393			f = LabelFrame(self.iframe, bd=4, text="Sources")
394			f.grid(row=0, column=c, sticky=N+E+W)
395			self.items.append(f)
396		item = SourceConf(self.items[self.icol], source)
397		self.sconfig.append(item)
398		item.grid(row=self.irow, column=0, sticky=E+W)
399		self.irow += 1
400
401	def drawbuttons(self):
402		self.apply = Button(self.buttons, text="Apply",
403		    command=self.apress)
404		self.default = Button(self.buttons, text="Revert",
405		    command=self.rpress)
406		self.checkall = Button(self.buttons, text="Check All",
407		    command=self.cpress)
408		self.uncheckall = Button(self.buttons, text="Uncheck All",
409		    command=self.upress)
410		self.checkall.grid(row=0, column=0, sticky=W)
411		self.uncheckall.grid(row=0, column=1, sticky=W)
412		self.apply.grid(row=0, column=2, sticky=W)
413		self.default.grid(row=0, column=3, sticky=W)
414		self.buttons.columnconfigure(0, weight=1)
415		self.buttons.columnconfigure(1, weight=1)
416		self.buttons.columnconfigure(2, weight=1)
417		self.buttons.columnconfigure(3, weight=1)
418
419	def apress(self):
420		disable_sources = []
421		enable_sources = []
422		for item in self.sconfig:
423			if (item.changed() == 0):
424				continue
425			if (item.enabled.get() == 1):
426				enable_sources.append(item.source)
427			else:
428				disable_sources.append(item.source)
429
430		if (len(disable_sources)):
431			graph.sourcehidelist(disable_sources)
432		if (len(enable_sources)):
433			graph.sourceshowlist(enable_sources)
434
435		for item in self.sconfig:
436			item.apply()
437
438	def rpress(self):
439		for item in self.sconfig:
440			item.revert()
441
442	def cpress(self):
443		for item in self.sconfig:
444			item.check()
445
446	def upress(self):
447		for item in self.sconfig:
448			item.uncheck()
449
450class SourceStats(Toplevel):
451	def __init__(self, source):
452		self.source = source
453		Toplevel.__init__(self)
454		self.resizable(0, 0)
455		self.title(source.name + " statistics")
456		self.evframe = LabelFrame(self,
457		    text="Event Count, Duration, Avg Duration")
458		self.evframe.grid(row=0, column=0, sticky=E+W)
459		eventtypes={}
460		for event in self.source.events:
461			if (event.type == "pad"):
462				continue
463			duration = event.duration
464			if (event.name in eventtypes):
465				(c, d) = eventtypes[event.name]
466				c += 1
467				d += duration
468				eventtypes[event.name] = (c, d)
469			else:
470				eventtypes[event.name] = (1, duration)
471		events = []
472		for k, v in eventtypes.iteritems():
473			(c, d) = v
474			events.append((k, c, d))
475		events.sort(key=itemgetter(1), reverse=True)
476
477		ypos = 0
478		for event in events:
479			(name, c, d) = event
480			Label(self.evframe, text=name, bd=1,
481			    relief=SUNKEN, anchor=W, width=30).grid(
482			    row=ypos, column=0, sticky=W+E)
483			Label(self.evframe, text=str(c), bd=1,
484			    relief=SUNKEN, anchor=W, width=10).grid(
485			    row=ypos, column=1, sticky=W+E)
486			Label(self.evframe, text=ticks2sec(d),
487			    bd=1, relief=SUNKEN, width=10).grid(
488			    row=ypos, column=2, sticky=W+E)
489			if (d and c):
490				d /= c
491			else:
492				d = 0
493			Label(self.evframe, text=ticks2sec(d),
494			    bd=1, relief=SUNKEN, width=10).grid(
495			    row=ypos, column=3, sticky=W+E)
496			ypos += 1
497		self.bind("<Control-w>", self.destroycb)
498
499	def destroycb(self, event):
500		self.destroy()
501
502
503class SourceContext(Menu):
504	def __init__(self, event, source):
505		self.source = source
506		Menu.__init__(self, tearoff=0, takefocus=0)
507		self.add_command(label="hide", command=self.hide)
508		self.add_command(label="hide group", command=self.hidegroup)
509		self.add_command(label="stats", command=self.stats)
510		self.tk_popup(event.x_root-3, event.y_root+3)
511
512	def hide(self):
513		graph.sourcehide(self.source)
514
515	def hidegroup(self):
516		grouplist = []
517		for source in sources:
518			if (source.group == self.source.group):
519				grouplist.append(source)
520		graph.sourcehidelist(grouplist)
521
522	def show(self):
523		graph.sourceshow(self.source)
524
525	def stats(self):
526		SourceStats(self.source)
527
528class EventView(Toplevel):
529	def __init__(self, event, canvas):
530		Toplevel.__init__(self)
531		self.resizable(0, 0)
532		self.title("Event")
533		self.event = event
534		self.buttons = Frame(self)
535		self.buttons.grid(row=0, column=0, sticky=E+W)
536		self.frame = Frame(self)
537		self.frame.grid(row=1, column=0, sticky=N+S+E+W)
538		self.canvas = canvas
539		self.drawlabels()
540		self.drawbuttons()
541		event.displayref(canvas)
542		self.bind("<Destroy>", self.destroycb)
543		self.bind("<Control-w>", self.destroycb)
544
545	def destroycb(self, event):
546		self.unbind("<Destroy>")
547		if (self.event != None):
548			self.event.displayunref(self.canvas)
549			self.event = None
550		self.destroy()
551
552	def clearlabels(self):
553		for label in self.frame.grid_slaves():
554			label.grid_remove()
555
556	def drawlabels(self):
557		ypos = 0
558		labels = self.event.labels()
559		while (len(labels) < 7):
560			labels.append(("", ""))
561		for label in labels:
562			name, value = label
563			linked = 0
564			if (name == "linkedto"):
565				linked = 1
566			l = Label(self.frame, text=name, bd=1, width=15,
567			    relief=SUNKEN, anchor=W)
568			if (linked):
569				fgcolor = "blue"
570			else:
571				fgcolor = "black"
572			r = Label(self.frame, text=value, bd=1,
573			    relief=SUNKEN, anchor=W, fg=fgcolor)
574			l.grid(row=ypos, column=0, sticky=E+W)
575			r.grid(row=ypos, column=1, sticky=E+W)
576			if (linked):
577				r.bind("<Button-1>", self.linkpress)
578			ypos += 1
579		self.frame.columnconfigure(1, minsize=80)
580
581	def drawbuttons(self):
582		self.back = Button(self.buttons, text="<", command=self.bpress)
583		self.forw = Button(self.buttons, text=">", command=self.fpress)
584		self.new = Button(self.buttons, text="new", command=self.npress)
585		self.back.grid(row=0, column=0, sticky=E+W)
586		self.forw.grid(row=0, column=1, sticky=E+W)
587		self.new.grid(row=0, column=2, sticky=E+W)
588		self.buttons.columnconfigure(2, weight=1)
589
590	def newevent(self, event):
591		self.event.displayunref(self.canvas)
592		self.clearlabels()
593		self.event = event
594		self.event.displayref(self.canvas)
595		self.drawlabels()
596
597	def npress(self):
598		EventView(self.event, self.canvas)
599
600	def bpress(self):
601		prev = self.event.prev()
602		if (prev == None):
603			return
604		while (prev.type == "pad"):
605			prev = prev.prev()
606			if (prev == None):
607				return
608		self.newevent(prev)
609
610	def fpress(self):
611		next = self.event.next()
612		if (next == None):
613			return
614		while (next.type == "pad"):
615			next = next.next()
616			if (next == None):
617				return
618		self.newevent(next)
619
620	def linkpress(self, wevent):
621		event = self.event.getlinked()
622		if (event != None):
623			self.newevent(event)
624
625class Event:
626	def __init__(self, source, name, cpu, timestamp, attrs):
627		self.source = source
628		self.name = name
629		self.cpu = cpu
630		self.timestamp = int(timestamp)
631		self.attrs = attrs
632		self.idx = None
633		self.item = None
634		self.dispcnt = 0
635		self.duration = 0
636		self.recno = lineno
637
638	def status(self):
639		statstr = self.name + " " + self.source.name
640		statstr += " on: cpu" + str(self.cpu)
641		statstr += " at: " + str(self.timestamp)
642		statstr += " attributes: "
643		for i in range(0, len(self.attrs)):
644			attr = self.attrs[i]
645			statstr += attr[0] + ": " + str(attr[1])
646			if (i != len(self.attrs) - 1):
647				statstr += ", "
648		status.set(statstr)
649
650	def labels(self):
651		return [("Source", self.source.name),
652			("Event", self.name),
653			("CPU", self.cpu),
654			("Timestamp", self.timestamp),
655			("KTR Line ", self.recno)
656		] + self.attrs
657
658	def mouseenter(self, canvas):
659		self.displayref(canvas)
660		self.status()
661
662	def mouseexit(self, canvas):
663		self.displayunref(canvas)
664		status.clear()
665
666	def mousepress(self, canvas):
667		EventView(self, canvas)
668
669	def draw(self, canvas, xpos, ypos, item):
670		self.item = item
671		if (item != None):
672			canvas.items[item] = self
673
674	def move(self, canvas, x, y):
675		if (self.item == None):
676			return;
677		canvas.move(self.item, x, y);
678
679	def next(self):
680		return self.source.eventat(self.idx + 1)
681
682	def nexttype(self, type):
683		next = self.next()
684		while (next != None and next.type != type):
685			next = next.next()
686		return (next)
687
688	def prev(self):
689		return self.source.eventat(self.idx - 1)
690
691	def displayref(self, canvas):
692		if (self.dispcnt == 0):
693			canvas.itemconfigure(self.item, width=2)
694		self.dispcnt += 1
695
696	def displayunref(self, canvas):
697		self.dispcnt -= 1
698		if (self.dispcnt == 0):
699			canvas.itemconfigure(self.item, width=0)
700			canvas.tag_raise("point", "state")
701
702	def getlinked(self):
703		for attr in self.attrs:
704			if (attr[0] != "linkedto"):
705				continue
706			source = ktrfile.findid(attr[1])
707			return source.findevent(self.timestamp)
708		return None
709
710class PointEvent(Event):
711	type = "point"
712	def __init__(self, source, name, cpu, timestamp, attrs):
713		Event.__init__(self, source, name, cpu, timestamp, attrs)
714
715	def draw(self, canvas, xpos, ypos):
716		color = colormap.lookup(self.name)
717		l = canvas.create_oval(xpos - XY_POINT, ypos,
718		    xpos + XY_POINT, ypos - (XY_POINT * 2),
719		    fill=color, width=0,
720		    tags=("event", self.type, self.name, self.source.tag))
721		Event.draw(self, canvas, xpos, ypos, l)
722
723		return xpos
724
725class StateEvent(Event):
726	type = "state"
727	def __init__(self, source, name, cpu, timestamp, attrs):
728		Event.__init__(self, source, name, cpu, timestamp, attrs)
729
730	def draw(self, canvas, xpos, ypos):
731		next = self.nexttype("state")
732		if (next == None):
733			return (xpos)
734		self.duration = duration = next.timestamp - self.timestamp
735		self.attrs.insert(0, ("duration", ticks2sec(duration)))
736		color = colormap.lookup(self.name)
737		if (duration < 0):
738			duration = 0
739			print("Unsynchronized timestamp")
740			print(self.cpu, self.timestamp)
741			print(next.cpu, next.timestamp)
742		delta = duration / canvas.ratio
743		l = canvas.create_rectangle(xpos, ypos,
744		    xpos + delta, ypos - 10, fill=color, width=0,
745		    tags=("event", self.type, self.name, self.source.tag))
746		Event.draw(self, canvas, xpos, ypos, l)
747
748		return (xpos + delta)
749
750class CountEvent(Event):
751	type = "count"
752	def __init__(self, source, count, cpu, timestamp, attrs):
753		count = int(count)
754		self.count = count
755		Event.__init__(self, source, "count", cpu, timestamp, attrs)
756
757	def draw(self, canvas, xpos, ypos):
758		next = self.nexttype("count")
759		if (next == None):
760			return (xpos)
761		color = colormap.lookup("count")
762		self.duration = duration = next.timestamp - self.timestamp
763		if (duration < 0):
764			duration = 0
765			print("Unsynchronized timestamp")
766			print(self.cpu, self.timestamp)
767			print(next.cpu, next.timestamp)
768		self.attrs.insert(0, ("count", self.count))
769		self.attrs.insert(1, ("duration", ticks2sec(duration)))
770		delta = duration / canvas.ratio
771		yhight = self.source.yscale() * self.count
772		l = canvas.create_rectangle(xpos, ypos - yhight,
773		    xpos + delta, ypos, fill=color, width=0,
774		    tags=("event", self.type, self.name, self.source.tag))
775		Event.draw(self, canvas, xpos, ypos, l)
776		return (xpos + delta)
777
778class PadEvent(StateEvent):
779	type = "pad"
780	def __init__(self, source, cpu, timestamp, last=0):
781		if (last):
782			cpu = source.events[len(source.events) -1].cpu
783		else:
784			cpu = source.events[0].cpu
785		StateEvent.__init__(self, source, "pad", cpu, timestamp, [])
786	def draw(self, canvas, xpos, ypos):
787		next = self.next()
788		if (next == None):
789			return (xpos)
790		duration = next.timestamp - self.timestamp
791		delta = duration / canvas.ratio
792		Event.draw(self, canvas, xpos, ypos, None)
793		return (xpos + delta)
794
795
796@total_ordering
797class EventSource:
798	def __init__(self, group, id):
799		self.name = id
800		self.events = []
801		self.cpuitems = []
802		self.group = group
803		self.y = 0
804		self.item = None
805		self.hidden = 0
806		self.tag = group + id
807
808	def __lt__(self, other):
809		if other is None:
810			return False
811		return (self.group < other.group or
812				self.group == other.group and self.name < other.name)
813
814	def __eq__(self, other):
815		if other is None:
816			return False
817		return self.group == other.group and self.name == other.name
818
819	# It is much faster to append items to a list then to insert them
820	# at the beginning.  As a result, we add events in reverse order
821	# and then swap the list during fixup.
822	def fixup(self):
823		self.events.reverse()
824
825	def addevent(self, event):
826		self.events.append(event)
827
828	def addlastevent(self, event):
829		self.events.insert(0, event)
830
831	def draw(self, canvas, ypos):
832		xpos = 10
833		cpux = 10
834		cpu = self.events[1].cpu
835		for i in range(0, len(self.events)):
836			self.events[i].idx = i
837		for event in self.events:
838			if (event.cpu != cpu and event.cpu != -1):
839				self.drawcpu(canvas, cpu, cpux, xpos, ypos)
840				cpux = xpos
841				cpu = event.cpu
842			xpos = event.draw(canvas, xpos, ypos)
843		self.drawcpu(canvas, cpu, cpux, xpos, ypos)
844
845	def drawname(self, canvas, ypos):
846		self.y = ypos
847		ypos = ypos - (self.ysize() / 2)
848		self.item = canvas.create_text(X_BORDER, ypos, anchor="w",
849		    text=self.name)
850		return (self.item)
851
852	def drawcpu(self, canvas, cpu, fromx, tox, ypos):
853		cpu = "CPU " + str(cpu)
854		color = cpucolormap.lookup(cpu)
855		# Create the cpu background colors default to hidden
856		l = canvas.create_rectangle(fromx,
857		    ypos - self.ysize() - canvas.bdheight,
858		    tox, ypos + canvas.bdheight, fill=color, width=0,
859		    tags=("cpubg", cpu, self.tag), state="hidden")
860		self.cpuitems.append(l)
861
862	def move(self, canvas, xpos, ypos):
863		canvas.move(self.tag, xpos, ypos)
864
865	def movename(self, canvas, xpos, ypos):
866		self.y += ypos
867		canvas.move(self.item, xpos, ypos)
868
869	def ysize(self):
870		return (Y_EVENTSOURCE)
871
872	def eventat(self, i):
873		if (i >= len(self.events) or i < 0):
874			return (None)
875		event = self.events[i]
876		return (event)
877
878	def findevent(self, timestamp):
879		for event in self.events:
880			if (event.timestamp >= timestamp and event.type != "pad"):
881				return (event)
882		return (None)
883
884class Counter(EventSource):
885	#
886	# Store a hash of counter groups that keeps the max value
887	# for a counter in this group for scaling purposes.
888	#
889	groups = {}
890	def __init__(self, group, id):
891		try:
892			Counter.cnt = Counter.groups[group]
893		except:
894			Counter.groups[group] = 0
895		EventSource.__init__(self, group, id)
896
897	def fixup(self):
898		for event in self.events:
899			if (event.type != "count"):
900				continue;
901			count = int(event.count)
902			if (count > Counter.groups[self.group]):
903				Counter.groups[self.group] = count
904		EventSource.fixup(self)
905
906	def ymax(self):
907		return (Counter.groups[self.group])
908
909	def ysize(self):
910		return (Y_COUNTER)
911
912	def yscale(self):
913		return (self.ysize() / self.ymax())
914
915class KTRFile:
916	def __init__(self, file):
917		self.timestamp_f = None
918		self.timestamp_l = None
919		self.locks = {}
920		self.ticks = {}
921		self.load = {}
922		self.crit = {}
923		self.stathz = 0
924		self.eventcnt = 0
925		self.taghash = {}
926
927		self.parse(file)
928		self.fixup()
929		global ticksps
930		ticksps = self.ticksps()
931		span = self.timespan()
932		ghz = float(ticksps) / 1000000000.0
933		#
934		# Update the title with some stats from the file
935		#
936		titlestr = "SchedGraph: "
937		titlestr += ticks2sec(span) + " at %.3f ghz, " % ghz
938		titlestr += str(len(sources)) + " event sources, "
939		titlestr += str(self.eventcnt) + " events"
940		root.title(titlestr)
941
942	def parse(self, file):
943		try:
944			ifp = open(file)
945		except:
946			print("Can't open", file)
947			sys.exit(1)
948
949		# quoteexp matches a quoted string, no escaping
950		quoteexp = "\"([^\"]*)\""
951
952		#
953		# commaexp matches a quoted string OR the string up
954		# to the first ','
955		#
956		commaexp = "(?:" + quoteexp + "|([^,]+))"
957
958		#
959		# colonstr matches a quoted string OR the string up
960		# to the first ':'
961		#
962		colonexp = "(?:" + quoteexp + "|([^:]+))"
963
964		#
965		# Match various manditory parts of the KTR string this is
966		# fairly inflexible until you get to attributes to make
967		# parsing faster.
968		#
969		hdrexp = "\s*(\d+)\s+(\d+)\s+(\d+)\s+"
970		groupexp = "KTRGRAPH group:" + quoteexp + ", "
971		idexp = "id:" + quoteexp + ", "
972		typeexp = "([^:]+):" + commaexp + ", "
973		attribexp = "attributes: (.*)"
974
975		#
976		# Matches optional attributes in the KTR string.  This
977		# tolerates more variance as the users supply these values.
978		#
979		attrexp = colonexp + "\s*:\s*(?:" + commaexp + ", (.*)|"
980		attrexp += quoteexp +"|(.*))"
981
982		# Precompile regexp
983		ktrre = re.compile(hdrexp + groupexp + idexp + typeexp + attribexp)
984		attrre = re.compile(attrexp)
985
986		global lineno
987		lineno = 0
988		for line in ifp.readlines():
989			lineno += 1
990			if ((lineno % 2048) == 0):
991				status.startup("Parsing line " + str(lineno))
992			m = ktrre.match(line);
993			if (m == None):
994				print("Can't parse", lineno, line, end=' ')
995				continue;
996			(index, cpu, timestamp, group, id, type, dat, dat1, attrstring) = m.groups();
997			if (dat == None):
998				dat = dat1
999			if (self.checkstamp(timestamp) == 0):
1000				print("Bad timestamp at", lineno, ":", end=' ')
1001				print(cpu, timestamp)
1002				continue
1003			#
1004			# Build the table of optional attributes
1005			#
1006			attrs = []
1007			while (attrstring != None):
1008				m = attrre.match(attrstring.strip())
1009				if (m == None):
1010					break;
1011				#
1012				# Name may or may not be quoted.
1013				#
1014				# For val we have four cases:
1015				# 1) quotes followed by comma and more
1016				#    attributes.
1017				# 2) no quotes followed by comma and more
1018				#    attributes.
1019				# 3) no more attributes or comma with quotes.
1020				# 4) no more attributes or comma without quotes.
1021				#
1022				(name, name1, val, val1, attrstring, end, end1) = m.groups();
1023				if (name == None):
1024					name = name1
1025				if (end == None):
1026					end = end1
1027				if (val == None):
1028					val = val1
1029				if (val == None):
1030					val = end
1031				if (name == "stathz"):
1032					self.setstathz(val, cpu)
1033				attrs.append((name, val))
1034			args = (dat, cpu, timestamp, attrs)
1035			e = self.makeevent(group, id, type, args)
1036			if (e == None):
1037				print("Unknown type", type, lineno, line, end=' ')
1038
1039	def makeevent(self, group, id, type, args):
1040		e = None
1041		source = self.makeid(group, id, type)
1042		if (type == "state"):
1043			e = StateEvent(source, *args)
1044		elif (type == "counter"):
1045			e = CountEvent(source, *args)
1046		elif (type == "point"):
1047			e = PointEvent(source, *args)
1048		if (e != None):
1049			self.eventcnt += 1
1050			source.addevent(e);
1051		return e
1052
1053	def setstathz(self, val, cpu):
1054		self.stathz = int(val)
1055		cpu = int(cpu)
1056		try:
1057			ticks = self.ticks[cpu]
1058		except:
1059			self.ticks[cpu] = 0
1060		self.ticks[cpu] += 1
1061
1062	def checkstamp(self, timestamp):
1063		timestamp = int(timestamp)
1064		if (self.timestamp_f == None):
1065			self.timestamp_f = timestamp;
1066		if (self.timestamp_l != None and
1067		    timestamp -2048> self.timestamp_l):
1068			return (0)
1069		self.timestamp_l = timestamp;
1070		return (1)
1071
1072	def makeid(self, group, id, type):
1073		tag = group + id
1074		if (tag in self.taghash):
1075			return self.taghash[tag]
1076		if (type == "counter"):
1077			source = Counter(group, id)
1078		else:
1079			source = EventSource(group, id)
1080		sources.append(source)
1081		self.taghash[tag] = source
1082		return (source)
1083
1084	def findid(self, id):
1085		for source in sources:
1086			if (source.name == id):
1087				return source
1088		return (None)
1089
1090	def timespan(self):
1091		return (self.timestamp_f - self.timestamp_l);
1092
1093	def ticksps(self):
1094		oneghz = 1000000000
1095		# Use user supplied clock first
1096		if (clockfreq != None):
1097			return int(clockfreq * oneghz)
1098
1099		# Check for a discovered clock
1100		if (self.stathz != 0):
1101			return (self.timespan() / self.ticks[0]) * int(self.stathz)
1102		# Pretend we have a 1ns clock
1103		print("WARNING: No clock discovered and no frequency ", end=' ')
1104		print("specified via the command line.")
1105		print("Using fake 1ghz clock")
1106		return (oneghz);
1107
1108	def fixup(self):
1109		for source in sources:
1110			e = PadEvent(source, -1, self.timestamp_l)
1111			source.addevent(e)
1112			e = PadEvent(source, -1, self.timestamp_f, last=1)
1113			source.addlastevent(e)
1114			source.fixup()
1115		sources.sort()
1116
1117class SchedNames(Canvas):
1118	def __init__(self, master, display):
1119		self.display = display
1120		self.parent = master
1121		self.bdheight = master.bdheight
1122		self.items = {}
1123		self.ysize = 0
1124		self.lines = []
1125		Canvas.__init__(self, master, width=120,
1126		    height=display["height"], bg='grey',
1127		    scrollregion=(0, 0, 50, 100))
1128
1129	def moveline(self, cur_y, y):
1130		for line in self.lines:
1131			(x0, y0, x1, y1) = self.coords(line)
1132			if (cur_y != y0):
1133				continue
1134			self.move(line, 0, y)
1135			return
1136
1137	def draw(self):
1138		status.startup("Drawing names")
1139		ypos = 0
1140		self.configure(scrollregion=(0, 0,
1141		    self["width"], self.display.ysize()))
1142		for source in sources:
1143			l = self.create_line(0, ypos, self["width"], ypos,
1144			    width=1, fill="black", tags=("all","sources"))
1145			self.lines.append(l)
1146			ypos += self.bdheight
1147			ypos += source.ysize()
1148			t = source.drawname(self, ypos)
1149			self.items[t] = source
1150			ypos += self.bdheight
1151		self.ysize = ypos
1152		self.create_line(0, ypos, self["width"], ypos,
1153		    width=1, fill="black", tags=("all",))
1154		self.bind("<Button-1>", self.master.mousepress);
1155		self.bind("<Button-3>", self.master.mousepressright);
1156		self.bind("<ButtonRelease-1>", self.master.mouserelease);
1157		self.bind("<B1-Motion>", self.master.mousemotion);
1158
1159	def updatescroll(self):
1160		self.configure(scrollregion=(0, 0,
1161		    self["width"], self.display.ysize()))
1162
1163
1164class SchedDisplay(Canvas):
1165	def __init__(self, master):
1166		self.ratio = 1
1167		self.parent = master
1168		self.bdheight = master.bdheight
1169		self.items = {}
1170		self.lines = []
1171		Canvas.__init__(self, master, width=800, height=500, bg='grey',
1172		     scrollregion=(0, 0, 800, 500))
1173
1174	def prepare(self):
1175		#
1176		# Compute a ratio to ensure that the file's timespan fits into
1177		# 2^31.  Although python may handle larger values for X
1178		# values, the Tk internals do not.
1179		#
1180		self.ratio = (ktrfile.timespan() - 1) / 2**31 + 1
1181
1182	def draw(self):
1183		ypos = 0
1184		xsize = self.xsize()
1185		for source in sources:
1186			status.startup("Drawing " + source.name)
1187			l = self.create_line(0, ypos, xsize, ypos,
1188			    width=1, fill="black", tags=("all",))
1189			self.lines.append(l)
1190			ypos += self.bdheight
1191			ypos += source.ysize()
1192			source.draw(self, ypos)
1193			ypos += self.bdheight
1194		self.tag_raise("point", "state")
1195		self.tag_lower("cpubg", ALL)
1196		self.create_line(0, ypos, xsize, ypos,
1197		    width=1, fill="black", tags=("lines",))
1198		self.tag_bind("event", "<Enter>", self.mouseenter)
1199		self.tag_bind("event", "<Leave>", self.mouseexit)
1200		self.bind("<Button-1>", self.mousepress)
1201		self.bind("<Button-3>", self.master.mousepressright);
1202		self.bind("<Button-4>", self.wheelup)
1203		self.bind("<Button-5>", self.wheeldown)
1204		self.bind("<ButtonRelease-1>", self.master.mouserelease);
1205		self.bind("<B1-Motion>", self.master.mousemotion);
1206
1207	def moveline(self, cur_y, y):
1208		for line in self.lines:
1209			(x0, y0, x1, y1) = self.coords(line)
1210			if (cur_y != y0):
1211				continue
1212			self.move(line, 0, y)
1213			return
1214
1215	def mouseenter(self, event):
1216		item, = self.find_withtag(CURRENT)
1217		self.items[item].mouseenter(self)
1218
1219	def mouseexit(self, event):
1220		item, = self.find_withtag(CURRENT)
1221		self.items[item].mouseexit(self)
1222
1223	def mousepress(self, event):
1224		# Find out what's beneath us
1225		items = self.find_withtag(CURRENT)
1226		if (len(items) == 0):
1227			self.master.mousepress(event)
1228			return
1229		# Only grab mouse presses for things with event tags.
1230		item = items[0]
1231		tags = self.gettags(item)
1232		for tag in tags:
1233			if (tag == "event"):
1234				self.items[item].mousepress(self)
1235				return
1236		# Leave the rest to the master window
1237		self.master.mousepress(event)
1238
1239	def wheeldown(self, event):
1240		self.parent.display_yview("scroll", 1, "units")
1241
1242	def wheelup(self, event):
1243		self.parent.display_yview("scroll", -1, "units")
1244
1245	def xsize(self):
1246		return ((ktrfile.timespan() / self.ratio) + (X_BORDER * 2))
1247
1248	def ysize(self):
1249		ysize = 0
1250		for source in sources:
1251			if (source.hidden == 1):
1252				continue
1253			ysize += self.parent.sourcesize(source)
1254		return ysize
1255
1256	def scaleset(self, ratio):
1257		if (ktrfile == None):
1258			return
1259		oldratio = self.ratio
1260		xstart, xend = self.xview()
1261		midpoint = xstart + ((xend - xstart) / 2)
1262
1263		self.ratio = ratio
1264		self.updatescroll()
1265		self.scale(ALL, 0, 0, float(oldratio) / ratio, 1)
1266
1267		xstart, xend = self.xview()
1268		xsize = (xend - xstart) / 2
1269		self.xview_moveto(midpoint - xsize)
1270
1271	def updatescroll(self):
1272		self.configure(scrollregion=(0, 0, self.xsize(), self.ysize()))
1273
1274	def scaleget(self):
1275		return self.ratio
1276
1277	def getcolor(self, tag):
1278		return self.itemcget(tag, "fill")
1279
1280	def getstate(self, tag):
1281		return self.itemcget(tag, "state")
1282
1283	def setcolor(self, tag, color):
1284		self.itemconfigure(tag, state="normal", fill=color)
1285
1286	def hide(self, tag):
1287		self.itemconfigure(tag, state="hidden")
1288
1289class GraphMenu(Frame):
1290	def __init__(self, master):
1291		Frame.__init__(self, master, bd=2, relief=RAISED)
1292		self.conf = Menubutton(self, text="Configure")
1293		self.confmenu = Menu(self.conf, tearoff=0)
1294		self.confmenu.add_command(label="Event Colors",
1295		    command=self.econf)
1296		self.confmenu.add_command(label="CPU Colors",
1297		    command=self.cconf)
1298		self.confmenu.add_command(label="Source Configure",
1299		    command=self.sconf)
1300		self.conf["menu"] = self.confmenu
1301		self.conf.pack(side=LEFT)
1302
1303	def econf(self):
1304		ColorConfigure(eventcolors, "Event Display Configuration")
1305
1306	def cconf(self):
1307		ColorConfigure(cpucolors, "CPU Background Colors")
1308
1309	def sconf(self):
1310		SourceConfigure()
1311
1312class SchedGraph(Frame):
1313	def __init__(self, master):
1314		Frame.__init__(self, master)
1315		self.menu = None
1316		self.names = None
1317		self.display = None
1318		self.scale = None
1319		self.status = None
1320		self.bdheight = Y_BORDER
1321		self.clicksource = None
1322		self.lastsource = None
1323		self.pack(expand=1, fill="both")
1324		self.buildwidgets()
1325		self.layout()
1326		self.bind_all("<Control-q>", self.quitcb)
1327
1328	def quitcb(self, event):
1329		self.quit()
1330
1331	def buildwidgets(self):
1332		global status
1333		self.menu = GraphMenu(self)
1334		self.display = SchedDisplay(self)
1335		self.names = SchedNames(self, self.display)
1336		self.scale = Scaler(self, self.display)
1337		status = self.status = Status(self)
1338		self.scrollY = Scrollbar(self, orient="vertical",
1339		    command=self.display_yview)
1340		self.display.scrollX = Scrollbar(self, orient="horizontal",
1341		    command=self.display.xview)
1342		self.display["xscrollcommand"] = self.display.scrollX.set
1343		self.display["yscrollcommand"] = self.scrollY.set
1344		self.names["yscrollcommand"] = self.scrollY.set
1345
1346	def layout(self):
1347		self.columnconfigure(1, weight=1)
1348		self.rowconfigure(1, weight=1)
1349		self.menu.grid(row=0, column=0, columnspan=3, sticky=E+W)
1350		self.names.grid(row=1, column=0, sticky=N+S)
1351		self.display.grid(row=1, column=1, sticky=W+E+N+S)
1352		self.scrollY.grid(row=1, column=2, sticky=N+S)
1353		self.display.scrollX.grid(row=2, column=0, columnspan=2,
1354		    sticky=E+W)
1355		self.scale.grid(row=3, column=0, columnspan=3, sticky=E+W)
1356		self.status.grid(row=4, column=0, columnspan=3, sticky=E+W)
1357
1358	def draw(self):
1359		self.master.update()
1360		self.display.prepare()
1361		self.names.draw()
1362		self.display.draw()
1363		self.status.startup("")
1364		#
1365		# Configure scale related values
1366		#
1367		scalemax = ktrfile.timespan() / int(self.display["width"])
1368		width = int(root.geometry().split('x')[0])
1369		self.constwidth = width - int(self.display["width"])
1370		self.scale.setmax(scalemax)
1371		self.scale.set(scalemax)
1372		self.display.xview_moveto(0)
1373		self.bind("<Configure>", self.resize)
1374
1375	def mousepress(self, event):
1376		self.clicksource = self.sourceat(event.y)
1377
1378	def mousepressright(self, event):
1379		source = self.sourceat(event.y)
1380		if (source == None):
1381			return
1382		SourceContext(event, source)
1383
1384	def mouserelease(self, event):
1385		if (self.clicksource == None):
1386			return
1387		newsource = self.sourceat(event.y)
1388		if (self.clicksource != newsource):
1389			self.sourceswap(self.clicksource, newsource)
1390		self.clicksource = None
1391		self.lastsource = None
1392
1393	def mousemotion(self, event):
1394		if (self.clicksource == None):
1395			return
1396		newsource = self.sourceat(event.y)
1397		#
1398		# If we get a None source they moved off the page.
1399		# swapsource() can't handle moving multiple items so just
1400		# pretend we never clicked on anything to begin with so the
1401		# user can't mouseover a non-contiguous area.
1402		#
1403		if (newsource == None):
1404			self.clicksource = None
1405			self.lastsource = None
1406			return
1407		if (newsource == self.lastsource):
1408			return;
1409		self.lastsource = newsource
1410		if (newsource != self.clicksource):
1411			self.sourceswap(self.clicksource, newsource)
1412
1413	# These are here because this object controls layout
1414	def sourcestart(self, source):
1415		return source.y - self.bdheight - source.ysize()
1416
1417	def sourceend(self, source):
1418		return source.y + self.bdheight
1419
1420	def sourcesize(self, source):
1421		return (self.bdheight * 2) + source.ysize()
1422
1423	def sourceswap(self, source1, source2):
1424		# Sort so we always know which one is on top.
1425		if (source2.y < source1.y):
1426			swap = source1
1427			source1 = source2
1428			source2 = swap
1429		# Only swap adjacent sources
1430		if (self.sourceend(source1) != self.sourcestart(source2)):
1431			return
1432		# Compute start coordinates and target coordinates
1433		y1 = self.sourcestart(source1)
1434		y2 = self.sourcestart(source2)
1435		y1targ = y1 + self.sourcesize(source2)
1436		y2targ = y1
1437		#
1438		# If the sizes are not equal, adjust the start of the lower
1439		# source to account for the lost/gained space.
1440		#
1441		if (source1.ysize() != source2.ysize()):
1442			diff = source2.ysize() - source1.ysize()
1443			self.names.moveline(y2, diff);
1444			self.display.moveline(y2, diff)
1445		source1.move(self.display, 0, y1targ - y1)
1446		source2.move(self.display, 0, y2targ - y2)
1447		source1.movename(self.names, 0, y1targ - y1)
1448		source2.movename(self.names, 0, y2targ - y2)
1449
1450	def sourcepicky(self, source):
1451		if (source.hidden == 0):
1452			return self.sourcestart(source)
1453		# Revert to group based sort
1454		sources.sort()
1455		prev = None
1456		for s in sources:
1457			if (s == source):
1458				break
1459			if (s.hidden == 0):
1460				prev = s
1461		if (prev == None):
1462			newy = 0
1463		else:
1464			newy = self.sourcestart(prev) + self.sourcesize(prev)
1465		return newy
1466
1467	def sourceshow(self, source):
1468		if (source.hidden == 0):
1469			return;
1470		newy = self.sourcepicky(source)
1471		off = newy - self.sourcestart(source)
1472		self.sourceshiftall(newy-1, self.sourcesize(source))
1473		self.sourceshift(source, off)
1474		source.hidden = 0
1475
1476	#
1477	# Optimized source show of multiple entries that only moves each
1478	# existing entry once.  Doing sourceshow() iteratively is too
1479	# expensive due to python's canvas.move().
1480	#
1481	def sourceshowlist(self, srclist):
1482		srclist.sort(key=attrgetter('y'))
1483		startsize = []
1484		for source in srclist:
1485			if (source.hidden == 0):
1486				srclist.remove(source)
1487			startsize.append((self.sourcepicky(source),
1488			    self.sourcesize(source)))
1489
1490		sources.sort(key=attrgetter('y'), reverse=True)
1491		self.status.startup("Updating display...");
1492		for source in sources:
1493			if (source.hidden == 1):
1494				continue
1495			nstart = self.sourcestart(source)
1496			size = 0
1497			for hidden in startsize:
1498				(start, sz) = hidden
1499				if (start <= nstart or start+sz <= nstart):
1500					size += sz
1501			self.sourceshift(source, size)
1502		idx = 0
1503		size = 0
1504		for source in srclist:
1505			(newy, sz) = startsize[idx]
1506			off = (newy + size) - self.sourcestart(source)
1507			self.sourceshift(source, off)
1508			source.hidden = 0
1509			size += sz
1510			idx += 1
1511		self.updatescroll()
1512		self.status.set("")
1513
1514	#
1515	# Optimized source hide of multiple entries that only moves each
1516	# remaining entry once.  Doing sourcehide() iteratively is too
1517	# expensive due to python's canvas.move().
1518	#
1519	def sourcehidelist(self, srclist):
1520		srclist.sort(key=attrgetter('y'))
1521		sources.sort(key=attrgetter('y'))
1522		startsize = []
1523		off = len(sources) * 100
1524		self.status.startup("Updating display...");
1525		for source in srclist:
1526			if (source.hidden == 1):
1527				srclist.remove(source)
1528			#
1529			# Remember our old position so we can sort things
1530			# below us when we're done.
1531			#
1532			startsize.append((self.sourcestart(source),
1533			    self.sourcesize(source)))
1534			self.sourceshift(source, off)
1535			source.hidden = 1
1536
1537		idx = 0
1538		size = 0
1539		for hidden in startsize:
1540			(start, sz) = hidden
1541			size += sz
1542			if (idx + 1 < len(startsize)):
1543				(stop, sz) = startsize[idx+1]
1544			else:
1545				stop = self.display.ysize()
1546			idx += 1
1547			for source in sources:
1548				nstart = self.sourcestart(source)
1549				if (nstart < start or source.hidden == 1):
1550					continue
1551				if (nstart >= stop):
1552					break;
1553				self.sourceshift(source, -size)
1554		self.updatescroll()
1555		self.status.set("")
1556
1557	def sourcehide(self, source):
1558		if (source.hidden == 1):
1559			return;
1560		# Move it out of the visible area
1561		off = len(sources) * 100
1562		start = self.sourcestart(source)
1563		self.sourceshift(source, off)
1564		self.sourceshiftall(start, -self.sourcesize(source))
1565		source.hidden = 1
1566
1567	def sourceshift(self, source, off):
1568		start = self.sourcestart(source)
1569		source.move(self.display, 0, off)
1570		source.movename(self.names, 0, off)
1571		self.names.moveline(start, off);
1572		self.display.moveline(start, off)
1573		#
1574		# We update the idle tasks to shrink the dirtied area so
1575		# it does not always include the entire screen.
1576		#
1577		self.names.update_idletasks()
1578		self.display.update_idletasks()
1579
1580	def sourceshiftall(self, start, off):
1581		self.status.startup("Updating display...");
1582		for source in sources:
1583			nstart = self.sourcestart(source)
1584			if (nstart < start):
1585				continue;
1586			self.sourceshift(source, off)
1587		self.updatescroll()
1588		self.status.set("")
1589
1590	def sourceat(self, ypos):
1591		(start, end) = self.names.yview()
1592		starty = start * float(self.names.ysize)
1593		ypos += starty
1594		for source in sources:
1595			if (source.hidden == 1):
1596				continue;
1597			yend = self.sourceend(source)
1598			ystart = self.sourcestart(source)
1599			if (ypos >= ystart and ypos <= yend):
1600				return source
1601		return None
1602
1603	def display_yview(self, *args):
1604		self.names.yview(*args)
1605		self.display.yview(*args)
1606
1607	def resize(self, *args):
1608		width = int(root.geometry().split('x')[0])
1609		scalemax = ktrfile.timespan() / (width - self.constwidth)
1610		self.scale.setmax(scalemax)
1611
1612	def updatescroll(self):
1613		self.names.updatescroll()
1614		self.display.updatescroll()
1615
1616	def setcolor(self, tag, color):
1617		self.display.setcolor(tag, color)
1618
1619	def hide(self, tag):
1620		self.display.hide(tag)
1621
1622	def getcolor(self, tag):
1623		return self.display.getcolor(tag)
1624
1625	def getstate(self, tag):
1626		return self.display.getstate(tag)
1627
1628if (len(sys.argv) != 2 and len(sys.argv) != 3):
1629	print("usage:", sys.argv[0], "<ktr file> [clock freq in ghz]")
1630	sys.exit(1)
1631
1632if (len(sys.argv) > 2):
1633	clockfreq = float(sys.argv[2])
1634
1635root = Tk()
1636root.title("SchedGraph")
1637colormap = Colormap(eventcolors)
1638cpucolormap = Colormap(cpucolors)
1639graph = SchedGraph(root)
1640ktrfile = KTRFile(sys.argv[1])
1641graph.draw()
1642root.mainloop()
1643