Initial import
This commit is contained in:
445
third_party/duktape/duk_heap_finalize.c
vendored
Normal file
445
third_party/duktape/duk_heap_finalize.c
vendored
Normal file
@@ -0,0 +1,445 @@
|
||||
/*
|
||||
* Finalizer handling.
|
||||
*/
|
||||
|
||||
#include "third_party/duktape/duk_internal.h"
|
||||
|
||||
#if defined(DUK_USE_FINALIZER_SUPPORT)
|
||||
|
||||
/*
|
||||
* Fake torture finalizer.
|
||||
*/
|
||||
|
||||
#if defined(DUK_USE_FINALIZER_TORTURE)
|
||||
DUK_LOCAL duk_ret_t duk__fake_global_finalizer(duk_hthread *thr) {
|
||||
DUK_DD(DUK_DDPRINT("fake global torture finalizer executed"));
|
||||
|
||||
/* Require a lot of stack to force a value stack grow/shrink. */
|
||||
duk_require_stack(thr, 100000);
|
||||
|
||||
/* Force a reallocation with pointer change for value stack
|
||||
* to maximize side effects.
|
||||
*/
|
||||
duk_hthread_valstack_torture_realloc(thr);
|
||||
|
||||
/* Inner function call, error throw. */
|
||||
duk_eval_string_noresult(thr,
|
||||
"(function dummy() {\n"
|
||||
" dummy.prototype = null; /* break reference loop */\n"
|
||||
" try {\n"
|
||||
" throw 'fake-finalizer-dummy-error';\n"
|
||||
" } catch (e) {\n"
|
||||
" void e;\n"
|
||||
" }\n"
|
||||
"})()");
|
||||
|
||||
/* The above creates garbage (e.g. a function instance). Because
|
||||
* the function/prototype reference loop is broken, it gets collected
|
||||
* immediately by DECREF. If Function.prototype has a _Finalizer
|
||||
* property (happens in some test cases), the garbage gets queued to
|
||||
* finalize_list. This still won't cause an infinite loop because
|
||||
* the torture finalizer is called once per finalize_list run and
|
||||
* the garbage gets handled in the same run. (If the garbage needs
|
||||
* mark-and-sweep collection, an infinite loop might ensue.)
|
||||
*/
|
||||
return 0;
|
||||
}
|
||||
|
||||
DUK_LOCAL void duk__run_global_torture_finalizer(duk_hthread *thr) {
|
||||
DUK_ASSERT(thr != NULL);
|
||||
|
||||
/* Avoid fake finalization when callstack limit is near. Otherwise
|
||||
* a callstack limit error will be created, then refzero'ed. The
|
||||
* +5 headroom is conservative.
|
||||
*/
|
||||
if (thr->heap->call_recursion_depth + 5 >= thr->heap->call_recursion_limit ||
|
||||
thr->callstack_top + 5 >= DUK_USE_CALLSTACK_LIMIT) {
|
||||
DUK_D(DUK_DPRINT("skip global torture finalizer, too little headroom for call recursion or call stack size"));
|
||||
return;
|
||||
}
|
||||
|
||||
/* Run fake finalizer. Avoid creating unnecessary garbage. */
|
||||
duk_push_c_function(thr, duk__fake_global_finalizer, 0 /*nargs*/);
|
||||
(void) duk_pcall(thr, 0 /*nargs*/);
|
||||
duk_pop(thr);
|
||||
}
|
||||
#endif /* DUK_USE_FINALIZER_TORTURE */
|
||||
|
||||
/*
|
||||
* Process the finalize_list to completion.
|
||||
*
|
||||
* An object may be placed on finalize_list by either refcounting or
|
||||
* mark-and-sweep. The refcount of objects placed by refcounting will be
|
||||
* zero; the refcount of objects placed by mark-and-sweep is > 0. In both
|
||||
* cases the refcount is bumped by 1 artificially so that a REFZERO event
|
||||
* can never happen while an object is waiting for finalization. Without
|
||||
* this bump a REFZERO could now happen because user code may call
|
||||
* duk_push_heapptr() and then pop a value even when it's on finalize_list.
|
||||
*
|
||||
* List processing assumes refcounts are kept up-to-date at all times, so
|
||||
* that once the finalizer returns, a zero refcount is a reliable reason to
|
||||
* free the object immediately rather than place it back to the heap. This
|
||||
* is the case because we run outside of refzero_list processing so that
|
||||
* DECREF cascades are handled fully inline.
|
||||
*
|
||||
* For mark-and-sweep queued objects (had_zero_refcount false) the object
|
||||
* may be freed immediately if its refcount is zero after the finalizer call
|
||||
* (i.e. finalizer removed the reference loop for the object). If not, the
|
||||
* next mark-and-sweep will collect the object unless it has become reachable
|
||||
* (i.e. rescued) by that time and its refcount hasn't fallen to zero before
|
||||
* that. Mark-and-sweep detects these objects because their FINALIZED flag
|
||||
* is set.
|
||||
*
|
||||
* There's an inherent limitation for mark-and-sweep finalizer rescuing: an
|
||||
* object won't get refinalized if (1) it's rescued, but (2) becomes
|
||||
* unreachable before mark-and-sweep has had time to notice it. The next
|
||||
* mark-and-sweep round simply doesn't have any information of whether the
|
||||
* object has been unreachable the whole time or not (the only way to get
|
||||
* that information would be a mark-and-sweep pass for *every finalized
|
||||
* object*). This is awkward for the application because the mark-and-sweep
|
||||
* round is not generally visible or under full application control.
|
||||
*
|
||||
* For refcount queued objects (had_zero_refcount true) the object is either
|
||||
* immediately freed or rescued, and waiting for a mark-and-sweep round is not
|
||||
* necessary (or desirable); FINALIZED is cleared when a rescued object is
|
||||
* queued back to heap_allocated. The object is eligible for finalization
|
||||
* again (either via refcounting or mark-and-sweep) immediately after being
|
||||
* rescued. If a refcount finalized object is placed into an unreachable
|
||||
* reference loop by its finalizer, it will get collected by mark-and-sweep
|
||||
* and currently the finalizer will execute again.
|
||||
*
|
||||
* There's a special case where:
|
||||
*
|
||||
* - Mark-and-sweep queues an object to finalize_list for finalization.
|
||||
* - The finalizer is executed, FINALIZED is set, and object is queued
|
||||
* back to heap_allocated, waiting for a new mark-and-sweep round.
|
||||
* - The object's refcount drops to zero before mark-and-sweep has a
|
||||
* chance to run another round and make a rescue/free decision.
|
||||
*
|
||||
* This is now handled by refzero code: if an object has a finalizer but
|
||||
* FINALIZED is already set, the object is freed without finalizer processing.
|
||||
* The outcome is the same as if mark-and-sweep was executed at that point;
|
||||
* mark-and-sweep would also free the object without another finalizer run.
|
||||
* This could also be changed so that the refzero-triggered finalizer *IS*
|
||||
* executed: being refzero collected implies someone has operated on the
|
||||
* object so it hasn't been totally unreachable the whole time. This would
|
||||
* risk a finalizer loop however.
|
||||
*/
|
||||
|
||||
DUK_INTERNAL void duk_heap_process_finalize_list(duk_heap *heap) {
|
||||
duk_heaphdr *curr;
|
||||
#if defined(DUK_USE_DEBUG)
|
||||
duk_size_t count = 0;
|
||||
#endif
|
||||
|
||||
DUK_DDD(DUK_DDDPRINT("duk_heap_process_finalize_list: %p", (void *) heap));
|
||||
|
||||
if (heap->pf_prevent_count != 0) {
|
||||
DUK_DDD(DUK_DDDPRINT("skip finalize_list processing: pf_prevent_count != 0"));
|
||||
return;
|
||||
}
|
||||
|
||||
/* Heap alloc prevents mark-and-sweep before heap_thread is ready. */
|
||||
DUK_ASSERT(heap != NULL);
|
||||
DUK_ASSERT(heap->heap_thread != NULL);
|
||||
DUK_ASSERT(heap->heap_thread->valstack != NULL);
|
||||
#if defined(DUK_USE_REFERENCE_COUNTING)
|
||||
DUK_ASSERT(heap->refzero_list == NULL);
|
||||
#endif
|
||||
|
||||
DUK_ASSERT(heap->pf_prevent_count == 0);
|
||||
heap->pf_prevent_count = 1;
|
||||
|
||||
/* Mark-and-sweep no longer needs to be prevented when running
|
||||
* finalizers: mark-and-sweep skips any rescue decisions if there
|
||||
* are any objects in finalize_list when mark-and-sweep is entered.
|
||||
* This protects finalized objects from incorrect rescue decisions
|
||||
* caused by finalize_list being a reachability root and only
|
||||
* partially processed. Freeing decisions are not postponed.
|
||||
*/
|
||||
|
||||
/* When finalizer torture is enabled, make a fake finalizer call with
|
||||
* maximum side effects regardless of whether finalize_list is empty.
|
||||
*/
|
||||
#if defined(DUK_USE_FINALIZER_TORTURE)
|
||||
duk__run_global_torture_finalizer(heap->heap_thread);
|
||||
#endif
|
||||
|
||||
/* Process finalize_list until it becomes empty. There's currently no
|
||||
* protection against a finalizer always creating more garbage.
|
||||
*/
|
||||
while ((curr = heap->finalize_list) != NULL) {
|
||||
#if defined(DUK_USE_REFERENCE_COUNTING)
|
||||
duk_bool_t queue_back;
|
||||
#endif
|
||||
|
||||
DUK_DD(DUK_DDPRINT("processing finalize_list entry: %p -> %!iO", (void *) curr, curr));
|
||||
|
||||
DUK_ASSERT(DUK_HEAPHDR_GET_TYPE(curr) == DUK_HTYPE_OBJECT); /* Only objects have finalizers. */
|
||||
DUK_ASSERT(!DUK_HEAPHDR_HAS_REACHABLE(curr));
|
||||
DUK_ASSERT(!DUK_HEAPHDR_HAS_TEMPROOT(curr));
|
||||
DUK_ASSERT(DUK_HEAPHDR_HAS_FINALIZABLE(curr)); /* All objects on finalize_list will have this flag (except object being finalized right now). */
|
||||
DUK_ASSERT(!DUK_HEAPHDR_HAS_FINALIZED(curr)); /* Queueing code ensures. */
|
||||
DUK_ASSERT(!DUK_HEAPHDR_HAS_READONLY(curr)); /* ROM objects never get freed (or finalized). */
|
||||
|
||||
#if defined(DUK_USE_ASSERTIONS)
|
||||
DUK_ASSERT(heap->currently_finalizing == NULL);
|
||||
heap->currently_finalizing = curr;
|
||||
#endif
|
||||
|
||||
/* Clear FINALIZABLE for object being finalized, so that
|
||||
* duk_push_heapptr() can properly ignore the object.
|
||||
*/
|
||||
DUK_HEAPHDR_CLEAR_FINALIZABLE(curr);
|
||||
|
||||
if (DUK_LIKELY(!heap->pf_skip_finalizers)) {
|
||||
/* Run the finalizer, duk_heap_run_finalizer() sets
|
||||
* and checks for FINALIZED to prevent the finalizer
|
||||
* from executing multiple times per finalization cycle.
|
||||
* (This safeguard shouldn't be actually needed anymore).
|
||||
*/
|
||||
|
||||
#if defined(DUK_USE_REFERENCE_COUNTING)
|
||||
duk_bool_t had_zero_refcount;
|
||||
#endif
|
||||
|
||||
/* The object's refcount is >0 throughout so it won't be
|
||||
* refzero processed prematurely.
|
||||
*/
|
||||
#if defined(DUK_USE_REFERENCE_COUNTING)
|
||||
DUK_ASSERT(DUK_HEAPHDR_GET_REFCOUNT(curr) >= 1);
|
||||
had_zero_refcount = (DUK_HEAPHDR_GET_REFCOUNT(curr) == 1); /* Preincremented on finalize_list insert. */
|
||||
#endif
|
||||
|
||||
DUK_ASSERT(!DUK_HEAPHDR_HAS_FINALIZED(curr));
|
||||
duk_heap_run_finalizer(heap, (duk_hobject *) curr); /* must never longjmp */
|
||||
DUK_ASSERT(DUK_HEAPHDR_HAS_FINALIZED(curr));
|
||||
/* XXX: assert that object is still in finalize_list
|
||||
* when duk_push_heapptr() allows automatic rescue.
|
||||
*/
|
||||
|
||||
#if defined(DUK_USE_REFERENCE_COUNTING)
|
||||
DUK_DD(DUK_DDPRINT("refcount after finalizer (includes bump): %ld", (long) DUK_HEAPHDR_GET_REFCOUNT(curr)));
|
||||
if (DUK_HEAPHDR_GET_REFCOUNT(curr) == 1) { /* Only artificial bump in refcount? */
|
||||
#if defined(DUK_USE_DEBUG)
|
||||
if (had_zero_refcount) {
|
||||
DUK_DD(DUK_DDPRINT("finalized object's refcount is zero -> free immediately (refcount queued)"));
|
||||
} else {
|
||||
DUK_DD(DUK_DDPRINT("finalized object's refcount is zero -> free immediately (mark-and-sweep queued)"));
|
||||
}
|
||||
#endif
|
||||
queue_back = 0;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
#if defined(DUK_USE_REFERENCE_COUNTING)
|
||||
queue_back = 1;
|
||||
if (had_zero_refcount) {
|
||||
/* When finalization is triggered
|
||||
* by refzero and we queue the object
|
||||
* back, clear FINALIZED right away
|
||||
* so that the object can be refinalized
|
||||
* immediately if necessary.
|
||||
*/
|
||||
DUK_HEAPHDR_CLEAR_FINALIZED(curr);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
/* Used during heap destruction: don't actually run finalizers
|
||||
* because we're heading into forced finalization. Instead,
|
||||
* queue finalizable objects back to the heap_allocated list.
|
||||
*/
|
||||
DUK_D(DUK_DPRINT("skip finalizers flag set, queue object to heap_allocated without finalizing"));
|
||||
DUK_ASSERT(!DUK_HEAPHDR_HAS_FINALIZED(curr));
|
||||
#if defined(DUK_USE_REFERENCE_COUNTING)
|
||||
queue_back = 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Dequeue object from finalize_list. Note that 'curr' may no
|
||||
* longer be finalize_list head because new objects may have
|
||||
* been queued to the list. As a result we can't optimize for
|
||||
* the single-linked heap case and must scan the list for
|
||||
* removal, typically the scan is very short however.
|
||||
*/
|
||||
DUK_HEAP_REMOVE_FROM_FINALIZE_LIST(heap, curr);
|
||||
|
||||
/* Queue back to heap_allocated or free immediately. */
|
||||
#if defined(DUK_USE_REFERENCE_COUNTING)
|
||||
if (queue_back) {
|
||||
/* FINALIZED is only cleared if object originally
|
||||
* queued for finalization by refcounting. For
|
||||
* mark-and-sweep FINALIZED is left set, so that
|
||||
* next mark-and-sweep round can make a rescue/free
|
||||
* decision.
|
||||
*/
|
||||
DUK_ASSERT(DUK_HEAPHDR_GET_REFCOUNT(curr) >= 1);
|
||||
DUK_HEAPHDR_PREDEC_REFCOUNT(curr); /* Remove artificial refcount bump. */
|
||||
DUK_HEAPHDR_CLEAR_FINALIZABLE(curr);
|
||||
DUK_HEAP_INSERT_INTO_HEAP_ALLOCATED(heap, curr);
|
||||
} else {
|
||||
/* No need to remove the refcount bump here. */
|
||||
DUK_ASSERT(DUK_HEAPHDR_GET_TYPE(curr) == DUK_HTYPE_OBJECT); /* currently, always the case */
|
||||
DUK_DD(DUK_DDPRINT("refcount finalize after finalizer call: %!O", curr));
|
||||
duk_hobject_refcount_finalize_norz(heap, (duk_hobject *) curr);
|
||||
duk_free_hobject(heap, (duk_hobject *) curr);
|
||||
DUK_DD(DUK_DDPRINT("freed hobject after finalization: %p", (void *) curr));
|
||||
}
|
||||
#else /* DUK_USE_REFERENCE_COUNTING */
|
||||
DUK_HEAPHDR_CLEAR_FINALIZABLE(curr);
|
||||
DUK_HEAP_INSERT_INTO_HEAP_ALLOCATED(heap, curr);
|
||||
#endif /* DUK_USE_REFERENCE_COUNTING */
|
||||
|
||||
#if defined(DUK_USE_DEBUG)
|
||||
count++;
|
||||
#endif
|
||||
|
||||
#if defined(DUK_USE_ASSERTIONS)
|
||||
DUK_ASSERT(heap->currently_finalizing != NULL);
|
||||
heap->currently_finalizing = NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* finalize_list will always be processed completely. */
|
||||
DUK_ASSERT(heap->finalize_list == NULL);
|
||||
|
||||
#if 0
|
||||
/* While NORZ macros are used above, this is unnecessary because the
|
||||
* only pending side effects are now finalizers, and finalize_list is
|
||||
* empty.
|
||||
*/
|
||||
DUK_REFZERO_CHECK_SLOW(heap->heap_thread);
|
||||
#endif
|
||||
|
||||
/* Prevent count may be bumped while finalizers run, but should always
|
||||
* be reliably unbumped by the time we get here.
|
||||
*/
|
||||
DUK_ASSERT(heap->pf_prevent_count == 1);
|
||||
heap->pf_prevent_count = 0;
|
||||
|
||||
#if defined(DUK_USE_DEBUG)
|
||||
DUK_DD(DUK_DDPRINT("duk_heap_process_finalize_list: %ld finalizers called", (long) count));
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* Run an duk_hobject finalizer. Must never throw an uncaught error
|
||||
* (but may throw caught errors).
|
||||
*
|
||||
* There is no return value. Any return value or error thrown by
|
||||
* the finalizer is ignored (although errors are debug logged).
|
||||
*
|
||||
* Notes:
|
||||
*
|
||||
* - The finalizer thread 'top' assertions are there because it is
|
||||
* critical that strict stack policy is observed (i.e. no cruft
|
||||
* left on the finalizer stack).
|
||||
*/
|
||||
|
||||
DUK_LOCAL duk_ret_t duk__finalize_helper(duk_hthread *thr, void *udata) {
|
||||
DUK_ASSERT(thr != NULL);
|
||||
DUK_UNREF(udata);
|
||||
|
||||
DUK_DDD(DUK_DDDPRINT("protected finalization helper running"));
|
||||
|
||||
/* [... obj] */
|
||||
|
||||
/* _Finalizer property is read without checking if the value is
|
||||
* callable or even exists. This is intentional, and handled
|
||||
* by throwing an error which is caught by the safe call wrapper.
|
||||
*
|
||||
* XXX: Finalizer lookup should traverse the prototype chain (to allow
|
||||
* inherited finalizers) but should not invoke accessors or proxy object
|
||||
* behavior. At the moment this lookup will invoke proxy behavior, so
|
||||
* caller must ensure that this function is not called if the target is
|
||||
* a Proxy.
|
||||
*/
|
||||
duk_get_prop_stridx_short(thr, -1, DUK_STRIDX_INT_FINALIZER); /* -> [... obj finalizer] */
|
||||
duk_dup_m2(thr);
|
||||
duk_push_boolean(thr, DUK_HEAP_HAS_FINALIZER_NORESCUE(thr->heap));
|
||||
DUK_DDD(DUK_DDDPRINT("calling finalizer"));
|
||||
duk_call(thr, 2); /* [ ... obj finalizer obj heapDestruct ] -> [ ... obj retval ] */
|
||||
DUK_DDD(DUK_DDDPRINT("finalizer returned successfully"));
|
||||
return 0;
|
||||
|
||||
/* Note: we rely on duk_safe_call() to fix up the stack for the caller,
|
||||
* so we don't need to pop stuff here. There is no return value;
|
||||
* caller determines rescued status based on object refcount.
|
||||
*/
|
||||
}
|
||||
|
||||
DUK_INTERNAL void duk_heap_run_finalizer(duk_heap *heap, duk_hobject *obj) {
|
||||
duk_hthread *thr;
|
||||
duk_ret_t rc;
|
||||
#if defined(DUK_USE_ASSERTIONS)
|
||||
duk_idx_t entry_top;
|
||||
#endif
|
||||
|
||||
DUK_DD(DUK_DDPRINT("running duk_hobject finalizer for object: %p", (void *) obj));
|
||||
|
||||
DUK_ASSERT(heap != NULL);
|
||||
DUK_ASSERT(heap->heap_thread != NULL);
|
||||
thr = heap->heap_thread;
|
||||
DUK_ASSERT(obj != NULL);
|
||||
DUK_ASSERT_VALSTACK_SPACE(heap->heap_thread, 1);
|
||||
|
||||
#if defined(DUK_USE_ASSERTIONS)
|
||||
entry_top = duk_get_top(thr);
|
||||
#endif
|
||||
/*
|
||||
* Get and call the finalizer. All of this must be wrapped
|
||||
* in a protected call, because even getting the finalizer
|
||||
* may trigger an error (getter may throw one, for instance).
|
||||
*/
|
||||
|
||||
/* ROM objects could inherit a finalizer, but they are never deemed
|
||||
* unreachable by mark-and-sweep, and their refcount never falls to 0.
|
||||
*/
|
||||
DUK_ASSERT(!DUK_HEAPHDR_HAS_READONLY((duk_heaphdr *) obj));
|
||||
|
||||
/* Duktape 2.1: finalize_list never contains objects with FINALIZED
|
||||
* set, so no need to check here.
|
||||
*/
|
||||
DUK_ASSERT(!DUK_HEAPHDR_HAS_FINALIZED((duk_heaphdr *) obj));
|
||||
#if 0
|
||||
if (DUK_HEAPHDR_HAS_FINALIZED((duk_heaphdr *) obj)) {
|
||||
DUK_D(DUK_DPRINT("object already finalized, avoid running finalizer twice: %!O", obj));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
DUK_HEAPHDR_SET_FINALIZED((duk_heaphdr *) obj); /* ensure never re-entered until rescue cycle complete */
|
||||
|
||||
#if defined(DUK_USE_ES6_PROXY)
|
||||
if (DUK_HOBJECT_IS_PROXY(obj)) {
|
||||
/* This may happen if duk_set_finalizer() or Duktape.fin() is
|
||||
* called for a Proxy object. In such cases the fast finalizer
|
||||
* flag will be set on the Proxy, not the target, and neither
|
||||
* will be finalized.
|
||||
*/
|
||||
DUK_D(DUK_DPRINT("object is a Proxy, skip finalizer call"));
|
||||
return;
|
||||
}
|
||||
#endif /* DUK_USE_ES6_PROXY */
|
||||
|
||||
duk_push_hobject(thr, obj); /* this also increases refcount by one */
|
||||
rc = duk_safe_call(thr, duk__finalize_helper, NULL /*udata*/, 0 /*nargs*/, 1 /*nrets*/); /* -> [... obj retval/error] */
|
||||
DUK_ASSERT_TOP(thr, entry_top + 2); /* duk_safe_call discipline */
|
||||
|
||||
if (rc != DUK_EXEC_SUCCESS) {
|
||||
/* Note: we ask for one return value from duk_safe_call to get this
|
||||
* error debugging here.
|
||||
*/
|
||||
DUK_D(DUK_DPRINT("wrapped finalizer call failed for object %p (ignored); error: %!T",
|
||||
(void *) obj, (duk_tval *) duk_get_tval(thr, -1)));
|
||||
}
|
||||
duk_pop_2(thr); /* -> [...] */
|
||||
|
||||
DUK_ASSERT_TOP(thr, entry_top);
|
||||
}
|
||||
|
||||
#else /* DUK_USE_FINALIZER_SUPPORT */
|
||||
|
||||
/* nothing */
|
||||
|
||||
#endif /* DUK_USE_FINALIZER_SUPPORT */
|
||||
Reference in New Issue
Block a user