diff mbox series

[1/1] build_perf: add commit annotations

Message ID 20260128125745.58440-2-albaherreriasdev@gmail.com
State Under Review
Headers show
Series build_perf: add commit annotations | expand

Commit Message

Alba Herrerías Jan. 28, 2026, 12:57 p.m. UTC
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

Comments

Ross Burton Feb. 6, 2026, 11:16 a.m. UTC | #1
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
diff mbox series

Patch

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))