space-data-module-sdk 0.8.18 → 0.8.20
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/LICENSE +1 -1
- package/README.md +2 -2
- package/docs/assembly.js +190 -0
- package/docs/browser-wasmedge-isomorphic.html +6 -1
- package/docs/byo-wasm-quickstart.html +6 -1
- package/docs/conformance.html +6 -1
- package/docs/events-abi.html +6 -1
- package/docs/families/analytics.html +7 -2
- package/docs/families/attitude.html +7 -2
- package/docs/families/behavior.html +7 -2
- package/docs/families/breakup.html +7 -2
- package/docs/families/conjunction.html +7 -2
- package/docs/families/data-source.html +7 -2
- package/docs/families/effects.html +7 -2
- package/docs/families/environment.html +7 -2
- package/docs/families/estimation.html +7 -2
- package/docs/families/gnc.html +7 -2
- package/docs/families/maneuver.html +7 -2
- package/docs/families/obstruction.html +7 -2
- package/docs/families/propagator.html +7 -2
- package/docs/families/propulsion.html +7 -2
- package/docs/families/reentry.html +7 -2
- package/docs/families/rf.html +7 -2
- package/docs/families/scheduler.html +7 -2
- package/docs/families/sensor.html +7 -2
- package/docs/families/signature.html +7 -2
- package/docs/flatsql-host-contract.html +7 -2
- package/docs/flatsql-host-contract.md +2 -2
- package/docs/flatsql-streaming-standard.html +6 -1
- package/docs/gpu-module-abi.html +6 -1
- package/docs/index.html +21 -14
- package/docs/isomorphic-pthreads.html +19 -2
- package/docs/isomorphic-pthreads.md +59 -1
- package/docs/isomorphic-sdn-runtime-plan.html +6 -1
- package/docs/language-runtime-matrix.html +6 -1
- package/docs/llms.txt +1 -1
- package/docs/module-bundle-runtime-plan.html +6 -1
- package/docs/module-publication-standard.html +6 -1
- package/docs/node-red-default-node-parity.html +6 -1
- package/docs/protect-and-sign.html +6 -1
- package/docs/protocol-installation.html +6 -1
- package/docs/publication-submission.html +6 -1
- package/docs/secrets-capability.html +6 -1
- package/docs/styles.css +32 -0
- package/docs/testing-harness.html +6 -1
- package/docs/tri-runtime-parity-gate.html +6 -1
- package/docs/tri-runtime-parity.html +6 -1
- package/package.json +1 -1
- package/src/flow/flowCompiler.js +34 -23
- package/src/host/browserModuleHarness.js +39 -35
- package/src/host/isomorphicLoader.js +13 -6
- package/src/host/wasiShim.js +57 -13
- package/src/host/wasiThreadBrowserWorker.mjs +1 -0
- package/src/host/wasiThreadHost.js +16 -1
- package/src/host/wasiThreadWorker.mjs +2 -1
- package/src/host/wasiThreadWorkerRuntime.js +2 -1
- package/src/testing/browserModuleHarness.js +2 -0
- package/src/testing/buildWasmEdgeRunner.js +147 -4
- package/src/testing/docker/wasmedge-parity.Dockerfile +23 -1
- package/src/testing/isomorphicHarness.js +5 -0
- package/src/testing/native/wasmedge-0.16.4-atomic-wait.patch +111 -0
- package/src/testing/native/wasmedge_wasi_threads_runner.c +345 -0
- package/src/testing/parityBrowserRunner.js +12 -4
- package/src/testing/parityHarness.js +11 -3
- package/src/testing/parityLanes.js +29 -5
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// WASI preview1 command host for the SDK parity lanes. WasmEdge 0.16.4's
|
|
2
|
+
// CLI enables atomics but does not implement wasi.thread-spawn. Like the SDN
|
|
3
|
+
// Go host, use a fresh instance per worker, shared memory, and ONE executor:
|
|
4
|
+
// atomic.wait/notify waiters belong to the executor in this runtime revision.
|
|
5
|
+
#define _POSIX_C_SOURCE 200809L
|
|
6
|
+
#include <wasmedge/wasmedge.h>
|
|
7
|
+
#include <pthread.h>
|
|
8
|
+
#include <stdbool.h>
|
|
9
|
+
#include <stdint.h>
|
|
10
|
+
#include <stdio.h>
|
|
11
|
+
#include <stdlib.h>
|
|
12
|
+
#include <string.h>
|
|
13
|
+
#include <time.h>
|
|
14
|
+
|
|
15
|
+
#define MAX_WORKERS 32
|
|
16
|
+
|
|
17
|
+
typedef struct {
|
|
18
|
+
WasmEdge_ModuleInstanceContext *instance;
|
|
19
|
+
WasmEdge_Async *async;
|
|
20
|
+
} Worker;
|
|
21
|
+
|
|
22
|
+
typedef struct {
|
|
23
|
+
WasmEdge_ExecutorContext *executor;
|
|
24
|
+
WasmEdge_StoreContext *store;
|
|
25
|
+
WasmEdge_ASTModuleContext *ast;
|
|
26
|
+
pthread_mutex_t mutex;
|
|
27
|
+
Worker workers[MAX_WORKERS];
|
|
28
|
+
int32_t next_tid;
|
|
29
|
+
bool closed;
|
|
30
|
+
bool failed;
|
|
31
|
+
bool exited;
|
|
32
|
+
} Threads;
|
|
33
|
+
|
|
34
|
+
static WasmEdge_String name(const char *s) {
|
|
35
|
+
return WasmEdge_StringWrap(s, (uint32_t)strlen(s));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static uint64_t monotonic_ms(void) {
|
|
39
|
+
struct timespec now;
|
|
40
|
+
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
41
|
+
return (uint64_t)now.tv_sec * 1000 + (uint64_t)now.tv_nsec / 1000000;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static bool wait_until(WasmEdge_Async *call, uint64_t deadline) {
|
|
45
|
+
uint64_t now = monotonic_ms();
|
|
46
|
+
return WasmEdge_AsyncWaitFor(call, now < deadline ? deadline - now : 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static void require_result(const char *step, WasmEdge_Result result) {
|
|
50
|
+
if (!WasmEdge_ResultOK(result)) {
|
|
51
|
+
fprintf(stderr, "wasm trap: %s: %s\n", step,
|
|
52
|
+
WasmEdge_ResultGetMessage(result));
|
|
53
|
+
exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Called with the mutex held. Only release instances after execution ends.
|
|
58
|
+
static void reap(Threads *g) {
|
|
59
|
+
for (size_t i = 0; i < MAX_WORKERS; ++i) {
|
|
60
|
+
Worker *w = &g->workers[i];
|
|
61
|
+
if (!w->async || !WasmEdge_AsyncWaitFor(w->async, 0)) continue;
|
|
62
|
+
WasmEdge_Result result = WasmEdge_AsyncGet(w->async, NULL, 0);
|
|
63
|
+
if (WasmEdge_ResultGetCode(result) == WasmEdge_ErrCode_Terminated) {
|
|
64
|
+
g->exited = true;
|
|
65
|
+
g->closed = true;
|
|
66
|
+
} else if (!WasmEdge_ResultOK(result)) {
|
|
67
|
+
fprintf(stderr, "wasm trap: wasi worker: %s\n",
|
|
68
|
+
WasmEdge_ResultGetMessage(result));
|
|
69
|
+
g->failed = true;
|
|
70
|
+
g->closed = true;
|
|
71
|
+
}
|
|
72
|
+
WasmEdge_AsyncDelete(w->async);
|
|
73
|
+
WasmEdge_ModuleInstanceDelete(w->instance);
|
|
74
|
+
*w = (Worker){0};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static WasmEdge_Result thread_spawn(void *data,
|
|
79
|
+
const WasmEdge_CallingFrameContext *frame, const WasmEdge_Value *in,
|
|
80
|
+
WasmEdge_Value *out) {
|
|
81
|
+
Threads *g = data;
|
|
82
|
+
out[0] = WasmEdge_ValueGenI32(-6); // WASI EAGAIN: caller may retry or run inline.
|
|
83
|
+
pthread_mutex_lock(&g->mutex);
|
|
84
|
+
if (g->closed) goto done;
|
|
85
|
+
reap(g);
|
|
86
|
+
if (g->closed || g->next_tid >= (1 << 29)) goto done;
|
|
87
|
+
Worker *w = NULL;
|
|
88
|
+
for (size_t i = 0; i < MAX_WORKERS; ++i) {
|
|
89
|
+
if (!g->workers[i].async) { w = &g->workers[i]; break; }
|
|
90
|
+
}
|
|
91
|
+
if (!w) goto done;
|
|
92
|
+
WasmEdge_Result result = WasmEdge_ExecutorInstantiate(
|
|
93
|
+
WasmEdge_CallingFrameGetExecutor(frame), &w->instance, g->store, g->ast);
|
|
94
|
+
if (!WasmEdge_ResultOK(result)) goto discard;
|
|
95
|
+
const WasmEdge_FunctionInstanceContext *entry =
|
|
96
|
+
WasmEdge_ModuleInstanceFindFunction(w->instance, name("wasi_thread_start"));
|
|
97
|
+
if (!entry) goto discard;
|
|
98
|
+
int32_t tid = g->next_tid++;
|
|
99
|
+
WasmEdge_Value args[] = {WasmEdge_ValueGenI32(tid), in[0]};
|
|
100
|
+
w->async = WasmEdge_ExecutorAsyncInvoke(WasmEdge_CallingFrameGetExecutor(frame), entry, args, 2);
|
|
101
|
+
if (!w->async) goto discard;
|
|
102
|
+
out[0] = WasmEdge_ValueGenI32(tid);
|
|
103
|
+
goto done;
|
|
104
|
+
discard:
|
|
105
|
+
if (w->instance) WasmEdge_ModuleInstanceDelete(w->instance);
|
|
106
|
+
*w = (Worker){0};
|
|
107
|
+
done:
|
|
108
|
+
pthread_mutex_unlock(&g->mutex);
|
|
109
|
+
return WasmEdge_Result_Success;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static WasmEdge_ModuleInstanceContext *memory_import(Threads *g) {
|
|
113
|
+
uint32_t count = WasmEdge_ASTModuleListImportsLength(g->ast);
|
|
114
|
+
const WasmEdge_ImportTypeContext **imports = calloc(count, sizeof(*imports));
|
|
115
|
+
if (count && !imports) exit(1);
|
|
116
|
+
WasmEdge_ASTModuleListImports(g->ast, imports, count);
|
|
117
|
+
WasmEdge_ModuleInstanceContext *env = NULL;
|
|
118
|
+
bool threaded = false;
|
|
119
|
+
for (uint32_t i = 0; i < count; ++i) {
|
|
120
|
+
WasmEdge_String mod = WasmEdge_ImportTypeGetModuleName(imports[i]);
|
|
121
|
+
WasmEdge_String field = WasmEdge_ImportTypeGetExternalName(imports[i]);
|
|
122
|
+
if (WasmEdge_StringIsEqual(mod, name("wasi")) &&
|
|
123
|
+
WasmEdge_StringIsEqual(field, name("thread-spawn"))) threaded = true;
|
|
124
|
+
if (WasmEdge_ImportTypeGetExternalType(imports[i]) !=
|
|
125
|
+
WasmEdge_ExternalType_Memory) continue;
|
|
126
|
+
if (env || !WasmEdge_StringIsEqual(mod, name("env")) ||
|
|
127
|
+
!WasmEdge_StringIsEqual(field, name("memory"))) {
|
|
128
|
+
fprintf(stderr, "unsupported imported memory: expected one env.memory\n");
|
|
129
|
+
exit(1);
|
|
130
|
+
}
|
|
131
|
+
const WasmEdge_MemoryTypeContext *type =
|
|
132
|
+
WasmEdge_ImportTypeGetMemoryType(g->ast, imports[i]);
|
|
133
|
+
WasmEdge_Limit limit = WasmEdge_MemoryTypeGetLimit(type);
|
|
134
|
+
if (!limit.Shared || !limit.HasMax) {
|
|
135
|
+
fprintf(stderr, "WASI threads require bounded shared memory\n");
|
|
136
|
+
exit(1);
|
|
137
|
+
}
|
|
138
|
+
env = WasmEdge_ModuleInstanceCreate(name("env"));
|
|
139
|
+
WasmEdge_MemoryInstanceContext *memory = WasmEdge_MemoryInstanceCreate(type);
|
|
140
|
+
if (!env || !memory) exit(1);
|
|
141
|
+
WasmEdge_ModuleInstanceAddMemory(env, name("memory"), memory);
|
|
142
|
+
require_result("register memory", WasmEdge_ExecutorRegisterImport(
|
|
143
|
+
g->executor, g->store, env));
|
|
144
|
+
}
|
|
145
|
+
free(imports);
|
|
146
|
+
if (threaded && !env) {
|
|
147
|
+
fprintf(stderr, "WASI threads require imported shared memory\n");
|
|
148
|
+
exit(1);
|
|
149
|
+
}
|
|
150
|
+
return env;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Reactor artifacts use the same PIV request/response bytes as commands. Stage
|
|
154
|
+
// the request through the public allocator, then monitor the direct call with
|
|
155
|
+
// the same worker-group lifecycle as _start. No guest ABI is reimplemented.
|
|
156
|
+
static uint32_t stage_direct_request(Threads *g,
|
|
157
|
+
WasmEdge_ModuleInstanceContext *instance,
|
|
158
|
+
WasmEdge_MemoryInstanceContext *memory, WasmEdge_Value args[3]) {
|
|
159
|
+
size_t size = 0, capacity = 4096;
|
|
160
|
+
uint8_t *input = malloc(capacity);
|
|
161
|
+
if (!input) exit(1);
|
|
162
|
+
for (;;) {
|
|
163
|
+
size += fread(input + size, 1, capacity - size, stdin);
|
|
164
|
+
if (ferror(stdin)) { fprintf(stderr, "failed to read stdin\n"); exit(1); }
|
|
165
|
+
if (feof(stdin)) break;
|
|
166
|
+
if (capacity >= (1U << 30)) { fprintf(stderr, "request exceeds 1 GiB\n"); exit(1); }
|
|
167
|
+
capacity *= 2;
|
|
168
|
+
uint8_t *grown = realloc(input, capacity);
|
|
169
|
+
if (!grown) exit(1);
|
|
170
|
+
input = grown;
|
|
171
|
+
}
|
|
172
|
+
const WasmEdge_FunctionInstanceContext *init =
|
|
173
|
+
WasmEdge_ModuleInstanceFindFunction(instance, name("_initialize"));
|
|
174
|
+
if (init) require_result("initialize", WasmEdge_ExecutorInvoke(g->executor, init, NULL, 0, NULL, 0));
|
|
175
|
+
const WasmEdge_FunctionInstanceContext *alloc =
|
|
176
|
+
WasmEdge_ModuleInstanceFindFunction(instance, name("plugin_alloc"));
|
|
177
|
+
if (!alloc || !memory) { fprintf(stderr, "direct module requires plugin_alloc and memory\n"); exit(1); }
|
|
178
|
+
uint32_t padded = ((uint32_t)size + 7U) & ~7U;
|
|
179
|
+
WasmEdge_Value allocation_size = WasmEdge_ValueGenI32((int32_t)(padded + 4));
|
|
180
|
+
WasmEdge_Value pointer;
|
|
181
|
+
require_result("allocate", WasmEdge_ExecutorInvoke(g->executor, alloc, &allocation_size, 1, &pointer, 1));
|
|
182
|
+
uint32_t ptr = (uint32_t)WasmEdge_ValueGetI32(pointer);
|
|
183
|
+
if (!ptr || ptr % 8) { fprintf(stderr, "invalid direct allocation\n"); exit(1); }
|
|
184
|
+
uint32_t length_ptr = ptr + padded;
|
|
185
|
+
const uint8_t zero[4] = {0};
|
|
186
|
+
require_result("copy request", WasmEdge_MemoryInstanceSetData(memory, input, ptr, (uint32_t)size));
|
|
187
|
+
require_result("clear response length", WasmEdge_MemoryInstanceSetData(memory, zero, length_ptr, 4));
|
|
188
|
+
free(input);
|
|
189
|
+
args[0] = pointer;
|
|
190
|
+
args[1] = WasmEdge_ValueGenI32((int32_t)size);
|
|
191
|
+
args[2] = WasmEdge_ValueGenI32((int32_t)length_ptr);
|
|
192
|
+
return length_ptr;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static bool write_direct_response(WasmEdge_MemoryInstanceContext *memory,
|
|
196
|
+
uint32_t ptr, uint32_t length_ptr) {
|
|
197
|
+
uint8_t bytes[4];
|
|
198
|
+
require_result("read response length", WasmEdge_MemoryInstanceGetData(memory, bytes, length_ptr, 4));
|
|
199
|
+
uint32_t length = (uint32_t)bytes[0] | (uint32_t)bytes[1] << 8 |
|
|
200
|
+
(uint32_t)bytes[2] << 16 | (uint32_t)bytes[3] << 24;
|
|
201
|
+
if (!ptr || ptr % 8 || !length) {
|
|
202
|
+
fprintf(stderr, "invalid direct response\n");
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
const uint8_t *data = WasmEdge_MemoryInstanceGetPointerConst(memory, ptr, length);
|
|
206
|
+
if (!data) { fprintf(stderr, "direct response is out of bounds\n"); return false; }
|
|
207
|
+
return fwrite(data, 1, length, stdout) == length;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
int main(int argc, char **argv) {
|
|
211
|
+
if (strcmp(WasmEdge_VersionGet(), "0.16.4") != 0) {
|
|
212
|
+
fprintf(stderr, "WASI threads runner requires WasmEdge 0.16.4; linked %s\n", WasmEdge_VersionGet());
|
|
213
|
+
return 2;
|
|
214
|
+
}
|
|
215
|
+
if (argc == 2 && strcmp(argv[1], "--version") == 0) {
|
|
216
|
+
printf("wasmedge version %s\nsdm-wasi-threads-runner 1\n",
|
|
217
|
+
WasmEdge_VersionGet());
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
// WasmEdge's default logger writes to stdout. Guest stdout is binary; all
|
|
221
|
+
// host diagnostics below are emitted to stderr instead.
|
|
222
|
+
WasmEdge_LogOff();
|
|
223
|
+
const char **envs = calloc((size_t)argc, sizeof(*envs));
|
|
224
|
+
if (!envs) return 1;
|
|
225
|
+
uint32_t env_count = 0;
|
|
226
|
+
bool stats = false;
|
|
227
|
+
int first = 1;
|
|
228
|
+
for (; first < argc; ++first) {
|
|
229
|
+
if (strcmp(argv[first], "--enable-threads") == 0) continue;
|
|
230
|
+
if (strcmp(argv[first], "--sdm-thread-stats") == 0) { stats = true; continue; }
|
|
231
|
+
if (strcmp(argv[first], "--env") == 0 && first + 1 < argc) {
|
|
232
|
+
envs[env_count++] = argv[++first];
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
if (first >= argc || argv[first][0] == '-') {
|
|
238
|
+
fprintf(stderr, "usage: %s [--env NAME=VALUE] module.wasm [args...]\n", argv[0]);
|
|
239
|
+
free(envs);
|
|
240
|
+
return 2;
|
|
241
|
+
}
|
|
242
|
+
WasmEdge_ConfigureContext *conf = WasmEdge_ConfigureCreate();
|
|
243
|
+
WasmEdge_ConfigureAddProposal(conf, WasmEdge_Proposal_Threads);
|
|
244
|
+
WasmEdge_LoaderContext *loader = WasmEdge_LoaderCreate(conf);
|
|
245
|
+
WasmEdge_ValidatorContext *validator = WasmEdge_ValidatorCreate(conf);
|
|
246
|
+
Threads g = {.executor = WasmEdge_ExecutorCreate(conf, NULL),
|
|
247
|
+
.store = WasmEdge_StoreCreate(), .next_tid = 1};
|
|
248
|
+
if (pthread_mutex_init(&g.mutex, NULL) != 0) return 1;
|
|
249
|
+
require_result("parse", WasmEdge_LoaderParseFromFile(loader, &g.ast, argv[first]));
|
|
250
|
+
require_result("validate", WasmEdge_ValidatorValidate(validator, g.ast));
|
|
251
|
+
WasmEdge_ModuleInstanceContext *wasi = WasmEdge_ModuleInstanceCreateWASI(
|
|
252
|
+
(const char *const *)(argv + first), (uint32_t)(argc - first),
|
|
253
|
+
envs, env_count, NULL, 0);
|
|
254
|
+
free(envs);
|
|
255
|
+
if (!wasi) return 1;
|
|
256
|
+
require_result("register WASI", WasmEdge_ExecutorRegisterImport(
|
|
257
|
+
g.executor, g.store, wasi));
|
|
258
|
+
WasmEdge_ModuleInstanceContext *env = memory_import(&g);
|
|
259
|
+
WasmEdge_ModuleInstanceContext *threads = WasmEdge_ModuleInstanceCreate(name("wasi"));
|
|
260
|
+
WasmEdge_ValType i32 = WasmEdge_ValTypeGenI32();
|
|
261
|
+
WasmEdge_FunctionTypeContext *type = WasmEdge_FunctionTypeCreate(&i32, 1, &i32, 1);
|
|
262
|
+
WasmEdge_FunctionInstanceContext *spawn = WasmEdge_FunctionInstanceCreate(
|
|
263
|
+
type, thread_spawn, &g, 0);
|
|
264
|
+
WasmEdge_FunctionTypeDelete(type);
|
|
265
|
+
if (!threads || !spawn) return 1;
|
|
266
|
+
WasmEdge_ModuleInstanceAddFunction(threads, name("thread-spawn"), spawn);
|
|
267
|
+
require_result("register wasi threads", WasmEdge_ExecutorRegisterImport(
|
|
268
|
+
g.executor, g.store, threads));
|
|
269
|
+
WasmEdge_ModuleInstanceContext *instance = NULL;
|
|
270
|
+
require_result("instantiate", WasmEdge_ExecutorInstantiate(
|
|
271
|
+
g.executor, &instance, g.store, g.ast));
|
|
272
|
+
const WasmEdge_FunctionInstanceContext *start =
|
|
273
|
+
WasmEdge_ModuleInstanceFindFunction(instance, name("_start"));
|
|
274
|
+
bool direct = start == NULL;
|
|
275
|
+
WasmEdge_Value direct_args[3], response_pointer;
|
|
276
|
+
uint32_t response_length_ptr = 0;
|
|
277
|
+
WasmEdge_MemoryInstanceContext *memory = env
|
|
278
|
+
? WasmEdge_ModuleInstanceFindMemory(env, name("memory"))
|
|
279
|
+
: WasmEdge_ModuleInstanceFindMemory(instance, name("memory"));
|
|
280
|
+
if (direct) {
|
|
281
|
+
start = WasmEdge_ModuleInstanceFindFunction(instance, name("plugin_invoke_stream"));
|
|
282
|
+
if (!start) { fprintf(stderr, "module has neither _start nor plugin_invoke_stream\n"); return 1; }
|
|
283
|
+
response_length_ptr = stage_direct_request(&g, instance, memory, direct_args);
|
|
284
|
+
}
|
|
285
|
+
WasmEdge_Async *main_call = WasmEdge_ExecutorAsyncInvoke(g.executor, start,
|
|
286
|
+
direct ? direct_args : NULL, direct ? 3 : 0);
|
|
287
|
+
if (!main_call) return 1;
|
|
288
|
+
for (;;) {
|
|
289
|
+
bool finished = WasmEdge_AsyncWaitFor(main_call, 1);
|
|
290
|
+
pthread_mutex_lock(&g.mutex);
|
|
291
|
+
reap(&g);
|
|
292
|
+
bool stop = finished || g.failed || g.exited;
|
|
293
|
+
if (stop) g.closed = true;
|
|
294
|
+
pthread_mutex_unlock(&g.mutex);
|
|
295
|
+
if (stop) break;
|
|
296
|
+
}
|
|
297
|
+
// Exit/trap in any thread ends the whole command. Cancel all executions
|
|
298
|
+
// before releasing shared memory; a stuck cancellation ends this process
|
|
299
|
+
// without freeing objects underneath live native workers.
|
|
300
|
+
uint64_t deadline = monotonic_ms() + 2000;
|
|
301
|
+
if (!WasmEdge_AsyncWaitFor(main_call, 0)) WasmEdge_AsyncCancel(main_call);
|
|
302
|
+
pthread_mutex_lock(&g.mutex);
|
|
303
|
+
for (size_t i = 0; i < MAX_WORKERS; ++i) {
|
|
304
|
+
if (g.workers[i].async) WasmEdge_AsyncCancel(g.workers[i].async);
|
|
305
|
+
}
|
|
306
|
+
pthread_mutex_unlock(&g.mutex);
|
|
307
|
+
if (!wait_until(main_call, deadline)) {
|
|
308
|
+
fprintf(stderr, "wasm trap: main cancellation timed out\n");
|
|
309
|
+
fflush(NULL);
|
|
310
|
+
_Exit(1);
|
|
311
|
+
}
|
|
312
|
+
WasmEdge_Result result = WasmEdge_AsyncGet(main_call,
|
|
313
|
+
direct ? &response_pointer : NULL, direct ? 1 : 0);
|
|
314
|
+
bool failed = g.failed || (!WasmEdge_ResultOK(result) && !g.exited);
|
|
315
|
+
if (failed) fprintf(stderr, "wasm trap: command: %s\n", WasmEdge_ResultGetMessage(result));
|
|
316
|
+
for (size_t i = 0; i < MAX_WORKERS; ++i) {
|
|
317
|
+
Worker *w = &g.workers[i];
|
|
318
|
+
if (!w->async) continue;
|
|
319
|
+
if (!wait_until(w->async, deadline)) {
|
|
320
|
+
fprintf(stderr, "wasm trap: worker cancellation timed out\n");
|
|
321
|
+
fflush(NULL);
|
|
322
|
+
_Exit(1);
|
|
323
|
+
}
|
|
324
|
+
WasmEdge_AsyncDelete(w->async);
|
|
325
|
+
WasmEdge_ModuleInstanceDelete(w->instance);
|
|
326
|
+
}
|
|
327
|
+
if (stats) fprintf(stderr, "sdm-wasi-threads: spawned=%d\n", g.next_tid - 1);
|
|
328
|
+
if (direct && !failed && !g.exited) {
|
|
329
|
+
failed = !write_direct_response(memory, (uint32_t)WasmEdge_ValueGetI32(response_pointer), response_length_ptr);
|
|
330
|
+
}
|
|
331
|
+
int exit_code = failed ? 1 : (int)WasmEdge_ModuleInstanceWASIGetExitCode(wasi);
|
|
332
|
+
WasmEdge_AsyncDelete(main_call);
|
|
333
|
+
WasmEdge_ModuleInstanceDelete(instance);
|
|
334
|
+
WasmEdge_ModuleInstanceDelete(threads);
|
|
335
|
+
if (env) WasmEdge_ModuleInstanceDelete(env);
|
|
336
|
+
WasmEdge_ModuleInstanceDelete(wasi);
|
|
337
|
+
WasmEdge_StoreDelete(g.store);
|
|
338
|
+
WasmEdge_ASTModuleDelete(g.ast);
|
|
339
|
+
WasmEdge_ExecutorDelete(g.executor);
|
|
340
|
+
WasmEdge_ValidatorDelete(validator);
|
|
341
|
+
WasmEdge_LoaderDelete(loader);
|
|
342
|
+
WasmEdge_ConfigureDelete(conf);
|
|
343
|
+
pthread_mutex_destroy(&g.mutex);
|
|
344
|
+
return exit_code;
|
|
345
|
+
}
|
|
@@ -91,6 +91,7 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
|
|
|
91
91
|
let stdout = new Uint8Array(0);
|
|
92
92
|
let stderr = new Uint8Array(0);
|
|
93
93
|
let harness = null;
|
|
94
|
+
let spawnCount = null;
|
|
94
95
|
try {
|
|
95
96
|
// Plaintext hygiene: no per-case copy. createBrowserModuleHarness
|
|
96
97
|
// compiles directly from a caller-supplied buffer without copying it
|
|
@@ -101,7 +102,10 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
|
|
|
101
102
|
// would only add a fresh, unzeroed plaintext copy per run.
|
|
102
103
|
harness = await createBrowserModuleHarness({
|
|
103
104
|
wasmSource: moduleBytes,
|
|
104
|
-
surface: "command",
|
|
105
|
+
surface: plan.surface ?? "command",
|
|
106
|
+
enableBrowserWasiThreads: true,
|
|
107
|
+
maxThreads: threadCount,
|
|
108
|
+
wasiThreadWorkerUrl: new URL("/wasi-thread-worker.js", globalThis.location.href).href,
|
|
105
109
|
args: planCase.args,
|
|
106
110
|
env: {
|
|
107
111
|
...planCase.env,
|
|
@@ -113,8 +117,11 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
|
|
|
113
117
|
} catch (error) {
|
|
114
118
|
({ exitClass, exitDetail } = classifyBrowserError(error));
|
|
115
119
|
} finally {
|
|
120
|
+
stderr = harness?.wasi?.stderr ?? stderr;
|
|
121
|
+
if (exitClass !== OK) stdout = harness?.wasi?.stdout ?? stdout;
|
|
122
|
+
spawnCount = harness?.threadHost?.spawnCount() ?? null;
|
|
116
123
|
try {
|
|
117
|
-
harness?.destroy?.();
|
|
124
|
+
await harness?.destroy?.();
|
|
118
125
|
} catch {
|
|
119
126
|
/* teardown must never mask the run result */
|
|
120
127
|
}
|
|
@@ -124,6 +131,7 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
|
|
|
124
131
|
threadCount,
|
|
125
132
|
exitClass,
|
|
126
133
|
exitDetail,
|
|
134
|
+
spawnCount,
|
|
127
135
|
stdoutBase64: bytesToBase64(stdout),
|
|
128
136
|
stderrBase64: bytesToBase64(stderr),
|
|
129
137
|
});
|
|
@@ -144,8 +152,8 @@ async function postDone(body) {
|
|
|
144
152
|
// Auto-run only inside a real browser page (the served runner). Node imports
|
|
145
153
|
// of this module (tests, bundlers) never auto-run.
|
|
146
154
|
if (
|
|
147
|
-
typeof globalThis.
|
|
148
|
-
typeof globalThis.
|
|
155
|
+
typeof globalThis.self !== "undefined" &&
|
|
156
|
+
typeof globalThis.location !== "undefined" &&
|
|
149
157
|
globalThis.__SDM_PARITY_AUTORUN__ !== false
|
|
150
158
|
) {
|
|
151
159
|
runParityPlanInBrowser({})
|
|
@@ -71,14 +71,18 @@ export function loadWasmEdgePin() {
|
|
|
71
71
|
if (!repository) {
|
|
72
72
|
throw new Error("wasmedgePin.json dockerImageRepository is required.");
|
|
73
73
|
}
|
|
74
|
+
const runnerIdentity = createHash("sha256")
|
|
75
|
+
.update(readFileSync(path.join(__dirname, "native/wasmedge_wasi_threads_runner.c")))
|
|
76
|
+
.update(readFileSync(path.join(__dirname, "native/wasmedge-0.16.4-atomic-wait.patch")))
|
|
77
|
+
.update(readFileSync(path.join(__dirname, String(pin.dockerfile))))
|
|
78
|
+
.digest("hex").slice(0, 16);
|
|
74
79
|
return Object.freeze({
|
|
75
80
|
wasmedgeVersion: version,
|
|
76
81
|
dockerImageRepository: repository,
|
|
77
82
|
dockerImage: `${repository}:${version}`,
|
|
83
|
+
dockerRunnerImage: `${repository}:${version}-threads-${runnerIdentity}`,
|
|
78
84
|
dockerfilePath: path.join(__dirname, String(pin.dockerfile)),
|
|
79
|
-
dockerfileContextDir:
|
|
80
|
-
path.join(__dirname, String(pin.dockerfile)),
|
|
81
|
-
),
|
|
85
|
+
dockerfileContextDir: __dirname,
|
|
82
86
|
pinPath,
|
|
83
87
|
});
|
|
84
88
|
}
|
|
@@ -550,6 +554,9 @@ export async function runParityHarness(options = {}) {
|
|
|
550
554
|
log: options.log ?? (() => {}),
|
|
551
555
|
chromeBinary: options.chromeBinary,
|
|
552
556
|
wasmedgeBinary: options.wasmedgeBinary,
|
|
557
|
+
wasmEdgeRunnerBinary: options.wasmEdgeRunnerBinary,
|
|
558
|
+
browserSurface: options.browserSurface ??
|
|
559
|
+
(WebAssembly.Module.exports(await WebAssembly.compile(loadableBytes)).some((entry) => entry.name === "_start") ? "command" : "direct"),
|
|
553
560
|
dockerBinary: options.dockerBinary,
|
|
554
561
|
dockerPlatform: options.dockerPlatform,
|
|
555
562
|
autoBuildDockerImage: options.autoBuildDockerImage !== false,
|
|
@@ -609,6 +616,7 @@ export async function runParityHarness(options = {}) {
|
|
|
609
616
|
threadCount: run.threadCount,
|
|
610
617
|
exitClass: run.exitClass,
|
|
611
618
|
exitDetail: run.exitDetail ?? null,
|
|
619
|
+
spawnCount: run.spawnCount ?? null,
|
|
612
620
|
stdoutSha256: sha256Hex(toUint8Array(run.stdout) ?? new Uint8Array(0)),
|
|
613
621
|
stdoutLength: (toUint8Array(run.stdout) ?? new Uint8Array(0)).length,
|
|
614
622
|
})),
|
|
@@ -30,6 +30,8 @@ import { promisify } from "node:util";
|
|
|
30
30
|
|
|
31
31
|
import { ExitClass, assertWasmEdgeVersionMatchesPin } from "./parityHarness.js";
|
|
32
32
|
import { normalizeWasmEdgeOutcome } from "./wasmedgeOutput.js";
|
|
33
|
+
import { isWasiThreadsModule } from "../host/wasiThreadHost.js";
|
|
34
|
+
import { resolveWasmEdgeWasiThreadsRunner } from "./buildWasmEdgeRunner.js";
|
|
33
35
|
|
|
34
36
|
const execFile = promisify(execFileCallback);
|
|
35
37
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -162,7 +164,10 @@ export async function resolveWasmEdgeBinary(context = {}) {
|
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
export async function runNativeWasmEdgeLane(context) {
|
|
165
|
-
const
|
|
167
|
+
const threaded = isWasiThreadsModule(await WebAssembly.compile(context.loadableBytes ?? context.wasmBytes));
|
|
168
|
+
const binary = threaded
|
|
169
|
+
? await resolveWasmEdgeWasiThreadsRunner(context)
|
|
170
|
+
: await resolveWasmEdgeBinary(context);
|
|
166
171
|
let versionOutput;
|
|
167
172
|
try {
|
|
168
173
|
versionOutput = (await execFile(binary, ["--version"])).stdout;
|
|
@@ -185,7 +190,8 @@ export async function runNativeWasmEdgeLane(context) {
|
|
|
185
190
|
for (const threadCount of planCase.threadCounts) {
|
|
186
191
|
const outcome = await spawnWithStdin(
|
|
187
192
|
binary,
|
|
188
|
-
|
|
193
|
+
[...(threaded ? ["--sdm-thread-stats"] : []),
|
|
194
|
+
...wasmedgeInvocationArgs(planCase, context.plan, threadCount)],
|
|
189
195
|
{
|
|
190
196
|
cwd: workdir,
|
|
191
197
|
env: { PATH: process.env.PATH ?? "" },
|
|
@@ -208,6 +214,7 @@ export async function runNativeWasmEdgeLane(context) {
|
|
|
208
214
|
exitDetail,
|
|
209
215
|
stdout: normalized.stdout,
|
|
210
216
|
stderr: normalized.stderr,
|
|
217
|
+
spawnCount: threaded ? Number(/sdm-wasi-threads: spawned=(\d+)/.exec(normalized.diagnosticText)?.[1] ?? NaN) : null,
|
|
211
218
|
stateFiles: null,
|
|
212
219
|
});
|
|
213
220
|
}
|
|
@@ -271,6 +278,8 @@ export async function ensureDockerParityImage(context) {
|
|
|
271
278
|
}
|
|
272
279
|
|
|
273
280
|
export async function runDockerWasmEdgeLane(context) {
|
|
281
|
+
const threaded = isWasiThreadsModule(await WebAssembly.compile(context.loadableBytes ?? context.wasmBytes));
|
|
282
|
+
if (threaded) context = { ...context, pin: { ...context.pin, dockerImage: context.pin.dockerRunnerImage } };
|
|
274
283
|
const dockerBinary = context.dockerBinary ?? "docker";
|
|
275
284
|
try {
|
|
276
285
|
await execFile(dockerBinary, ["--version"]);
|
|
@@ -300,8 +309,10 @@ export async function runDockerWasmEdgeLane(context) {
|
|
|
300
309
|
if (context.dockerPlatform) {
|
|
301
310
|
dockerArgs.push("--platform", String(context.dockerPlatform));
|
|
302
311
|
}
|
|
312
|
+
if (threaded) dockerArgs.push("--entrypoint", "/opt/wasmedge/bin/sdm-wasi-threads-runner");
|
|
303
313
|
dockerArgs.push(
|
|
304
314
|
image,
|
|
315
|
+
...(threaded ? ["--sdm-thread-stats"] : []),
|
|
305
316
|
...wasmedgeInvocationArgs(planCase, context.plan, threadCount),
|
|
306
317
|
);
|
|
307
318
|
const outcome = await spawnWithStdin(dockerBinary, dockerArgs, {
|
|
@@ -323,6 +334,7 @@ export async function runDockerWasmEdgeLane(context) {
|
|
|
323
334
|
exitDetail,
|
|
324
335
|
stdout: normalized.stdout,
|
|
325
336
|
stderr: normalized.stderr,
|
|
337
|
+
spawnCount: threaded ? Number(/sdm-wasi-threads: spawned=(\d+)/.exec(normalized.diagnosticText)?.[1] ?? NaN) : null,
|
|
326
338
|
stateFiles: null,
|
|
327
339
|
});
|
|
328
340
|
}
|
|
@@ -373,11 +385,17 @@ async function buildBrowserRunnerBundle() {
|
|
|
373
385
|
external: ["node:*", "hd-wallet-wasm"],
|
|
374
386
|
logLevel: "silent",
|
|
375
387
|
});
|
|
376
|
-
|
|
388
|
+
const threadWorker = await esbuild.build({
|
|
389
|
+
entryPoints: [path.join(__dirname, "../host/wasiThreadBrowserWorker.mjs")],
|
|
390
|
+
bundle: true, write: false, format: "esm", platform: "browser", target: ["chrome110"],
|
|
391
|
+
external: ["node:*", "hd-wallet-wasm"], logLevel: "silent",
|
|
392
|
+
});
|
|
393
|
+
return { runner: result.outputFiles[0].text, threadWorker: threadWorker.outputFiles[0].text };
|
|
377
394
|
}
|
|
378
395
|
|
|
379
396
|
function browserPlanPayload(context) {
|
|
380
397
|
return JSON.stringify({
|
|
398
|
+
surface: context.browserSurface ?? "command",
|
|
381
399
|
threadEnvVar: context.plan.threadEnvVar,
|
|
382
400
|
cases: context.plan.cases.map((planCase) => ({
|
|
383
401
|
id: planCase.id,
|
|
@@ -393,7 +411,7 @@ const RUNNER_HTML = `<!doctype html>
|
|
|
393
411
|
<html>
|
|
394
412
|
<head><meta charset="utf-8"><title>sdm parity runner</title></head>
|
|
395
413
|
<body><pre id="status">parity runner booting…</pre>
|
|
396
|
-
<script type="module"
|
|
414
|
+
<script type="module">new Worker("/runner.js", { type: "module" });</script></body>
|
|
397
415
|
</html>`;
|
|
398
416
|
|
|
399
417
|
export async function runBrowserLane(context) {
|
|
@@ -447,7 +465,12 @@ export async function runBrowserLane(context) {
|
|
|
447
465
|
...securityHeaders,
|
|
448
466
|
"Content-Type": "text/javascript; charset=utf-8",
|
|
449
467
|
});
|
|
450
|
-
response.end(runnerBundle);
|
|
468
|
+
response.end(runnerBundle.runner);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (url.pathname === "/wasi-thread-worker.js") {
|
|
472
|
+
response.writeHead(200, { ...securityHeaders, "Content-Type": "text/javascript; charset=utf-8" });
|
|
473
|
+
response.end(runnerBundle.threadWorker);
|
|
451
474
|
return;
|
|
452
475
|
}
|
|
453
476
|
if (url.pathname === "/plan") {
|
|
@@ -522,6 +545,7 @@ export async function runBrowserLane(context) {
|
|
|
522
545
|
threadCount: Number(run.threadCount),
|
|
523
546
|
exitClass: String(run.exitClass),
|
|
524
547
|
exitDetail: run.exitDetail ?? null,
|
|
548
|
+
spawnCount: run.spawnCount ?? null,
|
|
525
549
|
stdout: new Uint8Array(Buffer.from(String(run.stdoutBase64 ?? ""), "base64")),
|
|
526
550
|
stderr: new Uint8Array(Buffer.from(String(run.stderrBase64 ?? ""), "base64")),
|
|
527
551
|
stateFiles: null,
|