Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/vmstat_visualizer/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import click

import vmstat_visualizer.cli.visualizer as viz
from vmstat_visualizer.cli.multi_compare import compare_multi


@click.group(no_args_is_help=True)
Expand All @@ -17,6 +18,7 @@ def main():
"main is the entrypoint of vmstat-visualizer"
cli.add_command(viz.visualize)
cli.add_command(viz.compare)
cli.add_command(compare_multi)
cli(prog_name="vmstat-visualizer")


Expand Down
49 changes: 49 additions & 0 deletions src/vmstat_visualizer/cli/multi_compare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""CLI command for multi-file comparison."""

import click
from vmstat_visualizer.parser.parser import Parser
from vmstat_visualizer.multi_compare import plot_multi_compare


@click.command("compare-multi", no_args_is_help=True)
@click.argument("files", nargs=-1, type=click.Path(exists=True), required=True)
@click.option(
"-m",
"--metric",
type=click.Choice(["cpu", "memory", "system_load", "swap", "io", "all"],
case_sensitive=False),
required=True,
help="Metric to compare.",
)
@click.option(
"-o",
"--output-prefix",
default="vmstat",
help="Output filename prefix.",
)
@click.option(
"-e",
"--output-extension",
default="png",
help="Output format: png or svg.",
)
def compare_multi(files, metric, output_prefix, output_extension):
"""Compare N vmstat log files on a given metric.

Pass two or more log files as arguments.
"""
if len(files) < 2:
raise click.UsageError("At least 2 files are required for comparison.")

parsers = []
for f in files:
p = Parser(f)
p.parse()
print(f">>> Parsed {len(p.timeseries)} entries from {f}")
parsers.append(p)

plot_multi_compare(
parsers, metric,
output_file_prefix=output_prefix,
output_format=output_extension,
)
136 changes: 136 additions & 0 deletions src/vmstat_visualizer/multi_compare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Multi-file comparison plotting for N vmstat log files."""

import datetime
import os
import time
import matplotlib.pyplot as plt


LINE_STYLES = ['-', '--', '-.', ':', (0, (3, 1, 1, 1))]
COLORS = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6',
'#1abc9c', '#e67e22', '#34495e', '#16a085', '#c0392b']


def _style_for(idx):
"""Return (color, linestyle) for file index."""
color = COLORS[idx % len(COLORS)]
style = LINE_STYLES[idx % len(LINE_STYLES)]
return color, style


def plot_multi_compare(parsers, metric, output_file_prefix='vmstat',
output_format='png'):
"""Compare N parsed files on a given metric.

Args:
parsers: list of Parser instances (already parsed)
metric: one of 'cpu', 'memory', 'system_load', 'swap', 'io', 'all'
output_file_prefix: filename prefix
output_format: 'png' or 'svg'
"""
if metric == 'all':
for m in ['cpu', 'memory', 'system_load', 'swap', 'io']:
plot_multi_compare(parsers, m, output_file_prefix, output_format)
return

from vmstat_visualizer.parser.parser import Parser

all_metrics = []
filenames = []
for p in parsers:
pm = Parser.PlotMetrics(p.timeseries, relative_time=True)
all_metrics.append(pm)
filenames.append(os.path.basename(p.filename))

max_len = max(len(pm.t) for pm in all_metrics)
t = list(range(max_len))

now_unix = int(time.mktime(
datetime.datetime.now().replace(second=0, microsecond=0).timetuple()
))

metric_configs = {
'cpu': {
'title': 'CPU Usage (%) - Multi Comparison',
'ylabel': 'Percent',
'series': [
('User CPU', 'user_cpu_percent'),
('System CPU', 'system_cpu_percent'),
('Idle CPU', 'idle_cpu_percent'),
('Wait CPU', 'wait_cpu_percent'),
('Steal CPU', 'steal_cpu_percent'),
]
},
'memory': {
'title': 'Memory Usage (KB) - Multi Comparison',
'ylabel': 'Memory (KB)',
'series': [
('Free', 'free_memory_kb'),
('Inactive', 'inactive_memory_kb'),
('Active', 'active_memory_kb'),
('Swapped', 'swapped_memory_kb'),
]
},
'system_load': {
'title': 'System Load - Multi Comparison',
'ylabel': 'Processes',
'series': [
('Run Queue', 'run_queue'),
('Blocked', 'blocked_processes'),
]
},
'swap': {
'title': 'Swap Usage - Multi Comparison',
'ylabel': 'KB',
'series': [
('Swap In', 'swap_in_kb'),
('Swap Out', 'swap_out_kb'),
('Swapped', 'swapped_memory_kb'),
]
},
'io': {
'title': 'Block I/O - Multi Comparison',
'ylabel': 'Blocks',
'series': [
('Blocks In', 'blocks_in'),
('Blocks Out', 'blocks_out'),
]
},
}

config = metric_configs[metric]
fig, ax = plt.subplots(figsize=(12, 5))

for file_idx, (pm, fname) in enumerate(zip(all_metrics, filenames)):
color, style = _style_for(file_idx)
for series_name, attr in config['series']:
data = getattr(pm, attr, [])
if not data or all(v is None for v in data):
continue
values = [int(v) for v in data]
ax.plot(
t[:len(values)], values,
label=f'{series_name} ({fname})',
color=color, linestyle=style, linewidth=1.5,
alpha=0.8,
)

ax.set_title(config['title'])
ax.set_xlabel('Relative Time (seconds)')
ax.set_ylabel(config['ylabel'])
ax.set_ylim(bottom=0)
ax.legend(loc='best', fontsize=7, ncol=2)
plt.tight_layout()

starts = []
for p in parsers:
if p.timeseries:
starts.append(f"{os.path.basename(p.filename)}: {p.timeseries[0].time}")
start_text = " | ".join(starts)
plt.figtext(0.99, 0.01, f"Start times: {start_text}",
horizontalalignment='right', fontsize=6, color='gray')

outfile = f'{output_file_prefix}_{metric}_multi_{now_unix}.{output_format}'
plt.savefig(outfile, bbox_inches='tight')
plt.close()
print(f">>> Saved {outfile}")
Loading
Loading