import * as c from '../constants' import * as core from '@actions/core' import * as fs from 'fs' import * as github from '@actions/github' import {join} from 'path' import {tmpdir} from 'os' import {createPRComment, isPREvent, toSemVer} from '../utils' import {gte} from 'semver' const BUILD_OUTPUT_JSON_PATH = join(tmpdir(), 'native-image-build-output.json') const BYTES_TO_KiB = 1024 const BYTES_TO_MiB = 1024 * 1024 const BYTES_TO_GiB = 1024 * 1024 * 1024 const DOCS_BASE = 'https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/BuildOutput.md' const INPUT_NI_JOB_REPORTS = 'native-image-job-reports' const INPUT_NI_PR_REPORTS = 'native-image-pr-reports' const NATIVE_IMAGE_CONFIG_FILE = join( tmpdir(), 'native-image-options.properties' ) const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE' interface AnalysisResult { total: number reachable: number reflection: number jni: number } interface BuildOutput { general_info: { name: string graalvm_version: string java_version: string | null c_compiler: string | null garbage_collector: string } analysis_results: { classes: AnalysisResult fields: AnalysisResult methods: AnalysisResult } image_details: { total_bytes: number code_area: { bytes: number compilation_units: number } image_heap: { bytes: number resources: { count: number bytes: number } } debug_info?: { bytes: number } runtime_compiled_methods?: { count: number graph_encoding_bytes: number } } resource_usage: { cpu: { load: number total_cores: number } garbage_collection: { count: number total_secs: number } memory: { system_total: number peak_rss_bytes: number } } } export async function setUpNativeImageBuildReports( graalVMVersion: string ): Promise { const isRequired = areJobReportsEnabled() || arePRReportsEnabled() if (!isRequired) { return } const isSupported = graalVMVersion === c.VERSION_LATEST || graalVMVersion === c.VERSION_DEV || (!graalVMVersion.startsWith(c.MANDREL_NAMESPACE) && gte(toSemVer(graalVMVersion), '22.2.0')) if (!isSupported) { core.warning( `Build reports for PRs and job summaries are only available in GraalVM 22.2.0 or later. This build job uses GraalVM ${graalVMVersion}.` ) return } setNativeImageOption( `-H:BuildOutputJSONFile=${BUILD_OUTPUT_JSON_PATH.replace(/\\/g, '\\\\')}` ) // Escape backslashes for Windows } export async function generateReports(): Promise { if (areJobReportsEnabled() || arePRReportsEnabled()) { if (!fs.existsSync(BUILD_OUTPUT_JSON_PATH)) { core.warning( 'Unable to find build output data to create a report. Are you sure this build job has used GraalVM Native Image?' ) return } const buildOutput: BuildOutput = JSON.parse( fs.readFileSync(BUILD_OUTPUT_JSON_PATH, 'utf8') ) const report = createReport(buildOutput) if (areJobReportsEnabled()) { core.summary.addRaw(report) core.summary.write() } if (arePRReportsEnabled()) { createPRComment(report) } } } function areJobReportsEnabled(): boolean { return core.getInput(INPUT_NI_JOB_REPORTS) === 'true' } function arePRReportsEnabled(): boolean { return isPREvent() && core.getInput(INPUT_NI_PR_REPORTS) === 'true' } function getNativeImageOptionsFile(): string { let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV] if (optionsFile === undefined) { optionsFile = NATIVE_IMAGE_CONFIG_FILE core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile) } return optionsFile } function setNativeImageOption(value: string): void { const optionsFile = getNativeImageOptionsFile() if (fs.existsSync(optionsFile)) { fs.appendFileSync(optionsFile, ` ${value}`) } else { fs.writeFileSync(optionsFile, `NativeImageArgs = ${value}`) } } function createReport(data: BuildOutput): string { const context = github.context const info = data.general_info const analysis = data.analysis_results const details = data.image_details const debugInfoBytes = details.debug_info ? details.debug_info.bytes : 0 const otherBytes = details.total_bytes - details.code_area.bytes - details.image_heap.bytes - debugInfoBytes let debugInfoLine = '' if (details.debug_info) { debugInfoLine = ` Debug info ${bytesToHuman(debugInfoBytes)} ${toPercent(debugInfoBytes, details.total_bytes)} ` } const resources = data.resource_usage return `## GraalVM Native Image Build Report \`${info.name}\` generated as part of the '${ context.job }' job in run #${context.runNumber}. #### Environment
GraalVM version ${info.graalvm_version} C compiler ${info.c_compiler}
Java version ${info.java_version} Garbage collector ${info.garbage_collector}
#### Analysis Results
Category Types in % Fields in % Methods in %
Reachable ${analysis.classes.reachable} ${toPercent( analysis.classes.reachable, analysis.classes.total )} ${analysis.fields.reachable} ${toPercent( analysis.fields.reachable, analysis.fields.total )} ${analysis.methods.reachable} ${toPercent( analysis.methods.reachable, analysis.methods.total )}
Reflection ${analysis.classes.reflection} ${toPercent( analysis.classes.reflection, analysis.classes.total )} ${analysis.fields.reflection} ${toPercent( analysis.fields.reflection, analysis.fields.total )} ${analysis.methods.reflection} ${toPercent( analysis.methods.reflection, analysis.methods.total )}
JNI ${analysis.classes.jni} ${toPercent( analysis.classes.jni, analysis.classes.total )} ${analysis.fields.jni} ${toPercent( analysis.fields.jni, analysis.fields.total )} ${analysis.methods.jni} ${toPercent( analysis.methods.jni, analysis.methods.total )}
Loaded ${analysis.classes.total} 100.000% ${analysis.fields.total} 100.000% ${analysis.methods.total} 100.000%
#### Image Details ${debugInfoLine}
Category Size in % Details
Code area ${bytesToHuman(details.code_area.bytes)} ${toPercent( details.code_area.bytes, details.total_bytes )} ${ details.code_area.compilation_units } compilation units
Image heap ${bytesToHuman(details.image_heap.bytes)} ${toPercent( details.image_heap.bytes, details.total_bytes )} ${bytesToHuman( details.image_heap.resources.bytes )} for ${details.image_heap.resources.count} resources
Other data ${bytesToHuman(otherBytes)} ${toPercent(otherBytes, details.total_bytes)}
Total ${bytesToHuman( details.total_bytes )} 100.000%
#### Resource Usage
Garbage collection ${resources.garbage_collection.total_secs.toFixed( 2 )}s in ${resources.garbage_collection.count} GCs
Peak RSS ${bytesToHuman( resources.memory.peak_rss_bytes )} (${toPercent( resources.memory.peak_rss_bytes, resources.memory.system_total )} of ${bytesToHuman(resources.memory.system_total)} system memory)
CPU load ${resources.cpu.load.toFixed(3)} (${toPercent( resources.cpu.load, resources.cpu.total_cores )} of ${resources.cpu.total_cores} CPU cores)
Report generated by setup-graalvm.` } function toPercent(part: number, total: number): string { return `${((part / total) * 100).toFixed(3)}%` } function bytesToHuman(bytes: number): string { if (bytes < BYTES_TO_KiB) { return `${bytes.toFixed(2)}B` } else if (bytes < BYTES_TO_MiB) { return `${(bytes / BYTES_TO_KiB).toFixed(2)}KB` } else if (bytes < BYTES_TO_GiB) { return `${(bytes / BYTES_TO_MiB).toFixed(2)}MB` } else { return `${(bytes / BYTES_TO_GiB).toFixed(2)}GB` } }