blob: ffa9fd996155ca4323b48212683452b264b1a2cd [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import argparse
import json
import multiprocessing
import statistics
import threading
import time
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from multiprocessing.connection import Connection
class _DelayedHandler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
time.sleep(self.server.response_delay_seconds)
body = b"benchmark response"
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format_: str, *args: object) -> None:
pass
def _serve_delayed_http(port_sender: Connection, delay_seconds: float) -> None:
server = ThreadingHTTPServer(("127.0.0.1", 0), _DelayedHandler)
server.response_delay_seconds = delay_seconds
port_sender.send(server.server_address[1])
port_sender.close()
server.serve_forever(poll_interval=0.01)
def _measure(operation: Callable[[], object], iterations: int) -> list[float]:
samples = []
for _ in range(iterations):
started_at = time.perf_counter()
operation()
samples.append((time.perf_counter() - started_at) * 1_000)
return samples
def _summarize(samples: list[float]) -> dict[str, float]:
sorted_samples = sorted(samples)
p95_index = max(0, (len(sorted_samples) * 95 + 99) // 100 - 1)
return {
"median_ms": round(statistics.median(sorted_samples), 3),
"p95_ms": round(sorted_samples[p95_index], 3),
}
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Benchmark blocking Operator GIL release with local delayed HTTP I/O."
)
)
parser.add_argument("--delay-ms", type=float, default=50.0)
parser.add_argument("--iterations", type=int, default=30)
parser.add_argument("--warmups", type=int, default=3)
return parser.parse_args()
def main() -> None:
import opendal
args = _parse_args()
if args.delay_ms < 0:
message = "--delay-ms must be non-negative"
raise ValueError(message)
if args.iterations < 1:
message = "--iterations must be positive"
raise ValueError(message)
if args.warmups < 0:
message = "--warmups must be non-negative"
raise ValueError(message)
context = multiprocessing.get_context("spawn")
port_receiver, port_sender = context.Pipe(duplex=False)
server_process = context.Process(
target=_serve_delayed_http,
args=(port_sender, args.delay_ms / 1_000),
)
server_process.start()
port_sender.close()
try:
if not port_receiver.poll(10):
message = "delayed HTTP benchmark server did not start"
raise RuntimeError(message)
port = port_receiver.recv()
operator = opendal.Operator("http", endpoint=f"http://127.0.0.1:{port}")
for _ in range(args.warmups):
operator.read("warmup")
single_read = _measure(lambda: operator.read("single"), args.iterations)
sequential_pair = _measure(
lambda: (operator.read("first"), operator.read("second")),
args.iterations,
)
with ThreadPoolExecutor(max_workers=2) as executor:
def concurrent_pair() -> list[bytes]:
start = threading.Barrier(3)
def read(path: str) -> bytes:
start.wait()
return operator.read(path)
futures = [
executor.submit(read, "first"),
executor.submit(read, "second"),
]
start.wait()
return [future.result() for future in futures]
concurrent_pair_samples = _measure(concurrent_pair, args.iterations)
sequential_median = statistics.median(sequential_pair)
concurrent_median = statistics.median(concurrent_pair_samples)
result = {
"delay_ms": args.delay_ms,
"iterations": args.iterations,
"single_read": _summarize(single_read),
"sequential_two_reads": _summarize(sequential_pair),
"concurrent_two_reads": _summarize(concurrent_pair_samples),
"concurrency_speedup": round(sequential_median / concurrent_median, 3),
}
print(json.dumps(result, indent=2, sort_keys=True))
finally:
server_process.terminate()
server_process.join(10)
port_receiver.close()
if __name__ == "__main__":
main()