chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -0,0 +1,377 @@
"""Unit tests for the GPU profiler manager.
All GPU and dynolog dependencies are mocked out.
This test just verifies that commands are launched correctly and that
validations are correctly performed.
"""
import asyncio
import sys
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
import pytest
from ray.dashboard.modules.reporter.gpu_profile_manager import GpuProfilingManager
@pytest.fixture
def mock_node_has_gpus(monkeypatch):
monkeypatch.setattr(GpuProfilingManager, "node_has_gpus", lambda cls: True)
yield
@pytest.fixture
def mock_dynolog_binaries(monkeypatch):
monkeypatch.setattr("shutil.which", lambda cmd: f"/usr/bin/fake_{cmd}")
yield
@pytest.fixture
def mock_subprocess_popen(monkeypatch):
mock_popen = MagicMock()
mock_proc = MagicMock()
mock_popen.return_value = mock_proc
monkeypatch.setattr("subprocess.Popen", mock_popen)
yield (mock_popen, mock_proc)
LOCALHOST = "127.0.0.1"
@pytest.fixture
def mock_asyncio_create_subprocess_exec(monkeypatch):
mock_create_subprocess_exec = AsyncMock()
mock_async_proc = mock_create_subprocess_exec.return_value = AsyncMock()
mock_async_proc.communicate.return_value = b"mock stdout", b"mock stderr"
mock_async_proc.returncode = 0
monkeypatch.setattr("asyncio.create_subprocess_exec", mock_create_subprocess_exec)
yield (mock_create_subprocess_exec, mock_async_proc)
def test_node_has_gpus_uses_query_gpu_flag(tmp_path, monkeypatch):
"""node_has_gpus() must use --query-gpu to avoid FabricManager stalls."""
captured = {}
def fake_check_output(cmd, **kwargs):
captured["cmd"] = list(cmd)
return b""
monkeypatch.setattr("subprocess.check_output", fake_check_output)
# Clear the cache so the monkeypatched check_output is actually called.
GpuProfilingManager.node_has_gpus.cache_clear()
result = GpuProfilingManager.node_has_gpus()
GpuProfilingManager.node_has_gpus.cache_clear()
assert result is True
assert captured["cmd"] == [
"nvidia-smi",
"--query-gpu=name",
"--format=csv,noheader",
]
def test_enabled_does_not_call_node_has_gpus_when_dynolog_missing(
tmp_path, monkeypatch
):
"""enabled must short-circuit on missing dynolog bins before node_has_gpus()."""
node_has_gpus_called = []
def spy_node_has_gpus(self_or_cls):
node_has_gpus_called.append(True)
return True
monkeypatch.setattr(GpuProfilingManager, "node_has_gpus", spy_node_has_gpus)
# No dynolog binaries on PATH → shutil.which returns None for both.
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
assert not gpu_profiler.enabled
assert (
not node_has_gpus_called
), "node_has_gpus() must not be called when dynolog binaries are absent"
def test_enabled(tmp_path, mock_node_has_gpus, mock_dynolog_binaries):
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
assert gpu_profiler.enabled
def test_disabled_no_gpus(tmp_path, monkeypatch):
monkeypatch.setattr(
GpuProfilingManager, "node_has_gpus", classmethod(lambda cls: False)
)
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
assert not gpu_profiler.enabled
def test_disabled_no_dynolog_bin(tmp_path, mock_node_has_gpus):
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
assert not gpu_profiler.enabled
def test_start_monitoring_daemon(
tmp_path, mock_node_has_gpus, mock_dynolog_binaries, mock_subprocess_popen
):
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
mocked_popen, mocked_proc = mock_subprocess_popen
mocked_proc.pid = 123
mocked_proc.poll.return_value = None
gpu_profiler.start_monitoring_daemon()
assert gpu_profiler.is_monitoring_daemon_running
assert mocked_popen.call_count == 1
assert mocked_popen.call_args[0][0] == [
"/usr/bin/fake_dynolog",
"--enable_ipc_monitor",
"--port",
str(gpu_profiler._DYNOLOG_PORT),
]
# "Terminate" the daemon
mocked_proc.poll.return_value = 0
assert not gpu_profiler.is_monitoring_daemon_running
@pytest.mark.asyncio
async def test_gpu_profile_disabled(tmp_path):
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
assert not gpu_profiler.enabled
success, output = await gpu_profiler.gpu_profile(pid=123, num_iterations=1)
assert not success
assert output == gpu_profiler._DISABLED_ERROR_MESSAGE.format(
ip_address=gpu_profiler._ip_address
)
@pytest.mark.asyncio
async def test_gpu_profile_without_starting_daemon(
tmp_path, mock_node_has_gpus, mock_dynolog_binaries
):
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
assert not gpu_profiler.is_monitoring_daemon_running
with pytest.raises(RuntimeError, match="start_monitoring_daemon"):
await gpu_profiler.gpu_profile(pid=123, num_iterations=1)
@pytest.mark.asyncio
async def test_gpu_profile_with_dead_daemon(
tmp_path, mock_node_has_gpus, mock_dynolog_binaries, mock_subprocess_popen
):
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
gpu_profiler.start_monitoring_daemon()
mocked_popen, mocked_proc = mock_subprocess_popen
mocked_proc.pid = 123
# "Terminate" the daemon
mocked_proc.poll.return_value = 0
assert not gpu_profiler.is_monitoring_daemon_running
success, output = await gpu_profiler.gpu_profile(pid=456, num_iterations=1)
assert not success
print(output)
assert "GPU monitoring daemon" in output
@pytest.mark.asyncio
async def test_gpu_profile_on_dead_process(
tmp_path,
monkeypatch,
mock_node_has_gpus,
mock_dynolog_binaries,
mock_subprocess_popen,
):
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
gpu_profiler.start_monitoring_daemon()
_, mocked_proc = mock_subprocess_popen
mocked_proc.pid = 123
mocked_proc.poll.return_value = None
monkeypatch.setattr(GpuProfilingManager, "is_pid_alive", lambda cls, pid: False)
success, output = await gpu_profiler.gpu_profile(pid=456, num_iterations=1)
assert not success
assert output == gpu_profiler._DEAD_PROCESS_ERROR_MESSAGE.format(
pid=456, ip_address=gpu_profiler._ip_address
)
@pytest.mark.asyncio
async def test_gpu_profile_no_matched_processes(
tmp_path,
monkeypatch,
mock_node_has_gpus,
mock_dynolog_binaries,
mock_subprocess_popen,
mock_asyncio_create_subprocess_exec,
):
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
gpu_profiler.start_monitoring_daemon()
# Mock the daemon process
_, mocked_daemon_proc = mock_subprocess_popen
mocked_daemon_proc.pid = 123
mocked_daemon_proc.poll.return_value = None
monkeypatch.setattr(GpuProfilingManager, "is_pid_alive", lambda cls, pid: True)
# Mock the asyncio.create_subprocess_exec
(
mocked_create_subprocess_exec,
mocked_async_proc,
) = mock_asyncio_create_subprocess_exec
mocked_async_proc.communicate.return_value = (
f"{gpu_profiler._NO_PROCESSES_MATCHED_ERROR_MESSAGE_PREFIX}".encode(),
b"dummy stderr",
)
process_pid = 456
num_iterations = 1
success, output = await gpu_profiler.gpu_profile(
pid=process_pid, num_iterations=num_iterations
)
assert mocked_create_subprocess_exec.call_count == 1
assert not success
assert output == gpu_profiler._NO_PROCESSES_MATCHED_ERROR_MESSAGE.format(
pid=process_pid, ip_address=gpu_profiler._ip_address
)
@pytest.mark.asyncio
async def test_gpu_profile_timeout(
tmp_path,
monkeypatch,
mock_node_has_gpus,
mock_dynolog_binaries,
mock_subprocess_popen,
mock_asyncio_create_subprocess_exec,
):
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
gpu_profiler.start_monitoring_daemon()
# Mock the daemon process
_, mocked_daemon_proc = mock_subprocess_popen
mocked_daemon_proc.pid = 123
mocked_daemon_proc.poll.return_value = None
monkeypatch.setattr(GpuProfilingManager, "is_pid_alive", lambda cls, pid: True)
process_pid = 456
num_iterations = 1
task = asyncio.create_task(
gpu_profiler.gpu_profile(
pid=process_pid, num_iterations=num_iterations, _timeout_s=0.1
)
)
await asyncio.sleep(0.2)
success, output = await task
assert not success
assert "timed out" in output
@pytest.mark.asyncio
async def test_gpu_profile_process_dies_during_profiling(
tmp_path,
monkeypatch,
mock_node_has_gpus,
mock_dynolog_binaries,
mock_subprocess_popen,
mock_asyncio_create_subprocess_exec,
):
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
gpu_profiler.start_monitoring_daemon()
# Mock the daemon process
_, mocked_daemon_proc = mock_subprocess_popen
mocked_daemon_proc.pid = 123
mocked_daemon_proc.poll.return_value = None
monkeypatch.setattr(GpuProfilingManager, "is_pid_alive", lambda cls, pid: True)
process_pid = 456
num_iterations = 1
task = asyncio.create_task(
gpu_profiler.gpu_profile(pid=process_pid, num_iterations=num_iterations)
)
monkeypatch.setattr(GpuProfilingManager, "is_pid_alive", lambda cls, pid: False)
await asyncio.sleep(0.2)
success, output = await task
assert not success
assert output == gpu_profiler._DEAD_PROCESS_ERROR_MESSAGE.format(
pid=process_pid, ip_address=gpu_profiler._ip_address
)
@pytest.mark.asyncio
async def test_gpu_profile_success(
tmp_path,
monkeypatch,
mock_node_has_gpus,
mock_dynolog_binaries,
mock_subprocess_popen,
mock_asyncio_create_subprocess_exec,
):
gpu_profiler = GpuProfilingManager(tmp_path, ip_address=LOCALHOST)
gpu_profiler.start_monitoring_daemon()
# Mock the daemon process
_, mocked_daemon_proc = mock_subprocess_popen
mocked_daemon_proc.pid = 123
mocked_daemon_proc.poll.return_value = None
monkeypatch.setattr(GpuProfilingManager, "is_pid_alive", lambda cls, pid: True)
monkeypatch.setattr(
GpuProfilingManager, "_get_trace_filename", lambda cls: "dummy_trace.json"
)
dumped_trace_filepath = gpu_profiler._profile_dir_path / "dummy_trace.json"
dumped_trace_filepath.touch()
# Mock the asyncio.create_subprocess_exec
(
mocked_create_subprocess_exec,
mocked_async_proc,
) = mock_asyncio_create_subprocess_exec
process_pid = 456
num_iterations = 1
success, output = await gpu_profiler.gpu_profile(
pid=process_pid, num_iterations=num_iterations
)
# Verify the command was launched correctly
assert mocked_create_subprocess_exec.call_count == 1
profile_launch_args = list(mocked_create_subprocess_exec.call_args[0])
assert profile_launch_args[:6] == [
"/usr/bin/fake_dyno",
"--port",
str(gpu_profiler._DYNOLOG_PORT),
"gputrace",
"--pids",
str(process_pid),
]
assert "--log-file" in profile_launch_args
profile_log_file_arg = profile_launch_args[
profile_launch_args.index("--log-file") + 1
]
assert Path(profile_log_file_arg).is_relative_to(tmp_path)
assert "--iterations" in profile_launch_args
assert profile_launch_args[profile_launch_args.index("--iterations") + 1] == str(
num_iterations
)
assert success
assert output == str(dumped_trace_filepath.relative_to(tmp_path))
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,629 @@
"""Unit tests for GPU providers."""
import unittest
from unittest.mock import Mock, patch
from ray.dashboard.modules.reporter.gpu_providers import (
MB,
AmdGpuProvider,
GpuMetricProvider,
GpuProvider,
GpuProviderType,
GpuUtilizationInfo,
NvidiaGpuProvider,
ProcessGPUInfo,
)
class TestProcessGPUInfo(unittest.TestCase):
"""Test ProcessGPUInfo TypedDict."""
def test_creation(self):
"""Test ProcessGPUInfo creation."""
process_info = ProcessGPUInfo(
pid=1234, gpu_memory_usage=256, gpu_utilization=None
)
self.assertEqual(process_info["pid"], 1234)
self.assertEqual(process_info["gpu_memory_usage"], 256)
self.assertIsNone(process_info["gpu_utilization"])
class TestGpuUtilizationInfo(unittest.TestCase):
"""Test GpuUtilizationInfo TypedDict."""
def test_creation_with_processes(self):
"""Test GpuUtilizationInfo with process information."""
process1 = ProcessGPUInfo(pid=1234, gpu_memory_usage=256, gpu_utilization=None)
process2 = ProcessGPUInfo(pid=5678, gpu_memory_usage=512, gpu_utilization=None)
gpu_info = GpuUtilizationInfo(
index=0,
name="NVIDIA GeForce RTX 3080",
uuid="GPU-12345678-1234-1234-1234-123456789abc",
utilization_gpu=75,
memory_used=8192,
memory_total=10240,
processes_pids={1234: process1, 5678: process2},
)
self.assertEqual(gpu_info["index"], 0)
self.assertEqual(gpu_info["name"], "NVIDIA GeForce RTX 3080")
self.assertEqual(gpu_info["uuid"], "GPU-12345678-1234-1234-1234-123456789abc")
self.assertEqual(gpu_info["utilization_gpu"], 75)
self.assertEqual(gpu_info["memory_used"], 8192)
self.assertEqual(gpu_info["memory_total"], 10240)
self.assertEqual(len(gpu_info["processes_pids"]), 2)
self.assertIn(1234, gpu_info["processes_pids"])
self.assertIn(5678, gpu_info["processes_pids"])
self.assertEqual(gpu_info["processes_pids"][1234]["pid"], 1234)
self.assertEqual(gpu_info["processes_pids"][1234]["gpu_memory_usage"], 256)
self.assertEqual(gpu_info["processes_pids"][5678]["pid"], 5678)
self.assertEqual(gpu_info["processes_pids"][5678]["gpu_memory_usage"], 512)
def test_creation_without_processes(self):
"""Test GpuUtilizationInfo without process information."""
gpu_info = GpuUtilizationInfo(
index=1,
name="AMD Radeon RX 6800 XT",
uuid="GPU-87654321-4321-4321-4321-ba9876543210",
utilization_gpu=None,
memory_used=4096,
memory_total=16384,
processes_pids=None,
)
self.assertEqual(gpu_info["index"], 1)
self.assertEqual(gpu_info["name"], "AMD Radeon RX 6800 XT")
self.assertEqual(gpu_info["uuid"], "GPU-87654321-4321-4321-4321-ba9876543210")
self.assertIsNone(gpu_info["utilization_gpu"]) # Should be None, not -1
self.assertEqual(gpu_info["memory_used"], 4096)
self.assertEqual(gpu_info["memory_total"], 16384)
self.assertIsNone(gpu_info["processes_pids"]) # Should be None, not []
class TestGpuProvider(unittest.TestCase):
"""Test abstract GpuProvider class."""
def test_decode_bytes(self):
"""Test _decode method with bytes input."""
result = GpuProvider._decode(b"test string")
self.assertEqual(result, "test string")
def test_decode_string(self):
"""Test _decode method with string input."""
result = GpuProvider._decode("test string")
self.assertEqual(result, "test string")
def test_abstract_methods_not_implemented(self):
"""Test that abstract methods raise NotImplementedError."""
class IncompleteProvider(GpuProvider):
pass
with self.assertRaises(TypeError):
IncompleteProvider()
class TestNvidiaGpuProvider(unittest.TestCase):
"""Test NvidiaGpuProvider class."""
def setUp(self):
"""Set up test fixtures."""
self.provider = NvidiaGpuProvider()
def test_get_provider_name(self):
"""Test provider name."""
self.assertEqual(self.provider.get_provider_name(), GpuProviderType.NVIDIA)
@patch("ray._private.thirdparty.pynvml", create=True)
def test_is_available_success(self, mock_pynvml):
"""Test is_available when NVIDIA GPU is available."""
mock_pynvml.nvmlInit.return_value = None
mock_pynvml.nvmlShutdown.return_value = None
# Mock sys.modules to make the import work
import sys
original_modules = sys.modules.copy()
sys.modules["ray._private.thirdparty.pynvml"] = mock_pynvml
try:
self.assertTrue(self.provider.is_available())
mock_pynvml.nvmlInit.assert_called_once()
mock_pynvml.nvmlShutdown.assert_called_once()
finally:
# Restore original modules
sys.modules.clear()
sys.modules.update(original_modules)
@patch("ray._private.thirdparty.pynvml", create=True)
def test_is_available_failure(self, mock_pynvml):
"""Test is_available when NVIDIA GPU is not available."""
mock_pynvml.nvmlInit.side_effect = Exception("NVIDIA driver not found")
# Mock sys.modules to make the import work but nvmlInit fail
import sys
original_modules = sys.modules.copy()
sys.modules["ray._private.thirdparty.pynvml"] = mock_pynvml
try:
self.assertFalse(self.provider.is_available())
finally:
# Restore original modules
sys.modules.clear()
sys.modules.update(original_modules)
@patch("ray._private.thirdparty.pynvml", create=True)
def test_initialize_success(self, mock_pynvml):
"""Test successful initialization."""
# Ensure provider starts fresh
self.provider._initialized = False
mock_pynvml.nvmlInit.return_value = None
# Mock sys.modules to make the import work
import sys
original_modules = sys.modules.copy()
sys.modules["ray._private.thirdparty.pynvml"] = mock_pynvml
try:
self.assertTrue(self.provider._initialize())
self.assertTrue(self.provider._initialized)
mock_pynvml.nvmlInit.assert_called_once()
finally:
# Restore original modules
sys.modules.clear()
sys.modules.update(original_modules)
@patch("ray._private.thirdparty.pynvml", create=True)
def test_initialize_failure(self, mock_pynvml):
"""Test failed initialization."""
# Ensure provider starts fresh
self.provider._initialized = False
# Make nvmlInit fail
mock_pynvml.nvmlInit.side_effect = Exception("Initialization failed")
# Mock sys.modules to make the import work but nvmlInit fail
import sys
original_modules = sys.modules.copy()
sys.modules["ray._private.thirdparty.pynvml"] = mock_pynvml
try:
self.assertFalse(self.provider._initialize())
self.assertFalse(self.provider._initialized)
finally:
# Restore original modules
sys.modules.clear()
sys.modules.update(original_modules)
@patch("ray._private.thirdparty.pynvml", create=True)
def test_initialize_already_initialized(self, mock_pynvml):
"""Test initialization when already initialized."""
self.provider._initialized = True
self.assertTrue(self.provider._initialize())
mock_pynvml.nvmlInit.assert_not_called()
@patch("ray._private.thirdparty.pynvml", create=True)
def test_shutdown(self, mock_pynvml):
"""Test shutdown."""
self.provider._initialized = True
self.provider._pynvml = mock_pynvml
self.provider._shutdown()
self.assertFalse(self.provider._initialized)
mock_pynvml.nvmlShutdown.assert_called_once()
@patch("ray._private.thirdparty.pynvml", create=True)
def test_shutdown_not_initialized(self, mock_pynvml):
"""Test shutdown when not initialized."""
self.provider._shutdown()
mock_pynvml.nvmlShutdown.assert_not_called()
@patch("ray._private.thirdparty.pynvml", create=True)
def test_get_gpu_utilization_success(self, mock_pynvml):
"""Test successful GPU utilization retrieval."""
# Mock GPU device
mock_handle = Mock()
mock_memory_info = Mock()
mock_memory_info.used = 8 * MB * 1024 # 8GB used
mock_memory_info.total = 12 * MB * 1024 # 12GB total
mock_utilization_info = Mock()
mock_utilization_info.gpu = 75
mock_process = Mock()
mock_process.pid = 1234
mock_process.usedGpuMemory = 256 * MB
# Configure mocks
mock_pynvml.nvmlInit.return_value = None
mock_pynvml.nvmlDeviceGetCount.return_value = 1
mock_pynvml.nvmlDeviceGetHandleByIndex.return_value = mock_handle
mock_pynvml.nvmlDeviceGetMemoryInfo.return_value = mock_memory_info
mock_pynvml.nvmlDeviceGetUtilizationRates.return_value = mock_utilization_info
mock_pynvml.nvmlDeviceGetComputeRunningProcesses.return_value = [mock_process]
mock_pynvml.nvmlDeviceGetGraphicsRunningProcesses.return_value = []
mock_pynvml.nvmlDeviceGetName.return_value = b"NVIDIA GeForce RTX 3080"
mock_pynvml.nvmlDeviceGetUUID.return_value = (
b"GPU-12345678-1234-1234-1234-123456789abc"
)
mock_pynvml.nvmlShutdown.return_value = None
# Set up provider state
self.provider._pynvml = mock_pynvml
self.provider._initialized = True
result = self.provider.get_gpu_utilization()
self.assertEqual(len(result), 1)
gpu_info = result[0]
self.assertEqual(gpu_info["index"], 0)
self.assertEqual(gpu_info["name"], "NVIDIA GeForce RTX 3080")
self.assertEqual(gpu_info["uuid"], "GPU-12345678-1234-1234-1234-123456789abc")
self.assertEqual(gpu_info["utilization_gpu"], 75)
self.assertEqual(gpu_info["memory_used"], 8 * 1024) # 8GB in MB
self.assertEqual(gpu_info["memory_total"], 12 * 1024) # 12GB in MB
self.assertEqual(len(gpu_info["processes_pids"]), 1)
self.assertEqual(gpu_info["processes_pids"][1234]["pid"], 1234)
self.assertEqual(gpu_info["processes_pids"][1234]["gpu_memory_usage"], 256)
@patch("ray._private.thirdparty.pynvml", create=True)
def test_get_gpu_utilization_with_errors(self, mock_pynvml):
"""Test GPU utilization retrieval with partial errors."""
mock_handle = Mock()
mock_memory_info = Mock()
mock_memory_info.used = 4 * MB * 1024
mock_memory_info.total = 8 * MB * 1024
# Create mock NVML error class
class MockNVMLError(Exception):
pass
mock_pynvml.NVMLError = MockNVMLError
# Configure mocks with some failures
mock_pynvml.nvmlInit.return_value = None
mock_pynvml.nvmlDeviceGetCount.return_value = 1
mock_pynvml.nvmlDeviceGetHandleByIndex.return_value = mock_handle
mock_pynvml.nvmlDeviceGetMemoryInfo.return_value = mock_memory_info
mock_pynvml.nvmlDeviceGetUtilizationRates.side_effect = MockNVMLError(
"Utilization not available"
)
mock_pynvml.nvmlDeviceGetComputeRunningProcesses.side_effect = MockNVMLError(
"Process info not available"
)
mock_pynvml.nvmlDeviceGetGraphicsRunningProcesses.side_effect = MockNVMLError(
"Process info not available"
)
mock_pynvml.nvmlDeviceGetName.return_value = b"NVIDIA Tesla V100"
mock_pynvml.nvmlDeviceGetUUID.return_value = (
b"GPU-87654321-4321-4321-4321-ba9876543210"
)
mock_pynvml.nvmlShutdown.return_value = None
# Set up provider state
self.provider._pynvml = mock_pynvml
self.provider._initialized = True
result = self.provider.get_gpu_utilization()
self.assertEqual(len(result), 1)
gpu_info = result[0]
self.assertEqual(gpu_info["index"], 0)
self.assertEqual(gpu_info["name"], "NVIDIA Tesla V100")
self.assertEqual(gpu_info["utilization_gpu"], -1) # Should be -1 due to error
self.assertEqual(
gpu_info["processes_pids"], {}
) # Should be empty dict due to error
@patch("ray._private.thirdparty.pynvml", create=True)
def test_get_gpu_utilization_with_mig(self, mock_pynvml):
"""Test GPU utilization retrieval with MIG devices."""
# Mock regular GPU handle
mock_gpu_handle = Mock()
mock_memory_info = Mock()
mock_memory_info.used = 4 * MB * 1024
mock_memory_info.total = 8 * MB * 1024
# Mock MIG device handle and info
mock_mig_handle = Mock()
mock_mig_memory_info = Mock()
mock_mig_memory_info.used = 2 * MB * 1024
mock_mig_memory_info.total = 4 * MB * 1024
mock_mig_utilization_info = Mock()
mock_mig_utilization_info.gpu = 80
# Configure mocks for MIG-enabled GPU
mock_pynvml.nvmlInit.return_value = None
mock_pynvml.nvmlDeviceGetCount.return_value = 1
mock_pynvml.nvmlDeviceGetHandleByIndex.return_value = mock_gpu_handle
# MIG mode enabled
mock_pynvml.nvmlDeviceGetMigMode.return_value = (
True,
True,
) # (current, pending)
mock_pynvml.nvmlDeviceGetMaxMigDeviceCount.return_value = 1 # Only 1 MIG device
mock_pynvml.nvmlDeviceGetMigDeviceHandleByIndex.return_value = mock_mig_handle
# MIG device info
mock_pynvml.nvmlDeviceGetMemoryInfo.return_value = mock_mig_memory_info
mock_pynvml.nvmlDeviceGetUtilizationRates.return_value = (
mock_mig_utilization_info
)
mock_pynvml.nvmlDeviceGetComputeRunningProcesses.return_value = []
mock_pynvml.nvmlDeviceGetGraphicsRunningProcesses.return_value = []
mock_pynvml.nvmlDeviceGetName.return_value = b"NVIDIA A100-SXM4-40GB MIG 1g.5gb"
mock_pynvml.nvmlDeviceGetUUID.return_value = (
b"MIG-12345678-1234-1234-1234-123456789abc"
)
mock_pynvml.nvmlShutdown.return_value = None
# Set up provider state
self.provider._pynvml = mock_pynvml
self.provider._initialized = True
result = self.provider.get_gpu_utilization()
# Should return MIG device info instead of regular GPU
self.assertEqual(
len(result), 1
) # Only one MIG device due to exception handling
gpu_info = result[0]
self.assertEqual(gpu_info["index"], 0) # First MIG device (0 * 1000 + 0)
self.assertEqual(gpu_info["name"], "NVIDIA A100-SXM4-40GB MIG 1g.5gb")
self.assertEqual(gpu_info["uuid"], "MIG-12345678-1234-1234-1234-123456789abc")
self.assertEqual(gpu_info["utilization_gpu"], 80)
self.assertEqual(gpu_info["memory_used"], 2 * 1024) # 2GB in MB
self.assertEqual(gpu_info["memory_total"], 4 * 1024) # 4GB in MB
self.assertEqual(gpu_info["processes_pids"], {})
class TestAmdGpuProvider(unittest.TestCase):
"""Test AmdGpuProvider class."""
def setUp(self):
"""Set up test fixtures."""
self.provider = AmdGpuProvider()
def test_get_provider_name(self):
"""Test provider name."""
self.assertEqual(self.provider.get_provider_name(), GpuProviderType.AMD)
@patch("ray._private.thirdparty.pyamdsmi", create=True)
def test_is_available_success(self, mock_pyamdsmi):
"""Test is_available when AMD GPU is available."""
mock_pyamdsmi.smi_initialize.return_value = None
mock_pyamdsmi.smi_shutdown.return_value = None
self.assertTrue(self.provider.is_available())
mock_pyamdsmi.smi_initialize.assert_called_once()
mock_pyamdsmi.smi_shutdown.assert_called_once()
@patch("ray._private.thirdparty.pyamdsmi", create=True)
def test_is_available_failure(self, mock_pyamdsmi):
"""Test is_available when AMD GPU is not available."""
mock_pyamdsmi.smi_initialize.side_effect = Exception("AMD driver not found")
self.assertFalse(self.provider.is_available())
@patch("ray._private.thirdparty.pyamdsmi", create=True)
def test_initialize_success(self, mock_pyamdsmi):
"""Test successful initialization."""
mock_pyamdsmi.smi_initialize.return_value = None
self.assertTrue(self.provider._initialize())
self.assertTrue(self.provider._initialized)
mock_pyamdsmi.smi_initialize.assert_called_once()
@patch("ray._private.thirdparty.pyamdsmi", create=True)
def test_get_gpu_utilization_success(self, mock_pyamdsmi):
"""Test successful GPU utilization retrieval."""
mock_process = Mock()
mock_process.process_id = 5678
mock_process.vram_usage = 512 * MB
# Configure mocks
mock_pyamdsmi.smi_initialize.return_value = None
mock_pyamdsmi.smi_get_device_count.return_value = 1
mock_pyamdsmi.smi_get_device_id.return_value = "device_0"
mock_pyamdsmi.smi_get_device_utilization.return_value = 85
mock_pyamdsmi.smi_get_device_compute_process.return_value = [mock_process]
mock_pyamdsmi.smi_get_compute_process_info_by_device.return_value = [
mock_process
]
mock_pyamdsmi.smi_get_device_name.return_value = b"AMD Radeon RX 6800 XT"
mock_pyamdsmi.smi_get_device_unique_id.return_value = (
"GPU-13579bdf-9abc-def0-0000-000000000000"
)
mock_pyamdsmi.smi_get_device_memory_used.return_value = 6 * MB * 1024
mock_pyamdsmi.smi_get_device_memory_total.return_value = 16 * MB * 1024
mock_pyamdsmi.smi_shutdown.return_value = None
# Set up provider state
self.provider._pyamdsmi = mock_pyamdsmi
self.provider._initialized = True
result = self.provider.get_gpu_utilization()
self.assertEqual(len(result), 1)
gpu_info = result[0]
self.assertEqual(gpu_info["index"], 0)
self.assertEqual(gpu_info["name"], "AMD Radeon RX 6800 XT")
self.assertEqual(gpu_info["uuid"], "GPU-13579bdf-9abc-def0-0000-000000000000")
self.assertEqual(gpu_info["utilization_gpu"], 85)
self.assertEqual(gpu_info["memory_used"], 6 * 1024) # 6GB in MB
self.assertEqual(gpu_info["memory_total"], 16 * 1024) # 16GB in MB
self.assertEqual(len(gpu_info["processes_pids"]), 1)
self.assertEqual(gpu_info["processes_pids"][5678]["pid"], 5678)
self.assertEqual(gpu_info["processes_pids"][5678]["gpu_memory_usage"], 512)
class TestGpuMetricProvider(unittest.TestCase):
"""Test GpuMetricProvider class."""
def setUp(self):
"""Set up test fixtures."""
self.provider = GpuMetricProvider()
def test_init(self):
"""Test GpuMetricProvider initialization."""
self.assertIsNone(self.provider._provider)
self.assertTrue(self.provider._enable_metric_report)
self.assertEqual(len(self.provider._providers), 2)
self.assertFalse(self.provider._initialized)
@patch.object(NvidiaGpuProvider, "is_available", return_value=True)
@patch.object(AmdGpuProvider, "is_available", return_value=False)
def test_detect_gpu_provider_nvidia(
self, mock_amd_available, mock_nvidia_available
):
"""Test GPU provider detection when NVIDIA is available."""
provider = self.provider._detect_gpu_provider()
self.assertIsInstance(provider, NvidiaGpuProvider)
mock_nvidia_available.assert_called_once()
@patch.object(NvidiaGpuProvider, "is_available", return_value=False)
@patch.object(AmdGpuProvider, "is_available", return_value=True)
def test_detect_gpu_provider_amd(self, mock_amd_available, mock_nvidia_available):
"""Test GPU provider detection when AMD is available."""
provider = self.provider._detect_gpu_provider()
self.assertIsInstance(provider, AmdGpuProvider)
mock_nvidia_available.assert_called_once()
mock_amd_available.assert_called_once()
@patch.object(NvidiaGpuProvider, "is_available", return_value=False)
@patch.object(AmdGpuProvider, "is_available", return_value=False)
def test_detect_gpu_provider_none(self, mock_amd_available, mock_nvidia_available):
"""Test GPU provider detection when no GPUs are available."""
provider = self.provider._detect_gpu_provider()
self.assertIsNone(provider)
@patch("subprocess.check_output")
def test_should_disable_gpu_check_true(self, mock_subprocess):
"""Test should_disable_gpu_check returns True for specific conditions."""
mock_subprocess.return_value = "" # Empty result means AMD GPU module not live
class MockNVMLError(Exception):
pass
MockNVMLError.__name__ = "NVMLError_DriverNotLoaded"
error = MockNVMLError("NVIDIA driver not loaded")
result = self.provider._should_disable_gpu_check(error)
self.assertTrue(result)
@patch("subprocess.check_output")
def test_should_disable_gpu_check_false_wrong_error(self, mock_subprocess):
"""Test should_disable_gpu_check returns False for wrong error type."""
mock_subprocess.return_value = ""
error = Exception("Some other error")
result = self.provider._should_disable_gpu_check(error)
self.assertFalse(result)
@patch("subprocess.check_output")
def test_should_disable_gpu_check_false_amd_present(self, mock_subprocess):
"""Test should_disable_gpu_check returns False when AMD GPU is present."""
mock_subprocess.return_value = "live" # AMD GPU module is live
class MockNVMLError(Exception):
pass
MockNVMLError.__name__ = "NVMLError_DriverNotLoaded"
error = MockNVMLError("NVIDIA driver not loaded")
result = self.provider._should_disable_gpu_check(error)
self.assertFalse(result)
def test_get_gpu_usage_disabled(self):
"""Test get_gpu_usage when GPU usage check is disabled."""
self.provider._enable_metric_report = False
result = self.provider.get_gpu_usage()
self.assertEqual(result, [])
@patch.object(GpuMetricProvider, "_detect_gpu_provider")
def test_get_gpu_usage_no_provider(self, mock_detect):
"""Test get_gpu_usage when no GPU provider is available."""
mock_detect.return_value = None
with patch.object(
NvidiaGpuProvider, "_initialize", side_effect=Exception("No GPU")
):
result = self.provider.get_gpu_usage()
self.assertEqual(result, [])
self.provider._initialized = False # Reset for clean test
mock_detect.assert_called_once()
@patch.object(GpuMetricProvider, "_detect_gpu_provider")
def test_get_gpu_usage_success(self, mock_detect):
"""Test successful get_gpu_usage."""
mock_provider = Mock()
mock_provider.get_gpu_utilization.return_value = [
GpuUtilizationInfo(
index=0,
name="Test GPU",
uuid="test-uuid",
utilization_gpu=50,
memory_used=1024,
memory_total=2048,
processes_pids={
1234: ProcessGPUInfo(
pid=1234, gpu_memory_usage=1024, gpu_utilization=None
)
},
)
]
mock_detect.return_value = mock_provider
result = self.provider.get_gpu_usage()
self.assertEqual(len(result), 1)
self.assertEqual(result[0]["index"], 0)
self.assertEqual(result[0]["name"], "Test GPU")
mock_provider.get_gpu_utilization.assert_called_once()
def test_get_provider_name_no_provider(self):
"""Test get_provider_name when no provider is set."""
result = self.provider.get_provider_name()
self.assertIsNone(result)
def test_get_provider_name_with_provider(self):
"""Test get_provider_name when provider is set."""
mock_provider = Mock()
mock_provider.get_provider_name.return_value = GpuProviderType.NVIDIA
self.provider._provider = mock_provider
result = self.provider.get_provider_name()
self.assertEqual(result, "nvidia")
def test_is_metric_report_enabled(self):
"""Test is_metric_report_enabled."""
self.assertTrue(self.provider.is_metric_report_enabled())
self.provider._enable_metric_report = False
self.assertFalse(self.provider.is_metric_report_enabled())
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,121 @@
import signal
import sys
import pytest
import requests
import ray._private.ray_constants as ray_constants
from ray._common.network_utils import find_free_port
from ray._common.test_utils import wait_for_condition
from ray.tests.conftest import * # noqa: F401 F403
def test_healthz_head(monkeypatch, ray_start_cluster):
dashboard_port = find_free_port()
h = ray_start_cluster.add_node(dashboard_port=dashboard_port)
uri = f"http://localhost:{dashboard_port}/api/gcs_healthz"
wait_for_condition(lambda: requests.get(uri).status_code == 200)
h.all_processes[ray_constants.PROCESS_TYPE_GCS_SERVER][0].process.kill()
# It'll either timeout or just return an error
try:
wait_for_condition(lambda: requests.get(uri, timeout=1) != 200, timeout=4)
except RuntimeError as e:
assert "Read timed out" in str(e)
def test_healthz_agent_1(monkeypatch, ray_start_cluster):
agent_port = find_free_port()
h = ray_start_cluster.add_node(dashboard_agent_listen_port=agent_port)
uri = f"http://{h.node_ip_address}:{agent_port}/api/local_raylet_healthz"
wait_for_condition(lambda: requests.get(uri).status_code == 200)
h.all_processes[ray_constants.PROCESS_TYPE_GCS_SERVER][0].process.kill()
# GCS's failure will not lead to healthz failure
assert requests.get(uri).status_code == 200
@pytest.mark.skipif(sys.platform == "win32", reason="SIGSTOP only on posix")
def test_healthz_agent_2(monkeypatch, ray_start_cluster):
monkeypatch.setenv("RAY_health_check_failure_threshold", "3")
monkeypatch.setenv("RAY_health_check_timeout_ms", "100")
monkeypatch.setenv("RAY_health_check_period_ms", "1000")
monkeypatch.setenv("RAY_health_check_initial_delay_ms", "0")
agent_port = find_free_port()
h = ray_start_cluster.add_node(dashboard_agent_listen_port=agent_port)
uri = f"http://{h.node_ip_address}:{agent_port}/api/local_raylet_healthz"
wait_for_condition(lambda: requests.get(uri).status_code == 200)
h.all_processes[ray_constants.PROCESS_TYPE_RAYLET][0].process.send_signal(
signal.SIGSTOP
)
# GCS still think raylet is alive.
assert requests.get(uri).status_code == 200
# But after heartbeat timeout, it'll think the raylet is down.
wait_for_condition(lambda: requests.get(uri).status_code != 200)
def test_unified_healthz_head(monkeypatch, ray_start_cluster):
agent_port = find_free_port()
h = ray_start_cluster.add_node(dashboard_agent_listen_port=agent_port)
uri = f"http://{h.node_ip_address}:{agent_port}/api/healthz"
wait_for_condition(lambda: requests.get(uri).status_code == 200)
resp = requests.get(uri)
assert "raylet: success" in resp.text
assert "gcs: success" in resp.text
h.all_processes[ray_constants.PROCESS_TYPE_GCS_SERVER][0].process.kill()
wait_for_condition(lambda: requests.get(uri).status_code == 503)
resp = requests.get(uri)
assert "gcs: " in resp.text
assert "gcs: success" not in resp.text
@pytest.mark.skipif(sys.platform == "win32", reason="SIGSTOP only on posix")
def test_unified_healthz_worker(monkeypatch, ray_start_cluster):
monkeypatch.setenv("RAY_health_check_failure_threshold", "3")
monkeypatch.setenv("RAY_health_check_timeout_ms", "100")
monkeypatch.setenv("RAY_health_check_period_ms", "1000")
monkeypatch.setenv("RAY_health_check_initial_delay_ms", "0")
ray_start_cluster.add_node()
agent_port = find_free_port()
h = ray_start_cluster.add_node(dashboard_agent_listen_port=agent_port)
uri = f"http://{h.node_ip_address}:{agent_port}/api/healthz"
wait_for_condition(lambda: requests.get(uri).status_code == 200)
resp = requests.get(uri)
assert "gcs: success (no local gcs)" in resp.text
# Stop local raylet and verify this makes /healthz fail.
h.all_processes[ray_constants.PROCESS_TYPE_RAYLET][0].process.send_signal(
signal.SIGSTOP
)
wait_for_condition(lambda: requests.get(uri).status_code == 503)
resp = requests.get(uri)
assert "raylet: Local Raylet failed" in resp.text
def test_unified_healthz_worker_gcs_down(monkeypatch, ray_start_cluster):
h_head = ray_start_cluster.add_node()
agent_port = find_free_port()
h_worker = ray_start_cluster.add_node(dashboard_agent_listen_port=agent_port)
uri = f"http://{h_worker.node_ip_address}:{agent_port}/api/healthz"
wait_for_condition(lambda: requests.get(uri).status_code == 200)
resp = requests.get(uri)
assert "gcs: success (no local gcs)" in resp.text
# Stop the head GCS server.
h_head.all_processes[ray_constants.PROCESS_TYPE_GCS_SERVER][0].process.kill()
# Worker health check should still succeed.
assert requests.get(uri).status_code == 200
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,106 @@
import sys
from unittest.mock import MagicMock, patch
import pytest
from ray.dashboard.modules.reporter.jax_profile_manager import JaxProfilingManager
from ray.util.tpu import init_jax_profiler
@pytest.fixture
def mock_profiler_client():
mock_client = MagicMock()
mock_profiler_module = MagicMock()
mock_profiler_module.profiler_client = mock_client
modules_to_patch = {
"tensorflow": MagicMock(),
"tensorflow.python": MagicMock(),
"tensorflow.python.profiler": mock_profiler_module,
}
with patch.dict("sys.modules", modules_to_patch):
yield mock_client
@pytest.mark.asyncio
async def test_jax_profile_success(tmp_path, mock_profiler_client):
manager = JaxProfilingManager(tmp_path)
# Mock success
mock_profiler_client.trace.return_value = None
success, output = await manager.jax_profile(pid=123, port=6000, duration_s=2)
assert success
assert output.startswith("profiles")
assert "123_" in output
mock_profiler_client.trace.assert_called_once()
call = mock_profiler_client.trace.call_args
assert call.args[0] == "grpc://localhost:6000"
assert call.kwargs["logdir"].startswith(str(tmp_path / "profiles"))
assert call.kwargs["duration_ms"] == 2000
@pytest.mark.asyncio
async def test_jax_profile_failure(tmp_path, mock_profiler_client):
manager = JaxProfilingManager(tmp_path)
# Mock failure
mock_profiler_client.trace.side_effect = Exception("Connection failed")
success, output = await manager.jax_profile(pid=123, port=6000, duration_s=2)
assert not success
assert "Failed to capture trace: Connection failed" in output
@pytest.mark.asyncio
async def test_jax_profile_no_tensorflow(tmp_path):
manager = JaxProfilingManager(tmp_path)
# Force ImportError on tensorflow
with patch.dict("sys.modules", {"tensorflow": None}):
success, output = await manager.jax_profile(pid=123, port=6000, duration_s=2)
assert not success
assert "TensorFlow is required" in output
@patch("ray.util.tpu.os.getpid")
@patch("ray.util.tpu.os.getenv")
def test_setup_jax_profiler_success(mock_getenv, mock_getpid):
mock_getenv.return_value = "9999"
mock_getpid.return_value = 12345
mock_jax = MagicMock()
mock_worker = MagicMock()
mock_worker.node.node_id = "mock_node_id_hex"
with (
patch.dict("sys.modules", {"jax": mock_jax}),
patch("ray._private.worker.global_worker", mock_worker),
patch("ray.experimental.internal_kv._internal_kv_put") as mock_kv_put,
):
init_jax_profiler()
mock_jax.profiler.start_server.assert_called_once_with(9999)
import ray
mock_kv_put.assert_called_once_with(
"jax_profiler_port:mock_node_id_hex:12345",
b"9999",
namespace=ray._private.ray_constants.KV_NAMESPACE_DASHBOARD,
)
def test_setup_jax_profiler_no_jax():
with patch.dict("sys.modules", {"jax": None}):
# Should skip starting profiler and not raise error
init_jax_profiler()
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,270 @@
import os
import sys
import tempfile
import time
from unittest.mock import AsyncMock, patch
import pytest
import ray
from ray.dashboard.modules.reporter.profile_manager import (
CpuProfilingManager,
MemoryProfilingManager,
)
from ray.dashboard.tests.conftest import * # noqa
@pytest.fixture
def setup_memory_profiler():
with tempfile.TemporaryDirectory() as tmpdir:
memory_profiler = MemoryProfilingManager(tmpdir)
@ray.remote
class Actor:
def getpid(self):
return os.getpid()
def long_run(self):
print("Long-running task began.")
time.sleep(1000)
print("Long-running task completed.")
actor = Actor.remote()
yield actor, memory_profiler
@pytest.mark.asyncio
@pytest.mark.skipif(
os.environ.get("RAY_MINIMAL") == "1",
reason="This test is not supposed to work for minimal installation.",
)
@pytest.mark.skipif(sys.platform == "win32", reason="No memray on Windows.")
@pytest.mark.skipif(
sys.platform == "darwin",
reason="Fails on OSX, requires memray & lldb installed in osx image",
)
class TestMemoryProfiling:
async def test_basic_attach_profiler(self, setup_memory_profiler, shutdown_only):
# test basic attach profiler to running process
actor, memory_profiler = setup_memory_profiler
pid = ray.get(actor.getpid.remote())
actor.long_run.remote()
success, profiler_filename, message = await memory_profiler.attach_profiler(
pid, verbose=True
)
assert success, message
assert f"Success attaching memray to process {pid}" in message
assert profiler_filename in os.listdir(memory_profiler.profile_dir_path)
async def test_profiler_multiple_attach(self, setup_memory_profiler, shutdown_only):
# test multiple attaches
actor, memory_profiler = setup_memory_profiler
pid = ray.get(actor.getpid.remote())
actor.long_run.remote()
success, profiler_filename, message = await memory_profiler.attach_profiler(
pid, verbose=True
)
assert success, message
assert f"Success attaching memray to process {pid}" in message
assert profiler_filename in os.listdir(memory_profiler.profile_dir_path)
success, _, message = await memory_profiler.attach_profiler(pid)
assert success, message
assert f"Success attaching memray to process {pid}" in message
async def test_detach_profiler_successful(
self, setup_memory_profiler, shutdown_only
):
# test basic detach profiler
actor, memory_profiler = setup_memory_profiler
pid = ray.get(actor.getpid.remote())
actor.long_run.remote()
success, _, message = await memory_profiler.attach_profiler(pid, verbose=True)
assert success, message
success, message = await memory_profiler.detach_profiler(pid, verbose=True)
assert success, message
assert f"Success detaching memray from process {pid}" in message
async def test_detach_profiler_without_attach(
self, setup_memory_profiler, shutdown_only
):
# test detach profiler from unattached process
actor, memory_profiler = setup_memory_profiler
pid = ray.get(actor.getpid.remote())
success, message = await memory_profiler.detach_profiler(pid)
assert not success, message
assert "Failed to execute" in message
assert "no previous `memray attach`" in message
async def test_profiler_memray_not_installed(
self, setup_memory_profiler, shutdown_only
):
# test profiler when memray is not installed
actor, memory_profiler = setup_memory_profiler
pid = ray.get(actor.getpid.remote())
with patch("shutil.which", return_value=None):
success, _, message = await memory_profiler.attach_profiler(pid)
assert not success
assert "memray is not installed" in message
async def test_profiler_attach_process_not_found(
self, setup_memory_profiler, shutdown_only
):
# test basic attach profiler to non-existing process
_, memory_profiler = setup_memory_profiler
pid = 123456
success, _, message = await memory_profiler.attach_profiler(pid)
assert not success, message
assert "Failed to execute" in message
assert "The given process ID does not exist" in message
async def test_profiler_get_profiler_result(
self, setup_memory_profiler, shutdown_only
):
# test get profiler result from running process
actor, memory_profiler = setup_memory_profiler
pid = ray.get(actor.getpid.remote())
actor.long_run.remote()
success, profiler_filename, message = await memory_profiler.attach_profiler(
pid, verbose=True
)
assert success, message
assert f"Success attaching memray to process {pid}" in message
# get profiler result in flamegraph and table format
supported_formats = ["flamegraph", "table"]
unsupported_formats = ["json"]
for format in supported_formats + unsupported_formats:
success, message = await memory_profiler.get_profile_result(
pid, profiler_filename=profiler_filename, format=format
)
if format in supported_formats:
assert success, message
assert f"{format} report" in message.decode("utf-8")
else:
assert not success, message
assert f"{format} is not supported" in message
async def test_profiler_result_not_exist(
self, setup_memory_profiler, shutdown_only
):
# test get profiler result from unexisting process
_, memory_profiler = setup_memory_profiler
pid = 123456
profiler_filename = "non-existing-file"
success, message = await memory_profiler.get_profile_result(
pid, profiler_filename=profiler_filename, format=format
)
assert not success, message
assert f"process {pid} has not been profiled" in message
@pytest.mark.asyncio
@pytest.mark.skipif(sys.platform == "win32", reason="No py-spy on Windows.")
class TestCpuProfiling:
async def _capture_pyspy_cmd(self, **cpu_profile_kwargs):
"""Run cpu_profile with subprocess execution mocked out and return the
py-spy command that would have been executed.
We patch ``asyncio.create_subprocess_exec`` (the same primitive the
manager uses) and have the fake process exit non-zero so that
``cpu_profile`` short-circuits before attempting to read the (never
created) output file. The command is fully constructed before the
subprocess is spawned, so the captured args are valid regardless.
"""
with tempfile.TemporaryDirectory() as tmpdir:
cpu_profiler = CpuProfilingManager(tmpdir)
fake_process = AsyncMock()
fake_process.communicate.return_value = (b"", b"boom")
fake_process.returncode = 1
with patch(
"ray.dashboard.modules.reporter.profile_manager.shutil.which",
return_value="/fake/py-spy",
), patch(
"ray.dashboard.modules.reporter.profile_manager."
"_can_passwordless_sudo",
new=AsyncMock(return_value=False),
), patch(
"asyncio.create_subprocess_exec",
new=AsyncMock(return_value=fake_process),
) as mock_exec:
await cpu_profiler.cpu_profile(pid=12345, **cpu_profile_kwargs)
assert mock_exec.call_count == 1
# create_subprocess_exec(*cmd, ...) -> positional args are the cmd.
return list(mock_exec.call_args.args)
async def test_cpu_profile_idle_flag_added(self):
# idle=True should append `--idle` to the py-spy command.
cmd = await self._capture_pyspy_cmd(idle=True)
assert "--idle" in cmd
async def test_cpu_profile_idle_not_added_by_default(self):
# By default (idle=False) the `--idle` flag should be absent.
cmd = await self._capture_pyspy_cmd()
assert "--idle" not in cmd
async def test_cpu_profile_subprocesses_flag_added(self):
# subprocesses=True should append `--subprocesses` to the py-spy command.
cmd = await self._capture_pyspy_cmd(subprocesses=True)
assert "--subprocesses" in cmd
async def test_cpu_profile_subprocesses_not_added_by_default(self):
# By default the `--subprocesses` flag should be absent.
cmd = await self._capture_pyspy_cmd()
assert "--subprocesses" not in cmd
@pytest.mark.asyncio
@pytest.mark.skipif(sys.platform == "win32", reason="No py-spy on Windows.")
class TestTraceDump:
async def _capture_pyspy_cmd(self, **trace_dump_kwargs):
"""Run trace_dump with subprocess execution mocked out and return the
py-spy command that would have been executed.
"""
with tempfile.TemporaryDirectory() as tmpdir:
cpu_profiler = CpuProfilingManager(tmpdir)
fake_process = AsyncMock()
fake_process.communicate.return_value = (b"", b"boom")
fake_process.returncode = 1
with patch(
"ray.dashboard.modules.reporter.profile_manager.shutil.which",
return_value="/fake/py-spy",
), patch(
"ray.dashboard.modules.reporter.profile_manager."
"_can_passwordless_sudo",
new=AsyncMock(return_value=False),
), patch(
"asyncio.create_subprocess_exec",
new=AsyncMock(return_value=fake_process),
) as mock_exec:
await cpu_profiler.trace_dump(pid=12345, **trace_dump_kwargs)
assert mock_exec.call_count == 1
return list(mock_exec.call_args.args)
async def test_trace_dump_subprocesses_flag_added(self):
# subprocesses=True should append `--subprocesses` to the py-spy dump command.
cmd = await self._capture_pyspy_cmd(subprocesses=True)
assert "dump" in cmd
assert "--subprocesses" in cmd
async def test_trace_dump_subprocesses_not_added_by_default(self):
# By default the `--subprocesses` flag should be absent.
cmd = await self._capture_pyspy_cmd()
assert "--subprocesses" not in cmd
if __name__ == "__main__":
sys.exit(pytest.main(["-v", __file__]))
File diff suppressed because it is too large Load Diff