diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputSystemProfilerModule.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputSystemProfilerModule.cs new file mode 100644 index 0000000000..ee1a39fee0 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputSystemProfilerModule.cs @@ -0,0 +1,148 @@ +#if UNITY_EDITOR // Input System currently do not have proper asmdef for editor code. + +using Unity.Profiling.Editor; +using UnityEditor; +using UnityEditorInternal; +using UnityEngine.UIElements; + +namespace UnityEngine.InputSystem.Editor +{ + /// + /// A profiler module that integrates Input System with the Profiler editor window. + /// + [ProfilerModuleMetadata("Input System")] + internal sealed class InputSystemProfilerModule : ProfilerModule + { + /// + /// A profiler module detail view that extends the Profiler window and shows details for the selected frame. + /// + private sealed class InputSystemDetailsViewController : ProfilerModuleViewController + { + public InputSystemDetailsViewController(ProfilerWindow profilerWindow) + : base(profilerWindow) + {} + + private Label m_UpdateCountLabel; + private Label m_EventCountLabel; + private Label m_EventSizeLabel; + private Label m_AverageLatencyLabel; + private Label m_MaxLatencyLabel; + private Label m_EventProcessingTimeLabel; + private Label m_DeviceCountLabel; + private Label m_ControlCountLabel; + private Label m_StateBufferSizeLabel; + + private Label CreateLabel() + { + return new Label() { style = { paddingTop = 8, paddingLeft = 8 } }; + } + + protected override VisualElement CreateView() + { + var view = new VisualElement(); + + m_UpdateCountLabel = CreateLabel(); + m_EventCountLabel = CreateLabel(); + m_EventSizeLabel = CreateLabel(); + m_AverageLatencyLabel = CreateLabel(); + m_MaxLatencyLabel = CreateLabel(); + m_EventProcessingTimeLabel = CreateLabel(); + m_DeviceCountLabel = CreateLabel(); + m_ControlCountLabel = CreateLabel(); + m_StateBufferSizeLabel = CreateLabel(); + + view.Add(m_UpdateCountLabel); + view.Add(m_EventCountLabel); + view.Add(m_EventSizeLabel); + view.Add(m_AverageLatencyLabel); + view.Add(m_MaxLatencyLabel); + view.Add(m_EventProcessingTimeLabel); + view.Add(m_DeviceCountLabel); + view.Add(m_ControlCountLabel); + view.Add(m_StateBufferSizeLabel); + + // Populate the label with the current data for the selected frame. + ReloadData(); + + // Be notified when the selected frame index in the Profiler Window changes, so we can update the label. + ProfilerWindow.SelectedFrameIndexChanged += OnSelectedFrameIndexChanged; + + return view; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + // Unsubscribe from the Profiler window event that we previously subscribed to. + ProfilerWindow.SelectedFrameIndexChanged -= OnSelectedFrameIndexChanged; + } + + base.Dispose(disposing); + } + + void ReloadData() + { + var selectedFrameIndex = System.Convert.ToInt32(ProfilerWindow.selectedFrameIndex); + + var updateCount = ProfilerDriver.GetFormattedCounterValue(selectedFrameIndex, + InputStatistics.Category.Name, InputStatistics.UpdateCountName); + var eventCount = ProfilerDriver.GetFormattedCounterValue(selectedFrameIndex, + InputStatistics.Category.Name, InputStatistics.EventCountName); + var eventSizeBytes = ProfilerDriver.GetFormattedCounterValue(selectedFrameIndex, + InputStatistics.Category.Name, InputStatistics.EventSizeName); + var averageLatency = ProfilerDriver.GetFormattedCounterValue(selectedFrameIndex, + InputStatistics.Category.Name, InputStatistics.AverageLatencyName); + var maxLatency = ProfilerDriver.GetFormattedCounterValue(selectedFrameIndex, + InputStatistics.Category.Name, InputStatistics.MaxLatencyName); + var eventProcessingTime = ProfilerDriver.GetFormattedCounterValue(selectedFrameIndex, + InputStatistics.Category.Name, InputStatistics.EventProcessingTimeName); + var stateBufferSizeBytes = ProfilerDriver.GetFormattedCounterValue(selectedFrameIndex, + InputStatistics.Category.Name, InputStatistics.StateBufferSizeBytesName); + var deviceCount = ProfilerDriver.GetFormattedCounterValue(selectedFrameIndex, + InputStatistics.Category.Name, InputStatistics.DeviceCountName); + var controlCount = ProfilerDriver.GetFormattedCounterValue(selectedFrameIndex, + InputStatistics.Category.Name, InputStatistics.ControlCountName); + + m_UpdateCountLabel.text = $"{InputStatistics.UpdateCountName}: {updateCount}"; + m_EventCountLabel.text = $"{InputStatistics.EventCountName}: {eventCount}"; + m_EventSizeLabel.text = $"{InputStatistics.EventSizeName}: {eventSizeBytes}"; + m_AverageLatencyLabel.text = $"{InputStatistics.AverageLatencyName}: {averageLatency}"; + m_MaxLatencyLabel.text = $"{InputStatistics.MaxLatencyName}: {maxLatency}"; + m_EventProcessingTimeLabel.text = $"{InputStatistics.EventProcessingTimeName}: {eventProcessingTime}"; + m_StateBufferSizeLabel.text = $"{InputStatistics.StateBufferSizeBytesName}: {stateBufferSizeBytes}"; + m_DeviceCountLabel.text = $"{InputStatistics.DeviceCountName}: {deviceCount}"; + m_ControlCountLabel.text = $"{InputStatistics.ControlCountName}: {controlCount}"; + } + + void OnSelectedFrameIndexChanged(long selectedFrameIndex) + { + ReloadData(); + } + } + + private static readonly ProfilerCounterDescriptor[] Counters = new ProfilerCounterDescriptor[] + { + new ProfilerCounterDescriptor(InputStatistics.UpdateCountName, InputStatistics.Category), + new ProfilerCounterDescriptor(InputStatistics.EventCountName, InputStatistics.Category), + new ProfilerCounterDescriptor(InputStatistics.EventSizeName, InputStatistics.Category), + new ProfilerCounterDescriptor(InputStatistics.StateBufferSizeBytesName, InputStatistics.Category), + new ProfilerCounterDescriptor(InputStatistics.AverageLatencyName, InputStatistics.Category), + new ProfilerCounterDescriptor(InputStatistics.MaxLatencyName, InputStatistics.Category), + new ProfilerCounterDescriptor(InputStatistics.EventProcessingTimeName, InputStatistics.Category), + new ProfilerCounterDescriptor(InputStatistics.DeviceCountName, InputStatistics.Category), + new ProfilerCounterDescriptor(InputStatistics.ControlCountName, InputStatistics.Category), + }; + + public InputSystemProfilerModule() + : base(Counters) + {} + + public override ProfilerModuleViewController CreateDetailsViewController() + { + return new InputSystemDetailsViewController(ProfilerWindow); + } + } +} + +#endif // UNITY_EDITOR diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputSystemProfilerModule.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputSystemProfilerModule.cs.meta new file mode 100644 index 0000000000..87f697b3f2 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputSystemProfilerModule.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 820e154f9c3b47a2b7a50c6680dd4aed +timeCreated: 1729795357 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs index 10e3c17fb8..5b3504db2f 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs @@ -80,6 +80,13 @@ internal partial class InputManager static readonly ProfilerMarker k_InputOnDeviceChangeMarker = new ProfilerMarker("InpustSystem.onDeviceChange"); static readonly ProfilerMarker k_InputOnActionsChangeMarker = new ProfilerMarker("InpustSystem.onActionsChange"); + private int CountControls() + { + var count = m_DevicesCount; + for (var i = 0; i < m_DevicesCount; ++i) + count += m_Devices[i].allControls.Count; + return count; + } public InputMetrics metrics { @@ -89,11 +96,7 @@ public InputMetrics metrics result.currentNumDevices = m_DevicesCount; result.currentStateSizeInBytes = (int)m_StateBuffers.totalSize; - - // Count controls. - result.currentControlCount = m_DevicesCount; - for (var i = 0; i < m_DevicesCount; ++i) - result.currentControlCount += m_Devices[i].allControls.Count; + result.currentControlCount = CountControls(); // Count layouts. result.currentLayoutCount = m_Layouts.layoutTypes.Count; @@ -3055,6 +3058,10 @@ internal bool ShouldRunUpdate(InputUpdateType updateType) return (updateType & mask) != 0; } + struct UpdateMetrics + { + } + /// /// Process input events. /// @@ -3072,8 +3079,25 @@ internal bool ShouldRunUpdate(InputUpdateType updateType) /// which buffers we activate in the update and write the event data into. /// /// Thrown if OnUpdate is called recursively. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1809:AvoidExcessiveLocals", Justification = "TODO: Refactor later.")] private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer eventBuffer) + { + try + { + DoUpdate(updateType, ref eventBuffer); + } + finally + { + // According to documentation, profile counter calls should be stripped out automatically in + // non-development builds. + InputStatistics.DeviceCount.Sample(m_DevicesCount); + InputStatistics.StateBufferSizeBytes.Sample((int)m_StateBuffers.totalSize); + InputStatistics.ControlCount.Sample(CountControls()); + ++InputStatistics.UpdateCount.Value; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1809:AvoidExcessiveLocals", Justification = "TODO: Refactor later.")] + private unsafe void DoUpdate(InputUpdateType updateType, ref InputEventBuffer eventBuffer) { // NOTE: This is *not* using try/finally as we've seen unreliability in the EndSample() // execution (and we're not sure where it's coming from). @@ -3204,7 +3228,10 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev } var processingStartTime = Stopwatch.GetTimestamp(); + var totalEventCount = 0; + var totalEventSizeBytes = 0; var totalEventLag = 0.0; + var maxEventLag = 0.0; #if UNITY_EDITOR var isPlaying = gameIsPlaying; @@ -3465,9 +3492,14 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev // Update metrics. if (currentEventTimeInternal <= currentTime) - totalEventLag += currentTime - currentEventTimeInternal; - ++m_Metrics.totalEventCount; - m_Metrics.totalEventBytes += (int)currentEventReadPtr->sizeInBytes; + { + var lag = currentTime - currentEventTimeInternal; + totalEventLag += lag; + if (lag > maxEventLag) + maxEventLag = lag; + } + ++totalEventCount; + totalEventSizeBytes += (int)currentEventReadPtr->sizeInBytes; // Process. switch (currentEventType) @@ -3621,11 +3653,22 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev break; } - m_Metrics.totalEventProcessingTime += + ResetCurrentProcessedEventBytesForDevices(); + + // Update metrics (exposed via analytics and debugger) + var eventProcessingTime = ((double)(Stopwatch.GetTimestamp() - processingStartTime)) / Stopwatch.Frequency; + m_Metrics.totalEventCount += totalEventCount; + m_Metrics.totalEventBytes += totalEventSizeBytes; + m_Metrics.totalEventProcessingTime += eventProcessingTime; m_Metrics.totalEventLagTime += totalEventLag; - ResetCurrentProcessedEventBytesForDevices(); + // Profiler counters + InputStatistics.EventCount.Value += totalEventCount; + InputStatistics.EventSize.Value += totalEventSizeBytes; + InputStatistics.AverageLatency.Value += ((totalEventLag / totalEventCount) * 1e9); + InputStatistics.MaxLatency.Value += (maxEventLag * 1e9); + InputStatistics.EventProcessingTime.Value += eventProcessingTime * 1e9; // TODO Possible to replace Stopwatch with marker somehow? m_InputEventStream.Close(ref eventBuffer); } diff --git a/Packages/com.unity.inputsystem/InputSystem/Unity.InputSystem.asmdef b/Packages/com.unity.inputsystem/InputSystem/Unity.InputSystem.asmdef index 6bc8cc3e6a..b2c9545c97 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Unity.InputSystem.asmdef +++ b/Packages/com.unity.inputsystem/InputSystem/Unity.InputSystem.asmdef @@ -2,7 +2,8 @@ "name": "Unity.InputSystem", "rootNamespace": "", "references": [ - "Unity.ugui" + "Unity.ugui", + "Unity.Profiling.Core" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Packages/com.unity.inputsystem/InputSystem/Utilities/InputStatistics.cs b/Packages/com.unity.inputsystem/InputSystem/Utilities/InputStatistics.cs new file mode 100644 index 0000000000..4d7220718c --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Utilities/InputStatistics.cs @@ -0,0 +1,115 @@ +using Unity.Profiling; + +namespace UnityEngine.InputSystem +{ + /// + /// Input Statistics for Unity Profiler integration. + /// + internal static class InputStatistics + { + /// + /// The Profiler Category to be used. + /// + internal static readonly ProfilerCategory Category = ProfilerCategory.Input; + + internal const string EventCountName = "Total Input Event Count"; + internal const string EventSizeName = "Total Input Event Size"; + internal const string AverageLatencyName = "Average Input Latency"; + internal const string MaxLatencyName = "Max Input Latency"; + internal const string EventProcessingTimeName = "Total Input Event Processing Time"; + internal const string DeviceCountName = "Input Device Count"; + internal const string ControlCountName = "Active Control Count"; + internal const string CurrentStateMemoryBytesName = "Current State Memory Bytes"; + internal const string StateBufferSizeBytesName = "Total State Buffer Size"; + internal const string UpdateCountName = "Update Count"; + + /// + /// Counter reflecting the number of input events. + /// + /// + /// We use ProfilerCounterValue instead of ProfilerCounter since there may be multiple Input System updates + /// per frame and we want it to accumulate for the profilers perspective on what a frame is but auto-reset + /// when outside the profilers perspective of a frame. + /// + public static readonly ProfilerCounterValue EventCount = new ProfilerCounterValue( + Category, EventCountName, ProfilerMarkerDataUnit.Count, + ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush); + + /// + /// Counter reflecting the accumulated input event size in bytes. + /// + /// + /// We use ProfilerCounterValue instead of ProfilerCounter since there may be multiple Input System updates + /// per frame and we want it to accumulate for the profilers perspective on what a frame is but auto-reset + /// when outside the profilers perspective of a frame. + /// + public static readonly ProfilerCounterValue EventSize = new ProfilerCounterValue( + Category, EventSizeName, ProfilerMarkerDataUnit.Bytes, + ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush); + + /// + /// Counter value reflecting the average input latency. + /// + /// + /// We use ProfilerCounterValue instead of ProfilerCounter since there may be multiple Input System updates + /// per frame and we want it to accumulate for the profilers perspective on what a frame is but auto-reset + /// when outside the profilers perspective of a frame. + /// + public static readonly ProfilerCounterValue AverageLatency = new ProfilerCounterValue( + Category, AverageLatencyName, ProfilerMarkerDataUnit.TimeNanoseconds, + ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush); + + /// + /// Counter value reflecting the maximum input latency. + /// + /// + /// We use ProfilerCounterValue instead of ProfilerCounter since there may be multiple Input System updates + /// per frame and we want it to accumulate for the profilers perspective on what a frame is but auto-reset + /// when outside the profilers perspective of a frame. + /// + public static readonly ProfilerCounterValue MaxLatency = new ProfilerCounterValue( + Category, MaxLatencyName, ProfilerMarkerDataUnit.TimeNanoseconds, + ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush); + + /// + /// Counter value reflecting the accumulated event processing time (Update) during a rendering frame. + /// + /// + /// We use ProfilerCounterValue instead of ProfilerCounter since there may be multiple Input System updates + /// per frame and we want it to accumulate for the profilers perspective on what a frame is but auto-reset + /// when outside the profilers perspective of a frame. + /// + public static readonly ProfilerCounterValue EventProcessingTime = new ProfilerCounterValue( + Category, EventProcessingTimeName, ProfilerMarkerDataUnit.TimeNanoseconds, + ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush); + + /// + /// The number of devices currently added to the Input System. + /// + public static readonly ProfilerCounter DeviceCount = new ProfilerCounter( + Category, DeviceCountName, ProfilerMarkerDataUnit.Count); + + /// + /// The total number of device controls currently in the Input System. + /// + public static readonly ProfilerCounter ControlCount = new ProfilerCounter( + Category, ControlCountName, ProfilerMarkerDataUnit.Count); + + /// + /// The total state buffer size in bytes. + /// + public static readonly ProfilerCounter StateBufferSizeBytes = new ProfilerCounter( + Category, StateBufferSizeBytesName, ProfilerMarkerDataUnit.Bytes); + + /// + /// The total update count. + /// + /// + /// Update may get called multiple times, e.g. either via manual updates, dynamic update, fixed update + /// or editor update while running in the editor. + /// + public static readonly ProfilerCounterValue UpdateCount = new ProfilerCounterValue( + Category, UpdateCountName, ProfilerMarkerDataUnit.Count, + ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush); + } +} diff --git a/Packages/com.unity.inputsystem/InputSystem/Utilities/InputStatistics.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Utilities/InputStatistics.cs.meta new file mode 100644 index 0000000000..d48433f422 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Utilities/InputStatistics.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2656d8b72fb540fd836a3bc0c02f8e20 +timeCreated: 1729795584 \ No newline at end of file diff --git a/Packages/manifest.json b/Packages/manifest.json index 4acf434955..1e558fda9a 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -5,12 +5,12 @@ "com.unity.coding": "0.1.0-preview.24", "com.unity.ide.rider": "1.2.1", "com.unity.ide.visualstudio": "2.0.15", + "com.unity.profiling.core": "1.0.2", "com.unity.settings-manager": "1.0.3", "com.unity.test-framework": "1.4.5", "com.unity.test-framework.build": "0.0.1-preview.12", "com.unity.test-framework.performance": "3.0.3", "com.unity.test-framework.utp-reporter": "1.1.0-preview", - "com.unity.textmeshpro": "2.1.4", "com.unity.ugui": "1.0.0", "nuget.mono-cecil": "0.1.6-preview", "com.unity.modules.ai": "1.0.0",