add rc trace"

This commit is contained in:
2026-01-23 17:22:07 -06:00
parent beea76949c
commit a01b48dabc
3 changed files with 491 additions and 5 deletions

View File

@@ -96,13 +96,15 @@
/* dump memory usage before running the garbage collector */
//#define DUMP_MEM
//#define DUMP_OBJECTS /* dump objects in JS_FreeContext */
//#define DUMP_ATOMS /* dump atoms in JS_FreeContext */
//#define DUMP_ATOMS /* dump atoms in JS_FreeContext */
//#define DUMP_SHAPES /* dump shapes in JS_FreeContext */
//#define DUMP_READ_OBJECT
//#define DUMP_ROPE_REBALANCE
/* dump profiling statistics (counters and sampling) when freeing the runtime */
//#define DUMP_PROFILE
#define RC_TRACE
/* test the GC by forcing it before each object allocation */
//#define FORCE_GC_AT_MALLOC
@@ -379,6 +381,118 @@ struct JSGCObjectHeader {
struct list_head link;
};
#ifdef RC_TRACE
typedef struct RcEvent {
JSGCObjectHeader *ptr;
uint32_t tag;
JSGCObjectHeader *parent;
uint32_t parent_type;
int32_t parent_class_id;
uint32_t atom;
int32_t prop_index;
const char *edge;
int32_t ref_before;
int32_t ref_after;
const char *file;
int line;
const char *op;
} RcEvent;
#define RC_LOG_CAP 16384
static RcEvent rc_log[RC_LOG_CAP];
static uint32_t rc_log_i;
static void rc_dump_atom(JSRuntime *rt, uint32_t atom);
static inline void rc_log_event(JSGCObjectHeader *ptr, uint32_t tag,
JSGCObjectHeader *parent, uint32_t parent_type,
int32_t parent_class_id, const char *edge,
uint32_t atom, int32_t prop_index,
int32_t rb, int32_t ra,
const char *file, int line,
const char *op)
{
RcEvent *e = &rc_log[rc_log_i++ & (RC_LOG_CAP - 1)];
e->ptr = ptr;
e->tag = tag;
e->parent = parent;
e->parent_type = parent_type;
e->parent_class_id = parent_class_id;
e->atom = atom;
e->prop_index = prop_index;
e->edge = edge;
e->ref_before = rb;
e->ref_after = ra;
e->file = file;
e->line = line;
e->op = op;
}
static inline void rc_trace_inc_gc(JSGCObjectHeader *p, const char *file, int line)
{
int32_t rb = p->ref_count;
p->ref_count = rb + 1;
rc_log_event(p, p->gc_obj_type, NULL, 0, -1, NULL,
0, -1,
rb, p->ref_count, file, line, "++");
}
static inline void rc_trace_dec_gc(JSGCObjectHeader *p, const char *file, int line)
{
int32_t rb = p->ref_count;
p->ref_count = rb - 1;
rc_log_event(p, p->gc_obj_type, NULL, 0, -1, NULL,
0, -1,
rb, p->ref_count, file, line, "--");
}
static inline void rc_trace_dec_gc_edge(JSGCObjectHeader *p,
JSGCObjectHeader *parent,
uint32_t parent_type,
int32_t parent_class_id,
const char *edge,
uint32_t atom, int32_t prop_index,
const char *file, int line)
{
int32_t rb = p->ref_count;
p->ref_count = rb - 1;
rc_log_event(p, p->gc_obj_type, parent, parent_type, parent_class_id, edge,
atom, prop_index,
rb, p->ref_count, file, line, "--");
}
static void rc_dump_history(JSGCObjectHeader *p, const char *why)
{
int count = 0;
fprintf(stderr, "RC_TRACE: %s ptr=%p tag=%u ref=%d\n",
why, (void *)p, (unsigned)p->gc_obj_type, p->ref_count);
for (uint32_t k = 0; k < RC_LOG_CAP && count < 200; k++) {
RcEvent *e = &rc_log[(rc_log_i - 1 - k) & (RC_LOG_CAP - 1)];
if (e->ptr != p)
continue;
fprintf(stderr,
"RC %s ptr=%p tag=%u %d->%d edge=%s parent=%p ptype=%u pclass=%d atom=%u idx=%d at %s:%d\n",
e->op, (void *)e->ptr, (unsigned)e->tag,
e->ref_before, e->ref_after,
e->edge ? e->edge : "-",
(void *)e->parent, (unsigned)e->parent_type,
(int)e->parent_class_id,
(unsigned)e->atom, (int)e->prop_index,
e->file, e->line);
count++;
}
}
#define RC_GC_INC(p) rc_trace_inc_gc((p), __FILE__, __LINE__)
#define RC_GC_DEC(p) rc_trace_dec_gc((p), __FILE__, __LINE__)
#define RC_GC_DEC_EDGE(p, parent, parent_type, parent_class_id, edge, atom, prop_index) \
rc_trace_dec_gc_edge((p), (parent), (parent_type), (parent_class_id), (edge), (atom), (prop_index), __FILE__, __LINE__)
#else
#define RC_GC_INC(p) ((p)->ref_count++)
#define RC_GC_DEC(p) ((p)->ref_count--)
#define RC_GC_DEC_EDGE(p, parent, parent_type, parent_class_id, edge, atom, prop_index) ((p)->ref_count--)
#endif
typedef struct JSVarRef {
union {
JSGCObjectHeader header; /* must come first */
@@ -498,6 +612,59 @@ static inline JSAtomSymbol *js_atom_as_symbol(JSAtomStruct *p) {
return (JSAtomSymbol *)p;
}
#ifdef RC_TRACE
static void rc_dump_atom(JSRuntime *rt, uint32_t atom)
{
JSAtomStruct *p;
uint32_t len;
uint32_t i;
fprintf(stderr, "RC_TRACE: atom=%u ", atom);
if (atom == JS_ATOM_NULL || atom >= (uint32_t)rt->atom_size) {
fprintf(stderr, "<invalid>\n");
return;
}
p = rt->atom_array[atom];
if (!p) {
fprintf(stderr, "<null>\n");
return;
}
if (p->atom_type == JS_ATOM_TYPE_STRING) {
fprintf(stderr, "type=string ");
len = p->len;
fprintf(stderr, "len=%u value=\"", len);
if (p->is_wide_char) {
for (i = 0; i < len && i < 64; i++) {
uint16_t ch = p->u.str16[i];
if (ch >= 0x20 && ch < 0x7f) {
fputc((char)ch, stderr);
} else {
fprintf(stderr, "\\u%04x", ch);
}
}
} else {
for (i = 0; i < len && i < 64; i++) {
uint8_t ch = p->u.str8[i];
if (ch >= 0x20 && ch < 0x7f) {
fputc((char)ch, stderr);
} else {
fprintf(stderr, "\\x%02x", ch);
}
}
}
if (len > 64) {
fprintf(stderr, "...");
}
fprintf(stderr, "\"\n");
} else if (p->atom_type == JS_ATOM_TYPE_SYMBOL) {
JSAtomSymbol *sp = js_atom_as_symbol(p);
fprintf(stderr, "type=symbol obj_key=%d\n",
JS_VALUE_GET_TAG(sp->obj_key) != JS_TAG_NULL);
} else {
fprintf(stderr, "type=unknown(%u)\n", (unsigned)p->atom_type);
}
}
#endif
typedef struct JSStringRope {
JSRefCountHeader header; /* must come first, 32-bit */
uint32_t len;
@@ -1444,6 +1611,15 @@ static inline void js_free_string(JSRuntime *rt, JSString *str)
}
}
static inline void JS_MarkValueEdgeEx(JSRuntime *rt, JSValueConst val,
JSGCObjectHeader *parent, const char *edge,
uint32_t atom, int32_t prop_index);
static inline void JS_MarkValueEdge(JSRuntime *rt, JSValueConst val,
JSGCObjectHeader *parent, const char *edge)
{
JS_MarkValueEdgeEx(rt, val, parent, edge, 0, -1);
}
void JS_SetRuntimeInfo(JSRuntime *rt, const char *s)
{
if (rt)
@@ -5301,14 +5477,237 @@ static void mark_children(JSRuntime *rt, JSGCObjectHeader *gp,
static void gc_decref_child(JSRuntime *rt, JSGCObjectHeader *p)
{
#ifdef RC_TRACE
if (p->ref_count <= 0) {
rc_dump_history(p, "gc_decref_child pre-assert");
}
#endif
assert(p->ref_count > 0);
p->ref_count--;
RC_GC_DEC(p);
if (p->ref_count == 0 && p->mark == 1) {
list_del(&p->link);
list_add_tail(&p->link, &rt->tmp_obj_list);
}
}
static inline int32_t gc_parent_class_id(JSGCObjectHeader *parent)
{
if (parent && parent->gc_obj_type == JS_GC_OBJ_TYPE_JS_OBJECT) {
return ((JSObject *)parent)->class_id;
}
return -1;
}
#ifdef RC_TRACE
static JSGCObjectHeader *rc_edge_parent;
static const char *rc_edge_name;
static void gc_decref_child_dbg(JSRuntime *rt, JSGCObjectHeader *parent,
const char *edge, uint32_t atom,
int32_t prop_index, JSGCObjectHeader *p,
const char *file, int line)
{
uint32_t parent_type = parent ? parent->gc_obj_type : 0;
int32_t parent_class_id = gc_parent_class_id(parent);
if (p->ref_count <= 0) {
fprintf(stderr,
"RC_TRACE: gc_decref_child pre-assert ptr=%p ref=%d edge=%s parent=%p ptype=%u pclass=%d atom=%u idx=%d\n",
(void *)p, p->ref_count,
edge ? edge : "-",
(void *)parent, (unsigned)parent_type, (int)parent_class_id,
(unsigned)atom, (int)prop_index);
fprintf(stderr, "RC_TRACE: gc_decref_child pre-assert atom=%u idx=%d\n",
(unsigned)atom, (int)prop_index);
if (atom != JS_ATOM_NULL) {
rc_dump_atom(rt, atom);
}
rc_dump_history(p, "gc_decref_child pre-assert");
}
assert(p->ref_count > 0);
RC_GC_DEC_EDGE(p, parent, parent_type, parent_class_id, edge, atom, prop_index);
if (p->ref_count == 0 && p->mark == 1) {
list_del(&p->link);
list_add_tail(&p->link, &rt->tmp_obj_list);
}
}
static void gc_decref_child_edge(JSRuntime *rt, JSGCObjectHeader *p)
{
gc_decref_child_dbg(rt, rc_edge_parent, rc_edge_name, 0, -1, p, __FILE__, __LINE__);
}
#endif
static inline void JS_MarkValueEdgeEx(JSRuntime *rt, JSValueConst val,
JSGCObjectHeader *parent, const char *edge,
uint32_t atom, int32_t prop_index)
{
if (!JS_VALUE_HAS_REF_COUNT(val)) {
return;
}
switch (JS_VALUE_GET_TAG(val)) {
case JS_TAG_ARRAY:
case JS_TAG_OBJECT:
case JS_TAG_FUNCTION_BYTECODE:
{
JSGCObjectHeader *child = JS_VALUE_GET_PTR(val);
#ifdef RC_TRACE
gc_decref_child_dbg(rt, parent, edge, atom, prop_index,
child, __FILE__, __LINE__);
#else
gc_decref_child(rt, child);
#endif
}
break;
default:
break;
}
}
static void JS_MarkContextDecref(JSRuntime *rt, JSContext *ctx)
{
int i;
JS_MarkValueEdge(rt, ctx->global_obj, &ctx->header, "ctx.global_obj");
JS_MarkValueEdge(rt, ctx->global_var_obj, &ctx->header, "ctx.global_var_obj");
JS_MarkValueEdge(rt, ctx->throw_type_error, &ctx->header, "ctx.throw_type_error");
JS_MarkValueEdge(rt, ctx->eval_obj, &ctx->header, "ctx.eval_obj");
JS_MarkValueEdge(rt, ctx->array_proto_values, &ctx->header, "ctx.array_proto_values");
for(i = 0; i < JS_NATIVE_ERROR_COUNT; i++) {
JS_MarkValueEdge(rt, ctx->native_error_proto[i], &ctx->header, "ctx.native_error_proto");
}
for(i = 0; i < rt->class_count; i++) {
JS_MarkValueEdge(rt, ctx->class_proto[i], &ctx->header, "ctx.class_proto");
}
JS_MarkValueEdge(rt, ctx->array_ctor, &ctx->header, "ctx.array_ctor");
JS_MarkValueEdge(rt, ctx->regexp_ctor, &ctx->header, "ctx.regexp_ctor");
if (ctx->array_shape) {
#ifdef RC_TRACE
gc_decref_child_dbg(rt, &ctx->header, "ctx.array_shape", 0, -1,
&ctx->array_shape->header, __FILE__, __LINE__);
#else
gc_decref_child(rt, &ctx->array_shape->header);
#endif
}
}
static void mark_children_decref(JSRuntime *rt, JSGCObjectHeader *gp)
{
switch(gp->gc_obj_type) {
case JS_GC_OBJ_TYPE_JS_OBJECT:
{
JSObject *p = (JSObject *)gp;
JSShapeProperty *prs;
JSShape *sh;
int i;
sh = p->shape;
#ifdef RC_TRACE
gc_decref_child_dbg(rt, gp, "obj.shape", 0, -1, &sh->header,
__FILE__, __LINE__);
#else
gc_decref_child(rt, &sh->header);
#endif
prs = get_shape_prop(sh);
for(i = 0; i < sh->prop_count; i++) {
JSProperty *pr = &p->prop[i];
JSAtom prop_atom = prs->atom;
int prop_index = i;
if (prop_atom != JS_ATOM_NULL) {
if (prs->flags & JS_PROP_TMASK) {
if ((prs->flags & JS_PROP_TMASK) == JS_PROP_VARREF) {
#ifdef RC_TRACE
gc_decref_child_dbg(rt, gp, "obj.prop.var_ref",
prop_atom, prop_index,
&pr->u.var_ref->header,
__FILE__, __LINE__);
#else
gc_decref_child(rt, &pr->u.var_ref->header);
#endif
}
} else {
JS_MarkValueEdgeEx(rt, pr->u.value, gp, "obj.prop.value",
prop_atom, prop_index);
}
}
prs++;
}
if (p->class_id != JS_CLASS_OBJECT) {
JSClassGCMark *gc_mark;
gc_mark = rt->class_array[p->class_id].gc_mark;
if (gc_mark) {
#ifdef RC_TRACE
rc_edge_parent = gp;
rc_edge_name = "obj.class_gc_mark";
gc_mark(rt, JS_MKPTR(JS_TAG_OBJECT, p), gc_decref_child_edge);
#else
gc_mark(rt, JS_MKPTR(JS_TAG_OBJECT, p), gc_decref_child);
#endif
}
}
}
break;
case JS_GC_OBJ_TYPE_FUNCTION_BYTECODE:
{
JSFunctionBytecode *b = (JSFunctionBytecode *)gp;
int i;
for(i = 0; i < b->cpool_count; i++) {
JS_MarkValueEdge(rt, b->cpool[i], gp, "bytecode.cpool");
}
if (b->realm) {
#ifdef RC_TRACE
gc_decref_child_dbg(rt, gp, "bytecode.realm", 0, -1,
&b->realm->header, __FILE__, __LINE__);
#else
gc_decref_child(rt, &b->realm->header);
#endif
}
}
break;
case JS_GC_OBJ_TYPE_VAR_REF:
{
JSVarRef *var_ref = (JSVarRef *)gp;
if (var_ref->is_detached) {
JS_MarkValueEdge(rt, *var_ref->pvalue, gp, "var_ref.value");
}
}
break;
case JS_GC_OBJ_TYPE_SHAPE:
{
JSShape *sh = (JSShape *)gp;
if (sh->proto != NULL) {
#ifdef RC_TRACE
gc_decref_child_dbg(rt, gp, "shape.proto", 0, -1,
&sh->proto->header, __FILE__, __LINE__);
#else
gc_decref_child(rt, &sh->proto->header);
#endif
}
}
break;
case JS_GC_OBJ_TYPE_JS_CONTEXT:
{
JSContext *ctx = (JSContext *)gp;
JS_MarkContextDecref(rt, ctx);
}
break;
case JS_GC_OBJ_TYPE_ARRAY:
{
JSArray *arr = (JSArray *)gp;
uint32_t i;
for (i = 0; i < arr->len; i++) {
JS_MarkValueEdge(rt, arr->values[i], gp, "array.elem");
}
}
break;
default:
abort();
}
}
static void gc_decref(JSRuntime *rt)
{
struct list_head *el, *el1;
@@ -5322,7 +5721,7 @@ static void gc_decref(JSRuntime *rt)
list_for_each_safe(el, el1, &rt->gc_obj_list) {
p = list_entry(el, JSGCObjectHeader, link);
assert(p->mark == 0);
mark_children(rt, p, gc_decref_child);
mark_children_decref(rt, p);
p->mark = 1;
if (p->ref_count == 0) {
list_del(&p->link);
@@ -5333,7 +5732,7 @@ static void gc_decref(JSRuntime *rt)
static void gc_scan_incref_child(JSRuntime *rt, JSGCObjectHeader *p)
{
p->ref_count++;
RC_GC_INC(p);
if (p->ref_count == 1) {
/* ref_count was 0: remove from tmp_obj_list and add at the
end of gc_obj_list */
@@ -5345,7 +5744,7 @@ static void gc_scan_incref_child(JSRuntime *rt, JSGCObjectHeader *p)
static void gc_scan_incref_child2(JSRuntime *rt, JSGCObjectHeader *p)
{
p->ref_count++;
RC_GC_INC(p);
}
static void gc_scan(JSRuntime *rt)
@@ -5356,6 +5755,11 @@ static void gc_scan(JSRuntime *rt)
/* keep the objects with a refcount > 0 and their children. */
list_for_each(el, &rt->gc_obj_list) {
p = list_entry(el, JSGCObjectHeader, link);
#ifdef RC_TRACE
if (p->ref_count <= 0) {
rc_dump_history(p, "gc_scan pre-assert");
}
#endif
assert(p->ref_count > 0);
p->mark = 0; /* reset the mark for the next GC call */
mark_children(rt, p, gc_scan_incref_child);

24
test.ce
View File

@@ -4,12 +4,16 @@ var fd = use('fd')
var time = use('time')
var json = use('json')
var blob = use('blob')
var dbg = use('js')
// run gc with dbg.gc()
if (!args) args = []
var target_pkg = null // null = current package
var target_test = null // null = all tests, otherwise specific test file
var all_pkgs = false
var gc_after_each_test = false
// Actor test support
def ACTOR_TEST_TIMEOUT = 30000 // 30 seconds timeout for actor tests
@@ -43,6 +47,16 @@ function get_current_package_name() {
// cell test package all - run all tests from all packages
function parse_args() {
var cleaned_args = []
for (var i = 0; i < length(args); i++) {
if (args[i] == '-g') {
gc_after_each_test = true
} else {
push(cleaned_args, args[i])
}
}
args = cleaned_args
if (length(args) == 0) {
// cell test - run all tests for current package
if (!is_valid_package('.')) {
@@ -346,6 +360,9 @@ function run_tests(package_name, specific_test) {
push(file_result.tests, test_entry)
pkg_result.total++
if (gc_after_each_test) {
dbg.gc()
}
}
}
@@ -362,6 +379,9 @@ function run_tests(package_name, specific_test) {
pkg_result.failed++
file_result.failed++
pkg_result.total++
if (gc_after_each_test) {
dbg.gc()
}
}
push(pkg_result.files, file_result)
}
@@ -450,6 +470,10 @@ function handle_actor_message(msg) {
push(actor_test_results, entry)
}
if (gc_after_each_test) {
dbg.gc()
}
check_completion()
}

View File

@@ -3449,4 +3449,62 @@ return {
var result = extract(text, /hello/i)
if (result[0] != "Hello") throw "extract regex case insensitive failed"
},
// ============================================================================
// GC PATHOLOGICAL CASES
// ============================================================================
test_gc_cycle_object_self: function() {
var obj = {name: "root"}
obj.self = obj
if (obj.self != obj) throw "self cycle failed"
},
test_gc_cycle_array_self: function() {
var arr = []
push(arr, arr)
if (arr[0] != arr) throw "array self cycle failed"
},
test_gc_cycle_object_array_pair: function() {
var obj = {kind: "node"}
var arr = [obj]
obj.arr = arr
if (obj.arr[0] != obj) throw "object/array cycle failed"
},
test_gc_shared_references: function() {
var shared = {value: 42}
var a = {ref: shared}
var b = {ref: shared}
if (a.ref != shared || b.ref != shared) throw "shared reference failed"
},
test_gc_object_key_cycle: function() {
var k = {}
var v = {label: "value"}
var o = {}
o[k] = v
v.back = o
if (o[k].back != o) throw "object key cycle failed"
},
test_gc_object_text_key_mix: function() {
var obj = {}
var key = "alpha"
var inner = {token: "x"}
obj[key] = inner
obj["beta"] = [inner, obj]
if (obj.alpha.token != "x") throw "text key value failed"
if (obj.beta[1] != obj) throw "text key cycle failed"
},
test_gc_array_object_key_mix: function() {
var obj = {}
var arr = [obj]
obj[arr] = "anchor"
obj.self = obj
if (obj[arr] != "anchor") throw "array object key failed"
if (arr[0] != obj) throw "array object mix failed"
},
}