Skip to content

Commit 59cf077

Browse files
author
Oleksandr Dzhychko
committed
perf(bulk-model-sync): load module data into memory only on demand and release it after import
Load module data and consume module data lazily. Do not hold references to imported module data after importing a module so that it can be garbage collected.
1 parent 5e1934d commit 59cf077

File tree

3 files changed

+86
-39
lines changed

3 files changed

+86
-39
lines changed

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

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,13 @@ class ModelImporter(
5353
// Therefore, choose a map with is optimized for memory usage.
5454
// For the same reason store `INodeReference`s instead of `INode`s.
5555
// In a few cases, where we need the `INode` we can resolve it.
56-
private var originalIdToExisting = MemoryEfficientMap<String, INodeReference>()
56+
private val originalIdToExisting by lazy(::buildExistingIndex)
5757

5858
// Use`INode` instead of `INodeReference` in `postponedReferences` and `nodesToRemove`
5959
// because we know that we will always need the `INode`s in those cases.
6060
// Those cases are deleting nodes and adding references to nodes.
6161
private val postponedReferences = mutableListOf<PostponedReference>()
6262
private val nodesToRemove = HashSet<INode>()
63-
private var numExpectedNodes = 0
6463
private var currentNodeProgress = 0
6564
private val logger = KotlinLogging.logger {}
6665

@@ -104,34 +103,51 @@ class ModelImporter(
104103
*/
105104
@JvmName("importData")
106105
fun import(data: ModelData) {
107-
INodeResolutionScope.runWithAdditionalScope(root.getArea()) {
108-
logImportSize(data.root, logger)
109-
logger.info { "Building indices for import..." }
110-
originalIdToExisting = MemoryEfficientMap()
111-
postponedReferences.clear()
112-
nodesToRemove.clear()
113-
numExpectedNodes = countExpectedNodes(data.root)
114-
val progressReporter = ProgressReporter(numExpectedNodes.toULong(), logger)
115-
currentNodeProgress = 0
116-
buildExistingIndex(root)
106+
importIntoNodes(sequenceOf(ExistingAndExpectedNode(root, data)))
107+
}
117108

118-
logger.info { "Importing nodes..." }
119-
data.root.originalId()?.let { originalIdToExisting[it] = root.reference }
120-
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()
121120

122-
logger.info { "Synchronizing references..." }
123-
postponedReferences.forEach { it.setPostponedReference() }
121+
nodeCombinationsToImport.forEach { nodeCombination ->
122+
importIntoNode(nodeCombination.expectedNodeData, nodeCombination.existingNode)
123+
}
124124

125-
logger.info { "Removing extra nodes..." }
126-
nodesToRemove.forEach {
127-
doAndPotentiallyContinueOnErrors {
128-
if (it.isValid) { // if it's invalid then it's already removed
129-
it.remove()
130-
}
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()
131133
}
132134
}
135+
}
133136

134-
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)
135151
}
136152
}
137153

@@ -240,10 +256,12 @@ class ModelImporter(
240256
}
241257
}
242258

243-
private fun buildExistingIndex(root: INode) {
259+
private fun buildExistingIndex(): MemoryEfficientMap<String, INodeReference> {
260+
val localOriginalIdToExisting = MemoryEfficientMap<String, INodeReference>()
244261
root.getDescendants(true).forEach { node ->
245-
node.originalId()?.let { originalIdToExisting[it] = node.reference }
262+
node.originalId()?.let { localOriginalIdToExisting[it] = node.reference }
246263
}
264+
return localOriginalIdToExisting
247265
}
248266

249267
private fun syncProperties(node: INode, nodeData: NodeData) {
@@ -291,3 +309,8 @@ internal fun INode.originalId(): String? {
291309
internal fun NodeData.originalId(): String? {
292310
return properties[NodeData.idPropertyKey] ?: id
293311
}
312+
313+
data class ExistingAndExpectedNode(
314+
val existingNode: INode,
315+
val expectedNodeData: ModelData,
316+
)

bulk-model-sync-mps/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dependencies {
2121

2222
implementation(kotlin("stdlib"))
2323
implementation(libs.kotlin.logging)
24+
implementation(libs.kotlin.serialization.json)
2425
}
2526

2627
publishing {

bulk-model-sync-mps/src/main/kotlin/org/modelix/mps/model/sync/bulk/MPSBulkSynchronizer.kt

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@ package org.modelix.mps.model.sync.bulk
1919
import com.intellij.openapi.application.ApplicationManager
2020
import com.intellij.openapi.project.ProjectManager
2121
import jetbrains.mps.ide.project.ProjectHelper
22+
import kotlinx.serialization.ExperimentalSerializationApi
23+
import kotlinx.serialization.json.Json
24+
import kotlinx.serialization.json.decodeFromStream
25+
import org.jetbrains.mps.openapi.module.SModule
2226
import org.jetbrains.mps.openapi.module.SRepository
23-
import org.modelix.model.api.BuiltinLanguages
24-
import org.modelix.model.api.INode
27+
import org.modelix.model.data.ModelData
2528
import org.modelix.model.mpsadapters.MPSModuleAsNode
2629
import org.modelix.model.mpsadapters.MPSRepositoryAsNode
30+
import org.modelix.model.sync.bulk.ExistingAndExpectedNode
2731
import org.modelix.model.sync.bulk.ModelExporter
2832
import org.modelix.model.sync.bulk.ModelImporter
29-
import org.modelix.model.sync.bulk.importFilesAsRootChildren
3033
import org.modelix.model.sync.bulk.isModuleIncluded
3134
import java.io.File
3235
import java.util.concurrent.atomic.AtomicInteger
@@ -37,7 +40,8 @@ object MPSBulkSynchronizer {
3740
fun exportRepository() {
3841
val repository = getRepository()
3942
val includedModuleNames = parseRawPropertySet(System.getProperty("modelix.mps.model.sync.bulk.output.modules"))
40-
val includedModulePrefixes = parseRawPropertySet(System.getProperty("modelix.mps.model.sync.bulk.output.modules.prefixes"))
43+
val includedModulePrefixes =
44+
parseRawPropertySet(System.getProperty("modelix.mps.model.sync.bulk.output.modules.prefixes"))
4145

4246
repository.modelAccess.runReadAction {
4347
val allModules = repository.modules
@@ -62,6 +66,7 @@ object MPSBulkSynchronizer {
6266
}
6367
}
6468

69+
@OptIn(ExperimentalSerializationApi::class)
6570
@JvmStatic
6671
fun importRepository() {
6772
val repository = getRepository()
@@ -70,26 +75,44 @@ object MPSBulkSynchronizer {
7075
val inputPath = System.getProperty("modelix.mps.model.sync.bulk.input.path")
7176
val continueOnError = System.getProperty("modelix.mps.model.sync.bulk.input.continueOnError", "false").toBoolean()
7277
val jsonFiles = File(inputPath).listFiles()?.filter {
73-
it.extension == "json" && isModuleIncluded(it.nameWithoutExtension, includedModuleNames, includedModulePrefixes)
78+
it.extension == "json" && isModuleIncluded(
79+
it.nameWithoutExtension,
80+
includedModuleNames,
81+
includedModulePrefixes,
82+
)
7483
}
7584

7685
if (jsonFiles.isNullOrEmpty()) error("no json files found for included modules")
7786

7887
println("Found ${jsonFiles.size} modules to be imported")
7988
val access = repository.modelAccess
8089
access.runWriteInEDT {
90+
val allModules = repository.modules
91+
val includedModules: Iterable<SModule> = allModules.filter {
92+
isModuleIncluded(it.moduleName!!, includedModuleNames, includedModulePrefixes)
93+
}
94+
val numIncludedModules = includedModules.count()
8195
access.executeCommand {
8296
val repoAsNode = MPSRepositoryAsNode(repository)
83-
84-
// Without the filter MPS would attempt to delete all modules that are not included
85-
fun moduleFilter(node: INode): Boolean {
86-
if (node.getConceptReference()?.getUID() != BuiltinLanguages.MPSRepositoryConcepts.Module.getUID()) return true
87-
val moduleName = node.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) ?: return false
88-
return isModuleIncluded(moduleName, includedModuleNames, includedModulePrefixes)
89-
}
9097
println("Importing modules...")
9198
try {
92-
ModelImporter(repoAsNode, continueOnError, childFilter = ::moduleFilter).importFilesAsRootChildren(jsonFiles)
99+
println("Importing modules...")
100+
// `modulesToImport` lazily produces modules to import
101+
// so that loaded model data can be garbage collected.
102+
val modulesToImport = includedModules.asSequence().flatMapIndexed { index, module ->
103+
println("Importing module ${index + 1} of $numIncludedModules: '${module.moduleName}'")
104+
val fileName = inputPath + File.separator + module.moduleName + ".json"
105+
val moduleFile = File(fileName)
106+
if (moduleFile.exists()) {
107+
val expectedData: ModelData = moduleFile.inputStream().use(Json::decodeFromStream)
108+
sequenceOf(ExistingAndExpectedNode(MPSModuleAsNode(module), expectedData))
109+
} else {
110+
println("Skip importing ${module.moduleName}} because $fileName does not exist.")
111+
sequenceOf()
112+
}
113+
}
114+
ModelImporter(repoAsNode, continueOnError).importIntoNodes(modulesToImport)
115+
println("Import finished.")
93116
} catch (ex: Exception) {
94117
// Exceptions are only visible in the MPS log file by default
95118
ex.printStackTrace()

0 commit comments

Comments
 (0)