Files
Hasebe/Library/PackageCache/com.unity.scriptablebuildpipeline@61526b33bf2c/Tests/Editor/PrefabPackedStressTest.cs
T
2026-09-21 12:15:57 +03:00

205 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using Unity.Collections.LowLevel.Unsafe;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Tasks;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests
{
[TestFixture]
class PrefabPackedStressTest
{
static ObjectIdentifier MakeObjectId(GUID guid, long localIdentifierInFile, FileType fileType, string filePath)
{
var objectId = new ObjectIdentifier();
var boxed = (object)objectId;
var type = typeof(ObjectIdentifier);
type.GetField("m_GUID", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, guid);
type.GetField("m_LocalIdentifierInFile", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, localIdentifierInFile);
type.GetField("m_FileType", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, fileType);
type.GetField("m_FilePath", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, filePath);
return (ObjectIdentifier)boxed;
}
static long LongRandom(System.Random rand)
{
byte[] buffer = new byte[8];
rand.NextBytes(buffer);
return BitConverter.ToInt64(buffer, 0);
}
static unsafe GUID GuidRandom(System.Random rand)
{
GUID guid = new GUID();
byte[] bytes = new byte[16];
rand.NextBytes(bytes);
fixed (byte* ptr = &bytes[0])
UnsafeUtility.MemCpy(&guid, ptr, bytes.Length);
return guid;
}
void SetupSBP(out bool prevV2Hasher, out int prevSeed, out int prevHeader, bool v2Hasher = false, int seed = 0, int header = 2)
{
prevV2Hasher = ScriptableBuildPipeline.useV2Hasher;
ScriptableBuildPipeline.useV2Hasher = v2Hasher;
prevSeed = ScriptableBuildPipeline.fileIDHashSeed;
ScriptableBuildPipeline.fileIDHashSeed = seed;
prevHeader = ScriptableBuildPipeline.prefabPackedHeaderSize;
ScriptableBuildPipeline.prefabPackedHeaderSize = header;
}
void ResetSBP(bool prevV2Hasher, int prevSeed, int prevHeader)
{
ScriptableBuildPipeline.useV2Hasher = prevV2Hasher;
ScriptableBuildPipeline.fileIDHashSeed = prevSeed;
ScriptableBuildPipeline.prefabPackedHeaderSize = prevHeader;
}
// We want to ensure 1 million objects in a single asset is collision free with the default implementations we provide users.
static int LfidStressRunCount = 1000000;
// We want to ensure 1 object in a 1 million assets is collision free with the default implementations we provide users.
static int GuidStressRunCount = 1000000;
// We want to ensure that we have a low number of cluster collisions in a bundle as this ensures better loading performance.
// Ideally we want 0 collisions, but working with 64 bits in the way we do for build, that might be impossible without getting more collisions
// in the full lfid generation which will break a build, vs just slow downloading slightly.
static int BatchStressRunCount_Large = 10000;
static int BatchStressRunCount_Small = 100;
static int RandomSeed = 131072;
static ScriptableBuildPipeline.Settings DefaultSettings = new ScriptableBuildPipeline.Settings();
static object[] LfidStressCases =
{
new object[] { new PrefabPackedIdentifiers(), false, DefaultSettings.fileIDHashSeed, DefaultSettings.prefabPackedHeaderSize },
new object[] { new PrefabPackedIdentifiers(), true, DefaultSettings.fileIDHashSeed, DefaultSettings.prefabPackedHeaderSize }
};
[Test]
[TestCaseSource(nameof(LfidStressCases))]
public void SerializationIndexFromObjectIdentifier_Lfid_CollisionFreeStressTest(IDeterministicIdentifiers packingMethod, bool useV2Hasher, int seed, int headerSize)
{
SetupSBP(out bool prevV2Hasher, out int prevSeed, out int prevHeader, useV2Hasher, seed, headerSize);
System.Random rand = new System.Random(RandomSeed);
ObjectIdentifier objId = MakeObjectId(GuidRandom(rand), 3867712242362628071, FileType.MetaAssetType, "");
Dictionary<long, ObjectIdentifier> consumedIds = new Dictionary<long, ObjectIdentifier>(LfidStressRunCount);
for (int i = 0; i < LfidStressRunCount; i++)
{
objId.SetLocalIdentifierInFile(LongRandom(rand));
long lfid = packingMethod.SerializationIndexFromObjectIdentifier(objId);
if (consumedIds.TryGetValue(lfid, out var prevObjId))
{
if (objId == prevObjId)
continue;
else
Assert.Fail($"{objId} with {prevObjId} at {lfid}");
}
consumedIds.Add(lfid, objId);
}
ResetSBP(prevV2Hasher, prevSeed, prevHeader);
}
[Test]
[TestCaseSource(nameof(LfidStressCases))]
public void SerializationIndexFromObjectIdentifier_GUID_CollisionFreeStressTest(IDeterministicIdentifiers packingMethod, bool useV2Hasher, int seed, int headerSize)
{
SetupSBP(out bool prevV2Hasher, out int prevSeed, out int prevHeader, useV2Hasher, seed, headerSize);
System.Random rand = new System.Random(RandomSeed);
ObjectIdentifier objId = MakeObjectId(GuidRandom(rand), 3867712242362628071, FileType.MetaAssetType, "");
Dictionary<long, ObjectIdentifier> consumedIds = new Dictionary<long, ObjectIdentifier>(GuidStressRunCount);
for (int i = 0; i < GuidStressRunCount; i++)
{
objId.SetGuid(GuidRandom(rand));
long lfid = packingMethod.SerializationIndexFromObjectIdentifier(objId);
if (consumedIds.TryGetValue(lfid, out var prevObjId))
{
if (objId == prevObjId)
continue;
else
Assert.Fail($"{objId} with {prevObjId} at {lfid}");
}
consumedIds.Add(lfid, objId);
}
ResetSBP(prevV2Hasher, prevSeed, prevHeader);
}
[Test]
[TestCaseSource(nameof(LfidStressCases))]
public void SerializationIndexFromObjectIdentifier_BatchingQualityStressTest_Large(IDeterministicIdentifiers packingMethod, bool useV2Hasher, int seed, int headerSize)
{
BatchingQualityStressTest(packingMethod, useV2Hasher, seed, headerSize, BatchStressRunCount_Large);
}
[Test]
[TestCaseSource(nameof(LfidStressCases))]
public void SerializationIndexFromObjectIdentifier_BatchingQualityStressTest_Small(IDeterministicIdentifiers packingMethod, bool useV2Hasher, int seed, int headerSize)
{
BatchingQualityStressTest(packingMethod, useV2Hasher, seed, headerSize, BatchStressRunCount_Small);
}
void BatchingQualityStressTest(IDeterministicIdentifiers packingMethod, bool useV2Hasher, int seed, int headerSize, int runCount)
{
// This test is to check the quality of default clustering per source asset falls within certain guidelines.
// Reused-cluster count should stay well below 10% of samples. Worst hot-bucket size should stay below ~0.1%
// of runCount; for tiny runCount the float threshold rounds to zero, so use an integer exclusive cap with a
// small floor (matches large runs: e.g. 10k → ceil(10)=10 → allow max bucket size at most 9).
SetupSBP(out bool prevV2Hasher, out int prevSeed, out int prevHeader, useV2Hasher, seed, headerSize);
System.Random rand = new System.Random(RandomSeed);
ObjectIdentifier objId = MakeObjectId(GuidRandom(rand), 3867712242362628071, FileType.MetaAssetType, "");
Dictionary<int, int> clusters = new Dictionary<int, int>();
for (int i = 0; i < runCount; i++)
{
objId.SetGuid(GuidRandom(rand));
long lfid = packingMethod.SerializationIndexFromObjectIdentifier(objId);
// UUM-131143: cluster by the same high-bit asset prefix as PrefabPackedIdentifiers uses for
// contiguous-bundle ordering; do not use the first LE bytes of BitConverter.GetBytes(lfid).
int cluster = PrefabPackedSerializationIndexTestUtil.ToClusterDictionaryKey(
lfid, ScriptableBuildPipeline.prefabPackedHeaderSize);
clusters.TryGetValue(cluster, out int count);
clusters[cluster] = count + 1;
}
int[] collisionValues = clusters.Values.Where(x => x > 1).ToArray();
Array.Sort(collisionValues);
int collisions = collisionValues.Length;
// Maximum assets per cluster with multiple assets, lower is better
int maxCollisions = collisionValues.Length > 0 ? collisionValues.Last() : 0;
// Median assets per cluster with multiple assets, lower is better
int medCollisions = collisionValues.Length > 0 ? collisionValues[collisionValues.Length / 2] : 0;
Debug.Log($"Reused Clusters {collisions} ({(float)collisions/runCount*100:n2}%), Max {maxCollisions} ({(float)maxCollisions/runCount*100}%), Med {medCollisions} ({(float)medCollisions/runCount*100}%)");
Assert.IsTrue(runCount * 0.1f > collisions, "Reused cluster count > 10%");
int maxPerClusterExclusiveCap = Math.Max(3, (int)Math.Ceiling(runCount * 0.001f));
Assert.IsTrue(maxCollisions < maxPerClusterExclusiveCap,
$"Worst cluster bucket size {maxCollisions} must be below cap {maxPerClusterExclusiveCap} (~0.1% of {runCount} samples, with a minimum cap for small runs).");
ResetSBP(prevV2Hasher, prevSeed, prevHeader);
}
[Test]
public void CreateWriteCommand_ThrowsBuildException_OnCollision()
{
SetupSBP(out bool prevV2Hasher, out int prevSeed, out int prevHeader, false, 0, 4);
List<ObjectIdentifier> objectIds = new List<ObjectIdentifier>();
objectIds.Add(MakeObjectId(new GUID("066ce95d52fe15041854096a2145195e"), 3867712242362628071, FileType.MetaAssetType, ""));
objectIds.Add(MakeObjectId(new GUID("066ce95d52fe15041854096a2145195e"), 7498449973661844796, FileType.MetaAssetType, ""));
Assert.Throws(typeof(BuildFailedException), () => GenerateBundleCommands.CreateWriteCommand("InternalName", objectIds, new PrefabPackedIdentifiers(), out Hash128 renderpipelineHash));
ResetSBP(prevV2Hasher, prevSeed, prevHeader);
}
}
}