pytest-timing 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_timing/__init__.py +5 -0
- pytest_timing/cli.py +150 -0
- pytest_timing/collector.py +210 -0
- pytest_timing/model.py +522 -0
- pytest_timing/outputs.py +54 -0
- pytest_timing/plugin.py +382 -0
- pytest_timing/render/__init__.py +1 -0
- pytest_timing/render/ascii.py +283 -0
- pytest_timing/render/html.py +32 -0
- pytest_timing/render/trace.py +126 -0
- pytest_timing/static/__init__.py +0 -0
- pytest_timing/static/report.html +653 -0
- pytest_timing/xdist_compat.py +158 -0
- pytest_timing-0.1.0.dist-info/METADATA +161 -0
- pytest_timing-0.1.0.dist-info/RECORD +18 -0
- pytest_timing-0.1.0.dist-info/WHEEL +4 -0
- pytest_timing-0.1.0.dist-info/entry_points.txt +5 -0
- pytest_timing-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>pytest-timing report</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #ffffff;
|
|
10
|
+
--fg: #1a1a1a;
|
|
11
|
+
--muted: #666;
|
|
12
|
+
--border: #d0d0d0;
|
|
13
|
+
--row-alt: #f4f4f4;
|
|
14
|
+
--link: #0b5fa5;
|
|
15
|
+
--canvas-background: #f7f7f7;
|
|
16
|
+
--canvas-grid: #e6e6e6;
|
|
17
|
+
--canvas-axes: #303030;
|
|
18
|
+
--canvas-text: #303030;
|
|
19
|
+
--canvas-base: #95cce8;
|
|
20
|
+
--canvas-setup: #f0b165;
|
|
21
|
+
--canvas-teardown: #aa95e8;
|
|
22
|
+
--canvas-boot: #c9c9c9;
|
|
23
|
+
--canvas-collect: #95e8aa;
|
|
24
|
+
--canvas-merged: #b8c8d2;
|
|
25
|
+
--canvas-skipped: #dcdcdc;
|
|
26
|
+
--canvas-bad: #d43f3a;
|
|
27
|
+
--canvas-xpass: #d4a017;
|
|
28
|
+
--canvas-active: #2e9e3e;
|
|
29
|
+
--canvas-booting: #8a8a8a;
|
|
30
|
+
--canvas-collecting: #3a7bd5;
|
|
31
|
+
--canvas-rate: rgba(250, 119, 0, 0.12);
|
|
32
|
+
--canvas-rate-line: #fa7700;
|
|
33
|
+
--canvas-hl: #000;
|
|
34
|
+
--tooltip-bg: #fffbe6;
|
|
35
|
+
--tooltip-border: #c9b76a;
|
|
36
|
+
}
|
|
37
|
+
@media (prefers-color-scheme: dark) {
|
|
38
|
+
:root {
|
|
39
|
+
--bg: #17191c;
|
|
40
|
+
--fg: #e8e8e8;
|
|
41
|
+
--muted: #9a9a9a;
|
|
42
|
+
--border: #3a3d42;
|
|
43
|
+
--row-alt: #1f2226;
|
|
44
|
+
--link: #6fb3ff;
|
|
45
|
+
--canvas-background: #1e2126;
|
|
46
|
+
--canvas-grid: #2c3037;
|
|
47
|
+
--canvas-axes: #cfd3d8;
|
|
48
|
+
--canvas-text: #cfd3d8;
|
|
49
|
+
--canvas-base: #4f8fb8;
|
|
50
|
+
--canvas-setup: #c48a3f;
|
|
51
|
+
--canvas-teardown: #7f6bc4;
|
|
52
|
+
--canvas-boot: #4a4a4a;
|
|
53
|
+
--canvas-collect: #3f8f56;
|
|
54
|
+
--canvas-merged: #4b5a66;
|
|
55
|
+
--canvas-skipped: #3c3c3c;
|
|
56
|
+
--canvas-bad: #ff6b66;
|
|
57
|
+
--canvas-xpass: #e0b535;
|
|
58
|
+
--canvas-active: #5ad06a;
|
|
59
|
+
--canvas-booting: #a0a0a0;
|
|
60
|
+
--canvas-collecting: #6fa4ff;
|
|
61
|
+
--canvas-rate: rgba(250, 119, 0, 0.16);
|
|
62
|
+
--canvas-rate-line: #ff9a3c;
|
|
63
|
+
--canvas-hl: #fff;
|
|
64
|
+
--tooltip-bg: #2a2d33;
|
|
65
|
+
--tooltip-border: #5a5f68;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
* { box-sizing: border-box; }
|
|
69
|
+
body {
|
|
70
|
+
margin: 0; padding: 16px 24px 48px;
|
|
71
|
+
font: 14px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
72
|
+
color: var(--fg); background: var(--bg);
|
|
73
|
+
}
|
|
74
|
+
h1 { font-size: 22px; margin: 0 0 12px; }
|
|
75
|
+
h2 { font-size: 16px; margin: 28px 0 8px; }
|
|
76
|
+
.muted { color: var(--muted); }
|
|
77
|
+
code, .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12.5px; }
|
|
78
|
+
table.summary { border-collapse: collapse; margin-bottom: 8px; }
|
|
79
|
+
table.summary td { padding: 3px 12px 3px 0; vertical-align: top; }
|
|
80
|
+
table.summary td:first-child { font-weight: 600; white-space: nowrap; }
|
|
81
|
+
.error-text { color: var(--canvas-bad); }
|
|
82
|
+
.controls {
|
|
83
|
+
display: flex; flex-wrap: wrap; gap: 12px 28px; align-items: center;
|
|
84
|
+
padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px; margin: 8px 0 12px;
|
|
85
|
+
}
|
|
86
|
+
.controls label { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
|
87
|
+
.controls input[type=range] { width: 160px; }
|
|
88
|
+
.controls input[type=text], .controls input[type=number] {
|
|
89
|
+
font: inherit; padding: 2px 6px; border: 1px solid var(--border); border-radius: 4px;
|
|
90
|
+
background: var(--bg); color: var(--fg);
|
|
91
|
+
}
|
|
92
|
+
.controls input[type=number] { width: 64px; }
|
|
93
|
+
.controls .group { display: inline-flex; gap: 10px; align-items: center; }
|
|
94
|
+
.chart-wrap {
|
|
95
|
+
overflow-x: auto; border: 1px solid var(--border); border-radius: 6px;
|
|
96
|
+
background: var(--canvas-background); position: relative;
|
|
97
|
+
}
|
|
98
|
+
.chart-wrap svg { display: block; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; }
|
|
99
|
+
.legend { display: flex; flex-wrap: wrap; gap: 6px 18px; margin: 6px 0 0; font-size: 12px; }
|
|
100
|
+
.legend span { display: inline-flex; align-items: center; gap: 5px; }
|
|
101
|
+
.legend i { display: inline-block; width: 14px; height: 10px; border: 1px solid var(--border); }
|
|
102
|
+
.bar { cursor: pointer; }
|
|
103
|
+
.bar.dim { opacity: 0.25; }
|
|
104
|
+
.bar.hl rect.base { stroke: var(--canvas-hl); stroke-width: 2; }
|
|
105
|
+
rect.base { fill: var(--canvas-base); stroke: none; }
|
|
106
|
+
rect.ph-setup { fill: var(--canvas-setup); }
|
|
107
|
+
rect.ph-teardown { fill: var(--canvas-teardown); }
|
|
108
|
+
rect.ph-call { fill: var(--canvas-base); }
|
|
109
|
+
.bar.bad rect.base { stroke: var(--canvas-bad); stroke-width: 1.5; }
|
|
110
|
+
.bar.xpassed rect.base { stroke: var(--canvas-xpass); stroke-width: 1.5; }
|
|
111
|
+
.bar.skip rect.base { fill: var(--canvas-skipped); stroke: var(--muted); stroke-dasharray: 2 2; }
|
|
112
|
+
.bar.skip rect.ph-setup, .bar.skip rect.ph-teardown { opacity: 0.55; }
|
|
113
|
+
.bar.merged rect.base { fill: var(--canvas-merged); }
|
|
114
|
+
.bar.merged text { fill: var(--canvas-text); pointer-events: none; }
|
|
115
|
+
.band-boot { fill: url(#hatch-boot); }
|
|
116
|
+
.band-collect { fill: url(#hatch-collect); }
|
|
117
|
+
.axis { stroke: var(--canvas-axes); }
|
|
118
|
+
.grid { stroke: var(--canvas-grid); }
|
|
119
|
+
.lbl { fill: var(--canvas-text); }
|
|
120
|
+
.down-mark { stroke: var(--canvas-axes); stroke-width: 1.5; }
|
|
121
|
+
.down-err { stroke: var(--canvas-bad); stroke-width: 2; }
|
|
122
|
+
#tooltip {
|
|
123
|
+
position: fixed; z-index: 10; pointer-events: none; display: none;
|
|
124
|
+
max-width: 560px; padding: 8px 10px; font-size: 12px; line-height: 1.35;
|
|
125
|
+
background: var(--tooltip-bg); color: var(--fg); border: 1px solid var(--tooltip-border);
|
|
126
|
+
border-radius: 5px; box-shadow: 0 2px 8px rgba(0,0,0,0.25); word-break: break-all;
|
|
127
|
+
}
|
|
128
|
+
#tooltip b { font-weight: 600; }
|
|
129
|
+
#tooltip table { border-collapse: collapse; margin-top: 4px; }
|
|
130
|
+
#tooltip td { padding: 0 8px 0 0; }
|
|
131
|
+
table.tests { border-collapse: collapse; width: 100%; font-size: 13px; }
|
|
132
|
+
table.tests th, table.tests td { padding: 4px 8px; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
|
133
|
+
table.tests th { cursor: pointer; user-select: none; position: sticky; top: 0; background: var(--bg); }
|
|
134
|
+
table.tests th.sorted::after { content: " \25BE"; }
|
|
135
|
+
table.tests th.sorted.asc::after { content: " \25B4"; }
|
|
136
|
+
table.tests tbody tr:nth-child(even) { background: var(--row-alt); }
|
|
137
|
+
table.tests td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
138
|
+
table.tests td.nodeid { white-space: normal; word-break: break-all; }
|
|
139
|
+
table.tests td.o-failed, table.tests td.o-error, table.tests td.o-crashed, table.tests td.o-rerun { color: var(--canvas-bad); font-weight: 600; }
|
|
140
|
+
table.tests td.o-xpassed { color: var(--canvas-xpass); }
|
|
141
|
+
table.tests td.o-skipped, table.tests td.o-xfailed { color: var(--muted); }
|
|
142
|
+
.empty { padding: 24px; text-align: center; color: var(--muted); }
|
|
143
|
+
button { font: inherit; padding: 2px 10px; border: 1px solid var(--border); border-radius: 4px; background: var(--bg); color: var(--fg); cursor: pointer; }
|
|
144
|
+
button.on { background: var(--link); color: #fff; border-color: var(--link); }
|
|
145
|
+
</style>
|
|
146
|
+
</head>
|
|
147
|
+
<body>
|
|
148
|
+
<h1>pytest-timing report</h1>
|
|
149
|
+
<table class="summary" id="summary"></table>
|
|
150
|
+
|
|
151
|
+
<div class="controls">
|
|
152
|
+
<label>Min test time: <input type="range" id="min-unit-time" min="0" max="30" step="0.05" value="0"> <output id="min-unit-time-output">0s</output></label>
|
|
153
|
+
<label>Scale: <input type="range" id="scale" min="1" max="100" step="1" value="1"> <output id="scale-output"></output></label>
|
|
154
|
+
<span class="group">
|
|
155
|
+
<button id="view-lane" class="on">Lanes</button>
|
|
156
|
+
<button id="view-unit">Units</button>
|
|
157
|
+
<label>top <input type="number" id="top-n" min="1" max="5000" value="50"></label>
|
|
158
|
+
</span>
|
|
159
|
+
<span class="group" id="outcome-filters"></span>
|
|
160
|
+
<label>Filter: <input type="text" id="text-filter" placeholder="nodeid substring" size="28"></label>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<h2 id="pipeline-title">Worker lanes</h2>
|
|
164
|
+
<div class="chart-wrap" id="pipeline-container"></div>
|
|
165
|
+
<div class="legend" id="pipeline-legend"></div>
|
|
166
|
+
|
|
167
|
+
<h2>Concurrency</h2>
|
|
168
|
+
<div class="chart-wrap" id="timing-container"></div>
|
|
169
|
+
<div class="legend">
|
|
170
|
+
<span><i style="background:var(--canvas-active)"></i>Active workers</span>
|
|
171
|
+
<span><i style="background:var(--canvas-collecting)"></i>Collecting</span>
|
|
172
|
+
<span><i style="background:var(--canvas-booting)"></i>Booting</span>
|
|
173
|
+
<span><i style="background:var(--canvas-rate)"></i>Tests completed / s</span>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<h2>Tests <span class="muted" id="table-note"></span></h2>
|
|
177
|
+
<div style="overflow-x:auto"><table class="tests" id="tests-table"><thead></thead><tbody></tbody></table></div>
|
|
178
|
+
|
|
179
|
+
<div id="tooltip"></div>
|
|
180
|
+
<canvas id="measure" width="1" height="1" style="display:none"></canvas>
|
|
181
|
+
<script id="pytest-timing-data" type="application/json">__PYTEST_TIMING_DATA__</script>
|
|
182
|
+
<script>
|
|
183
|
+
(function () {
|
|
184
|
+
'use strict';
|
|
185
|
+
try {
|
|
186
|
+
var DATA = JSON.parse(document.getElementById('pytest-timing-data').textContent);
|
|
187
|
+
var RUN = DATA.run || {};
|
|
188
|
+
var WORKERS = DATA.workers || [];
|
|
189
|
+
var TESTS = DATA.tests || [];
|
|
190
|
+
var PHASES = ['setup', 'call', 'teardown'];
|
|
191
|
+
// Outcome semantics come from the model (pytest_timing.model.OUTCOME_REGISTRY).
|
|
192
|
+
var OUTCOME_INFO = DATA.outcomes || {};
|
|
193
|
+
var OUTCOMES = Object.keys(OUTCOME_INFO);
|
|
194
|
+
var BAD = {};
|
|
195
|
+
OUTCOMES.forEach(function (o) { if (OUTCOME_INFO[o].is_failure) BAD[o] = 1; });
|
|
196
|
+
TESTS.forEach(function (t) { if (OUTCOMES.indexOf(t.outcome) < 0) OUTCOMES.push(t.outcome); });
|
|
197
|
+
// Iterative extrema: spreading a huge array into Math.max overflows the argument limit.
|
|
198
|
+
function maxOf(arr, f, init) { var m = init === undefined ? -Infinity : init; for (var i = 0; i < arr.length; i++) { var v = f ? f(arr[i]) : arr[i]; if (v > m) m = v; } return m; }
|
|
199
|
+
function minOf(arr, f, init) { var m = init === undefined ? Infinity : init; for (var i = 0; i < arr.length; i++) { var v = f ? f(arr[i]) : arr[i]; if (v < m) m = v; } return m; }
|
|
200
|
+
var BOX_HEIGHT = 26, BAR_H = 20, LEFT_PAD = 8, RIGHT_PAD = 24, TOP_PAD = 24, BOTTOM_PAD = 22;
|
|
201
|
+
var TABLE_CAP = 2000;
|
|
202
|
+
|
|
203
|
+
// ---- data prep -------------------------------------------------------------------
|
|
204
|
+
var moduleIndex = {}, modules = [];
|
|
205
|
+
TESTS.forEach(function (t, i) {
|
|
206
|
+
t.i = i;
|
|
207
|
+
t.dur = t.stop - t.start;
|
|
208
|
+
t.module = t.nodeid.split('::')[0];
|
|
209
|
+
if (!(t.module in moduleIndex)) { moduleIndex[t.module] = modules.length; modules.push(t.module); }
|
|
210
|
+
t.m = moduleIndex[t.module];
|
|
211
|
+
t.ph = {};
|
|
212
|
+
PHASES.forEach(function (p) { var v = t.phases && t.phases[p]; if (v) t.ph[p] = { start: v[0], stop: v[1], dur: v[2] }; });
|
|
213
|
+
});
|
|
214
|
+
var laneIds = WORKERS.map(function (w) { return w.id; }), laneIndex = {};
|
|
215
|
+
laneIds.forEach(function (id, i) { laneIndex[id] = i; });
|
|
216
|
+
TESTS.forEach(function (t) { if (!(t.worker in laneIndex)) { laneIndex[t.worker] = laneIds.length; laneIds.push(t.worker); } });
|
|
217
|
+
var workerById = {};
|
|
218
|
+
WORKERS.forEach(function (w) { workerById[w.id] = w; });
|
|
219
|
+
// Orderings computed once; filters preserve them, so renders never sort the full list.
|
|
220
|
+
var TESTS_BY_START = TESTS.slice().sort(function (a, b) { return a.start - b.start || a.stop - b.stop; });
|
|
221
|
+
var TESTS_BY_DUR = TESTS.slice().sort(function (a, b) { return b.dur - a.dur || a.i - b.i; });
|
|
222
|
+
var SUMMARY = DATA.summary || { wall: 0, busy: 0, utilisation: 0, lanes: [] };
|
|
223
|
+
function events(intervals) {
|
|
224
|
+
var ev = [];
|
|
225
|
+
intervals.forEach(function (iv) { ev.push([iv[0], 1]); ev.push([iv[1], -1]); });
|
|
226
|
+
return ev.sort(function (a, b) { return a[0] - b[0] || a[1] - b[1]; });
|
|
227
|
+
}
|
|
228
|
+
var ACTIVE_EVENTS = events(TESTS.map(function (t) { return [t.start, t.stop]; }));
|
|
229
|
+
var BOOT_EVENTS = events(WORKERS.filter(function (w) { return w.ready != null && w.start != null && w.ready > w.start; }).map(function (w) { return [w.start, w.ready]; }));
|
|
230
|
+
var COLLECT_EVENTS = events(WORKERS.filter(function (w) { return w.ready != null && w.collected != null && w.collected > w.ready; }).map(function (w) { return [w.ready, w.collected]; }));
|
|
231
|
+
var WALL = Math.max(0, (RUN.stop || 0) - (RUN.start || 0));
|
|
232
|
+
WORKERS.forEach(function (w) { ['ready', 'collected', 'down'].forEach(function (k) { if (w[k] != null) WALL = Math.max(WALL, w[k]); }); });
|
|
233
|
+
TESTS.forEach(function (t) { WALL = Math.max(WALL, t.stop); });
|
|
234
|
+
if (WALL <= 0) WALL = 1;
|
|
235
|
+
var maxDur = 0;
|
|
236
|
+
TESTS.forEach(function (t) { if (t.dur > maxDur) maxDur = t.dur; });
|
|
237
|
+
|
|
238
|
+
var state = { minTime: 0, pxPerSec: 100, view: 'lane', topN: 50, outcomes: {}, text: '', pinned: null, sortKey: 'dur', sortDir: -1 };
|
|
239
|
+
OUTCOMES.forEach(function (o) { state.outcomes[o] = true; });
|
|
240
|
+
|
|
241
|
+
// ---- helpers ---------------------------------------------------------------------
|
|
242
|
+
function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
|
|
243
|
+
function fmt(s) {
|
|
244
|
+
if (s == null || isNaN(s)) return '-';
|
|
245
|
+
if (s >= 60) { var m = Math.floor(s / 60); return m + 'm ' + (s - m * 60).toFixed(1) + 's'; }
|
|
246
|
+
if (s >= 1) return s.toFixed(2) + 's';
|
|
247
|
+
if (s >= 0.001) return (s * 1000).toFixed(1) + 'ms';
|
|
248
|
+
return (s * 1e6).toFixed(0) + 'µs';
|
|
249
|
+
}
|
|
250
|
+
function pct(part, total) { return total > 0 ? ' (' + Math.round(part / total * 100) + '%)' : ''; }
|
|
251
|
+
function fmtDate(epoch) { try { return new Date(epoch * 1000).toISOString().replace('T', ' ').replace(/\.\d+Z$/, 'Z'); } catch (e) { return '-'; } }
|
|
252
|
+
var mctx = document.getElementById('measure').getContext('2d');
|
|
253
|
+
mctx.font = '11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';
|
|
254
|
+
function textWidth(s) { return mctx.measureText(s).width; }
|
|
255
|
+
function niceStep(pxPerSec, minPx) {
|
|
256
|
+
var steps = [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 1800, 3600];
|
|
257
|
+
for (var i = 0; i < steps.length; i++) if (steps[i] * pxPerSec >= minPx) return steps[i];
|
|
258
|
+
return steps[steps.length - 1];
|
|
259
|
+
}
|
|
260
|
+
function tickLabel(s) { return s >= 60 ? (s / 60).toFixed(s % 60 ? 1 : 0) + 'm' : (s < 1 ? (s * 1000).toFixed(0) + 'ms' : s + 's'); }
|
|
261
|
+
|
|
262
|
+
function filteredTests() {
|
|
263
|
+
var q = state.text.toLowerCase();
|
|
264
|
+
return TESTS.filter(function (t) {
|
|
265
|
+
return t.dur >= state.minTime && state.outcomes[t.outcome] !== false && (!q || t.nodeid.toLowerCase().indexOf(q) >= 0);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function barClass(t) {
|
|
269
|
+
var c = 'bar';
|
|
270
|
+
if (BAD[t.outcome]) c += ' bad';
|
|
271
|
+
else if (t.outcome === 'skipped' || t.outcome === 'xfailed') c += ' skip';
|
|
272
|
+
else if (t.outcome === 'xpassed') c += ' xpassed';
|
|
273
|
+
return c;
|
|
274
|
+
}
|
|
275
|
+
function axisSvg(x0, width, height, pxPerSec) {
|
|
276
|
+
var step = niceStep(pxPerSec, 70), out = [];
|
|
277
|
+
for (var s = 0; s <= WALL + 1e-9; s += step) {
|
|
278
|
+
var x = x0 + s * pxPerSec;
|
|
279
|
+
out.push('<line class="grid" x1="' + x.toFixed(1) + '" y1="' + TOP_PAD + '" x2="' + x.toFixed(1) + '" y2="' + (height - BOTTOM_PAD) + '"/>');
|
|
280
|
+
out.push('<text class="lbl" x="' + x.toFixed(1) + '" y="' + (height - 6) + '" text-anchor="middle">' + tickLabel(+s.toFixed(6)) + '</text>');
|
|
281
|
+
}
|
|
282
|
+
out.push('<line class="axis" x1="' + x0 + '" y1="' + (height - BOTTOM_PAD) + '" x2="' + (x0 + width) + '" y2="' + (height - BOTTOM_PAD) + '"/>');
|
|
283
|
+
out.push('<line class="axis" x1="' + x0 + '" y1="' + TOP_PAD + '" x2="' + x0 + '" y2="' + (height - BOTTOM_PAD) + '"/>');
|
|
284
|
+
return out.join('');
|
|
285
|
+
}
|
|
286
|
+
var DEFS = '<defs>' +
|
|
287
|
+
'<pattern id="hatch-boot" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="6" height="6" fill="var(--canvas-background)"/><line x1="0" y1="0" x2="0" y2="6" stroke="var(--canvas-boot)" stroke-width="2"/></pattern>' +
|
|
288
|
+
'<pattern id="hatch-collect" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)"><rect width="6" height="6" fill="var(--canvas-background)"/><line x1="0" y1="0" x2="0" y2="6" stroke="var(--canvas-collect)" stroke-width="2"/></pattern>' +
|
|
289
|
+
'</defs>';
|
|
290
|
+
|
|
291
|
+
// Draw one test span as a bar group. Returns SVG string.
|
|
292
|
+
function testBar(t, x0, y, pxPerSec, minPx) {
|
|
293
|
+
var x = x0 + t.start * pxPerSec, w = Math.max(minPx, t.dur * pxPerSec);
|
|
294
|
+
var s = '<g class="' + barClass(t) + '" data-i="' + t.i + '" data-m="' + t.m + '">';
|
|
295
|
+
s += '<rect class="base" x="' + x.toFixed(2) + '" y="' + y + '" width="' + w.toFixed(2) + '" height="' + BAR_H + '" rx="2"/>';
|
|
296
|
+
if (w >= 2) {
|
|
297
|
+
['setup', 'teardown'].forEach(function (p) {
|
|
298
|
+
var ph = t.ph[p]; if (!ph) return;
|
|
299
|
+
var px = x0 + ph.start * pxPerSec, pw = ph.dur * pxPerSec;
|
|
300
|
+
if (pw < 0.5) return;
|
|
301
|
+
s += '<rect class="ph-' + p + '" x="' + px.toFixed(2) + '" y="' + (y + 2) + '" width="' + pw.toFixed(2) + '" height="' + (BAR_H - 4) + '"/>';
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return s + '</g>';
|
|
305
|
+
}
|
|
306
|
+
// Merge sub-pixel spans into aggregate bars. Returns array of {merged:true, tests:[..], start, stop} or a test.
|
|
307
|
+
function clusterLane(spans, pxPerSec) {
|
|
308
|
+
var out = [], cur = null;
|
|
309
|
+
spans.forEach(function (t) {
|
|
310
|
+
var small = t.dur * pxPerSec < 1;
|
|
311
|
+
if (small && cur && (t.start - cur.stop) * pxPerSec <= 1) {
|
|
312
|
+
cur.tests.push(t); cur.stop = Math.max(cur.stop, t.stop); return;
|
|
313
|
+
}
|
|
314
|
+
if (cur) { out.push(cur); cur = null; }
|
|
315
|
+
if (small) cur = { merged: true, tests: [t], start: t.start, stop: t.stop };
|
|
316
|
+
else out.push(t);
|
|
317
|
+
});
|
|
318
|
+
if (cur) out.push(cur);
|
|
319
|
+
return out.map(function (c) { return (c.merged && c.tests.length === 1) ? c.tests[0] : c; });
|
|
320
|
+
}
|
|
321
|
+
var clusters = [];
|
|
322
|
+
function mergedBar(c, x0, y, pxPerSec) {
|
|
323
|
+
var id = clusters.length; clusters.push(c);
|
|
324
|
+
var x = x0 + c.start * pxPerSec, w = Math.max(3, (c.stop - c.start) * pxPerSec);
|
|
325
|
+
var bad = c.tests.some(function (t) { return BAD[t.outcome]; });
|
|
326
|
+
var s = '<g class="bar merged' + (bad ? ' bad' : '') + '" data-c="' + id + '">';
|
|
327
|
+
s += '<rect class="base" x="' + x.toFixed(2) + '" y="' + y + '" width="' + w.toFixed(2) + '" height="' + BAR_H + '" rx="2"/>';
|
|
328
|
+
var label = '×' + c.tests.length;
|
|
329
|
+
if (w > textWidth(label) + 4) s += '<text x="' + (x + w / 2).toFixed(1) + '" y="' + (y + BAR_H / 2 + 4) + '" text-anchor="middle">' + label + '</text>';
|
|
330
|
+
return s + '</g>';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---- pipeline: lane view -----------------------------------------------------------
|
|
334
|
+
function renderLanes(tests) {
|
|
335
|
+
clusters = [];
|
|
336
|
+
var byLane = {}; laneIds.forEach(function (id) { byLane[id] = []; });
|
|
337
|
+
var allowed = new Set(tests);
|
|
338
|
+
TESTS_BY_START.forEach(function (t) { if (allowed.has(t)) byLane[t.worker].push(t); });
|
|
339
|
+
var labelW = 0; laneIds.forEach(function (id) { labelW = Math.max(labelW, textWidth(id)); });
|
|
340
|
+
var x0 = LEFT_PAD + labelW + 10, pxPerSec = state.pxPerSec;
|
|
341
|
+
var width = Math.ceil(WALL * pxPerSec), height = TOP_PAD + laneIds.length * BOX_HEIGHT + BOTTOM_PAD;
|
|
342
|
+
var svg = ['<svg width="' + (x0 + width + RIGHT_PAD) + '" height="' + height + '">', DEFS];
|
|
343
|
+
svg.push(axisSvg(x0, width, height, pxPerSec));
|
|
344
|
+
laneIds.forEach(function (id, row) {
|
|
345
|
+
var y = TOP_PAD + row * BOX_HEIGHT, by = y + (BOX_HEIGHT - BAR_H) / 2, w = workerById[id];
|
|
346
|
+
svg.push('<text class="lbl" x="' + LEFT_PAD + '" y="' + (y + BOX_HEIGHT / 2 + 4) + '">' + esc(id) + '</text>');
|
|
347
|
+
if (w) {
|
|
348
|
+
var end = w.down != null ? w.down : WALL;
|
|
349
|
+
if (w.ready != null && w.ready > 0)
|
|
350
|
+
if (w.start != null) svg.push('<rect class="band-boot" x="' + (x0 + w.start * pxPerSec).toFixed(2) + '" y="' + by + '" width="' + ((w.ready - w.start) * pxPerSec).toFixed(2) + '" height="' + BAR_H + '"><title>' + esc(id) + ' booting: ' + fmt(w.ready - w.start) + '</title></rect>');
|
|
351
|
+
if (w.ready != null && w.collected != null && w.collected > w.ready)
|
|
352
|
+
svg.push('<rect class="band-collect" x="' + (x0 + w.ready * pxPerSec).toFixed(2) + '" y="' + by + '" width="' + ((w.collected - w.ready) * pxPerSec).toFixed(2) + '" height="' + BAR_H + '"><title>' + esc(id) + ' collecting: ' + fmt(w.collected - w.ready) + (w.items != null ? ' (' + w.items + ' items)' : '') + '</title></rect>');
|
|
353
|
+
if (w.down != null) {
|
|
354
|
+
var dx = (x0 + end * pxPerSec).toFixed(2);
|
|
355
|
+
svg.push('<line class="' + (w.error ? 'down-err' : 'down-mark') + '" x1="' + dx + '" y1="' + by + '" x2="' + dx + '" y2="' + (by + BAR_H) + '"><title>' + esc(id) + (w.error ? ' crashed: ' + esc(w.error) : ' shutdown') + ' at ' + fmt(w.down) + '</title></line>');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
var spans = byLane[id];
|
|
359
|
+
clusterLane(spans, pxPerSec).forEach(function (c) {
|
|
360
|
+
svg.push(c.merged ? mergedBar(c, x0, by, pxPerSec) : testBar(c, x0, by, pxPerSec, 1));
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
svg.push('</svg>');
|
|
364
|
+
return svg.join('');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ---- pipeline: unit view -----------------------------------------------------------
|
|
368
|
+
function renderUnits(tests) {
|
|
369
|
+
clusters = [];
|
|
370
|
+
var allowed = new Set(tests), top = [];
|
|
371
|
+
for (var k = 0; k < TESTS_BY_DUR.length && top.length < state.topN; k++) if (allowed.has(TESTS_BY_DUR[k])) top.push(TESTS_BY_DUR[k]);
|
|
372
|
+
top.sort(function (a, b) { return a.start - b.start; });
|
|
373
|
+
var pxPerSec = state.pxPerSec;
|
|
374
|
+
var numW = textWidth(String(top.length)) + 6, x0 = LEFT_PAD + numW + 6;
|
|
375
|
+
var width = Math.ceil(WALL * pxPerSec), height = TOP_PAD + top.length * BOX_HEIGHT + BOTTOM_PAD;
|
|
376
|
+
var labelMax = 0;
|
|
377
|
+
var labels = top.map(function (t) { var l = t.nodeid + ' (' + t.worker + '): ' + fmt(t.dur); labelMax = Math.max(labelMax, textWidth(l) + (t.start + t.dur) * pxPerSec); return l; });
|
|
378
|
+
var totalW = Math.max(width, Math.ceil(labelMax)) + RIGHT_PAD;
|
|
379
|
+
var svg = ['<svg width="' + (x0 + totalW) + '" height="' + height + '">', DEFS, axisSvg(x0, width, height, pxPerSec)];
|
|
380
|
+
top.forEach(function (t, row) {
|
|
381
|
+
var y = TOP_PAD + row * BOX_HEIGHT, by = y + (BOX_HEIGHT - BAR_H) / 2;
|
|
382
|
+
svg.push('<text class="lbl" x="' + (LEFT_PAD + numW) + '" y="' + (y + BOX_HEIGHT / 2 + 4) + '" text-anchor="end">' + (row + 1) + '</text>');
|
|
383
|
+
svg.push(testBar(t, x0, by, pxPerSec, 2));
|
|
384
|
+
svg.push('<text class="lbl" x="' + (x0 + t.stop * pxPerSec + 5).toFixed(1) + '" y="' + (y + BOX_HEIGHT / 2 + 4) + '">' + esc(labels[row]) + '</text>');
|
|
385
|
+
});
|
|
386
|
+
svg.push('</svg>');
|
|
387
|
+
return svg.join('');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function renderPipeline() {
|
|
391
|
+
var c = document.getElementById('pipeline-container'), tests = filteredTests();
|
|
392
|
+
document.getElementById('pipeline-title').textContent = state.view === 'lane' ? 'Worker lanes' : 'Slowest tests (top ' + state.topN + ')';
|
|
393
|
+
if (!TESTS.length && !laneIds.length) { c.innerHTML = '<div class="empty">No tests recorded.</div>'; return; }
|
|
394
|
+
if (!tests.length && state.view === 'unit') { c.innerHTML = '<div class="empty">No tests match the current filters.</div>'; return; }
|
|
395
|
+
c.innerHTML = state.view === 'lane' ? renderLanes(tests) : renderUnits(tests);
|
|
396
|
+
applyPin();
|
|
397
|
+
var lg = document.getElementById('pipeline-legend');
|
|
398
|
+
lg.innerHTML = '<span><i style="background:var(--canvas-base)"></i>call</span>' +
|
|
399
|
+
'<span><i style="background:var(--canvas-setup)"></i>setup</span>' +
|
|
400
|
+
'<span><i style="background:var(--canvas-teardown)"></i>teardown</span>' +
|
|
401
|
+
'<span><i style="border-color:var(--canvas-bad);border-width:2px"></i>failed / error / crashed / rerun</span>' +
|
|
402
|
+
'<span><i style="background:var(--canvas-skipped);border-style:dashed"></i>skipped / xfailed</span>' +
|
|
403
|
+
(state.view === 'lane' ? '<span><i style="background:repeating-linear-gradient(45deg,var(--canvas-boot) 0 2px,transparent 2px 5px)"></i>booting</span>' +
|
|
404
|
+
'<span><i style="background:repeating-linear-gradient(-45deg,var(--canvas-collect) 0 2px,transparent 2px 5px)"></i>collecting</span>' +
|
|
405
|
+
'<span><i style="background:var(--canvas-merged)"></i>merged tiny tests (×N)</span>' : '') +
|
|
406
|
+
'<span class="muted">hover for details, click to pin a module</span>';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ---- concurrency ----------------------------------------------------------------
|
|
410
|
+
function renderConcurrency() {
|
|
411
|
+
var c = document.getElementById('timing-container');
|
|
412
|
+
if (!TESTS.length && !WORKERS.length) { c.innerHTML = '<div class="empty">No data.</div>'; return; }
|
|
413
|
+
var pxPerSec = state.pxPerSec, H = 220, plotH = H - TOP_PAD - BOTTOM_PAD;
|
|
414
|
+
var labelW = textWidth('000') + 8, x0 = LEFT_PAD + labelW, width = Math.ceil(WALL * pxPerSec);
|
|
415
|
+
var cols = Math.max(1, width);
|
|
416
|
+
// Event sweep sampled per pixel column over a pre-sorted event list.
|
|
417
|
+
function sample(ev) {
|
|
418
|
+
var res = new Array(cols), cur = 0, k = 0, peak = 0;
|
|
419
|
+
for (var col = 0; col < cols; col++) {
|
|
420
|
+
var t = (col + 1) / pxPerSec, mx = cur;
|
|
421
|
+
while (k < ev.length && ev[k][0] <= t) { cur += ev[k][1]; if (cur > mx) mx = cur; k++; }
|
|
422
|
+
res[col] = mx; if (mx > peak) peak = mx;
|
|
423
|
+
}
|
|
424
|
+
return { v: res, peak: peak };
|
|
425
|
+
}
|
|
426
|
+
var active = sample(ACTIVE_EVENTS), booting = sample(BOOT_EVENTS), collecting = sample(COLLECT_EVENTS);
|
|
427
|
+
var peak = Math.max(1, active.peak, booting.peak, collecting.peak, laneIds.length);
|
|
428
|
+
// Tests completed per second: buckets follow the scale, at least 8px wide.
|
|
429
|
+
var bucket = niceStep(pxPerSec, 8), nb = Math.ceil(WALL / bucket) + 1, rate = new Array(nb), ratePeak = 0;
|
|
430
|
+
for (var b = 0; b < nb; b++) rate[b] = 0;
|
|
431
|
+
TESTS.forEach(function (t) { rate[Math.min(nb - 1, Math.floor(t.stop / bucket))] += 1; });
|
|
432
|
+
for (b = 0; b < nb; b++) { rate[b] /= bucket; if (rate[b] > ratePeak) ratePeak = rate[b]; }
|
|
433
|
+
var svg = ['<svg width="' + (x0 + width + RIGHT_PAD + 40) + '" height="' + H + '">'];
|
|
434
|
+
svg.push(axisSvg(x0, width, H, pxPerSec));
|
|
435
|
+
var yStep = peak <= 10 ? 1 : Math.ceil(peak / 8);
|
|
436
|
+
for (var yv = 0; yv <= peak; yv += yStep) {
|
|
437
|
+
var yy = TOP_PAD + plotH - yv / peak * plotH;
|
|
438
|
+
svg.push('<line class="grid" x1="' + x0 + '" y1="' + yy.toFixed(1) + '" x2="' + (x0 + width) + '" y2="' + yy.toFixed(1) + '"/>');
|
|
439
|
+
svg.push('<text class="lbl" x="' + (x0 - 4) + '" y="' + (yy + 4).toFixed(1) + '" text-anchor="end">' + yv + '</text>');
|
|
440
|
+
}
|
|
441
|
+
svg.push('<text class="lbl" x="' + LEFT_PAD + '" y="' + (TOP_PAD - 8) + '">workers</text>');
|
|
442
|
+
if (ratePeak > 0) {
|
|
443
|
+
var xr = x0 + width + 4;
|
|
444
|
+
svg.push('<text class="lbl" x="' + xr + '" y="' + (TOP_PAD - 8) + '">tests/s</text>');
|
|
445
|
+
svg.push('<text class="lbl" x="' + xr + '" y="' + (TOP_PAD + 4) + '">' + (ratePeak >= 10 ? ratePeak.toFixed(0) : ratePeak.toFixed(1)) + '</text>');
|
|
446
|
+
svg.push('<text class="lbl" x="' + xr + '" y="' + (TOP_PAD + plotH + 4) + '">0</text>');
|
|
447
|
+
var area = 'M' + x0 + ' ' + (TOP_PAD + plotH);
|
|
448
|
+
for (b = 0; b < nb; b++) {
|
|
449
|
+
var y1 = TOP_PAD + plotH - rate[b] / ratePeak * plotH, xa = x0 + b * bucket * pxPerSec, xb = Math.min(x0 + width, x0 + (b + 1) * bucket * pxPerSec);
|
|
450
|
+
area += ' L' + xa.toFixed(1) + ' ' + y1.toFixed(1) + ' L' + xb.toFixed(1) + ' ' + y1.toFixed(1);
|
|
451
|
+
}
|
|
452
|
+
area += ' L' + (x0 + width) + ' ' + (TOP_PAD + plotH) + ' Z';
|
|
453
|
+
svg.push('<path d="' + area + '" fill="var(--canvas-rate)" stroke="var(--canvas-rate-line)" stroke-width="0.75" stroke-opacity="0.6"/>');
|
|
454
|
+
}
|
|
455
|
+
function line(series, color) {
|
|
456
|
+
if (!series.peak) return '';
|
|
457
|
+
var d = '', prev = -1;
|
|
458
|
+
for (var col = 0; col < cols; col++) {
|
|
459
|
+
var v = series.v[col], y = TOP_PAD + plotH - v / peak * plotH, x = x0 + col;
|
|
460
|
+
if (col === 0) d += 'M' + x + ' ' + y.toFixed(1);
|
|
461
|
+
else if (v !== prev) d += ' L' + x + ' ' + (TOP_PAD + plotH - prev / peak * plotH).toFixed(1) + ' L' + x + ' ' + y.toFixed(1);
|
|
462
|
+
prev = v;
|
|
463
|
+
}
|
|
464
|
+
d += ' L' + (x0 + cols) + ' ' + (TOP_PAD + plotH - prev / peak * plotH).toFixed(1);
|
|
465
|
+
return '<path d="' + d + '" fill="none" stroke="' + color + '" stroke-width="1.5"/>';
|
|
466
|
+
}
|
|
467
|
+
svg.push(line(booting, 'var(--canvas-booting)'));
|
|
468
|
+
svg.push(line(collecting, 'var(--canvas-collecting)'));
|
|
469
|
+
svg.push(line(active, 'var(--canvas-active)'));
|
|
470
|
+
svg.push('</svg>');
|
|
471
|
+
c.innerHTML = svg.join('');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ---- table ----------------------------------------------------------------------
|
|
475
|
+
var COLS = [
|
|
476
|
+
['idx', '#'], ['nodeid', 'Test'], ['worker', 'Worker'], ['dur', 'Total'],
|
|
477
|
+
['setup', 'Setup'], ['call', 'Call'], ['teardown', 'Teardown'], ['outcome', 'Outcome'], ['start', 'Start']
|
|
478
|
+
];
|
|
479
|
+
function sortVal(t, key) {
|
|
480
|
+
if (key === 'idx') return t.i;
|
|
481
|
+
if (PHASES.indexOf(key) >= 0) return t.ph[key] ? t.ph[key].dur : 0;
|
|
482
|
+
return t[key];
|
|
483
|
+
}
|
|
484
|
+
function renderTable() {
|
|
485
|
+
var tests = filteredTests(), key = state.sortKey, dir = state.sortDir;
|
|
486
|
+
tests.sort(function (a, b) { var va = sortVal(a, key), vb = sortVal(b, key); return (va < vb ? -1 : va > vb ? 1 : 0) * dir || a.i - b.i; });
|
|
487
|
+
var thead = COLS.map(function (c) { return '<th data-key="' + c[0] + '" class="' + (c[0] === key ? 'sorted' + (dir > 0 ? ' asc' : '') : '') + '">' + c[1] + '</th>'; }).join('');
|
|
488
|
+
document.querySelector('#tests-table thead').innerHTML = '<tr>' + thead + '</tr>';
|
|
489
|
+
var shown = tests.slice(0, TABLE_CAP), rows = [];
|
|
490
|
+
shown.forEach(function (t, n) {
|
|
491
|
+
rows.push('<tr><td class="num">' + (n + 1) + '</td><td class="nodeid mono">' + esc(t.nodeid) + (t.attempt ? ' <span class="muted">(attempt ' + (t.attempt + 1) + ')</span>' : '') + '</td><td class="mono">' + esc(t.worker) + '</td>' +
|
|
492
|
+
'<td class="num">' + fmt(t.dur) + '</td>' +
|
|
493
|
+
PHASES.map(function (p) { var ph = t.ph[p]; return '<td class="num">' + (ph ? fmt(ph.dur) + pct(ph.dur, t.dur) : '-') + '</td>'; }).join('') +
|
|
494
|
+
'<td class="o-' + t.outcome + '">' + esc(t.outcome) + '</td><td class="num">' + fmt(t.start) + '</td></tr>');
|
|
495
|
+
});
|
|
496
|
+
document.querySelector('#tests-table tbody').innerHTML = rows.join('') || '<tr><td colspan="9" class="empty">No tests match the current filters.</td></tr>';
|
|
497
|
+
document.getElementById('table-note').textContent = tests.length > TABLE_CAP ? '(showing ' + TABLE_CAP + ' of ' + tests.length + ')' : '(' + tests.length + ' of ' + TESTS.length + ')';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ---- summary ---------------------------------------------------------------------
|
|
501
|
+
function renderSummary() {
|
|
502
|
+
var counts = {}; TESTS.forEach(function (t) { counts[t.outcome] = (counts[t.outcome] || 0) + 1; });
|
|
503
|
+
var busy = SUMMARY.busy, avail = 0, longest = null;
|
|
504
|
+
SUMMARY.lanes.forEach(function (l) {
|
|
505
|
+
avail += Math.max(0, l.down - l.ready);
|
|
506
|
+
if (l.last != null && (!longest || l.last > longest.last)) longest = { id: l.id, last: l.last };
|
|
507
|
+
});
|
|
508
|
+
var rows = [];
|
|
509
|
+
function row(k, v, cls) { rows.push('<tr><td>' + k + '</td><td' + (cls ? ' class="' + cls + '"' : '') + '>' + v + '</td></tr>'); }
|
|
510
|
+
row('Command:', '<code>' + esc((RUN.argv || []).join(' ') || '-') + '</code>');
|
|
511
|
+
row('Root:', '<code>' + esc(RUN.rootdir || '-') + '</code>');
|
|
512
|
+
row('Versions:', 'python ' + esc(RUN.python || '?') + ', pytest ' + esc(RUN.pytest || '?') + (RUN.xdist ? ', xdist ' + esc(RUN.xdist) : ', no xdist'));
|
|
513
|
+
row('Workers:', laneIds.length + (RUN.numprocesses != null ? ' (jobs=' + RUN.numprocesses + (RUN.dist ? ', dist=' + esc(RUN.dist) : '') + ')' : ''));
|
|
514
|
+
row('Run start:', fmtDate(RUN.start));
|
|
515
|
+
row('Total time:', fmt(WALL) + (RUN.complete === false ? ' <span class="error-text">(incomplete run)</span>' : ''));
|
|
516
|
+
var TERM = (DATA.terminations || {})[RUN.termination] || { label: RUN.termination || 'unknown' };
|
|
517
|
+
row('Ended:', esc(TERM.label) + (RUN.reason ? ' <span class="muted">' + esc(RUN.reason) + '</span>' : '') + (RUN.exit_status != null ? ', exit status ' + RUN.exit_status : ''), RUN.complete === false ? 'error-text' : undefined);
|
|
518
|
+
row('Tests:', TESTS.length + OUTCOMES.filter(function (o) { return counts[o]; }).map(function (o) { return ', ' + counts[o] + ' ' + o; }).join(''));
|
|
519
|
+
row('Busy time:', fmt(busy) + (avail > 0 ? ' of ' + fmt(avail) + ' available (' + Math.round(busy / avail * 100) + '% utilisation)' : ''));
|
|
520
|
+
if (longest && laneIds.length > 1) row('Critical lane:', esc(longest.id) + ', last test finished at ' + fmt(longest.last));
|
|
521
|
+
var collect = WORKERS.filter(function (w) { return w.ready != null && w.collected != null; });
|
|
522
|
+
if (collect.length) {
|
|
523
|
+
var cmax = maxOf(collect, function (w) { return w.collected - w.ready; });
|
|
524
|
+
var rmax = maxOf(collect.filter(function (w) { return w.start != null; }), function (w) { return w.ready - w.start; }, 0);
|
|
525
|
+
row('Worker startup:', 'boot up to ' + fmt(rmax) + ', collection up to ' + fmt(cmax));
|
|
526
|
+
}
|
|
527
|
+
var errs = WORKERS.filter(function (w) { return w.error; });
|
|
528
|
+
if (errs.length) row('Errors:', errs.map(function (w) { return esc(w.id) + ': ' + esc(w.error); }).join('<br>'), 'error-text');
|
|
529
|
+
document.getElementById('summary').innerHTML = rows.join('');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ---- tooltip / pin ----------------------------------------------------------------
|
|
533
|
+
var tip = document.getElementById('tooltip');
|
|
534
|
+
function phaseRows(t) {
|
|
535
|
+
return '<table>' + PHASES.map(function (p) { var ph = t.ph[p]; return ph ? '<tr><td>' + p + '</td><td>' + fmt(ph.dur) + pct(ph.dur, t.dur) + '</td></tr>' : ''; }).join('') + '</table>';
|
|
536
|
+
}
|
|
537
|
+
function tipForTest(t) {
|
|
538
|
+
return '<b>' + esc(t.nodeid) + '</b><br>' + esc(t.outcome) + ' on ' + esc(t.worker) + (t.attempt ? ' (attempt ' + (t.attempt + 1) + ')' : '') +
|
|
539
|
+
'<br>total ' + fmt(t.dur) + ', started at ' + fmt(t.start) + phaseRows(t);
|
|
540
|
+
}
|
|
541
|
+
function tipForCluster(c) {
|
|
542
|
+
var top = c.tests.slice().sort(function (a, b) { return b.dur - a.dur; }).slice(0, 10);
|
|
543
|
+
var total = 0; c.tests.forEach(function (t) { total += t.dur; });
|
|
544
|
+
return '<b>' + c.tests.length + ' tiny tests</b> merged, ' + fmt(c.start) + ' → ' + fmt(c.stop) + ', busy ' + fmt(total) + '<br><span class="muted">zoom in to separate them</span><table>' +
|
|
545
|
+
top.map(function (t) { return '<tr><td>' + fmt(t.dur) + '</td><td class="' + (BAD[t.outcome] ? 'error-text' : '') + '">' + esc(t.nodeid) + '</td></tr>'; }).join('') +
|
|
546
|
+
(c.tests.length > 10 ? '<tr><td colspan="2" class="muted">… and ' + (c.tests.length - 10) + ' more</td></tr>' : '') + '</table>';
|
|
547
|
+
}
|
|
548
|
+
function barTarget(ev) { var el = ev.target; while (el && el !== document.body && !(el.classList && el.classList.contains('bar'))) el = el.parentNode; return el && el !== document.body ? el : null; }
|
|
549
|
+
var pc = document.getElementById('pipeline-container');
|
|
550
|
+
pc.addEventListener('mousemove', function (ev) {
|
|
551
|
+
var g = barTarget(ev);
|
|
552
|
+
if (!g) { tip.style.display = 'none'; return; }
|
|
553
|
+
tip.innerHTML = g.dataset.c != null ? tipForCluster(clusters[+g.dataset.c]) : tipForTest(TESTS[+g.dataset.i]);
|
|
554
|
+
tip.style.display = 'block';
|
|
555
|
+
var x = ev.clientX + 14, y = ev.clientY + 14;
|
|
556
|
+
if (x + tip.offsetWidth > window.innerWidth - 8) x = Math.max(8, ev.clientX - tip.offsetWidth - 14);
|
|
557
|
+
if (y + tip.offsetHeight > window.innerHeight - 8) y = Math.max(8, ev.clientY - tip.offsetHeight - 14);
|
|
558
|
+
tip.style.left = x + 'px'; tip.style.top = y + 'px';
|
|
559
|
+
});
|
|
560
|
+
pc.addEventListener('mouseleave', function () { tip.style.display = 'none'; });
|
|
561
|
+
pc.addEventListener('click', function (ev) {
|
|
562
|
+
var g = barTarget(ev);
|
|
563
|
+
if (!g || g.dataset.i == null) { state.pinned = null; applyPin(); return; }
|
|
564
|
+
var m = +g.dataset.m;
|
|
565
|
+
state.pinned = state.pinned === m ? null : m;
|
|
566
|
+
applyPin();
|
|
567
|
+
});
|
|
568
|
+
function applyPin() {
|
|
569
|
+
var bars = pc.querySelectorAll('.bar');
|
|
570
|
+
for (var i = 0; i < bars.length; i++) {
|
|
571
|
+
var g = bars[i];
|
|
572
|
+
g.classList.remove('dim'); g.classList.remove('hl');
|
|
573
|
+
if (state.pinned == null) continue;
|
|
574
|
+
if (g.dataset.m != null && +g.dataset.m === state.pinned) g.classList.add('hl'); else g.classList.add('dim');
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ---- controls --------------------------------------------------------------------
|
|
579
|
+
var minInput = document.getElementById('min-unit-time'), minOut = document.getElementById('min-unit-time-output');
|
|
580
|
+
minInput.max = Math.max(0.05, Math.ceil(maxDur * 20) / 20);
|
|
581
|
+
minInput.step = Math.max(0.001, minInput.max / 200);
|
|
582
|
+
var minTimer = null;
|
|
583
|
+
minInput.addEventListener('input', function () {
|
|
584
|
+
state.minTime = +minInput.value; minOut.textContent = fmt(state.minTime);
|
|
585
|
+
clearTimeout(minTimer); minTimer = setTimeout(function () { renderPipeline(); renderTable(); }, 40);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
var scaleInput = document.getElementById('scale'), scaleOut = document.getElementById('scale-output');
|
|
589
|
+
var fitPx = Math.max(20, Math.min(4096, (document.body.clientWidth || 1200) - 160)) / WALL;
|
|
590
|
+
var scaleMin = Math.max(fitPx / 4, 0.001), scaleMax = Math.max(fitPx * 200, 2000);
|
|
591
|
+
scaleInput.min = 0; scaleInput.max = 1000; scaleInput.step = 1;
|
|
592
|
+
function scaleFromSlider(v) { return scaleMin * Math.pow(scaleMax / scaleMin, v / 1000); }
|
|
593
|
+
function sliderFromScale(s) { return Math.round(1000 * Math.log(s / scaleMin) / Math.log(scaleMax / scaleMin)); }
|
|
594
|
+
state.pxPerSec = fitPx;
|
|
595
|
+
scaleInput.value = sliderFromScale(fitPx);
|
|
596
|
+
function showScale() { scaleOut.textContent = (state.pxPerSec >= 10 ? state.pxPerSec.toFixed(0) : state.pxPerSec.toFixed(2)) + ' px/s'; }
|
|
597
|
+
showScale();
|
|
598
|
+
var scaleTimer = null;
|
|
599
|
+
scaleInput.addEventListener('input', function () {
|
|
600
|
+
state.pxPerSec = scaleFromSlider(+scaleInput.value); showScale();
|
|
601
|
+
clearTimeout(scaleTimer); scaleTimer = setTimeout(function () { renderPipeline(); renderConcurrency(); }, 40);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
document.getElementById('view-lane').addEventListener('click', function () { setView('lane'); });
|
|
605
|
+
document.getElementById('view-unit').addEventListener('click', function () { setView('unit'); });
|
|
606
|
+
function setView(v) {
|
|
607
|
+
state.view = v;
|
|
608
|
+
document.getElementById('view-lane').classList.toggle('on', v === 'lane');
|
|
609
|
+
document.getElementById('view-unit').classList.toggle('on', v === 'unit');
|
|
610
|
+
renderPipeline();
|
|
611
|
+
}
|
|
612
|
+
var topInput = document.getElementById('top-n');
|
|
613
|
+
topInput.addEventListener('change', function () { state.topN = Math.max(1, +topInput.value || 50); if (state.view === 'unit') renderPipeline(); });
|
|
614
|
+
|
|
615
|
+
var of = document.getElementById('outcome-filters'), present = {};
|
|
616
|
+
TESTS.forEach(function (t) { present[t.outcome] = 1; });
|
|
617
|
+
OUTCOMES.filter(function (o) { return present[o]; }).forEach(function (o) {
|
|
618
|
+
var l = document.createElement('label');
|
|
619
|
+
var cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = true;
|
|
620
|
+
cb.addEventListener('change', function () { state.outcomes[o] = cb.checked; renderPipeline(); renderTable(); });
|
|
621
|
+
l.appendChild(cb); l.appendChild(document.createTextNode(o));
|
|
622
|
+
of.appendChild(l);
|
|
623
|
+
});
|
|
624
|
+
var textInput = document.getElementById('text-filter'), textTimer = null;
|
|
625
|
+
textInput.addEventListener('input', function () {
|
|
626
|
+
clearTimeout(textTimer); textTimer = setTimeout(function () { state.text = textInput.value.trim(); renderPipeline(); renderTable(); }, 120);
|
|
627
|
+
});
|
|
628
|
+
document.querySelector('#tests-table thead').addEventListener('click', function (ev) {
|
|
629
|
+
var th = ev.target.closest ? ev.target.closest('th') : null;
|
|
630
|
+
if (!th || !th.dataset.key) return;
|
|
631
|
+
if (state.sortKey === th.dataset.key) state.sortDir = -state.sortDir;
|
|
632
|
+
else { state.sortKey = th.dataset.key; state.sortDir = (th.dataset.key === 'nodeid' || th.dataset.key === 'worker' || th.dataset.key === 'outcome' || th.dataset.key === 'idx' || th.dataset.key === 'start') ? 1 : -1; }
|
|
633
|
+
renderTable();
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// ---- go --------------------------------------------------------------------------
|
|
637
|
+
renderSummary();
|
|
638
|
+
renderPipeline();
|
|
639
|
+
renderConcurrency();
|
|
640
|
+
renderTable();
|
|
641
|
+
} catch (e) {
|
|
642
|
+
// Surface failures in the document so automated checks can see them.
|
|
643
|
+
var pre = document.createElement('pre');
|
|
644
|
+
pre.id = 'render-error';
|
|
645
|
+
pre.className = 'error-text';
|
|
646
|
+
pre.textContent = 'pytest-timing report failed to render: ' + ((e && e.stack) || e);
|
|
647
|
+
document.body.appendChild(pre);
|
|
648
|
+
throw e;
|
|
649
|
+
}
|
|
650
|
+
})();
|
|
651
|
+
</script>
|
|
652
|
+
</body>
|
|
653
|
+
</html>
|