Skip to content

Commit 8b8146d

Browse files
committed
added concurrency to fetches for faster audit sync
1 parent 285614a commit 8b8146d

3 files changed

Lines changed: 50 additions & 19 deletions

File tree

features/audit-scraping/background-controller.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ export interface AuditBatchDependencies {
2222
scrapeAudit: (auditId: string, tabId: number) => Promise<CachedAuditData>;
2323
saveAudit: (auditId: string, audit: CachedAuditData) => Promise<void>;
2424
broadcast: (state: "started" | "complete") => Promise<void>;
25-
delay?: (milliseconds: number) => Promise<void>;
2625
scrapeTimeoutMs?: number;
27-
requestDelayMs?: number;
26+
concurrency?: number;
2827
}
2928

3029
export class AuditBatchController {
@@ -59,8 +58,17 @@ export class AuditBatchController {
5958
const result: AuditBatchResult = { succeeded: [], failed: [] };
6059
await this.dependencies.broadcast("started");
6160

62-
try {
63-
for (const [index, auditId] of auditIds.entries()) {
61+
// A few plain page fetches in flight at once — fewer than a normal page
62+
// load opens against one host — keeps the "Syncing" window short.
63+
const queue = [...auditIds];
64+
let aborted = false;
65+
66+
const worker = async (): Promise<void> => {
67+
for (
68+
let auditId = queue.shift();
69+
auditId !== undefined && !aborted;
70+
auditId = queue.shift()
71+
) {
6472
try {
6573
const audit = await this.scrapeWithTimeout(auditId, tabId);
6674
await this.dependencies.saveAudit(auditId, audit);
@@ -71,20 +79,25 @@ export class AuditBatchController {
7179
// A dead session fails every remaining audit the same way; stop
7280
// instead of hammering the login redirect.
7381
if (error instanceof Error && error.message === "AUTH_REQUIRED") {
74-
result.failed.push(...auditIds.slice(index + 1));
75-
break;
76-
}
77-
}
78-
79-
if (index < auditIds.length - 1) {
80-
const delayMs = this.dependencies.requestDelayMs ?? 150;
81-
if (this.dependencies.delay) {
82-
await this.dependencies.delay(delayMs);
83-
} else {
84-
await new Promise((resolve) => setTimeout(resolve, delayMs));
82+
aborted = true;
83+
result.failed.push(...queue.splice(0));
8584
}
8685
}
8786
}
87+
};
88+
89+
try {
90+
await Promise.all(
91+
Array.from(
92+
{
93+
length: Math.min(
94+
this.dependencies.concurrency ?? 3,
95+
auditIds.length,
96+
),
97+
},
98+
() => worker(),
99+
),
100+
);
88101
} finally {
89102
await this.dependencies.broadcast("complete");
90103
const summary = `Audit batch complete: ${result.succeeded.length} succeeded, ${result.failed.length} failed`;

features/popup/popup-app.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,7 @@ export default function App() {
102102
});
103103
};
104104

105-
// The background owns the auth checks: it refuses on a known-dead session
106-
// and the run's own form fetch is a live check, either way opening the
107-
// login page — no need for a slow probe from here first.
105+
// The background owns the auth checks:
108106
const handleRerunAudit = async () => {
109107
setRunningAudit(true);
110108
try {

tests/audit-scraping/background-controller.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ function createController(
1515
scrapeAudit: async () => audit,
1616
saveAudit: async () => {},
1717
broadcast: async () => {},
18-
delay: async () => {},
1918
scrapeTimeoutMs: 20,
2019
...overrides,
2120
});
@@ -69,6 +68,7 @@ describe("audit batch controller", () => {
6968
scrapes++;
7069
throw new Error("AUTH_REQUIRED");
7170
},
71+
concurrency: 1,
7272
});
7373

7474
controller.start(["1", "2", "3"], TAB_ID);
@@ -79,6 +79,26 @@ describe("audit batch controller", () => {
7979
expect(scrapes).toBe(1);
8080
});
8181

82+
test("fetches audits in parallel up to the concurrency limit", async () => {
83+
let inFlight = 0;
84+
let maxInFlight = 0;
85+
const controller = createController({
86+
scrapeAudit: async () => {
87+
inFlight++;
88+
maxInFlight = Math.max(maxInFlight, inFlight);
89+
await new Promise((resolve) => setTimeout(resolve, 5));
90+
inFlight--;
91+
return audit;
92+
},
93+
concurrency: 2,
94+
});
95+
96+
controller.start(["1", "2", "3", "4"], TAB_ID);
97+
const result = await controller.waitForIdle();
98+
expect(result?.succeeded.toSorted()).toEqual(["1", "2", "3", "4"]);
99+
expect(maxInFlight).toBe(2);
100+
});
101+
82102
test("times out a scrape that never responds", async () => {
83103
const controller = createController({
84104
scrapeAudit: () => new Promise<never>(() => {}),

0 commit comments

Comments
 (0)