diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java index 3c06ad7e09..bbaf399f4a 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl2.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java @@ -39,6 +39,7 @@ class I2PSessionImpl2 extends I2PSessionImpl { private final static long SEND_TIMEOUT = 60 * 1000; // 60 seconds to send /** should we gzip each payload prior to sending it? */ private final static boolean SHOULD_COMPRESS = true; + private final static boolean SHOULD_DECOMPRESS = true; /** * Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey @@ -64,6 +65,8 @@ class I2PSessionImpl2 extends I2PSessionImpl { _context.statManager().createRateStat("i2cp.receiveStatusTime.4", "How long it took to get status=4 back", "i2cp", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("i2cp.receiveStatusTime.5", "How long it took to get status=5 back", "i2cp", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("i2cp.receiveStatusTime", "How long it took to get any status", "i2cp", new long[] { 60*1000, 10*60*1000 }); + _context.statManager().createRateStat("i2cp.tx.msgCompressed", "compressed size transferred", "i2cp", new long[] { 60*1000, 30*60*1000 }); + _context.statManager().createRateStat("i2cp.tx.msgExpanded", "size before compression", "i2cp", new long[] { 60*1000, 30*60*1000 }); } protected long getTimeout() { @@ -75,6 +78,27 @@ class I2PSessionImpl2 extends I2PSessionImpl { clearStates(); super.destroySession(sendDisconnect); } + + /** Don't bother if really small. + * Three 66-byte messages will fit in one tunnel message. + * Four messages don't fit no matter how small. So below 66 it isn't worth it. + * See ConnectionOptions.java in the streaming lib for similar calculations. + * Since we still have to pass it through gzip -0 the CPU savings + * is trivial but it's the best we can do for now. See below. + * i2cp.gzip defaults to SHOULD_COMPRESS = true. + * Perhaps the http server (which does its own compression) + * and P2P apps (with generally uncompressible data) should + * set to false. + */ + private static final int DONT_COMPRESS_SIZE = 66; + private boolean shouldCompress(int size) { + if (size <= DONT_COMPRESS_SIZE) + return false; + String p = getOptions().getProperty("i2cp.gzip"); + if (p != null) + return Boolean.valueOf(p).booleanValue(); + return SHOULD_COMPRESS; + } @Override public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException { @@ -92,8 +116,30 @@ class I2PSessionImpl2 extends I2PSessionImpl { throws I2PSessionException { if (_log.shouldLog(Log.DEBUG)) _log.debug("sending message"); if (isClosed()) throw new I2PSessionException("Already closed"); - if (SHOULD_COMPRESS) payload = DataHelper.compress(payload, offset, size); - else throw new IllegalStateException("we need to update sendGuaranteed to support partial send"); + + // Sadly there is no way to send something completely uncompressed in a backward-compatible way, + // so we have to still send it in a gzip format, which adds 23 bytes (2.4% for a 960-byte msg) + // (10 byte header + 5 byte block header + 8 byte trailer) + // In the future we can add a one-byte magic number != 0x1F to signal an uncompressed msg + // (Gzip streams start with 0x1F 0x8B 0x08) + // assuming we don't need the CRC-32 that comes with gzip (do we?) + // Maybe implement this soon in receiveMessage() below so we are ready + // in case we ever make an incompatible network change. + // This would save 22 of the 23 bytes and a little CPU. + boolean sc = shouldCompress(size); + if (sc) + payload = DataHelper.compress(payload, offset, size); + else + payload = DataHelper.compress(payload, offset, size, DataHelper.NO_COMPRESSION); + //else throw new IllegalStateException("we need to update sendGuaranteed to support partial send"); + + int compressed = payload.length; + if (_log.shouldLog(Log.INFO)) { + String d = dest.calculateHash().toBase64().substring(0,4); + _log.info("sending message to: " + d + " compress? " + sc + " sizeIn=" + size + " sizeOut=" + compressed); + } + _context.statManager().addRateData("i2cp.tx.msgCompressed", compressed, 0); + _context.statManager().addRateData("i2cp.tx.msgExpanded", size, 0); return sendBestEffort(dest, payload, keyUsed, tagsSent); } @@ -107,7 +153,8 @@ class I2PSessionImpl2 extends I2PSessionImpl { _log.error("Error: message " + msgId + " already received!"); return null; } - if (SHOULD_COMPRESS) { + // future - check magic number to see whether to decompress + if (SHOULD_DECOMPRESS) { try { return DataHelper.decompress(compressed); } catch (IOException ioe) { diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java index 2f1cbd5b26..835e6a0dd7 100644 --- a/core/java/src/net/i2p/data/DataHelper.java +++ b/core/java/src/net/i2p/data/DataHelper.java @@ -33,6 +33,7 @@ import java.util.Iterator; import java.util.List; import java.util.Properties; import java.util.TreeMap; +import java.util.zip.Deflater; import net.i2p.util.ByteCache; import net.i2p.util.OrderedProperties; @@ -852,15 +853,21 @@ public class DataHelper { } private static final int MAX_UNCOMPRESSED = 40*1024; + public static final int MAX_COMPRESSION = Deflater.BEST_COMPRESSION; + public static final int NO_COMPRESSION = Deflater.NO_COMPRESSION; /** compress the data and return a new GZIP compressed array */ public static byte[] compress(byte orig[]) { return compress(orig, 0, orig.length); } public static byte[] compress(byte orig[], int offset, int size) { + return compress(orig, offset, size, MAX_COMPRESSION); + } + public static byte[] compress(byte orig[], int offset, int size, int level) { if ((orig == null) || (orig.length <= 0)) return orig; if (size >= MAX_UNCOMPRESSED) throw new IllegalArgumentException("tell jrandom size=" + size); ReusableGZIPOutputStream out = ReusableGZIPOutputStream.acquire(); + out.setLevel(level); try { out.write(orig, offset, size); out.finish(); diff --git a/core/java/src/net/i2p/util/ReusableGZIPOutputStream.java b/core/java/src/net/i2p/util/ReusableGZIPOutputStream.java index 544c85ed27..ad259c6d66 100644 --- a/core/java/src/net/i2p/util/ReusableGZIPOutputStream.java +++ b/core/java/src/net/i2p/util/ReusableGZIPOutputStream.java @@ -3,6 +3,7 @@ package net.i2p.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.ArrayList; +import java.util.zip.Deflater; import java.util.zip.GZIPInputStream; import net.i2p.data.DataHelper; @@ -51,6 +52,10 @@ public class ReusableGZIPOutputStream extends ResettableGZIPOutputStream { public void reset() { super.reset(); _buffer.reset(); + def.setLevel(Deflater.BEST_COMPRESSION); + } + public void setLevel(int level) { + def.setLevel(level); } /** pull the contents of the stream written */ public byte[] getData() { return _buffer.toByteArray(); } diff --git a/history.txt b/history.txt index abf4bab010..6b75c82c11 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,13 @@ +2008-11-15 zzz + * Build files: + - Don't die if depend not available + - Only verify Jetty hash once + - Add streaming lib tests to depend task + * I2CP Compression: + - Add i2cp.gzip option (default true) + - Add compression stats + - Don't bother compressing if really small + 2008-11-13 zzz * Streaming: - Add more info to Connection.toString() for debugging diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index d066d26a85..6aeebda42a 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = "0.6.4"; - public final static long BUILD = 10; + public final static long BUILD = 11; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID);