From 98924f4ac0a07cd9d4bb74322364347493e73159 Mon Sep 17 00:00:00 2001 From: bd Date: Mon, 8 Sep 2025 14:50:40 -0400 Subject: Improve reporting format, separate samples based on tident --- nemesis/experiment.py | 32 +++++++++++++++++++++++--------- nemesis/nemesis.py | 34 +++++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 14 deletions(-) (limited to 'nemesis') diff --git a/nemesis/experiment.py b/nemesis/experiment.py index 9d179ea..353d013 100644 --- a/nemesis/experiment.py +++ b/nemesis/experiment.py @@ -16,9 +16,13 @@ class Experiment: # event loops participating in this this experiment loops = [] # a key-value pair where keys represent a handle and values - # represent number of times sampled. + # represent the period of time waiting _samples = None + # the amount of time required for a handle to be included in + # a report + _wait_time_threshold = 0.001 + def __init__(self, coro, speedup): self._coro = coro self._speedup = speedup @@ -35,11 +39,21 @@ class Experiment: def get_results(self): ret = f"Results for {self._coro} at {self._speedup} times speedup:\n" if len(self._samples) > 0: - ret += (f" {'HANDLE':<30} {'LOOP':<10} {'SEC':<10}") + ret += (f" {'NAME':<15} {'FUNC/CORO':<45} {'TIDENT':<16} {'SEC':<10}") + ret += (f"\n {'---':<15} {'---':<45} {'---':<16} {'---':<10}") + tot = 0 for key in self._sort_samples(self._samples): - ret += f"\n {self._get_sample_str(key)}" + name = self._trim_to_last_x_chars(key[0], 15) + ctxt = self._trim_to_last_x_chars(key[1], 45) + tid = key[2] + value = round(self._samples[key] / self._speedup, 4) + tot += value + if value >= self._wait_time_threshold: + ret += f"\n {name:<15} {ctxt:<45} {tid:<16} {value:<10}" + ret += f"\n {'':<79}---" + ret += f"\n {'':<79}{round(tot, 4)}" else: - ret += " No samples were gathered. (This is odd!)" + ret += " No samples were gathered. (This is odd!)" return ret def get_loops(self): @@ -47,7 +61,8 @@ class Experiment: def add_handles(self, handles, loop, time): for h in handles: - self._samples[h.__str__()] += time + h.append(loop._thread_id) + self._samples[tuple(h)] += time def _set_loops(self): self._loops = self._get_event_loops() @@ -81,12 +96,11 @@ class Experiment: frame = frame.f_back return None - def _get_sample_str(self, key): - value = self._samples[key] / self._speedup - return f"{key:29} {value:10}" - def _sort_samples(self, sample_dict): '''Returns SAMPLE_DICT in descending order by number of samples.''' return {k: v for k, v in sorted(sample_dict.items(), key=lambda item: item[1], reverse=True)} + + def _trim_to_last_x_chars(self, string, x): + return string[-x:] if len(string) >= x else string diff --git a/nemesis/nemesis.py b/nemesis/nemesis.py index 69d0c2b..c20d68d 100755 --- a/nemesis/nemesis.py +++ b/nemesis/nemesis.py @@ -25,7 +25,6 @@ Commentary: Code: ''' -from asyncio.base_events import _format_handle from collections import defaultdict from experiment import Experiment import argparse @@ -128,12 +127,21 @@ class Nemesis(object): def _get_waiting_handles(loop): handles = [] for handle in loop._ready: - if (fmt_handle := _format_handle(handle)) is not None \ - and fmt_handle not in handles: - # no duplicates - handles.append(fmt_handle) + # no duplicates + handle_info = Nemesis._parse_handle(handle) + if handle_info not in handles: + handles.append(handle_info) return handles + def _parse_handle(handle): + cb = handle._callback + if isinstance(getattr(cb, '__self__', None), asyncio.tasks.Task): + task = cb.__self__ + coro = task.get_coro() + return [task.get_name(), Nemesis.get_coro_name(coro)] + else: + return [str(type(handle).__name__), cb.__name__] + def _get_current_coro(loop): tid = loop._thread_id assert tid, f"{loop} is not running, yet we attempted to sample it!" @@ -173,6 +181,22 @@ class Nemesis(object): return False return True + def get_coro_name(coro): + ''' + Stolen from _format_coroutine in cpython/Lib/asyncio/coroutines.py + ''' + # Coroutines compiled with Cython sometimes don't have + # proper __qualname__ or __name__. While that is a bug + # in Cython, asyncio shouldn't crash with an AttributeError + # in its __repr__ functions. + if hasattr(coro, '__qualname__') and coro.__qualname__: + coro_name = coro.__qualname__ + elif hasattr(coro, '__name__') and coro.__name__: + coro_name = coro.__name__ + else: + # Stop masking Cython bugs, expose them in a friendly way. + coro_name = f'<{type(coro).__name__} without __name__>' + return f'{coro_name}()' the_globals = { '__name__': '__main__', -- cgit v1.2.3