This decision record captures a performance comparison between ChatGPT Desktop and another Electron 42 app (Claude Desktop) running side by side on the same X11 GNOME 4K host, what the launcher changed as a result, and — just as importantly — what was reviewed and deliberately left alone so future work does not re-litigate it without new evidence.
Evidence came from live process command lines, /proc/<pid>/maps, the
launcher log, and repository history rather than synthetic benchmarks.
--disable-dev-shm-usageis now passed only when/dev/shmis missing, not writable, or smaller than 1 GiB. The flag exists for containers with a tiny/dev/shm(Docker defaults to 64 MiB); everywhere else it pushed Chromium's renderer/GPU shared-memory buffers into disk-backed temp storage (observable as/tmp/.org.chromium.Chromium.*mappings in every process). Override:CODEX_ELECTRON_DISABLE_DEV_SHM_USAGE=auto|0|1.--force-renderer-accessibilityis now added only when an assistive technology is detected: Orca or brltty running, the GNOME screen-reader setting, the AT-SPI state thatcodex-computer-use-linux setupenables (org.a11y.Status IsEnabledvia busctl, or itsorg.gnome.desktop.interface toolkit-accessibilitygsettings fallback), or accessibility env markers. Keeping the accessibility engine on in every renderer makes each DOM update also rebuild and serialize the accessibility tree; the WSLg and wayland-gpu profiles already skipped the flag for that reason. Session-bus probes (gsettings/busctl) run under the launcher's ppid-guarded watchdog pattern capped at 0.5 s, so a broken session bus counts as "not detected" instead of delaying launch. Override:CODEX_FORCE_RENDERER_ACCESSIBILITY=1|0.
Both decisions are visible at runtime in the Electron launch mode: line of
~/.cache/codex-desktop/launcher.log (dev_shm_usage_disabled=,
renderer_accessibility_forced=).
These are security-posture flags, not measurable rendering-performance
factors. Removing them is a separate compatibility project: the Electron
SUID/user-namespace sandbox behaves differently across distributions and
container/AppImage environments, and the troubleshooting docs currently
promise --no-sandbox behavior. Out of scope for performance work.
On Wayland sessions the launcher intentionally trades compositing performance
for side-panel rendering stability. That is a documented workaround with an
explicit opt-out (CODEX_ELECTRON_DISABLE_GPU_COMPOSITING=0); do not remove
it for performance reasons without re-testing the side-panel flicker it
papers over.
The bundled Python server already uses ThreadingHTTPServer and serves
webview files with explicit no-store headers. The Linux packaging flow
patches upstream webview assets without renaming every hashed chunk, so the
server must force revalidation to keep Electron from reusing stale renderer
code after rebuilds or updates. Replacing it with a Rust server was evaluated
and rejected until evidence shows Python itself is the bottleneck — see
Webview server evaluation.
The invariant is unchanged: Electron never launches before the webview origin is verified, so Chromium cannot race a server that has not bound yet. Within that constraint the cold-start path now overlaps independent work instead of serializing it:
- The Python webview server is spawned first
(
start_webview_server), and its readiness wait plus origin verification run later (await_webview_server_ready), after the CLI lookup and plugin cache syncs — the server finishes booting while the launcher does unrelated work, leaving ~35 ms of residual wait instead of ~150 ms. - The five bundled plugin cache syncs run concurrently and are all
awaited before cold-start hooks dispatch (~110 ms instead of ~285 ms).
They only touch disjoint per-plugin cache directories; the shared
bundled marketplace metadata they previously each rewrote is staged
exactly once beforehand (
stage_bundled_marketplace_metadata), which is also what makes the concurrency safe.
Launching Electron itself in parallel with any of this remains rejected: the renderer needs a verified local origin, and the warm-start handoff markers depend on the current ordering.
Most of the visible loading-screen time is spent inside the upstream app:
the renderer blocks on codex app-server RPCs after the static assets load
(the launcher log shows individual calls such as app/list taking multiple
seconds on cold start). That is upstream application behavior inside
app.asar, not Linux adaptation glue, and is out of scope for this
repository beyond faithfully reporting it.
Launcher CLI preflight is best-effort, recently hardened, and not part of the
rendering path. One launch-path cost hid there, though: the CLI version log
line reads codex_cli_version through command substitution, and the probe's
watchdog subshell inherited that pipe. Its sleep 1 child survived the
watchdog kill and held the pipe open, so every cold launch stalled for the
full watchdog second even when the CLI answered in ~50 ms. The watchdog now
runs with stdout/stderr detached (>/dev/null 2>&1), which cut the
launch_state_refreshed_under_lock → electron_launch gap from ~1010 ms to
~74 ms. When adding bounded probes, keep watchdog subshells detached from any
caller pipe — an orphaned sleep holding an inherited fd blocks command
substitution until it exits.
The default update/version preflight remains asynchronous. Before any version
probe, the generated launcher only resolves the selected CLI path to a
canonical executable file. Missing, broken, or non-executable paths still block
startup with a direct reinstall or CODEX_CLI_PATH hint, but normal Linux
layouts such as group-writable user directories, symlinked home directories,
Linuxbrew, and standalone current symlinks are not rejected by a separate
trust policy. CODEX_SYNC_CLI_PREFLIGHT=1 still opts into the full synchronous
update/version check while preserving fail-soft behavior for a CLI that is not
known broken. A detected npm-managed CLI missing
@openai/codex-linux-x64 or @openai/codex-linux-arm64 is the blocking
exception: Electron cannot use that CLI, so the launcher waits for one repair
attempt. The updater's initial and post-repair CLI version probes are each
bounded to 5 seconds, the repair to 90 seconds, and the follow-up npm registry
lookup to 20 seconds. Each command runs in a dedicated process group that is
terminated on timeout, including child processes; failure then stops startup
with manual reinstall instructions instead of leaving the loading screen stuck.