fixing gc bugs; nearly idempotent

This commit is contained in:
2026-02-15 13:14:26 -06:00
parent 7de20b39da
commit ebd624b772
22 changed files with 656663 additions and 184850 deletions

View File

@@ -774,15 +774,31 @@ void *js_malloc (JSContext *ctx, size_t size) {
size = (size + 7) & ~7;
#ifdef FORCE_GC_AT_MALLOC
/* Force GC on every allocation for testing - but don't grow heap unless needed */
int need_space = (uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end;
if (ctx_gc(ctx, need_space, size) < 0) {
JS_ThrowOutOfMemory(ctx);
return NULL;
}
if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) {
JS_ThrowOutOfMemory(ctx);
return NULL;
/* Force GC on every allocation for testing */
{
int need_space = (uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end;
if (ctx_gc(ctx, need_space, size) < 0) {
JS_ThrowOutOfMemory(ctx);
return NULL;
}
/* If still no room after GC, grow and retry (same logic as normal path) */
if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) {
size_t live = (size_t)((uint8_t *)ctx->heap_free - (uint8_t *)ctx->heap_base);
size_t need = live + size;
size_t ns = ctx->current_block_size;
while (ns < need && ns < buddy_max_block(&ctx->rt->buddy))
ns *= 2;
ctx->next_block_size = ns;
if (ctx_gc (ctx, 1, size) < 0) {
JS_ThrowOutOfMemory (ctx);
return NULL;
}
ctx->next_block_size = ctx->current_block_size;
if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) {
JS_ThrowOutOfMemory (ctx);
return NULL;
}
}
}
#else
/* Check if we have space in current block */
@@ -1700,11 +1716,18 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
#endif
/* If <40% recovered, grow next block size for future allocations.
First poor recovery: double. Consecutive poor: quadruple. */
First poor recovery: double. Consecutive poor: quadruple.
Skip under FORCE_GC_AT_MALLOC — forced GC on every allocation creates
artificially poor recovery (no time to accumulate garbage), which
would cause runaway exponential heap growth. */
#ifdef DUMP_GC
int will_grow = 0;
#endif
#ifdef FORCE_GC_AT_MALLOC
if (0) {
#else
if (allow_grow && recovered > 0 && old_used > 0 && recovered < old_used * 2 / 5) {
#endif
size_t factor = ctx->gc_poor_streak >= 1 ? 4 : 2;
size_t grown = new_size * factor;
if (grown <= buddy_max_block(&ctx->rt->buddy)) {
@@ -2407,28 +2430,37 @@ JSValue JS_NewStringLen (JSContext *ctx, const char *buf, size_t buf_len) {
JSValue JS_ConcatString3 (JSContext *ctx, const char *str1, JSValue str2, const char *str3) {
JSText *b;
int len1, len3, str2_len;
JSGCRef str2_ref;
if (!JS_IsText (str2)) {
str2 = JS_ToString (ctx, str2);
if (JS_IsException (str2)) goto fail;
}
str2_len = js_string_value_len (str2);
/* Root str2 — pretext_init/pretext_write8/pretext_concat_value allocate
and can trigger GC, which would move the heap string str2 points to */
JS_PushGCRef (ctx, &str2_ref);
str2_ref.val = str2;
str2_len = js_string_value_len (str2_ref.val);
len1 = strlen (str1);
len3 = strlen (str3);
b = pretext_init (ctx, len1 + str2_len + len3);
if (!b) goto fail;
if (!b) goto fail_pop;
b = pretext_write8 (ctx, b, (const uint8_t *)str1, len1);
if (!b) goto fail;
b = pretext_concat_value (ctx, b, str2);
if (!b) goto fail;
if (!b) goto fail_pop;
b = pretext_concat_value (ctx, b, str2_ref.val);
if (!b) goto fail_pop;
b = pretext_write8 (ctx, b, (const uint8_t *)str3, len3);
if (!b) goto fail;
if (!b) goto fail_pop;
JS_PopGCRef (ctx, &str2_ref);
return pretext_end (ctx, b);
fail_pop:
JS_PopGCRef (ctx, &str2_ref);
fail:
return JS_EXCEPTION;
}
@@ -2555,13 +2587,25 @@ int js_string_compare_value_nocase (JSContext *ctx, JSValue op1, JSValue op2) {
JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2) {
if (unlikely (!JS_IsText (op1))) {
/* Root op2 across JS_ToString which can trigger GC */
JSGCRef op2_guard;
JS_PushGCRef (ctx, &op2_guard);
op2_guard.val = op2;
op1 = JS_ToString (ctx, op1);
op2 = op2_guard.val;
JS_PopGCRef (ctx, &op2_guard);
if (JS_IsException (op1)) {
return JS_EXCEPTION;
}
}
if (unlikely (!JS_IsText (op2))) {
/* Root op1 across JS_ToString which can trigger GC */
JSGCRef op1_guard;
JS_PushGCRef (ctx, &op1_guard);
op1_guard.val = op1;
op2 = JS_ToString (ctx, op2);
op1 = op1_guard.val;
JS_PopGCRef (ctx, &op1_guard);
if (JS_IsException (op2)) {
return JS_EXCEPTION;
}
@@ -3095,7 +3139,6 @@ JSValue JS_GetOwnPropertyNames (JSContext *ctx, JSValue obj) {
return JS_EXCEPTION;
}
/* Reading slots is GC-safe - no allocation */
JSRecord *rec = JS_VALUE_GET_OBJ (obj);
mask = (uint32_t)objhdr_cap56 (rec->mist_hdr);
@@ -3107,22 +3150,31 @@ JSValue JS_GetOwnPropertyNames (JSContext *ctx, JSValue obj) {
if (count == 0) return JS_NewArrayLen (ctx, 0);
/* Collect keys into stack buffer (JSValues are just uint64_t) */
JSValue *keys = alloca (count * sizeof (JSValue));
/* Root obj — JS_NewArrayLen allocates and can trigger GC, which
moves the record. Re-read keys from the moved record after. */
JSGCRef obj_ref;
JS_PushGCRef (ctx, &obj_ref);
obj_ref.val = obj;
JSValue arr = JS_NewArrayLen (ctx, count);
if (JS_IsException (arr)) {
JS_PopGCRef (ctx, &obj_ref);
return JS_EXCEPTION;
}
/* Re-read record pointer after possible GC */
rec = JS_VALUE_GET_OBJ (obj_ref.val);
mask = (uint32_t)objhdr_cap56 (rec->mist_hdr);
uint32_t idx = 0;
for (i = 1; i <= mask; i++) {
JSValue k = rec->slots[i].key;
if (JS_IsText (k)) keys[idx++] = k;
}
/* Now allocate and fill - GC point, but keys are on stack */
JSValue arr = JS_NewArrayLen (ctx, count);
if (JS_IsException (arr)) return JS_EXCEPTION;
for (i = 0; i < count; i++) {
JS_SetPropertyNumber (ctx, arr, i, keys[i]);
if (JS_IsText (k)) {
JS_SetPropertyNumber (ctx, arr, idx++, k);
}
}
JS_PopGCRef (ctx, &obj_ref);
return arr;
}
@@ -4111,17 +4163,22 @@ static JSValue JS_ToStringCheckObject (JSContext *ctx, JSValue val) {
}
static JSValue JS_ToQuotedString (JSContext *ctx, JSValue val1) {
JSValue val;
int i, len;
uint32_t c;
JSText *b;
char buf[16];
JSGCRef val_ref;
val = JS_ToStringCheckObject (ctx, val1);
JSValue val = JS_ToStringCheckObject (ctx, val1);
if (JS_IsException (val)) return val;
/* Root val — pretext_init/pretext_putc allocate and can trigger GC,
which would move the heap string val points to */
JS_PushGCRef (ctx, &val_ref);
val_ref.val = val;
/* Use js_string_value_len to handle both immediate and heap strings */
len = js_string_value_len (val);
len = js_string_value_len (val_ref.val);
b = pretext_init (ctx, len + 2);
if (!b) goto fail;
@@ -4129,7 +4186,7 @@ static JSValue JS_ToQuotedString (JSContext *ctx, JSValue val1) {
b = pretext_putc (ctx, b, '\"');
if (!b) goto fail;
for (i = 0; i < len; i++) {
c = js_string_value_get (val, i);
c = js_string_value_get (val_ref.val, i);
switch (c) {
case '\t':
c = 't';
@@ -4168,8 +4225,10 @@ static JSValue JS_ToQuotedString (JSContext *ctx, JSValue val1) {
}
b = pretext_putc (ctx, b, '\"');
if (!b) goto fail;
JS_PopGCRef (ctx, &val_ref);
return pretext_end (ctx, b);
fail:
JS_PopGCRef (ctx, &val_ref);
return JS_EXCEPTION;
}
@@ -5484,7 +5543,10 @@ static JSValue cjson_to_jsvalue (JSContext *ctx, const cJSON *item) {
arr_ref.val = JS_NewArrayLen (ctx, n);
for (int i = 0; i < n; i++) {
cJSON *child = cJSON_GetArrayItem (item, i);
JS_SetPropertyNumber (ctx, arr_ref.val, i, cjson_to_jsvalue (ctx, child));
/* Evaluate recursive call before reading arr_ref.val — the call
allocates and can trigger GC which moves the array */
JSValue elem = cjson_to_jsvalue (ctx, child);
JS_SetPropertyNumber (ctx, arr_ref.val, i, elem);
}
JSValue result = arr_ref.val;
JS_DeleteGCRef (ctx, &arr_ref);
@@ -5495,7 +5557,8 @@ static JSValue cjson_to_jsvalue (JSContext *ctx, const cJSON *item) {
JS_AddGCRef (ctx, &obj_ref);
obj_ref.val = JS_NewObject (ctx);
for (cJSON *child = item->child; child; child = child->next) {
JS_SetPropertyStr (ctx, obj_ref.val, child->string, cjson_to_jsvalue (ctx, child));
JSValue val = cjson_to_jsvalue (ctx, child);
JS_SetPropertyStr (ctx, obj_ref.val, child->string, val);
}
JSValue result = obj_ref.val;
JS_DeleteGCRef (ctx, &obj_ref);
@@ -5776,6 +5839,7 @@ JSValue JS_JSONStringify (JSContext *ctx, JSValue obj, JSValue replacer, JSValue
int res;
int64_t i, j, n;
JSGCRef obj_ref;
JSLocalRef *saved_local_frame = JS_GetLocalFrame (ctx);
/* Root obj since GC can happen during stringify setup */
JS_PushGCRef (ctx, &obj_ref);
@@ -5790,6 +5854,18 @@ JSValue JS_JSONStringify (JSContext *ctx, JSValue obj, JSValue replacer, JSValue
ret = JS_NULL;
wrapper = JS_NULL;
/* Root all jsc fields that hold heap objects — GC can fire during
stringify and would move these objects without updating the struct */
JSLocalRef lr_stack, lr_plist, lr_gap, lr_replacer;
lr_stack.ptr = &jsc->stack;
JS_PushLocalRef (ctx, &lr_stack);
lr_plist.ptr = &jsc->property_list;
JS_PushLocalRef (ctx, &lr_plist);
lr_gap.ptr = &jsc->gap;
JS_PushLocalRef (ctx, &lr_gap);
lr_replacer.ptr = &jsc->replacer_func;
JS_PushLocalRef (ctx, &lr_replacer);
/* Root the buffer for GC safety */
JS_PushGCRef (ctx, &jsc->b_root);
{
@@ -5869,6 +5945,8 @@ exception:
done1:
done:
JS_PopGCRef (ctx, &jsc->b_root);
/* Restore local refs pushed for jsc fields */
ctx->top_local_ref = saved_local_frame;
JS_PopGCRef (ctx, &obj_ref);
return ret;
}
@@ -7020,9 +7098,14 @@ JSValue js_cell_text_codepoint (JSContext *ctx, JSValue this_val, int argc, JSVa
* file. */
static JSText *pt_concat_value_to_string_free (JSContext *ctx, JSText *b, JSValue v) {
JSGCRef s_ref;
JSValue s = JS_ToString (ctx, v);
if (JS_IsException (s)) return NULL;
b = pretext_concat_value (ctx, b, s);
/* Root s — pretext_concat_value can trigger GC and move the heap string */
JS_PushGCRef (ctx, &s_ref);
s_ref.val = s;
b = pretext_concat_value (ctx, b, s_ref.val);
JS_PopGCRef (ctx, &s_ref);
return b;
}