Skip to content

Commit 4d55401

Browse files
committed
add custom sampling errors
Signed-off-by: Keming <kemingy94@gmail.com>
1 parent dcad05a commit 4d55401

File tree

5 files changed

+63
-22
lines changed

5 files changed

+63
-22
lines changed

Lib/profiling/sampling/__main__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"""
4747

4848
from .cli import main
49+
from .errors import SamplingUnknownProcessError, SamplingModuleNotFoundError, SamplingScriptNotFoundError
4950

5051
def handle_permission_error():
5152
"""Handle PermissionError by displaying appropriate error message."""
@@ -64,3 +65,9 @@ def handle_permission_error():
6465
main()
6566
except PermissionError:
6667
handle_permission_error()
68+
except SamplingUnknownProcessError as err:
69+
print(f"Tachyon cannot find the process: {err}", file=sys.stderr)
70+
sys.exit(1)
71+
except (SamplingModuleNotFoundError, SamplingScriptNotFoundError) as err:
72+
print(f"Tachyon cannot find the target: {err}", file=sys.stderr)
73+
sys.exit(1)

Lib/profiling/sampling/cli.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import time
1111
from contextlib import nullcontext
1212

13+
from .errors import SamplingUnknownProcessError, SamplingModuleNotFoundError, SamplingScriptNotFoundError
1314
from .sample import sample, sample_live, _is_process_running
1415
from .pstats_collector import PstatsCollector
1516
from .stack_collector import CollapsedStackCollector, FlamegraphCollector
@@ -744,7 +745,7 @@ def main():
744745
def _handle_attach(args):
745746
"""Handle the 'attach' command."""
746747
if not _is_process_running(args.pid):
747-
raise sys.exit(f"Process with PID {args.pid} is not running.")
748+
raise SamplingUnknownProcessError(args.pid)
748749
# Check if live mode is requested
749750
if args.live:
750751
_handle_live_attach(args, args.pid)
@@ -794,13 +795,13 @@ def _handle_run(args):
794795
added_cwd = True
795796
try:
796797
if importlib.util.find_spec(args.target) is None:
797-
sys.exit(f"Error: Module not found: {args.target}")
798+
raise SamplingModuleNotFoundError(args.target)
798799
finally:
799800
if added_cwd:
800801
sys.path.remove(cwd)
801802
else:
802803
if not os.path.exists(args.target):
803-
sys.exit(f"Error: Script not found: {args.target}")
804+
raise SamplingScriptNotFoundError(args.target)
804805

805806
# Check if live mode is requested
806807
if args.live:

Lib/profiling/sampling/errors.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Custom exceptions for the sampling profiler."""
2+
3+
class SamplingProfilerError(Exception):
4+
"""Base exception for sampling profiler errors."""
5+
6+
class SamplingPermissionError(SamplingProfilerError):
7+
def __init__(self):
8+
super().__init__(f"Insufficient permission to access process.")
9+
10+
class SamplingUnknownProcessError(SamplingProfilerError):
11+
def __init__(self, pid):
12+
self.pid = pid
13+
super().__init__(f"Process with PID '{pid}' does not exist.")
14+
15+
class SamplingScriptNotFoundError(SamplingProfilerError):
16+
def __init__(self, script_path):
17+
self.script_path = script_path
18+
super().__init__(f"Script '{script_path}' not found.")
19+
20+
class SamplingModuleNotFoundError(SamplingProfilerError):
21+
def __init__(self, module_name):
22+
self.module_name = module_name
23+
super().__init__(f"Module '{module_name}' not found.")

Lib/profiling/sampling/sample.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,29 @@ def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MOD
3535
self.mode = mode # Store mode for later use
3636
self.collect_stats = collect_stats
3737
try:
38-
if _FREE_THREADED_BUILD:
39-
self.unwinder = _remote_debugging.RemoteUnwinder(
40-
self.pid, all_threads=self.all_threads, mode=mode, native=native, gc=gc,
41-
opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
42-
cache_frames=True, stats=collect_stats
43-
)
44-
else:
45-
only_active_threads = bool(self.all_threads)
46-
self.unwinder = _remote_debugging.RemoteUnwinder(
47-
self.pid, only_active_thread=only_active_threads, mode=mode, native=native, gc=gc,
48-
opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
49-
cache_frames=True, stats=collect_stats
50-
)
38+
self.unwinder = self._new_unwinder(native, gc, opcodes, skip_non_matching_threads)
5139
except RuntimeError as err:
5240
raise SystemExit(err)
5341
# Track sample intervals and total sample count
5442
self.sample_intervals = deque(maxlen=100)
5543
self.total_samples = 0
5644
self.realtime_stats = False
5745

46+
def _new_unwinder(self, native, gc, opcodes, skip_non_matching_threads):
47+
if _FREE_THREADED_BUILD:
48+
unwinder = _remote_debugging.RemoteUnwinder(
49+
self.pid, all_threads=self.all_threads, mode=self.mode, native=native, gc=gc,
50+
opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
51+
cache_frames=True, stats=self.collect_stats
52+
)
53+
else:
54+
unwinder = _remote_debugging.RemoteUnwinder(
55+
self.pid, only_active_thread=bool(self.all_threads), mode=self.mode, native=native, gc=gc,
56+
opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
57+
cache_frames=True, stats=self.collect_stats
58+
)
59+
return unwinder
60+
5861
def sample(self, collector, duration_sec=10, *, async_aware=False):
5962
sample_interval_sec = self.sample_interval_usec / 1_000_000
6063
running_time = 0

Lib/test/test_profiling/test_sampling_profiler/test_cli.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from test.support import is_emscripten, requires_remote_subprocess_debugging
1717

1818
from profiling.sampling.cli import main
19+
from profiling.sampling.errors import SamplingScriptNotFoundError, SamplingModuleNotFoundError, SamplingUnknownProcessError
1920

2021

2122
class TestSampleProfilerCLI(unittest.TestCase):
@@ -203,12 +204,12 @@ def test_cli_mutually_exclusive_pid_script(self):
203204
with (
204205
mock.patch("sys.argv", test_args),
205206
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
206-
self.assertRaises(SystemExit) as cm,
207+
self.assertRaises(SamplingScriptNotFoundError) as cm,
207208
):
208209
main()
209210

210211
# Verify the error is about the non-existent script
211-
self.assertIn("12345", str(cm.exception.code))
212+
self.assertIn("12345", str(cm.exception))
212213

213214
def test_cli_no_target_specified(self):
214215
# In new CLI, must specify a subcommand
@@ -704,14 +705,20 @@ def test_async_aware_incompatible_with_all_threads(self):
704705
def test_run_nonexistent_script_exits_cleanly(self):
705706
"""Test that running a non-existent script exits with a clean error."""
706707
with mock.patch("sys.argv", ["profiling.sampling.cli", "run", "/nonexistent/script.py"]):
707-
with self.assertRaises(SystemExit) as cm:
708+
with self.assertRaisesRegex(SamplingScriptNotFoundError, "Script '[\\w/.]+' not found."):
708709
main()
709-
self.assertIn("Script not found", str(cm.exception.code))
710710

711711
@unittest.skipIf(is_emscripten, "subprocess not available")
712712
def test_run_nonexistent_module_exits_cleanly(self):
713713
"""Test that running a non-existent module exits with a clean error."""
714714
with mock.patch("sys.argv", ["profiling.sampling.cli", "run", "-m", "nonexistent_module_xyz"]):
715-
with self.assertRaises(SystemExit) as cm:
715+
with self.assertRaisesRegex(SamplingModuleNotFoundError, "Module '[\\w/.]+' not found."):
716+
main()
717+
718+
def test_cli_attach_nonexistent_pid(self):
719+
fake_pid = "99999"
720+
with mock.patch("sys.argv", ["profiling.sampling.cli", "attach", fake_pid]):
721+
with self.assertRaises(SamplingUnknownProcessError) as cm:
716722
main()
717-
self.assertIn("Module not found", str(cm.exception.code))
723+
724+
self.assertIn(fake_pid, str(cm.exception))

0 commit comments

Comments
 (0)