Skip to content

RunScope.kt

src/jvmMain/kotlin/afield/runtime/lifecycle/RunScope.kt · 461 lines

package afield.runtime.lifecycle

import afield.app.diagnostics.named
import afield.app.protocol.put
import afield.data.table.div
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.document.graph.retireBoxes
import afield.document.graph.stopTrackedJobs
import afield.runtime.execution.plus
import afield.ui.notation.with
import afield.ui.output.slider
import afield.ui.output.streamProcess

import afield.audio.synth.*
import afield.data.table.get
import afield.document.graph.Box
import afield.document.graph.box
import afield.document.graph.isTrue
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import afield.ui.output.templates.filter
import afield.app.diagnostics.logging.lprintln
import kotlinx.coroutines.CompletableJob
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asContextElement
import java.util.EnumSet
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import kotlin.coroutines.CoroutineContext

Run scopes: what a box's run owns, and which of stop, re-run or retire releases it.

Each run of a box gets a RunScope; registrations made while it executes are owned by that scope and carry a ReleasePolicy. BoxRunScopes holds a box's scopes, so a re-run supersedes the last run's scope, a stop releases only what is running, and retiring the box releases everything.

enum class ReleasePolicy

Which of the three box events — stop, re-run, retire — takes a registration back out.

policy kinds released by
KEYED render modules (every putOwned map), Runloop preRun/postRun hooks, anything claimed into a named slot retire only: box delete, tombstone, document close
ACCUMULATING control callbacks (a fresh UUID each run) the next run of the same strand, and retire
RUNNING audio patches, patSynth feeders, streamProcess children, coroutine jobs stop, the next run of the strand, and retire

A keyed registration is replaced by the next registration under the same key (see Ownership): that is Field's Map<String, …> idiom, where the key is chosen so a re-run replaces what the last run put there, and where launching something that goes on after a stop is the point. An accumulating registration has no key to be replaced by, so without the next run releasing it every run would add one. Only what is running stops with stop.

enum class ReleasePolicy {
    KEYED, ACCUMULATING, RUNNING;

companion object

    companion object {

fun forKind(kind: String, hasSlot: Boolean = true): ReleasePolicy

The policy of a ledger kind; an unlisted kind is KEYED if it claims a slot, else RUNNING.

        fun forKind(kind: String, hasSlot: Boolean = true): ReleasePolicy = when (kind) {
            LifecycleKinds.CONTROL_CALLBACK -> ACCUMULATING
            LifecycleKinds.AUDIO_PATCH, LifecycleKinds.PATSYNTH_FEEDER, LifecycleKinds.STREAM_PROCESS -> RUNNING
            LifecycleKinds.RENDER_MODULE, LifecycleKinds.RUNLOOP_PRE_RUN, LifecycleKinds.RUNLOOP_POST_RUN -> KEYED
            else -> if (hasSlot) KEYED else RUNNING
        }

internal val SUPERSEDED: Set<ReleasePolicy> = EnumSet.of(ACCUMULATING, RUNNING)

internal val RETIRED: Set<ReleasePolicy> = EnumSet.allOf(ReleasePolicy::class.java)

class OwnedEntry internal constructor(val resource: AutoCloseable, val policy: ReleasePolicy)

Something a RunScope owns, with the policy that says which event releases it. onRelease listeners run once, after resource is closed by a scope — how once learns that what its cached value holds is gone.

class OwnedEntry internal constructor(val resource: AutoCloseable, val policy: ReleasePolicy) {
    private val released = AtomicBoolean(false)
    private val listeners = CopyOnWriteArrayList<() -> Unit>()

val isReleased: Boolean get() = released.get()

True once a scope has released resource.

val isLive: Boolean get() = !released.get() && (resource !is Ownership || resource.isLive)

Not yet released, and (for an Ownership) still held by its subsystem.

fun onRelease(listener: () -> Unit) {

Run listener when this is released; at once if it already has been.

    fun onRelease(listener: () -> Unit) {
        listeners.add(listener)
        if (released.get() && listeners.remove(listener)) listener()
    }

internal fun release(onFailure: (Throwable) -> Unit) {

    internal fun release(onFailure: (Throwable) -> Unit) {
        if (!released.compareAndSet(false, true)) return
        runCatching { resource.close() }.onFailure(onFailure)
        val toRun = listeners.toList()
        listeners.clear()
        toRun.forEach { runCatching(it).onFailure(onFailure) }
    }

override fun toString(): String = "$policy $resource"

class RunScope internal constructor

One run of one box on one strand, and the owner of everything that run started.

Box code registers things with subsystems that outlive it — an audio patch in the mix, a slider callback, a Runloop hook, a module on a render target, a child process. A run scope is the single owner of those: whatever the run registers is owned here under a ReleasePolicy, and each of the three events that end things releases the policies it is responsible for, newest first.

Where scopes come from. A launch (launchAll, continueAll, a batch launch) opens a scope on its strand; running the whole box (execute-all) opens one on BoxRunScopes.RUN_STRAND. Running a fragment adds to the box's current BoxRunScopes.RUN_STRAND scope, since running one line does not undo the others. Everything else — another language's box, code a launched loop runs later — lands in that ambient scope through current's fallback. Each box also has one BoxRunScopes.parent scope, which no run supersedes: once values register there, and library code that makes something meant to outlive the run that asked for it can too.

What the three events do. - Supersede — once a new run of the strand has executed (after, not before, so a re-run replaces what the previous run drew rather than blanking it for the length of a compile), each older scope of the strand releases its ReleasePolicy.ACCUMULATING and ReleasePolicy.RUNNING things and closes. What it held under ReleasePolicy.KEYED moves to the box's parent scope and stays until its key is reused or the box retires: the key, not the run, decides. - Stop (stopTrackedJobs) releases the ReleasePolicy.RUNNING things of every scope the box has, the parent included, and cancels each scope's job. The run scopes stay open holding the rest — a stopped launch's slider still answers, and the next launch of that strand still supersedes it, which is why they are not merged into the parent. A stopped run scope remembers it was stopped: a running thing registered into it afterwards (by a straggler of the launch the stop invalidated) is released at once, and BoxRunScopes.ambient opens a fresh scope rather than hand it out. - Retire (close; retireBoxes, which delete, tombstone and document close go through) releases everything.

Something registered into a scope that has already ended is treated as though it had been there when it ended: released at once if that event releases its policy, kept otherwise — a superseded scope passes a keyed straggler on to the parent.

Finding the current scope. Execution sets it for the thread (element for coroutines, withCurrent for synchronous dispatch). Library code asks current (or currentFor, given the box) and calls own; the lifecycle ledger does that on every LifecycleLedger.claim, which is how audio patches, feeders, control callbacks, Runloop hooks and render modules end up here without their call sites knowing. Their policy comes from their kind (ReleasePolicy.forKind) unless the claim says otherwise.

job is a SupervisorJob that stop, supersede and close cancel: a parent for coroutines a run starts that should die with it. It is deliberately not one of the box's Box.jobs, which is what marks a box as running.

class RunScope internal constructor(
    val box: Box,
    val strand: String,
    val kind: String,
) : AutoCloseable {

val id: Long = nextId.incrementAndGet()

Opening order across the process; a strand's older scopes have smaller ids.

val job: CompletableJob = SupervisorJob()

Cancelled by stop, supersede and close; a parent for coroutines that belong to this run.

    private val lock = Any()
    private val owned = ArrayList<OwnedEntry>()
    private var pruneAt = PRUNE_START
    private val closedFlag = AtomicBoolean(false)

Policies this scope has released for good: one of these arriving later is released at once.

    private val ended: MutableSet<ReleasePolicy> = EnumSet.noneOf(ReleasePolicy::class.java)

val isClosed: Boolean get() = closedFlag.get()

True once this scope has been superseded or retired; it owns nothing after that.

val isStopped: Boolean get() = synchronized(lock) { ReleasePolicy.RUNNING in ended }

True once a stop (or a close) has reached this run scope. The parent scope never reports stopped.

val element: CoroutineContext get() = threadCurrent.asContextElement(this)

A coroutine context element that makes this the current scope wherever the coroutine runs.

fun <T : AutoCloseable> own(resource: T, policy: ReleasePolicy = defaultPolicy(resource)): T

Take ownership of resource under policy — by default an Ownership's own, and ReleasePolicy.RUNNING for anything else. Returns resource. If this scope has already ended, resource is released now when the event that ended it releases policy, and kept otherwise.

    fun <T : AutoCloseable> own(resource: T, policy: ReleasePolicy = defaultPolicy(resource)): T {
        adopt(OwnedEntry(resource, policy))
        return resource
    }

    private fun adopt(entry: OwnedEntry) {
        val verdict = synchronized(lock) {
            when {
                entry.policy in ended -> Verdict.RELEASE
                closedFlag.get() -> Verdict.FORWARD
                else -> {
                    owned.add(entry)
                    if (owned.size >= pruneAt) {
                        owned.removeAll { !it.isLive }
                        pruneAt = maxOf(PRUNE_START, owned.size * 2)
                    }
                    Verdict.KEEP
                }
            }
        }
        when (verdict) {
            Verdict.KEEP -> collectors.get()?.lastOrNull()?.add(entry)
            Verdict.RELEASE -> {
                collectors.get()?.lastOrNull()?.add(entry)
                releaseQuietly(entry)
            }
            Verdict.FORWARD -> box.runScopes.parent.adopt(entry)
        }
    }

fun own(policy: ReleasePolicy = ReleasePolicy.RUNNING, release: () -> Unit): AutoCloseable =

Own a release action under policy; returns the AutoCloseable that runs it, which disown accepts.

    fun own(policy: ReleasePolicy = ReleasePolicy.RUNNING, release: () -> Unit): AutoCloseable =
        own(AutoCloseable { release() }, policy)

fun ownJob(job: Job): Job

Cancel job when stop, supersede or retire reaches this scope. Returns job.

    fun ownJob(job: Job): Job {
        own(AutoCloseable { job.cancel() }, ReleasePolicy.RUNNING)
        return job
    }

fun disown(resource: AutoCloseable): Boolean

Stop owning resource without closing it — for something that has ended on its own.

    fun disown(resource: AutoCloseable): Boolean =
        synchronized(lock) { owned.removeAll { it.resource === resource } }

val liveCount: Int get() = synchronized(lock) { owned.count { it.isLive } }

How many owned things are still live (ledger ownerships the subsystem still holds, and the rest).

fun liveCount(policy: ReleasePolicy): Int

How many owned things under policy are still live.

    fun liveCount(policy: ReleasePolicy): Int =
        synchronized(lock) { owned.count { it.policy == policy && it.isLive } }

internal fun stop(latch: Boolean = true)

Stop's part: release every ReleasePolicy.RUNNING thing, newest first, and cancel job; the rest stays owned. With latch — every run scope — a running thing registered afterwards is released at once. The parent does not latch, since the next run's once registers there.

    internal fun stop(latch: Boolean = true) {
        val toRelease = synchronized(lock) {
            if (latch) ended.add(ReleasePolicy.RUNNING)
            val running = owned.filter { it.policy == ReleasePolicy.RUNNING }
            owned.removeAll { it.policy == ReleasePolicy.RUNNING }
            running
        }
        job.cancel()
        toRelease.asReversed().forEach(::releaseQuietly)
    }

internal fun supersede()

Supersede's part: release the ReleasePolicy.ACCUMULATING and ReleasePolicy.RUNNING things, newest first, hand the ReleasePolicy.KEYED ones to the box's parent scope, and close.

    internal fun supersede() {
        if (!closedFlag.compareAndSet(false, true)) return
        val (keyed, toRelease) = synchronized(lock) {
            ended.addAll(ReleasePolicy.SUPERSEDED)
            val split = owned.partition { it.policy == ReleasePolicy.KEYED }
            owned.clear()
            split
        }
        job.cancel()
        toRelease.asReversed().forEach(::releaseQuietly)
        box.runScopes.forget(this)
        if (keyed.isNotEmpty()) {
            val parent = box.runScopes.parent
            keyed.filter { it.isLive }.forEach { parent.adopt(it) }
        }
    }

override fun close()

Retire: release everything owned, newest first, and cancel job. Idempotent; one failure does not strand the rest.

    override fun close() {
        val toRelease = synchronized(lock) {
            ended.addAll(ReleasePolicy.RETIRED)
            owned.toList().also { owned.clear() }
        }
        val first = closedFlag.compareAndSet(false, true)
        job.cancel()
        toRelease.asReversed().forEach(::releaseQuietly)
        if (first) box.runScopes.forget(this)
    }

    private fun releaseQuietly(entry: OwnedEntry) {
        entry.release { failure ->
            lprintln("warning,box", "releasing $entry for box ${box.id} (strand $strand) threw: $failure")
        }
    }

override fun toString(): String

    override fun toString(): String =
        "RunScope(#$id ${box.id} strand=$strand kind=$kind closed=$isClosed stopped=$isStopped)"

    private enum class Verdict { KEEP, RELEASE, FORWARD }

companion object

    companion object {
        private const val PRUNE_START = 64
        private val nextId = AtomicLong()
        private val threadCurrent = ThreadLocal<RunScope?>()
        private val collectors = ThreadLocal<ArrayList<MutableList<OwnedEntry>>?>()

        private fun defaultPolicy(resource: AutoCloseable): ReleasePolicy =
            (resource as? Ownership)?.policy ?: ReleasePolicy.RUNNING

fun current(): RunScope? = threadCurrent.get() ?: Box.context.get()?.runScopes?.ambient()

The scope whatever is executing on this thread belongs to: the one execution installed, or failing that the ambient BoxRunScopes.RUN_STRAND scope of the executing box (Box.context). Null when no box is executing, and for a deleted box.

fun executingBox(): Box? = threadCurrent.get()?.box ?: Box.context.get()

The box whose run is executing on this thread: the installed scope's box, else Box.context.

fun currentFor(box: Box?): RunScope?

current, but for box specifically: its installed scope if that is box's, else box's ambient one.

        fun currentFor(box: Box?): RunScope? {
            if (box == null) return null
            val installed = threadCurrent.get()
            if (installed != null && installed.box === box) return installed
            return box.runScopes.ambient()
        }

fun <T> withCurrent(scope: RunScope?, block: () -> T): T {

Run block with scope as this thread's current scope.

        fun <T> withCurrent(scope: RunScope?, block: () -> T): T {
            val previous = threadCurrent.get()
            threadCurrent.set(scope)
            return try {
                block()
            } finally {
                threadCurrent.set(previous)
            }
        }

fun <T> collectingOwned(block: () -> T): Pair<T, List<OwnedEntry>> {

Run block and return, with its result, every OwnedEntry a scope took ownership of on this thread while it ran — how once knows what a cached value depends on. Registrations made on another thread (an inMain { } inside block) are not seen.

        fun <T> collectingOwned(block: () -> T): Pair<T, List<OwnedEntry>> {
            val stack = collectors.get() ?: ArrayList<MutableList<OwnedEntry>>().also { collectors.set(it) }
            val mine = ArrayList<OwnedEntry>()
            stack.add(mine)
            try {
                return block() to mine
            } finally {
                stack.removeAt(stack.lastIndex)
                // An enclosing collection depends on whatever this one saw, too.
                stack.lastOrNull()?.addAll(mine)
            }
        }
    }
}

fun <T> RunScope.withCurrent(block: () -> T): T = RunScope.withCurrent(this, block)

Run block with this scope as the thread's current RunScope, so what it registers is owned here.

class BoxRunScopes internal constructor(private val box: Box)

A box's RunScopes: at most a few open per strand, the newest of which is the strand's current run, plus the one parent scope that no run supersedes. See RunScope for when they open and close.

class BoxRunScopes internal constructor(private val box: Box) {
    private val open = CopyOnWriteArrayList<RunScope>()

    @Volatile
    private var parentScope: RunScope? = null

fun begin(strand: String, kind: String): RunScope

Open a new scope for a run of strand; older scopes of the strand stay open until supersede.

    fun begin(strand: String, kind: String): RunScope = RunScope(box, strand, kind).also { open.add(it) }

fun supersede(newer: RunScope)

Supersede every scope of newer's strand that was opened before it: newer has replaced them.

    fun supersede(newer: RunScope) {
        open.filter { it !== newer && it.strand == newer.strand && it.id < newer.id }.forEach { it.supersede() }
    }

val parent: RunScope

The box-level scope no re-run supersedes. What it owns lasts until a stop, for ReleasePolicy.RUNNING things, or until the box retires, for everything else. A superseded run's ReleasePolicy.KEYED registrations move here, and once.x { … } computes with it current, so a value made once is not taken apart by the next run.

Library code that makes something meant to outlive the run that asked for it registers here:

box.runScopes.parent.withCurrent { box.slider("gain", 0.0, 1.0) { gain = it } }
Once the box has retired this is a fresh, open scope again on the next ask — unless the box was deleted, when it stays the closed one, so a straggler's registration is released at once.

    val parent: RunScope
        get() {
            parentScope?.let { if (!it.isClosed || isTombstoned()) return it }
            synchronized(this) {
                parentScope?.let { if (!it.isClosed || isTombstoned()) return it }
                val fresh = RunScope(box, PARENT_STRAND, "parent")
                if (isTombstoned()) fresh.close()
                parentScope = fresh
                return fresh
            }
        }

    private fun isTombstoned() = box.getPropertyRaw("_tombstone").isTrue()

fun ambient(): RunScope?

The newest open RUN_STRAND scope that no stop has reached, opening one if there is none — where a fragment run, another language's execution, and code with no installed scope register. Null for a deleted box, so a straggler registering after deletion owns nothing (the ledger still attributes it).

    fun ambient(): RunScope? {
        if (isTombstoned()) return null
        open.lastOrNull { it.strand == RUN_STRAND && !it.isClosed && !it.isStopped }?.let { return it }
        synchronized(this) {
            open.lastOrNull { it.strand == RUN_STRAND && !it.isClosed && !it.isStopped }?.let { return it }
            return begin(RUN_STRAND, "ambient")
        }
    }

fun stopAll(): Int

Stop's part of the teardown: every scope, the parent included, releases what is ReleasePolicy.RUNNING. Returns how many run scopes there were.

    fun stopAll(): Int {
        val scopes = open.toList().asReversed()
        scopes.forEach { it.stop() }
        parentScope?.stop(latch = false)
        return scopes.size
    }

fun closeAll(): Int

Retire's part: close every scope, the parent last, releasing everything. Returns how many run scopes there were.

    fun closeAll(): Int {
        val scopes = open.toList().asReversed()
        scopes.forEach { it.close() }
        parentScope?.close()
        return scopes.size
    }

fun snapshot(): List<RunScope>

The open run scopes, oldest first, then the parent if it is open.

    fun snapshot(): List<RunScope> = open.toList() + listOfNotNull(parentScope?.takeIf { !it.isClosed })

val liveCount: Int get() = snapshot().sumOf { it.liveCount }

How many owned things across all open scopes are still live.

val liveRunningCount: Int get() = snapshot().sumOf { it.liveCount(ReleasePolicy.RUNNING) }

How many ReleasePolicy.RUNNING things across all open scopes are still live: what a stop would release.

internal fun forget(scope: RunScope)

    internal fun forget(scope: RunScope) {
        open.remove(scope)
    }

companion object

    companion object {

const val RUN_STRAND = "__run__"

The strand of whole-box runs and fragments — everything that is not a launch.

const val PARENT_STRAND = "__box__"

The strand name of parent, which no run is on.