Skip to content

Commit 2798c0b

Browse files
author
Robert Jackson
committed
Replace purgeModule cache busting with vm based sandboxing
The template compiler contents have to be evaluated separately for each addon in the build pipeline. If they are **not** the AST plugins from one addon leak through to other addons (or the app). This issue led us to attempt to purge the normal node require cache (the `purgeModule` code). This works (and has been in use for quite a while) but causes a non-trivial amount of memory overhead since each of the addons' ends up with a separate template compiler. This prevents JIT'ing and it causes the source code of the template compiler itself to be in memory many many many times (non-trivially increasing memory pressure). Migrating to `vm.Script` and sandboxed contexts (similar to what is done in FastBoot) resolves both of those issues. The script itself is cached and not reevaluated each time (removing the memory pressure issues) and the JIT information of the script context is also shared. Thanks to @krisselden for pointing out this improvement! (cherry picked from commit 8d5dbcf)
1 parent 3eb40a4 commit 2798c0b

File tree

2 files changed

+44
-75
lines changed

2 files changed

+44
-75
lines changed

lib/utils.js

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ const hashForDep = require('hash-for-dep');
66
const debugGenerator = require('heimdalljs-logger');
77
const logger = debugGenerator('ember-cli-htmlbars:utils');
88
const addDependencyTracker = require('./addDependencyTracker');
9+
const vm = require('vm');
10+
11+
const TemplateCompilerCache = new Map();
912

1013
const INLINE_PRECOMPILE_MODULES = Object.freeze({
1114
'ember-cli-htmlbars': 'hbs',
@@ -79,18 +82,10 @@ function buildParalleizedBabelPlugin(pluginInfo, templateCompilerPath, isProduct
7982
function buildOptions(projectConfig, templateCompilerPath, pluginInfo) {
8083
let EmberENV = projectConfig.EmberENV || {};
8184

82-
purgeModule(templateCompilerPath);
83-
84-
// do a full clone of the EmberENV (it is guaranteed to be structured
85-
// cloneable) to prevent ember-template-compiler.js from mutating
86-
// the shared global config
87-
let clonedEmberENV = JSON.parse(JSON.stringify(EmberENV));
88-
global.EmberENV = clonedEmberENV; // Needed for eval time feature flag checks
89-
9085
let htmlbarsOptions = {
9186
isHTMLBars: true,
9287
EmberENV: EmberENV,
93-
templateCompiler: require(templateCompilerPath),
88+
templateCompiler: getTemplateCompiler(templateCompilerPath, EmberENV),
9489
templateCompilerPath: templateCompilerPath,
9590

9691
plugins: {
@@ -102,37 +97,47 @@ function buildOptions(projectConfig, templateCompilerPath, pluginInfo) {
10297
pluginCacheKey: pluginInfo.cacheKeys,
10398
};
10499

105-
purgeModule(templateCompilerPath);
106-
107-
delete global.Ember;
108-
delete global.EmberENV;
109-
110100
return htmlbarsOptions;
111101
}
112102

113-
function purgeModule(templateCompilerPath) {
114-
// ensure we get a fresh templateCompilerModuleInstance per ember-addon
115-
// instance NOTE: this is a quick hack, and will only work as long as
116-
// templateCompilerPath is a single file bundle
117-
//
118-
// (╯°□°)╯︵ ɹǝqɯǝ
119-
//
120-
// we will also fix this in ember for future releases
121-
122-
// Module will be cached in .parent.children as well. So deleting from require.cache alone is not sufficient.
123-
let mod = require.cache[templateCompilerPath];
124-
if (mod && mod.parent) {
125-
let index = mod.parent.children.indexOf(mod);
126-
if (index >= 0) {
127-
mod.parent.children.splice(index, 1);
128-
} else {
129-
throw new TypeError(
130-
`ember-cli-htmlbars attempted to purge '${templateCompilerPath}' but something went wrong.`
131-
);
132-
}
103+
function getTemplateCompiler(templateCompilerPath, EmberENV = {}) {
104+
let templateCompilerFullPath = require.resolve(templateCompilerPath);
105+
let cacheData = TemplateCompilerCache.get(templateCompilerFullPath);
106+
107+
if (cacheData === undefined) {
108+
let templateCompilerContents = fs.readFileSync(templateCompilerFullPath, { encoding: 'utf-8' });
109+
let templateCompilerCacheKey = crypto
110+
.createHash('md5')
111+
.update(templateCompilerContents)
112+
.digest('hex');
113+
114+
cacheData = {
115+
script: new vm.Script(templateCompilerContents, {
116+
filename: templateCompilerPath,
117+
}),
118+
119+
templateCompilerCacheKey,
120+
};
121+
122+
TemplateCompilerCache.set(templateCompilerFullPath, cacheData);
133123
}
134124

135-
delete require.cache[templateCompilerPath];
125+
let { script } = cacheData;
126+
127+
// do a full clone of the EmberENV (it is guaranteed to be structured
128+
// cloneable) to prevent ember-template-compiler.js from mutating
129+
// the shared global config
130+
let clonedEmberENV = JSON.parse(JSON.stringify(EmberENV));
131+
132+
let context = vm.createContext({
133+
EmberENV: clonedEmberENV,
134+
module: { require, exports: {} },
135+
require,
136+
});
137+
138+
script.runInContext(context);
139+
140+
return context.module.exports;
136141
}
137142

138143
function registerPlugins(templateCompiler, plugins) {
@@ -222,8 +227,10 @@ function setup(pluginInfo, options) {
222227

223228
function makeCacheKey(templateCompilerPath, pluginInfo, extra) {
224229
let templateCompilerFullPath = require.resolve(templateCompilerPath);
225-
let templateCompilerCacheKey = fs.readFileSync(templateCompilerFullPath, { encoding: 'utf-8' });
230+
let { templateCompilerCacheKey } = TemplateCompilerCache.get(templateCompilerFullPath);
231+
226232
let cacheItems = [templateCompilerCacheKey, extra].concat(pluginInfo.cacheKeys.sort());
233+
227234
// extra may be undefined
228235
return cacheItems.filter(Boolean).join('|');
229236
}
@@ -288,7 +295,6 @@ function setupPlugins(wrappers) {
288295

289296
module.exports = {
290297
buildOptions,
291-
purgeModule,
292298
registerPlugins,
293299
unregisterPlugins,
294300
initializeEmberENV,
@@ -299,4 +305,5 @@ module.exports = {
299305
isColocatedBabelPluginRegistered,
300306
isInlinePrecompileBabelPluginRegistered,
301307
buildParalleizedBabelPlugin,
308+
getTemplateCompiler,
302309
};

node-tests/purge-module-test.js

Lines changed: 0 additions & 38 deletions
This file was deleted.

0 commit comments

Comments
 (0)