aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/connections/check_idle_wakes.py
blob: 4f1e4663256af9efc529af7488808ac83317308f (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
#!/usr/bin/env python3
"""Count real idle poll calls before/after deadline-driven HTTP sleeping."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import select
import signal
import subprocess
import tarfile
import tempfile
import time


def observe(binary, seconds):
    with tempfile.TemporaryDirectory(prefix='pure-idle-poll-') as name:
        root = Path(name)
        trace = root / 'poll.log'
        tracer = subprocess.Popen(['strace', '-qq', '-e', 'trace=poll,ppoll', '-o',
                                   str(trace), str(binary), '--store',
                                   str(root / 'store'), '--port', '0'],
                                  stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                  start_new_session=True)
        try:
            ready, _, _ = select.select([tracer.stdout], [], [], 10)
            assert ready, 'HTTP host did not become ready under strace'
            message = json.loads(tracer.stdout.readline())
            assert message['port'] > 0
            children = Path(f'/proc/{tracer.pid}/task/{tracer.pid}/children')
            pids = children.read_text().split()
            assert len(pids) == 1, pids
            time.sleep(seconds)
            os.kill(int(pids[0]), signal.SIGTERM)
            _, error = tracer.communicate(timeout=10)
            assert tracer.returncode == 0, (tracer.returncode, error[-1000:])
            captured = trace.read_text()
            calls = [line for line in captured.splitlines()
                     if line.startswith(('poll(', 'ppoll('))]
            values = []
            for line in calls:
                if line.startswith('ppoll('):
                    if re.search(r'\],\s*\d+,\s*NULL,', line):
                        values.append(-1)
                    elif match := re.search(r'\{tv_sec=(\d+), tv_nsec=(\d+)\}', line):
                        values.append(int(match.group(1)) * 1000 +
                                      int(match.group(2)) // 1_000_000)
                elif match := re.search(r',\s*(-?\d+)\)\s+=', line):
                    values.append(int(match.group(1)))
            return {'poll_calls': len(calls), 'timeout_values_ms': sorted(set(values)),
                    'trace_excerpt': captured[:500],
                    'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest()}
        finally:
            if tracer.poll() is None:
                os.killpg(tracer.pid, signal.SIGTERM)
                try:
                    tracer.communicate(timeout=5)
                except subprocess.TimeoutExpired:
                    os.killpg(tracer.pid, signal.SIGKILL)
                    tracer.communicate(timeout=5)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--before', type=Path, required=True)
    parser.add_argument('--after', type=Path, required=True)
    parser.add_argument('--output', type=Path)
    args = parser.parse_args()
    seconds = 1.25
    with tempfile.TemporaryDirectory(prefix='pure-idle-before-') as directory:
        baseline = args.before.resolve()
        if baseline.name.endswith('.tar.gz'):
            with tarfile.open(baseline, 'r:gz') as archive:
                body = archive.extractfile('pure-document/bin/pure-connections-fixed').read()
            baseline = Path(directory) / 'pure-connections-fixed'
            baseline.write_bytes(body)
            baseline.chmod(0o755)
        before = observe(baseline, seconds)
    after = observe(args.after.resolve(), seconds)
    assert before['poll_calls'] >= 10, before
    assert after['poll_calls'] <= 2, after
    assert -1 in after['timeout_values_ms'], after
    result = {'profile': 'idle-http-host-poll-syscalls',
              'observation_seconds': seconds, 'before': before, 'after': after,
              'deadline_driven_idle_wait': 'pass'}
    body = json.dumps(result, indent=2) + '\n'
    if args.output:
        args.output.parent.mkdir(parents=True, exist_ok=True)
        args.output.write_text(body)
    print(body, end='')


if __name__ == '__main__':
    main()