Search Unity

Feedback Memory leak in NativeQueue

Discussion in 'Entity Component System' started by arkano22, Jan 23, 2020.

  1. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,929
    Hi there,

    I'm currently using jobs, burst and native containers quite extensively. I recently discovered that the code I've been writting causes Unity to consume 16 Gb of memory in about 15-20 minutes.

    Upon further inspection, I isolated the cause to be NativeQueue. Searched the forums, and found this:
    https://forum.unity.com/threads/bug-nativequeue-does-not-work-properly-proofs.759593/#post-5400375

    Turns out that when you call Enqueue(), some memory is allocated to never be released. Over time this leak is a real problem. You can reproduce it using this code, it will leak at a rate of 1 Gb every ten seconds, approximately:

    Code (CSharp):
    1.  
    2. using Unity.Collections;
    3. using Unity.Mathematics;
    4. using UnityEngine;
    5.  
    6. public class QueueLeak : MonoBehaviour
    7. {
    8.     private struct Data
    9.     {
    10.         public float4x4 data0;
    11.         public float4x4 data1;
    12.         public float4x4 data2;
    13.         public float4x4 data3;
    14.         public float4x4 data4;
    15.         public float4x4 data5;
    16.         public float4x4 data6;
    17.         public float4x4 data7;
    18.         public float4x4 data8;
    19.         public float4x4 data9;
    20.         public float4x4 data10;
    21.         public float4x4 data11;
    22.         public float4x4 data12;
    23.         public float4x4 data13;
    24.         public float4x4 data14;
    25.         public float4x4 data15;
    26.         public float4x4 data16;
    27.         public float4x4 data17;
    28.         public float4x4 data18;
    29.         public float4x4 data19;
    30.     }
    31.  
    32.     private NativeQueue<Data> queue;
    33.  
    34.     private void OnEnable()
    35.     {
    36.         //Allocate
    37.         queue = new NativeQueue<Data>(Allocator.Persistent);
    38.     }
    39.  
    40.     private void OnDisable()
    41.     {
    42.         //Deallocate
    43.         queue.Dispose();
    44.     }
    45.  
    46.     private void Update()
    47.     {
    48.         for (int i = 0; i < 10000; i++)
    49.         {
    50.             queue.Enqueue(default);
    51.             queue.Dequeue();
    52.         }
    53.     }
    54. }
    Can anyone confirm it's not an obvious misuse of the queue, so that I can file a bug report?
     
    Dale-Nation and futurlab_xbox like this.
  2. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,929
    Upon inspecting the queue's source during these two calls:

    Code (CSharp):
    1.  
    2. queue.Enqueue(default);
    3. queue.Dequeue();
    4.  
    It seems the issue is that the amount of readers for the first block is always 2, so when Release() is called within Dequeue():

    Code (CSharp):
    1. public unsafe static void Release(NativeQueueBlockHeader* block, NativeQueueBlockPoolData* pool)
    2. {
    3.       if (0 == Interlocked.Decrement(ref block->m_NumReaders))
    4.       {
    5.             pool->FreeBlock(block);
    6.       }
    7. }
    the interlocked decrement can only get it to 1, never 0, and the block is never freed.

    How does this happen? When you call Enqueue(), AllocateWriteBlockMT is called and numReaders is initialized to 1 for the block:
    Code (CSharp):
    1.  
    2.            if (currentWriteBlock == null)
    3.             {
    4.                 currentWriteBlock = pool->AllocateBlock();
    5.                 currentWriteBlock->m_NextBlock = null;
    6.                 currentWriteBlock->m_NumItems = 0;
    7.                 currentWriteBlock->m_NumReaders = 1; //<----Here
    8.                 NativeQueueBlockHeader* prevLast = (NativeQueueBlockHeader*)Interlocked.Exchange(ref data->m_LastBlock, (IntPtr)currentWriteBlock);
    9.  
    10.                 if (prevLast == null)
    11.                 {
    12.                     data->m_FirstBlock = (IntPtr)currentWriteBlock;
    13.                 }
    14.                 else
    15.                 {
    16.                     prevLast->m_NextBlock = currentWriteBlock;
    17.                 }
    18.  
    19.                 data->SetCurrentWriteBlockTLS(threadIndex, currentWriteBlock);
    20.             }
    Now, when you call Dequeue(), internally it calls TryDequeue, which in turn:
    1. calls GetFirstBlock()
    2. reads its contents.
    3. calls Release() on the first block.
    At the end of GetFirstBlock(), right before returning the block, there's this:

    Code (CSharp):
    1. Interlocked.Increment(ref firstBlock->m_NumReaders);
    Now m_NumReaders is 2.

    After reading the block's contents , Release() is called, which will use FreeBlock() if this condition is met:
    Code (CSharp):
    1. if (0 == Interlocked.Decrement(ref block->m_NumReaders))
    But the decrement only gets m_NumReaders to 1, the condition is not met, the block never freed, and so there's a leak.

    I'm not sure how to fix this myself without breaking the rest of the queue, but I hope it helps.
     
    Last edited: Jan 23, 2020
  3. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,929
    futurlab_xbox likes this.
  4. Fabrice_Lete

    Fabrice_Lete

    Unity Technologies

    Joined:
    May 5, 2018
    Posts:
    55
    Hi, thanks for the detailed analysis!
    But it looks like this bug has been fixed in the following collections package [0.2.0] - 2019-11-22
    This package requires Unity 2019.3 0b11+ could it be that you're using an older version?
    I believe the bug on issuetracker should have been closed when the fix landed, for some reason that didn't happen and we're following up on that.
     
    futurlab_xbox likes this.