Optimizing Fabrica-Util's Delayer for Time-Sensitive Game Event Scheduling

Reuse xsync.Delayer instances across game frames, select on the buffered Wait() channel for lock-free expiry detection, and query TimeRemaining() to dynamically throttle workload intensity without triggering garbage collection pauses.

The go-pantheon/fabrica-util repository provides a specialized concurrency primitive designed for deterministic, high-frequency timing operations. Optimizing fabrica-util's Delayer for time-sensitive game event scheduling requires leveraging its zero-allocation reset patterns and non-blocking notification architecture to maintain consistent frame rates in demanding simulation loops.

Core Architecture in xsync/delayer.go

The Delayer type in xsync/delayer.go wraps a time.Timer with a thread-safe control layer. At its core, it maintains an expiryTime protected by a sync.RWMutex, allowing concurrent reads of timing state without contention.

The implementation uses a buffered channel (tick chan struct{}) with capacity 1 to signal expiry. This buffering is critical: when the underlying time.Timer fires, the send operation on tick never blocks, preventing goroutine leaks even if the consumer is temporarily busy. A separate stopCh and stopped flag coordinate graceful termination between the public API and the internal expiry-handling goroutine.

Key methods include:

  • SetExpiryTime(t time.Time) – Atomically updates the target time and resets the internal timer. If t is in the past, it emits a tick immediately.
  • Wait() chan struct{} – Returns the receive-only tick channel for use in select statements.
  • Reset() – Stops the pending timer, clears the expiry state, and marks the Delayer as stopped without deallocating the structure.
  • Close() – Like Reset, but additionally signals the internal goroutine via stopCh to terminate permanently.

Why Delayer Excels for Game Loops

Standard library timers often force a trade-off between allocation overhead (creating new timers per frame) or complex lifecycle management (stopping and draining channels). The Delayer eliminates this through fast reuse via Reset(), avoiding GC pressure in 60Hz or 120Hz loops where per-frame allocations cause stutter.

The design provides low-latency notification through the buffered tick channel. Unlike unbuffered channels that require both sender and receiver to be ready, Delayer's size-1 buffer ensures the timer callback completes instantly, moving synchronization overhead to the consumer side where select blocks efficiently.

Concurrent safety is enforced through strategic locking in xsync/delayer.go. The RWMutex allows multiple goroutines to call IsExpired() or TimeRemaining() simultaneously, while exclusive locks protect timer resets. This pattern is validated by the race detector tests in xsync/delayer_test.go.

Optimization Strategies for High-Frequency Scheduling

Reuse Delayers to Eliminate GC Pressure

Allocate Delayers once during system initialization, then reset them each frame. This pattern prevents the garbage collector from pausing your game loop to reclaim timer objects.

// Initialize once at startup
frameTimer := xsync.NewDelayer()
defer frameTimer.Close()

// Main loop at 60 FPS (16.67ms)
for {
    targetTime := time.Now().Add(16 * time.Millisecond)
    frameTimer.SetExpiryTime(targetTime)

    select {
    case <-frameTimer.Wait():
        updateGameState()
        renderFrame()
    case <-shutdownCh:
        return
    }
}

Leverage TimeRemaining for Adaptive Workloads

When frame budgets vary, use TimeRemaining() to scale AI complexity or physics fidelity dynamically. This prevents frame overruns that cause visible stuttering.

frameTimer.SetExpiryTime(time.Now().Add(16 * time.Millisecond))

// Guaranteed work
processInput()
updateAnimations()

// Optional work based on budget
if frameTimer.TimeRemaining() > 5*time.Millisecond {
    runExpensivePathfinding()
} else {
    runSimplePathfinding()
}

<-frameTimer.Wait() // Synchronize to frame boundary

Implement Safety Checks with IsExpired

For long-running operations that might exceed their time slice, poll IsExpired() to implement cooperative cancellation. This is crucial for procedurally generated content or complex physics calculations that must yield to the next frame.

deadline := xsync.NewDelayer()
deadline.SetExpiryTime(time.Now().Add(8 * time.Millisecond))

for _, entity := range worldEntities {
    if deadline.IsExpired() {
        break // Defer remaining work to next frame
    }
    updateEntityPhysics(entity)
}

Ensure Graceful Shutdown with Close()

Always invoke Close() when terminating game servers or unloading levels. This drains the internal stopCh and terminates the background goroutine, preventing resource leaks during hot-reloads or level transitions.

func (s *Server) Shutdown() {
    s.tickTimer.Close() // Signals stopCh and cleans up
    s.wg.Wait()
}

Production Code Examples

Fixed-Rate Physics Tick (20 Hz)

For deterministic simulation steps independent of frame rate, use a consistent tick duration:

const physicsRate = 50 * time.Millisecond

physicsTimer := xsync.NewDelayer()
defer physicsTimer.Close()

for {
    physicsTimer.SetExpiryTime(time.Now().Add(physicsRate))
    
    <-physicsTimer.Wait()
    stepPhysicsSimulation()
}

One-Shot World Event Scheduling

For sporadic events like boss spawns or weather changes, Delayer provides cleaner semantics than raw timers:

bossSpawn := xsync.NewDelayer()
defer bossSpawn.Close()

bossSpawn.SetExpiryTime(time.Now().Add(30 * time.Second))

go func() {
    <-bossSpawn.Wait()
    spawnWorldBoss()
}()

Concurrent State Monitoring

Multiple systems can safely query timing state without synchronization overhead:

// System A: Rendering
if frameTimer.TimeRemaining() < 2*time.Millisecond {
    skipParticleEffects()
}

// System B: Networking
if frameTimer.IsExpired() {
    sendQueuedPackets()
}

Benchmarking and Validation

The repository includes targeted benchmarks in xsync/delayer_test.go to verify performance under load. Run BenchmarkDelayer_SetExpiryTime to measure the latency of timer resets, and BenchmarkDelayer_ConcurrentAccess to validate that read-heavy workloads (multiple goroutines calling IsExpired()) scale without mutex contention.

Use these benchmarks to determine the maximum concurrency your game loop can sustain before timing precision degrades. The read-write mutex design typically supports thousands of concurrent reads per millisecond, sufficient for MMO-scale entity management.

Summary

  • Reuse instances: Call Reset() between frames rather than allocating new Delayers to eliminate GC pressure in tight loops.
  • Non-blocking waits: Select on Wait() to receive instant notifications via the buffered channel architecture.
  • Dynamic scaling: Query TimeRemaining() to adjust workload complexity and maintain consistent frame times.
  • Safe termination: Always invoke Close() during shutdown to prevent goroutine leaks.
  • Validate performance: Reference BenchmarkDelayer_ConcurrentAccess in xsync/delayer_test.go to ensure your concurrency model matches production load.

Frequently Asked Questions

How does Delayer prevent goroutine leaks compared to standard time.Timer?

Delayer uses a buffered tick channel with capacity 1 in xsync/delayer.go, ensuring that when the underlying time.Timer fires, the send operation completes immediately without blocking the timer's internal goroutine. Additionally, the Close() method signals a dedicated stopCh that terminates the expiry-monitoring goroutine, whereas raw timers require manual stopping and channel draining to avoid leaks.

Can multiple goroutines safely check timer status simultaneously?

Yes. The implementation guards expiryTime with a sync.RWMutex, allowing concurrent calls to IsExpired() and TimeRemaining() without blocking each other. Only write operations like SetExpiryTime() acquire exclusive locks, making the Delayer suitable for read-heavy game architectures where hundreds of entities query timing state concurrently.

What is the difference between Reset() and Close()?

Reset() stops the active timer and clears the expiry state, preparing the Delayer for immediate reuse while keeping the internal goroutine alive. Close() performs the same cleanup but additionally writes to stopCh, signaling the background goroutine to terminate permanently. Use Reset() between frames in recurring loops, and Close() only when permanently discarding the Delayer instance.

How precise is SetExpiryTime for sub-millisecond scheduling?

SetExpiryTime offers microsecond-level precision limited only by the Go runtime's timer resolution and OS scheduling. When passed a timestamp in the past, it emits a tick immediately through the buffered channel. For physics simulations requiring sub-millisecond accuracy, pair Delayer with time.Now() and time.Since() calculations, validating timing variance using the benchmarks provided in xsync/delayer_test.go.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →