circlerefs_test.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. """Test script to find circular references.
  2. Circular references are not leaks per se, because they will eventually
  3. be GC'd. However, on CPython, they prevent the reference-counting fast
  4. path from being used and instead rely on the slower full GC. This
  5. increases memory footprint and CPU overhead, so we try to eliminate
  6. circular references created by normal operation.
  7. """
  8. import asyncio
  9. import contextlib
  10. import gc
  11. import io
  12. import sys
  13. import traceback
  14. import types
  15. import typing
  16. import unittest
  17. import tornado
  18. from tornado import web, gen, httpclient
  19. from tornado.test.util import skipNotCPython
  20. def find_circular_references(garbage):
  21. """Find circular references in a list of objects.
  22. The garbage list contains objects that participate in a cycle,
  23. but also the larger set of objects kept alive by that cycle.
  24. This function finds subsets of those objects that make up
  25. the cycle(s).
  26. """
  27. def inner(level):
  28. for item in level:
  29. item_id = id(item)
  30. if item_id not in garbage_ids:
  31. continue
  32. if item_id in visited_ids:
  33. continue
  34. if item_id in stack_ids:
  35. candidate = stack[stack.index(item) :]
  36. candidate.append(item)
  37. found.append(candidate)
  38. continue
  39. stack.append(item)
  40. stack_ids.add(item_id)
  41. inner(gc.get_referents(item))
  42. stack.pop()
  43. stack_ids.remove(item_id)
  44. visited_ids.add(item_id)
  45. found: typing.List[object] = []
  46. stack = []
  47. stack_ids = set()
  48. garbage_ids = set(map(id, garbage))
  49. visited_ids = set()
  50. inner(garbage)
  51. return found
  52. @contextlib.contextmanager
  53. def assert_no_cycle_garbage():
  54. """Raise AssertionError if the wrapped code creates garbage with cycles."""
  55. gc.disable()
  56. gc.collect()
  57. gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_SAVEALL)
  58. yield
  59. try:
  60. # We have DEBUG_STATS on which causes gc.collect to write to stderr.
  61. # Capture the output instead of spamming the logs on passing runs.
  62. f = io.StringIO()
  63. old_stderr = sys.stderr
  64. sys.stderr = f
  65. try:
  66. gc.collect()
  67. finally:
  68. sys.stderr = old_stderr
  69. garbage = gc.garbage[:]
  70. # Must clear gc.garbage (the same object, not just replacing it with a
  71. # new list) to avoid warnings at shutdown.
  72. gc.garbage[:] = []
  73. if len(garbage) == 0:
  74. return
  75. for circular in find_circular_references(garbage):
  76. f.write("\n==========\n Circular \n==========")
  77. for item in circular:
  78. f.write(f"\n {repr(item)}")
  79. for item in circular:
  80. if isinstance(item, types.FrameType):
  81. f.write(f"\nLocals: {item.f_locals}")
  82. f.write(f"\nTraceback: {repr(item)}")
  83. traceback.print_stack(item)
  84. del garbage
  85. raise AssertionError(f.getvalue())
  86. finally:
  87. gc.set_debug(0)
  88. gc.enable()
  89. # GC behavior is cpython-specific
  90. @skipNotCPython
  91. class CircleRefsTest(unittest.TestCase):
  92. def test_known_leak(self):
  93. # Construct a known leak scenario to make sure the test harness works.
  94. class C:
  95. def __init__(self, name):
  96. self.name = name
  97. self.a: typing.Optional[C] = None
  98. self.b: typing.Optional[C] = None
  99. self.c: typing.Optional[C] = None
  100. def __repr__(self):
  101. return f"name={self.name}"
  102. with self.assertRaises(AssertionError) as cm:
  103. with assert_no_cycle_garbage():
  104. # a and b form a reference cycle. c is not part of the cycle,
  105. # but it cannot be GC'd while a and b are alive.
  106. a = C("a")
  107. b = C("b")
  108. c = C("c")
  109. a.b = b
  110. a.c = c
  111. b.a = a
  112. b.c = c
  113. del a, b
  114. self.assertIn("Circular", str(cm.exception))
  115. # Leading spaces ensure we only catch these at the beginning of a line, meaning they are a
  116. # cycle participant and not simply the contents of a locals dict or similar container. (This
  117. # depends on the formatting above which isn't ideal but this test evolved from a
  118. # command-line script) Note that the behavior here changed in python 3.11; in newer pythons
  119. # locals are handled a bit differently and the test passes without the spaces.
  120. self.assertIn(" name=a", str(cm.exception))
  121. self.assertIn(" name=b", str(cm.exception))
  122. self.assertNotIn(" name=c", str(cm.exception))
  123. async def run_handler(self, handler_class):
  124. app = web.Application(
  125. [
  126. (r"/", handler_class),
  127. ]
  128. )
  129. socket, port = tornado.testing.bind_unused_port()
  130. server = tornado.httpserver.HTTPServer(app)
  131. server.add_socket(socket)
  132. client = httpclient.AsyncHTTPClient()
  133. with assert_no_cycle_garbage():
  134. # Only the fetch (and the corresponding server-side handler)
  135. # are being tested for cycles. In particular, the Application
  136. # object has internal cycles (as of this writing) which we don't
  137. # care to fix since in real world usage the Application object
  138. # is effectively a global singleton.
  139. await client.fetch(f"http://127.0.0.1:{port}/")
  140. client.close()
  141. server.stop()
  142. socket.close()
  143. def test_sync_handler(self):
  144. class Handler(web.RequestHandler):
  145. def get(self):
  146. self.write("ok\n")
  147. asyncio.run(self.run_handler(Handler))
  148. def test_finish_exception_handler(self):
  149. class Handler(web.RequestHandler):
  150. def get(self):
  151. raise web.Finish("ok\n")
  152. asyncio.run(self.run_handler(Handler))
  153. def test_coro_handler(self):
  154. class Handler(web.RequestHandler):
  155. @gen.coroutine
  156. def get(self):
  157. yield asyncio.sleep(0.01)
  158. self.write("ok\n")
  159. asyncio.run(self.run_handler(Handler))
  160. def test_async_handler(self):
  161. class Handler(web.RequestHandler):
  162. async def get(self):
  163. await asyncio.sleep(0.01)
  164. self.write("ok\n")
  165. asyncio.run(self.run_handler(Handler))
  166. def test_run_on_executor(self):
  167. # From https://github.com/tornadoweb/tornado/issues/2620
  168. #
  169. # When this test was introduced it found cycles in IOLoop.add_future
  170. # and tornado.concurrent.chain_future.
  171. import concurrent.futures
  172. with concurrent.futures.ThreadPoolExecutor(1) as thread_pool:
  173. class Factory:
  174. executor = thread_pool
  175. @tornado.concurrent.run_on_executor
  176. def run(self):
  177. return None
  178. factory = Factory()
  179. async def main():
  180. # The cycle is not reported on the first call. It's not clear why.
  181. for i in range(2):
  182. await factory.run()
  183. with assert_no_cycle_garbage():
  184. asyncio.run(main())