Skip to content

Commit 2456877

Browse files
committed
perf(model-server): load model into memory before ModelQL query execution
ModelQL queries often iterate over large parts of the model. To archive a performance similar to the light-model-server (that runs directly on MPS models) the model is copied into a read-only in-memory data structure with optimized performance.
1 parent 383be43 commit 2456877

File tree

8 files changed

+376
-14
lines changed

8 files changed

+376
-14
lines changed

model-datastructure/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ kotlin {
5353
// implementation(libs.guava)
5454
// implementation(libs.apache.commons.io)
5555
// implementation("org.json:json:20230618")
56-
// implementation(libs.trove4j)
56+
implementation(libs.trove4j)
5757
implementation(libs.apache.commons.collections)
5858
//
5959
// implementation("com.google.oauth-client:google-oauth-client:1.34.1")

model-datastructure/src/commonMain/kotlin/org/modelix/model/lazy/BulkQuery.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class BulkQuery(private val store: IDeserializingKeyValueStore) : IBulkQuery {
5353
return Value(value)
5454
}
5555

56-
fun process() {
56+
override fun process() {
5757
if (processing) {
5858
throw RuntimeException("Already processing")
5959
}

model-datastructure/src/commonMain/kotlin/org/modelix/model/lazy/IBulkQuery.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package org.modelix.model.lazy
1818
import org.modelix.model.persistent.IKVValue
1919

2020
interface IBulkQuery {
21+
fun process()
2122
fun <I, O> map(input_: Iterable<I>, f: (I) -> Value<O>): Value<List<O>>
2223
fun <T> constant(value: T): Value<T>
2324
operator fun <T : IKVValue> get(hash: KVEntryReference<T>): Value<T?>

model-datastructure/src/commonMain/kotlin/org/modelix/model/lazy/NonBulkQuery.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ class NonBulkQuery(private val store: IDeserializingKeyValueStore) : IBulkQuery
3131
return constant(hash.getValue(store))
3232
}
3333

34+
override fun process() {
35+
// all requests are processed immediately
36+
}
37+
3438
class Value<T>(private val value: T) : IBulkQuery.Value<T> {
3539
override fun execute(): T {
3640
return value
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
package org.modelix.model.lazy
18+
19+
import org.modelix.model.IKeyValueStore
20+
21+
class NonCachingObjectStore(override val keyValueStore: IKeyValueStore) : IDeserializingKeyValueStore {
22+
23+
override fun <T> getAll(hashes_: Iterable<String>, deserializer: (String, String) -> T): Iterable<T> {
24+
val hashes = hashes_.toList()
25+
val serialized: Map<String, String?> = keyValueStore.getAll(hashes_)
26+
return hashes.map { hash ->
27+
val value = checkNotNull(serialized[hash]) { "Entry not found: $hash" }
28+
deserializer(hash, value)
29+
}
30+
}
31+
32+
override fun <T> get(hash: String, deserializer: (String) -> T): T? {
33+
return keyValueStore.get(hash)?.let(deserializer)
34+
}
35+
36+
override fun put(hash: String, deserialized: Any, serialized: String) {
37+
keyValueStore.put(hash, serialized)
38+
}
39+
40+
override fun prefetch(hash: String) {
41+
keyValueStore.prefetch(hash)
42+
}
43+
}
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
/*
2+
* Copyright (c) 2024.
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+
package org.modelix.model
18+
19+
import gnu.trove.map.TLongObjectMap
20+
import gnu.trove.map.hash.TLongObjectHashMap
21+
import org.modelix.model.api.ConceptReference
22+
import org.modelix.model.api.IBranch
23+
import org.modelix.model.api.IConcept
24+
import org.modelix.model.api.IConceptReference
25+
import org.modelix.model.api.ILanguageRepository
26+
import org.modelix.model.api.INode
27+
import org.modelix.model.api.INodeReference
28+
import org.modelix.model.api.ITree
29+
import org.modelix.model.api.NodeReference
30+
import org.modelix.model.api.PNodeReference
31+
import org.modelix.model.api.resolveInCurrentContext
32+
import org.modelix.model.area.IArea
33+
import org.modelix.model.area.IAreaListener
34+
import org.modelix.model.area.IAreaReference
35+
import org.modelix.model.lazy.CLTree
36+
import org.modelix.model.lazy.KVEntryReference
37+
import org.modelix.model.lazy.NonCachingObjectStore
38+
import org.modelix.model.persistent.CPHamtNode
39+
import org.modelix.model.persistent.CPNode
40+
import org.modelix.model.persistent.CPNodeRef
41+
import kotlin.system.measureTimeMillis
42+
import kotlin.time.Duration.Companion.milliseconds
43+
import kotlin.time.DurationUnit
44+
45+
private val LOG = mu.KotlinLogging.logger { }
46+
47+
class IncrementalInMemoryModel {
48+
private var lastModel: InMemoryModel? = null
49+
50+
@Synchronized
51+
fun getModel(tree: CLTree): InMemoryModel {
52+
val reusable = lastModel?.takeIf { it.branchId == tree.getId() }
53+
val newModel = if (reusable == null) {
54+
InMemoryModel.load(tree)
55+
} else {
56+
reusable.loadIncremental(tree)
57+
}
58+
lastModel = newModel
59+
return newModel
60+
}
61+
}
62+
63+
class InMemoryModel private constructor(
64+
val branchId: String,
65+
val loadedMapRef: KVEntryReference<CPHamtNode>,
66+
val nodeMap: TLongObjectMap<CPNode>,
67+
) {
68+
69+
companion object {
70+
fun load(tree: CLTree): InMemoryModel {
71+
return load(tree.getId(), tree.data.idToHash, tree.store.keyValueStore)
72+
}
73+
74+
fun load(branchId: String, slowMapRef: KVEntryReference<CPHamtNode>, store: IKeyValueStore): InMemoryModel {
75+
val fastMap: TLongObjectMap<CPNode> = TLongObjectHashMap()
76+
val bulkQuery = NonCachingObjectStore(store).newBulkQuery()
77+
LOG.info { "Start loading model into memory" }
78+
val duration = measureTimeMillis {
79+
bulkQuery.get(slowMapRef).onSuccess { slowMap ->
80+
slowMap!!.visitEntries(bulkQuery) { nodeId, nodeDataRef ->
81+
bulkQuery.get(nodeDataRef).onSuccess { nodeData ->
82+
if (nodeData != null) {
83+
fastMap.put(nodeId, nodeData)
84+
}
85+
}
86+
}
87+
}
88+
bulkQuery.process()
89+
}.milliseconds
90+
LOG.info { "Done loading model into memory after ${duration.toDouble(DurationUnit.SECONDS)} s" }
91+
return InMemoryModel(branchId, slowMapRef, fastMap)
92+
}
93+
}
94+
95+
fun loadIncremental(tree: CLTree): InMemoryModel {
96+
return loadIncremental(tree.data.idToHash, tree.store.keyValueStore)
97+
}
98+
fun loadIncremental(slowMapRef: KVEntryReference<CPHamtNode>, store: IKeyValueStore): InMemoryModel {
99+
if (slowMapRef.getHash() == loadedMapRef.getHash()) return this
100+
101+
val fastMap: TLongObjectMap<CPNode> = TLongObjectHashMap()
102+
val bulkQuery = NonCachingObjectStore(store).newBulkQuery()
103+
LOG.debug { "Model update started" }
104+
fastMap.putAll(nodeMap)
105+
val duration = measureTimeMillis {
106+
bulkQuery.map(listOf(slowMapRef, loadedMapRef)) { bulkQuery.get(it) }.onSuccess {
107+
val newSlowMap = it[0]!!
108+
val oldSlowMap = it[1]!!
109+
newSlowMap.visitChanges(
110+
oldSlowMap,
111+
object : CPHamtNode.IChangeVisitor {
112+
override fun visitChangesOnly(): Boolean = false
113+
override fun entryAdded(key: Long, value: KVEntryReference<CPNode>) {
114+
bulkQuery.get(value).onSuccess { nodeData ->
115+
if (nodeData != null) {
116+
fastMap.put(key, nodeData)
117+
}
118+
}
119+
}
120+
override fun entryRemoved(key: Long, value: KVEntryReference<CPNode>) {
121+
fastMap.remove(key)
122+
}
123+
override fun entryChanged(
124+
key: Long,
125+
oldValue: KVEntryReference<CPNode>,
126+
newValue: KVEntryReference<CPNode>,
127+
) {
128+
bulkQuery.get(newValue).onSuccess { nodeData ->
129+
if (nodeData != null) {
130+
fastMap.put(key, nodeData)
131+
}
132+
}
133+
}
134+
},
135+
bulkQuery,
136+
)
137+
}
138+
bulkQuery.process()
139+
}.milliseconds
140+
LOG.info { "Done updating model after ${duration.toDouble(DurationUnit.SECONDS)} s" }
141+
return InMemoryModel(branchId, slowMapRef, fastMap)
142+
}
143+
144+
fun getNodeData(nodeId: Long): CPNode = nodeMap.get(nodeId)
145+
fun getNode(nodeId: Long): InMemoryNode = InMemoryNode(this, nodeId)
146+
147+
fun getArea() = Area()
148+
149+
inner class Area : IArea {
150+
override fun getRoot(): INode {
151+
return getNode(ITree.ROOT_ID)
152+
}
153+
154+
override fun resolveConcept(ref: IConceptReference): IConcept? {
155+
TODO("Not yet implemented")
156+
}
157+
158+
override fun resolveNode(ref: INodeReference): INode? {
159+
return resolveOriginalNode(ref)
160+
}
161+
162+
override fun resolveOriginalNode(ref: INodeReference): INode? {
163+
return when (ref) {
164+
is PNodeReference -> getNode(ref.id).takeIf { ref.branchId == branchId }
165+
is InMemoryNode -> ref
166+
is NodeReference -> PNodeReference.tryDeserialize(ref.serialized)?.let { resolveOriginalNode(it) }
167+
else -> null
168+
}
169+
}
170+
171+
override fun resolveBranch(id: String): IBranch? {
172+
TODO("Not yet implemented")
173+
}
174+
175+
override fun collectAreas(): List<IArea> {
176+
TODO("Not yet implemented")
177+
}
178+
179+
override fun getReference(): IAreaReference {
180+
TODO("Not yet implemented")
181+
}
182+
183+
override fun resolveArea(ref: IAreaReference): IArea? {
184+
TODO("Not yet implemented")
185+
}
186+
187+
override fun <T> executeRead(f: () -> T): T {
188+
return f()
189+
}
190+
191+
override fun <T> executeWrite(f: () -> T): T {
192+
throw UnsupportedOperationException("read-only")
193+
}
194+
195+
override fun canRead(): Boolean {
196+
return true
197+
}
198+
199+
override fun canWrite(): Boolean {
200+
return false
201+
}
202+
203+
override fun addListener(l: IAreaListener) {
204+
TODO("Not yet implemented")
205+
}
206+
207+
override fun removeListener(l: IAreaListener) {
208+
TODO("Not yet implemented")
209+
}
210+
}
211+
}
212+
213+
data class InMemoryNode(val model: InMemoryModel, val nodeId: Long) : INode, INodeReference {
214+
fun getNodeData(): CPNode = model.getNodeData(nodeId)
215+
216+
override fun serialize(): String {
217+
return PNodeReference(nodeId, model.branchId).serialize()
218+
}
219+
220+
override fun getPropertyValue(role: String): String? = getNodeData().getPropertyValue(role)
221+
222+
override fun setPropertyValue(role: String, value: String?): Unit = throw UnsupportedOperationException("read-only")
223+
224+
override fun getArea(): IArea {
225+
return model.getArea()
226+
}
227+
228+
override val isValid: Boolean
229+
get() = model.nodeMap.containsKey(nodeId)
230+
override val reference: INodeReference
231+
get() = this
232+
override val concept: IConcept?
233+
get() = getConceptReference()?.let { ILanguageRepository.resolveConcept(it) }
234+
override val roleInParent: String?
235+
get() = getNodeData().roleInParent
236+
override val parent: INode?
237+
get() = getNodeData().parentId.takeIf { it != 0L }?.let { InMemoryNode(model, it) }
238+
239+
override fun getConceptReference(): IConceptReference? {
240+
return getNodeData().concept?.let { ConceptReference(it) }
241+
}
242+
243+
override fun getChildren(role: String?): Iterable<INode> {
244+
return allChildren.filter { it.roleInParent == role }
245+
}
246+
247+
override val allChildren: Iterable<INode>
248+
get() = getNodeData().getChildrenIds().map { InMemoryNode(model, it) }
249+
250+
override fun moveChild(role: String?, index: Int, child: INode) {
251+
throw UnsupportedOperationException("read-only")
252+
}
253+
254+
override fun addNewChild(role: String?, index: Int, concept: IConcept?): INode {
255+
throw UnsupportedOperationException("read-only")
256+
}
257+
258+
override fun removeChild(child: INode) {
259+
throw UnsupportedOperationException("read-only")
260+
}
261+
262+
override fun getReferenceTarget(role: String): INode? {
263+
val targetRef = getNodeData().getReferenceTarget(role)
264+
return when {
265+
targetRef == null -> null
266+
targetRef.isLocal -> InMemoryNode(model, targetRef.elementId)
267+
targetRef is CPNodeRef.ForeignRef -> NodeReference(targetRef.serializedRef).resolveInCurrentContext()
268+
else -> throw UnsupportedOperationException("Unsupported reference: $targetRef")
269+
}
270+
}
271+
272+
override fun getReferenceTargetRef(role: String): INodeReference? {
273+
val targetRef = getNodeData().getReferenceTarget(role)
274+
return when {
275+
targetRef == null -> null
276+
targetRef.isLocal -> InMemoryNode(model, targetRef.elementId).reference
277+
targetRef is CPNodeRef.ForeignRef -> NodeReference(targetRef.serializedRef)
278+
else -> throw UnsupportedOperationException("Unsupported reference: $targetRef")
279+
}
280+
}
281+
282+
override fun setReferenceTarget(role: String, target: INode?) {
283+
throw UnsupportedOperationException("read-only")
284+
}
285+
286+
override fun getPropertyRoles(): List<String> {
287+
return getNodeData().propertyRoles.toList()
288+
}
289+
290+
override fun getReferenceRoles(): List<String> {
291+
return getNodeData().referenceRoles.toList()
292+
}
293+
}

0 commit comments

Comments
 (0)