public inbox for openembedded-core@lists.openembedded.org
 help / color / mirror / Atom feed
* [PATCH 0/1] build_perf: add commit annotations
@ 2026-01-28 12:57 Alba Herrerías
  2026-01-28 12:57 ` [PATCH 1/1] " Alba Herrerías
  0 siblings, 1 reply; 5+ messages in thread
From: Alba Herrerías @ 2026-01-28 12:57 UTC (permalink / raw)
  To: openembedded-core; +Cc: engineering, Alba Herrerias

STA 2025 Milestone 4: Add commit annotations to openembedded-core build_perf pages. 
Now loads commit annotations from https://git.yoctoproject.org/yocto-buildstats/plain/annotations.json
and inserts them into the page where necessary. Also refactored and simplified charts instantiation
and template handling in general.

Alex Feyerke (1):
  build_perf: add commit annotations

 .../build_perf/html/measurement_chart.html    | 168 -------------
 scripts/lib/build_perf/html/report.html       | 222 +++++++++++++++---
 scripts/oe-build-perf-report                  | 153 +++++++++++-
 3 files changed, 339 insertions(+), 204 deletions(-)
 delete mode 100644 scripts/lib/build_perf/html/measurement_chart.html

-- 
2.40.1



^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH 1/1] build_perf: add commit annotations
  2026-01-28 12:57 [PATCH 0/1] build_perf: add commit annotations Alba Herrerías
@ 2026-01-28 12:57 ` Alba Herrerías
  2026-02-06 11:16   ` [OE-core] " Ross Burton
  0 siblings, 1 reply; 5+ messages in thread
From: Alba Herrerías @ 2026-01-28 12:57 UTC (permalink / raw)
  To: openembedded-core; +Cc: engineering, Alex Feyerke

From: Alex Feyerke <alex@neighbourhood.ie>

Also: refactoring and simplification of html rendering, esp. wrt. charts.
Signed-off-by: Alex Feyerke <alex@neighbourhood.ie>
---
 .../build_perf/html/measurement_chart.html    | 168 -------------
 scripts/lib/build_perf/html/report.html       | 222 +++++++++++++++---
 scripts/oe-build-perf-report                  | 153 +++++++++++-
 3 files changed, 339 insertions(+), 204 deletions(-)
 delete mode 100644 scripts/lib/build_perf/html/measurement_chart.html

diff --git a/scripts/lib/build_perf/html/measurement_chart.html b/scripts/lib/build_perf/html/measurement_chart.html
deleted file mode 100644
index 86435273cf..0000000000
--- a/scripts/lib/build_perf/html/measurement_chart.html
+++ /dev/null
@@ -1,168 +0,0 @@
-<script type="module">
-  // Get raw data
-  const rawData = [
-    {% for sample in measurement.samples %}
-      [{{ sample.commit_num }}, {{ sample.mean.gv_value() }}, {{ sample.start_time }}, '{{sample.commit}}'],
-    {% endfor %}
-  ];
-
-  const convertToMinute = (time) => {
-    return time[0]*60 + time[1] + time[2]/60 + time[3]/3600;
-  }
-
-  // Update value format to either minutes or leave as size value
-  const updateValue = (value) => {
-    // Assuming the array values are duration in the format [hours, minutes, seconds, milliseconds]
-    return Array.isArray(value) ? convertToMinute(value) : value
-  }
-
-  // Convert raw data to the format: [time, value]
-  const data = rawData.map(([commit, value, time]) => {
-    return [
-      // The Date object takes values in milliseconds rather than seconds. So to use a Unix timestamp we have to multiply it by 1000.
-      new Date(time * 1000).getTime(),
-      // Assuming the array values are duration in the format [hours, minutes, seconds, milliseconds]
-      updateValue(value)
-    ]
-  });
-
-  const commitCountList = rawData.map(([commit, value, time]) => {
-    return commit
-  });
-
-  const commitCountData = rawData.map(([commit, value, time]) => {
-    return updateValue(value)
-  });
-
-  // Set chart options
-  const option_start_time = {
-    tooltip: {
-      trigger: 'axis',
-      enterable: true,
-      position: function (point, params, dom, rect, size) {
-        return [point[0], '0%'];
-      },
-      formatter: function (param) {
-        const value = param[0].value[1]
-        const sample  = rawData.filter(([commit, dataValue]) => updateValue(dataValue) === value)
-        const formattedDate = new Date(sample[0][2] * 1000).toString().replace(/GMT[+-]\d{4}/, '').replace(/\(.*\)/, '(CEST)');
-
-        // Add commit hash to the tooltip as a link
-        const commitLink = `https://git.yoctoproject.org/poky/commit/?id=${sample[0][3]}`
-        if ('{{ measurement.value_type.quantity }}' == 'time') {
-          const hours = Math.floor(value/60)
-          const minutes = Math.floor(value % 60)
-          const seconds = Math.floor((value * 60) % 60)
-          return `<strong>Duration:</strong> ${hours}:${minutes}:${seconds}, <strong>Commit number:</strong> <a href="${commitLink}" target="_blank" rel="noreferrer noopener">${sample[0][0]}</a>, <br/> <strong>Start time:</strong> ${formattedDate}`
-        }
-        return `<strong>Size:</strong> ${value.toFixed(2)} MB, <strong>Commit number:</strong> <a href="${commitLink}" target="_blank" rel="noreferrer noopener">${sample[0][0]}</a>, <br/> <strong>Start time:</strong> ${formattedDate}`
-      ;}
-    },
-    xAxis: {
-      type: 'time',
-    },
-    yAxis: {
-      name: '{{ measurement.value_type.quantity }}' == 'time' ? 'Duration in minutes' : 'Disk size in MB',
-      type: 'value',
-      min: function(value) {
-        return Math.round(value.min - 0.5);
-      },
-      max: function(value) {
-        return Math.round(value.max + 0.5);
-      }
-    },
-    dataZoom: [
-      {
-        type: 'slider',
-        xAxisIndex: 0,
-        filterMode: 'none'
-      },
-    ],
-    series: [
-      {
-        name: '{{ measurement.value_type.quantity }}',
-        type: 'line',
-        symbol: 'none',
-        data: data
-      }
-    ]
-  };
-
-  const option_commit_count = {
-    tooltip: {
-      trigger: 'axis',
-      enterable: true,
-      position: function (point, params, dom, rect, size) {
-        return [point[0], '0%'];
-      },
-      formatter: function (param) {
-        const value = param[0].value
-        const sample  = rawData.filter(([commit, dataValue]) => updateValue(dataValue) === value)
-        const formattedDate = new Date(sample[0][2] * 1000).toString().replace(/GMT[+-]\d{4}/, '').replace(/\(.*\)/, '(CEST)');
-        // Add commit hash to the tooltip as a link
-        const commitLink = `https://git.yoctoproject.org/poky/commit/?id=${sample[0][3]}`
-        if ('{{ measurement.value_type.quantity }}' == 'time') {
-          const hours = Math.floor(value/60)
-          const minutes = Math.floor(value % 60)
-          const seconds = Math.floor((value * 60) % 60)
-          return `<strong>Duration:</strong> ${hours}:${minutes}:${seconds}, <strong>Commit number:</strong> <a href="${commitLink}" target="_blank" rel="noreferrer noopener">${sample[0][0]}</a>, <br/> <strong>Start time:</strong> ${formattedDate}`
-        }
-        return `<strong>Size:</strong> ${value.toFixed(2)} MB, <strong>Commit number:</strong> <a href="${commitLink}" target="_blank" rel="noreferrer noopener">${sample[0][0]}</a>, <br/> <strong>Start time:</strong> ${formattedDate}`
-      ;}
-    },
-    xAxis: {
-      name: 'Commit count',
-      type: 'category',
-      data: commitCountList
-    },
-    yAxis: {
-      name: '{{ measurement.value_type.quantity }}' == 'time' ? 'Duration in minutes' : 'Disk size in MB',
-      type: 'value',
-      min: function(value) {
-        return Math.round(value.min - 0.5);
-      },
-      max: function(value) {
-        return Math.round(value.max + 0.5);
-      }
-    },
-    dataZoom: [
-      {
-        type: 'slider',
-        xAxisIndex: 0,
-        filterMode: 'none'
-      },
-    ],
-    series: [
-      {
-        name: '{{ measurement.value_type.quantity }}',
-        type: 'line',
-        symbol: 'none',
-        data: commitCountData
-      }
-    ]
-  };
-
-  // Draw chart
-  const draw_chart = (chart_id, option) => {
-    let chart_name
-    const chart_div = document.getElementById(chart_id);
-    // Set dark mode
-    if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
-        chart_name= echarts.init(chart_div, 'dark', {
-        height: 320
-      });
-    } else {
-        chart_name= echarts.init(chart_div, null, {
-        height: 320
-      });
-    }
-    // Change chart size with browser resize
-    window.addEventListener('resize', function() {
-      chart_name.resize();
-    });
-    return chart_name.setOption(option);
-  }
-
-  draw_chart('{{ chart_elem_start_time_id }}', option_start_time)
-  draw_chart('{{ chart_elem_commit_count_id }}', option_commit_count)
-</script>
diff --git a/scripts/lib/build_perf/html/report.html b/scripts/lib/build_perf/html/report.html
index 28cd80e738..4b37893cd0 100644
--- a/scripts/lib/build_perf/html/report.html
+++ b/scripts/lib/build_perf/html/report.html
@@ -1,24 +1,8 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-{# Scripts, for visualization#}
-<!--START-OF-SCRIPTS-->
 <script src=" https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js "></script>
 
-{# Render measurement result charts #}
-{% for test in test_data %}
-  {% if test.status == 'SUCCESS' %}
-    {% for measurement in test.measurements %}
-      {% set chart_elem_start_time_id = test.name + '_' + measurement.name + '_chart_start_time' %}
-      {% set chart_elem_commit_count_id = test.name + '_' + measurement.name + '_chart_commit_count' %}
-      {% include 'measurement_chart.html' %}
-    {% endfor %}
-  {% endif %}
-{% endfor %}
-
-<!--END-OF-SCRIPTS-->
-
-{# Styles #}
 <style>
 :root {
   --text: #000;
@@ -103,12 +87,22 @@ table {
 tr {
   border-bottom: 1px solid var(--trborder);
 }
-tr:first-child {
+thead {
   border-bottom: 1px solid var(--trtopborder);
 }
 tr:last-child {
   border-bottom: none;
 }
+
+table.meta-table tbody td, table.meta-table tbody th {
+  vertical-align: top;
+  line-height: 1.5em;
+}
+
+.fixed-table-header-width {
+  width: 40%;
+}
+
 a {
   text-decoration: none;
   font-weight: bold;
@@ -133,6 +127,19 @@ button:hover {
 .tab button.active {
   background-color: #d6d9e0;
 }
+
+.chart-tooltip {
+  max-width: 70vw;
+}
+
+.chart-tooltip ul {
+  padding-left: 1.5em;
+}
+
+.chart-tooltip li {
+  max-width: 30em;
+  text-wrap: auto;
+}
 @media (prefers-color-scheme: dark) {
   :root {
     --text: #e9e8fa;
@@ -154,6 +161,16 @@ button:hover {
     background-color: #545a69;
   }
 }
+
+@media (max-width: 1024px) {
+  body {
+    margin-inline: 0.5em;
+  }
+  .card-container {
+    padding-inline: 0;
+  }
+}
+
 </style>
 
 <title>{{ title }}</title>
@@ -169,23 +186,28 @@ button:hover {
   <h2>General</h2>
   <h4>The table provides an overview of the comparison between two selected commits from the same branch.</h4>
   <table class="meta-table" style="width: 100%">
-    <tr>
-      <th></th>
-      <th>Current commit</th>
-      <th>Comparing with</th>
-    </tr>
-    {% for key, item in metadata.items() %}
-    <tr>
-      <th>{{ item.title }}</th>
-      {%if key == 'commit' %}
-        <td>{{ poky_link(item.value) }}</td>
-        <td>{{ poky_link(item.value_old) }}</td>
-      {% else %}
-        <td>{{ item.value }}</td>
-        <td>{{ item.value_old }}</td>
-      {% endif %}
-    </tr>
-    {% endfor %}
+    <thead>
+      <tr>
+        <th></th>
+        <th class="fixed-table-header-width">Current commit</th>
+        <th class="fixed-table-header-width">Comparing with</th>
+      </tr>
+    </thead>
+    <tbody>
+      {% for key, item in metadata.items() %}
+      <tr>
+        <th>{{ item.title }}</th>
+        {%if key == 'commit' %}
+          <td>{{ poky_link(item.value) }}{%if metadata.commit_annotation and metadata.commit_annotation.value %}<br>{{metadata.commit_annotation.value}}{% endif %}</td>
+          <td>{{ poky_link(item.value_old) }}{%if metadata.commit_annotation and metadata.commit_annotation.value %}<br>{{metadata.commit_annotation.value_old}}{% endif %}</td>
+        {% elif key == 'commit_annotation' %}
+        {% else %}
+          <td>{{ item.value }}</td>
+          <td>{{ item.value_old }}</td>
+        {% endif %}
+      </tr>
+      {% endfor %}
+    </tbody>
   </table>
 
   {# Test result summary #}
@@ -380,7 +402,137 @@ button:hover {
   {% endfor %}
 </div>
 
-<script>
+<script type="text/javascript">
+
+const getCommonChartConfig = (measurement) => {
+  return {
+    tooltip: {
+      trigger: 'axis',
+      enterable: true,
+      className: 'chart-tooltip',
+      position: function (point, params, dom, rect, size) {
+        return [point[0], '0%'];
+      },
+    },
+    xAxis: {
+      type: 'time',
+    },
+    yAxis: {
+      name: measurement.value_type == 'time' ? 'Duration in minutes' : 'Disk size in MB',
+      type: 'value',
+      min: function(value) {
+        return Math.round(value.min - 0.5);
+      },
+      max: function(value) {
+        return Math.round(value.max + 0.5);
+      }
+    },
+    dataZoom: [
+      {
+        type: 'slider',
+        xAxisIndex: 0,
+        filterMode: 'none'
+      },
+    ]
+  }
+}
+
+const drawChart = (chart_id, chartOptions) => {
+  const chart_div = document.getElementById(chart_id);
+  const theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default'
+
+  const chart = echarts.init(chart_div, theme, {height: 320});
+  chart.setOption(chartOptions);
+  window.addEventListener('resize', () => {
+    chart.resize();
+  });
+  requestAnimationFrame(() => chart.resize());
+}
+
+const generateTooltip = (param, sample, type) => {
+  // Data might be an array of arrays (for startTime charts) or an arraay of commit numbers (for commit number charts)
+  const value = Array.isArray(param[0].value) ? param[0].value[1] : param[0].value
+  const formattedDate = new Date(sample[0][0]).toString().replace(/GMT[+-]\d{4}/, '').replace(/\(.*\)/, '(CEST)');
+
+  // Add commit hash to the tooltip as a link
+  const commitLink = `https://git.yoctoproject.org/yocto-buildstats/commit/?id=${sample[0][2]}`
+  const commitAnnotation = commitAnnotations[sample[0][2]]
+  console.log('commitAnnotation', commitAnnotation)
+  if (type == 'time') {
+    const hours = Math.floor(value/60)
+    const minutes = Math.floor(value % 60)
+    const seconds = Math.floor((value * 60) % 60)
+    return `<ul>
+  <li><strong>Duration:</strong> ${hours}:${minutes}:${seconds}</li>
+  <li><strong>Commit number:</strong> <a href="${commitLink}" target="_blank" rel="noreferrer noopener">${sample[0][1]}</a></li>
+  ${commitAnnotation ? `<li><strong>Commit annotation:</strong> ${commitAnnotation}</li>` : ''}
+  <li><strong>Start time:</strong> ${formattedDate}</li>
+</ul>
+`
+  }
+  return `<strong>Size:</strong> ${value.toFixed(2)} MB, <strong>Commit number:</strong> <a href="${commitLink}" target="_blank" rel="noreferrer noopener">${sample[0][1]}</a>, <br/> <strong>Start time:</strong> ${formattedDate}`;
+}
+
+chartData.forEach(test => {
+  // sample array is:
+  // [start_time_in_ms, commit_num, commit_hash, duration_in_minutes_or_size]
+  test.measurements.forEach(measurement => {
+    const sharedChartConfig = getCommonChartConfig(measurement)
+    const startTimeChartConfig = {
+      ...sharedChartConfig,
+      tooltip: {
+        ...sharedChartConfig.tooltip,
+        formatter: function (param) {
+          const sample = measurement.combo_samples.filter(([start_time_in_ms, commit_num, commit_hash, duration_in_minutes_or_size]) => param[0].axisValue === start_time_in_ms)
+          return generateTooltip(param, sample, measurement.value_type)
+        }
+      },
+      series: [
+        {
+          name: measurement.value_type,
+          type: 'line',
+          symbol: 'none',
+          data: measurement.combo_samples.map((sample) => {
+            return [sample[0], sample[3]]
+          })
+        }
+      ]
+    }
+    const commitCountChartConfig = {
+      ...sharedChartConfig,
+      // Custom xAxis that displays the commit numbers
+      xAxis: {
+        name: 'Commit count',
+        type: 'category',
+        data: measurement.combo_samples.map((sample) => {
+            return sample[1]
+          })
+      },
+      tooltip: {
+        ...sharedChartConfig.tooltip,
+        formatter: function (param) {
+          const sample = measurement.combo_samples.filter(([start_time_in_ms, commit_num, commit_hash, duration_in_minutes_or_size]) => param[0].axisValue == commit_num)
+          return generateTooltip(param, sample, measurement.value_type)
+        }
+      },
+      series: [
+        {
+          name: measurement.value_type,
+          type: 'line',
+          symbol: 'none',
+          data: measurement.combo_samples.map((sample) => {
+            return sample[3]
+          })
+        }
+      ]
+    }
+    drawChart(measurement.chart_elem_start_time_id, startTimeChartConfig)
+    drawChart(measurement.chart_elem_commit_count_id, commitCountChartConfig)
+  })
+})
+
+
+
 function openChart(event, chartType, chartName) {
   let i, tabcontents, tablinks
   tabcontents = document.querySelectorAll(`.${chartName}_tabcontent > .tabcontent`);
diff --git a/scripts/oe-build-perf-report b/scripts/oe-build-perf-report
index a36f3c1bca..f9bdef2712 100755
--- a/scripts/oe-build-perf-report
+++ b/scripts/oe-build-perf-report
@@ -9,10 +9,15 @@
 
 import argparse
 import json
+import math
+import copy
 import logging
 import os
 import re
 import sys
+from urllib.request import urlopen
+from urllib import error
+
 from collections import namedtuple, OrderedDict
 from operator import attrgetter
 from xml.etree import ElementTree as ET
@@ -24,7 +29,7 @@ import scriptpath
 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,
-                               AggregateTestData)
+                               AggregateTestData, TimeVal, SizeVal)
 from build_perf import html
 from buildstats import BuildStats, diff_buildstats, BSVerDiff
 
@@ -292,6 +297,49 @@ class BSSummary(object):
             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()]
 
+# Helpers for generating chart JSON
+
+CLASS_MAP = {
+    TimeVal: "time",
+    SizeVal: "size",
+}
+
+# Sanitize inf and nan, because JSON doesn’t support those
+def sanitize(obj):
+    if isinstance(obj, float):
+        if not math.isfinite(obj):
+            return None
+        return obj
+
+    if isinstance(obj, dict):
+        return {k: sanitize(v) for k, v in obj.items()}
+
+    if isinstance(obj, list):
+        return [sanitize(v) for v in obj]
+
+    if isinstance(obj, tuple):
+        return [sanitize(v) for v in obj]  # JSON has no tuples
+
+    # leave everything else alone
+    return obj
+
+# Default json.dump handlers for unknown data types 
+def json_default(obj):
+    # Class objects (only TimeVal and SizeVal)
+    if obj in CLASS_MAP:
+        return CLASS_MAP[obj]
+    
+    # Custom object instances (BSSummary)
+    if hasattr(obj, "__dict__"):
+        # This happens _after_ sanitze(tests) in json.dump,
+        # so we need to sanitize again…
+        return sanitize(obj.__dict__)
+
+    # NaN / Infinity (JSON does not like these)
+    if isinstance(obj, float) and not math.isfinite(obj):
+        return None
+
+    raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
 
 def print_html_report(data, id_comp, buildstats):
     """Print report in html format"""
@@ -375,6 +423,109 @@ def print_html_report(data, id_comp, buildstats):
                             'max': get_data_item(data[-1][0], 'layers.meta.commit_count')}
                  }
 
+    # Get commit annotations from Yocto’s git,
+    # mush them into the metadata object,
+    # and also output them as a POJO so eCharts can use them
+
+    commitAnnotationsURL = 'https://git.yoctoproject.org/yocto-buildstats/plain/annotations.json'
+    commitAnnotationsJSON = {}
+    try:
+        response = urlopen(commitAnnotationsURL)
+        commitAnnotationsJSON = json.loads(response.read())
+        # Splice the annotations into the metadata
+        commit = metadata.get('commit', {})
+        annotations_out = {}
+
+        if (h := commit.get('value')) in commitAnnotationsJSON:
+            annotations_out['value'] = commitAnnotationsJSON[h]
+
+        if (h := commit.get('value_old')) in commitAnnotationsJSON:
+            annotations_out['value_old'] = commitAnnotationsJSON[h]
+
+        if annotations_out:
+            metadata['commit_annotation'] = annotations_out
+            metadata['commit_annotation']['title'] = "Commit annotation"
+
+    except error.URLError as e:
+        logging.debug(f"Couldn't find any commit annotations at {commitAnnotationsURL}, reason: {e.reason}.")
+    except json.decoder.JSONDecodeError as e:
+        logging.error(f"Invalid JSON received from {commitAnnotationsURL}, error: {e}")
+    except Exception as e:
+        logging.exception(f"Unexpected error while loading annotations: {e}")
+
+    ### JSONifying data for consumption by Apache eCharts
+    
+    # Make a copy of tests to pare down into what we need as JSON
+    tests_for_JSON = copy.deepcopy(tests)
+
+    # Some transformation pipeline functions, because we don't want to output the
+    # entire JSON to the html file, that would be wasteful.
+    def skip_failed_tests(test):
+        if test.get('status') != 'SUCCESS':
+            return None
+        return test
+
+    # Adds the div ids so each chart knows where to render itself into
+    def add_chart_ids(test):
+        for measurement in test.get('measurements', []):
+            measurement['chart_elem_start_time_id'] = f"{test.get('name', '')}_{measurement.get('name', '')}_chart_start_time"
+            measurement['chart_elem_commit_count_id'] = f"{test.get('name', '')}_{measurement.get('name', '')}_chart_commit_count"
+        return test
+
+    # Compose sample series data for the charts
+    def parse_samples(test):
+        for measurement in test.get('measurements', []):
+            start_time_samples = []
+            commit_count_samples = []
+            combo_samples = []
+            for sample in measurement.get('samples', []):
+                # Mean is either a TimeVal or a SizeVal
+                mean = sample.get('mean')
+                # One chart shows duration in minutes/size over start time
+                # The other shows duration in minutes/size over commit count
+                duration_in_minutes = None
+                if isinstance(mean, TimeVal):
+                    hh, mm, ss = mean.hms()
+                    duration_in_minutes = hh*60 + mm + int(ss)/60 + int(ss*1000) % 1000/3600;
+                # Charts either need the time in minutes (from TimeVal) as their value, or size (SizeVal)
+                start_time_samples.append([int(sample.get('start_time')) * 1000, duration_in_minutes if duration_in_minutes is not None else mean])
+                commit_count_samples.append([sample.get('commit_num'), duration_in_minutes if duration_in_minutes is not None else mean])
+                combo_samples.append([int(sample.get('start_time')) * 1000, sample.get('commit_num'), sample.get('commit'), duration_in_minutes if duration_in_minutes is not None else mean])
+            # For echarts consumption, all we need is samples in the format [time_in_milliseconds, build_length_in_minutes],
+            # eg [1760544944000, 59.42388888888889]
+            measurement['combo_samples'] = combo_samples
+        return test
+
+    # Throw away all the large things we don't need
+    def clean_measurements(test):
+        for m in test.get('measurements', []):
+            m.pop('buildstats', None)
+            m.pop('value', None)
+            m.pop('samples', None)
+        return test
+
+    # Apply pipeline
+    for i, test in enumerate(tests_for_JSON):
+        for func in (skip_failed_tests, add_chart_ids, parse_samples, clean_measurements):
+            test = func(test)
+            if test is None:  # skip this test entirely
+                break
+        if test is not None:
+            tests_for_JSON[i] = test
+    
+    json_str = json.dumps(
+        sanitize(tests_for_JSON),
+        indent=2,
+        default=json_default,
+        allow_nan=False,
+    )
+    
+    # Output all the JSON to the head of the report html
+    print(f"""<script type="text/javascript">
+    const commitAnnotations = {commitAnnotationsJSON}
+    const chartData = {json_str}
+</script>""")
+
     print(html.template.render(title="Build Perf Test Report",
                                metadata=metadata, test_data=tests,
                                chart_opts=chart_opts))
-- 
2.40.1



^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [OE-core] [PATCH 1/1] build_perf: add commit annotations
  2026-01-28 12:57 ` [PATCH 1/1] " Alba Herrerías
@ 2026-02-06 11:16   ` Ross Burton
  2026-02-12 15:20     ` [PATCH v2] build_perf: don’t bake commit annotations into html, always fetch dynamically Alba Herrerías
  0 siblings, 1 reply; 5+ messages in thread
From: Ross Burton @ 2026-02-06 11:16 UTC (permalink / raw)
  To: alba@thehoodiefirm.com
  Cc: openembedded-core@lists.openembedded.org,
	engineering@neighbourhood.ie, Alex Feyerke

Hi,

Thanks for the patch, much appreciated.

> On 28 Jan 2026, at 12:57, Alba Herrerías via lists.openembedded.org <alba=thehoodiefirm.com@lists.openembedded.org> wrote:
> +    # Get commit annotations from Yocto’s git,
> +    # mush them into the metadata object,
> +    # and also output them as a POJO so eCharts can use them
> +
> +    commitAnnotationsURL = 'https://git.yoctoproject.org/yocto-buildstats/plain/annotations.json'
> +    commitAnnotationsJSON = {}
> +    try:
> +        response = urlopen(commitAnnotationsURL)
> +        commitAnnotationsJSON = json.loads(response.read())

Would it be possible to load the annotations in the HTML with JavaScript when the page is loaded, so that the annotations are not “baked into” the HTML? If I was staring at build performance logs I’d want new annotations to be visible in all existing reports and not have to wait for another build perf run to complete to see them on a chart.

Thanks,
Ross

^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH v2] build_perf: don’t bake commit annotations into html, always fetch dynamically.
  2026-02-06 11:16   ` [OE-core] " Ross Burton
@ 2026-02-12 15:20     ` Alba Herrerías
  2026-02-12 15:45       ` Patchtest results for " patchtest
  0 siblings, 1 reply; 5+ messages in thread
From: Alba Herrerías @ 2026-02-12 15:20 UTC (permalink / raw)
  To: openembedded-core; +Cc: engineering, Ross.Burton, Alex Feyerke

From: Alex Feyerke <alex@neighbourhood.ie>

Signed-off-by: Alex Feyerke <alex@neighbourhood.ie>
---
 scripts/lib/build_perf/html/report.html | 20 ++++++++++++++++++--
 scripts/oe-build-perf-report            | 13 -------------
 2 files changed, 18 insertions(+), 15 deletions(-)

diff --git a/scripts/lib/build_perf/html/report.html b/scripts/lib/build_perf/html/report.html
index 4b37893cd0..779aa55573 100644
--- a/scripts/lib/build_perf/html/report.html
+++ b/scripts/lib/build_perf/html/report.html
@@ -140,6 +140,11 @@ button:hover {
   max-width: 30em;
   text-wrap: auto;
 }
+
+.annotation-container:not(:empty) {
+  display: inline-block;
+}
+
 @media (prefers-color-scheme: dark) {
   :root {
     --text: #e9e8fa;
@@ -198,8 +203,8 @@ button:hover {
       <tr>
         <th>{{ item.title }}</th>
         {%if key == 'commit' %}
-          <td>{{ poky_link(item.value) }}{%if metadata.commit_annotation and metadata.commit_annotation.value %}<br>{{metadata.commit_annotation.value}}{% endif %}</td>
-          <td>{{ poky_link(item.value_old) }}{%if metadata.commit_annotation and metadata.commit_annotation.value %}<br>{{metadata.commit_annotation.value_old}}{% endif %}</td>
+          <td>{{ poky_link(item.value) }}<span class="annotation-container" data-commit="{{ item.value }}"></span></td>
+          <td>{{ poky_link(item.value_old) }}<span class="annotation-container" data-commit="{{ item.value_old }}"></span></td>
         {% elif key == 'commit_annotation' %}
         {% else %}
           <td>{{ item.value }}</td>
@@ -529,6 +534,17 @@ chartData.forEach(test => {
     drawChart(measurement.chart_elem_start_time_id, startTimeChartConfig)
     drawChart(measurement.chart_elem_commit_count_id, commitCountChartConfig)
   })
+
+  // Splice commit annotations into the table at the top of the page
+  if (commitAnnotations) {
+    document.querySelectorAll("[data-commit]").forEach((item) => {
+      const commitNumber = item.getAttribute("data-commit")
+      const matchingAnnotation = commitAnnotations[commitNumber]
+      if (matchingAnnotation) {
+        item.innerText = matchingAnnotation
+      }
+    })
+  }
 })
 
 
diff --git a/scripts/oe-build-perf-report b/scripts/oe-build-perf-report
index f9bdef2712..02da745a6c 100755
--- a/scripts/oe-build-perf-report
+++ b/scripts/oe-build-perf-report
@@ -432,19 +432,6 @@ def print_html_report(data, id_comp, buildstats):
     try:
         response = urlopen(commitAnnotationsURL)
         commitAnnotationsJSON = json.loads(response.read())
-        # Splice the annotations into the metadata
-        commit = metadata.get('commit', {})
-        annotations_out = {}
-
-        if (h := commit.get('value')) in commitAnnotationsJSON:
-            annotations_out['value'] = commitAnnotationsJSON[h]
-
-        if (h := commit.get('value_old')) in commitAnnotationsJSON:
-            annotations_out['value_old'] = commitAnnotationsJSON[h]
-
-        if annotations_out:
-            metadata['commit_annotation'] = annotations_out
-            metadata['commit_annotation']['title'] = "Commit annotation"
 
     except error.URLError as e:
         logging.debug(f"Couldn't find any commit annotations at {commitAnnotationsURL}, reason: {e.reason}.")
-- 
2.40.1



^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Patchtest results for [PATCH v2] build_perf: don’t bake commit annotations into html, always fetch dynamically.
  2026-02-12 15:20     ` [PATCH v2] build_perf: don’t bake commit annotations into html, always fetch dynamically Alba Herrerías
@ 2026-02-12 15:45       ` patchtest
  0 siblings, 0 replies; 5+ messages in thread
From: patchtest @ 2026-02-12 15:45 UTC (permalink / raw)
  To: Alba Herrerías; +Cc: openembedded-core

[-- Attachment #1: Type: text/plain, Size: 2335 bytes --]

Thank you for your submission. Patchtest identified one
or more issues with the patch. Please see the log below for
more information:

---
Testing patch /home/patchtest/share/mboxes/v2-build_perf-don-t-bake-commit-annotations-into-html-always-fetch-dynamically..patch

FAIL: test shortlog format: Commit shortlog (first line of commit message) should follow the format "<target>: <summary>" (test_mbox.TestMbox.test_shortlog_format)
FAIL: test shortlog length: Edit shortlog so that it is 90 characters or less (currently 134 characters) (test_mbox.TestMbox.test_shortlog_length)

PASS: test Signed-off-by presence (test_mbox.TestMbox.test_signed_off_by_presence)
PASS: test author valid (test_mbox.TestMbox.test_author_valid)
PASS: test commit message presence (test_mbox.TestMbox.test_commit_message_presence)
PASS: test commit message user tags (test_mbox.TestMbox.test_commit_message_user_tags)
PASS: test mbox format (test_mbox.TestMbox.test_mbox_format)
PASS: test non-AUH upgrade (test_mbox.TestMbox.test_non_auh_upgrade)
PASS: test target mailing list (test_mbox.TestMbox.test_target_mailing_list)

SKIP: pretest pylint: No python related patches, skipping test (test_python_pylint.PyLint.pretest_pylint)
SKIP: test CVE tag format: No new CVE patches introduced (test_patch.TestPatch.test_cve_tag_format)
SKIP: test Signed-off-by presence: No new CVE patches introduced (test_patch.TestPatch.test_signed_off_by_presence)
SKIP: test Upstream-Status presence: No new CVE patches introduced (test_patch.TestPatch.test_upstream_status_presence_format)
SKIP: test bugzilla entry format: No bug ID found (test_mbox.TestMbox.test_bugzilla_entry_format)
SKIP: test pylint: No python related patches, skipping test (test_python_pylint.PyLint.test_pylint)
SKIP: test series merge on head: Merge test is disabled for now (test_mbox.TestMbox.test_series_merge_on_head)

---

Please address the issues identified and
submit a new revision of the patch, or alternatively, reply to this
email with an explanation of why the patch should be accepted. If you
believe these results are due to an error in patchtest, please submit a
bug at https://bugzilla.yoctoproject.org/ (use the 'Patchtest' category
under 'Yocto Project Subprojects'). For more information on specific
failures, see: https://wiki.yoctoproject.org/wiki/Patchtest. Thank
you!

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-02-12 15:45 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-01-28 12:57 [PATCH 0/1] build_perf: add commit annotations Alba Herrerías
2026-01-28 12:57 ` [PATCH 1/1] " Alba Herrerías
2026-02-06 11:16   ` [OE-core] " Ross Burton
2026-02-12 15:20     ` [PATCH v2] build_perf: don’t bake commit annotations into html, always fetch dynamically Alba Herrerías
2026-02-12 15:45       ` Patchtest results for " patchtest

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox