55import argparse
66import concurrent .futures
77from concurrent .futures import ThreadPoolExecutor
8+ import math
89import threading
910import queue
1011import glob
@@ -108,7 +109,7 @@ class Context:
108109 def __init__ (self , args : argparse .Namespace , executor : ThreadPoolExecutor | None ) -> None :
109110 self .data_filter : Pattern [str ] = args .data_filter
110111 self .submission_filter : Pattern [str ] = args .submission_filter
111- self .fixed_timelim : int | None = args .fixed_timelim
112+ self .fixed_timelim : float | None = args .fixed_timelim
112113 self .executor = executor
113114 self ._background_work : list [concurrent .futures .Future [object ]] = []
114115
@@ -343,7 +344,7 @@ def run_submission(self, sub, runner: Runner, context: Context) -> Result:
343344
344345 return (res , res_low , res_high )
345346
346- def run_normal (self , sub , infile : Path , time_limit : int , feedback_dir : Path ) -> SubmissionResult :
347+ def run_normal (self , sub , infile : Path , time_limit : float , feedback_dir : Path ) -> SubmissionResult :
347348 """
348349 Run a submission batch-style (non-interactive)
349350 """
@@ -354,7 +355,7 @@ def run_normal(self, sub, infile: Path, time_limit: int, feedback_dir: Path) ->
354355 infile = str (infile ),
355356 outfile = str (outfile ),
356357 errfile = str (errfile ),
357- timelim = time_limit + 1 ,
358+ timelim = math . ceil ( time_limit ) + 1 ,
358359 memlim = self ._problem .metadata .limits .memory ,
359360 work_dir = sub .path ,
360361 )
@@ -408,7 +409,7 @@ def run_submission_multipass(self, feedback_dir: Path, run_sub_fn) -> Submission
408409
409410 return SubmissionResult ('JE' , reason = f'Multipass validator did not give verdict in { validation_passes = } passes' )
410411
411- def run_submission_real (self , sub , context : Context , timelim : int , timelim_low : int , timelim_high : int ) -> Result :
412+ def run_submission_real (self , sub , context : Context , timelim : float , timelim_low : float , timelim_high : float ) -> Result :
412413 # This may be called off-main thread.
413414
414415 feedback_dir = Path (tempfile .mkdtemp (prefix = f'feedback-{ self .counter } -' , dir = self .problem .tmpdir ))
@@ -989,7 +990,11 @@ def check(self, context: Context) -> bool:
989990
990991 if self ._metadata .limits .time_limit is not None and not self ._metadata .limits .time_limit .is_integer ():
991992 self .warning (
992- 'Time limit configured to non-integer value. Problemtools does not yet support non-integer time limits, and will truncate'
993+ 'Time limit configured to non-integer value. This can be fragile, and may not be supported by your CCS (Kattis does not).'
994+ )
995+ if not self ._metadata .limits .time_resolution .is_integer ():
996+ self .warning (
997+ 'Time resolution is not an integer. This can be fragile, and may not be supported by your CCS (Kattis does not).'
993998 )
994999
9951000 return self ._check_res
@@ -1476,7 +1481,7 @@ def validate_interactive(
14761481 self ,
14771482 testcase : TestCase ,
14781483 submission ,
1479- timelim : int ,
1484+ timelim : float ,
14801485 errorhandler : Submissions ,
14811486 infile : str | None = None ,
14821487 feedback_dir_path : str | None = None ,
@@ -1489,7 +1494,7 @@ def validate_interactive(
14891494 errorhandler .error ('Could not locate interactive runner' )
14901495 return res
14911496 # file descriptor, wall time lim
1492- initargs = ['1' , str (2 * timelim )]
1497+ initargs = ['1' , str (math . ceil ( 2 * timelim ) )]
14931498 validator_args = [infile if infile else testcase .infile , testcase .ansfile , '<feedbackdir>' ]
14941499 submission_args = submission .get_runcmd (memlim = self .problem .metadata .limits .memory )
14951500
@@ -1540,7 +1545,7 @@ def validate_interactive(
15401545 if sub_runtime > timelim :
15411546 sub_runtime = timelim
15421547 res = self ._parse_validator_results (val , val_status , feedbackdir , testcase )
1543- elif is_TLE (sub_status , True ):
1548+ elif is_TLE (sub_status , True ) or sub_runtime > timelim :
15441549 res = SubmissionResult ('TLE' )
15451550 elif is_RTE (sub_status ):
15461551 res = SubmissionResult ('RTE' )
@@ -1622,7 +1627,7 @@ def validate(
16221627
16231628
16241629class Runner :
1625- def __init__ (self , problem : Problem , sub , context : Context , timelim : int , timelim_low : int , timelim_high : int ) -> None :
1630+ def __init__ (self , problem : Problem , sub , context : Context , timelim : float , timelim_low : float , timelim_high : float ) -> None :
16261631 self ._problem = problem
16271632 self ._sub = sub
16281633 self ._context = context
@@ -1759,15 +1764,17 @@ def __str__(self) -> str:
17591764 return 'submissions'
17601765
17611766 def check_submission (
1762- self , sub , context : Context , expected_verdict : Verdict , timelim : int , timelim_low : int , timelim_high : int
1767+ self , sub , context : Context , expected_verdict : Verdict , timelim : float , timelim_high : float
17631768 ) -> SubmissionResult :
17641769 desc = f'{ expected_verdict } submission { sub } '
17651770 partial = False
17661771 if expected_verdict == 'PAC' :
1767- # For partially accepted solutions, use the low timelim instead of the real one,
1768- # to make sure we have margin in both directions.
17691772 expected_verdict = 'AC'
17701773 partial = True
1774+ # For partially accepted, we don't want to use them to lower bound the time limit, but we do want
1775+ # to warn if they're slow enough that they would have affected the time limit, had they been used
1776+ # to compute it.
1777+ timelim_low = timelim / self .problem .metadata .limits .time_multipliers .ac_to_time_limit
17711778 else :
17721779 timelim_low = timelim
17731780
@@ -1822,24 +1829,34 @@ def start_background_work(self, context: Context) -> None:
18221829 for sub in self ._submissions [acr ]:
18231830 context .submit_background_work (lambda s : s .compile (), sub )
18241831
1832+ def _compute_time_limit (self , fixed_limit : float | None , lower_bound_runtime : float | None ) -> tuple [float , float ]:
1833+ if fixed_limit is None and lower_bound_runtime is None :
1834+ # 5 minutes is our currently hard coded upper bound for what to allow when we don't know the time limit yet
1835+ return 300.0 , 300.0
1836+
1837+ limits = self .problem .metadata .limits
1838+ if fixed_limit is not None :
1839+ timelim = fixed_limit
1840+ else :
1841+ assert lower_bound_runtime is not None , 'Assert to keep mypy happy'
1842+ exact_timelim = lower_bound_runtime * limits .time_multipliers .ac_to_time_limit
1843+ timelim = max (1 , math .ceil (exact_timelim / limits .time_resolution )) * limits .time_resolution
1844+
1845+ return timelim , timelim * limits .time_multipliers .time_limit_to_tle
1846+
18251847 def check (self , context : Context ) -> bool :
18261848 if self ._check_res is not None :
18271849 return self ._check_res
18281850 self ._check_res = True
18291851
18301852 limits = self .problem .metadata .limits
1831- time_multiplier = limits .time_multipliers .ac_to_time_limit
1832- safety_margin = limits .time_multipliers .time_limit_to_tle
1853+ ac_to_time_limit = limits .time_multipliers .ac_to_time_limit
18331854
1834- timelim_margin_lo = 300 # 5 minutes
1835- timelim_margin = 300
1836- timelim = 300
1855+ fixed_limit : float | None = context .fixed_timelim if context .fixed_timelim is not None else limits .time_limit
1856+ lower_bound_runtime : float | None = None # The runtime of the slowest submission used to lower bound the time limit.
18371857
1838- if limits .time_limit is not None :
1839- timelim = timelim_margin = int (limits .time_limit ) # TODO: Support non-integer time limits
1840- if context .fixed_timelim is not None :
1841- timelim = context .fixed_timelim
1842- timelim_margin = int (round (timelim * safety_margin ))
1858+ if limits .time_limit is not None and context .fixed_timelim is not None :
1859+ self .warning ('There is a fixed time limit in problem.yaml, and you provided one on command line. Using command line.' )
18431860
18441861 for verdict in Submissions ._VERDICTS :
18451862 acr = verdict [0 ]
@@ -1864,28 +1881,32 @@ def check(self, context: Context) -> bool:
18641881 self .error (f'Compile error for { acr } submission { sub } ' , additional_info = msg )
18651882 continue
18661883
1867- res = self .check_submission (sub , context , acr , timelim , timelim_margin_lo , timelim_margin )
1884+ timelim , timelim_high = self ._compute_time_limit (fixed_limit , lower_bound_runtime )
1885+ res = self .check_submission (sub , context , acr , timelim , timelim_high )
18681886 runtimes .append (res .runtime )
18691887
18701888 if acr == 'AC' :
18711889 if len (runtimes ) > 0 :
1872- max_runtime = max (runtimes )
1873- exact_timelim = max_runtime * time_multiplier
1874- max_runtime_str = f'{ max_runtime :.3f} '
1875- timelim = max (1 , int (0.5 + exact_timelim )) # TODO: properly support 2023-07 time limit computation
1876- timelim_margin_lo = max (1 , min (int (0.5 + exact_timelim / safety_margin ), timelim - 1 ))
1877- timelim_margin = max (timelim + 1 , int (0.5 + exact_timelim * safety_margin ))
1878- else :
1879- max_runtime_str = None
1880- if context .fixed_timelim is not None and context .fixed_timelim != timelim :
1881- self .msg (
1882- f' Solutions give timelim of { timelim } seconds, but will use provided fixed limit of { context .fixed_timelim } seconds instead'
1883- )
1884- timelim = context .fixed_timelim
1885- timelim_margin = round (timelim * safety_margin )
1890+ lower_bound_runtime = max (runtimes )
1891+
1892+ # Helper function to format numbers with at most 3 decimals and dealing with None
1893+ def _f_n (number : float | None ) -> str :
1894+ return f'{ round (number , 3 ):g} ' if number is not None else '-'
18861895
1896+ if fixed_limit is not None and lower_bound_runtime is not None :
1897+ if lower_bound_runtime * ac_to_time_limit > fixed_limit :
1898+ self .error (
1899+ f'Time limit fixed to { _f_n (fixed_limit )} , but slowest AC runs in { _f_n (lower_bound_runtime )} which is within a factor { _f_n (ac_to_time_limit )} .'
1900+ )
1901+ tl_from_subs , _ = self ._compute_time_limit (None , lower_bound_runtime )
1902+ if not math .isclose (fixed_limit , tl_from_subs ):
1903+ self .msg (
1904+ f' Solutions give timelim of { _f_n (tl_from_subs )} seconds, but will use provided fixed limit of { _f_n (fixed_limit )} seconds instead'
1905+ )
1906+
1907+ timelim , timelim_margin = self ._compute_time_limit (fixed_limit , lower_bound_runtime )
18871908 self .msg (
1888- f' Slowest AC runtime: { max_runtime_str } , setting timelim to { timelim } secs, safety margin to { timelim_margin } secs'
1909+ f' Slowest AC runtime: { _f_n ( lower_bound_runtime ) } , setting timelim to { _f_n ( timelim ) } secs, safety margin to { _f_n ( timelim_margin ) } secs'
18891910 )
18901911 self .problem ._set_timelim (timelim )
18911912
@@ -2149,7 +2170,7 @@ def argparser() -> argparse.ArgumentParser:
21492170 parser .add_argument (
21502171 '-t' ,
21512172 '--fixed_timelim' ,
2152- type = int ,
2173+ type = float ,
21532174 help = 'use this fixed time limit (useful in combination with -d and/or -s when all AC submissions might not be run on all data)' ,
21542175 )
21552176 parser .add_argument (
0 commit comments