Using the Profiling Trace service

The Profiling Trace service collects runtime profiling data from the Kanzi Engine and writes it to a JSON trace file compatible with the Perfetto trace viewer.

Platform support

The Profiling Trace service builds from sources on all known Kanzi target platforms. The target platform must support writing files to the filesystem and allow developers to obtain those files from the filesystem.

How the Trace service works

The Trace service operates as a three-stage pipeline:

  1. Collect – The service copies profiling samples from the Kanzi Engine profilers into internal storage.

  2. Store – Samples accumulate in storage until a write trigger occurs.

  3. Write – The service writes all stored samples to a JSON trace file.

You can trigger collection and writing independently using timers, commands, or events.

Understanding trace data

The amount of profiling data available in the trace depends on the build configuration of the Kanzi Engine. The built-in instrumentation of the Kanzi Engine uses compile-time profiling category flags that control whether profilers produce data.

Profiling build

In a Profiling build, the compiler enables all profiling categories and the trace contains the full set of data:

  • Startup profiling – Application startup timing (requires Application registration)

  • Main loop profiling – Frame timing, stage durations (Input, User, Render)

  • Domain node profiling – Node operations

  • Domain layout profiling – Layout operations

  • Domain render profiling – Render operations

  • Resource profiling – Resource loading timing

  • Default registry profiling – All interval profilers registered in the default profiling registry

  • Named interval profiling – Named interval profilers from the default profiling registry

  • Custom collection tasks – Any additional profiling data registered through the API, such as Performance Service counters and Log Service entries

Debug and Release builds

In Debug and Release builds, the compiler disables the built-in profiling categories of the Kanzi Engine. The Kanzi Engine profilers exist but produce no samples. The trace contains only:

  • Default registry profiling – All interval profilers registered in the default profiling registry

  • Named interval profiling – Named interval profilers from the default profiling registry

  • Custom collection tasks – Any additional profiling data registered through the API

To get the most detailed trace data, use a Profiling build of your application.

Reading the Render and Present phases

In Kanzi 4.1 the main thread records a frame, and a render thread executes the recorded commands and presents the frame. In a kzTrace the render thread is named OpenGL Thread with the OpenGL backend and Vulkan Thread with the Vulkan backend. The trace of Kanzi Monitor covers the main thread only.

At the beginning of each frame, the main thread waits until the GPU has completed the frame before the previous one, which used the same part of the staging buffers. The trace of Kanzi Monitor has no span for this wait, so it appears at the start of Render as time that no child span covers: the Render span is long, but its children (Node2DRender and the render passes) start only at its end. When the application renders a frame on every vertical sync, the wait can fill most of the frame however little the frame draws, and turning vertical sync off removes it.

Monitor's trace of the main thread in the Perfetto trace viewer: four frames, each with a long Render span whose children start only at its end

The main thread in the trace of Kanzi Monitor, with vertical sync on. Each Render span is long, and its children, Node2DRender and the render passes, start only at its end. The time before them is the wait.

The main thread can also wait at the end of a frame, in Present: before it hands the frame’s commands to the render thread, it waits until the render thread has finished the commands of the previous frame. When the render thread is slow, this wait fills the frame instead. The trace of Kanzi Monitor has no span for this wait either, so it appears as a long Present span.

Where the render thread waits differs between the backends, and only the render thread sees the difference: with OpenGL, the buffer swap at the end of the frame usually blocks until a buffer is free; with Vulkan, acquiring a buffer at the beginning of the frame blocks, and presenting does not.

Read the time in a Render span that no child span covers, and a long Present span, as waiting, not as rendering work. It also inflates the duration that the span name shows, such as Render: <n> ms, and the per-phase totals.

To confirm that a long Render is this wait:

  • Capture a kzTrace (overwatch.kztrace, see kzTrace: engine-native tracing) of the same moment. With OpenGL, the OpenGL Thread track shows a long Process PresentCommand. With Vulkan, the Vulkan Thread track shows a long Process Vulkan Commands, and the main-thread track shows the wait itself as a long BackendVulkan::waitForGPUFence inside CommandRecorder::beginRenderFrame. A kzTrace holds only the last few seconds of a running application, so capture it while the wait is happening.

  • Turn vertical sync off for one run with PresentMode = immediate in application.cfg. The wait disappears and Render shrinks to its children. Tearing can occur, so use this only to measure. With Vulkan, if the driver does not offer the immediate mode, Kanzi logs PresentMode::Immediate is not supported, defaulting to PresentMode::FIFO. and the wait stays.

Triggering a trace write

There are several ways to trigger a trace write:

  • On-demand command – Use the trace command from the local or remote console. This collects and writes immediately.

  • One-shot timer (WritingTimerOnceInterval) – Writes the trace once after a specified delay in milliseconds. Use this to capture startup behavior.

  • Repeating timer (WritingTimerRepeatInterval) – Writes the trace at regular intervals. Use this for continuous monitoring.

  • On exit (WritingOnExitEnabled) – Writes the trace when the application shuts down.

  • Frame duration threshold (WritingOnFrameDurationThreshold) – Writes the trace when a frame exceeds the specified duration in milliseconds. Use this to catch performance spikes. Kanzi Monitor disables this trigger when running in Kanzi Studio Preview.

Controlling sample collection and storage

By default, each collection replaces the previously stored samples. This means the trace file contains only the most recent data from each profiler buffer.

To accumulate samples over time, enable CollectWithAppendingEnabled. When appending is enabled, each collection adds new samples to the existing storage instead of replacing it. This builds up a longer trace at the cost of increased memory usage.

When CollectWithAppendingEnabled is enabled, you can also use CollectingOnFullSampleBufferEnabled (enabled by default). This automatically collects samples from a profiler when its sample buffer becomes full, preventing data loss when profilers produce data faster than the collection timer runs.

The CollectingTimerRepeatInterval setting controls how often samples are collected independently of writing. This is useful when you want to collect frequently to avoid losing samples, but write less often.

Clearing the trace storage

With CollectWithAppendingEnabled = 1 each collection adds to the storage, and a write does not empty it, so every trace holds everything recorded since the application started and a second write repeats the first. To start over, empty the storage:

overwatch.trace clear
→ {"success":true,"cleared":["storage"],"appending":true}

The next overwatch.trace then no longer holds what the storage held. A benchmark script can clear after it has built its scene, and again between measured sections, so that each trace holds one section. Also through the MCP clear_trace tool, and Clear on the Web UI’s Trace tab.

Note

This clears the trace of Kanzi Monitor, the one that trace and overwatch.trace write. It does not change a kzTrace, which Kanzi records and overwatch.kztrace captures. See kzTrace: engine-native tracing.

What a clear does not remove:

  • the samples the profilers recorded since the last collection, which are still in their buffers. The next collection adds them, so the next trace can begin a little before the clear. Collect often (CollectingTimerRepeatInterval) to keep that short;

  • the resource-loading events of a Profiling build, which the writers read from Kanzi’s resource profiler, not from the storage.

With appending off, each collection already replaces the storage, so a clear changes nothing, and the reply says so. With ServiceTraceEnabled = 0 there is no storage, and the reply says the Trace service is disabled.

storage is the only thing a clear covers, and the default: overwatch.trace clear storage is the same command. Any other argument is refused by name.

Choosing which profiling categories are recorded

Every profiler belongs to a profiling category, such as MainLoop, NodeOperation or a category your application registers. A disabled category records nothing, so it has no spans in any trace. Enabling only the categories a question needs keeps the trace small and keeps the profiling overhead in a measurement to the profilers you chose.

A category controls only what its profilers record. Disabling one of Kanzi Monitor’s own categories removes this from the trace:

  • PerformanceInfo: the Performance service’s counter tracks. The aggregate values of its profilers also stop changing, so code that reads them as shown in Reading profiler data sees the last recorded value. overwatch.perf, perfinfo, the performance watchers and their metrics keep working, because they read the current values directly.

  • Logging: the log entries on the KanziLog track. The Log service still captures every entry, so overwatch.logs and the on-screen log are unchanged.

  • SampleDataCollector: the spans of Kanzi Monitor’s own trace collection.

  • MonitorCommand: the command spans (see Finding the frame a command ran in). The commands still run.

Note

Kanzi has two kinds of category, and this section is about the first:

  • Profiling categories group the profilers that kzProfileScope and the engine’s own profiling use (kanzi::ProfilingCategory). The Trace service records them in the trace that trace and overwatch.trace write. They exist in every build configuration, for the categories that the build compiles in.

  • kzTrace categories group the scopes of Kanzi’s tracing subsystem (kzTraceCreateCategory, kzTrace), which overwatch.kztrace captures. They exist only in builds with tracing (KANZI_TRACING_BUILD, not Release). The settings and commands here do not change them; overwatch.kztracecategories lists them. See Listing the kzTrace categories.

At startup, the Trace service applies ProfilingCategoryFilter from kanzimonitor.cfg. It has the same syntax as ProfilingCategoryFilter in application.cfg: rules separated by ;, names in a rule separated by |, and * for every category.

  • The default, *=on, enables every category. This is what Kanzi Monitor has always done, and it is why a trace carries ResourceLoading, which Kanzi registers disabled.

  • Empty (ProfilingCategoryFilter =) leaves the categories as the application configured them.

  • Anything else is applied on top of the application’s filter. For example, for a lean benchmark trace:

ProfilingCategoryFilter = "*=off;MainLoop|MyCategory=on"

Whether the application’s own ProfilingCategoryFilter reaches Kanzi Monitor’s categories (Logging, MonitorCommand, PerformanceInfo, SampleDataCollector) depends on how Kanzi Monitor is loaded. Kanzi applies that filter to the categories that exist at that point. When Kanzi Monitor is linked into the application (KANZI_LINK_KZMONITOR, as in the example application), its categories already exist, and the filter applies to them like any other. When Kanzi Monitor is loaded as a plugin (PluginNames), they are registered later, and the filter does not reach them. Either way, the kanzimonitor.cfg setting is applied after the application’s filter, so it decides for whatever it names.

At runtime, use overwatch.profilingcategories:

overwatch.profilingcategories                                   # list every category and its state
overwatch.profilingcategories MainLoop                          # one category
overwatch.profilingcategories MainLoop off                      # disable one category
overwatch.profilingcategories filter "*=off;MainLoop=on"        # apply a filter
overwatch.profilingcategories reset                             # code defaults, then the startup filter

A filter is applied the way Kanzi applies ProfilingCategoryFilter: the valid rules take effect, and a malformed rule or a name that no category has is skipped. Kanzi only logs these, so overwatch.profilingcategories filter reports them in the response as ignoredRules and unknownNames, and refuses a filter in which no rule is valid. For the startup filter they are logged as warnings, and because those log lines are written before Kanzi Monitor captures logs, the listing also reports them as startupFilterIgnoredRules and startupFilterUnknownNames. Names are matched exactly as written, spaces included: in MainLoop | Generic the second name has a leading space and matches nothing, so write MainLoop|Generic.

A change applies to what is recorded from then on. A trace that accumulates samples (CollectWithAppendingEnabled) still contains what was recorded before the change.

To go back after changing categories, use overwatch.profilingcategories reset, or Reset in the Web UI. It returns every category to the state its code registered it with, then applies the startup filter (ProfilingCategoryFilter in kanzimonitor.cfg, reported as startupFilter) again. That is a defined state, not an undo, and it can differ from the state right after startup: the application’s own ProfilingCategoryFilter is not applied again, and anything the application changed itself is reset too. Kanzi keeps no record of earlier states, so Kanzi Monitor does not offer to return to one. Restarting the application returns to the startup state.

Which categories are listed. Kanzi cannot enumerate its profiling categories, so the list is built from two sources, and each entry says which one found it:

  • known: every category the Kanzi engine declares, and Kanzi Monitor’s own, when this build registers them. A category compiled out of the build, or one that belongs to another platform (DroidFw exists only on Android), is not listed.

  • fromProfiler: the category of every profiler in the default profiler registry. This is how an application’s or plugin’s categories are found.

An application or plugin category is therefore not listed when:

  • none of its profilers has run yet. A profiler is registered the first time its code runs, so the category appears after that.

  • its profilers are kept in a profiler registry of their own (kzProfileScopeExtended with a custom registry), or it has no profiler at all.

An unlisted category still works by its exact name: overwatch.profilingcategories MyCategory off finds it, and a filter such as *=off changes it like any other.

The startup filter reaches only the categories that exist when it is applied. A category registered later, for example by a plugin that loads after Kanzi Monitor or by code that has not run yet, keeps its own initial state. The same rule decides whether the application’s filter reaches Kanzi Monitor’s categories, as described above.

A name that is not found is reported as an error, with the list of names that are found. The legacy profcats [0|1] command still sets every category at once, and without an argument it reports how many of the listed categories are enabled.

Finding the frame a command ran in

Every command Kanzi Monitor runs is recorded as a span named after the command, for example overwatch.setprop, in the profiling category MonitorCommand. Use it to find the frame that a command changed, in any build. The engine’s MainLoop spans exist only in Profiling builds, so in a Release build the command span is the only marker of where a command ran.

  • The span covers the command’s handler, inside the frame and the TaskDispatcherTask that ran it. A command whose handler fails or throws still has its span.

  • Commands from the remote, serial and local consoles are all recorded, and so are the commands that a performance watcher or a property breakpoint runs, because they all run through the same command processor.

  • In the chrometrace writer, the span’s args carry the command (Scope name) and its arguments (Object name), at most 256 characters (a longer text is cut and ends in ...), so two setprop calls to different properties can be told apart. The perfetto writer names the span after the command but does not carry the arguments.

  • A name that is not a registered command is recorded under one span name, <unknown command>, with the whole line as its arguments.

  • Kanzi Monitor’s own commands are recorded too, overwatch.trace and overwatch.fetchtrace included. An analysis script should expect them.

  • Turn the spans off with overwatch.profilingcategories MonitorCommand off. See Choosing which profiling categories are recorded.

The span marks where the command ran, not every frame it affected. A property change is laid out and rendered in the frames after it, so read those frames too.

Each command keeps its most recent 1024 spans between collections. The sample buffer is circular, so a busier benchmark overwrites the oldest. With CollectWithAppendingEnabled = 1 and CollectingOnFullSampleBufferEnabled = 1, a full buffer is copied into the trace storage before it is overwritten, so nothing is lost.

Understanding output files

The trace output file is named using the pattern:

<SessionLabel>_<timestamp>_<sequence>.json

Where <SessionLabel> is the configured session label (default: tracing_output), <timestamp> is the session start time, and <sequence> is incremented for each write within the session.

  • On Windows and platforms other than Android, the output file is written to the current working directory.

  • On Android, the output file is written to the host application’s internal files directory (Context.getFilesDir()), resolved at runtime via JNI. The exact path depends on the package, typically /data/user/0/<package>/files/ on modern devices. To copy a trace off the device, use adb shell run-as <package> cat files/<trace>.json > <trace>.json.

To view the output file, open it in the Perfetto trace viewer.

Adding custom collection tasks

You can register custom collection tasks to include additional profiling data in the trace output. A collection task is a callback that the Trace service invokes when collecting samples. The callback writes profiler data into the sample storage, which is then exported to the trace file.

Each collection task has an event type that determines how the data appears in the trace viewer:

  • "duration" – Time intervals with a start and end. Use for profiling scoped operations such as function durations and stage timings. Appears as spans in the trace timeline.

  • "instant" – Single points in time with no duration. Use for events such as log messages. Appears as markers in the trace timeline.

  • "counter" – Numeric values over time. Use for metrics such as FPS and memory usage. Appears as counter tracks in the trace viewer.

In the chrometrace output, the ts (timestamp) and dur (duration) fields are in microseconds, emitted as fractional values so that sub-microsecond scopes keep their precision instead of being truncated to zero-duration (instant-looking) events. Timestamps are session-relative: every trace starts at 0 and counts up from the beginning of the capture, rather than carrying the platform’s absolute clock value.

The subtracted base is recorded in the header as otherdata.sessionBaseNs, in nanoseconds on the engine’s kanzi::Clock timebase — the same clock kzTrace stamps with. A profiling sample’s original clock value is ts * 1000 + sessionBaseNs, so a Monitor capture and a kzTrace capture of the same run can be shifted onto one timeline. The base is the earliest event the capture holds, resource profiling events included, so a capture of nothing but resource loading starts at 0 like any other. The perfetto writer rebases the same way, and records the same base as a ClockSnapshot packet: at trace time 0 on Perfetto’s BOOTTIME clock (id 6), the user-defined clock 96 — kanzi::Clock, sequence-scoped to the writer’s packet sequence — reads sessionBaseNs. A packet’s original clock value is timestamp + sessionBaseNs; trace_processor shows the pair in its clock_snapshot table. The profiling2chrometrace.py converter in the example tools does not rebase: its output has the same traceEvents shape but carries absolute clock values and no sessionBaseNs, so do not add the base to a converter-produced file — the timeline would silently shift.

A counter sample is skipped when the value is not plausible. The Kanzi framerate API returns inf, or an absurd finite value, for a frame of zero or near-zero duration — which typically happens at or near startup — and a single such sample in the “Frames per second” track stretches the trace viewer’s automatic counter scaling until every real sample is squashed flat. Trace-viewer counters carry the previous value forward across a skipped sample, so the track still renders continuously.

To register a custom collection task:

#include "profilinghelper_collector.hpp"

void onCollectMyData(SampleDataStorage& storage)
{
    // Write profiler registry data into the storage.
    storage.writeRegistry<MyProfilerType>(myProfilerRegistry);
}

ThreadIndex mainThreadIndex = SampleCollector::getKanziMainThreadIndex();
SampleCollector::registerGeneralCollectionTask(
    "my_custom_profiling",    // Name for the data storage
    onCollectMyData,          // Callback invoked during collection
    mainThreadIndex,          // Thread index for trace visualization
    "duration");              // Event type: "duration", "instant", or "counter"

Trace writers

The Trace Service drives output through a registry of named writers. Each writer registers itself under a name, has a human-readable description, and can be independently activated or deactivated — at startup via configuration or at runtime via a command.

Built-in writers:

Name

Description

chrometrace

Chrome Trace Format JSON (.json). Opens in Perfetto UI, chrome://tracing, and Android Performance Analyzer. Carries the full Monitor sample set, including resource samples.

perfetto

Native Perfetto protobuf (.perfetto-trace). Smaller on disk than chrometrace, supports typed counters and debug annotations, and is the canonical input format for Perfetto UI and Android Performance Analyzer. Implemented with a hand-rolled, header-only protobuf encoder — no external dependency. Carries the full Monitor sample set, including resource profiling samples.

perfetto-producer

Stream events through the in-process Perfetto producer SDK to the platform tracing daemon — Android traced or Linux perfetto. No file on disk; events appear in system traces captured by Android Performance Analyzer or the perfetto CLI alongside ATrace, SurfaceFlinger, binder, and other system producers. Requires the plugin to be built with -DKZMONITOR_PERFETTO_PRODUCER=ON (pre-built Android binaries already are). See the Streaming traces with the Perfetto producer SDK section below for the full workflow.

The ActiveWriters configuration key (see Configuring Kanzi Monitor) lists writers to activate at startup, as a comma-separated string. For example, to enable two writers at once:

ActiveWriters = chrometrace,otherwriter

If the key is absent or empty, chrometrace is activated by default.

Note

ActiveWriters is applied once during TraceService::initialize() and can only refer to writers that are already registered at that point — the built-in writers and any writer whose module initialises before the Trace Service. Writers registered by a downstream plugin whose initialize() runs later must call TraceWriterRegistry::activate() from their own init path (see the example plugin’s customperformancetools_module.cpp for the pattern). At runtime, overwatch.activatewriter <name> works for any registered writer regardless of registration time.

Unknown names in ActiveWriters log a warning (ActiveWriters: writer '<name>' is not registered; skipping) and are skipped. If the resulting active set is empty — for example, ActiveWriters = typo — the Trace Service additionally logs ActiveWriters resolved to zero active writers — no trace output will be produced. Selection is intentionally explicit: a typo silently disabling all tracing is preferable to a typo silently activating an unintended writer.

At runtime, the overwatch.writers command returns the registry contents as JSON, and overwatch.activatewriter / overwatch.deactivatewriter toggle individual writers. A writer the Trace service blocked is listed with "available": false and an unavailableReason, and activating it answers {"error":"Writer unavailable","name":...,"reason":...}, where a name no writer has answers Unknown writer. On Android the file writers are blocked when no files directory can be resolved; see Known issues. A trace write fires through every currently-active writer; writers added by future versions of Kanzi Monitor or by your own integration are picked up the same way without any change to the Trace Service itself.

Streaming traces with the Perfetto producer SDK

The perfetto-producer writer streams events through the in-process Perfetto producer SDK to the platform tracing daemon. Events appear in system traces captured by Android Performance Analyzer or the perfetto CLI alongside ATrace, SurfaceFlinger, binder, and other system producers — giving you a single timeline that correlates Kanzi work with the rest of the device.

Compared to the file-output writers (chrometrace and perfetto), the perfetto-producer writer:

  • Has no end-of-session “write the file” step — events flow as the application runs.

  • Co-captures with the rest of the system, making it easy to align Kanzi frame stages with Choreographer#doFrame, surface composition, and Binder traffic.

  • Requires the producer SDK to be built into Kanzi Monitor and a tracing daemon to be available on the platform.

Build prerequisites

The Perfetto producer SDK is gated behind a CMake option that defaults to off when building from source:

cmake -DKZMONITOR_PERFETTO_PRODUCER=ON ...

When the option is off, the perfetto-producer writer’s registered callback is a no-op and the writer never produces output. Activating it via ActiveWriters = perfetto-producer logs an informational message but otherwise does nothing.

The pre-built Android binaries shipped with Kanzi Monitor are built with -DKZMONITOR_PERFETTO_PRODUCER=ON. If you use those directly, the producer SDK is already linked and no extra flag is required. Verify in logcat: Perfetto producer SDK initialized (system + in-process backends) appears at startup.

Platform support

Platform

Daemon

Notes

Android

traced (system-wide)

Built into the platform on Android 9 (API 28) and later. Apps must be declared profileable: add <profileable android:shell="true" /> to AndroidManifest.xml. See Setting permissions on Android.

Linux desktop

perfetto (user-started)

The producer connects through /tmp/perfetto-producer; start a host perfetto daemon first (traced --background).

Windows, QNX, embedded

–

No platform tracing daemon. Use the in-process capture mode described below, or fall back to ActiveWriters = perfetto to write a .perfetto-trace file.

Configuration

Activate the perfetto-producer writer via ActiveWriters in kanzimonitor.cfg:

ServiceTraceEnabled = 1
ActiveWriters = perfetto-producer
CollectingTimerRepeatInterval = 1000
WritingTimerRepeatInterval = 1000
CollectWithAppendingEnabled = 1

The collect/write timers still drive when Kanzi Monitor drains its profiler ring buffers into the producer — the perfetto-producer writer is the existing collect/write pipeline with a different sink, not a separate path.

You can combine perfetto-producer with file-output writers, e.g. ActiveWriters = perfetto-producer,chrometrace to stream to APA and keep a JSON file for offline analysis.

Capturing a trace

There are two ways to capture the live stream.

External capture (recommended). Let the platform’s tracing daemon collect Kanzi events into a system trace.

On Android, the perfetto CLI on the device drives the capture. Save the following config to kanzi.pbtx and push it to the device:

buffers {
  size_kb: 32768
}
data_sources {
  config {
    name: "track_event"
    track_event_config {
      enabled_categories: "kanzi.mainloop"
      enabled_categories: "kanzi.resource"
      enabled_categories: "kanzi.render"
      enabled_categories: "kanzi.layout"
      enabled_categories: "kanzi.domain"
      enabled_categories: "kanzi.startup"
      enabled_categories: "kanzi.interop"
      enabled_categories: "kanzi.generic"
    }
  }
}
data_sources { config { name: "linux.ftrace" } }
duration_ms: 10000

Then capture and pull the trace. Feed the config on stdin (-c -) and write the output into /data/misc/perfetto-traces/: the on-device perfetto sandbox cannot read a config from /data/local/tmp nor write output there — both fail with errno 13 (permission denied).

adb shell rm -f /data/misc/perfetto-traces/kanzi.pftrace
cat kanzi.pbtx | adb shell perfetto --txt -c - -o /data/misc/perfetto-traces/kanzi.pftrace
adb pull /data/misc/perfetto-traces/kanzi.pftrace

On Windows PowerShell, use Get-Content kanzi.pbtx | in place of cat kanzi.pbtx |.

Note

The perfetto-producer writer streams on the Trace Service writing timer, not on every frame. To capture events from an already-running application, set WritingTimerRepeatInterval to a non-zero value (for example 1000) in kanzimonitor.cfg so the Monitor flushes repeatedly. Otherwise start the capture at application launch, so the single WritingTimerOnceInterval flush falls inside the capture window. With no flush during the window, the trace contains only the kanzi.* category registrations and no events.

Android Performance Analyzer drives the same workflow from a GUI — select the application and add the eight kanzi.* track-event categories to the recording config.

On Linux desktop, point the host perfetto CLI at the same config.

In-process capture. When no external daemon is available (Windows, embedded), the plugin can drive its own session at startup. Set:

ActiveWriters = perfetto-producer
PerfettoProducerCaptureSeconds = 10

Kanzi Monitor starts an in-process Perfetto session at plugin load, runs it for PerfettoProducerCaptureSeconds seconds, and writes the resulting binary to <SessionLabel>_<timestamp>_producer.pftrace in the same output directory as the other writers. PerfettoProducerCaptureSeconds is independent of ActiveWriters — it drives the in-process Perfetto session rather than the writer registry, so it works even if you only have chrometrace and perfetto writers active.

Track-event categories

Kanzi Monitor registers a fixed set of track-event categories that map onto Kanzi’s existing profiling categories. Enable any subset in your perfetto config to filter what gets recorded:

Category

Maps to

kanzi.mainloop

Main-loop phases (frame, input / user / render stage wrappers, present).

kanzi.resource

Resource acquire / load / deploy operations.

kanzi.render

Scene-graph render-time operations (Node2DRender, applyMaterial, …).

kanzi.layout

Scene-graph layout operations (measure, arrange, recursive walks).

kanzi.domain

Scene-graph node operations (attach, detach, property changed).

kanzi.startup

Startup phases (kzb load, prefab instantiate, …).

kanzi.interop

Interop bridges (JNI, scripting, native callbacks).

kanzi.generic

Other Kanzi profiling samples (logging, internal collector ops, fallback for unmapped categories).

Timestamps are re-based onto Perfetto’s BOOTTIME clock so Kanzi events line up with ATrace and SurfaceFlinger in the same timeline.

Limitations

  • The producer registers on the same thread that loaded the plugin. Slices are emitted from the Kanzi main thread, so events land on that thread’s track in Perfetto UI.

Available commands

Command

Description

trace

Write trace to file.

overwatch.profilingcategories

List profiling categories (not kzTrace categories), get or set one, or apply a filter. See Choosing which profiling categories are recorded.

overwatch.kztracecategories

List the kzTrace categories (read-only). See Listing the kzTrace categories.

kzTrace: engine-native tracing

In addition to the Monitor profiling trace, Kanzi Monitor supports Kanzi engine-native tracing through the overwatch.kztrace command. This uses the kanzi::logTraceToFile() API from the Kanzi Engine tracing subsystem (kanzi/core/tracing/tracing.hpp).

kzTrace captures data from the Kanzi Engine kzTrace scope macros, covering frame stages, rendering, resource loading, and node operations. Like the Monitor profiling trace, kzTrace includes engine-internal instrumentation, but the two systems collect data independently using different mechanisms. It produces Chrome Trace JSON that you can view in Perfetto alongside Monitor profiling traces.

See the Tracing API reference and Tracing application configuration in the Kanzi Engine documentation.

Note

kzTrace is only available in Debug and Profiling builds (KANZI_TRACING_BUILD). In Release builds, overwatch.kztrace returns an error message.

Note

On Android, kzTrace output goes to the host application’s internal files directory — the same directory as the Monitor profiling traces — so no storage permission is needed. Kanzi Monitor points the engine’s tracing subsystem at that directory when the plugin initializes, and points it there again on every capture. If the directory cannot be resolved at all – no Java entry point has run yet, or the Kanzi Context and JavaVM are unavailable – overwatch.kztrace retries the resolution and then refuses the capture with a reason, rather than letting the engine fall back to its own default of /sdcard/Download/ (which would need WRITE_EXTERNAL_STORAGE below SDK 30, or MANAGE_EXTERNAL_STORAGE from SDK 30) and reporting a write that did not happen (KZMON-764). Copy traces off the device with adb shell run-as <package> cat files/<trace>.json > <trace>.json.

kzTrace output files are named kztrace_<timestamp>_<sequence>.json and are written to the same output directory as Monitor profiling traces, on every platform.

See Using the Overwatch service for the overwatch.kztrace and overwatch.fetchkztrace command reference.

Listing the kzTrace categories

kzTrace groups its scopes in categories of its own, declared with kzTraceCreateCategory. They are not the profiling categories of Choosing which profiling categories are recorded. overwatch.kztracecategories lists the ones the loaded code registers, with the name, description and id of each:

overwatch.kztracecategories
{"available":true,"switchable":false,"categories":[
  {"name":"General","description":"This is a general category.","ids":[2,3]},
  {"name":"Gfx","description":"Category for Kanzi Graphics.","ids":[1]},
  {"name":"Node2D","description":"Tracing Scopes from the Node2D class.","ids":[4]},
  {"name":"Wgl","description":"Windows graphics library.","ids":[0]}], ...}
  • The list holds every category that loaded code uses. Kanzi keeps a list of its kzTrace categories, but a category is created only where a kzTrace, kzTraceInstant or kzTraceSample in a loaded module refers to it. One that is declared but never used, or used only by a plugin that has not loaded, is not listed. A category used from more than one module, such as General, has a copy in each with its own id. Kanzi Monitor lists it once, with every id in ids.

  • Every listed category records. Kanzi 4.1 has no way to enable or disable a kzTrace category at runtime, so this listing is read-only and there is no filter for it. A category declared with enabled = false in kzTraceCreateCategory has its scopes compiled out: a kzTrace in it records nothing. kzTraceInstant and kzTraceSample in that category still record, and a category used that way is listed.

  • A captured kzTrace does not say which category an event belongs to. Kanzi writes the file in Chrome-compatibility mode, without category ids, so a capture cannot be filtered by category afterwards either.

  • Release builds have no kzTrace. The command then answers {"available":false,...}, as overwatch.kztrace reports an error there.

The engine request to switch kzTrace categories, and to keep their ids in the written file, is KANZI-936. The Web UI shows this list in the kzTrace Categories card, beside the Profiling Categories card on the Trace tab, and the MCP server through get_kztrace_categories.

See also

Using the Performance service

Using the Log service

Configuring Kanzi Monitor