diff --git a/doc/json_schema.json b/doc/json_schema.json index 3d43ca94e..0aeba6de2 100644 --- a/doc/json_schema.json +++ b/doc/json_schema.json @@ -506,7 +506,7 @@ "type": "string" }, "topFormat": { - "description": "Output format for the `Top` module. See Wiki for formatting syntax\n 1. {name}: Process name\n 2. {pid}: Process ID\n 3. {cpu}: CPU usage\n 4. {mem}: Memory usage (RSS) in bytes\n 5. {disk-read}: Disk read bytes per second (0 if unsupported)\n 6. {disk-write}: Disk write bytes per second (0 if unsupported)\n 7. {cpu-percentage}: CPU usage percentage\n 8. {mem-formatted}: Memory usage (RSS) formatted\n 9. {disk-read-formatted}: Disk read formatted\n 10. {disk-write-formatted}: Disk write formatted", + "description": "Output format for the `Top` module. See Wiki for formatting syntax\n 1. {name}: Process name\n 2. {pid}: Process ID\n 3. {cpu}: CPU usage\n 4. {mem}: Memory usage (RSS) in bytes\n 5. {disk-read}: Disk read bytes per second (0 if unsupported)\n 6. {disk-write}: Disk write bytes per second (0 if unsupported)\n 7. {threads}: Number of threads\n 8. {cpu-percentage}: CPU usage percentage\n 9. {mem-formatted}: Memory usage (RSS) formatted\n 10. {disk-read-formatted}: Disk read formatted\n 11. {disk-write-formatted}: Disk write formatted", "type": "string" }, "themeFormat": { @@ -4441,22 +4441,23 @@ "properties": { "type": { "const": "top", - "description": "Print processes with the highest CPU, memory, disk I/O usage or latest start time" + "description": "Print processes with the highest CPU, memory, disk I/O usage, latest start time or thread count" }, "sort": { "type": "string", - "description": "Sort processes by CPU, memory, disk IO usage or start time", + "description": "Sort processes by CPU, memory, disk IO usage, start time or thread count", "enum": [ "cpu", "memory", "disk-read", "disk-write", - "start-time" + "start-time", + "threads" ], "default": "cpu" }, "showTypes": { - "description": "Specify which resource usages are detected and displayed.\nIf memory is enabled is enabled alone, only a single process snapshot is taken and `waitTime` sampling wait is skipped", + "description": "Specify which resource usages are detected and displayed.\nIf memory or threads is enabled is enabled alone, only a single process snapshot is taken and `waitTime` sampling wait is skipped", "default": [ "cpu", "memory", @@ -4468,7 +4469,8 @@ "enum": [ "cpu", "memory", - "disk" + "disk", + "threads" ] }, { @@ -4479,7 +4481,8 @@ "enum": [ "cpu", "memory", - "disk" + "disk", + "threads" ] } } diff --git a/src/detection/top/top.c b/src/detection/top/top.c index 26b5b76db..dd4442a7e 100644 --- a/src/detection/top/top.c +++ b/src/detection/top/top.c @@ -59,6 +59,12 @@ static int compareStartTimeResults(const FFTopProcessResult* a, const FFTopProce if (a->startTime > b->startTime) return -1; return (int) (a->pid - b->pid); } + +static int compareThreadsResults(const FFTopProcessResult* a, const FFTopProcessResult* b) { + if (a->threads < b->threads) return 1; + if (a->threads > b->threads) return -1; + return (int) (a->pid - b->pid); +} // clang-format on const char* ffDetectTopProcesses(FFTopOptions* options, FFlist* result) { @@ -67,8 +73,8 @@ const char* ffDetectTopProcesses(FFTopOptions* options, FFlist* result) { return nullptr; } - // Memory usage is instantaneous; when neither CPU time nor disk IO counters - // are requested, a single snapshot suffices and no sampling wait is needed. + // Memory usage and thread count are instantaneous; when neither CPU time nor disk IO + // counters are requested, a single snapshot suffices and no sampling wait is needed. const bool sampleOnce = (options->showTypes & (FF_TOP_TYPE_CPU | FF_TOP_TYPE_DISK)) == 0; if (sampleOnce) { @@ -92,6 +98,7 @@ const char* ffDetectTopProcesses(FFTopOptions* options, FFlist* result) { item->bytesWritten = 0; item->cpuPercent = 0; item->startTime = snap->startTime; + item->threads = snap->threads; ffStrbufInitMove(&item->name, &snap->name); } } else { @@ -141,6 +148,7 @@ const char* ffDetectTopProcesses(FFTopOptions* options, FFlist* result) { item->bytesWritten = (newItem->bytesWritten - oldItem->bytesWritten) * 1000u / (uint64_t) elapsed; item->cpuPercent = (double) (newItem->cpuTime - oldItem->cpuTime) / elapsed * 100.0; item->startTime = newItem->startTime; + item->threads = newItem->threads; ffStrbufInitMove(&item->name, &oldItem->name); } @@ -154,6 +162,7 @@ const char* ffDetectTopProcesses(FFTopOptions* options, FFlist* result) { : options->sort == FF_TOP_TYPE_DISK_READ ? (void*) compareDiskReadResults : options->sort == FF_TOP_TYPE_MEMORY ? (void*) compareMemoryResults : options->sort == FF_TOP_TYPE_START_TIME ? (void*) compareStartTimeResults + : options->sort == FF_TOP_TYPE_THREADS ? (void*) compareThreadsResults : (void*) compareCpuResults; ffListSort(result, sizeof(FFTopProcessResult), (void*) compare); if (result->length > options->nProcesses) { diff --git a/src/detection/top/top.h b/src/detection/top/top.h index de1289b4a..26f05d549 100644 --- a/src/detection/top/top.h +++ b/src/detection/top/top.h @@ -10,6 +10,7 @@ typedef struct FFTopProcessResult { uint64_t bytesRead; uint64_t bytesWritten; uint64_t startTime; + uint32_t threads; uint32_t pid; } FFTopProcessResult; @@ -20,6 +21,7 @@ typedef struct FFTopProcessSnapshot { uint64_t bytesRead; // Physical storage read uint64_t bytesWritten; // Physical storage written uint64_t startTime; // Differs between platforms + uint32_t threads; uint32_t pid; } FFTopProcessSnapshot; diff --git a/src/detection/top/top_apple.c b/src/detection/top/top_apple.c index 862431381..6a1692b0e 100644 --- a/src/detection/top/top_apple.c +++ b/src/detection/top/top_apple.c @@ -5,7 +5,7 @@ #include #include -const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { +const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL }; size_t length; @@ -54,6 +54,13 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { item->bytesRead = rusage.ri_diskio_bytesread; item->bytesWritten = rusage.ri_diskio_byteswritten; item->startTime = rusage.ri_proc_start_abstime; + item->threads = 0; + if (showTypes & FF_TOP_TYPE_THREADS) { + struct proc_taskinfo taskInfo; + if (proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, sizeof(taskInfo)) == sizeof(taskInfo)) { + item->threads = (uint32_t) taskInfo.pti_threadnum; + } + } } return nullptr; diff --git a/src/detection/top/top_bsd.c b/src/detection/top/top_bsd.c index 6c0f62018..108e5e97e 100644 --- a/src/detection/top/top_bsd.c +++ b/src/detection/top/top_bsd.c @@ -6,7 +6,7 @@ #include #include -const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { +const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC }; size_t length; @@ -51,6 +51,7 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { item->bytesRead = (uint64_t) proc->ki_rusage.ru_inblock * DEV_BSIZE; item->bytesWritten = (uint64_t) proc->ki_rusage.ru_oublock * DEV_BSIZE; + item->threads = (uint32_t) proc->ki_numthreads; } return NULL; diff --git a/src/detection/top/top_dbsd.c b/src/detection/top/top_dbsd.c index 56a572e65..9896b6f54 100644 --- a/src/detection/top/top_dbsd.c +++ b/src/detection/top/top_dbsd.c @@ -6,7 +6,7 @@ #include #include -const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { +const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL }; size_t length; @@ -52,6 +52,7 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { item->bytesRead = (uint64_t) (proc->kp_ru.ru_inblock + proc->kp_lwp.kl_ru.ru_inblock) * DEV_BSIZE; item->bytesWritten = (uint64_t) (proc->kp_ru.ru_oublock + proc->kp_lwp.kl_ru.ru_oublock) * DEV_BSIZE; + item->threads = (uint32_t) proc->kp_nthreads; } return nullptr; diff --git a/src/detection/top/top_gnu.c b/src/detection/top/top_gnu.c index 37845a640..9f9342216 100644 --- a/src/detection/top/top_gnu.c +++ b/src/detection/top/top_gnu.c @@ -4,7 +4,7 @@ #include #include -const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { +const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { struct ps_context* context = nullptr; if (ps_context_create(getproc(), &context) != 0) { return "ps_context_create(getproc()) failed"; @@ -24,7 +24,7 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { // Flags that cannot be fetched for some process are simply left unset; // each field must be checked before use. - if (proc_stat_list_set_flags(list, PSTAT_ARGS | PSTAT_STATE | PSTAT_TASK_BASIC) != 0) { + if (proc_stat_list_set_flags(list, PSTAT_ARGS | PSTAT_STATE | PSTAT_TASK_BASIC | ((showTypes & FF_TOP_TYPE_THREADS) ? PSTAT_NUM_THREADS : 0)) != 0) { error = "proc_stat_list_set_flags() failed"; goto done; } @@ -60,6 +60,13 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { item->cpuTime = 0; item->memBytes = 0; item->startTime = 0; + item->threads = 0; + + if (showTypes & FF_TOP_TYPE_THREADS) { + if (proc_stat_has(stat, PSTAT_NUM_THREADS)) { + item->threads = (uint32_t) proc_stat_num_threads(stat); + } + } if (proc_stat_has(stat, PSTAT_TASK_BASIC)) { const task_basic_info_t info = proc_stat_task_basic_info(stat); diff --git a/src/detection/top/top_haiku.c b/src/detection/top/top_haiku.c index 78310737a..32f73d686 100644 --- a/src/detection/top/top_haiku.c +++ b/src/detection/top/top_haiku.c @@ -3,7 +3,7 @@ #include #include -const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { +const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { int32 cookie = 0; team_info team; while (get_next_team_info(&cookie, &team) == B_OK) { @@ -41,6 +41,7 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { // Storage io counters are not exposed by Haiku; leave them zeroed. item->bytesRead = 0; item->bytesWritten = 0; + item->threads = (uint32_t) team.thread_count; } return nullptr; diff --git a/src/detection/top/top_linux.c b/src/detection/top/top_linux.c index 2436e3ce7..be71ab5a8 100644 --- a/src/detection/top/top_linux.c +++ b/src/detection/top/top_linux.c @@ -24,8 +24,6 @@ static bool parseStat(const char* buffer, size_t length, FFTopProcessSnapshot* r return false; } - result->cpuTime = 0; - char* p; for (uint32_t field = 3; field <= 22 /*start time*/; ++field) { switch (field) { @@ -43,6 +41,11 @@ static bool parseStat(const char* buffer, size_t length, FFTopProcessSnapshot* r cursor = p; break; + case 20: // number of threads + result->threads = (uint32_t) strtoul(cursor, &p, 10); + cursor = p; + break; + case 22: // start time result->startTime = (uint64_t) strtoull(cursor, &p, 10); cursor = p; @@ -128,6 +131,9 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { auto snapshot = FF_LIST_ADD(FFTopProcessSnapshot, *snapshots); ffStrbufInit(&snapshot->name); + snapshot->threads = 0; + snapshot->cpuTime = 0; + snapshot->startTime = 0; if (!parseStat(statBuffer, (size_t) statLength, snapshot)) { ffStrbufDestroy(&snapshot->name); diff --git a/src/detection/top/top_nbsd.c b/src/detection/top/top_nbsd.c index 1bc8e6543..8c9c4968d 100644 --- a/src/detection/top/top_nbsd.c +++ b/src/detection/top/top_nbsd.c @@ -6,7 +6,7 @@ #include #include -const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { +const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { int request[] = { CTL_KERN, KERN_PROC2, KERN_PROC_ALL, -1, sizeof(struct kinfo_proc2), INT_MAX }; size_t length; @@ -50,6 +50,14 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { item->bytesRead = (uint64_t) proc->p_uru_inblock * DEV_BSIZE; item->bytesWritten = (uint64_t) proc->p_uru_oublock * DEV_BSIZE; + item->threads = 0; + if (showTypes & FF_TOP_TYPE_THREADS) { + int lwpRequest[] = { CTL_KERN, KERN_LWP, proc->p_pid, sizeof(struct kinfo_lwp), 0 }; + size_t lwpLength = 0; + if (sysctl(lwpRequest, ARRAY_SIZE(lwpRequest), nullptr, &lwpLength, nullptr, 0) == 0) { + item->threads = (uint32_t) (lwpLength / sizeof(struct kinfo_lwp)); + } + } } return NULL; diff --git a/src/detection/top/top_obsd.c b/src/detection/top/top_obsd.c index f7519bd00..365ecfe7f 100644 --- a/src/detection/top/top_obsd.c +++ b/src/detection/top/top_obsd.c @@ -5,7 +5,7 @@ #include #include -const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { +const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { kvm_t* kd = kvm_open(nullptr, nullptr, nullptr, KVM_NO_FILES, nullptr); if (!kd) { return "kvm_open() failed"; @@ -37,6 +37,23 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { item->startTime = (uint64_t) proc->p_ustart_sec * 1000 + (uint64_t) proc->p_ustart_usec / 1000; item->bytesRead = (uint64_t) proc->p_uru_inblock * DEV_BSIZE; item->bytesWritten = (uint64_t) proc->p_uru_oublock * DEV_BSIZE; + item->threads = 0; + } + + if (showTypes & FF_TOP_TYPE_THREADS) { + int threadCount = 0; + const struct kinfo_proc* threads = kvm_getprocs(kd, + KERN_PROC_ALL | KERN_PROC_SHOW_THREADS, 0, sizeof(struct kinfo_proc), &threadCount); + if (threads) { + for (uint32_t i = 0; i < snapshots->length; ++i) { + FFTopProcessSnapshot* item = FF_LIST_GET(FFTopProcessSnapshot, *snapshots, i); + for (int j = 0; j < threadCount; ++j) { + if (threads[j].p_pid == (pid_t) item->pid && threads[j].p_tid != -1) { + ++item->threads; + } + } + } + } } kvm_close(kd); diff --git a/src/detection/top/top_sunos.c b/src/detection/top/top_sunos.c index 7dc9c880c..0054ae79a 100644 --- a/src/detection/top/top_sunos.c +++ b/src/detection/top/top_sunos.c @@ -60,6 +60,7 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes showTypes) { item->bytesRead = 0; item->bytesWritten = 0; + item->threads = (uint32_t) psinfo.pr_nlwp; if (showTypes & FF_TOP_TYPE_DISK) { prusage_t usage; if (ffReadFileDataRelative(subfd, "usage", sizeof(usage), &usage) == (ssize_t) sizeof(usage)) { diff --git a/src/detection/top/top_windows.c b/src/detection/top/top_windows.c index 604096403..2f1e5d66d 100644 --- a/src/detection/top/top_windows.c +++ b/src/detection/top/top_windows.c @@ -38,6 +38,7 @@ const char* ffTopGetProcessSnapshot(FFlist* snapshots, FFTopTypes) { item->memBytes = (uint64_t) info->VirtualMemoryCounters.WorkingSetSize; item->bytesRead = (uint64_t) info->IoCounters.ReadTransferCount; item->bytesWritten = (uint64_t) info->IoCounters.WriteTransferCount; + item->threads = info->NumberOfThreads; ffStrbufInitNWS(&item->name, info->ImageName.Length / sizeof(wchar_t), info->ImageName.Buffer); } if (info->NextEntryOffset == 0) { diff --git a/src/modules/top/option.h b/src/modules/top/option.h index 0422d3a8a..0c3ff406d 100644 --- a/src/modules/top/option.h +++ b/src/modules/top/option.h @@ -9,6 +9,7 @@ typedef enum FFTopTypes: uint8_t { FF_TOP_TYPE_DISK_READ = 1 << 2, FF_TOP_TYPE_DISK_WRITE = 1 << 3, FF_TOP_TYPE_START_TIME = 1 << 4, + FF_TOP_TYPE_THREADS = 1 << 5, FF_TOP_TYPE_DISK = FF_TOP_TYPE_DISK_READ | FF_TOP_TYPE_DISK_WRITE, } FFTopTypes; diff --git a/src/modules/top/top.c b/src/modules/top/top.c index cc39c9977..8ba755a83 100644 --- a/src/modules/top/top.c +++ b/src/modules/top/top.c @@ -28,6 +28,9 @@ static void printTopResult(FFTopOptions* options, uint32_t index, uint32_t total ffSizeAppendNum(process->bytesWritten, &output); ffStrbufAppendS(&output, "/s"); } + if (options->showTypes & FF_TOP_TYPE_THREADS) { + ffStrbufAppendF(&output, " - THR %u", process->threads); + } } ffStrbufPutTo(&output, stdout); } else { @@ -44,17 +47,18 @@ static void printTopResult(FFTopOptions* options, uint32_t index, uint32_t total ffSizeAppendNum(process->bytesWritten, &diskWriteFormatted); ffStrbufAppendS(&diskWriteFormatted, "/s"); FF_PRINT_FORMAT_CHECKED(FF_MODULE_GET_DISPLAY_NAME(Top), total == 1 ? 0 : (uint8_t) (index + 1), &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]){ - FF_ARG(process->name, "name"), - FF_ARG(process->pid, "pid"), - FF_ARG(process->cpuPercent, "cpu"), - FF_ARG(process->memBytes, "mem"), - FF_ARG(process->bytesRead, "disk-read"), - FF_ARG(process->bytesWritten, "disk-write"), - FF_ARG(cpuFormatted, "cpu-percentage"), - FF_ARG(memFormatted, "mem-formatted"), - FF_ARG(diskReadFormatted, "disk-read-formatted"), - FF_ARG(diskWriteFormatted, "disk-write-formatted"), - })); + FF_ARG(process->name, "name"), + FF_ARG(process->pid, "pid"), + FF_ARG(process->cpuPercent, "cpu"), + FF_ARG(process->memBytes, "mem"), + FF_ARG(process->bytesRead, "disk-read"), + FF_ARG(process->bytesWritten, "disk-write"), + FF_ARG(process->threads, "threads"), + FF_ARG(cpuFormatted, "cpu-percentage"), + FF_ARG(memFormatted, "mem-formatted"), + FF_ARG(diskReadFormatted, "disk-read-formatted"), + FF_ARG(diskWriteFormatted, "disk-write-formatted"), + })); } } @@ -121,6 +125,7 @@ void ffParseTopJsonObject(FFTopOptions* options, yyjson_val* module) { { "cpu", FF_TOP_TYPE_CPU }, { "memory", FF_TOP_TYPE_MEMORY }, { "disk", FF_TOP_TYPE_DISK }, + { "threads", FF_TOP_TYPE_THREADS }, {}, }); if (error == nullptr) { @@ -138,6 +143,7 @@ void ffParseTopJsonObject(FFTopOptions* options, yyjson_val* module) { { "cpu", FF_TOP_TYPE_CPU }, { "memory", FF_TOP_TYPE_MEMORY }, { "disk", FF_TOP_TYPE_DISK }, + { "threads", FF_TOP_TYPE_THREADS }, {}, }); if (error == nullptr) { @@ -175,6 +181,9 @@ void ffGenerateTopJsonConfig(FFTopOptions* options, yyjson_mut_doc* doc, yyjson_ if (options->showTypes & FF_TOP_TYPE_DISK) { yyjson_mut_arr_add_str(doc, showTypes, "disk"); } + if (options->showTypes & FF_TOP_TYPE_THREADS) { + yyjson_mut_arr_add_str(doc, showTypes, "threads"); + } yyjson_mut_obj_add_val(doc, module, "showTypes", showTypes); if (options->sort == FF_TOP_TYPE_CPU) { yyjson_mut_obj_add_str(doc, module, "sort", "cpu"); @@ -186,6 +195,8 @@ void ffGenerateTopJsonConfig(FFTopOptions* options, yyjson_mut_doc* doc, yyjson_ yyjson_mut_obj_add_str(doc, module, "sort", "disk-write"); } else if (options->sort == FF_TOP_TYPE_START_TIME) { yyjson_mut_obj_add_str(doc, module, "sort", "start-time"); + } else if (options->sort == FF_TOP_TYPE_THREADS) { + yyjson_mut_obj_add_str(doc, module, "sort", "threads"); } yyjson_mut_obj_add_uint(doc, module, "processes", options->nProcesses); yyjson_mut_obj_add_uint(doc, module, "waitTime", options->waitTime); @@ -215,6 +226,9 @@ bool ffGenerateTopJsonResult(FFTopOptions* options, yyjson_mut_doc* doc, yyjson_ yyjson_mut_obj_add_uint(doc, item, "bytesRead", process->bytesRead); yyjson_mut_obj_add_uint(doc, item, "bytesWritten", process->bytesWritten); } + if (options->showTypes & FF_TOP_TYPE_THREADS) { + yyjson_mut_obj_add_uint(doc, item, "threads", process->threads); + } yyjson_mut_obj_add_uint(doc, item, "startTime", process->startTime); } @@ -228,10 +242,10 @@ void ffInitTopOptions(FFTopOptions* options) { ffOptionInitModuleArg(&options->moduleArgs, "󰍛"); options->sort = FF_TOP_TYPE_CPU; options->showTypes = FF_TOP_TYPE_CPU | FF_TOP_TYPE_MEMORY - #if !__GNU__ && !__HAIKU__ +#if !__GNU__ && !__HAIKU__ | FF_TOP_TYPE_DISK - #endif - ; +#endif + ; options->nProcesses = 5; options->waitTime = 500; options->compact = false; @@ -280,6 +294,7 @@ FFModuleBaseInfo ffTopModuleInfo = { { "Memory usage (RSS) in bytes", "mem" }, { "Disk read bytes per second (0 if unsupported)", "disk-read" }, { "Disk write bytes per second (0 if unsupported)", "disk-write" }, + { "Number of threads", "threads" }, { "CPU usage percentage", "cpu-percentage" }, { "Memory usage (RSS) formatted", "mem-formatted" }, { "Disk read formatted", "disk-read-formatted" },