Skip to content
Merged
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,452 changes: 1,613 additions & 839 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ log4rs = "1.3.0"
tdigest = "0.2"
csv = "1.2"
csv-to-html = "0.5.10"
numeric-sort = "0.1.5"
regex = "1.12.3"
numeric-sort = "0.1"
regex = "1"
rmcp = { version = "1.5", features = ["server", "transport-io"], optional = true }
schemars = { version = "0.8", optional = true }

[target.'cfg(target_os = "linux")'.dependencies]
sysinfo = "0.26.2"
Expand All @@ -80,5 +82,6 @@ gimli = { version = "0.33", default-features = false, features = ["read-core", "
lzma-rs = "0.3"

[features]
default = []
default = ["mcp-server"]
hotline = []
mcp-server = ["dep:rmcp", "dep:schemars"]
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,31 @@ Shell to generate completions for [possible values: bash, elvish, fish, powershe

Install the auto complete script using sudo, or specify a download path

## MCP Server (AI Assistant Integration)

APerf includes a built-in [MCP](https://modelcontextprotocol.io/) server that lets AI assistants (Kiro, Claude Desktop, etc.) record data, generate reports, and analyze performance metrics interactively.

```bash
# Start the MCP server (used by AI clients, not run manually)
aperf server --mcp
```

**Kiro setup** — add to `~/.kiro/settings/mcp.json`:

```json
{
"mcpServers": {
"aperf-mcp": {
"command": "/path/to/aperf",
"args": ["server", "--mcp"],
"disabled": false
}
}
}
```

The server exposes 8 tools: `load_report`, `get_metrics`, `get_metric_values`, `get_analytical_findings`, `get_statistical_findings`, `get_flamegraph`, `record`, and `generate_report`. See [MCP Server docs](./docs/MCP-SERVER.md) for full details.

## APerf Issues?

> [!WARNING]
Expand Down Expand Up @@ -329,6 +354,7 @@ sudo sysctl -w kernel.kptr_restrict=0
- [Dependencies Reference](./docs/DEPENDENCIES.md#aperf-dependencies)
- [Development Guide](./docs/DEVELOPMENT.md)
- [Example Usage](./docs/EXAMPLE.md#aperf-example)
- [MCP Server (AI Assistant Integration)](./docs/MCP-SERVER.md)
- [Running on EKS](./docs/README-EKS.md#running-aperf-on-ekskubernetes)

## Security
Expand Down
197 changes: 197 additions & 0 deletions docs/MCP-SERVER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# APerf MCP Server

APerf includes a built-in [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that lets AI assistants record performance data, generate reports, and analyze APerf reports interactively.

## Usage

```bash
aperf server --mcp
```

This starts the MCP server on stdio. It's designed to be launched by an MCP client (Kiro, Claude Desktop, etc.), not run manually.

## MCP Client Configuration

### Kiro (`~/.kiro/settings/mcp.json`)

```json
{
"mcpServers": {
"aperf-mcp": {
"command": "/path/to/aperf",
"args": ["server", "--mcp"],
"disabled": false
}
}
}
```

Replace `/path/to/aperf` with the actual binary path.

## Available Tools

### Read-only tools (report analysis)

#### `load_report`

Load an APerf report and return metadata + available metrics. Must be called first before other analysis tools.

| Parameter | Type | Description |
|---|---|---|
| `report_path` | string | Absolute or relative path to the APerf report directory |

#### `get_metrics`

Query available metrics from the loaded report. Supports filtering.

| Parameter | Type | Description |
|---|---|---|
| `file_name` | string (optional) | JS filename, e.g. `"cpu_utilization.js"` |
| `category` | string (optional) | Data category, e.g. `"meminfo"` |

#### `get_metric_values`

Retrieve actual data values for a specific metric.

| Parameter | Type | Description |
|---|---|---|
| `metric_name` | string | Name of the metric (e.g. `"user"`, `"system"`) |
| `file_name` | string (optional) | JS filename containing the metric |
| `category` | string (optional) | Category/data_name (e.g. `"cpu_utilization"`) |
| `output_type` | string (optional) | Output format (default: `summary`, see below) |
| `cpu_ids` | string[] (optional) | Filter to specific CPUs. If omitted, averages across all. |
| `run_id` | string (optional) | Specific run ID (default: all runs) |
| `from_time` | int (optional) | Start of time range in seconds. Negative = relative to end (e.g. -60 = last 60s) |
| `to_time` | int (optional) | End of time range in seconds. Negative = relative to end (e.g. -10 = stop 10s before end) |

**Output types:**

| Type | Description | Token cost |
|------|-------------|------------|
| `summary` | Smart text with all stats (avg/min/max/std/p50/p90/p99), trend direction, spike and drop detection | Minimal (default) |
| `stats` | Aggregated statistics only (avg, std, min, max, p50, p90, p99, p99.9) | Minimal |
| `timeseries` | Full raw time-series data with timestamps and values | High |
| `compact` | Delta-encoded notation with run-length encoding | ~80% less than timeseries |
| `downsampled` | Fixed 50 buckets with min/avg/max per bucket | Bounded |

#### `get_analytical_findings`

Get analytical findings from the report's analytical engine. Findings represent rule-based detections (regressions, improvements, configuration mismatches). Sorted by absolute score (severity) descending.

| Parameter | Type | Description |
|---|---|---|
| `offset` | int (optional) | Start index for pagination, default 0 |
| `limit` | int (optional) | Max results per page, default 50 |

#### `get_statistical_findings`

Get statistical findings (metric stat deltas between runs). Computes the percentage change of each time-series metric's statistics compared to the base run (first run). Sorted by absolute delta descending. Requires a multi-run report.

| Parameter | Type | Description |
|---|---|---|
| `offset` | int (optional) | Start index for pagination, default 0 |
| `limit` | int (optional) | Max results per page, default 50 |
| `stat` | string (optional) | Filter by stat type: `avg`, `std`, `min`, `max`, `p50`, `p90`, `p99`, `p99_9` |
| `data_type` | string (optional) | Filter by data category (e.g. `cpu_utilization`, `meminfo`) |
| `min_delta_pct` | float (optional) | Minimum absolute delta percentage to include (e.g. 5.0) |

#### `get_flamegraph`

Query flamegraph data from the loaded report. Returns top functions by default (sorted by percentage), with optional regex filtering to search for specific functions. Supports normal, reverse, diff, and reverse-diff modes. Returns data for all runs by default, or specific runs if `run_id` is provided. In diff/reverse-diff mode, `run_id` must contain exactly 2 run IDs.

| Parameter | Type | Description |
|---|---|---|
| `flamegraph_type` | string (optional) | `normal` (default), `reverse`, `diff`, or `reverse-diff` |
| `run_id` | string[] (optional) | Run ID(s). Normal/reverse: omit for all runs, or list specific ones. Diff: exactly 2 IDs `[base, comparison]`, or omit for first two. |
| `limit` | int (optional) | Max functions to return per run, default 30 |
| `min_pct` | float (optional) | Minimum percentage threshold (default 0.1). In diff mode, min absolute delta % (default 0.5). |
| `filter` | string (optional) | Regex filter for function names (case-insensitive). Example: `"compact\|migrate"` |

**Examples:**

```
# Top 10 hottest functions across all runs
get_flamegraph(limit=10)

# Search for compaction-related functions
get_flamegraph(filter="compact|kcompactd|migrate")

# Reverse flamegraph for a specific run, filtered
get_flamegraph(flamegraph_type="reverse", run_id=["run1"], filter="lock")

# Diff between two runs — what got hotter/cooler
get_flamegraph(flamegraph_type="diff")

# Diff with explicit run IDs, filtered
get_flamegraph(flamegraph_type="diff", run_id=["run1", "run2"], filter="compact|writeback")

# Reverse diff
get_flamegraph(flamegraph_type="reverse-diff", run_id=["run1", "run2"])
```

### Write tools (data collection and report generation)

#### `record`

Record performance data on the current system. Runs `aperf record` as a subprocess.

| Parameter | Type | Description |
|---|---|---|
| `run_name` | string (optional) | Name of the run (default: `aperf_<timestamp>`) |
| `interval` | int (optional) | Collection interval in seconds (default: 1) |
| `period` | int (optional) | Recording duration in seconds (default: 10) |
| `profile` | bool (optional) | Enable CPU profiling using perf (default: false) |
| `perf_frequency` | int (optional) | Perf profiling frequency in Hz (default: 99) |
| `memory_allocation` | bool (optional) | Collect memory allocation data (default: false) |
| `dont_collect` | string (optional) | Comma-separated data types to skip |
| `collect_only` | string (optional) | Comma-separated data types to collect exclusively |
| `profile_java` | string (optional) | Profile JVMs — empty for all, or comma-separated PIDs/names |
| `pmu_config` | string (optional) | Path to custom PMU config file |

> Requires Linux and appropriate kernel permissions. See the main README for details.

#### `generate_report`

Generate an HTML report from one or more recorded APerf runs. For multi-run comparison, provide multiple run paths — the first run is used as the base for statistical findings.

| Parameter | Type | Description |
|---|---|---|
| `runs` | string[] | Paths to run directories or archives (at least one required) |
| `name` | string (optional) | Report name (default: `aperf_report_<timestamp>`) |

## Building

The MCP server is included in the default `aperf` build:

```bash
cargo build --release
# Binary at target/release/aperf
# Usage: aperf server --mcp
```

## Architecture

The MCP server code lives in `src/server/mcp/`:

```
src/server/
├── mod.rs ← Server subcommand dispatch
└── mcp/
├── mod.rs ← MCP entry point (tokio runtime + stdio transport)
├── tools.rs ← Tool definitions (#[tool_router] / #[tool_handler])
├── report.rs ← Report loading, validation, metric extraction
├── metadata.rs ← systeminfo.js parsing
└── js_parser.rs ← JS variable prefix stripping + JSON extraction
```

MCP dependencies (`rmcp`, `schemars`) are feature-gated behind the `mcp` feature (enabled by default).

## Running Tests

```bash
# All MCP server tests
cargo test --lib server

# Specific test
cargo test --lib server::mcp::tools::tests::test_build_record_args_full
```
15 changes: 15 additions & 0 deletions src/bin/aperf.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use anyhow::Result;
use aperf::completions::{setup_shell_completions, SetupShellCompletions};
use aperf::report::{report, Report};
#[cfg(feature = "mcp-server")]
use aperf::server::Server;
use aperf::{PDError, APERF_RUNLOG, APERF_TMP};
use clap::{CommandFactory, Parser, Subcommand};
use log::LevelFilter;
Expand Down Expand Up @@ -54,6 +56,10 @@ enum Commands {

/// Setup shell completions for APerf commands.
SetupShellCompletions(SetupShellCompletions),

/// Start a server (e.g. MCP for AI assistant integration).
#[cfg(feature = "mcp-server")]
Server(Server),
}

fn init_logger(verbose: u8, runlog: &PathBuf) -> Result<()> {
Expand Down Expand Up @@ -101,6 +107,12 @@ fn init_logger(verbose: u8, runlog: &PathBuf) -> Result<()> {
fn main() -> Result<()> {
let cli = Cli::parse();

// Server subcommand doesn't need temp dirs or log4rs — handle it early.
#[cfg(feature = "mcp-server")]
if let Commands::Server(ref s) = cli.command {
return aperf::server::run_server(s);
}

let tmp_dir = TempBuilder::new()
.prefix("aperf-tmp-")
.tempdir_in(&cli.tmp_dir)?;
Expand All @@ -123,6 +135,9 @@ fn main() -> Result<()> {
Commands::CustomPMU(r) => custom_pmu(&r),

Commands::SetupShellCompletions(r) => setup_shell_completions(&r, &mut Cli::command()),

#[cfg(feature = "mcp-server")]
Commands::Server(_) => unreachable!(), // handled above
}?;
fs::remove_dir_all(tmp_dir_path_buf)?;
Ok(())
Expand Down
1 change: 1 addition & 0 deletions src/computations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use strum_macros::Display;

/// Different statistics of the values contained in a Series
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(default)]
pub struct Statistics {
#[serde(serialize_with = "serialize_f64_fixed2")]
pub avg: f64,
Expand Down
2 changes: 2 additions & 0 deletions src/data/common/data_formats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub struct TimeSeriesData {
/// A map from the metric name to the metric's contents.
pub metrics: HashMap<String, TimeSeriesMetric>,
/// A list of all metric names to provide ordering for the graphs in the frontend.
#[serde(default)]
pub sorted_metric_names: Vec<String>,
}

Expand Down Expand Up @@ -114,6 +115,7 @@ pub struct Series {
#[serde(serialize_with = "serialize_f64_vec_fixed2")]
pub values: Vec<f64>,
/// Indicate whether the series is aggregate.
#[serde(default)]
#[serde(skip_serializing_if = "is_false")]
pub is_aggregate: bool,
}
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub mod profiling;
#[cfg(target_os = "linux")]
pub mod record;
pub mod report;
#[cfg(feature = "mcp-server")]
pub mod server;
pub mod visualizer;

use crate::analytics::{AnalyticalEngine, DataFindings};
Expand Down
Loading
Loading