diff --git a/docs/reference-manual/native-image/BuildOutput.md b/docs/reference-manual/native-image/BuildOutput.md index 4c96f85d339b..0ce44161076d 100644 --- a/docs/reference-manual/native-image/BuildOutput.md +++ b/docs/reference-manual/native-image/BuildOutput.md @@ -23,58 +23,60 @@ Below is the example output when building a native executable of the `HelloWorld ================================================================================ GraalVM Native Image: Generating 'helloworld' (executable)... ================================================================================ -[1/8] Initializing... (2.8s @ 0.15GB) - Java version: 25+13, vendor version: GraalVM CE 25-dev+13.1 +[1/8] Initializing... (2.0s @ 0.19GB) + Java version: 26+9, vendor version: GraalVM CE 26-dev+9.1 Graal compiler: optimization level: 2, target machine: x86-64-v3 - C compiler: gcc (linux, x86_64, 12.2.0) + C compiler: gcc (linux, x86_64, 15.2.1) Garbage collector: Serial GC (max heap size: 80% of RAM) -------------------------------------------------------------------------------- - Build resources: - - 13.24GB of memory (42.7% of system memory, using available memory) - - 16 thread(s) (100.0% of 16 available processor(s), determined at start) -[2/8] Performing analysis... [****] (4.5s @ 0.54GB) - 3,158 types, 3,625 fields, and 14,804 methods found reachable - 1,012 types, 36 fields, and 377 methods registered for reflection - 57 types, 57 fields, and 52 methods registered for JNI access +Build resources: + - 28.45GB of memory (42.5% of system memory, using available memory) + - 20 thread(s) (100.0% of 20 available processor(s), determined at start) +[2/8] Performing analysis... [******] (3.4s @ 0.40GB) + 3,297 types, 3,733 fields, and 15,247 methods found reachable + 1,066 types, 36 fields, and 415 methods registered for reflection + 58 types, 59 fields, and 52 methods registered for JNI access 0 downcalls and 0 upcalls registered for foreign access 4 native libraries: dl, pthread, rt, z -[3/8] Building universe... (0.8s @ 0.99GB) -[4/8] Parsing methods... [*] (0.6s @ 0.75GB) -[5/8] Inlining methods... [***] (0.3s @ 0.32GB) -[6/8] Compiling methods... [**] (3.7s @ 0.60GB) -[7/8] Laying out methods... [*] (0.8s @ 0.83GB) -[8/8] Creating image... [**] (3.1s @ 0.58GB) - 5.32MB (24.22%) for code area: 8,702 compilation units - 7.03MB (32.02%) for image heap: 93,301 objects and 5 resources - 8.96MB (40.83%) for debug info generated in 1.0s - 659.13kB ( 2.93%) for other data - 21.96MB in total image size, 21.04MB in total file size +[3/8] Building universe... (1.0s @ 0.60GB) +[4/8] Parsing methods... [*] (0.4s @ 0.62GB) +[5/8] Inlining methods... [****] (0.2s @ 0.59GB) +[6/8] Compiling methods... [**] (3.7s @ 0.66GB) +[7/8] Laying out methods... [*] (0.7s @ 0.60GB) +[8/8] Creating image... [**] (2.3s @ 0.65GB) + 5.24MB (21.86%) for code area: 8,788 compilation units + 7.67MB (32.01%) for image heap: 90,323 objects and 55 resources + 9.43MB (39.34%) for debug info generated in 0.3s + 11.05MB (46.13%) for other data + 23.96MB in total image size, 13.31MB in total file size -------------------------------------------------------------------------------- Top 10 origins of code area: Top 10 object types in image heap: - 4.03MB java.base 1.14MB byte[] for code metadata - 927.05kB svm.jar (Native Image) 927.31kB java.lang.String - 111.71kB java.logging 839.68kB byte[] for general heap data - 63.38kB org.graalvm.nativeimage.base 736.91kB java.lang.Class - 47.59kB jdk.proxy1 713.13kB byte[] for java.lang.String - 35.85kB jdk.proxy3 272.85kB c.o.s.c.h.DynamicHubCompanion - 27.06kB jdk.internal.vm.ci 250.83kB java.util.HashMap$Node - 23.44kB org.graalvm.sdk 196.52kB java.lang.Object[] - 11.42kB jdk.proxy2 182.77kB java.lang.String[] - 8.07kB jdk.graal.compiler 154.26kB byte[] for embedded resources - 1.39kB for 2 more packages 1.38MB for 884 more object types + 791.32kB java.base/java.util 1.41MB byte[] for code metadata + 363.66kB java.base/java.lang 1.21MB byte[] for string data + 323.39kB java.base/java.text 838.53kB java.base/java.lang.String + 241.87kB java.base/java.util.stream 633.02kB o.g.n.~e/c.o.s.c.h.Dyna~anion + 229.23kB java.base/java.util.regex 431.58kB heap alignment + 214.23kB java.base/java.util.concurrent 428.26kB java.base/java.lang.Class + 166.60kB o.g.n.~e/c.o.svm.core.code 323.23kB java.base/j.util.HashMap$Node + 153.78kB java.base/java.time.format 284.47kB byte[] for general heap data + 152.90kB java.base/java.math 232.06kB java.base/java.lang.Object[] + 142.02kB o.g.n.~e/c.o.s.c.genscavenge 183.10kB java.base/j.u.HashMap$Node[] + 2.32MB for 146 more packages 1.70MB for 966 more object types -------------------------------------------------------------------------------- Recommendations: + FUTR: Use '--future-defaults=all' to prepare for future releases. HEAP: Set max heap for improved and more predictable memory usage. CPU: Enable more CPU features with '-march=native' for improved performance. -------------------------------------------------------------------------------- - 0.8s (4.6% of total time) in 35 GCs | Peak RSS: 1.93GB | CPU load: 9.61 + 0.9s (6.1% of total time) in 54 GCs | Peak RSS: 1.82GB | CPU load: 13.25 -------------------------------------------------------------------------------- Build artifacts: + /home/janedoe/helloworld/gdb-debughelpers.py (debug_info) /home/janedoe/helloworld/helloworld (executable) /home/janedoe/helloworld/helloworld.debug (debug_info) /home/janedoe/helloworld/sources (debug_info) ================================================================================ -Finished generating 'helloworld' in 17.0s. +Finished generating 'helloworld' in 14.2s. ``` ## Build Stages diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java index 3a5669274f89..4eacc9da849b 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java @@ -803,6 +803,9 @@ public static boolean hasColorsEnabled(OptionValues values) { @Option(help = "Print GC warnings as part of build output", type = OptionType.User)// public static final HostedOptionKey BuildOutputGCWarnings = new HostedOptionKey<>(true); + @Option(help = "Write code breakdown information into CSV file", type = OptionType.User)// + public static final HostedOptionKey BuildOutputCodeBreakdownFile = new HostedOptionKey<>(false); + @BundleMember(role = BundleMember.Role.Output)// @Option(help = "Print build output statistics as JSON to the specified file. " + "The output conforms to the JSON schema located at: " + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/CodeBreakdownProvider.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/CodeBreakdownProvider.java index 06f978b845b9..5871090816d0 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/CodeBreakdownProvider.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/CodeBreakdownProvider.java @@ -24,40 +24,22 @@ */ package com.oracle.svm.hosted; -import java.security.CodeSource; import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; import org.graalvm.nativeimage.ImageSingletons; import com.oracle.svm.hosted.code.CompileQueue.CompileTask; -import com.oracle.svm.hosted.meta.HostedMethod; class CodeBreakdownProvider { - private Map codeBreakdown; + private Map, Long> codeBreakdown; CodeBreakdownProvider(Collection compilationTasks) { - Map nameToSizeMap = new HashMap<>(); - for (CompileTask task : compilationTasks) { - String key = null; - Class javaClass = task.method.getDeclaringClass().getJavaClass(); - Module module = javaClass.getModule(); - if (module.isNamed()) { - key = module.getName(); - if ("org.graalvm.nativeimage.builder".equals(key)) { - key = "svm.jar (Native Image)"; - } - } else { - key = findJARFile(javaClass); - if (key == null) { - key = findPackageOrClassName(task.method); - } - } - nameToSizeMap.merge(key, (long) task.result.getTargetCodeSize(), Long::sum); - } - codeBreakdown = Collections.unmodifiableMap(nameToSizeMap); + codeBreakdown = Map.copyOf(compilationTasks.stream().collect( + Collectors.groupingBy( + compileTask -> compileTask.method.getDeclaringClass().getJavaClass(), + Collectors.summingLong(compileTask -> compileTask.result.getTargetCodeSize())))); } /** @@ -66,31 +48,10 @@ class CodeBreakdownProvider { * * @return the code breakdown */ - public static Map getAndClear() { + public static Map, Long> getAndClear() { CodeBreakdownProvider singleton = ImageSingletons.lookup(CodeBreakdownProvider.class); - Map map = singleton.codeBreakdown; + var map = singleton.codeBreakdown; singleton.codeBreakdown = null; return map; } - - private static String findJARFile(Class javaClass) { - CodeSource codeSource = javaClass.getProtectionDomain().getCodeSource(); - if (codeSource != null && codeSource.getLocation() != null) { - String path = codeSource.getLocation().getPath(); - if (path.endsWith(".jar")) { - // Use String API to determine basename of path to handle both / and \. - return path.substring(Math.max(path.lastIndexOf('/') + 1, path.lastIndexOf('\\') + 1)); - } - } - return null; - } - - private static String findPackageOrClassName(HostedMethod method) { - String qualifier = method.format("%H"); - int lastDotIndex = qualifier.lastIndexOf('.'); - if (lastDotIndex > 0) { - qualifier = qualifier.substring(0, lastDotIndex); - } - return qualifier; - } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapBreakdownProvider.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapBreakdownProvider.java index e96e397d3be6..ba975b6d465d 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapBreakdownProvider.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapBreakdownProvider.java @@ -114,7 +114,7 @@ protected void calculate(BeforeImageWriteAccessImpl access, boolean resourcesAre } long objectSize = o.getSize(); totalObjectSize += objectSize; - classToDataMap.computeIfAbsent(o.getClazz(), c -> new HeapBreakdownEntry(c)).add(objectSize); + classToDataMap.computeIfAbsent(o.getClazz(), HeapBreakdownEntry::of).add(objectSize); if (reportStringBytesConstant && o.getObject() instanceof String string) { byte[] bytes = getInternalByteArray(string); /* Ensure every byte[] is counted only once. */ @@ -140,24 +140,24 @@ protected void calculate(BeforeImageWriteAccessImpl access, boolean resourcesAre long heapAlignmentSize = getTotalHeapSize() - totalObjectSize; assert heapAlignmentSize >= 0 : "Incorrect heap alignment detected: " + heapAlignmentSize; if (heapAlignmentSize > 0) { - HeapBreakdownEntry heapAlignmentEntry = new HeapBreakdownEntry("", "heap alignment", "#glossary-heap-alignment"); + HeapBreakdownEntry heapAlignmentEntry = HeapBreakdownEntry.of("", "heap alignment", "#glossary-heap-alignment"); heapAlignmentEntry.add(heapAlignmentSize); entries.add(heapAlignmentEntry); } /* Extract byte[] for Strings. */ if (stringByteArrayTotalSize > 0) { - addEntry(entries, byteArrayEntry, new HeapBreakdownEntry(BYTE_ARRAY_PREFIX + "java.lang.String"), stringByteArrayTotalSize, stringByteArrayTotalCount); + addEntry(entries, byteArrayEntry, HeapBreakdownEntry.of(BYTE_ARRAY_PREFIX + "string data"), stringByteArrayTotalSize, stringByteArrayTotalCount); } /* Extract byte[] for code info. */ List codeInfoByteArrayLengths = CodeInfoTable.getCurrentLayerImageCodeCache().getTotalByteArrayLengths(); long codeInfoSize = codeInfoByteArrayLengths.stream().map(l -> objectLayout.getArraySize(JavaKind.Byte, l, true)).reduce(0L, Long::sum); - addEntry(entries, byteArrayEntry, new HeapBreakdownEntry(BYTE_ARRAY_PREFIX, "code metadata", "#glossary-code-metadata"), codeInfoSize, codeInfoByteArrayLengths.size()); + addEntry(entries, byteArrayEntry, HeapBreakdownEntry.of(BYTE_ARRAY_PREFIX, "code metadata", "#glossary-code-metadata"), codeInfoSize, codeInfoByteArrayLengths.size()); /* Extract byte[] for metadata. */ int metadataByteLength = ImageSingletons.lookup(RuntimeMetadataDecoder.class).getMetadataByteLength(); if (metadataByteLength > 0) { long metadataSize = objectLayout.getArraySize(JavaKind.Byte, metadataByteLength, true); - addEntry(entries, byteArrayEntry, new HeapBreakdownEntry(BYTE_ARRAY_PREFIX, "reflection metadata", "#glossary-reflection-metadata"), metadataSize, 1); + addEntry(entries, byteArrayEntry, HeapBreakdownEntry.of(BYTE_ARRAY_PREFIX, "reflection metadata", "#glossary-reflection-metadata"), metadataSize, 1); } ProgressReporter reporter = ProgressReporter.singleton(); long resourcesByteArraySize = 0; @@ -177,7 +177,7 @@ protected void calculate(BeforeImageWriteAccessImpl access, boolean resourcesAre } } if (resourcesByteArraySize > 0) { - addEntry(entries, byteArrayEntry, new HeapBreakdownEntry(BYTE_ARRAY_PREFIX, "embedded resources", "#glossary-embedded-resources"), resourcesByteArraySize, resourcesByteArrayCount); + addEntry(entries, byteArrayEntry, HeapBreakdownEntry.of(BYTE_ARRAY_PREFIX, "embedded resources", "#glossary-embedded-resources"), resourcesByteArraySize, resourcesByteArrayCount); } } reporter.recordJsonMetric(ImageDetailKey.RESOURCE_SIZE_BYTES, resourcesByteArraySize); @@ -185,11 +185,11 @@ protected void calculate(BeforeImageWriteAccessImpl access, boolean resourcesAre if (graphEncodingByteLength >= 0) { long graphEncodingSize = objectLayout.getArraySize(JavaKind.Byte, graphEncodingByteLength, true); reporter.recordJsonMetric(ImageDetailKey.GRAPH_ENCODING_SIZE, graphEncodingSize); - addEntry(entries, byteArrayEntry, new HeapBreakdownEntry(BYTE_ARRAY_PREFIX, "graph encodings", "#glossary-graph-encodings"), graphEncodingSize, 1); + addEntry(entries, byteArrayEntry, HeapBreakdownEntry.of(BYTE_ARRAY_PREFIX, "graph encodings", "#glossary-graph-encodings"), graphEncodingSize, 1); } /* Add remaining byte[]. */ assert byteArrayEntry.byteSize >= 0 && byteArrayEntry.count >= 0; - addEntry(entries, byteArrayEntry, new HeapBreakdownEntry(BYTE_ARRAY_PREFIX, "general heap data", "#glossary-general-heap-data"), byteArrayEntry.byteSize, byteArrayEntry.count); + addEntry(entries, byteArrayEntry, HeapBreakdownEntry.of(BYTE_ARRAY_PREFIX, "general heap data", "#glossary-general-heap-data"), byteArrayEntry.byteSize, byteArrayEntry.count); assert byteArrayEntry.byteSize == 0 && byteArrayEntry.count == 0; setBreakdownEntries(entries); } @@ -209,26 +209,23 @@ private static byte[] getInternalByteArray(String string) { } } - public static class HeapBreakdownEntry { - final HeapBreakdownLabel label; + public abstract static class HeapBreakdownEntry { long byteSize; int count; - public HeapBreakdownEntry(HostedClass hostedClass) { - this(hostedClass.toJavaName(true)); + public static HeapBreakdownEntry of(HostedClass hostedClass) { + return new HeapBreakdownEntryForClass(hostedClass.getJavaClass()); } - public HeapBreakdownEntry(String name) { - label = new SimpleHeapObjectKindName(name); + public static HeapBreakdownEntry of(String name) { + return new HeapBreakdownEntryFixed(new SimpleHeapObjectKindName(name)); } - HeapBreakdownEntry(String prefix, String name, String htmlAnchor) { - label = new LinkyHeapObjectKindName(prefix, name, htmlAnchor); + public static HeapBreakdownEntry of(String prefix, String name, String htmlAnchor) { + return new HeapBreakdownEntryFixed(new LinkyHeapObjectKindName(prefix, name, htmlAnchor)); } - public HeapBreakdownLabel getLabel() { - return label; - } + public abstract HeapBreakdownLabel getLabel(int maxLength); public long getByteSize() { return byteSize; @@ -253,6 +250,41 @@ void remove(long subByteSize, int subCount) { } } + static class HeapBreakdownEntryFixed extends HeapBreakdownEntry { + + private final HeapBreakdownLabel label; + + HeapBreakdownEntryFixed(HeapBreakdownLabel label) { + this.label = label; + } + + @Override + public HeapBreakdownLabel getLabel(int unused) { + return label; + } + } + + static class HeapBreakdownEntryForClass extends HeapBreakdownEntry { + + private final Class clazz; + + HeapBreakdownEntryForClass(Class clazz) { + this.clazz = clazz; + } + + @Override + public HeapBreakdownLabel getLabel(int maxLength) { + if (maxLength >= 0) { + String moduleNamePrefix = ProgressReporterUtils.moduleNamePrefix(clazz.getModule()); + int maxLengthClassName = maxLength - moduleNamePrefix.length(); + String truncatedClassName = ProgressReporterUtils.truncateFQN(clazz.getTypeName(), maxLengthClassName); + return new SimpleHeapObjectKindName(moduleNamePrefix + truncatedClassName); + } else { + return new SimpleHeapObjectKindName(clazz.getTypeName()); + } + } + } + public interface HeapBreakdownLabel { String renderToString(LinkStrategy linkStrategy); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporter.java index 26d24c625a4b..84eebd8186a2 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporter.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporter.java @@ -30,6 +30,7 @@ import java.io.PrintWriter; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; @@ -52,8 +53,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import com.oracle.svm.core.configure.ConditionalRuntimeValue; -import com.oracle.svm.core.jdk.resources.ResourceStorageEntryBase; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.impl.ImageSingletonsSupport; @@ -73,9 +72,11 @@ import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.VM; +import com.oracle.svm.core.configure.ConditionalRuntimeValue; import com.oracle.svm.core.heap.Heap; import com.oracle.svm.core.hub.ClassForNameSupport; import com.oracle.svm.core.jdk.Resources; +import com.oracle.svm.core.jdk.resources.ResourceStorageEntryBase; import com.oracle.svm.core.option.AccumulatingLocatableMultiOptionValue; import com.oracle.svm.core.option.HostedOptionKey; import com.oracle.svm.core.option.HostedOptionValues; @@ -114,7 +115,7 @@ @SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = NoLayeredCallbacks.class, layeredInstallationKind = Independent.class) public class ProgressReporter { - private static final int CHARACTERS_PER_LINE; + static final int CHARACTERS_PER_LINE; private static final String HEADLINE_SEPARATOR; private static final String LINE_SEPARATOR; private static final int MAX_NUM_BREAKDOWN = 10; @@ -176,8 +177,8 @@ private enum BuildStage { static { CHARACTERS_PER_LINE = SubstrateUtil.isNonInteractiveTerminal() ? ProgressReporterCHelper.MAX_CHARACTERS_PER_LINE : ProgressReporterCHelper.getTerminalWindowColumnsClamped(); - HEADLINE_SEPARATOR = Utils.stringFilledWith(CHARACTERS_PER_LINE, "="); - LINE_SEPARATOR = Utils.stringFilledWith(CHARACTERS_PER_LINE, "-"); + HEADLINE_SEPARATOR = ProgressReporterUtils.stringFilledWith(CHARACTERS_PER_LINE, "="); + LINE_SEPARATOR = ProgressReporterUtils.stringFilledWith(CHARACTERS_PER_LINE, "-"); } public static ProgressReporter singleton() { @@ -442,9 +443,9 @@ private void printResourceInfo() { l().printLineSeparator(); l().yellowBold().doclink("Build resources", "#glossary-build-resources").a(":").reset().println(); - l().a(" - %s of memory (%.1f%% of system memory, %s)", ByteFormattingUtil.bytesToHuman(maxMemory), Utils.toPercentage(maxMemory, totalMemorySize), memoryUsageReason).println(); + l().a(" - %s of memory (%.1f%% of system memory, %s)", ByteFormattingUtil.bytesToHuman(maxMemory), ProgressReporterUtils.toPercentage(maxMemory, totalMemorySize), memoryUsageReason).println(); l().a(" - %s thread(s) (%.1f%% of %s available processor(s), %s)", - maxNumberOfThreads, Utils.toPercentage(maxNumberOfThreads, availableProcessors), availableProcessors, maxNumberOfThreadsSuffix).println(); + maxNumberOfThreads, ProgressReporterUtils.toPercentage(maxNumberOfThreads, availableProcessors), availableProcessors, maxNumberOfThreadsSuffix).println(); } public ReporterClosable printAnalysis(AnalysisUniverse universe, Collection libraries) { @@ -532,7 +533,7 @@ private void printAnalysisStatistics(AnalysisUniverse universe, Collection= 0) { recordJsonMetric(ImageDetailKey.RUNTIME_COMPILED_METHODS_COUNT, numRuntimeCompiledMethods); l().a("%,9d ", numRuntimeCompiledMethods).doclink("runtime compiled methods", "#glossary-runtime-methods") - .a(" (%.1f%% of all reachable methods)", Utils.toPercentage(numRuntimeCompiledMethods, reachableMethods), reachableMethods).println(); + .a(" (%.1f%% of all reachable methods)", ProgressReporterUtils.toPercentage(numRuntimeCompiledMethods, reachableMethods), reachableMethods).println(); } } @@ -590,7 +591,7 @@ public void printCreationEnd(int imageFileSize, int heapObjectCount, long imageH stagePrinter.end(imageTimer.getTotalTime() + writeTimer.getTotalTime() + archiveTimer.getTotalTime()); creationStageEndCompleted = true; String format = "%9s (%5.2f%%) for "; - l().a(format, ByteFormattingUtil.bytesToHuman(codeAreaSize), Utils.toPercentage(codeAreaSize, imageFileSize)) + l().a(format, ByteFormattingUtil.bytesToHuman(codeAreaSize), ProgressReporterUtils.toPercentage(codeAreaSize, imageFileSize)) .doclink("code area", "#glossary-code-area").a(":%,10d compilation units", numCompilations).println(); int numResources = 0; for (ConditionalRuntimeValue entry : Resources.currentLayer().resources().getValues()) { @@ -599,16 +600,16 @@ public void printCreationEnd(int imageFileSize, int heapObjectCount, long imageH } } recordJsonMetric(ImageDetailKey.IMAGE_HEAP_RESOURCE_COUNT, numResources); - l().a(format, ByteFormattingUtil.bytesToHuman(imageHeapSize), Utils.toPercentage(imageHeapSize, imageFileSize)) + l().a(format, ByteFormattingUtil.bytesToHuman(imageHeapSize), ProgressReporterUtils.toPercentage(imageHeapSize, imageFileSize)) .doclink("image heap", "#glossary-image-heap").a(":%,9d objects and %,d resource%s", heapObjectCount, numResources, numResources == 1 ? "" : "s").println(); long otherBytes = imageFileSize - codeAreaSize - imageHeapSize; if (debugInfoSize > 0) { recordJsonMetric(ImageDetailKey.DEBUG_INFO_SIZE, debugInfoSize); // Optional metric - DirectPrinter l = l().a(format, ByteFormattingUtil.bytesToHuman(debugInfoSize), Utils.toPercentage(debugInfoSize, imageFileSize)) + DirectPrinter l = l().a(format, ByteFormattingUtil.bytesToHuman(debugInfoSize), ProgressReporterUtils.toPercentage(debugInfoSize, imageFileSize)) .doclink("debug info", "#glossary-debug-info"); if (debugInfoTimer != null) { - l.a(" generated in %.1fs", Utils.millisToSeconds(debugInfoTimer.getTotalTime())); + l.a(" generated in %.1fs", ProgressReporterUtils.millisToSeconds(debugInfoTimer.getTotalTime())); } l.println(); if (!(ImageSingletons.contains(NativeImageDebugInfoStripFeature.class) && ImageSingletons.lookup(NativeImageDebugInfoStripFeature.class).hasStrippedSuccessfully())) { @@ -621,7 +622,7 @@ public void printCreationEnd(int imageFileSize, int heapObjectCount, long imageH recordJsonMetric(ImageDetailKey.TOTAL_SIZE, imageFileSize); recordJsonMetric(ImageDetailKey.CODE_AREA_SIZE, codeAreaSize); recordJsonMetric(ImageDetailKey.NUM_COMP_UNITS, numCompilations); - l().a(format, ByteFormattingUtil.bytesToHuman(otherBytes), Utils.toPercentage(otherBytes, imageFileSize)) + l().a(format, ByteFormattingUtil.bytesToHuman(otherBytes), ProgressReporterUtils.toPercentage(otherBytes, imageFileSize)) .doclink("other data", "#glossary-other-data").println(); l().a("%9s in total image size", ByteFormattingUtil.bytesToHuman(imageFileSize)); if (imageDiskFileSize >= 0) { @@ -640,18 +641,36 @@ public void ensureCreationStageEndCompleted() { } private void printBreakdowns() { + Map codeBreakdown = CodeBreakdownProvider.getAndClear().entrySet().stream() + .collect(Collectors.groupingBy( + entry -> ProgressReporterUtils.BreakDownClassifier.of(entry.getKey()), + Collectors.summingLong(Entry::getValue))); + + List> sortedBreakdownData = codeBreakdown.entrySet().stream() + .sorted(Entry.comparingByValue(Comparator.reverseOrder())).toList(); + + if (SubstrateOptions.BuildOutputCodeBreakdownFile.getValue()) { + String valueSeparator = ","; + List lines = new ArrayList<>(); + lines.add(String.join(valueSeparator, "Size", "Module", "Package", "Location")); + sortedBreakdownData.forEach(entry -> { + lines.add(entry.getValue() + valueSeparator + String.join(valueSeparator, entry.getKey().elements())); + }); + Path breakdownFile = SubstrateOptions.getImagePath().resolve(SubstrateOptions.Name.getValue() + ".code_breakdown.csv"); + BuildArtifacts.singleton().add(BuildArtifacts.ArtifactType.BUILD_INFO, breakdownFile); + try { + Files.write(breakdownFile, lines); + } catch (IOException e) { + throw VMError.shouldNotReachHere("Failed generating " + breakdownFile, e); + } + } + if (!SubstrateOptions.BuildOutputBreakdowns.getValue()) { return; } - l().printLineSeparator(); - Map codeBreakdown = CodeBreakdownProvider.getAndClear(); - Iterator> packagesBySize = codeBreakdown.entrySet().stream() - .sorted(Entry.comparingByValue(Comparator.reverseOrder())).iterator(); - - HeapBreakdownProvider heapBreakdown = HeapBreakdownProvider.singleton(); - Iterator typesBySizeInHeap = heapBreakdown.getSortedBreakdownEntries().iterator(); final TwoColumnPrinter p = new TwoColumnPrinter(); + l().printLineSeparator(); p.l().yellowBold().a(String.format("Top %d ", MAX_NUM_BREAKDOWN)).doclink("origins", "#glossary-code-area-origins").a(" of code area:") .jumpToMiddle() .a(String.format("Top %d object types in image heap:", MAX_NUM_BREAKDOWN)).reset().flushln(); @@ -660,26 +679,29 @@ private void printBreakdowns() { long printedHeapBytes = 0; long printedCodeItems = 0; long printedHeapItems = 0; + Iterator> packagesBySize = sortedBreakdownData.iterator(); + HeapBreakdownProvider heapBreakdown = HeapBreakdownProvider.singleton(); + Iterator typesBySizeInHeap = heapBreakdown.getSortedBreakdownEntries().iterator(); for (int i = 0; i < MAX_NUM_BREAKDOWN; i++) { String codeSizePart = ""; + /* <- 12% for module name -><- remainder for package FQN -> */ if (packagesBySize.hasNext()) { - Entry e = packagesBySize.next(); - String className = Utils.truncateClassOrPackageName(e.getKey()); - codeSizePart = String.format("%9s %s", ByteFormattingUtil.bytesToHuman(e.getValue()), className); - printedCodeBytes += e.getValue(); + Entry entry = packagesBySize.next(); + String sizeStr = getBreakdownSizeString(entry.getValue()); + String entryStr = entry.getKey().renderToString(p.middle() - sizeStr.length()); + codeSizePart = sizeStr + entryStr; + printedCodeBytes += entry.getValue(); printedCodeItems++; } String heapSizePart = ""; + /* <- 12% for module name -><- remainder for class FQN -> */ if (typesBySizeInHeap.hasNext()) { HeapBreakdownProvider.HeapBreakdownEntry e = typesBySizeInHeap.next(); - String className = e.label.renderToString(linkStrategy); - // Do not truncate special breakdown items, they can contain links. - if (e.label instanceof HeapBreakdownProvider.SimpleHeapObjectKindName) { - className = Utils.truncateClassOrPackageName(className); - } long byteSize = e.byteSize; - heapSizePart = String.format("%9s %s", ByteFormattingUtil.bytesToHuman(byteSize), className); + String sizeStr = getBreakdownSizeString(byteSize); + String labelStr = e.getLabel(CHARACTERS_PER_LINE - p.middle() - sizeStr.length()).renderToString(linkStrategy); + heapSizePart = sizeStr + labelStr; printedHeapBytes += byteSize; printedHeapItems++; } @@ -699,6 +721,10 @@ private void printBreakdowns() { .flushln(); } + private static String getBreakdownSizeString(long sizeInBytes) { + return String.format("%9s ", ByteFormattingUtil.bytesToHuman(sizeInBytes)); + } + private void printRecommendations() { if (!SubstrateOptions.BuildOutputRecommendations.getValue()) { return; @@ -711,7 +737,7 @@ private void printRecommendations() { l().printLineSeparator(); l().yellowBold().a("Recommendations:").reset().println(); for (UserRecommendation r : topApplicableRecommendations) { - String alignment = Utils.stringFilledWith(Math.max(1, 5 - r.id().length()), " "); + String alignment = ProgressReporterUtils.stringFilledWith(Math.max(1, 5 - r.id().length()), " "); l().a(" ").doclink(r.id(), "#recommendation-" + r.id().toLowerCase(Locale.ROOT)).a(":").a(alignment).a(r.description()).println(); } } @@ -744,7 +770,7 @@ public void printEpilog(Optional optionalImageName, Optional 0) { - cpuLoad = Utils.nanosToSeconds(processCPUTime) / totalProcessTimeSeconds; + cpuLoad = ProgressReporterUtils.nanosToSeconds(processCPUTime) / totalProcessTimeSeconds; p.a(" | ").doclink("CPU load", "#glossary-cpu-load").a(": ").a("%.2f", cpuLoad); } recordJsonMetric(ResourceUsageKey.CPU_LOAD, cpuLoad); @@ -880,7 +906,7 @@ private void checkForExcessiveGarbageCollection() { if (gcTimeDeltaMillis > EXCESSIVE_GC_MIN_THRESHOLD_MILLIS && ratio > EXCESSIVE_GC_RATIO) { l().redBold().a("GC warning").reset() .a(": %.1fs spent in %d GCs during the last stage, taking up %.2f%% of the time.", - Utils.millisToSeconds(gcTimeDeltaMillis), currentGCStats.totalCount - lastGCStats.totalCount, ratio * 100) + ProgressReporterUtils.millisToSeconds(gcTimeDeltaMillis), currentGCStats.totalCount - lastGCStats.totalCount, ratio * 100) .println(); l().a(" Please ensure more than %s of memory is available for Native Image", ByteFormattingUtil.bytesToHuman(ProgressReporterCHelper.getPeakRSS())).println(); l().a(" to reduce GC overhead and improve image build time.").println(); @@ -920,64 +946,6 @@ private static OperatingSystemMXBean getOperatingSystemMXBean() { return (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); } - private static final class Utils { - private static final double MILLIS_TO_SECONDS = 1000d; - private static final double NANOS_TO_SECONDS = 1000d * 1000d * 1000d; - - private static double millisToSeconds(double millis) { - return millis / MILLIS_TO_SECONDS; - } - - private static double nanosToSeconds(double nanos) { - return nanos / NANOS_TO_SECONDS; - } - - private static String getUsedMemory() { - return ByteFormattingUtil.bytesToHumanGB(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()); - } - - private static String stringFilledWith(int size, String fill) { - return new String(new char[size]).replace("\0", fill); - } - - private static double toPercentage(long part, long total) { - return part / (double) total * 100; - } - - private static String truncateClassOrPackageName(String classOrPackageName) { - int classNameLength = classOrPackageName.length(); - int maxLength = CHARACTERS_PER_LINE / 2 - 10; - if (classNameLength <= maxLength) { - return classOrPackageName; - } - StringBuilder sb = new StringBuilder(); - int currentDot = -1; - while (true) { - int nextDot = classOrPackageName.indexOf('.', currentDot + 1); - if (nextDot < 0) { // Not more dots, handle the rest and return. - String rest = classOrPackageName.substring(currentDot + 1); - int sbLength = sb.length(); - int restLength = rest.length(); - if (sbLength + restLength <= maxLength) { - sb.append(rest); - } else { - int remainingSpaceDivBy2 = (maxLength - sbLength) / 2; - sb.append(rest, 0, remainingSpaceDivBy2 - 1).append("~").append(rest, restLength - remainingSpaceDivBy2, restLength); - } - break; - } - sb.append(classOrPackageName.charAt(currentDot + 1)).append('.'); - if (sb.length() + (classNameLength - nextDot) <= maxLength) { - // Rest fits maxLength, append and return. - sb.append(classOrPackageName.substring(nextDot + 1)); - break; - } - currentDot = nextDot; - } - return sb.toString(); - } - } - private record GCStats(long totalCount, long totalTimeMillis) { private static GCStats getCurrent() { long totalCount = 0; @@ -1234,7 +1202,7 @@ private void appendStageStart() { } final String progressBarStartPadding() { - return Utils.stringFilledWith(PROGRESS_BAR_START - getCurrentTextLength(), " "); + return ProgressReporterUtils.stringFilledWith(PROGRESS_BAR_START - getCurrentTextLength(), " "); } void reportProgress() { @@ -1254,11 +1222,11 @@ void end(double totalTime) { a("]").reset(); } - String suffix = String.format("(%.1fs @ %s)", Utils.millisToSeconds(totalTime), Utils.getUsedMemory()); + String suffix = String.format("(%.1fs @ %s)", ProgressReporterUtils.millisToSeconds(totalTime), ProgressReporterUtils.getUsedMemory()); int textLength = getCurrentTextLength(); // TODO: `assert textLength > 0;` should be used here but tests do not start stages // properly (GR-35721) - String padding = Utils.stringFilledWith(Math.max(0, CHARACTERS_PER_LINE - textLength - suffix.length()), " "); + String padding = ProgressReporterUtils.stringFilledWith(Math.max(0, CHARACTERS_PER_LINE - textLength - suffix.length()), " "); a(padding).dim().a(suffix).reset().flushln(); activeBuildStage = null; @@ -1375,12 +1343,16 @@ public TwoColumnPrinter a(String value) { } TwoColumnPrinter jumpToMiddle() { - int remaining = (CHARACTERS_PER_LINE / 2) - getCurrentTextLength(); + int remaining = middle() - getCurrentTextLength(); assert remaining >= 0 : "Column text too wide"; - a(Utils.stringFilledWith(remaining, " ")); - assert getCurrentTextLength() == CHARACTERS_PER_LINE / 2; + a(ProgressReporterUtils.stringFilledWith(remaining, " ")); + assert getCurrentTextLength() == middle(); return this; } + + private int middle() { + return CHARACTERS_PER_LINE / 2; + } } public final class CenteredTextPrinter extends LinePrinter { @@ -1391,7 +1363,7 @@ CenteredTextPrinter getThis() { @Override public void flushln() { - String padding = Utils.stringFilledWith((Math.max(0, CHARACTERS_PER_LINE - getCurrentTextLength())) / 2, " "); + String padding = ProgressReporterUtils.stringFilledWith((Math.max(0, CHARACTERS_PER_LINE - getCurrentTextLength())) / 2, " "); print(padding); super.flushln(); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporterUtils.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporterUtils.java new file mode 100644 index 000000000000..603070275125 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporterUtils.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.hosted; + +import java.net.URL; +import java.security.CodeSource; + +final class ProgressReporterUtils { + + private static final double MILLIS_TO_SECONDS = 1000d; + private static final double NANOS_TO_SECONDS = 1000d * 1000d * 1000d; + public static final String TRUNCATION_PLACEHOLDER = "~"; + + static double millisToSeconds(double millis) { + return millis / MILLIS_TO_SECONDS; + } + + static double nanosToSeconds(double nanos) { + return nanos / NANOS_TO_SECONDS; + } + + static String getUsedMemory() { + return ByteFormattingUtil.bytesToHumanGB(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()); + } + + static String stringFilledWith(int size, String fill) { + return fill.repeat(size); + } + + static double toPercentage(long part, long total) { + return part / (double) total * 100; + } + + static String truncateFQN(String fqn, double maxLineRatio) { + return truncateFQN(fqn, maxLength(maxLineRatio)); + } + + static int maxLength(double maxLineRatio) { + return (int) Math.floor(ProgressReporter.CHARACTERS_PER_LINE * maxLineRatio); + } + + static String truncateFQN(String fqn, int maxLength) { + int classNameLength = fqn.length(); + if (classNameLength <= maxLength) { + return fqn; + } + StringBuilder sb = new StringBuilder(); + int currentDot = -1; + while (true) { + int nextDot = fqn.indexOf('.', currentDot + 1); + if (nextDot < 0) { // Not more dots, handle the rest and return. + String rest = fqn.substring(currentDot + 1); + int sbLength = sb.length(); + int restLength = rest.length(); + if (sbLength + restLength <= maxLength) { + sb.append(rest); + } else { + int remainingSpaceDivBy2 = (maxLength - sbLength) / 2; + sb.append(rest, 0, remainingSpaceDivBy2 - 1).append(TRUNCATION_PLACEHOLDER).append(rest, restLength - remainingSpaceDivBy2, restLength); + } + break; + } + sb.append(fqn.charAt(currentDot + 1)).append('.'); + if (sb.length() + (classNameLength - nextDot) <= maxLength) { + // Rest fits maxLength, append and return. + sb.append(fqn.substring(nextDot + 1)); + break; + } + currentDot = nextDot; + } + return sb.toString(); + } + + static String moduleNamePrefix(Module javaModule) { + if (!javaModule.isNamed()) { + return ""; + } + String moduleName = javaModule.getName(); + return truncateFQN(mapToNativeImageRuntime(moduleName), 0.12) + "/"; + } + + private static String mapToNativeImageRuntime(String moduleName) { + String modulePrefix = "org.graalvm.nativeimage."; + if (moduleName.equals(modulePrefix + "builder")) { + return modulePrefix + "runtime"; + } + return moduleName; + } + + record BreakDownClassifier(Package javaPackage, Module javaModule, String location) { + static BreakDownClassifier of(Class clazz) { + return new BreakDownClassifier(clazz.getPackage(), clazz.getModule(), sourcePath(clazz)); + } + + private static String sourcePath(Class clazz) { + CodeSource codeSource = clazz.getProtectionDomain().getCodeSource(); + if (codeSource != null) { + URL sourceLocation = codeSource.getLocation(); + if (sourceLocation != null && !"jrt".equals(sourceLocation.getProtocol())) { + return sourceLocation.getPath(); + } + } + return null; + } + + public String renderToString(int maxLength) { + String packageName = javaPackage == null ? "null" : javaPackage.getName(); + String moduleNamePrefix = moduleNamePrefix(javaModule); + // Give remainder of space to package-part + int maxLengthPackage = maxLength - moduleNamePrefix.length(); + return moduleNamePrefix + truncateFQN(packageName, maxLengthPackage); + } + + public String[] elements() { + String moduleName = javaModule.isNamed() ? javaModule.getName() : ""; + String packageName = javaPackage == null ? "" : javaPackage.getName(); + String locationName = location == null ? "" : location; + return new String[]{mapToNativeImageRuntime(moduleName), packageName, locationName}; + } + } +} diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/WebImageJSHeapBreakdownProvider.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/WebImageJSHeapBreakdownProvider.java index c8e29c7cdc2c..e1b099dbe2d0 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/WebImageJSHeapBreakdownProvider.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/WebImageJSHeapBreakdownProvider.java @@ -56,7 +56,7 @@ protected void calculate(FeatureImpl.BeforeImageWriteAccessImpl access, boolean for (ConstantIdentityMapping.IdentityNode node : identityMapping.identityNodes()) { HostedClass type = (HostedClass) providers.getMetaAccess().lookupJavaType(node.getDefinition().getConstant()); long size = node.getDefinition().getSize(); - objectTypeEntries.computeIfAbsent(type, HeapBreakdownEntry::new).add(size); + objectTypeEntries.computeIfAbsent(type, HeapBreakdownEntry::of).add(size); totalByteSize += size; }