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