mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 15:23:03 +02:00
chore(tooling): update pre-commit config and repo ignores
This commit is contained in:
parent
c8c727e9d5
commit
97c84e9bbf
@ -79,10 +79,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
**/node_modules/
|
**/node_modules/
|
||||||
|
|
||||||
# Coverage reports
|
|
||||||
coverage/
|
|
||||||
**/coverage/
|
|
||||||
|
|
||||||
# Caches
|
# Caches
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|||||||
@ -11,8 +11,8 @@
|
|||||||
default_language_version:
|
default_language_version:
|
||||||
python: python3
|
python: python3
|
||||||
|
|
||||||
# Fail fast on first error — also prevents stacking memory-heavy hooks
|
# Fail fast on first error (set to false to see all errors)
|
||||||
fail_fast: true
|
fail_fast: false
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
ci:
|
ci:
|
||||||
@ -155,8 +155,8 @@ repos:
|
|||||||
stages: [pre-push]
|
stages: [pre-push]
|
||||||
args:
|
args:
|
||||||
- --rcfile=pyproject.toml
|
- --rcfile=pyproject.toml
|
||||||
- --fail-under=7.5
|
- --fail-under=8.0
|
||||||
- --jobs=1
|
- --jobs=0
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- pytest
|
- pytest
|
||||||
- python-chess
|
- python-chess
|
||||||
@ -307,7 +307,6 @@ repos:
|
|||||||
types_or: [yaml, json, markdown]
|
types_or: [yaml, json, markdown]
|
||||||
exclude: ^(Bash/|\.venv/|.*\.lock$|C/compile_commands\.json)
|
exclude: ^(Bash/|\.venv/|.*\.lock$|C/compile_commands\.json)
|
||||||
stages: [pre-push]
|
stages: [pre-push]
|
||||||
args: [--no-cache]
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# SHELLCHECK - Shell script linting
|
# SHELLCHECK - Shell script linting
|
||||||
@ -338,10 +337,22 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: cppcheck
|
- id: cppcheck
|
||||||
name: cppcheck
|
name: cppcheck
|
||||||
entry: bash -c 'printf "%s\0" "$@" | xargs -0 -n 1 cppcheck --enable=warning,portability --check-level=normal --quiet --error-exitcode=1 --inline-suppr --suppress=missingIncludeSystem --suppress=syntaxError --suppress=nullPointerOutOfResources --suppress=ctunullpointerOutOfResources --suppress=ctunullpointerOutOfMemory --std=c11' --
|
entry: cppcheck
|
||||||
language: system
|
language: system
|
||||||
types_or: [c, c++]
|
types_or: [c, c++]
|
||||||
exclude: ^(pomodoro_app/|horatio/)
|
exclude: ^(pomodoro_app/|horatio/)
|
||||||
|
args:
|
||||||
|
- --enable=warning,portability
|
||||||
|
- --force
|
||||||
|
- --quiet
|
||||||
|
- --error-exitcode=1
|
||||||
|
- --inline-suppr
|
||||||
|
- --suppress=missingIncludeSystem
|
||||||
|
- --suppress=syntaxError
|
||||||
|
- --suppress=nullPointerOutOfResources
|
||||||
|
- --suppress=ctunullpointerOutOfResources
|
||||||
|
- --suppress=ctunullpointerOutOfMemory
|
||||||
|
- --std=c11
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# FLAWFINDER - C/C++ security scanner
|
# FLAWFINDER - C/C++ security scanner
|
||||||
@ -365,7 +376,7 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: eslint
|
- id: eslint
|
||||||
name: eslint
|
name: eslint
|
||||||
entry: bash -c 'NODE_OPTIONS="--max-old-space-size=512" npx eslint --no-warn-ignored "$@"' --
|
entry: npx eslint --no-warn-ignored
|
||||||
language: system
|
language: system
|
||||||
types_or: [ts, tsx]
|
types_or: [ts, tsx]
|
||||||
files: ^TS/
|
files: ^TS/
|
||||||
@ -432,13 +443,8 @@ repos:
|
|||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: pomodoro-app-flutter
|
- id: pomodoro-app-flutter
|
||||||
name: pomodoro_app flutter analyze & test (100% coverage)
|
name: pomodoro_app flutter analyze & test
|
||||||
entry: >-
|
entry: bash -c 'cd pomodoro_app && flutter pub get --enforce-lockfile && flutter analyze && flutter test'
|
||||||
bash -c 'cd pomodoro_app &&
|
|
||||||
flutter pub get --enforce-lockfile &&
|
|
||||||
flutter analyze --fatal-infos &&
|
|
||||||
flutter test --coverage &&
|
|
||||||
awk -F"[,:]" "/^DA:/{total++;if(\$3==0)uncov++}END{if(uncov>0){printf \"ERROR: pomodoro_app coverage %.1f%% (%d uncovered lines)\\n\",((total-uncov)/total)*100,uncov;exit 1}else{printf \"pomodoro_app coverage 100.0%%\\n\"}}" coverage/lcov.info'
|
|
||||||
language: system
|
language: system
|
||||||
files: ^pomodoro_app/
|
files: ^pomodoro_app/
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
@ -456,32 +462,3 @@ repos:
|
|||||||
files: ^horatio/
|
files: ^horatio/
|
||||||
stages: [pre-push]
|
stages: [pre-push]
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
||||||
# ===========================================================================
|
|
||||||
# TYPESCRIPT - Vitest coverage enforcement (push only)
|
|
||||||
# ===========================================================================
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: ts-battery-status-tests
|
|
||||||
name: TS battery-status vitest (100% coverage)
|
|
||||||
entry: bash -c 'cd TS/battery-status && NODE_OPTIONS="--max-old-space-size=512" npx vitest run --coverage'
|
|
||||||
language: system
|
|
||||||
files: ^TS/battery-status/
|
|
||||||
pass_filenames: false
|
|
||||||
stages: [pre-push]
|
|
||||||
|
|
||||||
- id: ts-champions-leauge-scores-tests
|
|
||||||
name: TS champions_leauge_scores vitest (100% coverage)
|
|
||||||
entry: bash -c 'cd TS/champions_leauge_scores && NODE_OPTIONS="--max-old-space-size=512" npx vitest run --coverage'
|
|
||||||
language: system
|
|
||||||
files: ^TS/champions_leauge_scores/
|
|
||||||
pass_filenames: false
|
|
||||||
stages: [pre-push]
|
|
||||||
|
|
||||||
- id: ts-two-inputs-tests
|
|
||||||
name: TS two-inputs vitest (100% coverage)
|
|
||||||
entry: bash -c 'cd TS/two-inputs && NODE_OPTIONS="--max-old-space-size=512" npx vitest run --coverage'
|
|
||||||
language: system
|
|
||||||
files: ^TS/two-inputs/
|
|
||||||
pass_filenames: false
|
|
||||||
stages: [pre-push]
|
|
||||||
|
|||||||
33
C/atop_agg/Makefile
Normal file
33
C/atop_agg/Makefile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
CC := gcc
|
||||||
|
CFLAGS := -O2 -std=c11 -D_POSIX_C_SOURCE=200809L -Wall -Wextra -Wno-unused-parameter
|
||||||
|
COV := -O0 -g --coverage -std=c11 -D_POSIX_C_SOURCE=200809L -Wall -Wextra -Wno-unused-parameter -DATOP_AGG_NO_MAIN
|
||||||
|
|
||||||
|
SRC := atop_agg.c
|
||||||
|
HDR := atop_agg.h
|
||||||
|
BIN := atop_agg
|
||||||
|
|
||||||
|
.PHONY: all clean rebuild test coverage
|
||||||
|
|
||||||
|
all: $(BIN)
|
||||||
|
|
||||||
|
$(BIN): $(SRC) $(HDR)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $(SRC)
|
||||||
|
|
||||||
|
test_atop_agg: test_atop_agg.c atop_agg.c atop_agg.h
|
||||||
|
$(CC) $(COV) -o test_atop_agg test_atop_agg.c atop_agg.c
|
||||||
|
|
||||||
|
test: test_atop_agg
|
||||||
|
./test_atop_agg
|
||||||
|
|
||||||
|
coverage: test_atop_agg
|
||||||
|
./test_atop_agg
|
||||||
|
lcov --capture --directory . --output-file coverage.info --no-external
|
||||||
|
lcov --remove coverage.info '*/test_atop_agg.c' --output-file coverage.info
|
||||||
|
genhtml coverage.info --output-directory coverage_html
|
||||||
|
@echo "Coverage report at coverage_html/index.html"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BIN) test_atop_agg *.o *.gcda *.gcno coverage.info
|
||||||
|
rm -rf coverage_html
|
||||||
|
|
||||||
|
rebuild: clean all
|
||||||
474
C/atop_agg/atop_agg.c
Normal file
474
C/atop_agg/atop_agg.c
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
/*
|
||||||
|
* atop_agg — fast per-PID aggregator for `atop -P PRC,PRM` output.
|
||||||
|
*
|
||||||
|
* Reads atop parseable output on stdin, folds it into per-PID CPU-tick
|
||||||
|
* and RSS trackers, and prints a compact TSV summary on stdout that a
|
||||||
|
* higher-level driver (Python) then name-folds into human-readable
|
||||||
|
* tables. This avoids the ~3s Python parse cost on a typical day's
|
||||||
|
* 1.7M-line atop dump; the C hot loop completes in well under a second
|
||||||
|
* so the pipeline runs at atop's own ~2s wall-clock floor.
|
||||||
|
*
|
||||||
|
* Output TSV lines:
|
||||||
|
* W<TAB>start_epoch<TAB>end_epoch<TAB>distinct_samples<TAB>median_interval
|
||||||
|
* C<TAB>pid<TAB>name<TAB>delta_ticks
|
||||||
|
* R<TAB>pid<TAB>name<TAB>peak_kb<TAB>sum_kb<TAB>samples
|
||||||
|
*/
|
||||||
|
#include "atop_agg.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A real-world day of atop on a dev box can see >700k distinct PIDs
|
||||||
|
* because every short-lived compiler/shell subprocess gets a fresh ID.
|
||||||
|
* 2M slots keeps the load factor below ~40% for that workload, keeping
|
||||||
|
* linear-probe chains short without dynamic resizing.
|
||||||
|
*/
|
||||||
|
#define HASH_CAP_BITS 21
|
||||||
|
#define HASH_CAP (1u << HASH_CAP_BITS)
|
||||||
|
#define HASH_MASK (HASH_CAP - 1u)
|
||||||
|
#define MAX_EPOCHS 4096
|
||||||
|
#define MAX_TOKENS 64
|
||||||
|
|
||||||
|
/* Knuth multiplicative hash → index in an open-addressed table. */
|
||||||
|
static unsigned int hash_pid(int pid)
|
||||||
|
{
|
||||||
|
unsigned int k = (unsigned int)pid;
|
||||||
|
return (k * 2654435761u) >> (32 - HASH_CAP_BITS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PidCpu *cpu_slot(State *s, int pid)
|
||||||
|
{
|
||||||
|
unsigned int h = hash_pid(pid);
|
||||||
|
for (unsigned int probes = 0; probes < HASH_CAP; probes++, h++)
|
||||||
|
{
|
||||||
|
PidCpu *slot = &s->cpu[h & HASH_MASK];
|
||||||
|
if (slot->pid == pid)
|
||||||
|
{
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
if (slot->pid == 0)
|
||||||
|
{
|
||||||
|
slot->pid = pid;
|
||||||
|
slot->first_ticks = -1;
|
||||||
|
slot->last_ticks = 0;
|
||||||
|
slot->samples = 0;
|
||||||
|
slot->name[0] = '\0';
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Table full — drop the sample rather than loop forever. */
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PidRam *ram_slot(State *s, int pid)
|
||||||
|
{
|
||||||
|
unsigned int h = hash_pid(pid);
|
||||||
|
for (unsigned int probes = 0; probes < HASH_CAP; probes++, h++)
|
||||||
|
{
|
||||||
|
PidRam *slot = &s->ram[h & HASH_MASK];
|
||||||
|
if (slot->pid == pid)
|
||||||
|
{
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
if (slot->pid == 0)
|
||||||
|
{
|
||||||
|
slot->pid = pid;
|
||||||
|
slot->peak_kb = 0;
|
||||||
|
slot->sum_kb = 0;
|
||||||
|
slot->samples = 0;
|
||||||
|
slot->name[0] = '\0';
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void add_epoch(State *s, long epoch)
|
||||||
|
{
|
||||||
|
/* Linear scan — there are only a few dozen distinct epochs per log. */
|
||||||
|
for (int i = 0; i < s->n_epochs; i++)
|
||||||
|
{
|
||||||
|
if (s->epochs[i] == epoch)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (s->n_epochs < MAX_EPOCHS)
|
||||||
|
{
|
||||||
|
s->epochs[s->n_epochs++] = epoch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Tokenise a whitespace-separated line in place. Fills *tokens* with
|
||||||
|
* pointers into *line* and returns the token count. A process name
|
||||||
|
* wrapped in parentheses is rejoined into a single token with spaces
|
||||||
|
* preserved (atop emits `(Web Content)` as three whitespace-split
|
||||||
|
* tokens, which we merge back).
|
||||||
|
*/
|
||||||
|
int tokenize_line(char *line, char **tokens, int max_tokens)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
char *p = line;
|
||||||
|
while (*p && n < max_tokens)
|
||||||
|
{
|
||||||
|
while (*p == ' ' || *p == '\t')
|
||||||
|
{
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (!*p || *p == '\n')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
char *start = p;
|
||||||
|
if (*p == '(')
|
||||||
|
{
|
||||||
|
/* Consume through the matching ')', preserving interior spaces. */
|
||||||
|
while (*p && *p != ')')
|
||||||
|
{
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (*p == ')')
|
||||||
|
{
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
while (*p && *p != ' ' && *p != '\t' && *p != '\n')
|
||||||
|
{
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (*p)
|
||||||
|
{
|
||||||
|
*p = '\0';
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
tokens[n++] = start;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copy *src* into *dst* (capacity *cap*), stripping a leading '(' and
|
||||||
|
* trailing ')' if both are present. Always null-terminates. If the
|
||||||
|
* resulting name is empty, writes "unknown".
|
||||||
|
*/
|
||||||
|
void copy_name(char *dst, size_t cap, const char *src)
|
||||||
|
{
|
||||||
|
size_t len = strlen(src);
|
||||||
|
size_t start = 0;
|
||||||
|
if (len >= 2 && src[0] == '(' && src[len - 1] == ')')
|
||||||
|
{
|
||||||
|
start = 1;
|
||||||
|
len -= 2;
|
||||||
|
}
|
||||||
|
if (len == 0)
|
||||||
|
{
|
||||||
|
const char *fallback = "unknown";
|
||||||
|
size_t flen = strlen(fallback);
|
||||||
|
if (flen >= cap)
|
||||||
|
{
|
||||||
|
flen = cap - 1;
|
||||||
|
}
|
||||||
|
memcpy(dst, fallback, flen);
|
||||||
|
dst[flen] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (len >= cap)
|
||||||
|
{
|
||||||
|
len = cap - 1;
|
||||||
|
}
|
||||||
|
memcpy(dst, src + start, len);
|
||||||
|
dst[len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parse one PRC/PRM line and update *s*. Unknown labels and malformed
|
||||||
|
* records are silently skipped (atop emits a stable schema, but guard
|
||||||
|
* against future changes and header/separator lines).
|
||||||
|
*/
|
||||||
|
void process_line(char *line, State *s)
|
||||||
|
{
|
||||||
|
char *tokens[MAX_TOKENS];
|
||||||
|
int n = tokenize_line(line, tokens, MAX_TOKENS);
|
||||||
|
if (n < 11)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const char *label = tokens[0];
|
||||||
|
int is_prc = (label[0] == 'P' && label[1] == 'R' && label[2] == 'C' && label[3] == '\0');
|
||||||
|
int is_prm = (label[0] == 'P' && label[1] == 'R' && label[2] == 'M' && label[3] == '\0');
|
||||||
|
if (!is_prc && !is_prm)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long epoch = strtol(tokens[2], NULL, 10);
|
||||||
|
int pid = (int)strtol(tokens[6], NULL, 10);
|
||||||
|
if (pid <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const char *name_tok = tokens[7];
|
||||||
|
if (is_prc)
|
||||||
|
{
|
||||||
|
long utime = strtol(tokens[9], NULL, 10);
|
||||||
|
long stime = strtol(tokens[10], NULL, 10);
|
||||||
|
long ticks = utime + stime;
|
||||||
|
add_epoch(s, epoch);
|
||||||
|
PidCpu *slot = cpu_slot(s, pid);
|
||||||
|
if (slot == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (slot->first_ticks < 0)
|
||||||
|
{
|
||||||
|
slot->first_ticks = ticks;
|
||||||
|
}
|
||||||
|
slot->last_ticks = ticks;
|
||||||
|
slot->samples++;
|
||||||
|
copy_name(slot->name, sizeof(slot->name), name_tok);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* PRM */
|
||||||
|
if (n < 12)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long rsize_kb = strtol(tokens[11], NULL, 10);
|
||||||
|
PidRam *slot = ram_slot(s, pid);
|
||||||
|
if (slot == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rsize_kb > slot->peak_kb)
|
||||||
|
{
|
||||||
|
slot->peak_kb = rsize_kb;
|
||||||
|
}
|
||||||
|
slot->sum_kb += rsize_kb;
|
||||||
|
slot->samples++;
|
||||||
|
copy_name(slot->name, sizeof(slot->name), name_tok);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmp_long(const void *a, const void *b)
|
||||||
|
{
|
||||||
|
long la = *(const long *)a;
|
||||||
|
long lb = *(const long *)b;
|
||||||
|
if (la < lb)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (la > lb)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FNV-1a 32-bit over a NUL-terminated string; used to key the name table. */
|
||||||
|
static unsigned int fnv1a(const char *s)
|
||||||
|
{
|
||||||
|
unsigned int h = 2166136261u;
|
||||||
|
while (*s)
|
||||||
|
{
|
||||||
|
h ^= (unsigned char)*s++;
|
||||||
|
h *= 16777619u;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Per-name aggregate, built in a second pass over cpu/ram tables so that
|
||||||
|
* the caller only has to parse a few thousand output rows instead of one
|
||||||
|
* row per PID. The name table is deliberately oversized (64k slots for an
|
||||||
|
* expected few-thousand names) to keep linear-probe chains short.
|
||||||
|
*/
|
||||||
|
#define NAME_CAP_BITS 16
|
||||||
|
#define NAME_CAP (1u << NAME_CAP_BITS)
|
||||||
|
#define NAME_MASK (NAME_CAP - 1u)
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char name[ATOP_AGG_NAME_MAX];
|
||||||
|
long cpu_ticks;
|
||||||
|
int cpu_pids;
|
||||||
|
long peak_kb;
|
||||||
|
long sum_avg_kb;
|
||||||
|
int rss_samples;
|
||||||
|
int ram_pids;
|
||||||
|
char used;
|
||||||
|
} NameAgg;
|
||||||
|
|
||||||
|
static NameAgg *name_slot(NameAgg *table, const char *name)
|
||||||
|
{
|
||||||
|
unsigned int h = fnv1a(name);
|
||||||
|
for (unsigned int probes = 0; probes < NAME_CAP; probes++, h++)
|
||||||
|
{
|
||||||
|
NameAgg *slot = &table[h & NAME_MASK];
|
||||||
|
if (!slot->used)
|
||||||
|
{
|
||||||
|
slot->used = 1;
|
||||||
|
/* copy_name already enforced \0-termination on the source. */
|
||||||
|
size_t i = 0;
|
||||||
|
while (name[i] && i + 1 < sizeof(slot->name))
|
||||||
|
{
|
||||||
|
slot->name[i] = name[i];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
slot->name[i] = '\0';
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
if (strcmp(slot->name, name) == 0)
|
||||||
|
{
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write the aggregated summary to *out* in the documented TSV schema. */
|
||||||
|
void emit_results(State *s, FILE *out)
|
||||||
|
{
|
||||||
|
long start_epoch = 0;
|
||||||
|
long end_epoch = 0;
|
||||||
|
long median_interval = 0;
|
||||||
|
if (s->n_epochs > 0)
|
||||||
|
{
|
||||||
|
qsort(s->epochs, (size_t)s->n_epochs, sizeof(long), cmp_long);
|
||||||
|
start_epoch = s->epochs[0];
|
||||||
|
end_epoch = s->epochs[s->n_epochs - 1];
|
||||||
|
if (s->n_epochs >= 2)
|
||||||
|
{
|
||||||
|
long deltas[MAX_EPOCHS];
|
||||||
|
for (int i = 0; i < s->n_epochs - 1; i++)
|
||||||
|
{
|
||||||
|
deltas[i] = s->epochs[i + 1] - s->epochs[i];
|
||||||
|
}
|
||||||
|
qsort(deltas, (size_t)(s->n_epochs - 1), sizeof(long), cmp_long);
|
||||||
|
median_interval = deltas[(s->n_epochs - 1) / 2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf(out, "W\t%ld\t%ld\t%d\t%ld\n", start_epoch, end_epoch, s->n_epochs, median_interval);
|
||||||
|
|
||||||
|
NameAgg *names = calloc(NAME_CAP, sizeof(NameAgg));
|
||||||
|
if (!names)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (unsigned int i = 0; i < HASH_CAP; i++)
|
||||||
|
{
|
||||||
|
PidCpu *slot = &s->cpu[i];
|
||||||
|
if (slot->pid == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long delta = slot->last_ticks;
|
||||||
|
if (slot->samples >= 2)
|
||||||
|
{
|
||||||
|
delta = slot->last_ticks - slot->first_ticks;
|
||||||
|
if (delta < 0)
|
||||||
|
{
|
||||||
|
delta = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NameAgg *na = name_slot(names, slot->name);
|
||||||
|
if (!na)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
na->cpu_ticks += delta;
|
||||||
|
na->cpu_pids++;
|
||||||
|
}
|
||||||
|
for (unsigned int i = 0; i < HASH_CAP; i++)
|
||||||
|
{
|
||||||
|
PidRam *slot = &s->ram[i];
|
||||||
|
if (slot->pid == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long avg_kb = slot->samples ? slot->sum_kb / slot->samples : 0;
|
||||||
|
NameAgg *na = name_slot(names, slot->name);
|
||||||
|
if (!na)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (slot->peak_kb > na->peak_kb)
|
||||||
|
{
|
||||||
|
na->peak_kb = slot->peak_kb;
|
||||||
|
}
|
||||||
|
na->sum_avg_kb += avg_kb;
|
||||||
|
na->rss_samples++;
|
||||||
|
na->ram_pids++;
|
||||||
|
}
|
||||||
|
for (unsigned int i = 0; i < NAME_CAP; i++)
|
||||||
|
{
|
||||||
|
NameAgg *na = &names[i];
|
||||||
|
if (!na->used)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int pids = na->cpu_pids > na->ram_pids ? na->cpu_pids : na->ram_pids;
|
||||||
|
fprintf(out, "N\t%s\t%ld\t%ld\t%ld\t%d\t%d\n", na->name, na->cpu_ticks, na->peak_kb,
|
||||||
|
na->sum_avg_kb, na->rss_samples, pids);
|
||||||
|
}
|
||||||
|
free(names);
|
||||||
|
}
|
||||||
|
|
||||||
|
State *state_new(void)
|
||||||
|
{
|
||||||
|
State *s = calloc(1, sizeof(State));
|
||||||
|
if (!s)
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
s->cpu = calloc(HASH_CAP, sizeof(PidCpu));
|
||||||
|
s->ram = calloc(HASH_CAP, sizeof(PidRam));
|
||||||
|
s->epochs = calloc(MAX_EPOCHS, sizeof(long));
|
||||||
|
if (!s->cpu || !s->ram || !s->epochs)
|
||||||
|
{
|
||||||
|
state_free(s);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
s->n_epochs = 0;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void state_free(State *s)
|
||||||
|
{
|
||||||
|
if (!s)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
free(s->cpu);
|
||||||
|
free(s->ram);
|
||||||
|
free(s->epochs);
|
||||||
|
free(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef ATOP_AGG_NO_MAIN
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
State *s = state_new();
|
||||||
|
if (!s)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "atop_agg: out of memory\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
char *line = NULL;
|
||||||
|
size_t cap = 0;
|
||||||
|
ssize_t got;
|
||||||
|
while ((got = getline(&line, &cap, stdin)) != -1)
|
||||||
|
{
|
||||||
|
process_line(line, s);
|
||||||
|
}
|
||||||
|
free(line);
|
||||||
|
emit_results(s, stdout);
|
||||||
|
state_free(s);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
42
C/atop_agg/atop_agg.h
Normal file
42
C/atop_agg/atop_agg.h
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#ifndef ATOP_AGG_H
|
||||||
|
#define ATOP_AGG_H
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* NAME_MAX capped to keep slot size compact; typical atop comm is 15 chars. */
|
||||||
|
#define ATOP_AGG_NAME_MAX 40
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
int pid;
|
||||||
|
char name[ATOP_AGG_NAME_MAX];
|
||||||
|
long first_ticks;
|
||||||
|
long last_ticks;
|
||||||
|
int samples;
|
||||||
|
} PidCpu;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
int pid;
|
||||||
|
char name[ATOP_AGG_NAME_MAX];
|
||||||
|
long peak_kb;
|
||||||
|
long sum_kb;
|
||||||
|
int samples;
|
||||||
|
} PidRam;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
PidCpu *cpu;
|
||||||
|
PidRam *ram;
|
||||||
|
long *epochs;
|
||||||
|
int n_epochs;
|
||||||
|
} State;
|
||||||
|
|
||||||
|
State *state_new(void);
|
||||||
|
void state_free(State *s);
|
||||||
|
int tokenize_line(char *line, char **tokens, int max_tokens);
|
||||||
|
void copy_name(char *dst, size_t cap, const char *src);
|
||||||
|
void process_line(char *line, State *s);
|
||||||
|
void emit_results(State *s, FILE *out);
|
||||||
|
|
||||||
|
#endif
|
||||||
12
C/atop_agg/run.sh
Executable file
12
C/atop_agg/run.sh
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build and demo atop_agg on today's atop log.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
make
|
||||||
|
LOG="${1:-/var/log/atop/atop_$(date +%Y%m%d)}"
|
||||||
|
if [[ ! -f "$LOG" ]]; then
|
||||||
|
echo "No atop log at $LOG; pass a path as arg 1." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Aggregating $LOG ..." >&2
|
||||||
|
atop -r "$LOG" -P PRC,PRM | ./atop_agg | head -20
|
||||||
226
C/atop_agg/test_atop_agg.c
Normal file
226
C/atop_agg/test_atop_agg.c
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
/*
|
||||||
|
* Unit tests for atop_agg helpers. Compiled with --coverage; aims for
|
||||||
|
* 100% line coverage of atop_agg.c (excluding main, which is guarded
|
||||||
|
* by -DATOP_AGG_NO_MAIN).
|
||||||
|
*/
|
||||||
|
#include "atop_agg.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static int failures = 0;
|
||||||
|
|
||||||
|
#define CHECK(cond) \
|
||||||
|
do \
|
||||||
|
{ \
|
||||||
|
if (!(cond)) \
|
||||||
|
{ \
|
||||||
|
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||||
|
failures++; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
static void test_copy_name(void)
|
||||||
|
{
|
||||||
|
char buf[16];
|
||||||
|
copy_name(buf, sizeof(buf), "(bash)");
|
||||||
|
CHECK(strcmp(buf, "bash") == 0);
|
||||||
|
|
||||||
|
copy_name(buf, sizeof(buf), "bash");
|
||||||
|
CHECK(strcmp(buf, "bash") == 0);
|
||||||
|
|
||||||
|
copy_name(buf, sizeof(buf), "()");
|
||||||
|
CHECK(strcmp(buf, "unknown") == 0);
|
||||||
|
|
||||||
|
copy_name(buf, sizeof(buf), "");
|
||||||
|
CHECK(strcmp(buf, "unknown") == 0);
|
||||||
|
|
||||||
|
/* Truncation. */
|
||||||
|
copy_name(buf, sizeof(buf), "(veryverylongnameabc)");
|
||||||
|
CHECK(strlen(buf) == sizeof(buf) - 1);
|
||||||
|
|
||||||
|
/* Fallback truncation: buf too small for "unknown" itself. */
|
||||||
|
char tiny[4];
|
||||||
|
copy_name(tiny, sizeof(tiny), "");
|
||||||
|
CHECK(strcmp(tiny, "unk") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_tokenize(void)
|
||||||
|
{
|
||||||
|
char line[] = "PRC host 1000 2026/01/01 12:00:00 600 123 (bash) S 10 20\n";
|
||||||
|
char *toks[32];
|
||||||
|
int n = tokenize_line(line, toks, 32);
|
||||||
|
CHECK(n == 11);
|
||||||
|
CHECK(strcmp(toks[0], "PRC") == 0);
|
||||||
|
CHECK(strcmp(toks[7], "(bash)") == 0);
|
||||||
|
CHECK(strcmp(toks[10], "20") == 0);
|
||||||
|
|
||||||
|
/* Multi-word parenthesised name. */
|
||||||
|
char line2[] = "PRM host 1000 d t 600 200 (Web Content) S 4096 1 2 0 0\n";
|
||||||
|
char *t2[32];
|
||||||
|
int n2 = tokenize_line(line2, t2, 32);
|
||||||
|
CHECK(n2 >= 12);
|
||||||
|
CHECK(strncmp(t2[7], "(Web Content)", 13) == 0);
|
||||||
|
|
||||||
|
/* Empty / whitespace-only line. */
|
||||||
|
char empty[] = " \n";
|
||||||
|
char *t3[4];
|
||||||
|
CHECK(tokenize_line(empty, t3, 4) == 0);
|
||||||
|
|
||||||
|
/* Max-tokens cap respected. */
|
||||||
|
char big[] = "a b c d e f g h i j k";
|
||||||
|
char *t4[3];
|
||||||
|
CHECK(tokenize_line(big, t4, 3) == 3);
|
||||||
|
|
||||||
|
/* Unclosed paren at EOL — consumed to end. */
|
||||||
|
char unclosed[] = "(abc";
|
||||||
|
char *t5[2];
|
||||||
|
int n5 = tokenize_line(unclosed, t5, 2);
|
||||||
|
CHECK(n5 == 1);
|
||||||
|
CHECK(strcmp(t5[0], "(abc") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_process_and_emit(void)
|
||||||
|
{
|
||||||
|
State *s = state_new();
|
||||||
|
assert(s != NULL);
|
||||||
|
|
||||||
|
/* Two PRC samples for PID 100: first utime+stime=30, last=100.
|
||||||
|
Delta should be 70. */
|
||||||
|
char prc1[] = "PRC h 1000 d t 600 100 (cc1) S 10 20\n";
|
||||||
|
char prc2[] = "PRC h 1600 d t 600 100 (cc1) S 70 30\n";
|
||||||
|
process_line(prc1, s);
|
||||||
|
process_line(prc2, s);
|
||||||
|
|
||||||
|
/* One PRM sample for PID 100: rss=4096 kB. */
|
||||||
|
char prm1[] = "PRM h 1000 d t 600 100 (cc1) S 4096 100 4096 0 0\n";
|
||||||
|
process_line(prm1, s);
|
||||||
|
|
||||||
|
/* PRC sample for PID 200 seen only once → delta == last_ticks. */
|
||||||
|
char prc3[] = "PRC h 1000 d t 600 200 (short) S 5 5\n";
|
||||||
|
process_line(prc3, s);
|
||||||
|
|
||||||
|
/* Header / separator / unknown label should be ignored. */
|
||||||
|
char header[] = "# comment line\n";
|
||||||
|
process_line(header, s);
|
||||||
|
char sep[] = "SEP\n";
|
||||||
|
process_line(sep, s);
|
||||||
|
char other[] = "CPU h 1000 d t 600 0 0 0 0 0 0 0 0\n";
|
||||||
|
process_line(other, s);
|
||||||
|
|
||||||
|
/* Malformed: pid <= 0. */
|
||||||
|
char bad_pid[] = "PRC h 1000 d t 600 0 (x) S 1 1\n";
|
||||||
|
process_line(bad_pid, s);
|
||||||
|
|
||||||
|
/* PRC short (<11 tokens) should not crash. */
|
||||||
|
char prc_short[] = "PRC h 1000 d t 600 300 (y) S 1\n";
|
||||||
|
process_line(prc_short, s);
|
||||||
|
|
||||||
|
/* PRM short (<12 tokens) should not crash. */
|
||||||
|
char prm_short[] = "PRM h 1000 d t 600 300 (y) S 4096 1 1 0\n";
|
||||||
|
process_line(prm_short, s);
|
||||||
|
|
||||||
|
/* Emit and sanity-check the output. */
|
||||||
|
char *buf = NULL;
|
||||||
|
size_t sz = 0;
|
||||||
|
FILE *out = open_memstream(&buf, &sz);
|
||||||
|
assert(out != NULL);
|
||||||
|
emit_results(s, out);
|
||||||
|
fclose(out);
|
||||||
|
CHECK(strstr(buf, "W\t1000\t1600\t2\t600\n") != NULL);
|
||||||
|
/* cc1: cpu delta 70 (pid 100 two samples) + 0 pids column via max(cpu,ram).
|
||||||
|
Peak RSS 4096, sum_avg 4096, rss_samples 1, pids max(1,1)=1. */
|
||||||
|
CHECK(strstr(buf, "N\tcc1\t70\t4096\t4096\t1\t1\n") != NULL);
|
||||||
|
/* short: single-sample pid 200 → delta == 10; no RAM, so peak/sum/rss=0. */
|
||||||
|
CHECK(strstr(buf, "N\tshort\t10\t0\t0\t0\t1\n") != NULL);
|
||||||
|
free(buf);
|
||||||
|
state_free(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_empty_and_single_epoch(void)
|
||||||
|
{
|
||||||
|
State *s = state_new();
|
||||||
|
/* No input at all → window line with zeroes. */
|
||||||
|
char *buf = NULL;
|
||||||
|
size_t sz = 0;
|
||||||
|
FILE *out = open_memstream(&buf, &sz);
|
||||||
|
emit_results(s, out);
|
||||||
|
fclose(out);
|
||||||
|
CHECK(strstr(buf, "W\t0\t0\t0\t0\n") != NULL);
|
||||||
|
free(buf);
|
||||||
|
state_free(s);
|
||||||
|
|
||||||
|
/* Exactly one epoch → median interval stays 0. */
|
||||||
|
s = state_new();
|
||||||
|
char prc[] = "PRC h 500 d t 600 50 (a) S 1 1\n";
|
||||||
|
process_line(prc, s);
|
||||||
|
buf = NULL;
|
||||||
|
sz = 0;
|
||||||
|
out = open_memstream(&buf, &sz);
|
||||||
|
emit_results(s, out);
|
||||||
|
fclose(out);
|
||||||
|
CHECK(strstr(buf, "W\t500\t500\t1\t0\n") != NULL);
|
||||||
|
free(buf);
|
||||||
|
state_free(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_delta_clamped_to_zero(void)
|
||||||
|
{
|
||||||
|
/* Counter reset: last < first → delta must clamp to 0. */
|
||||||
|
State *s = state_new();
|
||||||
|
char a[] = "PRC h 100 d t 600 77 (x) S 50 50\n";
|
||||||
|
char b[] = "PRC h 700 d t 600 77 (x) S 10 10\n";
|
||||||
|
process_line(a, s);
|
||||||
|
process_line(b, s);
|
||||||
|
char *buf = NULL;
|
||||||
|
size_t sz = 0;
|
||||||
|
FILE *out = open_memstream(&buf, &sz);
|
||||||
|
emit_results(s, out);
|
||||||
|
fclose(out);
|
||||||
|
CHECK(strstr(buf, "N\tx\t0\t") != NULL);
|
||||||
|
free(buf);
|
||||||
|
state_free(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_hash_collision(void)
|
||||||
|
{
|
||||||
|
/* Force two PIDs into adjacent slots (Knuth hash rarely collides on
|
||||||
|
small integers, but we sweep a range to exercise the linear-probe
|
||||||
|
branch). */
|
||||||
|
State *s = state_new();
|
||||||
|
for (int pid = 1; pid <= 2000; pid++)
|
||||||
|
{
|
||||||
|
char line[128];
|
||||||
|
snprintf(line, sizeof(line), "PRC h 1000 d t 600 %d (p) S 1 1\n", pid);
|
||||||
|
process_line(line, s);
|
||||||
|
snprintf(line, sizeof(line), "PRM h 1000 d t 600 %d (p) S 4096 1 1 0 0\n", pid);
|
||||||
|
process_line(line, s);
|
||||||
|
}
|
||||||
|
state_free(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_state_free_null(void)
|
||||||
|
{
|
||||||
|
/* Freeing NULL must be safe. */
|
||||||
|
state_free(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_copy_name();
|
||||||
|
test_tokenize();
|
||||||
|
test_process_and_emit();
|
||||||
|
test_empty_and_single_epoch();
|
||||||
|
test_delta_clamped_to_zero();
|
||||||
|
test_hash_collision();
|
||||||
|
test_state_free_null();
|
||||||
|
if (failures > 0)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%d test failures\n", failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("atop_agg tests: OK\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@ -73,9 +73,6 @@ unfixable = []
|
|||||||
# inside functions — they take seconds to load and aren't needed at module level.
|
# inside functions — they take seconds to load and aren't needed at module level.
|
||||||
"python_pkg/music_gen/_music_generation.py" = ["PLC0415"]
|
"python_pkg/music_gen/_music_generation.py" = ["PLC0415"]
|
||||||
"python_pkg/music_gen/_music_speech.py" = ["PLC0415"]
|
"python_pkg/music_gen/_music_speech.py" = ["PLC0415"]
|
||||||
# PyQt6 requires camelCase overrides (rowCount, headerData, paintEvent, etc.)
|
|
||||||
# and QModelIndex() default arguments in method signatures — both unfixable.
|
|
||||||
"python_pkg/fm24_searcher/gui.py" = ["N802", "B008"]
|
|
||||||
# Circular dependency: submodules import constants (W, H, CLIP_DUR, etc.)
|
# Circular dependency: submodules import constants (W, H, CLIP_DUR, etc.)
|
||||||
# from this module, so they must be imported lazily inside _build().
|
# from this module, so they must be imported lazily inside _build().
|
||||||
"python_pkg/moviepy_showcase/moviepy_showcase.py" = ["PLC0415"]
|
"python_pkg/moviepy_showcase/moviepy_showcase.py" = ["PLC0415"]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user