nanowatch 0.1.0__tar.gz
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.
- nanowatch-0.1.0/LICENSE.md +19 -0
- nanowatch-0.1.0/PKG-INFO +313 -0
- nanowatch-0.1.0/README.md +289 -0
- nanowatch-0.1.0/pyproject.toml +37 -0
- nanowatch-0.1.0/setup.cfg +4 -0
- nanowatch-0.1.0/src/nanowatch/__init__.py +69 -0
- nanowatch-0.1.0/src/nanowatch/core/__init__.py +3 -0
- nanowatch-0.1.0/src/nanowatch/core/collector.py +71 -0
- nanowatch-0.1.0/src/nanowatch/core/timer.py +108 -0
- nanowatch-0.1.0/src/nanowatch/interfaces/__init__.py +5 -0
- nanowatch-0.1.0/src/nanowatch/interfaces/decorators.py +144 -0
- nanowatch-0.1.0/src/nanowatch/interfaces/line_profiler.py +80 -0
- nanowatch-0.1.0/src/nanowatch/interfaces/middleware.py +112 -0
- nanowatch-0.1.0/src/nanowatch/interfaces/mixin.py +111 -0
- nanowatch-0.1.0/src/nanowatch/output/__init__.py +2 -0
- nanowatch-0.1.0/src/nanowatch/output/formatter.py +123 -0
- nanowatch-0.1.0/src/nanowatch.egg-info/PKG-INFO +313 -0
- nanowatch-0.1.0/src/nanowatch.egg-info/SOURCES.txt +20 -0
- nanowatch-0.1.0/src/nanowatch.egg-info/dependency_links.txt +1 -0
- nanowatch-0.1.0/src/nanowatch.egg-info/requires.txt +4 -0
- nanowatch-0.1.0/src/nanowatch.egg-info/top_level.txt +1 -0
- nanowatch-0.1.0/tests/test_nanowatch.py +287 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
nanowatch-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nanowatch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: High-precision Python performance measurement toolkit
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: performance,profiling,timing,benchmark,measurement
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Software Development
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE.md
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# nanowatch
|
|
26
|
+
|
|
27
|
+
High-precision Python performance measurement toolkit.
|
|
28
|
+
Nanosecond accuracy. Zero dependencies. Minimal output.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
pip install nanowatch
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or from source:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
pip install -e .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Interfaces
|
|
47
|
+
|
|
48
|
+
### 1. Function Decorator
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from nanowatch import watch
|
|
52
|
+
|
|
53
|
+
@watch
|
|
54
|
+
def compute(n):
|
|
55
|
+
return sum(range(n))
|
|
56
|
+
|
|
57
|
+
# With a custom label
|
|
58
|
+
@watch("heavy computation")
|
|
59
|
+
def compute(n):
|
|
60
|
+
return sum(range(n))
|
|
61
|
+
|
|
62
|
+
# Async functions work identically
|
|
63
|
+
@watch
|
|
64
|
+
async def fetch_data(url):
|
|
65
|
+
...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Output:**
|
|
69
|
+
```
|
|
70
|
+
compute 1.243 ms
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### 2. Code Block Context Manager
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from nanowatch import watch_block
|
|
79
|
+
|
|
80
|
+
with watch_block("db query"):
|
|
81
|
+
results = db.execute(sql)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### 3. Inline Call (no decorator needed)
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from nanowatch import watch_call
|
|
90
|
+
|
|
91
|
+
data = watch_call(json.loads, raw_string, name="parse response")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### 4. Class Mixin (auto-instruments all public methods)
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from nanowatch import WatchedMixin
|
|
100
|
+
|
|
101
|
+
class UserService(WatchedMixin):
|
|
102
|
+
_watch_prefix = "UserService" # optional, defaults to class name
|
|
103
|
+
|
|
104
|
+
def fetch_user(self, user_id):
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
def save_user(self, user):
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
def _internal_helper(self): # skipped (underscore prefix)
|
|
111
|
+
...
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Every call to `fetch_user` or `save_user` is automatically timed.
|
|
115
|
+
|
|
116
|
+
**Custom collector via DI:**
|
|
117
|
+
```python
|
|
118
|
+
from nanowatch import WatchedMixin, Collector
|
|
119
|
+
|
|
120
|
+
my_collector = Collector()
|
|
121
|
+
|
|
122
|
+
class OrderService(WatchedMixin):
|
|
123
|
+
_watch_collector = my_collector
|
|
124
|
+
_watch_prefix = "OrderService"
|
|
125
|
+
|
|
126
|
+
def create_order(self, data):
|
|
127
|
+
...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### 5. WSGI Middleware (Flask, Django, etc.)
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from nanowatch import WsgiMiddleware
|
|
136
|
+
|
|
137
|
+
# Flask
|
|
138
|
+
app.wsgi_app = WsgiMiddleware(app.wsgi_app)
|
|
139
|
+
|
|
140
|
+
# Django (in wsgi.py)
|
|
141
|
+
application = WsgiMiddleware(get_wsgi_application())
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Output per request:**
|
|
145
|
+
```
|
|
146
|
+
HTTP GET /api/users 4.231 ms [method=GET, path=/api/users]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### 6. ASGI Middleware (FastAPI, Starlette, etc.)
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from nanowatch import AsgiMiddleware
|
|
155
|
+
|
|
156
|
+
# FastAPI
|
|
157
|
+
app.add_middleware(AsgiMiddleware)
|
|
158
|
+
|
|
159
|
+
# Or manually wrap
|
|
160
|
+
app = AsgiMiddleware(app)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### 7. Line Profiler (checkpoint-based)
|
|
166
|
+
|
|
167
|
+
Measures time between named points inside a function.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from nanowatch import LineProfiler
|
|
171
|
+
|
|
172
|
+
def process_order(order):
|
|
173
|
+
prof = LineProfiler("process_order")
|
|
174
|
+
|
|
175
|
+
validate(order)
|
|
176
|
+
prof.mark("validated")
|
|
177
|
+
|
|
178
|
+
result = db.save(order)
|
|
179
|
+
prof.mark("saved to db")
|
|
180
|
+
|
|
181
|
+
notify(order)
|
|
182
|
+
prof.mark("notified")
|
|
183
|
+
|
|
184
|
+
prof.finish()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Output:**
|
|
188
|
+
```
|
|
189
|
+
process_order | validated 312 us [session=process_order, checkpoint=validated]
|
|
190
|
+
process_order | saved to db 2.841 ms [session=process_order, checkpoint=saved to db]
|
|
191
|
+
process_order | notified 1.102 ms [session=process_order, checkpoint=notified]
|
|
192
|
+
[nanowatch] session 'process_order' complete (3 checkpoints)
|
|
193
|
+
------------------------------------------------------------------------
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Reports
|
|
199
|
+
|
|
200
|
+
### Print summary to stdout
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
import nanowatch
|
|
204
|
+
|
|
205
|
+
# ... run your code ...
|
|
206
|
+
|
|
207
|
+
nanowatch.summary()
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Output:**
|
|
211
|
+
```
|
|
212
|
+
========================================================================
|
|
213
|
+
nanowatch | Performance Summary
|
|
214
|
+
2025-06-01 14:32:10
|
|
215
|
+
========================================================================
|
|
216
|
+
UserService.fetch_user
|
|
217
|
+
calls : 48
|
|
218
|
+
min : 812 us
|
|
219
|
+
max : 4.231 ms
|
|
220
|
+
avg : 1.103 ms
|
|
221
|
+
total : 52.944 ms
|
|
222
|
+
------------------------------------------------------------------------
|
|
223
|
+
HTTP GET /api/users 4.231 ms [method=GET, path=/api/users]
|
|
224
|
+
------------------------------------------------------------------------
|
|
225
|
+
Total tracked time : 1.204 s
|
|
226
|
+
Total measurements : 57
|
|
227
|
+
========================================================================
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Save to JSON file
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
nanowatch.save("perf_results.json")
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"generated_at": "2025-06-01T14:32:10.123456",
|
|
239
|
+
"total_measurements": 57,
|
|
240
|
+
"records": [
|
|
241
|
+
{
|
|
242
|
+
"name": "UserService.fetch_user",
|
|
243
|
+
"duration_ns": 1103000,
|
|
244
|
+
"duration_us": 1103.0,
|
|
245
|
+
"duration_ms": 1.103,
|
|
246
|
+
"duration_s": 0.001103,
|
|
247
|
+
"context": {}
|
|
248
|
+
}
|
|
249
|
+
],
|
|
250
|
+
"groups": {
|
|
251
|
+
"UserService.fetch_user": {
|
|
252
|
+
"count": 48,
|
|
253
|
+
"min_ns": 812000,
|
|
254
|
+
"max_ns": 4231000,
|
|
255
|
+
"avg_ns": 1103000,
|
|
256
|
+
"total_ns": 52944000
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Custom Collector (Isolation / Testing)
|
|
265
|
+
|
|
266
|
+
All interfaces accept an optional `collector` parameter for DI:
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from nanowatch import watch, Collector
|
|
270
|
+
|
|
271
|
+
test_collector = Collector()
|
|
272
|
+
|
|
273
|
+
@watch(collector=test_collector)
|
|
274
|
+
def my_fn():
|
|
275
|
+
...
|
|
276
|
+
|
|
277
|
+
my_fn()
|
|
278
|
+
print(test_collector.stats("my_fn"))
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Reset
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
nanowatch.reset() # clears the global collector
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Precision
|
|
292
|
+
|
|
293
|
+
All measurements use `time.perf_counter_ns`, Python's highest-resolution
|
|
294
|
+
monotonic clock. Results are stored as raw integers (nanoseconds) and
|
|
295
|
+
converted only for display.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Project Structure
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
src/nanowatch/
|
|
303
|
+
core/
|
|
304
|
+
timer.py # Timer, TimingRecord
|
|
305
|
+
collector.py # Collector, default_collector
|
|
306
|
+
interfaces/
|
|
307
|
+
decorators.py # @watch, watch_block, watch_call
|
|
308
|
+
mixin.py # WatchedMixin
|
|
309
|
+
middleware.py # WsgiMiddleware, AsgiMiddleware
|
|
310
|
+
line_profiler.py # LineProfiler
|
|
311
|
+
output/
|
|
312
|
+
formatter.py # console + file output
|
|
313
|
+
```
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# nanowatch
|
|
2
|
+
|
|
3
|
+
High-precision Python performance measurement toolkit.
|
|
4
|
+
Nanosecond accuracy. Zero dependencies. Minimal output.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
pip install nanowatch
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or from source:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
pip install -e .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Interfaces
|
|
23
|
+
|
|
24
|
+
### 1. Function Decorator
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from nanowatch import watch
|
|
28
|
+
|
|
29
|
+
@watch
|
|
30
|
+
def compute(n):
|
|
31
|
+
return sum(range(n))
|
|
32
|
+
|
|
33
|
+
# With a custom label
|
|
34
|
+
@watch("heavy computation")
|
|
35
|
+
def compute(n):
|
|
36
|
+
return sum(range(n))
|
|
37
|
+
|
|
38
|
+
# Async functions work identically
|
|
39
|
+
@watch
|
|
40
|
+
async def fetch_data(url):
|
|
41
|
+
...
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Output:**
|
|
45
|
+
```
|
|
46
|
+
compute 1.243 ms
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### 2. Code Block Context Manager
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from nanowatch import watch_block
|
|
55
|
+
|
|
56
|
+
with watch_block("db query"):
|
|
57
|
+
results = db.execute(sql)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### 3. Inline Call (no decorator needed)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from nanowatch import watch_call
|
|
66
|
+
|
|
67
|
+
data = watch_call(json.loads, raw_string, name="parse response")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### 4. Class Mixin (auto-instruments all public methods)
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from nanowatch import WatchedMixin
|
|
76
|
+
|
|
77
|
+
class UserService(WatchedMixin):
|
|
78
|
+
_watch_prefix = "UserService" # optional, defaults to class name
|
|
79
|
+
|
|
80
|
+
def fetch_user(self, user_id):
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
def save_user(self, user):
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
def _internal_helper(self): # skipped (underscore prefix)
|
|
87
|
+
...
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Every call to `fetch_user` or `save_user` is automatically timed.
|
|
91
|
+
|
|
92
|
+
**Custom collector via DI:**
|
|
93
|
+
```python
|
|
94
|
+
from nanowatch import WatchedMixin, Collector
|
|
95
|
+
|
|
96
|
+
my_collector = Collector()
|
|
97
|
+
|
|
98
|
+
class OrderService(WatchedMixin):
|
|
99
|
+
_watch_collector = my_collector
|
|
100
|
+
_watch_prefix = "OrderService"
|
|
101
|
+
|
|
102
|
+
def create_order(self, data):
|
|
103
|
+
...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### 5. WSGI Middleware (Flask, Django, etc.)
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from nanowatch import WsgiMiddleware
|
|
112
|
+
|
|
113
|
+
# Flask
|
|
114
|
+
app.wsgi_app = WsgiMiddleware(app.wsgi_app)
|
|
115
|
+
|
|
116
|
+
# Django (in wsgi.py)
|
|
117
|
+
application = WsgiMiddleware(get_wsgi_application())
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Output per request:**
|
|
121
|
+
```
|
|
122
|
+
HTTP GET /api/users 4.231 ms [method=GET, path=/api/users]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### 6. ASGI Middleware (FastAPI, Starlette, etc.)
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from nanowatch import AsgiMiddleware
|
|
131
|
+
|
|
132
|
+
# FastAPI
|
|
133
|
+
app.add_middleware(AsgiMiddleware)
|
|
134
|
+
|
|
135
|
+
# Or manually wrap
|
|
136
|
+
app = AsgiMiddleware(app)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### 7. Line Profiler (checkpoint-based)
|
|
142
|
+
|
|
143
|
+
Measures time between named points inside a function.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from nanowatch import LineProfiler
|
|
147
|
+
|
|
148
|
+
def process_order(order):
|
|
149
|
+
prof = LineProfiler("process_order")
|
|
150
|
+
|
|
151
|
+
validate(order)
|
|
152
|
+
prof.mark("validated")
|
|
153
|
+
|
|
154
|
+
result = db.save(order)
|
|
155
|
+
prof.mark("saved to db")
|
|
156
|
+
|
|
157
|
+
notify(order)
|
|
158
|
+
prof.mark("notified")
|
|
159
|
+
|
|
160
|
+
prof.finish()
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Output:**
|
|
164
|
+
```
|
|
165
|
+
process_order | validated 312 us [session=process_order, checkpoint=validated]
|
|
166
|
+
process_order | saved to db 2.841 ms [session=process_order, checkpoint=saved to db]
|
|
167
|
+
process_order | notified 1.102 ms [session=process_order, checkpoint=notified]
|
|
168
|
+
[nanowatch] session 'process_order' complete (3 checkpoints)
|
|
169
|
+
------------------------------------------------------------------------
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Reports
|
|
175
|
+
|
|
176
|
+
### Print summary to stdout
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
import nanowatch
|
|
180
|
+
|
|
181
|
+
# ... run your code ...
|
|
182
|
+
|
|
183
|
+
nanowatch.summary()
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Output:**
|
|
187
|
+
```
|
|
188
|
+
========================================================================
|
|
189
|
+
nanowatch | Performance Summary
|
|
190
|
+
2025-06-01 14:32:10
|
|
191
|
+
========================================================================
|
|
192
|
+
UserService.fetch_user
|
|
193
|
+
calls : 48
|
|
194
|
+
min : 812 us
|
|
195
|
+
max : 4.231 ms
|
|
196
|
+
avg : 1.103 ms
|
|
197
|
+
total : 52.944 ms
|
|
198
|
+
------------------------------------------------------------------------
|
|
199
|
+
HTTP GET /api/users 4.231 ms [method=GET, path=/api/users]
|
|
200
|
+
------------------------------------------------------------------------
|
|
201
|
+
Total tracked time : 1.204 s
|
|
202
|
+
Total measurements : 57
|
|
203
|
+
========================================================================
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Save to JSON file
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
nanowatch.save("perf_results.json")
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"generated_at": "2025-06-01T14:32:10.123456",
|
|
215
|
+
"total_measurements": 57,
|
|
216
|
+
"records": [
|
|
217
|
+
{
|
|
218
|
+
"name": "UserService.fetch_user",
|
|
219
|
+
"duration_ns": 1103000,
|
|
220
|
+
"duration_us": 1103.0,
|
|
221
|
+
"duration_ms": 1.103,
|
|
222
|
+
"duration_s": 0.001103,
|
|
223
|
+
"context": {}
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
"groups": {
|
|
227
|
+
"UserService.fetch_user": {
|
|
228
|
+
"count": 48,
|
|
229
|
+
"min_ns": 812000,
|
|
230
|
+
"max_ns": 4231000,
|
|
231
|
+
"avg_ns": 1103000,
|
|
232
|
+
"total_ns": 52944000
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Custom Collector (Isolation / Testing)
|
|
241
|
+
|
|
242
|
+
All interfaces accept an optional `collector` parameter for DI:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from nanowatch import watch, Collector
|
|
246
|
+
|
|
247
|
+
test_collector = Collector()
|
|
248
|
+
|
|
249
|
+
@watch(collector=test_collector)
|
|
250
|
+
def my_fn():
|
|
251
|
+
...
|
|
252
|
+
|
|
253
|
+
my_fn()
|
|
254
|
+
print(test_collector.stats("my_fn"))
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Reset
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
nanowatch.reset() # clears the global collector
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Precision
|
|
268
|
+
|
|
269
|
+
All measurements use `time.perf_counter_ns`, Python's highest-resolution
|
|
270
|
+
monotonic clock. Results are stored as raw integers (nanoseconds) and
|
|
271
|
+
converted only for display.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Project Structure
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
src/nanowatch/
|
|
279
|
+
core/
|
|
280
|
+
timer.py # Timer, TimingRecord
|
|
281
|
+
collector.py # Collector, default_collector
|
|
282
|
+
interfaces/
|
|
283
|
+
decorators.py # @watch, watch_block, watch_call
|
|
284
|
+
mixin.py # WatchedMixin
|
|
285
|
+
middleware.py # WsgiMiddleware, AsgiMiddleware
|
|
286
|
+
line_profiler.py # LineProfiler
|
|
287
|
+
output/
|
|
288
|
+
formatter.py # console + file output
|
|
289
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nanowatch"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "High-precision Python performance measurement toolkit"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["performance", "profiling", "timing", "benchmark", "measurement"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
"Topic :: Utilities",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest", "pytest-asyncio"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-dir]
|
|
33
|
+
"" = "src"
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
asyncio_mode = "auto"
|
|
37
|
+
testpaths = ["tests"]
|