fix(profiling): reduce lock contention in greenlet stack unwinding#16657
Conversation
Codeowners resolved as |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5c5362ef90
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
emmettbutler
left a comment
There was a problem hiding this comment.
Release note looks good
…4719d4343fc.yaml Co-authored-by: Emmett Butler <[email protected]>
taegyunkim
left a comment
There was a problem hiding this comment.
@KowalskiThomas Do you have any concern/comment for this PR?
KowalskiThomas
left a comment
There was a problem hiding this comment.
LGTM, thanks for doing this.
|
Resolved my comments to avoid popping this from the MQ. |
|
@KowalskiThomas The next step would be making sure that we clear greenlet task names from the string table. As far as I understand, we're guarded from the cases where we can't find string table entry given a key. |
Yes we do need to do that. I haven't gotten back to my memory leak PR yet since I was working on R&D stuff! |

Summary
unwind_greenlets()into two phases to reduce lock hold time ongreenlet_info_map_lockGreenletSnapshotstructsprocess_vm_readv/copy_type) outside the lockGreenletSnapshotstruct ingreenlets.hto hold the snapshotted stateMotivation
Before this fix,
unwind_greenlets()heldgreenlet_info_map_lockfor the entire duration of stack unwinding across all tracked greenlets. Since every greenlet switch callsupdate_greenlet_frame()under the same lock, applications with many tracked greenlets (e.g. gunicorn + gevent + psycopg2) experienced severe lock contention — greenlet switches stalled waiting for the sampler, leading to connection pool exhaustion and request timeouts.Why asyncio doesn't have this problem
Asyncio task unwinding (
unwind_tasks) doesn't suffer from this because it reads task state directly from CPython internals (linked lists in 3.14+, orWeakSetin older versions) viacopy_type()— no profiler-owned lock is involved during discovery or unwinding. The only profiler lock (task_link_map_lock) is held briefly for link-map cleanup, then released before the expensive coroutine stack unwinding begins.Greenlets lack CPython-native tracking, so the profiler must maintain its own
greenlet_info_mapwith frame pointers updated on every switch viaupdate_greenlet_frame(). This creates a lock shared between the sampler thread (reading) and the application thread (writing on every switch) — the exact contention pattern this fix addresses by making the read-side (Phase 1 snapshot) fast.Regression test confirms the issue on main
The regression test (
test_gevent_greenlet_switch_not_blocked_by_profiler) was cherry-picked to main in #16660 and fails there, confirming this branch fixes the contention.JIRA
SCP-1039
Testing
test_gevent_greenlet_switch_not_blocked_by_profilerregression test that:gevent.joinallwith a 30s timeout andtry/finallycleanup to prevent CI hangsRisks
unwind_frame()already handles invalid pointers gracefully viacopy_type()which returns non-zero on failure🤖 Generated with Claude Code