Merge pull request #1695 from DSheirer/1694-sdrplay-rsp-tuner-memory-leaks

#1694 SDRPlay RSP Memory Leak & Audio Playback Delays
This commit is contained in:
Denny Sheirer
2023-11-05 04:26:48 -05:00
committed by GitHub
13 changed files with 90 additions and 109 deletions

View File

@@ -68,8 +68,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
*/ */
private static final int PROCESSED_CHANNEL_RESULTS_THRESHOLD = 1024; private static final int PROCESSED_CHANNEL_RESULTS_THRESHOLD = 1024;
//Sized to process 1/20th batch of 152 buffers a second, at 40 times per second //Sized to process 40 times per second
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(152 / 20, 25); private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(25);
private FloatFFT_1D mFFT; private FloatFFT_1D mFFT;
private float[] mInlineSamples; private float[] mInlineSamples;
private float[] mInlineFilter; private float[] mInlineFilter;
@@ -406,9 +406,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
*/ */
public class IFFTProcessorDispatcher extends Dispatcher<List<float[]>> public class IFFTProcessorDispatcher extends Dispatcher<List<float[]>>
{ {
public IFFTProcessorDispatcher(int batchSize, long interval) public IFFTProcessorDispatcher(long interval)
{ {
super("sdrtrunk polyphase ifft processor", batchSize, interval); super("sdrtrunk polyphase ifft processor", interval);
//We create a listener interface to receive the batched channel results arrays from the scheduled thread pool //We create a listener interface to receive the batched channel results arrays from the scheduled thread pool
//dispatcher thread that is part of this continuous buffer processor. We perform an IFFT on each //dispatcher thread that is part of this continuous buffer processor. We perform an IFFT on each

View File

@@ -110,7 +110,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
} }
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING); mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 50, 10); mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 10);
mBufferDispatcher.setListener(mNativeBufferReceiver); mBufferDispatcher.setListener(mNativeBufferReceiver);
} }

View File

@@ -50,8 +50,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
mInputChannelCount = inputChannelCount; mInputChannelCount = inputChannelCount;
//Process 1/10th of the sample rate per second at a rate of 20 times a second (200% of anticipated rate) //Process 1/10th of the sample rate per second at a rate of 20 times a second (200% of anticipated rate)
mHeartbeatManager = heartbeatManager; mHeartbeatManager = heartbeatManager;
mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel", (int)(sampleRate / 10), mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel",50, mHeartbeatManager);
50, mHeartbeatManager);
mChannelResultsDispatcher.setListener(floats -> { mChannelResultsDispatcher.setListener(floats -> {
try try
{ {

View File

@@ -49,7 +49,7 @@ public class BinaryRecorder extends Module implements IByteBufferListener
private final static Logger mLog = LoggerFactory.getLogger(BinaryRecorder.class); private final static Logger mLog = LoggerFactory.getLogger(BinaryRecorder.class);
private static final int MAX_RECORDING_BYTE_SIZE = 524288; //500 kB private static final int MAX_RECORDING_BYTE_SIZE = 524288; //500 kB
private Dispatcher<ByteBuffer> mBufferProcessor = new Dispatcher<>("sdrtrunk binary recorder", 200, 250); private Dispatcher<ByteBuffer> mBufferProcessor = new Dispatcher<>("sdrtrunk binary recorder", 250);
private AtomicBoolean mRunning = new AtomicBoolean(); private AtomicBoolean mRunning = new AtomicBoolean();
private Path mBaseRecordingPath; private Path mBaseRecordingPath;
private String mRecordingIdentifier; private String mRecordingIdentifier;

View File

@@ -44,7 +44,7 @@ public class ComplexSamplesWaveRecorder extends Module implements IComplexSample
{ {
private final static Logger mLog = LoggerFactory.getLogger(ComplexSamplesWaveRecorder.class); private final static Logger mLog = LoggerFactory.getLogger(ComplexSamplesWaveRecorder.class);
private Dispatcher<ComplexSamples> mBufferProcessor = new Dispatcher<>("sdrtrunk complex wave recorder", 100, 250); private Dispatcher<ComplexSamples> mBufferProcessor = new Dispatcher<>("sdrtrunk complex wave recorder", 250);
private AtomicBoolean mRunning = new AtomicBoolean(); private AtomicBoolean mRunning = new AtomicBoolean();
private BufferWaveWriter mWriter; private BufferWaveWriter mWriter;
private String mFilePrefix; private String mFilePrefix;

View File

@@ -47,7 +47,7 @@ public class NativeBufferWaveRecorder extends Module implements Listener<INative
private static final Logger mLog = LoggerFactory.getLogger(ComplexSamplesWaveRecorder.class); private static final Logger mLog = LoggerFactory.getLogger(ComplexSamplesWaveRecorder.class);
private static final long STATUS_UPDATE_BYTE_INTERVAL = 1_048_576; private static final long STATUS_UPDATE_BYTE_INTERVAL = 1_048_576;
private static final long MAX_RECORDING_SIZE = (long)Integer.MAX_VALUE * 2l; private static final long MAX_RECORDING_SIZE = (long)Integer.MAX_VALUE * 2l;
private Dispatcher<INativeBuffer> mBufferProcessor = new Dispatcher<>("sdrtrunk native buffer wave recorder", 100, 250); private Dispatcher<INativeBuffer> mBufferProcessor = new Dispatcher<>("sdrtrunk native buffer wave recorder", 250);
private AtomicBoolean mRunning = new AtomicBoolean(); private AtomicBoolean mRunning = new AtomicBoolean();
private NativeBufferWaveWriter mWriter; private NativeBufferWaveWriter mWriter;

View File

@@ -74,8 +74,7 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
mQDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimation); mQDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimation);
//Set dispatcher to process 1/10 of estimated sample arrival rate, 20 times per second (up to 200% per interval) //Set dispatcher to process 1/10 of estimated sample arrival rate, 20 times per second (up to 200% per interval)
mBufferDispatcher = new Dispatcher("sdrtrunk heterodyne channel " + tunerChannel.getFrequency(), mBufferDispatcher = new Dispatcher("sdrtrunk heterodyne channel " + tunerChannel.getFrequency(), 50, getHeartbeatManager());
(int)(sampleRate / 10), 50, getHeartbeatManager());
mBufferDispatcher.setListener(new NativeBufferProcessor()); mBufferDispatcher.setListener(new NativeBufferProcessor());
//Setup the frequency mixer to the current source frequency //Setup the frequency mixer to the current source frequency

View File

@@ -55,7 +55,7 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
super(listener, tunerChannel); super(listener, tunerChannel);
mTunerController = tunerController; mTunerController = tunerController;
mBufferDispatcher = new Dispatcher<>("sdrtrunk pass-through channel " + tunerChannel.getFrequency(), mBufferDispatcher = new Dispatcher<>("sdrtrunk pass-through channel " + tunerChannel.getFrequency(),
250, 50, getHeartbeatManager()); 50, getHeartbeatManager());
mBufferDispatcher.setListener(new BufferProcessor()); mBufferDispatcher.setListener(new BufferProcessor());
} }

View File

@@ -70,7 +70,7 @@ public class SDRplay
private static final Logger mLog = LoggerFactory.getLogger(SDRplay.class); private static final Logger mLog = LoggerFactory.getLogger(SDRplay.class);
/** /**
* Foreign memory arenaallocation resource scope * Foreign memory arena allocation resource scope
*/ */
private final Arena mArena = Arena.openShared(); private final Arena mArena = Arena.openShared();

View File

@@ -37,24 +37,24 @@ public class CallbackFunctions
/** /**
* Constructs a callback functions for single-tuner use. * Constructs a callback functions for single-tuner use.
* @param arena for native memory allocation * @param longLifecycleArena for native memory allocation for long lifecycle objects.
* @param deviceEventListener for device events * @param deviceEventListener for device events
* @param streamListener for streaming samples * @param streamListener for streaming samples
* @param streamCallbackListener for streaming events * @param streamCallbackListener for streaming events
*/ */
public CallbackFunctions(Arena arena, IDeviceEventListener deviceEventListener, public CallbackFunctions(Arena longLifecycleArena, IDeviceEventListener deviceEventListener,
IStreamListener streamListener, IStreamCallbackListener streamCallbackListener) IStreamListener streamListener, IStreamCallbackListener streamCallbackListener)
{ {
//Create the event callback function //Create the event callback function
mDeviceEventAdapter = new DeviceEventAdapter(arena, deviceEventListener); mDeviceEventAdapter = new DeviceEventAdapter(deviceEventListener);
MemorySegment eventFunction = sdrplay_api_EventCallback_t.allocate(mDeviceEventAdapter, arena.scope()); MemorySegment eventFunction = sdrplay_api_EventCallback_t.allocate(mDeviceEventAdapter, longLifecycleArena.scope());
//Create the stream A callback function //Create the stream A callback function
mStreamACallbackAdapter = new StreamCallbackAdapter(arena, streamListener, streamCallbackListener); mStreamACallbackAdapter = new StreamCallbackAdapter(streamListener, streamCallbackListener);
MemorySegment streamAFunction = sdrplay_api_StreamCallback_t.allocate(mStreamACallbackAdapter, arena.scope()); MemorySegment streamAFunction = sdrplay_api_StreamCallback_t.allocate(mStreamACallbackAdapter, longLifecycleArena.scope());
//Create the callback functions union and populate the callback functions //Create the callback functions union and populate the callback functions
mCallbackFunctionsMemorySegment = sdrplay_api_CallbackFnsT.allocate(arena); mCallbackFunctionsMemorySegment = sdrplay_api_CallbackFnsT.allocate(longLifecycleArena);
sdrplay_api_CallbackFnsT.EventCbFn$set(mCallbackFunctionsMemorySegment, eventFunction); sdrplay_api_CallbackFnsT.EventCbFn$set(mCallbackFunctionsMemorySegment, eventFunction);
sdrplay_api_CallbackFnsT.StreamACbFn$set(mCallbackFunctionsMemorySegment, streamAFunction); sdrplay_api_CallbackFnsT.StreamACbFn$set(mCallbackFunctionsMemorySegment, streamAFunction);
} }
@@ -72,15 +72,15 @@ public class CallbackFunctions
IStreamCallbackListener streamCallbackListener) IStreamCallbackListener streamCallbackListener)
{ {
//Create the event callback function //Create the event callback function
mDeviceEventAdapter = new DeviceEventAdapter(arena, deviceEventListener); mDeviceEventAdapter = new DeviceEventAdapter(deviceEventListener);
MemorySegment eventFunction = sdrplay_api_EventCallback_t.allocate(mDeviceEventAdapter, arena.scope()); MemorySegment eventFunction = sdrplay_api_EventCallback_t.allocate(mDeviceEventAdapter, arena.scope());
//Create the stream A callback function //Create the stream A callback function
mStreamACallbackAdapter = new StreamCallbackAdapter(arena, streamAListener, streamCallbackListener); mStreamACallbackAdapter = new StreamCallbackAdapter(streamAListener, streamCallbackListener);
MemorySegment streamAFunction = sdrplay_api_StreamCallback_t.allocate(mStreamACallbackAdapter, arena.scope()); MemorySegment streamAFunction = sdrplay_api_StreamCallback_t.allocate(mStreamACallbackAdapter, arena.scope());
//Create the stream B callback function //Create the stream B callback function
mStreamBCallbackAdapter = new StreamCallbackAdapter(arena, streamBListener, streamCallbackListener); mStreamBCallbackAdapter = new StreamCallbackAdapter(streamBListener, streamCallbackListener);
MemorySegment streamBFunction = sdrplay_api_StreamCallback_t.allocate(mStreamBCallbackAdapter, arena.scope()); MemorySegment streamBFunction = sdrplay_api_StreamCallback_t.allocate(mStreamBCallbackAdapter, arena.scope());
//Create the callback functions union and populate the callback functions //Create the callback functions union and populate the callback functions

View File

@@ -36,24 +36,14 @@ import org.slf4j.LoggerFactory;
public class DeviceEventAdapter implements sdrplay_api_EventCallback_t public class DeviceEventAdapter implements sdrplay_api_EventCallback_t
{ {
private static final Logger mLog = LoggerFactory.getLogger(DeviceEventAdapter.class); private static final Logger mLog = LoggerFactory.getLogger(DeviceEventAdapter.class);
private Arena mArena;
private IDeviceEventListener mDeviceEventListener; private IDeviceEventListener mDeviceEventListener;
/** /**
* Constructs an instance. * Constructs an instance.
* @param arena to use in creating foreign memory segments to access foreign structures.
* @param listener to receive translated device events. * @param listener to receive translated device events.
*/ */
public DeviceEventAdapter(Arena arena, IDeviceEventListener listener) public DeviceEventAdapter(IDeviceEventListener listener)
{ {
if(arena == null)
{
throw new IllegalArgumentException("Resource scope must be non-null");
}
mArena = arena;
setListener(listener); setListener(listener);
} }
@@ -75,35 +65,44 @@ public class DeviceEventAdapter implements sdrplay_api_EventCallback_t
public void apply(int eventTypeId, int tunerSelectId, MemorySegment eventParametersPointer, public void apply(int eventTypeId, int tunerSelectId, MemorySegment eventParametersPointer,
MemorySegment callbackContext) MemorySegment callbackContext)
{ {
MemorySegment memorySegment = sdrplay_api_EventParamsT.ofAddress(eventParametersPointer, mArena.scope()); try(Arena arena = Arena.openConfined())
EventType eventType = EventType.fromValue(eventTypeId);
TunerSelect tunerSelect = TunerSelect.fromValue(tunerSelectId);
switch(eventType)
{ {
case GAIN_CHANGE -> { MemorySegment memorySegment = sdrplay_api_EventParamsT.ofAddress(eventParametersPointer, arena.scope());
mDeviceEventListener.processGainChange(tunerSelect, EventType eventType = EventType.fromValue(eventTypeId);
EventParametersFactory.createGainCallbackParameters(memorySegment)); TunerSelect tunerSelect = TunerSelect.fromValue(tunerSelectId);
}
case POWER_OVERLOAD_CHANGE -> { switch(eventType)
mDeviceEventListener.processPowerOverload(tunerSelect, {
EventParametersFactory.createPowerOverloadCallbackParameters(memorySegment)); case GAIN_CHANGE ->
} {
case DEVICE_REMOVED -> { mDeviceEventListener.processGainChange(tunerSelect,
mDeviceEventListener.processDeviceRemoval(tunerSelect); EventParametersFactory.createGainCallbackParameters(memorySegment));
} }
case RSP_DUO_MODE_CHANGE -> { case POWER_OVERLOAD_CHANGE ->
mDeviceEventListener.processRspDuoModeChange(tunerSelect, {
EventParametersFactory.createRspDuoModeCallbackParameters(memorySegment)); mDeviceEventListener.processPowerOverload(tunerSelect,
} EventParametersFactory.createPowerOverloadCallbackParameters(memorySegment));
case UNKNOWN -> { }
mLog.warn("Unknown device event callback ignored. Please contact the library developer as this may " + case DEVICE_REMOVED ->
"indicate a change to the SDRPlay API change. Tuner:" + tunerSelect + " Event Type ID:" + {
eventTypeId); mDeviceEventListener.processDeviceRemoval(tunerSelect);
mDeviceEventListener.processEvent(eventType, tunerSelect); }
} case RSP_DUO_MODE_CHANGE ->
default -> { {
throw new IllegalStateException("DeviceEventAdapter must be updated handle EventType." + eventType); mDeviceEventListener.processRspDuoModeChange(tunerSelect,
EventParametersFactory.createRspDuoModeCallbackParameters(memorySegment));
}
case UNKNOWN ->
{
mLog.warn("Unknown device event callback ignored. Please contact the library developer as this may " +
"indicate a change to the SDRPlay API change. Tuner:" + tunerSelect + " Event Type ID:" +
eventTypeId);
mDeviceEventListener.processEvent(eventType, tunerSelect);
}
default ->
{
throw new IllegalStateException("DeviceEventAdapter must be updated handle EventType." + eventType);
}
} }
} }
} }

View File

@@ -35,24 +35,16 @@ import org.slf4j.LoggerFactory;
public class StreamCallbackAdapter implements sdrplay_api_StreamCallback_t public class StreamCallbackAdapter implements sdrplay_api_StreamCallback_t
{ {
private static final Logger mLog = LoggerFactory.getLogger(StreamCallbackAdapter.class); private static final Logger mLog = LoggerFactory.getLogger(StreamCallbackAdapter.class);
private Arena mArena;
private IStreamListener mStreamListener; private IStreamListener mStreamListener;
private IStreamCallbackListener mStreamCallbackListener; private IStreamCallbackListener mStreamCallbackListener;
/** /**
* Constructs an instance of the callback implementation * Constructs an instance of the callback implementation
* @param arena for defining new foreign memory segments
* @param streamListener to receive transferred I/Q samples and event details * @param streamListener to receive transferred I/Q samples and event details
* @param listener to receive callback parameters * @param listener to receive callback parameters
*/ */
public StreamCallbackAdapter(Arena arena, IStreamListener streamListener, IStreamCallbackListener listener) public StreamCallbackAdapter(IStreamListener streamListener, IStreamCallbackListener listener)
{ {
if(arena == null)
{
throw new IllegalArgumentException("Arena must be non-null");
}
mArena = arena;
mStreamCallbackListener = listener; mStreamCallbackListener = listener;
setListener(streamListener); setListener(streamListener);
} }
@@ -81,24 +73,27 @@ public class StreamCallbackAdapter implements sdrplay_api_StreamCallback_t
{ {
if(mStreamListener != null || mStreamCallbackListener != null) if(mStreamListener != null || mStreamCallbackListener != null)
{ {
//Translate the callback parameters pointer to a memory segment and re-construct the parameters as a Java object try(Arena arena = Arena.openConfined())
StreamCallbackParameters parameters = new StreamCallbackParameters(sdrplay_api_StreamCbParamsT
.ofAddress(parametersPointer, mArena.scope()));
if(mStreamCallbackListener != null)
{ {
mStreamCallbackListener.process(mStreamListener.getTunerSelect(), parameters, reset); //Translate the callback parameters pointer to a memory segment and re-construct the parameters as a Java object
} StreamCallbackParameters parameters = new StreamCallbackParameters(sdrplay_api_StreamCbParamsT
.ofAddress(parametersPointer, arena.scope()));
if(mStreamListener != null) if(mStreamCallbackListener != null)
{ {
//Allocate memory segments from I/Q pointers, transfer from native to JVM array, and send to listener mStreamCallbackListener.process(mStreamListener.getTunerSelect(), parameters, reset);
long arrayByteSize = ValueLayout.JAVA_SHORT.byteSize() * sampleCount; }
MemorySegment iSamples = MemorySegment.ofAddress(iSamplesPointer.address(), arrayByteSize, mArena.scope());
MemorySegment qSamples = MemorySegment.ofAddress(qSamplesPointer.address(), arrayByteSize, mArena.scope()); if(mStreamListener != null)
short[] i = iSamples.toArray(ValueLayout.JAVA_SHORT); {
short[] q = qSamples.toArray(ValueLayout.JAVA_SHORT); //Allocate memory segments from I/Q pointers, transfer from native to JVM array, and send to listener
mStreamListener.processStream(i, q, parameters, Flag.evaluate(reset)); long arrayByteSize = ValueLayout.JAVA_SHORT.byteSize() * sampleCount;
MemorySegment iSamples = MemorySegment.ofAddress(iSamplesPointer.address(), arrayByteSize, arena.scope());
MemorySegment qSamples = MemorySegment.ofAddress(qSamplesPointer.address(), arrayByteSize, arena.scope());
short[] i = iSamples.toArray(ValueLayout.JAVA_SHORT);
short[] q = qSamples.toArray(ValueLayout.JAVA_SHORT);
mStreamListener.processStream(i, q, parameters, Flag.evaluate(reset));
}
} }
} }
} }

View File

@@ -41,46 +41,35 @@ import org.slf4j.LoggerFactory;
public class Dispatcher<E> implements Listener<E> public class Dispatcher<E> implements Listener<E>
{ {
private final static Logger mLog = LoggerFactory.getLogger(Dispatcher.class); private final static Logger mLog = LoggerFactory.getLogger(Dispatcher.class);
private static final long OVERFLOW_LOG_EVENT_WAIT_PERIOD = TimeUnit.SECONDS.toMillis(10); private final LinkedTransferQueue<E> mQueue = new LinkedTransferQueue<>();
private LinkedTransferQueue<E> mQueue = new LinkedTransferQueue<>();
private Listener<E> mListener; private Listener<E> mListener;
private AtomicBoolean mRunning = new AtomicBoolean(); private final AtomicBoolean mRunning = new AtomicBoolean();
private String mThreadName; private final String mThreadName;
private ScheduledExecutorService mExecutorService; private ScheduledExecutorService mExecutorService;
private ScheduledFuture<?> mScheduledFuture; private ScheduledFuture<?> mScheduledFuture;
private int mBatchSize; private final long mInterval;
private long mInterval;
private HeartbeatManager mHeartbeatManager; private HeartbeatManager mHeartbeatManager;
/** /**
* Constructs an instance of a Dispatcher with integrated heartbeat support. * Constructs an instance of a Dispatcher with integrated heartbeat support.
* @param threadName to name the dispatcher thread * @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. * @param interval for processing each batch in milliseconds.
* @param heartbeatManager to receive a heartbeat command at each processing interval. * @param heartbeatManager to receive a heartbeat command at each processing interval.
*/ */
public Dispatcher(String threadName, int batchSize, long interval, HeartbeatManager heartbeatManager) public Dispatcher(String threadName, long interval, HeartbeatManager heartbeatManager)
{ {
this(threadName, batchSize, interval); this(threadName, interval);
mHeartbeatManager = heartbeatManager; mHeartbeatManager = heartbeatManager;
} }
/** /**
* Constructs an instance * Constructs an instance
* @param threadName to name the dispatcher thread * @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. * @param interval for processing each batch in milliseconds.
*/ */
public Dispatcher(String threadName, int batchSize, long interval) public Dispatcher(String threadName, long interval)
{ {
mThreadName = threadName; mThreadName = threadName;
mBatchSize = batchSize;
mInterval = interval; mInterval = interval;
} }
@@ -171,7 +160,7 @@ public class Dispatcher<E> implements Listener<E>
{ {
List<E> elements = new ArrayList<>(); List<E> elements = new ArrayList<>();
mQueue.drainTo(elements, mBatchSize); mQueue.drainTo(elements);
for(E element: elements) for(E element: elements)
{ {
@@ -195,7 +184,7 @@ public class Dispatcher<E> implements Listener<E>
*/ */
class Processor implements Runnable class Processor implements Runnable
{ {
private AtomicBoolean mRunning = new AtomicBoolean(); private final AtomicBoolean mRunning = new AtomicBoolean();
@Override @Override
public void run() public void run()
@@ -214,7 +203,7 @@ public class Dispatcher<E> implements Listener<E>
*/ */
class ProcessorWithHeartbeat implements Runnable class ProcessorWithHeartbeat implements Runnable
{ {
private AtomicBoolean mRunning = new AtomicBoolean(); private final AtomicBoolean mRunning = new AtomicBoolean();
@Override @Override
public void run() public void run()