Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 246 additions & 26 deletions Execution/Interpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using SharpTS.Runtime.Types;
using SharpTS.TypeSystem;
using System.Collections.Frozen;
using System.Threading;

namespace SharpTS.Execution;

Expand Down Expand Up @@ -130,9 +131,6 @@ public Interpreter()
// Track all pending timers for cleanup on disposal
private readonly System.Collections.Concurrent.ConcurrentBag<Runtime.Types.SharpTSTimeout> _pendingTimers = new();

// Event loop for managing async handles (servers, timers, etc.)
private readonly EventLoop _eventLoop = new();

// Virtual timer system - timers are checked and executed on the main thread during loop iterations.
// This avoids thread scheduling issues on macOS where background threads may not get CPU time.
// Uses PriorityQueue for O(log n) insert and O(log n) extraction of due timers.
Expand All @@ -141,6 +139,15 @@ public Interpreter()
// Volatile flag for O(1) "queue empty" check without acquiring lock
private volatile bool _hasScheduledTimers;

// Active handles counter - keeps the event loop alive while there are active operations
private int _activeHandles;
private readonly object _activeHandlesLock = new();

// Event loop infrastructure - BlockingCollection for efficient waiting (no polling)
// SynchronizationContext routes async/await continuations back to the main thread
private readonly System.Collections.Concurrent.BlockingCollection<Action> _callbackQueue = new();
private InterpreterSynchronizationContext? _eventLoopSyncContext;

/// <summary>
/// Represents a scheduled timer callback that will be executed by the main thread.
/// </summary>
Expand All @@ -162,6 +169,36 @@ public VirtualTimer(long fireTimeMs, int intervalMs, Action callback, bool isInt
}
}

/// <summary>
/// Custom SynchronizationContext that routes async/await continuations back to the event loop.
/// Ensures all user callbacks execute on the main interpreter thread (Node.js semantics).
/// </summary>
private sealed class InterpreterSynchronizationContext : SynchronizationContext
{
private readonly Action<Action> _enqueue;

public InterpreterSynchronizationContext(Action<Action> enqueue)
=> _enqueue = enqueue;

/// <summary>
/// Posts a callback to be executed asynchronously on the event loop thread.
/// Called by .NET when an async operation completes.
/// </summary>
public override void Post(SendOrPostCallback d, object? state)
=> _enqueue(() => d(state));

/// <summary>
/// Sends a callback to be executed synchronously. Simplified to use Post.
/// </summary>
public override void Send(SendOrPostCallback d, object? state)
=> Post(d, state);

/// <summary>
/// Creates a copy of this SynchronizationContext.
/// </summary>
public override SynchronizationContext CreateCopy() => this;
}

/// <summary>
/// Gets whether this interpreter has been disposed.
/// Timer callbacks check this before executing to prevent race conditions.
Expand Down Expand Up @@ -196,53 +233,231 @@ internal VirtualTimer ScheduleTimer(int delayMs, int intervalMs, Action callback
_virtualTimerQueue.Enqueue(timer, fireTime);
_hasScheduledTimers = true;
}
// Wake the event loop if the timer fires soon (within 10ms)
// This ensures immediate timers (setTimeout(fn, 0)) are processed promptly
if (delayMs <= 10)
{
WakeEventLoop();
}
return timer;
}

/// <summary>
/// Wakes the event loop by enqueueing a no-op action.
/// Used when a timer or other operation needs prompt processing.
/// </summary>
private void WakeEventLoop()
{
if (!_isDisposed && !_callbackQueue.IsAddingCompleted)
{
try { _callbackQueue.Add(() => { }); }
catch (InvalidOperationException) { /* queue completed */ }
}
}

/// <summary>
/// Enqueues a callback to be executed on the main event loop thread.
/// Thread-safe - can be called from any thread (HTTP accept loop, async I/O, etc).
/// </summary>
/// <param name="action">The callback action to execute on the main thread.</param>
internal void EnqueueCallback(Action action)
{
if (!_isDisposed && !_callbackQueue.IsAddingCompleted)
{
try { _callbackQueue.Add(action); }
catch (InvalidOperationException) { /* queue completed */ }
}
}

/// <summary>
/// Calculates the timeout until the next timer fires.
/// Used by the event loop to efficiently wait without polling.
/// </summary>
/// <returns>TimeSpan until next timer, or 60 seconds if no timers pending.</returns>
private TimeSpan GetNextTimerTimeout()
{
lock (_virtualTimersLock)
{
// Remove cancelled timers at the front of the queue
while (_virtualTimerQueue.TryPeek(out var timer, out _))
{
if (!timer.IsCancelled) break;
_virtualTimerQueue.Dequeue();
}

if (!_virtualTimerQueue.TryPeek(out _, out var fireTime))
{
_hasScheduledTimers = false;
return TimeSpan.FromSeconds(60);
}

var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var ms = fireTime - now;

// Clamp to reasonable range: 0ms to 60 seconds
if (ms <= 0) return TimeSpan.Zero;
if (ms > 60000) return TimeSpan.FromSeconds(60);
return TimeSpan.FromMilliseconds(ms);
}
}

/// <summary>
/// Increments the active handles count. Used by servers, timers, etc. to keep the event loop alive.
/// </summary>
internal void Ref()
{
lock (_activeHandlesLock)
{
_activeHandles++;
}
}

/// <summary>
/// Decrements the active handles count. When count reaches zero, the event loop can exit.
/// </summary>
internal void Unref()
{
bool shouldWake = false;
lock (_activeHandlesLock)
{
if (_activeHandles > 0)
{
_activeHandles--;
shouldWake = _activeHandles == 0;
}
}

if (shouldWake)
{
WakeEventLoop();
}
}

/// <summary>
/// Registers an async handle with the interpreter's event loop.
/// The interpreter will keep running while this handle is active.
/// Compatibility shim for existing handle-based callers.
/// </summary>
internal void RegisterHandle(IAsyncHandle handle)
{
_eventLoop.Register(handle);
Ref();
}

/// <summary>
/// Unregisters an async handle from the interpreter's event loop.
/// Compatibility shim for existing handle-based callers.
/// </summary>
internal void UnregisterHandle(IAsyncHandle handle)
{
_eventLoop.Unregister(handle);
Unref();
}

/// <summary>
/// Gets whether there are active handles keeping the event loop alive.
/// </summary>
internal bool HasActiveHandles
{
get
{
lock (_activeHandlesLock)
{
return _activeHandles > 0;
}
}
}

/// <summary>
/// Runs the event loop, processing timers and keeping the process alive while there are active handles.
/// Uses efficient waiting via ManualResetEventSlim instead of polling.
/// Runs the event loop, processing callbacks until there are no more active handles.
/// This is the main loop that keeps the program alive for servers, timers, etc.
/// </summary>
private void RunEventLoop()
/// <remarks>
/// Uses a BlockingCollection for efficient waiting (no CPU polling).
/// Sets up a SynchronizationContext to route async/await continuations back to this thread.
/// This provides Node.js-compatible single-threaded semantics where all user callbacks
/// execute on the main thread, while I/O operations run on the ThreadPool.
/// </remarks>
public void RunEventLoop()
{
// Check if there are scheduled timers - they also count as active handles
bool HasTimersOrHandles() => _hasScheduledTimers || _eventLoop.HasActiveHandles();
// Set up SynchronizationContext so async/await continuations come back to this thread
_eventLoopSyncContext = new InterpreterSynchronizationContext(EnqueueCallback);
var previousSyncContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(_eventLoopSyncContext);

while (!_isDisposed && HasTimersOrHandles())
try
{
// Process any pending timer callbacks
ProcessPendingCallbacks();
while (!_isDisposed)
{
// Exit immediately if there's no work keeping the loop alive
if (!HasActiveHandles && _callbackQueue.Count == 0)
{
break;
}

// Calculate timeout until next timer fires
var timeout = GetNextTimerTimeout();

// Efficient wait: blocks until callback arrives OR timeout expires
// This uses no CPU while waiting (unlike Thread.Sleep polling)
if (_callbackQueue.TryTake(out var action, timeout))
{
// Execute the queued callback (HTTP request handler, async continuation, etc.)
try
{
action();
}
catch (Exception ex)
{
// Log uncaught exceptions but don't crash the event loop
Console.Error.WriteLine($"Uncaught exception in event loop callback: {ex.Message}");
}
}

// Process any due timers (setTimeout, setInterval callbacks)
ProcessPendingCallbacks();

// Exit condition: no active handles AND queue is empty
// This ensures all queued callbacks are processed before exiting (like Node.js)
if (!HasActiveHandles && _callbackQueue.Count == 0)
{
break;
}
}
}
finally
{
// Drain any remaining callbacks before fully exiting
// This handles edge cases where callbacks were queued during shutdown
DrainCallbackQueue();

// If only timers are active (no server handles), we need to continue the loop
// If there are active handles, the event loop will wait efficiently
if (_eventLoop.HasActiveHandles())
// Restore previous SynchronizationContext
SynchronizationContext.SetSynchronizationContext(previousSyncContext);

// Complete the queue so any pending Add() calls don't block
try { _callbackQueue.CompleteAdding(); }
catch (ObjectDisposedException) { /* already disposed */ }
}
}

/// <summary>
/// Drains any remaining callbacks from the queue during shutdown.
/// Ensures all queued work completes before the event loop fully exits.
/// </summary>
private void DrainCallbackQueue()
{
// Process any remaining callbacks synchronously
while (_callbackQueue.TryTake(out var action, TimeSpan.Zero))
{
try
{
// Let the event loop wait for state changes (with timeout for timer processing)
_eventLoop.Run(ProcessPendingCallbacks);
action();
}
else if (_hasScheduledTimers)
catch (Exception ex)
{
// Only timers active - sleep briefly then check again
Thread.Sleep(10);
Console.Error.WriteLine($"Uncaught exception during event loop drain: {ex.Message}");
}
}

// Final timer processing
ProcessPendingCallbacks();
}

/// <summary>
Expand Down Expand Up @@ -320,8 +535,9 @@ public void Dispose()
{
_isDisposed = true;

// Dispose the event loop first to stop any waiting
_eventLoop.Dispose();
// Complete the callback queue to unblock any waiting TryTake
try { _callbackQueue.CompleteAdding(); }
catch (ObjectDisposedException) { /* already disposed */ }

// Cancel all pending timers to release resources immediately
while (_pendingTimers.TryTake(out var timer))
Expand All @@ -336,6 +552,10 @@ public void Dispose()
_hasScheduledTimers = false;
}

// Dispose the callback queue
try { _callbackQueue.Dispose(); }
catch (ObjectDisposedException) { /* already disposed */ }

GC.SuppressFinalize(this);
}

Expand Down Expand Up @@ -753,7 +973,7 @@ private ExecutionResult ExecuteExport(Stmt.Export export)
{
var result = Execute(export.Declaration);
if (result.IsAbrupt) return result;

if (_currentModuleInstance != null)
{
_currentModuleInstance.DefaultExport = GetDeclaredValue(export.Declaration);
Expand Down Expand Up @@ -823,7 +1043,7 @@ private ExecutionResult ExecuteExport(Stmt.Export export)
}
}
}

return ExecutionResult.Success();
}

Expand Down
15 changes: 14 additions & 1 deletion Runtime/BuiltIns/TimerBuiltIns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static SharpTSTimeout SetTimeout(Interpreter interpreter, ISharpTSCallabl
{
var cts = new CancellationTokenSource();
var timeout = new SharpTSTimeout(cts);
timeout.AttachRefTracking(interpreter.Ref, interpreter.Unref);

// Ensure delay is non-negative
int delay = Math.Max(0, (int)delayMs);
Expand All @@ -33,7 +34,18 @@ public static SharpTSTimeout SetTimeout(Interpreter interpreter, ISharpTSCallabl
{
if (!cts.IsCancellationRequested && !interpreter.IsDisposed)
{
callback.Call(interpreter, args);
try
{
callback.Call(interpreter, args);
}
finally
{
timeout.ReleaseRef();
}
}
else
{
timeout.ReleaseRef();
}
}, isInterval: false);

Expand Down Expand Up @@ -74,6 +86,7 @@ public static SharpTSTimeout SetInterval(Interpreter interpreter, ISharpTSCallab
{
var cts = new CancellationTokenSource();
var interval = new SharpTSTimeout(cts);
interval.AttachRefTracking(interpreter.Ref, interpreter.Unref);
int delay = Math.Max(0, (int)delayMs);

// Schedule a virtual interval timer that will be checked and executed on the main thread
Expand Down
Loading