UpdateLevelVisibility using lots of bandwidth sending PackageName strings

Since one of the things that we will be charged for when renting servers for our game is bandwidth (mostly server --> client I think), we are currently attempting to minimise our bandwidth requirements.

I think that I understand the need for servers and clients to agree about what levels they currently have streamed in (in case streamed levels contain objects that require replication) though maybe I’m wrong. Regardless, it looks like package names, which are `FName`s, are used to identify these levels and are being serialised as strings. It feels like this could just use string hashes rather than full strings and save a chunk of bandwidth (or have some other pre-shared level ids).

Have I mis-undertsood the system (likely)? If not, then are there any options for preventing this behaviour (note that we don’t actually embed any networkables in streamed levels in our game) that I’m missing? Any advice or insight is very much appreciated.

[Image Removed]

[Image Removed]

Steps to Reproduce

  • Use the default replication system (not Iris).
  • Have a multi-player map which uses WorldPartition (no streaming on server).
  • Run dedicated server (with `-traceFile -trace=“frame,bookmarks,net” -netTrace=1`) and connect client to server.
  • Load above map and have client player move around causing level streaming.
  • View trace in Insights and inspect packet details in Networking tabs.
  • Note that `ClientAckUpdateLevelVisibility` (server --> client) and `ServerUpdateLevelVisibility` (client --> server) use lots of bandwidth seemingly on sending `FUpdateLevelVisibilityLevelInfo::PackageName`.

Modifying `FUpdateLevelVisibilityLevelInfo::NetSerialize` as follows (ripping off `UE::GameplayTags::GameplayTagDynamicSerialization::NetSerialize`) seems to help:

`bool FUpdateLevelVisibilityLevelInfo::NetSerialize(FArchive& Ar, UPackageMap* PackageMap, bool& bOutSuccess)
{
bool bArePackageAndFileTheSame = !!((PlayerControllerCVars::LevelVisibilityDontSerializeFileName) || (FileName == PackageName) || (FileName == NAME_None));
bool bLocalIsVisible = !!bIsVisible;
bool bLocalTryMakeVisible = !!bTryMakeVisible;

Ar.SerializeBits(&bArePackageAndFileTheSame, 1);
Ar.SerializeBits(&bLocalIsVisible, 1);
Ar.SerializeBits(&bLocalTryMakeVisible, 1);
// @CHANGE BEGIN

#if UE_WITH_IRIS
if (Ar.IsLoading())
{
const UE::Net::FNetTokenResolveContext* netTokenResolveContext = PackageMap ? PackageMap->GetNetTokenResolveContext() : nullptr;
UE::Net::FNameTokenStore* nameTokenStore = netTokenResolveContext ? netTokenResolveContext->NetTokenStore->GetDataStoreUE::Net::FNameTokenStore() : nullptr;

if (ensure(nameTokenStore))
{
UE::Net::FNetToken nameToken = nameTokenStore->ReadNetToken(Ar);
if (!Ar.IsError())
{
PackageName = nameTokenStore->ResolveToken(nameToken, netTokenResolveContext->RemoteNetTokenStoreState);
}
}
}
else
{
UE::Net::FNetTokenExportContext* netTokenExportContext = UE::Net::FNetTokenExportContext::GetNetTokenExportContext(Ar);
UE::Net::FNetTokenStore* netTokenStore = netTokenExportContext ? netTokenExportContext->GetNetTokenStore() : nullptr;
UE::Net::FNameTokenStore* nameTokenStore = netTokenStore ? netTokenStore->GetDataStoreUE::Net::FNameTokenStore() : nullptr;

if (ensure(nameTokenStore))
{
UE::Net::FNetToken netToken = nameTokenStore->GetOrCreateToken(PackageName);
nameTokenStore->WriteNetToken(Ar, netToken);
netTokenExportContext->AddNetTokenPendingExport(netToken);
}
}
#else // UE_WITH_IRIS
// @CHANGE END
Ar << PackageName;
// @CHANGE BEGIN
#endif // UE_WITH_IRIS
// @CHANGE END

if (!bArePackageAndFileTheSame)
{
Ar << FileName;
}
else if (Ar.IsLoading())
{
FileName = PackageName;
}

VisibilityRequestId.NetSerialize(Ar, PackageMap, bOutSuccess);

bIsVisible = bLocalIsVisible;
bTryMakeVisible = bLocalTryMakeVisible;

bOutSuccess = !Ar.IsError();
return true;
}To make the server\-to\-client save bandwidth too, it may also be worth changing \APlayerController::ClientAckUpdateLevelVisibility` to use the `FUpdateLevelVisibilityLevelInfo` as a parameter (otherwise it will just serialise the PackageName parameter as a string).

There may well be some pitfall that I’ve wondered into though or I may be missing something fundamental that invalidates this change.

Hi,

Improving the net serialization of FNames is something that has been discussed, such as by potentially sending some sort of ID or hash like you mentioned. I unfortunately can’t provide any estimate as to when a change like that would be made though.

If your streaming levels don’t contain any replicated actors, something you may consider trying is setting bClientOnlyVisible on these levels, as I believe ServerUpdateLevelVisibility won’t be called for these levels.

As for your change to FUpdateLevelVisibilityLevelInfo::NetSerialize, I’ve reached out to someone more familiar with NetTokens to get his input.

Thanks,

Alex

Thanks for the tip about `bClientOnlyVisible`. I’ll have a play and see if it works for our use case.

Hi,

A colleague took a look at your change here, and there doesn’t seem to be any pitfalls you’ll need to watch out for. Like you mentioned, we use the same kind of serialization for gameplay tags, and we already use net tokens for replicating FNames in Iris.

The only thing worth noting is that you’ll also need specific handling for replays. You can check out how UE::GameplayTags::GameplayTagDynamicSerialization handles this case with NetSerialize_ForReplay.

Thanks,

Alex

Thanks so much for taking a look. Definitely feels good to be reassured that I’m not barking up the wrong tree.

I’ll take a look at the `NetSerialize_ForReplay` too. I’d missed that.