@shapeshift-labs/frontier-mutation 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -32,6 +32,26 @@ The compiled patch contains concrete paths, indexes, and values. Queries are com
32
32
 
33
33
  The public surface is intentionally small: build selectors, build a mutation plan, then compile or commit it. Planner choices, compiler passes, and CRDT lowering stay behind options and result metadata.
34
34
 
35
+ ## Performance
36
+
37
+ Frontier Mutation was measured from this package with `npm run bench` on Node v26.1.0, darwin arm64. Timings are median microseconds per operation across 9 warmed rounds; p95 is shown to make noise visible. Patch bytes are `JSON.stringify(patch)` bytes because this package emits Frontier patches and does not own binary transport encoding.
38
+
39
+ | Fixture | Matches | Strategy | Patch | Bytes | Compile median | Compile p95 | Apply median |
40
+ | --- | ---: | --- | ---: | ---: | ---: | ---: | ---: |
41
+ | Selector increment, 1% sparse 10k-row table | 100 | row-field | 1 op | 1007 B | 2.74 ms | 3.40 ms | 4.40 us |
42
+ | Selector increment, indexed id IN | 100 | row-field | 1 op | 1003 B | 1.68 ms | 1.77 ms | 3.81 us |
43
+ | Selector increment, 10% 10k-row table | 1,000 | row-field | 1 op | 9.6 KiB | 2.86 ms | 2.92 ms | 17.17 us |
44
+ | Repeated arithmetic fold, 1000x | 0 | direct | 1 op | 20 B | 0.49 us | 0.60 us | 0.04 us |
45
+ | Repeated text append fold, 1000x | 0 | direct | 1 op | 1021 B | 0.63 us | 0.78 us | 0.08 us |
46
+
47
+ These are Frontier-only package measurements, not a competitor comparison. Hardware, Node version, selector shape, and table size will affect absolute timings.
48
+
49
+ Reproduce the package-local benchmark with:
50
+
51
+ ```sh
52
+ npm run bench
53
+ ```
54
+
35
55
  ## API Overview
36
56
 
37
57
  ```ts
@@ -0,0 +1,289 @@
1
+ import assert from 'node:assert';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { performance } from 'node:perf_hooks';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { applyPatchImmutable } from '@shapeshift-labs/frontier';
7
+ import {
8
+ createMutationPlan,
9
+ select
10
+ } from '../dist/index.js';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const rootDir = path.resolve(__dirname, '..');
14
+ const args = parseArgs(process.argv.slice(2));
15
+ const rounds = readPositiveInt(args.rounds, 9);
16
+ const outPath = args.out ? path.resolve(rootDir, args.out) : null;
17
+
18
+ let sink = 0;
19
+
20
+ const rows = [
21
+ runSelectorFixture(makeSparseSelectorFixture()),
22
+ runSelectorFixture(makeIndexedIdFixture()),
23
+ runSelectorFixture(makeDenseSelectorFixture()),
24
+ runDirectFixture(makeRepeatedArithmeticFixture()),
25
+ runDirectFixture(makeRepeatedTextFixture())
26
+ ];
27
+
28
+ const report = {
29
+ package: '@shapeshift-labs/frontier-mutation',
30
+ version: readPackageVersion(),
31
+ generatedAt: new Date().toISOString(),
32
+ node: process.version,
33
+ platform: process.platform + ' ' + process.arch,
34
+ rounds,
35
+ rows
36
+ };
37
+
38
+ if (outPath) {
39
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
40
+ fs.writeFileSync(outPath, JSON.stringify(report, null, 2) + '\n');
41
+ }
42
+
43
+ printReport(report);
44
+ if (sink === 42) console.log('sink=' + sink);
45
+
46
+ function runSelectorFixture(fixture) {
47
+ const result = fixture.plan.compilePatch(fixture.state, fixture.options);
48
+ assert.strictEqual(result.matched, fixture.expectedMatches);
49
+ const next = applyPatchImmutable(fixture.state, result.patch);
50
+ assert.ok(next);
51
+ const compileTiming = measure(() => {
52
+ const compiled = fixture.plan.compilePatch(fixture.state, fixture.options);
53
+ sink += compiled.patch.length + compiled.matched;
54
+ }, fixture.compileInner || 20);
55
+ const applyTiming = measure(() => {
56
+ const applied = applyPatchImmutable(fixture.state, result.patch);
57
+ sink += applied.rows.length;
58
+ }, fixture.applyInner || 1000);
59
+ return {
60
+ fixture: fixture.name,
61
+ matches: result.matched,
62
+ strategy: result.decisions[0]?.strategy || 'unknown',
63
+ patchOps: result.patch.length,
64
+ jsonPatchBytes: Buffer.byteLength(JSON.stringify(result.patch)),
65
+ compileMedianUs: round(compileTiming.median),
66
+ compileP95Us: round(compileTiming.p95),
67
+ applyMedianUs: round(applyTiming.median),
68
+ applyP95Us: round(applyTiming.p95)
69
+ };
70
+ }
71
+
72
+ function runDirectFixture(fixture) {
73
+ const result = fixture.plan.compilePatch(fixture.state, fixture.options);
74
+ const next = applyPatchImmutable(fixture.state, result.patch);
75
+ assert.ok(next);
76
+ const compileTiming = measure(() => {
77
+ const compiled = fixture.plan.compilePatch(fixture.state, fixture.options);
78
+ sink += compiled.patch.length;
79
+ }, fixture.compileInner || 5000);
80
+ const applyTiming = measure(() => {
81
+ const applied = applyPatchImmutable(fixture.state, result.patch);
82
+ sink += Object.keys(applied || {}).length;
83
+ }, fixture.applyInner || 10000);
84
+ return {
85
+ fixture: fixture.name,
86
+ matches: result.matched,
87
+ strategy: result.decisions[0]?.strategy || 'direct',
88
+ patchOps: result.patch.length,
89
+ jsonPatchBytes: Buffer.byteLength(JSON.stringify(result.patch)),
90
+ compileMedianUs: round(compileTiming.median),
91
+ compileP95Us: round(compileTiming.p95),
92
+ applyMedianUs: round(applyTiming.median),
93
+ applyP95Us: round(applyTiming.p95)
94
+ };
95
+ }
96
+
97
+ function makeSparseSelectorFixture() {
98
+ const state = { rows: makeRows(10000) };
99
+ const selector = select('/rows/*')
100
+ .where('bucket100', 'eq', 0)
101
+ .keyBy('id')
102
+ .indexBy('id');
103
+ return {
104
+ name: 'Selector increment, 1% sparse 10k-row table',
105
+ state,
106
+ plan: createMutationPlan().forEach(selector).increment('score', 1),
107
+ options: mutationOptions(),
108
+ expectedMatches: 100,
109
+ compileInner: 10,
110
+ applyInner: 1000
111
+ };
112
+ }
113
+
114
+ function makeIndexedIdFixture() {
115
+ const state = { rows: makeRows(10000) };
116
+ const ids = new Array(100);
117
+ for (let i = 0; i < ids.length; i++) ids[i] = 'row-' + (i * 97);
118
+ const selector = select('/rows/*')
119
+ .where('id', 'in', ids)
120
+ .keyBy('id')
121
+ .indexBy('id');
122
+ return {
123
+ name: 'Selector increment, indexed id IN',
124
+ state,
125
+ plan: createMutationPlan().forEach(selector).increment('score', 1),
126
+ options: mutationOptions(),
127
+ expectedMatches: 100,
128
+ compileInner: 10,
129
+ applyInner: 1000
130
+ };
131
+ }
132
+
133
+ function makeDenseSelectorFixture() {
134
+ const state = { rows: makeRows(10000) };
135
+ const selector = select('/rows/*')
136
+ .where('bucket10', 'eq', 0)
137
+ .keyBy('id')
138
+ .indexBy('id');
139
+ return {
140
+ name: 'Selector increment, 10% 10k-row table',
141
+ state,
142
+ plan: createMutationPlan().forEach(selector).increment('score', 1),
143
+ options: mutationOptions(),
144
+ expectedMatches: 1000,
145
+ compileInner: 8,
146
+ applyInner: 1000
147
+ };
148
+ }
149
+
150
+ function makeRepeatedArithmeticFixture() {
151
+ return {
152
+ name: 'Repeated arithmetic fold, 1000x',
153
+ state: { count: 0 },
154
+ plan: createMutationPlan().increment('/count', 1).repeat(1000),
155
+ options: { compact: true, strategy: 'auto' },
156
+ compileInner: 5000,
157
+ applyInner: 10000
158
+ };
159
+ }
160
+
161
+ function makeRepeatedTextFixture() {
162
+ return {
163
+ name: 'Repeated text append fold, 1000x',
164
+ state: { body: '' },
165
+ plan: createMutationPlan().appendText('/body', 'x').repeat(1000),
166
+ options: { compact: true, strategy: 'auto' },
167
+ compileInner: 3000,
168
+ applyInner: 10000
169
+ };
170
+ }
171
+
172
+ function mutationOptions() {
173
+ return {
174
+ compact: true,
175
+ strategy: 'auto',
176
+ planner: {
177
+ schema: {
178
+ tables: [{
179
+ path: '/rows',
180
+ key: 'id',
181
+ stableRowShape: true,
182
+ numericFields: ['score', 'bucket10', 'bucket100'],
183
+ selectorFields: ['id', 'bucket10', 'bucket100']
184
+ }]
185
+ }
186
+ }
187
+ };
188
+ }
189
+
190
+ function makeRows(count) {
191
+ const rows = new Array(count);
192
+ for (let i = 0; i < count; i++) {
193
+ rows[i] = {
194
+ id: 'row-' + i,
195
+ score: i,
196
+ bucket10: i % 10,
197
+ bucket100: i % 100,
198
+ label: 'row ' + i
199
+ };
200
+ }
201
+ return rows;
202
+ }
203
+
204
+ function measure(fn, inner) {
205
+ for (let i = 0; i < inner; i++) fn();
206
+ const samples = new Array(rounds);
207
+ for (let roundIndex = 0; roundIndex < rounds; roundIndex++) {
208
+ const start = performance.now();
209
+ for (let i = 0; i < inner; i++) fn();
210
+ samples[roundIndex] = ((performance.now() - start) * 1000) / inner;
211
+ }
212
+ samples.sort((left, right) => left - right);
213
+ return {
214
+ median: percentile(samples, 0.5),
215
+ p95: percentile(samples, 0.95)
216
+ };
217
+ }
218
+
219
+ function printReport(report) {
220
+ console.log('@shapeshift-labs/frontier-mutation package benchmark');
221
+ console.log('Node ' + report.node + ' on ' + report.platform + ', rounds=' + rounds);
222
+ console.log('These are Frontier-only package measurements, not competitor comparisons.');
223
+ console.log('');
224
+ console.log(padRight('Fixture', 43) + padLeft('Matches', 9) + padLeft('Strategy', 12) + padLeft('Patch', 8) + padLeft('Bytes', 9) + padLeft('Compile', 12) + padLeft('C p95', 11) + padLeft('Apply', 11));
225
+ for (const row of report.rows) {
226
+ console.log(
227
+ padRight(row.fixture, 43) +
228
+ padLeft(String(row.matches), 9) +
229
+ padLeft(row.strategy, 12) +
230
+ padLeft(String(row.patchOps), 8) +
231
+ padLeft(formatBytes(row.jsonPatchBytes), 9) +
232
+ padLeft(formatUs(row.compileMedianUs), 12) +
233
+ padLeft(formatUs(row.compileP95Us), 11) +
234
+ padLeft(formatUs(row.applyMedianUs), 11)
235
+ );
236
+ }
237
+ if (outPath) console.log('\nwrote ' + path.relative(rootDir, outPath));
238
+ }
239
+
240
+ function percentile(sorted, fraction) {
241
+ return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * fraction) - 1))];
242
+ }
243
+
244
+ function readPackageVersion() {
245
+ return JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8')).version;
246
+ }
247
+
248
+ function parseArgs(argv) {
249
+ const out = {};
250
+ for (let i = 0; i < argv.length; i++) {
251
+ const arg = argv[i];
252
+ if (arg === '--rounds') out.rounds = argv[++i];
253
+ else if (arg === '--out') out.out = argv[++i];
254
+ else if (arg === '--help' || arg === '-h') {
255
+ console.log('Usage: npm run bench -- [--rounds 9] [--out benchmarks/results/frontier-mutation-package-bench.json]');
256
+ process.exit(0);
257
+ } else {
258
+ throw new Error('unknown argument: ' + arg);
259
+ }
260
+ }
261
+ return out;
262
+ }
263
+
264
+ function readPositiveInt(value, fallback) {
265
+ if (value === undefined) return fallback;
266
+ const number = Number(value);
267
+ if (!Number.isInteger(number) || number <= 0) throw new Error('expected positive integer, got ' + value);
268
+ return number;
269
+ }
270
+
271
+ function round(value) {
272
+ return Math.round(value * 100) / 100;
273
+ }
274
+
275
+ function formatUs(value) {
276
+ return value >= 1000 ? (value / 1000).toFixed(2) + ' ms' : value.toFixed(2) + ' us';
277
+ }
278
+
279
+ function formatBytes(value) {
280
+ return value < 1024 ? value + ' B' : (value / 1024).toFixed(1) + ' KiB';
281
+ }
282
+
283
+ function padRight(value, width) {
284
+ return String(value).padEnd(width);
285
+ }
286
+
287
+ function padLeft(value, width) {
288
+ return String(value).padStart(width);
289
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-mutation",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Explicit mutation and selector plans compiled to Frontier patches or CRDT operations.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,11 +24,13 @@
24
24
  },
25
25
  "files": [
26
26
  "dist",
27
+ "benchmarks/package-bench.mjs",
27
28
  "README.md",
28
29
  "LICENSE"
29
30
  ],
30
31
  "scripts": {
31
32
  "build": "tsc -p tsconfig.json",
33
+ "bench": "npm run build && node benchmarks/package-bench.mjs",
32
34
  "test": "npm run build && node test/smoke.mjs",
33
35
  "prepack": "npm run test"
34
36
  },