package eu.hgross.blaubot.android.views;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.apache.commons.collections4.queue.CircularFifoQueue;
import java.util.ArrayList;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import eu.hgross.blaubot.android.R;
import eu.hgross.blaubot.core.Blaubot;
import eu.hgross.blaubot.messaging.BlaubotMessage;
import eu.hgross.blaubot.messaging.IBlaubotChannel;
import eu.hgross.blaubot.messaging.IBlaubotMessageListener;
import eu.hgross.blaubot.ui.BlaubotDebugViewConstants;
import eu.hgross.blaubot.ui.IBlaubotDebugView;
import eu.hgross.blaubot.util.Log;
/**
* Android view to send and receive many bytes in an endless loop through a blaubot channel
*
* Add this view to a blaubot instance like this: throughputview.registerBlaubotInstance(blaubot);
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*/
public class ThroughputView extends FrameLayout implements IBlaubotDebugView {
private static final short CHANNEL_ID = BlaubotDebugViewConstants.THROUGHPUT_VIEW_CHANNEL_ID;
private static final long UPDATE_INTERVAL = 150;
private Button mStartButton;
private Handler mUiHandler;
private Blaubot mBlaubot;
private IBlaubotChannel mChannel;
private TextView mResultTextView;
private ThroughputTester mThroughPutTester;
private Timer mUpdateTimer;
private static class ThroughputTester {
/**
* The size of one message in bytes to be sentBytes while testing
*/
private static final short MESSAGE_SIZE = 1400;
/**
* The period in ms that is taken into account when getting the throughput.
* Older packets are ignored
*/
private static final int MEASURE_PERIOD = 2000; // ms
private final IBlaubotChannel channel;
private static final String LOG_TAG = "ThroughputTester";
private final StartStopListener startStopListener;
private long sentBytes = 0;
private long sentMessages = 0;
private long receivedBytes = 0;
private long receivedMessages = 0;
private Object sendLock = new Object();
private Object receiveLock = new Object();
private CircularFifoQueue<Pair<Long, Integer>> lastRecMessages; // timestamp, bytes
private CircularFifoQueue<Pair<Long, Integer>> lastSendMessages; // timestamp, bytes
private ExecutorService executorService;
private volatile Runnable sendRunnable;
interface StartStopListener {
void onStart();
void onStop();
}
class SendTask implements Runnable {
private final Random random = new Random();
@Override
public void run() {
startStopListener.onStart();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "SendTask started");
}
reset();
while (sendRunnable == this) {
byte[] data = new byte[MESSAGE_SIZE];
random.nextBytes(data);
channel.publish(data);
onMessageSent(data);
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "SendTask finished");
}
startStopListener.onStop();
}
}
;
public ThroughputTester(IBlaubotChannel channel, StartStopListener startStopListener) {
this.startStopListener = startStopListener;
this.channel = channel;
this.lastRecMessages = new CircularFifoQueue<>(20);
this.lastSendMessages = new CircularFifoQueue<>(20);
channel.addMessageListener(new IBlaubotMessageListener() {
@Override
public void onMessage(BlaubotMessage blaubotMessage) {
onMessageReceived(blaubotMessage.getPayload());
}
});
}
/**
* Resets to the current timestamp
*/
public void reset() {
synchronized (sendLock) {
sentBytes = 0;
sentMessages = 0;
}
synchronized (receiveLock) {
receivedBytes = 0;
receivedMessages = 0;
}
}
private void onMessageSent(byte[] msg) {
final int size = msg.length;
synchronized (sendLock) {
lastSendMessages.add(new Pair<>(System.currentTimeMillis(), size));
sentBytes += size;
sentMessages += 1;
}
}
private void onMessageReceived(byte[] msg) {
final int size = msg.length;
synchronized (receiveLock) {
lastRecMessages.add(new Pair<>(System.currentTimeMillis(), size));
receivedBytes += size;
receivedMessages += 1;
}
}
/**
* The send throughput
*
* @return bytes / ms
*/
public float getSendThroughput() {
if (lastSendMessages.isEmpty()) {
return 0;
}
final ArrayList<Pair<Long, Integer>> data;
synchronized (sendLock) {
data = new ArrayList<>(lastSendMessages);
}
final long now = System.currentTimeMillis();
final long minTimestamp = now - MEASURE_PERIOD;
long startTime = 0;
int byteSum = 0;
for (Pair pair : data) {
final Long time = (Long) pair.first;
if (time < minTimestamp) {
continue;
} else if (startTime == 0) {
startTime = time;
}
final Integer bytes = (Integer) pair.second;
byteSum += bytes;
}
final float timespan = (now - startTime);
if (timespan == 0)
return 0;
return ((float) byteSum) / timespan;
}
/**
* The receive throughput
*
* @return bytes / ms
*/
public float getReceiveThroughput() {
if (lastRecMessages.isEmpty()) {
return 0;
}
final ArrayList<Pair<Long, Integer>> data;
synchronized (receiveLock) {
data = new ArrayList<>(lastRecMessages);
}
final long now = System.currentTimeMillis();
final long minTimestamp = now - MEASURE_PERIOD;
long startTime = 0;
int byteSum = 0;
for (Pair pair : data) {
final Long time = (Long) pair.first;
if (time < minTimestamp) {
continue;
} else if (startTime == 0) {
startTime = time;
}
final Integer bytes = (Integer) pair.second;
byteSum += bytes;
}
final float timespan = (now - startTime);
if (timespan == 0)
return 0;
return ((float) byteSum) / timespan;
}
public long getAvgBytesPerReceivedMessage() {
return receivedMessages / receivedBytes;
}
public long getAvgBytesPerSentMessage() {
return sentMessages / sentBytes;
}
public synchronized void start() {
stop();
executorService = Executors.newSingleThreadExecutor();
sendRunnable = new SendTask();
executorService.execute(sendRunnable);
}
public synchronized void stop() {
this.sendRunnable = null;
if (this.executorService != null) {
this.executorService.shutdown();
try {
this.executorService.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized boolean isStarted() {
return sendRunnable != null;
}
}
public ThroughputView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context, attrs);
}
public ThroughputView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView(context, attrs);
}
private void initView(Context context, AttributeSet attrs) {
View view = inflate(context, R.layout.blaubot_throughput_view, null);
mUiHandler = new Handler(Looper.getMainLooper());
addView(view);
mResultTextView = (TextView) findViewById(R.id.resultTextView);
mResultTextView.setMaxLines(2);
mResultTextView.setLines(2);
mStartButton = (Button) findViewById(R.id.startButton);
mStartButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mThroughPutTester.isStarted()) {
mThroughPutTester.stop();
} else {
mThroughPutTester.start();
}
mStartButton.setEnabled(false);
}
});
}
/**
* Listens to the used channel of the throughput tester and updates the ui on new messages
*/
private IBlaubotMessageListener mMessageListener = new IBlaubotMessageListener() {
@Override
public void onMessage(BlaubotMessage blaubotMessage) {
}
};
/**
* Register this view with the given blaubot instance
*
* @param blaubot the blaubot instance to connect with
*/
public void registerBlaubotInstance(Blaubot blaubot) {
if (mBlaubot != null) {
unregisterBlaubotInstance();
}
this.mBlaubot = blaubot;
this.mChannel = this.mBlaubot.createChannel(CHANNEL_ID);
this.mChannel.getChannelConfig().setPriority(BlaubotMessage.Priority.LOW);
this.mChannel.subscribe();
this.mThroughPutTester = new ThroughputTester(mChannel, new ThroughputTester.StartStopListener() {
@Override
public void onStart() {
mUiHandler.post(new Runnable() {
@Override
public void run() {
mStartButton.setEnabled(true);
mStartButton.setText(" Stop measurement");
}
});
}
@Override
public void onStop() {
mUiHandler.post(new Runnable() {
@Override
public void run() {
mStartButton.setEnabled(true);
mStartButton.setText(" Start measurement");
}
});
}
});
this.mChannel.addMessageListener(mMessageListener);
final TimerTask timerTask = new TimerTask() {
@Override
public void run() {
final float receiveThroughput = mThroughPutTester.getReceiveThroughput() * 1000;
final float sendThroughput = mThroughPutTester.getSendThroughput() * 1000;
mUiHandler.post(new Runnable() {
@Override
public void run() {
final String humanReadbaleRx = ViewUtils.humanReadableByteCount((int) receiveThroughput, false);
final String humanReadbaleTx = ViewUtils.humanReadableByteCount((int) sendThroughput, false);
StringBuilder sb = new StringBuilder("Rx: ");
sb.append(humanReadbaleRx);
sb.append("/s\n");
sb.append("Tx: ");
sb.append(humanReadbaleTx);
sb.append("/s");
mResultTextView.setText(sb.toString());
}
});
}
};
this.mUpdateTimer = new Timer();
this.mUpdateTimer.scheduleAtFixedRate(timerTask, 0, UPDATE_INTERVAL);
}
@Override
public void unregisterBlaubotInstance() {
if (mBlaubot != null) {
this.mChannel.unsubscribe();
this.mChannel.removeMessageListener(mMessageListener);
this.mBlaubot = null;
}
if (mUpdateTimer != null) {
mUpdateTimer.cancel();
mUpdateTimer = null;
}
}
}