From eb3afc5041102e1c5e6ad5f62de404f86f1e9096 Mon Sep 17 00:00:00 2001 From: kvs vishnu kumar Date: Sat, 19 Sep 2026 14:36:52 +0530 Subject: [PATCH 1/4] use os proc stat to fetch processStatTime for a PID instead of TaskstatsPID (cherry picked from commit 27bc75dedc2ad64e6beaee57c35924bba383c290) Conflict: this fork's NewProcess has no tracer or instrumentDone field. Its pid-reuse check in onProcessStart (#353) now compares the start time from /proc//stat, which has the same one-second resolution as the taskstats BeginTime it replaces. --- containers/container.go | 12 +++++------ containers/process.go | 5 ++--- containers/taskstats.go | 13 ------------ proc/proc.go | 45 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 22 deletions(-) diff --git a/containers/container.go b/containers/container.go index 3ad9ede8..e9e0eb6c 100644 --- a/containers/container.go +++ b/containers/container.go @@ -569,22 +569,22 @@ func (c *Container) Collect(ch chan<- prometheus.Metric) { func (c *Container) onProcessStart(pid uint32) *Process { c.lock.Lock() defer c.lock.Unlock() - stats, err := TaskstatsPID(pid) - if err != nil { + startedAt := proc.GetStartTime(pid) + if startedAt.IsZero() { return nil } if p := c.processes[pid]; p != nil { // Already registered for this same process: a connection can be // handled before the process start event (see attachTlsUprobes). // Replacing it would drop its uprobes without closing them. - if p.StartedAt.Equal(stats.BeginTime) { + if p.StartedAt.Equal(startedAt) { return p } // The pid was reused: the previous process exited unnoticed. c.closeProcess(pid, p) } c.zombieAt = time.Time{} - p := NewProcess(pid, stats, c.registry.tracer) + p := NewProcess(pid, startedAt, c.registry.tracer) if p == nil { return nil @@ -592,9 +592,9 @@ func (c *Container) onProcessStart(pid uint32) *Process { c.processes[pid] = p if c.startedAt.IsZero() { - c.startedAt = stats.BeginTime + c.startedAt = startedAt } else { - min := stats.BeginTime + min := startedAt for _, p := range c.processes { if p.StartedAt.Before(min) { min = p.StartedAt diff --git a/containers/process.go b/containers/process.go index d677fbf6..6f6f13ec 100644 --- a/containers/process.go +++ b/containers/process.go @@ -15,7 +15,6 @@ import ( "github.com/coroot/coroot-node-agent/gpu" "github.com/coroot/coroot-node-agent/proc" "github.com/jpillora/backoff" - "github.com/mdlayher/taskstats" ) type GpuUsage struct { @@ -66,8 +65,8 @@ type Process struct { nodejsChecked bool } -func NewProcess(pid uint32, stats *taskstats.Stats, tracer *ebpftracer.Tracer) *Process { - p := &Process{Pid: pid, StartedAt: stats.BeginTime} +func NewProcess(pid uint32, startedAt time.Time, tracer *ebpftracer.Tracer) *Process { + p := &Process{Pid: pid, StartedAt: startedAt} p.Flags, _ = proc.GetFlags(pid) p.ctx, p.cancelFunc = context.WithCancel(context.Background()) go p.instrument(tracer) diff --git a/containers/taskstats.go b/containers/taskstats.go index 25a645fe..3ea9dff4 100644 --- a/containers/taskstats.go +++ b/containers/taskstats.go @@ -33,16 +33,3 @@ func TaskstatsTGID(pid uint32) (*taskstats.Stats, error) { } return s, nil } - -func TaskstatsPID(pid uint32) (*taskstats.Stats, error) { - if taskstatsClient == nil { - return nil, fmt.Errorf("taskstats client not initialized") - } - taskstatsLock.Lock() - defer taskstatsLock.Unlock() - s, err := taskstatsClient.PID(int(pid)) - if err != nil { - return nil, err - } - return s, nil -} diff --git a/proc/proc.go b/proc/proc.go index c51348b9..fa2afa49 100644 --- a/proc/proc.go +++ b/proc/proc.go @@ -8,6 +8,7 @@ import ( "path" "strconv" "strings" + "time" "github.com/coroot/coroot-node-agent/cgroup" ) @@ -64,6 +65,50 @@ func ReadCgroup(pid uint32) (*cgroup.Cgroup, error) { return cgroup.NewFromProcessCgroupFile(Path(pid, "cgroup")) } +// GetStartTime returns the process start time derived from /proc//stat +func GetStartTime(pid uint32) time.Time { + data, err := os.ReadFile(Path(pid, "stat")) + if err != nil { + return time.Time{} + } + s := string(data) + idx := strings.LastIndex(s, ")") + if idx < 0 { + return time.Time{} + } + fields := strings.Fields(s[idx+1:]) + if len(fields) < 20 { + return time.Time{} + } + startTicks, err := strconv.ParseUint(fields[19], 10, 64) + if err != nil { + return time.Time{} + } + btime, err := bootTime() + if err != nil { + return time.Time{} + } + return time.Unix(btime+int64(float64(startTicks)/100), 0) +} + +func bootTime() (int64, error) { + data, err := os.ReadFile(root + "/stat") + if err != nil { + return 0, err + } + for _, line := range strings.Split(string(data), "\n") { + if !strings.HasPrefix(line, "btime ") { + continue + } + fields := strings.Fields(line) + if len(fields) != 2 { + return 0, fmt.Errorf("invalid btime line in /proc/stat: %q", line) + } + return strconv.ParseInt(fields[1], 10, 64) + } + return 0, fmt.Errorf("btime not found in /proc/stat") +} + func ListPids() ([]uint32, error) { root, err := os.Open(root) if err != nil { From 665682a2ddda0c8f18b7bdcd86facffd5c3f06cb Mon Sep 17 00:00:00 2001 From: kvs vishnu kumar Date: Wed, 23 Sep 2026 21:07:22 +0530 Subject: [PATCH 2/4] cache the bootTime of the host (cherry picked from commit 8c147002075082d60b91a0554a219f12d9ce8db0) --- proc/proc.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/proc/proc.go b/proc/proc.go index fa2afa49..0aa00c8e 100644 --- a/proc/proc.go +++ b/proc/proc.go @@ -8,12 +8,17 @@ import ( "path" "strconv" "strings" + "sync" "time" "github.com/coroot/coroot-node-agent/cgroup" ) -var root = "/proc" +var ( + root = "/proc" + bootTimeLock sync.Mutex + cachedBootTime int64 +) func Path(pid uint32, subpath ...string) string { return path.Join(append([]string{root, strconv.Itoa(int(pid))}, subpath...)...) @@ -92,6 +97,11 @@ func GetStartTime(pid uint32) time.Time { } func bootTime() (int64, error) { + bootTimeLock.Lock() + defer bootTimeLock.Unlock() + if cachedBootTime != 0 { + return cachedBootTime, nil + } data, err := os.ReadFile(root + "/stat") if err != nil { return 0, err @@ -104,7 +114,12 @@ func bootTime() (int64, error) { if len(fields) != 2 { return 0, fmt.Errorf("invalid btime line in /proc/stat: %q", line) } - return strconv.ParseInt(fields[1], 10, 64) + btime, err := strconv.ParseInt(fields[1], 10, 64) + if err != nil { + return 0, err + } + cachedBootTime = btime + return btime, nil } return 0, fmt.Errorf("btime not found in /proc/stat") } From 75f0a193e3d00ed4434d28cd535476c8ff4886fd Mon Sep 17 00:00:00 2001 From: Nikolay Sivko Date: Wed, 30 Sep 2026 14:33:41 -0300 Subject: [PATCH 3/4] proc: read the boot time once in init() instead of a lazily cached lookup (cherry picked from commit a9fd10174a90f5ea515896a2e5c0f0586dc01701) --- proc/proc.go | 56 ++++++++++++++++------------------------------------ 1 file changed, 17 insertions(+), 39 deletions(-) diff --git a/proc/proc.go b/proc/proc.go index 0aa00c8e..df00a6b4 100644 --- a/proc/proc.go +++ b/proc/proc.go @@ -8,18 +8,29 @@ import ( "path" "strconv" "strings" - "sync" "time" "github.com/coroot/coroot-node-agent/cgroup" ) var ( - root = "/proc" - bootTimeLock sync.Mutex - cachedBootTime int64 + root = "/proc" + bootTime int64 ) +func init() { + data, err := os.ReadFile(root + "/stat") + if err != nil { + return + } + for _, line := range strings.Split(string(data), "\n") { + if fields := strings.Fields(line); len(fields) == 2 && fields[0] == "btime" { + bootTime, _ = strconv.ParseInt(fields[1], 10, 64) + return + } + } +} + func Path(pid uint32, subpath ...string) string { return path.Join(append([]string{root, strconv.Itoa(int(pid))}, subpath...)...) } @@ -70,7 +81,6 @@ func ReadCgroup(pid uint32) (*cgroup.Cgroup, error) { return cgroup.NewFromProcessCgroupFile(Path(pid, "cgroup")) } -// GetStartTime returns the process start time derived from /proc//stat func GetStartTime(pid uint32) time.Time { data, err := os.ReadFile(Path(pid, "stat")) if err != nil { @@ -86,42 +96,10 @@ func GetStartTime(pid uint32) time.Time { return time.Time{} } startTicks, err := strconv.ParseUint(fields[19], 10, 64) - if err != nil { + if err != nil || bootTime == 0 { return time.Time{} } - btime, err := bootTime() - if err != nil { - return time.Time{} - } - return time.Unix(btime+int64(float64(startTicks)/100), 0) -} - -func bootTime() (int64, error) { - bootTimeLock.Lock() - defer bootTimeLock.Unlock() - if cachedBootTime != 0 { - return cachedBootTime, nil - } - data, err := os.ReadFile(root + "/stat") - if err != nil { - return 0, err - } - for _, line := range strings.Split(string(data), "\n") { - if !strings.HasPrefix(line, "btime ") { - continue - } - fields := strings.Fields(line) - if len(fields) != 2 { - return 0, fmt.Errorf("invalid btime line in /proc/stat: %q", line) - } - btime, err := strconv.ParseInt(fields[1], 10, 64) - if err != nil { - return 0, err - } - cachedBootTime = btime - return btime, nil - } - return 0, fmt.Errorf("btime not found in /proc/stat") + return time.Unix(bootTime+int64(float64(startTicks)/100), 0) } func ListPids() ([]uint32, error) { From dc03fca8880ca0858221bba7066af761bd8315d7 Mon Sep 17 00:00:00 2001 From: mayankpande88 Date: Thu, 8 Oct 2026 17:39:57 +0530 Subject: [PATCH 4/4] test(proc): cover GetStartTime Start ticks from field 22 of /proc//stat plus the boot time, a comm containing spaces and ')', a missing pid and an unknown boot time. --- proc/fixtures/123/stat | 1 + proc/start_time_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 proc/fixtures/123/stat create mode 100644 proc/start_time_test.go diff --git a/proc/fixtures/123/stat b/proc/fixtures/123/stat new file mode 100644 index 00000000..2b95a181 --- /dev/null +++ b/proc/fixtures/123/stat @@ -0,0 +1 @@ +123 (my (odd) proc) S 1 4242 4242 0 -1 4194560 100 0 0 0 5 3 0 0 20 0 1 0 12345 1000000 200 18446744073709551615 0 0 0 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 diff --git a/proc/start_time_test.go b/proc/start_time_test.go new file mode 100644 index 00000000..b5be80e7 --- /dev/null +++ b/proc/start_time_test.go @@ -0,0 +1,24 @@ +package proc + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestGetStartTime(t *testing.T) { + saved := bootTime + defer func() { bootTime = saved }() + bootTime = 1700000000 + + // starttime is field 22 of /proc//stat, in clock ticks since boot: + // 12345 ticks = 123.45s. The comm contains spaces and ')', so fields + // must be counted from the last ')'. + assert.Equal(t, time.Unix(1700000000+123, 0), GetStartTime(123)) + + assert.True(t, GetStartTime(999999).IsZero(), "missing pid") + + bootTime = 0 + assert.True(t, GetStartTime(123).IsZero(), "unknown boot time") +}