Skip to content

feat(scanner): add page-scale ribbon candidate #188

feat(scanner): add page-scale ribbon candidate

feat(scanner): add page-scale ribbon candidate #188

Workflow file for this run

name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
name: ${{ matrix.os }} / ${{ matrix.toolchain }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
toolchain: [stable]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --all --check
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- run: cargo test --workspace --all-features
- run: cargo doc --workspace --all-features --no-deps
- name: Enforce docs on phase-critical public APIs
env:
RUSTDOCFLAGS: -Dmissing-docs
run: |
cargo doc -p glyphnet-ecc --no-deps
cargo doc -p glyphnet-decode --no-deps
cargo doc -p glyphnet-scanner --no-deps
scanner-perf-gate:
name: scanner perf gate
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
env:
SCANNER_BENCH_TOLERANCE_PCT: "200"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Check PR branch perf gate
if: github.event_name == 'pull_request'
continue-on-error: true
run: SCANNER_BENCH_NAMES=scan_generated_ribbon_canvas_small,scan_generated_ribbon_canvas_medium,scan_generated_ribbon_canvas_large,scan_generated_matrix_roi,scan_generated_matrix_canvas,scan_real_debugger_screenshot SCANNER_BENCH_NON_GATING=scan_generated_matrix_canvas,scan_real_debugger_screenshot SCANNER_BENCH_OUTPUT_JSON=target/scanner-perf/pr.json scripts/check_scanner_perf.sh
- name: Capture base benchmark metrics
if: always() && github.event_name == 'pull_request'
continue-on-error: true
run: |
git worktree add /tmp/glyphnet-base origin/${{ github.base_ref }}
mkdir -p /tmp/glyphnet-base/scripts
cp scripts/check_scanner_perf.sh /tmp/glyphnet-base/scripts/check_scanner_perf.sh
cd /tmp/glyphnet-base
SCANNER_BENCH_NAMES=scan_generated_ribbon_canvas_small,scan_generated_ribbon_canvas_medium,scan_generated_ribbon_canvas_large,scan_generated_matrix_roi,scan_generated_matrix_canvas,scan_real_debugger_screenshot SCANNER_BENCH_NON_GATING=scan_generated_matrix_canvas,scan_real_debugger_screenshot SCANNER_BENCH_OUTPUT_JSON=target/scanner-perf/base.json ./scripts/check_scanner_perf.sh --no-fail
- name: Comment benchmark comparison on PR
if: always() && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const prPath = 'target/scanner-perf/pr.json';
const basePath = '/tmp/glyphnet-base/target/scanner-perf/base.json';
const pr = fs.existsSync(prPath)
? JSON.parse(fs.readFileSync(prPath, 'utf8'))
: null;
const base = fs.existsSync(basePath)
? JSON.parse(fs.readFileSync(basePath, 'utf8'))
: null;
const toMap = payload => new Map((payload?.cases || []).map(c => [c.bench_name, c]));
const prMap = toMap(pr);
const baseMap = toMap(base);
const benchNames = Array.from(new Set([...prMap.keys(), ...baseMap.keys()]));
const matrixBenchNames = benchNames.filter(name => name.includes('matrix'));
const matrixCases = matrixBenchNames
.map(name => prMap.get(name))
.filter(Boolean);
const matrixGatingCases = matrixCases.filter(c => c.gating !== false);
const matrixPassCount = matrixGatingCases.filter(c => c.status === 'pass').length;
const matrixFailCount = matrixGatingCases.filter(c => c.status === 'fail').length;
const ribbonBenchNames = benchNames.filter(name => !name.includes('matrix'));
const ribbonCases = ribbonBenchNames
.map(name => prMap.get(name))
.filter(Boolean);
const ribbonGatingCases = ribbonCases.filter(c => c.gating !== false);
const ribbonPassCount = ribbonGatingCases.filter(c => c.status === 'pass').length;
const ribbonFailCount = ribbonGatingCases.filter(c => c.status === 'fail').length;
const overallEmoji = pr ? (pr.status === 'pass' ? '✅' : '⚠️') : 'ℹ️';
const marker = '<!-- scanner-perf-comment -->';
const rows = benchNames.length === 0
? ['| Benchmark | Gating | PR median (ms) | Base median (ms) | Delta (ms) | Delta % |',
'|---|:---:|---:|---:|---:|---:|',
'| unavailable | - | - | - | - | - |']
: [
'| Benchmark | Gating | PR median (ms) | Base median (ms) | Delta (ms) | Delta % |',
'|---|:---:|---:|---:|---:|---:|',
...benchNames.map(name => {
const p = prMap.get(name);
const b = baseMap.get(name);
const prMs = p ? p.median_ms : null;
const baseMs = b ? b.median_ms : null;
const delta = (prMs != null && baseMs != null) ? (prMs - baseMs) : null;
const deltaPct = (delta != null && baseMs !== 0) ? (delta / baseMs) * 100 : null;
const prCell = prMs == null ? '-' : prMs.toFixed(3);
const baseCell = baseMs == null ? '-' : baseMs.toFixed(3);
const deltaCell = delta == null ? '-' : delta.toFixed(3);
const deltaPctCell = deltaPct == null ? '-' : `${deltaPct.toFixed(2)}%`;
const gateCell = p && p.gating === false ? 'No' : 'Yes';
const profile = p?.profile_id || b?.profile_id || (name.includes('matrix') ? 'MatrixCompat' : 'RibbonPrint');
return `| ${name} (${profile}) | ${gateCell} | ${prCell} | ${baseCell} | ${deltaCell} | ${deltaPctCell} |`;
})
];
const matrixRows = matrixBenchNames.length === 0
? [
'| Benchmark | Gating | PR median (ms) | Base median (ms) | Delta (ms) | Delta % |',
'|---|:---:|---:|---:|---:|---:|',
'| unavailable | - | - | - | - | - |',
]
: [
'| Benchmark | Gating | PR median (ms) | Base median (ms) | Delta (ms) | Delta % |',
'|---|:---:|---:|---:|---:|---:|',
...matrixBenchNames.map(name => {
const p = prMap.get(name);
const b = baseMap.get(name);
const prMs = p ? p.median_ms : null;
const baseMs = b ? b.median_ms : null;
const delta = (prMs != null && baseMs != null) ? (prMs - baseMs) : null;
const deltaPct = (delta != null && baseMs !== 0) ? (delta / baseMs) * 100 : null;
const prCell = prMs == null ? '-' : prMs.toFixed(3);
const baseCell = baseMs == null ? '-' : baseMs.toFixed(3);
const deltaCell = delta == null ? '-' : delta.toFixed(3);
const deltaPctCell = deltaPct == null ? '-' : `${deltaPct.toFixed(2)}%`;
const gateCell = p && p.gating === false ? 'No' : 'Yes';
const profile = p?.profile_id || b?.profile_id || 'MatrixCompat';
return `| ${name} (${profile}) | ${gateCell} | ${prCell} | ${baseCell} | ${deltaCell} | ${deltaPctCell} |`;
}),
];
const body = [
marker,
'## Scanner perf (ribbon + matrix fixtures)',
'',
pr
? `${overallEmoji} **PR gate status:** ${pr.status.toUpperCase()}`
: '⚠️ **PR gate status:** unavailable (PR benchmark did not produce target/scanner-perf/pr.json)',
pr
? `- **Ribbon budget:** ${pr.ribbon_budget_ms.toFixed(3)} ms (allowed ${pr.ribbon_allowed_ms.toFixed(3)} ms)`
: '- **Ribbon budget:** unavailable',
pr
? `- **Matrix budget:** ${pr.matrix_budget_ms.toFixed(3)} ms (allowed ${pr.matrix_allowed_ms.toFixed(3)} ms)`
: '- **Matrix budget:** unavailable',
pr
? `- **Tolerance:** ${pr.tolerance_pct.toFixed(1)}%`
: '- **Tolerance:** unavailable',
ribbonBenchNames.length > 0
? `- **Ribbon gating benches:** ${ribbonPassCount} pass / ${ribbonFailCount} fail`
: '- **Ribbon gating benches:** unavailable',
matrixBenchNames.length > 0
? `- **Matrix gating benches:** ${matrixPassCount} pass / ${matrixFailCount} fail`
: '- **Matrix gating benches:** unavailable',
'',
rows[0],
rows[1],
...rows.slice(2),
'',
'### Matrix subset',
...matrixRows,
'',
].join('\n');
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 });
const existing = comments.find(c => c.body && c.body.includes(marker) && c.user && c.user.type === 'Bot');
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number, body });
}
burst-reliability:
name: burst reliability
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Check PR branch burst reliability
if: github.event_name == 'pull_request'
continue-on-error: true
run: BURST_RELIABILITY_OUTPUT_JSON=target/burst-reliability/pr.json bash scripts/check_burst_reliability.sh --no-fail
- name: Capture base burst reliability
if: always() && github.event_name == 'pull_request'
continue-on-error: true
run: |
git worktree add /tmp/glyphnet-base origin/${{ github.base_ref }}
mkdir -p /tmp/glyphnet-base/scripts
cp scripts/check_burst_reliability.sh /tmp/glyphnet-base/scripts/check_burst_reliability.sh
cd /tmp/glyphnet-base
BURST_RELIABILITY_OUTPUT_JSON=target/burst-reliability/base.json bash ./scripts/check_burst_reliability.sh --no-fail
- name: Comment burst reliability comparison on PR
if: always() && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const prPath = 'target/burst-reliability/pr.json';
const basePath = '/tmp/glyphnet-base/target/burst-reliability/base.json';
const pr = fs.existsSync(prPath) ? JSON.parse(fs.readFileSync(prPath, 'utf8')) : null;
const base = fs.existsSync(basePath) ? JSON.parse(fs.readFileSync(basePath, 'utf8')) : null;
const byDrop = (payload) => new Map((payload?.cases || []).map(c => [Number(c.drop_rate).toFixed(2), c]));
const prMap = byDrop(pr);
const baseMap = byDrop(base);
const keys = Array.from(new Set([...prMap.keys(), ...baseMap.keys()])).sort();
const marker = '<!-- burst-reliability-comment -->';
const rows = keys.length === 0
? [
'| Drop rate | Gating | PR success | Base success | Delta | PR median frames | Base median frames |',
'|---:|:---:|---:|---:|---:|---:|---:|',
'| - | - | - | - | - | - | - |'
]
: [
'| Drop rate | Gating | PR success | Base success | Delta | PR median frames | Base median frames |',
'|---:|:---:|---:|---:|---:|---:|---:|',
...keys.map(k => {
const p = prMap.get(k);
const b = baseMap.get(k);
const ps = p ? (p.success_rate * 100) : null;
const bs = b ? (b.success_rate * 100) : null;
const delta = (ps != null && bs != null) ? (ps - bs) : null;
const pFrames = p?.median_frames ?? '-';
const bFrames = b?.median_frames ?? '-';
const gating = p ? (p.gating !== false ? 'Yes' : 'No') : (b && b.gating !== false ? 'Yes' : 'No');
return `| ${(Number(k) * 100).toFixed(0)}% | ${gating} | ${ps == null ? '-' : ps.toFixed(1) + '%'} | ${bs == null ? '-' : bs.toFixed(1) + '%'} | ${delta == null ? '-' : delta.toFixed(1) + 'pp'} | ${pFrames} | ${bFrames} |`;
})
];
const gatingCases = (pr?.cases || []).filter(c => c.gating !== false);
const passCount = gatingCases.filter(c => c.status === 'pass').length;
const failCount = gatingCases.filter(c => c.status === 'fail').length;
const body = [
marker,
'## Burst reliability (loss sweep)',
'',
pr
? `${pr.status === 'pass' ? '✅' : '⚠️'} **PR gate status:** ${pr.status.toUpperCase()}`
: '⚠️ **PR status:** unavailable',
pr
? `- **Gating rows:** ${passCount} pass / ${failCount} fail`
: '- **Gating rows:** unavailable',
'- **Non-gating rows:** none',
'',
...rows,
'',
].join('\n');
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 });
const existing = comments.find(c => c.body && c.body.includes(marker) && c.user && c.user.type === 'Bot');
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number, body });
}
wasm:
name: wasm32 check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- run: cargo check -p glyphnet-wasm --target wasm32-unknown-unknown
browser-sdk:
name: browser sdk
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- uses: oven-sh/setup-bun@v2
- uses: jetli/wasm-pack-action@v0.4.0
- name: Install browser SDK deps
working-directory: sdk/browser
run: bun install --frozen-lockfile
- name: Build browser SDK package
working-directory: sdk/browser
run: bun run build
- name: Typecheck browser SDK
working-directory: sdk/browser
run: bun run typecheck
- name: Dry-run npm pack
working-directory: sdk/browser
run: bun run pack:dry-run
- name: Consumer smoke test (install + TypeScript import)
run: |
set -euo pipefail
cd sdk/browser
PACKAGE_TGZ="$(npm pack --ignore-scripts | tail -n1)"
CONSUMER_DIR="/tmp/glyphnet-browser-consumer"
rm -rf "$CONSUMER_DIR"
mkdir -p "$CONSUMER_DIR"
cat > "$CONSUMER_DIR/package.json" <<'JSON'
{
"name": "glyphnet-browser-consumer-smoke",
"private": true,
"type": "module"
}
JSON
cd "$CONSUMER_DIR"
bun add "$GITHUB_WORKSPACE/sdk/browser/$PACKAGE_TGZ"
bun add -d typescript
cat > ./smoke.ts <<'TS'
import { initGlyphNet, GlyphNetBrowser } from "@glyphnet/browser";
void initGlyphNet;
void GlyphNetBrowser;
TS
bunx tsc --module es2022 --moduleResolution bundler --target es2022 --noEmit smoke.ts