Files
wehub-resource-sync e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:33 +08:00

259 lines
5.9 KiB
Go

package mcp
import (
"bytes"
"context"
"encoding/json"
"log"
"net/http"
"net/http/httptest"
"testing"
"go-micro.dev/v6/auth"
"go-micro.dev/v6/client"
"go-micro.dev/v6/registry"
)
// benchServer creates a Server with N pre-populated tools.
func benchServer(n int, opts Options) *Server {
if opts.Logger == nil {
opts.Logger = log.New(log.Writer(), "", 0)
}
if opts.Context == nil {
opts.Context = context.Background()
}
if opts.Client == nil {
opts.Client = client.DefaultClient
}
if opts.Registry == nil {
opts.Registry = registry.DefaultRegistry
}
s := &Server{
opts: opts,
tools: make(map[string]*Tool, n),
limiters: make(map[string]*rateLimiter),
}
for i := 0; i < n; i++ {
name := toolName(i)
s.tools[name] = &Tool{
Name: name,
Description: "Benchmark tool " + name,
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"id": map[string]interface{}{
"type": "string",
"description": "Resource identifier",
},
},
"required": []interface{}{"id"},
},
Service: "bench",
Endpoint: "Handler.Method",
}
}
return s
}
func toolName(i int) string {
return "bench.Handler.Method" + string(rune('A'+i%26))
}
// --- Benchmarks ---
// BenchmarkListTools measures tool listing throughput.
// This is the most common MCP operation — agents call it on every session start.
func BenchmarkListTools(b *testing.B) {
for _, numTools := range []int{10, 50, 100} {
b.Run(toolCountLabel(numTools), func(b *testing.B) {
s := benchServer(numTools, Options{})
req := httptest.NewRequest(http.MethodGet, "/mcp/tools", nil)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
s.handleListTools(w, req)
if w.Code != http.StatusOK {
b.Fatalf("unexpected status %d", w.Code)
}
}
})
}
}
// BenchmarkListToolsParallel measures concurrent tool listing.
func BenchmarkListToolsParallel(b *testing.B) {
s := benchServer(50, Options{})
req := httptest.NewRequest(http.MethodGet, "/mcp/tools", nil)
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
w := httptest.NewRecorder()
s.handleListTools(w, req)
}
})
}
// BenchmarkToolLookup measures tool name resolution from the tools map.
func BenchmarkToolLookup(b *testing.B) {
for _, numTools := range []int{10, 50, 100, 500} {
b.Run(toolCountLabel(numTools), func(b *testing.B) {
s := benchServer(numTools, Options{})
name := toolName(numTools / 2) // look up a tool in the middle
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s.toolsMu.RLock()
_, ok := s.tools[name]
s.toolsMu.RUnlock()
if !ok {
b.Fatal("tool not found")
}
}
})
}
}
// BenchmarkAuthInspect measures auth token inspection overhead.
func BenchmarkAuthInspect(b *testing.B) {
ma := &mockAuth{
accounts: map[string]*auth.Account{
"valid-token": {
ID: "bench-user",
Scopes: []string{"read", "write"},
},
},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
acc, err := ma.Inspect("valid-token")
if err != nil || acc.ID != "bench-user" {
b.Fatal("unexpected result")
}
}
}
// BenchmarkScopeCheck measures scope validation overhead per tool call.
func BenchmarkScopeCheck(b *testing.B) {
accountScopes := []string{"users:read", "users:write", "orders:read", "admin"}
requiredScopes := []string{"users:write"}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
hasScope(accountScopes, requiredScopes)
}
}
// BenchmarkAuditRecord measures audit record creation overhead.
func BenchmarkAuditRecord(b *testing.B) {
var records int
s := benchServer(10, Options{
AuditFunc: func(r AuditRecord) {
records++
},
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s.opts.AuditFunc(AuditRecord{
TraceID: "trace-123",
Tool: "bench.Handler.MethodA",
AccountID: "user-1",
Allowed: true,
})
}
}
// BenchmarkRateLimiter measures rate limiter check overhead.
func BenchmarkRateLimiter(b *testing.B) {
s := benchServer(10, Options{
RateLimit: &RateLimitConfig{
RequestsPerSecond: 1000000, // Very high so it doesn't block
Burst: 1000000,
},
})
// Initialize limiters for tools
for name := range s.tools {
s.limiters[name] = newRateLimiter(s.opts.RateLimit.RequestsPerSecond, s.opts.RateLimit.Burst)
}
name := toolName(0)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s.limitersMu.RLock()
l := s.limiters[name]
s.limitersMu.RUnlock()
l.Allow()
}
}
// BenchmarkJSONEncodeTool measures JSON serialization of tool definitions.
func BenchmarkJSONEncodeTool(b *testing.B) {
tool := &Tool{
Name: "myservice.Users.GetUser",
Description: "Retrieve a user by their unique ID. Returns the full profile.",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"id": map[string]interface{}{
"type": "string",
"description": "User ID in UUID format",
},
},
"required": []interface{}{"id"},
},
Scopes: []string{"users:read"},
Service: "myservice",
Endpoint: "Users.GetUser",
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(tool)
}
}
// BenchmarkJSONDecodeCallRequest measures parsing of incoming tool call requests.
func BenchmarkJSONDecodeCallRequest(b *testing.B) {
body := []byte(`{"tool":"myservice.Users.GetUser","arguments":{"id":"user-123"}}`)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var req struct {
Tool string `json:"tool"`
Arguments map[string]interface{} `json:"arguments"`
}
json.Unmarshal(body, &req)
}
}
// --- Helpers ---
func toolCountLabel(n int) string {
switch {
case n >= 500:
return "500_tools"
case n >= 100:
return "100_tools"
case n >= 50:
return "50_tools"
default:
return "10_tools"
}
}