Major sol-profiler overhaul

This commit is contained in:
Leonid Logvinov
2019-02-22 16:16:03 -08:00
parent 047de370d6
commit 04d8f46ff3
13 changed files with 320 additions and 61 deletions

View File

@@ -87,4 +87,4 @@
"publishConfig": {
"access": "public"
}
}
}

View File

@@ -59,6 +59,8 @@ export class SolCompilerArtifactAdapter extends AbstractArtifactAdapter {
sourceCodes[value.id] = source.source;
});
const contractData = {
abi: artifact.compilerOutput.abi,
name: artifact.contractName,
sourceCodes,
sources,
bytecode: artifact.compilerOutput.evm.bytecode.object,

View File

@@ -12,7 +12,7 @@ export {
BranchCoverage,
BranchDescription,
Subtrace,
TraceInfo,
SubTraceInfo,
Coverage,
LineColumn,
LineCoverage,
@@ -24,9 +24,10 @@ export {
FnMap,
OffsetToLocation,
StatementMap,
TraceInfoBase,
TraceInfoExistingContract,
TraceInfoNewContract,
TraceInfo,
SubTraceInfoBase,
SubTraceInfoExistingContract,
SubTraceInfoNewContract,
Sources,
SourceCodes,
} from './types';

View File

@@ -15,9 +15,9 @@ import {
Coverage,
SourceRange,
Subtrace,
TraceInfo,
TraceInfoExistingContract,
TraceInfoNewContract,
SubTraceInfo,
SubTraceInfoExistingContract,
SubTraceInfoNewContract,
} from './types';
import { utils } from './utils';
@@ -62,32 +62,43 @@ export class TraceCollector {
await mkdirpAsync('coverage');
fs.writeFileSync('coverage/coverage.json', stringifiedCoverage);
}
public async computeSingleTraceCoverageAsync(traceInfo: TraceInfo): Promise<void> {
public async getContractDataByTraceInfoIfExistsAsync(
address: string,
bytecode: string,
isContractCreation: boolean,
): Promise<ContractData | undefined> {
if (_.isUndefined(this._contractsData)) {
this._contractsData = await this._artifactAdapter.collectContractsDataAsync();
}
const isContractCreation = traceInfo.address === constants.NEW_CONTRACT;
const bytecode = isContractCreation
? (traceInfo as TraceInfoNewContract).bytecode
: (traceInfo as TraceInfoExistingContract).runtimeBytecode;
const contractData = utils.getContractDataIfExists(this._contractsData, bytecode);
if (_.isUndefined(contractData)) {
const shortenHex = (hex: string) => {
/**
* Length chooses so that both error messages are of the same length
* and it's enough data to figure out which artifact has a problem.
*/
const length = 18;
return `${hex.substr(0, length + 2)}...${hex.substr(hex.length - length, length)}`;
};
/**
* Length chooses so that both error messages are of the same length
* and it's enough data to figure out which artifact has a problem.
*/
const HEX_LENGTH = 16;
const errMsg = isContractCreation
? `Unable to find matching bytecode for contract creation ${chalk.bold(
shortenHex(bytecode),
utils.shortenHex(bytecode, HEX_LENGTH),
)}, please check your artifacts. Ignoring...`
: `Unable to find matching bytecode for contract address ${chalk.bold(
traceInfo.address,
address,
)}, please check your artifacts. Ignoring...`;
this._logger.warn(errMsg);
}
return contractData;
}
public async computeSingleTraceCoverageAsync(subTraceInfo: SubTraceInfo): Promise<void> {
const isContractCreation = subTraceInfo.address === constants.NEW_CONTRACT;
const bytecode = isContractCreation
? (subTraceInfo as SubTraceInfoNewContract).bytecode
: (subTraceInfo as SubTraceInfoExistingContract).runtimeBytecode;
const contractData = await this.getContractDataByTraceInfoIfExistsAsync(
subTraceInfo.address,
bytecode,
isContractCreation,
);
if (_.isUndefined(contractData)) {
return;
}
const bytecodeHex = stripHexPrefix(bytecode);
@@ -96,7 +107,7 @@ export class TraceCollector {
_.map(contractData.sources, (_sourcePath: string, fileIndex: string) => {
const singleFileCoverageForTrace = this._singleFileSubtraceHandler(
contractData,
traceInfo.subtrace,
subTraceInfo.subtrace,
pcToSourceRange,
_.parseInt(fileIndex),
);

View File

@@ -1,16 +1,21 @@
import { NodeType } from '@0x/web3-wrapper';
import * as fs from 'fs';
import * as _ from 'lodash';
import { constants } from './constants';
import { getContractAddressToTraces } from './trace';
import { TraceCollectionSubprovider } from './trace_collection_subprovider';
import { TraceInfo, TraceInfoExistingContract, TraceInfoNewContract } from './types';
import { SubTraceInfo, SubTraceInfoExistingContract, SubTraceInfoNewContract, TraceInfo } from './types';
// TraceInfoSubprovider is extended by subproviders which need to work with one
// TraceInfo at a time. It has one abstract method: _handleTraceInfoAsync, which
// is called for each TraceInfo.
export abstract class TraceInfoSubprovider extends TraceCollectionSubprovider {
protected abstract _handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void>;
protected abstract _handleSubTraceInfoAsync(subTraceInfo: SubTraceInfo): Promise<void>;
// tslint:disable prefer-function-over-method
protected _handleTraceInfoAsync(_traceInfo: TraceInfo): Promise<void> {
return Promise.resolve(undefined);
}
protected async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> {
await this._web3Wrapper.awaitTransactionMinedAsync(txHash, 0);
const nodeType = await this._web3Wrapper.getNodeTypeAsync();
@@ -24,6 +29,13 @@ export abstract class TraceInfoSubprovider extends TraceCollectionSubprovider {
const tracer = `
{
data: [],
extractStack: function (stack) {
var extract = [];
for (var i = 0; i < stack.length(); i++) {
extract.push('0x' + stack.peek(i).toString(16));
}
return extract;
},
step: function(log) {
const op = log.op.toString();
const opn = 0 | log.op.toNumber();
@@ -32,7 +44,16 @@ export abstract class TraceInfoSubprovider extends TraceCollectionSubprovider {
const gasCost = 0 | log.getCost();
const gas = 0 | log.getGas();
const isCall = opn == 0xf1 || opn == 0xf2 || opn == 0xf4 || opn == 0xf5 || opn == 0xfa;
const stack = isCall ? ['0x'+log.stack.peek(1).toString(16), null] : null;
const isMemoryAccess = opn == 0x51 || opn == 0x52 || opn == 0x53;
const isCallDataAccess = opn == 0x37;
var stack;
if (isCall) {
stack = ['0x'+log.stack.peek(1).toString(16), null];
} else if (isMemoryAccess) {
stack = ['0x'+log.stack.peek(0).toString(16)];
} else if (isCallDataAccess) {
stack = ['0x'+log.stack.peek(2).toString(16), '0x'+log.stack.peek(1).toString(16), '0x'+log.stack.peek(0).toString(16)];
}
this.data.push({ pc, gasCost, depth, op, stack, gas });
},
fault: function() { },
@@ -50,14 +71,23 @@ export abstract class TraceInfoSubprovider extends TraceCollectionSubprovider {
disableStorage: true,
});
}
const traceInfo = {
trace,
address,
data,
txHash,
};
await this._handleTraceInfoAsync(traceInfo);
const contractAddressToTraces = getContractAddressToTraces(trace.structLogs, address);
const subcallAddresses = _.keys(contractAddressToTraces);
if (address === constants.NEW_CONTRACT) {
for (const subcallAddress of subcallAddresses) {
let traceInfo: TraceInfoNewContract | TraceInfoExistingContract;
let subTraceInfo: SubTraceInfoNewContract | SubTraceInfoExistingContract;
const traceForThatSubcall = contractAddressToTraces[subcallAddress];
const subcallDepth = traceForThatSubcall[0].depth;
if (subcallAddress === 'NEW_CONTRACT') {
const traceForThatSubcall = contractAddressToTraces[subcallAddress];
traceInfo = {
subTraceInfo = {
subcallDepth,
subtrace: traceForThatSubcall,
txHash,
address: subcallAddress,
@@ -65,27 +95,29 @@ export abstract class TraceInfoSubprovider extends TraceCollectionSubprovider {
};
} else {
const runtimeBytecode = await this._web3Wrapper.getContractCodeAsync(subcallAddress);
const traceForThatSubcall = contractAddressToTraces[subcallAddress];
traceInfo = {
subTraceInfo = {
subcallDepth,
subtrace: traceForThatSubcall,
txHash,
address: subcallAddress,
runtimeBytecode,
};
}
await this._handleTraceInfoAsync(traceInfo);
await this._handleSubTraceInfoAsync(subTraceInfo);
}
} else {
for (const subcallAddress of subcallAddresses) {
const runtimeBytecode = await this._web3Wrapper.getContractCodeAsync(subcallAddress);
const traceForThatSubcall = contractAddressToTraces[subcallAddress];
const traceInfo: TraceInfoExistingContract = {
const subcallDepth = traceForThatSubcall[0].depth;
const subTraceInfo: SubTraceInfoExistingContract = {
subcallDepth,
subtrace: traceForThatSubcall,
txHash,
address: subcallAddress,
runtimeBytecode,
};
await this._handleTraceInfoAsync(traceInfo);
await this._handleSubTraceInfoAsync(subTraceInfo);
}
}
}

View File

@@ -1,4 +1,4 @@
import { StructLog } from 'ethereum-types';
import { ContractAbi, StructLog, TransactionTrace } from 'ethereum-types';
export interface LineColumn {
line: number;
@@ -83,6 +83,8 @@ export interface Sources {
}
export interface ContractData {
abi: ContractAbi;
name: string;
bytecode: string;
sourceMap: string;
runtimeBytecode: string;
@@ -94,22 +96,30 @@ export interface ContractData {
// Part of the trace executed within the same context
export type Subtrace = StructLog[];
export interface TraceInfoBase {
export interface SubTraceInfoBase {
subtrace: Subtrace;
txHash: string;
subcallDepth: number;
}
export interface TraceInfoNewContract extends TraceInfoBase {
export interface SubTraceInfoNewContract extends SubTraceInfoBase {
address: 'NEW_CONTRACT';
bytecode: string;
}
export interface TraceInfoExistingContract extends TraceInfoBase {
export interface SubTraceInfoExistingContract extends SubTraceInfoBase {
address: string;
runtimeBytecode: string;
}
export type TraceInfo = TraceInfoNewContract | TraceInfoExistingContract;
export type SubTraceInfo = SubTraceInfoNewContract | SubTraceInfoExistingContract;
export interface TraceInfo {
trace: TransactionTrace;
txHash: string;
address: string;
data: string | undefined;
}
export enum BlockParamLiteral {
Latest = 'latest',

View File

@@ -81,17 +81,44 @@ export const utils = {
return addressUtils.padZeros(new BigNumber(addHexPrefix(stackEntry)).toString(hexBase));
},
normalizeStructLogs(structLogs: StructLog[]): StructLog[] {
if (_.isEmpty(structLogs)) {
return structLogs;
}
if (structLogs[0].depth === 1) {
// Geth uses 1-indexed depth counter whilst ganache starts from 0
const newStructLogs = _.map(structLogs, structLog => {
const newStructLogs = _.map(structLogs, (structLog: StructLog, idx: number) => {
const newStructLog = {
...structLog,
depth: structLog.depth - 1,
};
if (newStructLog.op === 'STATICCALL') {
if (newStructLog.op === OpCode.StaticCall) {
// HACK(leo): Geth traces sometimes returns those gas costs incorrectly as very big numbers so we manually fix them.
newStructLog.gasCost = STATICCALL_GAS_COST;
}
// if (newStructLog.op === OpCode.MStore) {
// // HACK(leo): Geth traces sometimes returns those gas costs incorrectly as very big numbers so we manually fix them.
// newStructLog.gasCost = 3;
// }
// if (newStructLog.op === OpCode.MLoad) {
// // HACK(leo): Geth traces sometimes returns those gas costs incorrectly as very big numbers so we manually fix them.
// newStructLog.gasCost = 3;
// }
// if (newStructLog.op === OpCode.CallDataCopy) {
// // HACK(leo): Geth traces sometimes returns those gas costs incorrectly as very big numbers so we manually fix them.
// newStructLog.gasCost = 3;
// }
if (newStructLog.op === 'CALL') {
const HEX_BASE = 16;
const callAddress = parseInt(newStructLog.stack[0], HEX_BASE);
// HACK(leo): Geth traces sometimes returns those gas costs incorrectly as very big numbers so we manually fix them.
if (callAddress < 50) {
const nextStructLog = structLogs[idx + 1];
newStructLog.gasCost = structLog.gas - nextStructLog.gas;
} else {
newStructLog.gasCost = 700;
}
// newStructLog.gasCost = 700;
}
return newStructLog;
});
return newStructLogs;
@@ -104,4 +131,7 @@ export const utils = {
lines[0] = lines[0].slice(range.start.column);
return lines.join('\n');
},
shortenHex(hex: string, length: number): string {
return `${hex.substr(0, length + 2)}...${hex.substr(hex.length - length, length)}`;
},
};