7e3e94a08a
Add a comprehensive performance monitoring system that collects CPU, memory, swap, load average, network IO, and GPU stats. Provides both a REST API for the UI and a Prometheus /metrics endpoint. Backend changes: - New internal/perf package with configurable interval-based stats collection - GPU monitoring via LACT (Unix socket) and nvidia-smi fallback on Linux - Ring buffer (internal/ring) for time-series stat storage - Prometheus /metrics endpoint with all system and GPU metrics - Moved LogMonitor to internal/logmon package - New PerformanceConfig for hot-reloadable monitoring settings - REST /api/performance endpoint replacing SSE streaming UI changes: - New Performance page with real-time charts for CPU, memory, GPU, and network - Reusable PerformanceChart component - LLAMA_SWAP_URL environment variable support - Improved capture dialog display Other: - Example Grafana dashboard for Prometheus metrics - monitor-test standalone binary - Config schema and example updates fixes #596
251 lines
5.3 KiB
Go
251 lines
5.3 KiB
Go
package logmon
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLogMonitor(t *testing.T) {
|
|
logMonitor := NewWriter(io.Discard)
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
client1Messages := make([]byte, 0)
|
|
client2Messages := make([]byte, 0)
|
|
|
|
defer logMonitor.OnLogData(func(data []byte) {
|
|
client1Messages = append(client1Messages, data...)
|
|
wg.Done()
|
|
})()
|
|
|
|
defer logMonitor.OnLogData(func(data []byte) {
|
|
client2Messages = append(client2Messages, data...)
|
|
wg.Done()
|
|
})()
|
|
|
|
wg.Add(6) // 2 x 3 writes
|
|
|
|
logMonitor.Write([]byte("1"))
|
|
logMonitor.Write([]byte("2"))
|
|
logMonitor.Write([]byte("3"))
|
|
|
|
wg.Wait()
|
|
|
|
expectedHistory := "123"
|
|
history := string(logMonitor.GetHistory())
|
|
|
|
if history != expectedHistory {
|
|
t.Errorf("Expected history: %s, got: %s", expectedHistory, history)
|
|
}
|
|
|
|
c1Data := string(client1Messages)
|
|
if c1Data != expectedHistory {
|
|
t.Errorf("Client1 expected %s, got: %s", expectedHistory, c1Data)
|
|
}
|
|
|
|
c2Data := string(client2Messages)
|
|
if c2Data != expectedHistory {
|
|
t.Errorf("Client2 expected %s, got: %s", expectedHistory, c2Data)
|
|
}
|
|
}
|
|
|
|
func TestWrite_ImmutableBuffer(t *testing.T) {
|
|
lm := NewWriter(io.Discard)
|
|
|
|
msg := []byte("Hello, World!")
|
|
lenmsg := len(msg)
|
|
|
|
n, err := lm.Write(msg)
|
|
if err != nil {
|
|
t.Fatalf("Write failed: %v", err)
|
|
}
|
|
|
|
if n != lenmsg {
|
|
t.Errorf("Expected %d bytes written but got %d", lenmsg, n)
|
|
}
|
|
|
|
msg[0] = 'B'
|
|
|
|
history := lm.GetHistory()
|
|
|
|
expected := []byte("Hello, World!")
|
|
if !bytes.Equal(history, expected) {
|
|
t.Errorf("Expected history to be %q, got %q", expected, history)
|
|
}
|
|
}
|
|
|
|
func TestWrite_LogTimeFormat(t *testing.T) {
|
|
lm := NewWriter(io.Discard)
|
|
|
|
lm.timeFormat = time.RFC3339
|
|
|
|
lm.Info("Hello, World!")
|
|
|
|
history := lm.GetHistory()
|
|
|
|
timestamp := ""
|
|
fields := strings.Fields(string(history))
|
|
if len(fields) > 0 {
|
|
timestamp = fields[0]
|
|
} else {
|
|
t.Fatalf("Cannot extract string from history")
|
|
}
|
|
|
|
_, err := time.Parse(time.RFC3339, timestamp)
|
|
if err != nil {
|
|
t.Fatalf("Cannot find timestamp: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCircularBuffer_WrapAround(t *testing.T) {
|
|
cb := newCircularBuffer(10)
|
|
|
|
cb.Write([]byte("hello"))
|
|
if got := string(cb.GetHistory()); got != "hello" {
|
|
t.Errorf("Expected 'hello', got %q", got)
|
|
}
|
|
|
|
cb.Write([]byte("world"))
|
|
if got := string(cb.GetHistory()); got != "helloworld" {
|
|
t.Errorf("Expected 'helloworld', got %q", got)
|
|
}
|
|
|
|
cb.Write([]byte("12345"))
|
|
if got := string(cb.GetHistory()); got != "world12345" {
|
|
t.Errorf("Expected 'world12345', got %q", got)
|
|
}
|
|
|
|
cb.Write([]byte("abcdefghijklmnop"))
|
|
if got := string(cb.GetHistory()); got != "ghijklmnop" {
|
|
t.Errorf("Expected 'ghijklmnop', got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCircularBuffer_BoundaryConditions(t *testing.T) {
|
|
cb := newCircularBuffer(10)
|
|
if got := cb.GetHistory(); got != nil {
|
|
t.Errorf("Expected nil for empty buffer, got %q", got)
|
|
}
|
|
|
|
cb.Write([]byte("1234567890"))
|
|
if got := string(cb.GetHistory()); got != "1234567890" {
|
|
t.Errorf("Expected '1234567890', got %q", got)
|
|
}
|
|
|
|
cb = newCircularBuffer(10)
|
|
cb.Write([]byte("12345"))
|
|
cb.Write([]byte("67890"))
|
|
if got := string(cb.GetHistory()); got != "1234567890" {
|
|
t.Errorf("Expected '1234567890', got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestLogMonitor_LazyInit(t *testing.T) {
|
|
lm := NewWriter(io.Discard)
|
|
|
|
if lm.buffer != nil {
|
|
t.Error("Expected buffer to be nil before first write")
|
|
}
|
|
|
|
if got := lm.GetHistory(); got != nil {
|
|
t.Errorf("Expected nil history before first write, got %q", got)
|
|
}
|
|
|
|
lm.Write([]byte("test"))
|
|
|
|
if lm.buffer == nil {
|
|
t.Error("Expected buffer to be initialized after write")
|
|
}
|
|
|
|
if got := string(lm.GetHistory()); got != "test" {
|
|
t.Errorf("Expected 'test', got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestLogMonitor_Clear(t *testing.T) {
|
|
lm := NewWriter(io.Discard)
|
|
|
|
lm.Write([]byte("hello"))
|
|
if got := string(lm.GetHistory()); got != "hello" {
|
|
t.Errorf("Expected 'hello', got %q", got)
|
|
}
|
|
|
|
lm.Clear()
|
|
|
|
if lm.buffer != nil {
|
|
t.Error("Expected buffer to be nil after Clear")
|
|
}
|
|
|
|
if got := lm.GetHistory(); got != nil {
|
|
t.Errorf("Expected nil history after Clear, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestLogMonitor_ClearAndReuse(t *testing.T) {
|
|
lm := NewWriter(io.Discard)
|
|
|
|
lm.Write([]byte("first"))
|
|
lm.Clear()
|
|
lm.Write([]byte("second"))
|
|
|
|
if got := string(lm.GetHistory()); got != "second" {
|
|
t.Errorf("Expected 'second' after clear and reuse, got %q", got)
|
|
}
|
|
}
|
|
|
|
func BenchmarkLogMonitorWrite(b *testing.B) {
|
|
smallMsg := []byte("small message\n")
|
|
mediumMsg := []byte(strings.Repeat("medium message content ", 10) + "\n")
|
|
largeMsg := []byte(strings.Repeat("large message content for benchmarking ", 100) + "\n")
|
|
|
|
b.Run("SmallWrite", func(b *testing.B) {
|
|
lm := NewWriter(io.Discard)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
lm.Write(smallMsg)
|
|
}
|
|
})
|
|
|
|
b.Run("MediumWrite", func(b *testing.B) {
|
|
lm := NewWriter(io.Discard)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
lm.Write(mediumMsg)
|
|
}
|
|
})
|
|
|
|
b.Run("LargeWrite", func(b *testing.B) {
|
|
lm := NewWriter(io.Discard)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
lm.Write(largeMsg)
|
|
}
|
|
})
|
|
|
|
b.Run("WithSubscribers", func(b *testing.B) {
|
|
lm := NewWriter(io.Discard)
|
|
for i := 0; i < 5; i++ {
|
|
lm.OnLogData(func(data []byte) {})
|
|
}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
lm.Write(mediumMsg)
|
|
}
|
|
})
|
|
|
|
b.Run("GetHistory", func(b *testing.B) {
|
|
lm := NewWriter(io.Discard)
|
|
for i := 0; i < 1000; i++ {
|
|
lm.Write(mediumMsg)
|
|
}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
lm.GetHistory()
|
|
}
|
|
})
|
|
}
|