Repository navigation
Lot of GC when receive and send messages #4091
Description
Activity
- addedtype:bugBug ReportBug Reportstat:awaiting-triageStatus - Awaiting triage from the Netcode team.Status - Awaiting triage from the Netcode team.stat:reply-neededAwaiting reply from Unity accountAwaiting reply from Unity account
on Jul 18, 2026 The middle screenshot is unfortunately a known issue. It's due to the current design of NetworkTransform. We're working on removing it, but unfortunately it requires a pretty deep re-work of NetworkTransform. It's a tricky piece of work that is unfortunately likely to cause some regressions
The other two GC allocs look more solvable.
The first one is inside a call of
Activator.CreateInstance. The only NGO code that calls this method is inside Editor code, so it's unlikely to be the culprit here. Do you have any custom serialization configured in your project? Common culprits can be anyINetworkSerializabletypes or NetworkBehaviours withOnSynchronizeimplemented.The bottom screenshot is coming from an RPC send. Some of NGO's automatic serialization of RPC parameters will make GC allocs. Are you passing any parameters with that RPC? If you can share the RPC code here we might be able to help figure out how to get the same functionality without the GC allocs.
- addedtype:supportQuestions or other supportQuestions or other supportstat:InvestigatingIssue is currently being investigatedIssue is currently being investigatedand removedstat:awaiting-triageStatus - Awaiting triage from the Netcode team.Status - Awaiting triage from the Netcode team.
on Jul 23, 2026 - addedstat:awaiting-responseAwaiting response from author. This label should be added manually.Awaiting response from author. This label should be added manually.and removedstat:reply-neededAwaiting reply from Unity accountAwaiting reply from Unity account
on Jul 23, 2026 Thanks for the clarification.
First Screenshot
Yes, I have many structures and classes that implement INetworkSerializable. What exactly might affect allocations? I've provided the code for some of the implementations of this interface below.StateInfo
public struct StateInfo : INetworkSerializable { private float _lastSacrificeServerTime; public readonly float LastSacrificeServerTime => _lastSacrificeServerTime; private byte _sacrificeCount; public readonly byte SacrificeCount => _sacrificeCount; public StateInfo(float lastSacrificeTime, byte sacrificeCount) { _lastSacrificeServerTime = lastSacrificeTime; _sacrificeCount = sacrificeCount; } public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter { serializer.SerializeValue(ref _lastSacrificeServerTime); serializer.SerializeValue(ref _sacrificeCount); } }
CompressedNormal
public struct CompressedNormal : INetworkSerializable { private const float k_ThetaEncodeScale = short.MaxValue / 180f; private const float k_PhiEncodeScale = byte.MaxValue / 180f; private short _theta; private byte _phi; public readonly Vector3 DecompressedNormal { get { float t = _theta / k_ThetaEncodeScale * Mathf.Deg2Rad; float p = _phi / k_PhiEncodeScale * Mathf.Deg2Rad; return new Vector3( Mathf.Sin(p) * Mathf.Sin(t), Mathf.Cos(p), Mathf.Sin(p) * Mathf.Cos(t) ); } } public CompressedNormal(Vector3 normal) { float t = Mathf.Atan2(normal.x, normal.z) * Mathf.Rad2Deg; float p = Mathf.Acos(Mathf.Clamp(normal.y, -1f, 1f)) * Mathf.Rad2Deg; _theta = (short)Mathf.RoundToInt(t * k_ThetaEncodeScale); _phi = (byte) Mathf.RoundToInt(p * k_PhiEncodeScale); } public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter { serializer.SerializeValue(ref _theta); serializer.SerializeValue(ref _phi); } }
Bottom Screenshot
Yes, I am passing data using Rpc, but I am using a pre-created NativeArray, as well as a structure (TransformData) that contains the structures.NetworkTransformGroup
using Network.Utilities; using SimulationCycle; using System; using Unity.Collections; using Unity.Netcode; using UnityEngine; using Utilities.ComponentsGrabbers; using Utilities.Interpolation; namespace Network.TransformSynchronization { public class NetworkTransformGroup : NetworkBehaviour, IAuthoritative, IGlobalInterpolationCycleSubscriber { [Header("Interpolation Settings")] [SerializeField] private float _positionDeltaThreshold = 0.0001f; [SerializeField] private float _rotationDeltaThreshold = 0.0001f; private NetworkTransformGroupMember[] _networkTransformGroupMembers = Array.Empty<NetworkTransformGroupMember>(); private NativeArray<TransformData> _nativeTransformDatas; private bool __isActive = false; public bool IsActive { get => __isActive; set { if (__isActive == value) return; __isActive = value; if (value) { OnActivate(); } else { OnDeactivate(); } } } public int InterpolationExecutionOrder => -50; public virtual bool IsAuthoritative() => IsSpawned && IsServer; public override void OnNetworkSpawn() { base.OnNetworkSpawn(); if (IsAuthoritative()) { NetworkManager.NetworkTickSystem.Tick += OnTick; } } public override void OnNetworkDespawn() { base.OnNetworkDespawn(); Deinitialize(); NetworkManager.NetworkTickSystem.Tick -= OnTick; } public void Initialize(ComponentsGrabber modelRootGrabber) { Deinitialize(); _networkTransformGroupMembers = modelRootGrabber.GrabComponents<NetworkTransformGroupMember>(); _nativeTransformDatas = new NativeArray<TransformData>(_networkTransformGroupMembers.Length, Allocator.Persistent); foreach (var member in _networkTransformGroupMembers) { member.Initialize(IsAuthoritative, _positionDeltaThreshold, _rotationDeltaThreshold); } OnDeactivate(); } public void Deinitialize() { IsActive = false; foreach (var member in _networkTransformGroupMembers) { member.Deinitialize(); } if (_nativeTransformDatas.IsCreated) _nativeTransformDatas.Dispose(); _networkTransformGroupMembers = Array.Empty<NetworkTransformGroupMember>(); } public void OnInterpolationUpdate(float deltaTime, float interpolationFactor) { if (!IsActive || IsAuthoritative()) return; for (int i = 0; i < _networkTransformGroupMembers.Length; i++) { _networkTransformGroupMembers[i].TransformInterpolator.Interpolate(); } } private void OnActivate() { foreach (var member in _networkTransformGroupMembers) { member.IsActive = true; } } private void OnDeactivate() { foreach (var member in _networkTransformGroupMembers) { member.IsActive = false; } } private void OnTick() { if (IsActive == false) { return; } for (int i = 0; i < _networkTransformGroupMembers.Length; i++) { Transform memberTransform = _networkTransformGroupMembers[i].transform; _nativeTransformDatas[i] = new TransformData(memberTransform.position, memberTransform.rotation); } SetMembersAuthoritativePositionAndRotationNotServerRpc(_nativeTransformDatas); } [Rpc(SendTo.Everyone)] private void SetMembersAuthoritativePositionAndRotationNotServerRpc(NativeArray<TransformData> transformDatas) { if (IsServer) return; for (int i = 0; i < transformDatas.Length; i++) { if (i >= _networkTransformGroupMembers.Length) throw new IndexOutOfRangeException($"Transform index {i} is out of range."); TransformData transformData = transformDatas[i]; var member = _networkTransformGroupMembers[i]; TransformInterpolator interpolator = member.TransformInterpolator; interpolator.PreSimulationInterpolationUpdate(); interpolator.PostSimulationInterpolationUpdate(NetworkManager.ServerTime.FixedDeltaTime); interpolator.UpdateInterpolationTargetData(transformData.Position, transformData.Rotation); } } public void BeforeInterpolationUpdate(float deltaTime) { } public void AfterInterpolationUpdate(float deltaTime) { } } }
TransformData
public struct TransformData : INetworkSerializeByMemcpy { public Vector3 Position; public Quaternion Rotation; public TransformData(Vector3 position, Quaternion rotation) { Position = position; Rotation = rotation; } }
- addedstat:reply-neededAwaiting reply from Unity accountAwaiting reply from Unity accountand removedstat:awaiting-responseAwaiting response from author. This label should be added manually.Awaiting response from author. This label should be added manually.
on Jul 25, 2026 Thank you for your code! I can confirm that your custom serialization and RPC code looks solid.
I finally had a chance to take another look at this. I think I might have been able to track the issues down. I was able to reproduce the issue with RPC message sending (screenshot 3) and fix that.
The first two screenshots are a known ghost GC issue that we have that we've struggled to pin down and fix. However, with your screenshots I might have finally been able to pin it down. It's potentially related to different C# compilation environments handling
new T()on a struct type differently.Which compilation backend are you using? Mono or IL2CPP?
Feel free to check if the changes in #4119 resolve the issue.
- addedstat:awaiting-responseAwaiting response from author. This label should be added manually.Awaiting response from author. This label should be added manually.and removedstat:reply-neededAwaiting reply from Unity accountAwaiting reply from Unity account
on Aug 7, 2026 Which compilation backend are you using? Mono or IL2CPP?
IP2CPP
Thanks.
- addedstat:reply-neededAwaiting reply from Unity accountAwaiting reply from Unity accountand removedstat:awaiting-responseAwaiting response from author. This label should be added manually.Awaiting response from author. This label should be added manually.
on Aug 11, 2026 - removedstat:InvestigatingIssue is currently being investigatedIssue is currently being investigatedstat:reply-neededAwaiting reply from Unity accountAwaiting reply from Unity account
on Aug 11, 2026
Description
Hi, I'm experiencing a lot of memory allocations when sending (NetworkMessageManager.SendMessage) and receiving messages (NetworkMessageManager.ReceiveMessage). These allocations occur when sending RPCs, as well as due to built-in networking components (e.g., NetworkTransform). Profiling was performed by the host using a debug build of IL2CPP with the deep profiler. There were five people (including the host) logged on to the server at the time.
Screenshots
Environment