// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. #include "SizeMapModule.h" #include "STreeMap.h" #include "SSizeMap.h" #include "AssetRegistryModule.h" #include "AssetThumbnail.h" #include "ClassIconFinder.h" #include "UnitConversion.h" #define LOCTEXT_NAMESPACE "SizeMap" SSizeMap::SSizeMap() : TreeMapWidget( nullptr ), RootAssetPackageNames(), RootTreeMapNode( new FTreeMapNodeData() ), // @todo sizemap: Hard-coded thumbnail pool size. Not a big deal, but ideally move the constants elsewhere AssetThumbnailPool( new FAssetThumbnailPool(1024) ) { } SSizeMap::~SSizeMap() { if( AssetThumbnailPool.IsValid() ) { AssetThumbnailPool->ReleaseResources(); AssetThumbnailPool.Reset(); } } void SSizeMap::Construct( const FArguments& InArgs ) { ChildSlot [ SAssignNew( TreeMapWidget, STreeMap, RootTreeMapNode.ToSharedRef(), nullptr ) .OnTreeMapNodeDoubleClicked( this, &SSizeMap::OnTreeMapNodeDoubleClicked ) ]; } void SSizeMap::SetRootAssetPackageNames( const TArray& NewRootAssetPackageNames ) { RootAssetPackageNames = NewRootAssetPackageNames; FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); if ( AssetRegistryModule.Get().IsLoadingAssets() ) { // We are still discovering assets, listen for the completion delegate before building the graph if (!AssetRegistryModule.Get().OnFilesLoaded().IsBoundToObject(this)) { AssetRegistryModule.Get().OnFilesLoaded().AddSP( this, &SSizeMap::OnInitialAssetRegistrySearchComplete ); } } else { // All assets are already discovered, build the graph now. RefreshMap(); } } namespace SizeMapInternals { /** Serialization archive that discovers assets referenced by a specific Unreal object */ class FAssetReferenceFinder : public FArchiveUObject { public: FAssetReferenceFinder( UObject* Object ) : CurrentObject( Object ) { ArIsObjectReferenceCollector = true; ArIgnoreOuterRef = true; CurrentObject->Serialize( *this ); } FArchive& operator<<( UObject*& Object ) { // Only look at objects which are valid const bool bValidObject = Object && // Object should not be NULL !Object->HasAnyFlags( RF_Transient | RF_PendingKill ) && // Should not be transient or pending kill Object->HasAllFlags( RF_Public ) && // All assets should be public Object->GetOuter()->IsA( UPackage::StaticClass() ) && // Only want outer assets (these should be the only public assets, anyway) ( Cast( Object ) == nullptr ) && // skip actors ( Cast( Object ) == nullptr ) && // skip components ( Cast( Object ) == nullptr ); // skip UClasses if( bValidObject ) { // Ignore self referencing objects if( Object != CurrentObject ) { // ADD REFERENCE(CurrentObject DEPENDS on Object) ReferencedObjects.Add( Object ); } } return *this; } private: /** The object currently being serialized. */ UObject* CurrentObject; public: /** The set of referenced objects */ TSet< UObject* > ReferencedObjects; }; /** Given a size in bytes and a boolean that indicates whether the size is actually known to be correct, returns a pretty string to represent that size, such as "256.0 MB", or "unknown size" */ static FString MakeBestSizeString( const SIZE_T SizeInBytes, const bool bHasKnownSize ) { FString BestSizeString; const FNumericUnit BestUnit = FUnitConversion::QuantizeUnitsToBestFit( (double)SizeInBytes, EUnit::Bytes ); if( BestUnit.Units == EUnit::Bytes ) { // We ended up with bytes, so show a decimal number BestSizeString = FString::Printf( TEXT( "%s %s" ), *FText::AsNumber( static_cast(SizeInBytes) ).ToString(), *LOCTEXT( "Bytes", "bytes" ).ToString() ); } else { // Show a fractional number with the best possible units FNumberFormattingOptions NumberFormattingOptions; NumberFormattingOptions.MaximumFractionalDigits = 1; // @todo sizemap: We could make the number of digits customizable in the UI NumberFormattingOptions.MinimumFractionalDigits = 0; NumberFormattingOptions.MinimumIntegralDigits = 1; BestSizeString = FString::Printf( TEXT( "%s %s" ), *FText::AsNumber( BestUnit.Value, &NumberFormattingOptions ).ToString(), FUnitConversion::GetUnitDisplayString( BestUnit.Units ) ); } if( !bHasKnownSize ) { if( SizeInBytes == 0 ) { BestSizeString = LOCTEXT( "UnknownSize", "unknown size" ).ToString(); } else { BestSizeString = FString::Printf( TEXT( "%s %s" ), *LOCTEXT( "UnknownSizeButAtLeastThisBig", "at least" ).ToString(), *BestSizeString ); } } return BestSizeString; } } void SSizeMap::GatherDependenciesRecursively( FAssetRegistryModule& AssetRegistryModule, TSharedPtr& InAssetThumbnailPool, TMap>& VisitedAssetPackageNames, const TArray& AssetPackageNames, const TSharedPtr& Node, TSharedPtr& SharedRootNode, int32& NumAssetsWhichFailedToLoad ) { for( const FName AssetPackageName : AssetPackageNames ) { // Have we already added this asset to the tree? If so, we'll either move it to a "shared" group or (if its referenced again by the same // root-level asset) ignore it if( VisitedAssetPackageNames.Contains( AssetPackageName ) ) { // OK, we've determined that this asset has already been referenced by something else in our tree. We'll move it to a "shared" group // so all of the assets that are referenced in multiple places can be seen together. TSharedPtr ExistingNode = VisitedAssetPackageNames[ AssetPackageName ]; // Is the existing node not already under the "shared" group? Note that it might still be (indirectly) under // the "shared" group, in which case we'll still want to move it up to the root since we've figured out that it is // actually shared between multiple assets which themselves may be shared if( ExistingNode->Parent != SharedRootNode.Get() ) { // Don't bother moving any of the assets at the root level into a "shared" bucket. We're only trying to best // represent the memory used when all of the root-level assets have become loaded. It's OK if root-level assets // are referenced by other assets in the set -- we don't need to indicate they are shared explicitly FTreeMapNodeData* ExistingNodeParent = ExistingNode->Parent; check( ExistingNodeParent != nullptr ); const bool bExistingNodeIsAtRootLevel = ExistingNodeParent->Parent == nullptr; if( !bExistingNodeIsAtRootLevel ) { // OK, current asset (AssetPackageName) is definitely not a root level asset, but its already in the tree // somewhere as a non-shared, non-root level asset. We need to make sure that this Node's reference is not from the // same root-level asset as the ExistingNodeInTree. Otherwise, there's no need to move it to a 'shared' group. FTreeMapNodeData* MyParentNode = Node.Get(); check( MyParentNode != nullptr ); FTreeMapNodeData* MyRootLevelAssetNode = MyParentNode; while( MyRootLevelAssetNode->Parent != nullptr && MyRootLevelAssetNode->Parent->Parent != nullptr ) { MyRootLevelAssetNode = MyRootLevelAssetNode->Parent; } if( MyRootLevelAssetNode->Parent == nullptr ) { // No root asset (Node must be a root level asset itself!) MyRootLevelAssetNode = nullptr; } // Find the existing node's root level asset node FTreeMapNodeData* ExistingNodeRootLevelAssetNode = ExistingNodeParent; while( ExistingNodeRootLevelAssetNode->Parent->Parent != nullptr ) { ExistingNodeRootLevelAssetNode = ExistingNodeRootLevelAssetNode->Parent; } // If we're being referenced by another node within the same asset, no need to move it to a 'shared' group. if( MyRootLevelAssetNode != ExistingNodeRootLevelAssetNode ) { // This asset was already referenced by something else (or was in our top level list of assets to display sizes for) if( !SharedRootNode.IsValid() ) { // Find the root-most tree node FTreeMapNodeData* RootNode = MyParentNode; while( RootNode->Parent != nullptr ) { RootNode = RootNode->Parent; } SharedRootNode = MakeShareable( new FTreeMapNodeData() ); RootNode->Children.Add( SharedRootNode ); SharedRootNode->Parent = RootNode; // Keep back-pointer to parent node } // Reparent the node that we've now determined to be shared ExistingNode->Parent->Children.Remove( ExistingNode ); SharedRootNode->Children.Add( ExistingNode ); ExistingNode->Parent = SharedRootNode.Get(); } } } } else { // This asset is new to us so far! Let's add it to the tree. Later as we descend through references, we might find that the // asset is referenced by something else as well, in which case we'll pull it out and move it to a "shared" top-level box // Don't bother showing code references const FString AssetPackageNameString = AssetPackageName.ToString(); if( !AssetPackageNameString.StartsWith( TEXT( "/Script/" ) ) ) { FTreeMapNodeDataRef ChildTreeMapNode = MakeShareable( new FTreeMapNodeData() ); Node->Children.Add( ChildTreeMapNode ); ChildTreeMapNode->Parent = Node.Get(); // Keep back-pointer to parent node VisitedAssetPackageNames.Add( AssetPackageName, ChildTreeMapNode ); FNodeSizeMapData& NodeSizeMapData = NodeSizeMapDataMap.Add( ChildTreeMapNode ); // Set some defaults for this node. These will be used if we can't actually locate the asset. // @todo sizemap urgent: We need a better indication in the UI when there are one or more missing assets. Because missing assets have a size // of zero, they are nearly impossible to zoom into. At the least, we should have some Output Log spew when assets cannot be loaded NodeSizeMapData.AssetData.AssetName = AssetPackageName; NodeSizeMapData.AssetData.AssetClass = FName( *LOCTEXT( "MissingAsset", "MISSING!" ).ToString() ); NodeSizeMapData.AssetSize = 0; NodeSizeMapData.bHasKnownSize = false; // Find the asset using the asset registry // @todo sizemap urgent: Asset registry is faster but possibly not as exhaustive (no PostLoad created references, etc.) Maybe should be optional? // @todo sizemap urgent: When not using asset registry to find references, all references of map files are not showing up // @todo sizemap: We don't really need the asset registry given we need to load the objects to figure out their size, unless we make that AR-searchable. // ---> This would allow us to not have to wait for AR initialization. But if we made size AR-searchable, we could run very quickly for large data sets! // @todo sizemap urgent: When loading dependencies using the FAssetReferrenceFinder, we never find out about any missing references (unlike with the asset // registry approach, where we can tell when something is missing.) This could product misleading results! const bool bUseAssetRegistryForDependencies = true; const FString AssetPathString = AssetPackageNameString + TEXT(".") + FPackageName::GetLongPackageAssetName( AssetPackageNameString ); const FAssetData FoundAssetData = AssetRegistryModule.Get().GetAssetByObjectPath( FName( *AssetPathString ) ); if( FoundAssetData.IsValid() ) { NodeSizeMapData.AssetData = FoundAssetData; // Now actually load up the asset. We need it in memory in order to accurately determine its size. // @todo sizemap: We could async load these packages to make the editor experience a bit nicer (smoother progress) UObject* Asset = StaticLoadObject( UObject::StaticClass(), nullptr, *AssetPathString ); if( Asset != nullptr ) { TArray ReferencedAssetPackageNames; if( bUseAssetRegistryForDependencies ) { AssetRegistryModule.Get().GetDependencies( AssetPackageName, ReferencedAssetPackageNames ); } else { SizeMapInternals::FAssetReferenceFinder References( Asset ); for( UObject* Object : References.ReferencedObjects ) { ReferencedAssetPackageNames.Add( FName( *Object->GetOutermost()->GetPathName() ) ); } } NodeSizeMapData.AssetSize = Asset->GetResourceSize( EResourceSizeMode::Exclusive ); NodeSizeMapData.bHasKnownSize = ( NodeSizeMapData.AssetSize != UObject::RESOURCE_SIZE_NONE ); if( !NodeSizeMapData.bHasKnownSize ) { // Asset has no meaningful size NodeSizeMapData.AssetSize = 0; } // Now visit all of the assets that we are referencing GatherDependenciesRecursively( AssetRegistryModule, InAssetThumbnailPool, VisitedAssetPackageNames, ReferencedAssetPackageNames, ChildTreeMapNode, SharedRootNode, NumAssetsWhichFailedToLoad ); } else { ++NumAssetsWhichFailedToLoad; } } else { ++NumAssetsWhichFailedToLoad; } } } } } void SSizeMap::FinalizeNodesRecursively( TSharedPtr& Node, const TSharedPtr& SharedRootNode, int32& TotalAssetCount, SIZE_T& TotalSize, bool& bAnyUnknownSizes ) { // Process children first, so we can get the totals for the root node and shared nodes int32 SubtreeAssetCount = 0; SIZE_T SubtreeSize = 0; bool bAnyUnknownSizesInSubtree = false; { for( TSharedPtr ChildNode : Node->Children ) { FinalizeNodesRecursively( ChildNode, SharedRootNode, SubtreeAssetCount, SubtreeSize, bAnyUnknownSizesInSubtree ); } TotalAssetCount += SubtreeAssetCount; TotalSize += SubtreeSize; if( bAnyUnknownSizesInSubtree ) { bAnyUnknownSizes = true; } } if( Node == SharedRootNode ) { // @todo sizemap: Should we indicate in a non-shared parent node how many if its dependents ended up being in the "shared" bucket? Probably // not that important, because the user can choose to view that asset in isolation to see the full tree. Node->Name = FString::Printf( TEXT( "%s (%s)" ), *LOCTEXT( "SharedGroupName", "*SHARED*" ).ToString(), *SizeMapInternals::MakeBestSizeString( SubtreeSize, !bAnyUnknownSizes ) ); // Container nodes are always auto-sized Node->Size = 0.0f; } else if( Node->Parent == nullptr ) { // Tree root is always auto-sized Node->Size = 0.0f; } else { const FNodeSizeMapData& NodeSizeMapData = NodeSizeMapDataMap.FindChecked( Node.ToSharedRef() ); ++TotalAssetCount; TotalSize += NodeSizeMapData.AssetSize; if( !NodeSizeMapData.bHasKnownSize ) { bAnyUnknownSizes = true; } // Setup a thumbnail const FSlateBrush* DefaultThumbnailSlateBrush; { // For non-class types, use the default based upon the actual asset class // This has the side effect of not showing a class icon for assets that don't have a proper thumbnail image available bool bIsClassType = false; const UClass* ThumbnailClass = FClassIconFinder::GetIconClassForAssetData( NodeSizeMapData.AssetData, &bIsClassType ); const FName DefaultThumbnail = (bIsClassType) ? NAME_None : FName(*FString::Printf(TEXT("ClassThumbnail.%s"), *NodeSizeMapData.AssetData.AssetClass.ToString())); DefaultThumbnailSlateBrush = FClassIconFinder::FindThumbnailForClass(ThumbnailClass, DefaultThumbnail); // @todo sizemap urgent: Actually implement rendered thumbnail support, not just class-based background images // const int32 ThumbnailSize = 128; // @todo sizemap: Hard-coded thumbnail size. Move this elsewhere // TSharedRef AssetThumbnail( new FAssetThumbnail( NodeSizeMapData.AssetData, ThumbnailSize, ThumbnailSize, AssetThumbnailPool ) ); // ChildTreeMapNode->AssetThumbnail = AssetThumbnail->MakeThumbnailImage(); } if( Node->IsLeafNode() ) { Node->CenterText = SizeMapInternals::MakeBestSizeString( NodeSizeMapData.AssetSize, NodeSizeMapData.bHasKnownSize ); Node->Size = NodeSizeMapData.AssetSize; // The STreeMap widget is not expecting zero-sized leaf nodes. So we make them very small instead. if( Node->Size == 0 ) { Node->Size = 1; } // Leaf nodes get a background picture Node->BackgroundBrush = DefaultThumbnailSlateBrush; // "Asset name" // "Asset type" Node->Name = NodeSizeMapData.AssetData.AssetName.ToString(); Node->Name2 = NodeSizeMapData.AssetData.AssetClass.ToString(); } else { // Container nodes are always auto-sized Node->Size = 0.0f; const bool bNeedsSelfNode = NodeSizeMapData.AssetSize > 0; if( bNeedsSelfNode ) { // "Asset name" (size) Node->Name = FString::Printf( TEXT( "%s (%s)" ), *NodeSizeMapData.AssetData.AssetName.ToString(), *SizeMapInternals::MakeBestSizeString( SubtreeSize + NodeSizeMapData.AssetSize, !bAnyUnknownSizesInSubtree && NodeSizeMapData.bHasKnownSize ) ); // We have children, so make some space for our own asset's size within our box FTreeMapNodeDataRef ChildSelfTreeMapNode = MakeShareable( new FTreeMapNodeData() ); Node->Children.Add( ChildSelfTreeMapNode ); ChildSelfTreeMapNode->Parent = Node.Get(); // Keep back-pointer to parent node // Map the "self" node to the same node data as its parent NodeSizeMapDataMap.Add( ChildSelfTreeMapNode, NodeSizeMapData ); // "*SELF*" // "Asset type" ChildSelfTreeMapNode->Name = LOCTEXT( "SelfNodeLabel", "*SELF*" ).ToString(); ChildSelfTreeMapNode->Name2 = NodeSizeMapData.AssetData.AssetClass.ToString(); ChildSelfTreeMapNode->CenterText = SizeMapInternals::MakeBestSizeString( NodeSizeMapData.AssetSize, NodeSizeMapData.bHasKnownSize ); ChildSelfTreeMapNode->Size = NodeSizeMapData.AssetSize; // Leaf nodes get a background picture ChildSelfTreeMapNode->BackgroundBrush = DefaultThumbnailSlateBrush; } else { // "Asset name (asset type, size)" Node->Name = FString::Printf( TEXT( "%s (%s, %s)" ), *NodeSizeMapData.AssetData.AssetName.ToString(), *NodeSizeMapData.AssetData.AssetClass.ToString(), *SizeMapInternals::MakeBestSizeString( SubtreeSize + NodeSizeMapData.AssetSize, !bAnyUnknownSizesInSubtree && NodeSizeMapData.bHasKnownSize ) ); } } } // Sort all of my child nodes alphabetically. This is just so that we get deterministic results when viewing the // same sets of assets. Node->Children.StableSort( []( const FTreeMapNodeDataPtr& A, const FTreeMapNodeDataPtr& B ) { return A->Name < B->Name; } ); } void SSizeMap::RefreshMap() { // Wipe the current tree out RootTreeMapNode->Children.Empty(); NodeSizeMapDataMap.Empty(); // First, do a pass to gather asset dependencies and build up a tree TMap> VisitedAssetPackageNames; TSharedPtr SharedRootNode; int32 NumAssetsWhichFailedToLoad = 0; FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); GatherDependenciesRecursively( AssetRegistryModule, AssetThumbnailPool, VisitedAssetPackageNames, RootAssetPackageNames, RootTreeMapNode, SharedRootNode, NumAssetsWhichFailedToLoad ); // Next, do another pass over our tree to and count how big the assets are and to set the node labels. Also in this pass, we may // create some additional "self" nodes for assets that have children but also take up size themselves. int32 TotalAssetCount = 0; SIZE_T TotalSize = 0; bool bAnyUnknownSizes = false; FinalizeNodesRecursively( RootTreeMapNode, SharedRootNode, TotalAssetCount, TotalSize, bAnyUnknownSizes ); // Create a nice name for the tree! if( NumAssetsWhichFailedToLoad > 0 ) { RootTreeMapNode->Name = FString::Printf( TEXT( "%s %i %s" ), *LOCTEXT( "RootNode_WarningPrefix", "WARNING:" ).ToString(), NumAssetsWhichFailedToLoad, *LOCTEXT( "RootNode_NAssetsFailedToLoad", "assets were missing! Only partial results shown." ).ToString() ); } else if( RootAssetPackageNames.Num() == 1 && !SharedRootNode.IsValid() ) { // @todo sizemap: When zoomed right into one asset, can we use the Class color for the node instead of grey? // The root will only have one child, so go ahead and use that child as the actual root FTreeMapNodeDataPtr OnlyChild = RootTreeMapNode->Children[0]; OnlyChild->CopyNodeInto( *RootTreeMapNode ); RootTreeMapNode->Children = OnlyChild->Children; RootTreeMapNode->Parent = nullptr; for( const auto& ChildNode : RootTreeMapNode->Children ) { ChildNode->Parent = RootTreeMapNode.Get(); } // Use a more descriptive name for the root level node RootTreeMapNode->Name = FString::Printf( TEXT( "%s %s (%i %s)" ), *LOCTEXT( "RootNode_SizeMapForOneAsset", "Size map for" ).ToString(), *OnlyChild->Name, TotalAssetCount, *LOCTEXT( "RootNode_References", "total assets" ).ToString() ); } else { // Multiple assets (or at least some shared assets) at the root level RootTreeMapNode->BackgroundBrush = nullptr; RootTreeMapNode->Size = 0.0f; RootTreeMapNode->Parent = nullptr; RootTreeMapNode->Name = FString::Printf( TEXT( "%s %i %s (%i %s, %s)" ), *LOCTEXT( "RootNode_SizeMapForMultiple", "Size map for" ).ToString(), RootAssetPackageNames.Num(), *LOCTEXT( "RootNode_Assets", "assets" ).ToString(), TotalAssetCount, *LOCTEXT( "RootNode_References", "total assets" ).ToString(), *SizeMapInternals::MakeBestSizeString( TotalSize, !bAnyUnknownSizes ) ); } // OK, now refresh the actual tree map widget so our new tree will be displayed. const bool bShouldPlayTransition = false; TreeMapWidget->RebuildTreeMap( bShouldPlayTransition ); } void SSizeMap::OnInitialAssetRegistrySearchComplete() { RefreshMap(); } void SSizeMap::OnTreeMapNodeDoubleClicked( FTreeMapNodeData& TreeMapNodeData ) { const FNodeSizeMapData* NodeSizeMapData = NodeSizeMapDataMap.Find( TreeMapNodeData.AsShared() ); if( NodeSizeMapData != nullptr ) { TArray Assets; Assets.Add( NodeSizeMapData->AssetData ); GEditor->SyncBrowserToObjects( Assets ); } } // @todo sizemap urgent: We should add a spinner while we discover and load assets // @todo sizemap urgent: Audit common asset types and make sure they have a useful GetResourceSize() implementation // -> Some implementations are including the size of editor-only data (Static Mesh). This should be configurable! // @todo sizemap urgent: It would be great to be able to see 0-sized/unknown-sized/tiny-sized nodes somehow, or at least a count of them // - You'd almost want a way to zoom in super small. Or a little laserpointer effect that shows labels for super-tiny nodes // @todo sizemap urgent: Need inline help to figure out mousewheel? Single click currently does nothing. // @todo sizemap urgent: When an asset is not reporting a size, this tool should help to escalate that by making it very obvious that its missing (no GetResourceSize function) // @todo sizemap: Can "zoom into" nodes which is a bit weird. (Asset name no longer draws). Maybe disallow zooming all the way in? // --> Double-click current zooms directly into a single asset. Not super useful. Maybe only allow zooming down to the deepest nodes, not leaves // @todo sizemap: When the tab is restored from layout, it will be totally empty. Instead it should probably have instructions for how to show the sizes for assets. (Same with Reference Viewer) // @todo sizemap: We ideally want to replace the SReferenceTree code with SSizeMap // @todo sizemap: Add a tree view that shows all of the references along with their sizes (so you can see Very Small references) // @todo sizemap: It would be useful to be able to preview the sizes for specific platforms? (option in the UI) // @todo sizemap: Should we show the percentage of total size as an option in the UI (though, the size of the boxes show this pretty well.) // @todo sizemap: Should we show the folder part of the asset name as a tool-tip? // @todo sizemap: It might be nice to unload assets that were loaded by us after the size map is built up // @todo sizemap urgent: Trying size map for all engine content BROKE the window updating #undef LOCTEXT_NAMESPACE