Skip to content

Godot SDK

This guide explains how to automatically collect performance telemetry using the Framedash Godot SDK (C# addon).

  • Godot 4.3 or later with the .NET (C#) build. The standard GDScript-only build cannot compile the C# addon.
  • .NET 8 SDK or newer (any SDK that can target net8.0; the addon builds into your game assembly targeting net8.0)
  • Web (HTML5) export is not supported because Godot 4 does not support .NET web exports.

The SDK installs either from the Godot Asset Library (editor GUI) or manually from GitHub (scripted / CI-friendly). For a CI or scripted setup, use the manual installation below, since the Asset Library flow requires the editor GUI.

The SDK is published in the Godot Asset Library. This path is GUI-only. In the Godot editor:

  1. Open the AssetLib tab
  2. Search for Framedash Telemetry SDK and select version 0.1.8
  3. Click Download, then Install. Keep addons/framedash/ selected so the addon is installed at res://addons/framedash/

For a release-pinned or CI-friendly installation, clone the Framedash Godot SDK repository:

Terminal window
git clone --branch v0.1.8 --depth 1 https://github.com/crane-valley/framedash-godot-sdk.git

Pin a release tag (v0.1.8) rather than tracking the default branch, so the checkout is reproducible. After cloning, create the project’s addons/ directory if it does not exist, then copy the repository’s addons/framedash/ folder into res://addons/.

After installing with either method:

  1. Build the C# solution once. This generates the .csproj / .sln and compiles the addon so its autoload type exists.
  2. Open Project > Project Settings > Plugins in the Godot editor
  3. Enable Framedash Telemetry SDK

Enabling the plugin registers a Framedash autoload singleton. Initialize the SDK once near startup, for example from _Ready():

using Framedash;
using Godot;
public partial class GameBootstrap : Node
{
public override void _Ready()
{
TelemetrySDK.Initialize(
apiKey: "your-api-key",
buildId: "1.0.0");
}
}

endpointUrl and buildId are optional. When endpointUrl is omitted, the SDK uses https://ingest.framedash.dev/v1/events.

The SDK automatically collects:

  • FPS / Frame Time: computed from real frame delta time
  • Memory: Godot static memory
  • GPU / Render CPU Time: latest frame timings reported by RenderingServer
  • Game Thread Time: time spent in _process

session_start is sent on initialization, and perf_heartbeat is sent every 10 seconds.

TelemetrySDK.Instance.Track(
eventName: "player_death",
mapId: "map_01",
position: playerNode.GlobalPosition);

To attach categorical attributes or numeric metrics:

using System.Collections.Generic;
TelemetrySDK.Instance.Track(
eventName: "player_death",
mapId: "map_01",
position: playerNode.GlobalPosition,
attributes: new Dictionary<string, string> { { "cause", "fall_damage" } },
metrics: new Dictionary<string, float> { { "health", 0f } });

Tracked events buffer in memory and flush automatically on a 30-second interval, or when a batch reaches 100 events or 100 KB, whichever comes first. A long-running game never has to flush by hand.

A short-lived or headless run can exit before the next automatic flush. In that case, call Flush() explicitly and keep the process alive until delivery is confirmed (verbose logging prints the HTTP result). Flush() only starts the send asynchronously, and the final drain happens as the process winds down, so call Flush() and allow a brief moment before quitting rather than quitting on the same line. Godot SDK 0.1.5 and later drains the buffer synchronously on a clean shutdown (bounded to 2.5s). There is still no offline queue, so an event whose send fails within that budget, or one left buffered when the process is force-killed, is lost. See Troubleshooting.

TelemetrySDK.Instance.Flush();

By default events are sent anonymously. Call SetPlayerId after login to associate subsequent events with a player:

TelemetrySDK.Instance.SetPlayerId(playerId);

The SDK applies a global sampling rate (default 1.0 = keep all). High-frequency events can opt into a lower per-event-name rate that overrides the global rate at runtime:

Framedash.TelemetrySDK.Instance.SetEventSamplingRate("ai_pathfind_step", 0.05f); // ~5%
Framedash.TelemetrySDK.Instance.RemoveEventSamplingRate("ai_pathfind_step"); // back to the global rate

SetEventSamplingRate(string eventName, float rate) sets the rate, clamped to [0, 1], and RemoveEventSamplingRate(string eventName) drops the override. Auto-collected events (session_start, perf_heartbeat) bypass sampling.

Available in Godot SDK 0.1.4 and later. The SDK can time how long a scene or level takes to load and report it as a map_load event, which feeds the build-comparison / perf-diff regression gate and the dashboard load-time charts.

Wrap a load you control with BeginMapLoad / EndMapLoad:

TelemetrySDK.Instance.BeginMapLoad("Level_01");
// ... load the scene ...
TelemetrySDK.Instance.EndMapLoad();

BeginMapLoad starts a timer on a monotonic clock that pause and time-scale do not affect, and EndMapLoad stops it and emits the event. Calling BeginMapLoad again before EndMapLoad replaces the pending measurement.

If a custom or streaming loader already measures the time itself, report it directly:

TelemetrySDK.Instance.ReportMapLoad("Level_01", 1234f); // load time in milliseconds

ReportMapLoad drops the sample entirely when loadTimeMs is NaN, infinite, or negative; the value is not clamped.

Both paths emit a map_load event carrying metrics["load_time_ms"] and attributes["map_name"]. The event leaves map_id empty on purpose, so it stays out of spatial heatmaps and the activation gate. These calls are safe to make from any thread, never throw, and are a no-op before Initialize.

Available in Godot SDK 0.1.4 and later. The SDK can attach disk-read counters to the perf_heartbeat event under the io.read_bytes, io.read_time_ms, and io.read_ops metric keys. Each value is a delta since the previous heartbeat, and the keys appear only after a real sample has landed (there is no zero-stuffing). Like the other performance metrics, io.* feeds the perf-diff / builds-compare regression gate and the dashboard charts. There are no io.* threshold alerts.

Godot has no automatic disk-I/O source, so feed samples yourself with ReportIoSample:

TelemetrySDK.Instance.ReportIoSample(bytes: 1048576, readTimeMs: 3.2f, ops: 12);

Available in Godot SDK 0.1.5 and later. The SDK samples memory-category usage at the heartbeat cadence and automatically attaches it as mem.* metrics on perf_heartbeat events. The values come from Godot’s Performance monitors, and no opt-in is needed.

  • mem.vram: video memory in use, in bytes.
  • mem.textures: video memory used by textures, in bytes.
  • mem.buffers: video memory used by buffers, in bytes.

A reading of zero or below is omitted: an absent key means the value was not collected, and the SDK never sends a fabricated 0.

Besides perf_heartbeat, these keys attach to position-qualified events (any event with a non-empty map_id). Because perf_heartbeat carries an empty map_id and never enters the spatial heatmap grid, the SDK attaches the same sample to position-qualified events so per-cell memory heatmaps work. Position-qualified events carry a cached sample refreshed at the heartbeat cadence.

Caller-supplied metric keys always win, both on a key collision and on capacity: mem.* fills only the slots left below the 50-metric ingest cap, with mem.vram first.

The plugin-registered autoload is created from TelemetrySDK.cs using code defaults, so its [Export] fields are not edited through Project Settings. Configure the SDK by calling TelemetrySDK.Initialize(...) in normal setups.

If you add the TelemetrySDK script to a scene node that you control, for example your own autoload scene, then its [Export] fields for API key, endpoint, sampling rate, and camera rotation can be edited in the Inspector.

During your first integration, enable verbose logging to confirm delivery:

TelemetrySDK.Instance.VerboseLogging = true;

A transport failure logs the retry ladder, for example [Framedash] Retry 1/3 in 1.0s (HTTP 0). HTTP 0 means a transport-level failure (DNS, TLS, or timeout) rather than an API rejection. If events do not arrive, see Troubleshooting.

Godot’s editor Build generates the .csproj / .sln for a C# project. godot --headless --build-solutions --quit compiles an existing solution, but it cannot generate the initial .csproj / .sln on a project that has never been built: on a fresh CI-only project it can finish with exit code 0 and no error yet still write no project files (observed on 4.6.3 mono). So the first build cannot be a headless one.

For a CI-only project that has never been opened in the editor, create the project file yourself first. Hand-author <project>.csproj at the project root with Sdk="Godot.NET.Sdk/<engine version>" targeting net8.0, and match the Godot.NET.Sdk version to the Godot version you build with:

<Project Sdk="Godot.NET.Sdk/4.6.3">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
</Project>

If you can open the project in the editor at least once, the alternative is to click Build there once and commit the generated .csproj / .sln.

With the project file in place, build with the .NET SDK:

Terminal window
dotnet build

The compiled assembly lands in .godot/mono/temp/bin/Debug/. Because the plugin only registers an autoload that Initialize can create itself, you do not need to enable the plugin through the editor UI for a headless run.

See Troubleshooting if events do not arrive.

In an automated test or profiling run, tag the whole session so every event carries the CI build and its branch/commit/scenario. Call the automated-session API once from your test entry point:

using Framedash;
// All arguments are optional.
TelemetrySDK.Instance.BeginAutomatedSession(
buildId: "build-123",
branch: "main",
commit: "abc1234",
scenario: "nightly");
// ... run the automated scenario ...
TelemetrySDK.Instance.EndAutomatedSession();

The full signature is void BeginAutomatedSession(string buildId = null, string branch = null, string commit = null, string scenario = null). In CI, prefer BeginAutomatedSessionFromEnvironment(), which reads FRAMEDASH_BUILD_ID, FRAMEDASH_GIT_BRANCH, FRAMEDASH_GIT_COMMIT, and FRAMEDASH_TEST_SCENARIO (the variables framedash run-profile-test exports):

TelemetrySDK.Instance.BeginAutomatedSessionFromEnvironment();
// ... run the automated scenario ...
TelemetrySDK.Instance.EndAutomatedSession();

An automated session tags every event in the session with a build_id override and ci.branch / ci.commit / ci.scenario attributes, which is what feeds the build-comparison / perf-diff CI gate. It does not change any event’s source: the SDK’s automatic events (session_start, perf_heartbeat) stay source=automated and your Track events stay source=player, in CI as in normal play. See CI Profiling for the full pipeline.