-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add e2e CI workflow for Go benchmarks #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5ef45f6
8197815
1b4da8e
bb25b97
6318ae2
fffdf14
0220e29
9670d2a
883a2d0
48abee9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| name: "E2E: Go benchmarks" | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: [main, feat/github-actions] | ||
| paths: | ||
| - "apps/go/**" | ||
| - "configs/go-*.yaml" | ||
| - "lib/**" | ||
| - "templates/**" | ||
| - ".github/workflows/e2e-go.yml" | ||
| - ".github/workflows/benchmark.yml" | ||
| workflow_dispatch: | ||
| inputs: | ||
| sdk-version: | ||
| description: "sentry-go version (e.g. 0.42.0 or latest)" | ||
| default: "latest" | ||
|
|
||
| jobs: | ||
| resolve-version: | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| version: ${{ steps.sdk.outputs.version }} | ||
| steps: | ||
| - name: Resolve SDK version | ||
| id: sdk | ||
| env: | ||
| INPUT_VERSION: ${{ inputs.sdk-version }} | ||
| run: | | ||
| VERSION="${INPUT_VERSION:-latest}" | ||
| if [ "$VERSION" = "latest" ]; then | ||
| VERSION=$(curl -sf 'https://proxy.golang.org/github.com/getsentry/sentry-go/@latest' | python3 -c "import json,sys; print(json.load(sys.stdin)['Version'].lstrip('v'))") | ||
| echo "Resolved latest sentry-go version: $VERSION" | ||
| fi | ||
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | ||
sentry[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| bench: | ||
| needs: resolve-version | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| app: [go/net-http, go/gin, go/echo] | ||
| uses: ./.github/workflows/benchmark.yml | ||
| with: | ||
| app: ${{ matrix.app }} | ||
| sdk-version: ${{ needs.resolve-version.outputs.version }} | ||
| iterations: 1 | ||
| post-comment: true | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
|
|
||
| import json | ||
| import logging | ||
| import math | ||
| import os | ||
| import subprocess | ||
| import threading | ||
|
|
@@ -222,6 +223,13 @@ def run_benchmark( | |
| result = run_variant(config, variant, i, results_base) | ||
| all_results["iterations"].append(result) | ||
|
|
||
| # Compute summary with overhead metrics | ||
| all_results["summary"] = _compute_summary(all_results["iterations"]) | ||
| all_results["load"] = { | ||
| "rps": config.get("load", {}).get("rps"), | ||
| "duration": config.get("load", {}).get("duration"), | ||
| } | ||
|
|
||
| # Write aggregated results | ||
| results_path = os.path.join(results_base, "results.json") | ||
| with open(results_path, "w") as f: | ||
|
|
@@ -231,9 +239,67 @@ def run_benchmark( | |
| return all_results | ||
|
|
||
|
|
||
| def _percentile(sorted_values: list[float], p: float) -> float: | ||
| """Compute the p-th percentile from a sorted list of values.""" | ||
| if not sorted_values: | ||
| return 0.0 | ||
| k = (len(sorted_values) - 1) * (p / 100.0) | ||
| f = math.floor(k) | ||
| c = math.ceil(k) | ||
| if f == c: | ||
| return sorted_values[int(k)] | ||
| return sorted_values[f] * (c - k) + sorted_values[c] * (k - f) | ||
|
|
||
|
|
||
| def _extract_latencies(iterations: list[dict], variant: str) -> list[float]: | ||
| """Extract all latency values (in nanoseconds) for a given variant.""" | ||
| latencies = [] | ||
| for it in iterations: | ||
| if it.get("variant") != variant: | ||
| continue | ||
| for req in it.get("vegeta_results") or []: | ||
| latencies.append(req["latency"]) | ||
| return latencies | ||
|
|
||
|
|
||
| def _compute_summary(iterations: list[dict]) -> dict: | ||
| """Compute overhead summary by comparing baseline vs instrumented latencies.""" | ||
| baseline_latencies = sorted(_extract_latencies(iterations, "baseline")) | ||
| instrumented_latencies = sorted(_extract_latencies(iterations, "instrumented")) | ||
|
|
||
| if not baseline_latencies or not instrumented_latencies: | ||
| logger.warning("Missing baseline or instrumented data for summary computation") | ||
| return {"overhead": {}, "regression": False} | ||
|
|
||
| overhead = {} | ||
| regression = False | ||
| for name, p in [("p50", 50), ("p90", 90), ("p95", 95), ("p99", 99)]: | ||
| base_val = _percentile(baseline_latencies, p) | ||
| inst_val = _percentile(instrumented_latencies, p) | ||
| if base_val > 0: | ||
| pct = (inst_val - base_val) / base_val * 100.0 | ||
| overhead[name] = round(pct, 2) | ||
|
|
||
| base_mean = sum(baseline_latencies) / len(baseline_latencies) | ||
| inst_mean = sum(instrumented_latencies) / len(instrumented_latencies) | ||
| if base_mean > 0: | ||
| overhead["mean"] = round((inst_mean - base_mean) / base_mean * 100.0, 2) | ||
|
|
||
| # Flag regression if p99 overhead exceeds 10% | ||
| if overhead.get("p99", 0) > 10: | ||
| regression = True | ||
|
|
||
| return {"overhead": overhead, "regression": regression} | ||
|
|
||
|
|
||
| def _prepare_sdk_version(config: dict, sdk_version: str) -> None: | ||
| """Template the SDK version into requirements files.""" | ||
| app_dir = PROJECT_ROOT / config["app_dir"] | ||
| language = config.get("language", "") | ||
|
|
||
| if language == "go": | ||
| _prepare_go_sdk_version(app_dir, sdk_version) | ||
| return | ||
|
|
||
| # Python: render requirements-sentry.txt from template | ||
| tmpl_path = app_dir / "requirements-sentry.txt.tmpl" | ||
|
|
@@ -245,3 +311,47 @@ def _prepare_sdk_version(config: dict, sdk_version: str) -> None: | |
| with open(output_path, "w") as f: | ||
| f.write(rendered) | ||
| logger.info("Rendered %s with sdk_version=%s", output_path, sdk_version) | ||
|
|
||
|
|
||
| def _prepare_go_sdk_version(app_dir: Path, sdk_version: str) -> None: | ||
| """Update sentry-go module versions in a Go app's go.mod.""" | ||
| go_mod = app_dir / "go.mod" | ||
| if not go_mod.exists(): | ||
| return | ||
|
|
||
| # Find all sentry-go modules referenced in go.mod | ||
| with open(go_mod) as f: | ||
| content = f.read() | ||
|
|
||
| sentry_modules = [] | ||
| for line in content.splitlines(): | ||
| line = line.strip() | ||
| # Match lines like: github.com/getsentry/sentry-go v0.42.0 | ||
| # or: github.com/getsentry/sentry-go/gin v0.42.0 | ||
| if "github.com/getsentry/sentry-go" in line and not line.startswith("module"): | ||
| parts = line.split() | ||
| if parts: | ||
| mod = parts[0] | ||
sentry[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if mod.startswith("github.com/getsentry/sentry-go"): | ||
sentry[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| sentry_modules.append(mod) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Go.mod parser misses single-line require directivesHigh Severity The |
||
|
|
||
| if not sentry_modules: | ||
| logger.warning("No sentry-go modules found in %s", go_mod) | ||
| return | ||
|
|
||
| # Use go get to update each sentry-go module to the target version | ||
| version_spec = f"@v{sdk_version}" if sdk_version != "latest" and not sdk_version.startswith("v") else f"@{sdk_version}" | ||
| get_args = [f"{mod}{version_spec}" for mod in sentry_modules] | ||
|
|
||
| logger.info("Updating sentry-go modules in %s: %s", app_dir, get_args) | ||
| subprocess.run( | ||
| ["go", "get"] + get_args, | ||
| cwd=str(app_dir), | ||
| check=True, | ||
| ) | ||
| subprocess.run( | ||
| ["go", "mod", "tidy"], | ||
| cwd=str(app_dir), | ||
| check=True, | ||
| ) | ||
| logger.info("Updated go.mod in %s to sentry-go %s", app_dir, sdk_version) | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate setup-go step accidentally added to workflow
Low Severity
The
actions/setup-go@v5step (lines 53–57) is an exact duplicate of the step already present at lines 47–51. Both have the sameifcondition,go-version, andcachesetting. This appears to be an accidental copy-paste that causes Go to be set up twice for every Go benchmark run.