From 3c20d4928194be23c711e79f50a561c61d6d50a4 Mon Sep 17 00:00:00 2001 From: mattip Date: Tue, 15 Sep 2026 17:07:32 +0300 Subject: [PATCH 1/2] add windows to CI testing and fix race condition --- .github/workflows/tests.yml | 16 ++++++++------- src/vmprof_win.c | 39 ++++++++++++++++++++++++++++++------- vmprof/reader.py | 9 +++++---- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 04022a3..3caf3a8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,16 +20,18 @@ jobs: os: [ubuntu-latest] python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15.0-rc.2", "pypy-3.11"] experimental: [false] - # include: - # - os: macos-latest - # python: "3.10" - # experimental: false - # - os: windows-latest - # python: "3.10" - # experimental: false + include: + # Windows on the earliest and latest supported versions: + - os: windows-latest + python: "3.9" + experimental: false + - os: windows-latest + python: "3.15.0-rc.2" + experimental: false steps: - uses: actions/checkout@v7 - name: Install libunwind + if: runner.os == 'Linux' run: | sudo apt install -y libunwind-dev pkg-config libunwind --cflags --libs-only-l diff --git a/src/vmprof_win.c b/src/vmprof_win.c index 3511449..830d1d3 100644 --- a/src/vmprof_win.c +++ b/src/vmprof_win.c @@ -8,6 +8,8 @@ volatile int thread_started = 0; volatile int enabled = 0; +/* Set by the sampler thread while it takes and writes one sample. */ +static volatile LONG sampler_busy = 0; #ifndef RPYTHON_VMPROF static PY_WIN_THREAD_STATE *target_tstate = NULL; #endif @@ -16,11 +18,25 @@ HANDLE write_mutex; int prepare_concurrent_bufs(void) { - if (!(write_mutex = CreateMutex(NULL, FALSE, NULL))) - return -1; + if (write_mutex == NULL) { + if (!(write_mutex = CreateMutex(NULL, FALSE, NULL))) + return -1; + } return 0; } +/* Wait for the sampler thread to finish the sample it may be in the middle + of. Callers clear `enabled` first so that no new sample starts; without + this wait a stale sample could be written into the next profile, ahead of + its header. */ +static void wait_for_sampler(void) +{ + MemoryBarrier(); + while (sampler_busy) { + SwitchToThread(); + } +} + int vmprof_register_virtual_function(char *code_name, intptr_t code_uid, int auto_retry) { @@ -126,8 +142,9 @@ int vmprof_snapshot_thread(DWORD thread_id, PY_WIN_THREAD_STATE *tstate, prof_st #else frame = PyThreadState_GetFrame(tstate); #endif + /* leave room for the thread id appended below */ depth = vmp_walk_and_record_stack(frame, stack->stack, - MAX_STACK_DEPTH, 0, 0); + MAX_STACK_DEPTH - 1, 0, 0); #ifdef _MSC_VER } __except (EXCEPTION_EXECUTE_HANDLER) { depth = -1; @@ -193,19 +210,25 @@ long __stdcall vmprof_mainloop(void *arg) // cpython version while (1) { Sleep(vmprof_get_profile_interval_usec() * 1000); + sampler_busy = 1; + MemoryBarrier(); if (!enabled) { + sampler_busy = 0; continue; } tstate = get_current_thread_state(); if (!tstate) tstate = target_tstate; - if (!tstate) + if (!tstate) { + sampler_busy = 0; continue; + } depth = vmprof_snapshot_thread(tstate->thread_id, tstate, stack); if (depth > 0) { vmp_write_all((char*)stack + offsetof(prof_stacktrace_s, marker), SIZEOF_PROF_STACKTRACE + depth * sizeof(void*)); } + sampler_busy = 0; } #else // pypy version @@ -255,10 +278,9 @@ int vmprof_enable(int memory, int native, int real_time) RPY_EXTERN int vmprof_disable(void) { - char marker = MARKER_TRAILER; - (void)vmp_write_time_now(MARKER_TRAILER); - enabled = 0; + wait_for_sampler(); + (void)vmp_write_time_now(MARKER_TRAILER); #ifndef RPYTHON_VMPROF target_tstate = NULL; #endif @@ -270,6 +292,9 @@ RPY_EXTERN void vmprof_ignore_signals(int ignored) { enabled = !ignored; + if (ignored) { + wait_for_sampler(); + } } int vmp_native_enable(void) diff --git a/vmprof/reader.py b/vmprof/reader.py index 3d653a6..60c18ce 100644 --- a/vmprof/reader.py +++ b/vmprof/reader.py @@ -114,15 +114,16 @@ def detect_file_sizes(self): little = False self.setup_once(little_endian=little, word_size=4, addr_size=4) else: - firstbytes = self.read(8) - if firstbytes[0] == three: + secondbytes = self.read(8) + if secondbytes[0] == three: little = True self.setup_once(little_endian=little, word_size=8, addr_size=8) - elif firstbytes[7] == three: + elif secondbytes[7] == three: little = False self.setup_once(little_endian=little, word_size=8, addr_size=8) else: - raise NotImplementedError("could not determine word and addr size") + raise NotImplementedError("could not determine word and addr size, " + "file starts with %r" % (firstbytes + secondbytes,)) # determine if it is windows 64 bit # even though it migt be a 64bit log, teh addr_size is now 4 From 6d0312da6fab7b0eba5901d14ac497c190b8bf6a Mon Sep 17 00:00:00 2001 From: mattip Date: Tue, 15 Sep 2026 17:17:56 +0300 Subject: [PATCH 2/2] replace pytz with timezone --- dev_requirements.txt | 1 - pyproject.toml | 1 - test_requirements.txt | 1 - vmprof/reader.py | 2 +- vmprof/test/test_run.py | 1 - vmshare/binary.py | 7 +++++-- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 1d90dda..334f301 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,5 +1,4 @@ six requests backports.shutil_which -pytz colorama diff --git a/pyproject.toml b/pyproject.toml index 899a1a6..83a2211 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ requires-python = ">=3.9,<3.16" dependencies = [ "requests", "six", - "pytz", "colorama", ] classifiers = [ diff --git a/test_requirements.txt b/test_requirements.txt index b9400c3..6ae6714 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -7,6 +7,5 @@ setuptools>=77 pytest hypothesis colorama -pytz attrs py diff --git a/vmprof/reader.py b/vmprof/reader.py index 60c18ce..2abad57 100644 --- a/vmprof/reader.py +++ b/vmprof/reader.py @@ -244,7 +244,7 @@ def read_timeval(self): def read_timezone(self): timezone = self.read(8).strip(b'\x00') - # we should use pytz and parse iso8601 if we really support time zones + # we should use zoneinfo and parse iso8601 if we really support time zones return None def read_all(self): diff --git a/vmprof/test/test_run.py b/vmprof/test/test_run.py index 78a4e50..d55675f 100644 --- a/vmprof/test/test_run.py +++ b/vmprof/test/test_run.py @@ -7,7 +7,6 @@ import time import gzip import time -import pytz import vmprof import six from cffi import FFI diff --git a/vmshare/binary.py b/vmshare/binary.py index 66da6a0..2c3ee27 100644 --- a/vmshare/binary.py +++ b/vmshare/binary.py @@ -1,7 +1,7 @@ import sys import struct import array -import pytz +import zoneinfo WORD_SIZE = struct.calcsize('L') if sys.maxsize == 2**63-1: @@ -82,7 +82,10 @@ def read_timezone(fileobj): timezone = fileobj.read(8).strip(b'\x00') timezone = timezone.decode('ascii') if timezone: - return pytz.timezone(timezone) + try: + return zoneinfo.ZoneInfo(timezone) + except (zoneinfo.ZoneInfoNotFoundError, ValueError): + return None return None def encode_le_u16(value):