vibetunnel/benchmark/cmd/stream.go
Helmut Januschka 3c94e494fc
Add comprehensive VibeTunnel protocol benchmark tool (#18)
* Add comprehensive VibeTunnel protocol benchmark tool

Features:
- Complete HTTP API client implementation for VibeTunnel protocol
- Session management benchmarks (create/get/list/delete operations)
- SSE streaming performance testing with latency measurements
- Concurrent user load testing with realistic simulation
- Support for custom hostname/port configuration
- Detailed performance statistics and reporting

Commands:
- session: Test session lifecycle performance
- stream: Benchmark SSE streaming latency/throughput
- load: Concurrent user load testing

Tested against both Go (port 4031) and Rust (port 4044) servers.
Tool successfully creates sessions and measures performance metrics.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add configurable benchmark comparison tool

Features:
- Compare command with 10-100 configurable runs
- Cross-server API compatibility (Go/Rust fields)
- Session management and streaming benchmarks
- Performance analysis and winner detection
- Comprehensive statistics and throughput metrics

Results show Go server ~27-50% faster than Rust for session operations.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove API compatibility shims for clean unified format

- Unified SessionConfig to use Rust API format (command, workingDir)
- Updated all benchmark commands to use clean API
- Go and Rust servers now use identical API format
- 100-run test shows near-identical performance (1% difference)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Enable high-load testing up to 1000 runs

- Increased run limit from 100 to 1000 for stress testing
- Reveals resource exhaustion under extreme load
- Go server: 203/1000 success in first round, then failures
- Rust server: immediate failure under high load
- Both servers need connection pooling/rate limiting

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-06-18 23:38:11 +02:00

296 lines
No EOL
8.1 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cmd
import (
"fmt"
"sync"
"time"
"github.com/spf13/cobra"
"github.com/vibetunnel/benchmark/client"
)
var streamCmd = &cobra.Command{
Use: "stream",
Short: "Benchmark SSE streaming performance",
Long: `Test Server-Sent Events (SSE) streaming latency and throughput.
Measures event delivery times and handles concurrent streams.`,
RunE: runStreamBenchmark,
}
var (
streamSessions int
streamDuration time.Duration
streamCommands []string
streamConcurrent bool
streamInputDelay time.Duration
)
func init() {
rootCmd.AddCommand(streamCmd)
streamCmd.Flags().IntVarP(&streamSessions, "sessions", "s", 3, "Number of sessions to stream")
streamCmd.Flags().DurationVarP(&streamDuration, "duration", "d", 30*time.Second, "Benchmark duration")
streamCmd.Flags().StringSliceVar(&streamCommands, "commands", []string{"echo hello", "ls -la", "date"}, "Commands to execute")
streamCmd.Flags().BoolVar(&streamConcurrent, "concurrent", true, "Run streams concurrently")
streamCmd.Flags().DurationVar(&streamInputDelay, "input-delay", 2*time.Second, "Delay between command inputs")
}
func runStreamBenchmark(cmd *cobra.Command, args []string) error {
client := client.NewClient(hostname, port)
fmt.Printf("🚀 VibeTunnel SSE Stream Benchmark\n")
fmt.Printf("Target: %s:%d\n", hostname, port)
fmt.Printf("Sessions: %d\n", streamSessions)
fmt.Printf("Duration: %v\n", streamDuration)
fmt.Printf("Concurrent: %v\n\n", streamConcurrent)
// Test connectivity
fmt.Print("Testing connectivity... ")
if err := client.Ping(); err != nil {
return fmt.Errorf("server connectivity failed: %w", err)
}
fmt.Println("✅ Connected")
if streamConcurrent {
return benchmarkConcurrentStreams(client)
} else {
return benchmarkSequentialStreams(client)
}
}
func benchmarkConcurrentStreams(c *client.VibeTunnelClient) error {
fmt.Printf("\n📊 Concurrent SSE Stream Benchmark\n")
var wg sync.WaitGroup
results := make(chan *StreamResult, streamSessions)
startTime := time.Now()
// Start concurrent stream benchmarks
for i := 0; i < streamSessions; i++ {
wg.Add(1)
go func(sessionNum int) {
defer wg.Done()
result := benchmarkSingleStream(c, sessionNum)
results <- result
}(i)
}
// Wait for all streams to complete
wg.Wait()
close(results)
totalDuration := time.Since(startTime)
// Collect and analyze results
var allResults []*StreamResult
for result := range results {
allResults = append(allResults, result)
}
return analyzeStreamResults(allResults, totalDuration)
}
func benchmarkSequentialStreams(c *client.VibeTunnelClient) error {
fmt.Printf("\n📊 Sequential SSE Stream Benchmark\n")
var allResults []*StreamResult
startTime := time.Now()
for i := 0; i < streamSessions; i++ {
result := benchmarkSingleStream(c, i)
allResults = append(allResults, result)
}
totalDuration := time.Since(startTime)
return analyzeStreamResults(allResults, totalDuration)
}
type StreamResult struct {
SessionNum int
SessionID string
EventsReceived int
BytesReceived int64
FirstEventTime time.Duration
LastEventTime time.Duration
TotalDuration time.Duration
Errors []error
EventLatencies []time.Duration
}
func benchmarkSingleStream(c *client.VibeTunnelClient, sessionNum int) *StreamResult {
result := &StreamResult{
SessionNum: sessionNum,
EventLatencies: make([]time.Duration, 0),
}
startTime := time.Now()
// Create session
config := client.SessionConfig{
Name: fmt.Sprintf("stream-bench-%d", sessionNum),
Command: []string{"/bin/bash", "-i"},
WorkingDir: "/tmp",
Width: 80,
Height: 24,
Term: "xterm-256color",
Env: map[string]string{"BENCH": "true"},
}
session, err := c.CreateSession(config)
if err != nil {
result.Errors = append(result.Errors, fmt.Errorf("create session: %w", err))
return result
}
result.SessionID = session.ID
defer c.DeleteSession(session.ID)
if verbose {
fmt.Printf(" Session %d: Created %s\n", sessionNum+1, session.ID)
}
// Start streaming
stream, err := c.StreamSession(session.ID)
if err != nil {
result.Errors = append(result.Errors, fmt.Errorf("start stream: %w", err))
return result
}
defer stream.Close()
// Send commands and monitor stream
go func() {
time.Sleep(500 * time.Millisecond) // Wait for stream to establish
for i, command := range streamCommands {
if err := c.SendInput(session.ID, command+"\n"); err != nil {
result.Errors = append(result.Errors, fmt.Errorf("send command %d: %w", i, err))
continue
}
if verbose {
fmt.Printf(" Session %d: Sent command '%s'\n", sessionNum+1, command)
}
if i < len(streamCommands)-1 {
time.Sleep(streamInputDelay)
}
}
}()
// Monitor events
timeout := time.NewTimer(streamDuration)
defer timeout.Stop()
for {
select {
case event, ok := <-stream.Events:
if !ok {
result.TotalDuration = time.Since(startTime)
return result
}
eventTime := time.Since(startTime)
result.EventsReceived++
if result.EventsReceived == 1 {
result.FirstEventTime = eventTime
}
result.LastEventTime = eventTime
// Calculate event data size
if event.Event != nil {
result.BytesReceived += int64(len(event.Event.Data))
}
if verbose && result.EventsReceived <= 5 {
fmt.Printf(" Session %d: Event %d received at +%.1fms\n",
sessionNum+1, result.EventsReceived, float64(eventTime.Nanoseconds())/1e6)
}
case err, ok := <-stream.Errors:
if !ok {
result.TotalDuration = time.Since(startTime)
return result
}
result.Errors = append(result.Errors, err)
case <-timeout.C:
result.TotalDuration = time.Since(startTime)
return result
}
}
}
func analyzeStreamResults(results []*StreamResult, totalDuration time.Duration) error {
fmt.Printf("\n📈 Stream Performance Statistics\n")
fmt.Printf("Total Duration: %.2fs\n", totalDuration.Seconds())
var (
totalEvents int
totalBytes int64
totalErrors int
totalSessions int
avgFirstEvent time.Duration
avgLastEvent time.Duration
)
successfulSessions := 0
for _, result := range results {
totalSessions++
totalEvents += result.EventsReceived
totalBytes += result.BytesReceived
totalErrors += len(result.Errors)
if len(result.Errors) == 0 && result.EventsReceived > 0 {
successfulSessions++
avgFirstEvent += result.FirstEventTime
avgLastEvent += result.LastEventTime
}
if verbose {
fmt.Printf("\nSession %d (%s):\n", result.SessionNum+1, result.SessionID)
fmt.Printf(" Events: %d\n", result.EventsReceived)
fmt.Printf(" Bytes: %d\n", result.BytesReceived)
fmt.Printf(" First Event: %.1fms\n", float64(result.FirstEventTime.Nanoseconds())/1e6)
fmt.Printf(" Last Event: %.1fms\n", float64(result.LastEventTime.Nanoseconds())/1e6)
fmt.Printf(" Duration: %.2fs\n", result.TotalDuration.Seconds())
fmt.Printf(" Errors: %d\n", len(result.Errors))
for i, err := range result.Errors {
fmt.Printf(" Error %d: %v\n", i+1, err)
}
}
}
if successfulSessions > 0 {
avgFirstEvent /= time.Duration(successfulSessions)
avgLastEvent /= time.Duration(successfulSessions)
}
fmt.Printf("\nOverall Results:\n")
fmt.Printf(" Sessions: %d total, %d successful\n", totalSessions, successfulSessions)
fmt.Printf(" Events: %d total\n", totalEvents)
fmt.Printf(" Data: %.2f KB\n", float64(totalBytes)/1024)
fmt.Printf(" Errors: %d\n", totalErrors)
if successfulSessions > 0 {
fmt.Printf("\nLatency (average):\n")
fmt.Printf(" First Event: %.1fms\n", float64(avgFirstEvent.Nanoseconds())/1e6)
fmt.Printf(" Last Event: %.1fms\n", float64(avgLastEvent.Nanoseconds())/1e6)
fmt.Printf("\nThroughput:\n")
fmt.Printf(" Events/sec: %.1f\n", float64(totalEvents)/totalDuration.Seconds())
fmt.Printf(" KB/sec: %.2f\n", float64(totalBytes)/1024/totalDuration.Seconds())
fmt.Printf(" Success Rate: %.1f%%\n", float64(successfulSessions)/float64(totalSessions)*100)
}
if totalErrors > 0 {
fmt.Printf("\n⚠ %d errors encountered during benchmark\n", totalErrors)
} else {
fmt.Printf("\n✅ All streams completed successfully\n")
}
return nil
}