Coverage for src/hieraconf/cache_warming.py: 32%

59 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-02 21:44 +0000

1""" 

2Generic cache warming for configuration analysis. 

3 

4This module provides a generic function to pre-warm analysis caches for any 

5configuration hierarchy, eliminating first-load penalties in UI forms. 

6""" 

7 

8import dataclasses 

9import logging 

10from typing import Type, Set, Optional, get_args, get_origin, Callable 

11 

12# Optional introspection - install openhcs for full functionality 

13try: 

14 from openhcs.introspection.signature_analyzer import SignatureAnalyzer 

15 from openhcs.introspection.unified_parameter_analyzer import UnifiedParameterAnalyzer 

16 from openhcs.ui.shared.parameter_form_service import ParameterFormService 

17except ImportError: 

18 SignatureAnalyzer = None 

19 UnifiedParameterAnalyzer = None 

20 ParameterFormService = None 

21 

22logger = logging.getLogger(__name__) 

23 

24 

25def _extract_all_dataclass_types(base_type: Type, visited: Optional[Set[Type]] = None) -> Set[Type]: 

26 """ 

27 Recursively extract all dataclass types from a configuration hierarchy. 

28  

29 Uses type introspection to discover all nested dataclass fields automatically. 

30 This is fully generic and works for any dataclass hierarchy. 

31  

32 Args: 

33 base_type: Root dataclass type to analyze 

34 visited: Set of already-visited types (for cycle detection) 

35  

36 Returns: 

37 Set of all dataclass types found in the hierarchy 

38 """ 

39 if visited is None: 

40 visited = set() 

41 

42 # Avoid infinite recursion on circular references 

43 if base_type in visited: 

44 return visited 

45 

46 # Only process dataclasses 

47 if not dataclasses.is_dataclass(base_type): 

48 return visited 

49 

50 visited.add(base_type) 

51 

52 # Introspect all fields to find nested dataclasses 

53 for field in dataclasses.fields(base_type): 

54 field_type = field.type 

55 

56 # Handle Optional[T] -> extract T 

57 origin = get_origin(field_type) 

58 if origin is not None: 

59 # For Union types (including Optional), check all args 

60 args = get_args(field_type) 

61 for arg in args: 

62 if arg is type(None): 

63 continue 

64 if dataclasses.is_dataclass(arg): 

65 _extract_all_dataclass_types(arg, visited) 

66 elif dataclasses.is_dataclass(field_type): 

67 # Direct dataclass field 

68 _extract_all_dataclass_types(field_type, visited) 

69 

70 return visited 

71 

72 

73def prewarm_callable_analysis_cache(*callables: Callable) -> None: 

74 """ 

75 Pre-warm analysis caches for callable signatures (functions, methods, constructors). 

76 

77 This is useful for warming caches for step editors, function pattern editors, etc. 

78 that analyze function signatures rather than dataclass hierarchies. 

79 

80 Args: 

81 *callables: One or more callables to analyze (functions, methods, __init__, etc.) 

82 

83 Example: 

84 >>> from openhcs.core.steps.abstract import AbstractStep 

85 >>> from hieraconf import prewarm_callable_analysis_cache 

86 >>> prewarm_callable_analysis_cache(AbstractStep.__init__) 

87 """ 

88 for callable_obj in callables: 

89 SignatureAnalyzer.analyze(callable_obj) 

90 UnifiedParameterAnalyzer.analyze(callable_obj) 

91 

92 logger.debug(f"Pre-warmed analysis cache for {len(callables)} callables") 

93 

94 

95def prewarm_config_analysis_cache(base_config_type: Type) -> None: 

96 """ 

97 Pre-warm analysis caches for all config types in a hierarchy. 

98 

99 This is a fully generic function that: 

100 1. Uses type introspection to discover all dataclass types in the hierarchy 

101 2. Pre-analyzes each type to populate analysis caches 

102 3. Also analyzes the lazy version of the base config (if it exists) 

103 4. Eliminates 1000ms+ first-load penalty when opening config windows 

104 

105 After this runs, first load is as fast as second load (~170ms instead of ~1000ms). 

106 

107 Args: 

108 base_config_type: Root configuration type (e.g., GlobalPipelineConfig) 

109 

110 Example: 

111 >>> from myapp.config import GlobalConfig 

112 >>> from hieraconf import prewarm_config_analysis_cache 

113 >>> prewarm_config_analysis_cache(GlobalConfig) 

114 """ 

115 # Discover all dataclass types in the hierarchy using introspection 

116 config_types = _extract_all_dataclass_types(base_config_type) 

117 

118 # Also add the lazy version of the base config if it exists 

119 # (e.g., GlobalPipelineConfig -> PipelineConfig) 

120 lazy_name = base_config_type.__name__.replace("Global", "") 

121 if lazy_name != base_config_type.__name__: 

122 import importlib 

123 module = importlib.import_module(base_config_type.__module__) 

124 lazy_type = getattr(module, lazy_name, None) 

125 

126 if lazy_type is not None and dataclasses.is_dataclass(lazy_type): 

127 config_types.add(lazy_type) 

128 

129 # Create a single service instance to warm the class-level cache 

130 service = ParameterFormService() 

131 

132 # Pre-analyze all config types to populate caches 

133 for config_type in config_types: 

134 # Warm SignatureAnalyzer cache (dataclass field analysis) 

135 SignatureAnalyzer._analyze_dataclass(config_type) 

136 

137 # Warm UnifiedParameterAnalyzer cache (parameter info with descriptions) 

138 param_info = UnifiedParameterAnalyzer.analyze(config_type) 

139 

140 # Warm ParameterFormService cache (form structure analysis) 

141 # This is the expensive part that builds the recursive FormStructure 

142 if dataclasses.is_dataclass(config_type): 

143 # Extract parameters from the dataclass 

144 params = {} 

145 param_types = {} 

146 for field in dataclasses.fields(config_type): 

147 params[field.name] = None # Dummy value 

148 param_types[field.name] = field.type 

149 

150 # Analyze to warm the cache 

151 service.analyze_parameters( 

152 params, param_types, 

153 field_id='cache_warming', 

154 parameter_info=param_info, 

155 parent_dataclass_type=config_type 

156 ) 

157 

158 logger.debug(f"Pre-warmed analysis cache for {len(config_types)} config types") 

159