diff --git a/tieto-cpu-tracker/app/CMakeLists.txt b/tieto-cpu-tracker/app/CMakeLists.txt index b5caf61..a0d0ede 100644 --- a/tieto-cpu-tracker/app/CMakeLists.txt +++ b/tieto-cpu-tracker/app/CMakeLists.txt @@ -1 +1,28 @@ cmake_minimum_required(VERSION 3.5) + +project(cpu-tracker-app LANGUAGES C VERSION 0.1.0) + +set(C_STANDARD 11) +set(TARGET ${PROJECT_NAME}) +set(OUTPUT_NAME cpu-tracker) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +configure_file(src/main/Version.h.in Version.h) + +add_executable(${TARGET} + src/main/Main.c + src/main/CpuTracker.c + src/core/ProcAnalyzer.c + src/infrastructure/ProcReader.c + src/infrastructure/ProcPrinter.c + ) + +target_include_directories(${TARGET} PRIVATE src) +target_link_libraries(${TARGET} log thread) +set_target_properties(${TARGET} + PROPERTIES OUTPUT_NAME ${OUTPUT_NAME}) + +if (${BUILD_TESTS}) + add_subdirectory(tests/integration) +endif() \ No newline at end of file diff --git a/tieto-cpu-tracker/app/src/core/ProcAnalyzer.c b/tieto-cpu-tracker/app/src/core/ProcAnalyzer.c new file mode 100644 index 0000000..b4ff267 --- /dev/null +++ b/tieto-cpu-tracker/app/src/core/ProcAnalyzer.c @@ -0,0 +1,19 @@ +#include "core/ProcAnalyzer.h" + +#include + +void getCpuUsage(double results[], CpuStats stats[], CpuStats prevStats[], uint8_t numCores) +{ + // https://rosettacode.org/wiki/Linux_CPU_utilization + + for (int i = 0; i < numCores; i++) { + uint64_t prevTotal = prevStats[i].user + prevStats[i].nice + prevStats[i].system + prevStats[i].idle; + uint64_t currentTotal = stats[i].user + stats[i].nice + stats[i].system + stats[i].idle; + uint64_t totalDiff = currentTotal - prevTotal; + uint64_t idleDiff = stats[i].idle - prevStats[i].idle; + results[i] = (1.0 - (double)idleDiff / (double)totalDiff) * 100.0; + if (results[i] < 0 || isnan(results[i])) { + results[i] = 0; + } + } +} diff --git a/tieto-cpu-tracker/app/src/core/ProcAnalyzer.h b/tieto-cpu-tracker/app/src/core/ProcAnalyzer.h new file mode 100644 index 0000000..858cb28 --- /dev/null +++ b/tieto-cpu-tracker/app/src/core/ProcAnalyzer.h @@ -0,0 +1,17 @@ +#ifndef PROCANALYZER_H +#define PROCANALYZER_H + +#include + +typedef struct __attribute__((packed)) CpuStats { + char cpuName[20]; + uint64_t user; + uint64_t nice; + uint64_t system; + uint64_t idle; +} CpuStats; + +void getCpuUsage(double results[], CpuStats stats[], CpuStats prevStats[], uint8_t numCores); + +#endif // PROCANALYZER_H + diff --git a/tieto-cpu-tracker/app/src/infrastructure/ProcPrinter.c b/tieto-cpu-tracker/app/src/infrastructure/ProcPrinter.c new file mode 100644 index 0000000..a001187 --- /dev/null +++ b/tieto-cpu-tracker/app/src/infrastructure/ProcPrinter.c @@ -0,0 +1,10 @@ +#include "infrastructure/ProcPrinter.h" +#include + +void printCpuUsage(double results[], uint8_t size) +{ + for (uint8_t i = 0; i < size; i++) { + printf("CPU %d: %f\n", i, results[i]); + } + printf("---\n"); +} diff --git a/tieto-cpu-tracker/app/src/infrastructure/ProcPrinter.h b/tieto-cpu-tracker/app/src/infrastructure/ProcPrinter.h new file mode 100644 index 0000000..a6c56df --- /dev/null +++ b/tieto-cpu-tracker/app/src/infrastructure/ProcPrinter.h @@ -0,0 +1,9 @@ +#ifndef PROCPRINTER_H +#define PROCPRINTER_H + +#include "core/ProcAnalyzer.h" +#include + +void printCpuUsage(double results[], uint8_t size); + +#endif // PROCPRINTER_H diff --git a/tieto-cpu-tracker/app/src/infrastructure/ProcReader.c b/tieto-cpu-tracker/app/src/infrastructure/ProcReader.c new file mode 100644 index 0000000..dbb475b --- /dev/null +++ b/tieto-cpu-tracker/app/src/infrastructure/ProcReader.c @@ -0,0 +1,50 @@ +#include "infrastructure/ProcReader.h" + +#include +#include +#include +#include + +uint8_t countCpuCores(void) +{ + uint8_t count = 0; + char line[256]; + FILE* fp = fopen("/proc/cpuinfo", "r"); + if (fp == NULL) { + fprintf(stderr, "Unable to open /proc/cpuinfo\n"); + return 0; + } + + while (fgets(line, sizeof(line), fp)) { + if (strncmp(line, "core id", 7) == 0) { + ++count; + } + } + fclose(fp); + return count; +} + +void readCpuStats(CpuStats* stats, uint8_t numCores) +{ + // https://www.linuxhowtos.org/System/procstat.htm + + char line[256]; + FILE* fp = fopen("/proc/stat", "r"); + if (fp == NULL) { + fprintf(stderr, "Unable to open /proc/stat\n"); + exit(1); + } + + fgets(line, sizeof(line), fp); // ignore the first line + for (int i = 0; i < numCores; i++) { + fgets(line, sizeof(line), fp); + sscanf(line, "%s %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64, + stats[i].cpuName, + &stats[i].user, + &stats[i].nice, + &stats[i].system, + &stats[i].idle); + } + + fclose(fp); +} diff --git a/tieto-cpu-tracker/app/src/infrastructure/ProcReader.h b/tieto-cpu-tracker/app/src/infrastructure/ProcReader.h new file mode 100644 index 0000000..613ed83 --- /dev/null +++ b/tieto-cpu-tracker/app/src/infrastructure/ProcReader.h @@ -0,0 +1,10 @@ +#ifndef PROCREADER_H +#define PROCREADER_H + +#include "core/ProcAnalyzer.h" + +uint8_t countCpuCores(void); + +void readCpuStats(CpuStats* stats, uint8_t numCores); + +#endif // PROCREADER_H diff --git a/tieto-cpu-tracker/app/src/main/CpuTracker.c b/tieto-cpu-tracker/app/src/main/CpuTracker.c new file mode 100644 index 0000000..35ff97e --- /dev/null +++ b/tieto-cpu-tracker/app/src/main/CpuTracker.c @@ -0,0 +1,165 @@ +#include "CpuTracker.h" +#include "core/ProcAnalyzer.h" +#include "infrastructure/ProcReader.h" +#include "infrastructure/ProcPrinter.h" +#include "thread/Watchdog.h" +#include "log/Log.h" + +#include +#include +#include +#include +#include + +#define LOG_LINE_MAX 256 +#define LOG_TAG_MAX 16 + +static const int WATCHDOG_INTERVAL_MS = 2000; +static const int CPU_CHECK_INTERVAL_MS = 1000; +static const char* LOG_FILE = "/tmp/cpu-monitor.log"; + +typedef struct AppContext +{ + CpuStats* stats; + CpuStats* prevStats; + Watchdog* watchdog; + QueuedThread* readProcThread; + QueuedThread* analyzerThread; + QueuedThread* printerThread; + QueuedThread* loggerThread; + double* results; + uint8_t numCores; + BlockerHandle mainBlocker; + BlockerHandle readBlocker; + uint8_t reserved[5]; +} AppContext; + +typedef struct LogTuple +{ + char tag[LOG_TAG_MAX]; + char message[LOG_LINE_MAX]; +} LogTuple; + +static AppContext appCtx; + + +_Noreturn static void onThreadUnresponsive(Thread* thread); +static void threadedLog(const char* tag, const char* format, ...); + +static void readProcTask(void* arg); +static void analyzerTask(void* arg); +static void printerTask(void* arg); +static void logTask(void* arg); + + +void initCpuTracker(void) +{ + initLogger(LOG_FILE); + appCtx.numCores = countCpuCores(); + appCtx.prevStats = calloc(appCtx.numCores, sizeof(CpuStats)); + appCtx.stats = calloc(appCtx.numCores, sizeof(CpuStats)); + appCtx.results = calloc(appCtx.numCores, sizeof(double)); + appCtx.mainBlocker = createBlocker(); + appCtx.readBlocker = createBlocker(); + appCtx.watchdog = createWatchdog(WATCHDOG_INTERVAL_MS, + &onThreadUnresponsive, &threadedLog); + appCtx.readProcThread = createWatchedThread(appCtx.watchdog); + appCtx.analyzerThread = createWatchedThread(appCtx.watchdog); + appCtx.printerThread = createWatchedThread(appCtx.watchdog); + appCtx.loggerThread = createWatchedThread(appCtx.watchdog); +} + +void runCpuTracker(void) +{ + // this task will post itself again until the app is closed + postQueuedTask(appCtx.readProcThread, &readProcTask, &appCtx); + + startWatchdog(appCtx.watchdog); + lockBlocker(appCtx.mainBlocker); +} + +void stopCpuTracker(void) +{ + notifyBlocker(appCtx.mainBlocker); +} + +void destroyCpuTracker(void) +{ + stopWatchdog(appCtx.watchdog); + notifyBlocker(appCtx.readBlocker); + + joinWatchedThreads(appCtx.watchdog); + freeWatchedThreads(appCtx.watchdog); + freeWatchdog(&(appCtx.watchdog)); + freeBlocker(&(appCtx.mainBlocker)); + freeBlocker(&(appCtx.readBlocker)); + free(appCtx.results); + free(appCtx.prevStats); + free(appCtx.stats); + closeLogger(); +} + +// --- + +_Noreturn static void onThreadUnresponsive(Thread* thread) +{ + printf("Thread %p is unresponsive, exiting\n", (void*)(thread)); + usleep(1000 * 1000); + _exit(1); +} + +static void readProcTask(void* arg) +{ + (void)arg; + readCpuStats(appCtx.stats, appCtx.numCores); + + if (appCtx.prevStats[0].user != 0) { + getCpuUsage(appCtx.results, appCtx.stats, appCtx.prevStats, appCtx.numCores); + // NOTE: a mutex could be used here, but since the analyzer thread is idle + // until notified and we assume t(analyzer) < t(read) (including interval + // in the latter) there will be no data race. + postQueuedTask(appCtx.analyzerThread, &analyzerTask, NULL); + } + + memcpy(appCtx.prevStats, appCtx.stats, appCtx.numCores * sizeof(appCtx.stats)); + lockBlockerTimed(appCtx.readBlocker, CPU_CHECK_INTERVAL_MS); + postQueuedTask(appCtx.readProcThread, &readProcTask, NULL); +} + +static void analyzerTask(void* arg) +{ + (void)arg; + threadedLog("analyzer", "Processing results"); + getCpuUsage(appCtx.results, appCtx.stats, appCtx.prevStats, appCtx.numCores); + postQueuedTask(appCtx.printerThread, &printerTask, NULL); +} + +static void printerTask(void* arg) +{ + (void)arg; + threadedLog("printer", "Printing results"); + printCpuUsage(appCtx.results, appCtx.numCores); +} + +static void logTask(void* arg) +{ + LogTuple* tuple = (LogTuple*)arg; + writeLog(tuple->tag, tuple->message); + free(tuple); +} + +__attribute__((__format__ (__printf__, 2, 0))) +static void threadedLog(const char* tag, const char* format, ...) +{ + LogTuple* log = NULL; + va_list args; + if (!appCtx.watchdog->running) { // stop posting tasks if the application is exiting + return; + } + log = calloc(1, sizeof(LogTuple)); + va_start(args, format); + vsprintf((char*)log->message, format, args); + va_end(args); + strncpy(log->tag, tag, LOG_TAG_MAX); + postQueuedTask(appCtx.loggerThread, &logTask, log); +} diff --git a/tieto-cpu-tracker/app/src/main/CpuTracker.h b/tieto-cpu-tracker/app/src/main/CpuTracker.h new file mode 100644 index 0000000..7a2f649 --- /dev/null +++ b/tieto-cpu-tracker/app/src/main/CpuTracker.h @@ -0,0 +1,12 @@ +#ifndef CPUTRACKER_H +#define CPUTRACKER_H + +void initCpuTracker(void); + +void runCpuTracker(void); + +void stopCpuTracker(void); + +void destroyCpuTracker(void); + +#endif // CPUTRACKER_H diff --git a/tieto-cpu-tracker/app/src/main/Main.c b/tieto-cpu-tracker/app/src/main/Main.c new file mode 100644 index 0000000..32f9ab6 --- /dev/null +++ b/tieto-cpu-tracker/app/src/main/Main.c @@ -0,0 +1,29 @@ +#include "Version.h" +#include "CpuTracker.h" + +#include + +#define VERSION_BUFFER_SIZE (16) + +static void onSigInt(int sig) +{ + (void)sig; + printf("\nSIGINT received, exiting\n"); + stopCpuTracker(); +} + +int main(void) +{ + { + char vBuf[VERSION_BUFFER_SIZE]; + printf("CPU monitor %s\n", getVersionString(vBuf)); + } + + signal(SIGINT, onSigInt); + + initCpuTracker(); + runCpuTracker(); + printf("Stopping...\n"); + destroyCpuTracker(); + printf("Done.\n"); +} diff --git a/tieto-cpu-tracker/app/src/main/Version.h.in b/tieto-cpu-tracker/app/src/main/Version.h.in new file mode 100644 index 0000000..caca7a6 --- /dev/null +++ b/tieto-cpu-tracker/app/src/main/Version.h.in @@ -0,0 +1,19 @@ +#ifndef VERSION_H_IN +#define VERSION_H_IN + +#include +#include + +static const uint8_t VERSION_MAJOR = ${PROJECT_VERSION_MAJOR}; +static const uint8_t VERSION_MINOR = ${PROJECT_VERSION_MINOR}; +static const uint8_t VERSION_PATCH = ${PROJECT_VERSION_PATCH}; +static const char* VERSION_SUFFIX = "${PROJECT_VERSION_SUFFIX}"; + +static char* getVersionString(char* buffer) { + sprintf(buffer, "%d.%d.%d%s", + VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, + VERSION_SUFFIX); + return buffer; +} + +#endif // VERSION_H_IN \ No newline at end of file diff --git a/tieto-cpu-tracker/app/tests/integration/CMakeLists.txt b/tieto-cpu-tracker/app/tests/integration/CMakeLists.txt new file mode 100644 index 0000000..2e5339e --- /dev/null +++ b/tieto-cpu-tracker/app/tests/integration/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.5) + +set(C_STANDARD 11) +enable_testing() + +set(SRC_DIR ../../src) + +include_directories(${SRC_DIR}) + +add_executable(ProcReaderTest + ${SRC_DIR}/core/ProcAnalyzer.c + ${SRC_DIR}/infrastructure/ProcReader.c + ${SRC_DIR}/infrastructure/ProcPrinter.c + ProcReader.test.c + ) +add_test(ProcReaderTest ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/ProcReaderTest) diff --git a/tieto-cpu-tracker/app/tests/integration/ProcReader.test.c b/tieto-cpu-tracker/app/tests/integration/ProcReader.test.c new file mode 100644 index 0000000..8f6c49e --- /dev/null +++ b/tieto-cpu-tracker/app/tests/integration/ProcReader.test.c @@ -0,0 +1,35 @@ +#include "infrastructure/ProcReader.h" +#include "infrastructure/ProcPrinter.h" + +#include +#include +#include +#include +#include + +int main() { + uint8_t numCores = countCpuCores(); + + CpuStats* prevStats = calloc(numCores, sizeof(CpuStats)); + CpuStats* currentStats = calloc(numCores, sizeof(CpuStats)); + + double* results = calloc(numCores, sizeof(double)); + + for (int i = 0; i < 4; i++) { + readCpuStats(currentStats, numCores); + + if (prevStats[0].user != 0) { + getCpuUsage(results, currentStats, prevStats, numCores); + printCpuUsage(results, numCores); + } + + memcpy(prevStats, currentStats, numCores * sizeof(currentStats)); + + usleep(500 * 1000); + } + + free(results); + free(prevStats); + free(currentStats); + return 0; +}