@mikrojs/native 0.6.0-pr-70.g7bdc6dd → 0.6.0-pr-70.gd2fdc0d

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.
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
 
3
+ #include <stdbool.h>
3
4
  #include <stddef.h>
4
5
  #include <stdlib.h>
5
6
 
@@ -9,3 +10,19 @@ void* mik__mallocz(size_t size);
9
10
  void* mik__calloc(size_t count, size_t size);
10
11
  void mik__free(void* ptr);
11
12
  void* mik__realloc(void* ptr, size_t size);
13
+
14
+ /* QuickJS-heap allocators. Backed by libc malloc by default; routed to
15
+ * PSRAM via the platform's malloc_psram/calloc_psram/realloc_psram hooks
16
+ * when mik__set_quickjs_heap_psram(true) has been called and the platform
17
+ * supports PSRAM. Free is unchanged: PSRAM pointers from heap_caps_malloc
18
+ * free cleanly via standard free() on ESP-IDF. */
19
+ void* mik__js_malloc(size_t size);
20
+ void* mik__js_calloc(size_t count, size_t size);
21
+ void* mik__js_realloc(void* ptr, size_t size);
22
+
23
+ /* Toggle QuickJS heap allocations to PSRAM (no-op on platforms without
24
+ * PSRAM). Set once before JS_NewRuntime2; do not change mid-run since
25
+ * mik__js_realloc relies on the flag staying consistent with the heap
26
+ * the original pointer came from. */
27
+ void mik__set_quickjs_heap_psram(bool enable);
28
+ bool mik__is_quickjs_heap_psram(void);
@@ -10,6 +10,17 @@ typedef struct MIKRuntime MIKRuntime;
10
10
  typedef struct MIKRunOptions {
11
11
  int mem_limit;
12
12
  size_t stack_size;
13
+ /* Allocate the QuickJS heap from PSRAM instead of internal SRAM on
14
+ * chips with both. Frees ~150-250 KB of contiguous internal SRAM for
15
+ * mbedTLS, WiFi/BLE, and DMA buffers, the dominant root cause of
16
+ * HTTPS handshake failures (error 0x7002) on apps with significant
17
+ * retained JS state. JS execution is somewhat slower (PSRAM has
18
+ * higher access latency than internal SRAM); for typical embedded
19
+ * IO-bound workloads this is invisible against networking and IO
20
+ * costs. No effect on hosts and chips without PSRAM. Defaults false
21
+ * for backward compatibility; the firmware bootstrap flips it on
22
+ * when CONFIG_MIKROJS_QUICKJS_HEAP_PSRAM is set. */
23
+ bool use_psram_heap;
13
24
  } MIKRunOptions;
14
25
 
15
26
  typedef struct MIKConfig {
@@ -18,11 +18,37 @@ typedef struct MIKPlatform {
18
18
  size_t (*get_free_system_mem)(void);
19
19
  size_t (*get_min_free_system_mem)(void); /* All-time low watermark */
20
20
  size_t (*get_total_system_mem)(void);
21
- /** Largest contiguous free block (for diagnosing heap fragmentation
21
+ /** Largest contiguous free block (for diagnosing heap fragmentation:
22
22
  * an allocation larger than this fails even if total free is bigger).
23
23
  * On platforms without a native largest-block API, implementations
24
24
  * may return the same value as get_free_system_mem. */
25
25
  size_t (*get_largest_free_system_mem)(void);
26
+ /** Free internal-SRAM bytes. On chips with PSRAM this is the subset of
27
+ * free heap that lives in fast on-chip RAM, which is also what mbedTLS
28
+ * handshake buffers, WiFi/BLE drivers, and DMA-capable buffers compete
29
+ * for. Distinct from get_free_system_mem on PSRAM-equipped chips, where
30
+ * combined heap is dominated by PSRAM and hides internal-SRAM pressure.
31
+ * On hosts and chips without PSRAM, may return the same value as
32
+ * get_free_system_mem (or 0 if unavailable). */
33
+ size_t (*get_free_internal_mem)(void);
34
+ /** Largest contiguous free block in internal SRAM. Allocations that
35
+ * must land in internal RAM (such as mbedTLS record buffers) fail when
36
+ * this drops below their size, even when get_largest_free_system_mem
37
+ * reports plenty of contiguous PSRAM. Same fallback rules as
38
+ * get_free_internal_mem. */
39
+ size_t (*get_largest_free_internal_mem)(void);
40
+ /** Allocate `size` bytes from PSRAM/external RAM. Returns NULL if
41
+ * PSRAM is unavailable (host platforms, chips without PSRAM, or
42
+ * PSRAM exhaustion); the caller falls back to the regular allocator.
43
+ * On ESP32 with CONFIG_SPIRAM=y, implemented via heap_caps_malloc
44
+ * with MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT. The returned pointer is
45
+ * freed via standard free(). */
46
+ void* (*malloc_psram)(size_t size);
47
+ /** PSRAM-targeted calloc. Same fallback rules as malloc_psram. */
48
+ void* (*calloc_psram)(size_t count, size_t size);
49
+ /** PSRAM-targeted realloc. Grows or moves the allocation while keeping
50
+ * it in PSRAM. Same fallback rules as malloc_psram. */
51
+ void* (*realloc_psram)(void* ptr, size_t size);
26
52
  bool (*get_fs_info)(const char* label, size_t* total, size_t* used);
27
53
  void (*log)(int level, const char* tag, const char* fmt, ...);
28
54
  int (*stdout_write)(const void* buf, size_t len);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikrojs/native",
3
- "version": "0.6.0-pr-70.g7bdc6dd",
3
+ "version": "0.6.0-pr-70.gd2fdc0d",
4
4
  "description": "Mikro.js C++ runtime library and Node.js native addon",
5
5
  "keywords": [
6
6
  "esp32",
@@ -78,7 +78,7 @@
78
78
  "cmake-js": "^8.0.0",
79
79
  "node-addon-api": "^8.7.0",
80
80
  "node-gyp-build": "^4.8.4",
81
- "@mikrojs/quickjs": "0.6.0-pr-70.g7bdc6dd"
81
+ "@mikrojs/quickjs": "0.6.0-pr-70.gd2fdc0d"
82
82
  },
83
83
  "devDependencies": {
84
84
  "@swc/core": "^1.15.30",
@@ -2,6 +2,7 @@ import {Observable} from 'mikrojs/observable'
2
2
  import {err, ok} from 'mikrojs/result'
3
3
  import {Ble as NativeBle} from 'native:ble'
4
4
 
5
+ import type {Subscriber} from '../observable/types.js'
5
6
  import type {Result} from '../result/types.js'
6
7
  import type {
7
8
  AdvertiseHandle,
@@ -142,15 +143,31 @@ function normalizeServices(services: Service[]) {
142
143
 
143
144
  const native = new NativeBle()
144
145
 
145
- /* Per-event multicast sources backed by a single native.on registration each.
146
- * See observable.md Module integration. */
147
- const _onConnect = Observable.withEmitters<ConnectionInfo>()
148
- const _onDisconnect = Observable.withEmitters<ConnectionInfo>()
149
- const _onMtu = Observable.withEmitters<MtuInfo>()
150
-
151
- native.on('connect', (info) => _onConnect.next(info as ConnectionInfo))
152
- native.on('disconnect', (info) => _onDisconnect.next(info as ConnectionInfo))
153
- native.on('mtu', (info) => _onMtu.next(info as MtuInfo))
146
+ /* Lazy-attach: native.on runs only after the first JS subscriber and is
147
+ * removed when the last one leaves. Same pattern as wifi.ts; see the note
148
+ * there for the mbedTLS internal-RAM rationale. */
149
+ function lazyEvent<T>(eventName: string): Observable<T> {
150
+ const subscribers: Subscriber<T>[] = []
151
+ function handler(value: unknown): void {
152
+ const snapshot = subscribers.slice()
153
+ for (const s of snapshot) {
154
+ if (!s.closed) s.next(value as T)
155
+ }
156
+ }
157
+ return new Observable<T>((sub) => {
158
+ subscribers.push(sub)
159
+ if (subscribers.length === 1) {
160
+ native.on(eventName, handler)
161
+ }
162
+ sub.addTeardown(() => {
163
+ const i = subscribers.indexOf(sub)
164
+ if (i >= 0) subscribers.splice(i, 1)
165
+ if (subscribers.length === 0) {
166
+ native.off(eventName, handler)
167
+ }
168
+ })
169
+ })
170
+ }
154
171
 
155
172
  const ble: Ble = {
156
173
  get name(): string {
@@ -231,9 +248,9 @@ const peripheral: Peripheral = {
231
248
  return ok(handle)
232
249
  },
233
250
 
234
- onConnect: _onConnect.observable,
235
- onDisconnect: _onDisconnect.observable,
236
- onMtu: _onMtu.observable,
251
+ onConnect: lazyEvent<ConnectionInfo>('connect'),
252
+ onDisconnect: lazyEvent<ConnectionInfo>('disconnect'),
253
+ onMtu: lazyEvent<MtuInfo>('mtu'),
237
254
  }
238
255
 
239
256
  export {ble, peripheral}
@@ -10,10 +10,23 @@ export interface MemoryUsage {
10
10
  /** Total system heap in bytes (0 on host) */
11
11
  systemTotal: number
12
12
  /** Largest contiguous free block in bytes (0 on host). Allocations
13
- * larger than this fail even when `systemFree` is bigger the gap
13
+ * larger than this fail even when `systemFree` is bigger; the gap
14
14
  * between `systemLargestFree` and `systemFree` measures heap
15
15
  * fragmentation. */
16
16
  systemLargestFree: number
17
+ /** Free internal-SRAM bytes (0 on host). On chips with PSRAM this is
18
+ * the subset of free heap that lives in fast on-chip RAM, which
19
+ * also holds mbedTLS handshake buffers, WiFi/BLE driver state, and
20
+ * DMA-capable buffers. On PSRAM-equipped chips `systemFree` is
21
+ * dominated by PSRAM and can hide internal-SRAM pressure: watch
22
+ * `internalFree` instead when diagnosing TLS handshake or driver
23
+ * allocation failures. On chips without PSRAM, equals `systemFree`. */
24
+ internalFree: number
25
+ /** Largest contiguous free block in internal SRAM (0 on host).
26
+ * Allocations that must land in internal RAM (such as mbedTLS record
27
+ * buffers, ~16 KB each) fail when this drops below their size, even
28
+ * when `systemLargestFree` reports plenty of contiguous PSRAM. */
29
+ internalLargestFree: number
17
30
  }
18
31
 
19
32
  export interface Uptime {
@@ -2,6 +2,7 @@ import {Observable} from 'mikrojs/observable'
2
2
  import {err, ok} from 'mikrojs/result'
3
3
  import {Wifi as NativeWifi} from 'native:wifi'
4
4
 
5
+ import type {Subscriber} from '../observable/types.js'
5
6
  import type {Result} from '../result/types.js'
6
7
  import type {
7
8
  ApStartOptions,
@@ -37,22 +38,35 @@ const StatusFromCode = new Map<number, WifiStatus>(
37
38
 
38
39
  const native = new NativeWifi()
39
40
 
40
- /* One Observable per event type, each backed by a single native.on
41
- * registration. Subscribers share that registration via the multicast
42
- * source from Observable.withEmitters() registering the listener once
43
- * keeps the C-level callback list at minimum size regardless of how many
44
- * JS subscribers are attached. */
45
- const _onConnect = Observable.withEmitters<WifiConnectionInfo>()
46
- const _onDisconnect = Observable.withEmitters<WifiDisconnectReason>()
47
- const _onRssiLow = Observable.withEmitters<number>()
48
- const _onStationConnect = Observable.withEmitters<ApStationInfo>()
49
- const _onStationDisconnect = Observable.withEmitters<ApStationInfo>()
50
-
51
- native.on('connect', (info) => _onConnect.next(info as WifiConnectionInfo))
52
- native.on('disconnect', (reason) => _onDisconnect.next(reason as WifiDisconnectReason))
53
- native.on('rssi-low', (rssi) => _onRssiLow.next(rssi as number))
54
- native.on('station-connect', (info) => _onStationConnect.next(info as ApStationInfo))
55
- native.on('station-disconnect', (info) => _onStationDisconnect.next(info as ApStationInfo))
41
+ /* Lazy-attach Observable: native.on is only called once a JS subscriber
42
+ * appears, and native.off runs when the last one unsubscribes. Code paths
43
+ * that import wifi but don't subscribe (e.g. a fetch-only flow) take the
44
+ * exact same internal-RAM path as before observables existed important
45
+ * because mbedTLS handshake needs ~16 KB contiguous internal SRAM and
46
+ * each eager closure pinned at module load chips into that headroom. */
47
+ function lazyEvent<T>(eventName: string): Observable<T> {
48
+ const subscribers: Subscriber<T>[] = []
49
+ function handler(value: unknown): void {
50
+ /* Snapshot so unsubscribes during dispatch don't shift indices. */
51
+ const snapshot = subscribers.slice()
52
+ for (const s of snapshot) {
53
+ if (!s.closed) s.next(value as T)
54
+ }
55
+ }
56
+ return new Observable<T>((sub) => {
57
+ subscribers.push(sub)
58
+ if (subscribers.length === 1) {
59
+ native.on(eventName, handler)
60
+ }
61
+ sub.addTeardown(() => {
62
+ const i = subscribers.indexOf(sub)
63
+ if (i >= 0) subscribers.splice(i, 1)
64
+ if (subscribers.length === 0) {
65
+ native.off(eventName, handler)
66
+ }
67
+ })
68
+ })
69
+ }
56
70
 
57
71
  const ap: WifiAp = {
58
72
  start(options: ApStartOptions): Result<void, WifiError> {
@@ -88,8 +102,8 @@ const ap: WifiAp = {
88
102
  native.apSetInactiveTimeout(seconds)
89
103
  },
90
104
 
91
- onStationConnect: _onStationConnect.observable,
92
- onStationDisconnect: _onStationDisconnect.observable,
105
+ onStationConnect: lazyEvent<ApStationInfo>('station-connect'),
106
+ onStationDisconnect: lazyEvent<ApStationInfo>('station-disconnect'),
93
107
  }
94
108
 
95
109
  const MAX_CONNECT_RETRIES = 5
@@ -147,9 +161,9 @@ const wifi: Wifi = {
147
161
  return ok(asyncResult.value as ScanResult[])
148
162
  },
149
163
 
150
- onConnect: _onConnect.observable,
151
- onDisconnect: _onDisconnect.observable,
152
- onRssiLow: _onRssiLow.observable,
164
+ onConnect: lazyEvent<WifiConnectionInfo>('connect'),
165
+ onDisconnect: lazyEvent<WifiDisconnectReason>('disconnect'),
166
+ onRssiLow: lazyEvent<number>('rssi-low'),
153
167
 
154
168
  get mac(): string {
155
169
  const result = native.mac()
package/src/mem.cpp CHANGED
@@ -5,6 +5,8 @@
5
5
  #include <stdlib.h>
6
6
  #include <string.h>
7
7
 
8
+ #include "mikrojs/platform.h"
9
+
8
10
  /*
9
11
  * QuickJS uses malloc_usable_size to track memory consumption against its
10
12
  * memory limit. ESP-IDF lacks a standard malloc_usable_size, and the
@@ -61,3 +63,65 @@ void* mik__realloc(void* ptr, size_t size) {
61
63
  hdr_write(raw, size);
62
64
  return static_cast<char*>(raw) + HDR_SIZE;
63
65
  }
66
+
67
+ /* QuickJS-heap allocator. When the PSRAM flag is set, route through the
68
+ * platform's malloc_psram first; if the platform can't satisfy the request
69
+ * (no PSRAM, or PSRAM exhausted), fall back to libc malloc so the runtime
70
+ * keeps working. The fallback only matters on host builds and in PSRAM-OOM
71
+ * edge cases. Under normal ESP32 operation with CONFIG_SPIRAM=y, every
72
+ * allocation lands in PSRAM. */
73
+
74
+ static bool g_quickjs_heap_psram = false;
75
+
76
+ void mik__set_quickjs_heap_psram(bool enable) {
77
+ g_quickjs_heap_psram = enable;
78
+ }
79
+
80
+ bool mik__is_quickjs_heap_psram(void) {
81
+ return g_quickjs_heap_psram;
82
+ }
83
+
84
+ void* mik__js_malloc(size_t size) {
85
+ void* raw = nullptr;
86
+ if (g_quickjs_heap_psram) {
87
+ const MIKPlatform* p = MIK_GetPlatform();
88
+ if (p && p->malloc_psram) {
89
+ raw = p->malloc_psram(size + HDR_SIZE);
90
+ }
91
+ }
92
+ if (!raw) raw = malloc(size + HDR_SIZE);
93
+ if (!raw) return nullptr;
94
+ hdr_write(raw, size);
95
+ return static_cast<char*>(raw) + HDR_SIZE;
96
+ }
97
+
98
+ void* mik__js_calloc(size_t count, size_t size) {
99
+ if (size && count > SIZE_MAX / size) return nullptr;
100
+ size_t total = count * size;
101
+ void* raw = nullptr;
102
+ if (g_quickjs_heap_psram) {
103
+ const MIKPlatform* p = MIK_GetPlatform();
104
+ if (p && p->calloc_psram) {
105
+ raw = p->calloc_psram(1, total + HDR_SIZE);
106
+ }
107
+ }
108
+ if (!raw) raw = calloc(1, total + HDR_SIZE);
109
+ if (!raw) return nullptr;
110
+ hdr_write(raw, total);
111
+ return static_cast<char*>(raw) + HDR_SIZE;
112
+ }
113
+
114
+ void* mik__js_realloc(void* ptr, size_t size) {
115
+ void* raw = ptr ? static_cast<char*>(ptr) - HDR_SIZE : nullptr;
116
+ void* new_raw = nullptr;
117
+ if (g_quickjs_heap_psram) {
118
+ const MIKPlatform* p = MIK_GetPlatform();
119
+ if (p && p->realloc_psram) {
120
+ new_raw = p->realloc_psram(raw, size + HDR_SIZE);
121
+ }
122
+ }
123
+ if (!new_raw) new_raw = realloc(raw, size + HDR_SIZE);
124
+ if (!new_raw) return nullptr;
125
+ hdr_write(new_raw, size);
126
+ return static_cast<char*>(new_raw) + HDR_SIZE;
127
+ }
package/src/mik_sys.cpp CHANGED
@@ -47,6 +47,10 @@ static JSValue mik__sys_memory_usage(JSContext* ctx, JSValue this_val, int argc,
47
47
  JS_NewInt64(ctx, (int64_t)platform->get_total_system_mem()));
48
48
  JS_SetPropertyStr(ctx, obj, "systemLargestFree",
49
49
  JS_NewInt64(ctx, (int64_t)platform->get_largest_free_system_mem()));
50
+ JS_SetPropertyStr(ctx, obj, "internalFree",
51
+ JS_NewInt64(ctx, (int64_t)platform->get_free_internal_mem()));
52
+ JS_SetPropertyStr(ctx, obj, "internalLargestFree",
53
+ JS_NewInt64(ctx, (int64_t)platform->get_largest_free_internal_mem()));
50
54
  return obj;
51
55
  }
52
56
 
package/src/mikrojs.cpp CHANGED
@@ -17,16 +17,18 @@
17
17
 
18
18
  #define MIK__DEFAULT_STACK_SIZE 1024 * 1024 // 1 MB
19
19
 
20
- /* JS malloc functions */
20
+ /* JS malloc functions. Routed through the mik__js_* family so the QuickJS
21
+ * heap can be relocated to PSRAM on chips with both internal SRAM and PSRAM,
22
+ * leaving internal SRAM free for mbedTLS, WiFi/BLE, and DMA buffers. */
21
23
 
22
24
  static void* mik__mf_calloc(void* opaque, size_t count, size_t size) {
23
25
  (void)opaque;
24
- return mik__calloc(count, size);
26
+ return mik__js_calloc(count, size);
25
27
  }
26
28
 
27
29
  static void* mik__mf_malloc(void* opaque, size_t size) {
28
30
  (void)opaque;
29
- return mik__malloc(size);
31
+ return mik__js_malloc(size);
30
32
  }
31
33
 
32
34
  static void mik__mf_free(void* opaque, void* ptr) {
@@ -36,7 +38,7 @@ static void mik__mf_free(void* opaque, void* ptr) {
36
38
 
37
39
  static void* mik__mf_realloc(void* opaque, void* ptr, size_t size) {
38
40
  (void)opaque;
39
- return mik__realloc(ptr, size);
41
+ return mik__js_realloc(ptr, size);
40
42
  }
41
43
 
42
44
  static const JSMallocFunctions mik_mf = {
@@ -199,7 +201,11 @@ static void mik__promise_rejection_tracker(JSContext* ctx, JSValue promise, JSVa
199
201
  }
200
202
 
201
203
  void MIK_DefaultOptions(MIKRunOptions* options) {
202
- static MIKRunOptions default_options = {.mem_limit = 0, .stack_size = MIK__DEFAULT_STACK_SIZE};
204
+ static MIKRunOptions default_options = {
205
+ .mem_limit = 0,
206
+ .stack_size = MIK__DEFAULT_STACK_SIZE,
207
+ .use_psram_heap = false,
208
+ };
203
209
 
204
210
  memcpy(options, &default_options, sizeof(*options));
205
211
  }
@@ -228,6 +234,12 @@ MIKRuntime* MIK_NewRuntimeInternal(MIKRunOptions* options) {
228
234
  memcpy(&mik_rt->options, options, sizeof(*options));
229
235
  MIK_DefaultConfig(&mik_rt->config);
230
236
 
237
+ /* Switch the QuickJS heap to PSRAM before constructing the runtime,
238
+ * so JS_NewRuntime2 and every subsequent QuickJS allocation lands
239
+ * in PSRAM. The MIKRuntime struct itself stays in internal SRAM:
240
+ * it was allocated before this point. */
241
+ mik__set_quickjs_heap_psram(options->use_psram_heap);
242
+
231
243
  rt = JS_NewRuntime2(&mik_mf, NULL);
232
244
  CHECK_NOT_NULL(rt);
233
245
  mik_rt->rt = rt;
@@ -45,6 +45,31 @@ static size_t posix_get_largest_free_system_mem(void) {
45
45
  return 0; /* Not available on desktop */
46
46
  }
47
47
 
48
+ static size_t posix_get_free_internal_mem(void) {
49
+ return 0; /* No internal/PSRAM distinction on desktop */
50
+ }
51
+
52
+ static size_t posix_get_largest_free_internal_mem(void) {
53
+ return 0; /* No internal/PSRAM distinction on desktop */
54
+ }
55
+
56
+ static void* posix_malloc_psram(size_t size) {
57
+ (void)size;
58
+ return NULL; /* No PSRAM on desktop; caller falls back to libc */
59
+ }
60
+
61
+ static void* posix_calloc_psram(size_t count, size_t size) {
62
+ (void)count;
63
+ (void)size;
64
+ return NULL;
65
+ }
66
+
67
+ static void* posix_realloc_psram(void* ptr, size_t size) {
68
+ (void)ptr;
69
+ (void)size;
70
+ return NULL;
71
+ }
72
+
48
73
  static bool posix_get_fs_info(const char* label, size_t* total, size_t* used) {
49
74
  (void)label;
50
75
  (void)total;
@@ -111,6 +136,11 @@ static const MIKPlatform posix_platform = {
111
136
  .get_min_free_system_mem = posix_get_min_free_system_mem,
112
137
  .get_total_system_mem = posix_get_total_system_mem,
113
138
  .get_largest_free_system_mem = posix_get_largest_free_system_mem,
139
+ .get_free_internal_mem = posix_get_free_internal_mem,
140
+ .get_largest_free_internal_mem = posix_get_largest_free_internal_mem,
141
+ .malloc_psram = posix_malloc_psram,
142
+ .calloc_psram = posix_calloc_psram,
143
+ .realloc_psram = posix_realloc_psram,
114
144
  .get_fs_info = posix_get_fs_info,
115
145
  .log = posix_log,
116
146
  .stdout_write = posix_stdout_write,