The black screen after the splash, and the seconds hiding behind it
The ~22-second black screen after the boot splash was a dead two-second settle wait plus a synchronous package install sitting on the browser's start-up path; removing both took app-ready from 43.1s to 37.4s, and new timeline marks were added to find what is left.
Status
Built, tested off the hardware, synced to the first stick, 1 October 2026. Awaiting a bench boot. Ships by stick sync — no layer rebuild.
The problem
After the boot splash the Dell showed a dark, blurry, then black screen for a long time before the audit app appeared. The operator did not know what it was for and asked for it gone, and for the boot to be faster.
What the boot report already said
The station writes boot-report.txt to the stick. On the Dell (Latitude 3310,
three boots, all within a second of each other), measured from the kernel
starting — firmware and the boot loader add another ~15.5s before that:
| s | event |
|---|---|
| 0–21 | Linux boots, the splash shows |
| 21.3 | display manager starts — the splash ends |
| 25.7 | the kiosk session's autostart begins |
| 27.8 | backend starting |
| 29.1 | "browser launched" |
| 43.1 | app ready — the page is served |
So the screen in question was the ~22 seconds between 21s and 43s. The
layer was checked: autologin selects the station-kiosk session
(/usr/local/bin/station-session), so no GNOME desktop is involved. The session
paints the root window plain black (xsetroot -solid black) and starts the
app; the blurry frame before that is the display server holding the splash's
last frame while it starts.
What was found in that gap
- A dead 2-second wait.
station-autostart.shwaits for "the desktop to settle" (ALS_SETTLE=2from the session). The kiosk session has no desktop. - A package install on the browser's path. In kiosk mode the script ran
install_evtx_reader—dpkg -iof three.debs from the stick, plus theldconfigtrigger (~8s per run on this image, per earlier measurement) — synchronously, before starting the browser, for a tool only an audit needs minutes later. And the report's "browser launched" mark is logged before that install, so its cost was hiding inside the browser's apparent 14-second startup. - Black, by design, for the rest. Nothing on screen said "still starting".
What was built
gui/boot-backdrop.py+gui/boot-backdrop.png. Paints the same "Starting Xorkbench" artwork the splash showed onto the X root window the moment the kiosk session's autostart begins. Root window = behind everything: the browser covers it when it appears; X keeps the image after the script exits (RetainPermanent), so nothing stays running. Uses only what the image already has —python3-gi+ GdkPixbuf (decode and scale in C) and libX11 via ctypes, the wayfullscreen-x.pydoes; confirmed present in the casper manifests (minimal.manifest). Same cover-with-safe-area layout as the splash. Every failure path (no X, no gi, no image, non-24/32-bit or big-endian visual) does nothing and leaves the screen black as before.- No settle wait in the kiosk session — recognised by GDM's session name
(
station-kiosk) or by there being nognome-shell. - The event-log reader installs in the background, after the browser has started, in both session kinds; its log line now carries how long it took.
- Three new boot-report timeline marks —
boot artwork painted,browser process started(the real start, not the preparation), andevent-log reader ready— so the next report shows exactly where the remaining seconds go. sync-usb.ps1carries the two new files.
How it was proved
- The backdrop ran against a real X server (Xvfb, the same packages the stick has) at 1366×768, 1920×1080, 1024×768, 1280×1024 and 3440×1440: correct colours, full screen, safe area intact, 155–285 ms including Python start-up, root window captured after the script had exited. No display, missing image and a 16-bit screen all exit 0 and paint nothing.
test-boot-report.py68/68 (9 new: the kiosk timeline order — artwork before the backend, browser before the install finishes — and the autostart's order asserted from the script itself);test-evtx-reader.sh23/23 (still exactly three references to the install);test-layer-autostart.sh39/39; all 31 Python tool tests pass.
Not provable off the hardware: the actual seconds saved. Expected — 2s from the settle, plus whatever the package install was costing (likely several seconds); the next boot report will say.
Result on the Dell, and the next measurement
The first boot after the change (boot report, Latitude 3310):
| before | after | |
|---|---|---|
| app ready (after the kernel) | 43.1s | 37.4s |
| autostart → backend starting | 2.1s | 0.1s |
| browser start → page served | ~14s (incl. the install) | 6.1s |
| boot artwork | — | painted at 30.0s, 0.8s after the autostart began |
So the browser itself takes ~6s, and the ~7–8s that used to look like browser start-up was the package install.
What is left uncovered is the stretch from the display manager starting to
the autostart running — 7.3s on this boot against 4.4s on the earlier
one, and the artwork cannot go up before the session exists. One sample each
cannot say whether that is variance or a cost. So the boot report now has a
section for exactly that window (dm_to_session in server.py): the journal
lines between the two points from the display manager, X, logind, the user
session and the kernel (network daemons deliberately left out — their lines
can carry the Wi-Fi name and the report goes to the stick), with the longest
pauses named first and what came just before and after each. Three timeline
marks were added for its milestones (X server starting, auto-login session
opened, user session manager started). test-boot-report.py 79/79. The next
Dell boot will say which step is slow.
Next, if the operator wants more
Further gains need the layer (a mksquashfs patch and a bench boot), from
the same report: services on or near the critical path that this appliance
does not use — systemd-udev-settle (3.5s), zfs-load-module (2.0s),
apport (3.8s), gnome-remote-desktop (3.0s), and
NetworkManager-wait-online (5.7s, not on the app's path but competing for
the CPU). Masking them is a candidate; it should follow this change's
measurements, not precede them.
A published copy. Commit references and internal identifiers have been removed and the operator is not named; the engineering, the counts and the stated limits are unchanged.