Skip to content

strace_manager

strace_manager

Strace management for the Marianne daemon profiler.

Attaches strace -c to child processes to collect per-syscall count and time-percentage summaries. Also supports on-demand full trace via strace -f -t -p PID -o <file> for deep debugging.

Handles gracefully: - strace not installed - Permission denied (non-root) - Target process already exited - Strace process cleanup on daemon shutdown

Strace processes are tracked so they can be cleaned up by ProcessGroupManager on daemon shutdown.

Classes

StraceManager

StraceManager(enabled=True)

Manages strace attachment to child processes.

Typical lifecycle::

mgr = StraceManager(enabled=True)
await mgr.attach(pid)        # spawns ``strace -c -p PID``
...                           # time passes, child does work
summary = await mgr.detach(pid)  # SIGINT -> parse summary
await mgr.detach_all()        # cleanup on shutdown
Source code in src/marianne/daemon/profiler/strace_manager.py
def __init__(self, enabled: bool = True) -> None:
    self._enabled = enabled
    # Maps target PID -> strace asyncio.subprocess.Process
    self._attached: dict[int, asyncio.subprocess.Process] = {}
    # Maps target PID -> full-trace asyncio.subprocess.Process
    self._full_traces: dict[int, asyncio.subprocess.Process] = {}
Attributes
attached_pids property
attached_pids

PIDs currently being traced.

Functions
is_available staticmethod
is_available()

Check whether strace is available on this system.

Source code in src/marianne/daemon/profiler/strace_manager.py
@staticmethod
def is_available() -> bool:
    """Check whether strace is available on this system."""
    return _strace_path is not None
attach async
attach(pid)

Attach strace -c -p <pid> for syscall summary collection.

Parameters:

Name Type Description Default
pid int

Target process PID to trace.

required

Returns:

Type Description
bool

True if strace was successfully spawned, False otherwise.

Source code in src/marianne/daemon/profiler/strace_manager.py
async def attach(self, pid: int) -> bool:
    """Attach ``strace -c -p <pid>`` for syscall summary collection.

    Args:
        pid: Target process PID to trace.

    Returns:
        True if strace was successfully spawned, False otherwise.
    """
    if not self._enabled:
        _logger.debug("strace_disabled", pid=pid)
        return False

    if _strace_path is None:
        _logger.warning("strace_not_available")
        return False

    if pid in self._attached:
        _logger.debug("strace_already_attached", pid=pid)
        return True

    try:
        proc = await asyncio.create_subprocess_exec(
            _strace_path,
            "-c",
            "-p",
            str(pid),
            "-e",
            "trace=all",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        self._attached[pid] = proc
        _logger.info("strace_attached", pid=pid, strace_pid=proc.pid)
        return True
    except PermissionError:
        _logger.warning("strace_permission_denied", pid=pid)
        return False
    except FileNotFoundError:
        _logger.warning("strace_not_found", pid=pid)
        return False
    except ProcessLookupError:
        _logger.debug("strace_target_already_exited", pid=pid)
        return False
    except OSError as exc:
        _logger.warning("strace_attach_failed", pid=pid, error=str(exc))
        return False
detach async
detach(pid)

Detach strace from a process and parse the summary output.

Sends SIGINT to the strace process (which causes it to print its -c summary table to stderr), then parses the output.

Parameters:

Name Type Description Default
pid int

Target process PID to stop tracing.

required

Returns:

Type Description
dict[str, Any] | None

Dict with syscall_counts and syscall_time_pct mappings,

dict[str, Any] | None

or None if the pid was not being traced.

Source code in src/marianne/daemon/profiler/strace_manager.py
async def detach(self, pid: int) -> dict[str, Any] | None:
    """Detach strace from a process and parse the summary output.

    Sends SIGINT to the strace process (which causes it to print its
    ``-c`` summary table to stderr), then parses the output.

    Args:
        pid: Target process PID to stop tracing.

    Returns:
        Dict with ``syscall_counts`` and ``syscall_time_pct`` mappings,
        or None if the pid was not being traced.
    """
    proc = self._attached.pop(pid, None)
    if proc is None:
        return None

    # Send SIGINT to strace so it prints the summary
    try:
        if proc.returncode is None:
            proc.send_signal(signal.SIGINT)
    except (ProcessLookupError, OSError):
        # strace already exited
        pass

    try:
        _, stderr = await asyncio.wait_for(proc.communicate(), timeout=5)
    except TimeoutError:
        _logger.warning("strace_detach_timeout", pid=pid)
        try:
            proc.kill()
            await proc.wait()
        except (ProcessLookupError, OSError):
            pass
        return None

    if not stderr:
        return None

    output = stderr.decode(errors="replace")
    _logger.debug("strace_output_received", pid=pid, output_len=len(output))
    return self._parse_strace_summary(output)
attach_full_trace async
attach_full_trace(pid, output_file)

Attach a full strace (strace -f -t -p PID -o file).

This is the on-demand deep-trace triggered by mzt top --trace PID.

Parameters:

Name Type Description Default
pid int

Target process PID.

required
output_file Path

Path to write the full trace output.

required

Returns:

Type Description
bool

True if strace was successfully spawned, False otherwise.

Source code in src/marianne/daemon/profiler/strace_manager.py
async def attach_full_trace(self, pid: int, output_file: Path) -> bool:
    """Attach a full strace (``strace -f -t -p PID -o file``).

    This is the on-demand deep-trace triggered by ``mzt top --trace PID``.

    Args:
        pid: Target process PID.
        output_file: Path to write the full trace output.

    Returns:
        True if strace was successfully spawned, False otherwise.
    """
    if not self._enabled:
        return False

    if _strace_path is None:
        _logger.warning("strace_not_available_full")
        return False

    if pid in self._full_traces:
        _logger.debug("full_trace_already_attached", pid=pid)
        return True

    output_file.parent.mkdir(parents=True, exist_ok=True)

    try:
        proc = await asyncio.create_subprocess_exec(
            _strace_path,
            "-f",
            "-t",
            "-p",
            str(pid),
            "-o",
            str(output_file),
            stdout=asyncio.subprocess.DEVNULL,
            stderr=asyncio.subprocess.PIPE,
        )
        self._full_traces[pid] = proc
        _logger.info(
            "full_trace_attached",
            pid=pid,
            strace_pid=proc.pid,
            output_file=str(output_file),
        )
        return True
    except PermissionError:
        _logger.warning("full_trace_permission_denied", pid=pid)
        return False
    except FileNotFoundError:
        _logger.warning("full_trace_strace_not_found", pid=pid)
        return False
    except ProcessLookupError:
        _logger.debug("full_trace_target_exited", pid=pid)
        return False
    except OSError as exc:
        _logger.warning("full_trace_attach_failed", pid=pid, error=str(exc))
        return False
detach_all async
detach_all()

Detach and terminate all strace processes.

Called during daemon shutdown for cleanup.

Source code in src/marianne/daemon/profiler/strace_manager.py
async def detach_all(self) -> None:
    """Detach and terminate all strace processes.

    Called during daemon shutdown for cleanup.
    """
    all_procs: list[tuple[int, asyncio.subprocess.Process]] = []
    all_procs.extend(self._attached.items())
    all_procs.extend(self._full_traces.items())

    self._attached.clear()
    self._full_traces.clear()

    for item in all_procs:
        proc = item[1]
        try:
            if proc.returncode is None:
                proc.send_signal(signal.SIGINT)
        except (ProcessLookupError, OSError):
            continue

    # Give them a moment to exit cleanly, then force-kill
    for item in all_procs:
        proc = item[1]
        try:
            await asyncio.wait_for(proc.wait(), timeout=2)
        except TimeoutError:
            try:
                proc.kill()
                await proc.wait()
            except (ProcessLookupError, OSError):
                pass
        except (ProcessLookupError, OSError):
            pass

    if all_procs:
        _logger.info("strace_detach_all", count=len(all_procs))
get_strace_pids
get_strace_pids()

Return PIDs of all running strace subprocesses.

Useful for registering with ProcessGroupManager so they get cleaned up on daemon shutdown.

Source code in src/marianne/daemon/profiler/strace_manager.py
def get_strace_pids(self) -> list[int]:
    """Return PIDs of all running strace subprocesses.

    Useful for registering with ProcessGroupManager so they
    get cleaned up on daemon shutdown.
    """
    pids: list[int] = []
    for proc in self._attached.values():
        if proc.pid is not None and proc.returncode is None:
            pids.append(proc.pid)
    for proc in self._full_traces.values():
        if proc.pid is not None and proc.returncode is None:
            pids.append(proc.pid)
    return pids

Functions