11from __future__ import annotations
22
33import os
4+ import json
5+ import time
46import traceback
5- from typing import Any , List , Tuple , Optional
7+ from typing import Any , List , Tuple , Optional , Callable
68from urllib .parse import urlparse , unquote
79
810import cv2
1214from lf_toolkit .evaluation import Result , Params
1315
1416
17+ # Lazy loading
18+ from evaluation_function .lazy_load import LazyModule
19+
20+ torch = LazyModule ("torch" )
21+ ultralytics = LazyModule ("ultralytics" )
22+ _MODULE_IMPORT_T0 = time .perf_counter ()
23+
1524def _pget (params : Params , key : str , default : Any ) -> Any :
1625 try :
1726 return params .get (key , default ) # type: ignore
@@ -44,6 +53,14 @@ def _result(is_correct: bool, items: List[Tuple[str, str]]) -> Result:
4453 return Result (is_correct = is_correct , feedback_items = items )
4554
4655
56+ def _timeit (fn : Callable [[], Any ]) -> Tuple [Any , float ]:
57+ """Measure wall time of fn() using perf_counter()."""
58+ t0 = time .perf_counter ()
59+ out = fn ()
60+ dt = time .perf_counter () - t0
61+ return out , dt
62+
63+
4764def file_url_to_local_path (url : str ) -> str :
4865 parsed = urlparse (url )
4966 path = unquote (parsed .path )
@@ -78,8 +95,7 @@ def _load_bgr_image_from_url(url: str, timeout: int = 15) -> Tuple[Optional[np.n
7895 return None , str (e )
7996
8097
81- # ---------- NEW: ABCDE helpers (minimal & safe) ----------
82-
98+ # ABCDE helpers (minimal & safe)
8399def _candidate_model_paths () -> List [str ]:
84100 """
85101 Matches your repo layout:
@@ -98,6 +114,13 @@ def _candidate_model_paths() -> List[str]:
98114 ]
99115
100116
117+ def _add_common_timing (items : List [Tuple [str , str ]], t_handler0 : float ) -> None :
118+ """Append common timing fields."""
119+ now = time .perf_counter ()
120+ items .append (("t_module_import_to_handler_s" , f"{ now - _MODULE_IMPORT_T0 :.4f} " ))
121+ items .append (("t_handler_elapsed_s" , f"{ now - t_handler0 :.4f} " ))
122+
123+
101124def evaluation_function (response : Any , answer : Any , params : Params ) -> Result :
102125 """
103126 Smoke-test base + ABCDE diagnostics.
@@ -109,6 +132,7 @@ def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
109132 diag="torch" | "ultralytics" | "model_exists" | "load_model" | "infer_once"
110133 """
111134 items : List [Tuple [str , str ]] = []
135+ t_handler0 = time .perf_counter ()
112136
113137 try :
114138 fast_return : bool = bool (_pget (params , "fast_return" , True ))
@@ -117,7 +141,7 @@ def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
117141 debug : bool = bool (_pget (params , "debug" , True ))
118142 skip_load_check : bool = bool (_pget (params , "skip_load_check" , False ))
119143
120- # NEW: diag switch
144+ # diag switch
121145 diag : str = str (_pget (params , "diag" , "none" ) or "none" ).strip ().lower ()
122146
123147 items .append (("SMOKE" , "Hello / evaluation_function reached ✅" ))
@@ -128,32 +152,46 @@ def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
128152 items .append (("skip_load_check" , str (skip_load_check )))
129153 items .append (("debug" , str (debug )))
130154
131- # ---- A) torch import (no image needed, but we keep flow consistent) ----
155+ # Always include a cold-start proxy timing snapshot early
156+ _add_common_timing (items , t_handler0 )
157+
158+ # ----------------------------
159+ # A) torch lazy import timing
160+ # ----------------------------
132161 if diag == "torch" :
133162 try :
134- import torch # noqa
135- import torch
136- items .append (("A_torch" , "import OK" ))
163+ # Trigger actual import by touching a torch attribute
164+ _ , dt = _timeit (lambda : torch .__version__ )
165+ items .append (("A_torch" , "lazy import OK" ))
166+ items .append (("t_torch_import_s" , f"{ dt :.4f} " ))
137167 items .append (("torch_version" , str (torch .__version__ )))
168+ # Extra checks (may trigger internal queries)
138169 items .append (("cuda_available" , str (torch .cuda .is_available ())))
170+ _add_common_timing (items , t_handler0 )
139171 return _result (False , items )
140172 except Exception as e :
141173 items .append (("A_torch_FAIL" , f"{ type (e ).__name__ } : { e } " ))
142174 items .append (("TRACEBACK" , _escape_html (traceback .format_exc ()).replace ("\n " , "<br>" )))
175+ _add_common_timing (items , t_handler0 )
143176 return _result (False , items )
144177
145- # ---- B) ultralytics import ----
178+ # B) ultralytics lazy import timing
146179 if diag == "ultralytics" :
147180 try :
148- from ultralytics import YOLO # noqa: F401
149- items .append (("B_ultralytics" , "import OK" ))
181+ # Trigger ultralytics import by accessing YOLO symbol
182+ YOLO , dt = _timeit (lambda : ultralytics .YOLO )
183+ items .append (("B_ultralytics" , "lazy import OK" ))
184+ items .append (("t_ultralytics_import_s" , f"{ dt :.4f} " ))
185+ items .append (("YOLO_symbol" , str (YOLO )))
186+ _add_common_timing (items , t_handler0 )
150187 return _result (False , items )
151188 except Exception as e :
152189 items .append (("B_ultralytics_FAIL" , f"{ type (e ).__name__ } : { e } " ))
153190 items .append (("TRACEBACK" , _escape_html (traceback .format_exc ()).replace ("\n " , "<br>" )))
191+ _add_common_timing (items , t_handler0 )
154192 return _result (False , items )
155193
156- # ---- C) model existence ----
194+ # C) model existence
157195 if diag == "model_exists" :
158196 paths = _candidate_model_paths ()
159197 any_found = False
@@ -168,13 +206,15 @@ def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
168206 if not any_found :
169207 items .append (("C_FAIL" , "No model files found in candidate paths" ))
170208 items .append (("C_candidates" , " | " .join (paths )))
209+ _add_common_timing (items , t_handler0 )
171210 return _result (False , items )
172211
173- # ---- D/E require an image -> we continue below to load image first ----
212+ # D/E require an image -> continue to load image first
174213
175214 # 1) Validate input (unit test requirement)
176215 if not isinstance (response , list ) or len (response ) == 0 :
177216 items .append (("BAD_INPUT" , "No images uploaded." ))
217+ _add_common_timing (items , t_handler0 )
178218 return _result (False , items )
179219
180220 # 2) Extract first URL
@@ -191,15 +231,18 @@ def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
191231
192232 if not url :
193233 items .append (("LOAD_FAIL" , "LOAD_FAIL: first image has no url field" ))
234+ _add_common_timing (items , t_handler0 )
194235 return _result (False , items )
195236
196237 # 3) Load-check (keep your existing CI-safe behaviour)
197238 # D/E need an image anyway, so we must load here unless explicitly skipped.
198239 if (not skip_load_check ) or try_fetch or diag in ("load_model" , "infer_once" ):
199- img , err = _load_bgr_image_from_url (str (url ))
240+ (img , err ), dt_img = _timeit (lambda : _load_bgr_image_from_url (str (url )))
241+ items .append (("t_image_load_s" , f"{ dt_img :.4f} " ))
200242 if img is None :
201243 items .append (("LOAD_FAIL" , f"LOAD_FAIL: Failed to load image. ({ err } )" ))
202244 items .append (("url" , str (url )))
245+ _add_common_timing (items , t_handler0 )
203246 return _result (False , items )
204247
205248 h , w = img .shape [:2 ]
@@ -208,59 +251,86 @@ def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
208251 else :
209252 img = None # type: ignore
210253
211- # ---- D) load model only (no inference) ----
254+ # D) load model only (no inference)
212255 if diag == "load_model" :
213256 try :
214- from ultralytics import YOLO
257+ # Trigger ultralytics import lazily
258+ YOLO , dt_ul = _timeit (lambda : ultralytics .YOLO )
259+ items .append (("t_ultralytics_import_s" , f"{ dt_ul :.4f} " ))
260+
215261 model_path = next ((p for p in _candidate_model_paths () if os .path .exists (p )), None )
216262 if not model_path :
217263 items .append (("D_FAIL" , "No model file found to load" ))
218264 items .append (("D_candidates" , " | " .join (_candidate_model_paths ())))
265+ _add_common_timing (items , t_handler0 )
219266 return _result (False , items )
267+
220268 items .append (("D_model_path" , model_path ))
221- _ = YOLO (model_path )
269+
270+ # Time model init/load
271+ _ , dt_load = _timeit (lambda : YOLO (model_path ))
272+ items .append (("t_model_load_s" , f"{ dt_load :.4f} " ))
222273 items .append (("D_load" , "model loaded ✅" ))
274+
275+ _add_common_timing (items , t_handler0 )
223276 return _result (False , items )
224277 except Exception as e :
225278 items .append (("D_FAIL" , f"{ type (e ).__name__ } : { e } " ))
226279 items .append (("TRACEBACK" , _escape_html (traceback .format_exc ()).replace ("\n " , "<br>" )))
280+ _add_common_timing (items , t_handler0 )
227281 return _result (False , items )
228282
229- # ---- E) infer once (minimal) ----
283+ # E) infer once (minimal)
230284 if diag == "infer_once" :
231285 try :
232- from ultralytics import YOLO
286+ # Trigger ultralytics import lazily
287+ YOLO , dt_ul = _timeit (lambda : ultralytics .YOLO )
288+ items .append (("t_ultralytics_import_s" , f"{ dt_ul :.4f} " ))
289+
233290 model_path = next ((p for p in _candidate_model_paths () if os .path .exists (p )), None )
234291 if not model_path :
235292 items .append (("E_FAIL" , "No model file found for inference" ))
236293 items .append (("E_candidates" , " | " .join (_candidate_model_paths ())))
294+ _add_common_timing (items , t_handler0 )
237295 return _result (False , items )
238296
239297 if img is None :
240298 items .append (("E_FAIL" , "Image not loaded (img is None)" ))
299+ _add_common_timing (items , t_handler0 )
241300 return _result (False , items )
242301
243302 items .append (("E_model_path" , model_path ))
244- model = YOLO (model_path )
245- _ = model .predict (source = img , imgsz = 640 , conf = 0.25 , verbose = False )
303+
304+ # Time YOLO() construction separately from predict()
305+ model , dt_load = _timeit (lambda : YOLO (model_path ))
306+ items .append (("t_model_load_s" , f"{ dt_load :.4f} " ))
307+
308+ _ , dt_pred = _timeit (lambda : model .predict (source = img , imgsz = 640 , conf = 0.25 , verbose = False ))
309+ items .append (("t_predict_s" , f"{ dt_pred :.4f} " ))
310+
246311 items .append (("E_infer" , "predict done ✅" ))
312+ _add_common_timing (items , t_handler0 )
247313 return _result (False , items )
248314 except Exception as e :
249315 items .append (("E_FAIL" , f"{ type (e ).__name__ } : { e } " ))
250316 items .append (("TRACEBACK" , _escape_html (traceback .format_exc ()).replace ("\n " , "<br>" )))
317+ _add_common_timing (items , t_handler0 )
251318 return _result (False , items )
252319
253320 # 4) Optional early exit (keep your original platform smoke behaviour)
254321 if fast_return and not try_fetch :
255322 items .append (("note" , "fast_return=True (no YOLO). Load-check already done." ))
323+ _add_common_timing (items , t_handler0 )
256324 return _result (False , items )
257325
258326 # 5) Default end (still no YOLO in this build unless you add it)
259327 items .append (("note" , "No YOLO executed in default path. Use diag=... to pinpoint failures." ))
328+ _add_common_timing (items , t_handler0 )
260329 return _result (False , items )
261330
262331 except Exception as e :
263332 tb = _escape_html (traceback .format_exc ())
264333 items .append (("UNHANDLED" , f"{ type (e ).__name__ } : { e } " ))
265334 items .append (("TRACEBACK" , tb .replace ("\n " , "<br>" )))
266- return _result (False , items )
335+ _add_common_timing (items , t_handler0 )
336+ return _result (False , items )
0 commit comments