Skip to content

Lot of GC when receive and send messages #4091

Description

@hardsmoke

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

Image Image Image

Environment

  • OS: Windows 11
  • Unity Version: 6000.3.16f1
  • Netcode Version: 2.11.2
  • Netcode Topology: Host - Client

Activity

  1. EmandM commented on Jul 23, 2026

    @EmandM
    Member

    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 any INetworkSerializable types or NetworkBehaviours with OnSynchronize implemented.

    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.

  2. self-assigned this
    on Jul 23, 2026
  3. added
    type:supportQuestions or other support
    stat:InvestigatingIssue is currently being investigated
    and removed
    stat:awaiting-triageStatus - Awaiting triage from the Netcode team.
    on Jul 23, 2026
  4. added
    stat:awaiting-responseAwaiting response from author. This label should be added manually.
    and removed on Jul 23, 2026
  5. hardsmoke commented on Jul 25, 2026

    @hardsmoke
    Author

    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;
        }
    }
  6. added and removed
    stat:awaiting-responseAwaiting response from author. This label should be added manually.
    on Jul 25, 2026
  7. EmandM commented on Aug 7, 2026

    @EmandM
    Member

    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.

  8. added
    stat:awaiting-responseAwaiting response from author. This label should be added manually.
    and removed on Aug 7, 2026
  9. hardsmoke commented on Aug 11, 2026

    @hardsmoke
    Author

    Which compilation backend are you using? Mono or IL2CPP?

    IP2CPP

    Thanks.

  10. added and removed
    stat:awaiting-responseAwaiting response from author. This label should be added manually.
    on Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions