Skip to content

Commit 47ee72c

Browse files
benchmark tools
1 parent 5073274 commit 47ee72c

15 files changed

Lines changed: 1037 additions & 17 deletions

File tree

server/cmd/serve.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/spf13/viper"
1212

1313
"github.com/m1k1o/neko/server/internal/api"
14+
"github.com/m1k1o/neko/server/internal/benchmarks"
1415
"github.com/m1k1o/neko/server/internal/capture"
1516
"github.com/m1k1o/neko/server/internal/config"
1617
"github.com/m1k1o/neko/server/internal/desktop"
@@ -176,11 +177,19 @@ func (c *serve) Start(cmd *cobra.Command) {
176177
)
177178
c.managers.webSocket.Start()
178179

180+
// Create benchmark collector with target metrics
181+
// Typical WebRTC targets: 30 FPS, 2500 kbps
182+
benchmarkCollector := benchmarks.NewWebRTCStatsCollector(30.0, 2500.0)
183+
184+
// Set the benchmark collector in WebRTC manager
185+
c.managers.webRTC.SetBenchmarkCollector(benchmarkCollector)
186+
179187
c.managers.api = api.New(
180188
c.managers.session,
181189
c.managers.member,
182190
c.managers.desktop,
183191
c.managers.capture,
192+
benchmarkCollector,
184193
)
185194

186195
c.managers.plugins = plugins.New(
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package benchmark
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"strconv"
8+
"time"
9+
10+
"github.com/m1k1o/neko/server/internal/benchmarks"
11+
"github.com/m1k1o/neko/server/pkg/types"
12+
"github.com/m1k1o/neko/server/pkg/utils"
13+
"github.com/rs/zerolog"
14+
"github.com/rs/zerolog/log"
15+
)
16+
17+
type BenchmarkHandlerCtx struct {
18+
logger zerolog.Logger
19+
collector *benchmarks.WebRTCStatsCollector
20+
}
21+
22+
func New(collector *benchmarks.WebRTCStatsCollector) *BenchmarkHandlerCtx {
23+
return &BenchmarkHandlerCtx{
24+
logger: log.With().Str("module", "benchmark-api").Logger(),
25+
collector: collector,
26+
}
27+
}
28+
29+
func (h *BenchmarkHandlerCtx) Route(r types.Router) {
30+
// Internal benchmark endpoints (unauthenticated)
31+
r.Post("/start", h.StartBenchmark)
32+
}
33+
34+
// StartBenchmarkRequest represents the benchmark start request
35+
type StartBenchmarkRequest struct {
36+
Duration int `json:"duration"` // Duration in seconds
37+
}
38+
39+
// StartBenchmarkResponse represents the benchmark start response
40+
type StartBenchmarkResponse struct {
41+
Status string `json:"status"`
42+
Duration int `json:"duration"`
43+
}
44+
45+
// StartBenchmark handles POST /internal/benchmark/start
46+
func (h *BenchmarkHandlerCtx) StartBenchmark(w http.ResponseWriter, r *http.Request) error {
47+
// Parse duration from query parameter
48+
durationParam := r.URL.Query().Get("duration")
49+
duration := 10 // default 10 seconds
50+
51+
if durationParam != "" {
52+
if d, err := strconv.Atoi(durationParam); err == nil && d > 0 && d <= 60 {
53+
duration = d
54+
}
55+
}
56+
57+
h.logger.Info().
58+
Int("duration", duration).
59+
Msg("starting WebRTC benchmark")
60+
61+
// Run benchmark collection in background
62+
go func() {
63+
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(duration+5)*time.Second)
64+
defer cancel()
65+
66+
stats, err := h.collector.CollectStats(ctx, time.Duration(duration)*time.Second)
67+
if err != nil {
68+
h.logger.Error().Err(err).Msg("benchmark collection failed")
69+
return
70+
}
71+
72+
// Export stats to file for kernel-images to read
73+
if err := h.collector.ExportStats(stats); err != nil {
74+
h.logger.Error().Err(err).Msg("failed to export benchmark stats")
75+
return
76+
}
77+
78+
h.logger.Info().
79+
Float64("avg_fps", stats.FrameRateFPS.Achieved).
80+
Int("viewers", stats.ConcurrentViewers).
81+
Msg("benchmark completed and exported")
82+
}()
83+
84+
// Return immediate response
85+
response := StartBenchmarkResponse{
86+
Status: "started",
87+
Duration: duration,
88+
}
89+
90+
w.Header().Set("Content-Type", "application/json")
91+
w.WriteHeader(http.StatusOK)
92+
93+
if err := json.NewEncoder(w).Encode(response); err != nil {
94+
return utils.HttpInternalServerError().WithInternalErr(err)
95+
}
96+
97+
return nil
98+
}

server/internal/api/router.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,52 @@ import (
55
"errors"
66
"net/http"
77

8+
"github.com/m1k1o/neko/server/internal/api/benchmark"
89
"github.com/m1k1o/neko/server/internal/api/members"
910
"github.com/m1k1o/neko/server/internal/api/room"
1011
"github.com/m1k1o/neko/server/internal/api/sessions"
12+
"github.com/m1k1o/neko/server/internal/benchmarks"
1113
"github.com/m1k1o/neko/server/pkg/auth"
1214
"github.com/m1k1o/neko/server/pkg/types"
1315
"github.com/m1k1o/neko/server/pkg/utils"
1416
)
1517

1618
type ApiManagerCtx struct {
17-
sessions types.SessionManager
18-
members types.MemberManager
19-
desktop types.DesktopManager
20-
capture types.CaptureManager
21-
routers map[string]func(types.Router)
19+
sessions types.SessionManager
20+
members types.MemberManager
21+
desktop types.DesktopManager
22+
capture types.CaptureManager
23+
benchmarkCollector *benchmarks.WebRTCStatsCollector
24+
routers map[string]func(types.Router)
2225
}
2326

2427
func New(
2528
sessions types.SessionManager,
2629
members types.MemberManager,
2730
desktop types.DesktopManager,
2831
capture types.CaptureManager,
32+
benchmarkCollector *benchmarks.WebRTCStatsCollector,
2933
) *ApiManagerCtx {
3034

3135
return &ApiManagerCtx{
32-
sessions: sessions,
33-
members: members,
34-
desktop: desktop,
35-
capture: capture,
36-
routers: make(map[string]func(types.Router)),
36+
sessions: sessions,
37+
members: members,
38+
desktop: desktop,
39+
capture: capture,
40+
benchmarkCollector: benchmarkCollector,
41+
routers: make(map[string]func(types.Router)),
3742
}
3843
}
3944

4045
func (api *ApiManagerCtx) Route(r types.Router) {
4146
r.Post("/login", api.Login)
4247

48+
// Internal benchmark endpoint (unauthenticated)
49+
if api.benchmarkCollector != nil {
50+
benchmarkHandler := benchmark.New(api.benchmarkCollector)
51+
r.Route("/internal/benchmark", benchmarkHandler.Route)
52+
}
53+
4354
// Authenticated area
4455
r.Group(func(r types.Router) {
4556
r.Use(api.Authenticate)
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
//go:build linux
2+
3+
package benchmarks
4+
5+
import (
6+
"bufio"
7+
"fmt"
8+
"os"
9+
"runtime"
10+
"strconv"
11+
"strings"
12+
)
13+
14+
// CPUStats represents CPU usage statistics
15+
type CPUStats struct {
16+
User uint64
17+
System uint64
18+
Idle uint64
19+
Total uint64
20+
}
21+
22+
// GetProcessCPUStats retrieves CPU stats for the current process
23+
func GetProcessCPUStats() (*CPUStats, error) {
24+
// Read /proc/self/stat
25+
data, err := os.ReadFile("/proc/self/stat")
26+
if err != nil {
27+
return nil, fmt.Errorf("failed to read /proc/self/stat: %w", err)
28+
}
29+
30+
// Parse the stat file
31+
// Fields: pid comm state ... utime stime ...
32+
// utime is field 14 (index 13), stime is field 15 (index 14)
33+
fields := strings.Fields(string(data))
34+
if len(fields) < 15 {
35+
return nil, fmt.Errorf("unexpected /proc/self/stat format")
36+
}
37+
38+
utime, err := strconv.ParseUint(fields[13], 10, 64)
39+
if err != nil {
40+
return nil, fmt.Errorf("failed to parse utime: %w", err)
41+
}
42+
43+
stime, err := strconv.ParseUint(fields[14], 10, 64)
44+
if err != nil {
45+
return nil, fmt.Errorf("failed to parse stime: %w", err)
46+
}
47+
48+
return &CPUStats{
49+
User: utime,
50+
System: stime,
51+
Idle: 0,
52+
Total: utime + stime,
53+
}, nil
54+
}
55+
56+
// GetSystemCPUStats retrieves system-wide CPU stats
57+
func GetSystemCPUStats() (*CPUStats, error) {
58+
file, err := os.Open("/proc/stat")
59+
if err != nil {
60+
return nil, fmt.Errorf("failed to open /proc/stat: %w", err)
61+
}
62+
defer file.Close()
63+
64+
scanner := bufio.NewScanner(file)
65+
if !scanner.Scan() {
66+
return nil, fmt.Errorf("failed to read /proc/stat")
67+
}
68+
69+
line := scanner.Text()
70+
if !strings.HasPrefix(line, "cpu ") {
71+
return nil, fmt.Errorf("unexpected /proc/stat format")
72+
}
73+
74+
// cpu user nice system idle iowait irq softirq ...
75+
fields := strings.Fields(line)
76+
if len(fields) < 5 {
77+
return nil, fmt.Errorf("not enough fields in /proc/stat")
78+
}
79+
80+
user, _ := strconv.ParseUint(fields[1], 10, 64)
81+
nice, _ := strconv.ParseUint(fields[2], 10, 64)
82+
system, _ := strconv.ParseUint(fields[3], 10, 64)
83+
idle, _ := strconv.ParseUint(fields[4], 10, 64)
84+
85+
total := user + nice + system + idle
86+
if len(fields) >= 8 {
87+
iowait, _ := strconv.ParseUint(fields[5], 10, 64)
88+
irq, _ := strconv.ParseUint(fields[6], 10, 64)
89+
softirq, _ := strconv.ParseUint(fields[7], 10, 64)
90+
total += iowait + irq + softirq
91+
}
92+
93+
return &CPUStats{
94+
User: user + nice,
95+
System: system,
96+
Idle: idle,
97+
Total: total,
98+
}, nil
99+
}
100+
101+
// CalculateCPUPercent calculates CPU usage percentage from two snapshots
102+
func CalculateCPUPercent(before, after *CPUStats) float64 {
103+
if before == nil || after == nil {
104+
return 0.0
105+
}
106+
107+
deltaTotal := after.Total - before.Total
108+
if deltaTotal == 0 {
109+
return 0.0
110+
}
111+
112+
deltaUsed := (after.User + after.System) - (before.User + before.System)
113+
return (float64(deltaUsed) / float64(deltaTotal)) * 100.0
114+
}
115+
116+
// GetProcessMemoryMB returns current process memory usage in MB
117+
func GetProcessMemoryMB() float64 {
118+
var memStats runtime.MemStats
119+
runtime.ReadMemStats(&memStats)
120+
return float64(memStats.Alloc) / 1024 / 1024
121+
}
122+
123+
// GetProcessRSSMemoryMB returns RSS memory from /proc/self/status
124+
func GetProcessRSSMemoryMB() (float64, error) {
125+
file, err := os.Open("/proc/self/status")
126+
if err != nil {
127+
return 0, fmt.Errorf("failed to open /proc/self/status: %w", err)
128+
}
129+
defer file.Close()
130+
131+
scanner := bufio.NewScanner(file)
132+
for scanner.Scan() {
133+
line := scanner.Text()
134+
if strings.HasPrefix(line, "VmRSS:") {
135+
fields := strings.Fields(line)
136+
if len(fields) >= 2 {
137+
rssKB, err := strconv.ParseFloat(fields[1], 64)
138+
if err != nil {
139+
return 0, fmt.Errorf("failed to parse RSS: %w", err)
140+
}
141+
return rssKB / 1024, nil // Convert KB to MB
142+
}
143+
}
144+
}
145+
146+
return 0, fmt.Errorf("VmRSS not found in /proc/self/status")
147+
}

0 commit comments

Comments
 (0)