Skip to content

Commit 4ff0fad

Browse files
committed
Add ability to pre-compile scripts
1 parent 26ff2dd commit 4ff0fad

24 files changed

+606
-98
lines changed

src/React.Core/IReactSiteConfiguration.cs

+12
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,18 @@ public interface IReactSiteConfiguration
113113
/// </summary>
114114
IReactSiteConfiguration SetMaxUsagesPerEngine(int? maxUsagesPerEngine);
115115

116+
/// <summary>
117+
/// Gets or sets whether to allow the JavaScript pre-compilation (accelerates the
118+
/// initialization of JavaScript engines).
119+
/// </summary>
120+
bool AllowJavaScriptPrecompilation { get; set; }
121+
/// <summary>
122+
/// Sets whether to allow the JavaScript pre-compilation (accelerates the initialization of
123+
/// JavaScript engines).
124+
/// </summary>
125+
/// <returns></returns>
126+
IReactSiteConfiguration SetAllowJavaScriptPrecompilation(bool allowJavaScriptPrecompilation);
127+
116128
/// <summary>
117129
/// Gets or sets whether the built-in version of React is loaded. If <c>false</c>, you must
118130
/// provide your own version of React.

src/React.Core/JavaScriptEngineFactory.cs

+38-5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public class JavaScriptEngineFactory : IDisposable, IJavaScriptEngineFactory
2222
/// </summary>
2323
protected readonly IReactSiteConfiguration _config;
2424
/// <summary>
25+
/// Cache used for storing the pre-compiled scripts
26+
/// </summary>
27+
protected readonly ICache _cache;
28+
/// <summary>
2529
/// File system wrapper
2630
/// </summary>
2731
protected readonly IFileSystem _fileSystem;
@@ -57,11 +61,13 @@ protected readonly ConcurrentDictionary<int, IJsEngine> _engines
5761
public JavaScriptEngineFactory(
5862
IJsEngineSwitcher jsEngineSwitcher,
5963
IReactSiteConfiguration config,
64+
ICache cache,
6065
IFileSystem fileSystem
6166
)
6267
{
6368
_jsEngineSwitcher = jsEngineSwitcher;
6469
_config = config;
70+
_cache = cache;
6571
_fileSystem = fileSystem;
6672
#pragma warning disable 618
6773
_factory = GetFactory(_jsEngineSwitcher);
@@ -118,10 +124,11 @@ protected virtual void InitialiseEngine(IJsEngine engine)
118124
#else
119125
var thisAssembly = typeof(ReactEnvironment).GetTypeInfo().Assembly;
120126
#endif
121-
engine.ExecuteResource("React.Core.Resources.shims.js", thisAssembly);
127+
LoadResource(engine, "React.Core.Resources.shims.js", thisAssembly);
122128
if (_config.LoadReact)
123129
{
124-
engine.ExecuteResource(
130+
LoadResource(
131+
engine,
125132
_config.UseDebugReact
126133
? "React.Core.Resources.react.generated.js"
127134
: "React.Core.Resources.react.generated.min.js",
@@ -138,6 +145,25 @@ protected virtual void InitialiseEngine(IJsEngine engine)
138145
}
139146
}
140147

148+
/// <summary>
149+
/// Loads a code from embedded JavaScript resource into the engine.
150+
/// </summary>
151+
/// <param name="engine">Engine to load a code from embedded JavaScript resource</param>
152+
/// <param name="resourceName">The case-sensitive resource name</param>
153+
/// <param name="assembly">The assembly, which contains the embedded resource</param>
154+
private void LoadResource(IJsEngine engine, string resourceName, Assembly assembly)
155+
{
156+
if (_config.AllowJavaScriptPrecompilation
157+
&& engine.TryExecuteResourceWithPrecompilation(_cache, resourceName, assembly))
158+
{
159+
// Do nothing.
160+
}
161+
else
162+
{
163+
engine.ExecuteResource(resourceName, assembly);
164+
}
165+
}
166+
141167
/// <summary>
142168
/// Loads any user-provided scripts. Only scripts that don't need JSX transformation can
143169
/// run immediately here. JSX files are loaded in ReactEnvironment.
@@ -149,10 +175,17 @@ private void LoadUserScripts(IJsEngine engine)
149175
{
150176
try
151177
{
152-
var contents = _fileSystem.ReadAsString(file);
153-
engine.Execute(contents);
178+
if (_config.AllowJavaScriptPrecompilation
179+
&& engine.TryExecuteFileWithPrecompilation(_cache, _fileSystem, file))
180+
{
181+
// Do nothing.
182+
}
183+
else
184+
{
185+
engine.ExecuteFile(_fileSystem, file);
186+
}
154187
}
155-
catch (JsRuntimeException ex)
188+
catch (JsScriptException ex)
156189
{
157190
// We can't simply rethrow the exception here, as it's possible this is running
158191
// on a background thread (ie. as a response to a file changing). If we did

src/React.Core/JavaScriptEngineUtils.cs

+161
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*/
99

1010
using System;
11+
using System.Diagnostics;
12+
using System.Reflection;
1113
using JavaScriptEngineSwitcher.Core;
1214
using JavaScriptEngineSwitcher.Core.Helpers;
1315
using Newtonsoft.Json;
@@ -20,6 +22,20 @@ namespace React
2022
/// </summary>
2123
public static class JavaScriptEngineUtils
2224
{
25+
/// <summary>
26+
/// Cache key for the script resource precompilation
27+
/// </summary>
28+
private const string PRECOMPILED_JS_RESOURCE_CACHE_KEY = "PRECOMPILED_JS_RESOURCE_{0}";
29+
/// <summary>
30+
/// Cache key for the script file precompilation
31+
/// </summary>
32+
private const string PRECOMPILED_JS_FILE_CACHE_KEY = "PRECOMPILED_JS_FILE_{0}";
33+
/// <summary>
34+
/// Value that indicates whether a cache entry, that contains a precompiled script, should be
35+
/// evicted if it has not been accessed in a given span of time
36+
/// </summary>
37+
private readonly static TimeSpan PRECOMPILED_JS_CACHE_ENTRY_SLIDING_EXPIRATION = TimeSpan.FromMinutes(30);
38+
2339
/// <summary>
2440
/// Determines if the current environment supports the ClearScript V8 engine
2541
/// </summary>
@@ -61,6 +77,151 @@ Func<Exception, TException> exceptionFactory
6177
}
6278
}
6379

80+
/// <summary>
81+
/// Executes a code from JavaScript file.
82+
/// </summary>
83+
/// <param name="engine">Engine to execute code from JavaScript file</param>
84+
/// <param name="fileSystem">File system wrapper</param>
85+
/// <param name="path">Path to the JavaScript file</param>
86+
public static void ExecuteFile(this IJsEngine engine, IFileSystem fileSystem, string path)
87+
{
88+
var contents = fileSystem.ReadAsString(path);
89+
engine.Execute(contents, path);
90+
}
91+
92+
/// <summary>
93+
/// Tries to execute a code with pre-compilation.
94+
/// </summary>
95+
/// <param name="engine">Engine to execute code with pre-compilation</param>
96+
/// <param name="cache">Cache used for storing the pre-compiled scripts</param>
97+
/// <param name="fileSystem">File system wrapper</param>
98+
/// <param name="code">JavaScript code</param>
99+
/// <param name="path">Path to the file on which the executable code depends (required
100+
/// for cache management).</param>
101+
/// <returns>true if can perform a script pre-compilation; otherwise, false.</returns>
102+
public static bool TryExecuteWithPrecompilation(this IJsEngine engine, ICache cache, IFileSystem fileSystem,
103+
string code, string path)
104+
{
105+
if (!CheckPrecompilationAvailability(engine, cache))
106+
{
107+
return false;
108+
}
109+
110+
var cacheKey = string.Format(PRECOMPILED_JS_FILE_CACHE_KEY, path);
111+
var precompiledScript = cache.Get<IPrecompiledScript>(cacheKey);
112+
113+
if (precompiledScript == null)
114+
{
115+
precompiledScript = engine.Precompile(code, path);
116+
var fullPath = fileSystem.MapPath(path);
117+
cache.Set(
118+
cacheKey,
119+
precompiledScript,
120+
slidingExpiration: PRECOMPILED_JS_CACHE_ENTRY_SLIDING_EXPIRATION,
121+
cacheDependencyFiles: new[] { fullPath }
122+
);
123+
}
124+
125+
engine.Execute(precompiledScript);
126+
127+
return true;
128+
}
129+
130+
/// <summary>
131+
/// Tries to execute a code from JavaScript file with pre-compilation.
132+
/// </summary>
133+
/// <param name="engine">Engine to execute code from JavaScript file with pre-compilation</param>
134+
/// <param name="cache">Cache used for storing the pre-compiled scripts</param>
135+
/// <param name="fileSystem">File system wrapper</param>
136+
/// <param name="path">Path to the JavaScript file</param>
137+
/// <returns>true if can perform a script pre-compilation; otherwise, false.</returns>
138+
public static bool TryExecuteFileWithPrecompilation(this IJsEngine engine, ICache cache,
139+
IFileSystem fileSystem, string path)
140+
{
141+
if (!CheckPrecompilationAvailability(engine, cache))
142+
{
143+
return false;
144+
}
145+
146+
var cacheKey = string.Format(PRECOMPILED_JS_FILE_CACHE_KEY, path);
147+
var precompiledScript = cache.Get<IPrecompiledScript>(cacheKey);
148+
149+
if (precompiledScript == null)
150+
{
151+
var contents = fileSystem.ReadAsString(path);
152+
precompiledScript = engine.Precompile(contents, path);
153+
var fullPath = fileSystem.MapPath(path);
154+
cache.Set(
155+
cacheKey,
156+
precompiledScript,
157+
slidingExpiration: PRECOMPILED_JS_CACHE_ENTRY_SLIDING_EXPIRATION,
158+
cacheDependencyFiles: new[] { fullPath }
159+
);
160+
}
161+
162+
engine.Execute(precompiledScript);
163+
164+
return true;
165+
}
166+
167+
/// <summary>
168+
/// Tries to execute a code from embedded JavaScript resource with pre-compilation.
169+
/// </summary>
170+
/// <param name="engine">Engine to execute a code from embedded JavaScript resource with pre-compilation</param>
171+
/// <param name="cache">Cache used for storing the pre-compiled scripts</param>
172+
/// <param name="resourceName">The case-sensitive resource name</param>
173+
/// <param name="assembly">The assembly, which contains the embedded resource</param>
174+
/// <returns>true if can perform a script pre-compilation; otherwise, false.</returns>
175+
public static bool TryExecuteResourceWithPrecompilation(this IJsEngine engine, ICache cache,
176+
string resourceName, Assembly assembly)
177+
{
178+
if (!CheckPrecompilationAvailability(engine, cache))
179+
{
180+
return false;
181+
}
182+
183+
var cacheKey = string.Format(PRECOMPILED_JS_RESOURCE_CACHE_KEY, resourceName);
184+
var precompiledScript = cache.Get<IPrecompiledScript>(cacheKey);
185+
186+
if (precompiledScript == null)
187+
{
188+
precompiledScript = engine.PrecompileResource(resourceName, assembly);
189+
cache.Set(
190+
cacheKey,
191+
precompiledScript,
192+
slidingExpiration: PRECOMPILED_JS_CACHE_ENTRY_SLIDING_EXPIRATION
193+
);
194+
}
195+
196+
engine.Execute(precompiledScript);
197+
198+
return true;
199+
}
200+
201+
/// <summary>
202+
/// Checks a availability of the script pre-compilation
203+
/// </summary>
204+
/// <param name="engine">Instance of the JavaScript engine</param>
205+
/// <param name="cache">Cache used for storing the pre-compiled scripts</param>
206+
/// <returns>true if the script pre-compilation is available; otherwise, false.</returns>
207+
private static bool CheckPrecompilationAvailability(IJsEngine engine, ICache cache)
208+
{
209+
if (!engine.SupportsScriptPrecompilation)
210+
{
211+
Trace.WriteLine(string.Format("The {0} version {1} does not support the script pre-compilation.",
212+
engine.Name, engine.Version));
213+
return false;
214+
}
215+
216+
if (cache is NullCache)
217+
{
218+
Trace.WriteLine("Usage of script pre-compilation without caching does not make sense.");
219+
return false;
220+
}
221+
222+
return true;
223+
}
224+
64225
/// <summary>
65226
/// Calls a JavaScript function using the specified engine. If <typeparamref name="T"/> is
66227
/// not a scalar type, the function is assumed to return a string of JSON that can be

src/React.Core/React.Core.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
</ItemGroup>
3333

3434
<ItemGroup>
35-
<PackageReference Include="JavaScriptEngineSwitcher.Core" Version="3.0.0-beta4" />
35+
<PackageReference Include="JavaScriptEngineSwitcher.Core" Version="3.0.0-beta9" />
3636
<PackageReference Include="JSPool" Version="4.0.0-beta1" />
3737
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
3838
</ItemGroup>

src/React.Core/ReactEnvironment.cs

+21-3
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,17 @@ protected virtual void EnsureUserScriptsLoaded()
201201
var contents = Babel.TransformFile(file);
202202
try
203203
{
204-
Execute(contents);
204+
if (_config.AllowJavaScriptPrecompilation
205+
&& Engine.TryExecuteWithPrecompilation(_cache, _fileSystem, contents, file))
206+
{
207+
// Do nothing.
208+
}
209+
else
210+
{
211+
Engine.Execute(contents, file);
212+
}
205213
}
206-
catch (JsRuntimeException ex)
214+
catch (JsScriptException ex)
207215
{
208216
throw new ReactScriptLoadException(string.Format(
209217
"Error while loading \"{0}\": {1}",
@@ -474,7 +482,17 @@ private void EnsureBabelLoaded(IJsEngine engine)
474482
#else
475483
var assembly = typeof(ReactEnvironment).GetTypeInfo().Assembly;
476484
#endif
477-
engine.ExecuteResource("React.Core.Resources.babel.generated.min.js", assembly);
485+
const string resourceName = "React.Core.Resources.babel.generated.min.js";
486+
487+
if (_config.AllowJavaScriptPrecompilation
488+
&& engine.TryExecuteResourceWithPrecompilation(_cache, resourceName, assembly))
489+
{
490+
// Do nothing.
491+
}
492+
else
493+
{
494+
engine.ExecuteResource(resourceName, assembly);
495+
}
478496
}
479497
}
480498
}

src/React.Core/ReactSiteConfiguration.cs

+8-6
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public ReactSiteConfiguration()
3737
{
3838
BabelConfig = new BabelConfig();
3939
ReuseJavaScriptEngines = true;
40-
AllowMsieEngine = true;
40+
AllowJavaScriptPrecompilation = false;
4141
LoadBabel = true;
4242
LoadReact = true;
4343
JsonSerializerSettings = new JsonSerializerSettings
@@ -216,17 +216,19 @@ public IReactSiteConfiguration SetMaxUsagesPerEngine(int? maxUsagesPerEngine)
216216
}
217217

218218
/// <summary>
219-
/// Gets or sets whether the MSIE engine should be used if V8 is unavailable.
219+
/// Gets or sets whether to allow the JavaScript pre-compilation (accelerates the
220+
/// initialization of JavaScript engines).
220221
/// </summary>
221-
public bool AllowMsieEngine { get; set; }
222+
public bool AllowJavaScriptPrecompilation { get; set; }
222223

223224
/// <summary>
224-
/// Sets whether the MSIE engine should be used if V8 is unavailable.
225+
/// Sets whether to allow the JavaScript pre-compilation (accelerates the initialization of
226+
/// JavaScript engines).
225227
/// </summary>
226228
/// <returns></returns>
227-
public IReactSiteConfiguration SetAllowMsieEngine(bool allowMsieEngine)
229+
public IReactSiteConfiguration SetAllowJavaScriptPrecompilation(bool allowJavaScriptPrecompilation)
228230
{
229-
AllowMsieEngine = allowMsieEngine;
231+
AllowJavaScriptPrecompilation = allowJavaScriptPrecompilation;
230232
return this;
231233
}
232234

src/React.Sample.Cassette/React.Sample.Cassette.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,14 @@
6969
<Private>True</Private>
7070
</Reference>
7171
<Reference Include="JavaScriptEngineSwitcher.Core, Version=3.0.0.0, Culture=neutral, PublicKeyToken=c608b2a8cc9e4472, processorArchitecture=MSIL">
72-
<HintPath>..\packages\JavaScriptEngineSwitcher.Core.3.0.0-beta4\lib\net40-client\JavaScriptEngineSwitcher.Core.dll</HintPath>
72+
<HintPath>..\packages\JavaScriptEngineSwitcher.Core.3.0.0-beta9\lib\net40-client\JavaScriptEngineSwitcher.Core.dll</HintPath>
7373
</Reference>
7474
<Reference Include="JavaScriptEngineSwitcher.Msie, Version=3.0.0.0, Culture=neutral, PublicKeyToken=c608b2a8cc9e4472, processorArchitecture=MSIL">
7575
<HintPath>..\packages\JavaScriptEngineSwitcher.Msie.3.0.0-beta5\lib\net40-client\JavaScriptEngineSwitcher.Msie.dll</HintPath>
7676
</Reference>
7777
<Reference Include="Microsoft.CSharp" />
7878
<Reference Include="MsieJavaScriptEngine, Version=3.0.0.0, Culture=neutral, PublicKeyToken=a3a2846a37ac0d3e, processorArchitecture=MSIL">
79-
<HintPath>..\packages\MsieJavaScriptEngine.3.0.0-beta4\lib\net40-client\MsieJavaScriptEngine.dll</HintPath>
79+
<HintPath>..\packages\MsieJavaScriptEngine.3.0.0-beta5\lib\net40-client\MsieJavaScriptEngine.dll</HintPath>
8080
</Reference>
8181
<Reference Include="System" />
8282
<Reference Include="System.Data" />

0 commit comments

Comments
 (0)