mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-28 09:55:53 +00:00
* 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>
358 lines
No EOL
8.6 KiB
Go
358 lines
No EOL
8.6 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// VibeTunnelClient implements the VibeTunnel HTTP API protocol
|
|
type VibeTunnelClient struct {
|
|
baseURL string
|
|
httpClient *http.Client
|
|
authToken string
|
|
}
|
|
|
|
// SessionConfig represents session creation parameters
|
|
type SessionConfig struct {
|
|
Name string `json:"name"`
|
|
Command []string `json:"command"`
|
|
WorkingDir string `json:"workingDir"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
Term string `json:"term"`
|
|
Env map[string]string `json:"env"`
|
|
}
|
|
|
|
// SessionInfo represents session metadata
|
|
type SessionInfo struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Created time.Time `json:"created"`
|
|
ExitCode *int `json:"exit_code"`
|
|
Cmdline string `json:"cmdline"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
Cwd string `json:"cwd"`
|
|
Term string `json:"term"`
|
|
}
|
|
|
|
// AsciinemaEvent represents terminal output events
|
|
type AsciinemaEvent struct {
|
|
Time float64 `json:"time"`
|
|
Type string `json:"type"`
|
|
Data string `json:"data"`
|
|
}
|
|
|
|
// StreamEvent represents SSE stream events
|
|
type StreamEvent struct {
|
|
Type string `json:"type"`
|
|
Event *AsciinemaEvent `json:"event,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// NewClient creates a new VibeTunnel API client
|
|
func NewClient(hostname string, port int) *VibeTunnelClient {
|
|
return &VibeTunnelClient{
|
|
baseURL: fmt.Sprintf("http://%s:%d", hostname, port),
|
|
httpClient: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// SetAuth sets authentication token for requests
|
|
func (c *VibeTunnelClient) SetAuth(token string) {
|
|
c.authToken = token
|
|
}
|
|
|
|
// CreateSession creates a new terminal session
|
|
func (c *VibeTunnelClient) CreateSession(config SessionConfig) (*SessionInfo, error) {
|
|
data, err := json.Marshal(config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal config: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", c.baseURL+"/api/sessions", bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if c.authToken != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.authToken)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("do request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var session SessionInfo
|
|
if err := json.NewDecoder(resp.Body).Decode(&session); err != nil {
|
|
return nil, fmt.Errorf("decode response: %w", err)
|
|
}
|
|
|
|
return &session, nil
|
|
}
|
|
|
|
// GetSession retrieves session information by ID
|
|
func (c *VibeTunnelClient) GetSession(sessionID string) (*SessionInfo, error) {
|
|
req, err := http.NewRequest("GET", c.baseURL+"/api/sessions/"+sessionID, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
if c.authToken != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.authToken)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("do request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var session SessionInfo
|
|
if err := json.NewDecoder(resp.Body).Decode(&session); err != nil {
|
|
return nil, fmt.Errorf("decode response: %w", err)
|
|
}
|
|
|
|
return &session, nil
|
|
}
|
|
|
|
// ListSessions retrieves all sessions
|
|
func (c *VibeTunnelClient) ListSessions() ([]SessionInfo, error) {
|
|
req, err := http.NewRequest("GET", c.baseURL+"/api/sessions", nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
if c.authToken != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.authToken)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("do request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var sessions []SessionInfo
|
|
if err := json.NewDecoder(resp.Body).Decode(&sessions); err != nil {
|
|
return nil, fmt.Errorf("decode response: %w", err)
|
|
}
|
|
|
|
return sessions, nil
|
|
}
|
|
|
|
// SendInput sends input to a session
|
|
func (c *VibeTunnelClient) SendInput(sessionID, input string) error {
|
|
data := map[string]string{"input": input}
|
|
jsonData, err := json.Marshal(data)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal input: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", c.baseURL+"/api/sessions/"+sessionID+"/input", bytes.NewReader(jsonData))
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if c.authToken != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.authToken)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("do request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SSEStream represents an SSE connection for streaming events
|
|
type SSEStream struct {
|
|
resp *http.Response
|
|
Events chan StreamEvent
|
|
Errors chan error
|
|
done chan struct{}
|
|
}
|
|
|
|
// StreamSession opens an SSE connection to stream session events
|
|
func (c *VibeTunnelClient) StreamSession(sessionID string) (*SSEStream, error) {
|
|
req, err := http.NewRequest("GET", c.baseURL+"/api/sessions/"+sessionID+"/stream", nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Accept", "text/event-stream")
|
|
req.Header.Set("Cache-Control", "no-cache")
|
|
if c.authToken != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.authToken)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("do request: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
stream := &SSEStream{
|
|
resp: resp,
|
|
Events: make(chan StreamEvent, 100),
|
|
Errors: make(chan error, 10),
|
|
done: make(chan struct{}),
|
|
}
|
|
|
|
go stream.readLoop()
|
|
|
|
return stream, nil
|
|
}
|
|
|
|
// Close closes the SSE stream
|
|
func (s *SSEStream) Close() error {
|
|
close(s.done)
|
|
return s.resp.Body.Close()
|
|
}
|
|
|
|
// readLoop processes SSE events from the stream
|
|
func (s *SSEStream) readLoop() {
|
|
defer close(s.Events)
|
|
defer close(s.Errors)
|
|
|
|
buf := make([]byte, 4096)
|
|
var buffer strings.Builder
|
|
|
|
for {
|
|
select {
|
|
case <-s.done:
|
|
return
|
|
default:
|
|
}
|
|
|
|
n, err := s.resp.Body.Read(buf)
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
s.Errors <- fmt.Errorf("read stream: %w", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
buffer.Write(buf[:n])
|
|
content := buffer.String()
|
|
|
|
// Process complete SSE events
|
|
for {
|
|
eventEnd := strings.Index(content, "\n\n")
|
|
if eventEnd == -1 {
|
|
break
|
|
}
|
|
|
|
eventData := content[:eventEnd]
|
|
content = content[eventEnd+2:]
|
|
|
|
if strings.HasPrefix(eventData, "data: ") {
|
|
jsonData := strings.TrimPrefix(eventData, "data: ")
|
|
|
|
var event StreamEvent
|
|
if err := json.Unmarshal([]byte(jsonData), &event); err != nil {
|
|
s.Errors <- fmt.Errorf("unmarshal event: %w", err)
|
|
continue
|
|
}
|
|
|
|
select {
|
|
case s.Events <- event:
|
|
case <-s.done:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
buffer.Reset()
|
|
buffer.WriteString(content)
|
|
}
|
|
}
|
|
|
|
// DeleteSession deletes a session
|
|
func (c *VibeTunnelClient) DeleteSession(sessionID string) error {
|
|
req, err := http.NewRequest("DELETE", c.baseURL+"/api/sessions/"+sessionID, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
if c.authToken != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.authToken)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("do request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Ping tests server connectivity
|
|
func (c *VibeTunnelClient) Ping() error {
|
|
req, err := http.NewRequest("GET", c.baseURL+"/api/sessions", nil)
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
if c.authToken != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.authToken)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("do request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return nil
|
|
} |