Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions benchkit/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,7 @@
)
from benchkit.utils.system import get_boot_args
from benchkit.utils.tee import teeprint
from benchkit.utils.types import (
Command,
Constants,
Environment,
PathType,
Pretty,
SplitCommand,
)
from benchkit.utils.types import Command, Constants, Environment, PathType, Pretty, SplitCommand
from benchkit.utils.variables import list_groupby

RecordKey = str
Expand Down
6 changes: 1 addition & 5 deletions benchkit/cli/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
import black
import isort

from benchkit.cli.generate import (
generate_benchmark,
generate_campaign,
get_gitignore_content,
)
from benchkit.cli.generate import generate_benchmark, generate_campaign, get_gitignore_content
from benchkit.utils.misc import get_benchkit_temp_folder_str

_DOTGIT_DIR = Path(".git")
Expand Down
6 changes: 1 addition & 5 deletions benchkit/commandwrappers/javaperf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@
from typing import Dict, List, Optional

from benchkit.benchmark import RecordResult, WriteRecordFileFunction
from benchkit.commandwrappers.perf import (
PerfRecordWrap,
PerfStatWrap,
_perf_command_prefix,
)
from benchkit.commandwrappers.perf import PerfRecordWrap, PerfStatWrap, _perf_command_prefix
from benchkit.helpers.linux import ps
from benchkit.platforms import Platform
from benchkit.shell.shell import shell_interactive, shell_out
Expand Down
170 changes: 170 additions & 0 deletions benchkit/commandwrappers/perflock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Copyright (C) 2026 Vrije Universiteit Brussel. All rights reserved.
# SPDX-License-Identifier: MIT
"""
Command wrapper for the `perf lock` Linux utility which allows to capture lock statistics
when executing the wrapped command.
"""

import pathlib
import re
import time
from threading import Thread
from typing import Any, Dict, List, Optional

from benchkit.benchmark import RecordResult, WriteRecordFileFunction
from benchkit.commandwrappers.perf import PerfRecordWrap, _perf_command_prefix
from benchkit.helpers.linux import ps
from benchkit.platforms.generic import Platform
from benchkit.shell.shell import shell_out
from benchkit.shell.shellasync import AsyncProcess
from benchkit.utils.types import PathType, SplitCommand


class PerfLockWrap(PerfRecordWrap):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.attachment_thread = None
self._data_paths = {}

def attach_every_thread(self, **kwargs):
self.attachment_thread = Thread(target=self.attach_every_thread_worker, kwargs=kwargs)
self.attachment_thread.start()

def attach_every_thread_worker(
self,
process: AsyncProcess,
platform: Platform,
record_data_dir: pathlib.Path,
poll_ms: int = 10,
):
"""Command attachment that will attach to every thread of the wrapped process.

Args:
process (AsyncProcess): the process to attach perf-stat to.
platform (Platform): the platform where the process is running.
record_data_dir (pathlib.Path): the path to the record data directory of the benchmark.
poll_ms (int, optional): the period at which to poll the process to detect newly created
threads. Defaults to 10.
"""
perf_prefix = _perf_command_prefix(perf_bin=self._perf_bin, platform=platform)
prefix = ["sudo"] + perf_prefix + ["lock", "record"] + self.perf_record_options + ["-t"]

tids2perf_cmd = {}

while not process.is_finished():
current_tids = ps.get_threads_of_process(pid=process.pid, ignore_any_error_code=True)
for tid in current_tids:
if tid not in tids2perf_cmd:
perf_data_pathname = record_data_dir / f"perf-lock-record-tid-{tid}.data"
self._data_paths[tid] = perf_data_pathname
tids2perf_cmd[tid] = Thread(
target=self.attach_on_thread_worker,
kwargs={
"prefix": prefix,
"tid": tid,
"perf_data_pathname": perf_data_pathname,
},
)
tids2perf_cmd[tid].start()

time.sleep(poll_ms / 1000)

for current_process in tids2perf_cmd.values():
try:
current_process.join()
except AsyncProcess.AsyncProcessError:
pass

def attach_on_thread_worker(
self, prefix: list[str], tid: int, perf_data_pathname: pathlib.Path
):
cmd = prefix + [f"{tid}", "--output", f"{perf_data_pathname}"]
self.platform.comm.shell(command=cmd)

def _perf_report_command(self, perf_data_pathname: PathType) -> SplitCommand:
command = [
self._perf_bin,
"lock",
"report",
"--input",
f"{perf_data_pathname}",
]

return command

def post_run_hook_report(
self,
experiment_results_lines: List[RecordResult],
record_data_dir: PathType,
write_record_file_fun: WriteRecordFileFunction,
) -> Optional[Dict[str, Any]]:
"""Post run hook to generate extension of result dict holding the results of perf report.

Args:
experiment_results_lines (List[RecordResult]): the record results.
record_data_dir (PathType): path to the record data directory.
write_record_file_fun (WriteRecordFileFunction): callback to record a file into data
directory.
"""
assert experiment_results_lines and record_data_dir
assert self.attachment_thread is not None

self.attachment_thread.join()

time_regex = re.compile(r"^\s*(\d+\.?\d*)\s*(ns|us|ms|s|m|h)\s*$", re.IGNORECASE)

def parse_time_to_ns(s: str) -> float:
"""Parse strings like '1.6 us', '800 ns', '2.3 ms' -> nanoseconds (float)."""
m = time_regex.match(s.rstrip())
if not m:
raise ValueError(f"can't parse time value: {s}")
val = float(m.group(1))
unit = m.group(2).lower()
return (
val
* {"ns": 1.0, "us": 1e3, "ms": 1e6, "s": 1e9, "m": 1e9 * 60, "h": 1e9 * 3600}[unit]
)

row_re = re.compile(
r"""
^\s+
([a-zA-Z]+)?\s+ # Optional name
(\d+)\s+ # acquired
(\d+)\s* # contended
(\S+\s\S+)\s* # avg_wait
(\S+\s\S+)\s* # total_wait
(\S+\s\S+)\s* # max_wait
(\S+\s\S+)\s* # min_wait
$
""",
re.VERBOSE,
)

aggregation_dict = {"perf_lock_total_wait_ns": 0.0}

for tid, data_path in self._data_paths.items():
self._chown(pathname=data_path)
command = self._perf_report_command(perf_data_pathname=data_path)
report_file = (record_data_dir / pathlib.Path(f"perf-tid-{tid}.report")).as_posix()

# retrieve output into file first for posterity
file_command = command + ["--output=" + report_file]
shell_out(file_command, print_output=False)

with open(report_file) as f:
for line in f.readlines():
line = line.rstrip()
m = row_re.search(line)
if m:
# name = m.group(1)
# acquired = int(m.group(2))
# contended = int(m.group(3))
# avg_wait = parse_time_to_ns(m.group(4))
total_wait = parse_time_to_ns(m.group(5))
# max_wait = parse_time_to_ns(m.group(6))
# min_wait = parse_time_to_ns(m.group(7))

aggregation_dict["perf_lock_total_wait_ns"] += total_wait

self._data_paths = {}
return aggregation_dict
15 changes: 15 additions & 0 deletions benchkit/commandwrappers/speedupstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
from benchkit.commandattachments.offcputime import Offcputime
from benchkit.commandattachments.signal import Signal
from benchkit.commandwrappers import CommandWrapper
from benchkit.commandwrappers.perflock import PerfLockWrap
from benchkit.commandwrappers.strace import StraceWrap
from benchkit.dependencies.packages import PackageDependency
from benchkit.platforms import get_current_platform
from benchkit.utils.types import PathType


Expand All @@ -23,6 +25,13 @@ def __init__(self, libbpf_tools_dir: PathType) -> None:
self._llcstat = Llcstat(libbpf_tools_dir)
self._strace = StraceWrap(pid=True, summary=False, summary_only=True)

self._perflock = PerfLockWrap(
perf_record_options=[],
perf_report_options=[],
report_file=False,
report_interactive=False,
)

self._sigstop = Signal(signal_type=SIGSTOP)
self._sigcont = Signal(signal_type=SIGCONT)

Expand All @@ -36,6 +45,11 @@ def command_attachments(self):
self._offcputime.attachment,
self._llcstat.attachment,
self._strace.attachment,
lambda process, record_data_dir: self._perflock.attach_every_thread(
platform=get_current_platform(),
process=process,
record_data_dir=record_data_dir,
),
self._sigcont.attachment,
]

Expand All @@ -45,6 +59,7 @@ def post_run_hooks(self):
self._offcputime.post_run_hook,
self._llcstat.post_run_hook,
self._strace.post_run_hook,
self._perflock.post_run_hook_report,
]

def dependencies(self) -> List[PackageDependency]:
Expand Down
7 changes: 1 addition & 6 deletions benchkit/helpers/linux/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@
import pathlib
from typing import Dict, Iterable, List

from benchkit.helpers.linux.build import (
KernelEntry,
LinuxBuild,
Option,
configure_standard_kernel,
)
from benchkit.helpers.linux.build import KernelEntry, LinuxBuild, Option, configure_standard_kernel
from benchkit.shell.shell import shell_out
from benchkit.utils.types import PathType

Expand Down
3 changes: 2 additions & 1 deletion benchkit/helpers/linux/ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from benchkit.shell.shell import shell_out


def get_threads_of_process(pid: int) -> List[int]:
def get_threads_of_process(pid: int, ignore_any_error_code: bool = False) -> List[int]:
"""Get thread identifiers (TIDs) of the given process identifier (PID).

Args:
Expand All @@ -26,6 +26,7 @@ def get_threads_of_process(pid: int) -> List[int]:
f"ps -T -p {pid}",
print_input=False,
print_output=False,
ignore_any_error_code=ignore_any_error_code,
)

tids = []
Expand Down
7 changes: 1 addition & 6 deletions benchkit/hooks/stressNg.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@

from typing import List, Optional

from benchkit.benchmark import (
PostRunHook,
PreRunHook,
RecordResult,
WriteRecordFileFunction,
)
from benchkit.benchmark import PostRunHook, PreRunHook, RecordResult, WriteRecordFileFunction
from benchkit.platforms import get_current_platform
from benchkit.shell.shellasync import shell_async
from benchkit.utils.types import PathType
Expand Down
6 changes: 1 addition & 5 deletions benchkit/platforms/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@

from benchkit.communication import CommunicationLayer
from benchkit.platforms import evenorder
from benchkit.platforms.utils import (
get_nb_cpus_active,
get_nb_cpus_isolated,
get_nb_cpus_total,
)
from benchkit.platforms.utils import get_nb_cpus_active, get_nb_cpus_isolated, get_nb_cpus_total
from benchkit.utils import lscpu


Expand Down
6 changes: 1 addition & 5 deletions benchkit/sharedlibs/assignlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@
import os
from typing import Iterable, Optional, Tuple

from benchkit.sharedlibs import (
EnvironmentVariables,
FromSourceSharedLib,
LdPreloadLibraries,
)
from benchkit.sharedlibs import EnvironmentVariables, FromSourceSharedLib, LdPreloadLibraries
from benchkit.shell.shell import shell_out
from benchkit.utils.dir import (
caller_file_abs_path,
Expand Down
6 changes: 1 addition & 5 deletions benchkit/sharedlibs/tiltlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@
from typing import Tuple

from benchkit.platforms import Platform
from benchkit.sharedlibs import (
EnvironmentVariables,
FromSourceSharedLib,
LdPreloadLibraries,
)
from benchkit.sharedlibs import EnvironmentVariables, FromSourceSharedLib, LdPreloadLibraries


def cmake_configure_build(
Expand Down
5 changes: 1 addition & 4 deletions examples/cameracampaign/camera_campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
from typing import Any, Dict, Iterable, List

from pythainer.examples.builders import get_user_gui_builder
from pythainer.examples.installs import (
opencv_lib_install_from_src,
realsense2_lib_install_from_src,
)
from pythainer.examples.installs import opencv_lib_install_from_src, realsense2_lib_install_from_src
from pythainer.examples.runners import camera_runner, gui_runner, personal_runner
from pythainer.runners import ConcreteDockerRunner

Expand Down
5 changes: 1 addition & 4 deletions examples/ipc/ipc.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,7 @@ def main() -> None:

match target:
case Target.HARMONY:
from benchkit.devices.hdc import (
OpenHarmonyCommLayer,
OpenHarmonyDeviceConnector,
)
from benchkit.devices.hdc import OpenHarmonyCommLayer, OpenHarmonyDeviceConnector

bench_dir = "/data/local/tmp"
device = list(OpenHarmonyDeviceConnector.query_devices())[0]
Expand Down
5 changes: 4 additions & 1 deletion examples/rocksdb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ cd ../../..

Running the speedup stack campaign.
```
sudo -v
while true; do sudo -v; sleep 60; done &
./campaign_rocksdb_speedup_stacks.py
kill %1
```


Expand Down Expand Up @@ -88,9 +91,9 @@ sudo setcap cap_sys_resource,cap_sys_admin+eip ./klockstat
sudo setcap cap_sys_resource,cap_sys_admin+eip ./offcputime
sudo setcap cap_sys_resource,cap_sys_admin+eip ./llcstat
sudo setcap cap_sys_ptrace+ep /usr/bin/strace
kill %1
cd ../../..
./campaign_rocksdb_speedup_stacks.py
kill %1
```


Expand Down
Loading