__init__.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. # Copyright 2015, 2016 OpenMarket Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import functools
  15. import gc
  16. import inspect
  17. import itertools
  18. import logging
  19. import os
  20. import platform
  21. import threading
  22. import time
  23. from typing import Any, Callable, Dict, Iterable, Mapping, Optional, Tuple, Union
  24. import attr
  25. from prometheus_client import Counter, Gauge, Histogram
  26. from prometheus_client.core import (
  27. REGISTRY,
  28. CounterMetricFamily,
  29. GaugeHistogramMetricFamily,
  30. GaugeMetricFamily,
  31. )
  32. from twisted.internet import reactor
  33. from twisted.internet.defer import Deferred
  34. from twisted.python.threadpool import ThreadPool
  35. import synapse
  36. from synapse.metrics._exposition import (
  37. MetricsResource,
  38. generate_latest,
  39. start_http_server,
  40. )
  41. from synapse.util.versionstring import get_version_string
  42. logger = logging.getLogger(__name__)
  43. METRICS_PREFIX = "/_synapse/metrics"
  44. running_on_pypy = platform.python_implementation() == "PyPy"
  45. all_gauges: "Dict[str, Union[LaterGauge, InFlightGauge]]" = {}
  46. HAVE_PROC_SELF_STAT = os.path.exists("/proc/self/stat")
  47. class RegistryProxy:
  48. @staticmethod
  49. def collect():
  50. for metric in REGISTRY.collect():
  51. if not metric.name.startswith("__"):
  52. yield metric
  53. @attr.s(slots=True, hash=True)
  54. class LaterGauge:
  55. name = attr.ib(type=str)
  56. desc = attr.ib(type=str)
  57. labels = attr.ib(hash=False, type=Optional[Iterable[str]])
  58. # callback: should either return a value (if there are no labels for this metric),
  59. # or dict mapping from a label tuple to a value
  60. caller = attr.ib(
  61. type=Callable[
  62. [], Union[Mapping[Tuple[str, ...], Union[int, float]], Union[int, float]]
  63. ]
  64. )
  65. def collect(self):
  66. g = GaugeMetricFamily(self.name, self.desc, labels=self.labels)
  67. try:
  68. calls = self.caller()
  69. except Exception:
  70. logger.exception("Exception running callback for LaterGauge(%s)", self.name)
  71. yield g
  72. return
  73. if isinstance(calls, (int, float)):
  74. g.add_metric([], calls)
  75. else:
  76. for k, v in calls.items():
  77. g.add_metric(k, v)
  78. yield g
  79. def __attrs_post_init__(self):
  80. self._register()
  81. def _register(self):
  82. if self.name in all_gauges.keys():
  83. logger.warning("%s already registered, reregistering" % (self.name,))
  84. REGISTRY.unregister(all_gauges.pop(self.name))
  85. REGISTRY.register(self)
  86. all_gauges[self.name] = self
  87. class InFlightGauge:
  88. """Tracks number of things (e.g. requests, Measure blocks, etc) in flight
  89. at any given time.
  90. Each InFlightGauge will create a metric called `<name>_total` that counts
  91. the number of in flight blocks, as well as a metrics for each item in the
  92. given `sub_metrics` as `<name>_<sub_metric>` which will get updated by the
  93. callbacks.
  94. Args:
  95. name (str)
  96. desc (str)
  97. labels (list[str])
  98. sub_metrics (list[str]): A list of sub metrics that the callbacks
  99. will update.
  100. """
  101. def __init__(self, name, desc, labels, sub_metrics):
  102. self.name = name
  103. self.desc = desc
  104. self.labels = labels
  105. self.sub_metrics = sub_metrics
  106. # Create a class which have the sub_metrics values as attributes, which
  107. # default to 0 on initialization. Used to pass to registered callbacks.
  108. self._metrics_class = attr.make_class(
  109. "_MetricsEntry", attrs={x: attr.ib(0) for x in sub_metrics}, slots=True
  110. )
  111. # Counts number of in flight blocks for a given set of label values
  112. self._registrations: Dict = {}
  113. # Protects access to _registrations
  114. self._lock = threading.Lock()
  115. self._register_with_collector()
  116. def register(self, key, callback):
  117. """Registers that we've entered a new block with labels `key`.
  118. `callback` gets called each time the metrics are collected. The same
  119. value must also be given to `unregister`.
  120. `callback` gets called with an object that has an attribute per
  121. sub_metric, which should be updated with the necessary values. Note that
  122. the metrics object is shared between all callbacks registered with the
  123. same key.
  124. Note that `callback` may be called on a separate thread.
  125. """
  126. with self._lock:
  127. self._registrations.setdefault(key, set()).add(callback)
  128. def unregister(self, key, callback):
  129. """Registers that we've exited a block with labels `key`."""
  130. with self._lock:
  131. self._registrations.setdefault(key, set()).discard(callback)
  132. def collect(self):
  133. """Called by prometheus client when it reads metrics.
  134. Note: may be called by a separate thread.
  135. """
  136. in_flight = GaugeMetricFamily(
  137. self.name + "_total", self.desc, labels=self.labels
  138. )
  139. metrics_by_key = {}
  140. # We copy so that we don't mutate the list while iterating
  141. with self._lock:
  142. keys = list(self._registrations)
  143. for key in keys:
  144. with self._lock:
  145. callbacks = set(self._registrations[key])
  146. in_flight.add_metric(key, len(callbacks))
  147. metrics = self._metrics_class()
  148. metrics_by_key[key] = metrics
  149. for callback in callbacks:
  150. callback(metrics)
  151. yield in_flight
  152. for name in self.sub_metrics:
  153. gauge = GaugeMetricFamily(
  154. "_".join([self.name, name]), "", labels=self.labels
  155. )
  156. for key, metrics in metrics_by_key.items():
  157. gauge.add_metric(key, getattr(metrics, name))
  158. yield gauge
  159. def _register_with_collector(self):
  160. if self.name in all_gauges.keys():
  161. logger.warning("%s already registered, reregistering" % (self.name,))
  162. REGISTRY.unregister(all_gauges.pop(self.name))
  163. REGISTRY.register(self)
  164. all_gauges[self.name] = self
  165. class GaugeBucketCollector:
  166. """Like a Histogram, but the buckets are Gauges which are updated atomically.
  167. The data is updated by calling `update_data` with an iterable of measurements.
  168. We assume that the data is updated less frequently than it is reported to
  169. Prometheus, and optimise for that case.
  170. """
  171. __slots__ = (
  172. "_name",
  173. "_documentation",
  174. "_bucket_bounds",
  175. "_metric",
  176. )
  177. def __init__(
  178. self,
  179. name: str,
  180. documentation: str,
  181. buckets: Iterable[float],
  182. registry=REGISTRY,
  183. ):
  184. """
  185. Args:
  186. name: base name of metric to be exported to Prometheus. (a _bucket suffix
  187. will be added.)
  188. documentation: help text for the metric
  189. buckets: The top bounds of the buckets to report
  190. registry: metric registry to register with
  191. """
  192. self._name = name
  193. self._documentation = documentation
  194. # the tops of the buckets
  195. self._bucket_bounds = [float(b) for b in buckets]
  196. if self._bucket_bounds != sorted(self._bucket_bounds):
  197. raise ValueError("Buckets not in sorted order")
  198. if self._bucket_bounds[-1] != float("inf"):
  199. self._bucket_bounds.append(float("inf"))
  200. # We initially set this to None. We won't report metrics until
  201. # this has been initialised after a successful data update
  202. self._metric: Optional[GaugeHistogramMetricFamily] = None
  203. registry.register(self)
  204. def collect(self):
  205. # Don't report metrics unless we've already collected some data
  206. if self._metric is not None:
  207. yield self._metric
  208. def update_data(self, values: Iterable[float]):
  209. """Update the data to be reported by the metric
  210. The existing data is cleared, and each measurement in the input is assigned
  211. to the relevant bucket.
  212. """
  213. self._metric = self._values_to_metric(values)
  214. def _values_to_metric(self, values: Iterable[float]) -> GaugeHistogramMetricFamily:
  215. total = 0.0
  216. bucket_values = [0 for _ in self._bucket_bounds]
  217. for v in values:
  218. # assign each value to a bucket
  219. for i, bound in enumerate(self._bucket_bounds):
  220. if v <= bound:
  221. bucket_values[i] += 1
  222. break
  223. # ... and increment the sum
  224. total += v
  225. # now, aggregate the bucket values so that they count the number of entries in
  226. # that bucket or below.
  227. accumulated_values = itertools.accumulate(bucket_values)
  228. return GaugeHistogramMetricFamily(
  229. self._name,
  230. self._documentation,
  231. buckets=list(
  232. zip((str(b) for b in self._bucket_bounds), accumulated_values)
  233. ),
  234. gsum_value=total,
  235. )
  236. #
  237. # Detailed CPU metrics
  238. #
  239. class CPUMetrics:
  240. def __init__(self):
  241. ticks_per_sec = 100
  242. try:
  243. # Try and get the system config
  244. ticks_per_sec = os.sysconf("SC_CLK_TCK")
  245. except (ValueError, TypeError, AttributeError):
  246. pass
  247. self.ticks_per_sec = ticks_per_sec
  248. def collect(self):
  249. if not HAVE_PROC_SELF_STAT:
  250. return
  251. with open("/proc/self/stat") as s:
  252. line = s.read()
  253. raw_stats = line.split(") ", 1)[1].split(" ")
  254. user = GaugeMetricFamily("process_cpu_user_seconds_total", "")
  255. user.add_metric([], float(raw_stats[11]) / self.ticks_per_sec)
  256. yield user
  257. sys = GaugeMetricFamily("process_cpu_system_seconds_total", "")
  258. sys.add_metric([], float(raw_stats[12]) / self.ticks_per_sec)
  259. yield sys
  260. REGISTRY.register(CPUMetrics())
  261. #
  262. # Python GC metrics
  263. #
  264. gc_unreachable = Gauge("python_gc_unreachable_total", "Unreachable GC objects", ["gen"])
  265. gc_time = Histogram(
  266. "python_gc_time",
  267. "Time taken to GC (sec)",
  268. ["gen"],
  269. buckets=[
  270. 0.0025,
  271. 0.005,
  272. 0.01,
  273. 0.025,
  274. 0.05,
  275. 0.10,
  276. 0.25,
  277. 0.50,
  278. 1.00,
  279. 2.50,
  280. 5.00,
  281. 7.50,
  282. 15.00,
  283. 30.00,
  284. 45.00,
  285. 60.00,
  286. ],
  287. )
  288. class GCCounts:
  289. def collect(self):
  290. cm = GaugeMetricFamily("python_gc_counts", "GC object counts", labels=["gen"])
  291. for n, m in enumerate(gc.get_count()):
  292. cm.add_metric([str(n)], m)
  293. yield cm
  294. if not running_on_pypy:
  295. REGISTRY.register(GCCounts())
  296. #
  297. # PyPy GC / memory metrics
  298. #
  299. class PyPyGCStats:
  300. def collect(self):
  301. # @stats is a pretty-printer object with __str__() returning a nice table,
  302. # plus some fields that contain data from that table.
  303. # unfortunately, fields are pretty-printed themselves (i. e. '4.5MB').
  304. stats = gc.get_stats(memory_pressure=False) # type: ignore
  305. # @s contains same fields as @stats, but as actual integers.
  306. s = stats._s # type: ignore
  307. # also note that field naming is completely braindead
  308. # and only vaguely correlates with the pretty-printed table.
  309. # >>>> gc.get_stats(False)
  310. # Total memory consumed:
  311. # GC used: 8.7MB (peak: 39.0MB) # s.total_gc_memory, s.peak_memory
  312. # in arenas: 3.0MB # s.total_arena_memory
  313. # rawmalloced: 1.7MB # s.total_rawmalloced_memory
  314. # nursery: 4.0MB # s.nursery_size
  315. # raw assembler used: 31.0kB # s.jit_backend_used
  316. # -----------------------------
  317. # Total: 8.8MB # stats.memory_used_sum
  318. #
  319. # Total memory allocated:
  320. # GC allocated: 38.7MB (peak: 41.1MB) # s.total_allocated_memory, s.peak_allocated_memory
  321. # in arenas: 30.9MB # s.peak_arena_memory
  322. # rawmalloced: 4.1MB # s.peak_rawmalloced_memory
  323. # nursery: 4.0MB # s.nursery_size
  324. # raw assembler allocated: 1.0MB # s.jit_backend_allocated
  325. # -----------------------------
  326. # Total: 39.7MB # stats.memory_allocated_sum
  327. #
  328. # Total time spent in GC: 0.073 # s.total_gc_time
  329. pypy_gc_time = CounterMetricFamily(
  330. "pypy_gc_time_seconds_total",
  331. "Total time spent in PyPy GC",
  332. labels=[],
  333. )
  334. pypy_gc_time.add_metric([], s.total_gc_time / 1000)
  335. yield pypy_gc_time
  336. pypy_mem = GaugeMetricFamily(
  337. "pypy_memory_bytes",
  338. "Memory tracked by PyPy allocator",
  339. labels=["state", "class", "kind"],
  340. )
  341. # memory used by JIT assembler
  342. pypy_mem.add_metric(["used", "", "jit"], s.jit_backend_used)
  343. pypy_mem.add_metric(["allocated", "", "jit"], s.jit_backend_allocated)
  344. # memory used by GCed objects
  345. pypy_mem.add_metric(["used", "", "arenas"], s.total_arena_memory)
  346. pypy_mem.add_metric(["allocated", "", "arenas"], s.peak_arena_memory)
  347. pypy_mem.add_metric(["used", "", "rawmalloced"], s.total_rawmalloced_memory)
  348. pypy_mem.add_metric(["allocated", "", "rawmalloced"], s.peak_rawmalloced_memory)
  349. pypy_mem.add_metric(["used", "", "nursery"], s.nursery_size)
  350. pypy_mem.add_metric(["allocated", "", "nursery"], s.nursery_size)
  351. # totals
  352. pypy_mem.add_metric(["used", "totals", "gc"], s.total_gc_memory)
  353. pypy_mem.add_metric(["allocated", "totals", "gc"], s.total_allocated_memory)
  354. pypy_mem.add_metric(["used", "totals", "gc_peak"], s.peak_memory)
  355. pypy_mem.add_metric(["allocated", "totals", "gc_peak"], s.peak_allocated_memory)
  356. yield pypy_mem
  357. if running_on_pypy:
  358. REGISTRY.register(PyPyGCStats())
  359. #
  360. # Twisted reactor metrics
  361. #
  362. tick_time = Histogram(
  363. "python_twisted_reactor_tick_time",
  364. "Tick time of the Twisted reactor (sec)",
  365. buckets=[0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.5, 1, 2, 5],
  366. )
  367. pending_calls_metric = Histogram(
  368. "python_twisted_reactor_pending_calls",
  369. "Pending calls",
  370. buckets=[1, 2, 5, 10, 25, 50, 100, 250, 500, 1000],
  371. )
  372. #
  373. # Federation Metrics
  374. #
  375. sent_transactions_counter = Counter("synapse_federation_client_sent_transactions", "")
  376. events_processed_counter = Counter("synapse_federation_client_events_processed", "")
  377. event_processing_loop_counter = Counter(
  378. "synapse_event_processing_loop_count", "Event processing loop iterations", ["name"]
  379. )
  380. event_processing_loop_room_count = Counter(
  381. "synapse_event_processing_loop_room_count",
  382. "Rooms seen per event processing loop iteration",
  383. ["name"],
  384. )
  385. # Used to track where various components have processed in the event stream,
  386. # e.g. federation sending, appservice sending, etc.
  387. event_processing_positions = Gauge("synapse_event_processing_positions", "", ["name"])
  388. # Used to track the current max events stream position
  389. event_persisted_position = Gauge("synapse_event_persisted_position", "")
  390. # Used to track the received_ts of the last event processed by various
  391. # components
  392. event_processing_last_ts = Gauge("synapse_event_processing_last_ts", "", ["name"])
  393. # Used to track the lag processing events. This is the time difference
  394. # between the last processed event's received_ts and the time it was
  395. # finished being processed.
  396. event_processing_lag = Gauge("synapse_event_processing_lag", "", ["name"])
  397. event_processing_lag_by_event = Histogram(
  398. "synapse_event_processing_lag_by_event",
  399. "Time between an event being persisted and it being queued up to be sent to the relevant remote servers",
  400. ["name"],
  401. )
  402. # Build info of the running server.
  403. build_info = Gauge(
  404. "synapse_build_info", "Build information", ["pythonversion", "version", "osversion"]
  405. )
  406. build_info.labels(
  407. " ".join([platform.python_implementation(), platform.python_version()]),
  408. get_version_string(synapse),
  409. " ".join([platform.system(), platform.release()]),
  410. ).set(1)
  411. last_ticked = time.time()
  412. # 3PID send info
  413. threepid_send_requests = Histogram(
  414. "synapse_threepid_send_requests_with_tries",
  415. documentation="Number of requests for a 3pid token by try count. Note if"
  416. " there is a request with try count of 4, then there would have been one"
  417. " each for 1, 2 and 3",
  418. buckets=(1, 2, 3, 4, 5, 10),
  419. labelnames=("type", "reason"),
  420. )
  421. threadpool_total_threads = Gauge(
  422. "synapse_threadpool_total_threads",
  423. "Total number of threads currently in the threadpool",
  424. ["name"],
  425. )
  426. threadpool_total_working_threads = Gauge(
  427. "synapse_threadpool_working_threads",
  428. "Number of threads currently working in the threadpool",
  429. ["name"],
  430. )
  431. threadpool_total_min_threads = Gauge(
  432. "synapse_threadpool_min_threads",
  433. "Minimum number of threads configured in the threadpool",
  434. ["name"],
  435. )
  436. threadpool_total_max_threads = Gauge(
  437. "synapse_threadpool_max_threads",
  438. "Maximum number of threads configured in the threadpool",
  439. ["name"],
  440. )
  441. def register_threadpool(name: str, threadpool: ThreadPool) -> None:
  442. """Add metrics for the threadpool."""
  443. threadpool_total_min_threads.labels(name).set(threadpool.min)
  444. threadpool_total_max_threads.labels(name).set(threadpool.max)
  445. threadpool_total_threads.labels(name).set_function(lambda: len(threadpool.threads))
  446. threadpool_total_working_threads.labels(name).set_function(
  447. lambda: len(threadpool.working)
  448. )
  449. class ReactorLastSeenMetric:
  450. def collect(self):
  451. cm = GaugeMetricFamily(
  452. "python_twisted_reactor_last_seen",
  453. "Seconds since the Twisted reactor was last seen",
  454. )
  455. cm.add_metric([], time.time() - last_ticked)
  456. yield cm
  457. REGISTRY.register(ReactorLastSeenMetric())
  458. # The minimum time in seconds between GCs for each generation, regardless of the current GC
  459. # thresholds and counts.
  460. MIN_TIME_BETWEEN_GCS = (1.0, 10.0, 30.0)
  461. # The time (in seconds since the epoch) of the last time we did a GC for each generation.
  462. _last_gc = [0.0, 0.0, 0.0]
  463. def callFromThreadTimer(func):
  464. @functools.wraps(func)
  465. def callFromThread(f: Callable[..., Any], *args: object, **kwargs: object) -> None:
  466. @functools.wraps(f)
  467. def g(*args, **kwargs):
  468. callbacks = None
  469. if inspect.ismethod(f):
  470. if isinstance(f.__self__, Deferred):
  471. callbacks = list(f.__self__.callbacks)
  472. start = time.time()
  473. r = f(*args, **kwargs)
  474. end = time.time()
  475. if end - start > 0.5:
  476. logger.warning(
  477. "callFromThread took %f seconds. name: %s, callbacks: %s",
  478. end - start,
  479. f,
  480. callbacks,
  481. )
  482. return r
  483. func(g, *args, **kwargs)
  484. return callFromThread
  485. def runUntilCurrentTimer(reactor, func):
  486. @functools.wraps(func)
  487. def f(*args, **kwargs):
  488. now = reactor.seconds()
  489. num_pending = 0
  490. # _newTimedCalls is one long list of *all* pending calls. Below loop
  491. # is based off of impl of reactor.runUntilCurrent
  492. for delayed_call in reactor._newTimedCalls:
  493. if delayed_call.time > now:
  494. break
  495. if delayed_call.delayed_time > 0:
  496. continue
  497. num_pending += 1
  498. num_pending += len(reactor.threadCallQueue)
  499. start = time.time()
  500. ret = func(*args, **kwargs)
  501. end = time.time()
  502. if end - start > 0.05:
  503. logger.warning(
  504. "runUntilCurrent took %f seconds. num_pending: %d",
  505. end - start,
  506. num_pending,
  507. )
  508. # record the amount of wallclock time spent running pending calls.
  509. # This is a proxy for the actual amount of time between reactor polls,
  510. # since about 25% of time is actually spent running things triggered by
  511. # I/O events, but that is harder to capture without rewriting half the
  512. # reactor.
  513. tick_time.observe(end - start)
  514. pending_calls_metric.observe(num_pending)
  515. # Update the time we last ticked, for the metric to test whether
  516. # Synapse's reactor has frozen
  517. global last_ticked
  518. last_ticked = end
  519. if running_on_pypy:
  520. return ret
  521. # Check if we need to do a manual GC (since its been disabled), and do
  522. # one if necessary. Note we go in reverse order as e.g. a gen 1 GC may
  523. # promote an object into gen 2, and we don't want to handle the same
  524. # object multiple times.
  525. threshold = gc.get_threshold()
  526. counts = gc.get_count()
  527. for i in (2, 1, 0):
  528. # We check if we need to do one based on a straightforward
  529. # comparison between the threshold and count. We also do an extra
  530. # check to make sure that we don't a GC too often.
  531. if threshold[i] < counts[i] and MIN_TIME_BETWEEN_GCS[i] < end - _last_gc[i]:
  532. if i == 0:
  533. logger.debug("Collecting gc %d", i)
  534. else:
  535. logger.info("Collecting gc %d", i)
  536. start = time.time()
  537. unreachable = gc.collect(i)
  538. end = time.time()
  539. _last_gc[i] = end
  540. gc_time.labels(i).observe(end - start)
  541. gc_unreachable.labels(i).set(unreachable)
  542. return ret
  543. return f
  544. try:
  545. # Ensure the reactor has all the attributes we expect
  546. reactor.seconds # type: ignore
  547. reactor.runUntilCurrent # type: ignore
  548. reactor._newTimedCalls # type: ignore
  549. reactor.threadCallQueue # type: ignore
  550. # runUntilCurrent is called when we have pending calls. It is called once
  551. # per iteratation after fd polling.
  552. reactor.runUntilCurrent = runUntilCurrentTimer(reactor, reactor.runUntilCurrent) # type: ignore
  553. reactor.callFromThread = callFromThreadTimer(reactor.callFromThread)
  554. # We manually run the GC each reactor tick so that we can get some metrics
  555. # about time spent doing GC,
  556. if not running_on_pypy:
  557. gc.disable()
  558. except AttributeError:
  559. pass
  560. __all__ = [
  561. "MetricsResource",
  562. "generate_latest",
  563. "start_http_server",
  564. "LaterGauge",
  565. "InFlightGauge",
  566. "BucketCollector",
  567. ]