///|
fn frame_report(
capture : TaskCapture,
frame : FrameWire,
) -> TaskFrameReport raise LockError {
let catalog = process_map(frame)
let root = catalog.get(capture.root_pid)
let root_state = match root {
None => "not_observed"
Some(p) =>
if p.start_ticks != capture.start_ticks {
"identity_changed"
} else if frame.root_error != "" {
"unavailable"
} else {
"present"
}
}
let issues : Array[String] = []
if !frame.catalog_complete {
issues.push(
"Process catalog was incomplete; descendant discovery may be partial.",
)
}
if frame.root_error != "" {
issues.push("Root read status: " + frame.root_error)
}
if frame.locks_error != "" {
issues.push("Lock evidence unavailable: " + frame.locks_error)
}
let locks = parse(frame.locks)
let processes : Array[TaskProcess] = []
let members : Map[Int32, Bool] = Map([])
let blocker_pids : Map[Int32, Bool] = Map([])
if root_state == "present" {
for p in task_members(catalog, capture.root_pid) {
members[p.pid] = true
let owned = locks.matching(pid=p.pid)
let waiting = owned.filter(e => e.depth > 0)
let context = locks.with_blockers(waiting.map(e => e.line))
let blockers = context.filter(e => !waiting.any(w => w.line == e.line))
let blocker_identities : Array[String] = []
for e in blockers {
if e.owner is Process(pid) {
let id = match catalog.get(pid) {
Some(owner) => process_identity(owner)
None => "unobserved:\{pid}"
}
if !blocker_identities.contains(id) {
blocker_identities.push(id)
}
}
}
blocker_identities.sort()
for e in blockers {
if e.owner is Process(pid) {
blocker_pids[pid] = true
}
}
let files : Array[FileKey] = []
for e in owned {
if e.file is Some(key) && !files.contains(key) {
files.push(key)
}
}
let finding = if !waiting.is_empty() {
"confirmed_file_lock_wait"
} else if frame.locks_error != "" {
"lock_evidence_unavailable"
} else if p.state == "Z" {
"zombie"
} else if p.state == "T" || p.state == "t" {
"stopped_or_traced"
} else if p.state == "S" || p.state == "D" || p.state == "I" {
if p.wchan_status == "available" {
"sleeping_wait_channel_hint"
} else {
"sleeping_reason_unknown"
}
} else if p.state == "R" {
"runnable_progress_unknown"
} else {
"unclassified_process_state"
}
processes.push({
info: p,
finding,
waiting_lines: waiting.map(e => e.line),
requests: waiting,
blockers,
blocker_identities,
files,
})
}
}
let blocker_processes : Array[ProcessInfo] = []
for pid, _ in blocker_pids {
if !members.contains(pid) {
match catalog.get(pid) {
Some(p) => blocker_processes.push(p)
None =>
issues.push(
"Blocker PID \{pid} was not observable in the process catalog.",
)
}
}
}
blocker_processes.sort_by((a, b) => a.pid.compare(b.pid))
{
elapsed_ms: nonnegative(frame.elapsed_ms, 0),
root_state,
processes,
blocker_processes,
issues,
}
}
///|
fn process_identity(p : ProcessInfo) -> String {
"\{p.pid}:\{p.start_ticks}"
}
///|
/// Source lines and ordinal IDs deliberately do not participate in continuity.
fn blocker_signature(process : TaskProcess) -> String {
let parts = process.blockers.map(e => {
let json : Json = {
"kind": Json(e.kind),
"owner": Json(e.owner),
"file": Json(e.file),
"range": Json(e.range),
"mode": Json(e.mode),
"state": e.state,
}
json.stringify()
})
parts.sort()
process.blocker_identities.join(",") + ":" + parts.join("|")
}
///|
pub fn TaskCapture::report(self : TaskCapture) -> TaskReport raise LockError {
if self.frames.is_empty() {
invalid(0, "cannot report an empty task capture")
}
let frames = self.frames.map(frame => frame_report(self, frame))
let transitions : Array[TaskTransition] = []
let mut previous : Map[String, TaskProcess] = Map([])
let mut streaks : Map[String, (Int32, Int64)] = Map([])
let mut max_count = 0
let mut max_span = 0L
let next_steps : Array[String] = []
for frame in frames {
let current : Map[String, TaskProcess] = Map([])
let next_streaks : Map[String, (Int32, Int64)] = Map([])
for p in frame.processes {
let identity = process_identity(p.info)
current[identity] = p
let prior = previous.get(identity)
if prior is None {
transitions.push({
at_ms: frame.elapsed_ms,
pid: p.info.pid,
start_ticks: p.info.start_ticks,
change: "first_observed",
before: "",
after: p.finding,
})
} else if prior is Some(old) && old.finding != p.finding {
transitions.push({
at_ms: frame.elapsed_ms,
pid: p.info.pid,
start_ticks: p.info.start_ticks,
change: "finding_changed",
before: old.finding,
after: p.finding,
})
} else if prior is Some(old) &&
blocker_signature(old) != blocker_signature(p) {
transitions.push({
at_ms: frame.elapsed_ms,
pid: p.info.pid,
start_ticks: p.info.start_ticks,
change: "blocker_evidence_changed",
before: old.finding,
after: p.finding,
})
} else if prior is Some(old) &&
(
old.info.state != p.info.state ||
old.info.wchan != p.info.wchan ||
old.info.wchan_status != p.info.wchan_status
) {
transitions.push({
at_ms: frame.elapsed_ms,
pid: p.info.pid,
start_ticks: p.info.start_ticks,
change: "wait_hint_changed",
before: old.info.state + ":" + old.info.wchan,
after: p.info.state + ":" + p.info.wchan,
})
}
if p.finding == "confirmed_file_lock_wait" {
let (count, start) = match streaks.get(identity) {
Some((count, start)) => (count + 1, start)
None => (1, frame.elapsed_ms)
}
next_streaks[identity] = (count, start)
max_count = Int32::max(max_count, count)
max_span = Int64::max(max_span, frame.elapsed_ms - start)
let advice = "Inspect the recorded blocker PID and device/inode/range; verify the owner before taking action."
if !next_steps.contains(advice) {
next_steps.push(advice)
}
} else if p.finding == "sleeping_wait_channel_hint" ||
p.finding == "sleeping_reason_unknown" {
let advice = "A sleeping leader and wchan are clues only; inspect application progress, thread states and I/O evidence."
if !next_steps.contains(advice) {
next_steps.push(advice)
}
} else if p.finding == "zombie" || p.finding == "stopped_or_traced" {
let advice = "Inspect the parent process and job-control/debugger state for zombie or stopped processes."
if !next_steps.contains(advice) {
next_steps.push(advice)
}
}
}
for identity, old in previous {
if !current.contains(identity) {
transitions.push({
at_ms: frame.elapsed_ms,
pid: old.info.pid,
start_ticks: old.info.start_ticks,
change: "not_observed",
before: old.finding,
after: "not_observed",
})
}
}
if frame.root_state != "present" || !frame.issues.is_empty() {
let advice = "Review visibility/read errors and process identity; missing observations do not prove successful completion."
if !next_steps.contains(advice) {
next_steps.push(advice)
}
}
previous = current
streaks = next_streaks
}
if next_steps.is_empty() {
next_steps.push(
"No file-lock wait was confirmed in these samples; check application progress and logs.",
)
}
{
root_pid: self.root_pid,
root_start_ticks: self.start_ticks,
started_at: self.frames[0].captured_at,
sample_count: frames.length(),
observed_span_ms: frames.last().unwrap().elapsed_ms - frames[0].elapsed_ms,
max_consecutive_wait_samples: max_count,
max_sampled_wait_span_ms: max_span,
frames,
transitions,
next_steps,
}
}