Skip to content

Commit d749e6a

Browse files
authored
fix: Remove snapshot APIs that deadlock libuv thread pool (#1338)
## Problem @parcel/watcher's getEventsSince() and writeSnapshot() block libuv worker threads indefinitely via a native mutex deadlock in FSEventsBackend, saturating the default 4-thread pool and preventing clean shutdown. ## Changes 1. Remove snapshot-based change detection (getEventsSince/writeSnapshot calls) 2. Update @parcel/watcher to 2.5.6 for upstream bug fixes 3. Remove terminal font family setting from general settings ## How did you test this? Manually
1 parent 71eab6a commit d749e6a

8 files changed

Lines changed: 19 additions & 337 deletions

File tree

apps/code/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
"@opentelemetry/resources": "^2.5.0",
124124
"@opentelemetry/sdk-logs": "^0.208.0",
125125
"@opentelemetry/semantic-conventions": "^1.39.0",
126-
"@parcel/watcher": "^2.5.1",
126+
"@parcel/watcher": "^2.5.6",
127127
"@phosphor-icons/react": "^2.1.10",
128128
"@posthog/agent": "workspace:*",
129129
"@posthog/electron-trpc": "workspace:*",

apps/code/src/main/services/file-watcher/service.ts

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { existsSync } from "node:fs";
22
import fs from "node:fs/promises";
33
import path from "node:path";
44
import * as watcher from "@parcel/watcher";
5-
import { app } from "electron";
65
import { inject, injectable } from "inversify";
76
import { MAIN_TOKENS } from "../../di/tokens";
87
import { logger } from "../../utils/logger";
@@ -70,9 +69,6 @@ export class FileWatcherService extends TypedEventEmitter<FileWatcherEvents> {
7069
async startWatching(repoPath: string): Promise<void> {
7170
if (this.watchers.has(repoPath)) return;
7271

73-
await fs.mkdir(this.snapshotsDir, { recursive: true });
74-
await this.emitChangesSinceSnapshot(repoPath);
75-
7672
const pending: PendingChanges = {
7773
dirs: new Set(),
7874
files: new Set(),
@@ -107,53 +103,13 @@ export class FileWatcherService extends TypedEventEmitter<FileWatcherEvents> {
107103
if (!w) return;
108104

109105
if (w.pending.timer) clearTimeout(w.pending.timer);
110-
await this.saveSnapshot(repoPath);
111106
await this.watcherRegistry.unregister(w.filesId);
112107
for (const gitId of w.gitIds) {
113108
await this.watcherRegistry.unregister(gitId);
114109
}
115110
this.watchers.delete(repoPath);
116111
}
117112

118-
private get snapshotsDir(): string {
119-
return path.join(app.getPath("userData"), "snapshots");
120-
}
121-
122-
private snapshotPath(repoPath: string): string {
123-
return path.join(
124-
this.snapshotsDir,
125-
`${Buffer.from(repoPath).toString("base64url")}.snapshot`,
126-
);
127-
}
128-
129-
private async saveSnapshot(repoPath: string): Promise<void> {
130-
try {
131-
await watcher.writeSnapshot(repoPath, this.snapshotPath(repoPath), {
132-
ignore: IGNORE_PATTERNS,
133-
});
134-
} catch (error) {
135-
log.error("Failed to write snapshot:", error);
136-
}
137-
}
138-
139-
private async emitChangesSinceSnapshot(repoPath: string): Promise<void> {
140-
const snapshotPath = this.snapshotPath(repoPath);
141-
try {
142-
await fs.access(snapshotPath);
143-
} catch {
144-
return;
145-
}
146-
147-
const events = await watcher.getEventsSince(repoPath, snapshotPath, {
148-
ignore: IGNORE_PATTERNS,
149-
});
150-
151-
const affectedDirs = new Set(events.map((e) => path.dirname(e.path)));
152-
for (const dirPath of affectedDirs) {
153-
this.emit(FileWatcherEvent.DirectoryChanged, { repoPath, dirPath });
154-
}
155-
}
156-
157113
private async watchFiles(
158114
repoPath: string,
159115
pending: PendingChanges,

apps/code/src/renderer/features/settings/components/sections/GeneralSettings.tsx

Lines changed: 1 addition & 196 deletions
Original file line numberDiff line numberDiff line change
@@ -16,55 +16,23 @@ import {
1616
Slider,
1717
Switch,
1818
Text,
19-
TextField,
2019
} from "@radix-ui/themes";
2120
import { useTRPC } from "@renderer/trpc";
2221
import { getCloudUrlFromRegion } from "@shared/constants/oauth";
2322
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
24-
import { useSettingsStore as useTerminalSettingsStore } from "@stores/settingsStore";
2523
import type { ThemePreference } from "@stores/themeStore";
2624
import { useThemeStore } from "@stores/themeStore";
2725
import { useMutation, useQuery } from "@tanstack/react-query";
2826
import { track } from "@utils/analytics";
2927
import { playCompletionSound } from "@utils/sounds";
30-
import { useCallback, useEffect, useRef, useState } from "react";
28+
import { useCallback, useEffect } from "react";
3129
import { toast } from "sonner";
3230

33-
const CUSTOM_TERMINAL_FONT_VALUE = "custom";
34-
const CUSTOM_TERMINAL_FONT_COMMIT_DELAY_MS = 400;
35-
const TERMINAL_FONT_PRESETS = [
36-
{
37-
label: "System monospace",
38-
value: "monospace",
39-
},
40-
{
41-
label: "MesloLGL Nerd Font Mono",
42-
value: '"MesloLGL Nerd Font Mono", monospace',
43-
},
44-
{
45-
label: "JetBrains Mono",
46-
value: '"JetBrains Mono", monospace',
47-
},
48-
];
49-
5031
export function GeneralSettings() {
5132
const trpcReact = useTRPC();
5233
// Appearance state
5334
const theme = useThemeStore((state) => state.theme);
5435
const setTheme = useThemeStore((state) => state.setTheme);
55-
const terminalFontFamily = useTerminalSettingsStore(
56-
(state) => state.terminalFontFamily,
57-
);
58-
const terminalFontFamilyLoaded = useTerminalSettingsStore(
59-
(state) => state.terminalFontFamilyLoaded,
60-
);
61-
const loadTerminalFontFamily = useTerminalSettingsStore(
62-
(state) => state.loadTerminalFontFamily,
63-
);
64-
const setTerminalFontFamily = useTerminalSettingsStore(
65-
(state) => state.setTerminalFontFamily,
66-
);
67-
6836
// Power state
6937
const { preventSleepWhileRunning, setPreventSleepWhileRunning } =
7038
useSettingsStore();
@@ -150,35 +118,6 @@ export function GeneralSettings() {
150118
[desktopNotifications, setDesktopNotifications],
151119
);
152120

153-
const [customTerminalFont, setCustomTerminalFont] = useState<string>("");
154-
const customFontSaveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
155-
null,
156-
);
157-
158-
useEffect(() => {
159-
if (!terminalFontFamilyLoaded) {
160-
loadTerminalFontFamily();
161-
}
162-
}, [terminalFontFamilyLoaded, loadTerminalFontFamily]);
163-
164-
useEffect(() => {
165-
return () => {
166-
if (customFontSaveTimeoutRef.current) {
167-
clearTimeout(customFontSaveTimeoutRef.current);
168-
customFontSaveTimeoutRef.current = null;
169-
}
170-
};
171-
}, []);
172-
173-
useEffect(() => {
174-
const matchesPreset = TERMINAL_FONT_PRESETS.some(
175-
(preset) => preset.value === terminalFontFamily,
176-
);
177-
if (!matchesPreset) {
178-
setCustomTerminalFont(terminalFontFamily);
179-
}
180-
}, [terminalFontFamily]);
181-
182121
// Appearance handlers
183122
const handleThemeChange = useCallback(
184123
(value: ThemePreference) => {
@@ -192,91 +131,6 @@ export function GeneralSettings() {
192131
[theme, setTheme],
193132
);
194133

195-
const clearCustomFontSaveTimeout = useCallback(() => {
196-
if (customFontSaveTimeoutRef.current) {
197-
clearTimeout(customFontSaveTimeoutRef.current);
198-
customFontSaveTimeoutRef.current = null;
199-
}
200-
}, []);
201-
202-
const commitCustomTerminalFont = useCallback(
203-
(value: string) => {
204-
const normalizedValue = value.trim();
205-
if (!normalizedValue) {
206-
return;
207-
}
208-
209-
const previousValue = terminalFontFamily.trim() || "monospace";
210-
if (normalizedValue === previousValue) {
211-
return;
212-
}
213-
214-
track(ANALYTICS_EVENTS.SETTING_CHANGED, {
215-
setting_name: "terminal_font_family",
216-
new_value: normalizedValue,
217-
old_value: previousValue,
218-
});
219-
220-
setTerminalFontFamily(normalizedValue);
221-
},
222-
[setTerminalFontFamily, terminalFontFamily],
223-
);
224-
225-
const handleTerminalFontChange = useCallback(
226-
(value: string) => {
227-
clearCustomFontSaveTimeout();
228-
229-
if (value === CUSTOM_TERMINAL_FONT_VALUE) {
230-
if (!customTerminalFont.trim()) {
231-
setTerminalFontFamily("");
232-
return;
233-
}
234-
235-
commitCustomTerminalFont(customTerminalFont);
236-
return;
237-
}
238-
239-
if (value === terminalFontFamily) {
240-
return;
241-
}
242-
243-
track(ANALYTICS_EVENTS.SETTING_CHANGED, {
244-
setting_name: "terminal_font_family",
245-
new_value: value,
246-
old_value: terminalFontFamily,
247-
});
248-
249-
setTerminalFontFamily(value);
250-
},
251-
[
252-
clearCustomFontSaveTimeout,
253-
commitCustomTerminalFont,
254-
customTerminalFont,
255-
setTerminalFontFamily,
256-
terminalFontFamily,
257-
],
258-
);
259-
260-
const handleCustomTerminalFontChange = useCallback(
261-
(value: string) => {
262-
setCustomTerminalFont(value);
263-
clearCustomFontSaveTimeout();
264-
customFontSaveTimeoutRef.current = setTimeout(() => {
265-
commitCustomTerminalFont(value);
266-
}, CUSTOM_TERMINAL_FONT_COMMIT_DELAY_MS);
267-
},
268-
[clearCustomFontSaveTimeout, commitCustomTerminalFont],
269-
);
270-
271-
const handleCustomTerminalFontBlur = useCallback(() => {
272-
clearCustomFontSaveTimeout();
273-
commitCustomTerminalFont(customTerminalFont);
274-
}, [
275-
clearCustomFontSaveTimeout,
276-
commitCustomTerminalFont,
277-
customTerminalFont,
278-
]);
279-
280134
// Chat handlers
281135
const handleCompletionSoundChange = useCallback(
282136
(value: CompletionSound) => {
@@ -354,12 +208,6 @@ export function GeneralSettings() {
354208
[hedgehogMode, setHedgehogMode],
355209
);
356210

357-
const terminalFontSelection = TERMINAL_FONT_PRESETS.some(
358-
(preset) => preset.value === terminalFontFamily,
359-
)
360-
? terminalFontFamily
361-
: CUSTOM_TERMINAL_FONT_VALUE;
362-
363211
return (
364212
<Flex direction="column">
365213
{/* Appearance */}
@@ -385,49 +233,6 @@ export function GeneralSettings() {
385233
</Select.Root>
386234
</SettingRow>
387235

388-
<SettingRow
389-
label="Terminal font"
390-
description="Uses locally installed fonts. Nerd fonts are recommended for prompt glyphs"
391-
noBorder={terminalFontSelection !== CUSTOM_TERMINAL_FONT_VALUE}
392-
>
393-
<Select.Root
394-
value={terminalFontSelection}
395-
onValueChange={handleTerminalFontChange}
396-
size="1"
397-
>
398-
<Select.Trigger style={{ minWidth: "140px" }} />
399-
<Select.Content>
400-
{TERMINAL_FONT_PRESETS.map((preset) => (
401-
<Select.Item key={preset.value} value={preset.value}>
402-
{preset.label}
403-
</Select.Item>
404-
))}
405-
<Select.Item value={CUSTOM_TERMINAL_FONT_VALUE}>
406-
Custom font family
407-
</Select.Item>
408-
</Select.Content>
409-
</Select.Root>
410-
</SettingRow>
411-
412-
{terminalFontSelection === CUSTOM_TERMINAL_FONT_VALUE && (
413-
<SettingRow label="Custom font family" noBorder>
414-
<Flex direction="column" gap="1" style={{ minWidth: "200px" }}>
415-
<TextField.Root
416-
size="1"
417-
placeholder="Enter font family"
418-
value={customTerminalFont}
419-
onChange={(event) =>
420-
handleCustomTerminalFontChange(event.target.value)
421-
}
422-
onBlur={handleCustomTerminalFontBlur}
423-
/>
424-
<Text size="1" color="gray">
425-
Example: MesloLGL Nerd Font Mono
426-
</Text>
427-
</Flex>
428-
</SettingRow>
429-
)}
430-
431236
{/* Notifications */}
432237
<Text
433238
size="2"

apps/code/src/renderer/features/terminal/components/Terminal.tsx

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Box } from "@radix-ui/themes";
22
import { useTRPC } from "@renderer/trpc";
3-
import { useSettingsStore } from "@stores/settingsStore";
43
import { useThemeStore } from "@stores/themeStore";
54
import "@xterm/xterm/css/xterm.css";
65

@@ -32,15 +31,6 @@ export function Terminal({
3231
const trpcReact = useTRPC();
3332
const terminalRef = useRef<HTMLDivElement>(null);
3433
const isDarkMode = useThemeStore((state) => state.isDarkMode);
35-
const terminalFontFamily = useSettingsStore(
36-
(state) => state.terminalFontFamily,
37-
);
38-
const terminalFontFamilyLoaded = useSettingsStore(
39-
(state) => state.terminalFontFamilyLoaded,
40-
);
41-
const loadTerminalFontFamily = useSettingsStore(
42-
(state) => state.loadTerminalFontFamily,
43-
);
4434

4535
// Create instance (idempotent)
4636
useEffect(() => {
@@ -72,16 +62,6 @@ export function Terminal({
7262
terminalManager.setTheme(isDarkMode);
7363
}, [isDarkMode]);
7464

75-
useEffect(() => {
76-
if (!terminalFontFamilyLoaded) {
77-
loadTerminalFontFamily();
78-
}
79-
}, [terminalFontFamilyLoaded, loadTerminalFontFamily]);
80-
81-
useEffect(() => {
82-
terminalManager.setFontFamily(terminalFontFamily);
83-
}, [terminalFontFamily]);
84-
8565
// Subscribe to shell data events
8666
useSubscription(
8767
trpcReact.shell.onData.subscriptionOptions(

0 commit comments

Comments
 (0)