88
99import logging
1010import os
11+ import threading
1112import traceback
1213
1314from benchpress .lib import open_source
7677logger = logging .getLogger (__name__ )
7778
7879
80+ # Env var used to advertise the stage FIFO path to a child benchmark process.
81+ # When set, a stage-aware benchmark (e.g. WDL prod_set's run_prod.sh) writes
82+ # stage markers like ``START <stage_name>`` and ``STOP`` to that FIFO so
83+ # this hook can rotate the perf monitor set per sub-benchmark and emit one
84+ # folder of perf data per stage.
85+ PERF_STAGE_FIFO_ENV = "BENCHPRESS_PERF_STAGE_FIFO"
86+
87+
88+ def _resolve_subdir_option (opts ):
89+ """Return whether the user opted into stage-aware mode.
90+
91+ Stage-aware mode is requested either by setting ``stage_aware: true`` in
92+ the perf hook options, or by leaving the default (False). Today the only
93+ benchmark that emits stage markers is WDL prod_set, but the wiring is
94+ generic.
95+ """
96+ if not isinstance (opts , dict ):
97+ return False
98+ return bool (opts .get ("stage_aware" , False ))
99+
100+
79101class Perf (Hook ):
80102 def before_job (self , opts , job ):
81103 self .opts = DEFAULT_OPTIONS
@@ -91,8 +113,180 @@ def before_job(self, opts, job):
91113 if not os .path .isdir (self .benchmark_metrics_dir ):
92114 os .mkdir (self .benchmark_metrics_dir )
93115
116+ self ._stage_aware = _resolve_subdir_option (opts )
117+ self ._stage_thread = None
118+ self ._stage_fifo_path = None
119+ self ._stage_lock = threading .Lock ()
120+ self ._current_stage = None
121+ self ._current_monitors = []
122+
123+ if self ._stage_aware :
124+ self ._start_stage_coordinator (job )
125+ else :
126+ # Legacy / default path: start one set of monitors that span the
127+ # entire benchmark.
128+ self .monitors = self ._build_and_run_monitors (job , subdir = None )
129+
130+ def after_job (self , opts , job ):
131+ if self ._stage_aware :
132+ self ._stop_stage_coordinator ()
133+ return
134+ for monitor in self .monitors :
135+ monitor .terminate ()
136+ for monitor in self .monitors :
137+ monitor .write_csv ()
138+
139+ # ------------------------------------------------------------------
140+ # Stage-aware mode
141+ # ------------------------------------------------------------------
142+
143+ def _start_stage_coordinator (self , job ):
144+ """Open a FIFO under the benchmark metrics dir and spawn a thread
145+ that reads ``START <stage>`` / ``STOP`` commands. Each START gets
146+ a fresh monitor set whose CSVs land in
147+ ``benchmark_metrics_<uuid>/<stage>/``.
148+
149+ The FIFO path is exposed via the ``BENCHPRESS_PERF_STAGE_FIFO``
150+ env var so the child benchmark process can find it. We deliberately
151+ DO NOT start any monitors yet -- the coordinator launches them
152+ when the first STAGE command arrives.
153+ """
154+ self ._stage_fifo_path = os .path .join (
155+ self .benchmark_metrics_dir , "perf_stage.fifo"
156+ )
157+ # Recreate the FIFO each run so a stale one from a previous run
158+ # never confuses us.
159+ if os .path .exists (self ._stage_fifo_path ):
160+ os .unlink (self ._stage_fifo_path )
161+ os .mkfifo (self ._stage_fifo_path , 0o600 )
162+ os .environ [PERF_STAGE_FIFO_ENV ] = self ._stage_fifo_path
163+ self ._stage_stop_event = threading .Event ()
164+ self ._stage_thread = threading .Thread (
165+ target = self ._stage_loop ,
166+ args = (job ,),
167+ name = "perf-stage-coordinator" ,
168+ daemon = True ,
169+ )
170+ self ._stage_thread .start ()
171+ logger .info (
172+ f"Perf hook: stage-aware mode active, FIFO at { self ._stage_fifo_path } "
173+ )
174+
175+ def _stop_stage_coordinator (self ):
176+ # Tell the coordinator to exit and unblock the FIFO read by writing
177+ # a final STOP. A standalone writer also lets us flush any lingering
178+ # monitor set without depending on the benchmark script having sent
179+ # its own STOP.
180+ self ._stage_stop_event .set ()
181+ try :
182+ with open (self ._stage_fifo_path , "w" ) as f :
183+ f .write ("STOP\n " )
184+ f .write ("__EXIT__\n " )
185+ except Exception as e :
186+ logger .warning (f"Perf hook: failed to nudge stage FIFO: { e } " )
187+ if self ._stage_thread is not None :
188+ self ._stage_thread .join (timeout = 30 )
189+ # Clean up the FIFO and the env var so a subsequent benchmark in the
190+ # same process doesn't accidentally re-use stale state.
191+ try :
192+ os .unlink (self ._stage_fifo_path )
193+ except OSError :
194+ pass
195+ os .environ .pop (PERF_STAGE_FIFO_ENV , None )
196+
197+ def _stage_loop (self , job ):
198+ """Read commands from the FIFO until __EXIT__. Each line is one of:
199+
200+ START <stage_name>
201+ STOP
202+ __EXIT__
203+
204+ START allocates fresh monitors and runs them; STOP terminates them
205+ and writes their CSVs. Multiple START/STOP cycles are supported.
206+
207+ Implementation note: each writer that closes the FIFO causes EOF on
208+ the reader, so we have to re-open the FIFO after every burst rather
209+ than holding a single file handle. Each ``open(..., "r")`` blocks
210+ until a writer is available again -- which is exactly the behavior
211+ we want for a coordinator that's awaiting the next stage marker.
212+ """
213+ try :
214+ while not self ._stage_stop_event .is_set ():
215+ with open (self ._stage_fifo_path , "r" ) as fifo :
216+ for line in fifo :
217+ line = line .strip ()
218+ if not line :
219+ continue
220+ if line == "__EXIT__" :
221+ self ._end_current_stage (job )
222+ return
223+ if line .startswith ("START " ):
224+ stage = line [len ("START " ) :].strip ()
225+ self ._begin_stage (job , stage )
226+ continue
227+ if line == "STOP" :
228+ self ._end_current_stage (job )
229+ continue
230+ logger .warning (
231+ f"Perf hook: ignoring unknown stage command: { line !r} "
232+ )
233+ except Exception as e :
234+ logger .warning (
235+ f"Perf hook: stage coordinator crashed: { type (e ).__name__ } : { e } "
236+ )
237+
238+ def _begin_stage (self , job , stage_name ):
239+ with self ._stage_lock :
240+ if self ._current_stage is not None :
241+ # Implicit stop of the previous stage -- the script forgot to
242+ # close it. Don't lose data; flush before starting the next.
243+ logger .warning (
244+ f"Perf hook: implicit STOP of stage "
245+ f"{ self ._current_stage !r} before START { stage_name !r} "
246+ )
247+ self ._end_current_stage_locked (job )
248+ # Sanitize the stage name into a filesystem-friendly subdir.
249+ sanitized = _sanitize_subdir (stage_name )
250+ logger .info (f"Perf hook: starting stage { sanitized !r} " )
251+ self ._current_stage = sanitized
252+ self ._current_monitors = self ._build_and_run_monitors (job , subdir = sanitized )
253+
254+ def _end_current_stage (self , job ):
255+ with self ._stage_lock :
256+ self ._end_current_stage_locked (job )
257+
258+ def _end_current_stage_locked (self , job ):
259+ if self ._current_stage is None :
260+ return
261+ logger .info (f"Perf hook: stopping stage { self ._current_stage !r} " )
262+ for monitor in self ._current_monitors :
263+ try :
264+ monitor .terminate ()
265+ except Exception as e :
266+ logger .warning (
267+ f"Perf hook: terminating monitor { monitor .name } failed: { e } "
268+ )
269+ for monitor in self ._current_monitors :
270+ try :
271+ monitor .write_csv ()
272+ except Exception as e :
273+ logger .warning (f"Perf hook: write_csv on { monitor .name } failed: { e } " )
274+ self ._current_stage = None
275+ self ._current_monitors = []
276+
277+ # ------------------------------------------------------------------
278+ # Shared monitor setup
279+ # ------------------------------------------------------------------
280+
281+ def _build_and_run_monitors (self , job , subdir ):
282+ """Instantiate every enabled monitor (with the given subdir) and
283+ start it. Returns the list of started monitors.
284+
285+ Refactored out of the original ``before_job`` body so both default
286+ mode and stage-aware mode share the same monitor-bring-up logic.
287+ """
94288 should_run_perf_stat = True
95- self . monitors = []
289+ monitors = []
96290 for mon_name in AVAIL_MONITORS .keys ():
97291 # `enabled` is a perf-hook-level flag, not a monitor constructor
98292 # arg. Pop it out before passing the rest to the monitor class.
@@ -102,34 +296,45 @@ def before_job(self, opts, job):
102296 continue
103297 try :
104298 MonitorClass = AVAIL_MONITORS [mon_name ]
105- monitor = MonitorClass (job_uuid = job .uuid , ** init_args )
299+ monitor = MonitorClass (job_uuid = job .uuid , subdir = subdir , ** init_args )
106300 # We should disable PerfStat (and not run anything that uses PMU)
107301 # if IntelPerfSpect3 is enabled.
108302 if isinstance (monitor , topdown .IntelPerfSpect3 ) and monitor .supported :
109303 logger .info (
110304 "Disabling PerfStat to avoid conflict with IntelPerfSpect3"
111305 )
112306 should_run_perf_stat = False
113- self . monitors .append (monitor )
307+ monitors .append (monitor )
114308 except Exception as e :
115309 logger .warning (
116310 f"Failed to load the perf monitor { mon_name } due to the following exception:"
117311 )
118312 logger .warning (traceback .print_exception (type (e ), e , e .__traceback__ ))
119313
120- for monitor in self . monitors :
314+ for monitor in monitors :
121315 try :
122316 if isinstance (monitor , perfstat .PerfStat ) and not should_run_perf_stat :
123317 continue
124318 monitor .run ()
125319 except Exception as e :
126320 logger .warning (
127- f"Could not run perf monitor { mon_name } due to the following exception:"
321+ f"Could not run perf monitor { monitor . name } due to the following exception:"
128322 )
129323 logger .warning (traceback .print_exception (type (e ), e , e .__traceback__ ))
324+ return monitors
130325
131- def after_job (self , opts , job ):
132- for monitor in self .monitors :
133- monitor .terminate ()
134- for monitor in self .monitors :
135- monitor .write_csv ()
326+
327+ def _sanitize_subdir (name ):
328+ """Make a stage name safe to use as a directory component.
329+
330+ Strip path separators and trim. Keep alphanumerics, dashes, underscores;
331+ replace anything else with underscore.
332+ """
333+ out = []
334+ for ch in name .strip ():
335+ if ch .isalnum () or ch in "-_." :
336+ out .append (ch )
337+ else :
338+ out .append ("_" )
339+ cleaned = "" .join (out ).strip ("._" )
340+ return cleaned or "unnamed_stage"
0 commit comments