684 lines
18 KiB
Markdown
684 lines
18 KiB
Markdown
# Performance Benchmarking Operation
|
|
|
|
You are executing the **benchmark** operation to perform load testing, rendering benchmarks, query benchmarks, and regression detection.
|
|
|
|
## Parameters
|
|
|
|
**Received**: `$ARGUMENTS` (after removing 'benchmark' operation name)
|
|
|
|
Expected format: `type:"load|rendering|query|integration|all" [baseline:"version-or-tag"] [duration:"seconds"] [concurrency:"number"] [target:"url-or-endpoint"]`
|
|
|
|
**Parameter definitions**:
|
|
- `type` (required): Benchmark type - `load`, `rendering`, `query`, `integration`, or `all`
|
|
- `baseline` (optional): Baseline version for comparison (e.g., "v1.2.0", "main", "baseline-2025-10-14")
|
|
- `duration` (optional): Test duration in seconds (default: 60s)
|
|
- `concurrency` (optional): Number of concurrent users/connections (default: 50)
|
|
- `target` (optional): Specific URL or endpoint to benchmark
|
|
|
|
## Workflow
|
|
|
|
### 1. Setup Benchmarking Environment
|
|
|
|
```bash
|
|
# Install benchmarking tools
|
|
npm install -g k6 lighthouse-ci autocannon
|
|
|
|
# For database benchmarking
|
|
npm install -g pg-bench
|
|
|
|
# Create benchmark results directory
|
|
mkdir -p benchmark-results/$(date +%Y-%m-%d)
|
|
```
|
|
|
|
### 2. Load Testing with k6
|
|
|
|
**Basic Load Test Script**:
|
|
```javascript
|
|
// loadtest.js
|
|
import http from 'k6/http';
|
|
import { check, sleep } from 'k6';
|
|
import { Rate } from 'k6/metrics';
|
|
|
|
const errorRate = new Rate('errors');
|
|
|
|
export const options = {
|
|
stages: [
|
|
{ duration: '30s', target: 20 }, // Ramp up to 20 users
|
|
{ duration: '1m', target: 50 }, // Stay at 50 users
|
|
{ duration: '30s', target: 100 }, // Spike to 100 users
|
|
{ duration: '1m', target: 50 }, // Back to 50 users
|
|
{ duration: '30s', target: 0 }, // Ramp down
|
|
],
|
|
thresholds: {
|
|
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95% < 500ms, 99% < 1s
|
|
http_req_failed: ['rate<0.01'], // Error rate < 1%
|
|
errors: ['rate<0.1'],
|
|
},
|
|
};
|
|
|
|
export default function () {
|
|
const responses = http.batch([
|
|
['GET', 'https://api.example.com/users'],
|
|
['GET', 'https://api.example.com/posts'],
|
|
['GET', 'https://api.example.com/comments'],
|
|
]);
|
|
|
|
responses.forEach((res) => {
|
|
const success = check(res, {
|
|
'status is 200': (r) => r.status === 200,
|
|
'response time < 500ms': (r) => r.timings.duration < 500,
|
|
});
|
|
|
|
errorRate.add(!success);
|
|
});
|
|
|
|
sleep(1);
|
|
}
|
|
```
|
|
|
|
**Run Load Test**:
|
|
```bash
|
|
# Basic load test
|
|
k6 run loadtest.js
|
|
|
|
# Custom configuration
|
|
k6 run --vus 100 --duration 300s loadtest.js
|
|
|
|
# Output to JSON for analysis
|
|
k6 run --out json=results.json loadtest.js
|
|
|
|
# Cloud run (for distributed testing)
|
|
k6 cloud run loadtest.js
|
|
```
|
|
|
|
**Advanced Load Test with Scenarios**:
|
|
```javascript
|
|
// advanced-loadtest.js
|
|
import http from 'k6/http';
|
|
import { check } from 'k6';
|
|
|
|
export const options = {
|
|
scenarios: {
|
|
// Scenario 1: Constant load
|
|
constant_load: {
|
|
executor: 'constant-vus',
|
|
vus: 50,
|
|
duration: '5m',
|
|
tags: { scenario: 'constant' },
|
|
},
|
|
// Scenario 2: Spike test
|
|
spike_test: {
|
|
executor: 'ramping-vus',
|
|
startVUs: 0,
|
|
stages: [
|
|
{ duration: '10s', target: 200 },
|
|
{ duration: '30s', target: 200 },
|
|
{ duration: '10s', target: 0 },
|
|
],
|
|
startTime: '5m',
|
|
tags: { scenario: 'spike' },
|
|
},
|
|
// Scenario 3: Stress test
|
|
stress_test: {
|
|
executor: 'ramping-arrival-rate',
|
|
startRate: 50,
|
|
timeUnit: '1s',
|
|
stages: [
|
|
{ duration: '2m', target: 100 },
|
|
{ duration: '3m', target: 200 },
|
|
{ duration: '2m', target: 400 },
|
|
],
|
|
startTime: '10m',
|
|
tags: { scenario: 'stress' },
|
|
},
|
|
},
|
|
thresholds: {
|
|
'http_req_duration{scenario:constant}': ['p(95)<500'],
|
|
'http_req_duration{scenario:spike}': ['p(95)<1000'],
|
|
'http_req_failed': ['rate<0.05'],
|
|
},
|
|
};
|
|
|
|
export default function () {
|
|
const res = http.get('https://api.example.com/users');
|
|
check(res, {
|
|
'status is 200': (r) => r.status === 200,
|
|
});
|
|
}
|
|
```
|
|
|
|
### 3. Frontend Rendering Benchmarks
|
|
|
|
**Lighthouse CI Configuration**:
|
|
```json
|
|
// lighthouserc.json
|
|
{
|
|
"ci": {
|
|
"collect": {
|
|
"url": [
|
|
"http://localhost:3000",
|
|
"http://localhost:3000/dashboard",
|
|
"http://localhost:3000/profile"
|
|
],
|
|
"numberOfRuns": 3,
|
|
"settings": {
|
|
"preset": "desktop",
|
|
"throttling": {
|
|
"rttMs": 40,
|
|
"throughputKbps": 10240,
|
|
"cpuSlowdownMultiplier": 1
|
|
}
|
|
}
|
|
},
|
|
"assert": {
|
|
"assertions": {
|
|
"categories:performance": ["error", {"minScore": 0.9}],
|
|
"categories:accessibility": ["error", {"minScore": 0.9}],
|
|
"first-contentful-paint": ["error", {"maxNumericValue": 2000}],
|
|
"largest-contentful-paint": ["error", {"maxNumericValue": 2500}],
|
|
"cumulative-layout-shift": ["error", {"maxNumericValue": 0.1}],
|
|
"total-blocking-time": ["error", {"maxNumericValue": 300}]
|
|
}
|
|
},
|
|
"upload": {
|
|
"target": "filesystem",
|
|
"outputDir": "./benchmark-results"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Run Lighthouse CI**:
|
|
```bash
|
|
# Single run
|
|
lhci autorun
|
|
|
|
# Compare with baseline
|
|
lhci autorun --config=lighthouserc.json
|
|
|
|
# Upload results for comparison
|
|
lhci upload --target=temporary-public-storage
|
|
```
|
|
|
|
**Custom Rendering Benchmark**:
|
|
```javascript
|
|
// rendering-benchmark.js
|
|
const puppeteer = require('puppeteer');
|
|
|
|
async function benchmarkRendering(url, iterations = 10) {
|
|
const browser = await puppeteer.launch();
|
|
const results = [];
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
const page = await browser.newPage();
|
|
|
|
// Start performance measurement
|
|
await page.goto(url, { waitUntil: 'networkidle2' });
|
|
|
|
const metrics = await page.evaluate(() => {
|
|
const navigation = performance.getEntriesByType('navigation')[0];
|
|
const paint = performance.getEntriesByType('paint');
|
|
|
|
return {
|
|
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
|
|
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
|
|
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime,
|
|
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime,
|
|
domInteractive: navigation.domInteractive,
|
|
};
|
|
});
|
|
|
|
results.push(metrics);
|
|
await page.close();
|
|
}
|
|
|
|
await browser.close();
|
|
|
|
// Calculate averages
|
|
const avg = (key) => results.reduce((sum, r) => sum + r[key], 0) / results.length;
|
|
|
|
return {
|
|
avgDOMContentLoaded: avg('domContentLoaded'),
|
|
avgLoadComplete: avg('loadComplete'),
|
|
avgFirstPaint: avg('firstPaint'),
|
|
avgFirstContentfulPaint: avg('firstContentfulPaint'),
|
|
avgDOMInteractive: avg('domInteractive'),
|
|
};
|
|
}
|
|
|
|
// Run benchmark
|
|
benchmarkRendering('http://localhost:3000').then(console.log);
|
|
```
|
|
|
|
### 4. Database Query Benchmarks
|
|
|
|
**PostgreSQL - pg_bench**:
|
|
```bash
|
|
# Initialize benchmark database
|
|
pgbench -i -s 50 benchmark_db
|
|
|
|
# Run benchmark (50 clients, 1000 transactions each)
|
|
pgbench -c 50 -t 1000 benchmark_db
|
|
|
|
# Custom SQL script benchmark
|
|
cat > custom-queries.sql <<'EOF'
|
|
SELECT * FROM users WHERE email = 'test@example.com';
|
|
SELECT p.*, u.name FROM posts p JOIN users u ON p.user_id = u.id LIMIT 100;
|
|
EOF
|
|
|
|
pgbench -c 10 -t 100 -f custom-queries.sql benchmark_db
|
|
|
|
# Output JSON results
|
|
pgbench -c 50 -t 1000 --log --log-prefix=benchmark benchmark_db
|
|
```
|
|
|
|
**Custom Query Benchmark Script**:
|
|
```javascript
|
|
// query-benchmark.js
|
|
const { Pool } = require('pg');
|
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
|
|
async function benchmarkQuery(query, params = [], iterations = 1000) {
|
|
const times = [];
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
const start = process.hrtime.bigint();
|
|
await pool.query(query, params);
|
|
const end = process.hrtime.bigint();
|
|
|
|
times.push(Number(end - start) / 1_000_000); // Convert to ms
|
|
}
|
|
|
|
times.sort((a, b) => a - b);
|
|
|
|
return {
|
|
iterations,
|
|
min: times[0].toFixed(2),
|
|
max: times[times.length - 1].toFixed(2),
|
|
avg: (times.reduce((a, b) => a + b, 0) / times.length).toFixed(2),
|
|
p50: times[Math.floor(times.length * 0.50)].toFixed(2),
|
|
p95: times[Math.floor(times.length * 0.95)].toFixed(2),
|
|
p99: times[Math.floor(times.length * 0.99)].toFixed(2),
|
|
};
|
|
}
|
|
|
|
// Run benchmarks
|
|
async function runBenchmarks() {
|
|
console.log('Benchmarking user lookup by email...');
|
|
const userLookup = await benchmarkQuery(
|
|
'SELECT * FROM users WHERE email = $1',
|
|
['test@example.com']
|
|
);
|
|
console.log(userLookup);
|
|
|
|
console.log('\nBenchmarking posts with user join...');
|
|
const postsJoin = await benchmarkQuery(
|
|
'SELECT p.*, u.name FROM posts p JOIN users u ON p.user_id = u.id LIMIT 100'
|
|
);
|
|
console.log(postsJoin);
|
|
|
|
await pool.end();
|
|
}
|
|
|
|
runBenchmarks();
|
|
```
|
|
|
|
### 5. Integration/E2E Benchmarks
|
|
|
|
**Playwright Performance Testing**:
|
|
```javascript
|
|
// e2e-benchmark.js
|
|
const { chromium } = require('playwright');
|
|
|
|
async function benchmarkUserFlow(iterations = 10) {
|
|
const results = [];
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
const browser = await chromium.launch();
|
|
const context = await browser.newContext();
|
|
const page = await context.newPage();
|
|
|
|
const startTime = Date.now();
|
|
|
|
// User flow
|
|
await page.goto('http://localhost:3000');
|
|
await page.fill('input[name="email"]', 'user@example.com');
|
|
await page.fill('input[name="password"]', 'password123');
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForSelector('.dashboard');
|
|
await page.click('a[href="/profile"]');
|
|
await page.waitForSelector('.profile-page');
|
|
|
|
const endTime = Date.now();
|
|
results.push(endTime - startTime);
|
|
|
|
await browser.close();
|
|
}
|
|
|
|
const avg = results.reduce((a, b) => a + b, 0) / results.length;
|
|
const min = Math.min(...results);
|
|
const max = Math.max(...results);
|
|
|
|
return { avg, min, max, results };
|
|
}
|
|
|
|
benchmarkUserFlow().then(console.log);
|
|
```
|
|
|
|
### 6. Baseline Management and Comparison
|
|
|
|
**Save Baseline**:
|
|
```bash
|
|
# Save current performance as baseline
|
|
mkdir -p baselines/
|
|
|
|
# k6 results
|
|
k6 run --out json=baselines/baseline-$(date +%Y-%m-%d)-load.json loadtest.js
|
|
|
|
# Lighthouse results
|
|
lhci autorun --config=lighthouserc.json
|
|
cp -r .lighthouseci/ baselines/baseline-$(date +%Y-%m-%d)-lighthouse/
|
|
|
|
# Query benchmarks
|
|
node query-benchmark.js > baselines/baseline-$(date +%Y-%m-%d)-queries.json
|
|
```
|
|
|
|
**Compare with Baseline**:
|
|
```javascript
|
|
// compare-benchmarks.js
|
|
const fs = require('fs');
|
|
|
|
function compareBenchmarks(currentFile, baselineFile) {
|
|
const current = JSON.parse(fs.readFileSync(currentFile));
|
|
const baseline = JSON.parse(fs.readFileSync(baselineFile));
|
|
|
|
const metrics = ['p50', 'p95', 'p99', 'avg'];
|
|
const comparison = {};
|
|
|
|
metrics.forEach(metric => {
|
|
const currentValue = parseFloat(current[metric]);
|
|
const baselineValue = parseFloat(baseline[metric]);
|
|
const diff = currentValue - baselineValue;
|
|
const percentChange = (diff / baselineValue) * 100;
|
|
|
|
comparison[metric] = {
|
|
current: currentValue,
|
|
baseline: baselineValue,
|
|
diff: diff.toFixed(2),
|
|
percentChange: percentChange.toFixed(2),
|
|
regression: diff > 0,
|
|
};
|
|
});
|
|
|
|
return comparison;
|
|
}
|
|
|
|
// Usage
|
|
const comparison = compareBenchmarks(
|
|
'results/current-queries.json',
|
|
'baselines/baseline-2025-10-01-queries.json'
|
|
);
|
|
|
|
console.log('Performance Comparison:');
|
|
Object.entries(comparison).forEach(([metric, data]) => {
|
|
const emoji = data.regression ? '⚠️' : '✅';
|
|
console.log(`${emoji} ${metric}: ${data.percentChange}% change`);
|
|
});
|
|
```
|
|
|
|
### 7. Regression Detection
|
|
|
|
**Automated Regression Detection**:
|
|
```javascript
|
|
// detect-regression.js
|
|
function detectRegression(comparison, thresholds = {
|
|
p50: 10, // 10% increase is regression
|
|
p95: 15,
|
|
p99: 20,
|
|
}) {
|
|
const regressions = [];
|
|
|
|
Object.entries(comparison).forEach(([metric, data]) => {
|
|
const threshold = thresholds[metric] || 10;
|
|
|
|
if (data.percentChange > threshold) {
|
|
regressions.push({
|
|
metric,
|
|
change: data.percentChange,
|
|
threshold,
|
|
current: data.current,
|
|
baseline: data.baseline,
|
|
});
|
|
}
|
|
});
|
|
|
|
return {
|
|
hasRegression: regressions.length > 0,
|
|
regressions,
|
|
};
|
|
}
|
|
|
|
// Usage in CI/CD
|
|
const comparison = compareBenchmarks('current.json', 'baseline.json');
|
|
const regression = detectRegression(comparison);
|
|
|
|
if (regression.hasRegression) {
|
|
console.error('Performance regression detected!');
|
|
regression.regressions.forEach(r => {
|
|
console.error(`${r.metric}: ${r.change}% increase (threshold: ${r.threshold}%)`);
|
|
});
|
|
process.exit(1); // Fail CI build
|
|
}
|
|
```
|
|
|
|
### 8. Continuous Performance Monitoring
|
|
|
|
**GitHub Actions Workflow**:
|
|
```yaml
|
|
# .github/workflows/performance.yml
|
|
name: Performance Benchmarks
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [main]
|
|
schedule:
|
|
- cron: '0 0 * * *' # Daily at midnight
|
|
|
|
jobs:
|
|
benchmark:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v3
|
|
with:
|
|
node-version: '18'
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
npm ci
|
|
npm install -g k6 @lhci/cli
|
|
|
|
- name: Build application
|
|
run: npm run build
|
|
|
|
- name: Start server
|
|
run: npm start &
|
|
env:
|
|
NODE_ENV: production
|
|
|
|
- name: Wait for server
|
|
run: npx wait-on http://localhost:3000
|
|
|
|
- name: Run Lighthouse CI
|
|
run: lhci autorun --config=lighthouserc.json
|
|
|
|
- name: Run load tests
|
|
run: k6 run --out json=results-load.json loadtest.js
|
|
|
|
- name: Compare with baseline
|
|
run: node scripts/compare-benchmarks.js
|
|
|
|
- name: Upload results
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: benchmark-results
|
|
path: benchmark-results/
|
|
|
|
- name: Comment PR with results
|
|
if: github.event_name == 'pull_request'
|
|
uses: actions/github-script@v6
|
|
with:
|
|
script: |
|
|
const fs = require('fs');
|
|
const results = JSON.parse(fs.readFileSync('benchmark-results/summary.json'));
|
|
github.rest.issues.createComment({
|
|
issue_number: context.issue.number,
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
body: `## Performance Benchmark Results\n\n${results.summary}`
|
|
});
|
|
```
|
|
|
|
## Output Format
|
|
|
|
```markdown
|
|
# Performance Benchmark Report
|
|
|
|
**Benchmark Date**: [Date]
|
|
**Benchmark Type**: [load/rendering/query/integration/all]
|
|
**Baseline**: [version or "none"]
|
|
**Duration**: [test duration]
|
|
**Concurrency**: [concurrent users/connections]
|
|
|
|
## Executive Summary
|
|
|
|
[Summary of benchmark results and any regressions detected]
|
|
|
|
## Load Testing Results (k6)
|
|
|
|
### Test Configuration
|
|
- **Virtual Users**: 50 (ramped from 0 to 100)
|
|
- **Duration**: 5 minutes
|
|
- **Scenarios**: Constant load, spike test, stress test
|
|
|
|
### Results
|
|
|
|
| Metric | Value | Threshold | Status |
|
|
|--------|-------|-----------|--------|
|
|
| Total Requests | 45,230 | - | - |
|
|
| Request Rate | 150.77/s | - | - |
|
|
| Request Duration (p50) | 85ms | <200ms | ✅ Pass |
|
|
| Request Duration (p95) | 245ms | <500ms | ✅ Pass |
|
|
| Request Duration (p99) | 680ms | <1000ms | ✅ Pass |
|
|
| Failed Requests | 0.02% | <1% | ✅ Pass |
|
|
|
|
### Comparison with Baseline
|
|
|
|
| Metric | Current | Baseline (v1.2.0) | Change |
|
|
|--------|---------|-------------------|--------|
|
|
| p50 | 85ms | 120ms | -29% ✅ |
|
|
| p95 | 245ms | 450ms | -46% ✅ |
|
|
| p99 | 680ms | 980ms | -31% ✅ |
|
|
| Request Rate | 150.77/s | 85/s | +77% ✅ |
|
|
|
|
**Overall**: 46% improvement in p95 response time
|
|
|
|
## Frontend Rendering Benchmarks (Lighthouse)
|
|
|
|
### Home Page
|
|
|
|
| Metric | Score | Value | Baseline | Change |
|
|
|--------|-------|-------|----------|--------|
|
|
| Performance | 94 | - | 62 | +32 ✅ |
|
|
| FCP | - | 0.8s | 2.1s | -62% ✅ |
|
|
| LCP | - | 1.8s | 4.2s | -57% ✅ |
|
|
| TBT | - | 45ms | 280ms | -84% ✅ |
|
|
| CLS | - | 0.02 | 0.18 | -89% ✅ |
|
|
|
|
### Dashboard Page
|
|
|
|
| Metric | Score | Value | Baseline | Change |
|
|
|--------|-------|-------|----------|--------|
|
|
| Performance | 89 | - | 48 | +41 ✅ |
|
|
| LCP | - | 2.1s | 5.8s | -64% ✅ |
|
|
| TBT | - | 65ms | 420ms | -85% ✅ |
|
|
|
|
## Database Query Benchmarks
|
|
|
|
### User Lookup by Email (1000 iterations)
|
|
|
|
| Metric | Current | Baseline | Change |
|
|
|--------|---------|----------|--------|
|
|
| Min | 6ms | 380ms | -98% ✅ |
|
|
| Avg | 8ms | 450ms | -98% ✅ |
|
|
| p50 | 7ms | 445ms | -98% ✅ |
|
|
| p95 | 12ms | 520ms | -98% ✅ |
|
|
| p99 | 18ms | 680ms | -97% ✅ |
|
|
|
|
**Optimization**: Added index on users.email
|
|
|
|
### Posts with User Join (1000 iterations)
|
|
|
|
| Metric | Current | Baseline | Change |
|
|
|--------|---------|----------|--------|
|
|
| Avg | 45ms | 820ms | -95% ✅ |
|
|
| p95 | 68ms | 1200ms | -94% ✅ |
|
|
| p99 | 95ms | 2100ms | -95% ✅ |
|
|
|
|
**Optimization**: Fixed N+1 query with eager loading
|
|
|
|
## Integration/E2E Benchmarks
|
|
|
|
### User Login Flow (10 iterations)
|
|
|
|
| Metric | Value | Baseline | Change |
|
|
|--------|-------|----------|--------|
|
|
| Average | 1,245ms | 3,850ms | -68% ✅ |
|
|
| Min | 1,120ms | 3,200ms | -65% ✅ |
|
|
| Max | 1,420ms | 4,500ms | -68% ✅ |
|
|
|
|
**Flow**: Home → Login → Dashboard → Profile
|
|
|
|
## Regression Analysis
|
|
|
|
**Regressions Detected**: None
|
|
|
|
**Performance Improvements**: 12 metrics improved
|
|
- Load testing: 46% faster p95 response time
|
|
- Frontend rendering: 57% faster LCP
|
|
- Database queries: 98% faster average query time
|
|
- E2E flows: 68% faster completion time
|
|
|
|
## Recommendations
|
|
|
|
1. **Continue Monitoring**: Set up daily benchmarks to catch regressions early
|
|
2. **Performance Budget**: Establish budgets based on current metrics
|
|
- p95 response time < 300ms
|
|
- LCP < 2.5s
|
|
- Database queries < 100ms average
|
|
3. **Optimize Further**: Investigate remaining slow queries in analytics module
|
|
|
|
## Testing Instructions
|
|
|
|
### Run Load Tests
|
|
```bash
|
|
k6 run --vus 50 --duration 60s loadtest.js
|
|
```
|
|
|
|
### Run Rendering Benchmarks
|
|
```bash
|
|
lhci autorun --config=lighthouserc.json
|
|
```
|
|
|
|
### Run Query Benchmarks
|
|
```bash
|
|
node query-benchmark.js
|
|
```
|
|
|
|
### Compare with Baseline
|
|
```bash
|
|
node scripts/compare-benchmarks.js results/current.json baselines/baseline-2025-10-01.json
|
|
```
|