From 186d78fa2d145cf32c5c30a0c2cde338f11fd54d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Csan=C3=A1d=20Telbisz?= Date: Fri, 5 Jun 2026 09:50:58 +0200 Subject: [PATCH 1/3] consolidated diff cursor --- .../store/map/ConsolidatedDiffCursor.java | 84 +++++++++++++++++++ .../refinery/store/map/VersionedMap.java | 7 ++ .../internal/state/VersionedMapStateImpl.java | 8 +- 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 subprojects/store/src/main/java/tools/refinery/store/map/ConsolidatedDiffCursor.java 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..1daab56da --- /dev/null +++ b/subprojects/store/src/main/java/tools/refinery/store/map/ConsolidatedDiffCursor.java @@ -0,0 +1,84 @@ +package tools.refinery.store.map; + +import java.util.AbstractMap; +import java.util.AbstractMap.SimpleEntry; +import java.util.HashMap; + +public 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 (!storedChange.getValue().equals(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 (fromValue.equals(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..ca46c4fb5 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 @@ -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..94572da05 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 @@ -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(); From a472823e56d876757c832076b6fdc3ac755eadc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Csan=C3=A1d=20Telbisz?= Date: Fri, 5 Jun 2026 10:24:12 +0200 Subject: [PATCH 2/3] ConsolidatedDiffCursor test --- .../store/map/ConsolidatedDiffCursor.java | 12 +++-- .../map/tests/fuzz/DiffCursorFuzzTest.java | 52 ++++++++++++++----- 2 files changed, 48 insertions(+), 16 deletions(-) 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 index 1daab56da..459850292 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/map/ConsolidatedDiffCursor.java +++ b/subprojects/store/src/main/java/tools/refinery/store/map/ConsolidatedDiffCursor.java @@ -1,10 +1,16 @@ +/* + * 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; -public class ConsolidatedDiffCursor implements DiffCursor { +class ConsolidatedDiffCursor implements DiffCursor { private final DiffCursor wrappedDiffCursor; private DiffEntry[] diff; @@ -58,7 +64,7 @@ private void consolidate() { var storedChange = consolidatedChanges.get(wrappedDiffCursor.getKey()); V fromValue; if (storedChange != null) { - if (!storedChange.getValue().equals(wrappedDiffCursor.getFromValue())) { + if (!Objects.equals(storedChange.getValue(), wrappedDiffCursor.getFromValue())) { throw new IllegalStateException("Inconsistent diff cursor: mismatched previous value and from value"); } fromValue = storedChange.getKey(); @@ -67,7 +73,7 @@ private void consolidate() { } V toValue = wrappedDiffCursor.getToValue(); - if (fromValue.equals(toValue)) { + if (Objects.equals(fromValue, toValue)) { consolidatedChanges.remove(wrappedDiffCursor.getKey()); } else { consolidatedChanges.put(wrappedDiffCursor.getKey(), new SimpleEntry<>(fromValue, toValue)); 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..5f7a95db4 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 @@ -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); } From 9e6ce238ec6dbf0ce9c62206c261ebe65ee23969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Csan=C3=A1d=20Telbisz?= Date: Fri, 5 Jun 2026 10:49:10 +0200 Subject: [PATCH 3/3] propagate diff cursor consolidate changes to getter methods --- .../main/java/tools/refinery/store/map/VersionedMap.java | 2 +- .../store/map/internal/state/VersionedMapStateImpl.java | 2 +- .../java/tools/refinery/store/model/Interpretation.java | 8 ++++++-- .../src/main/java/tools/refinery/store/model/Model.java | 8 ++++++-- .../tools/refinery/store/model/internal/ModelImpl.java | 7 ++++--- .../store/model/internal/VersionedInterpretation.java | 6 +++--- .../refinery/store/map/tests/fuzz/DiffCursorFuzzTest.java | 2 +- 7 files changed, 22 insertions(+), 13 deletions(-) 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 ca46c4fb5..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 */ 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 94572da05..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 */ 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 5f7a95db4..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 */