summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbd <bdunahu@operationnull.com>2025-06-09 01:47:13 -0400
committerbd <bdunahu@operationnull.com>2025-06-09 01:47:13 -0400
commit06e9b57dfc8492984290914b5428b27508cef982 (patch)
treec40a29b86255b9e9417387992caf46525daf8bf7
parentf5afb948e7477aaf274e464cbe8a841ece2919ce (diff)
Rewrite for further similarities to Scalene < v1.0
-rw-r--r--mini-scalene.py137
-rw-r--r--tests/simult.py2
2 files changed, 97 insertions, 42 deletions
diff --git a/mini-scalene.py b/mini-scalene.py
index e593c94..4883cff 100644
--- a/mini-scalene.py
+++ b/mini-scalene.py
@@ -1,55 +1,108 @@
-import runpy
-import os
import sys
-import time
+import os
import threading
-
-
-# in milliseconds
-SAMPLE_INTERVAL = 10
-
-
-def die(message):
- print(message, file=sys.stderr)
- # use 'os' over 'sys', since we are calling from a child thread
- os._exit(1)
-
-
-def get_main_thread_id():
- for thread_id, frame in sys._current_frames().items():
- if threading.main_thread().ident == thread_id:
- return thread_id
- die("[mini-scalene] Couldn't find main thread--likely an unimplemented feature.")
-
-
-def sample_running_function():
- main_thread_id = get_main_thread_id()
-
- frame = sys._current_frames()[main_thread_id]
- code = frame.f_code
- print(f"[mini-scalene] Currently executing: {code.co_name} in {code.co_filename}:{frame.f_lineno}")
-
-
-def sampling_loop():
- while True:
- sample_running_function()
- time.sleep(SAMPLE_INTERVAL / 1000.0)
+import traceback
+import runpy
+import atexit
+import signal
+from collections import defaultdict
+
+
+class mini_scalene:
+ cpu_samples = defaultdict(lambda: 0)
+ total_cpu_samples = 0
+ signal_interval = 0.01
+
+ def __init__(self):
+ signal.signal(signal.SIGPROF, self.cpu_signal_handler)
+ signal.setitimer(signal.ITIMER_PROF,
+ self.signal_interval, self.signal_interval)
+
+ @staticmethod
+ def start():
+ atexit.register(mini_scalene.exit_handler)
+
+ @staticmethod
+ def exit_handler():
+ # Turn off the profiling signals.
+ signal.signal(signal.ITIMER_PROF, signal.SIG_IGN)
+ signal.signal(signal.SIGVTALRM, signal.SIG_IGN)
+ signal.setitimer(signal.ITIMER_PROF, 0)
+ # If we've collected any samples, dump them.
+ print("CPU usage:")
+ if mini_scalene.total_cpu_samples > 0:
+ # Sort the samples in descending order by number of samples.
+ mini_scalene.cpu_samples = {k: v for k, v in sorted(
+ mini_scalene.cpu_samples.items(), key=lambda item: item[1], reverse=True)}
+ for key in mini_scalene.cpu_samples:
+ print(key + " : " + str(mini_scalene.cpu_samples[key] * 100 / mini_scalene.total_cpu_samples) + "%" + " (" + str(
+ mini_scalene.cpu_samples[key]) + " total samples)")
+ else:
+ print("(did not run long enough to profile)")
+
+ @staticmethod
+ def cpu_signal_handler(sig, frame):
+ keys = mini_scalene.compute_frames_to_record(frame)
+ for key in keys:
+ mini_scalene.cpu_samples[mini_scalene.frame_to_string(key)] += 1
+ mini_scalene.total_cpu_samples += 1
+ return
+
+ @staticmethod
+ def compute_frames_to_record(this_frame):
+ """Collects all stack frames that Scalene actually processes."""
+ frames = [this_frame]
+ frames += [sys._current_frames().get(t.ident, None)
+ for t in threading.enumerate()]
+ # Process all the frames to remove ones we aren't going to track.
+ new_frames = []
+ for frame in frames:
+ if frame is None:
+ continue
+ fname = frame.f_code.co_filename
+ # Record samples only for files we care about.
+ if (len(fname)) == 0:
+ # 'eval/compile' gives no f_code.co_filename. We have
+ # to look back into the outer frame in order to check
+ # the co_filename.
+ fname = frame.f_back.f_code.co_filename
+ if not mini_scalene.should_trace(fname):
+ continue
+ new_frames.append(frame)
+ return new_frames
+
+ @staticmethod
+ def frame_to_string(frame):
+ co = frame.f_code
+ func_name = co.co_name
+ line_no = frame.f_lineno
+ filename = co.co_filename
+ return filename + '\t' + func_name + '\t' + str(line_no)
+
+ @staticmethod
+ def should_trace(filename):
+ # Don't trace the profiler itself.
+ if 'mini-scalene.py' in filename:
+ return False
+ # Don't trace Python builtins.
+ if '<frozen importlib._bootstrap>' in filename:
+ return False
+ if '<frozen importlib._bootstrap_external>' in filename:
+ return False
+ return True
def main():
- if len(sys.argv) < 2:
- die("[mini-scalene] (Usage): python3 mini-scalene.py file.py {args ...}")
+ assert len(
+ sys.argv) >= 2, "(Usage): python3 mini-scalene.py file.py {args ...}"
script = sys.argv[1]
- sys.argv = sys.argv[1:]
-
- t = threading.Thread(target=sampling_loop, daemon=True)
- t.start()
+ mini_scalene().start()
try:
runpy.run_path(script, run_name="__main__")
- except Exception as e:
- die(f"[mini-scalene] Script died unexpectedly: {e}")
+ except Exception:
+ traceback.print_exc()
if __name__ == "__main__":
diff --git a/tests/simult.py b/tests/simult.py
index a840db5..6228596 100644
--- a/tests/simult.py
+++ b/tests/simult.py
@@ -1,10 +1,12 @@
import asyncio
+
async def count():
print("before")
await asyncio.sleep(1)
print("after")
+
async def main():
await asyncio.gather(count(), count(), count())
print("done")