Skip to content

Conversation

@mbovel
Copy link
Member

@mbovel mbovel commented Dec 1, 2025

This PR addresses a performance regression recently found between Scala 3.7.4 and 3.8.0. The root cause is that we unzip many Tasty files that we don’t actually need, because TastyLoader reads files eagerly, whereas ClassfileLoader does so lazily. The issue became visible with the new standard library compiled by Scala 3, which now generates Tasty files.

By logging Abstract.toByteArray calls, I measured that we loaded 641 Tasty files (4.49 MB) to compile hello-world before this PR, whereas we now load only 100 Tasty files (0.80 MB) afterward.

mbovel referenced this pull request in dotty-staging/dotty Dec 1, 2025
This PR introduces 2 new experimental options:
`-Ybest-effort` and `-Ywith-best-effort-tasty`. It also introduces the
Best Effort TASTy format (BETASTy), a TASTy aligned file format able to hold some
errored trees. Behaviour of the options and the format is documented as
part of this PR in the `best-effort-compilation.md` docs file.

`-Ybest-effort` allows to create .betasty files in a failing compilation, while
`-Ywith-best-effort-tasty` allows to read them in subsequent compilations,
so that we can get information needed for IDEs from broken modules, and
modules depending on them. It is worth noting that these compilation
depending on Betty will never reach transform phases, and will never produce
any artifacts other then betasty and semanticdb. 

My overall aim was to be able to handle as many cases, with little as
little maintainance necessary as possible. This is for example why
pretty much the only required phases are Parser and Typer - they are
enough for, as far as I know, all necessary metals completions and I did
not find any issues with setting their products (best effort tasty
files) as dependencies. Requiring, for example, PostTyper, would require
for the errored trees to be able to somehow pass through that phase,
meaning a large investment from me into working the existing known error
cases through there (muddling the codebase in the process) and possibly
from the maintainers working on both Typer (to be able to produce
„correct” error trees) and PostTyper (to be able to consume „correct”
errored trees), which would obviously make the entire initiative
dreadful.

This is also why any tests are able to be put into a blacklist file, in
case something changes and a neg test will not pass, or a new test will
be added as part of issue fix that does not play well with the
best-effort features.
@mbovel mbovel force-pushed the mb/lazy-tasty-unitInfo branch from d072c42 to 4140995 Compare December 1, 2025 15:14
@mbovel mbovel marked this pull request as ready for review December 1, 2025 16:09
@mbovel mbovel requested a review from hamzaremmal December 1, 2025 16:11
@hamzaremmal
Copy link
Member

whereas we now load only 100 Tasty files

For a hello world program, this still feels like a lot. At least, it is better than 641 files.

@mbovel
Copy link
Member Author

mbovel commented Dec 1, 2025

For a hello world program, this still feels like a lot. At least, it is better than 641 files.

Yes, I think it would be interesting to try to reduce that number further in the future.

Here is what we currently load for hello world
ClassfileParser.run: /modules/java.base/java/lang/Class.class of size Some(54066)
ClassfileParser.run: /modules/java.base/java/lang/invoke/TypeDescriptor.class of size Some(353)
ClassfileParser.run: /modules/java.base/java/io/Serializable.class of size Some(113)
ClassfileParser.run: /modules/java.base/java/lang/reflect/GenericDeclaration.class of size Some(304)
ClassfileParser.run: /modules/java.base/java/lang/reflect/AnnotatedElement.class of size Some(4007)
ClassfileParser.run: /modules/java.base/java/lang/reflect/Type.class of size Some(306)
ClassfileParser.run: /modules/java.base/java/lang/constant/Constable.class of size Some(252)
ClassfileParser.run: /modules/java.base/java/lang/String.class of size Some(47651)
ClassfileParser.run: /modules/java.base/java/lang/CharSequence.class of size Some(2828)
ClassfileParser.run: /modules/java.base/java/lang/constant/ConstantDesc.class of size Some(716)
ClassfileParser.run: /modules/java.base/java/lang/Cloneable.class of size Some(109)
ClassfileParser.run: /modules/java.base/java/lang/invoke/TypeDescriptor$OfField.class of size Some(518)
ClassfileParser.run: /modules/java.base/java/lang/Comparable.class of size Some(235)
ClassfileParser.run: /modules/java.base/java/lang/ClassValue.class of size Some(5890)
ClassfileParser.run: /modules/java.base/java/lang/ClassValue$ClassValueMap.class of size Some(9384)
ClassfileParser.run: /modules/java.base/java/lang/ClassValue$Identity.class of size Some(380)
ClassfileParser.run: /modules/java.base/java/lang/ClassLoader.class of size Some(32310)
ClassfileParser.run: /modules/java.base/java/lang/IllegalAccessException.class of size Some(536)
ClassfileParser.run: /modules/java.base/java/lang/ReflectiveOperationException.class of size Some(828)
ClassfileParser.run: /modules/java.base/java/lang/Exception.class of size Some(983)
ClassfileParser.run: /modules/java.base/java/lang/Throwable.class of size Some(10432)
ClassfileParser.run: /modules/java.base/java/lang/InstantiationException.class of size Some(536)
ClassfileParser.run: /modules/java.base/java/lang/Deprecated.class of size Some(647)
ClassfileParser.run: /modules/java.base/java/lang/annotation/ElementType.class of size Some(1658)
ClassfileParser.run: /modules/java.base/java/lang/annotation/Annotation.class of size Some(355)
ClassfileParser.run: /modules/java.base/jdk/internal/reflect/CallerSensitive.class of size Some(404)
ClassfileParser.run: /modules/java.base/jdk/internal/vm/annotation/IntrinsicCandidate.class of size Some(435)
ClassfileParser.run: /modules/java.base/jdk/internal/vm/annotation/ForceInline.class of size Some(421)
ClassfileParser.run: /modules/java.base/java/lang/Module.class of size Some(28932)
ClassfileParser.run: /modules/java.base/java/lang/reflect/TypeVariable.class of size Some(589)
ClassfileParser.run: /modules/java.base/java/lang/Package.class of size Some(8826)
ClassfileParser.run: /modules/java.base/java/lang/NamedPackage.class of size Some(2157)
ClassfileParser.run: /modules/java.base/java/lang/SecurityException.class of size Some(806)
ClassfileParser.run: /modules/java.base/java/lang/RuntimeException.class of size Some(1004)
ClassfileParser.run: /modules/java.base/java/lang/reflect/Method.class of size Some(12738)
ClassfileParser.run: /modules/java.base/java/lang/reflect/Executable.class of size Some(15219)
ClassfileParser.run: /modules/java.base/java/lang/reflect/AccessibleObject.class of size Some(10946)
ClassfileParser.run: /modules/java.base/java/lang/reflect/Member.class of size Some(386)
ClassfileParser.run: /modules/java.base/java/lang/reflect/Constructor.class of size Some(13323)
ClassfileParser.run: /modules/java.base/java/lang/reflect/Field.class of size Some(14149)
ClassfileParser.run: /modules/java.base/java/lang/NoSuchFieldException.class of size Some(530)
ClassfileParser.run: /modules/java.base/java/lang/NoSuchMethodException.class of size Some(533)
ClassfileParser.run: /modules/java.base/java/lang/reflect/RecordComponent.class of size Some(5124)
ClassfileParser.run: /modules/java.base/java/util/List.class of size Some(8475)
ClassfileParser.run: /modules/java.base/java/io/InputStream.class of size Some(4612)
ClassfileParser.run: /modules/java.base/java/io/Closeable.class of size Some(208)
ClassfileParser.run: /modules/java.base/java/lang/AutoCloseable.class of size Some(187)
ClassfileParser.run: /modules/java.base/java/net/URL.class of size Some(19760)
ClassfileParser.run: /modules/java.base/java/security/ProtectionDomain.class of size Some(7913)
ClassfileParser.run: /modules/java.base/jdk/internal/reflect/ConstantPool.class of size Some(3786)
ClassfileParser.run: /modules/java.base/java/util/Map.class of size Some(14745)
ClassfileParser.run: /modules/java.base/sun/reflect/annotation/AnnotationType.class of size Some(6172)
ClassfileParser.run: /modules/java.base/java/lang/reflect/AnnotatedType.class of size Some(687)
ClassfileParser.run: /modules/java.base/java/util/Optional.class of size Some(6324)
ClassfileParser.run: /modules/java.base/java/lang/constant/ClassDesc.class of size Some(5417)
ClassfileParser.run: /modules/java.base/java/io/UnsupportedEncodingException.class of size Some(531)
ClassfileParser.run: /modules/java.base/java/io/IOException.class of size Some(773)
ClassfileParser.run: /modules/java.base/java/nio/charset/Charset.class of size Some(9260)
ClassfileParser.run: /modules/java.base/java/lang/StringBuffer.class of size Some(14820)
ClassfileParser.run: /modules/java.base/java/lang/AbstractStringBuilder.class of size Some(19785)
ClassfileParser.run: /modules/java.base/java/lang/Appendable.class of size Some(344)
ClassfileParser.run: /modules/java.base/java/lang/StringBuilder.class of size Some(12908)
ClassfileParser.run: /modules/java.base/java/util/Locale.class of size Some(32807)
ClassfileParser.run: /modules/java.base/java/util/stream/Stream.class of size Some(14434)
ClassfileParser.run: /modules/java.base/java/util/function/Function.class of size Some(2392)
ClassfileParser.run: /modules/java.base/java/util/stream/IntStream.class of size Some(8719)
ClassfileParser.run: /modules/java.base/java/lang/Integer.class of size Some(15728)
ClassfileParser.run: /modules/java.base/java/lang/Number.class of size Some(632)
ClassfileParser.run: /modules/java.base/java/lang/Void.class of size Some(519)
ClassfileParser.run: /modules/java.base/java/lang/invoke/MethodHandles.class of size Some(56404)
ClassfileParser.run: /modules/java.base/java/lang/invoke/MethodHandles$Lookup.class of size Some(40501)
ClassfileParser.run: /modules/java.base/java/lang/Enum.class of size Some(4911)
ClassfileParser.run: /modules/java.base/java/lang/invoke/TypeDescriptor$OfMethod.class of size Some(1357)
ClassfileParser.run: /modules/java.base/java/lang/Class$ReflectionData.class of size Some(1225)
ClassfileParser.run: /modules/java.base/java/lang/Class$Holder.class of size Some(866)
ClassfileParser.run: /modules/java.base/java/lang/Class$Atomic.class of size Some(2472)
ClassfileParser.run: /modules/java.base/java/lang/Class$EnclosingMethodInfo.class of size Some(2254)
ClassfileParser.run: /modules/java.base/java/lang/Class$AnnotationData.class of size Some(1013)
ClassfileParser.run: /modules/java.base/java/lang/ClassNotFoundException.class of size Some(2208)
ClassfileParser.run: /modules/java.base/java/lang/Boolean.class of size Some(3946)
ClassfileParser.run: /modules/java.base/jdk/internal/ValueBased.class of size Some(384)
ClassfileParser.run: /modules/java.base/java/lang/annotation/Target.class of size Some(483)
ClassfileParser.run: /modules/java.base/java/lang/annotation/Retention.class of size Some(492)
ClassfileParser.run: /modules/java.base/java/lang/annotation/Documented.class of size Some(443)
ClassfileParser.run: /modules/java.base/java/lang/Byte.class of size Some(5282)
ClassfileParser.run: /modules/java.base/java/lang/Short.class of size Some(5515)
ClassfileParser.run: /modules/java.base/java/lang/Character.class of size Some(18118)
ClassfileParser.run: /modules/java.base/java/lang/Long.class of size Some(16129)
ClassfileParser.run: /modules/java.base/java/lang/Float.class of size Some(5844)
ClassfileParser.run: /modules/java.base/java/lang/Double.class of size Some(6673)
ClassfileParser.run: /modules/java.base/java/lang/String$CaseInsensitiveComparator.class of size Some(1470)
ClassfileParser.run: /modules/java.base/java/nio/charset/CharacterCodingException.class of size Some(398)
AbstractFile.toByteArray: /localhome/bovel/scala3/hello.scala of size Some(111)
AbstractFile.toByteArray: scala/Unit.tasty of size Some(2347)
AbstractFile.toByteArray: scala/caps/package$package.tasty of size Some(1375)
AbstractFile.toByteArray: scala/caps/internal.tasty of size Some(2778)
AbstractFile.toByteArray: scala/compiletime/package$package.tasty of size Some(7664)
AbstractFile.toByteArray: scala/Predef.tasty of size Some(34982)
AbstractFile.toByteArray: scala/runtime/$throws$package.tasty of size Some(1019)
AbstractFile.toByteArray: scala/runtime/stdLibPatches/Predef.tasty of size Some(4530)
AbstractFile.toByteArray: scala/annotation/internal/RuntimeChecked.tasty of size Some(720)
AbstractFile.toByteArray: scala/collection/package.tasty of size Some(5168)
AbstractFile.toByteArray: scala/collection/immutable/package.tasty of size Some(1288)
AbstractFile.toByteArray: scala/annotation/Annotation.tasty of size Some(974)
AbstractFile.toByteArray: scala/annotation/internal/InlineParam.tasty of size Some(546)
AbstractFile.toByteArray: scala/Boolean.tasty of size Some(5219)
AbstractFile.toByteArray: scala/annotation/retainsByName.tasty of size Some(637)
AbstractFile.toByteArray: scala/annotation/StaticAnnotation.tasty of size Some(804)
AbstractFile.toByteArray: scala/annotation/internal/SourceFile.tasty of size Some(573)
AbstractFile.toByteArray: scala/LowPriorityImplicits.tasty of size Some(6823)
AbstractFile.toByteArray: scala/LowPriorityImplicits2.tasty of size Some(2221)
AbstractFile.toByteArray: scala/annotation/experimental.tasty of size Some(757)
AbstractFile.toByteArray: scala/caps/cap.tasty of size Some(1062)
AbstractFile.toByteArray: scala/Array.tasty of size Some(38241)
AbstractFile.toByteArray: scala/Int.tasty of size Some(18777)
AbstractFile.toByteArray: scala/language.tasty of size Some(47203)
AbstractFile.toByteArray: scala/runtime/stdLibPatches/language.tasty of size Some(36421)
AbstractFile.toByteArray: scala/annotation/meta/package.tasty of size Some(4758)
AbstractFile.toByteArray: scala/runtime/ModuleSerializationProxy.tasty of size Some(2187)
AbstractFile.toByteArray: scala/SerialVersionUID.tasty of size Some(1093)
AbstractFile.toByteArray: scala/annotation/ConstantAnnotation.tasty of size Some(1667)
AbstractFile.toByteArray: scala/Long.tasty of size Some(18542)
AbstractFile.toByteArray: scala/AnyVal.tasty of size Some(2536)
AbstractFile.toByteArray: scala/caps/unsafe.tasty of size Some(2812)
AbstractFile.toByteArray: scala/transient.tasty of size Some(517)
AbstractFile.toByteArray: scala/throws.tasty of size Some(1964)
AbstractFile.toByteArray: scala/package.tasty of size Some(6450)
AbstractFile.toByteArray: scala/deprecated.tasty of size Some(8022)
AbstractFile.toByteArray: scala/annotation/internal/AnnotationDefault.tasty of size Some(544)
AbstractFile.toByteArray: scala/native.tasty of size Some(932)
AbstractFile.toByteArray: scala/collection/immutable/Seq.tasty of size Some(1960)
AbstractFile.toByteArray: scala/Byte.tasty of size Some(18706)
AbstractFile.toByteArray: scala/Char.tasty of size Some(18690)
AbstractFile.toByteArray: scala/collection/immutable/List.tasty of size Some(24437)
AbstractFile.toByteArray: scala/collection/StrictOptimizedSeqFactory.tasty of size Some(3630)
AbstractFile.toByteArray: scala/collection/Seq.tasty of size Some(3756)
AbstractFile.toByteArray: scala/collection/SeqOps.tasty of size Some(63744)
AbstractFile.toByteArray: scala/collection/SeqFactory.tasty of size Some(4416)
AbstractFile.toByteArray: scala/caps/Pure.tasty of size Some(1036)
AbstractFile.toByteArray: scala/collection/IterableFactory.tasty of size Some(20315)
AbstractFile.toByteArray: scala/annotation/internal/Repeated.tasty of size Some(644)
AbstractFile.toByteArray: scala/collection/immutable/AbstractSeq.tasty of size Some(874)
AbstractFile.toByteArray: scala/collection/immutable/LinearSeq.tasty of size Some(1830)
AbstractFile.toByteArray: scala/collection/immutable/LinearSeqOps.tasty of size Some(1046)
AbstractFile.toByteArray: scala/collection/StrictOptimizedLinearSeqOps.tasty of size Some(2400)
AbstractFile.toByteArray: scala/collection/immutable/StrictOptimizedSeqOps.tasty of size Some(4046)
AbstractFile.toByteArray: scala/collection/IterableFactoryDefaults.tasty of size Some(3087)
AbstractFile.toByteArray: scala/collection/generic/package.tasty of size Some(1823)
AbstractFile.toByteArray: scala/collection/IterableOps.tasty of size Some(51460)
AbstractFile.toByteArray: scala/annotation/retainsCap.tasty of size Some(592)
AbstractFile.toByteArray: scala/collection/IterableOnce.tasty of size Some(9135)
AbstractFile.toByteArray: scala/collection/IterableOnceOps.tasty of size Some(65471)
AbstractFile.toByteArray: scala/collection/immutable/Iterable.tasty of size Some(1884)
AbstractFile.toByteArray: scala/collection/Iterable.tasty of size Some(7817)
AbstractFile.toByteArray: scala/PartialFunction.tasty of size Some(25833)
AbstractFile.toByteArray: scala/collection/immutable/SeqOps.tasty of size Some(977)
AbstractFile.toByteArray: scala/annotation/unchecked/uncheckedVariance.tasty of size Some(587)
AbstractFile.toByteArray: scala/Function1.tasty of size Some(4637)
AbstractFile.toByteArray: scala/Equals.tasty of size Some(1251)
AbstractFile.toByteArray: scala/inline.tasty of size Some(2150)
AbstractFile.toByteArray: scala/collection/immutable/Map.tasty of size Some(36814)
AbstractFile.toByteArray: scala/Tuple2.tasty of size Some(4318)
AbstractFile.toByteArray: scala/collection/Map.tasty of size Some(5520)
AbstractFile.toByteArray: scala/collection/MapOps.tasty of size Some(25853)
AbstractFile.toByteArray: scala/collection/MapFactoryDefaults.tasty of size Some(4783)
AbstractFile.toByteArray: scala/collection/immutable/MapOps.tasty of size Some(7791)
AbstractFile.toByteArray: scala/collection/immutable/Set.tasty of size Some(17662)
AbstractFile.toByteArray: scala/collection/Set.tasty of size Some(4238)
AbstractFile.toByteArray: scala/collection/SetOps.tasty of size Some(10878)
AbstractFile.toByteArray: scala/collection/immutable/SetOps.tasty of size Some(3297)
AbstractFile.toByteArray: scala/reflect/package.tasty of size Some(2721)
AbstractFile.toByteArray: scala/reflect/Typeable$package.tasty of size Some(1201)
AbstractFile.toByteArray: scala/reflect/OptManifest.tasty of size Some(651)
AbstractFile.toByteArray: scala/reflect/Manifest.tasty of size Some(8039)
AbstractFile.toByteArray: scala/reflect/ClassTag.tasty of size Some(8974)
AbstractFile.toByteArray: scala/reflect/ClassManifestDeprecatedApis.tasty of size Some(8558)
AbstractFile.toByteArray: scala/annotation/implicitNotFound.tasty of size Some(2005)
AbstractFile.toByteArray: scala/annotation/nowarn.tasty of size Some(4046)
AbstractFile.toByteArray: scala/annotation/targetName.tasty of size Some(760)
AbstractFile.toByteArray: scala/annotation/publicInBinary.tasty of size Some(1472)
AbstractFile.toByteArray: scala/annotation/internal/Body.tasty of size Some(572)
AbstractFile.toByteArray: scala/deprecatedInheritance.tasty of size Some(5298)
AbstractFile.toByteArray: scala/runtime/Arrays.tasty of size Some(2261)
AbstractFile.toByteArray: scala/annotation/meta/beanSetter.tasty of size Some(546)
AbstractFile.toByteArray: scala/annotation/meta/beanGetter.tasty of size Some(546)
AbstractFile.toByteArray: scala/annotation/meta/setter.tasty of size Some(537)
AbstractFile.toByteArray: scala/annotation/meta/getter.tasty of size Some(537)
AbstractFile.toByteArray: scala/runtime/Null$.tasty of size Some(717)
AbstractFile.toByteArray: scala/runtime/Nothing$.tasty of size Some(623)
AbstractFile.toByteArray: scala/Float.tasty of size Some(12200)
AbstractFile.toByteArray: scala/Double.tasty of size Some(12153)
AbstractFile.toByteArray: scala/Short.tasty of size Some(18677)
AbstractFile.toByteArray: scala/annotation/meta/field.tasty of size Some(535)

@hamzaremmal hamzaremmal added this to the 3.8.0 milestone Dec 1, 2025
@hamzaremmal hamzaremmal added the backport:nominated If we agree to backport this PR, replace this tag with "backport:accepted", otherwise delete it. label Dec 1, 2025
@hamzaremmal
Copy link
Member

For a hello world program, this still feels like a lot. At least, it is better than 641 files.

Yes, I think it would be interesting to try to reduce further in the future.

Here is what we currently load for hello world

I guess we are currently loading anything reachable from java.lang.String and scala.Predef, not sure.

@hamzaremmal hamzaremmal merged commit 70e4b2d into scala:main Dec 1, 2025
46 checks passed
@hamzaremmal hamzaremmal deleted the mb/lazy-tasty-unitInfo branch December 1, 2025 17:25
@WojciechMazur WojciechMazur added backport:accepted This PR needs to be backported, once it's been backported replace this tag by "backport:done" and removed backport:nominated If we agree to backport this PR, replace this tag with "backport:accepted", otherwise delete it. labels Dec 1, 2025
WojciechMazur added a commit that referenced this pull request Dec 2, 2025
…24616)

Backports #24604 to the 3.8.0-RC3.

PR submitted by the release tooling.
[skip ci]
@WojciechMazur WojciechMazur added backport:done This PR was successfully backported. and removed backport:accepted This PR needs to be backported, once it's been backported replace this tag by "backport:done" labels Dec 2, 2025
@mbovel
Copy link
Member Author

mbovel commented Dec 2, 2025

Performance at 3.8.0-RC2 and the latest nightly (with this PR merged):

image

@sjrd
Copy link
Member

sjrd commented Dec 2, 2025

When the scale allows it, you should base the Y axis on 0. As is, the graph is misleading. (and it's a shame because it's genuinely better).

@mbovel
Copy link
Member Author

mbovel commented Dec 2, 2025

Here is one with the Y axis starting at 0:

image

In the web visualizer, I have a checkbox allowing to start at 0 or not; I think both are useful depending on what you want to see.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:done This PR was successfully backported.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants