1#!/usr/bin/env python3
2#
3# Copyright 2018 Google LLC.
4#
5# This software may be used and distributed according to the terms of the
6# GNU General Public License version 2 or any later version.
7"""Tool read primitive events from a pipe to produce a catapult trace.
8
9Usage:
10    Terminal 1: $ catapipe.py /tmp/mypipe /tmp/trace.json
11    Terminal 2: $ HGCATAPULTSERVERPIPE=/tmp/mypipe hg root
12    <ctrl-c catapipe.py in Terminal 1>
13    $ catapult/tracing/bin/trace2html /tmp/trace.json  # produce /tmp/trace.html
14    <open trace.html in your browser of choice; the WASD keys are very useful>
15    (catapult is located at https://github.com/catapult-project/catapult)
16
17For now the event stream supports
18
19  START $SESSIONID ...
20
21and
22
23  END $SESSIONID ...
24
25events. Everything after the SESSIONID (which must not contain spaces)
26is used as a label for the event. Events are timestamped as of when
27they arrive in this process and are then used to produce catapult
28traces that can be loaded in Chrome's about:tracing utility. It's
29important that the event stream *into* this process stay simple,
30because we have to emit it from the shell scripts produced by
31run-tests.py.
32
33Typically you'll want to place the path to the named pipe in the
34HGCATAPULTSERVERPIPE environment variable, which both run-tests and hg
35understand. To trace *only* run-tests, use HGTESTCATAPULTSERVERPIPE instead.
36"""
37from __future__ import absolute_import, print_function
38
39import argparse
40import json
41import os
42import timeit
43
44_TYPEMAP = {
45    'START': 'B',
46    'END': 'E',
47    'COUNTER': 'C',
48}
49
50_threadmap = {}
51
52# Timeit already contains the whole logic about which timer to use based on
53# Python version and OS
54timer = timeit.default_timer
55
56
57def main():
58    parser = argparse.ArgumentParser()
59    parser.add_argument(
60        'pipe',
61        type=str,
62        nargs=1,
63        help='Path of named pipe to create and listen on.',
64    )
65    parser.add_argument(
66        'output',
67        default='trace.json',
68        type=str,
69        nargs='?',
70        help='Path of json file to create where the traces ' 'will be stored.',
71    )
72    parser.add_argument(
73        '--debug',
74        default=False,
75        action='store_true',
76        help='Print useful debug messages',
77    )
78    args = parser.parse_args()
79    fn = args.pipe[0]
80    os.mkfifo(fn)
81    try:
82        with open(fn) as f, open(args.output, 'w') as out:
83            out.write('[\n')
84            start = timer()
85            while True:
86                ev = f.readline().strip()
87                if not ev:
88                    continue
89                now = timer()
90                if args.debug:
91                    print(ev)
92                verb, session, label = ev.split(' ', 2)
93                if session not in _threadmap:
94                    _threadmap[session] = len(_threadmap)
95                if verb == 'COUNTER':
96                    amount, label = label.split(' ', 1)
97                    payload_args = {'value': int(amount)}
98                else:
99                    payload_args = {}
100                pid = _threadmap[session]
101                ts_micros = (now - start) * 1000000
102                out.write(
103                    json.dumps(
104                        {
105                            "name": label,
106                            "cat": "misc",
107                            "ph": _TYPEMAP[verb],
108                            "ts": ts_micros,
109                            "pid": pid,
110                            "tid": 1,
111                            "args": payload_args,
112                        }
113                    )
114                )
115                out.write(',\n')
116    finally:
117        os.unlink(fn)
118
119
120if __name__ == '__main__':
121    main()
122