@@ -209,6 +209,7 @@ def scan(
209209 * ,
210210 now_monotonic_s : float ,
211211 now_unix_ns : int ,
212+ trace_source_issues : dict [tuple [int , int ], tuple [str , ...]] | None = None ,
212213 ) -> list [Finding ]:
213214 findings : list [Finding ] = []
214215 if self .detect_heartbeat_timeouts :
@@ -242,6 +243,7 @@ def scan(
242243 elapsed_s = elapsed_s ,
243244 detection_phase = detection_phase ,
244245 timeout_s = timeout_s ,
246+ trace_source_issues = trace_source_issues ,
245247 )
246248 self ._emit_once (("collective_missing_enter" , * key ), missing , findings )
247249
@@ -366,22 +368,23 @@ def _detect_missing_enter(
366368 elapsed_s : float ,
367369 detection_phase : str ,
368370 timeout_s : float ,
371+ trace_source_issues : dict [tuple [int , int ], tuple [str , ...]] | None ,
369372 ) -> Finding :
370373 entered = sorted (collective .enters )
371374 missing = sorted (set (range (collective .expected_nranks )) - set (entered ))
372375 members = self ._comm_members .get ((collective .run_id , collective .comm_uid_hash ), {})
373376 rank_status : list [dict [str , Any ]] = []
374377 known_states : list [str ] = []
375- trace_event_loss_possible = False
378+ source_states : list [ str ] = []
376379 for comm_rank in missing :
377380 global_rank = members .get (comm_rank )
378381 status : dict [str , Any ] = {"comm_rank" : comm_rank , "rank" : global_rank }
379382 heartbeat = self ._heartbeats .get (global_rank ) if global_rank is not None else None
380- probe_status = self ._probe_status . get ( global_rank ) if global_rank is not None else None
381- dropped_events = _as_int (
382- probe_status . get ( "dropped_events" ) if probe_status else 0 , default = 0
383+ source = self ._trace_source_status (
384+ global_rank ,
385+ trace_source_issues = trace_source_issues ,
383386 )
384- trace_event_loss_possible = trace_event_loss_possible or dropped_events > 0
387+ source_states . append ( str ( source [ "status" ]))
385388 if heartbeat is None :
386389 status ["heartbeat" ] = "unknown"
387390 else :
@@ -395,21 +398,35 @@ def _detect_missing_enter(
395398 }
396399 )
397400 known_states .append (state )
398- status ["probe_dropped_events" ] = dropped_events
401+ status ["probe_dropped_events" ] = source ["dropped_events" ]
402+ status ["trace_source" ] = source
399403 rank_status .append (status )
400404
401- if trace_event_loss_possible :
405+ if "unavailable" in source_states :
406+ source_coverage = "insufficient"
407+ reason = "trace_source_unavailable"
408+ confidence = "unknown"
409+ elif "incomplete" in source_states :
410+ source_coverage = "insufficient"
411+ reason = "trace_source_incomplete"
412+ confidence = "unknown"
413+ elif "lossy" in source_states :
414+ source_coverage = "degraded"
402415 reason = "probe_event_loss_possible"
403416 confidence = "suspected"
404- elif "stale" in known_states :
405- reason = "rank_exit_or_crash_suspected"
406- confidence = "suspected"
407- elif known_states and all (state == "alive" for state in known_states ):
408- reason = "rank_alive_but_not_entered"
409- confidence = "observed"
410417 else :
411- reason = "missing_rank_status_unknown"
412- confidence = "observed"
418+ source_coverage = "sufficient"
419+ if "stale" in known_states :
420+ reason = "rank_exit_or_crash_suspected"
421+ confidence = "suspected"
422+ elif known_states and all (state == "alive" for state in known_states ):
423+ reason = "rank_alive_but_not_entered"
424+ confidence = "observed"
425+ else :
426+ reason = "missing_rank_status_unknown"
427+ confidence = "observed"
428+
429+ trace_event_loss_possible = source_coverage != "sufficient"
413430
414431 first_event = next (iter (collective .enters .values ()))
415432 return Finding (
@@ -430,10 +447,77 @@ def _detect_missing_enter(
430447 "threshold_reason" : self ._threshold_reason (detection_phase ),
431448 "reason" : reason ,
432449 "confidence" : confidence ,
450+ "trace_source_coverage" : source_coverage ,
433451 "trace_event_loss_possible" : trace_event_loss_possible ,
434452 },
435453 )
436454
455+ def _trace_source_status (
456+ self ,
457+ global_rank : int | None ,
458+ * ,
459+ trace_source_issues : dict [tuple [int , int ], tuple [str , ...]] | None ,
460+ ) -> dict [str , Any ]:
461+ """Describe whether absence of a trace event is usable evidence."""
462+
463+ issues : list [str ] = []
464+ if global_rank is None :
465+ return {
466+ "status" : "unavailable" ,
467+ "pid" : None ,
468+ "dropped_events" : 0 ,
469+ "issues" : ["communicator_rank_not_mapped" ],
470+ }
471+
472+ process = self ._processes .get (global_rank )
473+ probe_status = self ._probe_status .get (global_rank )
474+ process_pid = _as_int (process .event .get ("pid" ), default = - 1 ) if process is not None else - 1
475+ status_pid = (
476+ _as_int (probe_status .get ("pid" ), default = - 1 ) if probe_status is not None else - 1
477+ )
478+ if process is None :
479+ issues .append ("process_start_not_observed" )
480+ if probe_status is None :
481+ issues .append ("probe_status_not_observed" )
482+ if process_pid >= 0 and status_pid >= 0 and process_pid != status_pid :
483+ issues .append ("probe_status_pid_mismatch" )
484+
485+ pid = process_pid if process_pid >= 0 else status_pid
486+ if trace_source_issues is not None and pid >= 0 :
487+ source_key = (global_rank , pid )
488+ if source_key not in trace_source_issues :
489+ issues .append ("trace_file_not_observed" )
490+ else :
491+ issues .extend (trace_source_issues [source_key ])
492+
493+ dropped_events = _as_int (
494+ probe_status .get ("dropped_events" ) if probe_status else 0 ,
495+ default = 0 ,
496+ )
497+ unavailable_issues = {
498+ "process_start_not_observed" ,
499+ "probe_status_not_observed" ,
500+ "probe_status_pid_mismatch" ,
501+ "trace_file_not_observed" ,
502+ }
503+ if unavailable_issues .intersection (issues ):
504+ source_status = "unavailable"
505+ elif issues :
506+ source_status = "incomplete"
507+ elif dropped_events > 0 :
508+ source_status = "lossy"
509+ else :
510+ # This proves that the source was loaded and that the collector has
511+ # observed no concrete loss signal. It intentionally does not claim
512+ # continuous writer liveness.
513+ source_status = "available"
514+ return {
515+ "status" : source_status ,
516+ "pid" : pid if pid >= 0 else None ,
517+ "dropped_events" : dropped_events ,
518+ "issues" : sorted (set (issues )),
519+ }
520+
437521 def _collective_detection_phase (self , collective : _CollectiveRound ) -> str :
438522 """Return a stable phase for one collective round.
439523
0 commit comments