1 #!/usr/local/bin/python
3 # Copyright (c) 2002-2003, 2009, Jeffrey Roberson <jeff@freebsd.org>
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
9 # 1. Redistributions of source code must retain the above copyright
10 # notice unmodified, this list of conditions, and the following
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.
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.
35 # - Install the ports/x11-toolkits/py-tkinter package; e.g.
36 # portinstall x11-toolkits/py-tkinter package
37 # - Add KTR_SCHED to KTR_COMPILE and KTR_MASK in your KERNCONF; e.g.
39 # options KTR_ENTRIES=32768
40 # options KTR_COMPILE=(KTR_SCHED)
41 # options KTR_MASK=(KTR_SCHED)
42 # - It is encouraged to increase KTR_ENTRIES size to gather enough
43 # information for analysis; e.g.
44 # options KTR_ENTRIES=262144
45 # as 32768 entries may only correspond to a second or two of profiling
46 # data depending on your workload.
47 # - Rebuild kernel with proper changes to KERNCONF and boot new kernel.
48 # - Run your workload to be profiled.
49 # - While the workload is continuing (i.e. before it finishes), disable
50 # KTR tracing by setting 'sysctl debug.ktr.mask=0'. This is necessary
51 # to avoid a race condition while running ktrdump, i.e. the KTR ring buffer
52 # will cycle a bit while ktrdump runs, and this confuses schedgraph because
53 # the timestamps appear to go backwards at some point. Stopping KTR logging
54 # while the workload is still running is to avoid wasting log entries on
55 # "idle" time at the end.
56 # - Dump the trace to a file: 'ktrdump -ct > ktr.out'
57 # - Run the python script: 'python schedgraph.py ktr.out' optionally provide
58 # your cpu frequency in ghz: 'python schedgraph.py ktr.out 2.4'
61 # Add a per-source summary display
62 # "Vertical rule" to help relate data in different rows
63 # Mouse-over popup of full thread/event/row label (currently truncated)
64 # More visible anchors for popup event windows
66 # BUGS: 1) Only 8 CPUs are supported, more CPUs require more choices of
67 # colours to represent them ;-)
73 ("yielding", "yellow"),
74 ("swapped", "violet"),
75 ("suspended", "purple"),
78 ("blocked", "dark red"),
79 ("runq add", "yellow"),
80 ("runq rem", "yellow"),
81 ("thread exit", "grey"),
82 ("proc exit", "grey"),
83 ("callwheel idle", "grey"),
84 ("callout running", "green"),
85 ("lock acquire", "blue"),
86 ("lock contest", "purple"),
87 ("failed lock try", "red"),
88 ("lock release", "grey"),
89 ("statclock", "black"),
91 ("lend prio", "black"),
96 ("CPU 0", "light grey"),
97 ("CPU 1", "dark grey"),
98 ("CPU 2", "light blue"),
99 ("CPU 3", "light pink"),
100 ("CPU 4", "blanched almond"),
101 ("CPU 5", "slate grey"),
103 ("CPU 7", "thistle"),
108 "white", "thistle", "blanched almond", "tan", "chartreuse",
109 "dark red", "red", "pale violet red", "pink", "light pink",
110 "dark orange", "orange", "coral", "light coral",
111 "goldenrod", "gold", "yellow", "light yellow",
112 "dark green", "green", "light green", "light sea green",
113 "dark blue", "blue", "light blue", "steel blue", "light slate blue",
114 "dark violet", "violet", "purple", "blue violet",
115 "dark grey", "slate grey", "light grey",
135 def __init__(self, table):
139 self.map[entry[0]] = entry[1]
141 def lookup(self, name):
143 color = self.map[name]
145 color = colors[random.randrange(0, len(colors))]
146 print "Picking random color", color, "for", name
147 self.map[name] = color
148 self.table.append((name, color))
151 def ticks2sec(ticks):
153 ns = float(ticksps) / 1000000000
156 return ("%.2fns" % ticks)
159 return ("%.2fus" % ticks)
162 return ("%.2fms" % ticks)
164 return ("%.2fs" % ticks)
167 def __init__(self, master, target):
168 Frame.__init__(self, master)
171 self.label = Label(self, text="Ticks per pixel")
172 self.label.pack(side=LEFT)
173 self.resolution = 100
176 def scaleset(self, value):
177 self.target.scaleset(int(value))
179 def set(self, value):
180 self.scale.set(value)
182 def setmax(self, value):
184 # We can't reconfigure the to_ value so we delete the old
185 # window and make a new one when we resize.
187 if (self.scale != None):
188 self.scale.pack_forget()
190 self.scale = Scale(self, command=self.scaleset,
191 from_=100, to_=value, orient=HORIZONTAL,
192 resolution=self.resolution)
193 self.scale.pack(fill="both", expand=1)
194 self.scale.set(self.target.scaleget())
197 def __init__(self, master):
198 Frame.__init__(self, master)
199 self.label = Label(self, bd=1, relief=SUNKEN, anchor=W)
200 self.label.pack(fill="both", expand=1)
204 self.label.config(text=str)
207 self.label.config(text="")
209 def startup(self, str):
213 class ColorConf(Frame):
214 def __init__(self, master, name, color):
215 Frame.__init__(self, master)
216 if (graph.getstate(name) == "hidden"):
221 self.color = StringVar()
222 self.color_default = color
223 self.color_current = color
224 self.color.set(color)
225 self.enabled = IntVar()
226 self.enabled_default = enabled
227 self.enabled_current = enabled
228 self.enabled.set(enabled)
232 self.label = Label(self, text=self.name, anchor=W)
233 self.sample = Canvas(self, width=24, height=24,
235 self.rect = self.sample.create_rectangle(0, 0, 24, 24,
236 fill=self.color.get())
237 self.list = OptionMenu(self, self.color, command=self.setcolor,
239 self.checkbox = Checkbutton(self, text="enabled",
240 variable=self.enabled)
241 self.label.grid(row=0, column=0, sticky=E+W)
242 self.sample.grid(row=0, column=1)
243 self.list.grid(row=0, column=2, sticky=E+W)
244 self.checkbox.grid(row=0, column=3)
245 self.columnconfigure(0, weight=1)
246 self.columnconfigure(2, minsize=150)
248 def setcolor(self, color):
249 self.color.set(color)
250 self.sample.itemconfigure(self.rect, fill=color)
255 if (self.color_current != self.color.get()):
257 if (self.enabled_current != self.enabled.get()):
259 self.color_current = self.color.get()
260 self.enabled_current = self.enabled.get()
262 if (self.enabled_current):
263 graph.setcolor(self.name, self.color_current)
265 graph.hide(self.name)
268 graph.setcolor(self.name, self.color_current)
271 self.setcolor(self.color_default)
272 self.enabled.set(self.enabled_default)
274 class ColorConfigure(Toplevel):
275 def __init__(self, table, name):
276 Toplevel.__init__(self)
279 self.items = LabelFrame(self, text="Item Type")
280 self.buttons = Frame(self)
282 self.items.grid(row=0, column=0, sticky=E+W)
283 self.columnconfigure(0, weight=1)
284 self.buttons.grid(row=1, column=0, sticky=E+W)
288 color = graph.getcolor(type[0])
290 self.additem(type[0], color)
292 def additem(self, name, color):
293 item = ColorConf(self.items, name, color)
294 self.types.append(item)
295 item.grid(row=self.irow, column=0, sticky=E+W)
298 def drawbuttons(self):
299 self.apply = Button(self.buttons, text="Apply",
301 self.default = Button(self.buttons, text="Revert",
303 self.apply.grid(row=0, column=0, sticky=E+W)
304 self.default.grid(row=0, column=1, sticky=E+W)
305 self.buttons.columnconfigure(0, weight=1)
306 self.buttons.columnconfigure(1, weight=1)
309 for item in self.types:
313 for item in self.types:
316 class SourceConf(Frame):
317 def __init__(self, master, source):
318 Frame.__init__(self, master)
319 if (source.hidden == 1):
324 self.name = source.name
325 self.enabled = IntVar()
326 self.enabled_default = enabled
327 self.enabled_current = enabled
328 self.enabled.set(enabled)
332 self.label = Label(self, text=self.name, anchor=W)
333 self.checkbox = Checkbutton(self, text="enabled",
334 variable=self.enabled)
335 self.label.grid(row=0, column=0, sticky=E+W)
336 self.checkbox.grid(row=0, column=1)
337 self.columnconfigure(0, weight=1)
340 if (self.enabled_current != self.enabled.get()):
345 self.enabled_current = self.enabled.get()
348 self.enabled.set(self.enabled_default)
356 class SourceConfigure(Toplevel):
358 Toplevel.__init__(self)
360 self.title("Source Configuration")
362 self.iframe = Frame(self)
363 self.iframe.grid(row=0, column=0, sticky=E+W)
364 f = LabelFrame(self.iframe, bd=4, text="Sources")
366 self.buttons = Frame(self)
367 self.items[0].grid(row=0, column=0, sticky=E+W)
368 self.columnconfigure(0, weight=1)
372 for source in sources:
373 self.addsource(source)
375 self.buttons.grid(row=1, column=0, sticky=W)
377 def addsource(self, source):
382 f = LabelFrame(self.iframe, bd=4, text="Sources")
383 f.grid(row=0, column=c, sticky=N+E+W)
385 item = SourceConf(self.items[self.icol], source)
386 self.sconfig.append(item)
387 item.grid(row=self.irow, column=0, sticky=E+W)
390 def drawbuttons(self):
391 self.apply = Button(self.buttons, text="Apply",
393 self.default = Button(self.buttons, text="Revert",
395 self.checkall = Button(self.buttons, text="Check All",
397 self.uncheckall = Button(self.buttons, text="Uncheck All",
399 self.checkall.grid(row=0, column=0, sticky=W)
400 self.uncheckall.grid(row=0, column=1, sticky=W)
401 self.apply.grid(row=0, column=2, sticky=W)
402 self.default.grid(row=0, column=3, sticky=W)
403 self.buttons.columnconfigure(0, weight=1)
404 self.buttons.columnconfigure(1, weight=1)
405 self.buttons.columnconfigure(2, weight=1)
406 self.buttons.columnconfigure(3, weight=1)
411 for item in self.sconfig:
412 if (item.changed() == 0):
414 if (item.enabled.get() == 1):
415 enable_sources.append(item.source)
417 disable_sources.append(item.source)
419 if (len(disable_sources)):
420 graph.sourcehidelist(disable_sources)
421 if (len(enable_sources)):
422 graph.sourceshowlist(enable_sources)
424 for item in self.sconfig:
428 for item in self.sconfig:
432 for item in self.sconfig:
436 for item in self.sconfig:
439 # Reverse compare of second member of the tuple
440 def cmp_counts(x, y):
443 class SourceStats(Toplevel):
444 def __init__(self, source):
446 Toplevel.__init__(self)
448 self.title(source.name + " statistics")
449 self.evframe = LabelFrame(self,
450 text="Event Count, Duration, Avg Duration")
451 self.evframe.grid(row=0, column=0, sticky=E+W)
453 for event in self.source.events:
454 if (event.type == "pad"):
456 duration = event.duration
457 if (eventtypes.has_key(event.name)):
458 (c, d) = eventtypes[event.name]
461 eventtypes[event.name] = (c, d)
463 eventtypes[event.name] = (1, duration)
465 for k, v in eventtypes.iteritems():
467 events.append((k, c, d))
468 events.sort(cmp=cmp_counts)
473 Label(self.evframe, text=name, bd=1,
474 relief=SUNKEN, anchor=W, width=30).grid(
475 row=ypos, column=0, sticky=W+E)
476 Label(self.evframe, text=str(c), bd=1,
477 relief=SUNKEN, anchor=W, width=10).grid(
478 row=ypos, column=1, sticky=W+E)
479 Label(self.evframe, text=ticks2sec(d),
480 bd=1, relief=SUNKEN, width=10).grid(
481 row=ypos, column=2, sticky=W+E)
486 Label(self.evframe, text=ticks2sec(d),
487 bd=1, relief=SUNKEN, width=10).grid(
488 row=ypos, column=3, sticky=W+E)
492 class SourceContext(Menu):
493 def __init__(self, event, source):
495 Menu.__init__(self, tearoff=0, takefocus=0)
496 self.add_command(label="hide", command=self.hide)
497 self.add_command(label="hide group", command=self.hidegroup)
498 self.add_command(label="stats", command=self.stats)
499 self.tk_popup(event.x_root-3, event.y_root+3)
502 graph.sourcehide(self.source)
506 for source in sources:
507 if (source.group == self.source.group):
508 grouplist.append(source)
509 graph.sourcehidelist(grouplist)
512 graph.sourceshow(self.source)
515 SourceStats(self.source)
517 class EventView(Toplevel):
518 def __init__(self, event, canvas):
519 Toplevel.__init__(self)
523 self.buttons = Frame(self)
524 self.buttons.grid(row=0, column=0, sticky=E+W)
525 self.frame = Frame(self)
526 self.frame.grid(row=1, column=0, sticky=N+S+E+W)
530 event.displayref(canvas)
531 self.bind("<Destroy>", self.destroycb)
533 def destroycb(self, event):
534 self.unbind("<Destroy>")
535 if (self.event != None):
536 self.event.displayunref(self.canvas)
540 def clearlabels(self):
541 for label in self.frame.grid_slaves():
544 def drawlabels(self):
546 labels = self.event.labels()
547 while (len(labels) < 7):
548 labels.append(("", ""))
552 if (name == "linkedto"):
554 l = Label(self.frame, text=name, bd=1, width=15,
555 relief=SUNKEN, anchor=W)
560 r = Label(self.frame, text=value, bd=1,
561 relief=SUNKEN, anchor=W, fg=fgcolor)
562 l.grid(row=ypos, column=0, sticky=E+W)
563 r.grid(row=ypos, column=1, sticky=E+W)
565 r.bind("<Button-1>", self.linkpress)
567 self.frame.columnconfigure(1, minsize=80)
569 def drawbuttons(self):
570 self.back = Button(self.buttons, text="<", command=self.bpress)
571 self.forw = Button(self.buttons, text=">", command=self.fpress)
572 self.new = Button(self.buttons, text="new", command=self.npress)
573 self.back.grid(row=0, column=0, sticky=E+W)
574 self.forw.grid(row=0, column=1, sticky=E+W)
575 self.new.grid(row=0, column=2, sticky=E+W)
576 self.buttons.columnconfigure(2, weight=1)
578 def newevent(self, event):
579 self.event.displayunref(self.canvas)
582 self.event.displayref(self.canvas)
586 EventView(self.event, self.canvas)
589 prev = self.event.prev()
592 while (prev.type == "pad"):
599 next = self.event.next()
602 while (next.type == "pad"):
608 def linkpress(self, wevent):
609 event = self.event.getlinked()
614 def __init__(self, source, name, cpu, timestamp, attrs):
618 self.timestamp = int(timestamp)
627 statstr = self.name + " " + self.source.name
628 statstr += " on: cpu" + str(self.cpu)
629 statstr += " at: " + str(self.timestamp)
630 statstr += " attributes: "
631 for i in range(0, len(self.attrs)):
633 statstr += attr[0] + ": " + str(attr[1])
634 if (i != len(self.attrs) - 1):
639 return [("Source", self.source.name),
640 ("Event", self.name),
642 ("Timestamp", self.timestamp),
643 ("KTR Line ", self.recno)
646 def mouseenter(self, canvas):
647 self.displayref(canvas)
650 def mouseexit(self, canvas):
651 self.displayunref(canvas)
654 def mousepress(self, canvas):
655 EventView(self, canvas)
657 def draw(self, canvas, xpos, ypos, item):
660 canvas.items[item] = self
662 def move(self, canvas, x, y):
663 if (self.item == None):
665 canvas.move(self.item, x, y);
668 return self.source.eventat(self.idx + 1)
670 def nexttype(self, type):
672 while (next != None and next.type != type):
677 return self.source.eventat(self.idx - 1)
679 def displayref(self, canvas):
680 if (self.dispcnt == 0):
681 canvas.itemconfigure(self.item, width=2)
684 def displayunref(self, canvas):
686 if (self.dispcnt == 0):
687 canvas.itemconfigure(self.item, width=0)
688 canvas.tag_raise("point", "state")
691 for attr in self.attrs:
692 if (attr[0] != "linkedto"):
694 source = ktrfile.findid(attr[1])
695 return source.findevent(self.timestamp)
698 class PointEvent(Event):
700 def __init__(self, source, name, cpu, timestamp, attrs):
701 Event.__init__(self, source, name, cpu, timestamp, attrs)
703 def draw(self, canvas, xpos, ypos):
704 color = colormap.lookup(self.name)
705 l = canvas.create_oval(xpos - XY_POINT, ypos,
706 xpos + XY_POINT, ypos - (XY_POINT * 2),
708 tags=("event", self.type, self.name, self.source.tag))
709 Event.draw(self, canvas, xpos, ypos, l)
713 class StateEvent(Event):
715 def __init__(self, source, name, cpu, timestamp, attrs):
716 Event.__init__(self, source, name, cpu, timestamp, attrs)
718 def draw(self, canvas, xpos, ypos):
719 next = self.nexttype("state")
722 self.duration = duration = next.timestamp - self.timestamp
723 self.attrs.insert(0, ("duration", ticks2sec(duration)))
724 color = colormap.lookup(self.name)
727 print "Unsynchronized timestamp"
728 print self.cpu, self.timestamp
729 print next.cpu, next.timestamp
730 delta = duration / canvas.ratio
731 l = canvas.create_rectangle(xpos, ypos,
732 xpos + delta, ypos - 10, fill=color, width=0,
733 tags=("event", self.type, self.name, self.source.tag))
734 Event.draw(self, canvas, xpos, ypos, l)
736 return (xpos + delta)
738 class CountEvent(Event):
740 def __init__(self, source, count, cpu, timestamp, attrs):
743 Event.__init__(self, source, "count", cpu, timestamp, attrs)
745 def draw(self, canvas, xpos, ypos):
746 next = self.nexttype("count")
749 color = colormap.lookup("count")
750 self.duration = duration = next.timestamp - self.timestamp
753 print "Unsynchronized timestamp"
754 print self.cpu, self.timestamp
755 print next.cpu, next.timestamp
756 self.attrs.insert(0, ("count", self.count))
757 self.attrs.insert(1, ("duration", ticks2sec(duration)))
758 delta = duration / canvas.ratio
759 yhight = self.source.yscale() * self.count
760 l = canvas.create_rectangle(xpos, ypos - yhight,
761 xpos + delta, ypos, fill=color, width=0,
762 tags=("event", self.type, self.name, self.source.tag))
763 Event.draw(self, canvas, xpos, ypos, l)
764 return (xpos + delta)
766 class PadEvent(StateEvent):
768 def __init__(self, source, cpu, timestamp, last=0):
770 cpu = source.events[len(source.events) -1].cpu
772 cpu = source.events[0].cpu
773 StateEvent.__init__(self, source, "pad", cpu, timestamp, [])
774 def draw(self, canvas, xpos, ypos):
778 duration = next.timestamp - self.timestamp
779 delta = duration / canvas.ratio
780 Event.draw(self, canvas, xpos, ypos, None)
781 return (xpos + delta)
783 # Sort function for start y address
784 def source_cmp_start(x, y):
788 def __init__(self, group, id):
796 self.tag = group + id
798 def __cmp__(self, other):
801 if (self.group == other.group):
802 return cmp(self.name, other.name)
803 return cmp(self.group, other.group)
805 # It is much faster to append items to a list then to insert them
806 # at the beginning. As a result, we add events in reverse order
807 # and then swap the list during fixup.
809 self.events.reverse()
811 def addevent(self, event):
812 self.events.append(event)
814 def addlastevent(self, event):
815 self.events.insert(0, event)
817 def draw(self, canvas, ypos):
820 cpu = self.events[1].cpu
821 for i in range(0, len(self.events)):
822 self.events[i].idx = i
823 for event in self.events:
824 if (event.cpu != cpu and event.cpu != -1):
825 self.drawcpu(canvas, cpu, cpux, xpos, ypos)
828 xpos = event.draw(canvas, xpos, ypos)
829 self.drawcpu(canvas, cpu, cpux, xpos, ypos)
831 def drawname(self, canvas, ypos):
833 ypos = ypos - (self.ysize() / 2)
834 self.item = canvas.create_text(X_BORDER, ypos, anchor="w",
838 def drawcpu(self, canvas, cpu, fromx, tox, ypos):
839 cpu = "CPU " + str(cpu)
840 color = cpucolormap.lookup(cpu)
841 # Create the cpu background colors default to hidden
842 l = canvas.create_rectangle(fromx,
843 ypos - self.ysize() - canvas.bdheight,
844 tox, ypos + canvas.bdheight, fill=color, width=0,
845 tags=("cpubg", cpu, self.tag), state="hidden")
846 self.cpuitems.append(l)
848 def move(self, canvas, xpos, ypos):
849 canvas.move(self.tag, xpos, ypos)
851 def movename(self, canvas, xpos, ypos):
853 canvas.move(self.item, xpos, ypos)
856 return (Y_EVENTSOURCE)
858 def eventat(self, i):
859 if (i >= len(self.events)):
861 event = self.events[i]
864 def findevent(self, timestamp):
865 for event in self.events:
866 if (event.timestamp >= timestamp and event.type != "pad"):
870 class Counter(EventSource):
872 # Store a hash of counter groups that keeps the max value
873 # for a counter in this group for scaling purposes.
876 def __init__(self, group, id):
878 Counter.cnt = Counter.groups[group]
880 Counter.groups[group] = 0
881 EventSource.__init__(self, group, id)
884 for event in self.events:
885 if (event.type != "count"):
887 count = int(event.count)
888 if (count > Counter.groups[self.group]):
889 Counter.groups[self.group] = count
890 EventSource.fixup(self)
893 return (Counter.groups[self.group])
899 return (self.ysize() / self.ymax())
902 def __init__(self, file):
903 self.timestamp_f = None
904 self.timestamp_l = None
917 ticksps = self.ticksps()
918 span = self.timespan()
919 ghz = float(ticksps) / 1000000000.0
921 # Update the title with some stats from the file
923 titlestr = "SchedGraph: "
924 titlestr += ticks2sec(span) + " at %.3f ghz, " % ghz
925 titlestr += str(len(sources)) + " event sources, "
926 titlestr += str(self.eventcnt) + " events"
929 def parse(self, file):
933 print "Can't open", file
936 # quoteexp matches a quoted string, no escaping
937 quoteexp = "\"([^\"]*)\""
940 # commaexp matches a quoted string OR the string up
943 commaexp = "(?:" + quoteexp + "|([^,]+))"
946 # colonstr matches a quoted string OR the string up
949 colonexp = "(?:" + quoteexp + "|([^:]+))"
952 # Match various manditory parts of the KTR string this is
953 # fairly inflexible until you get to attributes to make
956 hdrexp = "\s*(\d+)\s+(\d+)\s+(\d+)\s+"
957 groupexp = "KTRGRAPH group:" + quoteexp + ", "
958 idexp = "id:" + quoteexp + ", "
959 typeexp = "([^:]+):" + commaexp + ", "
960 attribexp = "attributes: (.*)"
963 # Matches optional attributes in the KTR string. This
964 # tolerates more variance as the users supply these values.
966 attrexp = colonexp + "\s*:\s*(?:" + commaexp + ", (.*)|"
967 attrexp += quoteexp +"|(.*))"
970 ktrre = re.compile(hdrexp + groupexp + idexp + typeexp + attribexp)
971 attrre = re.compile(attrexp)
975 for line in ifp.readlines():
977 if ((lineno % 2048) == 0):
978 status.startup("Parsing line " + str(lineno))
979 m = ktrre.match(line);
981 print "Can't parse", lineno, line,
983 (index, cpu, timestamp, group, id, type, dat, dat1, attrstring) = m.groups();
986 if (self.checkstamp(timestamp) == 0):
987 print "Bad timestamp at", lineno, ":",
991 # Build the table of optional attributes
994 while (attrstring != None):
995 m = attrre.match(attrstring.strip())
999 # Name may or may not be quoted.
1001 # For val we have four cases:
1002 # 1) quotes followed by comma and more
1004 # 2) no quotes followed by comma and more
1006 # 3) no more attributes or comma with quotes.
1007 # 4) no more attributes or comma without quotes.
1009 (name, name1, val, val1, attrstring, end, end1) = m.groups();
1018 if (name == "stathz"):
1019 self.setstathz(val, cpu)
1020 attrs.append((name, val))
1021 args = (dat, cpu, timestamp, attrs)
1022 e = self.makeevent(group, id, type, args)
1024 print "Unknown type", type, lineno, line,
1026 def makeevent(self, group, id, type, args):
1028 source = self.makeid(group, id, type)
1029 if (type == "state"):
1030 e = StateEvent(source, *args)
1031 elif (type == "counter"):
1032 e = CountEvent(source, *args)
1033 elif (type == "point"):
1034 e = PointEvent(source, *args)
1040 def setstathz(self, val, cpu):
1041 self.stathz = int(val)
1044 ticks = self.ticks[cpu]
1047 self.ticks[cpu] += 1
1049 def checkstamp(self, timestamp):
1050 timestamp = int(timestamp)
1051 if (self.timestamp_f == None):
1052 self.timestamp_f = timestamp;
1053 if (self.timestamp_l != None and
1054 timestamp -2048> self.timestamp_l):
1056 self.timestamp_l = timestamp;
1059 def makeid(self, group, id, type):
1061 if (self.taghash.has_key(tag)):
1062 return self.taghash[tag]
1063 if (type == "counter"):
1064 source = Counter(group, id)
1066 source = EventSource(group, id)
1067 sources.append(source)
1068 self.taghash[tag] = source
1071 def findid(self, id):
1072 for source in sources:
1073 if (source.name == id):
1078 return (self.timestamp_f - self.timestamp_l);
1082 # Use user supplied clock first
1083 if (clockfreq != None):
1084 return int(clockfreq * oneghz)
1086 # Check for a discovered clock
1087 if (self.stathz != 0):
1088 return (self.timespan() / self.ticks[0]) * int(self.stathz)
1089 # Pretend we have a 1ns clock
1090 print "WARNING: No clock discovered and no frequency ",
1091 print "specified via the command line."
1092 print "Using fake 1ghz clock"
1096 for source in sources:
1097 e = PadEvent(source, -1, self.timestamp_l)
1099 e = PadEvent(source, -1, self.timestamp_f, last=1)
1100 source.addlastevent(e)
1104 class SchedNames(Canvas):
1105 def __init__(self, master, display):
1106 self.display = display
1107 self.parent = master
1108 self.bdheight = master.bdheight
1112 Canvas.__init__(self, master, width=120,
1113 height=display["height"], bg='grey',
1114 scrollregion=(0, 0, 50, 100))
1116 def moveline(self, cur_y, y):
1117 for line in self.lines:
1118 (x0, y0, x1, y1) = self.coords(line)
1121 self.move(line, 0, y)
1125 status.startup("Drawing names")
1127 self.configure(scrollregion=(0, 0,
1128 self["width"], self.display.ysize()))
1129 for source in sources:
1130 l = self.create_line(0, ypos, self["width"], ypos,
1131 width=1, fill="black", tags=("all","sources"))
1132 self.lines.append(l)
1133 ypos += self.bdheight
1134 ypos += source.ysize()
1135 t = source.drawname(self, ypos)
1136 self.items[t] = source
1137 ypos += self.bdheight
1139 self.create_line(0, ypos, self["width"], ypos,
1140 width=1, fill="black", tags=("all",))
1141 self.bind("<Button-1>", self.master.mousepress);
1142 self.bind("<Button-3>", self.master.mousepressright);
1143 self.bind("<ButtonRelease-1>", self.master.mouserelease);
1144 self.bind("<B1-Motion>", self.master.mousemotion);
1146 def updatescroll(self):
1147 self.configure(scrollregion=(0, 0,
1148 self["width"], self.display.ysize()))
1151 class SchedDisplay(Canvas):
1152 def __init__(self, master):
1154 self.parent = master
1155 self.bdheight = master.bdheight
1158 Canvas.__init__(self, master, width=800, height=500, bg='grey',
1159 scrollregion=(0, 0, 800, 500))
1163 # Compute a ratio to ensure that the file's timespan fits into
1164 # 2^31. Although python may handle larger values for X
1165 # values, the Tk internals do not.
1167 self.ratio = (ktrfile.timespan() - 1) / 2**31 + 1
1171 xsize = self.xsize()
1172 for source in sources:
1173 status.startup("Drawing " + source.name)
1174 l = self.create_line(0, ypos, xsize, ypos,
1175 width=1, fill="black", tags=("all",))
1176 self.lines.append(l)
1177 ypos += self.bdheight
1178 ypos += source.ysize()
1179 source.draw(self, ypos)
1180 ypos += self.bdheight
1181 self.tag_raise("point", "state")
1182 self.tag_lower("cpubg", ALL)
1183 self.create_line(0, ypos, xsize, ypos,
1184 width=1, fill="black", tags=("lines",))
1185 self.tag_bind("event", "<Enter>", self.mouseenter)
1186 self.tag_bind("event", "<Leave>", self.mouseexit)
1187 self.bind("<Button-1>", self.mousepress)
1188 self.bind("<Button-3>", self.master.mousepressright);
1189 self.bind("<Button-4>", self.wheelup)
1190 self.bind("<Button-5>", self.wheeldown)
1191 self.bind("<ButtonRelease-1>", self.master.mouserelease);
1192 self.bind("<B1-Motion>", self.master.mousemotion);
1194 def moveline(self, cur_y, y):
1195 for line in self.lines:
1196 (x0, y0, x1, y1) = self.coords(line)
1199 self.move(line, 0, y)
1202 def mouseenter(self, event):
1203 item, = self.find_withtag(CURRENT)
1204 self.items[item].mouseenter(self)
1206 def mouseexit(self, event):
1207 item, = self.find_withtag(CURRENT)
1208 self.items[item].mouseexit(self)
1210 def mousepress(self, event):
1211 # Find out what's beneath us
1212 items = self.find_withtag(CURRENT)
1213 if (len(items) == 0):
1214 self.master.mousepress(event)
1216 # Only grab mouse presses for things with event tags.
1218 tags = self.gettags(item)
1220 if (tag == "event"):
1221 self.items[item].mousepress(self)
1223 # Leave the rest to the master window
1224 self.master.mousepress(event)
1226 def wheeldown(self, event):
1227 self.parent.display_yview("scroll", 1, "units")
1229 def wheelup(self, event):
1230 self.parent.display_yview("scroll", -1, "units")
1233 return ((ktrfile.timespan() / self.ratio) + (X_BORDER * 2))
1237 for source in sources:
1238 if (source.hidden == 1):
1240 ysize += self.parent.sourcesize(source)
1243 def scaleset(self, ratio):
1244 if (ktrfile == None):
1246 oldratio = self.ratio
1247 xstart, xend = self.xview()
1248 midpoint = xstart + ((xend - xstart) / 2)
1252 self.scale(ALL, 0, 0, float(oldratio) / ratio, 1)
1254 xstart, xend = self.xview()
1255 xsize = (xend - xstart) / 2
1256 self.xview_moveto(midpoint - xsize)
1258 def updatescroll(self):
1259 self.configure(scrollregion=(0, 0, self.xsize(), self.ysize()))
1264 def getcolor(self, tag):
1265 return self.itemcget(tag, "fill")
1267 def getstate(self, tag):
1268 return self.itemcget(tag, "state")
1270 def setcolor(self, tag, color):
1271 self.itemconfigure(tag, state="normal", fill=color)
1273 def hide(self, tag):
1274 self.itemconfigure(tag, state="hidden")
1276 class GraphMenu(Frame):
1277 def __init__(self, master):
1278 Frame.__init__(self, master, bd=2, relief=RAISED)
1279 self.conf = Menubutton(self, text="Configure")
1280 self.confmenu = Menu(self.conf, tearoff=0)
1281 self.confmenu.add_command(label="Event Colors",
1283 self.confmenu.add_command(label="CPU Colors",
1285 self.confmenu.add_command(label="Source Configure",
1287 self.conf["menu"] = self.confmenu
1288 self.conf.pack(side=LEFT)
1291 ColorConfigure(eventcolors, "Event Display Configuration")
1294 ColorConfigure(cpucolors, "CPU Background Colors")
1299 class SchedGraph(Frame):
1300 def __init__(self, master):
1301 Frame.__init__(self, master)
1307 self.bdheight = Y_BORDER
1308 self.clicksource = None
1309 self.lastsource = None
1310 self.pack(expand=1, fill="both")
1314 def buildwidgets(self):
1316 self.menu = GraphMenu(self)
1317 self.display = SchedDisplay(self)
1318 self.names = SchedNames(self, self.display)
1319 self.scale = Scaler(self, self.display)
1320 status = self.status = Status(self)
1321 self.scrollY = Scrollbar(self, orient="vertical",
1322 command=self.display_yview)
1323 self.display.scrollX = Scrollbar(self, orient="horizontal",
1324 command=self.display.xview)
1325 self.display["xscrollcommand"] = self.display.scrollX.set
1326 self.display["yscrollcommand"] = self.scrollY.set
1327 self.names["yscrollcommand"] = self.scrollY.set
1330 self.columnconfigure(1, weight=1)
1331 self.rowconfigure(1, weight=1)
1332 self.menu.grid(row=0, column=0, columnspan=3, sticky=E+W)
1333 self.names.grid(row=1, column=0, sticky=N+S)
1334 self.display.grid(row=1, column=1, sticky=W+E+N+S)
1335 self.scrollY.grid(row=1, column=2, sticky=N+S)
1336 self.display.scrollX.grid(row=2, column=0, columnspan=2,
1338 self.scale.grid(row=3, column=0, columnspan=3, sticky=E+W)
1339 self.status.grid(row=4, column=0, columnspan=3, sticky=E+W)
1342 self.master.update()
1343 self.display.prepare()
1346 self.status.startup("")
1348 # Configure scale related values
1350 scalemax = ktrfile.timespan() / int(self.display["width"])
1351 width = int(root.geometry().split('x')[0])
1352 self.constwidth = width - int(self.display["width"])
1353 self.scale.setmax(scalemax)
1354 self.scale.set(scalemax)
1355 self.display.xview_moveto(0)
1356 self.bind("<Configure>", self.resize)
1358 def mousepress(self, event):
1359 self.clicksource = self.sourceat(event.y)
1361 def mousepressright(self, event):
1362 source = self.sourceat(event.y)
1363 if (source == None):
1365 SourceContext(event, source)
1367 def mouserelease(self, event):
1368 if (self.clicksource == None):
1370 newsource = self.sourceat(event.y)
1371 if (self.clicksource != newsource):
1372 self.sourceswap(self.clicksource, newsource)
1373 self.clicksource = None
1374 self.lastsource = None
1376 def mousemotion(self, event):
1377 if (self.clicksource == None):
1379 newsource = self.sourceat(event.y)
1381 # If we get a None source they moved off the page.
1382 # swapsource() can't handle moving multiple items so just
1383 # pretend we never clicked on anything to begin with so the
1384 # user can't mouseover a non-contiguous area.
1386 if (newsource == None):
1387 self.clicksource = None
1388 self.lastsource = None
1390 if (newsource == self.lastsource):
1392 self.lastsource = newsource
1393 if (newsource != self.clicksource):
1394 self.sourceswap(self.clicksource, newsource)
1396 # These are here because this object controls layout
1397 def sourcestart(self, source):
1398 return source.y - self.bdheight - source.ysize()
1400 def sourceend(self, source):
1401 return source.y + self.bdheight
1403 def sourcesize(self, source):
1404 return (self.bdheight * 2) + source.ysize()
1406 def sourceswap(self, source1, source2):
1407 # Sort so we always know which one is on top.
1408 if (source2.y < source1.y):
1412 # Only swap adjacent sources
1413 if (self.sourceend(source1) != self.sourcestart(source2)):
1415 # Compute start coordinates and target coordinates
1416 y1 = self.sourcestart(source1)
1417 y2 = self.sourcestart(source2)
1418 y1targ = y1 + self.sourcesize(source2)
1421 # If the sizes are not equal, adjust the start of the lower
1422 # source to account for the lost/gained space.
1424 if (source1.ysize() != source2.ysize()):
1425 diff = source2.ysize() - source1.ysize()
1426 self.names.moveline(y2, diff);
1427 self.display.moveline(y2, diff)
1428 source1.move(self.display, 0, y1targ - y1)
1429 source2.move(self.display, 0, y2targ - y2)
1430 source1.movename(self.names, 0, y1targ - y1)
1431 source2.movename(self.names, 0, y2targ - y2)
1433 def sourcepicky(self, source):
1434 if (source.hidden == 0):
1435 return self.sourcestart(source)
1436 # Revert to group based sort
1447 newy = self.sourcestart(prev) + self.sourcesize(prev)
1450 def sourceshow(self, source):
1451 if (source.hidden == 0):
1453 newy = self.sourcepicky(source)
1454 off = newy - self.sourcestart(source)
1455 self.sourceshiftall(newy-1, self.sourcesize(source))
1456 self.sourceshift(source, off)
1460 # Optimized source show of multiple entries that only moves each
1461 # existing entry once. Doing sourceshow() iteratively is too
1462 # expensive due to python's canvas.move().
1464 def sourceshowlist(self, srclist):
1465 srclist.sort(cmp=source_cmp_start)
1467 for source in srclist:
1468 if (source.hidden == 0):
1469 srclist.remove(source)
1470 startsize.append((self.sourcepicky(source),
1471 self.sourcesize(source)))
1473 sources.sort(cmp=source_cmp_start, reverse=True)
1474 self.status.startup("Updating display...");
1475 for source in sources:
1476 if (source.hidden == 1):
1478 nstart = self.sourcestart(source)
1480 for hidden in startsize:
1481 (start, sz) = hidden
1482 if (start <= nstart or start+sz <= nstart):
1484 self.sourceshift(source, size)
1487 for source in srclist:
1488 (newy, sz) = startsize[idx]
1489 off = (newy + size) - self.sourcestart(source)
1490 self.sourceshift(source, off)
1498 # Optimized source hide of multiple entries that only moves each
1499 # remaining entry once. Doing sourcehide() iteratively is too
1500 # expensive due to python's canvas.move().
1502 def sourcehidelist(self, srclist):
1503 srclist.sort(cmp=source_cmp_start)
1504 sources.sort(cmp=source_cmp_start)
1506 off = len(sources) * 100
1507 self.status.startup("Updating display...");
1508 for source in srclist:
1509 if (source.hidden == 1):
1510 srclist.remove(source)
1512 # Remember our old position so we can sort things
1513 # below us when we're done.
1515 startsize.append((self.sourcestart(source),
1516 self.sourcesize(source)))
1517 self.sourceshift(source, off)
1522 for hidden in startsize:
1523 (start, sz) = hidden
1525 if (idx + 1 < len(startsize)):
1526 (stop, sz) = startsize[idx+1]
1528 stop = self.display.ysize()
1530 for source in sources:
1531 nstart = self.sourcestart(source)
1532 if (nstart < start or source.hidden == 1):
1534 if (nstart >= stop):
1536 self.sourceshift(source, -size)
1540 def sourcehide(self, source):
1541 if (source.hidden == 1):
1543 # Move it out of the visible area
1544 off = len(sources) * 100
1545 start = self.sourcestart(source)
1546 self.sourceshift(source, off)
1547 self.sourceshiftall(start, -self.sourcesize(source))
1550 def sourceshift(self, source, off):
1551 start = self.sourcestart(source)
1552 source.move(self.display, 0, off)
1553 source.movename(self.names, 0, off)
1554 self.names.moveline(start, off);
1555 self.display.moveline(start, off)
1557 # We update the idle tasks to shrink the dirtied area so
1558 # it does not always include the entire screen.
1560 self.names.update_idletasks()
1561 self.display.update_idletasks()
1563 def sourceshiftall(self, start, off):
1564 self.status.startup("Updating display...");
1565 for source in sources:
1566 nstart = self.sourcestart(source)
1567 if (nstart < start):
1569 self.sourceshift(source, off)
1573 def sourceat(self, ypos):
1574 (start, end) = self.names.yview()
1575 starty = start * float(self.names.ysize)
1577 for source in sources:
1578 if (source.hidden == 1):
1580 yend = self.sourceend(source)
1581 ystart = self.sourcestart(source)
1582 if (ypos >= ystart and ypos <= yend):
1586 def display_yview(self, *args):
1587 self.names.yview(*args)
1588 self.display.yview(*args)
1590 def resize(self, *args):
1591 width = int(root.geometry().split('x')[0])
1592 scalemax = ktrfile.timespan() / (width - self.constwidth)
1593 self.scale.setmax(scalemax)
1595 def updatescroll(self):
1596 self.names.updatescroll()
1597 self.display.updatescroll()
1599 def setcolor(self, tag, color):
1600 self.display.setcolor(tag, color)
1602 def hide(self, tag):
1603 self.display.hide(tag)
1605 def getcolor(self, tag):
1606 return self.display.getcolor(tag)
1608 def getstate(self, tag):
1609 return self.display.getstate(tag)
1611 if (len(sys.argv) != 2 and len(sys.argv) != 3):
1612 print "usage:", sys.argv[0], "<ktr file> [clock freq in ghz]"
1615 if (len(sys.argv) > 2):
1616 clockfreq = float(sys.argv[2])
1619 root.title("SchedGraph")
1620 colormap = Colormap(eventcolors)
1621 cpucolormap = Colormap(cpucolors)
1622 graph = SchedGraph(root)
1623 ktrfile = KTRFile(sys.argv[1])