From 48bf3cff2b1488f558ff834b129d2d4f4287343d Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Wed, 30 Apr 2025 12:58:26 +0200 Subject: [PATCH 1/6] Add Enso test for NI size --- test/Base_Tests/src/System/Runner_Spec.enso | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/Base_Tests/src/System/Runner_Spec.enso b/test/Base_Tests/src/System/Runner_Spec.enso index cab50260c0bf..a41714b2720e 100644 --- a/test/Base_Tests/src/System/Runner_Spec.enso +++ b/test/Base_Tests/src/System/Runner_Spec.enso @@ -36,6 +36,17 @@ add_specs suite_builder = suite_builder.group "Engine runner" group_builder-> Test.with_clue clue <| retcode . should_equal 0 + ni_size_pending = if runs_in_aot then Nothing else "Only runs in AOT" + group_builder.specify "Size of the Native Image executable should be kept reasonably small" pending=ni_size_pending <| + min_size_mb = 150 + max_size_mb = 450 + exec = File.new enso_bin + exec.exists . should_be_true + Test.with_clue ("Size of the NI executable should be between [" + min_size_mb.to_text + "MB, " + max_size_mb.to_text + "MB]") <| + actual_size_mb = exec.size / (1024 * 1024) + (min_size_mb < actual_size_mb) . should_be_true + (actual_size_mb < max_size_mb) . should_be_true + runs_in_aot = prop = J_System.getProperty "org.graalvm.nativeimage.imagecode" From cb94968384092fda94e4d56cab36a05080b444e6 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Wed, 30 Apr 2025 13:00:46 +0200 Subject: [PATCH 2/6] better test clue --- test/Base_Tests/src/System/Runner_Spec.enso | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Base_Tests/src/System/Runner_Spec.enso b/test/Base_Tests/src/System/Runner_Spec.enso index a41714b2720e..bfe8b2075e0c 100644 --- a/test/Base_Tests/src/System/Runner_Spec.enso +++ b/test/Base_Tests/src/System/Runner_Spec.enso @@ -42,8 +42,8 @@ add_specs suite_builder = suite_builder.group "Engine runner" group_builder-> max_size_mb = 450 exec = File.new enso_bin exec.exists . should_be_true - Test.with_clue ("Size of the NI executable should be between [" + min_size_mb.to_text + "MB, " + max_size_mb.to_text + "MB]") <| - actual_size_mb = exec.size / (1024 * 1024) + actual_size_mb = exec.size / (1024 * 1024) + Test.with_clue ("Size of the NI executable should be between [" + min_size_mb.to_text + "MB, " + max_size_mb.to_text + "MB], but was: " + actual_size_mb.to_text + "MB.") <| (min_size_mb < actual_size_mb) . should_be_true (actual_size_mb < max_size_mb) . should_be_true From 8d77f79b930319993f108664477845648ceee198 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 5 May 2025 17:03:51 +0200 Subject: [PATCH 3/6] Revert "better test clue" This reverts commit cb94968384092fda94e4d56cab36a05080b444e6. --- test/Base_Tests/src/System/Runner_Spec.enso | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Base_Tests/src/System/Runner_Spec.enso b/test/Base_Tests/src/System/Runner_Spec.enso index bfe8b2075e0c..a41714b2720e 100644 --- a/test/Base_Tests/src/System/Runner_Spec.enso +++ b/test/Base_Tests/src/System/Runner_Spec.enso @@ -42,8 +42,8 @@ add_specs suite_builder = suite_builder.group "Engine runner" group_builder-> max_size_mb = 450 exec = File.new enso_bin exec.exists . should_be_true - actual_size_mb = exec.size / (1024 * 1024) - Test.with_clue ("Size of the NI executable should be between [" + min_size_mb.to_text + "MB, " + max_size_mb.to_text + "MB], but was: " + actual_size_mb.to_text + "MB.") <| + Test.with_clue ("Size of the NI executable should be between [" + min_size_mb.to_text + "MB, " + max_size_mb.to_text + "MB]") <| + actual_size_mb = exec.size / (1024 * 1024) (min_size_mb < actual_size_mb) . should_be_true (actual_size_mb < max_size_mb) . should_be_true From 18d496f06a41f17c544b649b45860221bbb7b0d3 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 5 May 2025 17:03:54 +0200 Subject: [PATCH 4/6] Revert "Add Enso test for NI size" This reverts commit 48bf3cff2b1488f558ff834b129d2d4f4287343d. --- test/Base_Tests/src/System/Runner_Spec.enso | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/Base_Tests/src/System/Runner_Spec.enso b/test/Base_Tests/src/System/Runner_Spec.enso index a41714b2720e..cab50260c0bf 100644 --- a/test/Base_Tests/src/System/Runner_Spec.enso +++ b/test/Base_Tests/src/System/Runner_Spec.enso @@ -36,17 +36,6 @@ add_specs suite_builder = suite_builder.group "Engine runner" group_builder-> Test.with_clue clue <| retcode . should_equal 0 - ni_size_pending = if runs_in_aot then Nothing else "Only runs in AOT" - group_builder.specify "Size of the Native Image executable should be kept reasonably small" pending=ni_size_pending <| - min_size_mb = 150 - max_size_mb = 450 - exec = File.new enso_bin - exec.exists . should_be_true - Test.with_clue ("Size of the NI executable should be between [" + min_size_mb.to_text + "MB, " + max_size_mb.to_text + "MB]") <| - actual_size_mb = exec.size / (1024 * 1024) - (min_size_mb < actual_size_mb) . should_be_true - (actual_size_mb < max_size_mb) . should_be_true - runs_in_aot = prop = J_System.getProperty "org.graalvm.nativeimage.imagecode" From c324144199aee7bafbd4c24a9e10722b4fbc7eca Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 5 May 2025 18:03:42 +0200 Subject: [PATCH 5/6] NI size is checked in sbt during buildEngineDistribution --- build.sbt | 14 +++++++++++++- project/NativeImage.scala | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) 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/NativeImage.scala b/project/NativeImage.scala index 2e05561669b0..a023ccde0d20 100644 --- a/project/NativeImage.scala +++ b/project/NativeImage.scala @@ -320,6 +320,43 @@ 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 productionNIBounds = (150, 450) + val testNIBounds = (110, 550) + val mb = bytes / (1024 * 1024) + val bounds = if (GraalVM.EnsoLauncher.release) { + productionNIBounds + } else { + testNIBounds + } + val isInBounds = + bounds._1 <= mb && mb <= bounds._2 + if (!isInBounds) { + logger.error( + s"Generated binary $generatedBin has unexpected size: $mb MB. " + + s"Expected size is between ${bounds._1} and ${bounds._2} 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: [${bounds._1}, ${bounds._2}] MB." + ) + } + } + /** [[File]] representing the artifact called `name` built with the Native * Image. */ From ee07a3c8eaabe9a78646ef594c3a122be5f08a5a Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Tue, 6 May 2025 12:46:04 +0200 Subject: [PATCH 6/6] NativeImageSize constants are more visible, and platform specific --- project/GraalVM.scala | 34 ++++++++++++++++++++++++++++++++++ project/NativeImage.scala | 18 ++++++------------ 2 files changed, 40 insertions(+), 12 deletions(-) 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 a023ccde0d20..f04761783ca3 100644 --- a/project/NativeImage.scala +++ b/project/NativeImage.scala @@ -332,27 +332,21 @@ object NativeImage { "Ensure that the dependency on `buildNativeImage` is properly set." ) } - val bytes = generatedBin.attributes.size() - val productionNIBounds = (150, 450) - val testNIBounds = (110, 550) - val mb = bytes / (1024 * 1024) - val bounds = if (GraalVM.EnsoLauncher.release) { - productionNIBounds - } else { - testNIBounds - } + val bytes = generatedBin.attributes.size() + val mb = bytes / (1024 * 1024) + val expectedSize = GraalVM.NativeImageSize.expectedSizeForCurrentPlatform() val isInBounds = - bounds._1 <= mb && mb <= bounds._2 + expectedSize.minMb <= mb && mb <= expectedSize.maxMb if (!isInBounds) { logger.error( s"Generated binary $generatedBin has unexpected size: $mb MB. " + - s"Expected size is between ${bounds._1} and ${bounds._2} 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: [${bounds._1}, ${bounds._2}] MB." + s"is within the expected size: [${expectedSize.minMb}, ${expectedSize.maxMb}] MB." ) } }