From b984386babdb29802ec3cad04a91a91963f06c9b Mon Sep 17 00:00:00 2001 From: yusing Date: Wed, 22 Jan 2025 05:44:04 +0800 Subject: [PATCH] fix: high cpu usage --- cmd/main.go | 1 + cmd/main_production.go | 5 ++++ cmd/main_prof.go | 17 ++++++++++++ internal/api/v1/mem_logger.go | 27 +++++++++++-------- internal/docker/client.go | 19 +++++++------ internal/list-icons.go | 11 +++++--- internal/net/http/accesslog/access_logger.go | 8 +++--- .../http/middleware/errorpage/error_page.go | 11 ++------ internal/net/http/middleware/rate_limit.go | 4 +-- internal/route/types/raw_entry.go | 2 +- internal/task/task.go | 18 ++++++++++--- next-release.md | 3 +++ 12 files changed, 82 insertions(+), 44 deletions(-) create mode 100644 cmd/main_production.go create mode 100644 cmd/main_prof.go diff --git a/cmd/main.go b/cmd/main.go index a479a67..7169993 100755 --- a/cmd/main.go +++ b/cmd/main.go @@ -39,6 +39,7 @@ func init() { } func main() { + initProfiling() args := common.GetArgs() switch args.Command { diff --git a/cmd/main_production.go b/cmd/main_production.go new file mode 100644 index 0000000..2ceaf84 --- /dev/null +++ b/cmd/main_production.go @@ -0,0 +1,5 @@ +//go:build production + +package main + +func initProfiling() {} diff --git a/cmd/main_prof.go b/cmd/main_prof.go new file mode 100644 index 0000000..2970217 --- /dev/null +++ b/cmd/main_prof.go @@ -0,0 +1,17 @@ +//go:build pprof + +package main + +import ( + "log" + "net/http" + _ "net/http/pprof" + "runtime" +) + +func initProfiling() { + runtime.GOMAXPROCS(2) + go func() { + log.Println(http.ListenAndServe(":7777", nil)) + }() +} diff --git a/internal/api/v1/mem_logger.go b/internal/api/v1/mem_logger.go index 434690a..390d990 100644 --- a/internal/api/v1/mem_logger.go +++ b/internal/api/v1/mem_logger.go @@ -23,8 +23,9 @@ type logEntryRange struct { type memLogger struct { bytes.Buffer - sync.Mutex - connChans F.Map[chan *logEntryRange, struct{}] + sync.RWMutex + notifyLock sync.RWMutex + connChans F.Map[chan *logEntryRange, struct{}] } const ( @@ -72,25 +73,28 @@ func MemLogger() io.Writer { } func (m *memLogger) Write(p []byte) (n int, err error) { - m.Lock() - + m.RLock() if m.Len() > maxMemLogSize { m.Truncate(truncateSize) } + m.RUnlock() - pos := m.Buffer.Len() n = len(p) + m.Lock() + pos := m.Len() _, err = m.Buffer.Write(p) + m.Unlock() + if err != nil { - m.Unlock() return } if m.connChans.Size() > 0 { - m.Unlock() timeout := time.NewTimer(1 * time.Second) defer timeout.Stop() + m.notifyLock.RLock() + defer m.notifyLock.RUnlock() m.connChans.Range(func(ch chan *logEntryRange, _ struct{}) bool { select { case ch <- &logEntryRange{pos, pos + n}: @@ -102,8 +106,6 @@ func (m *memLogger) Write(p []byte) (n int, err error) { }) return } - - m.Unlock() return } @@ -120,8 +122,11 @@ func (m *memLogger) ServeHTTP(config config.ConfigInstance, w http.ResponseWrite /* trunk-ignore(golangci-lint/errcheck) */ defer func() { _ = conn.CloseNow() + + m.notifyLock.Lock() m.connChans.Delete(logCh) close(logCh) + m.notifyLock.Unlock() }() if err := m.wsInitial(r.Context(), conn); err != nil { @@ -149,10 +154,10 @@ func (m *memLogger) wsStreamLog(ctx context.Context, conn *websocket.Conn, ch <- case <-ctx.Done(): return case logRange := <-ch: - m.Lock() + m.RLock() msg := m.Buffer.Bytes()[logRange.Start:logRange.End] err := m.writeBytes(ctx, conn, msg) - m.Unlock() + m.RUnlock() if err != nil { return } diff --git a/internal/docker/client.go b/internal/docker/client.go index a8ba759..f547334 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -12,7 +12,6 @@ import ( "github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/task" U "github.com/yusing/go-proxy/internal/utils" - F "github.com/yusing/go-proxy/internal/utils/functional" ) type ( @@ -27,7 +26,7 @@ type ( ) var ( - clientMap F.Map[string, *SharedClient] = F.NewMapOf[string, *SharedClient]() + clientMap = make(map[string]*SharedClient, 5) clientMapMu sync.Mutex clientOptEnvHost = []client.Opt{ @@ -38,11 +37,14 @@ var ( func init() { task.OnProgramExit("docker_clients_cleanup", func() { - clientMap.RangeAllParallel(func(_ string, c *SharedClient) { + clientMapMu.Lock() + defer clientMapMu.Unlock() + + for _, c := range clientMap { if c.Connected() { c.Client.Close() } - }) + } }) } @@ -71,8 +73,7 @@ func ConnectClient(host string) (*SharedClient, error) { clientMapMu.Lock() defer clientMapMu.Unlock() - // check if client exists - if client, ok := clientMap.Load(host); ok { + if client, ok := clientMap[host]; ok { client.refCount.Add() return client, nil } @@ -123,11 +124,13 @@ func ConnectClient(host string) (*SharedClient, error) { } c.l.Trace().Msg("client connected") - clientMap.Store(host, c) + clientMap[host] = c go func() { <-c.refCount.Zero() - clientMap.Delete(c.key) + clientMapMu.Lock() + delete(clientMap, c.key) + clientMapMu.Unlock() if c.Connected() { c.Client.Close() diff --git a/internal/list-icons.go b/internal/list-icons.go index ab6492f..f46a668 100644 --- a/internal/list-icons.go +++ b/internal/list-icons.go @@ -42,7 +42,7 @@ const updateInterval = 2 * time.Hour var ( iconsCache *Cache - iconsCahceMu sync.Mutex + iconsCahceMu sync.RWMutex lastUpdate time.Time ) @@ -71,14 +71,17 @@ func InitIconListCache() { } func ListAvailableIcons() (*Cache, error) { - iconsCahceMu.Lock() - defer iconsCahceMu.Unlock() - + iconsCahceMu.RLock() if time.Since(lastUpdate) < updateInterval { if !iconsCache.needUpdate() { + iconsCahceMu.RUnlock() return iconsCache, nil } } + iconsCahceMu.RUnlock() + + iconsCahceMu.Lock() + defer iconsCahceMu.Unlock() icons, err := fetchIconData() if err != nil { diff --git a/internal/net/http/accesslog/access_logger.go b/internal/net/http/accesslog/access_logger.go index 5116193..16c10d4 100644 --- a/internal/net/http/accesslog/access_logger.go +++ b/internal/net/http/accesslog/access_logger.go @@ -19,8 +19,8 @@ type ( io AccessLogIO buf bytes.Buffer // buffer for non-flushed log - bufMu sync.Mutex // protect buf - bufPool sync.Pool // buffer pool for formatting a single log line + bufMu sync.RWMutex + bufPool sync.Pool // buffer pool for formatting a single log line flushThreshold int @@ -123,10 +123,10 @@ func (l *AccessLogger) Flush(force bool) { return } if force || l.buf.Len() >= l.flushThreshold { - l.bufMu.Lock() + l.bufMu.RLock() l.write(l.buf.Bytes()) l.buf.Reset() - l.bufMu.Unlock() + l.bufMu.RUnlock() } } diff --git a/internal/net/http/middleware/errorpage/error_page.go b/internal/net/http/middleware/errorpage/error_page.go index d0afe85..2fb09e1 100644 --- a/internal/net/http/middleware/errorpage/error_page.go +++ b/internal/net/http/middleware/errorpage/error_page.go @@ -19,19 +19,12 @@ import ( const errPagesBasePath = common.ErrorPagesBasePath var ( - setupMu sync.Mutex + setupOnce sync.Once dirWatcher W.Watcher fileContentMap = F.NewMapOf[string, []byte]() ) func setup() { - setupMu.Lock() - defer setupMu.Unlock() - - if dirWatcher != nil { - return - } - t := task.RootTask("error_page", false) dirWatcher = W.NewDirectoryWatcher(t, errPagesBasePath) loadContent() @@ -39,7 +32,7 @@ func setup() { } func GetStaticFile(filename string) ([]byte, bool) { - setup() + setupOnce.Do(setup) return fileContentMap.Load(filename) } diff --git a/internal/net/http/middleware/rate_limit.go b/internal/net/http/middleware/rate_limit.go index 07557bf..fd0b1a2 100644 --- a/internal/net/http/middleware/rate_limit.go +++ b/internal/net/http/middleware/rate_limit.go @@ -49,8 +49,6 @@ func (rl *rateLimiter) newLimiter() *rate.Limiter { } func (rl *rateLimiter) limit(w http.ResponseWriter, r *http.Request) bool { - rl.mu.Lock() - host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { rl.AddTracef("unable to parse remote address %s", r.RemoteAddr) @@ -58,12 +56,12 @@ func (rl *rateLimiter) limit(w http.ResponseWriter, r *http.Request) bool { return false } + rl.mu.Lock() limiter, ok := rl.requestMap[host] if !ok { limiter = rl.newLimiter() rl.requestMap[host] = limiter } - rl.mu.Unlock() if limiter.Allow() { diff --git a/internal/route/types/raw_entry.go b/internal/route/types/raw_entry.go index c3fad07..c4b2081 100644 --- a/internal/route/types/raw_entry.go +++ b/internal/route/types/raw_entry.go @@ -172,7 +172,7 @@ func (e *RawEntry) Finalize() { } } - if e.Homepage == nil { + if e.Homepage.IsEmpty() { e.Homepage = homepage.NewItem(e.Alias) } diff --git a/internal/task/task.go b/internal/task/task.go index 0a0a5ed..19a5f8a 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -4,6 +4,7 @@ import ( "context" "runtime/debug" "sync" + "sync/atomic" "time" "github.com/yusing/go-proxy/internal/common" @@ -44,8 +45,11 @@ type ( callbacks map[*Callback]struct{} callbacksDone chan struct{} - finished chan struct{} - finishedCalled bool + finished chan struct{} + // finishedCalled == 1 Finish has been called + // but does not mean that the task is finished yet + // this is used to avoid calling Finish twice + finishedCalled uint32 mu sync.Mutex @@ -93,13 +97,19 @@ func (t *Task) OnCancel(about string, fn func()) { // Finish cancel all subtasks and wait for them to finish, // then marks the task as finished, with the given reason (if any). func (t *Task) Finish(reason any) { + if atomic.LoadUint32(&t.finishedCalled) == 1 { + return + } + t.mu.Lock() - if t.finishedCalled { + if t.finishedCalled == 1 { t.mu.Unlock() return } - t.finishedCalled = true + + t.finishedCalled = 1 t.mu.Unlock() + t.finish(reason) } diff --git a/next-release.md b/next-release.md index 559e7f3..182d326 100644 --- a/next-release.md +++ b/next-release.md @@ -116,6 +116,9 @@ GoDoxy v0.8.2 expected changes ``` - **new** Brand new rewritten WebUI + - View logs directly from WebUI + - Edit dashboard item config (overrides docker labels and include file) + - Health bubbles, latency, etc. rich info on dashboard items - **new** Support selfh.st icons: `@selfhst/.` _(e.g. `@selfhst/adguard-home.webp`)_ - also uses the display name on https://selfh.st/icons/ as default for our dashboard! - **new** GoDoxy server side favicon retreiving and caching