public suspend fun runHeadlessApplication()

in hot-reload-runtime-jvm/src/main/kotlin/org/jetbrains/compose/reload/jvm/runHeadless.kt [79:239]


public suspend fun runHeadlessApplication(
    timeout: Duration, width: Int, height: Int,
    content: @Composable () -> Unit
): Unit = coroutineScope {
    val applicationScope = this
    val messages = orchestration.asChannel()

    val sceneSize = computeSceneSize(width, height, content)
    val scene = ImageComposeScene(
        sceneSize.width, sceneSize.height, coroutineContext = applicationScope.coroutineContext
    )
    scene.setContent {
        Box(modifier = Modifier.fillMaxSize().background(Color.White)) {
            DevelopmentEntryPoint { content() }
        }
    }

    applicationScope.launch {
        delay(timeout)
        logger.error("Application timed out...")
        applicationScope.cancel("Application timed out...")
    }

    /* Main loop */
    applicationScope.launch {
        /*
        The virtual time can also be provided as orchestration state.
        The virtual time from such state has precedence and always be used
         */
        val virtualTimeState = orchestration.states.get(VirtualTimeState)

        /*
        The actual current virtual time.
        If the virtual time is provided by the orchestration, then this time will be used.
        Otherwise, the virtual time will automatically advance
         */
        var virtualTime = virtualTimeState.value?.time?.inWholeNanoseconds ?: 0
        val virtualFrameDuration = 256.milliseconds

        val lastMessageBufferSize = 48
        val lastMessagesBuffer = ArrayDeque<OrchestrationMessage>(48)
        var previousMessageClockTime: Instant? = null
        var previousSilenceWarningSystemClockTime = Clock.System.now()
        var silenceDuration = Duration.ZERO

        while (isActive) {
            virtualTime = virtualTimeState.value?.time?.inWholeNanoseconds
                ?: (virtualTime + virtualFrameDuration.inWholeNanoseconds)

            if (scene.hasInvalidations()) {
                scene.render(virtualTime)

                /*
                The 'continue' below requires us to yield the current thread before entering the loop again
                to prevent thread starvation.
                 */
                yield()

                /*
                If we still have invalidations after the render + yield,
                then we can throttle the loop
                 */
                if (scene.hasInvalidations()) {
                    logger.info("Throttling render loop at ${virtualTime.nanoseconds}")
                    delay(32.milliseconds)
                }

                /*
                If there is no virtual time provided by the orchestration, then we assume that invalidations
                are inconsistent states, and we shall advance time until we end up in a stable consistent state again.
                */
                if (virtualTimeState.value == null) {
                    continue
                }
            }

            val message = select {
                messages.onReceive { it }
                onTimeout(virtualFrameDuration) { null }
            }

            if (message == null || message.isSilenceWarning()) {
                silenceDuration += virtualFrameDuration

                val timeout = currentCoroutineContext()[SilenceTimeout]
                val previousWarningDuration = Clock.System.now() - previousSilenceWarningSystemClockTime

                if (timeout != null && silenceDuration >= timeout.timeout) {
                    val currentTime = Clock.System.now()
                        .toJavaInstant().atZone(ZoneId.systemDefault())
                        .format(DateTimeFormatter.ISO_TIME)

                    val previousMessageTime = previousMessageClockTime
                        ?.toJavaInstant()?.atZone(ZoneId.systemDefault())
                        ?.format(DateTimeFormatter.ISO_TIME) ?: "N/A"


                    throw TimeoutException(
                        """
                           🚨🔇⏰ Silence Timeout
                           
                           No messages received for: $silenceDuration
                           Current Time: $currentTime
                           Previous Message Time: $previousMessageTime
                           Last Messages: (${lastMessagesBuffer.size}):
                               - {{message}}
                       """.trimIndent().asTemplateOrThrow()
                            .renderOrThrow {
                                lastMessagesBuffer.forEach { message ->
                                    "message"(message.toString())
                                }
                            }
                    )
                }

                if (silenceDuration > 5.seconds && previousWarningDuration > 5.seconds) {
                    issueSilenceWarning(silenceDuration, timeout)
                    previousSilenceWarningSystemClockTime = Clock.System.now()
                }

                continue
            }

            if (lastMessagesBuffer.size >= lastMessageBufferSize) {
                lastMessagesBuffer.removeFirstOrNull()
            }

            lastMessagesBuffer.addLast(message)

            silenceDuration = Duration.ZERO
            previousMessageClockTime = Clock.System.now()

            if (message is ShutdownRequest && message.isApplicable()) {
                applicationScope.coroutineContext.job.cancelChildren()
                return@launch
            }

            if (message !is OrchestrationMessage.Ack && message !is OrchestrationMessage.LogMessage) {
                if (message is OrchestrationMessage.Ping) {
                    logger.info("Responding to ping '${message.messageId}'")
                }

                orchestration.send(OrchestrationMessage.Ack(message.messageId))
            }

            /* Break out for TestEvents and give the main thread time to handle that */
            if (message is OrchestrationMessage.TestEvent) {
                yield()
            }

            if (message is OrchestrationMessage.TakeScreenshotRequest) {
                logger.info("Taking screenshot: '${message.messageId}'")
                val baos = ByteArrayOutputStream()
                ImageIO.write(scene.render(virtualTime).toComposeImageBitmap().toAwtImage(), "png", baos)
                orchestration.send(OrchestrationMessage.Screenshot("png", baos.toByteArray()))
                logger.debug("Sent screenshot: '${message.messageId}'")
            }
        }

    }
}