components_tasks_TaskLaunchQueue.bs

import "pkg:/source/utils/misc.bs"
import "pkg:/source/utils/tasks.bs"

' The queue's state lives in this component's `m`, so only callFunc (which runs here, on
' the render thread) ever touches it. The rules are the pure taskQueue* functions in
' tasks.bs; this file only owns the array, the drain Timer, and the published depth.

sub init()
  m.queue = []
  ' One-shot, re-armed by each tick that leaves something waiting. Starting a Timer that
  ' is already running restarts its countdown (measured 2026-09-23 on a 512 MB Streaming
  ' Stick 3600X: launches 50 ms apart held a waiting launch until they stopped), so it
  ' is started when the queue goes from empty to waiting, never per launch.
  m.drainTimer = CreateObject("roSGNode", "Timer")
  m.drainTimer.repeat = false
  m.drainTimer.observeField("fire", "onDrainTick")
  m.top.appendChild(m.drainTimer)
end sub

' Queues `node` behind whatever waits. False only when TASK_QUEUE_CAP launches already
' wait; launchTask() then reports a refusal.
function enqueue(node as object) as boolean
  outcome = taskQueueAdd(m.queue, node, TASK_QUEUE_CAP)
  if outcome = "full" then return false
  if outcome = "queued"
    publishDepth()
    if m.queue.count() = 1 then armDrain(TASK_QUEUE_RETRY_MIN_SECONDS)
    #if perfTiming
      recordQueuedLaunch()
    #end if
  end if
  return true
end function

function isQueued(node as object) as boolean
  return taskQueueHolds(m.queue, node)
end function

sub onDrainTick()
  drained = drainTaskLaunchQueue(m.queue)
  m.queue = drained.waiting
  publishDepth()
  if m.queue.count() > 0 then armDrain(taskQueueRetrySeconds(m.drainTimer.duration, drained.started))
end sub

sub armDrain(seconds as float)
  m.drainTimer.duration = seconds
  m.drainTimer.control = "start"
end sub

' launchTask() reads this to send new launches to the back of the queue instead of past it.
sub publishDepth()
  m.global.taskLaunchQueued = m.queue.count()
end sub

#if perfTiming
  ' The queue's counterpart to the refusal trace in tasks.bs: present in every dev
  ' sideload, absent from every store build, readable from the port-8085 console:
  '
  '   ?m.global.taskLaunchQueuedTotal
  '   ?m.global.taskLaunchQueuePeak
  sub recordQueuedLaunch()
    store = m.global
    if not store.hasField("taskLaunchQueuedTotal") then store.addField("taskLaunchQueuedTotal", "integer", false)
    if not store.hasField("taskLaunchQueuePeak") then store.addField("taskLaunchQueuePeak", "integer", false)
    store["taskLaunchQueuedTotal"] = store["taskLaunchQueuedTotal"] + 1
    if m.queue.count() > store["taskLaunchQueuePeak"] then store["taskLaunchQueuePeak"] = m.queue.count()
  end sub
#end if