mirror of
https://github.com/python/cpython.git
synced 2026-05-29 22:01:05 +00:00
gh-138122: Make the tachyon profiler opcode-aware (#142394)
This commit is contained in:
parent
fa448451ab
commit
5b19c75b47
36 changed files with 3983 additions and 507 deletions
|
|
@ -13,6 +13,8 @@
|
|||
|
||||
**Source code:** :source:`Lib/profiling/sampling/`
|
||||
|
||||
.. program:: profiling.sampling
|
||||
|
||||
--------------
|
||||
|
||||
.. image:: tachyon-logo.png
|
||||
|
|
@ -146,6 +148,10 @@ Generate a line-by-line heatmap::
|
|||
|
||||
python -m profiling.sampling run --heatmap script.py
|
||||
|
||||
Enable opcode-level profiling to see which bytecode instructions are executing::
|
||||
|
||||
python -m profiling.sampling run --opcodes --flamegraph script.py
|
||||
|
||||
|
||||
Commands
|
||||
========
|
||||
|
|
@ -308,7 +314,7 @@ The two most fundamental parameters are the sampling interval and duration.
|
|||
Together, these determine how many samples will be collected during a profiling
|
||||
session.
|
||||
|
||||
The ``--interval`` option (``-i``) sets the time between samples in
|
||||
The :option:`--interval` option (:option:`-i`) sets the time between samples in
|
||||
microseconds. The default is 100 microseconds, which produces approximately
|
||||
10,000 samples per second::
|
||||
|
||||
|
|
@ -319,7 +325,7 @@ cost of slightly higher profiler CPU usage. Higher intervals reduce profiler
|
|||
overhead but may miss short-lived functions. For most applications, the
|
||||
default interval provides a good balance between accuracy and overhead.
|
||||
|
||||
The ``--duration`` option (``-d``) sets how long to profile in seconds. The
|
||||
The :option:`--duration` option (:option:`-d`) sets how long to profile in seconds. The
|
||||
default is 10 seconds::
|
||||
|
||||
python -m profiling.sampling run -d 60 script.py
|
||||
|
|
@ -337,8 +343,8 @@ Python programs often use multiple threads, whether explicitly through the
|
|||
:mod:`threading` module or implicitly through libraries that manage thread
|
||||
pools.
|
||||
|
||||
By default, the profiler samples only the main thread. The ``--all-threads``
|
||||
option (``-a``) enables sampling of all threads in the process::
|
||||
By default, the profiler samples only the main thread. The :option:`--all-threads`
|
||||
option (:option:`-a`) enables sampling of all threads in the process::
|
||||
|
||||
python -m profiling.sampling run -a script.py
|
||||
|
||||
|
|
@ -357,7 +363,7 @@ additional context about what the interpreter is doing at the moment each
|
|||
sample is taken. These synthetic frames help distinguish different types of
|
||||
execution that would otherwise be invisible.
|
||||
|
||||
The ``--native`` option adds ``<native>`` frames to indicate when Python has
|
||||
The :option:`--native` option adds ``<native>`` frames to indicate when Python has
|
||||
called into C code (extension modules, built-in functions, or the interpreter
|
||||
itself)::
|
||||
|
||||
|
|
@ -369,7 +375,7 @@ in the Python function that made the call. This is useful when optimizing
|
|||
code that makes heavy use of C extensions like NumPy or database drivers.
|
||||
|
||||
By default, the profiler includes ``<GC>`` frames when garbage collection is
|
||||
active. The ``--no-gc`` option suppresses these frames::
|
||||
active. The :option:`--no-gc` option suppresses these frames::
|
||||
|
||||
python -m profiling.sampling run --no-gc script.py
|
||||
|
||||
|
|
@ -379,10 +385,48 @@ see substantial time in ``<GC>`` frames, consider investigating object
|
|||
allocation rates or using object pooling.
|
||||
|
||||
|
||||
Opcode-aware profiling
|
||||
----------------------
|
||||
|
||||
The :option:`--opcodes` option enables instruction-level profiling that captures
|
||||
which Python bytecode instructions are executing at each sample::
|
||||
|
||||
python -m profiling.sampling run --opcodes --flamegraph script.py
|
||||
|
||||
This feature provides visibility into Python's bytecode execution, including
|
||||
adaptive specialization optimizations. When a generic instruction like
|
||||
``LOAD_ATTR`` is specialized at runtime into a more efficient variant like
|
||||
``LOAD_ATTR_INSTANCE_VALUE``, the profiler shows both the specialized name
|
||||
and the base instruction.
|
||||
|
||||
Opcode information appears in several output formats:
|
||||
|
||||
- **Live mode**: An opcode panel shows instruction-level statistics for the
|
||||
selected function, accessible via keyboard navigation
|
||||
- **Flame graphs**: Nodes display opcode information when available, helping
|
||||
identify which instructions consume the most time
|
||||
- **Heatmap**: Expandable bytecode panels per source line show instruction
|
||||
breakdown with specialization percentages
|
||||
- **Gecko format**: Opcode transitions are emitted as interval markers in the
|
||||
Firefox Profiler timeline
|
||||
|
||||
This level of detail is particularly useful for:
|
||||
|
||||
- Understanding the performance impact of Python's adaptive specialization
|
||||
- Identifying hot bytecode instructions that might benefit from optimization
|
||||
- Analyzing the effectiveness of different code patterns at the instruction level
|
||||
- Debugging performance issues that occur at the bytecode level
|
||||
|
||||
The :option:`--opcodes` option is compatible with :option:`--live`, :option:`--flamegraph`,
|
||||
:option:`--heatmap`, and :option:`--gecko` formats. It requires additional memory to store
|
||||
opcode information and may slightly reduce sampling performance, but provides
|
||||
unprecedented visibility into Python's execution model.
|
||||
|
||||
|
||||
Real-time statistics
|
||||
--------------------
|
||||
|
||||
The ``--realtime-stats`` option displays sampling rate statistics during
|
||||
The :option:`--realtime-stats` option displays sampling rate statistics during
|
||||
profiling::
|
||||
|
||||
python -m profiling.sampling run --realtime-stats script.py
|
||||
|
|
@ -434,7 +478,7 @@ CPU execution time, or time spent holding the global interpreter lock.
|
|||
Wall-clock mode
|
||||
---------------
|
||||
|
||||
Wall-clock mode (``--mode=wall``) captures all samples regardless of what the
|
||||
Wall-clock mode (:option:`--mode`\ ``=wall``) captures all samples regardless of what the
|
||||
thread is doing. This is the default mode and provides a complete picture of
|
||||
where time passes during program execution::
|
||||
|
||||
|
|
@ -454,7 +498,7 @@ latency.
|
|||
CPU mode
|
||||
--------
|
||||
|
||||
CPU mode (``--mode=cpu``) records samples only when the thread is actually
|
||||
CPU mode (:option:`--mode`\ ``=cpu``) records samples only when the thread is actually
|
||||
executing on a CPU core::
|
||||
|
||||
python -m profiling.sampling run --mode=cpu script.py
|
||||
|
|
@ -488,7 +532,7 @@ connection pooling, or reducing wait time instead.
|
|||
GIL mode
|
||||
--------
|
||||
|
||||
GIL mode (``--mode=gil``) records samples only when the thread holds Python's
|
||||
GIL mode (:option:`--mode`\ ``=gil``) records samples only when the thread holds Python's
|
||||
global interpreter lock::
|
||||
|
||||
python -m profiling.sampling run --mode=gil script.py
|
||||
|
|
@ -520,7 +564,7 @@ output goes to stdout, a file, or a directory depending on the format.
|
|||
pstats format
|
||||
-------------
|
||||
|
||||
The pstats format (``--pstats``) produces a text table similar to what
|
||||
The pstats format (:option:`--pstats`) produces a text table similar to what
|
||||
deterministic profilers generate. This is the default output format::
|
||||
|
||||
python -m profiling.sampling run script.py
|
||||
|
|
@ -567,31 +611,31 @@ interesting functions that highlights:
|
|||
samples (high cumulative/direct multiplier). These are frequently-nested
|
||||
functions that appear deep in many call chains.
|
||||
|
||||
Use ``--no-summary`` to suppress both the legend and summary sections.
|
||||
Use :option:`--no-summary` to suppress both the legend and summary sections.
|
||||
|
||||
To save pstats output to a file instead of stdout::
|
||||
|
||||
python -m profiling.sampling run -o profile.txt script.py
|
||||
|
||||
The pstats format supports several options for controlling the display.
|
||||
The ``--sort`` option determines the column used for ordering results::
|
||||
The :option:`--sort` option determines the column used for ordering results::
|
||||
|
||||
python -m profiling.sampling run --sort=tottime script.py
|
||||
python -m profiling.sampling run --sort=cumtime script.py
|
||||
python -m profiling.sampling run --sort=nsamples script.py
|
||||
|
||||
The ``--limit`` option restricts output to the top N entries::
|
||||
The :option:`--limit` option restricts output to the top N entries::
|
||||
|
||||
python -m profiling.sampling run --limit=30 script.py
|
||||
|
||||
The ``--no-summary`` option suppresses the header summary that precedes the
|
||||
The :option:`--no-summary` option suppresses the header summary that precedes the
|
||||
statistics table.
|
||||
|
||||
|
||||
Collapsed stacks format
|
||||
-----------------------
|
||||
|
||||
Collapsed stacks format (``--collapsed``) produces one line per unique call
|
||||
Collapsed stacks format (:option:`--collapsed`) produces one line per unique call
|
||||
stack, with a count of how many times that stack was sampled::
|
||||
|
||||
python -m profiling.sampling run --collapsed script.py
|
||||
|
|
@ -621,7 +665,7 @@ visualization where you can click to zoom into specific call paths.
|
|||
Flame graph format
|
||||
------------------
|
||||
|
||||
Flame graph format (``--flamegraph``) produces a self-contained HTML file with
|
||||
Flame graph format (:option:`--flamegraph`) produces a self-contained HTML file with
|
||||
an interactive flame graph visualization::
|
||||
|
||||
python -m profiling.sampling run --flamegraph script.py
|
||||
|
|
@ -667,7 +711,7 @@ or through their callees.
|
|||
Gecko format
|
||||
------------
|
||||
|
||||
Gecko format (``--gecko``) produces JSON output compatible with the Firefox
|
||||
Gecko format (:option:`--gecko`) produces JSON output compatible with the Firefox
|
||||
Profiler::
|
||||
|
||||
python -m profiling.sampling run --gecko script.py
|
||||
|
|
@ -694,14 +738,14 @@ Firefox Profiler timeline:
|
|||
- **Code type markers**: distinguish Python code from native (C extension) code
|
||||
- **GC markers**: indicate garbage collection activity
|
||||
|
||||
For this reason, the ``--mode`` option is not available with Gecko format;
|
||||
For this reason, the :option:`--mode` option is not available with Gecko format;
|
||||
all relevant data is captured automatically.
|
||||
|
||||
|
||||
Heatmap format
|
||||
--------------
|
||||
|
||||
Heatmap format (``--heatmap``) generates an interactive HTML visualization
|
||||
Heatmap format (:option:`--heatmap`) generates an interactive HTML visualization
|
||||
showing sample counts at the source line level::
|
||||
|
||||
python -m profiling.sampling run --heatmap script.py
|
||||
|
|
@ -744,7 +788,7 @@ interpretation of hierarchical visualizations.
|
|||
Live mode
|
||||
=========
|
||||
|
||||
Live mode (``--live``) provides a terminal-based real-time view of profiling
|
||||
Live mode (:option:`--live`) provides a terminal-based real-time view of profiling
|
||||
data, similar to the ``top`` command for system processes::
|
||||
|
||||
python -m profiling.sampling run --live script.py
|
||||
|
|
@ -760,6 +804,11 @@ and thread status statistics (GIL held percentage, CPU usage, GC time). The
|
|||
main table shows function statistics with the currently sorted column indicated
|
||||
by an arrow (▼).
|
||||
|
||||
When :option:`--opcodes` is enabled, an additional opcode panel appears below the
|
||||
main table, showing instruction-level statistics for the currently selected
|
||||
function. This panel displays which bytecode instructions are executing most
|
||||
frequently, including specialized variants and their base opcodes.
|
||||
|
||||
|
||||
Keyboard commands
|
||||
-----------------
|
||||
|
|
@ -813,12 +862,17 @@ Within live mode, keyboard commands control the display:
|
|||
:kbd:`h` or :kbd:`?`
|
||||
Show the help screen with all available commands.
|
||||
|
||||
:kbd:`j` / :kbd:`k` (or :kbd:`Up` / :kbd:`Down`)
|
||||
Navigate through opcode entries in the opcode panel (when ``--opcodes`` is
|
||||
enabled). These keys scroll through the instruction-level statistics for the
|
||||
currently selected function.
|
||||
|
||||
When profiling finishes (duration expires or target process exits), the display
|
||||
shows a "PROFILING COMPLETE" banner and freezes the final results. You can
|
||||
still navigate, sort, and filter the results before pressing :kbd:`q` to exit.
|
||||
|
||||
Live mode is incompatible with output format options (``--collapsed``,
|
||||
``--flamegraph``, and so on) because it uses an interactive terminal
|
||||
Live mode is incompatible with output format options (:option:`--collapsed`,
|
||||
:option:`--flamegraph`, and so on) because it uses an interactive terminal
|
||||
interface rather than producing file output.
|
||||
|
||||
|
||||
|
|
@ -826,7 +880,7 @@ Async-aware profiling
|
|||
=====================
|
||||
|
||||
For programs using :mod:`asyncio`, the profiler offers async-aware mode
|
||||
(``--async-aware``) that reconstructs call stacks based on the task structure
|
||||
(:option:`--async-aware`) that reconstructs call stacks based on the task structure
|
||||
rather than the raw Python frames::
|
||||
|
||||
python -m profiling.sampling run --async-aware async_script.py
|
||||
|
|
@ -846,16 +900,16 @@ and presenting stacks that reflect the ``await`` chain.
|
|||
Async modes
|
||||
-----------
|
||||
|
||||
The ``--async-mode`` option controls which tasks appear in the profile::
|
||||
The :option:`--async-mode` option controls which tasks appear in the profile::
|
||||
|
||||
python -m profiling.sampling run --async-aware --async-mode=running async_script.py
|
||||
python -m profiling.sampling run --async-aware --async-mode=all async_script.py
|
||||
|
||||
With ``--async-mode=running`` (the default), only the task currently executing
|
||||
With :option:`--async-mode`\ ``=running`` (the default), only the task currently executing
|
||||
on the CPU is profiled. This shows where your program is actively spending time
|
||||
and is the typical choice for performance analysis.
|
||||
|
||||
With ``--async-mode=all``, tasks that are suspended (awaiting I/O, locks, or
|
||||
With :option:`--async-mode`\ ``=all``, tasks that are suspended (awaiting I/O, locks, or
|
||||
other tasks) are also included. This mode is useful for understanding what your
|
||||
program is waiting on, but produces larger profiles since every suspended task
|
||||
appears in each sample.
|
||||
|
|
@ -884,8 +938,8 @@ Option restrictions
|
|||
-------------------
|
||||
|
||||
Async-aware mode uses a different stack reconstruction mechanism and is
|
||||
incompatible with: ``--native``, ``--no-gc``, ``--all-threads``, and
|
||||
``--mode=cpu`` or ``--mode=gil``.
|
||||
incompatible with: :option:`--native`, :option:`--no-gc`, :option:`--all-threads`, and
|
||||
:option:`--mode`\ ``=cpu`` or :option:`--mode`\ ``=gil``.
|
||||
|
||||
|
||||
Command-line interface
|
||||
|
|
@ -939,6 +993,13 @@ Sampling options
|
|||
|
||||
Enable async-aware profiling for asyncio programs.
|
||||
|
||||
.. option:: --opcodes
|
||||
|
||||
Gather bytecode opcode information for instruction-level profiling. Shows
|
||||
which bytecode instructions are executing, including specializations.
|
||||
Compatible with ``--live``, ``--flamegraph``, ``--heatmap``, and ``--gecko``
|
||||
formats only.
|
||||
|
||||
|
||||
Mode options
|
||||
------------
|
||||
|
|
|
|||
785
Doc/sphinx-warnings.txt
Normal file
785
Doc/sphinx-warnings.txt
Normal file
|
|
@ -0,0 +1,785 @@
|
|||
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:1243: WARNING: c:macro reference target not found: Py_TPFLAGS_HAVE_STACKLESS_EXTENSION [ref.macro]
|
||||
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:3008: WARNING: c:identifier reference target not found: view [ref.identifier]
|
||||
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:3015: WARNING: c:identifier reference target not found: view [ref.identifier]
|
||||
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:3015: WARNING: c:identifier reference target not found: view [ref.identifier]
|
||||
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:3022: WARNING: c:identifier reference target not found: view [ref.identifier]
|
||||
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:3025: WARNING: c:identifier reference target not found: view [ref.identifier]
|
||||
/home/pablogsal/github/python/main/Doc/c-api/typeobj.rst:3070: WARNING: c:identifier reference target not found: view [ref.identifier]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:16: WARNING: py:mod reference target not found: xml.etree [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:16: WARNING: py:mod reference target not found: sqlite [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:137: WARNING: py:func reference target not found: partial [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:144: WARNING: py:func reference target not found: partial [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:160: WARNING: py:meth reference target not found: open_item [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:173: WARNING: py:func reference target not found: update_wrapper [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:186: WARNING: py:func reference target not found: wraps [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:212: WARNING: py:func reference target not found: setup [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:274: WARNING: py:mod reference target not found: pkg [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:274: WARNING: py:mod reference target not found: pkg.main [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:274: WARNING: py:mod reference target not found: pkg.string [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:277: WARNING: py:mod reference target not found: pkg.string [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:277: WARNING: py:mod reference target not found: pkg.main [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:284: WARNING: py:mod reference target not found: pkg.string [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:284: WARNING: py:mod reference target not found: pkg.string [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:284: WARNING: py:mod reference target not found: py.std [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:292: WARNING: py:mod reference target not found: pkg.string [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:315: WARNING: py:mod reference target not found: pkg.main [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:315: WARNING: py:mod reference target not found: pkg.string [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:315: WARNING: py:mod reference target not found: A.B.C [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:334: WARNING: py:mod reference target not found: py.std [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:349: WARNING: py:mod reference target not found: pychecker.checker [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:393: WARNING: py:class reference target not found: Exception1 [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:393: WARNING: py:class reference target not found: Exception2 [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:493: WARNING: py:meth reference target not found: send [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:498: WARNING: py:meth reference target not found: send [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:504: WARNING: py:meth reference target not found: close [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:504: WARNING: py:meth reference target not found: close [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:524: WARNING: py:meth reference target not found: close [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:524: WARNING: py:meth reference target not found: close [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:536: WARNING: py:attr reference target not found: gi_frame [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:536: WARNING: py:attr reference target not found: gi_frame [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:625: WARNING: py:func reference target not found: localcontext [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:693: WARNING: py:class reference target not found: DatabaseConnection [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:748: WARNING: py:func reference target not found: contextmanager [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:910: WARNING: c:macro reference target not found: PY_SSIZE_T_CLEAN [ref.macro]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:933: WARNING: py:meth reference target not found: __index__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:941: WARNING: py:meth reference target not found: __int__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:941: WARNING: py:meth reference target not found: __int__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:946: WARNING: py:meth reference target not found: __index__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:957: WARNING: py:meth reference target not found: __index__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:978: WARNING: py:class reference target not found: defaultdict [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1020: WARNING: py:meth reference target not found: startswith [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1020: WARNING: py:meth reference target not found: endswith [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1030: WARNING: py:meth reference target not found: sort [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1052: WARNING: py:meth reference target not found: __hash__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1052: WARNING: py:meth reference target not found: __hash__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1168: WARNING: py:meth reference target not found: read [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1168: WARNING: py:meth reference target not found: readlines [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1206: WARNING: c:func reference target not found: open [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:func reference target not found: codec.lookup [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:class reference target not found: CodecInfo [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:class reference target not found: CodecInfo [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:attr reference target not found: encode [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:attr reference target not found: decode [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:attr reference target not found: incrementalencoder [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:attr reference target not found: incrementaldecoder [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:attr reference target not found: streamwriter [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1228: WARNING: py:attr reference target not found: streamreader [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1240: WARNING: py:class reference target not found: defaultdict [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1245: WARNING: py:class reference target not found: defaultdict [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1271: WARNING: py:class reference target not found: deque [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1288: WARNING: py:class reference target not found: Stats [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1292: WARNING: py:class reference target not found: reader [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1292: WARNING: py:attr reference target not found: line_num [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1292: WARNING: py:attr reference target not found: line_num [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1321: WARNING: py:meth reference target not found: SequenceMatcher.get_matching_blocks [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1330: WARNING: py:func reference target not found: testfile [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1330: WARNING: py:class reference target not found: DocFileSuite [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1345: WARNING: py:class reference target not found: FileInput [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1354: WARNING: py:func reference target not found: get_count [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1361: WARNING: py:func reference target not found: nsmallest [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1361: WARNING: py:func reference target not found: nlargest [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1361: WARNING: py:meth reference target not found: sort [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1384: WARNING: py:func reference target not found: format_string [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1384: WARNING: py:func reference target not found: currency [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1393: WARNING: py:func reference target not found: format_string [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1397: WARNING: py:func reference target not found: currency [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1404: WARNING: py:class reference target not found: mbox [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1404: WARNING: py:class reference target not found: MH [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1404: WARNING: py:class reference target not found: Maildir [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1404: WARNING: py:meth reference target not found: lock [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1404: WARNING: py:meth reference target not found: unlock [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1433: WARNING: py:func reference target not found: itemgetter [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1433: WARNING: py:func reference target not found: attrgetter [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1433: WARNING: py:attr reference target not found: a [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1433: WARNING: py:attr reference target not found: b [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1433: WARNING: py:meth reference target not found: sort [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1440: WARNING: py:class reference target not found: OptionParser [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1440: WARNING: py:attr reference target not found: epilog [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1440: WARNING: py:meth reference target not found: destroy [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1445: WARNING: py:attr reference target not found: stat_float_times [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1456: WARNING: py:func reference target not found: wait3 [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1456: WARNING: py:func reference target not found: wait4 [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1456: WARNING: py:func reference target not found: waitpid [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1456: WARNING: py:func reference target not found: wait3 [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1456: WARNING: py:func reference target not found: wait4 [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1456: WARNING: py:func reference target not found: wait3 [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1465: WARNING: py:attr reference target not found: st_gen [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1465: WARNING: py:attr reference target not found: st_birthtime [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1465: WARNING: py:attr reference target not found: st_flags [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1498: WARNING: py:mod reference target not found: pyexpat [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1501: WARNING: py:mod reference target not found: Queue [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1501: WARNING: py:meth reference target not found: join [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1501: WARNING: py:meth reference target not found: task_done [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1507: WARNING: py:mod reference target not found: regex [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1507: WARNING: py:mod reference target not found: regsub [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1507: WARNING: py:mod reference target not found: statcache [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1507: WARNING: py:mod reference target not found: tzparse [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1507: WARNING: py:mod reference target not found: whrandom [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1511: WARNING: py:mod reference target not found: dircmp [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1511: WARNING: py:mod reference target not found: ni [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1522: WARNING: py:attr reference target not found: rpc_paths [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1522: WARNING: py:attr reference target not found: rpc_paths [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1529: WARNING: py:const reference target not found: AF_NETLINK [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1541: WARNING: py:meth reference target not found: getfamily [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1541: WARNING: py:meth reference target not found: gettype [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1541: WARNING: py:meth reference target not found: getproto [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:class reference target not found: Struct [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:meth reference target not found: pack [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:meth reference target not found: unpack [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:func reference target not found: pack [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:func reference target not found: unpack [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:class reference target not found: Struct [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1548: WARNING: py:class reference target not found: Struct [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1565: WARNING: py:class reference target not found: Struct [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1585: WARNING: py:class reference target not found: TarFile [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1585: WARNING: py:meth reference target not found: extractall [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1607: WARNING: py:class reference target not found: UUID [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1607: WARNING: py:func reference target not found: uuid1 [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1607: WARNING: py:func reference target not found: uuid3 [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1607: WARNING: py:func reference target not found: uuid4 [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1607: WARNING: py:func reference target not found: uuid5 [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:class reference target not found: WeakKeyDictionary [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:class reference target not found: WeakValueDictionary [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:meth reference target not found: iterkeyrefs [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:meth reference target not found: keyrefs [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:class reference target not found: WeakKeyDictionary [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:meth reference target not found: itervaluerefs [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:meth reference target not found: valuerefs [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1634: WARNING: py:class reference target not found: WeakValueDictionary [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1641: WARNING: py:func reference target not found: open_new [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1641: WARNING: py:func reference target not found: open_new_tab [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1666: WARNING: py:class reference target not found: Compress [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1666: WARNING: py:class reference target not found: Decompress [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1666: WARNING: py:class reference target not found: Compress [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1666: WARNING: py:class reference target not found: Decompress [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1687: WARNING: py:class reference target not found: CDLL [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1687: WARNING: py:class reference target not found: CDLL [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1697: WARNING: py:func reference target not found: c_int [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1697: WARNING: py:func reference target not found: c_float [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1697: WARNING: py:func reference target not found: c_double [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1697: WARNING: py:func reference target not found: c_char_p [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1697: WARNING: py:attr reference target not found: value [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1706: WARNING: py:func reference target not found: c_char_p [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1706: WARNING: py:func reference target not found: create_string_buffer [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1715: WARNING: py:attr reference target not found: restype [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1762: WARNING: py:mod reference target not found: xml.etree [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1762: WARNING: py:mod reference target not found: ElementTree [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1762: WARNING: py:mod reference target not found: ElementPath [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1762: WARNING: py:mod reference target not found: ElementInclude [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1762: WARNING: py:mod reference target not found: cElementTree [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1772: WARNING: py:attr reference target not found: text [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1772: WARNING: py:attr reference target not found: tail [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1772: WARNING: py:class reference target not found: TextNode [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1778: WARNING: py:func reference target not found: parse [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1778: WARNING: py:class reference target not found: ElementTree [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1790: WARNING: py:class reference target not found: ElementTree [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1790: WARNING: py:meth reference target not found: getroot [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1790: WARNING: py:class reference target not found: Element [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1793: WARNING: py:func reference target not found: XML [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1793: WARNING: py:class reference target not found: Element [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1793: WARNING: py:class reference target not found: ElementTree [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1837: WARNING: py:class reference target not found: Element [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1845: WARNING: py:meth reference target not found: ElementTree.write [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1845: WARNING: py:func reference target not found: parse [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1914: WARNING: py:meth reference target not found: digest [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1914: WARNING: py:meth reference target not found: hexdigest [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1952: WARNING: py:class reference target not found: Connection [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1960: WARNING: py:class reference target not found: Connection [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1960: WARNING: py:class reference target not found: Cursor [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1960: WARNING: py:meth reference target not found: execute [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1978: WARNING: py:meth reference target not found: execute [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1998: WARNING: py:meth reference target not found: fetchone [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:1998: WARNING: py:meth reference target not found: fetchall [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:2117: WARNING: c:func reference target not found: PyParser_ASTFromString [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:2238: WARNING: py:attr reference target not found: gi_frame [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:2238: WARNING: py:attr reference target not found: gi_frame [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:2261: WARNING: py:attr reference target not found: rpc_paths [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/whatsnew/2.5.rst:2261: WARNING: py:attr reference target not found: rpc_paths [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10185: WARNING: py:meth reference target not found: asyncio.asyncio.run_coroutine_threadsafe [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10185: WARNING: py:class reference target not found: CancelledError [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10185: WARNING: py:class reference target not found: InvalidStateError [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10189: WARNING: py:func reference target not found: ntpath.commonpath [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10197: WARNING: py:meth reference target not found: configparser.RawConfigParser._read [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10200: WARNING: py:func reference target not found: ntpath.commonpath [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10257: WARNING: py:func reference target not found: inspect.findsource [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10325: WARNING: py:class reference target not found: tkinter.Checkbutton [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10325: WARNING: py:class reference target not found: tkinter.ttk.Checkbutton [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10339: WARNING: py:class reference target not found: logging.TimedRotatingFileHandler [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10560: WARNING: py:meth reference target not found: xml.sax.expatreader.ExpatParser.flush [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10631: WARNING: py:func reference target not found: platform.java_ver [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10654: WARNING: py:class reference target not found: logging.TimedRotatingFileHandler [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10665: WARNING: py:meth reference target not found: email.Message.as_string [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10704: WARNING: py:class reference target not found: logging.TimedRotatingFileHandler [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10727: WARNING: py:class reference target not found: StreamWriter [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10730: WARNING: py:meth reference target not found: asyncio.BaseEventLoop.shutdown_default_executor [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10738: WARNING: py:class reference target not found: dis.ArgResolver [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10752: WARNING: py:class reference target not found: type.MethodDescriptorType [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10752: WARNING: py:class reference target not found: type.WrapperDescriptorType [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10765: WARNING: py:meth reference target not found: DatagramTransport.sendto [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10774: WARNING: py:func reference target not found: posixpath.commonpath [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10778: WARNING: py:func reference target not found: posixpath.commonpath [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10787: WARNING: py:data reference target not found: VERIFY_X509_STRICT [ref.data]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10794: WARNING: py:meth reference target not found: importlib.resources.simple.ResourceHandle.open [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10813: WARNING: py:meth reference target not found: Profile.print_stats [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10816: WARNING: py:data reference target not found: socket.SO_BINDTOIFINDEX [ref.data]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10832: WARNING: py:func reference target not found: io.BufferedReader.tell [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10832: WARNING: py:func reference target not found: io.BufferedReader.seek [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10832: WARNING: py:func reference target not found: io.BufferedRandom.tell [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10832: WARNING: py:func reference target not found: io.BufferedRandom.seek [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:10952: WARNING: 'envvar' reference target not found: PYLAUNCHER_ALLOW_INSTALL [ref.envvar]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11142: WARNING: py:meth reference target not found: io.BufferedRandom.read1 [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11154: WARNING: py:meth reference target not found: tkinter.Text.count [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11243: WARNING: py:meth reference target not found: asyncio.BaseEventLoop.create_server [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11250: WARNING: py:exc reference target not found: FileNotFound [ref.exc]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11285: WARNING: py:class reference target not found: asyncio.selector_events.BaseSelectorEventLoop [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11296: WARNING: py:class reference target not found: tkinter.Text [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11296: WARNING: py:class reference target not found: tkinter.Canvas [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11361: WARNING: py:meth reference target not found: tkinter._test [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11378: WARNING: py:func reference target not found: lzma._decode_filter_properties [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11704: WARNING: py:func reference target not found: email.message.get_payload [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11769: WARNING: py:exc reference target not found: CancelledError [ref.exc]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11769: WARNING: py:exc reference target not found: CancelledError [ref.exc]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11789: WARNING: py:meth reference target not found: asyncio.StreamReaderProtocol.connection_made [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11815: WARNING: py:mod reference target not found: multiprocessing.manager [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11815: WARNING: py:mod reference target not found: multiprocessing.resource_sharer [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11864: WARNING: py:meth reference target not found: asyncio.futures.Future.set_exception [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11933: WARNING: py:const reference target not found: LOG_FTP [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11933: WARNING: py:const reference target not found: LOG_NETINFO [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11933: WARNING: py:const reference target not found: LOG_REMOTEAUTH [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11933: WARNING: py:const reference target not found: LOG_INSTALL [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11933: WARNING: py:const reference target not found: LOG_RAS [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:11933: WARNING: py:const reference target not found: LOG_LAUNCHD [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12017: WARNING: py:meth reference target not found: AbstractEventLoop.create_server [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12017: WARNING: py:meth reference target not found: BaseEventLoop.create_server [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12061: WARNING: py:meth reference target not found: Signature.format [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12083: WARNING: py:class reference target not found: QueueHandler [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12131: WARNING: py:func reference target not found: urllib.request.getproxies_environment [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12156: WARNING: py:exc reference target not found: PatternError [ref.exc]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12195: WARNING: py:meth reference target not found: ssl.SSLSocket.recv_into [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12252: WARNING: py:meth reference target not found: pathlib.PureWindowsPath.is_absolute [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12378: WARNING: py:func reference target not found: sysconfig.get_plaform [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12443: WARNING: py:attr reference target not found: object.__weakref__ [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12504: WARNING: py:class reference target not found: Traceback [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12570: WARNING: 'envvar' reference target not found: PYTHON_PRESITE=package.module [ref.envvar]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12578: WARNING: py:meth reference target not found: types.CodeType.replace [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12614: WARNING: py:meth reference target not found: StreamWriter.__del__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12755: WARNING: py:class reference target not found: IPv6Address [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12806: WARNING: py:meth reference target not found: tkinter.Text.count [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:12822: WARNING: py:mod reference target not found: zipinfo [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13038: WARNING: c:func reference target not found: PyUnstable_PerfTrampoline_CompileCode [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13038: WARNING: c:func reference target not found: PyUnstable_PerfTrampoline_SetPersistAfterFork [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13038: WARNING: c:func reference target not found: PyUnstable_CopyPerfMapFile [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13100: WARNING: py:func reference target not found: interpreter_clear [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13102: WARNING: c:func reference target not found: PyErr_Display [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13118: WARNING: 'envvar' reference target not found: PYTHONUOPS [ref.envvar]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13262: WARNING: 'envvar' reference target not found: PYTHONUOPS [ref.envvar]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13268: WARNING: py:meth reference target not found: multiprocessing.synchronize.SemLock.__setstate__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13268: WARNING: py:attr reference target not found: multiprocessing.synchronize.SemLock._is_fork_ctx [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13273: WARNING: py:attr reference target not found: multiprocessing.synchronize.SemLock.is_fork_ctx [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13273: WARNING: py:attr reference target not found: multiprocessing.synchronize.SemLock._is_fork_ctx [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13335: WARNING: 'envvar' reference target not found: PYTHONUOPS [ref.envvar]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13391: WARNING: 'envvar' reference target not found: PYTHONUOPS [ref.envvar]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13425: WARNING: py:meth reference target not found: dbm.ndbm.ndbm.clear [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13428: WARNING: py:meth reference target not found: dbm.gnu.gdbm.clear [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13449: WARNING: 'envvar' reference target not found: PYTHONUOPS [ref.envvar]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13466: WARNING: 'opcode' reference target not found: LOAD_ATTR_INSTANCE_VALUE [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13491: WARNING: 'opcode' reference target not found: LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13491: WARNING: 'opcode' reference target not found: LOAD_ATTR_NONDESCRIPTOR_NO_DICT [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13674: WARNING: py:class reference target not found: tokenize.TokenInfo [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13678: WARNING: py:class reference target not found: tokenize.TokenInfo [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13739: WARNING: py:meth reference target not found: BaseEventLoop._run_once [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13751: WARNING: py:class reference target not found: PureWindowsPath [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13812: WARNING: py:meth reference target not found: KqueueSelector.select [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:13990: WARNING: py:meth reference target not found: gzip.GzipFile.seek [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14024: WARNING: py:meth reference target not found: sqlite3.connection.close [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14115: WARNING: py:meth reference target not found: __repr__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14130: WARNING: py:meth reference target not found: clear [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14139: WARNING: py:class reference target not found: smptlib.SMTP [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14146: WARNING: py:meth reference target not found: PurePath.relative_to [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14157: WARNING: py:meth reference target not found: SelectSelector.select [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14163: WARNING: py:meth reference target not found: KqueueSelector.select [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14187: WARNING: py:meth reference target not found: zipfile.Path.match [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14229: WARNING: py:func reference target not found: multiprocessing.managers.convert_to_error [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14233: WARNING: py:attr reference target not found: pathlib.PurePath.pathmod [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14258: WARNING: py:mod reference target not found: multiprocessing.spawn [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14272: WARNING: py:meth reference target not found: __get__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14272: WARNING: py:meth reference target not found: __set__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14277: WARNING: c:func reference target not found: mp_init [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14287: WARNING: py:func reference target not found: pydoc.doc [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14326: WARNING: py:meth reference target not found: gzip.GzipFile.flush [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14372: WARNING: py:mod reference target not found: pyexpat [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14645: WARNING: c:func reference target not found: mp_to_unsigned_bin_n [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14645: WARNING: c:func reference target not found: mp_unsigned_bin_size [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14665: WARNING: py:func reference target not found: builtins.issubclass [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14688: WARNING: py:func reference target not found: concurrent.futures.thread._worker [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:14726: WARNING: py:func reference target not found: close [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:15149: WARNING: py:func reference target not found: ntpath.normcase [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:15555: WARNING: py:class reference target not found: http.client.SimpleHTTPRequestHandler [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:15814: WARNING: py:mod reference target not found: multiprocessing.process [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:15836: WARNING: py:func reference target not found: urllib.parse.unsplit [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:15957: WARNING: py:meth reference target not found: tkinter.Menu.index [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:15961: WARNING: py:class reference target not found: URLError [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:16193: WARNING: py:class reference target not found: urllib.request.AbstractHTTPHandler [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:16202: WARNING: py:meth reference target not found: tkinter.Canvas.coords [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:16360: WARNING: py:func reference target not found: ntpath.realpath [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:16466: WARNING: 'opcode' reference target not found: BINARY_SUBSCR [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:16484: WARNING: c:func reference target not found: PyErr_Display [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:16685: WARNING: py:mod reference target not found: concurrent.futures.process [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:16793: WARNING: 'opcode' reference target not found: FOR_ITER_RANGE [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:16834: WARNING: 'opcode' reference target not found: RETURN_CONST [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:16864: WARNING: py:func reference target not found: fileinput.hookcompressed [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:16939: WARNING: py:meth reference target not found: pathlib.PureWindowsPath.match [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17108: WARNING: 'opcode' reference target not found: COMPARE_AND_BRANCH [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17123: WARNING: py:mod reference target not found: importlib/_bootstrap [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17132: WARNING: py:mod reference target not found: opcode [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17195: WARNING: py:meth reference target not found: asyncio.DefaultEventLoopPolicy.get_event_loop [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17221: WARNING: py:data reference target not found: ctypes.wintypes.BYTE [ref.data]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17234: WARNING: py:mod reference target not found: elementtree [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17333: WARNING: 'opcode' reference target not found: IMPORT_STAR [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17333: WARNING: 'opcode' reference target not found: PRINT_EXPR [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17333: WARNING: 'opcode' reference target not found: STOPITERATION_ERROR [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17347: WARNING: py:func reference target not found: int.__sizeof__ [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17442: WARNING: py:const reference target not found: socket.IP_PKTINFO [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17453: WARNING: py:mod reference target not found: pyexpat [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17494: WARNING: py:func reference target not found: http.cookiejar.eff_request_host [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17500: WARNING: py:meth reference target not found: Fraction.is_integer [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17574: WARNING: py:func reference target not found: iscoroutinefunction [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17578: WARNING: py:class reference target not found: multiprocessing.queues.Queue [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17755: WARNING: py:class reference target not found: BaseHTTPRequestHandler [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17912: WARNING: py:meth reference target not found: asyncio.BaseDefaultEventLoopPolicy.get_event_loop [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17912: WARNING: py:class reference target not found: asyncio.BaseDefaultEventLoopPolicy [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17946: WARNING: py:meth reference target not found: TarFile.next [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17949: WARNING: py:class reference target not found: WeakMethod [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:17996: WARNING: py:exc reference target not found: sqlite.DataError [ref.exc]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:18129: WARNING: py:data reference target not found: sys._base_executable [ref.data]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:18212: WARNING: py:attr reference target not found: types.CodeType.co_code [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:18265: WARNING: py:class reference target not found: asyncio.AbstractChildWatcher [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:18314: WARNING: py:mod reference target not found: importlib._bootstrap [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:18353: WARNING: py:func reference target not found: os.ismount [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:18490: WARNING: py:func reference target not found: os.exec [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:18631: WARNING: py:func reference target not found: sys.getdxp [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:18922: WARNING: py:meth reference target not found: __index__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:18925: WARNING: py:meth reference target not found: bool.__repr__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19046: WARNING: py:attr reference target not found: types.CodeType.co_code [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19105: WARNING: py:attr reference target not found: __text_signature__ [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19105: WARNING: py:meth reference target not found: __get__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19180: WARNING: py:meth reference target not found: tkinter.Text.count [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19209: WARNING: py:meth reference target not found: asyncio.AbstractEventLoopPolicy.get_child_watcher [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19209: WARNING: py:meth reference target not found: asyncio.AbstractEventLoopPolicy.set_child_watcher [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19226: WARNING: py:class reference target not found: asyncio.MultiLoopChildWatcher [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19226: WARNING: py:class reference target not found: asyncio.FastChildWatcher [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19226: WARNING: py:class reference target not found: asyncio.SafeChildWatcher [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19231: WARNING: py:class reference target not found: asyncio.PidfdChildWatcher [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19271: WARNING: py:mod reference target not found: dataclass [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19284: WARNING: py:meth reference target not found: gzip.GzipFile.read [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19294: WARNING: py:class reference target not found: tkinter.Checkbutton [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19324: WARNING: py:mod reference target not found: multiprocessing.resource_tracker [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19382: WARNING: py:func reference target not found: threading.Event.__init__ [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19385: WARNING: py:class reference target not found: asyncio.streams.StreamReaderProtocol [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19421: WARNING: py:meth reference target not found: asyncio.AbstractChildWatcher.attach_loop [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19507: WARNING: py:meth reference target not found: wsgiref.types.InputStream.__iter__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19516: WARNING: c:identifier reference target not found: _PyAccu [ref.identifier]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19516: WARNING: c:identifier reference target not found: _PyUnicodeWriter [ref.identifier]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19516: WARNING: c:identifier reference target not found: _PyAccu [ref.identifier]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19547: WARNING: py:meth reference target not found: SSLContext.set_default_verify_paths [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19597: WARNING: py:mod reference target not found: xml.etree [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19597: WARNING: py:mod reference target not found: xml.etree [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19613: WARNING: py:attr reference target not found: dispatch_table [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19643: WARNING: py:func reference target not found: locale.format [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19646: WARNING: py:func reference target not found: ssl.match_hostname [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19646: WARNING: py:func reference target not found: ssl.match_hostname [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19646: WARNING: py:func reference target not found: ssl.match_hostname [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19654: WARNING: py:func reference target not found: ssl.wrap_socket [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19654: WARNING: py:func reference target not found: ssl.wrap_socket [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19661: WARNING: py:func reference target not found: ssl.RAND_pseudo_bytes [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19677: WARNING: py:class reference target not found: asyncio.PidfdChildWatcher [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19699: WARNING: py:func reference target not found: asyncio.iscoroutinefunction [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19782: WARNING: py:class reference target not found: multiprocessing.Pool [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19813: WARNING: py:class reference target not found: wsgiref.BaseHandler [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19819: WARNING: py:func reference target not found: locale.resetlocale [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19830: WARNING: py:func reference target not found: re.template [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19830: WARNING: py:const reference target not found: re.TEMPLATE [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19830: WARNING: py:const reference target not found: re.T [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19839: WARNING: py:exc reference target not found: re.error [ref.exc]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19856: WARNING: py:func reference target not found: venv.ensure_directories [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19863: WARNING: py:func reference target not found: sqlite.connect [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19863: WARNING: py:class reference target not found: sqlite.Connection [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:19968: WARNING: py:class reference target not found: multiprocessing.SharedMemory [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20023: WARNING: py:class reference target not found: QueueHandler [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20023: WARNING: py:class reference target not found: LogRecord [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20053: WARNING: py:class reference target not found: zipfile.ZipExtFile [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20068: WARNING: py:meth reference target not found: collections.UserDict.get [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20263: WARNING: py:meth reference target not found: calendar.LocaleTextCalendar.formatweekday [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20502: WARNING: py:func reference target not found: ntpath.normcase [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20618: WARNING: py:const reference target not found: Py_TPFLAGS_IMMUTABLETYPE [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20810: WARNING: py:class reference target not found: generic_alias_iterator [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20820: WARNING: py:class reference target not found: EncodingMap [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20850: WARNING: py:meth reference target not found: add_note [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20903: WARNING: py:class reference target not found: ctypes.UnionType [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20903: WARNING: py:class reference target not found: testcapi.RecursingInfinitelyError [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:20976: WARNING: py:func reference target not found: os.fcopyfile [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21008: WARNING: py:meth reference target not found: TextIOWrapper.reconfigure [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21032: WARNING: py:const reference target not found: signal.SIGRTMIN [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21032: WARNING: py:const reference target not found: signal.SIGRTMAX [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21075: WARNING: py:exc reference target not found: re.error [ref.exc]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21080: WARNING: py:class reference target not found: multiprocessing.BaseManager [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21087: WARNING: py:exc reference target not found: re.error [ref.exc]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21114: WARNING: py:func reference target not found: Tools.gdb.libpython.write_repr [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21139: WARNING: py:class reference target not found: TextIOWrapper [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21171: WARNING: py:class reference target not found: TextIOWrapper [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21219: WARNING: py:meth reference target not found: __init__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21277: WARNING: py:meth reference target not found: __init_subclass__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21288: WARNING: py:func reference target not found: CookieJar.__iter__ [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21310: WARNING: py:class reference target not found: asyncio.streams.StreamWriter [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21370: WARNING: 'envvar' reference target not found: PYTHONREGRTEST_UNICODE_GUARD [ref.envvar]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21382: WARNING: py:mod reference target not found: ctypes.macholib.dyld [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21382: WARNING: py:mod reference target not found: ctypes.macholib.dylib [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21382: WARNING: py:mod reference target not found: ctypes.macholib.framework [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21382: WARNING: py:mod reference target not found: ctypes.test [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21501: WARNING: 'opcode' reference target not found: JUMP_IF_NOT_EG_MATCH [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21511: WARNING: 'opcode' reference target not found: JUMP_IF_NOT_EXC_MATCH [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21523: WARNING: c:macro reference target not found: PY_CALL_TRAMPOLINE [ref.macro]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21541: WARNING: 'opcode' reference target not found: JUMP_ABSOLUTE [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21615: WARNING: py:const reference target not found: CTYPES_MAX_ARGCOUNT [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21630: WARNING: py:meth reference target not found: ZipFile.mkdir [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21648: WARNING: py:exc reference target not found: URLError [ref.exc]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21648: WARNING: py:class reference target not found: urllib.request.URLopener [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21648: WARNING: py:func reference target not found: urllib.request.URLopener.open_ftp [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21654: WARNING: py:func reference target not found: Exception.with_traceback [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21687: WARNING: py:meth reference target not found: zipfile._SharedFile.tell [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21687: WARNING: py:class reference target not found: ZipFile [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21784: WARNING: py:class reference target not found: asyncio.base_events.Server [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21803: WARNING: py:meth reference target not found: asyncio.AbstractEventLoop.sock_sendto [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21803: WARNING: py:meth reference target not found: asyncio.AbstractEventLoop.sock_recvfrom [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21803: WARNING: py:meth reference target not found: asyncio.AbstractEventLoop.sock_recvfrom_into [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21812: WARNING: py:class reference target not found: GenericAlias [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21843: WARNING: py:class reference target not found: BasicInterpolation [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21843: WARNING: py:class reference target not found: ExtendedInterpolation [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:21867: WARNING: py:meth reference target not found: MimeTypes.guess_type [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22007: WARNING: 'envvar' reference target not found: PYLAUNCHER_ALLOW_INSTALL [ref.envvar]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22121: WARNING: 'opcode' reference target not found: LOAD_METHOD [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22124: WARNING: 'opcode' reference target not found: BINARY_SUBSCR [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22164: WARNING: py:meth reference target not found: BaseException.__str__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22260: WARNING: py:meth reference target not found: mmap.find [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22260: WARNING: py:meth reference target not found: mmap.rfind [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22308: WARNING: py:meth reference target not found: __repr__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22381: WARNING: py:meth reference target not found: __eq__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22381: WARNING: py:meth reference target not found: __hash__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22396: WARNING: py:data reference target not found: re.RegexFlag.NOFLAG [ref.data]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22547: WARNING: py:meth reference target not found: __trunc__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22547: WARNING: py:meth reference target not found: __trunc__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22547: WARNING: py:meth reference target not found: __int__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22547: WARNING: py:meth reference target not found: __index__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22581: WARNING: py:meth reference target not found: BaseExceptionGroup.__new__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22587: WARNING: py:meth reference target not found: weakref.ref.__call__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22629: WARNING: py:data reference target not found: sys._base_executable [ref.data]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22662: WARNING: py:class reference target not found: asyncio.transports.WriteTransport [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22690: WARNING: py:meth reference target not found: mock.patch [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22743: WARNING: py:meth reference target not found: enum.Enum.__call__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22760: WARNING: py:attr reference target not found: __bases__ [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22815: WARNING: py:func reference target not found: test.support.requires_fork [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22818: WARNING: py:func reference target not found: test.support.requires_subprocess [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22982: WARNING: c:macro reference target not found: PyLong_BASE [ref.macro]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22988: WARNING: py:meth reference target not found: ExceptionGroup.split [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:22988: WARNING: py:meth reference target not found: ExceptionGroup.subgroup [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23077: WARNING: py:attr reference target not found: types.CodeType.co_firstlineno [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23109: WARNING: py:mod reference target not found: asyncio.windows_events [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23121: WARNING: py:attr reference target not found: webbrowser.MacOSXOSAScript._name [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23150: WARNING: py:meth reference target not found: add_argument_group [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23150: WARNING: py:meth reference target not found: add_argument_group [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23150: WARNING: py:meth reference target not found: add_mutually_exclusive_group [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23182: WARNING: py:attr reference target not found: __all__ [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23206: WARNING: py:meth reference target not found: __repr__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23244: WARNING: py:meth reference target not found: enum.Flag._missing_ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23267: WARNING: c:func reference target not found: Py_FrozenMain [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23422: WARNING: 'opcode' reference target not found: BINARY_SUBSCR [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23516: WARNING: py:meth reference target not found: turtle.RawTurtle.tiltangle [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23518: WARNING: py:meth reference target not found: turtle.RawTurtle.tiltangle [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23529: WARNING: py:mod reference target not found: sqlite [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23599: WARNING: py:class reference target not found: ProcessPoolExecutor [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:23749: WARNING: py:mod reference target not found: pyexpat [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24026: WARNING: py:func reference target not found: inspect.getabsfile [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24065: WARNING: py:class reference target not found: Signature [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24164: WARNING: py:mod reference target not found: test.libregrtest [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24218: WARNING: py:mod reference target not found: pyexpat [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24234: WARNING: py:meth reference target not found: argparse.parse_known_args [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24490: WARNING: py:meth reference target not found: __bytes__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24494: WARNING: py:meth reference target not found: __complex__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24788: WARNING: py:class reference target not found: sqlite.Statement [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24810: WARNING: c:func reference target not found: type_new [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24850: WARNING: py:func reference target not found: str.__getitem__ [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24913: WARNING: py:class reference target not found: pyexpat.xmlparser [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24923: WARNING: py:func reference target not found: threading._shutdown [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:24943: WARNING: py:meth reference target not found: unittest.IsolatedAsyncioTestCase.debug [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25007: WARNING: py:meth reference target not found: <unittest.TestLoader.loadTestsFromModule> TestLoader.loadTestsFromModule [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25071: WARNING: py:meth reference target not found: traceback.StackSummary.format_frame [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25077: WARNING: py:meth reference target not found: traceback.StackSummary.format_frame [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25156: WARNING: py:exc reference target not found: UnicodEncodeError [ref.exc]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25191: WARNING: py:meth reference target not found: collections.OrderedDict.pop [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25213: WARNING: py:mod reference target not found: rcompleter [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25277: WARNING: py:class reference target not found: ExitStack [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25277: WARNING: py:class reference target not found: AsyncExitStack [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25287: WARNING: py:const reference target not found: os.path.sep [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25292: WARNING: py:func reference target not found: StackSummary.format_frame [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25307: WARNING: py:func reference target not found: pdb.main [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25368: WARNING: py:meth reference target not found: bz2.BZ2File.write [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25368: WARNING: py:meth reference target not found: lzma.LZMAFile.write [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25398: WARNING: py:meth reference target not found: email.message.MIMEPart.as_string [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25412: WARNING: py:func reference target not found: parse_makefile [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25465: WARNING: py:deco reference target not found: asyncio.coroutine [ref.deco]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25465: WARNING: py:class reference target not found: asyncio.coroutines.CoroWrapper [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25512: WARNING: py:func reference target not found: runtime_checkable [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25545: WARNING: py:func reference target not found: functool.lru_cache [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25557: WARNING: py:meth reference target not found: pdb.Pdb.checkline [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25557: WARNING: py:meth reference target not found: pdb.Pdb.reset [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25574: WARNING: py:func reference target not found: shutil._unpack_zipfile [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25584: WARNING: py:func reference target not found: importlib._bootstrap._find_and_load [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25592: WARNING: py:meth reference target not found: loop.set_default_executor [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25597: WARNING: py:class reference target not found: asyncio.trsock.TransportSocket [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25638: WARNING: py:mod reference target not found: tkinter.tix [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25779: WARNING: py:class reference target not found: TextWrap [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25828: WARNING: py:meth reference target not found: __init__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25828: WARNING: py:meth reference target not found: __post_init__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25875: WARNING: py:func reference target not found: unittest.create_autospec [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:25961: WARNING: c:func reference target not found: Py_FrozenMain [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26014: WARNING: 'envvar' reference target not found: EnableControlFlowGuard [ref.envvar]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26137: WARNING: py:meth reference target not found: BufferedReader.peek [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26216: WARNING: c:func reference target not found: Py_FrozenMain [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26291: WARNING: py:func reference target not found: sqlite3.connect/handle [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26400: WARNING: c:func reference target not found: PyErr_Display [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26444: WARNING: c:func reference target not found: PyErr_Display [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26511: WARNING: py:func reference target not found: inspect.from_callable [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26511: WARNING: py:func reference target not found: inspect.from_function [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26511: WARNING: py:func reference target not found: inspect.from_callable [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26615: WARNING: py:data reference target not found: TypeGuard [ref.data]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26632: WARNING: py:func reference target not found: logging.fileConfig [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26728: WARNING: py:class reference target not found: asyncio.StreamReaderProtocol [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:26819: WARNING: py:mod reference target not found: test.libregrtest [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27132: WARNING: py:func reference target not found: subprocess.communicate [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27151: WARNING: py:func reference target not found: cleanup [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27172: WARNING: py:meth reference target not found: HTTPConnection.set_tunnel [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27453: WARNING: py:func reference target not found: multiprocess.synchronize [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27453: WARNING: py:class reference target not found: ProcessPoolExecutor [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27713: WARNING: py:func reference target not found: randbytes [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27722: WARNING: py:func reference target not found: TracebackException.format [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27722: WARNING: py:func reference target not found: TracebackException.format_exception_only [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27782: WARNING: py:class reference target not found: Threading.thread [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27809: WARNING: py:meth reference target not found: unittest.TestLoader().loadTestsFromTestCase [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27809: WARNING: py:meth reference target not found: unittest.makeSuite [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27951: WARNING: py:mod reference target not found: pyexpat [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27970: WARNING: py:class reference target not found: tkinter.Variable [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:27991: WARNING: py:func reference target not found: tkinter.NoDefaultRoot [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28021: WARNING: py:func reference target not found: tracemalloc.Traceback.__repr__ [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28029: WARNING: py:func reference target not found: atexit._run_exitfuncs [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28086: WARNING: py:func reference target not found: posixpath.expanduser [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28133: WARNING: py:mod reference target not found: zipimporter [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28141: WARNING: py:class reference target not found: tkinter.ttk.LabeledScale [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28147: WARNING: py:func reference target not found: a85encode [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28147: WARNING: py:func reference target not found: b85encode [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28226: WARNING: c:func reference target not found: Py_FrozenMain [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28370: WARNING: py:func reference target not found: inspect.findsource [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28370: WARNING: py:attr reference target not found: co_lineno [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28551: WARNING: py:func reference target not found: pprint._safe_repr [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28555: WARNING: c:func reference target not found: splice [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:28847: WARNING: c:func reference target not found: PyAST_Validate [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29065: WARNING: py:meth reference target not found: __class_getitem__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29215: WARNING: py:meth reference target not found: __dir__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29322: WARNING: py:mod reference target not found: winapi [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29335: WARNING: py:mod reference target not found: sha256 [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29375: WARNING: py:mod reference target not found: symbol [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29407: WARNING: py:mod reference target not found: parser [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29446: WARNING: c:func reference target not found: PyOS_Readline [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29606: WARNING: py:meth reference target not found: turtle.Vec2D.__rmul__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29694: WARNING: py:class reference target not found: shared_memory.SharedMemory [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29697: WARNING: py:meth reference target not found: collections.OrderedDict.pop [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29752: WARNING: py:func reference target not found: pdb.find_function [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29841: WARNING: py:func reference target not found: csv.writer.writerow [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29841: WARNING: py:meth reference target not found: csv.writer.writerows [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29866: WARNING: py:func reference target not found: hashlib.compare_digest [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29882: WARNING: py:mod reference target not found: symbol [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29930: WARNING: py:mod reference target not found: xml.etree.cElementTree [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29940: WARNING: py:func reference target not found: unittest.assertNoLogs [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29961: WARNING: py:class reference target not found: multiprocessing.context.get_all_start_methods [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:29976: WARNING: py:meth reference target not found: IMAP4.noop [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:30079: WARNING: py:data reference target not found: test.support.TESTFN [ref.data]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:30459: WARNING: py:meth reference target not found: Future.cancel [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:30459: WARNING: py:meth reference target not found: Task.cancel [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:30841: WARNING: py:meth reference target not found: ShareableList.__setitem__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:30844: WARNING: py:meth reference target not found: pathlib.Path.with_stem [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:30900: WARNING: py:func reference target not found: posix.sysconf [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:31305: WARNING: py:class reference target not found: multiprocessing.Pool [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:31416: WARNING: py:meth reference target not found: tempfile.SpooledTemporaryFile.softspace [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:31700: WARNING: py:meth reference target not found: list.__contains__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:31756: WARNING: py:meth reference target not found: io.BufferedReader.truncate [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:31810: WARNING: py:func reference target not found: unittest.case.shortDescription [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:31984: WARNING: c:member reference target not found: PyThreadState.on_delete [ref.member]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32015: WARNING: py:class reference target not found: functools.TopologicalSorter [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32042: WARNING: py:meth reference target not found: __aenter__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32042: WARNING: py:meth reference target not found: __aexit__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32141: WARNING: py:mod reference target not found: binhex [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32141: WARNING: py:func reference target not found: binascii.b2a_hqx [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32141: WARNING: py:func reference target not found: binascii.a2b_hqx [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32141: WARNING: py:func reference target not found: binascii.rlecode_hqx [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32141: WARNING: py:func reference target not found: binascii.rledecode_hqx [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32229: WARNING: py:func reference target not found: urllib.request.proxy_bypass_environment [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32238: WARNING: py:func reference target not found: mock.patch.stopall [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32238: WARNING: py:func reference target not found: mock.patch.dict [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32263: WARNING: py:func reference target not found: Popen.communicate [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32278: WARNING: py:func reference target not found: unittest.mock.attach_mock [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32311: WARNING: c:func reference target not found: setenv [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32311: WARNING: c:func reference target not found: unsetenv [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32534: WARNING: py:func reference target not found: is_cgi [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32582: WARNING: py:class reference target not found: zipfile.ZipExtFile [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32587: WARNING: py:func reference target not found: enum._decompose [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32649: WARNING: py:func reference target not found: test.support.run_python_until_end [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32649: WARNING: py:func reference target not found: test.support.assert_python_ok [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32649: WARNING: py:func reference target not found: test.support.assert_python_failure [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32790: WARNING: py:meth reference target not found: float.__getformat__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32974: WARNING: py:meth reference target not found: list.__contains__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32974: WARNING: py:meth reference target not found: tuple.__contains__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32978: WARNING: py:meth reference target not found: builtins.__import__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32985: WARNING: py:class reference target not found: ast.parameters [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:32994: WARNING: c:func reference target not found: PyErr_Display [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33138: WARNING: py:class reference target not found: asyncio.PidfdChildWatcher [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33144: WARNING: py:const reference target not found: fcntl.F_OFD_GETLK [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33144: WARNING: py:const reference target not found: fcntl.F_OFD_SETLK [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33144: WARNING: py:const reference target not found: fcntl.F_OFD_SETLKW [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33148: WARNING: py:class reference target not found: zipfile.ZipExtFile [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33164: WARNING: py:func reference target not found: pathlib.WindowsPath.glob [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33172: WARNING: py:attr reference target not found: si_code [ref.attr]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33175: WARNING: py:meth reference target not found: inspect.signature.bind [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33205: WARNING: py:func reference target not found: email.message.get [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33324: WARNING: py:meth reference target not found: loop.shutdown_default_executor [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33342: WARNING: py:meth reference target not found: datetime.utctimetuple [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33342: WARNING: py:meth reference target not found: datetime.utcnow [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33342: WARNING: py:meth reference target not found: datetime.utcfromtimestamp [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33366: WARNING: py:class reference target not found: ForwardReferences [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33386: WARNING: py:func reference target not found: tee [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33453: WARNING: py:class reference target not found: ArgumentParser [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33505: WARNING: py:meth reference target not found: writelines [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33539: WARNING: py:mod reference target not found: parser [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33556: WARNING: py:meth reference target not found: is_relative_to [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33556: WARNING: py:class reference target not found: PurePath [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33617: WARNING: py:func reference target not found: unittest.mock.attach_mock [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33645: WARNING: py:func reference target not found: multiprocessing.util.get_temp_dir [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33769: WARNING: py:meth reference target not found: RobotFileParser.crawl_delay [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33769: WARNING: py:meth reference target not found: RobotFileParser.request_rate [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33796: WARNING: py:meth reference target not found: CookieJar.make_cookies [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33829: WARNING: py:func reference target not found: socket.recv.fds [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:33849: WARNING: py:class reference target not found: ZipInfo [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:34004: WARNING: py:class reference target not found: Request [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:34135: WARNING: py:func reference target not found: test.support.catch_threading_exception [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:34363: WARNING: py:func reference target not found: os.realpath [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:34388: WARNING: 'envvar' reference target not found: PIP_USER [ref.envvar]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:34415: WARNING: c:func reference target not found: strcasecmp [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:34584: WARNING: c:macro reference target not found: PY_SSIZE_T_CLEAN [ref.macro]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:34861: WARNING: py:func reference target not found: copy_file_range [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:34997: WARNING: py:meth reference target not found: urllib.request.URLopener.retrieve [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.Stream [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.Stream [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:func reference target not found: asyncio.connect [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:func reference target not found: asyncio.connect_unix [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:func reference target not found: asyncio.connect_read_pipe [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:func reference target not found: asyncio.connect_write_pipe [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.Stream [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.StreamServer [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: UnixStreamServer [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.Stream [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: StreamReader [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: StreamWriter [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.FlowControlMixing [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35022: WARNING: py:class reference target not found: asyncio.StreamReaderProtocol [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35131: WARNING: py:meth reference target not found: wsgiref.handlers.BaseHandler.close [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35163: WARNING: py:meth reference target not found: csv.Writer.writerow [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35178: WARNING: py:meth reference target not found: asyncio.SelectorEventLoop.subprocess_exec [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35219: WARNING: py:meth reference target not found: asyncio.AbstractEventLoop.create_datagram_endpoint [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35460: WARNING: py:data reference target not found: posixpath.defpath [ref.data]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35662: WARNING: py:meth reference target not found: imap.IMAP4.logout [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35700: WARNING: py:class reference target not found: tkinter.PhotoImage [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35740: WARNING: rst:dir reference target not found: literalinclude [ref.dir]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35957: WARNING: c:identifier reference target not found: name [ref.identifier]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35957: WARNING: c:identifier reference target not found: name [ref.identifier]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:35957: WARNING: c:identifier reference target not found: str [ref.identifier]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36164: WARNING: py:class reference target not found: FileCookieJar [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36190: WARNING: py:class reference target not found: multiprocessing.Pool [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36413: WARNING: py:class reference target not found: multiprocessing.Pool [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36433: WARNING: py:meth reference target not found: datetime.fromtimestamp [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36440: WARNING: py:class reference target not found: xmlrpc.client.Transport [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36440: WARNING: py:class reference target not found: xmlrpc.client.SafeTransport [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36474: WARNING: py:func reference target not found: test.support.check_syntax_warning [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36651: WARNING: py:meth reference target not found: float.__format__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36651: WARNING: py:meth reference target not found: complex.__format__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36661: WARNING: py:func reference target not found: namedtuple [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36898: WARNING: py:class reference target not found: BuiltinMethodType [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36898: WARNING: py:class reference target not found: ModuleType [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36898: WARNING: py:class reference target not found: MethodWrapperType [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:36898: WARNING: py:class reference target not found: MethodWrapperType [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: BREAK_LOOP [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: CONTINUE_LOOP [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: SETUP_LOOP [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: SETUP_EXCEPT [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: ROT_FOUR [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: BEGIN_FINALLY [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: CALL_FINALLY [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: POP_FINALLY [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: END_FINALLY [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37094: WARNING: 'opcode' reference target not found: WITH_CLEANUP_START [ref.opcode]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37170: WARNING: py:class reference target not found: ast.Num [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37198: WARNING: py:meth reference target not found: threading.Thread.isAlive [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37243: WARNING: py:class reference target not found: unittest.runner.TextTestRunner [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37243: WARNING: py:mod reference target not found: unittest.runner [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37263: WARNING: py:meth reference target not found: multiprocessing.Pool.__enter__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37288: WARNING: py:class reference target not found: multiprocessing.Pool [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37291: WARNING: py:class reference target not found: Mock [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37300: WARNING: py:func reference target not found: distutils.utils.check_environ [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37304: WARNING: py:func reference target not found: posixpath.expanduser [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37451: WARNING: py:func reference target not found: multiprocessing.reduction.recvfds [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37552: WARNING: py:meth reference target not found: Executor.map [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37552: WARNING: py:func reference target not found: as_completed [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37562: WARNING: py:class reference target not found: QueueHandler [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37562: WARNING: py:class reference target not found: LogRecord [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37652: WARNING: py:class reference target not found: multiprocessing.managers.DictProxy [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37780: WARNING: py:meth reference target not found: asyncio.AbstractEventLoop.create_task [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37798: WARNING: py:meth reference target not found: AbstractEventLoop.set_default_executor [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:37843: WARNING: py:exc reference target not found: base64.Error [ref.exc]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38171: WARNING: py:class reference target not found: cProfile.Profile [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38261: WARNING: py:mod reference target not found: parser [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38300: WARNING: py:meth reference target not found: importlib.machinery.invalidate_caches [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38407: WARNING: py:meth reference target not found: hosts [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38585: WARNING: py:func reference target not found: socket.recvfrom [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38742: WARNING: py:func reference target not found: islice [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38769: WARNING: py:meth reference target not found: __getattr__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38769: WARNING: py:meth reference target not found: get [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38811: WARNING: py:func reference target not found: tearDownModule [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38826: WARNING: py:class reference target not found: multiprocessing.Pool [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38830: WARNING: py:mod reference target not found: test.bisect [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38830: WARNING: py:mod reference target not found: test.bisect_cmd [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38837: WARNING: py:func reference target not found: test.support.run_unittest [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:38837: WARNING: py:exc reference target not found: TestDidNotRun [ref.exc]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:39155: WARNING: py:meth reference target not found: datetime.fromtimestamp [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:39918: WARNING: py:mod reference target not found: parser [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:39938: WARNING: py:meth reference target not found: importlib.machinery.invalidate_caches [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:40155: WARNING: py:meth reference target not found: hosts [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:40191: WARNING: py:func reference target not found: islice [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:40419: WARNING: py:func reference target not found: socket.recvfrom [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:40446: WARNING: py:meth reference target not found: __getattr__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:40446: WARNING: py:meth reference target not found: get [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:40644: WARNING: py:meth reference target not found: asyncio.AbstractEventLoop.sendfile [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:40718: WARNING: py:meth reference target not found: get_resource_reader [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:41088: WARNING: py:class reference target not found: ProcessPoolExecutor [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:41280: WARNING: py:meth reference target not found: ssl.match_hostname [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:42557: WARNING: py:func reference target not found: asyncio._get_running_loop [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:42880: WARNING: py:mod reference target not found: macpath [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:43071: WARNING: py:const reference target not found: socket.TCP_NOTSENT_LOWAT [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:43258: WARNING: py:const reference target not found: socket.TCP_CONGESTION [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:43258: WARNING: py:const reference target not found: socket.TCP_USER_TIMEOUT [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:44232: WARNING: py:mod reference target not found: parser [ref.mod]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:44266: WARNING: py:meth reference target not found: hosts [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:44306: WARNING: py:func reference target not found: islice [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:44649: WARNING: py:meth reference target not found: __getattr__ [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:44649: WARNING: py:meth reference target not found: get [ref.meth]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:45334: WARNING: py:func reference target not found: asyncio._get_running_loop [ref.func]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:46469: WARNING: py:const reference target not found: socket.TCP_CONGESTION [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:46469: WARNING: py:const reference target not found: socket.TCP_USER_TIMEOUT [ref.const]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:48767: WARNING: py:class reference target not found: warnings.WarningMessage [ref.class]
|
||||
/home/pablogsal/github/python/main/Doc/build/NEWS:53512: WARNING: py:class reference target not found: email.feedparser.FeedParser [ref.class]
|
||||
|
|
@ -1937,6 +1937,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
|
|||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(only_keys));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(oparg));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opcode));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opcodes));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(open));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opener));
|
||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(operation));
|
||||
|
|
|
|||
|
|
@ -660,6 +660,7 @@ struct _Py_global_strings {
|
|||
STRUCT_FOR_ID(only_keys)
|
||||
STRUCT_FOR_ID(oparg)
|
||||
STRUCT_FOR_ID(opcode)
|
||||
STRUCT_FOR_ID(opcodes)
|
||||
STRUCT_FOR_ID(open)
|
||||
STRUCT_FOR_ID(opener)
|
||||
STRUCT_FOR_ID(operation)
|
||||
|
|
|
|||
1
Include/internal/pycore_runtime_init_generated.h
generated
1
Include/internal/pycore_runtime_init_generated.h
generated
|
|
@ -1935,6 +1935,7 @@ extern "C" {
|
|||
INIT_ID(only_keys), \
|
||||
INIT_ID(oparg), \
|
||||
INIT_ID(opcode), \
|
||||
INIT_ID(opcodes), \
|
||||
INIT_ID(open), \
|
||||
INIT_ID(opener), \
|
||||
INIT_ID(operation), \
|
||||
|
|
|
|||
|
|
@ -2420,6 +2420,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
|
|||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(opcodes);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
assert(PyUnicode_GET_LENGTH(string) != 1);
|
||||
string = &_Py_ID(open);
|
||||
_PyUnicode_InternStatic(interp, &string);
|
||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||
|
|
|
|||
|
|
@ -862,6 +862,84 @@ .tooltip-hint {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
Tooltip Bytecode/Opcode Section
|
||||
-------------------------------------------------------------------------- */
|
||||
|
||||
.tooltip-opcodes {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tooltip-opcodes-title {
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tooltip-opcodes-list {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.tooltip-opcode-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 60px 60px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.tooltip-opcode-name {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tooltip-opcode-name.specialized {
|
||||
color: var(--spec-high-text);
|
||||
}
|
||||
|
||||
.tooltip-opcode-base-hint {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.tooltip-opcode-badge {
|
||||
background: var(--spec-high);
|
||||
color: white;
|
||||
font-size: 9px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.tooltip-opcode-count {
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.tooltip-opcode-bar {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 2px;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tooltip-opcode-bar-fill {
|
||||
background: linear-gradient(90deg, var(--python-blue), var(--python-blue-light));
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
Responsive (Flamegraph-specific)
|
||||
-------------------------------------------------------------------------- */
|
||||
|
|
|
|||
|
|
@ -8,6 +8,32 @@ let currentThreadFilter = 'all';
|
|||
// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
|
||||
// and automatically switch with theme changes - no JS color arrays needed!
|
||||
|
||||
// Opcode mappings - loaded from embedded data (generated by Python)
|
||||
let OPCODE_NAMES = {};
|
||||
let DEOPT_MAP = {};
|
||||
|
||||
// Initialize opcode mappings from embedded data
|
||||
function initOpcodeMapping(data) {
|
||||
if (data && data.opcode_mapping) {
|
||||
OPCODE_NAMES = data.opcode_mapping.names || {};
|
||||
DEOPT_MAP = data.opcode_mapping.deopt || {};
|
||||
}
|
||||
}
|
||||
|
||||
// Get opcode info from opcode number
|
||||
function getOpcodeInfo(opcode) {
|
||||
const opname = OPCODE_NAMES[opcode] || `<${opcode}>`;
|
||||
const baseOpcode = DEOPT_MAP[opcode];
|
||||
const isSpecialized = baseOpcode !== undefined;
|
||||
const baseOpname = isSpecialized ? (OPCODE_NAMES[baseOpcode] || `<${baseOpcode}>`) : opname;
|
||||
|
||||
return {
|
||||
opname: opname,
|
||||
baseOpname: baseOpname,
|
||||
isSpecialized: isSpecialized
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// String Resolution
|
||||
// ============================================================================
|
||||
|
|
@ -249,6 +275,53 @@ function createPythonTooltip(data) {
|
|||
</div>`;
|
||||
}
|
||||
|
||||
// Create bytecode/opcode section if available
|
||||
let opcodeSection = "";
|
||||
const opcodes = d.data.opcodes;
|
||||
if (opcodes && typeof opcodes === 'object' && Object.keys(opcodes).length > 0) {
|
||||
// Sort opcodes by sample count (descending)
|
||||
const sortedOpcodes = Object.entries(opcodes)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 8); // Limit to top 8
|
||||
|
||||
const totalOpcodeSamples = sortedOpcodes.reduce((sum, [, count]) => sum + count, 0);
|
||||
const maxCount = sortedOpcodes[0][1] || 1;
|
||||
|
||||
const opcodeLines = sortedOpcodes.map(([opcode, count]) => {
|
||||
const opcodeInfo = getOpcodeInfo(parseInt(opcode, 10));
|
||||
const pct = ((count / totalOpcodeSamples) * 100).toFixed(1);
|
||||
const barWidth = (count / maxCount) * 100;
|
||||
const specializedBadge = opcodeInfo.isSpecialized
|
||||
? '<span class="tooltip-opcode-badge">SPECIALIZED</span>'
|
||||
: '';
|
||||
const baseOpHint = opcodeInfo.isSpecialized
|
||||
? `<span class="tooltip-opcode-base-hint">(${opcodeInfo.baseOpname})</span>`
|
||||
: '';
|
||||
const nameClass = opcodeInfo.isSpecialized
|
||||
? 'tooltip-opcode-name specialized'
|
||||
: 'tooltip-opcode-name';
|
||||
|
||||
return `
|
||||
<div class="tooltip-opcode-row">
|
||||
<div class="${nameClass}">
|
||||
${opcodeInfo.opname}${baseOpHint}${specializedBadge}
|
||||
</div>
|
||||
<div class="tooltip-opcode-count">${count.toLocaleString()} (${pct}%)</div>
|
||||
<div class="tooltip-opcode-bar">
|
||||
<div class="tooltip-opcode-bar-fill" style="width: ${barWidth}%;"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
opcodeSection = `
|
||||
<div class="tooltip-opcodes">
|
||||
<div class="tooltip-opcodes-title">Bytecode Instructions:</div>
|
||||
<div class="tooltip-opcodes-list">
|
||||
${opcodeLines}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const fileLocationHTML = isSpecialFrame ? "" : `
|
||||
<div class="tooltip-location">${filename}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
|
||||
|
||||
|
|
@ -275,6 +348,7 @@ function createPythonTooltip(data) {
|
|||
` : ''}
|
||||
</div>
|
||||
${sourceSection}
|
||||
${opcodeSection}
|
||||
<div class="tooltip-hint">
|
||||
${childCount > 0 ? "Click to zoom into this function" : "Leaf function - no children"}
|
||||
</div>
|
||||
|
|
@ -994,6 +1068,9 @@ function initFlamegraph() {
|
|||
processedData = resolveStringIndices(EMBEDDED_DATA);
|
||||
}
|
||||
|
||||
// Initialize opcode mapping from embedded data
|
||||
initOpcodeMapping(EMBEDDED_DATA);
|
||||
|
||||
originalData = processedData;
|
||||
initThreadFilter(processedData);
|
||||
|
||||
|
|
|
|||
|
|
@ -629,13 +629,18 @@ .legend {
|
|||
}
|
||||
|
||||
.legend-content {
|
||||
width: 94%;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.legend-separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-title {
|
||||
|
|
@ -643,12 +648,13 @@ .legend-title {
|
|||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-family: var(--font-sans);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-gradient {
|
||||
flex: 1;
|
||||
max-width: 300px;
|
||||
height: 24px;
|
||||
width: 150px;
|
||||
flex-shrink: 0;
|
||||
height: 20px;
|
||||
background: linear-gradient(90deg,
|
||||
var(--bg-tertiary) 0%,
|
||||
var(--heat-2) 25%,
|
||||
|
|
@ -666,6 +672,16 @@ .legend-labels {
|
|||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-sans);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Legend Controls Group - wraps toggles and bytecode button together */
|
||||
.legend-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Toggle Switch Styles */
|
||||
|
|
@ -677,6 +693,7 @@ .toggle-switch {
|
|||
user-select: none;
|
||||
font-family: var(--font-sans);
|
||||
transition: opacity var(--transition-fast);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-switch:hover {
|
||||
|
|
@ -687,13 +704,10 @@ .toggle-switch .toggle-label {
|
|||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
min-width: 55px;
|
||||
text-align: right;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.toggle-switch .toggle-label:last-child {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toggle-switch .toggle-label.active {
|
||||
|
|
@ -701,6 +715,20 @@ .toggle-switch .toggle-label.active {
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Reserve space for bold text to prevent layout shift on toggle */
|
||||
.toggle-switch .toggle-label::after {
|
||||
content: attr(data-text);
|
||||
font-weight: 600;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.toggle-switch.disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.toggle-track {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
|
|
@ -1117,6 +1145,15 @@ @media (max-width: 1100px) {
|
|||
.stats-summary {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.legend-content {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.legend-controls {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
|
@ -1136,6 +1173,7 @@ @media (max-width: 600px) {
|
|||
|
||||
.legend-content {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
|
@ -1143,4 +1181,400 @@ @media (max-width: 600px) {
|
|||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.legend-separator {
|
||||
width: 80%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.legend-controls {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.legend-controls .toggle-switch {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.legend-controls .toggle-switch .toggle-label:first-child {
|
||||
width: 70px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.legend-controls .toggle-switch .toggle-label:last-child {
|
||||
width: 90px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Compact code columns on small screens */
|
||||
.header-line-number,
|
||||
.line-number {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.header-samples-self,
|
||||
.header-samples-cumulative,
|
||||
.line-samples-self,
|
||||
.line-samples-cumulative {
|
||||
width: 55px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Adjust padding - headers need vertical, data rows don't */
|
||||
.header-line-number,
|
||||
.header-samples-self,
|
||||
.header-samples-cumulative {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.line-number,
|
||||
.line-samples-self,
|
||||
.line-samples-cumulative {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.bytecode-toggle {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
margin: 0 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--code-accent);
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
transition: transform var(--transition-fast), color var(--transition-fast);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bytecode-toggle:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.bytecode-spacer {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.bytecode-panel {
|
||||
margin-left: 90px;
|
||||
padding: 8px 15px;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 3px solid var(--accent);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Specialization summary bar */
|
||||
.bytecode-spec-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(100, 100, 100, 0.1);
|
||||
}
|
||||
|
||||
.bytecode-spec-summary .spec-pct {
|
||||
font-size: 1.4em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bytecode-spec-summary .spec-label {
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.bytecode-spec-summary .spec-detail {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9em;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.bytecode-spec-summary.high {
|
||||
background: var(--spec-high-bg);
|
||||
border-left: 3px solid var(--spec-high);
|
||||
}
|
||||
.bytecode-spec-summary.high .spec-pct,
|
||||
.bytecode-spec-summary.high .spec-label {
|
||||
color: var(--spec-high-text);
|
||||
}
|
||||
|
||||
.bytecode-spec-summary.medium {
|
||||
background: var(--spec-medium-bg);
|
||||
border-left: 3px solid var(--spec-medium);
|
||||
}
|
||||
.bytecode-spec-summary.medium .spec-pct,
|
||||
.bytecode-spec-summary.medium .spec-label {
|
||||
color: var(--spec-medium-text);
|
||||
}
|
||||
|
||||
.bytecode-spec-summary.low {
|
||||
background: var(--spec-low-bg);
|
||||
border-left: 3px solid var(--spec-low);
|
||||
}
|
||||
.bytecode-spec-summary.low .spec-pct,
|
||||
.bytecode-spec-summary.low .spec-label {
|
||||
color: var(--spec-low-text);
|
||||
}
|
||||
|
||||
.bytecode-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 80px 80px;
|
||||
gap: 12px;
|
||||
padding: 4px 8px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--code-border);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.bytecode-expand-all {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--code-border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bytecode-expand-all:hover,
|
||||
.bytecode-expand-all.expanded {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.bytecode-expand-all .expand-icon {
|
||||
font-size: 10px;
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.bytecode-expand-all.expanded .expand-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
INSTRUCTION SPAN HIGHLIGHTING
|
||||
(triggered only from bytecode panel hover)
|
||||
======================================== */
|
||||
|
||||
/* Highlight from bytecode panel hover */
|
||||
.instr-span.highlight-from-bytecode {
|
||||
outline: 3px solid #ff6b6b !important;
|
||||
background-color: rgba(255, 107, 107, 0.4) !important;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Bytecode instruction row */
|
||||
.bytecode-instruction {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 80px 80px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
margin: 2px 0;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.bytecode-instruction:hover,
|
||||
.bytecode-instruction.highlight {
|
||||
background-color: rgba(55, 118, 171, 0.15);
|
||||
}
|
||||
|
||||
.bytecode-instruction[data-locations] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bytecode-instruction[data-locations]:hover {
|
||||
background-color: rgba(255, 107, 107, 0.2);
|
||||
}
|
||||
|
||||
.bytecode-opname {
|
||||
font-weight: 600;
|
||||
font-family: var(--font-mono);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bytecode-opname.specialized {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .bytecode-opname.specialized {
|
||||
color: #81c784;
|
||||
}
|
||||
|
||||
.bytecode-opname .base-op {
|
||||
color: var(--code-text-muted);
|
||||
font-weight: normal;
|
||||
font-size: 0.9em;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.bytecode-samples {
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.bytecode-samples.hot {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.bytecode-heatbar {
|
||||
width: 60px;
|
||||
height: 12px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--code-border);
|
||||
}
|
||||
|
||||
.bytecode-heatbar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #00d4ff 0%, #ff6b00 100%);
|
||||
}
|
||||
|
||||
.specialization-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
font-size: 0.75em;
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
border-radius: 3px;
|
||||
margin-left: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .specialization-badge {
|
||||
background: rgba(129, 199, 132, 0.2);
|
||||
color: #81c784;
|
||||
}
|
||||
|
||||
.bytecode-empty {
|
||||
color: var(--code-text-muted);
|
||||
font-style: italic;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.bytecode-error {
|
||||
color: #d32f2f;
|
||||
font-style: italic;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
SPAN TOOLTIPS
|
||||
======================================== */
|
||||
|
||||
.span-tooltip {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 12px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
pointer-events: none;
|
||||
min-width: 160px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.span-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -7px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 7px 7px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--bg-primary) transparent transparent;
|
||||
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.span-tooltip-header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.span-tooltip-header.hot {
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.span-tooltip-header.warm {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.span-tooltip-header.cold {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.span-tooltip-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 4px 0;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.span-tooltip-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.span-tooltip-value {
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.span-tooltip-value.highlight {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.span-tooltip-section {
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.span-tooltip-opcode {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-secondary);
|
||||
padding: 3px 8px;
|
||||
margin: 2px 0;
|
||||
border-radius: var(--radius-sm);
|
||||
border-left: 2px solid var(--accent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,7 +289,6 @@ function toggleColorMode() {
|
|||
// ============================================================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Restore UI state (theme, etc.)
|
||||
restoreUIState();
|
||||
applyLineColors();
|
||||
|
||||
|
|
@ -308,19 +307,38 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
// Initialize toggle buttons
|
||||
const toggleColdBtn = document.getElementById('toggle-cold');
|
||||
if (toggleColdBtn) {
|
||||
toggleColdBtn.addEventListener('click', toggleColdCode);
|
||||
}
|
||||
if (toggleColdBtn) toggleColdBtn.addEventListener('click', toggleColdCode);
|
||||
|
||||
const colorModeBtn = document.getElementById('toggle-color-mode');
|
||||
if (colorModeBtn) {
|
||||
colorModeBtn.addEventListener('click', toggleColorMode);
|
||||
if (colorModeBtn) colorModeBtn.addEventListener('click', toggleColorMode);
|
||||
|
||||
// Initialize specialization view toggle (hide if no bytecode data)
|
||||
const hasBytecode = document.querySelectorAll('.bytecode-toggle').length > 0;
|
||||
|
||||
const specViewBtn = document.getElementById('toggle-spec-view');
|
||||
if (specViewBtn) {
|
||||
if (hasBytecode) {
|
||||
specViewBtn.addEventListener('click', toggleSpecView);
|
||||
} else {
|
||||
specViewBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Build scroll marker
|
||||
setTimeout(buildScrollMarker, 200);
|
||||
// Initialize expand-all bytecode button
|
||||
const expandAllBtn = document.getElementById('toggle-all-bytecode');
|
||||
if (expandAllBtn) {
|
||||
if (hasBytecode) {
|
||||
expandAllBtn.addEventListener('click', toggleAllBytecode);
|
||||
} else {
|
||||
expandAllBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Setup scroll-to-line behavior
|
||||
// Initialize span tooltips
|
||||
initSpanTooltips();
|
||||
|
||||
// Build scroll marker and scroll to target
|
||||
setTimeout(buildScrollMarker, 200);
|
||||
setTimeout(scrollToTargetLine, 100);
|
||||
});
|
||||
|
||||
|
|
@ -331,6 +349,400 @@ document.addEventListener('click', e => {
|
|||
}
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// SPECIALIZATION VIEW TOGGLE
|
||||
// ========================================
|
||||
|
||||
let specViewEnabled = false;
|
||||
|
||||
/**
|
||||
* Calculate heat color for given intensity (0-1)
|
||||
* Hot spans (>30%) get warm orange, cold spans get dimmed gray
|
||||
* @param {number} intensity - Value between 0 and 1
|
||||
* @returns {string} rgba color string
|
||||
*/
|
||||
function calculateHeatColor(intensity) {
|
||||
// Hot threshold: only spans with >30% of max samples get color
|
||||
if (intensity > 0.3) {
|
||||
// Normalize intensity above threshold to 0-1
|
||||
const normalizedIntensity = (intensity - 0.3) / 0.7;
|
||||
// Warm orange-red with increasing opacity for hotter spans
|
||||
const alpha = 0.25 + normalizedIntensity * 0.35; // 0.25 to 0.6
|
||||
const hotColor = getComputedStyle(document.documentElement).getPropertyValue('--span-hot-base').trim();
|
||||
return `rgba(${hotColor}, ${alpha})`;
|
||||
} else if (intensity > 0) {
|
||||
// Cold spans: very subtle gray, almost invisible
|
||||
const coldColor = getComputedStyle(document.documentElement).getPropertyValue('--span-cold-base').trim();
|
||||
return `rgba(${coldColor}, 0.1)`;
|
||||
}
|
||||
return 'transparent';
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply intensity-based heat colors to source spans
|
||||
* Hot spans get orange highlight, cold spans get dimmed
|
||||
* @param {boolean} enable - Whether to enable or disable span coloring
|
||||
*/
|
||||
function applySpanHeatColors(enable) {
|
||||
document.querySelectorAll('.instr-span').forEach(span => {
|
||||
const samples = enable ? (parseInt(span.dataset.samples) || 0) : 0;
|
||||
if (samples > 0) {
|
||||
const intensity = samples / (parseInt(span.dataset.maxSamples) || 1);
|
||||
span.style.backgroundColor = calculateHeatColor(intensity);
|
||||
span.style.borderRadius = '2px';
|
||||
span.style.padding = '0 1px';
|
||||
span.style.cursor = 'pointer';
|
||||
} else {
|
||||
span.style.cssText = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SPAN TOOLTIPS
|
||||
// ========================================
|
||||
|
||||
let activeTooltip = null;
|
||||
|
||||
/**
|
||||
* Create and show tooltip for a span
|
||||
*/
|
||||
function showSpanTooltip(span) {
|
||||
hideSpanTooltip();
|
||||
|
||||
const samples = parseInt(span.dataset.samples) || 0;
|
||||
const maxSamples = parseInt(span.dataset.maxSamples) || 1;
|
||||
const pct = span.dataset.pct || '0';
|
||||
const opcodes = span.dataset.opcodes || '';
|
||||
|
||||
if (samples === 0) return;
|
||||
|
||||
const intensity = samples / maxSamples;
|
||||
const isHot = intensity > 0.7;
|
||||
const isWarm = intensity > 0.3;
|
||||
const hotnessText = isHot ? 'Hot' : isWarm ? 'Warm' : 'Cold';
|
||||
const hotnessClass = isHot ? 'hot' : isWarm ? 'warm' : 'cold';
|
||||
|
||||
// Build opcodes rows - each opcode on its own row
|
||||
let opcodesHtml = '';
|
||||
if (opcodes) {
|
||||
const opcodeList = opcodes.split(',').map(op => op.trim()).filter(op => op);
|
||||
if (opcodeList.length > 0) {
|
||||
opcodesHtml = `
|
||||
<div class="span-tooltip-section">Opcodes:</div>
|
||||
${opcodeList.map(op => `<div class="span-tooltip-opcode">${op}</div>`).join('')}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.className = 'span-tooltip';
|
||||
tooltip.innerHTML = `
|
||||
<div class="span-tooltip-header ${hotnessClass}">${hotnessText}</div>
|
||||
<div class="span-tooltip-row">
|
||||
<span class="span-tooltip-label">Samples:</span>
|
||||
<span class="span-tooltip-value${isHot ? ' highlight' : ''}">${samples.toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="span-tooltip-row">
|
||||
<span class="span-tooltip-label">% of line:</span>
|
||||
<span class="span-tooltip-value">${pct}%</span>
|
||||
</div>
|
||||
${opcodesHtml}
|
||||
`;
|
||||
|
||||
document.body.appendChild(tooltip);
|
||||
activeTooltip = tooltip;
|
||||
|
||||
// Position tooltip above the span
|
||||
const rect = span.getBoundingClientRect();
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
|
||||
let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
|
||||
let top = rect.top - tooltipRect.height - 8;
|
||||
|
||||
// Keep tooltip in viewport
|
||||
if (left < 5) left = 5;
|
||||
if (left + tooltipRect.width > window.innerWidth - 5) {
|
||||
left = window.innerWidth - tooltipRect.width - 5;
|
||||
}
|
||||
if (top < 5) {
|
||||
top = rect.bottom + 8; // Show below if no room above
|
||||
}
|
||||
|
||||
tooltip.style.left = `${left + window.scrollX}px`;
|
||||
tooltip.style.top = `${top + window.scrollY}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide active tooltip
|
||||
*/
|
||||
function hideSpanTooltip() {
|
||||
if (activeTooltip) {
|
||||
activeTooltip.remove();
|
||||
activeTooltip = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize span tooltip handlers
|
||||
*/
|
||||
function initSpanTooltips() {
|
||||
document.addEventListener('mouseover', (e) => {
|
||||
const span = e.target.closest('.instr-span');
|
||||
if (span && specViewEnabled) {
|
||||
showSpanTooltip(span);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseout', (e) => {
|
||||
const span = e.target.closest('.instr-span');
|
||||
if (span) {
|
||||
hideSpanTooltip();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSpecView() {
|
||||
specViewEnabled = !specViewEnabled;
|
||||
const lines = document.querySelectorAll('.code-line');
|
||||
|
||||
if (specViewEnabled) {
|
||||
lines.forEach(line => {
|
||||
const specColor = line.getAttribute('data-spec-color');
|
||||
line.style.background = specColor || 'transparent';
|
||||
});
|
||||
} else {
|
||||
applyLineColors();
|
||||
}
|
||||
|
||||
applySpanHeatColors(specViewEnabled);
|
||||
updateToggleUI('toggle-spec-view', specViewEnabled);
|
||||
|
||||
// Disable/enable color mode toggle based on spec view state
|
||||
const colorModeToggle = document.getElementById('toggle-color-mode');
|
||||
if (colorModeToggle) {
|
||||
colorModeToggle.classList.toggle('disabled', specViewEnabled);
|
||||
}
|
||||
|
||||
buildScrollMarker();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// BYTECODE PANEL TOGGLE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Toggle bytecode panel visibility for a source line
|
||||
* @param {HTMLElement} button - The toggle button that was clicked
|
||||
*/
|
||||
function toggleBytecode(button) {
|
||||
const lineDiv = button.closest('.code-line');
|
||||
const lineId = lineDiv.id;
|
||||
const lineNum = lineId.replace('line-', '');
|
||||
const panel = document.getElementById(`bytecode-${lineNum}`);
|
||||
|
||||
if (!panel) return;
|
||||
|
||||
const isExpanded = panel.style.display !== 'none';
|
||||
|
||||
if (isExpanded) {
|
||||
panel.style.display = 'none';
|
||||
button.classList.remove('expanded');
|
||||
button.innerHTML = '▶'; // 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));
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
class="toolbar-btn theme-toggle"
|
||||
onclick="toggleTheme()"
|
||||
title="Toggle theme"
|
||||
aria-label="Toggle theme"
|
||||
id="theme-btn"
|
||||
>☾</button>
|
||||
</div>
|
||||
|
|
@ -64,18 +65,30 @@
|
|||
<div class="legend-gradient"></div>
|
||||
<div class="legend-labels">
|
||||
<span>Cold</span>
|
||||
<span>→</span>
|
||||
<span aria-hidden="true">→</span>
|
||||
<span>Hot</span>
|
||||
</div>
|
||||
<div class="toggle-switch" id="toggle-color-mode" title="Toggle between self time and total time coloring">
|
||||
<span class="toggle-label active">Self Time</span>
|
||||
<div class="toggle-track"></div>
|
||||
<span class="toggle-label">Total Time</span>
|
||||
</div>
|
||||
<div class="toggle-switch" id="toggle-cold" title="Toggle visibility of lines with zero samples">
|
||||
<span class="toggle-label active">Show All</span>
|
||||
<div class="toggle-track"></div>
|
||||
<span class="toggle-label">Hot Only</span>
|
||||
<div class="legend-separator" aria-hidden="true"></div>
|
||||
<div class="legend-controls">
|
||||
<div class="toggle-switch" id="toggle-color-mode" title="Toggle between self time and total time coloring">
|
||||
<span class="toggle-label active" data-text="Self Time">Self Time</span>
|
||||
<div class="toggle-track"></div>
|
||||
<span class="toggle-label" data-text="Total Time">Total Time</span>
|
||||
</div>
|
||||
<div class="toggle-switch" id="toggle-cold" title="Toggle visibility of lines with zero samples">
|
||||
<span class="toggle-label active" data-text="Show All">Show All</span>
|
||||
<div class="toggle-track"></div>
|
||||
<span class="toggle-label" data-text="Hot Only">Hot Only</span>
|
||||
</div>
|
||||
<div class="toggle-switch" id="toggle-spec-view" title="Color lines by specialization level (requires bytecode data)">
|
||||
<span class="toggle-label active" data-text="Heat">Heat</span>
|
||||
<div class="toggle-track"></div>
|
||||
<span class="toggle-label" data-text="Specialization">Specialization</span>
|
||||
</div>
|
||||
<div class="legend-separator" aria-hidden="true"></div>
|
||||
<button class="bytecode-expand-all" id="toggle-all-bytecode" title="Expand/collapse all bytecode panels (keyboard: b)">
|
||||
<span class="expand-icon" aria-hidden="true">▶</span> Bytecode
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ :root {
|
|||
--topbar-height: 56px;
|
||||
--statusbar-height: 32px;
|
||||
|
||||
/* Border radius */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-normal: 0.25s ease;
|
||||
|
|
@ -79,6 +84,21 @@ :root, [data-theme="light"] {
|
|||
--nav-caller-hover: #1d4ed8;
|
||||
--nav-callee: #dc2626;
|
||||
--nav-callee-hover: #b91c1c;
|
||||
|
||||
/* Specialization status colors */
|
||||
--spec-high: #4caf50;
|
||||
--spec-high-text: #2e7d32;
|
||||
--spec-high-bg: rgba(76, 175, 80, 0.15);
|
||||
--spec-medium: #ff9800;
|
||||
--spec-medium-text: #e65100;
|
||||
--spec-medium-bg: rgba(255, 152, 0, 0.15);
|
||||
--spec-low: #9e9e9e;
|
||||
--spec-low-text: #616161;
|
||||
--spec-low-bg: rgba(158, 158, 158, 0.15);
|
||||
|
||||
/* Heatmap span highlighting colors */
|
||||
--span-hot-base: 255, 100, 50;
|
||||
--span-cold-base: 150, 150, 150;
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
|
|
@ -103,15 +123,15 @@ [data-theme="dark"] {
|
|||
|
||||
--header-gradient: linear-gradient(135deg, #21262d 0%, #30363d 100%);
|
||||
|
||||
/* Dark mode heat palette - dark blue to teal to yellow to orange (cold to hot) */
|
||||
--heat-1: #4a7ba7;
|
||||
--heat-2: #5a9fa8;
|
||||
--heat-3: #6ab5b5;
|
||||
--heat-4: #7ec488;
|
||||
--heat-5: #a0d878;
|
||||
--heat-6: #c4de6a;
|
||||
--heat-7: #f4d44d;
|
||||
--heat-8: #ff6b35;
|
||||
/* Dark mode heat palette - muted colors that provide sufficient contrast with light text */
|
||||
--heat-1: rgba(74, 123, 167, 0.35);
|
||||
--heat-2: rgba(90, 159, 168, 0.38);
|
||||
--heat-3: rgba(106, 181, 181, 0.40);
|
||||
--heat-4: rgba(126, 196, 136, 0.42);
|
||||
--heat-5: rgba(160, 216, 120, 0.45);
|
||||
--heat-6: rgba(196, 222, 106, 0.48);
|
||||
--heat-7: rgba(244, 212, 77, 0.50);
|
||||
--heat-8: rgba(255, 107, 53, 0.55);
|
||||
|
||||
/* Code view specific - dark mode */
|
||||
--code-bg: #0d1117;
|
||||
|
|
@ -126,6 +146,21 @@ [data-theme="dark"] {
|
|||
--nav-caller-hover: #4184e4;
|
||||
--nav-callee: #f87171;
|
||||
--nav-callee-hover: #e53e3e;
|
||||
|
||||
/* Specialization status colors - dark theme */
|
||||
--spec-high: #81c784;
|
||||
--spec-high-text: #81c784;
|
||||
--spec-high-bg: rgba(129, 199, 132, 0.2);
|
||||
--spec-medium: #ffb74d;
|
||||
--spec-medium-text: #ffb74d;
|
||||
--spec-medium-bg: rgba(255, 183, 77, 0.2);
|
||||
--spec-low: #bdbdbd;
|
||||
--spec-low-text: #9e9e9e;
|
||||
--spec-low-bg: rgba(189, 189, 189, 0.15);
|
||||
|
||||
/* Heatmap span highlighting colors - dark theme */
|
||||
--span-hot-base: 255, 107, 53;
|
||||
--span-cold-base: 189, 189, 189;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -195,6 +195,12 @@ def _add_sampling_options(parser):
|
|||
dest="gc",
|
||||
help='Don\'t include artificial "<GC>" frames to denote active garbage collection',
|
||||
)
|
||||
sampling_group.add_argument(
|
||||
"--opcodes",
|
||||
action="store_true",
|
||||
help="Gather bytecode opcode information for instruction-level profiling "
|
||||
"(shows which bytecode instructions are executing, including specializations).",
|
||||
)
|
||||
sampling_group.add_argument(
|
||||
"--async-aware",
|
||||
action="store_true",
|
||||
|
|
@ -316,13 +322,15 @@ def _sort_to_mode(sort_choice):
|
|||
return sort_map.get(sort_choice, SORT_MODE_NSAMPLES)
|
||||
|
||||
|
||||
def _create_collector(format_type, interval, skip_idle):
|
||||
def _create_collector(format_type, interval, skip_idle, opcodes=False):
|
||||
"""Create the appropriate collector based on format type.
|
||||
|
||||
Args:
|
||||
format_type: The output format ('pstats', 'collapsed', 'flamegraph', 'gecko')
|
||||
format_type: The output format ('pstats', 'collapsed', 'flamegraph', 'gecko', 'heatmap')
|
||||
interval: Sampling interval in microseconds
|
||||
skip_idle: Whether to skip idle samples
|
||||
opcodes: Whether to collect opcode information (only used by gecko format
|
||||
for creating interval markers in Firefox Profiler)
|
||||
|
||||
Returns:
|
||||
A collector instance of the appropriate type
|
||||
|
|
@ -332,8 +340,10 @@ def _create_collector(format_type, interval, skip_idle):
|
|||
raise ValueError(f"Unknown format: {format_type}")
|
||||
|
||||
# Gecko format never skips idle (it needs both GIL and CPU data)
|
||||
# and is the only format that uses opcodes for interval markers
|
||||
if format_type == "gecko":
|
||||
skip_idle = False
|
||||
return collector_class(interval, skip_idle=skip_idle, opcodes=opcodes)
|
||||
|
||||
return collector_class(interval, skip_idle=skip_idle)
|
||||
|
||||
|
|
@ -446,6 +456,13 @@ def _validate_args(args, parser):
|
|||
"Gecko format automatically includes both GIL-holding and CPU status analysis."
|
||||
)
|
||||
|
||||
# Validate --opcodes is only used with compatible formats
|
||||
opcodes_compatible_formats = ("live", "gecko", "flamegraph", "heatmap")
|
||||
if args.opcodes and args.format not in opcodes_compatible_formats:
|
||||
parser.error(
|
||||
f"--opcodes is only compatible with {', '.join('--' + f for f in opcodes_compatible_formats)}."
|
||||
)
|
||||
|
||||
# Validate pstats-specific options are only used with pstats format
|
||||
if args.format != "pstats":
|
||||
issues = []
|
||||
|
|
@ -593,7 +610,7 @@ def _handle_attach(args):
|
|||
)
|
||||
|
||||
# Create the appropriate collector
|
||||
collector = _create_collector(args.format, args.interval, skip_idle)
|
||||
collector = _create_collector(args.format, args.interval, skip_idle, args.opcodes)
|
||||
|
||||
# Sample the process
|
||||
collector = sample(
|
||||
|
|
@ -606,6 +623,7 @@ def _handle_attach(args):
|
|||
async_aware=args.async_mode if args.async_aware else None,
|
||||
native=args.native,
|
||||
gc=args.gc,
|
||||
opcodes=args.opcodes,
|
||||
)
|
||||
|
||||
# Handle output
|
||||
|
|
@ -641,7 +659,7 @@ def _handle_run(args):
|
|||
)
|
||||
|
||||
# Create the appropriate collector
|
||||
collector = _create_collector(args.format, args.interval, skip_idle)
|
||||
collector = _create_collector(args.format, args.interval, skip_idle, args.opcodes)
|
||||
|
||||
# Profile the subprocess
|
||||
try:
|
||||
|
|
@ -655,6 +673,7 @@ def _handle_run(args):
|
|||
async_aware=args.async_mode if args.async_aware else None,
|
||||
native=args.native,
|
||||
gc=args.gc,
|
||||
opcodes=args.opcodes,
|
||||
)
|
||||
|
||||
# Handle output
|
||||
|
|
@ -685,6 +704,7 @@ def _handle_live_attach(args, pid):
|
|||
limit=20, # Default limit
|
||||
pid=pid,
|
||||
mode=mode,
|
||||
opcodes=args.opcodes,
|
||||
async_aware=args.async_mode if args.async_aware else None,
|
||||
)
|
||||
|
||||
|
|
@ -699,6 +719,7 @@ def _handle_live_attach(args, pid):
|
|||
async_aware=args.async_mode if args.async_aware else None,
|
||||
native=args.native,
|
||||
gc=args.gc,
|
||||
opcodes=args.opcodes,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -726,6 +747,7 @@ def _handle_live_run(args):
|
|||
limit=20, # Default limit
|
||||
pid=process.pid,
|
||||
mode=mode,
|
||||
opcodes=args.opcodes,
|
||||
async_aware=args.async_mode if args.async_aware else None,
|
||||
)
|
||||
|
||||
|
|
@ -741,6 +763,7 @@ def _handle_live_run(args):
|
|||
async_aware=args.async_mode if args.async_aware else None,
|
||||
native=args.native,
|
||||
gc=args.gc,
|
||||
opcodes=args.opcodes,
|
||||
)
|
||||
finally:
|
||||
# Clean up the subprocess
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from .constants import (
|
||||
DEFAULT_LOCATION,
|
||||
THREAD_STATUS_HAS_GIL,
|
||||
THREAD_STATUS_ON_CPU,
|
||||
THREAD_STATUS_GIL_REQUESTED,
|
||||
|
|
@ -12,6 +13,34 @@
|
|||
# Fallback definition if _remote_debugging is not available
|
||||
FrameInfo = None
|
||||
|
||||
|
||||
def normalize_location(location):
|
||||
"""Normalize location to a 4-tuple format.
|
||||
|
||||
Args:
|
||||
location: tuple (lineno, end_lineno, col_offset, end_col_offset) or None
|
||||
|
||||
Returns:
|
||||
tuple: (lineno, end_lineno, col_offset, end_col_offset)
|
||||
"""
|
||||
if location is None:
|
||||
return DEFAULT_LOCATION
|
||||
return location
|
||||
|
||||
|
||||
def extract_lineno(location):
|
||||
"""Extract lineno from location.
|
||||
|
||||
Args:
|
||||
location: tuple (lineno, end_lineno, col_offset, end_col_offset) or None
|
||||
|
||||
Returns:
|
||||
int: The line number (0 for synthetic frames)
|
||||
"""
|
||||
if location is None:
|
||||
return 0
|
||||
return location[0]
|
||||
|
||||
class Collector(ABC):
|
||||
@abstractmethod
|
||||
def collect(self, stack_frames):
|
||||
|
|
@ -117,11 +146,11 @@ def _build_linear_stacks(self, leaf_task_ids, task_map, child_to_parent):
|
|||
selected_parent, parent_count = parent_info
|
||||
if parent_count > 1:
|
||||
task_name = f"{task_name} ({parent_count} parents)"
|
||||
frames.append(FrameInfo(("<task>", 0, task_name)))
|
||||
frames.append(FrameInfo(("<task>", None, task_name, None)))
|
||||
current_id = selected_parent
|
||||
else:
|
||||
# Root task - no parent
|
||||
frames.append(FrameInfo(("<task>", 0, task_name)))
|
||||
frames.append(FrameInfo(("<task>", None, task_name, None)))
|
||||
current_id = None
|
||||
|
||||
# Yield the complete stack if we collected any frames
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@
|
|||
SORT_MODE_CUMUL_PCT = 4
|
||||
SORT_MODE_NSAMPLES_CUMUL = 5
|
||||
|
||||
# Default location for synthetic frames (native, GC) that have no source location
|
||||
# Format: (lineno, end_lineno, col_offset, end_col_offset)
|
||||
DEFAULT_LOCATION = (0, 0, -1, -1)
|
||||
|
||||
# Thread status flags
|
||||
try:
|
||||
from _remote_debugging import (
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import time
|
||||
|
||||
from .collector import Collector
|
||||
from .opcode_utils import get_opcode_info, format_opcode
|
||||
try:
|
||||
from _remote_debugging import THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED
|
||||
except ImportError:
|
||||
|
|
@ -26,6 +27,7 @@
|
|||
{"name": "GIL", "color": "green", "subcategories": ["Other"]},
|
||||
{"name": "CPU", "color": "purple", "subcategories": ["Other"]},
|
||||
{"name": "Code Type", "color": "red", "subcategories": ["Other"]},
|
||||
{"name": "Opcodes", "color": "magenta", "subcategories": ["Other"]},
|
||||
]
|
||||
|
||||
# Category indices
|
||||
|
|
@ -36,6 +38,7 @@
|
|||
CATEGORY_GIL = 4
|
||||
CATEGORY_CPU = 5
|
||||
CATEGORY_CODE_TYPE = 6
|
||||
CATEGORY_OPCODES = 7
|
||||
|
||||
# Subcategory indices
|
||||
DEFAULT_SUBCATEGORY = 0
|
||||
|
|
@ -56,9 +59,10 @@
|
|||
|
||||
|
||||
class GeckoCollector(Collector):
|
||||
def __init__(self, sample_interval_usec, *, skip_idle=False):
|
||||
def __init__(self, sample_interval_usec, *, skip_idle=False, opcodes=False):
|
||||
self.sample_interval_usec = sample_interval_usec
|
||||
self.skip_idle = skip_idle
|
||||
self.opcodes_enabled = opcodes
|
||||
self.start_time = time.time() * 1000 # milliseconds since epoch
|
||||
|
||||
# Global string table (shared across all threads)
|
||||
|
|
@ -91,6 +95,9 @@ def __init__(self, sample_interval_usec, *, skip_idle=False):
|
|||
# Track which threads have been initialized for state tracking
|
||||
self.initialized_threads = set()
|
||||
|
||||
# Opcode state tracking per thread: tid -> (opcode, lineno, col_offset, funcname, filename, start_time)
|
||||
self.opcode_state = {}
|
||||
|
||||
def _track_state_transition(self, tid, condition, active_dict, inactive_dict,
|
||||
active_name, inactive_name, category, current_time):
|
||||
"""Track binary state transitions and emit markers.
|
||||
|
|
@ -232,6 +239,30 @@ def collect(self, stack_frames):
|
|||
samples["time"].append(current_time)
|
||||
samples["eventDelay"].append(None)
|
||||
|
||||
# Track opcode state changes for interval markers (leaf frame only)
|
||||
if self.opcodes_enabled:
|
||||
leaf_frame = frames[0]
|
||||
filename, location, funcname, opcode = leaf_frame
|
||||
if isinstance(location, tuple):
|
||||
lineno, _, col_offset, _ = location
|
||||
else:
|
||||
lineno = location
|
||||
col_offset = -1
|
||||
|
||||
current_state = (opcode, lineno, col_offset, funcname, filename)
|
||||
|
||||
if tid not in self.opcode_state:
|
||||
# First observation - start tracking
|
||||
self.opcode_state[tid] = (*current_state, current_time)
|
||||
elif self.opcode_state[tid][:5] != current_state:
|
||||
# State changed - emit marker for previous state
|
||||
prev_opcode, prev_lineno, prev_col, prev_funcname, prev_filename, prev_start = self.opcode_state[tid]
|
||||
self._add_opcode_interval_marker(
|
||||
tid, prev_opcode, prev_lineno, prev_col, prev_funcname, prev_start, current_time
|
||||
)
|
||||
# Start tracking new state
|
||||
self.opcode_state[tid] = (*current_state, current_time)
|
||||
|
||||
self.sample_count += 1
|
||||
|
||||
def _create_thread(self, tid):
|
||||
|
|
@ -369,6 +400,36 @@ def _add_marker(self, tid, name, start_time, end_time, category):
|
|||
"tid": tid
|
||||
})
|
||||
|
||||
def _add_opcode_interval_marker(self, tid, opcode, lineno, col_offset, funcname, start_time, end_time):
|
||||
"""Add an interval marker for opcode execution span."""
|
||||
if tid not in self.threads or opcode is None:
|
||||
return
|
||||
|
||||
thread_data = self.threads[tid]
|
||||
opcode_info = get_opcode_info(opcode)
|
||||
# Use formatted opcode name (with base opcode for specialized ones)
|
||||
formatted_opname = format_opcode(opcode)
|
||||
|
||||
name_idx = self._intern_string(formatted_opname)
|
||||
|
||||
markers = thread_data["markers"]
|
||||
markers["name"].append(name_idx)
|
||||
markers["startTime"].append(start_time)
|
||||
markers["endTime"].append(end_time)
|
||||
markers["phase"].append(1) # 1 = interval marker
|
||||
markers["category"].append(CATEGORY_OPCODES)
|
||||
markers["data"].append({
|
||||
"type": "Opcode",
|
||||
"opcode": opcode,
|
||||
"opname": formatted_opname,
|
||||
"base_opname": opcode_info["base_opname"],
|
||||
"is_specialized": opcode_info["is_specialized"],
|
||||
"line": lineno,
|
||||
"column": col_offset if col_offset >= 0 else None,
|
||||
"function": funcname,
|
||||
"duration": end_time - start_time,
|
||||
})
|
||||
|
||||
def _process_stack(self, thread_data, frames):
|
||||
"""Process a stack and return the stack index."""
|
||||
if not frames:
|
||||
|
|
@ -386,17 +447,25 @@ def _process_stack(self, thread_data, frames):
|
|||
prefix_stack_idx = None
|
||||
|
||||
for frame_tuple in reversed(frames):
|
||||
# frame_tuple is (filename, lineno, funcname)
|
||||
filename, lineno, funcname = frame_tuple
|
||||
# frame_tuple is (filename, location, funcname, opcode)
|
||||
# location is (lineno, end_lineno, col_offset, end_col_offset) or just lineno
|
||||
filename, location, funcname, opcode = frame_tuple
|
||||
if isinstance(location, tuple):
|
||||
lineno, end_lineno, col_offset, end_col_offset = location
|
||||
else:
|
||||
# Legacy format: location is just lineno
|
||||
lineno = location
|
||||
col_offset = -1
|
||||
end_col_offset = -1
|
||||
|
||||
# Get or create function
|
||||
func_idx = self._get_or_create_func(
|
||||
thread_data, filename, funcname, lineno
|
||||
)
|
||||
|
||||
# Get or create frame
|
||||
# Get or create frame (include column for precise source location)
|
||||
frame_idx = self._get_or_create_frame(
|
||||
thread_data, func_idx, lineno
|
||||
thread_data, func_idx, lineno, col_offset
|
||||
)
|
||||
|
||||
# Check stack cache
|
||||
|
|
@ -494,10 +563,11 @@ def _get_or_create_resource(self, thread_data, filename):
|
|||
resource_cache[filename] = resource_idx
|
||||
return resource_idx
|
||||
|
||||
def _get_or_create_frame(self, thread_data, func_idx, lineno):
|
||||
def _get_or_create_frame(self, thread_data, func_idx, lineno, col_offset=-1):
|
||||
"""Get or create a frame entry."""
|
||||
frame_cache = thread_data["_frameCache"]
|
||||
frame_key = (func_idx, lineno)
|
||||
# Include column in cache key for precise frame identification
|
||||
frame_key = (func_idx, lineno, col_offset if col_offset >= 0 else None)
|
||||
|
||||
if frame_key in frame_cache:
|
||||
return frame_cache[frame_key]
|
||||
|
|
@ -531,7 +601,8 @@ def _get_or_create_frame(self, thread_data, func_idx, lineno):
|
|||
frame_inner_window_ids.append(None)
|
||||
frame_implementations.append(None)
|
||||
frame_lines.append(lineno if lineno else None)
|
||||
frame_columns.append(None)
|
||||
# Store column offset if available (>= 0), otherwise None
|
||||
frame_columns.append(col_offset if col_offset >= 0 else None)
|
||||
frame_optimizations.append(None)
|
||||
|
||||
frame_cache[frame_key] = frame_idx
|
||||
|
|
@ -558,6 +629,12 @@ def _finalize_markers(self):
|
|||
self._add_marker(tid, marker_name, state_dict[tid], end_time, category)
|
||||
del state_dict[tid]
|
||||
|
||||
# Close any open opcode markers
|
||||
for tid, state in list(self.opcode_state.items()):
|
||||
opcode, lineno, col_offset, funcname, filename, start_time = state
|
||||
self._add_opcode_interval_marker(tid, opcode, lineno, col_offset, funcname, start_time, end_time)
|
||||
self.opcode_state.clear()
|
||||
|
||||
def export(self, filename):
|
||||
"""Export the profile to a Gecko JSON file."""
|
||||
|
||||
|
|
@ -600,6 +677,31 @@ def spin():
|
|||
f"Open in Firefox Profiler: https://profiler.firefox.com/"
|
||||
)
|
||||
|
||||
def _build_marker_schema(self):
|
||||
"""Build marker schema definitions for Firefox Profiler."""
|
||||
schema = []
|
||||
|
||||
# Opcode marker schema (only if opcodes enabled)
|
||||
if self.opcodes_enabled:
|
||||
schema.append({
|
||||
"name": "Opcode",
|
||||
"display": ["marker-table", "marker-chart"],
|
||||
"tooltipLabel": "{marker.data.opname}",
|
||||
"tableLabel": "{marker.data.opname} at line {marker.data.line}",
|
||||
"chartLabel": "{marker.data.opname}",
|
||||
"fields": [
|
||||
{"key": "opname", "label": "Opcode", "format": "string", "searchable": True},
|
||||
{"key": "base_opname", "label": "Base Opcode", "format": "string"},
|
||||
{"key": "is_specialized", "label": "Specialized", "format": "string"},
|
||||
{"key": "line", "label": "Line", "format": "integer"},
|
||||
{"key": "column", "label": "Column", "format": "integer"},
|
||||
{"key": "function", "label": "Function", "format": "string"},
|
||||
{"key": "duration", "label": "Duration", "format": "duration"},
|
||||
],
|
||||
})
|
||||
|
||||
return schema
|
||||
|
||||
def _build_profile(self):
|
||||
"""Build the complete profile structure in processed format."""
|
||||
# Convert thread data to final format
|
||||
|
|
@ -649,7 +751,7 @@ def _build_profile(self):
|
|||
"CPUName": "",
|
||||
"product": "Python",
|
||||
"symbolicated": True,
|
||||
"markerSchema": [],
|
||||
"markerSchema": self._build_marker_schema(),
|
||||
"importedFrom": "Tachyon Sampling Profiler",
|
||||
"extensions": {
|
||||
"id": [],
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
from typing import Dict, List, Tuple
|
||||
|
||||
from ._css_utils import get_combined_css
|
||||
from .collector import normalize_location, extract_lineno
|
||||
from .stack_collector import StackTraceCollector
|
||||
|
||||
|
||||
|
|
@ -463,19 +464,27 @@ def __init__(self, *args, **kwargs):
|
|||
self.line_self_samples = collections.Counter()
|
||||
self.file_self_samples = collections.defaultdict(collections.Counter)
|
||||
|
||||
# Call graph data structures for navigation
|
||||
self.call_graph = collections.defaultdict(list)
|
||||
self.callers_graph = collections.defaultdict(list)
|
||||
# Call graph data structures for navigation (sets for O(1) deduplication)
|
||||
self.call_graph = collections.defaultdict(set)
|
||||
self.callers_graph = collections.defaultdict(set)
|
||||
self.function_definitions = {}
|
||||
|
||||
# Edge counting for call path analysis
|
||||
self.edge_samples = collections.Counter()
|
||||
|
||||
# Bytecode-level tracking data structures
|
||||
# Track samples per (file, lineno) -> {opcode: {'count': N, 'locations': set()}}
|
||||
# Locations are deduplicated via set to minimize memory usage
|
||||
self.line_opcodes = collections.defaultdict(dict)
|
||||
|
||||
# Statistics and metadata
|
||||
self._total_samples = 0
|
||||
self._path_info = get_python_path_info()
|
||||
self.stats = {}
|
||||
|
||||
# Opcode collection flag
|
||||
self.opcodes_enabled = False
|
||||
|
||||
# Template loader (loads all templates once)
|
||||
self._template_loader = _TemplateLoader()
|
||||
|
||||
|
|
@ -509,26 +518,37 @@ def process_frames(self, frames, thread_id):
|
|||
"""Process stack frames and count samples per line.
|
||||
|
||||
Args:
|
||||
frames: List of frame tuples (filename, lineno, funcname)
|
||||
frames[0] is the leaf (top of stack, where execution is)
|
||||
frames: List of (filename, location, funcname, opcode) tuples in
|
||||
leaf-to-root order. location is (lineno, end_lineno, col_offset, end_col_offset).
|
||||
opcode is None if not gathered.
|
||||
thread_id: Thread ID for this stack trace
|
||||
"""
|
||||
self._total_samples += 1
|
||||
|
||||
# Count each line in the stack and build call graph
|
||||
for i, frame_info in enumerate(frames):
|
||||
filename, lineno, funcname = frame_info
|
||||
for i, (filename, location, funcname, opcode) in enumerate(frames):
|
||||
# Normalize location to 4-tuple format
|
||||
lineno, end_lineno, col_offset, end_col_offset = normalize_location(location)
|
||||
|
||||
if not self._is_valid_frame(filename, lineno):
|
||||
continue
|
||||
|
||||
# frames[0] is the leaf - where execution is actually happening
|
||||
is_leaf = (i == 0)
|
||||
self._record_line_sample(filename, lineno, funcname, is_leaf=is_leaf)
|
||||
self._record_line_sample(filename, lineno, funcname, is_leaf=(i == 0))
|
||||
|
||||
if opcode is not None:
|
||||
# Set opcodes_enabled flag when we first encounter opcode data
|
||||
self.opcodes_enabled = True
|
||||
self._record_bytecode_sample(filename, lineno, opcode,
|
||||
end_lineno, col_offset, end_col_offset)
|
||||
|
||||
# Build call graph for adjacent frames
|
||||
if i + 1 < len(frames):
|
||||
self._record_call_relationship(frames[i], frames[i + 1])
|
||||
next_frame = frames[i + 1]
|
||||
next_lineno = extract_lineno(next_frame[1])
|
||||
self._record_call_relationship(
|
||||
(filename, lineno, funcname),
|
||||
(next_frame[0], next_lineno, next_frame[2])
|
||||
)
|
||||
|
||||
def _is_valid_frame(self, filename, lineno):
|
||||
"""Check if a frame should be included in the heatmap."""
|
||||
|
|
@ -557,6 +577,79 @@ def _record_line_sample(self, filename, lineno, funcname, is_leaf=False):
|
|||
if funcname and (filename, funcname) not in self.function_definitions:
|
||||
self.function_definitions[(filename, funcname)] = lineno
|
||||
|
||||
def _record_bytecode_sample(self, filename, lineno, opcode,
|
||||
end_lineno=None, col_offset=None, end_col_offset=None):
|
||||
"""Record a sample for a specific bytecode instruction.
|
||||
|
||||
Args:
|
||||
filename: Source filename
|
||||
lineno: Line number
|
||||
opcode: Opcode number being executed
|
||||
end_lineno: End line number (may be -1 if not available)
|
||||
col_offset: Column offset in UTF-8 bytes (may be -1 if not available)
|
||||
end_col_offset: End column offset in UTF-8 bytes (may be -1 if not available)
|
||||
"""
|
||||
key = (filename, lineno)
|
||||
|
||||
# Initialize opcode entry if needed - use set for location deduplication
|
||||
if opcode not in self.line_opcodes[key]:
|
||||
self.line_opcodes[key][opcode] = {'count': 0, 'locations': set()}
|
||||
|
||||
self.line_opcodes[key][opcode]['count'] += 1
|
||||
|
||||
# Store unique location info if column offset is available (not -1)
|
||||
if col_offset is not None and col_offset >= 0:
|
||||
# Use tuple as set key for deduplication
|
||||
loc_key = (end_lineno, col_offset, end_col_offset)
|
||||
self.line_opcodes[key][opcode]['locations'].add(loc_key)
|
||||
|
||||
def _get_bytecode_data_for_line(self, filename, lineno):
|
||||
"""Get bytecode disassembly data for instructions on a specific line.
|
||||
|
||||
Args:
|
||||
filename: Source filename
|
||||
lineno: Line number
|
||||
|
||||
Returns:
|
||||
List of dicts with instruction info, sorted by samples descending
|
||||
"""
|
||||
from .opcode_utils import get_opcode_info, format_opcode
|
||||
|
||||
key = (filename, lineno)
|
||||
opcode_data = self.line_opcodes.get(key, {})
|
||||
|
||||
result = []
|
||||
for opcode, data in opcode_data.items():
|
||||
info = get_opcode_info(opcode)
|
||||
# Handle both old format (int count) and new format (dict with count/locations)
|
||||
if isinstance(data, dict):
|
||||
count = data.get('count', 0)
|
||||
raw_locations = data.get('locations', set())
|
||||
# Convert set of tuples to list of dicts for JSON serialization
|
||||
if isinstance(raw_locations, set):
|
||||
locations = [
|
||||
{'end_lineno': loc[0], 'col_offset': loc[1], 'end_col_offset': loc[2]}
|
||||
for loc in raw_locations
|
||||
]
|
||||
else:
|
||||
locations = raw_locations
|
||||
else:
|
||||
count = data
|
||||
locations = []
|
||||
|
||||
result.append({
|
||||
'opcode': opcode,
|
||||
'opname': format_opcode(opcode),
|
||||
'base_opname': info['base_opname'],
|
||||
'is_specialized': info['is_specialized'],
|
||||
'samples': count,
|
||||
'locations': locations,
|
||||
})
|
||||
|
||||
# Sort by samples descending, then by opcode number
|
||||
result.sort(key=lambda x: (-x['samples'], x['opcode']))
|
||||
return result
|
||||
|
||||
def _record_call_relationship(self, callee_frame, caller_frame):
|
||||
"""Record caller/callee relationship between adjacent frames."""
|
||||
callee_filename, callee_lineno, callee_funcname = callee_frame
|
||||
|
|
@ -571,17 +664,15 @@ def _record_call_relationship(self, callee_frame, caller_frame):
|
|||
(callee_filename, callee_funcname), callee_lineno
|
||||
)
|
||||
|
||||
# Record caller -> callee relationship
|
||||
# Record caller -> callee relationship (set handles deduplication)
|
||||
caller_key = (caller_filename, caller_lineno)
|
||||
callee_info = (callee_filename, callee_def_line, callee_funcname)
|
||||
if callee_info not in self.call_graph[caller_key]:
|
||||
self.call_graph[caller_key].append(callee_info)
|
||||
self.call_graph[caller_key].add(callee_info)
|
||||
|
||||
# Record callee <- caller relationship
|
||||
# Record callee <- caller relationship (set handles deduplication)
|
||||
callee_key = (callee_filename, callee_def_line)
|
||||
caller_info = (caller_filename, caller_lineno, caller_funcname)
|
||||
if caller_info not in self.callers_graph[callee_key]:
|
||||
self.callers_graph[callee_key].append(caller_info)
|
||||
self.callers_graph[callee_key].add(caller_info)
|
||||
|
||||
# Count this call edge for path analysis
|
||||
edge_key = (caller_key, callee_key)
|
||||
|
|
@ -851,31 +942,184 @@ def _build_line_html(self, line_num: int, line_content: str,
|
|||
cumulative_display = ""
|
||||
tooltip = ""
|
||||
|
||||
# Get bytecode data for this line (if any)
|
||||
bytecode_data = self._get_bytecode_data_for_line(filename, line_num)
|
||||
has_bytecode = len(bytecode_data) > 0 and cumulative_samples > 0
|
||||
|
||||
# Build bytecode toggle button if data is available
|
||||
bytecode_btn_html = ''
|
||||
bytecode_panel_html = ''
|
||||
if has_bytecode:
|
||||
bytecode_json = html.escape(json.dumps(bytecode_data))
|
||||
|
||||
# Calculate specialization percentage
|
||||
total_samples = sum(d['samples'] for d in bytecode_data)
|
||||
specialized_samples = sum(d['samples'] for d in bytecode_data if d['is_specialized'])
|
||||
spec_pct = int(100 * specialized_samples / total_samples) if total_samples > 0 else 0
|
||||
|
||||
bytecode_btn_html = (
|
||||
f'<button class="bytecode-toggle" data-bytecode=\'{bytecode_json}\' '
|
||||
f'data-spec-pct="{spec_pct}" '
|
||||
f'onclick="toggleBytecode(this)" title="Show bytecode">▶</button>'
|
||||
)
|
||||
bytecode_panel_html = f' <div class="bytecode-panel" id="bytecode-{line_num}" style="display:none;"></div>\n'
|
||||
elif self.opcodes_enabled:
|
||||
# Add invisible spacer to maintain consistent indentation when opcodes are enabled
|
||||
bytecode_btn_html = '<div class="bytecode-spacer"></div>'
|
||||
|
||||
# Get navigation buttons
|
||||
nav_buttons_html = self._build_navigation_buttons(filename, line_num)
|
||||
|
||||
# Build line HTML with intensity data attributes
|
||||
line_html = html.escape(line_content.rstrip('\n'))
|
||||
# Build line HTML with instruction highlights if available
|
||||
line_html = self._render_source_with_highlights(line_content, line_num,
|
||||
filename, bytecode_data)
|
||||
title_attr = f' title="{html.escape(tooltip)}"' if tooltip else ""
|
||||
|
||||
# Specialization color for toggle mode (green gradient based on spec %)
|
||||
spec_color_attr = ''
|
||||
if has_bytecode:
|
||||
spec_color = self._format_specialization_color(spec_pct)
|
||||
spec_color_attr = f'data-spec-color="{spec_color}" '
|
||||
|
||||
return (
|
||||
f' <div class="code-line" '
|
||||
f'data-self-intensity="{self_intensity:.3f}" '
|
||||
f'data-cumulative-intensity="{cumulative_intensity:.3f}" '
|
||||
f'{spec_color_attr}'
|
||||
f'id="line-{line_num}"{title_attr}>\n'
|
||||
f' <div class="line-number">{line_num}</div>\n'
|
||||
f' <div class="line-samples-self">{self_display}</div>\n'
|
||||
f' <div class="line-samples-cumulative">{cumulative_display}</div>\n'
|
||||
f' {bytecode_btn_html}\n'
|
||||
f' <div class="line-content">{line_html}</div>\n'
|
||||
f' {nav_buttons_html}\n'
|
||||
f' </div>\n'
|
||||
f'{bytecode_panel_html}'
|
||||
)
|
||||
|
||||
def _render_source_with_highlights(self, line_content: str, line_num: int,
|
||||
filename: str, bytecode_data: list) -> str:
|
||||
"""Render source line with instruction highlight spans.
|
||||
|
||||
Simple: collect ranges with sample counts, assign each byte position to
|
||||
smallest covering range, then emit spans for contiguous runs with sample data.
|
||||
"""
|
||||
import html as html_module
|
||||
|
||||
content = line_content.rstrip('\n')
|
||||
if not content:
|
||||
return ''
|
||||
|
||||
# Collect all (start, end) -> {samples, opcodes} mapping from instructions
|
||||
# Multiple instructions may share the same range, so we sum samples and collect opcodes
|
||||
range_data = {}
|
||||
for instr in bytecode_data:
|
||||
samples = instr.get('samples', 0)
|
||||
opname = instr.get('opname', '')
|
||||
for loc in instr.get('locations', []):
|
||||
if loc.get('end_lineno', line_num) == line_num:
|
||||
start, end = loc.get('col_offset', -1), loc.get('end_col_offset', -1)
|
||||
if start >= 0 and end >= 0:
|
||||
key = (start, end)
|
||||
if key not in range_data:
|
||||
range_data[key] = {'samples': 0, 'opcodes': []}
|
||||
range_data[key]['samples'] += samples
|
||||
if opname and opname not in range_data[key]['opcodes']:
|
||||
range_data[key]['opcodes'].append(opname)
|
||||
|
||||
if not range_data:
|
||||
return html_module.escape(content)
|
||||
|
||||
# For each byte position, find the smallest covering range
|
||||
byte_to_range = {}
|
||||
for (start, end) in range_data.keys():
|
||||
for pos in range(start, end):
|
||||
if pos not in byte_to_range:
|
||||
byte_to_range[pos] = (start, end)
|
||||
else:
|
||||
# Keep smaller range
|
||||
old_start, old_end = byte_to_range[pos]
|
||||
if (end - start) < (old_end - old_start):
|
||||
byte_to_range[pos] = (start, end)
|
||||
|
||||
# Calculate totals for percentage and intensity
|
||||
total_line_samples = sum(d['samples'] for d in range_data.values())
|
||||
max_range_samples = max(d['samples'] for d in range_data.values()) if range_data else 1
|
||||
|
||||
# Render character by character
|
||||
result = []
|
||||
byte_offset = 0
|
||||
char_idx = 0
|
||||
current_range = None
|
||||
span_chars = []
|
||||
|
||||
def flush_span():
|
||||
nonlocal span_chars, current_range
|
||||
if span_chars:
|
||||
text = html_module.escape(''.join(span_chars))
|
||||
if current_range:
|
||||
data = range_data.get(current_range, {'samples': 0, 'opcodes': []})
|
||||
samples = data['samples']
|
||||
opcodes = ', '.join(data['opcodes'][:3]) # Top 3 opcodes
|
||||
if len(data['opcodes']) > 3:
|
||||
opcodes += f" +{len(data['opcodes']) - 3} more"
|
||||
pct = int(100 * samples / total_line_samples) if total_line_samples > 0 else 0
|
||||
result.append(f'<span class="instr-span" '
|
||||
f'data-col-start="{current_range[0]}" '
|
||||
f'data-col-end="{current_range[1]}" '
|
||||
f'data-samples="{samples}" '
|
||||
f'data-max-samples="{max_range_samples}" '
|
||||
f'data-pct="{pct}" '
|
||||
f'data-opcodes="{html_module.escape(opcodes)}">{text}</span>')
|
||||
else:
|
||||
result.append(text)
|
||||
span_chars = []
|
||||
|
||||
while char_idx < len(content):
|
||||
char = content[char_idx]
|
||||
char_bytes = len(char.encode('utf-8'))
|
||||
char_range = byte_to_range.get(byte_offset)
|
||||
|
||||
if char_range != current_range:
|
||||
flush_span()
|
||||
current_range = char_range
|
||||
|
||||
span_chars.append(char)
|
||||
byte_offset += char_bytes
|
||||
char_idx += 1
|
||||
|
||||
flush_span()
|
||||
return ''.join(result)
|
||||
|
||||
def _format_specialization_color(self, spec_pct: int) -> str:
|
||||
"""Format specialization color based on percentage.
|
||||
|
||||
Uses a gradient from gray (0%) through orange (50%) to green (100%).
|
||||
"""
|
||||
# Normalize to 0-1
|
||||
ratio = spec_pct / 100.0
|
||||
|
||||
if ratio >= 0.5:
|
||||
# Orange to green (50-100%)
|
||||
t = (ratio - 0.5) * 2 # 0 to 1
|
||||
r = int(255 * (1 - t)) # 255 -> 0
|
||||
g = int(180 + 75 * t) # 180 -> 255
|
||||
b = int(50 * (1 - t)) # 50 -> 0
|
||||
else:
|
||||
# Gray to orange (0-50%)
|
||||
t = ratio * 2 # 0 to 1
|
||||
r = int(158 + 97 * t) # 158 -> 255
|
||||
g = int(158 + 22 * t) # 158 -> 180
|
||||
b = int(158 - 108 * t) # 158 -> 50
|
||||
|
||||
alpha = 0.15 + 0.25 * ratio # 0.15 to 0.4
|
||||
return f"rgba({r}, {g}, {b}, {alpha})"
|
||||
|
||||
def _build_navigation_buttons(self, filename: str, line_num: int) -> str:
|
||||
"""Build navigation buttons for callers/callees."""
|
||||
line_key = (filename, line_num)
|
||||
caller_list = self._deduplicate_by_function(self.callers_graph.get(line_key, []))
|
||||
callee_list = self._deduplicate_by_function(self.call_graph.get(line_key, []))
|
||||
caller_list = self._deduplicate_by_function(self.callers_graph.get(line_key, set()))
|
||||
callee_list = self._deduplicate_by_function(self.call_graph.get(line_key, set()))
|
||||
|
||||
# Get edge counts for each caller/callee
|
||||
callers_with_counts = self._get_edge_counts(line_key, caller_list, is_caller=True)
|
||||
|
|
@ -907,8 +1151,12 @@ def _get_edge_counts(self, line_key: Tuple[str, int],
|
|||
result.sort(key=lambda x: x[3], reverse=True)
|
||||
return result
|
||||
|
||||
def _deduplicate_by_function(self, items: List[Tuple[str, int, str]]) -> List[Tuple[str, int, str]]:
|
||||
"""Remove duplicate entries based on (file, function) key."""
|
||||
def _deduplicate_by_function(self, items) -> List[Tuple[str, int, str]]:
|
||||
"""Remove duplicate entries based on (file, function) key.
|
||||
|
||||
Args:
|
||||
items: Iterable of (file, line, func) tuples (set or list)
|
||||
"""
|
||||
seen = {}
|
||||
result = []
|
||||
for file, line, func in items:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
import time
|
||||
import _colorize
|
||||
|
||||
from ..collector import Collector
|
||||
from ..collector import Collector, extract_lineno
|
||||
from ..constants import (
|
||||
THREAD_STATUS_HAS_GIL,
|
||||
THREAD_STATUS_ON_CPU,
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
COLOR_PAIR_SORTED_HEADER,
|
||||
)
|
||||
from .display import CursesDisplay
|
||||
from .widgets import HeaderWidget, TableWidget, FooterWidget, HelpWidget
|
||||
from .widgets import HeaderWidget, TableWidget, FooterWidget, HelpWidget, OpcodePanel
|
||||
from .trend_tracker import TrendTracker
|
||||
|
||||
|
||||
|
|
@ -67,6 +67,11 @@ class ThreadData:
|
|||
sample_count: int = 0
|
||||
gc_frame_samples: int = 0
|
||||
|
||||
# Opcode statistics: {location: {opcode: count}}
|
||||
opcode_stats: dict = field(default_factory=lambda: collections.defaultdict(
|
||||
lambda: collections.defaultdict(int)
|
||||
))
|
||||
|
||||
def increment_status_flag(self, status_flags):
|
||||
"""Update status counts based on status bit flags."""
|
||||
if status_flags & THREAD_STATUS_HAS_GIL:
|
||||
|
|
@ -103,6 +108,7 @@ def __init__(
|
|||
pid=None,
|
||||
display=None,
|
||||
mode=None,
|
||||
opcodes=False,
|
||||
async_aware=None,
|
||||
):
|
||||
"""
|
||||
|
|
@ -116,6 +122,7 @@ def __init__(
|
|||
pid: Process ID being profiled
|
||||
display: DisplayInterface implementation (None means curses will be used)
|
||||
mode: Profiling mode ('cpu', 'gil', etc.) - affects what stats are shown
|
||||
opcodes: Whether to show opcode panel (requires --opcodes flag)
|
||||
async_aware: Async tracing mode - None (sync only), "all" or "running"
|
||||
"""
|
||||
self.result = collections.defaultdict(
|
||||
|
|
@ -157,6 +164,12 @@ def __init__(
|
|||
}
|
||||
self.gc_frame_samples = 0 # Track samples with GC frames
|
||||
|
||||
# Opcode statistics: {location: {opcode: count}}
|
||||
self.opcode_stats = collections.defaultdict(lambda: collections.defaultdict(int))
|
||||
self.show_opcodes = opcodes # Show opcode panel when --opcodes flag is passed
|
||||
self.selected_row = 0 # Currently selected row in table for opcode view
|
||||
self.scroll_offset = 0 # Scroll offset for table when in opcode mode
|
||||
|
||||
# Interactive controls state
|
||||
self.paused = False # Pause UI updates (profiling continues)
|
||||
self.show_help = False # Show help screen
|
||||
|
|
@ -183,6 +196,7 @@ def __init__(
|
|||
self.table_widget = None
|
||||
self.footer_widget = None
|
||||
self.help_widget = None
|
||||
self.opcode_panel = None
|
||||
|
||||
# Color mode
|
||||
self._can_colorize = _colorize.can_colorize()
|
||||
|
|
@ -287,18 +301,29 @@ def process_frames(self, frames, thread_id=None):
|
|||
thread_data = self._get_or_create_thread_data(thread_id) if thread_id is not None else None
|
||||
|
||||
# Process each frame in the stack to track cumulative calls
|
||||
# frame.location is (lineno, end_lineno, col_offset, end_col_offset), int, or None
|
||||
for frame in frames:
|
||||
location = (frame.filename, frame.lineno, frame.funcname)
|
||||
lineno = extract_lineno(frame.location)
|
||||
location = (frame.filename, lineno, frame.funcname)
|
||||
self.result[location]["cumulative_calls"] += 1
|
||||
if thread_data:
|
||||
thread_data.result[location]["cumulative_calls"] += 1
|
||||
|
||||
# The top frame gets counted as an inline call (directly executing)
|
||||
top_location = (frames[0].filename, frames[0].lineno, frames[0].funcname)
|
||||
top_frame = frames[0]
|
||||
top_lineno = extract_lineno(top_frame.location)
|
||||
top_location = (top_frame.filename, top_lineno, top_frame.funcname)
|
||||
self.result[top_location]["direct_calls"] += 1
|
||||
if thread_data:
|
||||
thread_data.result[top_location]["direct_calls"] += 1
|
||||
|
||||
# Track opcode for top frame (the actively executing instruction)
|
||||
opcode = getattr(top_frame, 'opcode', None)
|
||||
if opcode is not None:
|
||||
self.opcode_stats[top_location][opcode] += 1
|
||||
if thread_data:
|
||||
thread_data.opcode_stats[top_location][opcode] += 1
|
||||
|
||||
def _get_sync_frame_iterator(self, stack_frames):
|
||||
"""Iterator for sync frames."""
|
||||
return self._iter_all_frames(stack_frames, skip_idle=self.skip_idle)
|
||||
|
|
@ -407,6 +432,7 @@ def _initialize_widgets(self, colors):
|
|||
self.table_widget = TableWidget(self.display, colors, self)
|
||||
self.footer_widget = FooterWidget(self.display, colors, self)
|
||||
self.help_widget = HelpWidget(self.display, colors)
|
||||
self.opcode_panel = OpcodePanel(self.display, colors, self)
|
||||
|
||||
def _render_display_sections(
|
||||
self, height, width, elapsed, stats_list, colors
|
||||
|
|
@ -427,6 +453,12 @@ def _render_display_sections(
|
|||
line, width, height=height, stats_list=stats_list
|
||||
)
|
||||
|
||||
# Render opcode panel if enabled
|
||||
if self.show_opcodes:
|
||||
line = self.opcode_panel.render(
|
||||
line, width, height=height, stats_list=stats_list
|
||||
)
|
||||
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
|
|
@ -719,6 +751,88 @@ def _handle_finished_input_update(self, had_input):
|
|||
if self.finished and had_input and self.display is not None:
|
||||
self._update_display()
|
||||
|
||||
def _get_visible_rows_info(self):
|
||||
"""Calculate visible rows and stats list for opcode navigation."""
|
||||
stats_list = self.build_stats_list()
|
||||
if self.display:
|
||||
height, _ = self.display.get_dimensions()
|
||||
extra_header = FINISHED_BANNER_EXTRA_LINES if self.finished else 0
|
||||
max_stats = max(0, height - HEADER_LINES - extra_header - FOOTER_LINES - SAFETY_MARGIN)
|
||||
stats_list = stats_list[:max_stats]
|
||||
visible_rows = max(1, height - 8 - 2 - 12)
|
||||
else:
|
||||
visible_rows = self.limit
|
||||
total_rows = len(stats_list)
|
||||
return stats_list, visible_rows, total_rows
|
||||
|
||||
def _move_selection_down(self):
|
||||
"""Move selection down in opcode mode with scrolling."""
|
||||
if not self.show_opcodes:
|
||||
return
|
||||
|
||||
stats_list, visible_rows, total_rows = self._get_visible_rows_info()
|
||||
if total_rows == 0:
|
||||
return
|
||||
|
||||
# Max scroll is when last item is at bottom
|
||||
max_scroll = max(0, total_rows - visible_rows)
|
||||
# Current absolute position
|
||||
abs_pos = self.scroll_offset + self.selected_row
|
||||
|
||||
# Only move if not at the last item
|
||||
if abs_pos < total_rows - 1:
|
||||
# Try to move selection within visible area first
|
||||
if self.selected_row < visible_rows - 1:
|
||||
self.selected_row += 1
|
||||
elif self.scroll_offset < max_scroll:
|
||||
# Scroll down
|
||||
self.scroll_offset += 1
|
||||
|
||||
# Clamp to valid range
|
||||
self.scroll_offset = min(self.scroll_offset, max_scroll)
|
||||
max_selected = min(visible_rows - 1, total_rows - self.scroll_offset - 1)
|
||||
self.selected_row = min(self.selected_row, max(0, max_selected))
|
||||
|
||||
def _move_selection_up(self):
|
||||
"""Move selection up in opcode mode with scrolling."""
|
||||
if not self.show_opcodes:
|
||||
return
|
||||
|
||||
if self.selected_row > 0:
|
||||
self.selected_row -= 1
|
||||
elif self.scroll_offset > 0:
|
||||
self.scroll_offset -= 1
|
||||
|
||||
# Clamp to valid range based on actual stats_list
|
||||
stats_list, visible_rows, total_rows = self._get_visible_rows_info()
|
||||
if total_rows > 0:
|
||||
max_scroll = max(0, total_rows - visible_rows)
|
||||
self.scroll_offset = min(self.scroll_offset, max_scroll)
|
||||
max_selected = min(visible_rows - 1, total_rows - self.scroll_offset - 1)
|
||||
self.selected_row = min(self.selected_row, max(0, max_selected))
|
||||
|
||||
def _navigate_to_previous_thread(self):
|
||||
"""Navigate to previous thread in PER_THREAD mode, or switch from ALL to PER_THREAD."""
|
||||
if len(self.thread_ids) > 0:
|
||||
if self.view_mode == "ALL":
|
||||
self.view_mode = "PER_THREAD"
|
||||
self.current_thread_index = len(self.thread_ids) - 1
|
||||
else:
|
||||
self.current_thread_index = (
|
||||
self.current_thread_index - 1
|
||||
) % len(self.thread_ids)
|
||||
|
||||
def _navigate_to_next_thread(self):
|
||||
"""Navigate to next thread in PER_THREAD mode, or switch from ALL to PER_THREAD."""
|
||||
if len(self.thread_ids) > 0:
|
||||
if self.view_mode == "ALL":
|
||||
self.view_mode = "PER_THREAD"
|
||||
self.current_thread_index = 0
|
||||
else:
|
||||
self.current_thread_index = (
|
||||
self.current_thread_index + 1
|
||||
) % len(self.thread_ids)
|
||||
|
||||
def _show_terminal_too_small(self, height, width):
|
||||
"""Display a message when terminal is too small."""
|
||||
A_BOLD = self.display.get_attr("A_BOLD")
|
||||
|
|
@ -896,27 +1010,37 @@ def _handle_input(self):
|
|||
if self._trend_tracker is not None:
|
||||
self._trend_tracker.toggle()
|
||||
|
||||
elif ch == curses.KEY_LEFT or ch == curses.KEY_UP:
|
||||
# Navigate to previous thread in PER_THREAD mode, or switch from ALL to PER_THREAD
|
||||
if len(self.thread_ids) > 0:
|
||||
if self.view_mode == "ALL":
|
||||
self.view_mode = "PER_THREAD"
|
||||
self.current_thread_index = 0
|
||||
else:
|
||||
self.current_thread_index = (
|
||||
self.current_thread_index - 1
|
||||
) % len(self.thread_ids)
|
||||
elif ch == ord("j") or ch == ord("J"):
|
||||
# Move selection down in opcode mode (with scrolling)
|
||||
self._move_selection_down()
|
||||
|
||||
elif ch == curses.KEY_RIGHT or ch == curses.KEY_DOWN:
|
||||
# Navigate to next thread in PER_THREAD mode, or switch from ALL to PER_THREAD
|
||||
if len(self.thread_ids) > 0:
|
||||
if self.view_mode == "ALL":
|
||||
self.view_mode = "PER_THREAD"
|
||||
self.current_thread_index = 0
|
||||
else:
|
||||
self.current_thread_index = (
|
||||
self.current_thread_index + 1
|
||||
) % len(self.thread_ids)
|
||||
elif ch == ord("k") or ch == ord("K"):
|
||||
# Move selection up in opcode mode (with scrolling)
|
||||
self._move_selection_up()
|
||||
|
||||
elif ch == curses.KEY_UP:
|
||||
# Move selection up (same as 'k') when in opcode mode
|
||||
if self.show_opcodes:
|
||||
self._move_selection_up()
|
||||
else:
|
||||
# Navigate to previous thread (same as KEY_LEFT)
|
||||
self._navigate_to_previous_thread()
|
||||
|
||||
elif ch == curses.KEY_DOWN:
|
||||
# Move selection down (same as 'j') when in opcode mode
|
||||
if self.show_opcodes:
|
||||
self._move_selection_down()
|
||||
else:
|
||||
# Navigate to next thread (same as KEY_RIGHT)
|
||||
self._navigate_to_next_thread()
|
||||
|
||||
elif ch == curses.KEY_LEFT:
|
||||
# Navigate to previous thread
|
||||
self._navigate_to_previous_thread()
|
||||
|
||||
elif ch == curses.KEY_RIGHT:
|
||||
# Navigate to next thread
|
||||
self._navigate_to_next_thread()
|
||||
|
||||
# Update display if input was processed while finished
|
||||
self._handle_finished_input_update(ch != -1)
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@
|
|||
# Finished banner display
|
||||
FINISHED_BANNER_EXTRA_LINES = 3 # Blank line + banner + blank line
|
||||
|
||||
# Opcode panel display
|
||||
OPCODE_PANEL_HEIGHT = 12 # Height reserved for opcode statistics panel
|
||||
|
||||
# Color pair IDs
|
||||
COLOR_PAIR_HEADER_BG = 4
|
||||
COLOR_PAIR_CYAN = 5
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
MIN_SAMPLE_RATE_FOR_SCALING,
|
||||
FOOTER_LINES,
|
||||
FINISHED_BANNER_EXTRA_LINES,
|
||||
OPCODE_PANEL_HEIGHT,
|
||||
)
|
||||
from ..constants import (
|
||||
THREAD_STATUS_HAS_GIL,
|
||||
|
|
@ -730,8 +731,21 @@ def draw_stats_rows(self, line, height, width, stats_list, column_flags):
|
|||
# Get trend tracker for color decisions
|
||||
trend_tracker = self.collector._trend_tracker
|
||||
|
||||
for stat in stats_list:
|
||||
if line >= height - FOOTER_LINES:
|
||||
# Check if opcode mode is enabled for row selection highlighting
|
||||
show_opcodes = getattr(self.collector, 'show_opcodes', False)
|
||||
selected_row = getattr(self.collector, 'selected_row', 0)
|
||||
scroll_offset = getattr(self.collector, 'scroll_offset', 0) if show_opcodes else 0
|
||||
A_REVERSE = self.display.get_attr("A_REVERSE")
|
||||
A_BOLD = self.display.get_attr("A_BOLD")
|
||||
|
||||
# Reserve space for opcode panel when enabled
|
||||
opcode_panel_height = OPCODE_PANEL_HEIGHT if show_opcodes else 0
|
||||
|
||||
# Apply scroll offset when in opcode mode
|
||||
display_stats = stats_list[scroll_offset:] if show_opcodes else stats_list
|
||||
|
||||
for row_idx, stat in enumerate(display_stats):
|
||||
if line >= height - FOOTER_LINES - opcode_panel_height:
|
||||
break
|
||||
|
||||
func = stat["func"]
|
||||
|
|
@ -752,8 +766,13 @@ def draw_stats_rows(self, line, height, width, stats_list, column_flags):
|
|||
else 0
|
||||
)
|
||||
|
||||
# Check if this row is selected
|
||||
is_selected = show_opcodes and row_idx == selected_row
|
||||
|
||||
# Helper function to get trend color for a specific column
|
||||
def get_trend_color(column_name):
|
||||
if is_selected:
|
||||
return A_REVERSE | A_BOLD
|
||||
trend = trends.get(column_name, "stable")
|
||||
if trend_tracker is not None:
|
||||
return trend_tracker.get_color(trend)
|
||||
|
|
@ -763,33 +782,45 @@ def get_trend_color(column_name):
|
|||
samples_str = f"{direct_calls}/{cumulative_calls}"
|
||||
col = 0
|
||||
|
||||
# Fill entire row with reverse video background for selected row
|
||||
if is_selected:
|
||||
self.add_str(line, 0, " " * (width - 1), A_REVERSE | A_BOLD)
|
||||
|
||||
# Show selection indicator when opcode panel is enabled
|
||||
if show_opcodes:
|
||||
if is_selected:
|
||||
self.add_str(line, col, "►", A_REVERSE | A_BOLD)
|
||||
else:
|
||||
self.add_str(line, col, " ", curses.A_NORMAL)
|
||||
col += 2
|
||||
|
||||
# Samples column - apply trend color based on nsamples trend
|
||||
nsamples_color = get_trend_color("nsamples")
|
||||
self.add_str(line, col, f"{samples_str:>13}", nsamples_color)
|
||||
self.add_str(line, col, f"{samples_str:>13} ", nsamples_color)
|
||||
col += 15
|
||||
|
||||
# Sample % column
|
||||
if show_sample_pct:
|
||||
sample_pct_color = get_trend_color("sample_pct")
|
||||
self.add_str(line, col, f"{sample_pct:>5.1f}", sample_pct_color)
|
||||
self.add_str(line, col, f"{sample_pct:>5.1f} ", sample_pct_color)
|
||||
col += 7
|
||||
|
||||
# Total time column
|
||||
if show_tottime:
|
||||
tottime_color = get_trend_color("tottime")
|
||||
self.add_str(line, col, f"{total_time:>10.3f}", tottime_color)
|
||||
self.add_str(line, col, f"{total_time:>10.3f} ", tottime_color)
|
||||
col += 12
|
||||
|
||||
# Cumul % column
|
||||
if show_cumul_pct:
|
||||
cumul_pct_color = get_trend_color("cumul_pct")
|
||||
self.add_str(line, col, f"{cum_pct:>5.1f}", cumul_pct_color)
|
||||
self.add_str(line, col, f"{cum_pct:>5.1f} ", cumul_pct_color)
|
||||
col += 7
|
||||
|
||||
# Cumul time column
|
||||
if show_cumtime:
|
||||
cumtime_color = get_trend_color("cumtime")
|
||||
self.add_str(line, col, f"{cumulative_time:>10.3f}", cumtime_color)
|
||||
self.add_str(line, col, f"{cumulative_time:>10.3f} ", cumtime_color)
|
||||
col += 12
|
||||
|
||||
# Function name column
|
||||
|
|
@ -804,7 +835,8 @@ def get_trend_color(column_name):
|
|||
if len(funcname) > func_width:
|
||||
func_display = funcname[: func_width - 3] + "..."
|
||||
func_display = f"{func_display:<{func_width}}"
|
||||
self.add_str(line, col, func_display, color_func)
|
||||
func_color = A_REVERSE | A_BOLD if is_selected else color_func
|
||||
self.add_str(line, col, func_display, func_color)
|
||||
col += func_width + 2
|
||||
|
||||
# File:line column
|
||||
|
|
@ -812,8 +844,9 @@ def get_trend_color(column_name):
|
|||
simplified_path = self.collector.simplify_path(filename)
|
||||
file_line = f"{simplified_path}:{lineno}"
|
||||
remaining_width = width - col - 1
|
||||
file_color = A_REVERSE | A_BOLD if is_selected else color_file
|
||||
self.add_str(
|
||||
line, col, file_line[:remaining_width], color_file
|
||||
line, col, file_line[:remaining_width], file_color
|
||||
)
|
||||
|
||||
line += 1
|
||||
|
|
@ -934,7 +967,8 @@ def render(self, line, width, **kwargs):
|
|||
(" S - Cycle through sort modes (backward)", A_NORMAL),
|
||||
(" t - Toggle view mode (ALL / per-thread)", A_NORMAL),
|
||||
(" x - Toggle trend colors (on/off)", A_NORMAL),
|
||||
(" ← → ↑ ↓ - Navigate threads (in per-thread mode)", A_NORMAL),
|
||||
(" j/k or ↑/↓ - Select next/previous function (--opcodes)", A_NORMAL),
|
||||
(" ← / → - Cycle through threads", A_NORMAL),
|
||||
(" + - Faster display refresh rate", A_NORMAL),
|
||||
(" - - Slower display refresh rate", A_NORMAL),
|
||||
("", A_NORMAL),
|
||||
|
|
@ -961,3 +995,99 @@ def render(self, line, width, **kwargs):
|
|||
self.add_str(start_line + i, col, text[: width - 3], attr)
|
||||
|
||||
return line # Not used for overlays
|
||||
|
||||
|
||||
class OpcodePanel(Widget):
|
||||
"""Widget for displaying opcode statistics for a selected function."""
|
||||
|
||||
def __init__(self, display, colors, collector):
|
||||
super().__init__(display, colors)
|
||||
self.collector = collector
|
||||
|
||||
def render(self, line, width, **kwargs):
|
||||
"""Render opcode statistics panel.
|
||||
|
||||
Args:
|
||||
line: Starting line number
|
||||
width: Available width
|
||||
kwargs: Must contain 'stats_list', 'height'
|
||||
|
||||
Returns:
|
||||
Next available line number
|
||||
"""
|
||||
from ..opcode_utils import get_opcode_info, format_opcode
|
||||
|
||||
stats_list = kwargs.get("stats_list", [])
|
||||
height = kwargs.get("height", 24)
|
||||
selected_row = self.collector.selected_row
|
||||
scroll_offset = getattr(self.collector, 'scroll_offset', 0)
|
||||
|
||||
A_BOLD = self.display.get_attr("A_BOLD")
|
||||
A_NORMAL = self.display.get_attr("A_NORMAL")
|
||||
color_cyan = self.colors.get("color_cyan", A_NORMAL)
|
||||
color_yellow = self.colors.get("color_yellow", A_NORMAL)
|
||||
color_magenta = self.colors.get("color_magenta", A_NORMAL)
|
||||
|
||||
# Get the selected function from stats_list (accounting for scroll)
|
||||
actual_index = scroll_offset + selected_row
|
||||
if not stats_list or actual_index >= len(stats_list):
|
||||
self.add_str(line, 0, "No function selected (use j/k to select)", A_NORMAL)
|
||||
return line + 1
|
||||
|
||||
selected_stat = stats_list[actual_index]
|
||||
func = selected_stat["func"]
|
||||
filename, lineno, funcname = func
|
||||
|
||||
# Get opcode stats for this function
|
||||
opcode_stats = self.collector.opcode_stats.get(func, {})
|
||||
|
||||
if not opcode_stats:
|
||||
self.add_str(line, 0, f"No opcode data for {funcname}() (requires --opcodes)", A_NORMAL)
|
||||
return line + 1
|
||||
|
||||
# Sort opcodes by count
|
||||
sorted_opcodes = sorted(opcode_stats.items(), key=lambda x: -x[1])
|
||||
total_opcode_samples = sum(opcode_stats.values())
|
||||
|
||||
# Draw header
|
||||
header = f"─── Opcodes for {funcname}() "
|
||||
header += "─" * max(0, width - len(header) - 1)
|
||||
self.add_str(line, 0, header[:width-1], color_cyan | A_BOLD)
|
||||
line += 1
|
||||
|
||||
# Calculate max samples for bar scaling
|
||||
max_count = sorted_opcodes[0][1] if sorted_opcodes else 1
|
||||
|
||||
# Draw opcode rows (limit to available space)
|
||||
max_rows = min(8, height - line - 3) # Leave room for footer
|
||||
bar_width = 20
|
||||
|
||||
for i, (opcode_num, count) in enumerate(sorted_opcodes[:max_rows]):
|
||||
if line >= height - 3:
|
||||
break
|
||||
|
||||
opcode_info = get_opcode_info(opcode_num)
|
||||
is_specialized = opcode_info["is_specialized"]
|
||||
name_display = format_opcode(opcode_num)
|
||||
|
||||
pct = (count / total_opcode_samples * 100) if total_opcode_samples > 0 else 0
|
||||
|
||||
# Draw bar
|
||||
bar_fill = int((count / max_count) * bar_width) if max_count > 0 else 0
|
||||
bar = "█" * bar_fill + "░" * (bar_width - bar_fill)
|
||||
|
||||
# Format: [████████░░░░] LOAD_ATTR 45.2% (1234)
|
||||
# Specialized opcodes shown in magenta, base opcodes in yellow
|
||||
name_color = color_magenta if is_specialized else color_yellow
|
||||
|
||||
row_text = f"[{bar}] {name_display:<35} {pct:>5.1f}% ({count:>6})"
|
||||
self.add_str(line, 2, row_text[:width-3], name_color)
|
||||
line += 1
|
||||
|
||||
# Show "..." if more opcodes exist
|
||||
if len(sorted_opcodes) > max_rows:
|
||||
remaining = len(sorted_opcodes) - max_rows
|
||||
self.add_str(line, 2, f"... and {remaining} more opcodes", A_NORMAL)
|
||||
line += 1
|
||||
|
||||
return line
|
||||
|
|
|
|||
94
Lib/profiling/sampling/opcode_utils.py
Normal file
94
Lib/profiling/sampling/opcode_utils.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"""Opcode utilities for bytecode-level profiler visualization.
|
||||
|
||||
This module provides utilities to get opcode names and detect specialization
|
||||
status using the opcode module's metadata. Used by heatmap and flamegraph
|
||||
collectors to display which bytecode instructions are executing at each
|
||||
source line, including Python's adaptive specialization optimizations.
|
||||
"""
|
||||
|
||||
import opcode
|
||||
|
||||
# Build opcode name mapping: opcode number -> opcode name
|
||||
# This includes both standard opcodes and specialized variants (Python 3.11+)
|
||||
_OPCODE_NAMES = dict(enumerate(opcode.opname))
|
||||
if hasattr(opcode, "_specialized_opmap"):
|
||||
for name, op in opcode._specialized_opmap.items():
|
||||
_OPCODE_NAMES[op] = name
|
||||
|
||||
# Build deopt mapping: specialized opcode number -> base opcode number
|
||||
# Python 3.11+ uses adaptive specialization where generic opcodes like
|
||||
# LOAD_ATTR can be replaced at runtime with specialized variants like
|
||||
# LOAD_ATTR_INSTANCE_VALUE. This mapping lets us show both forms.
|
||||
_DEOPT_MAP = {}
|
||||
if hasattr(opcode, "_specializations") and hasattr(
|
||||
opcode, "_specialized_opmap"
|
||||
):
|
||||
for base_name, variant_names in opcode._specializations.items():
|
||||
base_opcode = opcode.opmap.get(base_name)
|
||||
if base_opcode is not None:
|
||||
for variant_name in variant_names:
|
||||
variant_opcode = opcode._specialized_opmap.get(variant_name)
|
||||
if variant_opcode is not None:
|
||||
_DEOPT_MAP[variant_opcode] = base_opcode
|
||||
|
||||
|
||||
def get_opcode_info(opcode_num):
|
||||
"""Get opcode name and specialization info from an opcode number.
|
||||
|
||||
Args:
|
||||
opcode_num: The opcode number (0-255 or higher for specialized)
|
||||
|
||||
Returns:
|
||||
A dict with keys:
|
||||
- 'opname': The opcode name (e.g., 'LOAD_ATTR_INSTANCE_VALUE')
|
||||
- 'base_opname': The base opcode name (e.g., 'LOAD_ATTR')
|
||||
- 'is_specialized': True if this is a specialized instruction
|
||||
"""
|
||||
opname = _OPCODE_NAMES.get(opcode_num)
|
||||
if opname is None:
|
||||
return {
|
||||
"opname": f"<{opcode_num}>",
|
||||
"base_opname": f"<{opcode_num}>",
|
||||
"is_specialized": False,
|
||||
}
|
||||
|
||||
base_opcode = _DEOPT_MAP.get(opcode_num)
|
||||
if base_opcode is not None:
|
||||
base_opname = _OPCODE_NAMES.get(base_opcode, f"<{base_opcode}>")
|
||||
return {
|
||||
"opname": opname,
|
||||
"base_opname": base_opname,
|
||||
"is_specialized": True,
|
||||
}
|
||||
|
||||
return {
|
||||
"opname": opname,
|
||||
"base_opname": opname,
|
||||
"is_specialized": False,
|
||||
}
|
||||
|
||||
|
||||
def format_opcode(opcode_num):
|
||||
"""Format an opcode for display, showing base opcode for specialized ones.
|
||||
|
||||
Args:
|
||||
opcode_num: The opcode number (0-255 or higher for specialized)
|
||||
|
||||
Returns:
|
||||
A formatted string like 'LOAD_ATTR' or 'LOAD_ATTR_INSTANCE_VALUE (LOAD_ATTR)'
|
||||
"""
|
||||
info = get_opcode_info(opcode_num)
|
||||
if info["is_specialized"]:
|
||||
return f"{info['opname']} ({info['base_opname']})"
|
||||
return info["opname"]
|
||||
|
||||
|
||||
def get_opcode_mapping():
|
||||
"""Get opcode name and deopt mappings for JavaScript consumption.
|
||||
|
||||
Returns:
|
||||
A dict with keys:
|
||||
- 'names': Dict mapping opcode numbers to opcode names
|
||||
- 'deopt': Dict mapping specialized opcode numbers to base opcode numbers
|
||||
"""
|
||||
return {"names": _OPCODE_NAMES, "deopt": _DEOPT_MAP}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
import marshal
|
||||
|
||||
from _colorize import ANSIColors
|
||||
from .collector import Collector
|
||||
from .collector import Collector, extract_lineno
|
||||
|
||||
|
||||
class PstatsCollector(Collector):
|
||||
|
|
@ -23,12 +23,15 @@ def _process_frames(self, frames):
|
|||
return
|
||||
|
||||
# Process each frame in the stack to track cumulative calls
|
||||
# frame.location is int, tuple (lineno, end_lineno, col_offset, end_col_offset), or None
|
||||
for frame in frames:
|
||||
location = (frame.filename, frame.lineno, frame.funcname)
|
||||
self.result[location]["cumulative_calls"] += 1
|
||||
lineno = extract_lineno(frame.location)
|
||||
loc = (frame.filename, lineno, frame.funcname)
|
||||
self.result[loc]["cumulative_calls"] += 1
|
||||
|
||||
# The top frame gets counted as an inline call (directly executing)
|
||||
top_location = (frames[0].filename, frames[0].lineno, frames[0].funcname)
|
||||
top_lineno = extract_lineno(frames[0].location)
|
||||
top_location = (frames[0].filename, top_lineno, frames[0].funcname)
|
||||
self.result[top_location]["direct_calls"] += 1
|
||||
|
||||
# Track caller-callee relationships for call graph
|
||||
|
|
@ -36,8 +39,10 @@ def _process_frames(self, frames):
|
|||
callee_frame = frames[i - 1]
|
||||
caller_frame = frames[i]
|
||||
|
||||
callee = (callee_frame.filename, callee_frame.lineno, callee_frame.funcname)
|
||||
caller = (caller_frame.filename, caller_frame.lineno, caller_frame.funcname)
|
||||
callee_lineno = extract_lineno(callee_frame.location)
|
||||
caller_lineno = extract_lineno(caller_frame.location)
|
||||
callee = (callee_frame.filename, callee_lineno, callee_frame.funcname)
|
||||
caller = (caller_frame.filename, caller_lineno, caller_frame.funcname)
|
||||
|
||||
self.callers[callee][caller] += 1
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
|
||||
class SampleProfiler:
|
||||
def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MODE_WALL, native=False, gc=True, skip_non_matching_threads=True, collect_stats=False):
|
||||
def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MODE_WALL, native=False, gc=True, opcodes=False, skip_non_matching_threads=True, collect_stats=False):
|
||||
self.pid = pid
|
||||
self.sample_interval_usec = sample_interval_usec
|
||||
self.all_threads = all_threads
|
||||
|
|
@ -36,15 +36,15 @@ def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MOD
|
|||
if _FREE_THREADED_BUILD:
|
||||
self.unwinder = _remote_debugging.RemoteUnwinder(
|
||||
self.pid, all_threads=self.all_threads, mode=mode, native=native, gc=gc,
|
||||
skip_non_matching_threads=skip_non_matching_threads, cache_frames=True,
|
||||
stats=collect_stats
|
||||
opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
|
||||
cache_frames=True, stats=collect_stats
|
||||
)
|
||||
else:
|
||||
only_active_threads = bool(self.all_threads)
|
||||
self.unwinder = _remote_debugging.RemoteUnwinder(
|
||||
self.pid, only_active_thread=only_active_threads, mode=mode, native=native, gc=gc,
|
||||
skip_non_matching_threads=skip_non_matching_threads, cache_frames=True,
|
||||
stats=collect_stats
|
||||
opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
|
||||
cache_frames=True, stats=collect_stats
|
||||
)
|
||||
# Track sample intervals and total sample count
|
||||
self.sample_intervals = deque(maxlen=100)
|
||||
|
|
@ -289,6 +289,7 @@ def sample(
|
|||
async_aware=None,
|
||||
native=False,
|
||||
gc=True,
|
||||
opcodes=False,
|
||||
):
|
||||
"""Sample a process using the provided collector.
|
||||
|
||||
|
|
@ -302,6 +303,7 @@ def sample(
|
|||
GIL (only when holding GIL), ALL (includes GIL and CPU status)
|
||||
native: Whether to include native frames
|
||||
gc: Whether to include GC frames
|
||||
opcodes: Whether to include opcode information
|
||||
|
||||
Returns:
|
||||
The collector with collected samples
|
||||
|
|
@ -324,6 +326,7 @@ def sample(
|
|||
mode=mode,
|
||||
native=native,
|
||||
gc=gc,
|
||||
opcodes=opcodes,
|
||||
skip_non_matching_threads=skip_non_matching_threads,
|
||||
collect_stats=realtime_stats,
|
||||
)
|
||||
|
|
@ -346,6 +349,7 @@ def sample_live(
|
|||
async_aware=None,
|
||||
native=False,
|
||||
gc=True,
|
||||
opcodes=False,
|
||||
):
|
||||
"""Sample a process in live/interactive mode with curses TUI.
|
||||
|
||||
|
|
@ -359,6 +363,7 @@ def sample_live(
|
|||
GIL (only when holding GIL), ALL (includes GIL and CPU status)
|
||||
native: Whether to include native frames
|
||||
gc: Whether to include GC frames
|
||||
opcodes: Whether to include opcode information
|
||||
|
||||
Returns:
|
||||
The collector with collected samples
|
||||
|
|
@ -381,6 +386,7 @@ def sample_live(
|
|||
mode=mode,
|
||||
native=native,
|
||||
gc=gc,
|
||||
opcodes=opcodes,
|
||||
skip_non_matching_threads=skip_non_matching_threads,
|
||||
collect_stats=realtime_stats,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
import os
|
||||
|
||||
from ._css_utils import get_combined_css
|
||||
from .collector import Collector
|
||||
from .collector import Collector, extract_lineno
|
||||
from .opcode_utils import get_opcode_mapping
|
||||
from .string_table import StringTable
|
||||
|
||||
|
||||
|
|
@ -40,7 +41,11 @@ def __init__(self, *args, **kwargs):
|
|||
self.stack_counter = collections.Counter()
|
||||
|
||||
def process_frames(self, frames, thread_id):
|
||||
call_tree = tuple(reversed(frames))
|
||||
# Extract only (filename, lineno, funcname) - opcode not needed for collapsed stacks
|
||||
# frame is (filename, location, funcname, opcode)
|
||||
call_tree = tuple(
|
||||
(f[0], extract_lineno(f[1]), f[2]) for f in reversed(frames)
|
||||
)
|
||||
self.stack_counter[(call_tree, thread_id)] += 1
|
||||
|
||||
def export(self, filename):
|
||||
|
|
@ -213,6 +218,11 @@ def convert_children(children, min_samples):
|
|||
source_indices = [self._string_table.intern(line) for line in source]
|
||||
child_entry["source"] = source_indices
|
||||
|
||||
# Include opcode data if available
|
||||
opcodes = node.get("opcodes", {})
|
||||
if opcodes:
|
||||
child_entry["opcodes"] = dict(opcodes)
|
||||
|
||||
# Recurse
|
||||
child_entry["children"] = convert_children(
|
||||
node["children"], min_samples
|
||||
|
|
@ -259,6 +269,9 @@ def convert_children(children, min_samples):
|
|||
**stats
|
||||
}
|
||||
|
||||
# Build opcode mapping for JS
|
||||
opcode_mapping = get_opcode_mapping()
|
||||
|
||||
# If we only have one root child, make it the root to avoid redundant level
|
||||
if len(root_children) == 1:
|
||||
main_child = root_children[0]
|
||||
|
|
@ -273,6 +286,7 @@ def convert_children(children, min_samples):
|
|||
}
|
||||
main_child["threads"] = sorted(list(self._all_threads))
|
||||
main_child["strings"] = self._string_table.get_strings()
|
||||
main_child["opcode_mapping"] = opcode_mapping
|
||||
return main_child
|
||||
|
||||
return {
|
||||
|
|
@ -285,27 +299,41 @@ def convert_children(children, min_samples):
|
|||
"per_thread_stats": per_thread_stats_with_pct
|
||||
},
|
||||
"threads": sorted(list(self._all_threads)),
|
||||
"strings": self._string_table.get_strings()
|
||||
"strings": self._string_table.get_strings(),
|
||||
"opcode_mapping": opcode_mapping
|
||||
}
|
||||
|
||||
def process_frames(self, frames, thread_id):
|
||||
# Reverse to root->leaf
|
||||
call_tree = reversed(frames)
|
||||
"""Process stack frames into flamegraph tree structure.
|
||||
|
||||
Args:
|
||||
frames: List of (filename, location, funcname, opcode) tuples in
|
||||
leaf-to-root order. location is (lineno, end_lineno, col_offset, end_col_offset).
|
||||
opcode is None if not gathered.
|
||||
thread_id: Thread ID for this stack trace
|
||||
"""
|
||||
# Reverse to root->leaf order for tree building
|
||||
self._root["samples"] += 1
|
||||
self._total_samples += 1
|
||||
self._root["threads"].add(thread_id)
|
||||
self._all_threads.add(thread_id)
|
||||
|
||||
current = self._root
|
||||
for func in call_tree:
|
||||
for filename, location, funcname, opcode in reversed(frames):
|
||||
lineno = extract_lineno(location)
|
||||
func = (filename, lineno, funcname)
|
||||
func = self._func_intern.setdefault(func, func)
|
||||
children = current["children"]
|
||||
node = children.get(func)
|
||||
|
||||
node = current["children"].get(func)
|
||||
if node is None:
|
||||
node = {"samples": 0, "children": {}, "threads": set()}
|
||||
children[func] = node
|
||||
node = {"samples": 0, "children": {}, "threads": set(), "opcodes": collections.Counter()}
|
||||
current["children"][func] = node
|
||||
node["samples"] += 1
|
||||
node["threads"].add(thread_id)
|
||||
|
||||
if opcode is not None:
|
||||
node["opcodes"][opcode] += 1
|
||||
|
||||
current = node
|
||||
|
||||
def _get_source_lines(self, func):
|
||||
|
|
|
|||
|
|
@ -379,6 +379,31 @@ def _extract_coroutine_stacks(self, stack_trace):
|
|||
for task in stack_trace[0].awaited_by
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _frame_to_lineno_tuple(frame):
|
||||
"""Convert frame to (filename, lineno, funcname, opcode) tuple.
|
||||
|
||||
This extracts just the line number from the location, ignoring column
|
||||
offsets which can vary due to sampling timing (e.g., when two statements
|
||||
are on the same line, the sample might catch either one).
|
||||
"""
|
||||
filename, location, funcname, opcode = frame
|
||||
return (filename, location.lineno, funcname, opcode)
|
||||
|
||||
def _extract_coroutine_stacks_lineno_only(self, stack_trace):
|
||||
"""Extract coroutine stacks with line numbers only (no column offsets).
|
||||
|
||||
Use this for tests where sampling timing can cause column offset
|
||||
variations (e.g., 'expr1; expr2' on the same line).
|
||||
"""
|
||||
return {
|
||||
task.task_name: sorted(
|
||||
tuple(self._frame_to_lineno_tuple(frame) for frame in coro.call_stack)
|
||||
for coro in task.coroutine_stack
|
||||
)
|
||||
for task in stack_trace[0].awaited_by
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test classes
|
||||
|
|
@ -442,39 +467,25 @@ def foo():
|
|||
"Insufficient permissions to read the stack trace"
|
||||
)
|
||||
|
||||
thread_expected_stack_trace = [
|
||||
FrameInfo([script_name, 15, "foo"]),
|
||||
FrameInfo([script_name, 12, "baz"]),
|
||||
FrameInfo([script_name, 9, "bar"]),
|
||||
FrameInfo([threading.__file__, ANY, "Thread.run"]),
|
||||
FrameInfo(
|
||||
[
|
||||
threading.__file__,
|
||||
ANY,
|
||||
"Thread._bootstrap_inner",
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[threading.__file__, ANY, "Thread._bootstrap"]
|
||||
),
|
||||
]
|
||||
|
||||
# Find expected thread stack
|
||||
# Find expected thread stack by funcname
|
||||
found_thread = self._find_thread_with_frame(
|
||||
stack_trace,
|
||||
lambda f: f.funcname == "foo" and f.lineno == 15,
|
||||
lambda f: f.funcname == "foo" and f.location.lineno == 15,
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
found_thread, "Expected thread stack trace not found"
|
||||
)
|
||||
# Check the funcnames in order
|
||||
funcnames = [f.funcname for f in found_thread.frame_info]
|
||||
self.assertEqual(
|
||||
found_thread.frame_info, thread_expected_stack_trace
|
||||
funcnames[:6],
|
||||
["foo", "baz", "bar", "Thread.run", "Thread._bootstrap_inner", "Thread._bootstrap"]
|
||||
)
|
||||
|
||||
# Check main thread
|
||||
main_frame = FrameInfo([script_name, 19, "<module>"])
|
||||
found_main = self._find_frame_in_trace(
|
||||
stack_trace, lambda f: f == main_frame
|
||||
stack_trace,
|
||||
lambda f: f.funcname == "<module>" and f.location.lineno == 19,
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
found_main, "Main thread stack trace not found"
|
||||
|
|
@ -596,8 +607,10 @@ def new_eager_loop():
|
|||
},
|
||||
)
|
||||
|
||||
# Check coroutine stacks
|
||||
coroutine_stacks = self._extract_coroutine_stacks(
|
||||
# Check coroutine stacks (using line numbers only to avoid
|
||||
# flakiness from column offset variations when sampling
|
||||
# catches different statements on the same line)
|
||||
coroutine_stacks = self._extract_coroutine_stacks_lineno_only(
|
||||
stack_trace
|
||||
)
|
||||
self.assertEqual(
|
||||
|
|
@ -605,48 +618,36 @@ def new_eager_loop():
|
|||
{
|
||||
"Task-1": [
|
||||
(
|
||||
tuple(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit",
|
||||
]
|
||||
),
|
||||
tuple(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__",
|
||||
]
|
||||
),
|
||||
tuple([script_name, 26, "main"]),
|
||||
(taskgroups.__file__, ANY, "TaskGroup._aexit", None),
|
||||
(taskgroups.__file__, ANY, "TaskGroup.__aexit__", None),
|
||||
(script_name, 26, "main", None),
|
||||
)
|
||||
],
|
||||
"c2_root": [
|
||||
(
|
||||
tuple([script_name, 10, "c5"]),
|
||||
tuple([script_name, 14, "c4"]),
|
||||
tuple([script_name, 17, "c3"]),
|
||||
tuple([script_name, 20, "c2"]),
|
||||
(script_name, 10, "c5", None),
|
||||
(script_name, 14, "c4", None),
|
||||
(script_name, 17, "c3", None),
|
||||
(script_name, 20, "c2", None),
|
||||
)
|
||||
],
|
||||
"sub_main_1": [
|
||||
(tuple([script_name, 23, "c1"]),)
|
||||
((script_name, 23, "c1", None),)
|
||||
],
|
||||
"sub_main_2": [
|
||||
(tuple([script_name, 23, "c1"]),)
|
||||
((script_name, 23, "c1", None),)
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
# Check awaited_by coroutine stacks
|
||||
# Check awaited_by coroutine stacks (line numbers only)
|
||||
id_to_task = self._get_task_id_map(stack_trace)
|
||||
awaited_by_coroutine_stacks = {
|
||||
task.task_name: sorted(
|
||||
(
|
||||
id_to_task[coro.task_name].task_name,
|
||||
tuple(
|
||||
tuple(frame)
|
||||
self._frame_to_lineno_tuple(frame)
|
||||
for frame in coro.call_stack
|
||||
),
|
||||
)
|
||||
|
|
@ -662,51 +663,27 @@ def new_eager_loop():
|
|||
(
|
||||
"Task-1",
|
||||
(
|
||||
tuple(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit",
|
||||
]
|
||||
),
|
||||
tuple(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__",
|
||||
]
|
||||
),
|
||||
tuple([script_name, 26, "main"]),
|
||||
(taskgroups.__file__, ANY, "TaskGroup._aexit", None),
|
||||
(taskgroups.__file__, ANY, "TaskGroup.__aexit__", None),
|
||||
(script_name, 26, "main", None),
|
||||
),
|
||||
),
|
||||
(
|
||||
"sub_main_1",
|
||||
(tuple([script_name, 23, "c1"]),),
|
||||
((script_name, 23, "c1", None),),
|
||||
),
|
||||
(
|
||||
"sub_main_2",
|
||||
(tuple([script_name, 23, "c1"]),),
|
||||
((script_name, 23, "c1", None),),
|
||||
),
|
||||
],
|
||||
"sub_main_1": [
|
||||
(
|
||||
"Task-1",
|
||||
(
|
||||
tuple(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit",
|
||||
]
|
||||
),
|
||||
tuple(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__",
|
||||
]
|
||||
),
|
||||
tuple([script_name, 26, "main"]),
|
||||
(taskgroups.__file__, ANY, "TaskGroup._aexit", None),
|
||||
(taskgroups.__file__, ANY, "TaskGroup.__aexit__", None),
|
||||
(script_name, 26, "main", None),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
|
@ -714,21 +691,9 @@ def new_eager_loop():
|
|||
(
|
||||
"Task-1",
|
||||
(
|
||||
tuple(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit",
|
||||
]
|
||||
),
|
||||
tuple(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__",
|
||||
]
|
||||
),
|
||||
tuple([script_name, 26, "main"]),
|
||||
(taskgroups.__file__, ANY, "TaskGroup._aexit", None),
|
||||
(taskgroups.__file__, ANY, "TaskGroup.__aexit__", None),
|
||||
(script_name, 26, "main", None),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
|
@ -800,18 +765,20 @@ async def main():
|
|||
task = stack_trace[0].awaited_by[0]
|
||||
self.assertEqual(task.task_name, "Task-1")
|
||||
|
||||
# Check the coroutine stack
|
||||
# Check the coroutine stack (using line numbers only to avoid
|
||||
# flakiness from column offset variations when sampling
|
||||
# catches different statements on the same line)
|
||||
coroutine_stack = sorted(
|
||||
tuple(tuple(frame) for frame in coro.call_stack)
|
||||
tuple(self._frame_to_lineno_tuple(frame) for frame in coro.call_stack)
|
||||
for coro in task.coroutine_stack
|
||||
)
|
||||
self.assertEqual(
|
||||
coroutine_stack,
|
||||
[
|
||||
(
|
||||
tuple([script_name, 10, "gen_nested_call"]),
|
||||
tuple([script_name, 16, "gen"]),
|
||||
tuple([script_name, 19, "main"]),
|
||||
(script_name, 10, "gen_nested_call", None),
|
||||
(script_name, 16, "gen", None),
|
||||
(script_name, 19, "main", None),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
@ -899,31 +866,33 @@ async def main():
|
|||
},
|
||||
)
|
||||
|
||||
# Check coroutine stacks
|
||||
coroutine_stacks = self._extract_coroutine_stacks(
|
||||
# Check coroutine stacks (using line numbers only to avoid
|
||||
# flakiness from column offset variations when sampling
|
||||
# catches different statements on the same line)
|
||||
coroutine_stacks = self._extract_coroutine_stacks_lineno_only(
|
||||
stack_trace
|
||||
)
|
||||
self.assertEqual(
|
||||
coroutine_stacks,
|
||||
{
|
||||
"Task-1": [(tuple([script_name, 21, "main"]),)],
|
||||
"Task-1": [((script_name, 21, "main", None),)],
|
||||
"Task-2": [
|
||||
(
|
||||
tuple([script_name, 11, "deep"]),
|
||||
tuple([script_name, 15, "c1"]),
|
||||
(script_name, 11, "deep", None),
|
||||
(script_name, 15, "c1", None),
|
||||
)
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
# Check awaited_by coroutine stacks
|
||||
# Check awaited_by coroutine stacks (line numbers only)
|
||||
id_to_task = self._get_task_id_map(stack_trace)
|
||||
awaited_by_coroutine_stacks = {
|
||||
task.task_name: sorted(
|
||||
(
|
||||
id_to_task[coro.task_name].task_name,
|
||||
tuple(
|
||||
tuple(frame) for frame in coro.call_stack
|
||||
self._frame_to_lineno_tuple(frame) for frame in coro.call_stack
|
||||
),
|
||||
)
|
||||
for coro in task.awaited_by
|
||||
|
|
@ -935,7 +904,7 @@ async def main():
|
|||
{
|
||||
"Task-1": [],
|
||||
"Task-2": [
|
||||
("Task-1", (tuple([script_name, 21, "main"]),))
|
||||
("Task-1", ((script_name, 21, "main", None),))
|
||||
],
|
||||
},
|
||||
)
|
||||
|
|
@ -1023,8 +992,10 @@ async def main():
|
|||
},
|
||||
)
|
||||
|
||||
# Check coroutine stacks
|
||||
coroutine_stacks = self._extract_coroutine_stacks(
|
||||
# Check coroutine stacks (using line numbers only to avoid
|
||||
# flakiness from column offset variations when sampling
|
||||
# catches different statements on the same line)
|
||||
coroutine_stacks = self._extract_coroutine_stacks_lineno_only(
|
||||
stack_trace
|
||||
)
|
||||
self.assertEqual(
|
||||
|
|
@ -1032,40 +1003,28 @@ async def main():
|
|||
{
|
||||
"Task-1": [
|
||||
(
|
||||
tuple(
|
||||
[
|
||||
staggered.__file__,
|
||||
ANY,
|
||||
"staggered_race",
|
||||
]
|
||||
),
|
||||
tuple([script_name, 21, "main"]),
|
||||
(staggered.__file__, ANY, "staggered_race", None),
|
||||
(script_name, 21, "main", None),
|
||||
)
|
||||
],
|
||||
"Task-2": [
|
||||
(
|
||||
tuple([script_name, 11, "deep"]),
|
||||
tuple([script_name, 15, "c1"]),
|
||||
tuple(
|
||||
[
|
||||
staggered.__file__,
|
||||
ANY,
|
||||
"staggered_race.<locals>.run_one_coro",
|
||||
]
|
||||
),
|
||||
(script_name, 11, "deep", None),
|
||||
(script_name, 15, "c1", None),
|
||||
(staggered.__file__, ANY, "staggered_race.<locals>.run_one_coro", None),
|
||||
)
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
# Check awaited_by coroutine stacks
|
||||
# Check awaited_by coroutine stacks (line numbers only)
|
||||
id_to_task = self._get_task_id_map(stack_trace)
|
||||
awaited_by_coroutine_stacks = {
|
||||
task.task_name: sorted(
|
||||
(
|
||||
id_to_task[coro.task_name].task_name,
|
||||
tuple(
|
||||
tuple(frame) for frame in coro.call_stack
|
||||
self._frame_to_lineno_tuple(frame) for frame in coro.call_stack
|
||||
),
|
||||
)
|
||||
for coro in task.awaited_by
|
||||
|
|
@ -1080,14 +1039,8 @@ async def main():
|
|||
(
|
||||
"Task-1",
|
||||
(
|
||||
tuple(
|
||||
[
|
||||
staggered.__file__,
|
||||
ANY,
|
||||
"staggered_race",
|
||||
]
|
||||
),
|
||||
tuple([script_name, 21, "main"]),
|
||||
(staggered.__file__, ANY, "staggered_race", None),
|
||||
(script_name, 21, "main", None),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
|
@ -1209,12 +1162,12 @@ async def main():
|
|||
# Check the main task structure
|
||||
main_stack = [
|
||||
FrameInfo(
|
||||
[taskgroups.__file__, ANY, "TaskGroup._aexit"]
|
||||
[taskgroups.__file__, ANY, "TaskGroup._aexit", ANY]
|
||||
),
|
||||
FrameInfo(
|
||||
[taskgroups.__file__, ANY, "TaskGroup.__aexit__"]
|
||||
[taskgroups.__file__, ANY, "TaskGroup.__aexit__", ANY]
|
||||
),
|
||||
FrameInfo([script_name, 52, "main"]),
|
||||
FrameInfo([script_name, ANY, "main", ANY]),
|
||||
]
|
||||
self.assertIn(
|
||||
TaskInfo(
|
||||
|
|
@ -1236,6 +1189,7 @@ async def main():
|
|||
base_events.__file__,
|
||||
ANY,
|
||||
"Server.serve_forever",
|
||||
ANY,
|
||||
]
|
||||
)
|
||||
],
|
||||
|
|
@ -1252,6 +1206,7 @@ async def main():
|
|||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit",
|
||||
ANY,
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
|
|
@ -1259,10 +1214,11 @@ async def main():
|
|||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__",
|
||||
ANY,
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[script_name, ANY, "main"]
|
||||
[script_name, ANY, "main", ANY]
|
||||
),
|
||||
],
|
||||
ANY,
|
||||
|
|
@ -1287,13 +1243,15 @@ async def main():
|
|||
tasks.__file__,
|
||||
ANY,
|
||||
"sleep",
|
||||
ANY,
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[
|
||||
script_name,
|
||||
36,
|
||||
ANY,
|
||||
"echo_client",
|
||||
ANY,
|
||||
]
|
||||
),
|
||||
],
|
||||
|
|
@ -1310,6 +1268,7 @@ async def main():
|
|||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit",
|
||||
ANY,
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
|
|
@ -1317,13 +1276,15 @@ async def main():
|
|||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__",
|
||||
ANY,
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[
|
||||
script_name,
|
||||
39,
|
||||
ANY,
|
||||
"echo_client_spam",
|
||||
ANY,
|
||||
]
|
||||
),
|
||||
],
|
||||
|
|
@ -1336,36 +1297,24 @@ async def main():
|
|||
entries,
|
||||
)
|
||||
|
||||
expected_awaited_by = [
|
||||
CoroInfo(
|
||||
[
|
||||
[
|
||||
FrameInfo(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit",
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__",
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[script_name, 39, "echo_client_spam"]
|
||||
),
|
||||
],
|
||||
ANY,
|
||||
]
|
||||
)
|
||||
]
|
||||
# Find tasks awaited by echo_client_spam via TaskGroup
|
||||
def matches_awaited_by_pattern(task):
|
||||
if len(task.awaited_by) != 1:
|
||||
return False
|
||||
coro = task.awaited_by[0]
|
||||
if len(coro.call_stack) != 3:
|
||||
return False
|
||||
funcnames = [f.funcname for f in coro.call_stack]
|
||||
return funcnames == [
|
||||
"TaskGroup._aexit",
|
||||
"TaskGroup.__aexit__",
|
||||
"echo_client_spam",
|
||||
]
|
||||
|
||||
tasks_with_awaited = [
|
||||
task
|
||||
for task in entries
|
||||
if task.awaited_by == expected_awaited_by
|
||||
if matches_awaited_by_pattern(task)
|
||||
]
|
||||
self.assertGreaterEqual(len(tasks_with_awaited), NUM_TASKS)
|
||||
|
||||
|
|
@ -1396,25 +1345,12 @@ def test_self_trace(self):
|
|||
break
|
||||
|
||||
self.assertIsNotNone(this_thread_stack)
|
||||
self.assertEqual(
|
||||
this_thread_stack[:2],
|
||||
[
|
||||
FrameInfo(
|
||||
[
|
||||
__file__,
|
||||
get_stack_trace.__code__.co_firstlineno + 4,
|
||||
"get_stack_trace",
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[
|
||||
__file__,
|
||||
self.test_self_trace.__code__.co_firstlineno + 6,
|
||||
"TestGetStackTrace.test_self_trace",
|
||||
]
|
||||
),
|
||||
],
|
||||
)
|
||||
# Check the top two frames
|
||||
self.assertGreaterEqual(len(this_thread_stack), 2)
|
||||
self.assertEqual(this_thread_stack[0].funcname, "get_stack_trace")
|
||||
self.assertTrue(this_thread_stack[0].filename.endswith("test_external_inspection.py"))
|
||||
self.assertEqual(this_thread_stack[1].funcname, "TestGetStackTrace.test_self_trace")
|
||||
self.assertTrue(this_thread_stack[1].filename.endswith("test_external_inspection.py"))
|
||||
|
||||
@skip_if_not_supported
|
||||
@unittest.skipIf(
|
||||
|
|
@ -1815,7 +1751,7 @@ def main_work():
|
|||
found = self._find_frame_in_trace(
|
||||
all_traces,
|
||||
lambda f: f.funcname == "main_work"
|
||||
and f.lineno > 12,
|
||||
and f.location.lineno > 12,
|
||||
)
|
||||
if found:
|
||||
break
|
||||
|
|
@ -1865,6 +1801,136 @@ def main_work():
|
|||
finally:
|
||||
_cleanup_sockets(client_socket, server_socket)
|
||||
|
||||
@skip_if_not_supported
|
||||
@unittest.skipIf(
|
||||
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
||||
"Test only runs on Linux with process_vm_readv support",
|
||||
)
|
||||
def test_opcodes_collection(self):
|
||||
"""Test that opcodes are collected when the opcodes flag is set."""
|
||||
script = textwrap.dedent(
|
||||
"""\
|
||||
import time, sys, socket
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect(('localhost', {port}))
|
||||
|
||||
def foo():
|
||||
sock.sendall(b"ready")
|
||||
time.sleep(10_000)
|
||||
|
||||
foo()
|
||||
"""
|
||||
)
|
||||
|
||||
def get_trace_with_opcodes(pid):
|
||||
return RemoteUnwinder(pid, opcodes=True).get_stack_trace()
|
||||
|
||||
stack_trace, _ = self._run_script_and_get_trace(
|
||||
script, get_trace_with_opcodes, wait_for_signals=b"ready"
|
||||
)
|
||||
|
||||
# Find our foo frame and verify it has an opcode
|
||||
foo_frame = self._find_frame_in_trace(
|
||||
stack_trace, lambda f: f.funcname == "foo"
|
||||
)
|
||||
self.assertIsNotNone(foo_frame, "Could not find foo frame")
|
||||
self.assertIsInstance(foo_frame.opcode, int)
|
||||
self.assertGreaterEqual(foo_frame.opcode, 0)
|
||||
|
||||
@skip_if_not_supported
|
||||
@unittest.skipIf(
|
||||
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
||||
"Test only runs on Linux with process_vm_readv support",
|
||||
)
|
||||
def test_location_tuple_format(self):
|
||||
"""Test that location is a 4-tuple (lineno, end_lineno, col_offset, end_col_offset)."""
|
||||
script = textwrap.dedent(
|
||||
"""\
|
||||
import time, sys, socket
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect(('localhost', {port}))
|
||||
|
||||
def foo():
|
||||
sock.sendall(b"ready")
|
||||
time.sleep(10_000)
|
||||
|
||||
foo()
|
||||
"""
|
||||
)
|
||||
|
||||
def get_trace_with_opcodes(pid):
|
||||
return RemoteUnwinder(pid, opcodes=True).get_stack_trace()
|
||||
|
||||
stack_trace, _ = self._run_script_and_get_trace(
|
||||
script, get_trace_with_opcodes, wait_for_signals=b"ready"
|
||||
)
|
||||
|
||||
# Find our foo frame
|
||||
foo_frame = self._find_frame_in_trace(
|
||||
stack_trace, lambda f: f.funcname == "foo"
|
||||
)
|
||||
self.assertIsNotNone(foo_frame, "Could not find foo frame")
|
||||
|
||||
# Check location is a 4-tuple with valid values
|
||||
location = foo_frame.location
|
||||
self.assertIsInstance(location, tuple)
|
||||
self.assertEqual(len(location), 4)
|
||||
lineno, end_lineno, col_offset, end_col_offset = location
|
||||
self.assertIsInstance(lineno, int)
|
||||
self.assertGreater(lineno, 0)
|
||||
self.assertIsInstance(end_lineno, int)
|
||||
self.assertGreaterEqual(end_lineno, lineno)
|
||||
self.assertIsInstance(col_offset, int)
|
||||
self.assertGreaterEqual(col_offset, 0)
|
||||
self.assertIsInstance(end_col_offset, int)
|
||||
self.assertGreaterEqual(end_col_offset, col_offset)
|
||||
|
||||
@skip_if_not_supported
|
||||
@unittest.skipIf(
|
||||
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
||||
"Test only runs on Linux with process_vm_readv support",
|
||||
)
|
||||
def test_location_tuple_exact_values(self):
|
||||
"""Test exact values of location tuple including column offsets."""
|
||||
script = textwrap.dedent(
|
||||
"""\
|
||||
import time, sys, socket
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect(('localhost', {port}))
|
||||
|
||||
def foo():
|
||||
sock.sendall(b"ready")
|
||||
time.sleep(10_000)
|
||||
|
||||
foo()
|
||||
"""
|
||||
)
|
||||
|
||||
def get_trace_with_opcodes(pid):
|
||||
return RemoteUnwinder(pid, opcodes=True).get_stack_trace()
|
||||
|
||||
stack_trace, _ = self._run_script_and_get_trace(
|
||||
script, get_trace_with_opcodes, wait_for_signals=b"ready"
|
||||
)
|
||||
|
||||
foo_frame = self._find_frame_in_trace(
|
||||
stack_trace, lambda f: f.funcname == "foo"
|
||||
)
|
||||
self.assertIsNotNone(foo_frame, "Could not find foo frame")
|
||||
|
||||
# Can catch either sock.sendall (line 7) or time.sleep (line 8)
|
||||
location = foo_frame.location
|
||||
valid_locations = [
|
||||
(7, 7, 4, 26), # sock.sendall(b"ready")
|
||||
(8, 8, 4, 22), # time.sleep(10_000)
|
||||
]
|
||||
actual = (location.lineno, location.end_lineno,
|
||||
location.col_offset, location.end_col_offset)
|
||||
self.assertIn(actual, valid_locations)
|
||||
|
||||
|
||||
class TestUnsupportedPlatformHandling(unittest.TestCase):
|
||||
@unittest.skipIf(
|
||||
|
|
@ -2404,13 +2470,13 @@ def inner():
|
|||
|
||||
# Line numbers must be different and increasing (execution moves forward)
|
||||
self.assertLess(
|
||||
inner_a.lineno, inner_b.lineno, "Line B should be after line A"
|
||||
inner_a.location.lineno, inner_b.location.lineno, "Line B should be after line A"
|
||||
)
|
||||
self.assertLess(
|
||||
inner_b.lineno, inner_c.lineno, "Line C should be after line B"
|
||||
inner_b.location.lineno, inner_c.location.lineno, "Line C should be after line B"
|
||||
)
|
||||
self.assertLess(
|
||||
inner_c.lineno, inner_d.lineno, "Line D should be after line C"
|
||||
inner_c.location.lineno, inner_d.location.lineno, "Line D should be after line C"
|
||||
)
|
||||
|
||||
@skip_if_not_supported
|
||||
|
|
@ -2709,10 +2775,10 @@ def level1():
|
|||
funcs_no_cache = [f.funcname for f in frames_no_cache]
|
||||
self.assertEqual(funcs_cached, funcs_no_cache)
|
||||
|
||||
# Same line numbers
|
||||
lines_cached = [f.lineno for f in frames_cached]
|
||||
lines_no_cache = [f.lineno for f in frames_no_cache]
|
||||
self.assertEqual(lines_cached, lines_no_cache)
|
||||
# Same locations
|
||||
locations_cached = [f.location for f in frames_cached]
|
||||
locations_no_cache = [f.location for f in frames_no_cache]
|
||||
self.assertEqual(locations_cached, locations_no_cache)
|
||||
|
||||
@skip_if_not_supported
|
||||
@unittest.skipIf(
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@
|
|||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
|
||||
# Matches the C structseq LocationInfo from _remote_debugging
|
||||
LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset'])
|
||||
|
||||
from profiling.sampling.heatmap_collector import (
|
||||
HeatmapCollector,
|
||||
get_python_path_info,
|
||||
|
|
@ -214,7 +218,7 @@ def test_process_frames_increments_total_samples(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
initial_count = collector._total_samples
|
||||
frames = [('file.py', 10, 'func')]
|
||||
frames = [('file.py', (10, 10, -1, -1), 'func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
self.assertEqual(collector._total_samples, initial_count + 1)
|
||||
|
|
@ -223,7 +227,7 @@ def test_process_frames_records_line_samples(self):
|
|||
"""Test that process_frames records line samples."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('test.py', 5, 'test_func')]
|
||||
frames = [('test.py', (5, 5, -1, -1), 'test_func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
# Check that line was recorded
|
||||
|
|
@ -235,9 +239,9 @@ def test_process_frames_records_multiple_lines_in_stack(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [
|
||||
('file1.py', 10, 'func1'),
|
||||
('file2.py', 20, 'func2'),
|
||||
('file3.py', 30, 'func3')
|
||||
('file1.py', (10, 10, -1, -1), 'func1', None),
|
||||
('file2.py', (20, 20, -1, -1), 'func2', None),
|
||||
('file3.py', (30, 30, -1, -1), 'func3', None)
|
||||
]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
|
|
@ -251,8 +255,8 @@ def test_process_frames_distinguishes_self_samples(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [
|
||||
('leaf.py', 5, 'leaf_func'), # This is the leaf (top of stack)
|
||||
('caller.py', 10, 'caller_func')
|
||||
('leaf.py', (5, 5, -1, -1), 'leaf_func', None), # This is the leaf (top of stack)
|
||||
('caller.py', (10, 10, -1, -1), 'caller_func', None)
|
||||
]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
|
|
@ -267,7 +271,7 @@ def test_process_frames_accumulates_samples(self):
|
|||
"""Test that multiple calls accumulate samples."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('file.py', 10, 'func')]
|
||||
frames = [('file.py', (10, 10, -1, -1), 'func', None)]
|
||||
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
|
@ -282,11 +286,11 @@ def test_process_frames_ignores_invalid_frames(self):
|
|||
|
||||
# These should be ignored
|
||||
invalid_frames = [
|
||||
('<string>', 1, 'test'),
|
||||
('[eval]', 1, 'test'),
|
||||
('', 1, 'test'),
|
||||
(None, 1, 'test'),
|
||||
('__init__', 0, 'test'), # Special invalid frame
|
||||
('<string>', (1, 1, -1, -1), 'test', None),
|
||||
('[eval]', (1, 1, -1, -1), 'test', None),
|
||||
('', (1, 1, -1, -1), 'test', None),
|
||||
(None, (1, 1, -1, -1), 'test', None),
|
||||
('__init__', (0, 0, -1, -1), 'test', None), # Special invalid frame
|
||||
]
|
||||
|
||||
for frame in invalid_frames:
|
||||
|
|
@ -295,15 +299,15 @@ def test_process_frames_ignores_invalid_frames(self):
|
|||
# Should not record these invalid frames
|
||||
for frame in invalid_frames:
|
||||
if frame[0]:
|
||||
self.assertNotIn((frame[0], frame[1]), collector.line_samples)
|
||||
self.assertNotIn((frame[0], frame[1][0]), collector.line_samples)
|
||||
|
||||
def test_process_frames_builds_call_graph(self):
|
||||
"""Test that process_frames builds call graph relationships."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [
|
||||
('callee.py', 5, 'callee_func'),
|
||||
('caller.py', 10, 'caller_func')
|
||||
('callee.py', (5, 5, -1, -1), 'callee_func', None),
|
||||
('caller.py', (10, 10, -1, -1), 'caller_func', None)
|
||||
]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
|
|
@ -319,7 +323,7 @@ def test_process_frames_records_function_definitions(self):
|
|||
"""Test that process_frames records function definition locations."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('module.py', 42, 'my_function')]
|
||||
frames = [('module.py', (42, 42, -1, -1), 'my_function', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
self.assertIn(('module.py', 'my_function'), collector.function_definitions)
|
||||
|
|
@ -330,8 +334,8 @@ def test_process_frames_tracks_edge_samples(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [
|
||||
('callee.py', 5, 'callee'),
|
||||
('caller.py', 10, 'caller')
|
||||
('callee.py', (5, 5, -1, -1), 'callee', None),
|
||||
('caller.py', (10, 10, -1, -1), 'caller', None)
|
||||
]
|
||||
|
||||
# Process same call stack multiple times
|
||||
|
|
@ -355,7 +359,7 @@ def test_process_frames_with_file_samples_dict(self):
|
|||
"""Test that file_samples dict is properly populated."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('test.py', 10, 'func')]
|
||||
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
self.assertIn('test.py', collector.file_samples)
|
||||
|
|
@ -376,7 +380,7 @@ def test_export_creates_output_directory(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
# Add some data
|
||||
frames = [('test.py', 10, 'func')]
|
||||
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
output_path = os.path.join(self.test_dir, 'heatmap_output')
|
||||
|
|
@ -391,7 +395,7 @@ def test_export_creates_index_html(self):
|
|||
"""Test that export creates index.html."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('test.py', 10, 'func')]
|
||||
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
output_path = os.path.join(self.test_dir, 'heatmap_output')
|
||||
|
|
@ -406,7 +410,7 @@ def test_export_creates_file_htmls(self):
|
|||
"""Test that export creates individual file HTMLs."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('test.py', 10, 'func')]
|
||||
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
output_path = os.path.join(self.test_dir, 'heatmap_output')
|
||||
|
|
@ -433,7 +437,7 @@ def test_export_handles_html_suffix(self):
|
|||
"""Test that export handles .html suffix in output path."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('test.py', 10, 'func')]
|
||||
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
# Path with .html suffix should be stripped
|
||||
|
|
@ -451,9 +455,9 @@ def test_export_with_multiple_files(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
# Add samples for multiple files
|
||||
collector.process_frames([('file1.py', 10, 'func1')], thread_id=1)
|
||||
collector.process_frames([('file2.py', 20, 'func2')], thread_id=1)
|
||||
collector.process_frames([('file3.py', 30, 'func3')], thread_id=1)
|
||||
collector.process_frames([('file1.py', (10, 10, -1, -1), 'func1', None)], thread_id=1)
|
||||
collector.process_frames([('file2.py', (20, 20, -1, -1), 'func2', None)], thread_id=1)
|
||||
collector.process_frames([('file3.py', (30, 30, -1, -1), 'func3', None)], thread_id=1)
|
||||
|
||||
output_path = os.path.join(self.test_dir, 'multi_file')
|
||||
|
||||
|
|
@ -470,7 +474,7 @@ def test_export_index_contains_file_references(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
collector.set_stats(sample_interval_usec=100, duration_sec=1.0, sample_rate=100.0)
|
||||
|
||||
frames = [('mytest.py', 10, 'my_func')]
|
||||
frames = [('mytest.py', (10, 10, -1, -1), 'my_func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
output_path = os.path.join(self.test_dir, 'test_output')
|
||||
|
|
@ -494,7 +498,7 @@ def test_export_file_html_has_line_numbers(self):
|
|||
with open(temp_file, 'w') as f:
|
||||
f.write('def test():\n pass\n')
|
||||
|
||||
frames = [(temp_file, 1, 'test')]
|
||||
frames = [(temp_file, (1, 1, -1, -1), 'test', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
output_path = os.path.join(self.test_dir, 'line_test')
|
||||
|
|
@ -515,23 +519,39 @@ def test_export_file_html_has_line_numbers(self):
|
|||
|
||||
|
||||
class MockFrameInfo:
|
||||
"""Mock FrameInfo for testing since the real one isn't accessible."""
|
||||
"""Mock FrameInfo for testing.
|
||||
|
||||
def __init__(self, filename, lineno, funcname):
|
||||
Frame format: (filename, location, funcname, opcode) where:
|
||||
- location is a tuple (lineno, end_lineno, col_offset, end_col_offset)
|
||||
- opcode is an int or None
|
||||
"""
|
||||
|
||||
def __init__(self, filename, lineno, funcname, opcode=None):
|
||||
self.filename = filename
|
||||
self.lineno = lineno
|
||||
self.funcname = funcname
|
||||
self.opcode = opcode
|
||||
self.location = (lineno, lineno, -1, -1)
|
||||
|
||||
def __iter__(self):
|
||||
return iter((self.filename, self.location, self.funcname, self.opcode))
|
||||
|
||||
def __getitem__(self, index):
|
||||
return (self.filename, self.location, self.funcname, self.opcode)[index]
|
||||
|
||||
def __len__(self):
|
||||
return 4
|
||||
|
||||
def __repr__(self):
|
||||
return f"MockFrameInfo(filename='{self.filename}', lineno={self.lineno}, funcname='{self.funcname}')"
|
||||
return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})"
|
||||
|
||||
|
||||
class MockThreadInfo:
|
||||
"""Mock ThreadInfo for testing since the real one isn't accessible."""
|
||||
|
||||
def __init__(self, thread_id, frame_info):
|
||||
def __init__(self, thread_id, frame_info, status=0):
|
||||
self.thread_id = thread_id
|
||||
self.frame_info = frame_info
|
||||
self.status = status # Thread status flags
|
||||
|
||||
def __repr__(self):
|
||||
return f"MockThreadInfo(thread_id={self.thread_id}, frame_info={self.frame_info})"
|
||||
|
|
@ -559,13 +579,13 @@ def test_heatmap_collector_basic(self):
|
|||
self.assertEqual(len(collector.file_samples), 0)
|
||||
self.assertEqual(len(collector.line_samples), 0)
|
||||
|
||||
# Test collecting sample data
|
||||
# Test collecting sample data - frames are 4-tuples: (filename, location, funcname, opcode)
|
||||
test_frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(
|
||||
1,
|
||||
[("file.py", 10, "func1"), ("file.py", 20, "func2")],
|
||||
[MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")],
|
||||
)]
|
||||
)
|
||||
]
|
||||
|
|
@ -586,21 +606,21 @@ def test_heatmap_collector_export(self):
|
|||
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
# Create test data with multiple files
|
||||
# Create test data with multiple files using MockFrameInfo
|
||||
test_frames1 = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [("file.py", 10, "func1"), ("file.py", 20, "func2")])],
|
||||
[MockThreadInfo(1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")])],
|
||||
)
|
||||
]
|
||||
test_frames2 = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [("file.py", 10, "func1"), ("file.py", 20, "func2")])],
|
||||
[MockThreadInfo(1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")])],
|
||||
)
|
||||
] # Same stack
|
||||
test_frames3 = [
|
||||
MockInterpreterInfo(0, [MockThreadInfo(1, [("other.py", 5, "other_func")])])
|
||||
MockInterpreterInfo(0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])])
|
||||
]
|
||||
|
||||
collector.collect(test_frames1)
|
||||
|
|
@ -643,5 +663,95 @@ def test_heatmap_collector_export(self):
|
|||
self.assertIn("nav-btn", file_content)
|
||||
|
||||
|
||||
class TestHeatmapCollectorLocation(unittest.TestCase):
|
||||
"""Tests for HeatmapCollector location handling."""
|
||||
|
||||
def test_heatmap_with_full_location_info(self):
|
||||
"""Test HeatmapCollector uses full location tuple."""
|
||||
collector = HeatmapCollector(sample_interval_usec=1000)
|
||||
|
||||
# Frame with full location: (lineno, end_lineno, col_offset, end_col_offset)
|
||||
frame = MockFrameInfo("test.py", 10, "func")
|
||||
# Override with full location info
|
||||
frame.location = LocationInfo(10, 15, 4, 20)
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame])]
|
||||
)
|
||||
]
|
||||
collector.collect(frames)
|
||||
|
||||
# Verify data was collected with location info
|
||||
# HeatmapCollector uses file_samples dict with filename -> Counter of linenos
|
||||
self.assertIn("test.py", collector.file_samples)
|
||||
# Line 10 should have samples
|
||||
self.assertIn(10, collector.file_samples["test.py"])
|
||||
|
||||
def test_heatmap_with_none_location(self):
|
||||
"""Test HeatmapCollector handles None location gracefully."""
|
||||
collector = HeatmapCollector(sample_interval_usec=1000)
|
||||
|
||||
# Synthetic frame with None location
|
||||
frame = MockFrameInfo("~", 0, "<native>")
|
||||
frame.location = None
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame])]
|
||||
)
|
||||
]
|
||||
# Should not raise
|
||||
collector.collect(frames)
|
||||
|
||||
def test_heatmap_export_with_location_data(self):
|
||||
"""Test HeatmapCollector export includes location info."""
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
self.addCleanup(shutil.rmtree, tmp_dir)
|
||||
|
||||
collector = HeatmapCollector(sample_interval_usec=1000)
|
||||
|
||||
frame = MockFrameInfo("test.py", 10, "process")
|
||||
frame.location = LocationInfo(10, 12, 0, 30)
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame])]
|
||||
)
|
||||
]
|
||||
collector.collect(frames)
|
||||
|
||||
# Export should work
|
||||
with (captured_stdout(), captured_stderr()):
|
||||
collector.export(tmp_dir)
|
||||
self.assertTrue(os.path.exists(os.path.join(tmp_dir, "index.html")))
|
||||
|
||||
def test_heatmap_collector_frame_format(self):
|
||||
"""Test HeatmapCollector with 4-element frame format."""
|
||||
collector = HeatmapCollector(sample_interval_usec=1000)
|
||||
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(
|
||||
1,
|
||||
[
|
||||
MockFrameInfo("app.py", 100, "main", opcode=90),
|
||||
MockFrameInfo("utils.py", 50, "helper", opcode=100),
|
||||
MockFrameInfo("lib.py", 25, "process", opcode=None),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
collector.collect(frames)
|
||||
|
||||
# Should have recorded data for the files
|
||||
self.assertIn("app.py", collector.file_samples)
|
||||
self.assertIn("utils.py", collector.file_samples)
|
||||
self.assertIn("lib.py", collector.file_samples)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -1,21 +1,42 @@
|
|||
"""Common test helpers and mocks for live collector tests."""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from profiling.sampling.constants import (
|
||||
THREAD_STATUS_HAS_GIL,
|
||||
THREAD_STATUS_ON_CPU,
|
||||
)
|
||||
|
||||
|
||||
class MockFrameInfo:
|
||||
"""Mock FrameInfo for testing."""
|
||||
# Matches the C structseq LocationInfo from _remote_debugging
|
||||
LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset'])
|
||||
|
||||
def __init__(self, filename, lineno, funcname):
|
||||
|
||||
class MockFrameInfo:
|
||||
"""Mock FrameInfo for testing.
|
||||
|
||||
Frame format: (filename, location, funcname, opcode) where:
|
||||
- location is a tuple (lineno, end_lineno, col_offset, end_col_offset)
|
||||
- opcode is an int or None
|
||||
"""
|
||||
|
||||
def __init__(self, filename, lineno, funcname, opcode=None):
|
||||
self.filename = filename
|
||||
self.lineno = lineno
|
||||
self.funcname = funcname
|
||||
self.opcode = opcode
|
||||
self.location = LocationInfo(lineno, lineno, -1, -1)
|
||||
|
||||
def __iter__(self):
|
||||
return iter((self.filename, self.location, self.funcname, self.opcode))
|
||||
|
||||
def __getitem__(self, index):
|
||||
return (self.filename, self.location, self.funcname, self.opcode)[index]
|
||||
|
||||
def __len__(self):
|
||||
return 4
|
||||
|
||||
def __repr__(self):
|
||||
return f"MockFrameInfo(filename='{self.filename}', lineno={self.lineno}, funcname='{self.funcname}')"
|
||||
return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})"
|
||||
|
||||
|
||||
class MockThreadInfo:
|
||||
|
|
|
|||
|
|
@ -1,16 +1,36 @@
|
|||
"""Mock classes for sampling profiler tests."""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
# Matches the C structseq LocationInfo from _remote_debugging
|
||||
LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset'])
|
||||
|
||||
|
||||
class MockFrameInfo:
|
||||
"""Mock FrameInfo for testing since the real one isn't accessible."""
|
||||
"""Mock FrameInfo for testing.
|
||||
|
||||
def __init__(self, filename, lineno, funcname):
|
||||
Frame format: (filename, location, funcname, opcode) where:
|
||||
- location is a tuple (lineno, end_lineno, col_offset, end_col_offset)
|
||||
- opcode is an int or None
|
||||
"""
|
||||
|
||||
def __init__(self, filename, lineno, funcname, opcode=None):
|
||||
self.filename = filename
|
||||
self.lineno = lineno
|
||||
self.funcname = funcname
|
||||
self.opcode = opcode
|
||||
self.location = LocationInfo(lineno, lineno, -1, -1)
|
||||
|
||||
def __iter__(self):
|
||||
return iter((self.filename, self.location, self.funcname, self.opcode))
|
||||
|
||||
def __getitem__(self, index):
|
||||
return (self.filename, self.location, self.funcname, self.opcode)[index]
|
||||
|
||||
def __len__(self):
|
||||
return 4
|
||||
|
||||
def __repr__(self):
|
||||
return f"MockFrameInfo(filename='{self.filename}', lineno={self.lineno}, funcname='{self.funcname}')"
|
||||
return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})"
|
||||
|
||||
|
||||
class MockThreadInfo:
|
||||
|
|
|
|||
|
|
@ -14,9 +14,12 @@
|
|||
FlamegraphCollector,
|
||||
)
|
||||
from profiling.sampling.gecko_collector import GeckoCollector
|
||||
from profiling.sampling.collector import extract_lineno, normalize_location
|
||||
from profiling.sampling.opcode_utils import get_opcode_info, format_opcode
|
||||
from profiling.sampling.constants import (
|
||||
PROFILING_MODE_WALL,
|
||||
PROFILING_MODE_CPU,
|
||||
DEFAULT_LOCATION,
|
||||
)
|
||||
from _remote_debugging import (
|
||||
THREAD_STATUS_HAS_GIL,
|
||||
|
|
@ -30,7 +33,7 @@
|
|||
|
||||
from test.support import captured_stdout, captured_stderr
|
||||
|
||||
from .mocks import MockFrameInfo, MockThreadInfo, MockInterpreterInfo
|
||||
from .mocks import MockFrameInfo, MockThreadInfo, MockInterpreterInfo, LocationInfo
|
||||
from .helpers import close_and_unlink
|
||||
|
||||
|
||||
|
|
@ -42,9 +45,8 @@ def test_mock_frame_info_with_empty_and_unicode_values(self):
|
|||
# Test with empty strings
|
||||
frame = MockFrameInfo("", 0, "")
|
||||
self.assertEqual(frame.filename, "")
|
||||
self.assertEqual(frame.lineno, 0)
|
||||
self.assertEqual(frame.location.lineno, 0)
|
||||
self.assertEqual(frame.funcname, "")
|
||||
self.assertIn("filename=''", repr(frame))
|
||||
|
||||
# Test with unicode characters
|
||||
frame = MockFrameInfo("文件.py", 42, "函数名")
|
||||
|
|
@ -56,7 +58,7 @@ def test_mock_frame_info_with_empty_and_unicode_values(self):
|
|||
long_funcname = "func_" + "x" * 1000
|
||||
frame = MockFrameInfo(long_filename, 999999, long_funcname)
|
||||
self.assertEqual(frame.filename, long_filename)
|
||||
self.assertEqual(frame.lineno, 999999)
|
||||
self.assertEqual(frame.location.lineno, 999999)
|
||||
self.assertEqual(frame.funcname, long_funcname)
|
||||
|
||||
def test_pstats_collector_with_extreme_intervals_and_empty_data(self):
|
||||
|
|
@ -78,7 +80,7 @@ def test_pstats_collector_with_extreme_intervals_and_empty_data(self):
|
|||
test_frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(None, [MockFrameInfo("file.py", 10, "func")])],
|
||||
[MockThreadInfo(None, [MockFrameInfo("file.py", 10, "func", None)])],
|
||||
)
|
||||
]
|
||||
collector.collect(test_frames)
|
||||
|
|
@ -193,7 +195,7 @@ def test_collapsed_stack_collector_with_empty_and_deep_stacks(self):
|
|||
# Test with single frame stack
|
||||
test_frames = [
|
||||
MockInterpreterInfo(
|
||||
0, [MockThreadInfo(1, [("file.py", 10, "func")])]
|
||||
0, [MockThreadInfo(1, [MockFrameInfo("file.py", 10, "func")])]
|
||||
)
|
||||
]
|
||||
collector.collect(test_frames)
|
||||
|
|
@ -204,7 +206,7 @@ def test_collapsed_stack_collector_with_empty_and_deep_stacks(self):
|
|||
self.assertEqual(count, 1)
|
||||
|
||||
# Test with very deep stack
|
||||
deep_stack = [(f"file{i}.py", i, f"func{i}") for i in range(100)]
|
||||
deep_stack = [MockFrameInfo(f"file{i}.py", i, f"func{i}") for i in range(100)]
|
||||
test_frames = [MockInterpreterInfo(0, [MockThreadInfo(1, deep_stack)])]
|
||||
collector = CollapsedStackCollector(1000)
|
||||
collector.collect(test_frames)
|
||||
|
|
@ -317,7 +319,7 @@ def test_collapsed_stack_collector_basic(self):
|
|||
0,
|
||||
[
|
||||
MockThreadInfo(
|
||||
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
|
||||
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
@ -343,7 +345,7 @@ def test_collapsed_stack_collector_export(self):
|
|||
0,
|
||||
[
|
||||
MockThreadInfo(
|
||||
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
|
||||
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
@ -353,14 +355,14 @@ def test_collapsed_stack_collector_export(self):
|
|||
0,
|
||||
[
|
||||
MockThreadInfo(
|
||||
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
|
||||
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
|
||||
)
|
||||
],
|
||||
)
|
||||
] # Same stack
|
||||
test_frames3 = [
|
||||
MockInterpreterInfo(
|
||||
0, [MockThreadInfo(1, [("other.py", 5, "other_func")])]
|
||||
0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])]
|
||||
)
|
||||
]
|
||||
|
||||
|
|
@ -406,7 +408,7 @@ def test_flamegraph_collector_basic(self):
|
|||
0,
|
||||
[
|
||||
MockThreadInfo(
|
||||
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
|
||||
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
@ -454,7 +456,7 @@ def test_flamegraph_collector_export(self):
|
|||
0,
|
||||
[
|
||||
MockThreadInfo(
|
||||
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
|
||||
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
@ -464,14 +466,14 @@ def test_flamegraph_collector_export(self):
|
|||
0,
|
||||
[
|
||||
MockThreadInfo(
|
||||
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
|
||||
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
|
||||
)
|
||||
],
|
||||
)
|
||||
] # Same stack
|
||||
test_frames3 = [
|
||||
MockInterpreterInfo(
|
||||
0, [MockThreadInfo(1, [("other.py", 5, "other_func")])]
|
||||
0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])]
|
||||
)
|
||||
]
|
||||
|
||||
|
|
@ -518,7 +520,7 @@ def test_gecko_collector_basic(self):
|
|||
[
|
||||
MockThreadInfo(
|
||||
1,
|
||||
[("file.py", 10, "func1"), ("file.py", 20, "func2")],
|
||||
[MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
@ -608,7 +610,7 @@ def test_gecko_collector_export(self):
|
|||
0,
|
||||
[
|
||||
MockThreadInfo(
|
||||
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
|
||||
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
@ -618,14 +620,14 @@ def test_gecko_collector_export(self):
|
|||
0,
|
||||
[
|
||||
MockThreadInfo(
|
||||
1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]
|
||||
1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")]
|
||||
)
|
||||
],
|
||||
)
|
||||
] # Same stack
|
||||
test_frames3 = [
|
||||
MockInterpreterInfo(
|
||||
0, [MockThreadInfo(1, [("other.py", 5, "other_func")])]
|
||||
0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])]
|
||||
)
|
||||
]
|
||||
|
||||
|
|
@ -683,7 +685,7 @@ def test_gecko_collector_markers(self):
|
|||
[
|
||||
MockThreadInfo(
|
||||
1,
|
||||
[("test.py", 10, "python_func")],
|
||||
[MockFrameInfo("test.py", 10, "python_func")],
|
||||
status=HAS_GIL_ON_CPU,
|
||||
)
|
||||
],
|
||||
|
|
@ -698,7 +700,7 @@ def test_gecko_collector_markers(self):
|
|||
[
|
||||
MockThreadInfo(
|
||||
1,
|
||||
[("test.py", 15, "wait_func")],
|
||||
[MockFrameInfo("test.py", 15, "wait_func")],
|
||||
status=WAITING_FOR_GIL,
|
||||
)
|
||||
],
|
||||
|
|
@ -713,7 +715,7 @@ def test_gecko_collector_markers(self):
|
|||
[
|
||||
MockThreadInfo(
|
||||
1,
|
||||
[("test.py", 20, "python_func2")],
|
||||
[MockFrameInfo("test.py", 20, "python_func2")],
|
||||
status=HAS_GIL_ON_CPU,
|
||||
)
|
||||
],
|
||||
|
|
@ -728,7 +730,7 @@ def test_gecko_collector_markers(self):
|
|||
[
|
||||
MockThreadInfo(
|
||||
1,
|
||||
[("native.c", 100, "native_func")],
|
||||
[MockFrameInfo("native.c", 100, "native_func")],
|
||||
status=NO_GIL_ON_CPU,
|
||||
)
|
||||
],
|
||||
|
|
@ -902,8 +904,8 @@ def test_flamegraph_collector_stats_accumulation(self):
|
|||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(1, [("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(2, [("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
|
||||
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -917,9 +919,9 @@ def test_flamegraph_collector_stats_accumulation(self):
|
|||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(1, [("a.py", 1, "func_a")], status=THREAD_STATUS_GIL_REQUESTED),
|
||||
MockThreadInfo(2, [("b.py", 2, "func_b")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(3, [("c.py", 3, "func_c")], status=THREAD_STATUS_ON_CPU),
|
||||
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_GIL_REQUESTED),
|
||||
MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(3, [MockFrameInfo("c.py", 3, "func_c")], status=THREAD_STATUS_ON_CPU),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -936,7 +938,7 @@ def test_flamegraph_collector_stats_accumulation(self):
|
|||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(1, [("~", 0, "<GC>")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(1, [MockFrameInfo("~", 0, "<GC>")], status=THREAD_STATUS_HAS_GIL),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -960,9 +962,9 @@ def test_flamegraph_collector_per_thread_stats(self):
|
|||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(1, [("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(2, [("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
|
||||
MockThreadInfo(3, [("c.py", 3, "func_c")], status=THREAD_STATUS_GIL_REQUESTED),
|
||||
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
|
||||
MockThreadInfo(3, [MockFrameInfo("c.py", 3, "func_c")], status=THREAD_STATUS_GIL_REQUESTED),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -992,7 +994,7 @@ def test_flamegraph_collector_per_thread_stats(self):
|
|||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(1, [("a.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
|
||||
MockThreadInfo(1, [MockFrameInfo("a.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -1012,7 +1014,7 @@ def test_flamegraph_collector_percentage_calculations(self):
|
|||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(1, [("a.py", 1, "func")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func")], status=THREAD_STATUS_HAS_GIL),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -1023,7 +1025,7 @@ def test_flamegraph_collector_percentage_calculations(self):
|
|||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(1, [("a.py", 1, "func")], status=THREAD_STATUS_ON_CPU),
|
||||
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func")], status=THREAD_STATUS_ON_CPU),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -1046,7 +1048,7 @@ def test_flamegraph_collector_mode_handling(self):
|
|||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(1, [("a.py", 1, "func")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func")], status=THREAD_STATUS_HAS_GIL),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -1085,8 +1087,8 @@ def test_flamegraph_collector_json_structure_includes_stats(self):
|
|||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(1, [("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(2, [("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
|
||||
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -1142,13 +1144,13 @@ def test_flamegraph_collector_per_thread_gc_percentage(self):
|
|||
# First 5 samples: both threads, thread 1 has GC in 2
|
||||
for i in range(5):
|
||||
has_gc = i < 2 # First 2 samples have GC for thread 1
|
||||
frames_1 = [("~", 0, "<GC>")] if has_gc else [("a.py", 1, "func_a")]
|
||||
frames_1 = [MockFrameInfo("~", 0, "<GC>")] if has_gc else [MockFrameInfo("a.py", 1, "func_a")]
|
||||
stack_frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(1, frames_1, status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(2, [("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
|
||||
MockThreadInfo(2, [MockFrameInfo("b.py", 2, "func_b")], status=THREAD_STATUS_ON_CPU),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -1162,8 +1164,8 @@ def test_flamegraph_collector_per_thread_gc_percentage(self):
|
|||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(1, [("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(2, [("~", 0, "<GC>")], status=THREAD_STATUS_ON_CPU),
|
||||
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(2, [MockFrameInfo("~", 0, "<GC>")], status=THREAD_STATUS_ON_CPU),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -1173,7 +1175,7 @@ def test_flamegraph_collector_per_thread_gc_percentage(self):
|
|||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(1, [("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
|
||||
MockThreadInfo(1, [MockFrameInfo("a.py", 1, "func_a")], status=THREAD_STATUS_HAS_GIL),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -1201,3 +1203,434 @@ def test_flamegraph_collector_per_thread_gc_percentage(self):
|
|||
self.assertEqual(collector.per_thread_stats[2]["gc_samples"], 1)
|
||||
self.assertEqual(collector.per_thread_stats[2]["total"], 6)
|
||||
self.assertAlmostEqual(per_thread_stats[2]["gc_pct"], 10.0, places=1)
|
||||
|
||||
|
||||
class TestLocationHelpers(unittest.TestCase):
|
||||
"""Tests for location handling helper functions."""
|
||||
|
||||
def test_extract_lineno_from_location_info(self):
|
||||
"""Test extracting lineno from LocationInfo namedtuple."""
|
||||
loc = LocationInfo(42, 45, 0, 10)
|
||||
self.assertEqual(extract_lineno(loc), 42)
|
||||
|
||||
def test_extract_lineno_from_tuple(self):
|
||||
"""Test extracting lineno from plain tuple."""
|
||||
loc = (100, 105, 5, 20)
|
||||
self.assertEqual(extract_lineno(loc), 100)
|
||||
|
||||
def test_extract_lineno_from_none(self):
|
||||
"""Test extracting lineno from None (synthetic frames)."""
|
||||
self.assertEqual(extract_lineno(None), 0)
|
||||
|
||||
def test_normalize_location_with_location_info(self):
|
||||
"""Test normalize_location passes through LocationInfo."""
|
||||
loc = LocationInfo(10, 15, 0, 5)
|
||||
result = normalize_location(loc)
|
||||
self.assertEqual(result, loc)
|
||||
|
||||
def test_normalize_location_with_tuple(self):
|
||||
"""Test normalize_location passes through tuple."""
|
||||
loc = (10, 15, 0, 5)
|
||||
result = normalize_location(loc)
|
||||
self.assertEqual(result, loc)
|
||||
|
||||
def test_normalize_location_with_none(self):
|
||||
"""Test normalize_location returns DEFAULT_LOCATION for None."""
|
||||
result = normalize_location(None)
|
||||
self.assertEqual(result, DEFAULT_LOCATION)
|
||||
self.assertEqual(result, (0, 0, -1, -1))
|
||||
|
||||
|
||||
class TestOpcodeFormatting(unittest.TestCase):
|
||||
"""Tests for opcode formatting utilities."""
|
||||
|
||||
def test_get_opcode_info_standard_opcode(self):
|
||||
"""Test get_opcode_info for a standard opcode."""
|
||||
import opcode
|
||||
# LOAD_CONST is a standard opcode
|
||||
load_const = opcode.opmap.get('LOAD_CONST')
|
||||
if load_const is not None:
|
||||
info = get_opcode_info(load_const)
|
||||
self.assertEqual(info['opname'], 'LOAD_CONST')
|
||||
self.assertEqual(info['base_opname'], 'LOAD_CONST')
|
||||
self.assertFalse(info['is_specialized'])
|
||||
|
||||
def test_get_opcode_info_unknown_opcode(self):
|
||||
"""Test get_opcode_info for an unknown opcode."""
|
||||
info = get_opcode_info(999)
|
||||
self.assertEqual(info['opname'], '<999>')
|
||||
self.assertEqual(info['base_opname'], '<999>')
|
||||
self.assertFalse(info['is_specialized'])
|
||||
|
||||
def test_format_opcode_standard(self):
|
||||
"""Test format_opcode for a standard opcode."""
|
||||
import opcode
|
||||
load_const = opcode.opmap.get('LOAD_CONST')
|
||||
if load_const is not None:
|
||||
formatted = format_opcode(load_const)
|
||||
self.assertEqual(formatted, 'LOAD_CONST')
|
||||
|
||||
def test_format_opcode_specialized(self):
|
||||
"""Test format_opcode for a specialized opcode shows base in parens."""
|
||||
import opcode
|
||||
if not hasattr(opcode, '_specialized_opmap'):
|
||||
self.skipTest("No specialized opcodes in this Python version")
|
||||
if not hasattr(opcode, '_specializations'):
|
||||
self.skipTest("No specialization info in this Python version")
|
||||
|
||||
# Find any specialized opcode to test
|
||||
for base_name, variants in opcode._specializations.items():
|
||||
if not variants:
|
||||
continue
|
||||
variant_name = variants[0]
|
||||
variant_opcode = opcode._specialized_opmap.get(variant_name)
|
||||
if variant_opcode is None:
|
||||
continue
|
||||
formatted = format_opcode(variant_opcode)
|
||||
# Should show: VARIANT_NAME (BASE_NAME)
|
||||
self.assertIn(variant_name, formatted)
|
||||
self.assertIn(f'({base_name})', formatted)
|
||||
return
|
||||
|
||||
self.skipTest("No specialized opcodes found")
|
||||
|
||||
def test_format_opcode_unknown(self):
|
||||
"""Test format_opcode for an unknown opcode."""
|
||||
formatted = format_opcode(999)
|
||||
self.assertEqual(formatted, '<999>')
|
||||
|
||||
|
||||
class TestLocationInCollectors(unittest.TestCase):
|
||||
"""Tests for location tuple handling in each collector."""
|
||||
|
||||
def _make_frames_with_location(self, location, opcode=None):
|
||||
"""Create test frames with a specific location."""
|
||||
frame = MockFrameInfo("test.py", 0, "test_func", opcode)
|
||||
# Override the location
|
||||
frame.location = location
|
||||
return [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
|
||||
def test_pstats_collector_with_location_info(self):
|
||||
"""Test PstatsCollector handles LocationInfo properly."""
|
||||
collector = PstatsCollector(sample_interval_usec=1000)
|
||||
|
||||
# Frame with LocationInfo
|
||||
frame = MockFrameInfo("test.py", 42, "my_function")
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
collector.collect(frames)
|
||||
|
||||
# Should extract lineno from location
|
||||
key = ("test.py", 42, "my_function")
|
||||
self.assertIn(key, collector.result)
|
||||
self.assertEqual(collector.result[key]["direct_calls"], 1)
|
||||
|
||||
def test_pstats_collector_with_none_location(self):
|
||||
"""Test PstatsCollector handles None location (synthetic frames)."""
|
||||
collector = PstatsCollector(sample_interval_usec=1000)
|
||||
|
||||
# Create frame with None location (like GC frame)
|
||||
frame = MockFrameInfo("~", 0, "<GC>")
|
||||
frame.location = None # Synthetic frame has no location
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
collector.collect(frames)
|
||||
|
||||
# Should use lineno=0 for None location
|
||||
key = ("~", 0, "<GC>")
|
||||
self.assertIn(key, collector.result)
|
||||
|
||||
def test_collapsed_stack_with_location_info(self):
|
||||
"""Test CollapsedStackCollector handles LocationInfo properly."""
|
||||
collector = CollapsedStackCollector(1000)
|
||||
|
||||
frame1 = MockFrameInfo("main.py", 10, "main")
|
||||
frame2 = MockFrameInfo("utils.py", 25, "helper")
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame1, frame2], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
collector.collect(frames)
|
||||
|
||||
# Check that linenos were extracted correctly
|
||||
self.assertEqual(len(collector.stack_counter), 1)
|
||||
(path, _), count = list(collector.stack_counter.items())[0]
|
||||
# Reversed order: helper at top, main at bottom
|
||||
self.assertEqual(path[0], ("utils.py", 25, "helper"))
|
||||
self.assertEqual(path[1], ("main.py", 10, "main"))
|
||||
|
||||
def test_flamegraph_collector_with_location_info(self):
|
||||
"""Test FlamegraphCollector handles LocationInfo properly."""
|
||||
collector = FlamegraphCollector(sample_interval_usec=1000)
|
||||
|
||||
frame = MockFrameInfo("app.py", 100, "process_data")
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
collector.collect(frames)
|
||||
|
||||
data = collector._convert_to_flamegraph_format()
|
||||
# Verify the function name includes lineno from location
|
||||
strings = data.get("strings", [])
|
||||
name_found = any("process_data" in s and "100" in s for s in strings if isinstance(s, str))
|
||||
self.assertTrue(name_found, f"Expected to find 'process_data' with line 100 in {strings}")
|
||||
|
||||
def test_gecko_collector_with_location_info(self):
|
||||
"""Test GeckoCollector handles LocationInfo properly."""
|
||||
collector = GeckoCollector(sample_interval_usec=1000)
|
||||
|
||||
frame = MockFrameInfo("server.py", 50, "handle_request")
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
collector.collect(frames)
|
||||
|
||||
profile = collector._build_profile()
|
||||
# Check that the function was recorded
|
||||
self.assertEqual(len(profile["threads"]), 1)
|
||||
thread_data = profile["threads"][0]
|
||||
string_array = profile["shared"]["stringArray"]
|
||||
|
||||
# Verify function name is in string table
|
||||
self.assertIn("handle_request", string_array)
|
||||
|
||||
|
||||
class TestOpcodeHandling(unittest.TestCase):
|
||||
"""Tests for opcode field handling in collectors."""
|
||||
|
||||
def test_frame_with_opcode(self):
|
||||
"""Test MockFrameInfo properly stores opcode."""
|
||||
frame = MockFrameInfo("test.py", 10, "my_func", opcode=90)
|
||||
self.assertEqual(frame.opcode, 90)
|
||||
# Verify tuple representation includes opcode
|
||||
self.assertEqual(frame[3], 90)
|
||||
self.assertEqual(len(frame), 4)
|
||||
|
||||
def test_frame_without_opcode(self):
|
||||
"""Test MockFrameInfo with no opcode defaults to None."""
|
||||
frame = MockFrameInfo("test.py", 10, "my_func")
|
||||
self.assertIsNone(frame.opcode)
|
||||
self.assertIsNone(frame[3])
|
||||
|
||||
def test_collectors_ignore_opcode_for_key_generation(self):
|
||||
"""Test that collectors use (filename, lineno, funcname) as key, not opcode."""
|
||||
collector = PstatsCollector(sample_interval_usec=1000)
|
||||
|
||||
# Same function, different opcodes
|
||||
frame1 = MockFrameInfo("test.py", 10, "func", opcode=90)
|
||||
frame2 = MockFrameInfo("test.py", 10, "func", opcode=100)
|
||||
|
||||
frames1 = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame1], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
frames2 = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame2], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
|
||||
collector.collect(frames1)
|
||||
collector.collect(frames2)
|
||||
|
||||
# Should be counted as same function (opcode not in key)
|
||||
key = ("test.py", 10, "func")
|
||||
self.assertIn(key, collector.result)
|
||||
self.assertEqual(collector.result[key]["direct_calls"], 2)
|
||||
|
||||
|
||||
class TestGeckoOpcodeMarkers(unittest.TestCase):
|
||||
"""Tests for GeckoCollector opcode interval markers."""
|
||||
|
||||
def test_gecko_collector_opcodes_disabled_by_default(self):
|
||||
"""Test that opcode tracking is disabled by default."""
|
||||
collector = GeckoCollector(sample_interval_usec=1000)
|
||||
self.assertFalse(collector.opcodes_enabled)
|
||||
|
||||
def test_gecko_collector_opcodes_enabled(self):
|
||||
"""Test that opcode tracking can be enabled."""
|
||||
collector = GeckoCollector(sample_interval_usec=1000, opcodes=True)
|
||||
self.assertTrue(collector.opcodes_enabled)
|
||||
|
||||
def test_gecko_opcode_state_tracking(self):
|
||||
"""Test that GeckoCollector tracks opcode state changes."""
|
||||
collector = GeckoCollector(sample_interval_usec=1000, opcodes=True)
|
||||
|
||||
# First sample with opcode 90 (RAISE_VARARGS)
|
||||
frame1 = MockFrameInfo("test.py", 10, "func", opcode=90)
|
||||
frames1 = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame1], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
collector.collect(frames1)
|
||||
|
||||
# Should start tracking this opcode state
|
||||
self.assertIn(1, collector.opcode_state)
|
||||
state = collector.opcode_state[1]
|
||||
self.assertEqual(state[0], 90) # opcode
|
||||
self.assertEqual(state[1], 10) # lineno
|
||||
self.assertEqual(state[3], "func") # funcname
|
||||
|
||||
def test_gecko_opcode_state_change_emits_marker(self):
|
||||
"""Test that opcode state change emits an interval marker."""
|
||||
collector = GeckoCollector(sample_interval_usec=1000, opcodes=True)
|
||||
|
||||
# First sample: opcode 90
|
||||
frame1 = MockFrameInfo("test.py", 10, "func", opcode=90)
|
||||
frames1 = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame1], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
collector.collect(frames1)
|
||||
|
||||
# Second sample: different opcode 100
|
||||
frame2 = MockFrameInfo("test.py", 10, "func", opcode=100)
|
||||
frames2 = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame2], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
collector.collect(frames2)
|
||||
|
||||
# Should have emitted a marker for the first opcode
|
||||
thread_data = collector.threads[1]
|
||||
markers = thread_data["markers"]
|
||||
# At least one marker should have been added
|
||||
self.assertGreater(len(markers["name"]), 0)
|
||||
|
||||
def test_gecko_opcode_markers_not_emitted_when_disabled(self):
|
||||
"""Test that no opcode markers when opcodes=False."""
|
||||
collector = GeckoCollector(sample_interval_usec=1000, opcodes=False)
|
||||
|
||||
frame1 = MockFrameInfo("test.py", 10, "func", opcode=90)
|
||||
frames1 = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame1], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
collector.collect(frames1)
|
||||
|
||||
frame2 = MockFrameInfo("test.py", 10, "func", opcode=100)
|
||||
frames2 = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame2], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
collector.collect(frames2)
|
||||
|
||||
# opcode_state should not be tracked
|
||||
self.assertEqual(len(collector.opcode_state), 0)
|
||||
|
||||
def test_gecko_opcode_with_none_opcode(self):
|
||||
"""Test that None opcode doesn't cause issues."""
|
||||
collector = GeckoCollector(sample_interval_usec=1000, opcodes=True)
|
||||
|
||||
# Frame with no opcode (None)
|
||||
frame = MockFrameInfo("test.py", 10, "func", opcode=None)
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame], status=THREAD_STATUS_HAS_GIL)]
|
||||
)
|
||||
]
|
||||
collector.collect(frames)
|
||||
|
||||
# Should track the state but opcode is None
|
||||
self.assertIn(1, collector.opcode_state)
|
||||
self.assertIsNone(collector.opcode_state[1][0])
|
||||
|
||||
|
||||
class TestCollectorFrameFormat(unittest.TestCase):
|
||||
"""Tests verifying all collectors handle the 4-element frame format."""
|
||||
|
||||
def _make_sample_frames(self):
|
||||
"""Create sample frames with full format: (filename, location, funcname, opcode)."""
|
||||
return [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(
|
||||
1,
|
||||
[
|
||||
MockFrameInfo("app.py", 100, "main", opcode=90),
|
||||
MockFrameInfo("utils.py", 50, "helper", opcode=100),
|
||||
MockFrameInfo("lib.py", 25, "process", opcode=None),
|
||||
],
|
||||
status=THREAD_STATUS_HAS_GIL,
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
def test_pstats_collector_frame_format(self):
|
||||
"""Test PstatsCollector with 4-element frame format."""
|
||||
collector = PstatsCollector(sample_interval_usec=1000)
|
||||
collector.collect(self._make_sample_frames())
|
||||
|
||||
# All three functions should be recorded
|
||||
self.assertEqual(len(collector.result), 3)
|
||||
self.assertIn(("app.py", 100, "main"), collector.result)
|
||||
self.assertIn(("utils.py", 50, "helper"), collector.result)
|
||||
self.assertIn(("lib.py", 25, "process"), collector.result)
|
||||
|
||||
def test_collapsed_stack_frame_format(self):
|
||||
"""Test CollapsedStackCollector with 4-element frame format."""
|
||||
collector = CollapsedStackCollector(sample_interval_usec=1000)
|
||||
collector.collect(self._make_sample_frames())
|
||||
|
||||
self.assertEqual(len(collector.stack_counter), 1)
|
||||
(path, _), _ = list(collector.stack_counter.items())[0]
|
||||
# 3 frames in the path (reversed order)
|
||||
self.assertEqual(len(path), 3)
|
||||
|
||||
def test_flamegraph_collector_frame_format(self):
|
||||
"""Test FlamegraphCollector with 4-element frame format."""
|
||||
collector = FlamegraphCollector(sample_interval_usec=1000)
|
||||
collector.collect(self._make_sample_frames())
|
||||
|
||||
data = collector._convert_to_flamegraph_format()
|
||||
# Should have processed the frames
|
||||
self.assertIn("children", data)
|
||||
|
||||
def test_gecko_collector_frame_format(self):
|
||||
"""Test GeckoCollector with 4-element frame format."""
|
||||
collector = GeckoCollector(sample_interval_usec=1000)
|
||||
collector.collect(self._make_sample_frames())
|
||||
|
||||
profile = collector._build_profile()
|
||||
# Should have one thread with the frames
|
||||
self.assertEqual(len(profile["threads"]), 1)
|
||||
thread = profile["threads"][0]
|
||||
# Should have recorded 3 functions
|
||||
self.assertEqual(thread["funcTable"]["length"], 3)
|
||||
|
|
|
|||
|
|
@ -304,10 +304,10 @@ def test_collapsed_stack_with_recursion(self):
|
|||
MockThreadInfo(
|
||||
1,
|
||||
[
|
||||
("factorial.py", 10, "factorial"),
|
||||
("factorial.py", 10, "factorial"), # recursive
|
||||
("factorial.py", 10, "factorial"), # deeper
|
||||
("main.py", 5, "main"),
|
||||
MockFrameInfo("factorial.py", 10, "factorial"),
|
||||
MockFrameInfo("factorial.py", 10, "factorial"), # recursive
|
||||
MockFrameInfo("factorial.py", 10, "factorial"), # deeper
|
||||
MockFrameInfo("main.py", 5, "main"),
|
||||
],
|
||||
)
|
||||
],
|
||||
|
|
@ -318,13 +318,9 @@ def test_collapsed_stack_with_recursion(self):
|
|||
MockThreadInfo(
|
||||
1,
|
||||
[
|
||||
("factorial.py", 10, "factorial"),
|
||||
(
|
||||
"factorial.py",
|
||||
10,
|
||||
"factorial",
|
||||
), # different depth
|
||||
("main.py", 5, "main"),
|
||||
MockFrameInfo("factorial.py", 10, "factorial"),
|
||||
MockFrameInfo("factorial.py", 10, "factorial"), # different depth
|
||||
MockFrameInfo("main.py", 5, "main"),
|
||||
],
|
||||
)
|
||||
],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
Add bytecode-level instruction profiling to the sampling profiler via the
|
||||
new ``--opcodes`` flag. When enabled, the profiler captures which bytecode
|
||||
opcode is executing at each sample, including Python 3.11+ adaptive
|
||||
specializations, and visualizes this data in the heatmap, flamegraph, gecko,
|
||||
and live output formats. Patch by Pablo Galindo
|
||||
|
|
@ -190,6 +190,7 @@ typedef struct {
|
|||
typedef struct {
|
||||
PyTypeObject *RemoteDebugging_Type;
|
||||
PyTypeObject *TaskInfo_Type;
|
||||
PyTypeObject *LocationInfo_Type;
|
||||
PyTypeObject *FrameInfo_Type;
|
||||
PyTypeObject *CoroInfo_Type;
|
||||
PyTypeObject *ThreadInfo_Type;
|
||||
|
|
@ -228,6 +229,7 @@ typedef struct {
|
|||
int skip_non_matching_threads;
|
||||
int native;
|
||||
int gc;
|
||||
int opcodes;
|
||||
int cache_frames;
|
||||
int collect_stats; // whether to collect statistics
|
||||
uint32_t stale_invalidation_counter; // counter for throttling frame_cache_invalidate_stale
|
||||
|
|
@ -286,6 +288,7 @@ typedef int (*set_entry_processor_func)(
|
|||
* ============================================================================ */
|
||||
|
||||
extern PyStructSequence_Desc TaskInfo_desc;
|
||||
extern PyStructSequence_Desc LocationInfo_desc;
|
||||
extern PyStructSequence_Desc FrameInfo_desc;
|
||||
extern PyStructSequence_Desc CoroInfo_desc;
|
||||
extern PyStructSequence_Desc ThreadInfo_desc;
|
||||
|
|
@ -336,11 +339,20 @@ extern int parse_code_object(
|
|||
int32_t tlbc_index
|
||||
);
|
||||
|
||||
extern PyObject *make_location_info(
|
||||
RemoteUnwinderObject *unwinder,
|
||||
int lineno,
|
||||
int end_lineno,
|
||||
int col_offset,
|
||||
int end_col_offset
|
||||
);
|
||||
|
||||
extern PyObject *make_frame_info(
|
||||
RemoteUnwinderObject *unwinder,
|
||||
PyObject *file,
|
||||
PyObject *line,
|
||||
PyObject *func
|
||||
PyObject *location, // LocationInfo structseq or None for synthetic frames
|
||||
PyObject *func,
|
||||
PyObject *opcode
|
||||
);
|
||||
|
||||
/* Line table parsing */
|
||||
|
|
|
|||
34
Modules/_remote_debugging/clinic/module.c.h
generated
34
Modules/_remote_debugging/clinic/module.c.h
generated
|
|
@ -12,7 +12,8 @@ preserve
|
|||
PyDoc_STRVAR(_remote_debugging_RemoteUnwinder___init____doc__,
|
||||
"RemoteUnwinder(pid, *, all_threads=False, only_active_thread=False,\n"
|
||||
" mode=0, debug=False, skip_non_matching_threads=True,\n"
|
||||
" native=False, gc=False, cache_frames=False, stats=False)\n"
|
||||
" native=False, gc=False, opcodes=False,\n"
|
||||
" cache_frames=False, stats=False)\n"
|
||||
"--\n"
|
||||
"\n"
|
||||
"Initialize a new RemoteUnwinder object for debugging a remote Python process.\n"
|
||||
|
|
@ -32,6 +33,8 @@ PyDoc_STRVAR(_remote_debugging_RemoteUnwinder___init____doc__,
|
|||
" non-Python code.\n"
|
||||
" gc: If True, include artificial \"<GC>\" frames to denote active garbage\n"
|
||||
" collection.\n"
|
||||
" opcodes: If True, gather bytecode opcode information for instruction-level\n"
|
||||
" profiling.\n"
|
||||
" cache_frames: If True, enable frame caching optimization to avoid re-reading\n"
|
||||
" unchanged parent frames between samples.\n"
|
||||
" stats: If True, collect statistics about cache hits, memory reads, etc.\n"
|
||||
|
|
@ -53,7 +56,8 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
|
|||
int mode, int debug,
|
||||
int skip_non_matching_threads,
|
||||
int native, int gc,
|
||||
int cache_frames, int stats);
|
||||
int opcodes, int cache_frames,
|
||||
int stats);
|
||||
|
||||
static int
|
||||
_remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObject *kwargs)
|
||||
|
|
@ -61,7 +65,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
|
|||
int return_value = -1;
|
||||
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
|
||||
|
||||
#define NUM_KEYWORDS 10
|
||||
#define NUM_KEYWORDS 11
|
||||
static struct {
|
||||
PyGC_Head _this_is_not_used;
|
||||
PyObject_VAR_HEAD
|
||||
|
|
@ -70,7 +74,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
|
|||
} _kwtuple = {
|
||||
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
|
||||
.ob_hash = -1,
|
||||
.ob_item = { &_Py_ID(pid), &_Py_ID(all_threads), &_Py_ID(only_active_thread), &_Py_ID(mode), &_Py_ID(debug), &_Py_ID(skip_non_matching_threads), &_Py_ID(native), &_Py_ID(gc), &_Py_ID(cache_frames), &_Py_ID(stats), },
|
||||
.ob_item = { &_Py_ID(pid), &_Py_ID(all_threads), &_Py_ID(only_active_thread), &_Py_ID(mode), &_Py_ID(debug), &_Py_ID(skip_non_matching_threads), &_Py_ID(native), &_Py_ID(gc), &_Py_ID(opcodes), &_Py_ID(cache_frames), &_Py_ID(stats), },
|
||||
};
|
||||
#undef NUM_KEYWORDS
|
||||
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
|
||||
|
|
@ -79,14 +83,14 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
|
|||
# define KWTUPLE NULL
|
||||
#endif // !Py_BUILD_CORE
|
||||
|
||||
static const char * const _keywords[] = {"pid", "all_threads", "only_active_thread", "mode", "debug", "skip_non_matching_threads", "native", "gc", "cache_frames", "stats", NULL};
|
||||
static const char * const _keywords[] = {"pid", "all_threads", "only_active_thread", "mode", "debug", "skip_non_matching_threads", "native", "gc", "opcodes", "cache_frames", "stats", NULL};
|
||||
static _PyArg_Parser _parser = {
|
||||
.keywords = _keywords,
|
||||
.fname = "RemoteUnwinder",
|
||||
.kwtuple = KWTUPLE,
|
||||
};
|
||||
#undef KWTUPLE
|
||||
PyObject *argsbuf[10];
|
||||
PyObject *argsbuf[11];
|
||||
PyObject * const *fastargs;
|
||||
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
|
||||
Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1;
|
||||
|
|
@ -98,6 +102,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
|
|||
int skip_non_matching_threads = 1;
|
||||
int native = 0;
|
||||
int gc = 0;
|
||||
int opcodes = 0;
|
||||
int cache_frames = 0;
|
||||
int stats = 0;
|
||||
|
||||
|
|
@ -177,7 +182,16 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
|
|||
}
|
||||
}
|
||||
if (fastargs[8]) {
|
||||
cache_frames = PyObject_IsTrue(fastargs[8]);
|
||||
opcodes = PyObject_IsTrue(fastargs[8]);
|
||||
if (opcodes < 0) {
|
||||
goto exit;
|
||||
}
|
||||
if (!--noptargs) {
|
||||
goto skip_optional_kwonly;
|
||||
}
|
||||
}
|
||||
if (fastargs[9]) {
|
||||
cache_frames = PyObject_IsTrue(fastargs[9]);
|
||||
if (cache_frames < 0) {
|
||||
goto exit;
|
||||
}
|
||||
|
|
@ -185,12 +199,12 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
|
|||
goto skip_optional_kwonly;
|
||||
}
|
||||
}
|
||||
stats = PyObject_IsTrue(fastargs[9]);
|
||||
stats = PyObject_IsTrue(fastargs[10]);
|
||||
if (stats < 0) {
|
||||
goto exit;
|
||||
}
|
||||
skip_optional_kwonly:
|
||||
return_value = _remote_debugging_RemoteUnwinder___init___impl((RemoteUnwinderObject *)self, pid, all_threads, only_active_thread, mode, debug, skip_non_matching_threads, native, gc, cache_frames, stats);
|
||||
return_value = _remote_debugging_RemoteUnwinder___init___impl((RemoteUnwinderObject *)self, pid, all_threads, only_active_thread, mode, debug, skip_non_matching_threads, native, gc, opcodes, cache_frames, stats);
|
||||
|
||||
exit:
|
||||
return return_value;
|
||||
|
|
@ -419,4 +433,4 @@ _remote_debugging_RemoteUnwinder_get_stats(PyObject *self, PyObject *Py_UNUSED(i
|
|||
|
||||
return return_value;
|
||||
}
|
||||
/*[clinic end generated code: output=f1fd6c1d4c4c7254 input=a9049054013a1b77]*/
|
||||
/*[clinic end generated code: output=1943fb7a56197e39 input=a9049054013a1b77]*/
|
||||
|
|
|
|||
|
|
@ -155,48 +155,45 @@ parse_linetable(const uintptr_t addrq, const char* linetable, int firstlineno, L
|
|||
{
|
||||
const uint8_t* ptr = (const uint8_t*)(linetable);
|
||||
uintptr_t addr = 0;
|
||||
info->lineno = firstlineno;
|
||||
int computed_line = firstlineno; // Running accumulator, separate from output
|
||||
|
||||
while (*ptr != '\0') {
|
||||
// See InternalDocs/code_objects.md for where these magic numbers are from
|
||||
// and for the decoding algorithm.
|
||||
uint8_t first_byte = *(ptr++);
|
||||
uint8_t code = (first_byte >> 3) & 15;
|
||||
size_t length = (first_byte & 7) + 1;
|
||||
uintptr_t end_addr = addr + length;
|
||||
|
||||
switch (code) {
|
||||
case PY_CODE_LOCATION_INFO_NONE: {
|
||||
case PY_CODE_LOCATION_INFO_NONE:
|
||||
info->lineno = info->end_lineno = -1;
|
||||
info->column = info->end_column = -1;
|
||||
break;
|
||||
}
|
||||
case PY_CODE_LOCATION_INFO_LONG: {
|
||||
int line_delta = scan_signed_varint(&ptr);
|
||||
info->lineno += line_delta;
|
||||
info->end_lineno = info->lineno + scan_varint(&ptr);
|
||||
case PY_CODE_LOCATION_INFO_LONG:
|
||||
computed_line += scan_signed_varint(&ptr);
|
||||
info->lineno = computed_line;
|
||||
info->end_lineno = computed_line + scan_varint(&ptr);
|
||||
info->column = scan_varint(&ptr) - 1;
|
||||
info->end_column = scan_varint(&ptr) - 1;
|
||||
break;
|
||||
}
|
||||
case PY_CODE_LOCATION_INFO_NO_COLUMNS: {
|
||||
int line_delta = scan_signed_varint(&ptr);
|
||||
info->lineno += line_delta;
|
||||
case PY_CODE_LOCATION_INFO_NO_COLUMNS:
|
||||
computed_line += scan_signed_varint(&ptr);
|
||||
info->lineno = info->end_lineno = computed_line;
|
||||
info->column = info->end_column = -1;
|
||||
break;
|
||||
}
|
||||
case PY_CODE_LOCATION_INFO_ONE_LINE0:
|
||||
case PY_CODE_LOCATION_INFO_ONE_LINE1:
|
||||
case PY_CODE_LOCATION_INFO_ONE_LINE2: {
|
||||
int line_delta = code - 10;
|
||||
info->lineno += line_delta;
|
||||
info->end_lineno = info->lineno;
|
||||
case PY_CODE_LOCATION_INFO_ONE_LINE2:
|
||||
computed_line += code - 10;
|
||||
info->lineno = info->end_lineno = computed_line;
|
||||
info->column = *(ptr++);
|
||||
info->end_column = *(ptr++);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
uint8_t second_byte = *(ptr++);
|
||||
if ((second_byte & 128) != 0) {
|
||||
return false;
|
||||
}
|
||||
info->lineno = info->end_lineno = computed_line;
|
||||
info->column = code << 3 | (second_byte >> 4);
|
||||
info->end_column = info->column + (second_byte & 15);
|
||||
break;
|
||||
|
|
@ -215,8 +212,50 @@ parse_linetable(const uintptr_t addrq, const char* linetable, int firstlineno, L
|
|||
* ============================================================================ */
|
||||
|
||||
PyObject *
|
||||
make_frame_info(RemoteUnwinderObject *unwinder, PyObject *file, PyObject *line,
|
||||
PyObject *func)
|
||||
make_location_info(RemoteUnwinderObject *unwinder, int lineno, int end_lineno,
|
||||
int col_offset, int end_col_offset)
|
||||
{
|
||||
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
|
||||
PyObject *info = PyStructSequence_New(state->LocationInfo_Type);
|
||||
if (info == NULL) {
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create LocationInfo");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject *py_lineno = PyLong_FromLong(lineno);
|
||||
if (py_lineno == NULL) {
|
||||
Py_DECREF(info);
|
||||
return NULL;
|
||||
}
|
||||
PyStructSequence_SetItem(info, 0, py_lineno); // steals reference
|
||||
|
||||
PyObject *py_end_lineno = PyLong_FromLong(end_lineno);
|
||||
if (py_end_lineno == NULL) {
|
||||
Py_DECREF(info);
|
||||
return NULL;
|
||||
}
|
||||
PyStructSequence_SetItem(info, 1, py_end_lineno); // steals reference
|
||||
|
||||
PyObject *py_col_offset = PyLong_FromLong(col_offset);
|
||||
if (py_col_offset == NULL) {
|
||||
Py_DECREF(info);
|
||||
return NULL;
|
||||
}
|
||||
PyStructSequence_SetItem(info, 2, py_col_offset); // steals reference
|
||||
|
||||
PyObject *py_end_col_offset = PyLong_FromLong(end_col_offset);
|
||||
if (py_end_col_offset == NULL) {
|
||||
Py_DECREF(info);
|
||||
return NULL;
|
||||
}
|
||||
PyStructSequence_SetItem(info, 3, py_end_col_offset); // steals reference
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
PyObject *
|
||||
make_frame_info(RemoteUnwinderObject *unwinder, PyObject *file, PyObject *location,
|
||||
PyObject *func, PyObject *opcode)
|
||||
{
|
||||
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
|
||||
PyObject *info = PyStructSequence_New(state->FrameInfo_Type);
|
||||
|
|
@ -225,11 +264,13 @@ make_frame_info(RemoteUnwinderObject *unwinder, PyObject *file, PyObject *line,
|
|||
return NULL;
|
||||
}
|
||||
Py_INCREF(file);
|
||||
Py_INCREF(line);
|
||||
Py_INCREF(location);
|
||||
Py_INCREF(func);
|
||||
Py_INCREF(opcode);
|
||||
PyStructSequence_SetItem(info, 0, file);
|
||||
PyStructSequence_SetItem(info, 1, line);
|
||||
PyStructSequence_SetItem(info, 1, location);
|
||||
PyStructSequence_SetItem(info, 2, func);
|
||||
PyStructSequence_SetItem(info, 3, opcode);
|
||||
return info;
|
||||
}
|
||||
|
||||
|
|
@ -370,16 +411,43 @@ parse_code_object(RemoteUnwinderObject *unwinder,
|
|||
meta->first_lineno, &info);
|
||||
if (!ok) {
|
||||
info.lineno = -1;
|
||||
info.end_lineno = -1;
|
||||
info.column = -1;
|
||||
info.end_column = -1;
|
||||
}
|
||||
|
||||
PyObject *lineno = PyLong_FromLong(info.lineno);
|
||||
if (!lineno) {
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create line number object");
|
||||
// Create the LocationInfo structseq: (lineno, end_lineno, col_offset, end_col_offset)
|
||||
PyObject *location = make_location_info(unwinder,
|
||||
info.lineno,
|
||||
info.end_lineno,
|
||||
info.column,
|
||||
info.end_column);
|
||||
if (!location) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
PyObject *tuple = make_frame_info(unwinder, meta->file_name, lineno, meta->func_name);
|
||||
Py_DECREF(lineno);
|
||||
// Read the instruction opcode from target process if opcodes flag is set
|
||||
PyObject *opcode_obj = NULL;
|
||||
if (unwinder->opcodes) {
|
||||
uint16_t instruction_word = 0;
|
||||
if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, ip,
|
||||
sizeof(uint16_t), &instruction_word) == 0) {
|
||||
opcode_obj = PyLong_FromLong(instruction_word & 0xFF);
|
||||
if (!opcode_obj) {
|
||||
Py_DECREF(location);
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create opcode object");
|
||||
goto error;
|
||||
}
|
||||
} else {
|
||||
// Opcode read failed - clear the exception since opcode is optional
|
||||
PyErr_Clear();
|
||||
}
|
||||
}
|
||||
|
||||
PyObject *tuple = make_frame_info(unwinder, meta->file_name, location,
|
||||
meta->func_name, opcode_obj ? opcode_obj : Py_None);
|
||||
Py_DECREF(location);
|
||||
Py_XDECREF(opcode_obj);
|
||||
if (!tuple) {
|
||||
goto error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -337,8 +337,9 @@ process_frame_chain(
|
|||
extra_frame = &_Py_STR(native);
|
||||
}
|
||||
if (extra_frame) {
|
||||
// Use "~" as file, None as location (synthetic frame), None as opcode
|
||||
PyObject *extra_frame_info = make_frame_info(
|
||||
unwinder, _Py_LATIN1_CHR('~'), _PyLong_GetZero(), extra_frame);
|
||||
unwinder, _Py_LATIN1_CHR('~'), Py_None, extra_frame, Py_None);
|
||||
if (extra_frame_info == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,11 +28,28 @@ PyStructSequence_Desc TaskInfo_desc = {
|
|||
4
|
||||
};
|
||||
|
||||
// LocationInfo structseq type
|
||||
static PyStructSequence_Field LocationInfo_fields[] = {
|
||||
{"lineno", "Line number"},
|
||||
{"end_lineno", "End line number"},
|
||||
{"col_offset", "Column offset"},
|
||||
{"end_col_offset", "End column offset"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyStructSequence_Desc LocationInfo_desc = {
|
||||
"_remote_debugging.LocationInfo",
|
||||
"Source location information: (lineno, end_lineno, col_offset, end_col_offset)",
|
||||
LocationInfo_fields,
|
||||
4
|
||||
};
|
||||
|
||||
// FrameInfo structseq type
|
||||
static PyStructSequence_Field FrameInfo_fields[] = {
|
||||
{"filename", "Source code filename"},
|
||||
{"lineno", "Line number"},
|
||||
{"location", "LocationInfo structseq or None for synthetic frames"},
|
||||
{"funcname", "Function name"},
|
||||
{"opcode", "Opcode being executed (None if not gathered)"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -40,7 +57,7 @@ PyStructSequence_Desc FrameInfo_desc = {
|
|||
"_remote_debugging.FrameInfo",
|
||||
"Information about a frame",
|
||||
FrameInfo_fields,
|
||||
3
|
||||
4
|
||||
};
|
||||
|
||||
// CoroInfo structseq type
|
||||
|
|
@ -235,6 +252,7 @@ _remote_debugging.RemoteUnwinder.__init__
|
|||
skip_non_matching_threads: bool = True
|
||||
native: bool = False
|
||||
gc: bool = False
|
||||
opcodes: bool = False
|
||||
cache_frames: bool = False
|
||||
stats: bool = False
|
||||
|
||||
|
|
@ -255,6 +273,8 @@ Initialize a new RemoteUnwinder object for debugging a remote Python process.
|
|||
non-Python code.
|
||||
gc: If True, include artificial "<GC>" frames to denote active garbage
|
||||
collection.
|
||||
opcodes: If True, gather bytecode opcode information for instruction-level
|
||||
profiling.
|
||||
cache_frames: If True, enable frame caching optimization to avoid re-reading
|
||||
unchanged parent frames between samples.
|
||||
stats: If True, collect statistics about cache hits, memory reads, etc.
|
||||
|
|
@ -277,8 +297,9 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
|
|||
int mode, int debug,
|
||||
int skip_non_matching_threads,
|
||||
int native, int gc,
|
||||
int cache_frames, int stats)
|
||||
/*[clinic end generated code: output=b34ef8cce013c975 input=df2221ef114c3d6a]*/
|
||||
int opcodes, int cache_frames,
|
||||
int stats)
|
||||
/*[clinic end generated code: output=0031f743f4b9ad52 input=8fb61b24102dec6e]*/
|
||||
{
|
||||
// Validate that all_threads and only_active_thread are not both True
|
||||
if (all_threads && only_active_thread) {
|
||||
|
|
@ -297,6 +318,7 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
|
|||
|
||||
self->native = native;
|
||||
self->gc = gc;
|
||||
self->opcodes = opcodes;
|
||||
self->cache_frames = cache_frames;
|
||||
self->collect_stats = stats;
|
||||
self->stale_invalidation_counter = 0;
|
||||
|
|
@ -978,6 +1000,14 @@ _remote_debugging_exec(PyObject *m)
|
|||
return -1;
|
||||
}
|
||||
|
||||
st->LocationInfo_Type = PyStructSequence_NewType(&LocationInfo_desc);
|
||||
if (st->LocationInfo_Type == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (PyModule_AddType(m, st->LocationInfo_Type) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
st->FrameInfo_Type = PyStructSequence_NewType(&FrameInfo_desc);
|
||||
if (st->FrameInfo_Type == NULL) {
|
||||
return -1;
|
||||
|
|
@ -1051,6 +1081,7 @@ remote_debugging_traverse(PyObject *mod, visitproc visit, void *arg)
|
|||
RemoteDebuggingState *state = RemoteDebugging_GetState(mod);
|
||||
Py_VISIT(state->RemoteDebugging_Type);
|
||||
Py_VISIT(state->TaskInfo_Type);
|
||||
Py_VISIT(state->LocationInfo_Type);
|
||||
Py_VISIT(state->FrameInfo_Type);
|
||||
Py_VISIT(state->CoroInfo_Type);
|
||||
Py_VISIT(state->ThreadInfo_Type);
|
||||
|
|
@ -1065,6 +1096,7 @@ remote_debugging_clear(PyObject *mod)
|
|||
RemoteDebuggingState *state = RemoteDebugging_GetState(mod);
|
||||
Py_CLEAR(state->RemoteDebugging_Type);
|
||||
Py_CLEAR(state->TaskInfo_Type);
|
||||
Py_CLEAR(state->LocationInfo_Type);
|
||||
Py_CLEAR(state->FrameInfo_Type);
|
||||
Py_CLEAR(state->CoroInfo_Type);
|
||||
Py_CLEAR(state->ThreadInfo_Type);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue