Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/mypy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ on:
- "Tools/build/generate_levenshtein_examples.py"
- "Tools/build/generate_sbom.py"
- "Tools/build/generate_stdlib_module_names.py"
- "Tools/build/generate_trampoline_ehframe.py"
- "Tools/build/mypy.ini"
- "Tools/build/umarshal.py"
- "Tools/build/update_file.py"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ Tools/unicode/data/
/dist/
/jit_stencils*.h
/jit_unwind_info*.h
/trampoline_ehframe.h*
.jit-stamp
/platform
/profile-clean-stamp
Expand Down
3 changes: 2 additions & 1 deletion Doc/howto/perf_profiling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ relationship between this piece of code and the associated Python function using
samply support on macOS is available starting from Python 3.15.
Check the output of the ``configure`` build step or
check the output of ``python -m sysconfig | grep HAVE_PERF_TRAMPOLINE``
to see if your system is supported.
to see if your system is supported. Building the perf trampoline needs a
Python interpreter, found by ``configure`` as ``PYTHON_FOR_REGEN``.

For example, consider the following script:

Expand Down
5 changes: 5 additions & 0 deletions Doc/using/configure.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ To build CPython, you will need:

* Support for threads.

* Optionally, a Python interpreter, found by ``configure`` as
``PYTHON_FOR_REGEN``, to build the perf trampoline (see
:doc:`/howto/perf_profiling`). Without it, ``configure`` disables the perf
trampoline.

.. versionchanged:: 3.5
On Windows, Visual Studio 2015 or later is now required.

Expand Down
167 changes: 167 additions & 0 deletions Lib/test/test_perf_profiler.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import mmap
import struct
import tempfile
import unittest
import string
import subprocess
Expand Down Expand Up @@ -685,5 +688,169 @@ def tearDown(self) -> None:
file.unlink()


JITDUMP_MAGIC = 0x4A695444 # "JiTD"
JITDUMP_VERSION = 1
PERF_LOAD = 0
PERF_UNWINDING_INFO = 4
JITDUMP_ENDIAN = "<" if sys.byteorder == "little" else ">"
# Every jitdump record starts with event(u32), size(u32), timestamp(u64).
JITDUMP_RECORD_HEADER_SIZE = 16
# CodeLoadEvent: record header, pid(u32), tid(u32), vma(u64), code_addr(u64),
# code_size(u64), code_id(u64), then the NUL-terminated name.
CODE_LOAD_CODE_SIZE_OFFSET = JITDUMP_RECORD_HEADER_SIZE + 4 + 4 + 8 + 8
CODE_LOAD_NAME_OFFSET = CODE_LOAD_CODE_SIZE_OFFSET + 8 + 8
# CodeUnwindingInfoEvent: record header, unwind_data_size(u64),
# eh_frame_hdr_size(u64), mapped_size(u64), then the .eh_frame bytes followed
# by perf's 20-byte eh_frame_hdr (EhFrameHeader in perf_jit_trampoline.c),
# whose "from" field is the signed distance back to the code.
UNWIND_DATA_SIZE_OFFSET = JITDUMP_RECORD_HEADER_SIZE
UNWIND_EH_FRAME_OFFSET = JITDUMP_RECORD_HEADER_SIZE + 3 * 8
EH_FRAME_HDR_SIZE = 20
EH_FRAME_HDR_FROM_OFFSET = 12
# DWARF FDE pointer encodings: DW_EH_PE_pcrel | DW_EH_PE_sdata4 (ELF
# assemblers) and DW_EH_PE_pcrel | DW_EH_PE_absptr (Darwin assemblers).
DW_EH_PE_PCREL_SDATA4 = 0x1B
DW_EH_PE_PCREL_ABSPTR = 0x10


def _jitdump_records(data):
"""Yield (event, offset, size) for each record of a jitdump file."""
header_size = struct.unpack_from(f"{JITDUMP_ENDIAN}I", data, 8)[0]
pos = header_size
while pos < len(data):
if pos + JITDUMP_RECORD_HEADER_SIZE > len(data):
raise ValueError(f"truncated record header at offset {pos}")
event, size = struct.unpack_from(f"{JITDUMP_ENDIAN}II", data, pos)
if size < JITDUMP_RECORD_HEADER_SIZE or pos + size > len(data):
raise ValueError(f"record at offset {pos} has a bad size {size}")
yield event, pos, size
pos += size


def _fde_pointer_encoding(eh_frame):
"""Return the FDE pointer encoding byte of a version 1 "zR" CIE."""
cie_length = struct.unpack_from(f"{JITDUMP_ENDIAN}I", eh_frame, 0)[0]
cie_total = 4 + cie_length
pos = 12 # past length, CIE_id, version and "zR\0"
for _ in range(2): # code and data alignment factors (LEB128)
while eh_frame[pos] & 0x80:
pos += 1
pos += 1
pos += 1 # return address column
while eh_frame[pos] & 0x80: # augmentation data length (LEB128)
pos += 1
pos += 1
if pos >= cie_total:
raise ValueError("truncated CIE augmentation data")
return eh_frame[pos]


@unittest.skipIf(support.check_bolt_optimized(), "fails on BOLT instrumented binaries")
class TestJitdumpFileFormat(unittest.TestCase):
"""Validate the jitdump written by -Xperf_jit without requiring perf."""

def _require_usable_jitdump_dir(self):
"""Skip unless /tmp can hold and, except on macOS, exec-map a file."""
try:
with tempfile.NamedTemporaryFile(dir="/tmp", prefix="jit-probe-") as f:
if sys.platform != "darwin":
f.write(bytes(mmap.PAGESIZE))
f.flush()
with mmap.mmap(f.fileno(), mmap.PAGESIZE, flags=mmap.MAP_PRIVATE,
prot=mmap.PROT_READ | mmap.PROT_EXEC):
pass
except OSError as exc:
self.skipTest(f"/tmp is not usable for jitdump files: {exc}")

def _run_and_get_jitdump(self, code):
self._require_usable_jitdump_dir()
# The child prints its pid so we open exactly its own jitdump file
# rather than whatever another test worker left in /tmp.
code = "import os, sys\nsys.stdout.write(str(os.getpid()))\n" + code
_, out, _ = assert_python_ok("-Xperf_jit", "-c", code, PYTHON_JIT="0")
path = pathlib.Path(f"/tmp/jit-{int(out)}.dump")
self.assertTrue(path.exists(), f"{path} was not created")
self.addCleanup(path.unlink)
data = path.read_bytes()
self.assertTrue(data, f"{path} is empty, the jitdump header was not written")
return data

def _check_code_load(self, data, pos, size):
"""Validate a code load record and return its name and code size."""
code_size = struct.unpack_from(
f"{JITDUMP_ENDIAN}Q", data, pos + CODE_LOAD_CODE_SIZE_OFFSET)[0]
name_start = pos + CODE_LOAD_NAME_OFFSET
name_end = data.find(b"\x00", name_start, pos + size)
self.assertGreater(name_end, 0)
name = data[name_start:name_end].decode("utf-8", errors="replace")
# The machine code follows the name inside the load record.
self.assertLessEqual(name_end + 1 + code_size, pos + size, name)
return name, code_size

def _check_unwind_info(self, data, pos, size, name, code_size):
"""Check the FDE and perf header against their code load record."""
unwind_data_size, eh_frame_hdr_size = struct.unpack_from(
f"{JITDUMP_ENDIAN}QQ", data, pos + UNWIND_DATA_SIZE_OFFSET)
self.assertEqual(eh_frame_hdr_size, EH_FRAME_HDR_SIZE)
self.assertLessEqual(UNWIND_EH_FRAME_OFFSET + unwind_data_size, size)
eh_frame_size = unwind_data_size - eh_frame_hdr_size
self.assertGreater(eh_frame_size, 0)
start = pos + UNWIND_EH_FRAME_OFFSET
eh_frame = data[start:start + eh_frame_size]

cie_length, cie_id = struct.unpack_from(f"{JITDUMP_ENDIAN}II", eh_frame, 0)
self.assertEqual(cie_id, 0, "first entry must be a CIE")
self.assertEqual(eh_frame[8], 1, "CIE version must be 1")
self.assertEqual(eh_frame[9:12], b"zR\x00")
encoding = _fde_pointer_encoding(eh_frame)
if encoding == DW_EH_PE_PCREL_SDATA4:
fields = "iI"
elif encoding == DW_EH_PE_PCREL_ABSPTR:
fields = "qQ"
else:
self.fail(f"unexpected FDE pointer encoding {encoding:#x}")
# jit_unwind.c patches initial_location and address_range for
# perf's DSO layout, where the .eh_frame follows the code at
# code_size rounded up to 8 bytes.
pc_offset = 4 + cie_length + 8
initial_location, address_range = struct.unpack_from(
f"{JITDUMP_ENDIAN}{fields}", eh_frame, pc_offset)
self.assertEqual(address_range, code_size, name)
rounded_code_size = (code_size + 7) & ~7
self.assertEqual(initial_location, -(rounded_code_size + pc_offset), name)
# perf's eh_frame_hdr must point back at the code with the same
# rounding as the FDE.
hdr_from = struct.unpack_from(
f"{JITDUMP_ENDIAN}i", data,
start + eh_frame_size + EH_FRAME_HDR_FROM_OFFSET)[0]
self.assertEqual(hdr_from, -(rounded_code_size + eh_frame_size), name)

def _check_unwinding_records(self, data):
"""Return {name: code_size} after checking each unwind/load pair."""
records = list(_jitdump_records(data))
regions = {}
for index, (event, pos, size) in enumerate(records):
if event != PERF_UNWINDING_INFO:
continue
# Unwinding info immediately precedes the code it describes.
self.assertLess(index + 1, len(records))
load_event, load_pos, load_size = records[index + 1]
self.assertEqual(load_event, PERF_LOAD)
name, code_size = self._check_code_load(data, load_pos, load_size)
with self.subTest(region=name):
self._check_unwind_info(data, pos, size, name, code_size)
regions[name] = code_size
self.assertTrue(regions, "no CodeUnwindingInfoEvent found")
return regions

def test_jitdump_unwinding_info(self):
"""Each region's .eh_frame is patched for that region's size."""
data = self._run_and_get_jitdump("def my_test_func(): pass\nmy_test_func()")
magic, version = struct.unpack_from(f"{JITDUMP_ENDIAN}II", data, 0)
self.assertEqual((magic, version), (JITDUMP_MAGIC, JITDUMP_VERSION))
regions = self._check_unwinding_records(data)
self.assertTrue(any("my_test_func" in name for name in regions))


if __name__ == "__main__":
unittest.main()
Loading
Loading