High-precision Python performance measurement toolkit. Nanosecond accuracy. Zero dependencies. Minimal output.
pip install nanowatch
Or from source:
pip install -e .
from nanowatch import watch
@watch
def compute(n):
return sum(range(n))
# With a custom label
@watch("heavy computation")
def compute(n):
return sum(range(n))
# Async functions work identically
@watch
async def fetch_data(url):
...Output:
compute 1.243 ms
from nanowatch import watch_block
with watch_block("db query"):
results = db.execute(sql)from nanowatch import watch_call
data = watch_call(json.loads, raw_string, name="parse response")from nanowatch import WatchedMixin
class UserService(WatchedMixin):
_watch_prefix = "UserService" # optional, defaults to class name
def fetch_user(self, user_id):
...
def save_user(self, user):
...
def _internal_helper(self): # skipped (underscore prefix)
...Every call to fetch_user or save_user is automatically timed.
Custom collector via DI:
from nanowatch import WatchedMixin, Collector
my_collector = Collector()
class OrderService(WatchedMixin):
_watch_collector = my_collector
_watch_prefix = "OrderService"
def create_order(self, data):
...from nanowatch import WsgiMiddleware
# Flask
app.wsgi_app = WsgiMiddleware(app.wsgi_app)
# Django (in wsgi.py)
application = WsgiMiddleware(get_wsgi_application())Output per request:
HTTP GET /api/users 4.231 ms [method=GET, path=/api/users]
from nanowatch import AsgiMiddleware
# FastAPI
app.add_middleware(AsgiMiddleware)
# Or manually wrap
app = AsgiMiddleware(app)Measures time between named points inside a function.
from nanowatch import LineProfiler
def process_order(order):
prof = LineProfiler("process_order")
validate(order)
prof.mark("validated")
result = db.save(order)
prof.mark("saved to db")
notify(order)
prof.mark("notified")
prof.finish()Output:
process_order | validated 312 us [session=process_order, checkpoint=validated]
process_order | saved to db 2.841 ms [session=process_order, checkpoint=saved to db]
process_order | notified 1.102 ms [session=process_order, checkpoint=notified]
[nanowatch] session 'process_order' complete (3 checkpoints)
------------------------------------------------------------------------
import nanowatch
# ... run your code ...
nanowatch.summary()Output:
========================================================================
nanowatch | Performance Summary
2025-06-01 14:32:10
========================================================================
UserService.fetch_user
calls : 48
min : 812 us
max : 4.231 ms
avg : 1.103 ms
total : 52.944 ms
------------------------------------------------------------------------
HTTP GET /api/users 4.231 ms [method=GET, path=/api/users]
------------------------------------------------------------------------
Total tracked time : 1.204 s
Total measurements : 57
========================================================================
nanowatch.save("perf_results.json"){
"generated_at": "2025-06-01T14:32:10.123456",
"total_measurements": 57,
"records": [
{
"name": "UserService.fetch_user",
"duration_ns": 1103000,
"duration_us": 1103.0,
"duration_ms": 1.103,
"duration_s": 0.001103,
"context": {}
}
],
"groups": {
"UserService.fetch_user": {
"count": 48,
"min_ns": 812000,
"max_ns": 4231000,
"avg_ns": 1103000,
"total_ns": 52944000
}
}
}All interfaces accept an optional collector parameter for DI:
from nanowatch import watch, Collector
test_collector = Collector()
@watch(collector=test_collector)
def my_fn():
...
my_fn()
print(test_collector.stats("my_fn"))nanowatch.reset() # clears the global collectorAll measurements use time.perf_counter_ns, Python's highest-resolution
monotonic clock. Results are stored as raw integers (nanoseconds) and
converted only for display.
src/nanowatch/
core/
timer.py # Timer, TimingRecord
collector.py # Collector, default_collector
interfaces/
decorators.py # @watch, watch_block, watch_call
mixin.py # WatchedMixin
middleware.py # WsgiMiddleware, AsgiMiddleware
line_profiler.py # LineProfiler
output/
formatter.py # console + file output