-
Notifications
You must be signed in to change notification settings - Fork 122
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
[1.x] Local dependency invalidation improvement #1528
base: 1.10.x
Are you sure you want to change the base?
Conversation
…re aggresive first class transitive invalidaton
log.debug("Invalidate by brute force:\n\t" + firstClassTransitiveInvalidation) | ||
firstClassTransitiveInvalidation ++ secondClassInvalidation ++ thirdClassInvalidation ++ recompiledClasses | ||
firstClassInvalidation ++ firstClassTransitiveInvalidation ++ secondClassInvalidation ++ |
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.
Now that I think of it, it may actually be enough to have
firstClassTransitiveInvalidation ++ secondClassInvalidation ++ thirdClassInvalidation ++ recompiledClasses
as firstClassTransitiveInvalidations
is now computed from firstClassInvalidation
@rochala Thanks for the contribution! I also want to ping @Friendseeker for his views since he's been contributing to Zinc lately. |
Could you elaborate? Zinc already invalidates everything after hitting certain threshold, namely 50% of invalidated sources, or 3 cycle count zinc/internal/compiler-interface/src/main/contraband-java/xsbti/compile/IncOptions.java Lines 12 to 17 in ea947ab
Are you suggesting that we should increase the threshold to 4 instead?
It seems like we had similar issue reported in 2017 ('Inherited trait definitions do not seem to be handled gracefully by Zinc' #417), and I sent #424 to start tracking previously untracked (?) inheritance within same source. The PR introduced zinc/internal/compiler-bridge/src/main/scala/xsbt/Dependency.scala Lines 124 to 125 in ea947ab
I guess my thinking back then was that transitive invalidation of inheritance needs to be strict, but transitive invalidation of member ref doesn't matter, which is the basis of name hashing theory. So if the same-source object |
val recompiledClasses = classesToRecompile ++ getClasses(previous) ++ getClasses(analysis) | ||
val recompiledClasses = classesToRecompile ++ | ||
getClasses(previous) ++ getClasses(analysis) ++ | ||
invalidatedSources.flatMap(previous.relations.classNames) |
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 makes sense.
@@ -184,13 +184,14 @@ trait Relations { | |||
private[inc] def externalDependencies: ExternalDependencies | |||
|
|||
/** | |||
* The class dependency relation between classes introduced by member reference. | |||
* The class dependency relation between classes introduced by member reference excluding local references |
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 Zinc, the term local often means within the same subproject, i.e. local-to-Analysis, so "excluding same-source references" might be less ambiguous.
* | ||
* NOTE: All inheritance dependencies are included in this relation because in order to | ||
* inherit from a member you have to refer to it. If you check documentation of `inheritance` | ||
* you'll see that there's small oddity related to traits being the first parent of a | ||
* class/trait that results in additional parents being introduced due to normalization. | ||
* This relation properly accounts for that so the invariant that `memberRef` is a superset | ||
* | ||
* Because `inheritance` includes local relations, `memberRef` is not a superset of `inheritance` |
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.
Same here.
Also regarding this point, one complication might be the fact that both Scala 3.x and 2.13.x have decided to split out the compiler bridge to a binary artifact published on Maven Central, as opposed to on-the-fly compiled-from-source. This means some range of the Scala version would always embed this characteristics (or issue, depending on how we look at it). So if possible, fixing at at the Zinc level like in IncrementalCommon would be preferable, all else being equal. |
@@ -149,11 +148,11 @@ private[inc] abstract class IncrementalCommon( | |||
invalidatedSources: Set[VirtualFile], | |||
classFileManager: XClassFileManager, | |||
pruned: Analysis, | |||
override val previousAnalysis: Analysis, |
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.
Is this change relevant to the fix?
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.
Nah, my thinking was to clean up as this parameter that was not necessary and introduced the confusion by duplication of parameters previous
in CycleState
vs previousAnalysis
here.
Shall review later today. My new school semester is starting in Jan 6 so things got a little busy on my end. |
@Friendseeker No worries, if you're busy. I think the TLDR of this PR is that in an attempt to fix same-file inheritance invalidation, I managed to create a logical loop hole in Zinc in 2017 via #424. |
It seems like entering a transitive invalidation path may actually extend compilation instead of making it faster. Let me explain:
// source source1
class A
class B:
val a: A = ???
// source source2
class C:
val b: B = ???
class D()
val c: C = ???
// source source3
class E:
val d: D = ???
class F()
val e :e = ???
// source sourceN...
class G:
val f: F = ???
class H()
val g: G = ??? And now if you want to make change in lets say A, we will need to recompile every source
I'm not proposing to remove it, I'm rather suggesting that at the current state, we may be better to change it a bit as it is not working as intended. My only worry is that this change will not be possible without including local references. |
Hello,
This is an attempt to fix undercompilation which can happen with local dependencies.
This PR is basically trying to solve 2 separate issues:
Relations.memberRef
is a superset ofRelations.inheritance
is no longer valid.zinc/internal/zinc-core/src/main/scala/sbt/internal/inc/Relations.scala
Lines 193 to 194 in ea947ab
zinc/internal/compiler-bridge/src/main/scala/xsbt/Dependency.scala
Lines 110 to 125 in ea947ab
Basically, since we don't include local dependencies in member references any more, we won't include local inheritance either, so the assumption is no longer true.
This leads to both local inheritance and local member reference to not invalidate correctly, which can be seen in both added test cases.
My attempt at solving this is to actually include all recompiled classes coming from the compilation unit at https://github.com/rochala/zinc/blob/8e6861fc77750b8acf484234ff07082a9785f940/internal/zinc-core/src/main/scala/sbt/internal/inc/IncrementalCommon.scala#L170-L176
This allows us to include API changes for local dependencies and solves the first issue.
Relations.memberRef
our logic forzinc/internal/zinc-core/src/main/scala/sbt/internal/inc/IncrementalCommon.scala
Lines 502 to 508 in ea947ab
firstClassInvalidations
instead ofinitialInvalidations
. This will be a bit more heavy to compute, but the result will be something that was in my intended in the first place.Side note: We've actually identified infinite compilation happening, because it turns out that
firstClassTransitiveInvalidations
was not a subset offirstClassInvalidations
, the reason why is the same as in point 1. The latter includes local inheritance, whereas the former does not.After some internal testing, we've gone from 12 cycles to 4 in one problematic module. The reason of such high number of cycles was that we did not include local dependencies (and we had a lot of classes defined in a single source) thus they were not considered recompiled, while in fact they were.
Now the question time:
Q1. I see also another solution to those problems, we could try to meet the
membersRef
requirement and include local dependencies. This would also solve the issue with transitive invalidations. Do you know the reason why we ignored these kinds of dependencies ?Q2. Right now, transitive invalidation may actually result in a lot more compilation happening (it actually recompiles all sources once again). Maybe a whole different approach should be considered, e.g. invalidate everything after 4th cycle or something.