1717import time
1818
1919
20- def robust_rmtree (path , attempts = 20 , delay = 0.5 ):
20+ def robust_rmtree (path , attempts = 60 , delay = 1.0 ):
2121 """`shutil.rmtree` with retries.
2222
2323 Windows may briefly retain a file lock on objects (e.g. an mmap'd
2424 commit-graph) even after the process that opened them has exited,
25- causing `rmtree` to raise `PermissionError`. Retry a few times .
25+ causing `rmtree` to raise `PermissionError`. Retry generously .
2626 """
2727 for i in range (attempts ):
2828 try :
@@ -37,11 +37,9 @@ def robust_rmtree(path, attempts=20, delay=0.5):
3737def time_one_repack (binary , template , work ):
3838 """Run `<binary> -C <work> repack -adfq` once and return elapsed seconds.
3939
40- The work directory is overwritten with a fresh copy of `template`
41- before the timed command runs.
40+ The work directory must not already exist; it is created from
41+ `template` byte-for-byte before the timed command runs.
4242 """
43- if os .path .exists (work ):
44- robust_rmtree (work )
4543 shutil .copytree (template , work )
4644 cmd = [binary , "-C" , work , "-c" , "pack.threads=4" , "repack" , "-adfq" ]
4745 t0 = time .monotonic_ns ()
@@ -77,25 +75,48 @@ def main():
7775 rng = random .Random (args .seed )
7876 os .makedirs (os .path .dirname (args .results ) or "." , exist_ok = True )
7977
80- with open (args .results , "w" , newline = "" ) as f :
81- writer = csv .DictWriter (
82- f , fieldnames = ["iteration" , "position" , "variant" , "seconds" ])
83- writer .writeheader ()
84- for it in range (1 , args .iterations + 1 ):
85- order = list (binaries .keys ())
86- rng .shuffle (order )
87- print (f"=== iteration { it } : order = { order } ===" , flush = True )
88- for pos , variant in enumerate (order , start = 1 ):
89- seconds = time_one_repack (binaries [variant ], args .template , args .work )
90- writer .writerow ({
91- "iteration" : it ,
92- "position" : pos ,
93- "variant" : variant ,
94- "seconds" : f"{ seconds :.6f} " ,
95- })
96- f .flush ()
97- print (f" pos={ pos } variant={ variant } seconds={ seconds :.3f} " ,
98- flush = True )
78+ # The work directory used by --work is a *base name*; each timed run
79+ # uses a unique sibling like ${work}.1.1, ${work}.1.2, ... so we never
80+ # need to rmtree a directory whose files might still be mmap-locked
81+ # by the just-exited git process (a Windows-specific issue with the
82+ # commit-graph file).
83+ work_base = args .work
84+ if os .path .exists (work_base ):
85+ robust_rmtree (work_base )
86+ work_dirs = []
87+
88+ try :
89+ with open (args .results , "w" , newline = "" ) as f :
90+ writer = csv .DictWriter (
91+ f , fieldnames = ["iteration" , "position" , "variant" , "seconds" ])
92+ writer .writeheader ()
93+ for it in range (1 , args .iterations + 1 ):
94+ order = list (binaries .keys ())
95+ rng .shuffle (order )
96+ print (f"=== iteration { it } : order = { order } ===" , flush = True )
97+ for pos , variant in enumerate (order , start = 1 ):
98+ work = f"{ work_base } .{ it } .{ pos } "
99+ work_dirs .append (work )
100+ seconds = time_one_repack (binaries [variant ],
101+ args .template , work )
102+ writer .writerow ({
103+ "iteration" : it ,
104+ "position" : pos ,
105+ "variant" : variant ,
106+ "seconds" : f"{ seconds :.6f} " ,
107+ })
108+ f .flush ()
109+ print (f" pos={ pos } variant={ variant } "
110+ f"seconds={ seconds :.3f} " , flush = True )
111+ finally :
112+ # Cleanup is best-effort; if Windows still holds locks we accept
113+ # that the workspace will be reaped by the runner.
114+ for work in work_dirs :
115+ try :
116+ robust_rmtree (work )
117+ except OSError as e :
118+ print (f"warning: could not remove { work } : { e } " ,
119+ file = sys .stderr )
99120
100121 # Summary
101122 with open (args .results ) as f :
0 commit comments