Skip to content

Commit 0634b85

Browse files
S-furiDanySK
authored andcommitted
perf: improve speed, SAPERE fine-grained dependencies and memory usage
commit cbc430612a9a9063264da4f229fec1f7dc270ecd Author: S-furi <stefano.furi7@gmail.com> Date: Fri Feb 20 14:02:00 2026 +0100 perf(api): make use of persistent collections in observable collections this led to a great improvement in memory usage especially for neighbor-related streams where set of nodes where occupying large chunks of memory. This usage of persistent structures improved equality checks speed with an overall time reduction of approx a 10% wrt standard kotlin collections usage (from 22s independent test (SAPERE) to 20s) commit ef9301aed3555d7d97c262a755085ace39bc9c24 Author: S-furi <stefano.furi7@gmail.com> Date: Fri Feb 20 11:53:17 2026 +0100 perf(engine): introduce `BatchManager` to avoid redundant reschedule requests commit 6c04f40fcea4ce9f644cf2b9e162b999199d524e Author: S-furi <stefano.furi7@gmail.com> Date: Fri Feb 20 11:18:14 2026 +0100 perf(sapere): introduce finer-grained observables for lsa spaces
1 parent 1b14fe3 commit 0634b85

13 files changed

Lines changed: 239 additions & 99 deletions

File tree

alchemist-api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dependencies {
1717
api(libs.listset)
1818
implementation(libs.kotlin.reflect)
1919
api(libs.arrow.core)
20+
api(libs.kotlinx.collections.immutable)
2021
testImplementation(libs.kotlin.test)
2122
}
2223

alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableList.kt

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2010-2025, Danilo Pianini and contributors
2+
* Copyright (C) 2010-2026, Danilo Pianini and contributors
33
* listed, for each module, in the respective subproject's build.gradle.kts file.
44
*
55
* This file is part of Alchemist, and is distributed under the terms of the
@@ -9,7 +9,9 @@
99

1010
package it.unibo.alchemist.model.observation
1111

12-
import java.util.Collections
12+
import kotlinx.collections.immutable.PersistentList
13+
import kotlinx.collections.immutable.persistentListOf
14+
import kotlinx.collections.immutable.toPersistentList
1315

1416
/**
1517
* Represents a list that allows observation of its contents and provides notifications on changes.
@@ -85,12 +87,12 @@ interface ObservableList<T> : Observable<List<T>> {
8587
*/
8688
class ObservableMutableList<T> : ObservableList<T> {
8789

88-
private val backing: MutableList<T> = ArrayList()
90+
private var backing: PersistentList<T> = persistentListOf()
8991
private val sizeObservable = MutableObservable.observe(0)
9092

9193
override val observableSize: Observable<Int> = sizeObservable
9294

93-
override val current: List<T> get() = Collections.unmodifiableList(backing)
95+
override val current: List<T> get() = backing
9496

9597
override val observers: List<Any> get() = observingCallbacks.keys.toList()
9698

@@ -104,9 +106,9 @@ class ObservableMutableList<T> : ObservableList<T> {
104106
* @return `true` (as specified by [MutableList.add])
105107
*/
106108
fun add(item: T): Boolean {
107-
val result = backing.add(item)
109+
backing = backing.add(item)
108110
notifyObservers()
109-
return result
111+
return true
110112
}
111113

112114
/**
@@ -117,7 +119,7 @@ class ObservableMutableList<T> : ObservableList<T> {
117119
* @param item The item to be added to the list.
118120
*/
119121
fun add(index: Int, item: T) {
120-
backing.add(index, item)
122+
backing = backing.add(index, item)
121123
notifyObservers()
122124
}
123125

@@ -128,9 +130,9 @@ class ObservableMutableList<T> : ObservableList<T> {
128130
* @return `true` if this list changed as a result of the call
129131
*/
130132
fun addAll(items: Collection<T>): Boolean = if (items.isNotEmpty()) {
131-
val result = backing.addAll(items)
133+
backing = backing.addAll(items)
132134
notifyObservers()
133-
result
135+
true
134136
} else {
135137
false
136138
}
@@ -143,8 +145,10 @@ class ObservableMutableList<T> : ObservableList<T> {
143145
* @return `true` if the list contained the specified element
144146
*/
145147
fun remove(item: T): Boolean {
146-
val result = backing.remove(item)
148+
val newBacking = backing.remove(item)
149+
val result = newBacking !== backing
147150
if (result) {
151+
backing = newBacking
148152
notifyObservers()
149153
}
150154
return result
@@ -157,9 +161,10 @@ class ObservableMutableList<T> : ObservableList<T> {
157161
* @return the element that was removed from the list
158162
*/
159163
fun removeAt(index: Int): T {
160-
val result = backing.removeAt(index)
164+
val old = backing[index]
165+
backing = backing.removeAt(index)
161166
notifyObservers()
162-
return result
167+
return old
163168
}
164169

165170
/**
@@ -170,8 +175,9 @@ class ObservableMutableList<T> : ObservableList<T> {
170175
* @return the element previously at the specified position
171176
*/
172177
operator fun set(index: Int, element: T): T {
173-
val old = backing.set(index, element)
178+
val old = backing[index]
174179
if (old != element) {
180+
backing = backing.set(index, element)
175181
notifyObservers()
176182
}
177183
return old
@@ -182,7 +188,7 @@ class ObservableMutableList<T> : ObservableList<T> {
182188
*/
183189
fun clear() {
184190
if (backing.isNotEmpty()) {
185-
backing.clear()
191+
backing = persistentListOf()
186192
notifyObservers()
187193
}
188194
}
@@ -199,15 +205,15 @@ class ObservableMutableList<T> : ObservableList<T> {
199205
override fun dispose() {
200206
observingCallbacks.clear()
201207
sizeObservable.dispose()
202-
backing.clear()
208+
backing = persistentListOf()
203209
}
204210

205211
override operator fun get(index: Int): T = backing[index]
206212

207-
override fun toList(): List<T> = Collections.unmodifiableList(ArrayList(backing))
213+
override fun toList(): List<T> = backing
208214

209215
override fun copy(): ObservableMutableList<T> = ObservableMutableList<T>().apply {
210-
this@ObservableMutableList.backing.forEach(this::add)
216+
backing = this@ObservableMutableList.backing
211217
}
212218

213219
override operator fun contains(item: T): Boolean = backing.contains(item)
@@ -233,7 +239,7 @@ class ObservableMutableList<T> : ObservableList<T> {
233239

234240
private fun notifyObservers() {
235241
sizeObservable.update { backing.size }
236-
val snapshot = toList()
242+
val snapshot = backing
237243
observingCallbacks.values.forEach { callbacks ->
238244
callbacks.forEach { it(snapshot) }
239245
}
@@ -262,7 +268,7 @@ class ObservableMutableList<T> : ObservableList<T> {
262268
* @return A new [ObservableMutableList] containing the specified items.
263269
*/
264270
operator fun <T> invoke(vararg items: T): ObservableMutableList<T> = ObservableMutableList<T>().apply {
265-
items.forEach(this::add)
271+
backing = items.toList().toPersistentList()
266272
}
267273
}
268274
}

alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableMap.kt

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2010-2025, Danilo Pianini and contributors
2+
* Copyright (C) 2010-2026, Danilo Pianini and contributors
33
* listed, for each module, in the respective subproject's build.gradle.kts file.
44
*
55
* This file is part of Alchemist, and is distributed under the terms of the
@@ -14,7 +14,10 @@ import arrow.core.none
1414
import arrow.core.some
1515
import it.unibo.alchemist.model.observation.MutableObservable.Companion.observe
1616
import it.unibo.alchemist.model.observation.Observable.ObservableExtensions.currentOrNull
17-
import java.util.Collections
17+
import kotlinx.collections.immutable.PersistentMap
18+
import kotlinx.collections.immutable.mutate
19+
import kotlinx.collections.immutable.persistentMapOf
20+
import kotlinx.collections.immutable.toPersistentMap
1821

1922
/**
2023
* Represents an observable map that allows observation of changes to its contents,
@@ -91,12 +94,13 @@ interface ObservableMap<K, V> : Observable<Map<K, V>> {
9194
* @param V The type of mapped values.
9295
* @property backingMap The internal mutable map that stores the key-value pairs.
9396
*/
94-
open class ObservableMutableMap<K, V>(private val backingMap: MutableMap<K, V> = linkedMapOf()) : ObservableMap<K, V> {
97+
open class ObservableMutableMap<K, V>(initial: Map<K, V> = emptyMap()) : ObservableMap<K, V> {
9598

99+
private var backingMap: PersistentMap<K, V> = initial.toPersistentMap()
96100
private val keyObservables: MutableMap<K, MutableObservable<Option<V>>> = linkedMapOf()
97101
override val observingCallbacks: MutableMap<Any, List<(Map<K, V>) -> Unit>> = linkedMapOf()
98102

99-
override val current: Map<K, V> = Collections.unmodifiableMap(backingMap)
103+
override val current: Map<K, V> get() = backingMap
100104

101105
override val observers: List<Any> get() = observingCallbacks.keys.toList()
102106

@@ -116,8 +120,9 @@ open class ObservableMutableMap<K, V>(private val backingMap: MutableMap<K, V> =
116120
* @param value The value associated with the specified key.
117121
*/
118122
fun put(key: K, value: V) {
119-
val previous: V? = backingMap.put(key, value)
123+
val previous = backingMap[key]
120124
if (previous != value) {
125+
backingMap = backingMap.put(key, value)
121126
getAsMutable(key).update { value.some() }
122127
notifyMapObservers()
123128
}
@@ -130,9 +135,14 @@ open class ObservableMutableMap<K, V>(private val backingMap: MutableMap<K, V> =
130135
* @param key The key whose mapping is to be removed from the map.
131136
* @return the previous value associated with the key, or null if the key was not present in the map.
132137
*/
133-
fun remove(key: K): V? = backingMap.remove(key).also {
134-
val previousObservedValue = keyObservables[key]?.update { none() } ?: none()
135-
if (previousObservedValue.isSome()) notifyMapObservers()
138+
fun remove(key: K): V? {
139+
val previous = backingMap[key]
140+
if (previous != null || key in backingMap) {
141+
backingMap = backingMap.remove(key)
142+
keyObservables[key]?.update { none() }
143+
notifyMapObservers()
144+
}
145+
return previous
136146
}
137147

138148
/**
@@ -144,19 +154,20 @@ open class ObservableMutableMap<K, V>(private val backingMap: MutableMap<K, V> =
144154
*/
145155
fun clearAndPutAll(from: Map<K, V>) {
146156
var changed = false
147-
val keysToRemove = backingMap.keys.toSet() - from.keys
157+
val keysToRemove = backingMap.keys - from.keys
148158
if (keysToRemove.isNotEmpty()) {
159+
backingMap = backingMap.mutate { it -= keysToRemove }
149160
keysToRemove.forEach { key ->
150-
backingMap.remove(key)
151161
keyObservables[key]?.update { none() }
152162
}
153163
changed = true
154164
}
155165

156166
if (from.isNotEmpty()) {
157167
from.forEach { (key, value) ->
158-
val previous = backingMap.put(key, value)
168+
val previous = backingMap[key]
159169
if (previous != value) {
170+
backingMap = backingMap.put(key, value)
160171
getAsMutable(key).update { value.some() }
161172
changed = true
162173
}
@@ -170,7 +181,7 @@ open class ObservableMutableMap<K, V>(private val backingMap: MutableMap<K, V> =
170181

171182
override fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (Map<K, V>) -> Unit) {
172183
observingCallbacks[registrant] = observingCallbacks[registrant].orEmpty() + callback
173-
if (invokeOnRegistration) callback(current.toMap())
184+
if (invokeOnRegistration) callback(backingMap)
174185
}
175186

176187
override fun stopWatching(registrant: Any) {
@@ -189,9 +200,9 @@ open class ObservableMutableMap<K, V>(private val backingMap: MutableMap<K, V> =
189200
override fun dispose() {
190201
keyObservables.values.forEach { it.dispose() }
191202
keyObservables.clear()
192-
observingCallbacks.keys.forEach(::stopWatching)
203+
observingCallbacks.keys.toList().forEach(::stopWatching)
193204
observingCallbacks.clear()
194-
backingMap.clear()
205+
backingMap = persistentMapOf()
195206
}
196207

197208
override fun isEmpty(): Boolean = backingMap.isEmpty()
@@ -201,9 +212,9 @@ open class ObservableMutableMap<K, V>(private val backingMap: MutableMap<K, V> =
201212
*
202213
* @return A new instance of ObservableMutableMap containing the same key-value pairs as the original.
203214
*/
204-
fun copy(): ObservableMutableMap<K, V> = ObservableMutableMap(backingMap.toMutableMap())
215+
fun copy(): ObservableMutableMap<K, V> = ObservableMutableMap(backingMap)
205216

206-
override fun asMap(): Map<K, V> = Collections.unmodifiableMap(backingMap)
217+
override fun asMap(): Map<K, V> = backingMap
207218

208219
override operator fun get(key: K): Observable<Option<V>> = getAsMutable(key)
209220

@@ -224,8 +235,11 @@ open class ObservableMutableMap<K, V>(private val backingMap: MutableMap<K, V> =
224235
*/
225236
operator fun minus(key: K) = remove(key)
226237

227-
private fun notifyMapObservers() = observingCallbacks.values.forEach { callbacks ->
228-
callbacks.forEach { it(current.toMap()) }
238+
private fun notifyMapObservers() {
239+
val snapshot = backingMap
240+
observingCallbacks.values.forEach { callbacks ->
241+
callbacks.forEach { it(snapshot) }
242+
}
229243
}
230244

231245
private fun getAsMutable(key: K): MutableObservable<Option<V>> = keyObservables.getOrPut(key) { observe(none()) }

alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/ObservableSet.kt

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2010-2025, Danilo Pianini and contributors
2+
* Copyright (C) 2010-2026, Danilo Pianini and contributors
33
* listed, for each module, in the respective subproject's build.gradle.kts file.
44
*
55
* This file is part of Alchemist, and is distributed under the terms of the
@@ -9,9 +9,6 @@
99

1010
package it.unibo.alchemist.model.observation
1111

12-
import arrow.core.getOrElse
13-
import java.util.Collections
14-
1512
/**
1613
* Represents a set that allows observation of its contents and provides notifications on changes.
1714
* This interface supports observing the entire set, individual item membership, and various utility operations.
@@ -85,9 +82,9 @@ interface ObservableSet<T> : Observable<Set<T>> {
8582
*
8683
* @param T The type of elements maintained by this set.
8784
*/
88-
class ObservableMutableSet<T> : ObservableSet<T> {
85+
class ObservableMutableSet<T>(initial: Iterable<T> = emptyList()) : ObservableSet<T> {
8986

90-
private val backing = ObservableMutableMap<T, Boolean>()
87+
private val backing = ObservableMutableMap<T, Boolean>(initial.associateWith { true })
9188

9289
override val observableSize: Observable<Int> = backing.map { it.keys.size }
9390

@@ -138,7 +135,7 @@ class ObservableMutableSet<T> : ObservableSet<T> {
138135

139136
override fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (Set<T>) -> Unit) {
140137
observingCallbacks[registrant] = observingCallbacks[registrant].orEmpty() + callback
141-
backing.onChange(this to registrant, invokeOnRegistration) { callback(it.keys.toSet()) }
138+
backing.onChange(this to registrant, invokeOnRegistration) { callback(it.keys) }
142139
}
143140

144141
override fun stopWatching(registrant: Any) {
@@ -152,15 +149,13 @@ class ObservableMutableSet<T> : ObservableSet<T> {
152149
observingCallbacks.clear()
153150
}
154151

155-
override fun observeMembership(item: T): Observable<Boolean> = backing[item].map { opt -> opt.getOrElse { false } }
152+
override fun observeMembership(item: T): Observable<Boolean> = backing[item].map { opt -> opt.isSome() }
156153

157-
override fun toSet(): Set<T> = Collections.unmodifiableSet(backing.current.keys)
154+
override fun toSet(): Set<T> = backing.current.keys
158155

159156
override fun toList(): List<T> = backing.current.keys.toList()
160157

161-
override fun copy(): ObservableMutableSet<T> = ObservableMutableSet<T>().apply {
162-
this@ObservableMutableSet.backing.asMap().keys.forEach(this::add)
163-
}
158+
override fun copy(): ObservableMutableSet<T> = ObservableMutableSet(backing.asMap().keys)
164159

165160
override operator fun contains(item: T): Boolean = item in backing.current
166161

@@ -194,19 +189,15 @@ class ObservableMutableSet<T> : ObservableSet<T> {
194189
*
195190
* @return An instance of `ObservableMutableSet` containing all unique elements from the original list.
196191
*/
197-
fun <T> List<T>.toObservableSet(): ObservableMutableSet<T> = ObservableMutableSet<T>().also {
198-
this.forEach(it::add)
199-
}
192+
fun <T> List<T>.toObservableSet(): ObservableMutableSet<T> = ObservableMutableSet<T>(this)
200193

201194
/**
202195
* Converts the current set into an observable mutable set.
203196
* @see ObservableMutableSet
204197
*
205198
* @return An instance of `ObservableMutableSet` containing all the elements from the original set.
206199
*/
207-
fun <T> Set<T>.toObservableSet(): ObservableMutableSet<T> = ObservableMutableSet<T>().also {
208-
this.forEach(it::add)
209-
}
200+
fun <T> Set<T>.toObservableSet(): ObservableMutableSet<T> = ObservableMutableSet<T>(this)
210201

211202
/**
212203
* Creates a new [ObservableMutableSet] and populates it with the specified items.
@@ -215,8 +206,6 @@ class ObservableMutableSet<T> : ObservableSet<T> {
215206
* The items are provided as a variable number of arguments.
216207
* @return A new [ObservableMutableSet] containing the specified items.
217208
*/
218-
operator fun <T> invoke(vararg items: T): ObservableMutableSet<T> = ObservableMutableSet<T>().apply {
219-
items.forEach(this::add)
220-
}
209+
operator fun <T> invoke(vararg items: T): ObservableMutableSet<T> = ObservableMutableSet<T>(items.toList())
221210
}
222211
}

alchemist-api/src/test/kotlin/it/unibo/alchemist/model/observation/ObservableListTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class ObservableListTest : FunSpec({
9090
val list = ObservableMutableList(1, 2, 3)
9191
var nextExpected = 3
9292

93-
list.observableSize.onChange(this) { it shouldBe nextExpected }
93+
list.observableSize.onChange(this, false) { it shouldBe nextExpected }
9494

9595
repeat(3) {
9696
nextExpected++

0 commit comments

Comments
 (0)