You can stream the renderer's internal timing into your engine's profiler or write a trace file to track down slow frames.

The timing breaks down each phase of a frame (layout, painting, and JavaScript execution) alongside your own code in your profiler. Having that breakdown in your existing tools shows you which part of the UI pushed the frame over budget.

The `Profiler` interface is available in the Pro edition and higher, while render traces need the Enterprise edition (see [Editions and Feature Macros](/docs/2.0/editions-and-feature-macros)). Both interfaces are compiled out of lower editions behind `UL_HAS(PROFILER)` and `UL_HAS(RENDER_TRACE)`.

> 📘 Automatic Traces in AppCore
>
> In AppCore apps, setting `Settings::enable_profiler` to `true` causes `App::Create()` to write a timestamped `.perfetto-trace` file into a `profiler_traces` folder inside your diagnostics directory (see [App Lifecycle and Settings](/docs/2.0/app-lifecycle-and-settings)). That directory sits next to your log file and is determined by `Settings::developer_name` and `app_name`, never by any `Config::cache_path` you set. `App::is_profiler_active()` tells you if a trace is running, and `App::profiler_trace_path()` returns the path to the file.
>
> When you assign a profiler through `Platform::set_profiler()` before calling `App::Create()`, the app keeps your profiler and ignores the setting with a log warning. If writing the trace file fails, the error appears in your log and `App::is_profiler_active()` returns `false`.

## Writing a Trace File

If your app creates the `Renderer` directly, you can stream a Perfetto trace to disk using the library's built-in file writer.

### Creating the Trace Writer

Calling `CreateProfilerPerfetto(path)` returns a [`Profiler`](/api/cpp/2_0_0/classultralight_1_1_profiler.html) instance that writes a binary trace while keeping memory usage bounded. Partial traces stay valid, so you can still open the file at https://ui.perfetto.dev if your app terminates early.

Make sure the folder for the trace file already exists before creating the writer— if the file cannot be opened, the profiler leaves no message and records nothing.

`CreateProfilerPerfetto()` returns `nullptr` when the trace writer is not available on your platform or edition. You should check the pointer before passing it to `Platform::instance().set_profiler()` (see [Setting Up the Platform](/docs/2.0/setting-up-the-platform)).

### Lifetime and Teardown

Your profiler instance must outlive the `Renderer`. When shutting down, destroy the `Renderer` before clearing the platform slot with `nullptr`, then free the writer by calling `DestroyProfilerPerfetto()` (which accepts `nullptr` safely).

Guard all profiler calls with `#if UL_HAS(PROFILER)` so your code compiles on editions without profiler support:

```cpp
#include <Ultralight/Ultralight.h>

using namespace ultralight;

RefPtr<Renderer> renderer;

#if UL_HAS(PROFILER)
Profiler* profiler = nullptr;

void Init() {
  ///
  /// Create the trace writer and install it before creating the Renderer.
  /// (nullptr means tracing isn't available for this platform or edition.)
  ///
  profiler = CreateProfilerPerfetto("trace.perfetto-trace");
  if (profiler)
    Platform::instance().set_profiler(profiler);

  // Pseudo-code, set up the rest of the Platform (FileSystem, FontLoader) here.

  renderer = Renderer::Create();
}

void Shutdown() {
  ///
  /// Release the Renderer first-- the profiler must outlive it.
  ///
  renderer = nullptr;
  Platform::instance().set_profiler(nullptr);
  DestroyProfilerPerfetto(profiler);
}
#endif // UL_HAS(PROFILER)
```

## Forwarding to Your Own Profiler

You can route the renderer's timing data into tools like Tracy, Optick, or Unreal Insights by implementing your own `Profiler`.

### Installing a Custom Profiler

Subclass [`Profiler`](/api/cpp/2_0_0/classultralight_1_1_profiler.html) and provide definitions for its pure virtual methods.

Pass your instance to `Platform::instance().set_profiler()` before creating the `Renderer` or `App`. Your application keeps ownership of the profiler, and the object must outlive the `Renderer` or `App`.

> 🚧 Profiler callbacks must be thread-safe
>
> The library can invoke any `Profiler` callback concurrently from arbitrary threads. Your implementation must guard shared state or use thread-safe primitives.

### Scopes

`BeginScope(name)` and `EndScope()` delimit a named time interval on the calling thread. The library always balances these calls, so your profiler can track nested regions with a per-thread stack.

The `name` argument is a static string literal that lives as long as the library, allowing zone-based tools to retain the pointer directly without making a copy.

Scope names use dotted identifiers so profilers can filter and group timings by area.

| Prefix | Subsystem |
|---|---|
| `Ultralight.Update`, `Ultralight.Render`, `Ultralight.ViewPaint` | Engine updates, rendering, and View painting |
| `WebCore.Layout`, `WebCore.Paint`, `WebCore.EvaluateScript` | DOM layout, visual painting, and script evaluation |
| `JSC.CallFunction` | JavaScript function execution |
| `AppCore.Frame` | AppCore window frame updates |

### Frames

The renderer invokes `BeginFrame()` and `EndFrame()` around each frame pass, and you should never invoke them yourself.

Both methods have empty default implementations. You can override them to track latency from dirty state to display and spot frame drops (see [Updating and Rendering](/docs/2.0/updating-and-rendering)).

### Events and Counters

`EmitEvent()` marks an instant on the timeline (such as the start of a navigation), and its `detail` string may be `nullptr`.

`EmitCounter()` receives periodic numeric values like memory or cache sizes, which profiling tools typically plot as continuous graphs.

These resource counters arrive every `Config::resource_sample_interval` milliseconds (500 by default). Passing `0` disables counter sampling entirely without altering scope or event callbacks (see [Managing Memory](/docs/2.0/managing-memory)).

### Async Events

`BeginEvent()` and `EndEvent()` mark spans of work that can overlap or cross frame boundaries, such as page loads and asset fetches (the `detail` string on `BeginEvent()` may be `nullptr`).

Your implementation must return a non-zero identifier from `BeginEvent()`. The library hands that value back to `EndEvent()` when closing the operation, whereas returning `0` signals that no event was started.

### Custom Profiler Example

Here is a complete custom `Profiler` that routes scopes, frames, timeline events, and numeric counters into an engine profiler:

```cpp
#include <Ultralight/Ultralight.h>
#include <atomic>

using namespace ultralight;

#if UL_HAS(PROFILER)
class MyProfiler : public Profiler {
 public:
  ///
  /// Scopes are balanced and per-thread, a natural fit for a zone-based
  /// profiler.
  ///
  void BeginScope(const char* name) override {
    // Pseudo-code, push a named zone on your engine's profiler here.
    EngineProfilerPushZone(name);
  }

  void EndScope() override {
    // Pseudo-code, pop the current zone here.
    EngineProfilerPopZone();
  }

  ///
  /// The library brackets each rendering frame for us.
  ///
  void BeginFrame(uint64_t frame_id) override {
    // Pseudo-code, mark the start of a frame here.
    EngineProfilerFrameStart(frame_id);
  }

  void EndFrame() override {
    // Pseudo-code, mark the end of a frame here.
    EngineProfilerFrameEnd();
  }

  void EmitEvent(const char* name, const char* detail) override {
    // Pseudo-code, drop an instant marker here (detail may be nullptr).
    EngineProfilerMarker(name, detail ? detail : "");
  }

  void EmitCounter(const char* name, int64_t value) override {
    // Pseudo-code, plot a named value here.
    EngineProfilerPlot(name, value);
  }

  ///
  /// Async events overlap and cross frames, so hand back an id we can match
  /// later.
  ///
  uint64_t BeginEvent(const char* name, const char* detail) override {
    uint64_t id = next_event_id_++;
    // Pseudo-code, begin an async span with this id here.
    EngineProfilerBeginAsync(id, name);
    return id;
  }

  void EndEvent(uint64_t id) override {
    // Pseudo-code, end the async span with this id here.
    EngineProfilerEndAsync(id);
  }

 private:
  std::atomic<uint64_t> next_event_id_ { 1 };
};

MyProfiler my_profiler;

void InitProfiler() {
  ///
  /// Sample resource counters every 250 ms (0 turns them off) as part of your
  /// Config, then install our profiler before creating the Renderer. (It must
  /// outlive the Renderer.)
  ///
  Config config;
  config.resource_sample_interval = 250;
  Platform::instance().set_config(config);
  Platform::instance().set_profiler(&my_profiler);
}
#endif // UL_HAS(PROFILER)
```

## Render Traces

Enterprise edition licenses let you capture low-level rendering pipeline diagnostics in a Perfetto trace file.

### Recording Diagnostics

Call `Renderer::BeginRenderTrace(path, verbose)` to start recording pipeline activity into a `.perfetto-trace` file you can view at https://ui.perfetto.dev. The method returns `false` if tracing is not supported or if a session is already in progress.

When you finish capturing frames, call `Renderer::EndRenderTrace()` to flush and close the output file. You can check `Renderer::is_render_trace_active()` whenever you need to confirm that a session is recording.

### Trace Options and Concurrency

The `verbose` argument defaults to `true`, adding detailed stage annotations to the scope timings.

Render traces operate independently from the `Profiler` interface, so both can record at the same time:

```cpp
#include <Ultralight/Ultralight.h>

using namespace ultralight;

#if UL_HAS(RENDER_TRACE)
void CaptureRenderTrace(Renderer* renderer) {
  ///
  /// Record the rendering pipeline for a while, then flush and close the file.
  ///
  renderer->BeginRenderTrace("render.perfetto-trace");
  // Pseudo-code, run the frames you want to capture here.
  renderer->EndRenderTrace();
}
#endif // UL_HAS(RENDER_TRACE)
```

## Debugging Painting

Three settings help you investigate visual glitches when the problem is what the renderer draws rather than how long drawing takes.

### Compositor Debug Information

Setting `ViewConfig::enable_compositor_debug_info` (`false` by default) draws outlines around compositor layers and tiles, together with a repaint count on every layer.

These overlays only appear when the compositor is enabled. You can toggle them during execution with `View::set_compositor_debug_info_enabled()`.

### Continuous Repaints

Setting `Config::force_repaint` (`false` by default) tells the renderer to repaint all Views on every frame even when nothing is dirty.

This is useful for debugging shaders and profiling steady-state rendering load. Set the option on `Config` before calling `Renderer::Create()` (see [Creating the Renderer](/docs/2.0/creating-the-renderer#content-configuring-the-library)).

### Full-Layer Repainting

Setting `Config::paint_full_layers` (`false` by default) redraws an entire layer whenever any region in it changes, bypassing dirty-rectangle clipping.

Use this to determine whether visual artifacts come from dirty-region tracking. Redrawing whole layers lowers performance, so keep it disabled during normal gameplay.

Once you identify where your frames spend their time, [Optimizing Performance](/docs/2.0/optimizing-performance) covers the controls available for speeding them up.
