@mikrojs/native 0.19.0 → 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.
- package/CMakeLists.txt +3 -1
- package/include/mikrojs/mikrojs.h +21 -0
- package/include/mikrojs/ota_policy.h +17 -0
- package/include/mikrojs/platform.h +3 -0
- package/include/mikrojs/private.h +40 -0
- package/include/mikrojs/utils.h +5 -0
- package/package.json +5 -4
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/internal.d.ts +5 -5
- package/runtime/ota/ota.ts +2 -10
- package/runtime/ota/types.ts +58 -89
- package/runtime/test/test.ts +68 -31
- package/runtime/watchdog/types.ts +9 -0
- package/runtime/watchdog/watchdog.ts +7 -0
- package/src/eval_bytecode.cpp +1 -0
- package/src/mik_app_config.cpp +38 -0
- package/src/mik_console.cpp +5 -0
- package/src/mik_ota_policy.cpp +34 -0
- package/src/mik_repl.cpp +44 -24
- package/src/mik_watchdog.cpp +170 -0
- package/src/mikrojs.cpp +36 -1
- package/src/utils.cpp +21 -0
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,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Watchdog {
|
|
2
|
+
/** Report progress to the feed watchdog. Call it where real work completed,
|
|
3
|
+
* not from a bare timer. A no-op when `watchdog.feed` is not configured. */
|
|
4
|
+
feed(): void
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Progress reporting for the `feed` watchdog. The limits themselves are
|
|
8
|
+
* set in `mikro.config.ts` under `watchdog`, not from code. */
|
|
9
|
+
export declare const watchdog: Watchdog
|
package/src/eval_bytecode.cpp
CHANGED
package/src/mik_app_config.cpp
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#include "mikrojs/mikrojs.h"
|
|
2
2
|
|
|
3
|
+
#include <climits>
|
|
3
4
|
#include <cstdio>
|
|
4
5
|
#include <cstdlib>
|
|
5
6
|
#include <cstring>
|
|
@@ -24,6 +25,9 @@ void MIK_DefaultConfig(MIKConfig* config) {
|
|
|
24
25
|
config->log_dir[0] = '\0';
|
|
25
26
|
config->log_max_size = 64 * 1024;
|
|
26
27
|
config->log_flush = MIK_LOG_FLUSH_ERROR;
|
|
28
|
+
config->blocking_timeout_ms = MIK_WATCHDOG_BLOCKING_DEFAULT_MS;
|
|
29
|
+
config->feed_timeout_ms = 0;
|
|
30
|
+
config->awake_timeout_ms = 0;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
/* Minimal JSON parser for config file — avoids cJSON dependency.
|
|
@@ -60,6 +64,34 @@ static bool mik__json_get_number(const char* json, const char* key, double* out)
|
|
|
60
64
|
return true;
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
static bool mik__json_is_false(const char* json, const char* key) {
|
|
68
|
+
char pattern[128];
|
|
69
|
+
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
|
|
70
|
+
const char* p = strstr(json, pattern);
|
|
71
|
+
if (!p) return false;
|
|
72
|
+
p += strlen(pattern);
|
|
73
|
+
while (*p == ' ' || *p == '\t' || *p == ':') p++;
|
|
74
|
+
return strncmp(p, "false", 5) == 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Watchdog budget in ms. `false` and 0 disable; other values below 1 s
|
|
78
|
+
* clamp up with a warning, since a smaller budget fires on ordinary work. */
|
|
79
|
+
static int mik__json_get_watchdog_ms(const MIKPlatform* platform, const char* json,
|
|
80
|
+
const char* key, int current) {
|
|
81
|
+
if (mik__json_is_false(json, key)) return 0;
|
|
82
|
+
double num_val;
|
|
83
|
+
if (!mik__json_get_number(json, key, &num_val)) return current;
|
|
84
|
+
if (num_val == 0) return 0;
|
|
85
|
+
if (num_val < 1000) {
|
|
86
|
+
platform->log(MIK_LOG_WARN, TAG, "%s below 1000 ms (%ld); clamping to 1000", key,
|
|
87
|
+
(long)num_val);
|
|
88
|
+
return 1000;
|
|
89
|
+
}
|
|
90
|
+
/* Past INT_MAX the cast would wrap negative and silently disable it. */
|
|
91
|
+
if (num_val > INT_MAX) return INT_MAX;
|
|
92
|
+
return (int)num_val;
|
|
93
|
+
}
|
|
94
|
+
|
|
63
95
|
/* Read a JSON file into a malloc'd buffer. Caller must free(). Returns NULL on failure. */
|
|
64
96
|
static char* mik__read_json_file(const char* filepath, struct stat* st) {
|
|
65
97
|
if (stat(filepath, st) != 0) return nullptr;
|
|
@@ -333,6 +365,12 @@ int MIK_LoadConfig(const char* base_path, MIKConfig* config) {
|
|
|
333
365
|
if (mik__json_get_number(buf, "onPanic.duration", &num_val)) {
|
|
334
366
|
config->panic_sleep_duration_ms = (int)num_val;
|
|
335
367
|
}
|
|
368
|
+
config->blocking_timeout_ms = mik__json_get_watchdog_ms(
|
|
369
|
+
platform, buf, "watchdog.blocking", config->blocking_timeout_ms);
|
|
370
|
+
config->feed_timeout_ms = mik__json_get_watchdog_ms(platform, buf, "watchdog.feed",
|
|
371
|
+
config->feed_timeout_ms);
|
|
372
|
+
config->awake_timeout_ms = mik__json_get_watchdog_ms(
|
|
373
|
+
platform, buf, "watchdog.awake", config->awake_timeout_ms);
|
|
336
374
|
if (mik__json_get_number(buf, "stackSize", &num_val)) {
|
|
337
375
|
config->stack_size = (size_t)num_val;
|
|
338
376
|
}
|
package/src/mik_console.cpp
CHANGED
|
@@ -263,6 +263,11 @@ bool mik__report_uncaught(JSContext* ctx, JSValue exc, bool in_promise) {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
/* A blocking-watchdog timeout reaches here either through mik_dump_error
|
|
267
|
+
* (which already reported it; this is then a no-op) or as an unhandled
|
|
268
|
+
* rejection when the interrupted turn was a module evaluation. */
|
|
269
|
+
mik__watchdog_report_blocking(MIK_GetRuntime(ctx));
|
|
270
|
+
|
|
266
271
|
/* Fixed-size stack buffer so this function never allocates from the
|
|
267
272
|
* C++ heap. Previously we used std::string + mik_inspect here; both
|
|
268
273
|
* can throw std::bad_alloc under memory pressure, and with
|
package/src/mik_ota_policy.cpp
CHANGED
|
@@ -116,6 +116,40 @@ void MIKOtaStore::SetInFlight(bool in_flight) {
|
|
|
116
116
|
env_->kv_set_i32(env_->opaque, "ota.inflight", in_flight ? 1 : 0);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
bool MIKOtaStore::GetDecline(MIKOtaDeclineRecord* out) const {
|
|
120
|
+
if (!env_ || !env_->kv_get_str) return false;
|
|
121
|
+
if (!env_->kv_get_str(env_->opaque, "ota.declined", out->checksum, sizeof(out->checksum)) ||
|
|
122
|
+
out->checksum[0] == '\0') {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (!env_->kv_get_str(env_->opaque, "ota.declReason", out->reason, sizeof(out->reason)) ||
|
|
126
|
+
out->reason[0] == '\0') {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (!env_->kv_get_str(env_->opaque, "ota.declDetail", out->detail, sizeof(out->detail))) {
|
|
130
|
+
out->detail[0] = '\0';
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
void MIKOtaStore::SetDecline(const MIKOtaDeclineRecord& record) {
|
|
136
|
+
if (!env_ || !env_->kv_set_str) return;
|
|
137
|
+
env_->kv_set_str(env_->opaque, "ota.declined", record.checksum);
|
|
138
|
+
env_->kv_set_str(env_->opaque, "ota.declReason", record.reason);
|
|
139
|
+
if (record.detail[0]) {
|
|
140
|
+
env_->kv_set_str(env_->opaque, "ota.declDetail", record.detail);
|
|
141
|
+
} else if (env_->kv_remove) {
|
|
142
|
+
env_->kv_remove(env_->opaque, "ota.declDetail");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
void MIKOtaStore::ClearDecline() {
|
|
147
|
+
if (!env_ || !env_->kv_remove) return;
|
|
148
|
+
env_->kv_remove(env_->opaque, "ota.declined");
|
|
149
|
+
env_->kv_remove(env_->opaque, "ota.declReason");
|
|
150
|
+
env_->kv_remove(env_->opaque, "ota.declDetail");
|
|
151
|
+
}
|
|
152
|
+
|
|
119
153
|
// ── Offer parsing ────────────────────────────────────────────────────────────
|
|
120
154
|
|
|
121
155
|
bool mik__ota_parse_offer(const char* url, const char* checksum, int64_t size, bool allow_insecure,
|
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;
|
|
@@ -119,8 +120,8 @@ void mik__repl_proto_send_output(uint8_t msg_type, const void* data, size_t len)
|
|
|
119
120
|
* Returns false on transport EOF/error, when MIK_ProtocolExit() is
|
|
120
121
|
* signalled from inside the pump (the supervisor's mechanism for ending
|
|
121
122
|
* a test-file's serve loop after __testFileDone fires), or when MIK_Loop
|
|
122
|
-
* reports the attached runtime has halted
|
|
123
|
-
*
|
|
123
|
+
* reports the attached runtime has halted without a panic grace window
|
|
124
|
+
* (e.g. an unhandled rejection in a test that crashed mid-execution). */
|
|
124
125
|
bool mik__proto_read_exact(MIKReplTransport* transport, void* buf, size_t n) {
|
|
125
126
|
const MIKPlatform* platform = MIK_GetPlatform();
|
|
126
127
|
uint8_t* p = static_cast<uint8_t*>(buf);
|
|
@@ -133,11 +134,10 @@ bool mik__proto_read_exact(MIKReplTransport* transport, void* buf, size_t n) {
|
|
|
133
134
|
return false;
|
|
134
135
|
} else {
|
|
135
136
|
if (repl_mik_rt && !repl_paused) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
* next runtime — or restart, in non-supervisor mode. */
|
|
137
|
+
/* A panic arms restart_at_us and MIK_Loop takes the action
|
|
138
|
+
* itself at the deadline, so keep serving (--recover). Only
|
|
139
|
+
* a plain stop (test supervisor) ends the serve loop. */
|
|
140
|
+
if (MIK_Loop(repl_mik_rt) != 0 && repl_mik_rt->restart_at_us == 0) {
|
|
141
141
|
s_exit_serve_loop = true;
|
|
142
142
|
return false;
|
|
143
143
|
}
|
|
@@ -611,6 +611,7 @@ static bool handle_directive_impl(JSContext* ctx, const char* line, std::string&
|
|
|
611
611
|
out.append("App is already paused\n");
|
|
612
612
|
} else {
|
|
613
613
|
repl_paused = true;
|
|
614
|
+
mik__watchdog_pause_begin(repl_mik_rt);
|
|
614
615
|
out.append("App paused (timers, callbacks suspended). Use /resume to continue.\n");
|
|
615
616
|
}
|
|
616
617
|
return true;
|
|
@@ -621,6 +622,7 @@ static bool handle_directive_impl(JSContext* ctx, const char* line, std::string&
|
|
|
621
622
|
out.append("App is not paused\n");
|
|
622
623
|
} else {
|
|
623
624
|
repl_paused = false;
|
|
625
|
+
mik__watchdog_pause_end(repl_mik_rt);
|
|
624
626
|
out.append("App resumed\n");
|
|
625
627
|
}
|
|
626
628
|
return true;
|
|
@@ -852,6 +854,13 @@ bool mik__repl_is_paused(void) {
|
|
|
852
854
|
}
|
|
853
855
|
|
|
854
856
|
void mik__repl_set_paused(bool paused) {
|
|
857
|
+
if (paused != repl_paused) {
|
|
858
|
+
if (paused) {
|
|
859
|
+
mik__watchdog_pause_begin(repl_mik_rt);
|
|
860
|
+
} else {
|
|
861
|
+
mik__watchdog_pause_end(repl_mik_rt);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
855
864
|
repl_paused = paused;
|
|
856
865
|
}
|
|
857
866
|
|
|
@@ -1003,23 +1012,26 @@ void MIK_ProtocolOpen(MIKReplTransport* transport) {
|
|
|
1003
1012
|
|
|
1004
1013
|
}
|
|
1005
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
|
+
|
|
1006
1027
|
void MIK_ProtocolAttach(MIKRuntime* mik_rt) {
|
|
1007
1028
|
if (!mik_rt) return;
|
|
1008
1029
|
repl_ctx = MIK_GetJSContext(mik_rt);
|
|
1009
1030
|
repl_mik_rt = mik_rt;
|
|
1010
|
-
/*
|
|
1011
|
-
*
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
JS_ComputeMemoryUsage(JS_GetRuntime(repl_ctx), &mem);
|
|
1015
|
-
boot_heap_free =
|
|
1016
|
-
mem.malloc_limit > (int64_t)mem.malloc_size
|
|
1017
|
-
? (uint32_t)(mem.malloc_limit - (int64_t)mem.malloc_size)
|
|
1018
|
-
: 0;
|
|
1019
|
-
boot_system_free = (uint32_t)MIK_GetPlatform()->get_free_system_mem();
|
|
1020
|
-
boot_mem_reserved = mik_rt->config.mem_reserved;
|
|
1021
|
-
boot_mem_captured = true;
|
|
1022
|
-
}
|
|
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);
|
|
1023
1035
|
}
|
|
1024
1036
|
|
|
1025
1037
|
void MIK_ProtocolDetach(void) {
|
|
@@ -1035,6 +1047,9 @@ void MIK_ProtocolClose(void) {
|
|
|
1035
1047
|
}
|
|
1036
1048
|
repl_active = false;
|
|
1037
1049
|
repl_protocol_mode = false;
|
|
1050
|
+
if (repl_paused) {
|
|
1051
|
+
mik__watchdog_pause_end(repl_mik_rt);
|
|
1052
|
+
}
|
|
1038
1053
|
repl_paused = false;
|
|
1039
1054
|
repl_transport = nullptr;
|
|
1040
1055
|
repl_ctx = nullptr;
|
|
@@ -1082,6 +1097,7 @@ void MIK_ProtocolServeLoop(void) {
|
|
|
1082
1097
|
|
|
1083
1098
|
repl_evaluating = true;
|
|
1084
1099
|
repl_async_skipped = false;
|
|
1100
|
+
mik__blocking_begin(repl_mik_rt);
|
|
1085
1101
|
JSValue result = repl_eval_and_pump(ctx, code.c_str(), code.size());
|
|
1086
1102
|
repl_evaluating = false;
|
|
1087
1103
|
|
|
@@ -1094,6 +1110,10 @@ void MIK_ProtocolServeLoop(void) {
|
|
|
1094
1110
|
if (JS_IsException(result)) {
|
|
1095
1111
|
JSValue exc = JS_GetException(ctx);
|
|
1096
1112
|
|
|
1113
|
+
/* A synchronous throw never reaches mik_dump_error, so
|
|
1114
|
+
* consume the blocking-timeout flag here. */
|
|
1115
|
+
mik__watchdog_report_blocking(repl_mik_rt);
|
|
1116
|
+
|
|
1097
1117
|
/* Format the error */
|
|
1098
1118
|
std::string msg = "Uncaught ";
|
|
1099
1119
|
if (JS_IsObject(exc)) {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#include <quickjs.h>
|
|
2
|
+
|
|
3
|
+
#include "mikrojs/mikrojs.h"
|
|
4
|
+
#include "mikrojs/platform.h"
|
|
5
|
+
#include "mikrojs/private.h"
|
|
6
|
+
#include "mikrojs/utils.h"
|
|
7
|
+
|
|
8
|
+
/* ── Watchdog: blocking deadline, feed deadline, awake budget ─────────
|
|
9
|
+
*
|
|
10
|
+
* All three measure wall time via get_boot_us(). The blocking deadline is
|
|
11
|
+
* polled from the QuickJS interrupt handler (every 10k branches/calls);
|
|
12
|
+
* feed and awake are checked once per MIK_Loop pass. State is per-runtime,
|
|
13
|
+
* so the Node addon's parallel runtimes need no atomics. */
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
void mik__blocking_begin(MIKRuntime* mik_rt) {
|
|
17
|
+
if (!mik_rt) return;
|
|
18
|
+
mik_rt->blocking_start_us = 0;
|
|
19
|
+
mik_rt->blocking_armed = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
int mik__watchdog_interrupt(JSRuntime* rt, void* opaque) {
|
|
23
|
+
(void)rt;
|
|
24
|
+
MIKRuntime* mik_rt = static_cast<MIKRuntime*>(opaque);
|
|
25
|
+
int budget_ms = mik_rt->config.blocking_timeout_ms;
|
|
26
|
+
if (budget_ms <= 0 || !mik_rt->blocking_armed) {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
int64_t now = MIK_GetPlatform()->get_boot_us();
|
|
30
|
+
int64_t start = mik_rt->blocking_start_us;
|
|
31
|
+
/* now < start: the boot clock reset (deep sleep, test platform swap). */
|
|
32
|
+
if (start == 0 || now < start) {
|
|
33
|
+
mik_rt->blocking_start_us = now;
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
if (now - start >= (int64_t)budget_ms * 1000) {
|
|
37
|
+
/* One-shot: stay off while the error unwinds so finally blocks
|
|
38
|
+
* and error-handler property reads are not interrupted too. */
|
|
39
|
+
mik_rt->blocking_armed = false;
|
|
40
|
+
mik_rt->blocking_tripped = true;
|
|
41
|
+
/* The app could not feed while it was blocked; give it a fresh
|
|
42
|
+
* window so a surviving session (REPL eval) is not hit twice. */
|
|
43
|
+
mik_rt->feed_last_us = now;
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
bool mik__watchdog_report_blocking(MIKRuntime* mik_rt) {
|
|
50
|
+
if (!mik_rt || !mik_rt->blocking_tripped) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
mik_rt->blocking_tripped = false;
|
|
54
|
+
mik__print_error_line("[watchdog] WATCHDOG TRIGGERED: event loop blocking time exceeded "
|
|
55
|
+
"configured limit of %d ms",
|
|
56
|
+
mik_rt->config.blocking_timeout_ms);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
void mik__watchdog_arm(MIKRuntime* mik_rt) {
|
|
61
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
62
|
+
const MIKConfig* cfg = &mik_rt->config;
|
|
63
|
+
mik__blocking_begin(mik_rt);
|
|
64
|
+
mik_rt->feed_armed = cfg->feed_timeout_ms > 0;
|
|
65
|
+
mik_rt->feed_last_us = platform->get_boot_us();
|
|
66
|
+
mik_rt->awake_armed = cfg->awake_timeout_ms > 0;
|
|
67
|
+
mik_rt->awake_pause_offset_us = 0;
|
|
68
|
+
mik_rt->pause_start_us = 0;
|
|
69
|
+
if (mik_rt->awake_armed && cfg->panic_mode != MIK_PANIC_DEEP_SLEEP) {
|
|
70
|
+
/* Always visible: platform->log is silent on device by default. */
|
|
71
|
+
mik__print_error_line("[watchdog] awake limit of %d ms without onPanic.mode 'deepSleep': "
|
|
72
|
+
"the device restarts every %d ms",
|
|
73
|
+
cfg->awake_timeout_ms, cfg->awake_timeout_ms);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
void mik__watchdog_check(MIKRuntime* mik_rt) {
|
|
78
|
+
if (mik_rt->stop_requested || (!mik_rt->feed_armed && !mik_rt->awake_armed)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
/* A pending exception (a blocking timeout, or any uncaught throw from the
|
|
82
|
+
* entry) is about to be reported and stop the runtime. Firing feed or
|
|
83
|
+
* awake on top of it would replace that exception and lose its trace. */
|
|
84
|
+
if (JS_HasException(mik_rt->ctx)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
88
|
+
int64_t now = platform->get_boot_us();
|
|
89
|
+
|
|
90
|
+
if (mik_rt->awake_armed) {
|
|
91
|
+
int64_t elapsed_us = now - mik_rt->awake_pause_offset_us;
|
|
92
|
+
if (elapsed_us >= (int64_t)mik_rt->config.awake_timeout_ms * 1000) {
|
|
93
|
+
mik_rt->awake_armed = false;
|
|
94
|
+
mik__print_error_line("[watchdog] WATCHDOG TRIGGERED: awake time exceeded configured "
|
|
95
|
+
"limit of %d ms",
|
|
96
|
+
mik_rt->config.awake_timeout_ms);
|
|
97
|
+
/* No JS error: nothing to point a trace at, and this must not
|
|
98
|
+
* reach the OTA trial error handler (a slow link is not a bug). */
|
|
99
|
+
MIK_Stop(mik_rt);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (mik_rt->feed_armed) {
|
|
105
|
+
if (now < mik_rt->feed_last_us) {
|
|
106
|
+
mik_rt->feed_last_us = now;
|
|
107
|
+
} else if (now - mik_rt->feed_last_us >= (int64_t)mik_rt->config.feed_timeout_ms * 1000) {
|
|
108
|
+
mik_rt->feed_armed = false;
|
|
109
|
+
mik__print_error_line("[watchdog] WATCHDOG TRIGGERED: time since last feed() exceeded "
|
|
110
|
+
"configured limit of %d ms",
|
|
111
|
+
mik_rt->config.feed_timeout_ms);
|
|
112
|
+
/* No JS frame is active here, so uncatchable marking is moot: the
|
|
113
|
+
* JS_HasException branch in MIK_Loop routes this before any user
|
|
114
|
+
* code runs. */
|
|
115
|
+
JS_ThrowInternalError(mik_rt->ctx,
|
|
116
|
+
"watchdog: time since last feed() exceeded configured limit "
|
|
117
|
+
"of %d ms",
|
|
118
|
+
mik_rt->config.feed_timeout_ms);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
void mik__watchdog_wake(MIKRuntime* mik_rt) {
|
|
124
|
+
if (!mik_rt) return;
|
|
125
|
+
mik__blocking_begin(mik_rt);
|
|
126
|
+
/* The app could not feed while asleep; it gets a fresh window, as after
|
|
127
|
+
* a pause. The awake clock keeps counting: light sleep is still uptime. */
|
|
128
|
+
mik_rt->feed_last_us = MIK_GetPlatform()->get_boot_us();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
void mik__watchdog_pause_begin(MIKRuntime* mik_rt) {
|
|
132
|
+
if (!mik_rt || mik_rt->pause_start_us != 0) return;
|
|
133
|
+
mik_rt->pause_start_us = MIK_GetPlatform()->get_boot_us();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
void mik__watchdog_pause_end(MIKRuntime* mik_rt) {
|
|
137
|
+
if (!mik_rt || mik_rt->pause_start_us == 0) return;
|
|
138
|
+
int64_t now = MIK_GetPlatform()->get_boot_us();
|
|
139
|
+
if (now > mik_rt->pause_start_us) {
|
|
140
|
+
mik_rt->awake_pause_offset_us += now - mik_rt->pause_start_us;
|
|
141
|
+
}
|
|
142
|
+
mik_rt->pause_start_us = 0;
|
|
143
|
+
/* The app could not feed while suspended; it gets a fresh budget. */
|
|
144
|
+
mik_rt->feed_last_us = now;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ── native:mikro/watchdog ──────────────────────────────────────────── */
|
|
148
|
+
|
|
149
|
+
static JSValue mik__watchdog_feed(JSContext* ctx, JSValueConst this_val, int argc,
|
|
150
|
+
JSValueConst* argv) {
|
|
151
|
+
(void)this_val;
|
|
152
|
+
(void)argc;
|
|
153
|
+
(void)argv;
|
|
154
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
155
|
+
if (mik_rt && mik_rt->feed_armed) {
|
|
156
|
+
mik_rt->feed_last_us = MIK_GetPlatform()->get_boot_us();
|
|
157
|
+
}
|
|
158
|
+
return JS_UNDEFINED;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static int mik__watchdog_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
162
|
+
JS_SetModuleExport(ctx, m, "feed", JS_NewCFunction(ctx, mik__watchdog_feed, "feed", 0));
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
void mik__watchdog_init(JSContext* ctx) {
|
|
167
|
+
JSModuleDef* m = JS_NewCModule(ctx, "native:mikro/watchdog", mik__watchdog_module_init);
|
|
168
|
+
if (!m) return;
|
|
169
|
+
JS_AddModuleExport(ctx, m, "feed");
|
|
170
|
+
}
|