Skip to content

Commit 5119532

Browse files
committed
update CLAUDE.md - implementation details
1 parent 071609e commit 5119532

4 files changed

Lines changed: 93 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,50 @@ tests/ — All tests (unittest)
3636
utils.py — Shared helpers and base test class
3737
```
3838

39+
## How it works
40+
41+
### Hook mechanism
42+
`yappi.start()` calls `sys.setprofile(_profile_thread_callback)` on the current thread. When a new thread is encountered, yappi transplants its own `c_profilefunc` into the new thread's `PyThreadState` — this is how it profiles all threads without each one needing an explicit `setprofile` call.
43+
44+
Every `call`/`return`/`c_call`/`c_return` event from the interpreter is delivered to `_yappi._profile_event()` (C function).
45+
46+
### Core data structures (C layer)
47+
```
48+
contexts (global htab)
49+
└── context_id → _ctx
50+
├── cs — call stack (_cstack), tracks the current call chain
51+
├── rec_levels — htab tracking recursion depth per function
52+
├── t0 — profiling start tick
53+
├── sched_cnt — how many times this thread was scheduled
54+
└── tags (htab)
55+
└── tag_id → pits (htab)
56+
└── code_obj / m_ml → _pit (profile item)
57+
├── callcount, nonrecursive_callcount
58+
├── ttotal — total time including children
59+
├── tsubtotal — self time (excluding children)
60+
├── children — linked list of _pit_children_info (callee timing per caller-callee pair)
61+
└── coroutines — linked list of _coro (active coroutine frames + start tick)
62+
```
63+
64+
### Contexts
65+
A *context* maps to a thread by default. Context identity is stored as `_yappi_tid` in `ThreadState.dict` — a monotonic counter rather than the OS tid (which can be recycled). This design allows alternative context backends: for **greenlets**, a `context_id_callback` returns a per-greenlet ID so multiple greenlets sharing one OS thread appear as separate contexts.
66+
67+
### Tags
68+
An optional `tag_callback` returns an integer per call event. Stats are bucketed per `(context, tag)`, allowing you to segregate profiling data (e.g. by request, task type, etc.) without separate profiling sessions.
69+
70+
### Coroutines
71+
Each `_pit` (function) holds a linked list of `_coro` entries — one per concurrently suspended coroutine frame. When a coroutine is suspended (`FRAME_SUSPENDED`), its elapsed time is accumulated into the `_coro` entry without closing the `_pit`. On resumption, timing continues from where it left off. This correctly handles multiple concurrent coroutines calling the same function.
72+
73+
### Stat collection (Python layer)
74+
`get_func_stats()` / `get_thread_stats()` enumerate the C-side hash tables and materialize them as Python objects:
75+
76+
| C struct | Python wrapper | Collection |
77+
|----------|---------------|------------|
78+
| `_pit` | `YFuncStat` | `YFuncStats` |
79+
| `_ctx` | `YThreadStat` | `YThreadStats` |
80+
81+
`YFuncStat.children` is a `YChildFuncStats` collection (from `_pit_children_info`) representing direct callees with per-pair timing. Export formats (callgrind, pstat) are produced by converting these collections in Python.
82+
3983
## Key constraints
4084
- **Don't assume GIL protection in callbacks**: profiler callbacks can fire on any thread; C code must be thread-safe
4185
- **clear_stats() sequence**: pause → wait for in-flight callbacks → clear. Never free memory while callbacks may still be running

tests/test_functionality.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,6 +1441,40 @@ def worker():
14411441
t1.join()
14421442
yappi.stop()
14431443

1444+
def test_clear_stats_race(self):
1445+
# Regression test for issue #188: SIGSEGV when clear_stats() races
1446+
# with a tag_callback that releases the GIL (e.g. via time.sleep).
1447+
stop = threading.Event()
1448+
1449+
def _tag_cbk():
1450+
time.sleep(0.001)
1451+
return 1
1452+
1453+
def _worker():
1454+
def a():
1455+
pass
1456+
while not stop.is_set():
1457+
a()
1458+
1459+
def _clearer():
1460+
while not stop.is_set():
1461+
yappi.clear_stats()
1462+
time.sleep(0.001)
1463+
1464+
yappi.set_tag_callback(_tag_cbk)
1465+
yappi.start()
1466+
1467+
worker = threading.Thread(target=_worker)
1468+
clearer = threading.Thread(target=_clearer)
1469+
worker.start()
1470+
clearer.start()
1471+
1472+
time.sleep(2)
1473+
stop.set()
1474+
worker.join(timeout=2)
1475+
clearer.join(timeout=2)
1476+
yappi.stop()
1477+
14441478

14451479
class NonRecursiveFunctions(utils.YappiUnitTestCase):
14461480

yappi/_yappi.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ static long long ycurthreadindex = 0;
166166
static int yapphavestats; // start() called at least once or stats cleared?
167167
static int yapprunning;
168168
static int paused;
169+
static volatile int _callback_depth = 0;
169170
static time_t yappstarttime;
170171
static long long yappstarttick;
171172
static long long yappstoptick;
@@ -335,7 +336,9 @@ _call_funcobjargs(PyObject *func, PyObject *args)
335336

336337
_local_current_ctx = current_ctx;
337338
_local_prev_ctx = prev_ctx;
339+
_callback_depth++;
338340
result = PyObject_CallFunctionObjArgs(func, args);
341+
_callback_depth--;
339342
current_ctx = _local_current_ctx;
340343
prev_ctx = _local_prev_ctx;
341344

@@ -2164,6 +2167,16 @@ _resume(PyObject *self, PyObject *args)
21642167
Py_RETURN_NONE;
21652168
}
21662169

2170+
static PyObject*
2171+
_wait_for_callbacks(PyObject *self, PyObject *args)
2172+
{
2173+
Py_BEGIN_ALLOW_THREADS
2174+
while (_callback_depth > 0)
2175+
;
2176+
Py_END_ALLOW_THREADS
2177+
Py_RETURN_NONE;
2178+
}
2179+
21672180
static PyMethodDef yappi_methods[] = {
21682181
{"start", start, METH_VARARGS, NULL},
21692182
{"stop", stop, METH_NOARGS, NULL},
@@ -2187,6 +2200,7 @@ static PyMethodDef yappi_methods[] = {
21872200
{"_profile_event", profile_event, METH_VARARGS, NULL}, // for internal usage.
21882201
{"_pause", _pause, METH_VARARGS, NULL}, // for internal usage.
21892202
{"_resume", _resume, METH_VARARGS, NULL}, // for internal usage.
2203+
{"_wait_for_callbacks", _wait_for_callbacks, METH_NOARGS, NULL}, // for internal usage.
21902204
{NULL, NULL, 0, NULL} /* sentinel */
21912205
};
21922206

yappi/yappi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,7 @@ def clear_stats():
13171317
Clears all of the profile results.
13181318
"""
13191319
_yappi._pause()
1320+
_yappi._wait_for_callbacks()
13201321
try:
13211322
_yappi.clear_stats()
13221323
finally:

0 commit comments

Comments
 (0)