Skip to content
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

NEW: Input System Profiler Module #2038

Draft
wants to merge 7 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A profiler module that integrates Input System with the Profiler editor window.
/// </summary>
[ProfilerModuleMetadata("Input System")]
internal sealed class InputSystemProfilerModule : ProfilerModule
{
/// <summary>
/// A profiler module detail view that extends the Profiler window and shows details for the selected frame.
/// </summary>
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}";
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a basic label list at the moment. Is it possible to pass any extra information via the profiler API? Or extract sample data to e.g. create a distribution within detail window?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a ProfilerRecorder that you can use to gather samples, which then allows you to do more statistical computations.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ekcoh it seems to be possible to add more complex UITK elements. I think a list can be fine for now, and then we could add a nicer UI using UITK (there's more info here , which you probably have seen; and a more more complex UI example in this package where this is the result: image

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(InputStatistics.UpdateCountName, InputStatistics.Category),
new(InputStatistics.EventCountName, InputStatistics.Category),
new(InputStatistics.EventSizeName, InputStatistics.Category),
new(InputStatistics.StateBufferSizeBytesName, InputStatistics.Category),
new(InputStatistics.AverageLatencyName, InputStatistics.Category),
new(InputStatistics.MaxLatencyName, InputStatistics.Category),
new(InputStatistics.EventProcessingTimeName, InputStatistics.Category),
new(InputStatistics.DeviceCountName, InputStatistics.Category),
new(InputStatistics.ControlCountName, InputStatistics.Category),
};

public InputSystemProfilerModule()
: base(Counters)
{}

public override ProfilerModuleViewController CreateDetailsViewController()
{
return new InputSystemDetailsViewController(ProfilerWindow);
}
}
}

#endif // UNITY_EDITOR

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 54 additions & 11 deletions Packages/com.unity.inputsystem/InputSystem/InputManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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;
Expand Down Expand Up @@ -3055,6 +3058,10 @@ internal bool ShouldRunUpdate(InputUpdateType updateType)
return (updateType & mask) != 0;
}

struct UpdateMetrics
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To remove?

{
}

/// <summary>
/// Process input events.
/// </summary>
Expand All @@ -3072,8 +3079,25 @@ internal bool ShouldRunUpdate(InputUpdateType updateType)
/// which buffers we activate in the update and write the event data into.
/// </remarks>
/// <exception cref="InvalidOperationException">Thrown if OnUpdate is called recursively.</exception>
[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);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs more work to extract some stuff from DoUpdate below

}
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).
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Comment on lines 3494 to +3502
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This a bit more complex so I would extract this to a method, as OnUpdate is already big enoug.


// Process.
switch (currentEventType)
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a comment that these counters are PerFrame, as we can call update more than once?
Or Maybe a prefix of something like InputStatistics.EventCountPerFrame.Value or PerUpdate, something like that.

Also if could we use ProfileRecorder for this?

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?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this last line here is an anti pattern, ideally I would like to include the Update profiler marker into the profiler module, is this possible?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During hackweek I used StopWatch. It was preallocated and reset and reused every frame and accumulated in a ProfilerCounter

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which is what it looks like you're doing here :)


m_InputEventStream.Close(ref eventBuffer);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "Unity.InputSystem",
"rootNamespace": "",
"references": [
"Unity.ugui"
"Unity.ugui",
"Unity.Profiling.Core"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if this might create some overhead? Probably we could set a define like INPUTSYSTEM_USE_CUSTOMPROFILER or something similar, in case users don't want to have this code in their release builds.

],
"includePlatforms": [],
"excludePlatforms": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using Unity.Profiling;

namespace UnityEngine.InputSystem
{
/// <summary>
/// Input Statistics for Unity Profiler integration.
/// </summary>
internal static class InputStatistics
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd create a Editor/Internal/Profiler folder and but everything in there. I find odd to have this class next to a bunch of "Utilities" classes.

{
/// <summary>
/// The Profiler Category to be used.
/// </summary>
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";

/// <summary>
/// Counter reflecting the number of input events.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static readonly ProfilerCounterValue<int> EventCount = new ProfilerCounterValue<int>(
Category, EventCountName, ProfilerMarkerDataUnit.Count,
ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush);

/// <summary>
/// Counter reflecting the accumulated input event size in bytes.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static readonly ProfilerCounterValue<int> EventSize = new ProfilerCounterValue<int>(
Category, EventSizeName, ProfilerMarkerDataUnit.Bytes,
ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush);

/// <summary>
/// Counter value reflecting the average input latency.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static readonly ProfilerCounterValue<double> AverageLatency = new ProfilerCounterValue<double>(
Category, AverageLatencyName, ProfilerMarkerDataUnit.TimeNanoseconds,
ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush);

/// <summary>
/// Counter value reflecting the maximum input latency.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static readonly ProfilerCounterValue<double> MaxLatency = new ProfilerCounterValue<double>(
Category, MaxLatencyName, ProfilerMarkerDataUnit.TimeNanoseconds,
ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush);

/// <summary>
/// Counter value reflecting the accumulated event processing time (Update) during a rendering frame.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static readonly ProfilerCounterValue<double> EventProcessingTime = new ProfilerCounterValue<double>(
Category, EventProcessingTimeName, ProfilerMarkerDataUnit.TimeNanoseconds,
ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush);

/// <summary>
/// The number of devices currently added to the Input System.
/// </summary>
public static readonly ProfilerCounter<int> DeviceCount = new ProfilerCounter<int>(
Category, DeviceCountName, ProfilerMarkerDataUnit.Count);

/// <summary>
/// The total number of device controls currently in the Input System.
/// </summary>
public static readonly ProfilerCounter<int> ControlCount = new ProfilerCounter<int>(
Category, ControlCountName, ProfilerMarkerDataUnit.Count);

/// <summary>
/// The total state buffer size in bytes.
/// </summary>
public static readonly ProfilerCounter<int> StateBufferSizeBytes = new ProfilerCounter<int>(
Category, StateBufferSizeBytesName, ProfilerMarkerDataUnit.Bytes);

/// <summary>
/// The total update count.
/// </summary>
/// <remarks>
/// Update may get called multiple times, e.g. either via manual updates, dynamic update, fixed update
/// or editor update while running in the editor.
/// </remarks>
public static readonly ProfilerCounterValue<int> UpdateCount = new ProfilerCounterValue<int>(
Category, UpdateCountName, ProfilerMarkerDataUnit.Count,
ProfilerCounterOptions.FlushOnEndOfFrame | ProfilerCounterOptions.ResetToZeroOnFlush);
}
}
Loading