feat: stx addr encoding LRU cache

This commit is contained in:
Matthew Little
2021-11-16 12:39:25 +01:00
committed by GitHub
parent aaafb5ae2f
commit 285632a983
8 changed files with 217 additions and 4 deletions

3
.env
View File

@@ -84,3 +84,6 @@ MAINNET_SEND_MANY_CONTRACT_ID=SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.send-man
# IMGIX_DOMAIN=https://<your domain>.imgix.net
# IMGIX_TOKEN=<your token>
# Specify max number of STX address to store in an in-memory LRU cache (CPU optimization).
# Defaults to 50,000, which should result in around 25 megabytes of additional memory usage.
# STACKS_ADDRESS_CACHE_SIZE=10000

9
package-lock.json generated
View File

@@ -2489,6 +2489,11 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
"@types/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw=="
},
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -12436,7 +12441,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"requires": {
"yallist": "^4.0.0"
}
@@ -18025,8 +18029,7 @@
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yaml": {
"version": "1.10.2",

View File

@@ -108,6 +108,7 @@
"@stacks/transactions": "^v2.0.1",
"@types/dockerode": "^2.5.34",
"@types/express-list-endpoints": "^4.0.1",
"@types/lru-cache": "^5.1.1",
"@types/ws": "^7.2.5",
"big-integer": "^1.6.48",
"bignumber.js": "^9.0.1",
@@ -131,6 +132,7 @@
"http-proxy-middleware": "^2.0.1",
"jsonc-parser": "^3.0.0",
"jsonrpc-lite": "^2.1.0",
"lru-cache": "^6.0.0",
"micro-base58": "^0.5.0",
"nock": "^13.1.1",
"node-fetch": "^2.6.0",

90
src/c32-addr-cache.ts Normal file
View File

@@ -0,0 +1,90 @@
/*
This module is hacky and does things that should generally be avoided in this codebase. We are using a procedure to
"re-export" a function on an existing module in order to add some functionality specific to the API. In this case,
the `c32check.c32address` function is difficult to override within this codebase. Many entry points into the function
are from calls into stacks.js libs. A cleaner solution would involve implementing optional params to many stacks.js
functions to provide a stx address encoding function. However, that would be a significant change to the stacks.js libs,
and for now this approach is much easier and faster.
*/
import * as c32check from 'c32check';
import * as LruCache from 'lru-cache';
type c32AddressFn = typeof c32check.c32address;
const MAX_ADDR_CACHE_SIZE = 50_000;
export const ADDR_CACHE_ENV_VAR = 'STACKS_ADDRESS_CACHE_SIZE';
let addressLruCache: LruCache<string, string> | undefined;
export function getAddressLruCache() {
if (addressLruCache === undefined) {
let cacheSize = MAX_ADDR_CACHE_SIZE;
const envAddrCacheVar = process.env[ADDR_CACHE_ENV_VAR];
if (envAddrCacheVar) {
cacheSize = Number.parseInt(envAddrCacheVar);
}
addressLruCache = new LruCache<string, string>({ max: cacheSize });
}
return addressLruCache;
}
const c32EncodeInjectedSymbol = Symbol();
let origC32AddressProp: PropertyDescriptor | undefined;
let origC32AddressFn: c32AddressFn | undefined;
function createC32AddressCache(origFn: c32AddressFn): c32AddressFn {
const c32addressCached: c32AddressFn = (version, hash160hex) => {
const cacheKey = `${version}${hash160hex}`;
const addrCache = getAddressLruCache();
let addrVal = addrCache.get(cacheKey);
if (addrVal === undefined) {
addrVal = origFn(version, hash160hex);
addrCache.set(cacheKey, addrVal);
}
return addrVal;
};
Object.defineProperty(c32addressCached, c32EncodeInjectedSymbol, { value: true });
return c32addressCached;
}
/**
* Override the `c32address` function on the `c32check` module to use an LRU cache
* where commonly used encoded address strings can be cached.
*/
export function injectC32addressEncodeCache() {
// Skip if already injected
if (c32EncodeInjectedSymbol in c32check.c32address) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const c32checkModule = require('c32check');
const origProp = Object.getOwnPropertyDescriptor(c32checkModule, 'c32address');
if (!origProp) {
throw new Error(`Could not get property descriptor for 'c32address' on module 'c32check'`);
}
origC32AddressProp = origProp;
const origFn = origProp.get?.();
if (!origFn) {
throw new Error(`Falsy result for 'c32address' property getter on 'c32check' module`);
}
origC32AddressFn = origFn;
const newFn = createC32AddressCache(origFn);
// The exported module object specifies a property with a getter and setter (rather than simple indexer value),
// so use `defineProperty` to work around errors from trying to set/re-define the property with `c32checkModule.c32address = newFn`.
Object.defineProperty(c32checkModule, 'c32address', { get: () => newFn });
}
export function restoreC32AddressModule() {
if (addressLruCache !== undefined) {
addressLruCache.reset();
addressLruCache = undefined;
}
if (origC32AddressProp !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const c32checkModule = require('c32check');
Object.defineProperty(c32checkModule, 'c32address', origC32AddressProp);
origC32AddressProp = undefined;
}
}

View File

@@ -31,11 +31,14 @@ import * as getopts from 'getopts';
import * as fs from 'fs';
import * as path from 'path';
import * as net from 'net';
import { injectC32addressEncodeCache } from './c32-addr-cache';
loadDotEnv();
sourceMapSupport.install({ handleUncaughtExceptions: false });
injectC32addressEncodeCache();
registerShutdownConfig();
async function monitorCoreRpcConnection(): Promise<void> {

View File

@@ -1,4 +1,6 @@
import * as c32check from 'c32check';
import * as c32AddrCache from '../c32-addr-cache';
import { ADDR_CACHE_ENV_VAR } from '../c32-addr-cache';
import { getCurrentGitTag, has0xPrefix, isValidBitcoinAddress } from '../helpers';
test('get git tag', () => {
@@ -16,6 +18,52 @@ describe('has0xPrefix()', () => {
});
});
test('c32address lru caching', () => {
c32AddrCache.restoreC32AddressModule();
const origAddrCacheEnvVar = process.env[ADDR_CACHE_ENV_VAR];
process.env[ADDR_CACHE_ENV_VAR] = '5';
try {
// No LRU cache used for c32address fn
expect(c32AddrCache.getAddressLruCache().itemCount).toBe(0);
const stxAddr1 = 'SP2JKEZC09WVMR33NBSCWQAJC5GS590RP1FR9CK55';
const decodedAddr1 = c32check.c32addressDecode(stxAddr1);
const encodeResult1 = c32check.c32address(decodedAddr1[0], decodedAddr1[1]);
expect(encodeResult1).toBe(stxAddr1);
expect(c32AddrCache.getAddressLruCache().itemCount).toBe(0);
// Inject LRU cache into c32address fn, ensure it gets used
c32AddrCache.injectC32addressEncodeCache();
expect(c32AddrCache.getAddressLruCache().max).toBe(5);
const encodeResult2 = c32check.c32address(decodedAddr1[0], decodedAddr1[1]);
expect(encodeResult2).toBe(stxAddr1);
expect(c32AddrCache.getAddressLruCache().itemCount).toBe(1);
const encodeResult3 = c32check.c32address(decodedAddr1[0], decodedAddr1[1]);
expect(encodeResult3).toBe(stxAddr1);
expect(c32AddrCache.getAddressLruCache().itemCount).toBe(1);
// Test max cache size
c32AddrCache.getAddressLruCache().reset();
for (let i = 1; i < 10; i++) {
// hash160 hex string
const buff = Buffer.alloc(20);
buff[i] = i;
c32check.c32address(1, buff.toString('hex'));
expect(c32AddrCache.getAddressLruCache().itemCount).toBe(Math.min(i, 5));
}
// Sanity check: reset c32 lib to original state, ensure no LRU cache used
c32AddrCache.restoreC32AddressModule();
const encodeResult4 = c32check.c32address(decodedAddr1[0], decodedAddr1[1]);
expect(encodeResult4).toBe(stxAddr1);
expect(c32AddrCache.getAddressLruCache().itemCount).toBe(0);
} finally {
process.env[ADDR_CACHE_ENV_VAR] = origAddrCacheEnvVar;
c32AddrCache.restoreC32AddressModule();
}
});
test('bitcoin<->stacks address', () => {
const mainnetStxAddr = 'SP2JKEZC09WVMR33NBSCWQAJC5GS590RP1FR9CK55';
const mainnetBtcAddr = '1G4ayBXJvxZMoZpaNdZG6VyWwWq2mHpMjQ';

View File

@@ -6,7 +6,8 @@
"scripts": {
"build": "rimraf ./lib && npm run build:node",
"build:node": "tsc",
"start": "node ./lib/utils/src/index.js"
"start": "node ./lib/utils/src/index.js",
"address-cache-test": "npm run build && NODE_ENV=production node --expose-gc ./lib/utils/src/addr-lru-cache-test.js"
},
"prettier": "@stacks/prettier-config",
"dependencies": {

View File

@@ -0,0 +1,63 @@
import * as util from 'util';
import * as assert from 'assert';
import * as c32check from 'c32check';
import * as c32AddrCache from '../../src/c32-addr-cache';
if (!global.gc) {
throw new Error('Enable --expose-gc');
}
const iters = 500_000;
process.env[c32AddrCache.ADDR_CACHE_ENV_VAR] = iters.toString();
c32AddrCache.injectC32addressEncodeCache();
const buff = Buffer.alloc(20);
c32check.c32address(1, buff.toString('hex'));
const startMemory = process.memoryUsage();
const startRss = startMemory.rss;
const startMemoryStr = util.inspect(startMemory);
for (let i = 0; i < iters; i++) {
// hash160 hex string
buff.writeInt32LE(i);
c32check.c32address(1, buff.toString('hex'));
}
global.gc();
const endMemory = process.memoryUsage();
const endRss = endMemory.rss;
const endMemoryStr = util.inspect(endMemory);
console.log('Start memory', startMemoryStr);
console.log('End memory', endMemoryStr);
assert.equal(c32AddrCache.getAddressLruCache().itemCount, iters);
const rn = (num: number) => Math.round(num * 100) / 100;
const megabytes = (bytes: number) => rn(bytes / 1024 / 1024);
const byteDiff = (endRss - startRss) / (iters / 10_000);
console.log(`Start RSS: ${megabytes(startRss)}, end RSS: ${megabytes(endRss)}`);
console.log(`Around ${megabytes(byteDiff)} megabytes per 10k cache entries`);
/*
Several rounds of running this benchmark show "Around 4.44 megabytes per 10k cache entries":
Start memory {
rss: 26202112,
heapTotal: 5578752,
heapUsed: 3642392,
external: 1147316,
arrayBuffers: 59931
}
End memory {
rss: 259125248,
heapTotal: 216875008,
heapUsed: 181636328,
external: 1261038,
arrayBuffers: 18090
}
Start RSS: 24.99, end RSS: 247.12
*/