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.
Files changed (268) hide show
  1. package/README.md +612 -209
  2. package/dist/__tests__/helpers/console.d.ts +9 -0
  3. package/dist/__tests__/helpers/console.js +25 -0
  4. package/dist/__tests__/helpers/console.js.map +1 -0
  5. package/dist/auto.d.ts +30 -0
  6. package/dist/auto.js +91 -0
  7. package/dist/auto.js.map +1 -0
  8. package/dist/browser.d.ts +52 -144
  9. package/dist/browser.js +112 -439
  10. package/dist/browser.js.map +1 -1
  11. package/dist/cli.d.ts +68 -0
  12. package/dist/cli.js +528 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/core/allocations.d.ts +38 -0
  15. package/dist/core/allocations.js +85 -0
  16. package/dist/core/allocations.js.map +1 -0
  17. package/dist/core/cpu-profile.d.ts +84 -0
  18. package/dist/core/cpu-profile.js +181 -0
  19. package/dist/core/cpu-profile.js.map +1 -0
  20. package/dist/core/env.d.ts +10 -0
  21. package/dist/core/env.js +40 -0
  22. package/dist/core/env.js.map +1 -0
  23. package/dist/core/event-loop.d.ts +30 -0
  24. package/dist/core/event-loop.js +43 -0
  25. package/dist/core/event-loop.js.map +1 -0
  26. package/dist/core/explain.d.ts +48 -0
  27. package/dist/core/explain.js +236 -0
  28. package/dist/core/explain.js.map +1 -0
  29. package/dist/core/exporters.d.ts +111 -0
  30. package/dist/core/exporters.js +236 -0
  31. package/dist/core/exporters.js.map +1 -0
  32. package/dist/core/index.d.ts +9 -3
  33. package/dist/core/index.js +9 -3
  34. package/dist/core/index.js.map +1 -1
  35. package/dist/core/optimized-performance-monitor.d.ts +180 -26
  36. package/dist/core/optimized-performance-monitor.js +687 -175
  37. package/dist/core/optimized-performance-monitor.js.map +1 -1
  38. package/dist/core/prometheus.d.ts +36 -0
  39. package/dist/core/prometheus.js +93 -0
  40. package/dist/core/prometheus.js.map +1 -0
  41. package/dist/core/records.d.ts +97 -0
  42. package/dist/core/records.js +87 -0
  43. package/dist/core/records.js.map +1 -0
  44. package/dist/core/stats.d.ts +45 -0
  45. package/dist/core/stats.js +136 -0
  46. package/dist/core/stats.js.map +1 -0
  47. package/dist/core/timeline.d.ts +28 -0
  48. package/dist/core/timeline.js +65 -0
  49. package/dist/core/timeline.js.map +1 -0
  50. package/dist/drivers.d.ts +51 -0
  51. package/dist/drivers.js +205 -0
  52. package/dist/drivers.js.map +1 -0
  53. package/dist/esm/auto.d.ts +30 -0
  54. package/dist/esm/auto.js +87 -0
  55. package/dist/esm/auto.js.map +1 -0
  56. package/dist/esm/browser.d.ts +52 -144
  57. package/dist/esm/browser.js +101 -445
  58. package/dist/esm/browser.js.map +1 -1
  59. package/dist/esm/cli.d.ts +68 -0
  60. package/dist/esm/cli.js +520 -0
  61. package/dist/esm/cli.js.map +1 -0
  62. package/dist/esm/core/allocations.d.ts +38 -0
  63. package/dist/esm/core/allocations.js +82 -0
  64. package/dist/esm/core/allocations.js.map +1 -0
  65. package/dist/esm/core/cpu-profile.d.ts +84 -0
  66. package/dist/esm/core/cpu-profile.js +176 -0
  67. package/dist/esm/core/cpu-profile.js.map +1 -0
  68. package/dist/esm/core/env.d.ts +10 -0
  69. package/dist/esm/core/env.js +35 -0
  70. package/dist/esm/core/env.js.map +1 -0
  71. package/dist/esm/core/event-loop.d.ts +30 -0
  72. package/dist/esm/core/event-loop.js +40 -0
  73. package/dist/esm/core/event-loop.js.map +1 -0
  74. package/dist/esm/core/explain.d.ts +48 -0
  75. package/dist/esm/core/explain.js +232 -0
  76. package/dist/esm/core/explain.js.map +1 -0
  77. package/dist/esm/core/exporters.d.ts +111 -0
  78. package/dist/esm/core/exporters.js +229 -0
  79. package/dist/esm/core/exporters.js.map +1 -0
  80. package/dist/esm/core/index.d.ts +10 -4
  81. package/dist/esm/core/index.js +10 -4
  82. package/dist/esm/core/index.js.map +1 -1
  83. package/dist/esm/core/optimized-performance-monitor.d.ts +180 -26
  84. package/dist/esm/core/optimized-performance-monitor.js +685 -173
  85. package/dist/esm/core/optimized-performance-monitor.js.map +1 -1
  86. package/dist/esm/core/prometheus.d.ts +36 -0
  87. package/dist/esm/core/prometheus.js +89 -0
  88. package/dist/esm/core/prometheus.js.map +1 -0
  89. package/dist/esm/core/records.d.ts +97 -0
  90. package/dist/esm/core/records.js +82 -0
  91. package/dist/esm/core/records.js.map +1 -0
  92. package/dist/esm/core/stats.d.ts +45 -0
  93. package/dist/esm/core/stats.js +131 -0
  94. package/dist/esm/core/stats.js.map +1 -0
  95. package/dist/esm/core/timeline.d.ts +28 -0
  96. package/dist/esm/core/timeline.js +62 -0
  97. package/dist/esm/core/timeline.js.map +1 -0
  98. package/dist/esm/drivers.d.ts +51 -0
  99. package/dist/esm/drivers.js +197 -0
  100. package/dist/esm/drivers.js.map +1 -0
  101. package/dist/esm/index.d.ts +13 -5
  102. package/dist/esm/index.js +21 -17
  103. package/dist/esm/index.js.map +1 -1
  104. package/dist/esm/instrument.d.ts +88 -0
  105. package/dist/esm/instrument.js +274 -0
  106. package/dist/esm/instrument.js.map +1 -0
  107. package/dist/esm/integrations.d.ts +70 -0
  108. package/dist/esm/integrations.js +197 -0
  109. package/dist/esm/integrations.js.map +1 -0
  110. package/dist/esm/node-builtins.d.ts +1 -0
  111. package/dist/esm/node-builtins.js +32 -0
  112. package/dist/esm/node-builtins.js.map +1 -0
  113. package/dist/esm/package.json +3 -0
  114. package/dist/esm/register.d.ts +20 -0
  115. package/dist/esm/register.js +98 -0
  116. package/dist/esm/register.js.map +1 -0
  117. package/dist/esm/report-html.d.ts +11 -0
  118. package/dist/esm/report-html.js +106 -0
  119. package/dist/esm/report-html.js.map +1 -0
  120. package/dist/esm/types/index.d.ts +4 -80
  121. package/dist/esm/types/index.js +1 -2
  122. package/dist/esm/types/index.js.map +1 -1
  123. package/dist/esm/types/options.d.ts +72 -0
  124. package/dist/esm/types/options.js +2 -0
  125. package/dist/esm/types/options.js.map +1 -0
  126. package/dist/esm/watch.d.ts +17 -0
  127. package/dist/esm/watch.js +87 -0
  128. package/dist/esm/watch.js.map +1 -0
  129. package/dist/index.d.ts +10 -2
  130. package/dist/index.js +26 -29
  131. package/dist/index.js.map +1 -1
  132. package/dist/instrument.d.ts +88 -0
  133. package/dist/instrument.js +282 -0
  134. package/dist/instrument.js.map +1 -0
  135. package/dist/integrations.d.ts +70 -0
  136. package/dist/integrations.js +205 -0
  137. package/dist/integrations.js.map +1 -0
  138. package/dist/node-builtins.d.ts +1 -0
  139. package/dist/node-builtins.js +67 -0
  140. package/dist/node-builtins.js.map +1 -0
  141. package/dist/register.d.ts +20 -0
  142. package/dist/register.js +103 -0
  143. package/dist/register.js.map +1 -0
  144. package/dist/report-html.d.ts +11 -0
  145. package/dist/report-html.js +109 -0
  146. package/dist/report-html.js.map +1 -0
  147. package/dist/traceperf.browser.js +1910 -0
  148. package/dist/traceperf.browser.min.js +6 -0
  149. package/dist/types/index.d.ts +4 -80
  150. package/dist/types/index.js +1 -2
  151. package/dist/types/index.js.map +1 -1
  152. package/dist/types/options.d.ts +72 -0
  153. package/dist/types/{config.js → options.js} +1 -1
  154. package/dist/types/options.js.map +1 -0
  155. package/dist/watch.d.ts +17 -0
  156. package/dist/watch.js +90 -0
  157. package/dist/watch.js.map +1 -0
  158. package/package.json +99 -19
  159. package/dist/core/config.d.ts +0 -77
  160. package/dist/core/config.js +0 -116
  161. package/dist/core/config.js.map +0 -1
  162. package/dist/core/constants.d.ts +0 -78
  163. package/dist/core/constants.js +0 -109
  164. package/dist/core/constants.js.map +0 -1
  165. package/dist/core/logger.d.ts +0 -97
  166. package/dist/core/logger.js +0 -186
  167. package/dist/core/logger.js.map +0 -1
  168. package/dist/esm/core/config.d.ts +0 -77
  169. package/dist/esm/core/config.js +0 -112
  170. package/dist/esm/core/config.js.map +0 -1
  171. package/dist/esm/core/constants.d.ts +0 -78
  172. package/dist/esm/core/constants.js +0 -106
  173. package/dist/esm/core/constants.js.map +0 -1
  174. package/dist/esm/core/logger.d.ts +0 -97
  175. package/dist/esm/core/logger.js +0 -182
  176. package/dist/esm/core/logger.js.map +0 -1
  177. package/dist/esm/formatters/ascii.d.ts +0 -52
  178. package/dist/esm/formatters/ascii.js +0 -100
  179. package/dist/esm/formatters/ascii.js.map +0 -1
  180. package/dist/esm/formatters/cli.d.ts +0 -50
  181. package/dist/esm/formatters/cli.js +0 -96
  182. package/dist/esm/formatters/cli.js.map +0 -1
  183. package/dist/esm/formatters/index.d.ts +0 -3
  184. package/dist/esm/formatters/index.js +0 -4
  185. package/dist/esm/formatters/index.js.map +0 -1
  186. package/dist/esm/formatters/json.d.ts +0 -45
  187. package/dist/esm/formatters/json.js +0 -85
  188. package/dist/esm/formatters/json.js.map +0 -1
  189. package/dist/esm/trackers/execution.d.ts +0 -101
  190. package/dist/esm/trackers/execution.js +0 -226
  191. package/dist/esm/trackers/execution.js.map +0 -1
  192. package/dist/esm/trackers/index.d.ts +0 -2
  193. package/dist/esm/trackers/index.js +0 -3
  194. package/dist/esm/trackers/index.js.map +0 -1
  195. package/dist/esm/trackers/performance.d.ts +0 -68
  196. package/dist/esm/trackers/performance.js +0 -112
  197. package/dist/esm/trackers/performance.js.map +0 -1
  198. package/dist/esm/types/config.d.ts +0 -142
  199. package/dist/esm/types/config.js +0 -2
  200. package/dist/esm/types/config.js.map +0 -1
  201. package/dist/esm/types/logger.d.ts +0 -147
  202. package/dist/esm/types/logger.js +0 -2
  203. package/dist/esm/types/logger.js.map +0 -1
  204. package/dist/esm/utils/colors.d.ts +0 -87
  205. package/dist/esm/utils/colors.js +0 -146
  206. package/dist/esm/utils/colors.js.map +0 -1
  207. package/dist/esm/utils/index.d.ts +0 -3
  208. package/dist/esm/utils/index.js +0 -4
  209. package/dist/esm/utils/index.js.map +0 -1
  210. package/dist/esm/utils/proxy-tracker.d.ts +0 -68
  211. package/dist/esm/utils/proxy-tracker.js +0 -127
  212. package/dist/esm/utils/proxy-tracker.js.map +0 -1
  213. package/dist/esm/utils/stack.d.ts +0 -63
  214. package/dist/esm/utils/stack.js +0 -110
  215. package/dist/esm/utils/stack.js.map +0 -1
  216. package/dist/esm/utils/string.d.ts +0 -29
  217. package/dist/esm/utils/string.js +0 -45
  218. package/dist/esm/utils/string.js.map +0 -1
  219. package/dist/esm/utils/timing.d.ts +0 -65
  220. package/dist/esm/utils/timing.js +0 -99
  221. package/dist/esm/utils/timing.js.map +0 -1
  222. package/dist/formatters/ascii.d.ts +0 -52
  223. package/dist/formatters/ascii.js +0 -104
  224. package/dist/formatters/ascii.js.map +0 -1
  225. package/dist/formatters/cli.d.ts +0 -50
  226. package/dist/formatters/cli.js +0 -100
  227. package/dist/formatters/cli.js.map +0 -1
  228. package/dist/formatters/index.d.ts +0 -3
  229. package/dist/formatters/index.js +0 -20
  230. package/dist/formatters/index.js.map +0 -1
  231. package/dist/formatters/json.d.ts +0 -45
  232. package/dist/formatters/json.js +0 -89
  233. package/dist/formatters/json.js.map +0 -1
  234. package/dist/trackers/execution.d.ts +0 -101
  235. package/dist/trackers/execution.js +0 -230
  236. package/dist/trackers/execution.js.map +0 -1
  237. package/dist/trackers/index.d.ts +0 -2
  238. package/dist/trackers/index.js +0 -19
  239. package/dist/trackers/index.js.map +0 -1
  240. package/dist/trackers/performance.d.ts +0 -68
  241. package/dist/trackers/performance.js +0 -116
  242. package/dist/trackers/performance.js.map +0 -1
  243. package/dist/types/config.d.ts +0 -142
  244. package/dist/types/config.js.map +0 -1
  245. package/dist/types/logger.d.ts +0 -147
  246. package/dist/types/logger.js +0 -3
  247. package/dist/types/logger.js.map +0 -1
  248. package/dist/types.d.ts +0 -65
  249. package/dist/types.js +0 -6
  250. package/dist/types.js.map +0 -1
  251. package/dist/utils/colors.d.ts +0 -87
  252. package/dist/utils/colors.js +0 -160
  253. package/dist/utils/colors.js.map +0 -1
  254. package/dist/utils/index.d.ts +0 -3
  255. package/dist/utils/index.js +0 -20
  256. package/dist/utils/index.js.map +0 -1
  257. package/dist/utils/proxy-tracker.d.ts +0 -68
  258. package/dist/utils/proxy-tracker.js +0 -131
  259. package/dist/utils/proxy-tracker.js.map +0 -1
  260. package/dist/utils/stack.d.ts +0 -63
  261. package/dist/utils/stack.js +0 -116
  262. package/dist/utils/stack.js.map +0 -1
  263. package/dist/utils/string.d.ts +0 -29
  264. package/dist/utils/string.js +0 -50
  265. package/dist/utils/string.js.map +0 -1
  266. package/dist/utils/timing.d.ts +0 -65
  267. package/dist/utils/timing.js +0 -106
  268. package/dist/utils/timing.js.map +0 -1
package/README.md CHANGED
@@ -1,321 +1,724 @@
1
1
  # TracePerf
2
2
 
3
- Advanced performance tracking and execution monitoring for Node.js applications.
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
  [![npm version](https://img.shields.io/npm/v/traceperf.svg)](https://www.npmjs.com/package/traceperf)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- ## Features
8
+ ![traceperf output: a request's call tree with self and sync time, a blocked-event-loop warning and a stats table](docs/assets/terminal.svg)
9
9
 
10
- - **High-Performance Execution Monitoring**: Track function execution time with minimal overhead
11
- - **Memory Usage Tracking**: Monitor memory consumption of your functions and identify memory leaks
12
- - **Flexible Tracking Modes**: Choose between performance, balanced, and detailed tracking based on your needs
13
- - **Execution Flow Visualization**: Visualize execution flows with intelligent formatting
14
- - **Nested Function Tracking**: Track function calls within other functions to understand complex flows
15
- - **Customizable Threshold Detection**: Focus on functions that exceed specific execution time thresholds
16
- - **Sampling Control**: Adjust sampling rates to minimize performance impact in production
17
- - **Universal Module Compatibility**: Works with both CommonJS and ESM environments
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
- ## Installation
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 Start
30
+ ## Quick start
26
31
 
27
32
  ```javascript
28
- const traceperf = require('traceperf');
29
-
30
- // Track a synchronous function
31
- const result = traceperf.track(() => {
32
- // Your code here
33
- return 'result';
34
- }, { label: 'myFunction' });
35
-
36
- // Track an asynchronous function
37
- async function main() {
38
- const result = await traceperf.track(async () => {
39
- // Your async code here
40
- return 'async result';
41
- }, { label: 'asyncFunction' });
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
- ## Tracking Modes
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
- TracePerf provides different tracking modes to balance performance with detail:
56
+ ES modules work the same way:
48
57
 
49
58
  ```javascript
50
- const { TrackingMode, createTracePerf } = require('traceperf');
59
+ import tp, { createTracePerf, TrackingMode } from 'traceperf';
51
60
 
52
- // Create a custom instance with performance-focused tracking
53
- const performanceTracker = createTracePerf({
54
- trackingMode: TrackingMode.PERFORMANCE,
55
- silent: true,
56
- threshold: 100, // only track functions that take more than 100ms
57
- sampleRate: 0.1 // only track 10% of function calls
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
- ## Tracking Individual Functions
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
- // Track a single function execution
72
- const result = traceperf.track(() => {
73
- // Function body
74
- return someValue;
75
- }, {
76
- label: 'functionName',
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
- // Create a trackable version of an existing function
82
- const myFunction = (a, b) => a + b;
83
- const trackedFunction = traceperf.createTrackable(myFunction, {
84
- label: 'addition'
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
- // Now use it normally
88
- const sum = trackedFunction(5, 3); // will be tracked
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
- ## Tracking Methods in a Module
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 userService = {
95
- getUser: async (id) => { /* implementation */ },
96
- updateUser: async (id, data) => { /* implementation */ },
97
- deleteUser: async (id) => { /* implementation */ }
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
- // Register all methods for tracking
101
- const trackedUserService = traceperf.registerModule(userService);
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
- // Now all method calls will be tracked
104
- const user = await trackedUserService.getUser(123);
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
- ## Advanced Configuration
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 customTracker = createTracePerf({
113
- // Tracking mode affects detail level and performance impact
114
- trackingMode: TrackingMode.BALANCED,
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
- // Minimum execution time to track (milliseconds)
126
- threshold: 50,
127
-
128
- // Percentage of function calls to track (0.0 to 1.0)
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
- ## Browser Support
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
- TracePerf includes a browser-compatible version:
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
- import traceperf from 'traceperf/browser';
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
- // Use the same API as in Node.js
141
- traceperf.track(() => {
142
- // Your code
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
- ## Examples
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
- TracePerf includes several example files to help you get started:
280
+ ### Thresholds
149
281
 
150
- - **`examples/optimized-tracking-example.js`**: Demonstrates various ways to use the optimized tracking implementation
151
- - **`examples/browser-example.js`**: Shows how to use TracePerf in browser environments
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
- ### Tracking an Async Function
295
+ ### Logging arguments
154
296
 
155
297
  ```javascript
156
298
  const { createTracePerf } = require('traceperf');
157
- const traceperf = createTracePerf();
299
+ const tp = createTracePerf({ trackMemory: false });
158
300
 
159
- async function fetchData(url) {
160
- return traceperf.track(async () => {
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
- ### Tracking Nested Functions
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 traceperf = createTracePerf({ enableNestedTracking: true });
317
+ const tp = createTracePerf({ silent: true });
318
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
175
319
 
176
- async function processData() {
177
- return await traceperf.track(async () => {
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
- async function fetchData() {
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
- function transformData(data) {
193
- return traceperf.track(() => {
194
- // Implementation
195
- }, { label: 'transformData' });
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
- ### Optimizing for Production
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, TrackingMode } = require('traceperf');
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
- // Development environment
205
- const devTracker = createTracePerf({
206
- trackingMode: TrackingMode.DETAILED,
207
- silent: false,
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
- // Production environment
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
- // Use based on environment
222
- const traceperf = process.env.NODE_ENV === 'production' ? prodTracker : devTracker;
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
- ## Browser Usage
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
- To use TracePerf in the browser:
393
+ ### OpenTelemetry
228
394
 
229
- ```html
230
- <script src="dist/traceperf.browser.js"></script>
231
- <script>
232
- const { createTracePerf, TrackingMode } = TracePerf;
233
-
234
- const browserTracker = createTracePerf({
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
- Or with ES modules:
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
- import { createTracePerf, TrackingMode } from 'traceperf/browser';
410
+ const { createTracePerf, prometheusExporter } = require('traceperf');
250
411
 
251
- const browserTracker = createTracePerf({
252
- trackingMode: TrackingMode.BALANCED
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
- // Track a DOM operation
256
- browserTracker.track(() => {
257
- document.getElementById('output').textContent = 'Updated';
258
- }, { label: 'updateDOM' });
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
- ## Advanced Tracking Features
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
- ### Tracking with Different Modes
426
+ ### Slow-call alerts
264
427
 
265
428
  ```javascript
266
- const { createTracePerf, TrackingMode } = require('traceperf');
429
+ const { createTracePerf } = require('traceperf');
430
+ const tp = createTracePerf({ silent: true });
267
431
 
268
- // Performance mode - minimal overhead
269
- const performanceTracker = createTracePerf({
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
- // Balanced mode - moderate detail with reasonable overhead
274
- const balancedTracker = createTracePerf({
275
- trackingMode: TrackingMode.BALANCED
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
- // Detailed mode - maximum information
279
- const detailedTracker = createTracePerf({
280
- trackingMode: TrackingMode.DETAILED
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
- ### Using Thresholds to Filter Results
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
- const tracker = createTracePerf();
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
- // Only log functions that take more than 50ms
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
- // Different thresholds for different functions
294
- tracker.track(criticalFunction, { threshold: 10 }); // Low threshold for critical paths
295
- tracker.track(backgroundTask, { threshold: 200 }); // Higher threshold for background tasks
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
- ### Memory Tracking
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 { createTracePerf } = require('traceperf');
302
- const tracker = createTracePerf({ trackMemory: true });
560
+ const { profileAllocations } = require('traceperf');
303
561
 
304
- // Track memory usage for memory-intensive operations
305
- const result = tracker.track(() => {
306
- // Memory-intensive operations
307
- const largeArray = new Array(1000000).fill(Math.random());
308
- return processArray(largeArray);
309
- }, { label: 'memoryIntensiveOperation' });
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
- For more examples, see the example files in the `examples/` directory.
570
+ ```
571
+ true string
572
+ ```
313
573
 
314
- ## License
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
- MIT
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
- ## Website
671
+ ## How it compares
319
672
 
320
- Visit our website at [traceperf.com](https://traceperf.com) for more information, interactive examples, and detailed documentation.
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