@@ -52,6 +52,7 @@ class _ProfiledLock:
5252 "init_location" ,
5353 "acquired_time" ,
5454 "name" ,
55+ "is_internal" ,
5556 )
5657
5758 def __init__ (
@@ -60,6 +61,7 @@ def __init__(
6061 tracer : Optional [Tracer ],
6162 max_nframes : int ,
6263 capture_sampler : collector .CaptureSampler ,
64+ is_internal : bool = False ,
6365 ) -> None :
6466 self .__wrapped__ : Any = wrapped
6567 self .tracer : Optional [Tracer ] = tracer
@@ -71,6 +73,9 @@ def __init__(
7173 self .init_location : str = f"{ os .path .basename (code .co_filename )} :{ frame .f_lineno } "
7274 self .acquired_time : int = 0
7375 self .name : Optional [str ] = None
76+ # If True, this lock is internal to another sync primitive (e.g., Lock inside Semaphore)
77+ # and should not generate profile samples to avoid double-counting
78+ self .is_internal : bool = is_internal
7479
7580 ### DUNDER methods ###
7681
@@ -161,6 +166,11 @@ def _flush_sample(self, start: int, end: int, is_acquire: bool) -> None:
161166 end: End timestamp in nanoseconds
162167 is_acquire: True for acquire operations, False for release operations
163168 """
169+ # Skip profiling for internal locks (e.g., Lock inside Semaphore/Condition)
170+ # to avoid double-counting when multiple collectors are active
171+ if self .is_internal :
172+ return
173+
164174 handle : ddup .SampleHandle = ddup .SampleHandle ()
165175
166176 handle .push_monotonic_ns (end )
@@ -262,6 +272,8 @@ class LockCollector(collector.CaptureSamplerCollector):
262272 """Record lock usage."""
263273
264274 PROFILED_LOCK_CLASS : Type [Any ]
275+ PATCH_MODULE : Any # e.g., threading module
276+ PATCH_ATTR_NAME : str # e.g., "Lock", "RLock", "Semaphore"
265277
266278 def __init__ (
267279 self ,
@@ -275,11 +287,11 @@ def __init__(
275287 self .tracer : Optional [Tracer ] = tracer
276288 self ._original_lock : Any = None
277289
278- @ abc . abstractmethod
279- def _get_patch_target ( self ) -> Callable [..., Any ]: ...
290+ def _get_patch_target ( self ) -> Callable [..., Any ]:
291+ return getattr ( self . PATCH_MODULE , self . PATCH_ATTR_NAME )
280292
281- @ abc . abstractmethod
282- def _set_patch_target (self , value : Any ) -> None : ...
293+ def _set_patch_target ( self , value : Any ) -> None :
294+ setattr (self . PATCH_MODULE , self . PATCH_ATTR_NAME , value )
283295
284296 def _start_service (self ) -> None :
285297 """Start collecting lock usage."""
@@ -297,12 +309,30 @@ def patch(self) -> None:
297309 original_lock : Any = self ._original_lock # Capture non-None value
298310
299311 def _profiled_allocate_lock (* args : Any , ** kwargs : Any ) -> _ProfiledLock :
300- """Simple wrapper that returns profiled locks."""
312+ """Simple wrapper that returns profiled locks.
313+
314+ Detects if the lock is being created from within threading.py stdlib
315+ (i.e., internal to Semaphore/Condition) to avoid double-counting.
316+ """
317+ import threading as threading_module
318+
319+ # Check if caller is from threading.py (internal lock)
320+ is_internal : bool = False
321+ try :
322+ # Frame 0: _profiled_allocate_lock
323+ # Frame 1: _LockAllocatorWrapper.__call__
324+ # Frame 2: actual caller (threading.Lock() call site)
325+ caller_filename = sys ._getframe (2 ).f_code .co_filename
326+ is_internal = bool (threading_module .__file__ ) and caller_filename == threading_module .__file__
327+ except (ValueError , AttributeError ):
328+ pass
329+
301330 return self .PROFILED_LOCK_CLASS (
302331 wrapped = original_lock (* args , ** kwargs ),
303332 tracer = self .tracer ,
304333 max_nframes = self .nframes ,
305334 capture_sampler = self ._capture_sampler ,
335+ is_internal = is_internal ,
306336 )
307337
308338 self ._set_patch_target (_LockAllocatorWrapper (_profiled_allocate_lock ))
0 commit comments