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

OS Ubuntu 26.04 LTS
GNOME Shell 50.1 (Wayland)
GJS 1.88.0
Vitals version 80
GPU AMD Radeon RX 570
update-time 3 seconds
hot-sensors includes _gpu#1_usage_

The bug

sensors.js:791:

let nvidiaLabel = {'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:

let nvidiaLabel = {'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