// Copyright Epic Games, Inc. All Rights Reserved. #include "SourceControlAssetDataCache.h" #include "Async/Async.h" #include "AssetRegistry/AssetData.h" #include "AssetRegistry/AssetRegistryModule.h" #include "HAL/IConsoleManager.h" #include "Misc/CoreDelegates.h" #include "AssetRegistry/IAssetRegistry.h" #include "ISourceControlModule.h" #include "ISourceControlState.h" #include "SourceControlHelpers.h" #include "SourceControlOperations.h" #if WITH_EDITOR #include "GameFramework/Actor.h" #endif static TAutoConsoleVariable CVarSourceControlAssetDataCacheMaxAsyncTask( TEXT("SourceControlAssetDataCache.MaxAsyncTask"), 8, TEXT("Maximum number of task running in parallel to fetch AssetData information.") ); void FSourceControlAssetDataCache::Startup() { ISourceControlModule& SCCModule = ISourceControlModule::Get(); ProviderChangedDelegateHandle = SCCModule.RegisterProviderChanged(FSourceControlProviderChanged::FDelegate::CreateLambda([](ISourceControlProvider& OldProvider, ISourceControlProvider& NewProvider) { ISourceControlModule::Get().GetAssetDataCache().OnSourceControlProviderChanged(OldProvider, NewProvider); })); #if WITH_EDITOR auto ClearCachedAssetData = [](const UObject* Object) { // Transient packages should not be saved, therefore we can skip the asset data cache update for them to mitigate the perf impact of this call if (UPackage* Package = Object->GetPackage(); Package && !Package->HasAnyFlags(RF_Transient)) { FString Filename = USourceControlHelpers::PackageFilename(Package); ISourceControlModule::Get().GetAssetDataCache().ClearAssetData(Filename); } }; ActorLabelChangedDelegateHandle = FCoreDelegates::OnActorLabelChanged.AddLambda(ClearCachedAssetData); FCoreDelegates::OnAllModuleLoadingPhasesComplete.AddLambda([this, &ClearCachedAssetData]() { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(FName("AssetRegistry")); AssetUpdatedDelegateHandle = AssetRegistryModule.Get().OnAssetUpdated().AddLambda([&ClearCachedAssetData](const FAssetData& AssetData) { if (UObject* Object = AssetData.FastGetAsset()) { ClearCachedAssetData(Object); } }); }); #endif bIsSourceControlDialogShown = false; } void FSourceControlAssetDataCache::Shutdown() { #if WITH_EDITOR FCoreDelegates::OnActorLabelChanged.Remove(ActorLabelChangedDelegateHandle); if (FAssetRegistryModule* AssetRegistryModule = FModuleManager::GetModulePtr(TEXT("AssetRegistry"))) { AssetRegistryModule->Get().OnAssetUpdated().Remove(AssetUpdatedDelegateHandle); } #endif ISourceControlModule& SCCModule = ISourceControlModule::Get(); SCCModule.UnregisterProviderChanged(ProviderChangedDelegateHandle); ClearPendingTasks(); } bool FSourceControlAssetDataCache::GetAssetDataArray(FSourceControlStateRef InFileState, FAssetDataArrayPtr& OutAssetDataArrayPtr) { if (bIsSourceControlDialogShown) { return false; } FSourceControlAssetDataEntry* AssetDataEntry = AssetDataCache.Find(InFileState->GetFilename()); if (AssetDataEntry == nullptr) { AssetDataEntry = AddAssetInformationEntry(InFileState); } check(AssetDataEntry != nullptr); if (AssetDataEntry->bInitialized) { OutAssetDataArrayPtr = AssetDataEntry->AssetDataArrayPtr; return true; } return false; } FSourceControlAssetDataEntry* FSourceControlAssetDataCache::AddAssetInformationEntry(FSourceControlStateRef InFileState) { FString Filename = InFileState->GetFilename(); FSourceControlAssetDataEntry* AssetDataEntry = &AssetDataCache.Add(Filename); check(AssetDataEntry != nullptr); check(!AssetDataEntry->AssetDataArrayPtr.IsValid()); AssetDataEntry->bInitialized = false; AssetDataEntry->AssetDataArrayPtr = MakeShared>(); /* For deleted items, the file is not on disk anymore so the only way we can get the asset data is by getting the file from the depot. * For shelved files, if the file still exists locally, it will have been found before, otherwise, the history of the shelved file state will point to the remote version */ if (!InFileState->IsDeleted()) { AssetDataEntry->bInitialized = USourceControlHelpers::GetAssetData(Filename, *AssetDataEntry->AssetDataArrayPtr); } // At the moment, getting the asset data from non-external assets yields issues with the package path if (IAssetRegistry::Get()->IsPathBeautificationNeeded(Filename) && (InFileState->IsDeleted() || (AssetDataEntry->AssetDataArrayPtr->Num() == 0))) { check(!AssetDataEntry->bInitialized); if (InFileState->GetHistorySize() == 0) { FileHistoryToUpdate.Add(Filename); } else { AssetDataToFetch.Enqueue(InFileState); } } else { // We either already have AssetData or we can't go further with this asset. AssetDataEntry->bInitialized = true; } return AssetDataEntry; } void FSourceControlAssetDataCache::Tick() { GetFileHistory(); LaunchFetchAssetDataTasks(); UpdatePendingAssetData(); } void FSourceControlAssetDataCache::OnSourceControlDialogShown() { ClearPendingTasks(); bIsSourceControlDialogShown = true; } void FSourceControlAssetDataCache::OnSourceControlDialogClosed() { bIsSourceControlDialogShown = false; } void FSourceControlAssetDataCache::GetFileHistory() { if (FileHistoryToUpdate.Num() == 0) { return; } ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); TSharedRef UpdateStatusOperation = ISourceControlOperation::Create(); UpdateStatusOperation->SetUpdateHistory(true); UpdateStatusOperation->SetQuiet(true); SourceControlProvider.Execute(UpdateStatusOperation, FileHistoryToUpdate, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateLambda([FileHistoryToUpdateCopy = FileHistoryToUpdate](const FSourceControlOperationRef& Operation, ECommandResult::Type InResult) { ISourceControlModule::Get().GetAssetDataCache().OnUpdateHistoryComplete(FileHistoryToUpdateCopy, Operation, InResult); })); FileHistoryToUpdate.Reset(); } void FSourceControlAssetDataCache::OnUpdateHistoryComplete(const TArray& InUpdatedFiles, const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult) { if (InResult == ECommandResult::Failed) { UE_LOG(LogSourceControl, Warning, TEXT("UpdateHistory Failed.")); return; } ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); for (const FString& Filename : InUpdatedFiles) { // The AssetDataCache container entry might have been removed during FUpdateStatus. if (FSourceControlAssetDataEntry* AssetDataEntry = AssetDataCache.Find(Filename)) { FSourceControlStatePtr FileState = SourceControlProvider.GetState(Filename, EStateCacheUsage::Use); check(FileState.IsValid()); if (FileState->GetHistorySize() > 0) { AssetDataToFetch.Enqueue(FileState); } else { // History was not updated we probably cannot go further with this asset. AssetDataEntry->bInitialized = true; } } } } void FSourceControlAssetDataCache::ClearAssetData(const FString& Filename) { if (FSourceControlAssetDataEntry* AssetDataEntry = AssetDataCache.Find(Filename)) { if (AssetDataEntry->bInitialized) { AssetDataCache.Remove(Filename); } } } void FSourceControlAssetDataCache::LaunchFetchAssetDataTasks() { FSourceControlStatePtr FileState; const uint32 MaxAsyncTask = static_cast(CVarSourceControlAssetDataCacheMaxAsyncTask.GetValueOnGameThread()); check(MaxAsyncTask > 0); while ((CurrentAsyncTask < MaxAsyncTask) && AssetDataToFetch.Dequeue(FileState)) { check(FileState.IsValid()); FSourceControlStateRef FileStateRef = FileState.ToSharedRef(); FSourceControlAssetDataEntry* AssetDataEntry = AssetDataCache.Find(FileStateRef->GetFilename()); check(AssetDataEntry != nullptr); ++CurrentAsyncTask; AssetDataEntry->FetchAssetDataTask = Async(EAsyncExecution::TaskGraph, [FileStateRef]() { ISourceControlModule::Get().GetAssetDataCache().FetchAssetData(FileStateRef); }, []() { ISourceControlModule::Get().GetAssetDataCache().OnFetchAssetDataComplete(); }); } } void FSourceControlAssetDataCache::FetchAssetData(FSourceControlStateRef InFileState) { check(InFileState->GetHistorySize() > 0); FString TempFileName; TSharedPtr Revision = InFileState->GetHistoryItem(0); FSourceControlAssetDataEntry* AssetInformation = AssetDataCache.Find(InFileState->GetFilename()); check(AssetInformation != nullptr); check(!AssetInformation->bInitialized); check(AssetInformation->AssetDataArrayPtr.IsValid()); check(Revision.IsValid()); const int64 MaxFetchSize = (1 << 20); // 1MB const bool bShouldGetFile = (MaxFetchSize < 0 || MaxFetchSize > static_cast(Revision->GetFileSize())); if (bShouldGetFile && Revision->Get(TempFileName, EConcurrency::Asynchronous)) { FAssetDataToLoad ToLoad = { TempFileName, InFileState->GetFilename() }; AssetDataToLoad.Enqueue(MoveTemp(ToLoad)); } } void FSourceControlAssetDataCache::OnFetchAssetDataComplete() { --CurrentAsyncTask; } void FSourceControlAssetDataCache::UpdatePendingAssetData() { FAssetDataToLoad ToLoad; while (AssetDataToLoad.Dequeue(ToLoad)) { FSourceControlAssetDataEntry* AssetDataEntry = AssetDataCache.Find(ToLoad.Filename); check(AssetDataEntry != nullptr); USourceControlHelpers::GetAssetData(ToLoad.TempFilename, *(AssetDataEntry->AssetDataArrayPtr), nullptr); // We either already have AssetData or we can't go further with this asset. AssetDataEntry->bInitialized = true; } } void FSourceControlAssetDataCache::OnSourceControlProviderChanged(ISourceControlProvider& OldProvider, ISourceControlProvider& NewProvider) { ClearPendingTasks(); } void FSourceControlAssetDataCache::ClearPendingTasks() { TArray EntriesToRemove; // Wait for tasks to stop for (auto& Pair : AssetDataCache) { FAssetDataCache::KeyType& Filename = Pair.Key; FAssetDataCache::ValueType& AssetDataEntry = Pair.Value; if (AssetDataEntry.bInitialized) { continue; } AssetDataEntry.FetchAssetDataTask.Wait(); AssetDataEntry.FetchAssetDataTask.Reset(); EntriesToRemove.Add(Filename); } // We remove all previously pending entries, they will restart the fetching process if needed again for (const FString& Entry : EntriesToRemove) { AssetDataCache.Remove(Entry); } // Clear queues FileHistoryToUpdate.Reset(); AssetDataToFetch.Empty(); AssetDataToLoad.Empty(); }