Compare commits

..
Author SHA1 Message Date
LukeParkerDev 2d6abd8f72 perf(desktop): create windows before the background service connects
BackgroundServiceState awaited the first CLI service connection while
the layer was built, and windows are only created after every layer has
built. Fork the initial connect instead and let consumers await its
first outcome through 'connection', so the renderer loads its bundle
while the main process resolves the service.

On its own the gain is small because the WSL layer still awaits the CLI
version spawn; with the version cache (#49762) the renderer process
starts 32 ms earlier (512 -> 480 ms median) and the shell is visible at
932 ms instead of 961 ms.
2026-09-18 22:43:56 +10:00
4 changed files with 66 additions and 38 deletions
@@ -1,15 +1,57 @@
import { expect, test } from "bun:test"
import { Effect } from "effect"
import { Deferred, Effect, Fiber } from "effect"
import { BackgroundServiceState } from "./background-service-state"
test("new consumers receive the latest reconnected service", async () => {
const initial = { url: "http://127.0.0.1:4100", password: "first" }
const replacement = { url: "http://127.0.0.1:4200", password: "second" }
const service = await Effect.runPromise(
BackgroundServiceState.make({ initial: Effect.succeed(initial), reconnect: Effect.succeed(replacement) }),
Effect.scoped(
Effect.gen(function* () {
const state = yield* BackgroundServiceState.make({
initial: Effect.succeed(initial),
reconnect: Effect.succeed(replacement),
})
expect(yield* state.connection).toEqual(initial)
expect(yield* state.reconnect).toEqual(replacement)
return yield* state.connection
}),
),
)
expect(service).toEqual(replacement)
})
expect(await Effect.runPromise(service.connection)).toEqual(initial)
expect(await Effect.runPromise(service.reconnect)).toEqual(replacement)
expect(await Effect.runPromise(service.connection)).toEqual(replacement)
test("the state is ready before the initial connect finishes and consumers wait for it", async () => {
const initial = { url: "http://127.0.0.1:4100", password: "first" }
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const gate = yield* Deferred.make<void>()
const state = yield* BackgroundServiceState.make({
initial: Deferred.await(gate).pipe(Effect.as(initial)),
reconnect: Effect.succeed(initial),
})
const consumer = yield* Effect.forkChild(state.connection)
yield* Effect.sleep("10 millis")
expect(consumer.pollUnsafe()).toBeUndefined()
yield* Deferred.succeed(gate, undefined)
expect(yield* Fiber.join(consumer)).toEqual(initial)
}),
),
)
})
test("a failed initial connect fails consumers instead of hanging them", async () => {
const result = await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const state = yield* BackgroundServiceState.make({
initial: Effect.fail(new Error("service unavailable")),
reconnect: Effect.succeed({ url: "http://127.0.0.1:4100", password: "later" }),
})
return yield* state.connection.pipe(Effect.exit)
}),
),
)
expect(result._tag).toBe("Failure")
})
@@ -1,16 +1,30 @@
export * as BackgroundServiceState from "./background-service-state"
import { Effect, Exit, Ref } from "effect"
import { Deferred, Effect, Exit, Ref } from "effect"
import type { SidecarCredentials } from "./sidecar-credentials"
// The initial connect runs in the background: windows are created as soon as the main process
// is up, and the renderer loads its bundle while the CLI service is being resolved. Consumers
// await the first outcome through `connection`, so nothing observes the service before it exists.
export const make = Effect.fn("BackgroundServiceState.make")(function* (options: {
readonly initial: Effect.Effect<SidecarCredentials.Data, unknown>
readonly reconnect: Effect.Effect<SidecarCredentials.Data>
}) {
// Every Exit is an Effect, so the latest resolution replays directly for each consumer.
const current = yield* Ref.make<Exit.Exit<SidecarCredentials.Data, unknown>>(yield* options.initial.pipe(Effect.exit))
const current = yield* Ref.make<Exit.Exit<SidecarCredentials.Data, unknown> | undefined>(undefined)
const first = yield* Deferred.make<void>()
yield* options.initial.pipe(
Effect.exit,
Effect.flatMap((exit) => Ref.set(current, exit)),
Effect.ensuring(Deferred.succeed(first, undefined)),
Effect.forkScoped,
)
return {
connection: Ref.get(current).pipe(Effect.flatten, Effect.orDie),
connection: Deferred.await(first).pipe(
Effect.flatMap(() => Ref.get(current)),
Effect.flatMap((exit) => exit ?? Exit.die(new Error("background service connect did not resolve"))),
Effect.orDie,
),
reconnect: options.reconnect.pipe(Effect.tap((next) => Ref.set(current, Exit.succeed(next)))),
}
})
@@ -3,11 +3,9 @@ export * as DesktopCli from "./desktop-cli"
import { execFile, spawn } from "node:child_process"
import { promisify } from "node:util"
import { app } from "electron"
import { Context, Effect, FileSystem, Layer, Option, Path } from "effect"
import { Context, Effect, FileSystem, Layer, Path } from "effect"
import installer from "../../../../../install?raw"
import { DesktopPaths } from "../paths"
import { BUNDLED_CLI_VERSION_KEY } from "../storage/keys"
import { getStore } from "../storage/store"
import { parseCliVersion } from "./cli-version"
const execFileAsync = promisify(execFile)
@@ -81,36 +79,11 @@ const resolveBundledCli = Effect.fn("DesktopCli.resolveBundled")(function* (isol
? path.join(process.resourcesPath, executableName())
: path.join(paths.developmentResourcesRoot, isolated ? developmentExecutableName() : executableName())
yield* Effect.logInfo("v2 CLI executable resolved", { bundled, packaged: app.isPackaged })
const version = yield* bundledVersion(bundled)
const version = parseCliVersion(yield* run(bundled, ["--version"]))
const binary = app.isPackaged || isolated ? yield* installCli(bundled, version) : bundled
return { version, binary, command: [binary] }
})
// Spawning the bundled executable for `--version` costs ~400 ms of startup on a 200 MB binary, so
// the answer is remembered per executable identity and only re-read after an update replaces it.
const bundledVersion = Effect.fn("DesktopCli.bundledVersion")(function* (bundled: string) {
const fs = yield* FileSystem.FileSystem
const stat = yield* fs.stat(bundled).pipe(Effect.orElseSucceed(() => undefined))
const identity = stat ? `${stat.size}:${Option.getOrUndefined(stat.mtime)?.getTime() ?? ""}` : undefined
const store = getStore()
const cached = store.get(BUNDLED_CLI_VERSION_KEY)
if (identity && isVersionCache(cached) && cached.path === bundled && cached.identity === identity) {
yield* Effect.logInfo("v2 CLI version reused", { version: cached.version })
return cached.version
}
const version = parseCliVersion(yield* run(bundled, ["--version"]))
if (identity) store.set(BUNDLED_CLI_VERSION_KEY, { path: bundled, identity, version } satisfies VersionCache)
return version
})
type VersionCache = { path: string; identity: string; version: string }
function isVersionCache(value: unknown): value is VersionCache {
if (!value || typeof value !== "object") return false
const cache = value as Record<string, unknown>
return typeof cache.path === "string" && typeof cache.identity === "string" && typeof cache.version === "string"
}
export const cleanStages = Effect.fn("DesktopCli.cleanStages")(function* (binary: string) {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
@@ -5,4 +5,3 @@ export const WSL_SERVERS_KEY = "wslServers"
export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled"
export const BACKGROUND_COLOR_KEY = "backgroundColor"
export const WINDOW_IDS_KEY = "windowIds"
export const BUNDLED_CLI_VERSION_KEY = "bundledCliVersion"