@@ -189,10 +189,10 @@ class ContextMeter:
189189 A->B comms: network-write 0.567 seconds
190190 """
191191
192- _callbacks : ContextVar [list [ Callable [[Hashable , float , str ], None ]]]
192+ _callbacks : ContextVar [dict [ Hashable , Callable [[Hashable , float , str ], None ]]]
193193
194194 def __init__ (self ):
195- self ._callbacks = ContextVar (f"MetricHook<{ id (self )} >._callbacks" , default = [] )
195+ self ._callbacks = ContextVar (f"MetricHook<{ id (self )} >._callbacks" , default = {} )
196196
197197 def __reduce__ (self ):
198198 assert self is context_meter , "Found copy of singleton"
@@ -204,13 +204,28 @@ def _unpickle_singleton():
204204
205205 @contextmanager
206206 def add_callback (
207- self , callback : Callable [[Hashable , float , str ], None ]
207+ self ,
208+ callback : Callable [[Hashable , float , str ], None ],
209+ * ,
210+ key : Hashable | None = None ,
208211 ) -> Iterator [None ]:
209212 """Add a callback when entering the context and remove it when exiting it.
210213 The callback must accept the same parameters as :meth:`digest_metric`.
214+
215+ Parameters
216+ ----------
217+ callback: Callable
218+ ``f(label, value, unit)`` to be executed
219+ key: Hashable, optional
220+ Unique key for the callback. If two nested calls to ``add_callback`` use the
221+ same key, suppress the outermost callback.
211222 """
223+ if key is None :
224+ key = object ()
212225 cbs = self ._callbacks .get ()
213- tok = self ._callbacks .set (cbs + [callback ])
226+ cbs = cbs .copy ()
227+ cbs [key ] = callback
228+ tok = self ._callbacks .set (cbs )
214229 try :
215230 yield
216231 finally :
@@ -221,7 +236,7 @@ def digest_metric(self, label: Hashable, value: float, unit: str) -> None:
221236 metric.
222237 """
223238 cbs = self ._callbacks .get ()
224- for cb in cbs :
239+ for cb in cbs . values () :
225240 cb (label , value , unit )
226241
227242 @contextmanager
@@ -234,9 +249,10 @@ def meter(
234249 ) -> Iterator [MeterOutput ]:
235250 """Convenience context manager or decorator which calls func() before and after
236251 the wrapped code, calculates the delta, and finally calls :meth:`digest_metric`.
237- It also subtracts any other calls to :meth:`meter` or :meth:`digest_metric` with
238- the same unit performed within the context, so that the total is strictly
239- additive.
252+
253+ If unit=='seconds', it also subtracts any other calls to :meth:`meter` or
254+ :meth:`digest_metric` with the same unit performed within the context, so that
255+ the total is strictly additive.
240256
241257 Parameters
242258 ----------
@@ -256,10 +272,19 @@ def meter(
256272 nested calls to :meth:`meter`, then delta (for seconds only) is reduced by the
257273 inner metrics, to a minimum of ``floor``.
258274 """
275+ if unit != "seconds" :
276+ try :
277+ with meter (func , floor = floor ) as m :
278+ yield m
279+ finally :
280+ self .digest_metric (label , m .delta , unit )
281+ return
282+
283+ # If unit=="seconds", subtract time metered from the sub-contexts
259284 offsets = []
260285
261286 def callback (label2 : Hashable , value2 : float , unit2 : str ) -> None :
262- if unit2 == unit == "seconds" :
287+ if unit2 == unit :
263288 # This must be threadsafe to support callbacks invoked from
264289 # distributed.utils.offload; '+=' on a float would not be threadsafe!
265290 offsets .append (value2 )
@@ -316,14 +341,20 @@ def __init__(self, func: Callable[[], float] = timemod.perf_counter):
316341 self .start = func ()
317342 self .metrics = []
318343
344+ def _callback (self , label : Hashable , value : float , unit : str ) -> None :
345+ self .metrics .append ((label , value , unit ))
346+
319347 @contextmanager
320- def record (self ) -> Iterator [None ]:
348+ def record (self , * , key : Hashable | None = None ) -> Iterator [None ]:
321349 """Ingest metrics logged with :meth:`ContextMeter.digest_metric` or
322350 :meth:`ContextMeter.meter` and temporarily store them in :ivar:`metrics`.
351+
352+ Parameters
353+ ----------
354+ key: Hashable, optional
355+ See :meth:`ContextMeter.add_callback`
323356 """
324- with context_meter .add_callback (
325- lambda label , value , unit : self .metrics .append ((label , value , unit ))
326- ):
357+ with context_meter .add_callback (self ._callback , key = key ):
327358 yield
328359
329360 def finalize (
0 commit comments