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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions