Skip to content

Commit 030cf12

Browse files
authored
Merge pull request #1406 from StoDevX/logtimes
add "analyze gym output" script
2 parents 57cf845 + 59b5ab4 commit 030cf12

File tree

1 file changed

+213
-0
lines changed

1 file changed

+213
-0
lines changed

scripts/analyze-gym.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
#!/usr/bin/env python3
2+
3+
"""Usage: analyze-gym.py < travis-ios-log.log
4+
5+
You can download log files with the `travis` gem:
6+
$ travis logs 3498.3 > 3498.3.log
7+
8+
Then just pass that logfile to this script's stdin.
9+
$ python3 analyze-gym.py < 3498.3.log
10+
11+
Options:
12+
-h, --help Print this message
13+
-v Print the input lines as they are processed
14+
-m Change the "module" minimum time filter
15+
-s Change the "step" minimum time filter
16+
-d Print the python object for each step
17+
"""
18+
19+
import sys
20+
import re
21+
import json
22+
from pprint import pprint
23+
from datetime import timedelta
24+
25+
ansi_escape = re.compile(r'\x1b[^m]*m')
26+
fltime_pattern = r'\[(?P<hours>\d{1,2}):(?P<minutes>\d{1,2}):(?P<seconds>\d{1,2})\]'
27+
fltime_regex = re.compile(fltime_pattern)
28+
29+
30+
def remove_control_chars(string):
31+
"""Remove ANSI control codes from a string"""
32+
return ansi_escape.sub('', string)
33+
34+
35+
def to_timedelta(fastlanetime):
36+
"""Takes a 'fastlane time', like 20:52:42, and turns it
37+
into a timedelta from midnight.
38+
39+
>>> to_timedelta('20:52:42')
40+
timedelta(days=0, seconds=75162)
41+
"""
42+
groups = fltime_regex.match(fastlanetime).groupdict()
43+
return timedelta(**{k: int(v) for k, v in groups.items()})
44+
45+
46+
def pick(dictionary, *keys):
47+
"""Return a dictionary comprised of _only_ these keys.
48+
49+
>>> pick({1: 'a', 2: 'b'}, 1)
50+
{1: 'a'}
51+
"""
52+
return {k: v for k, v in dictionary.items() if k in keys}
53+
54+
55+
def unpick(dictionary, *keys):
56+
"""Return a dictionary comprised of everything _but_ these keys.
57+
58+
>>> pick({1: 'a', 2: 'b'}, 1)
59+
{2: 'b'}
60+
"""
61+
return {k: v for k, v in dictionary.items() if k not in keys}
62+
63+
64+
def pretty_td(delta):
65+
"""Turn a timedelta instance into a formatted string
66+
67+
>>> pretty_td(timedelta(minutes=3, seconds=32))
68+
'3m 32s'
69+
"""
70+
days, seconds = delta.days, delta.seconds
71+
hours = seconds // 3600
72+
minutes = (seconds % 3600) // 60
73+
seconds = (seconds % 60)
74+
# return days, hours, minutes, seconds
75+
return f'{minutes}m {seconds:02}s'
76+
77+
78+
def extract_gym_log(stream):
79+
"""Given a stream of text, extract the chunk that is the
80+
relevant `gym` log.
81+
"""
82+
start_gym_regex = re.compile(fltime_pattern + ': --- Step: gym ---')
83+
stop_gym_regex = re.compile(fltime_pattern + ': Cruising back')
84+
85+
reading = False
86+
for line in stream:
87+
line = remove_control_chars(line).strip()
88+
89+
if not reading and start_gym_regex.match(line):
90+
reading = True
91+
elif reading and stop_gym_regex.match(line):
92+
reading = False
93+
break
94+
95+
if reading:
96+
yield line
97+
98+
99+
def process(stream, *, verbose=False):
100+
"""Given a stream of text, process it into a data structure
101+
of how long various modules took to run.
102+
"""
103+
104+
time_regex = r'^(?P<time>' + fltime_pattern + r'): [^ ]+ '
105+
106+
# Indicates the start of a "module"
107+
module_regex = re.compile(time_regex + r'Building (?P<file>.*) \[(?P<mode>.*)\]$')
108+
109+
# The "verbs" are the different things that xcbuild? prints out as the steps go by
110+
verbs = ['Copying', 'Compiling', 'Building library', 'Linking', 'Processing', 'Generating', 'Running script', 'Touching', 'Signing']
111+
action_regex = re.compile(time_regex + f'(?P<action>{"|".join(verbs)}) ' + r'(?P<file>.*)$')
112+
113+
# Indicates the end of the log
114+
# (kinda a duplicate of stop_gym_regex)
115+
done_regex = re.compile(time_regex + r'Archive Succeeded')
116+
117+
modules = {}
118+
current_module_name = None
119+
for line in extract_gym_log(stream):
120+
if verbose:
121+
print(line, end='')
122+
123+
module_match = module_regex.match(line)
124+
action_match = action_regex.match(line)
125+
done_match = done_regex.match(line)
126+
127+
if module_match:
128+
if current_module_name is not None:
129+
# add the "end times" to the just-finished module
130+
last_module = modules[current_module_name]
131+
last_module['end'] = to_timedelta(module_match['time'])
132+
last_module['duration'] = last_module['end'] - last_module['start']
133+
134+
last_step = last_module['steps'][-1]
135+
last_step['end'] = last_module['end']
136+
last_step['duration'] = last_step['end'] - last_step['start']
137+
138+
current_module_name = module_match['file']
139+
modules[current_module_name] = {
140+
'start': to_timedelta(module_match['time']),
141+
'end': None,
142+
'duration': None,
143+
'mode': module_match['mode'],
144+
'steps': [],
145+
}
146+
147+
continue
148+
149+
elif action_match:
150+
if modules[current_module_name].get('steps', None):
151+
# add the "end times" to the previous step
152+
prior_step = modules[current_module_name]['steps'][-1]
153+
prior_step['end'] = to_timedelta(action_match['time'])
154+
prior_step['duration'] = prior_step['end'] - prior_step['start']
155+
156+
# make file paths relative to the project
157+
needle = 'AAO-React-Native/'
158+
filepath = action_match['file']
159+
if needle in filepath:
160+
filepath = filepath[filepath.index(needle) + len(needle):]
161+
162+
modules[current_module_name]['steps'].append({
163+
'start': to_timedelta(action_match['time']),
164+
'file': filepath,
165+
'action': action_match['action'],
166+
'end': None,
167+
'duration': None,
168+
})
169+
170+
elif done_match:
171+
m = modules[current_module_name]
172+
m['end'] = to_timedelta(done_match['time'])
173+
m['duration'] = m['end'] - m['start']
174+
m['steps'][-1]['end'] = to_timedelta(done_match['time'])
175+
176+
if modules[current_module_name].get('steps', None):
177+
# add the "end times" to the final step
178+
prior_step = modules[current_module_name]['steps'][-1]
179+
prior_step['end'] = to_timedelta(done_match['time'])
180+
prior_step['duration'] = prior_step['end'] - prior_step['start']
181+
182+
break
183+
184+
return modules
185+
186+
187+
def analyze(modules, *, module_duration_filter=10, step_duration_filter=1, debug=False):
188+
"""Pretty-print the analysis of how long the various modules
189+
took to build.
190+
"""
191+
for name, module in modules.items():
192+
if module['duration'] >= timedelta(seconds=module_duration_filter):
193+
if debug:
194+
pprint({name: unpick(module, 'steps')})
195+
print(f'{pretty_td(module["duration"])}: {name} [{module["mode"]}]')
196+
197+
for step in module['steps']:
198+
if step['duration'] >= timedelta(seconds=step_duration_filter):
199+
if debug:
200+
pprint(step)
201+
print(f' {pretty_td(step["duration"])}: {step["action"]} {step["file"]}')
202+
203+
204+
if __name__ == '__main__':
205+
args = sys.argv
206+
if '-h' in args or '--help' in args:
207+
print(__doc__)
208+
sys.exit(1)
209+
210+
analyze(process(sys.stdin, verbose='-v' in args),
211+
module_duration_filter=int(args[args.index('-m') + 1]) if '-m' in args else 10,
212+
step_duration_filter=int(args[args.index('-s') + 1]) if '-s' in args else 1,
213+
debug='-d' in args)

0 commit comments

Comments
 (0)