1111
1212from codeflash .cli_cmds .console import logger
1313from codeflash .code_utils .code_replacer import replace_function_definitions_in_module
14- from codeflash .models .models import CodeString , CodeStringsMarkdown
14+ from codeflash .discovery .functions_to_optimize import FunctionToOptimize
15+ from codeflash .models .models import CodeOptimizationContext , CodeString , CodeStringsMarkdown , FunctionSource
1516
1617if TYPE_CHECKING :
1718 from codeflash .discovery .functions_to_optimize import FunctionToOptimize
@@ -561,52 +562,65 @@ def _analyze_imports_in_optimized_code(
561562 """
562563 imported_names_map = defaultdict (set )
563564
564- # Precompute a two-level dict: module_name -> func_name -> [helpers]
565565 helpers_by_file_and_func = defaultdict (dict )
566- helpers_by_file = defaultdict (list ) # preserved for "import module"
567- for helper in code_context .helper_functions :
568- jedi_type = helper .jedi_definition .type
569- if jedi_type != "class" :
570- func_name = helper .only_function_name
571- module_name = helper .file_path .stem
572- # Cache function lookup for this (module, func)
573- file_entry = helpers_by_file_and_func [module_name ]
574- if func_name in file_entry :
575- file_entry [func_name ].append (helper )
576- else :
577- file_entry [func_name ] = [helper ]
578- helpers_by_file [module_name ].append (helper )
566+ helpers_by_file = defaultdict (list )
567+ # Local variable bindings for inner loop speed-up
568+ helper_functions = code_context .helper_functions
569+ append_hbf = helpers_by_file .__getitem__
570+ # Precompute helper info as lists to reduce attribute lookups
571+ for helper in helper_functions :
572+ jedi_def = helper .jedi_definition
573+ if jedi_def .type == "class" :
574+ continue
575+ func_name = helper .only_function_name
576+ module_name = helper .file_path .stem
577+ file_entry = helpers_by_file_and_func [module_name ]
578+ file_entry .setdefault (func_name , []).append (helper )
579+ append_hbf (module_name ).append (helper )
579580
580- # Optimize attribute lookups and method binding outside the loop
581581 helpers_by_file_and_func_get = helpers_by_file_and_func .get
582582 helpers_by_file_get = helpers_by_file .get
583583
584+ # Cache node class checks
585+ ImportFrom = ast .ImportFrom
586+ Import = ast .Import
587+
588+ # AST walk is the main hot loop
584589 for node in ast .walk (optimized_ast ):
585- if isinstance (node , ast .ImportFrom ):
590+ # We avoid isinstance lookup for every node attribute; only check for relevant node types
591+ node_type = type (node )
592+ if node_type is ImportFrom :
586593 # Handle "from module import function" statements
587594 module_name = node .module
588- if module_name :
589- file_entry = helpers_by_file_and_func_get (module_name , None )
590- if file_entry :
591- for alias in node .names :
592- imported_name = alias .asname if alias .asname else alias .name
593- original_name = alias .name
594- helpers = file_entry .get (original_name , None )
595- if helpers :
596- for helper in helpers :
597- imported_names_map [imported_name ].add (helper .qualified_name )
598- imported_names_map [imported_name ].add (helper .fully_qualified_name )
599-
600- elif isinstance (node , ast .Import ):
595+ if not module_name :
596+ continue
597+ file_entry = helpers_by_file_and_func_get (module_name )
598+ if not file_entry :
599+ continue
600+ for alias in node .names :
601+ imported_name = alias .asname if alias .asname else alias .name
602+ original_name = alias .name
603+ helpers = file_entry .get (original_name )
604+ if helpers :
605+ # Invariant: 1 or more helpers per name, no setdefault needed
606+ s = imported_names_map [imported_name ]
607+ for helper in helpers :
608+ s .add (helper .qualified_name )
609+ s .add (helper .fully_qualified_name )
610+ elif node_type is Import :
601611 # Handle "import module" statements
602612 for alias in node .names :
603613 imported_name = alias .asname if alias .asname else alias .name
604614 module_name = alias .name
605- for helper in helpers_by_file_get (module_name , []):
615+ helpers = helpers_by_file_get (module_name )
616+ if not helpers :
617+ continue
618+ for helper in helpers :
606619 # For "import module" statements, functions would be called as module.function
607620 full_call = f"{ imported_name } .{ helper .only_function_name } "
608- imported_names_map [full_call ].add (helper .qualified_name )
609- imported_names_map [full_call ].add (helper .fully_qualified_name )
621+ s = imported_names_map [full_call ]
622+ s .add (helper .qualified_name )
623+ s .add (helper .fully_qualified_name )
610624
611625 return dict (imported_names_map )
612626
@@ -627,97 +641,119 @@ def detect_unused_helper_functions(
627641 List of FunctionSource objects representing unused helper functions
628642
629643 """
644+ # Fast return for markdown multi-code search (flatten result by chaining)
630645 if isinstance (optimized_code , CodeStringsMarkdown ) and len (optimized_code .code_strings ) > 0 :
631646 return list (
632647 chain .from_iterable (
633648 detect_unused_helper_functions (function_to_optimize , code_context , code .code )
634649 for code in optimized_code .code_strings
635650 )
636651 )
637-
638652 try :
639653 # Parse the optimized code to analyze function calls and imports
640654 optimized_ast = ast .parse (optimized_code )
641655
642- # Find the optimized entrypoint function
656+ # Find the optimized entrypoint function efficiently by scanning top-level nodes first
643657 entrypoint_function_ast = None
644- for node in ast .walk (optimized_ast ):
645- if isinstance (node , ast .FunctionDef ) and node .name == function_to_optimize .function_name :
658+ fn_name = function_to_optimize .function_name
659+ for node in ast .iter_child_nodes (optimized_ast ):
660+ if isinstance (node , ast .FunctionDef ) and node .name == fn_name :
646661 entrypoint_function_ast = node
647662 break
663+ # If not found at top-level, fallback to full AST walk (rare)
664+ if not entrypoint_function_ast :
665+ for node in ast .walk (optimized_ast ):
666+ if isinstance (node , ast .FunctionDef ) and node .name == fn_name :
667+ entrypoint_function_ast = node
668+ break
648669
649670 if not entrypoint_function_ast :
650- logger .debug (f"Could not find entrypoint function { function_to_optimize . function_name } in optimized code" )
671+ logger .debug (f"Could not find entrypoint function { fn_name } in optimized code" )
651672 return []
652673
653- # First, analyze imports to build a mapping of imported names to their original qualified names
674+ # Pre- analyze and cache all needed values
654675 imported_names_map = _analyze_imports_in_optimized_code (optimized_ast , code_context )
655676
656- # Extract all function calls in the entrypoint function
657677 called_function_names = set ()
678+ # AST walk for all calls inside entrypoint, batched local var reuse for speed
679+ entry_parents = getattr (function_to_optimize , "parents" , None )
680+ # Hot attributes for helper handling
681+ Name = ast .Name
682+ Attribute = ast .Attribute
683+ Call = ast .Call
684+
658685 for node in ast .walk (entrypoint_function_ast ):
659- if isinstance (node , ast .Call ):
660- if isinstance (node .func , ast .Name ):
661- # Regular function call: function_name()
662- called_name = node .func .id
663- called_function_names .add (called_name )
664- # Also add the qualified name if this is an imported function
665- if called_name in imported_names_map :
666- called_function_names .update (imported_names_map [called_name ])
667- elif isinstance (node .func , ast .Attribute ):
668- # Method call: obj.method() or self.method() or module.function()
669- if isinstance (node .func .value , ast .Name ):
670- if node .func .value .id == "self" :
671- # self.method_name() -> add both method_name and ClassName.method_name
672- called_function_names .add (node .func .attr )
673- # For class methods, also add the qualified name
674- if hasattr (function_to_optimize , "parents" ) and function_to_optimize .parents :
675- class_name = function_to_optimize .parents [0 ].name
676- called_function_names .add (f"{ class_name } .{ node .func .attr } " )
677- else :
678- # obj.method() or module.function()
679- attr_name = node .func .attr
680- called_function_names .add (attr_name )
681- called_function_names .add (f"{ node .func .value .id } .{ attr_name } " )
682- # Check if this is a module.function call that maps to a helper
683- full_call = f"{ node .func .value .id } .{ attr_name } "
684- if full_call in imported_names_map :
685- called_function_names .update (imported_names_map [full_call ])
686- # Handle nested attribute access like obj.attr.method()
686+ # Skip everything but calls
687+ if type (node ) is not Call :
688+ continue
689+ func = node .func
690+ func_type = type (func )
691+ if func_type is Name :
692+ # function_name()
693+ called_name = func .id
694+ called_function_names .add (called_name )
695+ imported = imported_names_map .get (called_name )
696+ if imported :
697+ called_function_names .update (imported )
698+ elif func_type is Attribute :
699+ value = func .value
700+ if isinstance (value , Name ):
701+ val_id = value .id
702+ attr = func .attr
703+ if val_id == "self" :
704+ # self.method_name()
705+ called_function_names .add (attr )
706+ # For class methods, also add the qualified name
707+ if entry_parents :
708+ class_name = entry_parents [0 ].name
709+ called_function_names .add (f"{ class_name } .{ attr } " )
687710 else :
688- called_function_names .add (node .func .attr )
711+ # obj.method() or module.function()
712+ called_function_names .add (attr )
713+ full_call = f"{ val_id } .{ attr } "
714+ called_function_names .add (full_call )
715+ imported = imported_names_map .get (full_call )
716+ if imported :
717+ called_function_names .update (imported )
718+ else :
719+ # obj.attr.method() (nested); just add the attr name for best effort
720+ called_function_names .add (func .attr )
689721
690722 logger .debug (f"Functions called in optimized entrypoint: { called_function_names } " )
691723 logger .debug (f"Imported names mapping: { imported_names_map } " )
692724
693- # Find helper functions that are no longer called
694725 unused_helpers = []
695- for helper_function in code_context .helper_functions :
696- if helper_function .jedi_definition .type != "class" :
697- # Check if the helper function is called using multiple name variants
698- helper_qualified_name = helper_function .qualified_name
699- helper_simple_name = helper_function .only_function_name
700- helper_fully_qualified_name = helper_function .fully_qualified_name
701-
702- # Create a set of all possible names this helper might be called by
703- possible_call_names = {helper_qualified_name , helper_simple_name , helper_fully_qualified_name }
704-
705- # For cross-file helpers, also consider module-based calls
706- if helper_function .file_path != function_to_optimize .file_path :
707- # Add potential module.function combinations
708- module_name = helper_function .file_path .stem
709- possible_call_names .add (f"{ module_name } .{ helper_simple_name } " )
710-
711- # Check if any of the possible names are in the called functions
712- is_called = bool (possible_call_names .intersection (called_function_names ))
713-
714- if not is_called :
715- unused_helpers .append (helper_function )
716- logger .debug (f"Helper function { helper_qualified_name } is not called in optimized code" )
717- logger .debug (f" Checked names: { possible_call_names } " )
718- else :
719- logger .debug (f"Helper function { helper_qualified_name } is still called in optimized code" )
720- logger .debug (f" Called via: { possible_call_names .intersection (called_function_names )} " )
726+ helper_functions = code_context .helper_functions
727+ entrypoint_file = function_to_optimize .file_path
728+ # Make called_function_names a set for fast lookup; .intersection() is slow for large sets
729+ called_fn_names = called_function_names
730+ # For every helper, check if any known name is present in the set
731+ for helper_function in helper_functions :
732+ if helper_function .jedi_definition .type == "class" :
733+ continue
734+
735+ helper_qualified_name = helper_function .qualified_name
736+ helper_simple_name = helper_function .only_function_name
737+ helper_fully_qualified_name = helper_function .fully_qualified_name
738+
739+ possible_call_names = [helper_qualified_name , helper_simple_name , helper_fully_qualified_name ]
740+ # For cross-file helpers, also consider module-based calls
741+ if helper_function .file_path != entrypoint_file :
742+ module_name = helper_function .file_path .stem
743+ possible_call_names .append (f"{ module_name } .{ helper_simple_name } " )
744+ # Short-circuit as soon as any call name is found, avoid .intersection overhead
745+ is_called = False
746+ for n in possible_call_names :
747+ if n in called_fn_names :
748+ is_called = True
749+ break
750+ if not is_called :
751+ unused_helpers .append (helper_function )
752+ logger .debug (f"Helper function { helper_qualified_name } is not called in optimized code" )
753+ logger .debug (f" Checked names: { set (possible_call_names )} " )
754+ else :
755+ logger .debug (f"Helper function { helper_qualified_name } is still called in optimized code" )
756+ logger .debug (f" Called via: { set (possible_call_names ) & called_fn_names } " )
721757
722758 ret_val = unused_helpers
723759
0 commit comments