Nu je de basisprincipes van k6 en performance testing begrijpt, is het tijd om die kennis in de praktijk te brengen. Dit artikel voert je door het schrijven van echte tests, het integreren ervan in CI/CD pijpleidingen, en het gebruik van profiling tools om prestatieknelpunten te identificeren en op te lossen.
Dit is Deel 2 van een tweedelige serie belastingstests met k6:
Als je deel 1 nog niet gelezen hebt, begin daar dan om k6 basisprincipes, installatie en testtypen te begrijpen voordat je in implementatie gaat duiken.
Voor het uitvoeren van k6 tests, laten we instellen MinimalBlog voor het testen:
Het demo-project gebruiken:
cd Mostlylucid.MinimalBlog.Demo
dotnet run
Of het gebruik van Docker:
docker run -d -p 5000:8080 \
-v $(pwd)/Markdown:/app/Markdown \
--name minimalblog-test \
scottgal/minimalblog:latest
Voor het nauwkeurig testen van de prestaties, altijd in Release-modus inbouwen:
dotnet run --configuration Release
Debug vs Release-modusverschillen:
Debug-modus Debug-modus Debug-modus Release-modus
|--------|------------|--------------|
| Optimalisaties Gehandicapten Volledige JIT optimalisaties ingeschakeld
| Inlijning Minimale Agressieve methode inlijning
| Dode code Geholpen Geëlimineerd
| Debugsymbolen Full PDB, extra metadata Minimal or no
| Asserties | Debug.Assert() Actief Gecompileerd uit
| Controle van de grenzen Extra veiligheidscontroles Geoptimaliseerd waar het veilig is
| Typische overhead 2-10x tragere prestaties
Waarom debug-modus misleidende resultaten geeft:
Debug.Assert() verklaringen uitvoeren, het toevoegen van controles die productie niet zal hebbenWanneer Debug-modus voor het testen moet worden gebruikt:
Aanbevolen workflow:
# 1. Develop and debug with Debug mode
dotnet run # Debug mode (default)
# 2. Validate functionality works
k6 run --vus 1 --duration 10s smoke-test.js
# 3. Switch to Release for actual performance testing
dotnet run -c Release
# 4. Run full load tests
k6 run load-test.js
Vuistregel: Als je prestaties meet, gebruik Release. Als je bugs repareert, gebruik Debug.
Creëer verschillende markdown-posten om te testen met:
# Create test posts directory
mkdir -p TestMarkdown
# Create sample posts
for i in {1..10}; do
cat > TestMarkdown/test-post-$i.md <<EOF
# Test Post $i
This is test post number $i with some **bold** text and *italic* text.
## Sample Content
- Item 1
- Item 2
- Item 3
\`\`\`csharp
public class Test {
public int Value { get; set; }
}
\`\`\`
EOF
done
mkdir k6-tests
cd k6-tests
Nu zijn we klaar om onze testen te schrijven!
Laten we beginnen met een eenvoudige rooktest om de basisfunctionaliteit te controleren:
Bestand: smoke-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 1,
duration: '30s',
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests should be below 500ms
http_req_failed: ['rate<0.01'], // Less than 1% errors
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';
export default function() {
// Test homepage
let response = http.get(`${BASE_URL}/`);
check(response, {
'homepage status is 200': (r) => r.status === 200,
'homepage has content': (r) => r.body.length > 0,
'homepage response time OK': (r) => r.timings.duration < 500,
});
sleep(1);
// Test a single post
response = http.get(`${BASE_URL}/post/test-post-1`);
check(response, {
'post status is 200': (r) => r.status === 200,
'post has content': (r) => r.body.length > 0,
'post contains title': (r) => r.body.includes('Test Post 1'),
});
sleep(1);
// Test categories page
response = http.get(`${BASE_URL}/categories`);
check(response, {
'categories status is 200': (r) => r.status === 200,
'categories has content': (r) => r.body.length > 0,
});
sleep(1);
// Test category filter
response = http.get(`${BASE_URL}/category/Testing`);
check(response, {
'category filter status is 200': (r) => r.status === 200,
'category has posts': (r) => r.body.includes('Test Post'),
});
}
Voer de test uit:
k6 run smoke-test.js
Verwachte output:
✓ homepage status is 200
✓ homepage has content
✓ homepage response time OK
✓ post status is 200
✓ post has content
✓ post contains title
✓ categories status is 200
✓ categories has content
✓ category filter status is 200
✓ category has posts
checks.........................: 100.00% ✓ 300 ✗ 0
data_received..................: 1.2 MB 40 kB/s
data_sent......................: 15 kB 500 B/s
http_req_duration..............: avg=45ms min=12ms med=38ms max=156ms p(95)=98ms
http_reqs......................: 120 4/s
MinimaleBlog's prestaties zijn sterk afhankelijk van caching. Laten we controleren of het werkt:
Bestand: cache-test.js
import http from 'k6/http';
import { check, group } from 'k6';
export const options = {
vus: 1,
iterations: 10,
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';
export default function() {
group('Memory Cache Test - First Request', () => {
const start = Date.now();
const response = http.get(`${BASE_URL}/post/test-post-1`);
const duration = Date.now() - start;
check(response, {
'first request successful': (r) => r.status === 200,
});
console.log(`First request: ${duration}ms`);
});
group('Memory Cache Test - Cached Request', () => {
const start = Date.now();
const response = http.get(`${BASE_URL}/post/test-post-1`);
const duration = Date.now() - start;
check(response, {
'cached request successful': (r) => r.status === 200,
'cached request is faster': (r) => r.timings.duration < 100,
});
console.log(`Cached request: ${duration}ms`);
});
group('Output Cache Headers', () => {
const response = http.get(`${BASE_URL}/post/test-post-1`);
check(response, {
'has cache headers': (r) => r.headers['Cache-Control'] !== undefined,
'output cache working': (r) => {
const age = r.headers['Age'];
return age !== undefined && parseInt(age) >= 0;
},
});
console.log(`Cache-Control: ${response.headers['Cache-Control']}`);
console.log(`Age: ${response.headers['Age']}`);
});
}
Voer de test uit:
k6 run cache-test.js
Deze test zal u tonen:
Laten we nu testen onder realistische belasting:
Bestand: load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
export const options = {
stages: [
{ duration: '2m', target: 10 }, // Ramp up to 10 users
{ duration: '5m', target: 10 }, // Stay at 10 users
{ duration: '2m', target: 0 }, // Ramp down to 0
],
thresholds: {
http_req_duration: ['p(95)<300'], // 95% under 300ms
http_req_failed: ['rate<0.01'], // Less than 1% errors
errors: ['rate<0.1'], // Less than 10% errors
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';
const scenarios = [
{ weight: 50, path: '/' }, // 50% homepage
{ weight: 30, path: '/post/test-post-1' }, // 30% specific post
{ weight: 10, path: '/categories' }, // 10% categories
{ weight: 10, path: '/category/Testing' }, // 10% category filter
];
function weightedChoice(scenarios) {
const total = scenarios.reduce((sum, s) => sum + s.weight, 0);
let random = Math.random() * total;
for (const scenario of scenarios) {
random -= scenario.weight;
if (random <= 0) return scenario;
}
return scenarios[0];
}
export default function() {
const scenario = weightedChoice(scenarios);
const response = http.get(`${BASE_URL}${scenario.path}`);
const success = check(response, {
'status is 200': (r) => r.status === 200,
'response time OK': (r) => r.timings.duration < 500,
'has content': (r) => r.body.length > 0,
});
errorRate.add(!success);
sleep(Math.random() * 2 + 1); // Random sleep 1-3 seconds
}
Voer de test uit:
k6 run load-test.js
Vertolkingsresultaten:
scenarios: (100.00%) 1 scenario, 10 max VUs, 9m30s max duration
default: 2m00s ramp-up, 5m00s plateau, 2m00s ramp-down
✓ status is 200
✓ response time OK
✓ has content
checks.........................: 100.00% ✓ 3450 ✗ 0
data_received..................: 12 MB 22 kB/s
data_sent......................: 132 kB 244 B/s
errors.........................: 0.00% ✓ 0 ✗ 1150
http_req_duration..............: avg=42ms min=8ms med=35ms max=245ms p(95)=86ms p(99)=156ms
http_reqs......................: 1150 2.12/s
iteration_duration.............: avg=2.1s min=1.0s med=2.0s max=3.4s
vus............................: 10 min=0 max=10
Sleutelgegevens om te bekijken:
Laten we het breekpunt van MinimalBlog zoeken:
Bestand: stress-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 10 }, // Normal load
{ duration: '2m', target: 50 }, // Increase to 50
{ duration: '2m', target: 100 }, // Stress at 100
{ duration: '2m', target: 200 }, // High stress at 200
{ duration: '3m', target: 0 }, // Recovery
],
thresholds: {
http_req_duration: ['p(99)<3000'], // 99% under 3s even under stress
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';
export default function() {
const responses = http.batch([
['GET', `${BASE_URL}/`],
['GET', `${BASE_URL}/post/test-post-1`],
['GET', `${BASE_URL}/post/test-post-2`],
['GET', `${BASE_URL}/categories`],
]);
responses.forEach((response, index) => {
check(response, {
[`request ${index} status is 200`]: (r) => r.status === 200,
});
});
sleep(0.5);
}
export function handleSummary(data) {
return {
'stress-test-summary.json': JSON.stringify(data),
stdout: textSummary(data, { indent: ' ', enableColors: true }),
};
}
Voer de test uit:
k6 run stress-test.js
Let op:
Test plotselinge verkeerspieken (zoals worden gekenmerkt op Reddit of Hacker News):
Bestand: spike-test.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 10 }, // Normal traffic
{ duration: '30s', target: 200 }, // Sudden spike!
{ duration: '3m', target: 200 }, // Sustained spike
{ duration: '1m', target: 10 }, // Back to normal
{ duration: '1m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<1000'], // Allow higher latency during spike
http_req_failed: ['rate<0.05'], // Allow 5% errors during spike
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';
export default function() {
const response = http.get(`${BASE_URL}/`);
check(response, {
'status is 200 or 503': (r) => r.status === 200 || r.status === 503,
});
}
Voer de test uit:
k6 run spike-test.js
Bij deze test wordt het volgende gecontroleerd:
Long-running test om geheugenlekken of uitputting van hulpbronnen te detecteren:
Bestand: soak-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 20 }, // Ramp up
{ duration: '3h', target: 20 }, // Stay at 20 for 3 hours
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';
export default function() {
const response = http.get(`${BASE_URL}/post/test-post-${Math.floor(Math.random() * 10) + 1}`);
check(response, {
'status is 200': (r) => r.status === 200,
'response time stable': (r) => r.timings.duration < 500,
});
sleep(2);
}
Voer de test uit:
k6 run soak-test.js
Belangrijk: System resources monitoren tijdens weekproeven:
# In another terminal
watch -n 5 'dotnet-counters ps | grep -i minimal'
# Or use top/htop
htop -p $(pgrep -f MinimalBlog)
Let op:
Simuleer het werkelijke gebruikersgedrag:
Bestand: user-journey-test.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js';
export const options = {
vus: 10,
duration: '5m',
thresholds: {
'group_duration{group:::01_homepage}': ['avg<500'],
'group_duration{group:::02_browse_posts}': ['avg<500'],
'group_duration{group:::03_categories}': ['avg<500'],
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';
export default function() {
// Simulate a real user journey
group('01_homepage', () => {
const response = http.get(`${BASE_URL}/`);
check(response, {
'homepage loaded': (r) => r.status === 200,
'homepage has posts': (r) => r.body.includes('Test Post'),
});
sleep(Math.random() * 3 + 2); // Read homepage 2-5 seconds
});
group('02_browse_posts', () => {
// User clicks on a post
let response = http.get(`${BASE_URL}/post/test-post-1`);
check(response, {
'post loaded': (r) => r.status === 200,
'post has content': (r) => r.body.length > 500,
});
sleep(Math.random() * 20 + 10); // Read post 10-30 seconds
// User clicks another post
response = http.get(`${BASE_URL}/post/test-post-2`);
check(response, {
'second post loaded': (r) => r.status === 200,
});
sleep(Math.random() * 15 + 5); // Read second post 5-20 seconds
});
group('03_categories', () => {
// User explores categories
const response = http.get(`${BASE_URL}/categories`);
check(response, {
'categories loaded': (r) => r.status === 200,
});
sleep(2);
// User clicks a category
const categoryResponse = http.get(`${BASE_URL}/category/Testing`);
check(categoryResponse, {
'category posts loaded': (r) => r.status === 200,
'category has posts': (r) => r.body.includes('Test Post'),
});
sleep(Math.random() * 10 + 5); // Browse category 5-15 seconds
});
}
export function handleSummary(data) {
return {
'user-journey-report.html': htmlReport(data),
};
}
Voer de test uit:
k6 run user-journey-test.js
Dit levert een HTML-rapport op: user-journey-report.html
Als MetaWeblog ingeschakeld is, test het dan ook:
Bestand: metaweblog-test.js
import http from 'k6/http';
import { check } from 'k6';
import encoding from 'k6/encoding';
export const options = {
vus: 1,
iterations: 10,
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';
const USERNAME = __ENV.USERNAME || 'admin';
const PASSWORD = __ENV.PASSWORD || 'changeme';
function createXmlRpcRequest(methodName, params) {
return `<?xml version="1.0"?>
<methodCall>
<methodName>${methodName}</methodName>
<params>
${params}
</params>
</methodCall>`;
}
export default function() {
// Test getRecentPosts
const recentPostsXml = createXmlRpcRequest('blogger.getRecentPosts', `
<param><value><string>0</string></value></param>
<param><value><string>${USERNAME}</string></value></param>
<param><value><string>${PASSWORD}</string></value></param>
<param><value><int>10</int></value></param>
`);
const response = http.post(`${BASE_URL}/metaweblog`, recentPostsXml, {
headers: { 'Content-Type': 'text/xml' },
});
check(response, {
'MetaWeblog API responds': (r) => r.status === 200,
'MetaWeblog returns XML': (r) => r.body.includes('<?xml'),
'MetaWeblog has methodResponse': (r) => r.body.includes('methodResponse'),
});
}
Voer de test uit:
k6 run -e USERNAME=admin -e PASSWORD=yourpassword metaweblog-test.js
Het meest krachtige gebruik van k6 in CI/CD is als a kwaliteit poort - bouwt automatisch uit als de prestaties verslechteren. Hier vindt u hoe u uitgebreide k6 testen in GitHub Acties kunt implementeren.
Laten we beginnen met een basis workflow die draait op elke pull verzoek en blokkeert het samenvoegen als tests falen.
Aanmaken .github/workflows/k6-pr-check.yml:
name: k6 Performance Check
on:
pull_request:
branches: [ main ]
jobs:
performance-check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Setup k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
--keyserver hkp://keyserver.ubuntu.com:80 \
--recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | \
sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: Create test data
run: |
mkdir -p TestMarkdown
for i in {1..10}; do
cat > TestMarkdown/test-post-$i.md <<EOF
# Test Post $i
This is test post $i with **bold** and *italic* text.
\`\`\`csharp
public class Test { public int Value { get; set; } }
\`\`\`
EOF
done
- name: Build MinimalBlog
run: |
cd Mostlylucid.MinimalBlog.Demo
dotnet build --configuration Release
- name: Start MinimalBlog
run: |
cd Mostlylucid.MinimalBlog.Demo
dotnet run --configuration Release &
echo $! > app.pid
# Wait for app to be ready
timeout 60 bash -c 'until curl -sf http://localhost:5000 > /dev/null; do
echo "Waiting for app..."
sleep 2
done'
echo "- App is ready!"
- name: Run Smoke Test
id: smoke-test
run: |
k6 run --out json=smoke-results.json k6-tests/smoke-test.js
echo "smoke-test-passed=true" >> $GITHUB_OUTPUT
- name: Run Load Test
id: load-test
run: |
k6 run --out json=load-results.json k6-tests/load-test.js
echo "load-test-passed=true" >> $GITHUB_OUTPUT
- name: Run Cache Test
id: cache-test
run: |
k6 run --out json=cache-results.json k6-tests/cache-test.js
echo "cache-test-passed=true" >> $GITHUB_OUTPUT
- name: Stop MinimalBlog
if: always()
run: |
if [ -f Mostlylucid.MinimalBlog.Demo/app.pid ]; then
kill $(cat Mostlylucid.MinimalBlog.Demo/app.pid) || true
fi
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: k6-test-results
path: |
*-results.json
*.html
retention-days: 30
- name: Check Test Results
if: always()
run: |
echo "## k6 Performance Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.smoke-test.outputs.smoke-test-passed }}" == "true" ]; then
echo "- Smoke Test: PASSED" >> $GITHUB_STEP_SUMMARY
else
echo "- Smoke Test: FAILED" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ steps.load-test.outputs.load-test-passed }}" == "true" ]; then
echo "- Load Test: PASSED" >> $GITHUB_STEP_SUMMARY
else
echo "- Load Test: FAILED" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ steps.cache-test.outputs.cache-test-passed }}" == "true" ]; then
echo "- Cache Test: PASSED" >> $GITHUB_STEP_SUMMARY
else
echo "- Cache Test: FAILED" >> $GITHUB_STEP_SUMMARY
fi
Belangrijkste kenmerken:
sequenceDiagram
participant PR as Pull Request
participant GHA as GitHub Actions
participant App as MinimalBlog
participant k6 as k6 Tests
PR->>GHA: Trigger workflow
GHA->>GHA: Setup .NET & k6
GHA->>GHA: Create test data
GHA->>App: Build & Start
App-->>GHA: App ready
GHA->>k6: Run smoke test
k6-->>GHA: Results
GHA->>k6: Run load test
k6-->>GHA: Results
GHA->>k6: Run cache test
k6-->>GHA: Results
GHA->>App: Stop
alt Tests Pass
GHA->>PR: Mark as success
else Tests Fail
GHA->>PR: Block merge
end
Aanmaken .github/workflows/k6-pr-comment.yml om resultaten te plaatsen als PR-commentaar:
name: k6 Performance with PR Comment
on:
pull_request:
branches: [ main ]
permissions:
pull-requests: write
contents: read
jobs:
performance-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Setup k6
uses: grafana/setup-k6-action@v1
- name: Create test data
run: |
mkdir -p TestMarkdown
for i in {1..10}; do
cat > TestMarkdown/test-post-$i.md <<EOF
# Test Post $i
Test content for post $i.
EOF
done
- name: Build and Start App
run: |
cd Mostlylucid.MinimalBlog.Demo
dotnet build -c Release
dotnet run -c Release > app.log 2>&1 &
APP_PID=$!
echo $APP_PID > app.pid
timeout 60 bash -c 'until curl -sf http://localhost:5000; do sleep 2; done'
- name: Run k6 Tests
id: k6-test
run: |
# Run tests and capture output
k6 run --out json=results.json k6-tests/load-test.js > k6-output.txt 2>&1 || true
# Parse results
if grep -q "✓" k6-output.txt; then
echo "tests-passed=true" >> $GITHUB_OUTPUT
else
echo "tests-passed=false" >> $GITHUB_OUTPUT
fi
# Extract key metrics
P95=$(grep "http_req_duration" k6-output.txt | grep -oP 'p\(95\)=\K[0-9.]+' || echo "N/A")
RPS=$(grep "http_reqs" k6-output.txt | grep -oP '\d+\.\d+/s' || echo "N/A")
ERRORS=$(grep "http_req_failed" k6-output.txt | grep -oP '\d+\.\d+%' || echo "0%")
echo "p95=${P95}" >> $GITHUB_OUTPUT
echo "rps=${RPS}" >> $GITHUB_OUTPUT
echo "errors=${ERRORS}" >> $GITHUB_OUTPUT
- name: Comment PR
uses: actions/github-script@v7
if: always()
with:
script: |
const fs = require('fs');
const output = fs.readFileSync('k6-output.txt', 'utf8');
const testsPassed = '${{ steps.k6-test.outputs.tests-passed }}' === 'true';
const icon = testsPassed ? 'PASS' : 'FAIL';
const status = testsPassed ? 'PASSED' : 'FAILED';
const comment = `## ${icon} k6 Performance Test Results
**Status:** ${status}
### Key Metrics
| Metric | Value |
|--------|-------|
| P95 Response Time | ${{ steps.k6-test.outputs.p95 }}ms |
| Requests/sec | ${{ steps.k6-test.outputs.rps }} |
| Error Rate | ${{ steps.k6-test.outputs.errors }} |
### Thresholds
- P95 < 300ms
- Error rate < 1%
<details>
<summary>Full k6 Output</summary>
\`\`\`
${output}
\`\`\`
</details>
[View full test results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Stop App
if: always()
run: |
[ -f Mostlylucid.MinimalBlog.Demo/app.pid ] && \
kill $(cat Mostlylucid.MinimalBlog.Demo/app.pid) || true
- name: Upload Artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: k6-results
path: |
results.json
k6-output.txt
Mostlylucid.MinimalBlog.Demo/app.log
Deze workflow:
Aanmaken .github/workflows/k6-baseline.yml voor het detecteren van prestatieregressies:
name: Performance Regression Check
on:
pull_request:
branches: [ main ]
jobs:
regression-check:
runs-on: ubuntu-latest
steps:
- name: Checkout PR code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Setup k6
uses: grafana/setup-k6-action@v1
- name: Create test data
run: |
mkdir -p TestMarkdown
for i in {1..20}; do
echo "# Test Post $i" > TestMarkdown/test-$i.md
echo "" >> TestMarkdown/test-$i.md
echo '' >> TestMarkdown/test-$i.md
echo "Test content $i" >> TestMarkdown/test-$i.md
done
- name: Test PR Branch
run: |
cd Mostlylucid.MinimalBlog.Demo
dotnet build -c Release
dotnet run -c Release &
APP_PID=$!
timeout 60 bash -c 'until curl -sf http://localhost:5000; do sleep 2; done'
# Run test and save results
k6 run --summary-export=pr-results.json k6-tests/load-test.js
kill $APP_PID
sleep 5
- name: Checkout main branch
uses: actions/checkout@v4
with:
ref: main
path: baseline
- name: Test Baseline (main branch)
run: |
cd baseline/Mostlylucid.MinimalBlog.Demo
dotnet build -c Release
dotnet run -c Release &
APP_PID=$!
timeout 60 bash -c 'until curl -sf http://localhost:5000; do sleep 2; done'
# Run test and save results
k6 run --summary-export=baseline-results.json ../k6-tests/load-test.js
kill $APP_PID
- name: Compare Results
run: |
# Extract P95 from both runs
PR_P95=$(jq '.metrics.http_req_duration.values["p(95)"]' pr-results.json)
BASE_P95=$(jq '.metrics.http_req_duration.values["p(95)"]' baseline/baseline-results.json)
# Calculate percentage change
CHANGE=$(echo "scale=2; (($PR_P95 - $BASE_P95) / $BASE_P95) * 100" | bc)
echo "## Performance Comparison" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Branch | P95 Response Time |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------------------|" >> $GITHUB_STEP_SUMMARY
echo "| main (baseline) | ${BASE_P95}ms |" >> $GITHUB_STEP_SUMMARY
echo "| PR | ${PR_P95}ms |" >> $GITHUB_STEP_SUMMARY
echo "| Change | ${CHANGE}% |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Fail if performance degrades by more than 20%
if (( $(echo "$CHANGE > 20" | bc -l) )); then
echo "- Performance degraded by ${CHANGE}%" >> $GITHUB_STEP_SUMMARY
echo "::error::Performance regression detected: ${CHANGE}% slower than baseline"
exit 1
else
echo "- Performance acceptable" >> $GITHUB_STEP_SUMMARY
fi
Deze workflow:
graph TD
A[PR Submitted] --> B[Checkout PR Code]
B --> C[Test PR Branch]
C --> D[Checkout main Branch]
D --> E[Test Baseline]
E --> F{Calculate<br/>Difference}
F -->|>20% Slower| G[FAIL Build]
F -->|<20% Change| H[PASS Build]
G --> I[Block Merge]
H --> J[Allow Merge]
style G stroke:#ff6b6b
style H stroke:#51cf66
Start snel, Gate Early
Passende drempels gebruiken
javascript // Too strict for CI thresholds: { http_req_duration: ['p(95)<100'] }
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.