From ee316210ea43627bbc386d7263b7562ee2976585 Mon Sep 17 00:00:00 2001 From: Prashant V Date: Fri, 15 Aug 2025 08:27:39 -0700 Subject: [PATCH] process: Cache dlOpen lib for faster calls On darwin, calls that rely on purego functions such as process times or memory info will open/close the library, which dlOpen/dlCloses the same library repeatedly. This causes them to be significantly slower when they're used for periodic metrics emission. Cache the library on the process to avoid the repeated dlOpen/dlClose. The added benchmark shows significant improvements: ``` BenchmarkProcessCPUTime-10 211495 5682 ns/op 2776 B/op 64 allocs/op BenchmarkProcessCPUTime-10 1159842 1021 ns/op 760 B/op 15 allocs/op ``` --- process/process.go | 1 + process/process_darwin.go | 64 +++++++++++++++------------ process/process_osprocess_fallback.go | 6 +++ process/process_test.go | 7 +++ 4 files changed, 49 insertions(+), 29 deletions(-) create mode 100644 process/process_osprocess_fallback.go diff --git a/process/process.go b/process/process.go index 0bd4d9e1a1..a1d5b9a8a9 100644 --- a/process/process.go +++ b/process/process.go @@ -41,6 +41,7 @@ type Process struct { lastCPUTimes *cpu.TimesStat lastCPUTime time.Time + os osProcess tgid int32 } diff --git a/process/process_darwin.go b/process/process_darwin.go index 6fd57f3d84..0ad44bde07 100644 --- a/process/process_darwin.go +++ b/process/process_darwin.go @@ -14,6 +14,7 @@ import ( "sort" "strconv" "strings" + "sync" "unsafe" "golang.org/x/sys/unix" @@ -279,26 +280,40 @@ func callPsWithContext(ctx context.Context, arg string, pid int32, threadOption, return ret, nil } -type dlFuncs struct { - lib *common.Library +type osProcess struct { + dlOnce sync.Once + dlErr error + dlFuncs *dlFuncs +} +type dlFuncs struct { + lib *common.Library procPidPath common.ProcPidPathFunc procPidInfo common.ProcPidInfoFunc machTimeBaseInfo common.MachTimeBaseInfoFunc } -func loadProcFuncs() (*dlFuncs, error) { - lib, err := common.NewLibrary(common.System) - if err != nil { - return nil, err - } +func (p *osProcess) funcs() (*dlFuncs, error) { + p.dlOnce.Do(func() { + lib, err := common.NewLibrary(common.System) + if err != nil { + p.dlErr = err + return + } - return &dlFuncs{ - lib: lib, - procPidPath: common.GetFunc[common.ProcPidPathFunc](lib, common.ProcPidPathSym), - procPidInfo: common.GetFunc[common.ProcPidInfoFunc](lib, common.ProcPidInfoSym), - machTimeBaseInfo: common.GetFunc[common.MachTimeBaseInfoFunc](lib, common.MachTimeBaseInfoSym), - }, nil + p.dlFuncs = &dlFuncs{ + lib: lib, + procPidPath: common.GetFunc[common.ProcPidPathFunc](lib, common.ProcPidPathSym), + procPidInfo: common.GetFunc[common.ProcPidInfoFunc](lib, common.ProcPidInfoSym), + machTimeBaseInfo: common.GetFunc[common.MachTimeBaseInfoFunc](lib, common.MachTimeBaseInfoSym), + } + + // Close library when process is no longer referenced. + runtime.SetFinalizer(lib, func(lib *common.Library) { + lib.Close() + }) + }) + return p.dlFuncs, p.dlErr } func (f *dlFuncs) getTimeScaleToNanoSeconds() float64 { @@ -309,16 +324,11 @@ func (f *dlFuncs) getTimeScaleToNanoSeconds() float64 { return float64(timeBaseInfo.Numer) / float64(timeBaseInfo.Denom) } -func (f *dlFuncs) Close() { - f.lib.Close() -} - func (p *Process) ExeWithContext(_ context.Context) (string, error) { - funcs, err := loadProcFuncs() + funcs, err := p.os.funcs() if err != nil { - return "", err + return "", nil } - defer funcs.Close() buf := common.NewCStr(common.PROC_PIDPATHINFO_MAXSIZE) ret := funcs.procPidPath(p.Pid, buf.Addr(), common.PROC_PIDPATHINFO_MAXSIZE) @@ -343,11 +353,10 @@ type vnodePathInfo struct { // error. // Note: This might also work for other *BSD OSs. func (p *Process) CwdWithContext(_ context.Context) (string, error) { - funcs, err := loadProcFuncs() + funcs, err := p.os.funcs() if err != nil { - return "", err + return "", nil } - defer funcs.Close() // Lock OS thread to ensure the errno does not change runtime.LockOSThread() @@ -440,11 +449,10 @@ func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { } func (p *Process) NumThreadsWithContext(_ context.Context) (int32, error) { - funcs, err := loadProcFuncs() + funcs, err := p.os.funcs() if err != nil { return 0, err } - defer funcs.Close() var ti ProcTaskInfo funcs.procPidInfo(p.Pid, common.PROC_PIDTASKINFO, 0, uintptr(unsafe.Pointer(&ti)), int32(unsafe.Sizeof(ti))) @@ -453,11 +461,10 @@ func (p *Process) NumThreadsWithContext(_ context.Context) (int32, error) { } func (p *Process) TimesWithContext(_ context.Context) (*cpu.TimesStat, error) { - funcs, err := loadProcFuncs() + funcs, err := p.os.funcs() if err != nil { return nil, err } - defer funcs.Close() var ti ProcTaskInfo funcs.procPidInfo(p.Pid, common.PROC_PIDTASKINFO, 0, uintptr(unsafe.Pointer(&ti)), int32(unsafe.Sizeof(ti))) @@ -472,11 +479,10 @@ func (p *Process) TimesWithContext(_ context.Context) (*cpu.TimesStat, error) { } func (p *Process) MemoryInfoWithContext(_ context.Context) (*MemoryInfoStat, error) { - funcs, err := loadProcFuncs() + funcs, err := p.os.funcs() if err != nil { return nil, err } - defer funcs.Close() var ti ProcTaskInfo funcs.procPidInfo(p.Pid, common.PROC_PIDTASKINFO, 0, uintptr(unsafe.Pointer(&ti)), int32(unsafe.Sizeof(ti))) diff --git a/process/process_osprocess_fallback.go b/process/process_osprocess_fallback.go new file mode 100644 index 0000000000..f194755d2a --- /dev/null +++ b/process/process_osprocess_fallback.go @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build !darwin + +package process + +type osProcess struct{} diff --git a/process/process_test.go b/process/process_test.go index b1255a31f6..48888beced 100644 --- a/process/process_test.go +++ b/process/process_test.go @@ -797,3 +797,10 @@ func BenchmarkProcesses(b *testing.B) { require.NotEmpty(b, ps) } } + +func BenchmarkProcessCPUTime(b *testing.B) { + p := testGetProcess() + for i := 0; i < b.N; i++ { + p.Times() + } +}