Skip to content

feat(ebpf/python): add CPython 3.14 offsets#3

Open
gecube wants to merge 1 commit into
coroot:mainfrom
gecube:feat/python-3.14-offsets
Open

feat(ebpf/python): add CPython 3.14 offsets#3
gecube wants to merge 1 commit into
coroot:mainfrom
gecube:feat/python-3.14-offsets

Conversation

@gecube

@gecube gecube commented Jul 6, 2026

Copy link
Copy Markdown

Problem

Python 3.14 processes are not profiled. pyVersions has no {3, 14, *}
entry, so GetUserOffsets returns unsupported version — patch-guessing
only spans a single major.minor, so it never falls back across a minor
boundary. This surfaces downstream as coroot/coroot-node-agent#318 (no
profiles for Python 3.14 workloads).

Change

Add the CPython 3.14.0 offsets for amd64 and arm64.

Offsets were produced by the existing python_dwarfdump generator from a
release-configuration CPython 3.14.0 build (./configure --enable-shared CFLAGS="-O2 -gdwarf-4", no --with-pydebug), so the layout matches
the official python:3.14 images (PyObject is 16 bytes — a GIL build,
not free-threaded). The amd64 and arm64 rows are identical, consistent
with every existing entry.

The offsets reflect real 3.14 layout changes, cross-checked with pahole:

  • PyTupleObject gained Py_hash_t ob_hash at offset 24, moving ob_item
    from 24 to 32.
  • _PyInterpreterFrame localsplus/owner and _PyRuntimeState
    gilstate/autoTSSkey shifted.

No unwinder changes are needed: the eBPF program is offset-driven and the
existing version.Compare(Py313) >= 0 code path (PyCFrame removed,
current_frame used as the frame field) applies unchanged to 3.14. A
local test confirms GetUserOffsets resolves 3.14.0 exactly and
3.14.x via patch-guessing.

Generator robustness

python_dwarfdump panicked on the 3.14 build: modern GCC emits a
forward-declaration DIE for pyruntimestate (DW_AT_declaration, no
byte_size) in one compilation unit alongside the complete definitions in
others. GetTypeByName2 now skips the incomplete entry and keeps the
complete definition instead of panicking.

Python 3.14 processes were not profiled: pyVersions had no {3, 14, *}
entry, so GetUserOffsets returned "unsupported version" (patch-guessing
only spans a single major.minor). Add the 3.14.0 offsets for amd64 and
arm64, generated by python_dwarfdump from a release-config CPython 3.14.0
build (--enable-shared, -gdwarf-4, no --with-pydebug). amd64 and arm64
offsets are identical, matching the existing 3.13 entries.

Offsets reflect real 3.14 layout changes verified with pahole, e.g.
PyTupleObject gained Py_hash_t ob_hash, moving ob_item from 24 to 32.
The offset-driven eBPF unwinder and the existing >= Py313 code path
handle 3.14 with no further changes.

Also make python_dwarfdump tolerant of forward-declaration DIEs
(DW_AT_declaration, no byte_size) that modern GCC emits across
compilation units: GetTypeByName2 now skips the incomplete entry and
keeps the complete definition instead of panicking.

Signed-off-by: Gaál György <gb12335@gmail.com>
@gecube

gecube commented Jul 6, 2026

Copy link
Copy Markdown
Author

Some verification notes for reviewers.

How the offsets were produced

Generated with this repo's own python_dwarfdump, from a release-configuration CPython 3.14.0 build:

./configure --enable-shared CFLAGS="-O2 -gdwarf-4"   # no --with-pydebug
make -j
go run ./cmd/python_dwarfdump libpython3.14.so.1.0

No --with-pydebug, so PyObject is 16 bytes — a normal GIL build matching the official python:3.14 images, not a debug/free-threaded layout. -gdwarf-4 because gcc-14 defaults to DWARF5, which emits a forward-declaration DIE for pyruntimestate (DW_AT_declaration, no byte_size) alongside the complete definitions in other CUs; GetTypeByName2 panicked on it. Hence the small generator fix in this PR — skip the incomplete DIE and keep the complete definition.

Proofs

amd64 and arm64 offsets are identical (as with every existing entry — these fields are LP64 layout-independent).

The one surprising value is real. pahole on the same build confirms 3.14 added Py_hash_t ob_hash to PyTupleObject, moving ob_item 24 → 32:

typedef struct {
    PyVarObject ob_base;    /*  0 24 */
    Py_hash_t   ob_hash;    /* 24  8 */   <-- new in 3.14
    PyObject   *ob_item[1]; /* 32  8 */
} PyTupleObject;

Resolution + no code-path change. GetUserOffsets finds 3.14.0 exactly and 3.14.x via patch-guessing, and the existing version.Compare(Py313) >= 0 branch (PyCFrame removed, current_frame used as the frame field) handles 3.14 unchanged:

3.14.0  guess=false
3.14.3  guess=true
3.14.6  guess=true

Runtime A/B against a live python:3.14.6 process (same PID), an eBPF session with PythonEnabled: true:

  • without this PR:
    pyperf get python process data failed  err="unsupported python version ... {3 14 6}"  pid=463635
    
  • with this PR:
    pyperf process profiling init success  pid=463635
      py_data="...PyTupleObjectObItem:32 ...PyInterpreterFrameOwner:74 ...Version:{Major:3 Minor:14 Patch:6}..."
    

Fixes the Python 3.14 profiling gap reported in coroot/coroot-node-agent#318.

What's next

After this merges I'll bump the replace pin in coroot-node-agent to the new fork commit so it picks up 3.14 support. Happy to add per-patch rows (3.14.1–3.14.6) instead of relying on patch-guessing from 3.14.0 if you'd prefer that.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant