@svrnsec/pulse 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +883 -622
- package/SECURITY.md +86 -86
- package/bin/svrnsec-pulse.js +7 -7
- package/dist/{pulse.cjs.js → pulse.cjs} +6379 -6420
- package/dist/pulse.cjs.map +1 -0
- package/dist/pulse.esm.js +6380 -6421
- package/dist/pulse.esm.js.map +1 -1
- package/index.d.ts +895 -846
- package/package.json +185 -165
- package/pkg/pulse_core.js +174 -173
- package/src/analysis/audio.js +213 -213
- package/src/analysis/authenticityAudit.js +408 -390
- package/src/analysis/coherence.js +502 -502
- package/src/analysis/coordinatedBehavior.js +825 -0
- package/src/analysis/heuristic.js +428 -428
- package/src/analysis/jitter.js +446 -446
- package/src/analysis/llm.js +473 -472
- package/src/analysis/populationEntropy.js +404 -403
- package/src/analysis/provider.js +248 -248
- package/src/analysis/refraction.js +392 -0
- package/src/analysis/trustScore.js +356 -356
- package/src/cli/args.js +36 -36
- package/src/cli/commands/scan.js +192 -192
- package/src/cli/runner.js +157 -157
- package/src/collector/adaptive.js +200 -200
- package/src/collector/bio.js +297 -287
- package/src/collector/canvas.js +247 -239
- package/src/collector/dram.js +203 -203
- package/src/collector/enf.js +311 -311
- package/src/collector/entropy.js +195 -195
- package/src/collector/gpu.js +248 -245
- package/src/collector/idleAttestation.js +480 -480
- package/src/collector/sabTimer.js +189 -191
- package/src/fingerprint.js +475 -475
- package/src/index.js +342 -342
- package/src/integrations/react-native.js +462 -459
- package/src/integrations/react.js +184 -185
- package/src/middleware/express.js +155 -155
- package/src/middleware/next.js +174 -175
- package/src/proof/challenge.js +249 -249
- package/src/proof/engagementToken.js +426 -394
- package/src/proof/fingerprint.js +268 -268
- package/src/proof/validator.js +83 -143
- package/src/registry/serializer.js +349 -349
- package/src/terminal.js +263 -263
- package/src/update-notifier.js +259 -264
- package/dist/pulse.cjs.js.map +0 -1
|
@@ -1,200 +1,200 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @
|
|
3
|
-
*
|
|
4
|
-
* Runs the WASM probe in batches and stops early once the signal is decisive.
|
|
5
|
-
*
|
|
6
|
-
* Why this works:
|
|
7
|
-
* A KVM VM with QE=1.27 and lag-1 autocorr=0.67 is unambiguously a VM after
|
|
8
|
-
* just 50 iterations. Running 200 iterations confirms what was already obvious
|
|
9
|
-
* at 50 — it adds no new information but wastes 3 seconds of user time.
|
|
10
|
-
*
|
|
11
|
-
* Conversely, a physical device with healthy entropy needs more data to
|
|
12
|
-
* rule out edge cases, so it runs longer.
|
|
13
|
-
*
|
|
14
|
-
* Speed profile:
|
|
15
|
-
* Obvious VM (QE < 1.5, lag1 > 0.60) → stops at 50 iters → ~0.9s (75% faster)
|
|
16
|
-
* Clear HW (QE > 3.5, lag1 < 0.10) → stops at ~100 iters → ~1.8s (50% faster)
|
|
17
|
-
* Ambiguous (borderline metrics) → runs full 200 iters → ~3.5s (same)
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { detectQuantizationEntropy } from '../analysis/jitter.js';
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Quick classifier (cheap, runs after every batch)
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Fast signal-quality check. No Hurst, no thermal analysis — just the three
|
|
28
|
-
* metrics that converge quickest: QE, CV, and lag-1 autocorrelation.
|
|
29
|
-
*
|
|
30
|
-
* @param {number[]} timings
|
|
31
|
-
* @returns {{ vmConf: number, hwConf: number, qe: number, cv: number, lag1: number }}
|
|
32
|
-
*/
|
|
33
|
-
export function quickSignal(timings) {
|
|
34
|
-
const n = timings.length;
|
|
35
|
-
const mean = timings.reduce((s, v) => s + v, 0) / n;
|
|
36
|
-
const variance = timings.reduce((s, v) => s + (v - mean) ** 2, 0) / n;
|
|
37
|
-
const cv = mean > 0 ? Math.sqrt(variance) / mean : 0;
|
|
38
|
-
const qe = detectQuantizationEntropy(timings);
|
|
39
|
-
|
|
40
|
-
// Pearson autocorrelation at lag-1 (O(n), fits in a single pass)
|
|
41
|
-
let num = 0, da = 0, db = 0;
|
|
42
|
-
for (let i = 0; i < n - 1; i++) {
|
|
43
|
-
const a = timings[i] - mean;
|
|
44
|
-
const b = timings[i + 1] - mean;
|
|
45
|
-
num += a * b;
|
|
46
|
-
da += a * a;
|
|
47
|
-
db += b * b;
|
|
48
|
-
}
|
|
49
|
-
const lag1 = Math.sqrt(da * db) < 1e-14 ? 0 : num / Math.sqrt(da * db);
|
|
50
|
-
|
|
51
|
-
// VM confidence: each factor independently identifies the hypervisor footprint
|
|
52
|
-
const vmConf = Math.min(1,
|
|
53
|
-
(qe < 1.50 ? 0.40 : qe < 2.00 ? 0.20 : 0.0) +
|
|
54
|
-
(lag1 > 0.60 ? 0.35 : lag1 > 0.40 ? 0.18 : 0.0) +
|
|
55
|
-
(cv < 0.04 ? 0.25 : cv < 0.07 ? 0.10 : 0.0)
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
// HW confidence: must see all three positive signals together
|
|
59
|
-
const hwConf = Math.min(1,
|
|
60
|
-
(qe > 3.50 ? 0.38 : qe > 3.00 ? 0.22 : 0.0) +
|
|
61
|
-
(Math.abs(lag1) < 0.10 ? 0.32 : Math.abs(lag1) < 0.20 ? 0.15 : 0.0) +
|
|
62
|
-
(cv > 0.10 ? 0.30 : cv > 0.07 ? 0.14 : 0.0)
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
return { vmConf, hwConf, qe, cv, lag1 };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
// collectEntropyAdaptive
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @param {object} opts
|
|
74
|
-
* @param {number} [opts.minIterations=50] - never stop before this
|
|
75
|
-
* @param {number} [opts.maxIterations=200] - hard cap
|
|
76
|
-
* @param {number} [opts.batchSize=25] - WASM call granularity
|
|
77
|
-
* @param {number} [opts.vmThreshold=0.85] - stop early if VM confidence ≥ this
|
|
78
|
-
* @param {number} [opts.hwThreshold=0.80] - stop early if HW confidence ≥ this
|
|
79
|
-
* @param {number} [opts.hwMinIterations=75] - physical needs more data to confirm
|
|
80
|
-
* @param {number} [opts.matrixSize=64]
|
|
81
|
-
* @param {Function} [opts.onBatch] - called after each batch with interim signal
|
|
82
|
-
* @param {string} [opts.wasmPath]
|
|
83
|
-
* @param {Function} wasmModule - pre-initialised WASM module
|
|
84
|
-
* @returns {Promise<AdaptiveEntropyResult>}
|
|
85
|
-
*/
|
|
86
|
-
export async function collectEntropyAdaptive(wasmModule, opts = {}) {
|
|
87
|
-
const {
|
|
88
|
-
minIterations = 50,
|
|
89
|
-
maxIterations = 200,
|
|
90
|
-
batchSize = 25,
|
|
91
|
-
vmThreshold = 0.85,
|
|
92
|
-
hwThreshold = 0.80,
|
|
93
|
-
hwMinIterations = 75,
|
|
94
|
-
matrixSize = 64,
|
|
95
|
-
onBatch,
|
|
96
|
-
} = opts;
|
|
97
|
-
|
|
98
|
-
const wasm = wasmModule;
|
|
99
|
-
const allTimings = [];
|
|
100
|
-
const batches = []; // per-batch timing snapshots
|
|
101
|
-
let stoppedAt = null; // { reason, iterations, vmConf, hwConf }
|
|
102
|
-
let checksum = 0;
|
|
103
|
-
|
|
104
|
-
const t_start = Date.now();
|
|
105
|
-
|
|
106
|
-
while (allTimings.length < maxIterations) {
|
|
107
|
-
const n = Math.min(batchSize, maxIterations - allTimings.length);
|
|
108
|
-
const result = wasm.run_entropy_probe(n, matrixSize);
|
|
109
|
-
const chunk = Array.from(result.timings);
|
|
110
|
-
|
|
111
|
-
allTimings.push(...chunk);
|
|
112
|
-
checksum += result.checksum;
|
|
113
|
-
|
|
114
|
-
const sig = quickSignal(allTimings);
|
|
115
|
-
batches.push({ iterations: allTimings.length, ...sig });
|
|
116
|
-
|
|
117
|
-
// Fire progress callback with live signal so callers can stream to UI
|
|
118
|
-
if (typeof onBatch === 'function') {
|
|
119
|
-
try {
|
|
120
|
-
onBatch({
|
|
121
|
-
iterations: allTimings.length,
|
|
122
|
-
maxIterations,
|
|
123
|
-
pct: Math.round(allTimings.length / maxIterations * 100),
|
|
124
|
-
vmConf: sig.vmConf,
|
|
125
|
-
hwConf: sig.hwConf,
|
|
126
|
-
qe: sig.qe,
|
|
127
|
-
cv: sig.cv,
|
|
128
|
-
lag1: sig.lag1,
|
|
129
|
-
// Thresholds: 0.70 — high enough that a legitimate device won't be
|
|
130
|
-
// shown a false early verdict from a noisy first batch.
|
|
131
|
-
// 'borderline' surfaces when one axis is moderate but not decisive.
|
|
132
|
-
earlyVerdict: sig.vmConf > 0.70 ? 'vm'
|
|
133
|
-
: sig.hwConf > 0.70 ? 'physical'
|
|
134
|
-
: (sig.vmConf > 0.45 || sig.hwConf > 0.45) ? 'borderline'
|
|
135
|
-
: 'uncertain',
|
|
136
|
-
});
|
|
137
|
-
} catch {}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ── Early-exit checks ──────────────────────────────────────────────────
|
|
141
|
-
if (allTimings.length < minIterations) continue;
|
|
142
|
-
|
|
143
|
-
if (sig.vmConf >= vmThreshold) {
|
|
144
|
-
stoppedAt = { reason: 'VM_SIGNAL_DECISIVE', vmConf: sig.vmConf, hwConf: sig.hwConf };
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (allTimings.length >= hwMinIterations && sig.hwConf >= hwThreshold) {
|
|
149
|
-
stoppedAt = { reason: 'PHYSICAL_SIGNAL_DECISIVE', vmConf: sig.vmConf, hwConf: sig.hwConf };
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const elapsed = Date.now() - t_start;
|
|
155
|
-
const iterationsRan = allTimings.length;
|
|
156
|
-
const iterationsSaved = maxIterations - iterationsRan;
|
|
157
|
-
const speedupFactor = maxIterations / iterationsRan;
|
|
158
|
-
|
|
159
|
-
// ── Resolution probe using cached WASM call ────────────────────────────
|
|
160
|
-
const resResult = wasm.run_entropy_probe(1, 4); // tiny probe for resolution
|
|
161
|
-
const resProbe = Array.from(resResult.resolution_probe ?? []);
|
|
162
|
-
|
|
163
|
-
const resDeltas = [];
|
|
164
|
-
for (let i = 1; i < resProbe.length; i++) {
|
|
165
|
-
const d = resProbe[i] - resProbe[i - 1];
|
|
166
|
-
if (d > 0) resDeltas.push(d);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
timings: allTimings,
|
|
171
|
-
iterations: iterationsRan,
|
|
172
|
-
maxIterations,
|
|
173
|
-
checksum: checksum.toString(),
|
|
174
|
-
resolutionProbe: resProbe,
|
|
175
|
-
timerGranularityMs: resDeltas.length
|
|
176
|
-
? resDeltas.reduce((a, b) => Math.min(a, b), Infinity)
|
|
177
|
-
: null,
|
|
178
|
-
earlyExit: stoppedAt ? {
|
|
179
|
-
...stoppedAt,
|
|
180
|
-
iterationsSaved,
|
|
181
|
-
timeSavedMs: Math.round(iterationsSaved * (elapsed / iterationsRan)),
|
|
182
|
-
speedupFactor: +speedupFactor.toFixed(2),
|
|
183
|
-
} : null,
|
|
184
|
-
batches,
|
|
185
|
-
elapsedMs: elapsed,
|
|
186
|
-
collectedAt: t_start,
|
|
187
|
-
matrixSize,
|
|
188
|
-
phased: false, // adaptive replaces phased for speed
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* @typedef {object} AdaptiveEntropyResult
|
|
194
|
-
* @property {number[]} timings
|
|
195
|
-
* @property {number} iterations - how many actually ran
|
|
196
|
-
* @property {number} maxIterations - cap that was set
|
|
197
|
-
* @property {object|null} earlyExit - null if ran to completion
|
|
198
|
-
* @property {object[]} batches - per-batch signal snapshots
|
|
199
|
-
* @property {number} elapsedMs
|
|
200
|
-
*/
|
|
1
|
+
/**
|
|
2
|
+
* @svrnsec/pulse — Adaptive Entropy Probe
|
|
3
|
+
*
|
|
4
|
+
* Runs the WASM probe in batches and stops early once the signal is decisive.
|
|
5
|
+
*
|
|
6
|
+
* Why this works:
|
|
7
|
+
* A KVM VM with QE=1.27 and lag-1 autocorr=0.67 is unambiguously a VM after
|
|
8
|
+
* just 50 iterations. Running 200 iterations confirms what was already obvious
|
|
9
|
+
* at 50 — it adds no new information but wastes 3 seconds of user time.
|
|
10
|
+
*
|
|
11
|
+
* Conversely, a physical device with healthy entropy needs more data to
|
|
12
|
+
* rule out edge cases, so it runs longer.
|
|
13
|
+
*
|
|
14
|
+
* Speed profile:
|
|
15
|
+
* Obvious VM (QE < 1.5, lag1 > 0.60) → stops at 50 iters → ~0.9s (75% faster)
|
|
16
|
+
* Clear HW (QE > 3.5, lag1 < 0.10) → stops at ~100 iters → ~1.8s (50% faster)
|
|
17
|
+
* Ambiguous (borderline metrics) → runs full 200 iters → ~3.5s (same)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { detectQuantizationEntropy } from '../analysis/jitter.js';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Quick classifier (cheap, runs after every batch)
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fast signal-quality check. No Hurst, no thermal analysis — just the three
|
|
28
|
+
* metrics that converge quickest: QE, CV, and lag-1 autocorrelation.
|
|
29
|
+
*
|
|
30
|
+
* @param {number[]} timings
|
|
31
|
+
* @returns {{ vmConf: number, hwConf: number, qe: number, cv: number, lag1: number }}
|
|
32
|
+
*/
|
|
33
|
+
export function quickSignal(timings) {
|
|
34
|
+
const n = timings.length;
|
|
35
|
+
const mean = timings.reduce((s, v) => s + v, 0) / n;
|
|
36
|
+
const variance = timings.reduce((s, v) => s + (v - mean) ** 2, 0) / n;
|
|
37
|
+
const cv = mean > 0 ? Math.sqrt(variance) / mean : 0;
|
|
38
|
+
const qe = detectQuantizationEntropy(timings);
|
|
39
|
+
|
|
40
|
+
// Pearson autocorrelation at lag-1 (O(n), fits in a single pass)
|
|
41
|
+
let num = 0, da = 0, db = 0;
|
|
42
|
+
for (let i = 0; i < n - 1; i++) {
|
|
43
|
+
const a = timings[i] - mean;
|
|
44
|
+
const b = timings[i + 1] - mean;
|
|
45
|
+
num += a * b;
|
|
46
|
+
da += a * a;
|
|
47
|
+
db += b * b;
|
|
48
|
+
}
|
|
49
|
+
const lag1 = Math.sqrt(da * db) < 1e-14 ? 0 : num / Math.sqrt(da * db);
|
|
50
|
+
|
|
51
|
+
// VM confidence: each factor independently identifies the hypervisor footprint
|
|
52
|
+
const vmConf = Math.min(1,
|
|
53
|
+
(qe < 1.50 ? 0.40 : qe < 2.00 ? 0.20 : 0.0) +
|
|
54
|
+
(lag1 > 0.60 ? 0.35 : lag1 > 0.40 ? 0.18 : 0.0) +
|
|
55
|
+
(cv < 0.04 ? 0.25 : cv < 0.07 ? 0.10 : 0.0)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// HW confidence: must see all three positive signals together
|
|
59
|
+
const hwConf = Math.min(1,
|
|
60
|
+
(qe > 3.50 ? 0.38 : qe > 3.00 ? 0.22 : 0.0) +
|
|
61
|
+
(Math.abs(lag1) < 0.10 ? 0.32 : Math.abs(lag1) < 0.20 ? 0.15 : 0.0) +
|
|
62
|
+
(cv > 0.10 ? 0.30 : cv > 0.07 ? 0.14 : 0.0)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return { vmConf, hwConf, qe, cv, lag1 };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// collectEntropyAdaptive
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {object} opts
|
|
74
|
+
* @param {number} [opts.minIterations=50] - never stop before this
|
|
75
|
+
* @param {number} [opts.maxIterations=200] - hard cap
|
|
76
|
+
* @param {number} [opts.batchSize=25] - WASM call granularity
|
|
77
|
+
* @param {number} [opts.vmThreshold=0.85] - stop early if VM confidence ≥ this
|
|
78
|
+
* @param {number} [opts.hwThreshold=0.80] - stop early if HW confidence ≥ this
|
|
79
|
+
* @param {number} [opts.hwMinIterations=75] - physical needs more data to confirm
|
|
80
|
+
* @param {number} [opts.matrixSize=64]
|
|
81
|
+
* @param {Function} [opts.onBatch] - called after each batch with interim signal
|
|
82
|
+
* @param {string} [opts.wasmPath]
|
|
83
|
+
* @param {Function} wasmModule - pre-initialised WASM module
|
|
84
|
+
* @returns {Promise<AdaptiveEntropyResult>}
|
|
85
|
+
*/
|
|
86
|
+
export async function collectEntropyAdaptive(wasmModule, opts = {}) {
|
|
87
|
+
const {
|
|
88
|
+
minIterations = 50,
|
|
89
|
+
maxIterations = 200,
|
|
90
|
+
batchSize = 25,
|
|
91
|
+
vmThreshold = 0.85,
|
|
92
|
+
hwThreshold = 0.80,
|
|
93
|
+
hwMinIterations = 75,
|
|
94
|
+
matrixSize = 64,
|
|
95
|
+
onBatch,
|
|
96
|
+
} = opts;
|
|
97
|
+
|
|
98
|
+
const wasm = wasmModule;
|
|
99
|
+
const allTimings = [];
|
|
100
|
+
const batches = []; // per-batch timing snapshots
|
|
101
|
+
let stoppedAt = null; // { reason, iterations, vmConf, hwConf }
|
|
102
|
+
let checksum = 0;
|
|
103
|
+
|
|
104
|
+
const t_start = Date.now();
|
|
105
|
+
|
|
106
|
+
while (allTimings.length < maxIterations) {
|
|
107
|
+
const n = Math.min(batchSize, maxIterations - allTimings.length);
|
|
108
|
+
const result = wasm.run_entropy_probe(n, matrixSize);
|
|
109
|
+
const chunk = Array.from(result.timings);
|
|
110
|
+
|
|
111
|
+
allTimings.push(...chunk);
|
|
112
|
+
checksum += result.checksum;
|
|
113
|
+
|
|
114
|
+
const sig = quickSignal(allTimings);
|
|
115
|
+
batches.push({ iterations: allTimings.length, ...sig });
|
|
116
|
+
|
|
117
|
+
// Fire progress callback with live signal so callers can stream to UI
|
|
118
|
+
if (typeof onBatch === 'function') {
|
|
119
|
+
try {
|
|
120
|
+
onBatch({
|
|
121
|
+
iterations: allTimings.length,
|
|
122
|
+
maxIterations,
|
|
123
|
+
pct: Math.round(allTimings.length / maxIterations * 100),
|
|
124
|
+
vmConf: sig.vmConf,
|
|
125
|
+
hwConf: sig.hwConf,
|
|
126
|
+
qe: sig.qe,
|
|
127
|
+
cv: sig.cv,
|
|
128
|
+
lag1: sig.lag1,
|
|
129
|
+
// Thresholds: 0.70 — high enough that a legitimate device won't be
|
|
130
|
+
// shown a false early verdict from a noisy first batch.
|
|
131
|
+
// 'borderline' surfaces when one axis is moderate but not decisive.
|
|
132
|
+
earlyVerdict: sig.vmConf > 0.70 ? 'vm'
|
|
133
|
+
: sig.hwConf > 0.70 ? 'physical'
|
|
134
|
+
: (sig.vmConf > 0.45 || sig.hwConf > 0.45) ? 'borderline'
|
|
135
|
+
: 'uncertain',
|
|
136
|
+
});
|
|
137
|
+
} catch (e) { if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') console.error('[pulse] onBatch error:', e); }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Early-exit checks ──────────────────────────────────────────────────
|
|
141
|
+
if (allTimings.length < minIterations) continue;
|
|
142
|
+
|
|
143
|
+
if (sig.vmConf >= vmThreshold) {
|
|
144
|
+
stoppedAt = { reason: 'VM_SIGNAL_DECISIVE', vmConf: sig.vmConf, hwConf: sig.hwConf };
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (allTimings.length >= hwMinIterations && sig.hwConf >= hwThreshold) {
|
|
149
|
+
stoppedAt = { reason: 'PHYSICAL_SIGNAL_DECISIVE', vmConf: sig.vmConf, hwConf: sig.hwConf };
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const elapsed = Date.now() - t_start;
|
|
155
|
+
const iterationsRan = allTimings.length;
|
|
156
|
+
const iterationsSaved = maxIterations - iterationsRan;
|
|
157
|
+
const speedupFactor = maxIterations / iterationsRan;
|
|
158
|
+
|
|
159
|
+
// ── Resolution probe using cached WASM call ────────────────────────────
|
|
160
|
+
const resResult = wasm.run_entropy_probe(1, 4); // tiny probe for resolution
|
|
161
|
+
const resProbe = Array.from(resResult.resolution_probe ?? []);
|
|
162
|
+
|
|
163
|
+
const resDeltas = [];
|
|
164
|
+
for (let i = 1; i < resProbe.length; i++) {
|
|
165
|
+
const d = resProbe[i] - resProbe[i - 1];
|
|
166
|
+
if (d > 0) resDeltas.push(d);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
timings: allTimings,
|
|
171
|
+
iterations: iterationsRan,
|
|
172
|
+
maxIterations,
|
|
173
|
+
checksum: checksum.toString(),
|
|
174
|
+
resolutionProbe: resProbe,
|
|
175
|
+
timerGranularityMs: resDeltas.length
|
|
176
|
+
? resDeltas.reduce((a, b) => Math.min(a, b), Infinity)
|
|
177
|
+
: null,
|
|
178
|
+
earlyExit: stoppedAt ? {
|
|
179
|
+
...stoppedAt,
|
|
180
|
+
iterationsSaved,
|
|
181
|
+
timeSavedMs: Math.round(iterationsSaved * (elapsed / iterationsRan)),
|
|
182
|
+
speedupFactor: +speedupFactor.toFixed(2),
|
|
183
|
+
} : null,
|
|
184
|
+
batches,
|
|
185
|
+
elapsedMs: elapsed,
|
|
186
|
+
collectedAt: t_start,
|
|
187
|
+
matrixSize,
|
|
188
|
+
phased: false, // adaptive replaces phased for speed
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @typedef {object} AdaptiveEntropyResult
|
|
194
|
+
* @property {number[]} timings
|
|
195
|
+
* @property {number} iterations - how many actually ran
|
|
196
|
+
* @property {number} maxIterations - cap that was set
|
|
197
|
+
* @property {object|null} earlyExit - null if ran to completion
|
|
198
|
+
* @property {object[]} batches - per-batch signal snapshots
|
|
199
|
+
* @property {number} elapsedMs
|
|
200
|
+
*/
|