Skip to content
Draft
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
36 changes: 26 additions & 10 deletions src/iops_profiler/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ def __init__(self, shell):
# Pattern matches: B=0x[hex] in fs_usage output
self._fs_usage_byte_pattern = re.compile(FS_USAGE_BYTE_PATTERN)
# Set of syscall names for I/O operations (lowercase)
self._io_syscalls = set(STRACE_IO_SYSCALLS)
# Includes 32-bit variants (pread, pwrite) for completeness.
# Note: On 64-bit systems, these may not be traced (they don't exist),
# but including them in the parser is harmless - they simply won't appear in output.
self._io_syscalls = set(STRACE_IO_SYSCALLS + ["pread", "pwrite"])



@staticmethod
def parse_fs_usage_line_static(line, byte_pattern=None, collect_ops=False):
Expand Down Expand Up @@ -379,9 +384,9 @@ def measure_linux_strace(self, code, collect_ops=False):
# Create temporary file for strace output - we need the name, not file handle
output_file = tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False).name # noqa: SIM115

try:
# Start strace in the background
syscalls_to_trace = ",".join(STRACE_IO_SYSCALLS)
def _start_strace(syscalls_list):
"""Helper to start strace with given syscall list"""
syscalls_to_trace = ",".join(syscalls_list)
strace_cmd = [
"strace",
"-f", # Follow forks
Expand All @@ -392,14 +397,16 @@ def measure_linux_strace(self, code, collect_ops=False):
"-p",
str(pid),
]

# Start strace process
strace_proc = subprocess.Popen(
proc = subprocess.Popen(
strace_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)

# Give strace a moment to attach
time.sleep(STRACE_ATTACH_DELAY)
return proc

try:
# Try with all syscalls first, including 32-bit variants
syscalls_to_try = STRACE_IO_SYSCALLS + ["pread", "pwrite"]
strace_proc = _start_strace(syscalls_to_try)

# Check if strace started successfully
if strace_proc.poll() is not None:
Expand All @@ -408,7 +415,16 @@ def measure_linux_strace(self, code, collect_ops=False):
raise RuntimeError(
"strace failed - ptrace not permitted. This may be due to kernel security settings."
)
raise RuntimeError(f"Failed to start strace: {stderr}")
# If it failed due to invalid syscall, retry without 32-bit variants
if "invalid system call" in stderr:
strace_proc = _start_strace(STRACE_IO_SYSCALLS)
# Check if retry succeeded
if strace_proc.poll() is not None:
stdout, stderr = strace_proc.communicate()
raise RuntimeError(f"Failed to start strace after retry: {stderr}")
# Retry succeeded - continue with execution
else:
raise RuntimeError(f"Failed to start strace: {stderr}")

# Execute the code
start_time = time.time()
Expand Down
14 changes: 14 additions & 0 deletions tests/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ def test_pwrite64_operation(self, profiler):
assert op_type == "write"
assert bytes_transferred == 512

def test_pread_operation(self, profiler):
"""Test parsing a pread operation (32-bit)"""
line = '3385 pread(3, "...", 1024, 0) = 1024'
op_type, bytes_transferred = profiler.collector.parse_strace_line(line)
assert op_type == "read"
assert bytes_transferred == 1024

def test_pwrite_operation(self, profiler):
"""Test parsing a pwrite operation (32-bit)"""
line = '3385 pwrite(4, "data", 512, 1024) = 512'
op_type, bytes_transferred = profiler.collector.parse_strace_line(line)
assert op_type == "write"
assert bytes_transferred == 512

def test_readv_operation(self, profiler):
"""Test parsing a readv (vectored read) operation"""
line = '3385 readv(5, [{iov_base="...", iov_len=1024}], 1) = 1024'
Expand Down
Loading