Skip to content

Do not crash in backend when method is too large #14213

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

Closed
14sxlin opened this issue Jan 5, 2022 · 9 comments
Closed

Do not crash in backend when method is too large #14213

14sxlin opened this issue Jan 5, 2022 · 9 comments
Labels
area:backend good first issue Perfect for someone who wants to get started contributing itype:bug itype:crash Spree Suitable for a future Spree

Comments

@14sxlin
Copy link

14sxlin commented Jan 5, 2022

When I use Mirror of scala 3 to generate a typeclass list, the exception occurs. I know it's the hard limit of jvm of method size, but how can I circumvent this issue.

ps: When delete some fields of Data class it works, but any other solution?

Compiler version

sbt: 1.6.0

scala: 3.1.0

Minimized example

import scala.compiletime.*
import scala.deriving.Mirror

object Main extends App {

  trait FromString[A] {
    def convert(str: String): A
  }
  object FromString {
    given FromString[Int] = (str) => str.toInt
    given FromString[Double] = (str) => str.toDouble
    given FromString[String] = (str) => str
  }

  inline def getTypeclassInstances[F[_], A <: Tuple]: List[F[Any]] =
    inline erasedValue[A] match {
      case _: EmptyTuple => Nil
      case _: (head *: tail) =>
        val headTypeClass =
          summonInline[F[head]]
        val tailTypeClasses =
          getTypeclassInstances[F, tail]
        headTypeClass.asInstanceOf[F[Any]] :: getTypeclassInstances[F, tail]
    }

  inline def summonInstancesHelper[F[_], A](using
      m: Mirror.Of[A]
  ): List[F[Any]] =
    getTypeclassInstances[F, m.MirroredElemTypes]

  case class Data(
      ip: String,
      method: String,
      uri: String,
      protocal: String,
      httpStatus: Int,
      byteSent: Double,
      reqLength: Double,
      reqTime: Double,
      respTime: Double,
      referer: String,
      device: String
  )

  val types =
    summonInstancesHelper[FromString, Data]
  println(types.mkString("\r\n"))
}

Output

[error] scala.tools.asm.MethodTooLargeException: Method too large: parse/Main$.<clinit> ()V
[error] scala.tools.asm.MethodWriter.computeMethodInfoSize(MethodWriter.java:2087)
[error] scala.tools.asm.ClassWriter.toByteArray(ClassWriter.java:489)
[error] dotty.tools.backend.jvm.GenBCodePipeline$Worker2.getByteArray$1(GenBCode.scala:478)
[error] dotty.tools.backend.jvm.GenBCodePipeline$Worker2.addToQ3(GenBCode.scala:484)
[error] dotty.tools.backend.jvm.GenBCodePipeline$Worker2.run(GenBCode.scala:461)
[error] dotty.tools.backend.jvm.GenBCodePipeline.buildAndSendToDisk(GenBCode.scala:562)
[error] dotty.tools.backend.jvm.GenBCodePipeline.run(GenBCode.scala:525)
[error] dotty.tools.backend.jvm.GenBCode.run(GenBCode.scala:63)
[error] dotty.tools.dotc.core.Phases$Phase.runOn$$anonfun$1(Phases.scala:308)
[error] scala.collection.immutable.List.map(List.scala:246)
[error] dotty.tools.dotc.core.Phases$Phase.runOn(Phases.scala:309)
[error] dotty.tools.backend.jvm.GenBCode.runOn(GenBCode.scala:71)
[error] dotty.tools.dotc.Run.runPhases$4$$anonfun$4(Run.scala:261)
[error] scala.runtime.function.JProcedure1.apply(JProcedure1.java:15)
[error] scala.runtime.function.JProcedure1.apply(JProcedure1.java:10)
[error] scala.collection.ArrayOps$.foreach$extension(ArrayOps.scala:1323)
[error] dotty.tools.dotc.Run.runPhases$5(Run.scala:272)
[error] dotty.tools.dotc.Run.compileUnits$$anonfun$1(Run.scala:280)
[error] scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
[error] dotty.tools.dotc.util.Stats$.maybeMonitored(Stats.scala:68)
[error] dotty.tools.dotc.Run.compileUnits(Run.scala:289)
[error] dotty.tools.dotc.Run.compileSources(Run.scala:222)
[error] dotty.tools.dotc.Run.compile(Run.scala:206)
[error] dotty.tools.dotc.Driver.doCompile(Driver.scala:39)
[error] dotty.tools.xsbt.CompilerBridgeDriver.run(CompilerBridgeDriver.java:88)
[error] dotty.tools.xsbt.CompilerBridge.run(CompilerBridge.java:22)
[error] sbt.internal.inc.AnalyzingCompiler.compile(AnalyzingCompiler.scala:91)
[error] sbt.internal.inc.MixedAnalyzingCompiler.$anonfun$compile$7(MixedAnalyzingCompiler.scala:192)
[error] scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23)
[error] sbt.internal.inc.MixedAnalyzingCompiler.timed(MixedAnalyzingCompiler.scala:247)
[error] sbt.internal.inc.MixedAnalyzingCompiler.$anonfun$compile$4(MixedAnalyzingCompiler.scala:182)
[error] sbt.internal.inc.MixedAnalyzingCompiler.$anonfun$compile$4$adapted(MixedAnalyzingCompiler.scala:163)
[error] sbt.internal.inc.JarUtils$.withPreviousJar(JarUtils.scala:239)
[error] sbt.internal.inc.MixedAnalyzingCompiler.compileScala$1(MixedAnalyzingCompiler.scala:163)
[error] sbt.internal.inc.MixedAnalyzingCompiler.compile(MixedAnalyzingCompiler.scala:210)
[error] sbt.internal.inc.IncrementalCompilerImpl.$anonfun$compileInternal$1(IncrementalCompilerImpl.scala:528)
[error] sbt.internal.inc.IncrementalCompilerImpl.$anonfun$compileInternal$1$adapted(IncrementalCompilerImpl.scala:528)
[error] sbt.internal.inc.Incremental$.$anonfun$apply$5(Incremental.scala:177)
[error] sbt.internal.inc.Incremental$.$anonfun$apply$5$adapted(Incremental.scala:175)
[error] sbt.internal.inc.Incremental$$anon$2.run(Incremental.scala:461)
[error] sbt.internal.inc.IncrementalCommon$CycleState.next(IncrementalCommon.scala:116)
[error] sbt.internal.inc.IncrementalCommon$$anon$1.next(IncrementalCommon.scala:56)
[error] sbt.internal.inc.IncrementalCommon$$anon$1.next(IncrementalCommon.scala:52)
[error] sbt.internal.inc.IncrementalCommon.cycle(IncrementalCommon.scala:263)
[error] sbt.internal.inc.Incremental$.$anonfun$incrementalCompile$8(Incremental.scala:416)
[error] sbt.internal.inc.Incremental$.withClassfileManager(Incremental.scala:503)
[error] sbt.internal.inc.Incremental$.incrementalCompile(Incremental.scala:403)
[error] sbt.internal.inc.Incremental$.apply(Incremental.scala:169)
[error] sbt.internal.inc.IncrementalCompilerImpl.compileInternal(IncrementalCompilerImpl.scala:528)
[error] sbt.internal.inc.IncrementalCompilerImpl.$anonfun$compileIncrementally$1(IncrementalCompilerImpl.scala:482)
[error] sbt.internal.inc.IncrementalCompilerImpl.handleCompilationError(IncrementalCompilerImpl.scala:332)
[error] sbt.internal.inc.IncrementalCompilerImpl.compileIncrementally(IncrementalCompilerImpl.scala:420)
[error] sbt.internal.inc.IncrementalCompilerImpl.compile(IncrementalCompilerImpl.scala:137)
[error] sbt.Defaults$.compileIncrementalTaskImpl(Defaults.scala:2366)
[error] sbt.Defaults$.$anonfun$compileIncrementalTask$2(Defaults.scala:2316)
[error] sbt.internal.server.BspCompileTask$.$anonfun$compute$1(BspCompileTask.scala:30)
[error] sbt.internal.io.Retry$.apply(Retry.scala:46)
[error] sbt.internal.io.Retry$.apply(Retry.scala:28)
[error] sbt.internal.io.Retry$.apply(Retry.scala:23)
[error] sbt.internal.server.BspCompileTask$.compute(BspCompileTask.scala:30)
[error] sbt.Defaults$.$anonfun$compileIncrementalTask$1(Defaults.scala:2314)
[error] scala.Function1.$anonfun$compose$1(Function1.scala:49)
[error] sbt.internal.util.$tilde$greater.$anonfun$$u2219$1(TypeFunctions.scala:62)
[error] sbt.std.Transform$$anon$4.work(Transform.scala:68)
[error] sbt.Execute.$anonfun$submit$2(Execute.scala:282)
[error] sbt.internal.util.ErrorHandling$.wideConvert(ErrorHandling.scala:23)
[error] sbt.Execute.work(Execute.scala:291)
[error] sbt.Execute.$anonfun$submit$1(Execute.scala:282)
[error] sbt.ConcurrentRestrictions$$anon$4.$anonfun$submitValid$1(ConcurrentRestrictions.scala:265)
[error] sbt.CompletionService$$anon$2.call(CompletionService.scala:64)
[error] java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error] java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
[error] java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error] java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
[error] java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
[error] java.lang.Thread.run(Thread.java:748)
[error]
[error] stack trace is suppressed; run last Compile / compileIncremental for the full output
[error] (Compile / compileIncremental) scala.tools.asm.MethodTooLargeException: Method too large: parse/Main$.<clinit> ()V
[error] Total time: 2 s, completed 2022-1-5 14:11:19
@bishabosha
Copy link
Member

bishabosha commented Jan 5, 2022

you have duplicated your calls to getTypeclassInstances, without that it compiles though the backend:

 inline def getTypeclassInstances[F[_], A <: Tuple]: List[F[Any]] =
   inline erasedValue[A] match {
     case _: EmptyTuple => Nil
     case _: (head *: tail) =>
       val headTypeClass =
         summonInline[F[head]]
       val tailTypeClasses =
         getTypeclassInstances[F, tail]
-      headTypeClass.asInstanceOf[F[Any]] :: getTypeclassInstances[F, tail]
+      headTypeClass.asInstanceOf[F[Any]] :: tailTypeClasses
   }

in general though this could still crash with a very large case class - perhaps you can change headTypeClass and tailTypeClasses to def instead of val.

@14sxlin
Copy link
Author

14sxlin commented Jan 5, 2022

Oh, Good advice ! Thank you, it does help me.

@bishabosha bishabosha changed the title Method Too large when using Mirror Do not crash when method is too large Jan 5, 2022
@bishabosha
Copy link
Member

I say we could reopen this to give a better error message to the user, rather than crash

@bishabosha bishabosha reopened this Jan 5, 2022
@bishabosha bishabosha added itype:crash itype:bug Spree Suitable for a future Spree area:backend good first issue Perfect for someone who wants to get started contributing labels Jan 5, 2022
@bishabosha bishabosha changed the title Do not crash when method is too large Do not crash in backend when method is too large Jan 5, 2022
@devlaam
Copy link

devlaam commented Mar 9, 2022

I have found a related example (below, minimised from a real word application) that results in an “java.lang.OutOfMemoryError: Java heap space” on a 24GB heap. Only a slight modification results in an "MethodTooLargeException". So probably there is more going on behind the scenes. Previously discussed on the Scala Users Forum.

/**  Compile with:
sbt.version=1.6.2
scalaVersion := "3.1.1"
scalacOptions ++= Seq("-feature","-deprecation","-unchecked","-explain"),
libraryDependencies ++= Seq("com.softwaremill.quicklens" %% "quicklens" % "1.8.3")                                   
*/

package HeapError

import com.softwaremill.quicklens._

/* Presence of AnyVal turns 'MethodTooLargeException' into an 
   'java.lang.OutOfMemoryError: Java heap space' error */
class Moment(val value: Long) extends AnyVal with Ordered[Moment]
{ def compare(that: Moment) = (this.value - that.value).sign.toInt
  def isZero = value == 0 }
  
case class Timers(
  wakeTime:            Moment, 
  constructionTime:    Moment, 
  initializationTime:  Moment, 
  activationTime:      Moment, 
  clock:               Moment, 
  lastSignal:          Moment, 
  lastTransmission:    Moment, 
  lastHeartbeat:       Moment, 
  lastClockSet:        Moment, 
  lastFail:            Moment) 

case class Counters(
  missedAcks:   Int, 
  upSinceHB:    Int, 
  downSinceHB:  Int, 
  failSinceHB:  Int, 
  upSum:        Int, 
  downSum:      Int, 
  failSum:      Int) 

case class SystemState(
  mcuTemperature: Double, 
  batteryCharge:  Double, 
  bufferSpace:    Double, 
  health:         Double)
 
case class Monitor(timers: Timers, counters: Counters, systemState: SystemState) 
{ def setClockInit(time: Moment): Monitor = this
    .modifyAll(_.timers.clock, 
               _.timers.initializationTime,  
               _.timers.lastClockSet, 
               _.timers.lastSignal, 
               _.timers.lastTransmission)
    .setToIf(!time.isZero)(time) }

@anatoliykmetyuk
Copy link
Contributor

Minimized original to:

inline def g(inline x: Int): List[Int] =
  inline if x == 0 then Nil else
    val headTypeClass = 1
    val tailTypeClasses = g(x - 1)
    headTypeClass.asInstanceOf[Int] :: g(x - 1)

def f = g(11)
Exception in thread "main" scala.tools.asm.MethodTooLargeException: Method too large: Test$package$.f ()Lscala/collection/immutable/List; while compiling /Users/kmetiuk/Projects/scala3/playground/learning/Test.scala                                                                                     
scala.tools.asm.MethodTooLargeException: Method too large: Test$package$.f ()Lscala/collection/immutable/List;                                                                                                                                                                                              
        at scala.tools.asm.MethodWriter.computeMethodInfoSize(MethodWriter.java:2087)
        at scala.tools.asm.ClassWriter.toByteArray(ClassWriter.java:489)
        at dotty.tools.backend.jvm.GenBCodePipeline$Worker2.getByteArray$1(GenBCode.scala:484)
        at dotty.tools.backend.jvm.GenBCodePipeline$Worker2.addToQ3(GenBCode.scala:490)
        at dotty.tools.backend.jvm.GenBCodePipeline$Worker2.run(GenBCode.scala:467)
        at dotty.tools.backend.jvm.GenBCodePipeline.buildAndSendToDisk(GenBCode.scala:568)
        at dotty.tools.backend.jvm.GenBCodePipeline.run(GenBCode.scala:531)
        at dotty.tools.backend.jvm.GenBCode.run(GenBCode.scala:67)
        at dotty.tools.dotc.core.Phases$Phase.runOn$$anonfun$1(Phases.scala:311)
        at scala.collection.immutable.List.map(List.scala:246)
        at dotty.tools.dotc.core.Phases$Phase.runOn(Phases.scala:312)
        at dotty.tools.backend.jvm.GenBCode.runOn(GenBCode.scala:75)
        at dotty.tools.dotc.Run.runPhases$1$$anonfun$1(Run.scala:225)
        at scala.runtime.function.JProcedure1.apply(JProcedure1.java:15)
        at scala.runtime.function.JProcedure1.apply(JProcedure1.java:10)
        at scala.collection.ArrayOps$.foreach$extension(ArrayOps.scala:1328)
        at dotty.tools.dotc.Run.runPhases$1(Run.scala:236)
        at dotty.tools.dotc.Run.compileUnits$$anonfun$1(Run.scala:244)
        at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
        at dotty.tools.dotc.util.Stats$.maybeMonitored(Stats.scala:68)
        at dotty.tools.dotc.Run.compileUnits(Run.scala:253)
        at dotty.tools.dotc.Run.compileSources(Run.scala:186)
        at dotty.tools.dotc.Run.compile(Run.scala:170)
        at dotty.tools.dotc.Driver.doCompile(Driver.scala:35)
        at dotty.tools.dotc.Driver.process(Driver.scala:195)
        at dotty.tools.dotc.Driver.process(Driver.scala:163)
        at dotty.tools.dotc.Driver.process(Driver.scala:175)
        at dotty.tools.dotc.Driver.main(Driver.scala:205)
        at dotty.tools.dotc.Main.main(Main.scala)

This issue was picked for the Issue Spree 14 of 12th April. @dos65 and @anatoliykmetyuk will be working on it. If you have any insight into the issue or guidance on how to fix it, please leave it here.

@lrytz
Copy link
Member

lrytz commented Apr 8, 2022

What fix do you have in mind? A better error message when inlining leads to a method that's too large?

Estimating the size of a method from a compiler AST seems difficult, but maybe a warning once a certain threshold is passed would work? Even with an ASM MethodNode in the backend the size in bytes is not known. The Scala 2 inliner (which works on the ASM bytecode representation) uses a rough upper bound to estimate the size.

https://github.com/scala/scala/blob/6471120ef701bd10acfbc711088fd1fce2028b34/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala#L310-L312

@anatoliykmetyuk
Copy link
Contributor

The crash in question seems to be caused by an exponential space complexity of code generated by the Inliner. It is caused by the user input to the Inliner: each call to g expands to two recursive g calls. The fact that enormous trees are generated is thus known as early as the Inliner. So I see two possible ways to solve the crash.

  • Inliner-time Error Reporting: somewhere after Inliner, look at the size of the tree generated. If it is big (for some default value of "big" overridable by a -Y-level flag), report an error.
  • Backend-time Error Reporting: try-catch at the backend somewhere around the crash.

I am in favor of Inliner-time error reporting for the following reasons:

  • Catch the error as early as possible, don't waste CPU cycles and memory passing an enormous tree down the phases
  • A possibility of a pretty error message that points to the offending method. In contrast, I didn't find the capability to report errors with position from backend, but I didn't look too hard.

See also #9203 and #9237 – not merged since I didn't have the time to work on the requested adjustments; is not too related to this issue anyway. But maybe we want some kind of generic space complexity tracking solution shared between all compile-time computations. The problem in its general form: Scala's compiler is capable of running compile-time computations that, under certain user input, will generate trees that takes too much space.

WDYT @nicolasstucki ?

@dos65
Copy link
Contributor

dos65 commented Apr 14, 2022

During the spree, we discussed this issue with @nicolasstucki and decided that there aren't any good approach to properly detect that tree is too large at the Inliner phase.
We decided that the only we can do is to provide a bit nicer error messages for these cases. Done in #14943

@anatoliykmetyuk should we close this one?

@anatoliykmetyuk
Copy link
Contributor

Yes, this can be closed once #14943 is merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:backend good first issue Perfect for someone who wants to get started contributing itype:bug itype:crash Spree Suitable for a future Spree
Projects
None yet
Development

No branches or pull requests

7 participants