mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 13:03:13 +02:00
atop's `-P PRC` output inserts the clock-tick rate (HZ=100) between the
`state` and `utime` columns. Both the Python parser and the native C
aggregator read that constant as utime for every record, charging a flat
1 CPU-second per record — so cpu_seconds collapsed to pid_count and
short-lived fork-storm commands (xset, dd, chronyc) topped the CPU table
(xset showed 67h). The old test fixtures lacked the HZ field, so code and
tests agreed on the bug.
- _parse_prc / atop_agg.c: read utime/stime past the HZ field (after+2/+3,
tokens[10]/[11]); bump the length guards accordingly
- restore C/atop_agg (deleted in 89b4f59) under linux_configuration/C/,
where the build path resolves; corrected test fixtures to include HZ
- _atop_agg_binary: fall back to the Python parser when the C source tree
is gone instead of trusting an orphaned cached binary
- add regression tests proving HZ is not summed as CPU
- bundle the in-progress since-last-report multi-day aggregation (segments,
-b/-e bounding, persisted state, window merging) and its tests/conftest
- meta: gate linux_configuration/tests in pytest_changed_packages.py
Verified by running usage_report.py --date 20260604: Top CPU now led by
SkyrimSE; xset/dd/chronyc fall to ~0. C unit tests + full pytest suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
479 lines
12 KiB
C
479 lines
12 KiB
C
/*
|
|
* 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);
|
|
/* Both PRC and PRM need >= 12 fields: the 6-field generic prefix, pid,
|
|
(name), state, atop's per-label extra field (HZ for PRC / pagesize for
|
|
PRM), then the first data column we read at index 10/11. */
|
|
if (n < 12)
|
|
{
|
|
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)
|
|
{
|
|
/* atop inserts its clock-tick rate (HZ) at tokens[9], between the
|
|
state field and utime/stime, so the CPU columns live at [10]/[11].
|
|
Reading [9] charged a constant HZ (100) as CPU to every record —
|
|
the bug this fixes. */
|
|
long utime = strtol(tokens[10], NULL, 10);
|
|
long stime = strtol(tokens[11], 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: rsize_kb sits at tokens[11] (after state, pagesize, vsize); the
|
|
n < 12 length guard at the top already guarantees it is present. */
|
|
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
|