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}'")
}
}
}
}