#if UNITY_2022_2_OR_NEWER using System; using System.Collections.Generic; using UnityEditor.AddressableAssets.Build.Layout; using UnityEditor.Build.Reporting; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using BuildLayout = UnityEditor.AddressableAssets.Build.Layout.BuildLayout; namespace UnityEditor.AddressableAssets.BuildReportVisualizer { // Nested class that represents a generic item in the bundle view (can be an asset, header, or bundle). class BundlesViewBuildReportItem : IAddressablesBuildReportItem { public string Name { get; protected set; } public ulong FileSize { get; protected set; } public int RefsTo { get; protected set; } public int RefsBy { get; protected set; } public ulong FileSizeUncompressed { get; set; } public ulong FileSizePlusRefs { get; set; } public ulong FileSizeBundle { get; set; } public int AssetsCount { get; set; } public string GroupName { get; protected set; } public virtual void CreateGUI(VisualElement rootVisualElement) { } public virtual string GetCellContent(string colName) { if (colName == BuildReportUtility.BundlesContentViewColBundleName) return Name; else if (colName == BuildReportUtility.BundlesContentViewColSizePlusRefs) { if (FileSizePlusRefs == 0) return "--"; return BuildReportUtility.GetDenominatedBytesString(FileSizePlusRefs); } if (colName == BuildReportUtility.BundlesContentViewColSizeUncompressed) { if (FileSizeUncompressed == 0) return "--"; return BuildReportUtility.GetDenominatedBytesString(FileSizeUncompressed); } else if (colName == BuildReportUtility.BundlesContentViewBundleSize) { if (FileSizeBundle == 0) return "--"; return BuildReportUtility.GetDenominatedBytesString(FileSizeBundle); } else if (colName == BuildReportUtility.BundlesContentViewColRefsTo) { if (RefsTo == -1) return "--"; return RefsTo.ToString(); } else if (colName == BuildReportUtility.BundlesContentViewColRefsBy) { if (RefsBy == -1) return "--"; return RefsBy.ToString(); } return ""; } public string GetSortContent(string colName) { if (colName == BuildReportUtility.BundlesContentViewColSizePlusRefs) return FileSizePlusRefs.ToString(); if (colName == BuildReportUtility.BundlesContentViewColSizeUncompressed) return FileSizeUncompressed.ToString(); if (colName == BuildReportUtility.BundlesContentViewBundleSize) return FileSizeBundle.ToString(); return GetCellContent(colName); } } // Nested class that represents an bundle. class BundlesViewBuildReportBundle : BundlesViewBuildReportItem, IAddressablesBuildReportBundle { public BuildLayout.Bundle Bundle { get; set; } public List<BuildLayout.Bundle> Dependencies { get; set; } public List<BuildLayout.Bundle> DependentBundles { get; set; } public BundlesViewBuildReportBundle(BuildLayout.Bundle bundle) { Bundle = bundle; Name = bundle.Name; foreach (var file in bundle.Files) RefsTo += file.Assets.Count + file.OtherAssets.Count; RefsBy = bundle.DependentBundles.Count; GroupName = bundle.Group?.Name; Dependencies = bundle.Dependencies; DependentBundles = bundle.DependentBundles; foreach (var f in bundle.Files) { foreach (var ex in f.Assets) ExplicitAssetsFromLayout.Add(ex); } FileSizeBundle = bundle.FileSize; FileSizeUncompressed = bundle.UncompressedFileSize; FileSizePlusRefs = bundle.FileSize + bundle.ExpandedDependencyFileSize + bundle.DependencyFileSize; } public List<BuildLayout.ExplicitAsset> ExplicitAssetsFromLayout = new List<BuildLayout.ExplicitAsset>(); public List<BuildLayout.ExplicitAsset> ExplicitAssets { get; set; } public List<BuildLayout.DataFromOtherAsset> ImplicitAssets { get; set; } } class BundlesViewBuildReportAsset : BundlesViewBuildReportItem, IAddressablesBuildReportAsset { public BuildLayout.ExplicitAsset ExplicitAsset { get; } public BuildLayout.DataFromOtherAsset DataFromOtherAsset { get; } public List<BuildLayout.Bundle> Bundles { get;} public string AssetPath { get; private set; } public ulong SizeWDependencies { get; set; } public bool IsAddressable = false; public BundlesViewBuildReportAsset(BuildLayout.ExplicitAsset asset) { ExplicitAsset = asset; Bundles = new List<BuildLayout.Bundle>(){ asset.Bundle }; Name = asset.AddressableName; RefsTo = asset.InternalReferencedOtherAssets.Count + asset.ExternallyReferencedAssets.Count + asset.InternalReferencedExplicitAssets.Count; RefsBy = asset.ReferencingAssets != null ? asset.ReferencingAssets.Count : -1; FileSize = asset.SerializedSize + asset.StreamedSize; AssetPath = asset.AssetPath; IsAddressable = true; FileSizeUncompressed = asset.SerializedSize + asset.StreamedSize; FileSizePlusRefs = FileSizeUncompressed; foreach (var r in asset.ExternallyReferencedAssets) if (r != null) FileSizePlusRefs += r.SerializedSize + r.StreamedSize; foreach (var r in asset.InternalReferencedExplicitAssets) if (r != null) FileSizePlusRefs += r.SerializedSize + r.StreamedSize; SizeWDependencies = FileSizePlusRefs; } public BundlesViewBuildReportAsset(BuildLayout.DataFromOtherAsset asset) { DataFromOtherAsset = asset; Bundles = new List<BuildLayout.Bundle>() { asset.File.Bundle }; Name = asset.AssetPath; AssetPath = asset.AssetPath; RefsBy = asset.ReferencingAssets.Count; RefsTo = -1; IsAddressable = false; FileSizeUncompressed = asset.SerializedSize + asset.StreamedSize; FileSizePlusRefs = asset.SerializedSize + asset.StreamedSize; SizeWDependencies = FileSizePlusRefs; FileSizeBundle = 0; } } class BundlesViewBuildReportUnrelatedAssets : BundlesViewBuildReportItem { public BundlesViewBuildReportUnrelatedAssets(ulong assetSize, int assetCount) { Name = $"({assetCount} unrelated assets)"; FileSizeUncompressed = assetSize; } } class BundlesViewBuildReportIndirectlyReferencedBundles : BundlesViewBuildReportItem { public BundlesViewBuildReportIndirectlyReferencedBundles(List<BuildLayout.Bundle> bundles) { Name = bundles.Count > 1 ? $"{bundles.Count} indirectly referenced bundles" : $"{bundles.Count} indirectly referenced bundle"; FileSizeBundle = 0; FileSizeUncompressed = 0; HashSet<BuildLayout.Bundle> countedBundles = new HashSet<BuildLayout.Bundle>(); foreach (var b in bundles) { FileSizeBundle += b.FileSize; FileSizeUncompressed += b.UncompressedFileSize; if (!countedBundles.Contains(b)) { FileSizePlusRefs += b.FileSize; countedBundles.Add(b); } foreach (var depB in b.ExpandedDependencies) { if (!countedBundles.Contains(depB)) { FileSizePlusRefs += depB.FileSize; countedBundles.Add(depB); } } foreach (var depB in b.Dependencies) { if (!countedBundles.Contains(depB)) { FileSizePlusRefs += depB.FileSize; countedBundles.Add(depB); } } } } } class BundlesContentView : ContentView { IList<TreeViewItemData<BundlesViewBuildReportItem>> m_TreeRoots; public BundlesContentView(BuildReportHelperConsumer helperConsumer, DetailsView detailsView) : base(helperConsumer, detailsView) { } internal override ContentViewColumnData[] ColumnDataForView { get { return new ContentViewColumnData[] { new ContentViewColumnData(BuildReportUtility.BundlesContentViewColBundleName, this, true, "Bundle Name"), new ContentViewColumnData(BuildReportUtility.BundlesContentViewColSizePlusRefs, this, false, "Total Size (+ refs)"), new ContentViewColumnData(BuildReportUtility.BundlesContentViewColSizeUncompressed, this, false, "Uncompressed Size"), new ContentViewColumnData(BuildReportUtility.BundlesContentViewBundleSize, this, false, "Bundle File Size"), new ContentViewColumnData(BuildReportUtility.BundlesContentViewColRefsTo, this, false, "Refs To"), new ContentViewColumnData(BuildReportUtility.BundlesContentViewColRefsBy, this, false, "Refs By"), }; } } // Data about bundles from our currently selected build report. public override IList<IAddressablesBuildReportItem> CreateTreeViewItems(BuildLayout report) { List<IAddressablesBuildReportItem> buildReportBundles = new List<IAddressablesBuildReportItem>(); if (report == null) return buildReportBundles; foreach (BuildLayout.Bundle bundle in BuildLayoutHelpers.EnumerateBundles(report)) { var buildReportBundle = new BundlesViewBuildReportBundle(bundle); var explicitAssets = new List<BuildLayout.ExplicitAsset>(); var implicitAssets = new List<BuildLayout.DataFromOtherAsset>(); foreach (BuildLayout.File file in bundle.Files) { foreach (BuildLayout.ExplicitAsset asset in file.Assets) { explicitAssets.Add(asset); } foreach (BuildLayout.DataFromOtherAsset asset in file.OtherAssets) { implicitAssets.Add(asset); } } buildReportBundle.AssetsCount = explicitAssets.Count + implicitAssets.Count; buildReportBundle.ExplicitAssets = explicitAssets; buildReportBundle.ImplicitAssets = implicitAssets; buildReportBundles.Add(buildReportBundle); } return buildReportBundles; } public override void Consume(BuildLayout buildReport) { if (buildReport == null) return; m_DataHashtoReportItem = new Dictionary<Hash128, TreeDataReportItem>(); m_Report = buildReport; m_TreeItems = CreateTreeViewItems(m_Report); IList<TreeViewItemData<BundlesViewBuildReportItem>> treeRoots = CreateTreeRootsNestedList(m_TreeItems, m_DataHashtoReportItem); m_TreeView.SetRootItems(treeRoots); m_TreeView.Rebuild(); m_TreeView.columnSortingChanged += ColumnSortingChanged; } private void ColumnSortingChanged() { var columnList = m_TreeView.sortedColumns; IList<IAddressablesBuildReportItem> sortedRootList = new List<IAddressablesBuildReportItem>(); foreach (var col in columnList) { sortedRootList = SortByColumnDescription(col); } m_TreeView.SetRootItems(CreateTreeRootsNestedList(sortedRootList, m_DataHashtoReportItem)); m_TreeView.Rebuild(); } public override void CreateGUI(VisualElement rootVisualElement) { VisualElement view = rootVisualElement.Q<VisualElement>(BuildReportUtility.ContentView); TreeBuilder tb = new TreeBuilder() .With(ColumnDataForView) .With((items) => ItemsSelected.Invoke(items)); m_TreeView = tb.Build(); view.Add(m_TreeView); SetCallbacksForColumns(m_TreeView.columns, ColumnDataForView); m_SearchField = rootVisualElement.Q<ToolbarSearchField>(BuildReportUtility.SearchField); m_SearchField.RegisterValueChangedCallback(OnSearchValueChanged); m_SearchValue = m_SearchField.value; } private void OnSearchValueChanged(ChangeEvent<string> evt) { if (m_TreeItems == null) return; m_SearchValue = evt.newValue.ToLowerInvariant(); m_TreeRoots = CreateTreeRootsNestedList(m_TreeItems, m_DataHashtoReportItem); m_TreeView.SetRootItems(m_TreeRoots); m_TreeView.Rebuild(); } // Expresses bundle data as a hierarchal list of BuildReportBundleViewItem objects. public IList<TreeViewItemData<BundlesViewBuildReportItem>> CreateTreeRootsNestedList(IList<IAddressablesBuildReportItem> items, Dictionary<Hash128, TreeDataReportItem> dataHashToReportItem) { int id = 0; var roots = new List<TreeViewItemData<BundlesViewBuildReportItem>>(); foreach (BundlesViewBuildReportItem item in items) { BundlesViewBuildReportBundle bundle = item as BundlesViewBuildReportBundle; if (bundle == null) continue; bool includeAllDependencies = EntryAppearsInSearch(item, m_SearchValue); var children = CreateChildrenOfBundle(bundle, ref id, includeAllDependencies); if (children.Count > 0 || includeAllDependencies) { var rootItem = new TreeViewItemData<BundlesViewBuildReportItem>(++id, item, children); dataHashToReportItem.TryAdd(BuildReportUtility.ComputeDataHash(item.Name), new TreeDataReportItem(id, rootItem.data)); roots.Add(rootItem); } } return roots; } private List<TreeViewItemData<BundlesViewBuildReportItem>> CreateChildrenOfBundle(BundlesViewBuildReportBundle bundle, ref int id, bool includeAllDependencies) { var children = new List<TreeViewItemData<BundlesViewBuildReportItem>>(); var indirectlyReferencedBundleReportItems = new List<TreeViewItemData<BundlesViewBuildReportItem>>(); var indirectlyReferencedBundles = new List<BuildLayout.Bundle>(); CreateAssetEntries(children, out var bundlesReferencedByAssetEntries, bundle, ref id, includeAllDependencies); foreach (var depBundle in bundle.Bundle.ExpandedDependencies) { if (!bundlesReferencedByAssetEntries.Contains(depBundle)) { var reportBundle = new BundlesViewBuildReportBundle(depBundle); if (includeAllDependencies || EntryAppearsInSearch(reportBundle, m_SearchValue)) { indirectlyReferencedBundleReportItems.Add(new TreeViewItemData<BundlesViewBuildReportItem>(++id, reportBundle)); indirectlyReferencedBundles.Add(depBundle); } } } if (indirectlyReferencedBundles.Count > 0) { children.Add(new TreeViewItemData<BundlesViewBuildReportItem>(++id, new BundlesViewBuildReportIndirectlyReferencedBundles(indirectlyReferencedBundles), indirectlyReferencedBundleReportItems)); } return children; } private void CreateAssetEntries(List<TreeViewItemData<BundlesViewBuildReportItem>> children, out HashSet<BuildLayout.Bundle> directlyReferencedBundles, BundlesViewBuildReportBundle bundle, ref int id, bool includeAllDependencies) { directlyReferencedBundles = new HashSet<BuildLayout.Bundle>(); foreach (var asset in bundle.ExplicitAssets) { if (asset == null) continue; var reportAsset = new BundlesViewBuildReportAsset(asset); bool includeAsset = EntryAppearsInSearch(reportAsset, m_SearchValue); var childrenOfAsset = GenerateChildrenOfAsset(asset, ref id, directlyReferencedBundles, includeAllDependencies || includeAsset); if (includeAsset || includeAllDependencies || childrenOfAsset.Count > 0) { var dataItem = new TreeViewItemData<BundlesViewBuildReportItem>(++id, reportAsset, childrenOfAsset); m_DataHashtoReportItem.TryAdd(BuildReportUtility.ComputeDataHash(bundle.Name, asset.AddressableName), new TreeDataReportItem(id, dataItem.data)); children.Add(dataItem); } } foreach (var asset in bundle.ImplicitAssets) { if (asset == null) continue; var reportAsset = new BundlesViewBuildReportAsset(asset); if (EntryAppearsInSearch(reportAsset, m_SearchValue) || includeAllDependencies) { children.Add(new TreeViewItemData<BundlesViewBuildReportItem>(++id, reportAsset)); } } } private List<TreeViewItemData<BundlesViewBuildReportItem>> GenerateChildrenOfAsset(BuildLayout.ExplicitAsset asset, ref int id, HashSet<BuildLayout.Bundle> referencedBundles, bool includeAllDependencies) { var childrenOfAsset = new List<TreeViewItemData<BundlesViewBuildReportItem>>(); Dictionary<BuildLayout.Bundle, List<BuildLayout.ExplicitAsset>> bundleToAssetList = new Dictionary<BuildLayout.Bundle, List<BuildLayout.ExplicitAsset>>(); foreach (var dep in asset.InternalReferencedExplicitAssets) { var reportAsset = new BundlesViewBuildReportAsset(dep); if (EntryAppearsInSearch(reportAsset, m_SearchValue) || includeAllDependencies) { childrenOfAsset.Add(new TreeViewItemData<BundlesViewBuildReportItem>(++id, reportAsset)); } } foreach (var dep in asset.ExternallyReferencedAssets) { if (!bundleToAssetList.ContainsKey(dep.Bundle)) { bundleToAssetList.Add(dep.Bundle, new List<BuildLayout.ExplicitAsset>()); referencedBundles.Add(dep.Bundle); } bundleToAssetList[dep.Bundle].Add(dep); } foreach (var bundle in bundleToAssetList.Keys) { var reportBundle = new BundlesViewBuildReportBundle(bundle); bool bundleIncludedInSearch = EntryAppearsInSearch(reportBundle, m_SearchValue) || includeAllDependencies; var assetTreeViewItems = new List<TreeViewItemData<BundlesViewBuildReportItem>>(); var assetList = bundleToAssetList[bundle]; ulong unrelatedAssetSize = bundle.FileSize; foreach (var bundleAsset in assetList) { var reportBundleAsset = new BundlesViewBuildReportAsset(bundleAsset); if (bundleIncludedInSearch || EntryAppearsInSearch(reportBundleAsset, m_SearchValue)) { assetTreeViewItems.Add(new TreeViewItemData<BundlesViewBuildReportItem>(++id, reportBundleAsset)); unrelatedAssetSize -= bundleAsset.SerializedSize + bundleAsset.StreamedSize; } } int unrelatedAssetCount = bundle.AssetCount - assetList.Count; if (unrelatedAssetCount > 0 && bundleIncludedInSearch) assetTreeViewItems.Add(new TreeViewItemData<BundlesViewBuildReportItem>(++id, new BundlesViewBuildReportUnrelatedAssets(unrelatedAssetSize, unrelatedAssetCount))); if (bundleIncludedInSearch || assetTreeViewItems.Count > 0) { childrenOfAsset.Add(new TreeViewItemData<BundlesViewBuildReportItem>(++id, reportBundle, assetTreeViewItems)); } } foreach (var dep in asset.InternalReferencedOtherAssets) { var reportAsset = new BundlesViewBuildReportAsset(dep); if (EntryAppearsInSearch(reportAsset, m_SearchValue) || includeAllDependencies) { childrenOfAsset.Add(new TreeViewItemData<BundlesViewBuildReportItem>(++id, reportAsset)); } } return childrenOfAsset; } } } #endif