Skip to content

Commit

Permalink
Replace purgeModule cache busting with vm based sandboxing
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
rwjblue committed Feb 25, 2021
1 parent 4c9b338 commit 70225e8
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 74 deletions.
64 changes: 28 additions & 36 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const hashForDep = require('hash-for-dep');
const debugGenerator = require('heimdalljs-logger');
const logger = debugGenerator('ember-cli-htmlbars:utils');
const addDependencyTracker = require('./addDependencyTracker');
const vm = require('vm');

const TemplateCompilerScripts = new Map();

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

purgeModule(templateCompilerPath);

// do a full clone of the EmberENV (it is guaranteed to be structured
// cloneable) to prevent ember-template-compiler.js from mutating
// the shared global config
let clonedEmberENV = JSON.parse(JSON.stringify(EmberENV));
global.EmberENV = clonedEmberENV; // Needed for eval time feature flag checks

let htmlbarsOptions = {
isHTMLBars: true,
EmberENV: EmberENV,
templateCompiler: require(templateCompilerPath),
templateCompiler: getTemplateCompiler(templateCompilerPath, EmberENV),
templateCompilerPath: templateCompilerPath,

pluginNames: pluginInfo.pluginNames,
Expand All @@ -116,37 +111,35 @@ function buildOptions(projectConfig, templateCompilerPath, pluginInfo) {
pluginCacheKey: pluginInfo.cacheKeys,
};

purgeModule(templateCompilerPath);

delete global.Ember;
delete global.EmberENV;

return htmlbarsOptions;
}

function purgeModule(templateCompilerPath) {
// ensure we get a fresh templateCompilerModuleInstance per ember-addon
// instance NOTE: this is a quick hack, and will only work as long as
// templateCompilerPath is a single file bundle
//
// (╯°□°)╯︵ ɹǝqɯǝ
//
// we will also fix this in ember for future releases

// Module will be cached in .parent.children as well. So deleting from require.cache alone is not sufficient.
let mod = require.cache[templateCompilerPath];
if (mod && mod.parent) {
let index = mod.parent.children.indexOf(mod);
if (index >= 0) {
mod.parent.children.splice(index, 1);
} else {
throw new TypeError(
`ember-cli-htmlbars attempted to purge '${templateCompilerPath}' but something went wrong.`
);
}
function getTemplateCompiler(templateCompilerPath, EmberENV) {
let script = TemplateCompilerScripts.get(templateCompilerPath);

if (script === undefined) {
script = new vm.Script(fs.readFileSync(templateCompilerPath, { encoding: 'utf-8' }), {
filename: templateCompilerPath,
});

TemplateCompilerScripts.set(templateCompilerPath, script);
}

delete require.cache[templateCompilerPath];
// do a full clone of the EmberENV (it is guaranteed to be structured
// cloneable) to prevent ember-template-compiler.js from mutating
// the shared global config
let clonedEmberENV = JSON.parse(JSON.stringify(EmberENV));
global.EmberENV = clonedEmberENV; // Needed for eval time feature flag checks

let context = vm.createContext({
EmberENV: clonedEmberENV,
module: { require, exports: {} },
require,
});

script.runInContext(context);

return context.module.exports;
}

function registerPlugins(templateCompiler, plugins) {
Expand Down Expand Up @@ -332,7 +325,6 @@ function setupPlugins(wrappers) {

module.exports = {
buildOptions,
purgeModule,
registerPlugins,
unregisterPlugins,
initializeEmberENV,
Expand Down
38 changes: 0 additions & 38 deletions node-tests/purge-module-test.js

This file was deleted.

0 comments on commit 70225e8

Please sign in to comment.