| 29 | |
| 30 | |
| 31 | class CollapsedStackCollector(StackTraceCollector): |
| 32 | def __init__(self, *args, **kwargs): |
| 33 | super().__init__(*args, **kwargs) |
| 34 | self.stack_counter = collections.Counter() |
| 35 | |
| 36 | def process_frames(self, frames, thread_id, weight=1): |
| 37 | # Extract only (filename, lineno, funcname) - opcode not needed for collapsed stacks |
| 38 | # frame is (filename, location, funcname, opcode) |
| 39 | call_tree = tuple( |
| 40 | (f[0], extract_lineno(f[1]), f[2]) for f in reversed(frames) |
| 41 | ) |
| 42 | self.stack_counter[(call_tree, thread_id)] += weight |
| 43 | |
| 44 | def export(self, filename): |
| 45 | lines = [] |
| 46 | for (call_tree, thread_id), count in self.stack_counter.items(): |
| 47 | parts = [f"tid:{thread_id}"] |
| 48 | for file, line, func in call_tree: |
| 49 | # This is what pstats does for "special" frames: |
| 50 | if file == "~" and line == 0: |
| 51 | part = func |
| 52 | else: |
| 53 | part = f"{os.path.basename(file)}:{func}:{line}" |
| 54 | parts.append(part) |
| 55 | stack_str = ";".join(parts) |
| 56 | lines.append((stack_str, count)) |
| 57 | |
| 58 | lines.sort(key=lambda x: (-x[1], x[0])) |
| 59 | |
| 60 | with open(filename, "w") as f: |
| 61 | for stack, count in lines: |
| 62 | f.write(f"{stack} {count}\n") |
| 63 | print(f"Collapsed stack output written to {filename}") |
| 64 | |
| 65 | |
| 66 | class FlamegraphCollector(StackTraceCollector): |
no outgoing calls
searching dependent graphs…