|
- # -*- coding: utf-8 -*-
- # Part of Odoo. See LICENSE file for full copyright and licensing details.
- import reprlib
-
- shortener = reprlib.Repr()
- shortener.maxstring = 150
- shorten = shortener.repr
-
-
- class Speedscope:
- def __init__(self, name='Speedscope', init_stack_trace=None):
- self.init_stack_trace = init_stack_trace or []
- self.init_stack_trace_level = len(self.init_stack_trace)
- self.caller_frame = None
- self.convert_stack(self.init_stack_trace)
-
- self.init_caller_frame = None
- if self.init_stack_trace:
- self.init_caller_frame = self.init_stack_trace[-1]
- self.profiles_raw = {}
- self.name = name
- self.frames_indexes = {}
- self.frame_count = 0
- self.profiles = []
-
- def add(self, key, profile):
- for entry in profile:
- self.caller_frame = self.init_caller_frame
- self.convert_stack(entry['stack'] or [])
- if 'query' in entry:
- query = entry['query']
- full_query = entry['full_query']
- entry['stack'].append((f'sql({shorten(query)})', full_query, None))
- self.profiles_raw[key] = profile
-
- def convert_stack(self, stack):
- for index, frame in enumerate(stack):
- method = frame[2]
- line = ''
- number = ''
- if self.caller_frame and len(self.caller_frame) == 4:
- line = f"called at {self.caller_frame[0]} ({self.caller_frame[3].strip()})"
- number = self.caller_frame[1]
- stack[index] = (method, line, number,)
- self.caller_frame = frame
-
- def add_output(self, names, complete=True, display_name=None, use_context=True, **params):
- entries = []
- display_name = display_name or ','.join(names)
- for name in names:
- entries += self.profiles_raw[name]
- entries.sort(key=lambda e: e['start'])
- result = self.process(entries, use_context=use_context, **params)
- if not result:
- return self
- start = result[0]['at']
- end = result[-1]['at']
-
- if complete:
- start_stack = []
- end_stack = []
- init_stack_trace_ids = self.stack_to_ids(self.init_stack_trace, use_context and entries[0].get('exec_context'))
- for frame_id in init_stack_trace_ids:
- start_stack.append({
- "type": "O",
- "frame": frame_id,
- "at": start
- })
- for frame_id in reversed(init_stack_trace_ids):
- end_stack.append({
- "type": "C",
- "frame": frame_id,
- "at": end
- })
- result = start_stack + result + end_stack
-
- self.profiles.append({
- "name": display_name,
- "type": "evented",
- "unit": "seconds",
- "startValue": 0,
- "endValue": end - start,
- "events": result
- })
- return self
-
- def add_default(self):
- if len(self.profiles_raw) > 1:
- self.add_output(self.profiles_raw, display_name='Combined')
- self.add_output(self.profiles_raw, display_name='Combined no context', use_context=False)
- for key, profile in self.profiles_raw.items():
- sql = profile and profile[0].get('query')
- if sql:
- self.add_output([key], hide_gaps=True, display_name=f'{key} (no gap)')
- self.add_output([key], continuous=False, complete=False, display_name=f'{key} (density)')
-
- else:
- self.add_output([key], display_name=key)
- return self
-
- def make(self):
- if not self.profiles:
- self.add_default()
- return {
- "name": self.name,
- "activeProfileIndex": 0,
- "$schema": "https://www.speedscope.app/file-format-schema.json",
- "shared": {
- "frames": [{
- "name": frame[0],
- "file": frame[1],
- "line": frame[2]
- } for frame in self.frames_indexes]
- },
- "profiles": self.profiles,
- }
-
- def get_frame_id(self, frame):
- if frame not in self.frames_indexes:
- self.frames_indexes[frame] = self.frame_count
- self.frame_count += 1
- return self.frames_indexes[frame]
-
- def stack_to_ids(self, stack, context, stack_offset=0):
- """
- :param stack: A list of hashable frame
- :param context: an iterable of (level, value) ordered by level
- :param stack_offset: offset level for stack
-
- Assemble stack and context and return a list of ids representing
- this stack, adding each corresponding context at the corresponding
- level.
- """
- stack_ids = []
- context_iterator = iter(context or ())
- context_level, context_value = next(context_iterator, (None, None))
- # consume iterator until we are over stack_offset
- while context_level is not None and context_level < stack_offset:
- context_level, context_value = next(context_iterator, (None, None))
- for level, frame in enumerate(stack, start=stack_offset + 1):
- while context_level == level:
- context_frame = (", ".join(f"{k}={v}" for k, v in context_value.items()), '', '')
- stack_ids.append(self.get_frame_id(context_frame))
- context_level, context_value = next(context_iterator, (None, None))
- stack_ids.append(self.get_frame_id(frame))
- return stack_ids
-
- def process(self, entries, continuous=True, hide_gaps=False, use_context=True, constant_time=False):
- # constant_time parameters is mainly useful to hide temporality when focussing on sql determinism
- entry_end = previous_end = None
- if not entries:
- return []
- events = []
- current_stack_ids = []
- frames_start = entries[0]['start']
-
- # add last closing entry if missing
- last_entry = entries[-1]
- if last_entry['stack']:
- entries.append({'stack': [], 'start': last_entry['start'] + last_entry.get('time', 0)})
-
- for index, entry in enumerate(entries):
- if constant_time:
- entry_start = close_time = index
- else:
- previous_end = entry_end
-
- if hide_gaps and previous_end:
- entry_start = previous_end
- else:
- entry_start = entry['start'] - frames_start
-
- if previous_end and previous_end > entry_start:
- # skip entry if entry starts after another entry end
- continue
-
- if previous_end:
- close_time = min(entry_start, previous_end)
- else:
- close_time = entry_start
-
- entry_time = entry.get('time')
- entry_end = None if entry_time is None else entry_start + entry_time
-
- entry_stack_ids = self.stack_to_ids(
- entry['stack'] or [],
- use_context and entry.get('exec_context'),
- self.init_stack_trace_level
- )
- level = 0
- if continuous:
- level = -1
- for level, at_level in enumerate(zip(current_stack_ids, entry_stack_ids)):
- current, new = at_level
- if current != new:
- break
- else:
- level += 1
-
- for frame in reversed(current_stack_ids[level:]):
- events.append({
- "type": "C",
- "frame": frame,
- "at": close_time
- })
- for frame in entry_stack_ids[level:]:
- events.append({
- "type": "O",
- "frame": frame,
- "at": entry_start
- })
- current_stack_ids = entry_stack_ids
-
- return events
|