Harden simulation profile snapshots

This commit is contained in:
George
2026-06-07 13:31:39 +01:00
parent 2202ff0680
commit 0ce71b727a
2 changed files with 71 additions and 2 deletions
@@ -1,7 +1,9 @@
package com.livingworld.debug; package com.livingworld.debug;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.StringJoiner;
/** /**
* Immutable snapshot of one simulation profiling cycle. * Immutable snapshot of one simulation profiling cycle.
@@ -24,12 +26,26 @@ public record SimulationProfileSnapshot(
if (eventsPublished < 0 || regionsUpdated < 0 || savesPerformed < 0) { if (eventsPublished < 0 || regionsUpdated < 0 || savesPerformed < 0) {
throw new IllegalArgumentException("profile counts must not be negative"); 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() { 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" return "cycle=" + (totalCycleNanos / 1_000_000.0) + "ms"
+ ", modules=" + moduleTimings + ", modules=" + formattedTimings
+ ", events=" + eventsPublished + ", events=" + eventsPublished
+ ", regions=" + regionsUpdated + ", regions=" + regionsUpdated
+ ", saves=" + savesPerformed + ", 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"));
}
}