Skip to content

Commit fb983da

Browse files
author
Rene Damm
authored
FIX: Bad performance in InputAction.controls when empty (case 1347829, #1437).
1 parent ab55d24 commit fb983da

5 files changed

Lines changed: 141 additions & 25 deletions

File tree

Assets/Tests/InputSystem/CorePerformanceTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,62 @@ public void Performance_Rebinding_OneSuccessfulCycle()
410410
.Run();
411411
}
412412

413+
public enum LookupByName
414+
{
415+
CaseMatches,
416+
CaseDoesNotMatch
417+
}
418+
419+
[Test, Performance]
420+
[Category("Performance")]
421+
[TestCase(LookupByName.CaseMatches)]
422+
[TestCase(LookupByName.CaseDoesNotMatch)]
423+
public void Performance_LookupActionByName(LookupByName lookup)
424+
{
425+
const int kActionCount = 100;
426+
427+
var asset = ScriptableObject.CreateInstance<InputActionAsset>();
428+
var map = asset.AddActionMap("map");
429+
for (var n = 0; n < kActionCount; ++n)
430+
map.AddAction("action" + n);
431+
432+
Measure.Method(() =>
433+
{
434+
var _ = asset[(lookup == LookupByName.CaseDoesNotMatch ? "ACTION" : "action") + (int)(kActionCount * 0.75f)];
435+
})
436+
.MeasurementCount(100)
437+
.WarmupCount(5)
438+
.Run();
439+
}
440+
441+
[Test, Performance]
442+
[Category("Performance")]
443+
public void Performance_LookupActionByGuid()
444+
{
445+
const int kActionCount = 100;
446+
447+
InputAction actionToFind = null;
448+
449+
var asset = ScriptableObject.CreateInstance<InputActionAsset>();
450+
var map = asset.AddActionMap("map");
451+
for (var n = 0; n < kActionCount; ++n)
452+
{
453+
var action = map.AddAction("action" + n);
454+
action.GenerateId();
455+
456+
if (n == (int)(kActionCount * 0.75f))
457+
actionToFind = action;
458+
}
459+
460+
Measure.Method(() =>
461+
{
462+
Assert.That(asset[actionToFind.id.ToString()], Is.SameAs(actionToFind));
463+
})
464+
.MeasurementCount(100)
465+
.WarmupCount(5)
466+
.Run();
467+
}
468+
413469
// We're hitting MatchesPrefix a lot from rebinding, so make sure it's performing reasonably well.
414470
[Test, Performance]
415471
[Category("Performance")]

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ however, it has to be formatted properly to pass verification tests.
4242
- Fixed interactions such as `Press` not getting processed correctly when having multiple of them on different bindings of the same action and receiving simultaneous input on all of them ([case 1364667](https://issuetracker.unity3d.com/issues/new-input-system-stops-working-after-pressing-2-keyboard-buttons-at-the-same-time)).
4343
* If, for example, you bind the A and S key on the same action, put a `Press` interaction on both, and then press both keys, interactions would get missed or got stuck.
4444
- Fixed `InputAction.IsPressed`/`WasPressed`/`WasReleased` returning incorrect results when binding multiple buttons on the same action and pressing/releasing them simultaneously.
45+
- Improved performance of looking up actions by name.
46+
- Fixed `InputAction.controls` exhibiting bad performance when there were no controls bound to an action ([case 1347829](https://issuetracker.unity3d.com/issues/inputaction-dot-controls-are-accessed-slower-when-the-gamepad-slash-controller-is-not-connected)).
4547
- Fixed interactions involving timeouts (such as `HoldInteraction`) performing erroneous delayed triggers on actions when input is composed of multiple controls ([1251231](https://issuetracker.unity3d.com/issues/input-system-composites-hold-interaction-can-be-performed-when-no-keys-are-hold)).
4648
* For example, if you bind `Shift+B` using a `OneModifierComposite` and put a `HoldInteraction` on the binding, then depending on the order in which the keys are pressed, you would sometimes see the action spuriously getting triggered when in fact no input was received.
4749
- Fixed control schemes of bindings not getting updates when being pasted from one `.inputactions` asset into another ([case 1276106](https://issuetracker.unity3d.com/issues/input-system-control-schemes-are-not-resolved-when-copying-bindings-between-inputactionassets)).

Packages/com.unity.inputsystem/InputSystem/Actions/InputActionMap.cs

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,9 @@ internal int FindActionIndex(string nameOrId)
355355
if (m_Actions == null)
356356
return -1;
357357

358+
// First time we hit this method, we populate the lookup table.
359+
SetUpActionLookupTable();
360+
358361
var actionCount = m_Actions.Length;
359362

360363
var isOldBracedFormat = nameOrId.StartsWith("{") && nameOrId.EndsWith("}");
@@ -368,6 +371,9 @@ internal int FindActionIndex(string nameOrId)
368371
}
369372
}
370373

374+
if (m_ActionIndexByNameOrId.TryGetValue(nameOrId, out var actionIndex))
375+
return actionIndex;
376+
371377
for (var i = 0; i < actionCount; ++i)
372378
{
373379
var action = m_Actions[i];
@@ -378,6 +384,35 @@ internal int FindActionIndex(string nameOrId)
378384
return InputActionState.kInvalidIndex;
379385
}
380386

387+
private void SetUpActionLookupTable()
388+
{
389+
if (m_ActionIndexByNameOrId != null || m_Actions == null)
390+
return;
391+
392+
m_ActionIndexByNameOrId = new Dictionary<string, int>();
393+
394+
var actionCount = m_Actions.Length;
395+
for (var i = 0; i < actionCount; ++i)
396+
{
397+
var action = m_Actions[i];
398+
399+
// We want to make sure an action ID cannot change *after* we have created the table.
400+
// NOTE: The *name* of an action, however, *may* change.
401+
action.MakeSureIdIsInPlace();
402+
403+
// We create two lookup paths for each action:
404+
// (1) By case-sensitive name.
405+
// (2) By GUID string.
406+
m_ActionIndexByNameOrId[action.name] = i;
407+
m_ActionIndexByNameOrId[action.m_Id] = i;
408+
}
409+
}
410+
411+
internal void ClearActionLookupTable()
412+
{
413+
m_ActionIndexByNameOrId?.Clear();
414+
}
415+
381416
private int FindActionIndex(Guid id)
382417
{
383418
if (m_Actions == null)
@@ -663,11 +698,14 @@ IEnumerator IEnumerable.GetEnumerator()
663698
/// isn't, we create a separate array with the bindings sorted by action and have each action reference
664699
/// a slice through <see cref="InputAction.m_BindingsStartIndex"/> and <see cref="InputAction.m_BindingsCount"/>.
665700
/// </remarks>
666-
/// <seealso cref="SetUpPerActionCachedBindingData"/>
701+
/// <seealso cref="SetUpPerActionControlAndBindingArrays"/>
667702
[NonSerialized] private InputBinding[] m_BindingsForEachAction;
668703

669704
[NonSerialized] private InputControl[] m_ControlsForEachAction;
670705

706+
[NonSerialized] private bool m_ControlsForEachActionInitialized;
707+
[NonSerialized] private bool m_BindingsForEachActionInitialized;
708+
671709
/// <summary>
672710
/// Number of actions currently enabled in the map.
673711
/// </summary>
@@ -697,6 +735,8 @@ IEnumerator IEnumerable.GetEnumerator()
697735

698736
[NonSerialized] internal CallbackArray<Action<InputAction.CallbackContext>> m_ActionCallbacks;
699737

738+
[NonSerialized] internal Dictionary<string, int> m_ActionIndexByNameOrId;
739+
700740
internal static int s_DeferBindingResolution;
701741

702742
internal struct DeviceArray
@@ -780,8 +820,8 @@ internal ReadOnlyArray<InputBinding> GetBindingsForSingleAction(InputAction acti
780820
Debug.Assert(!action.isSingletonAction || m_SingletonAction == action, "Action is not a singleton action");
781821

782822
// See if we need to refresh.
783-
if (m_BindingsForEachAction == null)
784-
SetUpPerActionCachedBindingData();
823+
if (!m_BindingsForEachActionInitialized)
824+
SetUpPerActionControlAndBindingArrays();
785825

786826
return new ReadOnlyArray<InputBinding>(m_BindingsForEachAction, action.m_BindingsStartIndex,
787827
action.m_BindingsCount);
@@ -796,8 +836,8 @@ internal ReadOnlyArray<InputControl> GetControlsForSingleAction(InputAction acti
796836
Debug.Assert(action.m_ActionMap == this);
797837
Debug.Assert(!action.isSingletonAction || m_SingletonAction == action);
798838

799-
if (m_ControlsForEachAction == null)
800-
SetUpPerActionCachedBindingData();
839+
if (!m_ControlsForEachActionInitialized)
840+
SetUpPerActionControlAndBindingArrays();
801841

802842
return new ReadOnlyArray<InputControl>(m_ControlsForEachAction, action.m_ControlStartIndex,
803843
action.m_ControlCount);
@@ -815,11 +855,17 @@ internal ReadOnlyArray<InputControl> GetControlsForSingleAction(InputAction acti
815855
/// controls yet (i.e. <see cref="m_State"/> is <c>null</c>). Otherwise, using <see cref="InputAction.bindings"/>
816856
/// may trigger a control resolution which would be surprising.
817857
/// </remarks>
818-
private unsafe void SetUpPerActionCachedBindingData()
858+
private unsafe void SetUpPerActionControlAndBindingArrays()
819859
{
820860
// Handle case where we don't have any bindings.
821861
if (m_Bindings == null)
862+
{
863+
m_ControlsForEachAction = null;
864+
m_BindingsForEachAction = null;
865+
m_ControlsForEachActionInitialized = true;
866+
m_BindingsForEachActionInitialized = true;
822867
return;
868+
}
823869

824870
if (m_SingletonAction != null)
825871
{
@@ -1003,13 +1049,19 @@ private unsafe void SetUpPerActionCachedBindingData()
10031049
m_BindingsForEachAction = newBindingsArray;
10041050
}
10051051
}
1052+
1053+
m_ControlsForEachActionInitialized = true;
1054+
m_BindingsForEachActionInitialized = true;
10061055
}
10071056

10081057
////TODO: re-use allocations such that only grow the arrays and hit zero GC allocs when we already have enough memory
1009-
internal void ClearPerActionCachedBindingData()
1058+
internal void ClearCachedActionData()
10101059
{
1011-
m_BindingsForEachAction = null;
1012-
m_ControlsForEachAction = null;
1060+
m_BindingsForEachActionInitialized = default;
1061+
m_ControlsForEachActionInitialized = default;
1062+
m_BindingsForEachAction = default;
1063+
m_ControlsForEachAction = default;
1064+
m_ActionIndexByNameOrId = default;
10131065
}
10141066

10151067
internal void GenerateId()
@@ -1025,6 +1077,7 @@ internal bool LazyResolveBindings()
10251077
{
10261078
// Clear cached controls for actions. Don't need to necessarily clear m_BindingsForEachAction.
10271079
m_ControlsForEachAction = null;
1080+
m_ControlsForEachActionInitialized = false;
10281081

10291082
// If we haven't had to resolve bindings yet, we can wait until when we
10301083
// actually have to.
@@ -1085,6 +1138,8 @@ internal void ResolveBindingsIfNecessary()
10851138
/// </remarks>
10861139
internal void ResolveBindings()
10871140
{
1141+
m_ControlsForEachActionInitialized = false;
1142+
10881143
// Make sure that if we trigger callbacks as part of disabling and re-enabling actions,
10891144
// we don't trigger a re-resolve while we're already resolving bindings.
10901145
using (InputActionRebindingExtensions.DeferBindingResolution())
@@ -1196,6 +1251,7 @@ internal void ResolveBindings()
11961251

11971252
////TODO: determine whether we really need to wipe this; keep them if nothing has changed
11981253
map.m_ControlsForEachAction = null;
1254+
map.m_ControlsForEachActionInitialized = false;
11991255

12001256
if (map.m_SingletonAction != null)
12011257
InputActionState.NotifyListenersOfActionChange(InputActionChange.BoundControlsChanged, map.m_SingletonAction);
@@ -1807,7 +1863,8 @@ public void OnAfterDeserialize()
18071863

18081864
// Make sure we don't retain any cached per-action data when using serialization
18091865
// to doctor around in action map configurations in the editor.
1810-
ClearPerActionCachedBindingData();
1866+
ClearCachedActionData();
1867+
ClearActionLookupTable();
18111868
}
18121869

18131870
#endregion

Packages/com.unity.inputsystem/InputSystem/Actions/InputActionRebindingExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ public static int ApplyBindingOverride(this InputActionMap actionMap, InputBindi
706706

707707
if (matchCount > 0)
708708
{
709-
actionMap.ClearPerActionCachedBindingData();
709+
actionMap.ClearCachedActionData();
710710
actionMap.LazyResolveBindings();
711711
}
712712

@@ -740,7 +740,7 @@ public static void ApplyBindingOverride(this InputActionMap actionMap, int bindi
740740
actionMap.m_Bindings[bindingIndex].overrideInteractions = bindingOverride.overrideInteractions;
741741
actionMap.m_Bindings[bindingIndex].overrideProcessors = bindingOverride.overrideProcessors;
742742

743-
actionMap.ClearPerActionCachedBindingData();
743+
actionMap.ClearCachedActionData();
744744
actionMap.LazyResolveBindings();
745745
}
746746

@@ -835,7 +835,7 @@ public static void RemoveAllBindingOverrides(this IInputActionCollection2 action
835835
binding.RemoveOverrides();
836836
}
837837

838-
actionMap.ClearPerActionCachedBindingData();
838+
actionMap.ClearCachedActionData();
839839
actionMap.LazyResolveBindings();
840840
}
841841
}
@@ -874,7 +874,7 @@ public static void RemoveAllBindingOverrides(this InputAction action)
874874
bindings[i].overrideProcessors = null;
875875
}
876876

877-
actionMap.ClearPerActionCachedBindingData();
877+
actionMap.ClearCachedActionData();
878878
actionMap.LazyResolveBindings();
879879
}
880880

Packages/com.unity.inputsystem/InputSystem/Actions/InputActionSetupExtensions.cs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ public static InputAction AddAction(this InputActionMap map, string name, InputA
201201
if (map.asset != null)
202202
map.asset.MarkAsDirty();
203203

204-
map.ClearPerActionCachedBindingData();
204+
map.ClearCachedActionData();
205205
map.LazyResolveBindings();
206206

207207
return action;
@@ -246,7 +246,7 @@ public static void RemoveAction(this InputAction action)
246246
if (actionMap.asset != null)
247247
actionMap.asset.MarkAsDirty();
248248

249-
actionMap.ClearPerActionCachedBindingData();
249+
actionMap.ClearCachedActionData();
250250

251251
// Remove bindings to action from map.
252252
var newActionMapBindingCount = actionMap.m_Bindings.Length - bindingsForAction.Length;
@@ -508,7 +508,7 @@ private static int AddBindingInternal(InputActionMap map, InputBinding binding,
508508

509509
// Invalidate per-action binding sets so that this gets refreshed if
510510
// anyone queries it.
511-
map.ClearPerActionCachedBindingData();
511+
map.ClearCachedActionData();
512512

513513
// Make sure bindings get re-resolved.
514514
map.LazyResolveBindings();
@@ -836,6 +836,7 @@ public static void Rename(this InputAction action, string newName)
836836

837837
var oldName = action.m_Name;
838838
action.m_Name = newName;
839+
actionMap?.ClearActionLookupTable();
839840

840841
if (actionMap?.asset != null)
841842
actionMap?.asset.MarkAsDirty();
@@ -1058,7 +1059,7 @@ public BindingSyntax WithName(string name)
10581059
if (!valid)
10591060
throw new InvalidOperationException("Accessor is not valid");
10601061
m_ActionMap.m_Bindings[m_BindingIndexInMap].name = name;
1061-
m_ActionMap.ClearPerActionCachedBindingData();
1062+
m_ActionMap.ClearCachedActionData();
10621063
m_ActionMap.LazyResolveBindings();
10631064
return this;
10641065
}
@@ -1075,7 +1076,7 @@ public BindingSyntax WithPath(string path)
10751076
if (!valid)
10761077
throw new InvalidOperationException("Accessor is not valid");
10771078
m_ActionMap.m_Bindings[m_BindingIndexInMap].path = path;
1078-
m_ActionMap.ClearPerActionCachedBindingData();
1079+
m_ActionMap.ClearCachedActionData();
10791080
m_ActionMap.LazyResolveBindings();
10801081
return this;
10811082
}
@@ -1114,7 +1115,7 @@ public BindingSyntax WithGroups(string groups)
11141115

11151116
// Set groups on binding.
11161117
m_ActionMap.m_Bindings[m_BindingIndexInMap].groups = groups;
1117-
m_ActionMap.ClearPerActionCachedBindingData();
1118+
m_ActionMap.ClearCachedActionData();
11181119
m_ActionMap.LazyResolveBindings();
11191120

11201121
return this;
@@ -1147,7 +1148,7 @@ public BindingSyntax WithInteractions(string interactions)
11471148

11481149
// Set interactions on binding.
11491150
m_ActionMap.m_Bindings[m_BindingIndexInMap].interactions = interactions;
1150-
m_ActionMap.ClearPerActionCachedBindingData();
1151+
m_ActionMap.ClearCachedActionData();
11511152
m_ActionMap.LazyResolveBindings();
11521153

11531154
return this;
@@ -1193,7 +1194,7 @@ public BindingSyntax WithProcessors(string processors)
11931194

11941195
// Set processors on binding.
11951196
m_ActionMap.m_Bindings[m_BindingIndexInMap].processors = processors;
1196-
m_ActionMap.ClearPerActionCachedBindingData();
1197+
m_ActionMap.ClearCachedActionData();
11971198
m_ActionMap.LazyResolveBindings();
11981199

11991200
return this;
@@ -1221,7 +1222,7 @@ public BindingSyntax Triggering(InputAction action)
12211222
throw new ArgumentException(
12221223
$"Cannot change the action a binding triggers on singleton action '{action}'", nameof(action));
12231224
m_ActionMap.m_Bindings[m_BindingIndexInMap].action = action.name;
1224-
m_ActionMap.ClearPerActionCachedBindingData();
1225+
m_ActionMap.ClearCachedActionData();
12251226
m_ActionMap.LazyResolveBindings();
12261227
return this;
12271228
}
@@ -1242,7 +1243,7 @@ public BindingSyntax To(InputBinding binding)
12421243
throw new InvalidOperationException("Accessor is not valid");
12431244

12441245
m_ActionMap.m_Bindings[m_BindingIndexInMap] = binding;
1245-
m_ActionMap.ClearPerActionCachedBindingData();
1246+
m_ActionMap.ClearCachedActionData();
12461247
m_ActionMap.LazyResolveBindings();
12471248

12481249
// If it's a singleton action, we force the binding to stay with the action.
@@ -1464,7 +1465,7 @@ public void Erase()
14641465
ArrayHelpers.EraseAt(ref m_ActionMap.m_Bindings, m_BindingIndexInMap);
14651466
}
14661467

1467-
m_ActionMap.ClearPerActionCachedBindingData();
1468+
m_ActionMap.ClearCachedActionData();
14681469
m_ActionMap.LazyResolveBindings();
14691470

14701471
// We have switched to a different binding array. For singleton actions, we need to

0 commit comments

Comments
 (0)