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:
co-authored by
Claude Opus 5.5
parent
1cb9fd35d2
commit
88ea3e86fd
+44
-1
@@ -338,7 +338,50 @@ The lab's injected keyboard does not reach its nested session, so the
|
|||||||
check delivers keys through Qt's own key path to the focused page; a
|
check delivers keys through Qt's own key path to the focused page; a
|
||||||
physical Ctrl+Plus on a classroom keyboard is a hardware check.
|
physical Ctrl+Plus on a classroom keyboard is a hardware check.
|
||||||
|
|
||||||
## Turning it off
|
## Why lessons load slowly
|
||||||
|
|
||||||
|
Measured before anything was changed, and nothing about loading was
|
||||||
|
changed, because the evidence does not point at Wonder Ink.
|
||||||
|
|
||||||
|
In the lab, on the same connection, the same page, timed by each
|
||||||
|
engine's own Navigation Timing until the sign-in form exists (the site
|
||||||
|
is a 3.7 KB shell that builds itself in JavaScript, so its load event is
|
||||||
|
not the moment a teacher can use it):
|
||||||
|
|
||||||
|
| Until the sign-in page is usable | Wonder Ink | Chrome |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| cold cache, 3 runs | 2.86 – 3.00 s | 2.79 – 3.48 s |
|
||||||
|
| warm cache, 3 runs | 2.20 – 2.49 s | 1.96 – 2.15 s |
|
||||||
|
| window on screen after launch | 0.33 s | 0.42 s |
|
||||||
|
|
||||||
|
* **The site is the weight:** 119 – 160 requests and 3.2 MB for a sign-in
|
||||||
|
form, about 430 KB warm. Its console says it loads the Google Maps
|
||||||
|
JavaScript API *twice*, synchronously. That is Wonder Ink's to fix.
|
||||||
|
* Wonder Ink is about a quarter of a second slower warm: it revalidates
|
||||||
|
the page shell (62 – 87 ms to first byte) where Chrome serves it from
|
||||||
|
its cache. Not the "very long time" teachers describe.
|
||||||
|
* Memory: about 610 MB for one tab and 115 MB more for each further tab —
|
||||||
|
eight tabs, 1.4 GB — one renderer each. Idle CPU about 1%: nothing
|
||||||
|
spins in the background.
|
||||||
|
* The network: from the lab, 0.14 – 0.22 s to first byte; from the
|
||||||
|
development host, 0.42 – 0.44 s. Cloudflare in front, uncached.
|
||||||
|
|
||||||
|
What the lab **cannot** show is the machines: it draws in software for
|
||||||
|
both engines. A classroom whose graphics fail in QtWebEngine but work in
|
||||||
|
Chrome would look exactly like the complaint. So there is a command for a
|
||||||
|
technician, run from the maintenance terminal inside the teacher's
|
||||||
|
session (Meta+Ctrl+Alt+Shift+K):
|
||||||
|
|
||||||
|
classroom-wonderink --diagnose
|
||||||
|
|
||||||
|
It prints the engine's versions, the scale, **what WebGL reports as the
|
||||||
|
renderer** — a real driver's name, or "none … drawing in software" — and
|
||||||
|
times the sign-in page from a cold cache in a throwaway profile, beside a
|
||||||
|
running Wonder Ink without touching it. Compare its time with the lab's
|
||||||
|
2.9 s, and its renderer with Chrome's on the same machine (open
|
||||||
|
`chrome://gpu` there).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
One future device is a laptop that will not have Wonder Ink at all.
|
One future device is a laptop that will not have Wonder Ink at all.
|
||||||
|
|
||||||
|
|||||||
@@ -1693,9 +1693,120 @@ def build_profile(app, name="wonderink"):
|
|||||||
return profile
|
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():
|
def main():
|
||||||
config = read_device_config()
|
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
|
# 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
|
# 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.
|
# technician who ran the command by hand and wants to know why.
|
||||||
|
|||||||
@@ -313,6 +313,13 @@ check("Wonder Ink is not told twice",
|
|||||||
# Its PAGES are, because Chrome under Plasma turned out to be scaled twice
|
# Its PAGES are, because Chrome under Plasma turned out to be scaled twice
|
||||||
# (dpr 2.25 at 150%, measured) and a lesson has to be the size teachers
|
# (dpr 2.25 at 150%, measured) and a lesson has to be the size teachers
|
||||||
# read it at in the browser. The window itself is left to Qt.
|
# read it at in the browser. The window itself is left to Qt.
|
||||||
|
# A technician's command, never a teacher's: it runs in a throwaway
|
||||||
|
# profile that something owns (an unowned one crashed it), and it never
|
||||||
|
# touches the signed-in profile a teacher may have open beside it.
|
||||||
|
diag = wonderink_code[wonderink_code.index("def diagnose("):wonderink_code.index("def main(")]
|
||||||
|
check("--diagnose uses an owned, throwaway profile",
|
||||||
|
"QWebEngineProfile(app)" in diag and "build_profile" not in diag
|
||||||
|
and '"--diagnose" in sys.argv' in wonderink_code)
|
||||||
check("its pages start at the room's scale, which is the browser's size",
|
check("its pages start at the room's scale, which is the browser's size",
|
||||||
"room_zoom=ui_scale(config)" in wonderink_code
|
"room_zoom=ui_scale(config)" in wonderink_code
|
||||||
and "self._room_zoom * self._level" in wonderink_code)
|
and "self._room_zoom * self._level" in wonderink_code)
|
||||||
|
|||||||
Reference in New Issue
Block a user