All checks were successful
		
		
	
	Run Check Script / check (pull_request) Successful in 1m3s
				
			
		
			
				
	
	
		
			230 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			230 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import dash
 | 
						|
from dash import dcc, html, Input, Output, State, clientside_callback, ClientsideFunction
 | 
						|
import plotly.express as px
 | 
						|
import pandas as pd
 | 
						|
import dash_bootstrap_components as dbc
 | 
						|
import io
 | 
						|
 | 
						|
# --- Data Loading and Preparation ---
 | 
						|
# csv_data = """label,test_name,iops,bandwidth_kibps,latency_mean_ms,latency_stddev_ms
 | 
						|
# Ceph HDD Only,read-4k-sync-test,1474.302,5897,0.673,0.591
 | 
						|
# Ceph HDD Only,write-4k-sync-test,14.126,56,27.074,7.046
 | 
						|
# Ceph HDD Only,randread-4k-sync-test,225.140,900,4.436,6.918
 | 
						|
# Ceph HDD Only,randwrite-4k-sync-test,13.129,52,34.891,10.859
 | 
						|
# Ceph HDD Only,multiread-4k-sync-test,6873.675,27494,0.578,0.764
 | 
						|
# Ceph HDD Only,multiwrite-4k-sync-test,57.135,228,38.660,11.293
 | 
						|
# Ceph HDD Only,multirandread-4k-sync-test,2451.376,9805,1.626,2.515
 | 
						|
# Ceph HDD Only,multirandwrite-4k-sync-test,54.642,218,33.492,13.111
 | 
						|
# Ceph 2 Hosts WAL+DB SSD and 1 Host HDD,read-4k-sync-test,1495.700,5982,0.664,1.701
 | 
						|
# Ceph 2 Hosts WAL+DB SSD and 1 Host HDD,write-4k-sync-test,16.990,67,17.502,9.908
 | 
						|
# Ceph 2 Hosts WAL+DB SSD and 1 Host HDD,randread-4k-sync-test,159.256,637,6.274,9.232
 | 
						|
# Ceph 2 Hosts WAL+DB SSD and 1 Host HDD,randwrite-4k-sync-test,16.693,66,24.094,16.099
 | 
						|
# Ceph 2 Hosts WAL+DB SSD and 1 Host HDD,multiread-4k-sync-test,7305.559,29222,0.544,1.338
 | 
						|
# Ceph 2 Hosts WAL+DB SSD and 1 Host HDD,multiwrite-4k-sync-test,52.260,209,34.891,17.576
 | 
						|
# Ceph 2 Hosts WAL+DB SSD and 1 Host HDD,multirandread-4k-sync-test,700.606,2802,5.700,10.429
 | 
						|
# Ceph 2 Hosts WAL+DB SSD and 1 Host HDD,multirandwrite-4k-sync-test,52.723,210,29.709,25.829
 | 
						|
# Ceph 2 Hosts WAL+DB SSD Only,randwrite-4k-sync-test,90.037,360,3.617,8.321
 | 
						|
# Ceph WAL+DB SSD During Rebuild,randwrite-4k-sync-test,41.008,164,10.138,19.333
 | 
						|
# Ceph WAL+DB SSD OSD HDD,read-4k-sync-test,1520.299,6081,0.654,1.539
 | 
						|
# Ceph WAL+DB SSD OSD HDD,write-4k-sync-test,78.528,314,4.074,9.101
 | 
						|
# Ceph WAL+DB SSD OSD HDD,randread-4k-sync-test,153.303,613,6.518,9.036
 | 
						|
# Ceph WAL+DB SSD OSD HDD,randwrite-4k-sync-test,48.677,194,8.785,20.356
 | 
						|
# Ceph WAL+DB SSD OSD HDD,multiread-4k-sync-test,6804.880,27219,0.584,1.422
 | 
						|
# Ceph WAL+DB SSD OSD HDD,multiwrite-4k-sync-test,311.513,1246,4.978,9.458
 | 
						|
# Ceph WAL+DB SSD OSD HDD,multirandread-4k-sync-test,581.756,2327,6.869,10.204
 | 
						|
# Ceph WAL+DB SSD OSD HDD,multirandwrite-4k-sync-test,120.556,482,13.463,25.440
 | 
						|
# """
 | 
						|
#
 | 
						|
# df = pd.read_csv(io.StringIO(csv_data))
 | 
						|
df = pd.read_csv("iobench.csv")  # Replace with the actual file path
 | 
						|
df['bandwidth_mbps'] = df['bandwidth_kibps'] / 1024
 | 
						|
 | 
						|
# --- App Initialization and Global Settings ---
 | 
						|
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.FLATLY])
 | 
						|
 | 
						|
# Create master lists of options for checklists
 | 
						|
unique_labels = sorted(df['label'].unique())
 | 
						|
unique_tests = sorted(df['test_name'].unique())
 | 
						|
 | 
						|
# Create a consistent color map for each unique label
 | 
						|
color_map = {label: color for label, color in zip(unique_labels, px.colors.qualitative.Plotly)}
 | 
						|
 | 
						|
# --- App Layout ---
 | 
						|
app.layout = dbc.Container([
 | 
						|
    # Header
 | 
						|
    dbc.Row(dbc.Col(html.H1("Ceph iobench Performance Dashboard", className="text-primary"),), className="my-4 text-center"),
 | 
						|
 | 
						|
    # Controls and Graphs Row
 | 
						|
    dbc.Row([
 | 
						|
        # Control Panel Column
 | 
						|
        dbc.Col([
 | 
						|
            dbc.Card([
 | 
						|
                dbc.CardBody([
 | 
						|
                    html.H4("Control Panel", className="card-title"),
 | 
						|
                    html.Hr(),
 | 
						|
                    
 | 
						|
                    # Metric Selection
 | 
						|
                    dbc.Label("1. Select Metrics to Display:", html_for="metric-checklist", className="fw-bold"),
 | 
						|
                    dcc.Checklist(
 | 
						|
                        id='metric-checklist',
 | 
						|
                        options=[
 | 
						|
                            {'label': 'IOPS', 'value': 'iops'},
 | 
						|
                            {'label': 'Latency (ms)', 'value': 'latency_mean_ms'},
 | 
						|
                            {'label': 'Bandwidth (MB/s)', 'value': 'bandwidth_mbps'}
 | 
						|
                        ],
 | 
						|
                        value=['iops', 'latency_mean_ms', 'bandwidth_mbps'], # Default selection
 | 
						|
                        labelClassName="d-block"
 | 
						|
                    ),
 | 
						|
                    html.Hr(),
 | 
						|
 | 
						|
                    # Configuration Selection
 | 
						|
                    dbc.Label("2. Select Configurations:", html_for="config-checklist", className="fw-bold"),
 | 
						|
                    dbc.ButtonGroup([
 | 
						|
                        dbc.Button("All", id="config-select-all", n_clicks=0, color="primary", outline=True, size="sm"),
 | 
						|
                        dbc.Button("None", id="config-select-none", n_clicks=0, color="primary", outline=True, size="sm"),
 | 
						|
                    ], className="mb-2"),
 | 
						|
                    dcc.Checklist(
 | 
						|
                        id='config-checklist',
 | 
						|
                        options=[{'label': label, 'value': label} for label in unique_labels],
 | 
						|
                        value=unique_labels, # Select all by default
 | 
						|
                        labelClassName="d-block"
 | 
						|
                    ),
 | 
						|
                    html.Hr(),
 | 
						|
                    
 | 
						|
                    # Test Name Selection
 | 
						|
                    dbc.Label("3. Select Tests:", html_for="test-checklist", className="fw-bold"),
 | 
						|
                    dbc.ButtonGroup([
 | 
						|
                        dbc.Button("All", id="test-select-all", n_clicks=0, color="primary", outline=True, size="sm"),
 | 
						|
                        dbc.Button("None", id="test-select-none", n_clicks=0, color="primary", outline=True, size="sm"),
 | 
						|
                    ], className="mb-2"),
 | 
						|
                    dcc.Checklist(
 | 
						|
                        id='test-checklist',
 | 
						|
                        options=[{'label': test, 'value': test} for test in unique_tests],
 | 
						|
                        value=unique_tests, # Select all by default
 | 
						|
                        labelClassName="d-block"
 | 
						|
                    ),
 | 
						|
                ])
 | 
						|
            ], className="mb-4")
 | 
						|
        ], width=12, lg=4),
 | 
						|
 | 
						|
        # Graph Display Column
 | 
						|
        dbc.Col(id='graph-container', width=12, lg=8)
 | 
						|
    ])
 | 
						|
], fluid=True)
 | 
						|
 | 
						|
 | 
						|
# --- Callbacks ---
 | 
						|
 | 
						|
# Callback to handle "Select All" / "Select None" for configurations
 | 
						|
@app.callback(
 | 
						|
    Output('config-checklist', 'value'),
 | 
						|
    Input('config-select-all', 'n_clicks'),
 | 
						|
    Input('config-select-none', 'n_clicks'),
 | 
						|
    prevent_initial_call=True
 | 
						|
)
 | 
						|
def select_all_none_configs(all_clicks, none_clicks):
 | 
						|
    ctx = dash.callback_context
 | 
						|
    if not ctx.triggered:
 | 
						|
        return dash.no_update
 | 
						|
    
 | 
						|
    button_id = ctx.triggered[0]['prop_id'].split('.')[0]
 | 
						|
    if button_id == 'config-select-all':
 | 
						|
        return unique_labels
 | 
						|
    elif button_id == 'config-select-none':
 | 
						|
        return []
 | 
						|
    return dash.no_update
 | 
						|
 | 
						|
# Callback to handle "Select All" / "Select None" for tests
 | 
						|
@app.callback(
 | 
						|
    Output('test-checklist', 'value'),
 | 
						|
    Input('test-select-all', 'n_clicks'),
 | 
						|
    Input('test-select-none', 'n_clicks'),
 | 
						|
    prevent_initial_call=True
 | 
						|
)
 | 
						|
def select_all_none_tests(all_clicks, none_clicks):
 | 
						|
    ctx = dash.callback_context
 | 
						|
    if not ctx.triggered:
 | 
						|
        return dash.no_update
 | 
						|
    
 | 
						|
    button_id = ctx.triggered[0]['prop_id'].split('.')[0]
 | 
						|
    if button_id == 'test-select-all':
 | 
						|
        return unique_tests
 | 
						|
    elif button_id == 'test-select-none':
 | 
						|
        return []
 | 
						|
    return dash.no_update
 | 
						|
 | 
						|
 | 
						|
# Main callback to update graphs based on all selections
 | 
						|
@app.callback(
 | 
						|
    Output('graph-container', 'children'),
 | 
						|
    [Input('metric-checklist', 'value'),
 | 
						|
     Input('config-checklist', 'value'),
 | 
						|
     Input('test-checklist', 'value')]
 | 
						|
)
 | 
						|
def update_graphs(selected_metrics, selected_configs, selected_tests):
 | 
						|
    """
 | 
						|
    This function is triggered when any control's value changes.
 | 
						|
    It generates and returns a list of graphs based on all user selections.
 | 
						|
    """
 | 
						|
    # Handle cases where no selection is made to prevent errors and show a helpful message
 | 
						|
    if not all([selected_metrics, selected_configs, selected_tests]):
 | 
						|
        return dbc.Alert(
 | 
						|
            "Please select at least one item from each category (Metric, Configuration, and Test) to view data.",
 | 
						|
            color="info",
 | 
						|
            className="mt-4"
 | 
						|
        )
 | 
						|
 | 
						|
    # Filter the DataFrame based on all selected criteria
 | 
						|
    filtered_df = df[df['label'].isin(selected_configs) & df['test_name'].isin(selected_tests)]
 | 
						|
    
 | 
						|
    # If the filtered data is empty after selection, inform the user
 | 
						|
    if filtered_df.empty:
 | 
						|
        return dbc.Alert("No data available for the current selection.", color="warning", className="mt-4")
 | 
						|
 | 
						|
    graph_list = []
 | 
						|
    metric_titles = {
 | 
						|
        'iops': 'IOPS Comparison (Higher is Better)',
 | 
						|
        'latency_mean_ms': 'Mean Latency (ms) Comparison (Lower is Better)',
 | 
						|
        'bandwidth_mbps': 'Bandwidth (MB/s) Comparison (Higher is Better)'
 | 
						|
    }
 | 
						|
 | 
						|
    for metric in selected_metrics:
 | 
						|
        sort_order = 'total ascending' if metric == 'latency_mean_ms' else 'total descending'
 | 
						|
        error_y_param = 'latency_stddev_ms' if metric == 'latency_mean_ms' else None
 | 
						|
 | 
						|
        fig = px.bar(
 | 
						|
            filtered_df,
 | 
						|
            x='test_name',
 | 
						|
            y=metric,
 | 
						|
            color='label',
 | 
						|
            barmode='group',
 | 
						|
            color_discrete_map=color_map,
 | 
						|
            error_y=error_y_param,
 | 
						|
            title=metric_titles.get(metric, metric),
 | 
						|
            labels={
 | 
						|
                "test_name": "Benchmark Test Name",
 | 
						|
                "iops": "IOPS",
 | 
						|
                "latency_mean_ms": "Mean Latency (ms)",
 | 
						|
                "bandwidth_mbps": "Bandwidth (MB/s)",
 | 
						|
                "label": "Cluster Configuration"
 | 
						|
            }
 | 
						|
        )
 | 
						|
 | 
						|
        fig.update_layout(
 | 
						|
            height=500,
 | 
						|
            xaxis_title=None,
 | 
						|
            legend_title="Configuration",
 | 
						|
            title_x=0.5,
 | 
						|
            xaxis={'categoryorder': sort_order},
 | 
						|
            xaxis_tickangle=-45,
 | 
						|
            margin=dict(b=120) # Add bottom margin to prevent tick labels from being cut off
 | 
						|
        )
 | 
						|
        
 | 
						|
        graph_list.append(dbc.Row(dbc.Col(dcc.Graph(figure=fig)), className="mb-4"))
 | 
						|
 | 
						|
    return graph_list
 | 
						|
 | 
						|
# --- Run the App ---
 | 
						|
if __name__ == '__main__':
 | 
						|
    app.run(debug=True)
 |