|
| 1 | +// One-off probe: count how many Layout.append clones survive into the |
| 2 | +// finalized page wrapper vs. how many get rolled back by removeOverflow. |
| 3 | +// |
| 4 | +// Mechanism: |
| 5 | +// - Wrap Layout.prototype.append to (a) count calls and (b) tag every |
| 6 | +// returned clone with an expando __pagedjs_clone_tag = true. |
| 7 | +// - Wrap Node.prototype.cloneNode globally so we can also report the |
| 8 | +// gross cloneNode call count (which includes rebuildAncestors and |
| 9 | +// anything else outside Layout.append). |
| 10 | +// - At finalizePage, walk the just-finalized page wrapper counting |
| 11 | +// tagged survivors. (removeOverflow has already fired by this point.) |
| 12 | +// - At afterRendered, summarise totals + per-page distribution. |
| 13 | +// |
| 14 | +// Cost: O(1) per append + one tree walk per finalized page. Run with |
| 15 | +// --detach-pages --no-timing --additional-script ..\perf\instrument-clones.js |
| 16 | +// from a measure.mjs invocation. Numbers are reported via console.log |
| 17 | +// which measure.mjs forwards to stdout. |
| 18 | + |
| 19 | +(() => { |
| 20 | + const Layout = window.PagedLayout; |
| 21 | + if (!Layout) { |
| 22 | + console.log('[clone-count] ERROR: window.PagedLayout not exposed; bundle patch missing.'); |
| 23 | + return; |
| 24 | + } |
| 25 | + const origAppend = Layout.prototype.append; |
| 26 | + let appendCalls = 0; |
| 27 | + Layout.prototype.append = function (...args) { |
| 28 | + const clone = origAppend.apply(this, args); |
| 29 | + appendCalls++; |
| 30 | + if (clone) clone.__pagedjs_clone_tag = true; |
| 31 | + return clone; |
| 32 | + }; |
| 33 | + |
| 34 | + const origCloneNode = Node.prototype.cloneNode; |
| 35 | + let cloneNodeCalls = 0; |
| 36 | + Node.prototype.cloneNode = function (deep) { |
| 37 | + cloneNodeCalls++; |
| 38 | + return origCloneNode.call(this, deep); |
| 39 | + }; |
| 40 | + |
| 41 | + const perPage = []; // { appended, kept } |
| 42 | + let appendAtPageStart = 0; |
| 43 | + |
| 44 | + class CloneCountHandler extends Paged.Handler { |
| 45 | + beforePageLayout() { |
| 46 | + appendAtPageStart = appendCalls; |
| 47 | + } |
| 48 | + finalizePage(pageElement) { |
| 49 | + const appendedThisPage = appendCalls - appendAtPageStart; |
| 50 | + let kept = 0; |
| 51 | + const walker = document.createTreeWalker( |
| 52 | + pageElement, |
| 53 | + NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT |
| 54 | + ); |
| 55 | + let n; |
| 56 | + while ((n = walker.nextNode())) { |
| 57 | + if (n.__pagedjs_clone_tag) kept++; |
| 58 | + } |
| 59 | + perPage.push({ appended: appendedThisPage, kept }); |
| 60 | + } |
| 61 | + afterRendered(pages) { |
| 62 | + let totalAppended = 0; |
| 63 | + let totalKept = 0; |
| 64 | + let pagesWithOvershoot = 0; |
| 65 | + let maxOvershoot = 0; |
| 66 | + let maxOvershootPage = -1; |
| 67 | + const pcts = []; |
| 68 | + perPage.forEach((entry, idx) => { |
| 69 | + totalAppended += entry.appended; |
| 70 | + totalKept += entry.kept; |
| 71 | + const over = entry.appended - entry.kept; |
| 72 | + if (over > 0) pagesWithOvershoot++; |
| 73 | + if (over > maxOvershoot) { |
| 74 | + maxOvershoot = over; |
| 75 | + maxOvershootPage = idx; |
| 76 | + } |
| 77 | + pcts.push(entry.appended > 0 ? (over / entry.appended) * 100 : 0); |
| 78 | + }); |
| 79 | + const totalOvershoot = totalAppended - totalKept; |
| 80 | + const pct = totalAppended > 0 |
| 81 | + ? (totalOvershoot / totalAppended) * 100 |
| 82 | + : 0; |
| 83 | + |
| 84 | + console.log(`[clone-count] pages=${pages.length}`); |
| 85 | + console.log(`[clone-count] Layout.append calls (source-walker leaf clones): ${totalAppended}`); |
| 86 | + console.log(`[clone-count] survivors in finalized pages: ${totalKept}`); |
| 87 | + console.log(`[clone-count] overshoot (appended-then-removed): ${totalOvershoot} (${pct.toFixed(1)}%)`); |
| 88 | + console.log(`[clone-count] pages with any overshoot: ${pagesWithOvershoot}/${pages.length}`); |
| 89 | + console.log(`[clone-count] max overshoot on one page: ${maxOvershoot} (page index ${maxOvershootPage}, appended=${perPage[maxOvershootPage]?.appended ?? 0})`); |
| 90 | + console.log(`[clone-count] gross Node.cloneNode calls (incl. rebuildAncestors, handlers, etc.): ${cloneNodeCalls}`); |
| 91 | + console.log(`[clone-count] non-Layout.append clones: ${cloneNodeCalls - totalAppended}`); |
| 92 | + |
| 93 | + // Per-page overshoot % buckets. |
| 94 | + const buckets = [ |
| 95 | + { lo: 0, hi: 1 }, |
| 96 | + { lo: 1, hi: 5 }, |
| 97 | + { lo: 5, hi: 10 }, |
| 98 | + { lo: 10, hi: 20 }, |
| 99 | + { lo: 20, hi: 30 }, |
| 100 | + { lo: 30, hi: 50 }, |
| 101 | + { lo: 50, hi: 101 }, |
| 102 | + ]; |
| 103 | + const counts = buckets.map(() => 0); |
| 104 | + for (const p of pcts) { |
| 105 | + for (let i = 0; i < buckets.length; i++) { |
| 106 | + if (p >= buckets[i].lo && p < buckets[i].hi) { |
| 107 | + counts[i]++; |
| 108 | + break; |
| 109 | + } |
| 110 | + } |
| 111 | + } |
| 112 | + console.log(`[clone-count] per-page overshoot % distribution:`); |
| 113 | + for (let i = 0; i < buckets.length; i++) { |
| 114 | + const b = buckets[i]; |
| 115 | + const hi = b.hi === 101 ? '100' : String(b.hi); |
| 116 | + console.log(`[clone-count] ${String(b.lo).padStart(3)} - ${hi.padStart(3)}%: ${counts[i]} pages`); |
| 117 | + } |
| 118 | + |
| 119 | + // Cumulative percentile cutpoints. |
| 120 | + const sortedPcts = pcts.slice().sort((a, b) => a - b); |
| 121 | + const pickPct = (q) => sortedPcts[Math.min(sortedPcts.length - 1, Math.floor(q * sortedPcts.length))]; |
| 122 | + console.log(`[clone-count] per-page overshoot %: p50=${pickPct(0.5).toFixed(1)}% p90=${pickPct(0.9).toFixed(1)}% p99=${pickPct(0.99).toFixed(1)}% max=${pickPct(0.999).toFixed(1)}%`); |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + Paged.registerHandlers(CloneCountHandler); |
| 127 | + console.log('[clone-count] handler registered'); |
| 128 | +})(); |
0 commit comments