Content being re-cooked every time using latest "Modified & Dependencies" incremental cooking - What is best practice here?

Hello,

We’re working on a project where we need to often deploy to a specific hardware platform and we’re running in to long turnaround times during the cooking process in 5.6 when using the latest recommended “Modified & Dependencies” cook method. We have approximately 2,500 packages which are re-cooked every time, no matter what and it’s causing every deploy to take over 20 minutes which is seriously affecting our productivity.

We’ve attempted to dig deeper in to what is causing the problems via increased logging output and we currently believe it’s related to the `_Generated_` content folders as part of our levels which all use World Partition System. I don’t believe we’re using any custom logic which would force a cook-time regeneration of any content here but we’ve seen comments in sections of code that Generated content could be the problem. Is there anything we can do to help identify what specifically is causing this and/or any strategies for preventing this content to be re-cooked each time despite us having made no changes?

We have explored using the legacy “Modified Only” route which does give us quicker turnaround times (Cook times around 2-3 minutes) which is more manageable but as we understand this legacy system has its pitfalls. I was wondering if you could detail the kind of problems we might encounter using this legacy system or whater you think it’s a viable choice in 5.6 to give us faster turnaround times?

Any support on this would be greatly appreciated!

Thank you :smiling_face:

[Attachment Removed]

Sorry for the delay in this response. We are publishing a user guide for 5.7 that I think will answer all of these question; I’ll quote some excerpts from its rough draft. Note that we have made many robustness improvements to Incremental Cook between 5.6 and 5.7, so you should try to integrate to 5.7 as soon as possible to get those improvements. Even without those improvements however, Incremental Cook in 5.6 is much more robust thatn LegacyIterative, so you should use Incremental Cook if you can get it working performantly.

> To be fully activated on a project, IncrementalCook must be configured to allow assets of native C++ classes defined by the project (or the project’s plugins) to be Incrementally Skipped. We turn this off by default because project types might contain Hidden Dependencies on other packages, files, managers containing raw pointers to packages, or other types of inputs that we don’t automatically detect. If any such Hidden Dependencies exist, then updating those inputs would cause a False Incremental Skip and create an invalid build containing stale data.

How many packages do you have that are incrementally skipped when you cook back to back? Is it possible that the 2500 packages recooked every time are caused by this by-default disallowal of your native c++ types?

I don’t think the use of __Generated__ content folders is likely to be the cause of the packages not being skippable, it’s more likely to be caused by the missing config setting, which disproportionately affects map packages because map packages almost always include project-specific types.

I have copied the how-to section of the user guide for enabling your native types in the list below. Before that, some text explaining Hidden Package Dependencies. The how-to section has instructions for setting up TObjectPtr and analyzing your c++ types that are somewhat time-consuming, and the actual enablement just requires adding a single line to config. This text gives the explanation for why the TObjectPtr setup and validation is important. But depending on how complex your types are and what your tolerance is for failure, you can chose to skip these steps to start with and allow your types to be incrementally skipped without them, at the cost of possible stale data when hidden dependencies change.

> Correctness of Incremental Cook is a primary concern. Correctness for Incremental Cook is defined as having the same results as a cook would have if there was no previous cook and every package was an Incremental Newcook. Some Incremental Cook errors are unrelated to the data stored for each package, such as the incorrect compilation of non-package cook artifacts (AssetRegistry, ShaderLibraries, Metadata, licensee-specific artifacts). But the commonly occurring errors are a difference in the TargetDomain bytes or metadata of a previously cooked package from the bytes or metadata that would be created if it were Incrementally NewCooked. If a package is found to be Incrementally Stable and is therefore Incrementally Skipped, but if recooked its TargetDomain bytes or metadata would necessarily be different, the Incremental Skip is an error called a False Incremental Skip. False Incremental Skips are caused by an incomplete set of dependencies for the package.

> There are many kinds of dependencies a package can have that can affect the cooked bytes or the metadata stored for the package; a partial list of these:

  • PackageDependency: Values stored in a Target package on disk that was saved by the editor are read during load/save of the cooked version of a Source package and affect the bytes that are written.
    • Every TargetDomain package automatically has a PackageDependency on its WorkspaceDomain package.
    • PackageDependencies for other packages are automatically collected, for types that support it, by tracking of TObjectPtr dereferences (see below).
  • NativeClassDependency: A change to a native UClass causes a change in packages that have instances of (or otherwise depend on) that UClass.
    • The list of classes used by objects in the package are automatically collected, other classes can be added by system-specific code.
    • Changes to each native UClass that are exposed to Unreal’s reflection system (UPROPERTY, UCLASS, other fields managed by UnrealHeaderTool) are automatically collected and changes in them detected.
    • Changes to C++ hooks used by a class (e.g. native UObject::Serialize overrides) are not automatically detected and the programmer making changes to them must bump a version in the class’s AppendToClassSchema function.
    • Changes to non-native classes such as BlueprintGeneratedClasses are not detected by NativeClassDependency and instead are detected by a PackageDependency on the package containing the Blueprint.
  • ConfigDependency and ConsoleVariableDependency: Values read from ini, or from overrides on the CookCommandlet command line.
    • These are automatically collected by instrumentation in the config system.
    • Some C++ code can bypass the automatic collection and the authors of that code then need to declare them manually.

How-to Section from the user guide:

> Setting Up Incremental Cook

  • Allow your Project’s native C++ types to be incrementally skipped.
    • This may take some time to implement; you can skip this step and still gain IncrementalCook performance benefits from assets in your projects that only use already-validated Engine types, such as UTexture2D, UMaterial, and UStaticMesh.
    • Modify all of your native classes to use TObjectPtr rather than raw UObject* properties and (when feasible) function parameters.
      • TObjectPtr was written to function as a drop-in replacement for UObject* and does not have significant performance costs (with a small but noticeable exception when we subscribe to its updates during the cook for dependency tracking).
      • This change is mostly mechanical and we have a script to procedurally change your c++ code to make it. Running that script, compiling, and fixing a few (or none) compile errors for edge cases should be sufficient to make the change.
      • See the section on TObjectPtr in the UE5 Migration Guide: https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-engine-5-migration-guide
    • Statically analyze your classes that are likely to have hidden dependencies, and add code to declare them.
      • Loading non-Unreal-package files.
      • Reading UObject* gathered by a manager class not using TObjectPtr.
      • Caching the value of ConfigVariables or ConsoleVariables in function static variables, so that our automatic detection does not pick them up.
      • Classes with native Serialize/PostLoad/PreSave/OnCookEvent/BeginCacheForCookedPlatformData functions that are frequently changed. You will need to add an AppendToClassSchema function to your UObject subclass, and add a version number or guid to it, and instruct your C++ developers to bump that version when they make changes.
      • For how to declare dependencies, see <TODO: LinkToUnrealEngineC++CookingAPI>
    • Enable IncrementalCook of your types in config
      • Editor.ini:[CookSettings]:IncrementalClassScriptPackageAllowList
      • Add a line +IncrementalClassScriptPackageAllowList=Allow,<ProjectRoot>
      • Add the text “<ProjectRoot>” as a literal string, do not replace it with your project root. This string is interpreted by the cooker and replaced with all script packages in your Project or project plugins.
      • See Samples\Games\Lyra\Config\DefaultEditor.ini for an example
      • See the notes in Engine\Config\BaseEditor.ini, for the keys IncrementalClassScriptPackageAllowList,IncrementalClassAllowList,IncrementalClassDenyList under [CookSettings], for more detailed enable settings.
      • You may want to enable most of your types and disable specific problematic ones.

For your question of the problems with legacy “Modified Only”:

> In previous versions of Unreal, the mode of cooking that attempted to skip work of previously cooked packages had another name: Iterative Cook. We changed the name from Iterative Cook to Incremental Cook to indicate the improvement in functionality, and we now refer to that Iterative cook system as LegacyIterative. LegacyIterative is still available when selected by config, or when requirements for IncrementalCook (such as the use of ZenServer for storage of cook results) are not met. The primary failing of LegacyIterative cook was that it did not include NativeClassDependencies, so any change to any class would cause False Incremental Skips and the stale values would frequently cause the runtime to crash. LegacyIterative also had less robust detection of PackageDependencies, causing False Incremental Skips due to PackageDependencies that were Hidden Dependencies. LegacyIterative also had a coarser detection of ConfigDependencies, causing a performance problem - False Incremental Recooks.

[Attachment Removed]

There is a new cooker argument in 5.7 that will report on why packages were recooked. It might be difficult to pull those arguments into 5.6 because of merge conflicts. Are you able to wait until 5.7 for those tools? If you prefer to get started in 5.6, you could sync 5.7 and take a look at what the argument is doing and try to replicate it.

The argument is “-CookIncrementallyModifiedDiagnostics”, which sets the variable bIncrementallyModifiedDiagnostics. (It is also turned on by default in the IncrementalValidate job).

It creates the file in Saved\Cooked\<Platform>\<Project>\Metadata\ModifiedCookedPackages.txt which lists which dependency was found to be modified for each package.

We used this tool in 5.7 and are still using it in 5.8 to find and fix false incremental recooks in Fortnite that are caused by by the engine’s implementation of some of the auto-gathered cook dependencies that are overly broad; we expect these fixes to apply to many licensees as well.

[Attachment Removed]

We’ve upgraded to 5.7.1 and have run with that flag (plus enabled verbose logging on `LogEditorDomain`).

In the logs I’m seeing a few things:

Some things are being flagged as non-iterative due to C++ dependencies.

LogEditorDomain: Verbose: NonIterative Package /Game/<redacted> due to /Script/<redacted>

A couple of the /Script/ derived assets are referring to a 3rd party plugin which is marked as disabled. I presume this causes a problem?

Some others are due to WBP:

LogEditorDomain: Verbose: NonIterative Package /Game/UI/Widgets/WBP_SomeButton due to /Game/UI/Widgets/Buttons/WBP_Button.WBP_Button_C

I’m not sure if there’s any advice you can give around how do handle the cases where the parent is a Blueprint asset and it’s being rejected for incremental cooking?

[Attachment Removed]

Plugins are enabled based on the path to their .uplugin file. If you have plugins that are not under Engine and not under your project directory, you can add them with

+IncrementalClassScriptPackageAllowList=Allow,<RelativePathFromEngineBinariesWin64>If the relative path does not exist because the plugins are on another drive, or they are in a different location for different users of your workspace, then we will need to change the code to have some other way to specify them. The code that does the comparison is in ConstructTargetIterativeClassAllowList in Engine\Source\Editor\UnrealEd\Private\EditorDomain\EditorDomainUtils.cpp

I didn’t expect WidgetBlueprint to be denied in 5.7; it’s not in the IncrementalClassDenyList in //UE5/Release-5.7/Engine/Config/BaseEditor.ini. Maybe there’s a bug that you’re encountering.

To get a little more information, I am going to submit this change to the LogEditorDomain logging in IsIncrementalCookEnabled today; can you try it out and see what the new version of the log message says? Also, are there any other LogEditorDomain messages earlier in the log?

Engine\Source\Editor\UnrealEd\Private\TargetDomain\TargetDomainUtils.cpp

bool IsIncrementalCookEnabled(FName PackageName, bool bAllowAllClasses)
{
...
	if (!bAllowAllClasses)
	{
		UE::EditorDomain::FClassDigestMap& ClassDigests = UE::EditorDomain::GetClassDigests();
		FReadScopeLock ClassDigestsScopeLock(ClassDigests.Lock);
		for (FName ClassName : *ImportedClasses)
		{
			FTopLevelAssetPath ClassPath(WriteToString<256>(ClassName).ToView());
			UE::EditorDomain::FClassDigestData* ExistingData = nullptr;
			if (ClassPath.IsValid())
			{
				ExistingData = ClassDigests.Map.Find(ClassPath);
			}
			else if (!ClassName.IsNone())
			{
				// All classes are top-level objects, but user-defined structs are not top-level
				// objects.  In this code we don't need to handle user-defined structs because
				// we do not support deny-listing user-defined structs.  So if the ClassName is
				// not a top level asset, then ignore it.
				continue;
			}
 
			if (!ExistingData)
			{
				// !ExistingData -> !allowed, because caller has already called CalculatePackageDigest, so all
				// existing classes in the package have been added to ClassDigests.
				UE_LOG(LogEditorDomain, Verbose, TEXT("NonIterative Package %s due to missing class %s"),
					*PackageName.ToString(), *ClassPath.ToString());
				return false;
			}
 
			UE::EditorDomain::FClassDigestData* DataToRead = ExistingData;
			FTopLevelAssetPath* ClassToRead = &ClassPath;
			if (!DataToRead->bNative)
			{
				// TODO: We need to add a way to mark non-native classes (there can be many of them) as allowed or denied.
				// Currently we are allowing them all, so long as their closest native is allowed. But this is not completely
				// safe to do, because non-native classes can add constructionevents that e.g. use the Random function.
				ClassToRead = &DataToRead->ClosestNative;
				DataToRead = ClassDigests.Map.Find(DataToRead->ClosestNative);
				if (!DataToRead)
				{
					UE_LOG(LogEditorDomain, Verbose,
						TEXT("NonIterative Package %s due to missing native super class %s of non-native class %s"),
						*PackageName.ToString(), *ExistingData->ClosestNative.ToString(), *ClassPath.ToString());
					return false;
				}
			}
			if (!DataToRead->bTargetIterativeEnabled)
			{
				UE_LOG(LogEditorDomain, Verbose,
					TEXT("NonIterative Package %s due to bTargetIterativeEnabled=false on class %s%s"),
					*PackageName.ToString(), *ClassToRead->ToString(),
					(ExistingData == DataToRead ? TEXT("")
						: *FString::Printf(TEXT(", the closest native superclass of non-native class %s"), *ClassPath.ToString())));
				return false;
			}
		}
	}
	return true;
}

[Attachment Removed]

AllowListing will not help the case of “does not exist in memory”. Is the plugin enabled? If it is enabled, then there’s a bug we need to fix in the EditorDomain inspection of packages for which classes are used by the package. Possibly it’s occurring too early before the plugin is loaded and its classes are added to /Script. Possibly also the packages that are failing import the Plugin package but do not have any objects that rely on it, they’re just importing the script package for some other reason.

If the plugin is not enabled, and the imports correspond to UObjects in the package of that class as I expected, then I expect that you will get a failure to load for all of those UObjects, and any references to them will be nullptr. Hopefully that will show up as a log statement when you load the package. And furthermore, since you haven’t noticed and fixed it already, having those references be set to nullptr is not a bug and works fine. If that is the case, then the solution is to resave all those packages with the objects of the no-longer-available class; the references to the missing script package will be removed when the package is resaved with the class absent.

For the other cases, from WBP packages and packages with DataTableRows, I expect that the fix of allowlisting all of your plugins will solve those. If that’s not the case, let me know.

[Attachment Removed]

Have you found out any more information about the packages being recooked?

When a package is requested, it is supposed to be sent to an FRequestCluster which adds any dependencies to the cluster and evaluates each package for cookability and incrementally modified status.

Every package is supposed to pass through the function FRequestCluster::FGraphSearch::FExploreEdgesContext::SetIncrementallyModified which adds the package to the IncrementallyModifiedDiagnostics which writes those packages out to ModifiedCookPackages.txt.

The place to start debugging this is to check whether the package shows in the SetIncrementallyModified function, and if not, check whether it gets added to a cluster, which you can catch by putting an instrumented breakpoint in FRequestCluster::FindOrAddVertex.

[Attachment Removed]

Hello Matt!

Thank you so much for the incredibly detailed reply and apologies for the delayed response from my side.

We’re really looking forward to the full guide when it’s published and thank you for giving us early access.

Adding the line `+IncrementalClassScriptPackageAllowList=Allow,<ProjectRoot>` has helped a lot. Our cooking times for the specific platform we are targeting have reduced from around 20-25minutes to around 10-15 minutes as a result. We’re still seeing around 250 or so assets cooked each time regardless but this is a big improvement for us.

Our project is very much blueprint driven but our C++ classes do have good hygiene with use of TObjectPtr rather than raw pointers.

I wonder if there would be any way to identify the root cause of these remaining assets which are being stubborn and still re-cooking every time so we can try and improve our iteration times even further?

Thanks again for your support on this, we really appreciate it.

Alexander Houghton

[Attachment Removed]

Thank you Matt! We are planning to upgrade to 5.7 asap in order to use these new features.

[Attachment Removed]

Thanks for this. I pulled this change and it’s given some useful information in the logs that will be helpful tracking this down.

One of the logs is interesting. A plugin was used in this project but it’s now disabled in the uproject. Some of the assets getting cooked have references to some native classes provided by the plugin.

Class /Script/<ModuleRedacted>.<ClassRedacted> is imported by a package but does not exist in memory. EditorDomain keys for packages using it will be invalid if it still exists.

Looks like all of the classes that use this are being flagged as non-incremental. I’m not sure how to best handle this; the plugin does exist still - so perhaps it’s a case of allowlisting the native classes involved?

In regards to the WBP classes; it’s flagged some I expected - such as a button type being inherited from WBP_Button but it turned out that it also has a custom native base class.

It’s also highlighted some other interesting things, such as some WBP classes _referencing_ assets that have a native class. A few I’m looking at right now are native datatable row specifications.

LogEditorDomain: Verbose: NonIterative Package /Game/UI/Widgets/Menu/WBP_MyWidget due to bTargetIterativeEnabled=false on class /Script/MyGame.SomeDataTableRowHandle

I think the next step is to use this info from the logs to go and check the culprits and either allow list them or make them versionable.

[Attachment Removed]

> If that is the case, then the solution is to resave all those packages with the objects of the no-longer-available class; the references to the missing script package will be removed when the package is resaved with the class absent.

This does seem to be the case. The plugins are disabled, they were old UE 4 plugins that aren’t supported on UE5, and were never removed from the project, just disabled. I am going through the report and resaving these packages with the problem and they are gradually dropping out of the recook list (yay!).

> For the other cases, from WBP packages and packages with DataTableRows, I expect that the fix of allowlisting all of your plugins will solve those. If that’s not the case, let me know.

Yes, this was also the problem - allow listing the native classes for the data table rows did resolve this.

There’s one interesting issue to resolve:

/Game/BP_MyBlueprint, TargetDomainKey: Function: ValidateBPCookDependenciesImpl: ReportInvalidated was called : FCookDependency::Function('ValidateBPCookDependenciesImpl') failed to UpdateHash: Native Class Changed: /Script/MyGame.SomeNativeClass

When I look at the class, it’s got this:

UCLASS(BlueprintType, EditInlineNew)
class UMyNativeClass: public UObject
{
	GENERATED_BODY()
public:
	UPROPERTY()
	FGuid SomeUniqueIdProperty = FGuid::NewGuid();
 
 
.....
 
};

It looks like the author of this class wanted the inline instances to all have a unique guid, but I believe it’s causing the calculated hash to be different every time (from BlueprintDependencies.cpp -> ValidateBPCookDependenciesImpl. It’s calling HashNativeClass on (I presume) a default instance of that object?

Is this where explicitly using AppendToClassSchema would benefit? (eg: basically skip that property in the schema?)

[Attachment Removed]

Progress on this; I have removed the NewGuid() on default construction, and moved the check into PostInitProperties. The pattern I used was similar to that in UMaterialExpression

void UMyClass::PostInitProperties()
{
	Super::PostInitProperties();
	
	if (!IsTemplate())
	{
		UpdateUniqueGuid();
	}
}
 
void UMyClass::UpdateUniqueGuid()
{
	// If we are in the editor, and we don't have a valid GUID yet, generate one.
	if (GIsEditor && !FApp::IsGame() && !IsTemplate())
	{
		if (!UniqueGuid .IsValid())
		{
			if (IsRunningCookCommandlet())
			{
				UniqueGuid = FGuid::NewDeterministicGuid(GetPathName());
			}
			else
			{
				UniqueGuid = FGuid::NewGuid();;
			}
		}
	}
}

The ModifiedCookedPackages.txt file is now empty (yay!).

However, we still have approx 45 packages being recooked. I’m not sure what they are or why they aren’t in the incrementally modified file.

But this is some nice progress!

[Attachment Removed]

Thanks for this. I will take a look. Sorry for the long response time; I’ve been focussing on other aspects of content optimization that invokes the cooking

[Attachment Removed]