You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 1a95413
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
## [Unreleased]
9
9
10
10
### Added
11
+
-**What to do about the CPU, per library:** the CPU-cost finding now matches its hottest functions against known library costs and says what helps: Mongoose (`.lean()`), TypeORM (fewer relations, `getRawMany()`), Sequelize (`raw: true`), Prisma, Express's per-response ETag (`app.set('etag', false)`), class-transformer / class-validator, large JSON responses, password hashing, logging, garbage collection. Found on the NestJS app: 26% of its CPU was Express's `res.send`, 40% TypeORM query building.
11
12
-**`mysql` (the classic driver)**, used by TypeORM, Knex and many older apps: `instrumentMysql(tp, require('mysql'))`, auto-detected. Connections and pools (`pool.query` includes waiting for a free connection, and failures to get one), labelled like mysql2: `mysql SELECT users`.
12
13
-**`traceperf run` measures in two phases.** After the warm-up it sends `--probe` requests per path (default 10) one at a time, then the concurrent load. Per-request findings (a dominant call, sequential awaits, N+1) come from the one-at-a-time phase: under load every await also waits for the busy event loop, so a 1 ms query on a 60-row table measured 12 ms and was reported as "check this query's plan". Blocking, queueing, errors and CPU cost still come from the load.
13
14
-**Failed responses are called out first:** "2117 of 2117 responses failed (most: 2117× 401 Unauthorized), so the timings below describe error responses", with a hint (`-H` for 401/403, `--path` for 404). An expired token used to give "no issues found".
Copy file name to clipboardExpand all lines: README.md
+16-2Lines changed: 16 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -85,9 +85,22 @@ The database was fast; `buildReport` was blocking it. That only shows up when re
85
85
86
86
-**The blocking function is named even though nothing tracks it:**`run` samples the CPU (the V8 profiler) during the synchronous part of each blocking call. Native work is named by the app function that called it (`hashPassword → pbkdf2Sync`), code V8 inlined is credited to the inlined function, and garbage-collection pauses are reported as such. `--no-profile` turns this off.
87
87
-**Queueing the app can't see:** a request that arrives while the event loop is blocked waits in the socket before its handler starts. `run` compares what its clients waited with the app's own timings and reports the difference.
88
+
-**What each request costs:**`run` measures how busy the event loop was during the load and where the CPU went. When requests are expensive it says what that means and what helps, for the libraries it recognises (Mongoose, TypeORM, Sequelize, Prisma, Express, class-validator…):
89
+
90
+
```
91
+
🔥 Each request keeps the event loop busy ~10.7 ms, so one process tops out near 94 requests/s … CPU time went to:
fix: Mongoose is building full documents: add .lean() to read-only queries … and .select() only the fields you send
94
+
```
95
+
-**Timings you can trust:** after the warm-up, `run` sends a few requests one at a time (`--probe`) and takes the per-request findings (a call taking most of a request, sequential awaits, N+1) from those. Under concurrent load every `await` also waits for the busy event loop, which makes fast queries look slow.
96
+
-**Wrong setup is said first:** "2117 of 2117 responses failed (most: 401 Unauthorized)…" instead of timings of error pages.
97
+
98
+
Checked on real apps, not only on the planted ones: Prisma's REST example, the RealWorld API (Express + Prisma), an Express + Mongoose boilerplate and a NestJS + TypeORM app.
88
99
89
100
-`--path /x` (repeatable): what to request. Without `--path`, use the app yourself and press Ctrl-C for the report.
90
101
-`-c 10` concurrency, `-d 5s` duration or `-n 500` requests, `--method POST`, `--port 3000` (detected by default).
102
+
-`-H "Authorization: Bearer $TOKEN"` (repeatable): request headers, for APIs behind a login.
103
+
-`--probe 10`: requests per path sent one at a time before the load (0 for none).
91
104
-`--html report.html`, `--output calls.jsonl` (keep the trees), `--json`.
92
105
-`--warmup 3`: untimed requests per path first, so cold-start work doesn't look like a problem.
93
106
-`--no-profile`: don't sample the CPU.
@@ -99,7 +112,7 @@ The same naming works without `run`: `TRACEPERF_PROFILE_BLOCKING=1 node --requir
99
112
100
113
## No code changes: `traceperf/auto`
101
114
102
-
Start your app with traceperf preloaded. Every incoming HTTP request becomes a tree, with the outgoing `http`/`fetch` calls and the `pg`, `mysql2`, MongoDB/Mongoose, `ioredis`, Prisma and `better-sqlite3` queries it made nested under it (Drizzle runs on these drivers, so its queries show up too):
115
+
Start your app with traceperf preloaded. Every incoming HTTP request becomes a tree, with the outgoing `http`/`fetch` calls and the `pg`, `mysql2`, `mysql`, MongoDB/Mongoose, `ioredis`, Prisma and `better-sqlite3` queries it made nested under it (Drizzle runs on these drivers, so its queries show up too):
103
116
104
117
<!-- docs:skip -->
105
118
```bash
@@ -203,10 +216,11 @@ Ready-made helpers for popular clients. Pass the module you already use; tracepe
{test: /mongoose[\\/]lib[\\/](document|helpers|schema|types)/,advice: 'Mongoose is building full documents: add .lean() to read-only queries (plain objects, often several times less CPU) and .select() only the fields you send'},
273
+
{test: /typeorm[\\/](query-builder|driver[\\/]DriverUtils|metadata|entity-manager|persistence)/,advice: 'TypeORM spends CPU building queries and mapping rows to entities: on hot read paths load fewer relations and columns, or use getRawMany() / a raw query'},
274
+
{test: /sequelize[\\/]lib[\\/](model|instance|utils|dialects)/,advice: 'Sequelize is building model instances: use raw: true on hot read queries and limit attributes / includes'},
275
+
{test: /@prisma[\\/]client[\\/]runtime/,advice: 'Prisma\'s client is serialising queries and results: select only the fields you send and avoid deep include trees on hot paths'},
276
+
{test: /express[\\/]lib[\\/]response\.js|[\\/]etag[\\/]/,advice: "Express computes an ETag (a hash of the body) for every response: app.set('etag', false) if clients don't send conditional requests, and send smaller bodies"},
277
+
{test: /class-transformer|class-validator/,advice: 'class-transformer / class-validator run on every request (NestJS pipes and serialisers): skip them on hot read routes or validate less deeply'},
278
+
{test: /JSON\.stringify|fast-json-stringify|safe-stable-stringify/,advice: 'serialising large responses: paginate, send fewer fields, or use a schema-based serialiser (fast-json-stringify)'},
279
+
{test: /bcrypt|argon2|scrypt|pbkdf2/i,advice: 'password hashing is CPU by design: keep it to login routes, use the async API, and do not hash per request'},
280
+
{test: /winston|pino|morgan|debug[\\/]src|console/,advice: 'logging costs CPU per request: log less on hot routes, avoid pretty formatting in production, prefer pino'},
281
+
{test: /\(garbagecollection\)/,advice: 'much of it is garbage collection: allocate less per request (smaller objects and arrays, no needless copies)'},
282
+
];
283
+
284
+
/** Advice for the hottest functions (those with at least 10% of the CPU time), then the general fix */
constgeneral='run more processes (cluster or more instances) so requests stop waiting for each other';
293
+
returnhints.length ? `${hints.join('; ')}; beyond that, ${general}` : `cut the CPU each request needs (start with the functions above), or ${general}`;
294
+
}
295
+
270
296
/**
271
297
* The app was saturated *and* each request is expensive (5 ms of CPU or
272
298
* more, a ceiling near 200/s per process): a load test saturates any one
@@ -283,10 +309,7 @@ export function busyFinding(samples: LoadSample[], load: LoadResult, window: { s
? 'Mongoose is building full documents: add .lean() to read-only queries (plain objects, often several times less CPU) and select only the fields you send; beyond that, run more processes (cluster or more instances)'
289
-
: 'cut the CPU each request needs (start with the functions above), or run more processes (cluster or more instances) so requests stop waiting for each other';
0 commit comments