Skip to content

Commit bdaab0f

Browse files
author
ppoffice
committed
chore(perf): cache partial fragments with LRU
1 parent f5185d5 commit bdaab0f

2 files changed

Lines changed: 312 additions & 1 deletion

File tree

includes/helpers/override.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313
const cheerio = require('cheerio');
1414
const { existsSync } = require('fs');
1515
const { relative, dirname, join, extname } = require('path');
16+
const { LRUMap } = require('../utils/lru');
1617

1718
const __archives = [];
1819
const __categories = [];
1920
const __tags = [];
2021

22+
const __fragmentCache = new LRUMap(20);
23+
2124
module.exports = function (hexo) {
2225
hexo.extend.helper.register('_list_archives', function () {
2326
if (__archives.length) {
@@ -183,6 +186,9 @@ module.exports = function (hexo) {
183186
const fragment = relative(view_dir, path.substring(0, path.length - '.locals.js'.length));
184187
const cacheId = [fragment, language, md5(JSON.stringify(_locals))].join('-');
185188

186-
return partial(name, _locals, { cache: cacheId, only: options.only || false });
189+
if (!__fragmentCache.has(cacheId)) {
190+
__fragmentCache.set(cacheId, partial(name, _locals, { cache: false, only: options.only || false }));
191+
}
192+
return __fragmentCache.get(cacheId);
187193
});
188194
}

includes/utils/lru.js

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
/**
2+
* A doubly linked list-based Least Recently Used (LRU) cache. Will keep most
3+
* recently used items while discarding least recently used items when its limit
4+
* is reached.
5+
*
6+
* Licensed under MIT. Copyright (c) 2010 Rasmus Andersson <http://hunch.se/>
7+
* See README.md for details.
8+
*
9+
* Illustration of the design:
10+
*
11+
* entry entry entry entry
12+
* ______ ______ ______ ______
13+
* | head |.newer => | |.newer => | |.newer => | tail |
14+
* | A | | B | | C | | D |
15+
* |______| <= older.|______| <= older.|______| <= older.|______|
16+
*
17+
* removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added
18+
*/
19+
(function (g, f) {
20+
const e = typeof exports == 'object' ? exports : typeof g == 'object' ? g : {};
21+
f(e);
22+
if (typeof define == 'function' && define.amd) { define('lru', e); }
23+
})(this, function (exports) {
24+
25+
const NEWER = Symbol('newer');
26+
const OLDER = Symbol('older');
27+
28+
function LRUMap(limit, entries) {
29+
if (typeof limit !== 'number') {
30+
// called as (entries)
31+
entries = limit;
32+
limit = 0;
33+
}
34+
35+
this.size = 0;
36+
this.limit = limit;
37+
this.oldest = this.newest = undefined;
38+
this._keymap = new Map();
39+
40+
if (entries) {
41+
this.assign(entries);
42+
if (limit < 1) {
43+
this.limit = this.size;
44+
}
45+
}
46+
}
47+
48+
exports.LRUMap = LRUMap;
49+
50+
function Entry(key, value) {
51+
this.key = key;
52+
this.value = value;
53+
this[NEWER] = undefined;
54+
this[OLDER] = undefined;
55+
}
56+
57+
58+
LRUMap.prototype._markEntryAsUsed = function (entry) {
59+
if (entry === this.newest) {
60+
// Already the most recenlty used entry, so no need to update the list
61+
return;
62+
}
63+
// HEAD--------------TAIL
64+
// <.older .newer>
65+
// <--- add direction --
66+
// A B C <D> E
67+
if (entry[NEWER]) {
68+
if (entry === this.oldest) {
69+
this.oldest = entry[NEWER];
70+
}
71+
entry[NEWER][OLDER] = entry[OLDER]; // C <-- E.
72+
}
73+
if (entry[OLDER]) {
74+
entry[OLDER][NEWER] = entry[NEWER]; // C. --> E
75+
}
76+
entry[NEWER] = undefined; // D --x
77+
entry[OLDER] = this.newest; // D. --> E
78+
if (this.newest) {
79+
this.newest[NEWER] = entry; // E. <-- D
80+
}
81+
this.newest = entry;
82+
};
83+
84+
LRUMap.prototype.assign = function (entries) {
85+
let entry, limit = this.limit || Number.MAX_VALUE;
86+
this._keymap.clear();
87+
let it = entries[Symbol.iterator]();
88+
for (let itv = it.next(); !itv.done; itv = it.next()) {
89+
let e = new Entry(itv.value[0], itv.value[1]);
90+
this._keymap.set(e.key, e);
91+
if (!entry) {
92+
this.oldest = e;
93+
} else {
94+
entry[NEWER] = e;
95+
e[OLDER] = entry;
96+
}
97+
entry = e;
98+
if (limit-- == 0) {
99+
throw new Error('overflow');
100+
}
101+
}
102+
this.newest = entry;
103+
this.size = this._keymap.size;
104+
};
105+
106+
LRUMap.prototype.get = function (key) {
107+
// First, find our cache entry
108+
var entry = this._keymap.get(key);
109+
if (!entry) return; // Not cached. Sorry.
110+
// As <key> was found in the cache, register it as being requested recently
111+
this._markEntryAsUsed(entry);
112+
return entry.value;
113+
};
114+
115+
LRUMap.prototype.set = function (key, value) {
116+
var entry = this._keymap.get(key);
117+
118+
if (entry) {
119+
// update existing
120+
entry.value = value;
121+
this._markEntryAsUsed(entry);
122+
return this;
123+
}
124+
125+
// new entry
126+
this._keymap.set(key, (entry = new Entry(key, value)));
127+
128+
if (this.newest) {
129+
// link previous tail to the new tail (entry)
130+
this.newest[NEWER] = entry;
131+
entry[OLDER] = this.newest;
132+
} else {
133+
// we're first in -- yay
134+
this.oldest = entry;
135+
}
136+
137+
// add new entry to the end of the linked list -- it's now the freshest entry.
138+
this.newest = entry;
139+
++this.size;
140+
if (this.size > this.limit) {
141+
// we hit the limit -- remove the head
142+
this.shift();
143+
}
144+
145+
return this;
146+
};
147+
148+
LRUMap.prototype.shift = function () {
149+
// todo: handle special case when limit == 1
150+
var entry = this.oldest;
151+
if (entry) {
152+
if (this.oldest[NEWER]) {
153+
// advance the list
154+
this.oldest = this.oldest[NEWER];
155+
this.oldest[OLDER] = undefined;
156+
} else {
157+
// the cache is exhausted
158+
this.oldest = undefined;
159+
this.newest = undefined;
160+
}
161+
// Remove last strong reference to <entry> and remove links from the purged
162+
// entry being returned:
163+
entry[NEWER] = entry[OLDER] = undefined;
164+
this._keymap.delete(entry.key);
165+
--this.size;
166+
return [entry.key, entry.value];
167+
}
168+
};
169+
170+
// ----------------------------------------------------------------------------
171+
// Following code is optional and can be removed without breaking the core
172+
// functionality.
173+
174+
LRUMap.prototype.find = function (key) {
175+
let e = this._keymap.get(key);
176+
return e ? e.value : undefined;
177+
};
178+
179+
LRUMap.prototype.has = function (key) {
180+
return this._keymap.has(key);
181+
};
182+
183+
LRUMap.prototype['delete'] = function (key) {
184+
var entry = this._keymap.get(key);
185+
if (!entry) return;
186+
this._keymap.delete(entry.key);
187+
if (entry[NEWER] && entry[OLDER]) {
188+
// relink the older entry with the newer entry
189+
entry[OLDER][NEWER] = entry[NEWER];
190+
entry[NEWER][OLDER] = entry[OLDER];
191+
} else if (entry[NEWER]) {
192+
// remove the link to us
193+
entry[NEWER][OLDER] = undefined;
194+
// link the newer entry to head
195+
this.oldest = entry[NEWER];
196+
} else if (entry[OLDER]) {
197+
// remove the link to us
198+
entry[OLDER][NEWER] = undefined;
199+
// link the newer entry to head
200+
this.newest = entry[OLDER];
201+
} else {// if(entry[OLDER] === undefined && entry.newer === undefined) {
202+
this.oldest = this.newest = undefined;
203+
}
204+
205+
this.size--;
206+
return entry.value;
207+
};
208+
209+
LRUMap.prototype.clear = function () {
210+
// Not clearing links should be safe, as we don't expose live links to user
211+
this.oldest = this.newest = undefined;
212+
this.size = 0;
213+
this._keymap.clear();
214+
};
215+
216+
217+
function EntryIterator(oldestEntry) { this.entry = oldestEntry; }
218+
EntryIterator.prototype[Symbol.iterator] = function () { return this; }
219+
EntryIterator.prototype.next = function () {
220+
let ent = this.entry;
221+
if (ent) {
222+
this.entry = ent[NEWER];
223+
return { done: false, value: [ent.key, ent.value] };
224+
} else {
225+
return { done: true, value: undefined };
226+
}
227+
};
228+
229+
230+
function KeyIterator(oldestEntry) { this.entry = oldestEntry; }
231+
KeyIterator.prototype[Symbol.iterator] = function () { return this; }
232+
KeyIterator.prototype.next = function () {
233+
let ent = this.entry;
234+
if (ent) {
235+
this.entry = ent[NEWER];
236+
return { done: false, value: ent.key };
237+
} else {
238+
return { done: true, value: undefined };
239+
}
240+
};
241+
242+
function ValueIterator(oldestEntry) { this.entry = oldestEntry; }
243+
ValueIterator.prototype[Symbol.iterator] = function () { return this; }
244+
ValueIterator.prototype.next = function () {
245+
let ent = this.entry;
246+
if (ent) {
247+
this.entry = ent[NEWER];
248+
return { done: false, value: ent.value };
249+
} else {
250+
return { done: true, value: undefined };
251+
}
252+
};
253+
254+
255+
LRUMap.prototype.keys = function () {
256+
return new KeyIterator(this.oldest);
257+
};
258+
259+
LRUMap.prototype.values = function () {
260+
return new ValueIterator(this.oldest);
261+
};
262+
263+
LRUMap.prototype.entries = function () {
264+
return this;
265+
};
266+
267+
LRUMap.prototype[Symbol.iterator] = function () {
268+
return new EntryIterator(this.oldest);
269+
};
270+
271+
LRUMap.prototype.forEach = function (fun, thisObj) {
272+
if (typeof thisObj !== 'object') {
273+
thisObj = this;
274+
}
275+
let entry = this.oldest;
276+
while (entry) {
277+
fun.call(thisObj, entry.value, entry.key, this);
278+
entry = entry[NEWER];
279+
}
280+
};
281+
282+
/** Returns a JSON (array) representation */
283+
LRUMap.prototype.toJSON = function () {
284+
var s = new Array(this.size), i = 0, entry = this.oldest;
285+
while (entry) {
286+
s[i++] = { key: entry.key, value: entry.value };
287+
entry = entry[NEWER];
288+
}
289+
return s;
290+
};
291+
292+
/** Returns a String representation */
293+
LRUMap.prototype.toString = function () {
294+
var s = '', entry = this.oldest;
295+
while (entry) {
296+
s += String(entry.key) + ':' + entry.value;
297+
entry = entry[NEWER];
298+
if (entry) {
299+
s += ' < ';
300+
}
301+
}
302+
return s;
303+
};
304+
305+
});

0 commit comments

Comments
 (0)