Skip to content

Commit ae33ca8

Browse files
committed
Add support for tracking garbage collection and calls to native calls in the sampling profiler
- Introduce a new field in the GC state to store the frame that initiated garbage collection. - Update RemoteUnwinder to include options for including "<native>" and "<GC>" frames in the stack trace. - Modify the sampling profiler to accept parameters for controlling the inclusion of native and GC frames. - Enhance the stack collector to properly format and append these frames during profiling. - Add tests to verify the correct behavior of the profiler with respect to native and GC frames, including options to exclude them.
1 parent 95f6e12 commit ae33ca8

15 files changed

+302
-102
lines changed

Include/internal/pycore_debug_offsets.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ typedef struct _Py_DebugOffsets {
210210
struct _gc {
211211
uint64_t size;
212212
uint64_t collecting;
213+
uint64_t frame;
213214
} gc;
214215

215216
// Generator object offset;
@@ -351,6 +352,7 @@ typedef struct _Py_DebugOffsets {
351352
.gc = { \
352353
.size = sizeof(struct _gc_runtime_state), \
353354
.collecting = offsetof(struct _gc_runtime_state, collecting), \
355+
.frame = offsetof(struct _gc_runtime_state, frame), \
354356
}, \
355357
.gen_object = { \
356358
.size = sizeof(PyGenObject), \

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ struct _Py_global_strings {
4646
STRUCT_FOR_STR(dot_locals, ".<locals>")
4747
STRUCT_FOR_STR(empty, "")
4848
STRUCT_FOR_STR(format, ".format")
49+
STRUCT_FOR_STR(gc, "<GC>")
4950
STRUCT_FOR_STR(generic_base, ".generic_base")
5051
STRUCT_FOR_STR(json_decoder, "json.decoder")
5152
STRUCT_FOR_STR(kwdefaults, ".kwdefaults")
5253
STRUCT_FOR_STR(list_err, "list index out of range")
54+
STRUCT_FOR_STR(native, "<native>")
5355
STRUCT_FOR_STR(str_replace_inf, "1e309")
5456
STRUCT_FOR_STR(type_params, ".type_params")
5557
STRUCT_FOR_STR(utf_8, "utf-8")
@@ -486,6 +488,7 @@ struct _Py_global_strings {
486488
STRUCT_FOR_ID(fullerror)
487489
STRUCT_FOR_ID(func)
488490
STRUCT_FOR_ID(future)
491+
STRUCT_FOR_ID(gc)
489492
STRUCT_FOR_ID(generation)
490493
STRUCT_FOR_ID(get)
491494
STRUCT_FOR_ID(get_debug)
@@ -629,6 +632,7 @@ struct _Py_global_strings {
629632
STRUCT_FOR_ID(name_from)
630633
STRUCT_FOR_ID(namespace_separator)
631634
STRUCT_FOR_ID(namespaces)
635+
STRUCT_FOR_ID(native)
632636
STRUCT_FOR_ID(ndigits)
633637
STRUCT_FOR_ID(nested)
634638
STRUCT_FOR_ID(new_file_name)

Include/internal/pycore_interp_structs.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ struct _gc_runtime_state {
214214
struct gc_generation_stats generation_stats[NUM_GENERATIONS];
215215
/* true if we are currently running the collector */
216216
int collecting;
217+
// The frame that started the current collection. It might be NULL even when
218+
// collecting (if no Python frame is running):
219+
_PyInterpreterFrame *frame;
217220
/* list of uncollectable objects */
218221
PyObject *garbage;
219222
/* a list of callbacks to be invoked when collection is performed */

Include/internal/pycore_interpframe_structs.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ enum _frameowner {
2424
FRAME_OWNED_BY_GENERATOR = 1,
2525
FRAME_OWNED_BY_FRAME_OBJECT = 2,
2626
FRAME_OWNED_BY_INTERPRETER = 3,
27-
FRAME_OWNED_BY_CSTACK = 4,
27+
FRAME_OWNED_BY_CSTACK = 4, // XXX: Unused.
2828
};
2929

3030
struct _PyInterpreterFrame {

Include/internal/pycore_runtime_init_generated.h

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/profiling/sampling/sample.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,18 +136,18 @@ def _run_with_sync(original_cmd):
136136

137137

138138
class SampleProfiler:
139-
def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MODE_WALL):
139+
def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MODE_WALL, native=True, gc=True):
140140
self.pid = pid
141141
self.sample_interval_usec = sample_interval_usec
142142
self.all_threads = all_threads
143143
if _FREE_THREADED_BUILD:
144144
self.unwinder = _remote_debugging.RemoteUnwinder(
145-
self.pid, all_threads=self.all_threads, mode=mode
145+
self.pid, all_threads=self.all_threads, mode=mode, native=native, gc=gc
146146
)
147147
else:
148148
only_active_threads = bool(self.all_threads)
149149
self.unwinder = _remote_debugging.RemoteUnwinder(
150-
self.pid, only_active_thread=only_active_threads, mode=mode
150+
self.pid, only_active_thread=only_active_threads, mode=mode, native=native, gc=gc
151151
)
152152
# Track sample intervals and total sample count
153153
self.sample_intervals = deque(maxlen=100)
@@ -613,9 +613,11 @@ def sample(
613613
output_format="pstats",
614614
realtime_stats=False,
615615
mode=PROFILING_MODE_WALL,
616+
native=True,
617+
gc=True,
616618
):
617619
profiler = SampleProfiler(
618-
pid, sample_interval_usec, all_threads=all_threads, mode=mode
620+
pid, sample_interval_usec, all_threads=all_threads, mode=mode, native=native, gc=gc
619621
)
620622
profiler.realtime_stats = realtime_stats
621623

@@ -706,6 +708,8 @@ def wait_for_process_and_sample(pid, sort_value, args):
706708
output_format=args.format,
707709
realtime_stats=args.realtime_stats,
708710
mode=mode,
711+
native=args.native,
712+
gc=args.gc,
709713
)
710714

711715

@@ -756,9 +760,20 @@ def main():
756760
sampling_group.add_argument(
757761
"--realtime-stats",
758762
action="store_true",
759-
default=False,
760763
help="Print real-time sampling statistics (Hz, mean, min, max, stdev) during profiling",
761764
)
765+
sampling_group.add_argument(
766+
"--no-native",
767+
action="store_false",
768+
dest="native",
769+
help="Don't include artificial \"<native>\" frames to denote calls to non-Python code.",
770+
)
771+
sampling_group.add_argument(
772+
"--no-gc",
773+
action="store_false",
774+
dest="gc",
775+
help="Don't include artificial \"<GC>\" frames to denote active garbage collection.",
776+
)
762777

763778
# Mode options
764779
mode_group = parser.add_argument_group("Mode options")
@@ -915,6 +930,8 @@ def main():
915930
output_format=args.format,
916931
realtime_stats=args.realtime_stats,
917932
mode=mode,
933+
native=args.native,
934+
gc=args.gc,
918935
)
919936
elif args.module or args.args:
920937
if args.module:

Lib/profiling/sampling/stack_collector.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,16 @@ def process_frames(self, frames, thread_id):
3636
def export(self, filename):
3737
lines = []
3838
for (call_tree, thread_id), count in self.stack_counter.items():
39-
stack_str = ";".join(
40-
f"{os.path.basename(f[0])}:{f[2]}:{f[1]}" for f in call_tree
41-
)
42-
lines.append((f"tid:{thread_id};{stack_str}", count))
39+
parts = [f"tid:{thread_id}"]
40+
for file, line, func in call_tree:
41+
# This is what pstats does for "special" frames:
42+
if file == "~" and line == 0:
43+
part = func
44+
else:
45+
part = f"{os.path.basename(file)}:{func}:{line}"
46+
parts.append(part)
47+
stack_str = ";".join(parts)
48+
lines.append((stack_str, count))
4349

4450
lines.sort(key=lambda x: (-x[1], x[0]))
4551

Lib/test/test_external_inspection.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ def foo():
153153
FrameInfo([script_name, 12, "baz"]),
154154
FrameInfo([script_name, 9, "bar"]),
155155
FrameInfo([threading.__file__, ANY, "Thread.run"]),
156+
FrameInfo([threading.__file__, ANY, "Thread._bootstrap_inner"]),
157+
FrameInfo([threading.__file__, ANY, "Thread._bootstrap"]),
156158
]
157159
# Is possible that there are more threads, so we check that the
158160
# expected stack traces are in the result (looking at you Windows!)

0 commit comments

Comments
 (0)