Skip to content

Commit 562f9ea

Browse files
authored
frontend: rendering optimizations (#144)
* internal/static: add npm run release script * internal/static: bump plotly to 3.3.0 * internal/static: raf-based throttling for plot updates * internal/static: downsample series with more than 500 pts * internal/static: improve slicing in RingBuffer * internal/static: cache width calculations Prevent forced layout reflows * internal/static: memoize GC vertical line shapes * internal/static: disable downsampling * Cosmetics * internal/static: regenerate assets
1 parent 0c433a8 commit 562f9ea

File tree

7 files changed

+93
-26
lines changed

7 files changed

+93
-26
lines changed

internal/static/dist.zip

12.9 KB
Binary file not shown.

internal/static/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/static/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66
"scripts": {
77
"dev": "vite",
88
"preview": "vite preview",
9-
"build": "vite build"
9+
"build": "vite build",
10+
"release": "bash -lc \"rm -rf ./dist && vite build && zip -r dist.zip dist/*\""
1011
},
1112
"devDependencies": {
1213
"vite": "^6.4.1"
1314
},
1415
"dependencies": {
1516
"bootstrap": "^5.3.5",
1617
"bootstrap-icons": "^1.12.1",
17-
"plotly.js-cartesian-dist": "^3.0.1",
18+
"plotly.js-cartesian-dist": "^3.3.0",
1819
"tippy.js": "^6.3.7"
1920
}
2021
}

internal/static/src/PlotManager.js

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ function debounce(fn, delay) {
1212
}
1313

1414
export default class PlotManager {
15+
#shapesCache;
16+
#lastGcEnabled;
17+
1518
constructor(config) {
1619
this.container = document.getElementById("plots");
1720
this.plots = config.series.map((pd) => new Plot(pd));
21+
this.#shapesCache = new Map();
22+
this.#lastGcEnabled = null;
1823
this.#attach();
1924

2025
window.addEventListener(
@@ -41,14 +46,39 @@ export default class PlotManager {
4146
}
4247

4348
update(data, gcEnabled, timeRange, force = false) {
44-
// Create GC vertical lines.
49+
// Create GC vertical lines - only if needed.
4550
const shapes = new Map();
4651
if (gcEnabled) {
52+
// Only recreate shapes if GC state changed or events changed
53+
const gcStateChanged = this.#lastGcEnabled !== gcEnabled;
54+
4755
for (const [name, serie] of data.events) {
48-
shapes.set(name, createVerticalLines(serie));
56+
// Check if we need to regenerate shapes for this event
57+
const cached = this.#shapesCache.get(name);
58+
const eventsChanged =
59+
!cached ||
60+
cached.length !== serie.length ||
61+
(serie.length > 0 &&
62+
cached[cached.length - 1]?.x0?.getTime() !==
63+
serie[serie.length - 1]?.getTime());
64+
65+
if (gcStateChanged || eventsChanged || !this.#shapesCache.has(name)) {
66+
const newShapes = createVerticalLines(serie);
67+
this.#shapesCache.set(name, newShapes);
68+
shapes.set(name, newShapes);
69+
} else {
70+
shapes.set(name, this.#shapesCache.get(name));
71+
}
72+
}
73+
} else {
74+
// GC disabled, clear all shapes
75+
if (this.#lastGcEnabled !== false) {
76+
this.#shapesCache.clear();
4977
}
5078
}
5179

80+
this.#lastGcEnabled = gcEnabled;
81+
5282
// X-axis range.
5383
const now = data.times[data.times.length - 1];
5484
const xrange = [now - timeRange * 1000, now];
@@ -63,11 +93,11 @@ export default class PlotManager {
6393
const gd = document.getElementById(p.name());
6494
// We're being super defensive here to ensure that the div is
6595
// actually there (or Plotly.resize would fail).
66-
if (!gd) return;
67-
if (!p.isVisible()) return;
96+
if (!gd || !p.isVisible()) return;
6897
const { offsetWidth: w, offsetHeight: h } = gd;
6998
if (w === 0 || h === 0) return;
7099

100+
p.updateCachedWidth();
71101
Plotly.Plots.resize(gd);
72102
});
73103
}

internal/static/src/RingBuffer.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ export default class RingBuffer {
2020

2121
slice(lastN) {
2222
const n = Math.min(lastN, this.#size);
23-
const result = [];
24-
for (let i = this.#size - n; i < this.#size; i++) {
25-
result.push(this.#buf[(this.#start + i) % this.#buf.length]);
23+
const result = new Float64Array(n);
24+
25+
const startIdx = this.#size - n;
26+
for (let i = 0; i < n; i++) {
27+
result[i] = this.#buf[(this.#start + startIdx + i) % this.#buf.length];
2628
}
27-
return result;
29+
30+
return Array.from(result);
2831
}
2932

3033
get first() {

internal/static/src/app.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,31 @@ import "bootstrap/dist/js/bootstrap.min.js";
88
export let statsMgr;
99
export let plotMgr;
1010

11+
// RAF-based throttling for plot updates
12+
let rafId = null;
13+
let pendingUpdate = false;
14+
let forceNextUpdate = false;
15+
16+
const scheduleUpdate = () => {
17+
if (rafId !== null) return; // Already scheduled
18+
19+
rafId = requestAnimationFrame(() => {
20+
rafId = null;
21+
if (pendingUpdate && running) {
22+
const data = statsMgr.slice(timerange);
23+
plotMgr.update(data, gcEnabled, timerange, forceNextUpdate);
24+
pendingUpdate = false;
25+
forceNextUpdate = false;
26+
}
27+
});
28+
};
29+
1130
export const drawPlots = (force) => {
12-
if (running) {
13-
const data = statsMgr.slice(timerange);
14-
plotMgr.update(data, gcEnabled, timerange, force);
31+
pendingUpdate = true;
32+
if (force) {
33+
forceNextUpdate = true;
1534
}
35+
scheduleUpdate();
1636
};
1737

1838
export const connect = () => {

internal/static/src/plot.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Plot {
2222
#maximized;
2323
#cfg;
2424
#dataTemplate;
25+
#cachedWidth;
2526

2627
constructor(cfg) {
2728
cfg.layout.paper_bgcolor = themeColors[theme.getThemeMode()].paper_bgcolor;
@@ -33,6 +34,7 @@ class Plot {
3334
this.#updateCount = 0;
3435
this.#dataTemplate = [];
3536
this.#lastData = [{ x: new Date() }];
37+
this.#cachedWidth = null;
3638

3739
if (this.#cfg.type == "heatmap") {
3840
this.#dataTemplate.push({
@@ -80,8 +82,8 @@ class Plot {
8082
this.#htmlElt = div;
8183

8284
// Measure the final CSS width.
83-
const initialWidth = div.clientWidth;
84-
this.#plotlyLayout.width = initialWidth;
85+
this.#cachedWidth = div.clientWidth;
86+
this.#plotlyLayout.width = this.#cachedWidth;
8587
this.#plotlyLayout.height = defaultPlotHeight;
8688

8789
// Pass a single data with no data to create an empty plot (this removes
@@ -154,6 +156,7 @@ class Plot {
154156

155157
#extractData(data) {
156158
const serie = data.series.get(this.#cfg.name);
159+
157160
if (this.#cfg.type == "heatmap") {
158161
this.#dataTemplate[0].x = data.times;
159162
this.#dataTemplate[0].z = serie;
@@ -163,6 +166,7 @@ class Plot {
163166
for (let i = 0; i < this.#dataTemplate.length; i++) {
164167
this.#dataTemplate[i].x = data.times;
165168
this.#dataTemplate[i].y = serie[i];
169+
166170
this.#dataTemplate[i].stackgroup = this.#cfg.subplots[i].stackgroup;
167171
this.#dataTemplate[i].hoveron = this.#cfg.subplots[i].hoveron;
168172
this.#dataTemplate[i].type =
@@ -198,11 +202,8 @@ class Plot {
198202
this.#plotlyConfig.responsive = false;
199203
}
200204

201-
// **Re‐measure** container width each time
202-
const newWidth = this.#maximized
203-
? plotsDiv.clientWidth
204-
: this.#htmlElt.clientWidth;
205-
this.#plotlyLayout.width = newWidth;
205+
// Use cached width - only recalculated on resize
206+
this.#plotlyLayout.width = this.#cachedWidth;
206207

207208
this.#react();
208209
}
@@ -215,21 +216,33 @@ class Plot {
215216
this.#plotlyLayout = newLayoutObject(this.#cfg, this.#maximized);
216217
this.#plotlyConfig = newConfigObject(this.#cfg, this.#maximized);
217218

218-
this.#plotlyLayout.width = plotsDiv.clientWidth;
219+
this.#cachedWidth = plotsDiv.clientWidth;
220+
this.#plotlyLayout.width = this.#cachedWidth;
219221
this.#plotlyLayout.height = plotsDiv.parentElement.clientHeight - 50;
220222

221223
this.#react();
222224
}
223225

224226
minimize() {
225-
(this.#maximized = false), this.#maximized;
227+
this.#maximized = false;
226228

227229
this.#plotlyLayout = newLayoutObject(this.#cfg, this.#maximized);
228230
this.#plotlyConfig = newConfigObject(this.#cfg, this.#maximized);
229231

232+
this.#cachedWidth = this.#htmlElt.clientWidth;
233+
this.#plotlyLayout.width = this.#cachedWidth;
234+
230235
this.#react();
231236
}
232237

238+
updateCachedWidth() {
239+
if (this.#maximized) {
240+
this.#cachedWidth = plotsDiv.clientWidth;
241+
} else {
242+
this.#cachedWidth = this.#htmlElt.clientWidth;
243+
}
244+
}
245+
233246
#react() {
234247
Plotly.react(
235248
this.#htmlElt,

0 commit comments

Comments
 (0)