Coverage for src / tracekit / core / performance.py: 100%

82 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 23:04 +0000

1"""Performance timing and monitoring infrastructure. 

2 

3This module provides automatic performance timing for operations, 

4metric collection, and performance summary reporting. 

5 

6 

7Example: 

8 >>> from tracekit.core.performance import timed, get_performance_summary 

9 >>> @timed(threshold=0.1) 

10 ... def slow_operation(data): 

11 ... result = expensive_computation(data) 

12 ... return result 

13 >>> summary = get_performance_summary() 

14 

15References: 

16 - Python time.perf_counter() for high-resolution timing 

17 - Performance monitoring best practices 

18""" 

19 

20from __future__ import annotations 

21 

22import functools 

23import time 

24from collections import defaultdict 

25from collections.abc import Callable 

26from dataclasses import dataclass, field 

27from datetime import UTC, datetime 

28from typing import TYPE_CHECKING, Any, TypeVar 

29 

30import numpy as np 

31 

32if TYPE_CHECKING: 

33 from numpy.typing import NDArray 

34 

35 

36@dataclass 

37class PerformanceRecord: 

38 """Record of a timed operation. 

39 

40 Attributes: 

41 operation: Name of the operation (typically function name). 

42 duration: Duration in seconds. 

43 timestamp: ISO 8601 timestamp of when operation completed. 

44 metadata: Optional metadata about the operation. 

45 

46 References: 

47 LOG-006: Automatic Performance Timing 

48 """ 

49 

50 operation: str 

51 duration: float 

52 timestamp: str 

53 metadata: dict[str, Any] = field(default_factory=dict) 

54 

55 

56class PerformanceCollector: 

57 """Collects and aggregates performance timing data. 

58 

59 Thread-safe collector for recording and analyzing operation timings. 

60 Provides summary statistics per operation type. 

61 

62 Example: 

63 >>> collector = PerformanceCollector() 

64 >>> collector.record("fft_computation", 1.23, samples=1000000) 

65 >>> collector.record("fft_computation", 1.45, samples=2000000) 

66 >>> summary = collector.get_summary() 

67 >>> print(summary["fft_computation"]["mean"]) 

68 1.34 

69 

70 References: 

71 LOG-006: Automatic Performance Timing 

72 """ 

73 

74 def __init__(self) -> None: 

75 """Initialize the performance collector.""" 

76 self.records: list[PerformanceRecord] = [] 

77 

78 def record( 

79 self, 

80 operation: str, 

81 duration: float, 

82 **metadata: Any, 

83 ) -> None: 

84 """Record a performance measurement. 

85 

86 Args: 

87 operation: Name of the operation being timed. 

88 duration: Duration in seconds (use time.perf_counter()). 

89 **metadata: Additional metadata about the operation. 

90 

91 Example: 

92 >>> collector.record("load_trace", 0.5, file_size_mb=100) 

93 >>> collector.record("compute_fft", 2.3, samples=10000000) 

94 

95 References: 

96 LOG-006: Automatic Performance Timing 

97 """ 

98 self.records.append( 

99 PerformanceRecord( 

100 operation=operation, 

101 duration=duration, 

102 timestamp=datetime.now(UTC).isoformat(), 

103 metadata=metadata, 

104 ) 

105 ) 

106 

107 def get_summary(self) -> dict[str, dict[str, float]]: 

108 """Get summary statistics per operation. 

109 

110 Returns: 

111 Dictionary mapping operation names to statistics: 

112 - count: Number of times operation was performed 

113 - mean: Mean duration in seconds 

114 - std: Standard deviation of duration 

115 - min: Minimum duration 

116 - max: Maximum duration 

117 - total: Total time spent in operation 

118 

119 Example: 

120 >>> summary = collector.get_summary() 

121 >>> fft_stats = summary["fft_computation"] 

122 >>> print(f"FFT avg: {fft_stats['mean']:.3f}s") 

123 

124 References: 

125 LOG-006: Automatic Performance Timing 

126 """ 

127 by_op: dict[str, list[float]] = defaultdict(list) 

128 for record in self.records: 

129 by_op[record.operation].append(record.duration) 

130 

131 summary: dict[str, dict[str, float]] = {} 

132 for op, durations in by_op.items(): 

133 arr: NDArray[np.float64] = np.array(durations) 

134 summary[op] = { 

135 "count": float(len(arr)), 

136 "mean": float(np.mean(arr)), 

137 "std": float(np.std(arr)), 

138 "min": float(np.min(arr)), 

139 "max": float(np.max(arr)), 

140 "total": float(np.sum(arr)), 

141 } 

142 return summary 

143 

144 def clear(self) -> None: 

145 """Clear all performance records. 

146 

147 Useful for resetting measurements between test runs or 

148 analysis sessions. 

149 

150 Example: 

151 >>> collector.clear() 

152 """ 

153 self.records.clear() 

154 

155 def get_records( 

156 self, 

157 operation: str | None = None, 

158 since: datetime | None = None, 

159 ) -> list[PerformanceRecord]: 

160 """Get performance records with optional filtering. 

161 

162 Args: 

163 operation: Filter by operation name, or None for all. 

164 since: Filter records after this timestamp, or None for all. 

165 

166 Returns: 

167 List of matching performance records. 

168 

169 Example: 

170 >>> from datetime import datetime, timezone, timedelta 

171 >>> one_hour_ago = datetime.now(timezone.utc) - timedelta(hours=1) 

172 >>> recent = collector.get_records(since=one_hour_ago) 

173 """ 

174 records = self.records 

175 

176 if operation is not None: 

177 records = [r for r in records if r.operation == operation] 

178 

179 if since is not None: 

180 since_iso = since.isoformat() 

181 records = [r for r in records if r.timestamp >= since_iso] 

182 

183 return records 

184 

185 

186# Global performance collector 

187_global_collector = PerformanceCollector() 

188 

189 

190F = TypeVar("F", bound=Callable[..., Any]) 

191 

192 

193def timed( 

194 threshold: float | None = None, 

195 log_level: str = "DEBUG", 

196 collect: bool = True, 

197) -> Callable[[F], F]: 

198 """Decorator to time and log function execution. 

199 

200 Automatically times function execution using time.perf_counter() 

201 and optionally logs if duration exceeds threshold. 

202 

203 Args: 

204 threshold: Only log if duration exceeds this (seconds). None logs all. 

205 log_level: Log level for timing messages (DEBUG, INFO, WARNING, etc.). 

206 collect: Whether to collect timing in global PerformanceCollector. 

207 

208 Returns: 

209 Decorated function. 

210 

211 Example: 

212 >>> @timed(threshold=0.1) 

213 ... def slow_function(data): 

214 ... time.sleep(0.2) 

215 ... return process(data) 

216 

217 >>> @timed(log_level="INFO") 

218 ... def important_operation(): 

219 ... return compute_result() 

220 

221 References: 

222 LOG-006: Automatic Performance Timing 

223 """ 

224 

225 def decorator(func: F) -> F: 

226 @functools.wraps(func) 

227 def wrapper(*args: Any, **kwargs: Any) -> Any: 

228 start = time.perf_counter() 

229 try: 

230 return func(*args, **kwargs) 

231 finally: 

232 duration = time.perf_counter() - start 

233 

234 # Record in global collector 

235 if collect: 

236 _global_collector.record(func.__qualname__, duration) 

237 

238 # Log if threshold met 

239 if threshold is None or duration >= threshold: 

240 # Lazy import to avoid circular dependency 

241 from tracekit.core.logging import get_logger 

242 

243 logger = get_logger(func.__module__ or "tracekit") 

244 log_func = getattr(logger, log_level.lower()) 

245 log_func( 

246 f"Function {func.__qualname__} completed", 

247 extra={"duration_seconds": round(duration, 6)}, 

248 ) 

249 

250 return wrapper # type: ignore[return-value] 

251 

252 return decorator 

253 

254 

255def get_performance_summary() -> dict[str, dict[str, float]]: 

256 """Get summary of collected performance data from global collector. 

257 

258 Returns: 

259 Dictionary of performance statistics per operation. 

260 

261 Example: 

262 >>> summary = get_performance_summary() 

263 >>> for op, stats in summary.items(): 

264 ... print(f"{op}: {stats['mean']:.3f}s avg, {stats['count']:.0f} calls") 

265 

266 References: 

267 LOG-006: Automatic Performance Timing 

268 """ 

269 return _global_collector.get_summary() 

270 

271 

272def clear_performance_data() -> None: 

273 """Clear all collected performance data from global collector. 

274 

275 Example: 

276 >>> clear_performance_data() 

277 """ 

278 _global_collector.clear() 

279 

280 

281def get_performance_records( 

282 operation: str | None = None, 

283 since: datetime | None = None, 

284) -> list[PerformanceRecord]: 

285 """Get performance records from global collector. 

286 

287 Args: 

288 operation: Filter by operation name, or None for all. 

289 since: Filter records after this timestamp, or None for all. 

290 

291 Returns: 

292 List of matching performance records. 

293 

294 Example: 

295 >>> records = get_performance_records(operation="fft_computation") 

296 >>> for record in records: 

297 ... print(f"{record.timestamp}: {record.duration:.3f}s") 

298 """ 

299 return _global_collector.get_records(operation=operation, since=since) 

300 

301 

302class PerformanceContext: 

303 """Context manager for timing a block of code. 

304 

305 Example: 

306 >>> with PerformanceContext("data_loading") as ctx: 

307 ... data = load_large_file() 

308 >>> print(f"Loading took {ctx.duration:.3f}s") 

309 

310 References: 

311 LOG-006: Automatic Performance Timing 

312 """ 

313 

314 def __init__( 

315 self, 

316 operation: str, 

317 log_threshold: float | None = None, 

318 collect: bool = True, 

319 ): 

320 """Initialize performance context. 

321 

322 Args: 

323 operation: Name of the operation being timed. 

324 log_threshold: Log if duration exceeds threshold (seconds). 

325 collect: Whether to collect in global collector. 

326 """ 

327 self.operation = operation 

328 self.log_threshold = log_threshold 

329 self.collect = collect 

330 self.start_time: float = 0.0 

331 self.duration: float = 0.0 

332 

333 def __enter__(self) -> PerformanceContext: 

334 """Start timing.""" 

335 self.start_time = time.perf_counter() 

336 return self 

337 

338 def __exit__(self, *args: Any) -> None: 

339 """Stop timing and record.""" 

340 self.duration = time.perf_counter() - self.start_time 

341 

342 if self.collect: 

343 _global_collector.record(self.operation, self.duration) 

344 

345 if self.log_threshold is None or self.duration >= self.log_threshold: 

346 from tracekit.core.logging import get_logger 

347 

348 logger = get_logger("tracekit.performance") 

349 logger.debug( 

350 f"Operation {self.operation} completed", 

351 extra={"duration_seconds": round(self.duration, 6)}, 

352 )