Skip to content

fix: prevent favicon data retention in heartbeat debug logs#223

Merged
ErikBjare merged 2 commits intoActivityWatch:masterfrom
TimeToBuildBob:fix/favicon-memleak-abortcontroller
Apr 16, 2026
Merged

fix: prevent favicon data retention in heartbeat debug logs#223
ErikBjare merged 2 commits intoActivityWatch:masterfrom
TimeToBuildBob:fix/favicon-memleak-abortcontroller

Conversation

@TimeToBuildBob
Copy link
Copy Markdown
Contributor

Problem

The heartbeat module passes entire Tab objects to console.debug(). In Firefox, Tab.favIconUrl is a base64 data URI (often 10-20 KB). Browser console buffers retain references to logged objects, so over thousands of heartbeats the retained Tab references accumulate hundreds of MBs of duplicate favicon data in memory.

From the about:memory report in #222:

  • ~761 MB of strings — all base64-encoded favicon/icon data
  • One favicon: 8,804 copies × ~22 KB each = 189 MB
  • Another: 17,426 copies × ~9 KB each = 157 MB

Fix

  1. Destructure tab early: Extract only url, title, audible, incognito from the Tab object, so the full object (including favIconUrl) can be GC'd immediately
  2. Log only URL strings: Replace console.debug('...', tab) with console.debug('...', tab.url) — retains useful debug info without retaining the full object

This is the favicon half of #222. The AbortController leak (the other half, ~123 MB of leaked controllers + ~645 MB of closures) is being addressed in aw-client-js via ActivityWatch/aw-client-js#50.

Test plan

  • Build passes (npm run build)
  • Extension loads in Firefox/Chrome and sends heartbeats normally
  • After extended use, about:memory no longer shows large favicon string accumulation under the extension's background page

The console.debug calls passed entire Tab objects, which in Firefox
include favIconUrl as base64 data URIs. Browser console buffers retain
references to logged objects, so over thousands of heartbeats this
accumulated hundreds of MBs of duplicate favicon data in memory.

Fix: destructure only needed fields from Tab objects before use, and
log only the URL string instead of the full Tab object.

This addresses the favicon accumulation half of ActivityWatch#222 (the
AbortController leak is being fixed in aw-client-js via
ActivityWatch/aw-client-js#50).
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 16, 2026

Greptile Summary

This PR fixes a significant memory leak in Firefox where console.debug calls were retaining references to full Tab objects (including favIconUrl, a base64 data URI of 10–20 KB) in the browser's console buffer across thousands of heartbeats. The fix correctly replaces console.debug('...', tab) with console.debug('...', tab.url) in all listener functions, ensuring the console buffer only retains small strings.

Confidence Score: 5/5

Safe to merge — the core memory leak fix is correct and complete; only a minor over-removal of small safe data from two internal debug logs.

All findings are P2. The listener-level debug log changes correctly eliminate Tab object retention in the console buffer. The only concern is that previousData and data (neither of which contain favicon content) were also removed from two internal log calls, reducing debuggability without fixing any memory issue — but this does not affect runtime correctness or the leak fix itself.

No files require special attention beyond the single P2 suggestion on heartbeat.ts.

Important Files Changed

Filename Overview
src/background/heartbeat.ts Correctly eliminates Tab object references from console.debug calls to prevent console-buffer memory retention of large favIconUrl base64 strings; two debug log statements inside heartbeat() also remove small, safe data objects unnecessarily.

Sequence Diagram

sequenceDiagram
    participant Listener as Listener (alarm/tab/init)
    participant HB as heartbeat()
    participant Console as console.debug buffer
    participant GC as Garbage Collector

    Note over Listener,GC: BEFORE fix
    Listener->>Console: console.debug('...', tab) — retains Tab + favIconUrl
    Listener->>HB: heartbeat(client, tab, tabCount)
    HB->>Console: console.debug('Sending heartbeat', data)
    Note over Console: Tab object pinned in buffer<br/>favIconUrl (~20 KB) × thousands = ~761 MB leak

    Note over Listener,GC: AFTER fix
    Listener->>Console: console.debug('...', tab.url) — retains only string
    Listener->>HB: heartbeat(client, tab, tabCount)
    HB-->>GC: Tab object eligible for GC after function returns
    HB->>Console: console.debug('Sending heartbeat', url)
    Note over Console: Only URL strings retained — no favicon data
Loading

Reviews (1): Last reviewed commit: "fix: prevent favicon data retention in h..." | Re-trigger Greptile

Comment thread src/background/heartbeat.ts Outdated
Comment on lines +45 to +54
@@ -46,7 +51,7 @@ async function heartbeat(
config.heartbeat.intervalInSeconds + 20,
)
}
console.debug('Sending heartbeat', data)
console.debug('Sending heartbeat', url)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Over-removal of debug context for small data objects

previousData and data are both plain IEvent['data'] objects containing only url, title, audible, incognito, and tabCount — no favIconUrl or other large base64 content. Removing them from these two log calls reduces debugging utility (e.g., you can no longer see what changed between heartbeats, or what audible/incognito state was sent) without addressing any memory issue. The leak came from logging the full Tab object in the listener functions, not from logging data or previousData.

Suggested change
console.debug('Sending heartbeat for previous data', previousData)

And on line 54:

Suggested change
console.debug('Sending heartbeat', data)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. and only contain small fields (url, title, audible, incognito, tabCount) — no favicon data. Restoring them is the right call for debuggability. Fixed in bb10813.

@ErikBjare
Copy link
Copy Markdown
Member

@TimeToBuildBob Valid concern in #223 (comment)

These objects contain only small fields (url, title, audible, incognito,
tabCount) — no favIconUrl or large data. Removing them was unnecessary
and reduced debugging utility without fixing any memory issue.

The memory leak came from logging full Tab objects in the listener
functions, not from these internal heartbeat log calls.

Addresses Greptile review feedback on ActivityWatch#223.
@ErikBjare ErikBjare merged commit 50d1c1c into ActivityWatch:master Apr 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants