Coverage for openhcs/config_framework/cache_warming.py: 19.0%
54 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +0000
1"""
2Generic cache warming for configuration analysis.
4This module provides a generic function to pre-warm analysis caches for any
5configuration hierarchy, eliminating first-load penalties in UI forms.
6"""
8import dataclasses
9import logging
10from typing import Type, Set, Optional, get_args, get_origin, Callable
12from openhcs.introspection.signature_analyzer import SignatureAnalyzer
13from openhcs.introspection.unified_parameter_analyzer import UnifiedParameterAnalyzer
14from openhcs.ui.shared.parameter_form_service import ParameterFormService
16logger = logging.getLogger(__name__)
19def _extract_all_dataclass_types(base_type: Type, visited: Optional[Set[Type]] = None) -> Set[Type]:
20 """
21 Recursively extract all dataclass types from a configuration hierarchy.
23 Uses type introspection to discover all nested dataclass fields automatically.
24 This is fully generic and works for any dataclass hierarchy.
26 Args:
27 base_type: Root dataclass type to analyze
28 visited: Set of already-visited types (for cycle detection)
30 Returns:
31 Set of all dataclass types found in the hierarchy
32 """
33 if visited is None:
34 visited = set()
36 # Avoid infinite recursion on circular references
37 if base_type in visited:
38 return visited
40 # Only process dataclasses
41 if not dataclasses.is_dataclass(base_type):
42 return visited
44 visited.add(base_type)
46 # Introspect all fields to find nested dataclasses
47 for field in dataclasses.fields(base_type):
48 field_type = field.type
50 # Handle Optional[T] -> extract T
51 origin = get_origin(field_type)
52 if origin is not None:
53 # For Union types (including Optional), check all args
54 args = get_args(field_type)
55 for arg in args:
56 if arg is type(None):
57 continue
58 if dataclasses.is_dataclass(arg):
59 _extract_all_dataclass_types(arg, visited)
60 elif dataclasses.is_dataclass(field_type):
61 # Direct dataclass field
62 _extract_all_dataclass_types(field_type, visited)
64 return visited
67def prewarm_callable_analysis_cache(*callables: Callable) -> None:
68 """
69 Pre-warm analysis caches for callable signatures (functions, methods, constructors).
71 This is useful for warming caches for step editors, function pattern editors, etc.
72 that analyze function signatures rather than dataclass hierarchies.
74 Args:
75 *callables: One or more callables to analyze (functions, methods, __init__, etc.)
77 Example:
78 >>> from openhcs.core.steps.abstract import AbstractStep
79 >>> from openhcs.config_framework import prewarm_callable_analysis_cache
80 >>> prewarm_callable_analysis_cache(AbstractStep.__init__)
81 """
82 for callable_obj in callables:
83 SignatureAnalyzer.analyze(callable_obj)
84 UnifiedParameterAnalyzer.analyze(callable_obj)
86 logger.debug(f"Pre-warmed analysis cache for {len(callables)} callables")
89def prewarm_config_analysis_cache(base_config_type: Type) -> None:
90 """
91 Pre-warm analysis caches for all config types in a hierarchy.
93 This is a fully generic function that:
94 1. Uses type introspection to discover all dataclass types in the hierarchy
95 2. Pre-analyzes each type to populate analysis caches
96 3. Also analyzes the lazy version of the base config (if it exists)
97 4. Eliminates 1000ms+ first-load penalty when opening config windows
99 After this runs, first load is as fast as second load (~170ms instead of ~1000ms).
101 Args:
102 base_config_type: Root configuration type (e.g., GlobalPipelineConfig)
104 Example:
105 >>> from myapp.config import GlobalConfig
106 >>> from openhcs.config_framework import prewarm_config_analysis_cache
107 >>> prewarm_config_analysis_cache(GlobalConfig)
108 """
109 # Discover all dataclass types in the hierarchy using introspection
110 config_types = _extract_all_dataclass_types(base_config_type)
112 # Also add the lazy version of the base config if it exists
113 # (e.g., GlobalPipelineConfig -> PipelineConfig)
114 lazy_name = base_config_type.__name__.replace("Global", "")
115 if lazy_name != base_config_type.__name__:
116 import importlib
117 module = importlib.import_module(base_config_type.__module__)
118 lazy_type = getattr(module, lazy_name, None)
120 if lazy_type is not None and dataclasses.is_dataclass(lazy_type):
121 config_types.add(lazy_type)
123 # Create a single service instance to warm the class-level cache
124 service = ParameterFormService()
126 # Pre-analyze all config types to populate caches
127 for config_type in config_types:
128 # Warm SignatureAnalyzer cache (dataclass field analysis)
129 SignatureAnalyzer._analyze_dataclass(config_type)
131 # Warm UnifiedParameterAnalyzer cache (parameter info with descriptions)
132 param_info = UnifiedParameterAnalyzer.analyze(config_type)
134 # Warm ParameterFormService cache (form structure analysis)
135 # This is the expensive part that builds the recursive FormStructure
136 if dataclasses.is_dataclass(config_type):
137 # Extract parameters from the dataclass
138 params = {}
139 param_types = {}
140 for field in dataclasses.fields(config_type):
141 params[field.name] = None # Dummy value
142 param_types[field.name] = field.type
144 # Analyze to warm the cache
145 service.analyze_parameters(
146 params, param_types,
147 field_id='cache_warming',
148 parameter_info=param_info,
149 parent_dataclass_type=config_type
150 )
152 logger.debug(f"Pre-warmed analysis cache for {len(config_types)} config types")