Harden simulation profile snapshots
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package com.livingworld.debug;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
/**
|
||||
* Immutable snapshot of one simulation profiling cycle.
|
||||
@@ -24,12 +26,26 @@ public record SimulationProfileSnapshot(
|
||||
if (eventsPublished < 0 || regionsUpdated < 0 || savesPerformed < 0) {
|
||||
throw new IllegalArgumentException("profile counts must not be negative");
|
||||
}
|
||||
moduleTimings = Map.copyOf(new LinkedHashMap<>(moduleTimings));
|
||||
LinkedHashMap<String, Long> timingCopy = new LinkedHashMap<>();
|
||||
moduleTimings.forEach((moduleId, timingNanos) -> {
|
||||
if (moduleId == null || moduleId.isBlank()) {
|
||||
throw new IllegalArgumentException("Module IDs must not be blank");
|
||||
}
|
||||
if (timingNanos == null || timingNanos < 0) {
|
||||
throw new IllegalArgumentException("Module timings must be non-negative");
|
||||
}
|
||||
timingCopy.put(moduleId, timingNanos);
|
||||
});
|
||||
moduleTimings = Collections.unmodifiableMap(timingCopy);
|
||||
}
|
||||
|
||||
public String toHumanReadableString() {
|
||||
StringJoiner formattedTimings = new StringJoiner(", ", "{", "}");
|
||||
moduleTimings.forEach((moduleId, timingNanos) ->
|
||||
formattedTimings.add(moduleId + "=" + timingNanos / 1_000_000.0 + "ms"));
|
||||
|
||||
return "cycle=" + (totalCycleNanos / 1_000_000.0) + "ms"
|
||||
+ ", modules=" + moduleTimings
|
||||
+ ", modules=" + formattedTimings
|
||||
+ ", events=" + eventsPublished
|
||||
+ ", regions=" + regionsUpdated
|
||||
+ ", saves=" + savesPerformed
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.livingworld.debug;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SimulationProfileSnapshotTest {
|
||||
|
||||
@Test
|
||||
void defensivelyCopiesAndLocksModuleTimings() {
|
||||
Map<String, Long> timings = new LinkedHashMap<>();
|
||||
timings.put("ecology", 2_000_000L);
|
||||
|
||||
SimulationProfileSnapshot snapshot =
|
||||
new SimulationProfileSnapshot(3_000_000L, timings, 2, 3, 1, false);
|
||||
timings.put("settlements", 1_000_000L);
|
||||
|
||||
assertEquals(Map.of("ecology", 2_000_000L), snapshot.moduleTimings());
|
||||
assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> snapshot.moduleTimings().put("weather", 500_000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidValues() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new SimulationProfileSnapshot(-1, Map.of(), 0, 0, 0, false));
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new SimulationProfileSnapshot(0, Map.of("", 1L), 0, 0, 0, false));
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new SimulationProfileSnapshot(0, Map.of("test", -1L), 0, 0, 0, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatsMeasurementsForDebugOutput() {
|
||||
SimulationProfileSnapshot snapshot = new SimulationProfileSnapshot(
|
||||
3_000_000L, Map.of("test", 2_000_000L), 2, 3, 1, true);
|
||||
|
||||
String output = snapshot.toHumanReadableString();
|
||||
|
||||
assertTrue(output.contains("cycle=3.0ms"));
|
||||
assertTrue(output.contains("test=2.0ms"));
|
||||
assertTrue(output.contains("events=2"));
|
||||
assertTrue(output.contains("budgetExceeded=true"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user