@@ -31,9 +31,10 @@ def main():
3131import math
3232import sys
3333import time
34+ import threading
3435
3536from contextlib import contextmanager
36- from typing import Any , Callable , Dict , Generator , TypeVar
37+ from typing import Any , Callable , Dict , Generator , Optional , TypeVar
3738
3839TIMER_FORMAT_VERSION = "0.1.0"
3940
@@ -97,19 +98,31 @@ class GaugeNode:
9798 Tracks the most recent value of a metric. This is analogous to gauges in statsd.
9899 """
99100
100- __slots__ = ["value" , "min_value" , "max_value" , "count" ]
101+ __slots__ = ["value" , "min_value" , "max_value" , "count" , "_timestamp" ]
101102
102103 def __init__ (self , value : float ):
103104 self .value = value
104105 self .min_value = value
105106 self .max_value = value
106107 self .count = 1
108+ # Internal timestamp so we can determine priority.
109+ self ._timestamp = time .time ()
107110
108111 def update (self , new_value : float ) -> None :
109112 self .min_value = min (self .min_value , new_value )
110113 self .max_value = max (self .max_value , new_value )
111114 self .value = new_value
112115 self .count += 1
116+ self ._timestamp = time .time ()
117+
118+ def merge (self , other : "GaugeNode" ) -> None :
119+ if self ._timestamp < other ._timestamp :
120+ # Keep the "later" value
121+ self .value = other .value
122+ self ._timestamp = other ._timestamp
123+ self .min_value = min (self .min_value , other .min_value )
124+ self .max_value = max (self .max_value , other .max_value )
125+ self .count += other .count
113126
114127 def as_dict (self ) -> Dict [str , float ]:
115128 return {
@@ -232,9 +245,23 @@ def _add_default_metadata(self):
232245 self .metadata ["command_line_arguments" ] = " " .join (sys .argv )
233246
234247
235- # Global instance of a TimerStack. This is generally all that we need for profiling, but you can potentially
236- # create multiple instances and pass them to the contextmanager
237- _global_timer_stack = TimerStack ()
248+ # Maintain a separate "global" timer per thread, so that they don't accidentally conflict with each other.
249+ _thread_timer_stacks : Dict [int , TimerStack ] = {}
250+
251+
252+ def _get_thread_timer () -> TimerStack :
253+ ident = threading .get_ident ()
254+ if ident not in _thread_timer_stacks :
255+ timer_stack = TimerStack ()
256+ _thread_timer_stacks [ident ] = timer_stack
257+ return _thread_timer_stacks [ident ]
258+
259+
260+ def get_timer_stack_for_thread (t : threading .Thread ) -> Optional [TimerStack ]:
261+ if t .ident is None :
262+ # Thread hasn't started, shouldn't ever happen
263+ return None
264+ return _thread_timer_stacks .get (t .ident )
238265
239266
240267@contextmanager
@@ -243,7 +270,7 @@ def hierarchical_timer(name: str, timer_stack: TimerStack = None) -> Generator:
243270 Creates a scoped timer around a block of code. This time spent will automatically be incremented when
244271 the context manager exits.
245272 """
246- timer_stack = timer_stack or _global_timer_stack
273+ timer_stack = timer_stack or _get_thread_timer ()
247274 timer_node = timer_stack .push (name )
248275 start_time = time .perf_counter ()
249276
@@ -284,34 +311,52 @@ def set_gauge(name: str, value: float, timer_stack: TimerStack = None) -> None:
284311 """
285312 Updates the value of the gauge (or creates it if it hasn't been set before).
286313 """
287- timer_stack = timer_stack or _global_timer_stack
314+ timer_stack = timer_stack or _get_thread_timer ()
288315 timer_stack .set_gauge (name , value )
289316
290317
318+ def merge_gauges (gauges : Dict [str , GaugeNode ], timer_stack : TimerStack = None ) -> None :
319+ """
320+ Merge the gauges from another TimerStack with the provided one (or the
321+ current thread's stack if none is provided).
322+ :param gauges:
323+ :param timer_stack:
324+ :return:
325+ """
326+ timer_stack = timer_stack or _get_thread_timer ()
327+ for n , g in gauges .items ():
328+ if n in timer_stack .gauges :
329+ timer_stack .gauges [n ].merge (g )
330+ else :
331+ timer_stack .gauges [n ] = g
332+
333+
291334def add_metadata (key : str , value : str , timer_stack : TimerStack = None ) -> None :
292- timer_stack = timer_stack or _global_timer_stack
335+ timer_stack = timer_stack or _get_thread_timer ()
293336 timer_stack .add_metadata (key , value )
294337
295338
296339def get_timer_tree (timer_stack : TimerStack = None ) -> Dict [str , Any ]:
297340 """
298- Return the tree of timings from the TimerStack as a dictionary (or the global stack if none is provided)
341+ Return the tree of timings from the TimerStack as a dictionary (or the
342+ current thread's stack if none is provided)
299343 """
300- timer_stack = timer_stack or _global_timer_stack
344+ timer_stack = timer_stack or _get_thread_timer ()
301345 return timer_stack .get_timing_tree ()
302346
303347
304348def get_timer_root (timer_stack : TimerStack = None ) -> TimerNode :
305349 """
306- Get the root TimerNode of the timer_stack (or the global TimerStack if not specified)
350+ Get the root TimerNode of the timer_stack (or the current thread's
351+ TimerStack if not specified)
307352 """
308- timer_stack = timer_stack or _global_timer_stack
353+ timer_stack = timer_stack or _get_thread_timer ()
309354 return timer_stack .get_root ()
310355
311356
312357def reset_timers (timer_stack : TimerStack = None ) -> None :
313358 """
314- Reset the timer_stack (or the global TimerStack if not specified)
359+ Reset the timer_stack (or the current thread's TimerStack if not specified)
315360 """
316- timer_stack = timer_stack or _global_timer_stack
361+ timer_stack = timer_stack or _get_thread_timer ()
317362 timer_stack .reset ()
0 commit comments