Skip to content

Commit eb61aca

Browse files
committed
feat: index for efficient history queries
1 parent eafba21 commit eb61aca

File tree

3 files changed

+102
-80
lines changed

3 files changed

+102
-80
lines changed

model-datastructure/src/commonMain/kotlin/org/modelix/datastructures/model/HistoryIndexNode.kt

Lines changed: 86 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import org.modelix.model.persistent.CPVersion
1818
import org.modelix.model.persistent.Separators
1919
import org.modelix.streams.IStream
2020
import org.modelix.streams.flatten
21+
import org.modelix.streams.getBlocking
2122
import org.modelix.streams.plus
2223
import kotlin.math.abs
2324
import kotlin.math.max
@@ -38,7 +39,7 @@ sealed class HistoryIndexNode : IObjectData {
3839
abstract fun getAllVersionsReversed(): IStream.Many<ObjectReference<CPVersion>>
3940
abstract fun getRange(indexRange: LongRange): IStream.Many<HistoryIndexNode>
4041
fun getRange(indexRange: IntRange) = getRange(indexRange.first.toLong()..indexRange.last.toLong())
41-
abstract fun merge(self: Object<HistoryIndexNode>, otherObj: Object<HistoryIndexNode>): Object<HistoryIndexNode>
42+
abstract fun merge(self: Object<HistoryIndexNode>, otherObj: Object<HistoryIndexNode>): IStream.One<Object<HistoryIndexNode>>
4243

4344
/**
4445
* Each returned node spans at most the duration specified in [interval].
@@ -120,12 +121,13 @@ sealed class HistoryIndexNode : IObjectData {
120121
}
121122

122123
fun of(version1: Object<CPVersion>, version2: Object<CPVersion>): HistoryIndexNode {
123-
return of(version1).asObject(version1.graph).merge(of(version2).asObject(version2.graph)).data
124+
return of(version1).asObject(version1.graph).merge(of(version2).asObject(version2.graph)).getBlocking(version1.graph).data
124125
}
125126
}
126127
}
127128

128-
fun Object<HistoryIndexNode>.merge(otherObj: Object<HistoryIndexNode>): Object<HistoryIndexNode> = data.merge(this, otherObj)
129+
fun Object<HistoryIndexNode>.merge(otherObj: Object<HistoryIndexNode>) = data.merge(this, otherObj)
130+
fun IStream.One<Object<HistoryIndexNode>>.merge(otherObj: Object<HistoryIndexNode>) = flatMapOne { it.merge(otherObj) }
129131
fun Object<HistoryIndexNode>.concatUnbalanced(otherObj: Object<HistoryIndexNode>): Object<HistoryIndexNode> = data.concat(this, otherObj)
130132
val Object<HistoryIndexLeafNode>.time get() = data.time
131133

@@ -191,7 +193,7 @@ data class HistoryIndexLeafNode(
191193
override fun merge(
192194
self: Object<HistoryIndexNode>,
193195
otherObj: Object<HistoryIndexNode>,
194-
): Object<HistoryIndexNode> {
196+
): IStream.One<Object<HistoryIndexNode>> {
195197
val other = otherObj.data
196198
return when (other) {
197199
is HistoryIndexLeafNode -> {
@@ -202,7 +204,7 @@ data class HistoryIndexLeafNode(
202204
versions = (versions.associateBy { it.getHash() } + other.versions.associateBy { it.getHash() }).values.toList(),
203205
authors = authors + other.authors,
204206
time = time,
205-
).asObject(self.graph)
207+
).asObject(self.graph).let { IStream.of(it) }
206208
}
207209
}
208210
is HistoryIndexRangeNode -> {
@@ -275,62 +277,62 @@ data class HistoryIndexRangeNode(
275277
TODO("Not yet implemented")
276278
}
277279

278-
override fun merge(self: Object<HistoryIndexNode>, otherObj: Object<HistoryIndexNode>): Object<HistoryIndexNode> {
280+
override fun merge(self: Object<HistoryIndexNode>, otherObj: Object<HistoryIndexNode>): IStream.One<Object<HistoryIndexNode>> {
279281
val self = self as Object<HistoryIndexRangeNode>
280282
val other = otherObj.data
281-
val resolvedChild1 = child1.resolveNow()
282-
val resolvedChild2 = child2.resolveNow()
283-
when (other) {
284-
is HistoryIndexLeafNode -> {
285-
val range1 = resolvedChild1.data.timeRange
286-
val range2 = resolvedChild2.data.timeRange
287-
return when {
288-
other.time < range1.start -> otherObj.concatBalanced(resolvedChild1).concatBalanced(resolvedChild2)
289-
other.time <= range1.endInclusive -> resolvedChild1.merge(otherObj).concatBalanced(resolvedChild2)
290-
other.time < range2.start -> if (resolvedChild1.size <= resolvedChild2.size) {
291-
resolvedChild1.concatBalanced(otherObj).concatBalanced(resolvedChild2)
292-
} else {
293-
resolvedChild1.concatBalanced(otherObj.concatBalanced(resolvedChild2))
294-
}
295-
other.time <= range2.endInclusive -> resolvedChild1.concatBalanced(resolvedChild2.merge(otherObj))
296-
else -> resolvedChild1.concatBalanced(resolvedChild2.concatBalanced(otherObj))
297-
}
298-
}
299-
is HistoryIndexRangeNode -> {
300-
val range1 = resolvedChild1.data.timeRange
301-
val range2 = resolvedChild2.data.timeRange
302-
val intersects1 = other.timeRange.intersects(range1)
303-
val intersects2 = other.timeRange.intersects(range2)
304-
return when {
305-
intersects1 && intersects2 -> {
306-
resolvedChild1.merge(otherObj).merge(resolvedChild2)
307-
}
308-
intersects1 -> resolvedChild1.merge(otherObj).concatBalanced(resolvedChild2)
309-
intersects2 -> resolvedChild1.concatBalanced(resolvedChild2.merge(otherObj))
310-
other.maxTime < range1.start -> {
311-
if (other.size < resolvedChild2.size) {
312-
otherObj.concatBalanced(resolvedChild1).concatBalanced(resolvedChild2)
283+
return child1.requestBoth(child2) { resolvedChild1, resolvedChild2 ->
284+
when (other) {
285+
is HistoryIndexLeafNode -> {
286+
val range1 = resolvedChild1.data.timeRange
287+
val range2 = resolvedChild2.data.timeRange
288+
when {
289+
other.time < range1.start -> otherObj.concatBalanced(resolvedChild1).concatBalanced(resolvedChild2)
290+
other.time <= range1.endInclusive -> resolvedChild1.merge(otherObj).concatBalanced(resolvedChild2)
291+
other.time < range2.start -> if (resolvedChild1.size <= resolvedChild2.size) {
292+
resolvedChild1.concatBalanced(otherObj).concatBalanced(resolvedChild2)
313293
} else {
314-
otherObj.concatBalanced(self)
315-
}
316-
}
317-
other.maxTime < range2.start -> {
318-
if (resolvedChild2.size < resolvedChild1.size) {
319294
resolvedChild1.concatBalanced(otherObj.concatBalanced(resolvedChild2))
320-
} else {
321-
resolvedChild1.concatBalanced(otherObj).concatBalanced(resolvedChild2)
322295
}
296+
other.time <= range2.endInclusive -> resolvedChild1.concatBalanced(resolvedChild2.merge(otherObj))
297+
else -> resolvedChild1.concatBalanced(resolvedChild2.concatBalanced(otherObj))
323298
}
324-
else -> {
325-
if (other.size < resolvedChild1.size) {
326-
resolvedChild1.concatBalanced(resolvedChild2.concatBalanced(otherObj))
327-
} else {
328-
self.concatBalanced(otherObj)
299+
}
300+
is HistoryIndexRangeNode -> {
301+
val range1 = resolvedChild1.data.timeRange
302+
val range2 = resolvedChild2.data.timeRange
303+
val intersects1 = other.timeRange.intersects(range1)
304+
val intersects2 = other.timeRange.intersects(range2)
305+
when {
306+
intersects1 && intersects2 -> {
307+
resolvedChild1.merge(otherObj).merge(resolvedChild2)
308+
}
309+
intersects1 -> resolvedChild1.merge(otherObj).concatBalanced(resolvedChild2)
310+
intersects2 -> resolvedChild1.concatBalanced(resolvedChild2.merge(otherObj))
311+
other.maxTime < range1.start -> {
312+
if (other.size < resolvedChild2.size) {
313+
otherObj.concatBalanced(resolvedChild1).concatBalanced(resolvedChild2)
314+
} else {
315+
otherObj.concatBalanced(self)
316+
}
317+
}
318+
other.maxTime < range2.start -> {
319+
if (resolvedChild2.size < resolvedChild1.size) {
320+
resolvedChild1.concatBalanced(otherObj.concatBalanced(resolvedChild2))
321+
} else {
322+
resolvedChild1.concatBalanced(otherObj).concatBalanced(resolvedChild2)
323+
}
324+
}
325+
else -> {
326+
if (other.size < resolvedChild1.size) {
327+
resolvedChild1.concatBalanced(resolvedChild2.concatBalanced(otherObj))
328+
} else {
329+
self.concatBalanced(otherObj)
330+
}
329331
}
330332
}
331333
}
332334
}
333-
}
335+
}.flatten()
334336
}
335337

336338
override fun splitAtInterval(interval: Duration): IStream.Many<HistoryIndexNode> {
@@ -397,31 +399,42 @@ fun LongRange.intersect(other: LongRange): LongRange {
397399
return if (this.first > other.first) other.intersect(this) else other.first..min(this.last, other.last)
398400
}
399401

400-
fun Object<HistoryIndexNode>.rebalance(otherObj: Object<HistoryIndexNode>): Pair<Object<HistoryIndexNode>, Object<HistoryIndexNode>> {
401-
if (otherObj.height > height + 1) {
402-
val split1 = (otherObj.data as HistoryIndexRangeNode).child1.resolveNow()
403-
val split2 = (otherObj.data as HistoryIndexRangeNode).child2.resolveNow()
404-
val rebalanced = this.rebalance(split1)
405-
if (rebalanced.first.height <= split2.height) {
406-
return rebalanced.first.concatUnbalanced(rebalanced.second) to split2
407-
} else {
408-
return rebalanced.first to rebalanced.second.concatUnbalanced(split2)
409-
}
402+
private fun Object<HistoryIndexNode>.rebalance(otherObj: Object<HistoryIndexNode>): IStream.One<Pair<Object<HistoryIndexNode>, Object<HistoryIndexNode>>> {
403+
return if (otherObj.height > height + 1) {
404+
(otherObj.data as HistoryIndexRangeNode).child1.requestBoth((otherObj.data as HistoryIndexRangeNode).child2) { split1, split2 ->
405+
this.rebalance(split1).map { rebalanced ->
406+
if (rebalanced.first.height <= split2.height) {
407+
rebalanced.first.concatUnbalanced(rebalanced.second) to split2
408+
} else {
409+
rebalanced.first to rebalanced.second.concatUnbalanced(split2)
410+
}
411+
}
412+
}.flatten()
410413
} else if (height > otherObj.height + 1) {
411-
val split1 = (this.data as HistoryIndexRangeNode).child1.resolveNow()
412-
val split2 = (this.data as HistoryIndexRangeNode).child2.resolveNow()
413-
val rebalanced = split2.rebalance(otherObj)
414-
if (rebalanced.second.height > split1.height) {
415-
return split1.concatUnbalanced(rebalanced.first) to rebalanced.second
416-
} else {
417-
return split1 to rebalanced.first.concatUnbalanced(rebalanced.second)
418-
}
414+
(this.data as HistoryIndexRangeNode).child1.requestBoth((this.data as HistoryIndexRangeNode).child2) { split1, split2 ->
415+
split2.rebalance(otherObj).map { rebalanced ->
416+
if (rebalanced.second.height > split1.height) {
417+
split1.concatUnbalanced(rebalanced.first) to rebalanced.second
418+
} else {
419+
split1 to rebalanced.first.concatUnbalanced(rebalanced.second)
420+
}
421+
}
422+
}.flatten()
419423
} else {
420-
return this to otherObj
424+
IStream.of(this to otherObj)
425+
}
426+
}
427+
428+
private fun Object<HistoryIndexNode>.concatBalanced(otherObj: Object<HistoryIndexNode>): IStream.One<Object<HistoryIndexNode>> {
429+
return this.rebalance(otherObj).map { rebalanced ->
430+
rebalanced.first.concatUnbalanced(rebalanced.second)
421431
}
422432
}
423433

424-
fun Object<HistoryIndexNode>.concatBalanced(otherObj: Object<HistoryIndexNode>): Object<HistoryIndexNode> {
425-
val rebalanced = this.rebalance(otherObj)
426-
return rebalanced.first.concatUnbalanced(rebalanced.second)
434+
private fun IStream.One<Object<HistoryIndexNode>>.concatBalanced(otherObj: Object<HistoryIndexNode>): IStream.One<Object<HistoryIndexNode>> {
435+
return flatMapOne { it.concatBalanced(otherObj) }
436+
}
437+
438+
private fun Object<HistoryIndexNode>.concatBalanced(otherObj: IStream.One<Object<HistoryIndexNode>>): IStream.One<Object<HistoryIndexNode>> {
439+
return otherObj.flatMapOne { this.concatBalanced(it) }
427440
}

model-datastructure/src/commonTest/kotlin/HistoryIndexTest.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class HistoryIndexTest {
6767
val graph = version1.graph
6868
val history1 = HistoryIndexNode.of(version1.obj, version2.obj).asObject(graph)
6969
val history2 = HistoryIndexNode.of(version3.obj).asObject(graph)
70-
val history = history1.merge(history2)
70+
val history = history1.merge(history2).getBlocking(graph)
7171
assertEquals(3, history.size)
7272
assertEquals(3, history.height)
7373
}
@@ -82,6 +82,7 @@ class HistoryIndexTest {
8282
val history = HistoryIndexNode.of(version1.obj, version2.obj).asObject(graph)
8383
.merge(HistoryIndexNode.of(version3.obj).asObject(graph))
8484
.merge(HistoryIndexNode.of(version4.obj).asObject(graph))
85+
.getBlocking(graph)
8586
assertEquals(4, history.size)
8687
assertEquals(4, history.height)
8788
}
@@ -98,6 +99,7 @@ class HistoryIndexTest {
9899
.merge(HistoryIndexNode.of(version3.obj).asObject(graph))
99100
.merge(HistoryIndexNode.of(version4.obj).asObject(graph))
100101
.merge(HistoryIndexNode.of(version5.obj).asObject(graph))
102+
.getBlocking(graph)
101103
assertEquals(5, history.size)
102104
assertEquals(4, history.height)
103105
}
@@ -133,10 +135,12 @@ class HistoryIndexTest {
133135
.merge(HistoryIndexNode.of(version4b.obj).asObject(graph))
134136
.merge(HistoryIndexNode.of(version5b.obj).asObject(graph))
135137
.merge(HistoryIndexNode.of(version6b.obj).asObject(graph))
138+
.getBlocking(graph)
136139
val history = historyA
137140
.merge(historyB)
138141
.merge(HistoryIndexNode.of(version7.obj).asObject(graph))
139142
.merge(HistoryIndexNode.of(version8.obj).asObject(graph))
143+
.getBlocking(graph)
140144

141145
assertEquals(
142146
listOf(version1, version2, version3a, version3b, version4a, version4b, version5a, version5b, version6b, version7, version8).map { it.getObjectHash() },
@@ -154,7 +158,7 @@ class HistoryIndexTest {
154158
}
155159
val graph = versions.first().graph
156160
val history = versions.drop(1).fold(HistoryIndexNode.of(versions.first().asObject()).asObject(graph)) { acc, it ->
157-
acc.merge(HistoryIndexNode.of(it.asObject()).asObject(graph))
161+
acc.merge(HistoryIndexNode.of(it.asObject()).asObject(graph)).getBlocking(graph)
158162
}
159163
assertEquals(versions.size.toLong(), history.size)
160164
assertEquals(
@@ -172,7 +176,7 @@ class HistoryIndexTest {
172176
}
173177
val graph = versions.first().graph
174178
val history = versions.drop(1).shuffled(Random(78234554)).fold(HistoryIndexNode.of(versions.first().asObject()).asObject(graph)) { acc, it ->
175-
acc.merge(HistoryIndexNode.of(it.asObject()).asObject(graph))
179+
acc.merge(HistoryIndexNode.of(it.asObject()).asObject(graph)).getBlocking(graph)
176180
}
177181
assertEquals(versions.size.toLong(), history.size)
178182
assertEquals(
@@ -217,8 +221,13 @@ class HistoryIndexTest {
217221
}
218222

219223
private fun buildHistory(versions: List<CLVersion>): Object<HistoryIndexNode> {
220-
if (versions.size == 1) return HistoryIndexNode.of(versions.single().obj).asObject(versions.single().graph)
224+
val graph = versions.first().graph
225+
if (versions.size == 1) {
226+
return HistoryIndexNode.of(versions.single().obj).asObject(graph)
227+
}
221228
val centerIndex = versions.size / 2
222-
return buildHistory(versions.subList(0, centerIndex)).merge(buildHistory(versions.subList(centerIndex, versions.size)))
229+
return buildHistory(versions.subList(0, centerIndex))
230+
.merge(buildHistory(versions.subList(centerIndex, versions.size)))
231+
.getBlocking(graph)
223232
}
224233
}

model-server/src/main/kotlin/org/modelix/model/server/handlers/RepositoriesManager.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,8 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager {
416416
val newElement = HistoryIndexNode.of(version.obj).asObject(graph)
417417
val newIndex = when (parentIndices.size) {
418418
0 -> newElement
419-
1 -> parentIndices.single().merge(newElement)
420-
2 -> parentIndices[0].merge(parentIndices[1]).merge(newElement)
419+
1 -> parentIndices.single().merge(newElement).getBlocking(graph)
420+
2 -> parentIndices[0].merge(parentIndices[1]).merge(newElement).getBlocking(graph)
421421
else -> error("impossible")
422422
}
423423
newIndex.write()

0 commit comments

Comments
 (0)