Major sol-profiler overhaul
This commit is contained in:
@@ -87,4 +87,4 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
@@ -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';
|
||||
|
@@ -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),
|
||||
);
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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',
|
||||
|
@@ -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)}`;
|
||||
},
|
||||
};
|
||||
|
Reference in New Issue
Block a user