Profiling#
Use profiling to find expensive rendering stages, track resource memory, and compare workloads. ovrtx collects CPU and GPU measurements, grouped RTX stage timings, and memory categories. You can read these measurements while rendering continues, save them to files, or do both.
Capture lifecycle#
Profiling is process-wide. Create a renderer before starting capture, and assign one part of your application responsibility for starting and stopping it. A second start fails while profiling is active; it does not replace the capture.
Start with ovrtx.start_profiling() or
ovrtx_start_profiling(). With no output arguments, capture collects live
data without saving files. Rendering can continue concurrently with capture and
data reads.
Stop with ovrtx.stop_profiling() or
ovrtx_stop_profiling() before destroying the last renderer. Stopping
flushes and closes file output and may block. Arrange cleanup so it also runs
when your workload fails. Stopping an inactive capture succeeds while the
renderer system remains initialized.
Serialize capture-control calls with renderer teardown. Serialize data reads with capture restart and teardown. Stopping profiling and starting it again resets the capture clock and automatically resets each retained batch’s progress on its next read. Keep the batch alive between reads, including across restarts.
Profile a render loop#
The examples below capture 60 rendered frames and read profiling data every 500 ms, plus once after the last frame. The frame count is an example choice; use your application’s own loop condition. Rendering continues between reads. The final read collects samples available at that time; it does not wait for all asynchronously published measurements. Use file capture when you need a saved recording.
These examples assume a live renderer attached to a populated ovstage,
with /Render/Camera committed at ordinal. The C example also assumes
that ovrtx_update_from_stage() has completed for that state; Python
performs that update as part of ovrtx.Renderer.step().
See ovstage Integration for stage setup.
The Python example retains its copied batches for processing after capture. The C++ example uses the C API and processes each batch before reusing its storage. Both stop capture before the application destroys the renderer:
import time
import ovrtx
# renderer has an attached stage with /Render/Camera committed at ordinal.
data = ovrtx.ovrtx_performance_data_update_t()
next_read = time.monotonic()
updates = []
ovrtx.start_profiling()
try:
for frame in range(60):
renderer.step(
render_products={"/Render/Camera"},
delta_time=1 / 60,
ordinal=ordinal,
)
# Render every frame, but copy profiling data at most twice per second.
# Also read after the last frame to collect the latest available samples.
now = time.monotonic()
if now >= next_read or frame == 59:
collection_time, metrics = ovrtx.get_performance_data_update(data)
updates.append((collection_time, metrics)) # Python owns these copies.
next_read = now + 0.5
finally:
try:
ovrtx.release_performance_data_update(data)
finally:
ovrtx.stop_profiling()
# Python-owned copies survive batch reuse and release.
for collection_time, metrics in updates:
for metric in metrics:
for sample in metric["values"]:
print(
metric["name"], sample["timestamp_ns"],
sample["value"], metric["units"],
)
Include <ovrtx/ovrtx.h>, <chrono>, and <cstdio>.
ovrtx_result_t result{};
// renderer has an attached, updated stage committed at ordinal.
const ovx_string_t path = {"/Render/Camera", sizeof("/Render/Camera") - 1};
const ovrtx_render_product_set_t products = {&path, 1};
ovrtx_performance_data_update_t batch = {};
std::chrono::steady_clock::time_point next_read = std::chrono::steady_clock::now();
result = ovrtx_start_profiling(NULL);
if (result.status == OVRTX_API_SUCCESS)
{
for (int frame = 0; frame < 60; ++frame)
{
ovrtx_step_result_handle_t step = OVRTX_INVALID_STEP_RESULT_HANDLE;
const ovrtx_enqueue_result_t queued =
ovrtx_step_with_stage(renderer, products, 1.0 / 60, ordinal, &step);
result.status = queued.status;
if (result.status != OVRTX_API_SUCCESS)
break;
ovrtx_op_wait_result_t waited = {};
result = ovrtx_wait_op(renderer, queued.op_index, ovrtx_timeout_infinite, &waited);
const ovrtx_result_t destroyed = ovrtx_destroy_results(renderer, step);
if (result.status != OVRTX_API_SUCCESS || waited.num_error_ops != 0 ||
destroyed.status != OVRTX_API_SUCCESS)
{
result.status = OVRTX_API_ERROR;
break;
}
// Copy at most twice per second, plus once after the last frame.
const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
if (now >= next_read || frame == 59)
{
result = ovrtx_get_performance_data_update(&batch);
if (result.status != OVRTX_API_SUCCESS)
break;
next_read = now + std::chrono::milliseconds(500);
// Consume borrowed values before the next read replaces this batch.
for (size_t i = 0; i < batch.metric_count; ++i)
{
const ovrtx_performance_metric_t* metric = &batch.metrics[i];
for (size_t j = 0; j < metric->value_count; ++j)
printf("%.*s: %g %.*s\n", (int)metric->name.length, metric->name.ptr,
metric->values[j].value, (int)metric->units.length, metric->units.ptr);
}
}
}
// Both cleanup calls run even after a failed step or read.
const ovrtx_result_t released = ovrtx_release_performance_data_update(&batch);
const ovrtx_result_t stopped = ovrtx_stop_profiling();
if (result.status == OVRTX_API_SUCCESS)
result = released.status != OVRTX_API_SUCCESS ? released : stopped;
}
// Inspect result.status here, before destroying the renderer.
Read live measurements#
Use ovrtx.get_performance_data_update() or
ovrtx_get_performance_data_update() to copy a batch of measurements.
Initialize one batch per consumer and reuse it for subsequent reads:
The first read returns all samples still retained by the active capture.
Later reads return newly available samples, independently for each batch.
Structured and JSON reads using the same batch share progress.
A separate, initially empty batch rereads retained history without changing another consumer’s progress.
Samples follow collection order within each metric, including across reads.
Timestamps are nanoseconds since capture started, but a measurement published
later can have an earlier timestamp. Preserve the returned order.
collection_time_ns describes when the copy completed; it is informational,
not a filter or a bound on sample timestamps. Do not use it as a cursor.
Each metric provides:
uidandparent_uidto identify the metric and its default hierarchy. Metric identities belong to the current capture; do not retain them across restarts.name,units,info, andgroup_nameto describe the measurement.valuescontainingtimestamp_ns,value,parent_uid, andexecution_id. The sample parent identifies that invocation’s parent; execution identity attributes the sample to a CPU thread or RTX GPU, with zero meaning unspecified.average,minimum, andmaximumacross the active capture, rather than just the newly copied samples.
Use the supplied units when displaying values. Timestamp units and measurement units are separate: a timestamp is in nanoseconds even when its value describes milliseconds, a percentage, or memory in MiB.
In Python, initialize data = ovrtx.ovrtx_performance_data_update_t() before
the first read. Each read returns a collection time and a list of dictionaries.
These copies are owned by Python and remain valid after another read or batch
release. Call ovrtx.release_performance_data_update() in a finally
block before renderer teardown; the retained native batch still owns resources.
In C, zero-initialize an ovrtx_performance_data_update_t before first use.
Its strings and arrays remain valid until you update or release that container.
Copy any data you need to keep before reusing it. Do not copy the container’s
ownership or access the same container concurrently. Call
ovrtx_release_performance_data_update() before destroying the last
renderer, including on error paths.
The snippets below print a batch. In C, include <ovrtx/ovrtx.h> and
<stdio.h>, initialize ovrtx_performance_data_update_t data = {0}; once, and
retain data for subsequent reads. In Python, initialize the batch as
described above and retain it too.
import ovrtx
# data is zero-initialized once and retained between reads and restarts.
collection_time, metrics = ovrtx.get_performance_data_update(data)
for metric in metrics:
for sample in metric["values"]:
print(
metric["name"], sample["timestamp_ns"],
sample["value"], metric["units"],
)
// data is zero-initialized before first use. Reuse it on later reads.
result = ovrtx_get_performance_data_update(&data);
if (result.status == OVRTX_API_SUCCESS)
{
for (size_t i = 0; i < data.metric_count; ++i)
{
const ovrtx_performance_metric_t* metric = &data.metrics[i];
for (size_t j = 0; j < metric->value_count; ++j)
{
printf("%.*s: %g %.*s\n", (int)metric->name.length, metric->name.ptr,
metric->values[j].value, (int)metric->units.length, metric->units.ptr);
}
}
}
Reread retained history#
The snippets below use a separate batch to reread retained history without
resetting capture or advancing the original data reader. Release the history
batch after copying or inspecting it, then resume incremental reads with data:
import ovrtx
# A separate batch rereads retained history without advancing data.
history_data = ovrtx.ovrtx_performance_data_update_t()
try:
history_time, history = ovrtx.get_performance_data_update(history_data)
finally:
ovrtx.release_performance_data_update(history_data)
next_time, newer = ovrtx.get_performance_data_update(data)
// Separate ownership preserves the incremental reader's progress.
ovrtx_performance_data_update_t history = {};
result = ovrtx_get_performance_data_update(&history);
if (result.status == OVRTX_API_SUCCESS)
{
printf("History contains %zu metric descriptors\n", history.metric_count);
}
const ovrtx_result_t history_released = ovrtx_release_performance_data_update(&history);
// Resume the original reader after inspecting history.
result = ovrtx_get_performance_data_update(&data);
if (result.status == OVRTX_API_SUCCESS)
{
// Process data.metrics before the next read reuses this container.
}
Polling and retained history#
Copy every 500 ms to 1 s for background collection, or every 200 to 250 ms for a live display. These are recommended starting points, not sampling controls. Reading more often does not increase the producer’s sampling rate.
Live history is bounded. A fresh batch cannot recover overwritten samples. Choose a polling interval with room for scheduling delays, bursts, and the time needed to copy the batch. Use the sample rate of each metric, not just application FPS: a scope can produce several samples per frame.
Longer intervals produce larger batches. Copying and JSON serialization cost also grows with the number of metrics, particularly RTX and memory metrics. File capture is independent of live polling; enable it when you need a saved recording instead of relying on retained live history.
JSON updates#
Use ovrtx.get_performance_data_update_as_json() or
ovrtx_get_performance_data_update_as_json() when your consumer needs
JSON. The document contains collection_time_ns and a metrics array.
The retained progress is shared with structured updates on the same batch. Non-finite sample
values are represented by JSON null.
Python returns a timestamp and an owned string. C returns UTF-8 text in the
batch’s json field, with the same container lifetime and release rules as
structured updates.
The snippets below request JSON using the batch from the preceding read:
import json
import ovrtx
# JSON and structured reads advance the same retained batch.
collection_time, text = ovrtx.get_performance_data_update_as_json(data)
update = json.loads(text)
result = ovrtx_get_performance_data_update_as_json(&data);
if (result.status == OVRTX_API_SUCCESS)
{
// Consume the UTF-8 view before updating or releasing data.
fwrite(data.json.ptr, 1, data.json.length, stdout);
}
Stop capture#
The snippets below release capture resources. Put Python cleanup in a
finally block after a successful start so release and stop both run,
including on failed reads. Ensure the C cleanup path is reached
on failed reads as well, and inspect both cleanup results.
# Both cleanup calls run while the renderer is still alive.
try:
ovrtx.release_performance_data_update(data)
finally:
ovrtx.stop_profiling()
// Release borrowed strings/arrays and flush capture before renderer teardown.
ovrtx_result_t release_result = ovrtx_release_performance_data_update(&data);
ovrtx_result_t stop_result = ovrtx_stop_profiling();
Interpret RTX timings#
RTX timings group rendering work by GPU, render graph, viewport, and render tile. Groups describe stages such as G-Buffer, Path Tracing, Light Cache, Radiance Cache, DLSS, Post Processing, and Output Processing. Availability depends on the work performed; a missing stage is not a measured zero.
Grouped durations can combine separate operations. They do not describe a single contiguous period of GPU execution or GPU occupancy. Compare the same groups under comparable workloads, render settings, hardware, and capture conditions.
Parent containers include child work. Do not sum a container and its children to calculate a total. Keep GPU and viewport hierarchies separate when comparing measurements.
Interpret memory measurements#
RTX Memory measurements report tracked resource allocations in MiB
(1 MiB = 1,048,576 bytes). GPU allocations, host allocations, and virtual
reservations have separate hierarchies.
Category |
Contents |
|---|---|
Geometry |
BVH, vertex attributes and primvars, instance and scene data, and geometry buffers, with subcategories where available. |
Textures |
Tracked texture resources. |
Volumes |
Tracked volume resources. |
Gaussians |
Gaussian-owned resources, including their geometry and acceleration structures. |
SPG |
Resources owned by Sensor Processing Graphs. |
Output Buffers |
Tracked output buffers. |
Internal |
Shared reserves and resources not assigned to the other categories. |
Parents include their descendants. Do not add parent totals to child values, or add virtual reservations to allocated memory. Tracked host allocations are not process physical memory, and these resource totals are not a complete measurement of process heap usage or residency. Treat process-memory and device-memory indicators as separate measurements.
Save a capture#
Provide an output directory or basename when starting capture to enable file output. The directory must already exist and be writable. The basename must not contain path separators; provide it without an extension.
In Python, either argument being non-None enables saving. An empty string
selects that argument’s default. In C, either non-null string pointer enables
saving, including a pointer to an empty string. A null configuration, or two
null string pointers, selects live-only capture.
The default output location is the persistent user directory, and the default basename is timestamp-based. Existing recordings are not overwritten; name collisions add a timestamp suffix. C configuration strings are copied before the start call returns.
To save the render-loop example, replace its start call with the corresponding
snippet below. Keep the polling and cleanup unchanged. In Python,
output_path is your chosen directory path. In C++, output_path and
file_name are std::string values and the directory must already exist.
from pathlib import Path
import ovrtx
# Choose a directory before starting; start_profiling does not create it.
capture_dir = Path(output_path)
capture_dir.mkdir(parents=True, exist_ok=True)
ovrtx.start_profiling(output_path=str(capture_dir), file_name="profiling")
// output_path names an existing writable directory; file_name has no extension.
ovrtx_profiling_config_t config = {};
config.output_path = {output_path.data(), output_path.size()};
config.file_name = {file_name.data(), file_name.size()};
result = ovrtx_start_profiling(&config);
Capture writes three CSV files with the chosen basename:
<name>.rpm_info.csvdescribes the recorded metrics.<name>.rpm_data.csvcontains sampled values with timestamp, value, parent UID, and execution ID for each metric UID.<name>.rpm_summary.csvcontains summary statistics.
Stop capture before consuming the completed file set so pending output has been flushed. The public capture contains public measurements; it does not export detailed private render-pass or thread instrumentation.
Troubleshooting#
Capture does not start. Check the error returned by the start call. Create
a renderer first, ensure another capture is not already active, and verify
that any requested output directory exists and is writable. Python raises
RuntimeError; C callers check the result and use
ovrtx_get_last_error() for details.
No new samples. Confirm that capture is active and that the workload produces the metric you are reading. Continue polling at an appropriate cadence. Reuse the same batch for incremental reads; a new batch rereads retained history. Progress resets automatically after restarting capture.
History has gaps. Check the time between reads, including scheduling and copy delays, against the actual per-metric sampling rate. Poll more often or enable file capture for future recordings. Reading again with a fresh batch only returns samples still retained.
Further reading#
Application Flow — renderer lifetime and the application loop.
Python API Reference — Python profiling API reference.
C API Reference — C profiling API reference.