feat: measure why lessons load slowly, and give a technician the same tool

Measured before changing anything, and nothing about loading changed,
because the evidence does not point at Wonder Ink. Same lab, same page,
timed by each engine until the sign-in form exists: Wonder Ink 2.86-3.00 s
cold and 2.20-2.49 s warm, Chrome 2.79-3.48 s and 1.96-2.15 s. The site is
the weight — 119-160 requests and 3.2 MB for a sign-in form, and its
console reports loading the Google Maps API twice. Memory about 610 MB
for one tab, 115 MB per further tab; idle CPU about 1%.

The lab draws in software for both engines, so it cannot show the one
likely cause it has no hardware for: QtWebEngine failing to use a
classroom's GPU where Chrome succeeds. classroom-wonderink --diagnose,
run from the maintenance terminal in the teacher's session, prints the
engine versions, the scale, the WebGL renderer (a driver, or software),
and the sign-in page's time from a cold cache, in a throwaway profile
beside any running Wonder Ink.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-23 19:23:20 -05:00
co-authored by Claude Opus 5.5
parent 1cb9fd35d2
commit 88ea3e86fd
3 changed files with 162 additions and 1 deletions
+111
View File
@@ -1693,9 +1693,120 @@ def build_profile(app, name="wonderink"):
return profile
# What --diagnose asks a page. The renderer name WebGL reports is the
# plainest answer to "is this engine drawing with the GPU": a real driver's
# name, or llvmpipe / SwiftShader / nothing when it is drawing in software.
# chrome://gpu would say more and draws nothing in this engine build.
DIAGNOSE_GPU = """(function(){
var g = document.createElement('canvas').getContext('webgl');
if (!g) return 'none: no WebGL, so the engine is drawing in software';
var e = g.getExtension('WEBGL_debug_renderer_info');
return e ? g.getParameter(e.UNMASKED_RENDERER_WEBGL) : g.getParameter(g.RENDERER);
})()"""
DIAGNOSE_LOAD = """(function(){
var n = performance.getEntriesByType('navigation')[0] || {};
var r = performance.getEntriesByType('resource'), bytes = n.transferSize || 0;
r.forEach(function(x){ bytes += x.transferSize || 0; });
return JSON.stringify({ready: !!document.querySelector('input[type=email], input[type=password]'),
first_byte: Math.round(n.responseStart || 0), requests: r.length + 1,
kilobytes: Math.round(bytes / 1024)});
})()"""
def diagnose(config):
"""For a technician at a classroom: why is a lesson slow here?
Run from the maintenance terminal, inside the teacher's session, so
that it sees that session's display and graphics. Opens nothing a
teacher could use, in a throwaway profile, so it can run beside a
signed-in Wonder Ink without touching it, and times the same thing
tools/lab measured: the sign-in page from a cold cache.
"""
import json
import time
from PyQt6.QtCore import PYQT_VERSION_STR, QT_VERSION_STR
from PyQt6.QtWebEngineCore import qWebEngineChromiumVersion, qWebEngineVersion
started = time.monotonic()
app = QApplication(sys.argv[:1])
# Off the record, and OWNED: a profile nobody holds is deleted at
# once while its page still uses it — measured, a segfault, the same
# shape of mistake as the 0.2.2 printing crash.
profile = QWebEngineProfile(app)
view = QWebEngineView()
page = QWebEnginePage(profile, view)
view.setPage(page)
view.resize(1280, 800)
view.show()
room = ui_scale(config)
print("Wonder Ink diagnostics")
print(f" Qt {QT_VERSION_STR}, PyQt {PYQT_VERSION_STR}, QtWebEngine "
f"{qWebEngineVersion()} (Chromium {qWebEngineChromiumVersion()})")
print(f" platform {QGuiApplication.platformName()}, window scale "
f"{view.devicePixelRatioF():g}, UI_SCALE {room:g}, lessons open at "
f"{room * load_level():g}x zoom")
result = {}
def finish(code):
result["code"] = code
app.quit()
def gpu(renderer):
print(f" GPU (WebGL renderer): {renderer}")
view.setZoomFactor(1.0)
view.load(QUrl(HOME_PAGE))
poll()
def poll():
if time.monotonic() - started > 60:
print(" sign-in page: NOT READY after 60 s")
return finish(1)
view.page().runJavaScript(DIAGNOSE_LOAD, measured)
def measured(answer):
found = json.loads(answer) if answer else {}
if not found.get("ready"):
return QTimer.singleShot(100, poll)
print(f" sign-in page usable after {time.monotonic() - loading[0]:.2f} s, "
f"first byte {found['first_byte']} ms, {found['requests']} requests, "
f"{found['kilobytes']} KB")
print(" (the lab, on a wired connection: about 2.9 s cold, "
"130-160 requests, 3.2 MB)")
finish(0)
loading = [time.monotonic()]
view.loadStarted.connect(lambda: loading.__setitem__(0, time.monotonic()))
view.setHtml("<p>diagnostics</p>", QUrl("https://diagnose.invalid/"))
view.loadFinished.connect(
lambda ok: view.page().runJavaScript(DIAGNOSE_GPU, gpu)
if view.url().host() == "diagnose.invalid" else None
)
QTimer.singleShot(90_000, lambda: finish(1))
app.exec()
# The page goes before its profile, or Qt warns and may crash on the
# way out.
view.close()
page.deleteLater()
view.deleteLater()
QApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete.value)
sys.stdout.flush()
return result.get("code", 1)
def main():
config = read_device_config()
if "--diagnose" in sys.argv[1:]:
return diagnose(config)
# A device that has no Wonder Ink has no Wonder Ink. The panel does not
# offer it, so nothing should reach here; saying it out loud is for the
# technician who ran the command by hand and wants to know why.