traceperf 0.2.1 → 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 +608 -253
- 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,369 +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
|
-
|
|
78
|
-
|
|
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);
|
|
107
|
+
|
|
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"
|
|
80
118
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
const sum = trackedFunction(5, 3); // will be tracked
|
|
158
|
+
await tp.track(async () => { await new Db().query('SELECT * FROM users'); }, { label: 'handler' });
|
|
89
159
|
```
|
|
90
160
|
|
|
91
|
-
|
|
161
|
+
```
|
|
162
|
+
└─ handler: 0.112 ms
|
|
163
|
+
└─ db SELECT: 0.031 ms
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Ready-made helpers for popular clients. Pass the module you already use; traceperf doesn't depend on any of them:
|
|
167
|
+
|
|
168
|
+
<!-- docs:skip -->
|
|
169
|
+
```javascript
|
|
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:
|
|
92
188
|
|
|
93
189
|
```javascript
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
190
|
+
const { createTracePerf } = require('traceperf');
|
|
191
|
+
const traceperf = createTracePerf({ trackMemory: false });
|
|
192
|
+
|
|
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
|
+
}
|
|
203
|
+
|
|
204
|
+
async function processData() {
|
|
205
|
+
return traceperf.track(async () => transformData(await fetchData()), { label: 'processData' });
|
|
206
|
+
}
|
|
99
207
|
|
|
100
|
-
|
|
101
|
-
|
|
208
|
+
processData();
|
|
209
|
+
```
|
|
102
210
|
|
|
103
|
-
// Now all method calls will be tracked
|
|
104
|
-
const user = await trackedUserService.getUser(123);
|
|
105
211
|
```
|
|
212
|
+
└─ processData: 11.912 ms
|
|
213
|
+
├─ fetchData: 11.217 ms
|
|
214
|
+
└─ transformData: 0.075 ms
|
|
215
|
+
```
|
|
216
|
+
|
|
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.
|
|
106
218
|
|
|
107
|
-
##
|
|
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.
|
|
134
238
|
|
|
135
|
-
|
|
239
|
+
Options are strictly typed: `{ treshold: 50 }` doesn't compile.
|
|
240
|
+
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
```
|
|
147
269
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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)
|
|
275
|
+
```
|
|
152
276
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// Function implementation
|
|
156
|
-
}, { label: 'trackedBrowserFn' });
|
|
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.
|
|
157
279
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
280
|
+
### Thresholds
|
|
281
|
+
|
|
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
|
|
163
289
|
```
|
|
164
290
|
|
|
165
|
-
|
|
291
|
+
```
|
|
292
|
+
└─ slowFunction: 60.950 ms 📊 +144.98 KB
|
|
293
|
+
```
|
|
166
294
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
trackMemory: true
|
|
176
|
-
})
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Use the same API as in Node.js
|
|
180
|
-
browserTracker.track(() => {
|
|
181
|
-
// DOM operations or other browser-side logic
|
|
182
|
-
}, { label: 'domOperation' });
|
|
183
|
-
</script>
|
|
295
|
+
### Logging arguments
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
const { createTracePerf } = require('traceperf');
|
|
299
|
+
const tp = createTracePerf({ trackMemory: false });
|
|
300
|
+
|
|
301
|
+
const getUser = tp.createTrackable((id, opts) => ({ id }), { label: 'getUser', captureArgs: true });
|
|
302
|
+
getUser(123, { cache: true });
|
|
184
303
|
```
|
|
185
304
|
|
|
186
|
-
|
|
305
|
+
```
|
|
306
|
+
└─ getUser(123, {"cache":true}): 0.051 ms
|
|
307
|
+
```
|
|
187
308
|
|
|
188
|
-
|
|
309
|
+
Arguments are rendered safely and kept short. Circular, huge or exotic values never throw and never get walked in full.
|
|
189
310
|
|
|
190
|
-
|
|
191
|
-
- **Consistent Configuration**: Logger and tracker options are synchronized
|
|
192
|
-
- **Memory Tracking**: Both environments support memory usage tracking
|
|
193
|
-
- **Performance Optimization**: Browser-specific optimizations while maintaining API compatibility
|
|
194
|
-
- **Execution Flow**: Track complex execution flows consistently across environments
|
|
311
|
+
## Statistics
|
|
195
312
|
|
|
196
|
-
|
|
313
|
+
Every tracked call (including in `PERFORMANCE` mode and when silent) feeds per-label statistics with bounded memory:
|
|
197
314
|
|
|
198
315
|
```javascript
|
|
199
|
-
// Node.js
|
|
200
316
|
const { createTracePerf } = require('traceperf');
|
|
201
|
-
const
|
|
317
|
+
const tp = createTracePerf({ silent: true });
|
|
318
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
202
319
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const result = await someAsyncOperation();
|
|
211
|
-
return processResult(result);
|
|
212
|
-
}, {
|
|
213
|
-
label: 'mainOperation',
|
|
214
|
-
trackMemory: true
|
|
215
|
-
});
|
|
216
|
-
}
|
|
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' });
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < 20; i++) await handler();
|
|
324
|
+
|
|
325
|
+
tp.printStats();
|
|
326
|
+
console.log(tp.getStats('db.query').p95); // ms
|
|
217
327
|
```
|
|
218
328
|
|
|
219
|
-
|
|
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
|
|
334
|
+
```
|
|
220
335
|
|
|
221
|
-
|
|
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.
|
|
222
337
|
|
|
223
|
-
|
|
224
|
-
- **`examples/browser-example.js`**: Shows how to use TracePerf in browser environments
|
|
338
|
+
## Why was it slow? `traceperf explain`
|
|
225
339
|
|
|
226
|
-
|
|
340
|
+
`explain()` reads finished trees and says, in plain English, what probably made them slow:
|
|
227
341
|
|
|
228
342
|
```javascript
|
|
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
|
+
```
|
|
359
|
+
|
|
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
|
+
```
|
|
364
|
+
|
|
365
|
+
It looks for:
|
|
366
|
+
|
|
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');
|
|
229
381
|
const { createTracePerf } = require('traceperf');
|
|
230
|
-
const
|
|
382
|
+
const tp = createTracePerf({ silent: true });
|
|
231
383
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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()));
|
|
389
|
+
```
|
|
390
|
+
|
|
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.
|
|
392
|
+
|
|
393
|
+
### OpenTelemetry
|
|
394
|
+
|
|
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)] });
|
|
401
|
+
```
|
|
402
|
+
|
|
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
|
|
408
|
+
|
|
409
|
+
```javascript
|
|
410
|
+
const { createTracePerf, prometheusExporter } = require('traceperf');
|
|
411
|
+
|
|
412
|
+
const prom = prometheusExporter();
|
|
413
|
+
const tp = createTracePerf({ silent: true, plugins: [prom] });
|
|
414
|
+
tp.track(() => JSON.stringify({ ok: true }), { label: 'serialize' });
|
|
415
|
+
|
|
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
|
|
241
422
|
```
|
|
242
423
|
|
|
243
|
-
|
|
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.
|
|
425
|
+
|
|
426
|
+
### Slow-call alerts
|
|
244
427
|
|
|
245
428
|
```javascript
|
|
246
429
|
const { createTracePerf } = require('traceperf');
|
|
247
|
-
const
|
|
430
|
+
const tp = createTracePerf({ silent: true });
|
|
248
431
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return transformData(data);
|
|
254
|
-
}, {
|
|
255
|
-
label: 'processData'
|
|
256
|
-
});
|
|
257
|
-
}
|
|
432
|
+
tp.onSlow({ 'db.query': 50, '*': 500 }, (call, root) => {
|
|
433
|
+
console.warn(`slow: ${call.label} took ${call.duration.toFixed(0)} ms (in ${root.label})`);
|
|
434
|
+
});
|
|
435
|
+
```
|
|
258
436
|
|
|
259
|
-
|
|
260
|
-
return await traceperf.track(async () => {
|
|
261
|
-
// Implementation
|
|
262
|
-
}, { label: 'fetchData' });
|
|
263
|
-
}
|
|
437
|
+
`onSlow` checks every call in each finished tree, nested ones included, in every mode.
|
|
264
438
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
},
|
|
450
|
+
});
|
|
451
|
+
|
|
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());
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
```
|
|
481
|
+
└─ GET /users/:id: 14.218 ms
|
|
482
|
+
├─ db.findUser: 6.021 ms
|
|
483
|
+
└─ db.findPosts: 7.902 ms
|
|
270
484
|
```
|
|
271
485
|
|
|
272
|
-
|
|
486
|
+
## Production
|
|
273
487
|
|
|
274
488
|
```javascript
|
|
275
489
|
const { createTracePerf, TrackingMode } = require('traceperf');
|
|
276
490
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
trackingMode: TrackingMode.DETAILED
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
```
|
|
498
|
+
|
|
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).
|
|
500
|
+
|
|
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
|
|
527
|
+
```
|
|
528
|
+
|
|
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:
|
|
284
558
|
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
559
|
+
```javascript
|
|
560
|
+
const { profileAllocations } = require('traceperf');
|
|
561
|
+
|
|
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;
|
|
292
566
|
});
|
|
567
|
+
console.log(allocatedBytes > 0, typeof sites[0].functionName); // allocated bytes and the top allocation sites
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
true string
|
|
572
|
+
```
|
|
573
|
+
|
|
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:
|
|
579
|
+
|
|
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
|
|
293
619
|
|
|
294
|
-
|
|
295
|
-
|
|
620
|
+
tracker.track(() => {
|
|
621
|
+
document.getElementById('output').textContent = 'Updated';
|
|
622
|
+
}, { label: 'updateDOM' });
|
|
296
623
|
```
|
|
297
624
|
|
|
298
|
-
|
|
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
|
+
```
|
|
299
631
|
|
|
300
|
-
|
|
632
|
+
Without a bundler, use the script-tag build (`dist/traceperf.browser.min.js`, about 9 KB gzipped), which defines a global `TracePerf`:
|
|
301
633
|
|
|
302
634
|
```html
|
|
303
|
-
<script src="dist/traceperf.browser.js"></script>
|
|
635
|
+
<script src="https://unpkg.com/traceperf/dist/traceperf.browser.min.js"></script>
|
|
304
636
|
<script>
|
|
305
637
|
const { createTracePerf, TrackingMode } = TracePerf;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
trackingMode: TrackingMode.BALANCED,
|
|
309
|
-
trackMemory: true
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// Now use it to track your functions
|
|
638
|
+
const browserTracker = createTracePerf({ trackingMode: TrackingMode.BALANCED });
|
|
639
|
+
|
|
313
640
|
browserTracker.track(() => {
|
|
314
|
-
|
|
641
|
+
document.getElementById('output').textContent = 'Updated';
|
|
315
642
|
}, { label: 'domOperation' });
|
|
316
643
|
</script>
|
|
317
644
|
```
|
|
318
645
|
|
|
319
|
-
|
|
646
|
+
Memory deltas are shown only where the browser exposes `performance.memory` (Chromium).
|
|
320
647
|
|
|
321
|
-
|
|
322
|
-
import { createTracePerf, TrackingMode } from 'traceperf/browser';
|
|
648
|
+
### In Chrome DevTools
|
|
323
649
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
});
|
|
650
|
+
```javascript
|
|
651
|
+
import { createTracePerf, performanceTimeline } from 'traceperf/browser';
|
|
327
652
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
document.getElementById('output').textContent = 'Updated';
|
|
331
|
-
}, { label: 'updateDOM' });
|
|
653
|
+
const tp = createTracePerf({ silent: true, plugins: [performanceTimeline()] });
|
|
654
|
+
tp.track(() => [3, 1, 2].sort(), { label: 'sortItems' });
|
|
332
655
|
```
|
|
333
656
|
|
|
334
|
-
|
|
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
|
|
335
660
|
|
|
336
|
-
|
|
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:
|
|
337
662
|
|
|
663
|
+
<!-- docs:skip -->
|
|
338
664
|
```javascript
|
|
339
|
-
|
|
665
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
666
|
+
const tp = createTracePerf({ asyncLocalStorage: AsyncLocalStorage });
|
|
667
|
+
```
|
|
340
668
|
|
|
341
|
-
|
|
342
|
-
const performanceTracker = createTracePerf({
|
|
343
|
-
trackingMode: TrackingMode.PERFORMANCE
|
|
344
|
-
});
|
|
669
|
+
Workers deliberately coarsen timers (time only advances on I/O), so durations there are only approximate.
|
|
345
670
|
|
|
346
|
-
|
|
347
|
-
const balancedTracker = createTracePerf({
|
|
348
|
-
trackingMode: TrackingMode.BALANCED
|
|
349
|
-
});
|
|
671
|
+
## How it compares
|
|
350
672
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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 |
|
|
356
680
|
|
|
357
|
-
|
|
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).
|
|
358
682
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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).
|
|
362
721
|
|
|
363
|
-
|
|
364
|
-
tracker.track(slowFunction, { threshold: 50 });
|
|
722
|
+
## License
|
|
365
723
|
|
|
366
|
-
|
|
367
|
-
tracker.track(criticalFunction, { threshold: 10 }); // Low threshold for critical paths
|
|
368
|
-
tracker.track(backgroundTask, { threshold: 200 }); // Higher threshold for background tasks
|
|
369
|
-
```
|
|
724
|
+
MIT
|