Skip to content

Commit b31fb10

Browse files
committed
chore: benchmarks for lru-cache family
1 parent ff991d6 commit b31fb10

File tree

6 files changed

+556
-0
lines changed

6 files changed

+556
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
## 0.40.0
44

5+
* LRU Cache with Time-To-Keep expiration (@mrflip). (provisional)
6+
*
57
* Adding `#isEqual` to Set helpers (@mrflip). (provisional)
8+
* Improvements in test and benchmark ergonomics (@mrflip).
69

710
## 0.39.2
811

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
var random = require('pandemonium/random');
2+
var Benchmark = require('benchmark')
3+
var Keymaster = require('./key-distributions.js');
4+
5+
var TEST_CAP = 30000
6+
7+
function makeStandardKeys() {
8+
var StrKeys = {}
9+
var NumKeys = {}
10+
//
11+
// 400k entries with approx 42k distinct values btwn 0 and 60k, distributed 300k/65k/23k/10k/5k/3k (~97% in the top 30k)
12+
NumKeys.gen97 = Keymaster.longTailIntGen(60000, -0.4);
13+
NumKeys.arr97 = Keymaster.longTailArr(400000, 60000, -0.4);
14+
StrKeys.arr97 = Keymaster.stringifyArr(NumKeys.arr97);
15+
StrKeys.gen97 = Keymaster.longTailStrGen(60000, -0.4);
16+
NumKeys.arr97.note = 'Long-tail pool of 42,000 distinct numeric values, 97% in the top 30k, 75% in the top 10k'; StrKeys.arr97.note = NumKeys.arr97.note.replace(/numeric/, 'string');
17+
//
18+
// 400k entries with approx 50k distinct values btwn 0 and 60k, distributed 230k/80k/40k/22k/15k/10k (~88% in the top 30k)
19+
// var NumKeys.arr88 = Keymaster.longTailArr(400000, 60000, -0.7)
20+
//
21+
// 400k entries with approx 60k distinct values btwn 0 and 60k, distributed 135k/85k/61k/48k/39k/33k (~70% in the top 30k)
22+
NumKeys.gen70 = Keymaster.longTailIntGen(60000, -10);
23+
NumKeys.arr70 = Keymaster.longTailArr(400000, 60000, -10);
24+
StrKeys.arr70 = Keymaster.stringifyArr(NumKeys.arr70);
25+
StrKeys.gen70 = Keymaster.longTailStrGen(60000, -10);
26+
NumKeys.arr70.note = 'Long-tail pool of ~60,000 distinct numeric values, 70% in the top 30k, 33% in the top 10k'; StrKeys.arr70.note = NumKeys.arr70.note.replace(/numeric/, 'string');
27+
//
28+
// 120k entries with approx 52k distinct values btwn 0 and 60k, distributed evenly
29+
NumKeys.arrFlat = Keymaster.flatDistArr(120000, 60000);
30+
StrKeys.arrFlat = Keymaster.stringifyArr(NumKeys.arrFlat);
31+
//
32+
// 31k entries running 0-31k in order
33+
NumKeys.arrOrd = Keymaster.ascendingArr(31000, 31000);
34+
StrKeys.arrOrd = Keymaster.stringifyArr(NumKeys.arrOrd);
35+
//
36+
return { StrKeys, NumKeys }
37+
}
38+
39+
function read1(cache, arrA) {
40+
var count = arrA.length;
41+
for (var ii = 0; ii < count; ii++) {
42+
cache.get(arrA[ii % arrA.length])
43+
}
44+
}
45+
46+
function readFetch(cache, arrA, rng) {
47+
var count = arrA.length;
48+
for (var ii = 0; ii < count; ii++) {
49+
var keyA = arrA[ii % arrA.length];
50+
var result = cache.get(keyA);
51+
if (! result) {
52+
cache.set(keyA, rng());
53+
}
54+
}
55+
}
56+
57+
function readFetch2(cache, [arrA, arrB], rng) {
58+
var count = arrA.length;
59+
for (var ii = 0; ii < count; ii++) {
60+
var keyA = arrA[ii % arrA.length];
61+
var keyB = arrB[ii % arrB.length];
62+
var result;
63+
result = cache.get(keyA)
64+
if (! result) { cache.set(keyA, rng()); }
65+
result = cache.get(keyB)
66+
if (! result) { cache.set(keyB, rng()); }
67+
}
68+
}
69+
70+
function write1(cache, arrA) {
71+
var count = arrA.length;
72+
for (var ii = 0; ii < count; ii++) {
73+
var storeme = arrA[ii % arrA.length]
74+
cache.set(storeme, storeme)
75+
}
76+
}
77+
78+
function write1Read1(cache, [arrA, arrB], count) {
79+
var blen = arrB.length;
80+
if (! count) { count = arrA.length; }
81+
for (var ii = 0; ii < count; ii++) {
82+
var storeme = arrA[ii % arrA.length]
83+
cache.set(storeme, storeme)
84+
cache.get(arrB[ii % blen])
85+
}
86+
}
87+
88+
function write1Read4(cache, [arrA, arrB], count) {
89+
var blen = arrB.length;
90+
var boff0 = 0, boff1 = blen * 0.25, boff2 = blen * 0.50, boff3 = blen * 0.75;
91+
if (! count) { count = arrA.length; }
92+
for (var ii = 0; ii < count; ii++) {
93+
var storeme = arrA[ii % arrA.length]
94+
cache.set(storeme, storeme)
95+
cache.get(arrB[(ii + boff0) % blen])
96+
cache.get(arrB[(ii + boff1) % blen])
97+
cache.get(arrB[(ii + boff2) % blen])
98+
cache.get(arrB[(ii + boff3) % blen])
99+
}
100+
}
101+
102+
function writeSome(cache, arrA, frac = 0.2) {
103+
var count = arrA.length;
104+
for (var ii = 0; ii < count; ii++) {
105+
if (Math.random() > frac) { continue; }
106+
var storeme = arrA[ii % arrA.length];
107+
cache.set(storeme, storeme);
108+
}
109+
}
110+
111+
function delete1(cache, [arrA], count) {
112+
if (! count) { count = arrA.length; }
113+
for (var ii = 0; ii < count; ii++) {
114+
var delme = arrA[ii % arrA.length]
115+
cache.delete(delme, delme)
116+
}
117+
}
118+
119+
function makeLoadedCaches(CacheFactories, arrA, count, capacity = TEST_CAP, options) {
120+
var caches = CacheFactories.map((CacheFactory) => makeLoadedCache(CacheFactory, arrA, count, capacity, options));
121+
caches.note = `${capacity}-capacity caches${arrA.note ? ' preloaded with ' + arrA.note : ''}`
122+
return caches
123+
}
124+
125+
function makeCaches(CacheFactories, capacity = TEST_CAP, options = {}) {
126+
var caches = CacheFactories.map((CacheFactory) => {
127+
var cache = new CacheFactory(null, null, capacity, options);
128+
cache.name = CacheFactory.name;
129+
return cache;
130+
})
131+
caches.note = `${capacity}-capacity caches`
132+
return caches
133+
}
134+
135+
function makeLoadedCache(CacheFactory, arrA, count, capacity = TEST_CAP, options) {
136+
if (! count) { count = arrA.length; }
137+
var cache = new CacheFactory(null, null, capacity, options);
138+
cache.name = CacheFactory.name;
139+
write1(cache, arrA, count);
140+
var capK = Math.round(capacity / 1000);
141+
cache.note = `Pre-loaded ${cache.name}@${capK}k`;
142+
return cache;
143+
}
144+
145+
function times(count, func, ...args) {
146+
for (var ii = 0; ii < count; ii++) {
147+
func(ii, count, ...args);
148+
}
149+
}
150+
151+
async function promisedTimes(count, func, ...args) {
152+
var results = [];
153+
for (var ii = 0; ii < count; ii++) {
154+
var result = await func(ii, count, ...args);
155+
results.push(result);
156+
}
157+
return Promise.all(results);
158+
}
159+
160+
function round(val, decimals) {
161+
chunk = Math.round(Math.pow(10, decimals));
162+
return Math.round(val * chunk) / chunk;
163+
}
164+
165+
function sleep(millis) { return new Promise((yay) => setTimeout(yay, millis)); }
166+
167+
module.exports = {
168+
read1, readFetch, write1, write1Read1, write1Read4, delete1, writeSome,
169+
makeStandardKeys, makeLoadedCaches, makeLoadedCache, makeCaches,
170+
times, promisedTimes, round, sleep,
171+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
var randomString = require('pandemonium/random-string');
2+
var random = require('pandemonium/random');
3+
var typed = require('../../../utils/typed-arrays.js');
4+
var {snipToLast} = require('../../../utils/snip.js');
5+
6+
module.exports.random = random;
7+
module.exports.randomString = randomString;
8+
9+
function randArr(size, range, rng) {
10+
var ValArrayFactory = typed.getPointerArray(range);
11+
var arr = new ValArrayFactory(size)
12+
for (var ii = 0; ii < size; ii++) {
13+
arr[ii] = rng(ii);
14+
}
15+
return arr
16+
}
17+
module.exports.randArr = randArr;
18+
19+
function longTailArr(size, range, power) {
20+
var intgen = longTailIntGen(range, power)
21+
return randArr(size, range, intgen);
22+
}
23+
module.exports.longTailArr = longTailArr;
24+
25+
function flatDistArr(size, range, offset = 0) {
26+
var intgen = () => random(offset, range + offset);
27+
return randArr(size, range, intgen);
28+
}
29+
module.exports.flatDistArr = flatDistArr;
30+
31+
function ascendingArr(size, range) {
32+
var intgen = (ii) => (ii);
33+
return randArr(size, range, intgen);
34+
}
35+
module.exports.ascendingArr = ascendingArr;
36+
37+
function longTailIntGen(range, power = -0.8) {
38+
return function intgen() {
39+
var rand = Math.random()
40+
var yy = (1 - rand)**(power) - 1
41+
var result = Math.floor(0.25 * range * yy)
42+
if (result < range) { return result }
43+
return intgen()
44+
}
45+
}
46+
module.exports.longTailIntGen = longTailIntGen;
47+
48+
function longTailStrGen(range, power = -0.8, tag = '') {
49+
var intgen = longTailIntGen(range, power);
50+
return function strgen() {
51+
return String(intgen()) + tag;
52+
}
53+
}
54+
module.exports.longTailStrGen = longTailStrGen;
55+
56+
function stringifyArr(arr, tag = '') {
57+
var stringArr = [];
58+
for (var ii = 0; ii < arr.length; ii++) {
59+
stringArr.push(String(arr[ii]) + tag);
60+
}
61+
return stringArr;
62+
}
63+
module.exports.stringifyArr = stringifyArr;
64+
65+
function comparePairTails([kk1, vv1], [kk2, vv2]) {
66+
if (vv2 > vv1) { return 1 }
67+
if (vv2 < vv1) { return -1 }
68+
if (kk2 > kk1) { return -1 }
69+
if (kk2 < kk1) { return 1 }
70+
return 1
71+
}
72+
73+
function showDistribution(arr, chunk = 1) {
74+
var counts = new Map();
75+
for (var item of arr) {
76+
const bin = chunk * Math.floor(item / chunk)
77+
if (! counts.has(bin)) { counts.set(bin, 0); }
78+
counts.set(bin, 1 + counts.get(bin));
79+
}
80+
var entries = [...counts].sort(comparePairTails)
81+
var histo = new Map(entries)
82+
histo.last = entries[entries.length - 1]
83+
return histo
84+
}
85+
module.exports.showDistribution = showDistribution;
86+
87+
function examineDist(keys, chunks = 10_000) {
88+
var histA = showDistribution(keys, 1000)
89+
var histB = showDistribution(keys, chunks)
90+
console.log(
91+
keys.length,
92+
histA.size,
93+
snipToLast(histA.entries(), new Map(), {maxToDump: 25, last: histA.last, size: histA.size}),
94+
histB,
95+
)
96+
}
97+
module.exports.examineDist = examineDist;
98+
99+
// var HewJass = longTailArr(2_000_000, 1e6, -0.5);
100+
// examineDist(HewJass, 100_000);

benchmark/lru-cache/performance.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
var random = require('pandemonium/random');
2+
var Benchmark = require('benchmark')
3+
var Keymaster = require('./helpers/key-distributions.js');
4+
var Exerciser = require('./helpers/cache-exercisers.js');
5+
var LRUCache = require('../../lru-cache.js'),
6+
LRUMap = require('../../lru-map.js'),
7+
LRUCacheWithDelete = require('../../lru-cache-with-delete.js'),
8+
LRUMapWithDelete = require('../../lru-map-with-delete.js'),
9+
LRUCacheWithExpiry = require('../../lru-cache-with-expiry.js');
10+
11+
// Benchmark.options.minSamples = 3;
12+
13+
var CACHES = [LRUCacheWithExpiry, LRUCache] //, LRUMap, LRUMapWithDelete, LRUMap, LRUCacheWithDelete, LRUCache];
14+
15+
var {
16+
makeStandardKeys, write1Read1, write1Read4, write1, read1,
17+
} = Exerciser;
18+
var { StrKeys, NumKeys } = makeStandardKeys()
19+
20+
function runEmptyCacheBenches(Keyset, benchOptions = {}) {
21+
const { gen70, gen97, arr70, arr97, arrFlat, arrOrd } = Keyset
22+
23+
var emptyCaches = Exerciser.makeCaches(CACHES);
24+
scenario('Empty caches, repeated reads', emptyCaches, arr97.note, (cache) => (function() {
25+
read1(cache, arr97);
26+
}));
27+
}
28+
29+
function runLoadedCacheBenches(Keyset, benchOptions = {}) {
30+
const { gen70, gen97, arr70, arr97, arrFlat, arrOrd } = Keyset
31+
32+
var fullCaches = Exerciser.makeLoadedCaches(CACHES, arrOrd);
33+
34+
if (benchOptions.do_expires) {
35+
fullCaches.forEach((cache) => { if (cache.monitor) { cache.monitor(200, null, {logging: true}); } });
36+
}
37+
38+
scenario('1x flat writes, 4x gentle spread read', fullCaches, arr70.note, (cache) => (function() {
39+
write1Read4(cache, [arrFlat, arr70], arr70.length);
40+
}));
41+
42+
scenario('Individual get then set operations', fullCaches, '97% short tail keys', (cache) => (function() {
43+
cache.get(gen97());
44+
cache.set(gen97(), 'hi');
45+
}));
46+
47+
scenario('Individual get then set', fullCaches, 'flat distribution 33% larger than the cache', (cache) => (function() {
48+
cache.get(String(random(0, 40000)));
49+
cache.set(String(random(0, 40000)), 'hi');
50+
}));
51+
52+
scenario('Read-only sharp spread', fullCaches, arr97.note, (cache) => (function() {
53+
read1(cache, arr97);
54+
}));
55+
56+
scenario('Read-only gentle spread', fullCaches, arr70.note, (cache) => (function() {
57+
read1(cache, arr70);
58+
}));
59+
60+
}
61+
62+
function scenario(act, caches, dataNote, actionsFactory, info) {
63+
var suite = decoratedSuite(act, caches.note, dataNote);
64+
caches.forEach((cache) => {
65+
var actions = actionsFactory(cache, info);
66+
suite.add(`${padEnd(act, 40)} -- ${padEnd(cache.name, 18)} --`, actions);
67+
// console.log(actions())
68+
})
69+
suite.run({ minSamples: 36 });
70+
}
71+
72+
const SPACES = ' ';
73+
function padEnd(str, len) {
74+
var bite = str.length > len ? 0 : len - str.length;
75+
return `${str}${SPACES.slice(0, bite)}`;
76+
}
77+
78+
function decoratedSuite(act, subjectNote, dataNote) {
79+
return new Benchmark.Suite('Testing caches')
80+
.on('start', (event) => {
81+
console.log('\n ', act);
82+
console.log(' using', subjectNote);
83+
console.log(' with', String(dataNote) + "\n Results:");
84+
})
85+
.on('error', (event) => { console.error("error in benchmark", event.target.name, event.target.error) })
86+
.on('cycle', (event) => {
87+
const benchmark = event.target;
88+
console.log(" => ", benchmark.toString());
89+
})
90+
}
91+
92+
console.log('Running with String Keys');
93+
runLoadedCacheBenches(StrKeys);
94+
runEmptyCacheBenches(StrKeys);

0 commit comments

Comments
 (0)