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