Skip to content
Merged
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
120 changes: 120 additions & 0 deletions .github/workflows/player-perf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Player perf

on:
pull_request:
push:
branches: [main]

concurrency:
group: player-perf-${{ github.ref }}
cancel-in-progress: true

jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
perf: ${{ steps.filter.outputs.perf }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
perf:
- "packages/player/**"
- "packages/core/**"
- "package.json"
- "bun.lock"
- ".github/workflows/player-perf.yml"

perf-shards:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
Comment on lines +14 to +32
name: "Perf: ${{ matrix.shard }}"
needs: changes
if: needs.changes.outputs.perf == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- shard: load
scenarios: load
runs: "5"
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2

- uses: actions/setup-node@v4
with:
node-version: 22

- run: bun install --frozen-lockfile

# Player perf loads packages/player/dist/hyperframes-player.global.js
# and packages/core/dist/hyperframe.runtime.iife.js, so a full build is required.
- run: bun run build

- name: Set up Chrome (headless shell)
id: setup-chrome
uses: browser-actions/setup-chrome@v1
with:
chrome-version: stable

- name: Run player perf — ${{ matrix.shard }} (measure mode)
working-directory: packages/player
env:
PUPPETEER_EXECUTABLE_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
run: |
bun run perf \
--mode=measure \
--scenarios=${{ matrix.scenarios }} \
--runs=${{ matrix.runs }}

- name: Upload perf results
if: always()
uses: actions/upload-artifact@v4
with:
name: player-perf-${{ matrix.shard }}
path: packages/player/tests/perf/results/
if-no-files-found: warn
retention-days: 30

# Summary job — matches the required check name in branch protection.
# Logs an explicit "skipped" / "passed" / "failed" line both to stdout and to
# $GITHUB_STEP_SUMMARY so a false skip is obvious in the Checks UI without
# having to dig into the changes-job logs.
player-perf:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
Comment on lines +33 to +89
runs-on: ubuntu-latest
needs: [changes, perf-shards]
if: always()
steps:
- name: Check results
env:
PERF_FILTER_RESULT: ${{ needs.changes.outputs.perf }}
PERF_SHARDS_RESULT: ${{ needs.perf-shards.result }}
run: |
{
echo "## Player perf gate"
echo ""
echo "- paths-filter \`perf\` matched: \`${PERF_FILTER_RESULT}\`"
echo "- perf-shards result: \`${PERF_SHARDS_RESULT}\`"
echo ""
} >> "$GITHUB_STEP_SUMMARY"

if [ "${PERF_FILTER_RESULT}" != "true" ]; then
echo "::notice title=Player perf::SKIPPED — no changes under packages/player/**, packages/core/**, package.json, bun.lock, or .github/workflows/player-perf.yml. Auto-pass."
echo "**Status:** SKIPPED (no player/core changes — auto-pass)" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi

if [ "${PERF_SHARDS_RESULT}" != "success" ]; then
echo "::error title=Player perf::FAILED — perf-shards result was '${PERF_SHARDS_RESULT}'. See the per-shard logs above."
echo "**Status:** FAILED (perf-shards result: \`${PERF_SHARDS_RESULT}\`)" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi

echo "::notice title=Player perf::PASSED — all perf shards completed successfully."
echo "**Status:** PASSED" >> "$GITHUB_STEP_SUMMARY"

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {}
Comment on lines +90 to +120
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ coverage/
# Producer regression test failures (generated debugging artifacts)
packages/producer/tests/*/failures/

# Player perf test results (generated each run, attached as CI artifact)
packages/player/tests/perf/results/

# Rendered output (not test fixtures — those use git LFS)
output/
renders/
Expand Down
9 changes: 9 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"lint:fix": "oxlint --fix .",
"format": "oxfmt .",
"test": "bun run --filter '*' test",
"player:perf": "bun run --filter @hyperframes/player perf",
"format:check": "oxfmt --check .",
"knip": "knip",
"generate:previews": "tsx scripts/generate-template-previews.ts",
Expand Down
8 changes: 6 additions & 2 deletions packages/player/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@
},
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit",
"test": "vitest run"
"typecheck": "tsc --noEmit && tsc --noEmit -p tests/perf/tsconfig.json",
"test": "vitest run",
"perf": "bun run tests/perf/index.ts"
},
"devDependencies": {
"@types/bun": "^1.1.0",
"gsap": "^3.12.5",
"puppeteer-core": "^24.39.1",
"tsup": "^8.0.0",
"typescript": "^5.0.0",
"vitest": "^3.2.4"
Expand Down
10 changes: 10 additions & 0 deletions packages/player/tests/perf/baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compLoadColdP95Ms": 2000,
"compLoadWarmP95Ms": 1000,
"fpsMin": 55,
"scrubLatencyP95IsolatedMs": 80,
"scrubLatencyP95InlineMs": 33,
"driftMaxMs": 500,
"driftP95Ms": 100,
"allowedRegressionRatio": 0.1
}
115 changes: 115 additions & 0 deletions packages/player/tests/perf/fixtures/gsap-heavy/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>perf fixture: gsap-heavy</title>
<style>
:root {
color-scheme: dark;
}
html,
body {
margin: 0;
padding: 0;
background: #0b0b12;
color: #e6e6f0;
font-family:
system-ui,
-apple-system,
sans-serif;
overflow: hidden;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
}
.tile {
position: absolute;
width: 96px;
height: 96px;
border-radius: 12px;
background: linear-gradient(135deg, #4f46e5, #ec4899);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
transform: translate3d(0, 0, 0);
will-change: transform, opacity;
}
</style>
<script src="/vendor/gsap.min.js"></script>
<script data-hyperframes-runtime="1" src="/vendor/hyperframe.runtime.iife.js"></script>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-width="1920"
data-height="1080"
data-duration="10"
data-fps="60"
></div>
<script>
(function () {
var TILE_COUNT = 60;
var DURATION_SEC = 10;
var COLS = 12;
var ROWS = 5;
var TILE = 96;
var GAP_X = 1920 / COLS;
var GAP_Y = 1080 / ROWS;

var root = document.getElementById("root");
var tiles = [];
for (var i = 0; i < TILE_COUNT; i++) {
var col = i % COLS;
var row = Math.floor(i / COLS);
var el = document.createElement("div");
el.className = "tile";
el.style.left = col * GAP_X + (GAP_X - TILE) / 2 + "px";
el.style.top = row * GAP_Y + (GAP_Y - TILE) / 2 + "px";
el.setAttribute("data-tile-index", String(i));
root.appendChild(el);
tiles.push(el);
}

var tl = gsap.timeline({ paused: true });
for (var j = 0; j < tiles.length; j++) {
var t = tiles[j];
var phase = j / tiles.length;
var start = phase * (DURATION_SEC - 4);
tl.to(
t,
{
x: 200 * Math.cos(phase * Math.PI * 2),
y: 120 * Math.sin(phase * Math.PI * 2),
rotation: 360,
scale: 1.4,
opacity: 0.6,
borderRadius: "48px",
duration: 2,
ease: "power2.inOut",
},
start,
);
tl.to(
t,
{
x: 0,
y: 0,
rotation: 0,
scale: 1,
opacity: 1,
borderRadius: "12px",
duration: 2,
ease: "power2.inOut",
},
start + 2,
);
}

window.__timelines = window.__timelines || {};
window.__timelines["main"] = tl;
})();
</script>
</body>
</html>
Loading
Loading