package net.i2p.router.web;
import java.io.IOException;
import java.io.Serializable;
import java.io.Writer;
import java.text.DecimalFormat;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.RouterContext;
import net.i2p.router.peermanager.DBHistory;
import net.i2p.router.peermanager.PeerProfile;
import net.i2p.router.peermanager.ProfileOrganizer;
import net.i2p.stat.Rate;
import net.i2p.stat.RateAverages;
import net.i2p.stat.RateStat;
/**
* Helper class to refactor the HTML rendering from out of the ProfileOrganizer
*
*/
class ProfileOrganizerRenderer {
private final RouterContext _context;
private final ProfileOrganizer _organizer;
public ProfileOrganizerRenderer(ProfileOrganizer organizer, RouterContext context) {
_context = context;
_organizer = organizer;
}
/**
* @param mode 0 = high cap; 1 = all; 2 = floodfill
*/
public void renderStatusHTML(Writer out, int mode) throws IOException {
boolean full = mode == 1;
Set<Hash> peers = _organizer.selectAllPeers();
long now = _context.clock().now();
long hideBefore = now - 90*60*1000;
Set<PeerProfile> order = new TreeSet<PeerProfile>(mode == 2 ? new HashComparator() : new ProfileComparator());
int older = 0;
int standard = 0;
for (Hash peer : peers) {
if (_organizer.getUs().equals(peer)) continue;
PeerProfile prof = _organizer.getProfileNonblocking(peer);
if (prof == null)
continue;
if (mode == 2) {
RouterInfo info = _context.netDb().lookupRouterInfoLocally(peer);
if (info != null && info.getCapabilities().indexOf('f') >= 0)
order.add(prof);
continue;
}
if (prof.getLastSendSuccessful() <= hideBefore) {
older++;
continue;
}
if ((!full) && !_organizer.isHighCapacity(peer)) {
standard++;
continue;
}
order.add(prof);
}
int fast = 0;
int reliable = 0;
int integrated = 0;
StringBuilder buf = new StringBuilder(16*1024);
////
//// don't bother reindenting
////
if (mode < 2) {
//buf.append("<h2>").append(_t("Peer Profiles")).append("</h2>\n<p>");
buf.append(ngettext("Showing 1 recent profile.", "Showing {0} recent profiles.", order.size())).append('\n');
if (older > 0)
buf.append(ngettext("Hiding 1 older profile.", "Hiding {0} older profiles.", older)).append('\n');
if (standard > 0)
buf.append("<a href=\"/profiles?f=1\">").append(ngettext("Hiding 1 standard profile.", "Hiding {0} standard profiles.", standard)).append("</a>\n");
buf.append("</p>");
buf.append("<table>");
buf.append("<tr>");
buf.append("<th>").append(_t("Peer")).append("</th>");
buf.append("<th>").append(_t("Groups (Caps)")).append("</th>");
buf.append("<th>").append(_t("Speed")).append("</th>");
buf.append("<th>").append(_t("Capacity")).append("</th>");
buf.append("<th>").append(_t("Integration")).append("</th>");
buf.append("<th>").append(_t("Status")).append("</th>");
buf.append("<th> </th>");
buf.append("</tr>");
int prevTier = 1;
for (PeerProfile prof : order) {
Hash peer = prof.getPeer();
int tier = 0;
boolean isIntegrated = false;
if (_organizer.isFast(peer)) {
tier = 1;
fast++;
reliable++;
} else if (_organizer.isHighCapacity(peer)) {
tier = 2;
reliable++;
} else if (_organizer.isFailing(peer)) {
} else {
tier = 3;
}
if (_organizer.isWellIntegrated(peer)) {
isIntegrated = true;
integrated++;
}
if (tier != prevTier)
buf.append("<tr><td colspan=\"7\"><hr></td></tr>\n");
prevTier = tier;
buf.append("<tr><td align=\"center\" nowrap>");
buf.append(_context.commSystem().renderPeerHTML(peer));
// debug
//if(prof.getIsExpandedDB())
// buf.append(" ** ");
buf.append("</td><td align=\"center\">");
switch (tier) {
case 1: buf.append(_t("Fast, High Capacity")); break;
case 2: buf.append(_t("High Capacity")); break;
case 3: buf.append(_t("Standard")); break;
default: buf.append(_t("Failing")); break;
}
if (isIntegrated) buf.append(", ").append(_t("Integrated"));
RouterInfo info = _context.netDb().lookupRouterInfoLocally(peer);
if (info != null) {
// prevent HTML injection in the caps and version
buf.append(" (").append(DataHelper.stripHTML(info.getCapabilities()));
String v = info.getOption("router.version");
if (v != null)
buf.append(' ').append(DataHelper.stripHTML(v));
buf.append(')');
}
buf.append("<td align=\"right\">").append(num(prof.getSpeedValue()));
long bonus = prof.getSpeedBonus();
if (bonus != 0) {
if (bonus > 0)
buf.append(" (+");
else
buf.append(" (");
buf.append(bonus).append(')');
}
buf.append("</td><td align=\"right\">").append(num(prof.getCapacityValue()));
bonus = prof.getCapacityBonus();
if (bonus != 0) {
if (bonus > 0)
buf.append(" (+");
else
buf.append(" (");
buf.append(bonus).append(')');
}
buf.append("</td><td align=\"right\">").append(num(prof.getIntegrationValue()));
buf.append("</td><td align=\"center\">");
if (_context.banlist().isBanlisted(peer)) buf.append(_t("Banned"));
if (prof.getIsFailing()) buf.append(' ').append(_t("Failing"));
if (_context.commSystem().wasUnreachable(peer)) buf.append(' ').append(_t("Unreachable"));
RateAverages ra = RateAverages.getTemp();
Rate failed = prof.getTunnelHistory().getFailedRate().getRate(30*60*1000);
long fails = failed.computeAverages(ra, false).getTotalEventCount();
if (fails > 0) {
Rate accepted = prof.getTunnelCreateResponseTime().getRate(30*60*1000);
long total = fails + accepted.computeAverages(ra, false).getTotalEventCount();
if (total / fails <= 10) // hide if < 10%
buf.append(' ').append(fails).append('/').append(total).append(' ').append(_t("Test Fails"));
}
buf.append(" </td>");
//buf.append("<td nowrap align=\"center\"><a target=\"_blank\" href=\"dumpprofile.jsp?peer=")
// .append(peer.toBase64().substring(0,6)).append("\">").append(_t("profile")).append("</a>");
buf.append("<td nowrap align=\"center\"><a href=\"viewprofile?peer=")
.append(peer.toBase64()).append("\">").append(_t("profile")).append("</a>");
buf.append(" <a href=\"configpeer?peer=").append(peer.toBase64()).append("\">+-</a></td>\n");
buf.append("</tr>");
// let's not build the whole page in memory (~500 bytes per peer)
out.write(buf.toString());
buf.setLength(0);
}
buf.append("</table>");
////
//// don't bother reindenting
////
} else {
//buf.append("<h2><a name=\"flood\"></a>").append(_t("Floodfill and Integrated Peers"))
// .append(" (").append(integratedPeers.size()).append(")</h2>\n");
buf.append("<table>");
buf.append("<tr>");
buf.append("<th class=\"smallhead\">").append(_t("Peer")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("Caps")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("Integ. Value")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("Last Heard About")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("Last Heard From")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("Last Good Send")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("Last Bad Send")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("10m Resp. Time")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("1h Resp. Time")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("1d Resp. Time")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("Last Good Lookup")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("Last Bad Lookup")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("Last Good Store")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("Last Bad Store")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("1h Fail Rate")).append("</th>");
buf.append("<th class=\"smallhead\">").append(_t("1d Fail Rate")).append("</th>");
buf.append("</tr>");
RateAverages ra = RateAverages.getTemp();
for (PeerProfile prof : order) {
Hash peer = prof.getPeer();
buf.append("<tr><td align=\"center\" nowrap>");
buf.append(_context.commSystem().renderPeerHTML(peer));
buf.append("</td>");
RouterInfo info = _context.netDb().lookupRouterInfoLocally(peer);
if (info != null)
buf.append("<td align=\"center\">").append(DataHelper.stripHTML(info.getCapabilities())).append("</td>");
else
buf.append("<td> </td>");
buf.append("<td align=\"right\">").append(num(prof.getIntegrationValue())).append("</td>");
buf.append("<td align=\"right\">").append(formatInterval(now, prof.getLastHeardAbout())).append("</td>");
buf.append("<td align=\"right\">").append(formatInterval(now, prof.getLastHeardFrom())).append("</td>");
buf.append("<td align=\"right\">").append(formatInterval(now, prof.getLastSendSuccessful())).append("</td>");
buf.append("<td align=\"right\">").append(formatInterval(now, prof.getLastSendFailed())).append("</td>");
buf.append("<td align=\"right\">").append(avg(prof, 10*60*1000l, ra)).append("</td>");
buf.append("<td align=\"right\">").append(avg(prof, 60*60*1000l, ra)).append("</td>");
buf.append("<td align=\"right\">").append(avg(prof, 24*60*60*1000l, ra)).append("</td>");
DBHistory dbh = prof.getDBHistory();
if (dbh != null) {
buf.append("<td align=\"right\">").append(formatInterval(now, dbh.getLastLookupSuccessful())).append("</td>");
buf.append("<td align=\"right\">").append(formatInterval(now, dbh.getLastLookupFailed())).append("</td>");
buf.append("<td align=\"right\">").append(formatInterval(now, dbh.getLastStoreSuccessful())).append("</td>");
buf.append("<td align=\"right\">").append(formatInterval(now, dbh.getLastStoreFailed())).append("</td>");
buf.append("<td align=\"right\">").append(davg(dbh, 60*60*1000l, ra)).append("</td>");
buf.append("<td align=\"right\">").append(davg(dbh, 24*60*60*1000l, ra)).append("</td>");
} else {
for (int i = 0; i < 6; i++)
buf.append("<td align=\"right\">").append(_t(NA));
}
buf.append("</tr>\n");
}
buf.append("</table>");
////
//// don't bother reindenting
////
}
if (mode < 2) {
buf.append("<h3>").append(_t("Thresholds")).append("</h3>");
buf.append("<p><b>").append(_t("Speed")).append(":</b> ").append(num(_organizer.getSpeedThreshold()))
.append(" (").append(fast).append(' ').append(_t("fast peers")).append(")<br>");
buf.append("<b>").append(_t("Capacity")).append(":</b> ").append(num(_organizer.getCapacityThreshold()))
.append(" (").append(reliable).append(' ').append(_t("high capacity peers")).append(")<br>");
buf.append("<b>").append(_t("Integration")).append(":</b> ").append(num(_organizer.getIntegrationThreshold()))
.append(" (").append(integrated).append(' ').append(_t(" well integrated peers")).append(")</p>");
buf.append("<h3>").append(_t("Definitions")).append("</h3><ul>");
buf.append("<li><b>").append(_t("groups")).append("</b>: ").append(_t("as determined by the profile organizer")).append("</li>");
buf.append("<li><b>").append(_t("caps")).append("</b>: ").append(_t("capabilities in the netDb, not used to determine profiles")).append("</li>");
buf.append("<li><b>").append(_t("speed")).append("</b>: ").append(_t("peak throughput (bytes per second) over a 1 minute period that the peer has sustained in a single tunnel")).append("</li>");
buf.append("<li><b>").append(_t("capacity")).append("</b>: ").append(_t("how many tunnels can we ask them to join in an hour?")).append("</li>");
buf.append("<li><b>").append(_t("integration")).append("</b>: ").append(_t("how many new peers have they told us about lately?")).append("</li>");
buf.append("<li><b>").append(_t("status")).append("</b>: ").append(_t("is the peer banned, or unreachable, or failing tunnel tests?")).append("</li>");
buf.append("</ul>");
////
//// don't bother reindenting
////
} // mode < 2
out.write(buf.toString());
out.flush();
}
private class ProfileComparator extends HashComparator {
public int compare(PeerProfile left, PeerProfile right) {
if (_context.profileOrganizer().isFast(left.getPeer())) {
if (_context.profileOrganizer().isFast(right.getPeer())) {
return super.compare(left, right);
} else {
return -1; // fast comes first
}
} else if (_context.profileOrganizer().isHighCapacity(left.getPeer())) {
if (_context.profileOrganizer().isFast(right.getPeer())) {
return 1;
} else if (_context.profileOrganizer().isHighCapacity(right.getPeer())) {
return super.compare(left, right);
} else {
return -1;
}
} else if (_context.profileOrganizer().isFailing(left.getPeer())) {
if (_context.profileOrganizer().isFailing(right.getPeer())) {
return super.compare(left, right);
} else {
return 1;
}
} else {
// left is not failing
if (_context.profileOrganizer().isFast(right.getPeer())) {
return 1;
} else if (_context.profileOrganizer().isHighCapacity(right.getPeer())) {
return 1;
} else if (_context.profileOrganizer().isFailing(right.getPeer())) {
return -1;
} else {
return super.compare(left, right);
}
}
}
}
/**
* Used for floodfill-only page
* As of 0.9.29, sorts in true binary order, not base64 string
* @since 0.9.8
*/
private static class HashComparator implements Comparator<PeerProfile>, Serializable {
public int compare(PeerProfile left, PeerProfile right) {
return DataHelper.compareTo(left.getPeer().getData(), right.getPeer().getData());
}
}
private final static DecimalFormat _fmt = new DecimalFormat("###,##0.00");
private final static String num(double num) { synchronized (_fmt) { return _fmt.format(num); } }
private final static String NA = HelperBase._x("n/a");
private String avg (PeerProfile prof, long rate, RateAverages ra) {
RateStat rs = prof.getDbResponseTime();
if (rs == null)
return _t(NA);
Rate r = rs.getRate(rate);
if (r == null)
return _t(NA);
r.computeAverages(ra, false);
if (ra.getTotalEventCount() == 0)
return _t(NA);
return DataHelper.formatDuration2(Math.round(ra.getAverage()));
}
private String davg (DBHistory dbh, long rate, RateAverages ra) {
RateStat rs = dbh.getFailedLookupRate();
if (rs == null)
return "0%";
Rate r = rs.getRate(rate);
if (r == null)
return "0%";
r.computeAverages(ra, false);
if (ra.getTotalEventCount() <= 0)
return "0%";
double avg = 0.5 + 100 * ra.getAverage();
return ((int) avg) + "%";
}
/** @since 0.9.21 */
private String formatInterval(long now, long then) {
if (then <= 0)
return _t(NA);
// avoid 0 or negative
if (now <= then)
return DataHelper.formatDuration2(1);
return DataHelper.formatDuration2(now - then);
}
/** translate a string */
private String _t(String s) {
return Messages.getString(s, _context);
}
/** translate (ngettext) @since 0.8.5 */
public String ngettext(String s, String p, int n) {
return Messages.getString(n, s, p, _context);
}
}