Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding LRU cache to make Range lookups much faster #124

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions benchmarking/range.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
Performance benchmarking script for Range.js
*/

const XLSX = require("xlsx");
const XLSX_CALC = require("../src");
const workbook = XLSX.readFile(`${__dirname}/vlookup_large_range.xlsx`);

const times = []
const n = 100;

for (let i = 0; i < n; i++) {
const t0 = performance.now();
XLSX_CALC(workbook);
const t1 = performance.now();
const previous = t1 - t0;
times.push(previous)
}

const average = (times.reduce((sum, t) => sum + t, 0)) / n;
console.log(`Average time for ${n} executions: ${average.toFixed(2)} ms`)
Binary file added benchmarking/vlookup_large_range.xlsx
Binary file not shown.
34 changes: 34 additions & 0 deletions src/LRUCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class LRUCache {
constructor(capacity = 500) {
this.cache = new Map();
this.capacity = capacity;
}

clear() {
this.cache = new Map();
}

get(key) {
if (!this.cache.has(key)) return null;

let val = this.cache.get(key);

this.cache.delete(key);
this.cache.set(key, val);

return val;
}

set(key, value) {
this.cache.delete(key);

if (this.cache.size === this.capacity) {
this.cache.delete(this.cache.keys().next().value);
this.cache.set(key, value);
} else {
this.cache.set(key, value);
}
}
}

module.exports = LRUCache;
24 changes: 22 additions & 2 deletions src/Range.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"use strict";

const LRUCache = require('./LRUCache.js');
const col_str_2_int = require('./col_str_2_int.js');
const int_2_col_str = require('./int_2_col_str.js');
const getSanitizedSheetName = require('./getSanitizedSheetName.js');

module.exports = function Range(str_expression, formula) {
const Cache = new LRUCache()

function Range(str_expression, formula) {
this.parse = function() {
var range_expression, sheet_name, sheet;
if (str_expression.indexOf('!') != -1) {
Expand Down Expand Up @@ -37,7 +40,8 @@ module.exports = function Range(str_expression, formula) {
max_col: max_col,
};
};
this.calc = function() {

this._calc = function() {
var results = this.parse();
var sheet_name = results.sheet_name;
var sheet = results.sheet;
Expand Down Expand Up @@ -84,4 +88,20 @@ module.exports = function Range(str_expression, formula) {
}
return matrix;
};

this.calc = function() {
const cached = Cache.get(str_expression);
if(cached) {
return cached;
}
else {
const result = this._calc();
Cache.set(str_expression, result);
return result;
}
}
};

Range.cache = Cache

module.exports = Range;
4 changes: 4 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const col_str_2_int = require('./col_str_2_int.js');
const exec_formula = require('./exec_formula.js');
const find_all_cells_with_formulas = require('./find_all_cells_with_formulas.js');
const Calculator = require('./Calculator.js');
const { cache: RangeCache } = require('./Range.js');

var mymodule = function(workbook, options) {
var formulas = find_all_cells_with_formulas(workbook, exec_formula);
Expand All @@ -24,6 +25,9 @@ var mymodule = function(workbook, options) {
}
}
}

// Clear out cache for next calculation
RangeCache.clear();
};

mymodule.calculator = function calculator(workbook) {
Expand Down
7 changes: 5 additions & 2 deletions test/1-basic-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,10 @@ describe('XLSX_CALC', function() {
workbook.Sheets.Sheet1.A3 = { t: 'n', v: 2 };
workbook.Sheets.Sheet1.B1 = { f: 'A1:A3' };
var exec_formula = require('../src/exec_formula.js'),
find_all_cells_with_formulas = require('../src/find_all_cells_with_formulas.js');
find_all_cells_with_formulas = require('../src/find_all_cells_with_formulas.js'),
cache = require('../src/Range.js').cache;
cache.clear();
Comment on lines +787 to +788
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache needs to be cleared for this test.


var formula = find_all_cells_with_formulas(workbook, exec_formula)[0];
var range = exec_formula.build_expression(formula).args[0].calc();
var expected = [
Expand Down Expand Up @@ -2252,7 +2255,7 @@ describe('XLSX_CALC', function() {
assert.equal(workbook.Sheets.Sheet1.B8.v, 6)
})
})

describe('INDEX', function () {
it('returns the value of an element in a matrix, selected by the row and column number indexes', function () {
workbook.Sheets.Sheet1.A1 = { v: 'Data' };
Expand Down
56 changes: 56 additions & 0 deletions test/9-lru-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use strict";
const LRUCache = require('../src/LRUCache.js');
const assert = require('assert');

describe('LRU cache', () => {
it('should return null if missing from cache', () => {
const cache = new LRUCache()
assert.equal(cache.get('key'), null);
});

it('should cache results', () => {
const cache = new LRUCache()
cache.set('key', 'value')
assert.equal(cache.get('key'), 'value');
});

it('should remove least recently used if at capacity', () => {
const cache = new LRUCache(2)
cache.set('key1', 'value1')
cache.set('key2', 'value2')
cache.set('key3', 'value3')

// assert keys
assert.equal(cache.get('key1'), null);
assert.equal(cache.get('key2'), 'value2');
assert.equal(cache.get('key3'), 'value3');
});


it('should update cache when accessed', () => {
const cache = new LRUCache(2)
cache.set('key1', 'value1')
cache.set('key2', 'value2')
// accessing key 1 to update recently used
cache.get('key1')

cache.set('key3', 'value3')

assert.equal(cache.get('key1'), 'value1');
assert.equal(cache.get('key2'), null);
assert.equal(cache.get('key3'), 'value3');
});

it('should empty cache when cleared', () => {
const cache = new LRUCache()
cache.set('key1', 'value1')
cache.set('key2', 'value2')
cache.set('key3', 'value3')

cache.clear();

assert.equal(cache.get('key1'), null);
assert.equal(cache.get('key2'), null);
assert.equal(cache.get('key3'), null);
});
})