@elyracode/perf-tools 0.5.2

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 ADDED
@@ -0,0 +1,29 @@
1
+ # @elyracode/perf-tools
2
+
3
+ Performance analysis for Elyra -- N+1 queries, slow queries, missing indexes.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ elyra install npm:@elyracode/perf-tools
9
+ ```
10
+
11
+ ## Tools
12
+
13
+ | Tool | Description |
14
+ |------|-------------|
15
+ | `analyze_queries` | Scan logs and code for N+1 patterns, repeated queries, missing eager loading, missing indexes |
16
+ | `explain_query` | Run EXPLAIN on a SQL query to analyze execution plan |
17
+
18
+ ## Commands
19
+
20
+ - `/perf` -- Run full performance analysis
21
+
22
+ ## Usage
23
+
24
+ ```
25
+ > Analyze this project for N+1 queries
26
+ > Find models that need eager loading
27
+ > Explain this slow query: SELECT * FROM orders JOIN...
28
+ /perf
29
+ ```
@@ -0,0 +1,199 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import type { ExtensionAPI } from "@elyracode/coding-agent";
5
+ import { Type } from "typebox";
6
+
7
+ export default function (elyra: ExtensionAPI): void {
8
+
9
+ // ── Tool: analyze_queries ──
10
+ elyra.registerTool({
11
+ name: "analyze_queries",
12
+ label: "Analyze Database Queries",
13
+ description:
14
+ "Scan Laravel log files or source code for N+1 query patterns, " +
15
+ "repeated queries, and slow query candidates. " +
16
+ "Checks Eloquent models for missing eager loading, missing indexes, " +
17
+ "and common performance anti-patterns.",
18
+ parameters: Type.Object({
19
+ source: Type.Optional(
20
+ Type.Union([Type.Literal("logs"), Type.Literal("code"), Type.Literal("both")], {
21
+ description: "What to analyze: 'logs' (Laravel log file), 'code' (source scan), or 'both' (default)",
22
+ }),
23
+ ),
24
+ }),
25
+ execute: async (_toolCallId, params) => {
26
+ try {
27
+ const cwd = process.cwd();
28
+ const source = params.source ?? "both";
29
+ const findings: string[] = ["# Query Performance Analysis", ""];
30
+
31
+ if (source === "logs" || source === "both") {
32
+ findings.push("## Log Analysis");
33
+ const logPath = join(cwd, "storage", "logs", "laravel.log");
34
+ if (existsSync(logPath)) {
35
+ const log = readFileSync(logPath, "utf-8");
36
+ const lastChunk = log.slice(-100000); // Last 100KB
37
+
38
+ // Find query patterns
39
+ const queryMatches = lastChunk.match(/\[\d{4}-.*?select.*?from/gi) ?? [];
40
+ const duplicates = findDuplicatePatterns(queryMatches);
41
+
42
+ if (duplicates.length > 0) {
43
+ findings.push(`Found ${duplicates.length} potentially repeated query patterns:`);
44
+ for (const dup of duplicates.slice(0, 10)) {
45
+ findings.push(`- ${dup.pattern} (${dup.count} times)`);
46
+ }
47
+ }
48
+
49
+ // Find slow queries (if debug bar data exists)
50
+ const slowMatches = lastChunk.match(/\[.*?\] .*?(\d+\.?\d*)ms.*?select/gi) ?? [];
51
+ const slowQueries = slowMatches.filter((m) => {
52
+ const ms = m.match(/(\d+\.?\d*)ms/)?.[1];
53
+ return ms && parseFloat(ms) > 100;
54
+ });
55
+ if (slowQueries.length > 0) {
56
+ findings.push("", `Slow queries (>100ms): ${slowQueries.length}`);
57
+ for (const q of slowQueries.slice(0, 5)) {
58
+ findings.push(`- ${q.slice(0, 200)}`);
59
+ }
60
+ }
61
+
62
+ if (duplicates.length === 0 && slowQueries.length === 0) {
63
+ findings.push("No obvious query issues found in logs.");
64
+ }
65
+ } else {
66
+ findings.push("Laravel log file not found at storage/logs/laravel.log");
67
+ }
68
+ findings.push("");
69
+ }
70
+
71
+ if (source === "code" || source === "both") {
72
+ findings.push("## Code Analysis");
73
+
74
+ // Scan for N+1 patterns
75
+ try {
76
+ const n1Patterns = execSync(
77
+ `grep -rn --include="*.php" -E "->\\w+\\b" app/ | grep -v vendor | grep -v "->where\\|->select\\|->join\\|->with\\|->load" | grep "foreach\\|each\\|map\\|->get()\\|->all()" | head -20`,
78
+ { cwd, timeout: 15000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
79
+ ).trim();
80
+ if (n1Patterns) {
81
+ findings.push("### Potential N+1 Query Patterns");
82
+ findings.push("Files accessing relationships inside loops without eager loading:");
83
+ findings.push("```");
84
+ findings.push(n1Patterns);
85
+ findings.push("```");
86
+ }
87
+ } catch { /* no matches */ }
88
+
89
+ // Find models without $with or common relationship loading
90
+ try {
91
+ const models = execSync(
92
+ `grep -rl --include="*.php" "extends Model" app/ 2>/dev/null | head -20`,
93
+ { cwd, timeout: 10000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
94
+ ).trim();
95
+ if (models) {
96
+ const modelFiles = models.split("\n").filter((l) => l.trim());
97
+ const missingWith: string[] = [];
98
+ for (const modelFile of modelFiles) {
99
+ const modelContent = readFileSync(join(cwd, modelFile), "utf-8");
100
+ const hasRelationships = /function\s+\w+\(\).*?(?:hasMany|belongsTo|hasOne|belongsToMany|morphTo|morphMany)/s.test(modelContent);
101
+ const hasDefaultWith = /\$with\s*=\s*\[/.test(modelContent);
102
+ if (hasRelationships && !hasDefaultWith) {
103
+ missingWith.push(modelFile);
104
+ }
105
+ }
106
+ if (missingWith.length > 0) {
107
+ findings.push("", "### Models with Relationships but no $with Default");
108
+ findings.push("Consider adding `protected $with = [...]` for commonly loaded relationships:");
109
+ for (const f of missingWith) {
110
+ findings.push(`- ${f}`);
111
+ }
112
+ }
113
+ }
114
+ } catch { /* ignore */ }
115
+
116
+ // Find missing indexes (check migrations for foreign keys without indexes)
117
+ try {
118
+ const migrations = execSync(
119
+ `grep -rn --include="*.php" "foreignId\\|->foreign\\|references(" database/migrations/ 2>/dev/null | head -20`,
120
+ { cwd, timeout: 10000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
121
+ ).trim();
122
+ if (migrations) {
123
+ findings.push("", "### Foreign Keys (verify indexes exist)");
124
+ findings.push("These migrations define foreign keys -- ensure corresponding indexes exist:");
125
+ findings.push("```");
126
+ findings.push(migrations);
127
+ findings.push("```");
128
+ }
129
+ } catch { /* ignore */ }
130
+
131
+ findings.push("");
132
+ }
133
+
134
+ return {
135
+ content: [{ type: "text", text: findings.join("\n") }],
136
+ details: {},
137
+ };
138
+ } catch (error) {
139
+ const msg = error instanceof Error ? error.message : String(error);
140
+ return {
141
+ content: [{ type: "text", text: `Performance analysis failed: ${msg}` }],
142
+ details: {},
143
+ };
144
+ }
145
+ },
146
+ });
147
+
148
+ // ── Tool: explain_query ──
149
+ elyra.registerTool({
150
+ name: "explain_query",
151
+ label: "Explain SQL Query",
152
+ description:
153
+ "Run EXPLAIN on a SQL query to analyze its execution plan. " +
154
+ "Shows whether indexes are used, estimated rows, and join types. " +
155
+ "Works with MySQL and SQLite. Requires db-tools to be installed for database access.",
156
+ parameters: Type.Object({
157
+ sql: Type.String({ description: "The SQL query to explain" }),
158
+ engine: Type.Optional(
159
+ Type.Union([Type.Literal("mysql"), Type.Literal("sqlite")], {
160
+ description: "Database engine (default: auto-detect from .env)",
161
+ }),
162
+ ),
163
+ }),
164
+ execute: async (_toolCallId, params) => {
165
+ return {
166
+ content: [{
167
+ type: "text",
168
+ text: `Run EXPLAIN on this query and analyze the execution plan:\n\nEXPLAIN ${params.sql}\n\nLook for: full table scans (type=ALL), missing indexes, high row estimates, filesort, temporary tables.`,
169
+ }],
170
+ details: {},
171
+ };
172
+ },
173
+ });
174
+
175
+ // ── Commands ──
176
+ elyra.registerCommand("perf", {
177
+ description: "Analyze database query performance",
178
+ handler: async (_args, _ctx) => {
179
+ elyra.sendUserMessage("Analyze this project for database performance issues: N+1 queries, slow queries, missing indexes, and eager loading opportunities.");
180
+ },
181
+ });
182
+ }
183
+
184
+ interface DuplicatePattern {
185
+ pattern: string;
186
+ count: number;
187
+ }
188
+
189
+ function findDuplicatePatterns(queries: string[]): DuplicatePattern[] {
190
+ const normalized = queries.map((q) => q.replace(/\d+/g, "?").replace(/['"][^'"]*['"]/g, "?").slice(0, 100));
191
+ const counts = new Map<string, number>();
192
+ for (const q of normalized) {
193
+ counts.set(q, (counts.get(q) ?? 0) + 1);
194
+ }
195
+ return [...counts.entries()]
196
+ .filter(([_, count]) => count > 2)
197
+ .sort((a, b) => b[1] - a[1])
198
+ .map(([pattern, count]) => ({ pattern, count }));
199
+ }
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@elyracode/perf-tools",
3
+ "version": "0.5.2",
4
+ "description": "Elyra extension for performance analysis -- N+1 queries, slow queries, missing indexes",
5
+ "type": "module",
6
+ "keywords": ["elyra-package", "performance", "n+1", "queries", "optimization", "laravel"],
7
+ "license": "MIT",
8
+ "author": "Knut W. Horne",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/kwhorne/elyra.git",
12
+ "directory": "packages/perf-tools"
13
+ },
14
+ "elyra": { "extensions": ["./extensions/index.ts"] },
15
+ "peerDependencies": { "@elyracode/coding-agent": "*", "typebox": "*" },
16
+ "scripts": { "clean": "echo 'nothing to clean'", "build": "echo 'nothing to build'", "check": "echo 'nothing to check'" }
17
+ }