Introduce native-image-(job|pr)-reports feature.

Co-authored-by: Ondřej Douda <ondrej.douda@oracle.com>
This commit is contained in:
Fabio Niephaus
2022-11-03 17:13:30 +01:00
parent c2c2a82199
commit cb02f04137
12 changed files with 13133 additions and 8 deletions

View File

@@ -27,6 +27,7 @@
import * as core from '@actions/core'
import * as constants from './constants'
import {save} from './cache'
import {generateReports} from './features/reports'
/**
* Check given input and run a save process for the specified package manager
@@ -56,6 +57,7 @@ async function ignoreError(promise: Promise<void>): Promise<unknown> {
}
export async function run(): Promise<void> {
generateReports()
await ignoreError(saveCache())
}

View File

@@ -28,6 +28,9 @@ export const MANDREL_NAMESPACE = 'mandrel-'
export const GDS_BASE = 'https://gds.oracle.com/api/20220101'
export const GDS_GRAALVM_PRODUCT_ID = 'D53FAE8052773FFAE0530F15000AA6C6'
export const ENV_GITHUB_EVENT_NAME = 'GITHUB_EVENT_NAME'
export const EVENT_NAME_PULL_REQUEST = 'pull_request'
export type LatestReleaseResponse =
otypes.Endpoints['GET /repos/{owner}/{repo}/releases/latest']['response']

262
src/features/reports.ts Normal file
View File

@@ -0,0 +1,262 @@
import * as c from '../constants'
import * as core from '@actions/core'
import * as fs from 'fs'
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<void> {
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 function generateReports(): void {
if (areJobReportsEnabled() || arePRReportsEnabled()) {
if (!fs.existsSync(BUILD_OUTPUT_JSON_PATH)) {
core.warning(
'Unable to find build output data for creating 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 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 = `\n| [Debug info](${DOCS_BASE}#glossary-debug-info) | ${bytesToHuman(
debugInfoBytes
)} | ${toPercent(debugInfoBytes, details.total_bytes)} | |`
}
const resources = data.resource_usage
return `## Generated \`${info.name}\`
using [Native Image](https://www.graalvm.org/native-image/) from ${
info.graalvm_version
}.
#### Analysis Results
| Category | Classes | in % | Fields | in % | Methods | in % |
|:---------|--------:|-----:|-------:|-----:|--------:|-----:|
| [Reachable](${DOCS_BASE}#glossary-reachability) | ${
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](${DOCS_BASE}#glossary-reflection-registrations) | ${
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](${DOCS_BASE}#glossary-jni-access-registrations) | ${
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](${DOCS_BASE}#reachable-classes-fields-and-methods) | ${
analysis.classes.total
} | 100.000% | ${analysis.fields.total} | 100.000% | ${
analysis.methods.total
} | 100.000% |
#### Image Details
| Category | Size | in % | Details |
|:---------|-----:|-----:|:--------|
| [Code area](${DOCS_BASE}#glossary-code-area)| ${bytesToHuman(
details.code_area.bytes
)} | ${toPercent(details.code_area.bytes, details.total_bytes)} | ${
details.code_area.compilation_units
} compilation units |
| [Image heap](${DOCS_BASE}#glossary-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 |${debugInfoLine}
| [Other data](${DOCS_BASE}#glossary-other-data) | ${bytesToHuman(
otherBytes
)} | ${toPercent(otherBytes, details.total_bytes)} | |
| Total | **${bytesToHuman(details.total_bytes)}** | 100.000% | |
#### Resource Usage
| Category | |
|:---------|:------|
| [GCs](${DOCS_BASE}#glossary-garbage-collections) | ${resources.garbage_collection.total_secs.toFixed(
2
)}s in ${resources.garbage_collection.count} GCs |
| [Peak RSS](${DOCS_BASE}#glossary-peak-rss) | ${bytesToHuman(
resources.memory.peak_rss_bytes
)} |
| [CPU load](${DOCS_BASE}#glossary-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](https://github.com/marketplace/actions/github-action-for-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`
}
}

View File

@@ -9,6 +9,7 @@ import {setUpGUComponents} from './gu'
import {setUpMandrel} from './mandrel'
import {checkForUpdates, setUpNativeImageMusl} from './features'
import {setUpWindowsEnvironment} from './msvc'
import {setUpNativeImageBuildReports} from './features/reports'
async function run(): Promise<void> {
try {
@@ -79,6 +80,7 @@ async function run(): Promise<void> {
if (cache && isCacheAvailable()) {
await restore(cache)
}
setUpNativeImageBuildReports(graalvmVersion)
} catch (error) {
if (error instanceof Error) core.setFailed(error.message)
}

View File

@@ -1,5 +1,6 @@
import * as c from './constants'
import * as core from '@actions/core'
import * as github from '@actions/github'
import * as httpClient from '@actions/http-client'
import * as tc from '@actions/tool-cache'
import {ExecOptions, exec as e} from '@actions/exec'
@@ -35,7 +36,7 @@ export async function exec(
export async function getLatestRelease(
repo: string
): Promise<c.LatestReleaseResponse['data']> {
const githubToken = core.getInput('github-token')
const githubToken = getGitHubToken()
const options = githubToken.length > 0 ? {auth: githubToken} : {}
const octokit = new GitHub(options)
return (
@@ -112,3 +113,23 @@ export function toSemVer(version: string): string {
const patch = parts.length > 2 ? parts.slice(2).join('-') : '0'
return `${major}.${minor}.${patch}`
}
export function isPREvent(): boolean {
return process.env[c.ENV_GITHUB_EVENT_NAME] === c.EVENT_NAME_PULL_REQUEST
}
function getGitHubToken(): string {
return core.getInput(c.INPUT_GITHUB_TOKEN)
}
export async function createPRComment(content: string): Promise<void> {
if (!isPREvent()) {
throw new Error('Not a PR event.')
}
const context = github.context
await github.getOctokit(getGitHubToken()).rest.issues.createComment({
...context.repo,
issue_number: context.payload.pull_request?.number as number,
body: content
})
}