Skip to content

feat: record stacktrace for objects allocated in stack#76

Open
Lslightly wants to merge 9 commits into
cloudwego:mainfrom
Lslightly:stackvar-location
Open

feat: record stacktrace for objects allocated in stack#76
Lslightly wants to merge 9 commits into
cloudwego:mainfrom
Lslightly:stackvar-location

Conversation

@Lslightly

@Lslightly Lslightly commented Jun 23, 2026

Copy link
Copy Markdown

What type of PR is this?

feat

Check the PR title.

  • This PR title match the format: <type>(optional scope): <description>
  • The description of this PR title is user-oriented and clear enough for others to understand.
  • Attach the PR updating the user documentation if the current PR requires user awareness at the usage level. User docs repo

(Optional) Translate the PR title into Chinese.

(Optional) More detailed description for this PR(en: English/zh: Chinese).

en: For local variables in stack, the stacktrace is recorded as pprofIndex
zh(optional): 对于栈上局部变量,在pprofIndex中记录栈帧。

Take test/testdata/manygo/main.go as an example. The flamegraph is as follows. There are 3 groups of goroutines whose members share same stacktrace.

Hope that it supplements goroutine leak profile.

image

(Optional) Which issue(s) this PR fixes:

Fixes #65

(optional) The PR that updates user documentation:

@Lslightly

Copy link
Copy Markdown
Author

Hi @ppzqh and @jayantxie. Could you please review this pull request when you have time? I hope this PR resolves issue #65. Please let me know if there is anything that can be improved. Thanks for your time and effort in maintaining this tool!

@ppzqh

ppzqh commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Hi @Lslightly! Thanks for your work. I will review this.
BTW, please fix the error of tests.

@Lslightly

Lslightly commented Jul 8, 2026

Copy link
Copy Markdown
Author

An dummy function named stk_obj_split has been added to separate stacktrace and object reference graph.

I modified tests to use this dummy function to get the old "leaf" node, i.e. the object with the specified rootPrefixes. (Now go test -v -run=TestScenarios ./... passes. go test -v -race -coverprofile=coverage.out -covermode=atomic -coverpkg=github.com/cloudwego/goref/... ./... passes in my machine)

The flamegraph has been changed as follows, with a "stk_obj_split" frame separating stacktrace and object reference.

image

@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.00000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.83%. Comparing base (65e6157) to head (ec948c4).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
pkg/proc/protobuf.go 91.04% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            main      #76       +/-   ##
==========================================
+ Coverage   2.70%   61.83%   +59.13%     
==========================================
  Files         15       16        +1     
  Lines       2221     2319       +98     
==========================================
+ Hits          60     1434     +1374     
+ Misses      2155      726     -1429     
- Partials       6      159      +153     
Flag Coverage Δ
unit 61.83% <92.00%> (+59.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Lslightly

Lslightly commented Jul 8, 2026

Copy link
Copy Markdown
Author

Hi, @ppzqh. I fixed errors of tests. Do I need to add additional tests to increase the coverage rate? It seems that other merged PRs also have "Patch coverage" problem.

@ppzqh

ppzqh commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Yes, you need to add a TestScenario in the testCases. For now, the CI does not cover your added logic.

Comment thread pkg/proc/protobuf.go Outdated
remove G<id>, the bottom of stacktrace
@Lslightly Lslightly changed the title feat: record stacktrace with goroutine id for objects allocated in stack feat: record stacktrace for objects allocated in stack Jul 14, 2026
@Lslightly Lslightly requested a review from ppzqh July 14, 2026 14:27
Comment thread pkg/proc/protobuf.go Outdated
}
// write location
start := b.pb.startMessage()
b.pb.uint64Opt(tagLocation_ID, idx)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Location ID is keyed by function name, while Address and Line are PC-specific. Multiple PCs in the same function may therefore reuse incorrect source information. Since we only need function-level stacks, we can remove the PC/file/line metadata.

@Lslightly Lslightly Jul 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 8ae7f8e, I simplify the stacktrace to function-level stacks and remove the PC/file/line metadata.

If delve provides correct PC/file/line, then adding them allows user to get correct pprof source view. I try to keep my opinion to add them, but I can't figure out the use case for source view now. File/line seems meaningless for a temporary memory snapshot and function is enough.

Comment thread pkg/proc/protobuf.go Outdated

const StackTraceObjSplitLine string = "stk_obj_split"

func createStackTrace(b *profileBuilder, sf []proc.Stackframe, t *proc.Target, g *proc.G) *pprofIndex {

@ppzqh ppzqh Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since only function-level stacks are needed, createStackTrace should only append interned function names to pprofIndex. Keep Location and Function emission in flush, where stringIndex already guarantees deduplication. Then funcNameStrIdxSet is also not needed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides, stack frames and the separator are attribution context, not object-reference edges. Using pushHead makes them consume maxRefDepth and can skip local roots on deep stacks. We might need another helper function of pprofIndex to handle this case.

@Lslightly Lslightly Jul 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep Location and Function emission in flush, where stringIndex already guarantees deduplication. Then funcNameStrIdxSet is also not needed.

8ae7f8e keeps Location and Function emission in flush and removes logic to add PC/file/line in Location.

funcNameStrIdxSet has been removed.

We might need another helper function of pprofIndex to handle this case.

newHeadWithDepth helper function is added and depth of stacktrace node's pprofIndex is set to -1.

Is it better to set the depth to negative stack depth? Depth of ReferenceStackBoundary is 0, then -1, -2, ... The change made to object reference is that depth of objects referenced by local variables increase by 1.

Comment thread pkg/proc/reference.go Outdated
l.Name = sf[i].Current.Fn.Name + "." + l.Name
rv := ToReferenceVariable(l)
s.findRef(rv, nil)
s.findRef(rv, createStackTrace(s.pb, sf[i:], t, gr))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createStackTrace is called for every local variable, repeatedly rebuilding the same stack suffix. We can precompute immutable prefixes once per goroutine, build them in reverse to share suffix nodes, and reuse prefixes[i] for all locals in frame i

@Lslightly Lslightly Jul 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. That's true. We don't need to use pushHead repeatedly for each local variable to find pprofIndex previously built. prefixes[i] is much faster.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8ae7f8e with initStackTrace and pushReferenceStackBoundary

Comment thread pkg/proc/protobuf.go Outdated
depth int
}

const StackTraceObjSplitLine string = "stk_obj_split"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to a clearer one, for example:
const ReferenceStackSeparator string = "[goref:reference-stack-boundary]"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

1. remove file/line/PC metadata.
2. init stacktrace for each goroutine and reuse the stacktrace
@Lslightly Lslightly requested a review from ppzqh July 15, 2026 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Record stack trace for root objects which are local variables

2 participants