Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Android TCPclient / Garbage Collector : lag issue

Discussion in 'Scripting' started by FL53, Nov 28, 2018.

  1. FL53

    FL53

    Joined:
    Nov 28, 2018
    Posts:
    14
    Hi,

    I wrote a C++ TCP server on my computer which sends data at 100Hz (let's say X Hz since it can be modified easily).
    I wrote a TCPclient in C# in my app using Network stream to get the stream bytes (each packet send has a header and store next how many bytes are to be read). I used a thread (not asynchronous socket so) to dump the stream into a temporary buffer which is used in the main thread. The main thread is consuming the buffer at its own frequency and may process none, single or multiple packets.

    Testing on my computer everything goes well ! On another computer, same thing ... but trying to do that in Android device begins to raise an issue. "Lag is coming" : let's say it is working pretty well for some time and periodically a frame takes much more time to draw. Then the buffer has grown and there are too much packets to process in the next frame (which delays the frame and makes the buffer grow again :) ) ... I think this is a Garbage Collector issue but cannot find a way to fix that.

    I tried to "clean" my code and comment it to show the client side (Unity) of my development. You will it below:
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Net;
    5. using System.Net.Sockets;
    6. using System.Text;
    7. using System.IO;
    8. using System.Threading;
    9. using UnityEngine;
    10. using System.Runtime.InteropServices;
    11.  
    12.  
    13. public class TCP_client : MonoBehaviour
    14. {  
    15.    
    16.     // To manage the application quit event for the receiving thread
    17.     private bool m_quit = false;
    18.  
    19.    
    20.     // To process the packets
    21.     private const byte MSG_HEADER = 0x11;        // HEADER code
    22.     private const byte MSG_ID1 = 0x01;            // MSG1 code
    23.     private const byte MSG_ID2 = 0x02;            // MSG2 code
    24.    
    25.     public string m_serverName = "192.168.1.1";    // server IP
    26.     private const int TCP_PORT = 1234;            // communication port
    27.     private TcpClient socketConnection;            // TCP client object
    28.     private Thread rcvThread;                    // receiving thread
    29.     private Mutex m_mutex = new Mutex();        // mutex to manage shared data between main thread and receiving thread
    30.     private const int m_readTimeout = 1000;    // timeout for reading on socket
    31.    
    32.     // Buffers to store the stream bytes waiting the main thread to get it (kind of double buffer as for screen display)
    33.     // To avoid Garbage Collector issue I tried to make memory allocation using Marshal.AllocHGlobal ... 1Mo for each buffer !
    34.     IntPtr m_queue = Marshal.AllocHGlobal(524288);
    35.     int m_sizeQueue = 0;
    36.     IntPtr m_queueToProcess = Marshal.AllocHGlobal(524288);
    37.     int m_sizeQueueToProcess = 0;
    38.    
    39.     // to get the stream buffer inside the receiving thread
    40.     private byte[] bytesThread = new byte[1024];
    41.    
    42.     // To manage the stream inside the main thread
    43.     // Buffers are allocated there to avoid Garbage Collector issues on Android (but it does not avoid it :( )
    44.     private byte[] bytesTmp = new byte[5124288];        // array copied from the receiving thread
    45.     private byte[] concatenated = new byte[1048576];    // array using lasting bytes from bytes array and new bytes from bytes (cf. next line) to generate a new byte stream to process
    46.     private byte[] bytes = new byte[1048576];            // array used to process the packets (copy from bytesTmp or concatenated array regarding if concatenation is needed)
    47.     private bool concatenate_packet = false;            // should the main thread concatenate bytes & bytesTmp ?
    48.     private int length = 0;                                // length of buffer in bytes array
    49.     private int offset = 0;                                // offset in the buffer while processing
    50.  
    51.    
    52.    
    53.    
    54.     // receiving : called once, looping "forever" until application quits
    55.     private void rcv()
    56.     {
    57.      
    58.         IntPtr begin;    // offset pointer to fill the buffer incrementally
    59.  
    60.         while (!m_quit) // Loop if application is still running
    61.         {
    62.            
    63.             try
    64.             {
    65.                
    66.                 socketConnection = new TcpClient(m_serverName, TCP_PORT);    // connect the client to the server
    67.                 socketConnection.ReceiveTimeout = m_readTimeout;            // set a timeout on read so we can detect disconnection from server (server should send data at regular frequency)
    68.                 socketConnection.NoDelay = true;                            // ask for no delay (only to send messages quickly, not use in this example)
    69.  
    70.                 NetworkStream stream = socketConnection.GetStream();        // get the stream to sore the tcp packets into the buffer
    71.  
    72.                 int lengthThread = 0;    // length of current buffer in the stream
    73.  
    74.                 while (!m_quit)
    75.                 {
    76.                     lengthThread = stream.Read(bytesThread, 0, bytesThread.Length);        // store the stream buffer into bytesThread (allocated only once to avoid GC issues, I though !)
    77.                     if (lengthThread == 0)
    78.                     {
    79.                         if (socketConnection != null)
    80.                         {
    81.                             socketConnection.Close();
    82.                             socketConnection = null;
    83.                         }
    84.                         break; // Socket connection has been lost, try to connect again
    85.                     }
    86.  
    87.                     // modify the shared buffer > mutex
    88.                     m_mutex.WaitOne();
    89.                     {
    90.                         begin = (IntPtr)(m_queue.ToInt64() + m_sizeQueue);    // offset in the buffer
    91.                         Marshal.Copy(bytesThread, 0, begin, lengthThread);    // copy stream buffer into m_queue buffer
    92.                         m_sizeQueue += lengthThread;                        // update queue size
    93.                     }
    94.                     m_mutex.ReleaseMutex();
    95.                 }
    96.  
    97.             }
    98.             catch (Exception e)
    99.             {
    100.                 Debug.Log("Exception : " + e);
    101.  
    102.                 // If exception is raised : read issue on socket (timeout for instance), disconnection (server was killed for instance) >>>> delete socket & try to reconnect
    103.                 if (socketConnection != null)
    104.                 {
    105.                     socketConnection.Close();
    106.                     socketConnection = null;
    107.                 }
    108.  
    109.             }
    110.         }
    111.        
    112.         // while quitting if socket is not closed, do it
    113.         if (socketConnection != null)
    114.             socketConnection.Close();
    115.         socketConnection=null;
    116.  
    117.  
    118.     }
    119.    
    120.    
    121.     // Use this for initialization    
    122.     void Start()
    123.     {
    124.         // no screen saver on Android device !
    125.         Screen.sleepTimeout = SleepTimeout.NeverSleep;
    126.    
    127.         // create the receiving thread
    128.         try
    129.         {
    130.             rcvThread = new Thread(new ThreadStart(rcv));
    131.             rcvThread.IsBackground = false;
    132.             rcvThread.Start();
    133.         }
    134.         catch (Exception e)
    135.         {
    136.             Debug.Log("Exception creating the receiving thread : " + e);
    137.         }
    138.     }
    139.  
    140.     private void OnApplicationQuit()
    141.     {
    142.         // ask the thread to quit
    143.         m_quit = true;
    144.        
    145.         Thread.Sleep(m_readTimeout*2);
    146.        
    147.         // free the manually allocated buffers
    148.         Marshal.FreeHGlobal(m_queue);
    149.         Marshal.FreeHGlobal(m_queueToProcess);
    150.     }
    151.  
    152.  
    153.     // Update is called once per frame
    154.     void Update()
    155.     {
    156.         // use mutex to get the buffer received since last Update call
    157.         m_mutex.WaitOne();
    158.         {
    159.             // swap buffers to process one while filling the other in the receiving thread
    160.             IntPtr tmp    = m_queueToProcess;
    161.             m_queueToProcess = m_queue;
    162.             m_queue = tmp;
    163.             m_sizeQueueToProcess = m_sizeQueue;    // get the numbers of bytes to process
    164.             m_sizeQueue = 0;                    // tell the receiving thread to fill the buffer from the start
    165.         }
    166.         m_mutex.ReleaseMutex();
    167.  
    168.  
    169.         // Process the buffer  if not empty
    170.         if (m_sizeQueueToProcess > 0)
    171.         {
    172.             // copy the buffer into a bytes array (bytesTmp)
    173.             Marshal.Copy(m_queueToProcess, bytesTmp, 0, m_sizeQueueToProcess);
    174.  
    175.             // Check if concatenation is needed (if concatenation has been asked in previous Update call)
    176.             if (concatenate_packet && offset != length)
    177.             {
    178.                 // copy bytes to process from bytes (last buffer) at the beginning of concatenated array
    179.                 Buffer.BlockCopy(bytes, offset, concatenated, 0, length - offset);
    180.                 // Copy the new buffer (bytesTmp) after
    181.                 Buffer.BlockCopy(bytesTmp, 0, concatenated, length - offset, m_sizeQueueToProcess);
    182.                 // copy the resulting array into bytes
    183.                 Buffer.BlockCopy(concatenated, 0, bytes, 0, length - offset + m_sizeQueueToProcess);
    184.  
    185.                 // get the "concatenated size"
    186.                 length = m_sizeQueueToProcess + length - offset;
    187.  
    188.             }
    189.             else
    190.             {
    191.                 // copy the buffer (bytesTmp) into bytes
    192.                 Buffer.BlockCopy(bytesTmp,0,bytes,0,m_sizeQueueToProcess);
    193.                 // set the buffer length
    194.                 length = m_sizeQueueToProcess;
    195.             }
    196.             // no need to concatenate now ... may be modified in next loop
    197.             concatenate_packet = false;
    198.             // start from the beginning of array
    199.             offset = 0;
    200.  
    201.  
    202.          
    203.             // loop on the buffer to find packets (we can do at least 1 loop since the buffer is not empty)
    204.             do
    205.             {
    206.                 // check the header
    207.                 if (bytes[offset] != MSG_HEADER)
    208.                 {
    209.                     offset++;
    210.                     continue;                     // search header in next byte (should not happen ... since we are using TCP all the packets should arrive in order with first byte = MSG_HEADER)
    211.                 }
    212.                 // a packet is defined like this : [HEADER]   [   ID   ] [NB_BYTES ] [NB_BYTES bytes to process]
    213.                 //                                 [ 1 byte ] [ 1 byte ] [ 4 bytes ] [  NB_BYTES bytes         ]
    214.                
    215.                 if (length - offset <= 6)        // not enough bytes : it is only a part of a packet, no need to continue processing, wait for more data to complete the packet
    216.                 {
    217.                     concatenate_packet = true;
    218.                     break;
    219.                 }
    220.                
    221.                 // Look at the first byte after header to know which message is to be processed
    222.                 byte nbBytes = bytes[offset + 1]; // ID OF MESSAGE
    223.                 int nbBytes = BitConverter.ToInt32(offset + 2);
    224.                 if (size > length - offset + 2)     // not enough bytes : it is only a part of a packet, no need to continue processing, wait for more data to complete the packet
    225.                 {
    226.                     concatenate_packet = true;
    227.                     break;
    228.                 }
    229.  
    230.                 offset+=6;
    231.                
    232.                 switch (msgId)
    233.                 {
    234.                     case MSG_ID1: // 100Hz
    235.                         // process the message
    236.                         // Do what you want there
    237.                         while(nbBytes>0)
    238.                         {
    239.                             //Debug.Log(bytes[offset]);
    240.                            
    241.                             nbBytes--;
    242.                             offset ++;
    243.                         }
    244.                         break;
    245.                     case MSG_ID2: // only when button is pressed on server
    246.                         // process the message
    247.                         nbBytes -= nbBytes;
    248.                         offset  +=nbBytes;
    249.                         break;
    250.  
    251.                     default:
    252.                         break;
    253.                 }
    254.                
    255.             } while (!concatenate_packet && offset < length);
    256.         }
    257.  
    258.     }
    259.  
    260.  
    261. }
    Reading this, have you got an idea why this lag is appearing and how to make it disappear ?

    Regards
     
  2. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    Well it looks to me like your socket thread is going to hit 100% CPU usage on whatever core it is running on. That could be a problem depending on your android device. Other than that, your description sounds like your frame rate is too low on the main thread, rather than an issue on your socket thread. What does the profiler say? Can you measure how long m_mutex.WaitOne() is taking in Update? I'm not sure calling something that will block the main thread like that is a good idea.
     
  3. FL53

    FL53

    Joined:
    Nov 28, 2018
    Posts:
    14
    I did not measured the time of mutex but I will do it asap. It should not take long since the mutex is done only when copying the data received in socket thread, read method is bocking socket thread so when no new data is available main thread enter directly into the mutex section, else the main thread is only blocked until data is copied into the buffer and it "consumes" these data next.

    Main & socket threads seem to work together pretty well: socket thread is producing data and main thread is consuming them as soon as it can ... but lag is coming (seems to be on main AND socket thread looking at my log, that's why I though it is Garbage collector) and it is quite annoying since network packets are used to manage tracking of the head of the user and modify the camera transform/projection matrix. If I can "delete" the lag everything will go well, experience will be fluid !