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