/* ----------------------------------------------------------------------------- * * (c) The GHC Team, 1998-2021 * * Support for mapping info table pointers to source locations * * ---------------------------------------------------------------------------*/ #include "rts/PosixSource.h" #include "Rts.h" #include "Capability.h" #include "Hash.h" #include "IPE.h" #include "Printer.h" #include "Profiling.h" #include "RtsUtils.h" #include #include #if defined(TRACING) #include "Trace.h" #endif /* Note [The Info Table Provenance Entry (IPE) Map] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ IPEs are stored in a hash map from info table address (pointer) to IPE. This ensures cheap lookup and traversal. Unfortunately, inserting into the hash map is relatively expensive. To keep startup times low, there's a temporary data structure that is optimized for collecting IPE lists on registration. It's a singly linked list of IPE list buffers (IpeBufferListNode). These are emitted by the code generator, with generally one produced per module. Each contains an array of IPE entries and a link field (which is used to link buffers onto the pending list. For reasons of space efficiency, IPE entries are represented slightly differently in the object file than the InfoProvEnt which we ultimately expose to the user. Specifically, the IPEs in IpeBufferListNode are represented by IpeBufferEntrys, along with a corresponding string table. The string fields of InfoProvEnt are represented in IpeBufferEntry as 32-bit offsets into the string table. This allows us to halve the size of the buffer entries on 64-bit machines while significantly reducing the number of needed relocations, reducing linking cost. Moreover, the code generator takes care to deduplicate strings when generating the string table. When we insert a set of IpeBufferEntrys into the IPE hash-map we convert them to InfoProvEnts, which contain proper string pointers. Building the hash map is done lazily, i.e. on first lookup or traversal. For this all IPE lists of all IpeBufferListNode are traversed to insert all IPEs. After the content of a IpeBufferListNode has been inserted, it's freed. */ static Mutex ipeMapLock; static HashTable *ipeMap = NULL; // Accessed atomically static IpeBufferListNode *ipeBufferList = NULL; #if defined(THREADED_RTS) void initIpe(void) { initMutex(&ipeMapLock); } void exitIpe(void) { closeMutex(&ipeMapLock); } #else void initIpe(void) { } void exitIpe(void) { } #endif // THREADED_RTS static InfoProvEnt ipeBufferEntryToIpe(const IpeBufferListNode *node, const IpeBufferEntry *ent) { const char *strings = node->string_table; return (InfoProvEnt) { .info = ent->info, .prov = { .table_name = &strings[ent->table_name], .closure_desc = &strings[ent->closure_desc], .ty_desc = &strings[ent->ty_desc], .label = &strings[ent->label], .module = &strings[ent->module_name], .src_file = &strings[ent->src_file], .src_span = &strings[ent->src_span] } }; } #if defined(TRACING) static void traceIPEFromHashTable(void *data STG_UNUSED, StgWord key STG_UNUSED, const void *value) { InfoProvEnt *ipe = (InfoProvEnt *)value; traceIPE(ipe); } void dumpIPEToEventLog(void) { // Dump pending entries IpeBufferListNode *cursor = RELAXED_LOAD(&ipeBufferList); while (cursor != NULL) { for (uint32_t i = 0; i < cursor->count; i++) { const InfoProvEnt ent = ipeBufferEntryToIpe(cursor, &cursor->entries[i]); traceIPE(&ent); } cursor = cursor->next; } // Dump entries already in hashmap ACQUIRE_LOCK(&ipeMapLock); if (ipeMap != NULL) { mapHashTable(ipeMap, NULL, &traceIPEFromHashTable); } RELEASE_LOCK(&ipeMapLock); } #else void dumpIPEToEventLog(void) { } #endif /* Registering IPEs Adds the ent_list to the temporary buffer structure described in Note [The Info Table Provenance Entry (IPE) Map]. Statically initialized IPE lists are registered at startup by a C constructor function generated by the compiler (CodeOutput.hs) in a *.c file for each module. Since this is called in a static initializer we cannot rely on ipeMapLock; we instead use atomic CAS operations to add to the list. A performance test for IPE registration and lookup can be found here: https://gitlab.haskell.org/ghc/ghc/-/merge_requests/5724#note_370806 */ void registerInfoProvList(IpeBufferListNode *node) { while (true) { IpeBufferListNode *old = RELAXED_LOAD(&ipeBufferList); node->next = old; if (cas_ptr((volatile void **) &ipeBufferList, old, node) == (void *) old) { return; } } } InfoProvEnt *lookupIPE(const StgInfoTable *info) { updateIpeMap(); return lookupHashTable(ipeMap, (StgWord)info); } void updateIpeMap() { // Check if there's any work at all. If not so, we can circumvent locking, // which decreases performance. IpeBufferListNode *pending = xchg_ptr((void **) &ipeBufferList, NULL); if (ipeMap != NULL && pending == NULL) { return; } ACQUIRE_LOCK(&ipeMapLock); if (ipeMap == NULL) { ipeMap = allocHashTable(); } while (pending != NULL) { IpeBufferListNode *currentNode = pending; InfoProvEnt *ip_ents = stgMallocBytes(sizeof(InfoProvEnt) * currentNode->count, "updateIpeMap"); for (uint32_t i = 0; i < currentNode->count; i++) { const IpeBufferEntry *ent = ¤tNode->entries[i]; ip_ents[i] = ipeBufferEntryToIpe(currentNode, ent); insertHashTable(ipeMap, (StgWord) ent->info, &ip_ents[i]); } pending = currentNode->next; } RELEASE_LOCK(&ipeMapLock); }