feat(ui): honor the requested node budget up to a 10M ceiling

The layout endpoint treated the default render cap (2000) as a ceiling:
an explicit max_nodes above it was silently clamped back down, so the UI
could never show more than 2000 nodes regardless of the request. Make the
default a default — an explicit request is now honored up to a hard
ceiling of 10M nodes; CBM_UI_MAX_RENDER_NODES still adjusts the ceiling
for constrained deployments. The default budget rises to 5000, and the
layout refinement pass scales its iteration count down past 100k/500k
bodies to keep very large budgets responsive. Edges need no changes: the
layout already returns every edge between loaded nodes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-07-07 23:09:05 +02:00
1 parent 5554f8a46a
commit 926e818122
3 files changed
+69 -7

No files matched your search

+1 -1
View File
@@ -1333,7 +1333,7 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) {
return;
}
int max_nodes = 50000;
int max_nodes = 0; /* 0 → layout default budget */
if (cbm_http_query_param(req->query, "max_nodes", max_str, (int)sizeof(max_str))) {
int v = atoi(max_str);
if (v > 0)
+25 -6
View File
@@ -24,8 +24,8 @@
/* ── Constants ────────────────────────────────────────────────── */
#define DEFAULT_MAX_NODES 2000
#define HARD_MAX_NODES 200000
#define DEFAULT_MAX_NODES 5000
#define HARD_MAX_NODES 10000000
#define BH_THETA 1.2f
#define OCTREE_MAX_DEPTH 26 /* stop subdividing coincident points (OOM guard) */
#define OCTREE_MIN_HALF 1e-4f /* minimum octree cell half-size */
@@ -173,16 +173,19 @@ static float rand_float(uint32_t *seed) {
return (float)((*seed >> 16) & 0x7FFF) / 32768.0f - 0.5f;
}
/* Ceiling for a caller-requested node budget. CBM_UI_MAX_RENDER_NODES lowers
* (or raises, up to HARD_MAX_NODES) the ceiling for constrained deployments;
* without it the full hard ceiling is available to explicit requests. */
static int render_node_limit(void) {
const char *raw = getenv("CBM_UI_MAX_RENDER_NODES");
if (!raw || !raw[0]) {
return DEFAULT_MAX_NODES;
return HARD_MAX_NODES;
}
errno = 0;
char *end = NULL;
long v = strtol(raw, &end, 10);
if (errno != 0 || end == raw || *end != '\0' || v <= 0) {
return DEFAULT_MAX_NODES;
return HARD_MAX_NODES;
}
if (v > HARD_MAX_NODES) {
return HARD_MAX_NODES;
@@ -190,9 +193,15 @@ static int render_node_limit(void) {
return (int)v;
}
/* The default is a default, not a ceiling: an explicit request is honored up
* to the (env-adjustable) ceiling; only a missing/invalid request falls back
* to DEFAULT_MAX_NODES. */
static int clamp_max_nodes(int requested) {
int cap = render_node_limit();
if (requested <= 0 || requested > cap) {
if (requested <= 0) {
requested = DEFAULT_MAX_NODES;
}
if (requested > cap) {
return cap;
}
return requested;
@@ -317,7 +326,17 @@ typedef struct {
/* ── Local optimization (gentle, anchor-preserving) ───────────── */
static void local_optimize(body_t *b, int n, const int *es, const int *ed, int ne) {
for (int iter = 0; iter < LOCAL_ITERATIONS; iter++) {
/* Scale iteration effort down for very large graphs: each iteration is
* O(n log n) octree work, and past ~100k bodies the anchor layout already
* dominates the visible structure — fewer refinement passes keep huge
* budgets responsive instead of multiplying minutes of compute. */
int iterations = LOCAL_ITERATIONS;
if (n > 500000) {
iterations = 10;
} else if (n > 100000) {
iterations = 20;
}
for (int iter = 0; iter < iterations; iter++) {
for (int i = 0; i < n; i++) {
b[i].fx = 0;
b[i].fy = 0;
+43
View File
@@ -358,6 +358,48 @@ TEST(layout_clamps_render_cap_from_env) {
PASS();
}
/* A caller-requested budget above the default must be honored (up to the hard
* ceiling) when no env cap is set — the default is a default, not a ceiling. */
TEST(layout_honors_budget_above_default) {
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
const char *old_raw = getenv("CBM_UI_MAX_RENDER_NODES");
char *old_cap = old_raw ? strdup(old_raw) : NULL;
cbm_unsetenv("CBM_UI_MAX_RENDER_NODES");
cbm_store_upsert_project(store, "test", "/tmp/test");
enum { BUDGET_NODES = 5100 };
for (int i = 0; i < BUDGET_NODES; i++) {
char name[32], qn[64];
snprintf(name, sizeof(name), "fn%d", i);
snprintf(qn, sizeof(qn), "test::fn%d", i);
cbm_node_t n = {.project = "test",
.label = "Function",
.name = name,
.qualified_name = qn,
.file_path = "a.c",
.start_line = i,
.end_line = i + 1};
cbm_store_upsert_node(store, &n);
}
cbm_layout_result_t *r =
cbm_layout_compute(store, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, BUDGET_NODES);
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->node_count, BUDGET_NODES);
ASSERT_EQ(r->total_nodes, BUDGET_NODES);
cbm_layout_free(r);
cbm_store_close(store);
if (old_cap) {
cbm_setenv("CBM_UI_MAX_RENDER_NODES", old_cap, 1);
free(old_cap);
}
PASS();
}
TEST(layout_deterministic) {
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
@@ -759,6 +801,7 @@ SUITE(ui) {
RUN_TEST(layout_two_connected);
RUN_TEST(layout_respects_max_nodes);
RUN_TEST(layout_clamps_render_cap_from_env);
RUN_TEST(layout_honors_budget_above_default);
RUN_TEST(layout_deterministic);
RUN_TEST(layout_to_json);
RUN_TEST(layout_null_inputs);