Skip to content

Commit 443f93d

Browse files
committed
bpftrace proof of concept for contextvars based thread correlation
1 parent 5307dd0 commit 443f93d

5 files changed

Lines changed: 186 additions & 0 deletions

File tree

uprobe-contextvars/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM archlinux:base-20251019.0.436919
2+
RUN pacman -Syu --noconfirm bpftrace debuginfod python uv

uprobe-contextvars/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
## Running
2+
3+
```console
4+
docker compose up --remove-orphans --build
5+
```
6+
7+
## Sample output
8+
9+
```console
10+
bpftrace-1 | Trying to attach probe: uretprobe:/usr/lib/libpython3.13.so:context_run*
11+
bpftrace-1 | Attaching to 1 functions
12+
bpftrace-1 | /usr/lib/libpython3.13.so:context_run
13+
bpftrace-1 | Trying to attach probe: uprobe:/usr/lib/libpython3.13.so:context_run*
14+
bpftrace-1 | Attaching to 1 functions
15+
bpftrace-1 | /usr/lib/libpython3.13.so:context_run
16+
bpftrace-1 | Trying to attach probe: uprobe:/usr/lib/libpython3.13.so:PyContext_Exit
17+
bpftrace-1 | Trying to attach probe: uprobe:/usr/lib/libpython3.13.so:PyContext_Enter
18+
bpftrace-1 | Attached 4 probes
19+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd35f88900, .args = 0x7fdd36158a98, .nargs = 3, .kwnames = 0x0 }
20+
bpftrace-1 | tid=3062951 return context_run
21+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd36188240, .args = 0x7fdd35f37098, .nargs = 2, .kwnames = 0x0 }
22+
bpftrace-1 | tid=3062951 return context_run
23+
bpftrace-1 | tid=3062958 context_run { .self = 0x7fdd35f88900, .args = 0x7fdd3722e938, .nargs = 1, .kwnames = 0x0 }
24+
python-1 | 200
25+
bpftrace-1 | tid=3062958 return context_run
26+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd35f8b740, .args = 0x7fdd35ff7758, .nargs = 3, .kwnames = 0x0 }
27+
bpftrace-1 | tid=3062951 return context_run
28+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd364f40c0, .args = 0x7fdd3600d588, .nargs = 1, .kwnames = 0x0 }
29+
bpftrace-1 | tid=3062951 return context_run
30+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd364320c0, .args = 0x7fdd35f36118, .nargs = 2, .kwnames = 0x0 }
31+
bpftrace-1 | tid=3062951 return context_run
32+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd36188240, .args = 0x7fdd35f36118, .nargs = 2, .kwnames = 0x0 }
33+
bpftrace-1 | tid=3062951 return context_run
34+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd35f88900, .args = 0x7fdd36158a98, .nargs = 3, .kwnames = 0x0 }
35+
bpftrace-1 | tid=3062951 return context_run
36+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd36188240, .args = 0x7fdd35f36118, .nargs = 2, .kwnames = 0x0 }
37+
bpftrace-1 | tid=3062951 return context_run
38+
bpftrace-1 | tid=3062958 context_run { .self = 0x7fdd35f88900, .args = 0x7fdd3722e938, .nargs = 1, .kwnames = 0x0 }
39+
bpftrace-1 | tid=3062958 return context_run
40+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd35f8b740, .args = 0x7fdd35ff7758, .nargs = 3, .kwnames = 0x0 }
41+
bpftrace-1 | tid=3062951 return context_run
42+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd364f40c0, .args = 0x7fdd3600d738, .nargs = 1, .kwnames = 0x0 }
43+
bpftrace-1 | tid=3062951 return context_run
44+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd364320c0, .args = 0x7fdd35fbaf58, .nargs = 2, .kwnames = 0x0 }
45+
bpftrace-1 | tid=3062951 return context_run
46+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd36188240, .args = 0x7fdd35fbaf58, .nargs = 2, .kwnames = 0x0 }
47+
python-1 | 200
48+
```
49+
50+
In this example `3062951` is the main event loop thread and `3062958` is a thread pool worker,
51+
given work by `asyncio.to_thread()`. When work is run in the thread pool, asyncio wraps that in
52+
`context_run()` so you can track which thread started the work. The [self
53+
argument](https://github.com/python/cpython/blob/v3.13.9/Python/context.c#L649) is a pointer to
54+
the [Context object](https://docs.python.org/3/library/contextvars.html#contextvars.Context).
55+
56+
In the above example, Context `0x7fdd35f88900` moves between the two threads.
57+
```console
58+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd35f88900, .args = 0x7fdd36158a98, .nargs = 3, .kwnames = 0x0 }
59+
...
60+
bpftrace-1 | tid=3062958 context_run { .self = 0x7fdd35f88900, .args = 0x7fdd3722e938, .nargs = 1, .kwnames = 0x0 }
61+
...
62+
bpftrace-1 | tid=3062951 context_run { .self = 0x7fdd35f88900, .args = 0x7fdd36158a98, .nargs = 3, .kwnames = 0x0 }
63+
...
64+
bpftrace-1 | tid=3062958 context_run { .self = 0x7fdd35f88900, .args = 0x7fdd3722e938, .nargs = 1, .kwnames = 0x0 }
65+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Docker compose file for bpftrace and some python webapps
2+
3+
services:
4+
bpftrace:
5+
build:
6+
context: .
7+
dockerfile: Dockerfile
8+
privileged: true
9+
pid: "host"
10+
network_mode: "host"
11+
volumes:
12+
- .:/app
13+
- /sys/:/sys/
14+
working_dir: /app
15+
environment:
16+
- DEBUGINFOD_URLS=https://debuginfod.archlinux.org
17+
entrypoint: ["bpftrace", "-v", "print_calls.bt"]
18+
19+
20+
python:
21+
build:
22+
context: .
23+
dockerfile: Dockerfile
24+
environment:
25+
- PYTHONUNBUFFERED=1
26+
volumes:
27+
- .:/app
28+
working_dir: /app
29+
entrypoint: ["uv", "run", "--script", "plain.py"]
30+
depends_on:
31+
- bpftrace

uprobe-contextvars/plain.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# /// script
2+
# requires-python = ">=3.13"
3+
# dependencies = [
4+
# "requests",
5+
# ]
6+
# ///
7+
8+
import asyncio
9+
10+
import requests
11+
12+
13+
async def make_request() -> None:
14+
# Runs the blocking IO in a thread pool
15+
status_code = await asyncio.to_thread(
16+
lambda: requests.get("https://github.com").status_code
17+
)
18+
print(status_code)
19+
20+
21+
async def main() -> None:
22+
while True:
23+
await make_request()
24+
await asyncio.sleep(2)
25+
26+
27+
asyncio.run(main())

uprobe-contextvars/print_calls.bt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#define YELLOW "\033[33m"
2+
#define NC "\033[0m" // No Color (resets the terminal color)
3+
4+
// These are the official C API for enter/exiting a context, but is not called by the cpython
5+
// internals. Something like uvloop might use this.
6+
uprobe:/usr/lib/libpython3.13.so:PyContext_Enter {
7+
printf("%stid=%s%s %s %s\n", YELLOW, tid, NC, func, args);
8+
}
9+
10+
uprobe:/usr/lib/libpython3.13.so:PyContext_Exit {
11+
printf("%stid=%s%s %s %s\n", YELLOW, tid, NC, func, args);
12+
}
13+
14+
// This is the C implementation of contextvars.run() python function
15+
// https://github.com/python/cpython/blob/v3.13.9/Python/context.c#L648-L650. This plus the
16+
// PyContext_Enter/PyContext_Exit is probably the most reliable.
17+
uprobe:/usr/lib/libpython3.13.so:context_run* {
18+
printf("%stid=%s%s %s %s\n", YELLOW, tid, NC, func, args);
19+
}
20+
uretprobe:/usr/lib/libpython3.13.so:context_run* {
21+
printf("%stid=%s%s return %s\n", YELLOW, tid, NC, func);
22+
}
23+
24+
// -----------
25+
26+
// These might be useful for tracking parentage of new Context objects
27+
/*
28+
uretprobe:/usr/lib/libpython3.13.so:PyContext_CopyCurrent {
29+
printf("%s retval = %#x\n", func, retval);
30+
}
31+
uprobe:/usr/lib/libpython3.13.so:PyContext_Copy {
32+
printf("%s %s\n", func, args);
33+
}
34+
uretprobe:/usr/lib/libpython3.13.so:PyContext_Copy {
35+
printf("%s retval = %#x\n", func, retval);
36+
}
37+
*/
38+
39+
// These might be useful for watching the trace context for OTel python instrumented code
40+
/*
41+
uprobe:/usr/lib/libpython3.13.so:PyContextVar_Get {
42+
printf("%s %s\n", func, args);
43+
}
44+
uprobe:/usr/lib/libpython3.13.so:PyContextVar_Set {
45+
printf("%s %s\n", func, args);
46+
}
47+
*/
48+
49+
// These are called by PyContext_Enter/PyContext_Exit and context_run internally. It seems to
50+
// get inlined in many production builds of python (e.g. on Debian and uv managed python), so
51+
// might not be reliable.
52+
//
53+
// See https://github.com/python/cpython/blob/v3.13.9/Python/context.c#L112-L113
54+
/*
55+
uprobe:/usr/lib/libpython3.13.so:_PyContext_Enter {
56+
printf("tid=%s %s %s\n", tid, func, args);
57+
}
58+
uprobe:/usr/lib/libpython3.13.so:_PyContext_Exit {
59+
printf("tid=%s %s %s\n", tid, func, args);
60+
}
61+
*/

0 commit comments

Comments
 (0)