package com.ruoyi.shadow.kernel; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; import java.util.function.LongSupplier; /** One bounded diagnostic snapshot per poll, including polls with no task evidence. */ final class ShadowCycleDiagnostics { private final LongSupplier ticker; private final long started; private long phaseStarted; private String phase = "QUEUED"; private final Map phaseNanos = new LinkedHashMap(); private Map completed; private boolean cancelled; private Map dispatch = Collections.emptyMap(); ShadowCycleDiagnostics() { this(System::nanoTime); } ShadowCycleDiagnostics(LongSupplier ticker) { this.ticker = ticker; this.started = ticker.getAsLong(); this.phaseStarted = started; } synchronized void dispatch(com.ruoyi.shadow.facts.ShadowFactBatch batch, com.ruoyi.wcs.application.dispatch.DispatchSnapshot snapshot, com.ruoyi.wcs.application.dispatch.DispatchCycleResult result) { if (completed != null || cancelled) return; Map values = new LinkedHashMap(); values.put("eligiblePhases", snapshot.getReadySteps().size()); values.put("granted", result.getGranted().size()); values.put("held", result.getHeld().size()); values.put("yielded", result.getYielded().size()); Map observations = new LinkedHashMap(); for (com.ruoyi.shadow.facts.ShadowQueueFact queue : batch.getQueues()) { if (!queue.isActive()) continue; String reason = com.ruoyi.shadow.facts.ShadowQueueDispatchEligibility.observationReason(queue, batch); if (reason != null) observations.merge(reason, 1, Integer::sum); } values.put("observations", Collections.unmodifiableMap(observations)); int safe = 0; for (String id : snapshot.getTopology().getNodeIds()) { com.ruoyi.wcs.domain.topology.TopologyNode node = snapshot.getTopology().node(id); if (node.isSafeWait() || node.isSafeYield()) safe++; } values.put("validatedSafePointCount", safe); values.put("safePointStatus", safe == 0 ? "UNCONFIGURED" : "TOPOLOGY_VALIDATED"); List> holds = new ArrayList>(); for (com.ruoyi.wcs.application.dispatch.HeldDispatch hold : result.getHeld()) { if (holds.size() == 20) break; Map row = new LinkedHashMap(); row.put("taskId", hold.getOrderId()); row.put("vehicleId", hold.getVehicleId()); row.put("reasonCode", hold.getReasonCode()); List blockers = new ArrayList(hold.getBlockingVehicleIds()); row.put("blockingVehicleIds", Collections.unmodifiableList(blockers.subList(0, Math.min(20, blockers.size())))); holds.add(Collections.unmodifiableMap(row)); } values.put("holdSamples", Collections.unmodifiableList(holds)); dispatch = Collections.unmodifiableMap(values); } synchronized void enter(String next) { if (cancelled || Thread.currentThread().isInterrupted()) { throw new CancellationException("SHADOW_CYCLE_CANCELLED"); } if (completed != null) return; long now = ticker.getAsLong(); accumulate(now); phase = next; phaseStarted = now; } synchronized Map finish(String outcome, Thread worker) { if (completed != null) return completed; long now = ticker.getAsLong(); accumulate(now); Map snapshot = new LinkedHashMap(); snapshot.put("outcome", outcome); snapshot.put("phase", phase); snapshot.put("elapsedMillis", TimeUnit.NANOSECONDS.toMillis(now - started)); Map timings = new LinkedHashMap(); phaseNanos.forEach((key, value) -> timings.put(key, TimeUnit.NANOSECONDS.toMillis(value))); snapshot.put("phaseMillis", Collections.unmodifiableMap(timings)); snapshot.put("dispatch", dispatch); if (worker != null) { snapshot.put("workerState", worker.getState().name()); List stack = new ArrayList(); StackTraceElement[] frames = worker.getStackTrace(); for (int i = 0; i < Math.min(16, frames.length); i++) stack.add(frames[i].toString()); snapshot.put("workerStack", Collections.unmodifiableList(stack)); } completed = Collections.unmodifiableMap(snapshot); return completed; } synchronized Map cancel(String outcome, Thread worker) { cancelled = true; return finish(outcome, worker); } private void accumulate(long now) { phaseNanos.merge(phase, now - phaseStarted, Long::sum); } }