summaryrefslogtreecommitdiffstats
path: root/scripts/lib/build_perf
diff options
context:
space:
mode:
authorRichard Purdie <richard.purdie@linuxfoundation.org>2025-11-07 13:31:53 +0000
committerRichard Purdie <richard.purdie@linuxfoundation.org>2025-11-07 13:31:53 +0000
commit8c22ff0d8b70d9b12f0487ef696a7e915b9e3173 (patch)
treeefdc32587159d0050a69009bdf2330a531727d95 /scripts/lib/build_perf
parentd412d2747595c1cc4a5e3ca975e3adc31b2f7891 (diff)
downloadpoky-8c22ff0d8b70d9b12f0487ef696a7e915b9e3173.tar.gz
The poky repository master branch is no longer being updated.
You can either: a) switch to individual clones of bitbake, openembedded-core, meta-yocto and yocto-docs b) use the new bitbake-setup You can find information about either approach in our documentation: https://docs.yoctoproject.org/ Note that "poky" the distro setting is still available in meta-yocto as before and we continue to use and maintain that. Long live Poky! Some further information on the background of this change can be found in: https://lists.openembedded.org/g/openembedded-architecture/message/2179 Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'scripts/lib/build_perf')
-rw-r--r--scripts/lib/build_perf/__init__.py24
-rw-r--r--scripts/lib/build_perf/html.py12
-rw-r--r--scripts/lib/build_perf/html/measurement_chart.html168
-rw-r--r--scripts/lib/build_perf/html/report.html408
-rw-r--r--scripts/lib/build_perf/report.py342
-rw-r--r--scripts/lib/build_perf/scrape-html-report.js56
6 files changed, 0 insertions, 1010 deletions
diff --git a/scripts/lib/build_perf/__init__.py b/scripts/lib/build_perf/__init__.py
deleted file mode 100644
index dcbb78042d..0000000000
--- a/scripts/lib/build_perf/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
1#
2# Copyright (c) 2017, Intel Corporation.
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6"""Build performance test library functions"""
7
8def print_table(rows, row_fmt=None):
9 """Print data table"""
10 if not rows:
11 return
12 if not row_fmt:
13 row_fmt = ['{:{wid}} '] * len(rows[0])
14
15 # Go through the data to get maximum cell widths
16 num_cols = len(row_fmt)
17 col_widths = [0] * num_cols
18 for row in rows:
19 for i, val in enumerate(row):
20 col_widths[i] = max(col_widths[i], len(str(val)))
21
22 for row in rows:
23 print(*[row_fmt[i].format(col, wid=col_widths[i]) for i, col in enumerate(row)])
24
diff --git a/scripts/lib/build_perf/html.py b/scripts/lib/build_perf/html.py
deleted file mode 100644
index d1273c9c50..0000000000
--- a/scripts/lib/build_perf/html.py
+++ /dev/null
@@ -1,12 +0,0 @@
1#
2# Copyright (c) 2017, Intel Corporation.
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6"""Helper module for HTML reporting"""
7from jinja2 import Environment, PackageLoader
8
9
10env = Environment(loader=PackageLoader('build_perf', 'html'))
11
12template = env.get_template('report.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 @@
1<script type="module">
2 // Get raw data
3 const rawData = [
4 {% for sample in measurement.samples %}
5 [{{ sample.commit_num }}, {{ sample.mean.gv_value() }}, {{ sample.start_time }}, '{{sample.commit}}'],
6 {% endfor %}
7 ];
8
9 const convertToMinute = (time) => {
10 return time[0]*60 + time[1] + time[2]/60 + time[3]/3600;
11 }
12
13 // Update value format to either minutes or leave as size value
14 const updateValue = (value) => {
15 // Assuming the array values are duration in the format [hours, minutes, seconds, milliseconds]
16 return Array.isArray(value) ? convertToMinute(value) : value
17 }
18
19 // Convert raw data to the format: [time, value]
20 const data = rawData.map(([commit, value, time]) => {
21 return [
22 // The Date object takes values in milliseconds rather than seconds. So to use a Unix timestamp we have to multiply it by 1000.
23 new Date(time * 1000).getTime(),
24 // Assuming the array values are duration in the format [hours, minutes, seconds, milliseconds]
25 updateValue(value)
26 ]
27 });
28
29 const commitCountList = rawData.map(([commit, value, time]) => {
30 return commit
31 });
32
33 const commitCountData = rawData.map(([commit, value, time]) => {
34 return updateValue(value)
35 });
36
37 // Set chart options
38 const option_start_time = {
39 tooltip: {
40 trigger: 'axis',
41 enterable: true,
42 position: function (point, params, dom, rect, size) {
43 return [point[0], '0%'];
44 },
45 formatter: function (param) {
46 const value = param[0].value[1]
47 const sample = rawData.filter(([commit, dataValue]) => updateValue(dataValue) === value)
48 const formattedDate = new Date(sample[0][2] * 1000).toString().replace(/GMT[+-]\d{4}/, '').replace(/\(.*\)/, '(CEST)');
49
50 // Add commit hash to the tooltip as a link
51 const commitLink = `https://git.yoctoproject.org/poky/commit/?id=${sample[0][3]}`
52 if ('{{ measurement.value_type.quantity }}' == 'time') {
53 const hours = Math.floor(value/60)
54 const minutes = Math.floor(value % 60)
55 const seconds = Math.floor((value * 60) % 60)
56 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}`
57 }
58 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}`
59 ;}
60 },
61 xAxis: {
62 type: 'time',
63 },
64 yAxis: {
65 name: '{{ measurement.value_type.quantity }}' == 'time' ? 'Duration in minutes' : 'Disk size in MB',
66 type: 'value',
67 min: function(value) {
68 return Math.round(value.min - 0.5);
69 },
70 max: function(value) {
71 return Math.round(value.max + 0.5);
72 }
73 },
74 dataZoom: [
75 {
76 type: 'slider',
77 xAxisIndex: 0,
78 filterMode: 'none'
79 },
80 ],
81 series: [
82 {
83 name: '{{ measurement.value_type.quantity }}',
84 type: 'line',
85 symbol: 'none',
86 data: data
87 }
88 ]
89 };
90
91 const option_commit_count = {
92 tooltip: {
93 trigger: 'axis',
94 enterable: true,
95 position: function (point, params, dom, rect, size) {
96 return [point[0], '0%'];
97 },
98 formatter: function (param) {
99 const value = param[0].value
100 const sample = rawData.filter(([commit, dataValue]) => updateValue(dataValue) === value)
101 const formattedDate = new Date(sample[0][2] * 1000).toString().replace(/GMT[+-]\d{4}/, '').replace(/\(.*\)/, '(CEST)');
102 // Add commit hash to the tooltip as a link
103 const commitLink = `https://git.yoctoproject.org/poky/commit/?id=${sample[0][3]}`
104 if ('{{ measurement.value_type.quantity }}' == 'time') {
105 const hours = Math.floor(value/60)
106 const minutes = Math.floor(value % 60)
107 const seconds = Math.floor((value * 60) % 60)
108 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}`
109 }
110 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}`
111 ;}
112 },
113 xAxis: {
114 name: 'Commit count',
115 type: 'category',
116 data: commitCountList
117 },
118 yAxis: {
119 name: '{{ measurement.value_type.quantity }}' == 'time' ? 'Duration in minutes' : 'Disk size in MB',
120 type: 'value',
121 min: function(value) {
122 return Math.round(value.min - 0.5);
123 },
124 max: function(value) {
125 return Math.round(value.max + 0.5);
126 }
127 },
128 dataZoom: [
129 {
130 type: 'slider',
131 xAxisIndex: 0,
132 filterMode: 'none'
133 },
134 ],
135 series: [
136 {
137 name: '{{ measurement.value_type.quantity }}',
138 type: 'line',
139 symbol: 'none',
140 data: commitCountData
141 }
142 ]
143 };
144
145 // Draw chart
146 const draw_chart = (chart_id, option) => {
147 let chart_name
148 const chart_div = document.getElementById(chart_id);
149 // Set dark mode
150 if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
151 chart_name= echarts.init(chart_div, 'dark', {
152 height: 320
153 });
154 } else {
155 chart_name= echarts.init(chart_div, null, {
156 height: 320
157 });
158 }
159 // Change chart size with browser resize
160 window.addEventListener('resize', function() {
161 chart_name.resize();
162 });
163 return chart_name.setOption(option);
164 }
165
166 draw_chart('{{ chart_elem_start_time_id }}', option_start_time)
167 draw_chart('{{ chart_elem_commit_count_id }}', option_commit_count)
168</script>
diff --git a/scripts/lib/build_perf/html/report.html b/scripts/lib/build_perf/html/report.html
deleted file mode 100644
index 28cd80e738..0000000000
--- a/scripts/lib/build_perf/html/report.html
+++ /dev/null
@@ -1,408 +0,0 @@
1<!DOCTYPE html>
2<html lang="en">
3<head>
4{# Scripts, for visualization#}
5<!--START-OF-SCRIPTS-->
6<script src=" https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js "></script>
7
8{# Render measurement result charts #}
9{% for test in test_data %}
10 {% if test.status == 'SUCCESS' %}
11 {% for measurement in test.measurements %}
12 {% set chart_elem_start_time_id = test.name + '_' + measurement.name + '_chart_start_time' %}
13 {% set chart_elem_commit_count_id = test.name + '_' + measurement.name + '_chart_commit_count' %}
14 {% include 'measurement_chart.html' %}
15 {% endfor %}
16 {% endif %}
17{% endfor %}
18
19<!--END-OF-SCRIPTS-->
20
21{# Styles #}
22<style>
23:root {
24 --text: #000;
25 --bg: #fff;
26 --h2heading: #707070;
27 --link: #0000EE;
28 --trtopborder: #9ca3af;
29 --trborder: #e5e7eb;
30 --chartborder: #f0f0f0;
31 }
32.meta-table {
33 font-size: 14px;
34 text-align: left;
35 border-collapse: collapse;
36}
37.summary {
38 font-size: 14px;
39 text-align: left;
40 border-collapse: collapse;
41}
42.measurement {
43 padding: 8px 0px 8px 8px;
44 border: 2px solid var(--chartborder);
45 margin: 1.5rem 0;
46}
47.details {
48 margin: 0;
49 font-size: 12px;
50 text-align: left;
51 border-collapse: collapse;
52}
53.details th {
54 padding-right: 8px;
55}
56.details.plain th {
57 font-weight: normal;
58}
59.preformatted {
60 font-family: monospace;
61 white-space: pre-wrap;
62 background-color: #f0f0f0;
63 margin-left: 10px;
64}
65.card-container {
66 border-bottom-width: 1px;
67 padding: 1.25rem 3rem;
68 box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
69 border-radius: 0.25rem;
70}
71body {
72 font-family: 'Helvetica', sans-serif;
73 margin: 3rem 8rem;
74 background-color: var(--bg);
75 color: var(--text);
76}
77h1 {
78 text-align: center;
79}
80h2 {
81 font-size: 1.5rem;
82 margin-bottom: 0px;
83 color: var(--h2heading);
84 padding-top: 1.5rem;
85}
86h3 {
87 font-size: 1.3rem;
88 margin: 0px;
89 color: var(--h2heading);
90 padding: 1.5rem 0;
91}
92h4 {
93 font-size: 14px;
94 font-weight: lighter;
95 line-height: 1.2rem;
96 margin: auto;
97 padding-top: 1rem;
98}
99table {
100 margin-top: 1.5rem;
101 line-height: 2rem;
102}
103tr {
104 border-bottom: 1px solid var(--trborder);
105}
106tr:first-child {
107 border-bottom: 1px solid var(--trtopborder);
108}
109tr:last-child {
110 border-bottom: none;
111}
112a {
113 text-decoration: none;
114 font-weight: bold;
115 color: var(--link);
116}
117a:hover {
118 color: #8080ff;
119}
120button {
121 background-color: #F3F4F6;
122 border: none;
123 outline: none;
124 cursor: pointer;
125 padding: 10px 12px;
126 transition: 0.3s;
127 border-radius: 8px;
128 color: #3A4353;
129}
130button:hover {
131 background-color: #d6d9e0;
132}
133.tab button.active {
134 background-color: #d6d9e0;
135}
136@media (prefers-color-scheme: dark) {
137 :root {
138 --text: #e9e8fa;
139 --bg: #0F0C28;
140 --h2heading: #B8B7CB;
141 --link: #87cefa;
142 --trtopborder: #394150;
143 --trborder: #212936;
144 --chartborder: #b1b0bf;
145 }
146 button {
147 background-color: #28303E;
148 color: #fff;
149 }
150 button:hover {
151 background-color: #545a69;
152 }
153 .tab button.active {
154 background-color: #545a69;
155 }
156}
157</style>
158
159<title>{{ title }}</title>
160</head>
161
162{% macro poky_link(commit) -%}
163 <a href="http://git.yoctoproject.org/cgit/cgit.cgi/poky/log/?id={{ commit }}">{{ commit[0:11] }}</a>
164{%- endmacro %}
165
166<body><div>
167 <h1 style="text-align: center;">Performance Test Report</h1>
168 {# Test metadata #}
169 <h2>General</h2>
170 <h4>The table provides an overview of the comparison between two selected commits from the same branch.</h4>
171 <table class="meta-table" style="width: 100%">
172 <tr>
173 <th></th>
174 <th>Current commit</th>
175 <th>Comparing with</th>
176 </tr>
177 {% for key, item in metadata.items() %}
178 <tr>
179 <th>{{ item.title }}</th>
180 {%if key == 'commit' %}
181 <td>{{ poky_link(item.value) }}</td>
182 <td>{{ poky_link(item.value_old) }}</td>
183 {% else %}
184 <td>{{ item.value }}</td>
185 <td>{{ item.value_old }}</td>
186 {% endif %}
187 </tr>
188 {% endfor %}
189 </table>
190
191 {# Test result summary #}
192 <h2>Test result summary</h2>
193 <h4>The test summary presents a thorough breakdown of each test conducted on the branch, including details such as build time and disk space consumption. Additionally, it gives insights into the average time taken for test execution, along with absolute and relative values for a better understanding.</h4>
194 <table class="summary" style="width: 100%">
195 <tr>
196 <th>Test name</th>
197 <th>Measurement description</th>
198 <th>Mean value</th>
199 <th>Absolute difference</th>
200 <th>Relative difference</th>
201 </tr>
202 {% for test in test_data %}
203 {% if test.status == 'SUCCESS' %}
204 {% for measurement in test.measurements %}
205 <tr {{ row_style }}>
206 {% if loop.index == 1 %}
207 <td><a href=#{{test.name}}>{{ test.name }}: {{ test.description }}</a></td>
208 {% else %}
209 {# add empty cell in place of the test name#}
210 <td></td>
211 {% endif %}
212 {% if measurement.absdiff > 0 %}
213 {% set result_style = "color: red" %}
214 {% elif measurement.absdiff == measurement.absdiff %}
215 {% set result_style = "color: green" %}
216 {% else %}
217 {% set result_style = "color: orange" %}
218 {%endif %}
219 {% if measurement.reldiff|abs > 2 %}
220 {% set result_style = result_style + "; font-weight: bold" %}
221 {% endif %}
222 <td>{{ measurement.description }}</td>
223 <td style="font-weight: bold">{{ measurement.value.mean }}</td>
224 <td style="{{ result_style }}">{{ measurement.absdiff_str }}</td>
225 <td style="{{ result_style }}">{{ measurement.reldiff_str }}</td>
226 </tr>
227 {% endfor %}
228 {% else %}
229 <td style="font-weight: bold; color: red;">{{test.status }}</td>
230 <td></td> <td></td> <td></td> <td></td>
231 {% endif %}
232 {% endfor %}
233 </table>
234
235 {# Detailed test results #}
236 <h2>Test details</h2>
237 <h4>The following section provides details of each test, accompanied by charts representing build time and disk usage over time or by commit number.</h4>
238 {% for test in test_data %}
239 <h3 style="color: #000;" id={{test.name}}>{{ test.name }}: {{ test.description }}</h3>
240 {% if test.status == 'SUCCESS' %}
241 <div class="card-container">
242 {% for measurement in test.measurements %}
243 <div class="measurement">
244 <h3>{{ measurement.description }}</h3>
245 <div style="font-weight:bold;">
246 <span style="font-size: 23px;">{{ measurement.value.mean }}</span>
247 <span style="font-size: 20px; margin-left: 12px">
248 {% if measurement.absdiff > 0 %}
249 <span style="color: red">
250 {% elif measurement.absdiff == measurement.absdiff %}
251 <span style="color: green">
252 {% else %}
253 <span style="color: orange">
254 {% endif %}
255 {{ measurement.absdiff_str }} ({{measurement.reldiff_str}})
256 </span></span>
257 </div>
258 {# Table for trendchart and the statistics #}
259 <table style="width: 100%">
260 <tr>
261 <td style="width: 75%">
262 {# Linechart #}
263 <div class="tab {{ test.name }}_{{ measurement.name }}_tablinks">
264 <button class="tablinks active" onclick="openChart(event, '{{ test.name }}_{{ measurement.name }}_start_time', '{{ test.name }}_{{ measurement.name }}')">Chart with start time</button>
265 <button class="tablinks" onclick="openChart(event, '{{ test.name }}_{{ measurement.name }}_commit_count', '{{ test.name }}_{{ measurement.name }}')">Chart with commit count</button>
266 </div>
267 <div class="{{ test.name }}_{{ measurement.name }}_tabcontent">
268 <div id="{{ test.name }}_{{ measurement.name }}_start_time" class="tabcontent" style="display: block;">
269 <div id="{{ test.name }}_{{ measurement.name }}_chart_start_time"></div>
270 </div>
271 <div id="{{ test.name }}_{{ measurement.name }}_commit_count" class="tabcontent" style="display: none;">
272 <div id="{{ test.name }}_{{ measurement.name }}_chart_commit_count"></div>
273 </div>
274 </div>
275 </td>
276 <td>
277 {# Measurement statistics #}
278 <table class="details plain">
279 <tr>
280 <th>Test runs</th><td>{{ measurement.value.sample_cnt }}</td>
281 </tr><tr>
282 <th>-/+</th><td>-{{ measurement.value.minus }} / +{{ measurement.value.plus }}</td>
283 </tr><tr>
284 <th>Min</th><td>{{ measurement.value.min }}</td>
285 </tr><tr>
286 <th>Max</th><td>{{ measurement.value.max }}</td>
287 </tr><tr>
288 <th>Stdev</th><td>{{ measurement.value.stdev }}</td>
289 </tr><tr>
290 <th><div id="{{ test.name }}_{{ measurement.name }}_chart_png"></div></th>
291 <td></td>
292 </tr>
293 </table>
294 </td>
295 </tr>
296 </table>
297
298 {# Task and recipe summary from buildstats #}
299 {% if 'buildstats' in measurement %}
300 Task resource usage
301 <table class="details" style="width:100%">
302 <tr>
303 <th>Number of tasks</th>
304 <th>Top consumers of cputime</th>
305 </tr>
306 <tr>
307 <td style="vertical-align: top">{{ measurement.buildstats.tasks.count }} ({{ measurement.buildstats.tasks.change }})</td>
308 {# Table of most resource-hungry tasks #}
309 <td>
310 <table class="details plain">
311 {% for diff in measurement.buildstats.top_consumer|reverse %}
312 <tr>
313 <th>{{ diff.pkg }}.{{ diff.task }}</th>
314 <td>{{ '%0.0f' % diff.value2 }} s</td>
315 </tr>
316 {% endfor %}
317 </table>
318 </td>
319 </tr>
320 <tr>
321 <th>Biggest increase in cputime</th>
322 <th>Biggest decrease in cputime</th>
323 </tr>
324 <tr>
325 {# Table biggest increase in resource usage #}
326 <td>
327 <table class="details plain">
328 {% for diff in measurement.buildstats.top_increase|reverse %}
329 <tr>
330 <th>{{ diff.pkg }}.{{ diff.task }}</th>
331 <td>{{ '%+0.0f' % diff.absdiff }} s</td>
332 </tr>
333 {% endfor %}
334 </table>
335 </td>
336 {# Table biggest decrease in resource usage #}
337 <td>
338 <table class="details plain">
339 {% for diff in measurement.buildstats.top_decrease %}
340 <tr>
341 <th>{{ diff.pkg }}.{{ diff.task }}</th>
342 <td>{{ '%+0.0f' % diff.absdiff }} s</td>
343 </tr>
344 {% endfor %}
345 </table>
346 </td>
347 </tr>
348 </table>
349
350 {# Recipe version differences #}
351 {% if measurement.buildstats.ver_diff %}
352 <div style="margin-top: 16px">Recipe version changes</div>
353 <table class="details">
354 {% for head, recipes in measurement.buildstats.ver_diff.items() %}
355 <tr>
356 <th colspan="2">{{ head }}</th>
357 </tr>
358 {% for name, info in recipes|sort %}
359 <tr>
360 <td>{{ name }}</td>
361 <td>{{ info }}</td>
362 </tr>
363 {% endfor %}
364 {% endfor %}
365 </table>
366 {% else %}
367 <div style="margin-top: 16px">No recipe version changes detected</div>
368 {% endif %}
369 {% endif %}
370 </div>
371 {% endfor %}
372 </div>
373 {# Unsuccessful test #}
374 {% else %}
375 <span style="font-size: 150%; font-weight: bold; color: red;">{{ test.status }}
376 {% if test.err_type %}<span style="font-size: 75%; font-weight: normal">({{ test.err_type }})</span>{% endif %}
377 </span>
378 <div class="preformatted">{{ test.message }}</div>
379 {% endif %}
380 {% endfor %}
381</div>
382
383<script>
384function openChart(event, chartType, chartName) {
385 let i, tabcontents, tablinks
386 tabcontents = document.querySelectorAll(`.${chartName}_tabcontent > .tabcontent`);
387 tabcontents.forEach((tabcontent) => {
388 tabcontent.style.display = "none";
389 });
390
391 tablinks = document.querySelectorAll(`.${chartName}_tablinks > .tablinks`);
392 tablinks.forEach((tabLink) => {
393 tabLink.classList.remove('active');
394 });
395
396 const targetTab = document.getElementById(chartType)
397 targetTab.style.display = "block";
398
399 // Call resize on the ECharts instance to redraw the chart
400 const chartContainer = targetTab.querySelector('div')
401 echarts.init(chartContainer).resize();
402
403 event.currentTarget.classList.add('active');
404}
405</script>
406
407</body>
408</html>
diff --git a/scripts/lib/build_perf/report.py b/scripts/lib/build_perf/report.py
deleted file mode 100644
index f4e6a92e09..0000000000
--- a/scripts/lib/build_perf/report.py
+++ /dev/null
@@ -1,342 +0,0 @@
1#
2# Copyright (c) 2017, Intel Corporation.
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6"""Handling of build perf test reports"""
7from collections import OrderedDict, namedtuple
8from collections.abc import Mapping
9from datetime import datetime, timezone
10from numbers import Number
11from statistics import mean, stdev, variance
12
13
14AggregateTestData = namedtuple('AggregateTestData', ['metadata', 'results'])
15
16
17def isofmt_to_timestamp(string):
18 """Convert timestamp string in ISO 8601 format into unix timestamp"""
19 if '.' in string:
20 dt = datetime.strptime(string, '%Y-%m-%dT%H:%M:%S.%f')
21 else:
22 dt = datetime.strptime(string, '%Y-%m-%dT%H:%M:%S')
23 return dt.replace(tzinfo=timezone.utc).timestamp()
24
25
26def metadata_xml_to_json(elem):
27 """Convert metadata xml into JSON format"""
28 assert elem.tag == 'metadata', "Invalid metadata file format"
29
30 def _xml_to_json(elem):
31 """Convert xml element to JSON object"""
32 out = OrderedDict()
33 for child in elem.getchildren():
34 key = child.attrib.get('name', child.tag)
35 if len(child):
36 out[key] = _xml_to_json(child)
37 else:
38 out[key] = child.text
39 return out
40 return _xml_to_json(elem)
41
42
43def results_xml_to_json(elem):
44 """Convert results xml into JSON format"""
45 rusage_fields = ('ru_utime', 'ru_stime', 'ru_maxrss', 'ru_minflt',
46 'ru_majflt', 'ru_inblock', 'ru_oublock', 'ru_nvcsw',
47 'ru_nivcsw')
48 iostat_fields = ('rchar', 'wchar', 'syscr', 'syscw', 'read_bytes',
49 'write_bytes', 'cancelled_write_bytes')
50
51 def _read_measurement(elem):
52 """Convert measurement to JSON"""
53 data = OrderedDict()
54 data['type'] = elem.tag
55 data['name'] = elem.attrib['name']
56 data['legend'] = elem.attrib['legend']
57 values = OrderedDict()
58
59 # SYSRES measurement
60 if elem.tag == 'sysres':
61 for subel in elem:
62 if subel.tag == 'time':
63 values['start_time'] = isofmt_to_timestamp(subel.attrib['timestamp'])
64 values['elapsed_time'] = float(subel.text)
65 elif subel.tag == 'rusage':
66 rusage = OrderedDict()
67 for field in rusage_fields:
68 if 'time' in field:
69 rusage[field] = float(subel.attrib[field])
70 else:
71 rusage[field] = int(subel.attrib[field])
72 values['rusage'] = rusage
73 elif subel.tag == 'iostat':
74 values['iostat'] = OrderedDict([(f, int(subel.attrib[f]))
75 for f in iostat_fields])
76 elif subel.tag == 'buildstats_file':
77 values['buildstats_file'] = subel.text
78 else:
79 raise TypeError("Unknown sysres value element '{}'".format(subel.tag))
80 # DISKUSAGE measurement
81 elif elem.tag == 'diskusage':
82 values['size'] = int(elem.find('size').text)
83 else:
84 raise Exception("Unknown measurement tag '{}'".format(elem.tag))
85 data['values'] = values
86 return data
87
88 def _read_testcase(elem):
89 """Convert testcase into JSON"""
90 assert elem.tag == 'testcase', "Expecting 'testcase' element instead of {}".format(elem.tag)
91
92 data = OrderedDict()
93 data['name'] = elem.attrib['name']
94 data['description'] = elem.attrib['description']
95 data['status'] = 'SUCCESS'
96 data['start_time'] = isofmt_to_timestamp(elem.attrib['timestamp'])
97 data['elapsed_time'] = float(elem.attrib['time'])
98 measurements = OrderedDict()
99
100 for subel in elem.getchildren():
101 if subel.tag == 'error' or subel.tag == 'failure':
102 data['status'] = subel.tag.upper()
103 data['message'] = subel.attrib['message']
104 data['err_type'] = subel.attrib['type']
105 data['err_output'] = subel.text
106 elif subel.tag == 'skipped':
107 data['status'] = 'SKIPPED'
108 data['message'] = subel.text
109 else:
110 measurements[subel.attrib['name']] = _read_measurement(subel)
111 data['measurements'] = measurements
112 return data
113
114 def _read_testsuite(elem):
115 """Convert suite to JSON"""
116 assert elem.tag == 'testsuite', \
117 "Expecting 'testsuite' element instead of {}".format(elem.tag)
118
119 data = OrderedDict()
120 if 'hostname' in elem.attrib:
121 data['tester_host'] = elem.attrib['hostname']
122 data['start_time'] = isofmt_to_timestamp(elem.attrib['timestamp'])
123 data['elapsed_time'] = float(elem.attrib['time'])
124 tests = OrderedDict()
125
126 for case in elem.getchildren():
127 tests[case.attrib['name']] = _read_testcase(case)
128 data['tests'] = tests
129 return data
130
131 # Main function
132 assert elem.tag == 'testsuites', "Invalid test report format"
133 assert len(elem) == 1, "Too many testsuites"
134
135 return _read_testsuite(elem.getchildren()[0])
136
137
138def aggregate_metadata(metadata):
139 """Aggregate metadata into one, basically a sanity check"""
140 mutable_keys = ('pretty_name', 'version_id')
141
142 def aggregate_obj(aggregate, obj, assert_str=True):
143 """Aggregate objects together"""
144 assert type(aggregate) is type(obj), \
145 "Type mismatch: {} != {}".format(type(aggregate), type(obj))
146 if isinstance(obj, Mapping):
147 assert set(aggregate.keys()) == set(obj.keys())
148 for key, val in obj.items():
149 aggregate_obj(aggregate[key], val, key not in mutable_keys)
150 elif isinstance(obj, list):
151 assert len(aggregate) == len(obj)
152 for i, val in enumerate(obj):
153 aggregate_obj(aggregate[i], val)
154 elif not isinstance(obj, str) or (isinstance(obj, str) and assert_str):
155 assert aggregate == obj, "Data mismatch {} != {}".format(aggregate, obj)
156
157 if not metadata:
158 return {}
159
160 # Do the aggregation
161 aggregate = metadata[0].copy()
162 for testrun in metadata[1:]:
163 aggregate_obj(aggregate, testrun)
164 aggregate['testrun_count'] = len(metadata)
165 return aggregate
166
167
168def aggregate_data(data):
169 """Aggregate multiple test results JSON structures into one"""
170
171 mutable_keys = ('status', 'message', 'err_type', 'err_output')
172
173 class SampleList(list):
174 """Container for numerical samples"""
175 pass
176
177 def new_aggregate_obj(obj):
178 """Create new object for aggregate"""
179 if isinstance(obj, Number):
180 new_obj = SampleList()
181 new_obj.append(obj)
182 elif isinstance(obj, str):
183 new_obj = obj
184 else:
185 # Lists and and dicts are kept as is
186 new_obj = obj.__class__()
187 aggregate_obj(new_obj, obj)
188 return new_obj
189
190 def aggregate_obj(aggregate, obj, assert_str=True):
191 """Recursive "aggregation" of JSON objects"""
192 if isinstance(obj, Number):
193 assert isinstance(aggregate, SampleList)
194 aggregate.append(obj)
195 return
196
197 assert type(aggregate) == type(obj), \
198 "Type mismatch: {} != {}".format(type(aggregate), type(obj))
199 if isinstance(obj, Mapping):
200 for key, val in obj.items():
201 if not key in aggregate:
202 aggregate[key] = new_aggregate_obj(val)
203 else:
204 aggregate_obj(aggregate[key], val, key not in mutable_keys)
205 elif isinstance(obj, list):
206 for i, val in enumerate(obj):
207 if i >= len(aggregate):
208 aggregate[key] = new_aggregate_obj(val)
209 else:
210 aggregate_obj(aggregate[i], val)
211 elif isinstance(obj, str):
212 # Sanity check for data
213 if assert_str:
214 assert aggregate == obj, "Data mismatch {} != {}".format(aggregate, obj)
215 else:
216 raise Exception("BUG: unable to aggregate '{}' ({})".format(type(obj), str(obj)))
217
218 if not data:
219 return {}
220
221 # Do the aggregation
222 aggregate = data[0].__class__()
223 for testrun in data:
224 aggregate_obj(aggregate, testrun)
225 return aggregate
226
227
228class MeasurementVal(float):
229 """Base class representing measurement values"""
230 gv_data_type = 'number'
231
232 def gv_value(self):
233 """Value formatting for visualization"""
234 if self != self:
235 return "null"
236 else:
237 return self
238
239
240class TimeVal(MeasurementVal):
241 """Class representing time values"""
242 quantity = 'time'
243 gv_title = 'elapsed time'
244 gv_data_type = 'timeofday'
245
246 def hms(self):
247 """Split time into hours, minutes and seconeds"""
248 hhh = int(abs(self) / 3600)
249 mmm = int((abs(self) % 3600) / 60)
250 sss = abs(self) % 60
251 return hhh, mmm, sss
252
253 def __str__(self):
254 if self != self:
255 return "nan"
256 hh, mm, ss = self.hms()
257 sign = '-' if self < 0 else ''
258 if hh > 0:
259 return '{}{:d}:{:02d}:{:02.0f}'.format(sign, hh, mm, ss)
260 elif mm > 0:
261 return '{}{:d}:{:04.1f}'.format(sign, mm, ss)
262 elif ss > 1:
263 return '{}{:.1f} s'.format(sign, ss)
264 else:
265 return '{}{:.2f} s'.format(sign, ss)
266
267 def gv_value(self):
268 """Value formatting for visualization"""
269 if self != self:
270 return "null"
271 hh, mm, ss = self.hms()
272 return [hh, mm, int(ss), int(ss*1000) % 1000]
273
274
275class SizeVal(MeasurementVal):
276 """Class representing time values"""
277 quantity = 'size'
278 gv_title = 'size in MiB'
279 gv_data_type = 'number'
280
281 def __str__(self):
282 if self != self:
283 return "nan"
284 if abs(self) < 1024:
285 return '{:.1f} kiB'.format(self)
286 elif abs(self) < 1048576:
287 return '{:.2f} MiB'.format(self / 1024)
288 else:
289 return '{:.2f} GiB'.format(self / 1048576)
290
291 def gv_value(self):
292 """Value formatting for visualization"""
293 if self != self:
294 return "null"
295 return self / 1024
296
297def measurement_stats(meas, prefix='', time=0):
298 """Get statistics of a measurement"""
299 if not meas:
300 return {prefix + 'sample_cnt': 0,
301 prefix + 'mean': MeasurementVal('nan'),
302 prefix + 'stdev': MeasurementVal('nan'),
303 prefix + 'variance': MeasurementVal('nan'),
304 prefix + 'min': MeasurementVal('nan'),
305 prefix + 'max': MeasurementVal('nan'),
306 prefix + 'minus': MeasurementVal('nan'),
307 prefix + 'plus': MeasurementVal('nan')}
308
309 stats = {'name': meas['name']}
310 if meas['type'] == 'sysres':
311 val_cls = TimeVal
312 values = meas['values']['elapsed_time']
313 elif meas['type'] == 'diskusage':
314 val_cls = SizeVal
315 values = meas['values']['size']
316 else:
317 raise Exception("Unknown measurement type '{}'".format(meas['type']))
318 stats['val_cls'] = val_cls
319 stats['quantity'] = val_cls.quantity
320 stats[prefix + 'sample_cnt'] = len(values)
321
322 # Add start time for both type sysres and disk usage
323 start_time = time
324 mean_val = val_cls(mean(values))
325 min_val = val_cls(min(values))
326 max_val = val_cls(max(values))
327
328 stats[prefix + 'mean'] = mean_val
329 if len(values) > 1:
330 stats[prefix + 'stdev'] = val_cls(stdev(values))
331 stats[prefix + 'variance'] = val_cls(variance(values))
332 else:
333 stats[prefix + 'stdev'] = float('nan')
334 stats[prefix + 'variance'] = float('nan')
335 stats[prefix + 'min'] = min_val
336 stats[prefix + 'max'] = max_val
337 stats[prefix + 'minus'] = val_cls(mean_val - min_val)
338 stats[prefix + 'plus'] = val_cls(max_val - mean_val)
339 stats[prefix + 'start_time'] = start_time
340
341 return stats
342
diff --git a/scripts/lib/build_perf/scrape-html-report.js b/scripts/lib/build_perf/scrape-html-report.js
deleted file mode 100644
index 05a1f57001..0000000000
--- a/scripts/lib/build_perf/scrape-html-report.js
+++ /dev/null
@@ -1,56 +0,0 @@
1var fs = require('fs');
2var system = require('system');
3var page = require('webpage').create();
4
5// Examine console log for message from chart drawing
6page.onConsoleMessage = function(msg) {
7 console.log(msg);
8 if (msg === "ALL CHARTS READY") {
9 window.charts_ready = true;
10 }
11 else if (msg.slice(0, 11) === "CHART READY") {
12 var chart_id = msg.split(" ")[2];
13 console.log('grabbing ' + chart_id);
14 var png_data = page.evaluate(function (chart_id) {
15 var chart_div = document.getElementById(chart_id + '_png');
16 return chart_div.outerHTML;
17 }, chart_id);
18 fs.write(args[2] + '/' + chart_id + '.png', png_data, 'w');
19 }
20};
21
22// Check command line arguments
23var args = system.args;
24if (args.length != 3) {
25 console.log("USAGE: " + args[0] + " REPORT_HTML OUT_DIR\n");
26 phantom.exit(1);
27}
28
29// Open the web page
30page.open(args[1], function(status) {
31 if (status == 'fail') {
32 console.log("Failed to open file '" + args[1] + "'");
33 phantom.exit(1);
34 }
35});
36
37// Check status every 100 ms
38interval = window.setInterval(function () {
39 //console.log('waiting');
40 if (window.charts_ready) {
41 clearTimeout(timer);
42 clearInterval(interval);
43
44 var fname = args[1].replace(/\/+$/, "").split("/").pop()
45 console.log("saving " + fname);
46 fs.write(args[2] + '/' + fname, page.content, 'w');
47 phantom.exit(0);
48 }
49}, 100);
50
51// Time-out after 10 seconds
52timer = window.setTimeout(function () {
53 clearInterval(interval);
54 console.log("ERROR: timeout");
55 phantom.exit(1);
56}, 10000);