1212
1313import asyncio
1414import copy
15+ from contextlib import nullcontext
1516from dataclasses import dataclass
1617from typing import Any , Callable , Dict , List , Tuple , Union
1718
2930 SemaphoreResourceExecutor ,
3031)
3132from openjudge .utils .mapping import parse_data_with_mapper
33+ from openjudge .utils .timer import TimingCollector
3234
3335
3436@dataclass
@@ -155,6 +157,8 @@ def __init__(
155157 aggregators : Union [BaseAggregator , Callable , List [Union [BaseAggregator , Callable ]], None ] = None ,
156158 show_progress : bool = True ,
157159 executor : BaseResourceExecutor | None = None ,
160+ enable_timing : bool = False ,
161+ timing_collector : TimingCollector | None = None ,
158162 ) -> None :
159163 """Initialize the grading runner.
160164
@@ -169,6 +173,10 @@ def __init__(
169173 show_progress: Whether to display a progress bar during execution. Defaults to True.
170174 executor: Optional execution resource to manage task execution.
171175 Defaults to LocalController if not provided.
176+ enable_timing: Whether to collect latency metrics for the grading workflow.
177+ Defaults to False.
178+ timing_collector: Optional collector for storing timing records. When not
179+ provided and ``enable_timing=True``, a collector is created automatically.
172180
173181 Example:
174182 >>> # Initialize with multiple graders
@@ -182,6 +190,10 @@ def __init__(
182190 self .max_concurrency = max_concurrency
183191 self .show_progress = show_progress
184192 self .executor = executor or SemaphoreResourceExecutor (max_concurrency )
193+ self .enable_timing = enable_timing or timing_collector is not None
194+ self .timing_collector = timing_collector or (
195+ TimingCollector () if self .enable_timing else None
196+ )
185197
186198 # Handle aggregators
187199 if not aggregators :
@@ -198,6 +210,8 @@ async def _arun(
198210 grader : BaseGrader ,
199211 mapper : Dict [str , str ] | Callable | None ,
200212 executor : BaseResourceExecutor ,
213+ timing_collector : TimingCollector | None = None ,
214+ timing_metadata : dict [str , Any ] | None = None ,
201215 ) -> GraderResult :
202216 """Run a single evaluation asynchronously.
203217
@@ -236,21 +250,48 @@ async def _arun(
236250 ... }
237251 >>> result = await GradingRunner._arun(data, ContextGrader(), custom_mapper)
238252 """
239- try :
240- data = parse_data_with_mapper (data , mapper )
241- # Create an isolated grader instance for this evaluation to prevent state sharing
242- isolated_grader = copy .deepcopy (grader )
243-
244- # The grader itself handles the mapping internally
245- return await isolated_grader .aevaluate (executor = executor , ** data )
246- except Exception as e :
247- error_msg = f"Error in { grader .name } during evaluation: { str (e )} "
248- logger .error (error_msg )
249- return GraderError (
250- name = grader .name ,
251- reason = f"Error in { grader .name } during evaluation" ,
252- error = error_msg ,
253+ timing_context = (
254+ timing_collector .measure (
255+ "grading_runner.single_evaluation" ,
256+ metadata = {"grader_name" : grader .name , ** (timing_metadata or {})},
253257 )
258+ if timing_collector
259+ else nullcontext ()
260+ )
261+
262+ with timing_context :
263+ try :
264+ data = parse_data_with_mapper (data , mapper )
265+ # Create an isolated grader instance for this evaluation to prevent state sharing
266+ isolated_grader = copy .deepcopy (grader )
267+
268+ # The grader itself handles the mapping internally
269+ return await isolated_grader .aevaluate (executor = executor , ** data )
270+ except Exception as e :
271+ error_msg = f"Error in { grader .name } during evaluation: { str (e )} "
272+ logger .error (error_msg )
273+ return GraderError (
274+ name = grader .name ,
275+ reason = f"Error in { grader .name } during evaluation" ,
276+ error = error_msg ,
277+ )
278+
279+ def get_timing_records (self , name : str | None = None ) -> list :
280+ """Return collected timing records for the grading workflow."""
281+ if self .timing_collector is None :
282+ return []
283+ return self .timing_collector .get_records (name = name )
284+
285+ def get_timing_summary (self ) -> dict [str , dict [str , float | int ]]:
286+ """Return aggregate timing metrics collected by the runner."""
287+ if self .timing_collector is None :
288+ return {}
289+ return self .timing_collector .get_summary ()
290+
291+ def clear_timing_records (self ) -> None :
292+ """Clear previously collected timing records."""
293+ if self .timing_collector is not None :
294+ self .timing_collector .clear ()
254295
255296 async def arun (
256297 self ,
@@ -324,59 +365,85 @@ async def arun(
324365 ... else:
325366 ... print(f" Sample {i}: Error - {result.error}")
326367 """
327- # Create a dictionary to store result lists for each grader
328- grader_results : RunnerResult = {name : [] for name in self .grader_configs .keys ()}
329-
330- # Create coroutines for all evaluators and all samples
331- all_coroutines = []
332- coroutine_info = [] # Track (grader_name, sample_index) for each coroutine
333-
334- # Use the executor from self
335- executor = self .executor
336-
337- # Execute executor lifecycle
338- for name , config in self .grader_configs .items ():
339- grader = config .grader
340- mapper = config .mapper
341- assert grader is not None
342-
343- # Create coroutines for the current evaluator on all samples
344- for i , case in enumerate (dataset ):
345- all_coroutines .append (
346- self ._arun (data = case , grader = grader , mapper = mapper , executor = executor ),
347- )
348- coroutine_info .append (
349- (name , i ),
350- ) # Record grader name and sample index
351-
352- # Execute all evaluator-sample coroutines concurrently
353- if self .show_progress :
354- all_results = await tqdm_asyncio .gather (
355- * all_coroutines ,
356- desc = "Evaluating a dataset" ,
357- total = len (all_coroutines ),
368+ timing_context = (
369+ self .timing_collector .measure (
370+ "grading_runner.dataset" ,
371+ metadata = {
372+ "dataset_size" : len (dataset ),
373+ "grader_count" : len (self .grader_configs ),
374+ },
358375 )
359- else :
360- all_results = await asyncio .gather (* all_coroutines )
361-
362- # Initialize lists for all graders
363- for name in self .grader_configs .keys ():
364- grader_results [name ] = [None ] * len (dataset )
365-
366- # Organize results by grader
367- for (grader_name , sample_index ), result in zip (coroutine_info , all_results ):
368- grader_results [grader_name ][sample_index ] = result
369-
370- # Aggregate results
371- if self .aggregators :
372- for aggregator in self .aggregators :
373- aggregator_name = aggregator .__name__
374- grader_results [aggregator_name ] = [None ] * len (dataset )
375- for i in range (len (dataset )):
376- grader_results [aggregator_name ][i ] = aggregator (
377- {grader_name : grader_results [grader_name ][i ] for grader_name in self .grader_configs .keys ()},
376+ if self .timing_collector
377+ else nullcontext ()
378+ )
379+
380+ with timing_context :
381+ # Create a dictionary to store result lists for each grader
382+ grader_results : RunnerResult = {name : [] for name in self .grader_configs .keys ()}
383+
384+ # Create coroutines for all evaluators and all samples
385+ all_coroutines = []
386+ coroutine_info = [] # Track (grader_name, sample_index) for each coroutine
387+
388+ # Use the executor from self
389+ executor = self .executor
390+
391+ # Execute executor lifecycle
392+ for name , config in self .grader_configs .items ():
393+ grader = config .grader
394+ mapper = config .mapper
395+ assert grader is not None
396+
397+ # Create coroutines for the current evaluator on all samples
398+ for i , case in enumerate (dataset ):
399+ all_coroutines .append (
400+ self ._arun (
401+ data = case ,
402+ grader = grader ,
403+ mapper = mapper ,
404+ executor = executor ,
405+ timing_collector = self .timing_collector ,
406+ timing_metadata = {
407+ "grader_config_name" : name ,
408+ "sample_index" : i ,
409+ },
410+ ),
378411 )
379- return grader_results
412+ coroutine_info .append (
413+ (name , i ),
414+ ) # Record grader name and sample index
415+
416+ # Execute all evaluator-sample coroutines concurrently
417+ if self .show_progress :
418+ all_results = await tqdm_asyncio .gather (
419+ * all_coroutines ,
420+ desc = "Evaluating a dataset" ,
421+ total = len (all_coroutines ),
422+ )
423+ else :
424+ all_results = await asyncio .gather (* all_coroutines )
425+
426+ # Initialize lists for all graders
427+ for name in self .grader_configs .keys ():
428+ grader_results [name ] = [None ] * len (dataset )
429+
430+ # Organize results by grader
431+ for (grader_name , sample_index ), result in zip (coroutine_info , all_results ):
432+ grader_results [grader_name ][sample_index ] = result
433+
434+ # Aggregate results
435+ if self .aggregators :
436+ for aggregator in self .aggregators :
437+ aggregator_name = aggregator .__name__
438+ grader_results [aggregator_name ] = [None ] * len (dataset )
439+ for i in range (len (dataset )):
440+ grader_results [aggregator_name ][i ] = aggregator (
441+ {
442+ grader_name : grader_results [grader_name ][i ]
443+ for grader_name in self .grader_configs .keys ()
444+ },
445+ )
446+ return grader_results
380447
381448 async def arun_multiple_datasets (
382449 self ,
@@ -468,26 +535,36 @@ async def arun_multiple_datasets(
468535 - When batch processing, individual arun() progress bars are disabled to avoid
469536 display conflicts with the batch-level progress bar.
470537 """
471- # Temporarily disable show_progress for individual arun calls to avoid progress bar conflicts
472- original_show_progress = self .show_progress
473- self .show_progress = False
474-
475- try :
476- # Create tasks for each dataset
477- tasks = [self .arun (dataset , * args , ** kwargs ) for dataset in datasets ]
478-
479- # Execute all dataset tasks concurrently with progress bar
480- if original_show_progress :
481- all_results = await tqdm_asyncio .gather (
482- * tasks ,
483- desc = f"Evaluating { len (tasks )} datasets" ,
484- total = len (tasks ),
485- )
486- else :
487- all_results = await asyncio .gather (* tasks )
488-
489- # Return results as a list
490- return list (all_results )
491- finally :
492- # Restore original show_progress setting
493- self .show_progress = original_show_progress
538+ timing_context = (
539+ self .timing_collector .measure (
540+ "grading_runner.multi_dataset" ,
541+ metadata = {"dataset_count" : len (datasets )},
542+ )
543+ if self .timing_collector
544+ else nullcontext ()
545+ )
546+
547+ with timing_context :
548+ # Temporarily disable show_progress for individual arun calls to avoid progress bar conflicts
549+ original_show_progress = self .show_progress
550+ self .show_progress = False
551+
552+ try :
553+ # Create tasks for each dataset
554+ tasks = [self .arun (dataset , * args , ** kwargs ) for dataset in datasets ]
555+
556+ # Execute all dataset tasks concurrently with progress bar
557+ if original_show_progress :
558+ all_results = await tqdm_asyncio .gather (
559+ * tasks ,
560+ desc = f"Evaluating { len (tasks )} datasets" ,
561+ total = len (tasks ),
562+ )
563+ else :
564+ all_results = await asyncio .gather (* tasks )
565+
566+ # Return results as a list
567+ return list (all_results )
568+ finally :
569+ # Restore original show_progress setting
570+ self .show_progress = original_show_progress
0 commit comments