Skip to content

Commit ff19322

Browse files
committed
- Objects animated via Animation and/or Animator components are now searched for references
- NativeArrays are no longer searched for references, iterating over them can throw InvalidOperationException if the collection is disposed. It shouldn't matter since NativeArrays are meant to store only blittable structs which can't have any Object references (+surprisingly, it can reduce the search time substantially in some projects) - Fixed tooltips getting culled by the AssetUsageDetector window in the search results page
1 parent 51abec2 commit ff19322

File tree

5 files changed

+276
-97
lines changed

5 files changed

+276
-97
lines changed

Plugins/AssetUsageDetector/Editor/AssetUsageDetector.cs

+171-44
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,11 @@ public void Refresh( string path )
129129
private readonly Dictionary<Type, VariableGetterHolder[]> typeToVariables = new Dictionary<Type, VariableGetterHolder[]>( 4096 );
130130
// An optimization to search an object only once (key is a hash of the searched object)
131131
private readonly Dictionary<string, ReferenceNode> searchedObjects = new Dictionary<string, ReferenceNode>( 32768 );
132+
// An optimization to fetch an animation clip's curve bindings only once
133+
private readonly Dictionary<AnimationClip, EditorCurveBinding[]> animationClipUniqueBindings = new Dictionary<AnimationClip, EditorCurveBinding[]>( 256 );
132134
// An optimization to fetch the dependencies of an asset only once (key is the path of the asset)
133135
private Dictionary<string, CacheEntry> assetDependencyCache;
136+
private CacheEntry lastRefreshedCacheEntry;
134137

135138
// Dictionary to quickly find the function to search a specific type with
136139
private Dictionary<Type, Func<Object, ReferenceNode>> typeToSearchFunction;
@@ -145,6 +148,7 @@ public void Refresh( string path )
145148
private bool searchMaterialsForTexture;
146149

147150
private bool searchSerializableVariablesOnly;
151+
private bool prevSearchSerializableVariablesOnly;
148152

149153
private int searchDepthLimit; // Depth limit for recursively searching variables of objects
150154

@@ -168,8 +172,6 @@ public void Refresh( string path )
168172
private readonly List<ReferenceNode> nodesPool = new List<ReferenceNode>( 32 );
169173
private readonly List<VariableGetterHolder> validVariables = new List<VariableGetterHolder>( 32 );
170174

171-
private readonly string reflectionNameSpace = typeof( Assembly ).Namespace;
172-
173175
private string CachePath { get { return Application.dataPath + "/../Library/AssetUsageDetector.cache"; } } // Path of the cache file
174176

175177
// Search for references!
@@ -235,14 +237,16 @@ public SearchResult Run( Parameters searchParameters )
235237
this.fieldModifiers = searchParameters.fieldModifiers | BindingFlags.Instance | BindingFlags.DeclaredOnly;
236238
this.propertyModifiers = searchParameters.propertyModifiers | BindingFlags.Instance | BindingFlags.DeclaredOnly;
237239
this.searchDepthLimit = searchParameters.searchDepthLimit;
240+
this.searchSerializableVariablesOnly = !searchParameters.searchNonSerializableVariables;
238241

239242
// Initialize commonly used variables
240243
searchResult = new List<SearchResultGroup>(); // Overall search results
241244

242-
if( prevFieldModifiers != fieldModifiers || prevPropertyModifiers != propertyModifiers )
245+
if( prevFieldModifiers != fieldModifiers || prevPropertyModifiers != propertyModifiers || prevSearchSerializableVariablesOnly != searchSerializableVariablesOnly )
243246
typeToVariables.Clear();
244247

245248
searchedObjects.Clear();
249+
animationClipUniqueBindings.Clear();
246250
callStack.Clear();
247251
objectsToSearchSet.Clear();
248252
sceneObjectsToSearchSet.Clear();
@@ -265,6 +269,8 @@ public SearchResult Run( Parameters searchParameters )
265269
foreach( var cacheEntry in assetDependencyCache.Values )
266270
cacheEntry.searchResult = CacheEntry.Result.Unknown;
267271

272+
lastRefreshedCacheEntry = null;
273+
268274
if( typeToSearchFunction == null )
269275
{
270276
typeToSearchFunction = new Dictionary<Type, Func<Object, ReferenceNode>>()
@@ -287,6 +293,7 @@ public SearchResult Run( Parameters searchParameters )
287293

288294
prevFieldModifiers = fieldModifiers;
289295
prevPropertyModifiers = propertyModifiers;
296+
prevSearchSerializableVariablesOnly = searchSerializableVariablesOnly;
290297

291298
searchPrefabConnections = false;
292299
searchMonoBehavioursForScript = false;
@@ -428,9 +435,6 @@ public SearchResult Run( Parameters searchParameters )
428435
} );
429436
}
430437

431-
// By default, search only serializable variables for references
432-
searchSerializableVariablesOnly = !searchParameters.searchNonSerializableVariables;
433-
434438
// Initialize the nodes of searched asset(s)
435439
foreach( Object obj in objectsToSearchSet )
436440
searchedObjects.Add( obj.Hash(), PopReferenceNode( obj ) );
@@ -539,7 +543,7 @@ public SearchResult Run( Parameters searchParameters )
539543
searchResult.Add( currentSearchResultGroup );
540544
}
541545

542-
// Search non-serializable variables for references only if we are currently searching a scene and the editor is in play mode
546+
// Search non-serializable variables for references while searching a scene in play mode
543547
if( isInPlayMode )
544548
searchSerializableVariablesOnly = false;
545549

@@ -626,28 +630,39 @@ public SearchResult Run( Parameters searchParameters )
626630
}
627631
catch( Exception e )
628632
{
629-
Debug.LogException( e );
630-
633+
StringBuilder sb = new StringBuilder( objectsToSearchSet.Count * 50 + callStack.Count * 50 + 500 );
634+
sb.AppendLine( "<b>AssetUsageDetector Error:</b>" ).AppendLine();
631635
if( callStack.Count > 0 )
632636
{
633-
StringBuilder sb = new StringBuilder( callStack.Count * 50 );
634637
sb.AppendLine( "Stack contents: " );
635-
for( int i = 0; i < callStack.Count; i++ )
638+
for( int i = callStack.Count - 1; i >= 0; i-- )
636639
{
637640
sb.Append( i ).Append( ": " );
638641

639642
Object unityObject = callStack[i] as Object;
640-
if( unityObject != null )
643+
if( unityObject )
641644
sb.Append( unityObject.name ).Append( " (" ).Append( unityObject.GetType() ).AppendLine( ")" );
642645
else if( callStack[i] != null )
643646
sb.Append( callStack[i].GetType() ).AppendLine( " object" );
644647
else
645648
sb.AppendLine( "<<destroyed>>" );
646649
}
647650

648-
Debug.LogError( sb.ToString() );
651+
sb.AppendLine();
652+
}
653+
654+
sb.AppendLine( "Searching references of: " );
655+
foreach( Object obj in objectsToSearchSet )
656+
{
657+
if( obj )
658+
sb.Append( obj.name ).Append( " (" ).Append( obj.GetType() ).AppendLine( ")" );
649659
}
650660

661+
sb.AppendLine();
662+
sb.Append( e ).AppendLine();
663+
664+
Debug.LogError( sb.ToString() );
665+
651666
try
652667
{
653668
InitializeSearchResultNodes( searchResult );
@@ -1012,11 +1027,17 @@ private ReferenceNode SearchComponent( Object unityObject )
10121027
// If this component is an Animation, search its animation clips for references
10131028
foreach( AnimationState anim in (Animation) component )
10141029
referenceNode.AddLinkTo( SearchObject( anim.clip ) );
1030+
1031+
// Search the objects that are animated by this Animation component for references
1032+
SearchAnimatedObjects( referenceNode );
10151033
}
10161034
else if( component is Animator )
10171035
{
1018-
// If this component is an Animator, search its animation clips for references
1036+
// If this component is an Animator, search its animation clips for references (via AnimatorController)
10191037
referenceNode.AddLinkTo( SearchObject( ( (Animator) component ).runtimeAnimatorController ) );
1038+
1039+
// Search the objects that are animated by this Animator component for references
1040+
SearchAnimatedObjects( referenceNode );
10201041
}
10211042
#if UNITY_2017_2_OR_NEWER
10221043
else if( component is Tilemap )
@@ -1227,6 +1248,85 @@ private ReferenceNode SearchSpriteAtlas( Object unityObject )
12271248
}
12281249
#endif
12291250

1251+
// Find references from an Animation/Animator component to the objects that it animates
1252+
private void SearchAnimatedObjects( ReferenceNode referenceNode )
1253+
{
1254+
GameObject root = ( (Component) referenceNode.nodeObject ).gameObject;
1255+
AnimationClip[] clips = AnimationUtility.GetAnimationClips( root );
1256+
for( int i = 0; i < clips.Length; i++ )
1257+
{
1258+
AnimationClip clip = clips[i];
1259+
bool isClipUnique = true;
1260+
for( int j = i - 1; j >= 0; j-- )
1261+
{
1262+
if( clips[j] == clip )
1263+
{
1264+
isClipUnique = false;
1265+
break;
1266+
}
1267+
}
1268+
1269+
if( !isClipUnique )
1270+
continue;
1271+
1272+
EditorCurveBinding[] uniqueBindings;
1273+
if( !animationClipUniqueBindings.TryGetValue( clip, out uniqueBindings ) )
1274+
{
1275+
// Calculate all the "unique" paths that the animation clip's curves have
1276+
// Both float curves (GetCurveBindings) and object reference curves (GetObjectReferenceCurveBindings) are checked
1277+
List<EditorCurveBinding> _uniqueBindings = new List<EditorCurveBinding>( 2 );
1278+
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings( clip );
1279+
for( int j = 0; j < bindings.Length; j++ )
1280+
{
1281+
string bindingPath = bindings[j].path;
1282+
if( string.IsNullOrEmpty( bindingPath ) ) // Ignore the root animated object
1283+
continue;
1284+
1285+
bool isBindingUnique = true;
1286+
for( int k = _uniqueBindings.Count - 1; k >= 0; k-- )
1287+
{
1288+
if( bindingPath == _uniqueBindings[k].path )
1289+
{
1290+
isBindingUnique = false;
1291+
break;
1292+
}
1293+
}
1294+
1295+
if( isBindingUnique )
1296+
_uniqueBindings.Add( bindings[j] );
1297+
}
1298+
1299+
bindings = AnimationUtility.GetObjectReferenceCurveBindings( clip );
1300+
for( int j = 0; j < bindings.Length; j++ )
1301+
{
1302+
string bindingPath = bindings[j].path;
1303+
if( string.IsNullOrEmpty( bindingPath ) ) // Ignore the root animated object
1304+
continue;
1305+
1306+
bool isBindingUnique = true;
1307+
for( int k = _uniqueBindings.Count - 1; k >= 0; k-- )
1308+
{
1309+
if( bindingPath == _uniqueBindings[k].path )
1310+
{
1311+
isBindingUnique = false;
1312+
break;
1313+
}
1314+
}
1315+
1316+
if( isBindingUnique )
1317+
_uniqueBindings.Add( bindings[j] );
1318+
}
1319+
1320+
uniqueBindings = _uniqueBindings.ToArray();
1321+
animationClipUniqueBindings[clip] = uniqueBindings;
1322+
}
1323+
1324+
string clipName = clip.name;
1325+
for( int j = 0; j < uniqueBindings.Length; j++ )
1326+
referenceNode.AddLinkTo( SearchObject( AnimationUtility.GetAnimatedObject( root, uniqueBindings[j] ) ), "Animated via clip: " + clipName );
1327+
}
1328+
}
1329+
12301330
// Search through field and properties of an object for references with SerializedObject
12311331
private void SearchWithSerializedObject( ReferenceNode referenceNode )
12321332
{
@@ -1319,12 +1419,10 @@ private void SearchFieldsAndPropertiesOf( ReferenceNode referenceNode )
13191419
}
13201420
}
13211421
}
1322-
catch( UnassignedReferenceException )
1323-
{ }
1324-
catch( MissingReferenceException )
1325-
{ }
1326-
catch( MissingComponentException )
1327-
{ }
1422+
catch( UnassignedReferenceException ) { }
1423+
catch( MissingReferenceException ) { }
1424+
catch( MissingComponentException ) { }
1425+
catch( NotImplementedException ) { }
13281426
}
13291427
}
13301428

@@ -1341,7 +1439,7 @@ private VariableGetterHolder[] GetFilteredVariablesForType( Type type )
13411439
// 2- skip primitive types, enums and strings
13421440
// 3- skip common Unity types that can't hold any references (e.g. Vector3, Rect, Color, Quaternion)
13431441
//
1344-
// P.S. IsPrimitiveUnityType() extension function handles steps 2) and 3)
1442+
// P.S. IsIgnoredUnityType() extension function handles steps 2) and 3)
13451443

13461444
validVariables.Clear();
13471445

@@ -1361,16 +1459,7 @@ private VariableGetterHolder[] GetFilteredVariablesForType( Type type )
13611459
continue;
13621460

13631461
// Skip primitive types
1364-
Type variableType = field.FieldType;
1365-
if( variableType.IsPrimitiveUnityType() )
1366-
continue;
1367-
1368-
// Searching assembly variables for reference throws InvalidCastException on .NET 4.0 runtime
1369-
if( typeof( Type ).IsAssignableFrom( variableType ) || variableType.Namespace == reflectionNameSpace )
1370-
continue;
1371-
1372-
// Searching pointer variables for reference throws ArgumentException
1373-
if( variableType.IsPointer )
1462+
if( field.FieldType.IsIgnoredUnityType() )
13741463
continue;
13751464

13761465
// Additional filtering for fields:
@@ -1382,7 +1471,7 @@ private VariableGetterHolder[] GetFilteredVariablesForType( Type type )
13821471

13831472
VariableGetVal getter = field.CreateGetter( type );
13841473
if( getter != null )
1385-
validVariables.Add( new VariableGetterHolder( field, getter, field.IsSerializable() ) );
1474+
validVariables.Add( new VariableGetterHolder( field, getter, searchSerializableVariablesOnly ? field.IsSerializable() : true ) );
13861475
}
13871476

13881477
currType = currType.BaseType;
@@ -1404,16 +1493,7 @@ private VariableGetterHolder[] GetFilteredVariablesForType( Type type )
14041493
continue;
14051494

14061495
// Skip primitive types
1407-
Type variableType = property.PropertyType;
1408-
if( variableType.IsPrimitiveUnityType() )
1409-
continue;
1410-
1411-
// Searching assembly variables for reference throws InvalidCastException on .NET 4.0 runtime
1412-
if( typeof( Type ).IsAssignableFrom( variableType ) || variableType.Namespace == reflectionNameSpace )
1413-
continue;
1414-
1415-
// Searching pointer variables for reference throws ArgumentException
1416-
if( variableType.IsPointer )
1496+
if( property.PropertyType.IsIgnoredUnityType() )
14171497
continue;
14181498

14191499
// Skip properties without a getter function
@@ -1458,7 +1538,7 @@ private VariableGetterHolder[] GetFilteredVariablesForType( Type type )
14581538
{
14591539
VariableGetVal getter = property.CreateGetter();
14601540
if( getter != null )
1461-
validVariables.Add( new VariableGetterHolder( property, getter, property.IsSerializable() ) );
1541+
validVariables.Add( new VariableGetterHolder( property, getter, searchSerializableVariablesOnly ? property.IsSerializable() : true ) );
14621542
}
14631543
}
14641544

@@ -1506,8 +1586,55 @@ private bool AssetHasAnyReference( string assetPath )
15061586
FileInfo assetFile = new FileInfo( dependencies[i] );
15071587
if( !assetFile.Exists || assetFile.Length != fileSizes[i] )
15081588
{
1589+
// Although not reproduced, it is reported that this section caused StackOverflowException due to infinite loop,
1590+
// if that happens, log useful information to help reproduce the issue
1591+
if( lastRefreshedCacheEntry == cacheEntry )
1592+
{
1593+
StringBuilder sb = new StringBuilder( 1000 );
1594+
sb.AppendLine( "<b>Infinite loop while refreshing a cache entry, please report it to the author.</b>" ).AppendLine();
1595+
sb.Append( "Asset path: " ).AppendLine( assetPath );
1596+
1597+
for( int j = 0; j < 2; j++ )
1598+
{
1599+
if( j == 1 )
1600+
{
1601+
cacheEntry.Refresh( assetPath );
1602+
dependencies = cacheEntry.dependencies;
1603+
fileSizes = cacheEntry.fileSizes;
1604+
}
1605+
1606+
sb.AppendLine().AppendLine( j == 0 ? "Old Dependencies:" : "New Dependencies" );
1607+
for( int k = 0; k < dependencies.Length; k++ )
1608+
{
1609+
sb.Append( "- " ).Append( dependencies[k] );
1610+
1611+
if( Directory.Exists( dependencies[k] ) )
1612+
{
1613+
sb.Append( " (Dir)" );
1614+
if( fileSizes[k] != 0L )
1615+
sb.Append( " WasCachedAsFile: " ).Append( fileSizes[k] );
1616+
}
1617+
else
1618+
{
1619+
assetFile = new FileInfo( dependencies[k] );
1620+
sb.Append( " (File) " ).Append( "CachedSize: " ).Append( fileSizes[k] );
1621+
if( assetFile.Exists )
1622+
sb.Append( " RealSize: " ).Append( assetFile.Length );
1623+
else
1624+
sb.Append( " NoLongerExists" );
1625+
}
1626+
1627+
sb.AppendLine();
1628+
}
1629+
}
1630+
1631+
Debug.LogError( sb.ToString() );
1632+
return false;
1633+
}
1634+
15091635
cacheEntry.Refresh( assetPath );
15101636
cacheEntry.searchResult = CacheEntry.Result.Unknown;
1637+
lastRefreshedCacheEntry = cacheEntry;
15111638

15121639
return AssetHasAnyReference( assetPath );
15131640
}
@@ -1659,7 +1786,7 @@ private void LoadCache()
16591786
catch( Exception e )
16601787
{
16611788
assetDependencyCache = null;
1662-
Debug.LogException( e );
1789+
Debug.LogWarning( "Couldn't load cache (probably cache format has changed in an update), will regenerate cache.\n" + e.ToString() );
16631790
}
16641791
}
16651792
}

Plugins/AssetUsageDetector/Editor/AssetUsageDetectorWindow.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ private void OnGUI()
523523

524524
GUILayout.Space( 35 );
525525

526-
if( EditorGUILayout.ToggleLeft( "Shorter: Draw only the most relevant unique parts of the complete paths that start with a UnityEngine.Object", searchResultDrawParameters.pathDrawingMode == PathDrawingMode.ShortRelevantParts ) )
526+
if( EditorGUILayout.ToggleLeft( "Shorter: Draw only the most relevant parts (that start with a UnityEngine.Object) of the complete paths", searchResultDrawParameters.pathDrawingMode == PathDrawingMode.ShortRelevantParts ) )
527527
searchResultDrawParameters.pathDrawingMode = PathDrawingMode.ShortRelevantParts;
528528

529529
GUILayout.EndHorizontal();
@@ -532,7 +532,7 @@ private void OnGUI()
532532

533533
GUILayout.Space( 35 );
534534

535-
if( EditorGUILayout.ToggleLeft( "Shortest: Draw only the last two nodes of complete paths that are unique", searchResultDrawParameters.pathDrawingMode == PathDrawingMode.Shortest ) )
535+
if( EditorGUILayout.ToggleLeft( "Shortest: Draw only the last two nodes of complete paths", searchResultDrawParameters.pathDrawingMode == PathDrawingMode.Shortest ) )
536536
searchResultDrawParameters.pathDrawingMode = PathDrawingMode.Shortest;
537537

538538
GUILayout.EndHorizontal();

0 commit comments

Comments
 (0)