Files
gossipsub-hardening/scripts/Analysis-Template.ipynb
Yusef Napora 2ecf3e4cd3 add mesh count chart to notebook
also changes DERIVE_MESHES to default to false
2020-05-26 14:21:58 -04:00

577 lines
18 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Pubsub Test Analysis Notebook\n",
"\n",
"## Usage\n",
"\n",
"This notebook analyzes the output of a single pubsub test execution.\n",
"\n",
"You must run all the cells (`Cell > Run All` menu) to load the data and generate the charts.\n",
"\n",
"This may take quite some time and require considerable RAM on the first\n",
"run, but the results will be cached to `ANALYSIS_DIR/pandas` for future runs.\n",
"\n",
"<p/>\n",
"\n",
"<details>\n",
" <summary>Expand to show example data</summary>\n",
"\n",
"### `scores`\n",
"\n",
"The `scores` `DataFrame` contains peer score events, indexed by timestamp.\n",
"\n",
"**Example**:\n",
"\n",
"| timestamp | observer | peer | score |\n",
"|-------------------------------|------------------------------------------------------|------------------------------------------------------|-----------|\n",
"| 2020-03-31 16:46:22.675526100 | 12D3KooWM1Q8EazdTBaYidtmAnaEqjtAzCGkYypzjgUmDU3bNpAS | 12D3KooWGHEfmKenMpsGDu1xEuVw7itsEMMT6oBVbYx642627Etw | 0.0027 |\n",
"| 2020-03-31 16:46:22.675526100 | 12D3KooWM1Q8EazdTBaYidtmAnaEqjtAzCGkYypzjgUmDU3bNpAS | 12D3KooWN5fa46NhuVP9as8ANmZ54gAM5cPuhTuu1bMAY5wvgoPG | 16.5617 |\n",
"\n",
"\n",
"- `observer` is the peer assigning the score\n",
"- `peer` is the peer receiving the score\n",
"\n",
"### `metrics`\n",
"\n",
"The `metrics` `DataFrame` contains aggregated tracer metrics.\n",
"\n",
"**Example**:\n",
"\n",
"| | published | rejected | delivered | duplicates | droppedrpc | peersadded | peersremoved | topicsjoined | topicsleft | peer | sent_rpcs | sent_messages | sent_grafts | sent_prunes | sent_iwants | sent_ihaves | recv_rpcs | recv_messages | recv_grafts | recv_prunes | recv_iwants | recv_ihaves |\n",
"|----|-------------|------------|-------------|--------------|--------------|--------------|----------------|----------------|--------------|------------------------------------------------------|-------------|-----------------|---------------|---------------|---------------|---------------|-------------|-----------------|---------------|---------------|---------------|---------------|\n",
"| 0 | 0 | 0 | 721 | 0 | 0 | 2 | 1 | 1 | 0 | 12D3KooWM1Q8EazdTBaYidtmAnaEqjtAzCGkYypzjgUmDU3bNpAS | 722 | 721 | 0 | 0 | 0 | 0 | 727 | 721 | 2 | 0 | 0 | 0 |\n",
"| 1 | 0 | 0 | 721 | 0 | 0 | 2 | 1 | 1 | 0 | 12D3KooWN5fa46NhuVP9as8ANmZ54gAM5cPuhTuu1bMAY5wvgoPG | 724 | 721 | 1 | 0 | 0 | 0 | 726 | 721 | 1 | 0 | 0 | 0 |\n",
"\n",
" \n",
"</details>"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": [
"parameters"
]
},
"outputs": [],
"source": [
"# Parameters in this cell can be overriden using papermill\n",
"\n",
"# path to directory contaning output from the extract_test_outputs method in analyze.py\n",
"ANALYSIS_DIR=\".\"\n",
"\n",
"# dir to save figure images\n",
"FIGURE_OUT=\"./figures\"\n",
"\n",
"# path to zip file containing all figures\n",
"FIGURE_ZIP_OUT=\"./figures.zip\"\n",
"\n",
"# font sizes\n",
"SMALL_SIZE = 8\n",
"MEDIUM_SIZE = 10\n",
"BIGGER_SIZE = 18\n",
"\n",
"# If no cached 'meshes.gz' file exists when loading data, setting\n",
"# DERIVE_MESHES to True causes the mesh state to be derived from\n",
"# trace events. This will add several minutes to the load time,\n",
"# but the results will be cached to disk.\n",
"DERIVE_MESHES=False\n",
"\n",
"# When DERIVE_MESHES is true, you can change the sample frequency\n",
"# by setting MESH_SAMPLE_FREQ. This controls the size of the time window\n",
"# for each mesh \"snapshot\".\n",
"MESH_SAMPLE_FREQ='5s'"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import pandas as pd\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import toml\n",
"import ipywidgets as widgets\n",
"from pprint import pprint\n",
"import pathlib\n",
"import seaborn as sns\n",
"from durations import Duration\n",
"\n",
"import notebook_helper\n",
"from notebook_helper import no_scores_message, load_pandas, archive_figures, p25, p50, p75, p95, p99\n",
"\n",
"# load a helper to save figures to FIGURE_OUT\n",
"save_fig = notebook_helper.save_fig_fn(FIGURE_OUT)\n",
"\n",
"# render charts in a larger, zoomable style\n",
"%matplotlib notebook\n",
"\n",
"# turn off autosaving for the notebook\n",
"%autosave 0\n",
"\n",
"# prettify the colors\n",
"sns.set(color_codes=True)\n",
"\n",
"# helper to set font sizes for charts\n",
"def set_chart_fontsize(size):\n",
" plt.rc('font', size=size) # controls default text sizes\n",
" plt.rc('axes', titlesize=size) # fontsize of the axes title\n",
" plt.rc('axes', labelsize=size) # fontsize of the x and y labels\n",
" plt.rc('xtick', labelsize=size) # fontsize of the tick labels\n",
" plt.rc('ytick', labelsize=size) # fontsize of the tick labels\n",
" plt.rc('legend', fontsize=size) # legend fontsize\n",
" plt.rc('figure', titlesize=size) # fontsize of the figure title\n",
"\n",
" \n",
"# set chart fonts to BIGGER_SIZE by default\n",
"set_chart_fontsize(BIGGER_SIZE)\n",
"\n",
"# load data\n",
"print('loading test data from ' + ANALYSIS_DIR)\n",
"\n",
"tables = load_pandas(ANALYSIS_DIR, derive_meshes_if_missing=DERIVE_MESHES, mesh_sample_freq=MESH_SAMPLE_FREQ)\n",
"scores = tables['scores']\n",
"metrics = tables['metrics']\n",
"cdf = tables['cdf']\n",
"pdf = tables['pdf']\n",
"peers = tables['peers']\n",
"meshes = tables.get('meshes', notebook_helper.empty_meshes_table())\n",
"\n",
"# resample score index into 5s windows for plotting later\n",
"if not scores.empty:\n",
" print('resampling peer scores')\n",
" resample_interval = '5s'\n",
" sampled = scores.resample(resample_interval)\n",
" honest_sampled = scores.where(scores['honest']).resample(resample_interval)\n",
" attacker_sampled = scores.where(scores['honest'] == False).resample(resample_interval)\n",
"\n",
"honest_peers = peers.where(peers['honest']).dropna(how='all')\n",
"attack_peers = peers.where(~peers['honest']).dropna(how='all').dropna('columns')\n",
"\n",
"t_warm = honest_peers['t_warm'].max()\n",
"t_run = honest_peers['t_run'].max()\n",
"t_cool = honest_peers['t_cool'].max()\n",
"t_complete = honest_peers['t_complete'].max()\n",
"\n",
"# TODO: support multiple waves of attackers with different connect times\n",
"attack_connect_start = attack_peers['t_connect'].min()\n",
"attack_connect_end = attack_peers['t_connect'].max()\n",
"\n",
"params_panel, test_params = notebook_helper.test_params_panel(ANALYSIS_DIR)\n",
"\n",
"try:\n",
" s = test_params['PEER_SCORE_PARAMS']['Topics']['blocks']['MeshMessageDeliveriesActivation']\n",
" d = Duration(s)\n",
" d = pd.Timedelta(d.to_seconds(), unit='seconds')\n",
" mesh_activation_start = attack_connect_start + d\n",
" mesh_activation_end = attack_connect_end + d\n",
"except BaseException as err:\n",
" print('error determining mesh activation window: ', err) \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Test Parameters\n",
"\n",
"The cell below shows the parameters that were used to create the composition file for this test run.\n",
"\n",
"You can access the parameter values from other cells via the `test_params` dict, e.g.:\n",
"\n",
"```python\n",
"from durations import Duration\n",
"warmup = Duration(test_params['T_WARM'])\n",
"print('warmup seconds: {}'.format(warmup.to_seconds()))\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"params_panel"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Aggregations"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Latency Distribution"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### CDF"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig = notebook_helper.plot_latency_cdf(cdf)\n",
"save_fig(fig, 'latency-cdf')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### PDF"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig = notebook_helper.plot_latency_pdf(pdf)\n",
"save_fig(fig, 'latency-pdf')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### PDF (above p99)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig = notebook_helper.plot_latency_pdf_above_quantile(pdf, quantile=0.99)\n",
"save_fig(fig, 'latency-pdf-over-p99')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Tracestat summary\n",
"Only Publish and Deliver counts are accurate, the rest are filtered."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(notebook_helper.tracestat_summary(ANALYSIS_DIR))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Aggregated tracer metrics (per-peer)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"metrics[['published', 'delivered', 'rejected', 'duplicates', 'droppedrpc']].agg([np.min, np.max, np.median, np.mean]).rename(columns={'amax': 'max', 'amin': 'min', 'amedian': 'median', 'amean': 'mean'})\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### All peer scores, aggregated across the test runtime"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"if not scores.empty:\n",
" scores['score'].agg({'min': np.min, 'max': np.max, 'median': np.median, 'mean': np.mean})\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Aggregated score values for peers with negative scores\n",
"\n",
"- `observer` is the peer assigning the score. \n",
"- `peer` is the peer receiving the score."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"if not scores.empty:\n",
" neg = scores.where(scores['score'] < 0).groupby(['peer', 'observer'])\n",
" n = neg.agg({'score': [np.min, np.max, np.median, np.mean]})\n",
" n\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Show honest peers with negative scores, joined with tracer metrics"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"if not scores.empty:\n",
" # select columns from metrics table\n",
" m = metrics[['peer', 'published', 'delivered', 'rejected']]\n",
"\n",
" pd.DataFrame(n).merge(m, on='peer').groupby('peer').head()\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### global min/max score over time"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"time_annotations = [\n",
" {'label': 'warmup complete', 'time': t_run},\n",
" {'label': 'attackers connect', 'time': attack_connect_start, 'end_time': attack_connect_end},\n",
" {'label': 'delivery penalty activation', 'time': mesh_activation_start, 'end_time': mesh_activation_end},\n",
" {'label': 'cooldown begin', 'time': t_cool},\n",
"]\n",
"\n",
"def annotate_score_plot(plot, label, legend_anchor=None):\n",
" notebook_helper.annotate_score_plot(plot, label, legend_anchor=legend_anchor, time_annotations=time_annotations)\n",
"\n",
"if not scores.empty:\n",
" fig, ax = plt.subplots(4, figsize=(11, 12))\n",
" plt.subplots_adjust(hspace=0.5)\n",
" fig.suptitle(\"Min / Max Peer Scores\")\n",
" plot = sampled['score'].agg([np.min, np.max]).plot(ax=ax[0])\n",
" annotate_score_plot(plot, 'All Peers min/max')\n",
" \n",
" plot = honest_sampled['score'].agg([np.min, np.max]).plot(ax=ax[1])\n",
" annotate_score_plot(plot, 'Honest Peers min/max')\n",
" \n",
" plot = attacker_sampled['score'].agg([np.min, np.max]).plot(ax=ax[2])\n",
" \n",
" # draw last plot with legend anchored below\n",
" legends = annotate_score_plot(plot, 'Attacker Peers min/max', legend_anchor=(0, -0.2))\n",
" \n",
" # hide bottom plot so legend is visible\n",
" ax[3].set_visible(False)\n",
" \n",
" # write png\n",
" save_fig(fig, 'score-min-max')\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### global mean / median score over time"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"if not scores.empty: \n",
" # create 4 subplots - three for charts and one that we'll hide to make blank space\n",
" # for the legend\n",
" fig, ax = plt.subplots(4, figsize=(11, 12))\n",
" plt.subplots_adjust(hspace=0.5)\n",
" fig.suptitle('Mean / Median Peer Scores')\n",
" plot = sampled['score'].agg([np.mean, np.median]).plot(ax=ax[0])\n",
" annotate_score_plot(plot, 'All Peers mean/median')\n",
" \n",
" plot = honest_sampled['score'].agg([np.mean, np.median]).plot(ax=ax[1])\n",
" annotate_score_plot(plot, 'Honest Peers mean/median')\n",
" \n",
" plot = attacker_sampled['score'].agg([np.mean, np.median]).plot(ax=ax[2])\n",
" annotate_score_plot(plot, 'Attacker Peers mean/median', legend_anchor=(0, -0.2))\n",
" \n",
" # hide bottom plot so legend is visible\n",
" ax[3].set_visible(False)\n",
" \n",
" save_fig(fig, 'score-mean-median')\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### mean score distribution (all peers)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"if not scores.empty:\n",
" plot = scores[['peer', 'score']].groupby('peer').mean().plot.hist(bins=20)\n",
" fig = plot.get_figure()\n",
" fig.suptitle('Global score distribution (all peers)')\n",
" save_fig(fig, 'score-global-distribution')\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### score distributions (honest vs attacker)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"aggregations = [np.min, np.max, p25, p75, p95]\n",
"kwargs = {'kind': 'hist', 'subplots': True, 'sharex': True, 'sharey': True, 'figsize': (8, 10)}\n",
"\n",
"if not scores.empty:\n",
" honest_by_peer = scores.where(scores['honest']).groupby('peer')\n",
" attacker_by_peer = scores.where(scores['honest'] == False).groupby('peer')\n",
"\n",
" plots = honest_by_peer['score'].agg(aggregations).plot(title='Score distributions (honest peers)', **kwargs)\n",
" for p in plots:\n",
" p.set_ylabel('freq')\n",
" fig = plots[0].get_figure()\n",
" save_fig(fig, 'score-distributions-honest')\n",
"\n",
"\n",
" plots = attacker_by_peer['score'].agg(aggregations).plot(title='Score distributions (attacker peers)', **kwargs)\n",
" for p in plots:\n",
" p.set_ylabel('freq')\n",
" fig = plots[0].get_figure()\n",
" save_fig(fig, 'score-distributions-attacker')\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "code",
"execution_count": null,
"outputs": [],
"source": [
"if not meshes.empty:\n",
" fig = plt.figure()\n",
" m = meshes.drop(columns=['mesh', 'peer'])\n",
" sns.relplot(kind=\"line\", ci=\"sd\", data=m)\n",
" save_fig(fig, 'mesh-counts')\n",
"else:\n",
" notebook_helper.no_meshes_message()"
],
"metadata": {
"collapsed": false,
"pycharm": {
"name": "#%%\n"
}
}
},
{
"cell_type": "code",
"execution_count": null,
"outputs": [],
"source": [
"# zip all figure images into a bundle\n",
"archive_figures(FIGURE_OUT, FIGURE_ZIP_OUT)"
],
"metadata": {
"collapsed": false,
"pycharm": {
"name": "#%%\n"
}
}
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.7"
}
},
"nbformat": 4,
"nbformat_minor": 4
}