Hydrology occupancy and lookahead

- Index planned tiles by occupied chunk; empty chunks skip composition, tile residency and cave-cell lookups
- Plan the whole pregen area nearest first with several roots, past the tile budget when plans persist
- Restore persisted tiles without a planning root permit
- Never memoize answers the server thread gave before its hydrology plan landed
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-28 11:45:53 -04:00
1 parent 241260a762
commit d59517aeaa
18 files changed
+1006 -190

No files matched your search

@@ -0,0 +1,52 @@
package art.arcane.iris.generation.hydrology;
import org.junit.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class HydrologyChunkOccupancyTest {
@Test
public void occupiesExactlyTheChunksHoldingFootprintColumnsAtSignedCoordinates() {
int[][] columns = {{0, 0}, {15, 15}, {-1, -1}, {-17, 40}, {1000, -2000}, {-4097, 4096}};
HydrologyChunkOccupancy occupancy = HydrologyChunkOccupancy.of(footprint(columns));
HashSet<Long> expected = new HashSet<>();
for (int[] column : columns) {
expected.add(RiverFootprint.pack(Math.floorDiv(column[0], 16), Math.floorDiv(column[1], 16)));
}
int occupied = 0;
for (int chunkZ = -300; chunkZ <= 300; chunkZ++) {
for (int chunkX = -300; chunkX <= 300; chunkX++) {
boolean holds = occupancy.occupies(chunkX, chunkZ);
assertEquals(expected.contains(RiverFootprint.pack(chunkX, chunkZ)), holds);
occupied += holds ? 1 : 0;
}
}
assertEquals(expected.size(), occupied);
assertFalse(occupancy.occupies(Integer.MAX_VALUE >> 4, Integer.MIN_VALUE >> 4));
}
@Test
public void footprintsWithoutColumnsOccupyNothing() {
assertSame(HydrologyChunkOccupancy.EMPTY, HydrologyChunkOccupancy.of(RiverFootprint.empty()));
assertSame(HydrologyChunkOccupancy.EMPTY, HydrologyChunkOccupancy.of(null));
assertFalse(HydrologyChunkOccupancy.EMPTY.occupies(0, 0));
assertTrue(HydrologyChunkOccupancy.of(footprint(new int[][]{{3, 4}})).occupies(0, 0));
}
private static RiverFootprint footprint(int[][] columns) {
Map<Long, HydrologyColumnSample> samples = new HashMap<>();
for (int[] column : columns) {
samples.put(RiverFootprint.pack(column[0], column[1]),
new HydrologyColumnSample(column[0], column[1], 80, 63, false, "parent", List.of()));
}
return new RiverFootprint(samples);
}
}
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.testsupport.Await;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.volmlib.util.cache.CacheKey;
import com.github.benmanes.caffeine.cache.Cache;
import org.junit.Test;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
@@ -14,6 +15,7 @@ import java.lang.reflect.Field;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -399,6 +401,7 @@ public class HydrologyTileCacheTest {
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
publishesInto(tile, RiverFootprint.pack(0, 0));
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
when(tile.columnAt(anyInt(), anyInt())).thenAnswer(invocation -> {
@@ -625,8 +628,10 @@ public class HydrologyTileCacheTest {
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(
new HydrologyTileCache.PregenerationArea(0, 0, -1536, -1536, 2047, 2047))) {
cache.prefetchArea(-4096, -4096, 4095, 4095, 0, 0);
assertEquals(prefetchRectangle(-2, 2, -2, 2), new HashSet<>(planned));
assertEquals(25, planned.size());
cache.prefetchArea(-4096, -4096, 4095, 4095, 0, 0);
assertEquals(prefetchRectangle(-3, 2, -3, 2), new HashSet<>(planned));
planned.clear();
HydrologyTileKey demanded = new HydrologyTileKey(8, 8);
assertSame(tile, cache.get(demanded));
@@ -682,27 +687,81 @@ public class HydrologyTileCacheTest {
}
@Test
public void largePregenBoundsKeepInitialWarmupLocal() {
public void largePregenBoundsPlanTheNearestTilesUpToTheLookaheadLimit() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
doAnswer(invocation -> {
planned.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
usePreparedStore(cache, mock(PreparedHydrologyTileStore.class));
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(
new HydrologyTileCache.PregenerationArea(0, 0,
Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE))) {
assertEquals(prefetchRectangle(-2, 1, -2, 1), new HashSet<>(planned));
assertEquals(16, planned.size());
assertEquals(4096, planned.size());
assertEquals(4096, new HashSet<>(planned).size());
assertEquals(new HydrologyTileKey(0, 0), planned.getFirst());
assertEquals(prefetchRectangle(-1, 1, -1, 1), new HashSet<>(planned.subList(0, 9)));
assertTrue(new HashSet<>(planned).containsAll(prefetchRectangle(-31, 31, -31, 31)));
for (HydrologyTileKey key : planned) {
assertTrue(Math.max(Math.abs(key.tileX()), Math.abs(key.tileZ())) <= 32);
}
}
cache.close();
}
@Test
public void preparedStoreLetsPregenSpeculationRunPastTheTileBudget() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
RiverFootprint footprint = mock(RiverFootprint.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
when(tile.footprint()).thenReturn(footprint);
when(footprint.size()).thenReturn((int) (HydrologyCacheBudget.runtime().tileBytes() * 3L / 5L / 64L));
when(footprint.columns()).thenReturn(Map.of());
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
doAnswer(invocation -> {
planned.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add);
PreparedHydrologyTileStore store = mock(PreparedHydrologyTileStore.class);
usePreparedStore(cache, store);
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(pregenArea(0, 0))) {
drainPrefetchTasks(queued);
assertEquals(prefetchRectangle(-2, 1, -2, 1), new HashSet<>(planned));
assertEquals(16, planned.size());
assertTrue(cache.size() < 16);
verify(store, times(16)).save(tile);
assertTrue(cache.isPlanned(40, 40));
}
cache.close();
}
private static void usePreparedStore(HydrologyTileCache cache, PreparedHydrologyTileStore store) throws Exception {
Field persistentStore = HydrologyTileCache.class.getDeclaredField("persistentStore");
persistentStore.setAccessible(true);
persistentStore.set(cache, store);
}
/** Gives a mocked tile one footprint column in each listed chunk, so those chunks compose from it. */
private static void publishesInto(HydrologyTile tile, long... chunks) {
HashMap<Long, HydrologyColumnSample> columns = new HashMap<>();
for (long chunk : chunks) {
columns.put(RiverFootprint.pack(RiverFootprint.unpackX(chunk) * 16, RiverFootprint.unpackZ(chunk) * 16),
mock(HydrologyColumnSample.class));
}
RiverFootprint footprint = mock(RiverFootprint.class);
when(footprint.columns()).thenReturn(columns);
when(tile.footprint()).thenReturn(footprint);
}
private static HydrologyTileCache.PregenerationArea pregenArea(int x, int z) {
return new HydrologyTileCache.PregenerationArea(x, z, (long) x - 1024, (long) z - 1024,
(long) x + 1024, (long) z + 1024);
@@ -868,6 +927,7 @@ public class HydrologyTileCacheTest {
ArrayList<Long> visitedColumns = new ArrayList<>();
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
publishesInto(tile, RiverFootprint.pack(2, 3));
doAnswer(invocation -> {
int blockX = invocation.getArgument(0);
int blockZ = invocation.getArgument(1);
@@ -1642,6 +1702,7 @@ public class HydrologyTileCacheTest {
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
publishesInto(tile, RiverFootprint.pack(0, 0), RiverFootprint.pack(secondChunkX, 0));
CountDownLatch started = new CountDownLatch(2);
CountDownLatch release = new CountDownLatch(1);
doAnswer(invocation -> {
@@ -1675,6 +1736,7 @@ public class HydrologyTileCacheTest {
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
publishesInto(tile, RiverFootprint.pack(0, 0));
ExecutorService executor = Executors.newFixedThreadPool(8);
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 64);
@@ -1779,6 +1841,8 @@ public class HydrologyTileCacheTest {
HydrologyTile newTile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(oldTile, newTile);
publishesInto(oldTile, RiverFootprint.pack(0, 0));
publishesInto(newTile, RiverFootprint.pack(0, 0));
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
doAnswer(invocation -> {
@@ -1815,6 +1879,7 @@ public class HydrologyTileCacheTest {
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
publishesInto(tile, RiverFootprint.pack(0, 0));
HydrologyTileCache cache = new HydrologyTileCache(planner, 8);
doAnswer(invocation -> cache.columnAt(0, 0)).when(tile).columnAt(anyInt(), anyInt());
@@ -2338,6 +2403,7 @@ public class HydrologyTileCacheTest {
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
publishesInto(tile, RiverFootprint.pack(2, 2));
List<Runnable> queued = new ArrayList<>();
AtomicBoolean forbidden = new AtomicBoolean(true);
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add, forbidden::get);
@@ -2399,6 +2465,7 @@ public class HydrologyTileCacheTest {
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
publishesInto(tile, RiverFootprint.pack(2, 2));
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, null, () -> true);
cache.get(new HydrologyTileKey(0, 0));
AtomicBoolean evicted = new AtomicBoolean();
@@ -2417,4 +2484,97 @@ public class HydrologyTileCacheTest {
assertFalse(cache.columnSnapshot(40, 40).available());
}
@Test
public void occupancyAnswersEmptyChunksExactlyWhereCompositionFindsNoColumn() {
HydrologyPlannerSettings settings = featureSettings();
HydrologyPlanner planner = new HydrologyPlanner(811L, settings, this::featureTerrain);
HydrologyTileCache cache = new HydrologyTileCache(planner, 256);
int tileSize = settings.routing().tileSize();
int radius = settings.publicationRadius();
int emptyChunks = 0;
int publishedChunks = 0;
for (int chunkZ = -2; chunkZ <= 5; chunkZ++) {
for (int chunkX = -2; chunkX <= 5; chunkX++) {
boolean published = false;
for (int localZ = 0; localZ < 16; localZ++) {
for (int localX = 0; localX < 16; localX++) {
int x = chunkX * 16 + localX;
int z = chunkZ * 16 + localZ;
boolean expected = false;
for (int tileZ = Math.floorDiv(z - radius, tileSize); tileZ <= Math.floorDiv(z + radius, tileSize); tileZ++) {
for (int tileX = Math.floorDiv(x - radius, tileSize); tileX <= Math.floorDiv(x + radius, tileSize); tileX++) {
expected |= cache.get(new HydrologyTileKey(tileX, tileZ)).columnAt(x, z).isPresent();
}
}
assertEquals(expected, cache.columnAt(x, z).isPresent());
published |= expected;
}
}
if (published) {
publishedChunks++;
} else {
emptyChunks++;
}
}
}
assertTrue(emptyChunks > 0);
assertTrue(publishedChunks > 0);
}
@Test
public void emptyChunksAnswerFromOccupancyAfterTheirTilesLeaveMemory() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
publishesInto(tile, RiverFootprint.pack(0, 0));
HydrologyTileCache cache = new HydrologyTileCache(planner, 64);
HydrologyTileKey origin = new HydrologyTileKey(0, 0);
assertTrue(cache.columnAt(552, 552).isEmpty());
verify(planner, times(1)).plan(origin);
verify(tile, never()).columnAt(anyInt(), anyInt());
Field tilesField = HydrologyTileCache.class.getDeclaredField("tiles");
tilesField.setAccessible(true);
((Cache<?, ?>) tilesField.get(cache)).invalidateAll();
assertEquals(0, cache.size());
assertTrue(cache.columnAt(568, 552).isEmpty());
assertTrue(cache.isPlanned(568, 552));
assertTrue(cache.columnSnapshot(568, 552).available());
verify(planner, times(1)).plan(origin);
verify(tile, never()).columnAt(anyInt(), anyInt());
cache.columnAt(8, 8);
verify(tile, times(4 * 256)).columnAt(anyInt(), anyInt());
}
@Test
public void speculativeRootsLeaveOneRootPermitForDemand() {
assertEquals(1, HydrologyTileCache.prefetchSlots(1));
assertEquals(1, HydrologyTileCache.prefetchSlots(2));
assertEquals(3, HydrologyTileCache.prefetchSlots(4));
assertEquals(7, HydrologyTileCache.prefetchSlots(8));
}
@Test
public void nearestFirstWalksRingsInRowOrderInsideTheBounds() {
HydrologyTileCache.TileBounds bounds = new HydrologyTileCache.TileBounds(-2, -1, 3, 2);
List<HydrologyTileKey> keys = HydrologyTileCache.nearestFirst(bounds, 0, 0, Integer.MAX_VALUE);
ArrayList<HydrologyTileKey> expected = new ArrayList<>(prefetchRectangle(-2, 3, -1, 2));
expected.sort(Comparator.comparingInt((HydrologyTileKey key) -> Math.max(Math.abs(key.tileX()), Math.abs(key.tileZ())))
.thenComparingInt(HydrologyTileKey::tileZ)
.thenComparingInt(HydrologyTileKey::tileX));
assertEquals(expected, keys);
List<HydrologyTileKey> outside = HydrologyTileCache.nearestFirst(bounds, 9, -7, 5);
assertEquals(5, outside.size());
assertEquals(new HydrologyTileKey(3, -1), outside.getFirst());
for (HydrologyTileKey key : outside) {
assertTrue(key.tileX() >= -2 && key.tileX() <= 3 && key.tileZ() >= -1 && key.tileZ() <= 2);
}
}
}
@@ -61,10 +61,12 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class MantleHydrologyComponentTest {
@@ -2007,4 +2009,23 @@ public class MantleHydrologyComponentTest {
openToSurface.add(position);
}
}
@Test
public void chunksWithoutNearbyFootprintColumnsPublishNothingWithoutReadingCavePlans() {
IrisComplex complex = mock(IrisComplex.class);
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
ChunkContext context = mock(ChunkContext.class);
MantleWriter writer = mock(MantleWriter.class);
when(context.getComplex()).thenReturn(complex);
when(complex.getHydrologyRuntime()).thenReturn(runtime);
when(complex.allowsMantleChunkWrite(3, -4)).thenReturn(true);
when(runtime.hasColumnsAround(3, -4)).thenReturn(false);
new MantleHydrologyComponent(mock(EngineMantle.class)).generateLayer(writer, 3, -4, context);
verify(runtime).hasColumnsAround(3, -4);
verify(runtime, never()).cavePlansIn(anyInt(), anyInt(), anyInt(), anyInt());
verify(complex, never()).sampleHydrologyColumn(anyDouble(), anyDouble());
verifyNoInteractions(writer);
}
}
@@ -2,6 +2,7 @@ package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
@@ -74,6 +75,7 @@ public class MantleWriterPreObjectJournalTest {
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getComplex()).thenReturn(mock(IrisComplex.class));
when(engineMantle.getEngine()).thenReturn(mock(Engine.class));
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
matter = spy(new IrisMatter(16, 16, 16));
@@ -43,6 +43,7 @@ import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -657,4 +658,30 @@ public class ObjectDestinationTransactionTest {
private record Marker(String value) {
}
@Test
public void chunksThatCannotHoldHydrologyCellsSkipTheCellLookup() {
MantleWriter writer = writer();
IrisComplex complex = mock(IrisComplex.class);
when(writer.getEngine().getComplex()).thenReturn(complex);
when(complex.mayHoldHydrologyCells(1, 0)).thenReturn(true);
when(writer.getPrerequisiteDataIfPresent(anyInt(), anyInt(), anyInt(), any()))
.thenReturn(HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD));
NativeBlockState block = mock(NativeBlockState.class);
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
for (int x = 0; x < 16; x++) {
transaction.setData(x, 4, 0, block);
}
transaction.setData(16, 4, 0, block);
for (int x = 0; x < 16; x++) {
assertSame(block, transaction.get(x, 4, 0));
}
assertEquals(16, transaction.sourcePlanSince(0).mutationsFor(0, 0).size());
assertTrue(transaction.sourcePlanSince(0).mutationsFor(1, 0).isEmpty());
verify(complex, times(1)).mayHoldHydrologyCells(0, 0);
verify(writer, never()).getPrerequisiteDataIfPresent(0, 4, 0, HydrologyCaveCell.class);
verify(writer).getPrerequisiteDataIfPresent(16, 4, 0, HydrologyCaveCell.class);
}
}
@@ -0,0 +1,75 @@
package art.arcane.iris.generation.stream;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
public class ProvisionalSamplingTest {
@Test
public void provisionalValuesReachTheCallerWithoutBeingMemoized() {
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class));
AtomicBoolean planned = new AtomicBoolean();
AtomicInteger samples = new AtomicInteger();
ProceduralStream<Double> source = ProceduralStream.ofDouble((x, z) -> {
samples.incrementAndGet();
if (!planned.get()) {
ProvisionalSampling.mark();
return -1D;
}
return x + z;
});
CachedDoubleStream2D cached = new CachedDoubleStream2D("provisional", mock(Engine.class), source, 4);
assertEquals(-1D, cached.getDouble(3, 4), 0D);
assertEquals(-1D, cached.get(3D, 4D), 0D);
assertEquals(2, samples.get());
planned.set(true);
assertEquals(7D, cached.getDouble(3, 4), 0D);
assertEquals(7D, cached.getDouble(3, 4), 0D);
assertEquals(3, samples.get());
}
}
@Test
public void derivedCachesInheritTheMarkAndFillChunksWithoutStoringIt() {
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class));
AtomicBoolean planned = new AtomicBoolean();
AtomicInteger samples = new AtomicInteger();
ProceduralStream<Double> source = ProceduralStream.ofDouble((x, z) -> {
samples.incrementAndGet();
if (!planned.get() && x == 5 && z == 0) {
ProvisionalSampling.mark();
return -1D;
}
return 1D;
});
CachedDoubleStream2D base = new CachedDoubleStream2D("base", mock(Engine.class), source, 4);
CachedStream2D<String> derived = new CachedStream2D<>("derived", mock(Engine.class),
base.convert((Double value) -> value < 0D ? "natural" : "planned"), 4);
Object[] row = new Object[256];
derived.fillChunkRaw(0, 0, row);
assertEquals("natural", row[5]);
assertEquals("planned", row[6]);
assertEquals("natural", derived.get(5D, 0D));
planned.set(true);
int before = samples.get();
assertEquals("planned", derived.get(5D, 0D));
assertEquals(before + 1, samples.get());
assertEquals("planned", derived.get(6D, 0D));
assertEquals(before + 1, samples.get());
}
}
}