Skip to content

Bug with per-thread bytecode and profiling/instrumentation in freethreading #136396

Open
@colesbury

Description

@colesbury

Bug report

Bug description:

A bunch of the instrumentation state is per-code object, such as the active montiors. The modifications also typically happen lazily when a code object is executed after instrumentation is enabled/disabled.

if (monitors_are_empty(new_events) && monitors_are_empty(removed_events)) {
goto done;
}

However, if you create a new thread, then it will be initialized without the instrumented bytecodes. Here's an example that breaks:

  1. Enable instrumentation and call some function. This will replace things like CALL with INSTRUMENTED_CALL.
  2. Disable instrumentation. Note that this doesn't immediately change INSTRUMENTED_CALL back to CALL!
  3. Start a new thread, enable instrumentation, and call that same function - uh oh!

In (3), the new thread gets a clean copy of the bytecode without instrumentation:

cpython/Objects/codeobject.c

Lines 3333 to 3341 in 0240ef4

static void
copy_code(_Py_CODEUNIT *dst, PyCodeObject *co)
{
int code_len = (int) Py_SIZE(co);
for (int i = 0; i < code_len; i += _PyInstruction_GetLength(co, i)) {
dst[i] = _Py_GetBaseCodeUnit(co, i);
}
_PyCode_Quicken(dst, code_len, 1);
}

However, the code object still has instrumentation enabled, so the monitors_are_empty check above returns with instrumenting the bytecode. Missing events!

Adapted from @pablogsal's repro:

import sys
import threading
import dis

def looooooser(x):
    print("I am a looooooser")

def LOSER():
    looooooser(42)

TRACES = []

def tracing_function(frame, event, arg):
    function_name = frame.f_code.co_name
    TRACES.append((function_name, event, arg))

def func1():
    sys.setprofile(tracing_function)
    LOSER()
    sys.setprofile(None)
    TRACES.clear()

def func2():
    def thread_body():
        sys.setprofile(tracing_function)
        LOSER()
        sys.setprofile(None)

    dis.dis(looooooser, adaptive=True)

    # WHEN
    bg_thread = threading.Thread(target=thread_body)
    bg_thread.start()

    bg_thread.join()

    for trace in TRACES:
        print(trace)
    assert ('looooooser', 'call', None) in TRACES

func1()
func2()

cc @mpage

CPython versions tested on:

CPython main branch, 3.14, 3.15

Operating systems tested on:

No response

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy