Skip to content

Commit 6c8efd6

Browse files
test: Perf tests part 4. Adding example of performance test with spawning x network objects at once (#925)
Adding a first example of performance tests. This was used a test bench for multiprocess testing tools in Part 1-3. These tests will be skipped for normal CI as they need additional setup steps for device orchestration. They will spawn x number of objects server side, wait to see them client side and report on the number of failures, number of spawned objects per frame, etc. These tests create GC allocs before and after the tests. During the tests, the only allocs I could find were MLAPI related, which is kind of the point for these tests :)
1 parent 089c206 commit 6c8efd6

18 files changed

+550
-77
lines changed

testproject/Assets/Scenes/MultiprocessTestScene.unity

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,7 @@ MonoBehaviour:
743743
m_Script: {fileID: 11500000, guid: 068bf11ceb1344667af4cc40950f44f4, type: 3}
744744
m_Name:
745745
m_EditorClassIdentifier:
746-
referencedPrefab: {fileID: 5637023994061915634, guid: b0952a471c5a147cb92f6afcdb648f8a,
746+
ReferencedPrefab: {fileID: 5637023994061915634, guid: b0952a471c5a147cb92f6afcdb648f8a,
747747
type: 3}
748748
--- !u!114 &1211923378
749749
MonoBehaviour:
@@ -817,8 +817,6 @@ MonoBehaviour:
817817
m_Script: {fileID: 11500000, guid: ef1240e0784f84eadb77fe822e2e03c7, type: 3}
818818
m_Name:
819819
m_EditorClassIdentifier:
820-
isRegistering: 0
821-
hasRegistered: 0
822820
--- !u!1 &1674777071
823821
GameObject:
824822
m_ObjectHideFlags: 0

testproject/Assets/Scripts/ScriptsForAutomatedTesting.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using UnityEngine;
2+
3+
/// <summary>
4+
/// Serves as access point from code to a prefab
5+
/// </summary>
6+
public class PrefabReference : MonoBehaviour
7+
{
8+
[SerializeField]
9+
public GameObject ReferencedPrefab;
10+
11+
public static PrefabReference Instance { get; private set; }
12+
13+
public void Awake()
14+
{
15+
if (Instance != null)
16+
{
17+
Destroy(gameObject);
18+
return;
19+
}
20+
21+
Instance = this;
22+
}
23+
}

testproject/Assets/Scripts/ScriptsForAutomatedTesting/PrefabReference.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "ScriptsForAutomatedTesting",
3+
"rootNamespace": "",
4+
"references": [
5+
"GUID:753245531f1b64a6ea7491ec63a5947c",
6+
"GUID:1491147abca9d7d4bb7105af628b223e"
7+
],
8+
"includePlatforms": [],
9+
"excludePlatforms": [],
10+
"allowUnsafeCode": false,
11+
"overrideReferences": false,
12+
"precompiledReferences": [],
13+
"autoReferenced": true,
14+
"defineConstraints": [],
15+
"versionDefines": [],
16+
"noEngineReferences": false
17+
}

testproject/Assets/Scripts/ScriptsForAutomatedTesting/ScriptsForAutomatedTesting.asmdef.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testproject/Assets/Tests/Runtime/MultiprocessRuntime.meta

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testproject/Assets/Tests/Runtime/MultiprocessRuntime/Helpers.meta

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testproject/Assets/Tests/Runtime/MultiprocessRuntime/Helpers/BuildMultiprocessTestPlayer.cs

Lines changed: 92 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6,91 +6,118 @@
66
#endif
77
using UnityEngine;
88

9-
/// <summary>
10-
/// This is needed as Unity throws "An abnormal situation has occurred: the PlayerLoop internal function has been called recursively. Please contact Customer Support with a sample project so that we can reproduce the problem and troubleshoot it."
11-
/// when trying to build from Setup() steps in tests.
12-
/// </summary>
13-
public static class BuildMultiprocessTestPlayer
9+
namespace MLAPI.MultiprocessRuntimeTests
1410
{
15-
public const string MultiprocessBaseMenuName = "MLAPI/Multiprocess Test";
16-
public const string BuildAndExecuteMenuName = MultiprocessBaseMenuName + "/Build Test Player #t";
17-
public const string MainSceneName = "MultiprocessTestScene";
18-
private static string BuildPathDirectory => Path.Combine(Path.GetDirectoryName(Application.dataPath), "Builds", "MultiprocessTests");
19-
public static string BuildPath => Path.Combine(BuildPathDirectory, "MultiprocessTestPlayer");
11+
/// <summary>
12+
/// This is needed as Unity throws "An abnormal situation has occurred: the PlayerLoop internal function has been called recursively. Please contact Customer Support with a sample project so that we can reproduce the problem and troubleshoot it."
13+
/// when trying to build from Setup() steps in tests.
14+
/// </summary>
15+
public static class BuildMultiprocessTestPlayer
16+
{
17+
public const string MultiprocessBaseMenuName = "MLAPI/Multiprocess Test";
18+
public const string BuildAndExecuteMenuName = MultiprocessBaseMenuName + "/Build Test Player #t";
19+
public const string MainSceneName = "MultiprocessTestScene";
20+
21+
public const string BuildInfoFileName = "buildInfo.json";
22+
23+
private static string BuildPathDirectory => Path.Combine(Path.GetDirectoryName(Application.dataPath), "Builds", "MultiprocessTests");
24+
public static string BuildPath => Path.Combine(BuildPathDirectory, "MultiprocessTestPlayer");
2025

2126
#if UNITY_EDITOR
22-
[MenuItem(BuildAndExecuteMenuName)]
23-
public static void BuildRelease()
24-
{
25-
var report = BuildPlayer();
26-
if (report.summary.result != BuildResult.Succeeded)
27+
[MenuItem(BuildAndExecuteMenuName)]
28+
public static void BuildRelease()
2729
{
28-
throw new Exception($"Build failed! {report.summary.totalErrors} errors");
30+
var report = BuildPlayer();
31+
if (report.summary.result != BuildResult.Succeeded)
32+
{
33+
throw new Exception($"Build failed! {report.summary.totalErrors} errors");
34+
}
2935
}
30-
}
3136

32-
[MenuItem(MultiprocessBaseMenuName + "/Build Test Player (Debug)")]
33-
public static void BuildDebug()
34-
{
35-
var report = BuildPlayer(true);
36-
if (report.summary.result != BuildResult.Succeeded)
37+
[MenuItem(MultiprocessBaseMenuName + "/Build Test Player (Debug)")]
38+
public static void BuildDebug()
3739
{
38-
throw new Exception($"Build failed! {report.summary.totalErrors} errors");
40+
var report = BuildPlayer(true);
41+
if (report.summary.result != BuildResult.Succeeded)
42+
{
43+
throw new Exception($"Build failed! {report.summary.totalErrors} errors");
44+
}
3945
}
40-
}
4146

42-
[MenuItem(MultiprocessBaseMenuName + "/Delete Test Build")]
43-
public static void DeleteBuild()
44-
{
45-
if (Directory.Exists(BuildPathDirectory))
47+
[MenuItem(MultiprocessBaseMenuName + "/Delete Test Build")]
48+
public static void DeleteBuild()
4649
{
47-
Directory.Delete(BuildPathDirectory, recursive: true);
50+
if (Directory.Exists(BuildPathDirectory))
51+
{
52+
Directory.Delete(BuildPathDirectory, recursive: true);
53+
}
54+
else
55+
{
56+
Debug.Log($"[{nameof(BuildMultiprocessTestPlayer)}] build directory does not exist ({BuildPathDirectory}) not deleting anything");
57+
}
4858
}
49-
else
59+
60+
/// <summary>
61+
/// Needs a separate build than the standalone test builds since we don't want the player to try to connect to the editor to do test
62+
/// reporting. We only want to main node to do that, worker nodes should be dumb
63+
/// </summary>
64+
/// <returns></returns>
65+
private static BuildReport BuildPlayer(bool isDebug = false)
5066
{
51-
Debug.Log($"[{nameof(BuildMultiprocessTestPlayer)}] build directory does not exist ({BuildPathDirectory}) not deleting anything");
52-
}
53-
}
67+
// Save standalone build path to file so we can read it from standalone tests (that are not running from editor)
68+
SaveBuildInfo(new BuildInfo() { BuildPath = BuildPath, IsDebug = isDebug });
5469

55-
/// <summary>
56-
/// Needs a separate build than the standalone test builds since we don't want the player to try to connect to the editor to do test
57-
/// reporting. We only want to main node to do that, worker nodes should be dumb
58-
/// </summary>
59-
/// <returns></returns>
60-
private static BuildReport BuildPlayer(bool isDebug = false)
61-
{
62-
// Save standalone build path to file so we can read it from standalone tests (that are not running from editor)
63-
File.WriteAllText(Path.Combine(Application.streamingAssetsPath, MultiprocessOrchestration.BuildInfoFileName), BuildPath);
70+
// deleting so we don't end up testing on outdated builds if there's a build failure
71+
DeleteBuild();
6472

65-
// deleting so we don't end up testing on outdated builds if there's a build failure
66-
DeleteBuild();
73+
var buildOptions = BuildOptions.None;
74+
buildOptions |= BuildOptions.IncludeTestAssemblies;
75+
buildOptions |= BuildOptions.StrictMode;
76+
if (isDebug)
77+
{
78+
buildOptions |= BuildOptions.Development;
79+
buildOptions |= BuildOptions.AllowDebugging; // enable this if you want to debug your players. Your players
6780

68-
var buildOptions = BuildOptions.None;
69-
buildOptions |= BuildOptions.IncludeTestAssemblies;
70-
buildOptions |= BuildOptions.StrictMode;
71-
if (isDebug)
72-
{
73-
buildOptions |= BuildOptions.Development;
74-
buildOptions |= BuildOptions.AllowDebugging; // enable this if you want to debug your players. Your players
75-
// will have more connection permission popups when launching though
81+
// will have more connection permission popups when launching though
82+
}
83+
84+
var buildPathToUse = BuildPath;
85+
if (Application.platform == RuntimePlatform.WindowsPlayer || Application.platform == RuntimePlatform.WindowsEditor)
86+
{
87+
buildPathToUse += ".exe";
88+
}
89+
90+
Debug.Log($"Starting multiprocess player build using path {buildPathToUse}");
91+
92+
buildOptions &= ~BuildOptions.AutoRunPlayer;
93+
var buildReport = BuildPipeline.BuildPlayer(
94+
new[] { $"Assets/Scenes/{MainSceneName}.unity" },
95+
buildPathToUse,
96+
EditorUserBuildSettings.activeBuildTarget,
97+
buildOptions);
98+
99+
Debug.Log("Build finished");
100+
return buildReport;
76101
}
102+
#endif
77103

78-
var buildPathToUse = BuildPath;
79-
if (Application.platform == RuntimePlatform.WindowsPlayer || Application.platform == RuntimePlatform.WindowsEditor)
104+
[Serializable]
105+
public struct BuildInfo
80106
{
81-
buildPathToUse += ".exe";
107+
public string BuildPath;
108+
public bool IsDebug;
82109
}
83-
Debug.Log($"Starting multiprocess player build using path {buildPathToUse}");
84110

85-
buildOptions &= ~BuildOptions.AutoRunPlayer;
86-
var buildReport = BuildPipeline.BuildPlayer(
87-
new[] { $"Assets/Scenes/{MainSceneName}.unity" },
88-
buildPathToUse,
89-
EditorUserBuildSettings.activeBuildTarget,
90-
buildOptions);
111+
public static BuildInfo ReadBuildInfo()
112+
{
113+
var jsonString = File.ReadAllText(Path.Combine(Application.streamingAssetsPath, BuildInfoFileName));
114+
return JsonUtility.FromJson<BuildInfo>(jsonString);
115+
}
91116

92-
Debug.Log("Build finished");
93-
return buildReport;
117+
public static void SaveBuildInfo(BuildInfo toSave)
118+
{
119+
var buildInfoJson = JsonUtility.ToJson(toSave);
120+
File.WriteAllText(Path.Combine(Application.streamingAssetsPath, BuildInfoFileName), buildInfoJson);
121+
}
94122
}
95-
#endif
96123
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using UnityEngine;
3+
using MLAPI.Spawning;
4+
5+
namespace MLAPI.MultiprocessRuntimeTests
6+
{
7+
public class CustomPrefabSpawnerForPerformanceTests<T> : INetworkPrefabInstanceHandler, IDisposable where T : NetworkBehaviour
8+
{
9+
private GameObjectPool<T> m_ObjectPool;
10+
private Action<T> m_SetupSpawnedObject;
11+
private Action<T> m_OnRelease;
12+
13+
public CustomPrefabSpawnerForPerformanceTests(T prefabToSpawn, int maxObjectsToSpawn, Action<T> setupSpawnedObject, Action<T> onRelease)
14+
{
15+
m_ObjectPool = new GameObjectPool<T>();
16+
m_ObjectPool.Initialize(maxObjectsToSpawn, prefabToSpawn);
17+
m_SetupSpawnedObject = setupSpawnedObject;
18+
m_OnRelease = onRelease;
19+
}
20+
21+
public NetworkObject HandleNetworkPrefabSpawn(ulong ownerClientId, Vector3 position, Quaternion rotation)
22+
{
23+
var netBehaviour = m_ObjectPool.Get();
24+
var networkObject = netBehaviour.NetworkObject;
25+
Transform netTransform = networkObject.transform;
26+
netTransform.position = position;
27+
netTransform.rotation = rotation;
28+
m_SetupSpawnedObject(netBehaviour);
29+
return networkObject;
30+
}
31+
32+
public void HandleNetworkPrefabDestroy(NetworkObject networkObject)
33+
{
34+
var behaviour = networkObject.gameObject.GetComponent<T>(); // todo expensive, only used in teardown for now, should optimize eventually
35+
m_OnRelease(behaviour);
36+
Transform netTransform = networkObject.transform;
37+
netTransform.position = Vector3.zero;
38+
netTransform.rotation = Quaternion.identity;
39+
m_ObjectPool.Release(behaviour);
40+
}
41+
42+
public void Dispose()
43+
{
44+
m_ObjectPool.Dispose();
45+
}
46+
}
47+
}

testproject/Assets/Tests/Runtime/MultiprocessRuntime/Helpers/CustomPrefabSpawnerForPerformanceTests.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)