Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions Assets/Tests/InputSystem.Editor/InputLatencyCalculatorTests.cs
Original file line number Diff line number Diff line change
@@ -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

/// <summary>
/// Adds a number of samples to the calculator under test.
/// </summary>
/// <param name="currentRealtimeSinceStartup">The current timestamp (reflecting wall-clock time).</param>
/// <param name="samplesRealtimeSinceStartup">List of timestamps to be propagated as events to the calculator.</param>
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();
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 87 additions & 0 deletions Assets/Tests/InputSystem.Editor/SampleFrequencyCalculatorTests.cs
Original file line number Diff line number Diff line change
@@ -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

/// <summary>
/// Adds a number of samples to the calculator under test.
/// </summary>
/// <param name="samplesRealtimeSinceStartup">List of timestamps to be propagated as events to the calculator.</param>
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));
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"allowUnsafeCode": true,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
Expand Down
4 changes: 4 additions & 0 deletions Packages/com.unity.inputsystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion Packages/com.unity.inputsystem/Documentation~/Debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ In the Input Debugger window, navigate to the __Devices__ list and double-click

![Device in Input Debugger](Images/DeviceInDebugger.png)

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
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. This section also displays the current __sample frequency__ and __processing delay__ of the device.


__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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
__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.
__Processing delay__ indicates the average, minimum, and maximum time delay from when an input event or reading is created until the Input System has processed it. 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:

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -90,6 +92,7 @@ internal void OnDestroy()
InputSystem.onSettingsChange -= NeedControlValueRefresh;
Application.focusChanged -= OnApplicationFocusChange;
EditorApplication.playModeStateChanged += OnPlayModeChange;
EditorApplication.update -= OnEditorUpdate;
}

m_EventTrace?.Dispose();
Expand Down Expand Up @@ -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. " +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"when an input event is first created within Unity until its processed by the Input System. " +
"when an input event is first created within Unity until it's 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();
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading