@rog0x/mcp-perf-tools 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +210 -0
- package/dist/tools/benchmark.d.ts +27 -0
- package/dist/tools/benchmark.js +82 -0
- package/dist/tools/big-o-estimator.d.ts +13 -0
- package/dist/tools/big-o-estimator.js +80 -0
- package/dist/tools/bundle-analyzer.d.ts +25 -0
- package/dist/tools/bundle-analyzer.js +172 -0
- package/dist/tools/load-tester.d.ts +36 -0
- package/dist/tools/load-tester.js +185 -0
- package/dist/tools/memory-analyzer.d.ts +22 -0
- package/dist/tools/memory-analyzer.js +115 -0
- package/package.json +20 -0
- package/src/index.ts +257 -0
- package/src/tools/benchmark.ts +113 -0
- package/src/tools/big-o-estimator.ts +175 -0
- package/src/tools/bundle-analyzer.ts +195 -0
- package/src/tools/load-tester.ts +216 -0
- package/src/tools/memory-analyzer.ts +145 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.analyzeBundle = analyzeBundle;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const zlib = __importStar(require("node:zlib"));
|
|
40
|
+
/**
|
|
41
|
+
* Detect import/require patterns to approximate module boundaries.
|
|
42
|
+
* This is a heuristic for bundled JS files.
|
|
43
|
+
*/
|
|
44
|
+
function detectModules(content, bundlePath) {
|
|
45
|
+
const modules = [];
|
|
46
|
+
const treeShaking = [];
|
|
47
|
+
const totalSize = Buffer.byteLength(content, "utf-8");
|
|
48
|
+
// Look for common bundle patterns:
|
|
49
|
+
// Webpack: /***/ "./path/to/module.js": or numbered modules
|
|
50
|
+
// Rollup: define blocks
|
|
51
|
+
// Generic: large comment blocks separating modules
|
|
52
|
+
const webpackModulePattern = /\/\*{3}\/ ["']([^"']+)["']:|\/\*{3}\/ (\d+)/g;
|
|
53
|
+
const requirePattern = /require\(["']([^"']+)["']\)/g;
|
|
54
|
+
const importPattern = /(?:import|export)\s+.*?from\s+["']([^"']+)["']/g;
|
|
55
|
+
const moduleNames = new Set();
|
|
56
|
+
let match;
|
|
57
|
+
// Webpack-style modules
|
|
58
|
+
while ((match = webpackModulePattern.exec(content)) !== null) {
|
|
59
|
+
moduleNames.add(match[1] || match[2]);
|
|
60
|
+
}
|
|
61
|
+
// require() calls
|
|
62
|
+
while ((match = requirePattern.exec(content)) !== null) {
|
|
63
|
+
moduleNames.add(match[1]);
|
|
64
|
+
}
|
|
65
|
+
// ES import/export
|
|
66
|
+
while ((match = importPattern.exec(content)) !== null) {
|
|
67
|
+
moduleNames.add(match[1]);
|
|
68
|
+
}
|
|
69
|
+
// If we found module references, create approximate entries
|
|
70
|
+
for (const name of moduleNames) {
|
|
71
|
+
// Estimate module size by finding its content boundaries (rough heuristic)
|
|
72
|
+
const idx = content.indexOf(name);
|
|
73
|
+
if (idx === -1)
|
|
74
|
+
continue;
|
|
75
|
+
// Rough estimate: count bytes between this reference and the next or end
|
|
76
|
+
const estimatedSize = Math.round(totalSize / Math.max(moduleNames.size, 1));
|
|
77
|
+
modules.push({
|
|
78
|
+
filePath: name,
|
|
79
|
+
sizeBytes: estimatedSize,
|
|
80
|
+
sizeKB: Math.round((estimatedSize / 1024) * 100) / 100,
|
|
81
|
+
percentage: Math.round((estimatedSize / totalSize) * 100 * 100) / 100,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// If no patterns matched, treat whole file as single module
|
|
85
|
+
if (modules.length === 0) {
|
|
86
|
+
modules.push({
|
|
87
|
+
filePath: path.basename(bundlePath),
|
|
88
|
+
sizeBytes: totalSize,
|
|
89
|
+
sizeKB: Math.round((totalSize / 1024) * 100) / 100,
|
|
90
|
+
percentage: 100,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Tree-shaking opportunity detection
|
|
94
|
+
// Look for unused exports, side-effect patterns, etc.
|
|
95
|
+
const sideEffectPatterns = [
|
|
96
|
+
{ pattern: /\bconsole\.(log|warn|info|debug)\b/g, reason: "console statements found (side effects)" },
|
|
97
|
+
{ pattern: /\bwindow\.\w+\s*=/g, reason: "global window property assignment (side effect)" },
|
|
98
|
+
{ pattern: /\bglobal\.\w+\s*=/g, reason: "global property assignment (side effect)" },
|
|
99
|
+
];
|
|
100
|
+
for (const { pattern, reason } of sideEffectPatterns) {
|
|
101
|
+
const sideMatch = pattern.exec(content);
|
|
102
|
+
if (sideMatch) {
|
|
103
|
+
treeShaking.push({ filePath: bundlePath, reason });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Check for duplicate-looking module names (possible duplicates in bundle)
|
|
107
|
+
const baseNames = new Map();
|
|
108
|
+
for (const name of moduleNames) {
|
|
109
|
+
const base = path.basename(name);
|
|
110
|
+
if (!baseNames.has(base))
|
|
111
|
+
baseNames.set(base, []);
|
|
112
|
+
baseNames.get(base).push(name);
|
|
113
|
+
}
|
|
114
|
+
for (const [base, paths] of baseNames) {
|
|
115
|
+
if (paths.length > 1) {
|
|
116
|
+
treeShaking.push({
|
|
117
|
+
filePath: base,
|
|
118
|
+
reason: `Possible duplicate: "${base}" appears ${paths.length} times with different paths`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { modules, treeShaking };
|
|
123
|
+
}
|
|
124
|
+
function analyzeBundle(filePath) {
|
|
125
|
+
const resolvedPath = path.resolve(filePath);
|
|
126
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
127
|
+
throw new Error(`File not found: ${resolvedPath}`);
|
|
128
|
+
}
|
|
129
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
130
|
+
const rawSizeBytes = Buffer.byteLength(content, "utf-8");
|
|
131
|
+
const rawSizeKB = Math.round((rawSizeBytes / 1024) * 100) / 100;
|
|
132
|
+
// Gzip estimate
|
|
133
|
+
const gzipped = zlib.gzipSync(content, { level: 6 });
|
|
134
|
+
const gzipEstimateBytes = gzipped.length;
|
|
135
|
+
const gzipEstimateKB = Math.round((gzipEstimateBytes / 1024) * 100) / 100;
|
|
136
|
+
const compressionRatio = rawSizeBytes > 0
|
|
137
|
+
? Math.round((gzipEstimateBytes / rawSizeBytes) * 100 * 100) / 100
|
|
138
|
+
: 0;
|
|
139
|
+
const { modules, treeShaking } = detectModules(content, resolvedPath);
|
|
140
|
+
// Sort modules by size descending
|
|
141
|
+
modules.sort((a, b) => b.sizeBytes - a.sizeBytes);
|
|
142
|
+
const largestModules = modules.slice(0, 10);
|
|
143
|
+
const summary = [
|
|
144
|
+
`Bundle Analysis: ${path.basename(resolvedPath)}`,
|
|
145
|
+
` Raw Size: ${rawSizeKB} KB (${rawSizeBytes} bytes)`,
|
|
146
|
+
` Gzip Estimate: ${gzipEstimateKB} KB (${gzipEstimateBytes} bytes)`,
|
|
147
|
+
` Compression: ${compressionRatio}%`,
|
|
148
|
+
` Modules Detected: ${modules.length}`,
|
|
149
|
+
"",
|
|
150
|
+
"Largest Modules:",
|
|
151
|
+
...largestModules.map((m, i) => ` ${i + 1}. ${m.filePath} — ${m.sizeKB} KB (${m.percentage}%)`),
|
|
152
|
+
];
|
|
153
|
+
if (treeShaking.length > 0) {
|
|
154
|
+
summary.push("", "Tree-Shaking Opportunities:");
|
|
155
|
+
for (const opp of treeShaking) {
|
|
156
|
+
summary.push(` - ${opp.filePath}: ${opp.reason}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
filePath: resolvedPath,
|
|
161
|
+
rawSizeBytes,
|
|
162
|
+
rawSizeKB,
|
|
163
|
+
gzipEstimateBytes,
|
|
164
|
+
gzipEstimateKB,
|
|
165
|
+
compressionRatio,
|
|
166
|
+
moduleCount: modules.length,
|
|
167
|
+
largestModules,
|
|
168
|
+
treeShakingOpportunities: treeShaking,
|
|
169
|
+
summary: summary.join("\n"),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=bundle-analyzer.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface LoadTestConfig {
|
|
2
|
+
url: string;
|
|
3
|
+
totalRequests: number;
|
|
4
|
+
concurrency: number;
|
|
5
|
+
method?: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
body?: string;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface RequestResult {
|
|
11
|
+
statusCode: number | null;
|
|
12
|
+
durationMs: number;
|
|
13
|
+
error: string | null;
|
|
14
|
+
byteLength: number;
|
|
15
|
+
}
|
|
16
|
+
export interface LoadTestResult {
|
|
17
|
+
url: string;
|
|
18
|
+
totalRequests: number;
|
|
19
|
+
concurrency: number;
|
|
20
|
+
successCount: number;
|
|
21
|
+
errorCount: number;
|
|
22
|
+
errorRate: number;
|
|
23
|
+
totalDurationMs: number;
|
|
24
|
+
throughputReqPerSec: number;
|
|
25
|
+
avgResponseMs: number;
|
|
26
|
+
minResponseMs: number;
|
|
27
|
+
maxResponseMs: number;
|
|
28
|
+
medianResponseMs: number;
|
|
29
|
+
p95ResponseMs: number;
|
|
30
|
+
p99ResponseMs: number;
|
|
31
|
+
statusCodeDistribution: Record<string, number>;
|
|
32
|
+
totalBytesReceived: number;
|
|
33
|
+
summary: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function runLoadTest(config: LoadTestConfig): Promise<LoadTestResult>;
|
|
36
|
+
//# sourceMappingURL=load-tester.d.ts.map
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runLoadTest = runLoadTest;
|
|
37
|
+
const http = __importStar(require("node:http"));
|
|
38
|
+
const https = __importStar(require("node:https"));
|
|
39
|
+
const node_url_1 = require("node:url");
|
|
40
|
+
const node_perf_hooks_1 = require("node:perf_hooks");
|
|
41
|
+
function percentile(sorted, p) {
|
|
42
|
+
if (sorted.length === 0)
|
|
43
|
+
return 0;
|
|
44
|
+
const idx = (p / 100) * (sorted.length - 1);
|
|
45
|
+
const lower = Math.floor(idx);
|
|
46
|
+
const upper = Math.ceil(idx);
|
|
47
|
+
if (lower === upper)
|
|
48
|
+
return sorted[lower];
|
|
49
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (idx - lower);
|
|
50
|
+
}
|
|
51
|
+
function makeRequest(config) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const parsedUrl = new node_url_1.URL(config.url);
|
|
54
|
+
const isHttps = parsedUrl.protocol === "https:";
|
|
55
|
+
const transport = isHttps ? https : http;
|
|
56
|
+
const startTime = node_perf_hooks_1.performance.now();
|
|
57
|
+
const timeout = config.timeoutMs ?? 30000;
|
|
58
|
+
const options = {
|
|
59
|
+
hostname: parsedUrl.hostname,
|
|
60
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
61
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
62
|
+
method: config.method ?? "GET",
|
|
63
|
+
headers: config.headers ?? {},
|
|
64
|
+
timeout,
|
|
65
|
+
};
|
|
66
|
+
const req = transport.request(options, (res) => {
|
|
67
|
+
const chunks = [];
|
|
68
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
69
|
+
res.on("end", () => {
|
|
70
|
+
const durationMs = node_perf_hooks_1.performance.now() - startTime;
|
|
71
|
+
const body = Buffer.concat(chunks);
|
|
72
|
+
resolve({
|
|
73
|
+
statusCode: res.statusCode ?? null,
|
|
74
|
+
durationMs,
|
|
75
|
+
error: null,
|
|
76
|
+
byteLength: body.length,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
res.on("error", (err) => {
|
|
80
|
+
resolve({
|
|
81
|
+
statusCode: res.statusCode ?? null,
|
|
82
|
+
durationMs: node_perf_hooks_1.performance.now() - startTime,
|
|
83
|
+
error: err.message,
|
|
84
|
+
byteLength: 0,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
req.on("error", (err) => {
|
|
89
|
+
resolve({
|
|
90
|
+
statusCode: null,
|
|
91
|
+
durationMs: node_perf_hooks_1.performance.now() - startTime,
|
|
92
|
+
error: err.message,
|
|
93
|
+
byteLength: 0,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
req.on("timeout", () => {
|
|
97
|
+
req.destroy();
|
|
98
|
+
resolve({
|
|
99
|
+
statusCode: null,
|
|
100
|
+
durationMs: node_perf_hooks_1.performance.now() - startTime,
|
|
101
|
+
error: "Request timed out",
|
|
102
|
+
byteLength: 0,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
if (config.body) {
|
|
106
|
+
req.write(config.body);
|
|
107
|
+
}
|
|
108
|
+
req.end();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function runLoadTest(config) {
|
|
112
|
+
const { totalRequests, concurrency } = config;
|
|
113
|
+
const results = [];
|
|
114
|
+
const overallStart = node_perf_hooks_1.performance.now();
|
|
115
|
+
// Process requests in batches of `concurrency`
|
|
116
|
+
let sent = 0;
|
|
117
|
+
while (sent < totalRequests) {
|
|
118
|
+
const batchSize = Math.min(concurrency, totalRequests - sent);
|
|
119
|
+
const batch = [];
|
|
120
|
+
for (let i = 0; i < batchSize; i++) {
|
|
121
|
+
batch.push(makeRequest(config));
|
|
122
|
+
}
|
|
123
|
+
const batchResults = await Promise.all(batch);
|
|
124
|
+
results.push(...batchResults);
|
|
125
|
+
sent += batchSize;
|
|
126
|
+
}
|
|
127
|
+
const overallEnd = node_perf_hooks_1.performance.now();
|
|
128
|
+
const totalDurationMs = overallEnd - overallStart;
|
|
129
|
+
// Compute metrics
|
|
130
|
+
const durations = results.map((r) => r.durationMs);
|
|
131
|
+
const sortedDurations = [...durations].sort((a, b) => a - b);
|
|
132
|
+
const successCount = results.filter((r) => r.error === null && r.statusCode !== null && r.statusCode < 400).length;
|
|
133
|
+
const errorCount = totalRequests - successCount;
|
|
134
|
+
const errorRate = Math.round((errorCount / totalRequests) * 100 * 100) / 100;
|
|
135
|
+
const statusDist = {};
|
|
136
|
+
for (const r of results) {
|
|
137
|
+
const key = r.error ? "error" : String(r.statusCode);
|
|
138
|
+
statusDist[key] = (statusDist[key] ?? 0) + 1;
|
|
139
|
+
}
|
|
140
|
+
const totalBytesReceived = results.reduce((s, r) => s + r.byteLength, 0);
|
|
141
|
+
const throughputReqPerSec = totalDurationMs > 0 ? (totalRequests / totalDurationMs) * 1000 : 0;
|
|
142
|
+
const avgResponseMs = durations.reduce((a, b) => a + b, 0) / durations.length;
|
|
143
|
+
const result = {
|
|
144
|
+
url: config.url,
|
|
145
|
+
totalRequests,
|
|
146
|
+
concurrency,
|
|
147
|
+
successCount,
|
|
148
|
+
errorCount,
|
|
149
|
+
errorRate,
|
|
150
|
+
totalDurationMs,
|
|
151
|
+
throughputReqPerSec: Math.round(throughputReqPerSec * 100) / 100,
|
|
152
|
+
avgResponseMs: Math.round(avgResponseMs * 100) / 100,
|
|
153
|
+
minResponseMs: Math.round(sortedDurations[0] * 100) / 100,
|
|
154
|
+
maxResponseMs: Math.round(sortedDurations[sortedDurations.length - 1] * 100) / 100,
|
|
155
|
+
medianResponseMs: Math.round(percentile(sortedDurations, 50) * 100) / 100,
|
|
156
|
+
p95ResponseMs: Math.round(percentile(sortedDurations, 95) * 100) / 100,
|
|
157
|
+
p99ResponseMs: Math.round(percentile(sortedDurations, 99) * 100) / 100,
|
|
158
|
+
statusCodeDistribution: statusDist,
|
|
159
|
+
totalBytesReceived,
|
|
160
|
+
summary: "",
|
|
161
|
+
};
|
|
162
|
+
result.summary = [
|
|
163
|
+
`Load Test Results: ${config.method ?? "GET"} ${config.url}`,
|
|
164
|
+
` Total Requests: ${totalRequests}`,
|
|
165
|
+
` Concurrency: ${concurrency}`,
|
|
166
|
+
` Total Duration: ${totalDurationMs.toFixed(0)} ms`,
|
|
167
|
+
` Throughput: ${result.throughputReqPerSec} req/s`,
|
|
168
|
+
"",
|
|
169
|
+
"Response Times:",
|
|
170
|
+
` Min: ${result.minResponseMs} ms`,
|
|
171
|
+
` Max: ${result.maxResponseMs} ms`,
|
|
172
|
+
` Avg: ${result.avgResponseMs} ms`,
|
|
173
|
+
` Median: ${result.medianResponseMs} ms`,
|
|
174
|
+
` P95: ${result.p95ResponseMs} ms`,
|
|
175
|
+
` P99: ${result.p99ResponseMs} ms`,
|
|
176
|
+
"",
|
|
177
|
+
`Success: ${successCount} Errors: ${errorCount} Error Rate: ${errorRate}%`,
|
|
178
|
+
`Bytes Received: ${totalBytesReceived}`,
|
|
179
|
+
"",
|
|
180
|
+
"Status Code Distribution:",
|
|
181
|
+
...Object.entries(statusDist).map(([code, count]) => ` ${code}: ${count}`),
|
|
182
|
+
].join("\n");
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=load-tester.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface MemorySnapshot {
|
|
2
|
+
timestamp: number;
|
|
3
|
+
heapUsedMB: number;
|
|
4
|
+
heapTotalMB: number;
|
|
5
|
+
rssMB: number;
|
|
6
|
+
externalMB: number;
|
|
7
|
+
arrayBuffersMB: number;
|
|
8
|
+
}
|
|
9
|
+
export interface MemoryAnalysis {
|
|
10
|
+
current: MemorySnapshot;
|
|
11
|
+
history: MemorySnapshot[];
|
|
12
|
+
trend: "stable" | "growing" | "shrinking" | "insufficient_data";
|
|
13
|
+
growthRateMBPerSec: number | null;
|
|
14
|
+
potentialLeak: boolean;
|
|
15
|
+
summary: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function takeMemorySnapshot(): MemorySnapshot;
|
|
18
|
+
export declare function getSnapshotHistory(): MemorySnapshot[];
|
|
19
|
+
export declare function clearSnapshotHistory(): void;
|
|
20
|
+
export declare function analyzeMemory(): MemoryAnalysis;
|
|
21
|
+
export declare function formatSnapshot(snap: MemorySnapshot): string;
|
|
22
|
+
//# sourceMappingURL=memory-analyzer.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.takeMemorySnapshot = takeMemorySnapshot;
|
|
4
|
+
exports.getSnapshotHistory = getSnapshotHistory;
|
|
5
|
+
exports.clearSnapshotHistory = clearSnapshotHistory;
|
|
6
|
+
exports.analyzeMemory = analyzeMemory;
|
|
7
|
+
exports.formatSnapshot = formatSnapshot;
|
|
8
|
+
const toMB = (bytes) => Math.round((bytes / 1024 / 1024) * 100) / 100;
|
|
9
|
+
const snapshotHistory = [];
|
|
10
|
+
function takeMemorySnapshot() {
|
|
11
|
+
const mem = process.memoryUsage();
|
|
12
|
+
const snapshot = {
|
|
13
|
+
timestamp: Date.now(),
|
|
14
|
+
heapUsedMB: toMB(mem.heapUsed),
|
|
15
|
+
heapTotalMB: toMB(mem.heapTotal),
|
|
16
|
+
rssMB: toMB(mem.rss),
|
|
17
|
+
externalMB: toMB(mem.external),
|
|
18
|
+
arrayBuffersMB: toMB(mem.arrayBuffers),
|
|
19
|
+
};
|
|
20
|
+
snapshotHistory.push(snapshot);
|
|
21
|
+
return snapshot;
|
|
22
|
+
}
|
|
23
|
+
function getSnapshotHistory() {
|
|
24
|
+
return [...snapshotHistory];
|
|
25
|
+
}
|
|
26
|
+
function clearSnapshotHistory() {
|
|
27
|
+
snapshotHistory.length = 0;
|
|
28
|
+
}
|
|
29
|
+
function linearRegression(xs, ys) {
|
|
30
|
+
const n = xs.length;
|
|
31
|
+
if (n < 2)
|
|
32
|
+
return { slope: 0, intercept: ys[0] ?? 0, r2: 0 };
|
|
33
|
+
const sumX = xs.reduce((a, b) => a + b, 0);
|
|
34
|
+
const sumY = ys.reduce((a, b) => a + b, 0);
|
|
35
|
+
const sumXY = xs.reduce((a, x, i) => a + x * ys[i], 0);
|
|
36
|
+
const sumX2 = xs.reduce((a, x) => a + x * x, 0);
|
|
37
|
+
const denom = n * sumX2 - sumX * sumX;
|
|
38
|
+
if (denom === 0)
|
|
39
|
+
return { slope: 0, intercept: sumY / n, r2: 0 };
|
|
40
|
+
const slope = (n * sumXY - sumX * sumY) / denom;
|
|
41
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
42
|
+
// R-squared
|
|
43
|
+
const meanY = sumY / n;
|
|
44
|
+
const ssTot = ys.reduce((a, y) => a + (y - meanY) ** 2, 0);
|
|
45
|
+
const ssRes = ys.reduce((a, y, i) => a + (y - (slope * xs[i] + intercept)) ** 2, 0);
|
|
46
|
+
const r2 = ssTot === 0 ? 1 : 1 - ssRes / ssTot;
|
|
47
|
+
return { slope, intercept, r2 };
|
|
48
|
+
}
|
|
49
|
+
function analyzeMemory() {
|
|
50
|
+
const current = takeMemorySnapshot();
|
|
51
|
+
const history = getSnapshotHistory();
|
|
52
|
+
let trend = "insufficient_data";
|
|
53
|
+
let growthRateMBPerSec = null;
|
|
54
|
+
let potentialLeak = false;
|
|
55
|
+
if (history.length >= 3) {
|
|
56
|
+
const baseTime = history[0].timestamp;
|
|
57
|
+
const xs = history.map((s) => (s.timestamp - baseTime) / 1000); // seconds
|
|
58
|
+
const ys = history.map((s) => s.heapUsedMB);
|
|
59
|
+
const reg = linearRegression(xs, ys);
|
|
60
|
+
growthRateMBPerSec = reg.slope;
|
|
61
|
+
// Classify trend based on slope significance and R^2
|
|
62
|
+
const totalTimeSpan = xs[xs.length - 1] - xs[0];
|
|
63
|
+
if (totalTimeSpan < 0.1) {
|
|
64
|
+
trend = "insufficient_data";
|
|
65
|
+
}
|
|
66
|
+
else if (Math.abs(reg.slope) < 0.01) {
|
|
67
|
+
trend = "stable";
|
|
68
|
+
}
|
|
69
|
+
else if (reg.slope > 0) {
|
|
70
|
+
trend = "growing";
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
trend = "shrinking";
|
|
74
|
+
}
|
|
75
|
+
// Potential leak: consistent growth with high correlation
|
|
76
|
+
if (trend === "growing" && reg.r2 > 0.7 && reg.slope > 0.1) {
|
|
77
|
+
potentialLeak = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const lines = [
|
|
81
|
+
"Memory Analysis:",
|
|
82
|
+
` Heap Used: ${current.heapUsedMB} MB`,
|
|
83
|
+
` Heap Total: ${current.heapTotalMB} MB`,
|
|
84
|
+
` RSS: ${current.rssMB} MB`,
|
|
85
|
+
` External: ${current.externalMB} MB`,
|
|
86
|
+
` Array Buffers: ${current.arrayBuffersMB} MB`,
|
|
87
|
+
` Snapshots: ${history.length}`,
|
|
88
|
+
` Trend: ${trend}`,
|
|
89
|
+
];
|
|
90
|
+
if (growthRateMBPerSec !== null) {
|
|
91
|
+
lines.push(` Growth Rate: ${growthRateMBPerSec.toFixed(4)} MB/s`);
|
|
92
|
+
}
|
|
93
|
+
if (potentialLeak) {
|
|
94
|
+
lines.push(" WARNING: Potential memory leak detected (consistent heap growth).");
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
current,
|
|
98
|
+
history,
|
|
99
|
+
trend,
|
|
100
|
+
growthRateMBPerSec,
|
|
101
|
+
potentialLeak,
|
|
102
|
+
summary: lines.join("\n"),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function formatSnapshot(snap) {
|
|
106
|
+
return [
|
|
107
|
+
`Memory Snapshot (${new Date(snap.timestamp).toISOString()}):`,
|
|
108
|
+
` Heap Used: ${snap.heapUsedMB} MB`,
|
|
109
|
+
` Heap Total: ${snap.heapTotalMB} MB`,
|
|
110
|
+
` RSS: ${snap.rssMB} MB`,
|
|
111
|
+
` External: ${snap.externalMB} MB`,
|
|
112
|
+
` Array Buffers: ${snap.arrayBuffersMB} MB`,
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=memory-analyzer.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rog0x/mcp-perf-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Performance analysis tools for AI agents via MCP",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["mcp", "performance", "benchmark", "profiling"],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.7.3",
|
|
18
|
+
"@types/node": "^22.15.2"
|
|
19
|
+
}
|
|
20
|
+
}
|