mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-02-26 01:31:26 -05:00
AB#2191 Evaluate K-Bench benchmarks in CI
Install Python for K-bench evaluation Add scripts to evaluate the K-Bench results in CI Attach graphs to the workflow results in GitHub Actions
This commit is contained in:
parent
f4ff473677
commit
1952eb5721
25
.github/actions/k-bench/action.yml
vendored
25
.github/actions/k-bench/action.yml
vendored
@ -12,6 +12,15 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
|
- name: Setup python
|
||||||
|
uses: actions/setup-python@b55428b1882923874294fa556849718a1d7f2ca5 # tag=v4.2.0
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
|
||||||
|
- name: Install evaluation dependencies
|
||||||
|
shell: bash
|
||||||
|
run: pip install -r .github/actions/k-bench/evaluate/requirements.txt
|
||||||
|
|
||||||
- name: Checkout patched K-Bench
|
- name: Checkout patched K-Bench
|
||||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
|
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
|
||||||
with:
|
with:
|
||||||
@ -104,3 +113,19 @@ runs:
|
|||||||
with:
|
with:
|
||||||
path: "k-bench/out/kbench-constellation-${{ inputs.cloudProvider }}"
|
path: "k-bench/out/kbench-constellation-${{ inputs.cloudProvider }}"
|
||||||
name: "k-bench-constellation-${{ inputs.cloudProvider }}"
|
name: "k-bench-constellation-${{ inputs.cloudProvider }}"
|
||||||
|
|
||||||
|
- name: Parse test results and create diagrams
|
||||||
|
shell: bash
|
||||||
|
run: python .github/actions/k-bench/evaluate/main.py
|
||||||
|
env:
|
||||||
|
KBENCH_RESULTS: ${{ github.workspace }}/k-bench/out/
|
||||||
|
CSP: ${{ inputs.cloudProvider }}
|
||||||
|
|
||||||
|
- name: Upload benchmark results
|
||||||
|
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3.1.0
|
||||||
|
if: ${{ !env.ACT }}
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
*_perf.png
|
||||||
|
kbench_results.json
|
||||||
|
name: "benchmark_results"
|
||||||
|
0
.github/actions/k-bench/evaluate/evaluators/__init__.py
vendored
Normal file
0
.github/actions/k-bench/evaluate/evaluators/__init__.py
vendored
Normal file
71
.github/actions/k-bench/evaluate/evaluators/default.py
vendored
Normal file
71
.github/actions/k-bench/evaluate/evaluators/default.py
vendored
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"""Evaluator for the K-Bench default test."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
pod_latencies = {
|
||||||
|
'pod_create': 'create pod latency:',
|
||||||
|
'pod_list': 'list pod latency:',
|
||||||
|
'pod_get': 'get pod latency:',
|
||||||
|
'pod_update': 'update pod latency:',
|
||||||
|
'pod_delete': 'delete pod latency:',
|
||||||
|
}
|
||||||
|
|
||||||
|
deployment_latencies = {
|
||||||
|
'depl_create': 'create deployment latency:',
|
||||||
|
'depl_list': 'list deployment latency:',
|
||||||
|
'depl_update': 'update deployment latency:',
|
||||||
|
'depl_scale': 'scale deployment latency:',
|
||||||
|
'depl_delete': 'delete deployment latency:',
|
||||||
|
}
|
||||||
|
|
||||||
|
service_latencies = {
|
||||||
|
'svc_create': 'create service latency:',
|
||||||
|
'svc_list': 'list service latency:',
|
||||||
|
'svc_get': 'get service latency:',
|
||||||
|
'svc_update': 'update service latency:',
|
||||||
|
'svc_delete': 'delete service latency:',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def eval(tests: Dict[str, str]) -> Dict[str, Dict[str, float]]:
|
||||||
|
"""Read the results of the default tests.
|
||||||
|
|
||||||
|
Return a result dictionary.
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
for t in tests:
|
||||||
|
row = defaultdict(float)
|
||||||
|
# read the default result file
|
||||||
|
kbench = []
|
||||||
|
with open(os.path.join(tests[t], 'default', 'kbench.log'), 'r') as f:
|
||||||
|
kbench = f.readlines()
|
||||||
|
|
||||||
|
if not kbench:
|
||||||
|
raise Exception("Empty kbench.log")
|
||||||
|
|
||||||
|
subtests = [pod_latencies, service_latencies, deployment_latencies]
|
||||||
|
for latency_dict in subtests:
|
||||||
|
# Get the API Call Latencies (median)
|
||||||
|
for key in latency_dict:
|
||||||
|
line = get_line_containing_needle(
|
||||||
|
lines=kbench, needle=latency_dict[key])
|
||||||
|
median = get_median_from_line(line=line)
|
||||||
|
row[key] = float(median)
|
||||||
|
|
||||||
|
result[t] = row
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_median_from_line(line):
|
||||||
|
"""Extract the value (median) from the line."""
|
||||||
|
return re.search(r'\s(\d+\.\d+)(.+)', line).group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_line_containing_needle(lines, needle):
|
||||||
|
"""Find matching line from list of lines."""
|
||||||
|
matches = list(filter(lambda l: needle in l, lines))
|
||||||
|
if len(matches) > 1:
|
||||||
|
raise Exception(f"'{needle}' matched multiple times..")
|
||||||
|
return matches[0]
|
81
.github/actions/k-bench/evaluate/evaluators/fio.py
vendored
Normal file
81
.github/actions/k-bench/evaluate/evaluators/fio.py
vendored
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
"""Parse the fio logs.
|
||||||
|
|
||||||
|
Extracts the bandwidth for I/O,
|
||||||
|
from various fio benchmarks.
|
||||||
|
|
||||||
|
Example log file (extracting read and write bandwidth):
|
||||||
|
...
|
||||||
|
Run status group 0 (all jobs):
|
||||||
|
READ: bw=5311KiB/s (5438kB/s), 5311KiB/s-5311KiB/s (5438kB/s-5438kB/s), io=311MiB (327MB), run=60058-60058msec
|
||||||
|
WRITE: bw=2289KiB/s (2343kB/s), 2289KiB/s-2289KiB/s (2343kB/s-2343kB/s), io=134MiB (141MB), run=60058-60058msec
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
# get different mixes of read/write IO as subtests
|
||||||
|
subtests = {
|
||||||
|
'fio_root_async_R70W30': 'fio_async_randR70W30.out',
|
||||||
|
'fio_root_async_R100W0': 'fio_async_randR100W0.out',
|
||||||
|
'fio_root_async_R0W100': 'fio_async_randR0W100.out',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def eval(tests: Dict[str, str]) -> Dict[str, Dict[str, float]]:
|
||||||
|
"""Read the results of the fio tests.
|
||||||
|
Return a result dictionary.
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
for t in tests:
|
||||||
|
base_path = os.path.join(tests[t], 'dp_fio')
|
||||||
|
row = defaultdict(str)
|
||||||
|
for subtest in subtests:
|
||||||
|
try:
|
||||||
|
log_path = next(Path(base_path).rglob(subtests[subtest]))
|
||||||
|
except StopIteration:
|
||||||
|
raise Exception(
|
||||||
|
f"Error: No iperfclient.out found for network test {subtest} in {base_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(log_path) as f:
|
||||||
|
fio = f.readlines()
|
||||||
|
if not fio:
|
||||||
|
raise Exception(f"Empty fio log {subtest}?")
|
||||||
|
|
||||||
|
for line in fio:
|
||||||
|
if "READ" in line:
|
||||||
|
speed = get_io_bw_from_line(line)
|
||||||
|
row[subtest + '_R'] = speed
|
||||||
|
elif "WRITE" in line:
|
||||||
|
speed = get_io_bw_from_line(line)
|
||||||
|
row[subtest + '_W'] = speed
|
||||||
|
result[t] = row
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Dictionary to convert units
|
||||||
|
units = {
|
||||||
|
'KiB': 1/1024,
|
||||||
|
'MiB': 1,
|
||||||
|
'GiB': 1024,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_io_bw_from_line(line) -> float:
|
||||||
|
"""Get the IO bandwidth from line and convert to MiB/s.
|
||||||
|
|
||||||
|
Return the IO bandwidth in MiB/s
|
||||||
|
"""
|
||||||
|
# READ: bw=32.5MiB/s (34.1MB/s), 32.5MiB/s-32.5MiB/s (34.1MB/s-34.1MB/s), io=1954MiB (2048MB), run=60022-60022msec
|
||||||
|
match = re.search(r'bw=(\d+\.?\d+)(MiB|KiB|GiB)', line)
|
||||||
|
if not match:
|
||||||
|
raise Exception("Could not extract bw from fio line.")
|
||||||
|
num = float(match.group(1))
|
||||||
|
num = num * units[match.group(2)]
|
||||||
|
# return in MiB/s
|
||||||
|
return num
|
83
.github/actions/k-bench/evaluate/evaluators/network.py
vendored
Normal file
83
.github/actions/k-bench/evaluate/evaluators/network.py
vendored
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
"""Parse the iperf logs.
|
||||||
|
|
||||||
|
Extracts the bandwidth for sending and receiving,
|
||||||
|
from intranode and internode network benchmarks.
|
||||||
|
|
||||||
|
Example log file (extract the bitrate for sending and receiving):
|
||||||
|
...
|
||||||
|
s1: - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
s1: [ ID] Interval Transfer Bitrate Retr
|
||||||
|
s1: [ 5] 0.00-90.00 sec 11.0 GBytes 1.05 Gbits/sec 509 sender
|
||||||
|
s1: [ 5] 0.00-90.05 sec 11.1 GBytes 1.05 Gbits/sec receiver
|
||||||
|
s1:
|
||||||
|
s1: iperf Done.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
subtests = {
|
||||||
|
'net_internode': 'dp_network_internode',
|
||||||
|
'net_intranode': 'dp_network_intranode',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def eval(tests: Dict[str, str]) -> Dict[str, Dict[str, float]]:
|
||||||
|
"""Read the results of the network tests.
|
||||||
|
Return a result dictionary.
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
for t in tests:
|
||||||
|
row = defaultdict(str)
|
||||||
|
for subtest in subtests:
|
||||||
|
base_path = os.path.join(tests[t], subtests[subtest])
|
||||||
|
try:
|
||||||
|
log_path = next(Path(base_path).rglob('iperfclient.out'))
|
||||||
|
except StopIteration:
|
||||||
|
raise Exception(
|
||||||
|
f"Error: No iperfclient.out found for network test {subtest} in {base_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(log_path) as f:
|
||||||
|
iperfclient = f.readlines()
|
||||||
|
|
||||||
|
if not iperfclient:
|
||||||
|
raise Exception("Empty iperfclient?")
|
||||||
|
|
||||||
|
for line in iperfclient:
|
||||||
|
if "sender" in line:
|
||||||
|
speed = get_speed_from_line(line)
|
||||||
|
row[subtest + '_snd'] = speed
|
||||||
|
break
|
||||||
|
elif "receiver" in line:
|
||||||
|
speed = get_speed_from_line(line)
|
||||||
|
row[subtest + '_rcv'] = speed
|
||||||
|
break
|
||||||
|
result[t] = row
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Dictionary to convert units
|
||||||
|
units = {
|
||||||
|
'bits': 1e-6,
|
||||||
|
'Mbits': 1,
|
||||||
|
'Gbits': 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_speed_from_line(line) -> float:
|
||||||
|
"""Extract the network throughput from the line.
|
||||||
|
|
||||||
|
|
||||||
|
Returns the throughput as Mbit/s.
|
||||||
|
"""
|
||||||
|
match = re.search(
|
||||||
|
r'(\d+\.?\d+)\s(bits|Mbits|Gbits)\/sec[\s\d]+(sender|receiver)$', line)
|
||||||
|
if not match:
|
||||||
|
raise Exception("Could not extract speed from iperf line.")
|
||||||
|
num = float(match.group(1))
|
||||||
|
num = num * units[match.group(2)]
|
||||||
|
# return in Mbit/s
|
||||||
|
return float(num)
|
159
.github/actions/k-bench/evaluate/main.py
vendored
Normal file
159
.github/actions/k-bench/evaluate/main.py
vendored
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
"""Parse logs of K-Bench tests and generate performance graphs."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from evaluators import default, fio, network
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
BAR_COLOR = '#90FF99' # Mint Green
|
||||||
|
|
||||||
|
# Rotate bar labels by X degrees
|
||||||
|
LABEL_ROTATE_BY = 30
|
||||||
|
LABEL_FONTSIZE = 9
|
||||||
|
|
||||||
|
# Some lookup dictionaries for x axis
|
||||||
|
api_suffix = 'ms'
|
||||||
|
pod_key2header = {
|
||||||
|
'pod_create': 'Pod Create',
|
||||||
|
'pod_list': 'Pod List',
|
||||||
|
'pod_get': 'Pod Get',
|
||||||
|
'pod_update': 'Pod Update',
|
||||||
|
'pod_delete': 'Pod Delete',
|
||||||
|
}
|
||||||
|
svc_key2header = {
|
||||||
|
'svc_create': 'Service Create',
|
||||||
|
'svc_list': 'Service List',
|
||||||
|
'svc_update': 'Service Update',
|
||||||
|
'svc_delete': 'Service Delete',
|
||||||
|
'svc_get': 'Service Get',
|
||||||
|
}
|
||||||
|
depl_key2header = {
|
||||||
|
'depl_create': 'Deployment Create',
|
||||||
|
'depl_list': 'Deployment List',
|
||||||
|
'depl_update': 'Deployment Update',
|
||||||
|
'depl_scale': 'Deployment Scale',
|
||||||
|
'depl_delete': 'Deployment Delete',
|
||||||
|
}
|
||||||
|
|
||||||
|
fio_suffix = 'MiB/s'
|
||||||
|
fio_key2header = {
|
||||||
|
'fio_root_async_R70W30_R': 'async_R70W30 mix,\n seq. reads',
|
||||||
|
'fio_root_async_R70W30_W': 'async_R70W30 mix,\n seq. writes',
|
||||||
|
'fio_root_async_R100W0_R': 'async_R100W0 mix,\n seq. reads',
|
||||||
|
'fio_root_async_R0W100_W': 'async_R0W100 mix,\n seq. writes',
|
||||||
|
}
|
||||||
|
|
||||||
|
net_suffix = 'Mbit/s'
|
||||||
|
net_key2header = {
|
||||||
|
'net_internode_snd': f'iperf internode \n send ({net_suffix})',
|
||||||
|
'net_intranode_snd': f'iperf intranode \n send ({net_suffix})',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def configure() -> dict:
|
||||||
|
"""Set the config.
|
||||||
|
|
||||||
|
Raises BaseException if base_path or CSP missing.
|
||||||
|
|
||||||
|
Returns a config dict with the BASE_PATH to the tests
|
||||||
|
and the cloud service provider CSP.
|
||||||
|
"""
|
||||||
|
base_path = os.getenv('KBENCH_RESULTS', None)
|
||||||
|
if not base_path or not os.path.isdir(base_path):
|
||||||
|
raise Exception("Environment variable 'KBENCH_RESULTS' \
|
||||||
|
needs to point to the K-Bench results root folder")
|
||||||
|
|
||||||
|
csp = os.getenv('CSP', None)
|
||||||
|
if not csp:
|
||||||
|
raise Exception("Environment variable 'CSP' \
|
||||||
|
needs to name the cloud service provider.")
|
||||||
|
return {'BASE_PATH': base_path, 'CSP': csp}
|
||||||
|
|
||||||
|
|
||||||
|
def bar_chart(data, headers, title='', suffix='', val_label=True, y_log=False):
|
||||||
|
"""Generate a bar chart from data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (list): List of value points.
|
||||||
|
headers (list): List of headers (x-axis).
|
||||||
|
title (str, optional): The title for the chart. Defaults to "".
|
||||||
|
suffix (str, optional): The suffix for values e.g. "MiB/s". Defaults to "".
|
||||||
|
val_label (bool, optional): Put a label of the value over the bar chart. Defaults to True.
|
||||||
|
y_log (bool, optional): Set the y-axis to a logarithmic scale. Defaults to False.
|
||||||
|
Returns:
|
||||||
|
fig (matplotlib.pyplot.figure): The pyplot figure
|
||||||
|
"""
|
||||||
|
fig, ax = plt.subplots(figsize=(8, 5))
|
||||||
|
fig.patch.set_facecolor('white')
|
||||||
|
ax.set_xticks(np.arange(len(headers)))
|
||||||
|
ax.set_xticklabels(headers)
|
||||||
|
if y_log:
|
||||||
|
ax.set_yscale('log')
|
||||||
|
bars = ax.bar(headers, data, color=BAR_COLOR, edgecolor='black')
|
||||||
|
if val_label:
|
||||||
|
ax.bar_label(bars, fmt='%g {suffix}'.format(suffix=suffix))
|
||||||
|
plt.setp(ax.get_xticklabels(), fontsize=LABEL_FONTSIZE, rotation=LABEL_ROTATE_BY)
|
||||||
|
plt.title(f'{title} ({suffix})')
|
||||||
|
plt.tight_layout()
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Read, parse and evaluate the K-Bench tests.
|
||||||
|
|
||||||
|
Generate a human-readable table and diagrams.
|
||||||
|
"""
|
||||||
|
config = configure()
|
||||||
|
|
||||||
|
benchmark_path = os.path.join(
|
||||||
|
config['BASE_PATH'],
|
||||||
|
"kbench-constellation-" + config['CSP'],
|
||||||
|
)
|
||||||
|
if not os.path.exists(benchmark_path):
|
||||||
|
raise Exception(f'Path to benchmarks {benchmark_path} does not exist.')
|
||||||
|
|
||||||
|
tests = {f"constellation-{config['CSP']}": benchmark_path}
|
||||||
|
|
||||||
|
# Execute tests
|
||||||
|
default_results = default.eval(tests=tests)
|
||||||
|
network_results = network.eval(tests=tests)
|
||||||
|
fio_results = fio.eval(tests=tests)
|
||||||
|
|
||||||
|
combined_results = defaultdict(dict)
|
||||||
|
for test in tests:
|
||||||
|
combined_results[test].update(default_results[test])
|
||||||
|
combined_results[test].update(network_results[test])
|
||||||
|
combined_results[test].update(fio_results[test])
|
||||||
|
|
||||||
|
# Write the compact results.
|
||||||
|
with open('kbench_results.json', 'w') as w:
|
||||||
|
json.dump(combined_results, fp=w, sort_keys=False, indent=2)
|
||||||
|
|
||||||
|
# Generate graphs.
|
||||||
|
subject = list(combined_results.keys())[0]
|
||||||
|
data = combined_results[subject]
|
||||||
|
|
||||||
|
# Combine the evaluation of the Kubernetes API benchmarks
|
||||||
|
for i, api in enumerate([pod_key2header, svc_key2header, depl_key2header]):
|
||||||
|
api_data = [data[h] for h in api]
|
||||||
|
hdrs = api.values()
|
||||||
|
bar_chart(data=api_data, headers=hdrs, title="API Latency", suffix=api_suffix)
|
||||||
|
plt.savefig(f'api_{i}_perf.png', bbox_inches="tight")
|
||||||
|
|
||||||
|
# Network chart
|
||||||
|
net_data = [data[h] for h in net_key2header]
|
||||||
|
hdrs = net_key2header.values()
|
||||||
|
bar_chart(data=net_data, headers=hdrs, title="Network Throughput", suffix=net_suffix)
|
||||||
|
plt.savefig('net_perf.png', bbox_inches="tight")
|
||||||
|
|
||||||
|
# fio chart
|
||||||
|
fio_data = [data[h] for h in fio_key2header]
|
||||||
|
hdrs = fio_key2header.values()
|
||||||
|
bar_chart(data=fio_data, headers=hdrs, title="Storage Throughput", suffix=fio_suffix)
|
||||||
|
plt.savefig('storage_perf.png', bbox_inches="tight")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
2
.github/actions/k-bench/evaluate/requirements.txt
vendored
Normal file
2
.github/actions/k-bench/evaluate/requirements.txt
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
matplotlib==3.6.0
|
||||||
|
numpy==1.23.4
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -45,3 +45,6 @@ image/config.mk
|
|||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user