diff options
| author | Bjoern Brandenburg <bbb@mpi-sws.org> | 2016-03-22 15:53:55 -0400 |
|---|---|---|
| committer | Bjoern Brandenburg <bbb@mpi-sws.org> | 2016-03-22 16:00:36 -0400 |
| commit | 27474dfff59e40e1abf33e0b5dfe7a16dc73f4b1 (patch) | |
| tree | ead56b3783117e666d008498120f8de7f6627bfe | |
| parent | 17fedde91157d9e57a3c98c00c11ca03b0afcc78 (diff) | |
add st-draw: a pycairo based drawing tool for st_trace schedules
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | sched_trace/__init__.py | 202 | ||||
| -rw-r--r-- | sched_trace/draw.py | 231 | ||||
| -rw-r--r-- | sched_trace/file.py | 58 | ||||
| -rw-r--r-- | sched_trace/format.py | 118 | ||||
| -rwxr-xr-x | st-draw | 142 |
6 files changed, 754 insertions, 0 deletions
| @@ -1,6 +1,9 @@ | |||
| 1 | .sconsign.dblite | 1 | .sconsign.dblite |
| 2 | *.o | 2 | *.o |
| 3 | *.d | 3 | *.d |
| 4 | *.pyc | ||
| 5 | |||
| 6 | *.bin | ||
| 4 | ~* | 7 | ~* |
| 5 | *~ | 8 | *~ |
| 6 | ftcat | 9 | ftcat |
diff --git a/sched_trace/__init__.py b/sched_trace/__init__.py new file mode 100644 index 0000000..5db2c2d --- /dev/null +++ b/sched_trace/__init__.py | |||
| @@ -0,0 +1,202 @@ | |||
| 1 | from __future__ import division | ||
| 2 | |||
| 3 | from collections import defaultdict | ||
| 4 | from itertools import imap | ||
| 5 | |||
| 6 | import heapq | ||
| 7 | |||
| 8 | from .format import event, EVENTS_WITHOUT_TIMESTAMP, EVENTS | ||
| 9 | from .file import SchedTraceFile | ||
| 10 | |||
| 11 | def event_id(unpacked_record): | ||
| 12 | "Given a tuple from format.unpack(), return the ID of the event" | ||
| 13 | return unpacked_record[0] | ||
| 14 | |||
| 15 | def event_cpu(unpacked_record): | ||
| 16 | "Given a tuple from format.unpack(), return the CPU that recorded the event" | ||
| 17 | return unpacked_record[1] | ||
| 18 | |||
| 19 | def event_pid(unpacked_record): | ||
| 20 | "Given a tuple from format.unpack(), return the process ID of the event" | ||
| 21 | return unpacked_record[2] | ||
| 22 | |||
| 23 | def event_job_id(unpacked_record): | ||
| 24 | "Given a tuple from format.unpack(), return the job ID of the event" | ||
| 25 | return unpacked_record[3] | ||
| 26 | |||
| 27 | def event_name(unpacked_record): | ||
| 28 | "Given a tuple from format.unpack(), return the name of the event" | ||
| 29 | # first element of tuple from format.unpack() is event type | ||
| 30 | return event(unpacked_record[0]) | ||
| 31 | |||
| 32 | def event_time(unpacked_record): | ||
| 33 | "Given a tuple from format.unpack(), return the time of the event" | ||
| 34 | if unpacked_record[0] in EVENTS_WITHOUT_TIMESTAMP: | ||
| 35 | return 0 | ||
| 36 | else: | ||
| 37 | return unpacked_record[4] | ||
| 38 | |||
| 39 | def lookup_ids(events): | ||
| 40 | wanted = set() | ||
| 41 | for e in events: | ||
| 42 | if type(e) == int: | ||
| 43 | # directly add numeric IDs | ||
| 44 | wanted.add(e) | ||
| 45 | else: | ||
| 46 | # translate strings | ||
| 47 | wanted.add(EVENTS[e]) | ||
| 48 | return wanted | ||
| 49 | |||
| 50 | |||
| 51 | def cached(trace_attr): | ||
| 52 | cached_val = [] | ||
| 53 | def check_cache(*args, **kargs): | ||
| 54 | if not cached_val: | ||
| 55 | cached_val.append(trace_attr(*args, **kargs)) | ||
| 56 | return cached_val[0] | ||
| 57 | return check_cache | ||
| 58 | |||
| 59 | class SchedTrace(object): | ||
| 60 | def __init__(self, trace_files): | ||
| 61 | self.traces = [SchedTraceFile(f) for f in trace_files] | ||
| 62 | |||
| 63 | self._names = None | ||
| 64 | self._wcets = None | ||
| 65 | self._periods = None | ||
| 66 | self._phases = None | ||
| 67 | self._partitions = None | ||
| 68 | self._sys_rels = None | ||
| 69 | |||
| 70 | def __len__(self): | ||
| 71 | return sum((len(t) for t in self.traces)) | ||
| 72 | |||
| 73 | def __iter__(self): | ||
| 74 | for t in self.traces: | ||
| 75 | for rec in t: | ||
| 76 | yield rec | ||
| 77 | |||
| 78 | def events_of_type(self, *events): | ||
| 79 | wanted = lookup_ids(events) | ||
| 80 | for t in self.traces: | ||
| 81 | for rec in t.events_of_type(wanted): | ||
| 82 | yield rec | ||
| 83 | |||
| 84 | def events_of_type_chrono(self, *events): | ||
| 85 | wanted = lookup_ids(events) | ||
| 86 | |||
| 87 | def by_event_time(rec): | ||
| 88 | return (event_time(rec), rec) | ||
| 89 | |||
| 90 | trace_iters = [imap(by_event_time, t.events_of_type(wanted)) | ||
| 91 | for t in self.traces] | ||
| 92 | |||
| 93 | for (when, rec) in heapq.merge(*trace_iters): | ||
| 94 | yield rec | ||
| 95 | |||
| 96 | def events_in_range_of_type(self, start=0, end=0, *events, **kargs): | ||
| 97 | if 'sorted' in kargs and kargs['sorted']: | ||
| 98 | all = self.events_of_type_chrono(*events) | ||
| 99 | else: | ||
| 100 | all = self.events_of_type(*events) | ||
| 101 | |||
| 102 | for rec in all: | ||
| 103 | if start <= event_time(rec) <= end: | ||
| 104 | yield rec | ||
| 105 | |||
| 106 | def active_in_interval(self, start=0, end=0): | ||
| 107 | tasks = set() | ||
| 108 | cores = set() | ||
| 109 | for rec in self.events_of_type('ST_SWITCH_TO', 'ST_SWITCH_AWAY', 'ST_RELEASE'): | ||
| 110 | if start <= event_time(rec) <= end: | ||
| 111 | tasks.add(event_pid(rec)) | ||
| 112 | cores.add(event_cpu(rec)) | ||
| 113 | return tasks, cores | ||
| 114 | |||
| 115 | def scheduling_intervals(self): | ||
| 116 | for t in self.traces: | ||
| 117 | for interval in t.scheduling_intervals(): | ||
| 118 | yield interval | ||
| 119 | |||
| 120 | def scheduling_intervals_in_range(self, start=0, end=0): | ||
| 121 | for t in self.traces: | ||
| 122 | for (to, away) in t.scheduling_intervals(): | ||
| 123 | if not (end < event_time(to) or event_time(away) < start): | ||
| 124 | yield (to, away) | ||
| 125 | |||
| 126 | def identify_tasks(self): | ||
| 127 | self._names = defaultdict(str) | ||
| 128 | self._wcets = defaultdict(int) | ||
| 129 | self._periods = defaultdict(int) | ||
| 130 | self._phases = defaultdict(int) | ||
| 131 | self._partitions = defaultdict(int) | ||
| 132 | param_id = EVENTS['ST_PARAM'] | ||
| 133 | name_id = EVENTS['ST_NAME'] | ||
| 134 | for rec in self.events_of_type(param_id, name_id): | ||
| 135 | pid = rec[2] | ||
| 136 | if rec[0] == param_id: | ||
| 137 | wcet, period, phase, partition = rec[-4:] | ||
| 138 | self._wcets[pid] = wcet | ||
| 139 | self._periods[pid] = period | ||
| 140 | self._phases[pid] = phase | ||
| 141 | self._partitions[pid] = partition | ||
| 142 | elif rec[0] == name_id: | ||
| 143 | self._names[pid] = rec[-1] | ||
| 144 | |||
| 145 | @property | ||
| 146 | def task_wcets(self): | ||
| 147 | if self._wcets is None: | ||
| 148 | self.identify_tasks() | ||
| 149 | return self._wcets | ||
| 150 | |||
| 151 | @property | ||
| 152 | def task_periods(self): | ||
| 153 | if self._periods is None: | ||
| 154 | self.identify_tasks() | ||
| 155 | return self._periods | ||
| 156 | |||
| 157 | @property | ||
| 158 | def task_phases(self): | ||
| 159 | if self._phases is None: | ||
| 160 | self.identify_tasks() | ||
| 161 | return self._phases | ||
| 162 | |||
| 163 | @property | ||
| 164 | def task_partitions(self): | ||
| 165 | if self._partitions is None: | ||
| 166 | self.identify_tasks() | ||
| 167 | return self._partitions | ||
| 168 | |||
| 169 | @property | ||
| 170 | def task_names(self): | ||
| 171 | if self._names is None: | ||
| 172 | self.identify_tasks() | ||
| 173 | return self._names | ||
| 174 | |||
| 175 | @property | ||
| 176 | @cached | ||
| 177 | def system_releases(self): | ||
| 178 | return [rec[-1] for rec in self.events_of_type('ST_SYS_RELEASE')] | ||
| 179 | |||
| 180 | @property | ||
| 181 | @cached | ||
| 182 | def earliest_event_time(self): | ||
| 183 | earliest = None | ||
| 184 | for t in self.traces: | ||
| 185 | for rec in t: | ||
| 186 | if event_time(rec) > 0: | ||
| 187 | if earliest is None or event_time(rec) < earliest: | ||
| 188 | earliest = event_time(rec) | ||
| 189 | break | ||
| 190 | return earliest | ||
| 191 | |||
| 192 | @property | ||
| 193 | @cached | ||
| 194 | def latest_event_time(self): | ||
| 195 | latest = None | ||
| 196 | for t in self.traces: | ||
| 197 | for rec in reversed(t): | ||
| 198 | if event_time(rec) > 0: | ||
| 199 | if latest is None or event_time(rec) > latest: | ||
| 200 | latest = event_time(rec) | ||
| 201 | break | ||
| 202 | return latest | ||
diff --git a/sched_trace/draw.py b/sched_trace/draw.py new file mode 100644 index 0000000..4ccf245 --- /dev/null +++ b/sched_trace/draw.py | |||
| @@ -0,0 +1,231 @@ | |||
| 1 | from __future__ import division | ||
| 2 | |||
| 3 | import sys | ||
| 4 | import cairo | ||
| 5 | |||
| 6 | from math import ceil | ||
| 7 | |||
| 8 | from .format import EVENTS | ||
| 9 | |||
| 10 | from . import event_time, event_pid, event_cpu | ||
| 11 | |||
| 12 | ARROW_LINE_WIDTH = 2 # (pts) | ||
| 13 | ARROW_WIDTH = 12 # (pts) | ||
| 14 | ARROW_HEIGHT = 0.8 # (relative) | ||
| 15 | ALLOC_HEIGHT = 0.5 # (relative) | ||
| 16 | |||
| 17 | COLORS = [ | ||
| 18 | (0, 0, 0), | ||
| 19 | (1, 0, 0), | ||
| 20 | (0, 0, 1), | ||
| 21 | (0, 1, 0), | ||
| 22 | (1, 1, 0), | ||
| 23 | (1, 0, 1), | ||
| 24 | (0, 1, 1), | ||
| 25 | ] | ||
| 26 | |||
| 27 | def cpu_color(cpu): | ||
| 28 | return COLORS[cpu % len(COLORS)] | ||
| 29 | |||
| 30 | MAJOR_TICK_FONT_SIZE = 24 | ||
| 31 | TASK_LABEL_FONT_SIZE = 18 | ||
| 32 | TASK_LABEL_COLOR = (0, 0, 0) | ||
| 33 | |||
| 34 | GRID_WIDTH = 2 | ||
| 35 | MAJOR_TICK_WIDTH = 2 | ||
| 36 | MINOR_TICK_WIDTH = 1 | ||
| 37 | |||
| 38 | MAJOR_TICK_COLOR = (0.7, 0.7, 0.7) | ||
| 39 | MINOR_TICK_COLOR = (0.8, 0.8, 0.8) | ||
| 40 | |||
| 41 | GRID_COLOR = (0.7, 0.7, 0.7) | ||
| 42 | |||
| 43 | JOB_EVENT_COLOR = (0, 0, 0) | ||
| 44 | |||
| 45 | YRES = 100 | ||
| 46 | |||
| 47 | |||
| 48 | |||
| 49 | XLABEL_MARGIN = 72 | ||
| 50 | YLABEL_MARGIN = 200 | ||
| 51 | |||
| 52 | def center_text(c, x, y, msg): | ||
| 53 | x_bear, y_bear, width, height, x_adv, y_adv = c.text_extents(msg) | ||
| 54 | c.move_to(x, y) | ||
| 55 | c.rel_move_to(-width / 2, height) | ||
| 56 | c.show_text(msg) | ||
| 57 | |||
| 58 | def vcenter_right_align_text(c, x, y, msg): | ||
| 59 | x_bear, y_bear, width, height, x_adv, y_adv = c.text_extents(msg) | ||
| 60 | c.move_to(x, y) | ||
| 61 | c.rel_move_to(-width, height/2) | ||
| 62 | c.show_text(msg) | ||
| 63 | return y_adv | ||
| 64 | |||
| 65 | def render(opts, trace): | ||
| 66 | if opts.verbose: | ||
| 67 | print '[II] Identifying relevant tasks and CPUs...', | ||
| 68 | sys.stdout.flush() | ||
| 69 | tasks, cores = trace.active_in_interval(opts.start, opts.end) | ||
| 70 | if opts.verbose: | ||
| 71 | print '%d tasks, %d cores.' % (len(tasks), len(cores)) | ||
| 72 | |||
| 73 | # scale is given in points/ms, we need points/ns | ||
| 74 | xscale = opts.xscale/1E6 | ||
| 75 | yscale = opts.yscale/YRES | ||
| 76 | width = ceil(opts.margin * 2 + (opts.end - opts.start) * xscale + YLABEL_MARGIN) | ||
| 77 | |||
| 78 | height = ceil(opts.margin * 2 + (len(tasks) * YRES) * yscale + XLABEL_MARGIN) | ||
| 79 | |||
| 80 | if opts.verbose: | ||
| 81 | print '[II] Canvas size: %dpt x %dpt' % (width, height) | ||
| 82 | |||
| 83 | pdf = cairo.PDFSurface(opts.output, width, height) | ||
| 84 | |||
| 85 | task_idx = {} | ||
| 86 | for i, t in enumerate(sorted(tasks)): | ||
| 87 | task_idx[t] = i | ||
| 88 | |||
| 89 | |||
| 90 | c = cairo.Context(pdf) | ||
| 91 | c.translate(opts.margin + YLABEL_MARGIN, opts.margin) | ||
| 92 | |||
| 93 | # draw box | ||
| 94 | # c.rectangle(0, 0, width - opts.margin * 2, height - opts.margin * 2) | ||
| 95 | # c.stroke() | ||
| 96 | |||
| 97 | def xpos(x): | ||
| 98 | return (x - opts.start) * xscale | ||
| 99 | |||
| 100 | def ypos(y): | ||
| 101 | return (y * YRES) * yscale | ||
| 102 | |||
| 103 | # c.scale(xscale, yscale) | ||
| 104 | |||
| 105 | # draw minor tick lines | ||
| 106 | if opts.minor_ticks: | ||
| 107 | c.set_source_rgb(*MINOR_TICK_COLOR) | ||
| 108 | c.set_line_width(MINOR_TICK_WIDTH) | ||
| 109 | time = opts.start | ||
| 110 | while time <= opts.end: | ||
| 111 | x = xpos(time) | ||
| 112 | y = ypos(len(tasks)) | ||
| 113 | c.new_path() | ||
| 114 | c.move_to(x, y) | ||
| 115 | c.line_to(x, 0) | ||
| 116 | c.stroke() | ||
| 117 | time += opts.minor_ticks * 1E6 | ||
| 118 | |||
| 119 | |||
| 120 | # draw major tick lines | ||
| 121 | if opts.major_ticks: | ||
| 122 | c.set_source_rgb(*MAJOR_TICK_COLOR) | ||
| 123 | c.set_line_width(MAJOR_TICK_WIDTH) | ||
| 124 | c.set_font_size(MAJOR_TICK_FONT_SIZE) | ||
| 125 | time = opts.start | ||
| 126 | while time <= opts.end: | ||
| 127 | x = xpos(time) | ||
| 128 | y = ypos(len(tasks) + 0.2) | ||
| 129 | c.new_path() | ||
| 130 | c.move_to(x, y) | ||
| 131 | c.line_to(x, 0) | ||
| 132 | c.stroke() | ||
| 133 | y = ypos(len(tasks) + 0.3) | ||
| 134 | center_text(c, x, y, "%dms" % ((time - opts.start) / 1E6)) | ||
| 135 | time += opts.major_ticks * 1E6 | ||
| 136 | |||
| 137 | |||
| 138 | # raw allocations | ||
| 139 | box_height = ALLOC_HEIGHT * YRES * yscale | ||
| 140 | for (to, away) in trace.scheduling_intervals_in_range(opts.start, opts.end): | ||
| 141 | delta = (event_time(away) - event_time(to)) * xscale | ||
| 142 | pid = event_pid(to) | ||
| 143 | y = ypos(task_idx[pid] + ALLOC_HEIGHT) | ||
| 144 | x = xpos(event_time(to)) | ||
| 145 | c.new_path() | ||
| 146 | c.rectangle(x, y, delta, box_height) | ||
| 147 | c.set_source_rgb(*cpu_color(event_cpu(to))) | ||
| 148 | c.fill() | ||
| 149 | |||
| 150 | # draw task base lines | ||
| 151 | c.set_source_rgb(*GRID_COLOR) | ||
| 152 | c.set_line_width(GRID_WIDTH) | ||
| 153 | for t in tasks: | ||
| 154 | c.new_path() | ||
| 155 | y = ypos(task_idx[t] + 1) | ||
| 156 | x = xpos(opts.start) | ||
| 157 | c.move_to(x, y) | ||
| 158 | x = xpos(opts.end) | ||
| 159 | c.line_to(x, y) | ||
| 160 | c.stroke() | ||
| 161 | |||
| 162 | # draw releases and deadlines | ||
| 163 | c.set_source_rgb(*JOB_EVENT_COLOR) | ||
| 164 | c.set_line_width(ARROW_LINE_WIDTH) | ||
| 165 | # c.set_line_cap(cairo.LINE_JOIN_ROUND) | ||
| 166 | c.set_line_join(cairo.LINE_JOIN_ROUND) | ||
| 167 | arrow_width = ARROW_WIDTH / 2 | ||
| 168 | arrow_height = ARROW_HEIGHT * YRES * yscale | ||
| 169 | for rec in trace.events_in_range_of_type(opts.start, opts.end, 'ST_RELEASE'): | ||
| 170 | pid = event_pid(rec) | ||
| 171 | y = ypos(task_idx[pid] + 1) | ||
| 172 | |||
| 173 | rel = xpos(event_time(rec)) | ||
| 174 | c.new_path() | ||
| 175 | c.move_to(rel, y - (GRID_WIDTH - 1)) | ||
| 176 | c.rel_line_to(0, -arrow_height) | ||
| 177 | c.rel_line_to(-arrow_width, arrow_width) | ||
| 178 | c.rel_move_to(arrow_width, -arrow_width) | ||
| 179 | c.rel_line_to(arrow_width, arrow_width) | ||
| 180 | c.stroke() | ||
| 181 | |||
| 182 | dl = xpos(rec[-1]) | ||
| 183 | c.new_path() | ||
| 184 | c.move_to(dl, y - arrow_height - (GRID_WIDTH - 1)) | ||
| 185 | c.rel_line_to(0, arrow_height) | ||
| 186 | c.rel_line_to(-arrow_width, -arrow_width) | ||
| 187 | c.rel_move_to(arrow_width, arrow_width) | ||
| 188 | c.rel_line_to(arrow_width, -arrow_width) | ||
| 189 | c.stroke() | ||
| 190 | |||
| 191 | # draw job completions | ||
| 192 | for rec in trace.events_in_range_of_type(opts.start, opts.end, 'ST_COMPLETION'): | ||
| 193 | pid = event_pid(rec) | ||
| 194 | y = ypos(task_idx[pid] + 1) | ||
| 195 | x = xpos(event_time(rec)) | ||
| 196 | c.new_path() | ||
| 197 | c.move_to(x, y - (GRID_WIDTH - 1)) | ||
| 198 | c.rel_line_to(0, -arrow_height) | ||
| 199 | c.rel_move_to(-arrow_width, 0) | ||
| 200 | c.rel_line_to(2 * arrow_width, 0) | ||
| 201 | c.stroke() | ||
| 202 | |||
| 203 | |||
| 204 | # draw task labels | ||
| 205 | c.set_font_size(TASK_LABEL_FONT_SIZE) | ||
| 206 | c.set_source_rgb(*TASK_LABEL_COLOR) | ||
| 207 | for pid in tasks: | ||
| 208 | x = -24 | ||
| 209 | y = ypos(task_idx[pid] + 0.25) | ||
| 210 | vcenter_right_align_text(c, x, y, "%s/%d" % (trace.task_names[pid], pid)) | ||
| 211 | y = ypos(task_idx[pid] + 0.75) | ||
| 212 | vcenter_right_align_text(c, x, y, | ||
| 213 | "(%.2fms, %.2fms)" % (trace.task_wcets[pid] / 1E6, | ||
| 214 | trace.task_periods[pid] / 1E6)) | ||
| 215 | |||
| 216 | |||
| 217 | if opts.verbose: | ||
| 218 | print '[II] Finishing PDF...', | ||
| 219 | sys.stdout.flush() | ||
| 220 | pdf.finish() | ||
| 221 | if opts.verbose: | ||
| 222 | print 'done.' | ||
| 223 | |||
| 224 | if opts.verbose: | ||
| 225 | print '[II] Flushing PDF...', | ||
| 226 | sys.stdout.flush() | ||
| 227 | pdf.flush() | ||
| 228 | if opts.verbose: | ||
| 229 | print 'done.' | ||
| 230 | |||
| 231 | del pdf | ||
diff --git a/sched_trace/file.py b/sched_trace/file.py new file mode 100644 index 0000000..5c18163 --- /dev/null +++ b/sched_trace/file.py | |||
| @@ -0,0 +1,58 @@ | |||
| 1 | """Convenience wrapper around mmap'ed sched_trace binary files. | ||
| 2 | """ | ||
| 3 | |||
| 4 | from .format import RECORD_LEN, unpack, get_event, EVENTS | ||
| 5 | |||
| 6 | import mmap | ||
| 7 | |||
| 8 | class SchedTraceFile(object): | ||
| 9 | def __init__(self, fname): | ||
| 10 | self.source = fname | ||
| 11 | with open(fname, 'rw') as f: | ||
| 12 | self.mem = mmap.mmap(f.fileno(), 0, mmap.MAP_PRIVATE) | ||
| 13 | |||
| 14 | def __len__(self): | ||
| 15 | return len(self.mem) / RECORD_LEN | ||
| 16 | |||
| 17 | def events_of_type(self, event_ids): | ||
| 18 | for i in xrange(len(self)): | ||
| 19 | if get_event(self.mem, i) in event_ids: | ||
| 20 | record = self.mem[i * RECORD_LEN:] | ||
| 21 | yield unpack(record) | ||
| 22 | |||
| 23 | def __iter__(self): | ||
| 24 | for i in xrange(len(self)): | ||
| 25 | record = self.mem[i * RECORD_LEN:] | ||
| 26 | yield unpack(record) | ||
| 27 | |||
| 28 | def __reversed__(self): | ||
| 29 | for i in reversed(xrange(len(self))): | ||
| 30 | record = self.mem[i * RECORD_LEN:] | ||
| 31 | yield unpack(record) | ||
| 32 | |||
| 33 | def raw_iter(self): | ||
| 34 | for i in xrange(len(self)): | ||
| 35 | record = self.mem[i * RECORD_LEN:(i + 1) * RECORD_LEN] | ||
| 36 | yield record | ||
| 37 | |||
| 38 | def scheduling_intervals(self): | ||
| 39 | to_id = EVENTS['ST_SWITCH_TO'] | ||
| 40 | away_id = EVENTS['ST_SWITCH_AWAY'] | ||
| 41 | |||
| 42 | # assumption: we are looking at a per-CPU trace | ||
| 43 | events = self.events_of_type(set((to_id, away_id))) | ||
| 44 | |||
| 45 | rec = events.next() | ||
| 46 | while True: | ||
| 47 | # fast-forward to next SWITCH_TO | ||
| 48 | while rec[0] != to_id: | ||
| 49 | rec = events.next() | ||
| 50 | |||
| 51 | to = rec | ||
| 52 | # next one on this CPU should be a SWITCH_AWAY | ||
| 53 | rec = events.next() | ||
| 54 | away = rec | ||
| 55 | # check for event ID and matching PID and CPU and monotonic time | ||
| 56 | if away[0] == away_id and away[1] == to[1] and away[2] == to[2] \ | ||
| 57 | and to[4] <= away[4]: | ||
| 58 | yield (to, away) | ||
diff --git a/sched_trace/format.py b/sched_trace/format.py new file mode 100644 index 0000000..ca44a9a --- /dev/null +++ b/sched_trace/format.py | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | """Functions for reading and interpreting binary sched_trace files. | ||
| 2 | """ | ||
| 3 | |||
| 4 | import struct | ||
| 5 | |||
| 6 | HEADER_LEN = 8 # bytes | ||
| 7 | PAYLOAD_LEN = 16 # bytes | ||
| 8 | RECORD_LEN = 24 # bytes | ||
| 9 | |||
| 10 | EVENT_NAMES = [ | ||
| 11 | "ST_NAME", | ||
| 12 | "ST_PARAM", | ||
| 13 | "ST_RELEASE", | ||
| 14 | "ST_ASSIGNED", | ||
| 15 | "ST_SWITCH_TO", | ||
| 16 | "ST_SWITCH_AWAY", | ||
| 17 | "ST_COMPLETION", | ||
| 18 | "ST_BLOCK", | ||
| 19 | "ST_RESUME", | ||
| 20 | "ST_ACTION", | ||
| 21 | "ST_SYS_RELEASE" | ||
| 22 | ] | ||
| 23 | |||
| 24 | EVENTS = {} | ||
| 25 | for i, ev in enumerate(EVENT_NAMES): | ||
| 26 | EVENTS[ev] = i + 1 | ||
| 27 | EVENTS[i + 1] = ev | ||
| 28 | |||
| 29 | def event(id): | ||
| 30 | return EVENTS[id] if id in EVENTS else id | ||
| 31 | |||
| 32 | # struct st_trace_header { | ||
| 33 | # u8 type; /* Of what type is this record? */ | ||
| 34 | # u8 cpu; /* On which CPU was it recorded? */ | ||
| 35 | # u16 pid; /* PID of the task. */ | ||
| 36 | # u32 job; /* The job sequence number. */ | ||
| 37 | # }; | ||
| 38 | |||
| 39 | def get_event(mmaped_trace, index): | ||
| 40 | offset = index * RECORD_LEN | ||
| 41 | # first byte in the record is the event type ID | ||
| 42 | return ord(mmaped_trace[offset]) | ||
| 43 | # return struct.unpack("B", raw_data[:1])[0] | ||
| 44 | |||
| 45 | def unpack_header(raw_data): | ||
| 46 | """Returns (type, cpu, pid, job id)""" | ||
| 47 | return struct.unpack("BBHI", raw_data[:HEADER_LEN]) | ||
| 48 | |||
| 49 | def unpack_release(raw_data): | ||
| 50 | """Returns (release, deadline)""" | ||
| 51 | return struct.unpack("QQ", raw_data[HEADER_LEN:RECORD_LEN]) | ||
| 52 | |||
| 53 | def unpack_switch_to(raw_data): | ||
| 54 | """Returns (when, exec_time_so_far)""" | ||
| 55 | return struct.unpack("QI", raw_data[HEADER_LEN:RECORD_LEN - 4]) | ||
| 56 | |||
| 57 | def unpack_switch_away(raw_data): | ||
| 58 | """Returns (when, exec_time_so_far)""" | ||
| 59 | return struct.unpack("QQ", raw_data[HEADER_LEN:RECORD_LEN]) | ||
| 60 | |||
| 61 | def unpack_completion(raw_data): | ||
| 62 | """Returns (when, exec_time, forced)""" | ||
| 63 | (when, exec_time) = struct.unpack("QQ", raw_data[HEADER_LEN:RECORD_LEN]) | ||
| 64 | return (when, exec_time >> 1, exec_time & 0x1) | ||
| 65 | |||
| 66 | def unpack_block(raw_data): | ||
| 67 | """Returns (when,)""" | ||
| 68 | return struct.unpack("Q", raw_data[HEADER_LEN:RECORD_LEN - 8]) | ||
| 69 | |||
| 70 | def unpack_resume(raw_data): | ||
| 71 | """Returns (when,)""" | ||
| 72 | return struct.unpack("Q", raw_data[HEADER_LEN:RECORD_LEN - 8]) | ||
| 73 | |||
| 74 | def unpack_sys_release(raw_data): | ||
| 75 | """Returns (when, release)""" | ||
| 76 | return struct.unpack("QQ", raw_data[HEADER_LEN:RECORD_LEN]) | ||
| 77 | |||
| 78 | def unpack_name(raw_data): | ||
| 79 | """Returns (name,)""" | ||
| 80 | data = raw_data[HEADER_LEN:RECORD_LEN] | ||
| 81 | null_byte_idx = data.find('\0') | ||
| 82 | if null_byte_idx > -1: | ||
| 83 | data = data[:null_byte_idx] | ||
| 84 | return (data,) | ||
| 85 | |||
| 86 | def unpack_param(raw_data): | ||
| 87 | """Returns (WCET, period, phase, partition)""" | ||
| 88 | return struct.unpack("IIIB", raw_data[HEADER_LEN:RECORD_LEN - 3]) | ||
| 89 | |||
| 90 | UNPACK = { | ||
| 91 | EVENTS['ST_RELEASE']: unpack_release, | ||
| 92 | EVENTS['ST_SWITCH_TO']: unpack_switch_to, | ||
| 93 | EVENTS['ST_SWITCH_AWAY']: unpack_switch_away, | ||
| 94 | EVENTS['ST_COMPLETION']: unpack_completion, | ||
| 95 | EVENTS['ST_BLOCK']: unpack_block, | ||
| 96 | EVENTS['ST_RESUME']: unpack_resume, | ||
| 97 | EVENTS['ST_SYS_RELEASE']: unpack_sys_release, | ||
| 98 | EVENTS['ST_NAME']: unpack_name, | ||
| 99 | EVENTS['ST_PARAM']: unpack_param, | ||
| 100 | } | ||
| 101 | |||
| 102 | EVENTS_WITHOUT_TIMESTAMP = frozenset([ | ||
| 103 | EVENTS['ST_NAME'], | ||
| 104 | EVENTS['ST_PARAM'], | ||
| 105 | ]) | ||
| 106 | |||
| 107 | def when(raw_data): | ||
| 108 | ev = get_event(raw_data) | ||
| 109 | if ev in EVENTS_WITHOUT_TIMESTAMP: | ||
| 110 | return 0 | ||
| 111 | else: | ||
| 112 | # always first element for all others | ||
| 113 | return UNPACK[ev](raw_data)[0] | ||
| 114 | |||
| 115 | def unpack(raw_data): | ||
| 116 | hdr = unpack_header(raw_data) | ||
| 117 | payload = UNPACK[hdr[0]](raw_data) | ||
| 118 | return hdr + payload | ||
| @@ -0,0 +1,142 @@ | |||
| 1 | #!/usr/bin/env python | ||
| 2 | |||
| 3 | import argparse | ||
| 4 | import sys | ||
| 5 | import os.path | ||
| 6 | |||
| 7 | import cairo | ||
| 8 | |||
| 9 | from sched_trace import event_name, event_time, SchedTrace | ||
| 10 | |||
| 11 | import sched_trace.draw | ||
| 12 | |||
| 13 | def s2ns(secs): | ||
| 14 | return secs * 1E9 | ||
| 15 | |||
| 16 | def ms2ns(millis): | ||
| 17 | return millis * 1E6 | ||
| 18 | |||
| 19 | def ns2s(nanos): | ||
| 20 | return nanos / 1E9 | ||
| 21 | |||
| 22 | def parse_args(): | ||
| 23 | parser = argparse.ArgumentParser( | ||
| 24 | description="Render a LITMUS^RT schedule trace as a PDF.") | ||
| 25 | |||
| 26 | parser.add_argument('-o', '--output', | ||
| 27 | help='name of the generated PDF file') | ||
| 28 | |||
| 29 | parser.add_argument('-x', '--xscale', type=float, default=36, | ||
| 30 | help="how many points (1/72'') per millisecond?") | ||
| 31 | parser.add_argument('-y', '--yscale', type=float, default=72, | ||
| 32 | help="how many points (1/72'') per task/core?") | ||
| 33 | parser.add_argument('--margin', type=float, default=36, | ||
| 34 | help="how many points (1/72'') of margin around the schedule?") | ||
| 35 | |||
| 36 | parser.add_argument('--major-ticks', type=float, default=10, | ||
| 37 | help="how many milliseconds per major tick? (zero means off)") | ||
| 38 | parser.add_argument('--minor-ticks', type=float, default=1, | ||
| 39 | help="how many milliseconds per minor tick? (zero means off)") | ||
| 40 | |||
| 41 | parser.add_argument('-f', '--from', type=float, dest='start', | ||
| 42 | help='draw schedule starting at time TIME', metavar='TIME') | ||
| 43 | parser.add_argument('-u', '--until', type=float, dest='end', | ||
| 44 | help='draw schedule up to time TIME', metavar='TIME') | ||
| 45 | parser.add_argument('-l', '--length', type=float, default=1000, | ||
| 46 | help='draw schedule for LENGTH time units') | ||
| 47 | parser.add_argument('-r', '--relative', action='store_true', | ||
| 48 | help='interpret -f/-u options relative to system release') | ||
| 49 | |||
| 50 | parser.add_argument('-v', '--verbose', action='store_true', default=False, | ||
| 51 | help='output some information while drawing') | ||
| 52 | |||
| 53 | parser.add_argument('file', nargs='*', | ||
| 54 | help='the binary trace files collected with st-trace') | ||
| 55 | |||
| 56 | return parser.parse_args() | ||
| 57 | |||
| 58 | |||
| 59 | def default_output_name(input_files): | ||
| 60 | if not input_files: | ||
| 61 | return 'schedule.pdf' | ||
| 62 | candidate = input_files[0] | ||
| 63 | for i in xrange(len(candidate)): | ||
| 64 | if not all([f[i] == candidate[i] for f in input_files]): | ||
| 65 | break | ||
| 66 | candidate = os.path.basename(candidate[:i]) | ||
| 67 | if candidate.endswith('_cpu='): | ||
| 68 | candidate = candidate.replace('_cpu=', '') | ||
| 69 | if not candidate: | ||
| 70 | return 'schedule.pdf' | ||
| 71 | return candidate + '.pdf' | ||
| 72 | |||
| 73 | def main(args=sys.argv[1:]): | ||
| 74 | opts = parse_args() | ||
| 75 | |||
| 76 | if not opts.output: | ||
| 77 | opts.output = default_output_name(opts.file) | ||
| 78 | |||
| 79 | try: | ||
| 80 | trace = SchedTrace(opts.file) | ||
| 81 | except IOError, msg: | ||
| 82 | print 'Could not load trace files (%s)' % msg | ||
| 83 | sys.exit(1) | ||
| 84 | |||
| 85 | if opts.start: | ||
| 86 | # convert from ms | ||
| 87 | opts.start = ms2ns(opts.start) | ||
| 88 | # check if a relative time is given | ||
| 89 | if opts.relative: | ||
| 90 | if trace.system_releases: | ||
| 91 | opts.start += trace.system_releases[0] | ||
| 92 | else: | ||
| 93 | opts.start += trace.find_earliest_event | ||
| 94 | |||
| 95 | if opts.end: | ||
| 96 | # convert from ms | ||
| 97 | opts.end = ms2ns(opts.end) | ||
| 98 | # check if a relative time is given | ||
| 99 | if opts.relative: | ||
| 100 | if trace.system_releases: | ||
| 101 | opts.end += trace.system_releases[0] | ||
| 102 | else: | ||
| 103 | opts.end += trace.find_earliest_event | ||
| 104 | |||
| 105 | if not opts.start: | ||
| 106 | if opts.end and opts.length: | ||
| 107 | opts.start = opts.end - ms2ns(opts.length) | ||
| 108 | elif trace.system_releases: | ||
| 109 | opts.start = trace.system_releases[0] | ||
| 110 | else: | ||
| 111 | opts.start = trace.find_earliest_event | ||
| 112 | |||
| 113 | if not opts.end: | ||
| 114 | if opts.start and opts.length: | ||
| 115 | opts.end = opts.start + ms2ns(opts.length) | ||
| 116 | else: | ||
| 117 | opts.end = trace.latest_event_time | ||
| 118 | |||
| 119 | if opts.verbose: | ||
| 120 | print '[II] Rendering into file: %s' % opts.output | ||
| 121 | print '[II] Drawing %.2f seconds from time %.4fs until time %.4fs.' % \ | ||
| 122 | (ns2s(opts.end - opts.start), ns2s(opts.start), ns2s(opts.end)) | ||
| 123 | if len(trace.system_releases) == 1: | ||
| 124 | print '[II] The trace contains a system release at time %.4fs.' % \ | ||
| 125 | (ns2s(trace.system_releases[0])) | ||
| 126 | elif len(trace.system_releases) > 1: | ||
| 127 | print '[!!] The trace contains multiple system releases: %s' % \ | ||
| 128 | (' '.join(['%.4fs' % ns2s(x) for x in trace.system_releases])) | ||
| 129 | else: | ||
| 130 | print '[II] The trace does not contain a system release.' | ||
| 131 | |||
| 132 | sched_trace.draw.render(opts, trace) | ||
| 133 | |||
| 134 | # for pid in trace.task_names: | ||
| 135 | # print pid, '->', trace.task_names[pid], trace.task_wcets[pid], trace.task_periods[pid] | ||
| 136 | |||
| 137 | # for (to, away) in trace.scheduling_intervals_in_range(trace.system_releases[0], trace.system_releases[0] + 1E9): | ||
| 138 | # print to, away | ||
| 139 | |||
| 140 | |||
| 141 | if __name__ == '__main__': | ||
| 142 | main() | ||
