Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 @@ -22,6 +22,10 @@ however, it has to be formatted properly to pass verification tests.
### Changed
- Changed default input action asset name from New Controls to New Actions.

### 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
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 @@ -35,7 +37,7 @@ namespace UnityEngine.InputSystem.Editor
{
// Shows status and activity of a single input device in a separate window.
// Can also be used to alter the state of a device by making up state events.
internal sealed class InputDeviceDebuggerWindow : EditorWindow, ISerializationCallbackReceiver, IDisposable
internal sealed partial class InputDeviceDebuggerWindow : EditorWindow, ISerializationCallbackReceiver, IDisposable
{
// ATM the debugger window is super slow and repaints are very expensive. So keep the total
// number of events we can fit at a relatively low size until we have fixed that problem.
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,18 @@ 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 inputSystemLatencyTooltip =
"Displays the average/minimum/maximum observed input processing latency " +
"(Excluding OS, driver, firmware or transport latency) for data reported for this device. " +
"Note that additional experienced latency (a.k.a. input lag) will be introduced if any deferred " +
"processing and output delay (rendering, display refresh latency etc).";
if (!string.IsNullOrEmpty(m_DeviceLatencyString))
EditorGUILayout.LabelField(new GUIContent("Input Latency", inputSystemLatencyTooltip), new GUIContent(m_DeviceLatencyString), EditorStyles.label);
EditorGUILayout.EndVertical();

DrawControlTree();
Expand Down Expand Up @@ -290,6 +305,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 +352,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 +500,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 +510,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 +572,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