summaryrefslogtreecommitdiffstats
path: root/scripts/buildstats-diff
diff options
context:
space:
mode:
authorMarkus Lehtonen <markus.lehtonen@linux.intel.com>2017-09-15 16:04:37 +0300
committerRichard Purdie <richard.purdie@linuxfoundation.org>2017-09-18 11:07:30 +0100
commitb5fb3dd904cd22212c720f3c79d71952ecbaa9c2 (patch)
tree69620698ccaa22f97cfc7b3eb8be3e3118086c19 /scripts/buildstats-diff
parent873707489fafaa5e1bd43b03b068df74d5956b00 (diff)
downloadpoky-b5fb3dd904cd22212c720f3c79d71952ecbaa9c2.tar.gz
scripts/buildstats-diff: move code to lib/buildstats.py
Move over code from buildstats-diff to new scripts/lib/buildstats.py module in order to share code related to buildstats processing. Also, refactor the code, introducing new classes to make the code readable, maintainable and easier to debug. [YOCTO #11381] (From OE-Core rev: 8a2cd9afc95919737d8e75234e78bbc52e1494a1) Signed-off-by: Markus Lehtonen <markus.lehtonen@linux.intel.com> Signed-off-by: Ross Burton <ross.burton@intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'scripts/buildstats-diff')
-rwxr-xr-xscripts/buildstats-diff278
1 files changed, 28 insertions, 250 deletions
diff --git a/scripts/buildstats-diff b/scripts/buildstats-diff
index 8e64480eb3..ce82dabee9 100755
--- a/scripts/buildstats-diff
+++ b/scripts/buildstats-diff
@@ -15,15 +15,18 @@
15# 15#
16import argparse 16import argparse
17import glob 17import glob
18import json
19import logging 18import logging
20import math 19import math
21import os 20import os
22import re
23import sys 21import sys
24from collections import namedtuple
25from operator import attrgetter 22from operator import attrgetter
26 23
24# Import oe libs
25scripts_path = os.path.dirname(os.path.realpath(__file__))
26sys.path.append(os.path.join(scripts_path, 'lib'))
27from buildstats import BuildStats, diff_buildstats, taskdiff_fields
28
29
27# Setup logging 30# Setup logging
28logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") 31logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
29log = logging.getLogger() 32log = logging.getLogger()
@@ -34,196 +37,16 @@ class ScriptError(Exception):
34 pass 37 pass
35 38
36 39
37taskdiff_fields = ('pkg', 'pkg_op', 'task', 'task_op', 'value1', 'value2',
38 'absdiff', 'reldiff')
39TaskDiff = namedtuple('TaskDiff', ' '.join(taskdiff_fields))
40
41
42class BSTask(dict):
43 def __init__(self, *args, **kwargs):
44 self['start_time'] = None
45 self['elapsed_time'] = None
46 self['status'] = None
47 self['iostat'] = {}
48 self['rusage'] = {}
49 self['child_rusage'] = {}
50 super(BSTask, self).__init__(*args, **kwargs)
51
52 @property
53 def cputime(self):
54 """Sum of user and system time taken by the task"""
55 rusage = self['rusage']['ru_stime'] + self['rusage']['ru_utime']
56 if self['child_rusage']:
57 # Child rusage may have been optimized out
58 return rusage + self['child_rusage']['ru_stime'] + self['child_rusage']['ru_utime']
59 else:
60 return rusage
61
62 @property
63 def walltime(self):
64 """Elapsed wall clock time"""
65 return self['elapsed_time']
66
67 @property
68 def read_bytes(self):
69 """Bytes read from the block layer"""
70 return self['iostat']['read_bytes']
71
72 @property
73 def write_bytes(self):
74 """Bytes written to the block layer"""
75 return self['iostat']['write_bytes']
76
77 @property
78 def read_ops(self):
79 """Number of read operations on the block layer"""
80 if self['child_rusage']:
81 # Child rusage may have been optimized out
82 return self['rusage']['ru_inblock'] + self['child_rusage']['ru_inblock']
83 else:
84 return self['rusage']['ru_inblock']
85
86 @property
87 def write_ops(self):
88 """Number of write operations on the block layer"""
89 if self['child_rusage']:
90 # Child rusage may have been optimized out
91 return self['rusage']['ru_oublock'] + self['child_rusage']['ru_oublock']
92 else:
93 return self['rusage']['ru_oublock']
94
95
96def read_buildstats_file(buildstat_file):
97 """Convert buildstat text file into dict/json"""
98 bs_task = BSTask()
99 log.debug("Reading task buildstats from %s", buildstat_file)
100 end_time = None
101 with open(buildstat_file) as fobj:
102 for line in fobj.readlines():
103 key, val = line.split(':', 1)
104 val = val.strip()
105 if key == 'Started':
106 start_time = float(val)
107 bs_task['start_time'] = start_time
108 elif key == 'Ended':
109 end_time = float(val)
110 elif key.startswith('IO '):
111 split = key.split()
112 bs_task['iostat'][split[1]] = int(val)
113 elif key.find('rusage') >= 0:
114 split = key.split()
115 ru_key = split[-1]
116 if ru_key in ('ru_stime', 'ru_utime'):
117 val = float(val)
118 else:
119 val = int(val)
120 ru_type = 'rusage' if split[0] == 'rusage' else \
121 'child_rusage'
122 bs_task[ru_type][ru_key] = val
123 elif key == 'Status':
124 bs_task['status'] = val
125 if end_time is not None and start_time is not None:
126 bs_task['elapsed_time'] = end_time - start_time
127 else:
128 raise ScriptError("{} looks like a invalid buildstats file".format(buildstat_file))
129 return bs_task
130
131
132def read_buildstats_dir(bs_dir):
133 """Read buildstats directory"""
134 def split_nevr(nevr):
135 """Split name and version information from recipe "nevr" string"""
136 n_e_v, revision = nevr.rsplit('-', 1)
137 match = re.match(r'^(?P<name>\S+)-((?P<epoch>[0-9]{1,5})_)?(?P<version>[0-9]\S*)$',
138 n_e_v)
139 if not match:
140 # If we're not able to parse a version starting with a number, just
141 # take the part after last dash
142 match = re.match(r'^(?P<name>\S+)-((?P<epoch>[0-9]{1,5})_)?(?P<version>[^-]+)$',
143 n_e_v)
144 name = match.group('name')
145 version = match.group('version')
146 epoch = match.group('epoch')
147 return name, epoch, version, revision
148
149 if not os.path.isfile(os.path.join(bs_dir, 'build_stats')):
150 raise ScriptError("{} does not look like a buildstats directory".format(bs_dir))
151
152 log.debug("Reading buildstats directory %s", bs_dir)
153
154 buildstats = {}
155 subdirs = os.listdir(bs_dir)
156 for dirname in subdirs:
157 recipe_dir = os.path.join(bs_dir, dirname)
158 if not os.path.isdir(recipe_dir):
159 continue
160 name, epoch, version, revision = split_nevr(dirname)
161 recipe_bs = {'nevr': dirname,
162 'name': name,
163 'epoch': epoch,
164 'version': version,
165 'revision': revision,
166 'tasks': {}}
167 for task in os.listdir(recipe_dir):
168 recipe_bs['tasks'][task] = [read_buildstats_file(
169 os.path.join(recipe_dir, task))]
170 if name in buildstats:
171 raise ScriptError("Cannot handle multiple versions of the same "
172 "package ({})".format(name))
173 buildstats[name] = recipe_bs
174
175 return buildstats
176
177
178def bs_append(dst, src):
179 """Append data from another buildstats"""
180 if set(dst.keys()) != set(src.keys()):
181 raise ScriptError("Refusing to join buildstats, set of packages is "
182 "different")
183 for pkg, data in dst.items():
184 if data['nevr'] != src[pkg]['nevr']:
185 raise ScriptError("Refusing to join buildstats, package version "
186 "differs: {} vs. {}".format(data['nevr'], src[pkg]['nevr']))
187 if set(data['tasks'].keys()) != set(src[pkg]['tasks'].keys()):
188 raise ScriptError("Refusing to join buildstats, set of tasks "
189 "in {} differ".format(pkg))
190 for taskname, taskdata in data['tasks'].items():
191 taskdata.extend(src[pkg]['tasks'][taskname])
192
193
194def read_buildstats_json(path):
195 """Read buildstats from JSON file"""
196 buildstats = {}
197 with open(path) as fobj:
198 bs_json = json.load(fobj)
199 for recipe_bs in bs_json:
200 if recipe_bs['name'] in buildstats:
201 raise ScriptError("Cannot handle multiple versions of the same "
202 "package ({})".format(recipe_bs['name']))
203
204 if recipe_bs['epoch'] is None:
205 recipe_bs['nevr'] = "{}-{}-{}".format(recipe_bs['name'], recipe_bs['version'], recipe_bs['revision'])
206 else:
207 recipe_bs['nevr'] = "{}-{}_{}-{}".format(recipe_bs['name'], recipe_bs['epoch'], recipe_bs['version'], recipe_bs['revision'])
208
209 for task, data in recipe_bs['tasks'].copy().items():
210 recipe_bs['tasks'][task] = [BSTask(data)]
211
212 buildstats[recipe_bs['name']] = recipe_bs
213
214 return buildstats
215
216
217def read_buildstats(path, multi): 40def read_buildstats(path, multi):
218 """Read buildstats""" 41 """Read buildstats"""
219 if not os.path.exists(path): 42 if not os.path.exists(path):
220 raise ScriptError("No such file or directory: {}".format(path)) 43 raise ScriptError("No such file or directory: {}".format(path))
221 44
222 if os.path.isfile(path): 45 if os.path.isfile(path):
223 return read_buildstats_json(path) 46 return BuildStats.from_file_json(path)
224 47
225 if os.path.isfile(os.path.join(path, 'build_stats')): 48 if os.path.isfile(os.path.join(path, 'build_stats')):
226 return read_buildstats_dir(path) 49 return BuildStats.from_dir(path)
227 50
228 # Handle a non-buildstat directory 51 # Handle a non-buildstat directory
229 subpaths = sorted(glob.glob(path + '/*')) 52 subpaths = sorted(glob.glob(path + '/*'))
@@ -238,17 +61,16 @@ def read_buildstats(path, multi):
238 bs = None 61 bs = None
239 for subpath in subpaths: 62 for subpath in subpaths:
240 if os.path.isfile(subpath): 63 if os.path.isfile(subpath):
241 tmpbs = read_buildstats_json(subpath) 64 _bs = BuildStats.from_file_json(subpath)
242 else: 65 else:
243 tmpbs = read_buildstats_dir(subpath) 66 _bs = BuildStats.from_dir(subpath)
244 if not bs: 67 if bs is None:
245 bs = tmpbs 68 bs = _bs
246 else: 69 else:
247 log.debug("Joining buildstats") 70 bs.aggregate(_bs)
248 bs_append(bs, tmpbs)
249
250 if not bs: 71 if not bs:
251 raise ScriptError("No buildstats found under {}".format(path)) 72 raise ScriptError("No buildstats found under {}".format(path))
73
252 return bs 74 return bs
253 75
254 76
@@ -266,11 +88,11 @@ def print_ver_diff(bs1, bs2):
266 common_pkgs = pkgs2.intersection(pkgs1) 88 common_pkgs = pkgs2.intersection(pkgs1)
267 if common_pkgs: 89 if common_pkgs:
268 for pkg in common_pkgs: 90 for pkg in common_pkgs:
269 if bs1[pkg]['epoch'] != bs2[pkg]['epoch']: 91 if bs1[pkg].epoch != bs2[pkg].epoch:
270 echanged.append(pkg) 92 echanged.append(pkg)
271 elif bs1[pkg]['version'] != bs2[pkg]['version']: 93 elif bs1[pkg].version != bs2[pkg].version:
272 vchanged.append(pkg) 94 vchanged.append(pkg)
273 elif bs1[pkg]['revision'] != bs2[pkg]['revision']: 95 elif bs1[pkg].revision != bs2[pkg].revision:
274 rchanged.append(pkg) 96 rchanged.append(pkg)
275 else: 97 else:
276 unchanged.append(pkg) 98 unchanged.append(pkg)
@@ -288,37 +110,37 @@ def print_ver_diff(bs1, bs2):
288 print("\nNEW PACKAGES:") 110 print("\nNEW PACKAGES:")
289 print("-------------") 111 print("-------------")
290 for pkg in sorted(new_pkgs): 112 for pkg in sorted(new_pkgs):
291 print(fmt_str.format(pkg, bs2[pkg]['nevr'], maxlen=maxlen)) 113 print(fmt_str.format(pkg, bs2[pkg].nevr, maxlen=maxlen))
292 114
293 if deleted_pkgs: 115 if deleted_pkgs:
294 print("\nDELETED PACKAGES:") 116 print("\nDELETED PACKAGES:")
295 print("-----------------") 117 print("-----------------")
296 for pkg in sorted(deleted_pkgs): 118 for pkg in sorted(deleted_pkgs):
297 print(fmt_str.format(pkg, bs1[pkg]['nevr'], maxlen=maxlen)) 119 print(fmt_str.format(pkg, bs1[pkg].nevr, maxlen=maxlen))
298 120
299 fmt_str = " {0:{maxlen}} {1:<20} ({2})" 121 fmt_str = " {0:{maxlen}} {1:<20} ({2})"
300 if rchanged: 122 if rchanged:
301 print("\nREVISION CHANGED:") 123 print("\nREVISION CHANGED:")
302 print("-----------------") 124 print("-----------------")
303 for pkg in sorted(rchanged): 125 for pkg in sorted(rchanged):
304 field1 = "{} -> {}".format(pkg, bs1[pkg]['revision'], bs2[pkg]['revision']) 126 field1 = "{} -> {}".format(pkg, bs1[pkg].revision, bs2[pkg].revision)
305 field2 = "{} -> {}".format(bs1[pkg]['nevr'], bs2[pkg]['nevr']) 127 field2 = "{} -> {}".format(bs1[pkg].nevr, bs2[pkg].nevr)
306 print(fmt_str.format(pkg, field1, field2, maxlen=maxlen)) 128 print(fmt_str.format(pkg, field1, field2, maxlen=maxlen))
307 129
308 if vchanged: 130 if vchanged:
309 print("\nVERSION CHANGED:") 131 print("\nVERSION CHANGED:")
310 print("----------------") 132 print("----------------")
311 for pkg in sorted(vchanged): 133 for pkg in sorted(vchanged):
312 field1 = "{} -> {}".format(bs1[pkg]['version'], bs2[pkg]['version']) 134 field1 = "{} -> {}".format(bs1[pkg].version, bs2[pkg].version)
313 field2 = "{} -> {}".format(bs1[pkg]['nevr'], bs2[pkg]['nevr']) 135 field2 = "{} -> {}".format(bs1[pkg].nevr, bs2[pkg].nevr)
314 print(fmt_str.format(pkg, field1, field2, maxlen=maxlen)) 136 print(fmt_str.format(pkg, field1, field2, maxlen=maxlen))
315 137
316 if echanged: 138 if echanged:
317 print("\nEPOCH CHANGED:") 139 print("\nEPOCH CHANGED:")
318 print("--------------") 140 print("--------------")
319 for pkg in sorted(echanged): 141 for pkg in sorted(echanged):
320 field1 = "{} -> {}".format(bs1[pkg]['epoch'], bs2[pkg]['epoch']) 142 field1 = "{} -> {}".format(bs1[pkg].epoch, bs2[pkg].epoch)
321 field2 = "{} -> {}".format(bs1[pkg]['nevr'], bs2[pkg]['nevr']) 143 field2 = "{} -> {}".format(bs1[pkg].nevr, bs2[pkg].nevr)
322 print(fmt_str.format(pkg, field1, field2, maxlen=maxlen)) 144 print(fmt_str.format(pkg, field1, field2, maxlen=maxlen))
323 145
324 146
@@ -359,12 +181,10 @@ def print_task_diff(bs1, bs2, val_type, min_val=0, min_absdiff=0, sort_by=('absd
359 """Get cumulative sum of all tasks""" 181 """Get cumulative sum of all tasks"""
360 total = 0.0 182 total = 0.0
361 for recipe_data in buildstats.values(): 183 for recipe_data in buildstats.values():
362 for bs_task in recipe_data['tasks'].values(): 184 for bs_task in recipe_data.tasks.values():
363 total += sum([getattr(b, val_type) for b in bs_task]) / len(bs_task) 185 total += getattr(bs_task, val_type)
364 return total 186 return total
365 187
366 tasks_diff = []
367
368 if min_val: 188 if min_val:
369 print("Ignoring tasks less than {} ({})".format( 189 print("Ignoring tasks less than {} ({})".format(
370 val_to_str(min_val, True), val_to_str(min_val))) 190 val_to_str(min_val, True), val_to_str(min_val)))
@@ -373,49 +193,7 @@ def print_task_diff(bs1, bs2, val_type, min_val=0, min_absdiff=0, sort_by=('absd
373 val_to_str(min_absdiff, True), val_to_str(min_absdiff))) 193 val_to_str(min_absdiff, True), val_to_str(min_absdiff)))
374 194
375 # Prepare the data 195 # Prepare the data
376 pkgs = set(bs1.keys()).union(set(bs2.keys())) 196 tasks_diff = diff_buildstats(bs1, bs2, val_type, min_val, min_absdiff)
377 for pkg in pkgs:
378 tasks1 = bs1[pkg]['tasks'] if pkg in bs1 else {}
379 tasks2 = bs2[pkg]['tasks'] if pkg in bs2 else {}
380 if not tasks1:
381 pkg_op = '+ '
382 elif not tasks2:
383 pkg_op = '- '
384 else:
385 pkg_op = ' '
386
387 for task in set(tasks1.keys()).union(set(tasks2.keys())):
388 task_op = ' '
389 if task in tasks1:
390 # Average over all values
391 val1 = [getattr(b, val_type) for b in bs1[pkg]['tasks'][task]]
392 val1 = sum(val1) / len(val1)
393 else:
394 task_op = '+ '
395 val1 = 0
396 if task in tasks2:
397 # Average over all values
398 val2 = [getattr(b, val_type) for b in bs2[pkg]['tasks'][task]]
399 val2 = sum(val2) / len(val2)
400 else:
401 val2 = 0
402 task_op = '- '
403
404 if val1 == 0:
405 reldiff = float('inf')
406 else:
407 reldiff = 100 * (val2 - val1) / val1
408
409 if max(val1, val2) < min_val:
410 log.debug("Filtering out %s:%s (%s)", pkg, task,
411 val_to_str(max(val1, val2)))
412 continue
413 if abs(val2 - val1) < min_absdiff:
414 log.debug("Filtering out %s:%s (difference of %s)", pkg, task,
415 val_to_str(val2-val1))
416 continue
417 tasks_diff.append(TaskDiff(pkg, pkg_op, task, task_op, val1, val2,
418 val2-val1, reldiff))
419 197
420 # Sort our list 198 # Sort our list
421 for field in reversed(sort_by): 199 for field in reversed(sort_by):