-
Notifications
You must be signed in to change notification settings - Fork 4
Added Mac (M-series) support #351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
8444057
e57c2cc
963974e
a11def8
92b00ed
038e21e
df5efa7
5fc7160
da604be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ coverage.* | |
| build/ | ||
| dist/ | ||
| venvs/ | ||
| .DS_Store | ||
|
|
||
| # Produced by versioningit | ||
| src/con_duct/_version.py | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||||||||||||||||||||||||||||
| #!/usr/bin/env python3 | ||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||
| import collections | ||||||||||||||||||||||||||||||||||
CodyCBakerPhD marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||
| from collections import Counter | ||||||||||||||||||||||||||||||||||
| from collections.abc import Iterable, Iterator | ||||||||||||||||||||||||||||||||||
| from dataclasses import asdict, dataclass, field | ||||||||||||||||||||||||||||||||||
|
|
@@ -10,6 +11,7 @@ | |||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||
| import math | ||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||
| import platform | ||||||||||||||||||||||||||||||||||
| import re | ||||||||||||||||||||||||||||||||||
| import shutil | ||||||||||||||||||||||||||||||||||
| import signal | ||||||||||||||||||||||||||||||||||
|
|
@@ -20,11 +22,22 @@ | |||||||||||||||||||||||||||||||||
| import threading | ||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||
| from types import FrameType | ||||||||||||||||||||||||||||||||||
| from typing import IO, Any, Optional, TextIO | ||||||||||||||||||||||||||||||||||
| from typing import IO, Any, Callable, Optional, TextIO, Union | ||||||||||||||||||||||||||||||||||
| import warnings | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| __version__ = version("con-duct") | ||||||||||||||||||||||||||||||||||
| __schema_version__ = "0.2.2" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| is_mac_intel = sys.platform == "darwin" and os.uname().machine == "x86_64" | ||||||||||||||||||||||||||||||||||
| if is_mac_intel and not os.getenv("DUCT_IGNORE_INTEL_WARNING"): | ||||||||||||||||||||||||||||||||||
| message = ( | ||||||||||||||||||||||||||||||||||
| "Detected system macOS running on intel architecture - " | ||||||||||||||||||||||||||||||||||
| "duct may experience issues with sampling and signal handling.\n\n" | ||||||||||||||||||||||||||||||||||
| "Set the environment variable `DUCT_IGNORE_INTEL_WARNING` to suppress this warning.\n" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| warnings.warn(message=message, stacklevel=2) | ||||||||||||||||||||||||||||||||||
| SYSTEM = platform.system() | ||||||||||||||||||||||||||||||||||
| SKIPEMPTY_DEFAULT = True if SYSTEM == "Darwin" else False | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| lgr = logging.getLogger("con-duct") | ||||||||||||||||||||||||||||||||||
| DEFAULT_LOG_LEVEL = os.environ.get("DUCT_LOG_LEVEL", "INFO").upper() | ||||||||||||||||||||||||||||||||||
|
|
@@ -281,7 +294,15 @@ def add_pid(self, pid: int, stats: ProcessStats) -> None: | |||||||||||||||||||||||||||||||||
| self.stats[pid] = stats | ||||||||||||||||||||||||||||||||||
| self.timestamp = max(self.timestamp, stats.timestamp) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def aggregate(self: Sample, other: Sample) -> Sample: | ||||||||||||||||||||||||||||||||||
| def aggregate( | ||||||||||||||||||||||||||||||||||
| self: Sample, other: Sample, skipempty: bool = SKIPEMPTY_DEFAULT | ||||||||||||||||||||||||||||||||||
| ) -> Sample: | ||||||||||||||||||||||||||||||||||
| if skipempty and not other.stats and other.averages.num_samples == 0: | ||||||||||||||||||||||||||||||||||
| lgr.debug( | ||||||||||||||||||||||||||||||||||
| f"Other sample ({other=}) is empty during aggregation - returning the base sample." | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| return self | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| output = Sample() | ||||||||||||||||||||||||||||||||||
| for pid in self.stats.keys() | other.stats.keys(): | ||||||||||||||||||||||||||||||||||
| if (mine := self.stats.get(pid)) is not None: | ||||||||||||||||||||||||||||||||||
|
|
@@ -321,6 +342,128 @@ def for_json(self) -> dict[str, Any]: | |||||||||||||||||||||||||||||||||
| return d | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _get_sample_linux(session_id: int) -> Sample: | ||||||||||||||||||||||||||||||||||
| sample = Sample() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| ps_command = [ | ||||||||||||||||||||||||||||||||||
| "ps", | ||||||||||||||||||||||||||||||||||
| "-w", | ||||||||||||||||||||||||||||||||||
| "-s", | ||||||||||||||||||||||||||||||||||
| str(session_id), | ||||||||||||||||||||||||||||||||||
| "-o", | ||||||||||||||||||||||||||||||||||
| "pid,pcpu,pmem,rss,vsz,etime,stat,cmd", | ||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||
| output = subprocess.check_output(ps_command, text=True) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for line in output.splitlines()[1:]: | ||||||||||||||||||||||||||||||||||
| if not line: | ||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| pid, pcpu, pmem, rss_kib, vsz_kib, etime, stat, cmd = line.split(maxsplit=7) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| sample.add_pid( | ||||||||||||||||||||||||||||||||||
| pid=int(pid), | ||||||||||||||||||||||||||||||||||
| stats=ProcessStats( | ||||||||||||||||||||||||||||||||||
| pcpu=float(pcpu), | ||||||||||||||||||||||||||||||||||
| pmem=float(pmem), | ||||||||||||||||||||||||||||||||||
| rss=int(rss_kib) * 1024, | ||||||||||||||||||||||||||||||||||
| vsz=int(vsz_kib) * 1024, | ||||||||||||||||||||||||||||||||||
| timestamp=datetime.now().astimezone().isoformat(), | ||||||||||||||||||||||||||||||||||
| etime=etime, | ||||||||||||||||||||||||||||||||||
| stat=Counter([stat]), | ||||||||||||||||||||||||||||||||||
| cmd=cmd, | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| sample.averages = Averages.from_sample(sample=sample) | ||||||||||||||||||||||||||||||||||
| return sample | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _try_to_get_sid(pid: int) -> int: | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| It is possible that the `pid` returned by the top `ps` call no longer exists at time of `getsid` request. | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||
| return os.getsid(pid) | ||||||||||||||||||||||||||||||||||
| except Exception as exc: | ||||||||||||||||||||||||||||||||||
| lgr.debug(f"Error fetching session ID for PID {pid}: {str(exc)}") | ||||||||||||||||||||||||||||||||||
| return -1 | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _get_ps_lines_mac() -> list[str]: | ||||||||||||||||||||||||||||||||||
| ps_command = [ | ||||||||||||||||||||||||||||||||||
| "ps", | ||||||||||||||||||||||||||||||||||
| "-ax", | ||||||||||||||||||||||||||||||||||
| "-o", | ||||||||||||||||||||||||||||||||||
| "pid,pcpu,pmem,rss,vsz,etime,stat,args", | ||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||
| output = subprocess.check_output(ps_command, text=True) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| lines = [line for line in output.splitlines()[1:] if line] | ||||||||||||||||||||||||||||||||||
| return lines | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _add_sample_from_line_mac( | ||||||||||||||||||||||||||||||||||
| line: str, pid_to_matching_sid: dict[int, int], sample: Sample | ||||||||||||||||||||||||||||||||||
| ) -> Union[Sample, None]: | ||||||||||||||||||||||||||||||||||
| pid, pcpu, pmem, rss_kb, vsz_kb, etime, stat, cmd = line.split(maxsplit=7) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if pid_to_matching_sid.get(int(pid), None) is None: | ||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| sample.add_pid( | ||||||||||||||||||||||||||||||||||
| pid=int(pid), | ||||||||||||||||||||||||||||||||||
| stats=ProcessStats( | ||||||||||||||||||||||||||||||||||
| pcpu=float(pcpu), | ||||||||||||||||||||||||||||||||||
| pmem=float(pmem), | ||||||||||||||||||||||||||||||||||
| rss=int(rss_kb) * 1024, | ||||||||||||||||||||||||||||||||||
| vsz=int(vsz_kb) * 1024, | ||||||||||||||||||||||||||||||||||
| timestamp=datetime.now().astimezone().isoformat(), | ||||||||||||||||||||||||||||||||||
| etime=etime, | ||||||||||||||||||||||||||||||||||
| stat=Counter([stat]), | ||||||||||||||||||||||||||||||||||
| cmd=cmd, | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| return sample | ||||||||||||||||||||||||||||||||||
CodyCBakerPhD marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _get_sample_mac(session_id: int) -> Sample: | ||||||||||||||||||||||||||||||||||
| sample = Sample() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| lines = _get_ps_lines_mac() | ||||||||||||||||||||||||||||||||||
| pid_to_matching_sid = { | ||||||||||||||||||||||||||||||||||
| pid: sid | ||||||||||||||||||||||||||||||||||
| for line in lines | ||||||||||||||||||||||||||||||||||
| if (sid := _try_to_get_sid(pid=(pid := int(line.split(maxsplit=1)[0])))) | ||||||||||||||||||||||||||||||||||
| == session_id | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if not pid_to_matching_sid: | ||||||||||||||||||||||||||||||||||
| lgr.debug(f"No processes found for session ID {session_id}. ") | ||||||||||||||||||||||||||||||||||
CodyCBakerPhD marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| collections.deque( | ||||||||||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||||||||||
| _add_sample_from_line_mac( | ||||||||||||||||||||||||||||||||||
| line=line, pid_to_matching_sid=pid_to_matching_sid, sample=sample | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| for line in lines | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| maxlen=0, | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
CodyCBakerPhD marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||
| sample.averages = Averages.from_sample(sample=sample) | ||||||||||||||||||||||||||||||||||
| except AssertionError: | ||||||||||||||||||||||||||||||||||
| lgr.debug(f"Failed to compute averages for sample: {sample}") | ||||||||||||||||||||||||||||||||||
CodyCBakerPhD marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||
| return sample | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| _get_sample_per_system = { | ||||||||||||||||||||||||||||||||||
| "Linux": _get_sample_linux, | ||||||||||||||||||||||||||||||||||
| "Darwin": _get_sample_mac, | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| _get_sample: Callable[[int], Sample] = _get_sample_per_system[SYSTEM] # type: ignore[assignment] | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
Comment on lines
+463
to
+465
|
||||||||||||||||||||||||||||||||||
| _get_sample: Callable[[int], Sample] = _get_sample_per_system[SYSTEM] # type: ignore[assignment] | |
| def _get_sample(session_id: int) -> Sample: | |
| """Return a Sample for the given session_id, dispatching based on the current system. | |
| Raises a clear error on unsupported systems instead of failing with a KeyError | |
| at import time. | |
| """ | |
| try: | |
| backend = _get_sample_per_system[SYSTEM] | |
| except KeyError as exc: | |
| supported = ", ".join(sorted(_get_sample_per_system.keys())) | |
| raise RuntimeError( | |
| f"Unsupported system '{SYSTEM}'. Supported systems are: {supported}." | |
| ) from exc | |
| return backend(session_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving for @asmacdo decision - the current pattern was based on Yariks original suggestion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I have no issue with a KeyError right now. I'll add Windows support in the new year
Uh oh!
There was an error while loading. Please reload this page.