feat(javaTest): visualize source-aligned task phases

This commit is contained in:
zgw
2026-08-10 12:28:14 +08:00
parent c8c4498ed1
commit 49c374b4b3
12 changed files with 660 additions and 74 deletions

View File

@@ -23,9 +23,12 @@ public final class HistoricalDispatchVisualizerApplication {
HistoricalQueueDataset dataset = classLoader.getResource("shadow/history/manifest.json") != null
? loader.load(classLoader, "shadow/history")
: loader.load(Paths.get("src/test/resources/shadow/history"));
HistoricalSimulationInput input = new HistoricalSimulationInputFactory().create(dataset);
HistoricalSimulationInputFactory factory = new HistoricalSimulationInputFactory();
HistoricalSimulationInput businessInput = factory.create(dataset);
HistoricalSimulationInput rawInput = factory.createRawSnapshots(dataset);
HistoricalDispatchVisualizerServer server =
new HistoricalDispatchVisualizerServer(port, dataset, input);
new HistoricalDispatchVisualizerServer(
port, dataset, businessInput, rawInput);
CountDownLatch shutdown = new CountDownLatch(1);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
server.stop();

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulationResult;
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput;
import com.ft546.wcs.shadow.history.simulation.SimulationExecutionMode;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
@@ -35,25 +36,35 @@ public final class HistoricalDispatchVisualizerServer {
private final HttpServer httpServer;
private final ExecutorService executor;
private final ObjectMapper objectMapper;
private final HistoricalSimulationInput input;
private final HistoricalSimulationInput businessInput;
private final HistoricalSimulationInput rawInput;
private final PointToPointSimulationService pointService;
private final HistoricalWorkloadSimulationService historicalService;
private final HistoricalWorkloadSimulationService businessHistoricalService;
private final HistoricalWorkloadSimulationService rawHistoricalService;
private final SimulationVisualizationMapper mapper;
private final AtomicBoolean simulationBusy = new AtomicBoolean(false);
private final AtomicBoolean started = new AtomicBoolean(false);
public HistoricalDispatchVisualizerServer(
int port, HistoricalQueueDataset dataset, HistoricalSimulationInput input)
int port, HistoricalQueueDataset dataset,
HistoricalSimulationInput businessInput, HistoricalSimulationInput rawInput)
throws IOException {
if (port < 0 || port > 65_535) {
throw new IllegalArgumentException("port must be between 0 and 65535");
}
if (dataset == null || input == null) {
throw new IllegalArgumentException("dataset and input are required");
if (dataset == null || businessInput == null || rawInput == null) {
throw new IllegalArgumentException("dataset, business input and raw input are required");
}
this.input = input;
this.pointService = new PointToPointSimulationService(input);
this.historicalService = new HistoricalWorkloadSimulationService(input);
if (businessInput.getExecutionMode()
!= SimulationExecutionMode.SOURCE_ALIGNED_BUSINESS
|| rawInput.getExecutionMode() != SimulationExecutionMode.RAW_SNAPSHOT) {
throw new IllegalArgumentException("historical inputs do not match their replay modes");
}
this.businessInput = businessInput;
this.rawInput = rawInput;
this.pointService = new PointToPointSimulationService(businessInput);
this.businessHistoricalService = new HistoricalWorkloadSimulationService(businessInput);
this.rawHistoricalService = new HistoricalWorkloadSimulationService(rawInput);
this.mapper = new SimulationVisualizationMapper(dataset);
this.objectMapper = new ObjectMapper();
this.httpServer = HttpServer.create(new InetSocketAddress(
@@ -131,14 +142,14 @@ public final class HistoricalDispatchVisualizerServer {
Map<String, Object> health = new LinkedHashMap<String, Object>();
health.put("status", "UP");
health.put("mode", "OFFLINE_IN_MEMORY_SIMULATION");
health.put("mapLoaded", !input.getGridMap().getSites().isEmpty());
health.put("mapLoaded", !businessInput.getGridMap().getSites().isEmpty());
health.put("simulationBusy", simulationBusy.get());
writeJson(exchange, 200, health);
return;
}
if ("/api/bootstrap".equals(path)) {
requireMethod(method, "GET");
writeJson(exchange, 200, mapper.bootstrap(input));
writeJson(exchange, 200, mapper.bootstrap(businessInput));
return;
}
if ("/api/simulations/point-to-point".equals(path)) {
@@ -147,15 +158,19 @@ public final class HistoricalDispatchVisualizerServer {
PointToPointSimulationRequest request = objectMapper.readValue(
body, PointToPointSimulationRequest.class);
writeJson(exchange, 200, runSimulation(() ->
mapper.result(input, pointService.run(request))));
mapper.result(businessInput, pointService.run(request))));
return;
}
if ("/api/simulations/historical".equals(path)) {
requireMethod(method, "POST");
readBody(exchange);
HistoricalSimulationRequest request = objectMapper.readValue(
readBody(exchange), HistoricalSimulationRequest.class);
writeJson(exchange, 200, runSimulation(() -> {
HistoricalDispatchSimulationResult result = historicalService.run();
return mapper.result(input, result);
boolean raw = request.getMode() == SimulationExecutionMode.RAW_SNAPSHOT;
HistoricalSimulationInput selectedInput = raw ? rawInput : businessInput;
HistoricalDispatchSimulationResult result = raw
? rawHistoricalService.run() : businessHistoricalService.run();
return mapper.result(selectedInput, result);
}));
return;
}

View File

@@ -0,0 +1,33 @@
package com.ft546.wcs.shadow.history.visualizer;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ft546.wcs.shadow.history.simulation.SimulationExecutionMode;
/**
* 选择历史回放的审计口径。
*
* <p>业务模式把重复队列快照归并为真实父任务并执行二段式阶段;RAW 模式保留原先
* 240 行逐条运行的兼容结果。空请求默认进入业务模式,避免页面误把快照数当成业务任务数。
*/
public final class HistoricalSimulationRequest {
@JsonProperty("mode")
private String mode;
/** 供 Jackson 反序列化请求体使用。 */
public HistoricalSimulationRequest() {
}
/** 返回经过校验的回放口径;未传值时默认按生产源码语义回放。 */
public SimulationExecutionMode getMode() {
String normalized = mode == null || mode.trim().isEmpty()
? SimulationExecutionMode.SOURCE_ALIGNED_BUSINESS.name()
: mode.trim().toUpperCase();
try {
return SimulationExecutionMode.valueOf(normalized);
} catch (IllegalArgumentException exception) {
throw new IllegalArgumentException(
"INVALID_EXECUTION_MODE: mode must be SOURCE_ALIGNED_BUSINESS or RAW_SNAPSHOT",
exception);
}
}
}

View File

@@ -7,7 +7,7 @@ import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput;
import com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway;
import com.ft546.wcs.shadow.history.simulation.SimulationConfig;
/** 重新运行由 240 条历史快照组装出的业务父任务,并保留既定的确定性 PLC 故障注入。 */
/** 按调用方选择的不可变输入运行历史回放,并保留既定的确定性 PLC 故障注入。 */
public final class HistoricalWorkloadSimulationService {
private final HistoricalSimulationInput input;
@@ -17,6 +17,7 @@ public final class HistoricalWorkloadSimulationService {
}
public HistoricalDispatchSimulationResult run() {
// 每种模式都对自身任务序列稳定注入故障,保证业务/RAW 对比可重复而不共享运行状态。
DeterministicFaultPlan.Builder faults = DeterministicFaultPlan.builder();
for (int i = 0; i < input.getTasks().size(); i += 37) {
faults.plcFailure(input.getTasks().get(i).getSimulationTaskId(), 1,

View File

@@ -8,6 +8,7 @@ import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulationResult;
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput;
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationTask;
import com.ft546.wcs.shadow.history.simulation.HistoricalTaskSnapshotEvidence;
import com.ft546.wcs.shadow.history.simulation.FloorTransferJob;
import com.ft546.wcs.shadow.history.simulation.RoutePlanningEvidence;
import com.ft546.wcs.shadow.history.simulation.RerouteDecisionEvidence;
@@ -16,6 +17,7 @@ import com.ft546.wcs.shadow.history.simulation.SimulationMetrics;
import com.ft546.wcs.shadow.history.simulation.SimulationTaskResult;
import com.ft546.wcs.shadow.history.simulation.SimulationTaskStatus;
import com.ft546.wcs.shadow.history.simulation.SimulationTickEvent;
import com.ft546.wcs.shadow.history.simulation.TaskPhaseEvidence;
import com.ft546.wcs.demo.traffic.RouteCandidateEvaluation;
import com.ft546.wcs.demo.traffic.RoutePlanDecision;
import com.ft546.wcs.demo.traffic.RouteQuality;
@@ -121,6 +123,7 @@ public final class SimulationVisualizationMapper {
payload.put("plannerVersion", PLANNER_VERSION);
payload.put("mapSha256", dataset.getFullMapSha256());
payload.put("pathInfoUsedForPlanning", false);
payload.put("executionMode", result.getExecutionMode().name());
payload.put("summary", summary(result, definitions, input.getGridMap()));
payload.put("tasks", tasks);
payload.put("events", events);
@@ -135,6 +138,13 @@ public final class SimulationVisualizationMapper {
GridMap gridMap) {
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("inputCount", result.getTaskResults().size());
value.put("rawSnapshotCount", result.getInputAudit().getRawSnapshotCount());
value.put("uniqueQueueCount", result.getInputAudit().getUniqueQueueCount());
value.put("duplicateSnapshotCount", result.getInputAudit().getDuplicateSnapshotCount());
value.put("businessTaskCount", result.getInputAudit().getBusinessTaskCount());
value.put("operationalQueueCount", result.getInputAudit().getOperationalQueueCount());
value.put("incompleteVehicleReleaseCount",
result.getIncompleteVehicleReleaseCount());
value.put("acceleration", result.getAcceleration());
value.put("completedCount", result.getCompletedCount());
value.put("waitingCount", result.getWaitingCount());
@@ -199,6 +209,8 @@ public final class SimulationVisualizationMapper {
value.put("guid", record.getGuid());
value.put("taskType", definition == null
? record.getTaskType() : definition.getTaskType().name());
value.put("businessType", definition == null
? record.getTaskType() : definition.getTaskType().name());
value.put("priority", record.getPriority());
value.put("businessStartSite", record.getStartSite());
value.put("businessEndSite", record.getEndSite());
@@ -206,6 +218,10 @@ public final class SimulationVisualizationMapper {
? record.getStartSite() : definition.getPhysicalSource());
value.put("physicalEndSite", definition == null
? record.getEndSite() : definition.getPhysicalTarget());
value.put("sourceFrontSite", definition == null
? null : definition.getSourceFrontSite());
value.put("targetFrontSite", definition == null
? null : definition.getTargetFrontSite());
value.put("historicalVehicleId", record.getAgvId());
value.put("simulatedVehicleId", task.getSimulatedVehicleId());
value.put("assignmentMode", task.getAssignmentMode() == null
@@ -224,6 +240,13 @@ public final class SimulationVisualizationMapper {
value.put("arrivalMillis", task.getArrivalMillis());
value.put("firstAssignedMillis", task.getFirstAssignedMillis());
value.put("completedMillis", task.getCompletedMillis());
value.put("businessCompletedMillis", task.getBusinessCompletedMillis());
value.put("vehicleReleasedMillis", task.getVehicleReleasedMillis());
value.put("vehicleReleaseStatus", task.getVehicleReleaseStatus().name());
value.put("cargoState", task.getCargoState().name());
value.put("skippedStageReasons", task.getSkippedStageReasons());
value.put("snapshotEvidence", snapshotEvidence(definition));
value.put("phaseEvidence", phaseEvidence(task.getPhaseEvidence()));
value.put("waitingMillis", task.getWaitingMillis());
value.put("simulationDurationMillis", simulatedDuration);
value.put("historicalDurationMillis", historicalDuration);
@@ -259,6 +282,42 @@ public final class SimulationVisualizationMapper {
return value;
}
/** 输出父任务关联的原始空车、载货和重复扫描记录,供页面追溯 GUID,而不参与重新规划。 */
private List<Map<String, Object>> snapshotEvidence(HistoricalSimulationTask definition) {
List<Map<String, Object>> values = new ArrayList<Map<String, Object>>();
if (definition == null) return values;
for (HistoricalTaskSnapshotEvidence evidence : definition.getSnapshotEvidence()) {
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("snapshotRow", evidence.getSnapshotRow());
value.put("guid", evidence.getGuid());
value.put("queueTaskType", evidence.getQueueTaskType());
value.put("default02", evidence.getDefault02());
value.put("historicalVehicleId", evidence.getHistoricalVehicleId());
value.put("historicalPath", historicalPath(evidence.getHistoricalPath()));
values.add(value);
}
return values;
}
/** 输出仿真器冻结的逐阶段真实路径;前端只选择和绘制,不重新推导业务阶段。 */
private List<Map<String, Object>> phaseEvidence(List<TaskPhaseEvidence> evidence) {
List<Map<String, Object>> values = new ArrayList<Map<String, Object>>();
for (TaskPhaseEvidence phase : evidence) {
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("phase", phase.getPhase().name());
value.put("fromSite", phase.getFromSite());
value.put("toSite", phase.getToSite());
value.put("plannedPath", phase.getPlannedPath());
value.put("startedMillis", phase.getStartedMillis());
value.put("completedMillis", phase.getCompletedMillis());
value.put("holdCount", phase.getHoldCount());
value.put("retryCount", phase.getRetryCount());
value.put("completionReason", phase.getCompletionReason());
values.add(value);
}
return values;
}
/**
* 汇总父任务换层作业经过的楼层,供页面楼层选择器标记整条业务链涉及的地图。
*/