mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:07:04 +01:00
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
This commit is contained in:
parent
e6b9c730f2
commit
da1e0d3588
19
docs/superpowers/contracts/atop-agg-split-2026-08-17.json
Normal file
19
docs/superpowers/contracts/atop-agg-split-2026-08-17.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"title": "Split linux_configuration/C/atop_agg/atop_agg.c under the 250-line cap",
|
||||
"objective": "Bring atop_agg.c (478 lines) under the repo-wide 250-line file cap by splitting the single translation unit three ways along its existing concerns -- ingest, per-PID table management, and report emission -- with the TSV output the Python usage report consumes proven unchanged by the project's own C test binary at its existing 92.9% line / 100% function coverage.",
|
||||
"acceptance_criteria": [
|
||||
"atop_agg.c and all new .c/.h files are each at most 250 lines, and check_file_length.sh --all no longer lists any atop_agg file.",
|
||||
"make clean && make test compiles every unit under -Wall -Wextra with no new warnings and prints 'atop_agg tests: OK'. The compile line must name the new .c files, proving they are built rather than silently omitted.",
|
||||
"make coverage reports the same 92.9% lines (196 of 211) and 100% functions (13 of 13) as the pre-split baseline, measured after lcov strips test_atop_agg.c. Unchanged totals on unchanged source lines is what shows no code left test reach.",
|
||||
"The Makefile's SRC and HDR variables list every new file, and the test target compiles $(SRC) rather than a hardcoded atop_agg.c. Without this the build would link the old self-contained unit and the suite would pass with the new files unused -- a fail-open the tests cannot detect.",
|
||||
"The production binary (make, which builds at -O2 without -DATOP_AGG_NO_MAIN) links and emits a well-formed record, exercising the main path the test binary compiles out.",
|
||||
"Zero suppressions: no #pragma to silence a warning, no -Wno- flag added, and no change to the coverage or test targets' strictness."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Any change to the output format -- the W/C/R TSV record shapes are a contract with the Python driver in system-maintenance/bin and must be byte-identical.",
|
||||
"Any change to the hash geometry (HASH_CAP_BITS, MAX_EPOCHS, MAX_TOKENS) or the aggregation arithmetic, both of which the tests pin only loosely.",
|
||||
"Widening the public interface in atop_agg.h; declarations that must cross the new unit boundary go in a private internal header instead.",
|
||||
"Wiring atop_agg into CI, which has no workflow today -- a separate piece of work."
|
||||
],
|
||||
"verifier": "cd linux_configuration/C/atop_agg && make clean && make test (expect 'atop_agg tests: OK' and a compile line naming every unit); make coverage (expect 92.9% lines, 100% functions after the lcov strip pass); make clean && make && printf 'PRC\\tfoo\\t1\\t100\\ny\\n' | ./atop_agg (expect a well-formed W record, exit 0); bash ~/utils/scripts/check_file_length.sh --all (expect no atop_agg entry); git diff on atop_agg.h (expect empty). Captured at HEAD as the baseline and re-run after the split."
|
||||
}
|
||||
52
docs/superpowers/evidence/atop-agg-split-2026-08-17.json
Normal file
52
docs/superpowers/evidence/atop-agg-split-2026-08-17.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"intent": "Bring linux_configuration/C/atop_agg/atop_agg.c (478 lines) under the repo-wide 250-line cap with no change to the TSV summary the Python report consumes.",
|
||||
"scope": [
|
||||
"linux_configuration/C/atop_agg/atop_agg.c — reduced to ingest: tokenising atop output, folding lines into the tables, and main",
|
||||
"linux_configuration/C/atop_agg/atop_agg_state.c — new: the per-PID hash tables, slot lookup, epoch recording, state_new/state_free",
|
||||
"linux_configuration/C/atop_agg/atop_agg_emit.c — new: the ranked, name-aggregated report",
|
||||
"linux_configuration/C/atop_agg/atop_agg_internal.h — new: table geometry and the now-cross-unit table declarations",
|
||||
"linux_configuration/C/atop_agg/Makefile — SRC/HDR extended so every target compiles the new units",
|
||||
"Non-goal: any change to the output format, the hash geometry, or the aggregation arithmetic"
|
||||
],
|
||||
"changes": [
|
||||
"Split the single translation unit three ways along its existing concerns: ingest, table management, and reporting. The three .c files share the State type via the existing public atop_agg.h and the table geometry via a new private atop_agg_internal.h.",
|
||||
"hash_pid, cpu_slot, ram_slot and add_epoch lost their `static` qualifier: they are defined in atop_agg_state.c but called from process_line in atop_agg.c, so they now cross a translation-unit boundary and are declared in the internal header. This is the one visibility change in the commit and is noted in the header comment.",
|
||||
"Extended the Makefile's SRC and HDR variables and pointed the test target at $(SRC), so the binary, the test binary and the coverage target all compile the three units. Without this the new files would have been silently absent from every build — the test binary would still have linked and passed, since the old atop_agg.c alone was self-contained before the split."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "make clean && make test",
|
||||
"result": "pass",
|
||||
"evidence": "Compiles all three units under -Wall -Wextra with no warnings, and ./test_atop_agg prints 'atop_agg tests: OK'. The compile line in the output confirms atop_agg_state.c and atop_agg_emit.c are actually being built rather than skipped."
|
||||
},
|
||||
{
|
||||
"command": "make coverage",
|
||||
"result": "pass",
|
||||
"evidence": "92.9% lines (196 of 211) and 100% functions (13 of 13) after lcov strips the test file — identical to the pre-split baseline measured on the single file. Unchanged totals on unchanged source lines is what shows no code fell out of test reach; the higher 95.1%/344-line figure in the same output is the pre-strip pass that includes test_atop_agg.c itself."
|
||||
},
|
||||
{
|
||||
"command": "make clean && make && printf 'PRC\\tfoo\\t1\\t100\\ny\\n' | ./atop_agg",
|
||||
"result": "pass",
|
||||
"evidence": "The production binary (built with -O2 and without -DATOP_AGG_NO_MAIN, unlike the test binary) links and runs, emitting a well-formed W record. This checks the real main path, which the test binary compiles out."
|
||||
},
|
||||
{
|
||||
"command": "bash ~/utils/scripts/check_file_length.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "atop_agg.c 204, atop_agg_state.c 113, atop_agg_emit.c 178, atop_agg_internal.h 22 — no atop_agg file appears in the over-cap listing, down from a single 478-line file."
|
||||
},
|
||||
{
|
||||
"command": "Coverage measurement, run before choosing this target",
|
||||
"result": "pass",
|
||||
"evidence": "make coverage at HEAD reported 92.9% lines and 100% functions. That is the strongest verifier among everything still over the cap, and it corrected an earlier assumption of mine that the C file had no usable test gate — the Makefile has had both a test and a coverage target all along."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Four functions changed from static to external linkage. They keep their names and are declared only in a private header not shipped to callers, but the symbols are now visible to the linker and could collide with same-named symbols in a future translation unit added to this binary.",
|
||||
"The Makefile change is load-bearing in a way the tests cannot catch: had SRC not been extended, the build would have linked the old single unit and the suite would still have passed while the new files sat unused. Verified by reading the compile line in the make output rather than by trusting the exit status.",
|
||||
"atop_agg has no CI workflow; the shell-tests workflow lists linux_configuration/tests/*.sh only. make test is hand-run, so this coverage gate protects future edits only if someone runs it."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit — two edited files and three added files, with no data, config or schema migration.",
|
||||
"After rollback, run make clean && make test in linux_configuration/C/atop_agg; expect 'atop_agg tests: OK' from the single-file build."
|
||||
]
|
||||
}
|
||||
@ -2,8 +2,8 @@ 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
|
||||
SRC := atop_agg.c atop_agg_state.c atop_agg_emit.c
|
||||
HDR := atop_agg.h atop_agg_internal.h
|
||||
BIN := atop_agg
|
||||
|
||||
.PHONY: all clean rebuild test coverage
|
||||
@ -13,8 +13,8 @@ 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_atop_agg: test_atop_agg.c $(SRC) $(HDR)
|
||||
$(CC) $(COV) -o test_atop_agg test_atop_agg.c $(SRC)
|
||||
|
||||
test: test_atop_agg
|
||||
./test_atop_agg
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
* 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>
|
||||
@ -21,87 +22,7 @@
|
||||
#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
|
||||
@ -259,201 +180,6 @@ void process_line(char *line, State *s)
|
||||
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)
|
||||
{
|
||||
|
||||
178
linux_configuration/C/atop_agg/atop_agg_emit.c
Normal file
178
linux_configuration/C/atop_agg/atop_agg_emit.c
Normal file
@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Reporting half of atop_agg: turning the accumulated per-PID tables into the
|
||||
* ranked, name-aggregated summary the report consumes.
|
||||
*
|
||||
* Split from atop_agg.c, which owns ingest (tokenising atop output and folding
|
||||
* each line into the tables). The two share only the table geometry in
|
||||
* atop_agg_internal.h and the State type in atop_agg.h.
|
||||
*/
|
||||
#include "atop_agg.h"
|
||||
#include "atop_agg_internal.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
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);
|
||||
}
|
||||
35
linux_configuration/C/atop_agg/atop_agg_internal.h
Normal file
35
linux_configuration/C/atop_agg/atop_agg_internal.h
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Private constants shared between atop_agg.c (ingest) and atop_agg_emit.c
|
||||
* (reporting). Not part of the public interface in atop_agg.h: callers of the
|
||||
* library never need the table geometry, but both translation units size their
|
||||
* loops and stack buffers from it, so it cannot live in either .c file alone.
|
||||
*/
|
||||
#ifndef ATOP_AGG_INTERNAL_H
|
||||
#define ATOP_AGG_INTERNAL_H
|
||||
|
||||
#include "atop_agg.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
|
||||
|
||||
|
||||
/*
|
||||
* Table internals, shared because ingest (atop_agg.c) looks slots up while
|
||||
* allocation and epoch recording live in atop_agg_state.c. Not `static` any
|
||||
* more precisely because they now cross a translation-unit boundary.
|
||||
*/
|
||||
unsigned int hash_pid(int pid);
|
||||
PidCpu *cpu_slot(State *s, int pid);
|
||||
PidRam *ram_slot(State *s, int pid);
|
||||
void add_epoch(State *s, long epoch);
|
||||
|
||||
#endif /* ATOP_AGG_INTERNAL_H */
|
||||
113
linux_configuration/C/atop_agg/atop_agg_state.c
Normal file
113
linux_configuration/C/atop_agg/atop_agg_state.c
Normal file
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* The per-PID hash tables and the State they live in: slot lookup by PID,
|
||||
* epoch recording, and allocation/teardown.
|
||||
*
|
||||
* Split from atop_agg.c so the ingest half is just parsing. Open-addressed
|
||||
* with linear probing; the geometry is in atop_agg_internal.h because the
|
||||
* reporting half walks the same tables.
|
||||
*/
|
||||
#include "atop_agg.h"
|
||||
#include "atop_agg_internal.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
unsigned int hash_pid(int pid)
|
||||
{
|
||||
unsigned int k = (unsigned int)pid;
|
||||
return (k * 2654435761u) >> (32 - HASH_CAP_BITS);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user