Repository files navigation

Python XRay Profiler

See through your code. Lightweight execution profiler for Python.

Version: 0.5.0

Author: Serg Parf sergey.porfiriev@gmail.com

Xray Web Report

What it does

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.

Zero-touch instrumentation

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.

Key features

  • 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 if guards needed

Reporting

  • 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=KEY endpoint returns raw entries as JSON
  • CLIXray.report() prints color-coded tree grouped by worker with Top 5 slowest
  • CLI instant — real-time stderr with nested outline, shows every in/out as it happens

Quick Start

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

Spans (with duration)

# 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:

  • request
  • response

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})

Decorator

@Xray.profile() # auto-name: Class.methoddeffind_listings(params): ...
@Xray.profile('custom-name') # explicit namedefhelper(): ...

Class Decorator

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.

Runtime Patching

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.

Closure Wrapper

result=Xray.wrap(lambda: api_call(url), 'API::call', {'url': url})

Info Points (no duration)

Xray.info('cache-hit', {'key': k})
Xray.warning('rate-limit', {'remaining': 5})
Xray.alert('timeout', {'url': url, 'after_ms': 5000})

Setup

# 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 + disable

task_id auto-generates as xray-{8 hex chars} when not provided. thread_id defaults to threading.current_thread().name.

Reading Results

# 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 dicts

Multi-Process / Celery

Each 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.

Instant Mode

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.

Examples

CLI (multiprocess)

python3 example_multiprocess.py --default # 3 workers + Redis report
python3 example_multiprocess.py --instant # real-time stderr output

Grouped CLI report:

Xray CLI Report

Instant stderr output:

Xray Instant Report

Web (Flask)

pip3 install flask redis
python3 example_web.py

Open http://localhost:5000/ — auto-profiled page with execution panel at the bottom.

URLDescription
/Single-process demo with DB, ES, API, AI calls
/threadedMulti-worker demo (two iframe workers share task-id)
/api/search?q=miamiJSON API (profiler key in X-Xray-Key header)
/_profiler?k=KEYStandalone HTML report
/_profiler/json?k=KEYRaw JSON entries

Attach Profiler To Response

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)

See Also

  • internals.md — Redis format, entry structure, implementation details

About

Xray — See through your code. Lightweight Python execution profiler with Redis storage, web panel, and multi-worker support

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Python XRay Profiler

See through your code. Lightweight execution profiler for Python.

Version: 0.5.0

Author: Serg Parf sergey.porfiriev@gmail.com

Xray Web Report

What it does

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.

Zero-touch instrumentation

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.

Key features

  • 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 if guards needed

Reporting

  • 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=KEY endpoint returns raw entries as JSON
  • CLIXray.report() prints color-coded tree grouped by worker with Top 5 slowest
  • CLI instant — real-time stderr with nested outline, shows every in/out as it happens

Quick Start

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

Spans (with duration)

# 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:

  • request
  • response

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})

Decorator

@Xray.profile() # auto-name: Class.methoddeffind_listings(params): ...
@Xray.profile('custom-name') # explicit namedefhelper(): ...

Class Decorator

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.

Runtime Patching

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.

Closure Wrapper

result=Xray.wrap(lambda: api_call(url), 'API::call', {'url': url})

Info Points (no duration)

Xray.info('cache-hit', {'key': k})
Xray.warning('rate-limit', {'remaining': 5})
Xray.alert('timeout', {'url': url, 'after_ms': 5000})

Setup

# 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 + disable

task_id auto-generates as xray-{8 hex chars} when not provided. thread_id defaults to threading.current_thread().name.

Reading Results

# 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 dicts

Multi-Process / Celery

Each 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.

Instant Mode

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.

Examples

CLI (multiprocess)

python3 example_multiprocess.py --default # 3 workers + Redis report
python3 example_multiprocess.py --instant # real-time stderr output

Grouped CLI report:

Xray CLI Report

Instant stderr output:

Xray Instant Report

Web (Flask)

pip3 install flask redis
python3 example_web.py

Open http://localhost:5000/ — auto-profiled page with execution panel at the bottom.

URLDescription
/Single-process demo with DB, ES, API, AI calls
/threadedMulti-worker demo (two iframe workers share task-id)
/api/search?q=miamiJSON API (profiler key in X-Xray-Key header)
/_profiler?k=KEYStandalone HTML report
/_profiler/json?k=KEYRaw JSON entries

Attach Profiler To Response

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)

See Also

  • internals.md — Redis format, entry structure, implementation details

About

Xray — See through your code. Lightweight Python execution profiler with Redis storage, web panel, and multi-worker support

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Python XRay Profiler

See through your code. Lightweight execution profiler for Python.

Version: 0.5.0

Author: Serg Parf sergey.porfiriev@gmail.com

Xray Web Report

What it does

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.

Zero-touch instrumentation

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.

Key features

  • 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 if guards needed

Reporting

  • 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=KEY endpoint returns raw entries as JSON
  • CLIXray.report() prints color-coded tree grouped by worker with Top 5 slowest
  • CLI instant — real-time stderr with nested outline, shows every in/out as it happens

Quick Start

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

Spans (with duration)

# 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:

  • request
  • response

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})

Decorator

@Xray.profile() # auto-name: Class.methoddeffind_listings(params): ...
@Xray.profile('custom-name') # explicit namedefhelper(): ...

Class Decorator

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.

Runtime Patching

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.

Closure Wrapper

result=Xray.wrap(lambda: api_call(url), 'API::call', {'url': url})

Info Points (no duration)

Xray.info('cache-hit', {'key': k})
Xray.warning('rate-limit', {'remaining': 5})
Xray.alert('timeout', {'url': url, 'after_ms': 5000})

Setup

# 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 + disable

task_id auto-generates as xray-{8 hex chars} when not provided. thread_id defaults to threading.current_thread().name.

Reading Results

# 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 dicts

Multi-Process / Celery

Each 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.

Instant Mode

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.

Examples

CLI (multiprocess)

python3 example_multiprocess.py --default # 3 workers + Redis report
python3 example_multiprocess.py --instant # real-time stderr output

Grouped CLI report:

Xray CLI Report

Instant stderr output:

Xray Instant Report

Web (Flask)

pip3 install flask redis
python3 example_web.py

Open http://localhost:5000/ — auto-profiled page with execution panel at the bottom.

URLDescription
/Single-process demo with DB, ES, API, AI calls
/threadedMulti-worker demo (two iframe workers share task-id)
/api/search?q=miamiJSON API (profiler key in X-Xray-Key header)
/_profiler?k=KEYStandalone HTML report
/_profiler/json?k=KEYRaw JSON entries

Attach Profiler To Response

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)

See Also

  • internals.md — Redis format, entry structure, implementation details

About

Xray — See through your code. Lightweight Python execution profiler with Redis storage, web panel, and multi-worker support

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Python XRay Profiler

See through your code. Lightweight execution profiler for Python.

Version: 0.5.0

Author: Serg Parf sergey.porfiriev@gmail.com

Xray Web Report

What it does

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.

Zero-touch instrumentation

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.

Key features

  • 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 if guards needed

Reporting

  • 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=KEY endpoint returns raw entries as JSON
  • CLIXray.report() prints color-coded tree grouped by worker with Top 5 slowest
  • CLI instant — real-time stderr with nested outline, shows every in/out as it happens

Quick Start

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

Spans (with duration)

# 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:

  • request
  • response

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})

Decorator

@Xray.profile() # auto-name: Class.methoddeffind_listings(params): ...
@Xray.profile('custom-name') # explicit namedefhelper(): ...

Class Decorator

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.

Runtime Patching

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.

Closure Wrapper

result=Xray.wrap(lambda: api_call(url), 'API::call', {'url': url})

Info Points (no duration)

Xray.info('cache-hit', {'key': k})
Xray.warning('rate-limit', {'remaining': 5})
Xray.alert('timeout', {'url': url, 'after_ms': 5000})

Setup

# 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 + disable

task_id auto-generates as xray-{8 hex chars} when not provided. thread_id defaults to threading.current_thread().name.

Reading Results

# 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 dicts

Multi-Process / Celery

Each 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.

Instant Mode

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.

Examples

CLI (multiprocess)

python3 example_multiprocess.py --default # 3 workers + Redis report
python3 example_multiprocess.py --instant # real-time stderr output

Grouped CLI report:

Xray CLI Report

Instant stderr output:

Xray Instant Report

Web (Flask)

pip3 install flask redis
python3 example_web.py

Open http://localhost:5000/ — auto-profiled page with execution panel at the bottom.

URLDescription
/Single-process demo with DB, ES, API, AI calls
/threadedMulti-worker demo (two iframe workers share task-id)
/api/search?q=miamiJSON API (profiler key in X-Xray-Key header)
/_profiler?k=KEYStandalone HTML report
/_profiler/json?k=KEYRaw JSON entries

Attach Profiler To Response

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)

See Also

  • internals.md — Redis format, entry structure, implementation details

About

Xray — See through your code. Lightweight Python execution profiler with Redis storage, web panel, and multi-worker support

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Python XRay Profiler

See through your code. Lightweight execution profiler for Python.

Version: 0.5.0

Author: Serg Parf sergey.porfiriev@gmail.com

Xray Web Report

What it does

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.

Zero-touch instrumentation

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.

Key features

  • 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 if guards needed

Reporting

  • 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=KEY endpoint returns raw entries as JSON
  • CLIXray.report() prints color-coded tree grouped by worker with Top 5 slowest
  • CLI instant — real-time stderr with nested outline, shows every in/out as it happens

Quick Start

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

Spans (with duration)

# 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:

  • request
  • response

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})

Decorator

@Xray.profile() # auto-name: Class.methoddeffind_listings(params): ...
@Xray.profile('custom-name') # explicit namedefhelper(): ...

Class Decorator

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.

Runtime Patching

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.

Closure Wrapper

result=Xray.wrap(lambda: api_call(url), 'API::call', {'url': url})

Info Points (no duration)

Xray.info('cache-hit', {'key': k})
Xray.warning('rate-limit', {'remaining': 5})
Xray.alert('timeout', {'url': url, 'after_ms': 5000})

Setup

# 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 + disable

task_id auto-generates as xray-{8 hex chars} when not provided. thread_id defaults to threading.current_thread().name.

Reading Results

# 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 dicts

Multi-Process / Celery

Each 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.

Instant Mode

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.

Examples

CLI (multiprocess)

python3 example_multiprocess.py --default # 3 workers + Redis report
python3 example_multiprocess.py --instant # real-time stderr output

Grouped CLI report:

Xray CLI Report

Instant stderr output:

Xray Instant Report

Web (Flask)

pip3 install flask redis
python3 example_web.py

Open http://localhost:5000/ — auto-profiled page with execution panel at the bottom.

URLDescription
/Single-process demo with DB, ES, API, AI calls
/threadedMulti-worker demo (two iframe workers share task-id)
/api/search?q=miamiJSON API (profiler key in X-Xray-Key header)
/_profiler?k=KEYStandalone HTML report
/_profiler/json?k=KEYRaw JSON entries

Attach Profiler To Response

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)

See Also

  • internals.md — Redis format, entry structure, implementation details

About

Xray — See through your code. Lightweight Python execution profiler with Redis storage, web panel, and multi-worker support

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Python XRay Profiler

See through your code. Lightweight execution profiler for Python.

Version: 0.5.0

Author: Serg Parf sergey.porfiriev@gmail.com

Xray Web Report

What it does

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.

Zero-touch instrumentation

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.

Key features

  • 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 if guards needed

Reporting

  • 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=KEY endpoint returns raw entries as JSON
  • CLIXray.report() prints color-coded tree grouped by worker with Top 5 slowest
  • CLI instant — real-time stderr with nested outline, shows every in/out as it happens

Quick Start

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

Spans (with duration)

# 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:

  • request
  • response

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})

Decorator

@Xray.profile() # auto-name: Class.methoddeffind_listings(params): ...
@Xray.profile('custom-name') # explicit namedefhelper(): ...

Class Decorator

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.

Runtime Patching

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.

Closure Wrapper

result=Xray.wrap(lambda: api_call(url), 'API::call', {'url': url})

Info Points (no duration)

Xray.info('cache-hit', {'key': k})
Xray.warning('rate-limit', {'remaining': 5})
Xray.alert('timeout', {'url': url, 'after_ms': 5000})

Setup

# 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 + disable

task_id auto-generates as xray-{8 hex chars} when not provided. thread_id defaults to threading.current_thread().name.

Reading Results

# 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 dicts

Multi-Process / Celery

Each 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.

Instant Mode

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.

Examples

CLI (multiprocess)

python3 example_multiprocess.py --default # 3 workers + Redis report
python3 example_multiprocess.py --instant # real-time stderr output

Grouped CLI report:

Xray CLI Report

Instant stderr output:

Xray Instant Report

Web (Flask)

pip3 install flask redis
python3 example_web.py

Open http://localhost:5000/ — auto-profiled page with execution panel at the bottom.

URLDescription
/Single-process demo with DB, ES, API, AI calls
/threadedMulti-worker demo (two iframe workers share task-id)
/api/search?q=miamiJSON API (profiler key in X-Xray-Key header)
/_profiler?k=KEYStandalone HTML report
/_profiler/json?k=KEYRaw JSON entries

Attach Profiler To Response

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)

See Also

  • internals.md — Redis format, entry structure, implementation details

About

Xray — See through your code. Lightweight Python execution profiler with Redis storage, web panel, and multi-worker support

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Python XRay Profiler

See through your code. Lightweight execution profiler for Python.

Version: 0.5.0

Author: Serg Parf sergey.porfiriev@gmail.com

Xray Web Report

What it does

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.

Zero-touch instrumentation

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.

Key features

  • 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 if guards needed

Reporting

  • 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=KEY endpoint returns raw entries as JSON
  • CLIXray.report() prints color-coded tree grouped by worker with Top 5 slowest
  • CLI instant — real-time stderr with nested outline, shows every in/out as it happens

Quick Start

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

Spans (with duration)

# 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:

  • request
  • response

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})

Decorator

@Xray.profile() # auto-name: Class.methoddeffind_listings(params): ...
@Xray.profile('custom-name') # explicit namedefhelper(): ...

Class Decorator

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.

Runtime Patching

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.

Closure Wrapper

result=Xray.wrap(lambda: api_call(url), 'API::call', {'url': url})

Info Points (no duration)

Xray.info('cache-hit', {'key': k})
Xray.warning('rate-limit', {'remaining': 5})
Xray.alert('timeout', {'url': url, 'after_ms': 5000})

Setup

# 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 + disable

task_id auto-generates as xray-{8 hex chars} when not provided. thread_id defaults to threading.current_thread().name.

Reading Results

# 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 dicts

Multi-Process / Celery

Each 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.

Instant Mode

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.

Examples

CLI (multiprocess)

python3 example_multiprocess.py --default # 3 workers + Redis report
python3 example_multiprocess.py --instant # real-time stderr output

Grouped CLI report:

Xray CLI Report

Instant stderr output:

Xray Instant Report

Web (Flask)

pip3 install flask redis
python3 example_web.py

Open http://localhost:5000/ — auto-profiled page with execution panel at the bottom.

URLDescription
/Single-process demo with DB, ES, API, AI calls
/threadedMulti-worker demo (two iframe workers share task-id)
/api/search?q=miamiJSON API (profiler key in X-Xray-Key header)
/_profiler?k=KEYStandalone HTML report
/_profiler/json?k=KEYRaw JSON entries

Attach Profiler To Response

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)

See Also

  • internals.md — Redis format, entry structure, implementation details

About

Xray — See through your code. Lightweight Python execution profiler with Redis storage, web panel, and multi-worker support

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Python XRay Profiler

See through your code. Lightweight execution profiler for Python.

Version: 0.5.0

Author: Serg Parf sergey.porfiriev@gmail.com

Xray Web Report

What it does

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.

Zero-touch instrumentation

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.

Key features

  • 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 if guards needed

Reporting

  • 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=KEY endpoint returns raw entries as JSON
  • CLIXray.report() prints color-coded tree grouped by worker with Top 5 slowest
  • CLI instant — real-time stderr with nested outline, shows every in/out as it happens

Quick Start

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

Spans (with duration)

# 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:

  • request
  • response

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})

Decorator

@Xray.profile() # auto-name: Class.methoddeffind_listings(params): ...
@Xray.profile('custom-name') # explicit namedefhelper(): ...

Class Decorator

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.

Runtime Patching

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.

Closure Wrapper

result=Xray.wrap(lambda: api_call(url), 'API::call', {'url': url})

Info Points (no duration)

Xray.info('cache-hit', {'key': k})
Xray.warning('rate-limit', {'remaining': 5})
Xray.alert('timeout', {'url': url, 'after_ms': 5000})

Setup

# 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 + disable

task_id auto-generates as xray-{8 hex chars} when not provided. thread_id defaults to threading.current_thread().name.

Reading Results

# 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 dicts

Multi-Process / Celery

Each 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.

Instant Mode

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.

Examples

CLI (multiprocess)

python3 example_multiprocess.py --default # 3 workers + Redis report
python3 example_multiprocess.py --instant # real-time stderr output

Grouped CLI report:

Xray CLI Report

Instant stderr output:

Xray Instant Report

Web (Flask)

pip3 install flask redis
python3 example_web.py

Open http://localhost:5000/ — auto-profiled page with execution panel at the bottom.

URLDescription
/Single-process demo with DB, ES, API, AI calls
/threadedMulti-worker demo (two iframe workers share task-id)
/api/search?q=miamiJSON API (profiler key in X-Xray-Key header)
/_profiler?k=KEYStandalone HTML report
/_profiler/json?k=KEYRaw JSON entries

Attach Profiler To Response

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)

See Also

  • internals.md — Redis format, entry structure, implementation details

About

Xray — See through your code. Lightweight Python execution profiler with Redis storage, web panel, and multi-worker support

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages