mirror of https://github.com/usememos/memos
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
121 lines
3.6 KiB
Go
121 lines
3.6 KiB
Go
package profiler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/pprof"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// Profiler provides HTTP endpoints for memory profiling.
|
|
type Profiler struct {
|
|
memStatsLogInterval time.Duration
|
|
}
|
|
|
|
// NewProfiler creates a new profiler.
|
|
func NewProfiler() *Profiler {
|
|
return &Profiler{
|
|
memStatsLogInterval: 1 * time.Minute,
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes adds profiling endpoints to the Echo server.
|
|
func (*Profiler) RegisterRoutes(e *echo.Echo) {
|
|
// Register pprof handlers
|
|
g := e.Group("/debug/pprof")
|
|
g.GET("", echo.WrapHandler(http.HandlerFunc(pprof.Index)))
|
|
g.GET("/cmdline", echo.WrapHandler(http.HandlerFunc(pprof.Cmdline)))
|
|
g.GET("/profile", echo.WrapHandler(http.HandlerFunc(pprof.Profile)))
|
|
g.POST("/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol)))
|
|
g.GET("/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol)))
|
|
g.GET("/trace", echo.WrapHandler(http.HandlerFunc(pprof.Trace)))
|
|
g.GET("/allocs", echo.WrapHandler(http.HandlerFunc(pprof.Handler("allocs").ServeHTTP)))
|
|
g.GET("/block", echo.WrapHandler(http.HandlerFunc(pprof.Handler("block").ServeHTTP)))
|
|
g.GET("/goroutine", echo.WrapHandler(http.HandlerFunc(pprof.Handler("goroutine").ServeHTTP)))
|
|
g.GET("/heap", echo.WrapHandler(http.HandlerFunc(pprof.Handler("heap").ServeHTTP)))
|
|
g.GET("/mutex", echo.WrapHandler(http.HandlerFunc(pprof.Handler("mutex").ServeHTTP)))
|
|
g.GET("/threadcreate", echo.WrapHandler(http.HandlerFunc(pprof.Handler("threadcreate").ServeHTTP)))
|
|
|
|
// Add a custom memory stats endpoint.
|
|
g.GET("/memstats", func(c echo.Context) error {
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"alloc": m.Alloc,
|
|
"totalAlloc": m.TotalAlloc,
|
|
"sys": m.Sys,
|
|
"numGC": m.NumGC,
|
|
"heapAlloc": m.HeapAlloc,
|
|
"heapSys": m.HeapSys,
|
|
"heapInuse": m.HeapInuse,
|
|
"heapObjects": m.HeapObjects,
|
|
})
|
|
})
|
|
}
|
|
|
|
// StartMemoryMonitor starts a goroutine that periodically logs memory stats.
|
|
func (p *Profiler) StartMemoryMonitor(ctx context.Context) {
|
|
go func() {
|
|
ticker := time.NewTicker(p.memStatsLogInterval)
|
|
defer ticker.Stop()
|
|
|
|
// Store previous heap allocation to track growth.
|
|
var lastHeapAlloc uint64
|
|
var lastNumGC uint32
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
|
|
// Calculate heap growth since last check.
|
|
heapGrowth := int64(m.HeapAlloc) - int64(lastHeapAlloc)
|
|
gcCount := m.NumGC - lastNumGC
|
|
|
|
slog.Info("memory stats",
|
|
"heapAlloc", byteCountIEC(m.HeapAlloc),
|
|
"heapSys", byteCountIEC(m.HeapSys),
|
|
"heapObjects", m.HeapObjects,
|
|
"heapGrowth", byteCountIEC(uint64(heapGrowth)),
|
|
"numGoroutine", runtime.NumGoroutine(),
|
|
"numGC", m.NumGC,
|
|
"gcSince", gcCount,
|
|
"nextGC", byteCountIEC(m.NextGC),
|
|
"gcPause", time.Duration(m.PauseNs[(m.NumGC+255)%256]).String(),
|
|
)
|
|
|
|
// Track values for next iteration.
|
|
lastHeapAlloc = m.HeapAlloc
|
|
lastNumGC = m.NumGC
|
|
|
|
// Force GC if memory usage is high to see if objects can be reclaimed.
|
|
if m.HeapAlloc > 500*1024*1024 { // 500 MB threshold
|
|
slog.Info("forcing garbage collection due to high memory usage")
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// byteCountIEC converts bytes to a human-readable string (MiB, GiB).
|
|
func byteCountIEC(b uint64) string {
|
|
const unit = 1024
|
|
if b < unit {
|
|
return fmt.Sprintf("%d B", b)
|
|
}
|
|
div, exp := uint64(unit), 0
|
|
for n := b / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
|
}
|