-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Exclude from a report a part of bytecode that compiler generates for a synchronized statement and that is hard to cover by tests #501
Conversation
06ba2f1
to
117d36e
Compare
39baa53
to
9618e63
Compare
Analysis of 17391 classes from <project xmlns:jacoco="antlib:org.jacoco.ant" default="report">
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
<classpath path="jacocoant.jar"/>
</taskdef>
<target name="report">
<jacoco:report>
<executiondata>
<file file="jacoco.exec"/>
</executiondata>
<structure name="rt.jar">
<classfiles>
<fileset file="rt.jar"/>
</classfiles>
</structure>
</jacoco:report>
</target>
</project> takes 5 seconds on my laptop, both without this change as well as with it. So I believe we are safe to merge this. @marchof do you have objections or some review comments? |
c5a88c1
to
f0cc333
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Godin I added several comments for things that could be improved from my point of view (especially the documentation issues).
The unit testing facilities I commented about can be improved later from my point of view.
|
||
import org.objectweb.asm.tree.MethodNode; | ||
|
||
public interface IFilter { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the central API for filters. We should add some JavaDoc here. Especially about the usage contract: E.g. IFilter instances are re-used and must be thread safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof done ✅
|
||
import org.objectweb.asm.tree.AbstractInsnNode; | ||
|
||
public interface IFilterOutput { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JavaDoc would be nice too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof done ✅
|
||
public interface IFilterOutput { | ||
|
||
void ignore(AbstractInsnNode from, AbstractInsnNode to); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Javadoc: What exactly is the semantics of from
and to
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any constraints regarding from
and to
nodes? "to
must be a successor of from
in the instruction sequence. from
is inclusive, to
is exclusive".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof Actually both are inclusive. And we already use term "successor" in analysis of CFG, where its usage implies that not each instruction is a "successor" of preceding instruction from a list 😉 so not sure that "successor" will be a good unambiguous word here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof Javadoc added ✅
m.visitInsn(Opcodes.RETURN); | ||
|
||
new SynchronizedFilter().filter(m, this); | ||
assertEquals(m.instructions.get(12), from); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what means 12 or 16 here? Maybe we should enumerate instructions with comments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof fixed ✅
import org.objectweb.asm.tree.AbstractInsnNode; | ||
import org.objectweb.asm.tree.MethodNode; | ||
|
||
public class SynchronizedFilterTest implements IFilterOutput { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my point of view in JaCoCo core our unit tests should create full coverage on its own (with all paths). The validation tests come on top. Currently it looks like the only purpose of the unit test is the fill ECJ case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof I agree that code generated by javac should be captured in unit test and will do this.
However regarding
full coverage on its own (with all paths)
I doubt that generation of synthetic non-realistic bytecode in unit test or change of the code just to achieve full branch/path coverage makes sense. This is probably not that hard here for SynchronizedFilter
, but I expect this to be really tedious in case of filter for try-with-resources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof BTW we probably can exclude/separate verification tests from unit test coverage to enforce practice of completeness of unit tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof added bytecode that javac
produces and negative test. However this does not lead to a full branch coverage (few are still missed), but at least main paths are covered ✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Godin No problem. I assume we will need a (well tested) general purpose matcher facility soon. Then the number of branches within the specific filters will greatly reduce.
synchronized (lock) { // $line-monitorEnter$ | ||
nop(); // $line-body$ | ||
} // $line-monitorExit$ | ||
} // $line-after$ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about another nop() here? Otherwise we rely on the implicit return.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Such usage seems legitime to me given that implicit return
is always presented and always assigned to such line as verified by another test. Moreover we already use such pattern in Target03
and EnumImplicitMethods
, however in InterfaceDefaultMethodsTarget
and InterfaceOnlyDefaultMethodsTarget
we use explicit return
statements 😆 And BTW I've just realized that we test "before" and "after" only in case of exceptions, but not in case of control structures 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof added nop()
here so that to have a symmetry with before
marker at least in this test ✅
@@ -147,4 +148,8 @@ public void visitLookupSwitchInsnWithProbes(final Label dflt, | |||
final int[] keys, final Label[] labels, final IFrame frame) { | |||
} | |||
|
|||
public void accept(final MethodNode methodNode, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whooo, here we start mixing the ASM event and tree API. I consider this as a temporary hack until we fully move to the tree API ;-)
Nevertheless we should clearly document the method, especially the point in time when it is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Nothing is more permanent than the temporary" 😉 but future will show. We kind of already had such mix - see org.jacoco.core.internal.flow.LabelFlowAnalyzer#markLabels(org.objectweb.asm.tree.MethodNode)
, and exactly this gave me an idea of how to use ASM Tree API for filters without massive changes of everything, so this mix is worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevertheless we should clearly document the method, especially the point in time when it is called.
@marchof Not sure I understand this point - do you want to document method or its call site or both? And in any case - do you have idea about wording?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Godin The method itself:
/**
* This method can be overwritten to hook into the process of emitting the
* instructions of this method as <code>visitX()</code> events.
*
* @param methodNode
* the content to emit
* @param methodVisitor
* A visitor to emit the content to. Note that this is not
* necessarily this visitor instance but some wrapper which
* calculates the probes.
*/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof done ✅
@@ -108,6 +108,7 @@ public void visitEnd() { | |||
public void analyzeClass(final ClassReader reader) { | |||
final ClassVisitor visitor = createAnalyzingVisitor( | |||
CRC64.checksum(reader.b), reader.getClassName()); | |||
// TODO maybe we can SKIP_FRAMES here? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree in the comment but separate issue? On the other hand I'm not sure what the benefit would be as frames are ignored at analysis time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof I believe that in this case FrameNode
won't appear in methodNode.instructions
and since they anyway not used for analysis, we won't be wasting CPU cycles to skip them in filters - see method SynchronizedFilter.Matcher#next()
. While this doesn't play any (or at least not much) role for SynchronizedFilter
, the same method is used in filter for try-with-resources (#500). And now that you reminded me about that - probably I can demonstrate observable impact on performance using its implementation prior to the last optimizations. I'm fine with postponing this, however this shouldn't be forgotten and in fact I almost forgot about this, but good that left this comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for (final TryCatchBlockNode n : methodNode.tryCatchBlocks) { | ||
n.accept(methodVisitor); | ||
} | ||
for (int i = 0; i < methodNode.instructions.size(); i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a InsnList is traversed by index an internal array is created lazily. This can probably be avoided by using the linked list API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof as link in Javadoc shows - this was inspired by implementation of org.objectweb.asm.tree.MethodNode#accept(org.objectweb.asm.MethodVisitor)
, so strictly speaking there is no change in behavior in regards to creation of array. But you right that we can avoid this. And if so, then I'm wondering - how to prevent regression of this? Probably it can/should be avoided for instrumentation too? Are there other places that trigger or might trigger its creation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact I was wrong - default implementation in org.objectweb.asm.tree.MethodNode#accept(org.objectweb.asm.MethodVisitor)
does not construct array, don't know from where opposite came to my mind. Still wondering how to prevent such regressions, especially given that MethodNode#instructions
is accessible by filters now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only suggestions that come to mind are:
- Add build-time class file scanning for use of
InsnList.get(int)
. - Create
InsnList
subclass that throws and use it in unittests. I guess this would require subclassingClassAnalyzer
to overridevisitMethod
, which probably requires reimplementingAnalyzer.analyzeClass
/createAnalyzingVisitor
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More suggestions:
- A reflection based assertion that
InsnList.cache
is staysnull
- A JaCoCo coverage check that method
InsnList.toArray()
is not executed during our tests.
But I woudn't worry too much right now - unless performance testing shows a bottle neck here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And one more suggestion 😆 - InsnList
as well as whole MethodNode
comes to us via MethodSanitizer#visitEnd()
in ClassProbesAdapter
, where we can replace value of field instructions
(that is public in MethodNode
) by a new instance with overriden method toArray()
.
But I woudn't worry too much right now - unless performance testing shows a bottle neck here.
I've checked that our code doesn't trigger creation of array anymore.
So indeed - let's don't worry about this right now. ✅
@@ -25,6 +27,8 @@ | |||
|
|||
private Instruction predecessor; | |||
|
|||
public AbstractInsnNode node; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it is always set after the instance is created this should be final and set within the constructor.
For consistency I would prefer to make it private with a public getter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof indeed - this is a leftover of an early prototyping.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marchof fixed ✅
if (tryCatch.start == tryCatch.handler) { | ||
continue; | ||
} | ||
final AbstractInsnNode to = new Matcher(tryCatch.handler).match(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Number of allocations of Matcher
can be reduced here - can allocate and reuse just one for the whole lifetime of a method instead of allocation for TryCatchBlockNode
.
…r` marker instead of implicit return
@Godin Thanks for following up all my picky comments ;-) Let's merge this and continue with another filtering PR. I assume the overall implementation will improve step by step as we see more requirements and use cases. |
No description provided.