@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
|
|
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
|
|
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
|
|
98
|
-
"@mikrojs/schema": "0.20.0
|
|
97
|
+
"@mikrojs/registry": "0.20.0",
|
|
98
|
+
"@mikrojs/schema": "0.20.0"
|
|
99
99
|
},
|
|
100
100
|
"engines": {
|
|
101
101
|
"node": ">=24.0.0"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/runtime/test/test.ts
CHANGED
|
@@ -29,40 +29,62 @@ interface Suite {
|
|
|
29
29
|
|
|
30
30
|
const suites: Suite[] = []
|
|
31
31
|
let currentSuite: Suite | null = null
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
|
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
|
|
64
|
+
* and the suite would close below its baseline (negative delta).
|
|
53
65
|
*/
|
|
54
|
-
async function
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
472
|
+
// while the baseline is captured (it would otherwise be counted in it).
|
|
449
473
|
const {heapUsed: startHeap, systemFree: startFree} = memoryUsage()
|
|
450
|
-
|
|
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.
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
619
|
-
// "min free" read as one story.
|
|
620
|
-
//
|
|
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:
|
|
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
|
|
43
|
-
*
|
|
44
|
-
* handed rather than whatever is left whenever a client
|
|
45
|
-
* Reported on MSG_READY so
|
|
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
|
-
/*
|
|
1019
|
-
*
|
|
1020
|
-
|
|
1021
|
-
|
|
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) {
|