/**
* mysql-mcp - Performance Tests
*
* Timing-based tests to validate caching and algorithmic optimizations.
* These tests provide regression protection for documented performance improvements.
*/
import { describe, it, expect, beforeEach } from "vitest";
import {
getAllToolNames,
getToolGroup,
parseToolFilter,
filterTools,
clearToolFilterCaches,
} from "../filtering/ToolFilter.js";
import { MySQLAdapter } from "../adapters/mysql/MySQLAdapter.js";
import type { ToolDefinition } from "../types/index.js";
/**
* Measure execution time of a function over multiple iterations
*/
function measureTime(
fn: () => void,
iterations = 100,
): { avg: number; min: number; max: number } {
const times: number[] = [];
for (let i = 0; i < iterations; i++) {
const start = performance.now();
fn();
const end = performance.now();
times.push(end - start);
}
return {
avg: times.reduce((a, b) => a + b, 0) / times.length,
min: Math.min(...times),
max: Math.max(...times),
};
}
describe("Performance Tests", () => {
describe("getAllToolNames caching", () => {
beforeEach(() => {
clearToolFilterCaches();
});
it("should cache getAllToolNames for subsequent calls", () => {
// First call clears and rebuilds the cache
measureTime(() => {
clearToolFilterCaches();
getAllToolNames();
}, 10);
// Subsequent calls should use cache - don't clear between iterations
const initialCall = getAllToolNames();
const cachedCallTime = measureTime(() => {
getAllToolNames();
}, 100);
// Verify caching works (same reference)
expect(getAllToolNames()).toBe(initialCall);
// Cached calls should be very fast (< 0.5ms on average)
expect(cachedCallTime.avg).toBeLessThan(0.5);
});
it("should return consistent count of 192 tools", () => {
const tools = getAllToolNames();
expect(tools).toHaveLength(192);
});
});
describe("getToolGroup O(1) lookup", () => {
beforeEach(() => {
clearToolFilterCaches();
});
it("should have consistent lookup time regardless of tool position", () => {
// Warm up the cache
getToolGroup("mysql_read_query");
// Measure lookup time for first tool in core group
const firstToolTime = measureTime(() => {
getToolGroup("mysql_read_query");
}, 100);
// Measure lookup time for tool in last group (docstore)
const lastToolTime = measureTime(() => {
getToolGroup("mysql_doc_collection_info");
}, 100);
// Both should be very fast (O(1) Map lookup)
expect(firstToolTime.avg).toBeLessThan(0.1);
expect(lastToolTime.avg).toBeLessThan(0.1);
// Times should be similar (indicating O(1) behavior)
// Note: Ratio comparisons are unstable for very small numbers (e.g. 0.001ms vs 0.02ms is 20x variance).
// We check that the absolute difference is negligible (< 0.2ms)
const diff = Math.abs(firstToolTime.avg - lastToolTime.avg);
expect(diff).toBeLessThan(0.2);
});
it("should correctly identify tool groups", () => {
expect(getToolGroup("mysql_read_query")).toBe("core");
expect(getToolGroup("mysql_json_extract")).toBe("json");
expect(getToolGroup("mysql_doc_find")).toBe("docstore");
expect(getToolGroup("unknown_tool")).toBeUndefined();
});
});
describe("parseToolFilter performance", () => {
it("should parse empty filter quickly", () => {
const timing = measureTime(() => {
parseToolFilter("");
}, 100);
// Empty filter should be very fast
expect(timing.avg).toBeLessThan(1);
});
it("should parse complex filter chain efficiently", () => {
const complexFilter =
"-base-core,-base-admin,-ecosystem,+starter,+spatial,+performance";
const timing = measureTime(() => {
parseToolFilter(complexFilter);
}, 50);
// Complex filter should still be reasonably fast (< 5ms on average)
expect(timing.avg).toBeLessThan(5);
});
it("should handle repeated parsing of same filter", () => {
const filter = "starter";
// Warm up
parseToolFilter(filter);
const timing = measureTime(() => {
parseToolFilter(filter);
}, 100);
// Repeated parsing should be consistent
expect(timing.avg).toBeLessThan(3);
});
it("should correctly filter to expected tool counts", () => {
const starterConfig = parseToolFilter("starter");
expect(starterConfig.enabledTools.size).toBe(38);
const aiDataConfig = parseToolFilter("ai-data");
expect(aiDataConfig.enabledTools.size).toBe(45);
const dbaMonitorConfig = parseToolFilter("dba-monitor");
expect(dbaMonitorConfig.enabledTools.size).toBe(35);
const dbaSecureConfig = parseToolFilter("dba-secure");
expect(dbaSecureConfig.enabledTools.size).toBe(32);
const devPowerConfig = parseToolFilter("dev-power");
expect(devPowerConfig.enabledTools.size).toBe(46);
});
describe("filterTools performance", () => {
const mockHandler = async () => ({ result: "ok" });
it("should filter 191 tools efficiently", () => {
// Create mock tools matching all tool names
const allToolNames = getAllToolNames();
const mockTools: ToolDefinition[] = allToolNames.map((name) => ({
name,
description: `Description for ${name}`,
inputSchema: {},
group: getToolGroup(name) ?? "core",
handler: mockHandler,
}));
const config = parseToolFilter("starter");
const timing = measureTime(() => {
filterTools(mockTools, config);
}, 100);
// Filtering 191 tools should be fast (< 2ms on average)
expect(timing.avg).toBeLessThan(2);
// Verify correct filtering
const filtered = filterTools(mockTools, config);
expect(filtered).toHaveLength(38);
});
});
describe("MySQLAdapter tool definition caching", () => {
it("should cache tool definitions after first call", () => {
// Create adapter (no constructor args - uses connect() for config)
const adapter = new MySQLAdapter();
// First call builds the cache
const firstCall = adapter.getToolDefinitions();
expect(firstCall).toHaveLength(192);
// Subsequent calls should return same reference (cached)
const secondCall = adapter.getToolDefinitions();
expect(secondCall).toBe(firstCall);
// Measure cached call performance
const timing = measureTime(() => {
adapter.getToolDefinitions();
}, 100);
// Cached calls should be nearly instant
expect(timing.avg).toBeLessThan(0.5);
});
it("should return consistent tool count of 192 regardless of filter default", () => {
const adapter = new MySQLAdapter();
// getToolDefinitions returns ALL definitions available in the adapter,
// filtering happens at request time usually, or if we filter explicitly.
// Wait, MySQLAdapter.getToolDefinitions() returns all tools?
// Yes, checking the implementation... it usually does.
const tools = adapter.getToolDefinitions();
expect(tools).toHaveLength(192);
});
});
});
});