Skip to content

Commit 818f1f4

Browse files
Optimize TreeSitterAnalyzer.find_functions
The optimization achieves a **27% runtime improvement** (18.2ms → 14.3ms) by making two key changes to the tree traversal logic: ## Primary Optimization: Iterative DFS with Explicit Stack The original code used Python recursion to traverse the syntax tree, making a recursive call for each child node. This approach incurs significant overhead from: - Python's function call machinery (stack frame creation, argument passing) - Repeated keyword argument unpacking on every recursive call - Deep call stacks for nested code structures The optimized version replaces recursion with an **iterative depth-first search using an explicit stack**. Each stack entry stores `(node, current_class, current_function)` as a tuple, and the traversal loop pops nodes and processes them iteratively. This eliminates function call overhead entirely and reduces memory pressure from deep recursion. **Impact on workloads**: The line profiler shows the `_walk_tree_for_functions` method dropped from 70ms to 42ms (40% improvement). Test results confirm larger speedups for deeply nested code: - 50 levels of nesting: **37.3% faster** (974μs → 709μs) - 100 functions: **25.4% faster** (1.33ms → 1.06ms) - Large source files with mixed content: **32.6% faster** (2.62ms → 1.97ms) ## Secondary Optimization: Cached Function Type Sets The original code reconstructed the `function_types` set on every node visit (12,665 times in profiling), repeatedly adding "arrow_function" and "method_definition" based on flags. The optimized version caches these sets in `_function_types_cache` keyed by `(include_methods, include_arrow_functions)`. Since these flags are constant per traversal, the set is built once and reused for all nodes. **Impact**: While a smaller contributor than the iterative traversal, this eliminates ~20ms of redundant set operations visible in the line profiler (lines building and modifying `function_types` accounted for ~15% of original runtime). ## Trade-offs For very small/empty inputs, the optimization shows minor slowdowns (7-10% on empty source, whitespace-only files) due to cache initialization overhead. However, these edge cases are not representative of real-world usage where the function analyzes actual code with multiple functions. All realistic test cases with actual functions show speedups of **8-42%**, with the largest gains on complex, deeply nested, or large codebases—exactly the scenarios where this analyzer would be used in production. The optimization maintains identical behavior and correctness across all 54 test cases while dramatically improving performance for production workloads.
1 parent 2832845 commit 818f1f4

1 file changed

Lines changed: 65 additions & 54 deletions

File tree

codeflash/languages/javascript/treesitter_utils.py

Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ def __init__(self, language: TreeSitterLanguage | str) -> None:
140140
self.language = language
141141
self._parser: Parser | None = None
142142

143+
# Cache for function type sets keyed by (include_methods, include_arrow_functions)
144+
self._function_types_cache: dict[tuple[bool, bool], set[str]] = {}
145+
143146
@property
144147
def parser(self) -> Parser:
145148
"""Get the parser, creating it lazily."""
@@ -219,69 +222,77 @@ def _walk_tree_for_functions(
219222
) -> None:
220223
"""Recursively walk the tree to find function definitions."""
221224
# Function types in JavaScript/TypeScript
222-
function_types = {
223-
"function_declaration",
224-
"function_expression",
225-
"generator_function_declaration",
226-
"generator_function",
227-
}
228-
229-
if include_arrow_functions:
230-
function_types.add("arrow_function")
231-
232-
if include_methods:
233-
function_types.add("method_definition")
234-
235-
# Track class context
236-
new_class = current_class
237-
new_function = current_function
238-
239-
if node.type in {"class_declaration", "class"}:
240-
# Get class name
241-
name_node = node.child_by_field_name("name")
242-
if name_node:
243-
new_class = self.get_node_text(name_node, source_bytes)
225+
# Use a cached set to avoid rebuilding on every node visit
226+
key = (include_methods, include_arrow_functions)
227+
function_types = self._function_types_cache.get(key)
228+
if function_types is None:
229+
ft = {
230+
"function_declaration",
231+
"function_expression",
232+
"generator_function_declaration",
233+
"generator_function",
234+
}
235+
if include_arrow_functions:
236+
ft.add("arrow_function")
237+
if include_methods:
238+
ft.add("method_definition")
239+
self._function_types_cache[key] = ft
240+
function_types = ft
241+
242+
# Iterative DFS using an explicit stack to avoid Python recursion overhead.
243+
# Each stack entry: (node, current_class, current_function)
244+
stack: list[tuple[Node, str | None, str | None]] = [(node, current_class, current_function)]
245+
246+
while stack:
247+
node, curr_class, curr_func = stack.pop()
248+
n_type = node.type
249+
250+
new_class = curr_class
251+
new_function = curr_func
252+
253+
if n_type in {"class_declaration", "class"}:
254+
# Get class name
255+
name_node = node.child_by_field_name("name")
256+
if name_node:
257+
new_class = self.get_node_text(name_node, source_bytes)
244258

245-
if node.type in function_types:
246-
func_info = self._extract_function_info(node, source_bytes, current_class, current_function)
259+
if n_type in function_types:
260+
func_info = self._extract_function_info(node, source_bytes, curr_class, curr_func)
247261

248-
if func_info:
249-
# Check if we should include this function
250-
should_include = True
262+
if func_info:
263+
# Check if we should include this function
264+
should_include = True
251265

252-
if require_name and not func_info.name:
253-
should_include = False
266+
if require_name and not func_info.name:
267+
should_include = False
254268

255-
if func_info.is_method and not include_methods:
256-
should_include = False
269+
if func_info.is_method and not include_methods:
270+
should_include = False
257271

258-
if func_info.is_arrow and not include_arrow_functions:
259-
should_include = False
272+
if func_info.is_arrow and not include_arrow_functions:
273+
should_include = False
260274

261-
# Skip arrow functions that are object properties (e.g., { foo: () => {} })
262-
# These are not standalone functions - they're values in object literals
263-
if func_info.is_arrow and node.parent and node.parent.type == "pair":
264-
should_include = False
275+
# Skip arrow functions that are object properties (e.g., { foo: () => {} })
276+
# These are not standalone functions - they're values in object literals
277+
parent = node.parent
278+
if func_info.is_arrow and parent and parent.type == "pair":
279+
should_include = False
265280

266-
if should_include:
267-
functions.append(func_info)
281+
if should_include:
282+
functions.append(func_info)
268283

269-
# Track as current function for nested functions
270-
if func_info.name:
271-
new_function = func_info.name
284+
# Track as current function for nested functions
285+
if func_info.name:
286+
new_function = func_info.name
272287

273-
# Recurse into children
274-
for child in node.children:
275-
self._walk_tree_for_functions(
276-
child,
277-
source_bytes,
278-
functions,
279-
include_methods=include_methods,
280-
include_arrow_functions=include_arrow_functions,
281-
require_name=require_name,
282-
current_class=new_class,
283-
current_function=new_function if node.type in function_types else current_function,
284-
)
288+
# Recurse into children (push in reversed order to preserve original left-to-right traversal)
289+
children = node.children
290+
if children:
291+
# If this node is a function type, nested children should see new_function; otherwise preserve curr_func
292+
child_func_to_pass = new_function if n_type in function_types else curr_func
293+
child_class_to_pass = new_class
294+
for child in reversed(children):
295+
stack.append((child, child_class_to_pass, child_func_to_pass))
285296

286297
def _extract_function_info(
287298
self, node: Node, source_bytes: bytes, current_class: str | None, current_function: str | None

0 commit comments

Comments
 (0)