Summary
PopupMenuBase._connectItemSignals() (js/ui/popupMenu.js) connects four handlers on each menu item through this._signals. When the item is destroyed, the destroy handler disconnects activate, active-changed and sensitive-changed, but never its own destroy entry. That [ 'destroy', menuItem, callback, id ] entry stays in the parent menu's SignalManager._storage forever. It keeps the destroyed item, its actors, its GObjects and its closures reachable, so they are never garbage-collected.
Any applet that rebuilds its menu items leaks. The power applet makes this severe. It rebuilds all DeviceItems on every g-properties-changed of the csd-power proxy ([email protected]/applet.js:461 → _devicesChanged() → :715). With a Bluetooth mouse that reports its battery, csd-power emits PropertiesChanged about 6 times per second.
On my machine, cinnamon RssAnon grows about 6 MB/min while I work. At about 500 MB (RSS ~550 MB), I get 3–4 black flashes and then the whole session freezes. This happens about twice a day. Moving from 16 to 32 GB RAM changed nothing.
Environment
- Linux Mint 22.3 (Zena), kernel 7.0.0-34
- Cinnamon 6.6.9, cinnamon-settings-daemon 6.6.4, muffin 6.6.3, cjs 115.1, upower 1.90.3
- Framework laptop (BAT1), Logitech MX Vertical over Bluetooth (HID++ 4.5,
hidpp_battery_0)
Evidence
1. The power menu's SignalManager accumulates destroy entries. Counts are taken from the live session through org.Cinnamon.Eval:
| applet menu |
_signals._storage.length |
of which destroy |
| power |
15 349 → 15 411 in 20 s |
15 317 → 15 379 |
| network |
62 |
14 |
| sound |
41 |
7 |
| notifications |
23 |
4 |
Every other signal type stays flat. Only destroy grows, by about 3 per second.
2. GJS heap dumps (imports.system.dumpHeap), 4 minutes apart, RssAnon 190 → 235 MB:
| object type |
h1 |
h2 |
delta |
Object |
54 651 |
66 441 |
+11 790 |
GObject_Object |
45 317 |
55 208 |
+9 891 |
Function _connectItemSignals/< |
8 785 |
10 787 |
+2 002 |
GObject_Boxed |
196 |
1 866 |
+1 670 |
The number of actors on the stage (2 487) and the item count in each applet menu stay constant. The leaked items are detached, but still referenced.
3. Who creates the items. I wrapped PopupMenuBase.prototype._connectItemSignals for 45 s and recorded each caller: 352 calls, all from [email protected]/applet.js:715 (_devicesChanged → GetDevicesRemote callback).
4. Trigger rate. dbus-monitor on org.cinnamon.SettingsDaemon.Power:
| situation |
PropertiesChanged / 10 s |
content |
RssAnon growth |
| MX Vertical connected |
57 |
Percentage alternating 55 ↔ 91 (mouse ↔ laptop battery) |
~6 MB/min |
| Bluetooth on, mouse off (headset with no battery level) |
10 |
Percentage 91 ↔ 93, Icon charging ↔ discharging |
~1.5 MB/min |
| Bluetooth off |
3 |
— |
~flat |
UPower itself is quiet. The oscillation comes from csd-power's aggregated Percentage/Icon, which flips between the mouse and the laptop battery. That looks like a second bug in csd-power.
Suggested fix
In _connectItemSignals, the destroy handler should also disconnect itself, for example:
this._signals.connect(menuItem, 'destroy', (emitter) => {
this._signals.disconnect('activate', menuItem);
this._signals.disconnect('active-changed', menuItem);
this._signals.disconnect('sensitive-changed', menuItem);
this._signals.disconnect('destroy', menuItem); // missing
...
});
In the same handler, this._signals.disconnect('open-state-changed', this) looks wrong: it targets the parent menu instead of menuItem.menu.
Two optional hardening changes:
- the power applet could skip rebuilding when the device list and its values have not changed, or debounce
_devicesChanged;
csd-power should not alternate its aggregated Percentage between devices.
Relation to the original report (veth / Docker)
The earlier observation in this issue, a leak of about 0.4–0.6 MB per veth interface created by Docker, follows the same mechanism. The network applet rebuilds its device items on every NetworkManager device add/remove, and each destroyed item stays referenced through the same destroy entry. The power applet is simply the most frequent trigger.
Workaround
- Remove the power applet from the panel, or disconnect Bluetooth battery-reporting devices.
- As a safety net, set
gsettings set org.cinnamon.launcher memory-limit 420 and gsettings set org.cinnamon.launcher check-frequency 30. Cinnamon then restarts cleanly before the freeze.
Summary
PopupMenuBase._connectItemSignals()(js/ui/popupMenu.js) connects four handlers on each menu item throughthis._signals. When the item is destroyed, thedestroyhandler disconnectsactivate,active-changedandsensitive-changed, but never its owndestroyentry. That[ 'destroy', menuItem, callback, id ]entry stays in the parent menu'sSignalManager._storageforever. It keeps the destroyed item, its actors, its GObjects and its closures reachable, so they are never garbage-collected.Any applet that rebuilds its menu items leaks. The power applet makes this severe. It rebuilds all
DeviceItems on everyg-properties-changedof thecsd-powerproxy ([email protected]/applet.js:461→_devicesChanged()→:715). With a Bluetooth mouse that reports its battery,csd-poweremitsPropertiesChangedabout 6 times per second.On my machine,
cinnamonRssAnongrows about 6 MB/min while I work. At about 500 MB (RSS ~550 MB), I get 3–4 black flashes and then the whole session freezes. This happens about twice a day. Moving from 16 to 32 GB RAM changed nothing.Environment
hidpp_battery_0)Evidence
1. The power menu's SignalManager accumulates
destroyentries. Counts are taken from the live session throughorg.Cinnamon.Eval:_signals._storage.lengthdestroyEvery other signal type stays flat. Only
destroygrows, by about 3 per second.2. GJS heap dumps (
imports.system.dumpHeap), 4 minutes apart,RssAnon190 → 235 MB:ObjectGObject_ObjectFunction _connectItemSignals/<GObject_BoxedThe number of actors on the stage (2 487) and the item count in each applet menu stay constant. The leaked items are detached, but still referenced.
3. Who creates the items. I wrapped
PopupMenuBase.prototype._connectItemSignalsfor 45 s and recorded each caller: 352 calls, all from[email protected]/applet.js:715(_devicesChanged→GetDevicesRemotecallback).4. Trigger rate.
dbus-monitoronorg.cinnamon.SettingsDaemon.Power:PropertiesChanged/ 10 sRssAnongrowthPercentagealternating 55 ↔ 91 (mouse ↔ laptop battery)Percentage91 ↔ 93,Iconcharging ↔ dischargingUPower itself is quiet. The oscillation comes from
csd-power's aggregatedPercentage/Icon, which flips between the mouse and the laptop battery. That looks like a second bug incsd-power.Suggested fix
In
_connectItemSignals, thedestroyhandler should also disconnect itself, for example:In the same handler,
this._signals.disconnect('open-state-changed', this)looks wrong: it targets the parent menu instead ofmenuItem.menu.Two optional hardening changes:
_devicesChanged;csd-powershould not alternate its aggregatedPercentagebetween devices.Relation to the original report (veth / Docker)
The earlier observation in this issue, a leak of about 0.4–0.6 MB per veth interface created by Docker, follows the same mechanism. The network applet rebuilds its device items on every NetworkManager device add/remove, and each destroyed item stays referenced through the same
destroyentry. The power applet is simply the most frequent trigger.Workaround
gsettings set org.cinnamon.launcher memory-limit 420andgsettings set org.cinnamon.launcher check-frequency 30. Cinnamon then restarts cleanly before the freeze.