@zakkster/lite-perf-gate 1.0.1 → 1.2.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/CHANGELOG.md +25 -0
- package/PerfGate.d.ts +64 -0
- package/PerfGate.js +228 -1
- package/README.md +38 -0
- package/llms.txt +11 -0
- package/package.json +60 -60
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0] - 2026-07-07
|
|
4
|
+
|
|
5
|
+
- **`toNDJSON(x, meta?)`** -- NDJSON verdict output for CI artifacts.
|
|
6
|
+
Accepts a `suiteGate()` result, a `measure()` result, or an array of
|
|
7
|
+
either; emits `budget` lines before their `suite-gate` summary line and
|
|
8
|
+
`measure` lines, with optional per-run `meta` fields merged into every
|
|
9
|
+
line. Otherwise the API is frozen, per the suite roadmap.
|
|
10
|
+
|
|
11
|
+
## [1.1.0] - 2026-07-07
|
|
12
|
+
|
|
13
|
+
- **`suiteGate(config)`** -- SPP stream-fed budgets. Consumes a Float64Array
|
|
14
|
+
slab or any forEach record source (a lite-scope memory sink qualifies),
|
|
15
|
+
reduces per-budget metrics (count/sum/max/mean/last over slot t/a/b,
|
|
16
|
+
matched by packed header, stream+op, or op-only), and delegates every
|
|
17
|
+
threshold comparison to `verdict()` -- one comparison authority,
|
|
18
|
+
per-budget structured results. No package import: lite-perf-gate speaks
|
|
19
|
+
the Scope Probe Protocol (SPP v1), it does not depend on lite-scope.
|
|
20
|
+
suiteGate never touches process exit codes; runner semantics (0/1/3 in
|
|
21
|
+
the VersionMatrix scripts, 0/1/2 in `runGate`) stay in the runner layer.
|
|
22
|
+
CONT continuation records are never budget targets in v1.1.
|
|
23
|
+
- **Version discipline fix**: `VERSION` now reads `1.2.0` and the self-test
|
|
24
|
+
asserts it against `package.json` (the published 1.0.1 tarball shipped
|
|
25
|
+
`VERSION = '1.0.0'` because the old test pinned a literal instead of the
|
|
26
|
+
manifest -- that class of slip is now structurally impossible).
|
|
27
|
+
|
|
3
28
|
## [1.0.0] - 2026-07-07
|
|
4
29
|
|
|
5
30
|
Initial release. Generalized from the `@zakkster/lite-signal` zero-GC gate
|
package/PerfGate.d.ts
CHANGED
|
@@ -129,3 +129,67 @@ export function runGate(config: GateConfig): Promise<GateResult>;
|
|
|
129
129
|
|
|
130
130
|
/** @internal */
|
|
131
131
|
export function _controlKeepAlive(): number;
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// suiteGate (v1.1) + toNDJSON (v1.2)
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/** forEach-style SPP record source (e.g. a lite-scope memory sink). */
|
|
138
|
+
export interface SppRecordSource {
|
|
139
|
+
forEach(cb: (packed: number, t: number, a: number, b: number) => void): void;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface SuiteBudget {
|
|
143
|
+
name: string;
|
|
144
|
+
/** Exact packed header (streamId << 16 | opcode). */
|
|
145
|
+
packed?: number;
|
|
146
|
+
/** Stream id; combine with op for an exact match. */
|
|
147
|
+
stream?: number;
|
|
148
|
+
/** Opcode; alone it matches the op on any stream. */
|
|
149
|
+
op?: number;
|
|
150
|
+
/** Record slot to read. Default 'a'. */
|
|
151
|
+
slot?: 't' | 'a' | 'b';
|
|
152
|
+
/** Reduction over matching records. Default 'max'. */
|
|
153
|
+
reduce?: 'count' | 'sum' | 'max' | 'mean' | 'last';
|
|
154
|
+
/** Inclusive budget: verdict() flags value > max. */
|
|
155
|
+
max: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface SuiteBudgetResult {
|
|
159
|
+
name: string;
|
|
160
|
+
value: number;
|
|
161
|
+
count: number;
|
|
162
|
+
max: number;
|
|
163
|
+
pass: boolean;
|
|
164
|
+
reasons: string[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface SuiteGateResult {
|
|
168
|
+
name: string;
|
|
169
|
+
pass: boolean;
|
|
170
|
+
reasons: string[];
|
|
171
|
+
budgets: SuiteBudgetResult[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Evaluate SPP stream records against numeric budgets. Pure reduction with
|
|
176
|
+
* per-budget delegation to verdict(); no measurement, no process exit
|
|
177
|
+
* codes, no record emission. Wide-record CONT payloads are not budget
|
|
178
|
+
* targets in v1.1 (base-record t/a/b slots only).
|
|
179
|
+
*/
|
|
180
|
+
export function suiteGate(config: {
|
|
181
|
+
source: Float64Array | SppRecordSource;
|
|
182
|
+
budgets: SuiteBudget[];
|
|
183
|
+
name?: string;
|
|
184
|
+
}): SuiteGateResult;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Serialize suiteGate() and/or measure() results as NDJSON for CI
|
|
188
|
+
* artifacts: one JSON object per line, budget lines before their
|
|
189
|
+
* suite-gate summary, trailing newline. `meta` fields merge into every
|
|
190
|
+
* line; core fields win on collision.
|
|
191
|
+
*/
|
|
192
|
+
export function toNDJSON(
|
|
193
|
+
x: SuiteGateResult | MeasureResult | Array<SuiteGateResult | MeasureResult>,
|
|
194
|
+
meta?: Record<string, unknown>
|
|
195
|
+
): string;
|
package/PerfGate.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* MIT License
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
export const VERSION = '1.
|
|
20
|
+
export const VERSION = '1.2.0';
|
|
21
21
|
|
|
22
22
|
import {PerformanceObserver, constants} from 'node:perf_hooks';
|
|
23
23
|
import {setTimeout as sleep} from 'node:timers/promises';
|
|
@@ -428,3 +428,230 @@ export async function runGate(config) {
|
|
|
428
428
|
}
|
|
429
429
|
return {passed: passed, results: results};
|
|
430
430
|
}
|
|
431
|
+
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
// suiteGate -- SPP stream-fed budgets (v1.1)
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
// lite-perf-gate does NOT import @zakkster/lite-scope. Probes, gates, and
|
|
436
|
+
// consumers are coupled by the Scope Probe Protocol (SPP v1 -- PROTOCOL.md
|
|
437
|
+
// in lite-scope), never by packages. The two protocol facts used here:
|
|
438
|
+
// - a record is 4 numbers [packed, t, a, b], where
|
|
439
|
+
// packed = (streamId << 16 | opcode) >>> 0, both u16;
|
|
440
|
+
// - opcode 0x0F01 (CONT) is a wide-record continuation and is never a
|
|
441
|
+
// budget target. Budgets therefore see the base record's t/a/b slots;
|
|
442
|
+
// extended CONT payload slots are out of scope for v1.1 (a lite-scope
|
|
443
|
+
// side bridge can pre-reduce wide records if that ever gates).
|
|
444
|
+
// suiteGate never touches process exit codes: runner semantics (0 pass /
|
|
445
|
+
// 1 regression / 3 recapture in the VersionMatrix scripts, 0/1/2 in
|
|
446
|
+
// runGate) stay in the runner layer. All threshold comparison is delegated
|
|
447
|
+
// to verdict() -- one comparison authority, per-budget.
|
|
448
|
+
|
|
449
|
+
var SPP_OP_CONT = 0x0F01;
|
|
450
|
+
|
|
451
|
+
var SUITE_REDUCES = {count: 1, sum: 1, max: 1, mean: 1, last: 1};
|
|
452
|
+
var SUITE_SLOTS = {t: 1, a: 2, b: 3};
|
|
453
|
+
|
|
454
|
+
function suiteMatcher(b, i) {
|
|
455
|
+
if (b.packed !== undefined) {
|
|
456
|
+
if (!Number.isInteger(b.packed) || b.packed < 0 || b.packed > 0xFFFFFFFF) {
|
|
457
|
+
throw new RangeError('suiteGate: budget[' + i + '] packed must be a u32');
|
|
458
|
+
}
|
|
459
|
+
return {exact: b.packed >>> 0, op: -1};
|
|
460
|
+
}
|
|
461
|
+
if (b.op === undefined || !Number.isInteger(b.op) || b.op < 0 || b.op > 0xFFFF) {
|
|
462
|
+
throw new RangeError('suiteGate: budget[' + i + '] needs a u16 op (with optional stream), or packed');
|
|
463
|
+
}
|
|
464
|
+
if (b.stream !== undefined) {
|
|
465
|
+
if (!Number.isInteger(b.stream) || b.stream < 0 || b.stream > 0xFFFF) {
|
|
466
|
+
throw new RangeError('suiteGate: budget[' + i + '] stream must be a u16');
|
|
467
|
+
}
|
|
468
|
+
return {exact: ((b.stream << 16) | b.op) >>> 0, op: -1};
|
|
469
|
+
}
|
|
470
|
+
return {exact: -1, op: b.op}; // op-only: matches the op on any stream
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Evaluate SPP stream records against numeric budgets. Pure reduction plus
|
|
475
|
+
* per-budget delegation to verdict(); no measurement, no exit codes, no
|
|
476
|
+
* record emission (callers bridge verdicts to GATE_VERDICT meta records).
|
|
477
|
+
*
|
|
478
|
+
* @param {object} config
|
|
479
|
+
* @param {Float64Array | { forEach: (cb: (packed: number, t: number, a: number, b: number) => void) => void }} config.source
|
|
480
|
+
* A contiguous SPP slab (length divisible by 4) or any forEach-style
|
|
481
|
+
* record source (e.g. a lite-scope memory sink).
|
|
482
|
+
* @param {Array<{
|
|
483
|
+
* name: string,
|
|
484
|
+
* packed?: number, stream?: number, op?: number,
|
|
485
|
+
* slot?: 't' | 'a' | 'b',
|
|
486
|
+
* reduce?: 'count' | 'sum' | 'max' | 'mean' | 'last',
|
|
487
|
+
* max: number
|
|
488
|
+
* }>} config.budgets
|
|
489
|
+
* slot defaults to 'a', reduce to 'max'. Budgets that match zero records
|
|
490
|
+
* reduce to 0 (count 0 is reported so callers can tell silence from luck).
|
|
491
|
+
* @param {string} [config.name='suite-gate']
|
|
492
|
+
* @returns {{
|
|
493
|
+
* name: string, pass: boolean, reasons: string[],
|
|
494
|
+
* budgets: Array<{ name: string, value: number, count: number, max: number, pass: boolean, reasons: string[] }>
|
|
495
|
+
* }}
|
|
496
|
+
*/
|
|
497
|
+
export function suiteGate(config) {
|
|
498
|
+
if (!config || typeof config !== 'object') {
|
|
499
|
+
throw new TypeError('suiteGate: expects a config object');
|
|
500
|
+
}
|
|
501
|
+
const source = config.source;
|
|
502
|
+
const budgets = config.budgets;
|
|
503
|
+
if (!Array.isArray(budgets) || budgets.length === 0) {
|
|
504
|
+
throw new TypeError('suiteGate: budgets must be a non-empty array');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const n = budgets.length;
|
|
508
|
+
const match = new Array(n);
|
|
509
|
+
const slot = new Array(n);
|
|
510
|
+
const reduce = new Array(n);
|
|
511
|
+
const seen = {};
|
|
512
|
+
for (let i = 0; i < n; i++) {
|
|
513
|
+
const b = budgets[i];
|
|
514
|
+
if (!b || typeof b.name !== 'string' || b.name.length === 0) {
|
|
515
|
+
throw new TypeError('suiteGate: budget[' + i + '] needs a non-empty name');
|
|
516
|
+
}
|
|
517
|
+
if (seen[b.name]) throw new RangeError('suiteGate: duplicate budget name "' + b.name + '"');
|
|
518
|
+
seen[b.name] = 1;
|
|
519
|
+
if (typeof b.max !== 'number' || !isFinite(b.max)) {
|
|
520
|
+
throw new RangeError('suiteGate: budget "' + b.name + '" needs a finite numeric max');
|
|
521
|
+
}
|
|
522
|
+
const sl = b.slot === undefined ? 'a' : b.slot;
|
|
523
|
+
if (SUITE_SLOTS[sl] === undefined) {
|
|
524
|
+
throw new RangeError('suiteGate: budget "' + b.name + '" slot must be t, a, or b');
|
|
525
|
+
}
|
|
526
|
+
const rd = b.reduce === undefined ? 'max' : b.reduce;
|
|
527
|
+
if (SUITE_REDUCES[rd] === undefined) {
|
|
528
|
+
throw new RangeError('suiteGate: budget "' + b.name + '" reduce must be count, sum, max, mean, or last');
|
|
529
|
+
}
|
|
530
|
+
match[i] = suiteMatcher(b, i);
|
|
531
|
+
slot[i] = SUITE_SLOTS[sl];
|
|
532
|
+
reduce[i] = rd;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const count = new Float64Array(n);
|
|
536
|
+
const sum = new Float64Array(n);
|
|
537
|
+
const maxv = new Float64Array(n);
|
|
538
|
+
const last = new Float64Array(n);
|
|
539
|
+
maxv.fill(-Infinity);
|
|
540
|
+
|
|
541
|
+
function visit(packed, t, a, b) {
|
|
542
|
+
const op = packed & 0xFFFF;
|
|
543
|
+
if (op === SPP_OP_CONT) return;
|
|
544
|
+
for (let i = 0; i < n; i++) {
|
|
545
|
+
const m = match[i];
|
|
546
|
+
if (m.exact >= 0 ? (packed >>> 0) !== m.exact : op !== m.op) continue;
|
|
547
|
+
const val = slot[i] === 1 ? t : slot[i] === 2 ? a : b;
|
|
548
|
+
count[i] += 1;
|
|
549
|
+
sum[i] += val;
|
|
550
|
+
if (val > maxv[i]) maxv[i] = val;
|
|
551
|
+
last[i] = val;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (source && typeof source.forEach === 'function' && !(source instanceof Float64Array)) {
|
|
556
|
+
source.forEach(visit);
|
|
557
|
+
} else if (source instanceof Float64Array) {
|
|
558
|
+
if (source.length % 4 !== 0) {
|
|
559
|
+
throw new RangeError('suiteGate: slab length must be divisible by 4');
|
|
560
|
+
}
|
|
561
|
+
for (let r = 0; r < source.length; r += 4) {
|
|
562
|
+
visit(source[r], source[r + 1], source[r + 2], source[r + 3]);
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
throw new TypeError('suiteGate: source must be a Float64Array slab or a forEach record source');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const perBudget = [];
|
|
569
|
+
const reasons = [];
|
|
570
|
+
let pass = true;
|
|
571
|
+
for (let i = 0; i < n; i++) {
|
|
572
|
+
const b = budgets[i];
|
|
573
|
+
let value;
|
|
574
|
+
if (reduce[i] === 'count') value = count[i];
|
|
575
|
+
else if (reduce[i] === 'sum') value = sum[i];
|
|
576
|
+
else if (reduce[i] === 'max') value = count[i] > 0 ? maxv[i] : 0;
|
|
577
|
+
else if (reduce[i] === 'mean') value = count[i] > 0 ? sum[i] / count[i] : 0;
|
|
578
|
+
else value = count[i] > 0 ? last[i] : 0;
|
|
579
|
+
|
|
580
|
+
const counters = {};
|
|
581
|
+
counters[b.name] = value;
|
|
582
|
+
const ct = {};
|
|
583
|
+
ct[b.name] = b.max;
|
|
584
|
+
const v = verdict(
|
|
585
|
+
{name: b.name, minorHi: 0, majorHi: 0, retainedKB_hi: 0, counters_hi: counters},
|
|
586
|
+
{counters: ct}
|
|
587
|
+
);
|
|
588
|
+
if (!v.pass) pass = false;
|
|
589
|
+
for (let ri = 0; ri < v.reasons.length; ri++) reasons.push(v.reasons[ri]);
|
|
590
|
+
perBudget.push({
|
|
591
|
+
name: b.name, value: value, count: count[i], max: b.max,
|
|
592
|
+
pass: v.pass, reasons: v.reasons
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
name: typeof config.name === 'string' ? config.name : 'suite-gate',
|
|
598
|
+
pass: pass,
|
|
599
|
+
reasons: reasons,
|
|
600
|
+
budgets: perBudget
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
// toNDJSON -- verdict output for CI artifacts (v1.2)
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
|
|
608
|
+
function ndjsonSuiteGate(r, meta, lines) {
|
|
609
|
+
for (let i = 0; i < r.budgets.length; i++) {
|
|
610
|
+
const b = r.budgets[i];
|
|
611
|
+
lines.push(JSON.stringify(Object.assign({}, meta, {
|
|
612
|
+
type: 'budget', gate: r.name, name: b.name,
|
|
613
|
+
value: b.value, count: b.count, max: b.max, pass: b.pass
|
|
614
|
+
})));
|
|
615
|
+
}
|
|
616
|
+
lines.push(JSON.stringify(Object.assign({}, meta, {
|
|
617
|
+
type: 'suite-gate', name: r.name, pass: r.pass, reasons: r.reasons
|
|
618
|
+
})));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function ndjsonMeasure(r, meta, lines) {
|
|
622
|
+
lines.push(JSON.stringify(Object.assign({}, meta, {
|
|
623
|
+
type: 'measure', name: r.name, N: r.N, k: r.k,
|
|
624
|
+
minorLo: r.minorLo, minorHi: r.minorHi,
|
|
625
|
+
majorLo: r.majorLo, majorHi: r.majorHi,
|
|
626
|
+
retainedKB_lo: r.retainedKB_lo, retainedKB_hi: r.retainedKB_hi,
|
|
627
|
+
counters_lo: r.counters_lo, counters_hi: r.counters_hi
|
|
628
|
+
})));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Serialize gate output as NDJSON for CI artifacts: one JSON object per
|
|
633
|
+
* line, budget lines before their suite-gate summary line, trailing
|
|
634
|
+
* newline. Accepts a suiteGate() result, a measure() result, or an array
|
|
635
|
+
* mixing both. `meta` fields (run id, package, version, ...) are merged
|
|
636
|
+
* into every line; core fields win on collision.
|
|
637
|
+
*
|
|
638
|
+
* @param {object | object[]} x
|
|
639
|
+
* @param {object} [meta]
|
|
640
|
+
* @returns {string}
|
|
641
|
+
*/
|
|
642
|
+
export function toNDJSON(x, meta) {
|
|
643
|
+
const rows = Array.isArray(x) ? x : [x];
|
|
644
|
+
const m = meta && typeof meta === 'object' ? meta : {};
|
|
645
|
+
const lines = [];
|
|
646
|
+
for (let i = 0; i < rows.length; i++) {
|
|
647
|
+
const r = rows[i];
|
|
648
|
+
if (r && Array.isArray(r.budgets) && typeof r.pass === 'boolean') {
|
|
649
|
+
ndjsonSuiteGate(r, m, lines);
|
|
650
|
+
} else if (r && typeof r.minorHi === 'number' && typeof r.N === 'number') {
|
|
651
|
+
ndjsonMeasure(r, m, lines);
|
|
652
|
+
} else {
|
|
653
|
+
throw new TypeError('toNDJSON: row ' + i + ' is neither a suiteGate result nor a measure result');
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return lines.join('\n') + '\n';
|
|
657
|
+
}
|
package/README.md
CHANGED
|
@@ -24,6 +24,44 @@ npm install @zakkster/lite-perf-gate
|
|
|
24
24
|
|
|
25
25
|
Scavenge-counting is the only reliable detector of transient allocation. This library packages the methodology into a `node:test`-native harness that validates its own detector on every run.
|
|
26
26
|
|
|
27
|
+
|
|
28
|
+
## suiteGate -- SPP stream-fed budgets (v1.1)
|
|
29
|
+
|
|
30
|
+
Gate live probe streams, not just measured scenarios. `suiteGate()` reads
|
|
31
|
+
SPP records (a `Float64Array` slab or any `forEach(cb(packed, t, a, b))`
|
|
32
|
+
source -- a `@zakkster/lite-scope` memory sink qualifies), reduces each
|
|
33
|
+
budget's metric, and delegates every comparison to `verdict()`:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
import { suiteGate, toNDJSON } from '@zakkster/lite-perf-gate';
|
|
37
|
+
|
|
38
|
+
const gate = suiteGate({
|
|
39
|
+
name: 'ci-gate',
|
|
40
|
+
source: sink.toSlab(), // or the sink itself (forEach source)
|
|
41
|
+
budgets: [
|
|
42
|
+
{ name: 'gc.pause.max', stream: 2, op: 0x0201, slot: 'a', reduce: 'max', max: 8 },
|
|
43
|
+
{ name: 'leak.orphans', op: 0x0801, reduce: 'count', max: 0 },
|
|
44
|
+
{ name: 'inp.worst', op: 0x0601, slot: 'a', reduce: 'max', max: 200 }
|
|
45
|
+
]
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
process.stdout.write(toNDJSON(gate, { pkg: 'my-lib', run: process.env.CI_RUN }));
|
|
49
|
+
if (!gate.pass) process.exitCode = 1; // exit codes stay in YOUR runner
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
No package coupling: lite-perf-gate speaks the Scope Probe Protocol
|
|
53
|
+
(SPP v1, `PROTOCOL.md` in lite-scope) and never imports lite-scope.
|
|
54
|
+
CONT continuation records are never budget targets (base-record slots
|
|
55
|
+
only in v1.1). `suiteGate` returns structured per-budget verdicts and
|
|
56
|
+
leaves exit codes to the runner layer.
|
|
57
|
+
|
|
58
|
+
## toNDJSON -- CI artifacts (v1.2)
|
|
59
|
+
|
|
60
|
+
One JSON object per line: `budget` lines, then the `suite-gate` summary;
|
|
61
|
+
`measure()` results serialize as `measure` lines. Optional `meta` fields
|
|
62
|
+
(package, run id, versions) merge into every line. Pipe to a file in your
|
|
63
|
+
gate script and attach it as a CI artifact.
|
|
64
|
+
|
|
27
65
|
## Quick start
|
|
28
66
|
|
|
29
67
|
```js
|
package/llms.txt
CHANGED
|
@@ -63,3 +63,14 @@ is the only reliable detector of transient allocation in V8.
|
|
|
63
63
|
|
|
64
64
|
Single-file ESM. Imports only node:perf_hooks, node:timers/promises,
|
|
65
65
|
node:test, node:assert/strict.
|
|
66
|
+
|
|
67
|
+
v1.1 suiteGate(config): SPP stream-fed budgets. config.source = Float64Array
|
|
68
|
+
slab or { forEach(cb(packed,t,a,b)) } (lite-scope memory sink). budgets[] =
|
|
69
|
+
{ name, packed? | stream?+op | op, slot: t|a|b (default a), reduce:
|
|
70
|
+
count|sum|max|mean|last (default max), max }. Delegates every comparison to
|
|
71
|
+
verdict(); returns { name, pass, reasons, budgets: [{name, value, count,
|
|
72
|
+
max, pass, reasons}] }. Never sets exit codes; CONT records (op 0x0F01) are
|
|
73
|
+
never budget targets. No lite-scope import: coupled by SPP v1 protocol only.
|
|
74
|
+
v1.2 toNDJSON(x, meta?): NDJSON for CI artifacts; accepts suiteGate result,
|
|
75
|
+
measure result, or array; budget lines then suite-gate summary; meta merged
|
|
76
|
+
per line; trailing newline.
|
package/package.json
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"files": [
|
|
20
|
-
"PerfGate.js",
|
|
21
|
-
"PerfGate.d.ts",
|
|
22
|
-
"README.md",
|
|
23
|
-
"llms.txt",
|
|
24
|
-
"LICENSE",
|
|
25
|
-
"CHANGELOG.md"
|
|
26
|
-
],
|
|
27
|
-
"scripts": {
|
|
28
|
-
"test": "node --expose-gc --max-semi-space-size=4 --test test/self.test.mjs"
|
|
29
|
-
},
|
|
30
|
-
"keywords": [
|
|
31
|
-
"benchmark",
|
|
32
|
-
"regression",
|
|
33
|
-
"gate",
|
|
34
|
-
"zero-gc",
|
|
35
|
-
"gc",
|
|
36
|
-
"scavenge",
|
|
37
|
-
"allocation",
|
|
38
|
-
"performance",
|
|
39
|
-
"node-test",
|
|
40
|
-
"ci",
|
|
41
|
-
"perf-hooks"
|
|
42
|
-
],
|
|
43
|
-
"publishConfig": {
|
|
44
|
-
"access": "public"
|
|
45
|
-
},
|
|
46
|
-
"license": "MIT",
|
|
47
|
-
"homepage": "https://github.com/PeshoVurtoleta/lite-perf-gate#readme",
|
|
48
|
-
"repository": {
|
|
49
|
-
"type": "git",
|
|
50
|
-
"url": "git+https://github.com/PeshoVurtoleta/lite-perf-gate.git"
|
|
51
|
-
},
|
|
52
|
-
"bugs": {
|
|
53
|
-
"url": "https://github.com/PeshoVurtoleta/lite-perf-gate/issues",
|
|
54
|
-
"email": "shinikchiev@yahoo.com"
|
|
55
|
-
},
|
|
56
|
-
"funding": {
|
|
57
|
-
"type": "github",
|
|
58
|
-
"url": "https://github.com/sponsors/PeshoVurtoleta"
|
|
59
|
-
},
|
|
60
|
-
"engines": {
|
|
61
|
-
"node": ">=18"
|
|
2
|
+
"name": "@zakkster/lite-perf-gate",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
5
|
+
"description": "Zero-GC and performance regression gate for node:test. Scavenge-counting, scaling verdict, detector self-validation. Proves your hot path allocates nothing -- or names what did.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./PerfGate.js",
|
|
9
|
+
"module": "./PerfGate.js",
|
|
10
|
+
"types": "./PerfGate.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./PerfGate.d.ts",
|
|
14
|
+
"node": "./PerfGate.js",
|
|
15
|
+
"import": "./PerfGate.js",
|
|
16
|
+
"default": "./PerfGate.js"
|
|
62
17
|
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"PerfGate.js",
|
|
21
|
+
"PerfGate.d.ts",
|
|
22
|
+
"README.md",
|
|
23
|
+
"llms.txt",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "node --expose-gc --max-semi-space-size=4 --test test/self.test.mjs"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"benchmark",
|
|
32
|
+
"regression",
|
|
33
|
+
"gate",
|
|
34
|
+
"zero-gc",
|
|
35
|
+
"gc",
|
|
36
|
+
"scavenge",
|
|
37
|
+
"allocation",
|
|
38
|
+
"performance",
|
|
39
|
+
"node-test",
|
|
40
|
+
"ci",
|
|
41
|
+
"perf-hooks"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"homepage": "https://github.com/PeshoVurtoleta/lite-perf-gate#readme",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/PeshoVurtoleta/lite-perf-gate.git"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/PeshoVurtoleta/lite-perf-gate/issues",
|
|
54
|
+
"email": "shinikchiev@yahoo.com"
|
|
55
|
+
},
|
|
56
|
+
"funding": {
|
|
57
|
+
"type": "github",
|
|
58
|
+
"url": "https://github.com/sponsors/PeshoVurtoleta"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=18"
|
|
62
|
+
}
|
|
63
63
|
}
|