Skip to content

Commit acdb119

Browse files
slissonmhuster23
authored andcommitted
feat(model-api): INode.addNewChildren with better performance for bulk imports
It can create multiple children at the same time, allowing the copy-on-write implementation to update the parent node only once instead of creating a new version of the node for each added child.
1 parent a0c11c2 commit acdb119

File tree

23 files changed

+387
-101
lines changed

23 files changed

+387
-101
lines changed

bulk-model-sync-lib/src/commonMain/kotlin/org/modelix/model/sync/bulk/ModelImporter.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,17 @@ class ModelImporter(private val root: INode, private val continueOnError: Boolea
122122
val expectedNodes = data.children.filter { it.role == role }
123123
val existingNodes = node.getChildren(role).toList()
124124

125+
// optimization that uses the bulk operation .addNewChildren
126+
if (existingNodes.isEmpty() && expectedNodes.all { originalIdToExisting[it.originalId()] == null }) {
127+
node.addNewChildren(role, -1, expectedNodes.map { it.concept?.let { ConceptReference(it) } }).zip(expectedNodes).forEach { (newChild, expected) ->
128+
val expectedId = checkNotNull(expected.originalId()) { "Specified node '$expected' has no id" }
129+
newChild.setPropertyValue(NodeData.idPropertyKey, expectedId)
130+
originalIdToExisting[expectedId] = newChild
131+
syncNode(newChild, expected)
132+
}
133+
continue
134+
}
135+
125136
// optimization for when there is no change in the child list
126137
// size check first to avoid querying the original ID
127138
if (expectedNodes.size == existingNodes.size && expectedNodes.map { it.originalId() } == existingNodes.map { it.originalId() }) {

model-api/src/commonMain/kotlin/org/modelix/model/api/INode.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ interface INode {
117117
return addNewChild(role, index, concept?.resolve())
118118
}
119119

120+
fun addNewChildren(role: String?, index: Int, concepts: List<IConceptReference?>): List<INode> {
121+
return concepts.map { addNewChild(role, index, it) }
122+
}
123+
120124
/**
121125
* Removes the given node from this node's children.
122126
*

model-api/src/commonMain/kotlin/org/modelix/model/api/IWriteTransaction.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ interface IWriteTransaction : ITransaction {
102102
*/
103103
fun addNewChild(parentId: Long, role: String?, index: Int, childId: Long, concept: IConceptReference?)
104104

105+
fun addNewChildren(parentId: Long, role: String?, index: Int, concepts: Array<IConceptReference?>): LongArray
106+
fun addNewChildren(parentId: Long, role: String?, index: Int, childIds: LongArray, concepts: Array<IConceptReference?>)
107+
105108
/**
106109
* Deletes the given node.
107110
*

model-api/src/commonMain/kotlin/org/modelix/model/api/PNodeAdapter.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ open class PNodeAdapter(val nodeId: Long, val branch: IBranch) : INode, INodeEx
6868
return PNodeAdapter(branch.writeTransaction.addNewChild(nodeId, role, index, concept), branch)
6969
}
7070

71+
override fun addNewChildren(role: String?, index: Int, concepts: List<IConceptReference?>): List<INode> {
72+
return branch.writeTransaction.addNewChildren(nodeId, role, index, concepts.toTypedArray()).map {
73+
PNodeAdapter(it, branch)
74+
}
75+
}
76+
7177
override fun addNewChild(role: IChildLink, index: Int, concept: IConcept?): INode {
7278
return PNodeAdapter(branch.writeTransaction.addNewChild(nodeId, role.key(this), index, concept), branch)
7379
}

model-api/src/commonMain/kotlin/org/modelix/model/api/PNodeReference.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,47 @@ data class PNodeReference(val id: Long, val branchId: String) : INodeReference {
2727

2828
fun toLocal() = LocalPNodeReference(id)
2929

30+
override fun serialize(): String {
31+
return "${id.toString(16)}@$branchId"
32+
}
33+
3034
override fun toString(): String {
3135
return "PNodeReference_${id.toString(16)}@$branchId"
3236
}
37+
38+
companion object {
39+
fun deserialize(serialized: String): PNodeReference {
40+
return requireNotNull(tryDeserialize(serialized)) {
41+
"Not a valid PNodeReference: $serialized"
42+
}
43+
}
44+
fun tryDeserialize(serialized: String): PNodeReference? {
45+
if (serialized.startsWith("pnode:") && serialized.contains('@')) {
46+
val withoutPrefix = serialized.substringAfter("pnode:")
47+
val parts = withoutPrefix.split('@', limit = 2)
48+
if (parts.size != 2) return null
49+
val nodeId = parts[0].toLongOrNull(16) ?: return null
50+
return PNodeReference(nodeId, parts[1])
51+
} else if (serialized.startsWith("modelix:") && serialized.contains('/')) {
52+
// This would be a nicer serialization format that isn't used yet, but supporting it already will make
53+
// future changes easier without breaking old versions of this library.
54+
//
55+
// Example: modelix:25038f9e-e8ad-470a-9ae8-6978ed172184/1a5003b818f
56+
// Old format: pnode:1a5003b818f@25038f9e-e8ad-470a-9ae8-6978ed172184
57+
//
58+
// The 'modelix' prefix is more intuitive for a node stored inside a Modelix repository.
59+
// Having the repository ID first also feels more natural.
60+
val withoutPrefix = serialized.substringAfter("modelix:")
61+
val nodeIdStr = withoutPrefix.substringAfterLast('/')
62+
val branchId = withoutPrefix.substringBeforeLast('/')
63+
val nodeId = nodeIdStr.toLongOrNull(16)
64+
if (nodeId != null && branchId.isNotEmpty()) {
65+
return PNodeReference(nodeId, branchId)
66+
}
67+
}
68+
return null
69+
}
70+
}
3371
}
3472

3573
object PNodeReferenceSerializer : INodeReferenceSerializerEx {

model-api/src/commonMain/kotlin/org/modelix/model/api/TreePointer.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,27 @@ class TreePointer(private var tree_: ITree, val idGenerator: IIdGenerator = IdGe
117117
tree = tree.addNewChild(parentId, role, index, childId, concept)
118118
}
119119

120+
override fun addNewChildren(
121+
parentId: Long,
122+
role: String?,
123+
index: Int,
124+
concepts: Array<IConceptReference?>,
125+
): LongArray {
126+
val newIds = concepts.map { idGenerator.generate() }.toLongArray()
127+
addNewChildren(parentId, role, index, newIds, concepts)
128+
return newIds
129+
}
130+
131+
override fun addNewChildren(
132+
parentId: Long,
133+
role: String?,
134+
index: Int,
135+
childIds: LongArray,
136+
concepts: Array<IConceptReference?>,
137+
) {
138+
tree = tree.addNewChildren(parentId, role, index, childIds, concepts)
139+
}
140+
120141
override fun deleteNode(nodeId: Long) {
121142
tree = tree.deleteNode(nodeId)
122143
}

model-api/src/commonMain/kotlin/org/modelix/model/api/WriteTransaction.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ class WriteTransaction(_tree: ITree, branch: IBranch, idGenerator: IIdGenerator)
7979
tree = tree.addNewChild(parentId, role, index, childId, concept)
8080
}
8181

82+
override fun addNewChildren(parentId: Long, role: String?, index: Int, childIds: LongArray, concepts: Array<IConceptReference?>) {
83+
checkNotClosed()
84+
tree = tree.addNewChildren(parentId, role, index, childIds, concepts)
85+
}
86+
87+
override fun addNewChildren(parentId: Long, role: String?, index: Int, concepts: Array<IConceptReference?>): LongArray {
88+
checkNotClosed()
89+
val newIds = concepts.map { idGenerator.generate() }.toLongArray()
90+
addNewChildren(parentId, role, index, newIds, concepts)
91+
return newIds
92+
}
93+
8294
override fun deleteNode(nodeId: Long) {
8395
checkNotClosed()
8496
tree.getAllChildren(nodeId).forEach { deleteNode(it) }
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2023.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import org.modelix.model.api.PNodeReference
18+
import kotlin.test.Test
19+
import kotlin.test.assertEquals
20+
21+
class ReferenceSerializationTests {
22+
23+
@Test
24+
fun deserializePNodeReferenceOldFormat() {
25+
assertEquals(
26+
PNodeReference(0xabcd1234, "2bfd9f5e-95d0-11ee-b9d1-0242ac120002"),
27+
PNodeReference.deserialize("pnode:abcd1234@2bfd9f5e-95d0-11ee-b9d1-0242ac120002"),
28+
)
29+
}
30+
31+
@Test
32+
fun deserializePNodeReferenceNewFormat() {
33+
assertEquals(
34+
PNodeReference(0xabcd1234, "2bfd9f5e-95d0-11ee-b9d1-0242ac120002"),
35+
PNodeReference.deserialize("modelix:2bfd9f5e-95d0-11ee-b9d1-0242ac120002/abcd1234"),
36+
)
37+
}
38+
}

model-client/src/commonMain/kotlin/org/modelix/model/AutoTransactionsBranch.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ class AutoWriteTransaction(branch: IBranch) : AutoTransaction(branch), IWriteTra
6969
branch.computeWriteT { it.addNewChild(parentId, role, index, concept) }
7070
override fun addNewChild(parentId: Long, role: String?, index: Int, childId: Long, concept: IConcept?) =
7171
branch.computeWriteT { it.addNewChild(parentId, role, index, childId, concept) }
72+
override fun addNewChildren(parentId: Long, role: String?, index: Int, concepts: Array<IConceptReference?>) =
73+
branch.computeWriteT { it.addNewChildren(parentId, role, index, concepts) }
74+
override fun addNewChildren(parentId: Long, role: String?, index: Int, childIds: LongArray, concepts: Array<IConceptReference?>) =
75+
branch.computeWriteT { it.addNewChildren(parentId, role, index, childIds, concepts) }
7276
override fun addNewChild(
7377
parentId: Long,
7478
role: String?,

model-client/src/commonMain/kotlin/org/modelix/model/IncrementalBranch.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,34 @@ class IncrementalBranch(val branch: IBranch) : IBranch, IBranchWrapper {
280280
return childId
281281
}
282282

283+
override fun addNewChildren(
284+
parentId: Long,
285+
role: String?,
286+
index: Int,
287+
concepts: Array<IConceptReference?>,
288+
): LongArray {
289+
val childIds = transaction.addNewChildren(parentId, role, index, concepts)
290+
modified(ChildrenDependency(this@IncrementalBranch, parentId, role))
291+
childIds.forEach {
292+
modified(UnclassifiedNodeDependency(this@IncrementalBranch, it)) // see .containsNode
293+
}
294+
return childIds
295+
}
296+
297+
override fun addNewChildren(
298+
parentId: Long,
299+
role: String?,
300+
index: Int,
301+
childIds: LongArray,
302+
concepts: Array<IConceptReference?>,
303+
) {
304+
transaction.addNewChildren(parentId, role, index, childIds, concepts)
305+
modified(ChildrenDependency(this@IncrementalBranch, parentId, role))
306+
childIds.forEach {
307+
modified(UnclassifiedNodeDependency(this@IncrementalBranch, it)) // see .containsNode
308+
}
309+
}
310+
283311
override fun addNewChild(parentId: Long, role: String?, index: Int, childId: Long, concept: IConcept?) {
284312
transaction.addNewChild(parentId, role, index, childId, concept)
285313
modified(ChildrenDependency(this@IncrementalBranch, parentId, role))

0 commit comments

Comments
 (0)