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:
@ -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"
|
||||
|
11
res/layout/fragment_graph.xml
Normal file
11
res/layout/fragment_graph.xml
Normal 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>
|
@ -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>
|
||||
|
22
src/net/i2p/android/router/activity/GraphActivity.java
Normal file
22
src/net/i2p/android/router/activity/GraphActivity.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
159
src/net/i2p/android/router/fragment/GraphFragment.java
Normal file
159
src/net/i2p/android/router/fragment/GraphFragment.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
136
src/net/i2p/android/router/service/StatSummarizer.java
Normal file
136
src/net/i2p/android/router/service/StatSummarizer.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
80
src/net/i2p/android/router/service/SummaryListener.java
Normal file
80
src/net/i2p/android/router/service/SummaryListener.java
Normal 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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user