Addressables doesn't release from RAM

Hello everyone, it’s world streamer, it should create objects in the stream zone and delete them outside the stream zone, but assets doesn’t unload from RAM

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceLocations;

public class WorldStreamer : MonoBehaviour
{
    [SerializeField] private Transform _target;
    [SerializeField] public List<Chunk> _chunks = new();
    [SerializeField] private int _chunkSize = 256;
    [SerializeField] private bool _drawChunkGizmos;
    [SerializeField] private AssetLabelReference _mapLabel;

    private readonly ConcurrentBag<Chunk.ChunkObject> _objectsToInstance = new();
    private readonly ConcurrentBag<InstancedObject> _objectsToDestroy = new();
    private readonly Dictionary<string, InstancedObject> _instancedObjects = new();

    private Vector3 _position;
    private Transform _mapParent;

    private int _loadDistance = 500;

    private readonly System.Threading.CancellationTokenSource _cancelAsyncTaskWhenExitPlayMode = new();

    private void Start()
    {
        _mapParent = new GameObject(_mapLabel.labelString).transform;

        _ = Task.Run(()=>CheckVisibleObjects(_cancelAsyncTaskWhenExitPlayMode));
    }

    private void Update()
    {
        _position = _target == null ? transform.position : _target.position;

        for (int i = 0; i < _objectsToInstance.Count; i++)
        {
            if (_objectsToInstance.TryTake(out var result))
            {
                _instancedObjects[result.AssetReference.AssetGUID].AsyncOperationHandle = Addressables.InstantiateAsync(result.AssetReference, _mapParent);
            }
        }

        for (int i = 0; i < _objectsToDestroy.Count; i++)
        {
            if (_objectsToDestroy.TryTake(out var result))
            {
                Addressables.Release(result.AsyncOperationHandle);
            }
        }
    }

    private void CheckVisibleObjects(CancellationTokenSource cancellationTokenSource){
        DateTime dateTime = DateTime.Now;
        Vector3 position = Vector3.zero;

        while (!cancellationTokenSource.IsCancellationRequested)
        {
            if((float)(DateTime.Now - dateTime).TotalMilliseconds < 1000f/60f) continue;

            if(position == _position)
                continue;

            position = _position;

            foreach (var item in _chunks)
            {
                if (Vector3.Distance(item.Bounds.ClosestPoint(_position), _position) < _loadDistance)
                {
                    foreach (var mapObject in item.Objects)
                    {
                        bool isClose = Vector3.Distance(mapObject.Bounds.ClosestPoint(_position), _position) < _loadDistance;

                        string hashCode = mapObject.AssetReference.AssetGUID;

                        if (isClose && !_instancedObjects.ContainsKey(hashCode))
                        {
                            _instancedObjects.Add(hashCode, new(mapObject, default));

                            _objectsToInstance.Add(mapObject);
                        }
                    }
                }
            }

            foreach (var instancedObject in new List<InstancedObject>(_instancedObjects.Values))
            {
                string hashCode = instancedObject.ChunkObject.AssetReference.AssetGUID;

                if (Vector3.Distance(instancedObject.ChunkObject.Bounds.ClosestPoint(_position), _position) >= _loadDistance)
                {
                    if (instancedObject.AsyncOperationHandle.IsDone && instancedObject.AsyncOperationHandle.IsValid())
                    {
                        _objectsToDestroy.Add(instancedObject);
                        _instancedObjects.Remove(hashCode);
                    }
                }
            }

            dateTime = DateTime.Now;
        }
    }

    private void OnEnable()
    {
        GameSettingsManager.Instance.OnChangeLoadDistance.AddListener(ChangeDistance);
        EventManager.PlayerEvents.OnPlayerSpawned.AddListener(OnPlayerSpawned);

        _loadDistance = GameSettingsManager.Instance.LoadDistance;
    }
    private void OnDisable()
    {
        EventManager.PlayerEvents.OnPlayerSpawned.RemoveListener(OnPlayerSpawned);
        GameSettingsManager.Instance.OnChangeLoadDistance.RemoveListener(ChangeDistance);
    }

    private void OnDestroy() => _cancelAsyncTaskWhenExitPlayMode.Cancel();
    private void ChangeDistance(int distance) => _loadDistance = distance;
    private void OnPlayerSpawned(IPlayerController playerController) => _target = playerController.PlayerTransform;
       
    private void OnDrawGizmos(){
        if(!_drawChunkGizmos)
            return;

        foreach (var item in _chunks)
        {
            Gizmos.color = Color.yellow;
            DrawBoundsGizmo(item.Bounds);
        }
    }

    private void DrawBoundsGizmo(Bounds bounds)
    {
        Vector3 boundsCenter = bounds.center;
        Vector3 boundsExtents = bounds.extents;

        Vector3[] boundsVertices = new Vector3[]
        {
            boundsCenter + new Vector3(boundsExtents.x, 0, boundsExtents.z),
            boundsCenter + new Vector3(boundsExtents.x, 0, -boundsExtents.z),
            boundsCenter + new Vector3(-boundsExtents.x, 0, -boundsExtents.z),
            boundsCenter + new Vector3(-boundsExtents.x, 0, boundsExtents.z)
        };

        Gizmos.DrawLine(boundsVertices[0], boundsVertices[1]);
        Gizmos.DrawLine(boundsVertices[1], boundsVertices[2]);
        Gizmos.DrawLine(boundsVertices[2], boundsVertices[3]);
        Gizmos.DrawLine(boundsVertices[3], boundsVertices[0]);
    }

    [Serializable]
    public class Chunk
    {
        public Bounds Bounds;
        public List<ChunkObject> Objects;

        [Serializable]
        public class ChunkObject
        {
            public Bounds Bounds;
            public AssetReference AssetReference;

            public ChunkObject(Bounds bounds, AssetReference assetReference)
            {
                Bounds = bounds;
                AssetReference = assetReference;
            }
        }

        public Chunk(Bounds bounds)
        {
            Bounds = bounds;
            Objects = new();
        }
    }

    [Serializable]
    public class InstancedObject{
        public Chunk.ChunkObject ChunkObject;
        public AsyncOperationHandle<GameObject> AsyncOperationHandle;

        public InstancedObject(Chunk.ChunkObject chunkObject, AsyncOperationHandle<GameObject> asyncOperationHandle){
            ChunkObject = chunkObject;
            AsyncOperationHandle = asyncOperationHandle;
        }
    }

    [ContextMenu("Update Map Assets")]
    private void UpdateMapAssets()
    {
        List<Bounds> assetPositions = new();
        List<AssetReference> assetsReferences = new();
        _chunks.Clear();

        if (string.IsNullOrEmpty(_mapLabel.labelString))
        {
            Debug.LogError("Map label is not assigned or empty.");
            return;
        }

        Addressables.LoadAssetsAsync<GameObject>(_mapLabel, null).Completed += OnBoundsLoaded;
        Addressables.LoadResourceLocationsAsync(_mapLabel).Completed += OnAssetsLoaded;

        Debug.Log("Assets loading has started");

        void OnBoundsLoaded(AsyncOperationHandle<IList<GameObject>> handle)
        {
            handle.Completed -= OnBoundsLoaded;

            if (handle.Status == AsyncOperationStatus.Succeeded)
            {
                Bounds worldBounds = new();

                foreach (GameObject assetRef in handle.Result)
                {

                    if (assetRef.TryGetComponent<Renderer>(out var renderer))
                    {
                        assetPositions.Add(renderer.bounds);
                        worldBounds.Encapsulate(renderer.bounds);
                    }
                    else
                        Debug.Log($"Renderer component not found on: {assetRef.name}");
                }

                float originalSizeX = worldBounds.size.x;
                float originalSizeZ = worldBounds.size.z;

                int countX = Mathf.CeilToInt(originalSizeX / _chunkSize);
                int countZ = Mathf.CeilToInt(originalSizeZ / _chunkSize);

                float squareSizeX = originalSizeX / countX;
                float squareSizeZ = originalSizeZ / countZ;

                for (int i = 0; i < countX; i++)
                {
                    for (int j = 0; j < countZ; j++)
                    {
                        float minX = worldBounds.min.x + i * squareSizeX;
                        float minZ = worldBounds.min.z + j * squareSizeZ;

                        float maxX = minX + squareSizeX;
                        float maxZ = minZ + squareSizeZ;

                        Bounds squareBounds = new(new((minX + maxX) / 2, worldBounds.center.y, (minZ + maxZ) / 2),
                                                        new(squareSizeX, worldBounds.size.y, squareSizeZ));

                        _chunks.Add(new(squareBounds));
                    }
                }

                for (int i = 0; i < assetPositions.Count; i++)
                {
                    Bounds mapPrefab = assetPositions[i];
                    AssetReference assetReference = assetsReferences[i];

                    foreach (var item in _chunks)
                    {
                        if (item.Bounds.Contains(mapPrefab.center))
                        {
                            item.Objects.Add(new(mapPrefab, assetReference));
                            item.Bounds.Encapsulate(mapPrefab);
                            break;
                        }
                    }
                }

                for (int i = _chunks.Count - 1; i >= 0; i--)
                {
                    if (_chunks[i].Objects.Count == 0)
                        _chunks.RemoveAt(i);
                }

            }
            else
            {
                Debug.LogError($"AsyncOperation status: {handle.Status}");
            }

            Addressables.Release(handle);

            Debug.Log($"Loading assets has finished. Chunk Size: {_chunkSize} Chunks count: {_chunks.Count} Assets Count: {assetPositions.Count}");
        }

        void OnAssetsLoaded(AsyncOperationHandle<IList<IResourceLocation>> handle)
        {
            handle.Completed -= OnAssetsLoaded;

            foreach (IResourceLocation location in handle.Result)
                assetsReferences.Add(new(location.PrimaryKey));

            Addressables.Release(handle);
        }
    }
}

Maybe this helps: Addressables.Release

How do you measure memory usage? Unloading may not occur immediately.

If something keeps a reference to one of the addressable contents then unloading will likely not occur until the last reference is released. Best to test this with the simplemost example, ie load a single chunk and unload that, then check memory and whether any of the assets is still references somewhere.

I measure memory via unity profiler in editor or task manager in windows build, if I’m setting loadDistance to max then setting to zero then memory does’t release, I waited about a minute, but memory doesn’t release, loaded objects has reference by(2)(It’s mesh colliders and renderer components) and reference to(0) in memory profiler. I don’t see, where references remain

Task Manager is irrelevant, who knows what kind of caching (os or runtime) goes on behind the scenes.

Like I said, make a minimal testcase that is guaranteed to release memory and observe with MemoryProfiler. If that works its something with the project. Otherwise you have a more simple use case you can ask about or even report as a bug.

I tried Addressables.ReleaseInstance(result.AsyncOperationHandle) instead Addressables.Release and it worked, thank you for the help

1 Like