Container Tab Groups: Memory leak with User Agent overrides enabled

I have Container Tab Groups installed with “User Agent overrides” option enabled. In this setting, Firefox occationary consumes too much memory and goes unresponsive.

This is about:memory screenshot.

memory-report.json.gz.zip (709.9 KB)

Corresponding source for the add-on is here:

I could not locate the exact code that is causing the memory leak and would like to know if this is a Firefox bug, or the add-on bug.

Environment: Firefox 111.0 (macOS, arm64)

This issue is reproduced on my friend’s laptop.

1 Like

I added a previously-missing await to source and seeing if this changes anything.

Memory leak still seems to happen. Does anyone know what kind of coding practice can lead to this kind of memory leak? What are these “File” memory allocations?

This looks like a bug in the add-on.

The contentScripts.register API internally converts the received code parameter to a Blob, which is represented in the about:memory report as file(length=...).

I suggest to carefully examine your use of the contentScripts.register API, and to check whether that can explain the memory usage. In particular, check whether the unregister() method is always called.

From a quick look at your code, I can spot one hazard already: at https://github.com/menhera-org/TabArray/blob/4b0dd78e9d283ed6c5b2e2c9037c191f558fa5e2/src/overrides/ContentScriptRegistrar.ts#L43-L44,


    await this.unregister(cookieStoreId);
    // The above is basically:
    // await this.contentScripts.get(cookieStoreId)?.unregister();
    // this.contentScripts.delete(cookieStoreId);
    this.contentScripts.set(cookieStoreId, await browser.contentScripts.register({

It is clear that the intended logic is “unregister before registering again”.

But the implementation is not atomic due to the async logic, and it is possible for register() to be called without ever having a matching unregister. That may be more obvious if we unroll your code to emphasize the presence of await on separate lines:

/*1*/ let promise1 = this.contentScripts.get(cookieStoreId)?.unregister();
/*2*/ await promise1;
/*3*/ this.contentScripts.delete(cookieStoreId);
/*4*/ let promise2 = browser.contentScripts.register({ ... });
/*5*/ let registerResult = await promise2;
/*6*/ this.contentScripts.set(cookieStoreId, registerResult);

When the above logic is invoked twice without waiting for the completion of the first operation, then you will end up calling unregister() for the initial script twice (/*1*/), followed by registering two new scripts (/*4*/). Finally, at /*6*/ the registered script is saved twice after each other, where the first one is no longer accessible by any code.

To fix this issue, you need to ensure that the above logic does not run concurrently, OR that unregister() is guaranteed to be called when needed.

2 Likes

Thank you for the detailed reply, but there is one thing that is not clear. In the above screenshot. there are tons of blobs with sizes of tens of megabytes, but I do not know how content scripts can grow to that large sizes. Is there any description for this phenomenon?

I added a check to prevent registerAll() from running concurrently.