traceperf 0.2.0 → 1.3.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 +612 -209
- package/dist/__tests__/helpers/console.d.ts +9 -0
- package/dist/__tests__/helpers/console.js +25 -0
- package/dist/__tests__/helpers/console.js.map +1 -0
- package/dist/auto.d.ts +30 -0
- package/dist/auto.js +91 -0
- package/dist/auto.js.map +1 -0
- package/dist/browser.d.ts +52 -144
- package/dist/browser.js +112 -439
- package/dist/browser.js.map +1 -1
- package/dist/cli.d.ts +68 -0
- package/dist/cli.js +528 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/allocations.d.ts +38 -0
- package/dist/core/allocations.js +85 -0
- package/dist/core/allocations.js.map +1 -0
- package/dist/core/cpu-profile.d.ts +84 -0
- package/dist/core/cpu-profile.js +181 -0
- package/dist/core/cpu-profile.js.map +1 -0
- package/dist/core/env.d.ts +10 -0
- package/dist/core/env.js +40 -0
- package/dist/core/env.js.map +1 -0
- package/dist/core/event-loop.d.ts +30 -0
- package/dist/core/event-loop.js +43 -0
- package/dist/core/event-loop.js.map +1 -0
- package/dist/core/explain.d.ts +48 -0
- package/dist/core/explain.js +236 -0
- package/dist/core/explain.js.map +1 -0
- package/dist/core/exporters.d.ts +111 -0
- package/dist/core/exporters.js +236 -0
- package/dist/core/exporters.js.map +1 -0
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.js +9 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/optimized-performance-monitor.d.ts +180 -26
- package/dist/core/optimized-performance-monitor.js +687 -175
- package/dist/core/optimized-performance-monitor.js.map +1 -1
- package/dist/core/prometheus.d.ts +36 -0
- package/dist/core/prometheus.js +93 -0
- package/dist/core/prometheus.js.map +1 -0
- package/dist/core/records.d.ts +97 -0
- package/dist/core/records.js +87 -0
- package/dist/core/records.js.map +1 -0
- package/dist/core/stats.d.ts +45 -0
- package/dist/core/stats.js +136 -0
- package/dist/core/stats.js.map +1 -0
- package/dist/core/timeline.d.ts +28 -0
- package/dist/core/timeline.js +65 -0
- package/dist/core/timeline.js.map +1 -0
- package/dist/drivers.d.ts +51 -0
- package/dist/drivers.js +205 -0
- package/dist/drivers.js.map +1 -0
- package/dist/esm/auto.d.ts +30 -0
- package/dist/esm/auto.js +87 -0
- package/dist/esm/auto.js.map +1 -0
- package/dist/esm/browser.d.ts +52 -144
- package/dist/esm/browser.js +101 -445
- package/dist/esm/browser.js.map +1 -1
- package/dist/esm/cli.d.ts +68 -0
- package/dist/esm/cli.js +520 -0
- package/dist/esm/cli.js.map +1 -0
- package/dist/esm/core/allocations.d.ts +38 -0
- package/dist/esm/core/allocations.js +82 -0
- package/dist/esm/core/allocations.js.map +1 -0
- package/dist/esm/core/cpu-profile.d.ts +84 -0
- package/dist/esm/core/cpu-profile.js +176 -0
- package/dist/esm/core/cpu-profile.js.map +1 -0
- package/dist/esm/core/env.d.ts +10 -0
- package/dist/esm/core/env.js +35 -0
- package/dist/esm/core/env.js.map +1 -0
- package/dist/esm/core/event-loop.d.ts +30 -0
- package/dist/esm/core/event-loop.js +40 -0
- package/dist/esm/core/event-loop.js.map +1 -0
- package/dist/esm/core/explain.d.ts +48 -0
- package/dist/esm/core/explain.js +232 -0
- package/dist/esm/core/explain.js.map +1 -0
- package/dist/esm/core/exporters.d.ts +111 -0
- package/dist/esm/core/exporters.js +229 -0
- package/dist/esm/core/exporters.js.map +1 -0
- package/dist/esm/core/index.d.ts +10 -4
- package/dist/esm/core/index.js +10 -4
- package/dist/esm/core/index.js.map +1 -1
- package/dist/esm/core/optimized-performance-monitor.d.ts +180 -26
- package/dist/esm/core/optimized-performance-monitor.js +685 -173
- package/dist/esm/core/optimized-performance-monitor.js.map +1 -1
- package/dist/esm/core/prometheus.d.ts +36 -0
- package/dist/esm/core/prometheus.js +89 -0
- package/dist/esm/core/prometheus.js.map +1 -0
- package/dist/esm/core/records.d.ts +97 -0
- package/dist/esm/core/records.js +82 -0
- package/dist/esm/core/records.js.map +1 -0
- package/dist/esm/core/stats.d.ts +45 -0
- package/dist/esm/core/stats.js +131 -0
- package/dist/esm/core/stats.js.map +1 -0
- package/dist/esm/core/timeline.d.ts +28 -0
- package/dist/esm/core/timeline.js +62 -0
- package/dist/esm/core/timeline.js.map +1 -0
- package/dist/esm/drivers.d.ts +51 -0
- package/dist/esm/drivers.js +197 -0
- package/dist/esm/drivers.js.map +1 -0
- package/dist/esm/index.d.ts +13 -5
- package/dist/esm/index.js +21 -17
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/instrument.d.ts +88 -0
- package/dist/esm/instrument.js +274 -0
- package/dist/esm/instrument.js.map +1 -0
- package/dist/esm/integrations.d.ts +70 -0
- package/dist/esm/integrations.js +197 -0
- package/dist/esm/integrations.js.map +1 -0
- package/dist/esm/node-builtins.d.ts +1 -0
- package/dist/esm/node-builtins.js +32 -0
- package/dist/esm/node-builtins.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/register.d.ts +20 -0
- package/dist/esm/register.js +98 -0
- package/dist/esm/register.js.map +1 -0
- package/dist/esm/report-html.d.ts +11 -0
- package/dist/esm/report-html.js +106 -0
- package/dist/esm/report-html.js.map +1 -0
- package/dist/esm/types/index.d.ts +4 -80
- package/dist/esm/types/index.js +1 -2
- package/dist/esm/types/index.js.map +1 -1
- package/dist/esm/types/options.d.ts +72 -0
- package/dist/esm/types/options.js +2 -0
- package/dist/esm/types/options.js.map +1 -0
- package/dist/esm/watch.d.ts +17 -0
- package/dist/esm/watch.js +87 -0
- package/dist/esm/watch.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +26 -29
- package/dist/index.js.map +1 -1
- package/dist/instrument.d.ts +88 -0
- package/dist/instrument.js +282 -0
- package/dist/instrument.js.map +1 -0
- package/dist/integrations.d.ts +70 -0
- package/dist/integrations.js +205 -0
- package/dist/integrations.js.map +1 -0
- package/dist/node-builtins.d.ts +1 -0
- package/dist/node-builtins.js +67 -0
- package/dist/node-builtins.js.map +1 -0
- package/dist/register.d.ts +20 -0
- package/dist/register.js +103 -0
- package/dist/register.js.map +1 -0
- package/dist/report-html.d.ts +11 -0
- package/dist/report-html.js +109 -0
- package/dist/report-html.js.map +1 -0
- package/dist/traceperf.browser.js +1910 -0
- package/dist/traceperf.browser.min.js +6 -0
- package/dist/types/index.d.ts +4 -80
- package/dist/types/index.js +1 -2
- package/dist/types/index.js.map +1 -1
- package/dist/types/options.d.ts +72 -0
- package/dist/types/{config.js → options.js} +1 -1
- package/dist/types/options.js.map +1 -0
- package/dist/watch.d.ts +17 -0
- package/dist/watch.js +90 -0
- package/dist/watch.js.map +1 -0
- package/package.json +99 -19
- package/dist/core/config.d.ts +0 -77
- package/dist/core/config.js +0 -116
- package/dist/core/config.js.map +0 -1
- package/dist/core/constants.d.ts +0 -78
- package/dist/core/constants.js +0 -109
- package/dist/core/constants.js.map +0 -1
- package/dist/core/logger.d.ts +0 -97
- package/dist/core/logger.js +0 -186
- package/dist/core/logger.js.map +0 -1
- package/dist/esm/core/config.d.ts +0 -77
- package/dist/esm/core/config.js +0 -112
- package/dist/esm/core/config.js.map +0 -1
- package/dist/esm/core/constants.d.ts +0 -78
- package/dist/esm/core/constants.js +0 -106
- package/dist/esm/core/constants.js.map +0 -1
- package/dist/esm/core/logger.d.ts +0 -97
- package/dist/esm/core/logger.js +0 -182
- package/dist/esm/core/logger.js.map +0 -1
- package/dist/esm/formatters/ascii.d.ts +0 -52
- package/dist/esm/formatters/ascii.js +0 -100
- package/dist/esm/formatters/ascii.js.map +0 -1
- package/dist/esm/formatters/cli.d.ts +0 -50
- package/dist/esm/formatters/cli.js +0 -96
- package/dist/esm/formatters/cli.js.map +0 -1
- package/dist/esm/formatters/index.d.ts +0 -3
- package/dist/esm/formatters/index.js +0 -4
- package/dist/esm/formatters/index.js.map +0 -1
- package/dist/esm/formatters/json.d.ts +0 -45
- package/dist/esm/formatters/json.js +0 -85
- package/dist/esm/formatters/json.js.map +0 -1
- package/dist/esm/trackers/execution.d.ts +0 -101
- package/dist/esm/trackers/execution.js +0 -226
- package/dist/esm/trackers/execution.js.map +0 -1
- package/dist/esm/trackers/index.d.ts +0 -2
- package/dist/esm/trackers/index.js +0 -3
- package/dist/esm/trackers/index.js.map +0 -1
- package/dist/esm/trackers/performance.d.ts +0 -68
- package/dist/esm/trackers/performance.js +0 -112
- package/dist/esm/trackers/performance.js.map +0 -1
- package/dist/esm/types/config.d.ts +0 -142
- package/dist/esm/types/config.js +0 -2
- package/dist/esm/types/config.js.map +0 -1
- package/dist/esm/types/logger.d.ts +0 -147
- package/dist/esm/types/logger.js +0 -2
- package/dist/esm/types/logger.js.map +0 -1
- package/dist/esm/utils/colors.d.ts +0 -87
- package/dist/esm/utils/colors.js +0 -146
- package/dist/esm/utils/colors.js.map +0 -1
- package/dist/esm/utils/index.d.ts +0 -3
- package/dist/esm/utils/index.js +0 -4
- package/dist/esm/utils/index.js.map +0 -1
- package/dist/esm/utils/proxy-tracker.d.ts +0 -68
- package/dist/esm/utils/proxy-tracker.js +0 -127
- package/dist/esm/utils/proxy-tracker.js.map +0 -1
- package/dist/esm/utils/stack.d.ts +0 -63
- package/dist/esm/utils/stack.js +0 -110
- package/dist/esm/utils/stack.js.map +0 -1
- package/dist/esm/utils/string.d.ts +0 -29
- package/dist/esm/utils/string.js +0 -45
- package/dist/esm/utils/string.js.map +0 -1
- package/dist/esm/utils/timing.d.ts +0 -65
- package/dist/esm/utils/timing.js +0 -99
- package/dist/esm/utils/timing.js.map +0 -1
- package/dist/formatters/ascii.d.ts +0 -52
- package/dist/formatters/ascii.js +0 -104
- package/dist/formatters/ascii.js.map +0 -1
- package/dist/formatters/cli.d.ts +0 -50
- package/dist/formatters/cli.js +0 -100
- package/dist/formatters/cli.js.map +0 -1
- package/dist/formatters/index.d.ts +0 -3
- package/dist/formatters/index.js +0 -20
- package/dist/formatters/index.js.map +0 -1
- package/dist/formatters/json.d.ts +0 -45
- package/dist/formatters/json.js +0 -89
- package/dist/formatters/json.js.map +0 -1
- package/dist/trackers/execution.d.ts +0 -101
- package/dist/trackers/execution.js +0 -230
- package/dist/trackers/execution.js.map +0 -1
- package/dist/trackers/index.d.ts +0 -2
- package/dist/trackers/index.js +0 -19
- package/dist/trackers/index.js.map +0 -1
- package/dist/trackers/performance.d.ts +0 -68
- package/dist/trackers/performance.js +0 -116
- package/dist/trackers/performance.js.map +0 -1
- package/dist/types/config.d.ts +0 -142
- package/dist/types/config.js.map +0 -1
- package/dist/types/logger.d.ts +0 -147
- package/dist/types/logger.js +0 -3
- package/dist/types/logger.js.map +0 -1
- package/dist/types.d.ts +0 -65
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
- package/dist/utils/colors.d.ts +0 -87
- package/dist/utils/colors.js +0 -160
- package/dist/utils/colors.js.map +0 -1
- package/dist/utils/index.d.ts +0 -3
- package/dist/utils/index.js +0 -20
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/proxy-tracker.d.ts +0 -68
- package/dist/utils/proxy-tracker.js +0 -131
- package/dist/utils/proxy-tracker.js.map +0 -1
- package/dist/utils/stack.d.ts +0 -63
- package/dist/utils/stack.js +0 -116
- package/dist/utils/stack.js.map +0 -1
- package/dist/utils/string.d.ts +0 -29
- package/dist/utils/string.js +0 -50
- package/dist/utils/string.js.map +0 -1
- package/dist/utils/timing.d.ts +0 -65
- package/dist/utils/timing.js +0 -106
- package/dist/utils/timing.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,321 +1,724 @@
|
|
|
1
1
|
# TracePerf
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**See where the time goes in your code.** It prints the call tree while you develop, keeps cheap per-function statistics in production, and exports to Perfetto or OpenTelemetry when you need more.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/traceperf)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+

|
|
9
9
|
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
10
|
+
- **No code changes needed.** `node --require traceperf/auto app.js` tracks every request, outgoing call and database query, and tells you why slow requests were slow when the process exits.
|
|
11
|
+
- **Explains, not just measures.** `traceperf explain` points out blocked event loops (and which function blocked it), awaits that could run in parallel and N+1 queries.
|
|
12
|
+
- **Call trees, not just timers.** Tracked calls inside tracked calls nest automatically, including across `await` on Node.js.
|
|
13
|
+
- **Honest numbers.** You get total, self and synchronous time for each call. Detailed mode flags calls that blocked the event loop.
|
|
14
|
+
- **Production-ready.** Stats are bounded per label (p50/p95/p99), sampling is built in, and `enabled: false` makes tracking a pass-through costing ~40 ns.
|
|
15
|
+
- **Gets data out.** Chrome/Perfetto traces, OpenTelemetry spans, JSON lines, or your own `onComplete` hook.
|
|
16
|
+
- **Zero-effort coverage.** One line tracks every `fetch`, HTTP request or query, for pg, mysql2, MongoDB/Mongoose, ioredis, Prisma and the AWS SDK.
|
|
17
|
+
- **Built for teams.** It feeds Prometheus, catches regressions in CI (`traceperf compare --fail`), shows up in Chrome DevTools, and produces HTML flame-chart reports.
|
|
18
|
+
- **Fits your stack.** Express, Fastify, Hono, Koa, NestJS, Next.js route handlers, decorators, CommonJS, ESM, TypeScript, browsers, Node.js 14+, Bun, Deno, Cloudflare Workers and Vercel Edge. There are no runtime dependencies.
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
Try it in the browser: [the playground](examples/playground) is an Express API with a hidden N+1 query problem.
|
|
21
|
+
|
|
22
|
+
> Upgrading from 0.x? The tracking API is unchanged; only the old undocumented logging classes were removed. See [MIGRATION.md](MIGRATION.md).
|
|
23
|
+
|
|
24
|
+
## Install
|
|
20
25
|
|
|
21
26
|
```bash
|
|
22
27
|
npm install traceperf
|
|
23
28
|
```
|
|
24
29
|
|
|
25
|
-
## Quick
|
|
30
|
+
## Quick start
|
|
26
31
|
|
|
27
32
|
```javascript
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
//
|
|
33
|
-
return
|
|
34
|
-
}, { label: '
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
const { createTracePerf } = require('traceperf');
|
|
34
|
+
const tp = createTracePerf();
|
|
35
|
+
|
|
36
|
+
const fetchUser = tp.createTrackable(async (id) => {
|
|
37
|
+
await new Promise(r => setTimeout(r, 20)); // pretend this is a database call
|
|
38
|
+
return { id };
|
|
39
|
+
}, { label: 'fetchUser' });
|
|
40
|
+
|
|
41
|
+
const render = tp.createTrackable((user) => `<h1>${user.id}</h1>`, { label: 'render' });
|
|
42
|
+
|
|
43
|
+
await tp.track(async () => render(await fetchUser(42)), { label: 'handleRequest' });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Prints one tree when the outermost call finishes:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
└─ handleRequest: 26.412 ms 📊 +83.34 KB
|
|
50
|
+
├─ fetchUser: 20.312 ms 📊 +28.79 KB
|
|
51
|
+
└─ render: 0.314 ms 📊 +1.53 KB
|
|
43
52
|
```
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
`📊` is the change in V8 heap during the call. A negative value is marked `(gc)`, because it means a garbage collection ran during the call.
|
|
46
55
|
|
|
47
|
-
|
|
56
|
+
ES modules work the same way:
|
|
48
57
|
|
|
49
58
|
```javascript
|
|
50
|
-
|
|
59
|
+
import tp, { createTracePerf, TrackingMode } from 'traceperf';
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
tp.track(() => JSON.parse('{"a":1}'), { label: 'parse' }); // the default instance
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## No code changes: `traceperf/auto`
|
|
65
|
+
|
|
66
|
+
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 and `ioredis` queries it made nested under it:
|
|
67
|
+
|
|
68
|
+
<!-- docs:skip -->
|
|
69
|
+
```bash
|
|
70
|
+
node --require traceperf/auto server.js # CommonJS or ESM apps
|
|
71
|
+
node --import traceperf/auto server.mjs # ESM, Node.js >= 18.19
|
|
72
|
+
NODE_OPTIONS="--require traceperf/auto" npm start # through any script
|
|
73
|
+
```
|
|
59
74
|
|
|
60
|
-
// Create a custom instance with detailed tracking for development
|
|
61
|
-
const devTracker = createTracePerf({
|
|
62
|
-
trackingMode: TrackingMode.DETAILED,
|
|
63
|
-
trackMemory: true,
|
|
64
|
-
enableNestedTracking: true
|
|
65
|
-
});
|
|
66
75
|
```
|
|
76
|
+
traceperf: tracking server, http, fetch, pg
|
|
77
|
+
└─ GET /users/:id: 84.112 ms
|
|
78
|
+
├─ pg SELECT users: 12.331 ms
|
|
79
|
+
└─ fetch GET billing.internal/invoices: 70.950 ms
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
When the process exits (including Ctrl-C), it prints the statistics table and an explanation of what made calls slow (see [`traceperf explain`](#why-was-it-slow-traceperf-explain)). Ids in paths are collapsed (`/users/42` becomes `/users/:id`), so labels stay few.
|
|
83
|
+
|
|
84
|
+
Database clients are only instrumented when your `package.json` lists them. Configure it with environment variables:
|
|
85
|
+
|
|
86
|
+
| Variable | Effect |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `TRACEPERF_OUTPUT=calls.jsonl` | Append every request tree as JSON lines (for `traceperf report`, `explain`, `compare`, `watch`). Console trees are then off. |
|
|
89
|
+
| `TRACEPERF_THRESHOLD=50` | Only report calls taking at least 50 ms |
|
|
90
|
+
| `TRACEPERF_SAMPLE_RATE=0.1` | Track 10% of requests |
|
|
91
|
+
| `TRACEPERF_IGNORE=^/health` | Regular expression of request paths not to track |
|
|
92
|
+
| `TRACEPERF_DISABLE=http,fetch` | Skip targets: `server`, `http`, `fetch`, `pg`, `mysql2`, `mongodb`, `ioredis` |
|
|
93
|
+
| `TRACEPERF_SILENT=1` / `TRACEPERF_SUMMARY=0` / `TRACEPERF_QUIET=1` | No per-request trees / no exit summary / no startup line |
|
|
94
|
+
| `TRACEPERF_ENABLED=0` | Load nothing |
|
|
67
95
|
|
|
68
|
-
|
|
96
|
+
Your own `tp.track(...)` calls (with `require('traceperf')` or `import tp from 'traceperf'`) nest inside the request trees too. In code, the same thing is `autoInstrument(tp)`, and request tracking alone is `instrumentHttpServer(tp)`. Don't combine it with a framework middleware from [Frameworks](#frameworks), or each request is tracked twice.
|
|
97
|
+
|
|
98
|
+
## Ways to track
|
|
69
99
|
|
|
70
100
|
```javascript
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
threshold: 50, // ms
|
|
78
|
-
trackMemory: true
|
|
79
|
-
});
|
|
101
|
+
const { createTracePerf } = require('traceperf');
|
|
102
|
+
const tp = createTracePerf();
|
|
103
|
+
|
|
104
|
+
// 1. Wrap a function once, call it as usual (keeps `this`, name and length)
|
|
105
|
+
const add = tp.createTrackable((a, b) => a + b, { label: 'add' });
|
|
106
|
+
add(2, 3);
|
|
80
107
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
108
|
+
// 2. Track a single call
|
|
109
|
+
const total = tp.track(() => [1, 2, 3].reduce((a, b) => a + b), { label: 'sum' });
|
|
110
|
+
|
|
111
|
+
// 3. Track every method of an object or class instance (a proxy; state stays on the original)
|
|
112
|
+
class Cart {
|
|
113
|
+
items = [];
|
|
114
|
+
add(item) { this.items.push(item); return this.items.length; }
|
|
115
|
+
}
|
|
116
|
+
const cart = tp.trackObject(new Cart());
|
|
117
|
+
cart.add('book'); // reported as "Cart.add"
|
|
118
|
+
|
|
119
|
+
// 4. Track work that is not one function call (a "span")
|
|
120
|
+
const span = tp.start('import batch');
|
|
121
|
+
span.run(() => add(1, 1)); // tracked calls inside become children
|
|
122
|
+
span.end();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
With TypeScript (or any decorator-capable build), methods can be decorated. Both TypeScript 5 standard decorators and legacy `experimentalDecorators` are supported:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { traced } from 'traceperf';
|
|
129
|
+
|
|
130
|
+
class UserService {
|
|
131
|
+
@traced() // label: "UserService.getUser"
|
|
132
|
+
async getUser(id: string) { /* ... */ }
|
|
133
|
+
|
|
134
|
+
@traced({ threshold: 50 }) // only report slow calls
|
|
135
|
+
async search(q: string) { /* ... */ }
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`registerModule(obj)` is still available. It returns a copy of a plain object with its own methods tracked; `trackObject` is usually the better choice.
|
|
140
|
+
|
|
141
|
+
## Auto-instrumentation
|
|
142
|
+
|
|
143
|
+
Track I/O without wrapping each call. Every `instrument*` call patches in place, is safe to call twice, and returns an uninstall function.
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
const { createTracePerf, instrumentFetch, instrumentHttp, instrumentMethods } = require('traceperf');
|
|
147
|
+
const tp = createTracePerf({ trackMemory: false });
|
|
148
|
+
|
|
149
|
+
instrumentFetch(tp); // every fetch(): "fetch GET api.example.com/users"
|
|
150
|
+
instrumentHttp(tp); // node http/https (axios, got, ...): "http GET api.example.com/users"
|
|
151
|
+
|
|
152
|
+
// Any client: patch its prototype, with per-call labels
|
|
153
|
+
class Db { async query(sql) { return []; } }
|
|
154
|
+
instrumentMethods(tp, Db.prototype, ['query'], {
|
|
155
|
+
labelFor: (_method, [sql]) => `db ${String(sql).trim().split(/\s+/)[0]}`, // "db SELECT"
|
|
85
156
|
});
|
|
86
157
|
|
|
87
|
-
|
|
88
|
-
|
|
158
|
+
await tp.track(async () => { await new Db().query('SELECT * FROM users'); }, { label: 'handler' });
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
└─ handler: 0.112 ms
|
|
163
|
+
└─ db SELECT: 0.031 ms
|
|
89
164
|
```
|
|
90
165
|
|
|
91
|
-
|
|
166
|
+
Ready-made helpers for popular clients. Pass the module you already use; traceperf doesn't depend on any of them:
|
|
92
167
|
|
|
168
|
+
<!-- docs:skip -->
|
|
93
169
|
```javascript
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
170
|
+
const { instrumentPg, instrumentMysql2, instrumentMongo, instrumentRedis, instrumentAwsClient, prismaExtension } = require('traceperf');
|
|
171
|
+
|
|
172
|
+
instrumentPg(tp, require('pg')); // "pg SELECT users" (Pool.query included)
|
|
173
|
+
instrumentMysql2(tp, require('mysql2')); // "mysql UPDATE orders" (callback and promise APIs)
|
|
174
|
+
instrumentMongo(tp, require('mongodb')); // "mongo find users", "mongo insertOne users"
|
|
175
|
+
instrumentMongo(tp, require('mongoose').mongo); // Mongoose uses its own copy of the driver
|
|
176
|
+
instrumentRedis(tp, require('ioredis')); // "redis GET"
|
|
177
|
+
instrumentAwsClient(tp, s3Client); // "aws S3.GetObject"
|
|
178
|
+
const prisma = new PrismaClient().$extends(prismaExtension(tp)); // "prisma.User.findMany"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
SQL labels are the verb plus the first table (`sqlLabel()`), never the values, so stats stay grouped. These are tested against the real client libraries, with Postgres and MongoDB running for real.
|
|
182
|
+
|
|
183
|
+
`fetch` timing ends when the response headers arrive. `http` timing ends when the body has been read. Responses with status ≥ 500 mark the call failed.
|
|
184
|
+
|
|
185
|
+
## Nested tracking
|
|
186
|
+
|
|
187
|
+
Any tracked call made while another tracked call is running becomes its child. You don't need to pass anything through:
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
const { createTracePerf } = require('traceperf');
|
|
191
|
+
const traceperf = createTracePerf({ trackMemory: false });
|
|
99
192
|
|
|
100
|
-
|
|
101
|
-
|
|
193
|
+
async function fetchData() {
|
|
194
|
+
return traceperf.track(async () => {
|
|
195
|
+
await new Promise(r => setTimeout(r, 10));
|
|
196
|
+
return [1, 2, 3];
|
|
197
|
+
}, { label: 'fetchData' });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function transformData(data) {
|
|
201
|
+
return traceperf.track(() => data.map(x => x * 2), { label: 'transformData' });
|
|
202
|
+
}
|
|
102
203
|
|
|
103
|
-
|
|
104
|
-
|
|
204
|
+
async function processData() {
|
|
205
|
+
return traceperf.track(async () => transformData(await fetchData()), { label: 'processData' });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
processData();
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
└─ processData: 11.912 ms
|
|
213
|
+
├─ fetchData: 11.217 ms
|
|
214
|
+
└─ transformData: 0.075 ms
|
|
105
215
|
```
|
|
106
216
|
|
|
107
|
-
|
|
217
|
+
On Node.js this uses `AsyncLocalStorage`, so children started after an `await` are attached correctly and concurrent requests never get mixed up. In browsers, nesting follows the synchronous call stack: calls made after the first `await` of a tracked async function become separate trees.
|
|
218
|
+
|
|
219
|
+
## Configuration
|
|
108
220
|
|
|
109
221
|
```javascript
|
|
110
222
|
const { createTracePerf, TrackingMode } = require('traceperf');
|
|
111
223
|
|
|
112
|
-
const
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Enable or disable performance statistics in console
|
|
117
|
-
silent: false,
|
|
118
|
-
|
|
119
|
-
// Track memory usage (slight performance impact)
|
|
120
|
-
trackMemory: true,
|
|
121
|
-
|
|
122
|
-
// Enable tracking of nested function calls
|
|
224
|
+
const tp = createTracePerf({
|
|
225
|
+
trackingMode: TrackingMode.BALANCED, // PERFORMANCE | BALANCED (default) | DETAILED | DEBUG
|
|
226
|
+
silent: false, // no console output (stats, hooks and exporters still run)
|
|
227
|
+
trackMemory: true, // heap delta per call
|
|
123
228
|
enableNestedTracking: true,
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
sampleRate: 1.0
|
|
229
|
+
threshold: 0, // only report calls taking at least this many ms
|
|
230
|
+
sampleRate: 1, // fraction of calls to track (0..1)
|
|
231
|
+
enabled: true, // false: tracked functions are plain pass-through calls
|
|
232
|
+
blockingThreshold: 50, // DETAILED: flag calls whose sync part runs >= 50 ms
|
|
233
|
+
stats: true, // per-label statistics ({ reservoirSize, maxLabels } to tune)
|
|
130
234
|
});
|
|
131
235
|
```
|
|
132
236
|
|
|
133
|
-
|
|
237
|
+
Every option except `stats`, `plugins` and `reporter` can also be passed per call to `track`/`createTrackable` as `{ label, silent, trackMemory, trackingMode, threshold, sampleRate, captureArgs, enableNestedTracking }`. Anything you leave out follows the instance's current settings, so `tp.setConfig()`, `tp.enableSilentMode()` and `tp.setSampleRate()` also apply to functions wrapped earlier.
|
|
238
|
+
|
|
239
|
+
Options are strictly typed: `{ treshold: 50 }` doesn't compile.
|
|
134
240
|
|
|
135
|
-
|
|
241
|
+
### Tracking modes
|
|
242
|
+
|
|
243
|
+
| Mode | Console output | Memory | Use for |
|
|
244
|
+
|---|---|---|---|
|
|
245
|
+
| `PERFORMANCE` | none | no | production: stats, hooks and exporters only |
|
|
246
|
+
| `BALANCED` (default) | tree with duration and heap delta | yes | everyday development |
|
|
247
|
+
| `DETAILED` / `DEBUG` | adds self time, sync time, event-loop blocking and error messages | yes | finding the actual bottleneck |
|
|
248
|
+
|
|
249
|
+
`DETAILED` separates waiting from working:
|
|
136
250
|
|
|
137
251
|
```javascript
|
|
138
|
-
|
|
252
|
+
const { createTracePerf, TrackingMode } = require('traceperf');
|
|
253
|
+
const tp = createTracePerf({ trackingMode: TrackingMode.DETAILED, trackMemory: false });
|
|
254
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
255
|
+
|
|
256
|
+
const parseConfig = tp.createTrackable(() => {
|
|
257
|
+
const start = Date.now();
|
|
258
|
+
while (Date.now() - start < 60) {} // CPU-bound work
|
|
259
|
+
}, { label: 'parseConfig' });
|
|
260
|
+
|
|
261
|
+
await tp.track(async () => {
|
|
262
|
+
parseConfig();
|
|
263
|
+
await Promise.all([
|
|
264
|
+
tp.track(() => sleep(30), { label: 'loadA' }),
|
|
265
|
+
tp.track(() => sleep(30), { label: 'loadB' }),
|
|
266
|
+
]);
|
|
267
|
+
}, { label: 'startup' });
|
|
268
|
+
```
|
|
139
269
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
270
|
+
```
|
|
271
|
+
└─ startup: 93.880 ms (self 0.533 ms, sync 60.720 ms) ⚠ blocked event loop 60.720 ms
|
|
272
|
+
├─ parseConfig: 59.891 ms ⚠ blocked event loop 59.891 ms
|
|
273
|
+
├─ loadA: 29.756 ms (sync 0.200 ms)
|
|
274
|
+
└─ loadB: 33.208 ms (sync 0.059 ms)
|
|
144
275
|
```
|
|
145
276
|
|
|
146
|
-
|
|
277
|
+
- **self**: time not covered by tracked children. Overlapping children (as with `Promise.all`) are counted once.
|
|
278
|
+
- **sync**: for async calls, the part that ran before the first `await`. That part blocks everything else.
|
|
147
279
|
|
|
148
|
-
|
|
280
|
+
### Thresholds
|
|
149
281
|
|
|
150
|
-
|
|
151
|
-
|
|
282
|
+
```javascript
|
|
283
|
+
const { createTracePerf } = require('traceperf');
|
|
284
|
+
const tracker = createTracePerf({ threshold: 50 }); // calls under 50 ms print nothing
|
|
285
|
+
|
|
286
|
+
tracker.track(slowFunction, { label: 'slowFunction' }); // reported if >= 50 ms
|
|
287
|
+
tracker.track(criticalFunction, { threshold: 10 }); // stricter for a hot path
|
|
288
|
+
tracker.track(backgroundTask, { threshold: 200 }); // looser for background work
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
└─ slowFunction: 60.950 ms 📊 +144.98 KB
|
|
293
|
+
```
|
|
152
294
|
|
|
153
|
-
###
|
|
295
|
+
### Logging arguments
|
|
154
296
|
|
|
155
297
|
```javascript
|
|
156
298
|
const { createTracePerf } = require('traceperf');
|
|
157
|
-
const
|
|
299
|
+
const tp = createTracePerf({ trackMemory: false });
|
|
158
300
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const response = await fetch(url);
|
|
162
|
-
return response.json();
|
|
163
|
-
}, {
|
|
164
|
-
label: 'fetchData',
|
|
165
|
-
trackMemory: true
|
|
166
|
-
});
|
|
167
|
-
}
|
|
301
|
+
const getUser = tp.createTrackable((id, opts) => ({ id }), { label: 'getUser', captureArgs: true });
|
|
302
|
+
getUser(123, { cache: true });
|
|
168
303
|
```
|
|
169
304
|
|
|
170
|
-
|
|
305
|
+
```
|
|
306
|
+
└─ getUser(123, {"cache":true}): 0.051 ms
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Arguments are rendered safely and kept short. Circular, huge or exotic values never throw and never get walked in full.
|
|
310
|
+
|
|
311
|
+
## Statistics
|
|
312
|
+
|
|
313
|
+
Every tracked call (including in `PERFORMANCE` mode and when silent) feeds per-label statistics with bounded memory:
|
|
171
314
|
|
|
172
315
|
```javascript
|
|
173
316
|
const { createTracePerf } = require('traceperf');
|
|
174
|
-
const
|
|
317
|
+
const tp = createTracePerf({ silent: true });
|
|
318
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
175
319
|
|
|
176
|
-
async
|
|
177
|
-
|
|
178
|
-
// This function call will be automatically tracked as a child
|
|
179
|
-
const data = await fetchData();
|
|
180
|
-
return transformData(data);
|
|
181
|
-
}, {
|
|
182
|
-
label: 'processData'
|
|
183
|
-
});
|
|
184
|
-
}
|
|
320
|
+
const query = tp.createTrackable(async (ms) => sleep(ms), { label: 'db.query' });
|
|
321
|
+
const handler = tp.createTrackable(async () => { await query(2); await query(3); }, { label: 'GET /users' });
|
|
185
322
|
|
|
186
|
-
|
|
187
|
-
return await traceperf.track(async () => {
|
|
188
|
-
// Implementation
|
|
189
|
-
}, { label: 'fetchData' });
|
|
190
|
-
}
|
|
323
|
+
for (let i = 0; i < 20; i++) await handler();
|
|
191
324
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
325
|
+
tp.printStats();
|
|
326
|
+
console.log(tp.getStats('db.query').p95); // ms
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
label calls errors total ms mean p50 p95 p99 max self mean
|
|
331
|
+
---------- ----- ------ -------- ----- ----- ----- ----- ----- ---------
|
|
332
|
+
GET /users 20 0 119.6 5.979 5.761 7.207 8.257 8.257 0.134
|
|
333
|
+
db.query 40 0 116.9 2.922 3.124 4.189 4.782 4.782 2.922
|
|
197
334
|
```
|
|
198
335
|
|
|
199
|
-
|
|
336
|
+
`count`, `errors`, `total`, `min` and `max` are exact. Percentiles come from a 512-sample reservoir per label. Past 1,000 distinct labels, new labels are folded into `(other)`. `resetStats()` starts a new window.
|
|
337
|
+
|
|
338
|
+
## Why was it slow? `traceperf explain`
|
|
339
|
+
|
|
340
|
+
`explain()` reads finished trees and says, in plain English, what probably made them slow:
|
|
200
341
|
|
|
201
342
|
```javascript
|
|
202
|
-
const { createTracePerf,
|
|
343
|
+
const { createTracePerf, explain, formatFindings } = require('traceperf');
|
|
344
|
+
const roots = [];
|
|
345
|
+
const tp = createTracePerf({ silent: true, onComplete: r => roots.push(r) });
|
|
346
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
347
|
+
const getUser = tp.createTrackable(() => sleep(20), { label: 'getUser' });
|
|
348
|
+
const getOrders = tp.createTrackable(() => sleep(30), { label: 'getOrders' });
|
|
349
|
+
const getPrice = tp.createTrackable(() => sleep(2), { label: 'pg SELECT prices' });
|
|
350
|
+
|
|
351
|
+
await tp.track(async () => {
|
|
352
|
+
await getUser();
|
|
353
|
+
await getOrders();
|
|
354
|
+
for (let i = 0; i < 10; i++) await getPrice();
|
|
355
|
+
}, { label: 'GET /dashboard' });
|
|
356
|
+
|
|
357
|
+
console.log(formatFindings(explain(roots)));
|
|
358
|
+
```
|
|
203
359
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
trackMemory: true,
|
|
209
|
-
threshold: 0 // track everything
|
|
210
|
-
});
|
|
360
|
+
```
|
|
361
|
+
⇉ GET /dashboard awaited 12 calls one after another (getUser, getOrders, pg SELECT prices, pg SELECT prices, ...: 78 ms); if they are independent, Promise.all could save up to ~47 ms
|
|
362
|
+
🔁 GET /dashboard called pg SELECT prices 10 times (27 ms total); batching them into one call may help (N+1 pattern)
|
|
363
|
+
```
|
|
211
364
|
|
|
212
|
-
|
|
213
|
-
const prodTracker = createTracePerf({
|
|
214
|
-
trackingMode: TrackingMode.PERFORMANCE,
|
|
215
|
-
silent: true, // don't log to console
|
|
216
|
-
trackMemory: false, // minimize overhead
|
|
217
|
-
threshold: 100, // only track slow functions
|
|
218
|
-
sampleRate: 0.01 // track only 1% of function calls
|
|
219
|
-
});
|
|
365
|
+
It looks for:
|
|
220
366
|
|
|
221
|
-
|
|
222
|
-
|
|
367
|
+
- **Blocked event loop:** "req2 waited ~60 ms while `cpu` in req1, req3 blocked the event loop". Synchronous work in one request delays every other request, and it names the function that did it.
|
|
368
|
+
- **Sequential awaits** that could run in parallel, with the most time `Promise.all` could save.
|
|
369
|
+
- **Repeated calls** under one parent (N+1 queries).
|
|
370
|
+
- **A dominant child**, or time spent outside any tracked call.
|
|
371
|
+
- **Errors**, reported once, where they happened.
|
|
372
|
+
|
|
373
|
+
The same works on saved data: `traceperf explain calls.jsonl` (or a Chrome trace, or `-` for stdin), with `--json`, `--max N`, `--blocking ms` and `--min ms`. Findings are heuristics with their numbers, so check them against the tree before acting on them.
|
|
374
|
+
|
|
375
|
+
## Getting data out
|
|
376
|
+
|
|
377
|
+
### Perfetto / Chrome trace
|
|
378
|
+
|
|
379
|
+
```javascript
|
|
380
|
+
const fs = require('fs');
|
|
381
|
+
const { createTracePerf } = require('traceperf');
|
|
382
|
+
const tp = createTracePerf({ silent: true });
|
|
383
|
+
|
|
384
|
+
const recording = tp.startTrace();
|
|
385
|
+
await tp.track(async () => {
|
|
386
|
+
await Promise.all([1, 2, 3].map(i => tp.track(() => new Promise(r => setTimeout(r, 10 * i)), { label: `job ${i}` })));
|
|
387
|
+
}, { label: 'batch' });
|
|
388
|
+
fs.writeFileSync('trace.json', JSON.stringify(recording.stop()));
|
|
223
389
|
```
|
|
224
390
|
|
|
225
|
-
|
|
391
|
+
Open `trace.json` in [ui.perfetto.dev](https://ui.perfetto.dev), `chrome://tracing`, or the DevTools Performance panel. Concurrent async calls are put on separate tracks, so the flame chart stays valid.
|
|
226
392
|
|
|
227
|
-
|
|
393
|
+
### OpenTelemetry
|
|
228
394
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
trackingMode: TrackingMode.BALANCED,
|
|
236
|
-
trackMemory: true
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// Now use it to track your functions
|
|
240
|
-
browserTracker.track(() => {
|
|
241
|
-
// DOM operations or other browser-side logic
|
|
242
|
-
}, { label: 'domOperation' });
|
|
243
|
-
</script>
|
|
395
|
+
<!-- docs:skip -->
|
|
396
|
+
```javascript
|
|
397
|
+
const otel = require('@opentelemetry/api');
|
|
398
|
+
const { createTracePerf, openTelemetryExporter } = require('traceperf');
|
|
399
|
+
|
|
400
|
+
const tp = createTracePerf({ silent: true, plugins: [openTelemetryExporter(otel)] });
|
|
244
401
|
```
|
|
245
402
|
|
|
246
|
-
|
|
403
|
+
Each finished tree becomes spans with the real start and end times, parent/child links, error status and `traceperf.*` attributes. Top-level spans are parented to the OpenTelemetry context that was active when the call started, so they join your existing request traces in Jaeger, Honeycomb, Datadog or Grafana. traceperf doesn't depend on `@opentelemetry/api`; you pass in the one your app uses.
|
|
404
|
+
|
|
405
|
+
**Live mode** (`openTelemetryExporter(otel, { live: true })`) opens each span when the call starts and makes it the active context while the call runs, across `await`. Spans created by your other instrumentation (pg, http, Redis) then appear inside traceperf's spans.
|
|
406
|
+
|
|
407
|
+
### Prometheus
|
|
247
408
|
|
|
248
409
|
```javascript
|
|
249
|
-
|
|
410
|
+
const { createTracePerf, prometheusExporter } = require('traceperf');
|
|
250
411
|
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
});
|
|
412
|
+
const prom = prometheusExporter();
|
|
413
|
+
const tp = createTracePerf({ silent: true, plugins: [prom] });
|
|
414
|
+
tp.track(() => JSON.stringify({ ok: true }), { label: 'serialize' });
|
|
254
415
|
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
416
|
+
// app.get('/metrics', prom.handler);
|
|
417
|
+
console.log(prom.metrics().split('\n').filter(l => l.includes('_count')).join('\n'));
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
traceperf_call_duration_seconds_count{name="serialize"} 1
|
|
259
422
|
```
|
|
260
423
|
|
|
261
|
-
|
|
424
|
+
This exposes a `traceperf_call_duration_seconds` histogram and a `traceperf_call_errors_total` counter per label. Use `rootsOnly: true` for request-level metrics only.
|
|
262
425
|
|
|
263
|
-
###
|
|
426
|
+
### Slow-call alerts
|
|
264
427
|
|
|
265
428
|
```javascript
|
|
266
|
-
const { createTracePerf
|
|
429
|
+
const { createTracePerf } = require('traceperf');
|
|
430
|
+
const tp = createTracePerf({ silent: true });
|
|
267
431
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
trackingMode: TrackingMode.PERFORMANCE
|
|
432
|
+
tp.onSlow({ 'db.query': 50, '*': 500 }, (call, root) => {
|
|
433
|
+
console.warn(`slow: ${call.label} took ${call.duration.toFixed(0)} ms (in ${root.label})`);
|
|
271
434
|
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
`onSlow` checks every call in each finished tree, nested ones included, in every mode.
|
|
272
438
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
439
|
+
### Hooks, JSON lines and custom output
|
|
440
|
+
|
|
441
|
+
```javascript
|
|
442
|
+
const { createTracePerf, jsonLinesExporter, formatTree } = require('traceperf');
|
|
443
|
+
|
|
444
|
+
const tp = createTracePerf({
|
|
445
|
+
silent: true,
|
|
446
|
+
plugins: [jsonLinesExporter(line => {})], // one JSON document per finished tree
|
|
447
|
+
onComplete: (root) => { // every finished top-level call
|
|
448
|
+
if (root.duration > 100) console.warn(`slow: ${root.label}`);
|
|
449
|
+
},
|
|
276
450
|
});
|
|
277
451
|
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
452
|
+
tp.use({ name: 'my-sink', onStart: (e) => {}, onComplete: (root) => {} }); // returns an unsubscribe function
|
|
453
|
+
|
|
454
|
+
// Replace console output entirely, e.g. to route it through your logger
|
|
455
|
+
const custom = createTracePerf({ reporter: (root) => console.log(formatTree(root)) });
|
|
456
|
+
custom.track(() => 1, { label: 'viaCustomReporter' });
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Records (`ExecutionRecord`) contain `label`, `duration`, `selfDuration`, `syncDuration`, `async`, `memoryDelta`, `error`, `args`, `startEpochMs` and `children`, all in milliseconds. A plugin that throws is reported once and never breaks your code.
|
|
460
|
+
|
|
461
|
+
## Frameworks
|
|
462
|
+
|
|
463
|
+
One tree per request, labelled by route pattern, with everything tracked during the request nested under it. Responses with status ≥ 500 and thrown errors mark the request as failed.
|
|
464
|
+
|
|
465
|
+
<!-- docs:skip -->
|
|
466
|
+
```javascript
|
|
467
|
+
const { createTracePerf, expressMiddleware, fastifyPlugin, honoMiddleware, koaMiddleware, nestInterceptor, withTracing } = require('traceperf');
|
|
468
|
+
const tp = createTracePerf({ threshold: 100 }); // print only slow requests
|
|
469
|
+
|
|
470
|
+
app.use(expressMiddleware(tp)); // Express / Connect
|
|
471
|
+
await fastify.register(fastifyPlugin(tp)); // Fastify
|
|
472
|
+
app.use('*', honoMiddleware(tp)); // Hono
|
|
473
|
+
app.use(koaMiddleware(tp)); // Koa
|
|
474
|
+
nestApp.useGlobalInterceptors(nestInterceptor(tp)); // NestJS
|
|
475
|
+
export const GET = withTracing(tp, async (req) => { // Next.js route handlers, any fetch-style handler
|
|
476
|
+
return Response.json(await loadUsers());
|
|
281
477
|
});
|
|
282
478
|
```
|
|
283
479
|
|
|
284
|
-
|
|
480
|
+
```
|
|
481
|
+
└─ GET /users/:id: 14.218 ms
|
|
482
|
+
├─ db.findUser: 6.021 ms
|
|
483
|
+
└─ db.findPosts: 7.902 ms
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Production
|
|
285
487
|
|
|
286
488
|
```javascript
|
|
287
|
-
const { createTracePerf } = require('traceperf');
|
|
288
|
-
|
|
489
|
+
const { createTracePerf, TrackingMode } = require('traceperf');
|
|
490
|
+
|
|
491
|
+
const tp = createTracePerf(process.env.NODE_ENV === 'production'
|
|
492
|
+
? { trackingMode: TrackingMode.PERFORMANCE, sampleRate: 0.1 } // stats and exporters, no console
|
|
493
|
+
: { trackingMode: TrackingMode.DETAILED });
|
|
494
|
+
|
|
495
|
+
tp.disable(); // kill switch: tracked functions become plain calls
|
|
496
|
+
tp.enable();
|
|
497
|
+
```
|
|
289
498
|
|
|
290
|
-
|
|
291
|
-
tracker.track(slowFunction, { threshold: 50 });
|
|
499
|
+
Overhead per call of a trivial function on Node 24 (`npm run bench`). CI fails the build if any of these regress past generous budgets. For a comparison with `performance.now()`, `timerify` and OpenTelemetry spans, see [docs/benchmarks.md](docs/benchmarks.md).
|
|
292
500
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
501
|
+
| Scenario | Overhead |
|
|
502
|
+
|---|---|
|
|
503
|
+
| `enabled: false` / sampled out | ~40–60 ns |
|
|
504
|
+
| `PERFORMANCE` mode without nesting | ~0 (the function is returned as is) |
|
|
505
|
+
| tracked, no memory sampling, no nesting | ~0.8 µs |
|
|
506
|
+
| tracked, no memory sampling, with nesting | ~1.4 µs |
|
|
507
|
+
| tracked with memory sampling and nesting (default) | ~3.5–4.5 µs |
|
|
508
|
+
|
|
509
|
+
That's fine for request handlers, database calls and anything else that takes milliseconds. For tight loops called millions of times, track the loop rather than each iteration, or use `sampleRate`.
|
|
510
|
+
|
|
511
|
+
Safe settings, what each feature costs and how to roll traceperf out to a service are in [docs/production.md](docs/production.md).
|
|
512
|
+
|
|
513
|
+
### Analyzing saved data: `traceperf report`
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
npx traceperf report trace.json # from startTrace().stop()
|
|
517
|
+
npx traceperf report calls.jsonl --top 5 --sort p95 # from jsonLinesExporter()
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
```
|
|
521
|
+
120 calls, 3 labels (sorted by total)
|
|
522
|
+
|
|
523
|
+
label calls errors total ms mean p50 p95 p99 max self mean
|
|
524
|
+
...
|
|
525
|
+
Slowest 5 calls:
|
|
526
|
+
95.212 ms GET /users > db.query
|
|
296
527
|
```
|
|
297
528
|
|
|
298
|
-
|
|
529
|
+
Add `--json` for machine-readable output. A file of `-` reads stdin.
|
|
530
|
+
|
|
531
|
+
### Comparing runs: `traceperf compare`
|
|
532
|
+
|
|
533
|
+
Record a JSON-lines log before and after a change (or on `main` and on a PR), then:
|
|
534
|
+
|
|
535
|
+
```bash
|
|
536
|
+
npx traceperf compare before.jsonl after.jsonl --metric p95 --threshold 10 --fail
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
```
|
|
540
|
+
label before p95 after p95 change status
|
|
541
|
+
---------- ---------- --------- ------ ---------
|
|
542
|
+
db.query 4.741 8.795 +85.5% regressed
|
|
543
|
+
render 1.886 1.966 +4.2% unchanged
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
`--fail` exits with code 1 when anything regressed, so it can gate CI. Changes under `--min-delta` (0.5 ms by default) are ignored, so tiny functions don't fail builds on noise.
|
|
547
|
+
|
|
548
|
+
### Reports and live view
|
|
549
|
+
|
|
550
|
+
```bash
|
|
551
|
+
npx traceperf report trace.json --html report.html # one self-contained file: flame chart, stats, slowest calls
|
|
552
|
+
npx traceperf watch calls.jsonl # live top-style view while the app writes the log
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Allocation profiling
|
|
556
|
+
|
|
557
|
+
The `📊` heap delta is a hint. For real numbers, profile the allocations of a function:
|
|
299
558
|
|
|
300
559
|
```javascript
|
|
301
|
-
const {
|
|
302
|
-
const tracker = createTracePerf({ trackMemory: true });
|
|
560
|
+
const { profileAllocations } = require('traceperf');
|
|
303
561
|
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
562
|
+
const { allocatedBytes, sites } = await profileAllocations(() => {
|
|
563
|
+
const rows = [];
|
|
564
|
+
for (let i = 0; i < 10000; i++) rows.push({ id: i, label: `row ${i}` });
|
|
565
|
+
return rows.length;
|
|
566
|
+
});
|
|
567
|
+
console.log(allocatedBytes > 0, typeof sites[0].functionName); // allocated bytes and the top allocation sites
|
|
310
568
|
```
|
|
311
569
|
|
|
312
|
-
|
|
570
|
+
```
|
|
571
|
+
true string
|
|
572
|
+
```
|
|
313
573
|
|
|
314
|
-
|
|
574
|
+
This uses V8's sampling heap profiler through `node:inspector` and counts objects that were collected again (on Node 20+, see `countsCollected`).
|
|
575
|
+
|
|
576
|
+
### CPU hotspots for slow calls
|
|
577
|
+
|
|
578
|
+
Tracking shows *which call* was slow; `profileSlowCalls` shows *which lines* were busy during it, including code you never wrapped. It runs the V8 sampling profiler and, for every top-level call over the threshold, reports the functions that used the CPU:
|
|
315
579
|
|
|
316
|
-
|
|
580
|
+
<!-- docs:skip -->
|
|
581
|
+
```javascript
|
|
582
|
+
const { createTracePerf, profileSlowCalls, formatHotspots } = require('traceperf');
|
|
583
|
+
const tp = createTracePerf();
|
|
584
|
+
tp.use(profileSlowCalls({
|
|
585
|
+
threshold: 200, // ms
|
|
586
|
+
onProfile: (call, profile) => console.log(`${call.label}\n${formatHotspots(profile)}`),
|
|
587
|
+
}));
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
```
|
|
591
|
+
POST /import: 438 ms
|
|
592
|
+
315.0 ms parseRows /app/src/csv.js:18
|
|
593
|
+
121.1 ms hashRow /app/src/hash.js:4
|
|
594
|
+
cpu 436.0 ms, idle 0.0 ms, gc 2.4 ms
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
`idle` is time spent waiting for I/O or timers, so a slow call with a high idle time is waiting on something rather than computing. Results arrive when the profiler window closes (`window`, default 2 s; `flush()` forces it). Samples are process-wide, so concurrent work during the call shows up too. Sampling costs a few percent of CPU: turn it on while investigating, or on some instances only. Node.js only; `profiler.available` is false elsewhere.
|
|
598
|
+
|
|
599
|
+
### Event-loop delay
|
|
600
|
+
|
|
601
|
+
```javascript
|
|
602
|
+
const { monitorEventLoop } = require('traceperf');
|
|
603
|
+
|
|
604
|
+
const loop = monitorEventLoop();
|
|
605
|
+
setTimeout(() => {
|
|
606
|
+
console.log(loop.stats()); // { min, max, mean, stddev, p50, p90, p99 } in ms
|
|
607
|
+
loop.stop();
|
|
608
|
+
}, 100);
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
A tracked call tells you how long *it* took. Event-loop delay tells you whether something blocked everything else. This is Node.js only; elsewhere `supported` is `false`.
|
|
612
|
+
|
|
613
|
+
## Browser
|
|
614
|
+
|
|
615
|
+
```javascript
|
|
616
|
+
import { createTracePerf } from 'traceperf/browser';
|
|
617
|
+
|
|
618
|
+
const tracker = createTracePerf({ threshold: 16 }); // only calls longer than a frame
|
|
619
|
+
|
|
620
|
+
tracker.track(() => {
|
|
621
|
+
document.getElementById('output').textContent = 'Updated';
|
|
622
|
+
}, { label: 'updateDOM' });
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
The browser entry shares the core with Node.js, so options, nesting, statistics, hooks and exporters all work the same. It adds `info`/`warn`/`error`/`debug`/`group` logging methods and prints in a browser-friendly format:
|
|
626
|
+
|
|
627
|
+
```
|
|
628
|
+
Execution Flow:
|
|
629
|
+
→ updateDOM (21.14ms)
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
Without a bundler, use the script-tag build (`dist/traceperf.browser.min.js`, about 9 KB gzipped), which defines a global `TracePerf`:
|
|
633
|
+
|
|
634
|
+
```html
|
|
635
|
+
<script src="https://unpkg.com/traceperf/dist/traceperf.browser.min.js"></script>
|
|
636
|
+
<script>
|
|
637
|
+
const { createTracePerf, TrackingMode } = TracePerf;
|
|
638
|
+
const browserTracker = createTracePerf({ trackingMode: TrackingMode.BALANCED });
|
|
639
|
+
|
|
640
|
+
browserTracker.track(() => {
|
|
641
|
+
document.getElementById('output').textContent = 'Updated';
|
|
642
|
+
}, { label: 'domOperation' });
|
|
643
|
+
</script>
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Memory deltas are shown only where the browser exposes `performance.memory` (Chromium).
|
|
647
|
+
|
|
648
|
+
### In Chrome DevTools
|
|
649
|
+
|
|
650
|
+
```javascript
|
|
651
|
+
import { createTracePerf, performanceTimeline } from 'traceperf/browser';
|
|
652
|
+
|
|
653
|
+
const tp = createTracePerf({ silent: true, plugins: [performanceTimeline()] });
|
|
654
|
+
tp.track(() => [3, 1, 2].sort(), { label: 'sortItems' });
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Every tracked call becomes a `performance.measure()` entry. In Chrome 129+ these appear on a **traceperf** track in the Performance panel, with failed calls in red and duration, self and sync time in the tooltip. Elsewhere (including Node.js) they appear as User Timing entries. Entries are cleared after emitting, so long-running pages don't accumulate them; DevTools recordings still capture them.
|
|
658
|
+
|
|
659
|
+
### Edge runtimes
|
|
660
|
+
|
|
661
|
+
Cloudflare Workers and Vercel Edge work as-is. Nesting across `await` uses `AsyncLocalStorage`, which traceperf detects automatically where it is a global (Next.js Edge, Workers with `nodejs_compat`). Anywhere else, pass it in:
|
|
662
|
+
|
|
663
|
+
<!-- docs:skip -->
|
|
664
|
+
```javascript
|
|
665
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
666
|
+
const tp = createTracePerf({ asyncLocalStorage: AsyncLocalStorage });
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Workers deliberately coarsen timers (time only advances on I/O), so durations there are only approximate.
|
|
317
670
|
|
|
318
|
-
##
|
|
671
|
+
## How it compares
|
|
319
672
|
|
|
320
|
-
|
|
673
|
+
| | Call tree | Async-aware nesting | Per-label stats | Production use | Export | Setup |
|
|
674
|
+
|---|---|---|---|---|---|---|
|
|
675
|
+
| `console.time` | no | no | no | no | no | none |
|
|
676
|
+
| `performance.timerify` (Node) | no | no | via observer | yes | no | some |
|
|
677
|
+
| clinic.js / 0x | sampled flame graph | n/a | no | no (offline profiling) | own UI | run app under the tool |
|
|
678
|
+
| OpenTelemetry SDK | yes (spans) | yes | via backend | yes | everywhere | SDK + exporter + collector |
|
|
679
|
+
| **traceperf** | yes | yes (Node) | yes | yes (sampling, kill switch) | Perfetto, OTel, Prometheus, JSON | none (`--require traceperf/auto`) or one import |
|
|
680
|
+
|
|
681
|
+
Use a sampling profiler (clinic.js, `--cpu-prof`) when you don't know where to look, or let `profileSlowCalls` run one only around slow calls. Use traceperf when you want to measure specific functions, requests or spans continuously, in development and in production. If you already run OpenTelemetry, traceperf can feed it. A longer, honest comparison is in [docs/comparison.md](docs/comparison.md).
|
|
682
|
+
|
|
683
|
+
## Compatibility
|
|
684
|
+
|
|
685
|
+
- **Node.js** 14+ (tested on 14, 16, 18, 20, 22 and 24), **Bun**, **Deno**, **Cloudflare Workers** (workerd) and **Vercel Edge** (edge-runtime). Nesting across `await` works on all of them. Also tested in current Chromium, Firefox and WebKit.
|
|
686
|
+
- **Module formats:** CommonJS (`require`), ESM (`import`), TypeScript (`node10`, `node16`/`nodenext` and `bundler` resolution), bundlers (esbuild, webpack, Vite, ...), and `<script>`.
|
|
687
|
+
- **Runtime dependencies:** none.
|
|
688
|
+
|
|
689
|
+
## Limitations
|
|
690
|
+
|
|
691
|
+
- In browsers (and ESM on Node < 20.16), nesting doesn't survive `await`, because there is no `AsyncLocalStorage`. On those Node versions, starting with `--import traceperf/auto` fixes it.
|
|
692
|
+
- The heap delta is the V8 heap difference across the call. Other concurrent work and GC affect it, so treat it as a hint, not an allocation profile.
|
|
693
|
+
- `traceperf/auto` tracks Node's `http`/`https` servers (which Express, Fastify, Koa, Nest and most frameworks use), not `http2` or uWebSockets.js servers. AWS SDK clients still need `instrumentAwsClient(tp, client)`.
|
|
694
|
+
- `explain()` only sees tracked calls: it can say a request was delayed by blocking code, but names the function only if that function is tracked. `profileSlowCalls` finds untracked hot code.
|
|
695
|
+
- In the default OpenTelemetry mode, spans are created when the top-level call finishes. Use `{ live: true }` if other instrumentation's spans should nest inside traceperf's.
|
|
696
|
+
- `fetch` instrumentation times the request until the response headers arrive, not the time spent reading the body.
|
|
697
|
+
|
|
698
|
+
## Examples
|
|
699
|
+
|
|
700
|
+
- `examples/playground/`: an Express app with a hidden N+1 query problem.
|
|
701
|
+
- `examples/optimized-tracking-example.js`: the basics.
|
|
702
|
+
- `examples/stats-and-export.js`: statistics, spans, a Perfetto trace and JSON lines in a production-style setup.
|
|
703
|
+
- `examples/browser-example.html` / `.js`: browser usage.
|
|
704
|
+
- `examples/test/edge-case-tests.js`: deep async nesting, memory churn, sampling, error propagation.
|
|
705
|
+
- `examples/test/final-benchmark.js`: overhead by tracking mode.
|
|
706
|
+
|
|
707
|
+
## Contributing
|
|
708
|
+
|
|
709
|
+
`npm ci && npm run build:all && npm test && npm run lint` covers the basics. CI also runs:
|
|
710
|
+
|
|
711
|
+
- `npm run verify:package`: installs the packed tarball into fresh CJS, ESM, TypeScript and bundler projects.
|
|
712
|
+
- `npm run verify:docs`: runs every example in this README and checks its output.
|
|
713
|
+
- `npm run test:browser`: Playwright tests in Chromium, Firefox and WebKit.
|
|
714
|
+
- `npm run bench:check`: overhead and bundle-size budgets.
|
|
715
|
+
- `npm run test:runtimes`: smoke tests on Bun and Deno.
|
|
716
|
+
- `npm run docs:api`: the API reference (TypeDoc).
|
|
717
|
+
|
|
718
|
+
What's planned is in [ROADMAP.md](ROADMAP.md).
|
|
719
|
+
|
|
720
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
721
|
+
|
|
722
|
+
## License
|
|
321
723
|
|
|
724
|
+
MIT
|