diff --git a/source/quickjs.c b/source/quickjs.c index 9f606b54..76e81160 100644 --- a/source/quickjs.c +++ b/source/quickjs.c @@ -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, "\n"); + return; + } + p = rt->atom_array[atom]; + if (!p) { + fprintf(stderr, "\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); diff --git a/test.ce b/test.ce index 1004e7da..f92174d4 100644 --- a/test.ce +++ b/test.ce @@ -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() } diff --git a/tests/suite.cm b/tests/suite.cm index db216c57..8e9e2930 100644 --- a/tests/suite.cm +++ b/tests/suite.cm @@ -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" + }, }