This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-mime4j.git
commit 7f56f6501afac8ce2b26a70b0d4038b1096a241f Author: Benoit Tellier <btell...@linagora.com> AuthorDate: Mon Jun 20 11:20:08 2022 +0700 MIME4J-318 Buffer recycling for ContentUtil copies Parsing 100K messages locally drops from 4,9s to 4.6s... --- .../james/mime4j/codec/Base64InputStream.java | 24 ++- .../org/apache/james/mime4j/codec/DecoderUtil.java | 22 ++- .../mime4j/codec/QuotedPrintableInputStream.java | 34 +++- .../mime4j/io/BufferedLineReaderInputStream.java | 40 ++++- .../james/mime4j/io/LineReaderInputStream.java | 2 + .../mime4j/io/LineReaderInputStreamAdaptor.java | 6 + .../james/mime4j/io/MimeBoundaryInputStream.java | 6 + .../james/mime4j/parser/MimeStreamParser.java | 1 + .../james/mime4j/stream/DefaultFieldBuilder.java | 26 ++- .../apache/james/mime4j/stream/FieldBuilder.java | 5 +- .../org/apache/james/mime4j/stream/MimeEntity.java | 46 +++-- .../james/mime4j/stream/MimeTokenStream.java | 8 +- .../apache/james/mime4j/util/BufferRecycler.java | 159 ++++++++++++++++++ .../org/apache/james/mime4j/util/ContentUtil.java | 24 ++- .../james/mime4j/util/RecycledByteArrayBuffer.java | 187 +++++++++++++++++++++ .../mime4j/stream/DefaultFieldBuilderTest.java | 9 +- .../mime4j/message/DefaultMessageBuilder.java | 1 + .../james/mime4j/utils/search/MessageMatcher.java | 5 +- 18 files changed, 560 insertions(+), 45 deletions(-) diff --git a/core/src/main/java/org/apache/james/mime4j/codec/Base64InputStream.java b/core/src/main/java/org/apache/james/mime4j/codec/Base64InputStream.java index 33b9e1d1..b4724316 100644 --- a/core/src/main/java/org/apache/james/mime4j/codec/Base64InputStream.java +++ b/core/src/main/java/org/apache/james/mime4j/codec/Base64InputStream.java @@ -21,13 +21,29 @@ package org.apache.james.mime4j.codec; import java.io.IOException; import java.io.InputStream; +import java.lang.ref.SoftReference; +import org.apache.james.mime4j.util.BufferRecycler; import org.apache.james.mime4j.util.ByteArrayBuffer; +import org.apache.james.mime4j.util.RecycledByteArrayBuffer; /** * Performs Base-64 decoding on an underlying stream. */ public class Base64InputStream extends InputStream { + protected static final ThreadLocal<SoftReference<BufferRecycler>> _recyclerRef = new ThreadLocal<>(); + + public static BufferRecycler getBufferRecycler() { + SoftReference<BufferRecycler> ref = _recyclerRef.get(); + BufferRecycler br = (ref == null) ? null : ref.get(); + + if (br == null) { + br = new BufferRecycler(); + ref = new SoftReference<>(br); + _recyclerRef.set(ref); + } + return br; + } private static final int ENCODED_BUFFER_SIZE = 1536; private static final int[] BASE64_DECODE = new int[256]; @@ -47,7 +63,7 @@ public class Base64InputStream extends InputStream { private final InputStream in; private final byte[] encoded; - private final ByteArrayBuffer decodedBuf; + private final RecycledByteArrayBuffer decodedBuf; private int position = 0; // current index into encoded buffer private int size = 0; // current size of encoded buffer @@ -64,8 +80,8 @@ public class Base64InputStream extends InputStream { protected Base64InputStream(int bufsize, InputStream in, DecodeMonitor monitor) { if (in == null) throw new IllegalArgumentException(); - this.encoded = new byte[bufsize]; - this.decodedBuf = new ByteArrayBuffer(512); + this.encoded = getBufferRecycler().allocByteBuffer(1, bufsize); + this.decodedBuf = new RecycledByteArrayBuffer(getBufferRecycler(), 512); this.in = in; this.monitor = monitor; } @@ -130,6 +146,8 @@ public class Base64InputStream extends InputStream { return; closed = true; + getBufferRecycler().releaseByteBuffer(1, encoded); + decodedBuf.release(); } private int read0(final byte[] buffer, final int off, final int len) throws IOException { diff --git a/core/src/main/java/org/apache/james/mime4j/codec/DecoderUtil.java b/core/src/main/java/org/apache/james/mime4j/codec/DecoderUtil.java index 8d37f8b9..14a981dd 100644 --- a/core/src/main/java/org/apache/james/mime4j/codec/DecoderUtil.java +++ b/core/src/main/java/org/apache/james/mime4j/codec/DecoderUtil.java @@ -21,18 +21,34 @@ package org.apache.james.mime4j.codec; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.lang.ref.SoftReference; import java.nio.charset.Charset; import java.util.Collections; import java.util.Map; import org.apache.james.mime4j.io.InputStreams; +import org.apache.james.mime4j.util.BufferRecycler; import org.apache.james.mime4j.util.ByteArrayBuffer; import org.apache.james.mime4j.util.CharsetUtil; +import org.apache.james.mime4j.util.RecycledByteArrayBuffer; /** * Static methods for decoding strings, byte arrays and encoded words. */ public class DecoderUtil { + protected static final ThreadLocal<SoftReference<BufferRecycler>> _recyclerRef = new ThreadLocal<>(); + + public static BufferRecycler getBufferRecycler() { + SoftReference<BufferRecycler> ref = _recyclerRef.get(); + BufferRecycler br = (ref == null) ? null : ref.get(); + + if (br == null) { + br = new BufferRecycler(); + ref = new SoftReference<>(br); + _recyclerRef.set(ref); + } + return br; + } /** * Decodes a string containing quoted-printable encoded data. @@ -44,8 +60,8 @@ public class DecoderUtil { try { QuotedPrintableInputStream is = new QuotedPrintableInputStream( InputStreams.createAscii(s), monitor); + RecycledByteArrayBuffer buf = new RecycledByteArrayBuffer(getBufferRecycler(), s.length()); try { - ByteArrayBuffer buf = new ByteArrayBuffer(s.length()); int b; while ((b = is.read()) != -1) { buf.append(b); @@ -53,6 +69,7 @@ public class DecoderUtil { return buf.toByteArray(); } finally { is.close(); + buf.release(); } } catch (IOException ex) { // This should never happen! @@ -71,8 +88,8 @@ public class DecoderUtil { try { Base64InputStream is = new Base64InputStream( InputStreams.createAscii(s), monitor); + RecycledByteArrayBuffer buf = new RecycledByteArrayBuffer(getBufferRecycler(), s.length()); try { - ByteArrayBuffer buf = new ByteArrayBuffer(s.length()); int b; while ((b = is.read()) != -1) { buf.append(b); @@ -80,6 +97,7 @@ public class DecoderUtil { return buf.toByteArray(); } finally { is.close(); + buf.release(); } } catch (IOException ex) { // This should never happen! diff --git a/core/src/main/java/org/apache/james/mime4j/codec/QuotedPrintableInputStream.java b/core/src/main/java/org/apache/james/mime4j/codec/QuotedPrintableInputStream.java index 39428349..0f43ac52 100644 --- a/core/src/main/java/org/apache/james/mime4j/codec/QuotedPrintableInputStream.java +++ b/core/src/main/java/org/apache/james/mime4j/codec/QuotedPrintableInputStream.java @@ -21,13 +21,29 @@ package org.apache.james.mime4j.codec; import java.io.IOException; import java.io.InputStream; +import java.lang.ref.SoftReference; +import org.apache.james.mime4j.util.BufferRecycler; import org.apache.james.mime4j.util.ByteArrayBuffer; +import org.apache.james.mime4j.util.RecycledByteArrayBuffer; /** * Performs Quoted-Printable decoding on an underlying stream. */ public class QuotedPrintableInputStream extends InputStream { + protected static final ThreadLocal<SoftReference<BufferRecycler>> _recyclerRef = new ThreadLocal<>(); + + public static BufferRecycler getBufferRecycler() { + SoftReference<BufferRecycler> ref = _recyclerRef.get(); + BufferRecycler br = (ref == null) ? null : ref.get(); + + if (br == null) { + br = new BufferRecycler(); + ref = new SoftReference<>(br); + _recyclerRef.set(ref); + } + return br; + } private static final int DEFAULT_BUFFER_SIZE = 1024 * 2; @@ -38,8 +54,8 @@ public class QuotedPrintableInputStream extends InputStream { private final byte[] singleByte = new byte[1]; private final InputStream in; - private final ByteArrayBuffer decodedBuf; - private final ByteArrayBuffer blanks; + private final RecycledByteArrayBuffer decodedBuf; + private final RecycledByteArrayBuffer blanks; private final byte[] encoded; private int pos = 0; // current index into encoded buffer @@ -57,9 +73,10 @@ public class QuotedPrintableInputStream extends InputStream { protected QuotedPrintableInputStream(final int bufsize, final InputStream in, DecodeMonitor monitor) { super(); this.in = in; - this.encoded = new byte[bufsize]; - this.decodedBuf = new ByteArrayBuffer(512); - this.blanks = new ByteArrayBuffer(512); + BufferRecycler recycler = getBufferRecycler(); + this.encoded = recycler.allocByteBuffer(1, bufsize); + this.decodedBuf = new RecycledByteArrayBuffer(recycler, 512); + this.blanks = new RecycledByteArrayBuffer(recycler, 512); this.closed = false; this.monitor = monitor; } @@ -79,12 +96,13 @@ public class QuotedPrintableInputStream extends InputStream { /** * Terminates Quoted-Printable coded content. This method does NOT close * the underlying input stream. - * - * @throws IOException on I/O errors. */ @Override - public void close() throws IOException { + public void close() { closed = true; + getBufferRecycler().releaseByteBuffer(1, encoded); + decodedBuf.release(); + blanks.release(); } private int fillBuffer() throws IOException { diff --git a/core/src/main/java/org/apache/james/mime4j/io/BufferedLineReaderInputStream.java b/core/src/main/java/org/apache/james/mime4j/io/BufferedLineReaderInputStream.java index 64d4122b..4dea0b44 100644 --- a/core/src/main/java/org/apache/james/mime4j/io/BufferedLineReaderInputStream.java +++ b/core/src/main/java/org/apache/james/mime4j/io/BufferedLineReaderInputStream.java @@ -19,16 +19,32 @@ package org.apache.james.mime4j.io; +import org.apache.james.mime4j.util.BufferRecycler; import org.apache.james.mime4j.util.ByteArrayBuffer; +import org.apache.james.mime4j.util.RecycledByteArrayBuffer; import java.io.IOException; import java.io.InputStream; +import java.lang.ref.SoftReference; /** * Input buffer that can be used to search for patterns using Quick Search * algorithm in data read from an {@link InputStream}. */ public class BufferedLineReaderInputStream extends LineReaderInputStream { + protected static final ThreadLocal<SoftReference<BufferRecycler>> _recyclerRef = new ThreadLocal<>(); + + public static BufferRecycler getBufferRecycler() { + SoftReference<BufferRecycler> ref = _recyclerRef.get(); + BufferRecycler br = (ref == null) ? null : ref.get(); + + if (br == null) { + br = new BufferRecycler(); + ref = new SoftReference<>(br); + _recyclerRef.set(ref); + } + return br; + } private boolean truncated; @@ -56,12 +72,13 @@ public class BufferedLineReaderInputStream extends LineReaderInputStream { if (buffersize <= 0) { throw new IllegalArgumentException("Buffer size may not be negative or zero"); } - this.buffer = new byte[buffersize]; + BufferRecycler bufferRecycler = getBufferRecycler(); + this.buffer = bufferRecycler.allocByteBuffer(0, buffersize); this.bufpos = 0; this.buflen = 0; this.maxLineLen = maxLineLen; this.truncated = false; - this.shiftTable = new int[256]; + this.shiftTable = bufferRecycler.allocintBuffer(256); } public BufferedLineReaderInputStream( @@ -132,6 +149,12 @@ public class BufferedLineReaderInputStream extends LineReaderInputStream { this.truncated = true; } + public void release() { + BufferRecycler bufferRecycler = getBufferRecycler(); + bufferRecycler.releaseByteBuffer(0, buffer); + bufferRecycler.releaseIntBuffer(shiftTable); + } + protected boolean readAllowed() { return !this.truncated; } @@ -388,4 +411,17 @@ public class BufferedLineReaderInputStream extends LineReaderInputStream { return true; } + @Override + public boolean unread(RecycledByteArrayBuffer buf) { + if (tempBuffer) return false; + origBuffer = buffer; + origBuflen = buflen; + origBufpos = bufpos; + bufpos = 0; + buflen = buf.length(); + buffer = buf.buffer(); + tempBuffer = true; + return true; + } + } diff --git a/core/src/main/java/org/apache/james/mime4j/io/LineReaderInputStream.java b/core/src/main/java/org/apache/james/mime4j/io/LineReaderInputStream.java index 22d00ee0..792eb38d 100644 --- a/core/src/main/java/org/apache/james/mime4j/io/LineReaderInputStream.java +++ b/core/src/main/java/org/apache/james/mime4j/io/LineReaderInputStream.java @@ -20,6 +20,7 @@ package org.apache.james.mime4j.io; import org.apache.james.mime4j.util.ByteArrayBuffer; +import org.apache.james.mime4j.util.RecycledByteArrayBuffer; import java.io.FilterInputStream; import java.io.IOException; @@ -60,5 +61,6 @@ public abstract class LineReaderInputStream extends FilterInputStream { * @return true if the unread has been succesfull. */ public abstract boolean unread(ByteArrayBuffer buf); + public abstract boolean unread(RecycledByteArrayBuffer buf); } diff --git a/core/src/main/java/org/apache/james/mime4j/io/LineReaderInputStreamAdaptor.java b/core/src/main/java/org/apache/james/mime4j/io/LineReaderInputStreamAdaptor.java index 36775846..387c99df 100644 --- a/core/src/main/java/org/apache/james/mime4j/io/LineReaderInputStreamAdaptor.java +++ b/core/src/main/java/org/apache/james/mime4j/io/LineReaderInputStreamAdaptor.java @@ -20,6 +20,7 @@ package org.apache.james.mime4j.io; import org.apache.james.mime4j.util.ByteArrayBuffer; +import org.apache.james.mime4j.util.RecycledByteArrayBuffer; import java.io.IOException; import java.io.InputStream; @@ -123,6 +124,11 @@ public class LineReaderInputStreamAdaptor extends LineReaderInputStream { return bis != null && bis.unread(buf); } + @Override + public boolean unread(RecycledByteArrayBuffer buf) { + return bis != null && bis.unread(buf); + } + @Override public long skip(long count) throws IOException { if (count <= 0) { diff --git a/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java b/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java index 88cdfc08..140f6b65 100644 --- a/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java +++ b/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java @@ -23,6 +23,7 @@ import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.MimeIOException; import org.apache.james.mime4j.util.ByteArrayBuffer; import org.apache.james.mime4j.util.CharsetUtil; +import org.apache.james.mime4j.util.RecycledByteArrayBuffer; import java.io.IOException; @@ -356,4 +357,9 @@ public class MimeBoundaryInputStream extends LineReaderInputStream { public boolean unread(ByteArrayBuffer buf) { return false; } + + @Override + public boolean unread(RecycledByteArrayBuffer buf) { + return false; + } } diff --git a/core/src/main/java/org/apache/james/mime4j/parser/MimeStreamParser.java b/core/src/main/java/org/apache/james/mime4j/parser/MimeStreamParser.java index d72124c6..cc816f85 100644 --- a/core/src/main/java/org/apache/james/mime4j/parser/MimeStreamParser.java +++ b/core/src/main/java/org/apache/james/mime4j/parser/MimeStreamParser.java @@ -131,6 +131,7 @@ public class MimeStreamParser { bodyContent = mimeTokenStream.getInputStream(); } handler.body(desc, bodyContent); + bodyContent.close(); break; case T_END_BODYPART: handler.endBodyPart(); diff --git a/core/src/main/java/org/apache/james/mime4j/stream/DefaultFieldBuilder.java b/core/src/main/java/org/apache/james/mime4j/stream/DefaultFieldBuilder.java index 6c65613a..1cb67cca 100644 --- a/core/src/main/java/org/apache/james/mime4j/stream/DefaultFieldBuilder.java +++ b/core/src/main/java/org/apache/james/mime4j/stream/DefaultFieldBuilder.java @@ -19,17 +19,33 @@ package org.apache.james.mime4j.stream; +import java.lang.ref.SoftReference; import java.util.BitSet; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.io.MaxHeaderLengthLimitException; +import org.apache.james.mime4j.util.BufferRecycler; import org.apache.james.mime4j.util.ByteArrayBuffer; +import org.apache.james.mime4j.util.RecycledByteArrayBuffer; /** * Default implementation of {@link FieldBuilder}. * */ public class DefaultFieldBuilder implements FieldBuilder { + protected static final ThreadLocal<SoftReference<BufferRecycler>> _recyclerRef = new ThreadLocal<>(); + + public static BufferRecycler getBufferRecycler() { + SoftReference<BufferRecycler> ref = _recyclerRef.get(); + BufferRecycler br = (ref == null) ? null : ref.get(); + + if (br == null) { + br = new BufferRecycler(); + ref = new SoftReference<>(br); + _recyclerRef.set(ref); + } + return br; + } private static final BitSet FIELD_CHARS = new BitSet(); @@ -42,11 +58,11 @@ public class DefaultFieldBuilder implements FieldBuilder { } } - private final ByteArrayBuffer buf; + private final RecycledByteArrayBuffer buf; private final int maxlen; public DefaultFieldBuilder(int maxlen) { - this.buf = new ByteArrayBuffer(1024); + this.buf = new RecycledByteArrayBuffer(getBufferRecycler(), 4096); this.maxlen = maxlen; } @@ -88,8 +104,12 @@ public class DefaultFieldBuilder implements FieldBuilder { return field; } - public ByteArrayBuffer getRaw() { + public RecycledByteArrayBuffer getRaw() { return this.buf; } + public void release() { + this.buf.release(); + } + } diff --git a/core/src/main/java/org/apache/james/mime4j/stream/FieldBuilder.java b/core/src/main/java/org/apache/james/mime4j/stream/FieldBuilder.java index a7a5815a..76aeccd9 100644 --- a/core/src/main/java/org/apache/james/mime4j/stream/FieldBuilder.java +++ b/core/src/main/java/org/apache/james/mime4j/stream/FieldBuilder.java @@ -21,6 +21,7 @@ package org.apache.james.mime4j.stream; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.util.ByteArrayBuffer; +import org.apache.james.mime4j.util.RecycledByteArrayBuffer; /** * <p> @@ -68,6 +69,8 @@ public interface FieldBuilder { * Returns combined content of all lines processed so far or <code>null</code> * if the builder does not retain original raw content. */ - ByteArrayBuffer getRaw(); + RecycledByteArrayBuffer getRaw(); + + void release(); } diff --git a/core/src/main/java/org/apache/james/mime4j/stream/MimeEntity.java b/core/src/main/java/org/apache/james/mime4j/stream/MimeEntity.java index 657fbd72..2b404a98 100644 --- a/core/src/main/java/org/apache/james/mime4j/stream/MimeEntity.java +++ b/core/src/main/java/org/apache/james/mime4j/stream/MimeEntity.java @@ -21,6 +21,7 @@ package org.apache.james.mime4j.stream; import java.io.IOException; import java.io.InputStream; +import java.lang.ref.SoftReference; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.codec.Base64InputStream; @@ -34,11 +35,26 @@ import org.apache.james.mime4j.io.LineReaderInputStreamAdaptor; import org.apache.james.mime4j.io.MaxHeaderLimitException; import org.apache.james.mime4j.io.MaxLineLimitException; import org.apache.james.mime4j.io.MimeBoundaryInputStream; +import org.apache.james.mime4j.util.BufferRecycler; import org.apache.james.mime4j.util.ByteArrayBuffer; import org.apache.james.mime4j.util.CharsetUtil; import org.apache.james.mime4j.util.MimeUtil; +import org.apache.james.mime4j.util.RecycledByteArrayBuffer; class MimeEntity implements EntityStateMachine { + protected static final ThreadLocal<SoftReference<BufferRecycler>> _recyclerRef = new ThreadLocal<>(); + + public static BufferRecycler getBufferRecycler() { + SoftReference<BufferRecycler> ref = _recyclerRef.get(); + BufferRecycler br = (ref == null) ? null : ref.get(); + + if (br == null) { + br = new BufferRecycler(); + ref = new SoftReference<>(br); + _recyclerRef.set(ref); + } + return br; + } private final EntityState endState; private final MimeConfig config; @@ -93,19 +109,6 @@ class MimeEntity implements EntityStateMachine { config.getMaxLineLen()); } - MimeEntity( - LineNumberSource lineSource, - InputStream instream, - MimeConfig config, - EntityState startState, - EntityState endState, - BodyDescriptorBuilder bodyDescBuilder) { - this(lineSource, instream, config, startState, endState, - config.isStrictParsing() ? DecodeMonitor.STRICT : DecodeMonitor.SILENT, - new DefaultFieldBuilder(config.getMaxHeaderLen()), - bodyDescBuilder); - } - MimeEntity( LineNumberSource lineSource, InputStream instream, @@ -154,6 +157,19 @@ class MimeEntity implements EntityStateMachine { } public void stop() { + stopSoft(); + inbuffer.release(); + getBufferRecycler().releaseByteBuffer(0, tmpbuf); + } + + public void stopSoft() { + if(currentMimePartStream != null) { + try { + currentMimePartStream.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } this.inbuffer.truncate(); } @@ -271,7 +287,7 @@ class MimeEntity implements EntityStateMachine { monitor(Event.INVALID_HEADER); if (config.isMalformedHeaderStartsBody()) { LineReaderInputStream instream = getDataStream(); - ByteArrayBuffer buf = fieldBuilder.getRaw(); + RecycledByteArrayBuffer buf = fieldBuilder.getRaw(); // Complain, if raw data is not available or cannot be 'unread' if (buf == null || !instream.unread(buf)) { throw new MimeParseEventException(Event.INVALID_HEADER); @@ -387,7 +403,7 @@ class MimeEntity implements EntityStateMachine { private void advanceToBoundary() throws IOException { if (!dataStream.eof()) { if (tmpbuf == null) { - tmpbuf = new byte[2048]; + tmpbuf = getBufferRecycler().allocByteBuffer(0, 2048); } InputStream instream = getLimitedContentStream(); while (instream.read(tmpbuf)!= -1) { diff --git a/core/src/main/java/org/apache/james/mime4j/stream/MimeTokenStream.java b/core/src/main/java/org/apache/james/mime4j/stream/MimeTokenStream.java index 2efc8a51..715eeeb9 100644 --- a/core/src/main/java/org/apache/james/mime4j/stream/MimeTokenStream.java +++ b/core/src/main/java/org/apache/james/mime4j/stream/MimeTokenStream.java @@ -258,7 +258,8 @@ public class MimeTokenStream { * triggered 'start' events. */ public void stop() { - rootentity.stop(); + rootentity.stopSoft(); + fieldBuilder.release(); } /** @@ -380,7 +381,10 @@ public class MimeTokenStream { if (state != EntityState.T_END_OF_STREAM) { return state; } - entities.removeLast(); + final EntityStateMachine entityStateMachine = entities.removeLast(); + if (entityStateMachine instanceof MimeEntity) { + ((MimeEntity) entityStateMachine).stop(); + } if (entities.isEmpty()) { currentStateMachine = null; } else { diff --git a/core/src/main/java/org/apache/james/mime4j/util/BufferRecycler.java b/core/src/main/java/org/apache/james/mime4j/util/BufferRecycler.java new file mode 100644 index 00000000..5fa264b2 --- /dev/null +++ b/core/src/main/java/org/apache/james/mime4j/util/BufferRecycler.java @@ -0,0 +1,159 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.util; + +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicReferenceArray; + +/** + * This is a small utility class, whose main functionality is to allow + * simple reuse of raw byte/char buffers. It is usually used through + * <code>ThreadLocal</code> member of the owning class pointing to + * instance of this class through a <code>SoftReference</code>. The + * end result is a low-overhead GC-cleanable recycling: hopefully + * ideal for use by stream readers. + *<p> + * Rewritten in 2.10 to be thread-safe (see [jackson-core#479] for details), + * to not rely on {@code ThreadLocal} access. + */ +public class BufferRecycler { + protected final ArrayList<byte[]>[] _byteBuffers; + protected final ArrayList<char[]>[] _charBuffers; + protected final ArrayList<int[]> _intBuffers; + + /** + * Default constructor used for creating instances of this default + * implementation. + */ + public BufferRecycler() { + this(4, 4); + } + + /** + * Alternate constructor to be used by sub-classes, to allow customization + * of number of low-level buffers in use. + * + * @param bbCount Number of {@code byte[]} buffers to allocate + * @param cbCount Number of {@code char[]} buffers to allocate + * + * @since 2.4 + */ + protected BufferRecycler(int bbCount, int cbCount) { + _byteBuffers = new ArrayList[bbCount]; + for (int i = 0; i < bbCount; i++) { + _byteBuffers[i] = new ArrayList<>(); + } + _charBuffers = new ArrayList[cbCount]; + for (int i = 0; i < cbCount; i++) { + _charBuffers[i] = new ArrayList<>(); + } + _intBuffers = new ArrayList<>(); + } + + /** + * @param ix One of <code>READ_IO_BUFFER</code> constants. + * + * @return Buffer allocated (possibly recycled) + */ + public final byte[] allocByteBuffer(int ix) { + return allocByteBuffer(ix, 0); + } + + public final int[] allocintBuffer(int minSize) { + final int DEF_SIZE = 256; + if (minSize < DEF_SIZE) { + minSize = DEF_SIZE; + } + final ArrayList<int[]> buffers = _intBuffers; + int[] buffer = null; + if (buffers.size() > 0) { + buffer = buffers.remove(buffers.size() -1); + } + if (buffer == null || buffer.length < minSize) { + buffer = new int[minSize]; + } + return buffer; + } + + public byte[] allocByteBuffer(int ix, int minSize) { + final int DEF_SIZE = 4000; + if (minSize < DEF_SIZE) { + minSize = DEF_SIZE; + } + final ArrayList<byte[]> buffers = _byteBuffers[ix]; + byte[] buffer = null; + if (buffers.size() > 0) { + buffer = buffers.remove(buffers.size() -1); + } + if (buffer == null || buffer.length < minSize) { + buffer = balloc(minSize); + } + return buffer; + } + + public void releaseByteBuffer(int ix, byte[] buffer) { + if (buffer == null) { + return; + } + _byteBuffers[ix].add(buffer); + } + + public void releaseIntBuffer(int[] buffer) { + if (buffer == null) { + return; + } + _intBuffers.add(buffer); + } + + public final char[] allocCharBuffer(int ix) { + return allocCharBuffer(ix, 0); + } + + public char[] allocCharBuffer(int ix, int minSize) { + final int DEF_SIZE = 4000; + if (minSize < DEF_SIZE) { + minSize = DEF_SIZE; + } + final ArrayList<char[]> buffers = _charBuffers[ix]; + char[] buffer = null; + if (buffers.size() > 0) { + buffer = buffers.remove(buffers.size() -1); + } + if (buffer == null || buffer.length < minSize) { + buffer = calloc(minSize); + } + return buffer; + } + + public void releaseCharBuffer(int ix, char[] buffer) { + _charBuffers[ix].add(buffer); + } + + protected byte[] balloc(int size) { + return new byte[size]; + } + + protected char[] calloc(int size) { + return new char[size]; + } +} diff --git a/core/src/main/java/org/apache/james/mime4j/util/ContentUtil.java b/core/src/main/java/org/apache/james/mime4j/util/ContentUtil.java index b519f4dd..a6c29696 100644 --- a/core/src/main/java/org/apache/james/mime4j/util/ContentUtil.java +++ b/core/src/main/java/org/apache/james/mime4j/util/ContentUtil.java @@ -26,6 +26,7 @@ import java.io.Reader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.lang.ref.SoftReference; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; @@ -37,11 +38,24 @@ import org.apache.james.mime4j.Charsets; * Utility methods for converting textual content of a message. */ public class ContentUtil { + protected static final ThreadLocal<SoftReference<BufferRecycler>> _recyclerRef = new ThreadLocal<>(); + + public static BufferRecycler getBufferRecycler() { + SoftReference<BufferRecycler> ref = _recyclerRef.get(); + BufferRecycler br = (ref == null) ? null : ref.get(); + + if (br == null) { + br = new BufferRecycler(); + ref = new SoftReference<>(br); + _recyclerRef.set(ref); + } + return br; + } private ContentUtil() { } - static final int DEFAULT_COPY_BUFFER_SIZE = 1024; + static final int DEFAULT_COPY_BUFFER_SIZE = 4096; /** * Copies the contents of one stream to the other. @@ -50,11 +64,13 @@ public class ContentUtil { * @throws IOException */ public static void copy(final InputStream in, final OutputStream out) throws IOException { - final byte[] buffer = new byte[DEFAULT_COPY_BUFFER_SIZE]; + BufferRecycler bufferRecycler = getBufferRecycler(); + byte[] buffer = bufferRecycler.allocByteBuffer(0, DEFAULT_COPY_BUFFER_SIZE); int inputLength; while (-1 != (inputLength = in.read(buffer))) { out.write(buffer, 0, inputLength); } + bufferRecycler.releaseByteBuffer(0, buffer); } /** @@ -64,11 +80,13 @@ public class ContentUtil { * @throws IOException */ public static void copy(final Reader in, final Writer out) throws IOException { - final char[] buffer = new char[DEFAULT_COPY_BUFFER_SIZE]; + BufferRecycler bufferRecycler = getBufferRecycler(); + char[] buffer = bufferRecycler.allocCharBuffer(0, DEFAULT_COPY_BUFFER_SIZE); int inputLength; while (-1 != (inputLength = in.read(buffer))) { out.write(buffer, 0, inputLength); } + bufferRecycler.releaseCharBuffer(0, buffer); } public static byte[] buffer(final InputStream in) throws IOException { diff --git a/core/src/main/java/org/apache/james/mime4j/util/RecycledByteArrayBuffer.java b/core/src/main/java/org/apache/james/mime4j/util/RecycledByteArrayBuffer.java new file mode 100644 index 00000000..900381a4 --- /dev/null +++ b/core/src/main/java/org/apache/james/mime4j/util/RecycledByteArrayBuffer.java @@ -0,0 +1,187 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.util; + + +/** + * A resizable byte array. + */ +public final class RecycledByteArrayBuffer implements ByteSequence { + private final BufferRecycler bufferRecycler; + private byte[] buffer; + private int len; + + public RecycledByteArrayBuffer(BufferRecycler bufferRecycler, int capacity) { + super(); + if (capacity < 0) { + throw new IllegalArgumentException("Buffer capacity may not be negative"); + } + this.buffer = bufferRecycler.allocByteBuffer(0, capacity); + this.bufferRecycler = bufferRecycler; + } + + public RecycledByteArrayBuffer(BufferRecycler bufferRecycler, byte[] bytes, boolean dontCopy) { + this(bufferRecycler, bytes, bytes.length, dontCopy); + } + + public RecycledByteArrayBuffer(BufferRecycler bufferRecycler, byte[] bytes, int len, boolean dontCopy) { + if (bytes == null) + throw new IllegalArgumentException(); + if (len < 0 || len > bytes.length) + throw new IllegalArgumentException(); + + if (dontCopy) { + this.buffer = bytes; + } else { + this.buffer = bufferRecycler.allocByteBuffer(0, len); + System.arraycopy(bytes, 0, this.buffer, 0, len); + } + this.bufferRecycler = bufferRecycler; + this.len = len; + } + + private void expand(int newlen) { + byte newbuffer[] = bufferRecycler.allocByteBuffer(0, Math.max(this.buffer.length << 1, newlen)); + System.arraycopy(this.buffer, 0, newbuffer, 0, this.len); + bufferRecycler.releaseByteBuffer(0, buffer); + this.buffer = newbuffer; + } + + public void append(final byte[] b, int off, int len) { + if (b == null) { + return; + } + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) < 0) || ((off + len) > b.length)) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return; + } + int newlen = this.len + len; + if (newlen > this.buffer.length) { + expand(newlen); + } + System.arraycopy(b, off, this.buffer, this.len, len); + this.len = newlen; + } + + public void append(int b) { + int newlen = this.len + 1; + if (newlen > this.buffer.length) { + expand(newlen); + } + this.buffer[this.len] = (byte)b; + this.len = newlen; + } + + public void clear() { + this.len = 0; + } + + public byte[] toByteArray() { + byte[] b = new byte[this.len]; + if (this.len > 0) { + System.arraycopy(this.buffer, 0, b, 0, this.len); + } + return b; + } + + public byte byteAt(int i) { + if (i < 0 || i >= this.len) + throw new IndexOutOfBoundsException(); + + return this.buffer[i]; + } + + public int capacity() { + return this.buffer.length; + } + + public int length() { + return this.len; + } + + public byte[] buffer() { + return this.buffer; + } + + public int indexOf(byte b) { + return indexOf(b, 0, this.len); + } + + public int indexOf(byte b, int beginIndex, int endIndex) { + if (beginIndex < 0) { + beginIndex = 0; + } + if (endIndex > this.len) { + endIndex = this.len; + } + if (beginIndex > endIndex) { + return -1; + } + for (int i = beginIndex; i < endIndex; i++) { + if (this.buffer[i] == b) { + return i; + } + } + return -1; + } + + public void setLength(int len) { + if (len < 0 || len > this.buffer.length) { + throw new IndexOutOfBoundsException(); + } + this.len = len; + } + + public void remove(int off, int len) { + if ((off < 0) || (off > this.len) || (len < 0) || + ((off + len) < 0) || ((off + len) > this.len)) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return; + } + int remaining = this.len - off - len; + if (remaining > 0) { + System.arraycopy(this.buffer, off + len, this.buffer, off, remaining); + } + this.len -= len; + } + + public boolean isEmpty() { + return this.len == 0; + } + + public boolean isFull() { + return this.len == this.buffer.length; + } + + @Override + public String toString() { + return new String(toByteArray()); + } + + public void release() { + bufferRecycler.releaseByteBuffer(0, buffer); + } + +} diff --git a/core/src/test/java/org/apache/james/mime4j/stream/DefaultFieldBuilderTest.java b/core/src/test/java/org/apache/james/mime4j/stream/DefaultFieldBuilderTest.java index 98a630c1..e9d89fed 100644 --- a/core/src/test/java/org/apache/james/mime4j/stream/DefaultFieldBuilderTest.java +++ b/core/src/test/java/org/apache/james/mime4j/stream/DefaultFieldBuilderTest.java @@ -22,6 +22,7 @@ package org.apache.james.mime4j.stream; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.util.ByteArrayBuffer; import org.apache.james.mime4j.util.ByteSequence; +import org.apache.james.mime4j.util.RecycledByteArrayBuffer; import junit.framework.TestCase; @@ -40,7 +41,7 @@ public class DefaultFieldBuilderTest extends TestCase { builder.append(line("raw: stuff;\r\n")); builder.append(line(" more stuff;\r\n")); builder.append(line(" a lot more stuff\r\n")); - ByteArrayBuffer buf = builder.getRaw(); + RecycledByteArrayBuffer buf = builder.getRaw(); assertNotNull(buf); assertEquals("raw: stuff;\r\n more stuff;\r\n a lot more stuff\r\n", new String(buf.toByteArray(), "US-ASCII")); @@ -60,7 +61,7 @@ public class DefaultFieldBuilderTest extends TestCase { builder.append(line("raw : stuff;\r\n")); builder.append(line(" more stuff;\r\n")); builder.append(line(" a lot more stuff\r\n")); - ByteArrayBuffer buf = builder.getRaw(); + RecycledByteArrayBuffer buf = builder.getRaw(); assertNotNull(buf); assertEquals("raw : stuff;\r\n more stuff;\r\n a lot more stuff\r\n", new String(buf.toByteArray(), "US-ASCII")); @@ -80,7 +81,7 @@ public class DefaultFieldBuilderTest extends TestCase { builder.append(line("raw: stuff;\r\n")); builder.append(line(" more stuff;\r\n")); builder.append(line(" a lot more stuff")); - ByteArrayBuffer buf = builder.getRaw(); + RecycledByteArrayBuffer buf = builder.getRaw(); assertNotNull(buf); assertEquals("raw: stuff;\r\n more stuff;\r\n a lot more stuff", new String(buf.toByteArray(), "US-ASCII")); @@ -109,7 +110,7 @@ public class DefaultFieldBuilderTest extends TestCase { DefaultFieldBuilder builder = new DefaultFieldBuilder(0); builder.reset(); builder.append(line("raw: some stuff\r\n")); - ByteArrayBuffer buf = builder.getRaw(); + RecycledByteArrayBuffer buf = builder.getRaw(); assertNotNull(buf); assertEquals("raw: some stuff\r\n", new String(buf.toByteArray(), "US-ASCII")); builder.reset(); diff --git a/dom/src/main/java/org/apache/james/mime4j/message/DefaultMessageBuilder.java b/dom/src/main/java/org/apache/james/mime4j/message/DefaultMessageBuilder.java index db6cff8e..2a2bb2d3 100644 --- a/dom/src/main/java/org/apache/james/mime4j/message/DefaultMessageBuilder.java +++ b/dom/src/main/java/org/apache/james/mime4j/message/DefaultMessageBuilder.java @@ -319,6 +319,7 @@ public class DefaultMessageBuilder implements MessageBuilder { parser.setRecurse(); } parser.parse(is); + parser.stop(); return message; } catch (MimeException e) { throw new MimeIOException(e); diff --git a/james-utils/src/main/java/org/apache/james/mime4j/utils/search/MessageMatcher.java b/james-utils/src/main/java/org/apache/james/mime4j/utils/search/MessageMatcher.java index ad52255c..1881c41f 100644 --- a/james-utils/src/main/java/org/apache/james/mime4j/utils/search/MessageMatcher.java +++ b/james-utils/src/main/java/org/apache/james/mime4j/utils/search/MessageMatcher.java @@ -199,8 +199,9 @@ public class MessageMatcher { } private boolean checkBody(final CharBuffer buffer, MimeTokenStream parser) throws IOException { - final Reader reader = parser.getReader(); - return isFoundIn(reader, buffer); + try (Reader reader = parser.getReader()) { + return isFoundIn(reader, buffer); + } } private CharBuffer createBuffer(final CharSequence searchContent) { --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org