The worker's rusage collector already measures and sends "memory_swap"/"tasks_current" per job, but xce_host_runusage_t had no field to receive them at all -- a complete dead end, not a naming mismatch like bug-1735's cpu_time/mem_usage. Added mem_swap/ tasks_current fields (types.h), matching capnp fields on HostRunusage (controller.capnp, new ordinals @3/@4) for WAL field-completeness, and wired update_host_runusage() (misc.c) to parse them. Also fixed merge_job_runusage()'s (job.c) existing-host merge branch, which only copied cpu_time/mem_usage per host -- the two new fields would otherwise have been silently dropped on WAL replay/catchup for a host already seen. Also computed peak_cpu_time as the running max of aggregate_job_ runusage()'s per-call cpu_time sum across every report a job has ever received, guarding against a later report reducing it. Deliberately NOT computed: peak_cpu_duration and the three efficiency fields (mem_efficiency/avg_cpu_efficiency/peak_cpu_efficiency) -- these need a "used vs. requested" ratio against a job's resource request (a resreq expression tree, not a plain core/slot count) and/or an unspecified duration semantic. A documented scope decision, not an oversight -- see aggregate_job_runusage()'s doc comment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QxaSzHRB7pmYFp4cVnwkA
340 lines
13 KiB
C
340 lines
13 KiB
C
/* test_host_runusage.c -- bug-1735/bug-1736 regression suite
|
|
*
|
|
* bug-1735: job memory usage always reads 0 in the scheduler's simulation
|
|
* record file, despite the worker daemon genuinely measuring it. Two
|
|
* independent, confirmed root causes in misc.c/job.c:
|
|
*
|
|
* 1. update_host_runusage() (misc.c) only ever matched resnames
|
|
* "cputime"/"mem" -- but the REAL worker's rusage collector
|
|
* (collect_job_rusages(), xcecd_cgroups.c) sends "cpu_time_sec"/
|
|
* "memory_rss" (see that file's res_names[] array). The two never
|
|
* matched, so xce_host_runusage_t.cpu_time/mem_usage stayed at their
|
|
* calloc()'d zero forever, for every real (non-simulated) job.
|
|
*
|
|
* 2. update_job_rusages() (job.c) had a standing FIXME ("update other
|
|
* fields of job rusage later") -- xce_job_runusage_t's own job-level
|
|
* max_mem/avg_mem/used_mem/cpu_time were never derived from the
|
|
* per-host host_runusage[] entries update_host_runusage() populates,
|
|
* so a multi-host job's aggregate usage stayed 0 even once (1) above
|
|
* is fixed.
|
|
*
|
|
* bug-1736 (split from bug-1735, lower priority, display/reporting only):
|
|
* two more gaps in the same area.
|
|
*
|
|
* 3. xce_host_runusage_t had NO field at all to receive the worker's
|
|
* "memory_swap"/"tasks_current" resnames -- a complete dead end, not
|
|
* a naming mismatch. Added mem_swap/tasks_current fields plus parsing.
|
|
*
|
|
* 4. xce_job_runusage_t.peak_cpu_time was never computed. Added as the
|
|
* running MAX of aggregate_job_runusage()'s per-call cpu_time sum
|
|
* across every call for a given job (not just the latest report).
|
|
*
|
|
* peak_cpu_duration/mem_efficiency/avg_cpu_efficiency/peak_cpu_efficiency
|
|
* are DELIBERATELY still not covered -- see aggregate_job_runusage()'s own
|
|
* doc comment (misc.c) for why: no unambiguous formula/definition exists
|
|
* for these without a product decision, unlike peak_cpu_time.
|
|
*
|
|
* This file unit-tests all four fixes directly against the REAL misc.c
|
|
* (linked standalone, mirroring resval_typeresolve_unit_test's/
|
|
* sched_worker_change_unit_test's isolation rationale -- misc.c's
|
|
* update_host_runusage()/aggregate_job_runusage() are calloc/realloc/
|
|
* strcmp-only, no RTTI locking, no networking, so they link and run with
|
|
* zero fake/stub scaffolding). get_ep_worker()/get_ep_role() (same file)
|
|
* are untouched by this fix and unused by these tests, but still link
|
|
* fine via controller_unit_test_deps' existing librtti_dep.
|
|
*/
|
|
#include <math.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "cluster.h"
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* This project's shared unity_dep is built with double-precision support
|
|
* excluded (TEST_ASSERT_EQUAL_DOUBLE compiles to an unconditional "Unity
|
|
* Double Precision Disabled" failure) -- no other test file in this
|
|
* codebase uses it either. Plain epsilon comparison in ordinary C avoids
|
|
* Unity's own double formatting/compare path entirely. */
|
|
#define ASSERT_DOUBLE_EQ_MSG(expected, actual, msg) \
|
|
TEST_ASSERT_TRUE_MESSAGE(fabs((expected) - (actual)) < 1e-6, msg)
|
|
#define ASSERT_DOUBLE_EQ(expected, actual) \
|
|
ASSERT_DOUBLE_EQ_MSG(expected, actual, #actual " != " #expected)
|
|
|
|
static xce_rusage_t** make_usages(const char* name1, float val1,
|
|
const char* name2, float val2)
|
|
{
|
|
xce_rusage_t** usages = (xce_rusage_t**)calloc(2, sizeof(xce_rusage_t*));
|
|
|
|
usages[0] = (xce_rusage_t*)calloc(1, sizeof(xce_rusage_t));
|
|
usages[0]->resname = strdup(name1);
|
|
usages[0]->value = val1;
|
|
|
|
usages[1] = (xce_rusage_t*)calloc(1, sizeof(xce_rusage_t));
|
|
usages[1]->resname = strdup(name2);
|
|
usages[1]->value = val2;
|
|
|
|
return usages;
|
|
}
|
|
|
|
static void free_usages(xce_rusage_t** usages)
|
|
{
|
|
free(usages[0]->resname);
|
|
free(usages[0]);
|
|
free(usages[1]->resname);
|
|
free(usages[1]);
|
|
free(usages);
|
|
}
|
|
|
|
static xce_rusage_t** make_usages4(const char* n1, float v1, const char* n2,
|
|
float v2, const char* n3, float v3,
|
|
const char* n4, float v4)
|
|
{
|
|
const char* names[4] = { n1, n2, n3, n4 };
|
|
float values[4] = { v1, v2, v3, v4 };
|
|
xce_rusage_t** usages = (xce_rusage_t**)calloc(4, sizeof(xce_rusage_t*));
|
|
int i;
|
|
|
|
for (i = 0; i < 4; i++) {
|
|
usages[i] = (xce_rusage_t*)calloc(1, sizeof(xce_rusage_t));
|
|
usages[i]->resname = strdup(names[i]);
|
|
usages[i]->value = values[i];
|
|
}
|
|
|
|
return usages;
|
|
}
|
|
|
|
static void free_usages4(xce_rusage_t** usages)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < 4; i++) {
|
|
free(usages[i]->resname);
|
|
free(usages[i]);
|
|
}
|
|
free(usages);
|
|
}
|
|
|
|
static void free_runusage(xce_job_runusage_t* r)
|
|
{
|
|
int i;
|
|
|
|
if (r == NULL) {
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < r->num_hosts; i++) {
|
|
free(r->host_runusage[i]->host_name);
|
|
free(r->host_runusage[i]);
|
|
}
|
|
free(r->host_runusage);
|
|
free(r);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Tests -- update_host_runusage() resname alignment (root cause 1) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void
|
|
test_update_host_runusage_matches_real_worker_resnames(void)
|
|
{
|
|
xce_job_runusage_t* runusage = NULL;
|
|
xce_rusage_t** usages
|
|
= make_usages("cpu_time_sec", 42.0f, "memory_rss", 128.5f);
|
|
|
|
int rc = update_host_runusage(&runusage, strdup("host-a"), 2, usages);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
TEST_ASSERT_NOT_NULL(runusage);
|
|
TEST_ASSERT_EQUAL_INT(1, runusage->num_hosts);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
42, runusage->host_runusage[0]->cpu_time,
|
|
"cpu_time must be set from the worker's real 'cpu_time_sec' resname "
|
|
"(bug-1735 root cause 1) -- NOT the old, never-matching 'cputime'");
|
|
ASSERT_DOUBLE_EQ_MSG(
|
|
128.5, runusage->host_runusage[0]->mem_usage,
|
|
"mem_usage must be set from the worker's real 'memory_rss' resname "
|
|
"(bug-1735 root cause 1) -- NOT the old, never-matching 'mem'");
|
|
|
|
free_usages(usages);
|
|
free_runusage(runusage);
|
|
}
|
|
|
|
static void test_update_host_runusage_updates_existing_host_in_place(void)
|
|
{
|
|
xce_job_runusage_t* runusage = NULL;
|
|
xce_rusage_t** first = make_usages("cpu_time_sec", 10.0f, "memory_rss", 50.0f);
|
|
xce_rusage_t** second = make_usages("cpu_time_sec", 20.0f, "memory_rss", 75.0f);
|
|
|
|
update_host_runusage(&runusage, strdup("host-a"), 2, first);
|
|
update_host_runusage(&runusage, strdup("host-a"), 2, second);
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
1, runusage->num_hosts,
|
|
"a second report for the SAME host must update in place, not "
|
|
"append a duplicate host entry");
|
|
TEST_ASSERT_EQUAL_INT(20, runusage->host_runusage[0]->cpu_time);
|
|
ASSERT_DOUBLE_EQ(75.0, runusage->host_runusage[0]->mem_usage);
|
|
|
|
free_usages(first);
|
|
free_usages(second);
|
|
free_runusage(runusage);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Tests -- bug-1736: memory_swap/tasks_current (root cause 3) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void test_update_host_runusage_parses_swap_and_task_count(void)
|
|
{
|
|
xce_job_runusage_t* runusage = NULL;
|
|
xce_rusage_t** usages = make_usages4(
|
|
"cpu_time_sec", 5.0f, "memory_rss", 64.0f, "memory_swap", 12.5f,
|
|
"tasks_current", 3.0f);
|
|
|
|
int rc = update_host_runusage(&runusage, strdup("host-a"), 4, usages);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
ASSERT_DOUBLE_EQ_MSG(
|
|
12.5, runusage->host_runusage[0]->mem_swap,
|
|
"mem_swap must be set from the worker's 'memory_swap' resname "
|
|
"(bug-1736) -- previously had no field to land in at all");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
3, runusage->host_runusage[0]->tasks_current,
|
|
"tasks_current must be set from the worker's 'tasks_current' "
|
|
"resname (bug-1736) -- previously had no field to land in at all");
|
|
|
|
free_usages4(usages);
|
|
free_runusage(runusage);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Tests -- aggregate_job_runusage() (root cause 2) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static xce_job_runusage_t* make_runusage_with_hosts(int n, const int* cpu,
|
|
const double* mem)
|
|
{
|
|
xce_job_runusage_t* r = (xce_job_runusage_t*)calloc(1, sizeof(*r));
|
|
int i;
|
|
|
|
r->num_hosts = n;
|
|
r->host_runusage
|
|
= (xce_host_runusage_t**)calloc(n, sizeof(xce_host_runusage_t*));
|
|
|
|
for (i = 0; i < n; i++) {
|
|
r->host_runusage[i]
|
|
= (xce_host_runusage_t*)calloc(1, sizeof(xce_host_runusage_t));
|
|
r->host_runusage[i]->host_name = strdup("h");
|
|
r->host_runusage[i]->cpu_time = cpu[i];
|
|
r->host_runusage[i]->mem_usage = mem[i];
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
static void test_aggregate_job_runusage_single_host(void)
|
|
{
|
|
int cpu[] = { 30 };
|
|
double mem[] = { 256.0 };
|
|
xce_job_runusage_t* r = make_runusage_with_hosts(1, cpu, mem);
|
|
|
|
aggregate_job_runusage(r);
|
|
|
|
ASSERT_DOUBLE_EQ(256.0, r->used_mem);
|
|
ASSERT_DOUBLE_EQ(256.0, r->max_mem);
|
|
ASSERT_DOUBLE_EQ(256.0, r->avg_mem);
|
|
TEST_ASSERT_EQUAL_INT(30, r->cpu_time);
|
|
|
|
free_runusage(r);
|
|
}
|
|
|
|
static void test_aggregate_job_runusage_multi_host_sum_max_avg(void)
|
|
{
|
|
int cpu[] = { 10, 20, 30 };
|
|
double mem[] = { 100.0, 300.0, 200.0 };
|
|
xce_job_runusage_t* r = make_runusage_with_hosts(3, cpu, mem);
|
|
|
|
aggregate_job_runusage(r);
|
|
|
|
ASSERT_DOUBLE_EQ_MSG(
|
|
600.0, r->used_mem,
|
|
"used_mem is the SUM of every host's mem_usage (total footprint of "
|
|
"a multi-host job), not any single host's value");
|
|
ASSERT_DOUBLE_EQ_MSG(300.0, r->max_mem, "max_mem is the MAX across hosts");
|
|
ASSERT_DOUBLE_EQ_MSG(200.0, r->avg_mem, "avg_mem is the AVERAGE across hosts");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
60, r->cpu_time, "job-level cpu_time is the SUM across hosts");
|
|
|
|
free_runusage(r);
|
|
}
|
|
|
|
static void test_aggregate_job_runusage_peak_cpu_time_is_running_max(void)
|
|
{
|
|
int cpu1[] = { 50 };
|
|
double mem1[] = { 100.0 };
|
|
xce_job_runusage_t* r = make_runusage_with_hosts(1, cpu1, mem1);
|
|
|
|
aggregate_job_runusage(r);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(50, r->peak_cpu_time,
|
|
"peak_cpu_time tracks the first report");
|
|
|
|
/* Second report: a LOWER cpu_time (e.g. a merge/replay delivered an
|
|
* older report after a newer one) -- peak must NOT regress. */
|
|
r->host_runusage[0]->cpu_time = 20;
|
|
aggregate_job_runusage(r);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
50, r->peak_cpu_time,
|
|
"peak_cpu_time (bug-1736) is the running MAX across every report "
|
|
"this job has ever received, not just the latest one -- a lower "
|
|
"subsequent report must not regress it");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
20, r->cpu_time, "cpu_time itself IS the latest report, unlike peak");
|
|
|
|
/* Third report: a NEW high -- peak must advance. */
|
|
r->host_runusage[0]->cpu_time = 90;
|
|
aggregate_job_runusage(r);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(90, r->peak_cpu_time,
|
|
"peak_cpu_time advances on a new high");
|
|
|
|
free_runusage(r);
|
|
}
|
|
|
|
static void test_aggregate_job_runusage_null_and_empty_are_safe(void)
|
|
{
|
|
xce_job_runusage_t empty = { 0 };
|
|
|
|
aggregate_job_runusage(NULL); /* must not crash */
|
|
aggregate_job_runusage(&empty); /* num_hosts == 0, must not crash */
|
|
|
|
ASSERT_DOUBLE_EQ(0.0, empty.used_mem);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Standalone Unity main (this binary is its own test executable -- see */
|
|
/* meson.build's host_runusage_unit_test) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void) { return; }
|
|
|
|
int main(void)
|
|
{
|
|
printf("\n>> bug-1735: host/job rusage resname + aggregation fix\n\n");
|
|
|
|
UNITY_BEGIN();
|
|
|
|
RUN_TEST(test_update_host_runusage_matches_real_worker_resnames);
|
|
RUN_TEST(test_update_host_runusage_updates_existing_host_in_place);
|
|
RUN_TEST(test_update_host_runusage_parses_swap_and_task_count);
|
|
RUN_TEST(test_aggregate_job_runusage_single_host);
|
|
RUN_TEST(test_aggregate_job_runusage_multi_host_sum_max_avg);
|
|
RUN_TEST(test_aggregate_job_runusage_peak_cpu_time_is_running_max);
|
|
RUN_TEST(test_aggregate_job_runusage_null_and_empty_are_safe);
|
|
|
|
return UNITY_END();
|
|
}
|