cached.kt
src/jvmMain/kotlin/afield/runtime/lifecycle/cached.kt · 159 lines
@file:afield.runtime.completion.CompletionCategory("Workspace")
package afield.runtime.lifecycle
import afield.data.collections.get
import afield.data.collections.invoke
import afield.data.collections.s
import afield.data.collections.times
import afield.document.graph.minusAssign
import afield.document.graph.plusAssign
import afield.document.properties.compareTo
import afield.document.workspace.file
import afield.graphics.render.rangeTo
import afield.graphics.shader.compute
import afield.graphics.shader.miss
import afield.lang.stacksl.evolution.render
import afield.math.autodiff.compareTo
import afield.math.autodiff.div
import afield.math.autodiff.minus
import afield.math.autodiff.plus
import afield.math.autodiff.times
import afield.math.linalg.invoke
import afield.math.linalg.remAssign
import afield.math.linalg.set
import afield.math.linalg.times
import afield.math.units.div
import afield.math.units.minus
import afield.math.units.plus
import afield.math.units.times
import afield.audio.synth.*
import afield.graphics.vulkan.buffers.add
import afield.media.image.close
import afield.document.graph.Box
import afield.runtime.lifecycle.BoxRunScopes
import afield.runtime.lifecycle.OwnedEntry
import afield.runtime.lifecycle.ReleasePolicy
import afield.runtime.lifecycle.RunScope
import java.util.WeakHashMap
import kotlin.reflect.KClass
cached, which hands a box back the value it made on its last run for the same keys, and closes the old value when the keys change.
inline fun <reified T : Any> Box.cached(vararg keys: Any?, noinline make: () -> T): T =
Returns the T this box made last time for the same keys, making one with make only when
there is none:
val view = cached<ElectronRTTView>(renderer) { ElectronRTTView(640, 360) }
The cache key is the type T plus keys, compared with equals. Re-running the box with the
same renderer hands back the same view; a different renderer makes a new one.
The old value is closed. When a run asks for a T under keys that miss, every cached T
of this box that the current run has not asked for is evicted, and closed if it is
AutoCloseable (together with anything its make registered — a render module, a slider). So
changing a key and re-running replaces the old value rather than leaking it, while one run that
asks for several Ts under different keys (one view per renderer, in a loop) keeps them all.
Two cached<T> calls with the same keys in one box share one value; add a discriminating key
(cached<View>(renderer, "left")) to keep them apart.
Lifetime. Like once, make runs with the box's BoxRunScopes.parent scope current, so
what it registers belongs to the box, not to the run that happened to compute it. A cached value
is never taken apart by a re-run, and survives stop unless something it registered was
ReleasePolicy.RUNNING (an audio patch, a process) — then stop closes it and the next run makes
a fresh one rather than returning a dead one. Retiring the box (delete, document close) closes
everything. A value is closed at most once, even when it registered itself with the run scope.
A class declared in the box's own code is a new class each time the box compiles, so a value of it cannot be handed back after a recompile. When that happens the previous value is closed at once and the box's output says why, rather than the miss passing silently.
A plain inline function, not a macro: T is reified, so nothing about the call site needs
rewriting. Called outside any box (no Box.context), use the Box receiver form explicitly.
inline fun <reified T : Any> Box.cached(vararg keys: Any?, noinline make: () -> T): T =
BoxCache.lookup(this, T::class, keys.toList(), make)
object BoxCache
The per-box store behind cached; public only so the inline cached can reach it.
object BoxCache {
private class Entry(val type: KClass<*>, val keys: List<Any?>, val value: Any, val registered: List<OwnedEntry>) {
@Volatile
var lastScope: RunScope? = null
private var closed = false
fun close() {
synchronized(this) { if (closed) return; closed = true }
// Releasing what make registered closes the value too when it owned itself.
registered.forEach { it.release { } }
val v = value
if (v is AutoCloseable && registered.none { it.resource === v }) runCatching { v.close() }
}
}
private val boxes = WeakHashMap<Box, MutableList<Entry>>()
internal fun <T : Any> lookup(box: Box, type: KClass<T>, keys: List<Any?>, make: () -> T): T {
body — 54 lines, from line 101
@PublishedApi
internal fun <T : Any> lookup(box: Box, type: KClass<T>, keys: List<Any?>, make: () -> T): T {
val scope = RunScope.currentFor(box)
val evicted = ArrayList<Entry>()
var recompiled = false
synchronized(boxes) {
val entries = boxes.getOrPut(box) { ArrayList() }
entries.firstOrNull { it.type == type && it.keys == keys }?.let {
it.lastScope = scope
@Suppress("UNCHECKED_CAST")
return it.value as T
}
entries.filter { it.type == type && (scope == null || it.lastScope !== scope) }.forEach {
entries.remove(it)
evicted.add(it)
}
// The same class name under a different KClass is the previous compile's copy of a class
// declared in the box's own code: it can never hit again, so close it now, not at retire.
entries.filter { it.type != type && it.type.java.name == type.java.name }.forEach {
entries.remove(it)
evicted.add(it)
recompiled = true
}
}
evicted.forEach { it.close() }
if (recompiled) {
box.output.println(
"cached<${type.simpleName}>: ${type.simpleName} is declared in this box's code, so every " +
"compile makes a new class and the previous value cannot be reused; it was closed and " +
"a new one made. Cache a library type, or declare the class outside the box, to keep it."
)
}
val parent = box.runScopes.parent
var made: Entry? = null
val (value, registered) = RunScope.withCurrent(parent) {
RunScope.collectingOwned {
val v = make()
// Own the value too, so retire closes it even if make registered nothing.
if (v is AutoCloseable) parent.own(AutoCloseable { made?.close() }, ReleasePolicy.KEYED)
v
}
}
val entry = Entry(type, keys, value, registered).also { it.lastScope = scope }
made = entry
synchronized(boxes) { boxes.getOrPut(box) { ArrayList() }.add(entry) }
// Whatever releases a registration (stop, retire) also retires the cached value.
val forget = {
synchronized(boxes) { boxes[box]?.remove(entry) }
entry.close()
}
registered.forEach { it.onRelease(forget) }
return value
}
fun size(box: Box): Int = synchronized(boxes) { boxes[box]?.size ?: 0 }
How many values box has cached; for tests and inspection.