RESOLVED FIXED Bug 196956
Web Inspector: use weak collections for holding event listeners
https://bugs.webkit.org/show_bug.cgi?id=196956
Summary Web Inspector: use weak collections for holding event listeners
Devin Rousso
Reported 2019-04-15 21:13:57 PDT
The current `WI.Object` can create reference cycles when it saves `thisObject` via `addEventListener`. We should use a WeakMap/WeakSet to hold these objects (and maybe even the listener function) so that they can be GC'd.
Attachments
[Patch] WIP (21.09 KB, patch)
2019-04-15 23:34 PDT, Devin Rousso
hi: commit-queue-
[Patch] WIP (27.58 KB, patch)
2019-04-16 02:08 PDT, Devin Rousso
hi: commit-queue-
Archive of layout-test-results from ews103 for mac-highsierra (3.32 MB, application/zip)
2019-04-16 03:19 PDT, EWS Watchlist
no flags
Archive of layout-test-results from ews107 for mac-highsierra-wk2 (3.03 MB, application/zip)
2019-04-16 04:08 PDT, EWS Watchlist
no flags
Archive of layout-test-results from ews113 for mac-highsierra (2.89 MB, application/zip)
2019-04-16 14:20 PDT, EWS Watchlist
no flags
[Patch] WIP (120.23 KB, patch)
2019-06-18 22:11 PDT, Devin Rousso
hi: commit-queue-
[Patch] WIP (121.57 KB, patch)
2019-06-18 22:59 PDT, Devin Rousso
hi: commit-queue-
Archive of layout-test-results from ews116 for mac-highsierra (1.54 MB, application/zip)
2019-06-19 00:24 PDT, EWS Watchlist
no flags
[Patch] WIP (121.65 KB, patch)
2019-06-19 10:41 PDT, Devin Rousso
no flags
Patch (132.44 KB, patch)
2019-06-19 21:38 PDT, Devin Rousso
no flags
Patch (186.93 KB, patch)
2019-08-18 18:32 PDT, Devin Rousso
no flags
Archive of layout-test-results from ews101 for mac-highsierra (3.24 MB, application/zip)
2019-08-18 19:37 PDT, EWS Watchlist
no flags
Archive of layout-test-results from ews114 for mac-highsierra (3.02 MB, application/zip)
2019-08-18 20:20 PDT, EWS Watchlist
no flags
Patch (264.56 KB, patch)
2020-10-10 17:51 PDT, Devin Rousso
no flags
Patch (267.53 KB, patch)
2020-10-10 21:32 PDT, Devin Rousso
no flags
Patch (272.45 KB, patch)
2020-11-03 19:35 PST, Devin Rousso
no flags
Patch (272.51 KB, patch)
2020-11-03 23:40 PST, Devin Rousso
no flags
[Patch] followup (1.73 KB, patch)
2020-11-05 12:28 PST, Devin Rousso
no flags
Devin Rousso
Comment 1 2019-04-15 23:34:35 PDT
Created attachment 367502 [details] [Patch] WIP
Devin Rousso
Comment 2 2019-04-16 02:08:26 PDT
Created attachment 367517 [details] [Patch] WIP
EWS Watchlist
Comment 3 2019-04-16 03:19:51 PDT Comment hidden (obsolete)
EWS Watchlist
Comment 4 2019-04-16 03:19:53 PDT Comment hidden (obsolete)
EWS Watchlist
Comment 5 2019-04-16 04:08:20 PDT Comment hidden (obsolete)
EWS Watchlist
Comment 6 2019-04-16 04:08:22 PDT Comment hidden (obsolete)
EWS Watchlist
Comment 7 2019-04-16 14:20:42 PDT Comment hidden (obsolete)
EWS Watchlist
Comment 8 2019-04-16 14:20:44 PDT Comment hidden (obsolete)
Devin Rousso
Comment 9 2019-06-18 22:11:35 PDT
Created attachment 372431 [details] [Patch] WIP
EWS Watchlist
Comment 10 2019-06-18 22:14:42 PDT Comment hidden (obsolete)
Devin Rousso
Comment 11 2019-06-18 22:18:02 PDT
Comment on attachment 372431 [details] [Patch] WIP View in context: https://bugs.webkit.org/attachment.cgi?id=372431&action=review > Source/WebInspectorUI/UserInterface/Base/Object.js:37 > + console.assert(eventType, "Object.addEventListener: invalid eventType ", eventType, "(listener: ", listener, " thisObject: ", thisObject, ")"); Part of me also want's to add an `assert` that `eventType` exists in a `WI.*.Event` object that's related to `this` (e.g. `Object.values(this.prototype.constructor.Event).includes(eventType)`), but I'm not sure how worth it that would be, as it can basically be caught in style review instead. > Source/WebInspectorUI/UserInterface/Base/Object.js:45 > + if (window.InspectorTest && !thisObject) I'd like to remove this in the future (e.g. make all tests provide a `thisObject`), but I think this is fine for now, as it's basically guaranteed to keep the listener alive. I'm also not against the idea of allowing this for non-tests too, as it would eliminate much of the diff in this patch :P > Source/WebInspectorUI/UserInterface/Base/Object.js:67 > + console.assert(!listenersForThisObject.has(listener), "Object.addEventListener: duplicate listener ", listener, "(eventType: ", eventType, " thisObject ", thisObject, ")"); I added this extra `assert` so we can find places where we're doing unnecessary work :) > Source/WebInspectorUI/UserInterface/Base/Object.js:90 > + this._listeners.forEach((thisObjectsForEventType) => { NIT: this doesn't need to be an arrow function.
Devin Rousso
Comment 12 2019-06-18 22:59:55 PDT
Created attachment 372436 [details] [Patch] WIP
EWS Watchlist
Comment 13 2019-06-19 00:24:35 PDT Comment hidden (obsolete)
EWS Watchlist
Comment 14 2019-06-19 00:24:37 PDT Comment hidden (obsolete)
Devin Rousso
Comment 15 2019-06-19 10:41:42 PDT
Created attachment 372477 [details] [Patch] WIP Add overflow check.
EWS Watchlist
Comment 16 2019-06-19 12:38:26 PDT Comment hidden (obsolete)
Devin Rousso
Comment 17 2019-06-19 21:38:52 PDT
Devin Rousso
Comment 18 2019-06-19 21:42:14 PDT
Comment on attachment 372530 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=372530&action=review > Source/WebInspectorUI/UserInterface/Base/Object.js:-160 > - let listeners = listenersTable.toArray(); The only downside I can see to using a `WeakMap` is that it no longer preserves the order if there's more than one `thisObject`. Since `listener`s are stored in a `Set` (per `thisObject`), the order of execution (per `thisObject`) will match the order of insertion. If I had the following scenario: Object A adds Listener 1 Object B adds Listener 2 Object A adds Listener 3 Then the new order of execution would be [A1, A3, B2], which wouldn't match the "true" execution order. I think this is probably fine, however, as we likely don't have that many (if any) cases where the same object adds multiple different listeners to the same target, and for any where that's the case we should probably remove them.
Joseph Pecoraro
Comment 19 2019-06-26 11:27:29 PDT
There seems to be work on a WeakRef type in Ecmascript. That might be better than this.
Devin Rousso
Comment 20 2019-06-26 11:38:28 PDT
(In reply to Joseph Pecoraro from comment #19) > There seems to be work on a WeakRef type in Ecmascript. That might be better than this. If we use attachment 372530 [details] as is, the I don't think so actually. All it would entail is replacing the `WeakMap` with a `Map` where the keys are `WeakRef`s. The same issues with order preservation and anonymous functions being used as the `listener` without a `thisObject` still apply, albeit in a slightly different form. With that having been said, we could instead just use `WeakRef` in place of the `thisObject` with the existing `ListMultimap`, but frankly I'd like to simplify that system anyways. Another option would be to ditch the concept of a `Map` altogether and just use an array of `{thisObject: WeakRef(...), listener}` items. Calls to `addEventListener` and `removeEventListener` are "rare" (when compared to how often the `listener` itself is fired/used) that I think we can "afford" being less efficient there in order to preserve the order of addition.
Joseph Pecoraro
Comment 21 2019-08-02 20:16:54 PDT
Comment on attachment 372530 [details] Patch I don't like the idea of only Web Inspector having custom JS to iterate WeakMap/Set. I'd rather we wait for something like WeakRef. I'm going to r- but let me know if you disagree and I can take another look.
Joseph Pecoraro
Comment 22 2019-08-15 12:21:59 PDT
Seems we can implement our own InspectorIterableWeakMap / InspectorIterableWeakSet using WeakRef?
Devin Rousso
Comment 23 2019-08-15 13:12:16 PDT
(In reply to Joseph Pecoraro from comment #22) > Seems we can implement our own InspectorIterableWeakMap / InspectorIterableWeakSet using WeakRef? That's basically my plan. I'm thinking of moving back to an array of `{listener, thisObject}` and removing most of the `removeEventListener(null, null, this)` that wouldn't be needed anymore.
Nikita Vasilyev
Comment 24 2019-08-15 13:30:01 PDT
Comment on attachment 372530 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=372530&action=review > Source/WebInspectorUI/ChangeLog:8 > + Replace the `ListMultimap` with a `WeakMap` and the `LinkedList` with a `Set` for listeners. Note that 3-4 years ago when, iterating a linked list was significantly faster than Set.
Nikita Vasilyev
Comment 25 2019-08-15 13:34:06 PDT
Removing, adding, and triggering event listeners is a performance sensitive part of Web Inspector. It's worth measuring how long it takes to remove, add, fire >1000 events.
Devin Rousso
Comment 26 2019-08-18 18:32:59 PDT
EWS Watchlist
Comment 27 2019-08-18 19:37:11 PDT Comment hidden (obsolete)
EWS Watchlist
Comment 28 2019-08-18 19:37:13 PDT Comment hidden (obsolete)
EWS Watchlist
Comment 29 2019-08-18 20:20:44 PDT Comment hidden (obsolete)
EWS Watchlist
Comment 30 2019-08-18 20:20:46 PDT Comment hidden (obsolete)
Blaze Burg
Comment 31 2020-01-06 09:25:29 PST
Comment on attachment 376656 [details] Patch Clearing r?, patch is quite stale at this point.
Devin Rousso
Comment 32 2020-10-10 17:51:09 PDT
Created attachment 411024 [details] Patch `WeakRef` and `FinalizationRegistry` were enabled in r268284
Devin Rousso
Comment 33 2020-10-10 21:32:22 PDT
Blaze Burg
Comment 34 2020-11-03 16:16:14 PST
Comment on attachment 411030 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=411030&action=review r=me, great work! > Source/WebInspectorUI/ChangeLog:265 > + Remove the `closed` method that only `removeEventListener` as they are no longer needed Nit: grammaro > Source/WebInspectorUI/UserInterface/Base/Object.js:60 > + let wrappedCallback = (...args) => { Nice to get rid of unneeded `arguments`! > Source/WebInspectorUI/UserInterface/Base/Object.js:124 > // Make a copy with slice so mutations during the loop doesn't affect us. Nit: unnecessary comment. > Source/WebInspectorUI/UserInterface/Controllers/DebuggerManager.js:566 > + let promises = [this.awaitEvent(WI.DebuggerManager.Event.Paused, this)]; Nice! > Source/WebInspectorUI/UserInterface/Debug/Bootstrap.js:38 > +}, WI.settings.engineeringPauseForInternalScripts); LOL, oops > Source/WebInspectorUI/UserInterface/Models/Resource.js:1037 > + this.singleFireEventListener(WI.Resource.Event.LoadingDidFinish, resolve, this); Nice. > Source/WebInspectorUI/UserInterface/Views/AuditTreeElement.js:100 > + if (this.representedObject instanceof WI.AuditTestBase) { Cranky logic like this is why I wrote EventListenerSet in the first place. > Source/WebInspectorUI/UserInterface/Views/DataGridNode.js:493 > + this.dispatchEventToListeners(DataGridNode.Event.Populate); O_O > Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineView.js:-108 > - WI.HeapSnapshotWorkerProxy.singleton().addEventListener("HeapSnapshot.CollectionEvent", this._heapSnapshotCollectionEvent, this); o_O > Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineView.js:-264 > - WI.HeapSnapshotWorkerProxy.singleton().removeEventListener("HeapSnapshot.CollectionEvent", this._heapSnapshotCollectionEvent, this); It seems like we lost this removeEventListener.
Devin Rousso
Comment 35 2020-11-03 17:33:38 PST
Comment on attachment 411030 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=411030&action=review >> Source/WebInspectorUI/UserInterface/Views/AuditTreeElement.js:100 >> + if (this.representedObject instanceof WI.AuditTestBase) { > > Cranky logic like this is why I wrote EventListenerSet in the first place. Yeah I can see the appeal. IMO this happens so infrequently that it's not worth another data structure. If anything, I'd rather loosen the restrictions around `removeEventListener` so that you can call it even when nothing is registered which would allow us to drop all the `if`. I do see a benefit to having an assertion that the registration exists, but if others feel strongly I can change it (and this). >> Source/WebInspectorUI/UserInterface/Views/DataGridNode.js:493 >> + this.dispatchEventToListeners(DataGridNode.Event.Populate); > > O_O ikr >> Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineView.js:-264 >> - WI.HeapSnapshotWorkerProxy.singleton().removeEventListener("HeapSnapshot.CollectionEvent", this._heapSnapshotCollectionEvent, this); > > It seems like we lost this removeEventListener. This should just disappear when the `WI.HeapAllocationsTimelineView` is destructed, which should only ever happen if the user hides the heap allocations timeline. EDIT: after discussing this offline with you, I've decided to undo my removal of all the `removeEventListener` in `closed` throughout this patch in the hopes of not introducing subtle regressions and keeping behavior as close to what it was before
Devin Rousso
Comment 36 2020-11-03 19:35:43 PST
Created attachment 413134 [details] Patch streamline changes by - not eliminating `closed` and instead just replacing any `removeEventListener(null, null, this)` therein - not adding `closed` to handle `WI.DataGrid`, etc.
Devin Rousso
Comment 37 2020-11-03 23:40:03 PST
EWS
Comment 38 2020-11-04 03:30:10 PST
Committed r269359: <https://trac.webkit.org/changeset/269359> All reviewed patches have been landed. Closing bug and clearing flags on attachment 413142 [details].
Radar WebKit Bug Importer
Comment 39 2020-11-04 03:31:24 PST
Truitt Savell
Comment 40 2020-11-05 12:18:11 PST
It looks like the changes in https://trac.webkit.org/changeset/269359/webkit broke inspector/unit-tests/heap-snapshot-collection-event.html History: https://results.webkit.org/?suite=layout-tests&test=inspector%2Funit-tests%2Fheap-snapshot-collection-event.html Diff: --- /Volumes/Data/slave/catalina-release-tests-wk2/build/layout-test-results/inspector/unit-tests/heap-snapshot-collection-event-expected.txt +++ /Volumes/Data/slave/catalina-release-tests-wk2/build/layout-test-results/inspector/unit-tests/heap-snapshot-collection-event-actual.txt @@ -8,9 +8,7 @@ -- Running test case: HeapSnapshotCollectionEvent PASS: Should not have an error creating a snapshot. -PASS: Received HeapSnapshot.CollectionEvent. -PASS: Collection should include at least 200 nodes (100 objects and 100 strings). -PASS: Collection should affect the first snapshot. +!! TIMEOUT: took longer than 10000ms -- Running test case: HeapSnapshot.prototype.update PASS: 0 Objects were dead before.
Devin Rousso
Comment 41 2020-11-05 12:28:29 PST
Created attachment 413341 [details] [Patch] followup
Joseph Pecoraro
Comment 42 2020-11-05 12:29:23 PST
Comment on attachment 413341 [details] [Patch] followup rs=me
Keith Miller
Comment 43 2020-11-05 12:30:39 PST
Comment on attachment 413341 [details] [Patch] followup FWIW, it seems wrong that this test is expecting every object to be collected. That's a recipe for a flaky test, you should probably only test that at least one object was collected.
Ryan Haddad
Comment 44 2020-11-06 12:29:42 PST
Comment on attachment 413341 [details] [Patch] followup Clearing flags on attachment: 413341 Committed r269532: <https://trac.webkit.org/changeset/269532>
Ryan Haddad
Comment 45 2020-11-06 12:29:46 PST
All reviewed patches have been landed. Closing bug.
Note You need to log in before you can comment on or make changes to this bug.