Here’s a write-up of my experience with integrating Tracy, a very impressive profiler, into Compositor (on both Windows and macOS platforms) to get a detailed picture of what’s happening inside the app at runtime.
What is Tracy?
Tracy is a frame-based instrumentation profiler that allows you to visualize and analyze the performance of your application in great detail:

Frame-based means that it is well-suited to applications that have a clear frame-based structure, such as games. Frames do not need to be of a fixed duration, however, and they don’t need to be contiguous either. You can choose to use them, or not, that’s entirely up to you. In the screenshot above, the white lines demark (discontiguous) frames.
Instrumentation profiler means that you have to add code to your application to measure and record performance data. The instrumentation code will compile to no-ops when Tracy is disabled, so it won’t interfere with your application’s performance in non-profiling builds.
Tracy also has a sampling mode, but I did not try that. It also has GPU profiling and more. I encourage you to check out the documentation.
Tracy on Windows
As for prerequisites, I have the following installed on my Windows machine:
Building the Profiler App
Clone the https://github.com/wolfpld/tracy GitHub repository.
Then, in the root folder, run the following commands:
# configure
% cmake -S profiler -B profiler/build -DCMAKE_BUILD_TYPE=Release
# build
% cmake --build profiler/build --config Release
This should give you a tracy-profiler.exe executable in the profiler/build folder:

That’s it, just one self-contained tracy-profiler.exe executable.
For convenience, I have added a start menu shortcut to the executable.
Integrating Tracy into Compositor
Here’s an overview of Compositor’s project structure on Windows:

Compositor-Wincontains the Windows app & UI code and produces the app executable,Compositor.exe.WinSupportis a static library target containing Windows-specific, but not app-specific, support code for the app.WinInteropis a static library target containing COM interop code that needed to go into its own target.Coreis a static library target containing all the shared core functionality of the app. It’s written mostly in Swift, with a bit of C and C++.winrt-monorepois my Windows App SDK monorepo. Again, all static libraries here.TracyCis a static library target containing the C shims for Tracy’s macros (see below).
Compositor’s Windows build is based around CMake, a very flexible build system.
Here is the (slightly redacted) CMakeLists.txt in the Compositor-Win folder:
cmake_minimum_required(VERSION 3.29)
project(CompositorWin LANGUAGES C CXX Swift)
option(ENABLE_PROFILING "Enable profiling (Tracy)" OFF)
set(CMAKE_Swift_LANGUAGE_VERSION 5)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_STATIC_LIBRARY_PREFIX "lib")
...
# -- Compositor.exe --
file(GLOB_RECURSE COMPOSITOR_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/Sources/*.swift"
)
add_executable(Compositor ${COMPOSITOR_SOURCES})
...
# -- Core --
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../../Core"
"${CMAKE_BINARY_DIR}/Core-build")
# -- winrt-monorepo --
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../../../../winrt-monorepo"
"${CMAKE_BINARY_DIR}/winrt-monorepo-build")
# -- WinSupport --
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../WinSupport"
"${CMAKE_BINARY_DIR}/WinSupport-build")
# -- WinInterop --
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../WinInterop"
"${CMAKE_BINARY_DIR}/WinInterop-build")
# -- TracyC --
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../TracyC"
"${CMAKE_BINARY_DIR}/TracyC-build")
target_link_libraries(Compositor PRIVATE TracyC)
target_link_libraries(CompositorCore PRIVATE TracyC)
target_link_libraries(WinSupport PRIVATE TracyC)
target_link_libraries(Compositor PRIVATE
CompositorCore
WinSupport
WinInterop
CWinRT
UWP
Win2D
WinAppSDK
WindowsFoundation
WinUI
)
Setting up the TracyC Library
Since Tracy is mainly targeted at C and C++ projects, its instrumentation is based on macros. For integration into my mixed C/C++/Swift setup, I decided to wrap Tracy’s macros in Swift-friendly C shims.
For this, I have created a TracyC folder with two files:
TracySwiftShim.h
#pragma once
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
// Frame marks
void TracySwiftFrameMarkStart(const char* name);
void TracySwiftFrameMarkEnd(const char* name);
void TracySwiftFrameMarkNamed(const char* name);
// Thread name
void TracySwiftSetThreadName(const char* name);
// Zones
typedef struct TracySwiftZoneCtx { uint32_t id; int32_t active; } TracySwiftZoneCtx;
TracySwiftZoneCtx TracySwiftZoneBegin(const char* name);
void TracySwiftZoneEnd(TracySwiftZoneCtx ctx);
void TracySwiftZoneText(TracySwiftZoneCtx ctx, const char* text);
void TracySwiftZoneName(TracySwiftZoneCtx ctx, const char* name);
void TracySwiftZoneColor(TracySwiftZoneCtx ctx, uint32_t color);
void TracySwiftZoneValue(TracySwiftZoneCtx ctx, uint64_t value);
// ---- Plots ----
void TracySwiftPlot(const char* name, double value);
void TracySwiftPlotF(const char* name, float value);
void TracySwiftPlotI(const char* name, int64_t value);
void TracySwiftPlotConfig(const char* name, int32_t type, int32_t step, int32_t fill, uint32_t color);
// ---- Messages ----
// severity: use TracyMessageSeverity* values from TracyC.h (0..5)
// color: 0 means "default"
void TracySwiftMessage(const char* text);
void TracySwiftMessageColor(const char* text, uint32_t color);
void TracySwiftMessageWithSeverity(const char* text, int32_t severity);
void TracySwiftMessageColorWithSeverity(const char* text, int32_t severity, uint32_t color);
void TracySwiftAppInfo(const char* text);
#ifdef __cplusplus
}
#endif
and TracySwiftShim.c
#include "TracySwiftShim.h"
#ifdef TRACY_ENABLE
#include "tracy/TracyC.h"
#include <string.h>
// ---- Frame marks ----
void TracySwiftFrameMarkStart(const char* name) { ___tracy_emit_frame_mark_start(name); }
void TracySwiftFrameMarkEnd(const char* name) { ___tracy_emit_frame_mark_end(name); }
void TracySwiftFrameMarkNamed(const char* name) { ___tracy_emit_frame_mark(name); }
// ---- Thread name ----
void TracySwiftSetThreadName(const char* name) { ___tracy_set_thread_name(name); }
// ---- Zones ----
TracySwiftZoneCtx TracySwiftZoneBegin(const char* name)
{
const uint32_t line = 0;
const uint32_t color = 0;
const char* file = "file";
const char* func = "func";
const uint64_t srcloc = ___tracy_alloc_srcloc_name(
line,
file, strlen(file),
func, strlen(func),
name, name ? strlen(name) : 0,
color
);
int32_t active = 1;
TracyCZoneCtx ctx = ___tracy_emit_zone_begin_alloc(srcloc, active);
TracySwiftZoneCtx out;
out.id = ctx.id;
out.active = ctx.active;
return out;
}
void TracySwiftZoneEnd(TracySwiftZoneCtx ctx)
{
TracyCZoneCtx tctx; tctx.id = ctx.id; tctx.active = ctx.active;
___tracy_emit_zone_end(tctx);
}
void TracySwiftZoneText(TracySwiftZoneCtx ctx, const char* text)
{
if (!text) return;
TracyCZoneCtx tctx; tctx.id = ctx.id; tctx.active = ctx.active;
___tracy_emit_zone_text(tctx, text, strlen(text));
}
void TracySwiftZoneName(TracySwiftZoneCtx ctx, const char* name)
{
if (!name) return;
TracyCZoneCtx tctx; tctx.id = ctx.id; tctx.active = ctx.active;
___tracy_emit_zone_name(tctx, name, strlen(name));
}
void TracySwiftZoneColor(TracySwiftZoneCtx ctx, uint32_t color)
{
TracyCZoneCtx tctx; tctx.id = ctx.id; tctx.active = ctx.active;
___tracy_emit_zone_color(tctx, color);
}
void TracySwiftZoneValue(TracySwiftZoneCtx ctx, uint64_t value)
{
TracyCZoneCtx tctx; tctx.id = ctx.id; tctx.active = ctx.active;
___tracy_emit_zone_value(tctx, value);
}
// ---- Plots ----
void TracySwiftPlot(const char* name, double value) { ___tracy_emit_plot(name, value); }
void TracySwiftPlotF(const char* name, float value) { ___tracy_emit_plot_float(name, value); }
void TracySwiftPlotI(const char* name, int64_t value) { ___tracy_emit_plot_int(name, value); }
void TracySwiftPlotConfig(const char* name, int32_t type, int32_t step, int32_t fill, uint32_t color)
{
___tracy_emit_plot_config(name, type, step, fill, color);
}
// ---- Messages ----
// callstack_depth: use TRACY_CALLSTACK (from TracyC.h, defaults to 0 unless you set it)
// Using ___tracy_emit_logStringL means "null-terminated string"
static void TracySwiftEmitMessage(int32_t severity, uint32_t color, const char* text)
{
if (!text) return;
___tracy_emit_logStringL((int8_t)severity, (int32_t)color, TRACY_CALLSTACK, text);
}
void TracySwiftMessage(const char* text)
{
TracySwiftEmitMessage(TracyMessageSeverityInfo, 0, text);
}
void TracySwiftMessageColor(const char* text, uint32_t color)
{
TracySwiftEmitMessage(TracyMessageSeverityInfo, color, text);
}
void TracySwiftMessageWithSeverity(const char* text, int32_t severity)
{
TracySwiftEmitMessage(severity, 0, text);
}
void TracySwiftMessageColorWithSeverity(const char* text, int32_t severity, uint32_t color)
{
TracySwiftEmitMessage(severity, color, text);
}
void TracySwiftAppInfo(const char* text)
{
if (!text) return;
___tracy_emit_message_appinfo(text, strlen(text));
}
#else
// Profiling disabled: no-ops.
void TracySwiftFrameMarkStart(const char* name) { (void)name; }
void TracySwiftFrameMarkEnd(const char* name) { (void)name; }
void TracySwiftFrameMarkNamed(const char* name) { (void)name; }
void TracySwiftSetThreadName(const char* name) { (void)name; }
TracySwiftZoneCtx TracySwiftZoneBegin(const char* name)
{
(void)name;
TracySwiftZoneCtx ctx = { 0, 0 };
return ctx;
}
void TracySwiftZoneEnd(TracySwiftZoneCtx ctx) { (void)ctx; }
void TracySwiftZoneText(TracySwiftZoneCtx ctx, const char* text) { (void)ctx; (void)text; }
void TracySwiftZoneName(TracySwiftZoneCtx ctx, const char* name) { (void)ctx; (void)name; }
void TracySwiftZoneColor(TracySwiftZoneCtx ctx, uint32_t color) { (void)ctx; (void)color; }
void TracySwiftZoneValue(TracySwiftZoneCtx ctx, uint64_t value) { (void)ctx; (void)value; }
void TracySwiftPlot(const char* name, double value) { (void)name; (void)value; }
void TracySwiftPlotF(const char* name, float value) { (void)name; (void)value; }
void TracySwiftPlotI(const char* name, int64_t value) { (void)name; (void)value; }
void TracySwiftPlotConfig(const char* name, int32_t type, int32_t step, int32_t fill, uint32_t color)
{ (void)name; (void)type; (void)step; (void)fill; (void)color; }
void TracySwiftMessage(const char* text) { (void)text; }
void TracySwiftMessageColor(const char* text, uint32_t color) { (void)text; (void)color; }
void TracySwiftMessageWithSeverity(const char* text, int32_t severity) { (void)text; (void)severity; }
void TracySwiftMessageColorWithSeverity(const char* text, int32_t severity, uint32_t color)
{ (void)text; (void)severity; (void)color; }
void TracySwiftAppInfo(const char* text) { (void)text; }
#endif
And here is TracyC’s CMakeLists.txt:
cmake_minimum_required(VERSION 3.29)
include(FetchContent)
# --- Shim library that is always built ---
add_library(TracyC STATIC
TracySwiftShim.c
)
target_include_directories(TracyC PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}" # TracySwiftShim.h
)
# ---- modulemap ----
set(TRACY_C_MODULE_DIR "${CMAKE_CURRENT_BINARY_DIR}/include/TracyC")
file(MAKE_DIRECTORY "${TRACY_C_MODULE_DIR}")
set(TRACY_C_MODULEMAP "${TRACY_C_MODULE_DIR}/module.modulemap")
# We’ll include TracyC.h only when profiling is ON (see below).
set(TRACY_SHIM_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/TracySwiftShim.h")
file(TO_CMAKE_PATH "${TRACY_SHIM_HEADER}" TRACY_SHIM_HEADER_CMAKE)
# Write a minimal module that always exists (shim only).
file(WRITE "${TRACY_C_MODULEMAP}"
"module TracyC {\n"
" header \"${TRACY_SHIM_HEADER_CMAKE}\"\n"
" export *\n"
"}\n"
)
target_compile_options(TracyC PUBLIC
$<$<COMPILE_LANGUAGE:Swift>:
SHELL:-Xcc -fmodule-map-file=$<SHELL_PATH:${TRACY_C_MODULEMAP}>
-Xcc -I$<SHELL_PATH:${CMAKE_CURRENT_SOURCE_DIR}>
>
)
# --- Only when profiling is enabled: fetch Tracy + expose TracyC.h + link Tracy client ---
if(ENABLE_PROFILING)
FetchContent_Declare(
tracy
GIT_REPOSITORY https://github.com/wolfpld/tracy.git
GIT_TAG master
GIT_SHALLOW TRUE
GIT_PROGRESS TRUE
)
set(TRACY_NO_SYSTEM_TRACING ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(tracy)
# Make tracy headers visible to consumers
target_include_directories(TracyC PUBLIC
"${tracy_SOURCE_DIR}/public"
)
# Define TRACY_ENABLE for the shim and any dependents
target_compile_definitions(TracyC PUBLIC TRACY_ENABLE)
# Link the actual tracy client into the static library
target_link_libraries(TracyC PRIVATE Tracy::TracyClient)
# Update modulemap to include TracyC.h as well
set(TRACY_C_HEADER "${tracy_SOURCE_DIR}/public/tracy/TracyC.h")
file(TO_CMAKE_PATH "${TRACY_C_HEADER}" TRACY_C_HEADER_CMAKE)
file(WRITE "${TRACY_C_MODULEMAP}"
"module TracyC {\n"
" header \"${TRACY_C_HEADER_CMAKE}\"\n"
" header \"${TRACY_SHIM_HEADER_CMAKE}\"\n"
" export *\n"
"}\n"
)
# Ensure Swift can see tracy/public too
target_compile_options(TracyC PUBLIC
$<$<COMPILE_LANGUAGE:Swift>:
-Xcc -I$<SHELL_PATH:${tracy_SOURCE_DIR}/public>
>
)
set(_tracy_target "")
if(TARGET Tracy::TracyClient)
get_target_property(_aliased Tracy::TracyClient ALIASED_TARGET)
if(_aliased)
set(_tracy_target "${_aliased}") # real target behind alias
else()
set(_tracy_target "Tracy::TracyClient") # not an alias in this build
endif()
endif()
# Apply settings if we found a target to patch
if(_tracy_target AND TARGET "${_tracy_target}")
# Match Swift runtime expectations on Windows:
# use /MD and iterator debug level 0 even in profiling builds
set_property(TARGET "${_tracy_target}" PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreadedDLL" # /MD
)
target_compile_definitions("${_tracy_target}" PRIVATE
_ITERATOR_DEBUG_LEVEL=0
_HAS_ITERATOR_DEBUGGING=0
NDEBUG
)
endif()
endif()
There are certainly more elegant ways to do this, but this is what I (cough, and ChatGPT, cough) came up with.
The CMakePresets.json in the different folders are all identical:
{
"version": 6,
"cmakeMinimumRequired": {
"major": 3,
"minor": 29,
"patch": 0
},
"configurePresets": [
{
"name": "base-ninja-clang",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_C_COMPILER": "$env{SWIFT_TOOLCHAIN_PATH}/bin/clang.exe",
"CMAKE_CXX_COMPILER": "$env{SWIFT_TOOLCHAIN_PATH}/bin/clang++.exe",
"CMAKE_Swift_COMPILER": "$env{SWIFT_TOOLCHAIN_PATH}/bin/swiftc.exe",
"SWIFT_TOOLCHAIN_PATH": "$env{SWIFT_TOOLCHAIN_PATH}",
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"VCPKG_TARGET_TRIPLET": "$env{VCPKG_TARGET_TRIPLET}"
}
},
{
"name": "debug",
"inherits": "base-ninja-clang",
"displayName": "CMake Configure (Debug)",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"inherits": "base-ninja-clang",
"displayName": "CMake Configure (Release)",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "relwithdebinfo",
"inherits": "base-ninja-clang",
"displayName": "CMake Configure (RelWithDebInfo)",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{
"name": "profile",
"inherits": "relwithdebinfo",
"displayName": "CMake Configure (Profile)",
"cacheVariables": {
"ENABLE_PROFILING": "ON"
}
}
],
"buildPresets": [
{
"name": "debug",
"configurePreset": "debug"
},
{
"name": "release",
"configurePreset": "release"
},
{
"name": "relwithdebinfo",
"configurePreset": "relwithdebinfo"
},
{
"name": "profile",
"configurePreset": "profile"
}
]
}
The SWIFT_TOOLCHAIN_PATH environment variable points to the Swift toolchain installation
(which varies between the Windows installations on my two machines):

Instrumenting the Code
Tracy offers three ways of instrumentation:
- frames (contiguous or non-contiguous)
- zones (scopes, regions of interest)
- messages (points on the timeline)
Note that I am focusing on Tracy’s C API here. Usage from C++ would look different.
Frame-based Instrumentation
I am using Tracy’s discontinuous frame API (cf. the manual for details) to mark frame start and end points:
private func typesetFocusedPage(fromPos: Int = 0) {
TracySwiftFrameMarkStart("typesetFocusedPage") // <- mark start of frame
...
let completion: PagesCompletion = { ...
DispatchQueue.main.async {
...
TracySwiftFrameMarkEnd("typesetFocusedPage") // <- mark end of frame
}
}
...
let pagePassOperation = PagePassOperation(
...
completion: completion)
typesetterQueue.cancelAllOperations()
typesetterQueue.addOperation(pagePassOperation)
}

Zone-based Instrumentation
Zones are regions of interest that you want to mark on the timeline. Zones can be nested, which will create stacked regions in the timeline.
fileprivate final class PagePassOperation: AsyncOperation, @unchecked Sendable {
...
override func main() {
guard !isCancelled else {
finish()
return
}
zone = TracySwiftZoneBegin("PagePassOperation") // <- mark begin of zone
tex.focus(documentName, page: fromPage)
source.withCStringPointer(buffer: charBuffer) { cString in
tex.typesetFocusedPage(
...
completion: { [weak self] totalPages, reachedEndOfDocument in
self?.completion(totalPages, reachedEndOfDocument)
self?.finish()
})
}
}
override func finish() {
super.finish()
if let zone {
TracySwiftZoneEnd(zone) // <- mark end of zone
self.zone = nil
}
}
}

Messages
Messages can be added on top of frames or zones to mark points of interest (comparable
to os_signpost in Instruments).
let pageCompletion: PageCompletion = { ...
TracySwiftMessage("pageCompletion, before dispatching to main queue") // <-- mark point
DispatchQueue.main.async {
TracySwiftMessage("pageCompletion, on main queue") // <-- mark point
...
}
}
let pagePassOperation = PagePassOperation(
...
pageCompletion: pageCompletion,
...)
typesetterQueue.cancelAllOperations()
typesetterQueue.addOperation(pagePassOperation)
Note the small red triangles above the zone:


Windows Deployment
If you want to test your packaged Windows app in its installed state,
the AppxManifest.xml needs to have the privateNetworkClientServer capability
enabled for profiling, otherwise the profiler will not be able to connect to the app:
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities">
<Identity Name="Compositor"
Version="0.5.0.0"
Publisher="CN=Karl Traunmueller, C=Austria"
ProcessorArchitecture="arm64" />
...
<Capabilities>
<Capability Name="internetClient"/>
<Capability Name="privateNetworkClientServer"/> <!-- required for profiling -->
</Capabilities>
<Applications>
<Application Id="Compositor"
Executable="Compositor.exe"
...>
...
</Application>
</Applications>
</Package>
Tracy on macOS
Building the Profiler App
Same steps as on Windows: clone the https://github.com/wolfpld/tracy GitHub repository.
Then, in the root folder, run the following commands:
# configure
% cmake -S profiler -B profiler/build -DCMAKE_BUILD_TYPE=Release
# build
% cmake --build profiler/build --config Release
After a lot of exciting and colourful console action, you should see a tracy-profiler executable
in the profiler/build folder:

Integrating Tracy into Compositor
On macOS, Compositor’s project structure is quite different. The Xcode project that builds the app target,
Compositor-Mac, pulls in dependencies via Swift Package Manager (no CMake used here at all).
The two interesting packages here are CompositorCore and tracy:

CompositorCore’s Package.swift (again, slightly redacted) looks like this:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "CompositorCore",
platforms: [.macOS(.v10_15)],
products: [
.library(
name: "CompositorCore",
type: .static,
targets: ["CompositorCore"])
],
dependencies: [
...
.package(path: "../../../../ktraunmueller/tracy")
],
targets: [
// libraries (C/C++)
.target(
name: "lz4"
),
.target(
name: "spdlog",
...
),
// Core (C++)
.target(
name: "cppbridge",
dependencies: ["lz4", "spdlog"],
...
swiftSettings: [.interoperabilityMode(.Cxx)]
),
// Core (Swift)
.target(
name: "CompositorCore",
dependencies: [
"cppbridge",
"lz4",
"spdlog",
.product(name: "Collections", package: "swift-collections"),
.product(name: "TracyC", package: "tracy")
],
resources: [
...
],
swiftSettings: [.interoperabilityMode(.Cxx)]
),
.testTarget(
name: "CompositorCoreTests",
...
),
],
swiftLanguageModes: [.v5],
cLanguageStandard: .c17,
cxxLanguageStandard: .cxx17
)
Since all dependencies are pulled into the project via Swift Package Manager, the obvious choice
for integrating Tracy is to set up a Package.swift for Tracy.
What I opted for was to fork the Tracy repository, add
a Package.swift file, add the same C shims as before, and include all of that into the project.
Here’s the Package.swift file I created for Tracy:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "tracy",
platforms: [.macOS(.v10_15)],
products: [
.library(name: "TracyC", targets: ["TracyC"]),
],
targets: [
.target(
name: "TracyClient",
path: "public",
sources: ["TracyClient.cpp"],
publicHeadersPath: "include",
cxxSettings: [
.define("TRACY_ENABLE", to: "1"), // TODO pull in from environment
.headerSearchPath("."),
.headerSearchPath("tracy"),
]
),
.target(
name: "TracyC",
dependencies: ["TracyClient"],
path: "swift/TracyC",
sources: ["TracySwiftShim.c"],
publicHeadersPath: "include",
cSettings: [
.define("TRACY_ENABLE", to: "1"), // TODO pull in from environment
],
cxxSettings: [
.define("TRACY_ENABLE", to: "1"),
.headerSearchPath("../../public"),
.headerSearchPath("../../public/tracy"),
]
)
],
cxxLanguageStandard: .cxx17
)
macOS Deployment
Compositor is a sandboxed app on macOS. In order to allow the profiler to connect to the
app, I needed to add the com.apple.security.network.server entitlement to a new Compositor-Profile.entitlements file:

which is activated in via a new Profile build configuration

that pulls in the entitlements file in the build settings:

So whenever I build for profiling:

this entitlement is active and the profiler will be able to connect to the app.
Summing it Up
I think Tracy is great tool for analyzing the performance of Swift applications, especially on Windows, but also as an alternative to Instruments on macOS.