refactor: mem cache puts optimization

This commit is contained in:
ppedziwiatr
2021-11-05 19:31:32 +01:00
committed by Piotr Pędziwiatr
parent fd7a63db6d
commit 1f35fe6ce5
12 changed files with 16476 additions and 65 deletions

View File

@@ -40,7 +40,7 @@ describe.each(chunked)('.suite %#', (contracts: string[]) => {
const result2 = await SmartWeaveNodeFactory.memCached(arweave, 5) const result2 = await SmartWeaveNodeFactory.memCached(arweave, 5)
.contract(contractTxId) .contract(contractTxId)
.setEvaluationOptions({ .setEvaluationOptions({
fcpOptimization: true updateCacheForEachInteraction: false
}) })
.readState(); .readState();
const result2String = JSON.stringify(result2.state).trim(); const result2String = JSON.stringify(result2.state).trim();

View File

@@ -1,5 +1,5 @@
import { BlockHeightCacheResult, BlockHeightKey, BlockHeightSwCache } from '@smartweave/cache'; import { BlockHeightCacheResult, BlockHeightKey, BlockHeightSwCache } from '@smartweave/cache';
import { asc, deepCopy, desc } from '@smartweave/utils'; import { ascS, deepCopy, descS } from '@smartweave/utils';
import { LoggerFactory } from '@smartweave/logging'; import { LoggerFactory } from '@smartweave/logging';
/** /**
@@ -8,7 +8,9 @@ import { LoggerFactory } from '@smartweave/logging';
export class MemBlockHeightSwCache<V = any> implements BlockHeightSwCache<V> { export class MemBlockHeightSwCache<V = any> implements BlockHeightSwCache<V> {
private readonly logger = LoggerFactory.INST.create('MemBlockHeightSwCache'); private readonly logger = LoggerFactory.INST.create('MemBlockHeightSwCache');
protected storage: { [key: string]: Map<number, V> } = {}; // not using map here, as setting values in map seems to be slower
// then setting value for simple object - see tools/map-benchmark.ts
protected storage: { [contractId: string]: { [key: string]: V } } = {};
constructor(private maxStoredBlockHeights: number = Number.MAX_SAFE_INTEGER) {} constructor(private maxStoredBlockHeights: number = Number.MAX_SAFE_INTEGER) {}
@@ -21,15 +23,15 @@ export class MemBlockHeightSwCache<V = any> implements BlockHeightSwCache<V> {
return null; return null;
} }
const cached: Map<number, V> = this.storage[key]; const cached = this.storage[key];
// sort keys (ie. block heights) in asc order and get // sort keys (ie. block heights) in asc order and get
// the last element (ie. highest cached block height). // the last element (ie. highest cached block height).
const highestBlockHeight = [...cached.keys()].sort(asc).pop(); const highestBlockHeight = [...Object.keys(cached)].sort(ascS).pop();
return { return {
cachedHeight: highestBlockHeight, cachedHeight: +highestBlockHeight,
cachedValue: deepCopy(cached.get(highestBlockHeight)) cachedValue: deepCopy(cached[highestBlockHeight])
}; };
} }
@@ -38,32 +40,33 @@ export class MemBlockHeightSwCache<V = any> implements BlockHeightSwCache<V> {
return null; return null;
} }
const cached: Map<number, V> = this.storage[key]; const cached = this.storage[key];
// find first element in and desc-sorted keys array that is not higher than requested block height // find first element in and desc-sorted keys array that is not higher than requested block height
const highestBlockHeight = [...cached.keys()].sort(desc).find((cachedBlockHeight) => { const highestBlockHeight = [...Object.keys(cached)].sort(descS).find((cachedBlockHeight) => {
return cachedBlockHeight <= blockHeight; return +cachedBlockHeight <= blockHeight;
}); });
return highestBlockHeight === undefined return highestBlockHeight === undefined
? null ? null
: { : {
cachedHeight: highestBlockHeight, cachedHeight: +highestBlockHeight,
cachedValue: deepCopy(cached.get(highestBlockHeight)) cachedValue: deepCopy(cached[highestBlockHeight])
}; };
} }
async put({ cacheKey, blockHeight }: BlockHeightKey, value: V): Promise<void> { async put({ cacheKey, blockHeight }: BlockHeightKey, value: V): Promise<void> {
if (!(await this.contains(cacheKey))) { if (!(await this.contains(cacheKey))) {
this.storage[cacheKey] = new Map(); this.storage[cacheKey] = {};
} }
const cached = this.storage[cacheKey]; const cached = this.storage[cacheKey];
if (cached.size == this.maxStoredBlockHeights) { const cachedKeys = Object.keys(cached);
const toRemove = [...cached.keys()].sort(asc).shift(); if (cachedKeys.length == this.maxStoredBlockHeights) {
cached.delete(toRemove); const toRemove = [...cachedKeys].sort(ascS).shift();
delete cached[toRemove];
} }
cached.set(blockHeight, deepCopy(value)); cached['' + blockHeight] = value;
} }
async contains(key: string): Promise<boolean> { async contains(key: string): Promise<boolean> {
@@ -75,13 +78,13 @@ export class MemBlockHeightSwCache<V = any> implements BlockHeightSwCache<V> {
return null; return null;
} }
if (!this.storage[key].has(blockHeight)) { if (!this.storage[key].hasOwnProperty('' + blockHeight)) {
return null; return null;
} }
return { return {
cachedHeight: blockHeight, cachedHeight: blockHeight,
cachedValue: deepCopy(this.storage[key].get(blockHeight)) cachedValue: deepCopy(this.storage[key]['' + blockHeight])
}; };
} }
} }

View File

@@ -328,7 +328,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
]); ]);
this.logger.debug('contract and interactions load', benchmark.elapsed()); this.logger.debug('contract and interactions load', benchmark.elapsed());
sortedInteractions = await interactionsSorter.sort(interactions); sortedInteractions = await interactionsSorter.sort(interactions);
this.logger.debug('Sorted interactions', sortedInteractions); this.logger.trace('Sorted interactions', sortedInteractions);
handler = (await executorFactory.create(contractDefinition)) as HandlerApi<State>; handler = (await executorFactory.create(contractDefinition)) as HandlerApi<State>;
} else { } else {
this.logger.debug('State fully cached, not loading interactions.'); this.logger.debug('State fully cached, not loading interactions.');

View File

@@ -61,7 +61,10 @@ export interface StateEvaluator {
blockHeight: number blockHeight: number
): Promise<BlockHeightCacheResult<EvalStateResult<State>> | null>; ): Promise<BlockHeightCacheResult<EvalStateResult<State>> | null>;
transactionState<State>(transaction: GQLNodeInterface, contractTxId: string): Promise<EvalStateResult<State> | undefined>; transactionState<State>(
transaction: GQLNodeInterface,
contractTxId: string
): Promise<EvalStateResult<State> | undefined>;
} }
export class EvalStateResult<State> { export class EvalStateResult<State> {
@@ -69,7 +72,8 @@ export class EvalStateResult<State> {
readonly state: State, readonly state: State,
readonly validity: Record<string, boolean>, readonly validity: Record<string, boolean>,
readonly transactionId?: string, readonly transactionId?: string,
readonly blockId?: string) {} readonly blockId?: string
) {}
} }
export class DefaultEvaluationOptions implements EvaluationOptions { export class DefaultEvaluationOptions implements EvaluationOptions {

View File

@@ -148,32 +148,6 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
return null; return null;
} }
/*if (stateCache.cachedValue.length == 1) {
this.cLogger.debug('CacheValue size 1', stateCache.cachedValue.values().next().value);
return new BlockHeightCacheResult<EvalStateResult<State>>(
stateCache.cachedHeight,
stateCache.cachedValue.values().next().value
);
}
const sorter = new LexicographicalInteractionsSorter(this.arweave);
this.cLogger.debug('State cache', JSON.stringify(stateCache.cachedValue, mapReplacer));
const toSort = await Promise.all(
[...stateCache.cachedValue.values()].map(async (k) => {
return {
transactionId: k.transactionId,
sortKey: await sorter.createSortKey(k.blockId, k.transactionId, blockHeight)
};
})
);
const sorted = toSort.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
this.cLogger.debug('sorted:', sorted);
const lastKey = sorted.pop();
this.cLogger.debug('Last key: ', lastKey);*/
return new BlockHeightCacheResult<EvalStateResult<State>>( return new BlockHeightCacheResult<EvalStateResult<State>>(
stateCache.cachedHeight, stateCache.cachedHeight,
[...stateCache.cachedValue].pop() [...stateCache.cachedValue].pop()
@@ -198,7 +172,8 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
executionContext: ExecutionContext<State>, executionContext: ExecutionContext<State>,
state: EvalStateResult<State> state: EvalStateResult<State>
): Promise<void> { ): Promise<void> {
await this.putInCache(executionContext.contractDefinition.txId, transaction, state); //FIXME: https://github.com/redstone-finance/redstone-smartcontracts/issues/53
//await this.putInCache(executionContext.contractDefinition.txId, transaction, state);
} }
async transactionState<State>( async transactionState<State>(
@@ -228,10 +203,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
} }
const transactionId = transaction.id; const transactionId = transaction.id;
const blockHeight = transaction.block.height; const blockHeight = transaction.block.height;
this.cLogger.debug('putInCache:', {
state: state.state,
transactionId
});
const stateToCache = new EvalStateResult(state.state, state.validity, transactionId, transaction.block.id); const stateToCache = new EvalStateResult(state.state, state.validity, transactionId, transaction.block.id);
const stateCache = await this.cache.get(contractTxId, blockHeight); const stateCache = await this.cache.get(contractTxId, blockHeight);
if (stateCache != null) { if (stateCache != null) {

View File

@@ -65,7 +65,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
`Evaluating state for ${contractDefinition.txId} [${missingInteractions.length} non-cached of ${sortedInteractions.length} all]` `Evaluating state for ${contractDefinition.txId} [${missingInteractions.length} non-cached of ${sortedInteractions.length} all]`
); );
this.logger.debug('Base state:', baseState.state); this.logger.trace('Base state:', baseState.state);
let lastEvaluatedInteraction = null; let lastEvaluatedInteraction = null;
let errorMessage = null; let errorMessage = null;
@@ -215,7 +215,6 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
}); });
await this.onStateUpdate<State>(interactionTx, executionContext, new EvalStateResult(currentState, validity)); await this.onStateUpdate<State>(interactionTx, executionContext, new EvalStateResult(currentState, validity));
} }
// I'm really NOT a fan of this "modify" feature, but I don't have idea how to better // I'm really NOT a fan of this "modify" feature, but I don't have idea how to better

View File

@@ -15,7 +15,7 @@ export class LexicographicalInteractionsSorter implements InteractionsSorter {
return copy.sort((a, b) => a.sortKey.localeCompare(b.sortKey)); return copy.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
} }
private async addSortKey(txInfo: GQLEdgeInterface) { private async addSortKey(txInfo: GQLEdgeInterface) {
const { node } = txInfo; const { node } = txInfo;
txInfo.sortKey = await this.createSortKey(node.block.id, node.id, node.block.height); txInfo.sortKey = await this.createSortKey(node.block.id, node.id, node.block.height);

View File

@@ -29,8 +29,12 @@ export const mapReviver = (key: unknown, value: any) => {
export const asc = (a: number, b: number): number => a - b; export const asc = (a: number, b: number): number => a - b;
export const ascS = (a: string, b: string): number => +a - +b;
export const desc = (a: number, b: number): number => b - a; export const desc = (a: number, b: number): number => b - a;
export const descS = (a: string, b: string): number => +b - +a;
export function timeout(s: number): { timeoutId: number; timeoutPromise: Promise<any> } { export function timeout(s: number): { timeoutId: number; timeoutPromise: Promise<any> } {
let timeoutId = null; let timeoutId = null;
const timeoutPromise = new Promise((resolve, reject) => { const timeoutPromise = new Promise((resolve, reject) => {

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { SmartWeaveWebFactory } from '../src/core/web/SmartWeaveWebFactory'; import { SmartWeaveWebFactory } from '../src/core/web/SmartWeaveWebFactory';
import { FromFileInteractionsLoader } from './FromFileInteractionsLoader'; import { FromFileInteractionsLoader } from './FromFileInteractionsLoader';
import { readContract } from 'smartweave';
async function main() { async function main() {
LoggerFactory.use(new TsLogFactory()); LoggerFactory.use(new TsLogFactory());
@@ -20,13 +19,18 @@ async function main() {
logging: false // Enable network request logging logging: false // Enable network request logging
}); });
const contractTxId = 'Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY'; const contractTxId = 'w27141UQGgrCFhkiw9tL7A0-qWMQjbapU3mq2TfI4Cg';
const interactionsLoader = new FromFileInteractionsLoader(path.join(__dirname, 'data', 'interactions.json')); const interactionsLoader = new FromFileInteractionsLoader(path.join(__dirname, 'data', 'interactions.json'));
const smartweave = SmartWeaveWebFactory.memCachedBased(arweave).setInteractionsLoader(interactionsLoader).build(); const smartweave = SmartWeaveWebFactory.memCachedBased(arweave, 1)
.build();
const lootContract = smartweave.contract(contractTxId); const lootContract = smartweave
.contract(contractTxId)
.setEvaluationOptions({
updateCacheForEachInteraction: false
});
const { state, validity } = await lootContract.readState(); const { state, validity } = await lootContract.readState();
@@ -35,10 +39,6 @@ async function main() {
//fs.writeFileSync(path.join(__dirname, 'data', 'validity_old.json'), JSON.stringify(result.validity)); //fs.writeFileSync(path.join(__dirname, 'data', 'validity_old.json'), JSON.stringify(result.validity));
fs.writeFileSync(path.join(__dirname, 'data', 'state.json'), JSON.stringify(state)); fs.writeFileSync(path.join(__dirname, 'data', 'state.json'), JSON.stringify(state));
console.log('second read');
await lootContract.readState();
} }
main().catch((e) => console.error(e)); main().catch((e) => console.error(e));

27
tools/map-benchmark.ts Normal file
View File

@@ -0,0 +1,27 @@
/* eslint-disable */
import Arweave from 'arweave';
import { LoggerFactory } from '../src';
import { TsLogFactory } from '../src/logging/node/TsLogFactory';
async function main() {
LoggerFactory.use(new TsLogFactory());
LoggerFactory.INST.logLevel('debug');
console.time('benchmark_map');
const map = new Map();
for (let i = 0; i < 10_000_000; i++) {
map.set(i, 'duh');
}
console.timeEnd('benchmark_map');
console.time('benchmark_object');
const o = {};
for (let i = 0; i < 10_000_000; i++) {
o[i] = 'duh';
}
console.timeEnd('benchmark_object');
console.log(Object.keys(o).length);
}
main().catch((e) => console.error(e));