11from enum import Enum , auto
2- import logging
32import os
43import re
54import executor
65from executor import ExecCtxt , ExecResult , ExecArgs
76import dep_util
87import util
98import signal
9+ import threading
1010from dataclasses import dataclass
1111from typing import Tuple
12- from enum import Enum , auto
1312from pathlib import Path
14- import util
1513import analysis
1614
1715STATE_LOG = '[STATE_LOG] '
@@ -292,12 +290,9 @@ class ConcreteNode:
292290 # Exists when node is in READY
293291 assignments : "list[NodeId]"
294292
295- # Open file descriptors for the streaming dep files (opened lazily in gather_fs_actions)
296- read_stream_fd : object
297- write_stream_fd : object
298- # Leftover partial line from each stream file during incremental reads
299- read_stream_buf : str
300- write_stream_buf : str
293+ # Daemon reader threads — one per FIFO; started after exec_ctxt is set
294+ _reader_r : "threading.Thread"
295+ _reader_w : "threading.Thread"
301296
302297 def __init__ (self , cnid : ConcreteNodeId , node : Node , loop_list_context : HSLoopListContext ,
303298 spec_pre_env = None ):
@@ -454,45 +449,44 @@ def kill(self):
454449 self .exec_ctxt .process .kill ()
455450
456451 def _reset_stream_state (self ):
457- self .read_stream_fd = None
458- self .write_stream_fd = None
459- self .read_stream_buf = ''
460- self .write_stream_buf = ''
461-
462- def _open_stream_fd (self , suffix ):
463- path = util .sandboxed_path (self .exec_ctxt .sandbox_dir ,
464- self .exec_ctxt .trace_file + suffix )
452+ for t in (getattr (self , '_reader_r' , None ), getattr (self , '_reader_w' , None )):
453+ if t is not None and t .is_alive ():
454+ t .join (timeout = 0.5 )
455+ self ._reader_r = None
456+ self ._reader_w = None
457+
458+ def _run_stream_reader (self , path : str , is_write : bool ):
465459 try :
466- return open (path , 'r' )
467- except FileNotFoundError :
468- return None
469-
470- def _drain_stream (self , fd , buf : str ) -> tuple [set , str ]:
471- """Read new content from fd; return (complete paths, leftover partial line)."""
472- chunk = fd .read ()
473- if not chunk :
474- return set (), buf
475- buf += chunk
476- * complete , buf = buf .split ('\n ' )
477- paths = {p for line in complete if (p := line .strip ()) and not dep_util .should_filter (p )}
478- return paths , buf
479-
480- def gather_fs_actions (self ):
481- assert self .state in [NodeState .EXECUTING , NodeState .SPEC_EXECUTING ]
482- if self .read_stream_fd is None :
483- self .read_stream_fd = self ._open_stream_fd ('.r' )
484- if self .write_stream_fd is None :
485- self .write_stream_fd = self ._open_stream_fd ('.w' )
486- read_paths , write_paths = set (), set ()
487- if self .read_stream_fd :
488- read_paths , self .read_stream_buf = self ._drain_stream (self .read_stream_fd , self .read_stream_buf )
489- if self .write_stream_fd :
490- write_paths , self .write_stream_buf = self ._drain_stream (self .write_stream_fd , self .write_stream_buf )
491- self .update_rw_set (read_paths , write_paths )
460+ fd = open (path , 'r' )
461+ except OSError :
462+ return
463+ try :
464+ for line in fd :
465+ p = line .rstrip ('\n ' )
466+ if p and not dep_util .should_filter (p ):
467+ if is_write :
468+ self .rwset .write_set .add (p )
469+ else :
470+ self .rwset .read_set .add (p )
471+ finally :
472+ fd .close ()
473+
474+ def _start_reader_threads (self ):
475+ sandbox_dir = self .exec_ctxt .sandbox_dir
476+ trace_file = self .exec_ctxt .trace_file
477+ for suffix , is_write , attr in (('.r' , False , '_reader_r' ), ('.w' , True , '_reader_w' )):
478+ path = util .sandboxed_path (sandbox_dir , trace_file + suffix )
479+ t = threading .Thread (target = self ._run_stream_reader , args = (path , is_write ),
480+ daemon = True )
481+ t .start ()
482+ setattr (self , attr , t )
483+
484+ def _join_reader_threads (self ):
485+ for t in (getattr (self , '_reader_r' , None ), getattr (self , '_reader_w' , None )):
486+ if t is not None :
487+ t .join (timeout = 1.0 )
492488
493489 def get_rw_set (self ):
494- # if self.state in [NodeState.EXECUTING, NodeState.SPEC_EXECUTING]:
495- # self.gather_fs_actions()
496490 return self .rwset
497491
498492 fd_line = re .compile (r'(\d+) ([rwd]) (\d+) (.+)' )
@@ -550,7 +544,7 @@ def parse_env(content):
550544 function_body_lines .append (line )
551545 if line == '}' :
552546 inside_function = False
553- if not current_function in ignore_vars :
547+ if current_function not in ignore_vars :
554548 env_vars [current_function ] = '\n ' .join (function_body_lines )
555549 function_body_lines = []
556550 return env_vars
@@ -683,17 +677,16 @@ def start_executing(self, env_file):
683677 self .start_command (env_file )
684678 self .state = NodeState .EXECUTING
685679 self ._reset_stream_state ()
686-
687680 self .rwset = RWSet (set (), set ())
681+ self ._start_reader_threads ()
688682 self .trace_state ()
689683
690684 def start_spec_executing (self , env_file , speculated_nodes ):
691- # raise NotImplementedError
692685 assert self .state == NodeState .READY
693686 self .start_command (env_file , speculate = True , speculated_nodes = speculated_nodes )
694687 self ._reset_stream_state ()
695-
696688 self .rwset = RWSet (set (), set ())
689+ self ._start_reader_threads ()
697690 self .state = NodeState .SPEC_EXECUTING
698691 self .trace_state ()
699692
@@ -703,13 +696,11 @@ def collect_result(self):
703696 self .exec_result = ExecResult (self .exec_ctxt .process .returncode , self .exec_ctxt .process .pid )
704697 return self .exec_result .exit_code == 137 , self .runtime_finished ()
705698
706- def commit_frontier_execution (self ):
699+ def commit_frontier_execution (self ) -> int :
700+ """Commit the frontier node and return the missed-event count from trace_v3."""
707701 assert self .state == NodeState .EXECUTING
708- self .gather_fs_actions ()
709- if self .read_stream_fd :
710- self .read_stream_fd .close ()
711- if self .write_stream_fd :
712- self .write_stream_fd .close ()
702+ self ._join_reader_threads ()
703+ missed = dep_util .read_missed (self .exec_ctxt .sandbox_dir , self .exec_ctxt .trace_file )
713704 self .update_loop_list_context ()
714705 util .overhead_log (f"COMMIT|{ self .cnid } " )
715706 executor .commit_workspace (self .exec_ctxt .sandbox_dir )
@@ -719,16 +710,13 @@ def commit_frontier_execution(self):
719710 self .fixup_fds ()
720711 self .state = NodeState .COMMITTED
721712 self .trace_state ()
713+ return missed
722714
723715 def finish_spec_execution (self ) -> bool :
724- """Drain stream files, close them, and return True if trace_v3 missed events."""
716+ """Join reader threads and return True if trace_v3 missed events."""
725717 assert self .state == NodeState .SPEC_EXECUTING
726718 self .update_loop_list_context ()
727- self .gather_fs_actions ()
728- if self .read_stream_fd :
729- self .read_stream_fd .close ()
730- if self .write_stream_fd :
731- self .write_stream_fd .close ()
719+ self ._join_reader_threads ()
732720 missed = dep_util .read_missed (self .exec_ctxt .sandbox_dir , self .exec_ctxt .trace_file )
733721 self .fixup_fds ()
734722 self .state = NodeState .SPECULATED
0 commit comments