1313from operator import itemgetter
1414from pathlib import Path
1515from zlib import crc32
16+ import sqlite3
1617
1718RED = "\033 [91m"
1819GREEN = "\033 [92m"
3233
3334LANGUAGES = {
3435 "Python" : "{year}/day{day}/day{day}.py" ,
35- "PyPy" : "{year}/day{day}/day{day}.py" ,
36+ # "PyPy": "{year}/day{day}/day{day}.py",
3637 "Rust" : "{year}/target/release/day{day}" ,
3738 "C" : "{year}/build/day{day}_c" ,
3839 "C++" : "{year}/build/day{day}_cpp" ,
3940}
4041
42+ INTERPRETERS = {
43+ "Python" : "python3" ,
44+ "PyPy" : "pypy3" ,
45+ }
46+
4147
4248def get_cache ():
4349 """Retrieve the cache instance from memory or load it from disk."""
4450 cache = globals ().get ("_cache" )
4551 if cache is None :
46- cache_file = Path (__file__ ).parent .parent / "data" / "cache.json"
47- if cache_file .is_file ():
48- cache = json .loads (cache_file .read_bytes ())
49- else :
50- cache = {}
52+ cache_file = Path (__file__ ).parent .parent / "data" / "cache.db"
53+
54+ cache_db = sqlite3 .connect (cache_file )
55+
56+ cache_db .executescript (
57+ "create table if not exists solutions ("
58+ " key text primary key not null,"
59+ " mtime_ns int,"
60+ " elapsed float,"
61+ " status text,"
62+ " answers text);"
63+ )
64+ cache = {"db" : cache_db , "modified" : False }
5165 globals ()["_cache" ] = cache
52- cache [ "modified" ] = False
66+
5367 return cache
5468
5569
5670def save_cache ():
57- cache = get_cache ()
58- if cache ["modified" ]:
59- cache .pop ("modified" )
60- cache_file = Path (__file__ ).parent .parent / "data" / "cache.json"
61- cache_file .write_text (json .dumps (cache , indent = 2 , ensure_ascii = True ))
62- cache ["modified" ] = False
63- print (f"{ FEINT } { ITALIC } cache commited{ RESET } " )
71+ pass
72+ # cache = get_cache()
73+ # if cache["modified"]:
74+ # cache["db"].commit()
75+ # cache["modified"] = False
76+ # print(f"{FEINT}{ITALIC}cache commited{RESET}")
6477
6578
66- def check_cache (key , file_timestamp : Path , no_check = False ):
79+ def check_cache (key , file_timestamp : Path , no_age_check = False ):
6780 cache = get_cache ()
6881 key = str (key )
69- e = cache .get (key , None )
70- if e :
82+ db = cache ["db" ]
83+ cursor = db .execute ("select mtime_ns,elapsed,status,answers from solutions where key=?" , (key ,))
84+ row = cursor .fetchone ()
85+ if row :
7186 timestamp = file_timestamp .stat ().st_mtime_ns
72- if e ["timestamp" ] == timestamp or no_check :
73- return e
87+ if row [0 ] == timestamp or no_age_check :
88+ return {
89+ "elapsed" : row [1 ],
90+ "status" : row [2 ],
91+ "answers" : row [3 ].split ("\n " ),
92+ }
7493 else :
75- seconds = round ((timestamp - e ["timestamp" ]) / 1000000000 )
76- delta = timedelta (seconds = seconds )
94+ # seconds = round((timestamp - e["timestamp"]) / 1000000000)
95+ # delta = timedelta(seconds=seconds)
96+ # print(f"{FEINT}{ITALIC}entry {key} is out of date for {delta}{RESET}", end=f"{CR}")
7797
78- print (f"{ FEINT } { ITALIC } entry { key } is out of date for { delta } { RESET } " , end = f"{ CR } " )
98+ print (f"{ FEINT } { ITALIC } entry { key } is out of date{ RESET } " , end = f"{ CR } " )
7999
80100 else :
81- print (f"{ FEINT } { ITALIC } missing cache for { key } { RESET } " , end = f"{ CR } " )
101+ print (f"{ FEINT } { ITALIC } missing cache for { key } { RESET } " , end = f"{ CLEAR_EOL } { CR } " )
82102
83103
84- def update_cache (key , timestamp : Path , elapsed , status , answers ):
104+ def update_cache (key , timestamp : Path , elapsed : float , status : str , answers : t . Iterable ):
85105 cache = get_cache ()
106+ db = cache ["db" ]
86107 key = str (key )
87- e = cache .get (key , {})
88- e ["timestamp" ] = timestamp .stat ().st_mtime_ns
89- e ["elapsed" ] = elapsed
90- e ["status" ] = status
91- e ["answers" ] = answers
92- cache [key ] = e
93- cache ["modified" ] = True
94- return e
95108
109+ db .execute (
110+ "insert or replace into solutions (key,mtime_ns,elapsed,status,answers) values (?,?,?,?,?)" ,
111+ (key , timestamp .stat ().st_mtime_ns , elapsed , status , "\n " .join (answers )),
112+ )
113+
114+ # cache["modified"] = True
115+ db .commit ()
96116
97- def run (key : str , prog : Path , lang : str , file : Path , solution : t .List , refresh : bool , dry_run : bool ):
117+ return {
118+ "elapsed" : elapsed ,
119+ "status" : status ,
120+ "answers" : answers ,
121+ }
122+
123+
124+ def run (prog : Path , lang : str , file : Path , solution : t .List , warmup : bool ) -> t .Dict [str , t .Any ]:
98125 if not prog .is_file ():
99126 return
100127
101128 cmd = [prog .absolute ().as_posix ()]
102129
103- if lang == "Python" :
104- cmd .insert (0 , "python3" )
105- elif lang == "PyPy" :
106- cmd .insert (0 , "pypy3" )
107-
108- if refresh :
109- e = None
110- else :
111- e = check_cache (key , prog , dry_run )
112- if dry_run and not e :
113- return None
114- if e :
115- in_cache = True
130+ # add the interpreter
131+ interpreter = INTERPRETERS .get (lang )
132+ if interpreter :
133+ cmd .insert (0 , interpreter )
134+
135+ if warmup and lang == "Rust" :
136+ # under macOS, the first launch of a Rust program is slower (why ???)
137+ subprocess .call (cmd + ["--help" ], stdout = subprocess .DEVNULL )
138+
139+ start = time .time_ns ()
140+ out = subprocess .run (cmd + [file .absolute ()], stdout = subprocess .PIPE )
141+ elapsed = time .time_ns () - start
142+ answers = out .stdout .decode ().strip ()
143+
144+ status = "unknown"
145+ if solution :
146+ solution = solution .read_text ()
147+ if answers == solution .strip ():
148+ status = "ok"
149+ else :
150+ status = "error"
116151 else :
117- in_cache = False
118-
119- start = time .time_ns ()
120- out = subprocess .run (cmd + [file .absolute ()], stdout = subprocess .PIPE )
121- elapsed = time .time_ns () - start
122- answers = out .stdout .decode ().strip ()
123-
124- status = "unknown"
125- if solution :
126- solution = solution .read_text ()
127- if answers == solution .strip ():
128- status = "ok"
129- else :
130- status = "error"
152+ if answers == "" :
153+ status = "missing"
131154 else :
132- if answers == "" :
133- status = "fail"
134- else :
135- status = "unknown"
136-
137- e = update_cache (key , prog , elapsed , status , answers .split ("\n " ))
138-
139- e = deepcopy (e )
140- e ["cache" ] = in_cache
155+ status = "unknown"
141156
142- return e
157+ return { "elapsed" : elapsed , "status" : status , "answers" : answers . split ( " \n " )}
143158
144159
145160def make (year : Path , source : Path , dest : Path , cmd : str ):
@@ -155,11 +170,11 @@ def make(year: Path, source: Path, dest: Path, cmd: str):
155170 return
156171
157172 cmdline = f"{ cmd } -o { output } -Wall -Wextra -O3 -DSTANDALONE -I{ source .parent } { source } "
158- print (cmdline )
173+ print (f" { CR } { cmdline } " , end = "" )
159174 subprocess .check_call (cmdline , shell = True )
160175
161176
162- def build_all (filter_year ):
177+ def build_all (filter_year : int ):
163178 for year in range (2015 , 2024 ):
164179 if filter_year != 0 and year != filter_year :
165180 continue
@@ -183,13 +198,20 @@ def build_all(filter_year):
183198 make (year , src , f"day{ day } _cpp" , "c++ -std=c++17" )
184199
185200
186- def load_data (filter_year ):
201+ def load_data (filter_year , filter_user ):
187202 inputs = defaultdict (dict )
188203 solutions = defaultdict (dict )
189204
190205 for f in Path ("data" ).rglob ("*.in" ):
206+
207+ if f .name .startswith ("._" ):
208+ continue
209+
191210 assert len (f .parts ) == 4
192211
212+ if filter_user and f .parent .parent .name != filter_user :
213+ continue
214+
193215 year = int (f .parent .name )
194216 day = int (f .stem )
195217
@@ -201,7 +223,7 @@ def load_data(filter_year):
201223 crc = e ["status" ]
202224 else :
203225 crc = hex (crc32 (f .read_bytes ().strip ()) & 0xFFFFFFFF )
204- update_cache (f , f , 0 , crc , 0 )
226+ update_cache (f , f , 0 , crc , [] )
205227
206228 if crc not in inputs [year , day ]:
207229 inputs [year , day ][crc ] = f
@@ -219,7 +241,7 @@ def run_day(
219241):
220242 elapsed = defaultdict (list )
221243
222- first = True
244+ warmup = defaultdict ( lambda : True )
223245
224246 day_suffix = mday .removeprefix (str (day ))
225247 name_max_len = 16 - len (day_suffix )
@@ -242,12 +264,20 @@ def run_day(
242264 prog = Path (pattern .format (year = year , day = mday ))
243265 key = ":" .join (map (str , (year , day , crc , prog , lang .lower ())))
244266
245- if lang .lower () == "rust" and first and prog .is_file ():
246- # under macOS, the first launch of a program is slower
247- first = False
248- subprocess .call ([prog , "--help" ], stdout = subprocess .DEVNULL )
267+ if refresh :
268+ e = None
269+ in_cache = False
270+ else :
271+ e = check_cache (key , prog , dry_run )
272+ in_cache = e is not None
273+
274+ if not in_cache and not dry_run :
275+
276+ e = run (prog , lang , file , day_sols .get (crc ), warmup [lang ])
249277
250- e = run (key , prog , lang , file , day_sols .get (crc ), refresh , dry_run )
278+ if e :
279+ warmup [lang ] = False
280+ e = update_cache (key , prog , e ["elapsed" ], e ["status" ], e ["answers" ])
251281
252282 if not e :
253283 continue
@@ -257,24 +287,24 @@ def run_day(
257287 else :
258288 info = ""
259289
260- status_color = {"fail " : MAGENTA , "unknown" : GRAY , "error" : RED , "ok" : GREEN }[e ["status" ]]
290+ status_color = {"missing " : MAGENTA , "unknown" : GRAY , "error" : RED , "ok" : GREEN }[e ["status" ]]
261291
262292 line = (
263293 f"{ CR } { RESET } { CLEAR_EOL } "
264294 f"{ prefix } "
265295 f" { YELLOW } { lang :<7} { RESET } :"
266296 f" { status_color } { e ['status' ]:7} { RESET } "
267297 f" { WHITE } { e ['elapsed' ]/ 1e9 :7.3f} s"
268- f" { GRAY } { '☽' if e [ 'cache' ] else ' ' } "
298+ f" { GRAY } { '☽' if in_cache else ' ' } "
269299 f" { status_color } { str (e ['answers' ]):<40} { RESET } "
270300 f"{ info } "
271301 )
272302 print (line )
273303
274- if e ["status" ] == "fail " or e ["status" ] == "error" :
304+ if e ["status" ] == "missing " or e ["status" ] == "error" :
275305 problems .append (line )
276306
277- if not e [ "cache" ] and e ["elapsed" ] / 1e9 > 5 :
307+ if not in_cache and e ["elapsed" ] / 1e9 > 5 :
278308 save_cache ()
279309
280310 results .add (" " .join (e ["answers" ]))
@@ -298,11 +328,13 @@ def main():
298328 parser .add_argument ("-l" , "--language" , type = str , metavar = "LANG" , help = "filter by language" )
299329 parser .add_argument ("-r" , "--refresh" , action = "store_true" , help = "relaunch solutions" )
300330 parser .add_argument ("-n" , "--dry-run" , action = "store_true" , help = "do not run" )
331+ parser .add_argument ("--no-build" , action = "store_true" , help = "do not build" )
332+ parser .add_argument ("-u" , "--user" , dest = "filter_user" , type = str , help = "filter by user id" )
301333 parser .add_argument ("n" , type = int , nargs = "*" , help = "filter by year or year/day" )
302334
303335 args = parser .parse_args ()
304336
305- filter_year = 0 if len (args .n ) == 0 else args .n .pop (0 )
337+ filter_year = 0 if len (args .n ) == 0 else int ( args .n .pop (0 ) )
306338 filter_day = set (args .n )
307339 if args .language == "cpp" :
308340 args .language = "c++"
@@ -313,8 +345,11 @@ def main():
313345 problems = []
314346 stats_elapsed = dict ()
315347
316- build_all (filter_year )
317- inputs , sols = load_data (filter_year )
348+ if not args .no_build :
349+ build_all (filter_year )
350+ print (end = f"{ CR } { CLEAR_EOL } " )
351+
352+ inputs , sols = load_data (filter_year , args .filter_user )
318353
319354 for year in range (2015 , 2024 ):
320355 if filter_year != 0 and year != filter_year :
@@ -342,7 +377,7 @@ def main():
342377
343378 if elapsed :
344379 print (
345- " --> " ,
380+ f" { CLEAR_EOL } --> " ,
346381 " | " .join ((f"{ lang } : { t :.3f} s" for lang , t in elapsed .items ())),
347382 f"{ FEINT } ({ nb_samples } input{ 's' if nb_samples > 1 else '' } ){ RESET } " ,
348383 )
0 commit comments