Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/size-check-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Binary Size Check Comment

# Privileged companion to `Binary Size Check`. Triggered on the completion of
# that build workflow (which runs in an unprivileged context for safety on
# fork PRs). This workflow runs from the default branch with write access to
# the target repo, downloads the size report artifact, and posts the sticky
# PR comment.
#
# Pattern: https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/

on:
workflow_run:
workflows: ["Binary Size Check"]
types: [completed]

permissions:
pull-requests: write
actions: read
contents: read

jobs:
comment:
runs-on: ubuntu-latest
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
steps:
- name: Download size-check report artifact
uses: actions/download-artifact@v6
with:
name: size-check-report
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: report

- name: Post or update sticky PR comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
shell: bash
run: |
set -euo pipefail

PR=$(tr -d '[:space:]' < report/pr-number.txt)
if ! [[ "$PR" =~ ^[0-9]+$ ]]; then
echo "PR number from artifact is not numeric: '$PR'"
exit 1
fi

BODY_FILE=report/body.md
MARKER='<!-- binary-size-check -->'

if ! grep -qF "$MARKER" "$BODY_FILE"; then
echo "Body file is missing the expected marker; refusing to post."
exit 1
fi

EXISTING=$(gh api "repos/${REPO}/issues/${PR}/comments" --paginate \
| jq -r --arg m "$MARKER" '.[] | select(.body | contains($m)) | .id' \
| head -n 1)

if [ -n "$EXISTING" ]; then
echo "Updating existing comment $EXISTING on PR #$PR"
gh api "repos/${REPO}/issues/comments/${EXISTING}" --method PATCH -F "body=@${BODY_FILE}" > /dev/null
else
echo "Creating new comment on PR #$PR"
gh api "repos/${REPO}/issues/${PR}/comments" --method POST -F "body=@${BODY_FILE}" > /dev/null
fi
echo "Done."
88 changes: 88 additions & 0 deletions .github/workflows/size-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Binary Size Check

on:
pull_request:
branches: [master]

concurrency:
group: size-check-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
size-check:
runs-on: windows-latest
timeout-minutes: 90
# Intentionally no write permissions: this workflow builds untrusted PR code
# and uploads results as an artifact. The companion `Binary Size Check Comment`
# workflow (triggered on workflow_run) downloads the artifact and posts the
# sticky comment with full write perms from master.
# See https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/
permissions:
contents: read
steps:
- name: Checkout base ref
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base

- name: Checkout head ref
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
path: head

- name: Configure base (Win32 x64 D3D11, matches CI flags)
shell: cmd
working-directory: base
run: |
cmake -G "Visual Studio 17 2022" ^
-B build\Win32_x64 ^
-A x64 ^
-D BX_CONFIG_DEBUG=ON ^
-D GRAPHICS_API=D3D11 ^
-D BGFX_CONFIG_MAX_FRAME_BUFFERS=256 ^
-D BABYLON_DEBUG_TRACE=ON

- name: Build base Playground
shell: cmd
working-directory: base
run: cmake --build build\Win32_x64 --config RelWithDebInfo --target Playground -- /m

- name: Configure head (same flags)
shell: cmd
working-directory: head
run: |
cmake -G "Visual Studio 17 2022" ^
-B build\Win32_x64 ^
-A x64 ^
-D BX_CONFIG_DEBUG=ON ^
-D GRAPHICS_API=D3D11 ^
-D BGFX_CONFIG_MAX_FRAME_BUFFERS=256 ^
-D BABYLON_DEBUG_TRACE=ON

- name: Build head Playground
shell: cmd
working-directory: head
run: cmake --build build\Win32_x64 --config RelWithDebInfo --target Playground -- /m

- name: Measure sizes and format comment
shell: pwsh
run: |
./head/Scripts/MeasureBinarySize.ps1 `
-BaseBuildDir base/build/Win32_x64 `
-HeadBuildDir head/build/Win32_x64 `
-Config RelWithDebInfo `
-Platform "Win32 x64 D3D11 RelWithDebInfo" `
-BaseRef "${{ github.event.pull_request.base.sha }}" `
-HeadRef "${{ github.event.pull_request.head.sha }}" `
-PrNumber ${{ github.event.pull_request.number }} `
-OutputDir "${{ runner.temp }}/size-check-report"

- name: Upload size-check report artifact
uses: actions/upload-artifact@v6
with:
name: size-check-report
path: ${{ runner.temp }}/size-check-report
if-no-files-found: error
retention-days: 7
225 changes: 225 additions & 0 deletions Scripts/MeasureBinarySize.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<#
.SYNOPSIS
Compares binary sizes between two build trees and emits a markdown report.

.DESCRIPTION
Enumerates every .exe / .dll / .lib under each build directory's matching
configuration subfolder, computes per-artifact size deltas, and writes a
markdown report (Playground.exe delta headline, aggregate, top 10 other
movers) to "<OutputDir>/body.md".

Used by the size-check CI workflow. Also useful locally: build two trees
(a master worktree and your feature branch worktree) with the same cmake
flags and point this at them.

.PARAMETER BaseBuildDir
Path to the "base" build tree (the reference / before).

.PARAMETER HeadBuildDir
Path to the "head" build tree (the change / after).

.PARAMETER Config
Build configuration name (e.g. 'Release', 'RelWithDebInfo'). Artifacts are
selected by their path containing "\<Config>\".

.PARAMETER OutputDir
Directory where body.md (and pr-number.txt, if -PrNumber is given) are
written. Created if missing.

.PARAMETER Platform
Free-form description of the build configuration shown in the report
header. Defaults to "Win32 x64 D3D11 <Config>".

.PARAMETER BaseRef
Optional base git ref / SHA. Short-form shown in the report header.

.PARAMETER HeadRef
Optional head git ref / SHA. Short-form shown in the report header.

.PARAMETER PrNumber
Optional PR number. When provided, written to OutputDir/pr-number.txt so
the privileged comment workflow can locate the originating PR.

.EXAMPLE
./Scripts/MeasureBinarySize.ps1 `
-BaseBuildDir ../master/build/win32 `
-HeadBuildDir ../my-feature/build/win32 `
-Config Release `
-OutputDir ./size-report
#>

[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$BaseBuildDir,

[Parameter(Mandatory)]
[string]$HeadBuildDir,

[Parameter(Mandatory)]
[string]$Config,

[Parameter(Mandatory)]
[string]$OutputDir,

[string]$Platform,

[string]$BaseRef,

[string]$HeadRef,

[int]$PrNumber
)

$ErrorActionPreference = 'Stop'

$marker = '<!-- binary-size-check -->'

if (-not $Platform) {
$Platform = "Win32 x64 D3D11 $Config"
}

function Get-Artifacts {
param([string]$Root, [string]$Cfg)

if (-not (Test-Path $Root)) {
return @()
}
Get-ChildItem $Root -Recurse -File -Include *.exe,*.dll,*.lib -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "\\$Cfg\\" } |
ForEach-Object {
[PSCustomObject]@{
Rel = $_.FullName.Substring($Root.Length + 1)
Size = $_.Length
}
}
}

function Format-Bytes {
param([long]$N)

$sign = if ($N -lt 0) { '-' } else { '+' }
$abs = [math]::Abs($N)
if ($abs -ge 1MB) {
return "$sign$([math]::Round($abs / 1MB, 2)) MB"
}
if ($abs -ge 1KB) {
return "$sign$([math]::Round($abs / 1KB, 1)) KB"
}
return "$sign$abs B"
}

function Format-Count {
param([long]$N)
'{0:N0}' -f $N
}

$baseArts = Get-Artifacts -Root $BaseBuildDir -Cfg $Config
$headArts = Get-Artifacts -Root $HeadBuildDir -Cfg $Config

if ($baseArts.Count -eq 0 -or $headArts.Count -eq 0) {
throw "No artifacts found. base=$($baseArts.Count) head=$($headArts.Count). Build likely failed."
}

$basePg = $baseArts | Where-Object { $_.Rel -match 'Playground\.exe$' } | Select-Object -First 1
$headPg = $headArts | Where-Object { $_.Rel -match 'Playground\.exe$' } | Select-Object -First 1

if (-not $basePg -or -not $headPg) {
throw "Playground.exe missing. base=$($null -ne $basePg) head=$($null -ne $headPg)"
}

$pgDelta = $headPg.Size - $basePg.Size
$pgPct = if ($basePg.Size -gt 0) { [math]::Round(100 * $pgDelta / $basePg.Size, 3) } else { 0 }

# Build name -> {Base, Head} map so we can diff every artifact in either set.
$pairs = @{}
foreach ($a in $baseArts) {
$pairs[$a.Rel] = @{ Base = $a.Size; Head = 0 }
}
foreach ($a in $headArts) {
if (-not $pairs.ContainsKey($a.Rel)) {
$pairs[$a.Rel] = @{ Base = 0; Head = 0 }
}
$pairs[$a.Rel].Head = $a.Size
}
$rows = $pairs.Keys | ForEach-Object {
$p = $pairs[$_]
[PSCustomObject]@{
Rel = $_
Base = $p.Base
Head = $p.Head
Delta = $p.Head - $p.Base
}
}

# Top movers, excluding Playground.exe (already featured). 256 B threshold trims
# COFF timestamp noise on libs whose source didn't change.
$top = $rows |
Where-Object { $_.Rel -notmatch 'Playground\.exe$' -and [math]::Abs($_.Delta) -gt 256 } |
Sort-Object { [math]::Abs($_.Delta) } -Descending |
Select-Object -First 10

$baseTotal = ($baseArts | Measure-Object Size -Sum).Sum
$headTotal = ($headArts | Measure-Object Size -Sum).Sum
$totalDelta = $headTotal - $baseTotal
$totalPct = if ($baseTotal -gt 0) { [math]::Round(100 * $totalDelta / $baseTotal, 3) } else { 0 }

$sb = [System.Text.StringBuilder]::new()
[void]$sb.AppendLine($marker)
[void]$sb.AppendLine('## Binary size impact')
[void]$sb.AppendLine('')

$refLine = "Platform: ``$Platform``"
if ($BaseRef) { $refLine += " &nbsp;|&nbsp; base ``$($BaseRef.Substring(0, [math]::Min(7, $BaseRef.Length)))``" }
if ($HeadRef) { $refLine += " &nbsp;|&nbsp; head ``$($HeadRef.Substring(0, [math]::Min(7, $HeadRef.Length)))``" }
[void]$sb.AppendLine($refLine)
[void]$sb.AppendLine('')

[void]$sb.AppendLine('### Final linked binary')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('| Artifact | Base | Head | Δ bytes | Δ% |')
[void]$sb.AppendLine('|---|---:|---:|---:|---:|')
[void]$sb.AppendLine("| **Playground.exe** | $(Format-Count -N $basePg.Size) | $(Format-Count -N $headPg.Size) | **$(Format-Bytes -N $pgDelta)** | **$pgPct %** |")
[void]$sb.AppendLine("| Aggregate (.exe/.dll/.lib) | $(Format-Count -N $baseTotal) | $(Format-Count -N $headTotal) | $(Format-Bytes -N $totalDelta) | $totalPct % |")
[void]$sb.AppendLine('')

if ($top -and $top.Count -gt 0) {
[void]$sb.AppendLine('### Top movers (other static libs / DLLs, |Δ| > 256 B)')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('| Artifact | Base | Head | Δ bytes |')
[void]$sb.AppendLine('|---|---:|---:|---:|')
foreach ($r in $top) {
$shortRel = $r.Rel -replace '\\', '/'
[void]$sb.AppendLine("| ``$shortRel`` | $(Format-Count -N $r.Base) | $(Format-Count -N $r.Head) | $(Format-Bytes -N $r.Delta) |")
}
[void]$sb.AppendLine('')
} else {
[void]$sb.AppendLine('_No other artifact moved by more than 256 B._')
[void]$sb.AppendLine('')
}

[void]$sb.AppendLine('---')
[void]$sb.AppendLine('<sub>Comparison is informational only — no threshold enforcement. Small deltas on unchanged libs are typically COFF timestamps.</sub>')

$body = $sb.ToString()

New-Item -ItemType Directory -Force $OutputDir | Out-Null
$bodyFile = Join-Path $OutputDir 'body.md'
$body | Set-Content -Path $bodyFile -Encoding utf8

if ($PrNumber -gt 0) {
$prFile = Join-Path $OutputDir 'pr-number.txt'
$PrNumber | Set-Content -Path $prFile -Encoding utf8
}

# Surface in the GitHub Actions run summary when running in CI.
if ($env:GITHUB_STEP_SUMMARY) {
$body | Add-Content -Path $env:GITHUB_STEP_SUMMARY -Encoding utf8
}

Write-Host '=== binary size report ==='
Write-Host $body
Write-Host '=========================='
Write-Host ''
Write-Host "Headline: Playground.exe $(Format-Bytes -N $pgDelta) ($pgPct %)"
Write-Host "Output: $OutputDir"
Loading