See through your code. Lightweight execution profiler for Python.
Version: 0.5.0
Author: Serg Parf sergey.porfiriev@gmail.com
Xray traces function calls, measures timing, tracks memory, and captures parameters — then renders a Call Tree showing exactly what happened, how long each step took, and where the bottlenecks are.
Xray can profile existing code without modifying a single line of source.
Use Xray.patch() to inject profiling into any third-party library, framework,
or legacy class at runtime — database drivers, HTTP clients, ORM models,
API wrappers. Just call Xray.patch(SomeClass, 'method') at startup and
every call to that method is automatically traced with timing, call site,
and parameters. No decorators, no context managers, no refactoring needed.
- decorator (method):
@Xray.profile()— auto-profile a function - decorator (method):
@Xray.profile('name')— with custom name - decorator (class):
@Xray.trace_class()— auto-profile all public methods - decorator (class):
@Xray.trace_class(methods=['find'])— specific methods only - runtime patch:
Xray.patch(AnyClass, 'method')— instrument existing code without changes Xray.info()/warning()/alert()— events, checkpoints, error markers- Multi-worker — Redis-backed, thread-safe; multiple processes share one execution trace
- Zero overhead — disabled Xray returns no-op objects, no
ifguards needed
- Web — auto-injected HTML panel with Call Tree table, typed params, expand/collapse, color-coded timing, warning/alert badges, and compact Gantt-like Coverage map
- JSON —
/_profiler/json?k=KEYendpoint returns raw entries as JSON - CLI —
Xray.report()prints color-coded tree grouped by worker with Top 5 slowest - CLI instant — real-time stderr with nested outline, shows every
in/outas it happens
fromxrayimportXrayimportredis# If you want profiling:Xray.init(redis.Redis(host='redis')) # task_id auto-generated# If you do not want profiling:# do nothing, or call Xray.init(False) explicitly# Xray.init() is only needed when you want profiling ON.# Without init(), Xray.i(), decorators, and patched methods are safe no-ops.# Xray.init(False) is the explicit equivalent.# Overhead = ZERO, so instrumentation can stay in the code.# In enabled mode, overhead is minimal.withXray.i('ES::search', {'query': q}):
results=es.search(q)
# work happens as usual; data is recorded only when profiling is enabled# Context manager — recommendedwithXray.i('section-name', {'key': 'val'}) asspan:
result=do_work()
span.data({'rows': len(result)}) # add data mid-execution# Auto-name from caller (Class.method)withXray.i() asspan:
...When profiler is disabled, Xray.i() returns a no-op — safe to use without checks.
Special data keys:
requestresponse
They are only special for report presentation.
In the web/CLI reports, these two keys are rendered separately instead of being mixed into the inline data fields.
Storage, JSON output, and profiler semantics stay exactly the same as for any other data keys.
withXray.i('AI::classify', {'request': {'text': q, 'model': 'gpt-4o-mini'}}) asspan:
resp=classify(q)
span.data({'response': resp})@Xray.profile() # auto-name: Class.methoddeffind_listings(params): ...
@Xray.profile('custom-name') # explicit namedefhelper(): ...Auto-profile all (or specific) methods of a class. Each call creates a span
named ClassName.method_name. Private methods (_name) are skipped by default.
# All public methods — every call to find/enrich/save is auto-profiled@Xray.trace_class()classSearchService:
deffind(self, q): ... # → span "SearchService.find"defenrich(self, data): ... # → span "SearchService.enrich"defsave(self, item): ... # → span "SearchService.save"def_internal(self): ... # skipped (private)# Specific methods only@Xray.trace_class(methods=['find', 'save'])classSearchService:
deffind(self, q): ... # profileddefsave(self, item): ... # profileddefenrich(self, data): ... # NOT profiled# Include private methods too@Xray.trace_class(skip_private=False)classService:
defrun(self): ... # profileddef_setup(self): ... # profiled (skip_private=False)Useful for instrumenting service classes, repositories, and API clients
without adding with Xray.i() to every method.
Inject profiling into any existing class at runtime — no source changes required. Works on third-party libraries, framework internals, legacy code.
fromelasticsearchimportElasticsearchfrommyapp.dbimportDatabasePoolfrommyapp.cacheimportRedisCache# Single methodXray.patch(Elasticsearch, 'search')
# Multiple methodsXray.patch(Elasticsearch, ['search', 'index', 'delete'])
# All public methodsXray.patch(DatabasePool)
Xray.patch(RedisCache)Call Xray.patch() once at application startup. Every subsequent call to the
patched methods is automatically profiled — no changes to the original code.
result=Xray.wrap(lambda: api_call(url), 'API::call', {'url': url})Xray.info('cache-hit', {'key': k})
Xray.warning('rate-limit', {'remaining': 5})
Xray.alert('timeout', {'url': url, 'after_ms': 5000})# Redis mode — store entries, read report laterXray.init(redis_client) # task_id auto-generatedXray.init(redis_client, 'my-task-123') # explicit task_idXray.init(redis_client, thread_id='worker-1') # explicit thread_idXray.init(False) # explicit disabled mode (same as "not initialized")# Access current task_idprint(Xray.task_id()) # 'xray-a1b2c3d4' or 'my-task-123'# Instant mode — real-time stderr outputXray.init_instant()
# Close + disableXray.finish() # close root span (also called by atexit)Xray.disable() # finish + disabletask_id auto-generates as xray-{8 hex chars} when not provided.
thread_id defaults to threading.current_thread().name.
# CLI report (color-coded, grouped by worker)Xray.report() # current taskXray.report('other-task-id') # specific task# HTML report (Call Tree table)html=Xray.html_report() # returns HTML string# Semver versionprint(Xray.VERSION) # '0.5.0'# JSON (sorted entries + summary stats)data=Xray.json() # {'task_id', 'total_ms', 'entries', 'spans', 'warnings', 'alerts', 'data': [...]}# Raw entries (unsorted, as stored in Redis)entries=Xray.entries() # list of dictsEach worker calls Xray.init() with the same task_id but different thread_id.
Redis RPUSH is atomic — no conflicts.
# Worker 1Xray.init(r, 'job-abc', thread_id='w1')
# Worker 2Xray.init(r, 'job-abc', thread_id='w2')Report groups entries by thread_id automatically.
Real-time stderr output with nested outline — no Redis needed:
Xray.init_instant()
withXray.i('DB::query', {'table': 'users'}):
...Output:
P[0.0] init instant
P[0.1] in DB::query
app/db.py:45
table: "users"
P[15.3] out DB::query 15.2ms
app/db.py:45
table: "users"
rows: 150
Nested spans are indented. in lines are bold, out lines are dimmed.
python3 example_multiprocess.py --default # 3 workers + Redis report
python3 example_multiprocess.py --instant # real-time stderr outputGrouped CLI report:
Instant stderr output:
pip3 install flask redis
python3 example_web.pyOpen http://localhost:5000/ — auto-profiled page with execution panel at the bottom.
| URL | Description |
|---|---|
/ | Single-process demo with DB, ES, API, AI calls |
/threaded | Multi-worker demo (two iframe workers share task-id) |
/api/search?q=miami | JSON API (profiler key in X-Xray-Key header) |
/_profiler?k=KEY | Standalone HTML report |
/_profiler/json?k=KEY | Raw JSON entries |
Use Xray.attach_profiler() in web middleware to automatically add a collapsible profiler panel at the bottom of the page, while keeping the response integration inside the library.
Example:
@app.before_requestdefstart_profiler():
# ON/OFF logic example:# want_xray = isDeveloper() # turn ON for developers, OFF for visitorswant_xray=Falseifrequest.path.startswith('/_profiler') elseTrueifwant_xray:
Xray.init(redis_client) # task_id auto-generated@app.after_requestdefattach_profiler(response):
ifnotXray.task_id():
returnresponsereturnXray.attach_profiler(
response,
endpoint='/_profiler',
)This keeps web integration minimal and moves profiler response handling into the library.
The profiler panel fetches its HTML from a standalone endpoint:
@app.route('/_profiler')defprofiler_view():
task_id=request.args.get('k', '')
ifnottask_id:
return'Missing ?k= parameter', 400returnXray.html_report(task_id, redis_client=redis_client)- internals.md — Redis format, entry structure, implementation details


