aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--research/experiments/022-tpcc-grouped/owner-samples.py119
1 files changed, 74 insertions, 45 deletions
diff --git a/research/experiments/022-tpcc-grouped/owner-samples.py b/research/experiments/022-tpcc-grouped/owner-samples.py
index 5415cf6..a5b3a7b 100644
--- a/research/experiments/022-tpcc-grouped/owner-samples.py
+++ b/research/experiments/022-tpcc-grouped/owner-samples.py
@@ -1,11 +1,14 @@
#!/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
+ owner-samples.py SAMPLES OUT.txt sample, then sort
+ owner-samples.py --sort STACKS.txt sort stacks sampled before
-Waits for a pure-serve with a pure-owner thread, then attaches gdb SAMPLES
-times, a little apart, and keeps the owner's stack each time. Prints how
-many samples had each serve.c phase on the stack, and the innermost frames.
+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
@@ -14,16 +17,12 @@ import sys
import time
from pathlib import Path
-samples, out = int(sys.argv[1]), Path(sys.argv[2])
-
def owner():
for proc in Path('/proc').iterdir():
if not proc.name.isdigit():
continue
try:
- if (proc / 'comm').read_text().strip() != 'pure-serve':
- continue
for task in (proc / 'task').iterdir():
if (task / 'comm').read_text().strip() == 'pure-owner':
return int(proc.name), int(task.name)
@@ -32,41 +31,71 @@ def owner():
return None
-found = None
-while not found:
- found = owner()
- time.sleep(0.2)
-pid, tid = found
-time.sleep(10) # past loading, into the run
-stacks = []
-for _ in range(samples):
- 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)
- stacks.append(frames)
- time.sleep(0.3)
-out.write_text('\n'.join(' < '.join(s) for s in stacks) + '\n')
-phases = collections.Counter()
-inner = collections.Counter()
-for frames in stacks:
+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)
- phase = ('waiting' if names & {'pthread_cond_wait', 'pthread_cond_timedwait', '__futex_abstimed_wait_common'}
- and not names & {'execute'} else
- 'publish' if 'publish' in names else
- 'checkpoint_round' if 'checkpoint_round' in names else
- 'feed_outlets' if 'feed_outlets' in names else
- 'save' if 'save_burst' in names else
- 'hand_back' if 'hand_back' in names else
- 'apply' if 'apply_effect' in names else
- 'other')
- phases[phase] += 1
- inner[frames[0] if frames else '?'] += 1
-print(f'{len(stacks)} samples of the owner (pid {pid}, thread {tid})')
-for phase, n in phases.most_common():
- print(f' {phase:<18} {n:>4} {n / max(len(stacks), 1):.0%}')
-print('innermost frames:')
-for name, n in inner.most_common(15):
- print(f' {name:<40} {n:>4}')
+ 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]))