#1506 Resolves multiple performance issues: tuner freezing after a period of run time, buffer overflows, degraded audio playback. (#1513)

-Reduces message and event histories per channel from 500 to 200.
-Resolves issue of tuner freezing due to cancelled USB transfer buffers not being resubmitted.
-Reworks the audio playback system to remove in-progress call preemption and remove thread locks which can cause glitchy behavior during audio playback.
-Updates audio outputs to also use single thread pool for processing audio segments.
-Updates MBECallSequenceConverter to batch convert MBE to WAV audio.
-Unifies recording filename prefixes across baseband, mbe and binary recordings.
-Resolves issue with registering heartbeat manager to dispatcher.
-Uses single-thread thread pools for Dispatcher to support batch & process to overcome context switching consistency issue in Windows.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2023-04-11 04:07:41 -04:00
committed by GitHub
parent 32a8363b45
commit 372d2ff870
34 changed files with 802 additions and 1112 deletions

View File

@@ -18,52 +18,70 @@
*/
package io.github.dsheirer.util;
import io.github.dsheirer.controller.NamingThreadFactory;
import io.github.dsheirer.sample.Listener;
import java.util.concurrent.LinkedBlockingQueue;
import io.github.dsheirer.source.heartbeat.HeartbeatManager;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Threaded processor for receiving elements from a separate producer thread and forwarding those buffers to a
* registered listener on this consumer/dispatcher thread.
* Threaded scheduled processor for receiving elements from a separate producer thread and forwarding those buffers to a
* registered listener on this consumer/dispatcher thread. Internally uses a single-thread thread pool to effect a
* timer-based interval for processing to avoid excessive context switching inherent in a blocking queue. Sizes the
* thread pool to a single thread to ensure Garbage Collector can efficiently clean objects created on the thread.
*/
public class Dispatcher<E> implements Listener<E>
{
private final static Logger mLog = LoggerFactory.getLogger(Dispatcher.class);
private static final long OVERFLOW_LOG_EVENT_WAIT_PERIOD = TimeUnit.SECONDS.toMillis(10);
private LinkedBlockingQueue<E> mQueue;
private LinkedTransferQueue<E> mQueue = new LinkedTransferQueue<>();
private Listener<E> mListener;
private AtomicBoolean mRunning = new AtomicBoolean();
private String mThreadName;
private Thread mThread;
private E mPoisonPill;
private long mLastOverflowLogEvent;
private ScheduledExecutorService mExecutorService;
private ScheduledFuture<?> mScheduledFuture;
private int mBatchSize;
private long mInterval;
private HeartbeatManager mHeartbeatManager;
/**
* Constructs an instance
* @param maxSize of the internal queue
* Constructs an instance of a Dispatcher with integrated heartbeat support.
* @param threadName to name the dispatcher thread
* @param poisonPill of type E, used to kill the thread
* @param batchSize is maximum number of elements to process from the queue per interval. This should be sized to
* the anticipated number of elements per second, divided by the number of intervals per second and increased by
* a factor of two. This serves to constrain how many elements are processed per interval when the queue starts
* to back up, so that the thread doesn't dominate CPU resources.
* @param interval for processing each batch in milliseconds.
* @param heartbeatManager to receive a heartbeat command at each processing interval.
*/
public Dispatcher(int maxSize, String threadName, E poisonPill)
public Dispatcher(String threadName, int batchSize, long interval, HeartbeatManager heartbeatManager)
{
if(poisonPill == null)
{
throw new IllegalArgumentException("Poison pill must be non-null");
}
mQueue = new LinkedBlockingQueue<>(maxSize);
mThreadName = threadName;
mPoisonPill = poisonPill;
this(threadName, batchSize, interval);
mHeartbeatManager = heartbeatManager;
}
/**
* Listener to receive the queued buffers each time this processor runs.
* Constructs an instance
* @param threadName to name the dispatcher thread
* @param batchSize is maximum number of elements to process from the queue per interval. This should be sized to
* the anticipated number of elements per second, divided by the number of intervals per second and increased by
* a factor of two. This serves to constrain how many elements are processed per interval when the queue starts
* to back up, so that the thread doesn't dominate CPU resources.
* @param interval for processing each batch in milliseconds.
*/
protected Listener<E> getListener()
public Dispatcher(String threadName, int batchSize, long interval)
{
return mListener;
mThreadName = threadName;
mBatchSize = batchSize;
mInterval = interval;
}
/**
@@ -86,16 +104,7 @@ public class Dispatcher<E> implements Listener<E>
{
if(mRunning.get())
{
if(!mQueue.offer(e))
{
if(System.currentTimeMillis() > (mLastOverflowLogEvent + OVERFLOW_LOG_EVENT_WAIT_PERIOD))
{
mLastOverflowLogEvent = System.currentTimeMillis();
mLog.warn("Dispatcher - temporary buffer overflow for thread [" + mThreadName + "] - throwing away samples - " +
" processor flag:" + (mRunning.get() ? "running" : "stopped") +
" thread:" + (mThread != null ? (mThread.isAlive() ? mThread.getState() : "dead") : "null" ));
}
}
mQueue.add(e);
}
}
@@ -106,10 +115,22 @@ public class Dispatcher<E> implements Listener<E>
{
if(mRunning.compareAndSet(false, true))
{
mThread = new Thread(new Processor());
mThread.setName(mThreadName);
mThread.setPriority(Thread.MAX_PRIORITY);
mThread.start();
if(mScheduledFuture != null)
{
mScheduledFuture.cancel(true);
}
if(mExecutorService != null)
{
mExecutorService.shutdown();
mExecutorService = null;
}
mQueue.clear();
mExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(mThreadName));
Runnable r = (mHeartbeatManager != null ? new ProcessorWithHeartbeat() : new Processor());
mScheduledFuture = mExecutorService.scheduleAtFixedRate(r, 0, mInterval, TimeUnit.MILLISECONDS);
}
}
@@ -120,29 +141,21 @@ public class Dispatcher<E> implements Listener<E>
{
if(mRunning.compareAndSet(true, false))
{
mQueue.offer(mPoisonPill);
try
if(mScheduledFuture != null)
{
mThread.interrupt();
mThread.join();
mThread = null;
mScheduledFuture.cancel(true);
mScheduledFuture = null;
mQueue.clear();
}
catch(Exception e)
if(mExecutorService != null)
{
mLog.error("Timeout while waiting to join terminating buffer processor thread");
mExecutorService.shutdown();
mExecutorService = null;
}
}
}
/**
* Finishes processing any queued buffers and then stops the processing thread.
*/
public void flushAndStop()
{
mQueue.offer(mPoisonPill);
}
/**
* Indicates if this processor is currently running
*/
@@ -151,56 +164,75 @@ public class Dispatcher<E> implements Listener<E>
return mRunning.get();
}
/**
* Processes elements from the queue. Note: this should only be invoked on the Processor thread.
*/
private void process()
{
List<E> elements = new ArrayList<>();
mQueue.drainTo(elements, mBatchSize);
for(E element: elements)
{
if(mRunning.get() && mListener != null)
{
try
{
mListener.receive(element);
}
catch(Throwable t)
{
mLog.error("Error while dispatching element [" + element.getClass() + "] to listener [" +
mListener.getClass() + "]", t);
}
}
}
}
/**
* Processor to service the buffer queue and distribute the buffers to the registered listener
*/
class Processor implements Runnable
{
private AtomicBoolean mRunning = new AtomicBoolean();
@Override
public void run()
{
try
if(mRunning.compareAndSet(false, true))
{
mQueue.clear();
process();
mRunning.set(false);
}
}
}
E element;
/**
* Processor to service the buffer queue and distribute the buffers to the registered listener. Includes a
* support for commanding a heart beat with each processing interval.
*/
class ProcessorWithHeartbeat implements Runnable
{
private AtomicBoolean mRunning = new AtomicBoolean();
while(mRunning.get())
@Override
public void run()
{
if(mRunning.compareAndSet(false, true))
{
process();
try
{
try
{
element = mQueue.take();
if(mPoisonPill == element)
{
mRunning.set(false);
}
else if(element != null)
{
if(mListener == null)
{
throw new IllegalStateException("Listener for [" + mThreadName + "] is null");
}
mListener.receive(element);
}
}
catch(InterruptedException e)
{
//Normal shutdown is by interrupt
mRunning.set(false);
}
catch(Exception e)
{
mLog.error("Error while processing element", e);
}
mHeartbeatManager.broadcast();
}
catch(Throwable t)
{
mLog.error("Error broadcasting heartbeat during Dispatcher processing interval", t);
}
//Shutting down - clear the queue
mQueue.clear();
}
catch(Throwable t)
{
mLog.error("Unexpected error thrown from the Dispatcher thread", t);
mRunning.set(false);
}
}
}