Files
onefinity-firmware/src/js/restart-timing.js
Henrik Muehe 561d2fd7ea Restart timing: bbctrl.Trace, /api/diag/timing, UI marks
Add a lightweight, self-contained phase tracer for measuring end-to-end
bbctrl restart and Pi boot time. Disabled by setting BBCTRL_TRACE=0.

- src/py/bbctrl/Trace.py: monotonic-anchored event log + sd_notify helper.
- bbctrl/__init__.py: marks for imports, args parsed, ioloop, web init,
  listen, and an sd_notify READY=1 once HTTP is bound.
- bbctrl/Ctrl.py: spans around each subsystem (avr, i2c, lcd, mach,
  preplanner, jog, pwr, hooks, aux, mach.connect).
- bbctrl/Comm.py: avr.firmware_rebooted mark.
- bbctrl/Web.py: TimingHandler (GET /api/diag/timing) and
  UITimingHandler (PUT /api/diag/timing/ui), plus a ws.first_open mark.
- src/js/restart-timing.js + app.js: UI-side performance.now() marks
  (script.load, ws.open, ws.first_msg, ui.first_state, window.load),
  posted once to the controller.
- scripts/bbctrl.service: stdout/stderr -> journal so TRACE lines are
  visible via journalctl -u bbctrl. (Was StandardOutput=null.)

Revert: git revert this commit. To disable at runtime without
reverting, set BBCTRL_TRACE=0 in the bbctrl service environment.
2026-05-01 09:48:10 +02:00

81 lines
2.5 KiB
JavaScript

// Lightweight UI-side restart/cold-load timing.
//
// Records a few key marks using performance.now(), then POSTs them to
// /api/diag/timing/ui once 'ui.first_state' has fired. Disabled by
// setting window.BBCTRL_TRACE = false before this module is loaded.
//
// Marks collected:
// script.load -- this module evaluated
// ws.open -- websocket onopen
// ws.first_msg -- first message from controller
// ui.first_state -- first message that contained controller state
// window.load -- window 'load' event
//
// Aligning these with /api/diag/timing on the server gives the full
// picture from systemd start -> bbctrl up -> WS open -> UI rendered.
"use strict";
const _enabled = typeof window !== "undefined" && window.BBCTRL_TRACE !== false;
const _t0 = (typeof performance !== "undefined" && performance.now)
? performance.now()
: Date.now();
const _navStart = (typeof performance !== "undefined" && performance.timeOrigin)
? performance.timeOrigin
: Date.now();
const marks = [];
let posted = false;
function _now() {
return (typeof performance !== "undefined" && performance.now)
? performance.now() - _t0
: Date.now() - _t0;
}
function mark(name, fields) {
if (!_enabled) return;
marks.push(Object.assign({ n: name, t: Math.round(_now()) }, fields || {}));
}
function _post() {
if (!_enabled || posted) return;
posted = true;
const body = JSON.stringify({
navStart: _navStart,
t0_perf: _t0,
href: typeof location !== "undefined" ? location.href : "",
ua: typeof navigator !== "undefined" ? navigator.userAgent : "",
marks: marks,
});
try {
if (typeof fetch === "function") {
fetch("/api/diag/timing/ui", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: body,
keepalive: true,
}).catch(() => {});
}
} catch (e) { /* swallow */ }
}
// Record window load too; doesn't block posting.
if (_enabled && typeof window !== "undefined") {
window.addEventListener("load", () => mark("window.load"));
}
mark("script.load");
module.exports = {
enabled: _enabled,
mark: mark,
onWsOpen: () => mark("ws.open"),
onWsFirstMessage: () => mark("ws.first_msg"),
onFirstState: () => {
mark("ui.first_state");
// Defer slightly so any synchronous render finishes first.
setTimeout(_post, 100);
},
flush: _post,
};