Skip to content

Commit e3f1b4c

Browse files
authored
perf(browsers): reduce synchronous existsSync calls in file discovery (#495)
perf(browsers): replace existsSync loops with glob - Replace iterative existsSync calls with single fg.sync() glob patterns in profile discovery (BrowserAvailability.ts and EnhancedCookieQueryService.ts) - Cache browser installation checks at module level since installations don't change during process lifetime - Update tests to mock fast-glob instead of readdirSync Fixes #489
1 parent 2ee86e4 commit e3f1b4c

File tree

3 files changed

+73
-121
lines changed

3 files changed

+73
-121
lines changed

src/core/browsers/BrowserAvailability.ts

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
* @module BrowserAvailability
44
*/
55

6-
import { existsSync, readdirSync } from "node:fs";
6+
import { existsSync } from "node:fs";
77
import { homedir } from "node:os";
88
import { join } from "node:path";
99

10+
import fg from "fast-glob";
11+
1012
import { createTaggedLogger } from "@utils/logHelpers";
1113
import { getPlatform, isLinux, isMacOS, isWindows } from "@utils/platformUtils";
1214
import { execSimple } from "@utils/execSimple";
@@ -259,17 +261,26 @@ export const FIREFOX_DATA_DIRS: Partial<Record<string, string[]>> = {
259261
linux: [join(homedir(), ".mozilla", "firefox")],
260262
};
261263

264+
/** Module-level cache for browser installation checks — installations don't change during process lifetime */
265+
const installCache = new Map<BrowserType, boolean>();
266+
262267
/**
263268
* Checks if a browser is installed by looking for its paths
264269
* @param browser - The browser type to check
265270
* @returns True if the browser is installed
266271
*/
267272
function checkBrowserInstalled(browser: BrowserType): boolean {
273+
const cached = installCache.get(browser);
274+
if (cached !== undefined) {
275+
return cached;
276+
}
277+
268278
const platform = getPlatform();
269279

270280
// Defensive check: handle unexpected platform values gracefully
271281
// Check if platform exists in BROWSER_PATHS before accessing
272282
if (!(platform in BROWSER_PATHS)) {
283+
installCache.set(browser, false);
273284
return false;
274285
}
275286

@@ -282,10 +293,12 @@ function checkBrowserInstalled(browser: BrowserType): boolean {
282293
for (const path of paths) {
283294
if (existsSync(path)) {
284295
logger.debug("Found browser installation", { browser, path });
296+
installCache.set(browser, true);
285297
return true;
286298
}
287299
}
288300

301+
installCache.set(browser, false);
289302
return false;
290303
}
291304

@@ -370,27 +383,16 @@ export async function getBrowserVersionAsync(
370383
* @returns Array of profile paths found
371384
*/
372385
function findChromiumProfiles(basePath: string): string[] {
373-
const profiles: string[] = [];
374-
375386
if (!existsSync(basePath)) {
376-
return profiles;
377-
}
378-
379-
// Look for Default profile
380-
const defaultProfile = join(basePath, "Default");
381-
if (existsSync(defaultProfile)) {
382-
profiles.push(defaultProfile);
383-
}
384-
385-
// Look for numbered profiles (Profile 1, Profile 2, etc.)
386-
for (let i = 1; i <= 10; i++) {
387-
const profilePath = join(basePath, `Profile ${i}`);
388-
if (existsSync(profilePath)) {
389-
profiles.push(profilePath);
390-
}
387+
return [];
391388
}
392389

393-
return profiles;
390+
// Single glob replaces 12 existsSync calls (Default + Profile 1..10)
391+
return fg.sync(["Default", "Profile *"], {
392+
cwd: basePath,
393+
onlyDirectories: true,
394+
absolute: true,
395+
});
394396
}
395397

396398
/**
@@ -399,22 +401,12 @@ function findChromiumProfiles(basePath: string): string[] {
399401
* @returns Array of profile paths found
400402
*/
401403
function findFirefoxProfilesInPath(basePath: string): string[] {
402-
const profiles: string[] = [];
403-
404-
if (!existsSync(basePath)) {
405-
return profiles;
406-
}
407-
408-
const profileDirs: string[] = readdirSync(basePath);
409-
for (const dir of profileDirs) {
410-
// Firefox profiles usually have .default or .default-release suffix
411-
if (dir.includes("default")) {
412-
const profilePath = join(basePath, dir);
413-
profiles.push(profilePath);
414-
}
415-
}
416-
417-
return profiles;
404+
// Single glob replaces existsSync + readdirSync + manual filter
405+
return fg.sync(["*default*"], {
406+
cwd: basePath,
407+
onlyDirectories: true,
408+
absolute: true,
409+
});
418410
}
419411

420412
/**

src/core/browsers/sql/EnhancedCookieQueryService.ts

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
* Combines query building, connection management, and validation in a unified interface
44
*/
55

6-
import { existsSync, readdirSync } from "node:fs";
6+
import { existsSync } from "node:fs";
77
import { join } from "node:path";
88
import { homedir } from "node:os";
9+
10+
import fg from "fast-glob";
911
import type { SqliteDatabase } from "./adapters/DatabaseAdapter";
1012
import { createTaggedLogger, logError } from "@utils/logHelpers";
1113
import { getPlatform } from "@utils/platformUtils";
@@ -457,21 +459,11 @@ export class EnhancedCookieQueryService {
457459
return [];
458460
}
459461

460-
const cookieFiles: string[] = [];
461-
462-
// Check Default profile
463-
const defaultCookies = join(dataDir, "Default", "Cookies");
464-
if (existsSync(defaultCookies)) {
465-
cookieFiles.push(defaultCookies);
466-
}
467-
468-
// Check numbered profiles (Profile 1 through Profile 10)
469-
for (let i = 1; i <= 10; i++) {
470-
const profileCookies = join(dataDir, `Profile ${i}`, "Cookies");
471-
if (existsSync(profileCookies)) {
472-
cookieFiles.push(profileCookies);
473-
}
474-
}
462+
// Single glob replaces 12 existsSync calls (Default/Cookies + Profile 1..10/Cookies)
463+
const cookieFiles = fg.sync(["{Default,Profile *}/Cookies"], {
464+
cwd: dataDir,
465+
absolute: true,
466+
});
475467

476468
logger.debug("Discovered Chromium cookie files", {
477469
browser,
@@ -514,23 +506,11 @@ export class EnhancedCookieQueryService {
514506
return [];
515507
}
516508

517-
const cookieFiles: string[] = [];
518-
519-
try {
520-
const entries = readdirSync(profilesDir);
521-
for (const entry of entries) {
522-
if (entry.includes("default")) {
523-
const cookiesPath = join(profilesDir, entry, "cookies.sqlite");
524-
if (existsSync(cookiesPath)) {
525-
cookieFiles.push(cookiesPath);
526-
}
527-
}
528-
}
529-
} catch {
530-
logger.debug("Failed to read Firefox profiles directory", {
531-
profilesDir,
532-
});
533-
}
509+
// Single glob replaces readdirSync + existsSync loop
510+
const cookieFiles = fg.sync(["*default*/cookies.sqlite"], {
511+
cwd: profilesDir,
512+
absolute: true,
513+
});
534514

535515
logger.debug("Discovered Firefox cookie files", {
536516
count: cookieFiles.length,

src/core/browsers/sql/__tests__/EnhancedCookieQueryService.test.ts

Lines changed: 33 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ jest.mock("../adapters/DatabaseAdapter");
2727
// Mock node:fs for file discovery tests
2828
jest.mock("node:fs", () => ({
2929
existsSync: jest.fn().mockReturnValue(false),
30-
readdirSync: jest.fn().mockReturnValue([]),
30+
}));
31+
32+
// Mock fast-glob for file discovery tests (replaces readdirSync-based iteration)
33+
jest.mock("fast-glob", () => ({
34+
sync: jest.fn().mockReturnValue([]),
3135
}));
3236

3337
// Mock getPlatform to return "darwin" so path lookups are deterministic across CI
@@ -44,7 +48,7 @@ describe("EnhancedCookieQueryService", () => {
4448
let mockStmt: { all: jest.Mock; get: jest.Mock; run: jest.Mock };
4549
let manager: DatabaseConnectionManager;
4650
let mockExistsSync: jest.Mock;
47-
let mockReaddirSync: jest.Mock;
51+
let mockFgSync: jest.Mock;
4852

4953
beforeEach(() => {
5054
mockStmt = {
@@ -65,9 +69,11 @@ describe("EnhancedCookieQueryService", () => {
6569

6670
const fs = require("node:fs");
6771
mockExistsSync = fs.existsSync as jest.Mock;
68-
mockReaddirSync = fs.readdirSync as jest.Mock;
6972
mockExistsSync.mockReturnValue(false);
70-
mockReaddirSync.mockReturnValue([]);
73+
74+
const fg = require("fast-glob");
75+
mockFgSync = fg.sync as jest.Mock;
76+
mockFgSync.mockReturnValue([]);
7177

7278
manager = new DatabaseConnectionManager({
7379
maxConnections: 2,
@@ -156,21 +162,17 @@ describe("EnhancedCookieQueryService", () => {
156162

157163
describe("discoverBrowserFiles via queryCookies", () => {
158164
it("discovers Chromium cookie files from Default profile", async () => {
165+
// Data dir must exist for the guard check
159166
mockExistsSync.mockImplementation((...args: unknown[]) => {
160167
const p = String(args[0]).replace(/\\/g, "/");
161-
if (p.endsWith("/Default/Cookies")) {
162-
return true;
163-
}
164-
// Data dir must exist
165-
if (
168+
return (
166169
p.includes("Chrome") &&
167170
!p.includes("Default") &&
168171
!p.includes("Profile")
169-
) {
170-
return true;
171-
}
172-
return false;
172+
);
173173
});
174+
// fg.sync returns the discovered cookie files
175+
mockFgSync.mockReturnValue(["/fake/Chrome/Default/Cookies"]);
174176

175177
const options: EnhancedQueryOptions = {
176178
browser: "chrome",
@@ -184,20 +186,14 @@ describe("EnhancedCookieQueryService", () => {
184186
});
185187

186188
it("discovers Firefox cookie files from profile directories", async () => {
189+
// Profiles dir must exist for the guard check
187190
mockExistsSync.mockImplementation((...args: unknown[]) => {
188191
const p = String(args[0]).replace(/\\/g, "/");
189-
if (p.includes("Firefox") || p.includes("firefox")) {
190-
return true;
191-
}
192-
if (p.endsWith("abc123.default/cookies.sqlite")) {
193-
return true;
194-
}
195-
return false;
192+
return p.includes("Firefox") || p.includes("firefox");
196193
});
197-
mockReaddirSync.mockReturnValue([
198-
"abc123.default",
199-
"profiles.ini",
200-
"xyz789.default-release",
194+
// fg.sync returns the discovered cookie files
195+
mockFgSync.mockReturnValue([
196+
"/fake/Firefox/Profiles/abc123.default/cookies.sqlite",
201197
]);
202198

203199
const options: EnhancedQueryOptions = {
@@ -207,27 +203,20 @@ describe("EnhancedCookieQueryService", () => {
207203
};
208204

209205
await service.queryCookies(options);
210-
// Should have found at least the abc123.default profile
211-
expect(mockExistsSync).toHaveBeenCalledWith(
212-
expect.stringContaining("cookies.sqlite"),
213-
);
206+
// Should have attempted to query the discovered file
207+
expect(mockDb.prepare).toHaveBeenCalled();
214208
});
215209

216210
it("discovers Brave cookie files from Default profile", async () => {
217211
mockExistsSync.mockImplementation((...args: unknown[]) => {
218212
const p = String(args[0]).replace(/\\/g, "/");
219-
if (p.endsWith("/Default/Cookies")) {
220-
return true;
221-
}
222-
if (
213+
return (
223214
p.includes("Brave") &&
224215
!p.includes("Default") &&
225216
!p.includes("Profile")
226-
) {
227-
return true;
228-
}
229-
return false;
217+
);
230218
});
219+
mockFgSync.mockReturnValue(["/fake/Brave/Default/Cookies"]);
231220

232221
const options: EnhancedQueryOptions = {
233222
browser: "brave",
@@ -242,18 +231,11 @@ describe("EnhancedCookieQueryService", () => {
242231
it("discovers Arc cookie files from Default profile", async () => {
243232
mockExistsSync.mockImplementation((...args: unknown[]) => {
244233
const p = String(args[0]).replace(/\\/g, "/");
245-
if (p.endsWith("/Default/Cookies")) {
246-
return true;
247-
}
248-
if (
249-
p.includes("Arc") &&
250-
!p.includes("Default") &&
251-
!p.includes("Profile")
252-
) {
253-
return true;
254-
}
255-
return false;
234+
return (
235+
p.includes("Arc") && !p.includes("Default") && !p.includes("Profile")
236+
);
256237
});
238+
mockFgSync.mockReturnValue(["/fake/Arc/Default/Cookies"]);
257239

258240
const options: EnhancedQueryOptions = {
259241
browser: "arc",
@@ -292,16 +274,14 @@ describe("EnhancedCookieQueryService", () => {
292274
it("returns empty array when Chromium data dir exists but no profiles found", async () => {
293275
mockExistsSync.mockImplementation((...args: unknown[]) => {
294276
const p = String(args[0]).replace(/\\/g, "/");
295-
// Data dir exists but no profile Cookies files
296-
if (
277+
return (
297278
p.includes("Chrome") &&
298279
!p.includes("Default") &&
299280
!p.includes("Profile")
300-
) {
301-
return true;
302-
}
303-
return false;
281+
);
304282
});
283+
// fg.sync returns empty — no profile directories found
284+
mockFgSync.mockReturnValue([]);
305285

306286
const options: EnhancedQueryOptions = {
307287
browser: "chrome",

0 commit comments

Comments
 (0)