Add sustained simulation validation

This commit is contained in:
George
2026-06-07 13:36:54 +01:00
parent 025487dd40
commit e23f362b12
@@ -0,0 +1,213 @@
package com.livingworld.testing;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.livingworld.config.SimulationConfig;
import com.livingworld.core.services.ServiceRegistry;
import com.livingworld.core.simulation.DefaultTimeService;
import com.livingworld.core.simulation.PersistenceService;
import com.livingworld.core.simulation.RegionManager;
import com.livingworld.core.simulation.RegionUpdateJob;
import com.livingworld.core.simulation.SimulationManager;
import com.livingworld.core.simulation.SimulationScheduler;
import com.livingworld.core.simulation.UpdateReason;
import com.livingworld.debug.SimulationProfileSnapshot;
import com.livingworld.debug.SimulationProfiler;
import com.livingworld.events.LivingWorldEventBus;
import com.livingworld.modules.ModuleContext;
import com.livingworld.modules.ModuleRegistry;
import com.livingworld.regions.Region;
import com.livingworld.regions.RegionCoordinate;
import com.livingworld.regions.RegionFactory;
import com.livingworld.regions.RegionLifecycleState;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.Test;
class LongRunSimulationTest {
private static final int REGION_COUNT = 1_000;
private static final int SIMULATION_CYCLES = 10_000;
private static final int JOBS_PER_CYCLE = 50;
private static final int SAVE_INTERVAL_CYCLES = 100;
@Test
void simulationRemainsStableUnderSustainedLoad() {
org.junit.jupiter.api.Assertions.assertTimeout(
Duration.ofSeconds(30),
this::runSustainedSimulation);
}
private void runSustainedSimulation() {
SimulationConfig config = new SimulationConfig();
config.setMaxRegionsPerCycle(JOBS_PER_CYCLE);
config.setMaxMillisecondsPerCycle(1_000);
config.setEmergencyStopMilliseconds(1_000);
SimulationScheduler scheduler = new SimulationScheduler(config);
InMemoryRegionManager regionManager = new InMemoryRegionManager();
createRegions(regionManager);
TestSimulationModule testModule = new TestSimulationModule();
ModuleRegistry moduleRegistry = new ModuleRegistry();
moduleRegistry.register(testModule);
moduleRegistry.initializeAll(new ModuleContext(new ServiceRegistry()));
DefaultTimeService timeService = new DefaultTimeService();
SimulationProfiler profiler = new SimulationProfiler();
InMemoryPersistenceService persistence =
new InMemoryPersistenceService(regionManager.allRegions());
SimulationManager simulationManager = new SimulationManager(
scheduler,
regionManager,
moduleRegistry,
new LivingWorldEventBus(),
timeService,
persistence,
profiler);
for (int cycle = 0; cycle < SIMULATION_CYCLES; cycle++) {
queueCycleJobs(scheduler, regionManager, cycle);
simulationManager.runSimulationCycle();
if ((cycle + 1) % SAVE_INTERVAL_CYCLES == 0) {
persistence.saveDirtyRegions();
}
}
persistence.saveDirtyRegions();
SimulationProfileSnapshot snapshot = profiler.createSnapshot();
assertTrue(testModule.isInitialized());
assertEquals(SIMULATION_CYCLES * JOBS_PER_CYCLE, testModule.getUpdateCount());
assertEquals(SIMULATION_CYCLES, timeService.getSimulationTick());
assertEquals(SIMULATION_CYCLES, scheduler.getSimulationTickCounter());
assertEquals(0, scheduler.getQueuedJobCount());
assertTrue(persistence.getSaveCount() > 0);
assertTrue(persistence.getMaxDirtyObserved() <= REGION_COUNT);
assertEquals(0, persistence.countDirtyRegions());
assertTrue(snapshot.totalCycleNanos() > 0);
assertTrue(snapshot.moduleTimings().containsKey(TestSimulationModule.MODULE_ID));
assertEquals(JOBS_PER_CYCLE, snapshot.regionsUpdated());
assertFalse(snapshot.budgetExceeded());
assertTrue(regionManager.allRegions().stream()
.allMatch(region -> region.getLifecycleState() == RegionLifecycleState.ACTIVE));
}
private static void createRegions(InMemoryRegionManager regionManager) {
RegionFactory factory = new RegionFactory();
for (int index = 0; index < REGION_COUNT; index++) {
Region region = factory.createNewRegion(
new RegionCoordinate("minecraft:overworld", index, 0),
0);
region.clearDirty();
regionManager.add(region);
}
}
private static void queueCycleJobs(
SimulationScheduler scheduler,
InMemoryRegionManager regionManager,
int cycle) {
int firstRegion = cycle * JOBS_PER_CYCLE;
for (int offset = 0; offset < JOBS_PER_CYCLE; offset++) {
Region region = regionManager.get((firstRegion + offset) % REGION_COUNT);
scheduler.queueRegion(new RegionUpdateJob(
region.getCoordinate(),
1,
cycle,
Set.of(TestSimulationModule.MODULE_ID),
UpdateReason.NORMAL_ROLLING_UPDATE));
}
}
private static final class InMemoryRegionManager implements RegionManager {
private final Map<RegionCoordinate, Region> regions = new LinkedHashMap<>();
private final List<Region> orderedRegions = new ArrayList<>();
private void add(Region region) {
regions.put(region.getCoordinate(), region);
orderedRegions.add(region);
}
private Region get(int index) {
return orderedRegions.get(index);
}
private Collection<Region> allRegions() {
return orderedRegions;
}
@Override
public Optional<Region> resolve(RegionCoordinate coordinate) {
return Optional.ofNullable(regions.get(coordinate));
}
@Override
public List<Region> resolveAll(List<RegionCoordinate> coordinates) {
return coordinates.stream()
.map(regions::get)
.filter(java.util.Objects::nonNull)
.toList();
}
@Override
public void markDirty(Region region) {
region.markDirty();
}
}
private static final class InMemoryPersistenceService implements PersistenceService {
private final Collection<Region> regions;
private final Map<RegionCoordinate, Region> savedRegions = new LinkedHashMap<>();
private int saveCount;
private int maxDirtyObserved;
private InMemoryPersistenceService(Collection<Region> regions) {
this.regions = regions;
}
private void saveDirtyRegions() {
maxDirtyObserved = Math.max(maxDirtyObserved, countDirtyRegions());
regions.stream()
.filter(Region::isDirty)
.forEach(this::save);
}
private int countDirtyRegions() {
return (int) regions.stream().filter(Region::isDirty).count();
}
private int getSaveCount() {
return saveCount;
}
private int getMaxDirtyObserved() {
return maxDirtyObserved;
}
@Override
public void save(Region region) {
savedRegions.put(region.getCoordinate(), region);
region.clearDirty();
saveCount++;
}
@Override
public Optional<Region> load(RegionCoordinate coordinate) {
return Optional.ofNullable(savedRegions.get(coordinate));
}
@Override
public boolean supportsMigration() {
return false;
}
}
}