From bdaab0f8fdd1794a8b41ec3f796755f6bb62d349 Mon Sep 17 00:00:00 2001 From: ppoffice Date: Fri, 20 Dec 2019 18:06:20 -0500 Subject: [PATCH] chore(perf): cache partial fragments with LRU --- includes/helpers/override.js | 8 +- includes/utils/lru.js | 305 +++++++++++++++++++++++++++++++++++ 2 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 includes/utils/lru.js diff --git a/includes/helpers/override.js b/includes/helpers/override.js index fd3af3d..29988c0 100644 --- a/includes/helpers/override.js +++ b/includes/helpers/override.js @@ -13,11 +13,14 @@ const cheerio = require('cheerio'); const { existsSync } = require('fs'); const { relative, dirname, join, extname } = require('path'); +const { LRUMap } = require('../utils/lru'); const __archives = []; const __categories = []; const __tags = []; +const __fragmentCache = new LRUMap(20); + module.exports = function (hexo) { hexo.extend.helper.register('_list_archives', function () { if (__archives.length) { @@ -183,6 +186,9 @@ module.exports = function (hexo) { const fragment = relative(view_dir, path.substring(0, path.length - '.locals.js'.length)); const cacheId = [fragment, language, md5(JSON.stringify(_locals))].join('-'); - return partial(name, _locals, { cache: cacheId, only: options.only || false }); + if (!__fragmentCache.has(cacheId)) { + __fragmentCache.set(cacheId, partial(name, _locals, { cache: false, only: options.only || false })); + } + return __fragmentCache.get(cacheId); }); } \ No newline at end of file diff --git a/includes/utils/lru.js b/includes/utils/lru.js new file mode 100644 index 0000000..014d4c8 --- /dev/null +++ b/includes/utils/lru.js @@ -0,0 +1,305 @@ +/** + * A doubly linked list-based Least Recently Used (LRU) cache. Will keep most + * recently used items while discarding least recently used items when its limit + * is reached. + * + * Licensed under MIT. Copyright (c) 2010 Rasmus Andersson + * See README.md for details. + * + * Illustration of the design: + * + * entry entry entry entry + * ______ ______ ______ ______ + * | head |.newer => | |.newer => | |.newer => | tail | + * | A | | B | | C | | D | + * |______| <= older.|______| <= older.|______| <= older.|______| + * + * removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added + */ +(function (g, f) { + const e = typeof exports == 'object' ? exports : typeof g == 'object' ? g : {}; + f(e); + if (typeof define == 'function' && define.amd) { define('lru', e); } +})(this, function (exports) { + + const NEWER = Symbol('newer'); + const OLDER = Symbol('older'); + + function LRUMap(limit, entries) { + if (typeof limit !== 'number') { + // called as (entries) + entries = limit; + limit = 0; + } + + this.size = 0; + this.limit = limit; + this.oldest = this.newest = undefined; + this._keymap = new Map(); + + if (entries) { + this.assign(entries); + if (limit < 1) { + this.limit = this.size; + } + } + } + + exports.LRUMap = LRUMap; + + function Entry(key, value) { + this.key = key; + this.value = value; + this[NEWER] = undefined; + this[OLDER] = undefined; + } + + + LRUMap.prototype._markEntryAsUsed = function (entry) { + if (entry === this.newest) { + // Already the most recenlty used entry, so no need to update the list + return; + } + // HEAD--------------TAIL + // <.older .newer> + // <--- add direction -- + // A B C E + if (entry[NEWER]) { + if (entry === this.oldest) { + this.oldest = entry[NEWER]; + } + entry[NEWER][OLDER] = entry[OLDER]; // C <-- E. + } + if (entry[OLDER]) { + entry[OLDER][NEWER] = entry[NEWER]; // C. --> E + } + entry[NEWER] = undefined; // D --x + entry[OLDER] = this.newest; // D. --> E + if (this.newest) { + this.newest[NEWER] = entry; // E. <-- D + } + this.newest = entry; + }; + + LRUMap.prototype.assign = function (entries) { + let entry, limit = this.limit || Number.MAX_VALUE; + this._keymap.clear(); + let it = entries[Symbol.iterator](); + for (let itv = it.next(); !itv.done; itv = it.next()) { + let e = new Entry(itv.value[0], itv.value[1]); + this._keymap.set(e.key, e); + if (!entry) { + this.oldest = e; + } else { + entry[NEWER] = e; + e[OLDER] = entry; + } + entry = e; + if (limit-- == 0) { + throw new Error('overflow'); + } + } + this.newest = entry; + this.size = this._keymap.size; + }; + + LRUMap.prototype.get = function (key) { + // First, find our cache entry + var entry = this._keymap.get(key); + if (!entry) return; // Not cached. Sorry. + // As was found in the cache, register it as being requested recently + this._markEntryAsUsed(entry); + return entry.value; + }; + + LRUMap.prototype.set = function (key, value) { + var entry = this._keymap.get(key); + + if (entry) { + // update existing + entry.value = value; + this._markEntryAsUsed(entry); + return this; + } + + // new entry + this._keymap.set(key, (entry = new Entry(key, value))); + + if (this.newest) { + // link previous tail to the new tail (entry) + this.newest[NEWER] = entry; + entry[OLDER] = this.newest; + } else { + // we're first in -- yay + this.oldest = entry; + } + + // add new entry to the end of the linked list -- it's now the freshest entry. + this.newest = entry; + ++this.size; + if (this.size > this.limit) { + // we hit the limit -- remove the head + this.shift(); + } + + return this; + }; + + LRUMap.prototype.shift = function () { + // todo: handle special case when limit == 1 + var entry = this.oldest; + if (entry) { + if (this.oldest[NEWER]) { + // advance the list + this.oldest = this.oldest[NEWER]; + this.oldest[OLDER] = undefined; + } else { + // the cache is exhausted + this.oldest = undefined; + this.newest = undefined; + } + // Remove last strong reference to and remove links from the purged + // entry being returned: + entry[NEWER] = entry[OLDER] = undefined; + this._keymap.delete(entry.key); + --this.size; + return [entry.key, entry.value]; + } + }; + + // ---------------------------------------------------------------------------- + // Following code is optional and can be removed without breaking the core + // functionality. + + LRUMap.prototype.find = function (key) { + let e = this._keymap.get(key); + return e ? e.value : undefined; + }; + + LRUMap.prototype.has = function (key) { + return this._keymap.has(key); + }; + + LRUMap.prototype['delete'] = function (key) { + var entry = this._keymap.get(key); + if (!entry) return; + this._keymap.delete(entry.key); + if (entry[NEWER] && entry[OLDER]) { + // relink the older entry with the newer entry + entry[OLDER][NEWER] = entry[NEWER]; + entry[NEWER][OLDER] = entry[OLDER]; + } else if (entry[NEWER]) { + // remove the link to us + entry[NEWER][OLDER] = undefined; + // link the newer entry to head + this.oldest = entry[NEWER]; + } else if (entry[OLDER]) { + // remove the link to us + entry[OLDER][NEWER] = undefined; + // link the newer entry to head + this.newest = entry[OLDER]; + } else {// if(entry[OLDER] === undefined && entry.newer === undefined) { + this.oldest = this.newest = undefined; + } + + this.size--; + return entry.value; + }; + + LRUMap.prototype.clear = function () { + // Not clearing links should be safe, as we don't expose live links to user + this.oldest = this.newest = undefined; + this.size = 0; + this._keymap.clear(); + }; + + + function EntryIterator(oldestEntry) { this.entry = oldestEntry; } + EntryIterator.prototype[Symbol.iterator] = function () { return this; } + EntryIterator.prototype.next = function () { + let ent = this.entry; + if (ent) { + this.entry = ent[NEWER]; + return { done: false, value: [ent.key, ent.value] }; + } else { + return { done: true, value: undefined }; + } + }; + + + function KeyIterator(oldestEntry) { this.entry = oldestEntry; } + KeyIterator.prototype[Symbol.iterator] = function () { return this; } + KeyIterator.prototype.next = function () { + let ent = this.entry; + if (ent) { + this.entry = ent[NEWER]; + return { done: false, value: ent.key }; + } else { + return { done: true, value: undefined }; + } + }; + + function ValueIterator(oldestEntry) { this.entry = oldestEntry; } + ValueIterator.prototype[Symbol.iterator] = function () { return this; } + ValueIterator.prototype.next = function () { + let ent = this.entry; + if (ent) { + this.entry = ent[NEWER]; + return { done: false, value: ent.value }; + } else { + return { done: true, value: undefined }; + } + }; + + + LRUMap.prototype.keys = function () { + return new KeyIterator(this.oldest); + }; + + LRUMap.prototype.values = function () { + return new ValueIterator(this.oldest); + }; + + LRUMap.prototype.entries = function () { + return this; + }; + + LRUMap.prototype[Symbol.iterator] = function () { + return new EntryIterator(this.oldest); + }; + + LRUMap.prototype.forEach = function (fun, thisObj) { + if (typeof thisObj !== 'object') { + thisObj = this; + } + let entry = this.oldest; + while (entry) { + fun.call(thisObj, entry.value, entry.key, this); + entry = entry[NEWER]; + } + }; + + /** Returns a JSON (array) representation */ + LRUMap.prototype.toJSON = function () { + var s = new Array(this.size), i = 0, entry = this.oldest; + while (entry) { + s[i++] = { key: entry.key, value: entry.value }; + entry = entry[NEWER]; + } + return s; + }; + + /** Returns a String representation */ + LRUMap.prototype.toString = function () { + var s = '', entry = this.oldest; + while (entry) { + s += String(entry.key) + ':' + entry.value; + entry = entry[NEWER]; + if (entry) { + s += ' < '; + } + } + return s; + }; + +});