|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import sys |
| 4 | +from pathlib import Path |
| 5 | +from tempfile import TemporaryDirectory |
| 6 | +from time import time_ns |
| 7 | + |
| 8 | +from cantok import ConditionToken, SimpleToken |
| 9 | +from microbenchmark import Scenario, a |
| 10 | + |
| 11 | +from suby import run |
| 12 | + |
| 13 | +ITERATIONS = 100 |
| 14 | +SHORT_ITERATIONS = 20 |
| 15 | +PYTHON = Path(sys.executable) |
| 16 | + |
| 17 | + |
| 18 | +def run_with_delayed_condition_token_cancellation() -> None: |
| 19 | + with TemporaryDirectory() as temporary_directory: |
| 20 | + marker_file = Path(temporary_directory) / 'subprocess-started' |
| 21 | + subprocess_started_at_ns = None |
| 22 | + |
| 23 | + def should_cancel() -> bool: |
| 24 | + nonlocal subprocess_started_at_ns |
| 25 | + |
| 26 | + if not marker_file.exists(): |
| 27 | + return False |
| 28 | + if subprocess_started_at_ns is None: |
| 29 | + subprocess_started_at_ns = marker_file.stat().st_mtime_ns |
| 30 | + return time_ns() - subprocess_started_at_ns >= 10_000_000 |
| 31 | + |
| 32 | + run( |
| 33 | + PYTHON, |
| 34 | + '-c', |
| 35 | + ( |
| 36 | + 'import sys\n' |
| 37 | + 'import time\n' |
| 38 | + 'from pathlib import Path\n' |
| 39 | + 'Path(sys.argv[1]).touch()\n' |
| 40 | + 'time.sleep(1)' |
| 41 | + ), |
| 42 | + marker_file, |
| 43 | + split=False, |
| 44 | + token=ConditionToken(should_cancel), |
| 45 | + catch_exceptions=True, |
| 46 | + catch_output=True, |
| 47 | + ) |
| 48 | + |
| 49 | + |
| 50 | +simple_success = Scenario( |
| 51 | + run, |
| 52 | + a(PYTHON, '-c', 'pass'), |
| 53 | + name='simple_success', |
| 54 | + doc='Runs a minimal successful Python subprocess.', |
| 55 | + number=ITERATIONS, |
| 56 | +) |
| 57 | + |
| 58 | +python_version_output = Scenario( |
| 59 | + run, |
| 60 | + a(PYTHON, '-VV', catch_output=True), |
| 61 | + name='python_version_output', |
| 62 | + doc='Runs the current Python executable as a pathlib.Path and prints its detailed version.', |
| 63 | + number=ITERATIONS, |
| 64 | +) |
| 65 | + |
| 66 | +string_executable = Scenario( |
| 67 | + run, |
| 68 | + a(sys.executable, '-c', 'pass'), |
| 69 | + name='string_executable', |
| 70 | + doc='Runs a minimal command where the executable is supplied as a string.', |
| 71 | + number=ITERATIONS, |
| 72 | +) |
| 73 | + |
| 74 | +path_argument = Scenario( |
| 75 | + run, |
| 76 | + a(PYTHON, '-c "import sys; print(sys.argv[1])"', Path(__file__), catch_output=True), |
| 77 | + name='path_argument', |
| 78 | + doc='Runs a command with a pathlib.Path supplied as one of the subprocess arguments.', |
| 79 | + number=ITERATIONS, |
| 80 | +) |
| 81 | + |
| 82 | +multi_line_stdout = Scenario( |
| 83 | + run, |
| 84 | + a(PYTHON, '-c "for i in range(10): print(i)"', catch_output=True), |
| 85 | + name='multi_line_stdout', |
| 86 | + doc='Runs a successful command that writes several short stdout lines.', |
| 87 | + number=ITERATIONS, |
| 88 | +) |
| 89 | + |
| 90 | +large_stdout = Scenario( |
| 91 | + run, |
| 92 | + a(PYTHON, '-c "print(\'x\' * 10000)"', catch_output=True), |
| 93 | + name='large_stdout', |
| 94 | + doc='Runs a successful command that writes one larger stdout payload.', |
| 95 | + number=ITERATIONS, |
| 96 | +) |
| 97 | + |
| 98 | +stderr_output = Scenario( |
| 99 | + run, |
| 100 | + a(PYTHON, '-c "import sys; sys.stderr.write(\'error line\\\\n\')"', catch_output=True), |
| 101 | + name='stderr_output', |
| 102 | + doc='Runs a successful command that writes to stderr.', |
| 103 | + number=ITERATIONS, |
| 104 | +) |
| 105 | + |
| 106 | +mixed_stdout_stderr = Scenario( |
| 107 | + run, |
| 108 | + a(PYTHON, '-c "import sys; print(\'out\'); sys.stderr.write(\'err\\\\n\')"', catch_output=True), |
| 109 | + name='mixed_stdout_stderr', |
| 110 | + doc='Runs a successful command that writes to both stdout and stderr.', |
| 111 | + number=ITERATIONS, |
| 112 | +) |
| 113 | + |
| 114 | +many_short_lines = Scenario( |
| 115 | + run, |
| 116 | + a(PYTHON, '-c "for i in range(1000): print(i)"', catch_output=True), |
| 117 | + name='many_short_lines', |
| 118 | + doc='Runs a command that emits many small stdout lines for stream-reading overhead.', |
| 119 | + number=ITERATIONS, |
| 120 | +) |
| 121 | + |
| 122 | +moderate_python_work = Scenario( |
| 123 | + run, |
| 124 | + a(PYTHON, '-c "sum(range(100000))"'), |
| 125 | + name='moderate_python_work', |
| 126 | + doc='Runs a subprocess that performs a small amount of CPU work before exiting.', |
| 127 | + number=ITERATIONS, |
| 128 | +) |
| 129 | + |
| 130 | +short_sleep = Scenario( |
| 131 | + run, |
| 132 | + a(PYTHON, '-c "import time; time.sleep(0.01)"'), |
| 133 | + name='short_sleep', |
| 134 | + doc='Runs a subprocess that stays alive briefly without producing output.', |
| 135 | + number=SHORT_ITERATIONS, |
| 136 | +) |
| 137 | + |
| 138 | +simple_token_success = Scenario( |
| 139 | + run, |
| 140 | + a(PYTHON, '-c', 'pass', token=SimpleToken()), |
| 141 | + name='simple_token_success', |
| 142 | + doc='Runs a minimal subprocess while checking a non-cancelled SimpleToken.', |
| 143 | + number=ITERATIONS, |
| 144 | +) |
| 145 | + |
| 146 | +condition_token_success = Scenario( |
| 147 | + run, |
| 148 | + a(PYTHON, '-c', 'pass', token=ConditionToken(lambda: False)), |
| 149 | + name='condition_token_success', |
| 150 | + doc='Runs a minimal subprocess while polling a ConditionToken that remains active.', |
| 151 | + number=ITERATIONS, |
| 152 | +) |
| 153 | + |
| 154 | +cancelled_token_before_start = Scenario( |
| 155 | + run, |
| 156 | + a( |
| 157 | + PYTHON, |
| 158 | + '-c "import time; time.sleep(1)"', |
| 159 | + token=SimpleToken().cancel(), |
| 160 | + catch_exceptions=True, |
| 161 | + catch_output=True, |
| 162 | + ), |
| 163 | + name='cancelled_token_before_start', |
| 164 | + doc='Runs a subprocess with an already-cancelled token and catches the cancellation result.', |
| 165 | + number=SHORT_ITERATIONS, |
| 166 | +) |
| 167 | + |
| 168 | +condition_token_cancel_after_start = Scenario( |
| 169 | + run_with_delayed_condition_token_cancellation, |
| 170 | + name='condition_token_cancel_after_start', |
| 171 | + doc='Starts a subprocess and cancels it with a ConditionToken shortly after the subprocess reports startup.', |
| 172 | + number=SHORT_ITERATIONS, |
| 173 | +) |
| 174 | + |
| 175 | +all = ( # noqa: A001 |
| 176 | + simple_success |
| 177 | + + python_version_output |
| 178 | + + string_executable |
| 179 | + + path_argument |
| 180 | + + multi_line_stdout |
| 181 | + + large_stdout |
| 182 | + + stderr_output |
| 183 | + + mixed_stdout_stderr |
| 184 | + + many_short_lines |
| 185 | + + moderate_python_work |
| 186 | + + short_sleep |
| 187 | + + simple_token_success |
| 188 | + + condition_token_success |
| 189 | + + cancelled_token_before_start |
| 190 | + + condition_token_cancel_after_start |
| 191 | +) |
0 commit comments