Skip to content

di.kt

src/jvmMain/kotlin/afield/runtime/lifecycle/di.kt · 161 lines

package afield.runtime.lifecycle

import afield.app.protocol.handlers
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.box
import afield.runtime.execution.plus
import afield.ui.output.templates.templates

import afield.app.protocol.computeIfAbsent
import afield.audio.synth.*
import afield.data.table.get
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import afield.app.diagnostics.logging.lblock
import afield.app.diagnostics.logging.lprintln
import arrow.core.right
import io.github.classgraph.ClassGraph
import java.lang.reflect.Method
import java.util.concurrent.ConcurrentHashMap
import kotlin.system.measureNanoTime

Startup phases: functions annotated RunsAt are found by a classpath scan and called by runAll at a named phase.

The phase names are the constants at the top of this file (early, plugins, installHandlers, newRoot, ...). runAll calls every function registered for a phase in priority order, passing those arguments its parameters ask for. The same scan fills labelledArgumentFunctions.

const val plugins = "plugins" // time to install KTor plugins

Startup phase: install Ktor plugins.

const val early = "early" // early on in the startup

const val installHandlers = "installHandlers" // install message handlers for websockets

const val templatesRegistration = "templatesRegistration" // install KTor resource templates

const val newRoot = "newRoot" // whenever a new root is made

const val macroRegistration = "macroRegistration" // whenever a new root is made

const val returnValueRegistration

const val returnValueRegistration = "returnValueRegistration" // register handlers for return values from box execution

annotation class RunsAt(val value: String, val priority: Int = 0)

Marks a function to be called by runAll at phase value, lower priority first.

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class RunsAt(val value: String, val priority: Int = 0)

val labelledArgumentFunctions: MutableSet<String> = ConcurrentHashMap.newKeySet()

Names of the functions annotated @LabelsArgumentsFromSource, filled by the same scan as fieldSourceCache and read when macros are registered (afield.runtime.macros.registerLabelledArgumentMacros).

val fieldSourceCache: Map<String, List<Pair<Int, Method>>> by lazy(LazyThreadSafetyMode.SYNCHRONIZED)

val fieldSourceCache: Map<String, List<Pair<Int, Method>>> by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
    val v = ConcurrentHashMap<String, MutableList<Pair<Int, Method>>>()
    ClassGraph().enableClassInfo().enableAnnotationInfo().enableMethodInfo().enableMemoryMapping()
        .addClassLoader(Thread.currentThread().contextClassLoader).acceptPackages("afield")//.disableJarScanning()
//        .scanAsync(Runloop.miscPool, Runtime.getRuntime().availableProcessors()).get()
        .scan()
        .use { scanResult ->
            for (cz in scanResult.allStandardClasses) {
                cz.methodInfo.forEach { methodInfo ->
                    methodInfo.annotationInfo.forEach { annotationInfo ->
                        if (annotationInfo.name == "afield.runtime.macros.LabelsArgumentsFromSource") {
                            labelledArgumentFunctions.add(methodInfo.name)
                        }
                        if (annotationInfo.name == "afield.runtime.lifecycle.RunsAt") {
                            val method = methodInfo.loadClassAndGetMethod()
                            val time = annotationInfo.parameterValues.get("value").value.toString()
                            val priority = annotationInfo.parameterValues.get("priority").value as Int
                            v.computeIfAbsent(time) { mutableListOf() }.add(priority to method)
                        }
                    }
                }
            }
        }
    v
}

fun runAll(time: String, vararg args: Any?)

fun runAll(time: String, vararg args: Any?) {
    val ns = measureNanoTime {
        fieldSourceCache
    }
    if (ns / 1_000_000 > 10)
        lprintln("info,di", "runAll precache time is ${ns / 1_000_000} ms")

    lblock("info,di", "runall at `$time`") {
        val units = fieldSourceCache[time]?.sortedBy { it.first }.orEmpty()
        val announce = time in startupPhases
        units.forEachIndexed { i, unit ->
            val method = unit.second
            // Tick the pinned "startup" row through each boot-time unit as it runs.
            if (announce) {
                lprintln(
                    "info,startup",
                    "$time · ${method.declaringClass.simpleName}.${method.name} (${i + 1}/${units.size})"
                )
            }
            method.invoke(null, *selectInvocationArgs(method, args))
        }
    }
}

Boot-time phases whose units are announced on the pinned "startup" channel (see PinnedConsole).

source — 55 lines, from line 106
private val startupPhases = setOf(early, plugins, installHandlers)

private fun selectInvocationArgs(method: Method, providedArgs: Array<out Any?>): Array<Any?> {
    val parameterTypes = method.parameterTypes
    if (parameterTypes.isEmpty()) return emptyArray()
    if (providedArgs.isEmpty()) {
        throw IllegalArgumentException(
            "No arguments provided for ${method.declaringClass.name}.${method.name}${parameterTypes.contentToString()}"
        )
    }

    val used = BooleanArray(providedArgs.size)
    val selected = arrayOfNulls<Any?>(parameterTypes.size)

    parameterTypes.forEachIndexed { index, parameterType ->
        val matchIndex = providedArgs.indices.firstOrNull { candidateIndex ->
            !used[candidateIndex] && isCompatibleArgument(parameterType, providedArgs[candidateIndex])
        } ?: throw IllegalArgumentException(
            buildString {
                append("Cannot satisfy ")
                append(method.declaringClass.name)
                append('.')
                append(method.name)
                append(" parameter ")
                append(index)
                append(" of type ")
                append(parameterType.name)
                append(" from provided args ")
                append(providedArgs.map { it?.javaClass?.name ?: "null" })
            }
        )

        used[matchIndex] = true
        selected[index] = providedArgs[matchIndex]
    }

    return selected
}

private fun isCompatibleArgument(parameterType: Class<*>, arg: Any?): Boolean {
    if (arg == null) return !parameterType.isPrimitive
    if (parameterType.isAssignableFrom(arg.javaClass)) return true

    return when (parameterType) {
        java.lang.Boolean.TYPE -> arg is Boolean
        java.lang.Byte.TYPE -> arg is Byte
        java.lang.Short.TYPE -> arg is Short
        java.lang.Integer.TYPE -> arg is Int
        java.lang.Long.TYPE -> arg is Long
        java.lang.Float.TYPE -> arg is Float
        java.lang.Double.TYPE -> arg is Double
        java.lang.Character.TYPE -> arg is Char
        else -> false
    }
}