Skip to content

Commit

Permalink
perf(decode): add a cache for buffer-to-string conversions
Browse files Browse the repository at this point in the history
The tiny and small benchmarks were greatly improved:

+--------------+-------------------+-----------------+----------------+-------------+
|              │ tiny              │ small           │ medium         │ large       |
+--------------+-------------------+-----------------+----------------+-------------+
| before       │ 1,712,916 ops/sec │ 369,462 ops/sec │ 30,692 ops/sec │ 252 ops/sec |
+--------------+-------------------+-----------------+----------------+-------------+
| after        │ 3,138,712 ops/sec │ 642,057 ops/sec │ 34,143 ops/sec │ 252 ops/sec |
+--------------+-------------------+-----------------+----------------+-------------+

Note: only object keys are cached.
  • Loading branch information
darrachequesne committed Mar 15, 2020
1 parent 9bf1519 commit 3c0e5a6
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 1 deletion.
51 changes: 51 additions & 0 deletions lib/DecodeKeyCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const DEFAULT_MAX_SIZE = process.env.NOTEPACK_DECODE_KEY_CACHE_MAX_SIZE || 1024;
const DEFAULT_MAX_LENGTH = process.env.NOTEPACK_DECODE_KEY_CACHE_MAX_LENGTH || 16;

/**
* Store the buffer-to-string values in a tree
*/
class DecodeKeyCache {
constructor({ maxSize = DEFAULT_MAX_SIZE, maxLength = DEFAULT_MAX_LENGTH } = {}) {
this.size = 0;
this.maxSize = maxSize;
this.maxLength = maxLength;
this.cache = new Map();
for (let i = 1; i <= this.maxLength; i++) {
this.cache.set(i, new Map());
}
}

get(buffer, offset, length) {
if (length > this.maxLength) { return false; }
let node = this.cache.get(length);
for (let i = 0; i < length; i++) {
const byte = buffer[offset + i];
if (node.has(byte)) {
node = node.get(byte);
} else {
return false;
}
}
return node;
}

set(buffer, offset, length, value) {
if (length > this.maxLength || this.size >= this.maxSize) { return; }
this.size++;
let node = this.cache.get(length);
for (let i = 0; i < length; i++) {
const byte = buffer[offset + i];
if (i === length - 1) {
node.set(byte, value);
} else if (node.has(byte)) {
node = node.get(byte);
} else {
const newNode = new Map();
node.set(byte, newNode);
node = newNode;
}
}
}
}

module.exports = DecodeKeyCache;
18 changes: 17 additions & 1 deletion lib/decode.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
'use strict';

const DecodeKeyCache = require('./DecodeKeyCache');
const cache = new DecodeKeyCache();

function Decoder(buffer) {
this.offset = 0;
this.buffer = buffer;
this.useKeyCache = false;
}

Decoder.prototype.array = function (length) {
Expand All @@ -16,14 +20,26 @@ Decoder.prototype.array = function (length) {
Decoder.prototype.map = function (length) {
let key = '', value = {};
for (let i = 0; i < length; i++) {
key = this.parse();
this.useKeyCache = true;
key = this.parse(true);
this.useKeyCache = false;
value[key] = this.parse();
}
return value;
};

Decoder.prototype.str = function (length) {
if (this.useKeyCache) {
const valueFromCache = cache.get(this.buffer, this.offset, length);
if (valueFromCache) {
this.offset += length;
return valueFromCache;
}
}
const value = this.buffer.toString('utf8', this.offset, this.offset + length);
if (this.useKeyCache) {
cache.set(this.buffer, this.offset, length, value);
}
this.offset += length;
return value;
};
Expand Down
44 changes: 44 additions & 0 deletions test/DecodeKeyCache.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';

const expect = require('chai').expect;
const DecodeKeyCache = require('../lib/DecodeKeyCache');

describe('DecodeKeyCache', () => {
it('should get/set values', () => {
const cache = new DecodeKeyCache();
cache.set(Buffer.from([1, 1]), 0, 2, '11');
cache.set(Buffer.from([1, 2]), 0, 2, '12');
cache.set(Buffer.from([1, 2, 3]), 0, 3, '123');
cache.set(Buffer.from([1, 2, 3, 4]), 1, 2, '23');

expect(cache.get(Buffer.from([1, 1]), 0, 2)).to.equal('11');
expect(cache.get(Buffer.from([1, 2, 3, 4]), 0, 2)).to.equal('12');
expect(cache.get(Buffer.from([0, 1, 2, 3, 4]), 2, 2)).to.equal('23');
expect(cache.get(Buffer.from([1, 3]), 0, 1)).to.equal(false);
expect(cache.get(null, null, 17)).to.equal(false);
});

it('should respect maxLength limit', () => {
const cache = new DecodeKeyCache({ maxLength: 3 });
cache.set(Buffer.from([1]), 0, 1, '1');
cache.set(Buffer.from([1, 2]), 0, 2, '12');
cache.set(Buffer.from([1, 2, 3]), 0, 3, '123');
cache.set(Buffer.from([1, 2, 3, 4]), 0, 4, '1234');

expect(cache.size).to.equal(3);
expect(cache.get(Buffer.from([1, 2, 3, 4]), 0, 4)).to.equal(false);
});

it('should respect maxSize limit', () => {
const cache = new DecodeKeyCache({ maxSize: 2 });
cache.set(Buffer.from([1]), 0, 1, '1');
cache.set(Buffer.from([1, 2]), 0, 2, '12');
cache.set(Buffer.from([1, 2, 3]), 0, 3, '123');
cache.set(Buffer.from([1, 2, 3, 4]), 0, 4, '1234');

expect(cache.size).to.equal(2);
expect(cache.get(Buffer.from([1, 2, 3]), 0, 3)).to.equal(false);
});

});

0 comments on commit 3c0e5a6

Please sign in to comment.