|
| 1 | +# Python twin of `testdata/fractal.rs`: a self-contained copy of the CodSpeed |
| 2 | +# e2e Python benchmark (its `fractal.py` + `benchmark.py` merged), driven the |
| 3 | +# way CodSpeed drives a benchmark. |
| 4 | +# |
| 5 | +# Instrumentation is off at startup (run with --instr-atstart=no) and turned on |
| 6 | +# around the measured region via the `clgctl` shim, whose compiled path is |
| 7 | +# passed as argv[1]. The client requests fire several frames deep |
| 8 | +# (main -> run_benchmark -> warmup -> run_measured), mirroring the Rust twin, so |
| 9 | +# the seeder must reconstruct the native chain at the OFF->ON transition. |
| 10 | +# |
| 11 | +# Before starting, we skip the Python runtime objects (libpython + the python |
| 12 | +# executable) from Callgrind at runtime, exactly as pytest-codspeed's |
| 13 | +# instrument-hooks does in _callgrind_skip_python_runtime: the interpreter's own |
| 14 | +# C frames are folded into their callers so they don't obfuscate the graph. |
| 15 | +# Matching is by exact realpath, since Callgrind keys obj-skip on the mapped |
| 16 | +# object path. |
| 17 | + |
| 18 | +import ctypes |
| 19 | +import math |
| 20 | +import os |
| 21 | +import sys |
| 22 | +import sysconfig |
| 23 | +from typing import Dict, List |
| 24 | + |
| 25 | +clgctl = ctypes.CDLL(sys.argv[1]) |
| 26 | + |
| 27 | +# Benchmark workload parameters, matching the e2e `test_benchmark.py` / |
| 28 | +# `bench_fractal.rs` case: complex_fractal_benchmark(5, 3, 25). |
| 29 | +TREE_DEPTH = 5 |
| 30 | +BRANCH_FACTOR = 3 |
| 31 | +FIB_N = 25 |
| 32 | + |
| 33 | + |
| 34 | +def skip_python_runtime(): |
| 35 | + ldlibrary = sysconfig.get_config_var("LDLIBRARY") |
| 36 | + libdir = sysconfig.get_config_var("LIBDIR") |
| 37 | + libpython = next( |
| 38 | + ( |
| 39 | + p |
| 40 | + for p in ( |
| 41 | + os.path.join(libdir, ldlibrary) if ldlibrary and libdir else None, |
| 42 | + os.path.join(sys.prefix, "lib", ldlibrary) if ldlibrary else None, |
| 43 | + ) |
| 44 | + if p and os.path.exists(p) |
| 45 | + ), |
| 46 | + None, |
| 47 | + ) |
| 48 | + for path in (libpython, sys.executable): |
| 49 | + if path: |
| 50 | + clgctl.clg_add_obj_skip(os.path.realpath(path).encode()) |
| 51 | + |
| 52 | + |
| 53 | +class NodeMetadata: |
| 54 | + """Metadata for a fractal node.""" |
| 55 | + |
| 56 | + def __init__(self, depth: int, branch_factor: int): |
| 57 | + self.depth = depth |
| 58 | + self.branch_factor = branch_factor |
| 59 | + self.computed_hash = 0 |
| 60 | + |
| 61 | + |
| 62 | +class FractalNode: |
| 63 | + """A node in a fractal computation tree.""" |
| 64 | + |
| 65 | + def __init__(self, value: float, depth: int, branch_factor: int): |
| 66 | + self.value = value |
| 67 | + self.children: List[FractalNode] = [] |
| 68 | + self.metadata = NodeMetadata(depth, branch_factor) |
| 69 | + |
| 70 | + @classmethod |
| 71 | + def build_fractal( |
| 72 | + cls, depth: int, max_depth: int, branch_factor: int, seed: float |
| 73 | + ) -> "FractalNode": |
| 74 | + """Recursively build a fractal tree with branching patterns.""" |
| 75 | + node = cls(seed, depth, branch_factor) |
| 76 | + |
| 77 | + if depth < max_depth: |
| 78 | + for i in range(branch_factor): |
| 79 | + child_seed = cls._compute_child_value(seed, i, depth) |
| 80 | + child = cls.build_fractal(depth + 1, max_depth, branch_factor, child_seed) |
| 81 | + node.children.append(child) |
| 82 | + |
| 83 | + node.metadata.computed_hash = node.compute_tree_hash() |
| 84 | + return node |
| 85 | + |
| 86 | + @staticmethod |
| 87 | + def _compute_child_value(parent_value: float, child_index: int, depth: int) -> float: |
| 88 | + """Nested helper function to compute child values.""" |
| 89 | + base = parent_value * 0.618033988749 # Golden ratio conjugate |
| 90 | + offset = (child_index + 1) * (depth + 1) |
| 91 | + return abs(math.sin(base + offset)) * 100.0 |
| 92 | + |
| 93 | + def compute_tree_hash(self) -> int: |
| 94 | + """Recursively compute a hash of the entire tree structure.""" |
| 95 | + hash_value = int(self.value * 1000) |
| 96 | + hash_value = (hash_value * 31 + self.metadata.depth) & 0xFFFFFFFFFFFFFFFF |
| 97 | + for child in self.children: |
| 98 | + child_hash = child.compute_tree_hash() |
| 99 | + hash_value = (hash_value * 31 + child_hash) & 0xFFFFFFFFFFFFFFFF |
| 100 | + return hash_value |
| 101 | + |
| 102 | + def recursive_sum(self) -> float: |
| 103 | + """Recursively compute the sum of all values in the tree.""" |
| 104 | + children_sum = sum(child.recursive_sum() for child in self.children) |
| 105 | + return self.value + children_sum |
| 106 | + |
| 107 | + def max_path_sum(self) -> float: |
| 108 | + """Recursively find the maximum path sum from root to any leaf.""" |
| 109 | + if not self.children: |
| 110 | + return self.value |
| 111 | + max_child_path = max(child.max_path_sum() for child in self.children) |
| 112 | + return self.value + max_child_path |
| 113 | + |
| 114 | + def count_nodes(self) -> int: |
| 115 | + """Recursively count all nodes in the tree.""" |
| 116 | + return 1 + sum(child.count_nodes() for child in self.children) |
| 117 | + |
| 118 | + def collect_leaves(self, leaves: List[float]) -> None: |
| 119 | + """Recursively collect all leaf values.""" |
| 120 | + if not self.children: |
| 121 | + leaves.append(self.value) |
| 122 | + else: |
| 123 | + for child in self.children: |
| 124 | + child.collect_leaves(leaves) |
| 125 | + |
| 126 | + |
| 127 | +class TreeAnalysis: |
| 128 | + """Results of fractal tree analysis.""" |
| 129 | + |
| 130 | + def __init__( |
| 131 | + self, |
| 132 | + total_sum: float, |
| 133 | + node_count: int, |
| 134 | + max_path: float, |
| 135 | + leaf_variance: float, |
| 136 | + complexity_score: float, |
| 137 | + ): |
| 138 | + self.total_sum = total_sum |
| 139 | + self.node_count = node_count |
| 140 | + self.max_path = max_path |
| 141 | + self.leaf_variance = leaf_variance |
| 142 | + self.complexity_score = complexity_score |
| 143 | + |
| 144 | + |
| 145 | +def fibonacci_memo(n: int, memo: Dict[int, int]) -> int: |
| 146 | + """Compute Fibonacci with memoization (recursive with nested dict operations).""" |
| 147 | + if n <= 1: |
| 148 | + return n |
| 149 | + if n in memo: |
| 150 | + return memo[n] |
| 151 | + result = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo) |
| 152 | + memo[n] = result |
| 153 | + return result |
| 154 | + |
| 155 | + |
| 156 | +def compute_variance(values: List[float]) -> float: |
| 157 | + """Nested helper to compute variance.""" |
| 158 | + if not values: |
| 159 | + return 0.0 |
| 160 | + mean = sum(values) / len(values) |
| 161 | + variance = sum((v - mean) ** 2 for v in values) / len(values) |
| 162 | + return variance |
| 163 | + |
| 164 | + |
| 165 | +def recursive_path_score(value: float, depth: int) -> float: |
| 166 | + """Recursive helper for path scoring.""" |
| 167 | + if depth == 0 or value < 1.0: |
| 168 | + return value |
| 169 | + reduced = value * 0.8 |
| 170 | + return 1.0 + recursive_path_score(reduced, depth - 1) * 0.5 |
| 171 | + |
| 172 | + |
| 173 | +def compute_complexity_score(node_count: int, variance: float, max_path: float) -> float: |
| 174 | + """Nested helper to compute complexity score (with recursive internal call).""" |
| 175 | + base_score = math.log(node_count) * math.sqrt(variance) |
| 176 | + path_factor = recursive_path_score(max_path, 5) |
| 177 | + return base_score * path_factor |
| 178 | + |
| 179 | + |
| 180 | +def analyze_fractal_tree(tree: FractalNode, analysis_depth: int) -> TreeAnalysis: |
| 181 | + """Nested function that analyzes the fractal tree with multiple passes.""" |
| 182 | + total_sum = tree.recursive_sum() |
| 183 | + node_count = tree.count_nodes() |
| 184 | + max_path = tree.max_path_sum() |
| 185 | + |
| 186 | + leaves: List[float] = [] |
| 187 | + tree.collect_leaves(leaves) |
| 188 | + leaf_variance = compute_variance(leaves) |
| 189 | + |
| 190 | + if analysis_depth > 0: |
| 191 | + nested_analysis = analyze_fractal_tree(tree, analysis_depth - 1) |
| 192 | + return TreeAnalysis( |
| 193 | + total_sum=total_sum + nested_analysis.total_sum * 0.1, |
| 194 | + node_count=node_count, |
| 195 | + max_path=max(max_path, nested_analysis.max_path), |
| 196 | + leaf_variance=(leaf_variance + nested_analysis.leaf_variance) / 2.0, |
| 197 | + complexity_score=compute_complexity_score(node_count, leaf_variance, max_path), |
| 198 | + ) |
| 199 | + return TreeAnalysis( |
| 200 | + total_sum=total_sum, |
| 201 | + node_count=node_count, |
| 202 | + max_path=max_path, |
| 203 | + leaf_variance=leaf_variance, |
| 204 | + complexity_score=compute_complexity_score(node_count, leaf_variance, max_path), |
| 205 | + ) |
| 206 | + |
| 207 | + |
| 208 | +def complex_fractal_benchmark(tree_depth: int, branch_factor: int, fib_n: int) -> float: |
| 209 | + """Main benchmark: complex fractal tree computation.""" |
| 210 | + tree = FractalNode.build_fractal(0, tree_depth, branch_factor, 42.0) |
| 211 | + analysis = analyze_fractal_tree(tree, 2) |
| 212 | + |
| 213 | + memo: Dict[int, int] = {} |
| 214 | + fib_result = float(fibonacci_memo(fib_n, memo)) |
| 215 | + |
| 216 | + tree_hash = float(tree.compute_tree_hash()) |
| 217 | + tree_metric = ( |
| 218 | + analysis.total_sum |
| 219 | + + (analysis.node_count * 10.0) |
| 220 | + + analysis.max_path |
| 221 | + + analysis.leaf_variance |
| 222 | + ) |
| 223 | + return (tree_metric + fib_result + tree_hash) % 1_000_000.0 |
| 224 | + |
| 225 | + |
| 226 | +# Deepest frame: instrumentation is turned on here, with |
| 227 | +# main -> run_benchmark -> warmup -> run_measured already live on the native |
| 228 | +# stack but the shadow stack empty. The seeder reconstructs that chain. |
| 229 | +def run_measured() -> float: |
| 230 | + clgctl.clg_start() |
| 231 | + result = complex_fractal_benchmark(TREE_DEPTH, BRANCH_FACTOR, FIB_N) |
| 232 | + clgctl.clg_stop() |
| 233 | + return result |
| 234 | + |
| 235 | + |
| 236 | +# Two unmeasured warmup iterations (instrumentation still off) before the |
| 237 | +# measured run, like a real benchmark harness. |
| 238 | +def warmup() -> float: |
| 239 | + acc = 0.0 |
| 240 | + for _ in range(2): |
| 241 | + acc += complex_fractal_benchmark(TREE_DEPTH, BRANCH_FACTOR, FIB_N) |
| 242 | + return run_measured() |
| 243 | + |
| 244 | + |
| 245 | +def run_benchmark() -> float: |
| 246 | + return warmup() |
| 247 | + |
| 248 | + |
| 249 | +def main() -> None: |
| 250 | + skip_python_runtime() |
| 251 | + result = run_benchmark() |
| 252 | + assert 0 <= result < 1_000_000.0, result |
| 253 | + |
| 254 | + |
| 255 | +if __name__ == "__main__": |
| 256 | + main() |
0 commit comments