nano-profiler 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paul Köhler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # NanoProfiler
2
+
3
+ NanoProfiler is a small, dependency-free profiler implemented in TypeScript.
4
+ It provides a single `NanoProfiler` class with utilities to measure execution time (and optionally memory) for synchronous and asynchronous code blocks.
5
+ The library works in Node.js and browser environments.
6
+
7
+ The profiler is designed to produce as little overhead as possible, making it suitable for profiling high-frequency code paths.
8
+ It supports manual start/end profiling as well as automatic profiling of functions and code blocks.
9
+
10
+ ## Installation
11
+
12
+ Install from npm:
13
+
14
+ ```
15
+ npm install nano-profiler
16
+ ```
17
+
18
+ ## Quick usage
19
+
20
+ Import the class and create an instance (or use the singleton):
21
+
22
+ ```ts
23
+ import { NanoProfiler } from 'nano-profiler';
24
+
25
+ const profiler = new NanoProfiler( { trackMem: true } );
26
+
27
+ // profile a synchronous function
28
+ profiler.run( () => {
29
+ // work
30
+ }, 'syncTask' );
31
+
32
+ // profile an async function
33
+ await profiler.runAsync( async () => {
34
+ await doSomething();
35
+ }, 'asyncTask' );
36
+
37
+ // manual start/end
38
+ const session = profiler.start( 'manual' );
39
+ // work
40
+ profiler.end( session );
41
+
42
+ // get results
43
+ const entries = profiler.report();
44
+ const summary = profiler.summary();
45
+ ```
46
+
47
+ ## API reference
48
+
49
+ ### Instantiate
50
+
51
+ - `NanoProfiler.global()`
52
+ Returns the global singleton instance of `NanoProfiler`. This is useful for simple use cases where you don't need multiple profilers.
53
+ - `new NanoProfiler( options?, hooks? )`
54
+ Creates a new instance of `NanoProfiler` with optional configuration options and hooks.
55
+
56
+ ### Control
57
+
58
+ - `enable() : boolean`
59
+ Enables the profiler. Returns `true` to indicate that profiling is now active.
60
+ - `disable() : boolean`
61
+ Disables the profiler. Returns `false` to indicate that profiling is now inactive.
62
+ - `flush () : ProfilerEntry[]`
63
+ Clears all stored profiling entries and returns them as an array. This is useful for resetting the profiler state or retrieving results before starting a new profiling session.
64
+ - `genEnv() : 'node' | 'browser' | 'unknown'`
65
+ Returns the detected runtime environment. This can be used for environment-specific profiling logic or optimizations.
66
+ - `isActive() : boolean`
67
+ Returns `true` if the profiler is currently enabled and active, or `false` if it is disabled.
68
+ - `getEntryCount() : number`
69
+ Returns the total number of profiling entries that have been recorded since the last flush.
70
+
71
+ ### Profiling
72
+
73
+ - `run< T >( fn: () => T, label?: string, meta?: any ) : T`
74
+ Profiles the execution of a synchronous function `fn`. Optionally accepts a `label` for categorization and `meta` for additional data. Returns the result of the function.
75
+ - `async runAsync< T >( fn: () => Promise< T >, label?: string, meta?: any ) : Promise< T >`
76
+ Profiles the execution of an asynchronous function `fn`. Optionally accepts a `label` for categorization and `meta` for additional data. Returns a promise that resolves to the result of the function.
77
+ - `start( label?: string ) : string`
78
+ Starts profiling for a code block with an optional `label` and returns a unique identifier for the profiling session. This can be used for manual profiling of code blocks that don't fit well with the `run` or `runAsync` methods.
79
+ - `end( session: string ) : void`
80
+ Ends the profiling session identified by the `session` string returned from `start()`. This will record the profiling data for that session. If the provided session identifier does not correspond to an active profiling session, an error will be thrown.
81
+
82
+ ### Reports
83
+
84
+ - `report( label?: string ) : ProfilerEntry[]`
85
+ Returns an array of profiling entries. If a `label` is provided, only entries with that label will be returned.
86
+ - `summary( label?: string ) : ProfilerSummary`
87
+ Returns a summary of profiling results, including total time, average time, count, and optionally memory usage. If a `label` is provided, the summary will be for entries with that label.
88
+ - `hotspot( label?: string ) : ProfilerEntry | undefined`
89
+ Returns the single profiling entry with the longest execution time. If a `label` is provided, only entries with that label will be considered.
90
+ - `histogram( label?: string, bins: number = 10 ) : HistogramEntry[]`
91
+ Returns a histogram of execution times for the profiled entries. The `bins` parameter determines how many bins the histogram will have. If a `label` is provided, only entries with that label will be included in the histogram.
92
+ - `percentiles( label?: string, ps: number[] = [ 50, 90, 95, 99 ] ) : PercentileEntry[]`
93
+ Returns the specified percentiles of execution times for the profiled entries. The `ps` parameter is an array of percentiles to calculate (e.g., 50 for median, 90 for 90th percentile). If a `label` is provided, only entries with that label will be included in the percentile calculations.
94
+
95
+ ## Options
96
+
97
+ - `enabled` (boolean, default: `true`)
98
+ Whether the profiler is enabled by default.
99
+ - `trackMem` (boolean, default: `false`)
100
+ Whether to track memory usage in addition to execution time. Enabling this may have a performance impact.
101
+ - `storeResults` (boolean, default: `false`)
102
+ If set to `true`, the profiler stores results of profiled blocks/functions.
103
+ - `sampleRate` (number, default: `1`)
104
+ A value between `0` and `1` that determines the percentage of blocks/functions getting profiled. Useful for reducing overhead in high-frequency code.
105
+ - `maxEntries` (number, default: `10000`)
106
+ Maximum number of profiled time/memory entries. If the limit is reached, no more entries will be recorded until `flush()`.
107
+
108
+ ### Hooks
109
+
110
+ - `onEntry( entry: ProfileEntry ) : void`
111
+ A callback function that gets called whenever a new profile entry is recorded. Useful for real-time monitoring or custom logging.
112
+ - `onFlush( entries: ProfileEntry[] ) : void`
113
+ A callback function that gets called when the profiler is flushed. Receives all stored entries as an argument.
114
+
115
+ ----
116
+
117
+ Copyright (c) 2026 Paul Köhler (komed3). All rights reserved.
118
+ Released under the MIT license. See LICENSE file in the project root for details.
@@ -0,0 +1,251 @@
1
+ /**
2
+ * NanoProfiler is a lightweight and efficient profiling library for JavaScript and TypeScript.
3
+ *
4
+ * It provides functionality to measure execution time and memory usage of code blocks,
5
+ * with support for both synchronous and asynchronous functions.
6
+ *
7
+ * The profiler is designed to work in both Node.js and browser environments, automatically
8
+ * detecting the environment and using appropriate timing and memory measurement functions.
9
+ *
10
+ * @author Paul Köhler
11
+ * @license MIT
12
+ */
13
+ /** Environment types for the NanoProfiler. */
14
+ export type Env = 'node' | 'browser' | 'unknown';
15
+ /** Configuration options for the NanoProfiler. */
16
+ export interface ProfilerOptions {
17
+ enabled?: boolean;
18
+ trackMem?: boolean;
19
+ storeResults?: boolean;
20
+ sampleRate?: number;
21
+ maxEntries?: number;
22
+ }
23
+ /** Hooks for custom behavior on entry recording and flushing. */
24
+ export interface ProfilerHooks {
25
+ onEntry?: (entry: ProfilerEntry) => void;
26
+ onFlush?: (entry: ProfilerEntry[]) => void;
27
+ }
28
+ /** Profiling entry representing single measurement of time and memory usage. */
29
+ export interface ProfilerEntry<T = any> {
30
+ label?: string;
31
+ time: number;
32
+ mem?: number;
33
+ res?: T;
34
+ meta?: any;
35
+ }
36
+ /** Summary of profiling data, including total, max, min, and avg time and memory usage. */
37
+ export interface ProfilerSummary {
38
+ calls: number;
39
+ time: {
40
+ total: number;
41
+ max: number;
42
+ min: number;
43
+ avg: number;
44
+ };
45
+ mem?: {
46
+ total: number;
47
+ max: number;
48
+ min: number;
49
+ avg: number;
50
+ };
51
+ }
52
+ /** Histogram entry representing range of times and number of calls whin that range. */
53
+ export interface HistogramEntry {
54
+ bin: number;
55
+ calls: number;
56
+ }
57
+ /** Percentile entry representing a specific percentile and its corresponding time. */
58
+ export interface PercentileEntry {
59
+ percentile: number;
60
+ time: number;
61
+ }
62
+ /** Function type for running profiled code. */
63
+ export type RunnerFn<T = any> = (fn: () => T, label?: string, meta?: any) => T;
64
+ export type AsyncRunnerFn<T = any> = (fn: () => Promise<T>, label?: string, meta?: any) => Promise<T>;
65
+ /** Function type for timer functions that return the current time or memory usage. */
66
+ export type TimerFn = () => number;
67
+ /** The main NanoProfiler class that provides profiling functionality. */
68
+ export declare class NanoProfiler {
69
+ /** A singleton instance of NanoProfiler for global use. */
70
+ private static globalInstance?;
71
+ /**
72
+ * Returns the global instance of NanoProfiler, creating it if it doesn't already exist.
73
+ *
74
+ * @returns {NanoProfiler} The global NanoProfiler instance.
75
+ */
76
+ static global(): NanoProfiler;
77
+ /** Internal state and configuration. */
78
+ private readonly options;
79
+ private readonly maxEntries;
80
+ private readonly sampleRate;
81
+ private readonly hooks?;
82
+ private readonly env;
83
+ /** Timer functions for measuring time and memory usage, set up based on the detected environment. */
84
+ private now;
85
+ private mem;
86
+ /** Runner functions for executing profiled code, set up to record profiling data. */
87
+ private runner;
88
+ private runnerAsync;
89
+ /** Indicates whether the profiler is currently active (enabled) or not. */
90
+ private active;
91
+ /** An array to store profiling entries, and an index to keep track of the current position in the array. */
92
+ private entries;
93
+ private index;
94
+ /** A map to track active labels for manual start/end profiling. */
95
+ private tl;
96
+ /**
97
+ * Detects the current environment (Node.js, browser, or unknown).
98
+ *
99
+ * @returns {Env} The detected environment.
100
+ */
101
+ private detectEnv;
102
+ /**
103
+ * Sets up the appropriate timer function based on the detected environment.
104
+ *
105
+ * @returns {TimerFn} A function that returns the current time in milliseconds.
106
+ */
107
+ private setupNow;
108
+ /**
109
+ * Sets up the appropriate memory usage function based on the detected environment.
110
+ *
111
+ * @returns {TimerFn} A function that returns the current memory usage in bytes.
112
+ */
113
+ private setupMem;
114
+ /**
115
+ * Records a profiling entry with the given time, memory usage, result, label, and metadata.
116
+ *
117
+ * @param {number} time - The time taken for the profiled code to execute.
118
+ * @param {number | undefined} mem - The memory used during the execution (if tracking is enabled).
119
+ * @param {T} res - The result of the profiled code (if storing results is enabled).
120
+ * @param {string} [label] - An optional label for the profiling entry.
121
+ * @param {any} [meta] - Optional metadata to associate with the profiling entry.
122
+ */
123
+ private record;
124
+ /**
125
+ * Creates a new instance of NanoProfiler with the given options and hooks.
126
+ *
127
+ * @param {ProfilerOptions} [options] - Configuration options for the profiler.
128
+ * @param {ProfilerHooks} [hooks] - Hooks for custom behavior on entry recording and flushing.
129
+ */
130
+ constructor(options?: ProfilerOptions, hooks?: ProfilerHooks);
131
+ /**
132
+ * Enables the profiler.
133
+ *
134
+ * Sets up the runner functions to record profiling data when executing code,
135
+ * and returns true to indicate that the profiler is now active.
136
+ *
137
+ * @returns {boolean} True if the profiler was successfully enabled, false otherwise.
138
+ */
139
+ enable(): boolean;
140
+ /**
141
+ * Disables the profiler.
142
+ *
143
+ * @returns {boolean} False, indicating that the profiler is now disabled.
144
+ */
145
+ disable(): boolean;
146
+ /**
147
+ * Returns the detected environment in which the profiler is running.
148
+ *
149
+ * @returns {Env} The detected environment ('node', 'browser', or 'unknown').
150
+ */
151
+ getEnv(): Env;
152
+ /**
153
+ * Checks if the profiler is currently active (enabled).
154
+ *
155
+ * @returns {boolean} True if the profiler is active, false otherwise.
156
+ */
157
+ isActive(): boolean;
158
+ /**
159
+ * Returns the number of profiling entries that have been logged so far.
160
+ *
161
+ * @returns {number} The number of logged profiling entries.
162
+ */
163
+ getEntryCount(): number;
164
+ /**
165
+ * Runs the provided function while recording profiling data, using the configured runner function.
166
+ *
167
+ * @param {() => T} fn - The function to execute and profile.
168
+ * @param {string} [label] - An optional label to associate with the profiling entry.
169
+ * @param {any} [meta] - Optional metadata to associate with the profiling entry.
170
+ * @returns {T} The result of the executed function.
171
+ */
172
+ run<T>(fn: () => T, label?: string, meta?: any): T;
173
+ /**
174
+ * Runs the provided asynchronous function while recording profiling data,
175
+ * using the configured asynchronous runner function.
176
+ *
177
+ * @param {() => Promise<T>} fn - The asynchronous function to execute and profile.
178
+ * @param {string} [label] - Optional label to associate with the profiling entry.
179
+ * @param {any} [meta] - Optional metadata to associate with the profiling entry.
180
+ * @returns {Promise<T>} A promise that resolves to the result of the executed asynchronous function.
181
+ */
182
+ runAsync<T>(fn: () => Promise<T>, label?: string, meta?: any): Promise<T>;
183
+ /**
184
+ * Starts a manual profiling session with an optional label and returns
185
+ * a unique identifier for the session.
186
+ *
187
+ * @param {string} [label] - An optional label to associate with the profiling session.
188
+ * @return {string} A unique identifier for the profiling session.
189
+ */
190
+ start(label?: string): string;
191
+ /**
192
+ * Ends a manual profiling session with the given label and records the profiling data.
193
+ *
194
+ * @param {string} session - The unique identifier for the profiling session to end.
195
+ * @throws {Error} If the provided session does not correspond to an active profiling session.
196
+ */
197
+ end(session: string): void;
198
+ /**
199
+ * Retrieves profiling entries, optionally filtered by a specific label.
200
+ *
201
+ * @param {string} [label] - An optional label to filter the profiling entries.
202
+ * @returns {ProfilerEntry[]} An array of profiling entries that match the specified label.
203
+ */
204
+ report(label?: string): ProfilerEntry[];
205
+ /**
206
+ * Generates a summary of profiling data, optionally filtered by a specific label.
207
+ *
208
+ * Calculates total, maximum, minimum, and average time (and memory usage if tracking
209
+ * is enabled) for the profiling entries that match the specified label.
210
+ *
211
+ * @param {string} [label] - An optional label to filter the profiling entries.
212
+ * @returns {ProfilerSummary} A summary object containing aggregated profiling data.
213
+ */
214
+ summary(label?: string): ProfilerSummary;
215
+ /**
216
+ * Identifies the profiling entry with the longest execution time,
217
+ * optionally filtered by a specific label.
218
+ *
219
+ * @param {string} [label] - An optional label to filter the profiling entries.
220
+ * @returns {ProfilerEntry | undefined} The profiling entry with the longest execution time.
221
+ */
222
+ hotspot(label?: string): ProfilerEntry | undefined;
223
+ /**
224
+ * Generates a histogram of execution times for profiling entries,
225
+ * optionally filtered by a specific label.
226
+ *
227
+ * Calculates the minimum and maximum execution times, divides the range into
228
+ * specified bins, and counts the number of entries that fall into each bin to
229
+ * create a histogram representation of the execution times.
230
+ *
231
+ * @param {string} [label] - An optional label to filter the profiling entries.
232
+ * @param {number} [bins=10] - The number of bins to use for the histogram.
233
+ * @returns {HistogramEntry[]} An array of histogram entries.
234
+ */
235
+ histogram(label?: string, bins?: number): HistogramEntry[];
236
+ /**
237
+ * Calculates specific percentiles of execution times for profiling entries,
238
+ * optionally filtered by a specific label.
239
+ *
240
+ * @param {string} [label] - An optional label to filter the profiling entries.
241
+ * @param {number[]} [ps=[50, 90, 95, 99]] - An array of percentiles to calculate.
242
+ * @returns {PercentileEntry[]} An array of objects representing the calculated percentiles.
243
+ */
244
+ percentiles(label?: string, ps?: number[]): PercentileEntry[];
245
+ /**
246
+ * Flushes the recorded profiling entries, returning them and resetting the internal state.
247
+ *
248
+ * @returns {ProfilerEntry[]} An array of profiling entries that were flushed.
249
+ */
250
+ flush(): ProfilerEntry[];
251
+ }
@@ -0,0 +1,276 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_OPTIONS = {
4
+ enabled: true,
5
+ trackMem: false,
6
+ storeResults: false,
7
+ sampleRate: 1,
8
+ maxEntries: 10_000
9
+ };
10
+ class NanoProfiler {
11
+ static globalInstance;
12
+ static global() {
13
+ return (NanoProfiler.globalInstance ??= new NanoProfiler());
14
+ }
15
+ options;
16
+ maxEntries;
17
+ sampleRate;
18
+ hooks;
19
+ env;
20
+ now;
21
+ mem;
22
+ runner;
23
+ runnerAsync;
24
+ active;
25
+ entries;
26
+ index = 0;
27
+ tl = new Map();
28
+ detectEnv() {
29
+ if (typeof process !== 'undefined' && process.versions?.node) return 'node';
30
+ else if (typeof performance !== 'undefined') return 'browser';
31
+ return 'unknown';
32
+ }
33
+ setupNow() {
34
+ switch (this.env) {
35
+ case 'node':
36
+ return () => {
37
+ const t = process.hrtime();
38
+ return t[0] * 1e3 + t[1] * 1e-6;
39
+ };
40
+ case 'browser':
41
+ return () => performance.now();
42
+ default:
43
+ return () => Date.now();
44
+ }
45
+ }
46
+ setupMem() {
47
+ switch (this.env) {
48
+ case 'node':
49
+ return () => process.memoryUsage().heapUsed;
50
+ case 'browser':
51
+ return () => performance.memory?.usedJSHeapSize ?? 0;
52
+ default:
53
+ return () => 0;
54
+ }
55
+ }
56
+ record(time, mem, res, label, meta) {
57
+ if (this.index >= this.maxEntries) return;
58
+ const entry = this.entries[this.index] ?? (this.entries[this.index] = {});
59
+ this.index++;
60
+ entry.time = time;
61
+ entry.label = label;
62
+ entry.mem = mem;
63
+ entry.res = this.options.storeResults ? res : undefined;
64
+ entry.meta = meta;
65
+ this.hooks?.onEntry?.(entry);
66
+ }
67
+ constructor(options = {}, hooks) {
68
+ this.options = { ...DEFAULT_OPTIONS, ...options };
69
+ this.maxEntries = Number(this.options.maxEntries ?? 10_000);
70
+ this.sampleRate = Math.max(
71
+ 0,
72
+ Math.min(1, Number(this.options.sampleRate ?? 1))
73
+ );
74
+ this.hooks = hooks;
75
+ this.env = this.detectEnv();
76
+ this.now = this.setupNow();
77
+ this.mem = this.setupMem();
78
+ this.entries = new Array(this.maxEntries);
79
+ this.active = this.options.enabled ? this.enable() : this.disable();
80
+ }
81
+ enable() {
82
+ if (this.active) return true;
83
+ const trackMem = this.options.trackMem;
84
+ const sampleRate = this.sampleRate;
85
+ const self = this,
86
+ now = this.now,
87
+ mem = this.mem;
88
+ this.runner = (fn, label, meta) => {
89
+ if (sampleRate < 1 && Math.random() >= sampleRate) return fn();
90
+ const t0 = now(),
91
+ m0 = trackMem ? mem() : undefined;
92
+ let res;
93
+ try {
94
+ return (res = fn());
95
+ } finally {
96
+ self.record(
97
+ now() - t0,
98
+ trackMem ? mem() - m0 : undefined,
99
+ res,
100
+ label,
101
+ meta
102
+ );
103
+ }
104
+ };
105
+ this.runnerAsync = async (fn, label, meta) => {
106
+ if (sampleRate < 1 && Math.random() >= sampleRate) return await fn();
107
+ const t0 = now(),
108
+ m0 = trackMem ? mem() : undefined;
109
+ let res;
110
+ try {
111
+ return (res = await fn());
112
+ } finally {
113
+ self.record(
114
+ now() - t0,
115
+ trackMem ? mem() - m0 : undefined,
116
+ res,
117
+ label,
118
+ meta
119
+ );
120
+ }
121
+ };
122
+ return (this.active = true);
123
+ }
124
+ disable() {
125
+ this.runner = (fn) => fn();
126
+ this.runnerAsync = (fn) => fn();
127
+ return (this.active = false);
128
+ }
129
+ getEnv() {
130
+ return this.env;
131
+ }
132
+ isActive() {
133
+ return this.active;
134
+ }
135
+ getEntryCount() {
136
+ return this.index;
137
+ }
138
+ run(fn, label, meta) {
139
+ return this.runner(fn, label, meta);
140
+ }
141
+ async runAsync(fn, label, meta) {
142
+ return this.runnerAsync(fn, label, meta);
143
+ }
144
+ start(label) {
145
+ const session = crypto.randomUUID();
146
+ this.tl.set(session, {
147
+ label,
148
+ time: this.now(),
149
+ mem: this.options.trackMem ? this.mem() : undefined
150
+ });
151
+ return session;
152
+ }
153
+ end(session) {
154
+ const start = this.tl.get(session);
155
+ if (!start)
156
+ throw new Error(
157
+ `No active profiling session found for label: ${session}`
158
+ );
159
+ this.tl.delete(session);
160
+ this.record(
161
+ this.now() - start.time,
162
+ this.options.trackMem ? this.mem() - start.mem : undefined,
163
+ undefined,
164
+ start.label
165
+ );
166
+ }
167
+ report(label) {
168
+ const { entries, index } = this;
169
+ if (!label) return entries.slice(0, index);
170
+ const result = [];
171
+ for (let i = 0; i < index; i++) {
172
+ const e = entries[i];
173
+ if (e.label === label) result.push(e);
174
+ }
175
+ return result;
176
+ }
177
+ summary(label) {
178
+ const entries = this.report(label);
179
+ const calls = entries.length;
180
+ let tTotal = 0,
181
+ tMax = 0,
182
+ tMin = Infinity,
183
+ mTotal = 0,
184
+ mMax = 0,
185
+ mMin = Infinity;
186
+ for (const e of entries) {
187
+ const { time, mem } = e;
188
+ tTotal += time;
189
+ if (time > tMax) tMax = time;
190
+ if (time < tMin) tMin = time;
191
+ if (mem !== undefined) {
192
+ mTotal += mem;
193
+ if (mem > mMax) mMax = mem;
194
+ if (mem < mMin) mMin = mem;
195
+ }
196
+ }
197
+ const summary = {
198
+ calls,
199
+ time: {
200
+ total: tTotal,
201
+ max: tMax,
202
+ min: tMin === Infinity ? 0 : tMin,
203
+ avg: calls > 0 ? tTotal / calls : 0
204
+ }
205
+ };
206
+ if (mTotal > 0)
207
+ summary.mem = {
208
+ total: mTotal,
209
+ max: mMax,
210
+ min: mMin === Infinity ? 0 : mMin,
211
+ avg: calls > 0 ? mTotal / calls : 0
212
+ };
213
+ return summary;
214
+ }
215
+ hotspot(label) {
216
+ const entries = this.report(label);
217
+ if (entries.length === 0) return undefined;
218
+ let max;
219
+ for (const e of entries) if (!max || e.time > max.time) max = e;
220
+ return max;
221
+ }
222
+ histogram(label, bins = 10) {
223
+ const entries = this.report(label);
224
+ if (entries.length === 0) return [];
225
+ let min = Infinity;
226
+ let max = -Infinity;
227
+ for (const e of entries) {
228
+ const t = e.time;
229
+ if (t < min) min = t;
230
+ if (t > max) max = t;
231
+ }
232
+ if (min === Infinity) return [];
233
+ if (max === min) return [{ bin: min, calls: entries.length }];
234
+ const binSize = (max - min) / bins;
235
+ const histogram = new Array(bins);
236
+ for (let i = 0; i < bins; i++)
237
+ histogram[i] = { bin: min + i * binSize, calls: 0 };
238
+ for (const e of entries) {
239
+ const t = e.time;
240
+ let i = ((t - min) / binSize) | 0;
241
+ if (i >= bins) i = bins - 1;
242
+ histogram[i].calls++;
243
+ }
244
+ return histogram;
245
+ }
246
+ percentiles(label, ps = [50, 90, 95, 99]) {
247
+ const entries = this.report(label);
248
+ const n = entries.length;
249
+ if (n === 0) return [];
250
+ const times = new Array(n);
251
+ for (let i = 0; i < n; i++) times[i] = entries[i].time;
252
+ times.sort((a, b) => a - b);
253
+ const result = new Array(ps.length);
254
+ for (let i = 0; i < ps.length; i++) {
255
+ const p = ps[i];
256
+ const cp = p < 0 ? 0 : p > 100 ? 100 : p;
257
+ const idx = (cp / 100) * (n - 1);
258
+ const lo = idx | 0;
259
+ const hi = Math.ceil(idx);
260
+ const time =
261
+ lo === hi
262
+ ? times[lo]
263
+ : times[lo] + (times[hi] - times[lo]) * (idx - lo);
264
+ result[i] = { percentile: cp, time };
265
+ }
266
+ return result;
267
+ }
268
+ flush() {
269
+ const data = this.entries.slice(0, this.index);
270
+ this.hooks?.onFlush?.(data);
271
+ this.index = 0;
272
+ return data;
273
+ }
274
+ }
275
+
276
+ exports.NanoProfiler = NanoProfiler;
@@ -0,0 +1,274 @@
1
+ const DEFAULT_OPTIONS = {
2
+ enabled: true,
3
+ trackMem: false,
4
+ storeResults: false,
5
+ sampleRate: 1,
6
+ maxEntries: 10_000
7
+ };
8
+ class NanoProfiler {
9
+ static globalInstance;
10
+ static global() {
11
+ return (NanoProfiler.globalInstance ??= new NanoProfiler());
12
+ }
13
+ options;
14
+ maxEntries;
15
+ sampleRate;
16
+ hooks;
17
+ env;
18
+ now;
19
+ mem;
20
+ runner;
21
+ runnerAsync;
22
+ active;
23
+ entries;
24
+ index = 0;
25
+ tl = new Map();
26
+ detectEnv() {
27
+ if (typeof process !== 'undefined' && process.versions?.node) return 'node';
28
+ else if (typeof performance !== 'undefined') return 'browser';
29
+ return 'unknown';
30
+ }
31
+ setupNow() {
32
+ switch (this.env) {
33
+ case 'node':
34
+ return () => {
35
+ const t = process.hrtime();
36
+ return t[0] * 1e3 + t[1] * 1e-6;
37
+ };
38
+ case 'browser':
39
+ return () => performance.now();
40
+ default:
41
+ return () => Date.now();
42
+ }
43
+ }
44
+ setupMem() {
45
+ switch (this.env) {
46
+ case 'node':
47
+ return () => process.memoryUsage().heapUsed;
48
+ case 'browser':
49
+ return () => performance.memory?.usedJSHeapSize ?? 0;
50
+ default:
51
+ return () => 0;
52
+ }
53
+ }
54
+ record(time, mem, res, label, meta) {
55
+ if (this.index >= this.maxEntries) return;
56
+ const entry = this.entries[this.index] ?? (this.entries[this.index] = {});
57
+ this.index++;
58
+ entry.time = time;
59
+ entry.label = label;
60
+ entry.mem = mem;
61
+ entry.res = this.options.storeResults ? res : undefined;
62
+ entry.meta = meta;
63
+ this.hooks?.onEntry?.(entry);
64
+ }
65
+ constructor(options = {}, hooks) {
66
+ this.options = { ...DEFAULT_OPTIONS, ...options };
67
+ this.maxEntries = Number(this.options.maxEntries ?? 10_000);
68
+ this.sampleRate = Math.max(
69
+ 0,
70
+ Math.min(1, Number(this.options.sampleRate ?? 1))
71
+ );
72
+ this.hooks = hooks;
73
+ this.env = this.detectEnv();
74
+ this.now = this.setupNow();
75
+ this.mem = this.setupMem();
76
+ this.entries = new Array(this.maxEntries);
77
+ this.active = this.options.enabled ? this.enable() : this.disable();
78
+ }
79
+ enable() {
80
+ if (this.active) return true;
81
+ const trackMem = this.options.trackMem;
82
+ const sampleRate = this.sampleRate;
83
+ const self = this,
84
+ now = this.now,
85
+ mem = this.mem;
86
+ this.runner = (fn, label, meta) => {
87
+ if (sampleRate < 1 && Math.random() >= sampleRate) return fn();
88
+ const t0 = now(),
89
+ m0 = trackMem ? mem() : undefined;
90
+ let res;
91
+ try {
92
+ return (res = fn());
93
+ } finally {
94
+ self.record(
95
+ now() - t0,
96
+ trackMem ? mem() - m0 : undefined,
97
+ res,
98
+ label,
99
+ meta
100
+ );
101
+ }
102
+ };
103
+ this.runnerAsync = async (fn, label, meta) => {
104
+ if (sampleRate < 1 && Math.random() >= sampleRate) return await fn();
105
+ const t0 = now(),
106
+ m0 = trackMem ? mem() : undefined;
107
+ let res;
108
+ try {
109
+ return (res = await fn());
110
+ } finally {
111
+ self.record(
112
+ now() - t0,
113
+ trackMem ? mem() - m0 : undefined,
114
+ res,
115
+ label,
116
+ meta
117
+ );
118
+ }
119
+ };
120
+ return (this.active = true);
121
+ }
122
+ disable() {
123
+ this.runner = (fn) => fn();
124
+ this.runnerAsync = (fn) => fn();
125
+ return (this.active = false);
126
+ }
127
+ getEnv() {
128
+ return this.env;
129
+ }
130
+ isActive() {
131
+ return this.active;
132
+ }
133
+ getEntryCount() {
134
+ return this.index;
135
+ }
136
+ run(fn, label, meta) {
137
+ return this.runner(fn, label, meta);
138
+ }
139
+ async runAsync(fn, label, meta) {
140
+ return this.runnerAsync(fn, label, meta);
141
+ }
142
+ start(label) {
143
+ const session = crypto.randomUUID();
144
+ this.tl.set(session, {
145
+ label,
146
+ time: this.now(),
147
+ mem: this.options.trackMem ? this.mem() : undefined
148
+ });
149
+ return session;
150
+ }
151
+ end(session) {
152
+ const start = this.tl.get(session);
153
+ if (!start)
154
+ throw new Error(
155
+ `No active profiling session found for label: ${session}`
156
+ );
157
+ this.tl.delete(session);
158
+ this.record(
159
+ this.now() - start.time,
160
+ this.options.trackMem ? this.mem() - start.mem : undefined,
161
+ undefined,
162
+ start.label
163
+ );
164
+ }
165
+ report(label) {
166
+ const { entries, index } = this;
167
+ if (!label) return entries.slice(0, index);
168
+ const result = [];
169
+ for (let i = 0; i < index; i++) {
170
+ const e = entries[i];
171
+ if (e.label === label) result.push(e);
172
+ }
173
+ return result;
174
+ }
175
+ summary(label) {
176
+ const entries = this.report(label);
177
+ const calls = entries.length;
178
+ let tTotal = 0,
179
+ tMax = 0,
180
+ tMin = Infinity,
181
+ mTotal = 0,
182
+ mMax = 0,
183
+ mMin = Infinity;
184
+ for (const e of entries) {
185
+ const { time, mem } = e;
186
+ tTotal += time;
187
+ if (time > tMax) tMax = time;
188
+ if (time < tMin) tMin = time;
189
+ if (mem !== undefined) {
190
+ mTotal += mem;
191
+ if (mem > mMax) mMax = mem;
192
+ if (mem < mMin) mMin = mem;
193
+ }
194
+ }
195
+ const summary = {
196
+ calls,
197
+ time: {
198
+ total: tTotal,
199
+ max: tMax,
200
+ min: tMin === Infinity ? 0 : tMin,
201
+ avg: calls > 0 ? tTotal / calls : 0
202
+ }
203
+ };
204
+ if (mTotal > 0)
205
+ summary.mem = {
206
+ total: mTotal,
207
+ max: mMax,
208
+ min: mMin === Infinity ? 0 : mMin,
209
+ avg: calls > 0 ? mTotal / calls : 0
210
+ };
211
+ return summary;
212
+ }
213
+ hotspot(label) {
214
+ const entries = this.report(label);
215
+ if (entries.length === 0) return undefined;
216
+ let max;
217
+ for (const e of entries) if (!max || e.time > max.time) max = e;
218
+ return max;
219
+ }
220
+ histogram(label, bins = 10) {
221
+ const entries = this.report(label);
222
+ if (entries.length === 0) return [];
223
+ let min = Infinity;
224
+ let max = -Infinity;
225
+ for (const e of entries) {
226
+ const t = e.time;
227
+ if (t < min) min = t;
228
+ if (t > max) max = t;
229
+ }
230
+ if (min === Infinity) return [];
231
+ if (max === min) return [{ bin: min, calls: entries.length }];
232
+ const binSize = (max - min) / bins;
233
+ const histogram = new Array(bins);
234
+ for (let i = 0; i < bins; i++)
235
+ histogram[i] = { bin: min + i * binSize, calls: 0 };
236
+ for (const e of entries) {
237
+ const t = e.time;
238
+ let i = ((t - min) / binSize) | 0;
239
+ if (i >= bins) i = bins - 1;
240
+ histogram[i].calls++;
241
+ }
242
+ return histogram;
243
+ }
244
+ percentiles(label, ps = [50, 90, 95, 99]) {
245
+ const entries = this.report(label);
246
+ const n = entries.length;
247
+ if (n === 0) return [];
248
+ const times = new Array(n);
249
+ for (let i = 0; i < n; i++) times[i] = entries[i].time;
250
+ times.sort((a, b) => a - b);
251
+ const result = new Array(ps.length);
252
+ for (let i = 0; i < ps.length; i++) {
253
+ const p = ps[i];
254
+ const cp = p < 0 ? 0 : p > 100 ? 100 : p;
255
+ const idx = (cp / 100) * (n - 1);
256
+ const lo = idx | 0;
257
+ const hi = Math.ceil(idx);
258
+ const time =
259
+ lo === hi
260
+ ? times[lo]
261
+ : times[lo] + (times[hi] - times[lo]) * (idx - lo);
262
+ result[i] = { percentile: cp, time };
263
+ }
264
+ return result;
265
+ }
266
+ flush() {
267
+ const data = this.entries.slice(0, this.index);
268
+ this.hooks?.onFlush?.(data);
269
+ this.index = 0;
270
+ return data;
271
+ }
272
+ }
273
+
274
+ export { NanoProfiler };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "nano-profiler",
3
+ "description": "Fast and lightweight JavaScript profiler for measuring time and memory consumption",
4
+ "license": "MIT",
5
+ "version": "1.0.0",
6
+ "author": {
7
+ "name": "komed3 (Paul Köhler)",
8
+ "email": "webmaster@komed3.de",
9
+ "url": "https://komed3.de"
10
+ },
11
+ "homepage": "https://github.com/komed3/nano-profiler",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/komed3/nano-profiler.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/komed3/nano-profiler/issues"
18
+ },
19
+ "funding": {
20
+ "type": "ko-fi",
21
+ "url": "https://ko-fi.com/komed3"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README",
26
+ "LICENSE"
27
+ ],
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "types": "dist/NanoProfiler.d.ts",
32
+ "main": "dist/nano-profiler.cjs.js",
33
+ "module": "dist/nano-profiler.esm.js",
34
+ "scripts": {
35
+ "build": "rollup -c && tsc --emitDeclarationOnly"
36
+ },
37
+ "devDependencies": {
38
+ "@rollup/plugin-commonjs": "^29.0.2",
39
+ "@rollup/plugin-node-resolve": "^16.0.3",
40
+ "@rollup/plugin-typescript": "^12.3.0",
41
+ "@types/node": "^25.5.0",
42
+ "rollup": "^4.59.0",
43
+ "rollup-plugin-cleanup": "^3.2.1",
44
+ "rollup-plugin-prettier": "^4.1.2",
45
+ "ts-node": "^10.9.2",
46
+ "tslib": "^2.8.1",
47
+ "typescript": "^5.9.3"
48
+ }
49
+ }