Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
51 changes: 51 additions & 0 deletions packages/benchmark/memory-lru.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createBenchmark, getModuleName, printToConsole, generateAlphaNumeric } from "index.js";
import { CacheableMemory } from "cacheable";
import QuickLRU from 'quick-lru';
import { createLRU } from 'lru.min';

const bench = createBenchmark("Memory LRU Benchmark", 100000);

// Cacheable Memory
const cacheable = new CacheableMemory({lruSize: 80000 });
let cacheableName = getModuleName("Cacheable Memory", "1.10.0");

// QuickLRU
const quickLRU = new QuickLRU({maxSize: 80000});
let quickLRUName = getModuleName("quick-lru");

// lru.min
const lruMin = createLRU({ max: 80000 });
let lruMinName = getModuleName("lru.min");

// Map
const map = new Map<string, string>();
let mapName = getModuleName("Map", "22");

bench.add(`${cacheableName} - set / get`, async () => {
const alphaNumericData = generateAlphaNumeric();
cacheable.set(alphaNumericData.key, alphaNumericData.value);
cacheable.get(alphaNumericData.key);
});

bench.add(`${quickLRUName} - set / get`, async () => {
const alphaNumericData = generateAlphaNumeric();
quickLRU.set(alphaNumericData.key, alphaNumericData.value);
quickLRU.get(alphaNumericData.key);
});

bench.add(`${lruMinName} - set / get`, async () => {
const alphaNumericData = generateAlphaNumeric();
lruMin.set(alphaNumericData.key, alphaNumericData.value);
lruMin.get(alphaNumericData.key);
});

bench.add(`${mapName} - set / get`, async () => {
const alphaNumericData = generateAlphaNumeric();
map.set(alphaNumericData.key, alphaNumericData.value);
map.get(alphaNumericData.key);
});

await bench.run();

console.log(`*${bench.name} Results:*`);
printToConsole(bench);
49 changes: 29 additions & 20 deletions packages/benchmark/memory.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import { createBenchmark, getModuleName, printToConsole, generateAlphaNumeric } from "index.js";
import { CacheableMemory } from "cacheable";
import QuickLRU from 'quick-lru';
import { createLRU } from 'lru.min';
import NodeCache from 'node-cache';
import { BentoCache, bentostore } from 'bentocache';
import { memoryDriver } from 'bentocache/drivers/memory';

const bench = createBenchmark("Memory Benchmark", 5000);
const bench = createBenchmark("Memory Benchmark", 100000);

// Cacheable Memory
const cacheable = new CacheableMemory({ lruSize: 1000 });
let cacheableName = getModuleName("Cacheable", "1.8.9");

// QuickLRU
const quickLRU = new QuickLRU({maxSize: 1000});
let quickLRUName = getModuleName("quick-lru");

// lru.min
const lruMin = createLRU({ max: 1000 });
let lruMinName = getModuleName("lru.min");
const cacheable = new CacheableMemory();
let cacheableName = getModuleName("Cacheable Memory", "1.10.0");

// Node Cache
const nodeCache = new NodeCache();
let nodeCacheName = getModuleName("Node Cache");

// BentoCache with Memory Driver
const bento = new BentoCache({
default: 'myCache',
stores: {
// A first cache store named "myCache" using
// only L1 in-memory cache
myCache: bentostore()
.useL1Layer(memoryDriver({ maxSize: '10mb' }))
}
});
let bentoName = getModuleName("BentoCache");

// Map
const map = new Map<string, string>();
Expand All @@ -27,16 +36,16 @@ bench.add(`${cacheableName} - set / get`, async () => {
cacheable.get(alphaNumericData.key);
});

bench.add(`${quickLRUName} - set / get`, async () => {
bench.add(`${nodeCacheName} - set / get`, async () => {
const alphaNumericData = generateAlphaNumeric();
quickLRU.set(alphaNumericData.key, alphaNumericData.value);
quickLRU.get(alphaNumericData.key);
nodeCache.set(alphaNumericData.key, alphaNumericData.value);
nodeCache.get(alphaNumericData.key);
});

bench.add(`${lruMinName} - set / get`, async () => {
bench.add(`${bentoName} - set / get`, async () => {
const alphaNumericData = generateAlphaNumeric();
lruMin.set(alphaNumericData.key, alphaNumericData.value);
lruMin.get(alphaNumericData.key);
await bento.set({ key: alphaNumericData.key, value: alphaNumericData.value});
await bento.get({ key: alphaNumericData.key});
});

bench.add(`${mapName} - set / get`, async () => {
Expand All @@ -47,5 +56,5 @@ bench.add(`${mapName} - set / get`, async () => {

await bench.run();

console.log(`*${bench.name} Results:*`);
printToConsole(bench);

4 changes: 3 additions & 1 deletion packages/benchmark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@
"prepublish": "pnpm build",
"test": "echo 'no tests needed'",
"test:ci": "echo 'no tests needed'",
"benchmark:memory": "tsx ./memory.ts",
"benchmark:memory": "tsx ./memory.ts && tsx ./memory-lru.ts",
"clean": "rimraf ./dist ./coverage ./node_modules"
},
"dependencies": {
"@faker-js/faker": "^9.8.0",
"@monstermann/tinybench-pretty-printer": "^0.1.0",
"bentocache": "^1.4.0",
"cache-manager": "workspace:^",
"cacheable": "workspace:^",
"lru.min": "^1.1.2",
Expand All @@ -51,6 +52,7 @@
"license"
],
"devDependencies": {
"node-cache": "^5.1.2",
"tsx": "^4.19.4"
}
}
4 changes: 2 additions & 2 deletions packages/benchmark/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"module": "nodenext",
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */
"baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */

/* Emit */
Expand Down
116 changes: 112 additions & 4 deletions packages/cacheable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
* [Cacheable Statistics (Instance Only)](#cacheable-statistics-instance-only)
* [Cacheable - API](#cacheable---api)
* [CacheableMemory - In-Memory Cache](#cacheablememory---in-memory-cache)
* [CacheableMemory Store Hashing](#cacheablememory-store-hashing)
* [CacheableMemory LRU Feature](#cacheablememory-lru-feature)
* [CacheableMemory Performance](#cacheablememory-performance)
* [CacheableMemory Options](#cacheablememory-options)
* [CacheableMemory - API](#cacheablememory---api)
* [Keyv Storage Adapter - KeyvCacheableMemory](#keyv-storage-adapter---keyvcacheablememory)
Expand Down Expand Up @@ -387,12 +390,111 @@ This simple in-memory cache uses multiple Map objects and a with `expiration` an

By default we use lazy expiration deletion which means on `get` and `getMany` type functions we look if it is expired and then delete it. If you want to have a more aggressive expiration policy you can set the `checkInterval` property to a value greater than `0` which will check for expired keys at the interval you set.

Here are some of the main features of `CacheableMemory`:
* High performance in-memory cache with a robust API and feature set. 🚀
* Can scale past the `17 million keys` limit of a single `Map` via `hashStoreSize`. Default is `16` Map objects.
* LRU (Least Recently Used) cache feature to limit the number of keys in the cache via `lruSize`. Limit to `17 million keys` total.
* Expiration policy to delete expired keys with lazy deletion or aggressive deletion via `checkInterval`.
* `Wrap` feature to memoize `sync` and `async` functions with stampede protection.
* Ability to do many operations at once such as `setMany`, `getMany`, `deleteMany`, and `takeMany`.
* Supports `raw` data retrieval with `getRaw` and `getManyRaw` methods to get the full metadata of the cache entry.

## CacheableMemory Store Hashing

`CacheableMemory` uses `Map` objects to store the keys and values. To make this scale past the `17 million keys` limit of a single `Map` we use a hash to balance the data across multiple `Map` objects. This is done by hashing the key and using the hash to determine which `Map` object to use. The default hashing algorithm is `djb2Hash` but you can change it by setting the `storeHashAlgorithm` property in the options. By default we set the amount of `Map` objects to `16`.

NOTE: if you are using the LRU cache feature the `lruSize` no matter how many `Map` objects you have it will be limited to the `17 million keys` limit of a single `Map` object. This is because we use a double linked list to manage the LRU cache and it is not possible to have more than `17 million keys` in a single `Map` object.

Here is an example of how to set the number of `Map` objects and the hashing algorithm:

```javascript
import { CacheableMemory } from 'cacheable';
const cache = new CacheableMemory({
storeSize: 32, // set the number of Map objects to 32
});
cache.set('key', 'value');
const value = cache.get('key'); // value
```

Here is an example of how to use the `storeHashAlgorithm` property:

```javascript
import { CacheableMemory } from 'cacheable';
const cache = new CacheableMemory({ storeHashAlgorithm: 'sha256' });
cache.set('key', 'value');
const value = cache.get('key'); // value
```

If you want to provide your own hashing function you can set the `storeHashAlgorithm` property to a function that takes an object and returns a `number` that is in the range of the amount of `Map` stores you have.

```javascript
import { CacheableMemory } from 'cacheable';
/**
* Custom hash function that takes a key and the size of the store
* and returns a number between 0 and storeHashSize - 1.
* @param {string} key - The key to hash.
* @param {number} storeHashSize - The size of the store (number of Map objects).
* @returns {number} - A number between 0 and storeHashSize - 1.
*/
const customHash = (key, storeHashSize) => {
// custom hashing logic
return key.length % storeHashSize; // returns a number between 0 and 31 for 32 Map objects
};
const cache = new CacheableMemory({ storeHashAlgorithm: customHash, storeSize: 32 });
cache.set('key', 'value');
const value = cache.get('key'); // value
```

## CacheableMemory LRU Feature

You can enable the LRU (Least Recently Used) feature in `CacheableMemory` by setting the `lruSize` property in the options. This will limit the number of keys in the cache to the size you set. When the cache reaches the limit it will remove the least recently used keys from the cache. This is useful if you want to limit the memory usage of the cache.

When you set the `lruSize` we use a double linked list to manage the LRU cache and also set the `hashStoreSize` to `1` which means we will only use a single `Map` object for the LRU cache. This is because the LRU cache is managed by the double linked list and it is not possible to have more than `17 million keys` in a single `Map` object.

```javascript
import { CacheableMemory } from 'cacheable';
const cache = new CacheableMemory({ lruSize: 1 }); // sets the LRU cache size to 1000 keys and hashStoreSize to 1
cache.set('key1', 'value1');
cache.set('key2', 'value2');
const value1 = cache.get('key1');
console.log(value1); // undefined if the cache is full and key1 is the least recently used
const value2 = cache.get('key2');
console.log(value2); // value2 if key2 is still in the cache
console.log(cache.size()); // 1
```

NOTE: if you set the `lruSize` property to `0` after it was enabled the `hashStoreSize` will stay at `1` and the LRU cache will be disabled. This means that the cache will not limit the number of keys and will not remove any keys from the cache. If you want to expand the hash store size you can set the `hashStoreSize` property to a value greater than `1` to use multiple `Map` objects for the cache.

```javascript

## CacheableMemory Performance

Our goal with `cacheable` and `CacheableMemory` is to provide a high performance caching engine that is simple to use and has a robust API. We test it against other cacheing engines such that are less feature rich to make sure there is little difference. Here are some of the benchmarks we have run:

*Memory Benchmark Results:*
| name | summary | ops/sec | time/op | margin | samples |
|------------------------------------------|:---------:|----------:|----------:|:--------:|----------:|
| Map (v22) - set / get | 🥇 | 127K | 8µs | ±0.93% | 120K |
| Cacheable Memory (v1.10.0) - set / get | -1.3% | 125K | 8µs | ±1.17% | 118K |
| Node Cache - set / get | -4.8% | 121K | 9µs | ±1.24% | 114K |
| bentocache (v1.4.0) - set / get | -20% | 101K | 10µs | ±0.73% | 100K |

*Memory LRU Benchmark Results:*
| name | summary | ops/sec | time/op | margin | samples |
|------------------------------------------|:---------:|----------:|----------:|:--------:|----------:|
| Map (v22) - set / get | 🥇 | 127K | 8µs | ±1.30% | 120K |
| quick-lru (v7.0.1) - set / get | -0.3% | 126K | 8µs | ±0.87% | 120K |
| lru.min (v1.1.2) - set / get | -1.4% | 125K | 8µs | ±0.82% | 119K |
| Cacheable Memory (v1.10.0) - set / get | -5.5% | 120K | 9µs | ±0.83% | 113K |

## CacheableMemory Options

* `ttl`: The time to live for the cache in milliseconds. Default is `undefined` which is means indefinitely.
* `useClones`: If the cache should use clones for the values. Default is `true`.
* `lruSize`: The size of the LRU cache. Default is `0` which is unlimited.
* `checkInterval`: The interval to check for expired keys in milliseconds. Default is `0` which is disabled.
* `storeHashSize`: The number of `Map` objects to use for the cache. Default is `16`.
* `storeHashAlgorithm`: The hashing algorithm to use for the cache. Default is `djb2Hash`.

## CacheableMemory - API

Expand All @@ -410,13 +512,19 @@ By default we use lazy expiration deletion which means on `get` and `getMany` ty
* `takeMany([keys])`: Takes multiple values from the cache and deletes them.
* `wrap(function, WrapSyncOptions)`: Wraps a `sync` function in a cache.
* `clear()`: Clears the cache.
* `size()`: The number of keys in the cache.
* `keys()`: The keys in the cache.
* `items()`: The items in the cache as `CacheableStoreItem` example `{ key, value, expires? }`.
* `ttl`: The default time to live for the cache in milliseconds. Default is `undefined` which is disabled.
* `useClones`: If the cache should use clones for the values. Default is `true`.
* `lruSize`: The size of the LRU cache. Default is `0` which is unlimited.
* `size`: The number of keys in the cache.
* `checkInterval`: The interval to check for expired keys in milliseconds. Default is `0` which is disabled.
* `storeHashSize`: The number of `Map` objects to use for the cache. Default is `16`.
* `storeHashAlgorithm`: The hashing algorithm to use for the cache. Default is `djb2Hash`.
* `keys`: Get the keys in the cache. Not able to be set.
* `items`: Get the items in the cache as `CacheableStoreItem` example `{ key, value, expires? }`.
* `store`: The hash store for the cache which is an array of `Map` objects.
* `checkExpired()`: Checks for expired keys in the cache. This is used by the `checkInterval` property.
* `startIntervalCheck()`: Starts the interval check for expired keys if `checkInterval` is above 0 ms.
* `stopIntervalCheck()`: Stops the interval check for expired keys.
* `hash(object: any, algorithm = 'sha256'): string`: Hashes an object with the algorithm. Default is `sha256`.

# Keyv Storage Adapter - KeyvCacheableMemory

Expand Down
41 changes: 41 additions & 0 deletions packages/cacheable/src/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,44 @@ export function hash(object: any, algorithm = 'sha256'): string {
hasher.update(objectString);
return hasher.digest('hex');
}

export function hashToNumber(object: any, min = 0, max = 10, algorithm = 'sha256'): number {
// Convert the object to a string
const objectString = JSON.stringify(object);

// Check if the algorithm is supported
if (!crypto.getHashes().includes(algorithm)) {
throw new Error(`Unsupported hash algorithm: '${algorithm}'`);
}

// Create a hasher and update it with the object string
const hasher = crypto.createHash(algorithm);
hasher.update(objectString);

// Get the hash as a hexadecimal string
const hashHex = hasher.digest('hex');

// Convert the hex string to a number (base 16)
const hashNumber = Number.parseInt(hashHex, 16);

// Calculate the range size
const range = max - min + 1;

// Return a number within the specified range
return min + (hashNumber % range);
}

export function djb2Hash(string_: string, min = 0, max = 10): number {
// DJB2 hash algorithm
let hash = 5381;
for (let i = 0; i < string_.length; i++) {
// eslint-disable-next-line no-bitwise, unicorn/prefer-code-point
hash = (hash * 33) ^ string_.charCodeAt(i); // 33 is a prime multiplier
}

// Calculate the range size
const range = max - min + 1;

// Return a value within the specified range
return min + (Math.abs(hash) % range);
}
Loading