From 987d4d7aa08624e28d0ea18a1e66e359a0230c80 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Wed, 30 Sep 2026 08:35:40 +0100 Subject: Find pure-serve's owner by its thread, and sort its samples as 106 does --- .../experiments/022-tpcc-grouped/owner-samples.py | 119 +++++++++++++-------- 1 file 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])) -- cgit v1.2.3