@hviana/sema 0.3.0 → 0.4.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/AGENTS.md +71 -5
- package/dist/src/derive/src/deduction.d.ts +12 -1
- package/dist/src/derive/src/deduction.js +5 -1
- package/dist/src/derive/src/index.d.ts +1 -0
- package/dist/src/geometry.d.ts +3 -30
- package/dist/src/geometry.js +330 -82
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/meter.d.ts +171 -0
- package/dist/src/meter.js +269 -0
- package/dist/src/mind/attention.d.ts +0 -4
- package/dist/src/mind/attention.js +251 -23
- package/dist/src/mind/bridge.d.ts +10 -1
- package/dist/src/mind/bridge.js +179 -10
- package/dist/src/mind/canonical.d.ts +6 -1
- package/dist/src/mind/canonical.js +6 -1
- package/dist/src/mind/graph-search.d.ts +9 -0
- package/dist/src/mind/graph-search.js +59 -19
- package/dist/src/mind/junction.d.ts +10 -0
- package/dist/src/mind/junction.js +14 -0
- package/dist/src/mind/match.d.ts +40 -0
- package/dist/src/mind/match.js +125 -1
- package/dist/src/mind/mechanisms/cast.js +63 -6
- package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
- package/dist/src/mind/mechanisms/extraction.js +1 -88
- package/dist/src/mind/mechanisms/recall.d.ts +3 -0
- package/dist/src/mind/mechanisms/recall.js +77 -14
- package/dist/src/mind/mind.d.ts +55 -4
- package/dist/src/mind/mind.js +110 -91
- package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
- package/dist/src/mind/pipeline-mechanism.js +179 -10
- package/dist/src/mind/pipeline.js +52 -10
- package/dist/src/mind/primitives.d.ts +11 -15
- package/dist/src/mind/primitives.js +47 -28
- package/dist/src/mind/reasoning.d.ts +7 -1
- package/dist/src/mind/reasoning.js +40 -8
- package/dist/src/mind/recognition.js +93 -20
- package/dist/src/mind/traverse.d.ts +11 -0
- package/dist/src/mind/traverse.js +58 -5
- package/dist/src/mind/types.d.ts +28 -5
- package/dist/src/store.d.ts +15 -0
- package/dist/src/store.js +91 -6
- package/package.json +1 -1
- package/src/derive/src/deduction.ts +15 -0
- package/src/derive/src/index.ts +1 -0
- package/src/geometry.ts +350 -122
- package/src/index.ts +1 -0
- package/src/meter.ts +333 -0
- package/src/mind/attention.ts +268 -31
- package/src/mind/bridge.ts +187 -10
- package/src/mind/canonical.ts +6 -1
- package/src/mind/graph-search.ts +60 -21
- package/src/mind/junction.ts +12 -0
- package/src/mind/match.ts +146 -1
- package/src/mind/mechanisms/cast.ts +62 -6
- package/src/mind/mechanisms/extraction.ts +2 -103
- package/src/mind/mechanisms/recall.ts +84 -17
- package/src/mind/mind.ts +139 -98
- package/src/mind/pipeline-mechanism.ts +203 -13
- package/src/mind/pipeline.ts +65 -8
- package/src/mind/primitives.ts +49 -33
- package/src/mind/reasoning.ts +39 -7
- package/src/mind/recognition.ts +89 -19
- package/src/mind/traverse.ts +59 -5
- package/src/mind/types.ts +28 -5
- package/src/store.ts +75 -6
- package/test/14-scaling.test.mjs +17 -7
- package/test/31-audit.test.mjs +4 -1
- package/test/33-multi-candidate.test.mjs +13 -1
- package/test/36-already-answered-fusion.test.mjs +10 -3
- package/test/46-recognise-multibyte-edge.test.mjs +3 -3
- package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
- package/test/55-cost-meter.test.mjs +284 -0
- package/test/56-bridge-identity-admission.test.mjs +209 -0
- package/test/57-fusion-order.test.mjs +104 -0
- package/test/58-subquantum-sites.test.mjs +112 -0
- package/test/59-fold-invariance.test.mjs +226 -0
package/src/meter.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// meter.ts — the one computational-usage accounting surface.
|
|
2
|
+
//
|
|
3
|
+
// A Meter counts the WORK one inference call performs, layer by layer, so a
|
|
4
|
+
// slow response can be attributed instead of guessed at. It is the profiling
|
|
5
|
+
// counterpart of the rationale: the rationale says WHY an answer was chosen,
|
|
6
|
+
// the meter says WHAT IT COST to choose it.
|
|
7
|
+
//
|
|
8
|
+
// Four contracts, all load-bearing:
|
|
9
|
+
//
|
|
10
|
+
// 1. NEVER READ BY INFERENCE. No counter may reach a decision, a threshold,
|
|
11
|
+
// or an ordering. Determinism (AGENTS §2.1) survives only because the
|
|
12
|
+
// meter is write-only from the engine's point of view.
|
|
13
|
+
// 2. OFF BY DEFAULT, AND FREE WHEN OFF. Every call site is `meter?.x++` on
|
|
14
|
+
// a null field. Nothing allocates, nothing is keyed, nothing is timed
|
|
15
|
+
// unless a Meter is attached (`new Mind({ profile: true })`).
|
|
16
|
+
// 3. COUNTS, NOT TIMES, ARE THE PRODUCT. Counters are deterministic — the
|
|
17
|
+
// same query on the same store meters identically, so a regression is
|
|
18
|
+
// diffable. `elapsedMs` and the per-phase millisecond totals are the
|
|
19
|
+
// only non-deterministic fields and are reported separately.
|
|
20
|
+
// 4. ONE DEFINITION. This file is the only place a counter name exists.
|
|
21
|
+
// A layer that wants to be visible bumps a field here; it does not grow
|
|
22
|
+
// a private counter (the pattern `danglingReads`/`compactFailures` in
|
|
23
|
+
// store.ts predates this file and stays — those are HEALTH counters,
|
|
24
|
+
// session-lifetime and error-shaped, not per-response work).
|
|
25
|
+
//
|
|
26
|
+
// Layering: this module imports nothing. store.ts, the mind, and the graph
|
|
27
|
+
// search all reference it, and it references none of them.
|
|
28
|
+
|
|
29
|
+
/** Per-phase accumulation — one entry per mechanism `floor`/`run` and per
|
|
30
|
+
* named stage.
|
|
31
|
+
*
|
|
32
|
+
* PHASES NEST, AND ARE NOT DISJOINT. `think` contains every mechanism
|
|
33
|
+
* phase; a mechanism's `floor` contains whatever shared analysis it
|
|
34
|
+
* first-touched (`attention`, `weave`); `recall.run` contains
|
|
35
|
+
* `substitutionBridge`, which contains `recall.exhaustiveResonate`. Read a
|
|
36
|
+
* phase as "wall-clock spent inside this, inclusive" — never sum them and
|
|
37
|
+
* expect the total. `CostReport.elapsedMs` is the only whole.
|
|
38
|
+
*
|
|
39
|
+
* Shared analyses are charged to THEMSELVES, not to the mechanism that
|
|
40
|
+
* first-touched them: the first toucher pays the wall clock, but every
|
|
41
|
+
* later consumer gets the result free, so blaming it would misread which
|
|
42
|
+
* work is expensive. (Before this, the profile read "cast.floor costs
|
|
43
|
+
* 2.9 s"; what actually cost 2.7 s of that was the consensus climb, which
|
|
44
|
+
* cast merely paid for on everyone's behalf.) */
|
|
45
|
+
export interface PhaseCost {
|
|
46
|
+
/** How many times the phase ran. */
|
|
47
|
+
calls: number;
|
|
48
|
+
/** Wall-clock milliseconds spent inside it (non-deterministic). */
|
|
49
|
+
ms: number;
|
|
50
|
+
/** Work counters accrued INSIDE this phase — the same names as
|
|
51
|
+
* {@link CostReport.counters}, differenced across the phase's entry and
|
|
52
|
+
* exit, summed over its calls. Deterministic, unlike `ms`, and the field
|
|
53
|
+
* that answers "which phase did those 75,000 byte reads?" — a question a
|
|
54
|
+
* whole-response counter total cannot. Inclusive, like `ms`: a nested
|
|
55
|
+
* phase's work is counted in its parent too. */
|
|
56
|
+
counters: Record<string, number>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A snapshot of one inference call's computational usage. Plain data —
|
|
60
|
+
* JSON-serialisable, diffable between runs. */
|
|
61
|
+
export interface CostReport {
|
|
62
|
+
version: 1;
|
|
63
|
+
/** Wall-clock milliseconds of the whole call (non-deterministic). */
|
|
64
|
+
elapsedMs: number;
|
|
65
|
+
/** Query length in bytes — the scale every other number is read against. */
|
|
66
|
+
queryBytes: number;
|
|
67
|
+
/** Deterministic work counters, by layer. */
|
|
68
|
+
counters: Record<string, number>;
|
|
69
|
+
/** Per-phase call counts and millisecond totals, insertion-ordered.
|
|
70
|
+
* NESTED — see {@link PhaseCost}; do not sum. */
|
|
71
|
+
phases: Record<string, PhaseCost>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The mutable accumulator. One is created per profiled inference call and
|
|
75
|
+
* handed to every layer through `ctx.meter` / `store.meter`. */
|
|
76
|
+
export class Meter {
|
|
77
|
+
// ── Store: content reads ────────────────────────────────────────────────
|
|
78
|
+
/** `store.get` — one node record materialised (cache hit or DB row). */
|
|
79
|
+
nodeRecords = 0;
|
|
80
|
+
/** `store.bytes` / `store.bytesPrefix` — one reconstruction request. */
|
|
81
|
+
byteReads = 0;
|
|
82
|
+
/** Bytes actually handed back by those reads — the real I/O volume, and
|
|
83
|
+
* the number that exposes an unbounded read (AGENTS §2.8) that a call
|
|
84
|
+
* count alone hides. */
|
|
85
|
+
bytesRead = 0;
|
|
86
|
+
/** `store.contentLen`. */
|
|
87
|
+
lenReads = 0;
|
|
88
|
+
|
|
89
|
+
// ── Store: content-addressed identity ───────────────────────────────────
|
|
90
|
+
/** `store.findLeaf`. */
|
|
91
|
+
leafLookups = 0;
|
|
92
|
+
/** `store.findBranch`. */
|
|
93
|
+
branchLookups = 0;
|
|
94
|
+
/** `store.canonFind` — equivalence-class candidate proposal. */
|
|
95
|
+
canonLookups = 0;
|
|
96
|
+
|
|
97
|
+
// ── Store: structure (parents / containment) ────────────────────────────
|
|
98
|
+
/** `parents` + `parentsFirst` — materialising parent reads. */
|
|
99
|
+
parentReads = 0;
|
|
100
|
+
/** `hasParents` — indexed existence probe. */
|
|
101
|
+
parentProbes = 0;
|
|
102
|
+
/** `chainRun` — one bounded transparent-chain climb. */
|
|
103
|
+
chainRuns = 0;
|
|
104
|
+
/** `containers` + `containersSlice`. */
|
|
105
|
+
containerReads = 0;
|
|
106
|
+
/** `hasContainers` — indexed existence probe. */
|
|
107
|
+
containerProbes = 0;
|
|
108
|
+
|
|
109
|
+
// ── Store: learned edges ────────────────────────────────────────────────
|
|
110
|
+
/** `next` + `nextFirst`. */
|
|
111
|
+
edgeReads = 0;
|
|
112
|
+
/** `hasNext` — indexed existence probe. */
|
|
113
|
+
edgeProbes = 0;
|
|
114
|
+
/** `prev` + `prevFirst`. */
|
|
115
|
+
prevReads = 0;
|
|
116
|
+
/** `prevCount` — indexed count probe. */
|
|
117
|
+
prevProbes = 0;
|
|
118
|
+
|
|
119
|
+
// ── Store: halo ─────────────────────────────────────────────────────────
|
|
120
|
+
/** `halo` — one halo vector decoded. */
|
|
121
|
+
haloReads = 0;
|
|
122
|
+
/** `hasHalo` + `haloMass` — probes that never decode a vector. */
|
|
123
|
+
haloProbes = 0;
|
|
124
|
+
|
|
125
|
+
// ── Store: approximate search (the ANN indexes) ─────────────────────────
|
|
126
|
+
/** `resonate` calls that descended the content index. */
|
|
127
|
+
annQueries = 0;
|
|
128
|
+
/** `resonate` calls served from the per-flush read cache. */
|
|
129
|
+
annCacheHits = 0;
|
|
130
|
+
/** Stored vectors the content index actually scored — the dominant cost of
|
|
131
|
+
* a query on a large store, and the one counter that grows with N when a
|
|
132
|
+
* budget is missing. */
|
|
133
|
+
annVectorReads = 0;
|
|
134
|
+
/** `resonateHalo` calls. */
|
|
135
|
+
haloQueries = 0;
|
|
136
|
+
|
|
137
|
+
// ── Mind: perception & recognition ──────────────────────────────────────
|
|
138
|
+
/** `perceive` calls that actually folded (memo misses only). */
|
|
139
|
+
perceptions = 0;
|
|
140
|
+
/** Bytes folded by those perceptions — perception is O(bytes), so this is
|
|
141
|
+
* its true cost, and it is what a multi-turn regression shows up in first
|
|
142
|
+
* (re-folding the whole context instead of the new turn). */
|
|
143
|
+
perceivedBytes = 0;
|
|
144
|
+
/** `perceive` calls served from the per-response / conversation memo. */
|
|
145
|
+
perceiveHits = 0;
|
|
146
|
+
/** `recognise` calls that actually ran. */
|
|
147
|
+
recognitions = 0;
|
|
148
|
+
/** Bytes recognised by those calls. */
|
|
149
|
+
recognisedBytes = 0;
|
|
150
|
+
/** `recognise` calls served from the memo. */
|
|
151
|
+
recogniseHits = 0;
|
|
152
|
+
/** `resolve` — whole-span content-addressed identity requests. */
|
|
153
|
+
resolves = 0;
|
|
154
|
+
|
|
155
|
+
// ── Mind: the consensus climb ───────────────────────────────────────────
|
|
156
|
+
/** `climbAttentionAll` calls that actually climbed. */
|
|
157
|
+
climbs = 0;
|
|
158
|
+
/** `climbAttentionAll` calls served from `climbMemo`. */
|
|
159
|
+
climbHits = 0;
|
|
160
|
+
/** Query regions the climb voted on, summed over climbs. */
|
|
161
|
+
climbRegions = 0;
|
|
162
|
+
/** Nodes popped and examined by `edgeAncestors` ascents — the climb's
|
|
163
|
+
* inner loop, and the thing `hubBound` is supposed to be bounding. */
|
|
164
|
+
ancestorVisits = 0;
|
|
165
|
+
|
|
166
|
+
// ── Mind: matching & search ─────────────────────────────────────────────
|
|
167
|
+
/** `alignGraded` / `alignRuns` invocations. */
|
|
168
|
+
alignments = 0;
|
|
169
|
+
/** Σ (query bytes × context bytes) over those alignments — the alignment
|
|
170
|
+
* family is quadratic, so this is the honest unit. */
|
|
171
|
+
alignCells = 0;
|
|
172
|
+
/** `junctionContainersFrom` ascents started — the cross-region ladder's
|
|
173
|
+
* and the bridge's shared "which learnt whole contains these two forms?"
|
|
174
|
+
* walk. */
|
|
175
|
+
junctionWalks = 0;
|
|
176
|
+
/** Nodes popped by those ascents, against their √N·W budget — the counter
|
|
177
|
+
* that shows whether the walks are deciding early or burning the budget. */
|
|
178
|
+
junctionPops = 0;
|
|
179
|
+
|
|
180
|
+
/** `lightestDerivation` searches started. */
|
|
181
|
+
searches = 0;
|
|
182
|
+
/** Chart items popped by those searches. */
|
|
183
|
+
searchPops = 0;
|
|
184
|
+
/** Chart items pushed by those searches. */
|
|
185
|
+
searchPushes = 0;
|
|
186
|
+
|
|
187
|
+
// ── Mind: the mechanism market ──────────────────────────────────────────
|
|
188
|
+
/** `floor()` calls that returned a bound (the mechanism could fire). */
|
|
189
|
+
mechanismFloors = 0;
|
|
190
|
+
/** `floor()` calls that returned null (structurally impossible). */
|
|
191
|
+
mechanismSkips = 0;
|
|
192
|
+
/** `run()` calls — the ones the floor pruning let through. */
|
|
193
|
+
mechanismRuns = 0;
|
|
194
|
+
/** Candidates the decider weighed. */
|
|
195
|
+
candidates = 0;
|
|
196
|
+
/** Candidates refused before the competition for explaining less than 1/W
|
|
197
|
+
* of the query — the honesty-density floor (see pipeline.ts `consider`). */
|
|
198
|
+
thinRejects = 0;
|
|
199
|
+
|
|
200
|
+
// ── Phases ──────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
private readonly _phases = new Map<string, PhaseCost>();
|
|
203
|
+
private readonly _t0 = performance.now();
|
|
204
|
+
|
|
205
|
+
/** Every work counter's current value, by name — the snapshot `time`
|
|
206
|
+
* differences to attribute work to a phase. */
|
|
207
|
+
private snapshot(): Record<string, number> {
|
|
208
|
+
const out: Record<string, number> = {};
|
|
209
|
+
for (const [k, v] of Object.entries(this)) {
|
|
210
|
+
if (k.charCodeAt(0) === 0x5f) continue;
|
|
211
|
+
if (typeof v === "number") out[k] = v;
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Charge `ms`, one call, and a counter delta to a named phase.
|
|
217
|
+
* Insertion-ordered, so a report reads in execution order. */
|
|
218
|
+
charge(phase: string, ms: number, delta?: Record<string, number>): void {
|
|
219
|
+
let p = this._phases.get(phase);
|
|
220
|
+
if (p === undefined) {
|
|
221
|
+
this._phases.set(phase, p = { calls: 0, ms: 0, counters: {} });
|
|
222
|
+
}
|
|
223
|
+
p.calls++;
|
|
224
|
+
p.ms += ms;
|
|
225
|
+
if (delta) {
|
|
226
|
+
for (const [k, v] of Object.entries(delta)) {
|
|
227
|
+
if (v !== 0) p.counters[k] = (p.counters[k] ?? 0) + v;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Time one async phase and attribute the work done inside it. Returns
|
|
233
|
+
* the awaited value untouched — a meter never changes what a layer
|
|
234
|
+
* computes, only what is known about it. */
|
|
235
|
+
async time<T>(phase: string, fn: () => Promise<T>): Promise<T> {
|
|
236
|
+
const before = this.snapshot();
|
|
237
|
+
const t = performance.now();
|
|
238
|
+
try {
|
|
239
|
+
return await fn();
|
|
240
|
+
} finally {
|
|
241
|
+
const ms = performance.now() - t;
|
|
242
|
+
const after = this.snapshot();
|
|
243
|
+
const delta: Record<string, number> = {};
|
|
244
|
+
for (const k of Object.keys(after)) delta[k] = after[k] - before[k];
|
|
245
|
+
this.charge(phase, ms, delta);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Read-out ────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
/** The finished report. Zero-valued counters are dropped: a report should
|
|
252
|
+
* show what a query DID, not the whole vocabulary of what it might have. */
|
|
253
|
+
report(queryBytes: number): CostReport {
|
|
254
|
+
const counters: Record<string, number> = {};
|
|
255
|
+
for (const [k, v] of Object.entries(this)) {
|
|
256
|
+
// `_`-prefixed fields are the meter's own bookkeeping (`_t0` is a
|
|
257
|
+
// non-zero number and would otherwise read as a work counter).
|
|
258
|
+
if (k.charCodeAt(0) === 0x5f) continue;
|
|
259
|
+
if (typeof v === "number" && v !== 0) counters[k] = v;
|
|
260
|
+
}
|
|
261
|
+
const phases: Record<string, PhaseCost> = {};
|
|
262
|
+
for (const [k, v] of this._phases) {
|
|
263
|
+
phases[k] = { calls: v.calls, ms: v.ms, counters: { ...v.counters } };
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
version: 1,
|
|
267
|
+
elapsedMs: performance.now() - this._t0,
|
|
268
|
+
queryBytes,
|
|
269
|
+
counters,
|
|
270
|
+
phases,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Sum a set of reports into one — for aggregating a battery of probes or a
|
|
276
|
+
* multi-turn session. `elapsedMs` and `queryBytes` add; counters and phases
|
|
277
|
+
* merge by key. */
|
|
278
|
+
export function sumReports(reports: readonly CostReport[]): CostReport {
|
|
279
|
+
const counters: Record<string, number> = {};
|
|
280
|
+
const phases: Record<string, PhaseCost> = {};
|
|
281
|
+
let elapsedMs = 0;
|
|
282
|
+
let queryBytes = 0;
|
|
283
|
+
for (const r of reports) {
|
|
284
|
+
elapsedMs += r.elapsedMs;
|
|
285
|
+
queryBytes += r.queryBytes;
|
|
286
|
+
for (const [k, v] of Object.entries(r.counters)) {
|
|
287
|
+
counters[k] = (counters[k] ?? 0) + v;
|
|
288
|
+
}
|
|
289
|
+
for (const [k, v] of Object.entries(r.phases)) {
|
|
290
|
+
const p = phases[k] ??= { calls: 0, ms: 0, counters: {} };
|
|
291
|
+
p.calls += v.calls;
|
|
292
|
+
p.ms += v.ms;
|
|
293
|
+
for (const [ck, cv] of Object.entries(v.counters)) {
|
|
294
|
+
p.counters[ck] = (p.counters[ck] ?? 0) + cv;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { version: 1, elapsedMs, queryBytes, counters, phases };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** A human-readable rendering of a report — the shape a profiling run prints.
|
|
302
|
+
* Pure formatting; no ANSI, so it is safe to log anywhere. */
|
|
303
|
+
export function formatReport(r: CostReport): string {
|
|
304
|
+
const lines: string[] = [];
|
|
305
|
+
lines.push(
|
|
306
|
+
`cost: ${r.elapsedMs.toFixed(1)}ms over ${r.queryBytes} query byte(s)`,
|
|
307
|
+
);
|
|
308
|
+
const names = Object.keys(r.counters).sort();
|
|
309
|
+
const w = names.reduce((m, n) => Math.max(m, n.length), 0);
|
|
310
|
+
for (const n of names) {
|
|
311
|
+
lines.push(` ${n.padEnd(w)} ${r.counters[n].toLocaleString("en-US")}`);
|
|
312
|
+
}
|
|
313
|
+
const ph = Object.entries(r.phases);
|
|
314
|
+
if (ph.length > 0) {
|
|
315
|
+
lines.push(" phases (nested — inclusive, do not sum):");
|
|
316
|
+
const pw = ph.reduce((m, [n]) => Math.max(m, n.length), 0);
|
|
317
|
+
for (const [n, p] of ph) {
|
|
318
|
+
// The three heaviest counters inside the phase — enough to say WHAT it
|
|
319
|
+
// spent, without reprinting the whole vocabulary per line.
|
|
320
|
+
const top = Object.entries(p.counters)
|
|
321
|
+
.sort((a, b) => b[1] - a[1])
|
|
322
|
+
.slice(0, 3)
|
|
323
|
+
.map(([k, v]) => `${k} ${v.toLocaleString("en-US")}`)
|
|
324
|
+
.join(", ");
|
|
325
|
+
lines.push(
|
|
326
|
+
` ${n.padEnd(pw)} ${p.calls}× ${p.ms.toFixed(1)}ms${
|
|
327
|
+
top ? ` [${top}]` : ""
|
|
328
|
+
}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return lines.join("\n");
|
|
333
|
+
}
|