Skip to content

Commit ca0d36f

Browse files
ekcohchrstphrsxtn
andauthored
NEW: Added achievable frequency and latency (processing delay) to Input Debugger (ISX-2254) (#2142)
Co-authored-by: chrstphrsxtn <74899224+chrstphrsxtn@users.noreply.github.com>
1 parent 4c91637 commit ca0d36f

13 files changed

+445
-2
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using NUnit.Framework;
2+
using Unity.Collections;
3+
using UnityEngine.InputSystem.Editor;
4+
using UnityEngine.InputSystem.LowLevel;
5+
6+
namespace Tests.InputSystem.Editor
7+
{
8+
public class InputLatencyCalculatorTests
9+
{
10+
private InputLatencyCalculator m_Sut; // Software Under Test
11+
12+
/// <summary>
13+
/// Adds a number of samples to the calculator under test.
14+
/// </summary>
15+
/// <param name="currentRealtimeSinceStartup">The current timestamp (reflecting wall-clock time).</param>
16+
/// <param name="samplesRealtimeSinceStartup">List of timestamps to be propagated as events to the calculator.</param>
17+
private void GivenSamples(double currentRealtimeSinceStartup, params double[] samplesRealtimeSinceStartup)
18+
{
19+
var n = samplesRealtimeSinceStartup.Length;
20+
using (InputEventBuffer buffer = new InputEventBuffer())
21+
{
22+
unsafe
23+
{
24+
for (var i = 0; i < n; ++i)
25+
{
26+
var ptr = buffer.AllocateEvent(InputEvent.kBaseEventSize, 2048, Allocator.Temp);
27+
ptr->time = samplesRealtimeSinceStartup[i];
28+
m_Sut.ProcessSample(ptr, currentRealtimeSinceStartup);
29+
}
30+
}
31+
}
32+
}
33+
34+
private void AssertNoMetrics()
35+
{
36+
Assert.True(float.IsNaN(m_Sut.averageLatencySeconds));
37+
Assert.True(float.IsNaN(m_Sut.minLatencySeconds));
38+
Assert.True(float.IsNaN(m_Sut.maxLatencySeconds));
39+
}
40+
41+
[Test]
42+
public void MetricsShouldBeUndefined_WhenCalculatorHasNoSamples()
43+
{
44+
m_Sut = new InputLatencyCalculator(0.0);
45+
AssertNoMetrics();
46+
m_Sut.Update(10.0);
47+
AssertNoMetrics();
48+
}
49+
50+
[Test]
51+
public void MetricsShouldBeDefined_WhenCalculatorHasSamplesButPeriodHasNotElapsed()
52+
{
53+
m_Sut = new InputLatencyCalculator(1.0);
54+
GivenSamples(1.9, 1.1, 1.2, 1.9);
55+
m_Sut.Update(1.99);
56+
AssertNoMetrics();
57+
}
58+
59+
[Test]
60+
public void MetricsShouldOnlyUpdate_WhenPeriodHasElapsed()
61+
{
62+
m_Sut = new InputLatencyCalculator(0.0);
63+
GivenSamples(0.5, 0.3, 0.4, 0.5);
64+
m_Sut.Update(1.0);
65+
Assert.That(m_Sut.averageLatencySeconds, Is.EqualTo(0.1f));
66+
Assert.That(m_Sut.minLatencySeconds, Is.EqualTo(0.0f));
67+
Assert.That(m_Sut.maxLatencySeconds, Is.EqualTo(0.2f));
68+
}
69+
70+
[Test]
71+
public void MetricShouldNotBeAffected_WhenMoreThanAPeriodHasElapsed()
72+
{
73+
m_Sut = new InputLatencyCalculator(0.0);
74+
GivenSamples(0.5, 0.3, 0.4, 0.5);
75+
m_Sut.Update(2.0);
76+
Assert.That(m_Sut.averageLatencySeconds, Is.EqualTo(0.1f));
77+
Assert.That(m_Sut.minLatencySeconds, Is.EqualTo(0.0f));
78+
Assert.That(m_Sut.maxLatencySeconds, Is.EqualTo(0.2f));
79+
}
80+
81+
[Test]
82+
public void MetricsShouldReset_WhenNotReceivingASampleForAPeriod()
83+
{
84+
m_Sut = new InputLatencyCalculator(0.0);
85+
GivenSamples(0.5, 0.3, 0.4, 0.5);
86+
m_Sut.Update(1.0);
87+
m_Sut.Update(2.0);
88+
AssertNoMetrics();
89+
}
90+
}
91+
}

Assets/Tests/InputSystem.Editor/InputLatencyCalculatorTests.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using NUnit.Framework;
2+
using Unity.Collections;
3+
using UnityEngine.InputSystem.Editor;
4+
using UnityEngine.InputSystem.LowLevel;
5+
6+
namespace Tests.InputSystem.Editor
7+
{
8+
public class SampleFrequencyCalculatorTests
9+
{
10+
private SampleFrequencyCalculator m_Sut; // Software Under Test
11+
12+
/// <summary>
13+
/// Adds a number of samples to the calculator under test.
14+
/// </summary>
15+
/// <param name="samplesRealtimeSinceStartup">List of timestamps to be propagated as events to the calculator.</param>
16+
private void GivenSamples(params double[] samplesRealtimeSinceStartup)
17+
{
18+
var n = samplesRealtimeSinceStartup.Length;
19+
using (InputEventBuffer buffer = new InputEventBuffer())
20+
{
21+
unsafe
22+
{
23+
for (var i = 0; i < n; ++i)
24+
{
25+
var ptr = buffer.AllocateEvent(InputEvent.kBaseEventSize, 2048, Allocator.Temp);
26+
ptr->time = samplesRealtimeSinceStartup[i];
27+
m_Sut.ProcessSample(ptr);
28+
}
29+
}
30+
}
31+
}
32+
33+
[Test]
34+
public void FrequencyShouldBeZero_WhenCalculatorHasNoSamples()
35+
{
36+
m_Sut = new SampleFrequencyCalculator(10, 0.0);
37+
Assert.That(m_Sut.frequency, Is.EqualTo(0.0f));
38+
}
39+
40+
[Test]
41+
public void FrequencyShouldBeZero_WhenCalculatorHasSamplesButPeriodHasNotElapsed()
42+
{
43+
m_Sut = new SampleFrequencyCalculator(10.0f, 1.0);
44+
GivenSamples(1.1, 1.2, 1.9);
45+
m_Sut.Update(1.99);
46+
Assert.That(m_Sut.frequency, Is.EqualTo(0.0f));
47+
}
48+
49+
[Test]
50+
public void MetricsShouldOnlyUpdate_WhenPeriodHasElapsed()
51+
{
52+
m_Sut = new SampleFrequencyCalculator(10.0f, 0.0);
53+
GivenSamples(0.3, 0.4, 0.5);
54+
m_Sut.Update(1.0);
55+
Assert.That(m_Sut.frequency, Is.EqualTo(3.0f));
56+
}
57+
58+
[Test]
59+
public void MetricsShouldBeBasedOnActualElapsedPeriod_WhenMoreThanAPeriodHasElapsed()
60+
{
61+
m_Sut = new SampleFrequencyCalculator(10.0f, 0.0);
62+
GivenSamples(0.5, 0.3, 0.4, 0.5);
63+
m_Sut.Update(2.0);
64+
Assert.That(m_Sut.frequency, Is.EqualTo(2.0f));
65+
}
66+
67+
[Test]
68+
public void MetricsShouldNotBeUpdated_WhenLessThanAPeriodHasElapsed()
69+
{
70+
m_Sut = new SampleFrequencyCalculator(10.0f, 0.0);
71+
GivenSamples(0.5, 0.3, 0.4, 0.5);
72+
m_Sut.Update(2.0);
73+
m_Sut.Update(2.9);
74+
Assert.That(m_Sut.frequency, Is.EqualTo(2.0f));
75+
}
76+
77+
[Test]
78+
public void MetricsShouldReset_WhenNotReceivingASampleForAPeriod()
79+
{
80+
m_Sut = new SampleFrequencyCalculator(1.0f, 0.0);
81+
GivenSamples(0.5, 0.3, 0.4, 0.5);
82+
m_Sut.Update(1.0);
83+
m_Sut.Update(2.0);
84+
Assert.That(m_Sut.frequency, Is.EqualTo(0.0f));
85+
}
86+
}
87+
}

Assets/Tests/InputSystem.Editor/SampleFrequencyCalculatorTests.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/Tests/InputSystem.Editor/Unity.InputSystem.Tests.Editor.asmdef

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"Editor"
1212
],
1313
"excludePlatforms": [],
14-
"allowUnsafeCode": false,
14+
"allowUnsafeCode": true,
1515
"overrideReferences": true,
1616
"precompiledReferences": [
1717
"nunit.framework.dll"

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ however, it has to be formatted properly to pass verification tests.
3838
### Added
3939
- 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.
4040

41+
### Added
42+
- Added achievable average frequency diagnostic to Input Debugger device window (along with sensor frequency and global polling frequency information).
43+
- Added processing delay input system latency (average, minimum, maximum) diagnostics to Input Bugger device window.
44+
4145
## [1.13.0] - 2025-02-05
4246

4347
### Fixed

Packages/com.unity.inputsystem/Documentation~/Debugging.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ In the Input Debugger window, navigate to the __Devices__ list and double-click
4242

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

45-
The top of the Device window displays general information about the specific Device, such as name, manufacturer, and serial number.
45+
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.
46+
47+
__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.
48+
49+
__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.
4650

4751
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:
4852

-142 KB
Loading

Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#if UNITY_EDITOR
22
using System;
33
using System.Collections.Generic;
4+
using System.Globalization;
45
using System.Linq;
6+
using System.Text;
57
using UnityEditor;
68
using UnityEditor.IMGUI.Controls;
79
using UnityEngine.InputSystem.LowLevel;
@@ -90,6 +92,7 @@ internal void OnDestroy()
9092
InputSystem.onSettingsChange -= NeedControlValueRefresh;
9193
Application.focusChanged -= OnApplicationFocusChange;
9294
EditorApplication.playModeStateChanged += OnPlayModeChange;
95+
EditorApplication.update -= OnEditorUpdate;
9396
}
9497

9598
m_EventTrace?.Dispose();
@@ -143,6 +146,19 @@ internal void OnGUI()
143146
EditorGUILayout.LabelField("Flags", m_DeviceFlagsString);
144147
if (m_Device is Keyboard)
145148
EditorGUILayout.LabelField("Keyboard Layout", ((Keyboard)m_Device).keyboardLayout);
149+
const string sampleFrequencyTooltip = "Displays the current event or sample frequency of this device in Hertz (Hz) averaged over measurement period of 1 second. " +
150+
"The target frequency is device and backend dependent and may not be supported by all devices nor backends. " +
151+
"The Polling Frequency indicates system polling target frequency.";
152+
if (!string.IsNullOrEmpty(m_DeviceFrequencyString))
153+
EditorGUILayout.LabelField(new GUIContent("Sample Frequency", sampleFrequencyTooltip), new GUIContent(m_DeviceFrequencyString), EditorStyles.label);
154+
const string processingDelayTooltip =
155+
"Displays the average, minimum and maximum observed input processing delay. This shows the time from " +
156+
"when an input event is first created within Unity until its processed by the Input System. " +
157+
"Note that this excludes additional input latency introduced by OS, driver or device communication. " +
158+
"It also doesn't include output latency introduced by script processing, rendering, swap-chains, display refresh latency etc.";
159+
if (!string.IsNullOrEmpty(m_DeviceLatencyString))
160+
EditorGUILayout.LabelField(new GUIContent("Processing Delay", processingDelayTooltip),
161+
new GUIContent(m_DeviceLatencyString), EditorStyles.label);
146162
EditorGUILayout.EndVertical();
147163

148164
DrawControlTree();
@@ -290,6 +306,17 @@ private void InitializeWith(InputDevice device)
290306

291307
UpdateDeviceFlags();
292308

309+
// Query the sampling frequency of the device.
310+
// We do this synchronously here for simplicity.
311+
var queryFrequency = QuerySamplingFrequencyCommand.Create();
312+
var result = device.ExecuteCommand(ref queryFrequency);
313+
var targetFrequency = float.NaN;
314+
if (result >= 0)
315+
targetFrequency = queryFrequency.frequency;
316+
var realtimeSinceStartup = Time.realtimeSinceStartupAsDouble;
317+
m_SampleFrequencyCalculator = new SampleFrequencyCalculator(targetFrequency, realtimeSinceStartup);
318+
m_InputLatencyCalculator = new InputLatencyCalculator(realtimeSinceStartup);
319+
293320
// Set up event trace. The default trace size of 512kb fits a ton of events and will
294321
// likely bog down the UI if we try to display that many events. Instead, come up
295322
// with a more reasonable sized based on the state size of the device.
@@ -326,6 +353,84 @@ private void InitializeWith(InputDevice device)
326353
InputState.onChange += OnDeviceStateChange;
327354
Application.focusChanged += OnApplicationFocusChange;
328355
EditorApplication.playModeStateChanged += OnPlayModeChange;
356+
EditorApplication.update += OnEditorUpdate;
357+
}
358+
359+
private void OnEditorUpdate()
360+
{
361+
StringBuilder sb = null;
362+
bool needControlValueRefresh = false;
363+
var realtimeSinceStartup = Time.realtimeSinceStartupAsDouble;
364+
if (m_SampleFrequencyCalculator.Update(realtimeSinceStartup))
365+
{
366+
m_DeviceFrequencyString = CreateDeviceFrequencyString(ref sb);
367+
needControlValueRefresh = true;
368+
}
369+
if (m_InputLatencyCalculator.Update(realtimeSinceStartup))
370+
{
371+
m_DeviceLatencyString = CreateDeviceLatencyString(ref sb);
372+
needControlValueRefresh = true;
373+
}
374+
if (needControlValueRefresh)
375+
NeedControlValueRefresh();
376+
}
377+
378+
private string CreateDeviceFrequencyString(ref StringBuilder sb)
379+
{
380+
if (sb == null)
381+
sb = new StringBuilder();
382+
else
383+
sb.Clear();
384+
385+
// Display achievable frequency for device
386+
const string frequencyFormat = "0.000 Hz";
387+
sb.Append(m_SampleFrequencyCalculator.frequency.ToString(frequencyFormat, CultureInfo.InvariantCulture));
388+
389+
// Display target frequency reported for device
390+
sb.Append(" (Target @ ");
391+
sb.Append(float.IsNaN(m_SampleFrequencyCalculator.targetFrequency)
392+
? "n/a"
393+
: m_SampleFrequencyCalculator.targetFrequency.ToString(frequencyFormat));
394+
395+
// Display system-wide polling frequency
396+
sb.Append(", Polling-Frequency @ ");
397+
sb.Append(InputSystem.pollingFrequency.ToString(frequencyFormat));
398+
sb.Append(')');
399+
400+
return sb.ToString();
401+
}
402+
403+
private static void FormatLatency(StringBuilder sb, float value)
404+
{
405+
const string latencyFormat = "0.000 ms";
406+
if (float.IsNaN(value))
407+
{
408+
sb.Append("n/a");
409+
return;
410+
}
411+
412+
var millis = 1000.0f * value;
413+
sb.Append(millis <= 1000.0f
414+
? (millis).ToString(latencyFormat, CultureInfo.InvariantCulture)
415+
: ">1000.0 ms");
416+
}
417+
418+
private string CreateDeviceLatencyString(ref StringBuilder sb)
419+
{
420+
if (sb == null)
421+
sb = new StringBuilder();
422+
else
423+
sb.Clear();
424+
425+
// Display latency in seconds for device
426+
sb.Append("Average: ");
427+
FormatLatency(sb, m_InputLatencyCalculator.averageLatencySeconds);
428+
sb.Append(", Min: ");
429+
FormatLatency(sb, m_InputLatencyCalculator.minLatencySeconds);
430+
sb.Append(", Max: ");
431+
FormatLatency(sb, m_InputLatencyCalculator.maxLatencySeconds);
432+
433+
return sb.ToString();
329434
}
330435

331436
private void UpdateDeviceFlags()
@@ -396,6 +501,8 @@ internal static InputUpdateType DetermineUpdateTypeToShow(InputDevice device)
396501
private string m_DeviceIdString;
397502
private string m_DeviceUsagesString;
398503
private string m_DeviceFlagsString;
504+
private string m_DeviceFrequencyString;
505+
private string m_DeviceLatencyString;
399506
private InputDevice.DeviceFlags m_DeviceFlags;
400507
private InputControlTreeView m_ControlTree;
401508
private InputEventTreeView m_EventTree;
@@ -404,6 +511,8 @@ internal static InputUpdateType DetermineUpdateTypeToShow(InputDevice device)
404511
private InputEventTrace.ReplayController m_ReplayController;
405512
private InputEventTrace m_EventTrace;
406513
private InputUpdateType m_InputUpdateTypeShownInControlTree;
514+
private InputLatencyCalculator m_InputLatencyCalculator;
515+
private SampleFrequencyCalculator m_SampleFrequencyCalculator;
407516

408517
[SerializeField] private int m_DeviceId = InputDevice.InvalidDeviceId;
409518
[SerializeField] private TreeViewState m_ControlTreeState;
@@ -464,7 +573,12 @@ private void OnDeviceChange(InputDevice device, InputDeviceChange change)
464573
private void OnDeviceStateChange(InputDevice device, InputEventPtr eventPtr)
465574
{
466575
if (device == m_Device)
576+
{
577+
m_InputLatencyCalculator.ProcessSample(eventPtr);
578+
m_SampleFrequencyCalculator.ProcessSample(eventPtr);
579+
467580
NeedControlValueRefresh();
581+
}
468582
}
469583

470584
private static class Styles

0 commit comments

Comments
 (0)