返回 DeepSeek-Reasonix
app-memory-evidence.test.mjs
根目录 / desktop / frontend / bench / app-memory-evidence.test.mjs
1 import assert from "node:assert/strict";
2 import { test } from "node:test";
3 import "./app-memory-workers.test.mjs";
4 import { attributeRetention, BASELINE_NOT_SETTLED_REASON, evidenceIntegrity, retainedCohorts, screeningBlockers, summarizeHeap, TRANSIENT_EXCURSION_REASON } from "./app-memory-evidence.mjs";
5
6 const sample = (ids, roundTrips) => ({ phase: "full", roundTrips, lifecycle: {
7 liveRenderTokenIds: ids, liveRenderTokens: ids.length,
8 activeOperations: 0, activeSubscriptions: 6, invariantViolations: 0, overflow: false,
9 } });
10 test("one measured parser worker is independent of DOM listener retention", () => {
11 const values = [0, 1, 0, 1].map((count, index) => ({
12 ...sample([1, index + 2], index * 32),
13 dom: { nodes: 6750, jsEventListeners: 359 + count * 3 },
14 workers: count ? [["error", "error", "message"]] : [],
15 }));
16 assert.deepEqual(screeningBlockers(attributeRetention(values).reasons), []);
17 values.at(-1).dom.jsEventListeners++;
18 assert.ok(screeningBlockers(attributeRetention(values).reasons).includes("post-gc-dom-or-listener-drift"));
19 });
20 test("worker accumulation and extra listeners still block screening", () => {
21 const baseline = { ...sample([1, 2], 0), dom: { nodes: 6750, jsEventListeners: 359 }, workers: [] };
22 for (const workers of [[["message"], ["message"]], [["message", "message"]], [["error", "error", "error"]], [["resize"]]]) {
23 const tail = { ...sample([1, 3], 32), dom: { nodes: 6750, jsEventListeners: 365 }, workers };
24 assert.ok(screeningBlockers(attributeRetention([baseline, tail]).reasons).includes("worker-population-drift"));
25 }
26 });
27 test("deliberately retained cohorts remain detectable even when totals are constant", () => {
28 const samples = [sample([1, 2], 0), sample([3, 4], 32), sample([3, 5], 64), sample([3, 6], 96)];
29 assert.equal(evidenceIntegrity(samples), true);
30 assert.deepEqual(retainedCohorts(samples).at(-1).retainedPostBaseline, [3]);
31 });
32 test("probe overflow, duplicate IDs and cleanup underflow invalidate evidence", () => {
33 for (const mutation of [{ overflow: true }, { invariantViolations: 1 }, { activeSubscriptions: -1 }, { liveRenderTokenIds: [1, 1] }]) {
34 const value = sample([1, 2], 0);
35 Object.assign(value.lifecycle, mutation);
36 assert.equal(evidenceIntegrity([value]), false);
37 }
38 });
39 test("stable owner counters alone cannot establish whole-App retention attribution", () => {
40 const samples = [
41 { ...sample([1, 2], 0), dom: { nodes: 6000, jsEventListeners: 500 } },
42 { ...sample([1, 3], 32), dom: { nodes: 6024, jsEventListeners: 512 } },
43 { ...sample([1, 4], 64), dom: { nodes: 6024, jsEventListeners: 512 } },
44 { ...sample([1, 5], 96), dom: { nodes: 6024, jsEventListeners: 512 } },
45 ];
46 assert.equal(attributeRetention(samples).status, "needs-attribution");
47 assert.ok(attributeRetention(samples).reasons.includes("heap-retainer-and-control-evidence-required"));
48 });
49 test("a stable tail cannot hide growth earlier in the post-GC sequence", () => {
50 const samples = [6000, 6024, 6100, 6100, 6100].map((nodes, index) => ({
51 ...sample([1, index + 2], index * 32), dom: { nodes, jsEventListeners: 500 },
52 }));
53 assert.ok(attributeRetention(samples).reasons.includes("post-gc-dom-or-listener-drift"));
54 });
55 test("missing or non-finite native counters are invalid evidence", () => {
56 for (const dom of [undefined, {}, { nodes: NaN, jsEventListeners: 500 }, { nodes: 6000, jsEventListeners: -1 }]) {
57 const samples = Array.from({ length: 4 }, (_, index) => ({ ...sample([1, index + 2], index * 32), dom }));
58 assert.ok(attributeRetention(samples).reasons.includes("invalid-native-counters"));
59 }
60 });
61 test("subscriptions retained after a round trip require attribution", () => {
62 const samples = Array.from({ length: 4 }, (_, index) => ({
63 ...sample([1, index + 2], index * 32), dom: { nodes: 6000, jsEventListeners: 500 },
64 }));
65 for (const retained of samples.slice(1)) retained.lifecycle.activeSubscriptions++;
66 assert.ok(attributeRetention(samples).reasons.includes("subscription-population-drift"));
67 });
68 test("persistent post-baseline cohorts remain a qualification blocker", () => {
69 const samples = [
70 { ...sample([1, 2], 0), dom: { nodes: 6000, jsEventListeners: 500 } },
71 { ...sample([1, 3], 32), dom: { nodes: 6024, jsEventListeners: 512 } },
72 { ...sample([1, 3], 64), dom: { nodes: 6024, jsEventListeners: 512 } },
73 { ...sample([1, 3], 96), dom: { nodes: 6024, jsEventListeners: 512 } },
74 ];
75 assert.equal(attributeRetention(samples).status, "needs-attribution");
76 });
77 test("the automated gate blocks on screening failures, not the offline attribution duty", () => {
78 const clean = Array.from({ length: 4 }, (_, index) => ({
79 ...sample([1, index + 2], index * 32), dom: { nodes: 6024, jsEventListeners: 512 },
80 }));
81 assert.deepEqual(screeningBlockers(attributeRetention(clean).reasons), []);
82 const drift = [6000, 6024, 6100, 6100].map((nodes, index) => ({
83 ...sample([1, index + 2], index * 32), dom: { nodes, jsEventListeners: 500 },
84 }));
85 assert.deepEqual(screeningBlockers(attributeRetention(drift).reasons), ["post-gc-dom-or-listener-drift"]);
86 assert.deepEqual(screeningBlockers(["missing-attribution"]), ["missing-attribution"]);
87 });
88 test("a mid-sequence excursion that fully returns to baseline is an observation, not a blocker", () => {
89 // The real Linux/Chromium soak shows 614 listeners at phase transitions
90 // settling back to the 512 baseline; freed counters are not retention.
91 const samples = Array.from({ length: 21 }, (_, index) => ({
92 ...sample([1, index + 2], index * 32),
93 dom: { nodes: 6049, jsEventListeners: index === 6 || index === 13 ? 614 : 512 },
94 }));
95 const result = attributeRetention(samples);
96 assert.ok(result.reasons.includes(TRANSIENT_EXCURSION_REASON));
97 assert.deepEqual(screeningBlockers(result.reasons), []);
98 });
99 test("a displaced final tail remains a persistent blocker", () => {
100 const samples = Array.from({ length: 21 }, (_, index) => ({
101 ...sample([1, index + 2], index * 32),
102 dom: { nodes: 6049, jsEventListeners: index === 20 ? 614 : 512 },
103 }));
104 const result = attributeRetention(samples);
105 assert.ok(result.reasons.includes("post-gc-dom-or-listener-drift"));
106 assert.deepEqual(screeningBlockers(result.reasons), ["post-gc-dom-or-listener-drift"]);
107 });
108 test("a settled population below baseline records release instead of retention", () => {
109 // A real shard settled from 346 to 343 listeners with stable nodes,
110 // subscriptions, operations, and render cohorts. Fewer live listeners
111 // cannot be evidence of retained listeners.
112 const samples = Array.from({ length: 21 }, (_, index) => ({
113 ...sample([1, index + 2], index * 32),
114 dom: { nodes: 6724, jsEventListeners: index === 5 || index === 6 ? 456 : 346 },
115 }));
116 samples.push({ ...sample([1, 23], 512), phase: "settled", dom: { nodes: 6724, jsEventListeners: 343 } });
117 const result = attributeRetention(samples);
118 assert.ok(result.reasons.includes(TRANSIENT_EXCURSION_REASON));
119 assert.deepEqual(screeningBlockers(result.reasons), []);
120 });
121 test("an explicit settled tail sample is the authoritative resting state", () => {
122 // Round 5 CI data: the blip can land on the final round checkpoint; the
123 // quiescent sample after it proves recovery, so this must not block.
124 const blipBeforeSettled = Array.from({ length: 21 }, (_, index) => ({
125 ...sample([1, index + 2], index * 32),
126 dom: { nodes: 6049, jsEventListeners: index === 20 ? 614 : 512 },
127 }));
128 blipBeforeSettled.push({ ...sample([1, 23], 512), phase: "settled", dom: { nodes: 6049, jsEventListeners: 512 } });
129 const recovered = attributeRetention(blipBeforeSettled);
130 assert.ok(recovered.reasons.includes(TRANSIENT_EXCURSION_REASON));
131 assert.deepEqual(screeningBlockers(recovered.reasons), []);
132 // A settled tail that stays displaced is still a persistent blocker.
133 const stuck = blipBeforeSettled.map((sample_) => ({ ...sample_ }));
134 stuck[21] = { ...stuck[21], dom: { nodes: 6049, jsEventListeners: 614 } };
135 assert.deepEqual(screeningBlockers(attributeRetention(stuck).reasons), ["post-gc-dom-or-listener-drift"]);
136 });
137 test("an unsettled baseline is reported instead of being judged as displacement", () => {
138 // Round 5 CI data: the baseline sample itself caught the cleanup blip (616),
139 // every later reading rested at 514, and the gate misreported the series as
140 // persistent drift.
141 const samples = Array.from({ length: 21 }, (_, index) => ({
142 ...sample([1, index + 2], index * 32),
143 dom: { nodes: 6049, jsEventListeners: index === 0 ? 616 : 512 },
144 }));
145 samples[0] = { ...samples[0], baselineStable: false, baselineReadings: [{ nodes: 6049, jsEventListeners: 616 }, { nodes: 6049, jsEventListeners: 512 }] };
146 samples.push({ ...sample([1, 23], 512), phase: "settled", dom: { nodes: 6049, jsEventListeners: 512 } });
147 const result = attributeRetention(samples);
148 assert.ok(result.reasons.includes(BASELINE_NOT_SETTLED_REASON));
149 assert.deepEqual(screeningBlockers(result.reasons), [BASELINE_NOT_SETTLED_REASON]);
150 });
151 test("an unsettled baseline still blocks sustained growth away from a low point", () => {
152 // 616 -> 400 -> 450 -> 500 -> 550 ends below the baseline but keeps growing;
153 // skipping the displacement check must not hide it.
154 const listeners = [616, 400, 450, 500, 550];
155 const samples = listeners.map((value, index) => ({
156 ...sample([1, index + 2], index * 32),
157 dom: { nodes: 6049, jsEventListeners: value },
158 }));
159 samples[0] = { ...samples[0], baselineStable: false };
160 const result = attributeRetention(samples);
161 assert.ok(result.reasons.includes("post-gc-dom-or-listener-drift"));
162 assert.deepEqual(screeningBlockers(result.reasons), [BASELINE_NOT_SETTLED_REASON, "post-gc-dom-or-listener-drift"]);
163 });
164 test("a settled baseline keeps the displacement check", () => {
165 const samples = Array.from({ length: 21 }, (_, index) => ({
166 ...sample([1, index + 2], index * 32),
167 dom: { nodes: 6049, jsEventListeners: index === 0 ? 512 : 514 },
168 }));
169 samples[0] = { ...samples[0], baselineStable: true };
170 samples.push({ ...sample([1, 23], 512), phase: "settled", dom: { nodes: 6049, jsEventListeners: 514 } });
171 assert.deepEqual(screeningBlockers(attributeRetention(samples).reasons), ["post-gc-dom-or-listener-drift"]);
172 });
173 test("native objects are not automatically detached DOM", () => {
174 const heap = { snapshot: { meta: {
175 node_fields: ["type", "name", "id", "self_size", "detachedness"],
176 node_types: [["native", "code"], [], [], [], []],
177 } }, strings: ["HTMLDivElement", "compiled function"],
178 nodes: [0, 0, 1, 64, 1, 0, 0, 2, 64, 2, 1, 1, 3, 128, 0] };
179 const result = summarizeHeap(heap);
180 assert.equal(result.categories.native.count, 2);
181 assert.equal(result.categories.code.selfBytes, 128);
182 assert.deepEqual(result.detached.HTMLDivElement.ids, [2]);
183 heap.snapshot.meta.node_fields[4] = "unknown";
184 assert.equal(summarizeHeap(heap).detachednessAvailable, false);
185 assert.deepEqual(summarizeHeap(heap).detached, {});
186 });
187
187 lines Plain Text