gh-138122: Make the tachyon profiler opcode-aware (#142394)

This commit is contained in:
Pablo Galindo Salgado 2025-12-11 03:41:47 +00:00 committed by GitHub
parent fa448451ab
commit 5b19c75b47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 3983 additions and 507 deletions

View file

@ -13,6 +13,8 @@
**Source code:** :source:`Lib/profiling/sampling/`
.. program:: profiling.sampling
--------------
.. image:: tachyon-logo.png
@ -146,6 +148,10 @@ Generate a line-by-line heatmap::
python -m profiling.sampling run --heatmap script.py
Enable opcode-level profiling to see which bytecode instructions are executing::
python -m profiling.sampling run --opcodes --flamegraph script.py
Commands
========
@ -308,7 +314,7 @@ The two most fundamental parameters are the sampling interval and duration.
Together, these determine how many samples will be collected during a profiling
session.
The ``--interval`` option (``-i``) sets the time between samples in
The :option:`--interval` option (:option:`-i`) sets the time between samples in
microseconds. The default is 100 microseconds, which produces approximately
10,000 samples per second::
@ -319,7 +325,7 @@ cost of slightly higher profiler CPU usage. Higher intervals reduce profiler
overhead but may miss short-lived functions. For most applications, the
default interval provides a good balance between accuracy and overhead.
The ``--duration`` option (``-d``) sets how long to profile in seconds. The
The :option:`--duration` option (:option:`-d`) sets how long to profile in seconds. The
default is 10 seconds::
python -m profiling.sampling run -d 60 script.py
@ -337,8 +343,8 @@ Python programs often use multiple threads, whether explicitly through the
:mod:`threading` module or implicitly through libraries that manage thread
pools.
By default, the profiler samples only the main thread. The ``--all-threads``
option (``-a``) enables sampling of all threads in the process::
By default, the profiler samples only the main thread. The :option:`--all-threads`
option (:option:`-a`) enables sampling of all threads in the process::
python -m profiling.sampling run -a script.py
@ -357,7 +363,7 @@ additional context about what the interpreter is doing at the moment each
sample is taken. These synthetic frames help distinguish different types of
execution that would otherwise be invisible.
The ``--native`` option adds ``<native>`` frames to indicate when Python has
The :option:`--native` option adds ``<native>`` frames to indicate when Python has
called into C code (extension modules, built-in functions, or the interpreter
itself)::
@ -369,7 +375,7 @@ in the Python function that made the call. This is useful when optimizing
code that makes heavy use of C extensions like NumPy or database drivers.
By default, the profiler includes ``<GC>`` frames when garbage collection is
active. The ``--no-gc`` option suppresses these frames::
active. The :option:`--no-gc` option suppresses these frames::
python -m profiling.sampling run --no-gc script.py
@ -379,10 +385,48 @@ see substantial time in ``<GC>`` frames, consider investigating object
allocation rates or using object pooling.
Opcode-aware profiling
----------------------
The :option:`--opcodes` option enables instruction-level profiling that captures
which Python bytecode instructions are executing at each sample::
python -m profiling.sampling run --opcodes --flamegraph script.py
This feature provides visibility into Python's bytecode execution, including
adaptive specialization optimizations. When a generic instruction like
``LOAD_ATTR`` is specialized at runtime into a more efficient variant like
``LOAD_ATTR_INSTANCE_VALUE``, the profiler shows both the specialized name
and the base instruction.
Opcode information appears in several output formats:
- **Live mode**: An opcode panel shows instruction-level statistics for the
selected function, accessible via keyboard navigation
- **Flame graphs**: Nodes display opcode information when available, helping
identify which instructions consume the most time
- **Heatmap**: Expandable bytecode panels per source line show instruction
breakdown with specialization percentages
- **Gecko format**: Opcode transitions are emitted as interval markers in the
Firefox Profiler timeline
This level of detail is particularly useful for:
- Understanding the performance impact of Python's adaptive specialization
- Identifying hot bytecode instructions that might benefit from optimization
- Analyzing the effectiveness of different code patterns at the instruction level
- Debugging performance issues that occur at the bytecode level
The :option:`--opcodes` option is compatible with :option:`--live`, :option:`--flamegraph`,
:option:`--heatmap`, and :option:`--gecko` formats. It requires additional memory to store
opcode information and may slightly reduce sampling performance, but provides
unprecedented visibility into Python's execution model.
Real-time statistics
--------------------
The ``--realtime-stats`` option displays sampling rate statistics during
The :option:`--realtime-stats` option displays sampling rate statistics during
profiling::
python -m profiling.sampling run --realtime-stats script.py
@ -434,7 +478,7 @@ CPU execution time, or time spent holding the global interpreter lock.
Wall-clock mode
---------------
Wall-clock mode (``--mode=wall``) captures all samples regardless of what the
Wall-clock mode (:option:`--mode`\ ``=wall``) captures all samples regardless of what the
thread is doing. This is the default mode and provides a complete picture of
where time passes during program execution::
@ -454,7 +498,7 @@ latency.
CPU mode
--------
CPU mode (``--mode=cpu``) records samples only when the thread is actually
CPU mode (:option:`--mode`\ ``=cpu``) records samples only when the thread is actually
executing on a CPU core::
python -m profiling.sampling run --mode=cpu script.py
@ -488,7 +532,7 @@ connection pooling, or reducing wait time instead.
GIL mode
--------
GIL mode (``--mode=gil``) records samples only when the thread holds Python's
GIL mode (:option:`--mode`\ ``=gil``) records samples only when the thread holds Python's
global interpreter lock::
python -m profiling.sampling run --mode=gil script.py
@ -520,7 +564,7 @@ output goes to stdout, a file, or a directory depending on the format.
pstats format
-------------
The pstats format (``--pstats``) produces a text table similar to what
The pstats format (:option:`--pstats`) produces a text table similar to what
deterministic profilers generate. This is the default output format::
python -m profiling.sampling run script.py
@ -567,31 +611,31 @@ interesting functions that highlights:
samples (high cumulative/direct multiplier). These are frequently-nested
functions that appear deep in many call chains.
Use ``--no-summary`` to suppress both the legend and summary sections.
Use :option:`--no-summary` to suppress both the legend and summary sections.
To save pstats output to a file instead of stdout::
python -m profiling.sampling run -o profile.txt script.py
The pstats format supports several options for controlling the display.
The ``--sort`` option determines the column used for ordering results::
The :option:`--sort` option determines the column used for ordering results::
python -m profiling.sampling run --sort=tottime script.py
python -m profiling.sampling run --sort=cumtime script.py
python -m profiling.sampling run --sort=nsamples script.py
The ``--limit`` option restricts output to the top N entries::
The :option:`--limit` option restricts output to the top N entries::
python -m profiling.sampling run --limit=30 script.py
The ``--no-summary`` option suppresses the header summary that precedes the
The :option:`--no-summary` option suppresses the header summary that precedes the
statistics table.
Collapsed stacks format
-----------------------
Collapsed stacks format (``--collapsed``) produces one line per unique call
Collapsed stacks format (:option:`--collapsed`) produces one line per unique call
stack, with a count of how many times that stack was sampled::
python -m profiling.sampling run --collapsed script.py
@ -621,7 +665,7 @@ visualization where you can click to zoom into specific call paths.
Flame graph format
------------------
Flame graph format (``--flamegraph``) produces a self-contained HTML file with
Flame graph format (:option:`--flamegraph`) produces a self-contained HTML file with
an interactive flame graph visualization::
python -m profiling.sampling run --flamegraph script.py
@ -667,7 +711,7 @@ or through their callees.
Gecko format
------------
Gecko format (``--gecko``) produces JSON output compatible with the Firefox
Gecko format (:option:`--gecko`) produces JSON output compatible with the Firefox
Profiler::
python -m profiling.sampling run --gecko script.py
@ -694,14 +738,14 @@ Firefox Profiler timeline:
- **Code type markers**: distinguish Python code from native (C extension) code
- **GC markers**: indicate garbage collection activity
For this reason, the ``--mode`` option is not available with Gecko format;
For this reason, the :option:`--mode` option is not available with Gecko format;
all relevant data is captured automatically.
Heatmap format
--------------
Heatmap format (``--heatmap``) generates an interactive HTML visualization
Heatmap format (:option:`--heatmap`) generates an interactive HTML visualization
showing sample counts at the source line level::
python -m profiling.sampling run --heatmap script.py
@ -744,7 +788,7 @@ interpretation of hierarchical visualizations.
Live mode
=========
Live mode (``--live``) provides a terminal-based real-time view of profiling
Live mode (:option:`--live`) provides a terminal-based real-time view of profiling
data, similar to the ``top`` command for system processes::
python -m profiling.sampling run --live script.py
@ -760,6 +804,11 @@ and thread status statistics (GIL held percentage, CPU usage, GC time). The
main table shows function statistics with the currently sorted column indicated
by an arrow (▼).
When :option:`--opcodes` is enabled, an additional opcode panel appears below the
main table, showing instruction-level statistics for the currently selected
function. This panel displays which bytecode instructions are executing most
frequently, including specialized variants and their base opcodes.
Keyboard commands
-----------------
@ -813,12 +862,17 @@ Within live mode, keyboard commands control the display:
:kbd:`h` or :kbd:`?`
Show the help screen with all available commands.
:kbd:`j` / :kbd:`k` (or :kbd:`Up` / :kbd:`Down`)
Navigate through opcode entries in the opcode panel (when ``--opcodes`` is
enabled). These keys scroll through the instruction-level statistics for the
currently selected function.
When profiling finishes (duration expires or target process exits), the display
shows a "PROFILING COMPLETE" banner and freezes the final results. You can
still navigate, sort, and filter the results before pressing :kbd:`q` to exit.
Live mode is incompatible with output format options (``--collapsed``,
``--flamegraph``, and so on) because it uses an interactive terminal
Live mode is incompatible with output format options (:option:`--collapsed`,
:option:`--flamegraph`, and so on) because it uses an interactive terminal
interface rather than producing file output.
@ -826,7 +880,7 @@ Async-aware profiling
=====================
For programs using :mod:`asyncio`, the profiler offers async-aware mode
(``--async-aware``) that reconstructs call stacks based on the task structure
(:option:`--async-aware`) that reconstructs call stacks based on the task structure
rather than the raw Python frames::
python -m profiling.sampling run --async-aware async_script.py
@ -846,16 +900,16 @@ and presenting stacks that reflect the ``await`` chain.
Async modes
-----------
The ``--async-mode`` option controls which tasks appear in the profile::
The :option:`--async-mode` option controls which tasks appear in the profile::
python -m profiling.sampling run --async-aware --async-mode=running async_script.py
python -m profiling.sampling run --async-aware --async-mode=all async_script.py
With ``--async-mode=running`` (the default), only the task currently executing
With :option:`--async-mode`\ ``=running`` (the default), only the task currently executing
on the CPU is profiled. This shows where your program is actively spending time
and is the typical choice for performance analysis.
With ``--async-mode=all``, tasks that are suspended (awaiting I/O, locks, or
With :option:`--async-mode`\ ``=all``, tasks that are suspended (awaiting I/O, locks, or
other tasks) are also included. This mode is useful for understanding what your
program is waiting on, but produces larger profiles since every suspended task
appears in each sample.
@ -884,8 +938,8 @@ Option restrictions
-------------------
Async-aware mode uses a different stack reconstruction mechanism and is
incompatible with: ``--native``, ``--no-gc``, ``--all-threads``, and
``--mode=cpu`` or ``--mode=gil``.
incompatible with: :option:`--native`, :option:`--no-gc`, :option:`--all-threads`, and
:option:`--mode`\ ``=cpu`` or :option:`--mode`\ ``=gil``.
Command-line interface
@ -939,6 +993,13 @@ Sampling options
Enable async-aware profiling for asyncio programs.
.. option:: --opcodes
Gather bytecode opcode information for instruction-level profiling. Shows
which bytecode instructions are executing, including specializations.
Compatible with ``--live``, ``--flamegraph``, ``--heatmap``, and ``--gecko``
formats only.
Mode options
------------

785
Doc/sphinx-warnings.txt Normal file
View file

@ -0,0 +1,785 @@
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:1243: WARNING: c:macro reference target not found: Py_TPFLAGS_HAVE_STACKLESS_EXTENSION [ref.macro]
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:3008: WARNING: c:identifier reference target not found: view [ref.identifier]
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:3015: WARNING: c:identifier reference target not found: view [ref.identifier]
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:3015: WARNING: c:identifier reference target not found: view [ref.identifier]
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:3022: WARNING: c:identifier reference target not found: view [ref.identifier]
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:3025: WARNING: c:identifier reference target not found: view [ref.identifier]
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:3070: WARNING: c:identifier reference target not found: view [ref.identifier]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:16: WARNING: py:mod reference target not found: xml.etree [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:16: WARNING: py:mod reference target not found: sqlite [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:137: WARNING: py:func reference target not found: partial [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:144: WARNING: py:func reference target not found: partial [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:160: WARNING: py:meth reference target not found: open_item [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:173: WARNING: py:func reference target not found: update_wrapper [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:186: WARNING: py:func reference target not found: wraps [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:212: WARNING: py:func reference target not found: setup [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:274: WARNING: py:mod reference target not found: pkg [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:274: WARNING: py:mod reference target not found: pkg.main [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:274: WARNING: py:mod reference target not found: pkg.string [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:277: WARNING: py:mod reference target not found: pkg.string [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:277: WARNING: py:mod reference target not found: pkg.main [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:284: WARNING: py:mod reference target not found: pkg.string [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:284: WARNING: py:mod reference target not found: pkg.string [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:284: WARNING: py:mod reference target not found: py.std [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:292: WARNING: py:mod reference target not found: pkg.string [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:315: WARNING: py:mod reference target not found: pkg.main [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:315: WARNING: py:mod reference target not found: pkg.string [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:315: WARNING: py:mod reference target not found: A.B.C [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:334: WARNING: py:mod reference target not found: py.std [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:349: WARNING: py:mod reference target not found: pychecker.checker [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:393: WARNING: py:class reference target not found: Exception1 [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:393: WARNING: py:class reference target not found: Exception2 [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:493: WARNING: py:meth reference target not found: send [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:498: WARNING: py:meth reference target not found: send [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:504: WARNING: py:meth reference target not found: close [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:504: WARNING: py:meth reference target not found: close [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:524: WARNING: py:meth reference target not found: close [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:524: WARNING: py:meth reference target not found: close [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:536: WARNING: py:attr reference target not found: gi_frame [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:536: WARNING: py:attr reference target not found: gi_frame [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:625: WARNING: py:func reference target not found: localcontext [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:693: WARNING: py:class reference target not found: DatabaseConnection [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:748: WARNING: py:func reference target not found: contextmanager [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:910: WARNING: c:macro reference target not found: PY_SSIZE_T_CLEAN [ref.macro]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:933: WARNING: py:meth reference target not found: __index__ [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:941: WARNING: py:meth reference target not found: __int__ [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:941: WARNING: py:meth reference target not found: __int__ [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:946: WARNING: py:meth reference target not found: __index__ [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:957: WARNING: py:meth reference target not found: __index__ [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:978: WARNING: py:class reference target not found: defaultdict [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1020: WARNING: py:meth reference target not found: startswith [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1020: WARNING: py:meth reference target not found: endswith [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1030: WARNING: py:meth reference target not found: sort [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1052: WARNING: py:meth reference target not found: __hash__ [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1052: WARNING: py:meth reference target not found: __hash__ [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1168: WARNING: py:meth reference target not found: read [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1168: WARNING: py:meth reference target not found: readlines [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1206: WARNING: c:func reference target not found: open [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:func reference target not found: codec.lookup [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:class reference target not found: CodecInfo [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:class reference target not found: CodecInfo [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:attr reference target not found: encode [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:attr reference target not found: decode [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:attr reference target not found: incrementalencoder [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:attr reference target not found: incrementaldecoder [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:attr reference target not found: streamwriter [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:attr reference target not found: streamreader [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1240: WARNING: py:class reference target not found: defaultdict [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1245: WARNING: py:class reference target not found: defaultdict [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1271: WARNING: py:class reference target not found: deque [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1288: WARNING: py:class reference target not found: Stats [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1292: WARNING: py:class reference target not found: reader [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1292: WARNING: py:attr reference target not found: line_num [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1292: WARNING: py:attr reference target not found: line_num [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1321: WARNING: py:meth reference target not found: SequenceMatcher.get_matching_blocks [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1330: WARNING: py:func reference target not found: testfile [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1330: WARNING: py:class reference target not found: DocFileSuite [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1345: WARNING: py:class reference target not found: FileInput [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1354: WARNING: py:func reference target not found: get_count [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1361: WARNING: py:func reference target not found: nsmallest [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1361: WARNING: py:func reference target not found: nlargest [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1361: WARNING: py:meth reference target not found: sort [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1384: WARNING: py:func reference target not found: format_string [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1384: WARNING: py:func reference target not found: currency [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1393: WARNING: py:func reference target not found: format_string [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1397: WARNING: py:func reference target not found: currency [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1404: WARNING: py:class reference target not found: mbox [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1404: WARNING: py:class reference target not found: MH [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1404: WARNING: py:class reference target not found: Maildir [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1404: WARNING: py:meth reference target not found: lock [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1404: WARNING: py:meth reference target not found: unlock [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1433: WARNING: py:func reference target not found: itemgetter [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1433: WARNING: py:func reference target not found: attrgetter [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1433: WARNING: py:attr reference target not found: a [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1433: WARNING: py:attr reference target not found: b [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1433: WARNING: py:meth reference target not found: sort [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1440: WARNING: py:class reference target not found: OptionParser [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1440: WARNING: py:attr reference target not found: epilog [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1440: WARNING: py:meth reference target not found: destroy [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1445: WARNING: py:attr reference target not found: stat_float_times [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1456: WARNING: py:func reference target not found: wait3 [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1456: WARNING: py:func reference target not found: wait4 [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1456: WARNING: py:func reference target not found: waitpid [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1456: WARNING: py:func reference target not found: wait3 [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1456: WARNING: py:func reference target not found: wait4 [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1456: WARNING: py:func reference target not found: wait3 [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1465: WARNING: py:attr reference target not found: st_gen [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1465: WARNING: py:attr reference target not found: st_birthtime [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1465: WARNING: py:attr reference target not found: st_flags [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1498: WARNING: py:mod reference target not found: pyexpat [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1501: WARNING: py:mod reference target not found: Queue [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1501: WARNING: py:meth reference target not found: join [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1501: WARNING: py:meth reference target not found: task_done [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1507: WARNING: py:mod reference target not found: regex [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1507: WARNING: py:mod reference target not found: regsub [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1507: WARNING: py:mod reference target not found: statcache [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1507: WARNING: py:mod reference target not found: tzparse [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1507: WARNING: py:mod reference target not found: whrandom [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1511: WARNING: py:mod reference target not found: dircmp [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1511: WARNING: py:mod reference target not found: ni [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1522: WARNING: py:attr reference target not found: rpc_paths [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1522: WARNING: py:attr reference target not found: rpc_paths [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1529: WARNING: py:const reference target not found: AF_NETLINK [ref.const]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1541: WARNING: py:meth reference target not found: getfamily [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1541: WARNING: py:meth reference target not found: gettype [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1541: WARNING: py:meth reference target not found: getproto [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:class reference target not found: Struct [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:meth reference target not found: pack [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:meth reference target not found: unpack [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:func reference target not found: pack [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:func reference target not found: unpack [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:class reference target not found: Struct [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:class reference target not found: Struct [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1565: WARNING: py:class reference target not found: Struct [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1585: WARNING: py:class reference target not found: TarFile [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1585: WARNING: py:meth reference target not found: extractall [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1607: WARNING: py:class reference target not found: UUID [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1607: WARNING: py:func reference target not found: uuid1 [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1607: WARNING: py:func reference target not found: uuid3 [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1607: WARNING: py:func reference target not found: uuid4 [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1607: WARNING: py:func reference target not found: uuid5 [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:class reference target not found: WeakKeyDictionary [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:class reference target not found: WeakValueDictionary [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:meth reference target not found: iterkeyrefs [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:meth reference target not found: keyrefs [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:class reference target not found: WeakKeyDictionary [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:meth reference target not found: itervaluerefs [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:meth reference target not found: valuerefs [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:class reference target not found: WeakValueDictionary [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1641: WARNING: py:func reference target not found: open_new [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1641: WARNING: py:func reference target not found: open_new_tab [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1666: WARNING: py:class reference target not found: Compress [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1666: WARNING: py:class reference target not found: Decompress [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1666: WARNING: py:class reference target not found: Compress [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1666: WARNING: py:class reference target not found: Decompress [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1687: WARNING: py:class reference target not found: CDLL [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1687: WARNING: py:class reference target not found: CDLL [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1697: WARNING: py:func reference target not found: c_int [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1697: WARNING: py:func reference target not found: c_float [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1697: WARNING: py:func reference target not found: c_double [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1697: WARNING: py:func reference target not found: c_char_p [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1697: WARNING: py:attr reference target not found: value [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1706: WARNING: py:func reference target not found: c_char_p [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1706: WARNING: py:func reference target not found: create_string_buffer [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1715: WARNING: py:attr reference target not found: restype [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1762: WARNING: py:mod reference target not found: xml.etree [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1762: WARNING: py:mod reference target not found: ElementTree [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1762: WARNING: py:mod reference target not found: ElementPath [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1762: WARNING: py:mod reference target not found: ElementInclude [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1762: WARNING: py:mod reference target not found: cElementTree [ref.mod]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1772: WARNING: py:attr reference target not found: text [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1772: WARNING: py:attr reference target not found: tail [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1772: WARNING: py:class reference target not found: TextNode [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1778: WARNING: py:func reference target not found: parse [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1778: WARNING: py:class reference target not found: ElementTree [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1790: WARNING: py:class reference target not found: ElementTree [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1790: WARNING: py:meth reference target not found: getroot [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1790: WARNING: py:class reference target not found: Element [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1793: WARNING: py:func reference target not found: XML [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1793: WARNING: py:class reference target not found: Element [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1793: WARNING: py:class reference target not found: ElementTree [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1837: WARNING: py:class reference target not found: Element [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1845: WARNING: py:meth reference target not found: ElementTree.write [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1845: WARNING: py:func reference target not found: parse [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1914: WARNING: py:meth reference target not found: digest [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1914: WARNING: py:meth reference target not found: hexdigest [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1952: WARNING: py:class reference target not found: Connection [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1960: WARNING: py:class reference target not found: Connection [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1960: WARNING: py:class reference target not found: Cursor [ref.class]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1960: WARNING: py:meth reference target not found: execute [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1978: WARNING: py:meth reference target not found: execute [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1998: WARNING: py:meth reference target not found: fetchone [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1998: WARNING: py:meth reference target not found: fetchall [ref.meth]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:2117: WARNING: c:func reference target not found: PyParser_ASTFromString [ref.func]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:2238: WARNING: py:attr reference target not found: gi_frame [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:2238: WARNING: py:attr reference target not found: gi_frame [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:2261: WARNING: py:attr reference target not found: rpc_paths [ref.attr]
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:2261: WARNING: py:attr reference target not found: rpc_paths [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:10185: WARNING: py:meth reference target not found: asyncio.asyncio.run_coroutine_threadsafe [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:10185: WARNING: py:class reference target not found: CancelledError [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:10185: WARNING: py:class reference target not found: InvalidStateError [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:10189: WARNING: py:func reference target not found: ntpath.commonpath [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:10197: WARNING: py:meth reference target not found: configparser.RawConfigParser._read [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:10200: WARNING: py:func reference target not found: ntpath.commonpath [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:10257: WARNING: py:func reference target not found: inspect.findsource [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:10325: WARNING: py:class reference target not found: tkinter.Checkbutton [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:10325: WARNING: py:class reference target not found: tkinter.ttk.Checkbutton [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:10339: WARNING: py:class reference target not found: logging.TimedRotatingFileHandler [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:10560: WARNING: py:meth reference target not found: xml.sax.expatreader.ExpatParser.flush [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:10631: WARNING: py:func reference target not found: platform.java_ver [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:10654: WARNING: py:class reference target not found: logging.TimedRotatingFileHandler [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:10665: WARNING: py:meth reference target not found: email.Message.as_string [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:10704: WARNING: py:class reference target not found: logging.TimedRotatingFileHandler [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:10727: WARNING: py:class reference target not found: StreamWriter [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:10730: WARNING: py:meth reference target not found: asyncio.BaseEventLoop.shutdown_default_executor [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:10738: WARNING: py:class reference target not found: dis.ArgResolver [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:10752: WARNING: py:class reference target not found: type.MethodDescriptorType [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:10752: WARNING: py:class reference target not found: type.WrapperDescriptorType [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:10765: WARNING: py:meth reference target not found: DatagramTransport.sendto [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:10774: WARNING: py:func reference target not found: posixpath.commonpath [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:10778: WARNING: py:func reference target not found: posixpath.commonpath [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:10787: WARNING: py:data reference target not found: VERIFY_X509_STRICT [ref.data]
/home/pablogsal/github/python/main/Doc/build/NEWS:10794: WARNING: py:meth reference target not found: importlib.resources.simple.ResourceHandle.open [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:10813: WARNING: py:meth reference target not found: Profile.print_stats [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:10816: WARNING: py:data reference target not found: socket.SO_BINDTOIFINDEX [ref.data]
/home/pablogsal/github/python/main/Doc/build/NEWS:10832: WARNING: py:func reference target not found: io.BufferedReader.tell [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:10832: WARNING: py:func reference target not found: io.BufferedReader.seek [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:10832: WARNING: py:func reference target not found: io.BufferedRandom.tell [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:10832: WARNING: py:func reference target not found: io.BufferedRandom.seek [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:10952: WARNING: 'envvar' reference target not found: PYLAUNCHER_ALLOW_INSTALL [ref.envvar]
/home/pablogsal/github/python/main/Doc/build/NEWS:11142: WARNING: py:meth reference target not found: io.BufferedRandom.read1 [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:11154: WARNING: py:meth reference target not found: tkinter.Text.count [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:11243: WARNING: py:meth reference target not found: asyncio.BaseEventLoop.create_server [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:11250: WARNING: py:exc reference target not found: FileNotFound [ref.exc]
/home/pablogsal/github/python/main/Doc/build/NEWS:11285: WARNING: py:class reference target not found: asyncio.selector_events.BaseSelectorEventLoop [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:11296: WARNING: py:class reference target not found: tkinter.Text [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:11296: WARNING: py:class reference target not found: tkinter.Canvas [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:11361: WARNING: py:meth reference target not found: tkinter._test [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:11378: WARNING: py:func reference target not found: lzma._decode_filter_properties [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:11704: WARNING: py:func reference target not found: email.message.get_payload [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:11769: WARNING: py:exc reference target not found: CancelledError [ref.exc]
/home/pablogsal/github/python/main/Doc/build/NEWS:11769: WARNING: py:exc reference target not found: CancelledError [ref.exc]
/home/pablogsal/github/python/main/Doc/build/NEWS:11789: WARNING: py:meth reference target not found: asyncio.StreamReaderProtocol.connection_made [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:11815: WARNING: py:mod reference target not found: multiprocessing.manager [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:11815: WARNING: py:mod reference target not found: multiprocessing.resource_sharer [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:11864: WARNING: py:meth reference target not found: asyncio.futures.Future.set_exception [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:11933: WARNING: py:const reference target not found: LOG_FTP [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:11933: WARNING: py:const reference target not found: LOG_NETINFO [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:11933: WARNING: py:const reference target not found: LOG_REMOTEAUTH [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:11933: WARNING: py:const reference target not found: LOG_INSTALL [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:11933: WARNING: py:const reference target not found: LOG_RAS [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:11933: WARNING: py:const reference target not found: LOG_LAUNCHD [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:12017: WARNING: py:meth reference target not found: AbstractEventLoop.create_server [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:12017: WARNING: py:meth reference target not found: BaseEventLoop.create_server [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:12061: WARNING: py:meth reference target not found: Signature.format [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:12083: WARNING: py:class reference target not found: QueueHandler [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:12131: WARNING: py:func reference target not found: urllib.request.getproxies_environment [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:12156: WARNING: py:exc reference target not found: PatternError [ref.exc]
/home/pablogsal/github/python/main/Doc/build/NEWS:12195: WARNING: py:meth reference target not found: ssl.SSLSocket.recv_into [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:12252: WARNING: py:meth reference target not found: pathlib.PureWindowsPath.is_absolute [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:12378: WARNING: py:func reference target not found: sysconfig.get_plaform [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:12443: WARNING: py:attr reference target not found: object.__weakref__ [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:12504: WARNING: py:class reference target not found: Traceback [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:12570: WARNING: 'envvar' reference target not found: PYTHON_PRESITE=package.module [ref.envvar]
/home/pablogsal/github/python/main/Doc/build/NEWS:12578: WARNING: py:meth reference target not found: types.CodeType.replace [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:12614: WARNING: py:meth reference target not found: StreamWriter.__del__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:12755: WARNING: py:class reference target not found: IPv6Address [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:12806: WARNING: py:meth reference target not found: tkinter.Text.count [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:12822: WARNING: py:mod reference target not found: zipinfo [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:13038: WARNING: c:func reference target not found: PyUnstable_PerfTrampoline_CompileCode [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:13038: WARNING: c:func reference target not found: PyUnstable_PerfTrampoline_SetPersistAfterFork [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:13038: WARNING: c:func reference target not found: PyUnstable_CopyPerfMapFile [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:13100: WARNING: py:func reference target not found: interpreter_clear [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:13102: WARNING: c:func reference target not found: PyErr_Display [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:13118: WARNING: 'envvar' reference target not found: PYTHONUOPS [ref.envvar]
/home/pablogsal/github/python/main/Doc/build/NEWS:13262: WARNING: 'envvar' reference target not found: PYTHONUOPS [ref.envvar]
/home/pablogsal/github/python/main/Doc/build/NEWS:13268: WARNING: py:meth reference target not found: multiprocessing.synchronize.SemLock.__setstate__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:13268: WARNING: py:attr reference target not found: multiprocessing.synchronize.SemLock._is_fork_ctx [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:13273: WARNING: py:attr reference target not found: multiprocessing.synchronize.SemLock.is_fork_ctx [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:13273: WARNING: py:attr reference target not found: multiprocessing.synchronize.SemLock._is_fork_ctx [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:13335: WARNING: 'envvar' reference target not found: PYTHONUOPS [ref.envvar]
/home/pablogsal/github/python/main/Doc/build/NEWS:13391: WARNING: 'envvar' reference target not found: PYTHONUOPS [ref.envvar]
/home/pablogsal/github/python/main/Doc/build/NEWS:13425: WARNING: py:meth reference target not found: dbm.ndbm.ndbm.clear [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:13428: WARNING: py:meth reference target not found: dbm.gnu.gdbm.clear [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:13449: WARNING: 'envvar' reference target not found: PYTHONUOPS [ref.envvar]
/home/pablogsal/github/python/main/Doc/build/NEWS:13466: WARNING: 'opcode' reference target not found: LOAD_ATTR_INSTANCE_VALUE [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:13491: WARNING: 'opcode' reference target not found: LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:13491: WARNING: 'opcode' reference target not found: LOAD_ATTR_NONDESCRIPTOR_NO_DICT [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:13674: WARNING: py:class reference target not found: tokenize.TokenInfo [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:13678: WARNING: py:class reference target not found: tokenize.TokenInfo [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:13739: WARNING: py:meth reference target not found: BaseEventLoop._run_once [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:13751: WARNING: py:class reference target not found: PureWindowsPath [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:13812: WARNING: py:meth reference target not found: KqueueSelector.select [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:13990: WARNING: py:meth reference target not found: gzip.GzipFile.seek [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:14024: WARNING: py:meth reference target not found: sqlite3.connection.close [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:14115: WARNING: py:meth reference target not found: __repr__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:14130: WARNING: py:meth reference target not found: clear [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:14139: WARNING: py:class reference target not found: smptlib.SMTP [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:14146: WARNING: py:meth reference target not found: PurePath.relative_to [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:14157: WARNING: py:meth reference target not found: SelectSelector.select [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:14163: WARNING: py:meth reference target not found: KqueueSelector.select [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:14187: WARNING: py:meth reference target not found: zipfile.Path.match [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:14229: WARNING: py:func reference target not found: multiprocessing.managers.convert_to_error [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:14233: WARNING: py:attr reference target not found: pathlib.PurePath.pathmod [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:14258: WARNING: py:mod reference target not found: multiprocessing.spawn [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:14272: WARNING: py:meth reference target not found: __get__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:14272: WARNING: py:meth reference target not found: __set__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:14277: WARNING: c:func reference target not found: mp_init [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:14287: WARNING: py:func reference target not found: pydoc.doc [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:14326: WARNING: py:meth reference target not found: gzip.GzipFile.flush [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:14372: WARNING: py:mod reference target not found: pyexpat [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:14645: WARNING: c:func reference target not found: mp_to_unsigned_bin_n [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:14645: WARNING: c:func reference target not found: mp_unsigned_bin_size [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:14665: WARNING: py:func reference target not found: builtins.issubclass [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:14688: WARNING: py:func reference target not found: concurrent.futures.thread._worker [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:14726: WARNING: py:func reference target not found: close [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:15149: WARNING: py:func reference target not found: ntpath.normcase [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:15555: WARNING: py:class reference target not found: http.client.SimpleHTTPRequestHandler [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:15814: WARNING: py:mod reference target not found: multiprocessing.process [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:15836: WARNING: py:func reference target not found: urllib.parse.unsplit [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:15957: WARNING: py:meth reference target not found: tkinter.Menu.index [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:15961: WARNING: py:class reference target not found: URLError [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:16193: WARNING: py:class reference target not found: urllib.request.AbstractHTTPHandler [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:16202: WARNING: py:meth reference target not found: tkinter.Canvas.coords [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:16360: WARNING: py:func reference target not found: ntpath.realpath [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:16466: WARNING: 'opcode' reference target not found: BINARY_SUBSCR [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:16484: WARNING: c:func reference target not found: PyErr_Display [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:16685: WARNING: py:mod reference target not found: concurrent.futures.process [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:16793: WARNING: 'opcode' reference target not found: FOR_ITER_RANGE [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:16834: WARNING: 'opcode' reference target not found: RETURN_CONST [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:16864: WARNING: py:func reference target not found: fileinput.hookcompressed [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:16939: WARNING: py:meth reference target not found: pathlib.PureWindowsPath.match [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:17108: WARNING: 'opcode' reference target not found: COMPARE_AND_BRANCH [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:17123: WARNING: py:mod reference target not found: importlib/_bootstrap [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:17132: WARNING: py:mod reference target not found: opcode [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:17195: WARNING: py:meth reference target not found: asyncio.DefaultEventLoopPolicy.get_event_loop [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:17221: WARNING: py:data reference target not found: ctypes.wintypes.BYTE [ref.data]
/home/pablogsal/github/python/main/Doc/build/NEWS:17234: WARNING: py:mod reference target not found: elementtree [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:17333: WARNING: 'opcode' reference target not found: IMPORT_STAR [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:17333: WARNING: 'opcode' reference target not found: PRINT_EXPR [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:17333: WARNING: 'opcode' reference target not found: STOPITERATION_ERROR [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:17347: WARNING: py:func reference target not found: int.__sizeof__ [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:17442: WARNING: py:const reference target not found: socket.IP_PKTINFO [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:17453: WARNING: py:mod reference target not found: pyexpat [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:17494: WARNING: py:func reference target not found: http.cookiejar.eff_request_host [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:17500: WARNING: py:meth reference target not found: Fraction.is_integer [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:17574: WARNING: py:func reference target not found: iscoroutinefunction [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:17578: WARNING: py:class reference target not found: multiprocessing.queues.Queue [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:17755: WARNING: py:class reference target not found: BaseHTTPRequestHandler [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:17912: WARNING: py:meth reference target not found: asyncio.BaseDefaultEventLoopPolicy.get_event_loop [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:17912: WARNING: py:class reference target not found: asyncio.BaseDefaultEventLoopPolicy [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:17946: WARNING: py:meth reference target not found: TarFile.next [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:17949: WARNING: py:class reference target not found: WeakMethod [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:17996: WARNING: py:exc reference target not found: sqlite.DataError [ref.exc]
/home/pablogsal/github/python/main/Doc/build/NEWS:18129: WARNING: py:data reference target not found: sys._base_executable [ref.data]
/home/pablogsal/github/python/main/Doc/build/NEWS:18212: WARNING: py:attr reference target not found: types.CodeType.co_code [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:18265: WARNING: py:class reference target not found: asyncio.AbstractChildWatcher [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:18314: WARNING: py:mod reference target not found: importlib._bootstrap [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:18353: WARNING: py:func reference target not found: os.ismount [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:18490: WARNING: py:func reference target not found: os.exec [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:18631: WARNING: py:func reference target not found: sys.getdxp [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:18922: WARNING: py:meth reference target not found: __index__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:18925: WARNING: py:meth reference target not found: bool.__repr__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:19046: WARNING: py:attr reference target not found: types.CodeType.co_code [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:19105: WARNING: py:attr reference target not found: __text_signature__ [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:19105: WARNING: py:meth reference target not found: __get__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:19180: WARNING: py:meth reference target not found: tkinter.Text.count [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:19209: WARNING: py:meth reference target not found: asyncio.AbstractEventLoopPolicy.get_child_watcher [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:19209: WARNING: py:meth reference target not found: asyncio.AbstractEventLoopPolicy.set_child_watcher [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:19226: WARNING: py:class reference target not found: asyncio.MultiLoopChildWatcher [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:19226: WARNING: py:class reference target not found: asyncio.FastChildWatcher [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:19226: WARNING: py:class reference target not found: asyncio.SafeChildWatcher [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:19231: WARNING: py:class reference target not found: asyncio.PidfdChildWatcher [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:19271: WARNING: py:mod reference target not found: dataclass [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:19284: WARNING: py:meth reference target not found: gzip.GzipFile.read [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:19294: WARNING: py:class reference target not found: tkinter.Checkbutton [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:19324: WARNING: py:mod reference target not found: multiprocessing.resource_tracker [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:19382: WARNING: py:func reference target not found: threading.Event.__init__ [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19385: WARNING: py:class reference target not found: asyncio.streams.StreamReaderProtocol [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:19421: WARNING: py:meth reference target not found: asyncio.AbstractChildWatcher.attach_loop [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:19507: WARNING: py:meth reference target not found: wsgiref.types.InputStream.__iter__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:19516: WARNING: c:identifier reference target not found: _PyAccu [ref.identifier]
/home/pablogsal/github/python/main/Doc/build/NEWS:19516: WARNING: c:identifier reference target not found: _PyUnicodeWriter [ref.identifier]
/home/pablogsal/github/python/main/Doc/build/NEWS:19516: WARNING: c:identifier reference target not found: _PyAccu [ref.identifier]
/home/pablogsal/github/python/main/Doc/build/NEWS:19547: WARNING: py:meth reference target not found: SSLContext.set_default_verify_paths [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:19597: WARNING: py:mod reference target not found: xml.etree [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:19597: WARNING: py:mod reference target not found: xml.etree [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:19613: WARNING: py:attr reference target not found: dispatch_table [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:19643: WARNING: py:func reference target not found: locale.format [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19646: WARNING: py:func reference target not found: ssl.match_hostname [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19646: WARNING: py:func reference target not found: ssl.match_hostname [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19646: WARNING: py:func reference target not found: ssl.match_hostname [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19654: WARNING: py:func reference target not found: ssl.wrap_socket [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19654: WARNING: py:func reference target not found: ssl.wrap_socket [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19661: WARNING: py:func reference target not found: ssl.RAND_pseudo_bytes [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19677: WARNING: py:class reference target not found: asyncio.PidfdChildWatcher [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:19699: WARNING: py:func reference target not found: asyncio.iscoroutinefunction [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19782: WARNING: py:class reference target not found: multiprocessing.Pool [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:19813: WARNING: py:class reference target not found: wsgiref.BaseHandler [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:19819: WARNING: py:func reference target not found: locale.resetlocale [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19830: WARNING: py:func reference target not found: re.template [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19830: WARNING: py:const reference target not found: re.TEMPLATE [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:19830: WARNING: py:const reference target not found: re.T [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:19839: WARNING: py:exc reference target not found: re.error [ref.exc]
/home/pablogsal/github/python/main/Doc/build/NEWS:19856: WARNING: py:func reference target not found: venv.ensure_directories [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19863: WARNING: py:func reference target not found: sqlite.connect [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:19863: WARNING: py:class reference target not found: sqlite.Connection [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:19968: WARNING: py:class reference target not found: multiprocessing.SharedMemory [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:20023: WARNING: py:class reference target not found: QueueHandler [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:20023: WARNING: py:class reference target not found: LogRecord [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:20053: WARNING: py:class reference target not found: zipfile.ZipExtFile [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:20068: WARNING: py:meth reference target not found: collections.UserDict.get [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:20263: WARNING: py:meth reference target not found: calendar.LocaleTextCalendar.formatweekday [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:20502: WARNING: py:func reference target not found: ntpath.normcase [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:20618: WARNING: py:const reference target not found: Py_TPFLAGS_IMMUTABLETYPE [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:20810: WARNING: py:class reference target not found: generic_alias_iterator [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:20820: WARNING: py:class reference target not found: EncodingMap [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:20850: WARNING: py:meth reference target not found: add_note [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:20903: WARNING: py:class reference target not found: ctypes.UnionType [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:20903: WARNING: py:class reference target not found: testcapi.RecursingInfinitelyError [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:20976: WARNING: py:func reference target not found: os.fcopyfile [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:21008: WARNING: py:meth reference target not found: TextIOWrapper.reconfigure [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:21032: WARNING: py:const reference target not found: signal.SIGRTMIN [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:21032: WARNING: py:const reference target not found: signal.SIGRTMAX [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:21075: WARNING: py:exc reference target not found: re.error [ref.exc]
/home/pablogsal/github/python/main/Doc/build/NEWS:21080: WARNING: py:class reference target not found: multiprocessing.BaseManager [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:21087: WARNING: py:exc reference target not found: re.error [ref.exc]
/home/pablogsal/github/python/main/Doc/build/NEWS:21114: WARNING: py:func reference target not found: Tools.gdb.libpython.write_repr [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:21139: WARNING: py:class reference target not found: TextIOWrapper [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:21171: WARNING: py:class reference target not found: TextIOWrapper [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:21219: WARNING: py:meth reference target not found: __init__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:21277: WARNING: py:meth reference target not found: __init_subclass__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:21288: WARNING: py:func reference target not found: CookieJar.__iter__ [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:21310: WARNING: py:class reference target not found: asyncio.streams.StreamWriter [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:21370: WARNING: 'envvar' reference target not found: PYTHONREGRTEST_UNICODE_GUARD [ref.envvar]
/home/pablogsal/github/python/main/Doc/build/NEWS:21382: WARNING: py:mod reference target not found: ctypes.macholib.dyld [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:21382: WARNING: py:mod reference target not found: ctypes.macholib.dylib [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:21382: WARNING: py:mod reference target not found: ctypes.macholib.framework [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:21382: WARNING: py:mod reference target not found: ctypes.test [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:21501: WARNING: 'opcode' reference target not found: JUMP_IF_NOT_EG_MATCH [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:21511: WARNING: 'opcode' reference target not found: JUMP_IF_NOT_EXC_MATCH [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:21523: WARNING: c:macro reference target not found: PY_CALL_TRAMPOLINE [ref.macro]
/home/pablogsal/github/python/main/Doc/build/NEWS:21541: WARNING: 'opcode' reference target not found: JUMP_ABSOLUTE [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:21615: WARNING: py:const reference target not found: CTYPES_MAX_ARGCOUNT [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:21630: WARNING: py:meth reference target not found: ZipFile.mkdir [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:21648: WARNING: py:exc reference target not found: URLError [ref.exc]
/home/pablogsal/github/python/main/Doc/build/NEWS:21648: WARNING: py:class reference target not found: urllib.request.URLopener [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:21648: WARNING: py:func reference target not found: urllib.request.URLopener.open_ftp [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:21654: WARNING: py:func reference target not found: Exception.with_traceback [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:21687: WARNING: py:meth reference target not found: zipfile._SharedFile.tell [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:21687: WARNING: py:class reference target not found: ZipFile [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:21784: WARNING: py:class reference target not found: asyncio.base_events.Server [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:21803: WARNING: py:meth reference target not found: asyncio.AbstractEventLoop.sock_sendto [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:21803: WARNING: py:meth reference target not found: asyncio.AbstractEventLoop.sock_recvfrom [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:21803: WARNING: py:meth reference target not found: asyncio.AbstractEventLoop.sock_recvfrom_into [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:21812: WARNING: py:class reference target not found: GenericAlias [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:21843: WARNING: py:class reference target not found: BasicInterpolation [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:21843: WARNING: py:class reference target not found: ExtendedInterpolation [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:21867: WARNING: py:meth reference target not found: MimeTypes.guess_type [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22007: WARNING: 'envvar' reference target not found: PYLAUNCHER_ALLOW_INSTALL [ref.envvar]
/home/pablogsal/github/python/main/Doc/build/NEWS:22121: WARNING: 'opcode' reference target not found: LOAD_METHOD [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:22124: WARNING: 'opcode' reference target not found: BINARY_SUBSCR [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:22164: WARNING: py:meth reference target not found: BaseException.__str__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22260: WARNING: py:meth reference target not found: mmap.find [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22260: WARNING: py:meth reference target not found: mmap.rfind [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22308: WARNING: py:meth reference target not found: __repr__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22381: WARNING: py:meth reference target not found: __eq__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22381: WARNING: py:meth reference target not found: __hash__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22396: WARNING: py:data reference target not found: re.RegexFlag.NOFLAG [ref.data]
/home/pablogsal/github/python/main/Doc/build/NEWS:22547: WARNING: py:meth reference target not found: __trunc__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22547: WARNING: py:meth reference target not found: __trunc__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22547: WARNING: py:meth reference target not found: __int__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22547: WARNING: py:meth reference target not found: __index__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22581: WARNING: py:meth reference target not found: BaseExceptionGroup.__new__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22587: WARNING: py:meth reference target not found: weakref.ref.__call__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22629: WARNING: py:data reference target not found: sys._base_executable [ref.data]
/home/pablogsal/github/python/main/Doc/build/NEWS:22662: WARNING: py:class reference target not found: asyncio.transports.WriteTransport [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:22690: WARNING: py:meth reference target not found: mock.patch [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22743: WARNING: py:meth reference target not found: enum.Enum.__call__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22760: WARNING: py:attr reference target not found: __bases__ [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:22815: WARNING: py:func reference target not found: test.support.requires_fork [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:22818: WARNING: py:func reference target not found: test.support.requires_subprocess [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:22982: WARNING: c:macro reference target not found: PyLong_BASE [ref.macro]
/home/pablogsal/github/python/main/Doc/build/NEWS:22988: WARNING: py:meth reference target not found: ExceptionGroup.split [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:22988: WARNING: py:meth reference target not found: ExceptionGroup.subgroup [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:23077: WARNING: py:attr reference target not found: types.CodeType.co_firstlineno [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:23109: WARNING: py:mod reference target not found: asyncio.windows_events [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:23121: WARNING: py:attr reference target not found: webbrowser.MacOSXOSAScript._name [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:23150: WARNING: py:meth reference target not found: add_argument_group [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:23150: WARNING: py:meth reference target not found: add_argument_group [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:23150: WARNING: py:meth reference target not found: add_mutually_exclusive_group [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:23182: WARNING: py:attr reference target not found: __all__ [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:23206: WARNING: py:meth reference target not found: __repr__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:23244: WARNING: py:meth reference target not found: enum.Flag._missing_ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:23267: WARNING: c:func reference target not found: Py_FrozenMain [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:23422: WARNING: 'opcode' reference target not found: BINARY_SUBSCR [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:23516: WARNING: py:meth reference target not found: turtle.RawTurtle.tiltangle [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:23518: WARNING: py:meth reference target not found: turtle.RawTurtle.tiltangle [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:23529: WARNING: py:mod reference target not found: sqlite [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:23599: WARNING: py:class reference target not found: ProcessPoolExecutor [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:23749: WARNING: py:mod reference target not found: pyexpat [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:24026: WARNING: py:func reference target not found: inspect.getabsfile [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:24065: WARNING: py:class reference target not found: Signature [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:24164: WARNING: py:mod reference target not found: test.libregrtest [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:24218: WARNING: py:mod reference target not found: pyexpat [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:24234: WARNING: py:meth reference target not found: argparse.parse_known_args [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:24490: WARNING: py:meth reference target not found: __bytes__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:24494: WARNING: py:meth reference target not found: __complex__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:24788: WARNING: py:class reference target not found: sqlite.Statement [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:24810: WARNING: c:func reference target not found: type_new [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:24850: WARNING: py:func reference target not found: str.__getitem__ [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:24913: WARNING: py:class reference target not found: pyexpat.xmlparser [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:24923: WARNING: py:func reference target not found: threading._shutdown [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:24943: WARNING: py:meth reference target not found: unittest.IsolatedAsyncioTestCase.debug [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25007: WARNING: py:meth reference target not found: <unittest.TestLoader.loadTestsFromModule> TestLoader.loadTestsFromModule [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25071: WARNING: py:meth reference target not found: traceback.StackSummary.format_frame [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25077: WARNING: py:meth reference target not found: traceback.StackSummary.format_frame [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25156: WARNING: py:exc reference target not found: UnicodEncodeError [ref.exc]
/home/pablogsal/github/python/main/Doc/build/NEWS:25191: WARNING: py:meth reference target not found: collections.OrderedDict.pop [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25213: WARNING: py:mod reference target not found: rcompleter [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:25277: WARNING: py:class reference target not found: ExitStack [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:25277: WARNING: py:class reference target not found: AsyncExitStack [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:25287: WARNING: py:const reference target not found: os.path.sep [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:25292: WARNING: py:func reference target not found: StackSummary.format_frame [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:25307: WARNING: py:func reference target not found: pdb.main [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:25368: WARNING: py:meth reference target not found: bz2.BZ2File.write [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25368: WARNING: py:meth reference target not found: lzma.LZMAFile.write [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25398: WARNING: py:meth reference target not found: email.message.MIMEPart.as_string [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25412: WARNING: py:func reference target not found: parse_makefile [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:25465: WARNING: py:deco reference target not found: asyncio.coroutine [ref.deco]
/home/pablogsal/github/python/main/Doc/build/NEWS:25465: WARNING: py:class reference target not found: asyncio.coroutines.CoroWrapper [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:25512: WARNING: py:func reference target not found: runtime_checkable [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:25545: WARNING: py:func reference target not found: functool.lru_cache [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:25557: WARNING: py:meth reference target not found: pdb.Pdb.checkline [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25557: WARNING: py:meth reference target not found: pdb.Pdb.reset [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25574: WARNING: py:func reference target not found: shutil._unpack_zipfile [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:25584: WARNING: py:func reference target not found: importlib._bootstrap._find_and_load [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:25592: WARNING: py:meth reference target not found: loop.set_default_executor [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25597: WARNING: py:class reference target not found: asyncio.trsock.TransportSocket [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:25638: WARNING: py:mod reference target not found: tkinter.tix [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:25779: WARNING: py:class reference target not found: TextWrap [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:25828: WARNING: py:meth reference target not found: __init__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25828: WARNING: py:meth reference target not found: __post_init__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:25875: WARNING: py:func reference target not found: unittest.create_autospec [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:25961: WARNING: c:func reference target not found: Py_FrozenMain [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:26014: WARNING: 'envvar' reference target not found: EnableControlFlowGuard [ref.envvar]
/home/pablogsal/github/python/main/Doc/build/NEWS:26137: WARNING: py:meth reference target not found: BufferedReader.peek [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:26216: WARNING: c:func reference target not found: Py_FrozenMain [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:26291: WARNING: py:func reference target not found: sqlite3.connect/handle [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:26400: WARNING: c:func reference target not found: PyErr_Display [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:26444: WARNING: c:func reference target not found: PyErr_Display [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:26511: WARNING: py:func reference target not found: inspect.from_callable [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:26511: WARNING: py:func reference target not found: inspect.from_function [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:26511: WARNING: py:func reference target not found: inspect.from_callable [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:26615: WARNING: py:data reference target not found: TypeGuard [ref.data]
/home/pablogsal/github/python/main/Doc/build/NEWS:26632: WARNING: py:func reference target not found: logging.fileConfig [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:26728: WARNING: py:class reference target not found: asyncio.StreamReaderProtocol [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:26819: WARNING: py:mod reference target not found: test.libregrtest [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:27132: WARNING: py:func reference target not found: subprocess.communicate [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:27151: WARNING: py:func reference target not found: cleanup [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:27172: WARNING: py:meth reference target not found: HTTPConnection.set_tunnel [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:27453: WARNING: py:func reference target not found: multiprocess.synchronize [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:27453: WARNING: py:class reference target not found: ProcessPoolExecutor [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:27713: WARNING: py:func reference target not found: randbytes [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:27722: WARNING: py:func reference target not found: TracebackException.format [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:27722: WARNING: py:func reference target not found: TracebackException.format_exception_only [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:27782: WARNING: py:class reference target not found: Threading.thread [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:27809: WARNING: py:meth reference target not found: unittest.TestLoader().loadTestsFromTestCase [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:27809: WARNING: py:meth reference target not found: unittest.makeSuite [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:27951: WARNING: py:mod reference target not found: pyexpat [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:27970: WARNING: py:class reference target not found: tkinter.Variable [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:27991: WARNING: py:func reference target not found: tkinter.NoDefaultRoot [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:28021: WARNING: py:func reference target not found: tracemalloc.Traceback.__repr__ [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:28029: WARNING: py:func reference target not found: atexit._run_exitfuncs [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:28086: WARNING: py:func reference target not found: posixpath.expanduser [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:28133: WARNING: py:mod reference target not found: zipimporter [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:28141: WARNING: py:class reference target not found: tkinter.ttk.LabeledScale [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:28147: WARNING: py:func reference target not found: a85encode [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:28147: WARNING: py:func reference target not found: b85encode [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:28226: WARNING: c:func reference target not found: Py_FrozenMain [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:28370: WARNING: py:func reference target not found: inspect.findsource [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:28370: WARNING: py:attr reference target not found: co_lineno [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:28551: WARNING: py:func reference target not found: pprint._safe_repr [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:28555: WARNING: c:func reference target not found: splice [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:28847: WARNING: c:func reference target not found: PyAST_Validate [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:29065: WARNING: py:meth reference target not found: __class_getitem__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:29215: WARNING: py:meth reference target not found: __dir__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:29322: WARNING: py:mod reference target not found: winapi [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:29335: WARNING: py:mod reference target not found: sha256 [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:29375: WARNING: py:mod reference target not found: symbol [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:29407: WARNING: py:mod reference target not found: parser [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:29446: WARNING: c:func reference target not found: PyOS_Readline [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:29606: WARNING: py:meth reference target not found: turtle.Vec2D.__rmul__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:29694: WARNING: py:class reference target not found: shared_memory.SharedMemory [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:29697: WARNING: py:meth reference target not found: collections.OrderedDict.pop [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:29752: WARNING: py:func reference target not found: pdb.find_function [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:29841: WARNING: py:func reference target not found: csv.writer.writerow [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:29841: WARNING: py:meth reference target not found: csv.writer.writerows [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:29866: WARNING: py:func reference target not found: hashlib.compare_digest [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:29882: WARNING: py:mod reference target not found: symbol [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:29930: WARNING: py:mod reference target not found: xml.etree.cElementTree [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:29940: WARNING: py:func reference target not found: unittest.assertNoLogs [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:29961: WARNING: py:class reference target not found: multiprocessing.context.get_all_start_methods [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:29976: WARNING: py:meth reference target not found: IMAP4.noop [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:30079: WARNING: py:data reference target not found: test.support.TESTFN [ref.data]
/home/pablogsal/github/python/main/Doc/build/NEWS:30459: WARNING: py:meth reference target not found: Future.cancel [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:30459: WARNING: py:meth reference target not found: Task.cancel [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:30841: WARNING: py:meth reference target not found: ShareableList.__setitem__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:30844: WARNING: py:meth reference target not found: pathlib.Path.with_stem [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:30900: WARNING: py:func reference target not found: posix.sysconf [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:31305: WARNING: py:class reference target not found: multiprocessing.Pool [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:31416: WARNING: py:meth reference target not found: tempfile.SpooledTemporaryFile.softspace [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:31700: WARNING: py:meth reference target not found: list.__contains__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:31756: WARNING: py:meth reference target not found: io.BufferedReader.truncate [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:31810: WARNING: py:func reference target not found: unittest.case.shortDescription [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:31984: WARNING: c:member reference target not found: PyThreadState.on_delete [ref.member]
/home/pablogsal/github/python/main/Doc/build/NEWS:32015: WARNING: py:class reference target not found: functools.TopologicalSorter [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:32042: WARNING: py:meth reference target not found: __aenter__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:32042: WARNING: py:meth reference target not found: __aexit__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:32141: WARNING: py:mod reference target not found: binhex [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:32141: WARNING: py:func reference target not found: binascii.b2a_hqx [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32141: WARNING: py:func reference target not found: binascii.a2b_hqx [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32141: WARNING: py:func reference target not found: binascii.rlecode_hqx [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32141: WARNING: py:func reference target not found: binascii.rledecode_hqx [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32229: WARNING: py:func reference target not found: urllib.request.proxy_bypass_environment [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32238: WARNING: py:func reference target not found: mock.patch.stopall [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32238: WARNING: py:func reference target not found: mock.patch.dict [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32263: WARNING: py:func reference target not found: Popen.communicate [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32278: WARNING: py:func reference target not found: unittest.mock.attach_mock [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32311: WARNING: c:func reference target not found: setenv [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32311: WARNING: c:func reference target not found: unsetenv [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32534: WARNING: py:func reference target not found: is_cgi [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32582: WARNING: py:class reference target not found: zipfile.ZipExtFile [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:32587: WARNING: py:func reference target not found: enum._decompose [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32649: WARNING: py:func reference target not found: test.support.run_python_until_end [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32649: WARNING: py:func reference target not found: test.support.assert_python_ok [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32649: WARNING: py:func reference target not found: test.support.assert_python_failure [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:32790: WARNING: py:meth reference target not found: float.__getformat__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:32974: WARNING: py:meth reference target not found: list.__contains__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:32974: WARNING: py:meth reference target not found: tuple.__contains__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:32978: WARNING: py:meth reference target not found: builtins.__import__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:32985: WARNING: py:class reference target not found: ast.parameters [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:32994: WARNING: c:func reference target not found: PyErr_Display [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:33138: WARNING: py:class reference target not found: asyncio.PidfdChildWatcher [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:33144: WARNING: py:const reference target not found: fcntl.F_OFD_GETLK [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:33144: WARNING: py:const reference target not found: fcntl.F_OFD_SETLK [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:33144: WARNING: py:const reference target not found: fcntl.F_OFD_SETLKW [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:33148: WARNING: py:class reference target not found: zipfile.ZipExtFile [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:33164: WARNING: py:func reference target not found: pathlib.WindowsPath.glob [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:33172: WARNING: py:attr reference target not found: si_code [ref.attr]
/home/pablogsal/github/python/main/Doc/build/NEWS:33175: WARNING: py:meth reference target not found: inspect.signature.bind [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:33205: WARNING: py:func reference target not found: email.message.get [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:33324: WARNING: py:meth reference target not found: loop.shutdown_default_executor [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:33342: WARNING: py:meth reference target not found: datetime.utctimetuple [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:33342: WARNING: py:meth reference target not found: datetime.utcnow [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:33342: WARNING: py:meth reference target not found: datetime.utcfromtimestamp [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:33366: WARNING: py:class reference target not found: ForwardReferences [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:33386: WARNING: py:func reference target not found: tee [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:33453: WARNING: py:class reference target not found: ArgumentParser [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:33505: WARNING: py:meth reference target not found: writelines [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:33539: WARNING: py:mod reference target not found: parser [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:33556: WARNING: py:meth reference target not found: is_relative_to [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:33556: WARNING: py:class reference target not found: PurePath [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:33617: WARNING: py:func reference target not found: unittest.mock.attach_mock [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:33645: WARNING: py:func reference target not found: multiprocessing.util.get_temp_dir [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:33769: WARNING: py:meth reference target not found: RobotFileParser.crawl_delay [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:33769: WARNING: py:meth reference target not found: RobotFileParser.request_rate [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:33796: WARNING: py:meth reference target not found: CookieJar.make_cookies [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:33829: WARNING: py:func reference target not found: socket.recv.fds [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:33849: WARNING: py:class reference target not found: ZipInfo [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:34004: WARNING: py:class reference target not found: Request [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:34135: WARNING: py:func reference target not found: test.support.catch_threading_exception [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:34363: WARNING: py:func reference target not found: os.realpath [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:34388: WARNING: 'envvar' reference target not found: PIP_USER [ref.envvar]
/home/pablogsal/github/python/main/Doc/build/NEWS:34415: WARNING: c:func reference target not found: strcasecmp [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:34584: WARNING: c:macro reference target not found: PY_SSIZE_T_CLEAN [ref.macro]
/home/pablogsal/github/python/main/Doc/build/NEWS:34861: WARNING: py:func reference target not found: copy_file_range [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:34997: WARNING: py:meth reference target not found: urllib.request.URLopener.retrieve [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.Stream [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.Stream [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:func reference target not found: asyncio.connect [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:func reference target not found: asyncio.connect_unix [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:func reference target not found: asyncio.connect_read_pipe [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:func reference target not found: asyncio.connect_write_pipe [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.Stream [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.StreamServer [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: UnixStreamServer [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.Stream [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: StreamReader [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: StreamWriter [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.FlowControlMixing [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.StreamReaderProtocol [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:35131: WARNING: py:meth reference target not found: wsgiref.handlers.BaseHandler.close [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:35163: WARNING: py:meth reference target not found: csv.Writer.writerow [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:35178: WARNING: py:meth reference target not found: asyncio.SelectorEventLoop.subprocess_exec [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:35219: WARNING: py:meth reference target not found: asyncio.AbstractEventLoop.create_datagram_endpoint [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:35460: WARNING: py:data reference target not found: posixpath.defpath [ref.data]
/home/pablogsal/github/python/main/Doc/build/NEWS:35662: WARNING: py:meth reference target not found: imap.IMAP4.logout [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:35700: WARNING: py:class reference target not found: tkinter.PhotoImage [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:35740: WARNING: rst:dir reference target not found: literalinclude [ref.dir]
/home/pablogsal/github/python/main/Doc/build/NEWS:35957: WARNING: c:identifier reference target not found: name [ref.identifier]
/home/pablogsal/github/python/main/Doc/build/NEWS:35957: WARNING: c:identifier reference target not found: name [ref.identifier]
/home/pablogsal/github/python/main/Doc/build/NEWS:35957: WARNING: c:identifier reference target not found: str [ref.identifier]
/home/pablogsal/github/python/main/Doc/build/NEWS:36164: WARNING: py:class reference target not found: FileCookieJar [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:36190: WARNING: py:class reference target not found: multiprocessing.Pool [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:36413: WARNING: py:class reference target not found: multiprocessing.Pool [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:36433: WARNING: py:meth reference target not found: datetime.fromtimestamp [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:36440: WARNING: py:class reference target not found: xmlrpc.client.Transport [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:36440: WARNING: py:class reference target not found: xmlrpc.client.SafeTransport [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:36474: WARNING: py:func reference target not found: test.support.check_syntax_warning [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:36651: WARNING: py:meth reference target not found: float.__format__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:36651: WARNING: py:meth reference target not found: complex.__format__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:36661: WARNING: py:func reference target not found: namedtuple [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:36898: WARNING: py:class reference target not found: BuiltinMethodType [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:36898: WARNING: py:class reference target not found: ModuleType [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:36898: WARNING: py:class reference target not found: MethodWrapperType [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:36898: WARNING: py:class reference target not found: MethodWrapperType [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: BREAK_LOOP [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: CONTINUE_LOOP [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: SETUP_LOOP [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: SETUP_EXCEPT [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: ROT_FOUR [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: BEGIN_FINALLY [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: CALL_FINALLY [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: POP_FINALLY [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: END_FINALLY [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: WITH_CLEANUP_START [ref.opcode]
/home/pablogsal/github/python/main/Doc/build/NEWS:37170: WARNING: py:class reference target not found: ast.Num [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:37198: WARNING: py:meth reference target not found: threading.Thread.isAlive [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:37243: WARNING: py:class reference target not found: unittest.runner.TextTestRunner [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:37243: WARNING: py:mod reference target not found: unittest.runner [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:37263: WARNING: py:meth reference target not found: multiprocessing.Pool.__enter__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:37288: WARNING: py:class reference target not found: multiprocessing.Pool [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:37291: WARNING: py:class reference target not found: Mock [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:37300: WARNING: py:func reference target not found: distutils.utils.check_environ [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:37304: WARNING: py:func reference target not found: posixpath.expanduser [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:37451: WARNING: py:func reference target not found: multiprocessing.reduction.recvfds [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:37552: WARNING: py:meth reference target not found: Executor.map [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:37552: WARNING: py:func reference target not found: as_completed [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:37562: WARNING: py:class reference target not found: QueueHandler [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:37562: WARNING: py:class reference target not found: LogRecord [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:37652: WARNING: py:class reference target not found: multiprocessing.managers.DictProxy [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:37780: WARNING: py:meth reference target not found: asyncio.AbstractEventLoop.create_task [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:37798: WARNING: py:meth reference target not found: AbstractEventLoop.set_default_executor [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:37843: WARNING: py:exc reference target not found: base64.Error [ref.exc]
/home/pablogsal/github/python/main/Doc/build/NEWS:38171: WARNING: py:class reference target not found: cProfile.Profile [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:38261: WARNING: py:mod reference target not found: parser [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:38300: WARNING: py:meth reference target not found: importlib.machinery.invalidate_caches [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:38407: WARNING: py:meth reference target not found: hosts [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:38585: WARNING: py:func reference target not found: socket.recvfrom [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:38742: WARNING: py:func reference target not found: islice [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:38769: WARNING: py:meth reference target not found: __getattr__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:38769: WARNING: py:meth reference target not found: get [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:38811: WARNING: py:func reference target not found: tearDownModule [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:38826: WARNING: py:class reference target not found: multiprocessing.Pool [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:38830: WARNING: py:mod reference target not found: test.bisect [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:38830: WARNING: py:mod reference target not found: test.bisect_cmd [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:38837: WARNING: py:func reference target not found: test.support.run_unittest [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:38837: WARNING: py:exc reference target not found: TestDidNotRun [ref.exc]
/home/pablogsal/github/python/main/Doc/build/NEWS:39155: WARNING: py:meth reference target not found: datetime.fromtimestamp [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:39918: WARNING: py:mod reference target not found: parser [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:39938: WARNING: py:meth reference target not found: importlib.machinery.invalidate_caches [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:40155: WARNING: py:meth reference target not found: hosts [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:40191: WARNING: py:func reference target not found: islice [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:40419: WARNING: py:func reference target not found: socket.recvfrom [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:40446: WARNING: py:meth reference target not found: __getattr__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:40446: WARNING: py:meth reference target not found: get [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:40644: WARNING: py:meth reference target not found: asyncio.AbstractEventLoop.sendfile [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:40718: WARNING: py:meth reference target not found: get_resource_reader [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:41088: WARNING: py:class reference target not found: ProcessPoolExecutor [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:41280: WARNING: py:meth reference target not found: ssl.match_hostname [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:42557: WARNING: py:func reference target not found: asyncio._get_running_loop [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:42880: WARNING: py:mod reference target not found: macpath [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:43071: WARNING: py:const reference target not found: socket.TCP_NOTSENT_LOWAT [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:43258: WARNING: py:const reference target not found: socket.TCP_CONGESTION [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:43258: WARNING: py:const reference target not found: socket.TCP_USER_TIMEOUT [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:44232: WARNING: py:mod reference target not found: parser [ref.mod]
/home/pablogsal/github/python/main/Doc/build/NEWS:44266: WARNING: py:meth reference target not found: hosts [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:44306: WARNING: py:func reference target not found: islice [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:44649: WARNING: py:meth reference target not found: __getattr__ [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:44649: WARNING: py:meth reference target not found: get [ref.meth]
/home/pablogsal/github/python/main/Doc/build/NEWS:45334: WARNING: py:func reference target not found: asyncio._get_running_loop [ref.func]
/home/pablogsal/github/python/main/Doc/build/NEWS:46469: WARNING: py:const reference target not found: socket.TCP_CONGESTION [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:46469: WARNING: py:const reference target not found: socket.TCP_USER_TIMEOUT [ref.const]
/home/pablogsal/github/python/main/Doc/build/NEWS:48767: WARNING: py:class reference target not found: warnings.WarningMessage [ref.class]
/home/pablogsal/github/python/main/Doc/build/NEWS:53512: WARNING: py:class reference target not found: email.feedparser.FeedParser [ref.class]

View file

@ -1937,6 +1937,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(only_keys));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(oparg));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opcode));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opcodes));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(open));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opener));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(operation));

View file

@ -660,6 +660,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(only_keys)
STRUCT_FOR_ID(oparg)
STRUCT_FOR_ID(opcode)
STRUCT_FOR_ID(opcodes)
STRUCT_FOR_ID(open)
STRUCT_FOR_ID(opener)
STRUCT_FOR_ID(operation)

View file

@ -1935,6 +1935,7 @@ extern "C" {
INIT_ID(only_keys), \
INIT_ID(oparg), \
INIT_ID(opcode), \
INIT_ID(opcodes), \
INIT_ID(open), \
INIT_ID(opener), \
INIT_ID(operation), \

View file

@ -2420,6 +2420,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(opcodes);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(open);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));

View file

@ -862,6 +862,84 @@ .tooltip-hint {
text-align: center;
}
/* --------------------------------------------------------------------------
Tooltip Bytecode/Opcode Section
-------------------------------------------------------------------------- */
.tooltip-opcodes {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.tooltip-opcodes-title {
color: var(--accent);
font-size: 13px;
margin-bottom: 8px;
font-weight: 600;
}
.tooltip-opcodes-list {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px;
}
.tooltip-opcode-row {
display: grid;
grid-template-columns: 1fr 60px 60px;
gap: 8px;
align-items: center;
padding: 3px 0;
}
.tooltip-opcode-name {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tooltip-opcode-name.specialized {
color: var(--spec-high-text);
}
.tooltip-opcode-base-hint {
color: var(--text-muted);
font-size: 11px;
margin-left: 4px;
}
.tooltip-opcode-badge {
background: var(--spec-high);
color: white;
font-size: 9px;
padding: 1px 4px;
border-radius: 3px;
margin-left: 4px;
}
.tooltip-opcode-count {
text-align: right;
font-size: 11px;
color: var(--text-secondary);
}
.tooltip-opcode-bar {
background: var(--bg-secondary);
border-radius: 2px;
height: 8px;
overflow: hidden;
}
.tooltip-opcode-bar-fill {
background: linear-gradient(90deg, var(--python-blue), var(--python-blue-light));
height: 100%;
}
/* --------------------------------------------------------------------------
Responsive (Flamegraph-specific)
-------------------------------------------------------------------------- */

View file

@ -8,6 +8,32 @@ let currentThreadFilter = 'all';
// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
// and automatically switch with theme changes - no JS color arrays needed!
// Opcode mappings - loaded from embedded data (generated by Python)
let OPCODE_NAMES = {};
let DEOPT_MAP = {};
// Initialize opcode mappings from embedded data
function initOpcodeMapping(data) {
if (data && data.opcode_mapping) {
OPCODE_NAMES = data.opcode_mapping.names || {};
DEOPT_MAP = data.opcode_mapping.deopt || {};
}
}
// Get opcode info from opcode number
function getOpcodeInfo(opcode) {
const opname = OPCODE_NAMES[opcode] || `<${opcode}>`;
const baseOpcode = DEOPT_MAP[opcode];
const isSpecialized = baseOpcode !== undefined;
const baseOpname = isSpecialized ? (OPCODE_NAMES[baseOpcode] || `<${baseOpcode}>`) : opname;
return {
opname: opname,
baseOpname: baseOpname,
isSpecialized: isSpecialized
};
}
// ============================================================================
// String Resolution
// ============================================================================
@ -249,6 +275,53 @@ function createPythonTooltip(data) {
</div>`;
}
// Create bytecode/opcode section if available
let opcodeSection = "";
const opcodes = d.data.opcodes;
if (opcodes && typeof opcodes === 'object' && Object.keys(opcodes).length > 0) {
// Sort opcodes by sample count (descending)
const sortedOpcodes = Object.entries(opcodes)
.sort((a, b) => b[1] - a[1])
.slice(0, 8); // Limit to top 8
const totalOpcodeSamples = sortedOpcodes.reduce((sum, [, count]) => sum + count, 0);
const maxCount = sortedOpcodes[0][1] || 1;
const opcodeLines = sortedOpcodes.map(([opcode, count]) => {
const opcodeInfo = getOpcodeInfo(parseInt(opcode, 10));
const pct = ((count / totalOpcodeSamples) * 100).toFixed(1);
const barWidth = (count / maxCount) * 100;
const specializedBadge = opcodeInfo.isSpecialized
? '<span class="tooltip-opcode-badge">SPECIALIZED</span>'
: '';
const baseOpHint = opcodeInfo.isSpecialized
? `<span class="tooltip-opcode-base-hint">(${opcodeInfo.baseOpname})</span>`
: '';
const nameClass = opcodeInfo.isSpecialized
? 'tooltip-opcode-name specialized'
: 'tooltip-opcode-name';
return `
<div class="tooltip-opcode-row">
<div class="${nameClass}">
${opcodeInfo.opname}${baseOpHint}${specializedBadge}
</div>
<div class="tooltip-opcode-count">${count.toLocaleString()} (${pct}%)</div>
<div class="tooltip-opcode-bar">
<div class="tooltip-opcode-bar-fill" style="width: ${barWidth}%;"></div>
</div>
</div>`;
}).join('');
opcodeSection = `
<div class="tooltip-opcodes">
<div class="tooltip-opcodes-title">Bytecode Instructions:</div>
<div class="tooltip-opcodes-list">
${opcodeLines}
</div>
</div>`;
}
const fileLocationHTML = isSpecialFrame ? "" : `
<div class="tooltip-location">${filename}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
@ -275,6 +348,7 @@ function createPythonTooltip(data) {
` : ''}
</div>
${sourceSection}
${opcodeSection}
<div class="tooltip-hint">
${childCount > 0 ? "Click to zoom into this function" : "Leaf function - no children"}
</div>
@ -994,6 +1068,9 @@ function initFlamegraph() {
processedData = resolveStringIndices(EMBEDDED_DATA);
}
// Initialize opcode mapping from embedded data
initOpcodeMapping(EMBEDDED_DATA);
originalData = processedData;
initThreadFilter(processedData);

View file

@ -629,13 +629,18 @@ .legend {
}
.legend-content {
width: 94%;
max-width: 100%;
margin: 0 auto;
width: 100%;
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
flex-wrap: nowrap;
}
.legend-separator {
width: 1px;
height: 24px;
background: var(--border);
flex-shrink: 0;
}
.legend-title {
@ -643,12 +648,13 @@ .legend-title {
color: var(--text-primary);
font-size: 13px;
font-family: var(--font-sans);
flex-shrink: 0;
}
.legend-gradient {
flex: 1;
max-width: 300px;
height: 24px;
width: 150px;
flex-shrink: 0;
height: 20px;
background: linear-gradient(90deg,
var(--bg-tertiary) 0%,
var(--heat-2) 25%,
@ -666,6 +672,16 @@ .legend-labels {
font-size: 11px;
color: var(--text-muted);
font-family: var(--font-sans);
flex-shrink: 0;
}
/* Legend Controls Group - wraps toggles and bytecode button together */
.legend-controls {
display: flex;
align-items: center;
gap: 20px;
flex-shrink: 0;
margin-left: auto;
}
/* Toggle Switch Styles */
@ -677,6 +693,7 @@ .toggle-switch {
user-select: none;
font-family: var(--font-sans);
transition: opacity var(--transition-fast);
flex-shrink: 0;
}
.toggle-switch:hover {
@ -687,13 +704,10 @@ .toggle-switch .toggle-label {
font-size: 11px;
font-weight: 500;
color: var(--text-muted);
min-width: 55px;
text-align: right;
transition: color var(--transition-fast);
}
.toggle-switch .toggle-label:last-child {
text-align: left;
white-space: nowrap;
display: inline-flex;
flex-direction: column;
}
.toggle-switch .toggle-label.active {
@ -701,6 +715,20 @@ .toggle-switch .toggle-label.active {
font-weight: 600;
}
/* Reserve space for bold text to prevent layout shift on toggle */
.toggle-switch .toggle-label::after {
content: attr(data-text);
font-weight: 600;
height: 0;
visibility: hidden;
}
.toggle-switch.disabled {
opacity: 0.4;
pointer-events: none;
cursor: not-allowed;
}
.toggle-track {
position: relative;
width: 36px;
@ -1117,6 +1145,15 @@ @media (max-width: 1100px) {
.stats-summary {
grid-template-columns: repeat(2, 1fr);
}
.legend-content {
flex-wrap: wrap;
justify-content: center;
}
.legend-controls {
margin-left: 0;
}
}
@media (max-width: 900px) {
@ -1136,6 +1173,7 @@ @media (max-width: 600px) {
.legend-content {
flex-direction: column;
align-items: center;
gap: 12px;
}
@ -1143,4 +1181,400 @@ @media (max-width: 600px) {
width: 100%;
max-width: none;
}
.legend-separator {
width: 80%;
height: 1px;
}
.legend-controls {
flex-direction: column;
gap: 12px;
}
.legend-controls .toggle-switch {
justify-content: center;
}
.legend-controls .toggle-switch .toggle-label:first-child {
width: 70px;
text-align: right;
}
.legend-controls .toggle-switch .toggle-label:last-child {
width: 90px;
text-align: left;
}
/* Compact code columns on small screens */
.header-line-number,
.line-number {
width: 40px;
}
.header-samples-self,
.header-samples-cumulative,
.line-samples-self,
.line-samples-cumulative {
width: 55px;
font-size: 10px;
}
/* Adjust padding - headers need vertical, data rows don't */
.header-line-number,
.header-samples-self,
.header-samples-cumulative {
padding: 8px 4px;
}
.line-number,
.line-samples-self,
.line-samples-cumulative {
padding: 0 4px;
}
}
.bytecode-toggle {
flex-shrink: 0;
width: 20px;
height: 20px;
padding: 0;
margin: 0 4px;
border: none;
background: transparent;
color: var(--code-accent);
cursor: pointer;
font-size: 10px;
transition: transform var(--transition-fast), color var(--transition-fast);
display: inline-flex;
align-items: center;
justify-content: center;
}
.bytecode-toggle:hover {
color: var(--accent);
}
.bytecode-spacer {
flex-shrink: 0;
width: 20px;
height: 20px;
margin: 0 4px;
}
.bytecode-panel {
margin-left: 90px;
padding: 8px 15px;
background: var(--bg-secondary);
border-left: 3px solid var(--accent);
font-family: var(--font-mono);
font-size: 12px;
margin-bottom: 4px;
}
/* Specialization summary bar */
.bytecode-spec-summary {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
margin-bottom: 10px;
border-radius: var(--radius-sm);
background: rgba(100, 100, 100, 0.1);
}
.bytecode-spec-summary .spec-pct {
font-size: 1.4em;
font-weight: 700;
}
.bytecode-spec-summary .spec-label {
font-weight: 500;
text-transform: uppercase;
font-size: 0.85em;
letter-spacing: 0.5px;
}
.bytecode-spec-summary .spec-detail {
color: var(--text-secondary);
font-size: 0.9em;
margin-left: auto;
}
.bytecode-spec-summary.high {
background: var(--spec-high-bg);
border-left: 3px solid var(--spec-high);
}
.bytecode-spec-summary.high .spec-pct,
.bytecode-spec-summary.high .spec-label {
color: var(--spec-high-text);
}
.bytecode-spec-summary.medium {
background: var(--spec-medium-bg);
border-left: 3px solid var(--spec-medium);
}
.bytecode-spec-summary.medium .spec-pct,
.bytecode-spec-summary.medium .spec-label {
color: var(--spec-medium-text);
}
.bytecode-spec-summary.low {
background: var(--spec-low-bg);
border-left: 3px solid var(--spec-low);
}
.bytecode-spec-summary.low .spec-pct,
.bytecode-spec-summary.low .spec-label {
color: var(--spec-low-text);
}
.bytecode-header {
display: grid;
grid-template-columns: 1fr 80px 80px;
gap: 12px;
padding: 4px 8px;
font-weight: 600;
color: var(--text-secondary);
border-bottom: 1px solid var(--code-border);
margin-bottom: 4px;
}
.bytecode-expand-all {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--bg-secondary);
border: 1px solid var(--code-border);
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
flex-shrink: 0;
}
.bytecode-expand-all:hover,
.bytecode-expand-all.expanded {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.bytecode-expand-all .expand-icon {
font-size: 10px;
transition: transform var(--transition-fast);
}
.bytecode-expand-all.expanded .expand-icon {
transform: rotate(90deg);
}
/* ========================================
INSTRUCTION SPAN HIGHLIGHTING
(triggered only from bytecode panel hover)
======================================== */
/* Highlight from bytecode panel hover */
.instr-span.highlight-from-bytecode {
outline: 3px solid #ff6b6b !important;
background-color: rgba(255, 107, 107, 0.4) !important;
border-radius: 2px;
}
/* Bytecode instruction row */
.bytecode-instruction {
display: grid;
grid-template-columns: 1fr 80px 80px;
gap: 12px;
align-items: center;
padding: 4px 8px;
margin: 2px 0;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background-color var(--transition-fast);
}
.bytecode-instruction:hover,
.bytecode-instruction.highlight {
background-color: rgba(55, 118, 171, 0.15);
}
.bytecode-instruction[data-locations] {
cursor: pointer;
}
.bytecode-instruction[data-locations]:hover {
background-color: rgba(255, 107, 107, 0.2);
}
.bytecode-opname {
font-weight: 600;
font-family: var(--font-mono);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bytecode-opname.specialized {
color: #2e7d32;
}
[data-theme="dark"] .bytecode-opname.specialized {
color: #81c784;
}
.bytecode-opname .base-op {
color: var(--code-text-muted);
font-weight: normal;
font-size: 0.9em;
margin-left: 4px;
}
.bytecode-samples {
text-align: right;
font-weight: 600;
color: var(--accent);
font-family: var(--font-mono);
}
.bytecode-samples.hot {
color: #ff6b6b;
}
.bytecode-heatbar {
width: 60px;
height: 12px;
background: var(--bg-secondary);
border-radius: 2px;
overflow: hidden;
border: 1px solid var(--code-border);
}
.bytecode-heatbar-fill {
height: 100%;
background: linear-gradient(90deg, #00d4ff 0%, #ff6b00 100%);
}
.specialization-badge {
display: inline-block;
padding: 1px 6px;
font-size: 0.75em;
background: #e8f5e9;
color: #2e7d32;
border-radius: 3px;
margin-left: 6px;
font-weight: 600;
}
[data-theme="dark"] .specialization-badge {
background: rgba(129, 199, 132, 0.2);
color: #81c784;
}
.bytecode-empty {
color: var(--code-text-muted);
font-style: italic;
padding: 8px;
}
.bytecode-error {
color: #d32f2f;
font-style: italic;
padding: 8px;
}
/* ========================================
SPAN TOOLTIPS
======================================== */
.span-tooltip {
position: absolute;
z-index: 10000;
background: var(--bg-primary);
color: var(--text-primary);
padding: 10px 14px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
font-family: var(--font-sans);
font-size: 12px;
box-shadow: var(--shadow-lg);
pointer-events: none;
min-width: 160px;
max-width: 300px;
}
.span-tooltip::after {
content: '';
position: absolute;
bottom: -7px;
left: 50%;
transform: translateX(-50%);
border-width: 7px 7px 0;
border-style: solid;
border-color: var(--bg-primary) transparent transparent;
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
}
.span-tooltip-header {
font-weight: 600;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid var(--border);
color: var(--text-primary);
}
.span-tooltip-header.hot {
color: #e65100;
}
.span-tooltip-header.warm {
color: #f59e0b;
}
.span-tooltip-header.cold {
color: var(--text-muted);
}
.span-tooltip-row {
display: flex;
justify-content: space-between;
margin: 4px 0;
gap: 16px;
}
.span-tooltip-label {
color: var(--text-secondary);
}
.span-tooltip-value {
font-weight: 600;
text-align: right;
color: var(--text-primary);
}
.span-tooltip-value.highlight {
color: var(--accent);
}
.span-tooltip-section {
font-weight: 600;
color: var(--text-secondary);
font-size: 11px;
margin-top: 8px;
margin-bottom: 4px;
padding-top: 6px;
border-top: 1px solid var(--border);
}
.span-tooltip-opcode {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-primary);
background: var(--bg-secondary);
padding: 3px 8px;
margin: 2px 0;
border-radius: var(--radius-sm);
border-left: 2px solid var(--accent);
}

View file

@ -289,7 +289,6 @@ function toggleColorMode() {
// ============================================================================
document.addEventListener('DOMContentLoaded', function() {
// Restore UI state (theme, etc.)
restoreUIState();
applyLineColors();
@ -308,19 +307,38 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialize toggle buttons
const toggleColdBtn = document.getElementById('toggle-cold');
if (toggleColdBtn) {
toggleColdBtn.addEventListener('click', toggleColdCode);
}
if (toggleColdBtn) toggleColdBtn.addEventListener('click', toggleColdCode);
const colorModeBtn = document.getElementById('toggle-color-mode');
if (colorModeBtn) {
colorModeBtn.addEventListener('click', toggleColorMode);
if (colorModeBtn) colorModeBtn.addEventListener('click', toggleColorMode);
// Initialize specialization view toggle (hide if no bytecode data)
const hasBytecode = document.querySelectorAll('.bytecode-toggle').length > 0;
const specViewBtn = document.getElementById('toggle-spec-view');
if (specViewBtn) {
if (hasBytecode) {
specViewBtn.addEventListener('click', toggleSpecView);
} else {
specViewBtn.style.display = 'none';
}
}
// Build scroll marker
setTimeout(buildScrollMarker, 200);
// Initialize expand-all bytecode button
const expandAllBtn = document.getElementById('toggle-all-bytecode');
if (expandAllBtn) {
if (hasBytecode) {
expandAllBtn.addEventListener('click', toggleAllBytecode);
} else {
expandAllBtn.style.display = 'none';
}
}
// Setup scroll-to-line behavior
// Initialize span tooltips
initSpanTooltips();
// Build scroll marker and scroll to target
setTimeout(buildScrollMarker, 200);
setTimeout(scrollToTargetLine, 100);
});
@ -331,6 +349,400 @@ document.addEventListener('click', e => {
}
});
// ========================================
// SPECIALIZATION VIEW TOGGLE
// ========================================
let specViewEnabled = false;
/**
* Calculate heat color for given intensity (0-1)
* Hot spans (>30%) get warm orange, cold spans get dimmed gray
* @param {number} intensity - Value between 0 and 1
* @returns {string} rgba color string
*/
function calculateHeatColor(intensity) {
// Hot threshold: only spans with >30% of max samples get color
if (intensity > 0.3) {
// Normalize intensity above threshold to 0-1
const normalizedIntensity = (intensity - 0.3) / 0.7;
// Warm orange-red with increasing opacity for hotter spans
const alpha = 0.25 + normalizedIntensity * 0.35; // 0.25 to 0.6
const hotColor = getComputedStyle(document.documentElement).getPropertyValue('--span-hot-base').trim();
return `rgba(${hotColor}, ${alpha})`;
} else if (intensity > 0) {
// Cold spans: very subtle gray, almost invisible
const coldColor = getComputedStyle(document.documentElement).getPropertyValue('--span-cold-base').trim();
return `rgba(${coldColor}, 0.1)`;
}
return 'transparent';
}
/**
* Apply intensity-based heat colors to source spans
* Hot spans get orange highlight, cold spans get dimmed
* @param {boolean} enable - Whether to enable or disable span coloring
*/
function applySpanHeatColors(enable) {
document.querySelectorAll('.instr-span').forEach(span => {
const samples = enable ? (parseInt(span.dataset.samples) || 0) : 0;
if (samples > 0) {
const intensity = samples / (parseInt(span.dataset.maxSamples) || 1);
span.style.backgroundColor = calculateHeatColor(intensity);
span.style.borderRadius = '2px';
span.style.padding = '0 1px';
span.style.cursor = 'pointer';
} else {
span.style.cssText = '';
}
});
}
// ========================================
// SPAN TOOLTIPS
// ========================================
let activeTooltip = null;
/**
* Create and show tooltip for a span
*/
function showSpanTooltip(span) {
hideSpanTooltip();
const samples = parseInt(span.dataset.samples) || 0;
const maxSamples = parseInt(span.dataset.maxSamples) || 1;
const pct = span.dataset.pct || '0';
const opcodes = span.dataset.opcodes || '';
if (samples === 0) return;
const intensity = samples / maxSamples;
const isHot = intensity > 0.7;
const isWarm = intensity > 0.3;
const hotnessText = isHot ? 'Hot' : isWarm ? 'Warm' : 'Cold';
const hotnessClass = isHot ? 'hot' : isWarm ? 'warm' : 'cold';
// Build opcodes rows - each opcode on its own row
let opcodesHtml = '';
if (opcodes) {
const opcodeList = opcodes.split(',').map(op => op.trim()).filter(op => op);
if (opcodeList.length > 0) {
opcodesHtml = `
<div class="span-tooltip-section">Opcodes:</div>
${opcodeList.map(op => `<div class="span-tooltip-opcode">${op}</div>`).join('')}
`;
}
}
const tooltip = document.createElement('div');
tooltip.className = 'span-tooltip';
tooltip.innerHTML = `
<div class="span-tooltip-header ${hotnessClass}">${hotnessText}</div>
<div class="span-tooltip-row">
<span class="span-tooltip-label">Samples:</span>
<span class="span-tooltip-value${isHot ? ' highlight' : ''}">${samples.toLocaleString()}</span>
</div>
<div class="span-tooltip-row">
<span class="span-tooltip-label">% of line:</span>
<span class="span-tooltip-value">${pct}%</span>
</div>
${opcodesHtml}
`;
document.body.appendChild(tooltip);
activeTooltip = tooltip;
// Position tooltip above the span
const rect = span.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
let top = rect.top - tooltipRect.height - 8;
// Keep tooltip in viewport
if (left < 5) left = 5;
if (left + tooltipRect.width > window.innerWidth - 5) {
left = window.innerWidth - tooltipRect.width - 5;
}
if (top < 5) {
top = rect.bottom + 8; // Show below if no room above
}
tooltip.style.left = `${left + window.scrollX}px`;
tooltip.style.top = `${top + window.scrollY}px`;
}
/**
* Hide active tooltip
*/
function hideSpanTooltip() {
if (activeTooltip) {
activeTooltip.remove();
activeTooltip = null;
}
}
/**
* Initialize span tooltip handlers
*/
function initSpanTooltips() {
document.addEventListener('mouseover', (e) => {
const span = e.target.closest('.instr-span');
if (span && specViewEnabled) {
showSpanTooltip(span);
}
});
document.addEventListener('mouseout', (e) => {
const span = e.target.closest('.instr-span');
if (span) {
hideSpanTooltip();
}
});
}
function toggleSpecView() {
specViewEnabled = !specViewEnabled;
const lines = document.querySelectorAll('.code-line');
if (specViewEnabled) {
lines.forEach(line => {
const specColor = line.getAttribute('data-spec-color');
line.style.background = specColor || 'transparent';
});
} else {
applyLineColors();
}
applySpanHeatColors(specViewEnabled);
updateToggleUI('toggle-spec-view', specViewEnabled);
// Disable/enable color mode toggle based on spec view state
const colorModeToggle = document.getElementById('toggle-color-mode');
if (colorModeToggle) {
colorModeToggle.classList.toggle('disabled', specViewEnabled);
}
buildScrollMarker();
}
// ========================================
// BYTECODE PANEL TOGGLE
// ========================================
/**
* Toggle bytecode panel visibility for a source line
* @param {HTMLElement} button - The toggle button that was clicked
*/
function toggleBytecode(button) {
const lineDiv = button.closest('.code-line');
const lineId = lineDiv.id;
const lineNum = lineId.replace('line-', '');
const panel = document.getElementById(`bytecode-${lineNum}`);
if (!panel) return;
const isExpanded = panel.style.display !== 'none';
if (isExpanded) {
panel.style.display = 'none';
button.classList.remove('expanded');
button.innerHTML = '&#9654;'; // Right arrow
} else {
if (!panel.dataset.populated) {
populateBytecodePanel(panel, button);
}
panel.style.display = 'block';
button.classList.add('expanded');
button.innerHTML = '&#9660;'; // Down arrow
}
}
/**
* Populate bytecode panel with instruction data
* @param {HTMLElement} panel - The panel element to populate
* @param {HTMLElement} button - The button containing the bytecode data
*/
function populateBytecodePanel(panel, button) {
const bytecodeJson = button.getAttribute('data-bytecode');
if (!bytecodeJson) return;
// Get line number from parent
const lineDiv = button.closest('.code-line');
const lineNum = lineDiv ? lineDiv.id.replace('line-', '') : null;
try {
const instructions = JSON.parse(bytecodeJson);
if (!instructions.length) {
panel.innerHTML = '<div class="bytecode-empty">No bytecode data</div>';
panel.dataset.populated = 'true';
return;
}
const maxSamples = Math.max(...instructions.map(i => i.samples), 1);
// Calculate specialization stats
const totalSamples = instructions.reduce((sum, i) => sum + i.samples, 0);
const specializedSamples = instructions
.filter(i => i.is_specialized)
.reduce((sum, i) => sum + i.samples, 0);
const specPct = totalSamples > 0 ? Math.round(100 * specializedSamples / totalSamples) : 0;
const specializedCount = instructions.filter(i => i.is_specialized).length;
// Determine specialization level class
let specClass = 'low';
if (specPct >= 67) specClass = 'high';
else if (specPct >= 33) specClass = 'medium';
// Build specialization summary
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>
</div>`;
html += '<div class="bytecode-header">' +
'<span class="bytecode-opname">Instruction</span>' +
'<span class="bytecode-samples">Samples</span>' +
'<span>Heat</span></div>';
for (const instr of instructions) {
const heatPct = (instr.samples / maxSamples) * 100;
const isHot = heatPct > 50;
const specializedClass = instr.is_specialized ? ' specialized' : '';
const baseOpHtml = instr.is_specialized
? `<span class="base-op">(${escapeHtml(instr.base_opname)})</span>` : '';
const badge = instr.is_specialized
? '<span class="specialization-badge">SPECIALIZED</span>' : '';
// Build location data attributes for cross-referencing with source spans
const hasLocations = instr.locations && instr.locations.length > 0;
const locationData = hasLocations
? `data-locations='${JSON.stringify(instr.locations)}' data-line="${lineNum}" data-opcode="${instr.opcode}"`
: '';
html += `<div class="bytecode-instruction" ${locationData}>
<span class="bytecode-opname${specializedClass}">${escapeHtml(instr.opname)}${baseOpHtml}${badge}</span>
<span class="bytecode-samples${isHot ? ' hot' : ''}">${instr.samples.toLocaleString()}</span>
<div class="bytecode-heatbar"><div class="bytecode-heatbar-fill" style="width:${heatPct}%"></div></div>
</div>`;
}
panel.innerHTML = html;
panel.dataset.populated = 'true';
// Add hover handlers for bytecode instructions to highlight source spans
panel.querySelectorAll('.bytecode-instruction[data-locations]').forEach(instrEl => {
instrEl.addEventListener('mouseenter', highlightSourceFromBytecode);
instrEl.addEventListener('mouseleave', unhighlightSourceFromBytecode);
});
} catch (e) {
panel.innerHTML = '<div class="bytecode-error">Error loading bytecode</div>';
console.error('Error parsing bytecode data:', e);
}
}
/**
* Highlight source spans when hovering over bytecode instruction
*/
function highlightSourceFromBytecode(e) {
const instrEl = e.currentTarget;
const lineNum = instrEl.dataset.line;
const locationsStr = instrEl.dataset.locations;
if (!lineNum) return;
const lineDiv = document.getElementById(`line-${lineNum}`);
if (!lineDiv) return;
// Parse locations and highlight matching spans by column range
try {
const locations = JSON.parse(locationsStr || '[]');
const spans = lineDiv.querySelectorAll('.instr-span');
spans.forEach(span => {
const spanStart = parseInt(span.dataset.colStart);
const spanEnd = parseInt(span.dataset.colEnd);
for (const loc of locations) {
// Match if span's range matches instruction's location
if (spanStart === loc.col_offset && spanEnd === loc.end_col_offset) {
span.classList.add('highlight-from-bytecode');
break;
}
}
});
} catch (err) {
console.error('Error parsing locations:', err);
}
// Also highlight the instruction row itself
instrEl.classList.add('highlight');
}
/**
* Remove highlighting from source spans
*/
function unhighlightSourceFromBytecode(e) {
const instrEl = e.currentTarget;
const lineNum = instrEl.dataset.line;
if (!lineNum) return;
const lineDiv = document.getElementById(`line-${lineNum}`);
if (!lineDiv) return;
const spans = lineDiv.querySelectorAll('.instr-span.highlight-from-bytecode');
spans.forEach(span => {
span.classList.remove('highlight-from-bytecode');
});
instrEl.classList.remove('highlight');
}
/**
* Escape HTML special characters
* @param {string} text - Text to escape
* @returns {string} Escaped HTML
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Toggle all bytecode panels at once
*/
function toggleAllBytecode() {
const buttons = document.querySelectorAll('.bytecode-toggle');
if (buttons.length === 0) return;
const someExpanded = Array.from(buttons).some(b => b.classList.contains('expanded'));
const expandAllBtn = document.getElementById('toggle-all-bytecode');
buttons.forEach(button => {
const isExpanded = button.classList.contains('expanded');
if (someExpanded ? isExpanded : !isExpanded) {
toggleBytecode(button);
}
});
// Update the expand-all button state
if (expandAllBtn) {
expandAllBtn.classList.toggle('expanded', !someExpanded);
}
}
// Keyboard shortcut: 'b' toggles all bytecode panels
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
if (e.key === 'b' && !e.ctrlKey && !e.altKey && !e.metaKey) {
toggleAllBytecode();
}
});
// Handle hash changes
window.addEventListener('hashchange', () => setTimeout(scrollToTargetLine, 50));

View file

@ -22,6 +22,7 @@
class="toolbar-btn theme-toggle"
onclick="toggleTheme()"
title="Toggle theme"
aria-label="Toggle theme"
id="theme-btn"
>&#9790;</button>
</div>
@ -64,18 +65,30 @@
<div class="legend-gradient"></div>
<div class="legend-labels">
<span>Cold</span>
<span></span>
<span aria-hidden="true"></span>
<span>Hot</span>
</div>
<div class="toggle-switch" id="toggle-color-mode" title="Toggle between self time and total time coloring">
<span class="toggle-label active">Self Time</span>
<div class="toggle-track"></div>
<span class="toggle-label">Total Time</span>
</div>
<div class="toggle-switch" id="toggle-cold" title="Toggle visibility of lines with zero samples">
<span class="toggle-label active">Show All</span>
<div class="toggle-track"></div>
<span class="toggle-label">Hot Only</span>
<div class="legend-separator" aria-hidden="true"></div>
<div class="legend-controls">
<div class="toggle-switch" id="toggle-color-mode" title="Toggle between self time and total time coloring">
<span class="toggle-label active" data-text="Self Time">Self Time</span>
<div class="toggle-track"></div>
<span class="toggle-label" data-text="Total Time">Total Time</span>
</div>
<div class="toggle-switch" id="toggle-cold" title="Toggle visibility of lines with zero samples">
<span class="toggle-label active" data-text="Show All">Show All</span>
<div class="toggle-track"></div>
<span class="toggle-label" data-text="Hot Only">Hot Only</span>
</div>
<div class="toggle-switch" id="toggle-spec-view" title="Color lines by specialization level (requires bytecode data)">
<span class="toggle-label active" data-text="Heat">Heat</span>
<div class="toggle-track"></div>
<span class="toggle-label" data-text="Specialization">Specialization</span>
</div>
<div class="legend-separator" aria-hidden="true"></div>
<button class="bytecode-expand-all" id="toggle-all-bytecode" title="Expand/collapse all bytecode panels (keyboard: b)">
<span class="expand-icon" aria-hidden="true"></span> Bytecode
</button>
</div>
</div>
</div>

View file

@ -29,6 +29,11 @@ :root {
--topbar-height: 56px;
--statusbar-height: 32px;
/* Border radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
/* Transitions */
--transition-fast: 0.15s ease;
--transition-normal: 0.25s ease;
@ -79,6 +84,21 @@ :root, [data-theme="light"] {
--nav-caller-hover: #1d4ed8;
--nav-callee: #dc2626;
--nav-callee-hover: #b91c1c;
/* Specialization status colors */
--spec-high: #4caf50;
--spec-high-text: #2e7d32;
--spec-high-bg: rgba(76, 175, 80, 0.15);
--spec-medium: #ff9800;
--spec-medium-text: #e65100;
--spec-medium-bg: rgba(255, 152, 0, 0.15);
--spec-low: #9e9e9e;
--spec-low-text: #616161;
--spec-low-bg: rgba(158, 158, 158, 0.15);
/* Heatmap span highlighting colors */
--span-hot-base: 255, 100, 50;
--span-cold-base: 150, 150, 150;
}
/* Dark theme */
@ -103,15 +123,15 @@ [data-theme="dark"] {
--header-gradient: linear-gradient(135deg, #21262d 0%, #30363d 100%);
/* Dark mode heat palette - dark blue to teal to yellow to orange (cold to hot) */
--heat-1: #4a7ba7;
--heat-2: #5a9fa8;
--heat-3: #6ab5b5;
--heat-4: #7ec488;
--heat-5: #a0d878;
--heat-6: #c4de6a;
--heat-7: #f4d44d;
--heat-8: #ff6b35;
/* 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);
/* Code view specific - dark mode */
--code-bg: #0d1117;
@ -126,6 +146,21 @@ [data-theme="dark"] {
--nav-caller-hover: #4184e4;
--nav-callee: #f87171;
--nav-callee-hover: #e53e3e;
/* Specialization status colors - dark theme */
--spec-high: #81c784;
--spec-high-text: #81c784;
--spec-high-bg: rgba(129, 199, 132, 0.2);
--spec-medium: #ffb74d;
--spec-medium-text: #ffb74d;
--spec-medium-bg: rgba(255, 183, 77, 0.2);
--spec-low: #bdbdbd;
--spec-low-text: #9e9e9e;
--spec-low-bg: rgba(189, 189, 189, 0.15);
/* Heatmap span highlighting colors - dark theme */
--span-hot-base: 255, 107, 53;
--span-cold-base: 189, 189, 189;
}
/* --------------------------------------------------------------------------

View file

@ -195,6 +195,12 @@ def _add_sampling_options(parser):
dest="gc",
help='Don\'t include artificial "<GC>" frames to denote active garbage collection',
)
sampling_group.add_argument(
"--opcodes",
action="store_true",
help="Gather bytecode opcode information for instruction-level profiling "
"(shows which bytecode instructions are executing, including specializations).",
)
sampling_group.add_argument(
"--async-aware",
action="store_true",
@ -316,13 +322,15 @@ def _sort_to_mode(sort_choice):
return sort_map.get(sort_choice, SORT_MODE_NSAMPLES)
def _create_collector(format_type, interval, skip_idle):
def _create_collector(format_type, interval, skip_idle, opcodes=False):
"""Create the appropriate collector based on format type.
Args:
format_type: The output format ('pstats', 'collapsed', 'flamegraph', 'gecko')
format_type: The output format ('pstats', 'collapsed', 'flamegraph', 'gecko', 'heatmap')
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)
Returns:
A collector instance of the appropriate type
@ -332,8 +340,10 @@ def _create_collector(format_type, interval, skip_idle):
raise ValueError(f"Unknown format: {format_type}")
# 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":
skip_idle = False
return collector_class(interval, skip_idle=skip_idle, opcodes=opcodes)
return collector_class(interval, skip_idle=skip_idle)
@ -446,6 +456,13 @@ def _validate_args(args, parser):
"Gecko format automatically includes both GIL-holding and CPU status analysis."
)
# 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:
parser.error(
f"--opcodes is only compatible with {', '.join('--' + f for f in opcodes_compatible_formats)}."
)
# Validate pstats-specific options are only used with pstats format
if args.format != "pstats":
issues = []
@ -593,7 +610,7 @@ def _handle_attach(args):
)
# Create the appropriate collector
collector = _create_collector(args.format, args.interval, skip_idle)
collector = _create_collector(args.format, args.interval, skip_idle, args.opcodes)
# Sample the process
collector = sample(
@ -606,6 +623,7 @@ def _handle_attach(args):
async_aware=args.async_mode if args.async_aware else None,
native=args.native,
gc=args.gc,
opcodes=args.opcodes,
)
# Handle output
@ -641,7 +659,7 @@ def _handle_run(args):
)
# Create the appropriate collector
collector = _create_collector(args.format, args.interval, skip_idle)
collector = _create_collector(args.format, args.interval, skip_idle, args.opcodes)
# Profile the subprocess
try:
@ -655,6 +673,7 @@ def _handle_run(args):
async_aware=args.async_mode if args.async_aware else None,
native=args.native,
gc=args.gc,
opcodes=args.opcodes,
)
# Handle output
@ -685,6 +704,7 @@ def _handle_live_attach(args, pid):
limit=20, # Default limit
pid=pid,
mode=mode,
opcodes=args.opcodes,
async_aware=args.async_mode if args.async_aware else None,
)
@ -699,6 +719,7 @@ def _handle_live_attach(args, pid):
async_aware=args.async_mode if args.async_aware else None,
native=args.native,
gc=args.gc,
opcodes=args.opcodes,
)
@ -726,6 +747,7 @@ def _handle_live_run(args):
limit=20, # Default limit
pid=process.pid,
mode=mode,
opcodes=args.opcodes,
async_aware=args.async_mode if args.async_aware else None,
)
@ -741,6 +763,7 @@ def _handle_live_run(args):
async_aware=args.async_mode if args.async_aware else None,
native=args.native,
gc=args.gc,
opcodes=args.opcodes,
)
finally:
# Clean up the subprocess

View file

@ -1,5 +1,6 @@
from abc import ABC, abstractmethod
from .constants import (
DEFAULT_LOCATION,
THREAD_STATUS_HAS_GIL,
THREAD_STATUS_ON_CPU,
THREAD_STATUS_GIL_REQUESTED,
@ -12,6 +13,34 @@
# Fallback definition if _remote_debugging is not available
FrameInfo = None
def normalize_location(location):
"""Normalize location to a 4-tuple format.
Args:
location: tuple (lineno, end_lineno, col_offset, end_col_offset) or None
Returns:
tuple: (lineno, end_lineno, col_offset, end_col_offset)
"""
if location is None:
return DEFAULT_LOCATION
return location
def extract_lineno(location):
"""Extract lineno from location.
Args:
location: tuple (lineno, end_lineno, col_offset, end_col_offset) or None
Returns:
int: The line number (0 for synthetic frames)
"""
if location is None:
return 0
return location[0]
class Collector(ABC):
@abstractmethod
def collect(self, stack_frames):
@ -117,11 +146,11 @@ def _build_linear_stacks(self, leaf_task_ids, task_map, child_to_parent):
selected_parent, parent_count = parent_info
if parent_count > 1:
task_name = f"{task_name} ({parent_count} parents)"
frames.append(FrameInfo(("<task>", 0, task_name)))
frames.append(FrameInfo(("<task>", None, task_name, None)))
current_id = selected_parent
else:
# Root task - no parent
frames.append(FrameInfo(("<task>", 0, task_name)))
frames.append(FrameInfo(("<task>", None, task_name, None)))
current_id = None
# Yield the complete stack if we collected any frames

View file

@ -14,6 +14,10 @@
SORT_MODE_CUMUL_PCT = 4
SORT_MODE_NSAMPLES_CUMUL = 5
# Default location for synthetic frames (native, GC) that have no source location
# Format: (lineno, end_lineno, col_offset, end_col_offset)
DEFAULT_LOCATION = (0, 0, -1, -1)
# Thread status flags
try:
from _remote_debugging import (

View file

@ -7,6 +7,7 @@
import time
from .collector import Collector
from .opcode_utils import get_opcode_info, format_opcode
try:
from _remote_debugging import THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED
except ImportError:
@ -26,6 +27,7 @@
{"name": "GIL", "color": "green", "subcategories": ["Other"]},
{"name": "CPU", "color": "purple", "subcategories": ["Other"]},
{"name": "Code Type", "color": "red", "subcategories": ["Other"]},
{"name": "Opcodes", "color": "magenta", "subcategories": ["Other"]},
]
# Category indices
@ -36,6 +38,7 @@
CATEGORY_GIL = 4
CATEGORY_CPU = 5
CATEGORY_CODE_TYPE = 6
CATEGORY_OPCODES = 7
# Subcategory indices
DEFAULT_SUBCATEGORY = 0
@ -56,9 +59,10 @@
class GeckoCollector(Collector):
def __init__(self, sample_interval_usec, *, skip_idle=False):
def __init__(self, sample_interval_usec, *, skip_idle=False, opcodes=False):
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
# Global string table (shared across all threads)
@ -91,6 +95,9 @@ def __init__(self, sample_interval_usec, *, skip_idle=False):
# Track which threads have been initialized for state tracking
self.initialized_threads = set()
# Opcode state tracking per thread: tid -> (opcode, lineno, col_offset, funcname, filename, start_time)
self.opcode_state = {}
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.
@ -232,6 +239,30 @@ def collect(self, stack_frames):
samples["time"].append(current_time)
samples["eventDelay"].append(None)
# Track opcode state changes for interval markers (leaf frame only)
if self.opcodes_enabled:
leaf_frame = frames[0]
filename, location, funcname, opcode = leaf_frame
if isinstance(location, tuple):
lineno, _, col_offset, _ = location
else:
lineno = location
col_offset = -1
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)
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
)
# Start tracking new state
self.opcode_state[tid] = (*current_state, current_time)
self.sample_count += 1
def _create_thread(self, tid):
@ -369,6 +400,36 @@ def _add_marker(self, tid, name, start_time, end_time, category):
"tid": tid
})
def _add_opcode_interval_marker(self, tid, opcode, lineno, col_offset, funcname, start_time, end_time):
"""Add an interval marker for opcode execution span."""
if tid not in self.threads or opcode is None:
return
thread_data = self.threads[tid]
opcode_info = get_opcode_info(opcode)
# Use formatted opcode name (with base opcode for specialized ones)
formatted_opname = format_opcode(opcode)
name_idx = self._intern_string(formatted_opname)
markers = thread_data["markers"]
markers["name"].append(name_idx)
markers["startTime"].append(start_time)
markers["endTime"].append(end_time)
markers["phase"].append(1) # 1 = interval marker
markers["category"].append(CATEGORY_OPCODES)
markers["data"].append({
"type": "Opcode",
"opcode": opcode,
"opname": formatted_opname,
"base_opname": opcode_info["base_opname"],
"is_specialized": opcode_info["is_specialized"],
"line": lineno,
"column": col_offset if col_offset >= 0 else None,
"function": funcname,
"duration": end_time - start_time,
})
def _process_stack(self, thread_data, frames):
"""Process a stack and return the stack index."""
if not frames:
@ -386,17 +447,25 @@ def _process_stack(self, thread_data, frames):
prefix_stack_idx = None
for frame_tuple in reversed(frames):
# frame_tuple is (filename, lineno, funcname)
filename, lineno, funcname = frame_tuple
# frame_tuple is (filename, location, funcname, opcode)
# location is (lineno, end_lineno, col_offset, end_col_offset) or just lineno
filename, location, funcname, opcode = frame_tuple
if isinstance(location, tuple):
lineno, end_lineno, col_offset, end_col_offset = location
else:
# Legacy format: location is just lineno
lineno = location
col_offset = -1
end_col_offset = -1
# Get or create function
func_idx = self._get_or_create_func(
thread_data, filename, funcname, lineno
)
# Get or create frame
# Get or create frame (include column for precise source location)
frame_idx = self._get_or_create_frame(
thread_data, func_idx, lineno
thread_data, func_idx, lineno, col_offset
)
# Check stack cache
@ -494,10 +563,11 @@ def _get_or_create_resource(self, thread_data, filename):
resource_cache[filename] = resource_idx
return resource_idx
def _get_or_create_frame(self, thread_data, func_idx, lineno):
def _get_or_create_frame(self, thread_data, func_idx, lineno, col_offset=-1):
"""Get or create a frame entry."""
frame_cache = thread_data["_frameCache"]
frame_key = (func_idx, lineno)
# Include column in cache key for precise frame identification
frame_key = (func_idx, lineno, col_offset if col_offset >= 0 else None)
if frame_key in frame_cache:
return frame_cache[frame_key]
@ -531,7 +601,8 @@ def _get_or_create_frame(self, thread_data, func_idx, lineno):
frame_inner_window_ids.append(None)
frame_implementations.append(None)
frame_lines.append(lineno if lineno else None)
frame_columns.append(None)
# Store column offset if available (>= 0), otherwise None
frame_columns.append(col_offset if col_offset >= 0 else None)
frame_optimizations.append(None)
frame_cache[frame_key] = frame_idx
@ -558,6 +629,12 @@ def _finalize_markers(self):
self._add_marker(tid, marker_name, state_dict[tid], end_time, category)
del state_dict[tid]
# Close any open opcode markers
for tid, state in list(self.opcode_state.items()):
opcode, lineno, col_offset, funcname, filename, start_time = state
self._add_opcode_interval_marker(tid, opcode, lineno, col_offset, funcname, start_time, end_time)
self.opcode_state.clear()
def export(self, filename):
"""Export the profile to a Gecko JSON file."""
@ -600,6 +677,31 @@ def spin():
f"Open in Firefox Profiler: https://profiler.firefox.com/"
)
def _build_marker_schema(self):
"""Build marker schema definitions for Firefox Profiler."""
schema = []
# Opcode marker schema (only if opcodes enabled)
if self.opcodes_enabled:
schema.append({
"name": "Opcode",
"display": ["marker-table", "marker-chart"],
"tooltipLabel": "{marker.data.opname}",
"tableLabel": "{marker.data.opname} at line {marker.data.line}",
"chartLabel": "{marker.data.opname}",
"fields": [
{"key": "opname", "label": "Opcode", "format": "string", "searchable": True},
{"key": "base_opname", "label": "Base Opcode", "format": "string"},
{"key": "is_specialized", "label": "Specialized", "format": "string"},
{"key": "line", "label": "Line", "format": "integer"},
{"key": "column", "label": "Column", "format": "integer"},
{"key": "function", "label": "Function", "format": "string"},
{"key": "duration", "label": "Duration", "format": "duration"},
],
})
return schema
def _build_profile(self):
"""Build the complete profile structure in processed format."""
# Convert thread data to final format
@ -649,7 +751,7 @@ def _build_profile(self):
"CPUName": "",
"product": "Python",
"symbolicated": True,
"markerSchema": [],
"markerSchema": self._build_marker_schema(),
"importedFrom": "Tachyon Sampling Profiler",
"extensions": {
"id": [],

View file

@ -15,6 +15,7 @@
from typing import Dict, List, Tuple
from ._css_utils import get_combined_css
from .collector import normalize_location, extract_lineno
from .stack_collector import StackTraceCollector
@ -463,19 +464,27 @@ def __init__(self, *args, **kwargs):
self.line_self_samples = collections.Counter()
self.file_self_samples = collections.defaultdict(collections.Counter)
# Call graph data structures for navigation
self.call_graph = collections.defaultdict(list)
self.callers_graph = collections.defaultdict(list)
# Call graph data structures for navigation (sets for O(1) deduplication)
self.call_graph = collections.defaultdict(set)
self.callers_graph = collections.defaultdict(set)
self.function_definitions = {}
# Edge counting for call path analysis
self.edge_samples = collections.Counter()
# Bytecode-level tracking data structures
# Track samples per (file, lineno) -> {opcode: {'count': N, 'locations': set()}}
# Locations are deduplicated via set to minimize memory usage
self.line_opcodes = collections.defaultdict(dict)
# Statistics and metadata
self._total_samples = 0
self._path_info = get_python_path_info()
self.stats = {}
# Opcode collection flag
self.opcodes_enabled = False
# Template loader (loads all templates once)
self._template_loader = _TemplateLoader()
@ -509,26 +518,37 @@ def process_frames(self, frames, thread_id):
"""Process stack frames and count samples per line.
Args:
frames: List of frame tuples (filename, lineno, funcname)
frames[0] is the leaf (top of stack, where execution is)
frames: List of (filename, location, funcname, opcode) tuples in
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
"""
self._total_samples += 1
# Count each line in the stack and build call graph
for i, frame_info in enumerate(frames):
filename, lineno, funcname = frame_info
for i, (filename, location, funcname, opcode) in enumerate(frames):
# Normalize location to 4-tuple format
lineno, end_lineno, col_offset, end_col_offset = normalize_location(location)
if not self._is_valid_frame(filename, lineno):
continue
# frames[0] is the leaf - where execution is actually happening
is_leaf = (i == 0)
self._record_line_sample(filename, lineno, funcname, is_leaf=is_leaf)
self._record_line_sample(filename, lineno, funcname, is_leaf=(i == 0))
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)
# Build call graph for adjacent frames
if i + 1 < len(frames):
self._record_call_relationship(frames[i], frames[i + 1])
next_frame = frames[i + 1]
next_lineno = extract_lineno(next_frame[1])
self._record_call_relationship(
(filename, lineno, funcname),
(next_frame[0], next_lineno, next_frame[2])
)
def _is_valid_frame(self, filename, lineno):
"""Check if a frame should be included in the heatmap."""
@ -557,6 +577,79 @@ def _record_line_sample(self, filename, lineno, funcname, is_leaf=False):
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):
"""Record a sample for a specific bytecode instruction.
Args:
filename: Source filename
lineno: Line number
opcode: Opcode number being executed
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)
"""
key = (filename, lineno)
# Initialize opcode entry if needed - use set for location deduplication
if opcode not in self.line_opcodes[key]:
self.line_opcodes[key][opcode] = {'count': 0, 'locations': set()}
self.line_opcodes[key][opcode]['count'] += 1
# Store unique location info if column offset is available (not -1)
if col_offset is not None and col_offset >= 0:
# Use tuple as set key for deduplication
loc_key = (end_lineno, col_offset, end_col_offset)
self.line_opcodes[key][opcode]['locations'].add(loc_key)
def _get_bytecode_data_for_line(self, filename, lineno):
"""Get bytecode disassembly data for instructions on a specific line.
Args:
filename: Source filename
lineno: Line number
Returns:
List of dicts with instruction info, sorted by samples descending
"""
from .opcode_utils import get_opcode_info, format_opcode
key = (filename, lineno)
opcode_data = self.line_opcodes.get(key, {})
result = []
for opcode, data in opcode_data.items():
info = get_opcode_info(opcode)
# Handle both old format (int count) and new format (dict with count/locations)
if isinstance(data, dict):
count = data.get('count', 0)
raw_locations = data.get('locations', set())
# Convert set of tuples to list of dicts for JSON serialization
if isinstance(raw_locations, set):
locations = [
{'end_lineno': loc[0], 'col_offset': loc[1], 'end_col_offset': loc[2]}
for loc in raw_locations
]
else:
locations = raw_locations
else:
count = data
locations = []
result.append({
'opcode': opcode,
'opname': format_opcode(opcode),
'base_opname': info['base_opname'],
'is_specialized': info['is_specialized'],
'samples': count,
'locations': locations,
})
# Sort by samples descending, then by opcode number
result.sort(key=lambda x: (-x['samples'], x['opcode']))
return result
def _record_call_relationship(self, callee_frame, caller_frame):
"""Record caller/callee relationship between adjacent frames."""
callee_filename, callee_lineno, callee_funcname = callee_frame
@ -571,17 +664,15 @@ def _record_call_relationship(self, callee_frame, caller_frame):
(callee_filename, callee_funcname), callee_lineno
)
# Record caller -> callee relationship
# Record caller -> callee relationship (set handles deduplication)
caller_key = (caller_filename, caller_lineno)
callee_info = (callee_filename, callee_def_line, callee_funcname)
if callee_info not in self.call_graph[caller_key]:
self.call_graph[caller_key].append(callee_info)
self.call_graph[caller_key].add(callee_info)
# Record callee <- caller relationship
# Record callee <- caller relationship (set handles deduplication)
callee_key = (callee_filename, callee_def_line)
caller_info = (caller_filename, caller_lineno, caller_funcname)
if caller_info not in self.callers_graph[callee_key]:
self.callers_graph[callee_key].append(caller_info)
self.callers_graph[callee_key].add(caller_info)
# Count this call edge for path analysis
edge_key = (caller_key, callee_key)
@ -851,31 +942,184 @@ def _build_line_html(self, line_num: int, line_content: str,
cumulative_display = ""
tooltip = ""
# Get bytecode data for this line (if any)
bytecode_data = self._get_bytecode_data_for_line(filename, line_num)
has_bytecode = len(bytecode_data) > 0 and cumulative_samples > 0
# Build bytecode toggle button if data is available
bytecode_btn_html = ''
bytecode_panel_html = ''
if has_bytecode:
bytecode_json = html.escape(json.dumps(bytecode_data))
# Calculate specialization percentage
total_samples = sum(d['samples'] for d in bytecode_data)
specialized_samples = sum(d['samples'] for d in bytecode_data if d['is_specialized'])
spec_pct = int(100 * specialized_samples / total_samples) if total_samples > 0 else 0
bytecode_btn_html = (
f'<button class="bytecode-toggle" data-bytecode=\'{bytecode_json}\' '
f'data-spec-pct="{spec_pct}" '
f'onclick="toggleBytecode(this)" title="Show bytecode">&#9654;</button>'
)
bytecode_panel_html = f' <div class="bytecode-panel" id="bytecode-{line_num}" style="display:none;"></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>'
# Get navigation buttons
nav_buttons_html = self._build_navigation_buttons(filename, line_num)
# Build line HTML with intensity data attributes
line_html = html.escape(line_content.rstrip('\n'))
# Build line HTML with instruction highlights if available
line_html = self._render_source_with_highlights(line_content, line_num,
filename, bytecode_data)
title_attr = f' title="{html.escape(tooltip)}"' if tooltip else ""
# Specialization color for toggle mode (green gradient based on spec %)
spec_color_attr = ''
if has_bytecode:
spec_color = self._format_specialization_color(spec_pct)
spec_color_attr = f'data-spec-color="{spec_color}" '
return (
f' <div class="code-line" '
f'data-self-intensity="{self_intensity:.3f}" '
f'data-cumulative-intensity="{cumulative_intensity:.3f}" '
f'{spec_color_attr}'
f'id="line-{line_num}"{title_attr}>\n'
f' <div class="line-number">{line_num}</div>\n'
f' <div class="line-samples-self">{self_display}</div>\n'
f' <div class="line-samples-cumulative">{cumulative_display}</div>\n'
f' {bytecode_btn_html}\n'
f' <div class="line-content">{line_html}</div>\n'
f' {nav_buttons_html}\n'
f' </div>\n'
f'{bytecode_panel_html}'
)
def _render_source_with_highlights(self, line_content: str, line_num: int,
filename: str, bytecode_data: list) -> str:
"""Render source line with instruction highlight spans.
Simple: collect ranges with sample counts, assign each byte position to
smallest covering range, then emit spans for contiguous runs with sample data.
"""
import html as html_module
content = line_content.rstrip('\n')
if not content:
return ''
# Collect all (start, end) -> {samples, opcodes} mapping from instructions
# Multiple instructions may share the same range, so we sum samples and collect opcodes
range_data = {}
for instr in bytecode_data:
samples = instr.get('samples', 0)
opname = instr.get('opname', '')
for loc in instr.get('locations', []):
if loc.get('end_lineno', line_num) == line_num:
start, end = loc.get('col_offset', -1), loc.get('end_col_offset', -1)
if start >= 0 and end >= 0:
key = (start, end)
if key not in range_data:
range_data[key] = {'samples': 0, 'opcodes': []}
range_data[key]['samples'] += samples
if opname and opname not in range_data[key]['opcodes']:
range_data[key]['opcodes'].append(opname)
if not range_data:
return html_module.escape(content)
# For each byte position, find the smallest covering range
byte_to_range = {}
for (start, end) in range_data.keys():
for pos in range(start, end):
if pos not in byte_to_range:
byte_to_range[pos] = (start, end)
else:
# Keep smaller range
old_start, old_end = byte_to_range[pos]
if (end - start) < (old_end - old_start):
byte_to_range[pos] = (start, end)
# Calculate totals for percentage and intensity
total_line_samples = sum(d['samples'] for d in range_data.values())
max_range_samples = max(d['samples'] for d in range_data.values()) if range_data else 1
# Render character by character
result = []
byte_offset = 0
char_idx = 0
current_range = None
span_chars = []
def flush_span():
nonlocal span_chars, current_range
if span_chars:
text = html_module.escape(''.join(span_chars))
if current_range:
data = range_data.get(current_range, {'samples': 0, 'opcodes': []})
samples = data['samples']
opcodes = ', '.join(data['opcodes'][:3]) # Top 3 opcodes
if len(data['opcodes']) > 3:
opcodes += f" +{len(data['opcodes']) - 3} more"
pct = int(100 * samples / total_line_samples) if total_line_samples > 0 else 0
result.append(f'<span class="instr-span" '
f'data-col-start="{current_range[0]}" '
f'data-col-end="{current_range[1]}" '
f'data-samples="{samples}" '
f'data-max-samples="{max_range_samples}" '
f'data-pct="{pct}" '
f'data-opcodes="{html_module.escape(opcodes)}">{text}</span>')
else:
result.append(text)
span_chars = []
while char_idx < len(content):
char = content[char_idx]
char_bytes = len(char.encode('utf-8'))
char_range = byte_to_range.get(byte_offset)
if char_range != current_range:
flush_span()
current_range = char_range
span_chars.append(char)
byte_offset += char_bytes
char_idx += 1
flush_span()
return ''.join(result)
def _format_specialization_color(self, spec_pct: int) -> str:
"""Format specialization color based on percentage.
Uses a gradient from gray (0%) through orange (50%) to green (100%).
"""
# Normalize to 0-1
ratio = spec_pct / 100.0
if ratio >= 0.5:
# Orange to green (50-100%)
t = (ratio - 0.5) * 2 # 0 to 1
r = int(255 * (1 - t)) # 255 -> 0
g = int(180 + 75 * t) # 180 -> 255
b = int(50 * (1 - t)) # 50 -> 0
else:
# Gray to orange (0-50%)
t = ratio * 2 # 0 to 1
r = int(158 + 97 * t) # 158 -> 255
g = int(158 + 22 * t) # 158 -> 180
b = int(158 - 108 * t) # 158 -> 50
alpha = 0.15 + 0.25 * ratio # 0.15 to 0.4
return f"rgba({r}, {g}, {b}, {alpha})"
def _build_navigation_buttons(self, filename: str, line_num: int) -> str:
"""Build navigation buttons for callers/callees."""
line_key = (filename, line_num)
caller_list = self._deduplicate_by_function(self.callers_graph.get(line_key, []))
callee_list = self._deduplicate_by_function(self.call_graph.get(line_key, []))
caller_list = self._deduplicate_by_function(self.callers_graph.get(line_key, set()))
callee_list = self._deduplicate_by_function(self.call_graph.get(line_key, set()))
# Get edge counts for each caller/callee
callers_with_counts = self._get_edge_counts(line_key, caller_list, is_caller=True)
@ -907,8 +1151,12 @@ def _get_edge_counts(self, line_key: Tuple[str, int],
result.sort(key=lambda x: x[3], reverse=True)
return result
def _deduplicate_by_function(self, items: List[Tuple[str, int, str]]) -> List[Tuple[str, int, str]]:
"""Remove duplicate entries based on (file, function) key."""
def _deduplicate_by_function(self, items) -> List[Tuple[str, int, str]]:
"""Remove duplicate entries based on (file, function) key.
Args:
items: Iterable of (file, line, func) tuples (set or list)
"""
seen = {}
result = []
for file, line, func in items:

View file

@ -11,7 +11,7 @@
import time
import _colorize
from ..collector import Collector
from ..collector import Collector, extract_lineno
from ..constants import (
THREAD_STATUS_HAS_GIL,
THREAD_STATUS_ON_CPU,
@ -41,7 +41,7 @@
COLOR_PAIR_SORTED_HEADER,
)
from .display import CursesDisplay
from .widgets import HeaderWidget, TableWidget, FooterWidget, HelpWidget
from .widgets import HeaderWidget, TableWidget, FooterWidget, HelpWidget, OpcodePanel
from .trend_tracker import TrendTracker
@ -67,6 +67,11 @@ class ThreadData:
sample_count: int = 0
gc_frame_samples: int = 0
# Opcode statistics: {location: {opcode: count}}
opcode_stats: dict = field(default_factory=lambda: collections.defaultdict(
lambda: collections.defaultdict(int)
))
def increment_status_flag(self, status_flags):
"""Update status counts based on status bit flags."""
if status_flags & THREAD_STATUS_HAS_GIL:
@ -103,6 +108,7 @@ def __init__(
pid=None,
display=None,
mode=None,
opcodes=False,
async_aware=None,
):
"""
@ -116,6 +122,7 @@ def __init__(
pid: Process ID being profiled
display: DisplayInterface implementation (None means curses will be used)
mode: Profiling mode ('cpu', 'gil', etc.) - affects what stats are shown
opcodes: Whether to show opcode panel (requires --opcodes flag)
async_aware: Async tracing mode - None (sync only), "all" or "running"
"""
self.result = collections.defaultdict(
@ -157,6 +164,12 @@ def __init__(
}
self.gc_frame_samples = 0 # Track samples with GC frames
# Opcode statistics: {location: {opcode: count}}
self.opcode_stats = collections.defaultdict(lambda: collections.defaultdict(int))
self.show_opcodes = opcodes # Show opcode panel when --opcodes flag is passed
self.selected_row = 0 # Currently selected row in table for opcode view
self.scroll_offset = 0 # Scroll offset for table when in opcode mode
# Interactive controls state
self.paused = False # Pause UI updates (profiling continues)
self.show_help = False # Show help screen
@ -183,6 +196,7 @@ def __init__(
self.table_widget = None
self.footer_widget = None
self.help_widget = None
self.opcode_panel = None
# Color mode
self._can_colorize = _colorize.can_colorize()
@ -287,18 +301,29 @@ def process_frames(self, frames, thread_id=None):
thread_data = self._get_or_create_thread_data(thread_id) if thread_id is not None else None
# Process each frame in the stack to track cumulative calls
# frame.location is (lineno, end_lineno, col_offset, end_col_offset), int, or None
for frame in frames:
location = (frame.filename, frame.lineno, frame.funcname)
lineno = extract_lineno(frame.location)
location = (frame.filename, lineno, frame.funcname)
self.result[location]["cumulative_calls"] += 1
if thread_data:
thread_data.result[location]["cumulative_calls"] += 1
# The top frame gets counted as an inline call (directly executing)
top_location = (frames[0].filename, frames[0].lineno, frames[0].funcname)
top_frame = frames[0]
top_lineno = extract_lineno(top_frame.location)
top_location = (top_frame.filename, top_lineno, top_frame.funcname)
self.result[top_location]["direct_calls"] += 1
if thread_data:
thread_data.result[top_location]["direct_calls"] += 1
# Track opcode for top frame (the actively executing instruction)
opcode = getattr(top_frame, 'opcode', None)
if opcode is not None:
self.opcode_stats[top_location][opcode] += 1
if thread_data:
thread_data.opcode_stats[top_location][opcode] += 1
def _get_sync_frame_iterator(self, stack_frames):
"""Iterator for sync frames."""
return self._iter_all_frames(stack_frames, skip_idle=self.skip_idle)
@ -407,6 +432,7 @@ def _initialize_widgets(self, colors):
self.table_widget = TableWidget(self.display, colors, self)
self.footer_widget = FooterWidget(self.display, colors, self)
self.help_widget = HelpWidget(self.display, colors)
self.opcode_panel = OpcodePanel(self.display, colors, self)
def _render_display_sections(
self, height, width, elapsed, stats_list, colors
@ -427,6 +453,12 @@ def _render_display_sections(
line, width, height=height, stats_list=stats_list
)
# Render opcode panel if enabled
if self.show_opcodes:
line = self.opcode_panel.render(
line, width, height=height, stats_list=stats_list
)
except curses.error:
pass
@ -719,6 +751,88 @@ def _handle_finished_input_update(self, had_input):
if self.finished and had_input and self.display is not None:
self._update_display()
def _get_visible_rows_info(self):
"""Calculate visible rows and stats list for opcode navigation."""
stats_list = self.build_stats_list()
if self.display:
height, _ = self.display.get_dimensions()
extra_header = FINISHED_BANNER_EXTRA_LINES if self.finished else 0
max_stats = max(0, height - HEADER_LINES - extra_header - FOOTER_LINES - SAFETY_MARGIN)
stats_list = stats_list[:max_stats]
visible_rows = max(1, height - 8 - 2 - 12)
else:
visible_rows = self.limit
total_rows = len(stats_list)
return stats_list, visible_rows, total_rows
def _move_selection_down(self):
"""Move selection down in opcode mode with scrolling."""
if not self.show_opcodes:
return
stats_list, visible_rows, total_rows = self._get_visible_rows_info()
if total_rows == 0:
return
# Max scroll is when last item is at bottom
max_scroll = max(0, total_rows - visible_rows)
# Current absolute position
abs_pos = self.scroll_offset + self.selected_row
# Only move if not at the last item
if abs_pos < total_rows - 1:
# Try to move selection within visible area first
if self.selected_row < visible_rows - 1:
self.selected_row += 1
elif self.scroll_offset < max_scroll:
# Scroll down
self.scroll_offset += 1
# Clamp to valid range
self.scroll_offset = min(self.scroll_offset, max_scroll)
max_selected = min(visible_rows - 1, total_rows - self.scroll_offset - 1)
self.selected_row = min(self.selected_row, max(0, max_selected))
def _move_selection_up(self):
"""Move selection up in opcode mode with scrolling."""
if not self.show_opcodes:
return
if self.selected_row > 0:
self.selected_row -= 1
elif self.scroll_offset > 0:
self.scroll_offset -= 1
# Clamp to valid range based on actual stats_list
stats_list, visible_rows, total_rows = self._get_visible_rows_info()
if total_rows > 0:
max_scroll = max(0, total_rows - visible_rows)
self.scroll_offset = min(self.scroll_offset, max_scroll)
max_selected = min(visible_rows - 1, total_rows - self.scroll_offset - 1)
self.selected_row = min(self.selected_row, max(0, max_selected))
def _navigate_to_previous_thread(self):
"""Navigate to previous thread in PER_THREAD mode, or switch from ALL to PER_THREAD."""
if len(self.thread_ids) > 0:
if self.view_mode == "ALL":
self.view_mode = "PER_THREAD"
self.current_thread_index = len(self.thread_ids) - 1
else:
self.current_thread_index = (
self.current_thread_index - 1
) % len(self.thread_ids)
def _navigate_to_next_thread(self):
"""Navigate to next thread in PER_THREAD mode, or switch from ALL to PER_THREAD."""
if len(self.thread_ids) > 0:
if self.view_mode == "ALL":
self.view_mode = "PER_THREAD"
self.current_thread_index = 0
else:
self.current_thread_index = (
self.current_thread_index + 1
) % len(self.thread_ids)
def _show_terminal_too_small(self, height, width):
"""Display a message when terminal is too small."""
A_BOLD = self.display.get_attr("A_BOLD")
@ -896,27 +1010,37 @@ def _handle_input(self):
if self._trend_tracker is not None:
self._trend_tracker.toggle()
elif ch == curses.KEY_LEFT or ch == curses.KEY_UP:
# Navigate to previous thread in PER_THREAD mode, or switch from ALL to PER_THREAD
if len(self.thread_ids) > 0:
if self.view_mode == "ALL":
self.view_mode = "PER_THREAD"
self.current_thread_index = 0
else:
self.current_thread_index = (
self.current_thread_index - 1
) % len(self.thread_ids)
elif ch == ord("j") or ch == ord("J"):
# Move selection down in opcode mode (with scrolling)
self._move_selection_down()
elif ch == curses.KEY_RIGHT or ch == curses.KEY_DOWN:
# Navigate to next thread in PER_THREAD mode, or switch from ALL to PER_THREAD
if len(self.thread_ids) > 0:
if self.view_mode == "ALL":
self.view_mode = "PER_THREAD"
self.current_thread_index = 0
else:
self.current_thread_index = (
self.current_thread_index + 1
) % len(self.thread_ids)
elif ch == ord("k") or ch == ord("K"):
# Move selection up in opcode mode (with scrolling)
self._move_selection_up()
elif ch == curses.KEY_UP:
# Move selection up (same as 'k') when in opcode mode
if self.show_opcodes:
self._move_selection_up()
else:
# Navigate to previous thread (same as KEY_LEFT)
self._navigate_to_previous_thread()
elif ch == curses.KEY_DOWN:
# Move selection down (same as 'j') when in opcode mode
if self.show_opcodes:
self._move_selection_down()
else:
# Navigate to next thread (same as KEY_RIGHT)
self._navigate_to_next_thread()
elif ch == curses.KEY_LEFT:
# Navigate to previous thread
self._navigate_to_previous_thread()
elif ch == curses.KEY_RIGHT:
# Navigate to next thread
self._navigate_to_next_thread()
# Update display if input was processed while finished
self._handle_finished_input_update(ch != -1)

View file

@ -45,6 +45,9 @@
# Finished banner display
FINISHED_BANNER_EXTRA_LINES = 3 # Blank line + banner + blank line
# Opcode panel display
OPCODE_PANEL_HEIGHT = 12 # Height reserved for opcode statistics panel
# Color pair IDs
COLOR_PAIR_HEADER_BG = 4
COLOR_PAIR_CYAN = 5

View file

@ -20,6 +20,7 @@
MIN_SAMPLE_RATE_FOR_SCALING,
FOOTER_LINES,
FINISHED_BANNER_EXTRA_LINES,
OPCODE_PANEL_HEIGHT,
)
from ..constants import (
THREAD_STATUS_HAS_GIL,
@ -730,8 +731,21 @@ def draw_stats_rows(self, line, height, width, stats_list, column_flags):
# Get trend tracker for color decisions
trend_tracker = self.collector._trend_tracker
for stat in stats_list:
if line >= height - FOOTER_LINES:
# Check if opcode mode is enabled for row selection highlighting
show_opcodes = getattr(self.collector, 'show_opcodes', False)
selected_row = getattr(self.collector, 'selected_row', 0)
scroll_offset = getattr(self.collector, 'scroll_offset', 0) if show_opcodes else 0
A_REVERSE = self.display.get_attr("A_REVERSE")
A_BOLD = self.display.get_attr("A_BOLD")
# Reserve space for opcode panel when enabled
opcode_panel_height = OPCODE_PANEL_HEIGHT if show_opcodes else 0
# Apply scroll offset when in opcode mode
display_stats = stats_list[scroll_offset:] if show_opcodes else stats_list
for row_idx, stat in enumerate(display_stats):
if line >= height - FOOTER_LINES - opcode_panel_height:
break
func = stat["func"]
@ -752,8 +766,13 @@ def draw_stats_rows(self, line, height, width, stats_list, column_flags):
else 0
)
# Check if this row is selected
is_selected = show_opcodes and row_idx == selected_row
# Helper function to get trend color for a specific column
def get_trend_color(column_name):
if is_selected:
return A_REVERSE | A_BOLD
trend = trends.get(column_name, "stable")
if trend_tracker is not None:
return trend_tracker.get_color(trend)
@ -763,33 +782,45 @@ def get_trend_color(column_name):
samples_str = f"{direct_calls}/{cumulative_calls}"
col = 0
# Fill entire row with reverse video background for selected row
if is_selected:
self.add_str(line, 0, " " * (width - 1), A_REVERSE | A_BOLD)
# Show selection indicator when opcode panel is enabled
if show_opcodes:
if is_selected:
self.add_str(line, col, "", A_REVERSE | A_BOLD)
else:
self.add_str(line, col, " ", curses.A_NORMAL)
col += 2
# Samples column - apply trend color based on nsamples trend
nsamples_color = get_trend_color("nsamples")
self.add_str(line, col, f"{samples_str:>13}", nsamples_color)
self.add_str(line, col, f"{samples_str:>13} ", nsamples_color)
col += 15
# Sample % column
if show_sample_pct:
sample_pct_color = get_trend_color("sample_pct")
self.add_str(line, col, f"{sample_pct:>5.1f}", sample_pct_color)
self.add_str(line, col, f"{sample_pct:>5.1f} ", sample_pct_color)
col += 7
# Total time column
if show_tottime:
tottime_color = get_trend_color("tottime")
self.add_str(line, col, f"{total_time:>10.3f}", tottime_color)
self.add_str(line, col, f"{total_time:>10.3f} ", tottime_color)
col += 12
# Cumul % column
if show_cumul_pct:
cumul_pct_color = get_trend_color("cumul_pct")
self.add_str(line, col, f"{cum_pct:>5.1f}", cumul_pct_color)
self.add_str(line, col, f"{cum_pct:>5.1f} ", cumul_pct_color)
col += 7
# Cumul time column
if show_cumtime:
cumtime_color = get_trend_color("cumtime")
self.add_str(line, col, f"{cumulative_time:>10.3f}", cumtime_color)
self.add_str(line, col, f"{cumulative_time:>10.3f} ", cumtime_color)
col += 12
# Function name column
@ -804,7 +835,8 @@ def get_trend_color(column_name):
if len(funcname) > func_width:
func_display = funcname[: func_width - 3] + "..."
func_display = f"{func_display:<{func_width}}"
self.add_str(line, col, func_display, color_func)
func_color = A_REVERSE | A_BOLD if is_selected else color_func
self.add_str(line, col, func_display, func_color)
col += func_width + 2
# File:line column
@ -812,8 +844,9 @@ def get_trend_color(column_name):
simplified_path = self.collector.simplify_path(filename)
file_line = f"{simplified_path}:{lineno}"
remaining_width = width - col - 1
file_color = A_REVERSE | A_BOLD if is_selected else color_file
self.add_str(
line, col, file_line[:remaining_width], color_file
line, col, file_line[:remaining_width], file_color
)
line += 1
@ -934,7 +967,8 @@ def render(self, line, width, **kwargs):
(" S - Cycle through sort modes (backward)", A_NORMAL),
(" t - Toggle view mode (ALL / per-thread)", A_NORMAL),
(" x - Toggle trend colors (on/off)", A_NORMAL),
(" ← → ↑ ↓ - Navigate threads (in per-thread mode)", A_NORMAL),
(" j/k or ↑/↓ - Select next/previous function (--opcodes)", A_NORMAL),
(" ← / → - Cycle through threads", A_NORMAL),
(" + - Faster display refresh rate", A_NORMAL),
(" - - Slower display refresh rate", A_NORMAL),
("", A_NORMAL),
@ -961,3 +995,99 @@ def render(self, line, width, **kwargs):
self.add_str(start_line + i, col, text[: width - 3], attr)
return line # Not used for overlays
class OpcodePanel(Widget):
"""Widget for displaying opcode statistics for a selected function."""
def __init__(self, display, colors, collector):
super().__init__(display, colors)
self.collector = collector
def render(self, line, width, **kwargs):
"""Render opcode statistics panel.
Args:
line: Starting line number
width: Available width
kwargs: Must contain 'stats_list', 'height'
Returns:
Next available line number
"""
from ..opcode_utils import get_opcode_info, format_opcode
stats_list = kwargs.get("stats_list", [])
height = kwargs.get("height", 24)
selected_row = self.collector.selected_row
scroll_offset = getattr(self.collector, 'scroll_offset', 0)
A_BOLD = self.display.get_attr("A_BOLD")
A_NORMAL = self.display.get_attr("A_NORMAL")
color_cyan = self.colors.get("color_cyan", A_NORMAL)
color_yellow = self.colors.get("color_yellow", A_NORMAL)
color_magenta = self.colors.get("color_magenta", A_NORMAL)
# Get the selected function from stats_list (accounting for scroll)
actual_index = scroll_offset + selected_row
if not stats_list or actual_index >= len(stats_list):
self.add_str(line, 0, "No function selected (use j/k to select)", A_NORMAL)
return line + 1
selected_stat = stats_list[actual_index]
func = selected_stat["func"]
filename, lineno, funcname = func
# Get opcode stats for this function
opcode_stats = self.collector.opcode_stats.get(func, {})
if not opcode_stats:
self.add_str(line, 0, f"No opcode data for {funcname}() (requires --opcodes)", A_NORMAL)
return line + 1
# Sort opcodes by count
sorted_opcodes = sorted(opcode_stats.items(), key=lambda x: -x[1])
total_opcode_samples = sum(opcode_stats.values())
# Draw header
header = f"─── Opcodes for {funcname}() "
header += "" * max(0, width - len(header) - 1)
self.add_str(line, 0, header[:width-1], color_cyan | A_BOLD)
line += 1
# Calculate max samples for bar scaling
max_count = sorted_opcodes[0][1] if sorted_opcodes else 1
# Draw opcode rows (limit to available space)
max_rows = min(8, height - line - 3) # Leave room for footer
bar_width = 20
for i, (opcode_num, count) in enumerate(sorted_opcodes[:max_rows]):
if line >= height - 3:
break
opcode_info = get_opcode_info(opcode_num)
is_specialized = opcode_info["is_specialized"]
name_display = format_opcode(opcode_num)
pct = (count / total_opcode_samples * 100) if total_opcode_samples > 0 else 0
# Draw bar
bar_fill = int((count / max_count) * bar_width) if max_count > 0 else 0
bar = "" * bar_fill + "" * (bar_width - bar_fill)
# Format: [████████░░░░] LOAD_ATTR 45.2% (1234)
# Specialized opcodes shown in magenta, base opcodes in yellow
name_color = color_magenta if is_specialized else color_yellow
row_text = f"[{bar}] {name_display:<35} {pct:>5.1f}% ({count:>6})"
self.add_str(line, 2, row_text[:width-3], name_color)
line += 1
# Show "..." if more opcodes exist
if len(sorted_opcodes) > max_rows:
remaining = len(sorted_opcodes) - max_rows
self.add_str(line, 2, f"... and {remaining} more opcodes", A_NORMAL)
line += 1
return line

View file

@ -0,0 +1,94 @@
"""Opcode utilities for bytecode-level profiler visualization.
This module provides utilities to get opcode names and detect specialization
status using the opcode module's metadata. Used by heatmap and flamegraph
collectors to display which bytecode instructions are executing at each
source line, including Python's adaptive specialization optimizations.
"""
import opcode
# Build opcode name mapping: opcode number -> opcode name
# This includes both standard opcodes and specialized variants (Python 3.11+)
_OPCODE_NAMES = dict(enumerate(opcode.opname))
if hasattr(opcode, "_specialized_opmap"):
for name, op in opcode._specialized_opmap.items():
_OPCODE_NAMES[op] = name
# Build deopt mapping: specialized opcode number -> base opcode number
# Python 3.11+ uses adaptive specialization where generic opcodes like
# LOAD_ATTR can be replaced at runtime with specialized variants like
# LOAD_ATTR_INSTANCE_VALUE. This mapping lets us show both forms.
_DEOPT_MAP = {}
if hasattr(opcode, "_specializations") and hasattr(
opcode, "_specialized_opmap"
):
for base_name, variant_names in opcode._specializations.items():
base_opcode = opcode.opmap.get(base_name)
if base_opcode is not None:
for variant_name in variant_names:
variant_opcode = opcode._specialized_opmap.get(variant_name)
if variant_opcode is not None:
_DEOPT_MAP[variant_opcode] = base_opcode
def get_opcode_info(opcode_num):
"""Get opcode name and specialization info from an opcode number.
Args:
opcode_num: The opcode number (0-255 or higher for specialized)
Returns:
A dict with keys:
- 'opname': The opcode name (e.g., 'LOAD_ATTR_INSTANCE_VALUE')
- 'base_opname': The base opcode name (e.g., 'LOAD_ATTR')
- 'is_specialized': True if this is a specialized instruction
"""
opname = _OPCODE_NAMES.get(opcode_num)
if opname is None:
return {
"opname": f"<{opcode_num}>",
"base_opname": f"<{opcode_num}>",
"is_specialized": False,
}
base_opcode = _DEOPT_MAP.get(opcode_num)
if base_opcode is not None:
base_opname = _OPCODE_NAMES.get(base_opcode, f"<{base_opcode}>")
return {
"opname": opname,
"base_opname": base_opname,
"is_specialized": True,
}
return {
"opname": opname,
"base_opname": opname,
"is_specialized": False,
}
def format_opcode(opcode_num):
"""Format an opcode for display, showing base opcode for specialized ones.
Args:
opcode_num: The opcode number (0-255 or higher for specialized)
Returns:
A formatted string like 'LOAD_ATTR' or 'LOAD_ATTR_INSTANCE_VALUE (LOAD_ATTR)'
"""
info = get_opcode_info(opcode_num)
if info["is_specialized"]:
return f"{info['opname']} ({info['base_opname']})"
return info["opname"]
def get_opcode_mapping():
"""Get opcode name and deopt mappings for JavaScript consumption.
Returns:
A dict with keys:
- 'names': Dict mapping opcode numbers to opcode names
- 'deopt': Dict mapping specialized opcode numbers to base opcode numbers
"""
return {"names": _OPCODE_NAMES, "deopt": _DEOPT_MAP}

View file

@ -2,7 +2,7 @@
import marshal
from _colorize import ANSIColors
from .collector import Collector
from .collector import Collector, extract_lineno
class PstatsCollector(Collector):
@ -23,12 +23,15 @@ def _process_frames(self, frames):
return
# Process each frame in the stack to track cumulative calls
# frame.location is int, tuple (lineno, end_lineno, col_offset, end_col_offset), or None
for frame in frames:
location = (frame.filename, frame.lineno, frame.funcname)
self.result[location]["cumulative_calls"] += 1
lineno = extract_lineno(frame.location)
loc = (frame.filename, lineno, frame.funcname)
self.result[loc]["cumulative_calls"] += 1
# The top frame gets counted as an inline call (directly executing)
top_location = (frames[0].filename, frames[0].lineno, frames[0].funcname)
top_lineno = extract_lineno(frames[0].location)
top_location = (frames[0].filename, top_lineno, frames[0].funcname)
self.result[top_location]["direct_calls"] += 1
# Track caller-callee relationships for call graph
@ -36,8 +39,10 @@ def _process_frames(self, frames):
callee_frame = frames[i - 1]
caller_frame = frames[i]
callee = (callee_frame.filename, callee_frame.lineno, callee_frame.funcname)
caller = (caller_frame.filename, caller_frame.lineno, caller_frame.funcname)
callee_lineno = extract_lineno(callee_frame.location)
caller_lineno = extract_lineno(caller_frame.location)
callee = (callee_frame.filename, callee_lineno, callee_frame.funcname)
caller = (caller_frame.filename, caller_lineno, caller_frame.funcname)
self.callers[callee][caller] += 1

View file

@ -27,7 +27,7 @@
class SampleProfiler:
def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MODE_WALL, native=False, gc=True, skip_non_matching_threads=True, collect_stats=False):
def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MODE_WALL, native=False, gc=True, opcodes=False, skip_non_matching_threads=True, collect_stats=False):
self.pid = pid
self.sample_interval_usec = sample_interval_usec
self.all_threads = all_threads
@ -36,15 +36,15 @@ def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MOD
if _FREE_THREADED_BUILD:
self.unwinder = _remote_debugging.RemoteUnwinder(
self.pid, all_threads=self.all_threads, mode=mode, native=native, gc=gc,
skip_non_matching_threads=skip_non_matching_threads, cache_frames=True,
stats=collect_stats
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,
skip_non_matching_threads=skip_non_matching_threads, cache_frames=True,
stats=collect_stats
opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
cache_frames=True, stats=collect_stats
)
# Track sample intervals and total sample count
self.sample_intervals = deque(maxlen=100)
@ -289,6 +289,7 @@ def sample(
async_aware=None,
native=False,
gc=True,
opcodes=False,
):
"""Sample a process using the provided collector.
@ -302,6 +303,7 @@ def sample(
GIL (only when holding GIL), ALL (includes GIL and CPU status)
native: Whether to include native frames
gc: Whether to include GC frames
opcodes: Whether to include opcode information
Returns:
The collector with collected samples
@ -324,6 +326,7 @@ def sample(
mode=mode,
native=native,
gc=gc,
opcodes=opcodes,
skip_non_matching_threads=skip_non_matching_threads,
collect_stats=realtime_stats,
)
@ -346,6 +349,7 @@ def sample_live(
async_aware=None,
native=False,
gc=True,
opcodes=False,
):
"""Sample a process in live/interactive mode with curses TUI.
@ -359,6 +363,7 @@ def sample_live(
GIL (only when holding GIL), ALL (includes GIL and CPU status)
native: Whether to include native frames
gc: Whether to include GC frames
opcodes: Whether to include opcode information
Returns:
The collector with collected samples
@ -381,6 +386,7 @@ def sample_live(
mode=mode,
native=native,
gc=gc,
opcodes=opcodes,
skip_non_matching_threads=skip_non_matching_threads,
collect_stats=realtime_stats,
)

View file

@ -7,7 +7,8 @@
import os
from ._css_utils import get_combined_css
from .collector import Collector
from .collector import Collector, extract_lineno
from .opcode_utils import get_opcode_mapping
from .string_table import StringTable
@ -40,7 +41,11 @@ def __init__(self, *args, **kwargs):
self.stack_counter = collections.Counter()
def process_frames(self, frames, thread_id):
call_tree = tuple(reversed(frames))
# 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
def export(self, filename):
@ -213,6 +218,11 @@ def convert_children(children, min_samples):
source_indices = [self._string_table.intern(line) for line in source]
child_entry["source"] = source_indices
# Include opcode data if available
opcodes = node.get("opcodes", {})
if opcodes:
child_entry["opcodes"] = dict(opcodes)
# Recurse
child_entry["children"] = convert_children(
node["children"], min_samples
@ -259,6 +269,9 @@ def convert_children(children, min_samples):
**stats
}
# Build opcode mapping for JS
opcode_mapping = get_opcode_mapping()
# If we only have one root child, make it the root to avoid redundant level
if len(root_children) == 1:
main_child = root_children[0]
@ -273,6 +286,7 @@ def convert_children(children, min_samples):
}
main_child["threads"] = sorted(list(self._all_threads))
main_child["strings"] = self._string_table.get_strings()
main_child["opcode_mapping"] = opcode_mapping
return main_child
return {
@ -285,27 +299,41 @@ def convert_children(children, min_samples):
"per_thread_stats": per_thread_stats_with_pct
},
"threads": sorted(list(self._all_threads)),
"strings": self._string_table.get_strings()
"strings": self._string_table.get_strings(),
"opcode_mapping": opcode_mapping
}
def process_frames(self, frames, thread_id):
# Reverse to root->leaf
call_tree = reversed(frames)
"""Process stack frames into flamegraph tree structure.
Args:
frames: List of (filename, location, funcname, opcode) tuples in
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
"""
# Reverse to root->leaf order for tree building
self._root["samples"] += 1
self._total_samples += 1
self._root["threads"].add(thread_id)
self._all_threads.add(thread_id)
current = self._root
for func in call_tree:
for filename, location, funcname, opcode in reversed(frames):
lineno = extract_lineno(location)
func = (filename, lineno, funcname)
func = self._func_intern.setdefault(func, func)
children = current["children"]
node = children.get(func)
node = current["children"].get(func)
if node is None:
node = {"samples": 0, "children": {}, "threads": set()}
children[func] = node
node = {"samples": 0, "children": {}, "threads": set(), "opcodes": collections.Counter()}
current["children"][func] = node
node["samples"] += 1
node["threads"].add(thread_id)
if opcode is not None:
node["opcodes"][opcode] += 1
current = node
def _get_source_lines(self, func):

View file

@ -379,6 +379,31 @@ def _extract_coroutine_stacks(self, stack_trace):
for task in stack_trace[0].awaited_by
}
@staticmethod
def _frame_to_lineno_tuple(frame):
"""Convert frame to (filename, lineno, funcname, opcode) tuple.
This extracts just the line number from the location, ignoring column
offsets which can vary due to sampling timing (e.g., when two statements
are on the same line, the sample might catch either one).
"""
filename, location, funcname, opcode = frame
return (filename, location.lineno, funcname, opcode)
def _extract_coroutine_stacks_lineno_only(self, stack_trace):
"""Extract coroutine stacks with line numbers only (no column offsets).
Use this for tests where sampling timing can cause column offset
variations (e.g., 'expr1; expr2' on the same line).
"""
return {
task.task_name: sorted(
tuple(self._frame_to_lineno_tuple(frame) for frame in coro.call_stack)
for coro in task.coroutine_stack
)
for task in stack_trace[0].awaited_by
}
# ============================================================================
# Test classes
@ -442,39 +467,25 @@ def foo():
"Insufficient permissions to read the stack trace"
)
thread_expected_stack_trace = [
FrameInfo([script_name, 15, "foo"]),
FrameInfo([script_name, 12, "baz"]),
FrameInfo([script_name, 9, "bar"]),
FrameInfo([threading.__file__, ANY, "Thread.run"]),
FrameInfo(
[
threading.__file__,
ANY,
"Thread._bootstrap_inner",
]
),
FrameInfo(
[threading.__file__, ANY, "Thread._bootstrap"]
),
]
# Find expected thread stack
# Find expected thread stack by funcname
found_thread = self._find_thread_with_frame(
stack_trace,
lambda f: f.funcname == "foo" and f.lineno == 15,
lambda f: f.funcname == "foo" and f.location.lineno == 15,
)
self.assertIsNotNone(
found_thread, "Expected thread stack trace not found"
)
# Check the funcnames in order
funcnames = [f.funcname for f in found_thread.frame_info]
self.assertEqual(
found_thread.frame_info, thread_expected_stack_trace
funcnames[:6],
["foo", "baz", "bar", "Thread.run", "Thread._bootstrap_inner", "Thread._bootstrap"]
)
# Check main thread
main_frame = FrameInfo([script_name, 19, "<module>"])
found_main = self._find_frame_in_trace(
stack_trace, lambda f: f == main_frame
stack_trace,
lambda f: f.funcname == "<module>" and f.location.lineno == 19,
)
self.assertIsNotNone(
found_main, "Main thread stack trace not found"
@ -596,8 +607,10 @@ def new_eager_loop():
},
)
# Check coroutine stacks
coroutine_stacks = self._extract_coroutine_stacks(
# Check coroutine stacks (using line numbers only to avoid
# flakiness from column offset variations when sampling
# catches different statements on the same line)
coroutine_stacks = self._extract_coroutine_stacks_lineno_only(
stack_trace
)
self.assertEqual(
@ -605,48 +618,36 @@ def new_eager_loop():
{
"Task-1": [
(
tuple(
[
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
]
),
tuple(
[
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
]
),
tuple([script_name, 26, "main"]),
(taskgroups.__file__, ANY, "TaskGroup._aexit", None),
(taskgroups.__file__, ANY, "TaskGroup.__aexit__", None),
(script_name, 26, "main", None),
)
],
"c2_root": [
(
tuple([script_name, 10, "c5"]),
tuple([script_name, 14, "c4"]),
tuple([script_name, 17, "c3"]),
tuple([script_name, 20, "c2"]),
(script_name, 10, "c5", None),
(script_name, 14, "c4", None),
(script_name, 17, "c3", None),
(script_name, 20, "c2", None),
)
],
"sub_main_1": [
(tuple([script_name, 23, "c1"]),)
((script_name, 23, "c1", None),)
],
"sub_main_2": [
(tuple([script_name, 23, "c1"]),)
((script_name, 23, "c1", None),)
],
},
)
# Check awaited_by coroutine stacks
# Check awaited_by coroutine stacks (line numbers only)
id_to_task = self._get_task_id_map(stack_trace)
awaited_by_coroutine_stacks = {
task.task_name: sorted(
(
id_to_task[coro.task_name].task_name,
tuple(
tuple(frame)
self._frame_to_lineno_tuple(frame)
for frame in coro.call_stack
),
)
@ -662,51 +663,27 @@ def new_eager_loop():
(
"Task-1",
(
tuple(
[
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
]
),
tuple(
[
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
]
),
tuple([script_name, 26, "main"]),
(taskgroups.__file__, ANY, "TaskGroup._aexit", None),
(taskgroups.__file__, ANY, "TaskGroup.__aexit__", None),
(script_name, 26, "main", None),
),
),
(
"sub_main_1",
(tuple([script_name, 23, "c1"]),),
((script_name, 23, "c1", None),),
),
(
"sub_main_2",
(tuple([script_name, 23, "c1"]),),
((script_name, 23, "c1", None),),
),
],
"sub_main_1": [
(
"Task-1",
(
tuple(
[
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
]
),
tuple(
[
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
]
),
tuple([script_name, 26, "main"]),
(taskgroups.__file__, ANY, "TaskGroup._aexit", None),
(taskgroups.__file__, ANY, "TaskGroup.__aexit__", None),
(script_name, 26, "main", None),
),
)
],
@ -714,21 +691,9 @@ def new_eager_loop():
(
"Task-1",
(
tuple(
[
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
]
),
tuple(
[
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
]
),
tuple([script_name, 26, "main"]),
(taskgroups.__file__, ANY, "TaskGroup._aexit", None),
(taskgroups.__file__, ANY, "TaskGroup.__aexit__", None),
(script_name, 26, "main", None),
),
)
],
@ -800,18 +765,20 @@ async def main():
task = stack_trace[0].awaited_by[0]
self.assertEqual(task.task_name, "Task-1")
# Check the coroutine stack
# Check the coroutine stack (using line numbers only to avoid
# flakiness from column offset variations when sampling
# catches different statements on the same line)
coroutine_stack = sorted(
tuple(tuple(frame) for frame in coro.call_stack)
tuple(self._frame_to_lineno_tuple(frame) for frame in coro.call_stack)
for coro in task.coroutine_stack
)
self.assertEqual(
coroutine_stack,
[
(
tuple([script_name, 10, "gen_nested_call"]),
tuple([script_name, 16, "gen"]),
tuple([script_name, 19, "main"]),
(script_name, 10, "gen_nested_call", None),
(script_name, 16, "gen", None),
(script_name, 19, "main", None),
)
],
)
@ -899,31 +866,33 @@ async def main():
},
)
# Check coroutine stacks
coroutine_stacks = self._extract_coroutine_stacks(
# Check coroutine stacks (using line numbers only to avoid
# flakiness from column offset variations when sampling
# catches different statements on the same line)
coroutine_stacks = self._extract_coroutine_stacks_lineno_only(
stack_trace
)
self.assertEqual(
coroutine_stacks,
{
"Task-1": [(tuple([script_name, 21, "main"]),)],
"Task-1": [((script_name, 21, "main", None),)],
"Task-2": [
(
tuple([script_name, 11, "deep"]),
tuple([script_name, 15, "c1"]),
(script_name, 11, "deep", None),
(script_name, 15, "c1", None),
)
],
},
)
# Check awaited_by coroutine stacks
# Check awaited_by coroutine stacks (line numbers only)
id_to_task = self._get_task_id_map(stack_trace)
awaited_by_coroutine_stacks = {
task.task_name: sorted(
(
id_to_task[coro.task_name].task_name,
tuple(
tuple(frame) for frame in coro.call_stack
self._frame_to_lineno_tuple(frame) for frame in coro.call_stack
),
)
for coro in task.awaited_by
@ -935,7 +904,7 @@ async def main():
{
"Task-1": [],
"Task-2": [
("Task-1", (tuple([script_name, 21, "main"]),))
("Task-1", ((script_name, 21, "main", None),))
],
},
)
@ -1023,8 +992,10 @@ async def main():
},
)
# Check coroutine stacks
coroutine_stacks = self._extract_coroutine_stacks(
# Check coroutine stacks (using line numbers only to avoid
# flakiness from column offset variations when sampling
# catches different statements on the same line)
coroutine_stacks = self._extract_coroutine_stacks_lineno_only(
stack_trace
)
self.assertEqual(
@ -1032,40 +1003,28 @@ async def main():
{
"Task-1": [
(
tuple(
[
staggered.__file__,
ANY,
"staggered_race",
]
),
tuple([script_name, 21, "main"]),
(staggered.__file__, ANY, "staggered_race", None),
(script_name, 21, "main", None),
)
],
"Task-2": [
(
tuple([script_name, 11, "deep"]),
tuple([script_name, 15, "c1"]),
tuple(
[
staggered.__file__,
ANY,
"staggered_race.<locals>.run_one_coro",
]
),
(script_name, 11, "deep", None),
(script_name, 15, "c1", None),
(staggered.__file__, ANY, "staggered_race.<locals>.run_one_coro", None),
)
],
},
)
# Check awaited_by coroutine stacks
# Check awaited_by coroutine stacks (line numbers only)
id_to_task = self._get_task_id_map(stack_trace)
awaited_by_coroutine_stacks = {
task.task_name: sorted(
(
id_to_task[coro.task_name].task_name,
tuple(
tuple(frame) for frame in coro.call_stack
self._frame_to_lineno_tuple(frame) for frame in coro.call_stack
),
)
for coro in task.awaited_by
@ -1080,14 +1039,8 @@ async def main():
(
"Task-1",
(
tuple(
[
staggered.__file__,
ANY,
"staggered_race",
]
),
tuple([script_name, 21, "main"]),
(staggered.__file__, ANY, "staggered_race", None),
(script_name, 21, "main", None),
),
)
],
@ -1209,12 +1162,12 @@ async def main():
# Check the main task structure
main_stack = [
FrameInfo(
[taskgroups.__file__, ANY, "TaskGroup._aexit"]
[taskgroups.__file__, ANY, "TaskGroup._aexit", ANY]
),
FrameInfo(
[taskgroups.__file__, ANY, "TaskGroup.__aexit__"]
[taskgroups.__file__, ANY, "TaskGroup.__aexit__", ANY]
),
FrameInfo([script_name, 52, "main"]),
FrameInfo([script_name, ANY, "main", ANY]),
]
self.assertIn(
TaskInfo(
@ -1236,6 +1189,7 @@ async def main():
base_events.__file__,
ANY,
"Server.serve_forever",
ANY,
]
)
],
@ -1252,6 +1206,7 @@ async def main():
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
ANY,
]
),
FrameInfo(
@ -1259,10 +1214,11 @@ async def main():
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
ANY,
]
),
FrameInfo(
[script_name, ANY, "main"]
[script_name, ANY, "main", ANY]
),
],
ANY,
@ -1287,13 +1243,15 @@ async def main():
tasks.__file__,
ANY,
"sleep",
ANY,
]
),
FrameInfo(
[
script_name,
36,
ANY,
"echo_client",
ANY,
]
),
],
@ -1310,6 +1268,7 @@ async def main():
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
ANY,
]
),
FrameInfo(
@ -1317,13 +1276,15 @@ async def main():
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
ANY,
]
),
FrameInfo(
[
script_name,
39,
ANY,
"echo_client_spam",
ANY,
]
),
],
@ -1336,36 +1297,24 @@ async def main():
entries,
)
expected_awaited_by = [
CoroInfo(
[
[
FrameInfo(
[
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
]
),
FrameInfo(
[
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
]
),
FrameInfo(
[script_name, 39, "echo_client_spam"]
),
],
ANY,
]
)
]
# Find tasks awaited by echo_client_spam via TaskGroup
def matches_awaited_by_pattern(task):
if len(task.awaited_by) != 1:
return False
coro = task.awaited_by[0]
if len(coro.call_stack) != 3:
return False
funcnames = [f.funcname for f in coro.call_stack]
return funcnames == [
"TaskGroup._aexit",
"TaskGroup.__aexit__",
"echo_client_spam",
]
tasks_with_awaited = [
task
for task in entries
if task.awaited_by == expected_awaited_by
if matches_awaited_by_pattern(task)
]
self.assertGreaterEqual(len(tasks_with_awaited), NUM_TASKS)
@ -1396,25 +1345,12 @@ def test_self_trace(self):
break
self.assertIsNotNone(this_thread_stack)
self.assertEqual(
this_thread_stack[:2],
[
FrameInfo(
[
__file__,
get_stack_trace.__code__.co_firstlineno + 4,
"get_stack_trace",
]
),
FrameInfo(
[
__file__,
self.test_self_trace.__code__.co_firstlineno + 6,
"TestGetStackTrace.test_self_trace",
]
),
],
)
# Check the top two frames
self.assertGreaterEqual(len(this_thread_stack), 2)
self.assertEqual(this_thread_stack[0].funcname, "get_stack_trace")
self.assertTrue(this_thread_stack[0].filename.endswith("test_external_inspection.py"))
self.assertEqual(this_thread_stack[1].funcname, "TestGetStackTrace.test_self_trace")
self.assertTrue(this_thread_stack[1].filename.endswith("test_external_inspection.py"))
@skip_if_not_supported
@unittest.skipIf(
@ -1815,7 +1751,7 @@ def main_work():
found = self._find_frame_in_trace(
all_traces,
lambda f: f.funcname == "main_work"
and f.lineno > 12,
and f.location.lineno > 12,
)
if found:
break
@ -1865,6 +1801,136 @@ def main_work():
finally:
_cleanup_sockets(client_socket, server_socket)
@skip_if_not_supported
@unittest.skipIf(
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
"Test only runs on Linux with process_vm_readv support",
)
def test_opcodes_collection(self):
"""Test that opcodes are collected when the opcodes flag is set."""
script = textwrap.dedent(
"""\
import time, sys, socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', {port}))
def foo():
sock.sendall(b"ready")
time.sleep(10_000)
foo()
"""
)
def get_trace_with_opcodes(pid):
return RemoteUnwinder(pid, opcodes=True).get_stack_trace()
stack_trace, _ = self._run_script_and_get_trace(
script, get_trace_with_opcodes, wait_for_signals=b"ready"
)
# Find our foo frame and verify it has an opcode
foo_frame = self._find_frame_in_trace(
stack_trace, lambda f: f.funcname == "foo"
)
self.assertIsNotNone(foo_frame, "Could not find foo frame")
self.assertIsInstance(foo_frame.opcode, int)
self.assertGreaterEqual(foo_frame.opcode, 0)
@skip_if_not_supported
@unittest.skipIf(
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
"Test only runs on Linux with process_vm_readv support",
)
def test_location_tuple_format(self):
"""Test that location is a 4-tuple (lineno, end_lineno, col_offset, end_col_offset)."""
script = textwrap.dedent(
"""\
import time, sys, socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', {port}))
def foo():
sock.sendall(b"ready")
time.sleep(10_000)
foo()
"""
)
def get_trace_with_opcodes(pid):
return RemoteUnwinder(pid, opcodes=True).get_stack_trace()
stack_trace, _ = self._run_script_and_get_trace(
script, get_trace_with_opcodes, wait_for_signals=b"ready"
)
# Find our foo frame
foo_frame = self._find_frame_in_trace(
stack_trace, lambda f: f.funcname == "foo"
)
self.assertIsNotNone(foo_frame, "Could not find foo frame")
# Check location is a 4-tuple with valid values
location = foo_frame.location
self.assertIsInstance(location, tuple)
self.assertEqual(len(location), 4)
lineno, end_lineno, col_offset, end_col_offset = location
self.assertIsInstance(lineno, int)
self.assertGreater(lineno, 0)
self.assertIsInstance(end_lineno, int)
self.assertGreaterEqual(end_lineno, lineno)
self.assertIsInstance(col_offset, int)
self.assertGreaterEqual(col_offset, 0)
self.assertIsInstance(end_col_offset, int)
self.assertGreaterEqual(end_col_offset, col_offset)
@skip_if_not_supported
@unittest.skipIf(
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
"Test only runs on Linux with process_vm_readv support",
)
def test_location_tuple_exact_values(self):
"""Test exact values of location tuple including column offsets."""
script = textwrap.dedent(
"""\
import time, sys, socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', {port}))
def foo():
sock.sendall(b"ready")
time.sleep(10_000)
foo()
"""
)
def get_trace_with_opcodes(pid):
return RemoteUnwinder(pid, opcodes=True).get_stack_trace()
stack_trace, _ = self._run_script_and_get_trace(
script, get_trace_with_opcodes, wait_for_signals=b"ready"
)
foo_frame = self._find_frame_in_trace(
stack_trace, lambda f: f.funcname == "foo"
)
self.assertIsNotNone(foo_frame, "Could not find foo frame")
# Can catch either sock.sendall (line 7) or time.sleep (line 8)
location = foo_frame.location
valid_locations = [
(7, 7, 4, 26), # sock.sendall(b"ready")
(8, 8, 4, 22), # time.sleep(10_000)
]
actual = (location.lineno, location.end_lineno,
location.col_offset, location.end_col_offset)
self.assertIn(actual, valid_locations)
class TestUnsupportedPlatformHandling(unittest.TestCase):
@unittest.skipIf(
@ -2404,13 +2470,13 @@ def inner():
# Line numbers must be different and increasing (execution moves forward)
self.assertLess(
inner_a.lineno, inner_b.lineno, "Line B should be after line A"
inner_a.location.lineno, inner_b.location.lineno, "Line B should be after line A"
)
self.assertLess(
inner_b.lineno, inner_c.lineno, "Line C should be after line B"
inner_b.location.lineno, inner_c.location.lineno, "Line C should be after line B"
)
self.assertLess(
inner_c.lineno, inner_d.lineno, "Line D should be after line C"
inner_c.location.lineno, inner_d.location.lineno, "Line D should be after line C"
)
@skip_if_not_supported
@ -2709,10 +2775,10 @@ def level1():
funcs_no_cache = [f.funcname for f in frames_no_cache]
self.assertEqual(funcs_cached, funcs_no_cache)
# Same line numbers
lines_cached = [f.lineno for f in frames_cached]
lines_no_cache = [f.lineno for f in frames_no_cache]
self.assertEqual(lines_cached, lines_no_cache)
# Same locations
locations_cached = [f.location for f in frames_cached]
locations_no_cache = [f.location for f in frames_no_cache]
self.assertEqual(locations_cached, locations_no_cache)
@skip_if_not_supported
@unittest.skipIf(

View file

@ -4,8 +4,12 @@
import shutil
import tempfile
import unittest
from collections import namedtuple
from pathlib import Path
# Matches the C structseq LocationInfo from _remote_debugging
LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset'])
from profiling.sampling.heatmap_collector import (
HeatmapCollector,
get_python_path_info,
@ -214,7 +218,7 @@ def test_process_frames_increments_total_samples(self):
collector = HeatmapCollector(sample_interval_usec=100)
initial_count = collector._total_samples
frames = [('file.py', 10, 'func')]
frames = [('file.py', (10, 10, -1, -1), 'func', None)]
collector.process_frames(frames, thread_id=1)
self.assertEqual(collector._total_samples, initial_count + 1)
@ -223,7 +227,7 @@ def test_process_frames_records_line_samples(self):
"""Test that process_frames records line samples."""
collector = HeatmapCollector(sample_interval_usec=100)
frames = [('test.py', 5, 'test_func')]
frames = [('test.py', (5, 5, -1, -1), 'test_func', None)]
collector.process_frames(frames, thread_id=1)
# Check that line was recorded
@ -235,9 +239,9 @@ def test_process_frames_records_multiple_lines_in_stack(self):
collector = HeatmapCollector(sample_interval_usec=100)
frames = [
('file1.py', 10, 'func1'),
('file2.py', 20, 'func2'),
('file3.py', 30, 'func3')
('file1.py', (10, 10, -1, -1), 'func1', None),
('file2.py', (20, 20, -1, -1), 'func2', None),
('file3.py', (30, 30, -1, -1), 'func3', None)
]
collector.process_frames(frames, thread_id=1)
@ -251,8 +255,8 @@ def test_process_frames_distinguishes_self_samples(self):
collector = HeatmapCollector(sample_interval_usec=100)
frames = [
('leaf.py', 5, 'leaf_func'), # This is the leaf (top of stack)
('caller.py', 10, 'caller_func')
('leaf.py', (5, 5, -1, -1), 'leaf_func', None), # This is the leaf (top of stack)
('caller.py', (10, 10, -1, -1), 'caller_func', None)
]
collector.process_frames(frames, thread_id=1)
@ -267,7 +271,7 @@ def test_process_frames_accumulates_samples(self):
"""Test that multiple calls accumulate samples."""
collector = HeatmapCollector(sample_interval_usec=100)
frames = [('file.py', 10, 'func')]
frames = [('file.py', (10, 10, -1, -1), 'func', None)]
collector.process_frames(frames, thread_id=1)
collector.process_frames(frames, thread_id=1)
@ -282,11 +286,11 @@ def test_process_frames_ignores_invalid_frames(self):
# These should be ignored
invalid_frames = [
('<string>', 1, 'test'),
('[eval]', 1, 'test'),
('', 1, 'test'),
(None, 1, 'test'),
('__init__', 0, 'test'), # Special invalid frame
('<string>', (1, 1, -1, -1), 'test', None),
('[eval]', (1, 1, -1, -1), 'test', None),
('', (1, 1, -1, -1), 'test', None),
(None, (1, 1, -1, -1), 'test', None),
('__init__', (0, 0, -1, -1), 'test', None), # Special invalid frame
]
for frame in invalid_frames:
@ -295,15 +299,15 @@ def test_process_frames_ignores_invalid_frames(self):
# Should not record these invalid frames
for frame in invalid_frames:
if frame[0]:
self.assertNotIn((frame[0], frame[1]), collector.line_samples)
self.assertNotIn((frame[0], frame[1][0]), collector.line_samples)
def test_process_frames_builds_call_graph(self):
"""Test that process_frames builds call graph relationships."""
collector = HeatmapCollector(sample_interval_usec=100)
frames = [
('callee.py', 5, 'callee_func'),
('caller.py', 10, 'caller_func')
('callee.py', (5, 5, -1, -1), 'callee_func', None),
('caller.py', (10, 10, -1, -1), 'caller_func', None)
]
collector.process_frames(frames, thread_id=1)
@ -319,7 +323,7 @@ def test_process_frames_records_function_definitions(self):
"""Test that process_frames records function definition locations."""
collector = HeatmapCollector(sample_interval_usec=100)
frames = [('module.py', 42, 'my_function')]
frames = [('module.py', (42, 42, -1, -1), 'my_function', None)]
collector.process_frames(frames, thread_id=1)
self.assertIn(('module.py', 'my_function'), collector.function_definitions)
@ -330,8 +334,8 @@ def test_process_frames_tracks_edge_samples(self):
collector = HeatmapCollector(sample_interval_usec=100)
frames = [
('callee.py', 5, 'callee'),
('caller.py', 10, 'caller')
('callee.py', (5, 5, -1, -1), 'callee', None),
('caller.py', (10, 10, -1, -1), 'caller', None)
]
# Process same call stack multiple times
@ -355,7 +359,7 @@ def test_process_frames_with_file_samples_dict(self):
"""Test that file_samples dict is properly populated."""
collector = HeatmapCollector(sample_interval_usec=100)
frames = [('test.py', 10, 'func')]
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
collector.process_frames(frames, thread_id=1)
self.assertIn('test.py', collector.file_samples)
@ -376,7 +380,7 @@ def test_export_creates_output_directory(self):
collector = HeatmapCollector(sample_interval_usec=100)
# Add some data
frames = [('test.py', 10, 'func')]
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
collector.process_frames(frames, thread_id=1)
output_path = os.path.join(self.test_dir, 'heatmap_output')
@ -391,7 +395,7 @@ def test_export_creates_index_html(self):
"""Test that export creates index.html."""
collector = HeatmapCollector(sample_interval_usec=100)
frames = [('test.py', 10, 'func')]
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
collector.process_frames(frames, thread_id=1)
output_path = os.path.join(self.test_dir, 'heatmap_output')
@ -406,7 +410,7 @@ def test_export_creates_file_htmls(self):
"""Test that export creates individual file HTMLs."""
collector = HeatmapCollector(sample_interval_usec=100)
frames = [('test.py', 10, 'func')]
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
collector.process_frames(frames, thread_id=1)
output_path = os.path.join(self.test_dir, 'heatmap_output')
@ -433,7 +437,7 @@ def test_export_handles_html_suffix(self):
"""Test that export handles .html suffix in output path."""
collector = HeatmapCollector(sample_interval_usec=100)
frames = [('test.py', 10, 'func')]
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
collector.process_frames(frames, thread_id=1)
# Path with .html suffix should be stripped
@ -451,9 +455,9 @@ def test_export_with_multiple_files(self):
collector = HeatmapCollector(sample_interval_usec=100)
# Add samples for multiple files
collector.process_frames([('file1.py', 10, 'func1')], thread_id=1)
collector.process_frames([('file2.py', 20, 'func2')], thread_id=1)
collector.process_frames([('file3.py', 30, 'func3')], thread_id=1)
collector.process_frames([('file1.py', (10, 10, -1, -1), 'func1', None)], thread_id=1)
collector.process_frames([('file2.py', (20, 20, -1, -1), 'func2', None)], thread_id=1)
collector.process_frames([('file3.py', (30, 30, -1, -1), 'func3', None)], thread_id=1)
output_path = os.path.join(self.test_dir, 'multi_file')
@ -470,7 +474,7 @@ def test_export_index_contains_file_references(self):
collector = HeatmapCollector(sample_interval_usec=100)
collector.set_stats(sample_interval_usec=100, duration_sec=1.0, sample_rate=100.0)
frames = [('mytest.py', 10, 'my_func')]
frames = [('mytest.py', (10, 10, -1, -1), 'my_func', None)]
collector.process_frames(frames, thread_id=1)
output_path = os.path.join(self.test_dir, 'test_output')
@ -494,7 +498,7 @@ def test_export_file_html_has_line_numbers(self):
with open(temp_file, 'w') as f:
f.write('def test():\n pass\n')
frames = [(temp_file, 1, 'test')]
frames = [(temp_file, (1, 1, -1, -1), 'test', None)]
collector.process_frames(frames, thread_id=1)
output_path = os.path.join(self.test_dir, 'line_test')
@ -515,23 +519,39 @@ def test_export_file_html_has_line_numbers(self):
class MockFrameInfo:
"""Mock FrameInfo for testing since the real one isn't accessible."""
"""Mock FrameInfo for testing.
def __init__(self, filename, lineno, funcname):
Frame format: (filename, location, funcname, opcode) where:
- location is a tuple (lineno, end_lineno, col_offset, end_col_offset)
- opcode is an int or None
"""
def __init__(self, filename, lineno, funcname, opcode=None):
self.filename = filename
self.lineno = lineno
self.funcname = funcname
self.opcode = opcode
self.location = (lineno, lineno, -1, -1)
def __iter__(self):
return iter((self.filename, self.location, self.funcname, self.opcode))
def __getitem__(self, index):
return (self.filename, self.location, self.funcname, self.opcode)[index]
def __len__(self):
return 4
def __repr__(self):
return f"MockFrameInfo(filename='{self.filename}', lineno={self.lineno}, funcname='{self.funcname}')"
return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})"
class MockThreadInfo:
"""Mock ThreadInfo for testing since the real one isn't accessible."""
def __init__(self, thread_id, frame_info):
def __init__(self, thread_id, frame_info, status=0):
self.thread_id = thread_id
self.frame_info = frame_info
self.status = status # Thread status flags
def __repr__(self):
return f"MockThreadInfo(thread_id={self.thread_id}, frame_info={self.frame_info})"
@ -559,13 +579,13 @@ def test_heatmap_collector_basic(self):
self.assertEqual(len(collector.file_samples), 0)
self.assertEqual(len(collector.line_samples), 0)
# Test collecting sample data
# Test collecting sample data - frames are 4-tuples: (filename, location, funcname, opcode)
test_frames = [
MockInterpreterInfo(
0,
[MockThreadInfo(
1,
[("file.py", 10, "func1"), ("file.py", 20, "func2")],
[MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")],
)]
)
]
@ -586,21 +606,21 @@ def test_heatmap_collector_export(self):
collector = HeatmapCollector(sample_interval_usec=100)
# Create test data with multiple files
# Create test data with multiple files using MockFrameInfo
test_frames1 = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [("file.py", 10, "func1"), ("file.py", 20, "func2")])],
[MockThreadInfo(1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")])],
)
]
test_frames2 = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [("file.py", 10, "func1"), ("file.py", 20, "func2")])],
[MockThreadInfo(1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")])],
)
] # Same stack
test_frames3 = [
MockInterpreterInfo(0, [MockThreadInfo(1, [("other.py", 5, "other_func")])])
MockInterpreterInfo(0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])])
]
collector.collect(test_frames1)
@ -643,5 +663,95 @@ def test_heatmap_collector_export(self):
self.assertIn("nav-btn", file_content)
class TestHeatmapCollectorLocation(unittest.TestCase):
"""Tests for HeatmapCollector location handling."""
def test_heatmap_with_full_location_info(self):
"""Test HeatmapCollector uses full location tuple."""
collector = HeatmapCollector(sample_interval_usec=1000)
# Frame with full location: (lineno, end_lineno, col_offset, end_col_offset)
frame = MockFrameInfo("test.py", 10, "func")
# Override with full location info
frame.location = LocationInfo(10, 15, 4, 20)
frames = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame])]
)
]
collector.collect(frames)
# Verify data was collected with location info
# HeatmapCollector uses file_samples dict with filename -> Counter of linenos
self.assertIn("test.py", collector.file_samples)
# Line 10 should have samples
self.assertIn(10, collector.file_samples["test.py"])
def test_heatmap_with_none_location(self):
"""Test HeatmapCollector handles None location gracefully."""
collector = HeatmapCollector(sample_interval_usec=1000)
# Synthetic frame with None location
frame = MockFrameInfo("~", 0, "<native>")
frame.location = None
frames = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame])]
)
]
# Should not raise
collector.collect(frames)
def test_heatmap_export_with_location_data(self):
"""Test HeatmapCollector export includes location info."""
tmp_dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, tmp_dir)
collector = HeatmapCollector(sample_interval_usec=1000)
frame = MockFrameInfo("test.py", 10, "process")
frame.location = LocationInfo(10, 12, 0, 30)
frames = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame])]
)
]
collector.collect(frames)
# Export should work
with (captured_stdout(), captured_stderr()):
collector.export(tmp_dir)
self.assertTrue(os.path.exists(os.path.join(tmp_dir, "index.html")))
def test_heatmap_collector_frame_format(self):
"""Test HeatmapCollector with 4-element frame format."""
collector = HeatmapCollector(sample_interval_usec=1000)
frames = [
MockInterpreterInfo(
0,
[
MockThreadInfo(
1,
[
MockFrameInfo("app.py", 100, "main", opcode=90),
MockFrameInfo("utils.py", 50, "helper", opcode=100),
MockFrameInfo("lib.py", 25, "process", opcode=None),
],
)
],
)
]
collector.collect(frames)
# Should have recorded data for the files
self.assertIn("app.py", collector.file_samples)
self.assertIn("utils.py", collector.file_samples)
self.assertIn("lib.py", collector.file_samples)
if __name__ == "__main__":
unittest.main()

View file

@ -1,21 +1,42 @@
"""Common test helpers and mocks for live collector tests."""
from collections import namedtuple
from profiling.sampling.constants import (
THREAD_STATUS_HAS_GIL,
THREAD_STATUS_ON_CPU,
)
class MockFrameInfo:
"""Mock FrameInfo for testing."""
# Matches the C structseq LocationInfo from _remote_debugging
LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset'])
def __init__(self, filename, lineno, funcname):
class MockFrameInfo:
"""Mock FrameInfo for testing.
Frame format: (filename, location, funcname, opcode) where:
- location is a tuple (lineno, end_lineno, col_offset, end_col_offset)
- opcode is an int or None
"""
def __init__(self, filename, lineno, funcname, opcode=None):
self.filename = filename
self.lineno = lineno
self.funcname = funcname
self.opcode = opcode
self.location = LocationInfo(lineno, lineno, -1, -1)
def __iter__(self):
return iter((self.filename, self.location, self.funcname, self.opcode))
def __getitem__(self, index):
return (self.filename, self.location, self.funcname, self.opcode)[index]
def __len__(self):
return 4
def __repr__(self):
return f"MockFrameInfo(filename='{self.filename}', lineno={self.lineno}, funcname='{self.funcname}')"
return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})"
class MockThreadInfo:

View file

@ -1,16 +1,36 @@
"""Mock classes for sampling profiler tests."""
from collections import namedtuple
# Matches the C structseq LocationInfo from _remote_debugging
LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset'])
class MockFrameInfo:
"""Mock FrameInfo for testing since the real one isn't accessible."""
"""Mock FrameInfo for testing.
def __init__(self, filename, lineno, funcname):
Frame format: (filename, location, funcname, opcode) where:
- location is a tuple (lineno, end_lineno, col_offset, end_col_offset)
- opcode is an int or None
"""
def __init__(self, filename, lineno, funcname, opcode=None):
self.filename = filename
self.lineno = lineno
self.funcname = funcname
self.opcode = opcode
self.location = LocationInfo(lineno, lineno, -1, -1)
def __iter__(self):
return iter((self.filename, self.location, self.funcname, self.opcode))
def __getitem__(self, index):
return (self.filename, self.location, self.funcname, self.opcode)[index]
def __len__(self):
return 4
def __repr__(self):
return f"MockFrameInfo(filename='{self.filename}', lineno={self.lineno}, funcname='{self.funcname}')"
return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})"
class MockThreadInfo:

View file

@ -14,9 +14,12 @@
FlamegraphCollector,
)
from profiling.sampling.gecko_collector import GeckoCollector
from profiling.sampling.collector import extract_lineno, normalize_location
from profiling.sampling.opcode_utils import get_opcode_info, format_opcode
from profiling.sampling.constants import (
PROFILING_MODE_WALL,
PROFILING_MODE_CPU,
DEFAULT_LOCATION,
)
from _remote_debugging import (
THREAD_STATUS_HAS_GIL,
@ -30,7 +33,7 @@
from test.support import captured_stdout, captured_stderr
from .mocks import MockFrameInfo, MockThreadInfo, MockInterpreterInfo
from .mocks import MockFrameInfo, MockThreadInfo, MockInterpreterInfo, LocationInfo
from .helpers import close_and_unlink
@ -42,9 +45,8 @@ def test_mock_frame_info_with_empty_and_unicode_values(self):
# Test with empty strings
frame = MockFrameInfo("", 0, "")
self.assertEqual(frame.filename, "")
self.assertEqual(frame.lineno, 0)
self.assertEqual(frame.location.lineno, 0)
self.assertEqual(frame.funcname, "")
self.assertIn("filename=''", repr(frame))
# Test with unicode characters
frame = MockFrameInfo("文件.py", 42, "函数名")
@ -56,7 +58,7 @@ def test_mock_frame_info_with_empty_and_unicode_values(self):
long_funcname = "func_" + "x" * 1000
frame = MockFrameInfo(long_filename, 999999, long_funcname)
self.assertEqual(frame.filename, long_filename)
self.assertEqual(frame.lineno, 999999)
self.assertEqual(frame.location.lineno, 999999)
self.assertEqual(frame.funcname, long_funcname)
def test_pstats_collector_with_extreme_intervals_and_empty_data(self):
@ -78,7 +80,7 @@ def test_pstats_collector_with_extreme_intervals_and_empty_data(self):
test_frames = [
MockInterpreterInfo(
0,
[MockThreadInfo(None, [MockFrameInfo("file.py", 10, "func")])],
[MockThreadInfo(None, [MockFrameInfo("file.py", 10, "func", None)])],
)
]
collector.collect(test_frames)
@ -193,7 +195,7 @@ def test_collapsed_stack_collector_with_empty_and_deep_stacks(self):
# Test with single frame stack
test_frames = [
MockInterpreterInfo(
0, [MockThreadInfo(1, [("file.py", 10, "func")])]
0, [MockThreadInfo(1, [MockFrameInfo("file.py", 10, "func")])]
)
]
collector.collect(test_frames)
@ -204,7 +206,7 @@ def test_collapsed_stack_collector_with_empty_and_deep_stacks(self):
self.assertEqual(count, 1)
# Test with very deep stack
deep_stack = [(f"file{i}.py", i, f"func{i}") for i in range(100)]
deep_stack = [MockFrameInfo(f"file{i}.py", i, f"func{i}") for i in range(100)]
test_frames = [MockInterpreterInfo(0, [MockThreadInfo(1, deep_stack)])]
collector = CollapsedStackCollector(1000)
collector.collect(test_frames)
@ -317,7 +319,7 @@ def test_collapsed_stack_collector_basic(self):
0,
[
MockThreadInfo(
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
@ -343,7 +345,7 @@ def test_collapsed_stack_collector_export(self):
0,
[
MockThreadInfo(
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
@ -353,14 +355,14 @@ def test_collapsed_stack_collector_export(self):
0,
[
MockThreadInfo(
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
] # Same stack
test_frames3 = [
MockInterpreterInfo(
0, [MockThreadInfo(1, [("other.py", 5, "other_func")])]
0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])]
)
]
@ -406,7 +408,7 @@ def test_flamegraph_collector_basic(self):
0,
[
MockThreadInfo(
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
@ -454,7 +456,7 @@ def test_flamegraph_collector_export(self):
0,
[
MockThreadInfo(
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
@ -464,14 +466,14 @@ def test_flamegraph_collector_export(self):
0,
[
MockThreadInfo(
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
] # Same stack
test_frames3 = [
MockInterpreterInfo(
0, [MockThreadInfo(1, [("other.py", 5, "other_func")])]
0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])]
)
]
@ -518,7 +520,7 @@ def test_gecko_collector_basic(self):
[
MockThreadInfo(
1,
[("file.py", 10, "func1"), ("file.py", 20, "func2")],
[MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")],
)
],
)
@ -608,7 +610,7 @@ def test_gecko_collector_export(self):
0,
[
MockThreadInfo(
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
@ -618,14 +620,14 @@ def test_gecko_collector_export(self):
0,
[
MockThreadInfo(
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
] # Same stack
test_frames3 = [
MockInterpreterInfo(
0, [MockThreadInfo(1, [("other.py", 5, "other_func")])]
0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])]
)
]
@ -683,7 +685,7 @@ def test_gecko_collector_markers(self):
[
MockThreadInfo(
1,
[("test.py", 10, "python_func")],
[MockFrameInfo("test.py", 10, "python_func")],
status=HAS_GIL_ON_CPU,
)
],
@ -698,7 +700,7 @@ def test_gecko_collector_markers(self):
[
MockThreadInfo(
1,
[("test.py", 15, "wait_func")],
[MockFrameInfo("test.py", 15, "wait_func")],
status=WAITING_FOR_GIL,
)
],
@ -713,7 +715,7 @@ def test_gecko_collector_markers(self):
[
MockThreadInfo(
1,
[("test.py", 20, "python_func2")],
[MockFrameInfo("test.py", 20, "python_func2")],
status=HAS_GIL_ON_CPU,
)
],
@ -728,7 +730,7 @@ def test_gecko_collector_markers(self):
[
MockThreadInfo(
1,
[("native.c", 100, "native_func")],
[MockFrameInfo("native.c", 100, "native_func")],
status=NO_GIL_ON_CPU,
)
],
@ -902,8 +904,8 @@ def test_flamegraph_collector_stats_accumulation(self):
MockInterpreterInfo(
0,
[
MockThreadInfo(1, [("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(2, [("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
],
)
]
@ -917,9 +919,9 @@ def test_flamegraph_collector_stats_accumulation(self):
MockInterpreterInfo(
0,
[
MockThreadInfo(1, [("a.py", 1, "func_a")], status=THREAD_STATUS_GIL_REQUESTED),
MockThreadInfo(2, [("b.py", 2, "func_b")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(3, [("c.py", 3, "func_c")], status=THREAD_STATUS_ON_CPU),
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_GIL_REQUESTED),
MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(3, [MockFrameInfo("c.py", 3, "func_c")], status=THREAD_STATUS_ON_CPU),
],
)
]
@ -936,7 +938,7 @@ def test_flamegraph_collector_stats_accumulation(self):
MockInterpreterInfo(
0,
[
MockThreadInfo(1, [("~", 0, "<GC>")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(1, [MockFrameInfo("~", 0, "<GC>")], status=THREAD_STATUS_HAS_GIL),
],
)
]
@ -960,9 +962,9 @@ def test_flamegraph_collector_per_thread_stats(self):
MockInterpreterInfo(
0,
[
MockThreadInfo(1, [("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(2, [("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
MockThreadInfo(3, [("c.py", 3, "func_c")], status=THREAD_STATUS_GIL_REQUESTED),
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
MockThreadInfo(3, [MockFrameInfo("c.py", 3, "func_c")], status=THREAD_STATUS_GIL_REQUESTED),
],
)
]
@ -992,7 +994,7 @@ def test_flamegraph_collector_per_thread_stats(self):
MockInterpreterInfo(
0,
[
MockThreadInfo(1, [("a.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
MockThreadInfo(1, [MockFrameInfo("a.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
],
)
]
@ -1012,7 +1014,7 @@ def test_flamegraph_collector_percentage_calculations(self):
MockInterpreterInfo(
0,
[
MockThreadInfo(1, [("a.py", 1, "func")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func")], status=THREAD_STATUS_HAS_GIL),
],
)
]
@ -1023,7 +1025,7 @@ def test_flamegraph_collector_percentage_calculations(self):
MockInterpreterInfo(
0,
[
MockThreadInfo(1, [("a.py", 1, "func")], status=THREAD_STATUS_ON_CPU),
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func")], status=THREAD_STATUS_ON_CPU),
],
)
]
@ -1046,7 +1048,7 @@ def test_flamegraph_collector_mode_handling(self):
MockInterpreterInfo(
0,
[
MockThreadInfo(1, [("a.py", 1, "func")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func")], status=THREAD_STATUS_HAS_GIL),
],
)
]
@ -1085,8 +1087,8 @@ def test_flamegraph_collector_json_structure_includes_stats(self):
MockInterpreterInfo(
0,
[
MockThreadInfo(1, [("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(2, [("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
],
)
]
@ -1142,13 +1144,13 @@ def test_flamegraph_collector_per_thread_gc_percentage(self):
# First 5 samples: both threads, thread 1 has GC in 2
for i in range(5):
has_gc = i < 2 # First 2 samples have GC for thread 1
frames_1 = [("~", 0, "<GC>")] if has_gc else [("a.py", 1, "func_a")]
frames_1 = [MockFrameInfo("~", 0, "<GC>")] if has_gc else [MockFrameInfo("a.py", 1, "func_a")]
stack_frames = [
MockInterpreterInfo(
0,
[
MockThreadInfo(1, frames_1, status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(2, [("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
],
)
]
@ -1162,8 +1164,8 @@ def test_flamegraph_collector_per_thread_gc_percentage(self):
MockInterpreterInfo(
0,
[
MockThreadInfo(1, [("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(2, [("~", 0, "<GC>")], status=THREAD_STATUS_ON_CPU),
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(2, [MockFrameInfo("~", 0, "<GC>")], status=THREAD_STATUS_ON_CPU),
],
)
]
@ -1173,7 +1175,7 @@ def test_flamegraph_collector_per_thread_gc_percentage(self):
MockInterpreterInfo(
0,
[
MockThreadInfo(1, [("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
],
)
]
@ -1201,3 +1203,434 @@ def test_flamegraph_collector_per_thread_gc_percentage(self):
self.assertEqual(collector.per_thread_stats[2]["gc_samples"], 1)
self.assertEqual(collector.per_thread_stats[2]["total"], 6)
self.assertAlmostEqual(per_thread_stats[2]["gc_pct"], 10.0, places=1)
class TestLocationHelpers(unittest.TestCase):
"""Tests for location handling helper functions."""
def test_extract_lineno_from_location_info(self):
"""Test extracting lineno from LocationInfo namedtuple."""
loc = LocationInfo(42, 45, 0, 10)
self.assertEqual(extract_lineno(loc), 42)
def test_extract_lineno_from_tuple(self):
"""Test extracting lineno from plain tuple."""
loc = (100, 105, 5, 20)
self.assertEqual(extract_lineno(loc), 100)
def test_extract_lineno_from_none(self):
"""Test extracting lineno from None (synthetic frames)."""
self.assertEqual(extract_lineno(None), 0)
def test_normalize_location_with_location_info(self):
"""Test normalize_location passes through LocationInfo."""
loc = LocationInfo(10, 15, 0, 5)
result = normalize_location(loc)
self.assertEqual(result, loc)
def test_normalize_location_with_tuple(self):
"""Test normalize_location passes through tuple."""
loc = (10, 15, 0, 5)
result = normalize_location(loc)
self.assertEqual(result, loc)
def test_normalize_location_with_none(self):
"""Test normalize_location returns DEFAULT_LOCATION for None."""
result = normalize_location(None)
self.assertEqual(result, DEFAULT_LOCATION)
self.assertEqual(result, (0, 0, -1, -1))
class TestOpcodeFormatting(unittest.TestCase):
"""Tests for opcode formatting utilities."""
def test_get_opcode_info_standard_opcode(self):
"""Test get_opcode_info for a standard opcode."""
import opcode
# LOAD_CONST is a standard opcode
load_const = opcode.opmap.get('LOAD_CONST')
if load_const is not None:
info = get_opcode_info(load_const)
self.assertEqual(info['opname'], 'LOAD_CONST')
self.assertEqual(info['base_opname'], 'LOAD_CONST')
self.assertFalse(info['is_specialized'])
def test_get_opcode_info_unknown_opcode(self):
"""Test get_opcode_info for an unknown opcode."""
info = get_opcode_info(999)
self.assertEqual(info['opname'], '<999>')
self.assertEqual(info['base_opname'], '<999>')
self.assertFalse(info['is_specialized'])
def test_format_opcode_standard(self):
"""Test format_opcode for a standard opcode."""
import opcode
load_const = opcode.opmap.get('LOAD_CONST')
if load_const is not None:
formatted = format_opcode(load_const)
self.assertEqual(formatted, 'LOAD_CONST')
def test_format_opcode_specialized(self):
"""Test format_opcode for a specialized opcode shows base in parens."""
import opcode
if not hasattr(opcode, '_specialized_opmap'):
self.skipTest("No specialized opcodes in this Python version")
if not hasattr(opcode, '_specializations'):
self.skipTest("No specialization info in this Python version")
# Find any specialized opcode to test
for base_name, variants in opcode._specializations.items():
if not variants:
continue
variant_name = variants[0]
variant_opcode = opcode._specialized_opmap.get(variant_name)
if variant_opcode is None:
continue
formatted = format_opcode(variant_opcode)
# Should show: VARIANT_NAME (BASE_NAME)
self.assertIn(variant_name, formatted)
self.assertIn(f'({base_name})', formatted)
return
self.skipTest("No specialized opcodes found")
def test_format_opcode_unknown(self):
"""Test format_opcode for an unknown opcode."""
formatted = format_opcode(999)
self.assertEqual(formatted, '<999>')
class TestLocationInCollectors(unittest.TestCase):
"""Tests for location tuple handling in each collector."""
def _make_frames_with_location(self, location, opcode=None):
"""Create test frames with a specific location."""
frame = MockFrameInfo("test.py", 0, "test_func", opcode)
# Override the location
frame.location = location
return [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)]
)
]
def test_pstats_collector_with_location_info(self):
"""Test PstatsCollector handles LocationInfo properly."""
collector = PstatsCollector(sample_interval_usec=1000)
# Frame with LocationInfo
frame = MockFrameInfo("test.py", 42, "my_function")
frames = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)]
)
]
collector.collect(frames)
# Should extract lineno from location
key = ("test.py", 42, "my_function")
self.assertIn(key, collector.result)
self.assertEqual(collector.result[key]["direct_calls"], 1)
def test_pstats_collector_with_none_location(self):
"""Test PstatsCollector handles None location (synthetic frames)."""
collector = PstatsCollector(sample_interval_usec=1000)
# Create frame with None location (like GC frame)
frame = MockFrameInfo("~", 0, "<GC>")
frame.location = None # Synthetic frame has no location
frames = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)]
)
]
collector.collect(frames)
# Should use lineno=0 for None location
key = ("~", 0, "<GC>")
self.assertIn(key, collector.result)
def test_collapsed_stack_with_location_info(self):
"""Test CollapsedStackCollector handles LocationInfo properly."""
collector = CollapsedStackCollector(1000)
frame1 = MockFrameInfo("main.py", 10, "main")
frame2 = MockFrameInfo("utils.py", 25, "helper")
frames = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame1, frame2], status=THREAD_STATUS_HAS_GIL)]
)
]
collector.collect(frames)
# Check that linenos were extracted correctly
self.assertEqual(len(collector.stack_counter), 1)
(path, _), count = list(collector.stack_counter.items())[0]
# Reversed order: helper at top, main at bottom
self.assertEqual(path[0], ("utils.py", 25, "helper"))
self.assertEqual(path[1], ("main.py", 10, "main"))
def test_flamegraph_collector_with_location_info(self):
"""Test FlamegraphCollector handles LocationInfo properly."""
collector = FlamegraphCollector(sample_interval_usec=1000)
frame = MockFrameInfo("app.py", 100, "process_data")
frames = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)]
)
]
collector.collect(frames)
data = collector._convert_to_flamegraph_format()
# Verify the function name includes lineno from location
strings = data.get("strings", [])
name_found = any("process_data" in s and "100" in s for s in strings if isinstance(s, str))
self.assertTrue(name_found, f"Expected to find 'process_data' with line 100 in {strings}")
def test_gecko_collector_with_location_info(self):
"""Test GeckoCollector handles LocationInfo properly."""
collector = GeckoCollector(sample_interval_usec=1000)
frame = MockFrameInfo("server.py", 50, "handle_request")
frames = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)]
)
]
collector.collect(frames)
profile = collector._build_profile()
# Check that the function was recorded
self.assertEqual(len(profile["threads"]), 1)
thread_data = profile["threads"][0]
string_array = profile["shared"]["stringArray"]
# Verify function name is in string table
self.assertIn("handle_request", string_array)
class TestOpcodeHandling(unittest.TestCase):
"""Tests for opcode field handling in collectors."""
def test_frame_with_opcode(self):
"""Test MockFrameInfo properly stores opcode."""
frame = MockFrameInfo("test.py", 10, "my_func", opcode=90)
self.assertEqual(frame.opcode, 90)
# Verify tuple representation includes opcode
self.assertEqual(frame[3], 90)
self.assertEqual(len(frame), 4)
def test_frame_without_opcode(self):
"""Test MockFrameInfo with no opcode defaults to None."""
frame = MockFrameInfo("test.py", 10, "my_func")
self.assertIsNone(frame.opcode)
self.assertIsNone(frame[3])
def test_collectors_ignore_opcode_for_key_generation(self):
"""Test that collectors use (filename, lineno, funcname) as key, not opcode."""
collector = PstatsCollector(sample_interval_usec=1000)
# Same function, different opcodes
frame1 = MockFrameInfo("test.py", 10, "func", opcode=90)
frame2 = MockFrameInfo("test.py", 10, "func", opcode=100)
frames1 = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame1], status=THREAD_STATUS_HAS_GIL)]
)
]
frames2 = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame2], status=THREAD_STATUS_HAS_GIL)]
)
]
collector.collect(frames1)
collector.collect(frames2)
# Should be counted as same function (opcode not in key)
key = ("test.py", 10, "func")
self.assertIn(key, collector.result)
self.assertEqual(collector.result[key]["direct_calls"], 2)
class TestGeckoOpcodeMarkers(unittest.TestCase):
"""Tests for GeckoCollector opcode interval markers."""
def test_gecko_collector_opcodes_disabled_by_default(self):
"""Test that opcode tracking is disabled by default."""
collector = GeckoCollector(sample_interval_usec=1000)
self.assertFalse(collector.opcodes_enabled)
def test_gecko_collector_opcodes_enabled(self):
"""Test that opcode tracking can be enabled."""
collector = GeckoCollector(sample_interval_usec=1000, opcodes=True)
self.assertTrue(collector.opcodes_enabled)
def test_gecko_opcode_state_tracking(self):
"""Test that GeckoCollector tracks opcode state changes."""
collector = GeckoCollector(sample_interval_usec=1000, opcodes=True)
# First sample with opcode 90 (RAISE_VARARGS)
frame1 = MockFrameInfo("test.py", 10, "func", opcode=90)
frames1 = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame1], status=THREAD_STATUS_HAS_GIL)]
)
]
collector.collect(frames1)
# Should start tracking this opcode state
self.assertIn(1, collector.opcode_state)
state = collector.opcode_state[1]
self.assertEqual(state[0], 90) # opcode
self.assertEqual(state[1], 10) # lineno
self.assertEqual(state[3], "func") # funcname
def test_gecko_opcode_state_change_emits_marker(self):
"""Test that opcode state change emits an interval marker."""
collector = GeckoCollector(sample_interval_usec=1000, opcodes=True)
# First sample: opcode 90
frame1 = MockFrameInfo("test.py", 10, "func", opcode=90)
frames1 = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame1], status=THREAD_STATUS_HAS_GIL)]
)
]
collector.collect(frames1)
# Second sample: different opcode 100
frame2 = MockFrameInfo("test.py", 10, "func", opcode=100)
frames2 = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame2], status=THREAD_STATUS_HAS_GIL)]
)
]
collector.collect(frames2)
# Should have emitted a marker for the first opcode
thread_data = collector.threads[1]
markers = thread_data["markers"]
# At least one marker should have been added
self.assertGreater(len(markers["name"]), 0)
def test_gecko_opcode_markers_not_emitted_when_disabled(self):
"""Test that no opcode markers when opcodes=False."""
collector = GeckoCollector(sample_interval_usec=1000, opcodes=False)
frame1 = MockFrameInfo("test.py", 10, "func", opcode=90)
frames1 = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame1], status=THREAD_STATUS_HAS_GIL)]
)
]
collector.collect(frames1)
frame2 = MockFrameInfo("test.py", 10, "func", opcode=100)
frames2 = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame2], status=THREAD_STATUS_HAS_GIL)]
)
]
collector.collect(frames2)
# opcode_state should not be tracked
self.assertEqual(len(collector.opcode_state), 0)
def test_gecko_opcode_with_none_opcode(self):
"""Test that None opcode doesn't cause issues."""
collector = GeckoCollector(sample_interval_usec=1000, opcodes=True)
# Frame with no opcode (None)
frame = MockFrameInfo("test.py", 10, "func", opcode=None)
frames = [
MockInterpreterInfo(
0,
[MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)]
)
]
collector.collect(frames)
# Should track the state but opcode is None
self.assertIn(1, collector.opcode_state)
self.assertIsNone(collector.opcode_state[1][0])
class TestCollectorFrameFormat(unittest.TestCase):
"""Tests verifying all collectors handle the 4-element frame format."""
def _make_sample_frames(self):
"""Create sample frames with full format: (filename, location, funcname, opcode)."""
return [
MockInterpreterInfo(
0,
[
MockThreadInfo(
1,
[
MockFrameInfo("app.py", 100, "main", opcode=90),
MockFrameInfo("utils.py", 50, "helper", opcode=100),
MockFrameInfo("lib.py", 25, "process", opcode=None),
],
status=THREAD_STATUS_HAS_GIL,
)
],
)
]
def test_pstats_collector_frame_format(self):
"""Test PstatsCollector with 4-element frame format."""
collector = PstatsCollector(sample_interval_usec=1000)
collector.collect(self._make_sample_frames())
# All three functions should be recorded
self.assertEqual(len(collector.result), 3)
self.assertIn(("app.py", 100, "main"), collector.result)
self.assertIn(("utils.py", 50, "helper"), collector.result)
self.assertIn(("lib.py", 25, "process"), collector.result)
def test_collapsed_stack_frame_format(self):
"""Test CollapsedStackCollector with 4-element frame format."""
collector = CollapsedStackCollector(sample_interval_usec=1000)
collector.collect(self._make_sample_frames())
self.assertEqual(len(collector.stack_counter), 1)
(path, _), _ = list(collector.stack_counter.items())[0]
# 3 frames in the path (reversed order)
self.assertEqual(len(path), 3)
def test_flamegraph_collector_frame_format(self):
"""Test FlamegraphCollector with 4-element frame format."""
collector = FlamegraphCollector(sample_interval_usec=1000)
collector.collect(self._make_sample_frames())
data = collector._convert_to_flamegraph_format()
# Should have processed the frames
self.assertIn("children", data)
def test_gecko_collector_frame_format(self):
"""Test GeckoCollector with 4-element frame format."""
collector = GeckoCollector(sample_interval_usec=1000)
collector.collect(self._make_sample_frames())
profile = collector._build_profile()
# Should have one thread with the frames
self.assertEqual(len(profile["threads"]), 1)
thread = profile["threads"][0]
# Should have recorded 3 functions
self.assertEqual(thread["funcTable"]["length"], 3)

View file

@ -304,10 +304,10 @@ def test_collapsed_stack_with_recursion(self):
MockThreadInfo(
1,
[
("factorial.py", 10, "factorial"),
("factorial.py", 10, "factorial"), # recursive
("factorial.py", 10, "factorial"), # deeper
("main.py", 5, "main"),
MockFrameInfo("factorial.py", 10, "factorial"),
MockFrameInfo("factorial.py", 10, "factorial"), # recursive
MockFrameInfo("factorial.py", 10, "factorial"), # deeper
MockFrameInfo("main.py", 5, "main"),
],
)
],
@ -318,13 +318,9 @@ def test_collapsed_stack_with_recursion(self):
MockThreadInfo(
1,
[
("factorial.py", 10, "factorial"),
(
"factorial.py",
10,
"factorial",
), # different depth
("main.py", 5, "main"),
MockFrameInfo("factorial.py", 10, "factorial"),
MockFrameInfo("factorial.py", 10, "factorial"), # different depth
MockFrameInfo("main.py", 5, "main"),
],
)
],

View file

@ -0,0 +1,5 @@
Add bytecode-level instruction profiling to the sampling profiler via the
new ``--opcodes`` flag. When enabled, the profiler captures which bytecode
opcode is executing at each sample, including Python 3.11+ adaptive
specializations, and visualizes this data in the heatmap, flamegraph, gecko,
and live output formats. Patch by Pablo Galindo

View file

@ -190,6 +190,7 @@ typedef struct {
typedef struct {
PyTypeObject *RemoteDebugging_Type;
PyTypeObject *TaskInfo_Type;
PyTypeObject *LocationInfo_Type;
PyTypeObject *FrameInfo_Type;
PyTypeObject *CoroInfo_Type;
PyTypeObject *ThreadInfo_Type;
@ -228,6 +229,7 @@ typedef struct {
int skip_non_matching_threads;
int native;
int gc;
int opcodes;
int cache_frames;
int collect_stats; // whether to collect statistics
uint32_t stale_invalidation_counter; // counter for throttling frame_cache_invalidate_stale
@ -286,6 +288,7 @@ typedef int (*set_entry_processor_func)(
* ============================================================================ */
extern PyStructSequence_Desc TaskInfo_desc;
extern PyStructSequence_Desc LocationInfo_desc;
extern PyStructSequence_Desc FrameInfo_desc;
extern PyStructSequence_Desc CoroInfo_desc;
extern PyStructSequence_Desc ThreadInfo_desc;
@ -336,11 +339,20 @@ extern int parse_code_object(
int32_t tlbc_index
);
extern PyObject *make_location_info(
RemoteUnwinderObject *unwinder,
int lineno,
int end_lineno,
int col_offset,
int end_col_offset
);
extern PyObject *make_frame_info(
RemoteUnwinderObject *unwinder,
PyObject *file,
PyObject *line,
PyObject *func
PyObject *location, // LocationInfo structseq or None for synthetic frames
PyObject *func,
PyObject *opcode
);
/* Line table parsing */

View file

@ -12,7 +12,8 @@ preserve
PyDoc_STRVAR(_remote_debugging_RemoteUnwinder___init____doc__,
"RemoteUnwinder(pid, *, all_threads=False, only_active_thread=False,\n"
" mode=0, debug=False, skip_non_matching_threads=True,\n"
" native=False, gc=False, cache_frames=False, stats=False)\n"
" native=False, gc=False, opcodes=False,\n"
" cache_frames=False, stats=False)\n"
"--\n"
"\n"
"Initialize a new RemoteUnwinder object for debugging a remote Python process.\n"
@ -32,6 +33,8 @@ PyDoc_STRVAR(_remote_debugging_RemoteUnwinder___init____doc__,
" non-Python code.\n"
" gc: If True, include artificial \"<GC>\" frames to denote active garbage\n"
" collection.\n"
" opcodes: If True, gather bytecode opcode information for instruction-level\n"
" profiling.\n"
" cache_frames: If True, enable frame caching optimization to avoid re-reading\n"
" unchanged parent frames between samples.\n"
" stats: If True, collect statistics about cache hits, memory reads, etc.\n"
@ -53,7 +56,8 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
int mode, int debug,
int skip_non_matching_threads,
int native, int gc,
int cache_frames, int stats);
int opcodes, int cache_frames,
int stats);
static int
_remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObject *kwargs)
@ -61,7 +65,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
int return_value = -1;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
#define NUM_KEYWORDS 10
#define NUM_KEYWORDS 11
static struct {
PyGC_Head _this_is_not_used;
PyObject_VAR_HEAD
@ -70,7 +74,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
} _kwtuple = {
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
.ob_hash = -1,
.ob_item = { &_Py_ID(pid), &_Py_ID(all_threads), &_Py_ID(only_active_thread), &_Py_ID(mode), &_Py_ID(debug), &_Py_ID(skip_non_matching_threads), &_Py_ID(native), &_Py_ID(gc), &_Py_ID(cache_frames), &_Py_ID(stats), },
.ob_item = { &_Py_ID(pid), &_Py_ID(all_threads), &_Py_ID(only_active_thread), &_Py_ID(mode), &_Py_ID(debug), &_Py_ID(skip_non_matching_threads), &_Py_ID(native), &_Py_ID(gc), &_Py_ID(opcodes), &_Py_ID(cache_frames), &_Py_ID(stats), },
};
#undef NUM_KEYWORDS
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
@ -79,14 +83,14 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
# define KWTUPLE NULL
#endif // !Py_BUILD_CORE
static const char * const _keywords[] = {"pid", "all_threads", "only_active_thread", "mode", "debug", "skip_non_matching_threads", "native", "gc", "cache_frames", "stats", NULL};
static const char * const _keywords[] = {"pid", "all_threads", "only_active_thread", "mode", "debug", "skip_non_matching_threads", "native", "gc", "opcodes", "cache_frames", "stats", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "RemoteUnwinder",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[10];
PyObject *argsbuf[11];
PyObject * const *fastargs;
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1;
@ -98,6 +102,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
int skip_non_matching_threads = 1;
int native = 0;
int gc = 0;
int opcodes = 0;
int cache_frames = 0;
int stats = 0;
@ -177,7 +182,16 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
}
}
if (fastargs[8]) {
cache_frames = PyObject_IsTrue(fastargs[8]);
opcodes = PyObject_IsTrue(fastargs[8]);
if (opcodes < 0) {
goto exit;
}
if (!--noptargs) {
goto skip_optional_kwonly;
}
}
if (fastargs[9]) {
cache_frames = PyObject_IsTrue(fastargs[9]);
if (cache_frames < 0) {
goto exit;
}
@ -185,12 +199,12 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
goto skip_optional_kwonly;
}
}
stats = PyObject_IsTrue(fastargs[9]);
stats = PyObject_IsTrue(fastargs[10]);
if (stats < 0) {
goto exit;
}
skip_optional_kwonly:
return_value = _remote_debugging_RemoteUnwinder___init___impl((RemoteUnwinderObject *)self, pid, all_threads, only_active_thread, mode, debug, skip_non_matching_threads, native, gc, cache_frames, stats);
return_value = _remote_debugging_RemoteUnwinder___init___impl((RemoteUnwinderObject *)self, pid, all_threads, only_active_thread, mode, debug, skip_non_matching_threads, native, gc, opcodes, cache_frames, stats);
exit:
return return_value;
@ -419,4 +433,4 @@ _remote_debugging_RemoteUnwinder_get_stats(PyObject *self, PyObject *Py_UNUSED(i
return return_value;
}
/*[clinic end generated code: output=f1fd6c1d4c4c7254 input=a9049054013a1b77]*/
/*[clinic end generated code: output=1943fb7a56197e39 input=a9049054013a1b77]*/

View file

@ -155,48 +155,45 @@ parse_linetable(const uintptr_t addrq, const char* linetable, int firstlineno, L
{
const uint8_t* ptr = (const uint8_t*)(linetable);
uintptr_t addr = 0;
info->lineno = firstlineno;
int computed_line = firstlineno; // Running accumulator, separate from output
while (*ptr != '\0') {
// See InternalDocs/code_objects.md for where these magic numbers are from
// and for the decoding algorithm.
uint8_t first_byte = *(ptr++);
uint8_t code = (first_byte >> 3) & 15;
size_t length = (first_byte & 7) + 1;
uintptr_t end_addr = addr + length;
switch (code) {
case PY_CODE_LOCATION_INFO_NONE: {
case PY_CODE_LOCATION_INFO_NONE:
info->lineno = info->end_lineno = -1;
info->column = info->end_column = -1;
break;
}
case PY_CODE_LOCATION_INFO_LONG: {
int line_delta = scan_signed_varint(&ptr);
info->lineno += line_delta;
info->end_lineno = info->lineno + scan_varint(&ptr);
case PY_CODE_LOCATION_INFO_LONG:
computed_line += scan_signed_varint(&ptr);
info->lineno = computed_line;
info->end_lineno = computed_line + scan_varint(&ptr);
info->column = scan_varint(&ptr) - 1;
info->end_column = scan_varint(&ptr) - 1;
break;
}
case PY_CODE_LOCATION_INFO_NO_COLUMNS: {
int line_delta = scan_signed_varint(&ptr);
info->lineno += line_delta;
case PY_CODE_LOCATION_INFO_NO_COLUMNS:
computed_line += scan_signed_varint(&ptr);
info->lineno = info->end_lineno = computed_line;
info->column = info->end_column = -1;
break;
}
case PY_CODE_LOCATION_INFO_ONE_LINE0:
case PY_CODE_LOCATION_INFO_ONE_LINE1:
case PY_CODE_LOCATION_INFO_ONE_LINE2: {
int line_delta = code - 10;
info->lineno += line_delta;
info->end_lineno = info->lineno;
case PY_CODE_LOCATION_INFO_ONE_LINE2:
computed_line += code - 10;
info->lineno = info->end_lineno = computed_line;
info->column = *(ptr++);
info->end_column = *(ptr++);
break;
}
default: {
uint8_t second_byte = *(ptr++);
if ((second_byte & 128) != 0) {
return false;
}
info->lineno = info->end_lineno = computed_line;
info->column = code << 3 | (second_byte >> 4);
info->end_column = info->column + (second_byte & 15);
break;
@ -215,8 +212,50 @@ parse_linetable(const uintptr_t addrq, const char* linetable, int firstlineno, L
* ============================================================================ */
PyObject *
make_frame_info(RemoteUnwinderObject *unwinder, PyObject *file, PyObject *line,
PyObject *func)
make_location_info(RemoteUnwinderObject *unwinder, int lineno, int end_lineno,
int col_offset, int end_col_offset)
{
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
PyObject *info = PyStructSequence_New(state->LocationInfo_Type);
if (info == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create LocationInfo");
return NULL;
}
PyObject *py_lineno = PyLong_FromLong(lineno);
if (py_lineno == NULL) {
Py_DECREF(info);
return NULL;
}
PyStructSequence_SetItem(info, 0, py_lineno); // steals reference
PyObject *py_end_lineno = PyLong_FromLong(end_lineno);
if (py_end_lineno == NULL) {
Py_DECREF(info);
return NULL;
}
PyStructSequence_SetItem(info, 1, py_end_lineno); // steals reference
PyObject *py_col_offset = PyLong_FromLong(col_offset);
if (py_col_offset == NULL) {
Py_DECREF(info);
return NULL;
}
PyStructSequence_SetItem(info, 2, py_col_offset); // steals reference
PyObject *py_end_col_offset = PyLong_FromLong(end_col_offset);
if (py_end_col_offset == NULL) {
Py_DECREF(info);
return NULL;
}
PyStructSequence_SetItem(info, 3, py_end_col_offset); // steals reference
return info;
}
PyObject *
make_frame_info(RemoteUnwinderObject *unwinder, PyObject *file, PyObject *location,
PyObject *func, PyObject *opcode)
{
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
PyObject *info = PyStructSequence_New(state->FrameInfo_Type);
@ -225,11 +264,13 @@ make_frame_info(RemoteUnwinderObject *unwinder, PyObject *file, PyObject *line,
return NULL;
}
Py_INCREF(file);
Py_INCREF(line);
Py_INCREF(location);
Py_INCREF(func);
Py_INCREF(opcode);
PyStructSequence_SetItem(info, 0, file);
PyStructSequence_SetItem(info, 1, line);
PyStructSequence_SetItem(info, 1, location);
PyStructSequence_SetItem(info, 2, func);
PyStructSequence_SetItem(info, 3, opcode);
return info;
}
@ -370,16 +411,43 @@ parse_code_object(RemoteUnwinderObject *unwinder,
meta->first_lineno, &info);
if (!ok) {
info.lineno = -1;
info.end_lineno = -1;
info.column = -1;
info.end_column = -1;
}
PyObject *lineno = PyLong_FromLong(info.lineno);
if (!lineno) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create line number object");
// Create the LocationInfo structseq: (lineno, end_lineno, col_offset, end_col_offset)
PyObject *location = make_location_info(unwinder,
info.lineno,
info.end_lineno,
info.column,
info.end_column);
if (!location) {
goto error;
}
PyObject *tuple = make_frame_info(unwinder, meta->file_name, lineno, meta->func_name);
Py_DECREF(lineno);
// Read the instruction opcode from target process if opcodes flag is set
PyObject *opcode_obj = NULL;
if (unwinder->opcodes) {
uint16_t instruction_word = 0;
if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, ip,
sizeof(uint16_t), &instruction_word) == 0) {
opcode_obj = PyLong_FromLong(instruction_word & 0xFF);
if (!opcode_obj) {
Py_DECREF(location);
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create opcode object");
goto error;
}
} else {
// Opcode read failed - clear the exception since opcode is optional
PyErr_Clear();
}
}
PyObject *tuple = make_frame_info(unwinder, meta->file_name, location,
meta->func_name, opcode_obj ? opcode_obj : Py_None);
Py_DECREF(location);
Py_XDECREF(opcode_obj);
if (!tuple) {
goto error;
}

View file

@ -337,8 +337,9 @@ process_frame_chain(
extra_frame = &_Py_STR(native);
}
if (extra_frame) {
// Use "~" as file, None as location (synthetic frame), None as opcode
PyObject *extra_frame_info = make_frame_info(
unwinder, _Py_LATIN1_CHR('~'), _PyLong_GetZero(), extra_frame);
unwinder, _Py_LATIN1_CHR('~'), Py_None, extra_frame, Py_None);
if (extra_frame_info == NULL) {
return -1;
}

View file

@ -28,11 +28,28 @@ PyStructSequence_Desc TaskInfo_desc = {
4
};
// LocationInfo structseq type
static PyStructSequence_Field LocationInfo_fields[] = {
{"lineno", "Line number"},
{"end_lineno", "End line number"},
{"col_offset", "Column offset"},
{"end_col_offset", "End column offset"},
{NULL}
};
PyStructSequence_Desc LocationInfo_desc = {
"_remote_debugging.LocationInfo",
"Source location information: (lineno, end_lineno, col_offset, end_col_offset)",
LocationInfo_fields,
4
};
// FrameInfo structseq type
static PyStructSequence_Field FrameInfo_fields[] = {
{"filename", "Source code filename"},
{"lineno", "Line number"},
{"location", "LocationInfo structseq or None for synthetic frames"},
{"funcname", "Function name"},
{"opcode", "Opcode being executed (None if not gathered)"},
{NULL}
};
@ -40,7 +57,7 @@ PyStructSequence_Desc FrameInfo_desc = {
"_remote_debugging.FrameInfo",
"Information about a frame",
FrameInfo_fields,
3
4
};
// CoroInfo structseq type
@ -235,6 +252,7 @@ _remote_debugging.RemoteUnwinder.__init__
skip_non_matching_threads: bool = True
native: bool = False
gc: bool = False
opcodes: bool = False
cache_frames: bool = False
stats: bool = False
@ -255,6 +273,8 @@ Initialize a new RemoteUnwinder object for debugging a remote Python process.
non-Python code.
gc: If True, include artificial "<GC>" frames to denote active garbage
collection.
opcodes: If True, gather bytecode opcode information for instruction-level
profiling.
cache_frames: If True, enable frame caching optimization to avoid re-reading
unchanged parent frames between samples.
stats: If True, collect statistics about cache hits, memory reads, etc.
@ -277,8 +297,9 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
int mode, int debug,
int skip_non_matching_threads,
int native, int gc,
int cache_frames, int stats)
/*[clinic end generated code: output=b34ef8cce013c975 input=df2221ef114c3d6a]*/
int opcodes, int cache_frames,
int stats)
/*[clinic end generated code: output=0031f743f4b9ad52 input=8fb61b24102dec6e]*/
{
// Validate that all_threads and only_active_thread are not both True
if (all_threads && only_active_thread) {
@ -297,6 +318,7 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
self->native = native;
self->gc = gc;
self->opcodes = opcodes;
self->cache_frames = cache_frames;
self->collect_stats = stats;
self->stale_invalidation_counter = 0;
@ -978,6 +1000,14 @@ _remote_debugging_exec(PyObject *m)
return -1;
}
st->LocationInfo_Type = PyStructSequence_NewType(&LocationInfo_desc);
if (st->LocationInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->LocationInfo_Type) < 0) {
return -1;
}
st->FrameInfo_Type = PyStructSequence_NewType(&FrameInfo_desc);
if (st->FrameInfo_Type == NULL) {
return -1;
@ -1051,6 +1081,7 @@ remote_debugging_traverse(PyObject *mod, visitproc visit, void *arg)
RemoteDebuggingState *state = RemoteDebugging_GetState(mod);
Py_VISIT(state->RemoteDebugging_Type);
Py_VISIT(state->TaskInfo_Type);
Py_VISIT(state->LocationInfo_Type);
Py_VISIT(state->FrameInfo_Type);
Py_VISIT(state->CoroInfo_Type);
Py_VISIT(state->ThreadInfo_Type);
@ -1065,6 +1096,7 @@ remote_debugging_clear(PyObject *mod)
RemoteDebuggingState *state = RemoteDebugging_GetState(mod);
Py_CLEAR(state->RemoteDebugging_Type);
Py_CLEAR(state->TaskInfo_Type);
Py_CLEAR(state->LocationInfo_Type);
Py_CLEAR(state->FrameInfo_Type);
Py_CLEAR(state->CoroInfo_Type);
Py_CLEAR(state->ThreadInfo_Type);