@ranimontagna/agent-toolkit 0.1.4 → 0.1.5
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 +282 -277
- package/docs/assets/install-plan.svg +29 -0
- package/docs/assets/install-skill-packages.svg +31 -0
- package/docs/assets/install-status.svg +32 -0
- package/package.json +10 -9
- package/setup-agent-toolkit.sh +1 -1
- package/skills/backend/fastify-best-practices/LICENSE +21 -0
- package/skills/backend/fastify-best-practices/NOTICE.md +11 -0
- package/skills/backend/fastify-best-practices/SKILL.md +75 -0
- package/skills/backend/fastify-best-practices/rules/authentication.md +521 -0
- package/skills/backend/fastify-best-practices/rules/configuration.md +217 -0
- package/skills/backend/fastify-best-practices/rules/content-type.md +387 -0
- package/skills/backend/fastify-best-practices/rules/cors-security.md +445 -0
- package/skills/backend/fastify-best-practices/rules/database.md +320 -0
- package/skills/backend/fastify-best-practices/rules/decorators.md +416 -0
- package/skills/backend/fastify-best-practices/rules/deployment.md +423 -0
- package/skills/backend/fastify-best-practices/rules/error-handling.md +412 -0
- package/skills/backend/fastify-best-practices/rules/hooks.md +464 -0
- package/skills/backend/fastify-best-practices/rules/http-proxy.md +247 -0
- package/skills/backend/fastify-best-practices/rules/logging.md +402 -0
- package/skills/backend/fastify-best-practices/rules/performance.md +425 -0
- package/skills/backend/fastify-best-practices/rules/plugins.md +320 -0
- package/skills/backend/fastify-best-practices/rules/routes.md +467 -0
- package/skills/backend/fastify-best-practices/rules/schemas.md +585 -0
- package/skills/backend/fastify-best-practices/rules/serialization.md +475 -0
- package/skills/backend/fastify-best-practices/rules/testing.md +536 -0
- package/skills/backend/fastify-best-practices/rules/typescript.md +458 -0
- package/skills/backend/fastify-best-practices/rules/websockets.md +421 -0
- package/skills/backend/fastify-best-practices/tile.json +11 -0
- package/skills/core/agent-toolkit-maintainer/SKILL.md +16 -14
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: performance
|
|
3
|
+
description: Performance optimization for Fastify applications
|
|
4
|
+
metadata:
|
|
5
|
+
tags: performance, optimization, speed, benchmarking
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Performance Optimization
|
|
9
|
+
|
|
10
|
+
## Fastify is Fast by Default
|
|
11
|
+
|
|
12
|
+
Fastify is designed for performance. Key optimizations are built-in:
|
|
13
|
+
|
|
14
|
+
- Fast JSON serialization with `fast-json-stringify`
|
|
15
|
+
- Efficient routing with `find-my-way`
|
|
16
|
+
- Schema-based validation with `ajv` (compiled validators)
|
|
17
|
+
- Low overhead request/response handling
|
|
18
|
+
|
|
19
|
+
## Use @fastify/under-pressure for Load Shedding
|
|
20
|
+
|
|
21
|
+
Protect your application from overload with `@fastify/under-pressure`:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import underPressure from '@fastify/under-pressure';
|
|
25
|
+
|
|
26
|
+
app.register(underPressure, {
|
|
27
|
+
maxEventLoopDelay: 1000, // Max event loop delay in ms
|
|
28
|
+
maxHeapUsedBytes: 1000000000, // Max heap used (~1GB)
|
|
29
|
+
maxRssBytes: 1500000000, // Max RSS (~1.5GB)
|
|
30
|
+
maxEventLoopUtilization: 0.98, // Max event loop utilization
|
|
31
|
+
pressureHandler: (request, reply, type, value) => {
|
|
32
|
+
reply.code(503).send({
|
|
33
|
+
error: 'Service Unavailable',
|
|
34
|
+
message: `Server under pressure: ${type}`,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Health check that respects pressure
|
|
40
|
+
app.get('/health', async (request, reply) => {
|
|
41
|
+
return { status: 'ok' };
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Always Define Response Schemas
|
|
46
|
+
|
|
47
|
+
Response schemas enable fast-json-stringify, which is significantly faster than JSON.stringify:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// FAST - uses fast-json-stringify
|
|
51
|
+
app.get('/users', {
|
|
52
|
+
schema: {
|
|
53
|
+
response: {
|
|
54
|
+
200: {
|
|
55
|
+
type: 'array',
|
|
56
|
+
items: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
id: { type: 'string' },
|
|
60
|
+
name: { type: 'string' },
|
|
61
|
+
email: { type: 'string' },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}, async () => {
|
|
68
|
+
return db.users.findAll();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// SLOW - uses JSON.stringify
|
|
72
|
+
app.get('/users-slow', async () => {
|
|
73
|
+
return db.users.findAll();
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Avoid Dynamic Schema Compilation
|
|
78
|
+
|
|
79
|
+
Add schemas at startup, not at request time:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// GOOD - schemas compiled at startup
|
|
83
|
+
app.addSchema({ $id: 'user', ... });
|
|
84
|
+
|
|
85
|
+
app.get('/users', {
|
|
86
|
+
schema: { response: { 200: { $ref: 'user#' } } },
|
|
87
|
+
}, handler);
|
|
88
|
+
|
|
89
|
+
// BAD - schema compiled per request
|
|
90
|
+
app.get('/users', async (request, reply) => {
|
|
91
|
+
const schema = getSchemaForUser(request.user);
|
|
92
|
+
// This is slow!
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Use Logger Wisely
|
|
97
|
+
|
|
98
|
+
Pino is fast, but excessive logging has overhead:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import Fastify from 'fastify';
|
|
102
|
+
|
|
103
|
+
// Set log level via environment variable
|
|
104
|
+
const app = Fastify({
|
|
105
|
+
logger: {
|
|
106
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Avoid logging large objects
|
|
111
|
+
app.get('/data', async (request) => {
|
|
112
|
+
// BAD - logs entire payload
|
|
113
|
+
request.log.info({ data: largeObject }, 'Processing');
|
|
114
|
+
|
|
115
|
+
// GOOD - log only what's needed
|
|
116
|
+
request.log.info({ id: largeObject.id }, 'Processing');
|
|
117
|
+
|
|
118
|
+
return largeObject;
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Connection Pooling
|
|
123
|
+
|
|
124
|
+
Use connection pools for databases:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import postgres from 'postgres';
|
|
128
|
+
|
|
129
|
+
// Create pool at startup
|
|
130
|
+
const sql = postgres(process.env.DATABASE_URL, {
|
|
131
|
+
max: 20, // Maximum pool size
|
|
132
|
+
idle_timeout: 20,
|
|
133
|
+
connect_timeout: 10,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
app.decorate('db', sql);
|
|
137
|
+
|
|
138
|
+
// Connections are reused
|
|
139
|
+
app.get('/users', async () => {
|
|
140
|
+
return app.db`SELECT * FROM users LIMIT 100`;
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Avoid Blocking the Event Loop
|
|
145
|
+
|
|
146
|
+
Use `piscina` for CPU-intensive operations. It provides a robust worker thread pool:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import Piscina from 'piscina';
|
|
150
|
+
import { join } from 'node:path';
|
|
151
|
+
|
|
152
|
+
const piscina = new Piscina({
|
|
153
|
+
filename: join(import.meta.dirname, 'workers', 'compute.js'),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
app.post('/compute', async (request) => {
|
|
157
|
+
const result = await piscina.run(request.body);
|
|
158
|
+
return result;
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// workers/compute.js
|
|
164
|
+
export default function compute(data) {
|
|
165
|
+
// CPU-intensive work here
|
|
166
|
+
return processedResult;
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Stream Large Responses
|
|
171
|
+
|
|
172
|
+
Stream large payloads instead of buffering:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { createReadStream } from 'node:fs';
|
|
176
|
+
import { pipeline } from 'node:stream/promises';
|
|
177
|
+
|
|
178
|
+
// GOOD - stream file
|
|
179
|
+
app.get('/large-file', async (request, reply) => {
|
|
180
|
+
const stream = createReadStream('./large-file.json');
|
|
181
|
+
reply.type('application/json');
|
|
182
|
+
return reply.send(stream);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// BAD - load entire file into memory
|
|
186
|
+
app.get('/large-file-bad', async () => {
|
|
187
|
+
const content = await fs.readFile('./large-file.json', 'utf-8');
|
|
188
|
+
return JSON.parse(content);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Stream database results
|
|
192
|
+
app.get('/export', async (request, reply) => {
|
|
193
|
+
reply.type('application/json');
|
|
194
|
+
|
|
195
|
+
const cursor = db.users.findCursor();
|
|
196
|
+
reply.raw.write('[');
|
|
197
|
+
|
|
198
|
+
let first = true;
|
|
199
|
+
for await (const user of cursor) {
|
|
200
|
+
if (!first) reply.raw.write(',');
|
|
201
|
+
reply.raw.write(JSON.stringify(user));
|
|
202
|
+
first = false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
reply.raw.write(']');
|
|
206
|
+
reply.raw.end();
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Caching Strategies
|
|
211
|
+
|
|
212
|
+
Implement caching for expensive operations:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { LRUCache } from 'lru-cache';
|
|
216
|
+
|
|
217
|
+
const cache = new LRUCache<string, unknown>({
|
|
218
|
+
max: 1000,
|
|
219
|
+
ttl: 60000, // 1 minute
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
app.get('/expensive/:id', async (request) => {
|
|
223
|
+
const { id } = request.params;
|
|
224
|
+
const cacheKey = `expensive:${id}`;
|
|
225
|
+
|
|
226
|
+
const cached = cache.get(cacheKey);
|
|
227
|
+
if (cached) {
|
|
228
|
+
return cached;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result = await expensiveOperation(id);
|
|
232
|
+
cache.set(cacheKey, result);
|
|
233
|
+
|
|
234
|
+
return result;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Cache control headers
|
|
238
|
+
app.get('/static-data', async (request, reply) => {
|
|
239
|
+
reply.header('Cache-Control', 'public, max-age=3600');
|
|
240
|
+
return { data: 'static' };
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Request Coalescing with async-cache-dedupe
|
|
245
|
+
|
|
246
|
+
Use `async-cache-dedupe` for deduplicating concurrent identical requests and caching:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { createCache } from 'async-cache-dedupe';
|
|
250
|
+
|
|
251
|
+
const cache = createCache({
|
|
252
|
+
ttl: 60, // seconds
|
|
253
|
+
stale: 5, // serve stale while revalidating
|
|
254
|
+
storage: { type: 'memory' },
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
cache.define('fetchData', async (id: string) => {
|
|
258
|
+
return db.findById(id);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
app.get('/data/:id', async (request) => {
|
|
262
|
+
const { id } = request.params;
|
|
263
|
+
// Automatically deduplicates concurrent requests for the same id
|
|
264
|
+
// and caches the result
|
|
265
|
+
return cache.fetchData(id);
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
For distributed caching, use Redis storage:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import { createCache } from 'async-cache-dedupe';
|
|
273
|
+
import Redis from 'ioredis';
|
|
274
|
+
|
|
275
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
276
|
+
|
|
277
|
+
const cache = createCache({
|
|
278
|
+
ttl: 60,
|
|
279
|
+
storage: { type: 'redis', options: { client: redis } },
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Payload Limits
|
|
284
|
+
|
|
285
|
+
Set appropriate payload limits:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import Fastify from 'fastify';
|
|
289
|
+
|
|
290
|
+
const app = Fastify({
|
|
291
|
+
bodyLimit: 1048576, // 1MB default
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Per-route limit for file uploads
|
|
295
|
+
app.post('/upload', {
|
|
296
|
+
bodyLimit: 10485760, // 10MB for this route
|
|
297
|
+
}, uploadHandler);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Compression
|
|
301
|
+
|
|
302
|
+
Use compression for responses:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import fastifyCompress from '@fastify/compress';
|
|
306
|
+
|
|
307
|
+
app.register(fastifyCompress, {
|
|
308
|
+
global: true,
|
|
309
|
+
threshold: 1024, // Only compress responses > 1KB
|
|
310
|
+
encodings: ['gzip', 'deflate'],
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Disable for specific route
|
|
314
|
+
app.get('/already-compressed', {
|
|
315
|
+
compress: false,
|
|
316
|
+
}, handler);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Connection Timeouts
|
|
320
|
+
|
|
321
|
+
Configure appropriate timeouts:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
import Fastify from 'fastify';
|
|
325
|
+
|
|
326
|
+
const app = Fastify({
|
|
327
|
+
connectionTimeout: 30000, // 30 seconds
|
|
328
|
+
keepAliveTimeout: 5000, // 5 seconds
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Per-route timeout
|
|
332
|
+
app.get('/long-operation', {
|
|
333
|
+
config: {
|
|
334
|
+
timeout: 60000, // 60 seconds
|
|
335
|
+
},
|
|
336
|
+
}, async (request) => {
|
|
337
|
+
return longOperation();
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Disable Unnecessary Features
|
|
342
|
+
|
|
343
|
+
Disable features you don't need:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import Fastify from 'fastify';
|
|
347
|
+
|
|
348
|
+
const app = Fastify({
|
|
349
|
+
disableRequestLogging: true, // If you don't need request logs
|
|
350
|
+
trustProxy: false, // If not behind proxy
|
|
351
|
+
caseSensitive: true, // Enable for slight performance gain
|
|
352
|
+
ignoreDuplicateSlashes: false,
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Benchmarking
|
|
357
|
+
|
|
358
|
+
Use autocannon for load testing:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
# Install
|
|
362
|
+
npm install -g autocannon
|
|
363
|
+
|
|
364
|
+
# Basic benchmark
|
|
365
|
+
autocannon http://localhost:3000/api/users
|
|
366
|
+
|
|
367
|
+
# With options
|
|
368
|
+
autocannon -c 100 -d 30 -p 10 http://localhost:3000/api/users
|
|
369
|
+
# -c: connections
|
|
370
|
+
# -d: duration in seconds
|
|
371
|
+
# -p: pipelining factor
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// Programmatic benchmarking
|
|
376
|
+
import autocannon from 'autocannon';
|
|
377
|
+
|
|
378
|
+
const result = await autocannon({
|
|
379
|
+
url: 'http://localhost:3000/api/users',
|
|
380
|
+
connections: 100,
|
|
381
|
+
duration: 30,
|
|
382
|
+
pipelining: 10,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
console.log(autocannon.printResult(result));
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Profiling
|
|
389
|
+
|
|
390
|
+
Use `@platformatic/flame` for flame graph profiling:
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
npx @platformatic/flame app.js
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
This generates an interactive flame graph to identify performance bottlenecks.
|
|
397
|
+
|
|
398
|
+
## Memory Management
|
|
399
|
+
|
|
400
|
+
Monitor and optimize memory usage:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// Add health endpoint with memory info
|
|
404
|
+
app.get('/health', async () => {
|
|
405
|
+
const memory = process.memoryUsage();
|
|
406
|
+
return {
|
|
407
|
+
status: 'ok',
|
|
408
|
+
memory: {
|
|
409
|
+
heapUsed: Math.round(memory.heapUsed / 1024 / 1024) + 'MB',
|
|
410
|
+
heapTotal: Math.round(memory.heapTotal / 1024 / 1024) + 'MB',
|
|
411
|
+
rss: Math.round(memory.rss / 1024 / 1024) + 'MB',
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Avoid memory leaks in closures
|
|
417
|
+
app.addHook('onRequest', async (request) => {
|
|
418
|
+
// BAD - holding reference to large object
|
|
419
|
+
const largeData = await loadLargeData();
|
|
420
|
+
request.getData = () => largeData;
|
|
421
|
+
|
|
422
|
+
// GOOD - load on demand
|
|
423
|
+
request.getData = () => loadLargeData();
|
|
424
|
+
});
|
|
425
|
+
```
|