set threadMonitorLib [apply {{} { set cc [C] $cc cflags -I. -I./vendor/tracy/public $cc include "workqueue.h" $cc include "common.h" $cc code { extern ThreadControlBlock threads[]; extern int _Atomic threadCount; extern Db* db; extern int unsafe_workQueueCopy(WorkQueueItem* into, int maxn, WorkQueue* q); extern void traceItem(char* buf, size_t bufsz, WorkQueueItem item); Jim_Obj* itemToStringObj(WorkQueueItem item) { char buf[10000]; traceItem(buf, sizeof(buf), item); return Jim_NewStringObj(interp, buf, -1); } } $cc proc workerTids {} Jim_Obj* { // Fallback on macOS, where we don't have /proc to query all // threads of the Folk process. Jim_Obj* ret = Jim_NewListObj(interp, NULL, 0); for (int i = 0; i < threadCount; i++) { if (threads[i].tid != 0) { Jim_ListAppendElement(interp, ret, Jim_NewIntObj(interp, threads[i].tid)); } } return ret; } $cc proc workerInfo {int threadIndex} Jim_Obj* { if (threadIndex >= threadCount || threads[threadIndex].tid == 0) { return Jim_NewEmptyStringObj(interp); } ThreadControlBlock *thread = &threads[threadIndex]; Jim_Obj* ret = Jim_NewDictObj(interp, NULL, 0); WorkQueueItem item = thread->currentItem; Jim_DictAddElement(interp, ret, Jim_NewStringObj(interp, "op", -1), itemToStringObj(item)); WorkQueueItem items[100]; int nitems = unsafe_workQueueCopy(items, 100, thread->workQueue); Jim_Obj* workQueueObj = Jim_NewListObj(interp, NULL, 0); for (int i = 0; i < nitems; i++) { Jim_ListAppendElement(interp, workQueueObj, itemToStringObj(items[i])); } Jim_DictAddElement(interp, ret, Jim_NewStringObj(interp, "workQueue", -1), workQueueObj); Jim_DictAddElement(interp, ret, Jim_NewStringObj(interp, "isDeactivated", -1), Jim_NewIntObj(interp, thread->isDeactivated)); Jim_DictAddElement(interp, ret, Jim_NewStringObj(interp, "currentItemStartTimestamp", -1), Jim_ObjPrintf("%" PRId64, thread->currentItemStartTimestamp)); int64_t now = timestamp_get(thread->clockid); Jim_DictAddElement(interp, ret, Jim_NewStringObj(interp, "elapsed", -1), Jim_NewDoubleObj(interp, (double)(now - thread->currentItemStartTimestamp) / 1000.0)); Jim_DictAddElement(interp, ret, Jim_NewStringObj(interp, "_allocs", -1), Jim_NewDoubleObj(interp, thread->_allocs)); Jim_DictAddElement(interp, ret, Jim_NewStringObj(interp, "_frees", -1), Jim_NewDoubleObj(interp, thread->_frees)); return ret; } $cc proc workerInfoFromTid {int tid} Jim_Obj* { for (int i = 0; i < threadCount; i++) { if (threads[i].tid == tid) { return workerInfo(i); } } return Jim_NewEmptyStringObj(interp); } $cc code { #include "vendor/c11-queues/mpmc_queue.h" extern struct mpmc_queue globalWorkQueue; extern _Atomic int globalWorkQueueSize; } $cc proc globalWorkQueueAvailable {} size_t { return mpmc_queue_available(&globalWorkQueue); } # Unsafely peeks at the queue. $cc proc globalWorkQueueItems {} Jim_Obj* { Jim_Obj *ret = Jim_NewListObj(interp, NULL, 0); size_t head = globalWorkQueue.head; for (size_t i = head; i < globalWorkQueue.tail; i++) { struct mpmc_queue_cell *cell = &globalWorkQueue.buffer[i & globalWorkQueue.buffer_mask]; WorkQueueItem *ptr = cell->data; WorkQueueItem item = *ptr; Jim_ListAppendElement(interp, ret, itemToStringObj(item)); } return ret; } return [$cc compile] }}] Wish the web server handles route "/threads" with handler { set pid [pid] try { set tids [glob -tails -directory /proc/$pid/task *] set userStacks [dict create] set euStackOutput [exec eu-stack --pid=[pid] --verbose] set currentTid none foreach line [split $euStackOutput "\n"] { if {[regexp {TID ([0-9]+):} $line -> tid]} { set currentTid $tid } else { dict set userStacks $currentTid \ "[dict getdef $userStacks $currentTid {}]\n$line" } } } on error e { set tids [$threadMonitorLib workerTids] } set totalAllocsMinusFrees 0.0 html [subst {
[htmlEscape [join $workQueue "\n"]]
[htmlEscape $userStack]
[htmlEscape $kernelStack]
Total allocs - frees: [format "%.2f MB" [expr {$totalAllocsMinusFrees / 1000000.0}]]