summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mini-scalene.py90
-rw-r--r--tests/overload.py (renamed from tests/simult-busy.py)1
2 files changed, 76 insertions, 15 deletions
diff --git a/mini-scalene.py b/mini-scalene.py
index 01a8de2..4d3909b 100644
--- a/mini-scalene.py
+++ b/mini-scalene.py
@@ -1,33 +1,63 @@
import sys
+import argparse
import threading
import traceback
import runpy
import atexit
import signal
import asyncio
-import inspect
from typing import cast
from types import FrameType
from collections import defaultdict
+def parse_arguments():
+ '''Parse CLI args'''
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument('-a', '--async_off',
+ action='store_false',
+ help='Turn off experimental async profiling.',
+ default=True)
+
+ parser.add_argument('script', help='A python script to run')
+ parser.add_argument('s_args', nargs=argparse.REMAINDER,
+ help='python script args')
+
+ return parser.parse_args()
+
+
class mini_scalene:
+ '''A stripped-down version of SCALENE which tallies active lines during
+ execution.'''
+
+ # a key-value pair where keys represent frame metadata (see
+ # mini_scalene.frame_to_string) and values represent number of times
+ # sampled.
cpu_samples = defaultdict(lambda: 0)
total_cpu_samples = 0
+
+ # the time, in seconds, between samples
signal_interval = 0.01
+ # if we should try to profile asynchronous code. Used to observe
+ # effectiveness of the implementation.
+ profile_async = True
def __init__(self):
- signal.signal(signal.SIGPROF, self.cpu_signal_handler)
+ signal.signal(signal.SIGPROF,
+ self.cpu_signal_handler)
signal.setitimer(signal.ITIMER_PROF,
- self.signal_interval, self.signal_interval)
+ self.signal_interval,
+ self.signal_interval)
@staticmethod
- def start():
+ def start(profile_async):
+ mini_scalene.profile_async = profile_async
atexit.register(mini_scalene.exit_handler)
@staticmethod
def exit_handler():
- # Turn off the profiling signals.
+ '''Turn off our profiling signals and pretty-print profiling information.'''
signal.signal(signal.ITIMER_PROF, signal.SIG_IGN)
signal.signal(signal.SIGVTALRM, signal.SIG_IGN)
signal.setitimer(signal.ITIMER_PROF, 0)
@@ -36,10 +66,15 @@ class mini_scalene:
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)}
+ 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)")
+ 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)")
@@ -53,11 +88,13 @@ class mini_scalene:
@staticmethod
def compute_frames_to_record(this_frame):
- """Collects all stack frames that Scalene actually processes."""
+ '''Collects all stack frames that Scalene actually processes.'''
frames = [this_frame]
frames += [sys._current_frames().get(t.ident, None)
for t in threading.enumerate()]
frames += mini_scalene.get_async_frames()
+
+ frames = mini_scalene.filter_duplicated_frames(frames)
# Process all the frames to remove ones we aren't going to track.
new_frames = []
for frame in frames:
@@ -87,6 +124,8 @@ class mini_scalene:
@staticmethod
def frame_to_string(frame):
+ '''Pretty-prints a frame as a function/file name and a line number.
+ Additionally used a key for tallying lines.'''
co = frame.f_code
func_name = co.co_name
line_no = frame.f_lineno
@@ -95,13 +134,15 @@ class mini_scalene:
@staticmethod
def get_async_frames():
- if mini_scalene.is_event_loop_running():
+ '''Obtains the stack frames of all currently executing tasks.'''
+ if mini_scalene.is_event_loop_running() and mini_scalene.profile_async:
return [task.get_coro().cr_frame for task in asyncio.all_tasks()]
return []
@staticmethod
def should_trace(filename):
- # We're assuming Guix System. That makes it easy.
+ '''Returns FALSE if filename is uninteresting to the user.'''
+ # FIXME Assume GuixSD. Makes filtering easy
if '/gnu/store' in filename:
return False
if 'mini-scalene.py' in filename:
@@ -114,18 +155,37 @@ class mini_scalene:
@staticmethod
def is_event_loop_running() -> bool:
+ '''Returns TRUE if there is an exent loop running. This is what
+ `asyncio.get_event_loop()' did, before it was deprecated in 3.12'''
return asyncio.get_event_loop_policy()._local._loop is not None
+ @staticmethod
+ def filter_duplicated_frames(frames) -> bool:
+ s = set()
+ dup = []
+ for f in frames:
+ if f in s:
+ dup.append(f)
+ else:
+ s.add(f)
+ # TODO we probably have one because given get_async_frames returns the
+ # currently executing task. Would be an easy fix in that method.
+ # if there's more than one, I cannot explain it.
+ assert len(
+ dup) < 2, f"ERROR: More than 1 duplicate frame (shouldn't happen): {dup}"
+ if len(dup) != 0:
+ print(f"WARN: Duplicate frame found: {dup}", file=sys.stderr)
+ return list(s)
def main():
- assert len(sys.argv) >= 2, "(Usage): python3 mini-scalene.py file.py {args ...}"
+ args = parse_arguments()
- script = sys.argv[1]
- mini_scalene().start()
+ mini_scalene().start(args.async_off)
+ sys.argv = [args.script] + args.s_args
try:
- runpy.run_path(script, run_name="__main__")
+ runpy.run_path(args.script, run_name="__main__")
except Exception:
traceback.print_exc()
diff --git a/tests/simult-busy.py b/tests/overload.py
index 4cc4b2a..21ee125 100644
--- a/tests/simult-busy.py
+++ b/tests/overload.py
@@ -16,6 +16,7 @@ async def main():
while True:
if time.time() - start_time > 3.5:
break
+ # print(asyncio.all_tasks())
await asyncio.sleep(0) # yield
except KeyboardInterrupt:
pass