History

Harden Electron startup waits in dev and CI

- add configurable `T3CODE_ELECTRON_STARTUP_TIMEOUT_MS` to desktop startup and CI perf jobs - replace simple TCP wait with explicit renderer host probes and clearer timeout errors - include Electron startup timeout env in Turborepo global env passthrough

Julius Marminge committed 9 months ago

Commit 30f8a391 parent d45fe793 files · +82 −8Browse files

Unified · Split

3 files
  1. .github/workflows/ci.yml +3 −0
  2. apps/desktop/scripts/dev-electron.mjs +77 −7
  3. turbo.json +2 −1
.github/workflows/ci.yml+3−0
@@ -79,6 +79,7 @@ jobs: · 78 unmodified lines
79 env:79 env:
80 T3CODE_PERF_TIMEOUT_MS: "420000"80 T3CODE_PERF_TIMEOUT_MS: "420000"
81 T3CODE_PERF_MAX_LONG_DISPATCH_COUNT: "5"81 T3CODE_PERF_MAX_LONG_DISPATCH_COUNT: "5"
82 T3CODE_ELECTRON_STARTUP_TIMEOUT_MS: "180000"
82 run: xvfb-run -a bun run test:desktop-perf83 run: xvfb-run -a bun run test:desktop-perf
83 84
84 - name: Desktop perf trace test (fork PR)85 - name: Desktop perf trace test (fork PR)
@@ -86,6 +87,7 @@ jobs: · 1 unmodified line
86 env:87 env:
87 T3CODE_PERF_TIMEOUT_MS: "420000"88 T3CODE_PERF_TIMEOUT_MS: "420000"
88 T3CODE_PERF_MAX_LONG_DISPATCH_COUNT: "5"89 T3CODE_PERF_MAX_LONG_DISPATCH_COUNT: "5"
90 T3CODE_ELECTRON_STARTUP_TIMEOUT_MS: "180000"
89 run: xvfb-run -a bun run test:desktop-perf91 run: xvfb-run -a bun run test:desktop-perf
90 92
91 - name: Desktop perf trace test and PR summary93 - name: Desktop perf trace test and PR summary
@@ -94,6 +96,7 @@ jobs: · 2 unmodified lines
94 GH_TOKEN: ${{ github.token }}96 GH_TOKEN: ${{ github.token }}
95 T3CODE_PERF_TIMEOUT_MS: "420000"97 T3CODE_PERF_TIMEOUT_MS: "420000"
96 T3CODE_PERF_MAX_LONG_DISPATCH_COUNT: "5"98 T3CODE_PERF_MAX_LONG_DISPATCH_COUNT: "5"
99 T3CODE_ELECTRON_STARTUP_TIMEOUT_MS: "180000"
97 run: xvfb-run -a bun run test:desktop-perf:post100 run: xvfb-run -a bun run test:desktop-perf:post
98 101
99 - name: Upload desktop perf artifacts102 - name: Upload desktop perf artifacts
apps/desktop/scripts/dev-electron.mjs+77−7
@@ -1,19 +1,84 @@
1import { spawn } from "node:child_process";1import { spawn } from "node:child_process";
2import net from "node:net";
2 3
3import waitOn from "wait-on";4import waitOn from "wait-on";
4 5
5const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5173);6const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5173);
6const devServerUrl = `http://localhost:${port}`;7const STARTUP_TIMEOUT_MS = Number(process.env.T3CODE_ELECTRON_STARTUP_TIMEOUT_MS ?? 120_000);
7 8
9async function canConnect(host, probePort, timeoutMs = 1_000) {
10 return new Promise((resolve) => {
11 const socket = new net.Socket();
12 let settled = false;
13
14 const finish = (value) => {
15 if (settled) return;
16 settled = true;
17 socket.destroy();
18 resolve(value);
19 };
20
21 socket.setTimeout(timeoutMs);
22 socket.once("connect", () => finish(true));
23 socket.once("timeout", () => finish(false));
24 socket.once("error", () => finish(false));
25 socket.connect(probePort, host);
26 });
27}
28
29function waitForDevServer(probePort, timeoutMs) {
30 const startedAt = Date.now();
31 const candidates = [
32 { host: "127.0.0.1", url: `http://127.0.0.1:${probePort}` },
33 { host: "::1", url: `http://[::1]:${probePort}` },
34 { host: "localhost", url: `http://localhost:${probePort}` },
35 ];
36
37 return new Promise((resolve, reject) => {
38 const tick = () => {
39 void Promise.all(
40 candidates.map(async (candidate) => ({
41 candidate,
42 connected: await canConnect(candidate.host, probePort),
43 })),
44 )
45 .then((results) => {
46 const ready = results.find((entry) => entry.connected);
47 if (ready) {
48 resolve(ready.candidate.url);
49 return;
50 }
51
52 if (Date.now() - startedAt >= timeoutMs) {
53 reject(
54 new Error(
55 `[dev-electron] timed out after ${timeoutMs}ms waiting for renderer dev server on port ${probePort}`,
56 ),
57 );
58 return;
59 }
60
61 setTimeout(tick, 250);
62 })
63 .catch((error) => {
64 reject(error);
65 });
66 };
67
68 tick();
69 });
70}
71
72console.log("[dev-electron] waiting for desktop/server bundles");
8await waitOn({73await waitOn({
9 resources: [74 resources: ["file:dist-electron/main.js", "file:dist-electron/preload.js", "file:../server/dist/index.mjs"],
10 `tcp:${port}`,75 timeout: STARTUP_TIMEOUT_MS,
11 "file:dist-electron/main.js",
12 "file:dist-electron/preload.js",
13 "file:../server/dist/index.mjs",
14 ],
15});76});
16 77
78console.log(`[dev-electron] waiting for renderer dev server on port ${port}`);
79const devServerUrl = await waitForDevServer(port, STARTUP_TIMEOUT_MS);
80console.log(`[dev-electron] launching electron with renderer url ${devServerUrl}`);
81
17const command = process.platform === "win32" ? "electronmon.cmd" : "electronmon";82const command = process.platform === "win32" ? "electronmon.cmd" : "electronmon";
18const childEnv = { ...process.env };83const childEnv = { ...process.env };
19delete childEnv.ELECTRON_RUN_AS_NODE;84delete childEnv.ELECTRON_RUN_AS_NODE;
@@ -29,3 +94,8 @@ const child = spawn(command, ["dist-electron/main.js"], { · 9 unmodified lines
29child.on("exit", (code) => {94child.on("exit", (code) => {
30 process.exit(code ?? 0);95 process.exit(code ?? 0);
31});96});
97
98child.on("error", (error) => {
99 console.error("[dev-electron] failed to launch electronmon:", error.message);
100 process.exit(1);
101});
turbo.json+2−1
@@ -15,7 +15,8 @@ · 14 unmodified lines
15 "T3CODE_DESKTOP_PERF_AUTOMATION",15 "T3CODE_DESKTOP_PERF_AUTOMATION",
16 "T3CODE_DESKTOP_PERF_TRACE_OUT",16 "T3CODE_DESKTOP_PERF_TRACE_OUT",
17 "T3CODE_DESKTOP_PERF_DONE_OUT",17 "T3CODE_DESKTOP_PERF_DONE_OUT",
18 "T3CODE_DESKTOP_PERF_SEED_PATH"18 "T3CODE_DESKTOP_PERF_SEED_PATH",
19 "T3CODE_ELECTRON_STARTUP_TIMEOUT_MS"
19 ],20 ],
20 "tasks": {21 "tasks": {
21 "build": {22 "build": {