From Gnash Project Wiki
An ActionScript interpreter, like any script interpreter, needs to carefully take memory management into account. Ideally, any resource which is not accessible by any executable code should be released. This is not straighforward to achieve, as resources are shared between script values. Moreover, script values can be of composite types including other values, possibly pointing to themselves or creating reference chains of arbitrary size.
Old (intrusive-ptr based) design
The approach to memory management inherited from GameSWF was based on reference counting and a legacy smart_ptr class, coupled with a weak_ptr class rarely used. When reducing dependency on legacy code we switched to boost::intrusive_ptr instead, which allowed us not to change the underlying structures too much; also we dropped weak_ptr as the only place in which it was used was not really needed.
Pitfalls in old design
Circular dependencies are not properly handled. A set of ref-counted resources forming a chain is *never* deallocated. This simple ActionScript code shows the problem:
var a = new Object; a.member = a; delete a;
The a value will never be released as it would contain a reference to itself, so its refcount will be 1 forever.
A possible fix to this could be defining a "proper" owner (maybe the timeline in which a value is stored) and use weak pointers for every other location (like the Property set of objects).
Bugs in the implementation of old (mis)design
One of the problems with current implementation of the ref-counted approach is that there are mixed use of "smart" and "dumb" pointers. This is easily broken as doesn't ensure proper maintainance of reference counting, often resulting in invalid refcounts. One example is not respecting the RAII idiom. See the following example:
as_function* f = new as_functio(); call_method(as_value(f)); f->blow_up();
The as_value(f) assignment will temporarly assign the newly created object into a smart pointer, thus destroying the object when going out of scope. A fix would be storing the object into a smart pointer immediately:
intrusive_ptr<as_function> f = new as_function(); call_method(as_value(f.get())); f->still_alive();
Anway, the block above is tipically found in functions finally returning the object as a dumb pointer, thus making any effor inside the function body useless:
return f.get(); // intrusive_ptr goes out of scope, tragedy !
In order to fix these, we should make sure that any ref-counted resource allocator returns a smart pointer.
After live discussion about the topic we found what we consider a fairly simple and effective design to deal with memory management for Gnash. Basically we'll be implementing a custom garbage collector for managing complex actionscript variables, which are the only ones that are shared by references. In Gnash these are as_object class instances created by AcitionScript (ie: not those created by playhead control: placeobject/removeobejct). Note that any other value type is not heap-allocated, so does not represent a problem.
The Gnash Garbage Collector
Collected objects are the ones allocated due to ActionScript explicit or implicit calls. Explicit calls are any user-defined statements allocating new objects (arrays, Objects, script-created movieclips). Implicit calls are the built-in functions which do can be deleted by arbitrary user calls.
How are collectable objects managed
Any object which is a candidate for garbage collecting will need to be stored in a list owned by the collector. How would this happen has to be defined (more of a strategy: try to be less intrusive or just start a big rewrite). Anyway this list will be filled by an executing action context, whenever a collectable object is allocated on the heap.
How is garbage collected
Rather then using a threshold to start the collector conditionally, the plan is to start it at the very end of an execution context. Executions contexts are actually triggered by either playhead advancement OR user event, so whether to start the collector for user event triggered execution context has to be given a bit of more thought, to avoid killing performance. Anyway the idea is NOT to use a separate thread for collecting but rather doing it all in a predictable order from the main thread.
Collecting should ideally happen when the VM is in a "stable" state, which is at the end of an execution context. At this point any still-reachable object would have its roots in one of the currently live character instances (stage characters). ActionScript variables, generally speaking are either object properties, local variables, local registers or global registers. Local variables and registers only exists during a function call so we are sure we don't have to care about them at the very end of an execution context. Lifetime of global registers has to be researched on as that would influence what does the collector look at during the initial marking phase.
The collector would basically be a conservative collector, which is: any object on the collectables list will be marked as UNREACHABLE, an iterative scan starting from the roots will mark any still reachable object and a final purge will release still UNREACHABLE resources.
The algorithm will be around these lines:
- INITIALIZATION: all managed objects are constructed in the UNREACHABLE state, no scan needed for initialization.
- MARK SCAN: for each reachable object, if the object isn't marked as REACHABLE then mark it so and recurse.
- CLEANUP SCAN: Release all resources which are still markes as UNREACHABLE, mark the remaining as UNREACHABLE to be ready for next scan.
The MARK scan we start from the "root" objects, which has to be hardcoded as the entry point for reachability. The root objects should be:
- The Stage (movie_root, containing a list of levels)
- The _global object (VM::getGlobal)
- The timers targets
- The global action list targets (movie_root::_actionQueue, ExecutableCode)
- Key, Mouse and Stage listeners (see Listeners)
- Mouse entities (See Active mouse entities)
- Debugger ? [ NO, it seems it doesn't have ref_counted members ]
- Library of movie definitions. (See Movie definitions library)
Note that global registers are *NOT* roots, since they are stored in as_environments associated to sprite_instance. So global register values will be marked by the sprite_instance kept alive.
For Key, Mouse, Stage listeners we have a problem. If we mark them as being reachable, they'll never be released. If we do not mark them as being reachable, we need to make sure the objects' destructor properly unregister self from the listener set.
But in that case we must also make sure the listener container (be it Mouse, Stage or Key) wasn't already deleted by the GC ! Actually, this should not be a problem if there's no way for the object constructor to obtain a pointer to Mouse,Stage or Key listeners. Would be enough for these to be not-reachable.
UPDATE for Key listeners, it has been tested that a deleted object should NOT automatically unregisted self from being a key listener. This means we MUST keep key listeners alive (marked as reachable). This has only be tested for Key object listening, and just for user events.
UPDATE for Stage listeners, it has been tested that a deleted object should NOT automatically unregister self from being a Stage listener. This means we MUST keep Stage listeners alive (marked as reachable).
UPDATE for Mouse listeners, it has been tested that a deleted object should NOT automatically unregisted self from being a mouse listener. This means we MUST keep Mouse listeners alive (marked as reachable). This has only be tested for Mouse object listening, and just for user events.
Active mouse entities
These are the ones stored in the movie_root for mouse events. We should probably keep them alive, as they'll be replaced on next mouse event.
CURRENTLY, THESE ARE MARKED
Movie definitions library
Gnash currently keeps a library of movie definitions. movie_definition subclasses currently do NOT unregister self from the library so we must keep them alive even if the library is the only place that references them. Cleaning this up would be nice, and letting the instances needing those definitions to keep them alive seems a good way to go.
UPDATE We are not managing *definitions* trought the GC anymore, so they now are ref-counted only.
Collected pointers interface
We should desing the interface we want to use for creating garbage-collected pointers. Currently we use the ref-counted interface boost::intrusive_ptr<>. An intrusive_ptr can be copied (which automatically adds ref counting) or can go out of scope (which automatically drops ref counting). When ref count becomes 0 the object is destroied.
Now, keeping the same kind of interface would be possible but no object will be destroied till the garbage collector runs. If we want to allow explicit deletion we might have troubles in case we forget that another gc_ptr exists somewhere else to the same resource. For this danger (forgetting about other users) I'd just avoid having explicit destructors for GC-collected objects.
The downside is that the GC list of collectable objects will likely grow needlessly till next collect() run, which shoudn't be such a big problem anyway...
So, if we like this setup we'll substitute intrusive_ptr by gc_ptr, and use it in the exact same way as before. This would also let us switch between the two management architecture by using a define.
Actually, I'm wrong.. we have other probems :)
The problem is the smart pointer itself. We don't need any smart pointer if we just forbid explicit deletion and have consturctor automatically register to the GC.
- button_action leaks action_buffer instances, deleting in dtor triggers a segfault with clip_as_button2.swf (clicking on the top-right circle)
- any function always leaks as it's 'constructor' member references self.
Profiling the new GC
See ProfilingGC page.
Ideas for future improvements
- Allow explicit deletion of GC-managed pointers. Or, at the very least, force collection on some recently-released object.
- Mix GC and RC models for the same class.
- Implement weak pointers.
- Use a polymorphic pointer class to transport all pointers, regardless of their referent or the collection strategy used.
- Enable use of auto-storage objects as initializers for managed objects with behind-the-scenes allocation.