215 lines
8.0 KiB
Python
215 lines
8.0 KiB
Python
|
# -*- 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
|