Implemented stat graphing

StatSummarizer and SummaryListener are based on the same-named classes from the
routerconsole. Data is stored in memory using an AndroidPlot SimpleXYSeries.
Only the last 30 points are currently stored.
This commit is contained in:
str4d
2013-09-07 08:07:15 +00:00
parent 9c7b2142cf
commit 81d0441d2b
9 changed files with 446 additions and 0 deletions

View File

@ -93,6 +93,10 @@
android:label="I2P Logs"
android:parentActivityName=".activity.MainActivity" >
</activity>
<activity android:name=".activity.GraphActivity"
android:label="Graph"
android:parentActivityName=".activity.MainActivity" >
</activity>
<activity android:name=".activity.PeersActivity"
android:label="I2P Peers and Transport Status"
android:configChanges="orientation|keyboardHidden"

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<com.androidplot.xy.XYPlot
android:id="@+id/rate_stat_plot"
android:layout_width="match_parent"
android:layout_height="match_parent"
androidplot.title="Rate Stat Plot" />
</LinearLayout>

View File

@ -11,6 +11,10 @@
<item>@string/label_news</item>
<item>@string/label_website_nonanon</item>
<item>@string/label_faq_nonanon</item>
<item>Active peers</item>
<item>Memory used</item>
<item>Incoming bandwidth used</item>
<item>Outgoing bandwidth used</item>
</string-array>
<string-array name="setting0to3">
<item>0</item>

View File

@ -0,0 +1,22 @@
package net.i2p.android.router.activity;
import net.i2p.android.router.R;
import net.i2p.android.router.fragment.GraphFragment;
import net.i2p.android.router.service.RouterService;
import android.os.Bundle;
public class GraphActivity extends I2PActivityBase {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDrawerToggle.setDrawerIndicatorEnabled(false);
// Start with the base view
if (savedInstanceState == null) {
String rateName = getIntent().getStringExtra(GraphFragment.RATE_NAME);
long period = getIntent().getLongExtra(GraphFragment.RATE_PERIOD, 60 * 1000);
GraphFragment f = GraphFragment.newInstance(rateName, period);
getSupportFragmentManager().beginTransaction()
.add(R.id.main_fragment, f).commit();
}
}
}

View File

@ -24,6 +24,7 @@ import android.widget.ListView;
import net.i2p.android.i2ptunnel.activity.TunnelListActivity;
import net.i2p.android.router.R;
import net.i2p.android.router.binder.RouterBinder;
import net.i2p.android.router.fragment.GraphFragment;
import net.i2p.android.router.fragment.I2PFragmentBase;
import net.i2p.android.router.fragment.NewsFragment;
import net.i2p.android.router.fragment.WebFragment;
@ -174,6 +175,30 @@ public abstract class I2PActivityBase extends ActionBarActivity implements
faq.putExtra(WebFragment.HTML_URI, "http://www.i2p2.de/faq");
startActivity(faq);
break;
case 10:
Intent active = new Intent(I2PActivityBase.this, GraphActivity.class);
active.putExtra(GraphFragment.RATE_NAME, "router.activePeers");
active.putExtra(GraphFragment.RATE_PERIOD, 60 * 1000);
startActivity(active);
break;
case 11:
Intent mem = new Intent(I2PActivityBase.this, GraphActivity.class);
mem.putExtra(GraphFragment.RATE_NAME, "router.memoryUsed");
mem.putExtra(GraphFragment.RATE_PERIOD, 60 * 1000);
startActivity(mem);
break;
case 12:
Intent bwR = new Intent(I2PActivityBase.this, GraphActivity.class);
bwR.putExtra(GraphFragment.RATE_NAME, "bw.recvRate");
bwR.putExtra(GraphFragment.RATE_PERIOD, 60 * 1000);
startActivity(bwR);
break;
case 13:
Intent bwS = new Intent(I2PActivityBase.this, GraphActivity.class);
bwS.putExtra(GraphFragment.RATE_NAME, "bw.sendRate");
bwS.putExtra(GraphFragment.RATE_PERIOD, 60 * 1000);
startActivity(bwS);
break;
default:
Intent main = new Intent(I2PActivityBase.this, MainActivity.class);
startActivity(main);

View File

@ -0,0 +1,159 @@
package net.i2p.android.router.fragment;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.util.Observable;
import java.util.Observer;
import com.androidplot.Plot;
import com.androidplot.xy.BoundaryMode;
import com.androidplot.xy.LineAndPointFormatter;
import com.androidplot.xy.XYPlot;
import com.androidplot.xy.XYSeries;
import com.androidplot.xy.XYStepMode;
import net.i2p.android.router.R;
import net.i2p.android.router.service.StatSummarizer;
import net.i2p.android.router.service.SummaryListener;
import net.i2p.android.router.util.Util;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class GraphFragment extends I2PFragmentBase {
// redraws a plot whenever an update is received:
private class MyPlotUpdater implements Observer {
Plot plot;
public MyPlotUpdater(Plot plot) {
this.plot = plot;
}
public void update(Observable o, Object arg) {
Util.i("Redrawing plot");
plot.redraw();
}
}
public static final String RATE_NAME = "rate_name";
public static final String RATE_PERIOD = "rate_period";
private Handler _handler;
private SetupTask _setupTask;
private SummaryListener _listener;
private XYPlot _ratePlot;
private MyPlotUpdater _plotUpdater;
public static GraphFragment newInstance(String name, long period) {
GraphFragment f = new GraphFragment();
Bundle args = new Bundle();
args.putString(RATE_NAME, name);
args.putLong(RATE_PERIOD, period);
f.setArguments(args);
return f;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_handler = new Handler();
_setupTask = new SetupTask();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_graph, container, false);
_ratePlot = (XYPlot) v.findViewById(R.id.rate_stat_plot);
return v;
}
@Override
public void onStart() {
super.onStart();
_handler.removeCallbacks(_setupTask);
_handler.postDelayed(_setupTask, 100);
}
@Override
public void onStop() {
super.onStop();
if (_listener != null && _plotUpdater != null) {
Util.i("Removing plot updater from listener");
_listener.removeObserver(_plotUpdater);
}
_handler.removeCallbacks(_setupTask);
}
private class SetupTask implements Runnable {
public void run() {
String rateName = getArguments().getString(RATE_NAME);
long period = getArguments().getLong(RATE_PERIOD);
Util.i("Setting up " + rateName + "." + period);
if (StatSummarizer.instance() == null) {
Util.i("StatSummarizer is null, delaying setup");
_handler.postDelayed(this, 1000);
return;
}
_listener = StatSummarizer.instance().getListener(rateName, period);
if (_listener == null) {
Util.i("Listener is null, delaying setup");
_handler.postDelayed(this, 1000);
return;
}
XYSeries rateSeries = _listener.getSeries();
_plotUpdater = new MyPlotUpdater(_ratePlot);
_ratePlot.addSeries(rateSeries, new LineAndPointFormatter(Color.rgb(0, 0, 0), null, Color.rgb(0, 80, 0), null));
Util.i("Adding plot updater to listener");
_listener.addObserver(_plotUpdater);
_ratePlot.setDomainStepMode(XYStepMode.SUBDIVIDE);
_ratePlot.setDomainStepValue(SummaryListener.HISTORY_SIZE);
// thin out domain/range tick labels so they dont overlap each other:
_ratePlot.setTicksPerDomainLabel(5);
_ratePlot.setTicksPerRangeLabel(3);
_ratePlot.setRangeLowerBoundary(0, BoundaryMode.FIXED);
_ratePlot.setRangeValueFormat(new Format() {
@Override
public StringBuffer format(Object obj, StringBuffer toAppendTo,
FieldPosition pos) {
double val = ((Number) obj).doubleValue();
if (val >= 10 * 1000 * 1000)
return new DecimalFormat("0 M").format(val / (1000 * 1000), toAppendTo, pos);
else if (val >= 8 * 100 * 1000)
return new DecimalFormat("0.0 M").format(val / (1000 * 1000), toAppendTo, pos);
else if (val >= 10 * 1000)
return new DecimalFormat("0 k").format(val / (1000), toAppendTo, pos);
else if (val >= 8 * 100)
return new DecimalFormat("0.0 k").format(val / (1000), toAppendTo, pos);
else
return new DecimalFormat("0").format(val, toAppendTo, pos);
}
@Override
public Object parseObject(String source, ParsePosition pos) {
return null;
}
});
Util.i("Redrawing plot");
_ratePlot.redraw();
}
}
}

View File

@ -50,6 +50,10 @@ class LoadClientsJob extends JobImpl {
Job j = new RunI2PTunnel(getContext());
getContext().jobQueue().addJob(j);
Thread t = new I2PAppThread(new StatSummarizer(), "StatSummarizer", true);
t.setPriority(Thread.NORM_PRIORITY - 1);
t.start();
NewsFetcher fetcher = NewsFetcher.getInstance(getContext());
_fetcherThread = new I2PAppThread(fetcher, "NewsFetcher", true);
_fetcherThread.start();
@ -96,6 +100,7 @@ class LoadClientsJob extends JobImpl {
public void run() {
Util.i("client shutdown hook");
// i2ptunnel registers its own hook
// statsummarizer registers its own hook
if (_BOB != null)
BOB.stop();
if (_fetcherThread != null)

View File

@ -0,0 +1,136 @@
package net.i2p.android.router.service;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.concurrent.CopyOnWriteArrayList;
import net.i2p.router.RouterContext;
import net.i2p.stat.Rate;
import net.i2p.stat.RateStat;
public class StatSummarizer implements Runnable {
private final RouterContext _context;
private final List<SummaryListener> _listeners;
// TODO remove static instance
private static StatSummarizer _instance;
private volatile boolean _isRunning = true;
private Thread _thread;
public StatSummarizer() {
_context = RouterContext.listContexts().get(0);
_listeners = new CopyOnWriteArrayList<SummaryListener>();
_instance = this;
_context.addShutdownTask(new Shutdown());
}
public static StatSummarizer instance() { return _instance; }
public void run() {
_thread = Thread.currentThread();
String specs = "";
while (_isRunning && _context.router().isAlive()) {
specs = adjustDatabases(specs);
try { Thread.sleep(60 * 1000);} catch (InterruptedException ie) {}
}
}
public SummaryListener getListener(String rateName, long period) {
for (SummaryListener lsnr : _listeners) {
if (lsnr.getName().equals(rateName + "." + period))
return lsnr;
}
return null;
}
private static final String DEFAULT_DATABASES =
"bw.sendRate.60000"
+ ",bw.recvRate.60000"
+ ",router.memoryUsed.60000"
+ ",router.activePeers.60000";
private String adjustDatabases(String oldSpecs) {
String spec = _context.getProperty("stat.summaries", DEFAULT_DATABASES);
if ( ( (spec == null) && (oldSpecs == null) ) ||
( (spec != null) && (oldSpecs != null) && (oldSpecs.equals(spec))) )
return oldSpecs;
List<Rate> old = parseSpecs(oldSpecs);
List<Rate> newSpecs = parseSpecs(spec);
// remove old ones
for (Rate r : old) {
if (!newSpecs.contains(r))
removeDb(r);
}
// add new ones
StringBuilder buf = new StringBuilder();
boolean comma = false;
for (Rate r : newSpecs) {
if (!old.contains(r))
addDb(r);
if (comma)
buf.append(',');
else
comma = true;
buf.append(r.getRateStat().getName()).append(".").append(r.getPeriod());
}
return buf.toString();
}
private void removeDb(Rate r) {
for (SummaryListener lsnr : _listeners) {
if (lsnr.getRate().equals(r)) {
// no iter.remove() in COWAL
_listeners.remove(lsnr);
lsnr.stopListening();
return;
}
}
}
private void addDb(Rate r) {
SummaryListener lsnr = new SummaryListener(r);
lsnr.startListening();
_listeners.add(lsnr);
}
/**
* @param specs statName.period,statName.period,statName.period
* @return list of Rate objects
*/
List<Rate> parseSpecs(String specs) {
StringTokenizer tok = new StringTokenizer(specs, ",");
List<Rate> rv = new ArrayList<Rate>();
while (tok.hasMoreTokens()) {
String spec = tok.nextToken();
int split = spec.lastIndexOf('.');
if ( (split <= 0) || (split + 1 >= spec.length()) )
continue;
String name = spec.substring(0, split);
String per = spec.substring(split+1);
long period = -1;
try {
period = Long.parseLong(per);
RateStat rs = _context.statManager().getRate(name);
if (rs != null) {
Rate r = rs.getRate(period);
if (r != null)
rv.add(r);
}
} catch (NumberFormatException nfe) {}
}
return rv;
}
private class Shutdown implements Runnable {
public void run() {
_isRunning = false;
if (_thread != null)
_thread.interrupt();
for (SummaryListener lsnr : _listeners) {
lsnr.stopListening();
}
_listeners.clear();
}
}
}

View File

@ -0,0 +1,80 @@
package net.i2p.android.router.service;
import java.util.Observable;
import java.util.Observer;
import com.androidplot.xy.SimpleXYSeries;
import com.androidplot.xy.XYSeries;
import net.i2p.I2PAppContext;
import net.i2p.stat.Rate;
import net.i2p.stat.RateStat;
import net.i2p.stat.RateSummaryListener;
public class SummaryListener implements RateSummaryListener {
public static final int HISTORY_SIZE = 30;
private final I2PAppContext _context;
private final Rate _rate;
private String _name;
private SimpleXYSeries _series;
private MyObservable _notifier;
public SummaryListener(Rate r) {
_context = I2PAppContext.getGlobalContext();
_rate = r;
_notifier = new MyObservable();
}
// encapsulates management of the observers watching this rate for update events:
class MyObservable extends Observable {
@Override
public void notifyObservers() {
setChanged();
super.notifyObservers();
}
}
public void addObserver(Observer observer) {
_notifier.addObserver(observer);
}
public void removeObserver(Observer observer) {
_notifier.deleteObserver(observer);
}
public void add(double totalValue, long eventCount, double totalEventTime,
long period) {
long now = now();
long when = now / 1000;
double val = eventCount > 0 ? (totalValue / eventCount) : 0d;
if (_series.size() > HISTORY_SIZE)
_series.removeFirst();
_series.addLast(when, val);
_notifier.notifyObservers();
}
public Rate getRate() { return _rate; }
public String getName() { return _name; }
public XYSeries getSeries() { return _series; }
long now() { return _context.clock().now(); }
public void startListening() {
RateStat rs = _rate.getRateStat();
long period = _rate.getPeriod();
_name = rs.getName() + "." + period;
_series = new SimpleXYSeries(_name);
_series.useImplicitXVals();
_rate.setSummaryListener(this);
}
public void stopListening() {
_rate.setSummaryListener(null);
}
}