cpu: `Minor' in-order CPU model
[gem5.git] / util / minorview / model.py
1 # Copyright (c) 2013 ARM Limited
2 # All rights reserved
3 #
4 # The license below extends only to copyright in the software and shall
5 # not be construed as granting a license to any other intellectual
6 # property including but not limited to intellectual property relating
7 # to a hardware implementation of the functionality of the software
8 # licensed hereunder. You may use the software subject to the license
9 # terms below provided that you ensure that this notice is replicated
10 # unmodified and in its entirety in all distributions of the software,
11 # modified or unmodified, in source code or in binary form.
12 #
13 # Redistribution and use in source and binary forms, with or without
14 # modification, are permitted provided that the following conditions are
15 # met: redistributions of source code must retain the above copyright
16 # notice, this list of conditions and the following disclaimer;
17 # redistributions in binary form must reproduce the above copyright
18 # notice, this list of conditions and the following disclaimer in the
19 # documentation and/or other materials provided with the distribution;
20 # neither the name of the copyright holders nor the names of its
21 # contributors may be used to endorse or promote products derived from
22 # this software without specific prior written permission.
23 #
24 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 #
36 # Authors: Andrew Bardsley
37
38 import parse
39 import colours
40 from colours import unknownColour
41 from point import Point
42 import re
43 import blobs
44 from time import time as wall_time
45 import os
46
47 id_parts = "TSPLFE"
48
49 all_ids = set(id_parts)
50 no_ids = set([])
51
52 class BlobDataSelect(object):
53 """Represents which data is displayed for Ided object"""
54 def __init__(self):
55 # Copy all_ids
56 self.ids = set(all_ids)
57
58 def __and__(self, rhs):
59 """And for filtering"""
60 ret = BlobDataSelect()
61 ret.ids = self.ids.intersection(rhs.ids)
62 return ret
63
64 class BlobVisualData(object):
65 """Super class for block data colouring"""
66 def to_striped_block(self, select):
67 """Return an array of colours to use for a striped block"""
68 return unknownColour
69
70 def get_inst(self):
71 """Get an instruction Id (if any) from this data"""
72 return None
73
74 def get_line(self):
75 """Get a line Id (if any) from this data"""
76 return None
77
78 def __repr__(self):
79 return self.__class__.__name__ + '().from_string(' + \
80 self.__str__() + ')'
81
82 def __str__(self):
83 return ''
84
85 class Id(BlobVisualData):
86 """A line or instruction id"""
87 def __init__(self):
88 self.isFault = False
89 self.threadId = 0
90 self.streamSeqNum = 0
91 self.predictionSeqNum = 0
92 self.lineSeqNum = 0
93 self.fetchSeqNum = 0
94 self.execSeqNum = 0
95
96 def as_list(self):
97 return [self.threadId, self.streamSeqNum, self.predictionSeqNum,
98 self.lineSeqNum, self.fetchSeqNum, self.execSeqNum]
99
100 def __cmp__(self, right):
101 return cmp(self.as_list(), right.as_list())
102
103 def from_string(self, string):
104 m = re.match('^(F;)?(\d+)/(\d+)\.(\d+)/(\d+)(/(\d+)(\.(\d+))?)?',
105 string)
106
107 def seqnum_from_string(string):
108 if string is None:
109 return 0
110 else:
111 return int(string)
112
113 if m is None:
114 print 'Invalid Id string', string
115 else:
116 elems = m.groups()
117
118 if elems[0] is not None:
119 self.isFault = True
120 else:
121 self.isFault = False
122
123 self.threadId = seqnum_from_string(elems[1])
124 self.streamSeqNum = seqnum_from_string(elems[2])
125 self.predictionSeqNum = seqnum_from_string(elems[3])
126 self.lineSeqNum = seqnum_from_string(elems[4])
127 self.fetchSeqNum = seqnum_from_string(elems[6])
128 self.execSeqNum = seqnum_from_string(elems[8])
129 return self
130
131 def get_inst(self):
132 if self.fetchSeqNum != 0:
133 return self
134 else:
135 return None
136
137 def get_line(self):
138 return self
139
140 def __str__(self):
141 """Returns the usual id T/S.P/L/F.E string"""
142 return (
143 str(self.threadId) + '/' +
144 str(self.streamSeqNum) + '.' +
145 str(self.predictionSeqNum) + '/' +
146 str(self.lineSeqNum) + '/' +
147 str(self.fetchSeqNum) + '.' +
148 str(self.execSeqNum))
149
150 def to_striped_block(self, select):
151 ret = []
152
153 if self.isFault:
154 ret.append(colours.faultColour)
155
156 if 'T' in select.ids:
157 ret.append(colours.number_to_colour(self.threadId))
158 if 'S' in select.ids:
159 ret.append(colours.number_to_colour(self.streamSeqNum))
160 if 'P' in select.ids:
161 ret.append(colours.number_to_colour(self.predictionSeqNum))
162 if 'L' in select.ids:
163 ret.append(colours.number_to_colour(self.lineSeqNum))
164 if self.fetchSeqNum != 0 and 'F' in select.ids:
165 ret.append(colours.number_to_colour(self.fetchSeqNum))
166 if self.execSeqNum != 0 and 'E' in select.ids:
167 ret.append(colours.number_to_colour(self.execSeqNum))
168
169 if len(ret) == 0:
170 ret = [colours.unknownColour]
171
172 if self.isFault:
173 ret.append(colours.faultColour)
174
175 return ret
176
177 class Branch(BlobVisualData):
178 """Branch data new stream and prediction sequence numbers, a branch
179 reason and a new PC"""
180 def __init__(self):
181 self.newStreamSeqNum = 0
182 self.newPredictionSeqNum = 0
183 self.newPC = 0
184 self.reason = "NoBranch"
185 self.id = Id()
186
187 def from_string(self, string):
188 m = re.match('^(\w+);(\d+)\.(\d+);([0-9a-fA-Fx]+);(.*)$', string)
189
190 if m is not None:
191 self.reason, newStreamSeqNum, newPredictionSeqNum, \
192 newPC, id = m.groups()
193
194 self.newStreamSeqNum = int(newStreamSeqNum)
195 self.newPredictionSeqNum = int(newPredictionSeqNum)
196 self.newPC = int(newPC, 0)
197 self.id = special_view_decoder(Id)(id)
198 # self.branch = special_view_decoder(Branch)(branch)
199 else:
200 print "Bad Branch data:", string
201 return self
202
203 def to_striped_block(self, select):
204 return [colours.number_to_colour(self.newStreamSeqNum),
205 colours.number_to_colour(self.newPredictionSeqNum),
206 colours.number_to_colour(self.newPC)]
207
208 class Counts(BlobVisualData):
209 """Treat the input data as just a /-separated list of count values (or
210 just a single value)"""
211 def __init__(self):
212 self.counts = []
213
214 def from_string(self, string):
215 self.counts = map(int, re.split('/', string))
216 return self
217
218 def to_striped_block(self, select):
219 return map(colours.number_to_colour, self.counts)
220
221 class Colour(BlobVisualData):
222 """A fixed colour block, used for special colour decoding"""
223 def __init__(self, colour):
224 self.colour = colour
225
226 def to_striped_block(self, select):
227 return [self.colour]
228
229 class DcacheAccess(BlobVisualData):
230 """Data cache accesses [RW];id"""
231 def __init__(self):
232 self.direc = 'R'
233 self.id = Id()
234
235 def from_string(self, string):
236 self.direc, id = re.match('^([RW]);([^;]*);.*$', string).groups()
237 self.id.from_string(id)
238 return self
239
240 def get_inst(self):
241 return self.id
242
243 def to_striped_block(self, select):
244 if self.direc == 'R':
245 direc_colour = colours.readColour
246 elif self.direc == 'R':
247 direc_colour = colours.writeColour
248 else:
249 direc_colour = colours.errorColour
250 return [direc_colour] + self.id.to_striped_block(select)
251
252 class ColourPattern(object):
253 """Super class for decoders that make 2D grids rather than just single
254 striped blocks"""
255 def elems(self):
256 return []
257
258 def to_striped_block(self, select):
259 return [[[colours.errorColour]]]
260
261 def special_view_decoder(class_):
262 """Generate a decode function that checks for special character
263 arguments first (and generates a fixed colour) before building a
264 BlobVisualData of the given class"""
265 def decode(symbol):
266 if symbol in special_state_colours:
267 return Colour(special_state_colours[symbol])
268 else:
269 return class_().from_string(symbol)
270 return decode
271
272 class TwoDColours(ColourPattern):
273 """A 2D grid pattern decoder"""
274 def __init__(self, blockss):
275 self.blockss = blockss
276
277 @classmethod
278 def decoder(class_, elemClass, dataName):
279 """Factory for making decoders for particular block types"""
280 def decode(pairs):
281 if dataName not in pairs:
282 print 'TwoDColours: no event data called:', \
283 dataName, 'in:', pairs
284 return class_([[Colour(colours.errorColour)]])
285 else:
286 parsed = parse.list_parser(pairs[dataName])
287 return class_(parse.map2(special_view_decoder(elemClass), \
288 parsed))
289 return decode
290
291 @classmethod
292 def indexed_decoder(class_, elemClass, dataName, picPairs):
293 """Factory for making decoders for particular block types but
294 where the list elements are pairs of (index, data) and
295 strip and stripelems counts are picked up from the pair
296 data on the decoder's picture file. This gives a 2D layout
297 of the values with index 0 at strip=0, elem=0 and index 1
298 at strip=0, elem=1"""
299 def decode(pairs):
300 if dataName not in pairs:
301 print 'TwoDColours: no event data called:', \
302 dataName, 'in:', pairs
303 return class_([[Colour(colours.errorColour)]])
304 else:
305 strips = int(picPairs['strips'])
306 strip_elems = int(picPairs['stripelems'])
307
308 raw_iv_pairs = pairs[dataName]
309
310 parsed = parse.parse_indexed_list(raw_iv_pairs)
311
312 array = [[Colour(colours.emptySlotColour)
313 for i in xrange(0, strip_elems)]
314 for j in xrange(0, strips)]
315
316 for index, value in parsed:
317 try:
318 array[index % strips][index / strips] = \
319 special_view_decoder(elemClass)(value)
320 except:
321 print "Element out of range strips: %d," \
322 " stripelems %d, index: %d" % (strips,
323 strip_elems, index)
324
325 # return class_(array)
326 return class_(array)
327 return decode
328
329 def elems(self):
330 """Get a flat list of all elements"""
331 ret = []
332 for blocks in self.blockss:
333 ret += blocks
334 return ret
335
336 def to_striped_block(self, select):
337 return parse.map2(lambda d: d.to_striped_block(select), self.blockss)
338
339 class FrameColours(ColourPattern):
340 """Decode to a 2D grid which has a single occupied row from the event
341 data and some blank rows forming a frame with the occupied row as a
342 'title' coloured stripe"""
343 def __init__(self, block, numBlankSlots):
344 self.numBlankSlots = numBlankSlots
345 self.block = block
346
347 @classmethod
348 def decoder(class_, elemClass, numBlankSlots, dataName):
349 """Factory for element type"""
350 def decode(pairs):
351 if dataName not in pairs:
352 print 'FrameColours: no event data called:', dataName, \
353 'in:', pairs
354 return class_([Colour(colours.errorColour)])
355 else:
356 parsed = parse.list_parser(pairs[dataName])
357 return class_(special_view_decoder(elemClass)
358 (parsed[0][0]), numBlankSlots)
359 return decode
360
361 def elems(self):
362 return [self.block]
363
364 def to_striped_block(self, select):
365 return ([[self.block.to_striped_block(select)]] +
366 (self.numBlankSlots * [[[colours.backgroundColour]]]))
367
368 special_state_colours = {
369 'U': colours.unknownColour,
370 'B': colours.blockedColour,
371 '-': colours.bubbleColour,
372 '': colours.emptySlotColour,
373 'E': colours.emptySlotColour,
374 'R': colours.reservedSlotColour,
375 'X': colours.errorColour,
376 'F': colours.faultColour,
377 'r': colours.readColour,
378 'w': colours.writeColour
379 }
380
381 special_state_names = {
382 'U': '(U)nknown',
383 'B': '(B)locked',
384 '-': '(-)Bubble',
385 '': '()Empty',
386 'E': '(E)mpty',
387 'R': '(R)eserved',
388 'X': '(X)Error',
389 'F': '(F)ault',
390 'r': '(r)ead',
391 'w': '(w)rite'
392 }
393
394 special_state_chars = special_state_colours.keys()
395
396 # The complete set of available block data types
397 decoder_element_classes = {
398 'insts': Id,
399 'lines': Id,
400 'branch': Branch,
401 'dcache': DcacheAccess,
402 'counts': Counts
403 }
404
405 indexed_decoder_element_classes = {
406 'indexedCounts' : Counts
407 }
408
409 def find_colour_decoder(stripSpace, decoderName, dataName, picPairs):
410 """Make a colour decoder from some picture file blob attributes"""
411 if decoderName == 'frame':
412 return FrameColours.decoder(Counts, stripSpace, dataName)
413 elif decoderName in decoder_element_classes:
414 return TwoDColours.decoder(decoder_element_classes[decoderName],
415 dataName)
416 elif decoderName in indexed_decoder_element_classes:
417 return TwoDColours.indexed_decoder(
418 indexed_decoder_element_classes[decoderName], dataName, picPairs)
419 else:
420 return None
421
422 class IdedObj(object):
423 """An object identified by an Id carrying paired data.
424 The super class for Inst and Line"""
425
426 def __init__(self, id, pairs={}):
427 self.id = id
428 self.pairs = pairs
429
430 def __cmp__(self, right):
431 return cmp(self.id, right.id)
432
433 def table_line(self):
434 """Represent the object as a list of table row data"""
435 return []
436
437 # FIXME, add a table column titles?
438
439 def __repr__(self):
440 return ' '.join(self.table_line())
441
442 class Inst(IdedObj):
443 """A non-fault instruction"""
444 def __init__(self, id, disassembly, addr, pairs={}):
445 super(Inst,self).__init__(id, pairs)
446 if 'nextAddr' in pairs:
447 self.nextAddr = int(pairs['nextAddr'], 0)
448 del pairs['nextAddr']
449 else:
450 self.nextAddr = None
451 self.disassembly = disassembly
452 self.addr = addr
453
454 def table_line(self):
455 if self.nextAddr is not None:
456 addrStr = '0x%x->0x%x' % (self.addr, self.nextAddr)
457 else:
458 addrStr = '0x%x' % self.addr
459 ret = [addrStr, self.disassembly]
460 for name, value in self.pairs.iteritems():
461 ret.append("%s=%s" % (name, str(value)))
462 return ret
463
464 class InstFault(IdedObj):
465 """A fault instruction"""
466 def __init__(self, id, fault, addr, pairs={}):
467 super(InstFault,self).__init__(id, pairs)
468 self.fault = fault
469 self.addr = addr
470
471 def table_line(self):
472 ret = ["0x%x" % self.addr, self.fault]
473 for name, value in self.pairs:
474 ret.append("%s=%s", name, str(value))
475 return ret
476
477 class Line(IdedObj):
478 """A fetched line"""
479 def __init__(self, id, vaddr, paddr, size, pairs={}):
480 super(Line,self).__init__(id, pairs)
481 self.vaddr = vaddr
482 self.paddr = paddr
483 self.size = size
484
485 def table_line(self):
486 ret = ["0x%x/0x%x" % (self.vaddr, self.paddr), "%d" % self.size]
487 for name, value in self.pairs:
488 ret.append("%s=%s", name, str(value))
489 return ret
490
491 class LineFault(IdedObj):
492 """A faulting line"""
493 def __init__(self, id, fault, vaddr, pairs={}):
494 super(LineFault,self).__init__(id, pairs)
495 self.vaddr = vaddr
496 self.fault = fault
497
498 def table_line(self):
499 ret = ["0x%x" % self.vaddr, self.fault]
500 for name, value in self.pairs:
501 ret.append("%s=%s", name, str(value))
502 return ret
503
504 class BlobEvent(object):
505 """Time event for a single blob"""
506 def __init__(self, unit, time, pairs = {}):
507 # blob's unit name
508 self.unit = unit
509 self.time = time
510 # dict of picChar (blob name) to visual data
511 self.visuals = {}
512 # Miscellaneous unparsed MinorTrace line data
513 self.pairs = pairs
514 # Non-MinorTrace debug printout for this unit at this time
515 self.comments = []
516
517 def find_ided_objects(self, model, picChar, includeInstLines):
518 """Find instructions/lines mentioned in the blob's event
519 data"""
520 ret = []
521 if picChar in self.visuals:
522 blocks = self.visuals[picChar].elems()
523 def find_inst(data):
524 instId = data.get_inst()
525 lineId = data.get_line()
526 if instId is not None:
527 inst = model.find_inst(instId)
528 line = model.find_line(instId)
529 if inst is not None:
530 ret.append(inst)
531 if includeInstLines and line is not None:
532 ret.append(line)
533 elif lineId is not None:
534 line = model.find_line(lineId)
535 if line is not None:
536 ret.append(line)
537 map(find_inst, blocks)
538 return sorted(ret)
539
540 class BlobModel(object):
541 """Model bringing together blob definitions and parsed events"""
542 def __init__(self, unitNamePrefix=''):
543 self.blobs = []
544 self.unitNameToBlobs = {}
545 self.unitEvents = {}
546 self.clear_events()
547 self.picSize = Point(20,10)
548 self.lastTime = 0
549 self.unitNamePrefix = unitNamePrefix
550
551 def clear_events(self):
552 """Drop all events and times"""
553 self.lastTime = 0
554 self.times = []
555 self.insts = {}
556 self.lines = {}
557 self.numEvents = 0
558
559 for unit, events in self.unitEvents.iteritems():
560 self.unitEvents[unit] = []
561
562 def add_blob(self, blob):
563 """Add a parsed blob to the model"""
564 self.blobs.append(blob)
565 if blob.unit not in self.unitNameToBlobs:
566 self.unitNameToBlobs[blob.unit] = []
567
568 self.unitNameToBlobs[blob.unit].append(blob)
569
570 def add_inst(self, inst):
571 """Add a MinorInst instruction definition to the model"""
572 # Is this a non micro-op instruction. Microops (usually) get their
573 # fetchSeqNum == 0 varient stored first
574 macroop_key = (inst.id.fetchSeqNum, 0)
575 full_key = (inst.id.fetchSeqNum, inst.id.execSeqNum)
576
577 if inst.id.execSeqNum != 0 and macroop_key not in self.insts:
578 self.insts[macroop_key] = inst
579
580 self.insts[full_key] = inst
581
582 def find_inst(self, id):
583 """Find an instruction either as a microop or macroop"""
584 macroop_key = (id.fetchSeqNum, 0)
585 full_key = (id.fetchSeqNum, id.execSeqNum)
586
587 if full_key in self.insts:
588 return self.insts[full_key]
589 elif macroop_key in self.insts:
590 return self.insts[macroop_key]
591 else:
592 return None
593
594 def add_line(self, line):
595 """Add a MinorLine line to the model"""
596 self.lines[line.id.lineSeqNum] = line
597
598 def add_unit_event(self, event):
599 """Add a single event to the model. This must be an event at a
600 time >= the current maximum time"""
601 if event.unit in self.unitEvents:
602 events = self.unitEvents[event.unit]
603 if len(events) > 0 and events[len(events)-1].time > event.time:
604 print "Bad event ordering"
605 events.append(event)
606 self.numEvents += 1
607 self.lastTime = max(self.lastTime, event.time)
608
609 def extract_times(self):
610 """Extract a list of all the times from the seen events. Call after
611 reading events to give a safe index list to use for time indices"""
612 times = {}
613 for unitEvents in self.unitEvents.itervalues():
614 for event in unitEvents:
615 times[event.time] = 1
616 self.times = times.keys()
617 self.times.sort()
618
619 def find_line(self, id):
620 """Find a line by id"""
621 key = id.lineSeqNum
622 return self.lines.get(key, None)
623
624 def find_event_bisection(self, unit, time, events,
625 lower_index, upper_index):
626 """Find an event by binary search on time indices"""
627 while lower_index <= upper_index:
628 pivot = (upper_index + lower_index) / 2
629 pivotEvent = events[pivot]
630 event_equal = (pivotEvent.time == time or
631 (pivotEvent.time < time and
632 (pivot == len(events) - 1 or
633 events[pivot + 1].time > time)))
634
635 if event_equal:
636 return pivotEvent
637 elif time > pivotEvent.time:
638 if pivot == upper_index:
639 return None
640 else:
641 lower_index = pivot + 1
642 elif time < pivotEvent.time:
643 if pivot == lower_index:
644 return None
645 else:
646 upper_index = pivot - 1
647 else:
648 return None
649 return None
650
651 def find_unit_event_by_time(self, unit, time):
652 """Find the last event for the given unit at time <= time"""
653 if unit in self.unitEvents:
654 events = self.unitEvents[unit]
655 ret = self.find_event_bisection(unit, time, events,
656 0, len(events)-1)
657
658 return ret
659 else:
660 return None
661
662 def find_time_index(self, time):
663 """Find a time index close to the given time (where
664 times[return] <= time and times[return+1] > time"""
665 ret = 0
666 lastIndex = len(self.times) - 1
667 while ret < lastIndex and self.times[ret + 1] <= time:
668 ret += 1
669 return ret
670
671 def add_minor_inst(self, rest):
672 """Parse and add a MinorInst line to the model"""
673 pairs = parse.parse_pairs(rest)
674 other_pairs = dict(pairs)
675
676 id = Id().from_string(pairs['id'])
677 del other_pairs['id']
678
679 addr = int(pairs['addr'], 0)
680 del other_pairs['addr']
681
682 if 'inst' in other_pairs:
683 del other_pairs['inst']
684
685 # Collapse unnecessary spaces in disassembly
686 disassembly = re.sub(' *', ' ',
687 re.sub('^ *', '', pairs['inst']))
688
689 inst = Inst(id, disassembly, addr, other_pairs)
690 self.add_inst(inst)
691 elif 'fault' in other_pairs:
692 del other_pairs['fault']
693
694 inst = InstFault(id, pairs['fault'], addr, other_pairs)
695
696 self.add_inst(inst)
697
698 def add_minor_line(self, rest):
699 """Parse and add a MinorLine line to the model"""
700 pairs = parse.parse_pairs(rest)
701 other_pairs = dict(pairs)
702
703 id = Id().from_string(pairs['id'])
704 del other_pairs['id']
705
706 vaddr = int(pairs['vaddr'], 0)
707 del other_pairs['vaddr']
708
709 if 'paddr' in other_pairs:
710 del other_pairs['paddr']
711 del other_pairs['size']
712 paddr = int(pairs['paddr'], 0)
713 size = int(pairs['size'], 0)
714
715 self.add_line(Line(id,
716 vaddr, paddr, size, other_pairs))
717 elif 'fault' in other_pairs:
718 del other_pairs['fault']
719
720 self.add_line(LineFault(id, pairs['fault'], vaddr, other_pairs))
721
722 def load_events(self, file, startTime=0, endTime=None):
723 """Load an event file and add everything to this model"""
724 def update_comments(comments, time):
725 # Add a list of comments to an existing event, if there is one at
726 # the given time, or create a new, correctly-timed, event from
727 # the last event and attach the comments to that
728 for commentUnit, commentRest in comments:
729 event = self.find_unit_event_by_time(commentUnit, time)
730 # Find an event to which this comment can be attached
731 if event is None:
732 # No older event, make a new empty one
733 event = BlobEvent(commentUnit, time, {})
734 self.add_unit_event(event)
735 elif event.time != time:
736 # Copy the old event and make a new one with the right
737 # time and comment
738 newEvent = BlobEvent(commentUnit, time, event.pairs)
739 newEvent.visuals = dict(event.visuals)
740 event = newEvent
741 self.add_unit_event(event)
742 event.comments.append(commentRest)
743
744 self.clear_events()
745
746 # A negative time will *always* be different from an event time
747 time = -1
748 time_events = {}
749 last_time_lines = {}
750 minor_trace_line_count = 0
751 comments = []
752
753 default_colour = [[colours.unknownColour]]
754 next_progress_print_event_count = 1000
755
756 if not os.access(file, os.R_OK):
757 print 'Can\'t open file', file
758 exit(1)
759 else:
760 print 'Opening file', file
761
762 f = open(file)
763
764 start_wall_time = wall_time()
765
766 # Skip leading events
767 still_skipping = True
768 l = f.readline()
769 while l and still_skipping:
770 match = re.match('^\s*(\d+):', l)
771 if match is not None:
772 event_time = match.groups()
773 if int(event_time[0]) >= startTime:
774 still_skipping = False
775 else:
776 l = f.readline()
777 else:
778 l = f.readline()
779
780 match_line_re = re.compile(
781 '^\s*(\d+):\s*([\w\.]+):\s*(Minor\w+:)?\s*(.*)$')
782
783 # Parse each line of the events file, accumulating comments to be
784 # attached to MinorTrace events when the time changes
785 reached_end_time = False
786 while not reached_end_time and l:
787 match = match_line_re.match(l)
788 if match is not None:
789 event_time, unit, line_type, rest = match.groups()
790 event_time = int(event_time)
791
792 unit = re.sub('^' + self.unitNamePrefix + '\.?(.*)$',
793 '\\1', unit)
794
795 # When the time changes, resolve comments
796 if event_time != time:
797 if self.numEvents > next_progress_print_event_count:
798 print ('Parsed to time: %d' % event_time)
799 next_progress_print_event_count = (
800 self.numEvents + 1000)
801 update_comments(comments, time)
802 comments = []
803 time = event_time
804
805 if line_type is None:
806 # Treat this line as just a 'comment'
807 comments.append((unit, rest))
808 elif line_type == 'MinorTrace:':
809 minor_trace_line_count += 1
810
811 # Only insert this event if it's not the same as
812 # the last event we saw for this unit
813 if last_time_lines.get(unit, None) != rest:
814 event = BlobEvent(unit, event_time, {})
815 pairs = parse.parse_pairs(rest)
816 event.pairs = pairs
817
818 # Try to decode the colour data for this event
819 blobs = self.unitNameToBlobs.get(unit, [])
820 for blob in blobs:
821 if blob.visualDecoder is not None:
822 event.visuals[blob.picChar] = (
823 blob.visualDecoder(pairs))
824
825 self.add_unit_event(event)
826 last_time_lines[unit] = rest
827 elif line_type == 'MinorInst:':
828 self.add_minor_inst(rest)
829 elif line_type == 'MinorLine:':
830 self.add_minor_line(rest)
831
832 if endTime is not None and time > endTime:
833 reached_end_time = True
834
835 l = f.readline()
836
837 update_comments(comments, time)
838 self.extract_times()
839 f.close()
840
841 end_wall_time = wall_time()
842
843 print 'Total events:', minor_trace_line_count, 'unique events:', \
844 self.numEvents
845 print 'Time to parse:', end_wall_time - start_wall_time
846
847 def add_blob_picture(self, offset, pic, nameDict):
848 """Add a parsed ASCII-art pipeline markup to the model"""
849 pic_width = 0
850 for line in pic:
851 pic_width = max(pic_width, len(line))
852 pic_height = len(pic)
853
854 # Number of horizontal characters per 'pixel'. Should be 2
855 charsPerPixel = 2
856
857 # Clean up pic_width to a multiple of charsPerPixel
858 pic_width = (pic_width + charsPerPixel - 1) // 2
859
860 self.picSize = Point(pic_width, pic_height)
861
862 def pic_at(point):
863 """Return the char pair at the given point.
864 Returns None for characters off the picture"""
865 x, y = point.to_pair()
866 x *= 2
867 if y >= len(pic) or x >= len(pic[y]):
868 return None
869 else:
870 return pic[y][x:x + charsPerPixel]
871
872 def clear_pic_at(point):
873 """Clear the chars at point so we don't trip over them again"""
874 line = pic[point.y]
875 x = point.x * charsPerPixel
876 pic[point.y] = line[0:x] + (' ' * charsPerPixel) + \
877 line[x + charsPerPixel:]
878
879 def skip_same_char(start, increment):
880 """Skip characters which match pic_at(start)"""
881 char = pic_at(start)
882 hunt = start
883 while pic_at(hunt) == char:
884 hunt += increment
885 return hunt
886
887 def find_size(start):
888 """Find the size of a rectangle with top left hand corner at
889 start consisting of (at least) a -. shaped corner describing
890 the top right corner of a rectangle of the same char"""
891 char = pic_at(start)
892 hunt_x = skip_same_char(start, Point(1,0))
893 hunt_y = skip_same_char(start, Point(0,1))
894 off_bottom_right = (hunt_x * Point(1,0)) + (hunt_y * Point(0,1))
895 return off_bottom_right - start
896
897 def point_return(point):
898 """Carriage return, line feed"""
899 return Point(0, point.y + 1)
900
901 def find_arrow(start):
902 """Find a simple 1-char wide arrow"""
903
904 def body(endChar, contChar, direc):
905 arrow_point = start
906 arrow_point += Point(0, 1)
907 clear_pic_at(start)
908 while pic_at(arrow_point) == contChar:
909 clear_pic_at(arrow_point)
910 arrow_point += Point(0, 1)
911
912 if pic_at(arrow_point) == endChar:
913 clear_pic_at(arrow_point)
914 self.add_blob(blobs.Arrow('_', start + offset,
915 direc = direc,
916 size = (Point(1, 1) + arrow_point - start)))
917 else:
918 print 'Bad arrow', start
919
920 char = pic_at(start)
921 if char == '-\\':
922 body('-/', ' :', 'right')
923 elif char == '/-':
924 body('\\-', ': ', 'left')
925
926 blank_chars = [' ', ' :', ': ']
927
928 # Traverse the picture left to right, top to bottom to find blobs
929 seen_dict = {}
930 point = Point(0,0)
931 while pic_at(point) is not None:
932 while pic_at(point) is not None:
933 char = pic_at(point)
934 if char == '->':
935 self.add_blob(blobs.Arrow('_', point + offset,
936 direc = 'right'))
937 elif char == '<-':
938 self.add_blob(blobs.Arrow('_', point + offset,
939 direc = 'left'))
940 elif char == '-\\' or char == '/-':
941 find_arrow(point)
942 elif char in blank_chars:
943 pass
944 else:
945 if char not in seen_dict:
946 size = find_size(point)
947 topLeft = point + offset
948 if char not in nameDict:
949 # Unnamed blobs
950 self.add_blob(blobs.Block(char,
951 nameDict.get(char, '_'),
952 topLeft, size = size))
953 else:
954 # Named blobs, set visual info.
955 blob = nameDict[char]
956 blob.size = size
957 blob.topLeft = topLeft
958 self.add_blob(blob)
959 seen_dict[char] = True
960 point = skip_same_char(point, Point(1,0))
961 point = point_return(point)
962
963 def load_picture(self, filename):
964 """Load a picture file into the model"""
965 def parse_blob_description(char, unit, macros, pairsList):
966 # Parse the name value pairs in a blob-describing line
967 def expand_macros(pairs, newPairs):
968 # Recursively expand macros
969 for name, value in newPairs:
970 if name in macros:
971 expand_macros(pairs, macros[name])
972 else:
973 pairs[name] = value
974 return pairs
975
976 pairs = expand_macros({}, pairsList)
977
978 ret = None
979
980 typ = pairs.get('type', 'block')
981 colour = colours.name_to_colour(pairs.get('colour', 'black'))
982
983 if typ == 'key':
984 ret = blobs.Key(char, unit, Point(0,0), colour)
985 elif typ == 'block':
986 ret = blobs.Block(char, unit, Point(0,0), colour)
987 else:
988 print "Bad picture blog type:", typ
989
990 if 'hideId' in pairs:
991 hide = pairs['hideId']
992 ret.dataSelect.ids -= set(hide)
993
994 if typ == 'block':
995 ret.displayName = pairs.get('name', unit)
996 ret.nameLoc = pairs.get('nameLoc', 'top')
997 ret.shape = pairs.get('shape', 'box')
998 ret.stripDir = pairs.get('stripDir', 'horiz')
999 ret.stripOrd = pairs.get('stripOrd', 'LR')
1000 ret.blankStrips = int(pairs.get('blankStrips', '0'))
1001 ret.shorten = int(pairs.get('shorten', '0'))
1002
1003 if 'decoder' in pairs:
1004 decoderName = pairs['decoder']
1005 dataElement = pairs.get('dataElement', decoderName)
1006
1007 decoder = find_colour_decoder(ret.blankStrips,
1008 decoderName, dataElement, pairs)
1009 if decoder is not None:
1010 ret.visualDecoder = decoder
1011 else:
1012 print 'Bad visualDecoder requested:', decoderName
1013
1014 if 'border' in pairs:
1015 border = pairs['border']
1016 if border == 'thin':
1017 ret.border = 0.2
1018 elif border == 'mid':
1019 ret.border = 0.5
1020 else:
1021 ret.border = 1.0
1022 elif typ == 'key':
1023 ret.colours = pairs.get('colours', ret.colours)
1024
1025 return ret
1026
1027 def line_is_comment(line):
1028 """Returns true if a line starts with #, returns False
1029 for lines which are None"""
1030 return line is not None \
1031 and re.match('^\s*#', line) is not None
1032
1033 def get_line(f):
1034 """Get a line from file f extending that line if it ends in
1035 '\' and dropping lines that start with '#'s"""
1036 ret = f.readline()
1037
1038 # Discard comment lines
1039 while line_is_comment(ret):
1040 ret = f.readline()
1041
1042 if ret is not None:
1043 extend_match = re.match('^(.*)\\\\$', ret)
1044
1045 while extend_match is not None:
1046 new_line = f.readline()
1047
1048 if new_line is not None and not line_is_comment(new_line):
1049 line_wo_backslash, = extend_match.groups()
1050 ret = line_wo_backslash + new_line
1051 extend_match = re.match('^(.*)\\\\$', ret)
1052 else:
1053 extend_match = None
1054
1055 return ret
1056
1057 # Macros are recursively expanded into name=value pairs
1058 macros = {}
1059
1060 if not os.access(filename, os.R_OK):
1061 print 'Can\'t open file', filename
1062 exit(1)
1063 else:
1064 print 'Opening file', filename
1065
1066 f = open(filename)
1067 l = get_line(f)
1068 picture = []
1069 blob_char_dict = {}
1070
1071 self.unitEvents = {}
1072 self.clear_events()
1073
1074 # Actually parse the file
1075 in_picture = False
1076 while l:
1077 l = parse.remove_trailing_ws(l)
1078 l = re.sub('#.*', '', l)
1079
1080 if re.match("^\s*$", l) is not None:
1081 pass
1082 elif l == '<<<':
1083 in_picture = True
1084 elif l == '>>>':
1085 in_picture = False
1086 elif in_picture:
1087 picture.append(re.sub('\s*$', '', l))
1088 else:
1089 line_match = re.match(
1090 '^([a-zA-Z0-9][a-zA-Z0-9]):\s+([\w.]+)\s*(.*)', l)
1091 macro_match = re.match('macro\s+(\w+):(.*)', l)
1092
1093 if macro_match is not None:
1094 name, defn = macro_match.groups()
1095 macros[name] = parse.parse_pairs_list(defn)
1096 elif line_match is not None:
1097 char, unit, pairs = line_match.groups()
1098 blob = parse_blob_description(char, unit, macros,
1099 parse.parse_pairs_list(pairs))
1100 blob_char_dict[char] = blob
1101 # Setup the events structure
1102 self.unitEvents[unit] = []
1103 else:
1104 print 'Problem with Blob line:', l
1105
1106 l = get_line(f)
1107
1108 self.blobs = []
1109 self.add_blob_picture(Point(0,1), picture, blob_char_dict)