Skip to content

Commit bf3ce87

Browse files
committed
update
Signed-off-by: bitliu <[email protected]>
1 parent cedef91 commit bf3ce87

File tree

8 files changed

+399
-5
lines changed

8 files changed

+399
-5
lines changed

e2e/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ The framework includes the following test cases (all in `e2e/testcases/`):
5353
| Test Case | Description | Metrics |
5454
|-----------|-------------|---------|
5555
| `chat-completions-request` | Basic chat completions API test | Response validation |
56+
| `chat-completions-stress-request` | Sequential stress test with 1000 requests | Success rate, avg duration |
57+
| `chat-completions-progressive-stress` | Progressive QPS stress test (10/20/50/100 QPS) | Per-stage success rate, latency stats |
5658
| `domain-classify` | Domain classification accuracy | 65 cases, accuracy rate |
5759
| `cache` | Semantic cache hit rate | 5 groups, cache hit rate |
5860
| `pii-detection` | PII detection and blocking | 10 PII types, detection rate, block rate |
@@ -85,6 +87,16 @@ make e2e-test
8587
make e2e-test PROFILE=ai-gateway
8688
```
8789

90+
### Run specific test cases
91+
92+
```bash
93+
# Run only the progressive stress test
94+
./e2e/cmd/e2e/e2e --profile ai-gateway --test-cases chat-completions-progressive-stress --verbose
95+
96+
# Run multiple specific test cases
97+
./e2e/cmd/e2e/e2e --profile ai-gateway --test-cases chat-completions-request,chat-completions-progressive-stress
98+
```
99+
88100
### Run with custom options
89101

90102
```bash

e2e/profiles/ai-gateway/profile.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ func (p *Profile) GetTestCases() []string {
109109
return []string{
110110
"chat-completions-request",
111111
"chat-completions-stress-request",
112+
"chat-completions-progressive-stress",
112113
"domain-classify",
113114
"semantic-cache",
114115
"pii-detection",
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package testcases
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"sync"
8+
"time"
9+
10+
pkgtestcases "github.com/vllm-project/semantic-router/e2e/pkg/testcases"
11+
"k8s.io/client-go/kubernetes"
12+
)
13+
14+
func init() {
15+
pkgtestcases.Register("chat-completions-progressive-stress", pkgtestcases.TestCase{
16+
Description: "Progressive stress test with 10/20/50/100 QPS and success rate tracking",
17+
Tags: []string{"llm", "stress", "progressive", "qps"},
18+
Fn: testProgressiveStress,
19+
})
20+
}
21+
22+
// ProgressiveStageResult tracks results for a single QPS stage
23+
type ProgressiveStageResult struct {
24+
QPS int
25+
TotalReqs int
26+
SuccessCount int
27+
FailureCount int
28+
SuccessRate float64
29+
AvgDuration time.Duration
30+
MinDuration time.Duration
31+
MaxDuration time.Duration
32+
Results []StressTestResult
33+
}
34+
35+
func testProgressiveStress(ctx context.Context, client *kubernetes.Clientset, opts pkgtestcases.TestCaseOptions) error {
36+
if opts.Verbose {
37+
fmt.Println("[Test] Starting progressive stress test: 10/20/50/100 QPS")
38+
}
39+
40+
// Setup service connection and get local port
41+
localPort, stopPortForward, err := setupServiceConnection(ctx, client, opts)
42+
if err != nil {
43+
return err
44+
}
45+
defer stopPortForward()
46+
47+
// Define QPS stages and duration for each stage
48+
qpsStages := []int{10, 20, 50, 100}
49+
stageDuration := 30 * time.Second // Run each stage for 30 seconds
50+
51+
var stageResults []ProgressiveStageResult
52+
53+
// Run each QPS stage
54+
for _, qps := range qpsStages {
55+
if opts.Verbose {
56+
fmt.Printf("\n[Test] Starting stage: %d QPS for %v\n", qps, stageDuration)
57+
}
58+
59+
stageResult := runQPSStage(ctx, qps, stageDuration, localPort)
60+
stageResults = append(stageResults, stageResult)
61+
62+
if opts.Verbose {
63+
fmt.Printf("[Test] Stage %d QPS completed: %d/%d successful (%.2f%% success rate)\n",
64+
qps, stageResult.SuccessCount, stageResult.TotalReqs, stageResult.SuccessRate)
65+
}
66+
67+
// Brief pause between stages
68+
time.Sleep(2 * time.Second)
69+
}
70+
71+
// Print comprehensive summary
72+
printProgressiveResults(stageResults)
73+
74+
// Set details for reporting
75+
if opts.SetDetails != nil {
76+
details := make(map[string]interface{})
77+
for _, stage := range stageResults {
78+
stageKey := fmt.Sprintf("qps_%d", stage.QPS)
79+
details[stageKey] = map[string]interface{}{
80+
"total_requests": stage.TotalReqs,
81+
"successful": stage.SuccessCount,
82+
"failed": stage.FailureCount,
83+
"success_rate": fmt.Sprintf("%.2f%%", stage.SuccessRate),
84+
"avg_duration": stage.AvgDuration.Milliseconds(),
85+
"min_duration": stage.MinDuration.Milliseconds(),
86+
"max_duration": stage.MaxDuration.Milliseconds(),
87+
}
88+
}
89+
opts.SetDetails(details)
90+
}
91+
92+
return nil
93+
}
94+
95+
func runQPSStage(ctx context.Context, qps int, duration time.Duration, localPort string) ProgressiveStageResult {
96+
result := ProgressiveStageResult{
97+
QPS: qps,
98+
MinDuration: time.Hour, // Initialize with large value
99+
}
100+
101+
var mu sync.Mutex
102+
var wg sync.WaitGroup
103+
104+
// Calculate interval between requests
105+
interval := time.Second / time.Duration(qps)
106+
ticker := time.NewTicker(interval)
107+
defer ticker.Stop()
108+
109+
// Context with timeout for this stage
110+
stageCtx, cancel := context.WithTimeout(ctx, duration)
111+
defer cancel()
112+
113+
requestID := 0
114+
115+
// Send requests at the specified QPS rate
116+
for {
117+
select {
118+
case <-stageCtx.Done():
119+
// Stage duration completed, wait for all in-flight requests
120+
wg.Wait()
121+
return calculateStageStats(result)
122+
123+
case <-ticker.C:
124+
requestID++
125+
wg.Add(1)
126+
127+
go func(reqID int) {
128+
defer wg.Done()
129+
130+
// Send request
131+
reqResult := sendSingleRequest(ctx, reqID, localPort, false)
132+
133+
// Update results
134+
mu.Lock()
135+
result.Results = append(result.Results, reqResult)
136+
result.TotalReqs++
137+
if reqResult.Success {
138+
result.SuccessCount++
139+
} else {
140+
result.FailureCount++
141+
}
142+
mu.Unlock()
143+
}(requestID)
144+
}
145+
}
146+
}
147+
148+
func calculateStageStats(result ProgressiveStageResult) ProgressiveStageResult {
149+
if result.TotalReqs == 0 {
150+
return result
151+
}
152+
153+
// Calculate success rate
154+
result.SuccessRate = float64(result.SuccessCount) / float64(result.TotalReqs) * 100
155+
156+
// Calculate duration statistics
157+
var totalDuration time.Duration
158+
for _, r := range result.Results {
159+
totalDuration += r.Duration
160+
if r.Duration < result.MinDuration {
161+
result.MinDuration = r.Duration
162+
}
163+
if r.Duration > result.MaxDuration {
164+
result.MaxDuration = r.Duration
165+
}
166+
}
167+
168+
if len(result.Results) > 0 {
169+
result.AvgDuration = totalDuration / time.Duration(len(result.Results))
170+
}
171+
172+
// Reset MinDuration if it wasn't updated
173+
if result.MinDuration == time.Hour {
174+
result.MinDuration = 0
175+
}
176+
177+
return result
178+
}
179+
180+
func printProgressiveResults(stageResults []ProgressiveStageResult) {
181+
separator := strings.Repeat("=", 100)
182+
fmt.Println("\n" + separator)
183+
fmt.Println("Progressive Stress Test Results")
184+
fmt.Println(separator)
185+
186+
// Print header
187+
fmt.Printf("%-10s %-15s %-15s %-15s %-15s %-15s %-15s\n",
188+
"QPS", "Total Reqs", "Successful", "Failed", "Success Rate", "Avg Duration", "Max Duration")
189+
fmt.Println(strings.Repeat("-", 100))
190+
191+
// Print each stage
192+
for _, stage := range stageResults {
193+
fmt.Printf("%-10d %-15d %-15d %-15d %-15s %-15v %-15v\n",
194+
stage.QPS,
195+
stage.TotalReqs,
196+
stage.SuccessCount,
197+
stage.FailureCount,
198+
fmt.Sprintf("%.2f%%", stage.SuccessRate),
199+
stage.AvgDuration.Round(time.Millisecond),
200+
stage.MaxDuration.Round(time.Millisecond))
201+
}
202+
203+
fmt.Println(separator)
204+
205+
// Print summary statistics
206+
fmt.Println("\nSummary:")
207+
totalRequests := 0
208+
totalSuccess := 0
209+
for _, stage := range stageResults {
210+
totalRequests += stage.TotalReqs
211+
totalSuccess += stage.SuccessCount
212+
}
213+
overallSuccessRate := float64(totalSuccess) / float64(totalRequests) * 100
214+
fmt.Printf(" Overall: %d/%d successful (%.2f%% success rate)\n",
215+
totalSuccess, totalRequests, overallSuccessRate)
216+
217+
// Show failures for each stage
218+
fmt.Println("\nFailures by Stage:")
219+
for _, stage := range stageResults {
220+
if stage.FailureCount > 0 {
221+
fmt.Printf(" %d QPS: %d failures\n", stage.QPS, stage.FailureCount)
222+
// Show first 3 failures for this stage
223+
failureCount := 0
224+
for _, result := range stage.Results {
225+
if !result.Success && failureCount < 3 {
226+
failureCount++
227+
fmt.Printf(" Request #%d: %s (duration: %v)\n",
228+
result.RequestID, result.ErrorMessage, result.Duration)
229+
}
230+
if failureCount >= 3 {
231+
break
232+
}
233+
}
234+
} else {
235+
fmt.Printf(" %d QPS: No failures! 🎉\n", stage.QPS)
236+
}
237+
}
238+
fmt.Println()
239+
}

e2e/testcases/chat_completions_stress_request.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,13 @@ func sendSingleRequest(ctx context.Context, requestID int, localPort string, ver
101101

102102
start := time.Now()
103103

104-
// Prepare request body
104+
// Prepare request body with random content
105105
requestBody := map[string]interface{}{
106106
"model": "MoM",
107107
"messages": []map[string]string{
108108
{
109109
"role": "user",
110-
"content": fmt.Sprintf("Request #%d: What is 2+2?", requestID),
110+
"content": generateRandomContent(requestID),
111111
},
112112
},
113113
}

e2e/testcases/common.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package testcases
33
import (
44
"context"
55
"fmt"
6+
"math/rand"
67
"strings"
78
"time"
89

@@ -54,3 +55,92 @@ func setupServiceConnection(ctx context.Context, client *kubernetes.Clientset, o
5455

5556
return portParts[0], stopFunc, nil
5657
}
58+
59+
// Random content generation for stress tests
60+
61+
var (
62+
// Question templates for variety
63+
questionTemplates = []string{
64+
"What is %d + %d?",
65+
"Calculate %d * %d",
66+
"What is the result of %d - %d?",
67+
"Solve: %d divided by %d",
68+
"What is %d%% of %d?",
69+
"If I have %d apples and buy %d more, how many do I have?",
70+
"What is the square root of %d?",
71+
"What is %d to the power of %d?",
72+
"How many days are in %d weeks?",
73+
"What is the average of %d and %d?",
74+
}
75+
76+
topics = []string{
77+
"Explain the concept of machine learning",
78+
"What is the capital of France?",
79+
"How does photosynthesis work?",
80+
"What are the benefits of exercise?",
81+
"Describe the water cycle",
82+
"What is quantum computing?",
83+
"How do vaccines work?",
84+
"What causes climate change?",
85+
"Explain blockchain technology",
86+
"What is artificial intelligence?",
87+
"How does the internet work?",
88+
"What is the theory of relativity?",
89+
"Describe the solar system",
90+
"What is DNA?",
91+
"How do airplanes fly?",
92+
}
93+
94+
tasks = []string{
95+
"Write a short poem about nature",
96+
"Summarize the main points of renewable energy",
97+
"List 5 programming languages",
98+
"Describe a typical day in your life",
99+
"Explain how to make a sandwich",
100+
"Give me 3 tips for better sleep",
101+
"What are the colors of the rainbow?",
102+
"Name 5 countries in Europe",
103+
"Describe your favorite hobby",
104+
"What are the main food groups?",
105+
}
106+
)
107+
108+
// generateRandomContent generates random request content for stress testing
109+
func generateRandomContent(requestID int) string {
110+
// Use requestID as seed for reproducibility within a test run
111+
r := rand.New(rand.NewSource(time.Now().UnixNano() + int64(requestID)))
112+
113+
contentType := r.Intn(3)
114+
115+
switch contentType {
116+
case 0:
117+
// Math question
118+
template := questionTemplates[r.Intn(len(questionTemplates))]
119+
num1 := r.Intn(100) + 1
120+
num2 := r.Intn(100) + 1
121+
return fmt.Sprintf("Request #%d: "+template, requestID, num1, num2)
122+
case 1:
123+
// General topic
124+
topic := topics[r.Intn(len(topics))]
125+
return fmt.Sprintf("Request #%d: %s", requestID, topic)
126+
default:
127+
// Task
128+
task := tasks[r.Intn(len(tasks))]
129+
return fmt.Sprintf("Request #%d: %s", requestID, task)
130+
}
131+
}
132+
133+
// formatResponseHeaders formats HTTP response headers for logging
134+
func formatResponseHeaders(headers map[string][]string) string {
135+
if len(headers) == 0 {
136+
return " (no headers)"
137+
}
138+
139+
var sb strings.Builder
140+
for key, values := range headers {
141+
for _, value := range values {
142+
sb.WriteString(fmt.Sprintf(" %s: %s\n", key, value))
143+
}
144+
}
145+
return sb.String()
146+
}

0 commit comments

Comments
 (0)