authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-10-11 03:44:50+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 18:44:50-07:00
log1c7b267424a18d257c81d0e9a91f403fb20d2458
tree4b10793ae86cacb7762b63692934bc8a61e483b1
parent7f0b789380f95032a55c777817cde8b49e2dae5a
signature Signed by PGP key B5690EEEBB952194

perf(server): batch terminal history writes every 250 ms (#17183)


2 files changed, 46 insertions(+), 1 deletions(-)

apps/server/src/terminal/Manager.test.ts+45
......@@ -26,6 +26,7 @@ import * as Layer from "effect/Layer";
2626import * as Option from "effect/Option";
2727import * as PlatformError from "effect/PlatformError";
2828import * as Path from "effect/Path";
29import * as Queue from "effect/Queue";
2930import * as Ref from "effect/Ref";
3031import * as Schedule from "effect/Schedule";
3132import * as Schema from "effect/Schema";
......@@ -1400,6 +1401,50 @@ it.layer(
14001401 }),
14011402 );
14021403
1404 it.effect("coalesces live history writes while retaining the latest output on close", () =>
1405 Effect.gen(function* () {
1406 const fs = yield* FileSystem.FileSystem;
1407 const writes = yield* Queue.unbounded<string>();
1408 const output = yield* Queue.unbounded<string>();
1409 const trackedFileSystem = FileSystem.FileSystem.of({
1410 ...fs,
1411 writeFileString: (filePath, contents, options) =>
1412 fs
1413 .writeFileString(filePath, contents, options)
1414 .pipe(
1415 Effect.tap(() => (contents.length > 0 ? Queue.offer(writes, contents) : Effect.void)),
1416 ),
1417 });
1418 const { manager, ptyAdapter } = yield* createManager(5, { processKillGraceMs: 0 }).pipe(
1419 Effect.provideService(FileSystem.FileSystem, trackedFileSystem),
1420 );
1421 yield* manager.open(openInput());
1422 const unsubscribe = yield* manager.subscribe((event) =>
1423 event.type === "output" ? Queue.offer(output, event.data).pipe(Effect.asVoid) : Effect.void,
1424 );
1425 yield* Effect.addFinalizer(() => Effect.sync(unsubscribe));
1426 const process = ptyAdapter.processes[0]!;
1427
1428 process.emitData("first");
1429 yield* Queue.take(output);
1430 yield* TestClock.adjust("200 millis");
1431 expect(yield* Queue.size(writes)).toBe(0);
1432
1433 process.emitData(" second");
1434 yield* Queue.take(output);
1435 yield* TestClock.adjust("50 millis");
1436 expect(yield* Queue.take(writes)).toBe("first second");
1437
1438 process.emitData(" third");
1439 yield* Queue.take(output);
1440 const close = yield* manager.close({ threadId: "thread-1" }).pipe(Effect.forkScoped);
1441 yield* Effect.yieldNow;
1442 yield* TestClock.adjust("250 millis");
1443 yield* Fiber.join(close);
1444 expect((yield* manager.open(openInput())).history).toBe("first second third");
1445 }).pipe(Effect.provide(TestClock.layer())),
1446 );
1447
14031448 it.effect("caps persisted history to configured line limit", () =>
14041449 Effect.gen(function* () {
14051450 const { manager, ptyAdapter } = yield* createManager(3);
apps/server/src/terminal/Manager.ts+1-1
......@@ -98,7 +98,7 @@ export {
9898const DEFAULT_HISTORY_LINE_LIMIT = 5_000;
9999const DEFAULT_HISTORY_BYTE_LIMIT = 8 * 1024 * 1024;
100100const MAX_HISTORY_CHUNK_LENGTH = 16 * 1024;
101const DEFAULT_PERSIST_DEBOUNCE_MS = 40;
101const DEFAULT_PERSIST_DEBOUNCE_MS = 250;
102102const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000;
103103const MAX_SUBPROCESS_POLL_INTERVAL_MS = 60_000;
104104const DEFAULT_PROCESS_KILL_GRACE_MS = 1_000;