@celox-sim/celox 0.0.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/src/dut.ts ADDED
@@ -0,0 +1,449 @@
1
+ /**
2
+ * DUT (Device Under Test) accessor factory.
3
+ *
4
+ * Builds a plain object with Object.defineProperty getter/setters that
5
+ * read and write directly via DataView on a SharedArrayBuffer.
6
+ * No Proxy is used — every port becomes a concrete property whose
7
+ * accessors are fully visible to V8 inline caches.
8
+ */
9
+
10
+ import type {
11
+ NativeHandle,
12
+ PortInfo,
13
+ SignalLayout,
14
+ FourStateValue,
15
+ } from "./types.js";
16
+ import { isFourStateValue } from "./types.js";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Internal dirty-tracking state shared between DUT and Simulator/Simulation
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Mutable state shared between the DUT accessor and its owning
24
+ * Simulator/Simulation instance. The Simulator clears `dirty` after
25
+ * tick()/runUntil(); the DUT sets it on any input write and checks it
26
+ * before any output read.
27
+ * @internal
28
+ */
29
+ export interface DirtyState {
30
+ dirty: boolean;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // DataView helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /** Read an unsigned integer of the given byte-size (little-endian), masked to width. */
38
+ function readNumber(view: DataView, offset: number, width: number): number {
39
+ if (width <= 8) {
40
+ const raw = view.getUint8(offset);
41
+ return width === 8 ? raw : raw & ((1 << width) - 1);
42
+ }
43
+ if (width <= 16) {
44
+ const raw = view.getUint16(offset, true);
45
+ return width === 16 ? raw : raw & ((1 << width) - 1);
46
+ }
47
+ if (width <= 32) {
48
+ const raw = view.getUint32(offset, true);
49
+ return width === 32 ? raw : (raw & ((1 << width) - 1)) >>> 0;
50
+ }
51
+ // 33..53 bits — fits safely in a JS number
52
+ const lo = view.getUint32(offset, true);
53
+ const hi = view.getUint32(offset + 4, true) & ((1 << (width - 32)) - 1);
54
+ return lo + hi * 0x1_0000_0000;
55
+ }
56
+
57
+ /** Write an unsigned integer of the given byte-size (little-endian). */
58
+ function writeNumber(
59
+ view: DataView,
60
+ offset: number,
61
+ width: number,
62
+ value: number,
63
+ ): void {
64
+ if (width <= 8) {
65
+ view.setUint8(offset, value & ((1 << width) - 1));
66
+ } else if (width <= 16) {
67
+ view.setUint16(offset, value & ((1 << width) - 1), true);
68
+ } else if (width <= 32) {
69
+ view.setUint32(offset, value >>> 0, true);
70
+ } else {
71
+ // 33..53 bits
72
+ view.setUint32(offset, value >>> 0, true);
73
+ view.setUint32(offset + 4, Math.floor(value / 0x1_0000_0000) >>> 0, true);
74
+ }
75
+ }
76
+
77
+ /** Read a wide value (≥ 54 bits) as BigInt, little-endian. */
78
+ function readBigInt(view: DataView, offset: number, byteSize: number): bigint {
79
+ let result = 0n;
80
+ // Read 8 bytes at a time, then remaining bytes
81
+ const fullWords = Math.floor(byteSize / 8);
82
+ for (let i = 0; i < fullWords; i++) {
83
+ const word = view.getBigUint64(offset + i * 8, true);
84
+ result |= word << BigInt(i * 64);
85
+ }
86
+ const remaining = byteSize % 8;
87
+ if (remaining > 0) {
88
+ const base = offset + fullWords * 8;
89
+ for (let i = 0; i < remaining; i++) {
90
+ result |= BigInt(view.getUint8(base + i)) << BigInt(fullWords * 64 + i * 8);
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+
96
+ /** Write a wide value (≥ 54 bits) as BigInt, little-endian. */
97
+ function writeBigInt(
98
+ view: DataView,
99
+ offset: number,
100
+ byteSize: number,
101
+ value: bigint,
102
+ ): void {
103
+ const fullWords = Math.floor(byteSize / 8);
104
+ for (let i = 0; i < fullWords; i++) {
105
+ view.setBigUint64(offset + i * 8, value & 0xFFFF_FFFF_FFFF_FFFFn, true);
106
+ value >>= 64n;
107
+ }
108
+ const remaining = byteSize % 8;
109
+ if (remaining > 0) {
110
+ const base = offset + fullWords * 8;
111
+ for (let i = 0; i < remaining; i++) {
112
+ view.setUint8(base + i, Number(value & 0xFFn));
113
+ value >>= 8n;
114
+ }
115
+ }
116
+ }
117
+
118
+ /** Read a signal value from the DataView. Returns number or bigint. */
119
+ function readSignal(
120
+ view: DataView,
121
+ sig: SignalLayout,
122
+ ): number | bigint {
123
+ if (sig.width <= 53) {
124
+ return readNumber(view, sig.offset, sig.width);
125
+ }
126
+ return readBigInt(view, sig.offset, sig.byteSize);
127
+ }
128
+
129
+ /** Write a signal value to the DataView. Accepts number or bigint. */
130
+ function writeSignal(
131
+ view: DataView,
132
+ sig: SignalLayout,
133
+ value: number | bigint,
134
+ ): void {
135
+ if (sig.width <= 53 && typeof value === "number") {
136
+ writeNumber(view, sig.offset, sig.width, value);
137
+ } else {
138
+ const bigVal = typeof value === "bigint" ? value : BigInt(value);
139
+ writeBigInt(view, sig.offset, sig.byteSize, bigVal);
140
+ }
141
+ }
142
+
143
+ /** Write a 4-state value (value + mask) to the DataView. */
144
+ function writeFourState(
145
+ view: DataView,
146
+ sig: SignalLayout,
147
+ fsv: FourStateValue,
148
+ ): void {
149
+ writeSignal(view, sig, fsv.value);
150
+ // Mask is stored immediately after the value bytes
151
+ const maskLayout: SignalLayout = {
152
+ offset: sig.offset + sig.byteSize,
153
+ width: sig.width,
154
+ byteSize: sig.byteSize,
155
+ is4state: false,
156
+ direction: sig.direction,
157
+ };
158
+ writeSignal(view, maskLayout, fsv.mask);
159
+ }
160
+
161
+ /** Write all-X mask for a signal. */
162
+ function writeAllX(view: DataView, sig: SignalLayout): void {
163
+ // Value = 0, mask = all 1s
164
+ writeSignal(view, sig, sig.width <= 53 ? 0 : 0n);
165
+ const allOnes =
166
+ sig.width <= 53
167
+ ? (sig.width === 53 ? Number.MAX_SAFE_INTEGER : (1 << sig.width) - 1)
168
+ : (1n << BigInt(sig.width)) - 1n;
169
+ const maskLayout: SignalLayout = {
170
+ offset: sig.offset + sig.byteSize,
171
+ width: sig.width,
172
+ byteSize: sig.byteSize,
173
+ is4state: false,
174
+ direction: sig.direction,
175
+ };
176
+ writeSignal(view, maskLayout, allOnes);
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // DUT factory
181
+ // ---------------------------------------------------------------------------
182
+
183
+ /**
184
+ * Create a DUT accessor object with defineProperty-based getters/setters.
185
+ *
186
+ * @param buffer SharedArrayBuffer from NAPI create()
187
+ * @param layout Per-signal byte layout within the buffer
188
+ * @param portDefs Port metadata from the ModuleDefinition
189
+ * @param handle Native handle (for evalComb calls)
190
+ * @param state Shared dirty-tracking state
191
+ */
192
+ export function createDut<P>(
193
+ buffer: ArrayBuffer | SharedArrayBuffer,
194
+ layout: Record<string, SignalLayout>,
195
+ portDefs: Record<string, PortInfo>,
196
+ handle: NativeHandle,
197
+ state: DirtyState,
198
+ ): P {
199
+ const view = new DataView(buffer);
200
+ const obj = Object.create(null) as P;
201
+
202
+ // Iterate portDefs (not layout) so that interface ports are discovered
203
+ // even though their individual members are the ones that appear in layout.
204
+ for (const [name, port] of Object.entries(portDefs)) {
205
+ // Skip clock ports — they are controlled via tick()/addClock()
206
+ if (port.type === "clock") continue;
207
+
208
+ // Check for nested interface
209
+ if (port.interface) {
210
+ const nestedObj = createNestedDut(
211
+ view,
212
+ layout,
213
+ port.interface,
214
+ name,
215
+ handle,
216
+ state,
217
+ );
218
+ Object.defineProperty(obj, name, {
219
+ value: nestedObj,
220
+ enumerable: true,
221
+ configurable: false,
222
+ writable: false,
223
+ });
224
+ continue;
225
+ }
226
+
227
+ const sig = layout[name];
228
+ if (!sig) continue;
229
+
230
+ // Check for array port
231
+ if (port.arrayDims && port.arrayDims.length > 0) {
232
+ const arrayObj = createArrayDut(view, sig, port, handle, state);
233
+ Object.defineProperty(obj, name, {
234
+ value: arrayObj,
235
+ enumerable: true,
236
+ configurable: false,
237
+ writable: false,
238
+ });
239
+ continue;
240
+ }
241
+
242
+ // Scalar port — define getter/setter
243
+ defineSignalProperty(obj as object, name, view, sig, port, handle, state);
244
+ }
245
+
246
+ return obj;
247
+ }
248
+
249
+ /** Define a single scalar signal property on the target object. */
250
+ function defineSignalProperty(
251
+ target: object,
252
+ name: string,
253
+ view: DataView,
254
+ sig: SignalLayout,
255
+ port: PortInfo | undefined,
256
+ handle: NativeHandle,
257
+ state: DirtyState,
258
+ ): void {
259
+ const isOutput = port?.direction === "output";
260
+ const isInput = port?.direction === "input";
261
+
262
+ Object.defineProperty(target, name, {
263
+ get(): number | bigint {
264
+ // Output reads: lazy evalComb if dirty
265
+ if (state.dirty && !isInput) {
266
+ handle.evalComb();
267
+ state.dirty = false;
268
+ }
269
+ return readSignal(view, sig);
270
+ },
271
+
272
+ set(value: number | bigint | symbol | FourStateValue) {
273
+ if (isOutput) {
274
+ throw new Error(`Cannot write to output port '${name}'`);
275
+ }
276
+
277
+ if (value === Symbol.for("veryl:X")) {
278
+ if (!sig.is4state) {
279
+ throw new Error(`Port '${name}' is not 4-state; cannot assign X`);
280
+ }
281
+ writeAllX(view, sig);
282
+ } else if (isFourStateValue(value)) {
283
+ if (!sig.is4state) {
284
+ throw new Error(`Port '${name}' is not 4-state; cannot assign FourState`);
285
+ }
286
+ writeFourState(view, sig, value);
287
+ } else {
288
+ writeSignal(view, sig, value as number | bigint);
289
+ // Clear mask when writing a defined value to a 4-state signal
290
+ if (sig.is4state) {
291
+ const maskLayout: SignalLayout = {
292
+ offset: sig.offset + sig.byteSize,
293
+ width: sig.width,
294
+ byteSize: sig.byteSize,
295
+ is4state: false,
296
+ direction: sig.direction,
297
+ };
298
+ writeSignal(view, maskLayout, sig.width <= 53 ? 0 : 0n);
299
+ }
300
+ }
301
+
302
+ state.dirty = true;
303
+ },
304
+
305
+ enumerable: true,
306
+ configurable: false,
307
+ });
308
+ }
309
+
310
+ // ---------------------------------------------------------------------------
311
+ // Nested interface accessor
312
+ // ---------------------------------------------------------------------------
313
+
314
+ function createNestedDut(
315
+ view: DataView,
316
+ layout: Record<string, SignalLayout>,
317
+ members: Record<string, PortInfo>,
318
+ prefix: string,
319
+ handle: NativeHandle,
320
+ state: DirtyState,
321
+ ): object {
322
+ const obj = Object.create(null);
323
+
324
+ for (const [memberName, memberPort] of Object.entries(members)) {
325
+ const qualifiedName = `${prefix}.${memberName}`;
326
+ const sig = layout[qualifiedName];
327
+ if (!sig) continue;
328
+
329
+ if (memberPort.interface) {
330
+ const nested = createNestedDut(
331
+ view,
332
+ layout,
333
+ memberPort.interface,
334
+ qualifiedName,
335
+ handle,
336
+ state,
337
+ );
338
+ Object.defineProperty(obj, memberName, {
339
+ value: nested,
340
+ enumerable: true,
341
+ configurable: false,
342
+ writable: false,
343
+ });
344
+ } else {
345
+ defineSignalProperty(obj, memberName, view, sig, memberPort, handle, state);
346
+ }
347
+ }
348
+
349
+ return obj;
350
+ }
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // Array port accessor
354
+ // ---------------------------------------------------------------------------
355
+
356
+ function createArrayDut(
357
+ view: DataView,
358
+ baseSig: SignalLayout,
359
+ port: PortInfo,
360
+ handle: NativeHandle,
361
+ state: DirtyState,
362
+ ): object {
363
+ const dims = port.arrayDims!;
364
+ const elementWidth = port.width;
365
+ const elementByteSize = Math.ceil(elementWidth / 8);
366
+ const totalElements = dims.reduce((a, b) => a * b, 1);
367
+ const isOutput = port.direction === "output";
368
+ const isInput = port.direction === "input";
369
+ const baseOffset = baseSig.offset;
370
+ const is4state = baseSig.is4state;
371
+
372
+ return {
373
+ length: totalElements,
374
+
375
+ at(i: number): number | bigint {
376
+ if (state.dirty && !isInput) {
377
+ handle.evalComb();
378
+ state.dirty = false;
379
+ }
380
+ const offset = baseOffset + i * elementByteSize;
381
+ if (elementWidth <= 53) {
382
+ return readNumber(view, offset, elementWidth);
383
+ }
384
+ return readBigInt(view, offset, elementByteSize);
385
+ },
386
+
387
+ set(i: number, value: number | bigint | symbol | FourStateValue): void {
388
+ if (isOutput) {
389
+ throw new Error("Cannot write to output array port");
390
+ }
391
+ const offset = baseOffset + i * elementByteSize;
392
+ if (value === Symbol.for("veryl:X")) {
393
+ if (!is4state) {
394
+ throw new Error("Array port is not 4-state; cannot assign X");
395
+ }
396
+ const elemSig: SignalLayout = {
397
+ offset, width: elementWidth, byteSize: elementByteSize,
398
+ is4state, direction: baseSig.direction,
399
+ };
400
+ writeAllX(view, elemSig);
401
+ } else if (isFourStateValue(value)) {
402
+ if (!is4state) {
403
+ throw new Error("Array port is not 4-state; cannot assign FourState");
404
+ }
405
+ const elemSig: SignalLayout = {
406
+ offset, width: elementWidth, byteSize: elementByteSize,
407
+ is4state, direction: baseSig.direction,
408
+ };
409
+ writeFourState(view, elemSig, value);
410
+ } else if (elementWidth <= 53 && typeof value === "number") {
411
+ writeNumber(view, offset, elementWidth, value);
412
+ if (is4state) writeNumber(view, offset + elementByteSize, elementWidth, 0);
413
+ } else {
414
+ const bigVal = typeof value === "bigint" ? value : BigInt(value as number);
415
+ writeBigInt(view, offset, elementByteSize, bigVal);
416
+ if (is4state) writeBigInt(view, offset + elementByteSize, elementByteSize, 0n);
417
+ }
418
+ state.dirty = true;
419
+ },
420
+ };
421
+ }
422
+
423
+ // ---------------------------------------------------------------------------
424
+ // 4-state read helper (exported for advanced use)
425
+ // ---------------------------------------------------------------------------
426
+
427
+ /**
428
+ * Read the raw 4-state (value, mask) pair for a signal.
429
+ * Mask bits set to 1 indicate X/Z.
430
+ */
431
+ export function readFourState(
432
+ buffer: ArrayBuffer | SharedArrayBuffer,
433
+ sig: SignalLayout,
434
+ ): [value: number | bigint, mask: number | bigint] {
435
+ if (!sig.is4state) {
436
+ throw new Error("Signal is not 4-state");
437
+ }
438
+ const view = new DataView(buffer);
439
+ const value = readSignal(view, sig);
440
+ const maskSig: SignalLayout = {
441
+ offset: sig.offset + sig.byteSize,
442
+ width: sig.width,
443
+ byteSize: sig.byteSize,
444
+ is4state: false,
445
+ direction: sig.direction,
446
+ };
447
+ const mask = readSignal(view, maskSig);
448
+ return [value, mask];
449
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Performance benchmarks — mirrors `crates/celox/benches/simulation.rs`
3
+ * and `crates/celox/benches/overhead.rs`.
4
+ *
5
+ * Measures the same operations so JS and Rust numbers are directly comparable:
6
+ * 1. Build (JIT compile)
7
+ * 2. Single tick
8
+ * 3. 1M ticks in a loop
9
+ * 4. Simulator::tick vs Simulation::step overhead
10
+ */
11
+
12
+ import { bench, describe, afterAll } from "vitest";
13
+ import { Simulator } from "./simulator.js";
14
+ import { Simulation } from "./simulation.js";
15
+ import type { ModuleDefinition } from "./types.js";
16
+ import {
17
+ loadNativeAddon,
18
+ createSimulatorBridge,
19
+ } from "./napi-helpers.js";
20
+
21
+ const CODE = `
22
+ module Top #(
23
+ param N: u32 = 1000,
24
+ )(
25
+ clk: input clock,
26
+ rst: input reset,
27
+ cnt: output logic<32>[N],
28
+ ) {
29
+ for i in 0..N: g {
30
+ always_ff (clk, rst) {
31
+ if_reset {
32
+ cnt[i] = 0;
33
+ } else {
34
+ cnt[i] += 1;
35
+ }
36
+ }
37
+ }
38
+ }
39
+ `;
40
+
41
+ interface TopPorts {
42
+ rst: number;
43
+ readonly cnt: { at(i: number): number; readonly length: number };
44
+ }
45
+
46
+ describe("simulation", () => {
47
+ bench(
48
+ "simulation_build_top_n1000",
49
+ () => {
50
+ const sim = Simulator.fromSource<TopPorts>(CODE, "Top");
51
+ sim.dispose();
52
+ },
53
+ { iterations: 3, time: 0 },
54
+ );
55
+
56
+ const sim = Simulator.fromSource<TopPorts>(CODE, "Top");
57
+
58
+ // Reset sequence
59
+ sim.dut.rst = 1;
60
+ sim.tick();
61
+ sim.dut.rst = 0;
62
+ sim.tick();
63
+
64
+ afterAll(() => {
65
+ sim.dispose();
66
+ });
67
+
68
+ bench("simulation_tick_top_n1000_x1", () => {
69
+ sim.tick();
70
+ });
71
+
72
+ bench(
73
+ "simulation_tick_top_n1000_x1000000",
74
+ () => {
75
+ for (let i = 0; i < 1_000_000; i++) {
76
+ sim.tick();
77
+ }
78
+ },
79
+ { iterations: 3, time: 0 },
80
+ );
81
+
82
+ // Testbench pattern: write input + tick + read back
83
+ bench("testbench_tick_top_n1000_x1", () => {
84
+ sim.dut.rst = 0;
85
+ sim.tick();
86
+ // biome-ignore lint: read to measure full testbench cycle
87
+ sim.dut.rst;
88
+ });
89
+
90
+ bench(
91
+ "testbench_tick_top_n1000_x1000000",
92
+ () => {
93
+ for (let i = 0; i < 1_000_000; i++) {
94
+ sim.dut.rst = 0;
95
+ sim.tick();
96
+ // biome-ignore lint: read to measure full testbench cycle
97
+ sim.dut.rst;
98
+ }
99
+ },
100
+ { iterations: 3, time: 0 },
101
+ );
102
+
103
+ // Array access via .at() — use ModuleDefinition with arrayDims
104
+ const addon = loadNativeAddon();
105
+ const TopModule: ModuleDefinition<TopPorts> = {
106
+ __celox_module: true,
107
+ name: "Top",
108
+ source: CODE,
109
+ ports: {
110
+ clk: { direction: "input", type: "clock", width: 1 },
111
+ rst: { direction: "input", type: "reset", width: 1 },
112
+ cnt: { direction: "output", type: "logic", width: 32, arrayDims: [1000] },
113
+ },
114
+ events: ["clk"],
115
+ };
116
+ const simArr = Simulator.create<TopPorts>(TopModule, {
117
+ __nativeCreate: createSimulatorBridge(addon),
118
+ });
119
+ simArr.dut.rst = 1;
120
+ simArr.tick();
121
+ simArr.dut.rst = 0;
122
+ simArr.tick();
123
+
124
+ afterAll(() => {
125
+ simArr.dispose();
126
+ });
127
+
128
+ bench("testbench_array_tick_top_n1000_x1", () => {
129
+ simArr.dut.rst = 0;
130
+ simArr.tick();
131
+ // biome-ignore lint: read array element to measure .at() overhead
132
+ simArr.dut.cnt.at(0);
133
+ });
134
+
135
+ bench(
136
+ "testbench_array_tick_top_n1000_x1000000",
137
+ () => {
138
+ for (let i = 0; i < 1_000_000; i++) {
139
+ simArr.dut.rst = 0;
140
+ simArr.tick();
141
+ // biome-ignore lint: read array element to measure .at() overhead
142
+ simArr.dut.cnt.at(0);
143
+ }
144
+ },
145
+ { iterations: 3, time: 0 },
146
+ );
147
+ });
148
+
149
+ /**
150
+ * Overhead comparison — mirrors `crates/celox/benches/overhead.rs`.
151
+ *
152
+ * Compares Simulator.tick() vs Simulation.step() to measure the
153
+ * scheduling overhead of the time-based API.
154
+ */
155
+ describe("overhead", () => {
156
+ // Simulator.tick — same as Rust simulator_tick_x10000
157
+ const simTick = Simulator.fromSource<TopPorts>(CODE, "Top");
158
+ simTick.dut.rst = 1;
159
+ simTick.tick();
160
+ simTick.dut.rst = 0;
161
+ simTick.tick();
162
+
163
+ afterAll(() => {
164
+ simTick.dispose();
165
+ });
166
+
167
+ bench(
168
+ "simulator_tick_x10000",
169
+ () => {
170
+ for (let i = 0; i < 10_000; i++) {
171
+ simTick.tick();
172
+ }
173
+ },
174
+ { iterations: 3, time: 0 },
175
+ );
176
+
177
+ // Simulation.step — same as Rust simulation_step_x20000
178
+ const simStep = Simulation.fromSource<TopPorts>(CODE, "Top");
179
+ simStep.addClock("clk", { period: 10 });
180
+
181
+ afterAll(() => {
182
+ simStep.dispose();
183
+ });
184
+
185
+ bench(
186
+ "simulation_step_x20000",
187
+ () => {
188
+ // 20000 steps = 10000 cycles (rising + falling)
189
+ for (let i = 0; i < 20_000; i++) {
190
+ simStep.step();
191
+ }
192
+ },
193
+ { iterations: 3, time: 0 },
194
+ );
195
+ });
196
+
197
+ /**
198
+ * Simulation (time-based) benchmarks — mirrors the simulation describe
199
+ * above but uses the Simulation API instead of Simulator.
200
+ */
201
+ describe("simulation-time-based", () => {
202
+ bench(
203
+ "simulation_time_build_top_n1000",
204
+ () => {
205
+ const sim = Simulation.fromSource<TopPorts>(CODE, "Top");
206
+ sim.dispose();
207
+ },
208
+ { iterations: 3, time: 0 },
209
+ );
210
+
211
+ const sim = Simulation.fromSource<TopPorts>(CODE, "Top");
212
+ sim.addClock("clk", { period: 10 });
213
+
214
+ afterAll(() => {
215
+ sim.dispose();
216
+ });
217
+
218
+ bench("simulation_time_step_x1", () => {
219
+ sim.step();
220
+ });
221
+
222
+ bench(
223
+ "simulation_time_step_x1000000",
224
+ () => {
225
+ for (let i = 0; i < 1_000_000; i++) {
226
+ sim.step();
227
+ }
228
+ },
229
+ { iterations: 3, time: 0 },
230
+ );
231
+
232
+ bench(
233
+ "simulation_time_runUntil_1000000",
234
+ () => {
235
+ const base = sim.time();
236
+ sim.runUntil(base + 1_000_000);
237
+ },
238
+ { iterations: 3, time: 0 },
239
+ );
240
+ });