1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
#!/usr/bin/env python3
"""Where pure-serve's owner thread is: its stack sampled with gdb (a poor man's profiler; perf is not on this Pi).
owner-samples.py SAMPLES OUT.txt sample, then sort
owner-samples.py --sort STACKS.txt sort stacks sampled before
Waits for a process with a pure-owner thread (pure-serve, whatever its file
is named), then attaches gdb SAMPLES times, a little apart, and writes the
owner's stack each time as it goes (while the process lasts). Sorts the
samples by what the owner was doing (docs/research/106), and counts the
allocator's frames and what called them.
"""
import collections
import re
import subprocess
import sys
import time
from pathlib import Path
def owner():
for proc in Path('/proc').iterdir():
if not proc.name.isdigit():
continue
try:
for task in (proc / 'task').iterdir():
if (task / 'comm').read_text().strip() == 'pure-owner':
return int(proc.name), int(task.name)
except OSError:
continue
return None
def sample(samples, out):
found = None
while not found:
found = owner()
time.sleep(0.2)
pid, tid = found
time.sleep(10) # past loading, into the run
with out.open('w') as written:
for _ in range(samples):
if not Path(f'/proc/{pid}').exists():
break
text = subprocess.run(['gdb', '-batch', '-nx', '-p', str(pid), '-ex', 'set pagination off',
'-ex', 'thread apply all bt 40'], capture_output=True, text=True).stdout
# The owner's section: from its "Thread ... (LWP tid)" header to the next thread's.
match = re.search(rf'Thread [^\n]*\(LWP {tid}\)[^\n]*\n(.*?)(?=\nThread |\Z)', text, re.S)
if match:
frames = re.findall(r'^#\d+\s+(?:0x[0-9a-f]+ in )?(\S+)', match.group(1), re.M)
written.write(' < '.join(frames) + '\n')
written.flush()
time.sleep(0.3)
def kind(frames):
names = set(frames)
if 'apply_effect' in names:
if names & {'own', 'new_node', 'retire'}:
return 'applying: copying nodes a snapshot shares (own, new_node, retire)'
if names & {'decode_item', 'decode_cell'}:
return 'applying: reading items'
if 'pure_list_seek' in names or names & {'leaf_of', 'child_at', 'child', 'cell_at'}:
return 'applying: finding items (pure_list_seek, descending)'
if names & {'pure_list_insert', 'pure_list_set', 'pure_list_remove'}:
return 'applying: changing items'
return 'applying: the interpreter (run, dispatch)'
if 'publish' in names:
return 'publishing a snapshot, letting the old go'
if 'save_burst' in names:
return 'saving'
if 'hand_back' in names:
return 'handing answers back'
if names & {'pthread_cond_wait', 'pthread_cond_timedwait'}:
return 'waiting for requests'
return 'other'
def sort(path):
stacks = [line.split(' < ') for line in path.read_text().splitlines() if line]
kinds, allocations = collections.Counter(), collections.Counter()
for frames in stacks:
kinds[kind(frames)] += 1
for at, name in enumerate(frames):
if name in ('malloc', 'calloc', 'realloc', 'free'):
allocations[f'{name} < {frames[at + 1] if at + 1 < len(frames) else "?"}'] += 1
break
busy = len(stacks) - kinds['waiting for requests']
print(f'{len(stacks)} samples of the owner, {busy} busy')
for name, n in kinds.most_common():
print(f' {n:>3} {n / len(stacks):>4.0%} {name}')
print('the allocator (innermost call, and its caller):')
for name, n in allocations.most_common():
print(f' {n:>3} {name}')
if sys.argv[1] == '--sort':
sort(Path(sys.argv[2]))
else:
sample(int(sys.argv[1]), Path(sys.argv[2]))
sort(Path(sys.argv[2]))
|