Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 21 additions & 17 deletions apps/code/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ import {
import { isMacosPackagedUnsafeBundleLocation } from "./utils/macos-packaged-install-guard";
import { installMainFetchLogging } from "./utils/network-fetch-logger";
import { installRendererNetworkLogging } from "./utils/network-webrequest-logger";
import {
RendererCrashRecovery,
toSafeRendererUrl,
} from "./utils/renderer-crash-recovery";
import { createWindow } from "./window";

type FileWatcherEventsByKind = {
Expand Down Expand Up @@ -167,19 +171,10 @@ const RECOVERABLE_RENDER_REASONS = new Set([
]);
const CRASH_LOOP_WINDOW_MS = 30_000;
const CRASH_LOOP_THRESHOLD = 3;
const recentCrashTimestamps: number[] = [];

function isCrashLoop(): boolean {
const now = Date.now();
while (
recentCrashTimestamps.length > 0 &&
now - recentCrashTimestamps[0] > CRASH_LOOP_WINDOW_MS
) {
recentCrashTimestamps.shift();
}
recentCrashTimestamps.push(now);
return recentCrashTimestamps.length >= CRASH_LOOP_THRESHOLD;
}
const rendererCrashRecovery = new RendererCrashRecovery({
crashLoopThreshold: CRASH_LOOP_THRESHOLD,
crashLoopWindowMs: CRASH_LOOP_WINDOW_MS,
});

function crashDiagnostics() {
return {
Expand Down Expand Up @@ -211,9 +206,10 @@ app.on("render-process-gone", (_event, webContents, details) => {
posthogNodeAnalytics.flush().catch(() => {});

if (RECOVERABLE_RENDER_REASONS.has(details.reason)) {
if (isCrashLoop()) {
const recoveryAction = rendererCrashRecovery.nextAction();
if (recoveryAction === "stop") {
log.error("Crash loop detected, stopping auto-recovery", {
crashesInWindow: recentCrashTimestamps.length,
crashesInWindow: rendererCrashRecovery.recentCrashCount,
windowMs: CRASH_LOOP_WINDOW_MS,
});
return;
Expand All @@ -226,8 +222,16 @@ app.on("render-process-gone", (_event, webContents, details) => {
}
setImmediate(() => {
if (win.isDestroyed()) return;
log.info("Reloading webContents");
win.webContents.reload();
if (recoveryAction === "reset-route") {
const safeUrl = toSafeRendererUrl(win.webContents.getURL());
log.info("Reloading renderer without the crashing route", { safeUrl });
void win.loadURL(safeUrl).catch((error) => {
log.error("Failed to load renderer recovery URL", { error });
});
} else {
log.info("Reloading webContents");
win.webContents.reload();
}
log.info("Bringing window to foreground");
win.show();
win.moveTop();
Expand Down
38 changes: 38 additions & 0 deletions apps/code/src/main/utils/renderer-crash-recovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import {
RendererCrashRecovery,
toSafeRendererUrl,
} from "./renderer-crash-recovery";

describe("RendererCrashRecovery", () => {
it("escapes the crashing route before stopping automatic recovery", () => {
const recovery = new RendererCrashRecovery({
crashLoopThreshold: 3,
crashLoopWindowMs: 30_000,
});

expect(recovery.nextAction(0)).toBe("reload");
expect(recovery.nextAction(13_000)).toBe("reset-route");
expect(recovery.nextAction(20_000)).toBe("stop");
});

it("starts with a normal reload after the crash window expires", () => {
const recovery = new RendererCrashRecovery({
crashLoopThreshold: 3,
crashLoopWindowMs: 30_000,
});

expect(recovery.nextAction(0)).toBe("reload");
expect(recovery.nextAction(31_000)).toBe("reload");
});
});

describe("toSafeRendererUrl", () => {
it("removes the route while preserving the renderer entrypoint", () => {
expect(
toSafeRendererUrl(
"file:///Applications/PostHog%20Code.app/index.html#/website/site/tasks/task",
),
).toBe("file:///Applications/PostHog%20Code.app/index.html");
});
});
37 changes: 37 additions & 0 deletions apps/code/src/main/utils/renderer-crash-recovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export type RendererCrashRecoveryAction = "reload" | "reset-route" | "stop";

type RendererCrashRecoveryOptions = {
crashLoopThreshold: number;
crashLoopWindowMs: number;
};

export class RendererCrashRecovery {
private readonly recentCrashTimestamps: number[] = [];

constructor(private readonly options: RendererCrashRecoveryOptions) {}

nextAction(now = Date.now()): RendererCrashRecoveryAction {
while (
this.recentCrashTimestamps.length > 0 &&
now - this.recentCrashTimestamps[0] > this.options.crashLoopWindowMs
) {
this.recentCrashTimestamps.shift();
}

this.recentCrashTimestamps.push(now);
if (this.recentCrashTimestamps.length >= this.options.crashLoopThreshold) {
return "stop";
}
return this.recentCrashTimestamps.length === 1 ? "reload" : "reset-route";
}

get recentCrashCount(): number {
return this.recentCrashTimestamps.length;
}
}

export function toSafeRendererUrl(rendererUrl: string): string {
const url = new URL(rendererUrl);
url.hash = "";
return url.toString();
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
type VirtualizedListHandle,
} from "@posthog/ui/features/sessions/components/VirtualizedList";
import { CHAT_CONTENT_MAX_WIDTH } from "@posthog/ui/features/sessions/constants";
import { createConversationDiffPoolOptions } from "@posthog/ui/features/sessions/conversationDiffPoolOptions";
import { DIFFS_HIGHLIGHTER_OPTIONS } from "@posthog/ui/features/sessions/diffHighlighterOptions";
import { useContextUsage } from "@posthog/ui/features/sessions/hooks/useContextUsage";
import { useConversationItems } from "@posthog/ui/features/sessions/hooks/useConversationItems";
Expand Down Expand Up @@ -102,14 +103,7 @@ export function ConversationView({
}: ConversationViewProps) {
const diffWorkerFactory = useService<DiffWorkerFactory>(DIFF_WORKER_FACTORY);
const diffsPoolOptions = useMemo(
() => ({
workerFactory: () => diffWorkerFactory(),
totalASTLRUCacheSize: 200,
// Each pooled highlighter worker is a full V8 isolate with shiki
// grammars loaded (~40MB RSS); the library default of 8 costs hundreds
// of MB for parallelism conversation diffs don't need.
poolSize: 2,
}),
() => createConversationDiffPoolOptions(diffWorkerFactory),
[diffWorkerFactory],
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
import { SessionUpdateView } from "@posthog/ui/features/sessions/components/session-update/SessionUpdateView";
import { UserShellExecuteView } from "@posthog/ui/features/sessions/components/session-update/UserShellExecuteView";
import { CHAT_CONTENT_MAX_WIDTH } from "@posthog/ui/features/sessions/constants";
import { createConversationDiffPoolOptions } from "@posthog/ui/features/sessions/conversationDiffPoolOptions";
import { DIFFS_HIGHLIGHTER_OPTIONS } from "@posthog/ui/features/sessions/diffHighlighterOptions";
import { useConversationItems } from "@posthog/ui/features/sessions/hooks/useConversationItems";
import {
Expand Down Expand Up @@ -990,10 +991,7 @@ export function ChatThread({
}: ConversationViewProps) {
const diffWorkerFactory = useService<DiffWorkerFactory>(DIFF_WORKER_FACTORY);
const diffsPoolOptions = useMemo(
() => ({
workerFactory: () => diffWorkerFactory(),
totalASTLRUCacheSize: 200,
}),
() => createConversationDiffPoolOptions(diffWorkerFactory),
[diffWorkerFactory],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { describe, expect, it, vi } from "vitest";
import { createConversationDiffPoolOptions } from "./conversationDiffPoolOptions";

describe("createConversationDiffPoolOptions", () => {
it("limits syntax-highlighting workers for diff-heavy conversations", () => {
const worker = {} as Worker;
const diffWorkerFactory = vi.fn(() => worker);
const options = createConversationDiffPoolOptions(diffWorkerFactory);

expect(options.poolSize).toBe(2);
expect(options.totalASTLRUCacheSize).toBe(200);
expect(options.workerFactory()).toBe(worker);
expect(diffWorkerFactory).toHaveBeenCalledOnce();
});
});
11 changes: 11 additions & 0 deletions packages/ui/src/features/sessions/conversationDiffPoolOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { DiffWorkerFactory } from "@posthog/ui/shell/diffWorkerHost";

export function createConversationDiffPoolOptions(
diffWorkerFactory: DiffWorkerFactory,
) {
return {
workerFactory: () => diffWorkerFactory(),
totalASTLRUCacheSize: 200,
poolSize: 2,
};
}
Loading