Skip to content

Commit

Permalink
perf: unnecessary call to arweave.networkInfo when block height is re…
Browse files Browse the repository at this point in the history
…quest from client #70
  • Loading branch information
ppedziwiatr committed Jan 3, 2022
1 parent 027019f commit a49d27a
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 52 deletions.
1 change: 0 additions & 1 deletion src/__tests__/regression/test-cases/read-state.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@
"2caOW6ol9T8LHCMO8tVAx4GHRwv1q3fFc79KzKOtoww",
"HdZBZa0GfOEUYCubwvoSyxGUUgPmgy7RJb5l77T21bE",
"7Dp5r-UpZLDvHqsDbZbqWhCBwbYdJMKBuC3tFC-FF7U",
"YLVpmhSq5JmLltfg6R-5fL04rIRPrlSU22f6RQ6VyYE",
"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
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
47 changes: 47 additions & 0 deletions src/core/modules/impl/RedstoneGatewayContractDefinitionLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ContractDefinition, ContractDefinitionLoader, LoggerFactory, stripTrailingSlash, SwCache } from '@smartweave';
import Arweave from 'arweave';
import 'isomorphic-fetch';

/**
* 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 {
private readonly rLogger = LoggerFactory.INST.create('ContractDefinitionLoader');

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...
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.debug('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
29 changes: 16 additions & 13 deletions tools/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ import {SmartWeaveNodeFactory} from '../src/core/node/SmartWeaveNodeFactory';

const logger = LoggerFactory.INST.create('Contract');

LoggerFactory.use(new TsLogFactory());
LoggerFactory.INST.logLevel('info');
// LoggerFactory.use(new TsLogFactory());
LoggerFactory.INST.logLevel("error");
LoggerFactory.INST.logLevel("debug", "ArweaveGatewayInteractionsLoader");
LoggerFactory.INST.logLevel("debug", "HandlerBasedContract");
LoggerFactory.INST.logLevel("debug", "ContractDefinitionLoader");
LoggerFactory.INST.logLevel("debug", "CacheableContractInteractionsLoader");



async function main() {
Expand All @@ -22,36 +27,34 @@ async function main() {
logging: false // Enable network request logging
});

const contractTxId = 'Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY';
// const contractTxId = 'Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY';
const contractTxId = 't9T7DIOGxx4VWXoCEeYYarFYeERTpWIC1V3y-BPZgKE';

//const interactionsLoader = new FromFileInteractionsLoader(path.join(__dirname, 'data', 'interactions.json'));

// const smartweave = SmartWeaveWebFactory.memCachedBased(arweave).setInteractionsLoader(interactionsLoader).build();
const smartweaveR = SmartWeaveWebFactory
.memCachedBased(arweave, 1)
.setInteractionsLoader(new RedstoneGatewayInteractionsLoader(
'https://gateway.redstone.finance')
).build();
.memCachedBased(arweave, 1).build();

const usedBefore = Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100
/* const usedBefore = Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100
const contractR = smartweaveR.contract(contractTxId);
const {state, validity} = await contractR.readState();
const usedAfter = Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100
logger.warn("Heap used in MB", {
usedBefore,
usedAfter
});
});*/

const smartweave = SmartWeaveWebFactory.memCached(arweave, 1);
const smartweave = SmartWeaveWebFactory.memCached(arweave, 749180);
const contract = smartweave.contract(contractTxId);
const result = await contract.readState();
const result = await contract.readState(749180);


//fs.writeFileSync(path.join(__dirname, 'data', 'validity.json'), JSON.stringify(validity));

//fs.writeFileSync(path.join(__dirname, 'data', 'validity_old.json'), JSON.stringify(result.validity));
fs.writeFileSync(path.join(__dirname, 'data', 'state_redstone.json'), JSON.stringify(state));
fs.writeFileSync(path.join(__dirname, 'data', 'state_arweave.json'), JSON.stringify(result.state));
//fs.writeFileSync(path.join(__dirname, 'data', 'state_redstone.json'), JSON.stringify(state));
//fs.writeFileSync(path.join(__dirname, 'data', 'state_arweave.json'), JSON.stringify(result.state));

// console.log('second read');
// await lootContract.readState();
Expand Down
Loading

0 comments on commit a49d27a

Please sign in to comment.