aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/022-tpcc-grouped/owner-samples.py
blob: a5b3a7b1f44ec32efc3f2118a9631b1f925ce2b0 (plain)
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]))