@@ -25,6 +25,7 @@ import org.modelix.model.api.SerializedNodeReference
25
25
import org.modelix.model.api.getDescendants
26
26
import org.modelix.model.api.isChildRoleOrdered
27
27
import org.modelix.model.api.remove
28
+ import org.modelix.model.api.resolveInCurrentContext
28
29
import org.modelix.model.data.ModelData
29
30
import org.modelix.model.data.NodeData
30
31
import kotlin.jvm.JvmName
@@ -48,11 +49,17 @@ class ModelImporter(
48
49
private val continueOnError : Boolean ,
49
50
private val childFilter : (INode ) -> Boolean = { true },
50
51
) {
52
+ // We have seen imports where the `originalIdToExisting` had a dozen ten million entries.
53
+ // Therefore, choose a map with is optimized for memory usage.
54
+ // For the same reason store `INodeReference`s instead of `INode`s.
55
+ // In a few cases, where we need the `INode` we can resolve it.
56
+ private val originalIdToExisting by lazy(::buildExistingIndex)
51
57
52
- private val originalIdToExisting: MutableMap <String , INode > = mutableMapOf ()
58
+ // Use`INode` instead of `INodeReference` in `postponedReferences` and `nodesToRemove`
59
+ // because we know that we will always need the `INode`s in those cases.
60
+ // Those cases are deleting nodes and adding references to nodes.
53
61
private val postponedReferences = mutableListOf<PostponedReference >()
54
62
private val nodesToRemove = HashSet <INode >()
55
- private var numExpectedNodes = 0
56
63
private var currentNodeProgress = 0
57
64
private val logger = KotlinLogging .logger {}
58
65
@@ -96,34 +103,51 @@ class ModelImporter(
96
103
*/
97
104
@JvmName(" importData" )
98
105
fun import (data : ModelData ) {
99
- INodeResolutionScope .runWithAdditionalScope(root.getArea()) {
100
- logImportSize(data.root, logger)
101
- logger.info { " Building indices for import..." }
102
- originalIdToExisting.clear()
103
- postponedReferences.clear()
104
- nodesToRemove.clear()
105
- numExpectedNodes = countExpectedNodes(data.root)
106
- val progressReporter = ProgressReporter (numExpectedNodes.toULong(), logger)
107
- currentNodeProgress = 0
108
- buildExistingIndex(root)
106
+ importIntoNodes(sequenceOf(ExistingAndExpectedNode (root, data)))
107
+ }
109
108
110
- logger.info { " Importing nodes..." }
111
- data.root.originalId()?.let { originalIdToExisting[it] = root }
112
- syncNode(root, data.root, progressReporter)
109
+ /* *
110
+ * Incrementally updates existing children of the given with specified data.
111
+ *
112
+ * @param nodeCombinationsToImport Combinations of an old existing child and the new expected data.
113
+ * The combinations are consumed lazily.
114
+ * Callers can use this to load expected data on demand.
115
+ */
116
+ fun importIntoNodes (nodeCombinationsToImport : Sequence <ExistingAndExpectedNode >) {
117
+ logger.info { " Building indices for import..." }
118
+ postponedReferences.clear()
119
+ nodesToRemove.clear()
113
120
114
- logger.info { " Synchronizing references..." }
115
- postponedReferences.forEach { it.setPostponedReference() }
121
+ nodeCombinationsToImport.forEach { nodeCombination ->
122
+ importIntoNode(nodeCombination.expectedNodeData, nodeCombination.existingNode)
123
+ }
116
124
117
- logger.info { " Removing extra nodes..." }
118
- nodesToRemove.forEach {
119
- doAndPotentiallyContinueOnErrors {
120
- if (it.isValid) { // if it's invalid then it's already removed
121
- it.remove()
122
- }
125
+ logger.info { " Synchronizing references..." }
126
+ postponedReferences.forEach { it.setPostponedReference() }
127
+
128
+ logger.info { " Removing extra nodes..." }
129
+ nodesToRemove.forEach {
130
+ doAndPotentiallyContinueOnErrors {
131
+ if (it.isValid) { // if it's invalid then it's already removed
132
+ it.remove()
123
133
}
124
134
}
135
+ }
125
136
126
- logger.info { " Synchronization finished." }
137
+ logger.info { " Synchronization finished." }
138
+ }
139
+
140
+ private fun importIntoNode (expectedNodeData : ModelData , existingNode : INode = root) {
141
+ INodeResolutionScope .runWithAdditionalScope(existingNode.getArea()) {
142
+ logImportSize(expectedNodeData.root, logger)
143
+ logger.info { " Building indices for nodes import..." }
144
+ currentNodeProgress = 0
145
+ val numExpectedNodes = countExpectedNodes(expectedNodeData.root)
146
+ val progressReporter = ProgressReporter (numExpectedNodes.toULong(), logger)
147
+
148
+ logger.info { " Importing nodes..." }
149
+ expectedNodeData.root.originalId()?.let { originalIdToExisting[it] = existingNode.reference }
150
+ syncNode(existingNode, expectedNodeData.root, progressReporter)
127
151
}
128
152
}
129
153
@@ -145,15 +169,25 @@ class ModelImporter(
145
169
for (role in allRoles) {
146
170
val expectedNodes = expectedParent.children.filter { it.role == role }
147
171
val existingNodes = existingParent.getChildren(role).filter(childFilter).toList()
172
+ val allExpectedNodesDoNotExist by lazy {
173
+ expectedNodes.all { expectedNode ->
174
+ val originalId = expectedNode.originalId()
175
+ checkNotNull(originalId) { " Specified node '$expectedNode ' has no ID." }
176
+ originalIdToExisting[originalId] == null
177
+ }
178
+ }
148
179
149
180
// optimization that uses the bulk operation .addNewChildren
150
- if (existingNodes.isEmpty() && expectedNodes.all { originalIdToExisting[it.originalId()] == null }) {
151
- existingParent.addNewChildren(role, - 1 , expectedNodes.map { it.concept?.let { ConceptReference (it) } }).zip(expectedNodes).forEach { (newChild, expected) ->
152
- val expectedId = checkNotNull(expected.originalId()) { " Specified node '$expected ' has no id" }
153
- newChild.setPropertyValue(NodeData .idPropertyKey, expectedId)
154
- originalIdToExisting[expectedId] = newChild
155
- syncNode(newChild, expected, progressReporter)
156
- }
181
+ if (existingNodes.isEmpty() && allExpectedNodesDoNotExist) {
182
+ existingParent.addNewChildren(role, - 1 , expectedNodes.map { it.concept?.let { ConceptReference (it) } })
183
+ .zip(expectedNodes)
184
+ .forEach { (newChild, expected) ->
185
+ val expectedId = expected.originalId()
186
+ checkNotNull(expectedId) { " Specified node '$expected ' has no ID." }
187
+ newChild.setPropertyValue(NodeData .idPropertyKey, expectedId)
188
+ originalIdToExisting[expectedId] = newChild.reference
189
+ syncNode(newChild, expected, progressReporter)
190
+ }
157
191
continue
158
192
}
159
193
@@ -186,13 +220,18 @@ class ModelImporter(
186
220
val nodeAtIndex = existingChildren.getOrNull(newIndex)
187
221
val expectedConcept = expected.concept?.let { s -> ConceptReference (s) }
188
222
val childNode = if (nodeAtIndex?.originalId() != expectedId) {
189
- val existingNode = originalIdToExisting[expectedId]
190
- if (existingNode == null ) {
223
+ val existingNodeReference = originalIdToExisting[expectedId]
224
+ if (existingNodeReference == null ) {
191
225
val newChild = existingParent.addNewChild(role, newIndex, expectedConcept)
192
226
newChild.setPropertyValue(NodeData .idPropertyKey, expectedId)
193
- originalIdToExisting[expectedId] = newChild
227
+ originalIdToExisting[expectedId] = newChild.reference
194
228
newChild
195
229
} else {
230
+ val existingNode = existingNodeReference.resolveInCurrentContext()
231
+ checkNotNull(existingNode) {
232
+ // This reference should always be resolvable because the node existed or was created before.
233
+ " Could not resolve $existingNodeReference ."
234
+ }
196
235
// The existing child node is not only moved to a new index,
197
236
// it is potentially moved to a new parent and role.
198
237
existingParent.moveChild(role, newIndex, existingNode)
@@ -217,10 +256,12 @@ class ModelImporter(
217
256
}
218
257
}
219
258
220
- private fun buildExistingIndex (root : INode ) {
259
+ private fun buildExistingIndex (): MemoryEfficientMap <String , INodeReference > {
260
+ val localOriginalIdToExisting = MemoryEfficientMap <String , INodeReference >()
221
261
root.getDescendants(true ).forEach { node ->
222
- node.originalId()?.let { originalIdToExisting [it] = node }
262
+ node.originalId()?.let { localOriginalIdToExisting [it] = node.reference }
223
263
}
264
+ return localOriginalIdToExisting
224
265
}
225
266
226
267
private fun syncProperties (node : INode , nodeData : NodeData ) {
@@ -268,3 +309,8 @@ internal fun INode.originalId(): String? {
268
309
internal fun NodeData.originalId (): String? {
269
310
return properties[NodeData .idPropertyKey] ? : id
270
311
}
312
+
313
+ data class ExistingAndExpectedNode (
314
+ val existingNode : INode ,
315
+ val expectedNodeData : ModelData ,
316
+ )
0 commit comments