Skip to content

Commit aa4a5c8

Browse files
committed
fix(model-datastructure): avoid StackOverflow in LinearHistory
Imperative implementation instead of recursive in case of a very long history.
1 parent b170580 commit aa4a5c8

File tree

1 file changed

+45
-86
lines changed

1 file changed

+45
-86
lines changed

model-datastructure/src/commonMain/kotlin/org/modelix/model/LinearHistory.kt

Lines changed: 45 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -8,84 +8,32 @@ import org.modelix.model.persistent.CPVersion
88
/**
99
* Was introduced in https://github.com/modelix/modelix/commit/19c74bed5921028af3ac3ee9d997fc1c4203ad44
1010
* together with the UndoOp. The idea is that an undo should only revert changes if there is no other change that relies
11-
* on it. In that case the undo should do nothing to not indirectly undo newer changes.
11+
* on it. In that case the undo should do nothing, to not indirectly undo newer changes.
1212
* For example, if you added a node and someone else started changing properties on the that node, your undo should not
1313
* remove the node to not lose the property changes.
14+
* This requires the versions to be ordered in a way that the undo appears later.
1415
*/
15-
class SlowLinearHistory(val baseVersionHash: String?) {
16-
17-
val version2descendants: MutableMap<Long, MutableSet<Long>> = HashMap()
18-
val versions: MutableMap<Long, CLVersion> = HashMap()
19-
20-
/**
21-
* Oldest version first
22-
*/
23-
fun load(vararg fromVersions: CLVersion): List<CLVersion> {
24-
for (fromVersion in fromVersions) {
25-
collect(fromVersion, emptyList())
26-
}
27-
28-
var result: List<Long> = ArrayList()
29-
30-
for (version in versions.values.filter { !it.isMerge() }.sortedBy { it.id }) {
31-
val descendantIds = version2descendants[version.id]!!.sorted().toSet()
32-
val idsInResult = result.toHashSet()
33-
if (idsInResult.contains(version.id)) {
34-
result =
35-
result +
36-
descendantIds.filter { !idsInResult.contains(it) }
37-
} else {
38-
result =
39-
result.filter { !descendantIds.contains(it) } +
40-
version.id +
41-
result.filter { descendantIds.contains(it) } +
42-
descendantIds.filter { !idsInResult.contains(it) }
43-
}
44-
}
45-
return result.map { versions[it]!! }
46-
}
47-
48-
private fun collect(version: CLVersion, path: List<CLVersion>) {
49-
if (version.hash == baseVersionHash) return
50-
51-
if (!versions.containsKey(version.id)) versions[version.id] = version
52-
version2descendants.getOrPut(version.id) { HashSet() }.addAll(path.asSequence().map { it.id })
53-
54-
if (version.isMerge()) {
55-
val version1 = getVersion(version.data!!.mergedVersion1!!, version.store)
56-
val version2 = getVersion(version.data!!.mergedVersion2!!, version.store)
57-
collect(version1, path)
58-
collect(version2, path)
59-
} else {
60-
val previous = version.baseVersion
61-
if (previous != null) {
62-
collect(previous, path + version)
63-
}
64-
}
65-
}
66-
67-
private fun getVersion(hash: KVEntryReference<CPVersion>, store: IDeserializingKeyValueStore): CLVersion {
68-
return CLVersion(hash.getValue(store), store)
69-
}
70-
}
71-
7216
class LinearHistory(val baseVersionHash: String?) {
7317

7418
val version2directDescendants: MutableMap<Long, Set<Long>> = HashMap()
75-
val versions: MutableMap<Long, CLVersion> = HashMap()
19+
val versions: MutableMap<Long, CLVersion> = LinkedHashMap()
7620

7721
/**
78-
* Oldest version first
22+
* @param fromVersions it is assumed that the versions are sorted by the oldest version first. When merging a new
23+
* version into an existing one the new version should appear after the existing one. The resulting order
24+
* will prefer existing versions to new ones, meaning during the conflict resolution the existing changes
25+
* have a higher probability of surviving.
26+
* @returns oldest version first
7927
*/
8028
fun load(vararg fromVersions: CLVersion): List<CLVersion> {
8129
for (fromVersion in fromVersions) {
82-
collect(fromVersion, null)
30+
collect(fromVersion)
8331
}
8432

8533
var result: List<Long> = ArrayList()
8634

8735
for (version in versions.values.filter { !it.isMerge() }.sortedBy { it.id }) {
88-
val descendantIds = HashSet<Long>().also { collectAllDescendants(version.id, it) }.filter { !versions[it]!!.isMerge() }.sorted().toSet()
36+
val descendantIds = collectAllDescendants(version.id).filter { !versions[it]!!.isMerge() }.sorted().toSet()
8937
val idsInResult = result.toHashSet()
9038
if (idsInResult.contains(version.id)) {
9139
result =
@@ -102,34 +50,45 @@ class LinearHistory(val baseVersionHash: String?) {
10250
return result.map { versions[it]!! }
10351
}
10452

105-
private fun collectAllDescendants(ancestor: Long, result: MutableSet<Long>, visited: MutableSet<Long> = HashSet()) {
106-
if (!visited.add(ancestor)) return
107-
val descendants = version2directDescendants[ancestor] ?: return
53+
private fun collectAllDescendants(root: Long): Set<Long> {
54+
val result = LinkedHashSet<Long>()
55+
var previousSize = 0
56+
result += root
10857

109-
result += descendants
110-
descendants.forEach {
111-
collectAllDescendants(it, result, visited)
112-
}
113-
}
114-
115-
private fun collect(version: CLVersion, descendant: CLVersion?) {
116-
if (version.getContentHash() == baseVersionHash) return
117-
if (descendant != null) {
118-
version2directDescendants[version.id] = (version2directDescendants[version.id] ?: emptySet()) + setOf(descendant.id)
58+
while (previousSize != result.size) {
59+
val nextElements = result.asSequence().drop(previousSize).toList()
60+
previousSize = result.size
61+
for (ancestor in nextElements) {
62+
version2directDescendants[ancestor]?.let { result += it }
63+
}
11964
}
120-
if (versions.containsKey(version.id)) return
12165

122-
versions[version.id] = version
66+
return result.drop(1).toSet()
67+
}
12368

124-
if (version.isMerge()) {
125-
val version1 = getVersion(version.data!!.mergedVersion1!!, version.store)
126-
val version2 = getVersion(version.data!!.mergedVersion2!!, version.store)
127-
collect(version1, version)
128-
collect(version2, version)
129-
} else {
130-
val previous = version.baseVersion
131-
if (previous != null) {
132-
collect(previous, version)
69+
private fun collect(root: CLVersion) {
70+
if (root.getContentHash() == baseVersionHash) return
71+
72+
var previousSize = versions.size
73+
versions[root.id] = root
74+
75+
while (previousSize != versions.size) {
76+
val nextElements = versions.asSequence().drop(previousSize).map { it.value }.toList()
77+
previousSize = versions.size
78+
79+
for (descendant in nextElements) {
80+
val ancestors = if (descendant.isMerge()) {
81+
sequenceOf(
82+
getVersion(descendant.data!!.mergedVersion1!!, descendant.store),
83+
getVersion(descendant.data!!.mergedVersion2!!, descendant.store),
84+
)
85+
} else {
86+
sequenceOf(descendant.baseVersion)
87+
}.filterNotNull().filter { it.getContentHash() != baseVersionHash }.toList()
88+
for (ancestor in ancestors) {
89+
versions[ancestor.id] = ancestor
90+
version2directDescendants[ancestor.id] = (version2directDescendants[ancestor.id] ?: emptySet()) + setOf(descendant.id)
91+
}
13392
}
13493
}
13594
}

0 commit comments

Comments
 (0)