Add sustained simulation validation
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user