Skip to content

faulthandler: make per-call thread dump cap configurable #149085

Description

@efroemling

Feature or enhancement

Proposal:

faulthandler.dump_traceback() and faulthandler.dump_traceback_later() cap their output at 100 threads via a hardcoded MAX_NTHREADS = 100 in Python/traceback.c. Past the cap, the dump writes "...\n" and stops.

/* Python/traceback.c */
#define MAX_NTHREADS 100

const char* _Py_NO_SANITIZE_THREAD
_Py_DumpTracebackThreads(int fd, PyInterpreterState *interp,
                         PyThreadState *current_tstate)
{
    ...
    if (nthreads >= MAX_NTHREADS) {
        PUTS(fd, "...\n");
        break;
    }
    ...
}

The constant has been there since faulthandler was added in 2010 (gh-2762). It's still documented as a fixed limit in Doc/library/faulthandler.rst:

It is limited to 100 frames and 100 threads.

Proposal: add a keyword-only max_threads to both functions, default 100. Callers that omit it get identical behavior to today.

faulthandler.dump_traceback(file=sys.stderr, all_threads=True, *, max_threads=100)
faulthandler.dump_traceback_later(timeout, repeat=False, file=sys.stderr,
                                  exit=False, *, max_threads=100)

Motivation

100 was reasonable in 2010, but a few things can make it a problem now:

  1. Library-internal thread counts. A google-cloud-firestore Watch subscription spawns ~2 long-lived gRPC threads (google.api_core.bidi, grpc._channel.consume_request_iterator). 50 active subscriptions ≈ 100 threads. Other widely-used libraries (gRPC channels generally, ThreadPoolExecutor, asyncio's default executor) push it further. Modern server processes routinely run with 100+ long-lived threads, regardless of what the application code itself spawns.

  2. Dump order is newest-thread-first. _Py_DumpTracebackThreads walks tstate via PyInterpreterState_ThreadHead → tstate->next. New thread states are prepended to the list by add_threadstate() in Python/pystate.c, so this walk visits the most-recently-created thread first and the main thread last. With more than 100 threads alive, the cap is guaranteed to cut off the oldest entries - including the main thread.

When you reach for dump_traceback_later as a deadlock-detection watchdog, the dump can arrive missing the main thread - the one you actually wanted.

Has this already been discussed elsewhere?

Searched the issue tracker, PR history, Discourse, and python-dev. Nothing found. gh-98825 is adjacent (per-thread context for tracebacks) but doesn't touch the cap.

Links to previous discussion of this feature

None found.


Implementation

Patch ready: ~180 lines across 12 files (most auto-generated clinic + globals). Builds clean on current main. test_faulthandler passes 50/50, 6 platform skips. Includes:

  • Two tests in Lib/test/test_faulthandler.py:
    • test_dump_traceback_max_threads: spawns 6 worker threads, dumps with max_threads=3, asserts the truncation marker is present and only 3 thread headers appear.
    • test_dump_traceback_max_threads_default: dumps without the kwarg, asserts no marker on a process with <100 threads.
  • Doc updates in Doc/library/faulthandler.rst (signatures, versionchanged:: 3.15, the limits paragraph).
  • Misc/NEWS.d/next/Library/... entry.

Implementation summary

File Change
Python/traceback.c _Py_DumpTracebackThreads gains max_nthreads; MAX_NTHREADS define moves to the header (renamed _Py_TRACEBACK_MAX_NTHREADS).
Include/internal/pycore_traceback.h Expose _Py_TRACEBACK_MAX_NTHREADS; update function signature.
Include/internal/pycore_faulthandler.h Add max_nthreads to the watchdog thread struct.
Modules/faulthandler.c New max_threads clinic kwarg on both functions (default 100). Fatal-signal handler passes _Py_TRACEBACK_MAX_NTHREADS explicitly.
Python/pylifecycle.c _Py_FatalError_DumpTracebacks passes _Py_TRACEBACK_MAX_NTHREADS explicitly.
Doc/library/faulthandler.rst Document the kwarg + versionchanged.
Lib/test/test_faulthandler.py Two new tests.

Backward compatibility

Default max_threads=100 matches the old cap, so omitting the kwarg gives no change. In-tree fatal-signal callers and the watchdog all pass _Py_TRACEBACK_MAX_NTHREADS explicitly.

No public C API change. _Py_DumpTracebackThreads and _Py_TRACEBACK_MAX_NTHREADS both live in pycore_traceback.h, gated by Py_BUILD_CORE.

Open questions

  1. register() could get the same kwarg. Left it out to keep the diff minimal. Signal-handler path has signal-safety constraints that make the rationale weaker there but not zero. Easy follow-up.

  2. MAX_FRAME_DEPTH is similarly hardcoded. Same pattern would apply but ergonomics differ - a deep stack on a deadlocked thread is usually what you want, not what you'd cap. Probably a separate proposal.

I'll open a PR if there's interest as scoped.

CPython versions tested on

3.15 (current main).

Operating systems tested on

Linux, macOS.

Has this already been discussed elsewhere?

This is a minor feature, which does not need previous discussion elsewhere

Links to previous discussion of this feature:

No response

Linked PRs

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions