You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@skywalking.apache.org by li...@apache.org on 2026/07/06 05:30:30 UTC

(skywalking-rover) branch main updated: Fix some issue from logs, panics (#212)

This is an automated email from the ASF dual-hosted git repository.

mrproliu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-rover.git


The following commit(s) were added to refs/heads/main by this push:
     new b77436d  Fix some issue from logs, panics (#212)
b77436d is described below

commit b77436d99566aae0f766358d359145c77073ef11
Author: mrproliu <74...@qq.com>
AuthorDate: Mon Jul 6 13:30:25 2026 +0800

    Fix some issue from logs, panics (#212)
---
 CHANGES.md                                         |   6 +
 configs/rover_configs.yaml                         |   6 +-
 docs/en/setup/configuration/pprof.md               |   9 +-
 pkg/accesslog/collector/protocols/http1.go         |   5 +-
 pkg/accesslog/collector/protocols/http2.go         |  12 +-
 pkg/accesslog/collector/protocols/http2_test.go    | 186 +++++++++++++++++++++
 pkg/accesslog/collector/protocols/protocol.go      |  18 +-
 pkg/pprof/config.go                                |   5 +-
 pkg/pprof/module.go                                |   2 +-
 pkg/process/api/process.go                         |   2 +
 pkg/process/finders/base/tool.go                   |  10 ++
 pkg/process/finders/kubernetes/finder.go           |   2 +-
 pkg/process/finders/kubernetes/process.go          |  41 +++--
 pkg/tools/btf/linker.go                            |   4 +-
 pkg/tools/btf/lostsamples.go                       |  75 +++++++++
 .../config.go => tools/btf/lostsamples_test.go}    |  23 ++-
 pkg/tools/process/process.go                       |  34 ++--
 17 files changed, 394 insertions(+), 46 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 6b72e40..63f97d4 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -27,10 +27,16 @@ Release Notes.
 * Support detect ztunnel environment in the inbound request.
 * Increase the transmit buffer size in the network profiling and access log module.
 * Reduce eBPF verifier complexity by converting hot inline helpers to BPF-to-BPF calls, fixing the "program too large" failure when loading access log programs on newer kernels.
+* Reduce memory by not retaining ELF symbols for processes that are not being profiled.
+* Increase the default access log protocol per-CPU buffer to 1MB to reduce dropped samples.
+* Aggregate dropped perf event sample warnings periodically instead of logging every burst.
+* Lower the log level for connection query errors of short-lived exited processes.
+* Enable pprof by default but bind it to 127.0.0.1 so it is not exposed on the network.
 
 #### Bug Fixes
 * Fix the base image cannot run in the arm64.
 * Fix process fork tracepoint reporting thread TID instead of process TGID, causing repeated process detected/dead churn.
+* Fix panic in the access log module when handling HTTP/2 streams without a body.
 
 #### Documentation
 * Add a dead link checker in the CI.
diff --git a/configs/rover_configs.yaml b/configs/rover_configs.yaml
index c497c16..a5a6b0c 100644
--- a/configs/rover_configs.yaml
+++ b/configs/rover_configs.yaml
@@ -151,7 +151,7 @@ access_log:
     queue_size: ${ROVER_ACCESS_LOG_CONNECTION_ANALYZE_QUEUE_SIZE:2000}
   protocol_analyze:
     # The size of socket data buffer on each CPU
-    per_cpu_buffer: ${ROVER_ACCESS_LOG_PROTOCOL_ANALYZE_PER_CPU_BUFFER:400KB}
+    per_cpu_buffer: ${ROVER_ACCESS_LOG_PROTOCOL_ANALYZE_PER_CPU_BUFFER:1MB}
     # The count of parallel protocol event parse
     parse_parallels: ${ROVER_ACCESS_LOG_PROTOCOL_ANALYZE_PARSE_PARALLELS:2}
     # The count of parallel protocol analyzer
@@ -161,6 +161,8 @@ access_log:
 
 pprof:
   # Is active the pprof
-  active: ${ROVER_PPROF_ACTIVE:false}
+  active: ${ROVER_PPROF_ACTIVE:true}
+  # The bind host of the pprof HTTP server, defaults to 127.0.0.1 so it only listens on the local host
+  host: ${ROVER_PPROF_HOST:127.0.0.1}
   # The bind port of the pprof HTTP server
   port: ${ROVER_PPROF_PORT:6060}
\ No newline at end of file
diff --git a/docs/en/setup/configuration/pprof.md b/docs/en/setup/configuration/pprof.md
index 98e5675..51c5f59 100644
--- a/docs/en/setup/configuration/pprof.md
+++ b/docs/en/setup/configuration/pprof.md
@@ -4,10 +4,11 @@ Pprof is a feature to collect self runtime profiling data through `pprof` module
 
 ## Configuration
 
-| Name      | Default | Environment Key      | Description                         |
-|-----------|---------|----------------------|-------------------------------------|
-| `enabled` | `false` | `ROVER_PPROF_ACTIVE` | Enable pprof module.                |
-| `port`    | `6060`  | `ROVER_PPROF_PORT`   | The HTTP port to expose pprof data. |
+| Name      | Default     | Environment Key      | Description                                                                        |
+|-----------|-------------|----------------------|------------------------------------------------------------------------------------|
+| `enabled` | `true`      | `ROVER_PPROF_ACTIVE` | Enable pprof module.                                                                |
+| `host`    | `127.0.0.1` | `ROVER_PPROF_HOST`   | The bind host of the pprof HTTP server, only listens on the local host by default. |
+| `port`    | `6060`      | `ROVER_PPROF_PORT`   | The HTTP port to expose pprof data.                                                |
 
 ## Expose Paths
 
diff --git a/pkg/accesslog/collector/protocols/http1.go b/pkg/accesslog/collector/protocols/http1.go
index 8a76a2c..c24b1bc 100644
--- a/pkg/accesslog/collector/protocols/http1.go
+++ b/pkg/accesslog/collector/protocols/http1.go
@@ -220,10 +220,11 @@ func (p *HTTP1Protocol) HandleHTTPData(metrics *HTTP1Metrics, _ *PartitionConnec
 	details, idRange, allInclude = AppendSocketDetailsFromBuffer(details, response.HeaderBuffer(), idRange, allInclude)
 	details, idRange, allInclude = AppendSocketDetailsFromBuffer(details, response.BodyBuffer(), idRange, allInclude)
 
-	if !allInclude {
+	if !allInclude || idRange == nil || len(details) == 0 {
+		from, to := DataIDRangeBounds(idRange)
 		return fmt.Errorf("cannot found full detail events for HTTP/1.x protocol, "+
 			"data id: %d-%d, current details count: %d",
-			idRange.From, idRange.To, len(details))
+			from, to, len(details))
 	}
 
 	http1Log.Debugf("found fully HTTP1 request and response, contains %d detail events, "+
diff --git a/pkg/accesslog/collector/protocols/http2.go b/pkg/accesslog/collector/protocols/http2.go
index 0db9741..3e3aeb3 100644
--- a/pkg/accesslog/collector/protocols/http2.go
+++ b/pkg/accesslog/collector/protocols/http2.go
@@ -245,17 +245,19 @@ func (r *HTTP2Protocol) validateIsStreamOpenTooLong(connection *PartitionConnect
 
 func (r *HTTP2Protocol) HandleWholeStream(_ *PartitionConnection, stream *HTTP2Streaming) error {
 	details := make([]events.SocketDetail, 0)
-	var allInclude = true
+	allInclude := true
 	var idRange *buffer.DataIDRange
 	details, idRange, allInclude = AppendSocketDetailsFromBuffer(details, stream.ReqHeaderBuffer, idRange, allInclude)
 	details, idRange, allInclude = AppendSocketDetailsFromBuffer(details, stream.ReqBodyBuffer, idRange, allInclude)
 	details, idRange, allInclude = AppendSocketDetailsFromBuffer(details, stream.RespHeaderBuffer, idRange, allInclude)
 	details, idRange, allInclude = AppendSocketDetailsFromBuffer(details, stream.RespBodyBuffer, idRange, allInclude)
 
-	if !allInclude {
-		return fmt.Errorf("cannot found any detail events for HTTP/2 protocol, data id: %d-%d, current details count: %d",
-			stream.ReqHeaderBuffer.FirstSocketBuffer().DataID(), stream.RespBodyBuffer.LastSocketBuffer().DataID(),
-			len(details))
+	// idRange is nil / details empty when no buffer contributed any event (e.g. all buffers absent);
+	// guard it so the error path never dereferences a nil buffer as it used to (which panicked).
+	if !allInclude || idRange == nil || len(details) == 0 {
+		from, to := DataIDRangeBounds(idRange)
+		return fmt.Errorf("cannot found full detail events for HTTP/2 protocol, data id: %d-%d, current details count: %d",
+			from, to, len(details))
 	}
 	idRange.DeleteDetails(stream.ReqHeaderBuffer)
 
diff --git a/pkg/accesslog/collector/protocols/http2_test.go b/pkg/accesslog/collector/protocols/http2_test.go
new file mode 100644
index 0000000..9d80187
--- /dev/null
+++ b/pkg/accesslog/collector/protocols/http2_test.go
@@ -0,0 +1,186 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package protocols
+
+import (
+	"testing"
+	"time"
+
+	"github.com/apache/skywalking-rover/pkg/accesslog/common"
+	aclevents "github.com/apache/skywalking-rover/pkg/accesslog/events"
+	analyzeevents "github.com/apache/skywalking-rover/pkg/profiling/task/network/analyze/events"
+	"github.com/apache/skywalking-rover/pkg/tools/buffer"
+
+	v3 "skywalking.apache.org/repo/goapi/collect/ebpf/accesslog/v3"
+)
+
+// buildDetailBuffer builds a minimal, fully-captured buffer that holds a single detail event with
+// the given data id, so BuildDetails() is non-empty and BuildTotalDataIDRange() covers it.
+func buildDetailBuffer(dataID, start, end uint64) *buffer.Buffer {
+	b := buffer.NewBuffer()
+	b.AppendDataEvent(&analyzeevents.SocketDataUploadEvent{
+		DataID0: dataID, DataLen: 1, StartTime0: start, EndTime0: end, Finished: 1,
+	})
+	b.AppendDetailEvent(&aclevents.SocketDetailEvent{DataID0: dataID, StartTime: start, EndTime: end})
+	b.PrepareForReading()
+	return b
+}
+
+type noopQueueConsumer struct{}
+
+func (noopQueueConsumer) Consume(_ chan common.KernelLog, _ chan common.ProtocolLog) {}
+
+// TestAppendSocketDetailsFromBuffer covers the buffer-classification logic at the heart of the panic
+// fix: a nil (absent) buffer is skipped, a present-but-empty buffer is incomplete, and a
+// fully-captured buffer contributes its details.
+func TestAppendSocketDetailsFromBuffer(t *testing.T) {
+	// a nil buffer with allInclude=true must be skipped, not marked incomplete (the core fix).
+	if _, idRange, include := AppendSocketDetailsFromBuffer(nil, nil, nil, true); !include || idRange != nil {
+		t.Fatalf("nil buffer should be skipped: include=%v idRange=%v", include, idRange)
+	}
+	// once incomplete, a following nil buffer keeps the stream incomplete.
+	if _, _, include := AppendSocketDetailsFromBuffer(nil, nil, nil, false); include {
+		t.Fatal("incomplete state must be propagated through a nil buffer")
+	}
+	// a present-but-empty buffer is treated as incomplete.
+	if _, _, include := AppendSocketDetailsFromBuffer(nil, buffer.NewBuffer(), nil, true); include {
+		t.Fatal("empty buffer should be marked incomplete")
+	}
+	// a present buffer whose details fall outside its captured data-id range (e.g. dropped perf
+	// samples left a gap) is incomplete, not skipped.
+	incomplete := buffer.NewBuffer()
+	incomplete.AppendDataEvent(&analyzeevents.SocketDataUploadEvent{DataID0: 1, DataLen: 1, Finished: 1})
+	incomplete.AppendDetailEvent(&aclevents.SocketDetailEvent{DataID0: 5})
+	incomplete.PrepareForReading()
+	if _, _, include := AppendSocketDetailsFromBuffer(nil, incomplete, nil, true); include {
+		t.Fatal("buffer with details outside its data-id range should be incomplete")
+	}
+
+	// a fully-captured buffer contributes its detail and stays complete.
+	details, idRange, include := AppendSocketDetailsFromBuffer(nil, buildDetailBuffer(1, 10, 20), nil, true)
+	if !include || len(details) != 1 || idRange == nil || idRange.From != 1 || idRange.To != 1 {
+		t.Fatalf("complete buffer: include=%v len=%d idRange=%v", include, len(details), idRange)
+	}
+}
+
+func TestDataIDRangeBounds(t *testing.T) {
+	if from, to := DataIDRangeBounds(nil); from != 0 || to != 0 {
+		t.Fatalf("nil range must be (0,0), got (%d,%d)", from, to)
+	}
+	if from, to := DataIDRangeBounds(&buffer.DataIDRange{From: 3, To: 9}); from != 3 || to != 9 {
+		t.Fatalf("expected (3,9), got (%d,%d)", from, to)
+	}
+}
+
+// TestHTTP2HandleWholeStreamIncompleteNoPanic guards against the SIGSEGV that used to happen when a
+// stream reached HandleWholeStream without a fully-captured set of detail events. The error path
+// previously dereferenced possibly-nil buffers (stream.RespBodyBuffer.LastSocketBuffer().DataID());
+// it must now return an error safely instead of panicking.
+func TestHTTP2HandleWholeStreamIncompleteNoPanic(t *testing.T) {
+	r := &HTTP2Protocol{}
+	tests := []struct {
+		name   string
+		stream *HTTP2Streaming
+	}{
+		{
+			name:   "all buffers absent",
+			stream: &HTTP2Streaming{ReqHeader: map[string]string{}, RespHeader: map[string]string{}},
+		},
+		{
+			name: "present but empty request header buffer, bodies absent",
+			stream: &HTTP2Streaming{
+				ReqHeader:       map[string]string{},
+				RespHeader:      map[string]string{},
+				ReqHeaderBuffer: buffer.NewBuffer(),
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			err := r.HandleWholeStream(nil, test.stream)
+			if err == nil {
+				t.Fatal("expected an error when detail events are incomplete, got nil")
+			}
+		})
+	}
+}
+
+// TestHTTP2HandleWholeStreamReportsWhenBodiless proves the root-cause fix: a stream with request and
+// response headers but no bodies (e.g. a GET with a 204/304 response that ends on the HEADERS frame)
+// must now be reported instead of being dropped/panicking on the nil body buffers.
+func TestHTTP2HandleWholeStreamReportsWhenBodiless(t *testing.T) {
+	r := &HTTP2Protocol{ctx: &common.AccessLogContext{
+		Queue: common.NewQueue(100, time.Minute, noopQueueConsumer{}),
+	}}
+	stream := &HTTP2Streaming{
+		ReqHeader:        map[string]string{":path": "/", ":authority": "example"},
+		RespHeader:       map[string]string{":status": "204"},
+		ReqHeaderBuffer:  buildDetailBuffer(1, 10, 20),
+		RespHeaderBuffer: buildDetailBuffer(2, 30, 40),
+		// ReqBodyBuffer and RespBodyBuffer are intentionally nil (bodiless).
+	}
+	if err := r.HandleWholeStream(nil, stream); err != nil {
+		t.Fatalf("bodiless stream should be reported, got error: %v", err)
+	}
+}
+
+func TestHTTP2ParseHTTPMethod(t *testing.T) {
+	r := &HTTP2Protocol{}
+	tests := []struct {
+		method string
+		expect v3.AccessLogHTTPProtocolRequestMethod
+	}{
+		{"", v3.AccessLogHTTPProtocolRequestMethod_Get},      // absent method defaults to GET
+		{"post", v3.AccessLogHTTPProtocolRequestMethod_Post}, // lower-cased method is upper-cased first
+		{"patch", v3.AccessLogHTTPProtocolRequestMethod_Patch},
+		{"weird", v3.AccessLogHTTPProtocolRequestMethod_Get}, // unknown method falls back to GET
+	}
+	for _, test := range tests {
+		stream := &HTTP2Streaming{ReqHeader: map[string]string{}}
+		if test.method != "" {
+			stream.ReqHeader[":method"] = test.method
+		}
+		if got := r.ParseHTTPMethod(stream); got != test.expect {
+			t.Fatalf("method %q: expected %v, got %v", test.method, test.expect, got)
+		}
+	}
+}
+
+func TestHTTP2FirstDetail(t *testing.T) {
+	r := &HTTP2Protocol{}
+	def := &aclevents.SocketDetailEvent{DataID0: 99}
+	if got := r.FirstDetail(nil, def); got != def {
+		t.Fatal("nil buffer should return the default detail")
+	}
+	if got := r.FirstDetail(buffer.NewBuffer(), def); got != def {
+		t.Fatal("empty buffer should return the default detail")
+	}
+	if got := r.FirstDetail(buildDetailBuffer(7, 1, 2), def); got == def || got.DataID() != 7 {
+		t.Fatalf("present buffer should return its own first detail, got DataID=%d", got.DataID())
+	}
+}
+
+func TestHTTP2BufferSizeOfZero(t *testing.T) {
+	r := &HTTP2Protocol{}
+	if size := r.BufferSizeOfZero(nil); size != 0 {
+		t.Fatalf("nil buffer size must be 0, got %d", size)
+	}
+	if size := r.BufferSizeOfZero(buildDetailBuffer(1, 1, 2)); size == 0 {
+		t.Fatal("present buffer size must be non-zero")
+	}
+}
diff --git a/pkg/accesslog/collector/protocols/protocol.go b/pkg/accesslog/collector/protocols/protocol.go
index 673796c..63d68d8 100644
--- a/pkg/accesslog/collector/protocols/protocol.go
+++ b/pkg/accesslog/collector/protocols/protocol.go
@@ -58,9 +58,16 @@ type Protocol interface {
 
 func AppendSocketDetailsFromBuffer(result []events.SocketDetail, buf *buffer.Buffer, dataIDRange *buffer.DataIDRange,
 	allDetailInclude bool) ([]events.SocketDetail, *buffer.DataIDRange, bool) {
-	if buf == nil || !allDetailInclude {
+	// a previous buffer was already incomplete: keep propagating the incompleteness.
+	if !allDetailInclude {
 		return result, dataIDRange, false
 	}
+	// a nil buffer means this part of the message is legitimately absent (a request without body,
+	// or a response that ends the stream on the HEADERS frame such as 204/304/redirect or a gRPC
+	// trailers-only response): skip it rather than treating the whole message as incomplete.
+	if buf == nil {
+		return result, dataIDRange, true
+	}
 	details := buf.BuildDetails()
 	if details == nil || details.Len() == 0 {
 		return result, dataIDRange, false
@@ -81,6 +88,15 @@ func AppendSocketDetailsFromBuffer(result []events.SocketDetail, buf *buffer.Buf
 	return result, dataIDRange.Append(currentDataIDRange), true
 }
 
+// DataIDRangeBounds returns the from/to data id bounds of the range, or (0, 0) when it is nil, so
+// callers can build an error message without dereferencing a possibly-nil range.
+func DataIDRangeBounds(dataIDRange *buffer.DataIDRange) (from, to uint64) {
+	if dataIDRange != nil {
+		from, to = dataIDRange.From, dataIDRange.To
+	}
+	return
+}
+
 func AnalyzeTraceInfo(fetcher func(key string) string, protocolLog *logger.Logger) *v3.AccessLogTraceInfo {
 	context, err := tracing.AnalyzeTracingContext(func(key string) string {
 		return fetcher(key)
diff --git a/pkg/pprof/config.go b/pkg/pprof/config.go
index 496957c..9f29e83 100644
--- a/pkg/pprof/config.go
+++ b/pkg/pprof/config.go
@@ -22,7 +22,10 @@ import "github.com/apache/skywalking-rover/pkg/module"
 type Config struct {
 	module.Config `mapstructure:",squash"`
 
-	Port int `mapstructure:"port"`
+	// Host is the bind address of the pprof HTTP server. It defaults to 127.0.0.1 so the debug
+	// endpoints are only reachable from the local host and are not exposed on the network.
+	Host string `mapstructure:"host"`
+	Port int    `mapstructure:"port"`
 }
 
 func (c *Config) IsActive() bool {
diff --git a/pkg/pprof/module.go b/pkg/pprof/module.go
index 8505db9..39684b6 100644
--- a/pkg/pprof/module.go
+++ b/pkg/pprof/module.go
@@ -67,7 +67,7 @@ func (m *Module) Start(_ context.Context, mgr *module.Manager) error {
 	defer m.mutex.Unlock()
 
 	m.server = &http.Server{
-		Addr:              fmt.Sprintf(":%d", m.config.Port),
+		Addr:              fmt.Sprintf("%s:%d", m.config.Host, m.config.Port),
 		ReadHeaderTimeout: 3 * time.Second,
 		Handler:           mux,
 	}
diff --git a/pkg/process/api/process.go b/pkg/process/api/process.go
index aed6d18..75a09ba 100644
--- a/pkg/process/api/process.go
+++ b/pkg/process/api/process.go
@@ -78,6 +78,8 @@ type DetectedProcess interface {
 	DetectType() ProcessDetectType
 	// ProfilingStat of process
 	ProfilingStat() *profiling.Info
+	// SupportProfiling reports whether the process can be profiled without retaining its symbols
+	SupportProfiling() bool
 	// ExposePorts define which ports are exposed
 	ExposePorts() []int
 	// ExposeHosts define which hosts are exposed
diff --git a/pkg/process/finders/base/tool.go b/pkg/process/finders/base/tool.go
index f2413be..725cf61 100644
--- a/pkg/process/finders/base/tool.go
+++ b/pkg/process/finders/base/tool.go
@@ -44,6 +44,16 @@ func BuildProfilingStat(ps *process.Process) (*profiling.Info, error) {
 	return process_tool.ProfilingStat(ps.Pid, exePath)
 }
 
+// SupportProfiling reports whether the process's executable can be profiled without retaining the
+// parsed symbol data in memory (see process_tool.SupportProfiling).
+func SupportProfiling(ps *process.Process) (bool, error) {
+	exePath := tryToFindFileExecutePath(ps)
+	if exePath == "" {
+		return false, fmt.Errorf("could not found executable file")
+	}
+	return process_tool.SupportProfiling(exePath)
+}
+
 func tryToFindFileExecutePath(ps *process.Process) string {
 	exe, err := ps.Exe()
 	if err != nil {
diff --git a/pkg/process/finders/kubernetes/finder.go b/pkg/process/finders/kubernetes/finder.go
index 21eae18..2edfbbf 100644
--- a/pkg/process/finders/kubernetes/finder.go
+++ b/pkg/process/finders/kubernetes/finder.go
@@ -350,7 +350,7 @@ func (f *ProcessFinder) BuildNecessaryProperties(ps api.DetectedProcess) []*comm
 	return []*commonv3.KeyStringValuePair{
 		{
 			Key:   "support_ebpf_profiling",
-			Value: strconv.FormatBool(ps.ProfilingStat() != nil),
+			Value: strconv.FormatBool(ps.SupportProfiling()),
 		},
 	}
 }
diff --git a/pkg/process/finders/kubernetes/process.go b/pkg/process/finders/kubernetes/process.go
index bf20683..c96e47a 100644
--- a/pkg/process/finders/kubernetes/process.go
+++ b/pkg/process/finders/kubernetes/process.go
@@ -18,6 +18,9 @@
 package kubernetes
 
 import (
+	"errors"
+	"os"
+	"strings"
 	"sync"
 
 	"github.com/shirou/gopsutil/process"
@@ -31,11 +34,11 @@ type Process struct {
 	original *process.Process
 
 	// process data
-	pid           int32
-	cmd           string
-	profilingOnce sync.Once
-	profiling     *profiling.Info
-	podContainer  *PodContainer
+	pid                  int32
+	cmd                  string
+	supportProfilingOnce sync.Once
+	supportProfiling     bool
+	podContainer         *PodContainer
 
 	// entity for the backend
 	entity *api.ProcessEntity
@@ -67,12 +70,22 @@ func (p *Process) DetectType() api.ProcessDetectType {
 	return api.Kubernetes
 }
 
+// ProfilingStat builds the full profiling info (heavy: parses and retains the ELF symbol tables).
+// It intentionally does NOT cache the result on the process: the info is only needed while a
+// profiling task runs and is held by that task's runner, so it is released once the task ends.
 func (p *Process) ProfilingStat() *profiling.Info {
-	p.profilingOnce.Do(func() {
-		stat, _ := base.BuildProfilingStat(p.original)
-		p.profiling = stat
+	stat, _ := base.BuildProfilingStat(p.original)
+	return stat
+}
+
+// SupportProfiling reports whether the process can be profiled, WITHOUT retaining the symbol data.
+// The result is a bool that is cheap to cache, so keep-alive reporting does not re-parse the ELF
+// symbol tables of every discovered process on each report.
+func (p *Process) SupportProfiling() bool {
+	p.supportProfilingOnce.Do(func() {
+		p.supportProfiling, _ = base.SupportProfiling(p.original)
 	})
-	return p.profiling
+	return p.supportProfiling
 }
 
 func (p *Process) PodContainer() *PodContainer {
@@ -89,7 +102,15 @@ func (p *Process) ExposePorts() []int {
 	}
 	connections, err := p.original.Connections()
 	if err != nil {
-		log.Warnf("query the process connection error: pid: %d, error: %v", p.pid, err)
+		// A short-lived process may exit between discovery and this query, making its
+		// /proc/<pid>/net/* files disappear. That is an expected race for ephemeral processes
+		// (e.g. curl/sleep from load generators), not a real error, so keep it at debug level
+		// to avoid flooding the log.
+		if errors.Is(err, os.ErrNotExist) || strings.Contains(err.Error(), "no such file or directory") {
+			log.Debugf("skip querying connections for the exited process, pid: %d, error: %v", p.pid, err)
+		} else {
+			log.Warnf("query the process connection error: pid: %d, error: %v", p.pid, err)
+		}
 		return result
 	}
 	for _, c := range connections {
diff --git a/pkg/tools/btf/linker.go b/pkg/tools/btf/linker.go
index 3abb43b..271a10d 100644
--- a/pkg/tools/btf/linker.go
+++ b/pkg/tools/btf/linker.go
@@ -199,7 +199,9 @@ func (m *Linker) asyncReadEvent(rd *perf.Reader, emap *ebpf.Map, recordPool *per
 			}
 
 			if record.LostSamples != 0 {
-				log.Warnf("perf event queue(%s) full, dropped %d samples", emap.String(), record.LostSamples)
+				// accumulate and report in an aggregated, periodic summary instead of logging
+				// every burst on its own (see lostsamples.go).
+				recordLostSamples(emap.String(), record.LostSamples)
 				recordPool.PutRecord(record)
 				continue
 			}
diff --git a/pkg/tools/btf/lostsamples.go b/pkg/tools/btf/lostsamples.go
new file mode 100644
index 0000000..8114fd9
--- /dev/null
+++ b/pkg/tools/btf/lostsamples.go
@@ -0,0 +1,75 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package btf
+
+import (
+	"sync"
+	"time"
+)
+
+// lostSampleLogInterval is how often the aggregated dropped-sample summary is logged. The kernel
+// drops samples in bursts, so logging every single occurrence floods the log; instead the counts
+// are accumulated and reported once per interval.
+const lostSampleLogInterval = 30 * time.Second
+
+var lostSamples = &lostSampleCounter{counts: make(map[string]uint64)}
+
+// lostSampleCounter accumulates the number of perf event samples the kernel dropped (because the
+// per-CPU buffer was full and userspace did not drain it in time), keyed by the perf map name.
+type lostSampleCounter struct {
+	mutex       sync.Mutex
+	counts      map[string]uint64
+	loggerStart sync.Once
+}
+
+// recordLostSamples accumulates dropped samples for a perf map and lazily starts the periodic
+// aggregated logger on first loss.
+func recordLostSamples(mapName string, count uint64) {
+	lostSamples.mutex.Lock()
+	lostSamples.counts[mapName] += count
+	lostSamples.mutex.Unlock()
+
+	lostSamples.loggerStart.Do(func() {
+		go lostSamples.logPeriodically()
+	})
+}
+
+// drain atomically returns the samples accumulated since the last drain and resets the counter, so
+// each reporting interval directly reads the newly-dropped counts without tracking previous totals,
+// and the map does not grow unbounded.
+func (c *lostSampleCounter) drain() map[string]uint64 {
+	c.mutex.Lock()
+	defer c.mutex.Unlock()
+	if len(c.counts) == 0 {
+		return nil
+	}
+	drained := c.counts
+	c.counts = make(map[string]uint64)
+	return drained
+}
+
+func (c *lostSampleCounter) logPeriodically() {
+	ticker := time.NewTicker(lostSampleLogInterval)
+	defer ticker.Stop()
+	for range ticker.C {
+		for name, dropped := range c.drain() {
+			log.Warnf("perf event queue(%s) dropped %d samples in the last %s, "+
+				"consider increasing the per_cpu_buffer or parse_parallels", name, dropped, lostSampleLogInterval)
+		}
+	}
+}
diff --git a/pkg/pprof/config.go b/pkg/tools/btf/lostsamples_test.go
similarity index 61%
copy from pkg/pprof/config.go
copy to pkg/tools/btf/lostsamples_test.go
index 496957c..35028d1 100644
--- a/pkg/pprof/config.go
+++ b/pkg/tools/btf/lostsamples_test.go
@@ -15,16 +15,23 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package pprof
+package btf
 
-import "github.com/apache/skywalking-rover/pkg/module"
+import "testing"
 
-type Config struct {
-	module.Config `mapstructure:",squash"`
+func TestLostSampleCounterDrain(t *testing.T) {
+	c := &lostSampleCounter{counts: make(map[string]uint64)}
+	c.counts["q1"] += 3
+	c.counts["q1"] += 2
+	c.counts["q2"] += 7
 
-	Port int `mapstructure:"port"`
-}
+	drained := c.drain()
+	if drained["q1"] != 5 || drained["q2"] != 7 {
+		t.Fatalf("unexpected drained counts: %v", drained)
+	}
 
-func (c *Config) IsActive() bool {
-	return c.Active
+	// after a drain the counter must be reset, so a subsequent drain with no new samples is empty
+	if again := c.drain(); again != nil {
+		t.Fatalf("expected nil after reset, got: %v", again)
+	}
 }
diff --git a/pkg/tools/process/process.go b/pkg/tools/process/process.go
index 87930e2..31dac89 100644
--- a/pkg/tools/process/process.go
+++ b/pkg/tools/process/process.go
@@ -61,26 +61,40 @@ func KernelFileProfilingStat() (*profiling.Info, error) {
 	return kernelFinder.Analyze(profiling.KernelProcSymbolFilePath)
 }
 
-// ProfilingStat is validating the exe file could be profiling and get info
-func ProfilingStat(pid int32, exePath string) (*profiling.Info, error) {
+// SupportProfiling reports whether the executable can be profiled (not excluded and has a usable
+// symbol table) WITHOUT retaining the parsed symbol/module data in memory. Use this for the
+// discovery-time "support_ebpf_profiling" flag: the heavy symbol data is only needed while an
+// actual profiling task runs and is built on demand by ProfilingStat.
+func SupportProfiling(exePath string) (bool, error) {
 	stat, err := os.Stat(exePath)
 	if err != nil {
-		return nil, fmt.Errorf("check file error: %v", err)
+		return false, fmt.Errorf("check file error: %v", err)
 	}
 	for _, notSupport := range NotSupportProfilingExe {
 		if strings.HasPrefix(stat.Name(), notSupport) {
-			return nil, fmt.Errorf("not support %s language profiling", notSupport)
+			return false, fmt.Errorf("not support %s language profiling", notSupport)
 		}
 	}
-	context := newAnalyzeContext()
 
-	// the executable file must have the symbols
-	symbols, err := context.GetFinder(exePath).AnalyzeSymbols(exePath)
-	if err != nil || len(symbols) == 0 {
-		return nil, fmt.Errorf("could not found any symbol in the execute file: %s, error: %v", exePath, err)
+	// the executable file must have the symbols; the parsed symbols are discarded here so they
+	// are not retained in memory for every discovered process.
+	symbols, err := newAnalyzeContext().GetFinder(exePath).AnalyzeSymbols(exePath)
+	if err != nil {
+		return false, fmt.Errorf("could not analyze symbols in the executable file %s: %w", exePath, err)
+	}
+	if len(symbols) == 0 {
+		return false, fmt.Errorf("could not find any symbol in the executable file: %s", exePath)
+	}
+	return true, nil
+}
+
+// ProfilingStat is validating the exe file could be profiling and get info
+func ProfilingStat(pid int32, exePath string) (*profiling.Info, error) {
+	if support, err := SupportProfiling(exePath); !support {
+		return nil, err
 	}
 
-	return analyzeProfilingInfo(context, pid)
+	return analyzeProfilingInfo(newAnalyzeContext(), pid)
 }
 
 // Modules Read the profiling info of the process, without the symbol check