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.
Files changed (268) hide show
  1. package/README.md +608 -253
  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,369 +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);
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
- // Create a trackable version of an existing function
82
- const myFunction = (a, b) => a + b;
83
- const trackedFunction = traceperf.createTrackable(myFunction, {
84
- label: 'addition'
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' });
89
159
  ```
90
160
 
91
- ## Tracking Methods in a Module
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 userService = {
95
- getUser: async (id) => { /* implementation */ },
96
- updateUser: async (id, data) => { /* implementation */ },
97
- deleteUser: async (id) => { /* implementation */ }
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
- // Register all methods for tracking
101
- const trackedUserService = traceperf.registerModule(userService);
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
- ## Advanced Configuration
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.
134
238
 
135
- TracePerf provides a fully synchronized browser implementation that matches the Node.js API:
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
- import { createTracePerf, BrowserLogger } from 'traceperf/browser';
139
-
140
- // Create a browser-optimized instance
141
- const browserTracker = createTracePerf({
142
- logger: new BrowserLogger({
143
- silent: false,
144
- trackMemory: true
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
- // Track function execution
149
- browserTracker.track(() => {
150
- // Your browser-side code
151
- }, { label: 'browserOperation' });
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
- // Create trackable functions
154
- const trackedFn = browserTracker.createTrackable(() => {
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
- // Track async operations
159
- await browserTracker.track(async () => {
160
- const response = await fetch('/api/data');
161
- return response.json();
162
- }, { label: 'fetchData' });
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
- You can also use it via script tag:
291
+ ```
292
+ └─ slowFunction: 60.950 ms 📊 +144.98 KB
293
+ ```
166
294
 
167
- ```html
168
- <script src="dist/traceperf.browser.js"></script>
169
- <script>
170
- const { createTracePerf, BrowserLogger } = TracePerf;
171
-
172
- const browserTracker = createTracePerf({
173
- logger: new BrowserLogger({
174
- silent: false,
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
- ## Synchronized Features
305
+ ```
306
+ └─ getUser(123, {"cache":true}): 0.051 ms
307
+ ```
187
308
 
188
- TracePerf now provides a consistent API across Node.js and browser environments:
309
+ Arguments are rendered safely and kept short. Circular, huge or exotic values never throw and never get walked in full.
189
310
 
190
- - **Unified Tracking API**: The same tracking methods work identically in both environments
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
- Example of cross-environment usage:
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 nodeTracker = createTracePerf();
317
+ const tp = createTracePerf({ silent: true });
318
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
202
319
 
203
- // Browser
204
- import { createTracePerf } from 'traceperf/browser';
205
- const browserTracker = createTracePerf();
206
-
207
- // Both environments support the same API
208
- async function trackOperation(tracker) {
209
- return await tracker.track(async () => {
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
- ## Examples
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
- TracePerf includes several example files to help you get started:
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
- - **`examples/optimized-tracking-example.js`**: Demonstrates various ways to use the optimized tracking implementation
224
- - **`examples/browser-example.js`**: Shows how to use TracePerf in browser environments
338
+ ## Why was it slow? `traceperf explain`
225
339
 
226
- ### Tracking an Async Function
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 traceperf = createTracePerf();
382
+ const tp = createTracePerf({ silent: true });
231
383
 
232
- async function fetchData(url) {
233
- return traceperf.track(async () => {
234
- const response = await fetch(url);
235
- return response.json();
236
- }, {
237
- label: 'fetchData',
238
- trackMemory: true
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
- ### Tracking Nested Functions
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 traceperf = createTracePerf({ enableNestedTracking: true });
430
+ const tp = createTracePerf({ silent: true });
248
431
 
249
- async function processData() {
250
- return await traceperf.track(async () => {
251
- // This function call will be automatically tracked as a child
252
- const data = await fetchData();
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
- async function fetchData() {
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
- function transformData(data) {
266
- return traceperf.track(() => {
267
- // Implementation
268
- }, { label: 'transformData' });
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
- ### Optimizing for Production
486
+ ## Production
273
487
 
274
488
  ```javascript
275
489
  const { createTracePerf, TrackingMode } = require('traceperf');
276
490
 
277
- // Development environment
278
- const devTracker = createTracePerf({
279
- trackingMode: TrackingMode.DETAILED,
280
- silent: false,
281
- trackMemory: true,
282
- threshold: 0 // track everything
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
- // Production environment
286
- const prodTracker = createTracePerf({
287
- trackingMode: TrackingMode.PERFORMANCE,
288
- silent: true, // don't log to console
289
- trackMemory: false, // minimize overhead
290
- threshold: 100, // only track slow functions
291
- sampleRate: 0.01 // track only 1% of function calls
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
- // Use based on environment
295
- const traceperf = process.env.NODE_ENV === 'production' ? prodTracker : devTracker;
620
+ tracker.track(() => {
621
+ document.getElementById('output').textContent = 'Updated';
622
+ }, { label: 'updateDOM' });
296
623
  ```
297
624
 
298
- ## Browser Usage
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
- To use TracePerf in the browser:
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
- const browserTracker = createTracePerf({
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
- // DOM operations or other browser-side logic
641
+ document.getElementById('output').textContent = 'Updated';
315
642
  }, { label: 'domOperation' });
316
643
  </script>
317
644
  ```
318
645
 
319
- Or with ES modules:
646
+ Memory deltas are shown only where the browser exposes `performance.memory` (Chromium).
320
647
 
321
- ```javascript
322
- import { createTracePerf, TrackingMode } from 'traceperf/browser';
648
+ ### In Chrome DevTools
323
649
 
324
- const browserTracker = createTracePerf({
325
- trackingMode: TrackingMode.BALANCED
326
- });
650
+ ```javascript
651
+ import { createTracePerf, performanceTimeline } from 'traceperf/browser';
327
652
 
328
- // Track a DOM operation
329
- browserTracker.track(() => {
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
- ## Advanced Tracking Features
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
- ### Tracking with Different Modes
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
- const { createTracePerf, TrackingMode } = require('traceperf');
665
+ import { AsyncLocalStorage } from 'node:async_hooks';
666
+ const tp = createTracePerf({ asyncLocalStorage: AsyncLocalStorage });
667
+ ```
340
668
 
341
- // Performance mode - minimal overhead
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
- // Balanced mode - moderate detail with reasonable overhead
347
- const balancedTracker = createTracePerf({
348
- trackingMode: TrackingMode.BALANCED
349
- });
671
+ ## How it compares
350
672
 
351
- // Detailed mode - maximum information
352
- const detailedTracker = createTracePerf({
353
- trackingMode: TrackingMode.DETAILED
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
- ### Using Thresholds to Filter Results
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
- ```javascript
360
- const { createTracePerf } = require('traceperf');
361
- const tracker = createTracePerf();
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
- // Only log functions that take more than 50ms
364
- tracker.track(slowFunction, { threshold: 50 });
722
+ ## License
365
723
 
366
- // Different thresholds for different functions
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