@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
package/src/collector/entropy.js
CHANGED
|
@@ -1,195 +1,195 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @
|
|
3
|
-
*
|
|
4
|
-
* Bridges the Rust/WASM matrix-multiply probe into JavaScript.
|
|
5
|
-
* The WASM module is lazily initialised once and cached for subsequent calls.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { collectEntropyAdaptive } from './adaptive.js';
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// WASM loader (lazy singleton)
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
let _wasmModule = null;
|
|
14
|
-
let _initPromise = null;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Initialise (or return the cached) WASM module.
|
|
18
|
-
* Works in browsers (via fetch), in Electron (Node.js context), and in
|
|
19
|
-
* Jest/Vitest via a manual WASM path override.
|
|
20
|
-
*
|
|
21
|
-
* @param {string} [wasmPath] – override path/URL to the .wasm binary
|
|
22
|
-
*/
|
|
23
|
-
async function initWasm(wasmPath) {
|
|
24
|
-
if (_wasmModule) return _wasmModule;
|
|
25
|
-
if (_initPromise) return _initPromise;
|
|
26
|
-
|
|
27
|
-
_initPromise = (async () => {
|
|
28
|
-
// Dynamic import so bundlers can tree-shake this for server-only builds.
|
|
29
|
-
const { default: init, run_entropy_probe, run_memory_probe, compute_autocorrelation } =
|
|
30
|
-
await import('../../pkg/pulse_core.js');
|
|
31
|
-
|
|
32
|
-
const url = wasmPath ?? new URL('../../pkg/pulse_core_bg.wasm', import.meta.url).href;
|
|
33
|
-
await init(url);
|
|
34
|
-
|
|
35
|
-
_wasmModule = { run_entropy_probe, run_memory_probe, compute_autocorrelation };
|
|
36
|
-
return _wasmModule;
|
|
37
|
-
})();
|
|
38
|
-
|
|
39
|
-
return _initPromise;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
// collectEntropy
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Run the WASM entropy probe and return raw timing data.
|
|
48
|
-
*
|
|
49
|
-
* @param {object} opts
|
|
50
|
-
* @param {number} [opts.iterations=200] - number of matrix-multiply rounds
|
|
51
|
-
* @param {number} [opts.matrixSize=64] - N for the N×N matrices
|
|
52
|
-
* @param {number} [opts.memSizeKb=512] - size of the memory bandwidth probe
|
|
53
|
-
* @param {number} [opts.memIterations=50]
|
|
54
|
-
* @param {boolean} [opts.phased=true] - run cold/load/hot phases for entropy-jitter ratio
|
|
55
|
-
* @param {string} [opts.wasmPath] - optional custom WASM binary path
|
|
56
|
-
*
|
|
57
|
-
* @returns {Promise<EntropyResult>}
|
|
58
|
-
*/
|
|
59
|
-
export async function collectEntropy(opts = {}) {
|
|
60
|
-
const {
|
|
61
|
-
iterations = 200,
|
|
62
|
-
matrixSize = 64,
|
|
63
|
-
memSizeKb = 512,
|
|
64
|
-
memIterations = 50,
|
|
65
|
-
phased = true,
|
|
66
|
-
adaptive = false,
|
|
67
|
-
adaptiveThreshold = 0.85,
|
|
68
|
-
onBatch,
|
|
69
|
-
wasmPath,
|
|
70
|
-
} = opts;
|
|
71
|
-
|
|
72
|
-
const wasm = await initWasm(wasmPath);
|
|
73
|
-
const t_start = Date.now();
|
|
74
|
-
|
|
75
|
-
let phases = null;
|
|
76
|
-
let timings, resolutionProbe, checksum, timerGranularityMs;
|
|
77
|
-
let _adaptiveInfo = null;
|
|
78
|
-
|
|
79
|
-
// ── Adaptive mode: smart early exit, fastest for obvious VMs ──────────
|
|
80
|
-
if (adaptive) {
|
|
81
|
-
const r = await collectEntropyAdaptive(wasm, {
|
|
82
|
-
minIterations: 50,
|
|
83
|
-
maxIterations: iterations,
|
|
84
|
-
batchSize: 25,
|
|
85
|
-
vmThreshold: adaptiveThreshold,
|
|
86
|
-
hwThreshold: 0.80,
|
|
87
|
-
hwMinIterations: 75,
|
|
88
|
-
matrixSize,
|
|
89
|
-
onBatch,
|
|
90
|
-
});
|
|
91
|
-
timings = r.timings;
|
|
92
|
-
resolutionProbe = r.resolutionProbe ?? [];
|
|
93
|
-
checksum = r.checksum;
|
|
94
|
-
timerGranularityMs = r.timerGranularityMs;
|
|
95
|
-
_adaptiveInfo = { earlyExit: r.earlyExit, batches: r.batches, elapsedMs: r.elapsedMs };
|
|
96
|
-
|
|
97
|
-
// ── Phased collection: cold → load → hot ──────────────────────────────
|
|
98
|
-
// Each phase runs a separate WASM probe. On real hardware, sustained load
|
|
99
|
-
// increases thermal noise so Phase 3 (hot) entropy is measurably higher
|
|
100
|
-
// than Phase 1 (cold). A VM's hypervisor clock is insensitive to guest
|
|
101
|
-
// thermal state, so all three phases return nearly identical entropy.
|
|
102
|
-
} else if (phased && iterations >= 60) {
|
|
103
|
-
const coldN = Math.floor(iterations * 0.25); // ~25% cold
|
|
104
|
-
const loadN = Math.floor(iterations * 0.50); // ~50% sustained load
|
|
105
|
-
const hotN = iterations - coldN - loadN; // ~25% hot
|
|
106
|
-
|
|
107
|
-
const cold = wasm.run_entropy_probe(coldN, matrixSize);
|
|
108
|
-
const load = wasm.run_entropy_probe(loadN, matrixSize);
|
|
109
|
-
const hot = wasm.run_entropy_probe(hotN, matrixSize);
|
|
110
|
-
|
|
111
|
-
const coldTimings = Array.from(cold.timings);
|
|
112
|
-
const loadTimings = Array.from(load.timings);
|
|
113
|
-
const hotTimings = Array.from(hot.timings);
|
|
114
|
-
|
|
115
|
-
timings = [...coldTimings, ...loadTimings, ...hotTimings];
|
|
116
|
-
resolutionProbe = Array.from(cold.resolution_probe);
|
|
117
|
-
checksum = (cold.checksum + load.checksum + hot.checksum).toString();
|
|
118
|
-
|
|
119
|
-
const { detectQuantizationEntropy } = await import('../analysis/jitter.js');
|
|
120
|
-
const coldQE = detectQuantizationEntropy(coldTimings);
|
|
121
|
-
const hotQE = detectQuantizationEntropy(hotTimings);
|
|
122
|
-
|
|
123
|
-
phases = {
|
|
124
|
-
cold: { n: coldN, timings: coldTimings, qe: coldQE, mean: _mean(coldTimings) },
|
|
125
|
-
load: { n: loadN, timings: loadTimings, qe: detectQuantizationEntropy(loadTimings), mean: _mean(loadTimings) },
|
|
126
|
-
hot: { n: hotN, timings: hotTimings, qe: hotQE, mean: _mean(hotTimings) },
|
|
127
|
-
// The key signal: entropy growth under load.
|
|
128
|
-
// Real silicon: hotQE / coldQE typically 1.05 – 1.40
|
|
129
|
-
// VM: hotQE / coldQE typically 0.95 – 1.05 (flat)
|
|
130
|
-
entropyJitterRatio: coldQE > 0 ? hotQE / coldQE : 1.0,
|
|
131
|
-
};
|
|
132
|
-
} else {
|
|
133
|
-
// Single-phase fallback (fewer iterations or phased disabled)
|
|
134
|
-
const result = wasm.run_entropy_probe(iterations, matrixSize);
|
|
135
|
-
timings = Array.from(result.timings);
|
|
136
|
-
resolutionProbe = Array.from(result.resolution_probe);
|
|
137
|
-
checksum = result.checksum.toString();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ── Timer resolution (non-adaptive path only — adaptive computes its own) ─
|
|
141
|
-
if (!adaptive) {
|
|
142
|
-
const resDeltas = [];
|
|
143
|
-
for (let i = 1; i < resolutionProbe.length; i++) {
|
|
144
|
-
const d = resolutionProbe[i] - resolutionProbe[i - 1];
|
|
145
|
-
if (d > 0) resDeltas.push(d);
|
|
146
|
-
}
|
|
147
|
-
timerGranularityMs = resDeltas.length
|
|
148
|
-
? resDeltas.reduce((a, b) => Math.min(a, b), Infinity)
|
|
149
|
-
: null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ── Autocorrelation at diagnostic lags ────────────────────────────────
|
|
153
|
-
// Extended lags catch long-period steal-time rhythms (Xen: ~150 iters)
|
|
154
|
-
const lags = [1, 2, 3, 5, 10, 25, 50];
|
|
155
|
-
const autocorrelations = {};
|
|
156
|
-
for (const lag of lags) {
|
|
157
|
-
if (lag < timings.length) {
|
|
158
|
-
autocorrelations[`lag${lag}`] = wasm.compute_autocorrelation(timings, lag);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ── Secondary probe: memory bandwidth jitter ───────────────────────────
|
|
163
|
-
const memTimings = Array.from(wasm.run_memory_probe(memSizeKb, memIterations));
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
timings,
|
|
167
|
-
resolutionProbe,
|
|
168
|
-
timerGranularityMs,
|
|
169
|
-
autocorrelations,
|
|
170
|
-
memTimings,
|
|
171
|
-
phases,
|
|
172
|
-
checksum,
|
|
173
|
-
collectedAt: t_start,
|
|
174
|
-
iterations: timings.length, // actual count (adaptive may differ from requested)
|
|
175
|
-
matrixSize,
|
|
176
|
-
adaptive: _adaptiveInfo, // null in non-adaptive mode
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function _mean(arr) {
|
|
181
|
-
return arr.length ? arr.reduce((s, v) => s + v, 0) / arr.length : 0;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* @typedef {object} EntropyResult
|
|
186
|
-
* @property {number[]} timings - per-iteration wall-clock deltas (ms)
|
|
187
|
-
* @property {number[]} resolutionProbe - raw successive perf.now() readings
|
|
188
|
-
* @property {number|null} timerGranularityMs - effective timer resolution
|
|
189
|
-
* @property {object} autocorrelations - { lag1, lag2, lag3, lag5, lag10 }
|
|
190
|
-
* @property {number[]} memTimings - memory-probe timings (ms)
|
|
191
|
-
* @property {string} checksum - proof the computation ran
|
|
192
|
-
* @property {number} collectedAt - Date.now() at probe start
|
|
193
|
-
* @property {number} iterations
|
|
194
|
-
* @property {number} matrixSize
|
|
195
|
-
*/
|
|
1
|
+
/**
|
|
2
|
+
* @svrnsec/pulse — Entropy Collector
|
|
3
|
+
*
|
|
4
|
+
* Bridges the Rust/WASM matrix-multiply probe into JavaScript.
|
|
5
|
+
* The WASM module is lazily initialised once and cached for subsequent calls.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { collectEntropyAdaptive } from './adaptive.js';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// WASM loader (lazy singleton)
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
let _wasmModule = null;
|
|
14
|
+
let _initPromise = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialise (or return the cached) WASM module.
|
|
18
|
+
* Works in browsers (via fetch), in Electron (Node.js context), and in
|
|
19
|
+
* Jest/Vitest via a manual WASM path override.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} [wasmPath] – override path/URL to the .wasm binary
|
|
22
|
+
*/
|
|
23
|
+
async function initWasm(wasmPath) {
|
|
24
|
+
if (_wasmModule) return _wasmModule;
|
|
25
|
+
if (_initPromise) return _initPromise;
|
|
26
|
+
|
|
27
|
+
_initPromise = (async () => {
|
|
28
|
+
// Dynamic import so bundlers can tree-shake this for server-only builds.
|
|
29
|
+
const { default: init, run_entropy_probe, run_memory_probe, compute_autocorrelation } =
|
|
30
|
+
await import('../../pkg/pulse_core.js');
|
|
31
|
+
|
|
32
|
+
const url = wasmPath ?? new URL('../../pkg/pulse_core_bg.wasm', import.meta.url).href;
|
|
33
|
+
await init(url);
|
|
34
|
+
|
|
35
|
+
_wasmModule = { run_entropy_probe, run_memory_probe, compute_autocorrelation };
|
|
36
|
+
return _wasmModule;
|
|
37
|
+
})();
|
|
38
|
+
|
|
39
|
+
return _initPromise;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// collectEntropy
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run the WASM entropy probe and return raw timing data.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} opts
|
|
50
|
+
* @param {number} [opts.iterations=200] - number of matrix-multiply rounds
|
|
51
|
+
* @param {number} [opts.matrixSize=64] - N for the N×N matrices
|
|
52
|
+
* @param {number} [opts.memSizeKb=512] - size of the memory bandwidth probe
|
|
53
|
+
* @param {number} [opts.memIterations=50]
|
|
54
|
+
* @param {boolean} [opts.phased=true] - run cold/load/hot phases for entropy-jitter ratio
|
|
55
|
+
* @param {string} [opts.wasmPath] - optional custom WASM binary path
|
|
56
|
+
*
|
|
57
|
+
* @returns {Promise<EntropyResult>}
|
|
58
|
+
*/
|
|
59
|
+
export async function collectEntropy(opts = {}) {
|
|
60
|
+
const {
|
|
61
|
+
iterations = 200,
|
|
62
|
+
matrixSize = 64,
|
|
63
|
+
memSizeKb = 512,
|
|
64
|
+
memIterations = 50,
|
|
65
|
+
phased = true,
|
|
66
|
+
adaptive = false,
|
|
67
|
+
adaptiveThreshold = 0.85,
|
|
68
|
+
onBatch,
|
|
69
|
+
wasmPath,
|
|
70
|
+
} = opts;
|
|
71
|
+
|
|
72
|
+
const wasm = await initWasm(wasmPath);
|
|
73
|
+
const t_start = Date.now();
|
|
74
|
+
|
|
75
|
+
let phases = null;
|
|
76
|
+
let timings, resolutionProbe, checksum, timerGranularityMs;
|
|
77
|
+
let _adaptiveInfo = null;
|
|
78
|
+
|
|
79
|
+
// ── Adaptive mode: smart early exit, fastest for obvious VMs ──────────
|
|
80
|
+
if (adaptive) {
|
|
81
|
+
const r = await collectEntropyAdaptive(wasm, {
|
|
82
|
+
minIterations: 50,
|
|
83
|
+
maxIterations: iterations,
|
|
84
|
+
batchSize: 25,
|
|
85
|
+
vmThreshold: adaptiveThreshold,
|
|
86
|
+
hwThreshold: 0.80,
|
|
87
|
+
hwMinIterations: 75,
|
|
88
|
+
matrixSize,
|
|
89
|
+
onBatch,
|
|
90
|
+
});
|
|
91
|
+
timings = r.timings;
|
|
92
|
+
resolutionProbe = r.resolutionProbe ?? [];
|
|
93
|
+
checksum = r.checksum;
|
|
94
|
+
timerGranularityMs = r.timerGranularityMs;
|
|
95
|
+
_adaptiveInfo = { earlyExit: r.earlyExit, batches: r.batches, elapsedMs: r.elapsedMs };
|
|
96
|
+
|
|
97
|
+
// ── Phased collection: cold → load → hot ──────────────────────────────
|
|
98
|
+
// Each phase runs a separate WASM probe. On real hardware, sustained load
|
|
99
|
+
// increases thermal noise so Phase 3 (hot) entropy is measurably higher
|
|
100
|
+
// than Phase 1 (cold). A VM's hypervisor clock is insensitive to guest
|
|
101
|
+
// thermal state, so all three phases return nearly identical entropy.
|
|
102
|
+
} else if (phased && iterations >= 60) {
|
|
103
|
+
const coldN = Math.floor(iterations * 0.25); // ~25% cold
|
|
104
|
+
const loadN = Math.floor(iterations * 0.50); // ~50% sustained load
|
|
105
|
+
const hotN = iterations - coldN - loadN; // ~25% hot
|
|
106
|
+
|
|
107
|
+
const cold = wasm.run_entropy_probe(coldN, matrixSize);
|
|
108
|
+
const load = wasm.run_entropy_probe(loadN, matrixSize);
|
|
109
|
+
const hot = wasm.run_entropy_probe(hotN, matrixSize);
|
|
110
|
+
|
|
111
|
+
const coldTimings = Array.from(cold.timings);
|
|
112
|
+
const loadTimings = Array.from(load.timings);
|
|
113
|
+
const hotTimings = Array.from(hot.timings);
|
|
114
|
+
|
|
115
|
+
timings = [...coldTimings, ...loadTimings, ...hotTimings];
|
|
116
|
+
resolutionProbe = Array.from(cold.resolution_probe);
|
|
117
|
+
checksum = (cold.checksum + load.checksum + hot.checksum).toString();
|
|
118
|
+
|
|
119
|
+
const { detectQuantizationEntropy } = await import('../analysis/jitter.js');
|
|
120
|
+
const coldQE = detectQuantizationEntropy(coldTimings);
|
|
121
|
+
const hotQE = detectQuantizationEntropy(hotTimings);
|
|
122
|
+
|
|
123
|
+
phases = {
|
|
124
|
+
cold: { n: coldN, timings: coldTimings, qe: coldQE, mean: _mean(coldTimings) },
|
|
125
|
+
load: { n: loadN, timings: loadTimings, qe: detectQuantizationEntropy(loadTimings), mean: _mean(loadTimings) },
|
|
126
|
+
hot: { n: hotN, timings: hotTimings, qe: hotQE, mean: _mean(hotTimings) },
|
|
127
|
+
// The key signal: entropy growth under load.
|
|
128
|
+
// Real silicon: hotQE / coldQE typically 1.05 – 1.40
|
|
129
|
+
// VM: hotQE / coldQE typically 0.95 – 1.05 (flat)
|
|
130
|
+
entropyJitterRatio: coldQE > 0 ? hotQE / coldQE : 1.0,
|
|
131
|
+
};
|
|
132
|
+
} else {
|
|
133
|
+
// Single-phase fallback (fewer iterations or phased disabled)
|
|
134
|
+
const result = wasm.run_entropy_probe(iterations, matrixSize);
|
|
135
|
+
timings = Array.from(result.timings);
|
|
136
|
+
resolutionProbe = Array.from(result.resolution_probe);
|
|
137
|
+
checksum = result.checksum.toString();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Timer resolution (non-adaptive path only — adaptive computes its own) ─
|
|
141
|
+
if (!adaptive) {
|
|
142
|
+
const resDeltas = [];
|
|
143
|
+
for (let i = 1; i < resolutionProbe.length; i++) {
|
|
144
|
+
const d = resolutionProbe[i] - resolutionProbe[i - 1];
|
|
145
|
+
if (d > 0) resDeltas.push(d);
|
|
146
|
+
}
|
|
147
|
+
timerGranularityMs = resDeltas.length
|
|
148
|
+
? resDeltas.reduce((a, b) => Math.min(a, b), Infinity)
|
|
149
|
+
: null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Autocorrelation at diagnostic lags ────────────────────────────────
|
|
153
|
+
// Extended lags catch long-period steal-time rhythms (Xen: ~150 iters)
|
|
154
|
+
const lags = [1, 2, 3, 5, 10, 25, 50];
|
|
155
|
+
const autocorrelations = {};
|
|
156
|
+
for (const lag of lags) {
|
|
157
|
+
if (lag < timings.length) {
|
|
158
|
+
autocorrelations[`lag${lag}`] = wasm.compute_autocorrelation(timings, lag);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Secondary probe: memory bandwidth jitter ───────────────────────────
|
|
163
|
+
const memTimings = Array.from(wasm.run_memory_probe(memSizeKb, memIterations));
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
timings,
|
|
167
|
+
resolutionProbe,
|
|
168
|
+
timerGranularityMs,
|
|
169
|
+
autocorrelations,
|
|
170
|
+
memTimings,
|
|
171
|
+
phases,
|
|
172
|
+
checksum,
|
|
173
|
+
collectedAt: t_start,
|
|
174
|
+
iterations: timings.length, // actual count (adaptive may differ from requested)
|
|
175
|
+
matrixSize,
|
|
176
|
+
adaptive: _adaptiveInfo, // null in non-adaptive mode
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _mean(arr) {
|
|
181
|
+
return arr.length ? arr.reduce((s, v) => s + v, 0) / arr.length : 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @typedef {object} EntropyResult
|
|
186
|
+
* @property {number[]} timings - per-iteration wall-clock deltas (ms)
|
|
187
|
+
* @property {number[]} resolutionProbe - raw successive perf.now() readings
|
|
188
|
+
* @property {number|null} timerGranularityMs - effective timer resolution
|
|
189
|
+
* @property {object} autocorrelations - { lag1, lag2, lag3, lag5, lag10 }
|
|
190
|
+
* @property {number[]} memTimings - memory-probe timings (ms)
|
|
191
|
+
* @property {string} checksum - proof the computation ran
|
|
192
|
+
* @property {number} collectedAt - Date.now() at probe start
|
|
193
|
+
* @property {number} iterations
|
|
194
|
+
* @property {number} matrixSize
|
|
195
|
+
*/
|