Skip to content
Closed
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
35 changes: 27 additions & 8 deletions runpod/serverless/utils/rp_debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,43 @@

import datetime
from datetime import timezone
import functools
import platform
import time

import cpuinfo

# ---------------------------------------------------------------------------- #
# System Info #
# ---------------------------------------------------------------------------- #
OS_INFO = f"{platform.system()} {platform.release()}"

try:
PROCESSOR = cpuinfo.get_cpu_info()["brand_raw"]
except KeyError:
PROCESSOR = "Unable to get processor info."

PYTHON_VERSION = platform.python_version()


@functools.lru_cache(maxsize=1)
def _get_processor() -> str:
"""
Return the CPU brand string, probed on first use.

py-cpuinfo is slow on x86: it measures clock speed in a child process
around a fixed sleep(1). The worker imports this module on every start,
so probing here instead of at import keeps that second off cold starts
when the debugger is not enabled.
"""
import cpuinfo # pylint: disable=import-outside-toplevel

try:
return cpuinfo.get_cpu_info()["brand_raw"]
except KeyError:
return "Unable to get processor info."


def __getattr__(name: str):
"""Resolve PROCESSOR lazily for callers that read the module attribute."""
if name == "PROCESSOR":
return _get_processor()
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


class Checkpoints:
"""
A singleton class to store checkpoint times.
Expand Down Expand Up @@ -192,7 +211,7 @@ def get_debugger_output():

system_info = {
"os": OS_INFO,
"processor": PROCESSOR,
"processor": _get_processor(),
"python_version": PYTHON_VERSION,
"runpod": runpod.__version__,
}
Expand Down
76 changes: 71 additions & 5 deletions tests/test_serverless/test_utils/test_debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"""

import importlib
import subprocess
import sys
import time
import unittest
from unittest.mock import patch
Expand All @@ -23,18 +25,82 @@
def setUp(self):
self.checkpoints = Checkpoints()
self.checkpoints.clear()
rp_debugger._get_processor.cache_clear()

@patch("runpod.serverless.utils.rp_debugger.cpuinfo.get_cpu_info")
def tearDown(self):
rp_debugger._get_processor.cache_clear()

@patch("cpuinfo.get_cpu_info")
def test_import_does_not_probe_cpu(self, mock_get_cpu_info):
"""
Importing the module must not run py-cpuinfo; on x86 it sleeps 1s.
"""
importlib.reload(rp_debugger)

mock_get_cpu_info.assert_not_called()

def test_import_does_not_load_cpuinfo(self):
"""
Importing the module must not import py-cpuinfo, which spawns a subprocess at import.
"""
result = subprocess.run(
[
sys.executable,
"-c",
"import sys; import runpod.serverless.utils.rp_debugger; "
"print('cpuinfo' in sys.modules)",
Comment on lines +50 to +51
Comment on lines +50 to +51
],
capture_output=True,
text=True,
check=True,
)

self.assertEqual(result.stdout.strip(), "False")

@patch("cpuinfo.get_cpu_info")
def test_debugger_output_reports_processor(self, mock_get_cpu_info):
"""
The processor is looked up when debugger output is built.
"""
mock_get_cpu_info.return_value = {"brand_raw": "Test CPU"}

output = get_debugger_output()

self.assertEqual(output["system_info"]["processor"], "Test CPU")

@patch("cpuinfo.get_cpu_info")
def test_processor_lookup_is_cached(self, mock_get_cpu_info):
"""
The processor is probed once per process, not once per job.
"""
mock_get_cpu_info.return_value = {"brand_raw": "Test CPU"}

get_debugger_output()
get_debugger_output()

mock_get_cpu_info.assert_called_once()

@patch("cpuinfo.get_cpu_info")
def test_key_error(self, mock_get_cpu_info):
"""
Test that a KeyError is raised when an invalid key is used.
A missing brand string falls back to a placeholder.
"""
mock_get_cpu_info.side_effect = KeyError("Test Error")

importlib.reload(rp_debugger)
output = get_debugger_output()

self.assertEqual(
output["system_info"]["processor"], "Unable to get processor info."
)

@patch("cpuinfo.get_cpu_info")
def test_processor_attribute_is_lazy(self, mock_get_cpu_info):
"""
The PROCESSOR module attribute still resolves for existing callers.
"""
mock_get_cpu_info.return_value = {"brand_raw": "Test CPU"}

assert mock_get_cpu_info.called
self.assertEqual(rp_debugger.PROCESSOR, "Unable to get processor info.")
self.assertEqual(rp_debugger.PROCESSOR, "Test CPU")

def test_checkpoints(self):
"""
Expand Down
Loading