No copy stone blobs for wota messages #7

Open
opened 2025-05-25 21:14:58 +00:00 by john · 1 comment
Owner

When sending messages locally, stone objects can just be passed by reference; this would work for a lot of things, but the first obvious target is the arraybuffers working, then we can concern ourselves with others. If a stone array is sent over the network via nota, it would not be stone on the other side, but arrays arriving via wota may be stone.

When sending messages locally, stone objects can just be passed by reference; this would work for a lot of things, but the first obvious target is the arraybuffers working, then we can concern ourselves with others. If a stone array is sent over the network via nota, it would not be stone on the other side, but arrays arriving via wota may be stone.
john changed title from Add Stone to Enhance stone to lower copying 2025-06-06 15:32:26 +00:00
Author
Owner

Below is a concrete recipe for adding “stone‐blob” sharing into your Wota messaging and actor teardown without ever having to round-trip through full JS decode. The key ideas are:

Treat stone-blobs as first-class, ref-counted handles, not raw bytes.
Extend Wota with a new “stone-blob” tag so your encoder/decoder can bump or consume refcounts without copying big buffers.
Hook into actor teardown to sweep any live stone-blob handles still queued and drop their counts in C—no JS decode needed.

  1. Extend your C blob type

// in qjs_blob.h
typedef struct blob {
uint8_t *data; // actual bytes, NULL for stone-only
size_t length_bits; // bit-length of data buffer
int refcount; // new
bool stone; // new: true if immutable & shareable
} blob;

// factory for a new mutable blob
blob *blob_new(size_t bits) {
blob *b = malloc(sizeof *b);
b->data = calloc((bits+7)/8,1);
b->length_bits = bits;
b->refcount = 1;
b->stone = false;
return b;
}
// factory for a stone blob handle, assume data already exists
blob *blob_wrap(uint8_t *data, size_t bits) {
blob *b = malloc(sizeof *b);
b->data = data;
b->length_bits = bits;
b->refcount = 1;
b->stone = true;
return b;
}

static inline void blob_incref(blob *b) {
if (b) atomic_fetch_add(&b->refcount,1);
}
static void blob_destroy(blob *b) {
if (!b) return;
if (atomic_fetch_sub(&b->refcount,1) > 1) {
// still live somewhere else
return;
}
// actually free once refcount hits zero
if (b->data && !b->stone) free(b->data);
free(b);
}
– now blob_destroy unconditionally does a ref-decrement, and only frees buffer+struct when count hits zero.

  1. Signal stone-blobs in Wota encode

Modify your wota_encode_value when you see a JS-blob:

case JS_TAG_OBJECT: {
  • if (js_is_blob(ctx, replaced)) {
    
  •   size_t buf_len; void *buf_data = js_get_blob_data(...);
    
  •   wota_write_blob(&enc->wb, buf_len*8, (const char*)buf_data);
    
  •   break;
    
  • }
    
  • if (js_is_blob(ctx, replaced)) {
    
  •   blob *b = js_get_blob_handle(ctx, replaced);
    
  •   if (b->stone) {
    
  •     // stone-blob marker: zero length, then 64bit pointer
    
  •     wota_write_array(&enc->wb, 0);   // zero items
    
  •     wota_write_record(&enc->wb, 1);  // use record payload to hold a word
    
  •     uint64_t ptr = (uint64_t)(uintptr_t)b;
    
  •     wota_write_int_word(&enc->wb, (int64_t)ptr);
    
  •     // bump C-side refcount for mailbox
    
  •     blob_incref(b);
    
  •   } else {
    
  •     size_t buf_len; void *buf_data = js_get_blob_data(...);
    
  •     wota_write_blob(&enc->wb, buf_len*8, (const char*)buf_data);
    
  •   }
    
  •   break;
    
  • }
    

Here we encode a 0-length array as a special tag, then stick the raw blob* pointer in a 64-bit slot. We also blob_incref(b) so the actor’s mailbox owns a reference.

  1. Decode stone-blobs as shared handles

In decode_wota_value, catch that special case before the normal WOTA_BLOB branch:

case WOTA_ARR: {
  long long c; data_ptr = wota_read_array(&c,data_ptr);
  • if (c == 0) {
    
  •    // stone-blob path: next is our pointer word
    
  •    int64_t p; data_ptr = wota_read_int(&p,data_ptr);
    
  •    blob *b = (blob*)(uintptr_t)p;
    
  •    blob_incref(b);  // actor’s mailbox holds one
    
  •    *out_val = js_new_blob_handle(ctx, b);
    
  •    break;
    
  • }
    JSValue arr = JS_NewArray(ctx);
    // … normal array decode …
    

And leave your usual WOTA_BLOB code below unchanged for real data-blobs.

  1. Mailbox cleanup on actor death

Your existing actor_free already does:

for (int i = 0; i < arrlen(actor->letters); i++) {
if (actor->letters[i].type == LETTER_BLOB) {
blob_destroy(actor->letters[i].blob_data);
} else if (…) { … }
}
Because blob_destroy now decrements refcounts rather than forcibly freeing raw data, you don’t need to decode or touch JS at all: every stone-blob in the mailbox is simply dropped via its refcount--, and only truly freed if no other actor or wrapper holds it.

  1. What changes with your new GC

Wrappers in the bump arena
Your JS-side blob wrappers are still alloc’d in your bump arena, so they move on compaction—and when their refcount hits zero, your js_zone_free (no-op) just leaves a hole until the next semi-space GC.
Raw blob data stays separate
The actual large buffers remain out on the malloc heap (or a long-lived “blob_region”) and are freed only when the final blob_destroy sees refcount == 0.
No extra decode cost
You never run decode_wota_value on a big blob payload—stone-blobs skip the bit-copy entirely and just point at the existing data.
Summary
Tag stone blobs in your Wota encoder with a zero-length record + pointer.
Bump raw-blob refcount in the encoder, and again in the decoder, so the mailbox and the target actor both share ownership.
blob_destroy always decrefs; only frees when the count reaches zero.
Actor teardown already crawls actor->letters and calls blob_destroy on each—no JS-level decode needed.
This gives you zero-copy, ref-counted sharing of large stone-blobs across actors, full compatibility with your bump+copying GC for JS wrappers, and trivial mailbox cleanup in pure C.

Below is a concrete recipe for adding “stone‐blob” sharing into your Wota messaging and actor teardown without ever having to round-trip through full JS decode. The key ideas are: Treat stone-blobs as first-class, ref-counted handles, not raw bytes. Extend Wota with a new “stone-blob” tag so your encoder/decoder can bump or consume refcounts without copying big buffers. Hook into actor teardown to sweep any live stone-blob handles still queued and drop their counts in C—no JS decode needed. 1. Extend your C blob type // in qjs_blob.h typedef struct blob { uint8_t *data; // actual bytes, NULL for stone-only size_t length_bits; // bit-length of data buffer int refcount; // new bool stone; // new: true if immutable & shareable } blob; // factory for a new mutable blob blob *blob_new(size_t bits) { blob *b = malloc(sizeof *b); b->data = calloc((bits+7)/8,1); b->length_bits = bits; b->refcount = 1; b->stone = false; return b; } // factory for a *stone* blob handle, assume data already exists blob *blob_wrap(uint8_t *data, size_t bits) { blob *b = malloc(sizeof *b); b->data = data; b->length_bits = bits; b->refcount = 1; b->stone = true; return b; } static inline void blob_incref(blob *b) { if (b) atomic_fetch_add(&b->refcount,1); } static void blob_destroy(blob *b) { if (!b) return; if (atomic_fetch_sub(&b->refcount,1) > 1) { // still live somewhere else return; } // actually free once refcount hits zero if (b->data && !b->stone) free(b->data); free(b); } – now blob_destroy unconditionally does a ref-decrement, and only frees buffer+struct when count hits zero. 2. Signal stone-blobs in Wota encode Modify your wota_encode_value when you see a JS-blob: case JS_TAG_OBJECT: { - if (js_is_blob(ctx, replaced)) { - size_t buf_len; void *buf_data = js_get_blob_data(...); - wota_write_blob(&enc->wb, buf_len*8, (const char*)buf_data); - break; - } + if (js_is_blob(ctx, replaced)) { + blob *b = js_get_blob_handle(ctx, replaced); + if (b->stone) { + // stone-blob marker: zero length, then 64bit pointer + wota_write_array(&enc->wb, 0); // zero items + wota_write_record(&enc->wb, 1); // use record payload to hold a word + uint64_t ptr = (uint64_t)(uintptr_t)b; + wota_write_int_word(&enc->wb, (int64_t)ptr); + // bump C-side refcount for mailbox + blob_incref(b); + } else { + size_t buf_len; void *buf_data = js_get_blob_data(...); + wota_write_blob(&enc->wb, buf_len*8, (const char*)buf_data); + } + break; + } Here we encode a 0-length array as a special tag, then stick the raw blob* pointer in a 64-bit slot. We also blob_incref(b) so the actor’s mailbox owns a reference. 3. Decode stone-blobs as shared handles In decode_wota_value, catch that special case before the normal WOTA_BLOB branch: case WOTA_ARR: { long long c; data_ptr = wota_read_array(&c,data_ptr); + if (c == 0) { + // stone-blob path: next is our pointer word + int64_t p; data_ptr = wota_read_int(&p,data_ptr); + blob *b = (blob*)(uintptr_t)p; + blob_incref(b); // actor’s mailbox holds one + *out_val = js_new_blob_handle(ctx, b); + break; + } JSValue arr = JS_NewArray(ctx); // … normal array decode … And leave your usual WOTA_BLOB code below unchanged for real data-blobs. 4. Mailbox cleanup on actor death Your existing actor_free already does: for (int i = 0; i < arrlen(actor->letters); i++) { if (actor->letters[i].type == LETTER_BLOB) { blob_destroy(actor->letters[i].blob_data); } else if (…) { … } } Because blob_destroy now decrements refcounts rather than forcibly freeing raw data, you don’t need to decode or touch JS at all: every stone-blob in the mailbox is simply dropped via its refcount--, and only truly freed if no other actor or wrapper holds it. 5. What changes with your new GC Wrappers in the bump arena Your JS-side blob wrappers are still alloc’d in your bump arena, so they move on compaction—and when their refcount hits zero, your js_zone_free (no-op) just leaves a hole until the next semi-space GC. Raw blob data stays separate The actual large buffers remain out on the malloc heap (or a long-lived “blob_region”) and are freed only when the final blob_destroy sees refcount == 0. No extra decode cost You never run decode_wota_value on a big blob payload—stone-blobs skip the bit-copy entirely and just point at the existing data. Summary Tag stone blobs in your Wota encoder with a zero-length record + pointer. Bump raw-blob refcount in the encoder, and again in the decoder, so the mailbox and the target actor both share ownership. blob_destroy always decrefs; only frees when the count reaches zero. Actor teardown already crawls actor->letters and calls blob_destroy on each—no JS-level decode needed. This gives you zero-copy, ref-counted sharing of large stone-blobs across actors, full compatibility with your bump+copying GC for JS wrappers, and trivial mailbox cleanup in pure C.
john changed title from Enhance stone to lower copying to No copy stone blobs for wota messages 2025-06-09 05:35:32 +00:00
john added this to the 0.9.3 milestone 2025-06-09 05:36:26 +00:00
john added the
enhancement
label 2025-06-09 05:36:45 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: john/cell#7
No description provided.