nsfwapp/backend/performance.go
2026-03-23 15:42:14 +01:00

344 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// backend\performance.go
package main
import (
"bytes"
"context"
"encoding/json"
"io"
"math"
"net/http"
"os"
"runtime"
"strconv"
"strings"
"sync/atomic"
"time"
gocpu "github.com/shirou/gopsutil/v3/cpu"
godisk "github.com/shirou/gopsutil/v3/disk"
)
// Server-Startzeit für Perf/Uptime
var serverStartedAt = time.Now()
var lastCPUUsageBits uint64 // atomic float64 bits
func setLastCPUUsage(v float64) {
atomic.StoreUint64(&lastCPUUsageBits, math.Float64bits(v))
}
func getLastCPUUsage() float64 {
return math.Float64frombits(atomic.LoadUint64(&lastCPUUsageBits))
}
func startCPUUsageSampler(ctx context.Context) {
_, _ = gocpu.Percent(200*time.Millisecond, false)
t := time.NewTicker(2 * time.Second)
go func() {
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
p, err := gocpu.Percent(0, false)
if err != nil || len(p) == 0 {
continue
}
usage := p[0]
if math.IsNaN(usage) || math.IsInf(usage, 0) || usage < 0 {
continue
}
if usage > 100 {
usage = 100
}
setLastCPUUsage(usage)
}
}
}()
}
// ---------- Feste ffmpeg/ffprobe Limiter ----------
type FixedLimiter chan struct{}
func NewFixedLimiter(n int) FixedLimiter {
if n < 1 {
n = 1
}
return make(FixedLimiter, n)
}
func (l FixedLimiter) Acquire(ctx context.Context) error {
if l == nil {
return nil
}
if ctx == nil {
ctx = context.Background()
}
select {
case l <- struct{}{}:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (l FixedLimiter) Release() {
if l == nil {
return
}
select {
case <-l:
default:
}
}
func (l FixedLimiter) Limit() int {
return cap(l)
}
func (l FixedLimiter) InUse() int {
return len(l)
}
var (
previewSem FixedLimiter
thumbSem FixedLimiter
genSem FixedLimiter
durSem FixedLimiter
)
func envInt(name string) (int, bool) {
v := strings.TrimSpace(os.Getenv(name))
if v == "" {
return 0, false
}
n, err := strconv.Atoi(v)
if err != nil {
return 0, false
}
return n, true
}
func initFFmpegLimiters() {
previewN := 2
thumbN := 4
genN := 2
durN := 8
if n, ok := envInt("PREVIEW_WORKERS"); ok && n > 0 {
previewN = n
}
if n, ok := envInt("THUMB_WORKERS"); ok && n > 0 {
thumbN = n
}
if n, ok := envInt("GEN_WORKERS"); ok && n > 0 {
genN = n
}
if n, ok := envInt("DUR_WORKERS"); ok && n > 0 {
durN = n
}
previewSem = NewFixedLimiter(previewN)
thumbSem = NewFixedLimiter(thumbN)
genSem = NewFixedLimiter(genN)
durSem = NewFixedLimiter(durN)
setLastCPUUsage(0)
startCPUUsageSampler(context.Background())
}
func buildPerfSnapshot() map[string]any {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
s := getSettings()
recordDir, _ := resolvePathRelativeToApp(s.RecordDir)
var diskFreeBytes uint64
var diskTotalBytes uint64
var diskUsedPercent float64
diskPath := recordDir
if recordDir != "" {
if u, err := godisk.Usage(recordDir); err == nil && u != nil {
diskFreeBytes = u.Free
diskTotalBytes = u.Total
diskUsedPercent = u.UsedPercent
}
}
// ✅ Dynamische Disk-Schwellen (2× inFlight, Resume = +3GB)
pauseGB, resumeGB, inFlight, pauseNeed, resumeNeed := computeDiskThresholds()
resp := map[string]any{
"ts": time.Now().UTC().Format(time.RFC3339Nano),
"serverMs": time.Now().UTC().UnixMilli(),
"uptimeSec": time.Since(serverStartedAt).Seconds(),
"cpuPercent": func() float64 {
v := getLastCPUUsage()
if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 {
return 0
}
if v > 100 {
return 100
}
return v
}(),
"diskPath": diskPath,
"diskFreeBytes": diskFreeBytes,
"diskTotalBytes": diskTotalBytes,
"diskUsedPercent": diskUsedPercent,
"diskEmergency": atomic.LoadInt32(&diskEmergency) == 1,
"diskPauseBelowGB": pauseGB,
"diskResumeAboveGB": resumeGB,
"diskInFlightBytes": inFlight,
"diskInFlightHuman": formatBytesSI(u64ToI64(inFlight)),
"diskPauseNeedBytes": pauseNeed,
"diskPauseNeedHuman": formatBytesSI(u64ToI64(pauseNeed)),
"diskResumeNeedBytes": resumeNeed,
"diskResumeNeedHuman": formatBytesSI(u64ToI64(resumeNeed)),
"goroutines": runtime.NumGoroutine(),
"mem": map[string]any{
"alloc": ms.Alloc,
"heapAlloc": ms.HeapAlloc,
"heapInuse": ms.HeapInuse,
"sys": ms.Sys,
"numGC": ms.NumGC,
},
}
sem := map[string]any{}
if previewSem != nil {
sem["preview"] = map[string]any{
"inUse": previewSem.InUse(),
"limit": previewSem.Limit(),
}
}
if thumbSem != nil {
sem["thumb"] = map[string]any{
"inUse": thumbSem.InUse(),
"limit": thumbSem.Limit(),
}
}
if genSem != nil {
sem["gen"] = map[string]any{
"inUse": genSem.InUse(),
"limit": genSem.Limit(),
}
}
if durSem != nil {
sem["dur"] = map[string]any{
"inUse": durSem.InUse(),
"limit": durSem.Limit(),
}
}
if len(sem) > 0 {
resp["sem"] = sem
}
return resp
}
func pingHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
http.Error(w, "Nur GET/HEAD erlaubt", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusNoContent)
}
func perfHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Nur GET erlaubt", http.StatusMethodNotAllowed)
return
}
resp := buildPerfSnapshot()
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(resp)
}
func perfStreamHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Nur GET erlaubt", http.StatusMethodNotAllowed)
return
}
fl, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming nicht unterstützt", http.StatusInternalServerError)
return
}
ms := 5000
if q := r.URL.Query().Get("ms"); q != "" {
if v, err := strconv.Atoi(q); err == nil {
if v < 1000 {
v = 1000
}
if v > 30000 {
v = 30000
}
ms = v
}
}
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
ctx := r.Context()
send := func() error {
payload := buildPerfSnapshot()
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
return err
}
_, _ = io.WriteString(w, "event: perf\n")
_, _ = io.WriteString(w, "data: ")
_, _ = w.Write(buf.Bytes())
_, _ = io.WriteString(w, "\n")
fl.Flush()
return nil
}
_ = send()
t := time.NewTicker(time.Duration(ms) * time.Millisecond)
hb := time.NewTicker(15 * time.Second)
defer t.Stop()
defer hb.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
_ = send()
case <-hb.C:
_, _ = io.WriteString(w, ": keep-alive\n\n")
fl.Flush()
}
}
}