Skip to content

Commit

Permalink
Merge pull request #1529 from slightlyoff/njk-cache
Browse files Browse the repository at this point in the history
Cache intermediate NJK template compiles
  • Loading branch information
zachleat authored Feb 6, 2021
2 parents 24a13f8 + 8a47a07 commit 0cf3fd1
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 22 deletions.
3 changes: 2 additions & 1 deletion src/Eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const deleteRequireCache = require("./Util/DeleteRequireCache");
const config = require("./Config");
const bench = require("./BenchmarkManager");
const debug = require("debug")("Eleventy");
const eventBus = require("./EventBus");

/**
* @module 11ty/eleventy/Eleventy
Expand Down Expand Up @@ -449,7 +450,7 @@ Arguments:
* @param {String} changedFilePath - File that triggered a re-run (added or modified)
*/
async _addFileToWatchQueue(changedFilePath) {
TemplateContent.deleteCached(changedFilePath);
eventBus.emit("resourceModified", changedFilePath);
this.watchManager.addToPendingQueue(changedFilePath);
}

Expand Down
127 changes: 116 additions & 11 deletions src/Engines/Nunjucks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,111 @@ const TemplateEngine = require("./TemplateEngine");
const TemplatePath = require("../TemplatePath");
const EleventyErrorUtil = require("../EleventyErrorUtil");
const EleventyBaseError = require("../EleventyBaseError");
const eventBus = require("../EventBus");

/*
* The IFFE below apply a monkey-patch to Nunjucks internals to cache
* compiled templates and re-use them where possible.
*/
(function () {
let templateCache = new Map();

let getKey = (obj) => {
return [
obj.path || obj.tmplStr,
obj.tmplStr.length,
obj.env.asyncFilters.length,
obj.env.extensionsList
.map((e) => {
return e.__id || "";
})
.join(":"),
].join(" :: ");
};

let evictByPath = (path) => {
let keys = templateCache.keys();
// Likely to be slow; do we care?
for (let k of keys) {
if (k.indexOf(path) >= 0) {
templateCache.delete(k);
}
}
};
eventBus.on("resourceModified", evictByPath);

let _compile = NunjucksLib.Template.prototype._compile;
NunjucksLib.Template.prototype._compile = function _wrap_compile(...args) {
if (!this.compiled && !this.tmplProps && templateCache.has(getKey(this))) {
let pathProps = templateCache.get(getKey(this));
this.blocks = pathProps.blocks;
this.rootRenderFunc = pathProps.rootRenderFunc;
this.compiled = true;
} else {
_compile.call(this, ...args);
templateCache.set(getKey(this), {
blocks: this.blocks,
rootRenderFunc: this.rootRenderFunc,
});
}
};

let extensionIdCounter = 0;
let addExtension = NunjucksLib.Environment.prototype.addExtension;
NunjucksLib.Environment.prototype.addExtension = function _wrap_addExtension(
name,
ext
) {
if (!("__id" in ext)) {
ext.__id = extensionIdCounter++;
}
return addExtension.call(this, name, ext);
};

// NunjucksLib.runtime.Frame.prototype.set is the hotest in-template method.
// We replace it with a version that doesn't allocate a `parts` array on
// repeat key use.
let partsCache = new Map();
let partsFromCache = (name) => {
if (partsCache.has(name)) {
return partsCache.get(name);
}

let parts = name.split(".");
partsCache.set(name, parts);
return parts;
};

let frameSet = NunjucksLib.runtime.Frame.prototype.set;
NunjucksLib.runtime.Frame.prototype.set = function _replacement_set(
name,
val,
resolveUp
) {
let parts = partsFromCache(name);
let frame = this;
let obj = frame.variables;

if (resolveUp) {
if ((frame = this.resolve(parts[0], true))) {
frame.set(name, val);
return;
}
}

// A slightly faster version of the intermediate object allocation loop
let count = parts.length - 1;
let i = 0;
let id = parts[0];
while (i < count) {
if (!obj.hasOwnProperty(id)) {
obj = obj[id] = {};
}
id = parts[++i];
}
obj[id] = val;
};
})();

class EleventyShortcodeError extends EleventyBaseError {}

Expand All @@ -16,16 +121,17 @@ class Nunjucks extends TemplateEngine {
}

setLibrary(env) {
this.njkEnv =
env ||
new NunjucksLib.Environment(
new NunjucksLib.FileSystemLoader(
[super.getIncludesDir(), TemplatePath.getWorkingDir()],
{
noCache: true,
}
)
);
let fsLoader = new NunjucksLib.FileSystemLoader([
super.getIncludesDir(),
TemplatePath.getWorkingDir(),
]);
this.njkEnv = env || new NunjucksLib.Environment(fsLoader);
// Correct, but overbroad. Better would be to evict more granularly, but
// resolution from paths isn't straightforward.
eventBus.on("resourceModified", (path) => {
this.njkEnv.invalidateCache();
});

this.setEngineLib(this.njkEnv);

this.addFilters(this.config.nunjucksFilters);
Expand Down Expand Up @@ -148,7 +254,6 @@ class Nunjucks extends TemplateEngine {
});
} else {
try {
// console.log( shortcodeFn.toString() );
return new NunjucksLib.runtime.SafeString(
shortcodeFn.call(
Nunjucks._normalizeShortcodeContext(context),
Expand Down
17 changes: 17 additions & 0 deletions src/EventBus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const EventEmitter = require("./Util/AsyncEventEmitter");
const debug = require("debug")("Eleventy:EventBus");

/**
* @module 11ty/eleventy/EventBus
*/

debug("Setting up global EventBus.");
/**
* Provides a global event bus that modules deep down in the stack can
* subscribe to from a global singleton for decoupled pub/sub.
* @type * {module:11ty/eleventy/Util/AsyncEventEmitter~AsyncEventEmitter}
*/
let bus = new EventEmitter();
bus.setMaxListeners(100);

module.exports = bus;
4 changes: 4 additions & 0 deletions src/TemplateContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const config = require("./Config");
const debug = require("debug")("Eleventy:TemplateContent");
const debugDev = require("debug")("Dev:Eleventy:TemplateContent");
const bench = require("./BenchmarkManager").get("Aggregate");
const eventBus = require("./EventBus");

class TemplateContentFrontMatterError extends EleventyBaseError {}
class TemplateContentCompileError extends EleventyBaseError {}
Expand Down Expand Up @@ -277,5 +278,8 @@ class TemplateContent {

TemplateContent._inputCache = new Map();
TemplateContent._compileEngineCache = new Map();
eventBus.on("resourceModified", (path) => {
TemplateContent.deleteCached(path);
});

module.exports = TemplateContent;
28 changes: 18 additions & 10 deletions test/TemplateTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2049,11 +2049,9 @@ test("Engine Singletons", async (t) => {
});

test("Make sure layout cache takes new changes during watch (nunjucks)", async (t) => {
await fsp.writeFile(
"./test/stubs-layout-cache/_includes/include-script-1.js",
`alert("hi");`,
{ encoding: "utf8" }
);
let filePath = "./test/stubs-layout-cache/_includes/include-script-1.js";

await fsp.writeFile(filePath, `alert("hi");`, { encoding: "utf8" });

let tmpl = getNewTemplate(
"./test/stubs-layout-cache/test.njk",
Expand All @@ -2065,13 +2063,23 @@ test("Make sure layout cache takes new changes during watch (nunjucks)", async (

t.is((await tmpl.render(data)).trim(), '<script>alert("hi");</script>');

await fsp.writeFile(
"./test/stubs-layout-cache/_includes/include-script-1.js",
`alert("bye");`,
{ encoding: "utf8" }
);
let eventBus = require("../src/EventBus");
let chokidar = require("chokidar");
let watcher = chokidar.watch(filePath, { interval: 10, persistent: true });
watcher.on("change", (path, stats) => {
eventBus.emit("resourceModified", path);
});

await fsp.writeFile(filePath, `alert("bye");`, { encoding: "utf8" });

// Give Chokidar time to see the change;
await new Promise((res, rej) => {
setTimeout(res, 200);
});

t.is((await tmpl.render(data)).trim(), '<script>alert("bye");</script>');

await watcher.close();
});

test("Make sure layout cache takes new changes during watch (liquid)", async (t) => {
Expand Down

0 comments on commit 0cf3fd1

Please sign in to comment.