-
Notifications
You must be signed in to change notification settings - Fork 329
NEW: Added achievable frequency and latency (processing delay) to Input Debugger (ISX-2254) #2142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7cb1b4c
0a77be5
cdf7ad1
0a9a9ad
d479d44
45573a3
37b18db
40948db
8af0ad1
3cfa59d
8175826
30c285a
2858da1
ced5e1a
24378e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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.
| 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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| 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: | ||||||
|
|
||||||
|
|
||||||
| 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; | ||||||
|
|
@@ -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. " + | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| "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 | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.