mirror of
https://github.com/python/cpython.git
synced 2025-12-23 09:19:18 +00:00
Compare commits
80 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9704f1d84 | ||
|
|
a273bc99d2 | ||
|
|
5b5ee3c4bf | ||
|
|
9e51301234 | ||
|
|
714037ba84 | ||
|
|
be3c131640 | ||
|
|
665d2807a0 | ||
|
|
3c0888b25b | ||
|
|
a88d1b8dab | ||
|
|
700e9fad70 | ||
|
|
487e91c120 | ||
|
|
e728b006de | ||
|
|
9ded3dd4e9 | ||
|
|
ff7f62eb23 | ||
|
|
3960878143 | ||
|
|
6213a512bf | ||
|
|
09044dd42b | ||
|
|
b8d3fddba6 | ||
|
|
8d2d7bb2e7 | ||
|
|
2b4feee648 | ||
|
|
7607712b61 | ||
|
|
3cc57505e5 | ||
|
|
5989095dfd | ||
|
|
5b5263648f | ||
|
|
e46f28c6af | ||
|
|
4ea3c1a047 | ||
|
|
08bc03ff2a | ||
|
|
e2a7db7175 | ||
|
|
6b4bc6e6a2 | ||
|
|
6a4f10325d | ||
|
|
786f464c74 | ||
|
|
049c2526bf | ||
|
|
685272eb8a | ||
|
|
4aef138325 | ||
|
|
610aabfef2 | ||
|
|
220f0b1077 | ||
|
|
1391ee664c | ||
|
|
e79c39101a | ||
|
|
33d94abafd | ||
|
|
f54d44d333 | ||
|
|
e22c49522b | ||
|
|
4a8ecbad80 | ||
|
|
e4058d7cb1 | ||
|
|
14f0b5191a | ||
|
|
d2abd5733b | ||
|
|
0f01530bd5 | ||
|
|
ddfc155d3a | ||
|
|
1c544acaa5 | ||
|
|
71a7cb8887 | ||
|
|
fc80096a07 | ||
|
|
cbc0851ada | ||
|
|
8b64dd853d | ||
|
|
6e625f87d2 | ||
|
|
92243dc62c | ||
|
|
25397f9541 | ||
|
|
77b56eafde | ||
|
|
fba4584ffc | ||
|
|
e61a447d0e | ||
|
|
77c8e6a2b8 | ||
|
|
7d81eab923 | ||
|
|
568a819f67 | ||
|
|
2b466c47c3 | ||
|
|
49627dc991 | ||
|
|
d4095f25e8 | ||
|
|
1fc3039d71 | ||
|
|
c7dcb26520 | ||
|
|
248eb3efb3 | ||
|
|
1e9a0ee682 | ||
|
|
454485e564 | ||
|
|
8307a14d0e | ||
|
|
4fd006e712 | ||
|
|
c35b812e77 | ||
|
|
89729f2ef7 | ||
|
|
4345253981 | ||
|
|
8c87bcd7f2 | ||
|
|
6ee51a36b3 | ||
|
|
92d4aeafd5 | ||
|
|
16a305f152 | ||
|
|
a043407510 | ||
|
|
47ec96f133 |
196 changed files with 12563 additions and 3175 deletions
4
.gitattributes
vendored
4
.gitattributes
vendored
|
|
@ -83,8 +83,10 @@ Include/opcode.h generated
|
|||
Include/opcode_ids.h generated
|
||||
Include/token.h generated
|
||||
Lib/_opcode_metadata.py generated
|
||||
Lib/keyword.py generated
|
||||
Lib/idlelib/help.html generated
|
||||
Lib/keyword.py generated
|
||||
Lib/pydoc_data/topics.py generated
|
||||
Lib/pydoc_data/module_docs.py generated
|
||||
Lib/test/certdata/*.pem generated
|
||||
Lib/test/certdata/*.0 generated
|
||||
Lib/test/levenshtein_examples.json generated
|
||||
|
|
|
|||
2
.github/workflows/mypy.yml
vendored
2
.github/workflows/mypy.yml
vendored
|
|
@ -26,6 +26,7 @@ on:
|
|||
- "Tools/build/update_file.py"
|
||||
- "Tools/build/verify_ensurepip_wheels.py"
|
||||
- "Tools/cases_generator/**"
|
||||
- "Tools/check-c-api-docs/**"
|
||||
- "Tools/clinic/**"
|
||||
- "Tools/jit/**"
|
||||
- "Tools/peg_generator/**"
|
||||
|
|
@ -58,6 +59,7 @@ jobs:
|
|||
"Lib/tomllib",
|
||||
"Tools/build",
|
||||
"Tools/cases_generator",
|
||||
"Tools/check-c-api-docs",
|
||||
"Tools/clinic",
|
||||
"Tools/jit",
|
||||
"Tools/peg_generator",
|
||||
|
|
|
|||
14
.github/workflows/tail-call.yml
vendored
14
.github/workflows/tail-call.yml
vendored
|
|
@ -79,19 +79,17 @@ jobs:
|
|||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Native Windows (debug)
|
||||
- name: Native Windows MSVC (release)
|
||||
if: runner.os == 'Windows' && matrix.architecture != 'ARM64'
|
||||
shell: cmd
|
||||
run: |
|
||||
choco install llvm --allow-downgrade --no-progress --version ${{ matrix.llvm }}.1.0
|
||||
set PlatformToolset=clangcl
|
||||
set LLVMToolsVersion=${{ matrix.llvm }}.1.0
|
||||
set LLVMInstallDir=C:\Program Files\LLVM
|
||||
call ./PCbuild/build.bat --tail-call-interp -d -p ${{ matrix.architecture }}
|
||||
call ./PCbuild/rt.bat -d -p ${{ matrix.architecture }} -q --multiprocess 0 --timeout 4500 --verbose2 --verbose3
|
||||
choco install visualstudio2026buildtools --no-progress -y --force --params "--add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --locale en-US --passive"
|
||||
$env:PATH = "C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\MSBuild\Current\bin;$env:PATH"
|
||||
./PCbuild/build.bat --tail-call-interp -c Release -p ${{ matrix.architecture }} "/p:PlatformToolset=v145"
|
||||
./PCbuild/rt.bat -p ${{ matrix.architecture }} -q --multiprocess 0 --timeout 4500 --verbose2 --verbose3
|
||||
|
||||
# No tests (yet):
|
||||
- name: Emulated Windows (release)
|
||||
- name: Emulated Windows Clang (release)
|
||||
if: runner.os == 'Windows' && matrix.architecture == 'ARM64'
|
||||
shell: cmd
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -40,15 +40,15 @@ repos:
|
|||
files: ^Apple
|
||||
- id: ruff-format
|
||||
name: Run Ruff (format) on Doc/
|
||||
args: [--check]
|
||||
args: [--exit-non-zero-on-fix]
|
||||
files: ^Doc/
|
||||
- id: ruff-format
|
||||
name: Run Ruff (format) on Tools/build/check_warnings.py
|
||||
args: [--check, --config=Tools/build/.ruff.toml]
|
||||
args: [--exit-non-zero-on-fix, --config=Tools/build/.ruff.toml]
|
||||
files: ^Tools/build/check_warnings.py
|
||||
- id: ruff-format
|
||||
name: Run Ruff (format) on Tools/wasm/
|
||||
args: [--check, --config=Tools/wasm/.ruff.toml]
|
||||
args: [--exit-non-zero-on-fix, --config=Tools/wasm/.ruff.toml]
|
||||
files: ^Tools/wasm/
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
|
|
|
|||
|
|
@ -140,7 +140,8 @@ doctest:
|
|||
pydoc-topics: BUILDER = pydoc-topics
|
||||
pydoc-topics: build
|
||||
@echo "Building finished; now run this:" \
|
||||
"cp build/pydoc-topics/topics.py ../Lib/pydoc_data/topics.py"
|
||||
"cp build/pydoc-topics/topics.py ../Lib/pydoc_data/topics.py" \
|
||||
"&& cp build/pydoc-topics/module_docs.py ../Lib/pydoc_data/module_docs.py"
|
||||
|
||||
.PHONY: gettext
|
||||
gettext: BUILDER = gettext
|
||||
|
|
|
|||
|
|
@ -107,6 +107,46 @@ header files properly declare the entry points to be ``extern "C"``. As a result
|
|||
there is no need to do anything special to use the API from C++.
|
||||
|
||||
|
||||
.. _capi-system-includes:
|
||||
|
||||
System includes
|
||||
---------------
|
||||
|
||||
:file:`Python.h` includes several standard header files.
|
||||
C extensions should include the standard headers that they use,
|
||||
and should not rely on these implicit includes.
|
||||
The implicit includes are:
|
||||
|
||||
* ``<assert.h>``
|
||||
* ``<intrin.h>`` (on Windows)
|
||||
* ``<inttypes.h>``
|
||||
* ``<limits.h>``
|
||||
* ``<math.h>``
|
||||
* ``<stdarg.h>``
|
||||
* ``<wchar.h>``
|
||||
* ``<sys/types.h>`` (if present)
|
||||
|
||||
The following are included for backwards compatibility, unless using
|
||||
:ref:`Limited API <limited-c-api>` 3.13 or newer:
|
||||
|
||||
* ``<ctype.h>``
|
||||
* ``<unistd.h>`` (on POSIX)
|
||||
|
||||
The following are included for backwards compatibility, unless using
|
||||
:ref:`Limited API <limited-c-api>` 3.11 or newer:
|
||||
|
||||
* ``<errno.h>``
|
||||
* ``<stdio.h>``
|
||||
* ``<stdlib.h>``
|
||||
* ``<string.h>``
|
||||
|
||||
.. note::
|
||||
|
||||
Since Python may define some pre-processor definitions which affect the standard
|
||||
headers on some systems, you *must* include :file:`Python.h` before any standard
|
||||
headers are included.
|
||||
|
||||
|
||||
Useful macros
|
||||
=============
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
Pending removal in Python 3.20
|
||||
------------------------------
|
||||
|
||||
* The ``__version__`` attribute has been deprecated in these standard library
|
||||
modules and will be removed in Python 3.20.
|
||||
Use :py:data:`sys.version_info` instead.
|
||||
* The ``__version__``, ``version`` and ``VERSION`` attributes have been
|
||||
deprecated in these standard library modules and will be removed in
|
||||
Python 3.20. Use :py:data:`sys.version_info` instead.
|
||||
|
||||
- :mod:`argparse`
|
||||
- :mod:`csv`
|
||||
|
|
@ -24,6 +24,9 @@ Pending removal in Python 3.20
|
|||
- :mod:`tkinter.font`
|
||||
- :mod:`tkinter.ttk`
|
||||
- :mod:`wsgiref.simple_server`
|
||||
- :mod:`xml.etree.ElementTree`
|
||||
- :mod:`!xml.sax.expatreader`
|
||||
- :mod:`xml.sax.handler`
|
||||
- :mod:`zlib`
|
||||
|
||||
(Contributed by Hugo van Kemenade and Stan Ulbrych in :gh:`76007`.)
|
||||
|
|
|
|||
|
|
@ -3,154 +3,20 @@
|
|||
|
||||
.. _extending-intro:
|
||||
|
||||
******************************
|
||||
Extending Python with C or C++
|
||||
******************************
|
||||
********************************
|
||||
Using the C API: Assorted topics
|
||||
********************************
|
||||
|
||||
It is quite easy to add new built-in modules to Python, if you know how to
|
||||
program in C. Such :dfn:`extension modules` can do two things that can't be
|
||||
done directly in Python: they can implement new built-in object types, and they
|
||||
can call C library functions and system calls.
|
||||
|
||||
To support extensions, the Python API (Application Programmers Interface)
|
||||
defines a set of functions, macros and variables that provide access to most
|
||||
aspects of the Python run-time system. The Python API is incorporated in a C
|
||||
source file by including the header ``"Python.h"``.
|
||||
|
||||
The compilation of an extension module depends on its intended use as well as on
|
||||
your system setup; details are given in later chapters.
|
||||
|
||||
.. note::
|
||||
|
||||
The C extension interface is specific to CPython, and extension modules do
|
||||
not work on other Python implementations. In many cases, it is possible to
|
||||
avoid writing C extensions and preserve portability to other implementations.
|
||||
For example, if your use case is calling C library functions or system calls,
|
||||
you should consider using the :mod:`ctypes` module or the `cffi
|
||||
<https://cffi.readthedocs.io/>`_ library rather than writing
|
||||
custom C code.
|
||||
These modules let you write Python code to interface with C code and are more
|
||||
portable between implementations of Python than writing and compiling a C
|
||||
extension module.
|
||||
|
||||
|
||||
.. _extending-simpleexample:
|
||||
|
||||
A Simple Example
|
||||
================
|
||||
|
||||
Let's create an extension module called ``spam`` (the favorite food of Monty
|
||||
Python fans...) and let's say we want to create a Python interface to the C
|
||||
library function :c:func:`system` [#]_. This function takes a null-terminated
|
||||
character string as argument and returns an integer. We want this function to
|
||||
be callable from Python as follows:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import spam
|
||||
>>> status = spam.system("ls -l")
|
||||
|
||||
Begin by creating a file :file:`spammodule.c`. (Historically, if a module is
|
||||
called ``spam``, the C file containing its implementation is called
|
||||
:file:`spammodule.c`; if the module name is very long, like ``spammify``, the
|
||||
module name can be just :file:`spammify.c`.)
|
||||
|
||||
The first two lines of our file can be::
|
||||
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
|
||||
which pulls in the Python API (you can add a comment describing the purpose of
|
||||
the module and a copyright notice if you like).
|
||||
|
||||
.. note::
|
||||
|
||||
Since Python may define some pre-processor definitions which affect the standard
|
||||
headers on some systems, you *must* include :file:`Python.h` before any standard
|
||||
headers are included.
|
||||
|
||||
``#define PY_SSIZE_T_CLEAN`` was used to indicate that ``Py_ssize_t`` should be
|
||||
used in some APIs instead of ``int``.
|
||||
It is not necessary since Python 3.13, but we keep it here for backward compatibility.
|
||||
See :ref:`arg-parsing-string-and-buffers` for a description of this macro.
|
||||
|
||||
All user-visible symbols defined by :file:`Python.h` have a prefix of ``Py`` or
|
||||
``PY``, except those defined in standard header files.
|
||||
|
||||
.. tip::
|
||||
|
||||
For backward compatibility, :file:`Python.h` includes several standard header files.
|
||||
C extensions should include the standard headers that they use,
|
||||
and should not rely on these implicit includes.
|
||||
If using the limited C API version 3.13 or newer, the implicit includes are:
|
||||
|
||||
* ``<assert.h>``
|
||||
* ``<intrin.h>`` (on Windows)
|
||||
* ``<inttypes.h>``
|
||||
* ``<limits.h>``
|
||||
* ``<math.h>``
|
||||
* ``<stdarg.h>``
|
||||
* ``<wchar.h>``
|
||||
* ``<sys/types.h>`` (if present)
|
||||
|
||||
If :c:macro:`Py_LIMITED_API` is not defined, or is set to version 3.12 or older,
|
||||
the headers below are also included:
|
||||
|
||||
* ``<ctype.h>``
|
||||
* ``<unistd.h>`` (on POSIX)
|
||||
|
||||
If :c:macro:`Py_LIMITED_API` is not defined, or is set to version 3.10 or older,
|
||||
the headers below are also included:
|
||||
|
||||
* ``<errno.h>``
|
||||
* ``<stdio.h>``
|
||||
* ``<stdlib.h>``
|
||||
* ``<string.h>``
|
||||
|
||||
The next thing we add to our module file is the C function that will be called
|
||||
when the Python expression ``spam.system(string)`` is evaluated (we'll see
|
||||
shortly how it ends up being called)::
|
||||
|
||||
static PyObject *
|
||||
spam_system(PyObject *self, PyObject *args)
|
||||
{
|
||||
const char *command;
|
||||
int sts;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "s", &command))
|
||||
return NULL;
|
||||
sts = system(command);
|
||||
return PyLong_FromLong(sts);
|
||||
}
|
||||
|
||||
There is a straightforward translation from the argument list in Python (for
|
||||
example, the single expression ``"ls -l"``) to the arguments passed to the C
|
||||
function. The C function always has two arguments, conventionally named *self*
|
||||
and *args*.
|
||||
|
||||
The *self* argument points to the module object for module-level functions;
|
||||
for a method it would point to the object instance.
|
||||
|
||||
The *args* argument will be a pointer to a Python tuple object containing the
|
||||
arguments. Each item of the tuple corresponds to an argument in the call's
|
||||
argument list. The arguments are Python objects --- in order to do anything
|
||||
with them in our C function we have to convert them to C values. The function
|
||||
:c:func:`PyArg_ParseTuple` in the Python API checks the argument types and
|
||||
converts them to C values. It uses a template string to determine the required
|
||||
types of the arguments as well as the types of the C variables into which to
|
||||
store the converted values. More about this later.
|
||||
|
||||
:c:func:`PyArg_ParseTuple` returns true (nonzero) if all arguments have the right
|
||||
type and its components have been stored in the variables whose addresses are
|
||||
passed. It returns false (zero) if an invalid argument list was passed. In the
|
||||
latter case it also raises an appropriate exception so the calling function can
|
||||
return ``NULL`` immediately (as we saw in the example).
|
||||
The :ref:`tutorial <first-extension-module>` walked you through
|
||||
creating a C API extension module, but left many areas unexplained.
|
||||
This document looks at several concepts that you'll need to learn
|
||||
in order to write more complex extensions.
|
||||
|
||||
|
||||
.. _extending-errors:
|
||||
|
||||
Intermezzo: Errors and Exceptions
|
||||
=================================
|
||||
Errors and Exceptions
|
||||
=====================
|
||||
|
||||
An important convention throughout the Python interpreter is the following: when
|
||||
a function fails, it should set an exception condition and return an error value
|
||||
|
|
@ -321,194 +187,14 @@ call to :c:func:`PyErr_SetString` as shown below::
|
|||
}
|
||||
|
||||
|
||||
.. _backtoexample:
|
||||
|
||||
Back to the Example
|
||||
===================
|
||||
|
||||
Going back to our example function, you should now be able to understand this
|
||||
statement::
|
||||
|
||||
if (!PyArg_ParseTuple(args, "s", &command))
|
||||
return NULL;
|
||||
|
||||
It returns ``NULL`` (the error indicator for functions returning object pointers)
|
||||
if an error is detected in the argument list, relying on the exception set by
|
||||
:c:func:`PyArg_ParseTuple`. Otherwise the string value of the argument has been
|
||||
copied to the local variable :c:data:`!command`. This is a pointer assignment and
|
||||
you are not supposed to modify the string to which it points (so in Standard C,
|
||||
the variable :c:data:`!command` should properly be declared as ``const char
|
||||
*command``).
|
||||
|
||||
The next statement is a call to the Unix function :c:func:`system`, passing it
|
||||
the string we just got from :c:func:`PyArg_ParseTuple`::
|
||||
|
||||
sts = system(command);
|
||||
|
||||
Our :func:`!spam.system` function must return the value of :c:data:`!sts` as a
|
||||
Python object. This is done using the function :c:func:`PyLong_FromLong`. ::
|
||||
|
||||
return PyLong_FromLong(sts);
|
||||
|
||||
In this case, it will return an integer object. (Yes, even integers are objects
|
||||
on the heap in Python!)
|
||||
|
||||
If you have a C function that returns no useful argument (a function returning
|
||||
:c:expr:`void`), the corresponding Python function must return ``None``. You
|
||||
need this idiom to do so (which is implemented by the :c:macro:`Py_RETURN_NONE`
|
||||
macro)::
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
|
||||
:c:data:`Py_None` is the C name for the special Python object ``None``. It is a
|
||||
genuine Python object rather than a ``NULL`` pointer, which means "error" in most
|
||||
contexts, as we have seen.
|
||||
|
||||
|
||||
.. _methodtable:
|
||||
|
||||
The Module's Method Table and Initialization Function
|
||||
=====================================================
|
||||
|
||||
I promised to show how :c:func:`!spam_system` is called from Python programs.
|
||||
First, we need to list its name and address in a "method table"::
|
||||
|
||||
static PyMethodDef spam_methods[] = {
|
||||
...
|
||||
{"system", spam_system, METH_VARARGS,
|
||||
"Execute a shell command."},
|
||||
...
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
Note the third entry (``METH_VARARGS``). This is a flag telling the interpreter
|
||||
the calling convention to be used for the C function. It should normally always
|
||||
be ``METH_VARARGS`` or ``METH_VARARGS | METH_KEYWORDS``; a value of ``0`` means
|
||||
that an obsolete variant of :c:func:`PyArg_ParseTuple` is used.
|
||||
|
||||
When using only ``METH_VARARGS``, the function should expect the Python-level
|
||||
parameters to be passed in as a tuple acceptable for parsing via
|
||||
:c:func:`PyArg_ParseTuple`; more information on this function is provided below.
|
||||
|
||||
The :c:macro:`METH_KEYWORDS` bit may be set in the third field if keyword
|
||||
arguments should be passed to the function. In this case, the C function should
|
||||
accept a third ``PyObject *`` parameter which will be a dictionary of keywords.
|
||||
Use :c:func:`PyArg_ParseTupleAndKeywords` to parse the arguments to such a
|
||||
function.
|
||||
|
||||
The method table must be referenced in the module definition structure::
|
||||
|
||||
static struct PyModuleDef spam_module = {
|
||||
...
|
||||
.m_methods = spam_methods,
|
||||
...
|
||||
};
|
||||
|
||||
This structure, in turn, must be passed to the interpreter in the module's
|
||||
initialization function. The initialization function must be named
|
||||
:c:func:`!PyInit_name`, where *name* is the name of the module, and should be the
|
||||
only non-\ ``static`` item defined in the module file::
|
||||
|
||||
PyMODINIT_FUNC
|
||||
PyInit_spam(void)
|
||||
{
|
||||
return PyModuleDef_Init(&spam_module);
|
||||
}
|
||||
|
||||
Note that :c:macro:`PyMODINIT_FUNC` declares the function as ``PyObject *`` return type,
|
||||
declares any special linkage declarations required by the platform, and for C++
|
||||
declares the function as ``extern "C"``.
|
||||
|
||||
:c:func:`!PyInit_spam` is called when each interpreter imports its module
|
||||
:mod:`!spam` for the first time. (See below for comments about embedding Python.)
|
||||
A pointer to the module definition must be returned via :c:func:`PyModuleDef_Init`,
|
||||
so that the import machinery can create the module and store it in ``sys.modules``.
|
||||
|
||||
When embedding Python, the :c:func:`!PyInit_spam` function is not called
|
||||
automatically unless there's an entry in the :c:data:`PyImport_Inittab` table.
|
||||
To add the module to the initialization table, use :c:func:`PyImport_AppendInittab`,
|
||||
optionally followed by an import of the module::
|
||||
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
PyStatus status;
|
||||
PyConfig config;
|
||||
PyConfig_InitPythonConfig(&config);
|
||||
|
||||
/* Add a built-in module, before Py_Initialize */
|
||||
if (PyImport_AppendInittab("spam", PyInit_spam) == -1) {
|
||||
fprintf(stderr, "Error: could not extend in-built modules table\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* Pass argv[0] to the Python interpreter */
|
||||
status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
|
||||
if (PyStatus_Exception(status)) {
|
||||
goto exception;
|
||||
}
|
||||
|
||||
/* Initialize the Python interpreter. Required.
|
||||
If this step fails, it will be a fatal error. */
|
||||
status = Py_InitializeFromConfig(&config);
|
||||
if (PyStatus_Exception(status)) {
|
||||
goto exception;
|
||||
}
|
||||
PyConfig_Clear(&config);
|
||||
|
||||
/* Optionally import the module; alternatively,
|
||||
import can be deferred until the embedded script
|
||||
imports it. */
|
||||
PyObject *pmodule = PyImport_ImportModule("spam");
|
||||
if (!pmodule) {
|
||||
PyErr_Print();
|
||||
fprintf(stderr, "Error: could not import module 'spam'\n");
|
||||
}
|
||||
|
||||
// ... use Python C API here ...
|
||||
|
||||
return 0;
|
||||
|
||||
exception:
|
||||
PyConfig_Clear(&config);
|
||||
Py_ExitStatusException(status);
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
If you declare a global variable or a local static one, the module may
|
||||
experience unintended side-effects on re-initialisation, for example when
|
||||
removing entries from ``sys.modules`` or importing compiled modules into
|
||||
multiple interpreters within a process
|
||||
(or following a :c:func:`fork` without an intervening :c:func:`exec`).
|
||||
If module state is not yet fully :ref:`isolated <isolating-extensions-howto>`,
|
||||
authors should consider marking the module as having no support for subinterpreters
|
||||
(via :c:macro:`Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED`).
|
||||
|
||||
A more substantial example module is included in the Python source distribution
|
||||
as :file:`Modules/xxlimited.c`. This file may be used as a template or simply
|
||||
read as an example.
|
||||
|
||||
|
||||
.. _compilation:
|
||||
|
||||
Compilation and Linkage
|
||||
=======================
|
||||
Embedding an extension
|
||||
======================
|
||||
|
||||
There are two more things to do before you can use your new extension: compiling
|
||||
and linking it with the Python system. If you use dynamic loading, the details
|
||||
may depend on the style of dynamic loading your system uses; see the chapters
|
||||
about building extension modules (chapter :ref:`building`) and additional
|
||||
information that pertains only to building on Windows (chapter
|
||||
:ref:`building-on-windows`) for more information about this.
|
||||
|
||||
If you can't use dynamic loading, or if you want to make your module a permanent
|
||||
If you want to make your module a permanent
|
||||
part of the Python interpreter, you will have to change the configuration setup
|
||||
and rebuild the interpreter. Luckily, this is very simple on Unix: just place
|
||||
and rebuild the interpreter. On Unix, place
|
||||
your file (:file:`spammodule.c` for example) in the :file:`Modules/` directory
|
||||
of an unpacked source distribution, add a line to the file
|
||||
:file:`Modules/Setup.local` describing your file:
|
||||
|
|
@ -536,7 +222,7 @@ on the line in the configuration file as well, for instance:
|
|||
Calling Python Functions from C
|
||||
===============================
|
||||
|
||||
So far we have concentrated on making C functions callable from Python. The
|
||||
The tutorial concentrated on making C functions callable from Python. The
|
||||
reverse is also useful: calling Python functions from C. This is especially the
|
||||
case for libraries that support so-called "callback" functions. If a C
|
||||
interface makes use of callbacks, the equivalent Python often needs to provide a
|
||||
|
|
@ -581,7 +267,7 @@ be part of a module definition::
|
|||
}
|
||||
|
||||
This function must be registered with the interpreter using the
|
||||
:c:macro:`METH_VARARGS` flag; this is described in section :ref:`methodtable`. The
|
||||
:c:macro:`METH_VARARGS` flag in :c:type:`PyMethodDef.ml_flags`. The
|
||||
:c:func:`PyArg_ParseTuple` function and its arguments are documented in section
|
||||
:ref:`parsetuple`.
|
||||
|
||||
|
|
@ -676,14 +362,21 @@ the above example, we use :c:func:`Py_BuildValue` to construct the dictionary. :
|
|||
Py_DECREF(result);
|
||||
|
||||
|
||||
.. index:: single: PyArg_ParseTuple (C function)
|
||||
|
||||
.. _parsetuple:
|
||||
|
||||
Extracting Parameters in Extension Functions
|
||||
============================================
|
||||
|
||||
.. index:: single: PyArg_ParseTuple (C function)
|
||||
The :ref:`tutorial <first-extension-module>` uses a ":c:data:`METH_O`"
|
||||
function, which is limited to a single Python argument.
|
||||
If you want more, you can use :c:data:`METH_VARARGS` instead.
|
||||
With this flag, the C function will receive a :py:class:`tuple` of arguments
|
||||
instead of a single object.
|
||||
|
||||
The :c:func:`PyArg_ParseTuple` function is declared as follows::
|
||||
For unpacking the tuple, CPython provides the :c:func:`PyArg_ParseTuple`
|
||||
function, declared as follows::
|
||||
|
||||
int PyArg_ParseTuple(PyObject *arg, const char *format, ...);
|
||||
|
||||
|
|
@ -693,6 +386,19 @@ whose syntax is explained in :ref:`arg-parsing` in the Python/C API Reference
|
|||
Manual. The remaining arguments must be addresses of variables whose type is
|
||||
determined by the format string.
|
||||
|
||||
For example, to receive a single Python :py:class:`str` object and turn it
|
||||
into a C buffer, you would use ``"s"`` as the format string::
|
||||
|
||||
const char *command;
|
||||
if (!PyArg_ParseTuple(args, "s", &command)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
If an error is detected in the argument list, :c:func:`!PyArg_ParseTuple`
|
||||
returns ``NULL`` (the error indicator for functions returning object pointers);
|
||||
your function may return ``NULL``, relying on the exception set by
|
||||
:c:func:`PyArg_ParseTuple`.
|
||||
|
||||
Note that while :c:func:`PyArg_ParseTuple` checks that the Python arguments have
|
||||
the required types, it cannot check the validity of the addresses of C variables
|
||||
passed to the call: if you make mistakes there, your code will probably crash or
|
||||
|
|
@ -703,7 +409,6 @@ Note that any Python object references which are provided to the caller are
|
|||
|
||||
Some example calls::
|
||||
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
|
||||
::
|
||||
|
|
@ -773,6 +478,17 @@ Some example calls::
|
|||
Keyword Parameters for Extension Functions
|
||||
==========================================
|
||||
|
||||
If you also want your function to accept
|
||||
:term:`keyword arguments <keyword argument>`, use the :c:data:`METH_KEYWORDS`
|
||||
flag in combination with :c:data:`METH_VARARGS`.
|
||||
(:c:data:`!METH_KEYWORDS` can also be used with other flags; see its
|
||||
documentation for the allowed combinations.)
|
||||
|
||||
In this case, the C function should accept a third ``PyObject *`` parameter
|
||||
which will be a dictionary of keywords.
|
||||
Use :c:func:`PyArg_ParseTupleAndKeywords` to parse the arguments to such a
|
||||
function.
|
||||
|
||||
.. index:: single: PyArg_ParseTupleAndKeywords (C function)
|
||||
|
||||
The :c:func:`PyArg_ParseTupleAndKeywords` function is declared as follows::
|
||||
|
|
@ -833,19 +549,6 @@ Philbrick (philbrick@hks.com)::
|
|||
{NULL, NULL, 0, NULL} /* sentinel */
|
||||
};
|
||||
|
||||
static struct PyModuleDef keywdarg_module = {
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "keywdarg",
|
||||
.m_size = 0,
|
||||
.m_methods = keywdarg_methods,
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC
|
||||
PyInit_keywdarg(void)
|
||||
{
|
||||
return PyModuleDef_Init(&keywdarg_module);
|
||||
}
|
||||
|
||||
|
||||
.. _buildvalue:
|
||||
|
||||
|
|
@ -986,11 +689,11 @@ needed. Ownership of a reference can be transferred. There are three ways to
|
|||
dispose of an owned reference: pass it on, store it, or call :c:func:`Py_DECREF`.
|
||||
Forgetting to dispose of an owned reference creates a memory leak.
|
||||
|
||||
It is also possible to :dfn:`borrow` [#]_ a reference to an object. The
|
||||
It is also possible to :dfn:`borrow` [#borrow]_ a reference to an object. The
|
||||
borrower of a reference should not call :c:func:`Py_DECREF`. The borrower must
|
||||
not hold on to the object longer than the owner from which it was borrowed.
|
||||
Using a borrowed reference after the owner has disposed of it risks using freed
|
||||
memory and should be avoided completely [#]_.
|
||||
memory and should be avoided completely [#dont-check-refcount]_.
|
||||
|
||||
The advantage of borrowing over owning a reference is that you don't need to
|
||||
take care of disposing of the reference on all possible paths through the code
|
||||
|
|
@ -1169,7 +872,7 @@ checking.
|
|||
|
||||
The C function calling mechanism guarantees that the argument list passed to C
|
||||
functions (``args`` in the examples) is never ``NULL`` --- in fact it guarantees
|
||||
that it is always a tuple [#]_.
|
||||
that it is always a tuple [#old-calling-convention]_.
|
||||
|
||||
It is a severe error to ever let a ``NULL`` pointer "escape" to the Python user.
|
||||
|
||||
|
|
@ -1226,8 +929,8 @@ the module whose functions one wishes to call might not have been loaded yet!
|
|||
Portability therefore requires not to make any assumptions about symbol
|
||||
visibility. This means that all symbols in extension modules should be declared
|
||||
``static``, except for the module's initialization function, in order to
|
||||
avoid name clashes with other extension modules (as discussed in section
|
||||
:ref:`methodtable`). And it means that symbols that *should* be accessible from
|
||||
avoid name clashes with other extension modules. And it means that symbols
|
||||
that *should* be accessible from
|
||||
other extension modules must be exported in a different way.
|
||||
|
||||
Python provides a special mechanism to pass C-level information (pointers) from
|
||||
|
|
@ -1269,8 +972,9 @@ file corresponding to the module provides a macro that takes care of importing
|
|||
the module and retrieving its C API pointers; client modules only have to call
|
||||
this macro before accessing the C API.
|
||||
|
||||
The exporting module is a modification of the :mod:`!spam` module from section
|
||||
:ref:`extending-simpleexample`. The function :func:`!spam.system` does not call
|
||||
The exporting module is a modification of the :mod:`!spam` module from the
|
||||
:ref:`tutorial <first-extension-module>`.
|
||||
The function :func:`!spam.system` does not call
|
||||
the C library function :c:func:`system` directly, but a function
|
||||
:c:func:`!PySpam_System`, which would of course do something more complicated in
|
||||
reality (such as adding "spam" to every command). This function
|
||||
|
|
@ -1412,15 +1116,14 @@ code distribution).
|
|||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
.. [#] An interface for this function already exists in the standard module :mod:`os`
|
||||
--- it was chosen as a simple and straightforward example.
|
||||
.. [#borrow] The metaphor of "borrowing" a reference is not completely correct:
|
||||
the owner still has a copy of the reference.
|
||||
|
||||
.. [#] The metaphor of "borrowing" a reference is not completely correct: the owner
|
||||
still has a copy of the reference.
|
||||
|
||||
.. [#] Checking that the reference count is at least 1 **does not work** --- the
|
||||
.. [#dont-check-refcount] Checking that the reference count is at least 1
|
||||
**does not work** --- the
|
||||
reference count itself could be in freed memory and may thus be reused for
|
||||
another object!
|
||||
|
||||
.. [#] These guarantees don't hold when you use the "old" style calling convention ---
|
||||
.. [#old-calling-convention] These guarantees don't hold when you use the
|
||||
"old" style calling convention ---
|
||||
this is still found in much existing code.
|
||||
|
|
|
|||
667
Doc/extending/first-extension-module.rst
Normal file
667
Doc/extending/first-extension-module.rst
Normal file
|
|
@ -0,0 +1,667 @@
|
|||
.. highlight:: c
|
||||
|
||||
|
||||
.. _extending-simpleexample:
|
||||
.. _first-extension-module:
|
||||
|
||||
*********************************
|
||||
Your first C API extension module
|
||||
*********************************
|
||||
|
||||
This tutorial will take you through creating a simple
|
||||
Python extension module written in C or C++.
|
||||
|
||||
We will use the low-level Python C API directly.
|
||||
For easier ways to create extension modules, see
|
||||
the :ref:`recommended third party tools <c-api-tools>`.
|
||||
|
||||
The tutorial assumes basic knowledge about Python: you should be able to
|
||||
define functions in Python code before starting to write them in C.
|
||||
See :ref:`tutorial-index` for an introduction to Python itself.
|
||||
|
||||
The tutorial should be approachable for anyone who can write a basic C library.
|
||||
While we will mention several concepts that a C beginner would not be expected
|
||||
to know, like ``static`` functions or linkage declarations, understanding these
|
||||
is not necessary for success.
|
||||
|
||||
We will focus on giving you a "feel" of what Python's C API is like.
|
||||
It will not teach you important concepts, like error handling
|
||||
and reference counting, which are covered in later chapters.
|
||||
|
||||
We will assume that you use a Unix-like system (including macOS and
|
||||
Linux), or Windows.
|
||||
On other systems, you might need to adjust some details -- for example,
|
||||
a system command name.
|
||||
|
||||
You need to have a suitable C compiler and Python development headers installed.
|
||||
On Linux, headers are often in a package like ``python3-dev``
|
||||
or ``python3-devel``.
|
||||
|
||||
You need to be able to install Python packages.
|
||||
This tutorial uses `pip <https://pip.pypa.io/>`__ (``pip install``), but you
|
||||
can substitute any tool that can build and install ``pyproject.toml``-based
|
||||
projects, like `uv <https://docs.astral.sh/uv/>`_ (``uv pip install``).
|
||||
Preferably, have a :ref:`virtual environment <venv-def>` activated.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
This tutorial uses APIs that were added in CPython 3.15.
|
||||
To create an extension that's compatible with earlier versions of CPython,
|
||||
please follow an earlier version of this documentation.
|
||||
|
||||
This tutorial uses C syntax added in C11 and C++20.
|
||||
If your extension needs to be compatible with earlier standards,
|
||||
please follow tutorials in documentation for Python 3.14 or below.
|
||||
|
||||
|
||||
What we'll do
|
||||
=============
|
||||
|
||||
Let's create an extension module called ``spam`` [#why-spam]_,
|
||||
which will include a Python interface to the C
|
||||
standard library function :c:func:`system`.
|
||||
This function is defined in ``stdlib.h``.
|
||||
It takes a C string as argument, runs the argument as a system
|
||||
command, and returns a result value as an integer.
|
||||
A manual page for :c:func:`system` might summarize it this way::
|
||||
|
||||
#include <stdlib.h>
|
||||
int system(const char *command);
|
||||
|
||||
Note that like many functions in the C standard library,
|
||||
this function is already exposed in Python.
|
||||
In production, use :py:func:`os.system` or :py:func:`subprocess.run`
|
||||
rather than the module you'll write here.
|
||||
|
||||
We want this function to be callable from Python as follows:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import spam
|
||||
>>> status = spam.system("whoami")
|
||||
User Name
|
||||
>>> status
|
||||
0
|
||||
|
||||
.. note::
|
||||
|
||||
The system command ``whoami`` prints out your username.
|
||||
It's useful in tutorials like this one because it has the same name on
|
||||
both Unix and Windows.
|
||||
|
||||
|
||||
Start with the headers
|
||||
======================
|
||||
|
||||
Begin by creating a directory for this tutorial, and switching to it
|
||||
on the command line.
|
||||
Then, create a file named :file:`spammodule.c` in your directory.
|
||||
[#why-spammodule]_
|
||||
|
||||
In this file, we'll include two headers: :file:`Python.h` to pull in
|
||||
all declarations of the Python C API, and :file:`stdlib.h` for the
|
||||
:c:func:`system` function. [#stdlib-h]_
|
||||
|
||||
Add the following lines to :file:`spammodule.c`:
|
||||
|
||||
.. literalinclude:: ../includes/capi-extension/spammodule-01.c
|
||||
:start-at: <Python.h>
|
||||
:end-at: <stdlib.h>
|
||||
|
||||
Be sure to put :file:`stdlib.h`, and any other standard library includes,
|
||||
*after* :file:`Python.h`.
|
||||
On some systems, Python may define some pre-processor definitions
|
||||
that affect the standard headers.
|
||||
|
||||
|
||||
Running your build tool
|
||||
=======================
|
||||
|
||||
With only the includes in place, your extension won't do anything.
|
||||
Still, it's a good time to compile it and try to import it.
|
||||
This will ensure that your build tool works, so that you can make
|
||||
and test incremental changes as you follow the rest of the text.
|
||||
|
||||
CPython itself does not come with a tool to build extension modules;
|
||||
it is recommended to use a third-party project for this.
|
||||
In this tutorial, we'll use `meson-python`_.
|
||||
(If you want to use another one, see :ref:`first-extension-other-tools`.)
|
||||
|
||||
.. at the time of writing, meson-python has the least overhead for a
|
||||
simple extension using PyModExport.
|
||||
Change this if another tool makes things easier.
|
||||
|
||||
``meson-python`` requires defining a "project" using two extra files.
|
||||
|
||||
First, add ``pyproject.toml`` with these contents:
|
||||
|
||||
.. code-block:: toml
|
||||
|
||||
[build-system]
|
||||
build-backend = 'mesonpy'
|
||||
requires = ['meson-python']
|
||||
|
||||
[project]
|
||||
# Placeholder project information
|
||||
# (change this before distributing the module)
|
||||
name = 'sampleproject'
|
||||
version = '0'
|
||||
|
||||
Then, create ``meson.build`` containing the following:
|
||||
|
||||
.. code-block:: meson
|
||||
|
||||
project('sampleproject', 'c')
|
||||
|
||||
py = import('python').find_installation(pure: false)
|
||||
|
||||
py.extension_module(
|
||||
'spam', # name of the importable Python module
|
||||
'spammodule.c', # the C source file
|
||||
install: true,
|
||||
)
|
||||
|
||||
.. note::
|
||||
|
||||
See `meson-python documentation <meson-python>`_ for details on
|
||||
configuration.
|
||||
|
||||
Now, build install the *project in the current directory* (``.``) via ``pip``:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
python -m pip install .
|
||||
|
||||
.. tip::
|
||||
|
||||
If you don't have ``pip`` installed, run ``python -m ensurepip``,
|
||||
preferably in a :ref:`virtual environment <venv-def>`.
|
||||
(Or, if you prefer another tool that can build and install
|
||||
``pyproject.toml``-based projects, use that.)
|
||||
|
||||
.. _meson-python: https://mesonbuild.com/meson-python/
|
||||
.. _virtual environment: https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/#create-and-use-virtual-environments
|
||||
|
||||
Note that you will need to run this command again every time you change your
|
||||
extension.
|
||||
Unlike Python, C has an explicit compilation step.
|
||||
|
||||
When your extension is compiled and installed, start Python and try to
|
||||
import it.
|
||||
This should fail with the following exception:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import spam
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ImportError: dynamic module does not define module export function (PyModExport_spam or PyInit_spam)
|
||||
|
||||
|
||||
Module export hook
|
||||
==================
|
||||
|
||||
The exception you got when you tried to import the module told you that Python
|
||||
is looking for a "module export function", also known as a
|
||||
:ref:`module export hook <extension-export-hook>`.
|
||||
Let's define one.
|
||||
|
||||
First, add a prototype below the ``#include`` lines:
|
||||
|
||||
.. literalinclude:: ../includes/capi-extension/spammodule-01.c
|
||||
:start-after: /// Export hook prototype
|
||||
:end-before: ///
|
||||
|
||||
.. tip::
|
||||
The prototype is not strictly necessary, but some modern compilers emit
|
||||
warnings without it.
|
||||
It's generally better to add the prototype than to disable the warning.
|
||||
|
||||
The :c:macro:`PyMODEXPORT_FUNC` macro declares the function's
|
||||
return type, and adds any special linkage declarations needed
|
||||
to make the function visible and usable when CPython loads it.
|
||||
|
||||
After the prototype, add the function itself.
|
||||
For now, make it return ``NULL``:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
PyMODEXPORT_FUNC
|
||||
PyModExport_spam(void)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Compile and load the module again.
|
||||
You should get a different error this time.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import spam
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
SystemError: module export hook for module 'spam' failed without setting an exception
|
||||
|
||||
Simply returning ``NULL`` is *not* correct behavior for an export hook,
|
||||
and CPython complains about it.
|
||||
That's good -- it means that CPython found the function!
|
||||
Let's now make it do something useful.
|
||||
|
||||
|
||||
The slot table
|
||||
==============
|
||||
|
||||
Rather than ``NULL``, the export hook should return the information needed to
|
||||
create a module.
|
||||
Let's start with the basics: the name and docstring.
|
||||
|
||||
The information should be defined in a ``static`` array of
|
||||
:c:type:`PyModuleDef_Slot` entries, which are essentially key-value pairs.
|
||||
Define this array just before your export hook:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
static PyModuleDef_Slot spam_slots[] = {
|
||||
{Py_mod_name, "spam"},
|
||||
{Py_mod_doc, "A wonderful module with an example function"},
|
||||
{0, NULL}
|
||||
};
|
||||
|
||||
For both :c:data:`Py_mod_name` and :c:data:`Py_mod_doc`, the values are C
|
||||
strings -- that is, NUL-terminated, UTF-8 encoded byte arrays.
|
||||
|
||||
Note the zero-filled sentinel entry at the end.
|
||||
If you forget it, you'll trigger undefined behavior.
|
||||
|
||||
The array is defined as ``static`` -- that is, not visible outside this ``.c`` file.
|
||||
This will be a common theme.
|
||||
CPython only needs to access the export hook; all global variables
|
||||
and all other functions should generally be ``static``, so that they don't
|
||||
clash with other extensions.
|
||||
|
||||
Return this array from your export hook instead of ``NULL``:
|
||||
|
||||
.. code-block:: c
|
||||
:emphasize-lines: 4
|
||||
|
||||
PyMODEXPORT_FUNC
|
||||
PyModExport_spam(void)
|
||||
{
|
||||
return spam_slots;
|
||||
}
|
||||
|
||||
Now, recompile and try it out:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import spam
|
||||
>>> print(spam)
|
||||
<module 'spam' from '/home/encukou/dev/cpython/spam.so'>
|
||||
|
||||
You have an extension module!
|
||||
Try ``help(spam)`` to see the docstring.
|
||||
|
||||
The next step will be adding a function.
|
||||
|
||||
|
||||
.. _backtoexample:
|
||||
|
||||
Exposing a function
|
||||
===================
|
||||
|
||||
To expose the :c:func:`system` C function directly to Python,
|
||||
we'll need to write a layer of glue code to convert arguments from Python
|
||||
objects to C values, and the C return value back to Python.
|
||||
|
||||
One of the simplest ways to write glue code is a ":c:data:`METH_O`" function,
|
||||
which takes two Python objects and returns one.
|
||||
All Python objects -- regardless of the Python type -- are represented in C
|
||||
as pointers to the :c:type:`PyObject` structure.
|
||||
|
||||
Add such a function above the slots array::
|
||||
|
||||
static PyObject *
|
||||
spam_system(PyObject *self, PyObject *arg)
|
||||
{
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
For now, we ignore the arguments, and use the :c:macro:`Py_RETURN_NONE`
|
||||
macro, which expands to a ``return`` statement that properly returns
|
||||
a Python :py:data:`None` object.
|
||||
|
||||
Recompile your extension to make sure you don't have syntax errors.
|
||||
We haven't yet added ``spam_system`` to the module, so you might get a
|
||||
warning that ``spam_system`` is unused.
|
||||
|
||||
.. _methodtable:
|
||||
|
||||
Method definitions
|
||||
------------------
|
||||
|
||||
To expose the C function to Python, you will need to provide several pieces of
|
||||
information in a structure called
|
||||
:c:type:`PyMethodDef` [#why-pymethoddef]_:
|
||||
|
||||
* ``ml_name``: the name of the Python function;
|
||||
* ``ml_doc``: a docstring;
|
||||
* ``ml_meth``: the C function to be called; and
|
||||
* ``ml_flags``: a set of flags describing details like how Python arguments are
|
||||
passed to the C function.
|
||||
We'll use :c:data:`METH_O` here -- the flag that matches our
|
||||
``spam_system`` function's signature.
|
||||
|
||||
Because modules typically create several functions, these definitions
|
||||
need to be collected in an array, with a zero-filled sentinel at the end.
|
||||
Add this array just below the ``spam_system`` function:
|
||||
|
||||
.. literalinclude:: ../includes/capi-extension/spammodule-01.c
|
||||
:start-after: /// Module method table
|
||||
:end-before: ///
|
||||
|
||||
As with module slots, a zero-filled sentinel marks the end of the array.
|
||||
|
||||
Next, we'll add the method to the module.
|
||||
Add a :c:data:`Py_mod_methods` slot to your :c:type:`PyMethodDef` array:
|
||||
|
||||
.. literalinclude:: ../includes/capi-extension/spammodule-01.c
|
||||
:start-after: /// Module slot table
|
||||
:end-before: ///
|
||||
:emphasize-lines: 5
|
||||
|
||||
Recompile your extension again, and test it.
|
||||
Be sure to restart the Python interpreter, so that ``import spam`` picks
|
||||
up the new version of the module.
|
||||
|
||||
You should now be able to call the function:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import spam
|
||||
>>> print(spam.system)
|
||||
<built-in function system>
|
||||
>>> print(spam.system('whoami'))
|
||||
None
|
||||
|
||||
Note that our ``spam.system`` does not yet run the ``whoami`` command;
|
||||
it only returns ``None``.
|
||||
|
||||
Check that the function accepts exactly one argument, as specified by
|
||||
the :c:data:`METH_O` flag:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> print(spam.system('too', 'many', 'arguments'))
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: spam.system() takes exactly one argument (3 given)
|
||||
|
||||
|
||||
Returning an integer
|
||||
====================
|
||||
|
||||
Now, let's take a look at the return value.
|
||||
Instead of ``None``, we'll want ``spam.system`` to return a number -- that is,
|
||||
a Python :py:type:`int` object.
|
||||
Eventually this will be the exit code of a system command,
|
||||
but let's start with a fixed value, say, ``3``.
|
||||
|
||||
The Python C API provides a function to create a Python :py:type:`int` object
|
||||
from a C ``int`` value: :c:func:`PyLong_FromLong`. [#why-pylongfromlong]_
|
||||
|
||||
To call it, replace the ``Py_RETURN_NONE`` with the following 3 lines:
|
||||
|
||||
.. this could be a one-liner, but we want to show the data types here
|
||||
|
||||
.. code-block:: c
|
||||
:emphasize-lines: 4-6
|
||||
|
||||
static PyObject *
|
||||
spam_system(PyObject *self, PyObject *arg)
|
||||
{
|
||||
int status = 3;
|
||||
PyObject *result = PyLong_FromLong(status);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Recompile, restart the Python interpreter again,
|
||||
and check that the function now returns 3:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import spam
|
||||
>>> spam.system('whoami')
|
||||
3
|
||||
|
||||
|
||||
Accepting a string
|
||||
==================
|
||||
|
||||
Finally, let's handle the function argument.
|
||||
|
||||
Our C function, :c:func:`!spam_system`, takes two arguments.
|
||||
The first one, ``PyObject *self``, will be set to the ``spam`` module
|
||||
object.
|
||||
This isn't useful in our case, so we'll ignore it.
|
||||
|
||||
The other one, ``PyObject *arg``, will be set to the object that the user
|
||||
passed from Python.
|
||||
We expect that it should be a Python string.
|
||||
In order to use the information in it, we will need
|
||||
to convert it to a C value -- in this case, a C string (``const char *``).
|
||||
|
||||
There's a slight type mismatch here: Python's :py:class:`str` objects store
|
||||
Unicode text, but C strings are arrays of bytes.
|
||||
So, we'll need to *encode* the data, and we'll use the UTF-8 encoding for it.
|
||||
(UTF-8 might not always be correct for system commands, but it's what
|
||||
:py:meth:`str.encode` uses by default,
|
||||
and the C API has special support for it.)
|
||||
|
||||
The function to encode a Python string into a UTF-8 buffer is named
|
||||
:c:func:`PyUnicode_AsUTF8` [#why-pyunicodeasutf8]_.
|
||||
Call it like this:
|
||||
|
||||
.. code-block:: c
|
||||
:emphasize-lines: 4
|
||||
|
||||
static PyObject *
|
||||
spam_system(PyObject *self, PyObject *arg)
|
||||
{
|
||||
const char *command = PyUnicode_AsUTF8(arg);
|
||||
int status = 3;
|
||||
PyObject *result = PyLong_FromLong(status);
|
||||
return result;
|
||||
}
|
||||
|
||||
If :c:func:`PyUnicode_AsUTF8` is successful, *command* will point to the
|
||||
resulting array of bytes.
|
||||
This buffer is managed by the *arg* object, which means we don't need to free
|
||||
it, but we must follow some rules:
|
||||
|
||||
* We should only use the buffer inside the ``spam_system`` function.
|
||||
When ``spam_system`` returns, *arg* and the buffer it manages might be
|
||||
garbage-collected.
|
||||
* We must not modify it. This is why we use ``const``.
|
||||
|
||||
If :c:func:`PyUnicode_AsUTF8` was *not* successful, it returns a ``NULL``
|
||||
pointer.
|
||||
When calling *any* Python C API, we always need to handle such error cases.
|
||||
The way to do this in general is left for later chapters of this documentation.
|
||||
For now, be assured that we are already handling errors from
|
||||
:c:func:`PyLong_FromLong` correctly.
|
||||
|
||||
For the :c:func:`PyUnicode_AsUTF8` call, the correct way to handle errors is
|
||||
returning ``NULL`` from ``spam_system``.
|
||||
Add an ``if`` block for this:
|
||||
|
||||
|
||||
.. code-block:: c
|
||||
:emphasize-lines: 5-7
|
||||
|
||||
static PyObject *
|
||||
spam_system(PyObject *self, PyObject *arg)
|
||||
{
|
||||
const char *command = PyUnicode_AsUTF8(arg);
|
||||
if (command == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
int status = 3;
|
||||
PyObject *result = PyLong_FromLong(status);
|
||||
return result;
|
||||
}
|
||||
|
||||
That's it for the setup.
|
||||
Now, all that is left is calling the C library function :c:func:`system` with
|
||||
the ``char *`` buffer, and using its result instead of the ``3``:
|
||||
|
||||
.. code-block:: c
|
||||
:emphasize-lines: 8
|
||||
|
||||
static PyObject *
|
||||
spam_system(PyObject *self, PyObject *arg)
|
||||
{
|
||||
const char *command = PyUnicode_AsUTF8(arg);
|
||||
if (command == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
int status = system(command);
|
||||
PyObject *result = PyLong_FromLong(status);
|
||||
return result;
|
||||
}
|
||||
|
||||
Compile your module, restart Python, and test.
|
||||
This time, you should see your username -- the output of the ``whoami``
|
||||
system command:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import spam
|
||||
>>> result = spam.system('whoami')
|
||||
User Name
|
||||
>>> result
|
||||
0
|
||||
|
||||
You might also want to test error cases:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import spam
|
||||
>>> result = spam.system('nonexistent-command')
|
||||
sh: line 1: nonexistent-command: command not found
|
||||
>>> result
|
||||
32512
|
||||
|
||||
>>> spam.system(3)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: bad argument type for built-in operation
|
||||
|
||||
|
||||
The result
|
||||
==========
|
||||
|
||||
|
||||
Congratulations!
|
||||
You have written a complete Python C API extension module,
|
||||
and completed this tutorial!
|
||||
|
||||
Here is the entire source file, for your convenience:
|
||||
|
||||
.. _extending-spammodule-source:
|
||||
|
||||
.. literalinclude:: ../includes/capi-extension/spammodule-01.c
|
||||
:start-at: ///
|
||||
|
||||
|
||||
.. _first-extension-other-tools:
|
||||
|
||||
Appendix: Other build tools
|
||||
===========================
|
||||
|
||||
You should be able to follow this tutorial -- except the
|
||||
*Running your build tool* section itself -- with a build tool other
|
||||
than ``meson-python``.
|
||||
|
||||
The Python Packaging User Guide has a `list of recommended tools <https://packaging.python.org/en/latest/guides/tool-recommendations/#build-backends-for-extension-modules>`_;
|
||||
be sure to choose one for the C language.
|
||||
|
||||
|
||||
Workaround for missing PyInit function
|
||||
--------------------------------------
|
||||
|
||||
If your build tool output complains about missing ``PyInit_spam``,
|
||||
add the following function to your module for now:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
// A workaround
|
||||
void *PyInit_spam(void) { return NULL; }
|
||||
|
||||
This is a shim for an old-style :ref:`initialization function <extension-export-hook>`,
|
||||
which was required in extension modules for CPython 3.14 and below.
|
||||
Current CPython does not need it, but some build tools may still assume that
|
||||
all extension modules need to define it.
|
||||
|
||||
If you use this workaround, you will get the exception
|
||||
``SystemError: initialization of spam failed without raising an exception``
|
||||
instead of
|
||||
``ImportError: dynamic module does not define module export function``.
|
||||
|
||||
|
||||
Compiling directly
|
||||
------------------
|
||||
|
||||
Using a third-party build tool is heavily recommended,
|
||||
as it will take care of various details of your platform and Python
|
||||
installation, of naming the resulting extension, and, later, of distributing
|
||||
your work.
|
||||
|
||||
If you are building an extension for as *specific* system, or for yourself
|
||||
only, you might instead want to run your compiler directly.
|
||||
The way to do this is system-specific; be prepared for issues you will need
|
||||
to solve yourself.
|
||||
|
||||
Linux
|
||||
^^^^^
|
||||
|
||||
On Linux, the Python development package may include a ``python3-config``
|
||||
command that prints out the required compiler flags.
|
||||
If you use it, check that it corresponds to the CPython interpreter you'll use
|
||||
to load the module.
|
||||
Then, start with the following command:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
gcc --shared $(python3-config --cflags --ldflags) spammodule.c -o spam.so
|
||||
|
||||
This should generate a ``spam.so`` file that you need to put in a directory
|
||||
on :py:attr:`sys.path`.
|
||||
|
||||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
.. [#why-spam] ``spam`` is the favorite food of Monty Python fans...
|
||||
.. [#why-spammodule] The source file name is entirely up to you,
|
||||
though some tools can be picky about the ``.c`` extension.
|
||||
This tutorial uses the traditional ``*module.c`` suffix.
|
||||
Some people would just use :file:`spam.c` to implement a module
|
||||
named ``spam``,
|
||||
projects where Python isn't the primary language might use ``py_spam.c``,
|
||||
and so on.
|
||||
.. [#stdlib-h] Including :file:`stdlib.h` is technically not necessary,
|
||||
since :file:`Python.h` includes it and
|
||||
:ref:`several other standard headers <capi-system-includes>` for its own use
|
||||
or for backwards compatibility.
|
||||
However, it is good practice to explicitly include what you need.
|
||||
.. [#why-pymethoddef] The :c:type:`!PyMethodDef` structure is also used
|
||||
to create methods of classes, so there's no separate
|
||||
":c:type:`!PyFunctionDef`".
|
||||
.. [#why-pylongfromlong] The name :c:func:`PyLong_FromLong`
|
||||
might not seem obvious.
|
||||
``PyLong`` refers to a the Python :py:class:`int`, which was originally
|
||||
called ``long``; the ``FromLong`` refers to the C ``long`` (or ``long int``)
|
||||
type.
|
||||
.. [#why-pyunicodeasutf8] Here, ``PyUnicode`` refers to the original name of
|
||||
the Python :py:class:`str` class: ``unicode``.
|
||||
|
|
@ -5,15 +5,17 @@
|
|||
##################################################
|
||||
|
||||
This document describes how to write modules in C or C++ to extend the Python
|
||||
interpreter with new modules. Those modules can not only define new functions
|
||||
but also new object types and their methods. The document also describes how
|
||||
interpreter with new modules. Those modules can do what Python code does --
|
||||
define functions, object types and methods -- but also interact with native
|
||||
libraries or achieve better performance by avoiding the overhead of an
|
||||
interpreter. The document also describes how
|
||||
to embed the Python interpreter in another application, for use as an extension
|
||||
language. Finally, it shows how to compile and link extension modules so that
|
||||
they can be loaded dynamically (at run time) into the interpreter, if the
|
||||
underlying operating system supports this feature.
|
||||
|
||||
This document assumes basic knowledge about Python. For an informal
|
||||
introduction to the language, see :ref:`tutorial-index`. :ref:`reference-index`
|
||||
This document assumes basic knowledge about C and Python. For an informal
|
||||
introduction to Python, see :ref:`tutorial-index`. :ref:`reference-index`
|
||||
gives a more formal definition of the language. :ref:`library-index` documents
|
||||
the existing object types, functions and modules (both built-in and written in
|
||||
Python) that give the language its wide application range.
|
||||
|
|
@ -21,37 +23,75 @@ Python) that give the language its wide application range.
|
|||
For a detailed description of the whole Python/C API, see the separate
|
||||
:ref:`c-api-index`.
|
||||
|
||||
To support extensions, Python's C API (Application Programmers Interface)
|
||||
defines a set of functions, macros and variables that provide access to most
|
||||
aspects of the Python run-time system. The Python API is incorporated in a C
|
||||
source file by including the header ``"Python.h"``.
|
||||
|
||||
.. note::
|
||||
|
||||
The C extension interface is specific to CPython, and extension modules do
|
||||
not work on other Python implementations. In many cases, it is possible to
|
||||
avoid writing C extensions and preserve portability to other implementations.
|
||||
For example, if your use case is calling C library functions or system calls,
|
||||
you should consider using the :mod:`ctypes` module or the `cffi
|
||||
<https://cffi.readthedocs.io/>`_ library rather than writing
|
||||
custom C code.
|
||||
These modules let you write Python code to interface with C code and are more
|
||||
portable between implementations of Python than writing and compiling a C
|
||||
extension module.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
||||
first-extension-module.rst
|
||||
extending.rst
|
||||
newtypes_tutorial.rst
|
||||
newtypes.rst
|
||||
building.rst
|
||||
windows.rst
|
||||
embedding.rst
|
||||
|
||||
|
||||
Recommended third party tools
|
||||
=============================
|
||||
|
||||
This guide only covers the basic tools for creating extensions provided
|
||||
This document only covers the basic tools for creating extensions provided
|
||||
as part of this version of CPython. Some :ref:`third party tools
|
||||
<c-api-tools>` offer both simpler and more sophisticated approaches to creating
|
||||
C and C++ extensions for Python.
|
||||
|
||||
While this document is aimed at extension authors, it should also be helpful to
|
||||
the authors of such tools.
|
||||
For example, the tutorial module can serve as a simple test case for a build
|
||||
tool or sample expected output of a code generator.
|
||||
|
||||
Creating extensions without third party tools
|
||||
=============================================
|
||||
|
||||
C API Tutorial
|
||||
==============
|
||||
|
||||
This tutorial describes how to write a simple module in C or C++,
|
||||
using the Python C API -- that is, using the basic tools provided
|
||||
as part of this version of CPython.
|
||||
|
||||
|
||||
#. :ref:`first-extension-module`
|
||||
|
||||
|
||||
Guides for intermediate topics
|
||||
==============================
|
||||
|
||||
This section of the guide covers creating C and C++ extensions without
|
||||
assistance from third party tools. It is intended primarily for creators
|
||||
of those tools, rather than being a recommended way to create your own
|
||||
C extensions.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:pep:`489` -- Multi-phase extension module initialization
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:numbered:
|
||||
|
||||
extending.rst
|
||||
newtypes_tutorial.rst
|
||||
newtypes.rst
|
||||
building.rst
|
||||
windows.rst
|
||||
* :ref:`extending-intro`
|
||||
* :ref:`defining-new-types`
|
||||
* :ref:`new-types-topics`
|
||||
* :ref:`building`
|
||||
* :ref:`building-on-windows`
|
||||
|
||||
Embedding the CPython runtime in a larger application
|
||||
=====================================================
|
||||
|
|
@ -61,8 +101,4 @@ interpreter as the main application, it is desirable to instead embed
|
|||
the CPython runtime inside a larger application. This section covers
|
||||
some of the details involved in doing that successfully.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:numbered:
|
||||
|
||||
embedding.rst
|
||||
* :ref:`embedding`
|
||||
|
|
|
|||
172
Doc/glossary.rst
172
Doc/glossary.rst
|
|
@ -134,6 +134,14 @@ Glossary
|
|||
iterator's :meth:`~object.__anext__` method until it raises a
|
||||
:exc:`StopAsyncIteration` exception. Introduced by :pep:`492`.
|
||||
|
||||
atomic operation
|
||||
An operation that appears to execute as a single, indivisible step: no
|
||||
other thread can observe it half-done, and its effects become visible all
|
||||
at once. Python does not guarantee that high-level statements are atomic
|
||||
(for example, ``x += 1`` performs multiple bytecode operations and is not
|
||||
atomic). Atomicity is only guaranteed where explicitly documented. See
|
||||
also :term:`race condition` and :term:`data race`.
|
||||
|
||||
attached thread state
|
||||
|
||||
A :term:`thread state` that is active for the current OS thread.
|
||||
|
|
@ -289,6 +297,22 @@ Glossary
|
|||
advanced mathematical feature. If you're not aware of a need for them,
|
||||
it's almost certain you can safely ignore them.
|
||||
|
||||
concurrency
|
||||
The ability of a computer program to perform multiple tasks at the same
|
||||
time. Python provides libraries for writing programs that make use of
|
||||
different forms of concurrency. :mod:`asyncio` is a library for dealing
|
||||
with asynchronous tasks and coroutines. :mod:`threading` provides
|
||||
access to operating system threads and :mod:`multiprocessing` to
|
||||
operating system processes. Multi-core processors can execute threads and
|
||||
processes on different CPU cores at the same time (see
|
||||
:term:`parallelism`).
|
||||
|
||||
concurrent modification
|
||||
When multiple threads modify shared data at the same time. Concurrent
|
||||
modification without proper synchronization can cause
|
||||
:term:`race conditions <race condition>`, and might also trigger a
|
||||
:term:`data race <data race>`, data corruption, or both.
|
||||
|
||||
context
|
||||
This term has different meanings depending on where and how it is used.
|
||||
Some common meanings:
|
||||
|
|
@ -363,6 +387,28 @@ Glossary
|
|||
the :term:`cyclic garbage collector <garbage collection>` is to identify these groups and break the reference
|
||||
cycles so that the memory can be reclaimed.
|
||||
|
||||
data race
|
||||
A situation where multiple threads access the same memory location
|
||||
concurrently, at least one of the accesses is a write, and the threads
|
||||
do not use any synchronization to control their access. Data races
|
||||
lead to :term:`non-deterministic` behavior and can cause data corruption.
|
||||
Proper use of :term:`locks <lock>` and other :term:`synchronization primitives
|
||||
<synchronization primitive>` prevents data races. Note that data races
|
||||
can only happen in native code, but that :term:`native code` might be
|
||||
exposed in a Python API. See also :term:`race condition` and
|
||||
:term:`thread-safe`.
|
||||
|
||||
deadlock
|
||||
A situation in which two or more tasks (threads, processes, or coroutines)
|
||||
wait indefinitely for each other to release resources or complete actions,
|
||||
preventing any from making progress. For example, if thread A holds lock
|
||||
1 and waits for lock 2, while thread B holds lock 2 and waits for lock 1,
|
||||
both threads will wait indefinitely. In Python this often arises from
|
||||
acquiring multiple locks in conflicting orders or from circular
|
||||
join/await dependencies. Deadlocks can be avoided by always acquiring
|
||||
multiple :term:`locks <lock>` in a consistent order. See also
|
||||
:term:`lock` and :term:`reentrant`.
|
||||
|
||||
decorator
|
||||
A function returning another function, usually applied as a function
|
||||
transformation using the ``@wrapper`` syntax. Common examples for
|
||||
|
|
@ -662,6 +708,14 @@ Glossary
|
|||
requires the GIL to be held in order to use it. This refers to having an
|
||||
:term:`attached thread state`.
|
||||
|
||||
global state
|
||||
Data that is accessible throughout a program, such as module-level
|
||||
variables, class variables, or C static variables in :term:`extension modules
|
||||
<extension module>`. In multi-threaded programs, global state shared
|
||||
between threads typically requires synchronization to avoid
|
||||
:term:`race conditions <race condition>` and
|
||||
:term:`data races <data race>`.
|
||||
|
||||
hash-based pyc
|
||||
A bytecode cache file that uses the hash rather than the last-modified
|
||||
time of the corresponding source file to determine its validity. See
|
||||
|
|
@ -706,7 +760,9 @@ Glossary
|
|||
tuples. Such an object cannot be altered. A new object has to
|
||||
be created if a different value has to be stored. They play an important
|
||||
role in places where a constant hash value is needed, for example as a key
|
||||
in a dictionary.
|
||||
in a dictionary. Immutable objects are inherently :term:`thread-safe`
|
||||
because their state cannot be modified after creation, eliminating concerns
|
||||
about improperly synchronized :term:`concurrent modification`.
|
||||
|
||||
import path
|
||||
A list of locations (or :term:`path entries <path entry>`) that are
|
||||
|
|
@ -796,8 +852,9 @@ Glossary
|
|||
|
||||
CPython does not consistently apply the requirement that an iterator
|
||||
define :meth:`~iterator.__iter__`.
|
||||
And also please note that the free-threading CPython does not guarantee
|
||||
the thread-safety of iterator operations.
|
||||
And also please note that :term:`free-threaded <free threading>`
|
||||
CPython does not guarantee :term:`thread-safe` behavior of iterator
|
||||
operations.
|
||||
|
||||
|
||||
key function
|
||||
|
|
@ -835,10 +892,11 @@ Glossary
|
|||
:keyword:`if` statements.
|
||||
|
||||
In a multi-threaded environment, the LBYL approach can risk introducing a
|
||||
race condition between "the looking" and "the leaping". For example, the
|
||||
code, ``if key in mapping: return mapping[key]`` can fail if another
|
||||
:term:`race condition` between "the looking" and "the leaping". For example,
|
||||
the code, ``if key in mapping: return mapping[key]`` can fail if another
|
||||
thread removes *key* from *mapping* after the test, but before the lookup.
|
||||
This issue can be solved with locks or by using the EAFP approach.
|
||||
This issue can be solved with :term:`locks <lock>` or by using the
|
||||
:term:`EAFP` approach. See also :term:`thread-safe`.
|
||||
|
||||
lexical analyzer
|
||||
|
||||
|
|
@ -857,6 +915,19 @@ Glossary
|
|||
clause is optional. If omitted, all elements in ``range(256)`` are
|
||||
processed.
|
||||
|
||||
lock
|
||||
A :term:`synchronization primitive` that allows only one thread at a
|
||||
time to access a shared resource. A thread must acquire a lock before
|
||||
accessing the protected resource and release it afterward. If a thread
|
||||
attempts to acquire a lock that is already held by another thread, it
|
||||
will block until the lock becomes available. Python's :mod:`threading`
|
||||
module provides :class:`~threading.Lock` (a basic lock) and
|
||||
:class:`~threading.RLock` (a :term:`reentrant` lock). Locks are used
|
||||
to prevent :term:`race conditions <race condition>` and ensure
|
||||
:term:`thread-safe` access to shared data. Alternative design patterns
|
||||
to locks exist such as queues, producer/consumer patterns, and
|
||||
thread-local state. See also :term:`deadlock`, and :term:`reentrant`.
|
||||
|
||||
loader
|
||||
An object that loads a module.
|
||||
It must define the :meth:`!exec_module` and :meth:`!create_module` methods
|
||||
|
|
@ -942,8 +1013,11 @@ Glossary
|
|||
See :term:`method resolution order`.
|
||||
|
||||
mutable
|
||||
Mutable objects can change their value but keep their :func:`id`. See
|
||||
also :term:`immutable`.
|
||||
An :term:`object` with state that is allowed to change during the course
|
||||
of the program. In multi-threaded programs, mutable objects that are
|
||||
shared between threads require careful synchronization to avoid
|
||||
:term:`race conditions <race condition>`. See also :term:`immutable`,
|
||||
:term:`thread-safe`, and :term:`concurrent modification`.
|
||||
|
||||
named tuple
|
||||
The term "named tuple" applies to any type or class that inherits from
|
||||
|
|
@ -995,6 +1069,13 @@ Glossary
|
|||
|
||||
See also :term:`module`.
|
||||
|
||||
native code
|
||||
Code that is compiled to machine instructions and runs directly on the
|
||||
processor, as opposed to code that is interpreted or runs in a virtual
|
||||
machine. In the context of Python, native code typically refers to
|
||||
C, C++, Rust or Fortran code in :term:`extension modules <extension module>`
|
||||
that can be called from Python. See also :term:`extension module`.
|
||||
|
||||
nested scope
|
||||
The ability to refer to a variable in an enclosing definition. For
|
||||
instance, a function defined inside another function can refer to
|
||||
|
|
@ -1011,6 +1092,15 @@ Glossary
|
|||
properties, :meth:`~object.__getattribute__`, class methods, and static
|
||||
methods.
|
||||
|
||||
non-deterministic
|
||||
Behavior where the outcome of a program can vary between executions with
|
||||
the same inputs. In multi-threaded programs, non-deterministic behavior
|
||||
often results from :term:`race conditions <race condition>` where the
|
||||
relative timing or interleaving of threads affects the result.
|
||||
Proper synchronization using :term:`locks <lock>` and other
|
||||
:term:`synchronization primitives <synchronization primitive>` helps
|
||||
ensure deterministic behavior.
|
||||
|
||||
object
|
||||
Any data with state (attributes or value) and defined behavior
|
||||
(methods). Also the ultimate base class of any :term:`new-style
|
||||
|
|
@ -1041,6 +1131,16 @@ Glossary
|
|||
|
||||
See also :term:`regular package` and :term:`namespace package`.
|
||||
|
||||
parallelism
|
||||
Executing multiple operations at the same time (e.g. on multiple CPU
|
||||
cores). In Python builds with the
|
||||
:term:`global interpreter lock (GIL) <global interpreter lock>`, only one
|
||||
thread runs Python bytecode at a time, so taking advantage of multiple
|
||||
CPU cores typically involves multiple processes
|
||||
(e.g. :mod:`multiprocessing`) or native extensions that release the GIL.
|
||||
In :term:`free-threaded <free threading>` Python, multiple Python threads
|
||||
can run Python code simultaneously on different cores.
|
||||
|
||||
parameter
|
||||
A named entity in a :term:`function` (or method) definition that
|
||||
specifies an :term:`argument` (or in some cases, arguments) that the
|
||||
|
|
@ -1215,6 +1315,18 @@ Glossary
|
|||
>>> email.mime.text.__name__
|
||||
'email.mime.text'
|
||||
|
||||
race condition
|
||||
A condition of a program where the its behavior
|
||||
depends on the relative timing or ordering of events, particularly in
|
||||
multi-threaded programs. Race conditions can lead to
|
||||
:term:`non-deterministic` behavior and bugs that are difficult to
|
||||
reproduce. A :term:`data race` is a specific type of race condition
|
||||
involving unsynchronized access to shared memory. The :term:`LBYL`
|
||||
coding style is particularly susceptible to race conditions in
|
||||
multi-threaded code. Using :term:`locks <lock>` and other
|
||||
:term:`synchronization primitives <synchronization primitive>`
|
||||
helps prevent race conditions.
|
||||
|
||||
reference count
|
||||
The number of references to an object. When the reference count of an
|
||||
object drops to zero, it is deallocated. Some objects are
|
||||
|
|
@ -1236,6 +1348,25 @@ Glossary
|
|||
|
||||
See also :term:`namespace package`.
|
||||
|
||||
reentrant
|
||||
A property of a function or :term:`lock` that allows it to be called or
|
||||
acquired multiple times by the same thread without causing errors or a
|
||||
:term:`deadlock`.
|
||||
|
||||
For functions, reentrancy means the function can be safely called again
|
||||
before a previous invocation has completed, which is important when
|
||||
functions may be called recursively or from signal handlers. Thread-unsafe
|
||||
functions may be :term:`non-deterministic` if they're called reentrantly in a
|
||||
multithreaded program.
|
||||
|
||||
For locks, Python's :class:`threading.RLock` (reentrant lock) is
|
||||
reentrant, meaning a thread that already holds the lock can acquire it
|
||||
again without blocking. In contrast, :class:`threading.Lock` is not
|
||||
reentrant - attempting to acquire it twice from the same thread will cause
|
||||
a deadlock.
|
||||
|
||||
See also :term:`lock` and :term:`deadlock`.
|
||||
|
||||
REPL
|
||||
An acronym for the "read–eval–print loop", another name for the
|
||||
:term:`interactive` interpreter shell.
|
||||
|
|
@ -1340,6 +1471,18 @@ Glossary
|
|||
|
||||
See also :term:`borrowed reference`.
|
||||
|
||||
synchronization primitive
|
||||
A basic building block for coordinating (synchronizing) the execution of
|
||||
multiple threads to ensure :term:`thread-safe` access to shared resources.
|
||||
Python's :mod:`threading` module provides several synchronization primitives
|
||||
including :class:`~threading.Lock`, :class:`~threading.RLock`,
|
||||
:class:`~threading.Semaphore`, :class:`~threading.Condition`,
|
||||
:class:`~threading.Event`, and :class:`~threading.Barrier`. Additionally,
|
||||
the :mod:`queue` module provides multi-producer, multi-consumer queues
|
||||
that are especially useful in multithreaded programs. These
|
||||
primitives help prevent :term:`race conditions <race condition>` and
|
||||
coordinate thread execution. See also :term:`lock`.
|
||||
|
||||
t-string
|
||||
t-strings
|
||||
String literals prefixed with ``t`` or ``T`` are commonly called
|
||||
|
|
@ -1392,6 +1535,19 @@ Glossary
|
|||
See :ref:`Thread State and the Global Interpreter Lock <threads>` for more
|
||||
information.
|
||||
|
||||
thread-safe
|
||||
A module, function, or class that behaves correctly when used by multiple
|
||||
threads concurrently. Thread-safe code uses appropriate
|
||||
:term:`synchronization primitives <synchronization primitive>` like
|
||||
:term:`locks <lock>` to protect shared mutable state, or is designed
|
||||
to avoid shared mutable state entirely. In the
|
||||
:term:`free-threaded <free threading>` build, built-in types like
|
||||
:class:`dict`, :class:`list`, and :class:`set` use internal locking
|
||||
to make many operations thread-safe, although thread safety is not
|
||||
necessarily guaranteed. Code that is not thread-safe may experience
|
||||
:term:`race conditions <race condition>` and :term:`data races <data race>`
|
||||
when used in multi-threaded programs.
|
||||
|
||||
token
|
||||
|
||||
A small unit of source code, generated by the
|
||||
|
|
|
|||
55
Doc/includes/capi-extension/spammodule-01.c
Normal file
55
Doc/includes/capi-extension/spammodule-01.c
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/* This file needs to be kept in sync with the tutorial
|
||||
* at Doc/extending/first-extension-module.rst
|
||||
*/
|
||||
|
||||
/// Includes
|
||||
|
||||
#include <Python.h>
|
||||
#include <stdlib.h> // for system()
|
||||
|
||||
/// Implementation of spam.system
|
||||
|
||||
static PyObject *
|
||||
spam_system(PyObject *self, PyObject *arg)
|
||||
{
|
||||
const char *command = PyUnicode_AsUTF8(arg);
|
||||
if (command == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
int status = system(command);
|
||||
PyObject *result = PyLong_FromLong(status);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Module method table
|
||||
|
||||
static PyMethodDef spam_methods[] = {
|
||||
{
|
||||
.ml_name="system",
|
||||
.ml_meth=spam_system,
|
||||
.ml_flags=METH_O,
|
||||
.ml_doc="Execute a shell command.",
|
||||
},
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
/// Module slot table
|
||||
|
||||
static PyModuleDef_Slot spam_slots[] = {
|
||||
{Py_mod_name, "spam"},
|
||||
{Py_mod_doc, "A wonderful module with an example function"},
|
||||
{Py_mod_methods, spam_methods},
|
||||
{0, NULL}
|
||||
};
|
||||
|
||||
/// Export hook prototype
|
||||
|
||||
PyMODEXPORT_FUNC PyModExport_spam(void);
|
||||
|
||||
/// Module export hook
|
||||
|
||||
PyMODEXPORT_FUNC
|
||||
PyModExport_spam(void)
|
||||
{
|
||||
return spam_slots;
|
||||
}
|
||||
|
|
@ -139,12 +139,13 @@ Node classes
|
|||
The :meth:`~object.__repr__` output of :class:`~ast.AST` nodes includes
|
||||
the values of the node fields.
|
||||
|
||||
.. deprecated:: 3.8
|
||||
.. deprecated-removed:: 3.8 3.14
|
||||
|
||||
Old classes :class:`!ast.Num`, :class:`!ast.Str`, :class:`!ast.Bytes`,
|
||||
:class:`!ast.NameConstant` and :class:`!ast.Ellipsis` are still available,
|
||||
but they will be removed in future Python releases. In the meantime,
|
||||
instantiating them will return an instance of a different class.
|
||||
Previous versions of Python provided the AST classes :class:`!ast.Num`,
|
||||
:class:`!ast.Str`, :class:`!ast.Bytes`, :class:`!ast.NameConstant` and
|
||||
:class:`!ast.Ellipsis`, which were deprecated in Python 3.8. These classes
|
||||
were removed in Python 3.14, and their functionality has been replaced with
|
||||
:class:`ast.Constant`.
|
||||
|
||||
.. deprecated:: 3.9
|
||||
|
||||
|
|
@ -2419,12 +2420,12 @@ and classes for traversing abstract syntax trees:
|
|||
during traversal. For this a special visitor exists
|
||||
(:class:`NodeTransformer`) that allows modifications.
|
||||
|
||||
.. deprecated:: 3.8
|
||||
.. deprecated-removed:: 3.8 3.14
|
||||
|
||||
Methods :meth:`!visit_Num`, :meth:`!visit_Str`, :meth:`!visit_Bytes`,
|
||||
:meth:`!visit_NameConstant` and :meth:`!visit_Ellipsis` are deprecated
|
||||
now and will not be called in future Python versions. Add the
|
||||
:meth:`visit_Constant` method to handle all constant nodes.
|
||||
:meth:`!visit_NameConstant` and :meth:`!visit_Ellipsis` will not be called
|
||||
in Python 3.14+. Add the :meth:`visit_Constant` method instead to handle
|
||||
all constant nodes.
|
||||
|
||||
|
||||
.. class:: NodeTransformer()
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ Queue
|
|||
The queue can no longer grow.
|
||||
Future calls to :meth:`~Queue.put` raise :exc:`QueueShutDown`.
|
||||
Currently blocked callers of :meth:`~Queue.put` will be unblocked
|
||||
and will raise :exc:`QueueShutDown` in the formerly blocked thread.
|
||||
and will raise :exc:`QueueShutDown` in the formerly awaiting task.
|
||||
|
||||
If *immediate* is false (the default), the queue can be wound
|
||||
down normally with :meth:`~Queue.get` calls to extract tasks
|
||||
|
|
|
|||
|
|
@ -2651,9 +2651,42 @@ Broadly speaking, ``d.strftime(fmt)`` acts like the :mod:`time` module's
|
|||
``time.strftime(fmt, d.timetuple())`` although not all objects support a
|
||||
:meth:`~date.timetuple` method.
|
||||
|
||||
For the :meth:`.datetime.strptime` class method, the default value is
|
||||
``1900-01-01T00:00:00.000``: any components not specified in the format string
|
||||
will be pulled from the default value. [#]_
|
||||
For the :meth:`.datetime.strptime` and :meth:`.date.strptime` class methods,
|
||||
the default value is ``1900-01-01T00:00:00.000``: any components not specified
|
||||
in the format string will be pulled from the default value.
|
||||
|
||||
.. note::
|
||||
When used to parse partial dates lacking a year, :meth:`.datetime.strptime`
|
||||
and :meth:`.date.strptime` will raise when encountering February 29 because
|
||||
the default year of 1900 is *not* a leap year. Always add a default leap
|
||||
year to partial date strings before parsing.
|
||||
|
||||
|
||||
.. testsetup::
|
||||
|
||||
# doctest seems to turn the warning into an error which makes it
|
||||
# show up and require matching and prevents the actual interesting
|
||||
# exception from being raised.
|
||||
# Manually apply the catch_warnings context manager
|
||||
import warnings
|
||||
catch_warnings = warnings.catch_warnings()
|
||||
catch_warnings.__enter__()
|
||||
warnings.simplefilter("ignore")
|
||||
|
||||
.. testcleanup::
|
||||
|
||||
catch_warnings.__exit__()
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> value = "2/29"
|
||||
>>> datetime.strptime(value, "%m/%d")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: day 29 must be in range 1..28 for month 2 in year 1900
|
||||
>>> datetime.strptime(f"1904 {value}", "%Y %m/%d")
|
||||
datetime.datetime(1904, 2, 29, 0, 0)
|
||||
|
||||
Using ``datetime.strptime(date_string, format)`` is equivalent to::
|
||||
|
||||
|
|
@ -2790,7 +2823,7 @@ Notes:
|
|||
include a year in the format. If the value you need to parse lacks a year,
|
||||
append an explicit dummy leap year. Otherwise your code will raise an
|
||||
exception when it encounters leap day because the default year used by the
|
||||
parser is not a leap year. Users run into this bug every four years...
|
||||
parser (1900) is not a leap year. Users run into that bug every leap year.
|
||||
|
||||
.. doctest::
|
||||
|
||||
|
|
@ -2817,5 +2850,3 @@ Notes:
|
|||
.. [#] See R. H. van Gent's `guide to the mathematics of the ISO 8601 calendar
|
||||
<https://web.archive.org/web/20220531051136/https://webspace.science.uu.nl/~gent0113/calendar/isocalendar.htm>`_
|
||||
for a good explanation.
|
||||
|
||||
.. [#] Passing ``datetime.strptime('Feb 29', '%b %d')`` will fail since 1900 is not a leap year.
|
||||
|
|
|
|||
|
|
@ -947,12 +947,13 @@ Utilities and Decorators
|
|||
the member's name. Care must be taken if mixing *auto()* with manually
|
||||
specified values.
|
||||
|
||||
*auto* instances are only resolved when at the top level of an assignment:
|
||||
*auto* instances are only resolved when at the top level of an assignment, either by
|
||||
itself or as part of a tuple:
|
||||
|
||||
* ``FIRST = auto()`` will work (auto() is replaced with ``1``);
|
||||
* ``SECOND = auto(), -2`` will work (auto is replaced with ``2``, so ``2, -2`` is
|
||||
used to create the ``SECOND`` enum member;
|
||||
* ``THREE = [auto(), -3]`` will *not* work (``<auto instance>, -3`` is used to
|
||||
* ``THREE = [auto(), -3]`` will *not* work (``[<auto instance>, -3]`` is used to
|
||||
create the ``THREE`` enum member)
|
||||
|
||||
.. versionchanged:: 3.11.1
|
||||
|
|
|
|||
|
|
@ -328,6 +328,17 @@ To map anonymous memory, -1 should be passed as the fileno along with the length
|
|||
|
||||
.. versionadded:: 3.13
|
||||
|
||||
.. method:: set_name(name, /)
|
||||
|
||||
Annotate the memory mapping with the given *name* for easier identification
|
||||
in ``/proc/<pid>/maps`` if the kernel supports the feature and :option:`-X dev <-X>` is passed
|
||||
to Python or if Python is built in :ref:`debug mode <debug-build>`.
|
||||
The length of *name* must not exceed 67 bytes including the ``'\0'`` terminator.
|
||||
|
||||
.. availability:: Linux >= 5.17 (kernel built with ``CONFIG_ANON_VMA_NAME`` option)
|
||||
|
||||
.. versionadded:: next
|
||||
|
||||
.. method:: size()
|
||||
|
||||
Return the length of the file, which can be larger than the size of the
|
||||
|
|
|
|||
|
|
@ -520,7 +520,8 @@ can be overridden by the local file.
|
|||
To remove all commands from a breakpoint, type ``commands`` and follow it
|
||||
immediately with ``end``; that is, give no commands.
|
||||
|
||||
With no *bpnumber* argument, ``commands`` refers to the last breakpoint set.
|
||||
With no *bpnumber* argument, ``commands`` refers to the most recently set
|
||||
breakpoint that still exists.
|
||||
|
||||
You can use breakpoint commands to start your program up again. Simply use
|
||||
the :pdbcmd:`continue` command, or :pdbcmd:`step`,
|
||||
|
|
|
|||
|
|
@ -200,6 +200,36 @@ On most systems, attaching to another process requires appropriate permissions.
|
|||
See :ref:`profiling-permissions` for platform-specific requirements.
|
||||
|
||||
|
||||
.. _replay-command:
|
||||
|
||||
The ``replay`` command
|
||||
----------------------
|
||||
|
||||
The ``replay`` command converts binary profile files to other output formats::
|
||||
|
||||
python -m profiling.sampling replay profile.bin
|
||||
python -m profiling.sampling replay --flamegraph -o profile.html profile.bin
|
||||
|
||||
This command is useful when you have captured profiling data in binary format
|
||||
and want to analyze it later or convert it to a visualization format. Binary
|
||||
profiles can be replayed multiple times to different formats without
|
||||
re-profiling.
|
||||
|
||||
::
|
||||
|
||||
# Convert binary to pstats (default, prints to stdout)
|
||||
python -m profiling.sampling replay profile.bin
|
||||
|
||||
# Convert binary to flame graph
|
||||
python -m profiling.sampling replay --flamegraph -o output.html profile.bin
|
||||
|
||||
# Convert binary to gecko format for Firefox Profiler
|
||||
python -m profiling.sampling replay --gecko -o profile.json profile.bin
|
||||
|
||||
# Convert binary to heatmap
|
||||
python -m profiling.sampling replay --heatmap -o my_heatmap profile.bin
|
||||
|
||||
|
||||
Profiling in production
|
||||
-----------------------
|
||||
|
||||
|
|
@ -1041,6 +1071,59 @@ intuitive view that shows exactly where time is spent without requiring
|
|||
interpretation of hierarchical visualizations.
|
||||
|
||||
|
||||
Binary format
|
||||
-------------
|
||||
|
||||
Binary format (:option:`--binary`) produces a compact binary file for efficient
|
||||
storage of profiling data::
|
||||
|
||||
python -m profiling.sampling run --binary -o profile.bin script.py
|
||||
python -m profiling.sampling attach --binary -o profile.bin 12345
|
||||
|
||||
The :option:`--compression` option controls data compression:
|
||||
|
||||
- ``auto`` (default): Use zstd compression if available, otherwise no
|
||||
compression
|
||||
- ``zstd``: Force zstd compression (requires :mod:`compression.zstd` support)
|
||||
- ``none``: Disable compression
|
||||
|
||||
::
|
||||
|
||||
python -m profiling.sampling run --binary --compression=zstd -o profile.bin script.py
|
||||
|
||||
To analyze binary profiles, use the :ref:`replay-command` to convert them to
|
||||
other formats like flame graphs or pstats output.
|
||||
|
||||
|
||||
Record and replay workflow
|
||||
==========================
|
||||
|
||||
The binary format combined with the replay command enables a record-and-replay
|
||||
workflow that separates data capture from analysis. Rather than generating
|
||||
visualizations during profiling, you capture raw data to a compact binary file
|
||||
and convert it to different formats later.
|
||||
|
||||
This approach has three main benefits:
|
||||
|
||||
- Sampling runs faster because the work of building data structures for
|
||||
visualization is deferred until replay.
|
||||
- A single binary capture can be converted to multiple output formats
|
||||
without re-profiling: pstats for a quick overview, flame graph for visual
|
||||
exploration, heatmap for line-level detail.
|
||||
- Binary files are compact and easy to share with colleagues who can convert
|
||||
them to their preferred format.
|
||||
|
||||
A typical workflow::
|
||||
|
||||
# Capture profile in production or during tests
|
||||
python -m profiling.sampling attach --binary -o profile.bin 12345
|
||||
|
||||
# Later, analyze with different formats
|
||||
python -m profiling.sampling replay profile.bin
|
||||
python -m profiling.sampling replay --flamegraph -o profile.html profile.bin
|
||||
python -m profiling.sampling replay --heatmap -o heatmap profile.bin
|
||||
|
||||
|
||||
Live mode
|
||||
=========
|
||||
|
||||
|
|
@ -1252,6 +1335,10 @@ Global options
|
|||
|
||||
Attach to and profile a running process by PID.
|
||||
|
||||
.. option:: replay
|
||||
|
||||
Convert a binary profile file to another output format.
|
||||
|
||||
|
||||
Sampling options
|
||||
----------------
|
||||
|
|
@ -1335,12 +1422,22 @@ Output options
|
|||
|
||||
Generate HTML heatmap with line-level sample counts.
|
||||
|
||||
.. option:: --binary
|
||||
|
||||
Generate high-performance binary format for later conversion with the
|
||||
``replay`` command.
|
||||
|
||||
.. option:: --compression <type>
|
||||
|
||||
Compression for binary format: ``auto`` (use zstd if available, default),
|
||||
``zstd``, or ``none``.
|
||||
|
||||
.. option:: -o <path>, --output <path>
|
||||
|
||||
Output file or directory path. Default behavior varies by format:
|
||||
``--pstats`` writes to stdout, ``--flamegraph`` and ``--gecko`` generate
|
||||
files like ``flamegraph.PID.html``, and ``--heatmap`` creates a directory
|
||||
named ``heatmap_PID``.
|
||||
:option:`--pstats` writes to stdout, while other formats generate a file
|
||||
named ``<format>_<PID>.<ext>`` (for example, ``flamegraph_12345.html``).
|
||||
:option:`--heatmap` creates a directory named ``heatmap_<PID>``.
|
||||
|
||||
|
||||
pstats display options
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ Bookkeeping functions
|
|||
instead of the system time (see the :func:`os.urandom` function for details
|
||||
on availability).
|
||||
|
||||
If *a* is an int, it is used directly.
|
||||
If *a* is an int, its absolute value is used directly.
|
||||
|
||||
With version 2 (the default), a :class:`str`, :class:`bytes`, or :class:`bytearray`
|
||||
object gets converted to an :class:`int` and all of its bits are used.
|
||||
|
|
|
|||
|
|
@ -46,8 +46,10 @@ Any object can be tested for truth value, for use in an :keyword:`if` or
|
|||
By default, an object is considered true unless its class defines either a
|
||||
:meth:`~object.__bool__` method that returns ``False`` or a
|
||||
:meth:`~object.__len__` method that
|
||||
returns zero, when called with the object. [1]_ Here are most of the built-in
|
||||
objects considered false:
|
||||
returns zero, when called with the object. [1]_ If one of the methods raises an
|
||||
exception when called, the exception is propagated and the object does
|
||||
not have a truth value (for example, :data:`NotImplemented`).
|
||||
Here are most of the built-in objects considered false:
|
||||
|
||||
.. index::
|
||||
single: None (Built-in object)
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ class PydocTopicsBuilder(TextBuilder):
|
|||
def init(self) -> None:
|
||||
super().init()
|
||||
self.topics: dict[str, str] = {}
|
||||
self.module_docs: dict[str, str] = {}
|
||||
|
||||
def get_outdated_docs(self) -> str:
|
||||
# Return a string describing what an update build will build.
|
||||
|
|
@ -130,6 +131,15 @@ class PydocTopicsBuilder(TextBuilder):
|
|||
continue
|
||||
doc_labels.setdefault(docname, []).append((topic_label, label_id))
|
||||
|
||||
py_domain = env.domains['py']
|
||||
for module_name, module_info in py_domain.data['modules'].items():
|
||||
docname = module_info[0]
|
||||
if docname.startswith('library/'):
|
||||
doc_file = docname.replace('library/', '')
|
||||
self.module_docs[module_name] = (
|
||||
f"{doc_file}#module-{module_name}"
|
||||
)
|
||||
|
||||
for docname, label_ids in status_iterator(
|
||||
doc_labels.items(),
|
||||
"building topics... ",
|
||||
|
|
@ -161,6 +171,22 @@ topics = {{
|
|||
"""
|
||||
self.outdir.joinpath("topics.py").write_text(topics, encoding="utf-8")
|
||||
|
||||
module_docs_repr = "\n".join(
|
||||
f" '{module}': '{doc_file}',"
|
||||
for module, doc_file in sorted(self.module_docs.items())
|
||||
)
|
||||
module_docs = f"""\
|
||||
# Autogenerated by Sphinx on {asctime()}
|
||||
# as part of the release process.
|
||||
|
||||
module_docs = {{
|
||||
{module_docs_repr}
|
||||
}}
|
||||
"""
|
||||
self.outdir.joinpath("module_docs.py").write_text(
|
||||
module_docs, encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def _display_labels(item: tuple[str, Sequence[tuple[str, str]]]) -> str:
|
||||
_docname, label_ids = item
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ Summary -- Release highlights
|
|||
<whatsnew315-utf8-default>`
|
||||
* :pep:`782`: :ref:`A new PyBytesWriter C API to create a Python bytes object
|
||||
<whatsnew315-pep782>`
|
||||
* :ref:`The JIT compiler has been significantly upgraded <whatsnew315-jit>`
|
||||
* :ref:`Improved error messages <whatsnew315-improved-error-messages>`
|
||||
|
||||
|
||||
|
|
@ -591,6 +592,11 @@ mmap
|
|||
not be duplicated.
|
||||
(Contributed by Serhiy Storchaka in :gh:`78502`.)
|
||||
|
||||
* Added the :meth:`mmap.mmap.set_name` method
|
||||
to annotate an anonymous memory mapping
|
||||
if Linux kernel supports :manpage:`PR_SET_VMA_ANON_NAME <PR_SET_VMA(2const)>` (Linux 5.17 or newer).
|
||||
(Contributed by Donghee Na in :gh:`142419`.)
|
||||
|
||||
|
||||
os
|
||||
--
|
||||
|
|
@ -843,6 +849,16 @@ zlib
|
|||
Optimizations
|
||||
=============
|
||||
|
||||
* Builds using Visual Studio 2026 (MSVC 18) may now use the new
|
||||
:ref:`tail-calling interpreter <whatsnew314-tail-call-interpreter>`.
|
||||
Results on an early experimental MSVC compiler reported roughly 15% speedup
|
||||
on the geometric mean of pyperformance on Windows x86-64 over
|
||||
the switch-case interpreter. We have
|
||||
observed speedups ranging from 15% for large pure-Python libraries
|
||||
to 40% for long-running small pure-Python scripts on Windows.
|
||||
(Contributed by Chris Eibl, Ken Jin, and Brandt Bucher in :gh:`143068`.
|
||||
Special thanks to the MSVC team including Hulon Jenkins.)
|
||||
|
||||
csv
|
||||
---
|
||||
|
||||
|
|
@ -850,6 +866,91 @@ csv
|
|||
(Contributed by Maurycy Pawłowski-Wieroński in :gh:`137628`.)
|
||||
|
||||
|
||||
.. _whatsnew315-jit:
|
||||
|
||||
Upgraded JIT compiler
|
||||
=====================
|
||||
|
||||
Results from the `pyperformance <https://github.com/python/pyperformance>`__
|
||||
benchmark suite report
|
||||
`3-4% <https://github.com/facebookexperimental/free-threading-benchmarking/blob/main/results/bm-20251214-3.15.0a2%2B-6cddf04-JIT/bm-20251214-vultr-x86_64-python-6cddf04344a1e8ca9df5-3.15.0a2%2B-6cddf04-vs-base.svg>`__
|
||||
geometric mean performance improvement for the JIT over the standard CPython
|
||||
interpreter built with all optimizations enabled. The speedups for JIT
|
||||
builds versus no JIT builds range from roughly 20% slowdown to over
|
||||
100% speedup (ignoring the ``unpack_sequence`` microbenchmark) on
|
||||
x86-64 Linux and AArch64 macOS systems.
|
||||
|
||||
.. attention::
|
||||
These results are not yet final.
|
||||
|
||||
The major upgrades to the JIT are:
|
||||
|
||||
* LLVM 21 build-time dependency
|
||||
* New tracing frontend
|
||||
* Basic register allocation in the JIT
|
||||
* More JIT optimizations
|
||||
* Better machine code generation
|
||||
|
||||
.. rubric:: LLVM 21 build-time dependency
|
||||
|
||||
The JIT compiler now uses LLVM 21 for build-time stencil generation. As
|
||||
always, LLVM is only needed when building CPython with the JIT enabled;
|
||||
end users running Python do not need LLVM installed. Instructions for
|
||||
installing LLVM can be found in the `JIT compiler documentation
|
||||
<https://github.com/python/cpython/blob/main/Tools/jit/README.md>`__
|
||||
for all supported platforms.
|
||||
|
||||
(Contributed by Savannah Ostrowski in :gh:`140973`.)
|
||||
|
||||
.. rubric:: A new tracing frontend
|
||||
|
||||
The JIT compiler now supports significantly more bytecode operations and
|
||||
control flow than in Python 3.14, enabling speedups on a wider variety of
|
||||
code. For example, simple Python object creation is now understood by the
|
||||
3.15 JIT compiler. Overloaded operations and generators are also partially
|
||||
supported. This was made possible by an overhauled JIT tracing frontend
|
||||
that records actual execution paths through code, rather than estimating
|
||||
them as the previous implementation did.
|
||||
|
||||
(Contributed by Ken Jin in :gh:`139109`. Support for Windows added by
|
||||
Mark Shannon in :gh:`141703`.)
|
||||
|
||||
.. rubric:: Basic register allocation in the JIT
|
||||
|
||||
A basic form of register allocation has been added to the JIT compiler's
|
||||
optimizer. This allows the JIT compiler to avoid certain stack operations
|
||||
altogether and instead operate on registers. This allows the JIT to produce
|
||||
more efficient traces by avoiding reads and writes to memory.
|
||||
|
||||
(Contributed by Mark Shannon in :gh:`135379`.)
|
||||
|
||||
.. rubric:: More JIT optimizations
|
||||
|
||||
More `constant-propagation <https://en.wikipedia.org/wiki/Constant_folding>`__
|
||||
is now performed. This means when the JIT compiler detects that certain user
|
||||
code results in constants, the code can be simplified by the JIT.
|
||||
|
||||
(Contributed by Ken Jin and Savannah Ostrowski in :gh:`132732`.)
|
||||
|
||||
The JIT avoids :term:`reference count`\ s where possible. This generally
|
||||
reduces the cost of most operations in Python.
|
||||
|
||||
(Contributed by Ken Jin, Donghee Na, Zheao Li, Savannah Ostrowski,
|
||||
Noam Cohen, Tomas Roun, PuQing in :gh:`134584`.)
|
||||
|
||||
.. rubric:: Better machine code generation
|
||||
|
||||
The JIT compiler's machine code generator now produces better machine code
|
||||
for x86-64 and AArch64 macOS and Linux targets. In general, users should
|
||||
experience lower memory usage for generated machine code and more efficient
|
||||
machine code versus the old JIT.
|
||||
|
||||
(Contributed by Brandt Bucher in :gh:`136528` and :gh:`136528`.
|
||||
Implementation for AArch64 contributed by Mark Shannon in :gh:`139855`.
|
||||
Additional optimizations for AArch64 contributed by Mark Shannon and
|
||||
Diego Russo in :gh:`140683` and :gh:`142305`.)
|
||||
|
||||
|
||||
Removed
|
||||
=======
|
||||
|
||||
|
|
@ -1018,9 +1119,9 @@ New deprecations
|
|||
|
||||
* ``__version__``
|
||||
|
||||
* The ``__version__`` attribute has been deprecated in these standard library
|
||||
modules and will be removed in Python 3.20.
|
||||
Use :py:data:`sys.version_info` instead.
|
||||
* The ``__version__``, ``version`` and ``VERSION`` attributes have been
|
||||
deprecated in these standard library modules and will be removed in
|
||||
Python 3.20. Use :py:data:`sys.version_info` instead.
|
||||
|
||||
- :mod:`argparse`
|
||||
- :mod:`csv`
|
||||
|
|
@ -1041,6 +1142,9 @@ New deprecations
|
|||
- :mod:`tkinter.font`
|
||||
- :mod:`tkinter.ttk`
|
||||
- :mod:`wsgiref.simple_server`
|
||||
- :mod:`xml.etree.ElementTree`
|
||||
- :mod:`!xml.sax.expatreader`
|
||||
- :mod:`xml.sax.handler`
|
||||
- :mod:`zlib`
|
||||
|
||||
(Contributed by Hugo van Kemenade and Stan Ulbrych in :gh:`76007`.)
|
||||
|
|
|
|||
|
|
@ -523,6 +523,9 @@ _Py_atomic_store_uintptr_release(uintptr_t *obj, uintptr_t value);
|
|||
static inline void
|
||||
_Py_atomic_store_ssize_release(Py_ssize_t *obj, Py_ssize_t value);
|
||||
|
||||
static inline void
|
||||
_Py_atomic_store_int8_release(int8_t *obj, int8_t value);
|
||||
|
||||
static inline void
|
||||
_Py_atomic_store_int_release(int *obj, int value);
|
||||
|
||||
|
|
|
|||
|
|
@ -572,6 +572,10 @@ static inline void
|
|||
_Py_atomic_store_int_release(int *obj, int value)
|
||||
{ __atomic_store_n(obj, value, __ATOMIC_RELEASE); }
|
||||
|
||||
static inline void
|
||||
_Py_atomic_store_int8_release(int8_t *obj, int8_t value)
|
||||
{ __atomic_store_n(obj, value, __ATOMIC_RELEASE); }
|
||||
|
||||
static inline void
|
||||
_Py_atomic_store_ssize_release(Py_ssize_t *obj, Py_ssize_t value)
|
||||
{ __atomic_store_n(obj, value, __ATOMIC_RELEASE); }
|
||||
|
|
|
|||
|
|
@ -1066,6 +1066,19 @@ _Py_atomic_store_int_release(int *obj, int value)
|
|||
#endif
|
||||
}
|
||||
|
||||
static inline void
|
||||
_Py_atomic_store_int8_release(int8_t *obj, int8_t value)
|
||||
{
|
||||
#if defined(_M_X64) || defined(_M_IX86)
|
||||
*(int8_t volatile *)obj = value;
|
||||
#elif defined(_M_ARM64)
|
||||
_Py_atomic_ASSERT_ARG_TYPE(unsigned __int8);
|
||||
__stlr8((unsigned __int8 volatile *)obj, (unsigned __int8)value);
|
||||
#else
|
||||
# error "no implementation of _Py_atomic_store_int8_release"
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void
|
||||
_Py_atomic_store_ssize_release(Py_ssize_t *obj, Py_ssize_t value)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1023,6 +1023,14 @@ _Py_atomic_store_int_release(int *obj, int value)
|
|||
memory_order_release);
|
||||
}
|
||||
|
||||
static inline void
|
||||
_Py_atomic_store_int8_release(int8_t *obj, int8_t value)
|
||||
{
|
||||
_Py_USING_STD;
|
||||
atomic_store_explicit((_Atomic(int8_t)*)obj, value,
|
||||
memory_order_release);
|
||||
}
|
||||
|
||||
static inline void
|
||||
_Py_atomic_store_uint_release(unsigned int *obj, unsigned int value)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ _PyEval_EvalFrame(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwfl
|
|||
|
||||
#ifdef _Py_TIER2
|
||||
#ifdef _Py_JIT
|
||||
_Py_CODEUNIT *_Py_LazyJitTrampoline(
|
||||
_Py_CODEUNIT *_Py_LazyJitShim(
|
||||
struct _PyExecutorObject *current_executor, _PyInterpreterFrame *frame,
|
||||
_PyStackRef *stack_pointer, PyThreadState *tstate
|
||||
);
|
||||
|
|
@ -415,6 +415,17 @@ _Py_VectorCall_StackRefSteal(
|
|||
int total_args,
|
||||
_PyStackRef kwnames);
|
||||
|
||||
PyAPI_FUNC(PyObject*)
|
||||
_Py_VectorCallInstrumentation_StackRefSteal(
|
||||
_PyStackRef callable,
|
||||
_PyStackRef* arguments,
|
||||
int total_args,
|
||||
_PyStackRef kwnames,
|
||||
bool call_instrumentation,
|
||||
_PyInterpreterFrame* frame,
|
||||
_Py_CODEUNIT* this_instr,
|
||||
PyThreadState* tstate);
|
||||
|
||||
PyAPI_FUNC(PyObject *)
|
||||
_Py_BuiltinCallFast_StackRefSteal(
|
||||
_PyStackRef callable,
|
||||
|
|
@ -464,6 +475,11 @@ _Py_assert_within_stack_bounds(
|
|||
_PyInterpreterFrame *frame, _PyStackRef *stack_pointer,
|
||||
const char *filename, int lineno);
|
||||
|
||||
// Like PyMapping_GetOptionalItem, but returns the PyObject* instead of taking
|
||||
// it as an out parameter. This helps MSVC's escape analysis when used with
|
||||
// tail calling.
|
||||
PyAPI_FUNC(PyObject*) _PyMapping_GetOptionalItem2(PyObject* obj, PyObject* key, int* err);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -272,8 +272,7 @@ _PyDict_SendEvent(int watcher_bits,
|
|||
PyObject *value);
|
||||
|
||||
static inline void
|
||||
_PyDict_NotifyEvent(PyInterpreterState *interp,
|
||||
PyDict_WatchEvent event,
|
||||
_PyDict_NotifyEvent(PyDict_WatchEvent event,
|
||||
PyDictObject *mp,
|
||||
PyObject *key,
|
||||
PyObject *value)
|
||||
|
|
|
|||
|
|
@ -1653,9 +1653,11 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
|
|||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(co_varnames));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(code));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(col_offset));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(collector));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(command));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(comment_factory));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(compile_mode));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(compression));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(config));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(consts));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(context));
|
||||
|
|
@ -1718,7 +1720,9 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
|
|||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(event));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(eventmask));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exc));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exc_tb));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exc_type));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exc_val));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exc_value));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(excepthook));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exception));
|
||||
|
|
@ -1974,6 +1978,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
|
|||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(print_file_and_line));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(priority));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(progress));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(progress_callback));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(progress_routine));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(proto));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(protocol));
|
||||
|
|
@ -2014,6 +2019,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
|
|||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(reversed));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(rounding));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(salt));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sample_interval_us));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sched_priority));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(scheduler));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(script));
|
||||
|
|
@ -2053,8 +2059,10 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
|
|||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(spam));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(src));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(src_dir_fd));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stack_frames));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stacklevel));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(start));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(start_time_us));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(statement));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stats));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(status));
|
||||
|
|
@ -2095,6 +2103,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
|
|||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(times));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(timespec));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(timestamp));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(timestamp_us));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(timetuple));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(timeunit));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(top));
|
||||
|
|
|
|||
|
|
@ -376,9 +376,11 @@ struct _Py_global_strings {
|
|||
STRUCT_FOR_ID(co_varnames)
|
||||
STRUCT_FOR_ID(code)
|
||||
STRUCT_FOR_ID(col_offset)
|
||||
STRUCT_FOR_ID(collector)
|
||||
STRUCT_FOR_ID(command)
|
||||
STRUCT_FOR_ID(comment_factory)
|
||||
STRUCT_FOR_ID(compile_mode)
|
||||
STRUCT_FOR_ID(compression)
|
||||
STRUCT_FOR_ID(config)
|
||||
STRUCT_FOR_ID(consts)
|
||||
STRUCT_FOR_ID(context)
|
||||
|
|
@ -441,7 +443,9 @@ struct _Py_global_strings {
|
|||
STRUCT_FOR_ID(event)
|
||||
STRUCT_FOR_ID(eventmask)
|
||||
STRUCT_FOR_ID(exc)
|
||||
STRUCT_FOR_ID(exc_tb)
|
||||
STRUCT_FOR_ID(exc_type)
|
||||
STRUCT_FOR_ID(exc_val)
|
||||
STRUCT_FOR_ID(exc_value)
|
||||
STRUCT_FOR_ID(excepthook)
|
||||
STRUCT_FOR_ID(exception)
|
||||
|
|
@ -697,6 +701,7 @@ struct _Py_global_strings {
|
|||
STRUCT_FOR_ID(print_file_and_line)
|
||||
STRUCT_FOR_ID(priority)
|
||||
STRUCT_FOR_ID(progress)
|
||||
STRUCT_FOR_ID(progress_callback)
|
||||
STRUCT_FOR_ID(progress_routine)
|
||||
STRUCT_FOR_ID(proto)
|
||||
STRUCT_FOR_ID(protocol)
|
||||
|
|
@ -737,6 +742,7 @@ struct _Py_global_strings {
|
|||
STRUCT_FOR_ID(reversed)
|
||||
STRUCT_FOR_ID(rounding)
|
||||
STRUCT_FOR_ID(salt)
|
||||
STRUCT_FOR_ID(sample_interval_us)
|
||||
STRUCT_FOR_ID(sched_priority)
|
||||
STRUCT_FOR_ID(scheduler)
|
||||
STRUCT_FOR_ID(script)
|
||||
|
|
@ -776,8 +782,10 @@ struct _Py_global_strings {
|
|||
STRUCT_FOR_ID(spam)
|
||||
STRUCT_FOR_ID(src)
|
||||
STRUCT_FOR_ID(src_dir_fd)
|
||||
STRUCT_FOR_ID(stack_frames)
|
||||
STRUCT_FOR_ID(stacklevel)
|
||||
STRUCT_FOR_ID(start)
|
||||
STRUCT_FOR_ID(start_time_us)
|
||||
STRUCT_FOR_ID(statement)
|
||||
STRUCT_FOR_ID(stats)
|
||||
STRUCT_FOR_ID(status)
|
||||
|
|
@ -818,6 +826,7 @@ struct _Py_global_strings {
|
|||
STRUCT_FOR_ID(times)
|
||||
STRUCT_FOR_ID(timespec)
|
||||
STRUCT_FOR_ID(timestamp)
|
||||
STRUCT_FOR_ID(timestamp_us)
|
||||
STRUCT_FOR_ID(timetuple)
|
||||
STRUCT_FOR_ID(timeunit)
|
||||
STRUCT_FOR_ID(top)
|
||||
|
|
|
|||
|
|
@ -947,7 +947,6 @@ struct _is {
|
|||
struct _PyExecutorObject *executor_deletion_list_head;
|
||||
struct _PyExecutorObject *cold_executor;
|
||||
struct _PyExecutorObject *cold_dynamic_executor;
|
||||
int executor_deletion_list_remaining_capacity;
|
||||
size_t executor_creation_counter;
|
||||
_rare_events rare_events;
|
||||
PyDict_WatchCallback builtins_dict_watcher;
|
||||
|
|
|
|||
|
|
@ -17,25 +17,27 @@ extern "C" {
|
|||
#endif
|
||||
|
||||
#if defined(HAVE_PR_SET_VMA_ANON_NAME) && defined(__linux__)
|
||||
static inline void
|
||||
static inline int
|
||||
_PyAnnotateMemoryMap(void *addr, size_t size, const char *name)
|
||||
{
|
||||
#ifndef Py_DEBUG
|
||||
if (!_Py_GetConfig()->dev_mode) {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
// The name length cannot exceed 80 (including the '\0').
|
||||
assert(strlen(name) < 80);
|
||||
int old_errno = errno;
|
||||
prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, (unsigned long)addr, size, name);
|
||||
/* Ignore errno from prctl */
|
||||
/* See: https://bugzilla.redhat.com/show_bug.cgi?id=2302746 */
|
||||
errno = old_errno;
|
||||
int res = prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, (unsigned long)addr, size, name);
|
||||
if (res < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
static inline void
|
||||
static inline int
|
||||
_PyAnnotateMemoryMap(void *Py_UNUSED(addr), size_t Py_UNUSED(size), const char *Py_UNUSED(name))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
|||
14
Include/internal/pycore_opcode_metadata.h
generated
14
Include/internal/pycore_opcode_metadata.h
generated
|
|
@ -1081,7 +1081,7 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
|
|||
[BINARY_OP] = { true, INSTR_FMT_IBC0000, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
|
||||
[BINARY_OP_ADD_FLOAT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG },
|
||||
[BINARY_OP_ADD_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG },
|
||||
[BINARY_OP_ADD_UNICODE] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG },
|
||||
[BINARY_OP_ADD_UNICODE] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG },
|
||||
[BINARY_OP_EXTEND] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG },
|
||||
[BINARY_OP_INPLACE_ADD_UNICODE] = { true, INSTR_FMT_IXC0000, HAS_LOCAL_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
|
||||
[BINARY_OP_MULTIPLY_FLOAT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG },
|
||||
|
|
@ -1331,16 +1331,16 @@ _PyOpcode_macro_expansion[256] = {
|
|||
[BINARY_OP] = { .nuops = 1, .uops = { { _BINARY_OP, OPARG_SIMPLE, 4 } } },
|
||||
[BINARY_OP_ADD_FLOAT] = { .nuops = 5, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_ADD_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_ADD_UNICODE] = { .nuops = 3, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_UNICODE, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_ADD_UNICODE] = { .nuops = 5, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_UNICODE, OPARG_SIMPLE, 5 }, { _POP_TOP_UNICODE, OPARG_SIMPLE, 5 }, { _POP_TOP_UNICODE, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_EXTEND] = { .nuops = 2, .uops = { { _GUARD_BINARY_OP_EXTEND, 4, 1 }, { _BINARY_OP_EXTEND, 4, 1 } } },
|
||||
[BINARY_OP_INPLACE_ADD_UNICODE] = { .nuops = 3, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_INPLACE_ADD_UNICODE, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_MULTIPLY_FLOAT] = { .nuops = 5, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _BINARY_OP_MULTIPLY_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_MULTIPLY_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _BINARY_OP_MULTIPLY_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_SUBSCR_DICT] = { .nuops = 2, .uops = { { _GUARD_NOS_DICT, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_DICT, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_SUBSCR_GETITEM] = { .nuops = 4, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 5 }, { _BINARY_OP_SUBSCR_CHECK_FUNC, OPARG_SIMPLE, 5 }, { _BINARY_OP_SUBSCR_INIT_CALL, OPARG_SIMPLE, 5 }, { _PUSH_FRAME, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_SUBSCR_LIST_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_LIST_INT, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_SUBSCR_LIST_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_LIST_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_SUBSCR_LIST_SLICE] = { .nuops = 3, .uops = { { _GUARD_TOS_SLICE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_LIST_SLICE, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_SUBSCR_STR_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_STR_INT, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_SUBSCR_STR_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_STR_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_SUBSCR_TUPLE_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_TUPLE, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_TUPLE_INT, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_SUBTRACT_FLOAT] = { .nuops = 5, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBTRACT_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 } } },
|
||||
[BINARY_OP_SUBTRACT_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBTRACT_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 } } },
|
||||
|
|
@ -1425,7 +1425,7 @@ _PyOpcode_macro_expansion[256] = {
|
|||
[LOAD_ATTR] = { .nuops = 1, .uops = { { _LOAD_ATTR, OPARG_SIMPLE, 8 } } },
|
||||
[LOAD_ATTR_CLASS] = { .nuops = 3, .uops = { { _CHECK_ATTR_CLASS, 2, 1 }, { _LOAD_ATTR_CLASS, 4, 5 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } },
|
||||
[LOAD_ATTR_CLASS_WITH_METACLASS_CHECK] = { .nuops = 4, .uops = { { _CHECK_ATTR_CLASS, 2, 1 }, { _GUARD_TYPE_VERSION, 2, 3 }, { _LOAD_ATTR_CLASS, 4, 5 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } },
|
||||
[LOAD_ATTR_INSTANCE_VALUE] = { .nuops = 4, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _CHECK_MANAGED_OBJECT_HAS_VALUES, OPARG_SIMPLE, 3 }, { _LOAD_ATTR_INSTANCE_VALUE, 1, 3 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } },
|
||||
[LOAD_ATTR_INSTANCE_VALUE] = { .nuops = 5, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _CHECK_MANAGED_OBJECT_HAS_VALUES, OPARG_SIMPLE, 3 }, { _LOAD_ATTR_INSTANCE_VALUE, 1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } },
|
||||
[LOAD_ATTR_METHOD_LAZY_DICT] = { .nuops = 3, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _CHECK_ATTR_METHOD_LAZY_DICT, 1, 3 }, { _LOAD_ATTR_METHOD_LAZY_DICT, 4, 5 } } },
|
||||
[LOAD_ATTR_METHOD_NO_DICT] = { .nuops = 2, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_METHOD_NO_DICT, 4, 5 } } },
|
||||
[LOAD_ATTR_METHOD_WITH_VALUES] = { .nuops = 4, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT, OPARG_SIMPLE, 3 }, { _GUARD_KEYS_VERSION, 2, 3 }, { _LOAD_ATTR_METHOD_WITH_VALUES, 4, 5 } } },
|
||||
|
|
@ -1434,7 +1434,7 @@ _PyOpcode_macro_expansion[256] = {
|
|||
[LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES] = { .nuops = 4, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT, OPARG_SIMPLE, 3 }, { _GUARD_KEYS_VERSION, 2, 3 }, { _LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES, 4, 5 } } },
|
||||
[LOAD_ATTR_PROPERTY] = { .nuops = 5, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_PROPERTY_FRAME, 4, 5 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 9 }, { _PUSH_FRAME, OPARG_SIMPLE, 9 } } },
|
||||
[LOAD_ATTR_SLOT] = { .nuops = 3, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_SLOT, 1, 3 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } },
|
||||
[LOAD_ATTR_WITH_HINT] = { .nuops = 3, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_WITH_HINT, 1, 3 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } },
|
||||
[LOAD_ATTR_WITH_HINT] = { .nuops = 4, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_WITH_HINT, 1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } },
|
||||
[LOAD_BUILD_CLASS] = { .nuops = 1, .uops = { { _LOAD_BUILD_CLASS, OPARG_SIMPLE, 0 } } },
|
||||
[LOAD_COMMON_CONSTANT] = { .nuops = 1, .uops = { { _LOAD_COMMON_CONSTANT, OPARG_SIMPLE, 0 } } },
|
||||
[LOAD_CONST] = { .nuops = 1, .uops = { { _LOAD_CONST, OPARG_SIMPLE, 0 } } },
|
||||
|
|
@ -1484,7 +1484,7 @@ _PyOpcode_macro_expansion[256] = {
|
|||
[STORE_ATTR] = { .nuops = 1, .uops = { { _STORE_ATTR, OPARG_SIMPLE, 3 } } },
|
||||
[STORE_ATTR_INSTANCE_VALUE] = { .nuops = 4, .uops = { { _GUARD_TYPE_VERSION_AND_LOCK, 2, 1 }, { _GUARD_DORV_NO_DICT, OPARG_SIMPLE, 3 }, { _STORE_ATTR_INSTANCE_VALUE, 1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 } } },
|
||||
[STORE_ATTR_SLOT] = { .nuops = 3, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _STORE_ATTR_SLOT, 1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 } } },
|
||||
[STORE_ATTR_WITH_HINT] = { .nuops = 2, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _STORE_ATTR_WITH_HINT, 1, 3 } } },
|
||||
[STORE_ATTR_WITH_HINT] = { .nuops = 3, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _STORE_ATTR_WITH_HINT, 1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 } } },
|
||||
[STORE_DEREF] = { .nuops = 1, .uops = { { _STORE_DEREF, OPARG_SIMPLE, 0 } } },
|
||||
[STORE_FAST] = { .nuops = 1, .uops = { { _STORE_FAST, OPARG_SIMPLE, 0 } } },
|
||||
[STORE_FAST_LOAD_FAST] = { .nuops = 2, .uops = { { _STORE_FAST, OPARG_TOP, 0 }, { _LOAD_FAST, OPARG_BOTTOM, 0 } } },
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ typedef struct {
|
|||
uint8_t opcode;
|
||||
uint8_t oparg;
|
||||
uint8_t valid;
|
||||
uint8_t linked;
|
||||
uint8_t chain_depth; // Must be big enough for MAX_CHAIN_DEPTH - 1.
|
||||
bool warm;
|
||||
int32_t index; // Index of ENTER_EXECUTOR (if code isn't NULL, below).
|
||||
|
|
@ -55,11 +54,6 @@ typedef struct _PyExecutorObject {
|
|||
_PyExitData exits[1];
|
||||
} _PyExecutorObject;
|
||||
|
||||
/* If pending deletion list gets large enough, then scan,
|
||||
* and free any executors that aren't executing
|
||||
* i.e. any that aren't a thread's current_executor. */
|
||||
#define EXECUTOR_DELETE_LIST_MAX 100
|
||||
|
||||
// Export for '_opcode' shared extension (JIT compiler).
|
||||
PyAPI_FUNC(_PyExecutorObject*) _Py_GetExecutor(PyCodeObject *code, int offset);
|
||||
|
||||
|
|
@ -80,7 +74,6 @@ PyAPI_FUNC(void) _Py_Executors_InvalidateCold(PyInterpreterState *interp);
|
|||
#else
|
||||
# define _Py_Executors_InvalidateDependency(A, B, C) ((void)0)
|
||||
# define _Py_Executors_InvalidateAll(A, B) ((void)0)
|
||||
# define _Py_Executors_InvalidateCold(A) ((void)0)
|
||||
|
||||
#endif
|
||||
|
||||
|
|
|
|||
|
|
@ -14,21 +14,6 @@ extern "C" {
|
|||
#include "pycore_pyarena.h" // PyArena
|
||||
|
||||
_Py_DECLARE_STR(empty, "")
|
||||
#if defined(Py_DEBUG) && defined(Py_GIL_DISABLED)
|
||||
#define _parser_runtime_state_INIT \
|
||||
{ \
|
||||
.mutex = {0}, \
|
||||
.dummy_name = { \
|
||||
.kind = Name_kind, \
|
||||
.v.Name.id = &_Py_STR(empty), \
|
||||
.v.Name.ctx = Load, \
|
||||
.lineno = 1, \
|
||||
.col_offset = 0, \
|
||||
.end_lineno = 1, \
|
||||
.end_col_offset = 0, \
|
||||
}, \
|
||||
}
|
||||
#else
|
||||
#define _parser_runtime_state_INIT \
|
||||
{ \
|
||||
.dummy_name = { \
|
||||
|
|
@ -41,7 +26,6 @@ _Py_DECLARE_STR(empty, "")
|
|||
.end_col_offset = 0, \
|
||||
}, \
|
||||
}
|
||||
#endif
|
||||
|
||||
extern struct _mod* _PyParser_ASTFromString(
|
||||
const char *str,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ extern "C" {
|
|||
_Py_atomic_load_uint8(&value)
|
||||
#define FT_ATOMIC_STORE_UINT8(value, new_value) \
|
||||
_Py_atomic_store_uint8(&value, new_value)
|
||||
#define FT_ATOMIC_LOAD_INT8_RELAXED(value) \
|
||||
_Py_atomic_load_int8_relaxed(&value)
|
||||
#define FT_ATOMIC_LOAD_UINT8_RELAXED(value) \
|
||||
_Py_atomic_load_uint8_relaxed(&value)
|
||||
#define FT_ATOMIC_LOAD_UINT16_RELAXED(value) \
|
||||
|
|
@ -55,6 +57,10 @@ extern "C" {
|
|||
_Py_atomic_store_ptr_release(&value, new_value)
|
||||
#define FT_ATOMIC_STORE_UINTPTR_RELEASE(value, new_value) \
|
||||
_Py_atomic_store_uintptr_release(&value, new_value)
|
||||
#define FT_ATOMIC_STORE_INT8_RELAXED(value, new_value) \
|
||||
_Py_atomic_store_int8_relaxed(&value, new_value)
|
||||
#define FT_ATOMIC_STORE_INT8_RELEASE(value, new_value) \
|
||||
_Py_atomic_store_int8_release(&value, new_value)
|
||||
#define FT_ATOMIC_STORE_SSIZE_RELAXED(value, new_value) \
|
||||
_Py_atomic_store_ssize_relaxed(&value, new_value)
|
||||
#define FT_ATOMIC_STORE_SSIZE_RELEASE(value, new_value) \
|
||||
|
|
@ -134,6 +140,7 @@ extern "C" {
|
|||
#define FT_ATOMIC_LOAD_PTR_RELAXED(value) value
|
||||
#define FT_ATOMIC_LOAD_UINT8(value) value
|
||||
#define FT_ATOMIC_STORE_UINT8(value, new_value) value = new_value
|
||||
#define FT_ATOMIC_LOAD_INT8_RELAXED(value) value
|
||||
#define FT_ATOMIC_LOAD_UINT8_RELAXED(value) value
|
||||
#define FT_ATOMIC_LOAD_UINT16_RELAXED(value) value
|
||||
#define FT_ATOMIC_LOAD_UINT32_RELAXED(value) value
|
||||
|
|
@ -141,6 +148,8 @@ extern "C" {
|
|||
#define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) value = new_value
|
||||
#define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) value = new_value
|
||||
#define FT_ATOMIC_STORE_UINTPTR_RELEASE(value, new_value) value = new_value
|
||||
#define FT_ATOMIC_STORE_INT8_RELAXED(value, new_value) value = new_value
|
||||
#define FT_ATOMIC_STORE_INT8_RELEASE(value, new_value) value = new_value
|
||||
#define FT_ATOMIC_STORE_SSIZE_RELAXED(value, new_value) value = new_value
|
||||
#define FT_ATOMIC_STORE_SSIZE_RELEASE(value, new_value) value = new_value
|
||||
#define FT_ATOMIC_STORE_UINT8_RELAXED(value, new_value) value = new_value
|
||||
|
|
|
|||
9
Include/internal/pycore_runtime_init_generated.h
generated
9
Include/internal/pycore_runtime_init_generated.h
generated
|
|
@ -1651,9 +1651,11 @@ extern "C" {
|
|||
INIT_ID(co_varnames), \
|
||||
INIT_ID(code), \
|
||||
INIT_ID(col_offset), \
|
||||
INIT_ID(collector), \
|
||||
INIT_ID(command), \
|
||||
INIT_ID(comment_factory), \
|
||||
INIT_ID(compile_mode), \
|
||||
INIT_ID(compression), \
|
||||
INIT_ID(config), \
|
||||
INIT_ID(consts), \
|
||||
INIT_ID(context), \
|
||||
|
|
@ -1716,7 +1718,9 @@ extern "C" {
|
|||
INIT_ID(event), \
|
||||
INIT_ID(eventmask), \
|
||||
INIT_ID(exc), \
|
||||
INIT_ID(exc_tb), \
|
||||
INIT_ID(exc_type), \
|
||||
INIT_ID(exc_val), \
|
||||
INIT_ID(exc_value), \
|
||||
INIT_ID(excepthook), \
|
||||
INIT_ID(exception), \
|
||||
|
|
@ -1972,6 +1976,7 @@ extern "C" {
|
|||
INIT_ID(print_file_and_line), \
|
||||
INIT_ID(priority), \
|
||||
INIT_ID(progress), \
|
||||
INIT_ID(progress_callback), \
|
||||
INIT_ID(progress_routine), \
|
||||
INIT_ID(proto), \
|
||||
INIT_ID(protocol), \
|
||||
|
|
@ -2012,6 +2017,7 @@ extern "C" {
|
|||
INIT_ID(reversed), \
|
||||
INIT_ID(rounding), \
|
||||
INIT_ID(salt), \
|
||||
INIT_ID(sample_interval_us), \
|
||||
INIT_ID(sched_priority), \
|
||||
INIT_ID(scheduler), \
|
||||
INIT_ID(script), \
|
||||
|
|
@ -2051,8 +2057,10 @@ extern "C" {
|
|||
INIT_ID(spam), \
|
||||
INIT_ID(src), \
|
||||
INIT_ID(src_dir_fd), \
|
||||
INIT_ID(stack_frames), \
|
||||
INIT_ID(stacklevel), \
|
||||
INIT_ID(start), \
|
||||
INIT_ID(start_time_us), \
|
||||
INIT_ID(statement), \
|
||||
INIT_ID(stats), \
|
||||
INIT_ID(status), \
|
||||
|
|
@ -2093,6 +2101,7 @@ extern "C" {
|
|||
INIT_ID(times), \
|
||||
INIT_ID(timespec), \
|
||||
INIT_ID(timestamp), \
|
||||
INIT_ID(timestamp_us), \
|
||||
INIT_ID(timetuple), \
|
||||
INIT_ID(timeunit), \
|
||||
INIT_ID(top), \
|
||||
|
|
|
|||
|
|
@ -77,9 +77,7 @@ struct _fileutils_state {
|
|||
struct _parser_runtime_state {
|
||||
#ifdef Py_DEBUG
|
||||
long memo_statistics[_PYPEGEN_NSTATISTICS];
|
||||
#ifdef Py_GIL_DISABLED
|
||||
PyMutex mutex;
|
||||
#endif
|
||||
#else
|
||||
int _not_used;
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ struct _PyTraceMalloc_Config {
|
|||
} initialized;
|
||||
|
||||
/* Is tracemalloc tracing memory allocations?
|
||||
Variable protected by the TABLES_LOCK(). */
|
||||
Variable protected by the TABLES_LOCK() and stored atomically.
|
||||
Atomic store is used so that it can read without locking for the
|
||||
general case of checking if tracemalloc is enabled.
|
||||
*/
|
||||
int tracing;
|
||||
|
||||
/* limit of the number of frames in a traceback, 1 by default.
|
||||
|
|
|
|||
|
|
@ -82,6 +82,13 @@ typedef struct _PyThreadStateImpl {
|
|||
PyObject *asyncio_running_loop; // Strong reference
|
||||
PyObject *asyncio_running_task; // Strong reference
|
||||
|
||||
// Distinguishes between yield and return from PyEval_EvalFrame().
|
||||
// See gen_send_ex2() in Objects/genobject.c
|
||||
enum {
|
||||
GENERATOR_RETURN = 0,
|
||||
GENERATOR_YIELD = 1,
|
||||
} generator_return_kind;
|
||||
|
||||
/* Head of circular linked-list of all tasks which are instances of `asyncio.Task`
|
||||
or subclasses of it used in `asyncio.all_tasks`.
|
||||
*/
|
||||
|
|
|
|||
36
Include/internal/pycore_unicodeobject_generated.h
generated
36
Include/internal/pycore_unicodeobject_generated.h
generated
|
|
@ -1284,6 +1284,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
|
|||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(collector);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(command);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
|
|
@ -1296,6 +1300,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
|
|||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(compression);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(config);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
|
|
@ -1544,10 +1552,18 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
|
|||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(exc_tb);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(exc_type);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(exc_val);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(exc_value);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
|
|
@ -2568,6 +2584,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
|
|||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(progress_callback);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(progress_routine);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
|
|
@ -2728,6 +2748,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
|
|||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(sample_interval_us);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(sched_priority);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
|
|
@ -2884,6 +2908,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
|
|||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(stack_frames);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(stacklevel);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
|
|
@ -2892,6 +2920,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
|
|||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(start_time_us);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(statement);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
|
|
@ -3052,6 +3084,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
|
|||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(timestamp_us);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(timetuple);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
|
|
|
|||
1216
Include/internal/pycore_uop_ids.h
generated
1216
Include/internal/pycore_uop_ids.h
generated
File diff suppressed because it is too large
Load diff
103
Include/internal/pycore_uop_metadata.h
generated
103
Include/internal/pycore_uop_metadata.h
generated
|
|
@ -111,7 +111,7 @@ const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = {
|
|||
[_BINARY_OP_MULTIPLY_FLOAT] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_PURE_FLAG,
|
||||
[_BINARY_OP_ADD_FLOAT] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_PURE_FLAG,
|
||||
[_BINARY_OP_SUBTRACT_FLOAT] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_PURE_FLAG,
|
||||
[_BINARY_OP_ADD_UNICODE] = HAS_ERROR_FLAG | HAS_PURE_FLAG,
|
||||
[_BINARY_OP_ADD_UNICODE] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_PURE_FLAG,
|
||||
[_BINARY_OP_INPLACE_ADD_UNICODE] = HAS_LOCAL_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
|
||||
[_GUARD_BINARY_OP_EXTEND] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG,
|
||||
[_BINARY_OP_EXTEND] = HAS_ESCAPES_FLAG,
|
||||
|
|
@ -119,7 +119,7 @@ const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = {
|
|||
[_STORE_SLICE] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
|
||||
[_BINARY_OP_SUBSCR_LIST_INT] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG,
|
||||
[_BINARY_OP_SUBSCR_LIST_SLICE] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
|
||||
[_BINARY_OP_SUBSCR_STR_INT] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG,
|
||||
[_BINARY_OP_SUBSCR_STR_INT] = HAS_DEOPT_FLAG,
|
||||
[_GUARD_NOS_TUPLE] = HAS_EXIT_FLAG,
|
||||
[_GUARD_TOS_TUPLE] = HAS_EXIT_FLAG,
|
||||
[_BINARY_OP_SUBSCR_TUPLE_INT] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG,
|
||||
|
|
@ -189,9 +189,9 @@ const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = {
|
|||
[_GUARD_TYPE_VERSION] = HAS_EXIT_FLAG,
|
||||
[_GUARD_TYPE_VERSION_AND_LOCK] = HAS_EXIT_FLAG,
|
||||
[_CHECK_MANAGED_OBJECT_HAS_VALUES] = HAS_DEOPT_FLAG,
|
||||
[_LOAD_ATTR_INSTANCE_VALUE] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG,
|
||||
[_LOAD_ATTR_INSTANCE_VALUE] = HAS_DEOPT_FLAG,
|
||||
[_LOAD_ATTR_MODULE] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG,
|
||||
[_LOAD_ATTR_WITH_HINT] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG,
|
||||
[_LOAD_ATTR_WITH_HINT] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG,
|
||||
[_LOAD_ATTR_SLOT] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG,
|
||||
[_CHECK_ATTR_CLASS] = HAS_EXIT_FLAG,
|
||||
[_LOAD_ATTR_CLASS] = HAS_ESCAPES_FLAG,
|
||||
|
|
@ -335,6 +335,7 @@ const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = {
|
|||
[_POP_TWO_LOAD_CONST_INLINE_BORROW] = HAS_ESCAPES_FLAG,
|
||||
[_POP_CALL_LOAD_CONST_INLINE_BORROW] = HAS_ESCAPES_FLAG,
|
||||
[_POP_CALL_ONE_LOAD_CONST_INLINE_BORROW] = HAS_ESCAPES_FLAG,
|
||||
[_SHUFFLE_3_LOAD_CONST_INLINE_BORROW] = 0,
|
||||
[_POP_CALL_TWO_LOAD_CONST_INLINE_BORROW] = HAS_ESCAPES_FLAG,
|
||||
[_LOAD_CONST_UNDER_INLINE] = 0,
|
||||
[_LOAD_CONST_UNDER_INLINE_BORROW] = 0,
|
||||
|
|
@ -1050,12 +1051,12 @@ const _PyUopCachingInfo _PyUop_Caching[MAX_UOP_ID+1] = {
|
|||
},
|
||||
},
|
||||
[_BINARY_OP_ADD_UNICODE] = {
|
||||
.best = { 0, 1, 2, 3 },
|
||||
.best = { 0, 1, 2, 2 },
|
||||
.entries = {
|
||||
{ 1, 0, _BINARY_OP_ADD_UNICODE_r01 },
|
||||
{ 1, 1, _BINARY_OP_ADD_UNICODE_r11 },
|
||||
{ 1, 2, _BINARY_OP_ADD_UNICODE_r21 },
|
||||
{ 2, 3, _BINARY_OP_ADD_UNICODE_r32 },
|
||||
{ 3, 0, _BINARY_OP_ADD_UNICODE_r03 },
|
||||
{ 3, 1, _BINARY_OP_ADD_UNICODE_r13 },
|
||||
{ 3, 2, _BINARY_OP_ADD_UNICODE_r23 },
|
||||
{ -1, -1, -1 },
|
||||
},
|
||||
},
|
||||
[_BINARY_OP_INPLACE_ADD_UNICODE] = {
|
||||
|
|
@ -1108,7 +1109,7 @@ const _PyUopCachingInfo _PyUop_Caching[MAX_UOP_ID+1] = {
|
|||
.entries = {
|
||||
{ -1, -1, -1 },
|
||||
{ -1, -1, -1 },
|
||||
{ 1, 2, _BINARY_OP_SUBSCR_LIST_INT_r21 },
|
||||
{ 3, 2, _BINARY_OP_SUBSCR_LIST_INT_r23 },
|
||||
{ -1, -1, -1 },
|
||||
},
|
||||
},
|
||||
|
|
@ -1126,7 +1127,7 @@ const _PyUopCachingInfo _PyUop_Caching[MAX_UOP_ID+1] = {
|
|||
.entries = {
|
||||
{ -1, -1, -1 },
|
||||
{ -1, -1, -1 },
|
||||
{ 1, 2, _BINARY_OP_SUBSCR_STR_INT_r21 },
|
||||
{ 3, 2, _BINARY_OP_SUBSCR_STR_INT_r23 },
|
||||
{ -1, -1, -1 },
|
||||
},
|
||||
},
|
||||
|
|
@ -1752,11 +1753,11 @@ const _PyUopCachingInfo _PyUop_Caching[MAX_UOP_ID+1] = {
|
|||
},
|
||||
},
|
||||
[_LOAD_ATTR_INSTANCE_VALUE] = {
|
||||
.best = { 1, 1, 1, 1 },
|
||||
.best = { 0, 1, 2, 2 },
|
||||
.entries = {
|
||||
{ -1, -1, -1 },
|
||||
{ 1, 1, _LOAD_ATTR_INSTANCE_VALUE_r11 },
|
||||
{ -1, -1, -1 },
|
||||
{ 2, 0, _LOAD_ATTR_INSTANCE_VALUE_r02 },
|
||||
{ 2, 1, _LOAD_ATTR_INSTANCE_VALUE_r12 },
|
||||
{ 3, 2, _LOAD_ATTR_INSTANCE_VALUE_r23 },
|
||||
{ -1, -1, -1 },
|
||||
},
|
||||
},
|
||||
|
|
@ -1773,7 +1774,7 @@ const _PyUopCachingInfo _PyUop_Caching[MAX_UOP_ID+1] = {
|
|||
.best = { 1, 1, 1, 1 },
|
||||
.entries = {
|
||||
{ -1, -1, -1 },
|
||||
{ 1, 1, _LOAD_ATTR_WITH_HINT_r11 },
|
||||
{ 2, 1, _LOAD_ATTR_WITH_HINT_r12 },
|
||||
{ -1, -1, -1 },
|
||||
{ -1, -1, -1 },
|
||||
},
|
||||
|
|
@ -1837,7 +1838,7 @@ const _PyUopCachingInfo _PyUop_Caching[MAX_UOP_ID+1] = {
|
|||
.entries = {
|
||||
{ -1, -1, -1 },
|
||||
{ -1, -1, -1 },
|
||||
{ 0, 2, _STORE_ATTR_WITH_HINT_r20 },
|
||||
{ 1, 2, _STORE_ATTR_WITH_HINT_r21 },
|
||||
{ -1, -1, -1 },
|
||||
},
|
||||
},
|
||||
|
|
@ -2130,10 +2131,10 @@ const _PyUopCachingInfo _PyUop_Caching[MAX_UOP_ID+1] = {
|
|||
},
|
||||
},
|
||||
[_FOR_ITER_GEN_FRAME] = {
|
||||
.best = { 2, 2, 2, 2 },
|
||||
.best = { 0, 1, 2, 2 },
|
||||
.entries = {
|
||||
{ -1, -1, -1 },
|
||||
{ -1, -1, -1 },
|
||||
{ 3, 0, _FOR_ITER_GEN_FRAME_r03 },
|
||||
{ 3, 1, _FOR_ITER_GEN_FRAME_r13 },
|
||||
{ 3, 2, _FOR_ITER_GEN_FRAME_r23 },
|
||||
{ -1, -1, -1 },
|
||||
},
|
||||
|
|
@ -3065,6 +3066,15 @@ const _PyUopCachingInfo _PyUop_Caching[MAX_UOP_ID+1] = {
|
|||
{ 1, 3, _POP_CALL_ONE_LOAD_CONST_INLINE_BORROW_r31 },
|
||||
},
|
||||
},
|
||||
[_SHUFFLE_3_LOAD_CONST_INLINE_BORROW] = {
|
||||
.best = { 0, 1, 2, 3 },
|
||||
.entries = {
|
||||
{ 3, 0, _SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r03 },
|
||||
{ 3, 1, _SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r13 },
|
||||
{ 3, 2, _SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r23 },
|
||||
{ 3, 3, _SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r33 },
|
||||
},
|
||||
},
|
||||
[_POP_CALL_TWO_LOAD_CONST_INLINE_BORROW] = {
|
||||
.best = { 3, 3, 3, 3 },
|
||||
.entries = {
|
||||
|
|
@ -3414,18 +3424,17 @@ const uint16_t _PyUop_Uncached[MAX_UOP_REGS_ID+1] = {
|
|||
[_BINARY_OP_SUBTRACT_FLOAT_r03] = _BINARY_OP_SUBTRACT_FLOAT,
|
||||
[_BINARY_OP_SUBTRACT_FLOAT_r13] = _BINARY_OP_SUBTRACT_FLOAT,
|
||||
[_BINARY_OP_SUBTRACT_FLOAT_r23] = _BINARY_OP_SUBTRACT_FLOAT,
|
||||
[_BINARY_OP_ADD_UNICODE_r01] = _BINARY_OP_ADD_UNICODE,
|
||||
[_BINARY_OP_ADD_UNICODE_r11] = _BINARY_OP_ADD_UNICODE,
|
||||
[_BINARY_OP_ADD_UNICODE_r21] = _BINARY_OP_ADD_UNICODE,
|
||||
[_BINARY_OP_ADD_UNICODE_r32] = _BINARY_OP_ADD_UNICODE,
|
||||
[_BINARY_OP_ADD_UNICODE_r03] = _BINARY_OP_ADD_UNICODE,
|
||||
[_BINARY_OP_ADD_UNICODE_r13] = _BINARY_OP_ADD_UNICODE,
|
||||
[_BINARY_OP_ADD_UNICODE_r23] = _BINARY_OP_ADD_UNICODE,
|
||||
[_BINARY_OP_INPLACE_ADD_UNICODE_r20] = _BINARY_OP_INPLACE_ADD_UNICODE,
|
||||
[_GUARD_BINARY_OP_EXTEND_r22] = _GUARD_BINARY_OP_EXTEND,
|
||||
[_BINARY_OP_EXTEND_r21] = _BINARY_OP_EXTEND,
|
||||
[_BINARY_SLICE_r31] = _BINARY_SLICE,
|
||||
[_STORE_SLICE_r30] = _STORE_SLICE,
|
||||
[_BINARY_OP_SUBSCR_LIST_INT_r21] = _BINARY_OP_SUBSCR_LIST_INT,
|
||||
[_BINARY_OP_SUBSCR_LIST_INT_r23] = _BINARY_OP_SUBSCR_LIST_INT,
|
||||
[_BINARY_OP_SUBSCR_LIST_SLICE_r21] = _BINARY_OP_SUBSCR_LIST_SLICE,
|
||||
[_BINARY_OP_SUBSCR_STR_INT_r21] = _BINARY_OP_SUBSCR_STR_INT,
|
||||
[_BINARY_OP_SUBSCR_STR_INT_r23] = _BINARY_OP_SUBSCR_STR_INT,
|
||||
[_GUARD_NOS_TUPLE_r02] = _GUARD_NOS_TUPLE,
|
||||
[_GUARD_NOS_TUPLE_r12] = _GUARD_NOS_TUPLE,
|
||||
[_GUARD_NOS_TUPLE_r22] = _GUARD_NOS_TUPLE,
|
||||
|
|
@ -3529,9 +3538,11 @@ const uint16_t _PyUop_Uncached[MAX_UOP_REGS_ID+1] = {
|
|||
[_CHECK_MANAGED_OBJECT_HAS_VALUES_r11] = _CHECK_MANAGED_OBJECT_HAS_VALUES,
|
||||
[_CHECK_MANAGED_OBJECT_HAS_VALUES_r22] = _CHECK_MANAGED_OBJECT_HAS_VALUES,
|
||||
[_CHECK_MANAGED_OBJECT_HAS_VALUES_r33] = _CHECK_MANAGED_OBJECT_HAS_VALUES,
|
||||
[_LOAD_ATTR_INSTANCE_VALUE_r11] = _LOAD_ATTR_INSTANCE_VALUE,
|
||||
[_LOAD_ATTR_INSTANCE_VALUE_r02] = _LOAD_ATTR_INSTANCE_VALUE,
|
||||
[_LOAD_ATTR_INSTANCE_VALUE_r12] = _LOAD_ATTR_INSTANCE_VALUE,
|
||||
[_LOAD_ATTR_INSTANCE_VALUE_r23] = _LOAD_ATTR_INSTANCE_VALUE,
|
||||
[_LOAD_ATTR_MODULE_r11] = _LOAD_ATTR_MODULE,
|
||||
[_LOAD_ATTR_WITH_HINT_r11] = _LOAD_ATTR_WITH_HINT,
|
||||
[_LOAD_ATTR_WITH_HINT_r12] = _LOAD_ATTR_WITH_HINT,
|
||||
[_LOAD_ATTR_SLOT_r11] = _LOAD_ATTR_SLOT,
|
||||
[_CHECK_ATTR_CLASS_r01] = _CHECK_ATTR_CLASS,
|
||||
[_CHECK_ATTR_CLASS_r11] = _CHECK_ATTR_CLASS,
|
||||
|
|
@ -3544,7 +3555,7 @@ const uint16_t _PyUop_Uncached[MAX_UOP_REGS_ID+1] = {
|
|||
[_GUARD_DORV_NO_DICT_r22] = _GUARD_DORV_NO_DICT,
|
||||
[_GUARD_DORV_NO_DICT_r33] = _GUARD_DORV_NO_DICT,
|
||||
[_STORE_ATTR_INSTANCE_VALUE_r21] = _STORE_ATTR_INSTANCE_VALUE,
|
||||
[_STORE_ATTR_WITH_HINT_r20] = _STORE_ATTR_WITH_HINT,
|
||||
[_STORE_ATTR_WITH_HINT_r21] = _STORE_ATTR_WITH_HINT,
|
||||
[_STORE_ATTR_SLOT_r21] = _STORE_ATTR_SLOT,
|
||||
[_COMPARE_OP_r21] = _COMPARE_OP,
|
||||
[_COMPARE_OP_FLOAT_r01] = _COMPARE_OP_FLOAT,
|
||||
|
|
@ -3609,6 +3620,8 @@ const uint16_t _PyUop_Uncached[MAX_UOP_REGS_ID+1] = {
|
|||
[_ITER_NEXT_RANGE_r03] = _ITER_NEXT_RANGE,
|
||||
[_ITER_NEXT_RANGE_r13] = _ITER_NEXT_RANGE,
|
||||
[_ITER_NEXT_RANGE_r23] = _ITER_NEXT_RANGE,
|
||||
[_FOR_ITER_GEN_FRAME_r03] = _FOR_ITER_GEN_FRAME,
|
||||
[_FOR_ITER_GEN_FRAME_r13] = _FOR_ITER_GEN_FRAME,
|
||||
[_FOR_ITER_GEN_FRAME_r23] = _FOR_ITER_GEN_FRAME,
|
||||
[_INSERT_NULL_r10] = _INSERT_NULL,
|
||||
[_LOAD_SPECIAL_r00] = _LOAD_SPECIAL,
|
||||
|
|
@ -3816,6 +3829,10 @@ const uint16_t _PyUop_Uncached[MAX_UOP_REGS_ID+1] = {
|
|||
[_POP_TWO_LOAD_CONST_INLINE_BORROW_r21] = _POP_TWO_LOAD_CONST_INLINE_BORROW,
|
||||
[_POP_CALL_LOAD_CONST_INLINE_BORROW_r21] = _POP_CALL_LOAD_CONST_INLINE_BORROW,
|
||||
[_POP_CALL_ONE_LOAD_CONST_INLINE_BORROW_r31] = _POP_CALL_ONE_LOAD_CONST_INLINE_BORROW,
|
||||
[_SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r03] = _SHUFFLE_3_LOAD_CONST_INLINE_BORROW,
|
||||
[_SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r13] = _SHUFFLE_3_LOAD_CONST_INLINE_BORROW,
|
||||
[_SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r23] = _SHUFFLE_3_LOAD_CONST_INLINE_BORROW,
|
||||
[_SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r33] = _SHUFFLE_3_LOAD_CONST_INLINE_BORROW,
|
||||
[_POP_CALL_TWO_LOAD_CONST_INLINE_BORROW_r31] = _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW,
|
||||
[_LOAD_CONST_UNDER_INLINE_r02] = _LOAD_CONST_UNDER_INLINE,
|
||||
[_LOAD_CONST_UNDER_INLINE_r12] = _LOAD_CONST_UNDER_INLINE,
|
||||
|
|
@ -3904,10 +3921,9 @@ const char *const _PyOpcode_uop_name[MAX_UOP_REGS_ID+1] = {
|
|||
[_BINARY_OP_ADD_INT_r13] = "_BINARY_OP_ADD_INT_r13",
|
||||
[_BINARY_OP_ADD_INT_r23] = "_BINARY_OP_ADD_INT_r23",
|
||||
[_BINARY_OP_ADD_UNICODE] = "_BINARY_OP_ADD_UNICODE",
|
||||
[_BINARY_OP_ADD_UNICODE_r01] = "_BINARY_OP_ADD_UNICODE_r01",
|
||||
[_BINARY_OP_ADD_UNICODE_r11] = "_BINARY_OP_ADD_UNICODE_r11",
|
||||
[_BINARY_OP_ADD_UNICODE_r21] = "_BINARY_OP_ADD_UNICODE_r21",
|
||||
[_BINARY_OP_ADD_UNICODE_r32] = "_BINARY_OP_ADD_UNICODE_r32",
|
||||
[_BINARY_OP_ADD_UNICODE_r03] = "_BINARY_OP_ADD_UNICODE_r03",
|
||||
[_BINARY_OP_ADD_UNICODE_r13] = "_BINARY_OP_ADD_UNICODE_r13",
|
||||
[_BINARY_OP_ADD_UNICODE_r23] = "_BINARY_OP_ADD_UNICODE_r23",
|
||||
[_BINARY_OP_EXTEND] = "_BINARY_OP_EXTEND",
|
||||
[_BINARY_OP_EXTEND_r21] = "_BINARY_OP_EXTEND_r21",
|
||||
[_BINARY_OP_INPLACE_ADD_UNICODE] = "_BINARY_OP_INPLACE_ADD_UNICODE",
|
||||
|
|
@ -3930,11 +3946,11 @@ const char *const _PyOpcode_uop_name[MAX_UOP_REGS_ID+1] = {
|
|||
[_BINARY_OP_SUBSCR_INIT_CALL_r21] = "_BINARY_OP_SUBSCR_INIT_CALL_r21",
|
||||
[_BINARY_OP_SUBSCR_INIT_CALL_r31] = "_BINARY_OP_SUBSCR_INIT_CALL_r31",
|
||||
[_BINARY_OP_SUBSCR_LIST_INT] = "_BINARY_OP_SUBSCR_LIST_INT",
|
||||
[_BINARY_OP_SUBSCR_LIST_INT_r21] = "_BINARY_OP_SUBSCR_LIST_INT_r21",
|
||||
[_BINARY_OP_SUBSCR_LIST_INT_r23] = "_BINARY_OP_SUBSCR_LIST_INT_r23",
|
||||
[_BINARY_OP_SUBSCR_LIST_SLICE] = "_BINARY_OP_SUBSCR_LIST_SLICE",
|
||||
[_BINARY_OP_SUBSCR_LIST_SLICE_r21] = "_BINARY_OP_SUBSCR_LIST_SLICE_r21",
|
||||
[_BINARY_OP_SUBSCR_STR_INT] = "_BINARY_OP_SUBSCR_STR_INT",
|
||||
[_BINARY_OP_SUBSCR_STR_INT_r21] = "_BINARY_OP_SUBSCR_STR_INT_r21",
|
||||
[_BINARY_OP_SUBSCR_STR_INT_r23] = "_BINARY_OP_SUBSCR_STR_INT_r23",
|
||||
[_BINARY_OP_SUBSCR_TUPLE_INT] = "_BINARY_OP_SUBSCR_TUPLE_INT",
|
||||
[_BINARY_OP_SUBSCR_TUPLE_INT_r21] = "_BINARY_OP_SUBSCR_TUPLE_INT_r21",
|
||||
[_BINARY_OP_SUBTRACT_FLOAT] = "_BINARY_OP_SUBTRACT_FLOAT",
|
||||
|
|
@ -4168,6 +4184,8 @@ const char *const _PyOpcode_uop_name[MAX_UOP_REGS_ID+1] = {
|
|||
[_FORMAT_WITH_SPEC] = "_FORMAT_WITH_SPEC",
|
||||
[_FORMAT_WITH_SPEC_r21] = "_FORMAT_WITH_SPEC_r21",
|
||||
[_FOR_ITER_GEN_FRAME] = "_FOR_ITER_GEN_FRAME",
|
||||
[_FOR_ITER_GEN_FRAME_r03] = "_FOR_ITER_GEN_FRAME_r03",
|
||||
[_FOR_ITER_GEN_FRAME_r13] = "_FOR_ITER_GEN_FRAME_r13",
|
||||
[_FOR_ITER_GEN_FRAME_r23] = "_FOR_ITER_GEN_FRAME_r23",
|
||||
[_FOR_ITER_TIER_TWO] = "_FOR_ITER_TIER_TWO",
|
||||
[_FOR_ITER_TIER_TWO_r23] = "_FOR_ITER_TIER_TWO_r23",
|
||||
|
|
@ -4457,7 +4475,9 @@ const char *const _PyOpcode_uop_name[MAX_UOP_REGS_ID+1] = {
|
|||
[_LOAD_ATTR_CLASS] = "_LOAD_ATTR_CLASS",
|
||||
[_LOAD_ATTR_CLASS_r11] = "_LOAD_ATTR_CLASS_r11",
|
||||
[_LOAD_ATTR_INSTANCE_VALUE] = "_LOAD_ATTR_INSTANCE_VALUE",
|
||||
[_LOAD_ATTR_INSTANCE_VALUE_r11] = "_LOAD_ATTR_INSTANCE_VALUE_r11",
|
||||
[_LOAD_ATTR_INSTANCE_VALUE_r02] = "_LOAD_ATTR_INSTANCE_VALUE_r02",
|
||||
[_LOAD_ATTR_INSTANCE_VALUE_r12] = "_LOAD_ATTR_INSTANCE_VALUE_r12",
|
||||
[_LOAD_ATTR_INSTANCE_VALUE_r23] = "_LOAD_ATTR_INSTANCE_VALUE_r23",
|
||||
[_LOAD_ATTR_METHOD_LAZY_DICT] = "_LOAD_ATTR_METHOD_LAZY_DICT",
|
||||
[_LOAD_ATTR_METHOD_LAZY_DICT_r02] = "_LOAD_ATTR_METHOD_LAZY_DICT_r02",
|
||||
[_LOAD_ATTR_METHOD_LAZY_DICT_r12] = "_LOAD_ATTR_METHOD_LAZY_DICT_r12",
|
||||
|
|
@ -4481,7 +4501,7 @@ const char *const _PyOpcode_uop_name[MAX_UOP_REGS_ID+1] = {
|
|||
[_LOAD_ATTR_SLOT] = "_LOAD_ATTR_SLOT",
|
||||
[_LOAD_ATTR_SLOT_r11] = "_LOAD_ATTR_SLOT_r11",
|
||||
[_LOAD_ATTR_WITH_HINT] = "_LOAD_ATTR_WITH_HINT",
|
||||
[_LOAD_ATTR_WITH_HINT_r11] = "_LOAD_ATTR_WITH_HINT_r11",
|
||||
[_LOAD_ATTR_WITH_HINT_r12] = "_LOAD_ATTR_WITH_HINT_r12",
|
||||
[_LOAD_BUILD_CLASS] = "_LOAD_BUILD_CLASS",
|
||||
[_LOAD_BUILD_CLASS_r01] = "_LOAD_BUILD_CLASS_r01",
|
||||
[_LOAD_COMMON_CONSTANT] = "_LOAD_COMMON_CONSTANT",
|
||||
|
|
@ -4760,6 +4780,11 @@ const char *const _PyOpcode_uop_name[MAX_UOP_REGS_ID+1] = {
|
|||
[_SET_IP_r33] = "_SET_IP_r33",
|
||||
[_SET_UPDATE] = "_SET_UPDATE",
|
||||
[_SET_UPDATE_r10] = "_SET_UPDATE_r10",
|
||||
[_SHUFFLE_3_LOAD_CONST_INLINE_BORROW] = "_SHUFFLE_3_LOAD_CONST_INLINE_BORROW",
|
||||
[_SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r03] = "_SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r03",
|
||||
[_SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r13] = "_SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r13",
|
||||
[_SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r23] = "_SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r23",
|
||||
[_SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r33] = "_SHUFFLE_3_LOAD_CONST_INLINE_BORROW_r33",
|
||||
[_SPILL_OR_RELOAD] = "_SPILL_OR_RELOAD",
|
||||
[_SPILL_OR_RELOAD_r01] = "_SPILL_OR_RELOAD_r01",
|
||||
[_SPILL_OR_RELOAD_r02] = "_SPILL_OR_RELOAD_r02",
|
||||
|
|
@ -4782,7 +4807,7 @@ const char *const _PyOpcode_uop_name[MAX_UOP_REGS_ID+1] = {
|
|||
[_STORE_ATTR_SLOT] = "_STORE_ATTR_SLOT",
|
||||
[_STORE_ATTR_SLOT_r21] = "_STORE_ATTR_SLOT_r21",
|
||||
[_STORE_ATTR_WITH_HINT] = "_STORE_ATTR_WITH_HINT",
|
||||
[_STORE_ATTR_WITH_HINT_r20] = "_STORE_ATTR_WITH_HINT_r20",
|
||||
[_STORE_ATTR_WITH_HINT_r21] = "_STORE_ATTR_WITH_HINT_r21",
|
||||
[_STORE_DEREF] = "_STORE_DEREF",
|
||||
[_STORE_DEREF_r10] = "_STORE_DEREF_r10",
|
||||
[_STORE_FAST] = "_STORE_FAST",
|
||||
|
|
@ -5477,6 +5502,8 @@ int _PyUop_num_popped(int opcode, int oparg)
|
|||
return 2;
|
||||
case _POP_CALL_ONE_LOAD_CONST_INLINE_BORROW:
|
||||
return 3;
|
||||
case _SHUFFLE_3_LOAD_CONST_INLINE_BORROW:
|
||||
return 3;
|
||||
case _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW:
|
||||
return 4;
|
||||
case _LOAD_CONST_UNDER_INLINE:
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
#define PY_RELEASE_SERIAL 3
|
||||
|
||||
/* Version as a string */
|
||||
#define PY_VERSION "3.15.0a3"
|
||||
#define PY_VERSION "3.15.0a3+"
|
||||
/*--end constants--*/
|
||||
|
||||
|
||||
|
|
|
|||
489
InternalDocs/profiling_binary_format.md
Normal file
489
InternalDocs/profiling_binary_format.md
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
# Profiling Binary Format
|
||||
|
||||
The profiling module includes a binary file format for storing sampling
|
||||
profiler data. This document describes the format's structure and the
|
||||
design decisions behind it.
|
||||
|
||||
The implementation is in
|
||||
[`Modules/_remote_debugging/binary_io_writer.c`](../Modules/_remote_debugging/binary_io_writer.c)
|
||||
and [`Modules/_remote_debugging/binary_io_reader.c`](../Modules/_remote_debugging/binary_io_reader.c),
|
||||
with declarations in
|
||||
[`Modules/_remote_debugging/binary_io.h`](../Modules/_remote_debugging/binary_io.h).
|
||||
|
||||
## Overview
|
||||
|
||||
The sampling profiler can generate enormous amounts of data. A typical
|
||||
profiling session sampling at 1000 Hz for 60 seconds produces 60,000 samples.
|
||||
Each sample contains a full call stack, often 20-50 frames deep, and each
|
||||
frame includes a filename, function name, and line number. In a text-based
|
||||
format like collapsed stacks, this would mean repeating the same long file
|
||||
paths and function names thousands of times.
|
||||
|
||||
The binary format addresses this through two key strategies:
|
||||
|
||||
1. **Deduplication**: Strings and frames are stored once in lookup tables,
|
||||
then referenced by small integer indices. A 100-character file path that
|
||||
appears in 50,000 samples is stored once, not 50,000 times.
|
||||
|
||||
2. **Compact encoding**: Variable-length integers (varints) encode small
|
||||
values in fewer bytes. Since most indices are small (under 128), they
|
||||
typically need only one byte instead of four.
|
||||
|
||||
Together with optional zstd compression, these techniques reduce file sizes
|
||||
by 10-50x compared to text formats while also enabling faster I/O.
|
||||
|
||||
## File Layout
|
||||
|
||||
The file consists of five sections:
|
||||
|
||||
```
|
||||
+------------------+ Offset 0
|
||||
| Header | 64 bytes (fixed)
|
||||
+------------------+ Offset 64
|
||||
| |
|
||||
| Sample Data | Variable size (optionally compressed)
|
||||
| |
|
||||
+------------------+ string_table_offset
|
||||
| String Table | Variable size
|
||||
+------------------+ frame_table_offset
|
||||
| Frame Table | Variable size
|
||||
+------------------+ file_size - 32
|
||||
| Footer | 32 bytes (fixed)
|
||||
+------------------+ file_size
|
||||
```
|
||||
|
||||
The layout is designed for streaming writes during profiling. The profiler
|
||||
cannot know in advance how many unique strings or frames will be encountered,
|
||||
so these tables must be built incrementally and written at the end.
|
||||
|
||||
The header comes first so readers can quickly validate the file and locate
|
||||
the metadata tables. The sample data follows immediately, allowing the writer
|
||||
to stream samples directly to disk (or through a compression stream) without
|
||||
buffering the entire dataset in memory.
|
||||
|
||||
The string and frame tables are placed after sample data because they grow
|
||||
as new unique entries are discovered during profiling. By deferring their
|
||||
output until finalization, the writer avoids the complexity of reserving
|
||||
space or rewriting portions of the file.
|
||||
|
||||
The footer at the end contains counts needed to allocate arrays before
|
||||
parsing the tables. Placing it at a fixed offset from the end (rather than
|
||||
at a variable offset recorded in the header) means readers can locate it
|
||||
with a single seek to `file_size - 32`, without first reading the header.
|
||||
|
||||
## Header
|
||||
|
||||
```
|
||||
Offset Size Type Description
|
||||
+--------+------+---------+----------------------------------------+
|
||||
| 0 | 4 | uint32 | Magic number (0x54414348 = "TACH") |
|
||||
| 4 | 4 | uint32 | Format version |
|
||||
| 8 | 4 | bytes | Python version (major, minor, micro, |
|
||||
| | | | reserved) |
|
||||
| 12 | 8 | uint64 | Start timestamp (microseconds) |
|
||||
| 20 | 8 | uint64 | Sample interval (microseconds) |
|
||||
| 28 | 4 | uint32 | Total sample count |
|
||||
| 32 | 4 | uint32 | Thread count |
|
||||
| 36 | 8 | uint64 | String table offset |
|
||||
| 44 | 8 | uint64 | Frame table offset |
|
||||
| 52 | 4 | uint32 | Compression type (0=none, 1=zstd) |
|
||||
| 56 | 8 | bytes | Reserved (zero-filled) |
|
||||
+--------+------+---------+----------------------------------------+
|
||||
```
|
||||
|
||||
The magic number `0x54414348` ("TACH" for Tachyon) identifies the file format
|
||||
and also serves as an **endianness marker**. When read on a system with
|
||||
different byte order than the writer, it appears as `0x48434154`. The reader
|
||||
uses this to detect cross-endian files and automatically byte-swap all
|
||||
multi-byte integer fields.
|
||||
|
||||
The Python version field records the major, minor, and micro version numbers
|
||||
of the Python interpreter that generated the file. This allows analysis tools
|
||||
to detect version mismatches when replaying data collected on a different
|
||||
Python version, which may have different internal structures or behaviors.
|
||||
|
||||
The header is written as zeros initially, then overwritten with actual values
|
||||
during finalization. This requires the output stream to be seekable, which
|
||||
is acceptable since the format targets regular files rather than pipes or
|
||||
network streams.
|
||||
|
||||
## Sample Data
|
||||
|
||||
Sample data begins at offset 64 and extends to `string_table_offset`. Samples
|
||||
use delta compression to minimize redundancy when consecutive samples from the
|
||||
same thread have identical or similar call stacks.
|
||||
|
||||
### Stack Encoding Types
|
||||
|
||||
Each sample record begins with thread identification, then an encoding byte:
|
||||
|
||||
| Code | Name | Description |
|
||||
|------|------|-------------|
|
||||
| 0x00 | REPEAT | RLE: identical stack repeated N times |
|
||||
| 0x01 | FULL | Complete stack (first sample or no match) |
|
||||
| 0x02 | SUFFIX | Shares N frames from bottom of previous stack |
|
||||
| 0x03 | POP_PUSH | Remove M frames from top, add N new frames |
|
||||
|
||||
### Record Formats
|
||||
|
||||
**REPEAT (0x00) - Run-Length Encoded Identical Stacks:**
|
||||
```
|
||||
+-----------------+-----------+----------------------------------------+
|
||||
| thread_id | 8 bytes | Thread identifier (uint64, fixed) |
|
||||
| interpreter_id | 4 bytes | Interpreter ID (uint32, fixed) |
|
||||
| encoding | 1 byte | 0x00 (REPEAT) |
|
||||
| count | varint | Number of samples in this RLE group |
|
||||
| samples | varies | Interleaved: [delta: varint, status: 1]|
|
||||
| | | repeated count times |
|
||||
+-----------------+-----------+----------------------------------------+
|
||||
```
|
||||
The stack is inherited from this thread's previous sample. Each sample in the
|
||||
group gets its own timestamp delta and status byte, stored as interleaved pairs
|
||||
(delta1, status1, delta2, status2, ...) rather than separate arrays.
|
||||
|
||||
**FULL (0x01) - Complete Stack:**
|
||||
```
|
||||
+-----------------+-----------+----------------------------------------+
|
||||
| thread_id | 8 bytes | Thread identifier (uint64, fixed) |
|
||||
| interpreter_id | 4 bytes | Interpreter ID (uint32, fixed) |
|
||||
| encoding | 1 byte | 0x01 (FULL) |
|
||||
| timestamp_delta | varint | Microseconds since thread's last sample|
|
||||
| status | 1 byte | Thread state flags |
|
||||
| stack_depth | varint | Number of frames in call stack |
|
||||
| frame_indices | varint[] | Array of frame table indices |
|
||||
+-----------------+-----------+----------------------------------------+
|
||||
```
|
||||
Used for the first sample from a thread, or when delta encoding would not
|
||||
provide savings.
|
||||
|
||||
**SUFFIX (0x02) - Shared Suffix Match:**
|
||||
```
|
||||
+-----------------+-----------+----------------------------------------+
|
||||
| thread_id | 8 bytes | Thread identifier (uint64, fixed) |
|
||||
| interpreter_id | 4 bytes | Interpreter ID (uint32, fixed) |
|
||||
| encoding | 1 byte | 0x02 (SUFFIX) |
|
||||
| timestamp_delta | varint | Microseconds since thread's last sample|
|
||||
| status | 1 byte | Thread state flags |
|
||||
| shared_count | varint | Frames shared from bottom of prev stack|
|
||||
| new_count | varint | New frames at top of stack |
|
||||
| new_frames | varint[] | Array of new_count frame indices |
|
||||
+-----------------+-----------+----------------------------------------+
|
||||
```
|
||||
Used when a function call added frames to the top of the stack. The shared
|
||||
frames from the previous stack are kept, and new frames are prepended.
|
||||
|
||||
**POP_PUSH (0x03) - Pop and Push:**
|
||||
```
|
||||
+-----------------+-----------+----------------------------------------+
|
||||
| thread_id | 8 bytes | Thread identifier (uint64, fixed) |
|
||||
| interpreter_id | 4 bytes | Interpreter ID (uint32, fixed) |
|
||||
| encoding | 1 byte | 0x03 (POP_PUSH) |
|
||||
| timestamp_delta | varint | Microseconds since thread's last sample|
|
||||
| status | 1 byte | Thread state flags |
|
||||
| pop_count | varint | Frames to remove from top of prev stack|
|
||||
| push_count | varint | New frames to add at top |
|
||||
| new_frames | varint[] | Array of push_count frame indices |
|
||||
+-----------------+-----------+----------------------------------------+
|
||||
```
|
||||
Used when the code path changed: some frames were popped (function returns)
|
||||
and new frames were pushed (different function calls).
|
||||
|
||||
### Thread and Interpreter Identification
|
||||
|
||||
Thread IDs are 64-bit values that can be large (memory addresses on some
|
||||
platforms) and vary unpredictably. Using a fixed 8-byte encoding avoids
|
||||
the overhead of varint encoding for large values and simplifies parsing
|
||||
since the reader knows exactly where each field begins.
|
||||
|
||||
The interpreter ID identifies which Python sub-interpreter the thread
|
||||
belongs to, allowing analysis tools to separate activity across interpreters
|
||||
in processes using multiple sub-interpreters.
|
||||
|
||||
### Status Byte
|
||||
|
||||
The status byte is a bitfield encoding thread state at sample time:
|
||||
|
||||
| Bit | Flag | Meaning |
|
||||
|-----|-----------------------|--------------------------------------------|
|
||||
| 0 | THREAD_STATUS_HAS_GIL | Thread holds the GIL (Global Interpreter Lock) |
|
||||
| 1 | THREAD_STATUS_ON_CPU | Thread is actively running on a CPU core |
|
||||
| 2 | THREAD_STATUS_UNKNOWN | Thread state could not be determined |
|
||||
| 3 | THREAD_STATUS_GIL_REQUESTED | Thread is waiting to acquire the GIL |
|
||||
| 4 | THREAD_STATUS_HAS_EXCEPTION | Thread has a pending exception |
|
||||
|
||||
Multiple flags can be set simultaneously (e.g., a thread can hold the GIL
|
||||
while also running on CPU). Analysis tools use these to filter samples or
|
||||
visualize thread states over time.
|
||||
|
||||
### Timestamp Delta Encoding
|
||||
|
||||
Timestamps use delta encoding rather than absolute values. Absolute
|
||||
timestamps in microseconds require 8 bytes each, but consecutive samples
|
||||
from the same thread are typically separated by the sampling interval
|
||||
(e.g., 1000 microseconds), so the delta between them is small and fits
|
||||
in 1-2 varint bytes. The writer tracks the previous timestamp for each
|
||||
thread separately. The first sample from a thread encodes its delta from
|
||||
the profiling start time; subsequent samples encode the delta from that
|
||||
thread's previous sample. This per-thread tracking is necessary because
|
||||
samples are interleaved across threads in arrival order, not grouped by
|
||||
thread.
|
||||
|
||||
For REPEAT (RLE) records, timestamp deltas and status bytes are stored as
|
||||
interleaved pairs (delta, status, delta, status, ...) - one pair per
|
||||
repeated sample - allowing efficient batching while preserving the exact
|
||||
timing and state of each sample.
|
||||
|
||||
### Frame Indexing
|
||||
|
||||
Each frame in a call stack is represented by an index into the frame table
|
||||
rather than inline data. This provides massive space savings because call
|
||||
stacks are highly repetitive: the same function appears in many samples
|
||||
(hot functions), call stacks often share common prefixes (main -> app ->
|
||||
handler -> ...), and recursive functions create repeated frame sequences.
|
||||
A frame index is typically 1-2 varint bytes. Inline frame data would be
|
||||
20-200+ bytes (two strings plus a line number). For a profile with 100,000
|
||||
samples averaging 30 frames each, this reduces frame data from potentially
|
||||
gigabytes to tens of megabytes.
|
||||
|
||||
Frame indices are written innermost-first (the currently executing frame
|
||||
has index 0 in the array). This ordering works well with delta compression:
|
||||
function calls typically add frames at the top (index 0), while shared
|
||||
frames remain at the bottom.
|
||||
|
||||
## String Table
|
||||
|
||||
The string table stores deduplicated UTF-8 strings (filenames and function
|
||||
names). It begins at `string_table_offset` and contains entries in order of
|
||||
their assignment during writing:
|
||||
|
||||
```
|
||||
+----------------+
|
||||
| length: varint |
|
||||
| data: bytes |
|
||||
+----------------+ (repeated for each string)
|
||||
```
|
||||
|
||||
Strings are stored in the order they were first encountered during writing.
|
||||
The first unique filename gets index 0, the second gets index 1, and so on.
|
||||
Length-prefixing (rather than null-termination) allows strings containing
|
||||
null bytes and enables readers to allocate exact-sized buffers. The varint
|
||||
length encoding means short strings (under 128 bytes) need only one length
|
||||
byte.
|
||||
|
||||
## Frame Table
|
||||
|
||||
The frame table stores deduplicated frame entries:
|
||||
|
||||
```
|
||||
+----------------------+
|
||||
| filename_idx: varint |
|
||||
| funcname_idx: varint |
|
||||
| lineno: svarint |
|
||||
+----------------------+ (repeated for each frame)
|
||||
```
|
||||
|
||||
Each unique (filename, funcname, lineno) combination gets one entry. Two
|
||||
calls to the same function at different line numbers produce different
|
||||
frame entries; two calls at the same line number share one entry.
|
||||
|
||||
Strings and frames are deduplicated separately because they have different
|
||||
cardinalities and reference patterns. A codebase might have hundreds of
|
||||
unique source files but thousands of unique functions. Many functions share
|
||||
the same filename, so storing the filename index in each frame entry (rather
|
||||
than the full string) provides an additional layer of deduplication. A frame
|
||||
entry is just three varints (typically 3-6 bytes) rather than two full
|
||||
strings plus a line number.
|
||||
|
||||
Line numbers use signed varint (zigzag encoding) rather than unsigned to
|
||||
handle edge cases. Synthetic frames—generated frames that don't correspond
|
||||
directly to Python source code, such as C extension boundaries or internal
|
||||
interpreter frames—use line number 0 or -1 to indicate the absence of a
|
||||
source location. Zigzag encoding ensures these small negative values encode
|
||||
efficiently (−1 becomes 1, which is one byte) rather than requiring the
|
||||
maximum varint length.
|
||||
|
||||
## Footer
|
||||
|
||||
```
|
||||
Offset Size Type Description
|
||||
+--------+------+---------+----------------------------------------+
|
||||
| 0 | 4 | uint32 | String count |
|
||||
| 4 | 4 | uint32 | Frame count |
|
||||
| 8 | 8 | uint64 | Total file size |
|
||||
| 16 | 16 | bytes | Checksum (reserved, currently zeros) |
|
||||
+--------+------+---------+----------------------------------------+
|
||||
```
|
||||
|
||||
The string and frame counts allow readers to pre-allocate arrays of the
|
||||
correct size before parsing the tables. Without these counts, readers would
|
||||
need to either scan the tables twice (once to count, once to parse) or use
|
||||
dynamically-growing arrays.
|
||||
|
||||
The file size field provides a consistency check: if the actual file size
|
||||
does not match, the file may be truncated or corrupted.
|
||||
|
||||
The checksum field is reserved for future use. A checksum would allow
|
||||
detection of corruption but adds complexity and computation cost. The
|
||||
current implementation leaves this as zeros.
|
||||
|
||||
## Variable-Length Integer Encoding
|
||||
|
||||
The format uses LEB128 (Little Endian Base 128) for unsigned integers and
|
||||
zigzag + LEB128 for signed integers. These encodings are widely used
|
||||
(Protocol Buffers, DWARF debug info, WebAssembly) and well-understood.
|
||||
|
||||
### Unsigned Varint (LEB128)
|
||||
|
||||
Each byte stores 7 bits of data. The high bit indicates whether more bytes
|
||||
follow:
|
||||
|
||||
```
|
||||
Value Encoded bytes
|
||||
0-127 [0xxxxxxx] (1 byte)
|
||||
128-16383 [1xxxxxxx] [0xxxxxxx] (2 bytes)
|
||||
16384+ [1xxxxxxx] [1xxxxxxx] ... (3+ bytes)
|
||||
```
|
||||
|
||||
Most indices in profiling data are small. A profile with 1000 unique frames
|
||||
needs at most 2 bytes per frame index. The common case (indices under 128)
|
||||
needs only 1 byte.
|
||||
|
||||
### Signed Varint (Zigzag)
|
||||
|
||||
Standard LEB128 encodes −1 as a very large unsigned value, requiring many
|
||||
bytes. Zigzag encoding interleaves positive and negative values:
|
||||
|
||||
```
|
||||
0 -> 0 -1 -> 1 1 -> 2 -2 -> 3 2 -> 4
|
||||
```
|
||||
|
||||
This ensures small-magnitude values (whether positive or negative) encode
|
||||
in few bytes.
|
||||
|
||||
## Compression
|
||||
|
||||
When compression is enabled, the sample data region contains a zstd stream.
|
||||
The string table, frame table, and footer remain uncompressed so readers can
|
||||
access metadata without decompressing the entire file. A tool that only needs
|
||||
to report "this file contains 50,000 samples of 3 threads" can read the header
|
||||
and footer without touching the compressed sample data. This also simplifies
|
||||
the format: the header's offset fields point directly to the tables rather
|
||||
than to positions within a decompressed stream.
|
||||
|
||||
Zstd provides an excellent balance of compression ratio and speed. Profiling
|
||||
data compresses very well (often 5-10x) due to repetitive patterns: the same
|
||||
small set of frame indices appears repeatedly, and delta-encoded timestamps
|
||||
cluster around the sampling interval. Zstd's streaming API allows compression
|
||||
without buffering the entire dataset. The writer feeds sample data through
|
||||
the compressor incrementally, flushing compressed chunks to disk as they
|
||||
become available.
|
||||
|
||||
Level 5 compression is used as a default. Lower levels (1-3) are faster but
|
||||
compress less; higher levels (6+) compress more but slow down writing. Level
|
||||
5 provides good compression with minimal impact on profiling overhead.
|
||||
|
||||
## Reading and Writing
|
||||
|
||||
### Writing
|
||||
|
||||
1. Open the output file and write 64 zero bytes as a placeholder header
|
||||
2. Initialize empty string and frame dictionaries for deduplication
|
||||
3. For each sample:
|
||||
- Intern any new strings, assigning sequential indices
|
||||
- Intern any new frames, assigning sequential indices
|
||||
- Encode the sample record and write to the buffer
|
||||
- Flush the buffer through compression (if enabled) when full
|
||||
4. Flush remaining buffered data and finalize compression
|
||||
5. Write the string table (length-prefixed strings in index order)
|
||||
6. Write the frame table (varint-encoded entries in index order)
|
||||
7. Write the footer with final counts
|
||||
8. Seek to offset 0 and write the header with actual values
|
||||
|
||||
The writer maintains two dictionaries: one mapping strings to indices, one
|
||||
mapping (filename_idx, funcname_idx, lineno) tuples to frame indices. These
|
||||
enable O(1) lookup during interning.
|
||||
|
||||
### Reading
|
||||
|
||||
1. Read the header magic number to detect endianness (set `needs_swap` flag
|
||||
if the magic appears byte-swapped)
|
||||
2. Validate version and read remaining header fields (byte-swapping if needed)
|
||||
3. Seek to end − 32 and read the footer (byte-swapping counts if needed)
|
||||
4. Allocate string array of `string_count` elements
|
||||
5. Parse the string table, populating the array
|
||||
6. Allocate frame array of `frame_count * 3` uint32 elements
|
||||
7. Parse the frame table, populating the array
|
||||
8. If compressed, decompress the sample data region
|
||||
9. Iterate through samples, resolving indices to strings/frames
|
||||
(byte-swapping thread_id and interpreter_id if needed)
|
||||
|
||||
The reader builds lookup arrays rather than dictionaries since it only needs
|
||||
index-to-value mapping, not value-to-index.
|
||||
|
||||
## Platform Considerations
|
||||
|
||||
### Byte Ordering and Cross-Platform Portability
|
||||
|
||||
The binary format uses **native byte order** for all multi-byte integer
|
||||
fields when writing. However, the reader supports **cross-endian reading**:
|
||||
files written on a little-endian system (x86, ARM) can be read on a
|
||||
big-endian system (s390x, PowerPC), and vice versa.
|
||||
|
||||
The magic number doubles as an endianness marker. When read on a system with
|
||||
different byte order, it appears byte-swapped (`0x48434154` instead of
|
||||
`0x54414348`). The reader detects this and automatically byte-swaps all
|
||||
fixed-width integer fields during parsing.
|
||||
|
||||
Writers must use `memcpy()` from properly-sized integer types when writing
|
||||
fixed-width integer fields. When the source variable's type differs from the
|
||||
field width (e.g., `size_t` written as 4 bytes), explicit casting to the
|
||||
correct type (e.g., `uint32_t`) is required before `memcpy()`. On big-endian
|
||||
systems, copying from an oversized type would copy the wrong bytes—high-order
|
||||
zeros instead of the actual value.
|
||||
|
||||
The reader tracks whether byte-swapping is needed via a `needs_swap` flag set
|
||||
during header parsing. All fixed-width fields in the header, footer, and
|
||||
sample data are conditionally byte-swapped using Python's internal byte-swap
|
||||
functions (`_Py_bswap32`, `_Py_bswap64` from `pycore_bitutils.h`).
|
||||
|
||||
Variable-length integers (varints) are byte-order independent since they
|
||||
encode values one byte at a time using the LEB128 scheme, so they require
|
||||
no special handling for cross-endian reading.
|
||||
|
||||
### Memory-Mapped I/O
|
||||
|
||||
On Unix systems (Linux, macOS), the reader uses `mmap()` to map the file
|
||||
into the process address space. The kernel handles paging data in and out
|
||||
as needed, no explicit read() calls or buffer management are required,
|
||||
multiple readers can share the same physical pages, and sequential access
|
||||
patterns benefit from kernel read-ahead.
|
||||
|
||||
The implementation uses `madvise()` to hint the access pattern to the kernel:
|
||||
`MADV_SEQUENTIAL` indicates the file will be read linearly, enabling
|
||||
aggressive read-ahead. `MADV_WILLNEED` requests pre-faulting of pages.
|
||||
On Linux, `MAP_POPULATE` pre-faults all pages at mmap time rather than on
|
||||
first access, moving page fault overhead from the parsing loop to the
|
||||
initial mapping for more predictable performance. For large files (over
|
||||
32 MB), `MADV_HUGEPAGE` requests transparent huge pages (2 MB instead of
|
||||
4 KB) to reduce TLB pressure when accessing large amounts of data.
|
||||
|
||||
On Windows, the implementation falls back to standard file I/O with full
|
||||
file buffering. Profiling data files are typically small enough (tens to
|
||||
hundreds of megabytes) that this is acceptable.
|
||||
|
||||
The writer uses a 512 KB buffer to batch small writes. Each sample record
|
||||
is typically tens of bytes; writing these individually would incur excessive
|
||||
syscall overhead. The buffer accumulates data until full, then flushes in
|
||||
one write() call (or feeds through the compression stream).
|
||||
|
||||
## Future Considerations
|
||||
|
||||
The format reserves space for future extensions. The 12 reserved bytes in
|
||||
the header could hold additional metadata. The 16-byte checksum field in
|
||||
the footer is currently unused. The version field allows incompatible
|
||||
changes with graceful rejection. New compression types could be added
|
||||
(compression_type > 1).
|
||||
|
||||
Any changes that alter the meaning of existing fields or the parsing logic
|
||||
should increment the version number to prevent older readers from
|
||||
misinterpreting new files.
|
||||
|
|
@ -86,14 +86,15 @@ class REPLThread(threading.Thread):
|
|||
global return_code
|
||||
|
||||
try:
|
||||
banner = (
|
||||
f'asyncio REPL {sys.version} on {sys.platform}\n'
|
||||
f'Use "await" directly instead of "asyncio.run()".\n'
|
||||
f'Type "help", "copyright", "credits" or "license" '
|
||||
f'for more information.\n'
|
||||
)
|
||||
if not sys.flags.quiet:
|
||||
banner = (
|
||||
f'asyncio REPL {sys.version} on {sys.platform}\n'
|
||||
f'Use "await" directly instead of "asyncio.run()".\n'
|
||||
f'Type "help", "copyright", "credits" or "license" '
|
||||
f'for more information.\n'
|
||||
)
|
||||
|
||||
console.write(banner)
|
||||
console.write(banner)
|
||||
|
||||
if startup_path := os.getenv("PYTHONSTARTUP"):
|
||||
sys.audit("cpython.run_startup", startup_path)
|
||||
|
|
@ -240,4 +241,5 @@ if __name__ == '__main__':
|
|||
break
|
||||
|
||||
console.write('exiting asyncio REPL...\n')
|
||||
loop.close()
|
||||
sys.exit(return_code)
|
||||
|
|
|
|||
|
|
@ -794,7 +794,8 @@ class RawConfigParser(MutableMapping):
|
|||
"""
|
||||
elements_added = set()
|
||||
for section, keys in dictionary.items():
|
||||
section = str(section)
|
||||
if section is not UNNAMED_SECTION:
|
||||
section = str(section)
|
||||
try:
|
||||
self.add_section(section)
|
||||
except (DuplicateSectionError, ValueError):
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ def format_string(f, val, grouping=False, monetary=False):
|
|||
|
||||
Grouping is applied if the third parameter is true.
|
||||
Conversion uses monetary thousands separator and grouping strings if
|
||||
forth parameter monetary is true."""
|
||||
fourth parameter monetary is true."""
|
||||
global _percent_re
|
||||
if _percent_re is None:
|
||||
import re
|
||||
|
|
|
|||
|
|
@ -2181,11 +2181,7 @@ def _unlock_file(f):
|
|||
|
||||
def _create_carefully(path):
|
||||
"""Create a file if it doesn't exist and open for reading and writing."""
|
||||
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0o666)
|
||||
try:
|
||||
return open(path, 'rb+')
|
||||
finally:
|
||||
os.close(fd)
|
||||
return open(path, 'xb+')
|
||||
|
||||
def _create_temporary(path):
|
||||
"""Create a temp file based on path and open for reading and writing."""
|
||||
|
|
|
|||
28
Lib/pdb.py
28
Lib/pdb.py
|
|
@ -391,17 +391,22 @@ class Pdb(bdb.Bdb, cmd.Cmd):
|
|||
# Read ~/.pdbrc and ./.pdbrc
|
||||
self.rcLines = []
|
||||
if readrc:
|
||||
home_rcfile = os.path.expanduser("~/.pdbrc")
|
||||
local_rcfile = os.path.abspath(".pdbrc")
|
||||
|
||||
try:
|
||||
with open(os.path.expanduser('~/.pdbrc'), encoding='utf-8') as rcFile:
|
||||
self.rcLines.extend(rcFile)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
with open(".pdbrc", encoding='utf-8') as rcFile:
|
||||
self.rcLines.extend(rcFile)
|
||||
with open(home_rcfile, encoding='utf-8') as rcfile:
|
||||
self.rcLines.extend(rcfile)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if local_rcfile != home_rcfile:
|
||||
try:
|
||||
with open(local_rcfile, encoding='utf-8') as rcfile:
|
||||
self.rcLines.extend(rcfile)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self.commands = {} # associates a command list to breakpoint numbers
|
||||
self.commands_defining = False # True while in the process of defining
|
||||
# a command list
|
||||
|
|
@ -1315,7 +1320,14 @@ class Pdb(bdb.Bdb, cmd.Cmd):
|
|||
reached.
|
||||
"""
|
||||
if not arg:
|
||||
bnum = len(bdb.Breakpoint.bpbynumber) - 1
|
||||
for bp in reversed(bdb.Breakpoint.bpbynumber):
|
||||
if bp is None:
|
||||
continue
|
||||
bnum = bp.number
|
||||
break
|
||||
else:
|
||||
self.error('cannot set commands: no existing breakpoint')
|
||||
return
|
||||
else:
|
||||
try:
|
||||
bnum = int(arg)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ system restrictions or missing privileges.
|
|||
"""
|
||||
|
||||
from .cli import main
|
||||
from .errors import SamplingUnknownProcessError, SamplingModuleNotFoundError, SamplingScriptNotFoundError
|
||||
|
||||
def handle_permission_error():
|
||||
"""Handle PermissionError by displaying appropriate error message."""
|
||||
|
|
@ -64,3 +65,9 @@ if __name__ == '__main__':
|
|||
main()
|
||||
except PermissionError:
|
||||
handle_permission_error()
|
||||
except SamplingUnknownProcessError as err:
|
||||
print(f"Tachyon cannot find the process: {err}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except (SamplingModuleNotFoundError, SamplingScriptNotFoundError) as err:
|
||||
print(f"Tachyon cannot find the target: {err}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
|
|
|||
5
Lib/profiling/sampling/_format_utils.py
Normal file
5
Lib/profiling/sampling/_format_utils.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import locale
|
||||
|
||||
|
||||
def fmt(value: int | float, decimals: int = 1) -> str:
|
||||
return locale.format_string(f'%.{decimals}f', value, grouping=True)
|
||||
|
|
@ -5,6 +5,18 @@
|
|||
This file extends the shared foundation with heatmap-specific styles.
|
||||
========================================================================== */
|
||||
|
||||
/* Heatmap heat colors - using base.css colors with 60% opacity */
|
||||
[data-theme="dark"] {
|
||||
--heat-1: rgba(90, 123, 167, 0.60);
|
||||
--heat-2: rgba(106, 148, 168, 0.60);
|
||||
--heat-3: rgba(122, 172, 172, 0.60);
|
||||
--heat-4: rgba(142, 196, 152, 0.60);
|
||||
--heat-5: rgba(168, 216, 136, 0.60);
|
||||
--heat-6: rgba(200, 222, 122, 0.60);
|
||||
--heat-7: rgba(244, 212, 93, 0.60);
|
||||
--heat-8: rgba(255, 122, 69, 0.60);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
Layout Overrides (Heatmap-specific)
|
||||
-------------------------------------------------------------------------- */
|
||||
|
|
@ -1129,6 +1141,10 @@
|
|||
.line-samples-cumulative {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.bytecode-panel {
|
||||
margin: 8px 10px 8px 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.bytecode-toggle {
|
||||
|
|
@ -1160,13 +1176,77 @@
|
|||
}
|
||||
|
||||
.bytecode-panel {
|
||||
margin-left: 90px;
|
||||
padding: 8px 15px;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 3px solid var(--accent);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-md);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
padding: 0;
|
||||
margin: 8px 10px 8px 250px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 500px;
|
||||
flex: 1;
|
||||
transition: padding 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.bytecode-panel.expanded {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.bytecode-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: visible;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.bytecode-wrapper.expanded {
|
||||
max-height: 600px;
|
||||
opacity: 1;
|
||||
transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
/* Column backdrop matching table header columns (line/self/total) */
|
||||
.bytecode-columns {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.bytecode-wrapper.expanded .bytecode-columns {
|
||||
display: flex;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.bytecode-panel::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.bytecode-panel::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.bytecode-panel::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.bytecode-panel::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Specialization summary bar */
|
||||
|
|
|
|||
|
|
@ -15,37 +15,13 @@ let coldCodeHidden = false;
|
|||
// ============================================================================
|
||||
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const current = html.getAttribute('data-theme') || 'light';
|
||||
const next = current === 'light' ? 'dark' : 'light';
|
||||
html.setAttribute('data-theme', next);
|
||||
localStorage.setItem('heatmap-theme', next);
|
||||
|
||||
// Update theme button icon
|
||||
const btn = document.getElementById('theme-btn');
|
||||
if (btn) {
|
||||
btn.querySelector('.icon-moon').style.display = next === 'dark' ? 'none' : '';
|
||||
btn.querySelector('.icon-sun').style.display = next === 'dark' ? '' : 'none';
|
||||
}
|
||||
toggleAndSaveTheme();
|
||||
applyLineColors();
|
||||
|
||||
// Rebuild scroll marker with new theme colors
|
||||
buildScrollMarker();
|
||||
}
|
||||
|
||||
function restoreUIState() {
|
||||
// Restore theme
|
||||
const savedTheme = localStorage.getItem('heatmap-theme');
|
||||
if (savedTheme) {
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
const btn = document.getElementById('theme-btn');
|
||||
if (btn) {
|
||||
btn.querySelector('.icon-moon').style.display = savedTheme === 'dark' ? 'none' : '';
|
||||
btn.querySelector('.icon-sun').style.display = savedTheme === 'dark' ? '' : 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Functions
|
||||
// ============================================================================
|
||||
|
|
@ -542,20 +518,23 @@ function toggleBytecode(button) {
|
|||
const lineId = lineDiv.id;
|
||||
const lineNum = lineId.replace('line-', '');
|
||||
const panel = document.getElementById(`bytecode-${lineNum}`);
|
||||
const wrapper = document.getElementById(`bytecode-wrapper-${lineNum}`);
|
||||
|
||||
if (!panel) return;
|
||||
if (!panel || !wrapper) return;
|
||||
|
||||
const isExpanded = panel.style.display !== 'none';
|
||||
const isExpanded = panel.classList.contains('expanded');
|
||||
|
||||
if (isExpanded) {
|
||||
panel.style.display = 'none';
|
||||
panel.classList.remove('expanded');
|
||||
wrapper.classList.remove('expanded');
|
||||
button.classList.remove('expanded');
|
||||
button.innerHTML = '▶'; // Right arrow
|
||||
} else {
|
||||
if (!panel.dataset.populated) {
|
||||
populateBytecodePanel(panel, button);
|
||||
}
|
||||
panel.style.display = 'block';
|
||||
panel.classList.add('expanded');
|
||||
wrapper.classList.add('expanded');
|
||||
button.classList.add('expanded');
|
||||
button.innerHTML = '▼'; // Down arrow
|
||||
}
|
||||
|
|
@ -598,10 +577,12 @@ function populateBytecodePanel(panel, button) {
|
|||
else if (specPct >= 33) specClass = 'medium';
|
||||
|
||||
// Build specialization summary
|
||||
const instruction_word = instructions.length === 1 ? 'instruction' : 'instructions';
|
||||
const sample_word = totalSamples === 1 ? 'sample' : 'samples';
|
||||
let html = `<div class="bytecode-spec-summary ${specClass}">
|
||||
<span class="spec-pct">${specPct}%</span>
|
||||
<span class="spec-label">specialized</span>
|
||||
<span class="spec-detail">(${specializedCount}/${instructions.length} instructions, ${specializedSamples.toLocaleString()}/${totalSamples.toLocaleString()} samples)</span>
|
||||
<span class="spec-detail">(${specializedCount}/${instructions.length} ${instruction_word}, ${specializedSamples.toLocaleString()}/${totalSamples.toLocaleString()} ${sample_word})</span>
|
||||
</div>`;
|
||||
|
||||
html += '<div class="bytecode-header">' +
|
||||
|
|
|
|||
|
|
@ -19,35 +19,10 @@ function applyHeatmapBarColors() {
|
|||
// ============================================================================
|
||||
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const current = html.getAttribute('data-theme') || 'light';
|
||||
const next = current === 'light' ? 'dark' : 'light';
|
||||
html.setAttribute('data-theme', next);
|
||||
localStorage.setItem('heatmap-theme', next);
|
||||
|
||||
// Update theme button icon
|
||||
const btn = document.getElementById('theme-btn');
|
||||
if (btn) {
|
||||
btn.querySelector('.icon-moon').style.display = next === 'dark' ? 'none' : '';
|
||||
btn.querySelector('.icon-sun').style.display = next === 'dark' ? '' : 'none';
|
||||
}
|
||||
|
||||
toggleAndSaveTheme();
|
||||
applyHeatmapBarColors();
|
||||
}
|
||||
|
||||
function restoreUIState() {
|
||||
// Restore theme
|
||||
const savedTheme = localStorage.getItem('heatmap-theme');
|
||||
if (savedTheme) {
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
const btn = document.getElementById('theme-btn');
|
||||
if (btn) {
|
||||
btn.querySelector('.icon-moon').style.display = savedTheme === 'dark' ? 'none' : '';
|
||||
btn.querySelector('.icon-sun').style.display = savedTheme === 'dark' ? '' : 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Type Section Toggle (stdlib, project, etc)
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
|
|
|||
|
|
@ -39,6 +39,42 @@ function intensityToColor(intensity) {
|
|||
return rootStyle.getPropertyValue(`--heat-${level}`).trim();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Theme Support
|
||||
// ============================================================================
|
||||
|
||||
// Get the preferred theme from localStorage or browser preference
|
||||
function getPreferredTheme() {
|
||||
const saved = localStorage.getItem('heatmap-theme');
|
||||
if (saved) return saved;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
// Apply theme and update UI. Returns the applied theme.
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
const btn = document.getElementById('theme-btn');
|
||||
if (btn) {
|
||||
btn.querySelector('.icon-moon').style.display = theme === 'dark' ? 'none' : '';
|
||||
btn.querySelector('.icon-sun').style.display = theme === 'dark' ? '' : 'none';
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
// Toggle theme and save preference. Returns the new theme.
|
||||
function toggleAndSaveTheme() {
|
||||
const current = document.documentElement.getAttribute('data-theme') || 'light';
|
||||
const next = current === 'light' ? 'dark' : 'light';
|
||||
applyTheme(next);
|
||||
localStorage.setItem('heatmap-theme', next);
|
||||
return next;
|
||||
}
|
||||
|
||||
// Restore theme from localStorage, or use browser preference
|
||||
function restoreUIState() {
|
||||
applyTheme(getPreferredTheme());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Favicon (Reuse logo image as favicon)
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -124,15 +124,15 @@
|
|||
|
||||
--header-gradient: linear-gradient(135deg, #21262d 0%, #30363d 100%);
|
||||
|
||||
/* Dark mode heat palette - muted colors that provide sufficient contrast with light text */
|
||||
--heat-1: rgba(74, 123, 167, 0.35);
|
||||
--heat-2: rgba(90, 159, 168, 0.38);
|
||||
--heat-3: rgba(106, 181, 181, 0.40);
|
||||
--heat-4: rgba(126, 196, 136, 0.42);
|
||||
--heat-5: rgba(160, 216, 120, 0.45);
|
||||
--heat-6: rgba(196, 222, 106, 0.48);
|
||||
--heat-7: rgba(244, 212, 77, 0.50);
|
||||
--heat-8: rgba(255, 107, 53, 0.55);
|
||||
/* Dark mode heat palette - cool to warm gradient for visualization */
|
||||
--heat-1: rgba(90, 123, 167, 1);
|
||||
--heat-2: rgba(106, 148, 168, 1);
|
||||
--heat-3: rgba(122, 172, 172, 1);
|
||||
--heat-4: rgba(142, 196, 152, 1);
|
||||
--heat-5: rgba(168, 216, 136, 1);
|
||||
--heat-6: rgba(200, 222, 122, 1);
|
||||
--heat-7: rgba(244, 212, 93, 1);
|
||||
--heat-8: rgba(255, 122, 69, 1);
|
||||
|
||||
/* Code view specific - dark mode */
|
||||
--code-bg: #0d1117;
|
||||
|
|
|
|||
120
Lib/profiling/sampling/binary_collector.py
Normal file
120
Lib/profiling/sampling/binary_collector.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"""Thin Python wrapper around C binary writer for profiling data."""
|
||||
|
||||
import time
|
||||
|
||||
import _remote_debugging
|
||||
|
||||
from .collector import Collector
|
||||
|
||||
# Compression type constants (must match binary_io.h)
|
||||
COMPRESSION_NONE = 0
|
||||
COMPRESSION_ZSTD = 1
|
||||
|
||||
|
||||
def _resolve_compression(compression):
|
||||
"""Resolve compression type from string or int.
|
||||
|
||||
Args:
|
||||
compression: 'auto', 'zstd', 'none', or int (0/1)
|
||||
|
||||
Returns:
|
||||
int: Compression type constant
|
||||
"""
|
||||
if isinstance(compression, int):
|
||||
return compression
|
||||
|
||||
compression = compression.lower()
|
||||
if compression == 'none':
|
||||
return COMPRESSION_NONE
|
||||
elif compression == 'zstd':
|
||||
return COMPRESSION_ZSTD
|
||||
elif compression == 'auto':
|
||||
# Auto: use zstd if available, otherwise none
|
||||
if _remote_debugging.zstd_available():
|
||||
return COMPRESSION_ZSTD
|
||||
return COMPRESSION_NONE
|
||||
else:
|
||||
raise ValueError(f"Unknown compression type: {compression}")
|
||||
|
||||
|
||||
class BinaryCollector(Collector):
|
||||
"""High-performance binary collector using C implementation.
|
||||
|
||||
This collector writes profiling data directly to a binary file format
|
||||
with optional zstd compression. All I/O is performed in C for maximum
|
||||
throughput.
|
||||
|
||||
The binary format uses string/frame deduplication and varint encoding
|
||||
for efficient storage.
|
||||
"""
|
||||
|
||||
def __init__(self, filename, sample_interval_usec, *, skip_idle=False,
|
||||
compression='auto'):
|
||||
"""Create a new binary collector.
|
||||
|
||||
Args:
|
||||
filename: Path to output binary file
|
||||
sample_interval_usec: Sampling interval in microseconds
|
||||
skip_idle: If True, skip idle threads (not used in binary format)
|
||||
compression: 'auto', 'zstd', 'none', or int (0=none, 1=zstd)
|
||||
"""
|
||||
self.filename = filename
|
||||
self.sample_interval_usec = sample_interval_usec
|
||||
self.skip_idle = skip_idle
|
||||
|
||||
compression_type = _resolve_compression(compression)
|
||||
start_time_us = int(time.monotonic() * 1_000_000)
|
||||
self._writer = _remote_debugging.BinaryWriter(
|
||||
filename, sample_interval_usec, start_time_us, compression=compression_type
|
||||
)
|
||||
|
||||
def collect(self, stack_frames, timestamp_us=None):
|
||||
"""Collect profiling data from stack frames.
|
||||
|
||||
This passes stack_frames directly to the C writer which handles
|
||||
all encoding and buffering.
|
||||
|
||||
Args:
|
||||
stack_frames: List of InterpreterInfo objects from _remote_debugging
|
||||
timestamp_us: Optional timestamp in microseconds. If not provided,
|
||||
uses time.monotonic() to generate one.
|
||||
"""
|
||||
if timestamp_us is None:
|
||||
timestamp_us = int(time.monotonic() * 1_000_000)
|
||||
self._writer.write_sample(stack_frames, timestamp_us)
|
||||
|
||||
def collect_failed_sample(self):
|
||||
"""Record a failed sample attempt (no-op for binary format)."""
|
||||
pass
|
||||
|
||||
def export(self, filename=None):
|
||||
"""Finalize and close the binary file.
|
||||
|
||||
Args:
|
||||
filename: Ignored (binary files are written incrementally)
|
||||
"""
|
||||
self._writer.finalize()
|
||||
|
||||
@property
|
||||
def total_samples(self):
|
||||
return self._writer.total_samples
|
||||
|
||||
def get_stats(self):
|
||||
"""Get encoding statistics.
|
||||
|
||||
Returns:
|
||||
Dict with encoding statistics including repeat/full/suffix/pop-push
|
||||
record counts, frames written/saved, and compression ratio.
|
||||
"""
|
||||
return self._writer.get_stats()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Context manager exit - finalize unless there was an error."""
|
||||
if exc_type is None:
|
||||
self._writer.finalize()
|
||||
else:
|
||||
self._writer.close()
|
||||
return False
|
||||
128
Lib/profiling/sampling/binary_reader.py
Normal file
128
Lib/profiling/sampling/binary_reader.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
"""Thin Python wrapper around C binary reader for profiling data."""
|
||||
|
||||
|
||||
class BinaryReader:
|
||||
"""High-performance binary reader using C implementation.
|
||||
|
||||
This reader uses memory-mapped I/O (on Unix) for fast replay of
|
||||
profiling data from binary files.
|
||||
|
||||
Use as a context manager:
|
||||
with BinaryReader('profile.bin') as reader:
|
||||
info = reader.get_info()
|
||||
reader.replay_samples(collector, progress_callback)
|
||||
"""
|
||||
|
||||
def __init__(self, filename):
|
||||
"""Create a new binary reader.
|
||||
|
||||
Args:
|
||||
filename: Path to input binary file
|
||||
"""
|
||||
self.filename = filename
|
||||
self._reader = None
|
||||
|
||||
def __enter__(self):
|
||||
import _remote_debugging
|
||||
self._reader = _remote_debugging.BinaryReader(self.filename)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self._reader is not None:
|
||||
self._reader.close()
|
||||
self._reader = None
|
||||
return False
|
||||
|
||||
def get_info(self):
|
||||
"""Get metadata about the binary file.
|
||||
|
||||
Returns:
|
||||
dict: File metadata including:
|
||||
- sample_count: Number of samples in the file
|
||||
- sample_interval_us: Sampling interval in microseconds
|
||||
- start_time_us: Start timestamp in microseconds
|
||||
- string_count: Number of unique strings
|
||||
- frame_count: Number of unique frames
|
||||
- compression: Compression type used
|
||||
"""
|
||||
if self._reader is None:
|
||||
raise RuntimeError("Reader not open. Use as context manager.")
|
||||
return self._reader.get_info()
|
||||
|
||||
def replay_samples(self, collector, progress_callback=None):
|
||||
"""Replay samples from binary file through a collector.
|
||||
|
||||
This allows converting binary profiling data to other formats
|
||||
(e.g., flamegraph, pstats) by replaying through the appropriate
|
||||
collector.
|
||||
|
||||
Args:
|
||||
collector: A Collector instance with a collect() method
|
||||
progress_callback: Optional callable(current, total) for progress
|
||||
|
||||
Returns:
|
||||
int: Number of samples replayed
|
||||
"""
|
||||
if self._reader is None:
|
||||
raise RuntimeError("Reader not open. Use as context manager.")
|
||||
return self._reader.replay(collector, progress_callback)
|
||||
|
||||
@property
|
||||
def sample_count(self):
|
||||
if self._reader is None:
|
||||
raise RuntimeError("Reader not open. Use as context manager.")
|
||||
return self._reader.get_info()['sample_count']
|
||||
|
||||
def get_stats(self):
|
||||
"""Get reconstruction statistics from replay.
|
||||
|
||||
Returns:
|
||||
dict: Statistics about record types decoded and samples
|
||||
reconstructed during replay.
|
||||
"""
|
||||
if self._reader is None:
|
||||
raise RuntimeError("Reader not open. Use as context manager.")
|
||||
return self._reader.get_stats()
|
||||
|
||||
|
||||
def convert_binary_to_format(input_file, output_file, output_format,
|
||||
sample_interval_usec=None, progress_callback=None):
|
||||
"""Convert a binary profiling file to another format.
|
||||
|
||||
Args:
|
||||
input_file: Path to input binary file
|
||||
output_file: Path to output file
|
||||
output_format: Target format ('flamegraph', 'collapsed', 'pstats', etc.)
|
||||
sample_interval_usec: Override sample interval (uses file's if None)
|
||||
progress_callback: Optional callable(current, total) for progress
|
||||
|
||||
Returns:
|
||||
int: Number of samples converted
|
||||
"""
|
||||
from .gecko_collector import GeckoCollector
|
||||
from .stack_collector import FlamegraphCollector, CollapsedStackCollector
|
||||
from .pstats_collector import PStatsCollector
|
||||
|
||||
with BinaryReader(input_file) as reader:
|
||||
info = reader.get_info()
|
||||
interval = sample_interval_usec or info['sample_interval_us']
|
||||
|
||||
# Create appropriate collector based on format
|
||||
if output_format == 'flamegraph':
|
||||
collector = FlamegraphCollector(interval)
|
||||
elif output_format == 'collapsed':
|
||||
collector = CollapsedStackCollector(interval)
|
||||
elif output_format == 'pstats':
|
||||
collector = PStatsCollector(interval)
|
||||
elif output_format == 'gecko':
|
||||
collector = GeckoCollector(interval)
|
||||
else:
|
||||
raise ValueError(f"Unknown output format: {output_format}")
|
||||
|
||||
# Replay samples through collector
|
||||
count = reader.replay_samples(collector, progress_callback)
|
||||
|
||||
# Export to target format
|
||||
collector.export(output_file)
|
||||
|
||||
return count
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import locale
|
||||
import os
|
||||
import selectors
|
||||
import socket
|
||||
|
|
@ -10,11 +11,14 @@ import sys
|
|||
import time
|
||||
from contextlib import nullcontext
|
||||
|
||||
from .sample import sample, sample_live
|
||||
from .errors import SamplingUnknownProcessError, SamplingModuleNotFoundError, SamplingScriptNotFoundError
|
||||
from .sample import sample, sample_live, _is_process_running
|
||||
from .pstats_collector import PstatsCollector
|
||||
from .stack_collector import CollapsedStackCollector, FlamegraphCollector
|
||||
from .heatmap_collector import HeatmapCollector
|
||||
from .gecko_collector import GeckoCollector
|
||||
from .binary_collector import BinaryCollector
|
||||
from .binary_reader import BinaryReader
|
||||
from .constants import (
|
||||
PROFILING_MODE_ALL,
|
||||
PROFILING_MODE_WALL,
|
||||
|
|
@ -74,6 +78,7 @@ FORMAT_EXTENSIONS = {
|
|||
"flamegraph": "html",
|
||||
"gecko": "json",
|
||||
"heatmap": "html",
|
||||
"binary": "bin",
|
||||
}
|
||||
|
||||
COLLECTOR_MAP = {
|
||||
|
|
@ -82,6 +87,7 @@ COLLECTOR_MAP = {
|
|||
"flamegraph": FlamegraphCollector,
|
||||
"gecko": GeckoCollector,
|
||||
"heatmap": HeatmapCollector,
|
||||
"binary": BinaryCollector,
|
||||
}
|
||||
|
||||
def _setup_child_monitor(args, parent_pid):
|
||||
|
|
@ -179,7 +185,7 @@ def _parse_mode(mode_string):
|
|||
def _check_process_died(process):
|
||||
"""Check if process died and raise an error with stderr if available."""
|
||||
if process.poll() is None:
|
||||
return # Process still running
|
||||
return
|
||||
|
||||
# Process died - try to get stderr for error message
|
||||
stderr_msg = ""
|
||||
|
|
@ -364,7 +370,7 @@ def _add_mode_options(parser):
|
|||
)
|
||||
|
||||
|
||||
def _add_format_options(parser):
|
||||
def _add_format_options(parser, include_compression=True, include_binary=True):
|
||||
"""Add output format options to a parser."""
|
||||
output_group = parser.add_argument_group("Output options")
|
||||
format_group = output_group.add_mutually_exclusive_group()
|
||||
|
|
@ -403,8 +409,24 @@ def _add_format_options(parser):
|
|||
dest="format",
|
||||
help="Generate interactive HTML heatmap visualization with line-level sample counts",
|
||||
)
|
||||
if include_binary:
|
||||
format_group.add_argument(
|
||||
"--binary",
|
||||
action="store_const",
|
||||
const="binary",
|
||||
dest="format",
|
||||
help="Generate high-performance binary format (use 'replay' command to convert)",
|
||||
)
|
||||
parser.set_defaults(format="pstats")
|
||||
|
||||
if include_compression:
|
||||
output_group.add_argument(
|
||||
"--compression",
|
||||
choices=["auto", "zstd", "none"],
|
||||
default="auto",
|
||||
help="Compression for binary format: auto (use zstd if available), zstd, none",
|
||||
)
|
||||
|
||||
output_group.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
|
|
@ -459,15 +481,18 @@ def _sort_to_mode(sort_choice):
|
|||
return sort_map.get(sort_choice, SORT_MODE_NSAMPLES)
|
||||
|
||||
|
||||
def _create_collector(format_type, interval, skip_idle, opcodes=False):
|
||||
def _create_collector(format_type, interval, skip_idle, opcodes=False,
|
||||
output_file=None, compression='auto'):
|
||||
"""Create the appropriate collector based on format type.
|
||||
|
||||
Args:
|
||||
format_type: The output format ('pstats', 'collapsed', 'flamegraph', 'gecko', 'heatmap')
|
||||
format_type: The output format ('pstats', 'collapsed', 'flamegraph', 'gecko', 'heatmap', 'binary')
|
||||
interval: Sampling interval in microseconds
|
||||
skip_idle: Whether to skip idle samples
|
||||
opcodes: Whether to collect opcode information (only used by gecko format
|
||||
for creating interval markers in Firefox Profiler)
|
||||
output_file: Output file path (required for binary format)
|
||||
compression: Compression type for binary format ('auto', 'zstd', 'none')
|
||||
|
||||
Returns:
|
||||
A collector instance of the appropriate type
|
||||
|
|
@ -476,6 +501,13 @@ def _create_collector(format_type, interval, skip_idle, opcodes=False):
|
|||
if collector_class is None:
|
||||
raise ValueError(f"Unknown format: {format_type}")
|
||||
|
||||
# Binary format requires output file and compression
|
||||
if format_type == "binary":
|
||||
if output_file is None:
|
||||
raise ValueError("Binary format requires an output file")
|
||||
return collector_class(output_file, interval, skip_idle=skip_idle,
|
||||
compression=compression)
|
||||
|
||||
# Gecko format never skips idle (it needs both GIL and CPU data)
|
||||
# and is the only format that uses opcodes for interval markers
|
||||
if format_type == "gecko":
|
||||
|
|
@ -511,7 +543,12 @@ def _handle_output(collector, args, pid, mode):
|
|||
pid: Process ID (for generating filenames)
|
||||
mode: Profiling mode used
|
||||
"""
|
||||
if args.format == "pstats":
|
||||
if args.format == "binary":
|
||||
# Binary format already wrote to file incrementally, just finalize
|
||||
collector.export(None)
|
||||
filename = collector.filename
|
||||
print(f"Binary profile written to {filename} ({collector.total_samples} samples)")
|
||||
elif args.format == "pstats":
|
||||
if args.outfile:
|
||||
# If outfile is a directory, generate filename inside it
|
||||
if os.path.isdir(args.outfile):
|
||||
|
|
@ -544,6 +581,10 @@ def _validate_args(args, parser):
|
|||
args: Parsed command-line arguments
|
||||
parser: ArgumentParser instance for error reporting
|
||||
"""
|
||||
# Replay command has no special validation needed
|
||||
if getattr(args, 'command', None) == "replay":
|
||||
return
|
||||
|
||||
# Check if live mode is available
|
||||
if hasattr(args, 'live') and args.live and LiveStatsCollector is None:
|
||||
parser.error(
|
||||
|
|
@ -556,7 +597,7 @@ def _validate_args(args, parser):
|
|||
parser.error("--subprocesses is incompatible with --live mode.")
|
||||
|
||||
# Async-aware mode is incompatible with --native, --no-gc, --mode, and --all-threads
|
||||
if args.async_aware:
|
||||
if getattr(args, 'async_aware', False):
|
||||
issues = []
|
||||
if args.native:
|
||||
issues.append("--native")
|
||||
|
|
@ -573,7 +614,7 @@ def _validate_args(args, parser):
|
|||
)
|
||||
|
||||
# --async-mode requires --async-aware
|
||||
if hasattr(args, 'async_mode') and args.async_mode != "running" and not args.async_aware:
|
||||
if hasattr(args, 'async_mode') and args.async_mode != "running" and not getattr(args, 'async_aware', False):
|
||||
parser.error("--async-mode requires --async-aware to be enabled.")
|
||||
|
||||
# Live mode is incompatible with format options
|
||||
|
|
@ -601,7 +642,7 @@ def _validate_args(args, parser):
|
|||
return
|
||||
|
||||
# Validate gecko mode doesn't use non-wall mode
|
||||
if args.format == "gecko" and args.mode != "wall":
|
||||
if args.format == "gecko" and getattr(args, 'mode', 'wall') != "wall":
|
||||
parser.error(
|
||||
"--mode option is incompatible with --gecko. "
|
||||
"Gecko format automatically includes both GIL-holding and CPU status analysis."
|
||||
|
|
@ -609,7 +650,7 @@ def _validate_args(args, parser):
|
|||
|
||||
# Validate --opcodes is only used with compatible formats
|
||||
opcodes_compatible_formats = ("live", "gecko", "flamegraph", "heatmap")
|
||||
if args.opcodes and args.format not in opcodes_compatible_formats:
|
||||
if getattr(args, 'opcodes', False) and args.format not in opcodes_compatible_formats:
|
||||
parser.error(
|
||||
f"--opcodes is only compatible with {', '.join('--' + f for f in opcodes_compatible_formats)}."
|
||||
)
|
||||
|
|
@ -633,6 +674,16 @@ def _validate_args(args, parser):
|
|||
|
||||
def main():
|
||||
"""Main entry point for the CLI."""
|
||||
# Set locale for number formatting, restore on exit
|
||||
old_locale = locale.setlocale(locale.LC_ALL, None)
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
try:
|
||||
_main()
|
||||
finally:
|
||||
locale.setlocale(locale.LC_ALL, old_locale)
|
||||
|
||||
|
||||
def _main():
|
||||
# Create the main parser
|
||||
parser = argparse.ArgumentParser(
|
||||
description=_HELP_DESCRIPTION,
|
||||
|
|
@ -721,6 +772,30 @@ Examples:
|
|||
_add_format_options(attach_parser)
|
||||
_add_pstats_options(attach_parser)
|
||||
|
||||
# === REPLAY COMMAND ===
|
||||
replay_parser = subparsers.add_parser(
|
||||
"replay",
|
||||
help="Replay a binary profile and convert to another format",
|
||||
formatter_class=CustomFormatter,
|
||||
description="""Replay a binary profile file and convert to another format
|
||||
|
||||
Examples:
|
||||
# Convert binary to flamegraph
|
||||
`python -m profiling.sampling replay --flamegraph -o output.html profile.bin`
|
||||
|
||||
# Convert binary to pstats and print to stdout
|
||||
`python -m profiling.sampling replay profile.bin`
|
||||
|
||||
# Convert binary to gecko format
|
||||
`python -m profiling.sampling replay --gecko -o profile.json profile.bin`""",
|
||||
)
|
||||
replay_parser.add_argument(
|
||||
"input_file",
|
||||
help="Binary profile file to replay",
|
||||
)
|
||||
_add_format_options(replay_parser, include_compression=False, include_binary=False)
|
||||
_add_pstats_options(replay_parser)
|
||||
|
||||
# Parse arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
|
|
@ -731,6 +806,7 @@ Examples:
|
|||
command_handlers = {
|
||||
"run": _handle_run,
|
||||
"attach": _handle_attach,
|
||||
"replay": _handle_replay,
|
||||
}
|
||||
|
||||
# Execute the appropriate command
|
||||
|
|
@ -743,6 +819,8 @@ Examples:
|
|||
|
||||
def _handle_attach(args):
|
||||
"""Handle the 'attach' command."""
|
||||
if not _is_process_running(args.pid):
|
||||
raise SamplingUnknownProcessError(args.pid)
|
||||
# Check if live mode is requested
|
||||
if args.live:
|
||||
_handle_live_attach(args, args.pid)
|
||||
|
|
@ -760,8 +838,16 @@ def _handle_attach(args):
|
|||
mode != PROFILING_MODE_WALL if mode != PROFILING_MODE_ALL else False
|
||||
)
|
||||
|
||||
output_file = None
|
||||
if args.format == "binary":
|
||||
output_file = args.outfile or _generate_output_filename(args.format, args.pid)
|
||||
|
||||
# Create the appropriate collector
|
||||
collector = _create_collector(args.format, args.interval, skip_idle, args.opcodes)
|
||||
collector = _create_collector(
|
||||
args.format, args.interval, skip_idle, args.opcodes,
|
||||
output_file=output_file,
|
||||
compression=getattr(args, 'compression', 'auto')
|
||||
)
|
||||
|
||||
with _get_child_monitor_context(args, args.pid):
|
||||
collector = sample(
|
||||
|
|
@ -792,13 +878,13 @@ def _handle_run(args):
|
|||
added_cwd = True
|
||||
try:
|
||||
if importlib.util.find_spec(args.target) is None:
|
||||
sys.exit(f"Error: Module not found: {args.target}")
|
||||
raise SamplingModuleNotFoundError(args.target)
|
||||
finally:
|
||||
if added_cwd:
|
||||
sys.path.remove(cwd)
|
||||
else:
|
||||
if not os.path.exists(args.target):
|
||||
sys.exit(f"Error: Script not found: {args.target}")
|
||||
raise SamplingScriptNotFoundError(args.target)
|
||||
|
||||
# Check if live mode is requested
|
||||
if args.live:
|
||||
|
|
@ -829,8 +915,16 @@ def _handle_run(args):
|
|||
mode != PROFILING_MODE_WALL if mode != PROFILING_MODE_ALL else False
|
||||
)
|
||||
|
||||
output_file = None
|
||||
if args.format == "binary":
|
||||
output_file = args.outfile or _generate_output_filename(args.format, process.pid)
|
||||
|
||||
# Create the appropriate collector
|
||||
collector = _create_collector(args.format, args.interval, skip_idle, args.opcodes)
|
||||
collector = _create_collector(
|
||||
args.format, args.interval, skip_idle, args.opcodes,
|
||||
output_file=output_file,
|
||||
compression=getattr(args, 'compression', 'auto')
|
||||
)
|
||||
|
||||
with _get_child_monitor_context(args, process.pid):
|
||||
try:
|
||||
|
|
@ -949,5 +1043,48 @@ def _handle_live_run(args):
|
|||
process.wait()
|
||||
|
||||
|
||||
def _handle_replay(args):
|
||||
"""Handle the 'replay' command - convert binary profile to another format."""
|
||||
import os
|
||||
|
||||
if not os.path.exists(args.input_file):
|
||||
sys.exit(f"Error: Input file not found: {args.input_file}")
|
||||
|
||||
with BinaryReader(args.input_file) as reader:
|
||||
info = reader.get_info()
|
||||
interval = info['sample_interval_us']
|
||||
|
||||
print(f"Replaying {info['sample_count']} samples from {args.input_file}")
|
||||
print(f" Sample interval: {interval} us")
|
||||
print(f" Compression: {'zstd' if info.get('compression_type', 0) == 1 else 'none'}")
|
||||
|
||||
collector = _create_collector(args.format, interval, skip_idle=False)
|
||||
|
||||
def progress_callback(current, total):
|
||||
if total > 0:
|
||||
pct = current / total
|
||||
bar_width = 40
|
||||
filled = int(bar_width * pct)
|
||||
bar = '█' * filled + '░' * (bar_width - filled)
|
||||
print(f"\r [{bar}] {pct*100:5.1f}% ({current:,}/{total:,})", end="", flush=True)
|
||||
|
||||
count = reader.replay_samples(collector, progress_callback)
|
||||
print()
|
||||
|
||||
if args.format == "pstats":
|
||||
if args.outfile:
|
||||
collector.export(args.outfile)
|
||||
else:
|
||||
sort_choice = args.sort if args.sort is not None else "nsamples"
|
||||
limit = args.limit if args.limit is not None else 15
|
||||
sort_mode = _sort_to_mode(sort_choice)
|
||||
collector.print_stats(sort_mode, limit, not args.no_summary, PROFILING_MODE_WALL)
|
||||
else:
|
||||
filename = args.outfile or _generate_output_filename(args.format, os.getpid())
|
||||
collector.export(filename)
|
||||
|
||||
print(f"Replayed {count} samples")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -44,8 +44,17 @@ def extract_lineno(location):
|
|||
|
||||
class Collector(ABC):
|
||||
@abstractmethod
|
||||
def collect(self, stack_frames):
|
||||
"""Collect profiling data from stack frames."""
|
||||
def collect(self, stack_frames, timestamps_us=None):
|
||||
"""Collect profiling data from stack frames.
|
||||
|
||||
Args:
|
||||
stack_frames: List of InterpreterInfo objects
|
||||
timestamps_us: Optional list of timestamps in microseconds. If provided
|
||||
(from binary replay with RLE batching), use these instead of current
|
||||
time. If None, collectors should use time.monotonic() or similar.
|
||||
The list may contain multiple timestamps when samples are batched
|
||||
together (same stack, different times).
|
||||
"""
|
||||
|
||||
def collect_failed_sample(self):
|
||||
"""Collect data about a failed sample attempt."""
|
||||
|
|
@ -79,6 +88,17 @@ class Collector(ABC):
|
|||
# Phase 3: Build linear stacks from each leaf to root (optimized - no sorting!)
|
||||
yield from self._build_linear_stacks(leaf_task_ids, task_map, child_to_parent)
|
||||
|
||||
def _iter_stacks(self, stack_frames, skip_idle=False):
|
||||
"""Yield (frames, thread_id) for all stacks, handling both sync and async modes."""
|
||||
if stack_frames and hasattr(stack_frames[0], "awaited_by"):
|
||||
for frames, thread_id, _ in self._iter_async_frames(stack_frames):
|
||||
if frames:
|
||||
yield frames, thread_id
|
||||
else:
|
||||
for frames, thread_id in self._iter_all_frames(stack_frames, skip_idle=skip_idle):
|
||||
if frames:
|
||||
yield frames, thread_id
|
||||
|
||||
def _build_task_graph(self, awaited_info_list):
|
||||
task_map = {}
|
||||
child_to_parent = {} # Maps child_id -> (selected_parent_id, parent_count)
|
||||
|
|
|
|||
19
Lib/profiling/sampling/errors.py
Normal file
19
Lib/profiling/sampling/errors.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
"""Custom exceptions for the sampling profiler."""
|
||||
|
||||
class SamplingProfilerError(Exception):
|
||||
"""Base exception for sampling profiler errors."""
|
||||
|
||||
class SamplingUnknownProcessError(SamplingProfilerError):
|
||||
def __init__(self, pid):
|
||||
self.pid = pid
|
||||
super().__init__(f"Process with PID '{pid}' does not exist.")
|
||||
|
||||
class SamplingScriptNotFoundError(SamplingProfilerError):
|
||||
def __init__(self, script_path):
|
||||
self.script_path = script_path
|
||||
super().__init__(f"Script '{script_path}' not found.")
|
||||
|
||||
class SamplingModuleNotFoundError(SamplingProfilerError):
|
||||
def __init__(self, module_name):
|
||||
self.module_name = module_name
|
||||
super().__init__(f"Module '{module_name}' not found.")
|
||||
|
|
@ -66,7 +66,7 @@ class GeckoCollector(Collector):
|
|||
self.sample_interval_usec = sample_interval_usec
|
||||
self.skip_idle = skip_idle
|
||||
self.opcodes_enabled = opcodes
|
||||
self.start_time = time.time() * 1000 # milliseconds since epoch
|
||||
self.start_time = time.monotonic() * 1000 # milliseconds since start
|
||||
|
||||
# Global string table (shared across all threads)
|
||||
self.global_strings = ["(root)"] # Start with root
|
||||
|
|
@ -103,6 +103,9 @@ class GeckoCollector(Collector):
|
|||
# Opcode state tracking per thread: tid -> (opcode, lineno, col_offset, funcname, filename, start_time)
|
||||
self.opcode_state = {}
|
||||
|
||||
# For binary replay: track base timestamp (first sample's timestamp)
|
||||
self._replay_base_timestamp_us = None
|
||||
|
||||
def _track_state_transition(self, tid, condition, active_dict, inactive_dict,
|
||||
active_name, inactive_name, category, current_time):
|
||||
"""Track binary state transitions and emit markers.
|
||||
|
|
@ -138,18 +141,35 @@ class GeckoCollector(Collector):
|
|||
self._add_marker(tid, active_name, active_dict.pop(tid),
|
||||
current_time, category)
|
||||
|
||||
def collect(self, stack_frames):
|
||||
"""Collect a sample from stack frames."""
|
||||
current_time = (time.time() * 1000) - self.start_time
|
||||
def collect(self, stack_frames, timestamps_us=None):
|
||||
"""Collect samples from stack frames.
|
||||
|
||||
Args:
|
||||
stack_frames: List of interpreter/thread frame info
|
||||
timestamps_us: List of timestamps in microseconds (None for live sampling)
|
||||
"""
|
||||
# Handle live sampling (no timestamps provided)
|
||||
if timestamps_us is None:
|
||||
current_time = (time.monotonic() * 1000) - self.start_time
|
||||
times = [current_time]
|
||||
else:
|
||||
if not timestamps_us:
|
||||
return
|
||||
# Initialize base timestamp if needed
|
||||
if self._replay_base_timestamp_us is None:
|
||||
self._replay_base_timestamp_us = timestamps_us[0]
|
||||
# Convert all timestamps to times (ms relative to first sample)
|
||||
base = self._replay_base_timestamp_us
|
||||
times = [(ts - base) / 1000 for ts in timestamps_us]
|
||||
|
||||
first_time = times[0]
|
||||
|
||||
# Update interval calculation
|
||||
if self.sample_count > 0 and self.last_sample_time > 0:
|
||||
self.interval = (
|
||||
current_time - self.last_sample_time
|
||||
) / self.sample_count
|
||||
self.last_sample_time = current_time
|
||||
self.interval = (times[-1] - self.last_sample_time) / self.sample_count
|
||||
self.last_sample_time = times[-1]
|
||||
|
||||
# Process threads and track GC per thread
|
||||
# Process threads
|
||||
for interpreter_info in stack_frames:
|
||||
for thread_info in interpreter_info.threads:
|
||||
frames = thread_info.frame_info
|
||||
|
|
@ -167,92 +187,86 @@ class GeckoCollector(Collector):
|
|||
on_cpu = bool(status_flags & THREAD_STATUS_ON_CPU)
|
||||
gil_requested = bool(status_flags & THREAD_STATUS_GIL_REQUESTED)
|
||||
|
||||
# Track GIL possession (Has GIL / No GIL)
|
||||
# Track state transitions using first timestamp
|
||||
self._track_state_transition(
|
||||
tid, has_gil, self.has_gil_start, self.no_gil_start,
|
||||
"Has GIL", "No GIL", CATEGORY_GIL, current_time
|
||||
"Has GIL", "No GIL", CATEGORY_GIL, first_time
|
||||
)
|
||||
|
||||
# Track CPU state (On CPU / Off CPU)
|
||||
self._track_state_transition(
|
||||
tid, on_cpu, self.on_cpu_start, self.off_cpu_start,
|
||||
"On CPU", "Off CPU", CATEGORY_CPU, current_time
|
||||
"On CPU", "Off CPU", CATEGORY_CPU, first_time
|
||||
)
|
||||
|
||||
# Track code type (Python Code / Native Code)
|
||||
# This is tri-state: Python (has_gil), Native (on_cpu without gil), or Neither
|
||||
# Track code type
|
||||
if has_gil:
|
||||
self._track_state_transition(
|
||||
tid, True, self.python_code_start, self.native_code_start,
|
||||
"Python Code", "Native Code", CATEGORY_CODE_TYPE, current_time
|
||||
"Python Code", "Native Code", CATEGORY_CODE_TYPE, first_time
|
||||
)
|
||||
elif on_cpu:
|
||||
self._track_state_transition(
|
||||
tid, True, self.native_code_start, self.python_code_start,
|
||||
"Native Code", "Python Code", CATEGORY_CODE_TYPE, current_time
|
||||
"Native Code", "Python Code", CATEGORY_CODE_TYPE, first_time
|
||||
)
|
||||
else:
|
||||
# Thread is idle (neither has GIL nor on CPU) - close any open code markers
|
||||
# This handles the third state that _track_state_transition doesn't cover
|
||||
if tid in self.initialized_threads:
|
||||
if tid in self.python_code_start:
|
||||
self._add_marker(tid, "Python Code", self.python_code_start.pop(tid),
|
||||
current_time, CATEGORY_CODE_TYPE)
|
||||
first_time, CATEGORY_CODE_TYPE)
|
||||
if tid in self.native_code_start:
|
||||
self._add_marker(tid, "Native Code", self.native_code_start.pop(tid),
|
||||
current_time, CATEGORY_CODE_TYPE)
|
||||
first_time, CATEGORY_CODE_TYPE)
|
||||
|
||||
# Track "Waiting for GIL" intervals (one-sided tracking)
|
||||
# Track GIL wait
|
||||
if gil_requested:
|
||||
self.gil_wait_start.setdefault(tid, current_time)
|
||||
self.gil_wait_start.setdefault(tid, first_time)
|
||||
elif tid in self.gil_wait_start:
|
||||
self._add_marker(tid, "Waiting for GIL", self.gil_wait_start.pop(tid),
|
||||
current_time, CATEGORY_GIL)
|
||||
first_time, CATEGORY_GIL)
|
||||
|
||||
# Track exception state (Has Exception / No Exception)
|
||||
# Track exception state
|
||||
has_exception = bool(status_flags & THREAD_STATUS_HAS_EXCEPTION)
|
||||
self._track_state_transition(
|
||||
tid, has_exception, self.exception_start, self.no_exception_start,
|
||||
"Has Exception", "No Exception", CATEGORY_EXCEPTION, current_time
|
||||
"Has Exception", "No Exception", CATEGORY_EXCEPTION, first_time
|
||||
)
|
||||
|
||||
# Track GC events by detecting <GC> frames in the stack trace
|
||||
# This leverages the improved GC frame tracking from commit 336366fd7ca
|
||||
# which precisely identifies the thread that initiated GC collection
|
||||
# Track GC events
|
||||
has_gc_frame = any(frame[2] == "<GC>" for frame in frames)
|
||||
if has_gc_frame:
|
||||
# This thread initiated GC collection
|
||||
if tid not in self.gc_start_per_thread:
|
||||
self.gc_start_per_thread[tid] = current_time
|
||||
self.gc_start_per_thread[tid] = first_time
|
||||
elif tid in self.gc_start_per_thread:
|
||||
# End GC marker when no more GC frames are detected
|
||||
self._add_marker(tid, "GC Collecting", self.gc_start_per_thread.pop(tid),
|
||||
current_time, CATEGORY_GC)
|
||||
first_time, CATEGORY_GC)
|
||||
|
||||
# Mark thread as initialized after processing all state transitions
|
||||
# Mark thread as initialized
|
||||
self.initialized_threads.add(tid)
|
||||
|
||||
# Categorize: idle if neither has GIL nor on CPU
|
||||
# Skip idle threads if requested
|
||||
is_idle = not has_gil and not on_cpu
|
||||
|
||||
# Skip idle threads if skip_idle is enabled
|
||||
if self.skip_idle and is_idle:
|
||||
continue
|
||||
|
||||
if not frames:
|
||||
continue
|
||||
|
||||
# Process the stack
|
||||
# Process stack once to get stack_index
|
||||
stack_index = self._process_stack(thread_data, frames)
|
||||
|
||||
# Add sample - cache references to avoid dictionary lookups
|
||||
# Add samples with timestamps
|
||||
samples = thread_data["samples"]
|
||||
samples["stack"].append(stack_index)
|
||||
samples["time"].append(current_time)
|
||||
samples["eventDelay"].append(None)
|
||||
samples_stack = samples["stack"]
|
||||
samples_time = samples["time"]
|
||||
samples_delay = samples["eventDelay"]
|
||||
|
||||
# Track opcode state changes for interval markers (leaf frame only)
|
||||
if self.opcodes_enabled:
|
||||
for t in times:
|
||||
samples_stack.append(stack_index)
|
||||
samples_time.append(t)
|
||||
samples_delay.append(None)
|
||||
|
||||
# Handle opcodes
|
||||
if self.opcodes_enabled and frames:
|
||||
leaf_frame = frames[0]
|
||||
filename, location, funcname, opcode = leaf_frame
|
||||
if isinstance(location, tuple):
|
||||
|
|
@ -264,18 +278,15 @@ class GeckoCollector(Collector):
|
|||
current_state = (opcode, lineno, col_offset, funcname, filename)
|
||||
|
||||
if tid not in self.opcode_state:
|
||||
# First observation - start tracking
|
||||
self.opcode_state[tid] = (*current_state, current_time)
|
||||
self.opcode_state[tid] = (*current_state, first_time)
|
||||
elif self.opcode_state[tid][:5] != current_state:
|
||||
# State changed - emit marker for previous state
|
||||
prev_opcode, prev_lineno, prev_col, prev_funcname, prev_filename, prev_start = self.opcode_state[tid]
|
||||
self._add_opcode_interval_marker(
|
||||
tid, prev_opcode, prev_lineno, prev_col, prev_funcname, prev_start, current_time
|
||||
tid, prev_opcode, prev_lineno, prev_col, prev_funcname, prev_start, first_time
|
||||
)
|
||||
# Start tracking new state
|
||||
self.opcode_state[tid] = (*current_state, current_time)
|
||||
self.opcode_state[tid] = (*current_state, first_time)
|
||||
|
||||
self.sample_count += 1
|
||||
self.sample_count += len(times)
|
||||
|
||||
def _create_thread(self, tid):
|
||||
"""Create a new thread structure with processed profile format."""
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import collections
|
|||
import html
|
||||
import importlib.resources
|
||||
import json
|
||||
import locale
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
|
|
@ -15,6 +16,7 @@ from pathlib import Path
|
|||
from typing import Dict, List, Tuple
|
||||
|
||||
from ._css_utils import get_combined_css
|
||||
from ._format_utils import fmt
|
||||
from .collector import normalize_location, extract_lineno
|
||||
from .stack_collector import StackTraceCollector
|
||||
|
||||
|
|
@ -343,7 +345,7 @@ class _HtmlRenderer:
|
|||
<div class="type-header" onclick="toggleTypeSection(this)">
|
||||
<span class="type-icon">{icon}</span>
|
||||
<span class="type-title">{type_names[module_type]}</span>
|
||||
<span class="type-stats">({tree.count} {file_word}, {tree.samples:,} {sample_word})</span>
|
||||
<span class="type-stats">({tree.count} {file_word}, {tree.samples:n} {sample_word})</span>
|
||||
</div>
|
||||
<div class="type-content"{content_style}>
|
||||
'''
|
||||
|
|
@ -390,7 +392,7 @@ class _HtmlRenderer:
|
|||
parts.append(f'{indent} <span class="folder-icon">▶</span>')
|
||||
parts.append(f'{indent} <span class="folder-name">📁 {html.escape(name)}</span>')
|
||||
parts.append(f'{indent} <span class="folder-stats">'
|
||||
f'({node.count} {file_word}, {node.samples:,} {sample_word})</span>')
|
||||
f'({node.count} {file_word}, {node.samples:n} {sample_word})</span>')
|
||||
parts.append(f'{indent} </div>')
|
||||
parts.append(f'{indent} <div class="folder-content" style="display: none;">')
|
||||
|
||||
|
|
@ -431,10 +433,11 @@ class _HtmlRenderer:
|
|||
bar_width = min(stat.percentage, 100)
|
||||
|
||||
html_file = self.file_index[stat.filename]
|
||||
s = "" if stat.total_samples == 1 else "s"
|
||||
|
||||
return (f'{indent}<div class="file-item">\n'
|
||||
f'{indent} <a href="{html_file}" class="file-link" title="{full_path}">📄 {module_name}</a>\n'
|
||||
f'{indent} <span class="file-samples">{stat.total_samples:,} samples</span>\n'
|
||||
f'{indent} <span class="file-samples">{stat.total_samples:n} sample{s}</span>\n'
|
||||
f'{indent} <div class="heatmap-bar-container"><div class="heatmap-bar" style="width: {bar_width}px; height: {self.heatmap_bar_height}px;" data-intensity="{intensity:.3f}"></div></div>\n'
|
||||
f'{indent}</div>\n')
|
||||
|
||||
|
|
@ -518,7 +521,7 @@ class HeatmapCollector(StackTraceCollector):
|
|||
}
|
||||
self.stats.update(kwargs)
|
||||
|
||||
def process_frames(self, frames, thread_id):
|
||||
def process_frames(self, frames, thread_id, weight=1):
|
||||
"""Process stack frames and count samples per line.
|
||||
|
||||
Args:
|
||||
|
|
@ -526,8 +529,9 @@ class HeatmapCollector(StackTraceCollector):
|
|||
leaf-to-root order. location is (lineno, end_lineno, col_offset, end_col_offset).
|
||||
opcode is None if not gathered.
|
||||
thread_id: Thread ID for this stack trace
|
||||
weight: Number of samples this stack represents (for batched RLE)
|
||||
"""
|
||||
self._total_samples += 1
|
||||
self._total_samples += weight
|
||||
self._seen_lines.clear()
|
||||
|
||||
for i, (filename, location, funcname, opcode) in enumerate(frames):
|
||||
|
|
@ -545,15 +549,16 @@ class HeatmapCollector(StackTraceCollector):
|
|||
self._seen_lines.add(line_key)
|
||||
|
||||
self._record_line_sample(filename, lineno, funcname, is_leaf=is_leaf,
|
||||
count_cumulative=count_cumulative)
|
||||
count_cumulative=count_cumulative, weight=weight)
|
||||
|
||||
if opcode is not None:
|
||||
# Set opcodes_enabled flag when we first encounter opcode data
|
||||
self.opcodes_enabled = True
|
||||
self._record_bytecode_sample(filename, lineno, opcode,
|
||||
end_lineno, col_offset, end_col_offset)
|
||||
end_lineno, col_offset, end_col_offset,
|
||||
weight=weight)
|
||||
|
||||
# Build call graph for adjacent frames
|
||||
# Build call graph for adjacent frames (relationships are deduplicated anyway)
|
||||
if i + 1 < len(frames):
|
||||
next_frame = frames[i + 1]
|
||||
next_lineno = extract_lineno(next_frame[1])
|
||||
|
|
@ -575,24 +580,25 @@ class HeatmapCollector(StackTraceCollector):
|
|||
return True
|
||||
|
||||
def _record_line_sample(self, filename, lineno, funcname, is_leaf=False,
|
||||
count_cumulative=True):
|
||||
count_cumulative=True, weight=1):
|
||||
"""Record a sample for a specific line."""
|
||||
# Track cumulative samples (all occurrences in stack)
|
||||
if count_cumulative:
|
||||
self.line_samples[(filename, lineno)] += 1
|
||||
self.file_samples[filename][lineno] += 1
|
||||
self.line_samples[(filename, lineno)] += weight
|
||||
self.file_samples[filename][lineno] += weight
|
||||
|
||||
# Track self/leaf samples (only when at top of stack)
|
||||
if is_leaf:
|
||||
self.line_self_samples[(filename, lineno)] += 1
|
||||
self.file_self_samples[filename][lineno] += 1
|
||||
self.line_self_samples[(filename, lineno)] += weight
|
||||
self.file_self_samples[filename][lineno] += weight
|
||||
|
||||
# Record function definition location
|
||||
if funcname and (filename, funcname) not in self.function_definitions:
|
||||
self.function_definitions[(filename, funcname)] = lineno
|
||||
|
||||
def _record_bytecode_sample(self, filename, lineno, opcode,
|
||||
end_lineno=None, col_offset=None, end_col_offset=None):
|
||||
end_lineno=None, col_offset=None, end_col_offset=None,
|
||||
weight=1):
|
||||
"""Record a sample for a specific bytecode instruction.
|
||||
|
||||
Args:
|
||||
|
|
@ -602,6 +608,7 @@ class HeatmapCollector(StackTraceCollector):
|
|||
end_lineno: End line number (may be -1 if not available)
|
||||
col_offset: Column offset in UTF-8 bytes (may be -1 if not available)
|
||||
end_col_offset: End column offset in UTF-8 bytes (may be -1 if not available)
|
||||
weight: Number of samples this represents (for batched RLE)
|
||||
"""
|
||||
key = (filename, lineno)
|
||||
|
||||
|
|
@ -609,7 +616,7 @@ class HeatmapCollector(StackTraceCollector):
|
|||
if opcode not in self.line_opcodes[key]:
|
||||
self.line_opcodes[key][opcode] = {'count': 0, 'locations': set()}
|
||||
|
||||
self.line_opcodes[key][opcode]['count'] += 1
|
||||
self.line_opcodes[key][opcode]['count'] += weight
|
||||
|
||||
# Store unique location info if column offset is available (not -1)
|
||||
if col_offset is not None and col_offset >= 0:
|
||||
|
|
@ -761,7 +768,8 @@ class HeatmapCollector(StackTraceCollector):
|
|||
"""Print summary of exported heatmap."""
|
||||
print(f"Heatmap output written to {output_dir}/")
|
||||
print(f" - Index: {output_dir / 'index.html'}")
|
||||
print(f" - {len(file_stats)} source file(s) analyzed")
|
||||
s = "" if len(file_stats) == 1 else "s"
|
||||
print(f" - {len(file_stats)} source file{s} analyzed")
|
||||
|
||||
def _calculate_file_stats(self) -> List[FileStats]:
|
||||
"""Calculate statistics for each file.
|
||||
|
|
@ -824,7 +832,7 @@ class HeatmapCollector(StackTraceCollector):
|
|||
# Format error rate and missed samples with bar classes
|
||||
error_rate = self.stats.get('error_rate')
|
||||
if error_rate is not None:
|
||||
error_rate_str = f"{error_rate:.1f}%"
|
||||
error_rate_str = f"{fmt(error_rate)}%"
|
||||
error_rate_width = min(error_rate, 100)
|
||||
# Determine bar color class based on rate
|
||||
if error_rate < 5:
|
||||
|
|
@ -840,7 +848,7 @@ class HeatmapCollector(StackTraceCollector):
|
|||
|
||||
missed_samples = self.stats.get('missed_samples')
|
||||
if missed_samples is not None:
|
||||
missed_samples_str = f"{missed_samples:.1f}%"
|
||||
missed_samples_str = f"{fmt(missed_samples)}%"
|
||||
missed_samples_width = min(missed_samples, 100)
|
||||
if missed_samples < 5:
|
||||
missed_samples_class = "good"
|
||||
|
|
@ -859,10 +867,10 @@ class HeatmapCollector(StackTraceCollector):
|
|||
"<!-- INLINE_JS -->": f"<script>\n{self._template_loader.index_js}\n</script>",
|
||||
"<!-- PYTHON_LOGO -->": self._template_loader.logo_html,
|
||||
"<!-- PYTHON_VERSION -->": f"{sys.version_info.major}.{sys.version_info.minor}",
|
||||
"<!-- NUM_FILES -->": str(len(file_stats)),
|
||||
"<!-- TOTAL_SAMPLES -->": f"{self._total_samples:,}",
|
||||
"<!-- DURATION -->": f"{self.stats.get('duration_sec', 0):.1f}s",
|
||||
"<!-- SAMPLE_RATE -->": f"{self.stats.get('sample_rate', 0):.1f}",
|
||||
"<!-- NUM_FILES -->": f"{len(file_stats):n}",
|
||||
"<!-- TOTAL_SAMPLES -->": f"{self._total_samples:n}",
|
||||
"<!-- DURATION -->": fmt(self.stats.get('duration_sec', 0)),
|
||||
"<!-- SAMPLE_RATE -->": fmt(self.stats.get('sample_rate', 0)),
|
||||
"<!-- ERROR_RATE -->": error_rate_str,
|
||||
"<!-- ERROR_RATE_WIDTH -->": str(error_rate_width),
|
||||
"<!-- ERROR_RATE_CLASS -->": error_rate_class,
|
||||
|
|
@ -906,12 +914,12 @@ class HeatmapCollector(StackTraceCollector):
|
|||
# Populate template
|
||||
replacements = {
|
||||
"<!-- FILENAME -->": html.escape(filename),
|
||||
"<!-- TOTAL_SAMPLES -->": f"{file_stat.total_samples:,}",
|
||||
"<!-- TOTAL_SELF_SAMPLES -->": f"{file_stat.total_self_samples:,}",
|
||||
"<!-- NUM_LINES -->": str(file_stat.num_lines),
|
||||
"<!-- PERCENTAGE -->": f"{file_stat.percentage:.2f}",
|
||||
"<!-- MAX_SAMPLES -->": str(file_stat.max_samples),
|
||||
"<!-- MAX_SELF_SAMPLES -->": str(file_stat.max_self_samples),
|
||||
"<!-- TOTAL_SAMPLES -->": f"{file_stat.total_samples:n}",
|
||||
"<!-- TOTAL_SELF_SAMPLES -->": f"{file_stat.total_self_samples:n}",
|
||||
"<!-- NUM_LINES -->": f"{file_stat.num_lines:n}",
|
||||
"<!-- PERCENTAGE -->": fmt(file_stat.percentage, 2),
|
||||
"<!-- MAX_SAMPLES -->": f"{file_stat.max_samples:n}",
|
||||
"<!-- MAX_SELF_SAMPLES -->": f"{file_stat.max_self_samples:n}",
|
||||
"<!-- CODE_LINES -->": ''.join(code_lines_html),
|
||||
"<!-- INLINE_CSS -->": f"<style>\n{self._template_loader.file_css}\n</style>",
|
||||
"<!-- INLINE_JS -->": f"<script>\n{self._template_loader.file_js}\n</script>",
|
||||
|
|
@ -948,9 +956,9 @@ class HeatmapCollector(StackTraceCollector):
|
|||
else:
|
||||
self_intensity = 0
|
||||
|
||||
self_display = f"{self_samples:,}" if self_samples > 0 else ""
|
||||
cumulative_display = f"{cumulative_samples:,}"
|
||||
tooltip = f"Self: {self_samples:,}, Total: {cumulative_samples:,}"
|
||||
self_display = f"{self_samples:n}" if self_samples > 0 else ""
|
||||
cumulative_display = f"{cumulative_samples:n}"
|
||||
tooltip = f"Self: {self_samples:n}, Total: {cumulative_samples:n}"
|
||||
else:
|
||||
cumulative_intensity = 0
|
||||
self_intensity = 0
|
||||
|
|
@ -978,7 +986,17 @@ class HeatmapCollector(StackTraceCollector):
|
|||
f'data-spec-pct="{spec_pct}" '
|
||||
f'onclick="toggleBytecode(this)" title="Show bytecode">▶</button>'
|
||||
)
|
||||
bytecode_panel_html = f' <div class="bytecode-panel" id="bytecode-{line_num}" style="display:none;"></div>\n'
|
||||
# Wrapper contains columns + content panel
|
||||
bytecode_panel_html = (
|
||||
f' <div class="bytecode-wrapper" id="bytecode-wrapper-{line_num}">\n'
|
||||
f' <div class="bytecode-columns">'
|
||||
f'<div class="line-number"></div>'
|
||||
f'<div class="line-samples-self"></div>'
|
||||
f'<div class="line-samples-cumulative"></div>'
|
||||
f'</div>\n'
|
||||
f' <div class="bytecode-panel" id="bytecode-{line_num}"></div>\n'
|
||||
f' </div>\n'
|
||||
)
|
||||
elif self.opcodes_enabled:
|
||||
# Add invisible spacer to maintain consistent indentation when opcodes are enabled
|
||||
bytecode_btn_html = '<div class="bytecode-spacer"></div>'
|
||||
|
|
@ -1195,7 +1213,7 @@ class HeatmapCollector(StackTraceCollector):
|
|||
file, line, func, count = valid_items[0]
|
||||
target_html = self.file_index[file]
|
||||
nav_data = json.dumps({'link': f"{target_html}#line-{line}", 'func': func})
|
||||
title = f"Go to {btn_class}: {html.escape(func)} ({count:,} samples)"
|
||||
title = f"Go to {btn_class}: {html.escape(func)} ({count:n} samples)"
|
||||
return f'<button class="nav-btn {btn_class}" data-nav=\'{html.escape(nav_data)}\' title="{title}">{arrow}</button>'
|
||||
|
||||
# Multiple items - create menu
|
||||
|
|
@ -1210,5 +1228,5 @@ class HeatmapCollector(StackTraceCollector):
|
|||
for file, line, func, count in valid_items
|
||||
]
|
||||
items_json = html.escape(json.dumps(items_data))
|
||||
title = f"{len(items_data)} {btn_class}s ({total_samples:,} samples)"
|
||||
title = f"{len(items_data)} {btn_class}s ({total_samples:n} samples)"
|
||||
return f'<button class="nav-btn {btn_class}" data-nav-multi=\'{items_json}\' title="{title}">{arrow}</button>'
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ class LiveStatsCollector(Collector):
|
|||
self.failed_samples += 1
|
||||
self.total_samples += 1
|
||||
|
||||
def collect(self, stack_frames):
|
||||
def collect(self, stack_frames, timestamp_us=None):
|
||||
"""Collect and display profiling data."""
|
||||
if self.start_time is None:
|
||||
self.start_time = time.perf_counter()
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class PstatsCollector(Collector):
|
|||
self.skip_idle = skip_idle
|
||||
self._seen_locations = set()
|
||||
|
||||
def _process_frames(self, frames):
|
||||
def _process_frames(self, frames, weight=1):
|
||||
"""Process a single thread's frame stack."""
|
||||
if not frames:
|
||||
return
|
||||
|
|
@ -32,12 +32,12 @@ class PstatsCollector(Collector):
|
|||
location = (frame.filename, lineno, frame.funcname)
|
||||
if location not in self._seen_locations:
|
||||
self._seen_locations.add(location)
|
||||
self.result[location]["cumulative_calls"] += 1
|
||||
self.result[location]["cumulative_calls"] += weight
|
||||
|
||||
# The top frame gets counted as an inline call (directly executing)
|
||||
top_lineno = extract_lineno(frames[0].location)
|
||||
top_location = (frames[0].filename, top_lineno, frames[0].funcname)
|
||||
self.result[top_location]["direct_calls"] += 1
|
||||
self.result[top_location]["direct_calls"] += weight
|
||||
|
||||
# Track caller-callee relationships for call graph
|
||||
for i in range(1, len(frames)):
|
||||
|
|
@ -49,17 +49,12 @@ class PstatsCollector(Collector):
|
|||
callee = (callee_frame.filename, callee_lineno, callee_frame.funcname)
|
||||
caller = (caller_frame.filename, caller_lineno, caller_frame.funcname)
|
||||
|
||||
self.callers[callee][caller] += 1
|
||||
self.callers[callee][caller] += weight
|
||||
|
||||
def collect(self, stack_frames):
|
||||
if stack_frames and hasattr(stack_frames[0], "awaited_by"):
|
||||
# Async frame processing
|
||||
for frames, thread_id, task_id in self._iter_async_frames(stack_frames):
|
||||
self._process_frames(frames)
|
||||
else:
|
||||
# Regular frame processing
|
||||
for frames, thread_id in self._iter_all_frames(stack_frames, skip_idle=self.skip_idle):
|
||||
self._process_frames(frames)
|
||||
def collect(self, stack_frames, timestamps_us=None):
|
||||
weight = len(timestamps_us) if timestamps_us else 1
|
||||
for frames, _ in self._iter_stacks(stack_frames, skip_idle=self.skip_idle):
|
||||
self._process_frames(frames, weight=weight)
|
||||
|
||||
def export(self, filename):
|
||||
self.create_stats()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import _remote_debugging
|
||||
import os
|
||||
import pstats
|
||||
import statistics
|
||||
import sys
|
||||
import sysconfig
|
||||
|
|
@ -8,10 +7,7 @@ import time
|
|||
from collections import deque
|
||||
from _colorize import ANSIColors
|
||||
|
||||
from .pstats_collector import PstatsCollector
|
||||
from .stack_collector import CollapsedStackCollector, FlamegraphCollector
|
||||
from .heatmap_collector import HeatmapCollector
|
||||
from .gecko_collector import GeckoCollector
|
||||
from .binary_collector import BinaryCollector
|
||||
from .constants import (
|
||||
PROFILING_MODE_WALL,
|
||||
PROFILING_MODE_CPU,
|
||||
|
|
@ -19,6 +15,7 @@ from .constants import (
|
|||
PROFILING_MODE_ALL,
|
||||
PROFILING_MODE_EXCEPTION,
|
||||
)
|
||||
from ._format_utils import fmt
|
||||
try:
|
||||
from .live_collector import LiveStatsCollector
|
||||
except ImportError:
|
||||
|
|
@ -34,24 +31,30 @@ class SampleProfiler:
|
|||
self.all_threads = all_threads
|
||||
self.mode = mode # Store mode for later use
|
||||
self.collect_stats = collect_stats
|
||||
if _FREE_THREADED_BUILD:
|
||||
self.unwinder = _remote_debugging.RemoteUnwinder(
|
||||
self.pid, all_threads=self.all_threads, mode=mode, native=native, gc=gc,
|
||||
opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
|
||||
cache_frames=True, stats=collect_stats
|
||||
)
|
||||
else:
|
||||
only_active_threads = bool(self.all_threads)
|
||||
self.unwinder = _remote_debugging.RemoteUnwinder(
|
||||
self.pid, only_active_thread=only_active_threads, mode=mode, native=native, gc=gc,
|
||||
opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
|
||||
cache_frames=True, stats=collect_stats
|
||||
)
|
||||
try:
|
||||
self.unwinder = self._new_unwinder(native, gc, opcodes, skip_non_matching_threads)
|
||||
except RuntimeError as err:
|
||||
raise SystemExit(err) from err
|
||||
# Track sample intervals and total sample count
|
||||
self.sample_intervals = deque(maxlen=100)
|
||||
self.total_samples = 0
|
||||
self.realtime_stats = False
|
||||
|
||||
def _new_unwinder(self, native, gc, opcodes, skip_non_matching_threads):
|
||||
if _FREE_THREADED_BUILD:
|
||||
unwinder = _remote_debugging.RemoteUnwinder(
|
||||
self.pid, all_threads=self.all_threads, mode=self.mode, native=native, gc=gc,
|
||||
opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
|
||||
cache_frames=True, stats=self.collect_stats
|
||||
)
|
||||
else:
|
||||
unwinder = _remote_debugging.RemoteUnwinder(
|
||||
self.pid, only_active_thread=bool(self.all_threads), mode=self.mode, native=native, gc=gc,
|
||||
opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
|
||||
cache_frames=True, stats=self.collect_stats
|
||||
)
|
||||
return unwinder
|
||||
|
||||
def sample(self, collector, duration_sec=10, *, async_aware=False):
|
||||
sample_interval_sec = self.sample_interval_usec / 1_000_000
|
||||
running_time = 0
|
||||
|
|
@ -86,7 +89,7 @@ class SampleProfiler:
|
|||
collector.collect_failed_sample()
|
||||
errors += 1
|
||||
except Exception as e:
|
||||
if not self._is_process_running():
|
||||
if not _is_process_running(self.pid):
|
||||
break
|
||||
raise e from None
|
||||
|
||||
|
|
@ -129,14 +132,17 @@ class SampleProfiler:
|
|||
# Don't print stats for live mode (curses is handling display)
|
||||
is_live_mode = LiveStatsCollector is not None and isinstance(collector, LiveStatsCollector)
|
||||
if not is_live_mode:
|
||||
print(f"Captured {num_samples} samples in {running_time:.2f} seconds")
|
||||
print(f"Sample rate: {sample_rate:.2f} samples/sec")
|
||||
print(f"Error rate: {error_rate:.2f}%")
|
||||
print(f"Captured {num_samples:n} samples in {fmt(running_time, 2)} seconds")
|
||||
print(f"Sample rate: {fmt(sample_rate, 2)} samples/sec")
|
||||
print(f"Error rate: {fmt(error_rate, 2)}")
|
||||
|
||||
# Print unwinder stats if stats collection is enabled
|
||||
if self.collect_stats:
|
||||
self._print_unwinder_stats()
|
||||
|
||||
if isinstance(collector, BinaryCollector):
|
||||
self._print_binary_stats(collector)
|
||||
|
||||
# Pass stats to flamegraph collector if it's the right type
|
||||
if hasattr(collector, 'set_stats'):
|
||||
collector.set_stats(self.sample_interval_usec, running_time, sample_rate, error_rate, missed_samples, mode=self.mode)
|
||||
|
|
@ -145,25 +151,9 @@ class SampleProfiler:
|
|||
print(
|
||||
f"Warning: missed {expected_samples - num_samples} samples "
|
||||
f"from the expected total of {expected_samples} "
|
||||
f"({(expected_samples - num_samples) / expected_samples * 100:.2f}%)"
|
||||
f"({fmt((expected_samples - num_samples) / expected_samples * 100, 2)}%)"
|
||||
)
|
||||
|
||||
def _is_process_running(self):
|
||||
if sys.platform == "linux" or sys.platform == "darwin":
|
||||
try:
|
||||
os.kill(self.pid, 0)
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
elif sys.platform == "win32":
|
||||
try:
|
||||
_remote_debugging.RemoteUnwinder(self.pid)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
raise ValueError(f"Unsupported platform: {sys.platform}")
|
||||
|
||||
def _print_realtime_stats(self):
|
||||
"""Print real-time sampling statistics."""
|
||||
if len(self.sample_intervals) < 2:
|
||||
|
|
@ -195,16 +185,16 @@ class SampleProfiler:
|
|||
total = hits + partial + misses
|
||||
if total > 0:
|
||||
hit_pct = (hits + partial) / total * 100
|
||||
cache_stats_str = f" {ANSIColors.MAGENTA}Cache: {hit_pct:.1f}% ({hits}+{partial}/{misses}){ANSIColors.RESET}"
|
||||
cache_stats_str = f" {ANSIColors.MAGENTA}Cache: {fmt(hit_pct)}% ({hits}+{partial}/{misses}){ANSIColors.RESET}"
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
# Clear line and print stats
|
||||
print(
|
||||
f"\r\033[K{ANSIColors.BOLD_BLUE}Stats:{ANSIColors.RESET} "
|
||||
f"{ANSIColors.YELLOW}{mean_hz:.1f}Hz ({mean_us_per_sample:.1f}µs){ANSIColors.RESET} "
|
||||
f"{ANSIColors.GREEN}Min: {min_hz:.1f}Hz{ANSIColors.RESET} "
|
||||
f"{ANSIColors.RED}Max: {max_hz:.1f}Hz{ANSIColors.RESET} "
|
||||
f"{ANSIColors.YELLOW}{fmt(mean_hz)}Hz ({fmt(mean_us_per_sample)}µs){ANSIColors.RESET} "
|
||||
f"{ANSIColors.GREEN}Min: {fmt(min_hz)}Hz{ANSIColors.RESET} "
|
||||
f"{ANSIColors.RED}Max: {fmt(max_hz)}Hz{ANSIColors.RESET} "
|
||||
f"{ANSIColors.CYAN}N={self.total_samples}{ANSIColors.RESET}"
|
||||
f"{cache_stats_str}",
|
||||
end="",
|
||||
|
|
@ -234,10 +224,10 @@ class SampleProfiler:
|
|||
misses_pct = (frame_cache_misses / total_lookups * 100) if total_lookups > 0 else 0
|
||||
|
||||
print(f" {ANSIColors.CYAN}Frame Cache:{ANSIColors.RESET}")
|
||||
print(f" Total samples: {total_samples:,}")
|
||||
print(f" Full hits: {frame_cache_hits:,} ({ANSIColors.GREEN}{hits_pct:.1f}%{ANSIColors.RESET})")
|
||||
print(f" Partial hits: {frame_cache_partial_hits:,} ({ANSIColors.YELLOW}{partial_pct:.1f}%{ANSIColors.RESET})")
|
||||
print(f" Misses: {frame_cache_misses:,} ({ANSIColors.RED}{misses_pct:.1f}%{ANSIColors.RESET})")
|
||||
print(f" Total samples: {total_samples:n}")
|
||||
print(f" Full hits: {frame_cache_hits:n} ({ANSIColors.GREEN}{fmt(hits_pct)}%{ANSIColors.RESET})")
|
||||
print(f" Partial hits: {frame_cache_partial_hits:n} ({ANSIColors.YELLOW}{fmt(partial_pct)}%{ANSIColors.RESET})")
|
||||
print(f" Misses: {frame_cache_misses:n} ({ANSIColors.RED}{fmt(misses_pct)}%{ANSIColors.RESET})")
|
||||
|
||||
# Frame read stats
|
||||
frames_from_cache = stats.get('frames_read_from_cache', 0)
|
||||
|
|
@ -247,8 +237,8 @@ class SampleProfiler:
|
|||
memory_frame_pct = (frames_from_memory / total_frames * 100) if total_frames > 0 else 0
|
||||
|
||||
print(f" {ANSIColors.CYAN}Frame Reads:{ANSIColors.RESET}")
|
||||
print(f" From cache: {frames_from_cache:,} ({ANSIColors.GREEN}{cache_frame_pct:.1f}%{ANSIColors.RESET})")
|
||||
print(f" From memory: {frames_from_memory:,} ({ANSIColors.RED}{memory_frame_pct:.1f}%{ANSIColors.RESET})")
|
||||
print(f" From cache: {frames_from_cache:n} ({ANSIColors.GREEN}{fmt(cache_frame_pct)}%{ANSIColors.RESET})")
|
||||
print(f" From memory: {frames_from_memory:n} ({ANSIColors.RED}{fmt(memory_frame_pct)}%{ANSIColors.RESET})")
|
||||
|
||||
# Code object cache stats
|
||||
code_hits = stats.get('code_object_cache_hits', 0)
|
||||
|
|
@ -258,26 +248,95 @@ class SampleProfiler:
|
|||
code_misses_pct = (code_misses / total_code * 100) if total_code > 0 else 0
|
||||
|
||||
print(f" {ANSIColors.CYAN}Code Object Cache:{ANSIColors.RESET}")
|
||||
print(f" Hits: {code_hits:,} ({ANSIColors.GREEN}{code_hits_pct:.1f}%{ANSIColors.RESET})")
|
||||
print(f" Misses: {code_misses:,} ({ANSIColors.RED}{code_misses_pct:.1f}%{ANSIColors.RESET})")
|
||||
print(f" Hits: {code_hits:n} ({ANSIColors.GREEN}{fmt(code_hits_pct)}%{ANSIColors.RESET})")
|
||||
print(f" Misses: {code_misses:n} ({ANSIColors.RED}{fmt(code_misses_pct)}%{ANSIColors.RESET})")
|
||||
|
||||
# Memory operations
|
||||
memory_reads = stats.get('memory_reads', 0)
|
||||
memory_bytes = stats.get('memory_bytes_read', 0)
|
||||
if memory_bytes >= 1024 * 1024:
|
||||
memory_str = f"{memory_bytes / (1024 * 1024):.1f} MB"
|
||||
memory_str = f"{fmt(memory_bytes / (1024 * 1024))} MB"
|
||||
elif memory_bytes >= 1024:
|
||||
memory_str = f"{memory_bytes / 1024:.1f} KB"
|
||||
memory_str = f"{fmt(memory_bytes / 1024)} KB"
|
||||
else:
|
||||
memory_str = f"{memory_bytes} B"
|
||||
print(f" {ANSIColors.CYAN}Memory:{ANSIColors.RESET}")
|
||||
print(f" Read operations: {memory_reads:,} ({memory_str})")
|
||||
print(f" Read operations: {memory_reads:n} ({memory_str})")
|
||||
|
||||
# Stale invalidations
|
||||
stale_invalidations = stats.get('stale_cache_invalidations', 0)
|
||||
if stale_invalidations > 0:
|
||||
print(f" {ANSIColors.YELLOW}Stale cache invalidations: {stale_invalidations}{ANSIColors.RESET}")
|
||||
|
||||
def _print_binary_stats(self, collector):
|
||||
"""Print binary I/O encoding statistics."""
|
||||
try:
|
||||
stats = collector.get_stats()
|
||||
except (ValueError, RuntimeError):
|
||||
return # Collector closed or stats unavailable
|
||||
|
||||
print(f" {ANSIColors.CYAN}Binary Encoding:{ANSIColors.RESET}")
|
||||
|
||||
repeat_records = stats.get('repeat_records', 0)
|
||||
repeat_samples = stats.get('repeat_samples', 0)
|
||||
full_records = stats.get('full_records', 0)
|
||||
suffix_records = stats.get('suffix_records', 0)
|
||||
pop_push_records = stats.get('pop_push_records', 0)
|
||||
total_records = stats.get('total_records', 0)
|
||||
|
||||
if total_records > 0:
|
||||
repeat_pct = repeat_records / total_records * 100
|
||||
full_pct = full_records / total_records * 100
|
||||
suffix_pct = suffix_records / total_records * 100
|
||||
pop_push_pct = pop_push_records / total_records * 100
|
||||
else:
|
||||
repeat_pct = full_pct = suffix_pct = pop_push_pct = 0
|
||||
|
||||
print(f" Records: {total_records:,}")
|
||||
print(f" RLE repeat: {repeat_records:,} ({ANSIColors.GREEN}{repeat_pct:.1f}%{ANSIColors.RESET}) [{repeat_samples:,} samples]")
|
||||
print(f" Full stack: {full_records:,} ({full_pct:.1f}%)")
|
||||
print(f" Suffix match: {suffix_records:,} ({suffix_pct:.1f}%)")
|
||||
print(f" Pop-push: {pop_push_records:,} ({pop_push_pct:.1f}%)")
|
||||
|
||||
frames_written = stats.get('total_frames_written', 0)
|
||||
frames_saved = stats.get('frames_saved', 0)
|
||||
compression_pct = stats.get('frame_compression_pct', 0)
|
||||
|
||||
print(f" {ANSIColors.CYAN}Frame Efficiency:{ANSIColors.RESET}")
|
||||
print(f" Frames written: {frames_written:,}")
|
||||
print(f" Frames saved: {frames_saved:,} ({ANSIColors.GREEN}{compression_pct:.1f}%{ANSIColors.RESET})")
|
||||
|
||||
bytes_written = stats.get('bytes_written', 0)
|
||||
if bytes_written >= 1024 * 1024:
|
||||
bytes_str = f"{bytes_written / (1024 * 1024):.1f} MB"
|
||||
elif bytes_written >= 1024:
|
||||
bytes_str = f"{bytes_written / 1024:.1f} KB"
|
||||
else:
|
||||
bytes_str = f"{bytes_written} B"
|
||||
print(f" Bytes (pre-zstd): {bytes_str}")
|
||||
|
||||
|
||||
def _is_process_running(pid):
|
||||
if pid <= 0:
|
||||
return False
|
||||
if os.name == "posix":
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
except PermissionError:
|
||||
# EPERM means process exists but we can't signal it
|
||||
return True
|
||||
elif sys.platform == "win32":
|
||||
try:
|
||||
_remote_debugging.RemoteUnwinder(pid)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
raise ValueError(f"Unsupported platform: {sys.platform}")
|
||||
|
||||
|
||||
def sample(
|
||||
pid,
|
||||
|
|
|
|||
|
|
@ -18,21 +18,12 @@ class StackTraceCollector(Collector):
|
|||
self.sample_interval_usec = sample_interval_usec
|
||||
self.skip_idle = skip_idle
|
||||
|
||||
def collect(self, stack_frames, skip_idle=False):
|
||||
if stack_frames and hasattr(stack_frames[0], "awaited_by"):
|
||||
# Async-aware mode: process async task frames
|
||||
for frames, thread_id, task_id in self._iter_async_frames(stack_frames):
|
||||
if not frames:
|
||||
continue
|
||||
self.process_frames(frames, thread_id)
|
||||
else:
|
||||
# Sync-only mode
|
||||
for frames, thread_id in self._iter_all_frames(stack_frames, skip_idle=skip_idle):
|
||||
if not frames:
|
||||
continue
|
||||
self.process_frames(frames, thread_id)
|
||||
def collect(self, stack_frames, timestamps_us=None, skip_idle=False):
|
||||
weight = len(timestamps_us) if timestamps_us else 1
|
||||
for frames, thread_id in self._iter_stacks(stack_frames, skip_idle=skip_idle):
|
||||
self.process_frames(frames, thread_id, weight=weight)
|
||||
|
||||
def process_frames(self, frames, thread_id):
|
||||
def process_frames(self, frames, thread_id, weight=1):
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -41,13 +32,13 @@ class CollapsedStackCollector(StackTraceCollector):
|
|||
super().__init__(*args, **kwargs)
|
||||
self.stack_counter = collections.Counter()
|
||||
|
||||
def process_frames(self, frames, thread_id):
|
||||
def process_frames(self, frames, thread_id, weight=1):
|
||||
# Extract only (filename, lineno, funcname) - opcode not needed for collapsed stacks
|
||||
# frame is (filename, location, funcname, opcode)
|
||||
call_tree = tuple(
|
||||
(f[0], extract_lineno(f[1]), f[2]) for f in reversed(frames)
|
||||
)
|
||||
self.stack_counter[(call_tree, thread_id)] += 1
|
||||
self.stack_counter[(call_tree, thread_id)] += weight
|
||||
|
||||
def export(self, filename):
|
||||
lines = []
|
||||
|
|
@ -96,23 +87,26 @@ class FlamegraphCollector(StackTraceCollector):
|
|||
# Per-thread statistics
|
||||
self.per_thread_stats = {} # {thread_id: {has_gil, on_cpu, gil_requested, unknown, has_exception, total, gc_samples}}
|
||||
|
||||
def collect(self, stack_frames, skip_idle=False):
|
||||
def collect(self, stack_frames, timestamps_us=None, skip_idle=False):
|
||||
"""Override to track thread status statistics before processing frames."""
|
||||
# Increment sample count once per sample
|
||||
self._sample_count += 1
|
||||
# Weight is number of timestamps (samples with identical stack)
|
||||
weight = len(timestamps_us) if timestamps_us else 1
|
||||
|
||||
# Increment sample count by weight
|
||||
self._sample_count += weight
|
||||
|
||||
# Collect both aggregate and per-thread statistics using base method
|
||||
status_counts, has_gc_frame, per_thread_stats = self._collect_thread_status_stats(stack_frames)
|
||||
|
||||
# Merge aggregate status counts
|
||||
# Merge aggregate status counts (multiply by weight)
|
||||
for key in status_counts:
|
||||
self.thread_status_counts[key] += status_counts[key]
|
||||
self.thread_status_counts[key] += status_counts[key] * weight
|
||||
|
||||
# Update aggregate GC frame count
|
||||
if has_gc_frame:
|
||||
self.samples_with_gc_frames += 1
|
||||
self.samples_with_gc_frames += weight
|
||||
|
||||
# Merge per-thread statistics
|
||||
# Merge per-thread statistics (multiply by weight)
|
||||
for thread_id, stats in per_thread_stats.items():
|
||||
if thread_id not in self.per_thread_stats:
|
||||
self.per_thread_stats[thread_id] = {
|
||||
|
|
@ -125,10 +119,10 @@ class FlamegraphCollector(StackTraceCollector):
|
|||
"gc_samples": 0,
|
||||
}
|
||||
for key, value in stats.items():
|
||||
self.per_thread_stats[thread_id][key] += value
|
||||
self.per_thread_stats[thread_id][key] += value * weight
|
||||
|
||||
# Call parent collect to process frames
|
||||
super().collect(stack_frames, skip_idle=skip_idle)
|
||||
super().collect(stack_frames, timestamps_us, skip_idle=skip_idle)
|
||||
|
||||
def set_stats(self, sample_interval_usec, duration_sec, sample_rate,
|
||||
error_rate=None, missed_samples=None, mode=None):
|
||||
|
|
@ -311,7 +305,7 @@ class FlamegraphCollector(StackTraceCollector):
|
|||
"opcode_mapping": opcode_mapping
|
||||
}
|
||||
|
||||
def process_frames(self, frames, thread_id):
|
||||
def process_frames(self, frames, thread_id, weight=1):
|
||||
"""Process stack frames into flamegraph tree structure.
|
||||
|
||||
Args:
|
||||
|
|
@ -319,10 +313,11 @@ class FlamegraphCollector(StackTraceCollector):
|
|||
leaf-to-root order. location is (lineno, end_lineno, col_offset, end_col_offset).
|
||||
opcode is None if not gathered.
|
||||
thread_id: Thread ID for this stack trace
|
||||
weight: Number of samples this stack represents (for batched RLE)
|
||||
"""
|
||||
# Reverse to root->leaf order for tree building
|
||||
self._root["samples"] += 1
|
||||
self._total_samples += 1
|
||||
self._root["samples"] += weight
|
||||
self._total_samples += weight
|
||||
self._root["threads"].add(thread_id)
|
||||
self._all_threads.add(thread_id)
|
||||
|
||||
|
|
@ -336,11 +331,11 @@ class FlamegraphCollector(StackTraceCollector):
|
|||
if node is None:
|
||||
node = {"samples": 0, "children": {}, "threads": set(), "opcodes": collections.Counter()}
|
||||
current["children"][func] = node
|
||||
node["samples"] += 1
|
||||
node["samples"] += weight
|
||||
node["threads"].add(thread_id)
|
||||
|
||||
if opcode is not None:
|
||||
node["opcodes"][opcode] += 1
|
||||
node["opcodes"][opcode] += weight
|
||||
|
||||
current = node
|
||||
|
||||
|
|
|
|||
16
Lib/pydoc.py
16
Lib/pydoc.py
|
|
@ -483,10 +483,20 @@ class Doc:
|
|||
|
||||
if (self._is_stdlib_module(object, basedir) and
|
||||
object.__name__ not in ('xml.etree', 'test.test_pydoc.pydoc_mod')):
|
||||
if docloc.startswith(("http://", "https://")):
|
||||
docloc = "{}/{}.html".format(docloc.rstrip("/"), object.__name__.lower())
|
||||
|
||||
try:
|
||||
from pydoc_data import module_docs
|
||||
except ImportError:
|
||||
module_docs = None
|
||||
|
||||
if module_docs and object.__name__ in module_docs.module_docs:
|
||||
doc_name = module_docs.module_docs[object.__name__]
|
||||
if docloc.startswith(("http://", "https://")):
|
||||
docloc = "{}/{}".format(docloc.rstrip("/"), doc_name)
|
||||
else:
|
||||
docloc = os.path.join(docloc, doc_name)
|
||||
else:
|
||||
docloc = os.path.join(docloc, object.__name__.lower() + ".html")
|
||||
docloc = None
|
||||
else:
|
||||
docloc = None
|
||||
return docloc
|
||||
|
|
|
|||
321
Lib/pydoc_data/module_docs.py
generated
Normal file
321
Lib/pydoc_data/module_docs.py
generated
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
# Autogenerated by Sphinx on Sun Oct 12 12:02:22 2025
|
||||
# as part of the release process.
|
||||
|
||||
module_docs = {
|
||||
'__future__': '__future__#module-__future__',
|
||||
'__main__': '__main__#module-__main__',
|
||||
'_thread': '_thread#module-_thread',
|
||||
'_tkinter': 'tkinter#module-_tkinter',
|
||||
'abc': 'abc#module-abc',
|
||||
'aifc': 'aifc#module-aifc',
|
||||
'annotationlib': 'annotationlib#module-annotationlib',
|
||||
'argparse': 'argparse#module-argparse',
|
||||
'array': 'array#module-array',
|
||||
'ast': 'ast#module-ast',
|
||||
'asynchat': 'asynchat#module-asynchat',
|
||||
'asyncio': 'asyncio#module-asyncio',
|
||||
'asyncore': 'asyncore#module-asyncore',
|
||||
'atexit': 'atexit#module-atexit',
|
||||
'audioop': 'audioop#module-audioop',
|
||||
'base64': 'base64#module-base64',
|
||||
'bdb': 'bdb#module-bdb',
|
||||
'binascii': 'binascii#module-binascii',
|
||||
'bisect': 'bisect#module-bisect',
|
||||
'builtins': 'builtins#module-builtins',
|
||||
'bz2': 'bz2#module-bz2',
|
||||
'cProfile': 'profile#module-cProfile',
|
||||
'calendar': 'calendar#module-calendar',
|
||||
'cgi': 'cgi#module-cgi',
|
||||
'cgitb': 'cgitb#module-cgitb',
|
||||
'chunk': 'chunk#module-chunk',
|
||||
'cmath': 'cmath#module-cmath',
|
||||
'cmd': 'cmd#module-cmd',
|
||||
'code': 'code#module-code',
|
||||
'codecs': 'codecs#module-codecs',
|
||||
'codeop': 'codeop#module-codeop',
|
||||
'collections': 'collections#module-collections',
|
||||
'collections.abc': 'collections.abc#module-collections.abc',
|
||||
'colorsys': 'colorsys#module-colorsys',
|
||||
'compileall': 'compileall#module-compileall',
|
||||
'compression': 'compression#module-compression',
|
||||
'compression.zstd': 'compression.zstd#module-compression.zstd',
|
||||
'concurrent.futures': 'concurrent.futures#module-concurrent.futures',
|
||||
'concurrent.interpreters': 'concurrent.interpreters#module-concurrent.interpreters',
|
||||
'configparser': 'configparser#module-configparser',
|
||||
'contextlib': 'contextlib#module-contextlib',
|
||||
'contextvars': 'contextvars#module-contextvars',
|
||||
'copy': 'copy#module-copy',
|
||||
'copyreg': 'copyreg#module-copyreg',
|
||||
'crypt': 'crypt#module-crypt',
|
||||
'csv': 'csv#module-csv',
|
||||
'ctypes': 'ctypes#module-ctypes',
|
||||
'curses': 'curses#module-curses',
|
||||
'curses.ascii': 'curses.ascii#module-curses.ascii',
|
||||
'curses.panel': 'curses.panel#module-curses.panel',
|
||||
'curses.textpad': 'curses#module-curses.textpad',
|
||||
'dataclasses': 'dataclasses#module-dataclasses',
|
||||
'datetime': 'datetime#module-datetime',
|
||||
'dbm': 'dbm#module-dbm',
|
||||
'dbm.dumb': 'dbm#module-dbm.dumb',
|
||||
'dbm.gnu': 'dbm#module-dbm.gnu',
|
||||
'dbm.ndbm': 'dbm#module-dbm.ndbm',
|
||||
'dbm.sqlite3': 'dbm#module-dbm.sqlite3',
|
||||
'decimal': 'decimal#module-decimal',
|
||||
'difflib': 'difflib#module-difflib',
|
||||
'dis': 'dis#module-dis',
|
||||
'distutils': 'distutils#module-distutils',
|
||||
'doctest': 'doctest#module-doctest',
|
||||
'email': 'email#module-email',
|
||||
'email.charset': 'email.charset#module-email.charset',
|
||||
'email.contentmanager': 'email.contentmanager#module-email.contentmanager',
|
||||
'email.encoders': 'email.encoders#module-email.encoders',
|
||||
'email.errors': 'email.errors#module-email.errors',
|
||||
'email.generator': 'email.generator#module-email.generator',
|
||||
'email.header': 'email.header#module-email.header',
|
||||
'email.headerregistry': 'email.headerregistry#module-email.headerregistry',
|
||||
'email.iterators': 'email.iterators#module-email.iterators',
|
||||
'email.message': 'email.message#module-email.message',
|
||||
'email.mime': 'email.mime#module-email.mime',
|
||||
'email.mime.application': 'email.mime#module-email.mime.application',
|
||||
'email.mime.audio': 'email.mime#module-email.mime.audio',
|
||||
'email.mime.base': 'email.mime#module-email.mime.base',
|
||||
'email.mime.image': 'email.mime#module-email.mime.image',
|
||||
'email.mime.message': 'email.mime#module-email.mime.message',
|
||||
'email.mime.multipart': 'email.mime#module-email.mime.multipart',
|
||||
'email.mime.nonmultipart': 'email.mime#module-email.mime.nonmultipart',
|
||||
'email.mime.text': 'email.mime#module-email.mime.text',
|
||||
'email.parser': 'email.parser#module-email.parser',
|
||||
'email.policy': 'email.policy#module-email.policy',
|
||||
'email.utils': 'email.utils#module-email.utils',
|
||||
'encodings': 'codecs#module-encodings',
|
||||
'encodings.idna': 'codecs#module-encodings.idna',
|
||||
'encodings.mbcs': 'codecs#module-encodings.mbcs',
|
||||
'encodings.utf_8_sig': 'codecs#module-encodings.utf_8_sig',
|
||||
'ensurepip': 'ensurepip#module-ensurepip',
|
||||
'enum': 'enum#module-enum',
|
||||
'errno': 'errno#module-errno',
|
||||
'faulthandler': 'faulthandler#module-faulthandler',
|
||||
'fcntl': 'fcntl#module-fcntl',
|
||||
'filecmp': 'filecmp#module-filecmp',
|
||||
'fileinput': 'fileinput#module-fileinput',
|
||||
'fnmatch': 'fnmatch#module-fnmatch',
|
||||
'fractions': 'fractions#module-fractions',
|
||||
'ftplib': 'ftplib#module-ftplib',
|
||||
'functools': 'functools#module-functools',
|
||||
'gc': 'gc#module-gc',
|
||||
'getopt': 'getopt#module-getopt',
|
||||
'getpass': 'getpass#module-getpass',
|
||||
'gettext': 'gettext#module-gettext',
|
||||
'glob': 'glob#module-glob',
|
||||
'graphlib': 'graphlib#module-graphlib',
|
||||
'grp': 'grp#module-grp',
|
||||
'gzip': 'gzip#module-gzip',
|
||||
'hashlib': 'hashlib#module-hashlib',
|
||||
'heapq': 'heapq#module-heapq',
|
||||
'hmac': 'hmac#module-hmac',
|
||||
'html': 'html#module-html',
|
||||
'html.entities': 'html.entities#module-html.entities',
|
||||
'html.parser': 'html.parser#module-html.parser',
|
||||
'http': 'http#module-http',
|
||||
'http.client': 'http.client#module-http.client',
|
||||
'http.cookiejar': 'http.cookiejar#module-http.cookiejar',
|
||||
'http.cookies': 'http.cookies#module-http.cookies',
|
||||
'http.server': 'http.server#module-http.server',
|
||||
'idlelib': 'idle#module-idlelib',
|
||||
'imaplib': 'imaplib#module-imaplib',
|
||||
'imghdr': 'imghdr#module-imghdr',
|
||||
'imp': 'imp#module-imp',
|
||||
'importlib': 'importlib#module-importlib',
|
||||
'importlib.abc': 'importlib#module-importlib.abc',
|
||||
'importlib.machinery': 'importlib#module-importlib.machinery',
|
||||
'importlib.metadata': 'importlib.metadata#module-importlib.metadata',
|
||||
'importlib.resources': 'importlib.resources#module-importlib.resources',
|
||||
'importlib.resources.abc': 'importlib.resources.abc#module-importlib.resources.abc',
|
||||
'importlib.util': 'importlib#module-importlib.util',
|
||||
'inspect': 'inspect#module-inspect',
|
||||
'io': 'io#module-io',
|
||||
'ipaddress': 'ipaddress#module-ipaddress',
|
||||
'itertools': 'itertools#module-itertools',
|
||||
'json': 'json#module-json',
|
||||
'json.tool': 'json#module-json.tool',
|
||||
'keyword': 'keyword#module-keyword',
|
||||
'linecache': 'linecache#module-linecache',
|
||||
'locale': 'locale#module-locale',
|
||||
'logging': 'logging#module-logging',
|
||||
'logging.config': 'logging.config#module-logging.config',
|
||||
'logging.handlers': 'logging.handlers#module-logging.handlers',
|
||||
'lzma': 'lzma#module-lzma',
|
||||
'mailbox': 'mailbox#module-mailbox',
|
||||
'mailcap': 'mailcap#module-mailcap',
|
||||
'marshal': 'marshal#module-marshal',
|
||||
'math': 'math#module-math',
|
||||
'mimetypes': 'mimetypes#module-mimetypes',
|
||||
'mmap': 'mmap#module-mmap',
|
||||
'modulefinder': 'modulefinder#module-modulefinder',
|
||||
'msilib': 'msilib#module-msilib',
|
||||
'msvcrt': 'msvcrt#module-msvcrt',
|
||||
'multiprocessing': 'multiprocessing#module-multiprocessing',
|
||||
'multiprocessing.connection': 'multiprocessing#module-multiprocessing.connection',
|
||||
'multiprocessing.dummy': 'multiprocessing#module-multiprocessing.dummy',
|
||||
'multiprocessing.managers': 'multiprocessing#module-multiprocessing.managers',
|
||||
'multiprocessing.pool': 'multiprocessing#module-multiprocessing.pool',
|
||||
'multiprocessing.shared_memory': 'multiprocessing.shared_memory#module-multiprocessing.shared_memory',
|
||||
'multiprocessing.sharedctypes': 'multiprocessing#module-multiprocessing.sharedctypes',
|
||||
'netrc': 'netrc#module-netrc',
|
||||
'nis': 'nis#module-nis',
|
||||
'nntplib': 'nntplib#module-nntplib',
|
||||
'numbers': 'numbers#module-numbers',
|
||||
'operator': 'operator#module-operator',
|
||||
'optparse': 'optparse#module-optparse',
|
||||
'os': 'os#module-os',
|
||||
'os.path': 'os.path#module-os.path',
|
||||
'ossaudiodev': 'ossaudiodev#module-ossaudiodev',
|
||||
'pathlib': 'pathlib#module-pathlib',
|
||||
'pathlib.types': 'pathlib#module-pathlib.types',
|
||||
'pdb': 'pdb#module-pdb',
|
||||
'pickle': 'pickle#module-pickle',
|
||||
'pickletools': 'pickletools#module-pickletools',
|
||||
'pipes': 'pipes#module-pipes',
|
||||
'pkgutil': 'pkgutil#module-pkgutil',
|
||||
'platform': 'platform#module-platform',
|
||||
'plistlib': 'plistlib#module-plistlib',
|
||||
'poplib': 'poplib#module-poplib',
|
||||
'posix': 'posix#module-posix',
|
||||
'pprint': 'pprint#module-pprint',
|
||||
'profile': 'profile#module-profile',
|
||||
'profiling.sampling': 'profile#module-profiling.sampling',
|
||||
'pstats': 'profile#module-pstats',
|
||||
'pty': 'pty#module-pty',
|
||||
'pwd': 'pwd#module-pwd',
|
||||
'py_compile': 'py_compile#module-py_compile',
|
||||
'pyclbr': 'pyclbr#module-pyclbr',
|
||||
'pydoc': 'pydoc#module-pydoc',
|
||||
'queue': 'queue#module-queue',
|
||||
'quopri': 'quopri#module-quopri',
|
||||
'random': 'random#module-random',
|
||||
're': 're#module-re',
|
||||
'readline': 'readline#module-readline',
|
||||
'reprlib': 'reprlib#module-reprlib',
|
||||
'resource': 'resource#module-resource',
|
||||
'rlcompleter': 'rlcompleter#module-rlcompleter',
|
||||
'runpy': 'runpy#module-runpy',
|
||||
'sched': 'sched#module-sched',
|
||||
'secrets': 'secrets#module-secrets',
|
||||
'select': 'select#module-select',
|
||||
'selectors': 'selectors#module-selectors',
|
||||
'shelve': 'shelve#module-shelve',
|
||||
'shlex': 'shlex#module-shlex',
|
||||
'shutil': 'shutil#module-shutil',
|
||||
'signal': 'signal#module-signal',
|
||||
'site': 'site#module-site',
|
||||
'sitecustomize': 'site#module-sitecustomize',
|
||||
'smtpd': 'smtpd#module-smtpd',
|
||||
'smtplib': 'smtplib#module-smtplib',
|
||||
'sndhdr': 'sndhdr#module-sndhdr',
|
||||
'socket': 'socket#module-socket',
|
||||
'socketserver': 'socketserver#module-socketserver',
|
||||
'spwd': 'spwd#module-spwd',
|
||||
'sqlite3': 'sqlite3#module-sqlite3',
|
||||
'ssl': 'ssl#module-ssl',
|
||||
'stat': 'stat#module-stat',
|
||||
'statistics': 'statistics#module-statistics',
|
||||
'string': 'string#module-string',
|
||||
'string.templatelib': 'string.templatelib#module-string.templatelib',
|
||||
'stringprep': 'stringprep#module-stringprep',
|
||||
'struct': 'struct#module-struct',
|
||||
'subprocess': 'subprocess#module-subprocess',
|
||||
'sunau': 'sunau#module-sunau',
|
||||
'symtable': 'symtable#module-symtable',
|
||||
'sys': 'sys#module-sys',
|
||||
'sys.monitoring': 'sys.monitoring#module-sys.monitoring',
|
||||
'sysconfig': 'sysconfig#module-sysconfig',
|
||||
'syslog': 'syslog#module-syslog',
|
||||
'tabnanny': 'tabnanny#module-tabnanny',
|
||||
'tarfile': 'tarfile#module-tarfile',
|
||||
'telnetlib': 'telnetlib#module-telnetlib',
|
||||
'tempfile': 'tempfile#module-tempfile',
|
||||
'termios': 'termios#module-termios',
|
||||
'test': 'test#module-test',
|
||||
'test.regrtest': 'test#module-test.regrtest',
|
||||
'test.support': 'test#module-test.support',
|
||||
'test.support.bytecode_helper': 'test#module-test.support.bytecode_helper',
|
||||
'test.support.import_helper': 'test#module-test.support.import_helper',
|
||||
'test.support.os_helper': 'test#module-test.support.os_helper',
|
||||
'test.support.script_helper': 'test#module-test.support.script_helper',
|
||||
'test.support.socket_helper': 'test#module-test.support.socket_helper',
|
||||
'test.support.threading_helper': 'test#module-test.support.threading_helper',
|
||||
'test.support.warnings_helper': 'test#module-test.support.warnings_helper',
|
||||
'textwrap': 'textwrap#module-textwrap',
|
||||
'threading': 'threading#module-threading',
|
||||
'time': 'time#module-time',
|
||||
'timeit': 'timeit#module-timeit',
|
||||
'tkinter': 'tkinter#module-tkinter',
|
||||
'tkinter.colorchooser': 'tkinter.colorchooser#module-tkinter.colorchooser',
|
||||
'tkinter.commondialog': 'dialog#module-tkinter.commondialog',
|
||||
'tkinter.dnd': 'tkinter.dnd#module-tkinter.dnd',
|
||||
'tkinter.filedialog': 'dialog#module-tkinter.filedialog',
|
||||
'tkinter.font': 'tkinter.font#module-tkinter.font',
|
||||
'tkinter.messagebox': 'tkinter.messagebox#module-tkinter.messagebox',
|
||||
'tkinter.scrolledtext': 'tkinter.scrolledtext#module-tkinter.scrolledtext',
|
||||
'tkinter.simpledialog': 'dialog#module-tkinter.simpledialog',
|
||||
'tkinter.ttk': 'tkinter.ttk#module-tkinter.ttk',
|
||||
'token': 'token#module-token',
|
||||
'tokenize': 'tokenize#module-tokenize',
|
||||
'tomllib': 'tomllib#module-tomllib',
|
||||
'trace': 'trace#module-trace',
|
||||
'traceback': 'traceback#module-traceback',
|
||||
'tracemalloc': 'tracemalloc#module-tracemalloc',
|
||||
'tty': 'tty#module-tty',
|
||||
'turtle': 'turtle#module-turtle',
|
||||
'turtledemo': 'turtle#module-turtledemo',
|
||||
'types': 'types#module-types',
|
||||
'typing': 'typing#module-typing',
|
||||
'unicodedata': 'unicodedata#module-unicodedata',
|
||||
'unittest': 'unittest#module-unittest',
|
||||
'unittest.mock': 'unittest.mock#module-unittest.mock',
|
||||
'urllib': 'urllib#module-urllib',
|
||||
'urllib.error': 'urllib.error#module-urllib.error',
|
||||
'urllib.parse': 'urllib.parse#module-urllib.parse',
|
||||
'urllib.request': 'urllib.request#module-urllib.request',
|
||||
'urllib.response': 'urllib.request#module-urllib.response',
|
||||
'urllib.robotparser': 'urllib.robotparser#module-urllib.robotparser',
|
||||
'usercustomize': 'site#module-usercustomize',
|
||||
'uu': 'uu#module-uu',
|
||||
'uuid': 'uuid#module-uuid',
|
||||
'venv': 'venv#module-venv',
|
||||
'warnings': 'warnings#module-warnings',
|
||||
'wave': 'wave#module-wave',
|
||||
'weakref': 'weakref#module-weakref',
|
||||
'webbrowser': 'webbrowser#module-webbrowser',
|
||||
'winreg': 'winreg#module-winreg',
|
||||
'winsound': 'winsound#module-winsound',
|
||||
'wsgiref': 'wsgiref#module-wsgiref',
|
||||
'wsgiref.handlers': 'wsgiref#module-wsgiref.handlers',
|
||||
'wsgiref.headers': 'wsgiref#module-wsgiref.headers',
|
||||
'wsgiref.simple_server': 'wsgiref#module-wsgiref.simple_server',
|
||||
'wsgiref.types': 'wsgiref#module-wsgiref.types',
|
||||
'wsgiref.util': 'wsgiref#module-wsgiref.util',
|
||||
'wsgiref.validate': 'wsgiref#module-wsgiref.validate',
|
||||
'xdrlib': 'xdrlib#module-xdrlib',
|
||||
'xml': 'xml#module-xml',
|
||||
'xml.dom': 'xml.dom#module-xml.dom',
|
||||
'xml.dom.minidom': 'xml.dom.minidom#module-xml.dom.minidom',
|
||||
'xml.dom.pulldom': 'xml.dom.pulldom#module-xml.dom.pulldom',
|
||||
'xml.etree.ElementInclude': 'xml.etree.elementtree#module-xml.etree.ElementInclude',
|
||||
'xml.etree.ElementTree': 'xml.etree.elementtree#module-xml.etree.ElementTree',
|
||||
'xml.parsers.expat': 'pyexpat#module-xml.parsers.expat',
|
||||
'xml.parsers.expat.errors': 'pyexpat#module-xml.parsers.expat.errors',
|
||||
'xml.parsers.expat.model': 'pyexpat#module-xml.parsers.expat.model',
|
||||
'xml.sax': 'xml.sax#module-xml.sax',
|
||||
'xml.sax.handler': 'xml.sax.handler#module-xml.sax.handler',
|
||||
'xml.sax.saxutils': 'xml.sax.utils#module-xml.sax.saxutils',
|
||||
'xml.sax.xmlreader': 'xml.sax.reader#module-xml.sax.xmlreader',
|
||||
'xmlrpc': 'xmlrpc#module-xmlrpc',
|
||||
'xmlrpc.client': 'xmlrpc.client#module-xmlrpc.client',
|
||||
'xmlrpc.server': 'xmlrpc.server#module-xmlrpc.server',
|
||||
'zipapp': 'zipapp#module-zipapp',
|
||||
'zipfile': 'zipfile#module-zipfile',
|
||||
'zipimport': 'zipimport#module-zipimport',
|
||||
'zlib': 'zlib#module-zlib',
|
||||
'zoneinfo': 'zoneinfo#module-zoneinfo',
|
||||
}
|
||||
|
|
@ -135,6 +135,19 @@ class GeneralTest(unittest.TestCase):
|
|||
finally:
|
||||
atexit.unregister(func)
|
||||
|
||||
def test_eq_unregister_clear(self):
|
||||
# Issue #112127: callback's __eq__ may call unregister or _clear
|
||||
class Evil:
|
||||
def __eq__(self, other):
|
||||
action(other)
|
||||
return NotImplemented
|
||||
|
||||
for action in atexit.unregister, lambda o: atexit._clear():
|
||||
with self.subTest(action=action):
|
||||
atexit.register(lambda: None)
|
||||
atexit.unregister(Evil())
|
||||
atexit._clear()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -2060,6 +2060,37 @@ class ByteArrayTest(BaseBytesTest, unittest.TestCase):
|
|||
self.assertEqual(instance.ba[0], ord("?"), "Assigned bytearray not altered")
|
||||
self.assertEqual(instance.new_ba, bytearray(0x180), "Wrong object altered")
|
||||
|
||||
def test_search_methods_reentrancy_raises_buffererror(self):
|
||||
# gh-142560: Raise BufferError if buffer mutates during search arg conversion.
|
||||
class Evil:
|
||||
def __init__(self, ba):
|
||||
self.ba = ba
|
||||
def __buffer__(self, flags):
|
||||
self.ba.clear()
|
||||
return memoryview(self.ba)
|
||||
def __release_buffer__(self, view: memoryview) -> None:
|
||||
view.release()
|
||||
def __index__(self):
|
||||
self.ba.clear()
|
||||
return ord("A")
|
||||
|
||||
def make_case():
|
||||
ba = bytearray(b"A")
|
||||
return ba, Evil(ba)
|
||||
|
||||
for name in ("find", "count", "index", "rindex", "rfind"):
|
||||
ba, evil = make_case()
|
||||
with self.subTest(name):
|
||||
with self.assertRaises(BufferError):
|
||||
getattr(ba, name)(evil)
|
||||
|
||||
ba, evil = make_case()
|
||||
with self.assertRaises(BufferError):
|
||||
evil in ba
|
||||
with self.assertRaises(BufferError):
|
||||
ba.split(evil)
|
||||
with self.assertRaises(BufferError):
|
||||
ba.rsplit(evil)
|
||||
|
||||
class AssortedBytesTest(unittest.TestCase):
|
||||
#
|
||||
|
|
|
|||
|
|
@ -1877,6 +1877,26 @@ class TestUopsOptimization(unittest.TestCase):
|
|||
self.assertNotIn("_GUARD_TOS_UNICODE", uops)
|
||||
self.assertIn("_BINARY_OP_ADD_UNICODE", uops)
|
||||
|
||||
def test_binary_op_subscr_str_int(self):
|
||||
def testfunc(n):
|
||||
x = 0
|
||||
s = "hello"
|
||||
for _ in range(n):
|
||||
c = s[1] # _BINARY_OP_SUBSCR_STR_INT
|
||||
if c == 'e':
|
||||
x += 1
|
||||
return x
|
||||
|
||||
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
|
||||
self.assertEqual(res, TIER2_THRESHOLD)
|
||||
self.assertIsNotNone(ex)
|
||||
uops = get_opnames(ex)
|
||||
self.assertIn("_BINARY_OP_SUBSCR_STR_INT", uops)
|
||||
self.assertIn("_COMPARE_OP_STR", uops)
|
||||
self.assertIn("_POP_TOP_NOP", uops)
|
||||
self.assertNotIn("_POP_TOP", uops)
|
||||
self.assertNotIn("_POP_TOP_INT", uops)
|
||||
|
||||
def test_call_type_1_guards_removed(self):
|
||||
def testfunc(n):
|
||||
x = 0
|
||||
|
|
@ -2473,6 +2493,46 @@ class TestUopsOptimization(unittest.TestCase):
|
|||
uops = get_opnames(ex)
|
||||
self.assertIn("_POP_TOP_NOP", uops)
|
||||
|
||||
def test_load_attr_instance_value(self):
|
||||
def testfunc(n):
|
||||
class C():
|
||||
pass
|
||||
c = C()
|
||||
c.x = n
|
||||
x = 0
|
||||
for _ in range(n):
|
||||
x = c.x
|
||||
return x
|
||||
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
|
||||
self.assertEqual(res, TIER2_THRESHOLD)
|
||||
self.assertIsNotNone(ex)
|
||||
uops = get_opnames(ex)
|
||||
|
||||
self.assertIn("_LOAD_ATTR_INSTANCE_VALUE", uops)
|
||||
self.assertNotIn("_POP_TOP", uops)
|
||||
self.assertIn("_POP_TOP_NOP", uops)
|
||||
|
||||
def test_load_attr_with_hint(self):
|
||||
def testfunc(n):
|
||||
class C:
|
||||
pass
|
||||
c = C()
|
||||
c.x = 42
|
||||
for i in range(_testinternalcapi.SHARED_KEYS_MAX_SIZE - 1):
|
||||
setattr(c, f"_{i}", None)
|
||||
x = 0
|
||||
for i in range(n):
|
||||
x += c.x
|
||||
return x
|
||||
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
|
||||
self.assertEqual(res, 42 * TIER2_THRESHOLD)
|
||||
self.assertIsNotNone(ex)
|
||||
uops = get_opnames(ex)
|
||||
|
||||
self.assertIn("_LOAD_ATTR_WITH_HINT", uops)
|
||||
self.assertNotIn("_POP_TOP", uops)
|
||||
self.assertIn("_POP_TOP_NOP", uops)
|
||||
|
||||
def test_int_add_op_refcount_elimination(self):
|
||||
def testfunc(n):
|
||||
c = 1
|
||||
|
|
@ -2533,6 +2593,22 @@ class TestUopsOptimization(unittest.TestCase):
|
|||
self.assertIn("_POP_TOP_NOP", uops)
|
||||
self.assertNotIn("_POP_TOP", uops)
|
||||
|
||||
def test_unicode_add_op_refcount_elimination(self):
|
||||
def testfunc(n):
|
||||
c = "a"
|
||||
res = ""
|
||||
for _ in range(n):
|
||||
res = c + c
|
||||
return res
|
||||
|
||||
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
|
||||
self.assertEqual(res, "aa")
|
||||
self.assertIsNotNone(ex)
|
||||
uops = get_opnames(ex)
|
||||
self.assertIn("_BINARY_OP_ADD_UNICODE", uops)
|
||||
self.assertIn("_POP_TOP_NOP", uops)
|
||||
self.assertNotIn("_POP_TOP", uops)
|
||||
|
||||
def test_remove_guard_for_slice_list(self):
|
||||
def f(n):
|
||||
for i in range(n):
|
||||
|
|
@ -2593,6 +2669,26 @@ class TestUopsOptimization(unittest.TestCase):
|
|||
self.assertNotIn("_POP_TOP", uops)
|
||||
self.assertIn("_POP_TOP_NOP", uops)
|
||||
|
||||
def test_store_attr_with_hint(self):
|
||||
def testfunc(n):
|
||||
class C:
|
||||
pass
|
||||
c = C()
|
||||
for i in range(_testinternalcapi.SHARED_KEYS_MAX_SIZE - 1):
|
||||
setattr(c, f"_{i}", None)
|
||||
|
||||
for i in range(n):
|
||||
c.x = i
|
||||
return c.x
|
||||
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
|
||||
self.assertEqual(res, TIER2_THRESHOLD - 1)
|
||||
self.assertIsNotNone(ex)
|
||||
uops = get_opnames(ex)
|
||||
|
||||
self.assertIn("_STORE_ATTR_WITH_HINT", uops)
|
||||
self.assertNotIn("_POP_TOP", uops)
|
||||
self.assertIn("_POP_TOP_NOP", uops)
|
||||
|
||||
def test_store_subscr_int(self):
|
||||
def testfunc(n):
|
||||
l = [0, 0, 0, 0]
|
||||
|
|
@ -2933,6 +3029,74 @@ class TestUopsOptimization(unittest.TestCase):
|
|||
for _ in range(TIER2_THRESHOLD+1):
|
||||
obj.attr = EvilAttr(obj.__dict__)
|
||||
|
||||
def test_promoted_global_refcount_eliminated(self):
|
||||
result = script_helper.run_python_until_end('-c', textwrap.dedent("""
|
||||
import _testinternalcapi
|
||||
import opcode
|
||||
import _opcode
|
||||
|
||||
def get_first_executor(func):
|
||||
code = func.__code__
|
||||
co_code = code.co_code
|
||||
for i in range(0, len(co_code), 2):
|
||||
try:
|
||||
return _opcode.get_executor(code, i)
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_opnames(ex):
|
||||
return {item[0] for item in ex}
|
||||
|
||||
|
||||
def testfunc(n):
|
||||
y = []
|
||||
for i in range(n):
|
||||
x = tuple(y)
|
||||
return x
|
||||
|
||||
testfunc(_testinternalcapi.TIER2_THRESHOLD)
|
||||
|
||||
ex = get_first_executor(testfunc)
|
||||
assert ex is not None
|
||||
uops = get_opnames(ex)
|
||||
assert "_LOAD_GLOBAL_BUILTIN" not in uops
|
||||
assert "_LOAD_CONST_INLINE_BORROW" in uops
|
||||
assert "_POP_TOP_NOP" in uops
|
||||
assert "_POP_TOP" not in uops
|
||||
"""), PYTHON_JIT="1")
|
||||
self.assertEqual(result[0].rc, 0, result)
|
||||
|
||||
def test_constant_fold_tuple(self):
|
||||
def testfunc(n):
|
||||
for _ in range(n):
|
||||
t = (1,)
|
||||
p = len(t)
|
||||
|
||||
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
|
||||
self.assertIsNotNone(ex)
|
||||
uops = get_opnames(ex)
|
||||
|
||||
self.assertNotIn("_CALL_LEN", uops)
|
||||
|
||||
def test_binary_subscr_list_int(self):
|
||||
def testfunc(n):
|
||||
l = [1]
|
||||
x = 0
|
||||
for _ in range(n):
|
||||
y = l[0]
|
||||
x += y
|
||||
return x
|
||||
|
||||
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
|
||||
self.assertEqual(res, TIER2_THRESHOLD)
|
||||
self.assertIsNotNone(ex)
|
||||
uops = get_opnames(ex)
|
||||
|
||||
self.assertIn("_BINARY_OP_SUBSCR_LIST_INT", uops)
|
||||
self.assertNotIn("_POP_TOP", uops)
|
||||
self.assertNotIn("_POP_TOP_INT", uops)
|
||||
self.assertIn("_POP_TOP_NOP", uops)
|
||||
|
||||
def global_identity(x):
|
||||
return x
|
||||
|
|
|
|||
|
|
@ -2215,6 +2215,16 @@ class SectionlessTestCase(unittest.TestCase):
|
|||
cfg.add_section(configparser.UNNAMED_SECTION)
|
||||
cfg.set(configparser.UNNAMED_SECTION, 'a', '1')
|
||||
self.assertEqual('1', cfg[configparser.UNNAMED_SECTION]['a'])
|
||||
output = io.StringIO()
|
||||
cfg.write(output)
|
||||
self.assertEqual(output.getvalue(), 'a = 1\n\n')
|
||||
|
||||
cfg = configparser.ConfigParser(allow_unnamed_section=True)
|
||||
cfg[configparser.UNNAMED_SECTION] = {'a': '1'}
|
||||
self.assertEqual('1', cfg[configparser.UNNAMED_SECTION]['a'])
|
||||
output = io.StringIO()
|
||||
cfg.write(output)
|
||||
self.assertEqual(output.getvalue(), 'a = 1\n\n')
|
||||
|
||||
def test_disabled_error(self):
|
||||
with self.assertRaises(configparser.MissingSectionHeaderError):
|
||||
|
|
@ -2223,6 +2233,9 @@ class SectionlessTestCase(unittest.TestCase):
|
|||
with self.assertRaises(configparser.UnnamedSectionDisabledError):
|
||||
configparser.ConfigParser().add_section(configparser.UNNAMED_SECTION)
|
||||
|
||||
with self.assertRaises(configparser.UnnamedSectionDisabledError):
|
||||
configparser.ConfigParser()[configparser.UNNAMED_SECTION] = {'a': '1'}
|
||||
|
||||
def test_multiple_configs(self):
|
||||
cfg = configparser.ConfigParser(allow_unnamed_section=True)
|
||||
cfg.read_string('a = 1')
|
||||
|
|
|
|||
|
|
@ -186,5 +186,23 @@ class TestDefaultDict(unittest.TestCase):
|
|||
with self.assertRaises(TypeError):
|
||||
i |= None
|
||||
|
||||
def test_factory_conflict_with_set_value(self):
|
||||
key = "conflict_test"
|
||||
count = 0
|
||||
|
||||
def default_factory():
|
||||
nonlocal count
|
||||
count += 1
|
||||
local_count = count
|
||||
if count == 1:
|
||||
test_dict[key]
|
||||
return local_count
|
||||
|
||||
test_dict = defaultdict(default_factory)
|
||||
|
||||
self.assertEqual(count, 0)
|
||||
self.assertEqual(test_dict[key], 2)
|
||||
self.assertEqual(count, 2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -49,3 +49,74 @@ class TestFTGenerators(TestCase):
|
|||
self.concurrent_write_with_func(func=set_gen_name)
|
||||
with self.subTest(func=set_gen_qualname):
|
||||
self.concurrent_write_with_func(func=set_gen_qualname)
|
||||
|
||||
def test_concurrent_send(self):
|
||||
def gen():
|
||||
yield 1
|
||||
yield 2
|
||||
yield 3
|
||||
yield 4
|
||||
yield 5
|
||||
|
||||
def run_test(drive_generator):
|
||||
g = gen()
|
||||
values = []
|
||||
threading_helper.run_concurrently(drive_generator, self.NUM_THREADS, args=(g, values,))
|
||||
self.assertEqual(sorted(values), [1, 2, 3, 4, 5])
|
||||
|
||||
def call_next(g, values):
|
||||
while True:
|
||||
try:
|
||||
values.append(next(g))
|
||||
except ValueError:
|
||||
continue
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
with self.subTest(method='next'):
|
||||
run_test(call_next)
|
||||
|
||||
def call_send(g, values):
|
||||
while True:
|
||||
try:
|
||||
values.append(g.send(None))
|
||||
except ValueError:
|
||||
continue
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
with self.subTest(method='send'):
|
||||
run_test(call_send)
|
||||
|
||||
def for_iter_gen(g, values):
|
||||
while True:
|
||||
try:
|
||||
for value in g:
|
||||
values.append(value)
|
||||
else:
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
with self.subTest(method='for'):
|
||||
run_test(for_iter_gen)
|
||||
|
||||
def test_concurrent_close(self):
|
||||
def gen():
|
||||
for i in range(10):
|
||||
yield i
|
||||
time.sleep(0.001)
|
||||
|
||||
def drive_generator(g):
|
||||
while True:
|
||||
try:
|
||||
for value in g:
|
||||
if value == 5:
|
||||
g.close()
|
||||
else:
|
||||
return
|
||||
except ValueError as e:
|
||||
self.assertEqual(e.args[0], "generator already executing")
|
||||
|
||||
g = gen()
|
||||
threading_helper.run_concurrently(drive_generator, self.NUM_THREADS, args=(g,))
|
||||
|
|
|
|||
|
|
@ -91,6 +91,64 @@ class TestList(TestCase):
|
|||
with threading_helper.start_threads(threads):
|
||||
pass
|
||||
|
||||
def test_reverse(self):
|
||||
def reverse_list(b, l):
|
||||
b.wait()
|
||||
for _ in range(100):
|
||||
l.reverse()
|
||||
|
||||
def reader_list(b, l):
|
||||
b.wait()
|
||||
for _ in range(100):
|
||||
for i in range(10):
|
||||
self.assertTrue(0 <= l[i] < 10)
|
||||
|
||||
l = list(range(10))
|
||||
barrier = Barrier(2)
|
||||
threads = [Thread(target=reverse_list, args=(barrier, l)),
|
||||
Thread(target=reader_list, args=(barrier, l))]
|
||||
with threading_helper.start_threads(threads):
|
||||
pass
|
||||
|
||||
def test_slice_assignment1(self):
|
||||
def assign_slice(b, l):
|
||||
b.wait()
|
||||
for _ in range(100):
|
||||
l[2:5] = [7, 8, 9]
|
||||
|
||||
def reader_list(b, l):
|
||||
b.wait()
|
||||
for _ in range(100):
|
||||
self.assertIn(l[2], (2, 7))
|
||||
self.assertIn(l[3], (3, 8))
|
||||
self.assertIn(l[4], (4, 9))
|
||||
|
||||
l = list(range(10))
|
||||
barrier = Barrier(2)
|
||||
threads = [Thread(target=assign_slice, args=(barrier, l)),
|
||||
Thread(target=reader_list, args=(barrier, l))]
|
||||
with threading_helper.start_threads(threads):
|
||||
pass
|
||||
|
||||
def test_slice_assignment2(self):
|
||||
def assign_slice(b, l):
|
||||
b.wait()
|
||||
for _ in range(100):
|
||||
l[::2] = [10, 11, 12, 13, 14]
|
||||
|
||||
def reader_list(b, l):
|
||||
b.wait()
|
||||
for _ in range(100):
|
||||
for i in range(0, 10, 2):
|
||||
self.assertIn(l[i], (i, 10 + i // 2))
|
||||
|
||||
l = list(range(10))
|
||||
barrier = Barrier(2)
|
||||
threads = [Thread(target=assign_slice, args=(barrier, l)),
|
||||
Thread(target=reader_list, args=(barrier, l))]
|
||||
with threading_helper.start_threads(threads):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -134,6 +134,18 @@ class FinalizationTest(unittest.TestCase):
|
|||
self.assertEqual(len(resurrected), 1)
|
||||
self.assertIsInstance(resurrected[0].gi_code, types.CodeType)
|
||||
|
||||
def test_exhausted_generator_frame_cycle(self):
|
||||
def g():
|
||||
yield
|
||||
|
||||
generator = g()
|
||||
frame = generator.gi_frame
|
||||
self.assertIsNone(frame.f_back)
|
||||
next(generator)
|
||||
self.assertIsNone(frame.f_back)
|
||||
next(generator, None)
|
||||
self.assertIsNone(frame.f_back)
|
||||
|
||||
|
||||
class GeneratorTest(unittest.TestCase):
|
||||
|
||||
|
|
@ -290,6 +302,33 @@ class GeneratorTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual([1,2], list(i for i in C()))
|
||||
|
||||
def test_close_clears_frame(self):
|
||||
# gh-142766: Test that closing a generator clears its frame
|
||||
class DetectDelete:
|
||||
def __init__(self):
|
||||
DetectDelete.deleted = False
|
||||
|
||||
def __del__(self):
|
||||
DetectDelete.deleted = True
|
||||
|
||||
def generator(arg):
|
||||
yield
|
||||
|
||||
# Test a freshly created generator (not suspended)
|
||||
g = generator(DetectDelete())
|
||||
g.close()
|
||||
self.assertTrue(DetectDelete.deleted)
|
||||
|
||||
# Test a suspended generator
|
||||
g = generator(DetectDelete())
|
||||
next(g)
|
||||
g.close()
|
||||
self.assertTrue(DetectDelete.deleted)
|
||||
|
||||
# Clear via gi_frame.clear()
|
||||
g = generator(DetectDelete())
|
||||
g.gi_frame.clear()
|
||||
self.assertTrue(DetectDelete.deleted)
|
||||
|
||||
class ModifyUnderlyingIterableTest(unittest.TestCase):
|
||||
iterables = [
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ class MinidomTest(unittest.TestCase):
|
|||
self.assertEqual(dom.documentElement.childNodes[-1].data, "Hello")
|
||||
dom.unlink()
|
||||
|
||||
@support.requires_resource('cpu')
|
||||
def testAppendChildNoQuadraticComplexity(self):
|
||||
impl = getDOMImplementation()
|
||||
|
||||
|
|
@ -182,14 +183,18 @@ class MinidomTest(unittest.TestCase):
|
|||
children = [newdoc.createElement(f"child-{i}") for i in range(1, 2 ** 15 + 1)]
|
||||
element = top_element
|
||||
|
||||
start = time.time()
|
||||
start = time.monotonic()
|
||||
for child in children:
|
||||
element.appendChild(child)
|
||||
element = child
|
||||
end = time.time()
|
||||
end = time.monotonic()
|
||||
|
||||
# This example used to take at least 30 seconds.
|
||||
self.assertLess(end - start, 1)
|
||||
# Conservative assertion due to the wide variety of systems and
|
||||
# build configs timing based tests wind up run under.
|
||||
# A --with-address-sanitizer --with-pydebug build on a rpi5 still
|
||||
# completes this loop in <0.5 seconds.
|
||||
self.assertLess(end - start, 4)
|
||||
|
||||
def testSetAttributeNodeWithoutOwnerDocument(self):
|
||||
# regression test for gh-142754
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from test.support import (
|
|||
from test.support.import_helper import import_module
|
||||
from test.support.os_helper import TESTFN, unlink
|
||||
from test.support.script_helper import assert_python_ok
|
||||
import errno
|
||||
import unittest
|
||||
import os
|
||||
import re
|
||||
|
|
@ -1165,6 +1166,46 @@ class MmapTests(unittest.TestCase):
|
|||
m.flush(PAGESIZE)
|
||||
m.flush(PAGESIZE, PAGESIZE)
|
||||
|
||||
@unittest.skipUnless(sys.platform == 'linux', 'Linux only')
|
||||
@support.requires_linux_version(5, 17, 0)
|
||||
def test_set_name(self):
|
||||
# Test setting name on anonymous mmap
|
||||
m = mmap.mmap(-1, PAGESIZE)
|
||||
self.addCleanup(m.close)
|
||||
try:
|
||||
result = m.set_name('test_mapping')
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.EINVAL:
|
||||
# gh-142419: On Fedora, prctl(PR_SET_VMA_ANON_NAME) fails with
|
||||
# EINVAL because the kernel option CONFIG_ANON_VMA_NAME is
|
||||
# disabled.
|
||||
# See: https://bugzilla.redhat.com/show_bug.cgi?id=2302746
|
||||
self.skipTest("prctl() failed with EINVAL")
|
||||
else:
|
||||
raise
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test name length limit (80 chars including prefix "cpython:mmap:" and '\0')
|
||||
# Prefix is 13 chars, so max name is 66 chars
|
||||
long_name = 'x' * 66
|
||||
result = m.set_name(long_name)
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test name too long
|
||||
too_long_name = 'x' * 67
|
||||
with self.assertRaises(ValueError):
|
||||
m.set_name(too_long_name)
|
||||
|
||||
# Test that file-backed mmap raises error
|
||||
with open(TESTFN, 'wb+') as f:
|
||||
f.write(b'x' * PAGESIZE)
|
||||
f.flush()
|
||||
m2 = mmap.mmap(f.fileno(), PAGESIZE)
|
||||
self.addCleanup(m2.close)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
m2.set_name('should_fail')
|
||||
|
||||
|
||||
class LargeMmapTests(unittest.TestCase):
|
||||
|
||||
|
|
|
|||
|
|
@ -3478,6 +3478,49 @@ def test_pdb_issue_gh_65052():
|
|||
(Pdb) continue
|
||||
"""
|
||||
|
||||
def test_pdb_commands_last_breakpoint():
|
||||
"""See GH-142834
|
||||
|
||||
>>> def test_function():
|
||||
... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
|
||||
... foo = 1
|
||||
... bar = 2
|
||||
|
||||
>>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE
|
||||
... 'break 4',
|
||||
... 'break 3',
|
||||
... 'clear 2',
|
||||
... 'commands',
|
||||
... 'p "success"',
|
||||
... 'end',
|
||||
... 'continue',
|
||||
... 'clear 1',
|
||||
... 'commands',
|
||||
... 'continue',
|
||||
... ]):
|
||||
... test_function()
|
||||
> <doctest test.test_pdb.test_pdb_commands_last_breakpoint[0]>(2)test_function()
|
||||
-> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
|
||||
(Pdb) break 4
|
||||
Breakpoint 1 at <doctest test.test_pdb.test_pdb_commands_last_breakpoint[0]>:4
|
||||
(Pdb) break 3
|
||||
Breakpoint 2 at <doctest test.test_pdb.test_pdb_commands_last_breakpoint[0]>:3
|
||||
(Pdb) clear 2
|
||||
Deleted breakpoint 2 at <doctest test.test_pdb.test_pdb_commands_last_breakpoint[0]>:3
|
||||
(Pdb) commands
|
||||
(com) p "success"
|
||||
(com) end
|
||||
(Pdb) continue
|
||||
'success'
|
||||
> <doctest test.test_pdb.test_pdb_commands_last_breakpoint[0]>(4)test_function()
|
||||
-> bar = 2
|
||||
(Pdb) clear 1
|
||||
Deleted breakpoint 1 at <doctest test.test_pdb.test_pdb_commands_last_breakpoint[0]>:4
|
||||
(Pdb) commands
|
||||
*** cannot set commands: no existing breakpoint
|
||||
(Pdb) continue
|
||||
"""
|
||||
|
||||
|
||||
@support.force_not_colorized_test_class
|
||||
@support.requires_subprocess()
|
||||
|
|
@ -3563,10 +3606,22 @@ class PdbTestCase(unittest.TestCase):
|
|||
|
||||
def _fd_dir_for_pipe_targets(self):
|
||||
"""Return a directory exposing live file descriptors, if any."""
|
||||
return self._proc_fd_dir() or self._dev_fd_dir()
|
||||
|
||||
def _proc_fd_dir(self):
|
||||
"""Return /proc-backed fd dir when it can be used for pipes."""
|
||||
# GH-142836: Opening /proc/self/fd entries for pipes raises EACCES on
|
||||
# Solaris, so prefer other mechanisms there.
|
||||
if sys.platform.startswith("sunos"):
|
||||
return None
|
||||
|
||||
proc_fd = "/proc/self/fd"
|
||||
if os.path.isdir(proc_fd) and os.path.exists(os.path.join(proc_fd, '0')):
|
||||
return proc_fd
|
||||
return None
|
||||
|
||||
def _dev_fd_dir(self):
|
||||
"""Return /dev-backed fd dir when usable."""
|
||||
dev_fd = "/dev/fd"
|
||||
if os.path.isdir(dev_fd) and os.path.exists(os.path.join(dev_fd, '0')):
|
||||
if sys.platform.startswith("freebsd"):
|
||||
|
|
@ -3576,7 +3631,6 @@ class PdbTestCase(unittest.TestCase):
|
|||
except FileNotFoundError:
|
||||
return None
|
||||
return dev_fd
|
||||
|
||||
return None
|
||||
|
||||
def test_find_function_empty_file(self):
|
||||
|
|
@ -4039,6 +4093,23 @@ def bœr():
|
|||
f.write("invalid")
|
||||
self.assertEqual(pdb.Pdb().rcLines[0], "invalid")
|
||||
|
||||
def test_readrc_current_dir(self):
|
||||
with os_helper.temp_cwd() as cwd:
|
||||
rc_path = os.path.join(cwd, ".pdbrc")
|
||||
with open(rc_path, "w") as f:
|
||||
f.write("invalid")
|
||||
self.assertEqual(pdb.Pdb().rcLines[-1], "invalid")
|
||||
|
||||
def test_readrc_cwd_is_home(self):
|
||||
with os_helper.EnvironmentVarGuard() as env:
|
||||
env.unset("HOME")
|
||||
with os_helper.temp_cwd() as cwd, patch("os.path.expanduser"):
|
||||
rc_path = os.path.join(cwd, ".pdbrc")
|
||||
os.path.expanduser.return_value = rc_path
|
||||
with open(rc_path, "w") as f:
|
||||
f.write("invalid")
|
||||
self.assertEqual(pdb.Pdb().rcLines, ["invalid"])
|
||||
|
||||
def test_header(self):
|
||||
stdout = StringIO()
|
||||
header = 'Nobody expects... blah, blah, blah'
|
||||
|
|
|
|||
1081
Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py
Normal file
1081
Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -16,6 +16,7 @@ except ImportError:
|
|||
from test.support import is_emscripten, requires_remote_subprocess_debugging
|
||||
|
||||
from profiling.sampling.cli import main
|
||||
from profiling.sampling.errors import SamplingScriptNotFoundError, SamplingModuleNotFoundError, SamplingUnknownProcessError
|
||||
|
||||
|
||||
class TestSampleProfilerCLI(unittest.TestCase):
|
||||
|
|
@ -203,12 +204,12 @@ class TestSampleProfilerCLI(unittest.TestCase):
|
|||
with (
|
||||
mock.patch("sys.argv", test_args),
|
||||
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
|
||||
self.assertRaises(SystemExit) as cm,
|
||||
self.assertRaises(SamplingScriptNotFoundError) as cm,
|
||||
):
|
||||
main()
|
||||
|
||||
# Verify the error is about the non-existent script
|
||||
self.assertIn("12345", str(cm.exception.code))
|
||||
self.assertIn("12345", str(cm.exception))
|
||||
|
||||
def test_cli_no_target_specified(self):
|
||||
# In new CLI, must specify a subcommand
|
||||
|
|
@ -436,6 +437,7 @@ class TestSampleProfilerCLI(unittest.TestCase):
|
|||
|
||||
with (
|
||||
mock.patch("sys.argv", test_args),
|
||||
mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
|
||||
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
||||
):
|
||||
main()
|
||||
|
|
@ -475,6 +477,7 @@ class TestSampleProfilerCLI(unittest.TestCase):
|
|||
for test_args, expected_filename, expected_format in test_cases:
|
||||
with (
|
||||
mock.patch("sys.argv", test_args),
|
||||
mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
|
||||
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
||||
):
|
||||
main()
|
||||
|
|
@ -513,6 +516,7 @@ class TestSampleProfilerCLI(unittest.TestCase):
|
|||
|
||||
with (
|
||||
mock.patch("sys.argv", test_args),
|
||||
mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
|
||||
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
||||
):
|
||||
main()
|
||||
|
|
@ -534,6 +538,7 @@ class TestSampleProfilerCLI(unittest.TestCase):
|
|||
|
||||
with (
|
||||
mock.patch("sys.argv", test_args),
|
||||
mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
|
||||
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
||||
):
|
||||
main()
|
||||
|
|
@ -547,6 +552,7 @@ class TestSampleProfilerCLI(unittest.TestCase):
|
|||
|
||||
with (
|
||||
mock.patch("sys.argv", test_args),
|
||||
mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
|
||||
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
||||
):
|
||||
main()
|
||||
|
|
@ -562,6 +568,7 @@ class TestSampleProfilerCLI(unittest.TestCase):
|
|||
|
||||
with (
|
||||
mock.patch("sys.argv", test_args),
|
||||
mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
|
||||
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
||||
):
|
||||
main()
|
||||
|
|
@ -576,6 +583,7 @@ class TestSampleProfilerCLI(unittest.TestCase):
|
|||
|
||||
with (
|
||||
mock.patch("sys.argv", test_args),
|
||||
mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
|
||||
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
||||
):
|
||||
main()
|
||||
|
|
@ -697,14 +705,20 @@ class TestSampleProfilerCLI(unittest.TestCase):
|
|||
def test_run_nonexistent_script_exits_cleanly(self):
|
||||
"""Test that running a non-existent script exits with a clean error."""
|
||||
with mock.patch("sys.argv", ["profiling.sampling.cli", "run", "/nonexistent/script.py"]):
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
with self.assertRaisesRegex(SamplingScriptNotFoundError, "Script '[\\w/.]+' not found."):
|
||||
main()
|
||||
self.assertIn("Script not found", str(cm.exception.code))
|
||||
|
||||
@unittest.skipIf(is_emscripten, "subprocess not available")
|
||||
def test_run_nonexistent_module_exits_cleanly(self):
|
||||
"""Test that running a non-existent module exits with a clean error."""
|
||||
with mock.patch("sys.argv", ["profiling.sampling.cli", "run", "-m", "nonexistent_module_xyz"]):
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
with self.assertRaisesRegex(SamplingModuleNotFoundError, "Module '[\\w/.]+' not found."):
|
||||
main()
|
||||
self.assertIn("Module not found", str(cm.exception.code))
|
||||
|
||||
def test_cli_attach_nonexistent_pid(self):
|
||||
fake_pid = "99999"
|
||||
with mock.patch("sys.argv", ["profiling.sampling.cli", "attach", fake_pid]):
|
||||
with self.assertRaises(SamplingUnknownProcessError) as cm:
|
||||
main()
|
||||
|
||||
self.assertIn(fake_pid, str(cm.exception))
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ try:
|
|||
import profiling.sampling.sample
|
||||
from profiling.sampling.pstats_collector import PstatsCollector
|
||||
from profiling.sampling.stack_collector import CollapsedStackCollector
|
||||
from profiling.sampling.sample import SampleProfiler
|
||||
from profiling.sampling.sample import SampleProfiler, _is_process_running
|
||||
except ImportError:
|
||||
raise unittest.SkipTest(
|
||||
"Test only runs when _remote_debugging is available"
|
||||
|
|
@ -602,7 +602,7 @@ do_work()
|
|||
@requires_remote_subprocess_debugging()
|
||||
class TestSampleProfilerErrorHandling(unittest.TestCase):
|
||||
def test_invalid_pid(self):
|
||||
with self.assertRaises((OSError, RuntimeError)):
|
||||
with self.assertRaises((SystemExit, PermissionError)):
|
||||
collector = PstatsCollector(sample_interval_usec=100, skip_idle=False)
|
||||
profiling.sampling.sample.sample(-1, collector, duration_sec=1)
|
||||
|
||||
|
|
@ -638,7 +638,7 @@ class TestSampleProfilerErrorHandling(unittest.TestCase):
|
|||
sample_interval_usec=1000,
|
||||
all_threads=False,
|
||||
)
|
||||
self.assertTrue(profiler._is_process_running())
|
||||
self.assertTrue(_is_process_running(profiler.pid))
|
||||
self.assertIsNotNone(profiler.unwinder.get_stack_trace())
|
||||
subproc.process.kill()
|
||||
subproc.process.wait()
|
||||
|
|
@ -647,7 +647,7 @@ class TestSampleProfilerErrorHandling(unittest.TestCase):
|
|||
)
|
||||
|
||||
# Exit the context manager to ensure the process is terminated
|
||||
self.assertFalse(profiler._is_process_running())
|
||||
self.assertFalse(_is_process_running(profiler.pid))
|
||||
self.assertRaises(
|
||||
ProcessLookupError, profiler.unwinder.get_stack_trace
|
||||
)
|
||||
|
|
@ -863,3 +863,98 @@ asyncio.run(supervisor())
|
|||
self.assertGreater(cpu_percentage, 90.0,
|
||||
f"cpu_leaf should dominate samples in 'running' mode, "
|
||||
f"got {cpu_percentage:.1f}% ({cpu_leaf_samples}/{total})")
|
||||
|
||||
|
||||
def _generate_deep_generators_script(chain_depth=20, recurse_depth=150):
|
||||
"""Generate a script with deep nested generators for stress testing."""
|
||||
lines = [
|
||||
'import sys',
|
||||
'sys.setrecursionlimit(5000)',
|
||||
'',
|
||||
]
|
||||
# Generate chain of yield-from functions
|
||||
for i in range(chain_depth - 1):
|
||||
lines.extend([
|
||||
f'def deep_yield_chain_{i}(n):',
|
||||
f' yield ("L{i}", n)',
|
||||
f' yield from deep_yield_chain_{i + 1}(n)',
|
||||
'',
|
||||
])
|
||||
# Last chain function calls recursive_diver
|
||||
lines.extend([
|
||||
f'def deep_yield_chain_{chain_depth - 1}(n):',
|
||||
f' yield ("L{chain_depth - 1}", n)',
|
||||
f' yield from recursive_diver(n, {chain_depth})',
|
||||
'',
|
||||
'def recursive_diver(n, depth):',
|
||||
' yield (f"DIVE_{depth}", n)',
|
||||
f' if depth < {recurse_depth}:',
|
||||
' yield from recursive_diver(n, depth + 1)',
|
||||
' else:',
|
||||
' for i in range(5):',
|
||||
' yield (f"BOTTOM_{depth}", i)',
|
||||
'',
|
||||
'def oscillating_generator(iterations=1000):',
|
||||
' for i in range(iterations):',
|
||||
' yield ("OSCILLATE", i)',
|
||||
' yield from deep_yield_chain_0(i)',
|
||||
'',
|
||||
'def run_forever():',
|
||||
' while True:',
|
||||
' for _ in oscillating_generator(10):',
|
||||
' pass',
|
||||
'',
|
||||
'_test_sock.sendall(b"working")',
|
||||
'run_forever()',
|
||||
])
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
@requires_remote_subprocess_debugging()
|
||||
class TestDeepGeneratorFrameCache(unittest.TestCase):
|
||||
"""Test frame cache consistency with deep oscillating generator stacks."""
|
||||
|
||||
def test_all_stacks_share_same_base_frame(self):
|
||||
"""Verify all sampled stacks reach the entry point function.
|
||||
|
||||
When profiling deep generators that oscillate up and down the call
|
||||
stack, every sample should include the entry point function
|
||||
(run_forever) in its call chain. If the frame cache stores
|
||||
incomplete stacks, some samples will be missing this base function,
|
||||
causing broken flamegraphs.
|
||||
"""
|
||||
script = _generate_deep_generators_script()
|
||||
with test_subprocess(script, wait_for_working=True) as subproc:
|
||||
collector = CollapsedStackCollector(sample_interval_usec=1, skip_idle=False)
|
||||
|
||||
with (
|
||||
io.StringIO() as captured_output,
|
||||
mock.patch("sys.stdout", captured_output),
|
||||
):
|
||||
profiling.sampling.sample.sample(
|
||||
subproc.process.pid,
|
||||
collector,
|
||||
duration_sec=2,
|
||||
)
|
||||
|
||||
samples_with_entry_point = 0
|
||||
samples_without_entry_point = 0
|
||||
total_samples = 0
|
||||
|
||||
for (call_tree, _thread_id), count in collector.stack_counter.items():
|
||||
total_samples += count
|
||||
if call_tree:
|
||||
has_entry_point = call_tree and call_tree[0][2] == "<module>"
|
||||
if has_entry_point:
|
||||
samples_with_entry_point += count
|
||||
else:
|
||||
samples_without_entry_point += count
|
||||
|
||||
self.assertGreater(total_samples, 100,
|
||||
f"Expected at least 100 samples, got {total_samples}")
|
||||
|
||||
self.assertEqual(samples_without_entry_point, 0,
|
||||
f"Found {samples_without_entry_point}/{total_samples} samples "
|
||||
f"missing the entry point function 'run_forever'. This indicates "
|
||||
f"incomplete stacks are being returned, likely due to frame cache "
|
||||
f"storing partial stack traces.")
|
||||
|
|
|
|||
|
|
@ -252,6 +252,7 @@ class TestGilModeFiltering(unittest.TestCase):
|
|||
|
||||
with (
|
||||
mock.patch("sys.argv", test_args),
|
||||
mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
|
||||
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
||||
):
|
||||
try:
|
||||
|
|
@ -313,6 +314,7 @@ class TestGilModeFiltering(unittest.TestCase):
|
|||
|
||||
with (
|
||||
mock.patch("sys.argv", test_args),
|
||||
mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
|
||||
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
||||
):
|
||||
try:
|
||||
|
|
@ -432,6 +434,7 @@ class TestExceptionModeFiltering(unittest.TestCase):
|
|||
|
||||
with (
|
||||
mock.patch("sys.argv", test_args),
|
||||
mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
|
||||
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
||||
):
|
||||
try:
|
||||
|
|
@ -493,6 +496,7 @@ class TestExceptionModeFiltering(unittest.TestCase):
|
|||
|
||||
with (
|
||||
mock.patch("sys.argv", test_args),
|
||||
mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
|
||||
mock.patch("profiling.sampling.cli.sample") as mock_sample,
|
||||
):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -473,6 +473,32 @@ class PydocDocTest(unittest.TestCase):
|
|||
result, doc_loc = get_pydoc_text(xml.etree)
|
||||
self.assertEqual(doc_loc, "", "MODULE DOCS incorrectly includes a link")
|
||||
|
||||
def test_online_docs_link(self):
|
||||
import encodings.idna
|
||||
import importlib._bootstrap
|
||||
|
||||
module_docs = {
|
||||
'encodings': 'codecs#module-encodings',
|
||||
'encodings.idna': 'codecs#module-encodings.idna',
|
||||
}
|
||||
|
||||
with unittest.mock.patch('pydoc_data.module_docs.module_docs', module_docs):
|
||||
doc = pydoc.TextDoc()
|
||||
|
||||
basedir = os.path.dirname(encodings.__file__)
|
||||
doc_link = doc.getdocloc(encodings, basedir=basedir)
|
||||
self.assertIsNotNone(doc_link)
|
||||
self.assertIn('codecs#module-encodings', doc_link)
|
||||
self.assertNotIn('encodings.html', doc_link)
|
||||
|
||||
doc_link = doc.getdocloc(encodings.idna, basedir=basedir)
|
||||
self.assertIsNotNone(doc_link)
|
||||
self.assertIn('codecs#module-encodings.idna', doc_link)
|
||||
self.assertNotIn('encodings.idna.html', doc_link)
|
||||
|
||||
doc_link = doc.getdocloc(importlib._bootstrap, basedir=basedir)
|
||||
self.assertIsNone(doc_link)
|
||||
|
||||
def test_getpager_with_stdin_none(self):
|
||||
previous_stdin = sys.stdin
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -409,6 +409,12 @@ class TestAsyncioREPL(unittest.TestCase):
|
|||
expected = "toplevel contextvar test: ok"
|
||||
self.assertIn(expected, output, expected)
|
||||
|
||||
def test_quiet_mode(self):
|
||||
p = spawn_repl("-q", "-m", "asyncio", custom=True)
|
||||
output = kill_python(p)
|
||||
self.assertEqual(p.returncode, 0)
|
||||
self.assertEqual(output[:3], ">>>")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -1573,5 +1573,17 @@ class TestModuleAll(unittest.TestCase):
|
|||
check__all__(self, sax, extra=extra)
|
||||
|
||||
|
||||
class TestModule(unittest.TestCase):
|
||||
def test_deprecated__version__and__date__(self):
|
||||
for module in (sax.expatreader, sax.handler):
|
||||
with self.subTest(module=module):
|
||||
with self.assertWarnsRegex(
|
||||
DeprecationWarning,
|
||||
"'version' is deprecated and slated for removal in Python 3.20",
|
||||
) as cm:
|
||||
getattr(module, "version")
|
||||
self.assertEqual(cm.filename, __file__)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -6,9 +6,14 @@ import_helper.import_module('_sqlite3')
|
|||
import os
|
||||
import sqlite3
|
||||
|
||||
# make sure only print once
|
||||
_printed_version = False
|
||||
|
||||
# Implement the unittest "load tests" protocol.
|
||||
def load_tests(*args):
|
||||
if verbose:
|
||||
def load_tests(loader, tests, pattern):
|
||||
global _printed_version
|
||||
if verbose and not _printed_version:
|
||||
print(f"test_sqlite3: testing with SQLite version {sqlite3.sqlite_version}")
|
||||
_printed_version = True
|
||||
pkg_dir = os.path.dirname(__file__)
|
||||
return load_package_tests(pkg_dir, *args)
|
||||
return load_package_tests(pkg_dir, loader, tests, pattern)
|
||||
|
|
|
|||
|
|
@ -759,7 +759,7 @@ class NormalizationTest(unittest.TestCase):
|
|||
|
||||
@requires_resource('cpu')
|
||||
def test_normalization_3_2_0(self):
|
||||
testdatafile = findfile('NormalizationTest-3.2.0.txt', 'data')
|
||||
testdatafile = findfile('NormalizationTest-3.2.0.txt')
|
||||
with open(testdatafile, encoding='utf-8') as testdata:
|
||||
self.run_normalization_tests(testdata, unicodedata.ucd_3_2_0)
|
||||
|
||||
|
|
|
|||
|
|
@ -219,5 +219,81 @@ class TestThreadingMock(unittest.TestCase):
|
|||
self.assertEqual(m.call_count, LOOPS * THREADS)
|
||||
|
||||
|
||||
def test_call_args_thread_safe(self):
|
||||
m = ThreadingMock()
|
||||
LOOPS = 100
|
||||
THREADS = 10
|
||||
def test_function(thread_id):
|
||||
for i in range(LOOPS):
|
||||
m(thread_id, i)
|
||||
|
||||
oldswitchinterval = sys.getswitchinterval()
|
||||
setswitchinterval(1e-6)
|
||||
try:
|
||||
threads = [
|
||||
threading.Thread(target=test_function, args=(thread_id,))
|
||||
for thread_id in range(THREADS)
|
||||
]
|
||||
with threading_helper.start_threads(threads):
|
||||
pass
|
||||
finally:
|
||||
sys.setswitchinterval(oldswitchinterval)
|
||||
expected_calls = {
|
||||
(thread_id, i)
|
||||
for thread_id in range(THREADS)
|
||||
for i in range(LOOPS)
|
||||
}
|
||||
self.assertSetEqual({call.args for call in m.call_args_list}, expected_calls)
|
||||
|
||||
def test_method_calls_thread_safe(self):
|
||||
m = ThreadingMock()
|
||||
LOOPS = 100
|
||||
THREADS = 10
|
||||
def test_function(thread_id):
|
||||
for i in range(LOOPS):
|
||||
getattr(m, f"method_{thread_id}")(i)
|
||||
|
||||
oldswitchinterval = sys.getswitchinterval()
|
||||
setswitchinterval(1e-6)
|
||||
try:
|
||||
threads = [
|
||||
threading.Thread(target=test_function, args=(thread_id,))
|
||||
for thread_id in range(THREADS)
|
||||
]
|
||||
with threading_helper.start_threads(threads):
|
||||
pass
|
||||
finally:
|
||||
sys.setswitchinterval(oldswitchinterval)
|
||||
for thread_id in range(THREADS):
|
||||
self.assertEqual(getattr(m, f"method_{thread_id}").call_count, LOOPS)
|
||||
self.assertEqual({call.args for call in getattr(m, f"method_{thread_id}").call_args_list},
|
||||
{(i,) for i in range(LOOPS)})
|
||||
|
||||
def test_mock_calls_thread_safe(self):
|
||||
m = ThreadingMock()
|
||||
LOOPS = 100
|
||||
THREADS = 10
|
||||
def test_function(thread_id):
|
||||
for i in range(LOOPS):
|
||||
m(thread_id, i)
|
||||
|
||||
oldswitchinterval = sys.getswitchinterval()
|
||||
setswitchinterval(1e-6)
|
||||
try:
|
||||
threads = [
|
||||
threading.Thread(target=test_function, args=(thread_id,))
|
||||
for thread_id in range(THREADS)
|
||||
]
|
||||
with threading_helper.start_threads(threads):
|
||||
pass
|
||||
finally:
|
||||
sys.setswitchinterval(oldswitchinterval)
|
||||
expected_calls = {
|
||||
(thread_id, i)
|
||||
for thread_id in range(THREADS)
|
||||
for i in range(LOOPS)
|
||||
}
|
||||
self.assertSetEqual({call.args for call in m.mock_calls}, expected_calls)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -577,6 +577,23 @@ class OpenerDirectorTests(unittest.TestCase):
|
|||
self.assertRaises(TypeError,
|
||||
OpenerDirector().add_handler, NonHandler())
|
||||
|
||||
def test_no_protocol_methods(self):
|
||||
# test the case that methods starts with handler type without the protocol
|
||||
# like open*() or _open*().
|
||||
# These methods should be ignored
|
||||
|
||||
o = OpenerDirector()
|
||||
meth_spec = [
|
||||
["open"],
|
||||
["_open"],
|
||||
["error"]
|
||||
]
|
||||
|
||||
add_ordered_mock_handlers(o, meth_spec)
|
||||
|
||||
self.assertEqual(len(o.handle_open), 0)
|
||||
self.assertEqual(len(o.handle_error), 0)
|
||||
|
||||
def test_badly_named_methods(self):
|
||||
# test work-around for three methods that accidentally follow the
|
||||
# naming conventions for handler methods
|
||||
|
|
|
|||
|
|
@ -4705,6 +4705,19 @@ class C14NTest(unittest.TestCase):
|
|||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestModule(unittest.TestCase):
|
||||
def test_deprecated_version(self):
|
||||
with self.assertWarnsRegex(
|
||||
DeprecationWarning,
|
||||
"'VERSION' is deprecated and slated for removal in Python 3.20",
|
||||
) as cm:
|
||||
getattr(ET, "VERSION")
|
||||
self.assertEqual(cm.filename, __file__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
def setUpModule(module=None):
|
||||
# When invoked without a module, runs the Python ET tests by loading pyET.
|
||||
# Otherwise, uses the given module as the ET.
|
||||
|
|
|
|||
|
|
@ -1551,6 +1551,26 @@ class ZoneInfoCacheTest(TzPathUserMixin, ZoneInfoTestBase):
|
|||
except CustomError:
|
||||
pass
|
||||
|
||||
def test_weak_cache_descriptor_use_after_free(self):
|
||||
class BombDescriptor:
|
||||
def __get__(self, obj, owner):
|
||||
return {}
|
||||
|
||||
class EvilZoneInfo(self.klass):
|
||||
pass
|
||||
|
||||
# Must be set after the class creation.
|
||||
EvilZoneInfo._weak_cache = BombDescriptor()
|
||||
|
||||
key = "America/Los_Angeles"
|
||||
zone1 = EvilZoneInfo(key)
|
||||
self.assertEqual(str(zone1), key)
|
||||
|
||||
EvilZoneInfo.clear_cache()
|
||||
zone2 = EvilZoneInfo(key)
|
||||
self.assertEqual(str(zone2), key)
|
||||
self.assertIsNot(zone2, zone1)
|
||||
|
||||
|
||||
class CZoneInfoCacheTest(ZoneInfoCacheTest):
|
||||
module = c_zoneinfo
|
||||
|
|
|
|||
|
|
@ -415,6 +415,8 @@ class OpenerDirector:
|
|||
continue
|
||||
|
||||
i = meth.find("_")
|
||||
if i < 1:
|
||||
continue
|
||||
protocol = meth[:i]
|
||||
condition = meth[i+1:]
|
||||
|
||||
|
|
|
|||
|
|
@ -83,15 +83,12 @@ __all__ = [
|
|||
"SubElement",
|
||||
"tostring", "tostringlist",
|
||||
"TreeBuilder",
|
||||
"VERSION",
|
||||
"XML", "XMLID",
|
||||
"XMLParser", "XMLPullParser",
|
||||
"register_namespace",
|
||||
"canonicalize", "C14NWriterTarget",
|
||||
]
|
||||
|
||||
VERSION = "1.3.0"
|
||||
|
||||
import sys
|
||||
import re
|
||||
import warnings
|
||||
|
|
@ -2104,3 +2101,14 @@ except ImportError:
|
|||
pass
|
||||
else:
|
||||
_set_factories(Comment, ProcessingInstruction)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
def __getattr__(name):
|
||||
if name == "VERSION":
|
||||
from warnings import _deprecated
|
||||
|
||||
_deprecated("VERSION", remove=(3, 20))
|
||||
return "1.3.0" # Do not change
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue