specweave 1.0.239 → 1.0.241
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +31 -30
- package/README.md +1 -1
- package/bin/specweave.js +16 -0
- package/dist/plugins/specweave-ado/lib/ado-permission-gate.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-permission-gate.js +17 -2
- package/dist/plugins/specweave-ado/lib/ado-permission-gate.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +7 -0
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.js +53 -0
- package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-permission-gate.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-permission-gate.js +17 -2
- package/dist/plugins/specweave-jira/lib/jira-permission-gate.js.map +1 -1
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js +7 -3
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js.map +1 -1
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts.map +1 -1
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js +27 -19
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js.map +1 -1
- package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts +8 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts.map +1 -1
- package/dist/plugins/specweave-testing/lib/playwright-routing.js +10 -7
- package/dist/plugins/specweave-testing/lib/playwright-routing.js.map +1 -1
- package/dist/src/adapters/agents-md-generator.js +1 -1
- package/dist/src/adapters/agents-md-generator.js.map +1 -1
- package/dist/src/adapters/claude/README.md +1 -1
- package/dist/src/adapters/claude-md-generator.js +1 -1
- package/dist/src/adapters/claude-md-generator.js.map +1 -1
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +10 -1
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/refresh-marketplace.d.ts.map +1 -1
- package/dist/src/cli/commands/refresh-marketplace.js +7 -67
- package/dist/src/cli/commands/refresh-marketplace.js.map +1 -1
- package/dist/src/cli/commands/team.d.ts +20 -0
- package/dist/src/cli/commands/team.d.ts.map +1 -0
- package/dist/src/cli/commands/team.js +101 -0
- package/dist/src/cli/commands/team.js.map +1 -0
- package/dist/src/cli/helpers/init/claude-settings-env.d.ts +16 -0
- package/dist/src/cli/helpers/init/claude-settings-env.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/claude-settings-env.js +44 -0
- package/dist/src/cli/helpers/init/claude-settings-env.js.map +1 -0
- package/dist/src/cli/helpers/init/plugin-installer.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/plugin-installer.js +9 -13
- package/dist/src/cli/helpers/init/plugin-installer.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.js +12 -6
- package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/types.d.ts +2 -0
- package/dist/src/cli/helpers/issue-tracker/types.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/types.js.map +1 -1
- package/dist/src/core/increment/discipline-checker.js +1 -1
- package/dist/src/core/increment/discipline-checker.js.map +1 -1
- package/dist/src/core/increment/status-commands.d.ts.map +1 -1
- package/dist/src/core/increment/status-commands.js +7 -0
- package/dist/src/core/increment/status-commands.js.map +1 -1
- package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts +2 -2
- package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts.map +1 -1
- package/dist/src/core/lazy-loading/llm-plugin-detector.js +63 -25
- package/dist/src/core/lazy-loading/llm-plugin-detector.js.map +1 -1
- package/dist/src/core/reflection/reflect-handler.js +2 -2
- package/dist/src/core/reflection/reflect-handler.js.map +1 -1
- package/dist/src/core/session/handoff-context.js +2 -2
- package/dist/src/core/session/handoff-context.js.map +1 -1
- package/dist/src/sync/ado-reconciler.d.ts.map +1 -1
- package/dist/src/sync/ado-reconciler.js +21 -2
- package/dist/src/sync/ado-reconciler.js.map +1 -1
- package/dist/src/sync/engine.d.ts.map +1 -1
- package/dist/src/sync/engine.js +2 -0
- package/dist/src/sync/engine.js.map +1 -1
- package/dist/src/sync/github-reconciler.d.ts.map +1 -1
- package/dist/src/sync/github-reconciler.js +52 -26
- package/dist/src/sync/github-reconciler.js.map +1 -1
- package/dist/src/sync/jira-reconciler.d.ts.map +1 -1
- package/dist/src/sync/jira-reconciler.js +16 -3
- package/dist/src/sync/jira-reconciler.js.map +1 -1
- package/dist/src/sync/providers/ado.d.ts.map +1 -1
- package/dist/src/sync/providers/ado.js +4 -2
- package/dist/src/sync/providers/ado.js.map +1 -1
- package/dist/src/sync/providers/github.d.ts.map +1 -1
- package/dist/src/sync/providers/github.js +11 -0
- package/dist/src/sync/providers/github.js.map +1 -1
- package/dist/src/sync/providers/jira.d.ts.map +1 -1
- package/dist/src/sync/providers/jira.js +14 -2
- package/dist/src/sync/providers/jira.js.map +1 -1
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
- package/dist/src/sync/sync-coordinator.js +31 -6
- package/dist/src/sync/sync-coordinator.js.map +1 -1
- package/dist/src/utils/auto-install.js +4 -4
- package/dist/src/utils/auto-install.js.map +1 -1
- package/package.json +2 -2
- package/plugins/FINAL-AUDIT-RECOMMENDATIONS.md +3 -3
- package/plugins/SKILLS-VS-AGENTS.md +1 -1
- package/plugins/specweave/PLUGIN.md +0 -2
- package/plugins/specweave/commands/export-skills.md +1 -1
- package/plugins/specweave/commands/role-orchestrator.md +1 -1
- package/plugins/specweave/hooks/log-decision.sh +6 -0
- package/plugins/specweave/hooks/stop-auto-v5.sh +17 -1
- package/plugins/specweave/hooks/stop-reflect.sh +16 -2
- package/plugins/specweave/hooks/stop-sync.sh +17 -9
- package/plugins/specweave/hooks/user-prompt-submit.sh +119 -35
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js +52 -26
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
- package/plugins/specweave/scripts/read-grill-context.sh +149 -0
- package/plugins/specweave/skills/code-review/SKILL.md +608 -0
- package/plugins/specweave/skills/done/SKILL.md +1 -1
- package/plugins/specweave/skills/grill/SKILL.md +91 -0
- package/plugins/specweave/skills/performance/SKILL.md +6 -0
- package/plugins/specweave/skills/security/SKILL.md +7 -0
- package/plugins/specweave/skills/security-patterns/SKILL.md +6 -0
- package/plugins/specweave/skills/tdd-orchestrator/SKILL.md +1 -1
- package/plugins/specweave/skills/team-build/SKILL.md +1 -1
- package/plugins/specweave/skills/team-orchestrate/SKILL.md +1 -1
- package/plugins/specweave/skills/tech-lead/SKILL.md +7 -0
- package/plugins/specweave-ado/lib/ado-permission-gate.js +18 -2
- package/plugins/specweave-ado/lib/ado-permission-gate.ts +19 -2
- package/plugins/specweave-frontend/skills/frontend/SKILL.md +138 -2
- package/plugins/specweave-frontend/skills/i18n-expert/SKILL.md +989 -0
- package/plugins/specweave-github/hooks/github-auto-create-handler.sh +23 -1
- package/plugins/specweave-github/lib/github-feature-sync.js +41 -0
- package/plugins/specweave-github/lib/github-feature-sync.ts +62 -0
- package/plugins/specweave-infrastructure/PLUGIN.md +2 -1
- package/plugins/specweave-infrastructure/skills/gcp-deep-dive/SKILL.md +1172 -0
- package/plugins/specweave-infrastructure/skills/observability/SKILL.md +6 -0
- package/plugins/specweave-infrastructure/skills/opentelemetry/SKILL.md +6 -0
- package/plugins/specweave-jira/lib/jira-permission-gate.js +18 -2
- package/plugins/specweave-jira/lib/jira-permission-gate.ts +19 -2
- package/plugins/specweave-mobile/PLUGIN.md +1 -2
- package/plugins/specweave-mobile/README.md +13 -12
- package/plugins/specweave-mobile/skills/capacitor-ionic/SKILL.md +4 -18
- package/plugins/specweave-mobile/skills/deep-linking-push/SKILL.md +4 -22
- package/plugins/specweave-mobile/skills/expo/SKILL.md +4 -24
- package/plugins/specweave-mobile/skills/mobile-testing/SKILL.md +4 -22
- package/plugins/specweave-mobile/skills/react-native-expert/SKILL.md +404 -47
- package/plugins/specweave-testing/PLUGIN.md +3 -11
- package/plugins/specweave-testing/lib/playwright-cli-detector.js +8 -3
- package/plugins/specweave-testing/lib/playwright-cli-detector.ts +8 -3
- package/plugins/specweave-testing/lib/playwright-cli-runner.js +25 -20
- package/plugins/specweave-testing/lib/playwright-cli-runner.ts +24 -19
- package/plugins/specweave-testing/lib/playwright-routing.js +1 -6
- package/plugins/specweave-testing/lib/playwright-routing.ts +11 -8
- package/plugins/specweave-testing/skills/accessibility-testing/SKILL.md +998 -0
- package/plugins/specweave-testing/skills/e2e-testing/SKILL.md +29 -28
- package/plugins/specweave-testing/skills/mutation-testing/SKILL.md +769 -0
- package/plugins/specweave-testing/skills/performance-testing/SKILL.md +961 -0
- package/plugins/specweave-testing/skills/qa-engineer/SKILL.md +2 -0
- package/plugins/specweave/.specweave/logs/decisions.jsonl +0 -12
- package/plugins/specweave/.specweave/logs/reflect/reflect.log +0 -8
- package/plugins/specweave/.specweave/logs/stop-auto.log +0 -6
- package/plugins/specweave/.specweave/logs/stop-sync.log +0 -10
- package/plugins/specweave/.specweave/state/dashboard.json +0 -43
- package/plugins/specweave/skills/infrastructure/SKILL.md +0 -86
- package/plugins/specweave/skills/qa-lead/SKILL.md +0 -77
- package/plugins/specweave-mobile/skills/mobile-architect/SKILL.md +0 -30
- package/plugins/specweave-testing/commands/e2e-setup.md +0 -1103
- package/plugins/specweave-testing/commands/test-coverage.md +0 -983
- package/plugins/specweave-testing/commands/test-generate.md +0 -1160
- package/plugins/specweave-testing/commands/test-init.md +0 -413
- package/plugins/specweave-testing/commands/ui-automate.md +0 -182
- package/plugins/specweave-testing/commands/ui-inspect.md +0 -82
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Performance testing expert for load testing (k6, Artillery), web performance (Lighthouse CI, Core Web Vitals), database performance, and memory/resource profiling. Use for performance tests, load tests, stress tests, lighthouse audits, k6 scripts, web vitals monitoring, or performance budgets.
|
|
3
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
4
|
+
model: opus
|
|
5
|
+
context: fork
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Performance Testing Expert
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
Invoke this skill when the task involves:
|
|
13
|
+
- **Load testing**: "performance test", "load test", "stress test", "soak test", "spike test"
|
|
14
|
+
- **Web performance**: "lighthouse", "performance budget", "bundle size", "core web vitals"
|
|
15
|
+
- **Specific tools**: "k6", "artillery", "lighthouse ci", "webpagetest"
|
|
16
|
+
- **Metrics**: "web vitals", "LCP", "FID", "INP", "CLS", "TTFB", "p95 latency"
|
|
17
|
+
- **Database performance**: "slow query", "n+1 query", "connection pool"
|
|
18
|
+
- **Resource profiling**: "memory leak", "cpu profile", "network throttle"
|
|
19
|
+
|
|
20
|
+
## Core Expertise
|
|
21
|
+
|
|
22
|
+
- **Load testing** with k6 (primary) and Artillery
|
|
23
|
+
- **Web performance** with Lighthouse CI and performance budgets
|
|
24
|
+
- **Core Web Vitals** monitoring and optimization
|
|
25
|
+
- **Database performance** testing and query analysis
|
|
26
|
+
- **Memory and resource** leak detection and profiling
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. Load Testing with k6
|
|
31
|
+
|
|
32
|
+
k6 is the primary tool for HTTP load testing. It uses JavaScript ES6 modules, runs in Go for high performance, and integrates with CI/CD pipelines.
|
|
33
|
+
|
|
34
|
+
### Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# macOS
|
|
38
|
+
brew install k6
|
|
39
|
+
|
|
40
|
+
# Docker
|
|
41
|
+
docker run --rm -i grafana/k6 run - <script.js
|
|
42
|
+
|
|
43
|
+
# npm (for CI)
|
|
44
|
+
npm install -g k6
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Basic k6 Script
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
// load-tests/api-load.js
|
|
51
|
+
import http from 'k6/http';
|
|
52
|
+
import { check, sleep } from 'k6';
|
|
53
|
+
|
|
54
|
+
export const options = {
|
|
55
|
+
stages: [
|
|
56
|
+
{ duration: '1m', target: 20 }, // ramp up to 20 VUs
|
|
57
|
+
{ duration: '3m', target: 20 }, // hold at 20 VUs
|
|
58
|
+
{ duration: '1m', target: 0 }, // ramp down
|
|
59
|
+
],
|
|
60
|
+
thresholds: {
|
|
61
|
+
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
|
|
62
|
+
http_req_failed: ['rate<0.01'], // error rate below 1%
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default function () {
|
|
67
|
+
const res = http.get('https://api.example.com/users');
|
|
68
|
+
|
|
69
|
+
check(res, {
|
|
70
|
+
'status is 200': (r) => r.status === 200,
|
|
71
|
+
'response time < 500ms': (r) => r.timings.duration < 500,
|
|
72
|
+
'body has users': (r) => JSON.parse(r.body).length > 0,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
sleep(1);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### k6 Scenario Patterns
|
|
80
|
+
|
|
81
|
+
#### Stress Test (Find Breaking Point)
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
export const options = {
|
|
85
|
+
scenarios: {
|
|
86
|
+
stress: {
|
|
87
|
+
executor: 'ramping-vus',
|
|
88
|
+
startVUs: 0,
|
|
89
|
+
stages: [
|
|
90
|
+
{ duration: '2m', target: 100 },
|
|
91
|
+
{ duration: '5m', target: 100 },
|
|
92
|
+
{ duration: '2m', target: 200 },
|
|
93
|
+
{ duration: '5m', target: 200 },
|
|
94
|
+
{ duration: '2m', target: 300 },
|
|
95
|
+
{ duration: '5m', target: 300 },
|
|
96
|
+
{ duration: '5m', target: 0 },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
thresholds: {
|
|
101
|
+
http_req_duration: ['p(99)<1500'],
|
|
102
|
+
http_req_failed: ['rate<0.05'],
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Spike Test (Sudden Burst)
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
export const options = {
|
|
111
|
+
scenarios: {
|
|
112
|
+
spike: {
|
|
113
|
+
executor: 'ramping-vus',
|
|
114
|
+
startVUs: 0,
|
|
115
|
+
stages: [
|
|
116
|
+
{ duration: '30s', target: 10 }, // warm up
|
|
117
|
+
{ duration: '10s', target: 500 }, // spike
|
|
118
|
+
{ duration: '1m', target: 500 }, // hold spike
|
|
119
|
+
{ duration: '10s', target: 10 }, // drop
|
|
120
|
+
{ duration: '2m', target: 10 }, // recovery
|
|
121
|
+
{ duration: '30s', target: 0 }, // ramp down
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Soak Test (Extended Duration)
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
export const options = {
|
|
132
|
+
scenarios: {
|
|
133
|
+
soak: {
|
|
134
|
+
executor: 'constant-vus',
|
|
135
|
+
vus: 50,
|
|
136
|
+
duration: '4h',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
thresholds: {
|
|
140
|
+
http_req_duration: ['p(95)<800', 'p(99)<1500'],
|
|
141
|
+
http_req_failed: ['rate<0.01'],
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### Constant Request Rate
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
export const options = {
|
|
150
|
+
scenarios: {
|
|
151
|
+
constant_rate: {
|
|
152
|
+
executor: 'constant-arrival-rate',
|
|
153
|
+
rate: 100, // 100 requests per timeUnit
|
|
154
|
+
timeUnit: '1s',
|
|
155
|
+
duration: '5m',
|
|
156
|
+
preAllocatedVUs: 50,
|
|
157
|
+
maxVUs: 200,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### k6 Custom Metrics
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
import { Counter, Trend, Rate, Gauge } from 'k6/metrics';
|
|
167
|
+
|
|
168
|
+
const orderDuration = new Trend('order_duration');
|
|
169
|
+
const orderErrors = new Rate('order_errors');
|
|
170
|
+
const ordersCreated = new Counter('orders_created');
|
|
171
|
+
|
|
172
|
+
export default function () {
|
|
173
|
+
const start = Date.now();
|
|
174
|
+
const res = http.post('https://api.example.com/orders', JSON.stringify({
|
|
175
|
+
product: 'widget', quantity: 1,
|
|
176
|
+
}), { headers: { 'Content-Type': 'application/json' } });
|
|
177
|
+
|
|
178
|
+
orderDuration.add(Date.now() - start);
|
|
179
|
+
orderErrors.add(res.status !== 201);
|
|
180
|
+
if (res.status === 201) ordersCreated.add(1);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### k6 Authentication and Headers
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
import http from 'k6/http';
|
|
188
|
+
import { check } from 'k6';
|
|
189
|
+
|
|
190
|
+
export function setup() {
|
|
191
|
+
const loginRes = http.post('https://api.example.com/auth/login', JSON.stringify({
|
|
192
|
+
username: 'loadtest',
|
|
193
|
+
password: __ENV.LOAD_TEST_PASSWORD,
|
|
194
|
+
}), { headers: { 'Content-Type': 'application/json' } });
|
|
195
|
+
|
|
196
|
+
return { token: JSON.parse(loginRes.body).token };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default function (data) {
|
|
200
|
+
const params = {
|
|
201
|
+
headers: {
|
|
202
|
+
Authorization: `Bearer ${data.token}`,
|
|
203
|
+
'Content-Type': 'application/json',
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
const res = http.get('https://api.example.com/protected', params);
|
|
207
|
+
check(res, { 'authenticated request OK': (r) => r.status === 200 });
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### k6 Multi-Scenario (Mixed Workload)
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
export const options = {
|
|
215
|
+
scenarios: {
|
|
216
|
+
browse: {
|
|
217
|
+
executor: 'constant-vus', vus: 30, duration: '10m',
|
|
218
|
+
exec: 'browseProducts',
|
|
219
|
+
},
|
|
220
|
+
checkout: {
|
|
221
|
+
executor: 'ramping-arrival-rate', startRate: 5, timeUnit: '1s',
|
|
222
|
+
stages: [{ duration: '5m', target: 20 }, { duration: '5m', target: 20 }],
|
|
223
|
+
preAllocatedVUs: 50, exec: 'checkout',
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
thresholds: {
|
|
227
|
+
'http_req_duration{scenario:browse}': ['p(95)<300'],
|
|
228
|
+
'http_req_duration{scenario:checkout}': ['p(95)<1000'],
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export function browseProducts() {
|
|
233
|
+
http.get('https://api.example.com/products');
|
|
234
|
+
sleep(Math.random() * 3 + 1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function checkout() {
|
|
238
|
+
http.post('https://api.example.com/checkout', JSON.stringify({
|
|
239
|
+
items: [{ id: 1, qty: 1 }],
|
|
240
|
+
}), { headers: { 'Content-Type': 'application/json' } });
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Running k6
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
k6 run load-tests/api-load.js # Basic run
|
|
248
|
+
k6 run -e BASE_URL=https://staging.example.com load-tests/api-load.js # Env vars
|
|
249
|
+
k6 run --vus 100 --duration 5m load-tests/api-load.js # Override options
|
|
250
|
+
k6 run --out json=results.json load-tests/api-load.js # JSON output
|
|
251
|
+
k6 run --out influxdb=http://localhost:8086/k6 load-tests/api-load.js # Grafana
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 2. Load Testing with Artillery
|
|
257
|
+
|
|
258
|
+
Artillery uses YAML configuration and is well-suited for quick HTTP and WebSocket load tests.
|
|
259
|
+
|
|
260
|
+
### Artillery Configuration
|
|
261
|
+
|
|
262
|
+
```yaml
|
|
263
|
+
# load-tests/artillery.yml
|
|
264
|
+
config:
|
|
265
|
+
target: "https://api.example.com"
|
|
266
|
+
phases:
|
|
267
|
+
- duration: 60
|
|
268
|
+
arrivalRate: 5
|
|
269
|
+
name: "Warm up"
|
|
270
|
+
- duration: 120
|
|
271
|
+
arrivalRate: 20
|
|
272
|
+
rampTo: 50
|
|
273
|
+
name: "Ramp up"
|
|
274
|
+
- duration: 300
|
|
275
|
+
arrivalRate: 50
|
|
276
|
+
name: "Sustained load"
|
|
277
|
+
defaults:
|
|
278
|
+
headers:
|
|
279
|
+
Content-Type: "application/json"
|
|
280
|
+
ensure:
|
|
281
|
+
p95: 500
|
|
282
|
+
maxErrorRate: 1
|
|
283
|
+
|
|
284
|
+
scenarios:
|
|
285
|
+
- name: "Browse and Purchase"
|
|
286
|
+
weight: 70
|
|
287
|
+
flow:
|
|
288
|
+
- get:
|
|
289
|
+
url: "/products"
|
|
290
|
+
capture:
|
|
291
|
+
- json: "$[0].id"
|
|
292
|
+
as: "productId"
|
|
293
|
+
- think: 2
|
|
294
|
+
- get:
|
|
295
|
+
url: "/products/{{ productId }}"
|
|
296
|
+
- post:
|
|
297
|
+
url: "/cart"
|
|
298
|
+
json:
|
|
299
|
+
productId: "{{ productId }}"
|
|
300
|
+
quantity: 1
|
|
301
|
+
|
|
302
|
+
- name: "Search"
|
|
303
|
+
weight: 30
|
|
304
|
+
flow:
|
|
305
|
+
- get:
|
|
306
|
+
url: "/search?q=widget"
|
|
307
|
+
- think: 3
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Running Artillery
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
npx artillery run load-tests/artillery.yml # Run test
|
|
314
|
+
npx artillery run --output report.json load-tests/artillery.yml # Save results
|
|
315
|
+
npx artillery report report.json # HTML report
|
|
316
|
+
npx artillery quick --count 50 --num 10 https://api.example.com/health # Quick test
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## 3. Web Performance with Lighthouse CI
|
|
322
|
+
|
|
323
|
+
### Lighthouse CI Configuration
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
// lighthouserc.js
|
|
327
|
+
module.exports = {
|
|
328
|
+
ci: {
|
|
329
|
+
collect: {
|
|
330
|
+
url: [
|
|
331
|
+
'http://localhost:3000/',
|
|
332
|
+
'http://localhost:3000/products',
|
|
333
|
+
'http://localhost:3000/checkout',
|
|
334
|
+
],
|
|
335
|
+
startServerCommand: 'npm run start',
|
|
336
|
+
startServerReadyPattern: 'Server started',
|
|
337
|
+
numberOfRuns: 3,
|
|
338
|
+
settings: { preset: 'desktop' },
|
|
339
|
+
},
|
|
340
|
+
assert: {
|
|
341
|
+
assertions: {
|
|
342
|
+
'categories:performance': ['error', { minScore: 0.9 }],
|
|
343
|
+
'categories:accessibility': ['warn', { minScore: 0.9 }],
|
|
344
|
+
'categories:best-practices': ['warn', { minScore: 0.9 }],
|
|
345
|
+
|
|
346
|
+
// Core Web Vitals
|
|
347
|
+
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
|
|
348
|
+
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
|
|
349
|
+
'total-blocking-time': ['error', { maxNumericValue: 300 }],
|
|
350
|
+
|
|
351
|
+
// Bundle and resource budgets
|
|
352
|
+
'resource-summary:script:size': ['error', { maxNumericValue: 300000 }],
|
|
353
|
+
'resource-summary:total:size': ['error', { maxNumericValue: 800000 }],
|
|
354
|
+
'resource-summary:third-party:count': ['warn', { maxNumericValue: 10 }],
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
upload: {
|
|
358
|
+
target: 'temporary-public-storage',
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Performance Budgets
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
[
|
|
368
|
+
{
|
|
369
|
+
"path": "/*",
|
|
370
|
+
"timings": [
|
|
371
|
+
{ "metric": "interactive", "budget": 3000 },
|
|
372
|
+
{ "metric": "first-contentful-paint", "budget": 1500 },
|
|
373
|
+
{ "metric": "largest-contentful-paint", "budget": 2500 },
|
|
374
|
+
{ "metric": "cumulative-layout-shift", "budget": 0.1 },
|
|
375
|
+
{ "metric": "total-blocking-time", "budget": 300 }
|
|
376
|
+
],
|
|
377
|
+
"resourceSizes": [
|
|
378
|
+
{ "resourceType": "script", "budget": 300 },
|
|
379
|
+
{ "resourceType": "stylesheet", "budget": 100 },
|
|
380
|
+
{ "resourceType": "image", "budget": 500 },
|
|
381
|
+
{ "resourceType": "total", "budget": 1000 }
|
|
382
|
+
],
|
|
383
|
+
"resourceCounts": [
|
|
384
|
+
{ "resourceType": "script", "budget": 15 },
|
|
385
|
+
{ "resourceType": "third-party", "budget": 10 },
|
|
386
|
+
{ "resourceType": "total", "budget": 50 }
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
]
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Lighthouse CI in GitHub Actions
|
|
393
|
+
|
|
394
|
+
```yaml
|
|
395
|
+
# .github/workflows/lighthouse.yml
|
|
396
|
+
name: Lighthouse CI
|
|
397
|
+
on: [pull_request]
|
|
398
|
+
|
|
399
|
+
jobs:
|
|
400
|
+
lighthouse:
|
|
401
|
+
runs-on: ubuntu-latest
|
|
402
|
+
steps:
|
|
403
|
+
- uses: actions/checkout@v4
|
|
404
|
+
- uses: actions/setup-node@v4
|
|
405
|
+
with:
|
|
406
|
+
node-version: 20
|
|
407
|
+
- run: npm ci && npm run build
|
|
408
|
+
- name: Run Lighthouse CI
|
|
409
|
+
run: |
|
|
410
|
+
npm install -g @lhci/cli
|
|
411
|
+
lhci autorun
|
|
412
|
+
env:
|
|
413
|
+
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
|
414
|
+
- name: Upload Lighthouse Report
|
|
415
|
+
if: always()
|
|
416
|
+
uses: actions/upload-artifact@v4
|
|
417
|
+
with:
|
|
418
|
+
name: lighthouse-report
|
|
419
|
+
path: .lighthouseci/
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Lighthouse Programmatic API
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import lighthouse from 'lighthouse';
|
|
426
|
+
import * as chromeLauncher from 'chrome-launcher';
|
|
427
|
+
|
|
428
|
+
async function runLighthouse(url: string) {
|
|
429
|
+
const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
|
|
430
|
+
const result = await lighthouse(url, {
|
|
431
|
+
port: chrome.port, output: 'json', onlyCategories: ['performance'],
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const { categories, audits } = result.lhr;
|
|
435
|
+
console.log('Performance Score:', categories.performance.score * 100);
|
|
436
|
+
console.log('LCP:', audits['largest-contentful-paint'].displayValue);
|
|
437
|
+
console.log('TBT:', audits['total-blocking-time'].displayValue);
|
|
438
|
+
console.log('CLS:', audits['cumulative-layout-shift'].displayValue);
|
|
439
|
+
|
|
440
|
+
if (categories.performance.score < 0.9) {
|
|
441
|
+
console.error('Performance score below 90!');
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
await chrome.kill();
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## 4. Core Web Vitals Monitoring
|
|
451
|
+
|
|
452
|
+
### Thresholds
|
|
453
|
+
|
|
454
|
+
| Metric | Good | Needs Improvement | Poor |
|
|
455
|
+
|--------|------|-------------------|------|
|
|
456
|
+
| **LCP** (Largest Contentful Paint) | <= 2.5s | <= 4.0s | > 4.0s |
|
|
457
|
+
| **INP** (Interaction to Next Paint) | <= 200ms | <= 500ms | > 500ms |
|
|
458
|
+
| **CLS** (Cumulative Layout Shift) | <= 0.1 | <= 0.25 | > 0.25 |
|
|
459
|
+
| **FCP** (First Contentful Paint) | <= 1.8s | <= 3.0s | > 3.0s |
|
|
460
|
+
| **TTFB** (Time to First Byte) | <= 800ms | <= 1800ms | > 1800ms |
|
|
461
|
+
|
|
462
|
+
### PerformanceObserver API
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
// LCP Observer
|
|
466
|
+
function observeLCP(callback: (value: number) => void) {
|
|
467
|
+
const observer = new PerformanceObserver((list) => {
|
|
468
|
+
const entries = list.getEntries();
|
|
469
|
+
callback(entries[entries.length - 1].startTime);
|
|
470
|
+
});
|
|
471
|
+
observer.observe({ type: 'largest-contentful-paint', buffered: true });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// INP Observer (replaces FID)
|
|
475
|
+
function observeINP(callback: (value: number) => void) {
|
|
476
|
+
let maxDuration = 0;
|
|
477
|
+
const observer = new PerformanceObserver((list) => {
|
|
478
|
+
for (const entry of list.getEntries()) {
|
|
479
|
+
const inp = (entry as PerformanceEventTiming).duration;
|
|
480
|
+
if (inp > maxDuration) { maxDuration = inp; callback(inp); }
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
observer.observe({ type: 'event', buffered: true });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// CLS Observer
|
|
487
|
+
function observeCLS(callback: (value: number) => void) {
|
|
488
|
+
let clsValue = 0, sessionValue = 0;
|
|
489
|
+
let sessionEntries: PerformanceEntry[] = [];
|
|
490
|
+
const observer = new PerformanceObserver((list) => {
|
|
491
|
+
for (const entry of list.getEntries() as any[]) {
|
|
492
|
+
if (!entry.hadRecentInput) {
|
|
493
|
+
const first = sessionEntries[0], last = sessionEntries[sessionEntries.length - 1];
|
|
494
|
+
if (sessionValue && entry.startTime - last?.startTime < 1000
|
|
495
|
+
&& entry.startTime - first?.startTime < 5000) {
|
|
496
|
+
sessionValue += entry.value;
|
|
497
|
+
sessionEntries.push(entry);
|
|
498
|
+
} else {
|
|
499
|
+
sessionValue = entry.value;
|
|
500
|
+
sessionEntries = [entry];
|
|
501
|
+
}
|
|
502
|
+
if (sessionValue > clsValue) { clsValue = sessionValue; callback(clsValue); }
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
observer.observe({ type: 'layout-shift', buffered: true });
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Using the web-vitals Library
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';
|
|
514
|
+
|
|
515
|
+
function sendToAnalytics(metric: { name: string; value: number; rating: string; id: string }) {
|
|
516
|
+
fetch('/api/vitals', {
|
|
517
|
+
method: 'POST',
|
|
518
|
+
headers: { 'Content-Type': 'application/json' },
|
|
519
|
+
body: JSON.stringify({
|
|
520
|
+
name: metric.name, value: metric.value, rating: metric.rating,
|
|
521
|
+
url: window.location.href, timestamp: Date.now(),
|
|
522
|
+
}),
|
|
523
|
+
keepalive: true,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function initVitalsReporting() {
|
|
528
|
+
onLCP(sendToAnalytics);
|
|
529
|
+
onINP(sendToAnalytics);
|
|
530
|
+
onCLS(sendToAnalytics);
|
|
531
|
+
onFCP(sendToAnalytics);
|
|
532
|
+
onTTFB(sendToAnalytics);
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Real User Monitoring (RUM) Setup
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
class RealUserMonitoring {
|
|
540
|
+
constructor(private config: { endpoint: string; sampleRate: number; appVersion: string }) {}
|
|
541
|
+
|
|
542
|
+
init() {
|
|
543
|
+
if (Math.random() > this.config.sampleRate) return;
|
|
544
|
+
this.observeNavigation();
|
|
545
|
+
this.observeResources();
|
|
546
|
+
this.observeLongTasks();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private observeNavigation() {
|
|
550
|
+
new PerformanceObserver((list) => {
|
|
551
|
+
for (const entry of list.getEntries()) {
|
|
552
|
+
const nav = entry as PerformanceNavigationTiming;
|
|
553
|
+
this.report('navigation', {
|
|
554
|
+
ttfb: nav.responseStart - nav.requestStart,
|
|
555
|
+
domComplete: nav.domComplete - nav.domInteractive,
|
|
556
|
+
totalTime: nav.loadEventEnd - nav.startTime,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}).observe({ type: 'navigation', buffered: true });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private observeResources() {
|
|
563
|
+
new PerformanceObserver((list) => {
|
|
564
|
+
for (const entry of list.getEntries()) {
|
|
565
|
+
const res = entry as PerformanceResourceTiming;
|
|
566
|
+
if (res.duration > 1000) {
|
|
567
|
+
this.report('slow-resource', {
|
|
568
|
+
name: res.name, duration: res.duration, size: res.transferSize,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}).observe({ type: 'resource', buffered: true });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private observeLongTasks() {
|
|
576
|
+
new PerformanceObserver((list) => {
|
|
577
|
+
for (const entry of list.getEntries()) {
|
|
578
|
+
this.report('long-task', { duration: entry.duration, startTime: entry.startTime });
|
|
579
|
+
}
|
|
580
|
+
}).observe({ type: 'longtask', buffered: true });
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private report(type: string, data: Record<string, unknown>) {
|
|
584
|
+
fetch(this.config.endpoint, {
|
|
585
|
+
method: 'POST',
|
|
586
|
+
headers: { 'Content-Type': 'application/json' },
|
|
587
|
+
body: JSON.stringify({
|
|
588
|
+
type, data, appVersion: this.config.appVersion,
|
|
589
|
+
url: window.location.href, timestamp: Date.now(),
|
|
590
|
+
}),
|
|
591
|
+
keepalive: true,
|
|
592
|
+
}).catch(() => { /* RUM should never break the app */ });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Usage: sample 10% of users
|
|
597
|
+
const rum = new RealUserMonitoring({ endpoint: '/api/rum', sampleRate: 0.1, appVersion: '1.2.3' });
|
|
598
|
+
rum.init();
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## 5. Database Performance Testing
|
|
604
|
+
|
|
605
|
+
### Slow Query Detection
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
609
|
+
import { Pool } from 'pg';
|
|
610
|
+
|
|
611
|
+
describe('Database Query Performance', () => {
|
|
612
|
+
let pool: Pool;
|
|
613
|
+
|
|
614
|
+
beforeAll(async () => { pool = new Pool({ connectionString: process.env.DATABASE_URL }); });
|
|
615
|
+
afterAll(async () => { await pool.end(); });
|
|
616
|
+
|
|
617
|
+
it('should fetch user by ID within 50ms', async () => {
|
|
618
|
+
const start = performance.now();
|
|
619
|
+
await pool.query('SELECT * FROM users WHERE id = $1', [1]);
|
|
620
|
+
expect(performance.now() - start).toBeLessThan(50);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('should detect missing indexes via EXPLAIN ANALYZE', async () => {
|
|
624
|
+
const result = await pool.query(
|
|
625
|
+
"EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_email = 'test@example.com'"
|
|
626
|
+
);
|
|
627
|
+
const plan = result.rows.map((r) => r['QUERY PLAN']).join('\n');
|
|
628
|
+
// Should use index scan, not sequential scan on large tables
|
|
629
|
+
expect(plan).not.toMatch(/Seq Scan.*orders/);
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### Connection Pool Testing
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
describe('Connection Pool Behavior', () => {
|
|
638
|
+
it('should handle concurrent connections within pool limits', async () => {
|
|
639
|
+
const pool = new Pool({
|
|
640
|
+
connectionString: process.env.DATABASE_URL,
|
|
641
|
+
max: 10, idleTimeoutMillis: 5000, connectionTimeoutMillis: 3000,
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const concurrentQueries = 20;
|
|
645
|
+
const start = performance.now();
|
|
646
|
+
const results = await Promise.allSettled(
|
|
647
|
+
Array.from({ length: concurrentQueries }, () => pool.query('SELECT pg_sleep(0.1)'))
|
|
648
|
+
);
|
|
649
|
+
const duration = performance.now() - start;
|
|
650
|
+
|
|
651
|
+
expect(results.filter((r) => r.status === 'fulfilled').length).toBe(concurrentQueries);
|
|
652
|
+
// 10 pool connections, 20 queries at 100ms: ~200ms (2 batches), not 2000ms
|
|
653
|
+
expect(duration).toBeLessThan(1000);
|
|
654
|
+
await pool.end();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('should timeout when pool is exhausted', async () => {
|
|
658
|
+
const pool = new Pool({
|
|
659
|
+
connectionString: process.env.DATABASE_URL, max: 2, connectionTimeoutMillis: 500,
|
|
660
|
+
});
|
|
661
|
+
const held = await Promise.all([pool.connect(), pool.connect()]);
|
|
662
|
+
await expect(pool.connect()).rejects.toThrow(/timeout/);
|
|
663
|
+
for (const client of held) client.release();
|
|
664
|
+
await pool.end();
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### N+1 Query Detection
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
class QueryCounter {
|
|
673
|
+
queries: string[] = [];
|
|
674
|
+
private originalQuery: Function;
|
|
675
|
+
|
|
676
|
+
constructor(private pool: any) { this.originalQuery = pool.query.bind(pool); }
|
|
677
|
+
|
|
678
|
+
start() {
|
|
679
|
+
this.queries = [];
|
|
680
|
+
this.pool.query = (...args: any[]) => { this.queries.push(args[0]); return this.originalQuery(...args); };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
stop() { this.pool.query = this.originalQuery; }
|
|
684
|
+
get count() { return this.queries.length; }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
describe('N+1 Query Detection', () => {
|
|
688
|
+
it('should fetch orders with items in constant queries (not N+1)', async () => {
|
|
689
|
+
const counter = new QueryCounter(pool);
|
|
690
|
+
counter.start();
|
|
691
|
+
const orders = await orderService.getOrdersWithItems({ limit: 50 });
|
|
692
|
+
counter.stop();
|
|
693
|
+
|
|
694
|
+
// Expect at most 2-3 queries (orders + items batch), not 51 (1 + N)
|
|
695
|
+
expect(counter.count).toBeLessThanOrEqual(3);
|
|
696
|
+
expect(orders.length).toBeGreaterThan(0);
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
## 6. Memory and Resource Testing
|
|
704
|
+
|
|
705
|
+
### Memory Leak Detection in Node.js
|
|
706
|
+
|
|
707
|
+
```typescript
|
|
708
|
+
describe('Memory Leak Detection', () => {
|
|
709
|
+
it('should not leak memory during repeated operations', async () => {
|
|
710
|
+
if (global.gc) global.gc(); // Run with --expose-gc
|
|
711
|
+
const baselineMemory = process.memoryUsage().heapUsed;
|
|
712
|
+
const iterations = 1000;
|
|
713
|
+
|
|
714
|
+
for (let i = 0; i < iterations; i++) {
|
|
715
|
+
await processRequest({ id: i, data: 'test' });
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (global.gc) global.gc();
|
|
719
|
+
const growthPerIteration = (process.memoryUsage().heapUsed - baselineMemory) / iterations;
|
|
720
|
+
expect(growthPerIteration).toBeLessThan(1024); // < 1KB per iteration
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('should properly clean up event listeners', () => {
|
|
724
|
+
const emitter = new EventEmitter();
|
|
725
|
+
const initialListeners = emitter.listenerCount('data');
|
|
726
|
+
|
|
727
|
+
for (let i = 0; i < 100; i++) {
|
|
728
|
+
const service = new DataService(emitter);
|
|
729
|
+
service.start();
|
|
730
|
+
service.stop(); // Must remove listeners
|
|
731
|
+
}
|
|
732
|
+
expect(emitter.listenerCount('data')).toBe(initialListeners);
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### Browser Memory Profiling (Playwright)
|
|
738
|
+
|
|
739
|
+
```typescript
|
|
740
|
+
import { test, expect } from '@playwright/test';
|
|
741
|
+
|
|
742
|
+
test('should not leak memory on repeated modal open/close', async ({ page }) => {
|
|
743
|
+
await page.goto('/dashboard');
|
|
744
|
+
const getMemory = () => page.evaluate(() => (performance as any).memory?.usedJSHeapSize || 0);
|
|
745
|
+
const initialMemory = await getMemory();
|
|
746
|
+
|
|
747
|
+
for (let i = 0; i < 20; i++) {
|
|
748
|
+
await page.click('[data-testid="open-modal"]');
|
|
749
|
+
await page.waitForSelector('[data-testid="modal"]');
|
|
750
|
+
await page.click('[data-testid="close-modal"]');
|
|
751
|
+
await page.waitForSelector('[data-testid="modal"]', { state: 'hidden' });
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
await page.evaluate(() => { if ((window as any).gc) (window as any).gc(); });
|
|
755
|
+
expect(await getMemory() - initialMemory).toBeLessThan(5 * 1024 * 1024); // < 5MB growth
|
|
756
|
+
});
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### CPU Profiling
|
|
760
|
+
|
|
761
|
+
```typescript
|
|
762
|
+
import { Session } from 'inspector';
|
|
763
|
+
import { writeFileSync } from 'fs';
|
|
764
|
+
|
|
765
|
+
async function profileOperation(operation: () => Promise<void>) {
|
|
766
|
+
const session = new Session();
|
|
767
|
+
session.connect();
|
|
768
|
+
session.post('Profiler.enable');
|
|
769
|
+
session.post('Profiler.start');
|
|
770
|
+
|
|
771
|
+
await operation();
|
|
772
|
+
|
|
773
|
+
return new Promise<void>((resolve) => {
|
|
774
|
+
session.post('Profiler.stop', (err, { profile }) => {
|
|
775
|
+
if (!err) writeFileSync('cpu-profile.cpuprofile', JSON.stringify(profile));
|
|
776
|
+
session.disconnect();
|
|
777
|
+
resolve();
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
### Network Throttling Test Strategies
|
|
784
|
+
|
|
785
|
+
```typescript
|
|
786
|
+
import { test, expect } from '@playwright/test';
|
|
787
|
+
|
|
788
|
+
const networkConditions = {
|
|
789
|
+
'slow-3g': { downloadThroughput: 50_000, uploadThroughput: 25_000, latency: 400 },
|
|
790
|
+
'fast-3g': { downloadThroughput: 187_500, uploadThroughput: 75_000, latency: 150 },
|
|
791
|
+
'regular-4g': { downloadThroughput: 500_000, uploadThroughput: 250_000, latency: 50 },
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
const budgets: Record<string, number> = { 'slow-3g': 3000, 'fast-3g': 1500, 'regular-4g': 800 };
|
|
795
|
+
|
|
796
|
+
for (const [name, conditions] of Object.entries(networkConditions)) {
|
|
797
|
+
test(`page loads within budget on ${name}`, async ({ page }) => {
|
|
798
|
+
const cdp = await page.context().newCDPSession(page);
|
|
799
|
+
await cdp.send('Network.emulateNetworkConditions', { offline: false, ...conditions });
|
|
800
|
+
|
|
801
|
+
const start = Date.now();
|
|
802
|
+
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
803
|
+
expect(Date.now() - start).toBeLessThan(budgets[name]);
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Bundle Size Budget Tests
|
|
809
|
+
|
|
810
|
+
```typescript
|
|
811
|
+
import { describe, it, expect } from 'vitest';
|
|
812
|
+
import { statSync, readdirSync } from 'fs';
|
|
813
|
+
import { join } from 'path';
|
|
814
|
+
|
|
815
|
+
describe('Bundle Size Budget', () => {
|
|
816
|
+
const distDir = join(process.cwd(), 'dist');
|
|
817
|
+
|
|
818
|
+
function totalSize(dir: string, ext: string): number {
|
|
819
|
+
const files = readdirSync(dir, { recursive: true }) as string[];
|
|
820
|
+
return files
|
|
821
|
+
.filter((f) => f.endsWith(ext))
|
|
822
|
+
.reduce((sum, f) => sum + statSync(join(dir, f)).size, 0);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
it('total JS bundle should be under 300KB', () => {
|
|
826
|
+
expect(totalSize(distDir, '.js')).toBeLessThan(300 * 1024);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('total CSS should be under 100KB', () => {
|
|
830
|
+
expect(totalSize(distDir, '.css')).toBeLessThan(100 * 1024);
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
## 7. CI/CD Performance Regression Detection
|
|
838
|
+
|
|
839
|
+
### Performance Gate in GitHub Actions
|
|
840
|
+
|
|
841
|
+
```yaml
|
|
842
|
+
# .github/workflows/perf-check.yml
|
|
843
|
+
name: Performance Check
|
|
844
|
+
on: [pull_request]
|
|
845
|
+
|
|
846
|
+
jobs:
|
|
847
|
+
load-test:
|
|
848
|
+
runs-on: ubuntu-latest
|
|
849
|
+
steps:
|
|
850
|
+
- uses: actions/checkout@v4
|
|
851
|
+
- uses: actions/setup-node@v4
|
|
852
|
+
with:
|
|
853
|
+
node-version: 20
|
|
854
|
+
- run: npm ci && npm run build
|
|
855
|
+
- name: Start server
|
|
856
|
+
run: npm start &
|
|
857
|
+
env:
|
|
858
|
+
NODE_ENV: production
|
|
859
|
+
- run: npx wait-on http://localhost:3000 --timeout 30000
|
|
860
|
+
- name: Run k6 load test
|
|
861
|
+
uses: grafana/k6-action@v0.3.1
|
|
862
|
+
with:
|
|
863
|
+
filename: load-tests/api-load.js
|
|
864
|
+
flags: --out json=k6-results.json
|
|
865
|
+
- name: Upload results
|
|
866
|
+
if: always()
|
|
867
|
+
uses: actions/upload-artifact@v4
|
|
868
|
+
with:
|
|
869
|
+
name: k6-results
|
|
870
|
+
path: k6-results.json
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
### Performance Comparison Script
|
|
874
|
+
|
|
875
|
+
```typescript
|
|
876
|
+
interface PerfMetrics { p50: number; p95: number; p99: number; errorRate: number; throughput: number; }
|
|
877
|
+
|
|
878
|
+
function compareMetrics(base: PerfMetrics, current: PerfMetrics) {
|
|
879
|
+
const regressions: string[] = [];
|
|
880
|
+
const threshold = 0.10; // 10% regression threshold
|
|
881
|
+
|
|
882
|
+
if (current.p95 > base.p95 * (1 + threshold))
|
|
883
|
+
regressions.push(`p95 regressed: ${base.p95.toFixed(0)}ms -> ${current.p95.toFixed(0)}ms`);
|
|
884
|
+
if (current.errorRate > base.errorRate * (1 + threshold) && current.errorRate > 0.01)
|
|
885
|
+
regressions.push(`Error rate regressed: ${(base.errorRate*100).toFixed(2)}% -> ${(current.errorRate*100).toFixed(2)}%`);
|
|
886
|
+
if (current.throughput < base.throughput * (1 - threshold))
|
|
887
|
+
regressions.push(`Throughput regressed: ${base.throughput.toFixed(0)} -> ${current.throughput.toFixed(0)} rps`);
|
|
888
|
+
|
|
889
|
+
return { passed: regressions.length === 0, regressions };
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
---
|
|
894
|
+
|
|
895
|
+
## 8. Quick Reference
|
|
896
|
+
|
|
897
|
+
### CLI Commands
|
|
898
|
+
|
|
899
|
+
```bash
|
|
900
|
+
# k6
|
|
901
|
+
k6 run script.js # Run load test
|
|
902
|
+
k6 run --vus 50 --duration 2m # Override options
|
|
903
|
+
k6 run -e API_KEY=xxx script.js # Pass env vars
|
|
904
|
+
k6 run --out json=out.json # JSON output
|
|
905
|
+
k6 inspect script.js # Validate script
|
|
906
|
+
|
|
907
|
+
# Lighthouse CI
|
|
908
|
+
lhci autorun # Full CI pipeline
|
|
909
|
+
lhci collect --url=http://... # Collect only
|
|
910
|
+
lhci assert # Assert against config
|
|
911
|
+
lhci upload # Upload results
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
### Performance Testing Decision Matrix
|
|
915
|
+
|
|
916
|
+
| Scenario | Tool | Configuration |
|
|
917
|
+
|----------|------|---------------|
|
|
918
|
+
| API load testing | k6 | ramping-vus, thresholds |
|
|
919
|
+
| Stress/breaking point | k6 | ramping-vus with high targets |
|
|
920
|
+
| Soak testing | k6 | constant-vus, long duration |
|
|
921
|
+
| Spike testing | k6 | ramping-vus, sudden jumps |
|
|
922
|
+
| Web performance audit | Lighthouse CI | assert with budgets |
|
|
923
|
+
| Bundle size regression | Vitest | statSync + budget checks |
|
|
924
|
+
| Core Web Vitals (lab) | Lighthouse CI | performance category |
|
|
925
|
+
| Core Web Vitals (field) | web-vitals lib | RUM endpoint |
|
|
926
|
+
| Database queries | Vitest + pg | EXPLAIN ANALYZE |
|
|
927
|
+
| Memory leaks | Node inspector | heap snapshots |
|
|
928
|
+
| Network conditions | Playwright CDP | emulateNetworkConditions |
|
|
929
|
+
|
|
930
|
+
### Threshold Guidelines
|
|
931
|
+
|
|
932
|
+
| Metric | Target | Maximum |
|
|
933
|
+
|--------|--------|---------|
|
|
934
|
+
| API p95 latency | < 200ms | < 500ms |
|
|
935
|
+
| API p99 latency | < 500ms | < 1500ms |
|
|
936
|
+
| Error rate | < 0.1% | < 1% |
|
|
937
|
+
| LCP | < 2.5s | < 4.0s |
|
|
938
|
+
| INP | < 200ms | < 500ms |
|
|
939
|
+
| CLS | < 0.1 | < 0.25 |
|
|
940
|
+
| JS bundle (total) | < 200KB | < 300KB |
|
|
941
|
+
| CSS (total) | < 50KB | < 100KB |
|
|
942
|
+
| DB query | < 50ms | < 200ms |
|
|
943
|
+
|
|
944
|
+
## Best Practices
|
|
945
|
+
|
|
946
|
+
1. **Test in production-like environments** -- staging with similar data volumes and infra
|
|
947
|
+
2. **Establish baselines** before optimizing -- measure first, then improve
|
|
948
|
+
3. **Set realistic thresholds** -- based on SLAs and user expectations, not arbitrary numbers
|
|
949
|
+
4. **Automate in CI/CD** -- performance regressions should block merges
|
|
950
|
+
5. **Monitor real users (RUM)** -- lab data is a proxy; field data is truth
|
|
951
|
+
6. **Test at scale** -- seed databases with production-like data volumes
|
|
952
|
+
7. **Profile before optimizing** -- find the bottleneck, do not guess
|
|
953
|
+
8. **Budget for third parties** -- third-party scripts are the top performance killer
|
|
954
|
+
9. **Test degraded conditions** -- slow networks, cold caches, high concurrency
|
|
955
|
+
10. **Track trends over time** -- individual runs have variance; trends reveal regressions
|
|
956
|
+
|
|
957
|
+
## Related Skills
|
|
958
|
+
|
|
959
|
+
- `qa-engineer` - Overall test strategy and quality gates
|
|
960
|
+
- `e2e-testing` - Browser automation and E2E tests
|
|
961
|
+
- `unit-testing` - Unit tests and TDD workflow
|