**Source code:** :source:`Lib/profiling/sampling/`
+.. program:: profiling.sampling
+
--------------
.. image:: tachyon-logo.png
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
========
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::
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
: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
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)::
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
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
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::
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
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
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
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
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
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
- **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
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
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
-----------------
: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.
=====================
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
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.
-------------------
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
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
------------
--- /dev/null
+/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]
_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));
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)
INIT_ID(only_keys), \
INIT_ID(oparg), \
INIT_ID(opcode), \
+ INIT_ID(opcodes), \
INIT_ID(open), \
INIT_ID(opener), \
INIT_ID(operation), \
_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));
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)
-------------------------------------------------------------------------- */
// 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
// ============================================================================
</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>`;
` : ''}
</div>
${sourceSection}
+ ${opcodeSection}
<div class="tooltip-hint">
${childCount > 0 ? "Click to zoom into this function" : "Leaf function - no children"}
</div>
processedData = resolveStringIndices(EMBEDDED_DATA);
}
+ // Initialize opcode mapping from embedded data
+ initOpcodeMapping(EMBEDDED_DATA);
+
originalData = processedData;
initThreadFilter(processedData);
}
.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 {
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%,
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 */
user-select: none;
font-family: var(--font-sans);
transition: opacity var(--transition-fast);
+ flex-shrink: 0;
}
.toggle-switch:hover {
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 {
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;
.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) {
.legend-content {
flex-direction: column;
+ align-items: center;
gap: 12px;
}
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);
}
// ============================================================================
document.addEventListener('DOMContentLoaded', function() {
- // Restore UI state (theme, etc.)
restoreUIState();
applyLineColors();
// 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);
});
}
});
+// ========================================
+// 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 = '▶'; // Right arrow
+ } else {
+ if (!panel.dataset.populated) {
+ populateBytecodePanel(panel, button);
+ }
+ panel.style.display = 'block';
+ button.classList.add('expanded');
+ button.innerHTML = '▼'; // 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));
class="toolbar-btn theme-toggle"
onclick="toggleTheme()"
title="Toggle theme"
+ aria-label="Toggle theme"
id="theme-btn"
>☾</button>
</div>
<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>
--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;
--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 */
--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;
--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;
}
/* --------------------------------------------------------------------------
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",
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
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)
"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 = []
)
# 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(
async_aware=args.async_mode if args.async_aware else None,
native=args.native,
gc=args.gc,
+ opcodes=args.opcodes,
)
# Handle output
)
# 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:
async_aware=args.async_mode if args.async_aware else None,
native=args.native,
gc=args.gc,
+ opcodes=args.opcodes,
)
# Handle output
limit=20, # Default limit
pid=pid,
mode=mode,
+ opcodes=args.opcodes,
async_aware=args.async_mode if args.async_aware else None,
)
async_aware=args.async_mode if args.async_aware else None,
native=args.native,
gc=args.gc,
+ opcodes=args.opcodes,
)
limit=20, # Default limit
pid=process.pid,
mode=mode,
+ opcodes=args.opcodes,
async_aware=args.async_mode if args.async_aware else None,
)
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
from abc import ABC, abstractmethod
from .constants import (
+ DEFAULT_LOCATION,
THREAD_STATUS_HAS_GIL,
THREAD_STATUS_ON_CPU,
THREAD_STATUS_GIL_REQUESTED,
# 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):
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
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 (
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:
{"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
CATEGORY_GIL = 4
CATEGORY_CPU = 5
CATEGORY_CODE_TYPE = 6
+CATEGORY_OPCODES = 7
# Subcategory indices
DEFAULT_SUBCATEGORY = 0
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)
# 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.
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):
"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:
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
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]
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
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."""
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
"CPUName": "",
"product": "Python",
"symbolicated": True,
- "markerSchema": [],
+ "markerSchema": self._build_marker_schema(),
"importedFrom": "Tachyon Sampling Profiler",
"extensions": {
"id": [],
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
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()
"""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."""
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
(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)
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">▶</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)
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:
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,
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
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:
pid=None,
display=None,
mode=None,
+ opcodes=False,
async_aware=None,
):
"""
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(
}
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
self.table_widget = None
self.footer_widget = None
self.help_widget = None
+ self.opcode_panel = None
# Color mode
self._can_colorize = _colorize.can_colorize()
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)
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
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
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")
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 == 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("j") or ch == ord("J"):
+ # Move selection down in opcode mode (with scrolling)
+ self._move_selection_down()
+
+ 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)
# 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
MIN_SAMPLE_RATE_FOR_SCALING,
FOOTER_LINES,
FINISHED_BANNER_EXTRA_LINES,
+ OPCODE_PANEL_HEIGHT,
)
from ..constants import (
THREAD_STATUS_HAS_GIL,
# 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"]
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)
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
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
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
(" 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),
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
--- /dev/null
+"""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}
import marshal
from _colorize import ANSIColors
-from .collector import Collector
+from .collector import Collector, extract_lineno
class PstatsCollector(Collector):
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
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
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
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)
async_aware=None,
native=False,
gc=True,
+ opcodes=False,
):
"""Sample a process using the provided collector.
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
mode=mode,
native=native,
gc=gc,
+ opcodes=opcodes,
skip_non_matching_threads=skip_non_matching_threads,
collect_stats=realtime_stats,
)
async_aware=None,
native=False,
gc=True,
+ opcodes=False,
):
"""Sample a process in live/interactive mode with curses TUI.
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
mode=mode,
native=native,
gc=gc,
+ opcodes=opcodes,
skip_non_matching_threads=skip_non_matching_threads,
collect_stats=realtime_stats,
)
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
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):
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
**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]
}
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 {
"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):
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
"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"
},
)
- # 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(
{
"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
),
)
(
"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),
),
)
],
(
"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),
),
)
],
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),
)
],
)
},
)
- # 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
{
"Task-1": [],
"Task-2": [
- ("Task-1", (tuple([script_name, 21, "main"]),))
+ ("Task-1", ((script_name, 21, "main", None),))
],
},
)
},
)
- # 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(
{
"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
(
"Task-1",
(
- tuple(
- [
- staggered.__file__,
- ANY,
- "staggered_race",
- ]
- ),
- tuple([script_name, 21, "main"]),
+ (staggered.__file__, ANY, "staggered_race", None),
+ (script_name, 21, "main", None),
),
)
],
# 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(
base_events.__file__,
ANY,
"Server.serve_forever",
+ ANY,
]
)
],
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
+ ANY,
]
),
FrameInfo(
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
+ ANY,
]
),
FrameInfo(
- [script_name, ANY, "main"]
+ [script_name, ANY, "main", ANY]
),
],
ANY,
tasks.__file__,
ANY,
"sleep",
+ ANY,
]
),
FrameInfo(
[
script_name,
- 36,
+ ANY,
"echo_client",
+ ANY,
]
),
],
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
+ ANY,
]
),
FrameInfo(
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
+ ANY,
]
),
FrameInfo(
[
script_name,
- 39,
+ ANY,
"echo_client_spam",
+ ANY,
]
),
],
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)
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(
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
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(
# 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
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(
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,
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)
"""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
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)
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)
"""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)
# 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:
# 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)
"""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)
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
"""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)
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')
"""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')
"""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')
"""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
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')
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')
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')
class MockFrameInfo:
- """Mock FrameInfo for testing since the real one isn't accessible."""
+ """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):
+ 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})"
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")],
)]
)
]
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)
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()
"""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,
)
+# 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."""
+ """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):
+ 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:
"""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.
+
+ 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):
+ 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:
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,
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
# 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, "函数名")
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):
test_frames = [
MockInterpreterInfo(
0,
- [MockThreadInfo(None, [MockFrameInfo("file.py", 10, "func")])],
+ [MockThreadInfo(None, [MockFrameInfo("file.py", 10, "func", None)])],
)
]
collector.collect(test_frames)
# 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)
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)
0,
[
MockThreadInfo(
- 1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
+ 1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
0,
[
MockThreadInfo(
- 1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
+ 1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
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")])]
)
]
0,
[
MockThreadInfo(
- 1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
+ 1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
0,
[
MockThreadInfo(
- 1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
+ 1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
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")])]
)
]
[
MockThreadInfo(
1,
- [("file.py", 10, "func1"), ("file.py", 20, "func2")],
+ [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")],
)
],
)
0,
[
MockThreadInfo(
- 1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
+ 1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
)
],
)
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")])]
)
]
[
MockThreadInfo(
1,
- [("test.py", 10, "python_func")],
+ [MockFrameInfo("test.py", 10, "python_func")],
status=HAS_GIL_ON_CPU,
)
],
[
MockThreadInfo(
1,
- [("test.py", 15, "wait_func")],
+ [MockFrameInfo("test.py", 15, "wait_func")],
status=WAITING_FOR_GIL,
)
],
[
MockThreadInfo(
1,
- [("test.py", 20, "python_func2")],
+ [MockFrameInfo("test.py", 20, "python_func2")],
status=HAS_GIL_ON_CPU,
)
],
[
MockThreadInfo(
1,
- [("native.c", 100, "native_func")],
+ [MockFrameInfo("native.c", 100, "native_func")],
status=NO_GIL_ON_CPU,
)
],
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),
],
)
]
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),
],
)
]
MockInterpreterInfo(
0,
[
- MockThreadInfo(1, [("~", 0, "<GC>")], status=THREAD_STATUS_HAS_GIL),
+ MockThreadInfo(1, [MockFrameInfo("~", 0, "<GC>")], status=THREAD_STATUS_HAS_GIL),
],
)
]
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),
],
)
]
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),
],
)
]
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),
],
)
]
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),
],
)
]
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),
],
)
]
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),
],
)
]
# 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),
],
)
]
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),
],
)
]
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),
],
)
]
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)
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"),
],
)
],
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"),
],
)
],
--- /dev/null
+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
typedef struct {
PyTypeObject *RemoteDebugging_Type;
PyTypeObject *TaskInfo_Type;
+ PyTypeObject *LocationInfo_Type;
PyTypeObject *FrameInfo_Type;
PyTypeObject *CoroInfo_Type;
PyTypeObject *ThreadInfo_Type;
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
* ============================================================================ */
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;
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 */
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"
" 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"
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)
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
} _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)
# 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;
int skip_non_matching_threads = 1;
int native = 0;
int gc = 0;
+ int opcodes = 0;
int cache_frames = 0;
int stats = 0;
}
}
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;
}
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;
return return_value;
}
-/*[clinic end generated code: output=f1fd6c1d4c4c7254 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=1943fb7a56197e39 input=a9049054013a1b77]*/
{
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;
* ============================================================================ */
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);
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;
}
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;
}
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;
}
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}
};
"_remote_debugging.FrameInfo",
"Information about a frame",
FrameInfo_fields,
- 3
+ 4
};
// CoroInfo structseq type
skip_non_matching_threads: bool = True
native: bool = False
gc: bool = False
+ opcodes: bool = False
cache_frames: bool = False
stats: bool = False
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.
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) {
self->native = native;
self->gc = gc;
+ self->opcodes = opcodes;
self->cache_frames = cache_frames;
self->collect_stats = stats;
self->stale_invalidation_counter = 0;
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;
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);
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);