@@ -46,7 +46,7 @@ use crate::compile::builder::ExternalPortId;
4646use crate :: live_collections:: stream:: { ExactlyOnce , NoOrder , Ordering , Retries , TotalOrder } ;
4747use crate :: location:: dynamic:: LocationId ;
4848use crate :: sim:: graph:: { SimExternalPort , SimExternalPortRegistry } ;
49- use crate :: sim:: runtime:: SimHook ;
49+ use crate :: sim:: runtime:: { SimHook , SimInlineHook } ;
5050
5151struct QuiescenceState {
5252 /// Set to true when the scheduler reaches quiescence; reset to false when new input is sent.
@@ -663,38 +663,60 @@ impl<'a> CompiledSimInstance<'a> {
663663 /// Consumes this instance and constructs the [`LaunchedSim`] state struct, which is
664664 /// advanced incrementally via [`LaunchedSim::step`].
665665 fn start < W : std:: io:: Write > ( mut self , log_override : Option < W > ) -> LaunchedSim < W > {
666- let ( async_dfirs, tick_dfirs, hooks, inline_hooks) = self . dylib_result . take ( ) . unwrap ( ) ;
667-
668- let not_ready_observation = async_dfirs
669- . iter ( )
670- . map ( |( lid, c_id, _) | ( serde_json:: from_str ( lid) . unwrap ( ) , * c_id) )
666+ let ( async_dfirs, tick_dfirs, mut hooks, mut inline_hooks) =
667+ self . dylib_result . take ( ) . unwrap ( ) ;
668+
669+ // The generated code keys hooks and tick DFIRs by the same serialized location
670+ // strings, so we can move each tick's / observation's hooks out of the maps and
671+ // attach them directly. This lets the scheduler's hot paths avoid keyed lookups
672+ // (which would clone `LocationId`s) entirely.
673+ let not_ready_ticks = tick_dfirs
674+ . into_iter ( )
675+ . map ( |( lid, cluster_id, dfir) | {
676+ let location: LocationId = serde_json:: from_str ( lid) . unwrap ( ) ;
677+ let LocationId :: Tick ( _, parent_location) = location else {
678+ unreachable ! ( "tick DFIRs are always keyed by a tick location" )
679+ } ;
680+ SimTick {
681+ parent_location : * parent_location,
682+ cluster_id,
683+ dfir,
684+ hooks : hooks
685+ . remove ( & ( lid, cluster_id) )
686+ . expect ( "every tick DFIR must have at least one hook" ) ,
687+ inline_hooks : inline_hooks. remove ( & ( lid, cluster_id) ) . unwrap_or_default ( ) ,
688+ }
689+ } )
671690 . collect ( ) ;
672691
673692 let quiescence = CURRENT_SIM_CONNECTIONS . with ( |connections| {
674693 let connections = connections. borrow ( ) ;
675694 connections. quiescence . clone ( )
676695 } ) ;
677696
697+ let not_ready_observations = async_dfirs
698+ . iter ( )
699+ . map ( |( lid, cluster_id, _) | SimObservation {
700+ location : serde_json:: from_str ( lid) . unwrap ( ) ,
701+ cluster_id : * cluster_id,
702+ hooks : hooks. remove ( & ( * lid, * cluster_id) ) . unwrap_or_default ( ) ,
703+ } )
704+ . collect ( ) ;
705+
706+ debug_assert ! (
707+ hooks. is_empty( ) && inline_hooks. is_empty( ) ,
708+ "all hooks should belong to either a tick DFIR or a top-level location"
709+ ) ;
710+
678711 LaunchedSim {
679712 async_dfirs : async_dfirs
680713 . into_iter ( )
681714 . map ( |( lid, c_id, dfir) | ( serde_json:: from_str ( lid) . unwrap ( ) , c_id, dfir) )
682715 . collect ( ) ,
683716 possibly_ready_ticks : vec ! [ ] ,
684- not_ready_ticks : tick_dfirs
685- . into_iter ( )
686- . map ( |( lid, c_id, dfir) | ( serde_json:: from_str ( lid) . unwrap ( ) , c_id, dfir) )
687- . collect ( ) ,
688- possibly_ready_observation : vec ! [ ] ,
689- not_ready_observation,
690- hooks : hooks
691- . into_iter ( )
692- . map ( |( ( lid, cid) , hs) | ( ( serde_json:: from_str ( lid) . unwrap ( ) , cid) , hs) )
693- . collect ( ) ,
694- inline_hooks : inline_hooks
695- . into_iter ( )
696- . map ( |( ( lid, cid) , hs) | ( ( serde_json:: from_str ( lid) . unwrap ( ) , cid) , hs) )
697- . collect ( ) ,
717+ not_ready_ticks,
718+ possibly_ready_observations : vec ! [ ] ,
719+ not_ready_observations,
698720 log : if self . log {
699721 if let Some ( w) = log_override {
700722 LogKind :: Custom ( w)
@@ -1176,6 +1198,60 @@ impl<W: std::io::Write> std::fmt::Write for LogKind<W> {
11761198 }
11771199}
11781200
1201+ /// A tick-scoped DFIR together with the hooks that feed it data.
1202+ struct SimTick {
1203+ /// The location of the process/cluster the tick lives on, used to match this tick
1204+ /// against the async DFIR that produces its input data.
1205+ parent_location : LocationId ,
1206+ /// The cluster member ID, if the tick lives on a cluster.
1207+ cluster_id : Option < u32 > ,
1208+ /// The tick DFIR, executed once per tick.
1209+ dfir : DfirErased ,
1210+ /// Hooks (e.g. from `batch`) resolved *before* the tick runs, deciding what data to
1211+ /// release into it.
1212+ hooks : Vec < Box < dyn SimHook > > ,
1213+ /// Hooks (e.g. from `assume_ordering` inside the tick) resolved *while* the tick DFIR
1214+ /// is running, via a `tokio::select!` loop, for operators that block on ordering
1215+ /// decisions mid-tick.
1216+ inline_hooks : Vec < Box < dyn SimInlineHook > > ,
1217+ }
1218+
1219+ impl SimTick {
1220+ /// Whether the scheduler can execute this tick right now.
1221+ fn can_run ( & self ) -> bool {
1222+ // All hooks must be ready (have received input or have a last value)...
1223+ self . hooks . iter ( ) . all ( |hook| hook. is_ready ( ) )
1224+ // ...and at least one hook must be able to release data into the tick.
1225+ && self . hooks . iter ( ) . any ( |hook| hook_can_release ( & * * hook) )
1226+ }
1227+ }
1228+
1229+ /// A top-level location whose hooks (e.g. from `assume_ordering` on a non-tick stream)
1230+ /// need scheduling decisions, but which has no tick DFIR to execute. The scheduler just
1231+ /// resolves the hooks.
1232+ struct SimObservation {
1233+ /// The top-level location, used to match this observation against the async DFIR that
1234+ /// produces its input data.
1235+ location : LocationId ,
1236+ /// The cluster member ID, if the location is a cluster.
1237+ cluster_id : Option < u32 > ,
1238+ /// Hooks resolved when the scheduler selects this observation.
1239+ hooks : Vec < Box < dyn SimHook > > ,
1240+ }
1241+
1242+ impl SimObservation {
1243+ /// Whether the scheduler can resolve any of this observation's hooks right now.
1244+ fn can_run ( & self ) -> bool {
1245+ self . hooks . iter ( ) . any ( |hook| hook_can_release ( & * * hook) )
1246+ }
1247+ }
1248+
1249+ /// Whether the hook has already decided to release data, or has pending input that would
1250+ /// allow it to decide to do so.
1251+ fn hook_can_release ( hook : & dyn SimHook ) -> bool {
1252+ hook. current_decision ( ) . unwrap_or ( false ) || hook. can_make_nontrivial_decision ( )
1253+ }
1254+
11791255/// A running simulation, which manages the async DFIRs, tick DFIRs, and hook-based
11801256/// scheduling decisions for non-deterministic operators like `batch` and `assume_ordering`.
11811257///
@@ -1192,24 +1268,16 @@ struct LaunchedSim<W: std::io::Write> {
11921268 /// Top-level async DFIRs, one per process/cluster member. These run continuously and
11931269 /// produce data that feeds into ticks and observations.
11941270 async_dfirs : Vec < ( LocationId , Option < u32 > , DfirErased ) > ,
1195- /// Tick DFIRs whose parent async DFIR has made progress, so they may be ready to run.
1271+ /// Ticks whose parent async DFIR has made progress, so they may be ready to run.
11961272 /// The scheduler further filters these by checking whether their hooks have pending decisions.
1197- possibly_ready_ticks : Vec < ( LocationId , Option < u32 > , DfirErased ) > ,
1198- /// Tick DFIRs whose parent async DFIR has not yet made progress since they were last checked.
1199- not_ready_ticks : Vec < ( LocationId , Option < u32 > , DfirErased ) > ,
1200- /// Top-level locations whose async DFIR has made progress and whose hooks (from top-level
1201- /// `assume_ordering`) may have ordering decisions to resolve. Unlike ticks, these have no
1202- /// DFIR to execute — only hook resolution.
1203- possibly_ready_observation : Vec < ( LocationId , Option < u32 > ) > ,
1204- /// Top-level locations whose async DFIR has not yet made progress since they were last checked.
1205- not_ready_observation : Vec < ( LocationId , Option < u32 > ) > ,
1206- /// Hooks keyed by (location, cluster_member_id). These are resolved *before* a tick runs
1207- /// (for `batch` hooks) or standalone (for top-level `assume_ordering` hooks via observations).
1208- hooks : Hooks < LocationId > ,
1209- /// Inline hooks keyed by (tick location, cluster_member_id). These are resolved *during*
1210- /// tick execution via a `tokio::select!` loop, for operators like `assume_ordering` inside
1211- /// a tick that block on ordering decisions while the tick DFIR is running.
1212- inline_hooks : InlineHooks < LocationId > ,
1273+ possibly_ready_ticks : Vec < SimTick > ,
1274+ /// Ticks whose parent async DFIR has not yet made progress since they were last checked.
1275+ not_ready_ticks : Vec < SimTick > ,
1276+ /// Observations whose async DFIR has made progress, so their hooks may have decisions
1277+ /// to resolve.
1278+ possibly_ready_observations : Vec < SimObservation > ,
1279+ /// Observations whose async DFIR has not yet made progress since they were last checked.
1280+ not_ready_observations : Vec < SimObservation > ,
12131281 log : LogKind < W > ,
12141282 /// Represents quiescence state of the simulation.
12151283 quiescence : Rc < QuiescenceState > ,
@@ -1227,26 +1295,17 @@ impl<W: std::io::Write> LaunchedSim<W> {
12271295 for ( loc, c_id, dfir) in & mut self . async_dfirs {
12281296 if dfir. run_tick ( ) . await {
12291297 any_made_progress = true ;
1230- let ( now_ready, still_not_ready) : ( Vec < _ > , Vec < _ > ) = self
1231- . not_ready_ticks
1232- . drain ( ..)
1233- . partition ( |( tick_loc, tick_c_id, _) | {
1234- let LocationId :: Tick ( _, outer) = tick_loc else {
1235- unreachable ! ( )
1236- } ;
1237- outer. as_ref ( ) == loc && tick_c_id == c_id
1238- } ) ;
1239-
1240- self . possibly_ready_ticks . extend ( now_ready) ;
1241- self . not_ready_ticks . extend ( still_not_ready) ;
1242-
1243- let ( now_ready_obs, still_not_ready_obs) : ( Vec < _ > , Vec < _ > ) = self
1244- . not_ready_observation
1245- . drain ( ..)
1246- . partition ( |( obs_loc, obs_c_id) | obs_loc == loc && obs_c_id == c_id) ;
1247-
1248- self . possibly_ready_observation . extend ( now_ready_obs) ;
1249- self . not_ready_observation . extend ( still_not_ready_obs) ;
1298+
1299+ // This async DFIR may have produced new data, so the ticks and observations
1300+ // it feeds may now be ready.
1301+ self . possibly_ready_ticks
1302+ . extend ( self . not_ready_ticks . extract_if ( .., |tick| {
1303+ tick. parent_location == * loc && tick. cluster_id == * c_id
1304+ } ) ) ;
1305+ self . possibly_ready_observations . extend (
1306+ self . not_ready_observations
1307+ . extract_if ( .., |obs| obs. location == * loc && obs. cluster_id == * c_id) ,
1308+ ) ;
12501309 }
12511310 }
12521311
@@ -1256,47 +1315,22 @@ impl<W: std::io::Write> LaunchedSim<W> {
12561315
12571316 use bolero:: generator:: * ;
12581317
1259- let ( ready_tick, mut not_ready_tick) : ( Vec < _ > , Vec < _ > ) = self
1260- . possibly_ready_ticks
1261- . drain ( ..)
1262- . partition ( |( name, cid, _) | {
1263- let hooks = self . hooks . get ( & ( name. clone ( ) , * cid) ) . unwrap ( ) ;
1264- // All hooks must be ready (have received input or have a last value)
1265- hooks. iter ( ) . all ( |hook| hook. is_ready ( ) )
1266- // And at least one hook must be able to make progress
1267- && hooks. iter ( ) . any ( |hook| {
1268- hook. current_decision ( ) . unwrap_or ( false )
1269- || hook. can_make_nontrivial_decision ( )
1270- } )
1271- } ) ;
1272-
1273- self . possibly_ready_ticks = ready_tick;
1274- self . not_ready_ticks . append ( & mut not_ready_tick) ;
1275-
1276- let ( ready_obs, mut not_ready_obs) : ( Vec < _ > , Vec < _ > ) = self
1277- . possibly_ready_observation
1278- . drain ( ..)
1279- . partition ( |( name, cid) | {
1280- self . hooks
1281- . get ( & ( name. clone ( ) , * cid) )
1282- . into_iter ( )
1283- . flatten ( )
1284- . any ( |hook| {
1285- hook. current_decision ( ) . unwrap_or ( false )
1286- || hook. can_make_nontrivial_decision ( )
1287- } )
1288- } ) ;
1289-
1290- self . possibly_ready_observation = ready_obs;
1291- self . not_ready_observation . append ( & mut not_ready_obs) ;
1318+ // Send anything that can't make a scheduling decision back to the not-ready lists.
1319+ self . not_ready_ticks . extend (
1320+ self . possibly_ready_ticks
1321+ . extract_if ( .., |tick| !tick. can_run ( ) ) ,
1322+ ) ;
1323+ self . not_ready_observations . extend (
1324+ self . possibly_ready_observations
1325+ . extract_if ( .., |obs| !obs. can_run ( ) ) ,
1326+ ) ;
12921327
1293- if self . possibly_ready_ticks . is_empty ( ) && self . possibly_ready_observation . is_empty ( ) {
1328+ if self . possibly_ready_ticks . is_empty ( ) && self . possibly_ready_observations . is_empty ( ) {
12941329 // If any tick is blocked because a hook is not ready, that's a
12951330 // simulator bug — it means a singleton never received a value.
1296- for ( name, cid, _) in & self . not_ready_ticks {
1297- let hooks = self . hooks . get ( & ( name. clone ( ) , * cid) ) . unwrap ( ) ;
1331+ for tick in & self . not_ready_ticks {
12981332 abort_assert ! (
1299- hooks. iter( ) . all( |hook| hook. is_ready( ) ) ,
1333+ tick . hooks. iter( ) . all( |hook| hook. is_ready( ) ) ,
13001334 "tick has a hook that never became ready"
13011335 ) ;
13021336 }
@@ -1305,17 +1339,16 @@ impl<W: std::io::Write> LaunchedSim<W> {
13051339 self . quiescence . wait_for_resume ( ) . await ;
13061340 } else {
13071341 let next_tick_or_obs = ( 0 ..( self . possibly_ready_ticks . len ( )
1308- + self . possibly_ready_observation . len ( ) ) )
1342+ + self . possibly_ready_observations . len ( ) ) )
13091343 . any ( ) ;
13101344
13111345 if next_tick_or_obs < self . possibly_ready_ticks . len ( ) {
1312- let next_tick = next_tick_or_obs;
1313- let mut removed = self . possibly_ready_ticks . remove ( next_tick) ;
1346+ let mut tick = self . possibly_ready_ticks . remove ( next_tick_or_obs) ;
13141347
13151348 match & mut self . log {
13161349 LogKind :: Null => { }
13171350 LogKind :: Stderr => {
1318- if let Some ( cid) = & removed . 1 {
1351+ if let Some ( cid) = & tick . cluster_id {
13191352 eprintln ! (
13201353 "\n {}" ,
13211354 format!( "Running Tick (Cluster Member {})" , cid)
@@ -1347,13 +1380,10 @@ impl<W: std::io::Write> LaunchedSim<W> {
13471380 } )
13481381 } ) ;
13491382
1350- let hooks = self . hooks . get_mut ( & ( removed. 0 . clone ( ) , removed. 1 ) ) . unwrap ( ) ;
1351- run_hooks ( tick_decision_writer. as_mut ( ) , hooks) ;
1383+ run_hooks ( tick_decision_writer. as_mut ( ) , & mut tick. hooks ) ;
13521384
1353- let run_tick_future = removed. 2 . run_tick ( ) ;
1354- if let Some ( inline_hooks) =
1355- self . inline_hooks . get_mut ( & ( removed. 0 . clone ( ) , removed. 1 ) )
1356- {
1385+ let run_tick_future = tick. dfir . run_tick ( ) ;
1386+ if !tick. inline_hooks . is_empty ( ) {
13571387 let mut run_tick_future_pinned = pin ! ( run_tick_future) ;
13581388
13591389 loop {
@@ -1365,7 +1395,7 @@ impl<W: std::io::Write> LaunchedSim<W> {
13651395 }
13661396 _ = async { } => {
13671397 bolero_generator:: any:: scope:: borrow_with( |driver| {
1368- for hook in inline_hooks. iter_mut( ) {
1398+ for hook in tick . inline_hooks. iter_mut( ) {
13691399 if hook. pending_decision( ) {
13701400 if !hook. has_decision( ) {
13711401 hook. autonomous_decision( driver) ;
@@ -1386,25 +1416,22 @@ impl<W: std::io::Write> LaunchedSim<W> {
13861416 abort_assert ! ( run_tick_future. await , "tick DFIR run_tick() returned false" ) ;
13871417 }
13881418
1389- self . possibly_ready_ticks . push ( removed ) ;
1419+ self . possibly_ready_ticks . push ( tick ) ;
13901420 } else {
13911421 let next_obs = next_tick_or_obs - self . possibly_ready_ticks . len ( ) ;
1392- let mut default_hooks = vec ! [ ] ;
1393- let hooks = self
1394- . hooks
1395- . get_mut ( & self . possibly_ready_observation [ next_obs] )
1396- . unwrap_or ( & mut default_hooks) ;
1397-
13981422 let log_writer = ( !matches ! ( self . log, LogKind :: Null ) ) . then_some ( & mut self . log ) ;
1399- run_hooks ( log_writer, hooks) ;
1423+ run_hooks (
1424+ log_writer,
1425+ & mut self . possibly_ready_observations [ next_obs] . hooks ,
1426+ ) ;
14001427 }
14011428 }
14021429 }
14031430}
14041431
14051432fn run_hooks < W : std:: fmt:: Write > (
14061433 mut tick_decision_writer : Option < & mut W > ,
1407- hooks : & mut Vec < Box < dyn SimHook > > ,
1434+ hooks : & mut [ Box < dyn SimHook > ] ,
14081435) {
14091436 let mut remaining_decision_count = hooks. len ( ) ;
14101437 let mut made_nontrivial_decision = false ;
0 commit comments