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 { createChart, createPRComment, createRef, createTree, getPrBaseBranchMetrics, 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 INPUT_NI_JOB_METRIC_HISTORY = 'native-image-metric-history' const INPUT_NI_HISTORY_BUILD_COUNT = 'build-counts-for-metric-history' const INPUT_NI_PR_COMPARISON = 'native-image-pr-comparison' 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 vendor_version?: string c_compiler: string | null garbage_collector: string graal_compiler?: { optimization_level: string march: string pgo?: string[] } } analysis_results: { classes: AnalysisResult types?: AnalysisResult fields: AnalysisResult methods: AnalysisResult } image_details: { total_bytes: number code_area: { bytes: number compilation_units: number } image_heap: { bytes: number objects?: { count: 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 } total_secs?: number } } export async function setUpNativeImageBuildReports( isGraalVMforJDK17OrLater: boolean, graalVMVersion: string, ): Promise { const isRequired = areJobReportsEnabled() || arePRReportsEnabled() if (!isRequired) { return } const isSupported = isGraalVMforJDK17OrLater || 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 } core.info(`DEBUGGING: -H:BuildOutputJSONFile=${BUILD_OUTPUT_JSON_PATH.replace(/\\/g, '\\\\')}`) setNativeImageOption( `-H:BuildOutputJSONFile=${BUILD_OUTPUT_JSON_PATH.replace(/\\/g, '\\\\')}` )// Escape backslashes for Windows } export async function generateReports(): Promise { if (areJobReportsEnabled() || arePRReportsEnabled()) { core.info(`DEBUGGING: ${BUILD_OUTPUT_JSON_PATH}`) 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) await core.summary.write() } if (arePRReportsEnabled()) { await createPRComment(report) } const treeSha = await createTree(JSON.stringify(buildOutput)) await createRef(treeSha) if (areMetricHistoriesEnabled()) { /*const pushEvents = await getPushEvents(getBuildCountsForMetricHistory()) // Prepare data const timestamps = [] const shas = [] for (let i=0; i < pushEvents.length; i++) { timestamps.push(pushEvents[i].created_at) shas.push(pushEvents[i].payload.commits[pushEvents[i].payload.commits.length - 1].sha) } const imageData = await getImageData(shas) const commitDates = formatTimestamps(timestamps) const mermaidDiagramm = createHistoryDiagramm(shas, imageData, commitDates) core.summary.addRaw(mermaidDiagramm) await core.summary.write()*/ await createChart() } if (arePRBaseComparisonEnabled()) { const prMetrics: BuildOutput = JSON.parse( await getPrBaseBranchMetrics() ) await createPRComment(createPRComparison(buildOutput, prMetrics)) } } } function areJobReportsEnabled(): boolean { return core.getInput(INPUT_NI_JOB_REPORTS) === 'true' } function arePRReportsEnabled(): boolean { return isPREvent() && core.getInput(INPUT_NI_PR_REPORTS) === 'true' } function areMetricHistoriesEnabled(): boolean { return core.getInput(INPUT_NI_JOB_METRIC_HISTORY) === 'true' } function arePRBaseComparisonEnabled(): boolean { return isPREvent() && core.getInput(INPUT_NI_PR_COMPARISON) === 'true' } function getBuildCountsForMetricHistory(): number { return Number(core.getInput(INPUT_NI_HISTORY_BUILD_COUNT)) } 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 createPRComparison(dataRecent: BuildOutput, dataBase: BuildOutput): string { const detailsRecent = dataRecent.image_details const detailsBase = dataBase.image_details const debugInfoBytesRecent = detailsRecent.debug_info ? detailsRecent.debug_info.bytes : 0 const otherBytesRecent = detailsRecent.total_bytes - detailsRecent.code_area.bytes - detailsRecent.image_heap.bytes - debugInfoBytesRecent const debugInfoBytesBase = detailsBase.debug_info ? detailsBase.debug_info.bytes : 0 const otherBytesBase = detailsBase.total_bytes - detailsBase.code_area.bytes - detailsBase.image_heap.bytes - debugInfoBytesBase const baseBranch = process.env.GITHUB_BASE_REF const recentBranch = process.env.GITHUB_HEAD_REF return `## GraalVM Native Image PR comparison #### Image Details \`\`\`mermaid gantt title Native Image Size Details todayMarker off dateFormat X axisFormat % section Code area ${recentBranch} (${bytesToHuman(detailsRecent.code_area.bytes)}): active, 0, ${detailsRecent.code_area.bytes} ${baseBranch} (${bytesToHuman(detailsBase.code_area.bytes)}): 0, ${detailsBase.code_area.bytes} section Image heap ${recentBranch} (${bytesToHuman(detailsRecent.image_heap.bytes)}): active, 0, ${detailsRecent.image_heap.bytes} ${baseBranch} (${bytesToHuman(detailsBase.image_heap.bytes)}): 0, ${detailsBase.image_heap.bytes} section Other data ${recentBranch} (${bytesToHuman(otherBytesRecent)}): active, 0, ${otherBytesRecent} ${baseBranch} (${bytesToHuman(otherBytesBase)}): 0, ${otherBytesBase} section Total ${recentBranch} (${bytesToHuman(detailsRecent.total_bytes)}) : active, 0, ${detailsRecent.total_bytes} ${baseBranch} (${bytesToHuman(detailsBase.total_bytes)}) : 0, ${detailsBase.total_bytes} \`\`\` Report generated by setup-graalvm.` } function createHistoryDiagramm(shas: String[], metricDataList: any[], commitDates: any[]): string { let mermaidDiagramm = `## GraalVM Native Image PR comparison #### Image Details \`\`\`mermaid gantt title Native Image Size Details todayMarker off dateFormat X axisFormat % ` for (let i=0; iReport generated by setup-graalvm.` return mermaidDiagramm } function createReport(data: BuildOutput): string { const context = github.context const info = data.general_info const analysis = data.analysis_results const analysisTypes = analysis.types ? analysis.types : analysis.classes const details = data.image_details let objectCount = '' if (details.image_heap.objects) { objectCount = `${details.image_heap.objects.count.toLocaleString()} objects, ` } 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)} ` } let versionLine if (info.vendor_version) { versionLine = ` Java version ${info.java_version} Vendor version ${info.vendor_version} ` } else { versionLine = ` GraalVM version ${info.graalvm_version} Java version ${info.java_version} ` } let graalLine if (info.graal_compiler) { let pgoSuffix = '' const isOracleGraalVM = info.vendor_version && info.vendor_version.includes('Oracle GraalVM') if (isOracleGraalVM) { const pgo = info.graal_compiler.pgo const pgoText = pgo ? pgo.join('+') : 'off' pgoSuffix = `, PGO: ${pgoText}` } graalLine = ` Graal compiler optimization level: ${info.graal_compiler.optimization_level}, target machine: ${info.graal_compiler.march}${pgoSuffix} ` } const resources = data.resource_usage let totalTime = '' let gcTotalTimeRatio = '' if (resources.total_secs) { totalTime = ` in ${secondsToHuman(resources.total_secs)}` gcTotalTimeRatio = ` (${toPercent( resources.garbage_collection.total_secs, resources.total_secs )} of total time)` } return `## GraalVM Native Image Build Report \`${info.name}\` generated${totalTime} as part of the '${ context.job }' job in run #${context.runNumber}. #### Environment ${versionLine}${graalLine}
C compiler ${info.c_compiler}
Garbage collector ${info.garbage_collector}
#### Analysis Results
Category Types in % Fields in % Methods in %
Reachable ${analysisTypes.reachable.toLocaleString()} ${toPercent( analysisTypes.reachable, analysisTypes.total )} ${analysis.fields.reachable.toLocaleString()} ${toPercent( analysis.fields.reachable, analysis.fields.total )} ${analysis.methods.reachable.toLocaleString()} ${toPercent( analysis.methods.reachable, analysis.methods.total )}
Reflection ${analysisTypes.reflection.toLocaleString()} ${toPercent( analysisTypes.reflection, analysisTypes.total )} ${analysis.fields.reflection.toLocaleString()} ${toPercent( analysis.fields.reflection, analysis.fields.total )} ${analysis.methods.reflection.toLocaleString()} ${toPercent( analysis.methods.reflection, analysis.methods.total )}
JNI ${analysisTypes.jni.toLocaleString()} ${toPercent( analysisTypes.jni, analysisTypes.total )} ${analysis.fields.jni.toLocaleString()} ${toPercent( analysis.fields.jni, analysis.fields.total )} ${analysis.methods.jni.toLocaleString()} ${toPercent( analysis.methods.jni, analysis.methods.total )}
Loaded ${analysisTypes.total.toLocaleString()} 100.000% ${analysis.fields.total.toLocaleString()} 100.000% ${analysis.methods.total.toLocaleString()} 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.toLocaleString()} compilation units
Image heap ${bytesToHuman(details.image_heap.bytes)} ${toPercent( details.image_heap.bytes, details.total_bytes )} ${objectCount}${bytesToHuman( details.image_heap.resources.bytes )} for ${details.image_heap.resources.count.toLocaleString()} 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${gcTotalTimeRatio} 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` } } function secondsToHuman(seconds: number): string { if (seconds < 60) { return `${seconds.toFixed(1)}s` } else { return `${Math.trunc(seconds / 60)}m ${Math.trunc(seconds % 60)}s` } }