From 37b3720eac7e95954ced8941157f342e307b598a Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 8 Oct 2026 06:29:53 +0300 Subject: [PATCH] feat(dx): add Dev Container and informational grid benchmark workflow --- .devcontainer/Dockerfile | 14 ++++++ .devcontainer/devcontainer.json | 23 +++++++++ .github/workflows/benchmark.yml | 86 +++++++++++++++++++++++++++++++++ docs/ci-runners.md | 11 +++++ scripts/ci/bench_compare.py | 57 ++++++++++++++++++++++ 5 files changed, 191 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/benchmark.yml create mode 100644 scripts/ci/bench_compare.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..fd5e346f --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,14 @@ +FROM ghcr.io/cirruslabs/flutter:3.41.6 + +# Linux desktop toolchain + libs the app links against. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + clang cmake ninja-build pkg-config libgtk-3-dev libsecret-1-dev \ + libsqlite3-dev libglu1-mesa xvfb git curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Docker CLI so the dev stack (docker/docker-compose.yml) can be started from inside the container. +COPY --from=docker:27-cli /usr/local/bin/docker /usr/local/bin/docker +COPY --from=docker:27-cli /usr/local/libexec/docker/cli-plugins/docker-compose /usr/local/libexec/docker/cli-plugins/docker-compose + +ENV DISPLAY=:99 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..dbf02ce3 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +{ + "name": "Querya Desktop", + "build": { "dockerfile": "Dockerfile" }, + "features": { + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} + }, + "postCreateCommand": "flutter config --enable-linux-desktop && flutter pub get", + "postStartCommand": "docker compose -f docker/docker-compose.yml up -d || echo 'Docker stack not started (run it manually: cd docker && docker compose up -d)'", + "forwardPorts": [5432, 3306, 6379, 27017, 8123, 9000], + "portsAttributes": { + "5432": { "label": "PostgreSQL" }, + "3306": { "label": "MySQL" }, + "6379": { "label": "Redis" }, + "27017": { "label": "MongoDB" }, + "8123": { "label": "ClickHouse HTTP" } + }, + "customizations": { + "vscode": { + "extensions": ["Dart-Code.dart-code", "Dart-Code.flutter"] + } + }, + "hostRequirements": { "cpus": 4, "memory": "8gb" } +} diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 00000000..2daf301e --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,86 @@ +name: Benchmark + +# Informational: compares frame timings of benchmark/grid_perf_bench.dart on the +# PR against its base branch and comments when the PR is >5% slower. Never +# blocks a merge. + +on: + pull_request: + branches: [ dev, main ] + paths: + - 'lib/features/workspace/**' + - 'lib/core/database/**' + - 'lib/core/csv/**' + - 'benchmark/**' + - 'scripts/ci/bench_compare.py' + - '.github/workflows/benchmark.yml' + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +concurrency: + group: benchmark-${{ github.ref }} + cancel-in-progress: true + +jobs: + grid-bench: + name: Grid performance + # Forks get no write token and must never reach any self-hosted runner. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 45 + continue-on-error: true + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Linux dependencies + timeout-minutes: 8 + run: | + sudo apt-get -o Acquire::Retries=3 update + sudo apt-get -o Acquire::Retries=3 install -y clang cmake ninja-build pkg-config libgtk-3-dev libsecret-1-dev libsqlite3-dev xvfb libgl1-mesa-dri + + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.41.6' + channel: 'stable' + cache: true + + - name: Run benchmark on PR head + timeout-minutes: 15 + run: | + flutter pub get + xvfb-run -a -s '-screen 0 1920x1080x24' flutter run --profile -d linux \ + -t benchmark/grid_perf_bench.dart --dart-define=MODE=scroll \ + 2>&1 | tee "$RUNNER_TEMP/head.log" || true + + - name: Run benchmark on base + if: github.event_name == 'pull_request' + timeout-minutes: 15 + run: | + git worktree add "$RUNNER_TEMP/base" "origin/${{ github.base_ref }}" + rm -rf "$RUNNER_TEMP/base/benchmark" + cp -r benchmark "$RUNNER_TEMP/base/benchmark" + cd "$RUNNER_TEMP/base" + flutter pub get + xvfb-run -a -s '-screen 0 1920x1080x24' flutter run --profile -d linux \ + -t benchmark/grid_perf_bench.dart --dart-define=MODE=scroll \ + 2>&1 | tee "$RUNNER_TEMP/base.log" || true + + - name: Compare + id: compare + run: | + python3 scripts/ci/bench_compare.py "$RUNNER_TEMP/base.log" "$RUNNER_TEMP/head.log" 5 > "$RUNNER_TEMP/report.txt" + cat "$RUNNER_TEMP/report.txt" + echo "regression=$(head -1 "$RUNNER_TEMP/report.txt" | cut -d= -f2)" >> "$GITHUB_OUTPUT" + tail -n +2 "$RUNNER_TEMP/report.txt" > "$RUNNER_TEMP/report.md" + cat "$RUNNER_TEMP/report.md" >> "$GITHUB_STEP_SUMMARY" + + - name: Comment on regression + if: github.event_name == 'pull_request' && steps.compare.outputs.regression == '1' + env: + GH_TOKEN: ${{ github.token }} + run: gh pr comment "${{ github.event.pull_request.number }}" --body-file "$RUNNER_TEMP/report.md" diff --git a/docs/ci-runners.md b/docs/ci-runners.md index e4c76aa2..68cf794e 100644 --- a/docs/ci-runners.md +++ b/docs/ci-runners.md @@ -67,3 +67,14 @@ pinned in one place (`ci.yml`). - Keep the machine updated (`apt upgrade`) and clean old workspaces under `/home/runner/actions-runner-*/_work` if the disk fills. - The runners hold no secrets of ours; do not add any to the container. + +## Dev Container and benchmarks + +`.devcontainer/` provides a ready environment (Flutter 3.41.6, Linux desktop +toolchain, Docker CLI) for GitHub Codespaces or VS Code Dev Containers. On start +it runs `docker/docker-compose.yml` (PostgreSQL, MySQL, Redis, MongoDB, +ClickHouse) and forwards their ports. + +`.github/workflows/benchmark.yml` runs `benchmark/grid_perf_bench.dart` under +Xvfb on the PR and on its base branch and comments when frame times are more +than 5% worse. It is informational and never blocks a merge. diff --git a/scripts/ci/bench_compare.py b/scripts/ci/bench_compare.py new file mode 100644 index 00000000..17522511 --- /dev/null +++ b/scripts/ci/bench_compare.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Compare two grid_perf_bench logs and print a Markdown report. + +usage: bench_compare.py BASE.log HEAD.log [threshold_percent] + +Reads the `BENCH total p50=.. p90=.. p99=..` line of each log. Always exits 0: +the report is informational. The first output line is `REGRESSION=1` or +`REGRESSION=0` so a workflow can decide whether to comment. +""" +import re +import sys + + +def parse(path): + metrics = {} + try: + text = open(path, encoding="utf-8", errors="replace").read() + except OSError: + return metrics + m = re.search(r"BENCH total\s+p50=([\d.]+)\s+p90=([\d.]+)\s+p99=([\d.]+)", text) + if m: + metrics = {"p50": float(m[1]), "p90": float(m[2]), "p99": float(m[3])} + s = re.search(r"stutters\(>12\.5ms\)=(\d+)", text) + if s: + metrics["stutters"] = float(s[1]) + return metrics + + +def main(): + base, head = parse(sys.argv[1]), parse(sys.argv[2]) + threshold = float(sys.argv[3]) if len(sys.argv) > 3 else 5.0 + if not base or not head: + print("REGRESSION=0") + print("Benchmark produced no comparable output (base or head log has no `BENCH total` line).") + return + rows, regressed = [], False + for key in ("p50", "p90", "p99", "stutters"): + if key not in base or key not in head: + continue + b, h = base[key], head[key] + delta = (h - b) / b * 100 if b else (0.0 if h == 0 else 100.0) + # Tiny absolute differences are noise on a shared runner. + worse = delta > threshold and (h - b) > (0.3 if key != "stutters" else 2) + regressed = regressed or worse + unit = "" if key == "stutters" else " ms" + rows.append(f"| {key} | {b:.2f}{unit} | {h:.2f}{unit} | {delta:+.1f}% {'⚠️' if worse else ''} |") + print(f"REGRESSION={1 if regressed else 0}") + print("### Grid scroll benchmark") + print() + print("| metric | base | PR | change |") + print("|---|---|---|---|") + print("\n".join(rows)) + print() + print(f"Informational only (threshold {threshold:g}%). Shared CI runners are noisy; re-run before trusting a single result.") + + +main()