_nvidia_labels grows without bound: Array.includes() compares object identity, so the guard never matches (792,730 entries, ~200 MB) #581

Description

@crocy

Summary

_returnGpuValue() builds a fresh object literal on every call and then guards insertion with Array.prototype.includes(). includes() compares with SameValueZero, which for objects is reference identity — and the object is newly constructed each time, so the guard can never match an existing entry. Every GPU sensor reading appends another copy.

After 7 days 4 hours of uptime with update-time = 3, _nvidia_labels held 792,730 entries representing exactly 5 distinct values, retaining roughly 200 MB of JS heap.

This affects any GPU, not just Nvidia — the labels I accumulated are all gpu#1 on an AMD Radeon RX 570.

Environment

OSUbuntu 26.04 LTS
GNOME Shell50.1 (Wayland)
GJS1.88.0
Vitalsversion 80
GPUAMD Radeon RX 570
update-time3 seconds
hot-sensorsincludes _gpu#1_usage_

The bug

sensors.js:791:

letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.includes(nvidiaLabel))this._nvidia_labels.push(nvidiaLabel);

nvidiaLabel is allocated immediately above the check, so it is never reference-equal to anything already in the array. includes() returns false unconditionally and push() always runs.

The array's only consumer is _disableGpuLabels() (sensors.js:772), which iterates it to emit a disabled value per known GPU label — so the intent is clearly a set of distinct descriptors, and the duplicates serve no purpose beyond making that loop progressively slower.

Evidence

Read from the running shell after 7d 4h uptime:

_nvidia_labels.length = 792,730

Collapsing it by value yields 5 entries:

Memory Total | gpu#1
Graphics | gpu#1-group
Vendor | gpu#1
Usage | gpu#1
Memory Used | gpu#1

A sample of 500 consecutive entries showed a single distinct object shape (label,type,format), e.g.:

{"label":"Memory Total","type":"gpu#1","format":"memory"}

Growth rate observed live was ~0.4–1.3 entries/second depending on activity, consistent with 792k over the uptime.

Deduplicating the array in place and forcing a GC reclaimed ~200 MB of JS heap (measured as part of a 211.8 MB reclaim that also trimmed an unrelated ~946k-element numeric array in another extension, which can account for at most ~8 MB).

There is a secondary cost too: because includes() is O(n) and runs on every GPU reading, each poll scans the entire array. At 792k entries that is a full linear scan several times per update cycle, on the compositor's main thread.

Suggested fix

Compare by value:

letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.some(l=>l.label===label&&l.type===type&&l.format===format))this._nvidia_labels.push(nvidiaLabel);

This preserves _disableGpuLabels() semantics exactly while bounding the array to the distinct label set (5 entries on this hardware).

A Map keyed on `${label}|${type}|${format}` would also work and would make the membership test O(1) rather than O(n), which may be preferable given it runs on every poll.

Note on verification

I applied the some() version above to my local install and it passes a syntax check, but I have not yet exercised it at runtime — GNOME 45+ extensions are ES modules and cannot be reloaded without restarting the shell, so it takes effect at my next login. I'll follow up here once it has run for a day and I can confirm the array stays at 5 entries.

Reproduction

  1. Enable Vitals with a GPU sensor in hot-sensors (any vendor).
  2. Leave the session running.
  3. In Looking Glass:
Main.panel.statusArea.vitalsMenu._sensors._nvidia_labels.length

The length climbs monotonically and never plateaus.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      _nvidia_labels grows without bound: Array.includes() compares object identity, so the guard never matches (792,730 entries, ~200 MB) #581

      Description

      @crocy

      Summary

      _returnGpuValue() builds a fresh object literal on every call and then guards insertion with Array.prototype.includes(). includes() compares with SameValueZero, which for objects is reference identity — and the object is newly constructed each time, so the guard can never match an existing entry. Every GPU sensor reading appends another copy.

      After 7 days 4 hours of uptime with update-time = 3, _nvidia_labels held 792,730 entries representing exactly 5 distinct values, retaining roughly 200 MB of JS heap.

      This affects any GPU, not just Nvidia — the labels I accumulated are all gpu#1 on an AMD Radeon RX 570.

      Environment

      OSUbuntu 26.04 LTS
      GNOME Shell50.1 (Wayland)
      GJS1.88.0
      Vitalsversion 80
      GPUAMD Radeon RX 570
      update-time3 seconds
      hot-sensorsincludes _gpu#1_usage_

      The bug

      sensors.js:791:

      letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.includes(nvidiaLabel))this._nvidia_labels.push(nvidiaLabel);

      nvidiaLabel is allocated immediately above the check, so it is never reference-equal to anything already in the array. includes() returns false unconditionally and push() always runs.

      The array's only consumer is _disableGpuLabels() (sensors.js:772), which iterates it to emit a disabled value per known GPU label — so the intent is clearly a set of distinct descriptors, and the duplicates serve no purpose beyond making that loop progressively slower.

      Evidence

      Read from the running shell after 7d 4h uptime:

      _nvidia_labels.length = 792,730
      

      Collapsing it by value yields 5 entries:

      Memory Total | gpu#1
      Graphics | gpu#1-group
      Vendor | gpu#1
      Usage | gpu#1
      Memory Used | gpu#1
      

      A sample of 500 consecutive entries showed a single distinct object shape (label,type,format), e.g.:

      {"label":"Memory Total","type":"gpu#1","format":"memory"}

      Growth rate observed live was ~0.4–1.3 entries/second depending on activity, consistent with 792k over the uptime.

      Deduplicating the array in place and forcing a GC reclaimed ~200 MB of JS heap (measured as part of a 211.8 MB reclaim that also trimmed an unrelated ~946k-element numeric array in another extension, which can account for at most ~8 MB).

      There is a secondary cost too: because includes() is O(n) and runs on every GPU reading, each poll scans the entire array. At 792k entries that is a full linear scan several times per update cycle, on the compositor's main thread.

      Suggested fix

      Compare by value:

      letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.some(l=>l.label===label&&l.type===type&&l.format===format))this._nvidia_labels.push(nvidiaLabel);

      This preserves _disableGpuLabels() semantics exactly while bounding the array to the distinct label set (5 entries on this hardware).

      A Map keyed on `${label}|${type}|${format}` would also work and would make the membership test O(1) rather than O(n), which may be preferable given it runs on every poll.

      Note on verification

      I applied the some() version above to my local install and it passes a syntax check, but I have not yet exercised it at runtime — GNOME 45+ extensions are ES modules and cannot be reloaded without restarting the shell, so it takes effect at my next login. I'll follow up here once it has run for a day and I can confirm the array stays at 5 entries.

      Reproduction

      1. Enable Vitals with a GPU sensor in hot-sensors (any vendor).
      2. Leave the session running.
      3. In Looking Glass:
      Main.panel.statusArea.vitalsMenu._sensors._nvidia_labels.length

      The length climbs monotonically and never plateaus.

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          _nvidia_labels grows without bound: Array.includes() compares object identity, so the guard never matches (792,730 entries, ~200 MB) #581

          Description

          @crocy

          Summary

          _returnGpuValue() builds a fresh object literal on every call and then guards insertion with Array.prototype.includes(). includes() compares with SameValueZero, which for objects is reference identity — and the object is newly constructed each time, so the guard can never match an existing entry. Every GPU sensor reading appends another copy.

          After 7 days 4 hours of uptime with update-time = 3, _nvidia_labels held 792,730 entries representing exactly 5 distinct values, retaining roughly 200 MB of JS heap.

          This affects any GPU, not just Nvidia — the labels I accumulated are all gpu#1 on an AMD Radeon RX 570.

          Environment

          OSUbuntu 26.04 LTS
          GNOME Shell50.1 (Wayland)
          GJS1.88.0
          Vitalsversion 80
          GPUAMD Radeon RX 570
          update-time3 seconds
          hot-sensorsincludes _gpu#1_usage_

          The bug

          sensors.js:791:

          letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.includes(nvidiaLabel))this._nvidia_labels.push(nvidiaLabel);

          nvidiaLabel is allocated immediately above the check, so it is never reference-equal to anything already in the array. includes() returns false unconditionally and push() always runs.

          The array's only consumer is _disableGpuLabels() (sensors.js:772), which iterates it to emit a disabled value per known GPU label — so the intent is clearly a set of distinct descriptors, and the duplicates serve no purpose beyond making that loop progressively slower.

          Evidence

          Read from the running shell after 7d 4h uptime:

          _nvidia_labels.length = 792,730
          

          Collapsing it by value yields 5 entries:

          Memory Total | gpu#1
          Graphics | gpu#1-group
          Vendor | gpu#1
          Usage | gpu#1
          Memory Used | gpu#1
          

          A sample of 500 consecutive entries showed a single distinct object shape (label,type,format), e.g.:

          {"label":"Memory Total","type":"gpu#1","format":"memory"}

          Growth rate observed live was ~0.4–1.3 entries/second depending on activity, consistent with 792k over the uptime.

          Deduplicating the array in place and forcing a GC reclaimed ~200 MB of JS heap (measured as part of a 211.8 MB reclaim that also trimmed an unrelated ~946k-element numeric array in another extension, which can account for at most ~8 MB).

          There is a secondary cost too: because includes() is O(n) and runs on every GPU reading, each poll scans the entire array. At 792k entries that is a full linear scan several times per update cycle, on the compositor's main thread.

          Suggested fix

          Compare by value:

          letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.some(l=>l.label===label&&l.type===type&&l.format===format))this._nvidia_labels.push(nvidiaLabel);

          This preserves _disableGpuLabels() semantics exactly while bounding the array to the distinct label set (5 entries on this hardware).

          A Map keyed on `${label}|${type}|${format}` would also work and would make the membership test O(1) rather than O(n), which may be preferable given it runs on every poll.

          Note on verification

          I applied the some() version above to my local install and it passes a syntax check, but I have not yet exercised it at runtime — GNOME 45+ extensions are ES modules and cannot be reloaded without restarting the shell, so it takes effect at my next login. I'll follow up here once it has run for a day and I can confirm the array stays at 5 entries.

          Reproduction

          1. Enable Vitals with a GPU sensor in hot-sensors (any vendor).
          2. Leave the session running.
          3. In Looking Glass:
          Main.panel.statusArea.vitalsMenu._sensors._nvidia_labels.length

          The length climbs monotonically and never plateaus.

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              _nvidia_labels grows without bound: Array.includes() compares object identity, so the guard never matches (792,730 entries, ~200 MB) #581

              Description

              @crocy

              Summary

              _returnGpuValue() builds a fresh object literal on every call and then guards insertion with Array.prototype.includes(). includes() compares with SameValueZero, which for objects is reference identity — and the object is newly constructed each time, so the guard can never match an existing entry. Every GPU sensor reading appends another copy.

              After 7 days 4 hours of uptime with update-time = 3, _nvidia_labels held 792,730 entries representing exactly 5 distinct values, retaining roughly 200 MB of JS heap.

              This affects any GPU, not just Nvidia — the labels I accumulated are all gpu#1 on an AMD Radeon RX 570.

              Environment

              OSUbuntu 26.04 LTS
              GNOME Shell50.1 (Wayland)
              GJS1.88.0
              Vitalsversion 80
              GPUAMD Radeon RX 570
              update-time3 seconds
              hot-sensorsincludes _gpu#1_usage_

              The bug

              sensors.js:791:

              letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.includes(nvidiaLabel))this._nvidia_labels.push(nvidiaLabel);

              nvidiaLabel is allocated immediately above the check, so it is never reference-equal to anything already in the array. includes() returns false unconditionally and push() always runs.

              The array's only consumer is _disableGpuLabels() (sensors.js:772), which iterates it to emit a disabled value per known GPU label — so the intent is clearly a set of distinct descriptors, and the duplicates serve no purpose beyond making that loop progressively slower.

              Evidence

              Read from the running shell after 7d 4h uptime:

              _nvidia_labels.length = 792,730
              

              Collapsing it by value yields 5 entries:

              Memory Total | gpu#1
              Graphics | gpu#1-group
              Vendor | gpu#1
              Usage | gpu#1
              Memory Used | gpu#1
              

              A sample of 500 consecutive entries showed a single distinct object shape (label,type,format), e.g.:

              {"label":"Memory Total","type":"gpu#1","format":"memory"}

              Growth rate observed live was ~0.4–1.3 entries/second depending on activity, consistent with 792k over the uptime.

              Deduplicating the array in place and forcing a GC reclaimed ~200 MB of JS heap (measured as part of a 211.8 MB reclaim that also trimmed an unrelated ~946k-element numeric array in another extension, which can account for at most ~8 MB).

              There is a secondary cost too: because includes() is O(n) and runs on every GPU reading, each poll scans the entire array. At 792k entries that is a full linear scan several times per update cycle, on the compositor's main thread.

              Suggested fix

              Compare by value:

              letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.some(l=>l.label===label&&l.type===type&&l.format===format))this._nvidia_labels.push(nvidiaLabel);

              This preserves _disableGpuLabels() semantics exactly while bounding the array to the distinct label set (5 entries on this hardware).

              A Map keyed on `${label}|${type}|${format}` would also work and would make the membership test O(1) rather than O(n), which may be preferable given it runs on every poll.

              Note on verification

              I applied the some() version above to my local install and it passes a syntax check, but I have not yet exercised it at runtime — GNOME 45+ extensions are ES modules and cannot be reloaded without restarting the shell, so it takes effect at my next login. I'll follow up here once it has run for a day and I can confirm the array stays at 5 entries.

              Reproduction

              1. Enable Vitals with a GPU sensor in hot-sensors (any vendor).
              2. Leave the session running.
              3. In Looking Glass:
              Main.panel.statusArea.vitalsMenu._sensors._nvidia_labels.length

              The length climbs monotonically and never plateaus.

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  _nvidia_labels grows without bound: Array.includes() compares object identity, so the guard never matches (792,730 entries, ~200 MB) #581

                  Description

                  @crocy

                  Summary

                  _returnGpuValue() builds a fresh object literal on every call and then guards insertion with Array.prototype.includes(). includes() compares with SameValueZero, which for objects is reference identity — and the object is newly constructed each time, so the guard can never match an existing entry. Every GPU sensor reading appends another copy.

                  After 7 days 4 hours of uptime with update-time = 3, _nvidia_labels held 792,730 entries representing exactly 5 distinct values, retaining roughly 200 MB of JS heap.

                  This affects any GPU, not just Nvidia — the labels I accumulated are all gpu#1 on an AMD Radeon RX 570.

                  Environment

                  OSUbuntu 26.04 LTS
                  GNOME Shell50.1 (Wayland)
                  GJS1.88.0
                  Vitalsversion 80
                  GPUAMD Radeon RX 570
                  update-time3 seconds
                  hot-sensorsincludes _gpu#1_usage_

                  The bug

                  sensors.js:791:

                  letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.includes(nvidiaLabel))this._nvidia_labels.push(nvidiaLabel);

                  nvidiaLabel is allocated immediately above the check, so it is never reference-equal to anything already in the array. includes() returns false unconditionally and push() always runs.

                  The array's only consumer is _disableGpuLabels() (sensors.js:772), which iterates it to emit a disabled value per known GPU label — so the intent is clearly a set of distinct descriptors, and the duplicates serve no purpose beyond making that loop progressively slower.

                  Evidence

                  Read from the running shell after 7d 4h uptime:

                  _nvidia_labels.length = 792,730
                  

                  Collapsing it by value yields 5 entries:

                  Memory Total | gpu#1
                  Graphics | gpu#1-group
                  Vendor | gpu#1
                  Usage | gpu#1
                  Memory Used | gpu#1
                  

                  A sample of 500 consecutive entries showed a single distinct object shape (label,type,format), e.g.:

                  {"label":"Memory Total","type":"gpu#1","format":"memory"}

                  Growth rate observed live was ~0.4–1.3 entries/second depending on activity, consistent with 792k over the uptime.

                  Deduplicating the array in place and forcing a GC reclaimed ~200 MB of JS heap (measured as part of a 211.8 MB reclaim that also trimmed an unrelated ~946k-element numeric array in another extension, which can account for at most ~8 MB).

                  There is a secondary cost too: because includes() is O(n) and runs on every GPU reading, each poll scans the entire array. At 792k entries that is a full linear scan several times per update cycle, on the compositor's main thread.

                  Suggested fix

                  Compare by value:

                  letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.some(l=>l.label===label&&l.type===type&&l.format===format))this._nvidia_labels.push(nvidiaLabel);

                  This preserves _disableGpuLabels() semantics exactly while bounding the array to the distinct label set (5 entries on this hardware).

                  A Map keyed on `${label}|${type}|${format}` would also work and would make the membership test O(1) rather than O(n), which may be preferable given it runs on every poll.

                  Note on verification

                  I applied the some() version above to my local install and it passes a syntax check, but I have not yet exercised it at runtime — GNOME 45+ extensions are ES modules and cannot be reloaded without restarting the shell, so it takes effect at my next login. I'll follow up here once it has run for a day and I can confirm the array stays at 5 entries.

                  Reproduction

                  1. Enable Vitals with a GPU sensor in hot-sensors (any vendor).
                  2. Leave the session running.
                  3. In Looking Glass:
                  Main.panel.statusArea.vitalsMenu._sensors._nvidia_labels.length

                  The length climbs monotonically and never plateaus.

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      _nvidia_labels grows without bound: Array.includes() compares object identity, so the guard never matches (792,730 entries, ~200 MB) #581

                      Description

                      @crocy

                      Summary

                      _returnGpuValue() builds a fresh object literal on every call and then guards insertion with Array.prototype.includes(). includes() compares with SameValueZero, which for objects is reference identity — and the object is newly constructed each time, so the guard can never match an existing entry. Every GPU sensor reading appends another copy.

                      After 7 days 4 hours of uptime with update-time = 3, _nvidia_labels held 792,730 entries representing exactly 5 distinct values, retaining roughly 200 MB of JS heap.

                      This affects any GPU, not just Nvidia — the labels I accumulated are all gpu#1 on an AMD Radeon RX 570.

                      Environment

                      OSUbuntu 26.04 LTS
                      GNOME Shell50.1 (Wayland)
                      GJS1.88.0
                      Vitalsversion 80
                      GPUAMD Radeon RX 570
                      update-time3 seconds
                      hot-sensorsincludes _gpu#1_usage_

                      The bug

                      sensors.js:791:

                      letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.includes(nvidiaLabel))this._nvidia_labels.push(nvidiaLabel);

                      nvidiaLabel is allocated immediately above the check, so it is never reference-equal to anything already in the array. includes() returns false unconditionally and push() always runs.

                      The array's only consumer is _disableGpuLabels() (sensors.js:772), which iterates it to emit a disabled value per known GPU label — so the intent is clearly a set of distinct descriptors, and the duplicates serve no purpose beyond making that loop progressively slower.

                      Evidence

                      Read from the running shell after 7d 4h uptime:

                      _nvidia_labels.length = 792,730
                      

                      Collapsing it by value yields 5 entries:

                      Memory Total | gpu#1
                      Graphics | gpu#1-group
                      Vendor | gpu#1
                      Usage | gpu#1
                      Memory Used | gpu#1
                      

                      A sample of 500 consecutive entries showed a single distinct object shape (label,type,format), e.g.:

                      {"label":"Memory Total","type":"gpu#1","format":"memory"}

                      Growth rate observed live was ~0.4–1.3 entries/second depending on activity, consistent with 792k over the uptime.

                      Deduplicating the array in place and forcing a GC reclaimed ~200 MB of JS heap (measured as part of a 211.8 MB reclaim that also trimmed an unrelated ~946k-element numeric array in another extension, which can account for at most ~8 MB).

                      There is a secondary cost too: because includes() is O(n) and runs on every GPU reading, each poll scans the entire array. At 792k entries that is a full linear scan several times per update cycle, on the compositor's main thread.

                      Suggested fix

                      Compare by value:

                      letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.some(l=>l.label===label&&l.type===type&&l.format===format))this._nvidia_labels.push(nvidiaLabel);

                      This preserves _disableGpuLabels() semantics exactly while bounding the array to the distinct label set (5 entries on this hardware).

                      A Map keyed on `${label}|${type}|${format}` would also work and would make the membership test O(1) rather than O(n), which may be preferable given it runs on every poll.

                      Note on verification

                      I applied the some() version above to my local install and it passes a syntax check, but I have not yet exercised it at runtime — GNOME 45+ extensions are ES modules and cannot be reloaded without restarting the shell, so it takes effect at my next login. I'll follow up here once it has run for a day and I can confirm the array stays at 5 entries.

                      Reproduction

                      1. Enable Vitals with a GPU sensor in hot-sensors (any vendor).
                      2. Leave the session running.
                      3. In Looking Glass:
                      Main.panel.statusArea.vitalsMenu._sensors._nvidia_labels.length

                      The length climbs monotonically and never plateaus.

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          _nvidia_labels grows without bound: Array.includes() compares object identity, so the guard never matches (792,730 entries, ~200 MB) #581

                          Description

                          @crocy

                          Summary

                          _returnGpuValue() builds a fresh object literal on every call and then guards insertion with Array.prototype.includes(). includes() compares with SameValueZero, which for objects is reference identity — and the object is newly constructed each time, so the guard can never match an existing entry. Every GPU sensor reading appends another copy.

                          After 7 days 4 hours of uptime with update-time = 3, _nvidia_labels held 792,730 entries representing exactly 5 distinct values, retaining roughly 200 MB of JS heap.

                          This affects any GPU, not just Nvidia — the labels I accumulated are all gpu#1 on an AMD Radeon RX 570.

                          Environment

                          OSUbuntu 26.04 LTS
                          GNOME Shell50.1 (Wayland)
                          GJS1.88.0
                          Vitalsversion 80
                          GPUAMD Radeon RX 570
                          update-time3 seconds
                          hot-sensorsincludes _gpu#1_usage_

                          The bug

                          sensors.js:791:

                          letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.includes(nvidiaLabel))this._nvidia_labels.push(nvidiaLabel);

                          nvidiaLabel is allocated immediately above the check, so it is never reference-equal to anything already in the array. includes() returns false unconditionally and push() always runs.

                          The array's only consumer is _disableGpuLabels() (sensors.js:772), which iterates it to emit a disabled value per known GPU label — so the intent is clearly a set of distinct descriptors, and the duplicates serve no purpose beyond making that loop progressively slower.

                          Evidence

                          Read from the running shell after 7d 4h uptime:

                          _nvidia_labels.length = 792,730
                          

                          Collapsing it by value yields 5 entries:

                          Memory Total | gpu#1
                          Graphics | gpu#1-group
                          Vendor | gpu#1
                          Usage | gpu#1
                          Memory Used | gpu#1
                          

                          A sample of 500 consecutive entries showed a single distinct object shape (label,type,format), e.g.:

                          {"label":"Memory Total","type":"gpu#1","format":"memory"}

                          Growth rate observed live was ~0.4–1.3 entries/second depending on activity, consistent with 792k over the uptime.

                          Deduplicating the array in place and forcing a GC reclaimed ~200 MB of JS heap (measured as part of a 211.8 MB reclaim that also trimmed an unrelated ~946k-element numeric array in another extension, which can account for at most ~8 MB).

                          There is a secondary cost too: because includes() is O(n) and runs on every GPU reading, each poll scans the entire array. At 792k entries that is a full linear scan several times per update cycle, on the compositor's main thread.

                          Suggested fix

                          Compare by value:

                          letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.some(l=>l.label===label&&l.type===type&&l.format===format))this._nvidia_labels.push(nvidiaLabel);

                          This preserves _disableGpuLabels() semantics exactly while bounding the array to the distinct label set (5 entries on this hardware).

                          A Map keyed on `${label}|${type}|${format}` would also work and would make the membership test O(1) rather than O(n), which may be preferable given it runs on every poll.

                          Note on verification

                          I applied the some() version above to my local install and it passes a syntax check, but I have not yet exercised it at runtime — GNOME 45+ extensions are ES modules and cannot be reloaded without restarting the shell, so it takes effect at my next login. I'll follow up here once it has run for a day and I can confirm the array stays at 5 entries.

                          Reproduction

                          1. Enable Vitals with a GPU sensor in hot-sensors (any vendor).
                          2. Leave the session running.
                          3. In Looking Glass:
                          Main.panel.statusArea.vitalsMenu._sensors._nvidia_labels.length

                          The length climbs monotonically and never plateaus.

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              _nvidia_labels grows without bound: Array.includes() compares object identity, so the guard never matches (792,730 entries, ~200 MB) #581

                              Description

                              @crocy

                              Summary

                              _returnGpuValue() builds a fresh object literal on every call and then guards insertion with Array.prototype.includes(). includes() compares with SameValueZero, which for objects is reference identity — and the object is newly constructed each time, so the guard can never match an existing entry. Every GPU sensor reading appends another copy.

                              After 7 days 4 hours of uptime with update-time = 3, _nvidia_labels held 792,730 entries representing exactly 5 distinct values, retaining roughly 200 MB of JS heap.

                              This affects any GPU, not just Nvidia — the labels I accumulated are all gpu#1 on an AMD Radeon RX 570.

                              Environment

                              OSUbuntu 26.04 LTS
                              GNOME Shell50.1 (Wayland)
                              GJS1.88.0
                              Vitalsversion 80
                              GPUAMD Radeon RX 570
                              update-time3 seconds
                              hot-sensorsincludes _gpu#1_usage_

                              The bug

                              sensors.js:791:

                              letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.includes(nvidiaLabel))this._nvidia_labels.push(nvidiaLabel);

                              nvidiaLabel is allocated immediately above the check, so it is never reference-equal to anything already in the array. includes() returns false unconditionally and push() always runs.

                              The array's only consumer is _disableGpuLabels() (sensors.js:772), which iterates it to emit a disabled value per known GPU label — so the intent is clearly a set of distinct descriptors, and the duplicates serve no purpose beyond making that loop progressively slower.

                              Evidence

                              Read from the running shell after 7d 4h uptime:

                              _nvidia_labels.length = 792,730
                              

                              Collapsing it by value yields 5 entries:

                              Memory Total | gpu#1
                              Graphics | gpu#1-group
                              Vendor | gpu#1
                              Usage | gpu#1
                              Memory Used | gpu#1
                              

                              A sample of 500 consecutive entries showed a single distinct object shape (label,type,format), e.g.:

                              {"label":"Memory Total","type":"gpu#1","format":"memory"}

                              Growth rate observed live was ~0.4–1.3 entries/second depending on activity, consistent with 792k over the uptime.

                              Deduplicating the array in place and forcing a GC reclaimed ~200 MB of JS heap (measured as part of a 211.8 MB reclaim that also trimmed an unrelated ~946k-element numeric array in another extension, which can account for at most ~8 MB).

                              There is a secondary cost too: because includes() is O(n) and runs on every GPU reading, each poll scans the entire array. At 792k entries that is a full linear scan several times per update cycle, on the compositor's main thread.

                              Suggested fix

                              Compare by value:

                              letnvidiaLabel={'label': label,'type': type,'format': format};if(!this._nvidia_labels.some(l=>l.label===label&&l.type===type&&l.format===format))this._nvidia_labels.push(nvidiaLabel);

                              This preserves _disableGpuLabels() semantics exactly while bounding the array to the distinct label set (5 entries on this hardware).

                              A Map keyed on `${label}|${type}|${format}` would also work and would make the membership test O(1) rather than O(n), which may be preferable given it runs on every poll.

                              Note on verification

                              I applied the some() version above to my local install and it passes a syntax check, but I have not yet exercised it at runtime — GNOME 45+ extensions are ES modules and cannot be reloaded without restarting the shell, so it takes effect at my next login. I'll follow up here once it has run for a day and I can confirm the array stays at 5 entries.

                              Reproduction

                              1. Enable Vitals with a GPU sensor in hot-sensors (any vendor).
                              2. Leave the session running.
                              3. In Looking Glass:
                              Main.panel.statusArea.vitalsMenu._sensors._nvidia_labels.length

                              The length climbs monotonically and never plateaus.

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions