diff --git a/src/__tests__/integration/internal-writes/internal-write-callee.test.ts b/src/__tests__/integration/internal-writes/internal-write-callee.test.ts index f9bd4373..e45a145f 100644 --- a/src/__tests__/integration/internal-writes/internal-write-callee.test.ts +++ b/src/__tests__/integration/internal-writes/internal-write-callee.test.ts @@ -278,10 +278,14 @@ describe('Testing internal writes', () => { }); it('should write to cache at the end of interaction (if interaction with other contracts)', async () => { - await calleeContract.writeInteraction({ function: 'addAndWrite', contractId: callingContract.txId(), amount: 1 }); await calleeContract.writeInteraction({ function: 'add' }); await mineBlock(warp); + // this writeInteraction will cause the state with the previous 'add' to be added the cache + // - hence the 7 (not 6) entries in the cache at the end of this test + await calleeContract.writeInteraction({ function: 'addAndWrite', contractId: callingContract.txId(), amount: 1 }); + await mineBlock(warp); + await calleeContract.readState(); const entries2 = await currentContractEntries(calleeContract.txId()); expect(entries2.length).toEqual(7); diff --git a/src/__tests__/unit/evaluation-options.test.ts b/src/__tests__/unit/evaluation-options.test.ts index 97e266e9..f610be7d 100644 --- a/src/__tests__/unit/evaluation-options.test.ts +++ b/src/__tests__/unit/evaluation-options.test.ts @@ -1,16 +1,18 @@ import { EvaluationOptionsEvaluator } from '../../contract/EvaluationOptionsEvaluator'; import { WarpFactory } from '../../core/WarpFactory'; import { SourceType } from '../../core/modules/impl/WarpGatewayInteractionsLoader'; +import { DefaultEvaluationOptions } from '../../core/modules/StateEvaluator'; describe('Evaluation options evaluator', () => { const warp = WarpFactory.forLocal(); it('should properly set root evaluation options', async () => { const contract = warp.contract(null); + const defEvalOptions = new DefaultEvaluationOptions(); expect(new EvaluationOptionsEvaluator(contract.evaluationOptions(), {}).rootOptions).toEqual({ allowBigInt: false, - cacheEveryNInteractions: -1, + cacheEveryNInteractions: defEvalOptions.cacheEveryNInteractions, gasLimit: 9007199254740991, ignoreExceptions: true, internalWrites: false, @@ -48,7 +50,7 @@ describe('Evaluation options evaluator', () => { }).rootOptions ).toEqual({ allowBigInt: true, - cacheEveryNInteractions: -1, + cacheEveryNInteractions: defEvalOptions.cacheEveryNInteractions, gasLimit: 3453453, ignoreExceptions: true, internalWrites: true, @@ -81,7 +83,7 @@ describe('Evaluation options evaluator', () => { expect(new EvaluationOptionsEvaluator(contract2.evaluationOptions(), {}).rootOptions).toEqual({ allowBigInt: false, - cacheEveryNInteractions: -1, + cacheEveryNInteractions: defEvalOptions.cacheEveryNInteractions, gasLimit: 2222, ignoreExceptions: true, internalWrites: true, @@ -111,7 +113,7 @@ describe('Evaluation options evaluator', () => { }).rootOptions ).toEqual({ allowBigInt: false, - cacheEveryNInteractions: -1, + cacheEveryNInteractions: defEvalOptions.cacheEveryNInteractions, gasLimit: 2222, ignoreExceptions: true, internalWrites: false, @@ -141,7 +143,7 @@ describe('Evaluation options evaluator', () => { }).rootOptions ).toEqual({ allowBigInt: false, - cacheEveryNInteractions: -1, + cacheEveryNInteractions: defEvalOptions.cacheEveryNInteractions, gasLimit: 2222, ignoreExceptions: true, internalWrites: true, @@ -171,7 +173,7 @@ describe('Evaluation options evaluator', () => { }).rootOptions ).toEqual({ allowBigInt: false, - cacheEveryNInteractions: -1, + cacheEveryNInteractions: defEvalOptions.cacheEveryNInteractions, gasLimit: 2222, ignoreExceptions: true, internalWrites: true, diff --git a/src/common/SimpleLRUCache.ts b/src/common/SimpleLRUCache.ts new file mode 100644 index 00000000..cb46b4ae --- /dev/null +++ b/src/common/SimpleLRUCache.ts @@ -0,0 +1,42 @@ +export class SimpleLRUCache { + private readonly cache: Map; + private readonly capacity: number; + constructor(capacity: number) { + this.cache = new Map(); + this.capacity = capacity || 10; + } + + has(key: K) { + return this.cache.has(key); + } + + size(): number { + return this.cache.size; + } + + get(key: K): V { + if (!this.cache.has(key)) return null; + + const val = this.cache.get(key); + + this.cache.delete(key); + this.cache.set(key, val); + + return val; + } + + set(key: K, value: V) { + 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); + } + } + + keys(): K[] { + return Array.from(this.cache.keys()); + } +} diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index f57b03c9..bf698589 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -253,4 +253,6 @@ export interface Contract { getStorageValues(keys: string[]): Promise>>; interactionState(): InteractionState; + + clearChildren(): void; } diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 8f4b4545..e5967dcb 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -1043,4 +1043,11 @@ export class HandlerBasedContract implements Contract { this.logger.debug('Tags with inner calls', tags); } + + clearChildren(): void { + for (const child of this._children) { + child.clearChildren(); + } + this._children = []; + } } diff --git a/src/contract/states/ContractInteractionState.ts b/src/contract/states/ContractInteractionState.ts index 0af3f405..cc871c84 100644 --- a/src/contract/states/ContractInteractionState.ts +++ b/src/contract/states/ContractInteractionState.ts @@ -4,9 +4,10 @@ import { EvalStateResult } from '../../core/modules/StateEvaluator'; import { GQLNodeInterface } from '../../legacy/gqlResult'; import { Warp } from '../../core/Warp'; import { SortKeyCacheRangeOptions } from '../../cache/SortKeyCacheRangeOptions'; +import { SimpleLRUCache } from '../../common/SimpleLRUCache'; export class ContractInteractionState implements InteractionState { - private readonly _json = new Map>>(); + private readonly _json = new Map>>(); private readonly _initialJson = new Map>(); private readonly _kv = new Map>(); @@ -22,8 +23,8 @@ export class ContractInteractionState implements InteractionState { getLessOrEqual(contractTxId: string, sortKey?: string): SortKeyCacheResult> | null { const states = this._json.get(contractTxId); - if (states != null && states.size > 0) { - let keys = Array.from(states.keys()); + if (states != null && states.size() > 0) { + let keys = states.keys(); if (sortKey) { keys = keys.filter((k) => k.localeCompare(sortKey) <= 0); } @@ -61,7 +62,7 @@ export class ContractInteractionState implements InteractionState { return storage.kvMap(sortKey, options); } - async commit(interaction: GQLNodeInterface): Promise { + async commit(interaction: GQLNodeInterface, forceStore = false): Promise { if (interaction.dry) { await this.rollbackKVs(); return this.reset(); @@ -74,7 +75,7 @@ export class ContractInteractionState implements InteractionState { latestState.set(k, state.cachedValue); } }); - await this.doStoreJson(latestState, interaction); + await this.doStoreJson(latestState, interaction, forceStore); await this.commitKVs(); } finally { this.reset(); @@ -103,7 +104,8 @@ export class ContractInteractionState implements InteractionState { update(contractTxId: string, state: EvalStateResult, sortKey: string): void { if (!this._json.has(contractTxId)) { - this._json.set(contractTxId, new Map>()); + const cache = new SimpleLRUCache>(10); + this._json.set(contractTxId, cache); } this._json.get(contractTxId).set(sortKey, state); } @@ -128,8 +130,12 @@ export class ContractInteractionState implements InteractionState { this._kv.clear(); } - private async doStoreJson(states: Map>, interaction: GQLNodeInterface) { - if (states.size > 1) { + private async doStoreJson( + states: Map>, + interaction: GQLNodeInterface, + forceStore = false + ) { + if (states.size > 1 || forceStore) { for (const [k, v] of states) { await this._warp.stateEvaluator.putInCache(k, interaction, v); } diff --git a/src/contract/states/InteractionState.ts b/src/contract/states/InteractionState.ts index 85f01d20..cbae210b 100644 --- a/src/contract/states/InteractionState.ts +++ b/src/contract/states/InteractionState.ts @@ -28,7 +28,7 @@ export interface InteractionState { * Called by the {@link DefaultStateEvaluator} at the end every root's contract interaction evaluation * - IFF the result.type == 'ok'. */ - commit(interaction: GQLNodeInterface): Promise; + commit(interaction: GQLNodeInterface, forceStore?: boolean): Promise; commitKV(): Promise; diff --git a/src/core/modules/impl/DefaultStateEvaluator.ts b/src/core/modules/impl/DefaultStateEvaluator.ts index 144b5b6c..d480e034 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -313,11 +313,16 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { // if that's the end of the root contract's interaction - commit all the uncommitted states to cache. if (contract.isRoot()) { + contract.clearChildren(); // update the uncommitted state of the root contract if (lastConfirmedTxState) { contract.interactionState().update(contract.txId(), lastConfirmedTxState.state, lastConfirmedTxState.tx.sortKey); if (validity[missingInteraction.id]) { - await contract.interactionState().commit(missingInteraction); + let forceStateStoreToCache = false; + if (executionContext.evaluationOptions.cacheEveryNInteractions > 0) { + forceStateStoreToCache = i % executionContext.evaluationOptions.cacheEveryNInteractions === 0; + } + await contract.interactionState().commit(missingInteraction, forceStateStoreToCache); } else { await contract.interactionState().rollback(missingInteraction); }