master ohmu/fastfetch-openbsd / src / detection / top / top.c
ohmu  ·  2026-10-15
  1#include "top.h"
  2
  3#include "common/time.h"
  4
  5static FFlist first;
  6static double startTick;
  7static FFTopTypes preparedTypes;
  8
  9void ffPrepareTopProcesses(FFTopTypes showTypes) {
 10    if (startTick != 0) {
 11        return; // Already prepared
 12    }
 13
 14    if ((showTypes & (FF_TOP_TYPE_CPU | FF_TOP_TYPE_DISK)) == 0) {
 15        return; // Memory usage is instantaneous; no baseline snapshot is needed
 16    }
 17
 18    // Within one module `showTypes` cannot change between this call and `ffDetectTopProcesses`:
 19    // `ffPrepareCommandOption` and `parseStructureCommand` both build the options through
 20    // `initStructureModuleOptions`, which merges the module object from the JSON config, so the
 21    // baseline always matches what the second snapshot collects.
 22    preparedTypes = showTypes;
 23    ffListInit(&first);
 24    startTick = ffTimeGetTick();
 25    ffTopGetProcessSnapshot(&first, showTypes);
 26}
 27
 28// clang-format off
 29static int compareCpuResults(const FFTopProcessResult* a, const FFTopProcessResult* b) {
 30    if (a->cpuPercent < b->cpuPercent) return 1;
 31    if (a->cpuPercent > b->cpuPercent) return -1;
 32    return (int) (a->pid - b->pid);
 33}
 34
 35static int compareMemoryResults(const FFTopProcessResult* a, const FFTopProcessResult* b) {
 36    if (a->memBytes < b->memBytes) return 1;
 37    if (a->memBytes > b->memBytes) return -1;
 38    return (int) (a->pid - b->pid);
 39}
 40
 41static int compareDiskReadResults(const FFTopProcessResult* a, const FFTopProcessResult* b) {
 42    if (a->bytesRead < b->bytesRead) return 1;
 43    if (a->bytesRead > b->bytesRead) return -1;
 44    return (int) (a->pid - b->pid);
 45}
 46
 47static int compareDiskWriteResults(const FFTopProcessResult* a, const FFTopProcessResult* b) {
 48    if (a->bytesWritten < b->bytesWritten) return 1;
 49    if (a->bytesWritten > b->bytesWritten) return -1;
 50    return (int) (a->pid - b->pid);
 51}
 52
 53static int compareStartTimeResults(const FFTopProcessResult* a, const FFTopProcessResult* b) {
 54    if (a->startTime < b->startTime) return 1;
 55    if (a->startTime > b->startTime) return -1;
 56    return (int) (a->pid - b->pid);
 57}
 58
 59static int compareThreadsResults(const FFTopProcessResult* a, const FFTopProcessResult* b) {
 60    if (a->threads < b->threads) return 1;
 61    if (a->threads > b->threads) return -1;
 62    return (int) (a->pid - b->pid);
 63}
 64// clang-format on
 65
 66const char* ffDetectTopProcesses(FFTopOptions* options, FFlist* result) {
 67    ffListClear(result);
 68    if (options->nProcesses == 0) {
 69        return nullptr;
 70    }
 71
 72    // A baseline collected for a different set of counters is answered here rather than in
 73    // ffPrepareTopProcesses, which has no way to report an error to the caller. The module that
 74    // triggered the mismatch is the one that sees it; see ffPrepareTopProcesses.
 75    if (options->showTypes != preparedTypes && (options->showTypes & (FF_TOP_TYPE_CPU | FF_TOP_TYPE_DISK)) != 0) {
 76        return "`top` modules with different `showTypes` cannot share a run";
 77    }
 78
 79    // Memory usage and thread count are instantaneous; when neither CPU time nor disk IO
 80    // counters are requested, a single snapshot suffices and no sampling wait is needed.
 81    const bool sampleOnce = (options->showTypes & (FF_TOP_TYPE_CPU | FF_TOP_TYPE_DISK)) == 0;
 82
 83    if (sampleOnce) {
 84        FF_LIST_AUTO_DESTROY snapshots = ffListCreate();
 85        const char* error = ffTopGetProcessSnapshot(&snapshots, options->showTypes);
 86        if (error) {
 87            FF_LIST_FOR_EACH (FFTopProcessSnapshot, item, snapshots) {
 88                ffStrbufDestroy(&item->name);
 89            }
 90            return error;
 91        }
 92        if (snapshots.length == 0) {
 93            return "No processes found";
 94        }
 95
 96        FF_LIST_FOR_EACH (FFTopProcessSnapshot, snap, snapshots) {
 97            FFTopProcessResult* item = FF_LIST_ADD(FFTopProcessResult, *result);
 98            item->pid = snap->pid;
 99            item->memBytes = snap->memBytes;
100            item->bytesRead = 0;
101            item->bytesWritten = 0;
102            item->cpuPercent = 0;
103            item->startTime = snap->startTime;
104            item->threads = snap->threads;
105            ffStrbufInitMove(&item->name, &snap->name);
106        }
107    } else {
108        if (startTick == 0) {
109            ffPrepareTopProcesses(options->showTypes);
110        }
111
112        double elapsedTime = ffTimeGetTick() - startTick;
113        if (elapsedTime < (double) options->waitTime) {
114            ffTimeSleep(options->waitTime - (uint32_t) elapsedTime);
115        }
116
117        if (first.length == 0) {
118            return "No processes found";
119        }
120
121        ffListReserve(result, sizeof(FFTopProcessResult), first.length < options->nProcesses ? first.length : options->nProcesses);
122
123        FF_LIST_AUTO_DESTROY second = ffListCreateA(sizeof(FFTopProcessSnapshot), first.length);
124        const char* error = ffTopGetProcessSnapshot(&second, options->showTypes);
125        const double elapsed = ffTimeGetTick() - startTick;
126
127        if (error || elapsed <= 0) {
128            FF_LIST_FOR_EACH (FFTopProcessSnapshot, item, second) {
129                ffStrbufDestroy(&item->name);
130            }
131            return error ?: "Invalid process sampling interval";
132        }
133
134        FF_LIST_FOR_EACH (FFTopProcessSnapshot, oldItem, first) {
135            FFTopProcessSnapshot* newItem = nullptr;
136            FF_LIST_FOR_EACH (FFTopProcessSnapshot, item, second) {
137                if (item->pid == oldItem->pid) {
138                    newItem = item;
139                    break;
140                }
141            }
142            if (!newItem || newItem->startTime != oldItem->startTime || newItem->cpuTime < oldItem->cpuTime || newItem->bytesRead < oldItem->bytesRead || newItem->bytesWritten < oldItem->bytesWritten) {
143                ffStrbufDestroy(&oldItem->name);
144                continue;
145            }
146
147            FFTopProcessResult* item = FF_LIST_ADD(FFTopProcessResult, *result);
148            item->pid = newItem->pid;
149            item->memBytes = newItem->memBytes;
150            item->bytesRead = (newItem->bytesRead - oldItem->bytesRead) * 1000u / (uint64_t) elapsed;
151            item->bytesWritten = (newItem->bytesWritten - oldItem->bytesWritten) * 1000u / (uint64_t) elapsed;
152            item->cpuPercent = (double) (newItem->cpuTime - oldItem->cpuTime) / elapsed * 100.0;
153            item->startTime = newItem->startTime;
154            item->threads = newItem->threads;
155            ffStrbufInitMove(&item->name, &oldItem->name);
156        }
157
158        // Reuse `second` as the baseline of the next call
159        ffListDestroy(&first);
160        ffListInitMove(&first, &second);
161        startTick = ffTimeGetTick();
162    }
163
164    const void* compare = options->sort == FF_TOP_TYPE_DISK_WRITE ? (void*) compareDiskWriteResults
165        : options->sort == FF_TOP_TYPE_DISK_READ                  ? (void*) compareDiskReadResults
166        : options->sort == FF_TOP_TYPE_MEMORY                     ? (void*) compareMemoryResults
167        : options->sort == FF_TOP_TYPE_START_TIME                 ? (void*) compareStartTimeResults
168        : options->sort == FF_TOP_TYPE_THREADS                    ? (void*) compareThreadsResults
169                                                                  : (void*) compareCpuResults;
170    ffListSort(result, sizeof(FFTopProcessResult), (void*) compare);
171    if (result->length > options->nProcesses) {
172        FFTopProcessResult* items = (FFTopProcessResult*) result->data;
173        for (uint32_t i = options->nProcesses; i < result->length; ++i) {
174            ffStrbufDestroy(&items[i].name);
175        }
176        result->length = options->nProcesses;
177    }
178
179    return nullptr;
180}