Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
a1177c7
Add desktop perf trace automation and CI reporting
juliusmarminge Feb 16, 2026
8d90f14
Extract desktop perf automation into dedicated module
juliusmarminge Feb 16, 2026
25cc139
Handle fork PR desktop perf runs and harden trace shutdown
juliusmarminge Feb 16, 2026
41caab9
Track large-thread render timings in desktop perf trace
juliusmarminge Feb 16, 2026
419ecc0
Add desktop perf seed data and update perf trace workflow
juliusmarminge Feb 16, 2026
220497a
Expand desktop perf trace with terminal and render delta metrics
juliusmarminge Feb 16, 2026
ae08eb8
Pad perf trace summary columns for stable markdown tables
juliusmarminge Feb 16, 2026
758b3e1
Harden desktop perf CI with deps, artifacts, and timeout hints
juliusmarminge Feb 16, 2026
4622dbd
Raise desktop perf timeout and long-dispatch threshold in CI
juliusmarminge Feb 16, 2026
a293a01
Harden Electron startup waits in dev and CI
juliusmarminge Feb 16, 2026
0bca5a4
Replace wait-on with native bundle readiness polling
juliusmarminge Feb 16, 2026
6721804
Enable Linux Electron sandbox override for desktop perf runs
juliusmarminge Feb 16, 2026
c48a7b9
Fix dev-electron bundle readiness race in CI
juliusmarminge Feb 16, 2026
1d76eb5
Avoid cleaning desktop bundle in watch mode for perf CI
juliusmarminge Feb 16, 2026
2dc8fcd
maybe fix errors?
juliusmarminge Feb 17, 2026
47d5dc2
lint fix
juliusmarminge Feb 17, 2026
e666957
fix: update CI preload artifact name to .cjs and add freshness grace …
cursoragent Feb 17, 2026
1a98fc6
maybe?
juliusmarminge Feb 17, 2026
1a39d75
whoops
juliusmarminge Feb 17, 2026
ff45694
Fix desktop CI bundle readiness checks
cursoragent Feb 17, 2026
ca93003
Accept prebuilt desktop bundles in dev startup
cursoragent Feb 17, 2026
42ef93a
Run CI workflow on desktop perf fix branch
cursoragent Feb 17, 2026
52920ab
Rewrite perf seed project paths for CI runtime
cursoragent Feb 17, 2026
5c68c77
Harden perf automation against missing sidebar selectors
cursoragent Feb 17, 2026
a1dfc84
Make perf scripted interactions best-effort in CI
cursoragent Feb 17, 2026
7f6786f
Retry desktop project bootstrap when list is transiently empty
cursoragent Feb 17, 2026
0014818
Add detailed diagnostics for missing perf interaction UI
cursoragent Feb 17, 2026
ae27c20
Seed renderer state atomically before reload
cursoragent Feb 17, 2026
cbbfd0c
Instrument persisted state hydration for perf CI debugging
cursoragent Feb 17, 2026
2c0a3bd
Skip desktop project bootstrap during perf state replay
cursoragent Feb 17, 2026
ae1626b
Retry perf seed when post-reload threads are missing
cursoragent Feb 17, 2026
1314980
Continue perf terminal automation when split fails
cursoragent Feb 17, 2026
142b728
Require core renderer interactions in perf gate
cursoragent Feb 17, 2026
adba5d8
Reduce desktop perf scripted workload for CI stability
cursoragent Feb 17, 2026
91fb3a5
Trim perf interaction intensity to reduce CI dispatch spikes
cursoragent Feb 17, 2026
a41c0ab
Relax CI long-dispatch perf ceiling for desktop traces
cursoragent Feb 17, 2026
e69fe94
Remove temporary perf state debug instrumentation
cursoragent Feb 17, 2026
9b99c9b
Remove temporary branch-specific CI push trigger
cursoragent Feb 17, 2026
c2966e6
Scope perf bootstrap skip to explicit automation sessions
cursoragent Feb 17, 2026
ae6712b
Keep perf seed retry logic while trimming verbose diagnostics
cursoragent Feb 17, 2026
cdf7b01
Simplify perf seed reload helper return contract
cursoragent Feb 17, 2026
eb20d1f
Disable terminal perf scripting in CI and restore strict dispatch cei…
cursoragent Feb 17, 2026
0e26c94
Preserve terminal perf interactions by default outside CI
cursoragent Feb 17, 2026
4339050
Require explicit opt-in for terminal perf scripting
cursoragent Feb 17, 2026
356a5c4
Default terminal perf scripting off in CI when env unset
cursoragent Feb 17, 2026
6365657
Add tested parser for terminal perf interaction toggle
cursoragent Feb 17, 2026
2b96cc2
Document CI terminal perf toggle rationale
cursoragent Feb 17, 2026
7f5e9a7
Expand terminal perf toggle parser test coverage
cursoragent Feb 17, 2026
51b89ef
Document terminal perf toggle default behavior
cursoragent Feb 17, 2026
def1086
Harden perf toggle fallback for CI truthy values
cursoragent Feb 17, 2026
33eee2e
Expand CI fallback coverage in perf toggle tests
cursoragent Feb 17, 2026
b6036ce
Support yes/no env aliases in perf toggle parser
cursoragent Feb 17, 2026
5f7e22a
Add CI false-alias and blank-toggle perf tests
cursoragent Feb 17, 2026
e0e3e3a
Document perf toggle aliases and unknown CI fallback
cursoragent Feb 17, 2026
709a5c1
Reduce desktop perf scripted interaction intensity
cursoragent Feb 17, 2026
e70bc7b
Minimize desktop perf interaction workload further
cursoragent Feb 17, 2026
d95ccbf
Disable benchmark follow-up passes during CI perf runs
cursoragent Feb 17, 2026
142ce0f
Make CI perf benchmark pass count explicitly configurable
cursoragent Feb 17, 2026
2afff24
Extract benchmark follow-up pass parsing into perf config
cursoragent Feb 17, 2026
edd758d
Disable optional renderer perf interactions in CI
cursoragent Feb 17, 2026
37dfce2
Cap benchmark follow-up pass override for perf safety
cursoragent Feb 17, 2026
179e24a
Skip optional scroll load and shorten CI typing input
cursoragent Feb 17, 2026
c3b5475
Add CI toggle to skip benchmark thread sweep
cursoragent Feb 17, 2026
0a733fe
Expand benchmark sweep toggle tests and CI docs
cursoragent Feb 17, 2026
7aff63f
Unify perf toggle fallback logic across config helpers
cursoragent Feb 17, 2026
a787ec4
Expand benchmark pass parser boundary tests
cursoragent Feb 17, 2026
2090002
Log perf interaction toggles in automation artifacts
cursoragent Feb 17, 2026
f0c5334
Add optional renderer fallback coverage and CI env docs
cursoragent Feb 17, 2026
5069734
Include perf interaction config in PR summary output
cursoragent Feb 17, 2026
65b9144
Add consistency tests for perf toggle CI fallback
cursoragent Feb 17, 2026
7ed1df1
Broaden benchmark pass parser edge-case coverage
cursoragent Feb 17, 2026
8c1a85e
Include terminal toggle status in perf PR summary
cursoragent Feb 17, 2026
c2ddc46
Show benchmark thread ids in perf PR summary
cursoragent Feb 17, 2026
5eb73a8
Document perf toggle and benchmark pass resolution semantics
cursoragent Feb 17, 2026
5d9bade
Expand perf toggle alias consistency coverage
cursoragent Feb 17, 2026
87aa2d6
Harden desktop perf env threshold parsing
cursoragent Feb 17, 2026
cd2846b
Expose resolved perf thresholds in trace output
cursoragent Feb 17, 2026
ef95679
Set explicit keypress perf thresholds in CI
cursoragent Feb 17, 2026
0542b8e
Deduplicate desktop perf CI env configuration
cursoragent Feb 17, 2026
1a6b684
Revert "Deduplicate desktop perf CI env configuration"
cursoragent Feb 17, 2026
a122cb7
Normalize numeric perf env parsing semantics
cursoragent Feb 17, 2026
273a84c
Include fallback values in perf env warnings
cursoragent Feb 17, 2026
b0eef5c
Strengthen desktop perf trace reporting and seed requirements
juliusmarminge Feb 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 85 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
branches:
- main

permissions:
contents: read
pull-requests: write

jobs:
quality:
name: Lint, Typecheck, Test, Build
Expand Down Expand Up @@ -46,5 +50,84 @@ jobs:

- name: Verify preload bundle output
run: |
test -f apps/desktop/dist-electron/preload.js
grep -nE "desktopBridge|getWsUrl|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.js
test -f apps/desktop/dist-electron/preload.cjs
grep -nE "desktopBridge|getWsUrl|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.cjs

Comment thread
cursor[bot] marked this conversation as resolved.
- name: Install desktop runtime deps (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
xvfb \
xauth \
libgtk-3-0 \
libnotify4 \
libnss3 \
libxss1 \
libxtst6 \
libatspi2.0-0 \
libdrm2 \
libgbm1 \
libxrandr2 \
libxshmfence1 \
libxi6 \
xdg-utils
sudo apt-get install -y libasound2t64 || sudo apt-get install -y libasound2

- name: Desktop perf trace test
if: github.event_name != 'pull_request'
env:
ELECTRON_DISABLE_SANDBOX: "1"
T3CODE_PERF_TIMEOUT_MS: "420000"
T3CODE_PERF_MAX_KEYPRESS_AVG_MS: "12"
T3CODE_PERF_MAX_KEYPRESS_MAX_MS: "24"
T3CODE_PERF_MAX_LONG_DISPATCH_COUNT: "5"
# Terminal PTY scripting is flaky in CI VMs; renderer perf checks remain enforced.
T3CODE_DESKTOP_PERF_RUN_TERMINAL: "0"
# Keep only core renderer interactions in CI perf runs for lower variance.
T3CODE_DESKTOP_PERF_RUN_OPTIONAL_RENDERER: "0"
# Skip benchmark sweep loops in CI to reduce EventDispatch variance.
T3CODE_DESKTOP_PERF_RUN_BENCHMARK_SWEEP: "0"
# Keep benchmark follow-up passes disabled in CI by default.
T3CODE_DESKTOP_PERF_BENCHMARK_FOLLOW_UP_PASSES: "0"
T3CODE_ELECTRON_STARTUP_TIMEOUT_MS: "180000"
run: xvfb-run -a bun run test:desktop-perf

- name: Desktop perf trace test (fork PR)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
env:
ELECTRON_DISABLE_SANDBOX: "1"
T3CODE_PERF_TIMEOUT_MS: "420000"
T3CODE_PERF_MAX_KEYPRESS_AVG_MS: "12"
T3CODE_PERF_MAX_KEYPRESS_MAX_MS: "24"
T3CODE_PERF_MAX_LONG_DISPATCH_COUNT: "5"
T3CODE_DESKTOP_PERF_RUN_TERMINAL: "0"
T3CODE_DESKTOP_PERF_RUN_OPTIONAL_RENDERER: "0"
T3CODE_DESKTOP_PERF_RUN_BENCHMARK_SWEEP: "0"
T3CODE_DESKTOP_PERF_BENCHMARK_FOLLOW_UP_PASSES: "0"
T3CODE_ELECTRON_STARTUP_TIMEOUT_MS: "180000"
run: xvfb-run -a bun run test:desktop-perf

- name: Desktop perf trace test and PR summary
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
env:
GH_TOKEN: ${{ github.token }}
ELECTRON_DISABLE_SANDBOX: "1"
T3CODE_PERF_TIMEOUT_MS: "420000"
T3CODE_PERF_MAX_KEYPRESS_AVG_MS: "12"
T3CODE_PERF_MAX_KEYPRESS_MAX_MS: "24"
T3CODE_PERF_MAX_LONG_DISPATCH_COUNT: "5"
T3CODE_DESKTOP_PERF_RUN_TERMINAL: "0"
T3CODE_DESKTOP_PERF_RUN_OPTIONAL_RENDERER: "0"
T3CODE_DESKTOP_PERF_RUN_BENCHMARK_SWEEP: "0"
T3CODE_DESKTOP_PERF_BENCHMARK_FOLLOW_UP_PASSES: "0"
T3CODE_ELECTRON_STARTUP_TIMEOUT_MS: "180000"
run: xvfb-run -a bun run test:desktop-perf:post
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Upload desktop perf artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: desktop-perf-artifacts
path: /tmp/t3code-perf-artifacts
if-no-files-found: ignore
7 changes: 4 additions & 3 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
"name": "@t3tools/desktop",
"version": "0.0.0",
"private": true,
"main": "dist-electron/main.js",
"type": "module",
"main": "dist-electron/main.mjs",
"scripts": {
"dev": "bun run --parallel dev:bundle dev:electron",
"dev:bundle": "tsdown --watch",
"dev:bundle": "tsdown --watch --no-clean",
"dev:electron": "bun run scripts/dev-electron.mjs",
"build": "tsdown",
"start": "electron dist-electron/main.js",
"start": "electron dist-electron/main.mjs",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"smoke-test": "node scripts/smoke-test.mjs"
Expand Down
191 changes: 178 additions & 13 deletions apps/desktop/scripts/dev-electron.mjs
Original file line number Diff line number Diff line change
@@ -1,24 +1,184 @@
import { spawn } from "node:child_process";

import waitOn from "wait-on";
import fs from "node:fs";
import net from "node:net";
import path from "node:path";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused wait-on dependency after import removal

Low Severity

The import waitOn from "wait-on" was the sole usage of the wait-on package. This diff replaces it with custom waiting logic but leaves wait-on as a devDependency in apps/desktop/package.json. The package (which pulls in axios, joi, lodash, rxjs) is now dead weight in the dependency tree.

Additional Locations (1)

Fix in Cursor Fix in Web


const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5173);
const devServerUrl = `http://localhost:${port}`;

await waitOn({
resources: [
`tcp:${port}`,
"file:dist-electron/main.js",
"file:dist-electron/preload.js",
"file:../server/dist/index.mjs",
],
});
const STARTUP_TIMEOUT_MS = Number(process.env.T3CODE_ELECTRON_STARTUP_TIMEOUT_MS ?? 120_000);

async function canConnect(host, probePort, timeoutMs = 1_000) {
return new Promise((resolve) => {
const socket = new net.Socket();
let settled = false;

const finish = (value) => {
if (settled) return;
settled = true;
socket.destroy();
resolve(value);
};

socket.setTimeout(timeoutMs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low

scripts/dev-electron.mjs:21 socket.setTimeout() sets an inactivity timeout, not a connection timeout. Consider wrapping the entire operation in a setTimeout with finish(false) to guarantee the function resolves within timeoutMs.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/desktop/scripts/dev-electron.mjs around line 21:

`socket.setTimeout()` sets an inactivity timeout, not a connection timeout. Consider wrapping the entire operation in a `setTimeout` with `finish(false)` to guarantee the function resolves within `timeoutMs`.

Evidence trail:
Viewed `apps/desktop/scripts/dev-electron.mjs:11-26` at `ce27526` (shows `socket.setTimeout(timeoutMs);`).

socket.once("connect", () => finish(true));
socket.once("timeout", () => finish(false));
socket.once("error", () => finish(false));
socket.connect(probePort, host);
});
}

Comment thread
macroscopeapp[bot] marked this conversation as resolved.
const f = (p) => path.join(import.meta.dirname, "..", p);

function inspectBundleFile(bundleFile) {
let stat = null;
try {
stat = fs.statSync(bundleFile.path);
} catch {
return {
...bundleFile,
status: "missing",
mtimeMs: null,
reason: "not found",
};
}

return {
...bundleFile,
status: "ready",
mtimeMs: stat.mtimeMs,
reason: "ok",
};
}

function describeBundleStates(states) {
return states
.map((state) => {
const mtime =
typeof state.mtimeMs === "number" ? new Date(state.mtimeMs).toISOString() : "n/a";
return `${state.label}=${state.status} (mtime=${mtime}; reason=${state.reason})`;
})
.join("; ");
}

function waitForDesktopBundles(timeoutMs) {
const startedAt = Date.now();
const bundleFiles = [
{
path: f("dist-electron/main.mjs"),
label: "desktop/main.mjs",
},
{
path: f("dist-electron/preload.cjs"),
label: "desktop/preload.cjs",
},
{
path: f("../server/dist/index.mjs"),
label: "server/index.mjs",
},
];
let lastProgressLogAt = 0;

return new Promise((resolve, reject) => {
const tick = () => {
const states = bundleFiles.map(inspectBundleFile);
const missing = states.filter((state) => state.status === "missing");

if (missing.length === 0) {
resolve();
return;
}

const elapsedMs = Date.now() - startedAt;
if (elapsedMs >= timeoutMs) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low

scripts/dev-electron.mjs:91 If T3CODE_ELECTRON_STARTUP_TIMEOUT_MS is a non-numeric string, STARTUP_TIMEOUT_MS becomes NaN, causing the timeout check to never trigger (comparisons with NaN are always false). Consider validating the timeout value or using Number.isNaN() to guard against infinite polling.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/desktop/scripts/dev-electron.mjs around line 91:

If `T3CODE_ELECTRON_STARTUP_TIMEOUT_MS` is a non-numeric string, `STARTUP_TIMEOUT_MS` becomes `NaN`, causing the timeout check to never trigger (comparisons with `NaN` are always `false`). Consider validating the timeout value or using `Number.isNaN()` to guard against infinite polling.

Evidence trail:
Viewed `apps/desktop/scripts/dev-electron.mjs:1-8` and `apps/desktop/scripts/dev-electron.mjs:40-115` and `apps/desktop/scripts/dev-electron.mjs:124-190` at commit `19f1569` in https://github.com/pingdotgg/codething-mvp/tree/19f15693b75c02d9a2d0bdb1cc002629526804c2.

const parts = [];
if (missing.length > 0) {
parts.push(`missing: ${missing.map((state) => state.label).join(", ")}`);
}
reject(
new Error(
`[dev-electron] timed out after ${timeoutMs}ms waiting for bundles (${parts.join("; ")})\n[dev-electron] bundle state: ${describeBundleStates(states)}`,
),
);
return;
}

if (Date.now() - lastProgressLogAt >= 5_000) {
lastProgressLogAt = Date.now();
const waitParts = [];
if (missing.length > 0) {
waitParts.push(`missing=${missing.map((state) => state.label).join(",")}`);
}
console.log(
`[dev-electron] still waiting for bundles after ${elapsedMs}ms (${waitParts.join("; ")})`,
);
}

setTimeout(tick, 250);
};

tick();
});
}

function waitForDevServer(probePort, timeoutMs) {
const startedAt = Date.now();
const candidates = [
{ host: "127.0.0.1", url: `http://127.0.0.1:${probePort}` },
{ host: "::1", url: `http://[::1]:${probePort}` },
{ host: "localhost", url: `http://localhost:${probePort}` },
];

return new Promise((resolve, reject) => {
const tick = () => {
void Promise.all(
candidates.map(async (candidate) => ({
candidate,
connected: await canConnect(candidate.host, probePort),
})),
)
.then((results) => {
const ready = results.find((entry) => entry.connected);
if (ready) {
resolve(ready.candidate.url);
return;
}

if (Date.now() - startedAt >= timeoutMs) {
reject(
new Error(
`[dev-electron] timed out after ${timeoutMs}ms waiting for renderer dev server on port ${probePort}`,
),
);
return;
}

setTimeout(tick, 250);
})
.catch((error) => {
reject(error);
});
};

tick();
});
}

console.log("[dev-electron] waiting for desktop/server bundles");
await waitForDesktopBundles(STARTUP_TIMEOUT_MS);

console.log(`[dev-electron] waiting for renderer dev server on port ${port}`);
const devServerUrl = await waitForDevServer(port, STARTUP_TIMEOUT_MS);
console.log(`[dev-electron] launching electron with renderer url ${devServerUrl}`);

const command = process.platform === "win32" ? "electronmon.cmd" : "electronmon";
const childEnv = { ...process.env };
delete childEnv.ELECTRON_RUN_AS_NODE;

const child = spawn(command, ["dist-electron/main.js"], {
if (process.platform === "linux" && !childEnv.ELECTRON_DISABLE_SANDBOX) {
childEnv.ELECTRON_DISABLE_SANDBOX = "1";
console.log("[dev-electron] enabling ELECTRON_DISABLE_SANDBOX=1 on Linux");
}

const child = spawn(command, ["dist-electron/main.mjs"], {
stdio: "inherit",
env: {
...childEnv,
Expand All @@ -29,3 +189,8 @@ const child = spawn(command, ["dist-electron/main.js"], {
child.on("exit", (code) => {
process.exit(code ?? 0);
});

child.on("error", (error) => {
console.error("[dev-electron] failed to launch electronmon:", error.message);
process.exit(1);
});
Loading