testsAndMisc/linux_configuration/C/atop_agg/atop_agg.c
Krzysztof kuhy Rudnicki da1e0d3588 atop_agg: split atop_agg.c 478 -> 204 + state + emit
Three units along the file's own concerns:

  atop_agg.c (204)        ingest -- tokenising atop output, folding lines, main
  atop_agg_state.c (113)  the per-PID hash tables, slot lookup, epochs, alloc
  atop_agg_emit.c (178)   the ranked, name-aggregated report

Table geometry and the four table functions that now cross a unit boundary go
in a private atop_agg_internal.h. Those four lose `static` -- the one
visibility change here.

The Makefile edit is the load-bearing part: SRC had to grow or the build would
have linked the old single unit and the suite would have passed with the new
files unused. Checked by reading the compile line, not the exit status.

I had written this file off as unverifiable earlier. Wrong: the Makefile has
had test and coverage targets all along. Coverage is 92.9% lines / 100%
functions on the same 211 source lines, before and after -- the best verifier
of anything left over the cap. Production binary builds at -O2 and runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 10:49:46 +02:00

205 lines
5.5 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 "atop_agg_internal.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
/* Knuth multiplicative hash → index in an open-addressed table. */
/*
* 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);
}
#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