// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; namespace Microsoft.MixedReality.GraphicsTools.Editor { /// /// The Dependency Window displays how assets reference and depend on each other. Dependencies are calculated by parsing GUIDs within /// project YAML files (note, script to script dependencies are not considered). /// /// The window displays a list of assets that the currently selected asset depends on, and a hierarchical list of assets that depend /// on it. If nothing depends on the currently selected asset, you can consider deleting it from your project (note that some assets /// are loaded programmatically via APIs like Shader.Find() and may not be caught by the dependency tracker). The window can also /// display just a list of all assets which are not referenced by any other assets and could be considered for deletion. /// public class DependencyWindow : EditorWindow { private int selectedToolbarIndex = 0; private Vector2 scrollPosition = Vector2.zero; // Asset Dependency Graph private Object assetSelection = null; private int maxDisplayDepth = 4; private float dependencyGraphRefreshTime = 0.0f; // Unreferenced Asset List private bool excludeUnityScenes = false; private bool excludePackageAssets = true; private class DependencyGraphNode { public string guid = null; public string path = null; public string extension = null; public Object targetObject = null; public HashSet assetsThisDependsOn = new HashSet(); public HashSet assetsDependentOnThis = new HashSet(); } private Dictionary dependencyGraph = new Dictionary(); private const string windowTitle = "Dependency Window"; private static readonly string[] toolbarTitles = { "Asset Dependency Graph", "Unreferenced Asset List" }; private const string guidPrefix = "guid: "; private const int guidCharacterCount = 32; private static readonly string nullGuid = new string('0', guidCharacterCount); private readonly string[] assetsWithDependencies = { ".unity", ".prefab", ".asset", ".mat", ".anim", ".controller", ".playable", }; private readonly string[] assetsWithMetaDependencies = { ".fbx", }; private readonly string[] assetsWhichCanBeUnreferenced = { ".cs", ".asmdef", ".rsp", ".targets", ".pfx", ".md", ".txt", ".nuspec", ".yml", ".json", ".pdf", ".cginc", ".hlsl" }; [MenuItem("Window/Graphics Tools/Dependency Window")] private static void ShowWindow() { var window = GetWindow(typeof(SceneView)); window.titleContent = new GUIContent(windowTitle, EditorGUIUtility.IconContent("d_EditCollider").image); window.minSize = new Vector2(580.0f, 380.0f); window.RefreshDependencyGraph(); window.Show(); } private void OnGUI() { // Don't allow the dependency window to be used when the editor is playing. if (EditorApplication.isPlaying || EditorApplication.isPaused) { GUI.enabled = false; } DrawHeader(); DrawVerifyAssetSerializationMode(); DrawDependencyGraphStatistics(); int previousSelectedToolbarIndex = selectedToolbarIndex; selectedToolbarIndex = GUILayout.Toolbar(selectedToolbarIndex, toolbarTitles); if (previousSelectedToolbarIndex != selectedToolbarIndex) { scrollPosition = Vector2.zero; } EditorGUILayout.Space(); if (selectedToolbarIndex == 0) { assetSelection = EditorGUILayout.ObjectField("Asset Selection", assetSelection, typeof(Object), false); maxDisplayDepth = EditorGUILayout.IntSlider("Max Display Depth", maxDisplayDepth, 1, 32); DrawDependencyGraphForSelection(); } else if (selectedToolbarIndex == 1) { excludeUnityScenes = EditorGUILayout.Toggle("Exclude Unity Scenes", excludeUnityScenes); excludePackageAssets = EditorGUILayout.Toggle("Exclude Package Assets", excludePackageAssets); string tooltip = "Although certain asset types may not be directly referenced by other assets as tracked via Unity meta files, these assets may be utilized and/or necessary to a project in other ways.\n\nThus, this list of asset extensions are ignored and always excluded in the list below.\n\n"; foreach (string extension in assetsWhichCanBeUnreferenced) { tooltip += extension + "\n"; } EditorGUILayout.LabelField(new GUIContent("Some asset types are always excluded", EditorGUIUtility.IconContent("_Help").image, tooltip)); DrawUnreferencedAssetList(); } } private void OnSelectionChange() { if (Selection.objects.Length != 0) { if (IsAsset(AssetDatabase.GetAssetPath(Selection.objects[0]))) { assetSelection = Selection.objects[0]; Repaint(); } } } private static void DrawHeader() { EditorGUILayout.LabelField("This tool displays how assets reference and depend on each other. Dependencies are calculated by parsing guids within project YAML files, code dependencies are not considered.", EditorStyles.wordWrappedLabel); EditorGUILayout.Space(); } private static void DrawVerifyAssetSerializationMode() { if (EditorSettings.serializationMode != SerializationMode.ForceText) { using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.HelpBox("Dependencies can only be tracked with text assets. Please change the project serialization mode to \"Force Text\"", MessageType.Error); if (GUILayout.Button("Fix Now")) { EditorSettings.serializationMode = SerializationMode.ForceText; } } } } private void DrawDependencyGraphStatistics() { using (new EditorGUILayout.HorizontalScope()) { if (dependencyGraph.Count == 0) { GUILayout.Label(string.Format("The dependency graph contains 0 assets, please refresh the dependency graph.", dependencyGraph.Count), EditorStyles.boldLabel); } else { GUILayout.Label(string.Format("The dependency graph contains {0:n0} assets and took {1:0.00} seconds to build.", dependencyGraph.Count, dependencyGraphRefreshTime)); } if (GUILayout.Button("Refresh")) { RefreshDependencyGraph(); } } EditorGUILayout.Space(); if (GUI.enabled) { GUI.enabled = dependencyGraph.Count != 0; } } private void DrawDependencyGraphForSelection() { EditorGUILayout.Space(); EditorGUILayout.BeginVertical("Box"); { EditorGUILayout.LabelField("Dependency Graph", EditorStyles.boldLabel); EditorGUILayout.LabelField(string.Empty, GUI.skin.horizontalSlider); var file = AssetDatabase.GetAssetPath(assetSelection); DependencyGraphNode node; if (dependencyGraph.TryGetValue(AssetDatabase.AssetPathToGUID(file), out node)) { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); { EditorGUILayout.LabelField("This asset depends on:", EditorStyles.boldLabel); if (node.assetsThisDependsOn.Count != 0) { foreach (var dependency in node.assetsThisDependsOn) { DrawDependencyGraphNode(dependency); } } else { EditorGUILayout.HelpBox("Nothing.", MessageType.Info); } EditorGUILayout.LabelField(string.Empty, GUI.skin.horizontalSlider); EditorGUILayout.LabelField("Assets that depend on this:", EditorStyles.boldLabel); if (node.assetsDependentOnThis.Count != 0) { foreach (var dependency in node.assetsDependentOnThis) { DrawDependencyGraphNodeRecurse(dependency, 0, maxDisplayDepth); } } else { EditorGUILayout.HelpBox("Nothing, you could consider deleting this asset if it isn't referenced programmatically.", MessageType.Warning); } } EditorGUILayout.EndScrollView(); } else { if (IsAsset(file)) { EditorGUILayout.HelpBox(string.Format("Failed to find data for {0} try refreshing the dependency graph.", file), MessageType.Warning); } else { EditorGUILayout.HelpBox("Please select an asset above to see the dependency graph.", MessageType.Info); } } } EditorGUILayout.EndVertical(); } private void DrawUnreferencedAssetList() { EditorGUILayout.Space(); EditorGUILayout.BeginVertical("Box"); { EditorGUILayout.LabelField("Unreferenced Asset List", EditorStyles.boldLabel); var unreferencedAssetCount = 0; scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); { foreach (var kvp in dependencyGraph) { var node = kvp.Value; if (node.assetsDependentOnThis.Count != 0) { continue; } if (DoesNodeHaveExtension(node, assetsWhichCanBeUnreferenced)) { continue; } if (excludeUnityScenes && DoesNodeHaveExtension(node, ".unity")) { continue; } if (excludePackageAssets && IsInPackages(node)) { continue; } DrawDependencyGraphNode(kvp.Value); ++unreferencedAssetCount; } } EditorGUILayout.EndScrollView(); EditorGUILayout.LabelField(string.Format("{0:n0} assets are not referenced by any other asset.", unreferencedAssetCount)); } EditorGUILayout.EndVertical(); } private void RefreshDependencyGraph() { var beginTime = DateTime.UtcNow; dependencyGraph.Clear(); // Get all meta files from the assets and package cache directories. const string metaExtension = ".meta"; var metaFiles = new List(); metaFiles.AddRange(Directory.GetFiles(Application.dataPath, "*" + metaExtension, SearchOption.AllDirectories)); metaFiles.AddRange(Directory.GetFiles(Path.Combine(Directory.GetParent(Application.dataPath).FullName, "Library\\PackageCache"), "*" + metaExtension, SearchOption.AllDirectories)); metaFiles.AddRange(Directory.GetFiles(Path.Combine(Directory.GetParent(Application.dataPath).FullName, "Packages"), "*" + metaExtension, SearchOption.AllDirectories)); for (int i = 0; i < metaFiles.Count; ++i) { var progress = (float)i / metaFiles.Count; if (EditorUtility.DisplayCancelableProgressBar(windowTitle, "Building dependency graph...", progress)) { break; } var metaFile = metaFiles[i]; var file = metaFile.Substring(0, metaFile.Length - metaExtension.Length); if (!IsAsset(file)) { continue; } string guid = GetGuidFromMeta(metaFile); if (!IsGuidValid(guid)) { continue; } var node = EnsureNode(guid); List dependencies = new List(); // Check if this asset can have dependencies. if (DoesNodeHaveExtension(node, assetsWithDependencies)) { dependencies.AddRange(GetDependenciesInFile(file)); } // Some files have dependencies in their meta files. if (DoesNodeHaveExtension(node, assetsWithMetaDependencies)) { dependencies.AddRange(GetDependenciesInFile(metaFile)); } // Add linkage between this node and its dependencies and the dependency and this node. foreach (var dependency in dependencies) { node.assetsThisDependsOn.Add(EnsureNode(dependency)); EnsureNode(dependency).assetsDependentOnThis.Add(node); } } EditorUtility.ClearProgressBar(); dependencyGraphRefreshTime = (float)((DateTime.UtcNow - beginTime).TotalSeconds); } private DependencyGraphNode EnsureNode(string guid) { DependencyGraphNode node; if (!dependencyGraph.TryGetValue(guid, out node)) { node = new DependencyGraphNode(); node.guid = guid; node.path = AssetDatabase.GUIDToAssetPath(guid); node.extension = Path.GetExtension(node.path).ToLowerInvariant(); node.targetObject = AssetDatabase.LoadMainAssetAtPath(node.path); dependencyGraph.Add(guid, node); } return node; } private static bool IsAsset(string file) { return File.Exists(file); } private static bool DoesNodeHaveExtension(DependencyGraphNode node, string targetExtension) { if (!string.IsNullOrEmpty(node.extension)) { return targetExtension.Equals(node.extension); } return false; } private static bool DoesNodeHaveExtension(DependencyGraphNode node, string[] targetExtensions) { if (!string.IsNullOrEmpty(node.extension)) { return Array.IndexOf(targetExtensions, node.extension) >= 0; } return false; } private static bool IsInPackages(DependencyGraphNode node) { if (!string.IsNullOrEmpty(node.path)) { return node.path.StartsWith("Packages"); } return false; } private static bool IsGuidValid(string guid) { return !string.IsNullOrEmpty(guid) && guid != nullGuid && !string.IsNullOrEmpty(Path.GetExtension(AssetDatabase.GUIDToAssetPath(guid))); } private static string GetGuidFromMeta(string file) { try { string[] lines = File.ReadAllLines(file); foreach (var line in lines) { if (line.StartsWith(guidPrefix)) { return line.Split(' ')[1]; } } } catch (Exception e) { Debug.LogException(e); } return null; } private static List GetDependenciesInFile(string file) { var guids = new List(); try { foreach (var line in File.ReadAllLines(file)) { var index = line.IndexOf(guidPrefix); if (index > 0) { var guid = line.Substring(index + guidPrefix.Length, guidCharacterCount); if (IsGuidValid(guid) && !guids.Contains(guid)) { guids.Add(guid); } } } } catch (Exception e) { Debug.LogException(e); } return guids; } private static void DrawDependencyGraphNode(DependencyGraphNode node) { DrawDependencyGraphNode(node, 0, int.MaxValue); } private static void DrawDependencyGraphNode(DependencyGraphNode node, int depth, int maxDepth) { using (new EditorGUILayout.HorizontalScope()) { GUILayout.Space(depth * 8); if (depth != maxDepth) { if (string.IsNullOrEmpty(node.path)) { EditorGUILayout.LabelField(string.Format("Missing Asset: {0}", node.guid)); } else { EditorGUILayout.ObjectField(string.Empty, node.targetObject, typeof(Object), false); } } else { EditorGUILayout.LabelField("Max display depth was exceeded..."); } } } private static void DrawDependencyGraphNodeRecurse(DependencyGraphNode node, int depth, int maxDepth) { // To avoid infinite recursion with circular dependencies, stop at a max depth. if (depth != maxDepth) { DrawDependencyGraphNode(node, depth, maxDepth); foreach (var dependency in node.assetsDependentOnThis) { DrawDependencyGraphNodeRecurse(dependency, depth + 1, maxDepth); // Only draw the first child at max depth. if ((depth + 1) == maxDepth) { break; } } } else { DrawDependencyGraphNode(null, depth, maxDepth); } } } }