oe-build-perf-report: task and recipe details in html report
cover oe-build-perf-report: task and recipe details in html report
This patchset improves the html report generated by scripts/oe-build-perf-report in two ways. First, it will show a short summary of the bitbake tasks of a build. The second part of the patchset will show recipe version changes between two builds.
[YOCTO #11381]
[YOCTO #11382]
The following changes since commit d72d116e0228cc0e4391d0558b2d32c3fd5e399c:
README.qemu: Add from meta-yocto as it belongs in core (2017-09-14 12:39:29 +0100)
are available in the git repository at:
git://git.openembedded.org/openembedded-core-contrib marquiz/fixes-11381
http://cgit.openembedded.org/openembedded-core-contrib/log/?h=marquiz/fixes-11381
Markus Lehtonen (4):
scripts/buildstats-diff: move code to lib/buildstats.py scripts/oe-build-perf-report: summary of task resource usage scripts/buildstats-diff: move more code to lib/buildstats.py scripts/oe-build-perf-report: show recipe version changes in html
report
scripts/buildstats-diff | 340 +++++-------------------------- scripts/lib/build_perf/html/report.html | 80 +++++++- scripts/lib/buildstats.py | 349 ++++++++++++++++++++++++++++++++ scripts/oe-build-perf-report | 105 +++++++--- 4 files changed, 556 insertions(+), 318 deletions(-) create mode 100644 scripts/lib/buildstats.py
Signature · 1 line
2.12.3
PATCH 1/4 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]
--- scripts/buildstats-diff | 278 +++++------------------------------------- scripts/lib/buildstats.py | 301 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 329 insertions(+), 250 deletions(-) create mode 100644 scripts/lib/buildstats.py
scripts/buildstats-diff +28 -250
@@ -15,15 +15,18 @@ # import argparse import glob -import json import logging import math import os -import re import sys -from collections import namedtuple from operator import attrgetter +# Import oe libs +scripts_path = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(scripts_path, 'lib')) +from buildstats import BuildStats, diff_buildstats, taskdiff_fields + + # Setup logging logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") log = logging.getLogger()
@@ -34,196 +37,16 @@ class ScriptError(Exception): pass -taskdiff_fields = ('pkg', 'pkg_op', 'task', 'task_op', 'value1', 'value2', - 'absdiff', 'reldiff') -TaskDiff = namedtuple('TaskDiff', ' '.join(taskdiff_fields)) - - -class BSTask(dict): - def __init__(self, *args, **kwargs): - self['start_time'] = None - self['elapsed_time'] = None - self['status'] = None - self['iostat'] = {} - self['rusage'] = {} - self['child_rusage'] = {} - super(BSTask, self).__init__(*args, **kwargs) - - @property - def cputime(self): - """Sum of user and system time taken by the task""" - rusage = self['rusage']['ru_stime'] + self['rusage']['ru_utime'] - if self['child_rusage']: - # Child rusage may have been optimized out - return rusage + self['child_rusage']['ru_stime'] + self['child_rusage']['ru_utime'] - else: - return rusage - - @property - def walltime(self): - """Elapsed wall clock time""" - return self['elapsed_time'] - - @property - def read_bytes(self): - """Bytes read from the block layer""" - return self['iostat']['read_bytes'] - - @property - def write_bytes(self): - """Bytes written to the block layer""" - return self['iostat']['write_bytes'] - - @property - def read_ops(self): - """Number of read operations on the block layer""" - if self['child_rusage']: - # Child rusage may have been optimized out - return self['rusage']['ru_inblock'] + self['child_rusage']['ru_inblock'] - else: - return self['rusage']['ru_inblock'] - - @property - def write_ops(self): - """Number of write operations on the block layer""" - if self['child_rusage']: - # Child rusage may have been optimized out - return self['rusage']['ru_oublock'] + self['child_rusage']['ru_oublock'] - else: - return self['rusage']['ru_oublock'] - - -def read_buildstats_file(buildstat_file): - """Convert buildstat text file into dict/json""" - bs_task = BSTask() - log.debug("Reading task buildstats from %s", buildstat_file) - end_time = None - with open(buildstat_file) as fobj: - for line in fobj.readlines(): - key, val = line.split(':', 1) - val = val.strip() - if key == 'Started': - start_time = float(val) - bs_task['start_time'] = start_time - elif key == 'Ended': - end_time = float(val) - elif key.startswith('IO '): - split = key.split() - bs_task['iostat'][split[1]] = int(val) - elif key.find('rusage') >= 0: - split = key.split() - ru_key = split[-1] - if ru_key in ('ru_stime', 'ru_utime'): - val = float(val) - else: - val = int(val) - ru_type = 'rusage' if split[0] == 'rusage' else \ - 'child_rusage' - bs_task[ru_type][ru_key] = val - elif key == 'Status': - bs_task['status'] = val - if end_time is not None and start_time is not None: - bs_task['elapsed_time'] = end_time - start_time - else: - raise ScriptError("{} looks like a invalid buildstats file".format(buildstat_file)) - return bs_task - - -def read_buildstats_dir(bs_dir): - """Read buildstats directory""" - def split_nevr(nevr): - """Split name and version information from recipe "nevr" string""" - n_e_v, revision = nevr.rsplit('-', 1) - match = re.match(r'^(?P<name>\S+)-((?P<epoch>[0-9]{1,5})_)?(?P<version>[0-9]\S*)$', - n_e_v) - if not match: - # If we're not able to parse a version starting with a number, just - # take the part after last dash - match = re.match(r'^(?P<name>\S+)-((?P<epoch>[0-9]{1,5})_)?(?P<version>[^-]+)$', - n_e_v) - name = match.group('name') - version = match.group('version') - epoch = match.group('epoch') - return name, epoch, version, revision - - if not os.path.isfile(os.path.join(bs_dir, 'build_stats')): - raise ScriptError("{} does not look like a buildstats directory".format(bs_dir)) - - log.debug("Reading buildstats directory %s", bs_dir) - - buildstats = {} - subdirs = os.listdir(bs_dir) - for dirname in subdirs: - recipe_dir = os.path.join(bs_dir, dirname) - if not os.path.isdir(recipe_dir): - continue - name, epoch, version, revision = split_nevr(dirname) - recipe_bs = {'nevr': dirname, - 'name': name, - 'epoch': epoch, - 'version': version, - 'revision': revision, - 'tasks': {}} - for task in os.listdir(recipe_dir): - recipe_bs['tasks'][task] = [read_buildstats_file( - os.path.join(recipe_dir, task))] - if name in buildstats: - raise ScriptError("Cannot handle multiple versions of the same " - "package ({})".format(name)) - buildstats[name] = recipe_bs - - return buildstats - - -def bs_append(dst, src): - """Append data from another buildstats""" - if set(dst.keys()) != set(src.keys()): - raise ScriptError("Refusing to join buildstats, set of packages is " - "different") - for pkg, data in dst.items(): - if data['nevr'] != src[pkg]['nevr']: - raise ScriptError("Refusing to join buildstats, package version " - "differs: {} vs. {}".format(data['nevr'], src[pkg]['nevr'])) - if set(data['tasks'].keys()) != set(src[pkg]['tasks'].keys()): - raise ScriptError("Refusing to join buildstats, set of tasks " - "in {} differ".format(pkg)) - for taskname, taskdata in data['tasks'].items(): - taskdata.extend(src[pkg]['tasks'][taskname]) - - -def read_buildstats_json(path): - """Read buildstats from JSON file""" - buildstats = {} - with open(path) as fobj: - bs_json = json.load(fobj) - for recipe_bs in bs_json: - if recipe_bs['name'] in buildstats: - raise ScriptError("Cannot handle multiple versions of the same " - "package ({})".format(recipe_bs['name'])) - - if recipe_bs['epoch'] is None: - recipe_bs['nevr'] = "{}-{}-{}".format(recipe_bs['name'], recipe_bs['version'], recipe_bs['revision']) - else: - recipe_bs['nevr'] = "{}-{}_{}-{}".format(recipe_bs['name'], recipe_bs['epoch'], recipe_bs['version'], recipe_bs['revision']) - - for task, data in recipe_bs['tasks'].copy().items(): - recipe_bs['tasks'][task] = [BSTask(data)] - - buildstats[recipe_bs['name']] = recipe_bs - - return buildstats - - def read_buildstats(path, multi): """Read buildstats""" if not os.path.exists(path): raise ScriptError("No such file or directory: {}".format(path)) if os.path.isfile(path): - return read_buildstats_json(path) + return BuildStats.from_file_json(path) if os.path.isfile(os.path.join(path, 'build_stats')): - return read_buildstats_dir(path) + return BuildStats.from_dir(path) # Handle a non-buildstat directory subpaths = sorted(glob.glob(path + '/*'))
@@ -238,17 +61,16 @@ def read_buildstats(path, multi): bs = None for subpath in subpaths: if os.path.isfile(subpath): - tmpbs = read_buildstats_json(subpath) + _bs = BuildStats.from_file_json(subpath) else: - tmpbs = read_buildstats_dir(subpath) - if not bs: - bs = tmpbs + _bs = BuildStats.from_dir(subpath) + if bs is None: + bs = _bs else: - log.debug("Joining buildstats") - bs_append(bs, tmpbs) - + bs.aggregate(_bs) if not bs: raise ScriptError("No buildstats found under {}".format(path)) + return bs
@@ -266,11 +88,11 @@ def print_ver_diff(bs1, bs2): common_pkgs = pkgs2.intersection(pkgs1) if common_pkgs: for pkg in common_pkgs: - if bs1[pkg]['epoch'] != bs2[pkg]['epoch']: + if bs1[pkg].epoch != bs2[pkg].epoch: echanged.append(pkg) - elif bs1[pkg]['version'] != bs2[pkg]['version']: + elif bs1[pkg].version != bs2[pkg].version: vchanged.append(pkg) - elif bs1[pkg]['revision'] != bs2[pkg]['revision']: + elif bs1[pkg].revision != bs2[pkg].revision: rchanged.append(pkg) else: unchanged.append(pkg)
@@ -288,37 +110,37 @@ def print_ver_diff(bs1, bs2): print("\nNEW PACKAGES:") print("-------------") for pkg in sorted(new_pkgs): - print(fmt_str.format(pkg, bs2[pkg]['nevr'], maxlen=maxlen)) + print(fmt_str.format(pkg, bs2[pkg].nevr, maxlen=maxlen)) if deleted_pkgs: print("\nDELETED PACKAGES:") print("-----------------") for pkg in sorted(deleted_pkgs): - print(fmt_str.format(pkg, bs1[pkg]['nevr'], maxlen=maxlen)) + print(fmt_str.format(pkg, bs1[pkg].nevr, maxlen=maxlen)) fmt_str = " {0:{maxlen}} {1:<20} ({2})" if rchanged: print("\nREVISION CHANGED:") print("-----------------") for pkg in sorted(rchanged): - field1 = "{} -> {}".format(pkg, bs1[pkg]['revision'], bs2[pkg]['revision']) - field2 = "{} -> {}".format(bs1[pkg]['nevr'], bs2[pkg]['nevr']) + field1 = "{} -> {}".format(pkg, bs1[pkg].revision, bs2[pkg].revision) + field2 = "{} -> {}".format(bs1[pkg].nevr, bs2[pkg].nevr) print(fmt_str.format(pkg, field1, field2, maxlen=maxlen)) if vchanged: print("\nVERSION CHANGED:") print("----------------") for pkg in sorted(vchanged): - field1 = "{} -> {}".format(bs1[pkg]['version'], bs2[pkg]['version']) - field2 = "{} -> {}".format(bs1[pkg]['nevr'], bs2[pkg]['nevr']) + field1 = "{} -> {}".format(bs1[pkg].version, bs2[pkg].version) + field2 = "{} -> {}".format(bs1[pkg].nevr, bs2[pkg].nevr) print(fmt_str.format(pkg, field1, field2, maxlen=maxlen)) if echanged: print("\nEPOCH CHANGED:") print("--------------") for pkg in sorted(echanged): - field1 = "{} -> {}".format(bs1[pkg]['epoch'], bs2[pkg]['epoch']) - field2 = "{} -> {}".format(bs1[pkg]['nevr'], bs2[pkg]['nevr']) + field1 = "{} -> {}".format(bs1[pkg].epoch, bs2[pkg].epoch) + field2 = "{} -> {}".format(bs1[pkg].nevr, bs2[pkg].nevr) print(fmt_str.format(pkg, field1, field2, maxlen=maxlen))
@@ -359,12 +181,10 @@ def print_task_diff(bs1, bs2, val_type, min_val=0, min_absdiff=0, sort_by=('absd """Get cumulative sum of all tasks""" total = 0.0 for recipe_data in buildstats.values(): - for bs_task in recipe_data['tasks'].values(): - total += sum([getattr(b, val_type) for b in bs_task]) / len(bs_task) + for bs_task in recipe_data.tasks.values(): + total += getattr(bs_task, val_type) return total - tasks_diff = [] - if min_val: print("Ignoring tasks less than {} ({})".format( 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 val_to_str(min_absdiff, True), val_to_str(min_absdiff))) # Prepare the data - pkgs = set(bs1.keys()).union(set(bs2.keys())) - for pkg in pkgs: - tasks1 = bs1[pkg]['tasks'] if pkg in bs1 else {} - tasks2 = bs2[pkg]['tasks'] if pkg in bs2 else {} - if not tasks1: - pkg_op = '+ ' - elif not tasks2: - pkg_op = '- ' - else: - pkg_op = ' ' - - for task in set(tasks1.keys()).union(set(tasks2.keys())): - task_op = ' ' - if task in tasks1: - # Average over all values - val1 = [getattr(b, val_type) for b in bs1[pkg]['tasks'][task]] - val1 = sum(val1) / len(val1) - else: - task_op = '+ ' - val1 = 0 - if task in tasks2: - # Average over all values - val2 = [getattr(b, val_type) for b in bs2[pkg]['tasks'][task]] - val2 = sum(val2) / len(val2) - else: - val2 = 0 - task_op = '- ' - - if val1 == 0: - reldiff = float('inf') - else: - reldiff = 100 * (val2 - val1) / val1 - - if max(val1, val2) < min_val: - log.debug("Filtering out %s:%s (%s)", pkg, task, - val_to_str(max(val1, val2))) - continue - if abs(val2 - val1) < min_absdiff: - log.debug("Filtering out %s:%s (difference of %s)", pkg, task, - val_to_str(val2-val1)) - continue - tasks_diff.append(TaskDiff(pkg, pkg_op, task, task_op, val1, val2, - val2-val1, reldiff)) + tasks_diff = diff_buildstats(bs1, bs2, val_type, min_val, min_absdiff) # Sort our list for field in reversed(sort_by):
scripts/lib/buildstats.py new file +301 -0
301 lines not on this page · raw message
Signature · 1 line
2.12.3
PATCH 2/4 scripts/oe-build-perf-report: summary of task resource usage
In reply to Markus LehtonenUtilize buildstats, if available, and show a summary of the resource usage of bitbake tasks in the html report. The details provided are:
- total number of tasks
- top 5 resource-hungry tasks (cputime)
- top 5 increase in resource usage (cputime)
- top 5 decrease in resource usage (cputime)
[YOCTO #11381]
--- scripts/lib/build_perf/html/report.html | 60 +++++++++++++++++++++- scripts/lib/buildstats.py | 8 +++ scripts/oe-build-perf-report | 90 ++++++++++++++++++++++++--------- 3 files changed, 133 insertions(+), 25 deletions(-)
scripts/lib/build_perf/html/report.html +58 -2
@@ -53,9 +53,11 @@ summary th, .meta-table td { border-collapse: collapse; } .details th { - font-weight: normal; padding-right: 8px; } +.details.plain th { + font-weight: normal; +} .preformatted { font-family: monospace; white-space: pre-wrap;
@@ -165,6 +167,7 @@ h3 { {{ measurement.absdiff_str }} ({{measurement.reldiff}}) </span></span> </div> + {# Table for trendchart and the statistics #} <table style="width: 100%"> <tr> <td style="width: 75%">
@@ -173,7 +176,7 @@ h3 { </td> <td> {# Measurement statistics #} - <table class="details"> + <table class="details plain"> <tr> <th>Test runs</th><td>{{ measurement.value.sample_cnt }}</td> </tr><tr>
@@ -191,6 +194,59 @@ h3 { </td> </tr> </table> + + {# Task and recipe summary from buildstats #} + {% if 'buildstats' in measurement %} + Task resource usage + <table class="details" style="width:100%"> + <tr> + <th>Number of tasks</th> + <th>Top consumers of cputime</th> + </tr> + <tr> + <td style="vertical-align: top">{{ measurement.buildstats.tasks.count }} ({{ measurement.buildstats.tasks.change }})</td> + {# Table of most resource-hungry tasks #} + <td> + <table class="details plain"> + {% for diff in measurement.buildstats.top_consumer|reverse %} + <tr> + <th>{{ diff.pkg }}.{{ diff.task }}</th> + <td>{{ '%0.0f' % diff.value2 }} s</td> + </tr> + {% endfor %} + </table> + </td> + </tr> + <tr> + <th>Biggest increase in cputime</th> + <th>Biggest decrease in cputime</th> + </tr> + <tr> + {# Table biggest increase in resource usage #} + <td> + <table class="details plain"> + {% for diff in measurement.buildstats.top_increase|reverse %} + <tr> + <th>{{ diff.pkg }}.{{ diff.task }}</th> + <td>{{ '%+0.0f' % diff.absdiff }} s</td> + </tr> + {% endfor %} + </table> + </td> + {# Table biggest decrease in resource usage #} + <td> + <table class="details plain"> + {% for diff in measurement.buildstats.top_decrease %} + <tr> + <th>{{ diff.pkg }}.{{ diff.task }}</th> + <td>{{ '%+0.0f' % diff.absdiff }} s</td> + </tr> + {% endfor %} + </table> + </td> + </tr> + </table> + {% endif %} </div> {% endfor %} {# Unsuccessful test #}
scripts/lib/buildstats.py +8 -0
@@ -180,6 +180,14 @@ class BSRecipe(object): class BuildStats(dict): """Class representing buildstats of one build""" + @property + def num_tasks(self): + """Get number of tasks""" + num = 0 + for recipe in self.values(): + num += len(recipe.tasks) + return num + @classmethod def from_json(cls, bs_json): """Create new BuildStats object from JSON object"""
scripts/oe-build-perf-report +67 -23
149 lines not on this page · raw message
Signature · 1 line
2.12.3
PATCH 3/4 scripts/buildstats-diff: move more code to lib/buildstats.py
In reply to Markus LehtonenMore refactoring of buildstats-diff script. Move recipe version comparison functionality to scripts/lib/buildstats.py. This patch also compasses some wording changes, i.e. changing 'package' to 'recipe'.
[YOCTO #11382]
--- scripts/buildstats-diff | 86 +++++++++++++++++------------------------------ scripts/lib/buildstats.py | 31 ++++++++++++++++- 2 files changed, 61 insertions(+), 56 deletions(-)
scripts/buildstats-diff +31 -55
@@ -24,7 +24,7 @@ from operator import attrgetter # Import oe libs scripts_path = os.path.dirname(os.path.realpath(__file__)) sys.path.append(os.path.join(scripts_path, 'lib')) -from buildstats import BuildStats, diff_buildstats, taskdiff_fields +from buildstats import BuildStats, diff_buildstats, taskdiff_fields, BSVerDiff # Setup logging
@@ -76,72 +76,48 @@ def read_buildstats(path, multi): def print_ver_diff(bs1, bs2): """Print package version differences""" - pkgs1 = set(bs1.keys()) - pkgs2 = set(bs2.keys()) - new_pkgs = pkgs2 - pkgs1 - deleted_pkgs = pkgs1 - pkgs2 - - echanged = [] - vchanged = [] - rchanged = [] - unchanged = [] - common_pkgs = pkgs2.intersection(pkgs1) - if common_pkgs: - for pkg in common_pkgs: - if bs1[pkg].epoch != bs2[pkg].epoch: - echanged.append(pkg) - elif bs1[pkg].version != bs2[pkg].version: - vchanged.append(pkg) - elif bs1[pkg].revision != bs2[pkg].revision: - rchanged.append(pkg) - else: - unchanged.append(pkg) - maxlen = max([len(pkg) for pkg in pkgs1.union(pkgs2)]) + diff = BSVerDiff(bs1, bs2) + + maxlen = max([len(r) for r in set(bs1.keys()).union(set(bs2.keys()))]) fmt_str = " {:{maxlen}} ({})" -# if unchanged: -# print("\nUNCHANGED PACKAGES:") -# print("-------------------") -# maxlen = max([len(pkg) for pkg in unchanged]) -# for pkg in sorted(unchanged): -# print(fmt_str.format(pkg, bs2[pkg]['nevr'], maxlen=maxlen)) - - if new_pkgs: - print("\nNEW PACKAGES:") - print("-------------") - for pkg in sorted(new_pkgs): - print(fmt_str.format(pkg, bs2[pkg].nevr, maxlen=maxlen)) - - if deleted_pkgs: - print("\nDELETED PACKAGES:") - print("-----------------") - for pkg in sorted(deleted_pkgs): - print(fmt_str.format(pkg, bs1[pkg].nevr, maxlen=maxlen)) + + if diff.new: + print("\nNEW RECIPES:") + print("------------") + for name, val in sorted(diff.new.items()): + print(fmt_str.format(name, val.nevr, maxlen=maxlen)) + + if diff.dropped: + print("\nDROPPED RECIPES:") + print("----------------") + for name, val in sorted(diff.dropped.items()): + print(fmt_str.format(name, val.nevr, maxlen=maxlen)) fmt_str = " {0:{maxlen}} {1:<20} ({2})" - if rchanged: + if diff.rchanged: print("\nREVISION CHANGED:") print("-----------------") - for pkg in sorted(rchanged): - field1 = "{} -> {}".format(pkg, bs1[pkg].revision, bs2[pkg].revision) - field2 = "{} -> {}".format(bs1[pkg].nevr, bs2[pkg].nevr) - print(fmt_str.format(pkg, field1, field2, maxlen=maxlen)) + for name, val in sorted(diff.rchanged.items()): + field1 = "{} -> {}".format(val.left.revision, val.right.revision) + field2 = "{} -> {}".format(val.left.nevr, val.right.nevr) + print(fmt_str.format(name, field1, field2, maxlen=maxlen)) - if vchanged: + if diff.vchanged: print("\nVERSION CHANGED:") print("----------------") - for pkg in sorted(vchanged): - field1 = "{} -> {}".format(bs1[pkg].version, bs2[pkg].version) - field2 = "{} -> {}".format(bs1[pkg].nevr, bs2[pkg].nevr) - print(fmt_str.format(pkg, field1, field2, maxlen=maxlen)) + for name, val in sorted(diff.vchanged.items()): + field1 = "{} -> {}".format(val.left.version, val.right.version) + field2 = "{} -> {}".format(val.left.nevr, val.right.nevr) + print(fmt_str.format(name, field1, field2, maxlen=maxlen)) - if echanged: + if diff.echanged: print("\nEPOCH CHANGED:") print("--------------") - for pkg in sorted(echanged): - field1 = "{} -> {}".format(bs1[pkg].epoch, bs2[pkg].epoch) - field2 = "{} -> {}".format(bs1[pkg].nevr, bs2[pkg].nevr) - print(fmt_str.format(pkg, field1, field2, maxlen=maxlen)) + for name, val in sorted(diff.echanged.items()): + field1 = "{} -> {}".format(val.left.epoch, val.right.epoch) + field2 = "{} -> {}".format(val.left.nevr, val.right.nevr) + print(fmt_str.format(name, field1, field2, maxlen=maxlen)) def print_task_diff(bs1, bs2, val_type, min_val=0, min_absdiff=0, sort_by=('absdiff',)):
scripts/lib/buildstats.py +30 -1
@@ -15,7 +15,7 @@ import json import logging import os import re -from collections import namedtuple +from collections import namedtuple,OrderedDict from statistics import mean
@@ -307,3 +307,32 @@ def diff_buildstats(bs1, bs2, stat_attr, min_val=None, min_absdiff=None): tasks_diff.append(TaskDiff(pkg, pkg_op, task, task_op, val1, val2, val2-val1, reldiff)) return tasks_diff + + +class BSVerDiff(object): + """Class representing recipe version differences between two buildstats""" + def __init__(self, bs1, bs2): + RecipeVerDiff = namedtuple('RecipeVerDiff', 'left right') + + recipes1 = set(bs1.keys()) + recipes2 = set(bs2.keys()) + + self.new = dict([(r, bs2[r]) for r in sorted(recipes2 - recipes1)]) + self.dropped = dict([(r, bs1[r]) for r in sorted(recipes1 - recipes2)]) + self.echanged = {} + self.vchanged = {} + self.rchanged = {} + self.unchanged = {} + + common = recipes2.intersection(recipes1) + if common: + for recipe in common: + rdiff = RecipeVerDiff(bs1[recipe], bs2[recipe]) + if bs1[recipe].epoch != bs2[recipe].epoch: + self.echanged[recipe] = rdiff + elif bs1[recipe].version != bs2[recipe].version: + self.vchanged[recipe] = rdiff + elif bs1[recipe].revision != bs2[recipe].revision: + self.rchanged[recipe] = rdiff + else: + self.unchanged[recipe] = rdiff
Signature · 1 line
2.12.3
PATCH 4/4 scripts/oe-build-perf-report: show recipe version changes in html report
In reply to Markus LehtonenIf buildstats are available (for a certain measurement), show recipe version changes between the two builds that are being compared. The information shown includes new and dropped recipes as well as changes in recipe version, revision or epoch.
[YOCTO #11382]
--- scripts/lib/build_perf/html/report.html | 20 ++++++++++++++++++++ scripts/lib/buildstats.py | 15 +++++++++++++-- scripts/oe-build-perf-report | 17 ++++++++++++++++- 3 files changed, 49 insertions(+), 3 deletions(-)
scripts/lib/build_perf/html/report.html +20 -0
@@ -246,6 +246,26 @@ h3 { </td> </tr> </table> + + {# Recipe version differences #} + {% if measurement.buildstats.ver_diff %} + <div style="margin-top: 16px">Recipe version changes</div> + <table class="details"> + {% for head, recipes in measurement.buildstats.ver_diff.items() %} + <tr> + <th colspan="2">{{ head }}</th> + </tr> + {% for name, info in recipes|sort %} + <tr> + <td>{{ name }}</td> + <td>{{ info }}</td> + </tr> + {% endfor %} + {% endfor %} + </table> + {% else %} + <div style="margin-top: 16px">No recipe version changes detected</div> + {% endif %} {% endif %} </div> {% endfor %}
scripts/lib/buildstats.py +13 -2
@@ -157,9 +157,9 @@ class BSRecipe(object): self.version = version self.revision = revision if epoch is None: - self.nevr = "{}-{}-{}".format(name, version, revision) + self.evr = "{}-{}".format(version, revision) else: - self.nevr = "{}-{}_{}-{}".format(name, epoch, version, revision) + self.evr = "{}_{}-{}".format(epoch, version, revision) self.tasks = {} def aggregate(self, bsrecipe):
@@ -176,6 +176,10 @@ class BSRecipe(object): self.tasks[taskname] = BSTaskAggregate([self.tasks[taskname]]) self.tasks[taskname].append(taskdata) + @property + def nevr(self): + return self.name + '-' + self.evr + class BuildStats(dict): """Class representing buildstats of one build"""
@@ -323,6 +327,7 @@ class BSVerDiff(object): self.vchanged = {} self.rchanged = {} self.unchanged = {} + self.empty_diff = False common = recipes2.intersection(recipes1) if common:
@@ -336,3 +341,9 @@ class BSVerDiff(object): self.rchanged[recipe] = rdiff else: self.unchanged[recipe] = rdiff + + if len(recipes1) == len(recipes2) == len(self.unchanged): + self.empty_diff = True + + def __bool__(self): + return not self.empty_diff
scripts/oe-build-perf-report +16 -1
@@ -31,7 +31,7 @@ from build_perf import print_table from build_perf.report import (metadata_xml_to_json, results_xml_to_json, aggregate_data, aggregate_metadata, measurement_stats) from build_perf import html -from buildstats import BuildStats, diff_buildstats +from buildstats import BuildStats, diff_buildstats, BSVerDiff scriptpath.add_oe_lib_path()
@@ -338,6 +338,7 @@ class BSSummary(object): self.top_consumer = None self.top_decrease = None self.top_increase = None + self.ver_diff = OrderedDict() tasks_diff = diff_buildstats(bs1, bs2, 'cputime')
@@ -350,6 +351,20 @@ class BSSummary(object): self.top_decrease = tasks_diff[0:5] self.top_increase = tasks_diff[-5:] + # Compare recipe versions and prepare data for display + ver_diff = BSVerDiff(bs1, bs2) + if ver_diff: + if ver_diff.new: + self.ver_diff['New recipes'] = [(n, r.evr) for n, r in ver_diff.new.items()] + if ver_diff.dropped: + self.ver_diff['Dropped recipes'] = [(n, r.evr) for n, r in ver_diff.dropped.items()] + if ver_diff.echanged: + self.ver_diff['Epoch changed'] = [(n, "{} → {}".format(r.left.evr, r.right.evr)) for n, r in ver_diff.echanged.items()] + if ver_diff.vchanged: + self.ver_diff['Version changed'] = [(n, "{} → {}".format(r.left.version, r.right.version)) for n, r in ver_diff.vchanged.items()] + if ver_diff.rchanged: + self.ver_diff['Revision changed'] = [(n, "{} → {}".format(r.left.evr, r.right.evr)) for n, r in ver_diff.rchanged.items()] + def print_html_report(data, id_comp, buildstats): """Print report in html format"""
Signature · 1 line
2.12.3