-
Notifications
You must be signed in to change notification settings - Fork 263
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
Improve implementation - perhaps with debug_traceTransaction #111
Comments
I think this makes a lot of sense. I really like the opcode idea. I was imagining something slightly different (probably impractical) before I read your description of how it might work. But like:
One thing I find daunting about the opening proposal is tracking upstream solc. They have a blistering production schedule. In my fantasy there are no dependencies, lol. And solidity-coverage runs very slowly. RE: |
Yeah, I really don't want to track that either. Your proposal is the sensible way to implement the functionality found in the truffle debugger, definitely. There's no need to eavesdrop on testrpc though; noting the blocknumber before and after running the tests would allow us to just iterate over all the blocks to find the transactions that ran (this requires disabling
I am aware #63 is floating around with my name on it still 😉 . It's clear that performance can be greatly improved even with instrumentation. Personally, I think I am of the opinion that prioritising accurate coverage is more valuable than any of these issues; I'm open to discussions on that front though! |
@area Yes in complete agreement with you about speed. I don't care about #63 especially - current performance is fine except maybe for things that are actually market simulations in disguise. Was just noting that one virtue of the Out of curiosity could you explain what we have to do to the snapshot and why that's required? @subhodi I hope you don't mind but I'm going to delete your comment since it's unrelated to this issue. Please open a separate issue describing the errors your seeing and we'll be happy to help you. Keeping the issue topics well ordered is important because it helps other people find solutions by search when they run into difficulties. Sorry, thanks! |
When using This isn't an issue currently, because even though the chain gets reset, we've already logged the instrumentation events. |
Ah ok right! That's an important detail. Definitely goes into the 'benefits of an opcode' column. [Edit Ignore last sentence: incoherent.] |
So are we looking at something like having
? |
It's avoidable to fork hook([
'ethereumjs-vm/lib/opcodes.js',
'ethereumjs-vm/lib/opFns.js'
], function (exports, name, path) {
if (path.indexOf('opcodes') !== -1) {
// If it's ethereumjs-vm/lib/opcodes.js, then we wrap the exported
// function to detect our own opcode (0xfd).
const _super = exports
exports = function (op, full) {
const coverOp = {
name: 'COVER',
opcode: op,
fee: 0,
in: 0,
out: 0,
dynamic: false,
async: false
}
return op === 0xfd ? coverOp : _super(op, full)
}
return exports
} else {
// If it's ethereumjs-vm/lib/opFns.js then we add our opcode
// function - this simply logs cover, but it can be more complex.
// For example, it could take values off the stack (e.g. type of coverage being done)
Object.assign(exports, {
COVER: function (runState) {
console.log('Cover opcode')
}
})
}
return exports
}) Using it is like using vm.runCode({
code: Buffer.from(code, 'hex'),
gasLimit: Buffer.from('ffffffff', 'hex')
}, function(err, results) {
console.log('returned: ' + results.return.toString('hex'));
}) So, instrumentation just needs to add the appropriate opcodes in the contracts instead of events. Obviously this approach depends on the internals of |
@onbjerg Wow! Great observation, thank you. It may also be possible to combine this approach with listening to the vm's step event to capture coverage ops in real time. If so we won't have to worry about Also worth noting that the |
Yep, you can just use the step event instead. That approach is actually a bit cleaner. You don't need to use testrpc, you can use |
Ah right! Thanks so much @onbjerg, really helpful. |
Yeah, that's brilliant. I'm not sure how I've gone so long without hearing about |
Just to be clear, the hook function is a modified version of |
Good to know! I definitely think there's promise to this approach, but it's going to require some thought. Ideally, we'd have support for something like
from Solidity, otherwise we're going to have to interact with (and 'instrument') the actual compiled code, I think? @onbjerg, in your test, did you just insert If we figured out a way to do that, however, I think with the addition of a 'free push' opcode as well, we could pretty straightforwardly get all the information out that we wanted by prior to calling our opcode putting information on to the stack that we wanted (e.g. which branch of a conditional was being called), and then clearing those elements from the stack as part of our opcode. To get the 'true' behaviour of the uninstrumented contracts, there's another hurdle to be overcome. Contract deployment of anything that's instrumented will cost a different amount from an uninstrumented contract. So I think we'd need to override CODECOPY to not count anything related to instrumentation against the cost of the deployment. EDIT: Not even that is guaranteed to work; if the instrumentation stops the solidity compiler optimising something, then the gas costs could still be different. Bonus terrible (maybe?) implementation ideaIf we can't introduce custom opcodes during compilation, let's do it afterwards! We're going to have to mess with
or some other piece of code that's very unlikely to actually end up in the compiled bytecode. This compiles to Someone talk me out of this, please? 😛 |
@area I just inserted
The (rough) general strategy is now this:
Now, here's the last crucial part: since this is essentially a custom provider (set in const provider = ganache.provider({ ... })
// 🐒 patch
const _super = provider.close.bind(provider)
provider.close = function (cb) {
reporter.generateReports()
_super(cb)
} Basically, we're the man in the middle. Whenever the provider is closed (i.e. tests are done and the network is no longer used), we save the reports to disk. No disk I/O needed except here. Thus, we are actually just inspecting what opcodes are run, and because we get the "instrumentation" points from the opcode offsets in the bytecode, we can calculate backwards via. the source mappings to get the actual lines of code in the Solidity source. This is great on multiple levels
Obvious drawbacks
Everything this approach does is essentially only observe the execution and record whenever a point of interest is run. I'd really appreciate feedback on this approach, as I already started hacking some of these ideas together this weekend. |
@onbjerg Yes, this is clearly a good design. The 'instrumentation' piece will be a big deal to figure out and get working correctly. But the mechanics seem ideal. Still haven't checked but if the VM is exposed on the If it's not exposed we could PR that over there. It wouldn't affect them. A side note - if we continue to parse the code we should start using federicobond's antlr parser, which is more robust and has excellent line number / column pos reporting at the node level. That might help a little. |
We still have to know when the tests are done though.... [Editing, more] - The ganache-core hook is really cool. Another weird possibility is somehow leveraging the event hooks available in a mocha reporter? I just wrote a little eth-gas mocha reporter yesterday and there's a suite end hook available to third party reporters. |
Yeah, that's a really nice approach - basically the way that it should be done. Using those source mappings is what I wanted to do, but using I would definitely like to keep the ability to not require Truffle 4 available, though it does seem like Truffle is dominating at this point, so not essential for now. |
True. This approach works with other frameworks, too, since it is just a provider. I think it should default to looking for source mappings etc. for Truffle, though. This could probably be configured in the provider :) |
@onbjerg Sending you an org invite just in case you have any interest in experimenting with your approach in a branch here. 🙂 Also going to create 'V2' tag for the issues - we might be at the point where the steps we'll need to take are a little clearer. |
Great, thanks! I'll look at it this weekend if I have time, I'm in the process of moving 🚚 |
I'm on tenterhooks here, @onbjerg 😉 |
Implemented with Happy new year @area! |
Given that Truffle 4 is now in beta, and comes with a snazzy debugger that we can (in theory) crib notes from, it seems like a good moment to start a discussion along these lines.
On the surface, it seems like doing no instrumentation at all and using
debug_traceTransaction
alongside the demonstrated methodology in the truffle debugger would save a lot of headaches that instrumentation can otherwise cause.The obvious downside to this approach would be that we would no longer be able to trace lines of code executed with an
eth_call
, which would limit the maximum coverage we could display to a number below 100%. We could, however, exclude functions labelled withpure
andview
(formerly known asconstant
), though they can still obviously be called by transactions that change blockchain state, and so not showing them as covered might be confusing.I'm considering this a blue-skies issue for now, so I'm open to suggestions, however far-fetched they might seem. I'll kick it off:
etheruemjs-vm
, and introduce another opcode for our coverage events that cost no gas.ethereumjs-vm
, so that it 'knows' what code it is executing and keeps track of it.The text was updated successfully, but these errors were encountered: