@agorapete/wllama 3.5.1-q2.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/.gitmodules +3 -0
- package/.prettierignore +38 -0
- package/AGENTS.md +1 -0
- package/CMakeLists.txt +131 -0
- package/LICENCE +21 -0
- package/README-dev.md +178 -0
- package/README.md +225 -0
- package/README_banner.png +0 -0
- package/assets/screenshot_0.png +0 -0
- package/cpp/generate_glue_prototype.js +115 -0
- package/cpp/glue.hpp +664 -0
- package/cpp/test_glue.cpp +80 -0
- package/cpp/wllama-context.h +1172 -0
- package/cpp/wllama-fs.h +148 -0
- package/cpp/wllama.cpp +187 -0
- package/cpp/wllama.h +6 -0
- package/esm/cache-manager.d.ts +130 -0
- package/esm/debug.d.ts +28 -0
- package/esm/glue/glue.d.ts +22 -0
- package/esm/glue/messages.d.ts +146 -0
- package/esm/huggingface.d.ts +31 -0
- package/esm/index.cjs +3406 -0
- package/esm/index.d.ts +8 -0
- package/esm/index.js +3387 -0
- package/esm/index.min.js +1 -0
- package/esm/index.min.js.map +1 -0
- package/esm/model-manager.d.ts +136 -0
- package/esm/storage/cos.d.ts +36 -0
- package/esm/storage/index.d.ts +33 -0
- package/esm/storage/opfs.d.ts +12 -0
- package/esm/types/oai-compat.d.ts +278 -0
- package/esm/types/types.d.ts +112 -0
- package/esm/utils.d.ts +119 -0
- package/esm/wasm/source-map.d.ts +1 -0
- package/esm/wasm/wllama.wasm +0 -0
- package/esm/wasm-from-cdn.d.ts +8 -0
- package/esm/wllama.d.ts +397 -0
- package/esm/worker.d.ts +92 -0
- package/esm/workers-code/generated.d.ts +4 -0
- package/guides/intro-v2.md +132 -0
- package/guides/intro-v3.1.md +40 -0
- package/guides/intro-v3.md +230 -0
- package/index.ts +1 -0
- package/package.json +71 -0
- package/scripts/bisect_test.sh +33 -0
- package/scripts/build_hf_space.sh +26 -0
- package/scripts/build_source_map.js +269 -0
- package/scripts/build_wasm.sh +19 -0
- package/scripts/build_worker.sh +38 -0
- package/scripts/check_debug_build.js +30 -0
- package/scripts/check_package_size.js +25 -0
- package/scripts/docker-compose.yml +76 -0
- package/scripts/generate_wasm_from_cdn.js +24 -0
- package/scripts/http_server.js +44 -0
- package/scripts/post_build.sh +32 -0
- package/src/cache-manager.ts +358 -0
- package/src/debug.ts +111 -0
- package/src/glue/glue.ts +291 -0
- package/src/glue/messages.ts +773 -0
- package/src/huggingface.ts +151 -0
- package/src/index.ts +8 -0
- package/src/mjs.test.ts +44 -0
- package/src/model-manager.test.ts +200 -0
- package/src/model-manager.ts +359 -0
- package/src/storage/cos.test.ts +83 -0
- package/src/storage/cos.ts +171 -0
- package/src/storage/index.ts +40 -0
- package/src/storage/opfs.ts +119 -0
- package/src/types/oai-compat.ts +342 -0
- package/src/types/types.ts +133 -0
- package/src/utils.test.ts +231 -0
- package/src/utils.ts +403 -0
- package/src/wasm/source-map.ts +7 -0
- package/src/wasm/wllama.js +1 -0
- package/src/wasm/wllama.wasm +0 -0
- package/src/wasm-from-cdn.ts +13 -0
- package/src/wllama.test.ts +392 -0
- package/src/wllama.ts +1138 -0
- package/src/wllama.wgpu.test.ts +62 -0
- package/src/worker.ts +443 -0
- package/src/workers-code/generated.ts +11 -0
- package/src/workers-code/llama-cpp.js +511 -0
- package/src/workers-code/opfs-utils.js +150 -0
- package/tsconfig.build.json +34 -0
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +61 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { getHFFileSHA256 } from './huggingface';
|
|
2
|
+
import type { DownloadProgressCallback } from './model-manager';
|
|
3
|
+
import { COSBackend } from './storage/cos';
|
|
4
|
+
import type { StorageBackend, StorageFileHint } from './storage/index';
|
|
5
|
+
|
|
6
|
+
const PREFIX_METADATA = '__metadata__';
|
|
7
|
+
|
|
8
|
+
export type DownloadOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Callback function to track download progress
|
|
11
|
+
*/
|
|
12
|
+
progressCallback?: DownloadProgressCallback;
|
|
13
|
+
/**
|
|
14
|
+
* Additional metadata to be stored with the downloaded file
|
|
15
|
+
*/
|
|
16
|
+
metadataAdditional?: Record<string, any>;
|
|
17
|
+
/**
|
|
18
|
+
* Custom headers for the request. Useful for authentication (e.g. Bearer token)
|
|
19
|
+
*/
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
/**
|
|
22
|
+
* Abort signal for the request
|
|
23
|
+
*/
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// To prevent breaking change, we fill etag with a pre-defined value
|
|
28
|
+
export const POLYFILL_ETAG = 'polyfill_for_older_version';
|
|
29
|
+
|
|
30
|
+
export interface CacheEntry {
|
|
31
|
+
/**
|
|
32
|
+
* Storage key for this file, in the format: `${hashSHA1(fullURL)}_${fileName}`
|
|
33
|
+
*/
|
|
34
|
+
name: string;
|
|
35
|
+
/**
|
|
36
|
+
* Size of file (in bytes)
|
|
37
|
+
*/
|
|
38
|
+
size: number;
|
|
39
|
+
/**
|
|
40
|
+
* Other metadata
|
|
41
|
+
*/
|
|
42
|
+
metadata: CacheEntryMetadata;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CacheEntryMetadata {
|
|
46
|
+
/**
|
|
47
|
+
* ETag header from remote request
|
|
48
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
|
|
49
|
+
*/
|
|
50
|
+
etag: string;
|
|
51
|
+
/**
|
|
52
|
+
* Remote file size (in bytes), used for integrity check
|
|
53
|
+
*/
|
|
54
|
+
originalSize: number;
|
|
55
|
+
/**
|
|
56
|
+
* Original URL of the remote model. Unused for now
|
|
57
|
+
*/
|
|
58
|
+
originalURL: string;
|
|
59
|
+
/**
|
|
60
|
+
* URL to mmproj file, if exists
|
|
61
|
+
*/
|
|
62
|
+
mmprojURL?: string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Optional SHA256, mostly used by COS backend
|
|
65
|
+
*/
|
|
66
|
+
sha256?: string | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hintFromMetadata(
|
|
70
|
+
metadata: CacheEntryMetadata | null
|
|
71
|
+
): StorageFileHint | undefined {
|
|
72
|
+
if (!metadata) return undefined;
|
|
73
|
+
if (metadata.sha256) return { sha256: metadata.sha256 };
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Manages cached model files, backed by a pluggable StorageBackend.
|
|
79
|
+
*
|
|
80
|
+
* Defaults to OPFS (Origin Private File System).
|
|
81
|
+
*/
|
|
82
|
+
export class CacheManager {
|
|
83
|
+
private sb: StorageBackend;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param backends Array of storage backends to use, in order of preference ; if first is available, use it, otherwise try the next one.
|
|
87
|
+
*/
|
|
88
|
+
constructor(backends: StorageBackend[] = [new COSBackend()]) {
|
|
89
|
+
for (const backend of backends) {
|
|
90
|
+
if (backend.isSupported()) {
|
|
91
|
+
this.sb = backend;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw new Error('No supported storage backend found');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Convert a given URL into a storage key.
|
|
100
|
+
*
|
|
101
|
+
* Format: `${hashSHA1(fullURL)}_${fileName}`
|
|
102
|
+
*/
|
|
103
|
+
async getNameFromURL(url: string): Promise<string> {
|
|
104
|
+
return urlToFileName(url, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @deprecated Use `download()` instead
|
|
109
|
+
*
|
|
110
|
+
* Write a new file to cache. This will overwrite existing file.
|
|
111
|
+
*
|
|
112
|
+
* @param name The file name returned by `getNameFromURL()` or `list()`
|
|
113
|
+
*/
|
|
114
|
+
async write(
|
|
115
|
+
name: string,
|
|
116
|
+
stream: ReadableStream,
|
|
117
|
+
metadata: CacheEntryMetadata
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
// write file first, then metadata
|
|
120
|
+
await this.sb.write(name, stream);
|
|
121
|
+
await this.writeMetadata(name, metadata);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async download(url: string, options: DownloadOptions = {}): Promise<void> {
|
|
125
|
+
const fileKey = await urlToFileName(url, '');
|
|
126
|
+
|
|
127
|
+
// Fetch sha256 before the GET so we can skip the download entirely if the
|
|
128
|
+
// file is already in COS (avoids opening a connection just to cancel it).
|
|
129
|
+
const sha256 = await getHFFileSHA256(url, options.headers ?? {});
|
|
130
|
+
const hint = sha256 ? { sha256 } : undefined;
|
|
131
|
+
|
|
132
|
+
if (hint && (await this.sb.getSize(fileKey, hint)) !== -1) {
|
|
133
|
+
// File already in COS. Metadata is origin-local (OPFS), so it may be
|
|
134
|
+
// absent on a different origin or after a crash between write and
|
|
135
|
+
// writeMetadata. Ensure it exists before returning.
|
|
136
|
+
if (!(await this.getMetadata(fileKey))) {
|
|
137
|
+
const head = await fetch(url, {
|
|
138
|
+
method: 'HEAD',
|
|
139
|
+
...(options.headers ? { headers: options.headers } : {}),
|
|
140
|
+
});
|
|
141
|
+
const contentLength = head.headers.get('content-length');
|
|
142
|
+
const etag = (head.headers.get('etag') || '').replace(
|
|
143
|
+
/[^A-Za-z0-9]/g,
|
|
144
|
+
''
|
|
145
|
+
);
|
|
146
|
+
await this.writeMetadata(fileKey, {
|
|
147
|
+
originalURL: url,
|
|
148
|
+
originalSize: parseInt(contentLength ?? '0', 10),
|
|
149
|
+
etag,
|
|
150
|
+
sha256,
|
|
151
|
+
...(options.metadataAdditional ?? {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const response = await fetch(url, {
|
|
158
|
+
...(options.headers ? { headers: options.headers } : {}),
|
|
159
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!response.ok || !response.body) {
|
|
163
|
+
throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const contentLength = response.headers.get('content-length');
|
|
167
|
+
const etag = (response.headers.get('etag') || '').replace(
|
|
168
|
+
/[^A-Za-z0-9]/g,
|
|
169
|
+
''
|
|
170
|
+
);
|
|
171
|
+
const total = parseInt(contentLength ?? '0', 10);
|
|
172
|
+
|
|
173
|
+
const progressCallback = options.progressCallback;
|
|
174
|
+
let loaded = 0;
|
|
175
|
+
let lastProgressAt = 0;
|
|
176
|
+
|
|
177
|
+
const progressStream = new TransformStream<Uint8Array, Uint8Array>({
|
|
178
|
+
transform(chunk, controller) {
|
|
179
|
+
loaded += chunk.byteLength;
|
|
180
|
+
if (progressCallback) {
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
if (now - lastProgressAt > 100) {
|
|
183
|
+
lastProgressAt = now;
|
|
184
|
+
progressCallback({ loaded, total });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
controller.enqueue(chunk);
|
|
188
|
+
},
|
|
189
|
+
flush() {
|
|
190
|
+
progressCallback?.({ loaded, total: total || loaded });
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const metadata: CacheEntryMetadata = {
|
|
195
|
+
originalURL: url,
|
|
196
|
+
originalSize: total,
|
|
197
|
+
etag,
|
|
198
|
+
...(options.metadataAdditional ?? {}),
|
|
199
|
+
};
|
|
200
|
+
if (sha256) {
|
|
201
|
+
metadata.sha256 = sha256;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await this.sb.write(
|
|
205
|
+
fileKey,
|
|
206
|
+
response.body.pipeThrough(progressStream),
|
|
207
|
+
hint
|
|
208
|
+
);
|
|
209
|
+
await this.writeMetadata(fileKey, metadata);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Open a file in cache for reading
|
|
214
|
+
*
|
|
215
|
+
* @param nameOrURL The file name returned by `getNameFromURL()` or `list()`, or the original URL of the remote file
|
|
216
|
+
* @returns Blob, or null if file does not exist
|
|
217
|
+
*/
|
|
218
|
+
async open(nameOrURL: string): Promise<Blob | null> {
|
|
219
|
+
const hint1 = hintFromMetadata(await this.getMetadata(nameOrURL));
|
|
220
|
+
const direct = await this.sb.read(nameOrURL, hint1);
|
|
221
|
+
if (direct) return direct;
|
|
222
|
+
// also accept the original URL
|
|
223
|
+
const key = await urlToFileName(nameOrURL, '');
|
|
224
|
+
const hint2 = hintFromMetadata(await this.getMetadata(key));
|
|
225
|
+
return this.sb.read(key, hint2);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get the size of a file in stored cache
|
|
230
|
+
*
|
|
231
|
+
* NOTE: in case the download is stopped mid-way (i.e. user close browser tab), the file maybe corrupted, size maybe different from `metadata.originalSize`
|
|
232
|
+
*
|
|
233
|
+
* @param name The file name returned by `getNameFromURL()` or `list()`
|
|
234
|
+
* @returns number of bytes, or -1 if file does not exist
|
|
235
|
+
*/
|
|
236
|
+
async getSize(name: string): Promise<number> {
|
|
237
|
+
const hint = hintFromMetadata(await this.getMetadata(name));
|
|
238
|
+
return this.sb.getSize(name, hint);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get metadata of a cached file
|
|
243
|
+
*/
|
|
244
|
+
async getMetadata(name: string): Promise<CacheEntryMetadata | null> {
|
|
245
|
+
const blob = await this.sb.read(`${PREFIX_METADATA}${name}`);
|
|
246
|
+
const cachedSize = await this.sb.getSize(name);
|
|
247
|
+
if (!blob) {
|
|
248
|
+
return cachedSize > 0
|
|
249
|
+
? // files created by older version of wllama don't have metadata; polyfill it
|
|
250
|
+
{
|
|
251
|
+
etag: POLYFILL_ETAG,
|
|
252
|
+
originalSize: cachedSize,
|
|
253
|
+
originalURL: '',
|
|
254
|
+
}
|
|
255
|
+
: // cached file not found
|
|
256
|
+
null;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
return await new Response(blob).json();
|
|
260
|
+
} catch (e) {
|
|
261
|
+
// metadata corrupted; caller will re-download
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* List all files currently in cache
|
|
268
|
+
*/
|
|
269
|
+
async list(): Promise<CacheEntry[]> {
|
|
270
|
+
const all = await this.sb.list();
|
|
271
|
+
const metadataMap: Record<string, CacheEntryMetadata> = {};
|
|
272
|
+
|
|
273
|
+
for (const { key } of all) {
|
|
274
|
+
if (key.startsWith(PREFIX_METADATA)) {
|
|
275
|
+
const blob = await this.sb.read(key);
|
|
276
|
+
if (blob) {
|
|
277
|
+
const meta = await new Response(blob).json().catch(() => null);
|
|
278
|
+
metadataMap[key.slice(PREFIX_METADATA.length)] = meta;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const result: CacheEntry[] = [];
|
|
284
|
+
for (const { key, size } of all) {
|
|
285
|
+
if (!key.startsWith(PREFIX_METADATA)) {
|
|
286
|
+
result.push({
|
|
287
|
+
name: key,
|
|
288
|
+
size,
|
|
289
|
+
metadata: metadataMap[key] || {
|
|
290
|
+
originalSize: size,
|
|
291
|
+
originalURL: '',
|
|
292
|
+
etag: '',
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Clear all files currently in cache
|
|
302
|
+
*/
|
|
303
|
+
async clear(): Promise<void> {
|
|
304
|
+
await this.deleteMany(() => true);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Delete a single file in cache
|
|
309
|
+
*
|
|
310
|
+
* @param nameOrURL Can be either an URL or a name returned by `getNameFromURL()` or `list()`
|
|
311
|
+
*/
|
|
312
|
+
async delete(nameOrURL: string): Promise<void> {
|
|
313
|
+
const name2 = await this.getNameFromURL(nameOrURL);
|
|
314
|
+
await this.deleteMany(
|
|
315
|
+
(entry) => entry.name === nameOrURL || entry.name === name2
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Delete multiple files in cache.
|
|
321
|
+
*
|
|
322
|
+
* @param predicate A predicate like `array.filter(item => boolean)`
|
|
323
|
+
*/
|
|
324
|
+
async deleteMany(predicate: (e: CacheEntry) => boolean): Promise<void> {
|
|
325
|
+
const list = await this.list();
|
|
326
|
+
for (const item of list) {
|
|
327
|
+
if (predicate(item)) {
|
|
328
|
+
await this.sb.delete(item.name);
|
|
329
|
+
await this.sb.delete(`${PREFIX_METADATA}${item.name}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Write the metadata of the file to disk.
|
|
336
|
+
*/
|
|
337
|
+
async writeMetadata(
|
|
338
|
+
name: string,
|
|
339
|
+
metadata: CacheEntryMetadata
|
|
340
|
+
): Promise<void> {
|
|
341
|
+
const blob = new Blob([JSON.stringify(metadata)], { type: 'text/plain' });
|
|
342
|
+
await this.sb.write(`${PREFIX_METADATA}${name}`, blob.stream());
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export default CacheManager;
|
|
347
|
+
|
|
348
|
+
async function urlToFileName(url: string, prefix: string): Promise<string> {
|
|
349
|
+
const hashBuffer = await crypto.subtle.digest(
|
|
350
|
+
'SHA-1',
|
|
351
|
+
new TextEncoder().encode(url)
|
|
352
|
+
);
|
|
353
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
354
|
+
const hashHex = hashArray
|
|
355
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
356
|
+
.join('');
|
|
357
|
+
return `${prefix}${hashHex}_${url.split('/').pop()}`;
|
|
358
|
+
}
|
package/src/debug.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { WASM_SOURCE_MAP } from './wasm/source-map';
|
|
2
|
+
|
|
3
|
+
interface DecodedMap {
|
|
4
|
+
firstId: number;
|
|
5
|
+
funcNames: (string | null)[]; // indexed by (funcId - firstId)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const cache = new Map<string, DecodedMap>();
|
|
9
|
+
|
|
10
|
+
async function loadMap(buildKey: string): Promise<DecodedMap> {
|
|
11
|
+
if (cache.has(buildKey)) return cache.get(buildKey)!;
|
|
12
|
+
|
|
13
|
+
const b64 = WASM_SOURCE_MAP[buildKey];
|
|
14
|
+
if (!b64) throw new Error(`No source map for build "${buildKey}"`);
|
|
15
|
+
|
|
16
|
+
const gzipped = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
17
|
+
const ds = new DecompressionStream('gzip');
|
|
18
|
+
const writer = ds.writable.getWriter();
|
|
19
|
+
writer.write(gzipped);
|
|
20
|
+
writer.close();
|
|
21
|
+
const buf = await new Response(ds.readable).arrayBuffer();
|
|
22
|
+
const dv = new DataView(buf);
|
|
23
|
+
const bytes = new Uint8Array(buf);
|
|
24
|
+
|
|
25
|
+
const firstId = dv.getUint32(0, true);
|
|
26
|
+
const funcCount = dv.getUint32(4, true);
|
|
27
|
+
const numNames = dv.getUint32(8, true);
|
|
28
|
+
|
|
29
|
+
// Read name table
|
|
30
|
+
const td = new TextDecoder();
|
|
31
|
+
const names: string[] = [];
|
|
32
|
+
let pos = 12;
|
|
33
|
+
for (let i = 0; i < numNames; i++) {
|
|
34
|
+
const len = bytes[pos++];
|
|
35
|
+
names.push(td.decode(bytes.subarray(pos, pos + len)));
|
|
36
|
+
pos += len;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Read u16 index array
|
|
40
|
+
const funcNames: (string | null)[] = [];
|
|
41
|
+
for (let i = 0; i < funcCount; i++) {
|
|
42
|
+
const idx = dv.getUint16(pos, true);
|
|
43
|
+
pos += 2;
|
|
44
|
+
funcNames.push(idx === 0xffff ? null : names[idx]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const entry: DecodedMap = { firstId, funcNames };
|
|
48
|
+
cache.set(buildKey, entry);
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const Debug = {
|
|
53
|
+
/**
|
|
54
|
+
* Resolves a list of wasm function indices to their cleaned symbol names.
|
|
55
|
+
*/
|
|
56
|
+
decodeFuncIds: async (
|
|
57
|
+
funcIds: number[],
|
|
58
|
+
isCompatBuild: boolean
|
|
59
|
+
): Promise<{ funcId: number; name: string }[]> => {
|
|
60
|
+
const buildKey = isCompatBuild ? 'compat' : 'default';
|
|
61
|
+
const { firstId, funcNames } = await loadMap(buildKey);
|
|
62
|
+
return funcIds.map((funcId) => {
|
|
63
|
+
const i = funcId - firstId;
|
|
64
|
+
const name =
|
|
65
|
+
i >= 0 && i < funcNames.length && funcNames[i]
|
|
66
|
+
? funcNames[i]!
|
|
67
|
+
: '(unknown)';
|
|
68
|
+
return { funcId, name };
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* Annotates a wasm stack trace string with resolved function names.
|
|
73
|
+
*
|
|
74
|
+
* Example input from Chrome:
|
|
75
|
+
* at http://localhost:8080/esm/wasm/wllama.wasm:wasm-function[775]:0x74251
|
|
76
|
+
* at async blob:http://localhost:8080/53a863cc-7227-45cc-8594-ddbbf5257f20:317:28
|
|
77
|
+
*
|
|
78
|
+
* Example input from Firefox:
|
|
79
|
+
* @http://localhost:8080/esm/wasm/wllama.wasm:wasm-function[796]:0x7dfe2
|
|
80
|
+
* at wModuleInit/WebAssembly.promising/< (9b6a2acd-d909-44e2-b021-d42fb9087cfb:15:32) index.js:1433:45
|
|
81
|
+
*
|
|
82
|
+
* Example input from Safari:
|
|
83
|
+
* 2441@wasm-function[2441]
|
|
84
|
+
* at wrapper (d746f19e-4523-4f36-ba06-d0969acc0b05:22:126009)
|
|
85
|
+
*
|
|
86
|
+
* Example output:
|
|
87
|
+
* wasm-func[775] (server_response::send)
|
|
88
|
+
*/
|
|
89
|
+
decodeStackTrace: async (
|
|
90
|
+
stack: string,
|
|
91
|
+
isCompatBuild: boolean
|
|
92
|
+
): Promise<string> => {
|
|
93
|
+
// match wasm-function[N] from Chrome, Firefox and Safari stack formats
|
|
94
|
+
const re = /wasm-function\[(\d+)\]/g;
|
|
95
|
+
const funcIds = [
|
|
96
|
+
...new Set([...stack.matchAll(re)].map((m) => parseInt(m[1]))),
|
|
97
|
+
];
|
|
98
|
+
if (funcIds.length === 0) return stack;
|
|
99
|
+
|
|
100
|
+
const resolved = await Debug.decodeFuncIds(funcIds, isCompatBuild);
|
|
101
|
+
|
|
102
|
+
return resolved
|
|
103
|
+
.map((r) => {
|
|
104
|
+
if (r.name === '(unknown)') {
|
|
105
|
+
return ` wasm-func[${r.funcId}] (unknown)`;
|
|
106
|
+
}
|
|
107
|
+
return ` wasm-func[${r.funcId}] (${r.name})`;
|
|
108
|
+
})
|
|
109
|
+
.join('\n');
|
|
110
|
+
},
|
|
111
|
+
};
|