]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-138122: Make the tachyon profiler opcode-aware (#142394)
authorPablo Galindo Salgado <Pablogsal@gmail.com>
Thu, 11 Dec 2025 03:41:47 +0000 (03:41 +0000)
committerGitHub <noreply@github.com>
Thu, 11 Dec 2025 03:41:47 +0000 (03:41 +0000)
36 files changed:
Doc/library/profiling.sampling.rst
Doc/sphinx-warnings.txt [new file with mode: 0644]
Include/internal/pycore_global_objects_fini_generated.h
Include/internal/pycore_global_strings.h
Include/internal/pycore_runtime_init_generated.h
Include/internal/pycore_unicodeobject_generated.h
Lib/profiling/sampling/_flamegraph_assets/flamegraph.css
Lib/profiling/sampling/_flamegraph_assets/flamegraph.js
Lib/profiling/sampling/_heatmap_assets/heatmap.css
Lib/profiling/sampling/_heatmap_assets/heatmap.js
Lib/profiling/sampling/_heatmap_assets/heatmap_pyfile_template.html
Lib/profiling/sampling/_shared_assets/base.css
Lib/profiling/sampling/cli.py
Lib/profiling/sampling/collector.py
Lib/profiling/sampling/constants.py
Lib/profiling/sampling/gecko_collector.py
Lib/profiling/sampling/heatmap_collector.py
Lib/profiling/sampling/live_collector/collector.py
Lib/profiling/sampling/live_collector/constants.py
Lib/profiling/sampling/live_collector/widgets.py
Lib/profiling/sampling/opcode_utils.py [new file with mode: 0644]
Lib/profiling/sampling/pstats_collector.py
Lib/profiling/sampling/sample.py
Lib/profiling/sampling/stack_collector.py
Lib/test/test_external_inspection.py
Lib/test/test_profiling/test_heatmap.py
Lib/test/test_profiling/test_sampling_profiler/_live_collector_helpers.py
Lib/test/test_profiling/test_sampling_profiler/mocks.py
Lib/test/test_profiling/test_sampling_profiler/test_collectors.py
Lib/test/test_profiling/test_sampling_profiler/test_integration.py
Misc/NEWS.d/next/Library/2025-12-07-23-21-13.gh-issue-138122.m3EF9E.rst [new file with mode: 0644]
Modules/_remote_debugging/_remote_debugging.h
Modules/_remote_debugging/clinic/module.c.h
Modules/_remote_debugging/code_objects.c
Modules/_remote_debugging/frames.c
Modules/_remote_debugging/module.c

index e2e354c3d134fef9fb444c8a0b4508c45e802c3e..23e9173a815d2208a5cd074ea0021b354cbd2b3b 100644 (file)
@@ -13,6 +13,8 @@
 
 **Source code:** :source:`Lib/profiling/sampling/`
 
+.. program:: profiling.sampling
+
 --------------
 
 .. image:: tachyon-logo.png
@@ -146,6 +148,10 @@ Generate a line-by-line heatmap::
 
    python -m profiling.sampling run --heatmap script.py
 
+Enable opcode-level profiling to see which bytecode instructions are executing::
+
+   python -m profiling.sampling run --opcodes --flamegraph script.py
+
 
 Commands
 ========
@@ -308,7 +314,7 @@ The two most fundamental parameters are the sampling interval and duration.
 Together, these determine how many samples will be collected during a profiling
 session.
 
-The ``--interval`` option (``-i``) sets the time between samples in
+The :option:`--interval` option (:option:`-i`) sets the time between samples in
 microseconds. The default is 100 microseconds, which produces approximately
 10,000 samples per second::
 
@@ -319,7 +325,7 @@ cost of slightly higher profiler CPU usage. Higher intervals reduce profiler
 overhead but may miss short-lived functions. For most applications, the
 default interval provides a good balance between accuracy and overhead.
 
-The ``--duration`` option (``-d``) sets how long to profile in seconds. The
+The :option:`--duration` option (:option:`-d`) sets how long to profile in seconds. The
 default is 10 seconds::
 
    python -m profiling.sampling run -d 60 script.py
@@ -337,8 +343,8 @@ Python programs often use multiple threads, whether explicitly through the
 :mod:`threading` module or implicitly through libraries that manage thread
 pools.
 
-By default, the profiler samples only the main thread. The ``--all-threads``
-option (``-a``) enables sampling of all threads in the process::
+By default, the profiler samples only the main thread. The :option:`--all-threads`
+option (:option:`-a`) enables sampling of all threads in the process::
 
    python -m profiling.sampling run -a script.py
 
@@ -357,7 +363,7 @@ additional context about what the interpreter is doing at the moment each
 sample is taken. These synthetic frames help distinguish different types of
 execution that would otherwise be invisible.
 
-The ``--native`` option adds ``<native>`` frames to indicate when Python has
+The :option:`--native` option adds ``<native>`` frames to indicate when Python has
 called into C code (extension modules, built-in functions, or the interpreter
 itself)::
 
@@ -369,7 +375,7 @@ in the Python function that made the call. This is useful when optimizing
 code that makes heavy use of C extensions like NumPy or database drivers.
 
 By default, the profiler includes ``<GC>`` frames when garbage collection is
-active. The ``--no-gc`` option suppresses these frames::
+active. The :option:`--no-gc` option suppresses these frames::
 
    python -m profiling.sampling run --no-gc script.py
 
@@ -379,10 +385,48 @@ see substantial time in ``<GC>`` frames, consider investigating object
 allocation rates or using object pooling.
 
 
+Opcode-aware profiling
+----------------------
+
+The :option:`--opcodes` option enables instruction-level profiling that captures
+which Python bytecode instructions are executing at each sample::
+
+   python -m profiling.sampling run --opcodes --flamegraph script.py
+
+This feature provides visibility into Python's bytecode execution, including
+adaptive specialization optimizations. When a generic instruction like
+``LOAD_ATTR`` is specialized at runtime into a more efficient variant like
+``LOAD_ATTR_INSTANCE_VALUE``, the profiler shows both the specialized name
+and the base instruction.
+
+Opcode information appears in several output formats:
+
+- **Live mode**: An opcode panel shows instruction-level statistics for the
+  selected function, accessible via keyboard navigation
+- **Flame graphs**: Nodes display opcode information when available, helping
+  identify which instructions consume the most time
+- **Heatmap**: Expandable bytecode panels per source line show instruction
+  breakdown with specialization percentages
+- **Gecko format**: Opcode transitions are emitted as interval markers in the
+  Firefox Profiler timeline
+
+This level of detail is particularly useful for:
+
+- Understanding the performance impact of Python's adaptive specialization
+- Identifying hot bytecode instructions that might benefit from optimization
+- Analyzing the effectiveness of different code patterns at the instruction level
+- Debugging performance issues that occur at the bytecode level
+
+The :option:`--opcodes` option is compatible with :option:`--live`, :option:`--flamegraph`,
+:option:`--heatmap`, and :option:`--gecko` formats. It requires additional memory to store
+opcode information and may slightly reduce sampling performance, but provides
+unprecedented visibility into Python's execution model.
+
+
 Real-time statistics
 --------------------
 
-The ``--realtime-stats`` option displays sampling rate statistics during
+The :option:`--realtime-stats` option displays sampling rate statistics during
 profiling::
 
    python -m profiling.sampling run --realtime-stats script.py
@@ -434,7 +478,7 @@ CPU execution time, or time spent holding the global interpreter lock.
 Wall-clock mode
 ---------------
 
-Wall-clock mode (``--mode=wall``) captures all samples regardless of what the
+Wall-clock mode (:option:`--mode`\ ``=wall``) captures all samples regardless of what the
 thread is doing. This is the default mode and provides a complete picture of
 where time passes during program execution::
 
@@ -454,7 +498,7 @@ latency.
 CPU mode
 --------
 
-CPU mode (``--mode=cpu``) records samples only when the thread is actually
+CPU mode (:option:`--mode`\ ``=cpu``) records samples only when the thread is actually
 executing on a CPU core::
 
    python -m profiling.sampling run --mode=cpu script.py
@@ -488,7 +532,7 @@ connection pooling, or reducing wait time instead.
 GIL mode
 --------
 
-GIL mode (``--mode=gil``) records samples only when the thread holds Python's
+GIL mode (:option:`--mode`\ ``=gil``) records samples only when the thread holds Python's
 global interpreter lock::
 
    python -m profiling.sampling run --mode=gil script.py
@@ -520,7 +564,7 @@ output goes to stdout, a file, or a directory depending on the format.
 pstats format
 -------------
 
-The pstats format (``--pstats``) produces a text table similar to what
+The pstats format (:option:`--pstats`) produces a text table similar to what
 deterministic profilers generate. This is the default output format::
 
    python -m profiling.sampling run script.py
@@ -567,31 +611,31 @@ interesting functions that highlights:
   samples (high cumulative/direct multiplier). These are frequently-nested
   functions that appear deep in many call chains.
 
-Use ``--no-summary`` to suppress both the legend and summary sections.
+Use :option:`--no-summary` to suppress both the legend and summary sections.
 
 To save pstats output to a file instead of stdout::
 
    python -m profiling.sampling run -o profile.txt script.py
 
 The pstats format supports several options for controlling the display.
-The ``--sort`` option determines the column used for ordering results::
+The :option:`--sort` option determines the column used for ordering results::
 
    python -m profiling.sampling run --sort=tottime script.py
    python -m profiling.sampling run --sort=cumtime script.py
    python -m profiling.sampling run --sort=nsamples script.py
 
-The ``--limit`` option restricts output to the top N entries::
+The :option:`--limit` option restricts output to the top N entries::
 
    python -m profiling.sampling run --limit=30 script.py
 
-The ``--no-summary`` option suppresses the header summary that precedes the
+The :option:`--no-summary` option suppresses the header summary that precedes the
 statistics table.
 
 
 Collapsed stacks format
 -----------------------
 
-Collapsed stacks format (``--collapsed``) produces one line per unique call
+Collapsed stacks format (:option:`--collapsed`) produces one line per unique call
 stack, with a count of how many times that stack was sampled::
 
    python -m profiling.sampling run --collapsed script.py
@@ -621,7 +665,7 @@ visualization where you can click to zoom into specific call paths.
 Flame graph format
 ------------------
 
-Flame graph format (``--flamegraph``) produces a self-contained HTML file with
+Flame graph format (:option:`--flamegraph`) produces a self-contained HTML file with
 an interactive flame graph visualization::
 
    python -m profiling.sampling run --flamegraph script.py
@@ -667,7 +711,7 @@ or through their callees.
 Gecko format
 ------------
 
-Gecko format (``--gecko``) produces JSON output compatible with the Firefox
+Gecko format (:option:`--gecko`) produces JSON output compatible with the Firefox
 Profiler::
 
    python -m profiling.sampling run --gecko script.py
@@ -694,14 +738,14 @@ Firefox Profiler timeline:
 - **Code type markers**: distinguish Python code from native (C extension) code
 - **GC markers**: indicate garbage collection activity
 
-For this reason, the ``--mode`` option is not available with Gecko format;
+For this reason, the :option:`--mode` option is not available with Gecko format;
 all relevant data is captured automatically.
 
 
 Heatmap format
 --------------
 
-Heatmap format (``--heatmap``) generates an interactive HTML visualization
+Heatmap format (:option:`--heatmap`) generates an interactive HTML visualization
 showing sample counts at the source line level::
 
    python -m profiling.sampling run --heatmap script.py
@@ -744,7 +788,7 @@ interpretation of hierarchical visualizations.
 Live mode
 =========
 
-Live mode (``--live``) provides a terminal-based real-time view of profiling
+Live mode (:option:`--live`) provides a terminal-based real-time view of profiling
 data, similar to the ``top`` command for system processes::
 
    python -m profiling.sampling run --live script.py
@@ -760,6 +804,11 @@ and thread status statistics (GIL held percentage, CPU usage, GC time). The
 main table shows function statistics with the currently sorted column indicated
 by an arrow (▼).
 
+When :option:`--opcodes` is enabled, an additional opcode panel appears below the
+main table, showing instruction-level statistics for the currently selected
+function. This panel displays which bytecode instructions are executing most
+frequently, including specialized variants and their base opcodes.
+
 
 Keyboard commands
 -----------------
@@ -813,12 +862,17 @@ Within live mode, keyboard commands control the display:
 :kbd:`h` or :kbd:`?`
    Show the help screen with all available commands.
 
+:kbd:`j` / :kbd:`k` (or :kbd:`Up` / :kbd:`Down`)
+   Navigate through opcode entries in the opcode panel (when ``--opcodes`` is
+   enabled). These keys scroll through the instruction-level statistics for the
+   currently selected function.
+
 When profiling finishes (duration expires or target process exits), the display
 shows a "PROFILING COMPLETE" banner and freezes the final results. You can
 still navigate, sort, and filter the results before pressing :kbd:`q` to exit.
 
-Live mode is incompatible with output format options (``--collapsed``,
-``--flamegraph``, and so on) because it uses an interactive terminal
+Live mode is incompatible with output format options (:option:`--collapsed`,
+:option:`--flamegraph`, and so on) because it uses an interactive terminal
 interface rather than producing file output.
 
 
@@ -826,7 +880,7 @@ Async-aware profiling
 =====================
 
 For programs using :mod:`asyncio`, the profiler offers async-aware mode
-(``--async-aware``) that reconstructs call stacks based on the task structure
+(:option:`--async-aware`) that reconstructs call stacks based on the task structure
 rather than the raw Python frames::
 
    python -m profiling.sampling run --async-aware async_script.py
@@ -846,16 +900,16 @@ and presenting stacks that reflect the ``await`` chain.
 Async modes
 -----------
 
-The ``--async-mode`` option controls which tasks appear in the profile::
+The :option:`--async-mode` option controls which tasks appear in the profile::
 
    python -m profiling.sampling run --async-aware --async-mode=running async_script.py
    python -m profiling.sampling run --async-aware --async-mode=all async_script.py
 
-With ``--async-mode=running`` (the default), only the task currently executing
+With :option:`--async-mode`\ ``=running`` (the default), only the task currently executing
 on the CPU is profiled. This shows where your program is actively spending time
 and is the typical choice for performance analysis.
 
-With ``--async-mode=all``, tasks that are suspended (awaiting I/O, locks, or
+With :option:`--async-mode`\ ``=all``, tasks that are suspended (awaiting I/O, locks, or
 other tasks) are also included. This mode is useful for understanding what your
 program is waiting on, but produces larger profiles since every suspended task
 appears in each sample.
@@ -884,8 +938,8 @@ Option restrictions
 -------------------
 
 Async-aware mode uses a different stack reconstruction mechanism and is
-incompatible with: ``--native``, ``--no-gc``, ``--all-threads``, and
-``--mode=cpu`` or ``--mode=gil``.
+incompatible with: :option:`--native`, :option:`--no-gc`, :option:`--all-threads`, and
+:option:`--mode`\ ``=cpu`` or :option:`--mode`\ ``=gil``.
 
 
 Command-line interface
@@ -939,6 +993,13 @@ Sampling options
 
    Enable async-aware profiling for asyncio programs.
 
+.. option:: --opcodes
+
+   Gather bytecode opcode information for instruction-level profiling. Shows
+   which bytecode instructions are executing, including specializations.
+   Compatible with ``--live``, ``--flamegraph``, ``--heatmap``, and ``--gecko``
+   formats only.
+
 
 Mode options
 ------------
diff --git a/Doc/sphinx-warnings.txt b/Doc/sphinx-warnings.txt
new file mode 100644 (file)
index 0000000..777b34a
--- /dev/null
@@ -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]
index d23d6d4f91bc284802a0ea2c86026125b784b191..6473a3c64a6c23a199b78312fb79e17aa5b31ded 100644 (file)
@@ -1937,6 +1937,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(only_keys));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(oparg));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opcode));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opcodes));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(open));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opener));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(operation));
index 5c3ea474ad09b7805aa9322814f542f36d8ac785..ec720de2524e6e80fcb22fac7ffce6710bb45ddf 100644 (file)
@@ -660,6 +660,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(only_keys)
         STRUCT_FOR_ID(oparg)
         STRUCT_FOR_ID(opcode)
+        STRUCT_FOR_ID(opcodes)
         STRUCT_FOR_ID(open)
         STRUCT_FOR_ID(opener)
         STRUCT_FOR_ID(operation)
index 31d88339a134252b1e96a3bf9dde5f64f1e1e103..b32083db98e29e7556a938bdd810193ae5c5f8ca 100644 (file)
@@ -1935,6 +1935,7 @@ extern "C" {
     INIT_ID(only_keys), \
     INIT_ID(oparg), \
     INIT_ID(opcode), \
+    INIT_ID(opcodes), \
     INIT_ID(open), \
     INIT_ID(opener), \
     INIT_ID(operation), \
index c5b01ff9876643a4222b5391191439c212ce220b..f3756fde2c40737d569074ad31c36a007834ac00 100644 (file)
@@ -2420,6 +2420,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
     assert(PyUnicode_GET_LENGTH(string) != 1);
+    string = &_Py_ID(opcodes);
+    _PyUnicode_InternStatic(interp, &string);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    assert(PyUnicode_GET_LENGTH(string) != 1);
     string = &_Py_ID(open);
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
index a6d5a3b90416fdaa3ce68978c56a33a713d08c82..ee699f2982616aaefd7498e1549d68f21b337e94 100644 (file)
@@ -862,6 +862,84 @@ body.resizing-sidebar {
   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)
    -------------------------------------------------------------------------- */
index 494d156a8dddfcc54339f0219870fe9ac714e6ff..3076edd1d68cbac583f29ecef7b8fed1c71abd32 100644 (file)
@@ -8,6 +8,32 @@ let currentThreadFilter = 'all';
 // Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
 // and automatically switch with theme changes - no JS color arrays needed!
 
+// Opcode mappings - loaded from embedded data (generated by Python)
+let OPCODE_NAMES = {};
+let DEOPT_MAP = {};
+
+// Initialize opcode mappings from embedded data
+function initOpcodeMapping(data) {
+    if (data && data.opcode_mapping) {
+        OPCODE_NAMES = data.opcode_mapping.names || {};
+        DEOPT_MAP = data.opcode_mapping.deopt || {};
+    }
+}
+
+// Get opcode info from opcode number
+function getOpcodeInfo(opcode) {
+    const opname = OPCODE_NAMES[opcode] || `<${opcode}>`;
+    const baseOpcode = DEOPT_MAP[opcode];
+    const isSpecialized = baseOpcode !== undefined;
+    const baseOpname = isSpecialized ? (OPCODE_NAMES[baseOpcode] || `<${baseOpcode}>`) : opname;
+
+    return {
+        opname: opname,
+        baseOpname: baseOpname,
+        isSpecialized: isSpecialized
+    };
+}
+
 // ============================================================================
 // String Resolution
 // ============================================================================
@@ -249,6 +275,53 @@ function createPythonTooltip(data) {
         </div>`;
     }
 
+    // Create bytecode/opcode section if available
+    let opcodeSection = "";
+    const opcodes = d.data.opcodes;
+    if (opcodes && typeof opcodes === 'object' && Object.keys(opcodes).length > 0) {
+      // Sort opcodes by sample count (descending)
+      const sortedOpcodes = Object.entries(opcodes)
+        .sort((a, b) => b[1] - a[1])
+        .slice(0, 8); // Limit to top 8
+
+      const totalOpcodeSamples = sortedOpcodes.reduce((sum, [, count]) => sum + count, 0);
+      const maxCount = sortedOpcodes[0][1] || 1;
+
+      const opcodeLines = sortedOpcodes.map(([opcode, count]) => {
+        const opcodeInfo = getOpcodeInfo(parseInt(opcode, 10));
+        const pct = ((count / totalOpcodeSamples) * 100).toFixed(1);
+        const barWidth = (count / maxCount) * 100;
+        const specializedBadge = opcodeInfo.isSpecialized
+          ? '<span class="tooltip-opcode-badge">SPECIALIZED</span>'
+          : '';
+        const baseOpHint = opcodeInfo.isSpecialized
+          ? `<span class="tooltip-opcode-base-hint">(${opcodeInfo.baseOpname})</span>`
+          : '';
+        const nameClass = opcodeInfo.isSpecialized
+          ? 'tooltip-opcode-name specialized'
+          : 'tooltip-opcode-name';
+
+        return `
+          <div class="tooltip-opcode-row">
+            <div class="${nameClass}">
+              ${opcodeInfo.opname}${baseOpHint}${specializedBadge}
+            </div>
+            <div class="tooltip-opcode-count">${count.toLocaleString()} (${pct}%)</div>
+            <div class="tooltip-opcode-bar">
+              <div class="tooltip-opcode-bar-fill" style="width: ${barWidth}%;"></div>
+            </div>
+          </div>`;
+      }).join('');
+
+      opcodeSection = `
+        <div class="tooltip-opcodes">
+          <div class="tooltip-opcodes-title">Bytecode Instructions:</div>
+          <div class="tooltip-opcodes-list">
+            ${opcodeLines}
+          </div>
+        </div>`;
+    }
+
     const fileLocationHTML = isSpecialFrame ? "" : `
       <div class="tooltip-location">${filename}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
 
@@ -275,6 +348,7 @@ function createPythonTooltip(data) {
         ` : ''}
       </div>
       ${sourceSection}
+      ${opcodeSection}
       <div class="tooltip-hint">
         ${childCount > 0 ? "Click to zoom into this function" : "Leaf function - no children"}
       </div>
@@ -994,6 +1068,9 @@ function initFlamegraph() {
     processedData = resolveStringIndices(EMBEDDED_DATA);
   }
 
+  // Initialize opcode mapping from embedded data
+  initOpcodeMapping(EMBEDDED_DATA);
+
   originalData = processedData;
   initThreadFilter(processedData);
 
index b6f086f11cae5e21ddbc8ca7b5549792f31ebc05..669554a7e7e8f07d956e0af68131710e3f3b063f 100644 (file)
 }
 
 .legend-content {
-  width: 94%;
-  max-width: 100%;
-  margin: 0 auto;
+  width: 100%;
   display: flex;
   align-items: center;
   gap: 20px;
-  flex-wrap: wrap;
+  flex-wrap: nowrap;
+}
+
+.legend-separator {
+  width: 1px;
+  height: 24px;
+  background: var(--border);
+  flex-shrink: 0;
 }
 
 .legend-title {
   color: var(--text-primary);
   font-size: 13px;
   font-family: var(--font-sans);
+  flex-shrink: 0;
 }
 
 .legend-gradient {
-  flex: 1;
-  max-width: 300px;
-  height: 24px;
+  width: 150px;
+  flex-shrink: 0;
+  height: 20px;
   background: linear-gradient(90deg,
     var(--bg-tertiary) 0%,
     var(--heat-2) 25%,
   font-size: 11px;
   color: var(--text-muted);
   font-family: var(--font-sans);
+  flex-shrink: 0;
+}
+
+/* Legend Controls Group - wraps toggles and bytecode button together */
+.legend-controls {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  flex-shrink: 0;
+  margin-left: auto;
 }
 
 /* Toggle Switch Styles */
   user-select: none;
   font-family: var(--font-sans);
   transition: opacity var(--transition-fast);
+  flex-shrink: 0;
 }
 
 .toggle-switch:hover {
   font-size: 11px;
   font-weight: 500;
   color: var(--text-muted);
-  min-width: 55px;
-  text-align: right;
   transition: color var(--transition-fast);
-}
-
-.toggle-switch .toggle-label:last-child {
-  text-align: left;
+  white-space: nowrap;
+  display: inline-flex;
+  flex-direction: column;
 }
 
 .toggle-switch .toggle-label.active {
   font-weight: 600;
 }
 
+/* Reserve space for bold text to prevent layout shift on toggle */
+.toggle-switch .toggle-label::after {
+  content: attr(data-text);
+  font-weight: 600;
+  height: 0;
+  visibility: hidden;
+}
+
+.toggle-switch.disabled {
+  opacity: 0.4;
+  pointer-events: none;
+  cursor: not-allowed;
+}
+
 .toggle-track {
   position: relative;
   width: 36px;
   .stats-summary {
     grid-template-columns: repeat(2, 1fr);
   }
+
+  .legend-content {
+    flex-wrap: wrap;
+    justify-content: center;
+  }
+
+  .legend-controls {
+    margin-left: 0;
+  }
 }
 
 @media (max-width: 900px) {
 
   .legend-content {
     flex-direction: column;
+    align-items: center;
     gap: 12px;
   }
 
     width: 100%;
     max-width: none;
   }
+
+  .legend-separator {
+    width: 80%;
+    height: 1px;
+  }
+
+  .legend-controls {
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .legend-controls .toggle-switch {
+    justify-content: center;
+  }
+
+  .legend-controls .toggle-switch .toggle-label:first-child {
+    width: 70px;
+    text-align: right;
+  }
+
+  .legend-controls .toggle-switch .toggle-label:last-child {
+    width: 90px;
+    text-align: left;
+  }
+
+  /* Compact code columns on small screens */
+  .header-line-number,
+  .line-number {
+    width: 40px;
+  }
+
+  .header-samples-self,
+  .header-samples-cumulative,
+  .line-samples-self,
+  .line-samples-cumulative {
+    width: 55px;
+    font-size: 10px;
+  }
+
+  /* Adjust padding - headers need vertical, data rows don't */
+  .header-line-number,
+  .header-samples-self,
+  .header-samples-cumulative {
+    padding: 8px 4px;
+  }
+
+  .line-number,
+  .line-samples-self,
+  .line-samples-cumulative {
+    padding: 0 4px;
+  }
+}
+
+.bytecode-toggle {
+  flex-shrink: 0;
+  width: 20px;
+  height: 20px;
+  padding: 0;
+  margin: 0 4px;
+  border: none;
+  background: transparent;
+  color: var(--code-accent);
+  cursor: pointer;
+  font-size: 10px;
+  transition: transform var(--transition-fast), color var(--transition-fast);
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.bytecode-toggle:hover {
+  color: var(--accent);
+}
+
+.bytecode-spacer {
+  flex-shrink: 0;
+  width: 20px;
+  height: 20px;
+  margin: 0 4px;
+}
+
+.bytecode-panel {
+  margin-left: 90px;
+  padding: 8px 15px;
+  background: var(--bg-secondary);
+  border-left: 3px solid var(--accent);
+  font-family: var(--font-mono);
+  font-size: 12px;
+  margin-bottom: 4px;
+}
+
+/* Specialization summary bar */
+.bytecode-spec-summary {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 12px;
+  margin-bottom: 10px;
+  border-radius: var(--radius-sm);
+  background: rgba(100, 100, 100, 0.1);
+}
+
+.bytecode-spec-summary .spec-pct {
+  font-size: 1.4em;
+  font-weight: 700;
+}
+
+.bytecode-spec-summary .spec-label {
+  font-weight: 500;
+  text-transform: uppercase;
+  font-size: 0.85em;
+  letter-spacing: 0.5px;
+}
+
+.bytecode-spec-summary .spec-detail {
+  color: var(--text-secondary);
+  font-size: 0.9em;
+  margin-left: auto;
+}
+
+.bytecode-spec-summary.high {
+  background: var(--spec-high-bg);
+  border-left: 3px solid var(--spec-high);
+}
+.bytecode-spec-summary.high .spec-pct,
+.bytecode-spec-summary.high .spec-label {
+  color: var(--spec-high-text);
+}
+
+.bytecode-spec-summary.medium {
+  background: var(--spec-medium-bg);
+  border-left: 3px solid var(--spec-medium);
+}
+.bytecode-spec-summary.medium .spec-pct,
+.bytecode-spec-summary.medium .spec-label {
+  color: var(--spec-medium-text);
+}
+
+.bytecode-spec-summary.low {
+  background: var(--spec-low-bg);
+  border-left: 3px solid var(--spec-low);
+}
+.bytecode-spec-summary.low .spec-pct,
+.bytecode-spec-summary.low .spec-label {
+  color: var(--spec-low-text);
+}
+
+.bytecode-header {
+  display: grid;
+  grid-template-columns: 1fr 80px 80px;
+  gap: 12px;
+  padding: 4px 8px;
+  font-weight: 600;
+  color: var(--text-secondary);
+  border-bottom: 1px solid var(--code-border);
+  margin-bottom: 4px;
+}
+
+.bytecode-expand-all {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 12px;
+  background: var(--bg-secondary);
+  border: 1px solid var(--code-border);
+  border-radius: var(--radius-sm);
+  color: var(--text-secondary);
+  font-size: 12px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all var(--transition-fast);
+  flex-shrink: 0;
+}
+
+.bytecode-expand-all:hover,
+.bytecode-expand-all.expanded {
+  background: var(--accent);
+  color: white;
+  border-color: var(--accent);
+}
+
+.bytecode-expand-all .expand-icon {
+  font-size: 10px;
+  transition: transform var(--transition-fast);
+}
+
+.bytecode-expand-all.expanded .expand-icon {
+  transform: rotate(90deg);
+}
+
+/* ========================================
+   INSTRUCTION SPAN HIGHLIGHTING
+   (triggered only from bytecode panel hover)
+   ======================================== */
+
+/* Highlight from bytecode panel hover */
+.instr-span.highlight-from-bytecode {
+  outline: 3px solid #ff6b6b !important;
+  background-color: rgba(255, 107, 107, 0.4) !important;
+  border-radius: 2px;
+}
+
+/* Bytecode instruction row */
+.bytecode-instruction {
+  display: grid;
+  grid-template-columns: 1fr 80px 80px;
+  gap: 12px;
+  align-items: center;
+  padding: 4px 8px;
+  margin: 2px 0;
+  border-radius: var(--radius-sm);
+  cursor: pointer;
+  transition: background-color var(--transition-fast);
+}
+
+.bytecode-instruction:hover,
+.bytecode-instruction.highlight {
+  background-color: rgba(55, 118, 171, 0.15);
+}
+
+.bytecode-instruction[data-locations] {
+  cursor: pointer;
+}
+
+.bytecode-instruction[data-locations]:hover {
+  background-color: rgba(255, 107, 107, 0.2);
+}
+
+.bytecode-opname {
+  font-weight: 600;
+  font-family: var(--font-mono);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.bytecode-opname.specialized {
+  color: #2e7d32;
+}
+
+[data-theme="dark"] .bytecode-opname.specialized {
+  color: #81c784;
+}
+
+.bytecode-opname .base-op {
+  color: var(--code-text-muted);
+  font-weight: normal;
+  font-size: 0.9em;
+  margin-left: 4px;
+}
+
+.bytecode-samples {
+  text-align: right;
+  font-weight: 600;
+  color: var(--accent);
+  font-family: var(--font-mono);
+}
+
+.bytecode-samples.hot {
+  color: #ff6b6b;
+}
+
+.bytecode-heatbar {
+  width: 60px;
+  height: 12px;
+  background: var(--bg-secondary);
+  border-radius: 2px;
+  overflow: hidden;
+  border: 1px solid var(--code-border);
+}
+
+.bytecode-heatbar-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #00d4ff 0%, #ff6b00 100%);
+}
+
+.specialization-badge {
+  display: inline-block;
+  padding: 1px 6px;
+  font-size: 0.75em;
+  background: #e8f5e9;
+  color: #2e7d32;
+  border-radius: 3px;
+  margin-left: 6px;
+  font-weight: 600;
+}
+
+[data-theme="dark"] .specialization-badge {
+  background: rgba(129, 199, 132, 0.2);
+  color: #81c784;
+}
+
+.bytecode-empty {
+  color: var(--code-text-muted);
+  font-style: italic;
+  padding: 8px;
+}
+
+.bytecode-error {
+  color: #d32f2f;
+  font-style: italic;
+  padding: 8px;
+}
+
+/* ========================================
+   SPAN TOOLTIPS
+   ======================================== */
+
+.span-tooltip {
+  position: absolute;
+  z-index: 10000;
+  background: var(--bg-primary);
+  color: var(--text-primary);
+  padding: 10px 14px;
+  border-radius: var(--radius-md);
+  border: 1px solid var(--border);
+  font-family: var(--font-sans);
+  font-size: 12px;
+  box-shadow: var(--shadow-lg);
+  pointer-events: none;
+  min-width: 160px;
+  max-width: 300px;
+}
+
+.span-tooltip::after {
+  content: '';
+  position: absolute;
+  bottom: -7px;
+  left: 50%;
+  transform: translateX(-50%);
+  border-width: 7px 7px 0;
+  border-style: solid;
+  border-color: var(--bg-primary) transparent transparent;
+  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
+}
+
+.span-tooltip-header {
+  font-weight: 600;
+  margin-bottom: 8px;
+  padding-bottom: 6px;
+  border-bottom: 1px solid var(--border);
+  color: var(--text-primary);
+}
+
+.span-tooltip-header.hot {
+  color: #e65100;
+}
+
+.span-tooltip-header.warm {
+  color: #f59e0b;
+}
+
+.span-tooltip-header.cold {
+  color: var(--text-muted);
+}
+
+.span-tooltip-row {
+  display: flex;
+  justify-content: space-between;
+  margin: 4px 0;
+  gap: 16px;
+}
+
+.span-tooltip-label {
+  color: var(--text-secondary);
+}
+
+.span-tooltip-value {
+  font-weight: 600;
+  text-align: right;
+  color: var(--text-primary);
+}
+
+.span-tooltip-value.highlight {
+  color: var(--accent);
+}
+
+.span-tooltip-section {
+  font-weight: 600;
+  color: var(--text-secondary);
+  font-size: 11px;
+  margin-top: 8px;
+  margin-bottom: 4px;
+  padding-top: 6px;
+  border-top: 1px solid var(--border);
+}
+
+.span-tooltip-opcode {
+  font-family: var(--font-mono);
+  font-size: 11px;
+  color: var(--text-primary);
+  background: var(--bg-secondary);
+  padding: 3px 8px;
+  margin: 2px 0;
+  border-radius: var(--radius-sm);
+  border-left: 2px solid var(--accent);
 }
index 5a7ff5dd61ad3aa8469e1cc37802b82cc7356d1f..9cedb2d84698b6608934e35ca514e3b6107322d2 100644 (file)
@@ -289,7 +289,6 @@ function toggleColorMode() {
 // ============================================================================
 
 document.addEventListener('DOMContentLoaded', function() {
-    // Restore UI state (theme, etc.)
     restoreUIState();
     applyLineColors();
 
@@ -308,19 +307,38 @@ document.addEventListener('DOMContentLoaded', function() {
 
     // Initialize toggle buttons
     const toggleColdBtn = document.getElementById('toggle-cold');
-    if (toggleColdBtn) {
-        toggleColdBtn.addEventListener('click', toggleColdCode);
-    }
+    if (toggleColdBtn) toggleColdBtn.addEventListener('click', toggleColdCode);
 
     const colorModeBtn = document.getElementById('toggle-color-mode');
-    if (colorModeBtn) {
-        colorModeBtn.addEventListener('click', toggleColorMode);
+    if (colorModeBtn) colorModeBtn.addEventListener('click', toggleColorMode);
+
+    // Initialize specialization view toggle (hide if no bytecode data)
+    const hasBytecode = document.querySelectorAll('.bytecode-toggle').length > 0;
+
+    const specViewBtn = document.getElementById('toggle-spec-view');
+    if (specViewBtn) {
+        if (hasBytecode) {
+            specViewBtn.addEventListener('click', toggleSpecView);
+        } else {
+            specViewBtn.style.display = 'none';
+        }
     }
 
-    // Build scroll marker
-    setTimeout(buildScrollMarker, 200);
+    // Initialize expand-all bytecode button
+    const expandAllBtn = document.getElementById('toggle-all-bytecode');
+    if (expandAllBtn) {
+        if (hasBytecode) {
+            expandAllBtn.addEventListener('click', toggleAllBytecode);
+        } else {
+            expandAllBtn.style.display = 'none';
+        }
+    }
 
-    // Setup scroll-to-line behavior
+    // Initialize span tooltips
+    initSpanTooltips();
+
+    // Build scroll marker and scroll to target
+    setTimeout(buildScrollMarker, 200);
     setTimeout(scrollToTargetLine, 100);
 });
 
@@ -331,6 +349,400 @@ document.addEventListener('click', e => {
     }
 });
 
+// ========================================
+// SPECIALIZATION VIEW TOGGLE
+// ========================================
+
+let specViewEnabled = false;
+
+/**
+ * Calculate heat color for given intensity (0-1)
+ * Hot spans (>30%) get warm orange, cold spans get dimmed gray
+ * @param {number} intensity - Value between 0 and 1
+ * @returns {string} rgba color string
+ */
+function calculateHeatColor(intensity) {
+    // Hot threshold: only spans with >30% of max samples get color
+    if (intensity > 0.3) {
+        // Normalize intensity above threshold to 0-1
+        const normalizedIntensity = (intensity - 0.3) / 0.7;
+        // Warm orange-red with increasing opacity for hotter spans
+        const alpha = 0.25 + normalizedIntensity * 0.35;  // 0.25 to 0.6
+        const hotColor = getComputedStyle(document.documentElement).getPropertyValue('--span-hot-base').trim();
+        return `rgba(${hotColor}, ${alpha})`;
+    } else if (intensity > 0) {
+        // Cold spans: very subtle gray, almost invisible
+        const coldColor = getComputedStyle(document.documentElement).getPropertyValue('--span-cold-base').trim();
+        return `rgba(${coldColor}, 0.1)`;
+    }
+    return 'transparent';
+}
+
+/**
+ * Apply intensity-based heat colors to source spans
+ * Hot spans get orange highlight, cold spans get dimmed
+ * @param {boolean} enable - Whether to enable or disable span coloring
+ */
+function applySpanHeatColors(enable) {
+    document.querySelectorAll('.instr-span').forEach(span => {
+        const samples = enable ? (parseInt(span.dataset.samples) || 0) : 0;
+        if (samples > 0) {
+            const intensity = samples / (parseInt(span.dataset.maxSamples) || 1);
+            span.style.backgroundColor = calculateHeatColor(intensity);
+            span.style.borderRadius = '2px';
+            span.style.padding = '0 1px';
+            span.style.cursor = 'pointer';
+        } else {
+            span.style.cssText = '';
+        }
+    });
+}
+
+// ========================================
+// SPAN TOOLTIPS
+// ========================================
+
+let activeTooltip = null;
+
+/**
+ * Create and show tooltip for a span
+ */
+function showSpanTooltip(span) {
+    hideSpanTooltip();
+
+    const samples = parseInt(span.dataset.samples) || 0;
+    const maxSamples = parseInt(span.dataset.maxSamples) || 1;
+    const pct = span.dataset.pct || '0';
+    const opcodes = span.dataset.opcodes || '';
+
+    if (samples === 0) return;
+
+    const intensity = samples / maxSamples;
+    const isHot = intensity > 0.7;
+    const isWarm = intensity > 0.3;
+    const hotnessText = isHot ? 'Hot' : isWarm ? 'Warm' : 'Cold';
+    const hotnessClass = isHot ? 'hot' : isWarm ? 'warm' : 'cold';
+
+    // Build opcodes rows - each opcode on its own row
+    let opcodesHtml = '';
+    if (opcodes) {
+        const opcodeList = opcodes.split(',').map(op => op.trim()).filter(op => op);
+        if (opcodeList.length > 0) {
+            opcodesHtml = `
+                <div class="span-tooltip-section">Opcodes:</div>
+                ${opcodeList.map(op => `<div class="span-tooltip-opcode">${op}</div>`).join('')}
+            `;
+        }
+    }
+
+    const tooltip = document.createElement('div');
+    tooltip.className = 'span-tooltip';
+    tooltip.innerHTML = `
+        <div class="span-tooltip-header ${hotnessClass}">${hotnessText}</div>
+        <div class="span-tooltip-row">
+            <span class="span-tooltip-label">Samples:</span>
+            <span class="span-tooltip-value${isHot ? ' highlight' : ''}">${samples.toLocaleString()}</span>
+        </div>
+        <div class="span-tooltip-row">
+            <span class="span-tooltip-label">% of line:</span>
+            <span class="span-tooltip-value">${pct}%</span>
+        </div>
+        ${opcodesHtml}
+    `;
+
+    document.body.appendChild(tooltip);
+    activeTooltip = tooltip;
+
+    // Position tooltip above the span
+    const rect = span.getBoundingClientRect();
+    const tooltipRect = tooltip.getBoundingClientRect();
+
+    let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
+    let top = rect.top - tooltipRect.height - 8;
+
+    // Keep tooltip in viewport
+    if (left < 5) left = 5;
+    if (left + tooltipRect.width > window.innerWidth - 5) {
+        left = window.innerWidth - tooltipRect.width - 5;
+    }
+    if (top < 5) {
+        top = rect.bottom + 8; // Show below if no room above
+    }
+
+    tooltip.style.left = `${left + window.scrollX}px`;
+    tooltip.style.top = `${top + window.scrollY}px`;
+}
+
+/**
+ * Hide active tooltip
+ */
+function hideSpanTooltip() {
+    if (activeTooltip) {
+        activeTooltip.remove();
+        activeTooltip = null;
+    }
+}
+
+/**
+ * Initialize span tooltip handlers
+ */
+function initSpanTooltips() {
+    document.addEventListener('mouseover', (e) => {
+        const span = e.target.closest('.instr-span');
+        if (span && specViewEnabled) {
+            showSpanTooltip(span);
+        }
+    });
+
+    document.addEventListener('mouseout', (e) => {
+        const span = e.target.closest('.instr-span');
+        if (span) {
+            hideSpanTooltip();
+        }
+    });
+}
+
+function toggleSpecView() {
+    specViewEnabled = !specViewEnabled;
+    const lines = document.querySelectorAll('.code-line');
+
+    if (specViewEnabled) {
+        lines.forEach(line => {
+            const specColor = line.getAttribute('data-spec-color');
+            line.style.background = specColor || 'transparent';
+        });
+    } else {
+        applyLineColors();
+    }
+
+    applySpanHeatColors(specViewEnabled);
+    updateToggleUI('toggle-spec-view', specViewEnabled);
+
+    // Disable/enable color mode toggle based on spec view state
+    const colorModeToggle = document.getElementById('toggle-color-mode');
+    if (colorModeToggle) {
+        colorModeToggle.classList.toggle('disabled', specViewEnabled);
+    }
+
+    buildScrollMarker();
+}
+
+// ========================================
+// BYTECODE PANEL TOGGLE
+// ========================================
+
+/**
+ * Toggle bytecode panel visibility for a source line
+ * @param {HTMLElement} button - The toggle button that was clicked
+ */
+function toggleBytecode(button) {
+    const lineDiv = button.closest('.code-line');
+    const lineId = lineDiv.id;
+    const lineNum = lineId.replace('line-', '');
+    const panel = document.getElementById(`bytecode-${lineNum}`);
+
+    if (!panel) return;
+
+    const isExpanded = panel.style.display !== 'none';
+
+    if (isExpanded) {
+        panel.style.display = 'none';
+        button.classList.remove('expanded');
+        button.innerHTML = '&#9654;';  // Right arrow
+    } else {
+        if (!panel.dataset.populated) {
+            populateBytecodePanel(panel, button);
+        }
+        panel.style.display = 'block';
+        button.classList.add('expanded');
+        button.innerHTML = '&#9660;';  // Down arrow
+    }
+}
+
+/**
+ * Populate bytecode panel with instruction data
+ * @param {HTMLElement} panel - The panel element to populate
+ * @param {HTMLElement} button - The button containing the bytecode data
+ */
+function populateBytecodePanel(panel, button) {
+    const bytecodeJson = button.getAttribute('data-bytecode');
+    if (!bytecodeJson) return;
+
+    // Get line number from parent
+    const lineDiv = button.closest('.code-line');
+    const lineNum = lineDiv ? lineDiv.id.replace('line-', '') : null;
+
+    try {
+        const instructions = JSON.parse(bytecodeJson);
+        if (!instructions.length) {
+            panel.innerHTML = '<div class="bytecode-empty">No bytecode data</div>';
+            panel.dataset.populated = 'true';
+            return;
+        }
+
+        const maxSamples = Math.max(...instructions.map(i => i.samples), 1);
+
+        // Calculate specialization stats
+        const totalSamples = instructions.reduce((sum, i) => sum + i.samples, 0);
+        const specializedSamples = instructions
+            .filter(i => i.is_specialized)
+            .reduce((sum, i) => sum + i.samples, 0);
+        const specPct = totalSamples > 0 ? Math.round(100 * specializedSamples / totalSamples) : 0;
+        const specializedCount = instructions.filter(i => i.is_specialized).length;
+
+        // Determine specialization level class
+        let specClass = 'low';
+        if (specPct >= 67) specClass = 'high';
+        else if (specPct >= 33) specClass = 'medium';
+
+        // Build specialization summary
+        let html = `<div class="bytecode-spec-summary ${specClass}">
+            <span class="spec-pct">${specPct}%</span>
+            <span class="spec-label">specialized</span>
+            <span class="spec-detail">(${specializedCount}/${instructions.length} instructions, ${specializedSamples.toLocaleString()}/${totalSamples.toLocaleString()} samples)</span>
+        </div>`;
+
+        html += '<div class="bytecode-header">' +
+            '<span class="bytecode-opname">Instruction</span>' +
+            '<span class="bytecode-samples">Samples</span>' +
+            '<span>Heat</span></div>';
+
+        for (const instr of instructions) {
+            const heatPct = (instr.samples / maxSamples) * 100;
+            const isHot = heatPct > 50;
+            const specializedClass = instr.is_specialized ? ' specialized' : '';
+            const baseOpHtml = instr.is_specialized
+                ? `<span class="base-op">(${escapeHtml(instr.base_opname)})</span>` : '';
+            const badge = instr.is_specialized
+                ? '<span class="specialization-badge">SPECIALIZED</span>' : '';
+
+            // Build location data attributes for cross-referencing with source spans
+            const hasLocations = instr.locations && instr.locations.length > 0;
+            const locationData = hasLocations
+                ? `data-locations='${JSON.stringify(instr.locations)}' data-line="${lineNum}" data-opcode="${instr.opcode}"`
+                : '';
+
+            html += `<div class="bytecode-instruction" ${locationData}>
+                <span class="bytecode-opname${specializedClass}">${escapeHtml(instr.opname)}${baseOpHtml}${badge}</span>
+                <span class="bytecode-samples${isHot ? ' hot' : ''}">${instr.samples.toLocaleString()}</span>
+                <div class="bytecode-heatbar"><div class="bytecode-heatbar-fill" style="width:${heatPct}%"></div></div>
+            </div>`;
+        }
+
+        panel.innerHTML = html;
+        panel.dataset.populated = 'true';
+
+        // Add hover handlers for bytecode instructions to highlight source spans
+        panel.querySelectorAll('.bytecode-instruction[data-locations]').forEach(instrEl => {
+            instrEl.addEventListener('mouseenter', highlightSourceFromBytecode);
+            instrEl.addEventListener('mouseleave', unhighlightSourceFromBytecode);
+        });
+    } catch (e) {
+        panel.innerHTML = '<div class="bytecode-error">Error loading bytecode</div>';
+        console.error('Error parsing bytecode data:', e);
+    }
+}
+
+/**
+ * Highlight source spans when hovering over bytecode instruction
+ */
+function highlightSourceFromBytecode(e) {
+    const instrEl = e.currentTarget;
+    const lineNum = instrEl.dataset.line;
+    const locationsStr = instrEl.dataset.locations;
+
+    if (!lineNum) return;
+
+    const lineDiv = document.getElementById(`line-${lineNum}`);
+    if (!lineDiv) return;
+
+    // Parse locations and highlight matching spans by column range
+    try {
+        const locations = JSON.parse(locationsStr || '[]');
+        const spans = lineDiv.querySelectorAll('.instr-span');
+        spans.forEach(span => {
+            const spanStart = parseInt(span.dataset.colStart);
+            const spanEnd = parseInt(span.dataset.colEnd);
+            for (const loc of locations) {
+                // Match if span's range matches instruction's location
+                if (spanStart === loc.col_offset && spanEnd === loc.end_col_offset) {
+                    span.classList.add('highlight-from-bytecode');
+                    break;
+                }
+            }
+        });
+    } catch (err) {
+        console.error('Error parsing locations:', err);
+    }
+
+    // Also highlight the instruction row itself
+    instrEl.classList.add('highlight');
+}
+
+/**
+ * Remove highlighting from source spans
+ */
+function unhighlightSourceFromBytecode(e) {
+    const instrEl = e.currentTarget;
+    const lineNum = instrEl.dataset.line;
+
+    if (!lineNum) return;
+
+    const lineDiv = document.getElementById(`line-${lineNum}`);
+    if (!lineDiv) return;
+
+    const spans = lineDiv.querySelectorAll('.instr-span.highlight-from-bytecode');
+    spans.forEach(span => {
+        span.classList.remove('highlight-from-bytecode');
+    });
+
+    instrEl.classList.remove('highlight');
+}
+
+/**
+ * Escape HTML special characters
+ * @param {string} text - Text to escape
+ * @returns {string} Escaped HTML
+ */
+function escapeHtml(text) {
+    const div = document.createElement('div');
+    div.textContent = text;
+    return div.innerHTML;
+}
+
+/**
+ * Toggle all bytecode panels at once
+ */
+function toggleAllBytecode() {
+    const buttons = document.querySelectorAll('.bytecode-toggle');
+    if (buttons.length === 0) return;
+
+    const someExpanded = Array.from(buttons).some(b => b.classList.contains('expanded'));
+    const expandAllBtn = document.getElementById('toggle-all-bytecode');
+
+    buttons.forEach(button => {
+        const isExpanded = button.classList.contains('expanded');
+        if (someExpanded ? isExpanded : !isExpanded) {
+            toggleBytecode(button);
+        }
+    });
+
+    // Update the expand-all button state
+    if (expandAllBtn) {
+        expandAllBtn.classList.toggle('expanded', !someExpanded);
+    }
+}
+
+// Keyboard shortcut: 'b' toggles all bytecode panels
+document.addEventListener('keydown', function(e) {
+    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
+        return;
+    }
+    if (e.key === 'b' && !e.ctrlKey && !e.altKey && !e.metaKey) {
+        toggleAllBytecode();
+    }
+});
+
 // Handle hash changes
 window.addEventListener('hashchange', () => setTimeout(scrollToTargetLine, 50));
 
index 55f70f90c4211cbdc47a684d58c9b09ca33ec855..9e57643d59ff9a2f9a085dd10415ebbce404bbbe 100644 (file)
@@ -22,6 +22,7 @@
                     class="toolbar-btn theme-toggle"
                     onclick="toggleTheme()"
                     title="Toggle theme"
+                    aria-label="Toggle theme"
                     id="theme-btn"
                 >&#9790;</button>
             </div>
                 <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>
index 5da44249e2e5f913238ee6aec9dc9aad7165f533..4117cbb8f2fd50be2363909566a1bfba5a096b00 100644 (file)
   --topbar-height: 56px;
   --statusbar-height: 32px;
 
+  /* Border radius */
+  --radius-sm: 4px;
+  --radius-md: 8px;
+  --radius-lg: 12px;
+
   /* Transitions */
   --transition-fast: 0.15s ease;
   --transition-normal: 0.25s ease;
   --nav-caller-hover: #1d4ed8;
   --nav-callee: #dc2626;
   --nav-callee-hover: #b91c1c;
+
+  /* Specialization status colors */
+  --spec-high: #4caf50;
+  --spec-high-text: #2e7d32;
+  --spec-high-bg: rgba(76, 175, 80, 0.15);
+  --spec-medium: #ff9800;
+  --spec-medium-text: #e65100;
+  --spec-medium-bg: rgba(255, 152, 0, 0.15);
+  --spec-low: #9e9e9e;
+  --spec-low-text: #616161;
+  --spec-low-bg: rgba(158, 158, 158, 0.15);
+
+  /* Heatmap span highlighting colors */
+  --span-hot-base: 255, 100, 50;
+  --span-cold-base: 150, 150, 150;
 }
 
 /* Dark theme */
 
   --header-gradient: linear-gradient(135deg, #21262d 0%, #30363d 100%);
 
-  /* Dark mode heat palette - dark blue to teal to yellow to orange (cold to hot) */
-  --heat-1: #4a7ba7;
-  --heat-2: #5a9fa8;
-  --heat-3: #6ab5b5;
-  --heat-4: #7ec488;
-  --heat-5: #a0d878;
-  --heat-6: #c4de6a;
-  --heat-7: #f4d44d;
-  --heat-8: #ff6b35;
+  /* Dark mode heat palette - muted colors that provide sufficient contrast with light text */
+  --heat-1: rgba(74, 123, 167, 0.35);
+  --heat-2: rgba(90, 159, 168, 0.38);
+  --heat-3: rgba(106, 181, 181, 0.40);
+  --heat-4: rgba(126, 196, 136, 0.42);
+  --heat-5: rgba(160, 216, 120, 0.45);
+  --heat-6: rgba(196, 222, 106, 0.48);
+  --heat-7: rgba(244, 212, 77, 0.50);
+  --heat-8: rgba(255, 107, 53, 0.55);
 
   /* Code view specific - dark mode */
   --code-bg: #0d1117;
   --nav-caller-hover: #4184e4;
   --nav-callee: #f87171;
   --nav-callee-hover: #e53e3e;
+
+  /* Specialization status colors - dark theme */
+  --spec-high: #81c784;
+  --spec-high-text: #81c784;
+  --spec-high-bg: rgba(129, 199, 132, 0.2);
+  --spec-medium: #ffb74d;
+  --spec-medium-text: #ffb74d;
+  --spec-medium-bg: rgba(255, 183, 77, 0.2);
+  --spec-low: #bdbdbd;
+  --spec-low-text: #9e9e9e;
+  --spec-low-bg: rgba(189, 189, 189, 0.15);
+
+  /* Heatmap span highlighting colors - dark theme */
+  --span-hot-base: 255, 107, 53;
+  --span-cold-base: 189, 189, 189;
 }
 
 /* --------------------------------------------------------------------------
index 0a082c0c6386ee4a7729db19b1b2530e2125ae03..22bfce8c2ead9928e3defa51b898991b16cd4f44 100644 (file)
@@ -195,6 +195,12 @@ def _add_sampling_options(parser):
         dest="gc",
         help='Don\'t include artificial "<GC>" frames to denote active garbage collection',
     )
+    sampling_group.add_argument(
+        "--opcodes",
+        action="store_true",
+        help="Gather bytecode opcode information for instruction-level profiling "
+        "(shows which bytecode instructions are executing, including specializations).",
+    )
     sampling_group.add_argument(
         "--async-aware",
         action="store_true",
@@ -316,13 +322,15 @@ def _sort_to_mode(sort_choice):
     return sort_map.get(sort_choice, SORT_MODE_NSAMPLES)
 
 
-def _create_collector(format_type, interval, skip_idle):
+def _create_collector(format_type, interval, skip_idle, opcodes=False):
     """Create the appropriate collector based on format type.
 
     Args:
-        format_type: The output format ('pstats', 'collapsed', 'flamegraph', 'gecko')
+        format_type: The output format ('pstats', 'collapsed', 'flamegraph', 'gecko', 'heatmap')
         interval: Sampling interval in microseconds
         skip_idle: Whether to skip idle samples
+        opcodes: Whether to collect opcode information (only used by gecko format
+                 for creating interval markers in Firefox Profiler)
 
     Returns:
         A collector instance of the appropriate type
@@ -332,8 +340,10 @@ def _create_collector(format_type, interval, skip_idle):
         raise ValueError(f"Unknown format: {format_type}")
 
     # Gecko format never skips idle (it needs both GIL and CPU data)
+    # and is the only format that uses opcodes for interval markers
     if format_type == "gecko":
         skip_idle = False
+        return collector_class(interval, skip_idle=skip_idle, opcodes=opcodes)
 
     return collector_class(interval, skip_idle=skip_idle)
 
@@ -446,6 +456,13 @@ def _validate_args(args, parser):
             "Gecko format automatically includes both GIL-holding and CPU status analysis."
         )
 
+    # Validate --opcodes is only used with compatible formats
+    opcodes_compatible_formats = ("live", "gecko", "flamegraph", "heatmap")
+    if args.opcodes and args.format not in opcodes_compatible_formats:
+        parser.error(
+            f"--opcodes is only compatible with {', '.join('--' + f for f in opcodes_compatible_formats)}."
+        )
+
     # Validate pstats-specific options are only used with pstats format
     if args.format != "pstats":
         issues = []
@@ -593,7 +610,7 @@ def _handle_attach(args):
     )
 
     # Create the appropriate collector
-    collector = _create_collector(args.format, args.interval, skip_idle)
+    collector = _create_collector(args.format, args.interval, skip_idle, args.opcodes)
 
     # Sample the process
     collector = sample(
@@ -606,6 +623,7 @@ def _handle_attach(args):
         async_aware=args.async_mode if args.async_aware else None,
         native=args.native,
         gc=args.gc,
+        opcodes=args.opcodes,
     )
 
     # Handle output
@@ -641,7 +659,7 @@ def _handle_run(args):
     )
 
     # Create the appropriate collector
-    collector = _create_collector(args.format, args.interval, skip_idle)
+    collector = _create_collector(args.format, args.interval, skip_idle, args.opcodes)
 
     # Profile the subprocess
     try:
@@ -655,6 +673,7 @@ def _handle_run(args):
             async_aware=args.async_mode if args.async_aware else None,
             native=args.native,
             gc=args.gc,
+            opcodes=args.opcodes,
         )
 
         # Handle output
@@ -685,6 +704,7 @@ def _handle_live_attach(args, pid):
         limit=20,  # Default limit
         pid=pid,
         mode=mode,
+        opcodes=args.opcodes,
         async_aware=args.async_mode if args.async_aware else None,
     )
 
@@ -699,6 +719,7 @@ def _handle_live_attach(args, pid):
         async_aware=args.async_mode if args.async_aware else None,
         native=args.native,
         gc=args.gc,
+        opcodes=args.opcodes,
     )
 
 
@@ -726,6 +747,7 @@ def _handle_live_run(args):
         limit=20,  # Default limit
         pid=process.pid,
         mode=mode,
+        opcodes=args.opcodes,
         async_aware=args.async_mode if args.async_aware else None,
     )
 
@@ -741,6 +763,7 @@ def _handle_live_run(args):
             async_aware=args.async_mode if args.async_aware else None,
             native=args.native,
             gc=args.gc,
+            opcodes=args.opcodes,
         )
     finally:
         # Clean up the subprocess
index f63ea0afd8ac0afb09dd91d2879cc4a258be80b4..22055cf84007b671a0f2b3bc95a892535513a27e 100644 (file)
@@ -1,5 +1,6 @@
 from abc import ABC, abstractmethod
 from .constants import (
+    DEFAULT_LOCATION,
     THREAD_STATUS_HAS_GIL,
     THREAD_STATUS_ON_CPU,
     THREAD_STATUS_GIL_REQUESTED,
@@ -12,6 +13,34 @@ except ImportError:
     # 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 @@ class Collector(ABC):
                     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
index be2ae60a88f114acf1effb502f9089dc0015c74c..b05f1703c8505f5bc02dfda6d26881f3dc78a4af 100644 (file)
@@ -14,6 +14,10 @@ SORT_MODE_SAMPLE_PCT = 3
 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 (
index 921cd625f04e3f904eee481c2aa8f9692604fc0f..b25ee079dd6ce9ec994ad57a32074b90083ce555 100644 (file)
@@ -7,6 +7,7 @@ import threading
 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 @@ GECKO_CATEGORIES = [
     {"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_GC = 3
 CATEGORY_GIL = 4
 CATEGORY_CPU = 5
 CATEGORY_CODE_TYPE = 6
+CATEGORY_OPCODES = 7
 
 # Subcategory indices
 DEFAULT_SUBCATEGORY = 0
@@ -56,9 +59,10 @@ STACKWALK_DISABLED = 0
 
 
 class GeckoCollector(Collector):
-    def __init__(self, sample_interval_usec, *, skip_idle=False):
+    def __init__(self, sample_interval_usec, *, skip_idle=False, opcodes=False):
         self.sample_interval_usec = sample_interval_usec
         self.skip_idle = skip_idle
+        self.opcodes_enabled = opcodes
         self.start_time = time.time() * 1000  # milliseconds since epoch
 
         # Global string table (shared across all threads)
@@ -91,6 +95,9 @@ class GeckoCollector(Collector):
         # 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 @@ class GeckoCollector(Collector):
                 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 @@ class GeckoCollector(Collector):
             "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 @@ class GeckoCollector(Collector):
         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 @@ class GeckoCollector(Collector):
         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 @@ class GeckoCollector(Collector):
         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 @@ class GeckoCollector(Collector):
                 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 @@ class GeckoCollector(Collector):
             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 @@ class GeckoCollector(Collector):
                 "CPUName": "",
                 "product": "Python",
                 "symbolicated": True,
-                "markerSchema": [],
+                "markerSchema": self._build_marker_schema(),
                 "importedFrom": "Tachyon Sampling Profiler",
                 "extensions": {
                     "id": [],
index f705fbe3a32eb9285cdf340f78b40568700761ad..a860ed870e3e406f65b87d07894fe2ba9c55a7f3 100644 (file)
@@ -15,6 +15,7 @@ from pathlib import Path
 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 @@ class HeatmapCollector(StackTraceCollector):
         self.line_self_samples = collections.Counter()
         self.file_self_samples = collections.defaultdict(collections.Counter)
 
-        # Call graph data structures for navigation
-        self.call_graph = collections.defaultdict(list)
-        self.callers_graph = collections.defaultdict(list)
+        # Call graph data structures for navigation (sets for O(1) deduplication)
+        self.call_graph = collections.defaultdict(set)
+        self.callers_graph = collections.defaultdict(set)
         self.function_definitions = {}
 
         # Edge counting for call path analysis
         self.edge_samples = collections.Counter()
 
+        # Bytecode-level tracking data structures
+        # Track samples per (file, lineno) -> {opcode: {'count': N, 'locations': set()}}
+        # Locations are deduplicated via set to minimize memory usage
+        self.line_opcodes = collections.defaultdict(dict)
+
         # Statistics and metadata
         self._total_samples = 0
         self._path_info = get_python_path_info()
         self.stats = {}
 
+        # Opcode collection flag
+        self.opcodes_enabled = False
+
         # Template loader (loads all templates once)
         self._template_loader = _TemplateLoader()
 
@@ -509,26 +518,37 @@ class HeatmapCollector(StackTraceCollector):
         """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 @@ class HeatmapCollector(StackTraceCollector):
         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 @@ class HeatmapCollector(StackTraceCollector):
             (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 @@ class HeatmapCollector(StackTraceCollector):
             cumulative_display = ""
             tooltip = ""
 
+        # Get bytecode data for this line (if any)
+        bytecode_data = self._get_bytecode_data_for_line(filename, line_num)
+        has_bytecode = len(bytecode_data) > 0 and cumulative_samples > 0
+
+        # Build bytecode toggle button if data is available
+        bytecode_btn_html = ''
+        bytecode_panel_html = ''
+        if has_bytecode:
+            bytecode_json = html.escape(json.dumps(bytecode_data))
+
+            # Calculate specialization percentage
+            total_samples = sum(d['samples'] for d in bytecode_data)
+            specialized_samples = sum(d['samples'] for d in bytecode_data if d['is_specialized'])
+            spec_pct = int(100 * specialized_samples / total_samples) if total_samples > 0 else 0
+
+            bytecode_btn_html = (
+                f'<button class="bytecode-toggle" data-bytecode=\'{bytecode_json}\' '
+                f'data-spec-pct="{spec_pct}" '
+                f'onclick="toggleBytecode(this)" title="Show bytecode">&#9654;</button>'
+            )
+            bytecode_panel_html = f'        <div class="bytecode-panel" id="bytecode-{line_num}" style="display:none;"></div>\n'
+        elif self.opcodes_enabled:
+            # Add invisible spacer to maintain consistent indentation when opcodes are enabled
+            bytecode_btn_html = '<div class="bytecode-spacer"></div>'
+
         # Get navigation buttons
         nav_buttons_html = self._build_navigation_buttons(filename, line_num)
 
-        # Build line HTML with intensity data attributes
-        line_html = html.escape(line_content.rstrip('\n'))
+        # Build line HTML with instruction highlights if available
+        line_html = self._render_source_with_highlights(line_content, line_num,
+                                                         filename, bytecode_data)
         title_attr = f' title="{html.escape(tooltip)}"' if tooltip else ""
 
+        # Specialization color for toggle mode (green gradient based on spec %)
+        spec_color_attr = ''
+        if has_bytecode:
+            spec_color = self._format_specialization_color(spec_pct)
+            spec_color_attr = f'data-spec-color="{spec_color}" '
+
         return (
             f'        <div class="code-line" '
             f'data-self-intensity="{self_intensity:.3f}" '
             f'data-cumulative-intensity="{cumulative_intensity:.3f}" '
+            f'{spec_color_attr}'
             f'id="line-{line_num}"{title_attr}>\n'
             f'            <div class="line-number">{line_num}</div>\n'
             f'            <div class="line-samples-self">{self_display}</div>\n'
             f'            <div class="line-samples-cumulative">{cumulative_display}</div>\n'
+            f'            {bytecode_btn_html}\n'
             f'            <div class="line-content">{line_html}</div>\n'
             f'            {nav_buttons_html}\n'
             f'        </div>\n'
+            f'{bytecode_panel_html}'
         )
 
+    def _render_source_with_highlights(self, line_content: str, line_num: int,
+                                        filename: str, bytecode_data: list) -> str:
+        """Render source line with instruction highlight spans.
+
+        Simple: collect ranges with sample counts, assign each byte position to
+        smallest covering range, then emit spans for contiguous runs with sample data.
+        """
+        import html as html_module
+
+        content = line_content.rstrip('\n')
+        if not content:
+            return ''
+
+        # Collect all (start, end) -> {samples, opcodes} mapping from instructions
+        # Multiple instructions may share the same range, so we sum samples and collect opcodes
+        range_data = {}
+        for instr in bytecode_data:
+            samples = instr.get('samples', 0)
+            opname = instr.get('opname', '')
+            for loc in instr.get('locations', []):
+                if loc.get('end_lineno', line_num) == line_num:
+                    start, end = loc.get('col_offset', -1), loc.get('end_col_offset', -1)
+                    if start >= 0 and end >= 0:
+                        key = (start, end)
+                        if key not in range_data:
+                            range_data[key] = {'samples': 0, 'opcodes': []}
+                        range_data[key]['samples'] += samples
+                        if opname and opname not in range_data[key]['opcodes']:
+                            range_data[key]['opcodes'].append(opname)
+
+        if not range_data:
+            return html_module.escape(content)
+
+        # For each byte position, find the smallest covering range
+        byte_to_range = {}
+        for (start, end) in range_data.keys():
+            for pos in range(start, end):
+                if pos not in byte_to_range:
+                    byte_to_range[pos] = (start, end)
+                else:
+                    # Keep smaller range
+                    old_start, old_end = byte_to_range[pos]
+                    if (end - start) < (old_end - old_start):
+                        byte_to_range[pos] = (start, end)
+
+        # Calculate totals for percentage and intensity
+        total_line_samples = sum(d['samples'] for d in range_data.values())
+        max_range_samples = max(d['samples'] for d in range_data.values()) if range_data else 1
+
+        # Render character by character
+        result = []
+        byte_offset = 0
+        char_idx = 0
+        current_range = None
+        span_chars = []
+
+        def flush_span():
+            nonlocal span_chars, current_range
+            if span_chars:
+                text = html_module.escape(''.join(span_chars))
+                if current_range:
+                    data = range_data.get(current_range, {'samples': 0, 'opcodes': []})
+                    samples = data['samples']
+                    opcodes = ', '.join(data['opcodes'][:3])  # Top 3 opcodes
+                    if len(data['opcodes']) > 3:
+                        opcodes += f" +{len(data['opcodes']) - 3} more"
+                    pct = int(100 * samples / total_line_samples) if total_line_samples > 0 else 0
+                    result.append(f'<span class="instr-span" '
+                                  f'data-col-start="{current_range[0]}" '
+                                  f'data-col-end="{current_range[1]}" '
+                                  f'data-samples="{samples}" '
+                                  f'data-max-samples="{max_range_samples}" '
+                                  f'data-pct="{pct}" '
+                                  f'data-opcodes="{html_module.escape(opcodes)}">{text}</span>')
+                else:
+                    result.append(text)
+                span_chars = []
+
+        while char_idx < len(content):
+            char = content[char_idx]
+            char_bytes = len(char.encode('utf-8'))
+            char_range = byte_to_range.get(byte_offset)
+
+            if char_range != current_range:
+                flush_span()
+                current_range = char_range
+
+            span_chars.append(char)
+            byte_offset += char_bytes
+            char_idx += 1
+
+        flush_span()
+        return ''.join(result)
+
+    def _format_specialization_color(self, spec_pct: int) -> str:
+        """Format specialization color based on percentage.
+
+        Uses a gradient from gray (0%) through orange (50%) to green (100%).
+        """
+        # Normalize to 0-1
+        ratio = spec_pct / 100.0
+
+        if ratio >= 0.5:
+            # Orange to green (50-100%)
+            t = (ratio - 0.5) * 2  # 0 to 1
+            r = int(255 * (1 - t))  # 255 -> 0
+            g = int(180 + 75 * t)   # 180 -> 255
+            b = int(50 * (1 - t))   # 50 -> 0
+        else:
+            # Gray to orange (0-50%)
+            t = ratio * 2  # 0 to 1
+            r = int(158 + 97 * t)   # 158 -> 255
+            g = int(158 + 22 * t)   # 158 -> 180
+            b = int(158 - 108 * t)  # 158 -> 50
+
+        alpha = 0.15 + 0.25 * ratio  # 0.15 to 0.4
+        return f"rgba({r}, {g}, {b}, {alpha})"
+
     def _build_navigation_buttons(self, filename: str, line_num: int) -> str:
         """Build navigation buttons for callers/callees."""
         line_key = (filename, line_num)
-        caller_list = self._deduplicate_by_function(self.callers_graph.get(line_key, []))
-        callee_list = self._deduplicate_by_function(self.call_graph.get(line_key, []))
+        caller_list = self._deduplicate_by_function(self.callers_graph.get(line_key, set()))
+        callee_list = self._deduplicate_by_function(self.call_graph.get(line_key, set()))
 
         # Get edge counts for each caller/callee
         callers_with_counts = self._get_edge_counts(line_key, caller_list, is_caller=True)
@@ -907,8 +1151,12 @@ class HeatmapCollector(StackTraceCollector):
         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:
index 5edb02e6e887045ef90fc638909fdfdfefbb3316..3d25b5969835c00b32ab834cfbaa06b1e107d757 100644 (file)
@@ -11,7 +11,7 @@ import sysconfig
 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 @@ from .constants import (
     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 @@ class LiveStatsCollector(Collector):
         pid=None,
         display=None,
         mode=None,
+        opcodes=False,
         async_aware=None,
     ):
         """
@@ -116,6 +122,7 @@ class LiveStatsCollector(Collector):
             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 @@ class LiveStatsCollector(Collector):
         }
         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 @@ class LiveStatsCollector(Collector):
         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 @@ class LiveStatsCollector(Collector):
         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 @@ class LiveStatsCollector(Collector):
             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 @@ class LiveStatsCollector(Collector):
                 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 @@ class LiveStatsCollector(Collector):
         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 @@ class LiveStatsCollector(Collector):
             if self._trend_tracker is not None:
                 self._trend_tracker.toggle()
 
-        elif ch == curses.KEY_LEFT or ch == curses.KEY_UP:
-            # Navigate to previous thread in PER_THREAD mode, or switch from ALL to PER_THREAD
-            if len(self.thread_ids) > 0:
-                if self.view_mode == "ALL":
-                    self.view_mode = "PER_THREAD"
-                    self.current_thread_index = 0
-                else:
-                    self.current_thread_index = (
-                        self.current_thread_index - 1
-                    ) % len(self.thread_ids)
-
-        elif ch == curses.KEY_RIGHT or ch == curses.KEY_DOWN:
-            # Navigate to next thread in PER_THREAD mode, or switch from ALL to PER_THREAD
-            if len(self.thread_ids) > 0:
-                if self.view_mode == "ALL":
-                    self.view_mode = "PER_THREAD"
-                    self.current_thread_index = 0
-                else:
-                    self.current_thread_index = (
-                        self.current_thread_index + 1
-                    ) % len(self.thread_ids)
+        elif ch == ord("j") or ch == ord("J"):
+            # Move selection down in opcode mode (with scrolling)
+            self._move_selection_down()
+
+        elif ch == ord("k") or ch == ord("K"):
+            # Move selection up in opcode mode (with scrolling)
+            self._move_selection_up()
+
+        elif ch == curses.KEY_UP:
+            # Move selection up (same as 'k') when in opcode mode
+            if self.show_opcodes:
+                self._move_selection_up()
+            else:
+                # Navigate to previous thread (same as KEY_LEFT)
+                self._navigate_to_previous_thread()
+
+        elif ch == curses.KEY_DOWN:
+            # Move selection down (same as 'j') when in opcode mode
+            if self.show_opcodes:
+                self._move_selection_down()
+            else:
+                # Navigate to next thread (same as KEY_RIGHT)
+                self._navigate_to_next_thread()
+
+        elif ch == curses.KEY_LEFT:
+            # Navigate to previous thread
+            self._navigate_to_previous_thread()
+
+        elif ch == curses.KEY_RIGHT:
+            # Navigate to next thread
+            self._navigate_to_next_thread()
 
         # Update display if input was processed while finished
         self._handle_finished_input_update(ch != -1)
index e4690c90bafb7f0fd9a7280a7c3021c318f3eb3d..8462c0de3fd680b8b0d01fe4535cccc05d1773ea 100644 (file)
@@ -45,6 +45,9 @@ MIN_SAMPLE_RATE_FOR_SCALING = 100
 # 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
index 2af8caa2c2f6d9d3abd184b24594122307fda35f..869405671ffeed54d5f168d7213e75aa7e6e298a 100644 (file)
@@ -20,6 +20,7 @@ from .constants import (
     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 @@ class TableWidget(Widget):
         # 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 @@ class TableWidget(Widget):
                 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 @@ class TableWidget(Widget):
             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 @@ class TableWidget(Widget):
                 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 @@ class TableWidget(Widget):
                     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 @@ class HelpWidget(Widget):
             ("  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 @@ class HelpWidget(Widget):
                 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
diff --git a/Lib/profiling/sampling/opcode_utils.py b/Lib/profiling/sampling/opcode_utils.py
new file mode 100644 (file)
index 0000000..71b3538
--- /dev/null
@@ -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}
index 4fe3acfa9ff80e6357adf54423b2587505a780bb..8d787c62bb0677310037237ecbd46dd7507e5ef7 100644 (file)
@@ -2,7 +2,7 @@ import collections
 import marshal
 
 from _colorize import ANSIColors
-from .collector import Collector
+from .collector import Collector, extract_lineno
 
 
 class PstatsCollector(Collector):
@@ -23,12 +23,15 @@ class PstatsCollector(Collector):
             return
 
         # Process each frame in the stack to track cumulative calls
+        # frame.location is int, tuple (lineno, end_lineno, col_offset, end_col_offset), or None
         for frame in frames:
-            location = (frame.filename, frame.lineno, frame.funcname)
-            self.result[location]["cumulative_calls"] += 1
+            lineno = extract_lineno(frame.location)
+            loc = (frame.filename, lineno, frame.funcname)
+            self.result[loc]["cumulative_calls"] += 1
 
         # The top frame gets counted as an inline call (directly executing)
-        top_location = (frames[0].filename, frames[0].lineno, frames[0].funcname)
+        top_lineno = extract_lineno(frames[0].location)
+        top_location = (frames[0].filename, top_lineno, frames[0].funcname)
         self.result[top_location]["direct_calls"] += 1
 
         # Track caller-callee relationships for call graph
@@ -36,8 +39,10 @@ class PstatsCollector(Collector):
             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
 
index dd4ea1edbf668d23f03669b6899d7fe219d11c7e..d5b8e21134ca18741f4f10e099c9809ed80ec752 100644 (file)
@@ -27,7 +27,7 @@ _FREE_THREADED_BUILD = sysconfig.get_config_var("Py_GIL_DISABLED") is not None
 
 
 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 @@ class SampleProfiler:
         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,
     )
index aa9cdf2468f683d77fca79fc09da387da36d7c41..e5b86719f00b01407f1dab4f729be026265e99f6 100644 (file)
@@ -7,7 +7,8 @@ import linecache
 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 @@ class CollapsedStackCollector(StackTraceCollector):
         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 @@ class FlamegraphCollector(StackTraceCollector):
                     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 @@ class FlamegraphCollector(StackTraceCollector):
                 **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 @@ class FlamegraphCollector(StackTraceCollector):
             }
             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 @@ class FlamegraphCollector(StackTraceCollector):
                 "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):
index a97242483a89421bd39623a9a4c8ca90a34e21c1..365beec49497a8775a28a6a8e5cbf7f7a8d349ef 100644 (file)
@@ -379,6 +379,31 @@ class RemoteInspectionTestBase(unittest.TestCase):
             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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                             "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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                             },
                         )
 
-                        # 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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                             {
                                 "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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                                     (
                                         "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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                                     (
                                         "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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                     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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                         },
                     )
 
-                    # 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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                         {
                             "Task-1": [],
                             "Task-2": [
-                                ("Task-1", (tuple([script_name, 21, "main"]),))
+                                ("Task-1", ((script_name, 21, "main", None),))
                             ],
                         },
                     )
@@ -1023,8 +992,10 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                         },
                     )
 
-                    # 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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                         {
                             "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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                                 (
                                     "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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                     # 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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                                                         base_events.__file__,
                                                         ANY,
                                                         "Server.serve_forever",
+                                                        ANY,
                                                     ]
                                                 )
                                             ],
@@ -1252,6 +1206,7 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                                                         taskgroups.__file__,
                                                         ANY,
                                                         "TaskGroup._aexit",
+                                                        ANY,
                                                     ]
                                                 ),
                                                 FrameInfo(
@@ -1259,10 +1214,11 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                                                         taskgroups.__file__,
                                                         ANY,
                                                         "TaskGroup.__aexit__",
+                                                        ANY,
                                                     ]
                                                 ),
                                                 FrameInfo(
-                                                    [script_name, ANY, "main"]
+                                                    [script_name, ANY, "main", ANY]
                                                 ),
                                             ],
                                             ANY,
@@ -1287,13 +1243,15 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                                                         tasks.__file__,
                                                         ANY,
                                                         "sleep",
+                                                        ANY,
                                                     ]
                                                 ),
                                                 FrameInfo(
                                                     [
                                                         script_name,
-                                                        36,
+                                                        ANY,
                                                         "echo_client",
+                                                        ANY,
                                                     ]
                                                 ),
                                             ],
@@ -1310,6 +1268,7 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                                                         taskgroups.__file__,
                                                         ANY,
                                                         "TaskGroup._aexit",
+                                                        ANY,
                                                     ]
                                                 ),
                                                 FrameInfo(
@@ -1317,13 +1276,15 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                                                         taskgroups.__file__,
                                                         ANY,
                                                         "TaskGroup.__aexit__",
+                                                        ANY,
                                                     ]
                                                 ),
                                                 FrameInfo(
                                                     [
                                                         script_name,
-                                                        39,
+                                                        ANY,
                                                         "echo_client_spam",
+                                                        ANY,
                                                     ]
                                                 ),
                                             ],
@@ -1336,36 +1297,24 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                         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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                 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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
                             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 @@ class TestGetStackTrace(RemoteInspectionTestBase):
             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 @@ sock.connect(('localhost', {port}))
 
         # 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 @@ sock.connect(('localhost', {port}))
         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(
index 24bf3d21c2fa0403cca960b095cca9cf20333f72..b1bfdf868b085aca085362f162e472225a7751a6 100644 (file)
@@ -4,8 +4,12 @@ import os
 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 @@ class TestHeatmapCollectorProcessFrames(unittest.TestCase):
         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 @@ class TestHeatmapCollectorProcessFrames(unittest.TestCase):
         """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 @@ class TestHeatmapCollectorProcessFrames(unittest.TestCase):
         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 @@ class TestHeatmapCollectorProcessFrames(unittest.TestCase):
         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 @@ class TestHeatmapCollectorProcessFrames(unittest.TestCase):
         """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 @@ class TestHeatmapCollectorProcessFrames(unittest.TestCase):
 
         # 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 @@ class TestHeatmapCollectorProcessFrames(unittest.TestCase):
         # 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 @@ class TestHeatmapCollectorProcessFrames(unittest.TestCase):
         """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 @@ class TestHeatmapCollectorProcessFrames(unittest.TestCase):
         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 @@ class TestHeatmapCollectorProcessFrames(unittest.TestCase):
         """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 @@ class TestHeatmapCollectorExport(unittest.TestCase):
         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 @@ class TestHeatmapCollectorExport(unittest.TestCase):
         """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 @@ class TestHeatmapCollectorExport(unittest.TestCase):
         """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 @@ class TestHeatmapCollectorExport(unittest.TestCase):
         """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 @@ class TestHeatmapCollectorExport(unittest.TestCase):
         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 @@ class TestHeatmapCollectorExport(unittest.TestCase):
         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 @@ class TestHeatmapCollectorExport(unittest.TestCase):
         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 @@ class TestHeatmapCollectorExport(unittest.TestCase):
 
 
 class MockFrameInfo:
-    """Mock FrameInfo for testing since the real one isn't accessible."""
+    """Mock FrameInfo for testing.
+
+    Frame format: (filename, location, funcname, opcode) where:
+    - location is a tuple (lineno, end_lineno, col_offset, end_col_offset)
+    - opcode is an int or None
+    """
 
-    def __init__(self, filename, lineno, funcname):
+    def __init__(self, filename, lineno, funcname, opcode=None):
         self.filename = filename
-        self.lineno = lineno
         self.funcname = funcname
+        self.opcode = opcode
+        self.location = (lineno, lineno, -1, -1)
+
+    def __iter__(self):
+        return iter((self.filename, self.location, self.funcname, self.opcode))
+
+    def __getitem__(self, index):
+        return (self.filename, self.location, self.funcname, self.opcode)[index]
+
+    def __len__(self):
+        return 4
 
     def __repr__(self):
-        return f"MockFrameInfo(filename='{self.filename}', lineno={self.lineno}, funcname='{self.funcname}')"
+        return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})"
 
 
 class MockThreadInfo:
     """Mock ThreadInfo for testing since the real one isn't accessible."""
 
-    def __init__(self, thread_id, frame_info):
+    def __init__(self, thread_id, frame_info, status=0):
         self.thread_id = thread_id
         self.frame_info = frame_info
+        self.status = status  # Thread status flags
 
     def __repr__(self):
         return f"MockThreadInfo(thread_id={self.thread_id}, frame_info={self.frame_info})"
@@ -559,13 +579,13 @@ class TestHeatmapCollector(unittest.TestCase):
         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 @@ class TestHeatmapCollector(unittest.TestCase):
 
         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 @@ class TestHeatmapCollector(unittest.TestCase):
         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()
index 4bb6877f16fda24e7548f327fe24e5943307a2cf..2c67289509914038c68e1f5b876e1eafe786dc79 100644 (file)
@@ -1,21 +1,42 @@
 """Common test helpers and mocks for live collector tests."""
 
+from collections import namedtuple
+
 from profiling.sampling.constants import (
     THREAD_STATUS_HAS_GIL,
     THREAD_STATUS_ON_CPU,
 )
 
 
+# Matches the C structseq LocationInfo from _remote_debugging
+LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset'])
+
+
 class MockFrameInfo:
-    """Mock FrameInfo for testing."""
+    """Mock FrameInfo for testing.
+
+    Frame format: (filename, location, funcname, opcode) where:
+    - location is a tuple (lineno, end_lineno, col_offset, end_col_offset)
+    - opcode is an int or None
+    """
 
-    def __init__(self, filename, lineno, funcname):
+    def __init__(self, filename, lineno, funcname, opcode=None):
         self.filename = filename
-        self.lineno = lineno
         self.funcname = funcname
+        self.opcode = opcode
+        self.location = LocationInfo(lineno, lineno, -1, -1)
+
+    def __iter__(self):
+        return iter((self.filename, self.location, self.funcname, self.opcode))
+
+    def __getitem__(self, index):
+        return (self.filename, self.location, self.funcname, self.opcode)[index]
+
+    def __len__(self):
+        return 4
 
     def __repr__(self):
-        return f"MockFrameInfo(filename='{self.filename}', lineno={self.lineno}, funcname='{self.funcname}')"
+        return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})"
 
 
 class MockThreadInfo:
index 7083362c7714f176a56a34d413c32d53f987d111..4e0f7a87c6da5460979e61736d8500b694f4d574 100644 (file)
@@ -1,16 +1,36 @@
 """Mock classes for sampling profiler tests."""
 
+from collections import namedtuple
+
+# Matches the C structseq LocationInfo from _remote_debugging
+LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset'])
+
 
 class MockFrameInfo:
-    """Mock FrameInfo for testing since the real one isn't accessible."""
+    """Mock FrameInfo for testing.
+
+    Frame format: (filename, location, funcname, opcode) where:
+    - location is a tuple (lineno, end_lineno, col_offset, end_col_offset)
+    - opcode is an int or None
+    """
 
-    def __init__(self, filename, lineno, funcname):
+    def __init__(self, filename, lineno, funcname, opcode=None):
         self.filename = filename
-        self.lineno = lineno
         self.funcname = funcname
+        self.opcode = opcode
+        self.location = LocationInfo(lineno, lineno, -1, -1)
+
+    def __iter__(self):
+        return iter((self.filename, self.location, self.funcname, self.opcode))
+
+    def __getitem__(self, index):
+        return (self.filename, self.location, self.funcname, self.opcode)[index]
+
+    def __len__(self):
+        return 4
 
     def __repr__(self):
-        return f"MockFrameInfo(filename='{self.filename}', lineno={self.lineno}, funcname='{self.funcname}')"
+        return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})"
 
 
 class MockThreadInfo:
index e8c12c2221549afb99ce2d7758b5b5a959ad67a0..75c4e79591000bfad05b3f2b678bd325f4b988a3 100644 (file)
@@ -14,9 +14,12 @@ try:
         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 @@ except ImportError:
 
 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
         # 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
         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 @@ class TestSampleProfilerComponents(unittest.TestCase):
         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 @@ class TestSampleProfilerComponents(unittest.TestCase):
         # 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
         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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                 [
                     MockThreadInfo(
                         1,
-                        [("file.py", 10, "func1"), ("file.py", 20, "func2")],
+                        [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")],
                     )
                 ],
             )
@@ -608,7 +610,7 @@ class TestSampleProfilerComponents(unittest.TestCase):
                 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                     [
                         MockThreadInfo(
                             1,
-                            [("test.py", 10, "python_func")],
+                            [MockFrameInfo("test.py", 10, "python_func")],
                             status=HAS_GIL_ON_CPU,
                         )
                     ],
@@ -698,7 +700,7 @@ class TestSampleProfilerComponents(unittest.TestCase):
                     [
                         MockThreadInfo(
                             1,
-                            [("test.py", 15, "wait_func")],
+                            [MockFrameInfo("test.py", 15, "wait_func")],
                             status=WAITING_FOR_GIL,
                         )
                     ],
@@ -713,7 +715,7 @@ class TestSampleProfilerComponents(unittest.TestCase):
                     [
                         MockThreadInfo(
                             1,
-                            [("test.py", 20, "python_func2")],
+                            [MockFrameInfo("test.py", 20, "python_func2")],
                             status=HAS_GIL_ON_CPU,
                         )
                     ],
@@ -728,7 +730,7 @@ class TestSampleProfilerComponents(unittest.TestCase):
                     [
                         MockThreadInfo(
                             1,
-                            [("native.c", 100, "native_func")],
+                            [MockFrameInfo("native.c", 100, "native_func")],
                             status=NO_GIL_ON_CPU,
                         )
                     ],
@@ -902,8 +904,8 @@ class TestSampleProfilerComponents(unittest.TestCase):
             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 @@ class TestSampleProfilerComponents(unittest.TestCase):
             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 @@ class TestSampleProfilerComponents(unittest.TestCase):
             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 @@ class TestSampleProfilerComponents(unittest.TestCase):
             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 @@ class TestSampleProfilerComponents(unittest.TestCase):
             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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
             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 @@ class TestSampleProfilerComponents(unittest.TestCase):
             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 @@ class TestSampleProfilerComponents(unittest.TestCase):
         # 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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                     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 @@ class TestSampleProfilerComponents(unittest.TestCase):
                     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 @@ class TestSampleProfilerComponents(unittest.TestCase):
         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)
index e92b3f45fbc379dfd2b373b47be967cb4fca951a..029952da6977517905bbb30c012bbaeca4991c8c 100644 (file)
@@ -304,10 +304,10 @@ class TestRecursiveFunctionProfiling(unittest.TestCase):
                     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 @@ class TestRecursiveFunctionProfiling(unittest.TestCase):
                     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"),
                         ],
                     )
                 ],
diff --git a/Misc/NEWS.d/next/Library/2025-12-07-23-21-13.gh-issue-138122.m3EF9E.rst b/Misc/NEWS.d/next/Library/2025-12-07-23-21-13.gh-issue-138122.m3EF9E.rst
new file mode 100644 (file)
index 0000000..5cc54e6
--- /dev/null
@@ -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
index 7f3c0d363f56c6a8a638a377a90dbeacb98663c9..0aa98349296b8ab10f0bf5a3916ceb0911e5856d 100644 (file)
@@ -190,6 +190,7 @@ typedef struct {
 typedef struct {
     PyTypeObject *RemoteDebugging_Type;
     PyTypeObject *TaskInfo_Type;
+    PyTypeObject *LocationInfo_Type;
     PyTypeObject *FrameInfo_Type;
     PyTypeObject *CoroInfo_Type;
     PyTypeObject *ThreadInfo_Type;
@@ -228,6 +229,7 @@ typedef struct {
     int skip_non_matching_threads;
     int native;
     int gc;
+    int opcodes;
     int cache_frames;
     int collect_stats;  // whether to collect statistics
     uint32_t stale_invalidation_counter;  // counter for throttling frame_cache_invalidate_stale
@@ -286,6 +288,7 @@ typedef int (*set_entry_processor_func)(
  * ============================================================================ */
 
 extern PyStructSequence_Desc TaskInfo_desc;
+extern PyStructSequence_Desc LocationInfo_desc;
 extern PyStructSequence_Desc FrameInfo_desc;
 extern PyStructSequence_Desc CoroInfo_desc;
 extern PyStructSequence_Desc ThreadInfo_desc;
@@ -336,11 +339,20 @@ extern int parse_code_object(
     int32_t tlbc_index
 );
 
+extern PyObject *make_location_info(
+    RemoteUnwinderObject *unwinder,
+    int lineno,
+    int end_lineno,
+    int col_offset,
+    int end_col_offset
+);
+
 extern PyObject *make_frame_info(
     RemoteUnwinderObject *unwinder,
     PyObject *file,
-    PyObject *line,
-    PyObject *func
+    PyObject *location,  // LocationInfo structseq or None for synthetic frames
+    PyObject *func,
+    PyObject *opcode
 );
 
 /* Line table parsing */
index 03127b753cc813dc7a88b283b2be265a13f5dbf3..353929c4643dbdc79cc3b649e1c32a13d1927f49 100644 (file)
@@ -12,7 +12,8 @@ preserve
 PyDoc_STRVAR(_remote_debugging_RemoteUnwinder___init____doc__,
 "RemoteUnwinder(pid, *, all_threads=False, only_active_thread=False,\n"
 "               mode=0, debug=False, skip_non_matching_threads=True,\n"
-"               native=False, gc=False, cache_frames=False, stats=False)\n"
+"               native=False, gc=False, opcodes=False,\n"
+"               cache_frames=False, stats=False)\n"
 "--\n"
 "\n"
 "Initialize a new RemoteUnwinder object for debugging a remote Python process.\n"
@@ -32,6 +33,8 @@ PyDoc_STRVAR(_remote_debugging_RemoteUnwinder___init____doc__,
 "            non-Python code.\n"
 "    gc: If True, include artificial \"<GC>\" frames to denote active garbage\n"
 "        collection.\n"
+"    opcodes: If True, gather bytecode opcode information for instruction-level\n"
+"             profiling.\n"
 "    cache_frames: If True, enable frame caching optimization to avoid re-reading\n"
 "                 unchanged parent frames between samples.\n"
 "    stats: If True, collect statistics about cache hits, memory reads, etc.\n"
@@ -53,7 +56,8 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
                                                int mode, int debug,
                                                int skip_non_matching_threads,
                                                int native, int gc,
-                                               int cache_frames, int stats);
+                                               int opcodes, int cache_frames,
+                                               int stats);
 
 static int
 _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObject *kwargs)
@@ -61,7 +65,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
     int return_value = -1;
     #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
 
-    #define NUM_KEYWORDS 10
+    #define NUM_KEYWORDS 11
     static struct {
         PyGC_Head _this_is_not_used;
         PyObject_VAR_HEAD
@@ -70,7 +74,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
     } _kwtuple = {
         .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
         .ob_hash = -1,
-        .ob_item = { &_Py_ID(pid), &_Py_ID(all_threads), &_Py_ID(only_active_thread), &_Py_ID(mode), &_Py_ID(debug), &_Py_ID(skip_non_matching_threads), &_Py_ID(native), &_Py_ID(gc), &_Py_ID(cache_frames), &_Py_ID(stats), },
+        .ob_item = { &_Py_ID(pid), &_Py_ID(all_threads), &_Py_ID(only_active_thread), &_Py_ID(mode), &_Py_ID(debug), &_Py_ID(skip_non_matching_threads), &_Py_ID(native), &_Py_ID(gc), &_Py_ID(opcodes), &_Py_ID(cache_frames), &_Py_ID(stats), },
     };
     #undef NUM_KEYWORDS
     #define KWTUPLE (&_kwtuple.ob_base.ob_base)
@@ -79,14 +83,14 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
     #  define KWTUPLE NULL
     #endif  // !Py_BUILD_CORE
 
-    static const char * const _keywords[] = {"pid", "all_threads", "only_active_thread", "mode", "debug", "skip_non_matching_threads", "native", "gc", "cache_frames", "stats", NULL};
+    static const char * const _keywords[] = {"pid", "all_threads", "only_active_thread", "mode", "debug", "skip_non_matching_threads", "native", "gc", "opcodes", "cache_frames", "stats", NULL};
     static _PyArg_Parser _parser = {
         .keywords = _keywords,
         .fname = "RemoteUnwinder",
         .kwtuple = KWTUPLE,
     };
     #undef KWTUPLE
-    PyObject *argsbuf[10];
+    PyObject *argsbuf[11];
     PyObject * const *fastargs;
     Py_ssize_t nargs = PyTuple_GET_SIZE(args);
     Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1;
@@ -98,6 +102,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
     int skip_non_matching_threads = 1;
     int native = 0;
     int gc = 0;
+    int opcodes = 0;
     int cache_frames = 0;
     int stats = 0;
 
@@ -177,7 +182,16 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
         }
     }
     if (fastargs[8]) {
-        cache_frames = PyObject_IsTrue(fastargs[8]);
+        opcodes = PyObject_IsTrue(fastargs[8]);
+        if (opcodes < 0) {
+            goto exit;
+        }
+        if (!--noptargs) {
+            goto skip_optional_kwonly;
+        }
+    }
+    if (fastargs[9]) {
+        cache_frames = PyObject_IsTrue(fastargs[9]);
         if (cache_frames < 0) {
             goto exit;
         }
@@ -185,12 +199,12 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
             goto skip_optional_kwonly;
         }
     }
-    stats = PyObject_IsTrue(fastargs[9]);
+    stats = PyObject_IsTrue(fastargs[10]);
     if (stats < 0) {
         goto exit;
     }
 skip_optional_kwonly:
-    return_value = _remote_debugging_RemoteUnwinder___init___impl((RemoteUnwinderObject *)self, pid, all_threads, only_active_thread, mode, debug, skip_non_matching_threads, native, gc, cache_frames, stats);
+    return_value = _remote_debugging_RemoteUnwinder___init___impl((RemoteUnwinderObject *)self, pid, all_threads, only_active_thread, mode, debug, skip_non_matching_threads, native, gc, opcodes, cache_frames, stats);
 
 exit:
     return return_value;
@@ -419,4 +433,4 @@ _remote_debugging_RemoteUnwinder_get_stats(PyObject *self, PyObject *Py_UNUSED(i
 
     return return_value;
 }
-/*[clinic end generated code: output=f1fd6c1d4c4c7254 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=1943fb7a56197e39 input=a9049054013a1b77]*/
index 2cd2505d0f966b966393b4bcedf909743fbfdef3..98fe74e8cb6331e5be207227bae4a0e657abae3c 100644 (file)
@@ -155,48 +155,45 @@ parse_linetable(const uintptr_t addrq, const char* linetable, int firstlineno, L
 {
     const uint8_t* ptr = (const uint8_t*)(linetable);
     uintptr_t addr = 0;
-    info->lineno = firstlineno;
+    int computed_line = firstlineno;  // Running accumulator, separate from output
 
     while (*ptr != '\0') {
-        // See InternalDocs/code_objects.md for where these magic numbers are from
-        // and for the decoding algorithm.
         uint8_t first_byte = *(ptr++);
         uint8_t code = (first_byte >> 3) & 15;
         size_t length = (first_byte & 7) + 1;
         uintptr_t end_addr = addr + length;
+
         switch (code) {
-            case PY_CODE_LOCATION_INFO_NONE: {
+            case PY_CODE_LOCATION_INFO_NONE:
+                info->lineno = info->end_lineno = -1;
+                info->column = info->end_column = -1;
                 break;
-            }
-            case PY_CODE_LOCATION_INFO_LONG: {
-                int line_delta = scan_signed_varint(&ptr);
-                info->lineno += line_delta;
-                info->end_lineno = info->lineno + scan_varint(&ptr);
+            case PY_CODE_LOCATION_INFO_LONG:
+                computed_line += scan_signed_varint(&ptr);
+                info->lineno = computed_line;
+                info->end_lineno = computed_line + scan_varint(&ptr);
                 info->column = scan_varint(&ptr) - 1;
                 info->end_column = scan_varint(&ptr) - 1;
                 break;
-            }
-            case PY_CODE_LOCATION_INFO_NO_COLUMNS: {
-                int line_delta = scan_signed_varint(&ptr);
-                info->lineno += line_delta;
+            case PY_CODE_LOCATION_INFO_NO_COLUMNS:
+                computed_line += scan_signed_varint(&ptr);
+                info->lineno = info->end_lineno = computed_line;
                 info->column = info->end_column = -1;
                 break;
-            }
             case PY_CODE_LOCATION_INFO_ONE_LINE0:
             case PY_CODE_LOCATION_INFO_ONE_LINE1:
-            case PY_CODE_LOCATION_INFO_ONE_LINE2: {
-                int line_delta = code - 10;
-                info->lineno += line_delta;
-                info->end_lineno = info->lineno;
+            case PY_CODE_LOCATION_INFO_ONE_LINE2:
+                computed_line += code - 10;
+                info->lineno = info->end_lineno = computed_line;
                 info->column = *(ptr++);
                 info->end_column = *(ptr++);
                 break;
-            }
             default: {
                 uint8_t second_byte = *(ptr++);
                 if ((second_byte & 128) != 0) {
                     return false;
                 }
+                info->lineno = info->end_lineno = computed_line;
                 info->column = code << 3 | (second_byte >> 4);
                 info->end_column = info->column + (second_byte & 15);
                 break;
@@ -215,8 +212,50 @@ parse_linetable(const uintptr_t addrq, const char* linetable, int firstlineno, L
  * ============================================================================ */
 
 PyObject *
-make_frame_info(RemoteUnwinderObject *unwinder, PyObject *file, PyObject *line,
-                PyObject *func)
+make_location_info(RemoteUnwinderObject *unwinder, int lineno, int end_lineno,
+                   int col_offset, int end_col_offset)
+{
+    RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
+    PyObject *info = PyStructSequence_New(state->LocationInfo_Type);
+    if (info == NULL) {
+        set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create LocationInfo");
+        return NULL;
+    }
+
+    PyObject *py_lineno = PyLong_FromLong(lineno);
+    if (py_lineno == NULL) {
+        Py_DECREF(info);
+        return NULL;
+    }
+    PyStructSequence_SetItem(info, 0, py_lineno);  // steals reference
+
+    PyObject *py_end_lineno = PyLong_FromLong(end_lineno);
+    if (py_end_lineno == NULL) {
+        Py_DECREF(info);
+        return NULL;
+    }
+    PyStructSequence_SetItem(info, 1, py_end_lineno);  // steals reference
+
+    PyObject *py_col_offset = PyLong_FromLong(col_offset);
+    if (py_col_offset == NULL) {
+        Py_DECREF(info);
+        return NULL;
+    }
+    PyStructSequence_SetItem(info, 2, py_col_offset);  // steals reference
+
+    PyObject *py_end_col_offset = PyLong_FromLong(end_col_offset);
+    if (py_end_col_offset == NULL) {
+        Py_DECREF(info);
+        return NULL;
+    }
+    PyStructSequence_SetItem(info, 3, py_end_col_offset);  // steals reference
+
+    return info;
+}
+
+PyObject *
+make_frame_info(RemoteUnwinderObject *unwinder, PyObject *file, PyObject *location,
+                PyObject *func, PyObject *opcode)
 {
     RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
     PyObject *info = PyStructSequence_New(state->FrameInfo_Type);
@@ -225,11 +264,13 @@ make_frame_info(RemoteUnwinderObject *unwinder, PyObject *file, PyObject *line,
         return NULL;
     }
     Py_INCREF(file);
-    Py_INCREF(line);
+    Py_INCREF(location);
     Py_INCREF(func);
+    Py_INCREF(opcode);
     PyStructSequence_SetItem(info, 0, file);
-    PyStructSequence_SetItem(info, 1, line);
+    PyStructSequence_SetItem(info, 1, location);
     PyStructSequence_SetItem(info, 2, func);
+    PyStructSequence_SetItem(info, 3, opcode);
     return info;
 }
 
@@ -370,16 +411,43 @@ done_tlbc:
                               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;
     }
index eaf3287c6fec1215be458186778eeb0283128cda..abde60c45766a56d263339e89634045a8c27cd88 100644 (file)
@@ -337,8 +337,9 @@ process_frame_chain(
             extra_frame = &_Py_STR(native);
         }
         if (extra_frame) {
+            // Use "~" as file, None as location (synthetic frame), None as opcode
             PyObject *extra_frame_info = make_frame_info(
-                unwinder, _Py_LATIN1_CHR('~'), _PyLong_GetZero(), extra_frame);
+                unwinder, _Py_LATIN1_CHR('~'), Py_None, extra_frame, Py_None);
             if (extra_frame_info == NULL) {
                 return -1;
             }
index 123e4f5c4d780c8919ad68ba0d6e972faa9adbeb..9b05b911658190517e1c000184014a86ab36ebcb 100644 (file)
@@ -28,11 +28,28 @@ PyStructSequence_Desc TaskInfo_desc = {
     4
 };
 
+// LocationInfo structseq type
+static PyStructSequence_Field LocationInfo_fields[] = {
+    {"lineno", "Line number"},
+    {"end_lineno", "End line number"},
+    {"col_offset", "Column offset"},
+    {"end_col_offset", "End column offset"},
+    {NULL}
+};
+
+PyStructSequence_Desc LocationInfo_desc = {
+    "_remote_debugging.LocationInfo",
+    "Source location information: (lineno, end_lineno, col_offset, end_col_offset)",
+    LocationInfo_fields,
+    4
+};
+
 // FrameInfo structseq type
 static PyStructSequence_Field FrameInfo_fields[] = {
     {"filename", "Source code filename"},
-    {"lineno", "Line number"},
+    {"location", "LocationInfo structseq or None for synthetic frames"},
     {"funcname", "Function name"},
+    {"opcode", "Opcode being executed (None if not gathered)"},
     {NULL}
 };
 
@@ -40,7 +57,7 @@ PyStructSequence_Desc FrameInfo_desc = {
     "_remote_debugging.FrameInfo",
     "Information about a frame",
     FrameInfo_fields,
-    3
+    4
 };
 
 // CoroInfo structseq type
@@ -235,6 +252,7 @@ _remote_debugging.RemoteUnwinder.__init__
     skip_non_matching_threads: bool = True
     native: bool = False
     gc: bool = False
+    opcodes: bool = False
     cache_frames: bool = False
     stats: bool = False
 
@@ -255,6 +273,8 @@ Args:
             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);