From f3eb1957b670ef345c6f5eed277aa60ff20176c1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 11 Feb 2022 20:13:39 +0100 Subject: [PATCH 1/6] Scheduler. --- Jint.Tests/Jint.Tests.csproj | 1 - Jint.Tests/Runtime/SchedulingTests.cs | 66 +++++++++++++++++ Jint/Engine.cs | 60 ++++++++++++++- Jint/Jint.csproj | 3 +- Jint/Scheduling/DeferredTask.cs | 44 +++++++++++ Jint/Scheduling/IDeferredTask.cs | 11 +++ Jint/Scheduling/Scheduler.cs | 101 ++++++++++++++++++++++++++ 7 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 Jint.Tests/Runtime/SchedulingTests.cs create mode 100644 Jint/Scheduling/DeferredTask.cs create mode 100644 Jint/Scheduling/IDeferredTask.cs create mode 100644 Jint/Scheduling/Scheduler.cs diff --git a/Jint.Tests/Jint.Tests.csproj b/Jint.Tests/Jint.Tests.csproj index 13d7ef1cf1..15be8accce 100644 --- a/Jint.Tests/Jint.Tests.csproj +++ b/Jint.Tests/Jint.Tests.csproj @@ -1,7 +1,6 @@  net6.0 - $(TargetFrameworks);net461 ..\Jint\Jint.snk true false diff --git a/Jint.Tests/Runtime/SchedulingTests.cs b/Jint.Tests/Runtime/SchedulingTests.cs new file mode 100644 index 0000000000..7f55a31cff --- /dev/null +++ b/Jint.Tests/Runtime/SchedulingTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Jint.Tests.Runtime +{ + public class SchedulingTests + { + class Context + { + public string Result { get; set; } + } + + [Fact] + public async Task CanRunAsyncCode() + { + var engine = new Engine(); + + var ctx = new Context(); + + engine.SetValue("ctx", ctx); + engine.SetValue("setTimeout", new Action(async (callback, delay) => + { + using (var task = engine.CreateTask()) + { + await Task.Delay(delay); + + task.Invoke(callback); + } + })); + + var result = await engine.EvaluateAsync(@" + setTimeout(function () { + ctx.Result = 'Hello World'; + }, 100); + "); + + Assert.Equal("Hello World", ctx.Result); + } + + [Fact] + public async Task CanRunSyncCode() + { + var engine = new Engine(); + + var ctx = new Context(); + + engine.SetValue("ctx", ctx); + engine.SetValue("setTimeout", new Action((callback, delay) => + { + using (var task = engine.CreateTask()) + { + task.Invoke(callback); + } + })); + + var result = await engine.EvaluateAsync(@" + setTimeout(function () { + ctx.Result = 'Hello World'; + }, 100); + "); + + Assert.Equal("Hello World", ctx.Result); + } + } +} diff --git a/Jint/Engine.cs b/Jint/Engine.cs index ad5935bcfa..217ffd2f4a 100644 --- a/Jint/Engine.cs +++ b/Jint/Engine.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Esprima; using Esprima.Ast; using Jint.Native; @@ -20,6 +21,7 @@ using Jint.Runtime.Interpreter; using Jint.Runtime.Interpreter.Expressions; using Jint.Runtime.References; +using Jint.Scheduling; namespace Jint { @@ -36,6 +38,7 @@ public partial class Engine internal EvaluationContext _activeEvaluationContext; private readonly EventLoop _eventLoop = new(); + private Scheduler _scheduler; // lazy properties private DebugHandler _debugHandler; @@ -51,6 +54,7 @@ public partial class Engine internal readonly JsValueArrayPool _jsValueArrayPool; internal readonly ExtensionMethodCache _extensionMethods; + public ITypeConverter ClrTypeConverter { get; internal set; } // cache of types used when resolving CLR type names @@ -253,8 +257,46 @@ public Engine Execute(string source) public Engine Execute(string source, ParserOptions parserOptions) => Execute(new JavaScriptParser(source, parserOptions).ParseScript()); + public async Task EvaluateAsync(string source) + { + await ExecuteAsync(source, DefaultParserOptions); + return _completionValue; + } + + public async Task EvaluateAsync(string source, ParserOptions parserOptions) + { + await ExecuteAsync(source, parserOptions); + return _completionValue; + } + + public async Task EvaluateAsync(Script script) + { + await ExecuteAsync(script); + return _completionValue; + } + + public Task ExecuteAsync(string source) + => ExecuteAsync(source, DefaultParserOptions); + + public Task ExecuteAsync(string source, ParserOptions parserOptions) + => ExecuteAsync(new JavaScriptParser(source, parserOptions).ParseScript()); + public Engine Execute(Script script) { + _ = ExecuteAsync(script); + + return this; + } + + public async Task ExecuteAsync(Script script) + { + if (_scheduler != null) + { + throw new InvalidOperationException("Another call is pending."); + } + + _scheduler = new Scheduler(); + Engine DoInvoke() { GlobalDeclarationInstantiation( @@ -285,17 +327,27 @@ Engine DoInvoke() // TODO what about callstack and thrown exceptions? RunAvailableContinuations(_eventLoop); - _completionValue = result.GetValueOrDefault(); - return this; } - var strict = _isStrict || script.Strict; - ExecuteWithConstraints(strict, DoInvoke); + var task = _scheduler.CreateTask(); + + task.Invoke(() => + { + var strict = _isStrict || script.Strict; + ExecuteWithConstraints(strict, DoInvoke); + }); + + await _scheduler.Completion; return this; } + public IDeferredTask CreateTask() + { + return _scheduler.CreateTask(); + } + /// /// EXPERIMENTAL! Subject to change. /// diff --git a/Jint/Jint.csproj b/Jint/Jint.csproj index 6b3b23e88d..1097671b69 100644 --- a/Jint/Jint.csproj +++ b/Jint/Jint.csproj @@ -1,7 +1,7 @@  en-US - net461;netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1 Jint.snk true latest @@ -11,5 +11,6 @@ + diff --git a/Jint/Scheduling/DeferredTask.cs b/Jint/Scheduling/DeferredTask.cs new file mode 100644 index 0000000000..5b0884b743 --- /dev/null +++ b/Jint/Scheduling/DeferredTask.cs @@ -0,0 +1,44 @@ +using System; + +namespace Jint.Scheduling +{ + internal class DeferredTask : IDeferredTask + { + private readonly Scheduler _scheduler; + private bool _isCompleted; + + public DeferredTask(Scheduler scheduler) + { + _scheduler = scheduler; + } + + public void Dispose() + { + Cancel(); + } + + public void Cancel() + { + if (_isCompleted) + { + return; + } + + _isCompleted = true; + + _scheduler.Cancel(this); + } + + public void Invoke(Action action) + { + if (_isCompleted) + { + return; + } + + _isCompleted = true; + + _scheduler.Invoke(this, action); + } + } +} diff --git a/Jint/Scheduling/IDeferredTask.cs b/Jint/Scheduling/IDeferredTask.cs new file mode 100644 index 0000000000..5d6eacf664 --- /dev/null +++ b/Jint/Scheduling/IDeferredTask.cs @@ -0,0 +1,11 @@ +using System; + +namespace Jint.Scheduling +{ + public interface IDeferredTask : IDisposable + { + void Invoke(Action action); + + void Cancel(); + } +} diff --git a/Jint/Scheduling/Scheduler.cs b/Jint/Scheduling/Scheduler.cs new file mode 100644 index 0000000000..20ecdd8506 --- /dev/null +++ b/Jint/Scheduling/Scheduler.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Jint.Scheduling +{ + internal sealed class Scheduler : IDisposable + { + private readonly TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); + private readonly HashSet _pendingTasks = new HashSet(); + private readonly Queue _inlinedTasks = new Queue(); + private bool _isRunning; + private bool _isDisposed; + + public Task Completion + { + get => _taskCompletionSource.Task; + } + + public void Dispose() + { + _isDisposed = true; + } + + public IDeferredTask CreateTask() + { + var task = new DeferredTask(this); + + _pendingTasks.Add(task); + + return task; + } + + public void Invoke(DeferredTask task, Action action) + { + if (_isDisposed) + { + return; + } + + _pendingTasks.Remove(task); + + if (_isRunning) + { + _inlinedTasks.Enqueue(action); + return; + } + + _isRunning = true; + try + { + action(); + + RunAvailableContinuations(); + + TryComplete(); + } + catch (Exception ex) + { + _taskCompletionSource.TrySetException(ex); + } + finally + { + _isRunning = false; + } + } + + internal void Cancel(DeferredTask deferredTask) + { + _pendingTasks.Remove(deferredTask); + + TryComplete(); + } + + private void TryComplete() + { + if (_pendingTasks.Count == 0) + { + _taskCompletionSource.TrySetResult(true); + } + } + + private void RunAvailableContinuations() + { + var queue = _inlinedTasks; + + while (true) + { + if (queue.Count == 0) + { + return; + } + + var nextContinuation = queue.Dequeue(); + + // note that continuation can enqueue new events + nextContinuation(); + } + } + } +} From 9c67783708054c2191552222776edab1a818d614 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 11 Feb 2022 20:14:30 +0100 Subject: [PATCH 2/6] Another test --- Jint.Tests/Runtime/SchedulingTests.cs | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Jint.Tests/Runtime/SchedulingTests.cs b/Jint.Tests/Runtime/SchedulingTests.cs index 7f55a31cff..1a27d5a769 100644 --- a/Jint.Tests/Runtime/SchedulingTests.cs +++ b/Jint.Tests/Runtime/SchedulingTests.cs @@ -37,6 +37,36 @@ public async Task CanRunAsyncCode() Assert.Equal("Hello World", ctx.Result); } + [Fact] + public async Task CanRunNestedAsyncCode() + { + var engine = new Engine(); + + var ctx = new Context(); + + engine.SetValue("ctx", ctx); + engine.SetValue("setTimeout", new Action(async (callback, delay) => + { + using (var task = engine.CreateTask()) + { + await Task.Delay(delay); + + task.Invoke(callback); + } + })); + + var result = await engine.EvaluateAsync(@" + setTimeout(function () { + setTimeout(function () { + setTimeout(function () { + ctx.Result = 'Hello World'; + }, 100); + }, 100); + }, 100); + "); + + Assert.Equal("Hello World", ctx.Result); + } [Fact] public async Task CanRunSyncCode() From ce7ed7c1ad4e5ca0948c32bf4d7944b51ea566d8 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 11 Feb 2022 20:18:16 +0100 Subject: [PATCH 3/6] Get rid of EvaluateAsync. --- Jint/Engine.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Jint/Engine.cs b/Jint/Engine.cs index 217ffd2f4a..f59b6ac244 100644 --- a/Jint/Engine.cs +++ b/Jint/Engine.cs @@ -257,24 +257,6 @@ public Engine Execute(string source) public Engine Execute(string source, ParserOptions parserOptions) => Execute(new JavaScriptParser(source, parserOptions).ParseScript()); - public async Task EvaluateAsync(string source) - { - await ExecuteAsync(source, DefaultParserOptions); - return _completionValue; - } - - public async Task EvaluateAsync(string source, ParserOptions parserOptions) - { - await ExecuteAsync(source, parserOptions); - return _completionValue; - } - - public async Task EvaluateAsync(Script script) - { - await ExecuteAsync(script); - return _completionValue; - } - public Task ExecuteAsync(string source) => ExecuteAsync(source, DefaultParserOptions); From 8a4daf70fb1a0d4c66e23526a7aa04e7df1d522f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 11 Feb 2022 20:27:09 +0100 Subject: [PATCH 4/6] Fix tests. --- Jint.Tests/Runtime/SchedulingTests.cs | 6 ++--- Jint/Engine.cs | 38 ++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/Jint.Tests/Runtime/SchedulingTests.cs b/Jint.Tests/Runtime/SchedulingTests.cs index 1a27d5a769..b70a0fc1d3 100644 --- a/Jint.Tests/Runtime/SchedulingTests.cs +++ b/Jint.Tests/Runtime/SchedulingTests.cs @@ -29,7 +29,7 @@ public async Task CanRunAsyncCode() } })); - var result = await engine.EvaluateAsync(@" + await engine.ExecuteAsync(@" setTimeout(function () { ctx.Result = 'Hello World'; }, 100); @@ -55,7 +55,7 @@ public async Task CanRunNestedAsyncCode() } })); - var result = await engine.EvaluateAsync(@" + await engine.ExecuteAsync(@" setTimeout(function () { setTimeout(function () { setTimeout(function () { @@ -84,7 +84,7 @@ public async Task CanRunSyncCode() } })); - var result = await engine.EvaluateAsync(@" + await engine.ExecuteAsync(@" setTimeout(function () { ctx.Result = 'Hello World'; }, 100); diff --git a/Jint/Engine.cs b/Jint/Engine.cs index f59b6ac244..fd363e4943 100644 --- a/Jint/Engine.cs +++ b/Jint/Engine.cs @@ -265,7 +265,43 @@ public Task ExecuteAsync(string source, ParserOptions parserOptions) public Engine Execute(Script script) { - _ = ExecuteAsync(script); + Engine DoInvoke() + { + GlobalDeclarationInstantiation( + script, + Realm.GlobalEnv); + + var list = new JintStatementList(null, script.Body); + + Completion result; + try + { + result = list.Execute(_activeEvaluationContext); + } + catch + { + // unhandled exception + ResetCallStack(); + throw; + } + + if (result.Type == CompletionType.Throw) + { + var ex = new JavaScriptException(result.GetValueOrDefault()).SetCallstack(this, result.Location); + ResetCallStack(); + throw ex; + } + + // TODO what about callstack and thrown exceptions? + RunAvailableContinuations(_eventLoop); + + _completionValue = result.GetValueOrDefault(); + + return this; + } + + var strict = _isStrict || script.Strict; + ExecuteWithConstraints(strict, DoInvoke); return this; } From e252098d9f2cea05dc773fac5964128921389b90 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 11 Feb 2022 20:47:05 +0100 Subject: [PATCH 5/6] Fix scheduler usage. --- Jint/Engine.cs | 68 +++++++++++++++--------------------- Jint/Scheduling/Scheduler.cs | 4 ++- 2 files changed, 31 insertions(+), 41 deletions(-) diff --git a/Jint/Engine.cs b/Jint/Engine.cs index fd363e4943..aaed1ab37a 100644 --- a/Jint/Engine.cs +++ b/Jint/Engine.cs @@ -38,7 +38,7 @@ public partial class Engine internal EvaluationContext _activeEvaluationContext; private readonly EventLoop _eventLoop = new(); - private Scheduler _scheduler; + private readonly Stack _schedulers = new Stack(); // lazy properties private DebugHandler _debugHandler; @@ -265,56 +265,46 @@ public Task ExecuteAsync(string source, ParserOptions parserOptions) public Engine Execute(Script script) { - Engine DoInvoke() + using (var scheduler = new Scheduler()) { - GlobalDeclarationInstantiation( - script, - Realm.GlobalEnv); - - var list = new JintStatementList(null, script.Body); - - Completion result; try { - result = list.Execute(_activeEvaluationContext); - } - catch - { - // unhandled exception - ResetCallStack(); - throw; - } + _schedulers.Push(scheduler); - if (result.Type == CompletionType.Throw) + ExecuteWithScheduler(scheduler, script); + } + finally { - var ex = new JavaScriptException(result.GetValueOrDefault()).SetCallstack(this, result.Location); - ResetCallStack(); - throw ex; + _schedulers.Pop(); } - - // TODO what about callstack and thrown exceptions? - RunAvailableContinuations(_eventLoop); - - _completionValue = result.GetValueOrDefault(); - - return this; } - var strict = _isStrict || script.Strict; - ExecuteWithConstraints(strict, DoInvoke); - return this; } public async Task ExecuteAsync(Script script) { - if (_scheduler != null) + using (var scheduler = new Scheduler()) { - throw new InvalidOperationException("Another call is pending."); + try + { + _schedulers.Push(scheduler); + + ExecuteWithScheduler(scheduler, script); + + await scheduler.Completion; + } + finally + { + _schedulers.Pop(); + } } - _scheduler = new Scheduler(); + return this; + } + private void ExecuteWithScheduler(Scheduler scheduler, Script script) + { Engine DoInvoke() { GlobalDeclarationInstantiation( @@ -345,25 +335,23 @@ Engine DoInvoke() // TODO what about callstack and thrown exceptions? RunAvailableContinuations(_eventLoop); + _completionValue = result.GetValueOrDefault(); + return this; } - var task = _scheduler.CreateTask(); + var task = scheduler.CreateTask(); task.Invoke(() => { var strict = _isStrict || script.Strict; ExecuteWithConstraints(strict, DoInvoke); }); - - await _scheduler.Completion; - - return this; } public IDeferredTask CreateTask() { - return _scheduler.CreateTask(); + return _schedulers.Peek().CreateTask(); } /// diff --git a/Jint/Scheduling/Scheduler.cs b/Jint/Scheduling/Scheduler.cs index 20ecdd8506..f4a59fde68 100644 --- a/Jint/Scheduling/Scheduler.cs +++ b/Jint/Scheduling/Scheduler.cs @@ -11,6 +11,7 @@ internal sealed class Scheduler : IDisposable private readonly Queue _inlinedTasks = new Queue(); private bool _isRunning; private bool _isDisposed; + private bool _isMainFlow = true; public Task Completion { @@ -55,12 +56,13 @@ public void Invoke(DeferredTask task, Action action) TryComplete(); } - catch (Exception ex) + catch (Exception ex) when (!_isMainFlow) { _taskCompletionSource.TrySetException(ex); } finally { + _isMainFlow = false; _isRunning = false; } } From ac93f8b4cc532604e84a69ba03c4c96804904ab8 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 11 Feb 2022 20:49:59 +0100 Subject: [PATCH 6/6] Fix scheduler. --- Jint/Engine.cs | 9 +++++++-- Jint/Scheduling/Scheduler.cs | 11 +++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Jint/Engine.cs b/Jint/Engine.cs index aaed1ab37a..af0371e9ee 100644 --- a/Jint/Engine.cs +++ b/Jint/Engine.cs @@ -265,7 +265,7 @@ public Task ExecuteAsync(string source, ParserOptions parserOptions) public Engine Execute(Script script) { - using (var scheduler = new Scheduler()) + using (var scheduler = new Scheduler(false)) { try { @@ -284,7 +284,7 @@ public Engine Execute(Script script) public async Task ExecuteAsync(Script script) { - using (var scheduler = new Scheduler()) + using (var scheduler = new Scheduler(true)) { try { @@ -351,6 +351,11 @@ Engine DoInvoke() public IDeferredTask CreateTask() { + if (_schedulers.Count == 0) + { + throw new InvalidOperationException("Not within a script."); + } + return _schedulers.Peek().CreateTask(); } diff --git a/Jint/Scheduling/Scheduler.cs b/Jint/Scheduling/Scheduler.cs index f4a59fde68..630957e32c 100644 --- a/Jint/Scheduling/Scheduler.cs +++ b/Jint/Scheduling/Scheduler.cs @@ -12,6 +12,12 @@ internal sealed class Scheduler : IDisposable private bool _isRunning; private bool _isDisposed; private bool _isMainFlow = true; + private bool _allowAsyncFlow; + + public Scheduler(bool allowAsyncFlow) + { + _allowAsyncFlow = allowAsyncFlow; + } public Task Completion { @@ -47,6 +53,11 @@ public void Invoke(DeferredTask task, Action action) return; } + if (!_allowAsyncFlow) + { + throw new InvalidOperationException("You can only run async task when using ExecuteAsync()"); + } + _isRunning = true; try {