Files
monibuca/plugin/debug/chart.go
2024-10-25 09:09:36 +08:00

291 lines
5.4 KiB
Go

package plugin_debug
import (
"embed"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"runtime"
"runtime/pprof"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/process"
)
//go:embed static/*
var staticFS embed.FS
var staticFSHandler = http.FileServer(http.FS(staticFS))
type update struct {
Ts int64
BytesAllocated uint64
GcPause uint64
CPUUser float64
CPUSys float64
Block int
Goroutine int
Heap int
Mutex int
Threadcreate int
}
type consumer struct {
id uint
c chan update
}
type server struct {
consumers []consumer
consumersMutex sync.RWMutex
}
type SimplePair struct {
Ts uint64
Value uint64
}
type CPUPair struct {
Ts uint64
User float64
Sys float64
}
type PprofPair struct {
Ts uint64
Block int
Goroutine int
Heap int
Mutex int
Threadcreate int
}
type DataStorage struct {
BytesAllocated []SimplePair
GcPauses []SimplePair
CPUUsage []CPUPair
Pprof []PprofPair
}
const (
maxCount int = 86400
)
var (
data DataStorage
lastPause uint32
mutex sync.RWMutex
lastConsumerID uint
s server
upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
prevSysTime float64
prevUserTime float64
myProcess *process.Process
)
func init() {
myProcess, _ = process.NewProcess(int32(os.Getpid()))
// preallocate arrays in data, helps save on reallocations caused by append()
// when maxCount is large
data.BytesAllocated = make([]SimplePair, 0, maxCount)
data.GcPauses = make([]SimplePair, 0, maxCount)
data.CPUUsage = make([]CPUPair, 0, maxCount)
data.Pprof = make([]PprofPair, 0, maxCount)
go s.gatherData()
}
func (s *server) gatherData() {
timer := time.Tick(time.Second)
for now := range timer {
nowUnix := now.Unix()
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
u := update{
Ts: nowUnix * 1000,
Block: pprof.Lookup("block").Count(),
Goroutine: pprof.Lookup("goroutine").Count(),
Heap: pprof.Lookup("heap").Count(),
Mutex: pprof.Lookup("mutex").Count(),
Threadcreate: pprof.Lookup("threadcreate").Count(),
}
data.Pprof = append(data.Pprof, PprofPair{
uint64(nowUnix) * 1000,
u.Block,
u.Goroutine,
u.Heap,
u.Mutex,
u.Threadcreate,
})
cpuTimes, err := myProcess.Times()
if err != nil {
cpuTimes = &cpu.TimesStat{}
}
if prevUserTime != 0 {
u.CPUUser = cpuTimes.User - prevUserTime
u.CPUSys = cpuTimes.System - prevSysTime
data.CPUUsage = append(data.CPUUsage, CPUPair{uint64(nowUnix) * 1000, u.CPUUser, u.CPUSys})
}
prevUserTime = cpuTimes.User
prevSysTime = cpuTimes.System
mutex.Lock()
bytesAllocated := ms.Alloc
u.BytesAllocated = bytesAllocated
data.BytesAllocated = append(data.BytesAllocated, SimplePair{uint64(nowUnix) * 1000, bytesAllocated})
if lastPause == 0 || lastPause != ms.NumGC {
gcPause := ms.PauseNs[(ms.NumGC+255)%256]
u.GcPause = gcPause
data.GcPauses = append(data.GcPauses, SimplePair{uint64(nowUnix) * 1000, gcPause})
lastPause = ms.NumGC
}
if len(data.BytesAllocated) > maxCount {
data.BytesAllocated = data.BytesAllocated[len(data.BytesAllocated)-maxCount:]
}
if len(data.GcPauses) > maxCount {
data.GcPauses = data.GcPauses[len(data.GcPauses)-maxCount:]
}
mutex.Unlock()
s.sendToConsumers(u)
}
}
func (s *server) sendToConsumers(u update) {
s.consumersMutex.RLock()
defer s.consumersMutex.RUnlock()
for _, c := range s.consumers {
c.c <- u
}
}
func (s *server) removeConsumer(id uint) {
s.consumersMutex.Lock()
defer s.consumersMutex.Unlock()
var consumerID uint
var consumerFound bool
for i, c := range s.consumers {
if c.id == id {
consumerFound = true
consumerID = uint(i)
break
}
}
if consumerFound {
s.consumers = append(s.consumers[:consumerID], s.consumers[consumerID+1:]...)
}
}
func (s *server) addConsumer() consumer {
s.consumersMutex.Lock()
defer s.consumersMutex.Unlock()
lastConsumerID++
c := consumer{
id: lastConsumerID,
c: make(chan update),
}
s.consumers = append(s.consumers, c)
return c
}
func (s *server) dataFeedHandler(w http.ResponseWriter, r *http.Request) {
var (
lastPing time.Time
lastPong time.Time
)
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
conn.SetPongHandler(func(s string) error {
lastPong = time.Now()
return nil
})
// read and discard all messages
go func(c *websocket.Conn) {
for {
if _, _, err := c.NextReader(); err != nil {
c.Close()
break
}
}
}(conn)
c := s.addConsumer()
defer func() {
s.removeConsumer(c.id)
conn.Close()
}()
var i uint
for u := range c.c {
conn.WriteJSON(u)
i++
if i%10 == 0 {
if diff := lastPing.Sub(lastPong); diff > time.Second*60 {
return
}
now := time.Now()
if err := conn.WriteControl(websocket.PingMessage, nil, now.Add(time.Second)); err != nil {
return
}
lastPing = now
}
}
}
func dataHandler(w http.ResponseWriter, r *http.Request) {
mutex.RLock()
defer mutex.RUnlock()
if e := r.ParseForm(); e != nil {
log.Print("error parsing form")
return
}
callback := r.FormValue("callback")
fmt.Fprintf(w, "%v(", callback)
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.Encode(data)
fmt.Fprint(w, ")")
}