Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: unnecessary call to arweave.networkInfo when block height is re… #71

Merged
merged 5 commits into from
Jan 3, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"ts-node": "^10.2.1",
"tsc-alias": "1.3.10",
"tsconfig-paths": "^3.10.1",
"typescript": "4.5.2"
"typescript": "4.5.2",
"safe-stable-stringify": "2.3.1"
}
}
10 changes: 6 additions & 4 deletions src/__tests__/regression/read-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { interactRead, readContract } from 'smartweave';
import Arweave from 'arweave';
import { LoggerFactory, SmartWeaveNodeFactory } from '@smartweave';

const stringify = require('safe-stable-stringify')

function* chunks(arr, n) {
for (let i = 0; i < arr.length; i += n) {
// note: wrapping with an array to make it compatible with describe.each
Expand Down Expand Up @@ -34,10 +36,10 @@ describe.each(chunked)('.suite %#', (contracts: string[]) => {
async (contractTxId: string) => {
console.log('readContract', contractTxId);
const result = await readContract(arweave, contractTxId);
const resultString = JSON.stringify(result).trim();
const resultString = stringify(result).trim();
console.log('readState', contractTxId);
const result2 = await SmartWeaveNodeFactory.memCached(arweave, 1).contract(contractTxId).readState();
const result2String = JSON.stringify(result2.state).trim();
const result2String = stringify(result2.state).trim();
expect(result2String).toEqual(resultString);
},
600000
Expand All @@ -49,13 +51,13 @@ describe('readState', () => {
const contractTxId = 'CbGCxBJn6jLeezqDl1w3o8oCSeRCb-MmtZNKPodla-0';
const blockHeight = 707892;
const result = await readContract(arweave, contractTxId, blockHeight);
const resultString = JSON.stringify(result).trim();
const resultString = stringify(result).trim();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the t9T7DIOGxx4VWXoCEeYYarFYeERTpWIC1V3y-BPZgKE contract the JSON.stringify turned to be non-deterministic.


const result2 = await SmartWeaveNodeFactory.memCached(arweave, 1)
.contract(contractTxId)
.setEvaluationOptions({ updateCacheForEachInteraction: false })
.readState(blockHeight);
const result2String = JSON.stringify(result2.state).trim();
const result2String = stringify(result2.state).trim();

expect(result2String).toEqual(resultString);
});
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/regression/test-cases/read-state.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[
"Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY",
"t9T7DIOGxx4VWXoCEeYYarFYeERTpWIC1V3y-BPZgKE",
"5pSyVjFI07z8mbLeQhYBMsQ4M_MPidXIGX6T77rnF2s",
"-8A6RexFkpfWwuyVO98wzSFZh0d6VJuI-buTJvlwOJQ",
"AVTqjPQGCCXim7Nl_gn3HMjE4k0Zi_eTFRJCNEVXZxw",
Expand Down Expand Up @@ -86,7 +87,6 @@
"2caOW6ol9T8LHCMO8tVAx4GHRwv1q3fFc79KzKOtoww",
"HdZBZa0GfOEUYCubwvoSyxGUUgPmgy7RJb5l77T21bE",
"7Dp5r-UpZLDvHqsDbZbqWhCBwbYdJMKBuC3tFC-FF7U",
"YLVpmhSq5JmLltfg6R-5fL04rIRPrlSU22f6RQ6VyYE",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing. Slowing down the whole SDK just because of this one, abandoned contract, is not very wise :-)
original issue: ArweaveTeam/SmartWeave#92

"EOtTxCGktZe_J2DTM0D5h04crjlpjgeygA1R6Pmo_qM",
"92Tq6BKm6pvVkKW8_6Fb13QWTdUzBRLnG9scMBNWYZ4",
"w27141UQGgrCFhkiw9tL7A0-qWMQjbapU3mq2TfI4Cg",
Expand Down
2 changes: 1 addition & 1 deletion src/contract/Contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export interface Contract<State = unknown> {
* "root" contract network info - so that the whole execution is performed with the
* same network info)
*/
getNetworkInfo(): NetworkInfoInterface;
getNetworkInfo(): Partial<NetworkInfoInterface>;

/**
* Get the block height requested by user for the given interaction with contract
Expand Down
34 changes: 24 additions & 10 deletions src/contract/HandlerBasedContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
* Only the 'root' contract call should read this data from Arweave - all the inner calls ("child" contracts)
* should reuse this data from the parent ("calling") contract.
*/
private _networkInfo?: NetworkInfoInterface = null;
private _networkInfo?: Partial<NetworkInfoInterface> = null;

private _rootBlockHeight: number = null;

Expand Down Expand Up @@ -98,21 +98,27 @@ export class HandlerBasedContract<State> implements Contract<State> {
contractTxId: this._contractTxId,
currentTx
});
const initBenchmark = Benchmark.measure();
this.maybeResetRootContract(blockHeight);

const { stateEvaluator } = this.smartweave;
const benchmark = Benchmark.measure();
const executionContext = await this.createExecutionContext(this._contractTxId, blockHeight);
this.logger.info('Execution Context', {
blockHeight: executionContext.blockHeight,
srcTxId: executionContext.contractDefinition?.srcTxId,
missingInteractions: executionContext.sortedInteractions.length,
cachedStateHeight: executionContext.cachedState?.cachedHeight
});
this.logger.debug('context', benchmark.elapsed());
benchmark.reset();
initBenchmark.stop();

const stateBenchmark = Benchmark.measure();
const result = await stateEvaluator.eval(executionContext, currentTx || []);
this.logger.debug('state', benchmark.elapsed());
stateBenchmark.stop();

this.logger.info('Benchmark', {
'init time': initBenchmark.elapsed(),
'evaluation time': stateBenchmark.elapsed()
});
return result as EvalStateResult<State>;
}

Expand Down Expand Up @@ -232,7 +238,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
return this._callStack;
}

getNetworkInfo(): NetworkInfoInterface {
getNetworkInfo(): Partial<NetworkInfoInterface> {
return this._networkInfo;
}

Expand Down Expand Up @@ -281,9 +287,15 @@ export class HandlerBasedContract<State> implements Contract<State> {
const benchmark = Benchmark.measure();
// if this is a "root" call (ie. original call from SmartWeave's client)
if (this._parentContract == null) {
this.logger.debug('Reading network info for root call');
currentNetworkInfo = await arweave.network.getInfo();
this._networkInfo = currentNetworkInfo;
if (blockHeight) {
this._networkInfo = {
height: blockHeight
};
} else {
this.logger.debug('Reading network info for root call');
currentNetworkInfo = await arweave.network.getInfo();
this._networkInfo = currentNetworkInfo;
}
} else {
// if that's a call from within contract's source code
this.logger.debug('Reusing network info from the calling contract');
Expand All @@ -300,7 +312,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
blockHeight = currentNetworkInfo.height;
}
this.logger.debug('network info', benchmark.elapsed());

benchmark.reset();

const cachedState = await stateEvaluator.latestAvailableState<State>(contractTxId, blockHeight);
Expand All @@ -309,6 +320,9 @@ export class HandlerBasedContract<State> implements Contract<State> {
cachedBlockHeight = cachedState.cachedHeight;
}

this.logger.debug('cache lookup', benchmark.elapsed());
benchmark.reset();

let contractDefinition,
interactions = [],
sortedInteractions = [],
Expand Down
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './modules/CreateContract';

export * from './modules/impl/BlockHeightInteractionsSorter';
export * from './modules/impl/ContractDefinitionLoader';
export * from './modules/impl/RedstoneGatewayContractDefinitionLoader';
export * from './modules/impl/ArweaveGatewayInteractionsLoader';
export * from './modules/impl/RedstoneGatewayInteractionsLoader';
export * from './modules/impl/DefaultStateEvaluator';
Expand Down
26 changes: 22 additions & 4 deletions src/core/modules/impl/ContractDefinitionLoader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { ContractDefinition, DefinitionLoader, getTag, LoggerFactory, SmartWeaveTags, SwCache } from '@smartweave';
import {
Benchmark,
ContractDefinition,
DefinitionLoader,
getTag,
LoggerFactory,
SmartWeaveTags,
SwCache
} from '@smartweave';
import Arweave from 'arweave';
import Transaction from 'arweave/web/lib/transaction';

Expand All @@ -8,31 +16,41 @@ export class ContractDefinitionLoader implements DefinitionLoader {
constructor(
private readonly arweave: Arweave,
// TODO: cache should be removed from the core layer and implemented in a wrapper of the core implementation
private readonly cache?: SwCache<string, ContractDefinition<unknown>>
protected readonly cache?: SwCache<string, ContractDefinition<unknown>>
) {}

async load<State>(contractTxId: string, forcedSrcTxId?: string): Promise<ContractDefinition<State>> {
if (!forcedSrcTxId && this.cache?.contains(contractTxId)) {
this.logger.debug('ContractDefinitionLoader: Hit from cache!');
return Promise.resolve(this.cache?.get(contractTxId) as ContractDefinition<State>);
}

const benchmark = Benchmark.measure();
const contract = await this.doLoad<State>(contractTxId, forcedSrcTxId);
this.logger.info(`Contract definition loaded in: ${benchmark.elapsed()}`);
this.cache?.put(contractTxId, contract);

return contract;
}

async doLoad<State>(contractTxId: string, forcedSrcTxId?: string): Promise<ContractDefinition<State>> {
const benchmark = Benchmark.measure();
const contractTx = await this.arweave.transactions.get(contractTxId);
const owner = await this.arweave.wallets.ownerToAddress(contractTx.owner);
this.logger.debug('Contract tx and owner', benchmark.elapsed());
benchmark.reset();

const contractSrcTxId = forcedSrcTxId ? forcedSrcTxId : getTag(contractTx, SmartWeaveTags.CONTRACT_SRC_TX_ID);

const minFee = getTag(contractTx, SmartWeaveTags.MIN_FEE);
this.logger.debug('Tags decoding', benchmark.elapsed());
benchmark.reset();

const contractSrcTx = await this.arweave.transactions.get(contractSrcTxId);
this.logger.debug('Contract src tx load', benchmark.elapsed());
benchmark.reset();

const src = contractSrcTx.get('data', { decode: true, string: true });
const initState = JSON.parse(await this.evalInitialState(contractTx));
this.logger.debug('Parsing src and init state', benchmark.elapsed());

return {
txId: contractTxId,
Expand Down
7 changes: 2 additions & 5 deletions src/core/modules/impl/ContractHandlerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '@smartweave';
import BigNumber from 'bignumber.js';
import * as clarity from '@weavery/clarity';
import * as v8 from "v8";

export class ContractHandlerApi<State> implements HandlerApi<State> {
private readonly contractLogger: RedStoneLogger;
Expand Down Expand Up @@ -63,11 +64,7 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
this.assignWrite(executionContext, currentTx);
this.assignRefreshState(executionContext);

// strangely - state is for some reason modified for some contracts (eg. YLVpmhSq5JmLltfg6R-5fL04rIRPrlSU22f6RQ6VyYE)
// when calling any async (even simple timeout) function here...
// that's (ie. deepCopy) a dumb workaround for this issue
// see https://github.com/ArweaveTeam/SmartWeave/pull/92 for more details
const handlerResult = deepCopy(await Promise.race([timeoutPromise, handler(stateCopy, interaction)]));
const handlerResult = await Promise.race([timeoutPromise, handler(stateCopy, interaction)]);

if (handlerResult && (handlerResult.state || handlerResult.result)) {
return {
Expand Down
9 changes: 4 additions & 5 deletions src/core/modules/impl/DefaultStateEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
let lastEvaluatedInteraction = null;
let errorMessage = null;

for (const missingInteraction of missingInteractions) {
const missingInteractionsLength = missingInteractions.length;
for (let i = 0; i < missingInteractionsLength; i++) {
const missingInteraction = missingInteractions[i];
const singleInteractionBenchmark = Benchmark.measure();

const interactionTx: GQLNodeInterface = missingInteraction.node;
Expand All @@ -81,7 +83,6 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
}/${missingInteractions.length} [of all:${sortedInteractions.length}]`
);

// verifying whether state isn't already available for this exact interaction.
const isInteractWrite = this.tagsParser.isInteractWrite(missingInteraction, contractDefinition.txId);

this.logger.debug('interactWrite?:', isInteractWrite);
Expand Down Expand Up @@ -163,8 +164,6 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
caller: interactionTx.owner.address
};

const intermediaryCacheHit = false;

const interactionData = {
interaction,
interactionTx,
Expand Down Expand Up @@ -199,7 +198,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {

interactionCall.update({
cacheHit: false,
intermediaryCacheHit,
intermediaryCacheHit: false,
outputState: stackTrace.saveState ? currentState : undefined,
executionTime: singleInteractionBenchmark.elapsed(true) as number,
valid: validity[interactionTx.id],
Expand Down
48 changes: 48 additions & 0 deletions src/core/modules/impl/RedstoneGatewayContractDefinitionLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ContractDefinition, LoggerFactory, stripTrailingSlash, SwCache } from '@smartweave';
import Arweave from 'arweave';
import 'isomorphic-fetch';
import { ContractDefinitionLoader } from './ContractDefinitionLoader';

/**
* An extension to {@link ContractDefinitionLoader} that makes use of
* Redstone Gateway ({@link https://github.com/redstone-finance/redstone-sw-gateway})
* to load Contract Data.
*
* If the contract data is not available on RedStone Gateway - it fallbacks to default implementation
* in {@link ContractDefinitionLoader} - i.e. loads the definition from Arweave gateway.
*/
export class RedstoneGatewayContractDefinitionLoader extends ContractDefinitionLoader {
ppedziwiatr marked this conversation as resolved.
Show resolved Hide resolved
private readonly rLogger = LoggerFactory.INST.create('RedstoneGatewayContractDefinitionLoader');

constructor(
private readonly baseUrl: string,
arweave: Arweave,
cache?: SwCache<string, ContractDefinition<unknown>>
) {
super(arweave, cache);
this.baseUrl = stripTrailingSlash(baseUrl);
}

async doLoad<State>(contractTxId: string, forcedSrcTxId?: string): Promise<ContractDefinition<State>> {
if (forcedSrcTxId) {
// no support for the evolve yet..
return await super.doLoad(contractTxId, forcedSrcTxId);
}

try {
return await fetch(`${this.baseUrl}/gateway/contracts/${contractTxId}`)
.then((res) => {
return res.ok ? res.json() : Promise.reject(res);
})
.catch((error) => {
if (error.body?.message) {
this.rLogger.error(error.body.message);
}
throw new Error(`Unable to retrieve contract data. Redstone gateway responded with status ${error.status}.`);
});
} catch (e) {
this.rLogger.warn('Falling back to default contracts loader');
return await super.doLoad(contractTxId, forcedSrcTxId);
}
}
}
1 change: 1 addition & 0 deletions src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
GQLNodeInterface
} from '@smartweave';
import 'isomorphic-fetch';

interface Paging {
total: string;
limit: number;
Expand Down
4 changes: 4 additions & 0 deletions src/logging/Benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export class Benchmark {
this.end = null;
}

public stop() {
this.end = Date.now();
}

public elapsed(rawValue = false): string | number {
if (this.end === null) {
this.end = Date.now();
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/CacheableContractInteractionsLoader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Benchmark,
BlockHeightKey,
BlockHeightSwCache,
EvaluationOptions,
Expand Down Expand Up @@ -26,6 +27,7 @@ export class CacheableContractInteractionsLoader implements InteractionsLoader {
toBlockHeight: number,
evaluationOptions?: EvaluationOptions
): Promise<GQLEdgeInterface[]> {
const benchmark = Benchmark.measure();
this.logger.debug('Loading interactions', {
contractId,
fromBlockHeight,
Expand Down Expand Up @@ -73,6 +75,8 @@ export class CacheableContractInteractionsLoader implements InteractionsLoader {
// - that's why "result" variable is not used here
await this.cache.put(new BlockHeightKey(contractId, toBlockHeight), valueToCache);

this.logger.debug(`Interactions loaded in ${benchmark.elapsed()}`);

return result;
}
}
10 changes: 10 additions & 0 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
/* eslint-disable */
import cloneDeep from 'lodash/cloneDeep';

const isNode = new Function('try {return this===global;}catch(e){return false;}');

let v8 = null;
if (isNode()) {
v8 = require('v8');
}

export const sleep = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
};

export const deepCopy = (input: unknown): any => {
if (v8) {
return v8.deserialize(v8.serialize(input));
}
return cloneDeep(input);
// note: parse/stringify combination is slooow: https://jsben.ch/bWfk9
//return JSON.parse(JSON.stringify(input, mapReplacer), mapReviver);
Expand Down
Loading