Profiling Swift Applications on Windows and macOS with Tracy

13 min read Original article ↗

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-Win contains the Windows app & UI code and produces the app executable, Compositor.exe.
  • WinSupport is a static library target containing Windows-specific, but not app-specific, support code for the app.
  • WinInterop is a static library target containing COM interop code that needed to go into its own target.
  • Core is 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-monorepo is my Windows App SDK monorepo. Again, all static libraries here.
  • TracyC is 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.