measure-fn 3.1.0 → 3.2.1
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/.github/workflows/ci.yml +20 -0
- package/LICENSE +21 -0
- package/README.md +90 -70
- package/SKILL.md +41 -44
- package/bench.ts +54 -0
- package/example.ts +42 -21
- package/index.test.ts +259 -234
- package/index.ts +111 -27
- package/package.json +2 -2
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
name: CI
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches: [master, main]
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: oven-sh/setup-bun@v1
|
|
15
|
+
with:
|
|
16
|
+
bun-version: latest
|
|
17
|
+
|
|
18
|
+
- run: bun install
|
|
19
|
+
- run: bun test
|
|
20
|
+
- run: bun run example.ts
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 7flash
|
|
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
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
# measure-fn
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
**Stop writing blind code.** Every function you write either succeeds or fails, takes some amount of time, and lives inside a larger flow. `measure-fn` makes all of that visible — automatically, hierarchically.
|
|
4
6
|
|
|
5
7
|
```
|
|
6
|
-
[18:
|
|
7
|
-
[18:
|
|
8
|
-
[18:
|
|
9
|
-
[18:
|
|
10
|
-
[18:
|
|
11
|
-
[18:
|
|
12
|
-
[18:
|
|
13
|
-
[18:
|
|
14
|
-
[18:
|
|
15
|
-
[18:
|
|
16
|
-
[18:
|
|
17
|
-
[18:
|
|
8
|
+
[18:50:04.893] [a] ✓ Load config 0.09ms → {"env":"prod","port":3000}
|
|
9
|
+
[18:50:04.894] [b] = App ready
|
|
10
|
+
[18:50:04.895] [e] ... Parallel Fetch
|
|
11
|
+
[18:50:04.895] [e-a] ... Fetch User (userId=1)
|
|
12
|
+
[18:50:04.895] [e-b] ... Fetch User (userId=2)
|
|
13
|
+
[18:50:04.950] [e-b] ✓ Fetch User 55.58ms → {"id":2,"name":"User 2"}
|
|
14
|
+
[18:50:04.981] [e-a] ✓ Fetch User 85.93ms → {"id":1,"name":"User 1"}
|
|
15
|
+
[18:50:04.981] [e] ✓ Parallel Fetch 86.08ms
|
|
16
|
+
[18:50:05.072] [f] ✓ DB query 91.12ms → {"rows":42} ⚠ OVER BUDGET (30.00ms)
|
|
17
|
+
[18:50:05.775] [m] ... Fetch all users (20 items)
|
|
18
|
+
[18:50:06.179] [m] = 5/20 (0.4s, 12/s)
|
|
19
|
+
[18:50:07.450] [m] ✓ Fetch all users (20 items) 1.7s → "20/20 ok"
|
|
20
|
+
[18:50:07.450] [api:a] ... GET /users
|
|
21
|
+
[18:50:07.450] [db:a] ... SELECT users
|
|
22
|
+
[18:50:07.493] [db:a] ✓ SELECT users 43.07ms → [{"id":1},{"id":2}]
|
|
23
|
+
[18:50:07.493] [api:a] ✓ GET /users 43.32ms → [{"id":1},{"id":2}]
|
|
24
|
+
[18:50:08.721] [o] ✓ Slow op 1.2s → "slow"
|
|
18
25
|
```
|
|
19
26
|
|
|
20
|
-
No setup. No dashboards. Just wrap your functions.
|
|
27
|
+
No setup. No dashboards. Just wrap your functions.
|
|
21
28
|
|
|
22
29
|
## Install
|
|
23
30
|
|
|
@@ -30,16 +37,16 @@ bun add measure-fn
|
|
|
30
37
|
```typescript
|
|
31
38
|
import { measure, measureSync } from 'measure-fn';
|
|
32
39
|
|
|
33
|
-
// Sync leaf — single line with result
|
|
34
|
-
const config = measureSync('Parse config', () => JSON.parse(
|
|
40
|
+
// Sync leaf — single line with auto-printed result
|
|
41
|
+
const config = measureSync('Parse config', () => JSON.parse(str));
|
|
35
42
|
// → [a] ✓ Parse config 0.20ms → {"port":3000}
|
|
36
43
|
|
|
37
44
|
// Async — start + end
|
|
38
|
-
await measure('Fetch data', async () => {
|
|
39
|
-
return await fetch(
|
|
45
|
+
const data = await measure('Fetch data', async () => {
|
|
46
|
+
return await fetch(url).then(r => r.json());
|
|
40
47
|
});
|
|
41
48
|
// → [b] ... Fetch data
|
|
42
|
-
// → [b] ✓ Fetch data 245.12ms → [{"id":1}
|
|
49
|
+
// → [b] ✓ Fetch data 245.12ms → [{"id":1}]
|
|
43
50
|
```
|
|
44
51
|
|
|
45
52
|
## Output Format
|
|
@@ -51,62 +58,75 @@ await measure('Fetch data', async () => {
|
|
|
51
58
|
| `[id] ✗ label Nms (err)` | Error | `[a] ✗ Fetch 2ms (timeout)` |
|
|
52
59
|
| `[id] = label` | Annotation | `[a] = checkpoint` |
|
|
53
60
|
|
|
54
|
-
**No indentation, no colors.** IDs encode hierarchy
|
|
61
|
+
**No indentation, no colors.** IDs encode hierarchy. Return values auto-print. Circular refs → `[Circular]`, long values truncated.
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
**Smart duration**: `0.10ms` → `1.2s` → `2m 5s`
|
|
57
64
|
|
|
58
65
|
## API
|
|
59
66
|
|
|
60
67
|
### `measure(label, fn?)` — async
|
|
61
68
|
|
|
62
69
|
```typescript
|
|
63
|
-
|
|
70
|
+
// Simple
|
|
71
|
+
const user = await measure('Fetch user', () => fetchUser(1));
|
|
64
72
|
|
|
65
|
-
// Nested
|
|
73
|
+
// Nested + parallel
|
|
66
74
|
await measure('Pipeline', async (m) => {
|
|
67
|
-
const user = await m('Get user', () => fetchUser(1));
|
|
68
|
-
if (!user) return;
|
|
69
|
-
|
|
70
75
|
await Promise.all([
|
|
71
|
-
m(
|
|
72
|
-
m(
|
|
76
|
+
m({ label: 'Fetch', userId: 1 }, () => fetchUser(1)),
|
|
77
|
+
m({ label: 'Fetch', userId: 2 }, () => fetchUser(2)),
|
|
73
78
|
]);
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
// Annotation
|
|
77
|
-
await measure('checkpoint
|
|
82
|
+
await measure('checkpoint');
|
|
78
83
|
```
|
|
79
84
|
|
|
80
85
|
### `measureSync(label, fn?)` — synchronous
|
|
81
86
|
|
|
82
87
|
```typescript
|
|
83
|
-
// Leaf
|
|
84
|
-
const
|
|
88
|
+
// Leaf — single line
|
|
89
|
+
const hash = measureSync('Hash', () => computeHash(data));
|
|
85
90
|
|
|
86
91
|
// With children — start + end
|
|
87
|
-
measureSync('
|
|
88
|
-
const data = m('Parse
|
|
92
|
+
measureSync('Report', (m) => {
|
|
93
|
+
const data = m('Parse', () => parse(raw));
|
|
89
94
|
return m('Summarize', () => summarize(data));
|
|
90
95
|
});
|
|
91
96
|
```
|
|
92
97
|
|
|
93
|
-
### `measure.
|
|
98
|
+
### `measure.wrap(label, fn)` — decorator
|
|
99
|
+
|
|
100
|
+
Wrap a function once, every call is measured:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const getUser = measure.wrap('Get user', fetchUser);
|
|
104
|
+
await getUser(1); // → [a] ... Get user → [a] ✓ Get user 82ms → {...}
|
|
105
|
+
await getUser(2); // → [b] ... Get user → [b] ✓ Get user 75ms → {...}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `measure.batch(label, items, fn, opts?)` — array processing with progress
|
|
94
109
|
|
|
95
110
|
```typescript
|
|
96
|
-
const
|
|
97
|
-
|
|
111
|
+
const results = await measure.batch('Process users', userIds, async (id) => {
|
|
112
|
+
return await processUser(id);
|
|
113
|
+
}, { every: 100 }); // log progress every 100 items
|
|
114
|
+
```
|
|
115
|
+
Output:
|
|
116
|
+
```
|
|
117
|
+
[a] ... Process users (500 items)
|
|
118
|
+
[a] = 100/500 (1.2s, 83/s)
|
|
119
|
+
[a] = 200/500 (2.1s, 95/s)
|
|
120
|
+
[a] ✓ Process users (500 items) 5.3s → "500/500 ok"
|
|
98
121
|
```
|
|
99
122
|
|
|
100
123
|
### `measure.retry(label, opts, fn)` — retry with backoff
|
|
101
124
|
|
|
102
125
|
```typescript
|
|
103
126
|
const result = await measure.retry('Flaky API', {
|
|
104
|
-
attempts: 3,
|
|
105
|
-
delay: 1000, // ms between retries
|
|
106
|
-
backoff: 2, // multiplier per retry
|
|
127
|
+
attempts: 3, delay: 1000, backoff: 2
|
|
107
128
|
}, () => fetchFlakyApi());
|
|
108
129
|
```
|
|
109
|
-
Output:
|
|
110
130
|
```
|
|
111
131
|
[a] ... Flaky API [1/3]
|
|
112
132
|
[a] ✗ Flaky API [1/3] 102ms (timeout)
|
|
@@ -116,69 +136,68 @@ Output:
|
|
|
116
136
|
|
|
117
137
|
### `measure.assert(label, fn)` — throw if null
|
|
118
138
|
|
|
119
|
-
Type-narrowing variant that re-throws on error instead of returning `null`:
|
|
120
|
-
|
|
121
139
|
```typescript
|
|
122
140
|
const user = await measure.assert('Get user', () => fetchUser(1));
|
|
123
|
-
//
|
|
141
|
+
// guaranteed non-null, or throws
|
|
124
142
|
```
|
|
125
143
|
|
|
126
|
-
###
|
|
127
|
-
|
|
128
|
-
Separate namespace and counter, avoids ID collisions across modules:
|
|
144
|
+
### Budget — warn on slow operations
|
|
129
145
|
|
|
130
146
|
```typescript
|
|
131
|
-
|
|
147
|
+
await measure({ label: 'DB query', budget: 100 }, async () => {
|
|
148
|
+
return await db.query('SELECT * FROM users');
|
|
149
|
+
});
|
|
150
|
+
// → [a] ✓ DB query 245ms → [...] ⚠ OVER BUDGET (100ms)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### `createMeasure(prefix)` — scoped instances
|
|
132
154
|
|
|
155
|
+
```typescript
|
|
133
156
|
const api = createMeasure('api');
|
|
134
157
|
const db = createMeasure('db');
|
|
135
158
|
|
|
136
159
|
await api.measure('GET /users', async () => {
|
|
137
|
-
return await db.measure('SELECT
|
|
160
|
+
return await db.measure('SELECT', () => query('...'));
|
|
138
161
|
});
|
|
139
162
|
// → [api:a] ... GET /users
|
|
140
|
-
// → [db:a]
|
|
141
|
-
// → [db:a] ✓ SELECT users 44ms → [...]
|
|
163
|
+
// → [db:a] ✓ SELECT 44ms → [...]
|
|
142
164
|
// → [api:a] ✓ GET /users 45ms → [...]
|
|
143
165
|
```
|
|
144
166
|
|
|
145
|
-
### `configure(opts)` — runtime
|
|
167
|
+
### `configure(opts)` — runtime config
|
|
146
168
|
|
|
147
169
|
```typescript
|
|
148
|
-
import { configure } from 'measure-fn';
|
|
149
|
-
|
|
150
170
|
configure({
|
|
151
171
|
silent: true, // suppress all output
|
|
152
172
|
timestamps: true, // prepend [HH:MM:SS.mmm]
|
|
153
|
-
maxResultLength: 200, // truncate results
|
|
173
|
+
maxResultLength: 200, // truncate results (default: 80)
|
|
154
174
|
logger: (event) => { // custom event handler
|
|
155
|
-
|
|
156
|
-
myTelemetryService.track(event);
|
|
175
|
+
myTelemetry.track(event);
|
|
157
176
|
}
|
|
158
177
|
});
|
|
159
178
|
```
|
|
160
179
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
### `resetCounter()`
|
|
180
|
+
Env: `MEASURE_SILENT=1`, `MEASURE_TIMESTAMPS=1`
|
|
164
181
|
|
|
165
|
-
|
|
182
|
+
### `measure.timed(label, fn?)` — programmatic timing
|
|
166
183
|
|
|
167
184
|
```typescript
|
|
168
|
-
|
|
169
|
-
beforeEach(() => {
|
|
170
|
-
resetCounter();
|
|
171
|
-
configure({ silent: false, logger: null, timestamps: false });
|
|
172
|
-
});
|
|
185
|
+
const { result, duration } = await measure.timed('Fetch', () => fetchUsers());
|
|
173
186
|
```
|
|
174
187
|
|
|
175
|
-
|
|
188
|
+
### Utilities
|
|
176
189
|
|
|
177
|
-
|
|
190
|
+
```typescript
|
|
191
|
+
import { safeStringify, formatDuration, resetCounter } from 'measure-fn';
|
|
178
192
|
|
|
193
|
+
safeStringify({ circular: self }); // handles circular refs, truncates
|
|
194
|
+
formatDuration(91234); // "1m 31s"
|
|
195
|
+
resetCounter(); // reset ID counter for tests
|
|
179
196
|
```
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
|
|
198
|
+
## Error Handling
|
|
199
|
+
|
|
200
|
+
**measure never throws** (except `.assert()`). On error: logs `✗`, returns `null`, prints stack to stderr.
|
|
182
201
|
|
|
183
202
|
## Types
|
|
184
203
|
|
|
@@ -186,11 +205,12 @@ beforeEach(() => {
|
|
|
186
205
|
export type MeasureEvent = {
|
|
187
206
|
type: 'start' | 'success' | 'error' | 'annotation';
|
|
188
207
|
id: string; label: string; depth: number;
|
|
189
|
-
duration?: number; result?: unknown;
|
|
190
|
-
|
|
208
|
+
duration?: number; result?: unknown; error?: unknown;
|
|
209
|
+
meta?: Record<string, unknown>; budget?: number;
|
|
191
210
|
};
|
|
192
211
|
export type TimedResult<T> = { result: T | null; duration: number };
|
|
193
212
|
export type RetryOpts = { attempts?: number; delay?: number; backoff?: number };
|
|
213
|
+
export type BatchOpts = { every?: number };
|
|
194
214
|
```
|
|
195
215
|
|
|
196
216
|
## Zero Dependencies
|
package/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Zero-dependency function performance measurement with hierarchical
|
|
|
5
5
|
|
|
6
6
|
# measure-fn — Structured Program Measurement
|
|
7
7
|
|
|
8
|
-
**Every program you write should be observable from day one.**
|
|
8
|
+
**Every program you write should be observable from day one.**
|
|
9
9
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
@@ -16,80 +16,74 @@ bun add measure-fn
|
|
|
16
16
|
## Output Format
|
|
17
17
|
|
|
18
18
|
```
|
|
19
|
-
[id] ... label — async
|
|
19
|
+
[id] ... label — start (async / sync with children)
|
|
20
20
|
[id] ✓ label Nms → val — success with auto-printed result
|
|
21
21
|
[id] ✗ label Nms (err) — error
|
|
22
|
-
[id] = label — annotation
|
|
22
|
+
[id] = label — annotation / progress
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
No indentation, no colors. IDs encode hierarchy.
|
|
25
|
+
No indentation, no colors. IDs encode hierarchy. Smart duration: `0.10ms` → `1.2s` → `2m 5s`.
|
|
26
26
|
|
|
27
|
-
## Core
|
|
28
|
-
|
|
29
|
-
### Label First, Function Second
|
|
27
|
+
## Core API
|
|
30
28
|
|
|
31
29
|
```typescript
|
|
32
|
-
import { measure, measureSync } from 'measure-fn';
|
|
30
|
+
import { measure, measureSync, createMeasure, configure } from 'measure-fn';
|
|
33
31
|
|
|
32
|
+
// Async
|
|
34
33
|
const users = await measure('Fetch users', () => fetchUsers());
|
|
35
|
-
const config = measureSync('Load config', () => loadConfig());
|
|
36
|
-
```
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
// Sync (leaf = single line)
|
|
36
|
+
const config = measureSync('Load config', () => loadConfig());
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
// Nested + parallel
|
|
41
39
|
await measure('Pipeline', async (m) => {
|
|
42
40
|
await Promise.all([
|
|
43
41
|
m({ label: 'Fetch', userId: 1 }, () => fetchUser(1)),
|
|
44
42
|
m({ label: 'Fetch', userId: 2 }, () => fetchUser(2)),
|
|
45
43
|
]);
|
|
46
44
|
});
|
|
47
|
-
```
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
// Wrap: decorator pattern — wrap once, measure every call
|
|
47
|
+
const getUser = measure.wrap('Get user', fetchUser);
|
|
48
|
+
await getUser(1);
|
|
49
|
+
await getUser(2);
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// Batch: process array with progress logging
|
|
52
|
+
await measure.batch('Process', items, async (item) => transform(item), { every: 100 });
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
// Retry: automatic retry with backoff
|
|
55
|
+
await measure.retry('Flaky', { attempts: 3, delay: 1000, backoff: 2 }, () => flakyApi());
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
await measure.
|
|
57
|
-
```
|
|
57
|
+
// Assert: throws if null (type-narrowing)
|
|
58
|
+
const user = await measure.assert('Get user', () => fetchUser(1));
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
// Budget: warn when operation exceeds time limit
|
|
61
|
+
await measure({ label: 'DB query', budget: 100 }, () => query());
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
const user = await measure.assert('Get user', () => fetchUser(1)); // throws if null
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Scoped Instances
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
import { createMeasure } from 'measure-fn';
|
|
63
|
+
// Scoped: separate namespace and counter
|
|
69
64
|
const api = createMeasure('api'); // → [api:a], [api:b], ...
|
|
70
65
|
const db = createMeasure('db'); // → [db:a], [db:b], ...
|
|
71
66
|
```
|
|
72
67
|
|
|
73
|
-
|
|
68
|
+
## Configuration
|
|
74
69
|
|
|
75
70
|
```typescript
|
|
76
|
-
|
|
71
|
+
configure({
|
|
72
|
+
silent: true, // suppress output
|
|
73
|
+
timestamps: true, // [HH:MM:SS.mmm] prefix
|
|
74
|
+
maxResultLength: 200, // result truncation (default: 80)
|
|
75
|
+
logger: (event) => ..., // custom event handler
|
|
76
|
+
});
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
Env: `MEASURE_SILENT=1`, `MEASURE_TIMESTAMPS=1`
|
|
80
|
+
|
|
81
|
+
## Utilities
|
|
80
82
|
|
|
81
83
|
```typescript
|
|
82
|
-
import {
|
|
83
|
-
configure({
|
|
84
|
-
silent: true, // suppress output
|
|
85
|
-
timestamps: true, // [HH:MM:SS.mmm] prefix
|
|
86
|
-
maxResultLength: 200, // result truncation (default: 80)
|
|
87
|
-
logger: (event) => ..., // custom event handler
|
|
88
|
-
});
|
|
84
|
+
import { safeStringify, formatDuration, resetCounter } from 'measure-fn';
|
|
89
85
|
```
|
|
90
86
|
|
|
91
|
-
Env vars: `MEASURE_SILENT=1`, `MEASURE_TIMESTAMPS=1`
|
|
92
|
-
|
|
93
87
|
## API Reference
|
|
94
88
|
|
|
95
89
|
| Export | Description |
|
|
@@ -98,12 +92,15 @@ Env vars: `MEASURE_SILENT=1`, `MEASURE_TIMESTAMPS=1`
|
|
|
98
92
|
| `measure.timed(label, fn?)` | Returns `{ result, duration }` |
|
|
99
93
|
| `measure.retry(label, opts, fn)` | Retry with backoff |
|
|
100
94
|
| `measure.assert(label, fn)` | Throws if null |
|
|
95
|
+
| `measure.wrap(label, fn)` | Returns measured version of fn |
|
|
96
|
+
| `measure.batch(label, items, fn, opts?)` | Array processing with progress |
|
|
101
97
|
| `measureSync(label, fn?)` | Sync measurement |
|
|
102
|
-
| `measureSync.timed
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `configure(opts)` | Set silent/timestamps/logger/maxResultLength |
|
|
98
|
+
| `measureSync.timed/assert/wrap` | Sync variants |
|
|
99
|
+
| `createMeasure(prefix)` | Scoped instance |
|
|
100
|
+
| `configure(opts)` | Runtime configuration |
|
|
106
101
|
| `resetCounter()` | Reset global ID counter |
|
|
102
|
+
| `safeStringify(value)` | Safe JSON with circular ref handling |
|
|
103
|
+
| `formatDuration(ms)` | Smart duration formatting |
|
|
107
104
|
|
|
108
105
|
## Testing
|
|
109
106
|
|
package/bench.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
import { measure, measureSync, configure } from "./index.ts";
|
|
3
|
+
|
|
4
|
+
const ITERATIONS = 100_000;
|
|
5
|
+
|
|
6
|
+
function noop() { }
|
|
7
|
+
|
|
8
|
+
async function run() {
|
|
9
|
+
console.log(`Running benchmark with ${ITERATIONS.toLocaleString()} iterations...`);
|
|
10
|
+
configure({ silent: true }); // Disable logging to measure pure overhead
|
|
11
|
+
|
|
12
|
+
// Baseline
|
|
13
|
+
const startBase = performance.now();
|
|
14
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
15
|
+
noop();
|
|
16
|
+
}
|
|
17
|
+
const timeBase = performance.now() - startBase;
|
|
18
|
+
const perOpBase = timeBase / ITERATIONS;
|
|
19
|
+
console.log(`Baseline (noop): ${(timeBase).toFixed(2)}ms total, ${perOpBase.toFixed(6)}ms/op`);
|
|
20
|
+
|
|
21
|
+
// measureSync overhead
|
|
22
|
+
const startSync = performance.now();
|
|
23
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
24
|
+
measureSync('test', noop);
|
|
25
|
+
}
|
|
26
|
+
const timeSync = performance.now() - startSync;
|
|
27
|
+
const overheadSync = (timeSync - timeBase) / ITERATIONS;
|
|
28
|
+
console.log(`measureSync: ${(timeSync).toFixed(2)}ms total, ${overheadSync.toFixed(6)}ms overhead/op`);
|
|
29
|
+
|
|
30
|
+
// measure (async) overhead
|
|
31
|
+
const startAsync = performance.now();
|
|
32
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
33
|
+
await measure('test', async () => { });
|
|
34
|
+
}
|
|
35
|
+
const timeAsync = performance.now() - startAsync;
|
|
36
|
+
const overheadAsync = (timeAsync - timeBase) / ITERATIONS;
|
|
37
|
+
console.log(`measure (async): ${(timeAsync).toFixed(2)}ms total, ${overheadAsync.toFixed(6)}ms overhead/op`);
|
|
38
|
+
|
|
39
|
+
// Nested overhead (depth 3)
|
|
40
|
+
const startNested = performance.now();
|
|
41
|
+
for (let i = 0; i < ITERATIONS / 10; i++) { // reduce iterations
|
|
42
|
+
measureSync('root', (m) => {
|
|
43
|
+
m('child', (m2) => {
|
|
44
|
+
m2('leaf', noop);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const timeNested = performance.now() - startNested;
|
|
49
|
+
// 3 measurements per iteration
|
|
50
|
+
const perOpNested = timeNested / (ITERATIONS / 10);
|
|
51
|
+
console.log(`Nested (depth 3): ${(timeNested).toFixed(2)}ms total, ${perOpNested.toFixed(6)}ms per tree (3 ops)`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
run();
|
package/example.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { measure, measureSync, configure, createMeasure, type MeasureFn } from "./index.ts";
|
|
1
|
+
import { measure, measureSync, configure, createMeasure, safeStringify, type MeasureFn } from "./index.ts";
|
|
2
2
|
|
|
3
3
|
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
4
4
|
|
|
@@ -11,14 +11,13 @@ async function fetchUser(userId: number) {
|
|
|
11
11
|
async function flakyApi() {
|
|
12
12
|
await sleep(30);
|
|
13
13
|
if (Math.random() < 0.6) throw new Error('Service unavailable');
|
|
14
|
-
return { status: 'ok'
|
|
14
|
+
return { status: 'ok' };
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
async function main() {
|
|
18
|
-
// Enable timestamps for production-style output
|
|
19
18
|
configure({ timestamps: true });
|
|
20
19
|
|
|
21
|
-
// ─── Sync leaf: single line + result
|
|
20
|
+
// ─── Sync leaf: single line + auto-printed result ──────────────
|
|
22
21
|
measureSync('Load config', () => ({ env: 'prod', port: 3000 }));
|
|
23
22
|
|
|
24
23
|
// ─── Annotation ────────────────────────────────────────────────
|
|
@@ -31,17 +30,14 @@ async function main() {
|
|
|
31
30
|
return { rows, count: rows?.length ?? 0 };
|
|
32
31
|
});
|
|
33
32
|
|
|
34
|
-
// ─── Circular
|
|
33
|
+
// ─── Circular ref (safe stringify) ─────────────────────────────
|
|
35
34
|
measureSync('Circular ref', () => {
|
|
36
35
|
const obj: any = { name: 'root' };
|
|
37
36
|
obj.self = obj;
|
|
38
37
|
return obj;
|
|
39
38
|
});
|
|
40
39
|
|
|
41
|
-
// ───
|
|
42
|
-
measureSync('Long result', () => ({ data: 'x'.repeat(200) }));
|
|
43
|
-
|
|
44
|
-
// ─── Parallel async (interleaved output) ───────────────────────
|
|
40
|
+
// ─── Parallel async (interleaved) ──────────────────────────────
|
|
45
41
|
await measure('Parallel Fetch', async (m) => {
|
|
46
42
|
await Promise.all([
|
|
47
43
|
m({ label: 'Fetch User', userId: 1 }, () => fetchUser(1)),
|
|
@@ -50,18 +46,31 @@ async function main() {
|
|
|
50
46
|
]);
|
|
51
47
|
});
|
|
52
48
|
|
|
53
|
-
// ───
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
console.log('Assert caught:', e);
|
|
59
|
-
}
|
|
49
|
+
// ─── Budget: warn when operation exceeds time limit ────────────
|
|
50
|
+
await measure({ label: 'DB query', budget: 30 }, async () => {
|
|
51
|
+
await sleep(80); // intentionally slow
|
|
52
|
+
return { rows: 42 };
|
|
53
|
+
});
|
|
60
54
|
|
|
61
|
-
// ───
|
|
55
|
+
// ─── Retry with backoff ────────────────────────────────────────
|
|
62
56
|
await measure.retry('Flaky API', { attempts: 3, delay: 100, backoff: 2 }, flakyApi);
|
|
63
57
|
|
|
64
|
-
// ───
|
|
58
|
+
// ─── Assert: throws if null ────────────────────────────────────
|
|
59
|
+
const user = await measure.assert('Assert user', () => fetchUser(1));
|
|
60
|
+
console.log(`Asserted: ${user.name}`);
|
|
61
|
+
|
|
62
|
+
// ─── Wrap: decorator pattern ───────────────────────────────────
|
|
63
|
+
const getUser = measure.wrap('Get user', fetchUser);
|
|
64
|
+
await getUser(1);
|
|
65
|
+
await getUser(2);
|
|
66
|
+
|
|
67
|
+
// ─── Batch: process array with progress ────────────────────────
|
|
68
|
+
const userIds = Array.from({ length: 20 }, (_, i) => i + 1);
|
|
69
|
+
await measure.batch('Fetch all users', userIds, async (id) => {
|
|
70
|
+
return await fetchUser(id);
|
|
71
|
+
}, { every: 5 });
|
|
72
|
+
|
|
73
|
+
// ─── Scoped instances ──────────────────────────────────────────
|
|
65
74
|
const api = createMeasure('api');
|
|
66
75
|
const db = createMeasure('db');
|
|
67
76
|
|
|
@@ -72,9 +81,21 @@ async function main() {
|
|
|
72
81
|
});
|
|
73
82
|
});
|
|
74
83
|
|
|
75
|
-
// ───
|
|
76
|
-
const
|
|
77
|
-
|
|
84
|
+
// ─── safeStringify utility ─────────────────────────────────────
|
|
85
|
+
const circular: any = { a: 1 };
|
|
86
|
+
circular.self = circular;
|
|
87
|
+
console.log(`safeStringify: ${safeStringify(circular)}`);
|
|
88
|
+
|
|
89
|
+
// ─── Smart duration formatting (simulated long op) ─────────────
|
|
90
|
+
await measure('Quick op', async () => {
|
|
91
|
+
await sleep(5);
|
|
92
|
+
return 'fast';
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await measure('Slow op', async () => {
|
|
96
|
+
await sleep(1200);
|
|
97
|
+
return 'slow';
|
|
98
|
+
});
|
|
78
99
|
}
|
|
79
100
|
|
|
80
101
|
main().then(() => console.log('\n✅ Done.'));
|