Skip to content

Commit 7cb1b4c

Browse files
committed
ADD: Added frequency and latency (processing delay) to Input debugger.
1 parent 2163cc1 commit 7cb1b4c

File tree

1 file changed

+226
-2
lines changed

1 file changed

+226
-2
lines changed

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

Lines changed: 226 additions & 2 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,14 @@ 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 average event or sample frequency of this device in Hertz (Hz). " +
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 inputSystemLatencyTooltip = "Displays the average input system latency (Excluding OS, driver, firmware or transport latency) for data reported for this device.";
155+
if (!string.IsNullOrEmpty(m_DeviceLatencyString))
156+
EditorGUILayout.LabelField(new GUIContent("Input Latency", inputSystemLatencyTooltip), new GUIContent(m_DeviceLatencyString), EditorStyles.label);
146157
EditorGUILayout.EndVertical();
147158

148159
DrawControlTree();
@@ -287,9 +298,20 @@ private void InitializeWith(InputDevice device)
287298
m_DeviceId = device.deviceId;
288299
m_DeviceIdString = device.deviceId.ToString();
289300
m_DeviceUsagesString = string.Join(", ", device.usages.Select(x => x.ToString()).ToArray());
290-
301+
291302
UpdateDeviceFlags();
292303

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

331431
private void UpdateDeviceFlags()
@@ -396,6 +496,8 @@ internal static InputUpdateType DetermineUpdateTypeToShow(InputDevice device)
396496
private string m_DeviceIdString;
397497
private string m_DeviceUsagesString;
398498
private string m_DeviceFlagsString;
499+
private string m_DeviceFrequencyString;
500+
private string m_DeviceLatencyString;
399501
private InputDevice.DeviceFlags m_DeviceFlags;
400502
private InputControlTreeView m_ControlTree;
401503
private InputEventTreeView m_EventTree;
@@ -404,6 +506,8 @@ internal static InputUpdateType DetermineUpdateTypeToShow(InputDevice device)
404506
private InputEventTrace.ReplayController m_ReplayController;
405507
private InputEventTrace m_EventTrace;
406508
private InputUpdateType m_InputUpdateTypeShownInControlTree;
509+
private LatencyCalculator m_LatencyCalculator;
510+
private FrequencyCalculator m_FrequencyCalculator;
407511

408512
[SerializeField] private int m_DeviceId = InputDevice.InvalidDeviceId;
409513
[SerializeField] private TreeViewState m_ControlTreeState;
@@ -461,10 +565,130 @@ private void OnDeviceChange(InputDevice device, InputDeviceChange change)
461565
}
462566
}
463567

568+
private struct LatencyCalculator
569+
{
570+
private double m_LastUpdateTime;
571+
private double m_AccumulatedLatencySeconds;
572+
private double m_AccumulatedMinLatencySeconds;
573+
private double m_AccumulatedMaxLatencySeconds;
574+
private int m_SampleCount;
575+
576+
public LatencyCalculator(double realtimeSinceStartup)
577+
{
578+
m_LastUpdateTime = realtimeSinceStartup;
579+
m_AccumulatedLatencySeconds = 0.0;
580+
m_AccumulatedMinLatencySeconds = 0.0;
581+
m_AccumulatedMaxLatencySeconds = 0.0;
582+
m_SampleCount = 0;
583+
averageLatencySeconds = float.NaN;
584+
minLatencySeconds = float.NaN;
585+
maxLatencySeconds = float.NaN;
586+
}
587+
588+
public void ProcessSample(InputEventPtr eventPtr) => ProcessSample(eventPtr, Time.realtimeSinceStartupAsDouble);
589+
590+
public void ProcessSample(InputEventPtr eventPtr, double realtimeSinceStartup)
591+
{
592+
if (!eventPtr.valid)
593+
return;
594+
595+
var ageInSeconds = realtimeSinceStartup - eventPtr.time;
596+
m_AccumulatedLatencySeconds += ageInSeconds;
597+
if (++m_SampleCount == 1)
598+
{
599+
m_AccumulatedMinLatencySeconds = ageInSeconds;
600+
m_AccumulatedMaxLatencySeconds = ageInSeconds;
601+
}
602+
else if (ageInSeconds < m_AccumulatedMaxLatencySeconds)
603+
m_AccumulatedMinLatencySeconds = ageInSeconds;
604+
else if (ageInSeconds > m_AccumulatedMaxLatencySeconds)
605+
m_AccumulatedMaxLatencySeconds = ageInSeconds;
606+
}
607+
608+
public float averageLatencySeconds { get; private set; }
609+
public float minLatencySeconds { get; private set; }
610+
public float maxLatencySeconds { get; private set; }
611+
612+
public bool Update() => Update(Time.realtimeSinceStartupAsDouble);
613+
614+
public bool Update(double realtimeSinceStartup)
615+
{
616+
var timeSinceLastUpdate = realtimeSinceStartup - m_LastUpdateTime;
617+
if (timeSinceLastUpdate < 1.0)
618+
return false; // Only update once per second (and avoid division by zero)
619+
620+
if (m_SampleCount == 0)
621+
{
622+
averageLatencySeconds = float.NaN;
623+
minLatencySeconds = float.NaN;
624+
maxLatencySeconds = float.NaN;
625+
}
626+
else
627+
{
628+
averageLatencySeconds = (float)(m_AccumulatedLatencySeconds / m_SampleCount);
629+
minLatencySeconds = (float)m_AccumulatedMinLatencySeconds;
630+
maxLatencySeconds = (float)m_AccumulatedMaxLatencySeconds;
631+
}
632+
633+
m_LastUpdateTime = realtimeSinceStartup;
634+
m_SampleCount = 0;
635+
636+
m_AccumulatedLatencySeconds = 0.0;
637+
638+
return true;
639+
}
640+
}
641+
642+
private struct FrequencyCalculator
643+
{
644+
private double m_LastUpdateTime;
645+
private int m_SampleCount;
646+
647+
public FrequencyCalculator(float targetFrequency, double realtimeSinceStartup)
648+
{
649+
this.targetFrequency = targetFrequency;
650+
this.m_SampleCount = 0;
651+
this.frequency = 0.0f;
652+
this.m_LastUpdateTime = realtimeSinceStartup;
653+
}
654+
655+
public float targetFrequency { get; private set; }
656+
public float frequency { get; private set; }
657+
658+
public void ProcessSample(InputEventPtr eventPtr)
659+
{
660+
// Only count actual samples instead of device-state changes which may be reported anyway it seems.
661+
// For determining frequency we at least absolute do not want to count state changes not driven
662+
// by an associated event/sample.
663+
if (eventPtr != null)
664+
++m_SampleCount;
665+
}
666+
667+
public bool Update() => Update(Time.realtimeSinceStartupAsDouble);
668+
669+
public bool Update(double realtimeSinceStartup)
670+
{
671+
var timeSinceLastUpdate = realtimeSinceStartup - m_LastUpdateTime;
672+
if (timeSinceLastUpdate < 1.0)
673+
return false; // Only update once per second (and avoid division by zero)
674+
675+
m_LastUpdateTime = realtimeSinceStartup;
676+
frequency = (float)(m_SampleCount / timeSinceLastUpdate);
677+
m_SampleCount = 0;
678+
679+
return true;
680+
}
681+
}
682+
464683
private void OnDeviceStateChange(InputDevice device, InputEventPtr eventPtr)
465684
{
466685
if (device == m_Device)
467-
NeedControlValueRefresh();
686+
{
687+
m_LatencyCalculator.ProcessSample(eventPtr);
688+
m_FrequencyCalculator.ProcessSample(eventPtr);
689+
690+
NeedControlValueRefresh();
691+
}
468692
}
469693

470694
private static class Styles

0 commit comments

Comments
 (0)