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
14 changes: 14 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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" }
}
86 changes: 86 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -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"
11 changes: 11 additions & 0 deletions docs/ci-runners.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
57 changes: 57 additions & 0 deletions scripts/ci/bench_compare.py
Original file line number Diff line number Diff line change
@@ -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()
Loading