Skip to content

Commit

Permalink
fix: trying to fix some heap problems
Browse files Browse the repository at this point in the history
  • Loading branch information
Tadeuchi committed Sep 27, 2023
1 parent fcc8c27 commit 6c49f1e
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 8 additions & 6 deletions src/__tests__/unit/evaluation-options.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -48,7 +50,7 @@ describe('Evaluation options evaluator', () => {
}).rootOptions
).toEqual({
allowBigInt: true,
cacheEveryNInteractions: -1,
cacheEveryNInteractions: defEvalOptions.cacheEveryNInteractions,
gasLimit: 3453453,
ignoreExceptions: true,
internalWrites: true,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -111,7 +113,7 @@ describe('Evaluation options evaluator', () => {
}).rootOptions
).toEqual({
allowBigInt: false,
cacheEveryNInteractions: -1,
cacheEveryNInteractions: defEvalOptions.cacheEveryNInteractions,
gasLimit: 2222,
ignoreExceptions: true,
internalWrites: false,
Expand Down Expand Up @@ -141,7 +143,7 @@ describe('Evaluation options evaluator', () => {
}).rootOptions
).toEqual({
allowBigInt: false,
cacheEveryNInteractions: -1,
cacheEveryNInteractions: defEvalOptions.cacheEveryNInteractions,
gasLimit: 2222,
ignoreExceptions: true,
internalWrites: true,
Expand Down Expand Up @@ -171,7 +173,7 @@ describe('Evaluation options evaluator', () => {
}).rootOptions
).toEqual({
allowBigInt: false,
cacheEveryNInteractions: -1,
cacheEveryNInteractions: defEvalOptions.cacheEveryNInteractions,
gasLimit: 2222,
ignoreExceptions: true,
internalWrites: true,
Expand Down
42 changes: 42 additions & 0 deletions src/common/SimpleLRUCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export class SimpleLRUCache<K, V> {
private readonly cache: Map<K, V>;
private readonly capacity: number;
constructor(capacity: number) {
this.cache = new Map<K, V>();
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());
}
}
2 changes: 2 additions & 0 deletions src/contract/Contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,4 +253,6 @@ export interface Contract<State = unknown> {
getStorageValues(keys: string[]): Promise<SortKeyCacheResult<Map<string, unknown>>>;

interactionState(): InteractionState;

clearChildren(): void;
}
7 changes: 7 additions & 0 deletions src/contract/HandlerBasedContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1043,4 +1043,11 @@ export class HandlerBasedContract<State> implements Contract<State> {

this.logger.debug('Tags with inner calls', tags);
}

clearChildren(): void {
for (const child of this._children) {
child.clearChildren();
}
this._children = [];
}
}
22 changes: 14 additions & 8 deletions src/contract/states/ContractInteractionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Map<string, EvalStateResult<unknown>>>();
private readonly _json = new Map<string, SimpleLRUCache<string, EvalStateResult<unknown>>>();
private readonly _initialJson = new Map<string, EvalStateResult<unknown>>();
private readonly _kv = new Map<string, SortKeyCache<unknown>>();

Expand All @@ -22,8 +23,8 @@ export class ContractInteractionState implements InteractionState {

getLessOrEqual(contractTxId: string, sortKey?: string): SortKeyCacheResult<EvalStateResult<unknown>> | 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);
}
Expand Down Expand Up @@ -61,7 +62,7 @@ export class ContractInteractionState implements InteractionState {
return storage.kvMap(sortKey, options);
}

async commit(interaction: GQLNodeInterface): Promise<void> {
async commit(interaction: GQLNodeInterface, forceStore = false): Promise<void> {
if (interaction.dry) {
await this.rollbackKVs();
return this.reset();
Expand All @@ -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();
Expand Down Expand Up @@ -103,7 +104,8 @@ export class ContractInteractionState implements InteractionState {

update(contractTxId: string, state: EvalStateResult<unknown>, sortKey: string): void {
if (!this._json.has(contractTxId)) {
this._json.set(contractTxId, new Map<string, EvalStateResult<unknown>>());
const cache = new SimpleLRUCache<string, EvalStateResult<unknown>>(10);
this._json.set(contractTxId, cache);
}
this._json.get(contractTxId).set(sortKey, state);
}
Expand All @@ -128,8 +130,12 @@ export class ContractInteractionState implements InteractionState {
this._kv.clear();
}

private async doStoreJson(states: Map<string, EvalStateResult<unknown>>, interaction: GQLNodeInterface) {
if (states.size > 1) {
private async doStoreJson(
states: Map<string, EvalStateResult<unknown>>,
interaction: GQLNodeInterface,
forceStore = false
) {
if (states.size > 1 || forceStore) {
for (const [k, v] of states) {
await this._warp.stateEvaluator.putInCache(k, interaction, v);
}
Expand Down
2 changes: 1 addition & 1 deletion src/contract/states/InteractionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
commit(interaction: GQLNodeInterface, forceStore?: boolean): Promise<void>;

commitKV(): Promise<void>;

Expand Down
7 changes: 6 additions & 1 deletion src/core/modules/impl/DefaultStateEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down

0 comments on commit 6c49f1e

Please sign in to comment.