22from __future__ import annotations
33
44import _thread
5- import abc
65import os .path
76import sys
87import time
@@ -52,6 +51,7 @@ class _ProfiledLock:
5251 "init_location" ,
5352 "acquired_time" ,
5453 "name" ,
54+ "is_internal" ,
5555 )
5656
5757 def __init__ (
@@ -60,6 +60,7 @@ def __init__(
6060 tracer : Optional [Tracer ],
6161 max_nframes : int ,
6262 capture_sampler : collector .CaptureSampler ,
63+ is_internal : bool = False ,
6364 ) -> None :
6465 self .__wrapped__ : Any = wrapped
6566 self .tracer : Optional [Tracer ] = tracer
@@ -71,6 +72,9 @@ def __init__(
7172 self .init_location : str = f"{ os .path .basename (code .co_filename )} :{ frame .f_lineno } "
7273 self .acquired_time : int = 0
7374 self .name : Optional [str ] = None
75+ # If True, this lock is internal to another sync primitive (e.g., Lock inside Semaphore)
76+ # and should not generate profile samples to avoid double-counting
77+ self .is_internal : bool = is_internal
7478
7579 ### DUNDER methods ###
7680
@@ -161,6 +165,11 @@ def _flush_sample(self, start: int, end: int, is_acquire: bool) -> None:
161165 end: End timestamp in nanoseconds
162166 is_acquire: True for acquire operations, False for release operations
163167 """
168+ # Skip profiling for internal locks (e.g., Lock inside Semaphore/Condition)
169+ # to avoid double-counting when multiple collectors are active
170+ if self .is_internal :
171+ return
172+
164173 handle : ddup .SampleHandle = ddup .SampleHandle ()
165174
166175 handle .push_monotonic_ns (end )
@@ -262,6 +271,8 @@ class LockCollector(collector.CaptureSamplerCollector):
262271 """Record lock usage."""
263272
264273 PROFILED_LOCK_CLASS : Type [Any ]
274+ PATCH_MODULE : Any # e.g., threading module
275+ PATCH_ATTR_NAME : str # e.g., "Lock", "RLock", "Semaphore"
265276
266277 def __init__ (
267278 self ,
@@ -275,11 +286,11 @@ def __init__(
275286 self .tracer : Optional [Tracer ] = tracer
276287 self ._original_lock : Any = None
277288
278- @ abc . abstractmethod
279- def _get_patch_target ( self ) -> Callable [..., Any ]: ...
289+ def _get_patch_target ( self ) -> Callable [..., Any ]:
290+ return getattr ( self . PATCH_MODULE , self . PATCH_ATTR_NAME )
280291
281- @ abc . abstractmethod
282- def _set_patch_target (self , value : Any ) -> None : ...
292+ def _set_patch_target ( self , value : Any ) -> None :
293+ setattr (self . PATCH_MODULE , self . PATCH_ATTR_NAME , value )
283294
284295 def _start_service (self ) -> None :
285296 """Start collecting lock usage."""
@@ -297,12 +308,30 @@ def patch(self) -> None:
297308 original_lock : Any = self ._original_lock # Capture non-None value
298309
299310 def _profiled_allocate_lock (* args : Any , ** kwargs : Any ) -> _ProfiledLock :
300- """Simple wrapper that returns profiled locks."""
311+ """Simple wrapper that returns profiled locks.
312+
313+ Detects if the lock is being created from within threading.py stdlib
314+ (i.e., internal to Semaphore/Condition) to avoid double-counting.
315+ """
316+ import threading as threading_module
317+
318+ # Check if caller is from threading.py (internal lock)
319+ is_internal : bool = False
320+ try :
321+ # Frame 0: _profiled_allocate_lock
322+ # Frame 1: _LockAllocatorWrapper.__call__
323+ # Frame 2: actual caller (threading.Lock() call site)
324+ caller_filename = sys ._getframe (2 ).f_code .co_filename
325+ is_internal = bool (threading_module .__file__ ) and caller_filename == threading_module .__file__
326+ except (ValueError , AttributeError ):
327+ pass
328+
301329 return self .PROFILED_LOCK_CLASS (
302330 wrapped = original_lock (* args , ** kwargs ),
303331 tracer = self .tracer ,
304332 max_nframes = self .nframes ,
305333 capture_sampler = self ._capture_sampler ,
334+ is_internal = is_internal ,
306335 )
307336
308337 self ._set_patch_target (_LockAllocatorWrapper (_profiled_allocate_lock ))
0 commit comments