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 consumedIds = new Dictionary(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 consumedIds = new Dictionary(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 clusters = new Dictionary(); 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 objectIds = new List(); 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); } } }