Skip to content

Commit 5536c6e

Browse files
Robert Jacksonrwjblue
authored andcommitted
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!
1 parent 4c9b338 commit 5536c6e

File tree

2 files changed

+44
-78
lines changed

2 files changed

+44
-78
lines changed

lib/utils.js

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

1114
const INLINE_PRECOMPILE_MODULES = Object.freeze({
1215
'ember-cli-htmlbars': 'hbs',
@@ -92,18 +95,10 @@ function buildParalleizedBabelPlugin(
9295
function buildOptions(projectConfig, templateCompilerPath, pluginInfo) {
9396
let EmberENV = projectConfig.EmberENV || {};
9497

95-
purgeModule(templateCompilerPath);
96-
97-
// do a full clone of the EmberENV (it is guaranteed to be structured
98-
// cloneable) to prevent ember-template-compiler.js from mutating
99-
// the shared global config
100-
let clonedEmberENV = JSON.parse(JSON.stringify(EmberENV));
101-
global.EmberENV = clonedEmberENV; // Needed for eval time feature flag checks
102-
10398
let htmlbarsOptions = {
10499
isHTMLBars: true,
105100
EmberENV: EmberENV,
106-
templateCompiler: require(templateCompilerPath),
101+
templateCompiler: getTemplateCompiler(templateCompilerPath, EmberENV),
107102
templateCompilerPath: templateCompilerPath,
108103

109104
pluginNames: pluginInfo.pluginNames,
@@ -116,37 +111,47 @@ function buildOptions(projectConfig, templateCompilerPath, pluginInfo) {
116111
pluginCacheKey: pluginInfo.cacheKeys,
117112
};
118113

119-
purgeModule(templateCompilerPath);
120-
121-
delete global.Ember;
122-
delete global.EmberENV;
123-
124114
return htmlbarsOptions;
125115
}
126116

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

149-
delete require.cache[templateCompilerPath];
139+
let { script } = cacheData;
140+
141+
// do a full clone of the EmberENV (it is guaranteed to be structured
142+
// cloneable) to prevent ember-template-compiler.js from mutating
143+
// the shared global config
144+
let clonedEmberENV = JSON.parse(JSON.stringify(EmberENV));
145+
146+
let context = vm.createContext({
147+
EmberENV: clonedEmberENV,
148+
module: { require, exports: {} },
149+
require,
150+
});
151+
152+
script.runInContext(context);
153+
154+
return context.module.exports;
150155
}
151156

152157
function registerPlugins(templateCompiler, plugins) {
@@ -259,11 +264,10 @@ function setup(pluginInfo, options) {
259264

260265
function makeCacheKey(templateCompilerPath, pluginInfo, extra) {
261266
let templateCompilerFullPath = require.resolve(templateCompilerPath);
262-
let templateCompilerCacheKey = crypto
263-
.createHash('md5')
264-
.update(fs.readFileSync(templateCompilerFullPath, { encoding: 'utf-8' }))
265-
.digest('hex');
267+
let { templateCompilerCacheKey } = TemplateCompilerCache.get(templateCompilerFullPath);
268+
266269
let cacheItems = [templateCompilerCacheKey, extra].concat(pluginInfo.cacheKeys.sort());
270+
267271
// extra may be undefined
268272
return cacheItems.filter(Boolean).join('|');
269273
}
@@ -332,7 +336,6 @@ function setupPlugins(wrappers) {
332336

333337
module.exports = {
334338
buildOptions,
335-
purgeModule,
336339
registerPlugins,
337340
unregisterPlugins,
338341
initializeEmberENV,
@@ -343,4 +346,5 @@ module.exports = {
343346
isColocatedBabelPluginRegistered,
344347
isInlinePrecompileBabelPluginRegistered,
345348
buildParalleizedBabelPlugin,
349+
getTemplateCompiler,
346350
};

node-tests/purge-module-test.js

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

0 commit comments

Comments
 (0)