Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d2cb665
fix(semantics): remove hand-rolled SemanticsNode creation that caused…
Mar 23, 2026
2c648b2
perf/fix: three code-review improvements
Mar 23, 2026
30c3736
feat(devtools): complete hyper_render_devtools integration
Mar 23, 2026
1f93829
docs/feat(heuristics): document form gap and add hasForms() check
Mar 23, 2026
739026b
fix(P0): replace unbounded image cache with LRU eviction
Mar 23, 2026
e85b5b3
perf(P0): eliminate regex re-compilation and intrinsic-width O(N) jank
Mar 23, 2026
917cc22
fix(core): P1 edge-case hardening — tap slop, deep-link schemes, calc…
Mar 23, 2026
3356ac3
fix(a11y,perf): P2 — viewport semantics for headings/links + skip mea…
Mar 23, 2026
3736aca
fix: Fix some bugs
Mar 23, 2026
deb7ba9
fix: token-based image cancellation, table OOM cap, surrogate-safe te…
vietnguyentuan2019 Mar 23, 2026
15c2929
fix(selection): prevent StackOverflow when releasing scroll hold afte…
vietnguyentuan2019 Mar 23, 2026
962bf1c
fix(selection): replace GestureDetector with Listener to unblock Copy…
vietnguyentuan2019 Mar 23, 2026
08908d5
test(p2): add 42-test suite covering P2 fixes; update accessibility d…
Mar 24, 2026
614b510
merge: sync latest main into refactor/improve; fix 3 flaky integratio…
Mar 24, 2026
da7ffce
feat(selection): cross-chunk Selection Orchestrator for virtualized mode
Mar 24, 2026
075c0ed
feat(svg): add native SVG rendering via flutter_svg
Mar 24, 2026
5c10580
fix(demo): use value: instead of initialValue: in DropdownButtonFormF…
Mar 24, 2026
065ee85
fix(float): add parse-time guard to avoid wasted space at chunk bound…
Mar 24, 2026
fe28e34
perf(kinsoku): replace O(N) string scan with O(1) Set<int> lookup tables
Mar 24, 2026
0923b68
perf(kinsoku): upgrade Set<int> to Uint8List bitmask for O(1) direct-…
Mar 24, 2026
c09c9df
fix: table ANR guard + cross-chunk float continuity
Mar 24, 2026
dc62376
feat(table): vertical-align top/middle/bottom for table cells
Mar 24, 2026
9f7fd4e
feat(table): add screen-reader semantics for tables and header cells
Mar 24, 2026
0a54f09
fix: temp for CSS Keyframes/Animations
Mar 24, 2026
91136f0
feat: Update info, code, documents for release 1.1.2
vietnguyentuan2019 Mar 24, 2026
f33c0eb
feat: v1.1.2 — binary search selection, devtools v1.0.0, 3-pipeline C…
Mar 25, 2026
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
144 changes: 112 additions & 32 deletions .github/workflows/analyze.yml
Original file line number Diff line number Diff line change
@@ -1,60 +1,140 @@
name: Static Analysis
name: Pre-flight Checks

# ─────────────────────────────────────────────────────────────────────────────
# Pipeline 1 — Pre-flight (target: < 2 min)
#
# Runs on every push / PR as the fast gate that must pass before any heavy
# job is allowed to start. Three responsibilities:
#
# 1. dart format — code style, fails on any formatting diff
# 2. flutter analyze --fatal-infos — zero errors/warnings/infos enforced
# 3. Changed-path detection — outputs which packages were touched so that
# downstream jobs (test.yml) know what to test
#
# Optimisations vs the old setup:
# • Single runner (ubuntu-22.04), single Flutter version — no duplication
# • Skips ALL checks when only *.md / doc/ / .github/ files changed
# • pub-cache restored from cache before `flutter pub get`
# ─────────────────────────────────────────────────────────────────────────────

env:
FLUTTER_VERSION: "3.29.2"

on:
push:
branches: [ main, develop ]
branches: [main, develop]
pull_request:
branches: [ main, develop ]
branches: [main, develop]

jobs:
analyze:
name: Analyze Code
runs-on: ubuntu-latest
preflight:
name: Format · Analyze · Path-filter
runs-on: ubuntu-22.04

outputs:
# Which packages (or groups) have changed — consumed by test.yml
changed_core: ${{ steps.filter.outputs.core }}
changed_html: ${{ steps.filter.outputs.html }}
changed_markdown: ${{ steps.filter.outputs.markdown }}
changed_highlight: ${{ steps.filter.outputs.highlight }}
changed_clipboard: ${{ steps.filter.outputs.clipboard }}
changed_devtools: ${{ steps.filter.outputs.devtools }}
changed_root: ${{ steps.filter.outputs.root }}
changed_any_dart: ${{ steps.filter.outputs.any_dart }}

steps:
- name: Checkout code
- name: Checkout
uses: actions/checkout@v4

# ── Detect changed paths ────────────────────────────────────────────────
- name: Detect changed paths
id: filter
uses: dorny/paths-filter@v3
with:
filters: |
core:
- 'packages/hyper_render_core/**'
html:
- 'packages/hyper_render_html/**'
markdown:
- 'packages/hyper_render_markdown/**'
highlight:
- 'packages/hyper_render_highlight/**'
clipboard:
- 'packages/hyper_render_clipboard/**'
devtools:
- 'packages/hyper_render_devtools/**'
root:
- 'lib/**'
- 'test/**'
- 'pubspec.yaml'
- 'pubspec.lock'
any_dart:
- '**/*.dart'
- '**/pubspec.yaml'
- '**/pubspec.lock'
- '**/analysis_options.yaml'

# ── Skip everything if only docs / CI config changed ───────────────────
- name: Skip if docs-only change
if: steps.filter.outputs.any_dart == 'false'
run: |
echo "✓ Only non-Dart files changed — skipping format & analyze"
echo " (golden.yml and benchmark.yml handle their own triggers)"

# ── Flutter setup (only when Dart files changed) ────────────────────────
- name: Setup Flutter
if: steps.filter.outputs.any_dart == 'true'
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: stable
cache: true

- name: Get dependencies
- name: Restore pub cache
if: steps.filter.outputs.any_dart == 'true'
uses: actions/cache@v4
with:
path: |
~/.pub-cache
${{ env.PUB_CACHE }}
key: pub-ubuntu-${{ hashFiles('**/pubspec.lock') }}
restore-keys: pub-ubuntu-

- name: flutter pub get
if: steps.filter.outputs.any_dart == 'true'
run: flutter pub get

- name: Run flutter analyze
run: flutter analyze --no-pub > analyze_report.txt || true
# ── dart format ─────────────────────────────────────────────────────────
- name: dart format (fail on diff)
if: steps.filter.outputs.any_dart == 'true'
run: dart format --set-exit-if-changed .

- name: Check for errors
# ── flutter analyze ─────────────────────────────────────────────────────
- name: flutter analyze --fatal-infos
if: steps.filter.outputs.any_dart == 'true'
run: |
errors=$(grep -c "error •" analyze_report.txt || true)
warnings=$(grep -c "warning •" analyze_report.txt || true)
infos=$(grep -c "info •" analyze_report.txt || true)

echo "Static Analysis Results:"
echo " Errors: $errors"
echo " Warnings: $warnings"
echo " Infos: $infos"
flutter analyze --no-pub --fatal-infos 2>&1 | tee analyze_report.txt
EXIT=${PIPESTATUS[0]}

# Fail if there are errors
if [ "$errors" -gt 0 ]; then
echo "❌ Found $errors errors"
cat analyze_report.txt
exit 1
fi
ERRORS=$(grep -c "error •" analyze_report.txt 2>/dev/null || true)
WARNS=$(grep -c "warning •" analyze_report.txt 2>/dev/null || true)
INFOS=$(grep -c "info •" analyze_report.txt 2>/dev/null || true)

# Warn if there are many warnings
if [ "$warnings" -gt 20 ]; then
echo "⚠️ High number of warnings: $warnings"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " Static Analysis Results"
echo " Errors: $ERRORS"
echo " Warnings: $WARNS"
echo " Infos: $INFOS"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

echo "✅ Static analysis passed"
exit $EXIT

- name: Upload analyze report
if: always()
if: always() && steps.filter.outputs.any_dart == 'true'
uses: actions/upload-artifact@v4
with:
name: analyze-report
name: analyze-report-${{ github.run_number }}
path: analyze_report.txt
retention-days: 7
195 changes: 161 additions & 34 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -1,63 +1,190 @@
name: Benchmarks
name: Performance Regression

on:
# Run benchmarks weekly
schedule:
- cron: '0 0 * * 0' # Every Sunday at midnight UTC
# ─────────────────────────────────────────────────────────────────────────────
# Two jobs:
#
# layout-regression — runs on every PR and every push to main.
# • Executes benchmark/layout_regression.dart
# • Fails if any fixture's median layout time exceeds its hard threshold
# • Posts a PR comment with the full results table
#
# full-benchmark — runs weekly + on release branches.
# • Executes the original benchmark/parse_benchmark.dart (throughput info)
# • Never fails CI (informational only) — results are uploaded as artifacts
# ─────────────────────────────────────────────────────────────────────────────

# Allow manual trigger
workflow_dispatch:
env:
FLUTTER_VERSION: "3.29.2" # keep in sync with golden.yml

# Run on release branches
on:
pull_request:
branches: [main, develop]
push:
branches:
- 'release/**'
branches: [main]
schedule:
- cron: '0 0 * * 0' # weekly full benchmark (Sunday midnight UTC)
workflow_dispatch:

jobs:
benchmark:
name: Run Performance Benchmarks
runs-on: ubuntu-latest
# ── Layout regression guard (runs on every PR) ────────────────────────────
layout-regression:
name: Layout Regression (60 FPS guard)
runs-on: ubuntu-22.04
# Skip on the weekly schedule — that's for the full-benchmark job only
if: github.event_name != 'schedule'

steps:
- name: Checkout code
- name: Checkout
uses: actions/checkout@v4

- name: Setup Flutter
- name: Setup Flutter (pinned)
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: stable
cache: true

- name: Get dependencies
run: flutter pub get

- name: Run benchmarks
- name: Run layout regression benchmark
id: bench
run: |
echo "Running benchmarks..."
flutter test benchmark/ --no-test-randomize-ordering-seed
mkdir -p benchmark/results

- name: Save benchmark results
run: |
mkdir -p benchmark_results
echo "Date: $(date)" > benchmark_results/latest.txt
echo "Commit: ${{ github.sha }}" >> benchmark_results/latest.txt
echo "" >> benchmark_results/latest.txt
echo "See test output above for detailed results" >> benchmark_results/latest.txt
# Run with JSON reporter so we can parse pass/fail
flutter test benchmark/layout_regression.dart \
--reporter expanded \
2>&1 | tee benchmark/results/ci_run.txt

- name: Upload benchmark results
# Extract overall pass/fail from test exit code
EXIT_CODE=${PIPESTATUS[0]}

# Parse the JSON result files for the summary table
SUMMARY=$(python3 - <<'PYEOF'
import json, os, glob

files = sorted(glob.glob('benchmark/results/layout_*.json'))
if not files:
print("No result file generated.")
else:
data = json.load(open(files[-1]))
rows = []
any_fail = False
for r in data.get('results', []):
icon = "✅" if r["passed"] else "❌"
rows.append(
f"| {icon} | `{r['fixture']}` | {r['threshold_ms']} | "
f"{r['median_ms']} | {r['p95_ms']} |"
)
if not r["passed"]:
any_fail = True

header = (
"| | Fixture | Budget (ms) | Median (ms) | P95 (ms) |\n"
"|---|---|---|---|---|"
)
print(header)
print("\n".join(rows))
if any_fail:
print("\n**One or more fixtures exceeded the 16 ms budget.**")
PYEOF
)

echo "summary<<EOF" >> "$GITHUB_OUTPUT"
echo "$SUMMARY" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT"

exit $EXIT_CODE

- name: Upload result JSON
if: always()
uses: actions/upload-artifact@v4
with:
name: benchmark-results
path: benchmark_results/
name: layout-regression-${{ github.run_number }}
path: benchmark/results/
retention-days: 30

- name: Comment on PR (if applicable)
if: github.event_name == 'pull_request'
- name: Post PR comment
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
const exitCode = '${{ steps.bench.outputs.exit_code }}';
const summary = `${{ steps.bench.outputs.summary }}`;
const passed = exitCode === '0';
const icon = passed ? '✅' : '❌';
const headline = passed
? '## ✅ Layout Regression — All fixtures within 60 FPS budget'
: '## ❌ Layout Regression — Budget exceeded';

const body = [
headline,
'',
summary,
'',
`> Flutter \`${{ env.FLUTTER_VERSION }}\` · ubuntu-22.04`,
'',
passed
? '_No action required._'
: [
'**Action required:** a layout fixture exceeded its millisecond',
'budget. Profile the regression with:',
'```bash',
'flutter test benchmark/layout_regression.dart --reporter expanded',
'```',
'and check `_performLineLayout` / `_buildCharacterMapping` for',
'any new O(N²) or O(N log N) paths introduced in this PR.',
].join('\n'),
].join('\n');

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
body: '🏎️ Benchmark tests completed. Check the Actions tab for detailed results.'
})
issue_number: context.issue.number,
body,
});

# ── Full throughput benchmark (weekly, informational) ────────────────────
full-benchmark:
name: Full Throughput Benchmark
runs-on: ubuntu-22.04
if: >-
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
startsWith(github.ref, 'refs/heads/release/')

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Flutter (pinned)
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: stable
cache: true

- name: Get dependencies
run: flutter pub get

- name: Run parse benchmarks
run: |
flutter test benchmark/parse_benchmark.dart \
--no-test-randomize-ordering-seed \
--reporter expanded \
2>&1 | tee benchmark/results/parse_$(date +%Y%m%d).txt

- name: Run layout regression (informational — never fails here)
run: |
flutter test benchmark/layout_regression.dart \
--reporter expanded \
2>&1 | tee benchmark/results/layout_$(date +%Y%m%d).txt || true

- name: Upload benchmark results
uses: actions/upload-artifact@v4
with:
name: benchmark-full-${{ github.run_number }}
path: benchmark/results/
retention-days: 90
Loading
Loading