@mikrojs/native 0.20.0-next.20260904225731 → 0.20.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.
@@ -410,6 +410,17 @@ void MIK_ProtocolClose(void);
410
410
  * and must detach before freeing. */
411
411
  void MIK_ProtocolAttach(MIKRuntime* mik_rt);
412
412
 
413
+ /* Record what `mik_rt` was handed at boot, reported on MSG_READY. Call once,
414
+ * from the boot path, at the point the app's entry is about to be evaluated:
415
+ * the figures then describe the floor an app starts from rather than whatever
416
+ * happens to be free when a client connects. Later calls are ignored, so a
417
+ * supervisor that swaps runtimes can't overwrite the boot reading with a
418
+ * per-test one. MIK_ProtocolAttach calls this itself, which covers embedders
419
+ * that never call it; a boot path with a test supervisor should call it
420
+ * explicitly, before the supervisor allocates, so both modes report the same
421
+ * figure. */
422
+ void MIK_CaptureBootMemory(MIKRuntime* mik_rt);
423
+
413
424
  /* Unbind the current runtime. Call before MIK_FreeRuntime when swapping
414
425
  * runtimes mid-session. The session remains open. */
415
426
  void MIK_ProtocolDetach(void);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikrojs/native",
3
- "version": "0.20.0-next.20260904225731",
3
+ "version": "0.20.0",
4
4
  "description": "Mikro.js C++ runtime library and Node.js native addon",
5
5
  "keywords": [
6
6
  "esp32",
@@ -87,15 +87,15 @@
87
87
  "cmake-js": "^8.0.0",
88
88
  "node-addon-api": "^8.7.0",
89
89
  "node-gyp-build": "^4.8.4",
90
- "@mikrojs/quickjs": "0.20.0-next.20260904225731+071a2a9"
90
+ "@mikrojs/quickjs": "0.20.0"
91
91
  },
92
92
  "devDependencies": {
93
93
  "@swc/core": "^1.15.30",
94
94
  "@types/node": "^24.12.2",
95
95
  "esbuild": "^0.28.0",
96
96
  "terser": "^5.46.2",
97
- "@mikrojs/registry": "0.20.0-next.20260904225731+071a2a9",
98
- "@mikrojs/schema": "0.20.0-next.20260904225731+071a2a9"
97
+ "@mikrojs/registry": "0.20.0",
98
+ "@mikrojs/schema": "0.20.0"
99
99
  },
100
100
  "engines": {
101
101
  "node": ">=24.0.0"
@@ -29,40 +29,62 @@ interface Suite {
29
29
 
30
30
  const suites: Suite[] = []
31
31
  let currentSuite: Suite | null = null
32
- let heapBaseline = 0
33
- /** Free system heap (bytes) at the baseline point, or 0 on the host (no
34
- * system heap). Reference point for the "peak" figure: baseline free minus
35
- * the run's low-water is the most the suite needed at once. Recaptured after
36
- * each suite's beforeAll, in step with heapBaseline, so warmup is excluded
37
- * from the figure. */
32
+ /** JS heap (bytes) the file still holds above baseline, summed over suites.
33
+ * Retention accumulates, so a per-suite figure is added rather than
34
+ * replacing the last one: a leak in suite 1 is still a leak once suite 5
35
+ * has run. */
36
+ let heapRetained = 0
37
+ /** Baseline the running suite is measured against. Set to the previous
38
+ * suite's closing heap, or recaptured after a beforeAll so that suite's
39
+ * warmup is excluded. */
40
+ let suiteBaseline = 0
41
+ /** Free system heap (bytes) at the start of the run, or 0 on the host (no
42
+ * system heap). Never recaptured: sysUsed is a peak, and warmup (module
43
+ * loads, TLS, wifi) is memory the file genuinely needed at once, so
44
+ * excluding it would understate how close the run came to OOM. Retention is
45
+ * the figure that wants warmup excluded, and it has its own baseline. */
38
46
  let sysFreeStart = 0
39
47
  /** Lowest free system heap (bytes) sampled (post-gc) this run, or 0 on the
40
- * host. The suite's closest sampled approach to OOM. The module re-inits
48
+ * host. The file's closest sampled approach to OOM. The module re-inits
41
49
  * for each test file (fresh runtime per file), so this is a true per-file
42
50
  * figure, not a process-lifetime watermark shared across files. */
43
51
  let sysFreeFloor = 0
52
+ /** Free system heap at the running suite's start, for its own peak. */
53
+ let suiteFreeStart = 0
54
+ /** Lowest free system heap sampled during the running suite. */
55
+ let suiteFreeFloor = 0
44
56
 
45
57
  /**
46
- * Recapture the heap baseline. Called by the harness after each suite's
58
+ * Recapture the suite baseline. Called by the harness after each suite's
47
59
  * beforeAll resolves so warmup allocations (module loads, fetch/TLS
48
60
  * lazy-init, wifi connection) don't count toward heapDelta. A microtask
49
61
  * yield before the gc lets the beforeAll async frame's locals become
50
62
  * collectible — otherwise the baseline would be inflated by vars that
51
63
  * were still pinned by the suspended closure when beforeAll resolved,
52
- * and heapAfter would come in lower than baseline (negative delta).
64
+ * and the suite would close below its baseline (negative delta).
53
65
  */
54
- async function captureHeapBaseline(): Promise<void> {
66
+ async function captureSuiteBaseline(): Promise<void> {
55
67
  await Promise.resolve()
68
+ gc()
69
+ const {heapUsed} = memoryUsage()
70
+ suiteBaseline = heapUsed
71
+ }
72
+
73
+ /**
74
+ * Close out the running suite: fold its retention into the file total and
75
+ * start the next suite from where this one ended. Returns the suite's own
76
+ * figures for the suite_end event, which is what a reader needs to find the
77
+ * suite behind a file-level regression.
78
+ */
79
+ function closeSuite(): {retained: number; sysUsed: number} {
56
80
  gc()
57
81
  const {heapUsed, systemFree} = memoryUsage()
58
- heapBaseline = heapUsed
59
- // Recapture the system-heap baseline and reset the per-file low-water in
60
- // step with heapBaseline, so warmup (module loads, TLS, wifi) is excluded
61
- // from sysUsed the same way it is from heapDelta.
62
- if (systemFree > 0) {
63
- sysFreeStart = systemFree
64
- sysFreeFloor = systemFree
65
- }
82
+ const retained = heapUsed - suiteBaseline
83
+ heapRetained += retained
84
+ suiteBaseline = heapUsed
85
+ if (systemFree > 0 && systemFree < suiteFreeFloor) suiteFreeFloor = systemFree
86
+ const sysUsed = suiteFreeStart > suiteFreeFloor ? suiteFreeStart - suiteFreeFloor : 0
87
+ return {retained, sysUsed}
66
88
  }
67
89
 
68
90
  function newSuite(name: string, flags: {skip?: boolean; only?: boolean; todo?: boolean}): Suite {
@@ -372,7 +394,7 @@ type TestEvent =
372
394
  | {e: 2; s: string; t: string; d: number}
373
395
  | {e: 3; s: string; t: string; d: number; m: string}
374
396
  | {e: 4; s: string; t: string}
375
- | {e: 5; s: string}
397
+ | {e: 5; s: string; hr?: number; su?: number}
376
398
  | {
377
399
  e: 6
378
400
  p: number
@@ -382,6 +404,7 @@ type TestEvent =
382
404
  d: number
383
405
  hb?: number
384
406
  ha?: number
407
+ hr?: number
385
408
  su?: number
386
409
  sf?: number
387
410
  tb?: number
@@ -425,6 +448,7 @@ function emitHeap(): void {
425
448
  if (mem.systemFree > 0) {
426
449
  evt.f = mem.systemFree
427
450
  if (sysFreeFloor === 0 || mem.systemFree < sysFreeFloor) sysFreeFloor = mem.systemFree
451
+ if (suiteFreeFloor === 0 || mem.systemFree < suiteFreeFloor) suiteFreeFloor = mem.systemFree
428
452
  }
429
453
  if (mem.systemMinFree > 0) evt.mf = mem.systemMinFree
430
454
  emit(evt)
@@ -445,9 +469,10 @@ async function run(): Promise<void> {
445
469
  // folded into the baseline automatically.
446
470
  gc()
447
471
  // Destructure to primitives so the live memoryUsage() object isn't held
448
- // while heapBaseline is captured (it would otherwise be counted in it).
472
+ // while the baseline is captured (it would otherwise be counted in it).
449
473
  const {heapUsed: startHeap, systemFree: startFree} = memoryUsage()
450
- heapBaseline = startHeap
474
+ heapRetained = 0
475
+ suiteBaseline = startHeap
451
476
  if (startFree > 0) {
452
477
  sysFreeStart = startFree
453
478
  sysFreeFloor = startFree
@@ -459,7 +484,10 @@ async function run(): Promise<void> {
459
484
  const hasOnly = suites.some((s) => s.only || s.tests.some((t) => t.only))
460
485
 
461
486
  for (const suite of suites) {
487
+ // Open this suite's own peak window on the post-gc sample emitHeap takes.
488
+ suiteFreeFloor = 0
462
489
  emitHeap()
490
+ suiteFreeStart = suiteFreeFloor
463
491
  emit({e: 1, s: suite.name, n: suite.tests.length})
464
492
 
465
493
  if (suite.skip) {
@@ -527,11 +555,10 @@ async function run(): Promise<void> {
527
555
  continue
528
556
  }
529
557
  // Recapture the baseline now that beforeAll has fully resolved and
530
- // its closure frame is eligible for collection. Multi-suite files
531
- // let the last successful beforeAll set the baseline fine because
532
- // every earlier suite's test allocations have already been bounded
533
- // by the previous (stricter) baseline.
534
- await captureHeapBaseline()
558
+ // its closure frame is eligible for collection. Only this suite is
559
+ // measured against it: earlier suites were already closed out into
560
+ // heapRetained, so their allocations survive the recapture.
561
+ await captureSuiteBaseline()
535
562
  }
536
563
 
537
564
  for (const t of suite.tests) {
@@ -600,7 +627,10 @@ async function run(): Promise<void> {
600
627
  }
601
628
  }
602
629
 
603
- emit({e: 5, s: suite.name})
630
+ const closed = closeSuite()
631
+ const endEvt: TestEvent = {e: 5, s: suite.name, hr: closed.retained}
632
+ if (closed.sysUsed > 0) endEvt.su = closed.sysUsed
633
+ emit(endEvt)
604
634
  }
605
635
 
606
636
  gc()
@@ -610,14 +640,20 @@ async function run(): Promise<void> {
610
640
  if (endFree > 0 && (sysFreeFloor === 0 || endFree < sysFreeFloor)) {
611
641
  sysFreeFloor = endFree
612
642
  }
643
+ // Fold in whatever ran outside a suite's own accounting: a file with no
644
+ // suites at all, and the skip/todo bookkeeping between them.
645
+ heapRetained += heapAfter - suiteBaseline
613
646
  const timersAfter = activeTimers()
614
647
  const pendingAfter = pendingHttpCount()
615
648
 
616
649
  // Per-file system-heap figures. sysFreeFloor is the lowest free heap we
617
650
  // sampled (post-gc) this run; sysUsed is how far free heap fell from the
618
- // baseline to that low. They sum back to the baseline free, so "peak" and
619
- // "min free" read as one story. Samples are taken between tests, so a
620
- // transient peak inside a single test can dip below what sysFreeFloor saw.
651
+ // run's start to that low. They sum back to the starting free, so "peak"
652
+ // and "min free" read as one story. Measured from the start of the run and
653
+ // never rebaselined: unlike retention, a peak wants warmup counted, since
654
+ // memory a beforeAll takes is memory the file needed at once. Samples are
655
+ // taken between tests, so a transient peak inside a single test can dip
656
+ // below what sysFreeFloor saw.
621
657
  const sysUsed = sysFreeStart > sysFreeFloor ? sysFreeStart - sysFreeFloor : 0
622
658
 
623
659
  const doneEvt: TestEvent = {
@@ -627,8 +663,9 @@ async function run(): Promise<void> {
627
663
  k: skipped,
628
664
  o: todo,
629
665
  d: elapsedMs(startTime),
630
- hb: heapBaseline,
666
+ hb: startHeap,
631
667
  ha: heapAfter,
668
+ hr: heapRetained,
632
669
  tb: timersBefore,
633
670
  ta: timersAfter,
634
671
  pb: pendingBefore,
package/src/mik_repl.cpp CHANGED
@@ -39,10 +39,11 @@ static MIKReplTransport* repl_transport = nullptr;
39
39
  static uint8_t ready_buf[384];
40
40
  static size_t ready_len = 0;
41
41
 
42
- /* Memory left for the app, captured once when the first runtime attaches: that
43
- * happens before the entry is evaluated, so these describe the floor the app is
44
- * handed rather than whatever is left whenever a client happens to connect.
45
- * Reported on MSG_READY so `mikro profile` is a read, not a deploy. */
42
+ /* Memory left for the app, captured once from the boot path (see
43
+ * MIK_CaptureBootMemory) before the entry is evaluated, so these describe the
44
+ * floor the app is handed rather than whatever is left whenever a client
45
+ * happens to connect. Reported on MSG_READY so reading them is a read, not a
46
+ * deploy. */
46
47
  static bool boot_mem_captured = false;
47
48
  static uint32_t boot_heap_free = 0;
48
49
  static uint32_t boot_system_free = 0;
@@ -1011,23 +1012,26 @@ void MIK_ProtocolOpen(MIKReplTransport* transport) {
1011
1012
 
1012
1013
  }
1013
1014
 
1015
+ void MIK_CaptureBootMemory(MIKRuntime* mik_rt) {
1016
+ if (!mik_rt || boot_mem_captured) return;
1017
+ JSMemoryUsage mem;
1018
+ JS_ComputeMemoryUsage(JS_GetRuntime(MIK_GetJSContext(mik_rt)), &mem);
1019
+ boot_heap_free = mem.malloc_limit > (int64_t)mem.malloc_size
1020
+ ? (uint32_t)(mem.malloc_limit - (int64_t)mem.malloc_size)
1021
+ : 0;
1022
+ boot_system_free = (uint32_t)MIK_GetPlatform()->get_free_system_mem();
1023
+ boot_mem_reserved = mik_rt->config.mem_reserved;
1024
+ boot_mem_captured = true;
1025
+ }
1026
+
1014
1027
  void MIK_ProtocolAttach(MIKRuntime* mik_rt) {
1015
1028
  if (!mik_rt) return;
1016
1029
  repl_ctx = MIK_GetJSContext(mik_rt);
1017
1030
  repl_mik_rt = mik_rt;
1018
- /* First attach only. In test mode a fresh runtime attaches per file, and
1019
- * those would otherwise overwrite the boot floor with per-test figures. */
1020
- if (!boot_mem_captured) {
1021
- JSMemoryUsage mem;
1022
- JS_ComputeMemoryUsage(JS_GetRuntime(repl_ctx), &mem);
1023
- boot_heap_free =
1024
- mem.malloc_limit > (int64_t)mem.malloc_size
1025
- ? (uint32_t)(mem.malloc_limit - (int64_t)mem.malloc_size)
1026
- : 0;
1027
- boot_system_free = (uint32_t)MIK_GetPlatform()->get_free_system_mem();
1028
- boot_mem_reserved = mik_rt->config.mem_reserved;
1029
- boot_mem_captured = true;
1030
- }
1031
+ /* Fallback for boot paths that don't capture explicitly. First attach
1032
+ * only: in test mode a fresh runtime attaches per file, and those would
1033
+ * otherwise overwrite the boot floor with per-test figures. */
1034
+ MIK_CaptureBootMemory(mik_rt);
1031
1035
  }
1032
1036
 
1033
1037
  void MIK_ProtocolDetach(void) {