Fix memory leaks in device monitor teardown path#85
Open
pablopda wants to merge 2 commits intonoroadsleft000:mainfrom
Open
Fix memory leaks in device monitor teardown path#85pablopda wants to merge 2 commits intonoroadsleft000:mainfrom
pablopda wants to merge 2 commits intonoroadsleft000:mainfrom
Conversation
On repeated enable/disable cycles, gnome-shell RSS grew steadily. Three root causes were identified: 1. DeviceMonitor.deinit() never disconnected per-device state-changed signals. Those handlers held references to the monitor instance and kept scheduling GLib timeouts on a half-torn-down object, leaking both the signal connections and the timeout sources. 2. DeviceMonitor.deinit() was never called at all. DevicePresenter had no deinit() method, so the entire teardown chain stopped at AppController — the monitor stayed alive with all its NM client signals still connected. 3. The _devices map was never cleared, accumulating stale GObject references to NM.Device instances for interfaces that had been removed (e.g. Docker veth pairs being torn down). Additionally, DeviceMonitor.init() subscribed to 9 NM client signals when only 5 are needed. any-device-added/removed are supersets of device-added/removed and caused double reloads. connection-added/removed fire on profile config edits, not connectivity changes, adding noise without value. Changes: - DeviceMonitor.deinit(): disconnect per-device state-changed signals, cancel any pending debounce timer, and clear the _devices map before disconnecting NM client signals. - DeviceMonitor._loadDevices(): clear _devices map before rebuilding it so removed interfaces do not persist as stale entries. Skip interfaces not managed by NetworkManager (null from get_device_by_iface). - DeviceMonitor.init(): remove any-device-added, any-device-removed, connection-added, and connection-removed signals. Keep device-added, device-removed, active-connection-added, active-connection-removed, and notify::metered (5 total). - DeviceMonitor: add _scheduleReload() to debounce rapid NM signal bursts (e.g. Docker container start/stop) into a single 500ms deferred _loadDevices() call. - DevicePresenter: add deinit() that unsubscribes from the device-reset broadcaster and calls _deviceMonitor.deinit(). Convert resetDeviceStats to an arrow property so the same reference works for both subscribe and unsubscribe (consistent with AppController's handler pattern). - AppController.deinit(): call _devicePresenter.deinit() between saveStats() and _appSettingsModel.deinit() to wire the full teardown chain.
…onitor Wrap device enumeration in try/catch to handle transient NM errors from short-lived interfaces (Docker veth pairs, etc.) that can disappear between /proc/net/dev read and NM query. Also guard get_ip4_config / get_ip6_config calls which can throw on devices in transitional states. Add skipLibCheck to tsconfig.json to avoid build failures from conflicting ambient @types declarations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Repeated enable/disable cycles of the extension cause gnome-shell's RSS to grow continuously. This is especially noticeable in environments with frequent network interface churn (Docker, VPN reconnects, USB tethering).
Three separate leaks were identified:
1. Dangling per-device
state-changedsignalsDeviceMonitor.deinit()disconnected the NM client-level signals but never touched the per-devicestate-changedhandlers. After disable, those handlers still held references to the monitor instance and kept scheduling GLib timeouts against a half-torn-down object — leaking both the signal connections and the timeout sources.2. Broken teardown chain —
DeviceMonitor.deinit()was never calledDevicePresenterhad nodeinit()method, soAppController.deinit()could never propagate cleanup downward. The entire monitor stayed alive across disable cycles with all its NM signals still connected.On the
DevicePresenterside,resetDeviceStatswas subscribed to the device-reset broadcaster via.bind(this), which creates a new function reference on every call. SinceEventBroadcaster.unsubscribe()relies on identity comparison (indexOf), there was no way to unsubscribe that handler — it leaked on every cycle.3. Stale device map entries
_loadDevices()adds entries tothis._devicesby key, but never cleared the map before rebuilding it. Interfaces that disappeared between reloads (e.g. Docker veth pairs being torn down) persisted as staleNM.DeviceGObject references indefinitely.What changed
DeviceMonitor.tsdeinit()now cancels the debounce timer, disconnects all per-devicestate-changedsignals, and clears the_devicesmap — in addition to the existing NM client signal cleanup._loadDevices()clears_devicesbefore rebuilding so removed interfaces don't persist. Also skips interfaces not managed by NetworkManager (get_device_by_ifacereturning null), which prevents crashes on orphaned veth pairs.init()connects 5 NM signals instead of 9. Removedany-device-added/any-device-removed(supersets ofdevice-added/device-removedthat caused double reloads) andconnection-added/connection-removed(fire on profile config edits, not connectivity changes)._scheduleReload()debounces rapid NM signal bursts into a single deferred_loadDevices()call (500ms), preventing redundant/proc/net/devreads during container start/stop storms.DevicePresenter.tsdeinit()that unsubscribes from the device-reset broadcaster and calls_deviceMonitor.deinit(), completing the teardown chain.resetDeviceStatsfrom a regular method to an arrow property, giving it a stablethis-bound reference that works with bothsubscribe()andunsubscribe(). This is consistent with howAppControlleralready handlesonRightClickandonSettingChanged.AppController.ts_devicePresenter.deinit()in the teardown path, placed betweensaveStats()(needs live data) and_appSettingsModel.deinit().Resulting teardown chain
Test plan
npx tsc --skipLibCheckcompiles without errors