diff --git a/Assets/Tests/InputSystem.Editor/InputLatencyCalculatorTests.cs b/Assets/Tests/InputSystem.Editor/InputLatencyCalculatorTests.cs
new file mode 100644
index 0000000000..e8a099be37
--- /dev/null
+++ b/Assets/Tests/InputSystem.Editor/InputLatencyCalculatorTests.cs
@@ -0,0 +1,91 @@
+using NUnit.Framework;
+using Unity.Collections;
+using UnityEngine.InputSystem.Editor;
+using UnityEngine.InputSystem.LowLevel;
+
+namespace Tests.InputSystem.Editor
+{
+ public class InputLatencyCalculatorTests
+ {
+ private InputLatencyCalculator m_Sut; // Software Under Test
+
+ ///
+ /// Adds a number of samples to the calculator under test.
+ ///
+ /// The current timestamp (reflecting wall-clock time).
+ /// List of timestamps to be propagated as events to the calculator.
+ private void GivenSamples(double currentRealtimeSinceStartup, params double[] samplesRealtimeSinceStartup)
+ {
+ var n = samplesRealtimeSinceStartup.Length;
+ using (InputEventBuffer buffer = new InputEventBuffer())
+ {
+ unsafe
+ {
+ for (var i = 0; i < n; ++i)
+ {
+ var ptr = buffer.AllocateEvent(InputEvent.kBaseEventSize, 2048, Allocator.Temp);
+ ptr->time = samplesRealtimeSinceStartup[i];
+ m_Sut.ProcessSample(ptr, currentRealtimeSinceStartup);
+ }
+ }
+ }
+ }
+
+ private void AssertNoMetrics()
+ {
+ Assert.True(float.IsNaN(m_Sut.averageLatencySeconds));
+ Assert.True(float.IsNaN(m_Sut.minLatencySeconds));
+ Assert.True(float.IsNaN(m_Sut.maxLatencySeconds));
+ }
+
+ [Test]
+ public void MetricsShouldBeUndefined_WhenCalculatorHasNoSamples()
+ {
+ m_Sut = new InputLatencyCalculator(0.0);
+ AssertNoMetrics();
+ m_Sut.Update(10.0);
+ AssertNoMetrics();
+ }
+
+ [Test]
+ public void MetricsShouldBeDefined_WhenCalculatorHasSamplesButPeriodHasNotElapsed()
+ {
+ m_Sut = new InputLatencyCalculator(1.0);
+ GivenSamples(1.9, 1.1, 1.2, 1.9);
+ m_Sut.Update(1.99);
+ AssertNoMetrics();
+ }
+
+ [Test]
+ public void MetricsShouldOnlyUpdate_WhenPeriodHasElapsed()
+ {
+ m_Sut = new InputLatencyCalculator(0.0);
+ GivenSamples(0.5, 0.3, 0.4, 0.5);
+ m_Sut.Update(1.0);
+ Assert.That(m_Sut.averageLatencySeconds, Is.EqualTo(0.1f));
+ Assert.That(m_Sut.minLatencySeconds, Is.EqualTo(0.0f));
+ Assert.That(m_Sut.maxLatencySeconds, Is.EqualTo(0.2f));
+ }
+
+ [Test]
+ public void MetricShouldNotBeAffected_WhenMoreThanAPeriodHasElapsed()
+ {
+ m_Sut = new InputLatencyCalculator(0.0);
+ GivenSamples(0.5, 0.3, 0.4, 0.5);
+ m_Sut.Update(2.0);
+ Assert.That(m_Sut.averageLatencySeconds, Is.EqualTo(0.1f));
+ Assert.That(m_Sut.minLatencySeconds, Is.EqualTo(0.0f));
+ Assert.That(m_Sut.maxLatencySeconds, Is.EqualTo(0.2f));
+ }
+
+ [Test]
+ public void MetricsShouldReset_WhenNotReceivingASampleForAPeriod()
+ {
+ m_Sut = new InputLatencyCalculator(0.0);
+ GivenSamples(0.5, 0.3, 0.4, 0.5);
+ m_Sut.Update(1.0);
+ m_Sut.Update(2.0);
+ AssertNoMetrics();
+ }
+ }
+}
diff --git a/Assets/Tests/InputSystem.Editor/InputLatencyCalculatorTests.cs.meta b/Assets/Tests/InputSystem.Editor/InputLatencyCalculatorTests.cs.meta
new file mode 100644
index 0000000000..f3c1c871ee
--- /dev/null
+++ b/Assets/Tests/InputSystem.Editor/InputLatencyCalculatorTests.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 92802a97ff47467cb31ea193f60fe812
+timeCreated: 1739993502
\ No newline at end of file
diff --git a/Assets/Tests/InputSystem.Editor/SampleFrequencyCalculatorTests.cs b/Assets/Tests/InputSystem.Editor/SampleFrequencyCalculatorTests.cs
new file mode 100644
index 0000000000..1b2e3ec82b
--- /dev/null
+++ b/Assets/Tests/InputSystem.Editor/SampleFrequencyCalculatorTests.cs
@@ -0,0 +1,87 @@
+using NUnit.Framework;
+using Unity.Collections;
+using UnityEngine.InputSystem.Editor;
+using UnityEngine.InputSystem.LowLevel;
+
+namespace Tests.InputSystem.Editor
+{
+ public class SampleFrequencyCalculatorTests
+ {
+ private SampleFrequencyCalculator m_Sut; // Software Under Test
+
+ ///
+ /// Adds a number of samples to the calculator under test.
+ ///
+ /// List of timestamps to be propagated as events to the calculator.
+ private void GivenSamples(params double[] samplesRealtimeSinceStartup)
+ {
+ var n = samplesRealtimeSinceStartup.Length;
+ using (InputEventBuffer buffer = new InputEventBuffer())
+ {
+ unsafe
+ {
+ for (var i = 0; i < n; ++i)
+ {
+ var ptr = buffer.AllocateEvent(InputEvent.kBaseEventSize, 2048, Allocator.Temp);
+ ptr->time = samplesRealtimeSinceStartup[i];
+ m_Sut.ProcessSample(ptr);
+ }
+ }
+ }
+ }
+
+ [Test]
+ public void FrequencyShouldBeZero_WhenCalculatorHasNoSamples()
+ {
+ m_Sut = new SampleFrequencyCalculator(10, 0.0);
+ Assert.That(m_Sut.frequency, Is.EqualTo(0.0f));
+ }
+
+ [Test]
+ public void FrequencyShouldBeZero_WhenCalculatorHasSamplesButPeriodHasNotElapsed()
+ {
+ m_Sut = new SampleFrequencyCalculator(10.0f, 1.0);
+ GivenSamples(1.1, 1.2, 1.9);
+ m_Sut.Update(1.99);
+ Assert.That(m_Sut.frequency, Is.EqualTo(0.0f));
+ }
+
+ [Test]
+ public void MetricsShouldOnlyUpdate_WhenPeriodHasElapsed()
+ {
+ m_Sut = new SampleFrequencyCalculator(10.0f, 0.0);
+ GivenSamples(0.3, 0.4, 0.5);
+ m_Sut.Update(1.0);
+ Assert.That(m_Sut.frequency, Is.EqualTo(3.0f));
+ }
+
+ [Test]
+ public void MetricsShouldBeBasedOnActualElapsedPeriod_WhenMoreThanAPeriodHasElapsed()
+ {
+ m_Sut = new SampleFrequencyCalculator(10.0f, 0.0);
+ GivenSamples(0.5, 0.3, 0.4, 0.5);
+ m_Sut.Update(2.0);
+ Assert.That(m_Sut.frequency, Is.EqualTo(2.0f));
+ }
+
+ [Test]
+ public void MetricsShouldNotBeUpdated_WhenLessThanAPeriodHasElapsed()
+ {
+ m_Sut = new SampleFrequencyCalculator(10.0f, 0.0);
+ GivenSamples(0.5, 0.3, 0.4, 0.5);
+ m_Sut.Update(2.0);
+ m_Sut.Update(2.9);
+ Assert.That(m_Sut.frequency, Is.EqualTo(2.0f));
+ }
+
+ [Test]
+ public void MetricsShouldReset_WhenNotReceivingASampleForAPeriod()
+ {
+ m_Sut = new SampleFrequencyCalculator(1.0f, 0.0);
+ GivenSamples(0.5, 0.3, 0.4, 0.5);
+ m_Sut.Update(1.0);
+ m_Sut.Update(2.0);
+ Assert.That(m_Sut.frequency, Is.EqualTo(0.0f));
+ }
+ }
+}
diff --git a/Assets/Tests/InputSystem.Editor/SampleFrequencyCalculatorTests.cs.meta b/Assets/Tests/InputSystem.Editor/SampleFrequencyCalculatorTests.cs.meta
new file mode 100644
index 0000000000..624ab43fc5
--- /dev/null
+++ b/Assets/Tests/InputSystem.Editor/SampleFrequencyCalculatorTests.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 6af1a2c27474415ebf7a5943bea7906b
+timeCreated: 1739993470
\ No newline at end of file
diff --git a/Assets/Tests/InputSystem.Editor/Unity.InputSystem.Tests.Editor.asmdef b/Assets/Tests/InputSystem.Editor/Unity.InputSystem.Tests.Editor.asmdef
index b0f395230f..c5ecbdbc5d 100644
--- a/Assets/Tests/InputSystem.Editor/Unity.InputSystem.Tests.Editor.asmdef
+++ b/Assets/Tests/InputSystem.Editor/Unity.InputSystem.Tests.Editor.asmdef
@@ -11,7 +11,7 @@
"Editor"
],
"excludePlatforms": [],
- "allowUnsafeCode": false,
+ "allowUnsafeCode": true,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md
index c3ac3ee766..ee8d62752e 100644
--- a/Packages/com.unity.inputsystem/CHANGELOG.md
+++ b/Packages/com.unity.inputsystem/CHANGELOG.md
@@ -38,6 +38,10 @@ however, it has to be formatted properly to pass verification tests.
### Added
- An alternative way to access if an action state reached a certain phase during this rendering frame (Update()). This can be utilized even if the InputSystem update mode is set to manual or FixedUpdate. It can be used to access the action phase during rendering, eg for perform updates to the UI.
+### Added
+- Added achievable average frequency diagnostic to Input Debugger device window (along with sensor frequency and global polling frequency information).
+- Added processing delay input system latency (average, minimum, maximum) diagnostics to Input Bugger device window.
+
## [1.13.0] - 2025-02-05
### Fixed
diff --git a/Packages/com.unity.inputsystem/Documentation~/Debugging.md b/Packages/com.unity.inputsystem/Documentation~/Debugging.md
index 28af1b73de..84040dcfcb 100644
--- a/Packages/com.unity.inputsystem/Documentation~/Debugging.md
+++ b/Packages/com.unity.inputsystem/Documentation~/Debugging.md
@@ -42,7 +42,11 @@ In the Input Debugger window, navigate to the __Devices__ list and double-click

-The top of the Device window displays general information about the specific Device, such as name, manufacturer, and serial number.
+The top of the Device window displays general information about the specific Device, such as name, manufacturer, associated layout, device flags, device ID and serial number. In addition, this section also display the current __sample frequency__ and __processing delay__ of the deivce.
+
+__Sample frequency__ indicates the frequency in Hertz (Hz) at which the Input System is currently processing samples or events. For devices receiving events this reflects the current flow of events received by the system. For devices receiving periodic readings this reflects the achievable sample rate of the system. The latter may be compared to the globally configured target sampling frequency, while achievable event frequency is uncorrelated to the sample polling frequency setting.
+
+__Processing delay__ indicates the average, minimum and maximum latency contribution from creating an input event or reading until the Input System has processed the same input event. Note that this excludes any additional input delay caused by OS, drivers or device communication. Also note that this excludes any additional output latency that may be caused by additional processing, rendering, GPU swap-chains or display refresh rate.
The __Controls__ section lists the Device's Controls and their individual states. This is useful when debugging input issues, because you can verify whether the data that the Input System receives from the Input Device is what you expect it to be. There are two buttons at the top of this panel:
diff --git a/Packages/com.unity.inputsystem/Documentation~/Images/DeviceInDebugger.png b/Packages/com.unity.inputsystem/Documentation~/Images/DeviceInDebugger.png
index 46ebb943c9..7e5ed00f81 100644
Binary files a/Packages/com.unity.inputsystem/Documentation~/Images/DeviceInDebugger.png and b/Packages/com.unity.inputsystem/Documentation~/Images/DeviceInDebugger.png differ
diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs
index 2658ec1922..a97ac39554 100644
--- a/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs
+++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs
@@ -1,7 +1,9 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
+using System.Text;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine.InputSystem.LowLevel;
@@ -90,6 +92,7 @@ internal void OnDestroy()
InputSystem.onSettingsChange -= NeedControlValueRefresh;
Application.focusChanged -= OnApplicationFocusChange;
EditorApplication.playModeStateChanged += OnPlayModeChange;
+ EditorApplication.update -= OnEditorUpdate;
}
m_EventTrace?.Dispose();
@@ -143,6 +146,19 @@ internal void OnGUI()
EditorGUILayout.LabelField("Flags", m_DeviceFlagsString);
if (m_Device is Keyboard)
EditorGUILayout.LabelField("Keyboard Layout", ((Keyboard)m_Device).keyboardLayout);
+ const string sampleFrequencyTooltip = "Displays the current event or sample frequency of this device in Hertz (Hz) averaged over measurement period of 1 second. " +
+ "The target frequency is device and backend dependent and may not be supported by all devices nor backends. " +
+ "The Polling Frequency indicates system polling target frequency.";
+ if (!string.IsNullOrEmpty(m_DeviceFrequencyString))
+ EditorGUILayout.LabelField(new GUIContent("Sample Frequency", sampleFrequencyTooltip), new GUIContent(m_DeviceFrequencyString), EditorStyles.label);
+ const string processingDelayTooltip =
+ "Displays the average, minimum and maximum observed input processing delay. This shows the time from " +
+ "when an input event is first created within Unity until its processed by the Input System. " +
+ "Note that this excludes additional input latency introduced by OS, driver or device communication. " +
+ "It also doesn't include output latency introduced by script processing, rendering, swap-chains, display refresh latency etc.";
+ if (!string.IsNullOrEmpty(m_DeviceLatencyString))
+ EditorGUILayout.LabelField(new GUIContent("Processing Delay", processingDelayTooltip),
+ new GUIContent(m_DeviceLatencyString), EditorStyles.label);
EditorGUILayout.EndVertical();
DrawControlTree();
@@ -290,6 +306,17 @@ private void InitializeWith(InputDevice device)
UpdateDeviceFlags();
+ // Query the sampling frequency of the device.
+ // We do this synchronously here for simplicity.
+ var queryFrequency = QuerySamplingFrequencyCommand.Create();
+ var result = device.ExecuteCommand(ref queryFrequency);
+ var targetFrequency = float.NaN;
+ if (result >= 0)
+ targetFrequency = queryFrequency.frequency;
+ var realtimeSinceStartup = Time.realtimeSinceStartupAsDouble;
+ m_SampleFrequencyCalculator = new SampleFrequencyCalculator(targetFrequency, realtimeSinceStartup);
+ m_InputLatencyCalculator = new InputLatencyCalculator(realtimeSinceStartup);
+
// Set up event trace. The default trace size of 512kb fits a ton of events and will
// likely bog down the UI if we try to display that many events. Instead, come up
// with a more reasonable sized based on the state size of the device.
@@ -326,6 +353,84 @@ private void InitializeWith(InputDevice device)
InputState.onChange += OnDeviceStateChange;
Application.focusChanged += OnApplicationFocusChange;
EditorApplication.playModeStateChanged += OnPlayModeChange;
+ EditorApplication.update += OnEditorUpdate;
+ }
+
+ private void OnEditorUpdate()
+ {
+ StringBuilder sb = null;
+ bool needControlValueRefresh = false;
+ var realtimeSinceStartup = Time.realtimeSinceStartupAsDouble;
+ if (m_SampleFrequencyCalculator.Update(realtimeSinceStartup))
+ {
+ m_DeviceFrequencyString = CreateDeviceFrequencyString(ref sb);
+ needControlValueRefresh = true;
+ }
+ if (m_InputLatencyCalculator.Update(realtimeSinceStartup))
+ {
+ m_DeviceLatencyString = CreateDeviceLatencyString(ref sb);
+ needControlValueRefresh = true;
+ }
+ if (needControlValueRefresh)
+ NeedControlValueRefresh();
+ }
+
+ private string CreateDeviceFrequencyString(ref StringBuilder sb)
+ {
+ if (sb == null)
+ sb = new StringBuilder();
+ else
+ sb.Clear();
+
+ // Display achievable frequency for device
+ const string frequencyFormat = "0.000 Hz";
+ sb.Append(m_SampleFrequencyCalculator.frequency.ToString(frequencyFormat, CultureInfo.InvariantCulture));
+
+ // Display target frequency reported for device
+ sb.Append(" (Target @ ");
+ sb.Append(float.IsNaN(m_SampleFrequencyCalculator.targetFrequency)
+ ? "n/a"
+ : m_SampleFrequencyCalculator.targetFrequency.ToString(frequencyFormat));
+
+ // Display system-wide polling frequency
+ sb.Append(", Polling-Frequency @ ");
+ sb.Append(InputSystem.pollingFrequency.ToString(frequencyFormat));
+ sb.Append(')');
+
+ return sb.ToString();
+ }
+
+ private static void FormatLatency(StringBuilder sb, float value)
+ {
+ const string latencyFormat = "0.000 ms";
+ if (float.IsNaN(value))
+ {
+ sb.Append("n/a");
+ return;
+ }
+
+ var millis = 1000.0f * value;
+ sb.Append(millis <= 1000.0f
+ ? (millis).ToString(latencyFormat, CultureInfo.InvariantCulture)
+ : ">1000.0 ms");
+ }
+
+ private string CreateDeviceLatencyString(ref StringBuilder sb)
+ {
+ if (sb == null)
+ sb = new StringBuilder();
+ else
+ sb.Clear();
+
+ // Display latency in seconds for device
+ sb.Append("Average: ");
+ FormatLatency(sb, m_InputLatencyCalculator.averageLatencySeconds);
+ sb.Append(", Min: ");
+ FormatLatency(sb, m_InputLatencyCalculator.minLatencySeconds);
+ sb.Append(", Max: ");
+ FormatLatency(sb, m_InputLatencyCalculator.maxLatencySeconds);
+
+ return sb.ToString();
}
private void UpdateDeviceFlags()
@@ -396,6 +501,8 @@ internal static InputUpdateType DetermineUpdateTypeToShow(InputDevice device)
private string m_DeviceIdString;
private string m_DeviceUsagesString;
private string m_DeviceFlagsString;
+ private string m_DeviceFrequencyString;
+ private string m_DeviceLatencyString;
private InputDevice.DeviceFlags m_DeviceFlags;
private InputControlTreeView m_ControlTree;
private InputEventTreeView m_EventTree;
@@ -404,6 +511,8 @@ internal static InputUpdateType DetermineUpdateTypeToShow(InputDevice device)
private InputEventTrace.ReplayController m_ReplayController;
private InputEventTrace m_EventTrace;
private InputUpdateType m_InputUpdateTypeShownInControlTree;
+ private InputLatencyCalculator m_InputLatencyCalculator;
+ private SampleFrequencyCalculator m_SampleFrequencyCalculator;
[SerializeField] private int m_DeviceId = InputDevice.InvalidDeviceId;
[SerializeField] private TreeViewState m_ControlTreeState;
@@ -464,7 +573,12 @@ private void OnDeviceChange(InputDevice device, InputDeviceChange change)
private void OnDeviceStateChange(InputDevice device, InputEventPtr eventPtr)
{
if (device == m_Device)
+ {
+ m_InputLatencyCalculator.ProcessSample(eventPtr);
+ m_SampleFrequencyCalculator.ProcessSample(eventPtr);
+
NeedControlValueRefresh();
+ }
}
private static class Styles
diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputLatencyCalculator.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputLatencyCalculator.cs
new file mode 100644
index 0000000000..dabf9f2ec7
--- /dev/null
+++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputLatencyCalculator.cs
@@ -0,0 +1,83 @@
+#if UNITY_EDITOR
+using UnityEngine.InputSystem.LowLevel;
+
+namespace UnityEngine.InputSystem.Editor
+{
+ ///
+ /// A utility for computing latency based on input events.
+ ///
+ internal struct InputLatencyCalculator
+ {
+ private double m_LastUpdateTime;
+ private double m_AccumulatedLatencySeconds;
+ private double m_AccumulatedMinLatencySeconds;
+ private double m_AccumulatedMaxLatencySeconds;
+ private int m_SampleCount;
+
+ public InputLatencyCalculator(double realtimeSinceStartup)
+ {
+ m_LastUpdateTime = realtimeSinceStartup;
+ m_AccumulatedLatencySeconds = 0.0;
+ m_AccumulatedMinLatencySeconds = 0.0;
+ m_AccumulatedMaxLatencySeconds = 0.0;
+ m_SampleCount = 0;
+ averageLatencySeconds = float.NaN;
+ minLatencySeconds = float.NaN;
+ maxLatencySeconds = float.NaN;
+ }
+
+ public void ProcessSample(InputEventPtr eventPtr) =>
+ ProcessSample(eventPtr, Time.realtimeSinceStartupAsDouble);
+
+ public void ProcessSample(InputEventPtr eventPtr, double realtimeSinceStartup)
+ {
+ if (!eventPtr.valid)
+ return;
+
+ var ageInSeconds = realtimeSinceStartup - eventPtr.time;
+ m_AccumulatedLatencySeconds += ageInSeconds;
+ if (++m_SampleCount == 1)
+ {
+ m_AccumulatedMinLatencySeconds = ageInSeconds;
+ m_AccumulatedMaxLatencySeconds = ageInSeconds;
+ }
+ else if (ageInSeconds < m_AccumulatedMaxLatencySeconds)
+ m_AccumulatedMinLatencySeconds = ageInSeconds;
+ else if (ageInSeconds > m_AccumulatedMaxLatencySeconds)
+ m_AccumulatedMaxLatencySeconds = ageInSeconds;
+ }
+
+ public float averageLatencySeconds { get; private set; }
+ public float minLatencySeconds { get; private set; }
+ public float maxLatencySeconds { get; private set; }
+
+ public bool Update() => Update(Time.realtimeSinceStartupAsDouble);
+
+ public bool Update(double realtimeSinceStartup)
+ {
+ var timeSinceLastUpdate = realtimeSinceStartup - m_LastUpdateTime;
+ if (timeSinceLastUpdate < 1.0)
+ return false; // Only update once per second (and avoid division by zero)
+
+ if (m_SampleCount == 0)
+ {
+ averageLatencySeconds = float.NaN;
+ minLatencySeconds = float.NaN;
+ maxLatencySeconds = float.NaN;
+ }
+ else
+ {
+ averageLatencySeconds = (float)(m_AccumulatedLatencySeconds / m_SampleCount);
+ minLatencySeconds = (float)m_AccumulatedMinLatencySeconds;
+ maxLatencySeconds = (float)m_AccumulatedMaxLatencySeconds;
+ }
+
+ m_LastUpdateTime = realtimeSinceStartup;
+ m_SampleCount = 0;
+ m_AccumulatedLatencySeconds = 0.0;
+
+ return true;
+ }
+ }
+}
+#endif // UNITY_EDITOR
diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputLatencyCalculator.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputLatencyCalculator.cs.meta
new file mode 100644
index 0000000000..42f43645e6
--- /dev/null
+++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputLatencyCalculator.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 58dca4037ed747dba3154af0d815053a
+timeCreated: 1739993205
\ No newline at end of file
diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/SampleFrequencyCalculator.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/SampleFrequencyCalculator.cs
new file mode 100644
index 0000000000..14c499f3c9
--- /dev/null
+++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/SampleFrequencyCalculator.cs
@@ -0,0 +1,48 @@
+using UnityEngine.InputSystem.LowLevel;
+
+namespace UnityEngine.InputSystem.Editor
+{
+ ///
+ /// A utility for computing sample frequency based on input events.
+ ///
+ internal struct SampleFrequencyCalculator
+ {
+ private double m_LastUpdateTime;
+ private int m_SampleCount;
+
+ public SampleFrequencyCalculator(float targetFrequency, double realtimeSinceStartup)
+ {
+ this.targetFrequency = targetFrequency;
+ this.m_SampleCount = 0;
+ this.frequency = 0.0f;
+ this.m_LastUpdateTime = realtimeSinceStartup;
+ }
+
+ public float targetFrequency { get; private set; }
+ public float frequency { get; private set; }
+
+ public void ProcessSample(InputEventPtr eventPtr)
+ {
+ // Only count actual samples instead of device-state changes which may be reported anyway it seems.
+ // For determining frequency we at least absolute do not want to count state changes not driven
+ // by an associated event/sample.
+ if (eventPtr != null)
+ ++m_SampleCount;
+ }
+
+ public bool Update() => Update(Time.realtimeSinceStartupAsDouble);
+
+ public bool Update(double realtimeSinceStartup)
+ {
+ var timeSinceLastUpdate = realtimeSinceStartup - m_LastUpdateTime;
+ if (timeSinceLastUpdate < 1.0)
+ return false; // Only update once per second (and avoid division by zero)
+
+ m_LastUpdateTime = realtimeSinceStartup;
+ frequency = (float)(m_SampleCount / timeSinceLastUpdate);
+ m_SampleCount = 0;
+
+ return true;
+ }
+ }
+}
diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/SampleFrequencyCalculator.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/SampleFrequencyCalculator.cs.meta
new file mode 100644
index 0000000000..718fd68130
--- /dev/null
+++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/SampleFrequencyCalculator.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: fdd7c783de844fefa8df8acfd36b9dc9
+timeCreated: 1739993278
\ No newline at end of file