Skip to content
This repository was archived by the owner on Mar 31, 2026. It is now read-only.

Commit 09b1d9d

Browse files
authored
feat: add time based benchmarks (#1749)
feat: add time based benchmarks
1 parent ab62d72 commit 09b1d9d

6 files changed

Lines changed: 501 additions & 8 deletions

File tree

tests/perf/microbenchmarks/_utils.py

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,24 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
from typing import Any, List
14+
from typing import Any, List, Optional
1515
import statistics
1616
import io
1717
import os
18+
import socket
19+
import psutil
1820

21+
_C4_STANDARD_192_NIC = "ens3" # can be fetched via ip link show
1922

2023
def publish_benchmark_extra_info(
2124
benchmark: Any,
2225
params: Any,
2326
benchmark_group: str = "read",
2427
true_times: List[float] = [],
28+
download_bytes_list: Optional[List[int]] = None,
29+
duration: Optional[int] = None,
2530
) -> None:
31+
2632
"""
2733
Helper function to publish benchmark parameters to the extra_info property.
2834
"""
@@ -41,13 +47,23 @@ def publish_benchmark_extra_info(
4147
benchmark.extra_info["processes"] = params.num_processes
4248
benchmark.group = benchmark_group
4349

44-
object_size = params.file_size_bytes
45-
num_files = params.num_files
46-
total_uploaded_mib = object_size / (1024 * 1024) * num_files
47-
min_throughput = total_uploaded_mib / benchmark.stats["max"]
48-
max_throughput = total_uploaded_mib / benchmark.stats["min"]
49-
mean_throughput = total_uploaded_mib / benchmark.stats["mean"]
50-
median_throughput = total_uploaded_mib / benchmark.stats["median"]
50+
if download_bytes_list is not None:
51+
assert duration is not None, "Duration must be provided if total_bytes_transferred is provided."
52+
throughputs_list = [x / duration / (1024 * 1024) for x in download_bytes_list]
53+
min_throughput = min(throughputs_list)
54+
max_throughput = max(throughputs_list)
55+
mean_throughput = statistics.mean(throughputs_list)
56+
median_throughput = statistics.median(throughputs_list)
57+
58+
59+
else:
60+
object_size = params.file_size_bytes
61+
num_files = params.num_files
62+
total_uploaded_mib = object_size / (1024 * 1024) * num_files
63+
min_throughput = total_uploaded_mib / benchmark.stats["max"]
64+
max_throughput = total_uploaded_mib / benchmark.stats["min"]
65+
mean_throughput = total_uploaded_mib / benchmark.stats["mean"]
66+
median_throughput = total_uploaded_mib / benchmark.stats["median"]
5167

5268
benchmark.extra_info["throughput_MiB_s_min"] = min_throughput
5369
benchmark.extra_info["throughput_MiB_s_max"] = max_throughput
@@ -165,3 +181,74 @@ def seek(self, offset, whence=io.SEEK_SET):
165181
# Clamp position to valid range [0, size]
166182
self._pos = max(0, min(new_pos, self._size))
167183
return self._pos
184+
185+
186+
def get_nic_pci(nic):
187+
"""Gets the PCI address of a network interface."""
188+
return os.path.basename(os.readlink(f"/sys/class/net/{nic}/device"))
189+
190+
191+
def get_irqs_for_pci(pci):
192+
"""Gets the IRQs associated with a PCI address."""
193+
irqs = []
194+
with open("/proc/interrupts") as f:
195+
for line in f:
196+
if pci in line:
197+
irq = line.split(":")[0].strip()
198+
irqs.append(irq)
199+
return irqs
200+
201+
202+
def get_affinity(irq):
203+
"""Gets the CPU affinity of an IRQ."""
204+
path = f"/proc/irq/{irq}/smp_affinity_list"
205+
try:
206+
with open(path) as f:
207+
return f.read().strip()
208+
except FileNotFoundError:
209+
return "N/A"
210+
211+
212+
def get_primary_interface_name():
213+
primary_ip = None
214+
215+
# 1. Determine the Local IP used for internet access
216+
# We use UDP (SOCK_DGRAM) so we don't actually send a handshake/packet
217+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
218+
try:
219+
# connect() to a public IP (Google DNS) to force route resolution
220+
s.connect(('8.8.8.8', 80))
221+
primary_ip = s.getsockname()[0]
222+
except Exception:
223+
# Fallback if no internet
224+
return None
225+
finally:
226+
s.close()
227+
228+
# 2. Match that IP to an interface name using psutil
229+
if primary_ip:
230+
interfaces = psutil.net_if_addrs()
231+
for name, addresses in interfaces.items():
232+
for addr in addresses:
233+
# check if this interface has the IP we found
234+
if addr.address == primary_ip:
235+
return name
236+
return None
237+
238+
239+
def get_irq_affinity():
240+
"""Gets the set of CPUs for a given network interface."""
241+
nic = get_primary_interface_name()
242+
if not nic:
243+
nic = _C4_STANDARD_192_NIC
244+
245+
pci = get_nic_pci(nic)
246+
irqs = get_irqs_for_pci(pci)
247+
cpus = set()
248+
for irq in irqs:
249+
affinity_str = get_affinity(irq)
250+
if affinity_str != "N/A":
251+
for part in affinity_str.split(','):
252+
if '-' not in part:
253+
cpus.add(int(part))
254+
return cpus
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import pytest
15+
16+
17+
@pytest.fixture
18+
def workload_params(request):
19+
params = request.param
20+
files_names = [f'fio-go_storage_fio.0.{i}' for i in range(0, params.num_processes)]
21+
return params, files_names
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import itertools
15+
import os
16+
from typing import Dict, List
17+
18+
import yaml
19+
20+
try:
21+
from tests.perf.microbenchmarks.time_based.reads.parameters import (
22+
TimeBasedReadParameters,
23+
)
24+
except ModuleNotFoundError:
25+
from reads.parameters import TimeBasedReadParameters
26+
27+
28+
def _get_params() -> Dict[str, List[TimeBasedReadParameters]]:
29+
"""Generates a dictionary of benchmark parameters for time based read operations."""
30+
params: Dict[str, List[TimeBasedReadParameters]] = {}
31+
config_path = os.path.join(os.path.dirname(__file__), "config.yaml")
32+
with open(config_path, "r") as f:
33+
config = yaml.safe_load(f)
34+
35+
common_params = config["common"]
36+
bucket_types = common_params["bucket_types"]
37+
file_sizes_mib = common_params["file_sizes_mib"]
38+
chunk_sizes_kib = common_params["chunk_sizes_kib"]
39+
num_ranges = common_params["num_ranges"]
40+
rounds = common_params["rounds"]
41+
duration = common_params["duration"]
42+
warmup_duration = common_params["warmup_duration"]
43+
44+
bucket_map = {
45+
"zonal": os.environ.get(
46+
"DEFAULT_RAPID_ZONAL_BUCKET",
47+
config["defaults"]["DEFAULT_RAPID_ZONAL_BUCKET"],
48+
),
49+
"regional": os.environ.get(
50+
"DEFAULT_STANDARD_BUCKET", config["defaults"]["DEFAULT_STANDARD_BUCKET"]
51+
),
52+
}
53+
54+
for workload in config["workload"]:
55+
workload_name = workload["name"]
56+
params[workload_name] = []
57+
pattern = workload["pattern"]
58+
processes = workload["processes"]
59+
coros = workload["coros"]
60+
61+
# Create a product of all parameter combinations
62+
product = itertools.product(
63+
bucket_types,
64+
file_sizes_mib,
65+
chunk_sizes_kib,
66+
num_ranges,
67+
processes,
68+
coros,
69+
)
70+
71+
for (
72+
bucket_type,
73+
file_size_mib,
74+
chunk_size_kib,
75+
num_ranges_val,
76+
num_processes,
77+
num_coros,
78+
) in product:
79+
file_size_bytes = file_size_mib * 1024 * 1024
80+
chunk_size_bytes = chunk_size_kib * 1024
81+
bucket_name = bucket_map[bucket_type]
82+
83+
num_files = num_processes * num_coros
84+
85+
# Create a descriptive name for the parameter set
86+
name = f"{pattern}_{bucket_type}_{num_processes}p_{file_size_mib}MiB_{chunk_size_kib}KiB_{num_ranges_val}ranges"
87+
88+
params[workload_name].append(
89+
TimeBasedReadParameters(
90+
name=name,
91+
workload_name=workload_name,
92+
pattern=pattern,
93+
bucket_name=bucket_name,
94+
bucket_type=bucket_type,
95+
num_coros=num_coros,
96+
num_processes=num_processes,
97+
num_files=num_files,
98+
rounds=rounds,
99+
chunk_size_bytes=chunk_size_bytes,
100+
file_size_bytes=file_size_bytes,
101+
duration=duration,
102+
warmup_duration=warmup_duration,
103+
num_ranges=num_ranges_val,
104+
)
105+
)
106+
return params
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
common:
2+
bucket_types:
3+
- "regional"
4+
- "zonal"
5+
file_sizes_mib:
6+
- 10240 # 10GiB
7+
chunk_sizes_kib: [64] # 16KiB
8+
num_ranges: [1]
9+
rounds: 1
10+
duration: 30 # seconds
11+
warmup_duration: 5 # seconds
12+
13+
workload:
14+
############# multi process multi coroutine #########
15+
- name: "read_seq_multi_process"
16+
pattern: "seq"
17+
coros: [1]
18+
processes: [96]
19+
20+
21+
- name: "read_rand_multi_process"
22+
pattern: "rand"
23+
coros: [1]
24+
processes: [1]
25+
26+
defaults:
27+
DEFAULT_RAPID_ZONAL_BUCKET: "chandrasiri-benchmarks-zb"
28+
DEFAULT_STANDARD_BUCKET: "chandrasiri-benchmarks-rb"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from dataclasses import dataclass
15+
from tests.perf.microbenchmarks.parameters import IOBenchmarkParameters
16+
17+
18+
@dataclass
19+
class TimeBasedReadParameters(IOBenchmarkParameters):
20+
pattern: str
21+
duration: int
22+
warmup_duration: int
23+
num_ranges: int

0 commit comments

Comments
 (0)