Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Scheduler #1082

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 0 additions & 1 deletion Jint.Tests/Jint.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">$(TargetFrameworks);net461</TargetFrameworks>
<AssemblyOriginatorKeyFile>..\Jint\Jint.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
<IsPackable>false</IsPackable>
Expand Down
96 changes: 96 additions & 0 deletions Jint.Tests/Runtime/SchedulingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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<Action, int>(async (callback, delay) =>
{
using (var task = engine.CreateTask())
{
await Task.Delay(delay);

task.Invoke(callback);
}
}));

await engine.ExecuteAsync(@"
setTimeout(function () {
ctx.Result = 'Hello World';
}, 100);
");

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<Action, int>(async (callback, delay) =>
{
using (var task = engine.CreateTask())
{
await Task.Delay(delay);

task.Invoke(callback);
}
}));

await engine.ExecuteAsync(@"
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {
ctx.Result = 'Hello World';
}, 100);
}, 100);
}, 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<Action, int>((callback, delay) =>
{
using (var task = engine.CreateTask())
{
task.Invoke(callback);
}
}));

await engine.ExecuteAsync(@"
setTimeout(function () {
ctx.Result = 'Hello World';
}, 100);
");

Assert.Equal("Hello World", ctx.Result);
}
}
}
70 changes: 70 additions & 0 deletions Jint/Engine.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,6 +21,7 @@
using Jint.Runtime.Interpreter;
using Jint.Runtime.Interpreter.Expressions;
using Jint.Runtime.References;
using Jint.Scheduling;

namespace Jint
{
Expand All @@ -36,6 +38,7 @@ public partial class Engine
internal EvaluationContext _activeEvaluationContext;

private readonly EventLoop _eventLoop = new();
private Scheduler _scheduler;

// lazy properties
private DebugHandler _debugHandler;
Expand All @@ -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
Expand Down Expand Up @@ -253,6 +257,12 @@ public Engine Execute(string source)
public Engine Execute(string source, ParserOptions parserOptions)
=> Execute(new JavaScriptParser(source, parserOptions).ParseScript());

public Task<Engine> ExecuteAsync(string source)
=> ExecuteAsync(source, DefaultParserOptions);

public Task<Engine> ExecuteAsync(string source, ParserOptions parserOptions)
=> ExecuteAsync(new JavaScriptParser(source, parserOptions).ParseScript());

public Engine Execute(Script script)
{
Engine DoInvoke()
Expand Down Expand Up @@ -296,6 +306,66 @@ Engine DoInvoke()
return this;
}

public async Task<Engine> ExecuteAsync(Script script)
{
if (_scheduler != null)
{
throw new InvalidOperationException("Another call is pending.");
}

_scheduler = new Scheduler();

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);

return this;
}

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();
}

/// <summary>
/// EXPERIMENTAL! Subject to change.
///
Expand Down
3 changes: 2 additions & 1 deletion Jint/Jint.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NeutralLanguage>en-US</NeutralLanguage>
<TargetFrameworks>net461;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<AssemblyOriginatorKeyFile>Jint.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
<LangVersion>latest</LangVersion>
Expand All @@ -11,5 +11,6 @@
<PackageReference Include="Esprima" Version="2.1.2" />
<PackageReference Include="IsExternalInit" Version="1.0.1" PrivateAssets="all" />
<PackageReference Include="Nullable" Version="1.3.0" PrivateAssets="all" />
<PackageReference Include="System.Threading.Tasks" Version="4.3.0" />
</ItemGroup>
</Project>
44 changes: 44 additions & 0 deletions Jint/Scheduling/DeferredTask.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
11 changes: 11 additions & 0 deletions Jint/Scheduling/IDeferredTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;

namespace Jint.Scheduling
{
public interface IDeferredTask : IDisposable
{
void Invoke(Action action);

void Cancel();
}
}
101 changes: 101 additions & 0 deletions Jint/Scheduling/Scheduler.cs
Original file line number Diff line number Diff line change
@@ -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<object> _taskCompletionSource = new TaskCompletionSource<object>();
private readonly HashSet<DeferredTask> _pendingTasks = new HashSet<DeferredTask>();
private readonly Queue<Action> _inlinedTasks = new Queue<Action>();
private bool _isRunning;
private bool _isDisposed;

public Task Completion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a strong opinion that Jint should not have Task as a part of it's API surface area, otherwise there is an implicit promise (no pun intended) of taking care of thread safety, which is not something Jint is interested in (I might be wrong here).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe as a small piece of "evidence", no JS engines I know provide comprehensive async support. You basically have to do all the steps I described in the issue in a host environment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean with async support?

For me, in this context "async" is everything that is not invoked immedately. SetTimeout, setInterval, Promises, callbacks and so on. Even module loading is actually a promise in javascript.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean the actual async runtime is usually outside of JS engines, e.g. they have a low level primitive that a hosting environment needs to implement. Specifically: HostEnqueuePromiseJob from the language spec https://www.ecma-international.org/wp-content/uploads/ECMA-262_12th_edition_june_2021.pdf

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a bit easier link: https://tc39.es/ecma262/#sec-hostenqueuepromisejob , I haven't really gone into this but basically custom hosts should be possible in Jint (we should make it so and improve when features are missing), there's already the Host concept and ideally Jint would have all expected behavior and allow adding missing more controversial bits.

{
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();
}
}
}
}