feat(coverage): gas usage improvements

Do not submit event for function and branch
but detect where the statement is
This commit is contained in:
Anthony Laibe 2018-12-18 14:51:48 +00:00 committed by Iuri Matias
parent 8a6d075cd1
commit 0118b1a409
7 changed files with 79 additions and 65 deletions

View File

@ -1,5 +1,5 @@
import * as path from "path"; import * as path from "path";
import parser, { Location } from "solidity-parser-antlr"; import parser, { LineColumn, Location } from "solidity-parser-antlr";
import { EventLog } from "web3/types"; import { EventLog } from "web3/types";
import { decrypt } from "./eventId"; import { decrypt } from "./eventId";
@ -12,11 +12,8 @@ import { BranchType, Coverage } from "./types";
const fs = require("../../core/fs"); const fs = require("../../core/fs");
enum EventEnum { const STATEMENT_EVENT = "__StatementCoverage";
statement = "__StatementCoverage", const POINT_FACTOR = 1000000000;
branch = "__BranchCoverage",
function = "__FunctionCoverage",
}
let id = 0; let id = 0;
function nextId() { function nextId() {
@ -31,6 +28,7 @@ export class ContractEnhanced {
public source: string; public source: string;
private ast: parser.ASTNode; private ast: parser.ASTNode;
private coverageFilepath: string; private coverageFilepath: string;
private functionsBodyLocation: {[id: number]: Location} = {};
constructor(public filepath: string) { constructor(public filepath: string) {
this.id = nextId(); this.id = nextId();
@ -71,29 +69,24 @@ export class ContractEnhanced {
public updateCoverage(events: EventLog[]) { public updateCoverage(events: EventLog[]) {
events.filter(this.filterCoverageEvent).forEach((event) => { events.filter(this.filterCoverageEvent).forEach((event) => {
const value = parseInt(event.returnValues[0], 10); const value = parseInt(event.returnValues[0], 10);
const {contractId, injectionPointId, locationIdx} = decrypt(value); const {contractId, injectionPointId} = decrypt(value);
if (contractId !== this.id) { if (contractId !== this.id) {
return; return;
} }
switch (event.event) {
case "__StatementCoverage": {
this.coverage.s[injectionPointId] += 1; this.coverage.s[injectionPointId] += 1;
const statement = this.coverage.statementMap[injectionPointId]; const location = this.coverage.statementMap[injectionPointId];
this.coverage.l[statement.start.line] += 1; this.coverage.l[location.start.line] += 1;
break;
} const fnMapId = this.findFnMapId(location);
case "__FunctionCoverage": { if (fnMapId) {
this.coverage.f[injectionPointId] += 1; this.coverage.f[fnMapId] += 1;
const fn = this.coverage.fnMap[injectionPointId];
this.coverage.l[fn.line] += 1;
break;
}
case "__BranchCoverage": {
this.coverage.b[injectionPointId][locationIdx] += 1;
break;
} }
const [fnBranchId, locationId] = this.findFnBranchIdAndLocationId(location);
if (fnBranchId) {
this.coverage.b[fnBranchId][locationId] += 1;
} }
}); });
} }
@ -118,7 +111,7 @@ export class ContractEnhanced {
return coverageId; return coverageId;
} }
public addFunction(location: Location, name: string) { public addFunction(location: Location, name: string, bodyLocation: Location) {
const coverageId = this.getNewCoverageId(this.coverage.fnMap); const coverageId = this.getNewCoverageId(this.coverage.fnMap);
const line = location.start.line; const line = location.start.line;
this.coverage.fnMap[coverageId] = { this.coverage.fnMap[coverageId] = {
@ -127,7 +120,7 @@ export class ContractEnhanced {
name, name,
}; };
this.coverage.f[coverageId] = 0; this.coverage.f[coverageId] = 0;
this.coverage.l[line] = 0; this.functionsBodyLocation[coverageId] = bodyLocation;
return coverageId; return coverageId;
} }
@ -137,7 +130,51 @@ export class ContractEnhanced {
} }
private filterCoverageEvent(event: EventLog) { private filterCoverageEvent(event: EventLog) {
return [EventEnum.function, EventEnum.branch, EventEnum.statement].includes(event.event as EventEnum); return STATEMENT_EVENT === event.event;
}
private findFnMapId(location: Location) {
const result = Object.keys(this.functionsBodyLocation).find((value: string) => {
const bodyLocation = this.functionsBodyLocation[parseInt(value, 10)];
return this.isWithin(location.start, bodyLocation);
});
if (result) {
return parseInt(result, 10);
}
return 0;
}
private findFnBranchIdAndLocationId(location: Location) {
let fnBranchId = 0;
let locationId = 0;
Object.keys(this.coverage.branchMap).forEach((value: string) => {
if (fnBranchId && locationId) {
return;
}
const branch = this.coverage.branchMap[parseInt(value, 10)];
branch.locations.forEach((branchLocation, index) => {
if (this.isWithin(location.start, branchLocation)) {
fnBranchId = parseInt(value, 10);
locationId = index;
}
});
});
return [fnBranchId, locationId];
}
private isWithin(point: LineColumn, location: Location) {
const toCompare = this.getComparablePoint(point);
return toCompare >= this.getComparablePoint(location.start) && toCompare <= this.getComparablePoint(location.end);
}
private getComparablePoint(point: LineColumn) {
return point.line * POINT_FACTOR + point.column;
} }
} }

View File

@ -1,24 +1,22 @@
const CONTRACT_ID_FACTOR = 100000000; const CONTRACT_ID_FACTOR = 1000000;
const INJECTION_POINT_ID_FACTOR = 10000;
/** /**
* Convert the 3 params as uint32 where the first 2 digits are for contractsId, * Convert the 2 params as uint32 where the first 4 digits are for contractsId,
* the next 3 digit are for injectionPoint id and the rest if for the locationIds * the followings one are for injectionPoint id
* *
* @export * @export
* @param {number} contractId * @param {number} contractId
* @param {number} injectionPointId * @param {number} injectionPointId
* @param {number} [locationIdx] * @returns a number representing the 2 params as uint32
* @returns a number representing the 3 params as uint32
*/ */
export function encrypt(contractId: number, injectionPointId: number, locationIdx?: number) { export function encrypt(contractId: number, injectionPointId: number) {
return contractId * CONTRACT_ID_FACTOR + injectionPointId * INJECTION_POINT_ID_FACTOR + (locationIdx || 0); return contractId * CONTRACT_ID_FACTOR + injectionPointId;
} }
/** /**
* Explore the uint32 into contractId, injectionPointId and locationIds where * Explore the uint32 into contractId, injectionPointId and locationIds where
* the first 2 digits are for contractsId, * the first 4 digits are for contractsId,
* the next 3 digit are for injectionPoint id and the rest if for the locationIds * the rest are for injectionPoint id
* *
* @export * @export
* @param {number} value * @param {number} value
@ -26,8 +24,7 @@ export function encrypt(contractId: number, injectionPointId: number, locationId
*/ */
export function decrypt(value: number) { export function decrypt(value: number) {
const contractId = Math.floor(value / CONTRACT_ID_FACTOR); const contractId = Math.floor(value / CONTRACT_ID_FACTOR);
const injectionPointId = Math.floor(value / INJECTION_POINT_ID_FACTOR) - contractId * INJECTION_POINT_ID_FACTOR; const injectionPointId = value - contractId * CONTRACT_ID_FACTOR;
const locationIdx = value - contractId * CONTRACT_ID_FACTOR - injectionPointId * INJECTION_POINT_ID_FACTOR;
return {contractId, injectionPointId, locationIdx}; return {contractId, injectionPointId};
} }

View File

@ -1,6 +1,6 @@
import * as globule from "globule"; import * as globule from "globule";
import * as path from "path"; import * as path from "path";
import { Contract as Web3Contract } from "web3/types"; import Web3Contract from "web3/eth/contract";
import { Contract } from "../../../typings/contract"; import { Contract } from "../../../typings/contract";
import { Embark } from "../../../typings/embark"; import { Embark } from "../../../typings/embark";

View File

@ -11,10 +11,6 @@ export class Injector {
switch (injectionPoint.type) { switch (injectionPoint.type) {
case "statement": case "statement":
return this.statement(injectionPoint); return this.statement(injectionPoint);
case "function":
return this.function(injectionPoint);
case "branch":
return this.branch(injectionPoint);
case "contractDefinition": case "contractDefinition":
return this.contractDefinition(injectionPoint); return this.contractDefinition(injectionPoint);
} }
@ -25,21 +21,9 @@ export class Injector {
this.insertAt(injectionPoint.location.start.line - 1, data); this.insertAt(injectionPoint.location.start.line - 1, data);
} }
private function(injectionPoint: InjectionPoint) {
const data = `emit __FunctionCoverage(${encrypt(this.contract.id, injectionPoint.id)});`;
this.insertAt(injectionPoint.location.start.line, data);
}
private branch(injectionPoint: InjectionPoint) {
const data = `emit __BranchCoverage(${encrypt(this.contract.id, injectionPoint.id, injectionPoint.locationIdx)});`;
this.insertAt(injectionPoint.location.start.line, data);
}
private contractDefinition(injectionPoint: InjectionPoint) { private contractDefinition(injectionPoint: InjectionPoint) {
const data = [ const data = [
"event __FunctionCoverage(uint32 value);",
"event __StatementCoverage(uint32 value);", "event __StatementCoverage(uint32 value);",
"event __BranchCoverage(uint32 value);",
].join("\n"); ].join("\n");
this.insertAt(injectionPoint.location.start.line, data); this.insertAt(injectionPoint.location.start.line, data);

View File

@ -44,8 +44,7 @@ export class Instrumenter {
return; return;
} }
const id = this.contract.addFunction(node.loc, node.name); this.contract.addFunction(node.loc, node.name, node.body.loc);
this.addInjectionPoints("function", id, node.body.loc);
} }
public instrumentStatement(node: Statement) { public instrumentStatement(node: Statement) {
@ -69,10 +68,7 @@ export class Instrumenter {
return acc; return acc;
}, []); }, []);
const id = this.contract.addBranch(node.loc.start.line, "if", locations); this.contract.addBranch(node.loc.start.line, "if", locations);
locations.forEach((location, index) => {
this.addInjectionPoints("branch", id, location, index);
});
} }
private addInjectionPoints(type: InjectionPointType, id: number, location: Location, locationIdx?: number) { private addInjectionPoints(type: InjectionPointType, id: number, location: Location, locationIdx?: number) {

View File

@ -1,6 +1,6 @@
import { Location } from "solidity-parser-antlr"; import { Location } from "solidity-parser-antlr";
export type InjectionPointType = "statement" | "branch" | "function" | "contractDefinition"; export type InjectionPointType = "statement" | "contractDefinition";
export type BranchType = "if" | "switch"; export type BranchType = "if" | "switch";
export interface InjectionPoint { export interface InjectionPoint {

View File

@ -1,6 +1,6 @@
import Handlebars from "handlebars"; import Handlebars from "handlebars";
import * as path from "path"; import * as path from "path";
import { ABIDefinition } from "web3/types"; import { ABIDefinition } from "web3/eth/abi";
import { Contract } from "../../../../../typings/contract"; import { Contract } from "../../../../../typings/contract";
import { Embark } from "../../../../../typings/embark"; import { Embark } from "../../../../../typings/embark";