-
Notifications
You must be signed in to change notification settings - Fork 939
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
Default ClassfileManager doesn't recover from partially successful compilation results #958
Comments
The ticket contains detailed description of the problem. The test case just shows that if we set `incOptions := sbt.inc.IncOptions.Default` then the incremental compiler doesn't recover from compiler errors properly.
The ticket contains detailed description of the problem. The test case just shows that if we set `incOptions := sbt.inc.IncOptions.Default` then the incremental compiler doesn't recover from compiler errors properly.
It seems to me that the problem is that A's class file does not correspond to A.scala's hash. I will have to look at the code, but if the check is |
Are you talking about this line: val removedProducts = previous.allProducts.filter( p => !equivS.equiv( previous.product(p), current.product(p) ) ).toSet I believe it uses this Equiv instance: implicit val equivStamp: Equiv[Stamp] = new Equiv[Stamp] {
def equiv(a: Stamp, b: Stamp) = (a,b) match {
case (h1: Hash, h2: Hash) => h1.value sameElements h2.value
case (e1: Exists, e2: Exists) => e1.value == e2.value
case (lm1: LastModified, lm2: LastModified) => lm1.value == lm2.value
case _ => false
}
} I don't know why this logic do not invalidate those products then. |
Yes, that's what I mean. There are two possibilities that I can think of. One is that the resolution of last modified is |
You are right that class files use Exists Stamp, see:
Do you think it would be safe to switch to last-modified stamps? |
Yes and no. I think |
I just tried diff --git a/compile/inc/src/main/scala/sbt/inc/Compile.scala b/compile/inc/src/main/scala/sbt/inc/Compile.scala
index ce1803c..b46ebf9 100644
--- a/compile/inc/src/main/scala/sbt/inc/Compile.scala
+++ b/compile/inc/src/main/scala/sbt/inc/Compile.scala
@@ -19,7 +19,7 @@ object IncrementalCompile
output: Output, log: Logger,
options: IncOptions): (Boolean, Analysis) =
{
- val current = Stamps.initial(Stamp.exists, Stamp.hash, Stamp.lastModified)
+ val current = Stamps.initial(Stamp.hash, Stamp.hash, Stamp.lastModified) All tests still pass and this issue is being fixed. I agree that both performance might be a concern. As far as I can see products Stamps are not cached. Could it be that they are recalculated for every iteration of incremental compiler? Also, I agree that LastModified might have unfortunate effect of introducing more recompilations triggered by external tools (e.g. IDE) overwriting class files. For that reasons I'd be in favor of hashing which is always correct. |
If last modified might be a performance concern, hashing is definitely one. Hashing would have the same problem- if you overwrite the class file, the hash would change. I think we can fall back on our ideal inc. compiler definition where the outputs should be the same as if you had done a full compilation. A full compilation would overwrite those class files. |
This is useful when an error occurs in a later incremental step that requires a fix in the originally changed files. CC @gkossakowski
I was thinking that hashing would help us with scenario where you use sbt and IDE at the same time. You hit save in IDE and sources are recompiled. Then you switch to sbt and run Therefore I agree that LastModified is enough. When it comes to performance I just tested it:
As you can see, there's no noticeable difference. |
Great! It looks like using last modified is the correct, immediate fix for this issue. |
@harrah: agreed! Does this warrant another RC of sbt 0.13.1? I can submit the fix tomorrow. |
The ticket contains detailed description of the problem. The test case just shows that if we set `incOptions := sbt.inc.IncOptions.Default` then the incremental compiler doesn't recover from compiler errors properly.
The #958 describes a scenario where partially successful results are produced in form of class files written to disk. However, if compilation fails down the road we do not record any new compilation results (products) in Analysis object. This leads to Analysis object and disk contents to get out of sync. One way to solve this problem is to use transactional ClassfileManager that commits changes to class files on disk only when entire incremental compilation session is successful. Otherwise, new class files are rolled back to previous state. The other way to solve this problem is to record time stamps of class files in Analysis object. This way, incremental compiler can detect that class files and Analysis object got out of sync and recover from that by recompiling corresponding sources. This commit uses latter solution which enables simpler (non-transactional) ClassfileManager to handle scenario from #958. Fixes #958
The ticket contains detailed description of the problem. The test case just shows that if we set `incOptions := sbt.inc.IncOptions.Default` then the incremental compiler doesn't recover from compiler errors properly.
The #958 describes a scenario where partially successful results are produced in form of class files written to disk. However, if compilation fails down the road we do not record any new compilation results (products) in Analysis object. This leads to Analysis object and disk contents to get out of sync. One way to solve this problem is to use transactional ClassfileManager that commits changes to class files on disk only when entire incremental compilation session is successful. Otherwise, new class files are rolled back to previous state. The other way to solve this problem is to record time stamps of class files in Analysis object. This way, incremental compiler can detect that class files and Analysis object got out of sync and recover from that by recompiling corresponding sources. This commit uses latter solution which enables simpler (non-transactional) ClassfileManager to handle scenario from #958. Fixes #958
Let's assume that
incOptions := sbt.inc.IncOptions.Default
which means we are using default compiler options. Let's start with these sources:and compile them. Then let's comment out
initialized
method:and try to compile. The incremental compiler will compile
A.scala
first, then it will try to recompileB.scala
and it will fail. The error will get reported as expected. Now, if we changeA.scala
back to its original content:and we try to compile again we expect it should succeed. However, it will report the same error as with second compile attempt.
Let's see what happened exactly. After commenting out
initialized
inA.scala
incremental compiler invoked Scala compiler that compiledA.scala
and produced a new class file for A. It failed to compileB.scala
so class file for B got removed. However, since the whole incremental compilation session failed the Analysis object rolled back so it has a hash of originalA.scala
file.Now, once we changed
A.scala
back to original file and we asked to recompile again. Since Analysis object contains the hash of original file the incremental compiler doesn't notice the change toA.scala
. However, since class file forB.scala
has been removed (and that wasn't recorded) it tries to recompileB.scala
and uses stale class file for A. Here's the relevant log output:The solution to this problem is to make ClassfileManager transactional so it commits changes to class files only when entire session of incremental compilation succeeds. This way Analysis object and class files stay in sync.
The text was updated successfully, but these errors were encountered: