| #!/usr/bin/env python2.7 |
| |
| # Copyright (c) 2012, 2014 ARM Limited |
| # All rights reserved |
| # |
| # The license below extends only to copyright in the software and shall |
| # not be construed as granting a license to any other intellectual |
| # property including but not limited to intellectual property relating |
| # to a hardware implementation of the functionality of the software |
| # licensed hereunder. You may use the software subject to the license |
| # terms below provided that you ensure that this notice is replicated |
| # unmodified and in its entirety in all distributions of the software, |
| # modified or unmodified, in source code or in binary form. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer; |
| # redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution; |
| # neither the name of the copyright holders nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| # |
| # Author: Dam Sunwoo |
| # |
| |
| # This script converts gem5 output to ARM DS-5 Streamline .apc project file |
| # (Requires the gem5 runs to be run with ContextSwitchStatsDump enabled and |
| # some patches applied to target Linux kernel.) |
| # |
| # Usage: |
| # m5stats2streamline.py <stat_config.ini> <gem5 run folder> <dest .apc folder> |
| # |
| # <stat_config.ini>: .ini file that describes which stats to be included |
| # in conversion. Sample .ini files can be found in |
| # util/streamline. |
| # NOTE: this is NOT the gem5 config.ini file. |
| # |
| # <gem5 run folder>: Path to gem5 run folder (must contain config.ini, |
| # stats.txt[.gz], and system.tasks.txt.) |
| # |
| # <dest .apc folder>: Destination .apc folder path |
| # |
| # APC project generation based on Gator v17 (DS-5 v5.17) |
| # Subsequent versions should be backward compatible |
| |
| import re, sys, os |
| from ConfigParser import ConfigParser |
| import gzip |
| import xml.etree.ElementTree as ET |
| import xml.dom.minidom as minidom |
| import shutil |
| import zlib |
| |
| import argparse |
| |
| parser = argparse.ArgumentParser( |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| description=""" |
| Converts gem5 runs to ARM DS-5 Streamline .apc project file. |
| (NOTE: Requires gem5 runs to be run with ContextSwitchStatsDump |
| enabled and some patches applied to the target Linux kernel.) |
| |
| APC project generation based on Gator v17 (DS-5 v5.17) |
| Subsequent versions should be backward compatible |
| """) |
| |
| parser.add_argument("stat_config_file", metavar="<stat_config.ini>", |
| help=".ini file that describes which stats to be included \ |
| in conversion. Sample .ini files can be found in \ |
| util/streamline. NOTE: this is NOT the gem5 config.ini \ |
| file.") |
| |
| parser.add_argument("input_path", metavar="<gem5 run folder>", |
| help="Path to gem5 run folder (must contain config.ini, \ |
| stats.txt[.gz], and system.tasks.txt.)") |
| |
| parser.add_argument("output_path", metavar="<dest .apc folder>", |
| help="Destination .apc folder path") |
| |
| parser.add_argument("--num-events", action="store", type=int, |
| default=1000000, |
| help="Maximum number of scheduling (context switch) \ |
| events to be processed. Set to truncate early. \ |
| Default=1000000") |
| |
| parser.add_argument("--gzipped-bmp-not-supported", action="store_true", |
| help="Do not use gzipped .bmp files for visual annotations. \ |
| This option is only required when using Streamline versions \ |
| older than 5.14") |
| |
| parser.add_argument("--verbose", action="store_true", |
| help="Enable verbose output") |
| |
| args = parser.parse_args() |
| |
| if not re.match("(.*)\.apc", args.output_path): |
| print "ERROR: <dest .apc folder> should end with '.apc'!" |
| sys.exit(1) |
| |
| # gzipped BMP files for visual annotation is supported in Streamline 5.14. |
| # Setting this to True will significantly compress the .apc binary file that |
| # includes frame buffer snapshots. |
| gzipped_bmp_supported = not args.gzipped_bmp_not_supported |
| |
| ticks_in_ns = -1 |
| |
| # Default max # of events. Increase this for longer runs. |
| num_events = args.num_events |
| |
| start_tick = -1 |
| end_tick = -1 |
| |
| # Parse gem5 config.ini file to determine some system configurations. |
| # Number of CPUs, L2s, etc. |
| def parseConfig(config_file): |
| global num_cpus, num_l2 |
| |
| print "\n===============================" |
| print "Parsing gem5 config.ini file..." |
| print config_file |
| print "===============================\n" |
| config = ConfigParser() |
| if not config.read(config_file): |
| print "ERROR: config file '", config_file, "' not found" |
| sys.exit(1) |
| |
| if config.has_section("system.cpu"): |
| num_cpus = 1 |
| else: |
| num_cpus = 0 |
| while config.has_section("system.cpu" + str(num_cpus)): |
| num_cpus += 1 |
| |
| if config.has_section("system.l2_cache"): |
| num_l2 = 1 |
| else: |
| num_l2 = 0 |
| while config.has_section("system.l2_cache" + str(num_l2)): |
| num_l2 += 1 |
| |
| print "Num CPUs:", num_cpus |
| print "Num L2s:", num_l2 |
| print "" |
| |
| return (num_cpus, num_l2) |
| |
| |
| process_dict = {} |
| thread_dict = {} |
| |
| process_list = [] |
| |
| idle_uid = -1 |
| kernel_uid = -1 |
| |
| class Task(object): |
| def __init__(self, uid, pid, tgid, task_name, is_process, tick): |
| if pid == 0: # Idle |
| self.uid = 0 |
| elif pid == -1: # Kernel |
| self.uid = 0 |
| else: |
| self.uid = uid |
| self.pid = pid |
| self.tgid = tgid |
| self.is_process = is_process |
| self.task_name = task_name |
| self.children = [] |
| self.tick = tick # time this task first appeared |
| |
| class Event(object): |
| def __init__(self, tick, task): |
| self.tick = tick |
| self.task = task |
| |
| ############################################################ |
| # Types used in APC Protocol |
| # - packed32, packed64 |
| # - int32 |
| # - string |
| ############################################################ |
| |
| def packed32(x): |
| ret = [] |
| more = True |
| while more: |
| b = x & 0x7f |
| x = x >> 7 |
| if (((x == 0) and ((b & 0x40) == 0)) or \ |
| ((x == -1) and ((b & 0x40) != 0))): |
| more = False |
| else: |
| b = b | 0x80 |
| ret.append(b) |
| return ret |
| |
| # For historical reasons, 32/64-bit versions of functions are presevered |
| def packed64(x): |
| return packed32(x) |
| |
| # variable length packed 4-byte signed value |
| def unsigned_packed32(x): |
| ret = [] |
| if ((x & 0xffffff80) == 0): |
| ret.append(x & 0x7f) |
| elif ((x & 0xffffc000) == 0): |
| ret.append((x | 0x80) & 0xff) |
| ret.append((x >> 7) & 0x7f) |
| elif ((x & 0xffe00000) == 0): |
| ret.append((x | 0x80) & 0xff) |
| ret.append(((x >> 7) | 0x80) & 0xff) |
| ret.append((x >> 14) & 0x7f) |
| elif ((x & 0xf0000000) == 0): |
| ret.append((x | 0x80) & 0xff) |
| ret.append(((x >> 7) | 0x80) & 0xff) |
| ret.append(((x >> 14) | 0x80) & 0xff) |
| ret.append((x >> 21) & 0x7f) |
| else: |
| ret.append((x | 0x80) & 0xff) |
| ret.append(((x >> 7) | 0x80) & 0xff) |
| ret.append(((x >> 14) | 0x80) & 0xff) |
| ret.append(((x >> 21) | 0x80) & 0xff) |
| ret.append((x >> 28) & 0x0f) |
| return ret |
| |
| # variable length packed 8-byte signed value |
| def unsigned_packed64(x): |
| ret = [] |
| if ((x & 0xffffffffffffff80) == 0): |
| ret.append(x & 0x7f) |
| elif ((x & 0xffffffffffffc000) == 0): |
| ret.append((x | 0x80) & 0xff) |
| ret.append((x >> 7) & 0x7f) |
| elif ((x & 0xffffffffffe00000) == 0): |
| ret.append((x | 0x80) & 0xff) |
| ret.append(((x >> 7) | 0x80) & 0xff) |
| ret.append((x >> 14) & 0x7f) |
| elif ((x & 0xfffffffff0000000) == 0): |
| ret.append((x | 0x80) & 0xff) |
| ret.append(((x >> 7) | 0x80) & 0xff) |
| ret.append(((x >> 14) | 0x80) & 0xff) |
| ret.append((x >> 21) & 0x7f) |
| elif ((x & 0xfffffff800000000) == 0): |
| ret.append((x | 0x80) & 0xff) |
| ret.append(((x >> 7) | 0x80) & 0xff) |
| ret.append(((x >> 14) | 0x80) & 0xff) |
| ret.append(((x >> 21) | 0x80) & 0xff) |
| ret.append((x >> 28) & 0x7f) |
| elif ((x & 0xfffffc0000000000) == 0): |
| ret.append((x | 0x80) & 0xff) |
| ret.append(((x >> 7) | 0x80) & 0xff) |
| ret.append(((x >> 14) | 0x80) & 0xff) |
| ret.append(((x >> 21) | 0x80) & 0xff) |
| ret.append(((x >> 28) | 0x80) & 0xff) |
| ret.append((x >> 35) & 0x7f) |
| elif ((x & 0xfffe000000000000) == 0): |
| ret.append((x | 0x80) & 0xff) |
| ret.append(((x >> 7) | 0x80) & 0xff) |
| ret.append(((x >> 14) | 0x80) & 0xff) |
| ret.append(((x >> 21) | 0x80) & 0xff) |
| ret.append(((x >> 28) | 0x80) & 0xff) |
| ret.append(((x >> 35) | 0x80) & 0xff) |
| ret.append((x >> 42) & 0x7f) |
| elif ((x & 0xff00000000000000) == 0): |
| ret.append((x | 0x80) & 0xff) |
| ret.append(((x >> 7) | 0x80) & 0xff) |
| ret.append(((x >> 14) | 0x80) & 0xff) |
| ret.append(((x >> 21) | 0x80) & 0xff) |
| ret.append(((x >> 28) | 0x80) & 0xff) |
| ret.append(((x >> 35) | 0x80) & 0xff) |
| ret.append(((x >> 42) | 0x80) & 0xff) |
| ret.append((x >> 49) & 0x7f) |
| elif ((x & 0x8000000000000000) == 0): |
| ret.append((x | 0x80) & 0xff) |
| ret.append(((x >> 7) | 0x80) & 0xff) |
| ret.append(((x >> 14) | 0x80) & 0xff) |
| ret.append(((x >> 21) | 0x80) & 0xff) |
| ret.append(((x >> 28) | 0x80) & 0xff) |
| ret.append(((x >> 35) | 0x80) & 0xff) |
| ret.append(((x >> 42) | 0x80) & 0xff) |
| ret.append(((x >> 49) | 0x80) & 0xff) |
| ret.append((x >> 56) & 0x7f) |
| else: |
| ret.append((x | 0x80) & 0xff) |
| ret.append(((x >> 7) | 0x80) & 0xff) |
| ret.append(((x >> 14) | 0x80) & 0xff) |
| ret.append(((x >> 21) | 0x80) & 0xff) |
| ret.append(((x >> 28) | 0x80) & 0xff) |
| ret.append(((x >> 35) | 0x80) & 0xff) |
| ret.append(((x >> 42) | 0x80) & 0xff) |
| ret.append(((x >> 49) | 0x80) & 0xff) |
| ret.append(((x >> 56) | 0x80) & 0xff) |
| ret.append((x >> 63) & 0x7f) |
| return ret |
| |
| # 4-byte signed little endian |
| def int32(x): |
| ret = [] |
| ret.append(x & 0xff) |
| ret.append((x >> 8) & 0xff) |
| ret.append((x >> 16) & 0xff) |
| ret.append((x >> 24) & 0xff) |
| return ret |
| |
| # 2-byte signed little endian |
| def int16(x): |
| ret = [] |
| ret.append(x & 0xff) |
| ret.append((x >> 8) & 0xff) |
| return ret |
| |
| # a packed32 length followed by the specified number of characters |
| def stringList(x): |
| ret = [] |
| ret += packed32(len(x)) |
| for i in x: |
| ret.append(i) |
| return ret |
| |
| def utf8StringList(x): |
| ret = [] |
| for i in x: |
| ret.append(ord(i)) |
| return ret |
| |
| # packed64 time value in nanoseconds relative to the uptime from the |
| # Summary message. |
| def timestampList(x): |
| ret = packed64(x) |
| return ret |
| |
| |
| ############################################################ |
| # Write binary |
| ############################################################ |
| |
| def writeBinary(outfile, binary_list): |
| for i in binary_list: |
| outfile.write("%c" % i) |
| |
| ############################################################ |
| # APC Protocol Frame Types |
| ############################################################ |
| |
| def addFrameHeader(frame_type, body, core): |
| ret = [] |
| |
| if frame_type == "Summary": |
| code = 1 |
| elif frame_type == "Backtrace": |
| code = 2 |
| elif frame_type == "Name": |
| code = 3 |
| elif frame_type == "Counter": |
| code = 4 |
| elif frame_type == "Block Counter": |
| code = 5 |
| elif frame_type == "Annotate": |
| code = 6 |
| elif frame_type == "Sched Trace": |
| code = 7 |
| elif frame_type == "GPU Trace": |
| code = 8 |
| elif frame_type == "Idle": |
| code = 9 |
| else: |
| print "ERROR: Unknown frame type:", frame_type |
| sys.exit(1) |
| |
| packed_code = packed32(code) |
| |
| packed_core = packed32(core) |
| |
| length = int32(len(packed_code) + len(packed_core) + len(body)) |
| |
| ret = length + packed_code + packed_core + body |
| return ret |
| |
| |
| # Summary frame |
| # - timestamp: packed64 |
| # - uptime: packed64 |
| def summaryFrame(timestamp, uptime): |
| frame_type = "Summary" |
| newline_canary = stringList("1\n2\r\n3\r4\n\r5") |
| monotonic_delta = packed64(0) |
| end_of_attr = stringList("") |
| body = newline_canary + packed64(timestamp) + packed64(uptime) |
| body += monotonic_delta + end_of_attr |
| ret = addFrameHeader(frame_type, body, 0) |
| return ret |
| |
| # Backtrace frame |
| # - not implemented yet |
| def backtraceFrame(): |
| pass |
| |
| # Cookie name message |
| # - cookie: packed32 |
| # - name: string |
| def cookieNameFrame(cookie, name): |
| frame_type = "Name" |
| packed_code = packed32(1) |
| body = packed_code + packed32(cookie) + stringList(name) |
| ret = addFrameHeader(frame_type, body, 0) |
| return ret |
| |
| # Thread name message |
| # - timestamp: timestamp |
| # - thread id: packed32 |
| # - name: string |
| def threadNameFrame(timestamp, thread_id, name): |
| frame_type = "Name" |
| packed_code = packed32(2) |
| body = packed_code + timestampList(timestamp) + \ |
| packed32(thread_id) + stringList(name) |
| ret = addFrameHeader(frame_type, body, 0) |
| return ret |
| |
| # Core name message |
| # - name: string |
| # - core_id: packed32 |
| # - cpuid: packed32 |
| def coreNameFrame(name, core_id, cpuid): |
| frame_type = "Name" |
| packed_code = packed32(3) |
| body = packed_code + packed32(core_id) + packed32(cpuid) + stringList(name) |
| ret = addFrameHeader(frame_type, body, 0) |
| return ret |
| |
| # IRQ Cookie name message |
| # - cookie: packed32 |
| # - name: string |
| # - irq: packed32 |
| def irqCookieNameFrame(cookie, name, irq): |
| frame_type = "Name" |
| packed_code = packed32(5) |
| body = packed_code + packed32(cookie) + stringList(name) + packed32(irq) |
| ret = addFrameHeader(frame_type, body, 0) |
| return ret |
| |
| # Counter frame message |
| # - timestamp: timestamp |
| # - core: packed32 |
| # - key: packed32 |
| # - value: packed64 |
| def counterFrame(timestamp, core, key, value): |
| frame_type = "Counter" |
| body = timestampList(timestamp) + packed32(core) + packed32(key) + \ |
| packed64(value) |
| ret = addFrameHeader(frame_type, body, core) |
| return ret |
| |
| # Block Counter frame message |
| # - key: packed32 |
| # - value: packed64 |
| def blockCounterFrame(core, key, value): |
| frame_type = "Block Counter" |
| body = packed32(key) + packed64(value) |
| ret = addFrameHeader(frame_type, body, core) |
| return ret |
| |
| # Annotate frame messages |
| # - core: packed32 |
| # - tid: packed32 |
| # - timestamp: timestamp |
| # - size: packed32 |
| # - body |
| def annotateFrame(core, tid, timestamp, size, userspace_body): |
| frame_type = "Annotate" |
| body = packed32(core) + packed32(tid) + timestampList(timestamp) + \ |
| packed32(size) + userspace_body |
| ret = addFrameHeader(frame_type, body, core) |
| return ret |
| |
| # Scheduler Trace frame messages |
| # Sched Switch |
| # - Code: 1 |
| # - timestamp: timestamp |
| # - pid: packed32 |
| # - tid: packed32 |
| # - cookie: packed32 |
| # - state: packed32 |
| def schedSwitchFrame(core, timestamp, pid, tid, cookie, state): |
| frame_type = "Sched Trace" |
| body = packed32(1) + timestampList(timestamp) + packed32(pid) + \ |
| packed32(tid) + packed32(cookie) + packed32(state) |
| ret = addFrameHeader(frame_type, body, core) |
| return ret |
| |
| # Sched Thread Exit |
| # - Code: 2 |
| # - timestamp: timestamp |
| # - tid: packed32 |
| def schedThreadExitFrame(core, timestamp, pid, tid, cookie, state): |
| frame_type = "Sched Trace" |
| body = packed32(2) + timestampList(timestamp) + packed32(tid) |
| ret = addFrameHeader(frame_type, body, core) |
| return ret |
| |
| # GPU Trace frame messages |
| # - Not implemented yet |
| def gpuTraceFrame(): |
| pass |
| |
| # Idle frame messages |
| # Enter Idle |
| # - code: 1 |
| # - timestamp: timestamp |
| # - core: packed32 |
| def enterIdleFrame(timestamp, core): |
| frame_type = "Idle" |
| body = packed32(1) + timestampList(timestamp) + packed32(core) |
| ret = addFrameHeader(frame_type, body, core) |
| return ret |
| |
| # Exit Idle |
| # - code: 2 |
| # - timestamp: timestamp |
| # - core: packed32 |
| def exitIdleFrame(timestamp, core): |
| frame_type = "Idle" |
| body = packed32(2) + timestampList(timestamp) + packed32(core) |
| ret = addFrameHeader(frame_type, body, core) |
| return ret |
| |
| |
| #################################################################### |
| def parseProcessInfo(task_file): |
| print "\n===============================" |
| print "Parsing Task file..." |
| print task_file |
| print "===============================\n" |
| |
| global start_tick, end_tick, num_cpus |
| global process_dict, thread_dict, process_list |
| global event_list, unified_event_list |
| global idle_uid, kernel_uid |
| |
| event_list = [] |
| unified_event_list = [] |
| for cpu in range(num_cpus): |
| event_list.append([]) |
| |
| uid = 1 # uid 0 is reserved for idle |
| |
| # Dummy Tasks for frame buffers and system diagrams |
| process = Task(uid, 9999, 9999, "framebuffer", True, 0) |
| process_list.append(process) |
| uid += 1 |
| thread = Task(uid, 9999, 9999, "framebuffer", False, 0) |
| process.children.append(thread) |
| uid += 1 |
| process = Task(uid, 9998, 9998, "System", True, 0) |
| process_list.append(process) |
| # if we don't find the real kernel, use this to keep things going |
| kernel_uid = uid |
| uid += 1 |
| thread = Task(uid, 9998, 9998, "System", False, 0) |
| process.children.append(thread) |
| uid += 1 |
| |
| ext = os.path.splitext(task_file)[1] |
| |
| try: |
| if ext == ".gz": |
| process_file = gzip.open(task_file, 'rb') |
| else: |
| process_file = open(task_file, 'rb') |
| except: |
| print "ERROR opening task file:", task_file |
| print "Make sure context switch task dumping is enabled in gem5." |
| sys.exit(1) |
| |
| process_re = re.compile("tick=(\d+)\s+(\d+)\s+cpu_id=(\d+)\s+" + |
| "next_pid=([-\d]+)\s+next_tgid=([-\d]+)\s+next_task=(.*)") |
| |
| task_name_failure_warned = False |
| |
| for line in process_file: |
| match = re.match(process_re, line) |
| if match: |
| tick = int(match.group(1)) |
| if (start_tick < 0): |
| start_tick = tick |
| cpu_id = int(match.group(3)) |
| pid = int(match.group(4)) |
| tgid = int(match.group(5)) |
| task_name = match.group(6) |
| |
| if not task_name_failure_warned: |
| if task_name == "FailureIn_curTaskName": |
| print "-------------------------------------------------" |
| print "WARNING: Task name not set correctly!" |
| print "Process/Thread info will not be displayed correctly" |
| print "Perhaps forgot to apply m5struct.patch to kernel?" |
| print "-------------------------------------------------" |
| task_name_failure_warned = True |
| |
| if not tgid in process_dict: |
| if tgid == pid: |
| # new task is parent as well |
| if args.verbose: |
| print "new process", uid, pid, tgid, task_name |
| if tgid == 0: |
| # new process is the "idle" task |
| process = Task(uid, pid, tgid, "idle", True, tick) |
| idle_uid = 0 |
| else: |
| process = Task(uid, pid, tgid, task_name, True, tick) |
| else: |
| if tgid == 0: |
| process = Task(uid, tgid, tgid, "idle", True, tick) |
| idle_uid = 0 |
| else: |
| # parent process name not known yet |
| process = Task(uid, tgid, tgid, "_Unknown_", True, tick) |
| if tgid == -1: # kernel |
| kernel_uid = 0 |
| uid += 1 |
| process_dict[tgid] = process |
| process_list.append(process) |
| else: |
| if tgid == pid: |
| if process_dict[tgid].task_name == "_Unknown_": |
| if args.verbose: |
| print "new process", \ |
| process_dict[tgid].uid, pid, tgid, task_name |
| process_dict[tgid].task_name = task_name |
| if process_dict[tgid].task_name != task_name and tgid != 0: |
| process_dict[tgid].task_name = task_name |
| |
| if not pid in thread_dict: |
| if args.verbose: |
| print "new thread", \ |
| uid, process_dict[tgid].uid, pid, tgid, task_name |
| thread = Task(uid, pid, tgid, task_name, False, tick) |
| uid += 1 |
| thread_dict[pid] = thread |
| process_dict[tgid].children.append(thread) |
| else: |
| if thread_dict[pid].task_name != task_name: |
| thread_dict[pid].task_name = task_name |
| |
| if args.verbose: |
| print tick, uid, cpu_id, pid, tgid, task_name |
| |
| task = thread_dict[pid] |
| event = Event(tick, task) |
| event_list[cpu_id].append(event) |
| unified_event_list.append(event) |
| |
| if len(unified_event_list) == num_events: |
| print "Truncating at", num_events, "events!" |
| break |
| print "Found %d events." % len(unified_event_list) |
| |
| for process in process_list: |
| if process.pid > 9990: # fix up framebuffer ticks |
| process.tick = start_tick |
| print process.uid, process.pid, process.tgid, \ |
| process.task_name, str(process.tick) |
| for thread in process.children: |
| if thread.pid > 9990: |
| thread.tick = start_tick |
| print "\t", thread.uid, thread.pid, thread.tgid, \ |
| thread.task_name, str(thread.tick) |
| |
| end_tick = tick |
| |
| print "Start tick:", start_tick |
| print "End tick: ", end_tick |
| print "" |
| |
| return |
| |
| |
| def initOutput(output_path): |
| if not os.path.exists(output_path): |
| os.mkdir(output_path) |
| |
| def ticksToNs(tick): |
| if ticks_in_ns < 0: |
| print "ticks_in_ns not set properly!" |
| sys.exit(1) |
| |
| return tick / ticks_in_ns |
| |
| def writeXmlFile(xml, filename): |
| f = open(filename, "w") |
| txt = ET.tostring(xml) |
| f.write(minidom.parseString(txt).toprettyxml()) |
| f.close() |
| |
| |
| # StatsEntry that contains individual statistics |
| class StatsEntry(object): |
| def __init__(self, name, group, group_index, per_cpu, key): |
| |
| # Full name of statistics |
| self.name = name |
| |
| # Streamline group name that statistic will belong to |
| self.group = group |
| |
| # Index of statistics within group (used to change colors within groups) |
| self.group_index = group_index |
| |
| # Shorter name with "system" stripped off |
| # and symbols converted to alphanumerics |
| self.short_name = re.sub("system\.", "", name) |
| self.short_name = re.sub(":", "_", name) |
| |
| # Regex for this stat (string version used to construct union regex) |
| self.regex_string = "^" + name + "\s+([\d\.]+)" |
| self.regex = re.compile("^" + name + "\s+([\d\.e\-]+)\s+# (.*)$", re.M) |
| self.description = "" |
| |
| # Whether this stat is use per CPU or not |
| self.per_cpu = per_cpu |
| |
| # Key used in .apc protocol (as described in captured.xml) |
| self.key = key |
| |
| # List of values of stat per timestamp |
| self.values = [] |
| |
| # Whether this stat has been found for the current timestamp |
| self.found = False |
| |
| # Whether this stat has been found at least once |
| # (to suppress too many warnings) |
| self.not_found_at_least_once = False |
| |
| # Field used to hold ElementTree subelement for this stat |
| self.ET_element = None |
| |
| # Create per-CPU stat name and regex, etc. |
| if self.per_cpu: |
| self.per_cpu_regex_string = [] |
| self.per_cpu_regex = [] |
| self.per_cpu_name = [] |
| self.per_cpu_found = [] |
| for i in range(num_cpus): |
| if num_cpus > 1: |
| per_cpu_name = re.sub("#", str(i), self.name) |
| else: |
| per_cpu_name = re.sub("#", "", self.name) |
| |
| self.per_cpu_name.append(per_cpu_name) |
| print "\t", per_cpu_name |
| |
| self.per_cpu_regex_string.\ |
| append("^" + per_cpu_name + "\s+[\d\.]+") |
| self.per_cpu_regex.append(re.compile("^" + per_cpu_name + \ |
| "\s+([\d\.e\-]+)\s+# (.*)$", re.M)) |
| self.values.append([]) |
| self.per_cpu_found.append(False) |
| |
| def append_value(self, val, per_cpu_index = None): |
| if self.per_cpu: |
| self.values[per_cpu_index].append(str(val)) |
| else: |
| self.values.append(str(val)) |
| |
| # Global stats object that contains the list of stats entries |
| # and other utility functions |
| class Stats(object): |
| def __init__(self): |
| self.stats_list = [] |
| self.tick_list = [] |
| self.next_key = 1 |
| |
| def register(self, name, group, group_index, per_cpu): |
| print "registering stat:", name, "group:", group, group_index |
| self.stats_list.append(StatsEntry(name, group, group_index, per_cpu, \ |
| self.next_key)) |
| self.next_key += 1 |
| |
| # Union of all stats to accelerate parsing speed |
| def createStatsRegex(self): |
| regex_strings = []; |
| print "\nnum entries in stats_list", len(self.stats_list) |
| for entry in self.stats_list: |
| if entry.per_cpu: |
| for i in range(num_cpus): |
| regex_strings.append(entry.per_cpu_regex_string[i]) |
| else: |
| regex_strings.append(entry.regex_string) |
| |
| self.regex = re.compile('|'.join(regex_strings)) |
| |
| |
| def registerStats(config_file): |
| print "===============================" |
| print "Parsing stats config.ini file..." |
| print config_file |
| print "===============================" |
| |
| config = ConfigParser() |
| if not config.read(config_file): |
| print "ERROR: config file '", config_file, "' not found!" |
| sys.exit(1) |
| |
| print "\nRegistering Stats..." |
| |
| stats = Stats() |
| |
| per_cpu_stat_groups = config.options('PER_CPU_STATS') |
| for group in per_cpu_stat_groups: |
| i = 0 |
| per_cpu_stats_list = config.get('PER_CPU_STATS', group).split('\n') |
| for item in per_cpu_stats_list: |
| if item: |
| stats.register(item, group, i, True) |
| i += 1 |
| |
| per_l2_stat_groups = config.options('PER_L2_STATS') |
| for group in per_l2_stat_groups: |
| i = 0 |
| per_l2_stats_list = config.get('PER_L2_STATS', group).split('\n') |
| for item in per_l2_stats_list: |
| if item: |
| for l2 in range(num_l2): |
| if num_l2 > 1: |
| name = re.sub("#", str(l2), item) |
| else: |
| name = re.sub("#", "", item) |
| stats.register(name, group, i, False) |
| i += 1 |
| |
| other_stat_groups = config.options('OTHER_STATS') |
| for group in other_stat_groups: |
| i = 0 |
| other_stats_list = config.get('OTHER_STATS', group).split('\n') |
| for item in other_stats_list: |
| if item: |
| stats.register(item, group, i, False) |
| i += 1 |
| |
| stats.createStatsRegex() |
| |
| return stats |
| |
| # Parse and read in gem5 stats file |
| # Streamline counters are organized per CPU |
| def readGem5Stats(stats, gem5_stats_file): |
| print "\n===============================" |
| print "Parsing gem5 stats file..." |
| print gem5_stats_file |
| print "===============================\n" |
| ext = os.path.splitext(gem5_stats_file)[1] |
| |
| window_start_regex = \ |
| re.compile("^---------- Begin Simulation Statistics ----------") |
| window_end_regex = \ |
| re.compile("^---------- End Simulation Statistics ----------") |
| final_tick_regex = re.compile("^final_tick\s+(\d+)") |
| |
| global ticks_in_ns |
| sim_freq_regex = re.compile("^sim_freq\s+(\d+)") |
| sim_freq = -1 |
| |
| try: |
| if ext == ".gz": |
| f = gzip.open(gem5_stats_file, "r") |
| else: |
| f = open(gem5_stats_file, "r") |
| except: |
| print "ERROR opening stats file", gem5_stats_file, "!" |
| sys.exit(1) |
| |
| stats_not_found_list = stats.stats_list[:] |
| window_num = 0 |
| |
| while (True): |
| error = False |
| try: |
| line = f.readline() |
| except IOError: |
| print "" |
| print "WARNING: IO error in stats file" |
| print "(gzip stream not closed properly?)...continuing for now" |
| error = True |
| if not line: |
| break |
| |
| # Find out how many gem5 ticks in 1ns |
| if sim_freq < 0: |
| m = sim_freq_regex.match(line) |
| if m: |
| sim_freq = int(m.group(1)) # ticks in 1 sec |
| ticks_in_ns = int(sim_freq / 1e9) |
| print "Simulation frequency found! 1 tick == %e sec\n" \ |
| % (1.0 / sim_freq) |
| |
| # Final tick in gem5 stats: current absolute timestamp |
| m = final_tick_regex.match(line) |
| if m: |
| tick = int(m.group(1)) |
| if tick > end_tick: |
| break |
| stats.tick_list.append(tick) |
| |
| |
| if (window_end_regex.match(line) or error): |
| if args.verbose: |
| print "new window" |
| for stat in stats.stats_list: |
| if stat.per_cpu: |
| for i in range(num_cpus): |
| if not stat.per_cpu_found[i]: |
| if not stat.not_found_at_least_once: |
| print "WARNING: stat not found in window #", \ |
| window_num, ":", stat.per_cpu_name[i] |
| print "suppressing further warnings for " + \ |
| "this stat" |
| stat.not_found_at_least_once = True |
| stat.values[i].append(str(0)) |
| stat.per_cpu_found[i] = False |
| else: |
| if not stat.found: |
| if not stat.not_found_at_least_once: |
| print "WARNING: stat not found in window #", \ |
| window_num, ":", stat.name |
| print "suppressing further warnings for this stat" |
| stat.not_found_at_least_once = True |
| stat.values.append(str(0)) |
| stat.found = False |
| stats_not_found_list = stats.stats_list[:] |
| window_num += 1 |
| if error: |
| break |
| |
| # Do a single regex of the union of all stats first for speed |
| if stats.regex.match(line): |
| # Then loop through only the stats we haven't seen in this window |
| for stat in stats_not_found_list[:]: |
| if stat.per_cpu: |
| for i in range(num_cpus): |
| m = stat.per_cpu_regex[i].match(line) |
| if m: |
| if stat.name == "ipc": |
| value = str(int(float(m.group(1)) * 1000)) |
| else: |
| value = str(int(float(m.group(1)))) |
| if args.verbose: |
| print stat.per_cpu_name[i], value |
| stat.values[i].append(value) |
| stat.per_cpu_found[i] = True |
| all_found = True |
| for j in range(num_cpus): |
| if not stat.per_cpu_found[j]: |
| all_found = False |
| if all_found: |
| stats_not_found_list.remove(stat) |
| if stat.description == "": |
| stat.description = m.group(2) |
| else: |
| m = stat.regex.match(line) |
| if m: |
| value = str(int(float(m.group(1)))) |
| if args.verbose: |
| print stat.name, value |
| stat.values.append(value) |
| stat.found = True |
| stats_not_found_list.remove(stat) |
| if stat.description == "": |
| stat.description = m.group(2) |
| f.close() |
| |
| |
| # Create session.xml file in .apc folder |
| def doSessionXML(output_path): |
| session_file = output_path + "/session.xml" |
| |
| xml = ET.Element("session") |
| |
| xml.set("version", "1") |
| xml.set("call_stack_unwinding", "no") |
| xml.set("parse_debug_info", "no") |
| xml.set("high_resolution", "yes") |
| xml.set("buffer_mode", "streaming") |
| xml.set("sample_rate", "low") |
| |
| # Setting duration to zero for now. Doesn't affect visualization. |
| xml.set("duration", "0") |
| |
| xml.set("target_host", "") |
| xml.set("target_port", "8080") |
| |
| writeXmlFile(xml, session_file) |
| |
| |
| # Create captured.xml file in .apc folder |
| def doCapturedXML(output_path, stats): |
| captured_file = output_path + "/captured.xml" |
| |
| xml = ET.Element("captured") |
| xml.set("version", "1") |
| xml.set("protocol", "17") |
| xml.set("backtrace_processing", "none") |
| |
| target = ET.SubElement(xml, "target") |
| target.set("name", "gem5") |
| target.set("sample_rate", "1000") |
| target.set("cores", str(num_cpus)) |
| |
| counters = ET.SubElement(xml, "counters") |
| for stat in stats.stats_list: |
| s = ET.SubElement(counters, "counter") |
| stat_name = re.sub("\.", "_", stat.short_name) |
| stat_name = re.sub("#", "", stat_name) |
| s.set("title", stat.group) |
| s.set("name", stat_name) |
| s.set("color", "0x00000000") |
| s.set("key", "0x%08x" % stat.key) |
| s.set("type", stat_name) |
| s.set("event", "0x00000000") |
| if stat.per_cpu: |
| s.set("per_cpu", "yes") |
| else: |
| s.set("per_cpu", "no") |
| s.set("display", "") |
| s.set("units", "") |
| s.set("average_selection", "no") |
| s.set("description", stat.description) |
| |
| writeXmlFile(xml, captured_file) |
| |
| # Writes out Streamline cookies (unique IDs per process/thread) |
| def writeCookiesThreads(blob): |
| thread_list = [] |
| for process in process_list: |
| if process.uid > 0: |
| print "cookie", process.task_name, process.uid |
| writeBinary(blob, cookieNameFrame(process.uid, process.task_name)) |
| |
| # pid and tgid need to be positive values -- no longer true? |
| for thread in process.children: |
| thread_list.append(thread) |
| |
| # Threads need to be sorted in timestamp order |
| thread_list.sort(key = lambda x: x.tick) |
| for thread in thread_list: |
| print "thread", thread.task_name, (ticksToNs(thread.tick)),\ |
| thread.tgid, thread.pid |
| writeBinary(blob, threadNameFrame(ticksToNs(thread.tick),\ |
| thread.pid, thread.task_name)) |
| |
| # Writes context switch info as Streamline scheduling events |
| def writeSchedEvents(blob): |
| for cpu in range(num_cpus): |
| for event in event_list[cpu]: |
| timestamp = ticksToNs(event.tick) |
| pid = event.task.tgid |
| tid = event.task.pid |
| if process_dict.has_key(event.task.tgid): |
| cookie = process_dict[event.task.tgid].uid |
| else: |
| cookie = 0 |
| |
| # State: |
| # 0: waiting on other event besides I/O |
| # 1: Contention/pre-emption |
| # 2: Waiting on I/O |
| # 3: Waiting on mutex |
| # Hardcoding to 0 for now. Other states not implemented yet. |
| state = 0 |
| |
| if args.verbose: |
| print cpu, timestamp, pid, tid, cookie |
| |
| writeBinary(blob,\ |
| schedSwitchFrame(cpu, timestamp, pid, tid, cookie, state)) |
| |
| # Writes selected gem5 statistics as Streamline counters |
| def writeCounters(blob, stats): |
| timestamp_list = [] |
| for tick in stats.tick_list: |
| if tick > end_tick: |
| break |
| timestamp_list.append(ticksToNs(tick)) |
| |
| for stat in stats.stats_list: |
| if stat.per_cpu: |
| stat_length = len(stat.values[0]) |
| else: |
| stat_length = len(stat.values) |
| |
| for n in range(len(timestamp_list)): |
| for stat in stats.stats_list: |
| if stat.per_cpu: |
| for i in range(num_cpus): |
| writeBinary(blob, counterFrame(timestamp_list[n], i, \ |
| stat.key, int(float(stat.values[i][n])))) |
| else: |
| writeBinary(blob, counterFrame(timestamp_list[n], 0, \ |
| stat.key, int(float(stat.values[n])))) |
| |
| # Streamline can display LCD frame buffer dumps (gzipped bmp) |
| # This function converts the frame buffer dumps to the Streamline format |
| def writeVisualAnnotations(blob, input_path, output_path): |
| frame_path = input_path + "/frames_system.vncserver" |
| if not os.path.exists(frame_path): |
| return |
| |
| frame_count = 0 |
| file_list = os.listdir(frame_path) |
| file_list.sort() |
| re_fb = re.compile("fb\.(\d+)\.(\d+)\.bmp.gz") |
| |
| # Use first non-negative pid to tag visual annotations |
| annotate_pid = -1 |
| for e in unified_event_list: |
| pid = e.task.pid |
| if pid >= 0: |
| annotate_pid = pid |
| break |
| |
| for fn in file_list: |
| m = re_fb.match(fn) |
| if m: |
| seq = m.group(1) |
| tick = int(m.group(2)) |
| if tick > end_tick: |
| break |
| frame_count += 1 |
| |
| userspace_body = [] |
| userspace_body += packed32(0x1C) # escape code |
| userspace_body += packed32(0x04) # visual code |
| |
| text_annotation = "image_" + str(ticksToNs(tick)) + ".bmp.gz" |
| userspace_body += int16(len(text_annotation)) |
| userspace_body += utf8StringList(text_annotation) |
| |
| if gzipped_bmp_supported: |
| # copy gzipped bmp directly |
| bytes_read = open(frame_path + "/" + fn, "rb").read() |
| else: |
| # copy uncompressed bmp |
| bytes_read = gzip.open(frame_path + "/" + fn, "rb").read() |
| |
| userspace_body += int32(len(bytes_read)) |
| userspace_body += bytes_read |
| |
| writeBinary(blob, annotateFrame(0, annotate_pid, ticksToNs(tick), \ |
| len(userspace_body), userspace_body)) |
| |
| print "\nfound", frame_count, "frames for visual annotation.\n" |
| |
| |
| def createApcProject(input_path, output_path, stats): |
| initOutput(output_path) |
| |
| blob = open(output_path + "/0000000000", "wb") |
| |
| # Summary frame takes current system time and system uptime. |
| # Filling in with random values for now. |
| writeBinary(blob, summaryFrame(1234, 5678)) |
| |
| writeCookiesThreads(blob) |
| |
| print "writing Events" |
| writeSchedEvents(blob) |
| |
| print "writing Counters" |
| writeCounters(blob, stats) |
| |
| print "writing Visual Annotations" |
| writeVisualAnnotations(blob, input_path, output_path) |
| |
| doSessionXML(output_path) |
| doCapturedXML(output_path, stats) |
| |
| blob.close() |
| |
| |
| |
| ####################### |
| # Main Routine |
| |
| input_path = args.input_path |
| output_path = args.output_path |
| |
| #### |
| # Make sure input path exists |
| #### |
| if not os.path.exists(input_path): |
| print "ERROR: Input path %s does not exist!" % input_path |
| sys.exit(1) |
| |
| #### |
| # Parse gem5 configuration file to find # of CPUs and L2s |
| #### |
| (num_cpus, num_l2) = parseConfig(input_path + "/config.ini") |
| |
| #### |
| # Parse task file to find process/thread info |
| #### |
| parseProcessInfo(input_path + "/system.tasks.txt") |
| |
| #### |
| # Parse stat config file and register stats |
| #### |
| stat_config_file = args.stat_config_file |
| stats = registerStats(stat_config_file) |
| |
| #### |
| # Parse gem5 stats |
| #### |
| # Check if both stats.txt and stats.txt.gz exist and warn if both exist |
| if os.path.exists(input_path + "/stats.txt") and \ |
| os.path.exists(input_path + "/stats.txt.gz"): |
| print "WARNING: Both stats.txt.gz and stats.txt exist. \ |
| Using stats.txt.gz by default." |
| |
| gem5_stats_file = input_path + "/stats.txt.gz" |
| if not os.path.exists(gem5_stats_file): |
| gem5_stats_file = input_path + "/stats.txt" |
| if not os.path.exists(gem5_stats_file): |
| print "ERROR: stats.txt[.gz] file does not exist in %s!" % input_path |
| sys.exit(1) |
| |
| readGem5Stats(stats, gem5_stats_file) |
| |
| #### |
| # Create Streamline .apc project folder |
| #### |
| createApcProject(input_path, output_path, stats) |
| |
| print "All done!" |