fix(desktop): let the spawn coordinator follow the live pool max

Composition of #92581 on #100985: the hard cap is a constructor constant,
so raising the pool max in Settings would have left new spawns queued
behind the launch-time value. Add setLimit(); slot hand-off now goes
through a single #drain that respects the current cap, which also fixes
the original release path handing a slot to the next waiter even when the
cap had just been lowered (test: lowering never revokes granted slots; new
requests queue until under cap). main.ts constructs from
poolLimits.maxBackends and pushes changes from setPoolLimits(); pinned by
a wiring test.
This commit is contained in:
kshitijk4poor
2026-09-03 12:12:24 +05:30
committed by kshitij
parent c401756a6a
commit f260ea5347
2 changed files with 87 additions and 6 deletions
@@ -269,6 +269,56 @@ test('a failed or repeated cleanup releases exactly one slot', async () => {
})
test('raising the limit at runtime drains queued waiters into the new slots', async () => {
const coordinator = new LocalBackendSpawnCoordinator(1)
const first = await coordinator.acquire('a')
const queuedB = coordinator.request('b')
const queuedC = coordinator.request('c')
await flush()
assert.equal(coordinator.activeCount, 1)
assert.equal(coordinator.queuedCount, 2)
coordinator.setLimit(2)
const releaseB = await queuedB.acquired
assert.equal(coordinator.activeCount, 2)
assert.equal(coordinator.queuedCount, 1)
first()
const releaseC = await queuedC.acquired
assert.equal(coordinator.activeCount, 2)
releaseB()
releaseC()
assert.equal(coordinator.activeCount, 0)
})
test('lowering the limit never revokes granted slots; new requests queue until under cap', async () => {
const coordinator = new LocalBackendSpawnCoordinator(3)
const releases = await Promise.all(['a', 'b', 'c'].map(key => coordinator.acquire(key)))
coordinator.setLimit(1)
assert.equal(coordinator.activeCount, 3, 'granted slots stay granted')
const queued = coordinator.request('d')
await flush()
assert.equal(coordinator.queuedCount, 1)
releases[0]()
releases[1]()
await flush()
assert.equal(coordinator.queuedCount, 1, 'still over the new cap of 1')
releases[2]()
const releaseD = await queued.acquired
assert.equal(coordinator.activeCount, 1)
releaseD()
})
test('setLimit rejects a non-positive or fractional cap', () => {
const coordinator = new LocalBackendSpawnCoordinator(2)
assert.throws(() => coordinator.setLimit(0), RangeError)
assert.throws(() => coordinator.setLimit(1.5), RangeError)
assert.equal(coordinator.limit, 2)
})
// ── main.ts wiring ──────────────────────────────────────────────────────────
// The coordinator is only as good as the timeout main.ts hands it. A queued
// ticket that outlives the renderer's backend-boot budget holds the pool key
@@ -296,4 +346,12 @@ test('a failed or repeated cleanup releases exactly one slot', async () => {
assert.match(mainSource, /localBackendSpawnCoordinator\.request\(poolKey, \{ timeoutMs: POOL_SLOT_WAIT_MS \}\)/)
assert.doesNotMatch(mainSource, /request\(poolKey, \{ timeoutMs: POOL_IDLE_MS \}\)/)
})
test('main.ts pushes the live pool max into the coordinator when the preference changes', () => {
// Pool sizing is a live device preference (#92581); the hard cap must
// follow it, otherwise raising the max in Settings would leave spawns
// queued behind the launch-time value.
assert.match(mainSource, /new LocalBackendSpawnCoordinator\(poolLimits\.maxBackends\)/)
assert.match(mainSource, /localBackendSpawnCoordinator\.setLimit\(poolLimits\.maxBackends\)/)
})
}
@@ -27,7 +27,7 @@ export async function releaseLocalBackendSlotAfterExit(
* the child exits or the start fails. Remote descriptors never call request().
*/
export class LocalBackendSpawnCoordinator {
readonly #limit: number
#limit: number
#active = 0
#queue: Waiter[] = []
@@ -43,6 +43,25 @@ export class LocalBackendSpawnCoordinator {
return this.#active
}
get limit(): number {
return this.#limit
}
/**
* Adopt a new cap at runtime (the pool size is a live device preference).
* Raising it drains waiters into the newly freed slots immediately; lowering
* it never revokes a granted slot — the running backends simply stay over
* the cap until they exit, and LRU eviction (main.ts) converges the pool.
*/
setLimit(limit: number): void {
if (!Number.isInteger(limit) || limit < 1) {
throw new RangeError('Local backend spawn limit must be a positive integer.')
}
this.#limit = limit
this.#drain()
}
get queuedCount(): number {
return this.#queue.length
}
@@ -119,12 +138,16 @@ export class LocalBackendSpawnCoordinator {
released = true
this.#active -= 1
const next = this.#queue.shift()
this.#drain()
}
}
if (next) {
this.#clearTimer(next)
next.resolve(this.#grant())
}
/** Hand free slots to queued waiters while under the (possibly lowered) cap. */
#drain(): void {
while (this.#active < this.#limit && this.#queue.length > 0) {
const next = this.#queue.shift()!
this.#clearTimer(next)
next.resolve(this.#grant())
}
}
}