diff --git a/.github/workflows/metrics.yml b/.github/workflows/metrics.yml new file mode 100644 index 000000000..2ba4c190f --- /dev/null +++ b/.github/workflows/metrics.yml @@ -0,0 +1,60 @@ +# Copyright (c) 2023-2026, Nubificus LTD +# +# Licensed 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. + +name: metrics + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 1' # every Monday at midnight + +jobs: + measure: + runs-on: [self-hosted, linux, amd64] + strategy: + fail-fast: false + matrix: + include: + - unikernel: solo5-hvt + image: harbor.nbfc.io/nubificus/urunc/redis-hvt-rumprun:latest + - unikernel: unikraft-nginx + image: harbor.nbfc.io/nubificus/urunc/nginx-unikraft-fc-initrd:latest + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup timestamping shim + run: | + sudo tee /usr/local/bin/containerd-shim-uruncts-v2 > /dev/null << 'EOT' + #!/bin/bash + URUNC_TIMESTAMPS=1 /usr/local/bin/containerd-shim-urunc-v2 $@ + EOT + sudo chmod +x /usr/local/bin/containerd-shim-uruncts-v2 + + - name: Run metrics measurement + run: | + cd script/performance + sudo python3 measure_to_csv.py \ + 5 \ + ${{ matrix.image }} \ + metrics-${{ matrix.unikernel }}.csv + + - name: Upload CSV artifact + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: metrics-${{ matrix.unikernel }} + path: script/performance/metrics-${{ matrix.unikernel }}.csv + if-no-files-found: warn diff --git a/script/performance/__modules__.py b/script/performance/__modules__.py index 3463c1d64..99b16ec3f 100644 --- a/script/performance/__modules__.py +++ b/script/performance/__modules__.py @@ -117,7 +117,9 @@ def sorted(self) -> List[Timestamp]: return temp -def parseSingleContainerTimestamps(filename: str, containerID: str) -> List[str]: +def parseSingleContainerTimestamps( + filename: str, containerID: str +) -> List[str]: with open(filename, 'r') as f: data = f.readlines() return [line for line in data if containerID in line] @@ -127,8 +129,19 @@ def emptyFile(filename: str) -> None: open(filename, "w").close() -def spawnContainer() -> str: - command = "nerdctl run --name redis-test -d --snapshotter devmapper --runtime io.containerd.uruncts.v2 harbor.nbfc.io/nubificus/urunc/redis-hvt-rumprun:latest" +def spawnContainer( + image: str = ( + "harbor.nbfc.io/nubificus/urunc/redis-hvt-rumprun:latest" + ), + name: str = "redis-test", + snapshotter: str = "devmapper", + runtime: str = "io.containerd.uruncts.v2" +) -> str: + command = ( + f"nerdctl run --name {name} -d" + f" --snapshotter {snapshotter}" + f" --runtime {runtime} {image}" + ) cmdParts = command.split(" ") cmd = run(cmdParts, stdout=PIPE, @@ -138,15 +151,15 @@ def spawnContainer() -> str: return containerID -def deleteContainer() -> bool: - command = "nerdctl rm --force redis-test" +def deleteContainer(name: str = "redis-test") -> bool: + command = f"nerdctl rm --force {name}" cmdParts = command.split(" ") cmd = run(cmdParts, stdout=PIPE, text=True) response = cmd.stdout - containerID = response.splitlines()[-1] - return containerID == "redis-test" + result = response.splitlines()[-1] + return result == name def myprint(msg: str): diff --git a/script/performance/measure_to_csv.py b/script/performance/measure_to_csv.py new file mode 100644 index 000000000..eda7dc083 --- /dev/null +++ b/script/performance/measure_to_csv.py @@ -0,0 +1,99 @@ +# Copyright (c) 2023-2026, Nubificus LTD +# +# Licensed 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. + +from __modules__ import ( + emptyFile, spawnContainer, deleteContainer, + parseSingleContainerTimestamps, TimestampSeries, myprint +) +from sys import argv +from time import sleep +import csv + +LOGFILE = "/tmp/urunc.zlog" +DELAY = 2 + +# Key phase definitions: (start_tsID, end_tsID, column_name) +PHASES = [ + ("TS00", "TS11", "create_ns"), # full create phase + ("TS12", "TS18", "start_ns"), # full start phase + ("TS16", "TS17", "network_ns"), # network setup + ("TS17", "TS18", "disk_ns"), # disk setup +] + + +def get_phase_duration(series, start_id, end_id): + ts_map = {ts.tsID: ts for ts in series.sorted} + if start_id in ts_map and end_id in ts_map: + return ts_map[end_id].timestamp - ts_map[start_id].timestamp + return "N/A" + + +def main(): + if len(argv) != 4: + print("Error: Missing arguments!") + print("") + print("Usage:") + print(f"\t{argv[0]} ") + print("") + print("Example:") + print(f"\t{argv[0]} 5 " + "harbor.nbfc.io/nubificus/urunc/" + "redis-hvt-rumprun:latest metrics.csv") + exit(1) + + iterations = int(argv[1]) + image = argv[2] + output_file = argv[3] + name = "urunc-metrics-test" + + myprint(f"Collecting metrics for {iterations} iterations") + myprint(f"Image: {image}") + sleep(2) + + emptyFile(LOGFILE) + container_ids = [] + + for i in range(iterations): + myprint(f"Running iteration {i+1} of {iterations}") + container_id = spawnContainer(image=image, name=name) + container_ids.append(container_id) + sleep(DELAY) + success = deleteContainer(name=name) + if not success: + print("Error removing container.") + exit(1) + + myprint("Writing CSV...") + + with open(output_file, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["containerID", "create_ns", "start_ns", + "network_ns", "disk_ns"]) + for container_id in container_ids: + data = parseSingleContainerTimestamps( + filename=LOGFILE, containerID=container_id) + if not data: + continue + series = TimestampSeries(data=data) + row = [container_id] + for start_id, end_id, _ in PHASES: + row.append(get_phase_duration(series, start_id, end_id)) + writer.writerow(row) + + myprint(f"Saved metrics to {output_file}") + emptyFile(LOGFILE) + + +if __name__ == "__main__": + main()