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.
This commit is contained in:
2026-05-01 09:48:10 +02:00
parent 73c6a4f160
commit 561d2fd7ea
8 changed files with 328 additions and 14 deletions

View File

@@ -885,6 +885,31 @@ class RemoteDiagnosticsHandler(bbctrl.APIHandler):
'message': e.reason or "Unknown"
})
class TimingHandler(bbctrl.APIHandler):
"""Return the bbctrl process startup timeline as JSON.
Includes monotonic-anchored events from bbctrl.Trace, the wall
clock anchor (so the timeline can be aligned with journalctl /
systemd-analyze output), and the most recent UI-side timing
payload posted by the browser.
"""
def get(self):
import bbctrl.Trace as _T
self.write_json(_T.timeline())
class UITimingHandler(bbctrl.APIHandler):
"""Browser posts its performance.now() marks here once per load."""
def put_ok(self):
import bbctrl.Trace as _T
# self.json is parsed in APIHandler.prepare()
try:
_T.set_ui_timing(self.json)
_T.mark('ui.posted_timing',
marks=len(self.json.get('marks', []) or []))
except Exception: pass
# Base class for Web Socket connections
class ClientConnection(object):
def __init__(self, app):
@@ -960,6 +985,12 @@ class SockJSConnection(ClientConnection, sockjs.tornado.SockJSConnection):
ip = info.ip
if 'X-Real-IP' in info.headers: ip = info.headers['X-Real-IP']
self.app.get_ctrl(id).log.get('Web').info('Connection from %s' % ip)
try:
if not getattr(self.app, '_first_ws', False):
self.app._first_ws = True
import bbctrl.Trace as _T
_T.mark('ws.first_open', ip=ip)
except Exception: pass
super().on_open(id)
@@ -989,6 +1020,8 @@ class Web(tornado.web.Application):
handlers = [
(r'/websocket', WSConnection),
(r'/api/diag/timing', TimingHandler),
(r'/api/diag/timing/ui', UITimingHandler),
(r'/api/log', LogHandler),
(r'/api/message/(\d+)/ack', MessageAckHandler),
(r'/api/bugreport', BugReportHandler),