| #!/usr/bin/env python3 | |
| # 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 event.task.tgid in process_dict: | |
| 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!") |