Skip to content

Commit 7cd5cda

Browse files
Optimize _add_timing_instrumentation
This optimization achieves a **15% runtime improvement** (10.2ms → 8.81ms) by replacing recursive AST traversal with iterative stack-based traversal in two critical functions: `collect_test_methods` and `collect_target_calls`. ## Key Changes **1. Iterative AST Traversal (Primary Speedup)** - Replaced recursive tree walking with explicit stack-based iteration - In `collect_test_methods`: Changed from recursive calls to `while stack` loop with `stack.extend(reversed(current.children))` - In `collect_target_calls`: Similar transformation using explicit stack management - **Impact**: Line profiler shows `collect_test_methods` dropped from 24.2% to 3.8% of total runtime (81% reduction in that function) **2. Why This Works in Python** - Python function calls have significant overhead (frame creation, argument binding, scope setup) - Recursive traversal compounds this overhead across potentially deep AST trees - Iterative approach uses a simple list for the stack, avoiding repeated function call overhead - The `reversed()` call ensures children are processed in the same order as recursive traversal, preserving correctness **3. Performance Characteristics** Based on annotated tests: - **Large method bodies** (500+ lines): 23.8% faster - most benefit from reduced recursion overhead - **Many test methods** (100 methods): 9.2% faster - cumulative savings across many traversals - **Simple cases**: 2-5% faster - overhead reduction still measurable - **Empty/no-match cases**: Minor regression (8-9% slower) due to negligible baseline times (12-40μs) ## Impact on Workloads The function references show `_add_timing_instrumentation` is called from test instrumentation code. This optimization particularly benefits: - **Java projects with large test suites** containing many `@Test` methods - **Complex test methods** with deep AST structures and multiple method invocations - **Batch instrumentation operations** where the function is called repeatedly The iterative approach scales better than recursion as AST depth and method count increase, making it especially valuable for large Java codebases where instrumentation is applied across hundreds of test methods.
1 parent b42d97a commit 7cd5cda

1 file changed

Lines changed: 24 additions & 16 deletions

File tree

codeflash/languages/java/instrumentation.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -727,24 +727,31 @@ def has_test_annotation(method_node: Any) -> bool:
727727
return False
728728

729729
def collect_test_methods(node: Any, out: list[tuple[Any, Any]]) -> None:
730-
if node.type == "method_declaration" and has_test_annotation(node):
731-
body_node = node.child_by_field_name("body")
732-
if body_node is not None:
733-
out.append((node, body_node))
734-
for child in node.children:
735-
collect_test_methods(child, out)
730+
stack = [node]
731+
while stack:
732+
current = stack.pop()
733+
if current.type == "method_declaration" and has_test_annotation(current):
734+
body_node = current.child_by_field_name("body")
735+
if body_node is not None:
736+
out.append((current, body_node))
737+
continue
738+
if current.children:
739+
stack.extend(reversed(current.children))
736740

737741
def collect_target_calls(node: Any, wrapper_bytes: bytes, func: str, out: list[Any]) -> None:
738-
if node.type == "method_invocation":
739-
name_node = node.child_by_field_name("name")
740-
if name_node and analyzer.get_node_text(name_node, wrapper_bytes) == func:
741-
# Skip if inside lambda or complex expression
742-
if not _is_inside_lambda(node) and not _is_inside_complex_expression(node):
743-
out.append(node)
744-
else:
745-
logger.debug("Skipping instrumentation of %s inside lambda or complex expression", func)
746-
for child in node.children:
747-
collect_target_calls(child, wrapper_bytes, func, out)
742+
stack = [node]
743+
while stack:
744+
current = stack.pop()
745+
if current.type == "method_invocation":
746+
name_node = current.child_by_field_name("name")
747+
if name_node and analyzer.get_node_text(name_node, wrapper_bytes) == func:
748+
if not _is_inside_lambda(current) and not _is_inside_complex_expression(current):
749+
out.append(current)
750+
else:
751+
logger.debug("Skipping instrumentation of %s inside lambda or complex expression", func)
752+
if current.children:
753+
stack.extend(reversed(current.children))
754+
748755

749756
def reindent_block(text: str, target_indent: str) -> str:
750757
lines = text.splitlines()
@@ -842,6 +849,7 @@ def build_instrumented_body(body_text: str, next_wrapper_id: int, base_indent: s
842849
calls: list[Any] = []
843850
collect_target_calls(wrapped_body, wrapper_bytes, func_name, calls)
844851

852+
845853
indent = base_indent
846854
inner_indent = f"{indent} "
847855
inner_body_indent = f"{inner_indent} "

0 commit comments

Comments
 (0)