Add native Windows support (#91)

- Native Windows daemon using TCP loopback endpoints
- Direct-mode fallback for CLI/daemon compatibility
- Comment operations over RPC
- PowerShell installer script
- Go 1.24 requirement
- Cross-OS testing documented

Co-authored-by: danshapiro <danshapiro@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-c6230265-055f-4af1-9712-4481061886db
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-20 21:08:49 -07:00
parent 94a23cae39
commit a86f3e139e
58 changed files with 1707 additions and 729 deletions

View File

@@ -11,22 +11,22 @@ import (
// Metrics holds all telemetry data for the daemon
type Metrics struct {
mu sync.RWMutex
// Request metrics
requestCounts map[string]int64 // operation -> count
requestErrors map[string]int64 // operation -> error count
requestLatency map[string][]time.Duration // operation -> latency samples (bounded slice)
maxSamples int
requestCounts map[string]int64 // operation -> count
requestErrors map[string]int64 // operation -> error count
requestLatency map[string][]time.Duration // operation -> latency samples (bounded slice)
maxSamples int
// Connection metrics
totalConns int64
rejectedConns int64
totalConns int64
rejectedConns int64
// Cache metrics (handled separately via atomic in Server)
cacheEvictions int64
cacheEvictions int64
// System start time (for uptime calculation)
startTime time.Time
startTime time.Time
}
// NewMetrics creates a new metrics collector
@@ -44,9 +44,9 @@ func NewMetrics() *Metrics {
func (m *Metrics) RecordRequest(operation string, latency time.Duration) {
m.mu.Lock()
defer m.mu.Unlock()
m.requestCounts[operation]++
// Add latency sample to bounded slice
samples := m.requestLatency[operation]
if len(samples) >= m.maxSamples {
@@ -61,7 +61,7 @@ func (m *Metrics) RecordRequest(operation string, latency time.Duration) {
func (m *Metrics) RecordError(operation string) {
m.mu.Lock()
defer m.mu.Unlock()
m.requestErrors[operation]++
}
@@ -84,7 +84,7 @@ func (m *Metrics) RecordCacheEviction() {
func (m *Metrics) Snapshot(cacheHits, cacheMisses int64, cacheSize, activeConns int) MetricsSnapshot {
// Copy data under a short critical section
m.mu.RLock()
// Build union of all operations (from both counts and errors)
opsSet := make(map[string]struct{})
for op := range m.requestCounts {
@@ -93,12 +93,12 @@ func (m *Metrics) Snapshot(cacheHits, cacheMisses int64, cacheSize, activeConns
for op := range m.requestErrors {
opsSet[op] = struct{}{}
}
// Copy counts, errors, and latency slices
countsCopy := make(map[string]int64, len(opsSet))
errorsCopy := make(map[string]int64, len(opsSet))
latCopy := make(map[string][]time.Duration, len(opsSet))
for op := range opsSet {
countsCopy[op] = m.requestCounts[op]
errorsCopy[op] = m.requestErrors[op]
@@ -107,90 +107,90 @@ func (m *Metrics) Snapshot(cacheHits, cacheMisses int64, cacheSize, activeConns
latCopy[op] = append([]time.Duration(nil), samples...)
}
}
m.mu.RUnlock()
// Compute statistics outside the lock
uptime := time.Since(m.startTime)
// Calculate per-operation stats
operations := make([]OperationMetrics, 0, len(opsSet))
for op := range opsSet {
count := countsCopy[op]
errors := errorsCopy[op]
samples := latCopy[op]
// Ensure success count is never negative
successCount := count - errors
if successCount < 0 {
successCount = 0
}
opMetrics := OperationMetrics{
Operation: op,
TotalCount: count,
ErrorCount: errors,
SuccessCount: successCount,
}
// Calculate latency percentiles if we have samples
if len(samples) > 0 {
opMetrics.Latency = calculateLatencyStats(samples)
}
operations = append(operations, opMetrics)
}
// Sort by total count (most frequent first)
sort.Slice(operations, func(i, j int) bool {
return operations[i].TotalCount > operations[j].TotalCount
})
// Get memory stats
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
return MetricsSnapshot{
Timestamp: time.Now(),
UptimeSeconds: uptime.Seconds(),
Operations: operations,
CacheHits: cacheHits,
CacheMisses: cacheMisses,
CacheSize: cacheSize,
CacheEvictions: atomic.LoadInt64(&m.cacheEvictions),
TotalConns: atomic.LoadInt64(&m.totalConns),
ActiveConns: activeConns,
RejectedConns: atomic.LoadInt64(&m.rejectedConns),
MemoryAllocMB: memStats.Alloc / 1024 / 1024,
MemorySysMB: memStats.Sys / 1024 / 1024,
GoroutineCount: runtime.NumGoroutine(),
Timestamp: time.Now(),
UptimeSeconds: uptime.Seconds(),
Operations: operations,
CacheHits: cacheHits,
CacheMisses: cacheMisses,
CacheSize: cacheSize,
CacheEvictions: atomic.LoadInt64(&m.cacheEvictions),
TotalConns: atomic.LoadInt64(&m.totalConns),
ActiveConns: activeConns,
RejectedConns: atomic.LoadInt64(&m.rejectedConns),
MemoryAllocMB: memStats.Alloc / 1024 / 1024,
MemorySysMB: memStats.Sys / 1024 / 1024,
GoroutineCount: runtime.NumGoroutine(),
}
}
// MetricsSnapshot is a point-in-time view of all metrics
type MetricsSnapshot struct {
Timestamp time.Time `json:"timestamp"`
UptimeSeconds float64 `json:"uptime_seconds"`
Operations []OperationMetrics `json:"operations"`
CacheHits int64 `json:"cache_hits"`
CacheMisses int64 `json:"cache_misses"`
CacheSize int `json:"cache_size"`
CacheEvictions int64 `json:"cache_evictions"`
TotalConns int64 `json:"total_connections"`
ActiveConns int `json:"active_connections"`
RejectedConns int64 `json:"rejected_connections"`
MemoryAllocMB uint64 `json:"memory_alloc_mb"`
MemorySysMB uint64 `json:"memory_sys_mb"`
GoroutineCount int `json:"goroutine_count"`
Timestamp time.Time `json:"timestamp"`
UptimeSeconds float64 `json:"uptime_seconds"`
Operations []OperationMetrics `json:"operations"`
CacheHits int64 `json:"cache_hits"`
CacheMisses int64 `json:"cache_misses"`
CacheSize int `json:"cache_size"`
CacheEvictions int64 `json:"cache_evictions"`
TotalConns int64 `json:"total_connections"`
ActiveConns int `json:"active_connections"`
RejectedConns int64 `json:"rejected_connections"`
MemoryAllocMB uint64 `json:"memory_alloc_mb"`
MemorySysMB uint64 `json:"memory_sys_mb"`
GoroutineCount int `json:"goroutine_count"`
}
// OperationMetrics holds metrics for a single operation type
type OperationMetrics struct {
Operation string `json:"operation"`
TotalCount int64 `json:"total_count"`
SuccessCount int64 `json:"success_count"`
ErrorCount int64 `json:"error_count"`
Latency LatencyStats `json:"latency,omitempty"`
Operation string `json:"operation"`
TotalCount int64 `json:"total_count"`
SuccessCount int64 `json:"success_count"`
ErrorCount int64 `json:"error_count"`
Latency LatencyStats `json:"latency,omitempty"`
}
// LatencyStats holds latency percentile data in milliseconds
@@ -208,32 +208,32 @@ func calculateLatencyStats(samples []time.Duration) LatencyStats {
if len(samples) == 0 {
return LatencyStats{}
}
// Sort samples
sorted := make([]time.Duration, len(samples))
copy(sorted, samples)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i] < sorted[j]
})
n := len(sorted)
// Calculate percentiles with defensive clamping
p50Idx := min(n-1, n*50/100)
p95Idx := min(n-1, n*95/100)
p99Idx := min(n-1, n*99/100)
// Calculate average
var sum time.Duration
for _, d := range sorted {
sum += d
}
avg := sum / time.Duration(n)
// Convert to milliseconds
toMS := func(d time.Duration) float64 {
return float64(d) / float64(time.Millisecond)
}
return LatencyStats{
MinMS: toMS(sorted[0]),
P50MS: toMS(sorted[p50Idx]),