Skip to content

notifications@cinnamon.org/applet.js: fixes and performance improvements - #13929

Open
ntwest wants to merge 8 commits into
linuxmint:masterfrom
ntwest:fix/notifications-applet
Open

notifications@cinnamon.org/applet.js: fixes and performance improvements#13929
ntwest wants to merge 8 commits into
linuxmint:masterfrom
ntwest:fix/notifications-applet

Conversation

@ntwest

@ntwest ntwest commented Aug 11, 2026

Copy link
Copy Markdown

Speed up the notifications applet

I was playing with the "peon-ping" app and it left a few hundred notifications in my tray.
Opening it froze the shell for about a second; clearing it froze the shell for seven to ten seconds.
These eight commits fix that, plus a few smaller bugs, and include a test script.

1. Add Looking Glass checks for the notifications applet.38eb1ddb9

Test only. Ten checks, all passing on master unchanged, plus benchmark(), which produces every
figure below. Each commit after this one adds the check for the bug it fixes, so the finished series
has twenty-one. Those later checks detect the bug their own commit fixes and fail before it.

2 to 6. Five lifecycle and allocation fixes.718bfb606, 54fe40592, 7dc878b2c,
eaf69f111, 8b6d25f2c

Each is independent and small. The menu outlived the applet, so every reload leaked another one.
The critical blink re-armed a one second timeout and threw away the source id, so nothing could
cancel it. Handlers connected with a raw connect() are not covered by disconnectAllSignals(),
so a notification still listed at removal kept the applet alive. The _destroyed check came after
actor.unparent(), so the tray handing back a notification destroyed while its banner was up meant
touching a disposed object. And timeify() built a Gio.Settings per notification, so opening a
tray of 200 built 200 of them.

7. Move notification actors instead of reparenting them.53c16b201

Re-adding a child to a mapped container makes St restyle its whole subtree, and update_list()
rebuilt the list on every change.

At 100 notifications 6.6.9 This commit
Clear the tray 8,262 ms 17.2 ms
Burst of 100 arrivals 7,828 ms 260.9 ms
Dismiss one 146.8 ms 0.5 ms
Reorder the list 154.0 ms 2.6 ms

All four still walk the list, so they still grow with the tray, just no longer quadratically. Also
closes two Clutter error paths hit while the tray has borrowed a row for a banner.

8. Parent only the notification rows near the viewport.55246260b

Only the rows near the viewport are parented, with an idle pass filling outward. Spacers reserve the
height of the rest, so the scrollbar is right without building it. The cap scales with the viewport,
so a rotated monitor is not a special case.

At 200 notifications Commit 7 This commit
Menu open 288.8 ms 90.2 ms
Menu reopen 189.4 ms 54.9 ms

Against commit 7, which this builds on. Opening still grows with the tray, 33.5 ms at 20 against
90.2 ms at 200. Reordering regressed, 2.6 ms to 16.7 ms at 100, since a reorder now rebuilds the
offsets.

Taking only some of this

You can take the first few commits and stop anywhere, without the rest. What you cannot do is take
one on its own: each builds on the ones before it, so no single commit applies to master by itself.

Stopping after commit 6 gives the four lifecycle bugs and the allocation fix. Stopping after commit
7 adds the reparenting fix and the large speedups, with no new architecture, and is the conservative
choice. Commit 8 adds the virtualization.

Checking it

From Looking Glass: let t = imports.testing.testNotificationsApplet, then t.checkOrder() and so
on. Each returns true or false and logs every case. checkMenuNotLeaked() and
checkSignalsDisconnected() reload the applet, so use a container or a throwaway session.

Known limits

Every figure is the median of three runs, taken in one sitting on one machine against Cinnamon
6.6.9. Absolute numbers move a lot with machine load, so the ratios are the durable part. Row
heights were checked on three themes. Scroll behaviour is verified in a container: the scroll
position is set and the geometry read back in a separate D-Bus call, so a relayout completes
between the two.

ntwest added 8 commits August 11, 2026 08:35
…tions applet.

Covers:
- rendered order in both sort directions
- banner handoff and handback, including a revision while banner up
- urgency picking the panel icon
- an arrival while the menu is open
- transient notifications
- the panel and menu chrome
- a source holding several notifications
- a clear whose destroy() fails partway
- benchmark() times the operations that block the main loop
…t is removed.

on_applet_removed_from_panel() left the menu in the ui group, so every
reload leaked another one, still drawable and still showing a stale
count above buttons whose applet was gone.

Test: checkMenuNotLeaked()
critical_blink() threw away the timeout id, so nothing could cancel it.
It kept ticking after the applet was removed, against icons already
destroyed, and a second chain could start while the first was still
queued.

Test: checkCriticalBlink()
… when the applet is removed.

'scrolling-changed' and 'destroy' are connected on every notification
through raw connect(), so disconnectAllSignals() never reaches them. A
notification still listed at removal kept a closure holding the applet
alive.

Test: checkSignalsDisconnected()
…troyed notification.

_notification_added() called actor.unparent() before checking
_destroyed, and the tray hands a notification back after hiding its
banner even when it was destroyed while shown. The actor is gone by
then, so that was a critical on a disposed object.

Test: checkDestroyedHandback()
…update.

timeify() constructed a Gio.Settings and read the clock format for every
notification, so opening a tray of 200 built 200 of them.

Test: checkClockSettings()
…d of reparenting them.

update_list() rebuilt the whole list on every change, so clearing N
notifications reparented every remaining actor N times. Clearing and
bursts of arrivals stop being quadratic in the size of the tray.

Also fixes two errors. The applet's list and the bin's children are not
the same set while the tray has borrowed an actor to show as a banner:
_reorderNotifications() rebuilt the bin from the list, calling
add_child() on an actor that already had a parent, and _clear_all()
removed that actor from a container that was not its parent.

Test: checkBorrowedActor()
…ws near the viewport.

Opening a tray of 100+ notifications blocked for hundreds of ms and grew
with the tray. Only the rows near the viewport are parented now, spacers
reserve the height of the rest, and the cap scales with the viewport.
Now opening and reopening a large tray get much cheaper.

Test: checkRowHeights(), checkRenderedGeometry(), checkGrowOnly(),
      checkPseudoClasses(), checkOffsetsCurrent()
@github-actions

Copy link
Copy Markdown

Best-practices scanner

This is a regex-based check for API usage that can pose security, performance or
maintainability issues, or that may already be provided by Cinnamon. Most findings
are advisory and do not automatically disqualify a pull request.

This check is not perfect and will not replace a normal review.


Found 1 potential issue(s):

⚠️ WARNING

⚠️ lang_bind

files/usr/share/cinnamon/applets/notifications@cinnamon.org/applet.js:412

this._blinkTimeoutId = Mainloop.timeout_add_seconds(1, Lang.bind(this, this.critical_blink));

Lang.bind() is deprecated. Use arrow functions (() => {}) or Function.prototype.bind() instead.


Automated pattern check.

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.

1 participant