Repository navigation
Conversation
|
interesting ... you might have a point in here and I like lazy any time ... although, I would use @webreflection/utils/map instead so that there is the const map = wm.get(this) ?? wm.put(this, new Map);that's it, that'd be my change, adding |
|
Happy to use your util, two things in the way. The blocker is the CJS build: the package exports only an "import" condition, so the generated cjs/ cannot load it. npm test fails there, since the tests run against cjs/. Add a require condition (or a dual build) and I will switch this PR to One detail about the one-line version: |
What this PR does
DOMEventTarget's constructor ranwm.set(this, new Map). SinceNodeextends it, every element, text node, comment and attribute allocated an emptyMapplus aWeakMapentry, although almost no node in a parsed document ever gets a listener. Both cost memory, and the WeakMap entries are ephemerons, which the garbage collector has to trace separately.The map is now created when the first listener is added.
dispatchEventalready handled a missing entry (invokeListenersstarts withconst map = wm.get(currentTarget); if (map && ...)), so onlyaddEventListener(create on demand) andremoveEventListener(skip when there is no map) needed a line each.This is the single largest effect of the campaign, and it is mostly a garbage-collection effect: in a profile of the extraction workload, GC went from 29% to 6% of samples.
Verification (this branch vs
main)maindocumentElement.cloneNode(true))content.jssequence ondom.html)w3c.html)/pages/*)Retained heap per parsed
w3c.htmldocument, measured by allocating 300 documents and forcing a collection: 675,278 -> 300,888 bytes, -55.4% (this metric is allocation-counting, so it is stable to a fraction of a byte).query-simple(querySelectorAll('div'),getElementsByTagName('p'),getElementsByClassName) reports a regression in the A/B harness, but it is an artefact of loading two module instances into one process: measured standalone, one process per revision, the same workload is equal or faster after the change (13.6 / 16.9 / 12.4 ms before, 10.9 / 11.9 / 14.0 ms after, three runs each). The repo's own benchmark below also showsquerySelectorAll('div')andgetElementsByTagName('p')unchanged.External cross-check with the repo's own
npm run benchmark:html(the 12 MB page), two runs per side. These are standalone runs, not paired, so they carry run-to-run drift; the effects here are far larger than that drift.mainhtml.cloneNode(true)html.innerHTMLround tripquerySelectorAll('div')Observable surface
removeEventListeneron a target that never had a listener now returns without touching a map. Before it read an empty map and found nothing: same result, no throw in either version.WeakMapno longer holds an entry for every node, only for targets that have had a listener added. The map is module-private and not reachable from user code, so this is visible only as lower memory.removeEventListenerremoves the type entry, not the map.addEventListener/removeEventListener/dispatchEventsemantics, includingonce,capture/bubbling order andhandleEventobjects, are untouched.DOMEventTargethas no constructor any more, soEventTarget.prototype.constructoris now the implicit one.new EventTargetkeeps working,instanceofis unchanged.Reproducing the numbers
The harness lives on the campaign branch of my fork:
zirkelc/linkedom@perf/autoresearch. It holdsperf/*.mts(A/B harness, characterisation guard, memory harness, profiler, the 14 cases),perf/plan.md(method, calibration, every experiment and why it was kept or discarded) andperf/experiments.tsv(the log, including the 10 discarded experiments and their numbers).One run takes about 4.5 minutes on an idle machine. Judge a case only when both runs agree; the suite total is the headline number, and
GEOMEANweights every case equally. The fixtures are live pages, so a fresh download changes the absolute milliseconds (not the ratios);perf/fetch-fixtures.shlists the URLs.npm run benchmark:htmlon each side reproduces the external cross-check.Companion PRs from the same campaign, independent of each other and of this one:
Stacked, the four together measure 2.02x on the suite total and -62% on retained heap per document; on
npm run benchmark:html(the 12 MB page) parsing goes from 767/711 ms to 259/235 ms,cloneNode(true)from 591/527 ms to 84/85 ms and the total benchmark time from 4.23/4.09 s to 2.43/2.41 s. Each PR can be taken or left on its own; the numbers in each body are that branch measured alone againstmain.