Multiple Serial Ports CPU usage on Windows

Hi everyone !

I’m facing an issue by switching from Mac (XCode) to Windows (VS19).

I’ve developped an audio app which is connected to 18 usb serial ports, with respectively for each one an SerialPortInputStream and SerialPortOutputStream objects.

On mac averything work fine with the connectivity, the performances of the app when idle after launch show a very low CPU consumption.

However, by compiling on VS19 the same exact code, the CPU usage is proportional to the number of serial ports connected. Indeed, the CPU consumption on Windows with no Serial Port is equivalent to the one on OSX with all the SerialPorts connected.
On Windows the CPU stays between 95 and 100 % from the app launch, which is, as you can imagine, a big problem for app use.

I guess the difference stands in the way both OS deal with Serial Port communications.

Does anyone has an idea about how to reduce this CPU usage ?

Do I have to change anything in the windows setup or IDE configuration ?

Thanks for your help !

JUCE doesn’t have any SerialPort classes, are you getting those from the code shared in this thread?

Yes I’m using these classes. Sorry I did not precise it.

While I host this code, it is old and crufty. I use it in a number of projects, and I usually wish for something that is maintained by someone else! lol. I don’t know if I will have anytime to debug, but I can look at the code and give advice. To start with, are you seeing an actual performance hit or just using the cpu usage? The serial port threads can spin when there is nothing going on, which will be cpu usage, but not a performance hit. Second, can you test with only inputs or outputs to determine if one of those threads is what is causing the issue? Lastly doing some performance profiling would be helpful, so we can know where the actual time is being spent.

The main problem is the Thread implementation.
You must use WaitForMultipleEvents for Events and characters with overlapped in paralell.

Timeout for ReadIntervalTimeout = 10ms. All other values to 0.

After someon is signalled use GetOverlappedResult to return the number of characters read.
DO NOT read 1 character !!.
This make no sense. In kernel RX interrupt more bytes transferred (FIFO Size) and immediatly signalled to USER mode RX thread.

elli

Thanks for the pointers @elli . I did not write this code, so I can’t really comment on the intentions behind implementation, but, this is generic code, and the library cannot know how many characters the client wants, and it may be 1. My assumption has been that one of the reasons is for balancing this with performance and monitoring for time-outs. As well, there is a thread per serial port, so WaitForMultipleEvents can’t be used. If I had the spare time I would look at how we might improve this code, but that isn’t possible at this point, modulo relatively simple bug fixes. But, if you want to offer up some code as a starting point, I would gladly look at that . :slight_smile:

I Understand, but here is general blocker for many people, I know that, I have had many customer and saw many implementations. Communication over a serial port is always ASYNC and it make no sense for an application to read blocked some characters. Characters are signalled from a wrapper layer when someone is avail and the application must consumes this async. Do not make any assumptions how
many characters next avail. This is a bad design. A serial line is no error free.

WaitForMultipleEvents can be used in a thread, My implementaton read 3 events. One to terminate the thread in a safe way, one for line event signalisation and one for characters.
My problem tomorror I start my vacation and so I have no more time next weeks, so I can get only some pseudo code to illustrate what I mean.

HANDLE handlesToWaitFor[3];
 
handlesToWaitFor[0] = overlappedRead.hEvent;
handlesToWaitFor[1] = overlappedComm.hEvent;
handlesToWaitFor[2] = hCloseEvent;
  
COMMTIMEOUTS comTimeOuts;

comTimeOuts.ReadIntervalTimeout         = 10;
comTimeOuts.ReadTotalTimeoutMultiplier  = 0;
comTimeOuts.ReadTotalTimeoutConstant    = 0 ;
comTimeOuts.WriteTotalTimeoutMultiplier = 0 ;
comTimeOuts.WriteTotalTimeoutConstant   = 0 ;

::SetCommTimeouts( hDevice, &comTimeOuts ) ;

while ( bRxThreadRunning )
{
   if ( ! fWaitingOnRead)
   {
     bool result =  ::ReadFile( hDevice,
				   rxBuffer,
				   rxBufferSize,
				   &ulNumberOfBytesRead,
				   &overlappedRead
                  };
    if ( result )
    {
		signal RX
    }    
    else
    {
		fWaitingOnRead = true;
    }
  }
  
   if ( ! fWaitingOnStatus)
   {
      
       ::WaitCommEvent(hDevice, &ulEventMask, &overlappedComm))
       ...
   }
          
          
    if ( fWaitingOnStatus && fWaitingOnRead )
    {
     //
     // Wait until some event occurs (data to read; error; stopping).
     //

      ulHandleSignaled = ::WaitForMultipleObjects(3, handlesToWaitFor, FALSE, INFINITE);
      switch(ulHandleSignaled)
      {
       case WAIT_OBJECT_0:
         if ( ::GetOverlappedResult( hDevice, &overlappedRead, &ulNumberOfBytesRead, FALSE )
         {
			signal RX
         }
         else
         {
          ...
         }
         break;
         
        case WAIT_OBJECT_0 + 1: // status signaled.
           ::GetOverlappedResult( hDevice, &overlappedComm, &ulOvResult, FALSE);
           ...
           break;
           
         case WAIT_OBJECT_0 + 2:     // Signal to end the thread.  
           bRxThreadRunning = FALSE;
           break;
    }
                  
}


I have written serial drivers for windows and knows what is going on from hardware interrupt until this character is consumed from application.

regards
elli

Thanks for the code. I will look at it in the next few days.

But, to be clear, based on your statement, "and it make no sense for an application to read blocked some characters" this library does not block the application for i/o operations. The read/write threads operate on a buffer. the client side read function just copies any data from the buffer without blocking on the read. although it can block on the copy operation from the read thread, but this is only during the copy, not the serial read. The client side write functions simply copy into a buffer, and the actual serial write function picks it up from there, again only blocking for the time to copy the data BEFORE sending.

The cpu usage the person is seeing is not because of client code being blocked. I suspect the cpu usage being observed is not causing any major performance issues, but instead is just representing spinning going on in the threads.

But, having said that, as I indicated, I will look at what you hav sent.