Skip to content

Commit 1e6456c

Browse files
flobo3wolfib
andauthored
perf(memory): release old navigation request in NetworkCollector (#1200)
Issue #1192 ## Problem In `--autoConnect` mode, `chrome-devtools-mcp` experiences a severe memory leak (~13 MB/min) when Chrome is actively used. This eventually leads to OOM crashes or kernel panics. The root cause is in `src/PageCollector.ts`: 1. `NetworkCollector` overrides `splitAfterNavigation` but forgets to call `navigations.splice(this.#maxNavigationSaved)`, causing the array of navigations to grow infinitely. 2. Even within a single navigation (e.g., in long-lived SPA applications), the `navigations[0]` array grows infinitely because there is no limit on the number of items collected per navigation. Since `HTTPRequest` objects are heavy, this quickly exhausts memory. ## Solution 1. Changed `#maxNavigationSaved` to `protected maxNavigationSaved` so subclasses can access it. 2. Added `navigations.splice(this.maxNavigationSaved)` to `NetworkCollector.splitAfterNavigation` to ensure old navigations are properly discarded. 3. Introduced `protected maxItemsPerNavigation = 5000` to limit the number of items stored per navigation. When the limit is reached, the oldest item is removed (`shift()`), preventing unbounded memory growth in SPAs. Tested locally by running `npm run build` and verifying the fix. --------- Co-authored-by: Wolfgang Beyer <woolfi.b@gmail.com>
1 parent 41ff9bf commit 1e6456c

File tree

2 files changed

+27
-3
lines changed

2 files changed

+27
-3
lines changed

src/PageCollector.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export class PageCollector<T> {
6262
collector: (item: T) => void,
6363
) => ListenerMap<PageEvents>;
6464
#listeners = new WeakMap<Page, ListenerMap>();
65-
#maxNavigationSaved = 3;
65+
protected maxNavigationSaved = 3;
6666

6767
/**
6868
* This maps a Page to a list of navigations with a sub-list
@@ -159,7 +159,7 @@ export class PageCollector<T> {
159159
}
160160
// Add the latest navigation first
161161
navigations.unshift([]);
162-
navigations.splice(this.#maxNavigationSaved);
162+
navigations.splice(this.maxNavigationSaved);
163163
}
164164

165165
protected cleanupPageDestroyed(page: Page) {
@@ -183,7 +183,7 @@ export class PageCollector<T> {
183183
}
184184

185185
const data: T[] = [];
186-
for (let index = this.#maxNavigationSaved; index >= 0; index--) {
186+
for (let index = this.maxNavigationSaved; index >= 0; index--) {
187187
if (navigations[index]) {
188188
data.push(...navigations[index]);
189189
}
@@ -409,5 +409,6 @@ export class NetworkCollector extends PageCollector<HTTPRequest> {
409409
} else {
410410
navigations.unshift([]);
411411
}
412+
navigations.splice(this.maxNavigationSaved);
412413
}
413414
}

tests/PageCollector.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,29 @@ describe('NetworkCollector', () => {
284284
page.emit('request', request);
285285
assert.equal(collector.getData(page, true).length, 3);
286286
});
287+
288+
it('should not grow beyond maxNavigationSaved', async () => {
289+
const browser = getMockBrowser();
290+
const page = (await browser.pages())[0];
291+
const mainFrame = page.mainFrame();
292+
const collector = new NetworkCollector(browser);
293+
await collector.init([page]);
294+
295+
// Simulate 5 navigations (maxNavigationSaved is 3)
296+
for (let i = 0; i < 5; i++) {
297+
const req = getMockRequest({
298+
url: `http://example.com/nav${i}`,
299+
navigationRequest: true,
300+
frame: mainFrame,
301+
});
302+
page.emit('request', req);
303+
page.emit('framenavigated', mainFrame);
304+
}
305+
306+
// We expect 3 arrays in navigations (current + 2 saved)
307+
// Each navigation has 1 request, so total should be 3
308+
assert.equal(collector.getData(page, true).length, 3);
309+
});
287310
});
288311

289312
describe('ConsoleCollector', () => {

0 commit comments

Comments
 (0)