From d8b3d2c508df0a0e0f3ba49ef787377c74e3b153 Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Sun, 20 May 2012 18:15:36 +0000 Subject: [PATCH] * i2psnark: - Create sparse files at torrent creation and delay "ballooning" until first write (ticket #641) - Redo clear messages button - Concurrent message queue --- .../src/org/klomp/snark/SnarkManager.java | 33 +++++----- .../java/src/org/klomp/snark/Storage.java | 63 ++++++++++++++++--- .../org/klomp/snark/web/I2PSnarkServlet.java | 18 +++--- .../resources/themes/snark/ubergine/snark.css | 8 ++- .../resources/themes/snark/vanilla/snark.css | 8 ++- 5 files changed, 95 insertions(+), 35 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index c8c83c6bcb..36db296a29 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -19,7 +19,9 @@ import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeMap; +import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; import net.i2p.I2PAppContext; import net.i2p.data.Base64; @@ -50,7 +52,7 @@ public class SnarkManager implements Snark.CompleteListener { private Properties _config; private final I2PAppContext _context; private final Log _log; - private final List<String> _messages; + private final Queue<String> _messages; private final I2PSnarkUtil _util; private PeerCoordinatorSet _peerCoordinatorSet; private ConnectionAcceptor _connectionAcceptor; @@ -123,7 +125,7 @@ public class SnarkManager implements Snark.CompleteListener { _addSnarkLock = new Object(); _context = I2PAppContext.getGlobalContext(); _log = _context.logManager().getLog(SnarkManager.class); - _messages = new ArrayList(16); + _messages = new LinkedBlockingQueue(); _util = new I2PSnarkUtil(_context); _configFile = new File(CONFIG_FILE); if (!_configFile.isAbsolute()) @@ -154,13 +156,12 @@ public class SnarkManager implements Snark.CompleteListener { /** hook to I2PSnarkUtil for the servlet */ public I2PSnarkUtil util() { return _util; } - private static final int MAX_MESSAGES = 5; + private static final int MAX_MESSAGES = 100; public void addMessage(String message) { - synchronized (_messages) { - _messages.add(message); - while (_messages.size() > MAX_MESSAGES) - _messages.remove(0); + _messages.offer(message); + while (_messages.size() > MAX_MESSAGES) { + _messages.poll(); } if (_log.shouldLog(Log.INFO)) _log.info("MSG: " + message); @@ -168,16 +169,14 @@ public class SnarkManager implements Snark.CompleteListener { /** newest last */ public List<String> getMessages() { - synchronized (_messages) { - return new ArrayList(_messages); - } + if (_messages.isEmpty()) + return Collections.EMPTY_LIST; + return new ArrayList(_messages); } /** @since 0.9 */ public void clearMessages() { - synchronized (_messages) { _messages.clear(); - } } /** @@ -1192,13 +1191,11 @@ public class SnarkManager implements Snark.CompleteListener { // don't bother delaying if auto start is false long delay = 60 * 1000 * getStartupDelayMinutes(); if (delay > 0 && shouldAutoStart()) { - _messages.add(_("Adding torrents in {0}", DataHelper.formatDuration2(delay))); + addMessage(_("Adding torrents in {0}", DataHelper.formatDuration2(delay))); try { Thread.sleep(delay); } catch (InterruptedException ie) {} - // the first message was a "We are starting up in 1m" - synchronized (_messages) { - if (_messages.size() == 1) - _messages.remove(0); - } + // Remove that first message + if (_messages.size() == 1) + _messages.poll(); } // here because we need to delay until I2CP is up diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java index 34b81bbeaa..467094d7fc 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java @@ -50,6 +50,8 @@ public class Storage private File[] RAFfile; // File to make it easier to reopen /** priorities by file; default 0; may be null. @since 0.8.1 */ private int[] priorities; + /** is the file empty and sparse? */ + private boolean[] isSparse; private final StorageListener listener; private final I2PSnarkUtil _util; @@ -73,6 +75,8 @@ public class Storage private static final Map<String, String> _filterNameCache = new ConcurrentHashMap(); + private static final boolean _isWindows = System.getProperty("os.name").startsWith("Win"); + /** * Creates a new storage based on the supplied MetaInfo. This will * try to create and/or check all needed files in the MetaInfo. @@ -202,7 +206,7 @@ public class Storage RAFtime = new long[size]; RAFfile = new File[size]; priorities = new int[size]; - + isSparse = new boolean[size]; int i = 0; Iterator it = files.iterator(); @@ -462,6 +466,7 @@ public class Storage RAFlock = new Object[1]; RAFtime = new long[1]; RAFfile = new File[1]; + isSparse = new boolean[1]; lengths[0] = metainfo.getTotalLength(); RAFlock[0] = new Object(); RAFfile[0] = base; @@ -488,6 +493,7 @@ public class Storage RAFlock = new Object[size]; RAFtime = new long[size]; RAFfile = new File[size]; + isSparse = new boolean[size]; for (int i = 0; i < size; i++) { List<String> path = files.get(i); @@ -701,6 +707,7 @@ public class Storage } // Make sure all files are available and of correct length + // The files should all exist as they have been created with zero length by createFilesFromNames() for (int i = 0; i < rafs.length; i++) { long length = RAFfile[i].length(); @@ -725,6 +732,7 @@ public class Storage SnarkManager.instance().addMessage(msg); _util.debug(msg, Snark.ERROR); changed = true; + resume = true; _probablyComplete = false; // to force RW synchronized(RAFlock[i]) { checkRAF(i); @@ -798,26 +806,54 @@ public class Storage } } - /** this calls openRAF(); caller must synnchronize and call closeRAF() */ + /** + * This creates a (presumably) sparse file so that reads won't fail with IOE. + * Sets isSparse[nr] = true. balloonFile(nr) should be called later to + * defrag the file. + * + * This calls openRAF(); caller must synchronize and call closeRAF(). + */ private void allocateFile(int nr) throws IOException { // caller synchronized openRAF(nr, false); // RW - // XXX - Is this the best way to make sure we have enough space for - // the whole file? long remaining = lengths[nr]; if (listener != null) listener.storageCreateFile(this, names[nr], remaining); + rafs[nr].setLength(remaining); + // don't bother ballooning later on Windows since there is no sparse file support + // until JDK7 using the JSR-203 interface. + // RAF seeks/writes do not create sparse files. + // Windows will zero-fill up to the point of the write, which + // will make the file fairly unfragmented, on average, at least until + // near the end where it will get exponentially more fragmented. + if (!_isWindows) + isSparse[nr] = true; + // caller will close rafs[nr] + if (listener != null) + listener.storageAllocated(this, lengths[nr]); + } + + /** + * This "balloons" the file with zeros to eliminate disk fragmentation., + * Overwrites the entire file with zeros. Sets isSparse[nr] = false. + * + * Caller must synchronize and call checkRAF() or openRAF(). + * @since 0.9.1 + */ + private void balloonFile(int nr) throws IOException + { + _util.debug("Ballooning " + nr + ": " + RAFfile[nr], Snark.INFO); + long remaining = lengths[nr]; final int ZEROBLOCKSIZE = (int) Math.min(remaining, 32*1024); byte[] zeros = new byte[ZEROBLOCKSIZE]; + rafs[nr].seek(0); while (remaining > 0) { int size = (int) Math.min(remaining, ZEROBLOCKSIZE); rafs[nr].write(zeros, 0, size); remaining -= size; } - // caller will close rafs[nr] - if (listener != null) - listener.storageAllocated(this, lengths[nr]); + isSparse[nr] = false; } @@ -909,6 +945,17 @@ public class Storage int len = (start + need < raflen) ? need : (int)(raflen - start); synchronized(RAFlock[i]) { checkRAF(i); + if (isSparse[i]) { + // If the file is a newly created sparse file, + // AND we aren't skipping it, balloon it with all + // zeros to un-sparse it by allocating the space. + // Obviously this could take a while. + // Once we have written to it, it isn't empty/sparse any more. + if (priorities == null || priorities[i] >= 0) + balloonFile(i); + else + isSparse[i] = false; + } rafs[i].seek(start); //rafs[i].write(bs, off + written, len); pp.write(rafs[i], off + written, len); @@ -958,7 +1005,7 @@ public class Storage } return true; - } + } /** * This is a dup of MetaInfo.getPieceLength() but we need it diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index 06e4a6f432..b012d9b5ee 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -297,19 +297,23 @@ public class I2PSnarkServlet extends DefaultServlet { private void writeMessages(PrintWriter out, boolean isConfigure, String peerString) throws IOException { List<String> msgs = _manager.getMessages(); if (!msgs.isEmpty()) { - out.write("<div class=\"snarkMessages\"><ul>"); - for (int i = msgs.size()-1; i >= 0; i--) { - String msg = msgs.get(i); - out.write("<li>" + msg + "</li>\n"); - } - out.write("</ul><p><a href=\"/i2psnark/"); + out.write("<div class=\"snarkMessages\">"); + out.write("<a href=\"/i2psnark/"); if (isConfigure) out.write("configure"); if (peerString.length() > 0) out.write(peerString + "&"); else out.write("?"); - out.write("action=Clear&nonce=" + _nonce + "\">" + _("clear messages") + "</a></p></div>"); + out.write("action=Clear&nonce=" + _nonce + "\">" + + "<img src=\"" + _imgPath + "delete.png\" title=\"" + _("clear messages") + + "\" alt=\"" + _("clear messages") + "\"></a>" + + "<ul>"); + for (int i = msgs.size()-1; i >= 0; i--) { + String msg = msgs.get(i); + out.write("<li>" + msg + "</li>\n"); + } + out.write("</ul></div>"); } } diff --git a/installer/resources/themes/snark/ubergine/snark.css b/installer/resources/themes/snark/ubergine/snark.css index 5462a2e4fe..7336b97c95 100644 --- a/installer/resources/themes/snark/ubergine/snark.css +++ b/installer/resources/themes/snark/ubergine/snark.css @@ -93,7 +93,7 @@ body { border: 1px solid #000; overflow: auto; color: #26f; - max-height: 82px; + max-height: 76px; min-height: 45px; width: auto; background: #2a192a url('/themes/snark/ubergine/images/hat.png') no-repeat scroll right center; @@ -118,6 +118,12 @@ body { text-align: right; } +.snarkMessages img { + float: right; + margin: -3px -4px 4px 4px; + opacity: 0.8; +} + .logshim { margin-top: -10px !important; } diff --git a/installer/resources/themes/snark/vanilla/snark.css b/installer/resources/themes/snark/vanilla/snark.css index ad17393c5b..8a711f1827 100644 --- a/installer/resources/themes/snark/vanilla/snark.css +++ b/installer/resources/themes/snark/vanilla/snark.css @@ -92,7 +92,7 @@ body { border: 1px solid #000; overflow: auto; color: #26f; - max-height: 82px; + max-height: 76px; min-height: 45px; width: auto; background: #eda url('/themes/snark/ubergine/images/hat.png') no-repeat scroll right center; @@ -121,6 +121,12 @@ body { text-align: right; } +.snarkMessages img { + float: right; + margin: -3px -4px 4px 4px; + opacity: 0.8; +} + .logshim { margin-top: -10px !important; } -- GitLab