Skip to content

Commit 6f0af04

Browse files
committed
test(callgrind-utils): add fractal fixture with deep instrumentation start
Pure-compute port of the CodSpeed fractal benchmark: a rich recursive call graph (build/hash/sum/max-path/count/leaves + memoized fib + multi-pass analysis) that fires the Callgrind client requests several frames deep (main -> run_benchmark -> warmup -> run_measured), exercising the shadow-stack seeder. Integer arithmetic and a static node pool keep the graph free of libc/libm frames so the snapshots are stable across platforms. Wired into both fixture_canonical_json and fixture_full_trace.
1 parent 990e110 commit 6f0af04

4 files changed

Lines changed: 994 additions & 0 deletions

File tree

callgrind-utils/testdata/fractal.c

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
// Build with `-g -O0` so the functions are real (no inlining) and carry debug
2+
// names:
3+
// cc -g -O0 -I callgrind -I include ...
4+
5+
#include <callgrind.h>
6+
7+
#define MAX_DEPTH 5
8+
#define BRANCH_FACTOR 3
9+
#define FIB_N 25
10+
#define MAX_NODES 1024
11+
12+
typedef struct FractalNode {
13+
long value;
14+
int depth;
15+
unsigned long computed_hash;
16+
struct FractalNode *children[BRANCH_FACTOR];
17+
int num_children;
18+
} FractalNode;
19+
20+
// Bump-allocated node pool: avoids the allocator frames a heap tree would leak
21+
// into the profile. Reset at the start of every tree build.
22+
static FractalNode g_pool[MAX_NODES];
23+
static int g_pool_used;
24+
25+
static FractalNode *pool_alloc(void) {
26+
FractalNode *node = &g_pool[g_pool_used++];
27+
node->value = 0;
28+
node->depth = 0;
29+
node->computed_hash = 0;
30+
node->num_children = 0;
31+
return node;
32+
}
33+
34+
// Deterministic child seed (integer stand-in for the original golden-ratio sine).
35+
static long compute_child_value(long parent_value, int child_index, int depth) {
36+
unsigned long base = (unsigned long)parent_value * 2654435761UL;
37+
unsigned long offset = (unsigned long)(child_index + 1) * (unsigned long)(depth + 1);
38+
return (long)(((base ^ (offset * 40503UL)) % 100UL) + 1UL);
39+
}
40+
41+
static unsigned long compute_tree_hash(const FractalNode *node) {
42+
unsigned long hash = (unsigned long)node->value;
43+
hash = hash * 31 + (unsigned long)node->depth;
44+
45+
for (int i = 0; i < node->num_children; i++) {
46+
hash = hash * 31 + compute_tree_hash(node->children[i]);
47+
}
48+
return hash;
49+
}
50+
51+
static FractalNode *build_fractal(int depth, long seed) {
52+
FractalNode *node = pool_alloc();
53+
node->value = seed;
54+
node->depth = depth;
55+
56+
if (depth < MAX_DEPTH) {
57+
node->num_children = BRANCH_FACTOR;
58+
for (int i = 0; i < BRANCH_FACTOR; i++) {
59+
long child_seed = compute_child_value(seed, i, depth);
60+
node->children[i] = build_fractal(depth + 1, child_seed);
61+
}
62+
}
63+
64+
node->computed_hash = compute_tree_hash(node);
65+
return node;
66+
}
67+
68+
static long recursive_sum(const FractalNode *node) {
69+
long children_sum = 0;
70+
for (int i = 0; i < node->num_children; i++) {
71+
children_sum += recursive_sum(node->children[i]);
72+
}
73+
return node->value + children_sum;
74+
}
75+
76+
static long max_path_sum(const FractalNode *node) {
77+
if (node->num_children == 0) {
78+
return node->value;
79+
}
80+
81+
long max_child_path = 0;
82+
for (int i = 0; i < node->num_children; i++) {
83+
long child_path = max_path_sum(node->children[i]);
84+
if (child_path > max_child_path) {
85+
max_child_path = child_path;
86+
}
87+
}
88+
return node->value + max_child_path;
89+
}
90+
91+
static int count_nodes(const FractalNode *node) {
92+
int count = 1;
93+
for (int i = 0; i < node->num_children; i++) {
94+
count += count_nodes(node->children[i]);
95+
}
96+
return count;
97+
}
98+
99+
// Collected leaves land in a shared buffer; the caller resets g_leaf_count.
100+
static long g_leaves[MAX_NODES];
101+
static int g_leaf_count;
102+
103+
static void collect_leaves(const FractalNode *node) {
104+
if (node->num_children == 0) {
105+
g_leaves[g_leaf_count++] = node->value;
106+
return;
107+
}
108+
for (int i = 0; i < node->num_children; i++) {
109+
collect_leaves(node->children[i]);
110+
}
111+
}
112+
113+
static int fibonacci_memo(int n, int *memo) {
114+
if (n <= 1) {
115+
return n;
116+
}
117+
if (memo[n] != -1) {
118+
return memo[n];
119+
}
120+
121+
int result = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo);
122+
memo[n] = result;
123+
return result;
124+
}
125+
126+
static long compute_variance(const long *values, int count) {
127+
if (count == 0) {
128+
return 0;
129+
}
130+
131+
long mean = 0;
132+
for (int i = 0; i < count; i++) {
133+
mean += values[i];
134+
}
135+
mean /= count;
136+
137+
long variance = 0;
138+
for (int i = 0; i < count; i++) {
139+
long diff = values[i] - mean;
140+
variance += diff * diff;
141+
}
142+
return variance / count;
143+
}
144+
145+
static long recursive_path_score(long value, int depth) {
146+
if (depth == 0 || value < 2) {
147+
return value;
148+
}
149+
long reduced = (value * 4) / 5;
150+
return 1 + recursive_path_score(reduced, depth - 1) / 2;
151+
}
152+
153+
static long compute_complexity_score(int node_count, long variance, long max_path) {
154+
long base_score = (long)node_count * variance;
155+
long path_factor = recursive_path_score(max_path, 5);
156+
return base_score + path_factor;
157+
}
158+
159+
typedef struct {
160+
long total_sum;
161+
int node_count;
162+
long max_path;
163+
long leaf_variance;
164+
long complexity_score;
165+
} TreeAnalysis;
166+
167+
static TreeAnalysis analyze_fractal_tree(FractalNode *tree, int analysis_depth) {
168+
long total_sum = recursive_sum(tree);
169+
int node_count = count_nodes(tree);
170+
long max_path = max_path_sum(tree);
171+
172+
g_leaf_count = 0;
173+
collect_leaves(tree);
174+
long leaf_variance = compute_variance(g_leaves, g_leaf_count);
175+
176+
TreeAnalysis analysis;
177+
if (analysis_depth > 0) {
178+
TreeAnalysis nested = analyze_fractal_tree(tree, analysis_depth - 1);
179+
analysis.total_sum = total_sum + nested.total_sum / 10;
180+
analysis.node_count = node_count;
181+
analysis.max_path = max_path > nested.max_path ? max_path : nested.max_path;
182+
analysis.leaf_variance = (leaf_variance + nested.leaf_variance) / 2;
183+
analysis.complexity_score =
184+
compute_complexity_score(node_count, leaf_variance, max_path);
185+
return analysis;
186+
}
187+
188+
analysis.total_sum = total_sum;
189+
analysis.node_count = node_count;
190+
analysis.max_path = max_path;
191+
analysis.leaf_variance = leaf_variance;
192+
analysis.complexity_score = compute_complexity_score(node_count, leaf_variance, max_path);
193+
return analysis;
194+
}
195+
196+
static long complex_fractal_benchmark(void) {
197+
g_pool_used = 0;
198+
FractalNode *tree = build_fractal(0, 42);
199+
200+
TreeAnalysis analysis = analyze_fractal_tree(tree, 2);
201+
202+
int memo[FIB_N + 1];
203+
for (int i = 0; i <= FIB_N; i++) {
204+
memo[i] = -1;
205+
}
206+
long fib_result = fibonacci_memo(FIB_N, memo);
207+
208+
long tree_hash = (long)compute_tree_hash(tree);
209+
long tree_metric = analysis.total_sum + (long)analysis.node_count * 10 + analysis.max_path +
210+
analysis.leaf_variance + analysis.complexity_score;
211+
212+
return (tree_metric + fib_result + tree_hash) % 1000000;
213+
}
214+
215+
// Deepest frame: this is where instrumentation is turned on, with
216+
// main -> run_benchmark -> warmup -> run_measured already live on the native
217+
// stack but the shadow stack empty. The seeder reconstructs that chain.
218+
static long run_measured(void) {
219+
CALLGRIND_START_INSTRUMENTATION;
220+
CALLGRIND_ZERO_STATS;
221+
222+
long result = complex_fractal_benchmark();
223+
224+
CALLGRIND_STOP_INSTRUMENTATION;
225+
return result;
226+
}
227+
228+
// Two unmeasured warmup iterations (instrumentation still off) before the
229+
// measured run, like a real benchmark harness.
230+
static long warmup(void) {
231+
volatile long acc = 0;
232+
for (int i = 0; i < 2; i++) {
233+
acc += complex_fractal_benchmark();
234+
}
235+
(void)acc;
236+
return run_measured();
237+
}
238+
239+
static long run_benchmark(void) {
240+
return warmup();
241+
}
242+
243+
int main(void) {
244+
volatile long result = run_benchmark();
245+
(void)result;
246+
return 0;
247+
}

callgrind-utils/tests/snapshot.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ fn run_callgrind(bin: &Path) -> String {
8585
#[case("chain")]
8686
#[case("diamond")]
8787
#[case("mutual")]
88+
#[case("fractal")]
8889
fn fixture_canonical_json(#[case] stem: &str) {
8990
let bin = compile_fixture(stem);
9091
let raw = run_callgrind(&bin);
@@ -127,6 +128,7 @@ fn run_callgrind_full(bin: &Path) -> String {
127128
#[case("chain")]
128129
#[case("diamond")]
129130
#[case("mutual")]
131+
#[case("fractal")]
130132
fn fixture_full_trace(#[case] stem: &str) {
131133
let bin = compile_fixture(stem);
132134
let raw = run_callgrind_full(&bin);

0 commit comments

Comments
 (0)