diff options
-rw-r--r-- | nemesis/causal_event_loop.py | 24 | ||||
-rw-r--r-- | nemesis/html_gen.py | 79 | ||||
-rwxr-xr-x | nemesis/nemesis.py | 37 |
3 files changed, 109 insertions, 31 deletions
diff --git a/nemesis/causal_event_loop.py b/nemesis/causal_event_loop.py index 8514dfc..1a9be98 100644 --- a/nemesis/causal_event_loop.py +++ b/nemesis/causal_event_loop.py @@ -52,6 +52,8 @@ class CausalEventLoop(asyncio.SelectorEventLoop): _time_fd_registered = dict() _time_entered_coro = None _accumulated_time = 0 + _num_processed_tasks = 0 + _total_time_in_coro = 0 _time_dilation = 1.0 _processing = collections.deque() @@ -63,12 +65,22 @@ class CausalEventLoop(asyncio.SelectorEventLoop): _select = self._selector.select def select(timeout: float): - return _select(timeout / self._time_dilation) + return _select(None if timeout == None else timeout / self._time_dilation) self._selector.select = select - def _set_dilation(self, dilation): + def set_dilation(self, dilation): self._time_dilation = dilation + + # reset experiment counters self._time_entered_coro = None + self._num_processed_tasks = 0 + self._total_time_in_coro = 0 + + def get_time_in_coro(self): + return self.total_time_in_coro + + def get_num_processed_tasks(self): + return self._num_processed_tasks def _get_time_in_coro(self): t = self._accumulated_time @@ -168,10 +180,16 @@ class CausalEventLoop(asyncio.SelectorEventLoop): logger.warning('Executing %s took %.3f seconds', _format_handle(handle), dt * 1/self._time_dilation) + time_inside_coro = self._get_time_in_coro() + + # update stat counters + self._total_time_in_coro += time_inside_coro + self._num_processed_tasks += 1 + # calculate the amount of time to slow this callback down by. We only want # to add time proportionate to the amount of time spent OUTSIDE of the # coroutine of interest. - time_outside_coro = dt - self._get_time_in_coro() + time_outside_coro = dt - time_inside_coro delay = time_outside_coro * (1 / self._time_dilation - 1) t0 = super().time() # do it this way so the python interpreter still receives signals. diff --git a/nemesis/html_gen.py b/nemesis/html_gen.py index ea7df15..44c7d56 100644 --- a/nemesis/html_gen.py +++ b/nemesis/html_gen.py @@ -1,38 +1,83 @@ import plotly.graph_objects as go from plotly.subplots import make_subplots +import hashlib + +def get_color(name): + hash_object = hashlib.md5(name.encode()) + color_index = int(hash_object.hexdigest(), 16) % 360 + return f'hsl({color_index}, 100%, 50%)' def plot_results(results, filename): - fig = make_subplots(rows=len(results), cols=1, shared_xaxes=True) + fig = make_subplots(rows=3, cols=1, shared_xaxes=True) for i, (coro_name, x_values) in enumerate(results.items(), start=1): x_list = [] - y_list = [] - hover_text = [] + y_starve_list = [] + y_throughput_list = [] + y_time_in_coro_list = [] + starve_hover_text = [] + throughput_hover_text = [] for speedup, experiments in x_values.items(): for experiment in experiments: - y_value = sum(experiment.values()) + starve = experiment["time_starving"] + processed = experiment["processed"] + time_in_coro = experiment["time_in_coro"] + real_duration = experiment["real_duration"] x_list.append(speedup) - y_list.append(y_value) - breakdown = "<br>".join([f" {key[0]} ({key[1]}): {round(value, 4)}" for key, value in experiment.items()]) - hover_text.append(f"{coro_name}<br>Speedup: {speedup}<br>Total Wait: {round(y_value, 4)}<br>Breakdown:<br>{breakdown}") + # handle starve graph + starve_time = sum(starve.values()) + y_starve_list.append(starve_time) + + breakdown = "<br>".join([f" {key[0]} ({key[1]}): {round(value, 4)}" for key, value in starve.items()]) + starve_hover_text.append(f"({speedup}, {round(starve_time, 4)})<br>{coro_name}<br>Breakdown:<br>{breakdown}") + + # handle throughput graph + throughput = (speedup * processed) / real_duration + y_throughput_list.append(throughput) + + throughput_hover_text.append(f"({speedup}, {round(throughput, 4 )})<br>{coro_name}<br>True experiment duration: {round(real_duration, 4)}<br>Total Processed: {processed}") + + # handle time in coro graph + y_time_in_coro_list.append(time_in_coro / speedup) fig.add_trace(go.Scatter( x=x_list, - y=y_list, + y=y_starve_list, mode='markers', name=coro_name, hoverinfo='text', - hovertext=hover_text, - )) - - fig.update_layout( - title="Potential Speedups for ", - xaxis_title="Speedup (times faster)", - yaxis_title="Total Wait (seconds)", - showlegend=True, - ) + hovertext=starve_hover_text, + marker=dict(color=get_color(coro_name)), + showlegend=True, + ), row=1, col=1) + + fig.add_trace(go.Scatter( + x=x_list, + y=y_throughput_list, + mode='markers', + name=coro_name, + hovertext=throughput_hover_text, + marker=dict(color=get_color(coro_name)), + showlegend=False, + ), row=2, col=1) + + fig.add_trace(go.Scatter( + x=x_list, + y=y_time_in_coro_list, + mode='markers', + name=coro_name, + marker=dict(color=get_color(coro_name)), + showlegend=False, + ), row=3, col=1) + + fig.update_xaxes(title_text="Speedup (times faster)", row=1, col=1) + fig.update_xaxes(title_text="Speedup (times faster)", row=2, col=1) + fig.update_xaxes(title_text="Speedup (times faster)", row=3, col=1) + fig.update_yaxes(title_text="Total Starvation Time (seconds)", row=1, col=1) + fig.update_yaxes(title_text="Total Throughput (handles per second)", row=2, col=1) + fig.update_yaxes(title_text="Synchronous Time in Coroutine (seconds)", row=3, col=1) fig.write_html(filename) diff --git a/nemesis/nemesis.py b/nemesis/nemesis.py index 46e8094..ce5e0a6 100755 --- a/nemesis/nemesis.py +++ b/nemesis/nemesis.py @@ -72,6 +72,10 @@ class Nemesis(object): experiment_coro = None # the speedup of the current experiment experiment_spdp = None + # the total time this experiment has been running + experiment_time = None + # The number of seconds this performance experiment should run + experiment_dura = None # results from previous experiments. Keys represent names of coroutines. results = defaultdict(lambda: defaultdict(lambda: [])) @@ -82,9 +86,6 @@ class Nemesis(object): # The base duration of each performance experiment e_duration = None - # The number of seconds remaining in this performance experiment. - r_duration = None - # A mapping of event loops to the previous running coroutine. prev_coro = defaultdict(lambda: None) @@ -93,7 +94,6 @@ class Nemesis(object): Nemesis.signal_interval = signal_interval Nemesis.e_duration = e_duration Nemesis.filename = filename - Nemesis.r_duration = 0 @staticmethod def start(): @@ -112,24 +112,39 @@ class Nemesis(object): @staticmethod def _start_experiment(coro, speedup): - Nemesis.r_duration = Nemesis.e_duration * speedup + Nemesis.experiment_dura = Nemesis.e_duration * speedup Nemesis.prev_coro = defaultdict(lambda: None) Nemesis.experiment_coro = coro Nemesis.experiment_spdp = speedup + Nemesis.experiment_time = 0 loops = Nemesis._get_event_loops() for l in loops: if not isinstance(l, CausalEventLoop): raise RuntimeException("Nemesis requires a custom event loop to insert slowdowns. It does not work on programs which change the event loop policy.") - l._set_dilation(1.0 / speedup) + l.set_dilation(1.0 / speedup) Nemesis.experiment_data = Experiment(loops) @staticmethod def _stop_experiment(): if Nemesis.experiment_data is not None: - print(f'finished running experiment on {Nemesis.experiment_coro}') - results = Nemesis.experiment_data.samples + loops = Nemesis.experiment_data.get_loops() + + num_processed = 0 + sec_in_coro = 0 + + for loop in loops: + num_processed += loop.get_num_processed_tasks() + sec_in_coro += loop.get_num_processed_tasks() + + print(f'Ran {Nemesis.experiment_coro} at {Nemesis.experiment_spdp} speed') + results = { + "processed": num_processed, + "time_starving": Nemesis.experiment_data.samples, + "time_in_coro": sec_in_coro, + "real_duration": Nemesis.experiment_time, + } Nemesis.results[Nemesis.experiment_coro][Nemesis.experiment_spdp].append(results) del Nemesis.experiment_data @@ -139,7 +154,6 @@ class Nemesis(object): passed_time = curr_sample - Nemesis.last_sample Nemesis.last_sample = curr_sample - if getattr(Nemesis, 'experiment_data', None): loops = Nemesis.experiment_data.get_loops() exp_coro = Nemesis.experiment_coro @@ -157,8 +171,9 @@ class Nemesis(object): handles = Nemesis._get_waiting_handles(loop) Nemesis.experiment_data.add_handles(handles, loop, passed_time) - Nemesis.r_duration -= passed_time - if (Nemesis.r_duration <= 0): + + Nemesis.experiment_time += passed_time + if (Nemesis.experiment_dura <= Nemesis.experiment_time): Nemesis._stop_experiment() else: |