gooderp18绿色标准版
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

731 lines
28KB

  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from datetime import datetime
  4. import gc
  5. import json
  6. import logging
  7. import sys
  8. import time
  9. import threading
  10. import re
  11. import functools
  12. from psycopg2 import OperationalError
  13. from odoo import tools
  14. from odoo.tools import SQL
  15. _logger = logging.getLogger(__name__)
  16. # ensure we have a non patched time for profiling times when using freezegun
  17. real_datetime_now = datetime.now
  18. real_time = time.time.__call__
  19. def _format_frame(frame):
  20. code = frame.f_code
  21. return (code.co_filename, frame.f_lineno, code.co_name, '')
  22. def _format_stack(stack):
  23. return [list(frame) for frame in stack]
  24. def get_current_frame(thread=None):
  25. if thread:
  26. frame = sys._current_frames()[thread.ident]
  27. else:
  28. frame = sys._getframe()
  29. while frame.f_code.co_filename == __file__:
  30. frame = frame.f_back
  31. return frame
  32. def _get_stack_trace(frame, limit_frame=None):
  33. stack = []
  34. while frame is not None and frame != limit_frame:
  35. stack.append(_format_frame(frame))
  36. frame = frame.f_back
  37. if frame is None and limit_frame:
  38. _logger.error("Limit frame was not found")
  39. return list(reversed(stack))
  40. def stack_size():
  41. frame = get_current_frame()
  42. size = 0
  43. while frame:
  44. size += 1
  45. frame = frame.f_back
  46. return size
  47. def make_session(name=''):
  48. return f'{real_datetime_now():%Y-%m-%d %H:%M:%S} {name}'
  49. def force_hook():
  50. """
  51. Force periodic profiling collectors to generate some stack trace. This is
  52. useful before long calls that do not release the GIL, so that the time
  53. spent in those calls is attributed to a specific stack trace, instead of
  54. some arbitrary former frame.
  55. """
  56. thread = threading.current_thread()
  57. for func in getattr(thread, 'profile_hooks', ()):
  58. func()
  59. class Collector:
  60. """
  61. Base class for objects that collect profiling data.
  62. A collector object is used by a profiler to collect profiling data, most
  63. likely a list of stack traces with time and some context information added
  64. by ExecutionContext decorator on current thread.
  65. This is a generic implementation of a basic collector, to be inherited.
  66. It defines default behaviors for creating an entry in the collector.
  67. """
  68. name = None # symbolic name of the collector
  69. _registry = {} # map collector names to their class
  70. @classmethod
  71. def __init_subclass__(cls):
  72. if cls.name:
  73. cls._registry[cls.name] = cls
  74. cls._registry[cls.__name__] = cls
  75. @classmethod
  76. def make(cls, name, *args, **kwargs):
  77. """ Instantiate a collector corresponding to the given name. """
  78. return cls._registry[name](*args, **kwargs)
  79. def __init__(self):
  80. self._processed = False
  81. self._entries = []
  82. self.profiler = None
  83. def start(self):
  84. """ Start the collector. """
  85. def stop(self):
  86. """ Stop the collector. """
  87. def add(self, entry=None, frame=None):
  88. """ Add an entry (dict) to this collector. """
  89. # todo add entry count limit
  90. self._entries.append({
  91. 'stack': self._get_stack_trace(frame),
  92. 'exec_context': getattr(self.profiler.init_thread, 'exec_context', ()),
  93. 'start': real_time(),
  94. **(entry or {}),
  95. })
  96. def _get_stack_trace(self, frame=None):
  97. """ Return the stack trace to be included in a given entry. """
  98. frame = frame or get_current_frame(self.profiler.init_thread)
  99. return _get_stack_trace(frame, self.profiler.init_frame)
  100. def post_process(self):
  101. for entry in self._entries:
  102. stack = entry.get('stack', [])
  103. self.profiler._add_file_lines(stack)
  104. @property
  105. def entries(self):
  106. """ Return the entries of the collector after postprocessing. """
  107. if not self._processed:
  108. self.post_process()
  109. self._processed = True
  110. return self._entries
  111. def summary(self):
  112. return f"{'='*10} {self.name} {'='*10} \n Entries: {len(self._entries)}"
  113. class SQLCollector(Collector):
  114. """
  115. Saves all executed queries in the current thread with the call stack.
  116. """
  117. name = 'sql'
  118. def start(self):
  119. init_thread = self.profiler.init_thread
  120. if not hasattr(init_thread, 'query_hooks'):
  121. init_thread.query_hooks = []
  122. init_thread.query_hooks.append(self.hook)
  123. def stop(self):
  124. self.profiler.init_thread.query_hooks.remove(self.hook)
  125. def hook(self, cr, query, params, query_start, query_time):
  126. self.add({
  127. 'query': str(query),
  128. 'full_query': str(cr._format(query, params)),
  129. 'start': query_start,
  130. 'time': query_time,
  131. })
  132. def summary(self):
  133. total_time = sum(entry['time'] for entry in self._entries) or 1
  134. sql_entries = ''
  135. for entry in self._entries:
  136. sql_entries += f"\n{'-' * 100}'\n'{entry['time']} {'*' * int(entry['time'] / total_time * 100)}'\n'{entry['full_query']}"
  137. return super().summary() + sql_entries
  138. class PeriodicCollector(Collector):
  139. """
  140. Record execution frames asynchronously at most every `interval` seconds.
  141. :param interval (float): time to wait in seconds between two samples.
  142. """
  143. name = 'traces_async'
  144. def __init__(self, interval=0.01): # check duration. dynamic?
  145. super().__init__()
  146. self.active = False
  147. self.frame_interval = interval
  148. self.__thread = threading.Thread(target=self.run)
  149. self.last_frame = None
  150. def run(self):
  151. self.active = True
  152. last_time = real_time()
  153. while self.active: # maybe add a check on parent_thread state?
  154. duration = real_time() - last_time
  155. if duration > self.frame_interval * 10 and self.last_frame:
  156. # The profiler has unexpectedly slept for more than 10 frame intervals. This may
  157. # happen when calling a C library without releasing the GIL. In that case, the
  158. # last frame was taken before the call, and the next frame is after the call, and
  159. # the call itself does not appear in any of those frames: the duration of the call
  160. # is incorrectly attributed to the last frame.
  161. self._entries[-1]['stack'].append(('profiling', 0, '⚠ Profiler freezed for %s s' % duration, ''))
  162. self.last_frame = None # skip duplicate detection for the next frame.
  163. self.add()
  164. last_time = real_time()
  165. time.sleep(self.frame_interval)
  166. self._entries.append({'stack': [], 'start': real_time()}) # add final end frame
  167. def start(self):
  168. interval = self.profiler.params.get('traces_async_interval')
  169. if interval:
  170. self.frame_interval = min(max(float(interval), 0.001), 1)
  171. init_thread = self.profiler.init_thread
  172. if not hasattr(init_thread, 'profile_hooks'):
  173. init_thread.profile_hooks = []
  174. init_thread.profile_hooks.append(self.add)
  175. self.__thread.start()
  176. def stop(self):
  177. self.active = False
  178. self.__thread.join()
  179. self.profiler.init_thread.profile_hooks.remove(self.add)
  180. def add(self, entry=None, frame=None):
  181. """ Add an entry (dict) to this collector. """
  182. frame = frame or get_current_frame(self.profiler.init_thread)
  183. if frame == self.last_frame:
  184. # don't save if the frame is exactly the same as the previous one.
  185. # maybe modify the last entry to add a last seen?
  186. return
  187. self.last_frame = frame
  188. super().add(entry=entry, frame=frame)
  189. class SyncCollector(Collector):
  190. """
  191. Record complete execution synchronously.
  192. Note that --limit-memory-hard may need to be increased when launching Odoo.
  193. """
  194. name = 'traces_sync'
  195. def start(self):
  196. if sys.gettrace() is not None:
  197. _logger.error("Cannot start SyncCollector, settrace already set: %s", sys.gettrace())
  198. assert not self._processed, "You cannot start SyncCollector after accessing entries."
  199. sys.settrace(self.hook) # todo test setprofile, but maybe not multithread safe
  200. def stop(self):
  201. sys.settrace(None)
  202. def hook(self, _frame, event, _arg=None):
  203. if event == 'line':
  204. return
  205. entry = {'event': event, 'frame': _format_frame(_frame)}
  206. if event == 'call' and _frame.f_back:
  207. # we need the parent frame to determine the line number of the call
  208. entry['parent_frame'] = _format_frame(_frame.f_back)
  209. self.add(entry, frame=_frame)
  210. return self.hook
  211. def _get_stack_trace(self, frame=None):
  212. # Getting the full stack trace is slow, and not useful in this case.
  213. # SyncCollector only saves the top frame and event at each call and
  214. # recomputes the complete stack at the end.
  215. return None
  216. def post_process(self):
  217. # Transform the evented traces to full stack traces. This processing
  218. # could be avoided since speedscope will transform that back to
  219. # evented anyway, but it is actually simpler to integrate into the
  220. # current speedscope logic, especially when mixed with SQLCollector.
  221. # We could improve it by saving as evented and manage it later.
  222. stack = []
  223. for entry in self._entries:
  224. frame = entry.pop('frame')
  225. event = entry.pop('event')
  226. if event == 'call':
  227. if stack:
  228. stack[-1] = entry.pop('parent_frame')
  229. stack.append(frame)
  230. elif event == 'return':
  231. stack.pop()
  232. entry['stack'] = stack[:]
  233. super().post_process()
  234. class QwebTracker():
  235. @classmethod
  236. def wrap_render(cls, method_render):
  237. @functools.wraps(method_render)
  238. def _tracked_method_render(self, template, values=None, **options):
  239. current_thread = threading.current_thread()
  240. execution_context_enabled = getattr(current_thread, 'profiler_params', {}).get('execution_context_qweb')
  241. qweb_hooks = getattr(current_thread, 'qweb_hooks', ())
  242. if execution_context_enabled or qweb_hooks:
  243. # To have the new compilation cached because the generated code will change.
  244. # Therefore 'profile' is a key to the cache.
  245. options['profile'] = True
  246. return method_render(self, template, values, **options)
  247. return _tracked_method_render
  248. @classmethod
  249. def wrap_compile(cls, method_compile):
  250. @functools.wraps(method_compile)
  251. def _tracked_compile(self, template):
  252. if not self.env.context.get('profile'):
  253. return method_compile(self, template)
  254. template_functions, def_name = method_compile(self, template)
  255. render_template = template_functions[def_name]
  256. def profiled_method_compile(self, values):
  257. options = template_functions['options']
  258. ref = options.get('ref')
  259. ref_xml = options.get('ref_xml')
  260. qweb_tracker = QwebTracker(ref, ref_xml, self.env.cr)
  261. self = self.with_context(qweb_tracker=qweb_tracker)
  262. if qweb_tracker.execution_context_enabled:
  263. with ExecutionContext(template=ref):
  264. return render_template(self, values)
  265. return render_template(self, values)
  266. template_functions[def_name] = profiled_method_compile
  267. return (template_functions, def_name)
  268. return _tracked_compile
  269. @classmethod
  270. def wrap_compile_directive(cls, method_compile_directive):
  271. @functools.wraps(method_compile_directive)
  272. def _tracked_compile_directive(self, el, options, directive, level):
  273. if not options.get('profile') or directive in ('inner-content', 'tag-open', 'tag-close'):
  274. return method_compile_directive(self, el, options, directive, level)
  275. enter = f"{' ' * 4 * level}self.env.context['qweb_tracker'].enter_directive({directive!r}, {el.attrib!r}, {options['_qweb_error_path_xml'][0]!r})"
  276. leave = f"{' ' * 4 * level}self.env.context['qweb_tracker'].leave_directive({directive!r}, {el.attrib!r}, {options['_qweb_error_path_xml'][0]!r})"
  277. code_directive = method_compile_directive(self, el, options, directive, level)
  278. return [enter, *code_directive, leave] if code_directive else []
  279. return _tracked_compile_directive
  280. def __init__(self, view_id, arch, cr):
  281. current_thread = threading.current_thread() # don't store current_thread on self
  282. self.execution_context_enabled = getattr(current_thread, 'profiler_params', {}).get('execution_context_qweb')
  283. self.qweb_hooks = getattr(current_thread, 'qweb_hooks', ())
  284. self.context_stack = []
  285. self.cr = cr
  286. self.view_id = view_id
  287. for hook in self.qweb_hooks:
  288. hook('render', self.cr.sql_log_count, view_id=view_id, arch=arch)
  289. def enter_directive(self, directive, attrib, xpath):
  290. execution_context = None
  291. if self.execution_context_enabled:
  292. directive_info = {}
  293. if ('t-' + directive) in attrib:
  294. directive_info['t-' + directive] = repr(attrib['t-' + directive])
  295. if directive == 'set':
  296. if 't-value' in attrib:
  297. directive_info['t-value'] = repr(attrib['t-value'])
  298. if 't-valuef' in attrib:
  299. directive_info['t-valuef'] = repr(attrib['t-valuef'])
  300. for key in attrib:
  301. if key.startswith('t-set-') or key.startswith('t-setf-'):
  302. directive_info[key] = repr(attrib[key])
  303. elif directive == 'foreach':
  304. directive_info['t-as'] = repr(attrib['t-as'])
  305. elif directive == 'groups' and 'groups' in attrib and not directive_info.get('t-groups'):
  306. directive_info['t-groups'] = repr(attrib['groups'])
  307. elif directive == 'att':
  308. for key in attrib:
  309. if key.startswith('t-att-') or key.startswith('t-attf-'):
  310. directive_info[key] = repr(attrib[key])
  311. elif directive == 'options':
  312. for key in attrib:
  313. if key.startswith('t-options-'):
  314. directive_info[key] = repr(attrib[key])
  315. elif ('t-' + directive) not in attrib:
  316. directive_info['t-' + directive] = None
  317. execution_context = tools.profiler.ExecutionContext(**directive_info, xpath=xpath)
  318. execution_context.__enter__()
  319. self.context_stack.append(execution_context)
  320. for hook in self.qweb_hooks:
  321. hook('enter', self.cr.sql_log_count, view_id=self.view_id, xpath=xpath, directive=directive, attrib=attrib)
  322. def leave_directive(self, directive, attrib, xpath):
  323. if self.execution_context_enabled:
  324. self.context_stack.pop().__exit__()
  325. for hook in self.qweb_hooks:
  326. hook('leave', self.cr.sql_log_count, view_id=self.view_id, xpath=xpath, directive=directive, attrib=attrib)
  327. class QwebCollector(Collector):
  328. """
  329. Record qweb execution with directive trace.
  330. """
  331. name = 'qweb'
  332. def __init__(self):
  333. super().__init__()
  334. self.events = []
  335. def hook(event, sql_log_count, **kwargs):
  336. self.events.append((event, kwargs, sql_log_count, real_time()))
  337. self.hook = hook
  338. def _get_directive_profiling_name(self, directive, attrib):
  339. expr = ''
  340. if directive == 'set':
  341. if 't-set' in attrib:
  342. expr = f"t-set={repr(attrib['t-set'])}"
  343. if 't-value' in attrib:
  344. expr += f" t-value={repr(attrib['t-value'])}"
  345. if 't-valuef' in attrib:
  346. expr += f" t-valuef={repr(attrib['t-valuef'])}"
  347. for key in attrib:
  348. if key.startswith('t-set-') or key.startswith('t-setf-'):
  349. if expr:
  350. expr += ' '
  351. expr += f"{key}={repr(attrib[key])}"
  352. elif directive == 'foreach':
  353. expr = f"t-foreach={repr(attrib['t-foreach'])} t-as={repr(attrib['t-as'])}"
  354. elif directive == 'options':
  355. if attrib.get('t-options'):
  356. expr = f"t-options={repr(attrib['t-options'])}"
  357. for key in attrib:
  358. if key.startswith('t-options-'):
  359. expr = f"{expr} {key}={repr(attrib[key])}"
  360. elif directive == 'att':
  361. for key in attrib:
  362. if key == 't-att' or key.startswith('t-att-') or key.startswith('t-attf-'):
  363. if expr:
  364. expr += ' '
  365. expr += f"{key}={repr(attrib[key])}"
  366. elif ('t-' + directive) in attrib:
  367. expr = f"t-{directive}={repr(attrib['t-' + directive])}"
  368. else:
  369. expr = f"t-{directive}"
  370. return expr
  371. def start(self):
  372. init_thread = self.profiler.init_thread
  373. if not hasattr(init_thread, 'qweb_hooks'):
  374. init_thread.qweb_hooks = []
  375. init_thread.qweb_hooks.append(self.hook)
  376. def stop(self):
  377. self.profiler.init_thread.qweb_hooks.remove(self.hook)
  378. def post_process(self):
  379. last_event_query = None
  380. last_event_time = None
  381. stack = []
  382. results = []
  383. archs = {}
  384. for event, kwargs, sql_count, time in self.events:
  385. if event == 'render':
  386. archs[kwargs['view_id']] = kwargs['arch']
  387. continue
  388. # update the active directive with the elapsed time and queries
  389. if stack:
  390. top = stack[-1]
  391. top['delay'] += time - last_event_time
  392. top['query'] += sql_count - last_event_query
  393. last_event_time = time
  394. last_event_query = sql_count
  395. directive = self._get_directive_profiling_name(kwargs['directive'], kwargs['attrib'])
  396. if directive:
  397. if event == 'enter':
  398. data = {
  399. 'view_id': kwargs['view_id'],
  400. 'xpath': kwargs['xpath'],
  401. 'directive': directive,
  402. 'delay': 0,
  403. 'query': 0,
  404. }
  405. results.append(data)
  406. stack.append(data)
  407. else:
  408. assert event == "leave"
  409. data = stack.pop()
  410. self.add({'results': {'archs': archs, 'data': results}})
  411. super().post_process()
  412. class ExecutionContext:
  413. """
  414. Add some context on thread at current call stack level.
  415. This context stored by collector beside stack and is used by Speedscope
  416. to add a level to the stack with this information.
  417. """
  418. def __init__(self, **context):
  419. self.context = context
  420. self.previous_context = None
  421. def __enter__(self):
  422. current_thread = threading.current_thread()
  423. self.previous_context = getattr(current_thread, 'exec_context', ())
  424. current_thread.exec_context = self.previous_context + ((stack_size(), self.context),)
  425. def __exit__(self, *_args):
  426. threading.current_thread().exec_context = self.previous_context
  427. class Profiler:
  428. """
  429. Context manager to use to start the recording of some execution.
  430. Will save sql and async stack trace by default.
  431. """
  432. def __init__(self, collectors=None, db=..., profile_session=None,
  433. description=None, disable_gc=False, params=None, log=False):
  434. """
  435. :param db: database name to use to save results.
  436. Will try to define database automatically by default.
  437. Use value ``None`` to not save results in a database.
  438. :param collectors: list of string and Collector object Ex: ['sql', PeriodicCollector(interval=0.2)]. Use `None` for default collectors
  439. :param profile_session: session description to use to reproup multiple profile. use make_session(name) for default format.
  440. :param description: description of the current profiler Suggestion: (route name/test method/loading module, ...)
  441. :param disable_gc: flag to disable gc durring profiling (usefull to avoid gc while profiling, especially during sql execution)
  442. :param params: parameters usable by collectors (like frame interval)
  443. """
  444. self.start_time = 0
  445. self.duration = 0
  446. self.profile_session = profile_session or make_session()
  447. self.description = description
  448. self.init_frame = None
  449. self.init_stack_trace = None
  450. self.init_thread = None
  451. self.disable_gc = disable_gc
  452. self.filecache = {}
  453. self.params = params or {} # custom parameters usable by collectors
  454. self.profile_id = None
  455. self.log = log
  456. self.sub_profilers = []
  457. if db is ...:
  458. # determine database from current thread
  459. db = getattr(threading.current_thread(), 'dbname', None)
  460. if not db:
  461. # only raise if path is not given and db is not explicitely disabled
  462. raise Exception('Database name cannot be defined automaticaly. \n Please provide a valid/falsy dbname or path parameter')
  463. self.db = db
  464. # collectors
  465. if collectors is None:
  466. collectors = ['sql', 'traces_async']
  467. self.collectors = []
  468. for collector in collectors:
  469. if isinstance(collector, str):
  470. try:
  471. collector = Collector.make(collector)
  472. except Exception:
  473. _logger.error("Could not create collector with name %r", collector)
  474. continue
  475. collector.profiler = self
  476. self.collectors.append(collector)
  477. def __enter__(self):
  478. self.init_thread = threading.current_thread()
  479. try:
  480. self.init_frame = get_current_frame(self.init_thread)
  481. self.init_stack_trace = _get_stack_trace(self.init_frame)
  482. except KeyError:
  483. # when using thread pools (gevent) the thread won't exist in the current_frames
  484. # this case is managed by http.py but will still fail when adding a profiler
  485. # inside a piece of code that may be called by a longpolling route.
  486. # in this case, avoid crashing the caller and disable all collectors
  487. self.init_frame = self.init_stack_trace = self.collectors = []
  488. self.db = self.params = None
  489. message = "Cannot start profiler, thread not found. Is the thread part of a thread pool?"
  490. if not self.description:
  491. self.description = message
  492. _logger.warning(message)
  493. if self.description is None:
  494. frame = self.init_frame
  495. code = frame.f_code
  496. self.description = f"{frame.f_code.co_name} ({code.co_filename}:{frame.f_lineno})"
  497. if self.params:
  498. self.init_thread.profiler_params = self.params
  499. if self.disable_gc and gc.isenabled():
  500. gc.disable()
  501. self.start_time = real_time()
  502. for collector in self.collectors:
  503. collector.start()
  504. return self
  505. def __exit__(self, *args):
  506. try:
  507. for collector in self.collectors:
  508. collector.stop()
  509. self.duration = real_time() - self.start_time
  510. self._add_file_lines(self.init_stack_trace)
  511. if self.db:
  512. # pylint: disable=import-outside-toplevel
  513. from odoo.sql_db import db_connect # only import from odoo if/when needed.
  514. with db_connect(self.db).cursor() as cr:
  515. values = {
  516. "name": self.description,
  517. "session": self.profile_session,
  518. "create_date": real_datetime_now(),
  519. "init_stack_trace": json.dumps(_format_stack(self.init_stack_trace)),
  520. "duration": self.duration,
  521. "entry_count": self.entry_count(),
  522. "sql_count": sum(len(collector.entries) for collector in self.collectors if collector.name == 'sql')
  523. }
  524. for collector in self.collectors:
  525. if collector.entries:
  526. values[collector.name] = json.dumps(collector.entries)
  527. query = SQL(
  528. "INSERT INTO ir_profile(%s) VALUES %s RETURNING id",
  529. SQL(",").join(map(SQL.identifier, values)),
  530. tuple(values.values()),
  531. )
  532. cr.execute(query)
  533. self.profile_id = cr.fetchone()[0]
  534. _logger.info('ir_profile %s (%s) created', self.profile_id, self.profile_session)
  535. except OperationalError:
  536. _logger.exception("Could not save profile in database")
  537. finally:
  538. if self.disable_gc:
  539. gc.enable()
  540. if self.params:
  541. del self.init_thread.profiler_params
  542. if self.log:
  543. _logger.info(self.summary())
  544. def _add_file_lines(self, stack):
  545. for index, frame in enumerate(stack):
  546. (filename, lineno, name, line) = frame
  547. if line != '':
  548. continue
  549. # retrieve file lines from the filecache
  550. if not lineno:
  551. continue
  552. try:
  553. filelines = self.filecache[filename]
  554. except KeyError:
  555. try:
  556. with tools.file_open(filename, filter_ext=('.py',)) as f:
  557. filelines = f.readlines()
  558. except (ValueError, FileNotFoundError): # mainly for <decorator> "filename"
  559. filelines = None
  560. self.filecache[filename] = filelines
  561. # fill in the line
  562. if filelines is not None:
  563. line = filelines[lineno - 1]
  564. stack[index] = (filename, lineno, name, line)
  565. def entry_count(self):
  566. """ Return the total number of entries collected in this profiler. """
  567. return sum(len(collector.entries) for collector in self.collectors)
  568. def format_path(self, path):
  569. """
  570. Utility function to format a path for this profiler.
  571. This is mainly useful to uniquify a path between executions.
  572. """
  573. return path.format(
  574. time=real_datetime_now().strftime("%Y%m%d-%H%M%S"),
  575. len=self.entry_count(),
  576. desc=re.sub("[^0-9a-zA-Z-]+", "_", self.description)
  577. )
  578. def json(self):
  579. """
  580. Utility function to generate a json version of this profiler.
  581. This is useful to write profiling entries into a file, such as::
  582. with Profiler(db=None) as profiler:
  583. do_stuff()
  584. filename = p.format_path('/home/foo/{desc}_{len}.json')
  585. with open(filename, 'w') as f:
  586. f.write(profiler.json())
  587. """
  588. return json.dumps({
  589. "name": self.description,
  590. "session": self.profile_session,
  591. "create_date": real_datetime_now().strftime("%Y%m%d-%H%M%S"),
  592. "init_stack_trace": _format_stack(self.init_stack_trace),
  593. "duration": self.duration,
  594. "collectors": {collector.name: collector.entries for collector in self.collectors},
  595. }, indent=4)
  596. def summary(self):
  597. result = ''
  598. for profiler in [self, *self.sub_profilers]:
  599. for collector in profiler.collectors:
  600. result += f'\n{self.description}\n{collector.summary()}'
  601. return result
  602. class Nested:
  603. """
  604. Utility to nest another context manager inside a profiler.
  605. The profiler should only be called directly in the "with" without nesting it
  606. with ExitStack. If not, the retrieval of the 'init_frame' may be incorrect
  607. and lead to an error "Limit frame was not found" when profiling. Since the
  608. stack will ignore all stack frames inside this file, the nested frames will
  609. be ignored, too. This is also why Nested() does not use
  610. contextlib.contextmanager.
  611. """
  612. def __init__(self, profiler, context_manager):
  613. self.profiler = profiler
  614. self.context_manager = context_manager
  615. def __enter__(self):
  616. self.profiler.__enter__()
  617. return self.context_manager.__enter__()
  618. def __exit__(self, exc_type, exc_value, traceback):
  619. try:
  620. return self.context_manager.__exit__(exc_type, exc_value, traceback)
  621. finally:
  622. self.profiler.__exit__(exc_type, exc_value, traceback)
上海开阖软件有限公司 沪ICP备12045867号-1