From 6ad41d6fb6db6cf8de07877ab3cbcc1143d46648 Mon Sep 17 00:00:00 2001
From: "Maarten A. Breddels" <maartenbreddels@gmail.com>
Date: Tue, 15 Dec 2020 08:46:45 +0100
Subject: [PATCH] make perf2trace a subcommand

---
 README.md           |   2 +-
 per4m/__main__.py   | 199 ++++--------------------------------------
 per4m/perf2trace.py | 205 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 225 insertions(+), 181 deletions(-)
 create mode 100644 per4m/perf2trace.py

diff --git a/README.md b/README.md
index 1a7753f..ad5aa58 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ Report saved.
 ## Step 3
 Convert `perf.data` to [Trace Event Format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/)
 
-    $ perf script --no-inline | per4m --no-running -o example1perf.json
+    $ perf script --no-inline | per4m perf2trace --no-running -o example1perf.json
 
 
 ## Step 4
diff --git a/per4m/__main__.py b/per4m/__main__.py
index 8e208f4..036a12c 100644
--- a/per4m/__main__.py
+++ b/per4m/__main__.py
@@ -1,192 +1,31 @@
+import os
 import sys
-import json
-import argparse
 
+usage = """usage per4m [-h] {perf2trace,...}
 
-def read_events(input):
-    first_line = True
-    stacktrace = []
-    header = None
-    for line in sys.stdin:
-        line = line.strip()
-        if first_line:
-            header = line
-            stacktrace = []
-            first_line = False
-        else:
-            if not line:  # done
-                yield header, stacktrace
-                first_line = True
-                continue
-            stacktrace.append(line)
+optional arguments:
+  -h, --help          show this help message and exit
 
+positional arguments:
+    perf2trace          convert perf.data to TraceEvent json data
+    ...                 anything else will start up the gui, see usage below
 
-def parse_values(parts, **types):
-    values = {}
-    for part in parts:
-        if "=" in part:
-            key, value = part.split('=', 1)
-            value = types.get(key, str)(value)
-            values[key] = value
-    for key in types:
-        if key not in values:
-            raise ValueError(f'Expected to find key {key} in {parts}')
-    return values
+Examples:
+$ perf script --no-inline | per4m -v
 
-usage = """
-
-Convert perf results to Trace Event Format.
-
-Usage:
-
-Always run perf with -e 'sched:*' --call-graph dwarf -k CLOCK_MONOTONIC, the rest of the events are extra
-$ perf record -e 'sched:*' --call-graph dwarf -k CLOCK_MONOTONIC -e L1-dcache-load-misses -e instructions -e cycles -e page-faults -- python -m per4m.example1 
-Run with --no-online, otherwise it may run slow
-$ perf script --no-inline | per4m --no-running -o example1perf.json
-$ viztracer --combine example1.json example1perf.json -o example1.html
 """
 
-def main(argv=sys.argv):
-    argv = sys.argv
-    parser = argparse.ArgumentParser(argv[0],
-        formatter_class=argparse.RawDescriptionHelpFormatter,
-        usage=usage)
-    parser.add_argument('--verbose', '-v', action='count', default=0)
-    parser.add_argument('--sleeping', help="store sleeping phase (default: %(default)s)", default=True, action='store_true')
-    parser.add_argument('--no-sleeping', dest="sleeping", action='store_false')
-    parser.add_argument('--running', help="show running phase (default: %(default)s)", default=True, action='store_true')
-    parser.add_argument('--no-running', dest="running", action='store_false')
-    parser.add_argument('--all-tracepoints', help="store all tracepoints phase (default: %(default)s)", default=False, action='store_true')
 
-    parser.add_argument('--output', '-o', dest="output", default='perf.json', help="Output filename (default %(default)s)")
-
-
-    args = parser.parse_args(argv[1:])
-
-    trace_events = []
-
-    # useful for debugging, to have the pids a name
-    pid_names = {}
-    # pid_names = {872068: "main", 872070: "t1", 872071: "t2"}
-    # a bit pendantic to keep these separated
-    last_run_time = {}
-    last_sleep_time = {}
-    verbose = args.verbose
-    time_first = None
-    store_runing = args.running
-    store_sleeping = args.sleeping
-    parent_pid = {}  # maps pid/tid to the parent
-    count = None
-    for header, stacktrace in read_events(sys.stdin):
-        try:
-            if args.verbose >= 3:
-                print(header)
-            pid = None
-            parts = header.split()
-            # python 302629 [011] 3485124.180312:       sched:sched_switch: prev_comm=python prev_pid=302629 prev_prio=120 prev_state=S ==> next_comm=swapper/11 next_pid=0 next_prio=120
-            event = parts[4][:-1] # strip off ':'
-            if ":" in event:  # tracepoint
-                dso, triggerpid, cpu, time, _, *other = parts
-                tracepoint = True
-            else:  # counter etc
-                dso, triggerpid, time, count, _, *other = parts
-                tracepoint = False
-            triggerpid = int(triggerpid)
-            time = float(time[:-1]) * 1e6
-            if time_first is None:
-                time_first = time
-            def log(*args, time=time/1e6):
-                offset = time - time_first/1e6
-                print(f"{time:13.6f}[+{offset:5.4f}]", *args)
-            if args.all_tracepoints and tracepoint:
-                trace_events.append({'name': event, 'pid': parent_pid.get(pid, pid), 'tid': triggerpid, 'ts': time, 'ph': 'i', 's': 'g'})
-            first_line = False
-            gil_event = None
-            if event == "sched:sched_switch":
-                # e.g. python 393320 [040] 3498299.441431:                sched:sched_switch: prev_comm=python prev_pid=393320 prev_prio=120 prev_state=S ==> next_comm=swapper/40 next_pid=0 next_prio=120
-                values = parse_values(parts, prev_pid=int)
-                # we are going to sleep?
-                pid = values['prev_pid']
-                if values['prev_state'] == 'R':
-                    # this happens when a process just started, so we just set the start time
-                    last_sleep_time[pid] = time
-                    continue
-                # if values['prev_comm'] != "python":
-                #     if verbose >= 2:
-                #         log(f'Skipping switch from {values["prev_comm"]}')
-                name = pid_names.get(pid, pid)
-                if pid not in last_run_time:
-                    # raise ValueError(f'pid {pid} not seen running before, only {last_run_time}')
-                    continue
-                dur = time  - last_run_time[pid]
-                if verbose:
-                    log(f'{name} will switch to state={values["prev_state"]}, ran for {dur}')
-                if store_runing:
-                    event = {"pid": parent_pid.get(pid, pid), "tid": pid, "ts": last_run_time[pid], "dur": dur, "name": 'R', "ph": "X", "cat": "process state"}
-                    trace_events.append(event)
-
-                last_sleep_time[pid] = time
-                del last_run_time[pid]
-            elif event == "sched:sched_wakeup":
-                # e.g: swapper     0 [040] 3498299.642199:                sched:sched_waking: comm=python pid=393320 prio=120 target_cpu=040
-                prev_state = None
-                values = parse_values(parts, pid=int)
-                pid = values['pid']
-                comm = values['comm']
-                # if comm != "python":
-                #     if verbose >= 2:
-                #         log(f'Skip waking event for {comm}')
-                #     continue
-                if pid not in last_sleep_time:
-                    # raise ValueError(f'pid {pid} not seen sleeping before, only {last_sleep_time}')
-                    # this can happen when we did not see the creation
-                    # q
-                    last_run_time[pid] = time
-                    continue
-                if verbose:
-                    name = pid_names.get(pid, pid)
-                    log(f'Waking up {name}')
-                if store_sleeping:
-                    dur = time  - last_sleep_time[pid]
-                    event = {"pid": parent_pid.get(pid, pid), "tid": pid, "ts": last_sleep_time[pid], "dur": dur, "name": f"S", "ph": "X", "cat": "process state"} #3579742.654826
-                    trace_events.append(event)
-                last_run_time[pid] = time
-                del last_sleep_time[pid]
-            elif event == "sched:sched_process_exec":
-                if verbose:
-                    name = pid_names.get(triggerpid, triggerpid)
-                    log(f'Starting (exec) {name}')
-            elif event == "sched:sched_wakeup_new":
-                # e.g: swapper     0 [040] 3498299.642199:                sched:sched_waking: comm=python pid=393320 prio=120 target_cpu=040
-                values = parse_values(parts, pid=int)
-                pid = values['pid']
-                if verbose:
-                    name = pid_names.get(pid, pid)
-                    log(f'Starting (new) {name}')
-                last_run_time[pid] = time
-            elif event == "sched:sched_process_fork":
-                values = parse_values(parts, pid=int, child_pid=int)
-                # set up a child parent relationship for better visualization
-                pid, child_pid = values['pid'], values['child_pid']
-                if verbose:
-                    log(f'Process {pid} forked {child_pid}')
-                parent_pid[child_pid] = pid
-            elif not tracepoint:
-                event = {"pid": 'counters', "ts": time, "name": event, "ph": "C", "args": {event: count}}
-                trace_events.append(event)
-            else:
-                if verbose >= 2:
-                    print("SKIP", header)
-                pass
-        except BrokenPipeError:
-            break
-        except:
-            print("error on line", header)
-            raise
-            
-    with open(args.output, 'w') as f:
-        json.dump({'traceEvents': trace_events}, f)
-    print(f"Wrote to {args.output}")
+def main(args=sys.argv):
+    if len(args) > 1 and args[1] in ["-h", "--help"]:
+        print(usage)
+        sys.exit(0)
+    elif len(args) > 1 and args[1] == "perf2trace":
+        from .perf2trace import main
+        main([os.path.basename(args[0]) + " " + args[1]] + args[2:])
+    else:
+        print(usage)
+        sys.exit(0)
 
 
 if __name__ == '__main__':
diff --git a/per4m/perf2trace.py b/per4m/perf2trace.py
new file mode 100644
index 0000000..c240ae5
--- /dev/null
+++ b/per4m/perf2trace.py
@@ -0,0 +1,205 @@
+import sys
+import json
+import argparse
+
+
+def read_events(input):
+    first_line = True
+    stacktrace = []
+    header = None
+    for line in sys.stdin:
+        line = line.strip()
+        if first_line:
+            header = line
+            stacktrace = []
+            first_line = False
+        else:
+            if not line:  # done
+                yield header, stacktrace
+                first_line = True
+                continue
+            stacktrace.append(line)
+
+
+def parse_values(parts, **types):
+    values = {}
+    for part in parts:
+        if "=" in part:
+            key, value = part.split('=', 1)
+            value = types.get(key, str)(value)
+            values[key] = value
+    for key in types:
+        if key not in values:
+            raise ValueError(f'Expected to find key {key} in {parts}')
+    return values
+
+
+def in_stacktrace(function_name, stacktrace):
+    return any(function_name in call for call in stacktrace)
+
+
+def takes_gil(stacktrace):
+    return in_stacktrace('take_gil', stacktrace)
+
+
+def drops_gil(stacktrace):
+    return in_stacktrace('drop_gil', stacktrace)
+
+
+usage = """
+
+Convert perf.data to Trace Event Format.
+
+Usage:
+
+Always run perf with -e 'sched:*' --call-graph dwarf -k CLOCK_MONOTONIC, the rest of the events are extra
+$ perf record -e 'sched:*' --call-graph dwarf -k CLOCK_MONOTONIC -e L1-dcache-load-misses -e instructions -e cycles -e page-faults -- python -m per4m.example1 
+Run with --no-online, otherwise it may run slow
+$ perf script --no-inline | per4m perf2trace --no-running -o example1perf.json
+$ viztracer --combine example1.json example1perf.json -o example1.html
+"""
+
+def main(argv=sys.argv):
+    parser = argparse.ArgumentParser(argv[0],
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        usage=usage)
+    parser.add_argument('--verbose', '-v', action='count', default=0)
+    parser.add_argument('--sleeping', help="store sleeping phase (default: %(default)s)", default=True, action='store_true')
+    parser.add_argument('--no-sleeping', dest="sleeping", action='store_false')
+    parser.add_argument('--running', help="show running phase (default: %(default)s)", default=True, action='store_true')
+    parser.add_argument('--no-running', dest="running", action='store_false')
+    parser.add_argument('--all-tracepoints', help="store all tracepoints phase (default: %(default)s)", default=False, action='store_true')
+
+    parser.add_argument('--output', '-o', dest="output", default='perf.json', help="Output filename (default %(default)s)")
+
+
+    args = parser.parse_args(argv[1:])
+
+    trace_events = []
+
+    # useful for debugging, to have the pids a name
+    pid_names = {}
+    # pid_names = {872068: "main", 872070: "t1", 872071: "t2"}
+    # a bit pendantic to keep these separated
+    last_run_time = {}
+    last_sleep_time = {}
+    verbose = args.verbose
+    time_first = None
+    store_runing = args.running
+    store_sleeping = args.sleeping
+    parent_pid = {}  # maps pid/tid to the parent
+    count = None
+    for header, stacktrace in read_events(sys.stdin):
+        try:
+            if args.verbose >= 3:
+                print(header)
+            pid = None
+            parts = header.split()
+            # python 302629 [011] 3485124.180312:       sched:sched_switch: prev_comm=python prev_pid=302629 prev_prio=120 prev_state=S ==> next_comm=swapper/11 next_pid=0 next_prio=120
+            event = parts[4][:-1] # strip off ':'
+            if ":" in event:  # tracepoint
+                dso, triggerpid, cpu, time, _, *other = parts
+                tracepoint = True
+            else:  # counter etc
+                dso, triggerpid, time, count, _, *other = parts
+                tracepoint = False
+            triggerpid = int(triggerpid)
+            time = float(time[:-1]) * 1e6
+            if time_first is None:
+                time_first = time
+            def log(*args, time=time/1e6):
+                offset = time - time_first/1e6
+                print(f"{time:13.6f}[+{offset:5.4f}]", *args)
+            if args.all_tracepoints and tracepoint:
+                trace_events.append({'name': event, 'pid': parent_pid.get(pid, pid), 'tid': triggerpid, 'ts': time, 'ph': 'i', 's': 'g'})
+            first_line = False
+            gil_event = None
+            if event == "sched:sched_switch":
+                # e.g. python 393320 [040] 3498299.441431:                sched:sched_switch: prev_comm=python prev_pid=393320 prev_prio=120 prev_state=S ==> next_comm=swapper/40 next_pid=0 next_prio=120
+                values = parse_values(parts, prev_pid=int)
+                # we are going to sleep?
+                pid = values['prev_pid']
+                if values['prev_state'] == 'R':
+                    # this happens when a process just started, so we just set the start time
+                    last_sleep_time[pid] = time
+                    continue
+                # if values['prev_comm'] != "python":
+                #     if verbose >= 2:
+                #         log(f'Skipping switch from {values["prev_comm"]}')
+                name = pid_names.get(pid, pid)
+                if pid not in last_run_time:
+                    # raise ValueError(f'pid {pid} not seen running before, only {last_run_time}')
+                    continue
+                dur = time  - last_run_time[pid]
+                if verbose:
+                    log(f'{name} will switch to state={values["prev_state"]}, ran for {dur}')
+                if store_runing:
+                    event = {"pid": parent_pid.get(pid, pid), "tid": pid, "ts": last_run_time[pid], "dur": dur, "name": 'R', "ph": "X", "cat": "process state"}
+                    trace_events.append(event)
+
+                last_sleep_time[pid] = time
+                del last_run_time[pid]
+            elif event == "sched:sched_wakeup":
+                # e.g: swapper     0 [040] 3498299.642199:                sched:sched_waking: comm=python pid=393320 prio=120 target_cpu=040
+                prev_state = None
+                values = parse_values(parts, pid=int)
+                pid = values['pid']
+                comm = values['comm']
+                # if comm != "python":
+                #     if verbose >= 2:
+                #         log(f'Skip waking event for {comm}')
+                #     continue
+                if pid not in last_sleep_time:
+                    # raise ValueError(f'pid {pid} not seen sleeping before, only {last_sleep_time}')
+                    # this can happen when we did not see the creation
+                    # q
+                    last_run_time[pid] = time
+                    continue
+                if verbose:
+                    name = pid_names.get(pid, pid)
+                    log(f'Waking up {name}')
+                if store_sleeping:
+                    dur = time  - last_sleep_time[pid]
+                    event = {"pid": parent_pid.get(pid, pid), "tid": pid, "ts": last_sleep_time[pid], "dur": dur, "name": f"S", "ph": "X", "cat": "process state"} #3579742.654826
+                    trace_events.append(event)
+                last_run_time[pid] = time
+                del last_sleep_time[pid]
+            elif event == "sched:sched_process_exec":
+                if verbose:
+                    name = pid_names.get(triggerpid, triggerpid)
+                    log(f'Starting (exec) {name}')
+            elif event == "sched:sched_wakeup_new":
+                # e.g: swapper     0 [040] 3498299.642199:                sched:sched_waking: comm=python pid=393320 prio=120 target_cpu=040
+                values = parse_values(parts, pid=int)
+                pid = values['pid']
+                if verbose:
+                    name = pid_names.get(pid, pid)
+                    log(f'Starting (new) {name}')
+                last_run_time[pid] = time
+            elif event == "sched:sched_process_fork":
+                values = parse_values(parts, pid=int, child_pid=int)
+                # set up a child parent relationship for better visualization
+                pid, child_pid = values['pid'], values['child_pid']
+                if verbose:
+                    log(f'Process {pid} forked {child_pid}')
+                parent_pid[child_pid] = pid
+            elif not tracepoint:
+                event = {"pid": 'counters', "ts": time, "name": event, "ph": "C", "args": {event: count}}
+                trace_events.append(event)
+            else:
+                if verbose >= 2:
+                    print("SKIP", header)
+                pass
+        except BrokenPipeError:
+            break
+        except:
+            print("error on line", header)
+            raise
+            
+    with open(args.output, 'w') as f:
+        json.dump({'traceEvents': trace_events}, f)
+    print(f"Wrote to {args.output}")
+
+
+if __name__ == '__main__':
+    main()