diff --git a/build.sbt b/build.sbt index 462377942d80..55165423413c 100644 --- a/build.sbt +++ b/build.sbt @@ -305,6 +305,8 @@ lazy val Benchmark = config("bench") extend sbt.Test lazy val rebuildNativeImage = taskKey[Unit]("Force to rebuild native image") lazy val buildNativeImage = taskKey[Unit]("Ensure that the Native Image is built.") +lazy val checkNativeImageSize = + taskKey[Unit]("Ensures the generated Native Image has reasonable size") // ============================================================================ // === Global Project ========================================================= @@ -4064,7 +4066,16 @@ lazy val `engine-runner` = project "enso", targetDir = engineDistributionRoot.value / "bin" ) - }.value + }.value, + checkNativeImageSize := Def + .taskDyn { + NativeImage.checkNativeImageSize( + name = "enso", + targetDir = engineDistributionRoot.value / "bin" + ) + } + .dependsOn(buildNativeImage) + .value ) .dependsOn(`version-output`) .dependsOn(pkg) @@ -5671,6 +5682,7 @@ buildEngineDistributionNoIndex := Def.taskIf { createEnginePackageNoIndex.value if (shouldBuildNativeImage.value) { (`engine-runner` / buildNativeImage).value + (`engine-runner` / checkNativeImageSize).value } }.value diff --git a/project/GraalVM.scala b/project/GraalVM.scala index 7874f19dcbc0..30a28482815a 100644 --- a/project/GraalVM.scala +++ b/project/GraalVM.scala @@ -72,6 +72,40 @@ object GraalVM { def release = native && !test && !debug && !fast && !disableLanguageServer } + case class NativeImageSize( + minMb: Int, + maxMb: Int + ) + + object NativeImageSize { + def expectedSizeForCurrentPlatform(): NativeImageSize = { + if (EnsoLauncher.release) { + if (Platform.isWindows) { + windowsX64Release + } else if (Platform.isLinux) { + linuxX64Release + } else if (Platform.isMacOS && Platform.isAmd64) { + macX64Release + } else if (Platform.isMacOS && Platform.isArm64) { + macARM64Release + } else { + throw new IllegalArgumentException("Unexpected platform") + } + } else { + testNISize + } + } + + // Expected production NI sizes deduced from sizes on latest + // nightly builds: https://github.com/enso-org/enso/pull/12996#discussion_r2073742680 + // With maximal size relaxed by 30 MB. + private val windowsX64Release = NativeImageSize(200, 420) + private val linuxX64Release = NativeImageSize(200, 463) + private val macX64Release = NativeImageSize(200, 408) + private val macARM64Release = NativeImageSize(200, 423) + private val testNISize = NativeImageSize(100, 550) + } + /** Has the user requested to use Espresso for Java interop? */ private def isEspressoMode(): Boolean = "espresso".equals(System.getenv("ENSO_JAVA")) diff --git a/project/NativeImage.scala b/project/NativeImage.scala index 2e05561669b0..f04761783ca3 100644 --- a/project/NativeImage.scala +++ b/project/NativeImage.scala @@ -320,6 +320,37 @@ object NativeImage { } } + def checkNativeImageSize( + name: String, + targetDir: File + ): Def.Initialize[Task[Unit]] = Def.task { + val generatedBin = artifactFile(targetDir, name) + val logger = streams.value.log + if (!generatedBin.exists) { + logger.error(s"Generated binary $generatedBin does not exist.") + logger.error( + "Ensure that the dependency on `buildNativeImage` is properly set." + ) + } + val bytes = generatedBin.attributes.size() + val mb = bytes / (1024 * 1024) + val expectedSize = GraalVM.NativeImageSize.expectedSizeForCurrentPlatform() + val isInBounds = + expectedSize.minMb <= mb && mb <= expectedSize.maxMb + if (!isInBounds) { + logger.error( + s"Generated binary $generatedBin has unexpected size: $mb MB. " + + s"Expected size is between ${expectedSize.minMb} and ${expectedSize.maxMb} MB." + ) + throw new RuntimeException(s"Generated binary $generatedBin is too large") + } else { + logger.info( + s"Generated binary $generatedBin size ($mb MB) " + + s"is within the expected size: [${expectedSize.minMb}, ${expectedSize.maxMb}] MB." + ) + } + } + /** [[File]] representing the artifact called `name` built with the Native * Image. */