Skip to content

Commit 6e7795c

Browse files
committed
COUTIME
1 parent 074f3b2 commit 6e7795c

File tree

7 files changed

+234
-22
lines changed

7 files changed

+234
-22
lines changed

Lib/profiling/sampling/collector.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ def collect(self, stack_frames):
1010
def export(self, filename):
1111
"""Export collected data to a file."""
1212

13-
def _iter_all_frames(self, stack_frames):
13+
def _iter_all_frames(self, stack_frames, skip_idle=False):
1414
"""Iterate over all frame stacks from all interpreters and threads."""
15+
print("Skipping idle threads" if skip_idle else "Including idle threads")
1516
for interpreter_info in stack_frames:
1617
for thread_info in interpreter_info.threads:
18+
print(thread_info.status)
1719
frames = thread_info.frame_info
1820
if frames:
1921
yield frames

Lib/profiling/sampling/pstats_collector.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
class PstatsCollector(Collector):
8-
def __init__(self, sample_interval_usec):
8+
def __init__(self, sample_interval_usec, *, skip_idle=False):
99
self.result = collections.defaultdict(
1010
lambda: dict(total_rec_calls=0, direct_calls=0, cumulative_calls=0)
1111
)
@@ -14,6 +14,7 @@ def __init__(self, sample_interval_usec):
1414
self.callers = collections.defaultdict(
1515
lambda: collections.defaultdict(int)
1616
)
17+
self.skip_idle = skip_idle
1718

1819
def _process_frames(self, frames):
1920
"""Process a single thread's frame stack."""
@@ -40,7 +41,7 @@ def _process_frames(self, frames):
4041
self.callers[callee][caller] += 1
4142

4243
def collect(self, stack_frames):
43-
for frames in self._iter_all_frames(stack_frames):
44+
for frames in self._iter_all_frames(stack_frames, skip_idle=self.skip_idle):
4445
self._process_frames(frames)
4546

4647
def export(self, filename):

Lib/profiling/sampling/sample.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,7 @@ def sample(
583583
show_summary=True,
584584
output_format="pstats",
585585
realtime_stats=False,
586+
skip_idle=False,
586587
):
587588
profiler = SampleProfiler(
588589
pid, sample_interval_usec, all_threads=all_threads
@@ -592,9 +593,9 @@ def sample(
592593
collector = None
593594
match output_format:
594595
case "pstats":
595-
collector = PstatsCollector(sample_interval_usec)
596+
collector = PstatsCollector(sample_interval_usec, skip_idle=skip_idle)
596597
case "collapsed":
597-
collector = CollapsedStackCollector()
598+
collector = CollapsedStackCollector(skip_idle=skip_idle)
598599
filename = filename or f"collapsed.{pid}.txt"
599600
case _:
600601
raise ValueError(f"Invalid output format: {output_format}")
@@ -644,6 +645,7 @@ def wait_for_process_and_sample(pid, sort_value, args):
644645
filename = args.outfile
645646
if not filename and args.format == "collapsed":
646647
filename = f"collapsed.{pid}.txt"
648+
skip_idle = True if args.mode == "cpu" else False
647649

648650
sample(
649651
pid,
@@ -656,6 +658,7 @@ def wait_for_process_and_sample(pid, sort_value, args):
656658
show_summary=not args.no_summary,
657659
output_format=args.format,
658660
realtime_stats=args.realtime_stats,
661+
skip_idle=skip_idle,
659662
)
660663

661664

@@ -710,6 +713,15 @@ def main():
710713
help="Print real-time sampling statistics (Hz, mean, min, max, stdev) during profiling",
711714
)
712715

716+
# Mode options
717+
mode_group = parser.add_argument_group("Mode options")
718+
mode_group.add_argument(
719+
"--mode",
720+
choices=["wall", "cpu"],
721+
default="wall-time",
722+
help="Sampling mode: wall-time (default, skip_idle=False) or cpu-time (skip_idle=True)",
723+
)
724+
713725
# Output format selection
714726
output_group = parser.add_argument_group("Output options")
715727
output_format = output_group.add_mutually_exclusive_group()
@@ -826,6 +838,9 @@ def main():
826838
elif target_count > 1:
827839
parser.error("only one target type can be specified: -p/--pid, -m/--module, or script")
828840

841+
# Set skip_idle based on mode
842+
skip_idle = True if args.mode == "cpu" else False
843+
829844
if args.pid:
830845
sample(
831846
args.pid,
@@ -838,6 +853,7 @@ def main():
838853
show_summary=not args.no_summary,
839854
output_format=args.format,
840855
realtime_stats=args.realtime_stats,
856+
skip_idle=skip_idle,
841857
)
842858
elif args.module or args.args:
843859
if args.module:

Lib/profiling/sampling/stack_collector.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66

77
class StackTraceCollector(Collector):
8-
def __init__(self):
8+
def __init__(self, *, skip_idle=False):
99
self.call_trees = []
1010
self.function_samples = collections.defaultdict(int)
11+
self.skip_idle = skip_idle
1112

1213
def _process_frames(self, frames):
1314
"""Process a single thread's frame stack."""
@@ -23,7 +24,7 @@ def _process_frames(self, frames):
2324
self.function_samples[frame] += 1
2425

2526
def collect(self, stack_frames):
26-
for frames in self._iter_all_frames(stack_frames):
27+
for frames in self._iter_all_frames(stack_frames, skip_idle=self.skip_idle):
2728
self._process_frames(frames)
2829

2930

Lib/test/test_external_inspection.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,6 +1670,103 @@ def test_unsupported_platform_error(self):
16701670
str(cm.exception)
16711671
)
16721672

1673+
class TestDetectionOfThreadStatus(unittest.TestCase):
1674+
@unittest.skipIf(
1675+
sys.platform not in ("linux", "darwin", "win32"),
1676+
"Test only runs on unsupported platforms (not Linux, macOS, or Windows)",
1677+
)
1678+
@unittest.skipIf(sys.platform == "android", "Android raises Linux-specific exception")
1679+
def test_thread_status_detection(self):
1680+
port = find_unused_port()
1681+
script = textwrap.dedent(
1682+
f"""\
1683+
import time, sys, socket, threading
1684+
import os
1685+
1686+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1687+
sock.connect(('localhost', {port}))
1688+
1689+
def sleeper():
1690+
tid = threading.get_native_id()
1691+
sock.sendall(f'ready:sleeper:{{tid}}\\n'.encode())
1692+
time.sleep(10000)
1693+
1694+
def busy():
1695+
tid = threading.get_native_id()
1696+
sock.sendall(f'ready:busy:{{tid}}\\n'.encode())
1697+
x = 0
1698+
while True:
1699+
x = x + 1
1700+
time.sleep(0.5)
1701+
1702+
t1 = threading.Thread(target=sleeper)
1703+
t2 = threading.Thread(target=busy)
1704+
t1.start()
1705+
t2.start()
1706+
sock.sendall(b'ready:main\\n')
1707+
t1.join()
1708+
t2.join()
1709+
sock.close()
1710+
"""
1711+
)
1712+
with os_helper.temp_dir() as work_dir:
1713+
script_dir = os.path.join(work_dir, "script_pkg")
1714+
os.mkdir(script_dir)
1715+
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1716+
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1717+
server_socket.bind(("localhost", port))
1718+
server_socket.settimeout(SHORT_TIMEOUT)
1719+
server_socket.listen(1)
1720+
1721+
script_name = _make_test_script(script_dir, "thread_status_script", script)
1722+
client_socket = None
1723+
try:
1724+
p = subprocess.Popen([sys.executable, script_name])
1725+
client_socket, _ = server_socket.accept()
1726+
server_socket.close()
1727+
response = b""
1728+
sleeper_tid = None
1729+
busy_tid = None
1730+
while True:
1731+
chunk = client_socket.recv(1024)
1732+
response += chunk
1733+
if b"ready:main" in response and b"ready:sleeper" in response and b"ready:busy" in response:
1734+
# Parse TIDs from the response
1735+
for line in response.split(b"\n"):
1736+
if line.startswith(b"ready:sleeper:"):
1737+
try:
1738+
sleeper_tid = int(line.split(b":")[-1])
1739+
except Exception:
1740+
pass
1741+
elif line.startswith(b"ready:busy:"):
1742+
try:
1743+
busy_tid = int(line.split(b":")[-1])
1744+
except Exception:
1745+
pass
1746+
break
1747+
1748+
unwinder = RemoteUnwinder(p.pid, all_threads=True)
1749+
time.sleep(0.2) # Give a bit of time to let threads settle
1750+
traces = unwinder.get_stack_trace()
1751+
1752+
# Find threads and their statuses
1753+
statuses = {}
1754+
for interpreter_info in traces:
1755+
for thread_info in interpreter_info.threads:
1756+
statuses[thread_info.thread_id] = thread_info.status
1757+
1758+
self.assertIsNotNone(sleeper_tid, "Sleeper thread id not received")
1759+
self.assertIsNotNone(busy_tid, "Busy thread id not received")
1760+
self.assertIn(sleeper_tid, statuses, "Sleeper tid not found in sampled threads")
1761+
self.assertIn(busy_tid, statuses, "Busy tid not found in sampled threads")
1762+
self.assertEqual(statuses[sleeper_tid], 1, "Sleeper thread should be idle (1)")
1763+
self.assertEqual(statuses[busy_tid], 0, "Busy thread should be running (0)")
1764+
1765+
finally:
1766+
if client_socket is not None:
1767+
client_socket.close()
1768+
p.terminate()
1769+
p.wait(timeout=SHORT_TIMEOUT)
16731770

16741771
if __name__ == "__main__":
16751772
unittest.main()

0 commit comments

Comments
 (0)