Skip to content

Commit ff957fc

Browse files
committed
Experimental support for pooling/reusing JavaScript engines to improve performance. References #13
1 parent fa7ed58 commit ff957fc

11 files changed

+137
-78
lines changed

src/React.Sample.Mvc4/App_Start/ReactConfig.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public static void Configure()
1717
{
1818
ReactSiteConfiguration.Configuration
1919
.SetUseHarmony(true)
20+
.SetReuseJavaScriptEngines(true)
2021
.AddScript("~/Content/Sample.jsx");
2122
}
2223
}

src/React.Sample.Mvc4/App_Start/RouteConfig.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class RouteConfig
1717
public static void RegisterRoutes(RouteCollection routes)
1818
{
1919
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
20+
routes.IgnoreRoute("favicon.ico");
2021

2122
routes.MapRoute(
2223
name: "Comments",

src/React.Tests/Core/JavaScriptEngineFactoryTest.cs

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class JavaScriptEngineFactoryTest
1919
{
2020
private JavaScriptEngineFactory CreateFactory()
2121
{
22+
var config = new Mock<IReactSiteConfiguration>();
2223
var registration = new JavaScriptEngineFactory.Registration
2324
{
2425
Factory = () =>
@@ -29,34 +30,7 @@ private JavaScriptEngineFactory CreateFactory()
2930
},
3031
Priority = 1
3132
};
32-
return new JavaScriptEngineFactory(new[] { registration });
33-
}
34-
35-
[Test]
36-
public void ShouldCallOnNewEngineWhenCreatingNew()
37-
{
38-
var factory = CreateFactory();
39-
var called = false;
40-
factory.GetEngineForCurrentThread(engine =>
41-
{
42-
Assert.NotNull(engine);
43-
called = true;
44-
});
45-
factory.DisposeEngineForCurrentThread();
46-
47-
Assert.True(called);
48-
}
49-
50-
[Test]
51-
public void ShouldNotCallOnNewEngineWhenUsingExisting()
52-
{
53-
var factory = CreateFactory();
54-
var called = false;
55-
factory.GetEngineForCurrentThread();
56-
factory.GetEngineForCurrentThread(engine => { called = true; });
57-
factory.DisposeEngineForCurrentThread();
58-
59-
Assert.False(called);
33+
return new JavaScriptEngineFactory(new[] { registration }, config.Object);
6034
}
6135

6236
[Test]

src/React.Tests/Core/ReactEnvironmentTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public void ExecuteWithLargerStackIfRequiredWithNewThread()
4242

4343
environment.ExecuteWithLargerStackIfRequired<int>("foo");
4444
mocks.EngineFactory.Verify(
45-
x => x.GetEngineForCurrentThread(It.IsAny<Action<IJsEngine>>()),
45+
x => x.GetEngineForCurrentThread(),
4646
Times.Exactly(2),
4747
"Two engines should be created (initial thread and new thread)"
4848
);
@@ -111,7 +111,7 @@ public Mocks()
111111
FileSystem = new Mock<IFileSystem>();
112112
FileCacheHash = new Mock<IFileCacheHash>();
113113

114-
EngineFactory.Setup(x => x.GetEngineForCurrentThread(It.IsAny<Action<IJsEngine>>())).Returns(Engine.Object);
114+
EngineFactory.Setup(x => x.GetEngineForCurrentThread()).Returns(Engine.Object);
115115
}
116116

117117
public ReactEnvironment CreateReactEnvironment()
@@ -126,4 +126,4 @@ public ReactEnvironment CreateReactEnvironment()
126126
}
127127
}
128128
}
129-
}
129+
}

src/React/IJavaScriptEngineFactory.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,28 @@ namespace React
99
public interface IJavaScriptEngineFactory
1010
{
1111
/// <summary>
12-
/// Gets the JavaScript engine for the current thread
12+
/// Gets the JavaScript engine for the current thread. It is recommended to use
13+
/// <see cref="GetEngine"/> instead, which will pool/reuse engines.
1314
/// </summary>
14-
/// <param name="onNewEngine">
15-
/// Called if a brand new JavaScript engine is being created for this thread.
16-
/// Should handle initialisation.
17-
/// </param>
1815
/// <returns>The JavaScript engine</returns>
19-
IJsEngine GetEngineForCurrentThread(Action<IJsEngine> onNewEngine);
16+
IJsEngine GetEngineForCurrentThread();
2017

2118
/// <summary>
22-
/// Disposes the JavaScript engine for the current thread.
19+
/// Disposes the JavaScript engine for the current thread. This should only be used
20+
/// if the engine was acquired through <see cref="GetEngineForCurrentThread"/>.
2321
/// </summary>
2422
void DisposeEngineForCurrentThread();
23+
24+
/// <summary>
25+
/// Gets a JavaScript engine from the pool.
26+
/// </summary>
27+
/// <returns>The JavaScript engine</returns>
28+
IJsEngine GetEngine();
29+
30+
/// <summary>
31+
/// Returns an engine to the pool so it can be reused
32+
/// </summary>
33+
/// <param name="engine">Engine to return</param>
34+
void ReturnEngineToPool(IJsEngine engine);
2535
}
2636
}

src/React/IReactSiteConfiguration.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ public interface IReactSiteConfiguration
4444
/// </summary>
4545
IReactSiteConfiguration SetUseHarmony(bool useHarmony);
4646

47+
/// <summary>
48+
/// Gets or sets whether JavaScript engines should be reused across requests.
49+
/// </summary>
50+
///
51+
bool ReuseJavaScriptEngines { get; set; }
52+
/// <summary>
53+
/// Sets whether JavaScript engines should be reused across requests.
54+
/// </summary>
55+
IReactSiteConfiguration SetReuseJavaScriptEngines(bool value);
56+
4757
/// <summary>
4858
/// Gets or sets the configuration for JSON serializer.
4959
/// </summary>
@@ -53,7 +63,7 @@ public interface IReactSiteConfiguration
5363
/// Sets the configuration for json serializer.
5464
/// </summary>
5565
/// <remarks>
56-
/// Thic confiquration is used when component initialization script
66+
/// This confiquration is used when component initialization script
5767
/// is being generated server-side.
5868
/// </remarks>
5969
/// <param name="settings">The settings.</param>

src/React/JavaScriptEngineFactory.cs

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq;
66
using System.Threading;
77
using JavaScriptEngineSwitcher.Core;
8+
using JSPool;
89
using React.Exceptions;
910

1011
namespace React
@@ -14,6 +15,10 @@ namespace React
1415
/// </summary>
1516
public class JavaScriptEngineFactory : IDisposable, IJavaScriptEngineFactory
1617
{
18+
/// <summary>
19+
/// React configuration for the current site
20+
/// </summary>
21+
protected readonly IReactSiteConfiguration _config;
1722
/// <summary>
1823
/// Function used to create new JavaScript engine instances.
1924
/// </summary>
@@ -23,32 +28,56 @@ public class JavaScriptEngineFactory : IDisposable, IJavaScriptEngineFactory
2328
/// </summary>
2429
protected readonly ConcurrentDictionary<int, IJsEngine> _engines
2530
= new ConcurrentDictionary<int, IJsEngine>();
31+
/// <summary>
32+
/// Pool of JavaScript engines to use
33+
/// </summary>
34+
protected readonly IJsPool _pool;
2635

2736
/// <summary>
2837
/// Initializes a new instance of the <see cref="JavaScriptEngineFactory"/> class.
2938
/// </summary>
30-
public JavaScriptEngineFactory(IEnumerable<Registration> availableFactories)
39+
public JavaScriptEngineFactory(IEnumerable<Registration> availableFactories, IReactSiteConfiguration config)
3140
{
41+
_config = config;
3242
_factory = GetFactory(availableFactories);
43+
if (_config.ReuseJavaScriptEngines)
44+
{
45+
_pool = new JsPool(new JsPoolConfig
46+
{
47+
EngineFactory = _factory,
48+
Initializer = InitialiseEngine,
49+
});
50+
}
51+
}
52+
53+
/// <summary>
54+
/// Loads standard React and JSXTransformer scripts into the engine.
55+
/// </summary>
56+
protected virtual void InitialiseEngine(IJsEngine engine)
57+
{
58+
var thisAssembly = typeof(ReactEnvironment).Assembly;
59+
engine.ExecuteResource("React.Resources.shims.js", thisAssembly);
60+
engine.ExecuteResource("React.Resources.react-with-addons.js", thisAssembly);
61+
engine.Execute("var React = global.React");
62+
63+
// Only load JSX Transformer if engine supports it
64+
if (engine.SupportsJsxTransformer())
65+
{
66+
engine.ExecuteResource("React.Resources.JSXTransformer.js", thisAssembly);
67+
}
3368
}
3469

3570
/// <summary>
36-
/// Gets the JavaScript engine for the current thread
71+
/// Gets the JavaScript engine for the current thread. It is recommended to use
72+
/// <see cref="GetEngine"/> instead, which will pool/reuse engines.
3773
/// </summary>
38-
/// <param name="onNewEngine">
39-
/// Called if a brand new JavaScript engine is being created for this thread.
40-
/// Should handle initialisation.
41-
/// </param>
4274
/// <returns>The JavaScript engine</returns>
43-
public virtual IJsEngine GetEngineForCurrentThread(Action<IJsEngine> onNewEngine = null)
75+
public virtual IJsEngine GetEngineForCurrentThread()
4476
{
4577
return _engines.GetOrAdd(Thread.CurrentThread.ManagedThreadId, id =>
4678
{
4779
var engine = _factory();
48-
if (onNewEngine != null)
49-
{
50-
onNewEngine(engine);
51-
}
80+
InitialiseEngine(engine);
5281
return engine;
5382
});
5483
}
@@ -68,6 +97,24 @@ public virtual void DisposeEngineForCurrentThread()
6897
}
6998
}
7099

100+
/// <summary>
101+
/// Gets a JavaScript engine from the pool.
102+
/// </summary>
103+
/// <returns>The JavaScript engine</returns>
104+
public virtual IJsEngine GetEngine()
105+
{
106+
return _pool.GetEngine();
107+
}
108+
109+
/// <summary>
110+
/// Returns an engine to the pool so it can be reused
111+
/// </summary>
112+
/// <param name="engine">Engine to return</param>
113+
public virtual void ReturnEngineToPool(IJsEngine engine)
114+
{
115+
_pool.ReturnEngineToPool(engine);
116+
}
117+
71118
/// <summary>
72119
/// Gets a factory for the most appropriate JavaScript engine for the current environment.
73120
/// The first functioning JavaScript engine with the lowest priority will be used.

src/React/React.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@
6262
<HintPath>..\packages\JavaScriptEngineSwitcher.Jint.1.2.1\lib\net40\Jint.dll</HintPath>
6363
<Private>True</Private>
6464
</Reference>
65+
<Reference Include="JSPool, Version=0.1.0.0, Culture=neutral, PublicKeyToken=2fc7775f73072640, processorArchitecture=MSIL">
66+
<SpecificVersion>False</SpecificVersion>
67+
<HintPath>..\packages\JSPool.0.1.1\lib\net40-Client\JSPool.dll</HintPath>
68+
</Reference>
6569
<Reference Include="MsieJavaScriptEngine">
6670
<HintPath>..\packages\MsieJavaScriptEngine.1.5.0\lib\net40\MsieJavaScriptEngine.dll</HintPath>
6771
<Private>True</Private>

src/React/ReactEnvironment.cs

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,12 @@ public class ReactEnvironment : IReactEnvironment, IDisposable
6868
/// <summary>
6969
/// Version number of ReactJS.NET
7070
/// </summary>
71-
protected readonly Lazy<string> _version = new Lazy<string>(GetVersion);
71+
protected readonly Lazy<string> _version = new Lazy<string>(GetVersion);
72+
/// <summary>
73+
/// Contains an engine acquired from a pool of engines. Only used if
74+
/// <see cref="IReactSiteConfiguration.ReuseJavaScriptEngines"/> is enabled.
75+
/// </summary>
76+
protected readonly Lazy<IJsEngine> _engineFromPool;
7277

7378
/// <summary>
7479
/// Number of components instantiated in this environment
@@ -103,17 +108,19 @@ IFileCacheHash fileCacheHash
103108
_jsxTransformer = new Lazy<IJsxTransformer>(() =>
104109
new JsxTransformer(this, _cache, _fileSystem, _fileCacheHash, _config)
105110
);
111+
_engineFromPool = new Lazy<IJsEngine>(() => _engineFactory.GetEngine());
106112
}
107113

108114
/// <summary>
109-
/// Gets the JavaScript engine for the current thread. If an engine has not yet been
110-
/// created, create it and execute the startup scripts.
115+
/// Gets the JavaScript engine to use for this environment.
111116
/// </summary>
112117
protected virtual IJsEngine Engine
113118
{
114119
get
115120
{
116-
return _engineFactory.GetEngineForCurrentThread(InitialiseEngine);
121+
return _config.ReuseJavaScriptEngines
122+
? _engineFromPool.Value
123+
: _engineFactory.GetEngineForCurrentThread();
117124
}
118125
}
119126

@@ -149,23 +156,6 @@ public virtual string Version
149156
get { return _version.Value; }
150157
}
151158

152-
/// <summary>
153-
/// Loads standard React and JSXTransformer scripts into the engine.
154-
/// </summary>
155-
protected virtual void InitialiseEngine(IJsEngine engine)
156-
{
157-
var thisAssembly = typeof(ReactEnvironment).Assembly;
158-
engine.ExecuteResource("React.Resources.shims.js", thisAssembly);
159-
engine.ExecuteResource("React.Resources.react-with-addons.js", thisAssembly);
160-
engine.Execute("var React = global.React");
161-
162-
// Only load JSX Transformer if engine supports it
163-
if (engine.SupportsJsxTransformer())
164-
{
165-
engine.ExecuteResource("React.Resources.JSXTransformer.js", thisAssembly);
166-
}
167-
}
168-
169159
/// <summary>
170160
/// Ensures any user-provided scripts have been loaded
171161
/// </summary>
@@ -362,6 +352,13 @@ public string TransformJsx(string input)
362352
/// <returns>Result returned from JavaScript code</returns>
363353
public virtual T ExecuteWithLargerStackIfRequired<T>(string function, params object[] args)
364354
{
355+
// This hack is not required when pooling JavaScript engines, since pooled MSIE
356+
// engines already execute on their own thread with a larger stack.
357+
if (_config.ReuseJavaScriptEngines)
358+
{
359+
return Execute<T>(function, args);
360+
}
361+
365362
try
366363
{
367364
return Execute<T>(function, args);
@@ -375,10 +372,11 @@ public virtual T ExecuteWithLargerStackIfRequired<T>(string function, params obj
375372
Exception innerEx = null;
376373
var thread = new Thread(() =>
377374
{
375+
// New engine will be created here (as this is a new thread)
376+
var engine = _engineFactory.GetEngineForCurrentThread();
378377
try
379378
{
380-
// New engine will be created here (as this is a new thread)
381-
result = Execute<T>(function, args);
379+
result = engine.CallFunction<T>(function, args);
382380
}
383381
catch (Exception threadEx)
384382
{
@@ -421,6 +419,10 @@ private static string GetVersion()
421419
public virtual void Dispose()
422420
{
423421
_engineFactory.DisposeEngineForCurrentThread();
422+
if (_engineFromPool.IsValueCreated)
423+
{
424+
_engineFactory.ReturnEngineToPool(_engineFromPool.Value);
425+
}
424426
}
425427

426428
/// <summary>

0 commit comments

Comments
 (0)