diff --git a/subprojects/store/src/main/java/tools/refinery/store/map/ConsolidatedDiffCursor.java b/subprojects/store/src/main/java/tools/refinery/store/map/ConsolidatedDiffCursor.java new file mode 100644 index 000000000..459850292 --- /dev/null +++ b/subprojects/store/src/main/java/tools/refinery/store/map/ConsolidatedDiffCursor.java @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2026 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.store.map; + +import java.util.AbstractMap; +import java.util.AbstractMap.SimpleEntry; +import java.util.HashMap; +import java.util.Objects; + +class ConsolidatedDiffCursor implements DiffCursor { + + private final DiffCursor wrappedDiffCursor; + private DiffEntry[] diff; + private int cursorIndex = -1; + + private record DiffEntry(K key, V from, V to) { + } + + public ConsolidatedDiffCursor(DiffCursor diffCursor) { + wrappedDiffCursor = diffCursor; + } + + @Override + public K getKey() { + return diff != null && 0 <= cursorIndex && cursorIndex < diff.length ? diff[cursorIndex].key : null; + } + + @Override + public V getValue() { + return getToValue(); + } + + @Override + public boolean isTerminated() { + return diff != null && cursorIndex >= diff.length; + } + + @Override + public boolean move() { + if (diff == null) { + consolidate(); + } + + cursorIndex++; + return cursorIndex < diff.length; + } + + @Override + public V getFromValue() { + return diff != null && 0 <= cursorIndex && cursorIndex < diff.length ? diff[cursorIndex].from : null; + } + + @Override + public V getToValue() { + return diff != null && 0 <= cursorIndex && cursorIndex < diff.length ? diff[cursorIndex].to : null; + } + + private void consolidate() { + HashMap> consolidatedChanges = new HashMap<>(); + while (wrappedDiffCursor.move()) { + var storedChange = consolidatedChanges.get(wrappedDiffCursor.getKey()); + V fromValue; + if (storedChange != null) { + if (!Objects.equals(storedChange.getValue(), wrappedDiffCursor.getFromValue())) { + throw new IllegalStateException("Inconsistent diff cursor: mismatched previous value and from value"); + } + fromValue = storedChange.getKey(); + } else { + fromValue = wrappedDiffCursor.getFromValue(); + } + V toValue = wrappedDiffCursor.getToValue(); + + if (Objects.equals(fromValue, toValue)) { + consolidatedChanges.remove(wrappedDiffCursor.getKey()); + } else { + consolidatedChanges.put(wrappedDiffCursor.getKey(), new SimpleEntry<>(fromValue, toValue)); + } + } + + diff = new DiffEntry[consolidatedChanges.size()]; + int i = 0; + for (var entry : consolidatedChanges.entrySet()) { + diff[i] = new DiffEntry<>(entry.getKey(), entry.getValue().getKey(), entry.getValue().getValue()); + i++; + } + } +} diff --git a/subprojects/store/src/main/java/tools/refinery/store/map/VersionedMap.java b/subprojects/store/src/main/java/tools/refinery/store/map/VersionedMap.java index 28194b58e..24da5470e 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/map/VersionedMap.java +++ b/subprojects/store/src/main/java/tools/refinery/store/map/VersionedMap.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * SPDX-FileCopyrightText: 2021-2026 The Refinery Authors * * SPDX-License-Identifier: EPL-2.0 */ @@ -17,4 +17,11 @@ public non-sealed interface VersionedMap extends AnyVersionedMap { void putAll(Cursor cursor); DiffCursor getDiffCursor(Version state); + + default DiffCursor getDiffCursor(Version state, boolean consolidate) { + if (!consolidate) { + return getDiffCursor(state); + } + return new ConsolidatedDiffCursor<>(getDiffCursor(state)); + } } diff --git a/subprojects/store/src/main/java/tools/refinery/store/map/internal/state/VersionedMapStateImpl.java b/subprojects/store/src/main/java/tools/refinery/store/map/internal/state/VersionedMapStateImpl.java index 57eeccf6e..cf052b6f7 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/map/internal/state/VersionedMapStateImpl.java +++ b/subprojects/store/src/main/java/tools/refinery/store/map/internal/state/VersionedMapStateImpl.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023 The Refinery Authors + * SPDX-FileCopyrightText: 2023-2026 The Refinery Authors * * SPDX-License-Identifier: EPL-2.0 */ @@ -82,7 +82,7 @@ public void putAll(Cursor cursor) { while (keyIterator.hasNext()) { var key = keyIterator.next(); var value = valueIterator.next(); - this.put(key,value); + this.put(key, value); } } else { while (cursor.move()) { @@ -122,6 +122,10 @@ public DiffCursor getDiffCursor(Version toVersion) { return new MapDiffCursor<>(this.defaultValue, fromCursor, toCursor); } + @Override + public DiffCursor getDiffCursor(Version state, boolean consolidate) { + return getDiffCursor(state); + } @Override public Version commit() { @@ -157,7 +161,7 @@ public void checkIntegrity() { @Override public int contentHashCode(ContentHashCode mode) { // Calculating the root hashCode is always fast, because {@link Node} caches its hashCode. - if(root == null) { + if (root == null) { return 0; } else { return root.hashCode(); diff --git a/subprojects/store/src/main/java/tools/refinery/store/model/Interpretation.java b/subprojects/store/src/main/java/tools/refinery/store/model/Interpretation.java index 1b15e4cff..0c17072c3 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/model/Interpretation.java +++ b/subprojects/store/src/main/java/tools/refinery/store/model/Interpretation.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * SPDX-FileCopyrightText: 2021-2026 The Refinery Authors * * SPDX-License-Identifier: EPL-2.0 */ @@ -25,7 +25,11 @@ public non-sealed interface Interpretation extends AnyInterpretation { void putAll(Cursor cursor); - DiffCursor getDiffCursor(Version to); + default DiffCursor getDiffCursor(Version to) { + return getDiffCursor(to, false); + } + + DiffCursor getDiffCursor(Version to, boolean consolidate); void addListener(InterpretationListener listener, boolean alsoWhenRestoring); diff --git a/subprojects/store/src/main/java/tools/refinery/store/model/Model.java b/subprojects/store/src/main/java/tools/refinery/store/model/Model.java index 3eb6ef9a1..ca7af1916 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/model/Model.java +++ b/subprojects/store/src/main/java/tools/refinery/store/model/Model.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * SPDX-FileCopyrightText: 2021-2026 The Refinery Authors * * SPDX-License-Identifier: EPL-2.0 */ @@ -29,7 +29,11 @@ default AnyInterpretation getInterpretation(AnySymbol symbol) { Interpretation getInterpretation(Symbol symbol); - ModelDiffCursor getDiffCursor(Version to); + default ModelDiffCursor getDiffCursor(Version to) { + return getDiffCursor(to, false); + } + + ModelDiffCursor getDiffCursor(Version to, boolean consolidate); Optional tryGetAdapter(Class adapterType); diff --git a/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelImpl.java b/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelImpl.java index 0fc3ca5ab..6624c9b2c 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelImpl.java +++ b/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelImpl.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * SPDX-FileCopyrightText: 2021-2026 The Refinery Authors * * SPDX-License-Identifier: EPL-2.0 */ @@ -60,11 +60,12 @@ public Interpretation getInterpretation(Symbol symbol) { } @Override - public ModelDiffCursor getDiffCursor(Version to) { + public ModelDiffCursor getDiffCursor(Version to, boolean consolidate) { var diffCursors = HashMap.>newHashMap(interpretations.size()); int i = 0; for (var entry : interpretations.entrySet()) { - diffCursors.put(entry.getKey(), entry.getValue().getDiffCursor(ModelVersion.getInternalVersion(to, i++))); + diffCursors.put(entry.getKey(), entry.getValue().getDiffCursor(ModelVersion.getInternalVersion(to, i++), + consolidate)); } return new ModelDiffCursor(diffCursors); } diff --git a/subprojects/store/src/main/java/tools/refinery/store/model/internal/VersionedInterpretation.java b/subprojects/store/src/main/java/tools/refinery/store/model/internal/VersionedInterpretation.java index dcf0ad08b..4988c5fc6 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/model/internal/VersionedInterpretation.java +++ b/subprojects/store/src/main/java/tools/refinery/store/model/internal/VersionedInterpretation.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * SPDX-FileCopyrightText: 2021-2026 The Refinery Authors * * SPDX-License-Identifier: EPL-2.0 */ @@ -106,8 +106,8 @@ public void putAll(Cursor cursor) { } @Override - public DiffCursor getDiffCursor(Version to) { - return map.getDiffCursor(to); + public DiffCursor getDiffCursor(Version to, boolean consolidate) { + return map.getDiffCursor(to, consolidate); } Version commit() { diff --git a/subprojects/store/src/test/java/tools/refinery/store/map/tests/fuzz/DiffCursorFuzzTest.java b/subprojects/store/src/test/java/tools/refinery/store/map/tests/fuzz/DiffCursorFuzzTest.java index 94259edc1..899f693be 100644 --- a/subprojects/store/src/test/java/tools/refinery/store/map/tests/fuzz/DiffCursorFuzzTest.java +++ b/subprojects/store/src/test/java/tools/refinery/store/map/tests/fuzz/DiffCursorFuzzTest.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * SPDX-FileCopyrightText: 2021-2026 The Refinery Authors * * SPDX-License-Identifier: EPL-2.0 */ @@ -10,7 +10,11 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import tools.refinery.store.map.*; +import tools.refinery.store.map.DiffCursor; +import tools.refinery.store.map.Version; +import tools.refinery.store.map.VersionedMap; +import tools.refinery.store.map.VersionedMapStore; +import tools.refinery.store.map.VersionedMapStoreFactoryBuilder; import tools.refinery.store.map.tests.fuzz.utils.FuzzTestUtils; import tools.refinery.store.map.tests.utils.MapTestEnvironment; @@ -19,13 +23,20 @@ import java.util.Random; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.fail; -import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.*; +import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.commitFrequencyOptions; +import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.keyCounts; +import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.nullDefaultOptions; +import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.randomSeedOptions; +import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.storeConfigs; +import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.valueCounts; class DiffCursorFuzzTest { private void runFuzzTest(String scenario, int seed, int steps, int maxKey, int maxValue, boolean nullDefault, - int commitFrequency, boolean commitBeforeDiffCursor, - VersionedMapStoreFactoryBuilder builder) { + int commitFrequency, boolean commitBeforeDiffCursor, + VersionedMapStoreFactoryBuilder builder) { String[] values = MapTestEnvironment.prepareValues(maxValue, nullDefault); VersionedMapStore store = builder.defaultValue(values[0]).build().createOne(); @@ -34,11 +45,11 @@ private void runFuzzTest(String scenario, int seed, int steps, int maxKey, int m } private void iterativeRandomPutsAndCommitsThenDiffCursor(String scenario, VersionedMapStore store, - int steps, int maxKey, String[] values, int seed, - int commitFrequency, boolean commitBeforeDiffCursor) { + int steps, int maxKey, String[] values, int seed, + int commitFrequency, boolean commitBeforeDiffCursor) { int largestCommit = -1; - Map index2Version = new HashMap<>(); + Map index2Version = new HashMap<>(); { // 1. build a map with versions @@ -56,7 +67,7 @@ private void iterativeRandomPutsAndCommitsThenDiffCursor(String scenario, Versio } if (index % commitFrequency == 0) { Version version = versioned.commit(); - index2Version.put(index,version); + index2Version.put(index, version); largestCommit = index; } if (index % 10000 == 0) @@ -78,13 +89,28 @@ private void iterativeRandomPutsAndCommitsThenDiffCursor(String scenario, Versio VersionedMap oracle = store.createMap(index2Version.get(travelToVersion)); - if(commitBeforeDiffCursor) { + if (commitBeforeDiffCursor) { moving.commit(); } DiffCursor diffCursor = moving.getDiffCursor(index2Version.get(travelToVersion)); + + DiffCursor consolidatedDiffCursor = + moving.getDiffCursor(index2Version.get(travelToVersion), true); + HashMap consolidatedToValues = new HashMap<>(); + while (consolidatedDiffCursor.move()) { + assertEquals(moving.get(consolidatedDiffCursor.getKey()), + consolidatedDiffCursor.getFromValue()); + assertFalse(consolidatedToValues.containsKey(consolidatedDiffCursor.getKey())); + consolidatedToValues.put(consolidatedDiffCursor.getKey(), consolidatedDiffCursor.getToValue()); + } + moving.putAll(diffCursor); moving.commit(); + for (var entry : consolidatedToValues.entrySet()) { + assertEquals(moving.get(entry.getKey()), entry.getValue()); + } + MapTestEnvironment.compareTwoMaps(scenario + ":c" + index, oracle, moving); moving.restore(index2Version.get(travelToVersion)); @@ -114,15 +140,15 @@ private void iterativeRandomPutsAndCommitsThenDiffCursor(String scenario, Versio @Timeout(value = 10) @Tag("fuzz") void parametrizedFuzz(int ignoredTests, int steps, int noKeys, int noValues, boolean nullDefault, - int commitFrequency, int seed, boolean commitBeforeDiffCursor, - VersionedMapStoreFactoryBuilder builder) { + int commitFrequency, int seed, boolean commitBeforeDiffCursor, + VersionedMapStoreFactoryBuilder builder) { runFuzzTest("DiffCursorS" + steps + "K" + noKeys + "V" + noValues + "s" + seed, seed, steps, noKeys, noValues, nullDefault, commitFrequency, commitBeforeDiffCursor, builder); } static Stream parametrizedFuzz() { return FuzzTestUtils.permutationWithSize(new Object[]{100}, keyCounts, valueCounts, nullDefaultOptions, - commitFrequencyOptions, randomSeedOptions, new Object[]{false,true}, storeConfigs); + commitFrequencyOptions, randomSeedOptions, new Object[]{false, true}, storeConfigs); } @ParameterizedTest(name = title) @@ -130,7 +156,7 @@ static Stream parametrizedFuzz() { @Tag("fuzz") @Tag("slow") void parametrizedSlowFuzz(int ignoredTests, int steps, int noKeys, int noValues, boolean nullDefault, int commitFrequency, - int seed, boolean commitBeforeDiffCursor, VersionedMapStoreFactoryBuilder builder) { + int seed, boolean commitBeforeDiffCursor, VersionedMapStoreFactoryBuilder builder) { runFuzzTest("DiffCursorS" + steps + "K" + noKeys + "V" + noValues + "s" + seed, seed, steps, noKeys, noValues, nullDefault, commitFrequency, commitBeforeDiffCursor, builder); }