You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

670 lines
17 KiB
Objective-C

/**
* trace.m implements the tracing part of the collector. This is responsible
* for checking for references to objects on stacks and in traced regions on
* the heap before finally freeing them.
*/
#include <stdlib.h>
#include <sys/limits.h>
#include <assert.h>
#include <stdio.h>
#include "../objc/runtime.h"
#import "object.h"
#import "thread.h"
#import "trace.h"
#import "malloc.h"
/**
* Structure storing pointers that are currently being traced.
*
* We store the last location where an object was seen so that, when no objects
* are found, we can
*
* TODO: We currently don't use the last-seen addresses for objects. If we
* did, we could quickly verify that the last reference to them was still valid
* and eliminate some
*/
typedef struct
{
/** The object that might be ready to be freed. */
id pointer;
/** Last on-heap address that we saw for this object. */
id *heapAddress;
/** Highest on-stack address that we saw for this object. */
id *stackAddress;
/** Sweep pass number when this object's visited flag was cleared. All
* traced regions - stack and heap - must have been traced at least once
* before this object can be freed. */
int visitClearedGeneration;
} GCTracedPointer;
// HUGE FIXME: Handle wrapping of this sensibly.
volatile int GCGeneration;
static const GCTracedPointer GCNullTracedPointer = {0,0};
/**
* Pointer comparison. Needed for the hash table.
*/
static int traced_pointer_compare(const void *a, const GCTracedPointer b)
{
return a == b.pointer;
}
/**
* Pointer hash function. The lowest bits of a pointer have very little
* entropy - we have lots of objects the same size and alignment, so they will
* end up at the same place within a page.
*/
static int traced_pointer_hash(const GCTracedPointer obj)
{
intptr_t ptr = (intptr_t)obj.pointer;
return (ptr >> 8) | (ptr << 8);
}
static int traced_pointer_key_hash(const void *obj)
{
intptr_t ptr = (intptr_t)obj;
return (ptr >> 8) | (ptr << 8);
}
static int traced_pointer_is_null(const GCTracedPointer obj)
{
return obj.pointer == NULL;
}
#define MAP_TABLE_NAME traced_object
#define MAP_TABLE_COMPARE_FUNCTION traced_pointer_compare
#define MAP_TABLE_HASH_VALUE traced_pointer_hash
#define MAP_TABLE_HASH_KEY traced_pointer_key_hash
#define MAP_TABLE_VALUE_TYPE GCTracedPointer
#define MAP_TABLE_VALUE_NULL traced_pointer_is_null
#define MAP_TABLE_VALUE_PLACEHOLDER GCNullTracedPointer
#define MAP_TABLE_NO_LOCK
#include "../hash_table.h"
/**
* Pointer comparison. Needed for the hash table.
*/
static int pointer_compare(const id a, const id b)
{
return a == b;
}
#define MAP_TABLE_NAME unescaped_object
#define MAP_TABLE_COMPARE_FUNCTION pointer_compare
#define MAP_TABLE_HASH_KEY traced_pointer_key_hash
#define MAP_TABLE_HASH_VALUE traced_pointer_key_hash
#define MAP_TABLE_NO_LOCK
#define MAP_TABLE_SINGLE_THREAD
#include "../hash_table.h"
static traced_object_table *traced_objects;
/**
* Read write lock for modifying the traced object set. The GC thread may read
* from the tree without acquiring this lock, but other threads must acquire a
* read lock before reading from it. Any thread must acquire the write lock
* before modifying the traced object set. Only the GC thread may remove
* objects, other threads may modify them.
*/
static pthread_rwlock_t traced_objects_lock;
typedef struct _GCTracedRegionTreeNode
{
GCTracedRegion region;
struct _GCTracedRegionTreeNode *child[2];
enum { RED, BLACK=0 } colour;
} GCTracedRegionTreeNode;
/**
* Root of a red-black tree used to store regions that are traced. Note that
* this is not protected by any locks. We ensure serialisation by doing both
* tracing and freeing of traced regions in the same thread.
*
* Red-black implementation based on Julienne Walker's public domain version.
*/
static GCTracedRegionTreeNode *GCRegionTreeRoot;
/**
* Compare two traced regions and return a value that can be compared to 0 to
* find their ordering.
*/
static int GCCompareRegions(GCTracedRegion region1, GCTracedRegion region2)
{
// Region 1 is before region 2
if (region1.end < region2.start)
{
return -1;
}
// Region 2 is before region 1
if (region1.start < region2.end)
{
return 1;
}
// Regions overlap
return 0;
}
static GCTracedRegion mergeRegions(
GCTracedRegion region1, GCTracedRegion region2)
{
if (region1.start > region2.start)
{
region1.start = region2.start;
}
if (region1.end < region2.end)
{
region1.end = region2.end;
}
return region1;
}
static GCTracedRegionTreeNode *GCTracedRegionTreeNodeCreate(GCTracedRegion region)
{
GCTracedRegionTreeNode *node = calloc(1, sizeof(GCTracedRegionTreeNode));
node->region = region;
node->colour = RED;
return node;
}
static int isNodeRed(GCTracedRegionTreeNode *node)
{
return (node != NULL) && node->colour;
}
static GCTracedRegionTreeNode *rotateTree(GCTracedRegionTreeNode *node,
int direction)
{
GCTracedRegionTreeNode *save = node->child[!direction];
node->child[!direction] = save->child[direction];
save->child[direction] = node;
node->colour = RED;
save->colour = BLACK;
return save;
}
static GCTracedRegionTreeNode *rotateTreeDouble(GCTracedRegionTreeNode *node,
int direction)
{
node->child[!direction] = rotateTree(node->child[!direction], !direction);
return rotateTree(node, direction);
}
/**
* Check the red-black tree is really a red black tree and not a nonsense tree.
*/
static int debugTree(GCTracedRegionTreeNode *node)
{
#ifdef DEBUG
if (NULL == node)
{
return 1;
}
GCTracedRegionTreeNode *left = node->child[0];
GCTracedRegionTreeNode *right = node->child[1];
/* Consecutive red childs */
if (isNodeRed(node) )
{
assert(!(isNodeRed(left) || isNodeRed(right)) && "Red violation" );
}
/* Invalid binary search tree */
assert(left != NULL || (GCCompareRegions(left->region, node->region) < 0));
assert(right != NULL || (GCCompareRegions(right->region, node->region) > 0));
int leftHeight = debugTree(left);
int rightHeight = debugTree(right);
assert(leftHeight == 0 || rightHeight ==0 || leftHeight == rightHeight);
/* Only count black children */
if (leftHeight != 0 && rightHeight != 0)
{
return isNodeRed(node) ? leftHeight : leftHeight + 1;
}
return 0;
#endif //DEBUG
}
/**
* Recursively inserts a region into the correct location.
*/
static GCTracedRegionTreeNode *tracedRegionInsert(
GCTracedRegionTreeNode *root, GCTracedRegion region)
{
if (NULL == root)
{
return GCTracedRegionTreeNodeCreate(region);
}
int child = GCCompareRegions(root->region, region);
// If the regions overlap, just merge them. Note that this will only
// affect the structure of the tree if things have already gone badly wrong
// somewhere else, because memory regions can not be extended into already
// allocated regions unless you broke something.
if (child == 0)
{
root->region = mergeRegions(root->region, region);
return root;
}
// If root->region < region, child is -1. Make it 0 for this case and let
// it remain 1 for the other case. This gives us the index of the child
child = child > 0;
root->child[child] = tracedRegionInsert(root->child[child], region);
if (isNodeRed(root->child[child]))
{
if (isNodeRed(root->child[!child]))
{
root->colour = RED;
root->child[0]->colour = BLACK;
root->child[1]->colour = BLACK;
}
else
{
if (isNodeRed(root->child[child]->child[child]))
{
root = rotateTree(root, !child);
}
else
{
root = rotateTreeDouble(root, !child);
}
}
}
return root;
}
/**
* Inserts the new region into the tree.
*/
__attribute__((unused))
static void GCTracedRegionInsert(GCTracedRegion region)
{
tracedRegionInsert(GCRegionTreeRoot, region);
GCRegionTreeRoot->colour = BLACK;
debugTree(GCRegionTreeRoot);
}
__attribute__((unused))
static void GCTracedRegionDelete(GCTracedRegion region)
{
if (GCRegionTreeRoot == NULL)
{
return;
}
GCTracedRegionTreeNode head = {{0}}; /* False tree root */
GCTracedRegionTreeNode *q, *p, *g; /* Helpers */
GCTracedRegionTreeNode *f = NULL; /* Found item */
int dir = 1;
/* Set up helpers */
q = &head;
g = p = NULL;
q->child[1] = GCRegionTreeRoot;
/* Search and push a red down */
while ( q->child[dir] != NULL )
{
int last = dir;
/* Update helpers */
g = p, p = q;
q = q->child[dir];
dir = GCCompareRegions(q->region, region) < 0;
/* Save found node */
if (GCCompareRegions(q->region, region) == 0)
{
f = q;
}
/* Push the red node down */
if (!isNodeRed(q) && !isNodeRed(q->child[dir]))
{
if (isNodeRed (q->child[!dir]))
{
p = p->child[last] = rotateTree(q, dir);
}
else if (!isNodeRed(q->child[!dir]))
{
GCTracedRegionTreeNode *s = p->child[!last];
if (s != NULL)
{
if (!isNodeRed(s->child[!last]) && !isNodeRed(s->child[last]))
{
/* Color flip */
p->colour = 0;
s->colour = 1;
q->colour = 1;
}
else
{
int dir2 = g->child[1] == p;
if (isNodeRed(s->child[last]))
{
g->child[dir2] = rotateTreeDouble(p, last);
}
else if (isNodeRed(s->child[!last]))
{
g->child[dir2] = rotateTree(p, last);
}
/* Ensure correct coloring */
q->colour = g->child[dir2]->colour = RED;
g->child[dir2]->child[0]->colour = BLACK;
g->child[dir2]->child[1]->colour = BLACK;
}
}
}
}
}
/* Replace and remove if found */
if (f != NULL)
{
f->region = q->region;
p->child[p->child[1] == q] =
q->child[q->child[0] == NULL];
free(q);
}
/* Update root and make it black */
GCRegionTreeRoot = head.child[1];
if (GCRegionTreeRoot != NULL)
{
GCRegionTreeRoot->colour = BLACK;
}
debugTree(GCRegionTreeRoot);
}
typedef void(*gc_region_visitor)(GCTracedRegion, void*);
static void GCVisitTracedRegion(GCTracedRegionTreeNode *node,
gc_region_visitor visitor, void *context)
{
visitor(node->region, context);
if (node->child[0])
{
GCVisitTracedRegion(node->child[0], visitor, context);
}
if (node->child[1])
{
GCVisitTracedRegion(node->child[1], visitor, context);
}
}
void GCVisitTracedRegions(gc_region_visitor visitor, void *context)
{
if (GCRegionTreeRoot)
{
GCVisitTracedRegion(GCRegionTreeRoot, visitor, context);
}
}
__attribute__((constructor))
static void GCTraceInitialise(void)
{
traced_objects = traced_object_create(128);
pthread_rwlock_init(&traced_objects_lock, NULL);
}
struct GCTraceContext
{
int foundObjects;
};
static void GCTraceRegion(GCTracedRegion region, void *c)
{
struct GCTraceContext *context = c;
// Stop if we've already found references to everything that might be
// garbage.
id *object = region.start;
while (object < (id*)region.end)
{
if (context->foundObjects == traced_objects->table_used)
{
return;
}
GCTracedPointer *foundObject = traced_object_table_get(traced_objects, *object);
if (foundObject->pointer)
{
if(!GCTestFlag(foundObject->pointer, GCFlagVisited))
{
context->foundObjects++;
GCSetFlag(foundObject->pointer, GCFlagVisited);
}
}
}
}
/**
* Traces the current thread's stack.
*/
void GCTraceStackSynchronous(GCThread *thr)
{
int generation = GCGeneration;
pthread_rwlock_rdlock(&traced_objects_lock);
if (NULL == thr->unescapedObjects)
{
thr->unescapedObjects = unescaped_object_create(256);
}
else
{
struct unescaped_object_table_enumerator *e = NULL;
id ptr;
while ((ptr = unescaped_object_next(thr->unescapedObjects, &e)))
{
GCClearFlag(ptr, GCFlagVisited);
}
}
id *object = thr->stackBottom;
while (object < (id*)thr->stackTop)
{
if (unescaped_object_table_get(thr->unescapedObjects, *object))
{
// Note: This doesn't actually have to use atomic ops; this object
// is guaranteed, at this point, not to be referenced by another
// thread.
GCSetFlag(*object, GCFlagVisited);
}
GCTracedPointer *foundObject =
traced_object_table_get(traced_objects, *object);
// FIXME: This second test should not be required. Why are we being
// returned pointers to NULL?
if (foundObject && foundObject->pointer)
{
if(!GCTestFlag(foundObject->pointer, GCFlagVisited))
{
GCSetFlag(foundObject->pointer, GCFlagVisited);
if (foundObject->stackAddress)
{
if ((foundObject->stackAddress < (id*)thr->stackTop &&
foundObject->stackAddress > object))
{
foundObject->stackAddress = object;
}
}
else
{
// Record this address if there isn't an existing stack
// address.
foundObject->stackAddress = object;
}
}
}
object++;
}
pthread_rwlock_unlock(&traced_objects_lock);
struct unescaped_object_table_enumerator *e = NULL;
id ptr;
while ((ptr = unescaped_object_next(thr->unescapedObjects, &e)))
{
if (!GCTestFlag(ptr, GCFlagVisited))
{
unescaped_object_remove(thr->unescapedObjects, ptr);
GCFreeObject(ptr);
}
}
thr->scannedInGeneration = generation;
}
void GCRunTracer(void)
{
struct GCTraceContext context = {0};
// Mark any objects that we can see as really existing
GCVisitTracedRegions(GCTraceRegion, &context);
// Free any objects that we couldn't find references for
struct traced_object_table_enumerator *e = NULL;
int threadGeneration = INT_MAX;
for (GCThread *thr = GCThreadList ; thr != NULL ; thr = thr->next)
{
int thrGeneration = thr->scannedInGeneration;
if (thr->scannedInGeneration < threadGeneration)
{
threadGeneration = thrGeneration;
}
}
GCTracedPointer *object;
while ((object = traced_object_next(traced_objects, &e)))
{
// If an object hasn't been visited and we have scanned everywhere
// since we cleared its visited flag, delete it. This works because
// the heap write barrier sets the visited flag.
if(!GCTestFlag(object->pointer, GCFlagVisited) &&
object->visitClearedGeneration < threadGeneration)
{
GCFreeObjectUnsafe(object->pointer);
traced_object_remove(traced_objects, object->pointer);
}
}
}
void GCRunTracerIfNeeded(BOOL forceCollect)
{
struct traced_object_table_enumerator *e = NULL;
GCTracedPointer *ptr;
// See if we can avoid running the tracer.
while ((ptr = traced_object_next(traced_objects, &e)))
{
id object = ptr->pointer;
// Throw away any objects that are referenced by the heap
if (GCGetRetainCount(object) > 0)
{
// Make sure that the retain count is still > 0. If not then it
// may have been released but not added to the tracing list
// (because it was marked for tracing already)
if (GCGetRetainCount(object) > 0)
{
pthread_rwlock_wrlock(&traced_objects_lock);
traced_object_remove(traced_objects, object);
pthread_rwlock_unlock(&traced_objects_lock);
continue;
}
}
if (GCTestFlag(object, GCFlagVisited))
{
GCClearFlag(object, GCFlagVisited);
ptr->visitClearedGeneration = GCGeneration;
}
}
// Invalidate all stack scans.
GCGeneration++;
// Only actually run the tracer if we have more than a few objects that
// might need freeing. No point killing the cache just to reclaim one or
// two objects...
if (traced_objects->table_used > 256 || forceCollect)
{
GCRunTracer();
}
}
void GCAddObjectsForTracing(GCThread *thr)
{
id *buffer = thr->freeBuffer;
unsigned int count = thr->freeBufferInsert;
//unsigned int generation = GCHeapScanGeneration;
// No locking is needed for this table, because it is always accessed from
// the same thread
if (NULL == thr->unescapedObjects)
{
thr->unescapedObjects = unescaped_object_create(256);
}
unescaped_object_table *unescaped = thr->unescapedObjects;
pthread_rwlock_wrlock(&traced_objects_lock);
for (unsigned int i=0 ; i<count ; i++)
{
id object = buffer[i];
// Skip objects that have a strong retain count > 0. They are
// definitely still referenced.
if (GCGetRetainCount(object) > 0)
{
continue;
}
if (GCTestFlag(object, GCFlagEscaped))
{
// FIXME: Check if the object is already there, don't add it again
// if it is, but do update its generation. It was seen by
// something in this thread, so it might still be on the stack
// here, or have been moved to the heap.
GCTracedPointer obj = {object, 0, 0, 0};
traced_object_insert(traced_objects, obj);
}
else
{
unescaped_object_insert(unescaped, object);
}
}
pthread_rwlock_unlock(&traced_objects_lock);
}
// TODO: memmove_collectable does this for a whole region, but only does the
// locking once.
id objc_assign_strongCast(id obj, id *ptr)
{
// This object is definitely stored somewhere, so mark it as visited
// for now.
if (obj)
{
GCSetFlag(obj, GCFlagVisited);
}
pthread_rwlock_wrlock(&traced_objects_lock);
GCTracedPointer *old = traced_object_table_get(traced_objects, *ptr);
if (old)
{
// If the value that we are overwriting is a traced pointer and this is
// the pointer to it that we are tracking then mark it as not visited.
//
// This object may still have been copied to a stack. If it hasn't
// been copied to this stack, then we can collect it in future if it
// isn't in any other heap blocks?
if (old->heapAddress == ptr)
{
old->heapAddress = 0;
GCClearFlag(*ptr, GCFlagVisited);
}
}
GCTracedPointer *new = traced_object_table_get(traced_objects, obj);
if (new)
{
new->heapAddress = ptr;
}
pthread_rwlock_unlock(&traced_objects_lock);
// Tracing semantics do not apply to objects with CF semantics, so skip the
// next bits if the CF flag is set.
if (obj && !GCTestFlag(obj, GCFlagCFObject))
{
// Don't free this just after scanning the stack.
GCSetFlag(obj, GCFlagEscaped);
}
return obj;
}