asm streaming: recycle the 64 KB output buffer after each flush
A throttled forever-counter on the asm tier ran for ~5.7 s and
then trapped 'index out of bounds' — the WAT writes output to a
fixed 64 KB region at 0x10000–0x1FFFF, and a fast (display X)
(newline) loop accumulates faster than the buffer can drain. After
about 6,400 ticks at ~10 chars each, overflowed the
region into the source buffer at 0x20000 and the next i32.store8
fell off linear memory.
Fix is a contract change on emit_chunk: its signature picks up an
i32 return — 1 tells the WAT to recycle (zero output_len AND
flush_start), 0 keeps the original 'just advance flush_start'
semantics so callers that read lumbda_output_ptr/len after eval
still see the full buffer.
The asm loader returns 1 from emit_chunk, accumulating every
flushed slice into refs.accumulated. evalLisp's return value is now
refs.accumulated + the trailing (still-unflushed) buffer slice
rather than just the lumbda_output_ptr/len slice — caller still
gets the complete output, the WAT-side buffer just keeps recycling.
Test stubs already declared emit_chunk() {} which returns
undefined — JS->wasm i32 coercion turns that into 0, preserving
the no-recycle behavior they expected. unit/integration suites
(31 tests total) still pass.
Trailing flush in lumbda_eval also honors the return value: if the
host consumed, zero both offsets so the final lumbda_output_ptr/len
read returns 0 bytes (loader already accumulated the trailing
slice — no need to re-deliver). Earlier draft of this patch
double-emitted the trailing slice because we read it through both
the emit_chunk path and the final-buffer path.
This commit is contained in:
parent
25d2765ed9
commit
cdb4fc9715
8 changed files with 226 additions and 59 deletions
|
|
@ -13,7 +13,16 @@ async function _bootstrap() {
|
|||
|
||||
// Closure-captured holder so the imported function can see the
|
||||
// instance's memory after instantiation completes.
|
||||
const refs = { instance: null, bendUrl: null, currentOnChunk: null };
|
||||
// accumulated holds the full eval output for evalLisp's return
|
||||
// value — the WAT now recycles the 64 KB output buffer after
|
||||
// each emit_chunk so a long-running printer doesn't overflow,
|
||||
// but the loader must still hand the caller the complete text.
|
||||
const refs = {
|
||||
instance: null,
|
||||
bendUrl: null,
|
||||
currentOnChunk: null,
|
||||
accumulated: "",
|
||||
};
|
||||
|
||||
const importObj = {
|
||||
env: {
|
||||
|
|
@ -45,15 +54,31 @@ async function _bootstrap() {
|
|||
return err.length;
|
||||
}
|
||||
},
|
||||
// emit_chunk(ptr, len) — WAT's out_char calls this on every
|
||||
// newline (and lumbda_eval calls it once more at the end for
|
||||
// trailing non-newline content) so the worker can postMessage
|
||||
// a chunk to the playground panel as work happens, matching
|
||||
// the C-tier Module.print + pyodide _StreamingStdout streams.
|
||||
// emit_chunk(ptr, len) → i32 — WAT's out_char calls this
|
||||
// on every newline (and lumbda_eval calls it once more at
|
||||
// the end for trailing non-newline content) so the
|
||||
// worker can postMessage a chunk to the playground panel
|
||||
// as work happens, matching the C-tier Module.print +
|
||||
// pyodide _StreamingStdout streams.
|
||||
//
|
||||
// Returns 1 to tell the WAT to RECYCLE the output region
|
||||
// (zero output_len + flush_start). The output buffer is
|
||||
// a fixed 64 KB slot at 0x10000–0x1FFFF; a tight printing
|
||||
// loop (e.g. a forever counter) blew past it into the
|
||||
// source buffer at 0x20000 and crashed with "index out
|
||||
// of bounds" after ~6,400 ticks. The loader accumulates
|
||||
// every chunk into refs.accumulated so evalLisp can still
|
||||
// return the full output to its caller even though the
|
||||
// WAT-side buffer keeps recycling.
|
||||
emit_chunk(ptr, len) {
|
||||
if (!refs.currentOnChunk || len <= 0) return;
|
||||
const mem = new Uint8Array(refs.instance.exports.memory.buffer, ptr, len);
|
||||
refs.currentOnChunk(new TextDecoder().decode(mem));
|
||||
if (len > 0) {
|
||||
const mem = new Uint8Array(
|
||||
refs.instance.exports.memory.buffer, ptr, len);
|
||||
const text = new TextDecoder().decode(mem);
|
||||
refs.accumulated += text;
|
||||
if (refs.currentOnChunk) refs.currentOnChunk(text);
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -73,6 +98,7 @@ async function _bootstrap() {
|
|||
const mem = new Uint8Array(exp.memory.buffer);
|
||||
mem.set(srcBytes, srcPtr);
|
||||
refs.currentOnChunk = onChunk || null;
|
||||
refs.accumulated = "";
|
||||
try {
|
||||
exp.lumbda_eval(srcBytes.length);
|
||||
} catch (e) {
|
||||
|
|
@ -80,9 +106,17 @@ async function _bootstrap() {
|
|||
return `error: ${e.message}`;
|
||||
}
|
||||
refs.currentOnChunk = null;
|
||||
// Trailing buffer content the WAT didn't already flush
|
||||
// (e.g. a final value's repr without a trailing newline)
|
||||
// — append to the accumulated stream so the caller still
|
||||
// gets the complete output, just as if the buffer hadn't
|
||||
// been recycled.
|
||||
const outPtr = exp.lumbda_output_ptr();
|
||||
const outLen = exp.lumbda_output_len();
|
||||
return dec.decode(new Uint8Array(exp.memory.buffer, outPtr, outLen));
|
||||
const trailing = outLen > 0
|
||||
? dec.decode(new Uint8Array(exp.memory.buffer, outPtr, outLen))
|
||||
: "";
|
||||
return refs.accumulated + trailing;
|
||||
},
|
||||
setBendUrl(url) { refs.bendUrl = url || null; },
|
||||
heapStats() {
|
||||
|
|
|
|||
|
|
@ -46,14 +46,22 @@
|
|||
(import "env" "bend_call"
|
||||
(func $js_bend_call (param i32 i32 i32) (result i32)))
|
||||
|
||||
;; emit_chunk(ptr, len) — flush a slice of the output buffer to JS
|
||||
;; for streaming. Called from out_char whenever a newline is emitted
|
||||
;; (and from lumbda_eval at the very end for any trailing content
|
||||
;; without a newline). Loader's host function forwards the bytes to
|
||||
;; the current onChunk callback so the worker can postMessage the
|
||||
;; chunk to the playground panel as work happens.
|
||||
;; emit_chunk(ptr, len) → i32 — flush a slice of the output buffer
|
||||
;; to JS for streaming. Called from out_char whenever a newline is
|
||||
;; emitted (and from lumbda_eval at the very end for any trailing
|
||||
;; content without a newline). Loader's host function forwards the
|
||||
;; bytes to the current onChunk callback so the worker can
|
||||
;; postMessage the chunk to the playground panel as work happens.
|
||||
;;
|
||||
;; Returns 1 if the host consumed the chunk and the WAT should
|
||||
;; recycle the buffer (zero output_len + flush_start so a long
|
||||
;; running loop doesn't overflow the 64 KB output region into the
|
||||
;; source buffer at 0x20000). Returns 0 (or undefined → coerced
|
||||
;; to 0) if the host stub didn't consume — tests + node harnesses
|
||||
;; that read lumbda_output_ptr/len after eval keep their full
|
||||
;; buffer behavior, the playground loader recycles aggressively.
|
||||
(import "env" "emit_chunk"
|
||||
(func $js_emit_chunk (param i32 i32)))
|
||||
(func $js_emit_chunk (param i32 i32) (result i32)))
|
||||
|
||||
;; ─── Memory & exports ──────────────────────────────────────────
|
||||
(memory (export "memory") 32 4096) ;; 32 pages = 2 MB initial, grow to 256 MB
|
||||
|
|
@ -1290,19 +1298,32 @@
|
|||
|
||||
;; ─── Output buffer ─────────────────────────────────────────────
|
||||
(func $out_char (param $c i32)
|
||||
(local $consumed i32)
|
||||
(i32.store8
|
||||
(i32.add (i32.const 0x10000) (global.get $output_len))
|
||||
(local.get $c))
|
||||
(global.set $output_len (i32.add (global.get $output_len) (i32.const 1)))
|
||||
;; Newline flushes the slice [flush_start, output_len) to JS so
|
||||
;; the playground panel can render line-by-line during eval
|
||||
;; instead of waiting for the full evalLisp to return.
|
||||
;; instead of waiting for the full evalLisp to return. If the
|
||||
;; host consumed the chunk (return value = 1) we recycle the
|
||||
;; output region by zeroing both offsets — a tight printing
|
||||
;; loop overflowed the 64 KB buffer at 0x20000 otherwise. If
|
||||
;; the host returned 0 (e.g. test stubs that don't drain), we
|
||||
;; just advance flush_start so a later lumbda_output_ptr/len
|
||||
;; read still sees the full accumulated buffer.
|
||||
(if (i32.eq (local.get $c) (i32.const 10))
|
||||
(then
|
||||
(call $js_emit_chunk
|
||||
(i32.add (i32.const 0x10000) (global.get $flush_start))
|
||||
(i32.sub (global.get $output_len) (global.get $flush_start)))
|
||||
(global.set $flush_start (global.get $output_len)))))
|
||||
(local.set $consumed
|
||||
(call $js_emit_chunk
|
||||
(i32.add (i32.const 0x10000) (global.get $flush_start))
|
||||
(i32.sub (global.get $output_len) (global.get $flush_start))))
|
||||
(if (local.get $consumed)
|
||||
(then
|
||||
(global.set $output_len (i32.const 0))
|
||||
(global.set $flush_start (i32.const 0)))
|
||||
(else
|
||||
(global.set $flush_start (global.get $output_len)))))))
|
||||
|
||||
(func $out_str (param $ptr i32) (param $len i32)
|
||||
(local $i i32)
|
||||
|
|
@ -4256,13 +4277,23 @@
|
|||
;; Trailing-content flush — print_value's repr doesn't end with a
|
||||
;; newline, so any bytes past $flush_start would otherwise miss
|
||||
;; the streaming path and only reach the UI through the final
|
||||
;; lumbda_output_ptr / lumbda_output_len read.
|
||||
(if (i32.gt_u (global.get $output_len) (global.get $flush_start))
|
||||
(then
|
||||
(call $js_emit_chunk
|
||||
(i32.add (i32.const 0x10000) (global.get $flush_start))
|
||||
(i32.sub (global.get $output_len) (global.get $flush_start)))
|
||||
(global.set $flush_start (global.get $output_len))))
|
||||
;; lumbda_output_ptr / lumbda_output_len read. If the host
|
||||
;; consumed it, recycle the buffer so the final output_ptr/len
|
||||
;; read returns 0 bytes — the loader already accumulated the
|
||||
;; trailing slice via emit_chunk, no need to re-deliver. If the
|
||||
;; host didn't consume (return 0), advance flush_start so a
|
||||
;; later final-buffer read still sees the content.
|
||||
(block $skip_trail
|
||||
(br_if $skip_trail
|
||||
(i32.le_u (global.get $output_len) (global.get $flush_start)))
|
||||
(if (call $js_emit_chunk
|
||||
(i32.add (i32.const 0x10000) (global.get $flush_start))
|
||||
(i32.sub (global.get $output_len) (global.get $flush_start)))
|
||||
(then
|
||||
(global.set $output_len (i32.const 0))
|
||||
(global.set $flush_start (i32.const 0)))
|
||||
(else
|
||||
(global.set $flush_start (global.get $output_len)))))
|
||||
;; Trigger GC when the heap exceeds 60% of available memory. This is
|
||||
;; the only safe collection point — the eval call stack has unwound,
|
||||
;; so the only roots are the globals the collector knows about.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,16 @@ async function _bootstrap() {
|
|||
|
||||
// Closure-captured holder so the imported function can see the
|
||||
// instance's memory after instantiation completes.
|
||||
const refs = { instance: null, bendUrl: null, currentOnChunk: null };
|
||||
// accumulated holds the full eval output for evalLisp's return
|
||||
// value — the WAT now recycles the 64 KB output buffer after
|
||||
// each emit_chunk so a long-running printer doesn't overflow,
|
||||
// but the loader must still hand the caller the complete text.
|
||||
const refs = {
|
||||
instance: null,
|
||||
bendUrl: null,
|
||||
currentOnChunk: null,
|
||||
accumulated: "",
|
||||
};
|
||||
|
||||
const importObj = {
|
||||
env: {
|
||||
|
|
@ -45,15 +54,31 @@ async function _bootstrap() {
|
|||
return err.length;
|
||||
}
|
||||
},
|
||||
// emit_chunk(ptr, len) — WAT's out_char calls this on every
|
||||
// newline (and lumbda_eval calls it once more at the end for
|
||||
// trailing non-newline content) so the worker can postMessage
|
||||
// a chunk to the playground panel as work happens, matching
|
||||
// the C-tier Module.print + pyodide _StreamingStdout streams.
|
||||
// emit_chunk(ptr, len) → i32 — WAT's out_char calls this
|
||||
// on every newline (and lumbda_eval calls it once more at
|
||||
// the end for trailing non-newline content) so the
|
||||
// worker can postMessage a chunk to the playground panel
|
||||
// as work happens, matching the C-tier Module.print +
|
||||
// pyodide _StreamingStdout streams.
|
||||
//
|
||||
// Returns 1 to tell the WAT to RECYCLE the output region
|
||||
// (zero output_len + flush_start). The output buffer is
|
||||
// a fixed 64 KB slot at 0x10000–0x1FFFF; a tight printing
|
||||
// loop (e.g. a forever counter) blew past it into the
|
||||
// source buffer at 0x20000 and crashed with "index out
|
||||
// of bounds" after ~6,400 ticks. The loader accumulates
|
||||
// every chunk into refs.accumulated so evalLisp can still
|
||||
// return the full output to its caller even though the
|
||||
// WAT-side buffer keeps recycling.
|
||||
emit_chunk(ptr, len) {
|
||||
if (!refs.currentOnChunk || len <= 0) return;
|
||||
const mem = new Uint8Array(refs.instance.exports.memory.buffer, ptr, len);
|
||||
refs.currentOnChunk(new TextDecoder().decode(mem));
|
||||
if (len > 0) {
|
||||
const mem = new Uint8Array(
|
||||
refs.instance.exports.memory.buffer, ptr, len);
|
||||
const text = new TextDecoder().decode(mem);
|
||||
refs.accumulated += text;
|
||||
if (refs.currentOnChunk) refs.currentOnChunk(text);
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -73,6 +98,7 @@ async function _bootstrap() {
|
|||
const mem = new Uint8Array(exp.memory.buffer);
|
||||
mem.set(srcBytes, srcPtr);
|
||||
refs.currentOnChunk = onChunk || null;
|
||||
refs.accumulated = "";
|
||||
try {
|
||||
exp.lumbda_eval(srcBytes.length);
|
||||
} catch (e) {
|
||||
|
|
@ -80,9 +106,17 @@ async function _bootstrap() {
|
|||
return `error: ${e.message}`;
|
||||
}
|
||||
refs.currentOnChunk = null;
|
||||
// Trailing buffer content the WAT didn't already flush
|
||||
// (e.g. a final value's repr without a trailing newline)
|
||||
// — append to the accumulated stream so the caller still
|
||||
// gets the complete output, just as if the buffer hadn't
|
||||
// been recycled.
|
||||
const outPtr = exp.lumbda_output_ptr();
|
||||
const outLen = exp.lumbda_output_len();
|
||||
return dec.decode(new Uint8Array(exp.memory.buffer, outPtr, outLen));
|
||||
const trailing = outLen > 0
|
||||
? dec.decode(new Uint8Array(exp.memory.buffer, outPtr, outLen))
|
||||
: "";
|
||||
return refs.accumulated + trailing;
|
||||
},
|
||||
setBendUrl(url) { refs.bendUrl = url || null; },
|
||||
heapStats() {
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -13,7 +13,16 @@ async function _bootstrap() {
|
|||
|
||||
// Closure-captured holder so the imported function can see the
|
||||
// instance's memory after instantiation completes.
|
||||
const refs = { instance: null, bendUrl: null, currentOnChunk: null };
|
||||
// accumulated holds the full eval output for evalLisp's return
|
||||
// value — the WAT now recycles the 64 KB output buffer after
|
||||
// each emit_chunk so a long-running printer doesn't overflow,
|
||||
// but the loader must still hand the caller the complete text.
|
||||
const refs = {
|
||||
instance: null,
|
||||
bendUrl: null,
|
||||
currentOnChunk: null,
|
||||
accumulated: "",
|
||||
};
|
||||
|
||||
const importObj = {
|
||||
env: {
|
||||
|
|
@ -45,15 +54,31 @@ async function _bootstrap() {
|
|||
return err.length;
|
||||
}
|
||||
},
|
||||
// emit_chunk(ptr, len) — WAT's out_char calls this on every
|
||||
// newline (and lumbda_eval calls it once more at the end for
|
||||
// trailing non-newline content) so the worker can postMessage
|
||||
// a chunk to the playground panel as work happens, matching
|
||||
// the C-tier Module.print + pyodide _StreamingStdout streams.
|
||||
// emit_chunk(ptr, len) → i32 — WAT's out_char calls this
|
||||
// on every newline (and lumbda_eval calls it once more at
|
||||
// the end for trailing non-newline content) so the
|
||||
// worker can postMessage a chunk to the playground panel
|
||||
// as work happens, matching the C-tier Module.print +
|
||||
// pyodide _StreamingStdout streams.
|
||||
//
|
||||
// Returns 1 to tell the WAT to RECYCLE the output region
|
||||
// (zero output_len + flush_start). The output buffer is
|
||||
// a fixed 64 KB slot at 0x10000–0x1FFFF; a tight printing
|
||||
// loop (e.g. a forever counter) blew past it into the
|
||||
// source buffer at 0x20000 and crashed with "index out
|
||||
// of bounds" after ~6,400 ticks. The loader accumulates
|
||||
// every chunk into refs.accumulated so evalLisp can still
|
||||
// return the full output to its caller even though the
|
||||
// WAT-side buffer keeps recycling.
|
||||
emit_chunk(ptr, len) {
|
||||
if (!refs.currentOnChunk || len <= 0) return;
|
||||
const mem = new Uint8Array(refs.instance.exports.memory.buffer, ptr, len);
|
||||
refs.currentOnChunk(new TextDecoder().decode(mem));
|
||||
if (len > 0) {
|
||||
const mem = new Uint8Array(
|
||||
refs.instance.exports.memory.buffer, ptr, len);
|
||||
const text = new TextDecoder().decode(mem);
|
||||
refs.accumulated += text;
|
||||
if (refs.currentOnChunk) refs.currentOnChunk(text);
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -73,6 +98,7 @@ async function _bootstrap() {
|
|||
const mem = new Uint8Array(exp.memory.buffer);
|
||||
mem.set(srcBytes, srcPtr);
|
||||
refs.currentOnChunk = onChunk || null;
|
||||
refs.accumulated = "";
|
||||
try {
|
||||
exp.lumbda_eval(srcBytes.length);
|
||||
} catch (e) {
|
||||
|
|
@ -80,9 +106,17 @@ async function _bootstrap() {
|
|||
return `error: ${e.message}`;
|
||||
}
|
||||
refs.currentOnChunk = null;
|
||||
// Trailing buffer content the WAT didn't already flush
|
||||
// (e.g. a final value's repr without a trailing newline)
|
||||
// — append to the accumulated stream so the caller still
|
||||
// gets the complete output, just as if the buffer hadn't
|
||||
// been recycled.
|
||||
const outPtr = exp.lumbda_output_ptr();
|
||||
const outLen = exp.lumbda_output_len();
|
||||
return dec.decode(new Uint8Array(exp.memory.buffer, outPtr, outLen));
|
||||
const trailing = outLen > 0
|
||||
? dec.decode(new Uint8Array(exp.memory.buffer, outPtr, outLen))
|
||||
: "";
|
||||
return refs.accumulated + trailing;
|
||||
},
|
||||
setBendUrl(url) { refs.bendUrl = url || null; },
|
||||
heapStats() {
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -13,7 +13,16 @@ async function _bootstrap() {
|
|||
|
||||
// Closure-captured holder so the imported function can see the
|
||||
// instance's memory after instantiation completes.
|
||||
const refs = { instance: null, bendUrl: null, currentOnChunk: null };
|
||||
// accumulated holds the full eval output for evalLisp's return
|
||||
// value — the WAT now recycles the 64 KB output buffer after
|
||||
// each emit_chunk so a long-running printer doesn't overflow,
|
||||
// but the loader must still hand the caller the complete text.
|
||||
const refs = {
|
||||
instance: null,
|
||||
bendUrl: null,
|
||||
currentOnChunk: null,
|
||||
accumulated: "",
|
||||
};
|
||||
|
||||
const importObj = {
|
||||
env: {
|
||||
|
|
@ -45,15 +54,31 @@ async function _bootstrap() {
|
|||
return err.length;
|
||||
}
|
||||
},
|
||||
// emit_chunk(ptr, len) — WAT's out_char calls this on every
|
||||
// newline (and lumbda_eval calls it once more at the end for
|
||||
// trailing non-newline content) so the worker can postMessage
|
||||
// a chunk to the playground panel as work happens, matching
|
||||
// the C-tier Module.print + pyodide _StreamingStdout streams.
|
||||
// emit_chunk(ptr, len) → i32 — WAT's out_char calls this
|
||||
// on every newline (and lumbda_eval calls it once more at
|
||||
// the end for trailing non-newline content) so the
|
||||
// worker can postMessage a chunk to the playground panel
|
||||
// as work happens, matching the C-tier Module.print +
|
||||
// pyodide _StreamingStdout streams.
|
||||
//
|
||||
// Returns 1 to tell the WAT to RECYCLE the output region
|
||||
// (zero output_len + flush_start). The output buffer is
|
||||
// a fixed 64 KB slot at 0x10000–0x1FFFF; a tight printing
|
||||
// loop (e.g. a forever counter) blew past it into the
|
||||
// source buffer at 0x20000 and crashed with "index out
|
||||
// of bounds" after ~6,400 ticks. The loader accumulates
|
||||
// every chunk into refs.accumulated so evalLisp can still
|
||||
// return the full output to its caller even though the
|
||||
// WAT-side buffer keeps recycling.
|
||||
emit_chunk(ptr, len) {
|
||||
if (!refs.currentOnChunk || len <= 0) return;
|
||||
const mem = new Uint8Array(refs.instance.exports.memory.buffer, ptr, len);
|
||||
refs.currentOnChunk(new TextDecoder().decode(mem));
|
||||
if (len > 0) {
|
||||
const mem = new Uint8Array(
|
||||
refs.instance.exports.memory.buffer, ptr, len);
|
||||
const text = new TextDecoder().decode(mem);
|
||||
refs.accumulated += text;
|
||||
if (refs.currentOnChunk) refs.currentOnChunk(text);
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -73,6 +98,7 @@ async function _bootstrap() {
|
|||
const mem = new Uint8Array(exp.memory.buffer);
|
||||
mem.set(srcBytes, srcPtr);
|
||||
refs.currentOnChunk = onChunk || null;
|
||||
refs.accumulated = "";
|
||||
try {
|
||||
exp.lumbda_eval(srcBytes.length);
|
||||
} catch (e) {
|
||||
|
|
@ -80,9 +106,17 @@ async function _bootstrap() {
|
|||
return `error: ${e.message}`;
|
||||
}
|
||||
refs.currentOnChunk = null;
|
||||
// Trailing buffer content the WAT didn't already flush
|
||||
// (e.g. a final value's repr without a trailing newline)
|
||||
// — append to the accumulated stream so the caller still
|
||||
// gets the complete output, just as if the buffer hadn't
|
||||
// been recycled.
|
||||
const outPtr = exp.lumbda_output_ptr();
|
||||
const outLen = exp.lumbda_output_len();
|
||||
return dec.decode(new Uint8Array(exp.memory.buffer, outPtr, outLen));
|
||||
const trailing = outLen > 0
|
||||
? dec.decode(new Uint8Array(exp.memory.buffer, outPtr, outLen))
|
||||
: "";
|
||||
return refs.accumulated + trailing;
|
||||
},
|
||||
setBendUrl(url) { refs.bendUrl = url || null; },
|
||||
heapStats() {
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue