Skip to contentSkip to conversation

oe-build-perf-report: task and recipe details in html report

PATCH New Markus Lehtonen 1 participant 5 messages Sep 15, 2017

lore
Share & tools
lore
#0 lore raw

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
#1 lore raw

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
index 8e64480eb3..ce82dabee9 100755
@@ -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
#2 lore raw

PATCH 2/4 scripts/oe-build-perf-report: summary of task resource usage

In reply to Markus Lehtonen

Utilize 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
index 165cbb811c..a7ca5b0cb0 100644
@@ -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
index 9eb60b1c69..bd6332176a 100644
@@ -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
#3 lore raw

PATCH 3/4 scripts/buildstats-diff: move more code to lib/buildstats.py

In reply to Markus Lehtonen

More 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
index ce82dabee9..a128dd324f 100755
@@ -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
index bd6332176a..b1c9e617c6 100644
@@ -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
#4 lore raw

PATCH 4/4 scripts/oe-build-perf-report: show recipe version changes in html report

In reply to Markus Lehtonen

If 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
index a7ca5b0cb0..00ecec884b 100644
@@ -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
index b1c9e617c6..d9aadf3cb8 100644
@@ -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
index 8d9c53f91f..7296450b9c 100755
@@ -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, "{} &rarr; {}".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, "{} &rarr; {}".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, "{} &rarr; {}".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