/*
 * tclThreadStorage.c --
 *
 *	This file implements platform independent thread storage operations.
 *
 * Copyright (c) 2003-2004 by Joe Mistachkin
 *
 * See the file "license.terms" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 */

#include "tclInt.h"

#if defined(TCL_THREADS)

/*
 * This is the thread storage cache array and it's accompanying mutex. The
 * elements are pairs of thread Id and an associated hash table pointer; the
 * hash table being pointed to contains the thread storage for it's associated
 * thread. The purpose of this cache is to minimize the number of hash table
 * lookups in the master thread storage hash table.
 */

static Tcl_Mutex threadStorageLock;

/*
 * This is the struct used for a thread storage cache slot. It contains the
 * owning thread Id and the associated hash table pointer.
 */

typedef struct ThreadStorage {
    Tcl_ThreadId id;		/* the owning thread id */
    Tcl_HashTable *hashTablePtr;/* the hash table for the thread */
} ThreadStorage;

/*
 * These are the prototypes for the custom hash table allocation functions
 * used by the thread storage subsystem.
 */

static Tcl_HashEntry *	AllocThreadStorageEntry(Tcl_HashTable *tablePtr,
			    void *keyPtr);
static void		FreeThreadStorageEntry(Tcl_HashEntry *hPtr);
static Tcl_HashTable *	ThreadStorageGetHashTable(Tcl_ThreadId id);

/*
 * This is the hash key type for thread storage. We MUST use this in
 * combination with the new hash key type flag TCL_HASH_KEY_SYSTEM_HASH
 * because these hash tables MAY be used by the threaded memory allocator.
 */

static Tcl_HashKeyType tclThreadStorageHashKeyType = {
    TCL_HASH_KEY_TYPE_VERSION,		/* version */
    TCL_HASH_KEY_SYSTEM_HASH | TCL_HASH_KEY_RANDOMIZE_HASH,
				        /* flags */
    NULL,				/* hashKeyProc */
    NULL,				/* compareKeysProc */
    AllocThreadStorageEntry,		/* allocEntryProc */
    FreeThreadStorageEntry		/* freeEntryProc */
};

/*
 * This is an invalid thread value.
 */

#define STORAGE_INVALID_THREAD  (Tcl_ThreadId)0

/*
 * This is the value for an invalid thread storage key.
 */

#define STORAGE_INVALID_KEY	0

/*
 * This is the first valid key for use by external callers. All the values
 * below this are RESERVED for future use.
 */

#define STORAGE_FIRST_KEY	1

/*
 * This is the default number of thread storage cache slots. This define may
 * need to be fine tuned for maximum performance.
 */

#define STORAGE_CACHE_SLOTS	97

/*
 * This is the master thread storage hash table. It is keyed on thread Id and
 * contains values that are hash tables for each thread. The thread specific
 * hash tables contain the actual thread storage.
 */

static Tcl_HashTable threadStorageHashTable;

/*
 * This is the next thread data key value to use. We increment this everytime
 * we "allocate" one. It is initially set to 1 in TclInitThreadStorage.
 */

static int nextThreadStorageKey = STORAGE_INVALID_KEY;

/*
 * This is the master thread storage cache. Per Kevin Kenny's idea, this
 * prevents unnecessary lookups for threads that use a lot of thread storage.
 */

static volatile ThreadStorage threadStorageCache[STORAGE_CACHE_SLOTS];

/*
 *----------------------------------------------------------------------
 *
 * AllocThreadStorageEntry --
 *
 *	Allocate space for a Tcl_HashEntry using TclpSysAlloc (not ckalloc).
 *	We do this because the threaded memory allocator MAY use the thread
 *	storage hash tables.
 *
 * Results:
 *	The return value is a pointer to the created entry.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static Tcl_HashEntry *
AllocThreadStorageEntry(
    Tcl_HashTable *tablePtr,	/* Hash table. */
    void *keyPtr)		/* Key to store in the hash table entry. */
{
    Tcl_HashEntry *hPtr;

    hPtr = (Tcl_HashEntry *) TclpSysAlloc(sizeof(Tcl_HashEntry), 0);
    hPtr->key.oneWordValue = keyPtr;
    hPtr->clientData = NULL;
    
    return hPtr;
}

/*
 *----------------------------------------------------------------------
 *
 * FreeThreadStorageEntry --
 *
 *	Frees space for a Tcl_HashEntry using TclpSysFree (not ckfree). We do
 *	this because the threaded memory allocator MAY use the thread storage
 *	hash tables.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static void
FreeThreadStorageEntry(
    Tcl_HashEntry *hPtr)	/* Hash entry to free. */
{
    TclpSysFree((char *) hPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * ThreadStorageGetHashTable --
 *
 *	This procedure returns a hash table pointer to be used for thread
 *	storage for the specified thread. 
 *
 * Results:
 *	A hash table pointer for the specified thread, or NULL if the hash
 *	table has not been created yet.
 *
 * Side effects:
 *	May change an entry in the master thread storage cache to point to the
 *	specified thread and it's associated hash table.
 *
 * Thread safety:
 *	This function assumes that integer operations are safe (atomic)
 *	on all (currently) supported Tcl platforms.  Hence there are 
 * 	places where shared integer arithmetic is done w/o protective locks.    
 *
 *----------------------------------------------------------------------
 */

static Tcl_HashTable *
ThreadStorageGetHashTable(
    Tcl_ThreadId id)		/* Id of thread to get hash table for */
{
    int index = PTR2UINT(id) % STORAGE_CACHE_SLOTS;
    Tcl_HashEntry *hPtr;
    int isNew;
    Tcl_HashTable *hashTablePtr;

    /*
     * It's important that we pick up the hash table pointer BEFORE comparing
     * thread Id in case another thread is in the critical region changing
     * things out from under you.
     *
     * Thread safety: threadStorageCache is accessed w/o locks in order to
     * avoid serialization of all threads at this hot-spot. It is safe to
     * do this here because (threadStorageCache[index].id != id) test below
     * should be atomic on all (currently) supported platforms and there 
     * are no devastatig side effects of the test.
     *
     * Note Valgrind users: this place will show up as a race-condition in
     * helgrind-tool output. To silence this warnings, define VALGRIND
     * symbol at compilation time. 
     */

#if !defined(VALGRIND)
    hashTablePtr = threadStorageCache[index].hashTablePtr;
    if (threadStorageCache[index].id != id) {
	Tcl_MutexLock(&threadStorageLock);
#else
    Tcl_MutexLock(&threadStorageLock);
    hashTablePtr = threadStorageCache[index].hashTablePtr;
    if (threadStorageCache[index].id != id) {
#endif

	/*
	 * It's not in the cache, so we look it up...
	 */

	hPtr = Tcl_FindHashEntry(&threadStorageHashTable, (char *) id);

	if (hPtr != NULL) {
	    /*
	     * We found it, extract the hash table pointer.
	     */

	    hashTablePtr = Tcl_GetHashValue(hPtr);
	} else {
	    /*
	     * The thread specific hash table is not found.
	     */

	    hashTablePtr = NULL;
	}

	if (hashTablePtr == NULL) {
	    hashTablePtr = (Tcl_HashTable *)
		    TclpSysAlloc(sizeof(Tcl_HashTable), 0);

	    if (hashTablePtr == NULL) {
		Tcl_Panic("could not allocate thread specific hash table, "
			"TclpSysAlloc failed from ThreadStorageGetHashTable!");
	    }
	    Tcl_InitCustomHashTable(hashTablePtr, TCL_CUSTOM_TYPE_KEYS,
		    &tclThreadStorageHashKeyType);

	    /*
	     * Add new thread storage hash table to the master hash table.
	     */

	    hPtr = Tcl_CreateHashEntry(&threadStorageHashTable, (char *) id,
		    &isNew);

	    if (hPtr == NULL) {
		Tcl_Panic("Tcl_CreateHashEntry failed from "
			"ThreadStorageGetHashTable!");
	    }
	    Tcl_SetHashValue(hPtr, hashTablePtr);
	}

	/*
	 * Now, we put it in the cache since it is highly likely it will be
	 * needed again shortly.
	 */

	threadStorageCache[index].id = id;
	threadStorageCache[index].hashTablePtr = hashTablePtr;
#if !defined(VALGRIND)
	Tcl_MutexUnlock(&threadStorageLock);
    }
#else
    }
    Tcl_MutexUnlock(&threadStorageLock);
#endif

    return hashTablePtr;
}

/*
 *----------------------------------------------------------------------
 *
 * TclInitThreadStorage --
 *
 *	Initializes the thread storage allocator.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	This procedure initializes the master hash table that maps thread ID
 *	onto the individual index tables that map thread data key to thread
 *	data. It also creates a cache that enables fast lookup of the thread
 *	data block array for a recently executing thread without using
 *	spinlocks.
 *
 * This procedure is called from an extremely early point in Tcl's
 * initialization. In particular, it may not use ckalloc/ckfree because they
 * may depend on thread-local storage (it uses TclpSysAlloc and TclpSysFree
 * instead). It may not depend on synchronization primitives - but no threads
 * other than the master thread have yet been launched.
 *
 *----------------------------------------------------------------------
 */

void
TclInitThreadStorage(void)
{
    Tcl_InitCustomHashTable(&threadStorageHashTable, TCL_CUSTOM_TYPE_KEYS,
	    &tclThreadStorageHashKeyType);

    /*
     * We also initialize the cache.
     */

    memset((void*) &threadStorageCache, 0,
	    sizeof(ThreadStorage) * STORAGE_CACHE_SLOTS);

    /*
     * Now, we set the first value to be used for a thread data key.
     */

    nextThreadStorageKey = STORAGE_FIRST_KEY;
}

/*
 *----------------------------------------------------------------------
 *
 * TclpThreadDataKeyGet --
 *
 *	This procedure returns a pointer to a block of thread local storage.
 *
 * Results:
 *	A thread-specific pointer to the data structure, or NULL if the memory
 *	has not been assigned to this key for this thread.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

void *
TclpThreadDataKeyGet(
    Tcl_ThreadDataKey *keyPtr)	/* Identifier for the data chunk, really
				 * (int**) */
{
    Tcl_HashTable *hashTablePtr =
	    ThreadStorageGetHashTable(Tcl_GetCurrentThread());
    Tcl_HashEntry *hPtr = Tcl_FindHashEntry(hashTablePtr, (char *) keyPtr);

    if (hPtr == NULL) {
	return NULL;
    }
    return Tcl_GetHashValue(hPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * TclpThreadDataKeySet --
 *
 *	This procedure sets the pointer to a block of thread local storage.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Sets up the thread so future calls to TclpThreadDataKeyGet with this
 *	key will return the data pointer.
 *
 *----------------------------------------------------------------------
 */

void
TclpThreadDataKeySet(
    Tcl_ThreadDataKey *keyPtr,	/* Identifier for the data chunk, really
				 * (pthread_key_t **) */
    void *data)			/* Thread local storage */
{
    Tcl_HashTable *hashTablePtr;
    Tcl_HashEntry *hPtr;
    int dummy;

    hashTablePtr = ThreadStorageGetHashTable(Tcl_GetCurrentThread());
    hPtr = Tcl_CreateHashEntry(hashTablePtr, (char *)keyPtr, &dummy);

    Tcl_SetHashValue(hPtr, data);
}

/*
 *----------------------------------------------------------------------
 *
 * TclpFinalizeThreadDataThread --
 *
 *	This procedure cleans up the thread storage hash table for the
 *	current thread.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Frees all associated thread storage, all hash table entries for
 *	the thread's thread storage, and the hash table itself.
 *
 *----------------------------------------------------------------------
 */

void
TclpFinalizeThreadDataThread(void)
{
    Tcl_ThreadId id = Tcl_GetCurrentThread();
				/* Id of the thread to finalize. */
    int index = PTR2UINT(id) % STORAGE_CACHE_SLOTS;
    Tcl_HashEntry *hPtr;	/* Hash entry for current thread in master
				 * table. */
    Tcl_HashTable* hashTablePtr;/* Pointer to the hash table holding TSD
				 * blocks for the current thread*/
    Tcl_HashSearch search;	/* Search object to walk the TSD blocks in the
				 * designated thread */
    Tcl_HashEntry *hPtr2;	/* Hash entry for a TSD block in the
				 * designated thread. */

    Tcl_MutexLock(&threadStorageLock);
    hPtr = Tcl_FindHashEntry(&threadStorageHashTable, (char*)id);
    if (hPtr == NULL) {
	hashTablePtr = NULL;
    } else {
	/*
	 * We found it, extract the hash table pointer.
	 */

	hashTablePtr = Tcl_GetHashValue(hPtr);
	Tcl_DeleteHashEntry(hPtr);

	/*
	 * Make sure cache entry for this thread is NULL.
	 */

	if (threadStorageCache[index].id == id) {
	    /*
	     * We do not step on another thread's cache entry. This is
	     * especially important if we are creating and exiting a lot of
	     * threads.
	     */

	    threadStorageCache[index].id = STORAGE_INVALID_THREAD;
	    threadStorageCache[index].hashTablePtr = NULL;
	}
    }
    Tcl_MutexUnlock(&threadStorageLock);

    /*
     * The thread's hash table has been extracted and removed from the master
     * hash table. Now clean up the thread.
     */

    if (hashTablePtr != NULL) {
	/*
	 * Free all TSD
	 */

	for (hPtr2 = Tcl_FirstHashEntry(hashTablePtr, &search); hPtr2 != NULL;
		hPtr2 = Tcl_NextHashEntry(&search)) {
	    void *blockPtr = Tcl_GetHashValue(hPtr2);

	    if (blockPtr != NULL) {
		/*
		 * The block itself was allocated in Tcl_GetThreadData using
		 * ckalloc; use ckfree to dispose of it.
		 */

		ckfree(blockPtr);
	    }
	}

	/*
	 * Delete thread specific hash table and free the struct.
	 */

	Tcl_DeleteHashTable(hashTablePtr);
	TclpSysFree((char *) hashTablePtr);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * TclFinalizeThreadStorage --
 *
 *	This procedure cleans up the master thread storage hash table, all
 *	thread specific hash tables, and the thread storage cache.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The master thread storage hash table and thread storage cache are
 *	reset to their initial (empty) state.
 *
 *----------------------------------------------------------------------
 */

void
TclFinalizeThreadStorage(void)
{
    Tcl_HashSearch search;	/* We need to hit every thread with this
				 * search. */
    Tcl_HashEntry *hPtr;	/* Hash entry for current thread in master
				 * table. */
    Tcl_MutexLock(&threadStorageLock);

    /*
     * We are going to delete the hash table for every thread now. This hash
     * table should be empty at this point, except for one entry for the
     * current thread.
     */

    for (hPtr = Tcl_FirstHashEntry(&threadStorageHashTable, &search);
	    hPtr != NULL; hPtr = Tcl_NextHashEntry(&search)) {
	Tcl_HashTable *hashTablePtr = Tcl_GetHashValue(hPtr);

	if (hashTablePtr != NULL) {
	    /*
	     * Delete thread specific hash table for the thread in question
	     * and free the struct.
	     */

	    Tcl_DeleteHashTable(hashTablePtr);
	    TclpSysFree((char *)hashTablePtr);
	}

	/*
	 * Delete thread specific entry from master hash table.
	 */

	Tcl_SetHashValue(hPtr, NULL);
    }

    Tcl_DeleteHashTable(&threadStorageHashTable);

    /*
     * Clear out the thread storage cache as well.
     */

    memset((void*) &threadStorageCache, 0,
	    sizeof(ThreadStorage) * STORAGE_CACHE_SLOTS);

    /*
     * Reset this to zero, it will be set to STORAGE_FIRST_KEY if the thread
     * storage subsystem gets reinitialized
     */

    nextThreadStorageKey = STORAGE_INVALID_KEY;

    Tcl_MutexUnlock(&threadStorageLock);
}

#else /* !defined(TCL_THREADS) */

/*
 * Stub functions for non-threaded builds
 */

void
TclInitThreadStorage(void)
{
}

void
TclpFinalizeThreadDataThread(void)
{
}

void
TclFinalizeThreadStorage(void)
{
}

#endif /* defined(TCL_THREADS) && defined(USE_THREAD_STORAGE) */

/*
 * Local Variables:
 * mode: c
 * c-basic-offset: 4
 * fill-column: 78
 * End:
 */
