Skip to content

Commit 8ceb819

Browse files
slissonmhuster23
authored andcommitted
feat(model-datastructure): new BulkUpdateOp to merge large updates into a single operation
A bulk import usually generates a lot of change operations, but an operation should capture the intended change as high level as possible. In case of a bulk import the intention is to put the model into the resulting state, which can be captured more accurately and more memory efficient with the new operation.
1 parent acdb119 commit 8ceb819

File tree

4 files changed

+141
-5
lines changed

4 files changed

+141
-5
lines changed

bulk-model-sync-gradle/src/main/kotlin/org/modelix/model/sync/bulk/gradle/tasks/ImportIntoModelServer.kt

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ import org.gradle.api.tasks.TaskAction
3030
import org.modelix.model.ModelFacade
3131
import org.modelix.model.api.ILanguage
3232
import org.modelix.model.api.ILanguageRepository
33+
import org.modelix.model.api.INode
34+
import org.modelix.model.api.PNodeAdapter
3335
import org.modelix.model.client2.ModelClientV2
3436
import org.modelix.model.client2.runWrite
3537
import org.modelix.model.lazy.RepositoryId
38+
import org.modelix.model.operations.OTBranch
3639
import org.modelix.model.sync.bulk.ModelImporter
3740
import org.modelix.model.sync.bulk.importFilesAsRootChildren
3841
import org.modelix.model.sync.bulk.isModuleIncluded
@@ -84,11 +87,20 @@ abstract class ImportIntoModelServer @Inject constructor(of: ObjectFactory) : De
8487
runBlocking {
8588
client.init()
8689
client.runWrite(branchRef) { rootNode ->
87-
logger.info("Got root node: {}", rootNode)
88-
logger.info("Importing...")
89-
ModelImporter(rootNode, continueOnError.get()).importFilesAsRootChildren(files)
90-
logger.info("Import finished")
90+
rootNode.runBulkUpdate {
91+
logger.info("Got root node: {}", rootNode)
92+
logger.info("Importing...")
93+
ModelImporter(rootNode, continueOnError.get()).importFilesAsRootChildren(files)
94+
logger.info("Import finished")
95+
}
9196
}
9297
}
9398
}
9499
}
100+
101+
/**
102+
* Memory optimization that doesn't record individual change operations, but only the result.
103+
*/
104+
private fun INode.runBulkUpdate(body: () -> Unit) {
105+
((this as PNodeAdapter).branch as OTBranch).runBulkUpdate(body = body)
106+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing,
9+
* software distributed under the License is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11+
* KIND, either express or implied. See the License for the
12+
* specific language governing permissions and limitations
13+
* under the License.
14+
*/
15+
16+
package org.modelix.model.operations
17+
18+
import org.modelix.model.api.ITree
19+
import org.modelix.model.api.IWriteTransaction
20+
import org.modelix.model.lazy.CLTree
21+
import org.modelix.model.lazy.IDeserializingKeyValueStore
22+
import org.modelix.model.lazy.KVEntryReference
23+
import org.modelix.model.persistent.CPTree
24+
import org.modelix.model.persistent.IKVValue
25+
import org.modelix.model.persistent.SerializationUtil
26+
27+
class BulkUpdateOp(
28+
val resultTreeHash: KVEntryReference<CPTree>,
29+
val subtreeRootId: Long,
30+
) : AbstractOperation() {
31+
32+
override fun getReferencedEntries(): List<KVEntryReference<IKVValue>> = listOf(resultTreeHash)
33+
34+
/**
35+
* Since this operation is recorded at the end of a bulk update we need to create an IAppliedOperation without
36+
* actually applying it again.
37+
*/
38+
fun afterApply(baseTree: CLTree) = Applied(baseTree)
39+
40+
override fun apply(transaction: IWriteTransaction, store: IDeserializingKeyValueStore): IAppliedOperation {
41+
val baseTree = transaction.tree as CLTree
42+
val resultTree = getResultTree(store)
43+
TODO("Change the (sub)tree so that it is identical to the resultTree")
44+
return Applied(baseTree)
45+
}
46+
47+
private fun getResultTree(store: IDeserializingKeyValueStore): CLTree = CLTree(resultTreeHash.getValue(store), store)
48+
49+
override fun toString(): String {
50+
return "BulkUpdateOp ${resultTreeHash.getHash()}, ${SerializationUtil.longToHex(subtreeRootId)}"
51+
}
52+
53+
override fun toCode(): String {
54+
return """// TODO BulkUpdateOp"""
55+
}
56+
57+
inner class Applied(val baseTree: CLTree) : AbstractOperation.Applied(), IAppliedOperation {
58+
override fun getOriginalOp() = this@BulkUpdateOp
59+
60+
override fun invert(): List<IOperation> {
61+
return listOf(BulkUpdateOp(KVEntryReference(baseTree.data), subtreeRootId))
62+
}
63+
}
64+
65+
override fun captureIntend(tree: ITree, store: IDeserializingKeyValueStore): IOperationIntend {
66+
return Intend()
67+
}
68+
69+
inner class Intend : IOperationIntend {
70+
override fun restoreIntend(tree: ITree): List<IOperation> {
71+
// The intended change is to put the model into the given state. Any concurrent change can just be
72+
// overwritten as long as the subtree root as the starting point still exists.
73+
return if (tree.containsNode(subtreeRootId)) {
74+
listOf(getOriginalOp())
75+
} else {
76+
listOf(NoOp())
77+
}
78+
}
79+
80+
override fun getOriginalOp() = this@BulkUpdateOp
81+
}
82+
}

model-datastructure/src/commonMain/kotlin/org/modelix/model/operations/OTBranch.kt

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,48 @@ import org.modelix.model.api.ITransaction
2323
import org.modelix.model.api.ITree
2424
import org.modelix.model.api.IWriteTransaction
2525
import org.modelix.model.api.runSynchronized
26+
import org.modelix.model.lazy.CLTree
2627
import org.modelix.model.lazy.IDeserializingKeyValueStore
28+
import org.modelix.model.lazy.KVEntryReference
2729

2830
class OTBranch(
2931
private val branch: IBranch,
3032
private val idGenerator: IIdGenerator,
3133
private val store: IDeserializingKeyValueStore,
3234
) : IBranch {
35+
private var bulkUpdateMode: Boolean = false
3336
private var currentOperations: MutableList<IAppliedOperation> = ArrayList()
3437
private val completedChanges: MutableList<OpsAndTree> = ArrayList()
3538
private val id: String = branch.getId()
3639

40+
/**
41+
* This records all changes as a single operation instead of a long list of fine-grained changes.
42+
* It is assumed that the intended change is to put the model into the resulting state.
43+
*
44+
* @param subtreeRootNodeId if the update is not applied on the whole model, but only on a part of it,
45+
* this ID specifies the root node of that subtree.
46+
*/
47+
fun runBulkUpdate(subtreeRootNodeId: Long = ITree.ROOT_ID, body: () -> Unit) {
48+
check(canWrite()) { "Only allowed inside a write transaction" }
49+
if (bulkUpdateMode) return body()
50+
try {
51+
bulkUpdateMode = true
52+
val baseTree = branch.transaction.tree as CLTree
53+
body()
54+
val resultTree = branch.transaction.tree as CLTree
55+
currentOperations += BulkUpdateOp(KVEntryReference(resultTree.data), subtreeRootNodeId).afterApply(baseTree)
56+
} finally {
57+
bulkUpdateMode = false
58+
}
59+
}
60+
61+
fun isInBulkMode() = bulkUpdateMode
62+
3763
fun operationApplied(op: IAppliedOperation) {
3864
check(canWrite()) { "Only allowed inside a write transaction" }
39-
currentOperations.add(op)
65+
if (!bulkUpdateMode) {
66+
currentOperations.add(op)
67+
}
4068
}
4169

4270
override fun getId(): String {

model-datastructure/src/commonMain/kotlin/org/modelix/model/persistent/OperationSerializer.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import org.modelix.model.lazy.KVEntryReference
2424
import org.modelix.model.operations.AddNewChildOp
2525
import org.modelix.model.operations.AddNewChildSubtreeOp
2626
import org.modelix.model.operations.AddNewChildrenOp
27+
import org.modelix.model.operations.BulkUpdateOp
2728
import org.modelix.model.operations.DeleteNodeOp
2829
import org.modelix.model.operations.IOperation
2930
import org.modelix.model.operations.MoveNodeOp
@@ -130,6 +131,19 @@ class OperationSerializer private constructor() {
130131
}
131132
},
132133
)
134+
INSTANCE.registerSerializer(
135+
BulkUpdateOp::class,
136+
object : Serializer<BulkUpdateOp> {
137+
override fun serialize(op: BulkUpdateOp): String {
138+
return longToHex(op.subtreeRootId) + SEPARATOR + op.resultTreeHash.getHash()
139+
}
140+
141+
override fun deserialize(serialized: String): BulkUpdateOp {
142+
val parts = serialized.split(SEPARATOR).toTypedArray()
143+
return BulkUpdateOp(KVEntryReference(parts[1], CPTree.DESERIALIZER), longFromHex(parts[0]))
144+
}
145+
},
146+
)
133147
INSTANCE.registerSerializer(
134148
DeleteNodeOp::class,
135149
object : Serializer<DeleteNodeOp> {

0 commit comments

Comments
 (0)