Reduce memory overhead in Message and MimeMessage

Message and MimeMessage were creating a lot of unnecessary sub-objects
even when not needed, so do a bunch of lazy initialization.  This should
raise the bar on the size of gigantic inboxes giving us trouble.

* Specific optimizations:
  * Replace date formatter with a shared static
  * lazy create mHeader (ArrayList)
  * lazy create mFlags (HashSet)
  * optimize MimeHeader fields class
  * lazy create local message-ID (expensive-to-make uuid String)
  * make message-id string less expensive to create
* Other cleanups:
  * add some override annotations
  * privatize some members
  * update a fragile test (not a deep fix, it's still fragile)

Side effect, should be faster too.

Bug: 2357564
Bug: 2093422
Change-Id: I8a873879d402e2662339d5398ad0b15da6e580e9
This commit is contained in:
Andrew Stadler 2010-03-09 20:30:47 -08:00
parent 94c6f4f84f
commit 44552da606
6 changed files with 80 additions and 54 deletions

View File

@ -26,7 +26,7 @@ public abstract class Message implements Part, Body {
protected String mUid;
protected HashSet<Flag> mFlags = new HashSet<Flag>();
private HashSet<Flag> mFlags = null;
protected Date mInternalDate;
@ -93,24 +93,35 @@ public abstract class Message implements Part, Body {
public abstract void removeHeader(String name) throws MessagingException;
// Always use these instead of getHeader("Message-ID") or setHeader("Message-ID");
public abstract void setMessageId(String messageId) throws MessagingException;
public abstract String getMessageId() throws MessagingException;
public abstract void setBody(Body body) throws MessagingException;
public boolean isMimeType(String mimeType) throws MessagingException {
return getContentType().startsWith(mimeType);
}
private HashSet<Flag> getFlagSet() {
if (mFlags == null) {
mFlags = new HashSet<Flag>();
}
return mFlags;
}
/*
* TODO Refactor Flags at some point to be able to store user defined flags.
*/
public Flag[] getFlags() {
return mFlags.toArray(new Flag[] {});
return getFlagSet().toArray(new Flag[] {});
}
public void setFlag(Flag flag, boolean set) throws MessagingException {
if (set) {
mFlags.add(flag);
getFlagSet().add(flag);
} else {
mFlags.remove(flag);
getFlagSet().remove(flag);
}
}
@ -126,7 +137,7 @@ public abstract class Message implements Part, Body {
}
public boolean isSet(Flag flag) {
return mFlags.contains(flag);
return getFlagSet().contains(flag);
}
public abstract void saveChanges() throws MessagingException;

View File

@ -131,10 +131,9 @@ public class MimeHeader {
writer.flush();
}
class Field {
String name;
String value;
private static class Field {
final String name;
final String value;
public Field(String name, String value) {
this.name = name;

View File

@ -33,7 +33,6 @@ import org.apache.james.mime4j.field.Field;
import android.text.TextUtils;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -49,28 +48,31 @@ import java.util.regex.Pattern;
* RFC 2045 style headers.
*/
public class MimeMessage extends Message {
protected MimeHeader mHeader = new MimeHeader();
protected MimeHeader mExtendedHeader;
private MimeHeader mHeader;
private MimeHeader mExtendedHeader;
// NOTE: The fields here are transcribed out of headers, and values stored here will supercede
// the values found in the headers. Use caution to prevent any out-of-phase errors. In
// particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
protected Address[] mFrom;
protected Address[] mTo;
protected Address[] mCc;
protected Address[] mBcc;
protected Address[] mReplyTo;
protected Date mSentDate;
private Address[] mFrom;
private Address[] mTo;
private Address[] mCc;
private Address[] mBcc;
private Address[] mReplyTo;
private Date mSentDate;
private Body mBody;
protected int mSize;
// Shared random source for generating local message-id values
private static java.util.Random sRandom = new java.util.Random();
// In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
// "Jan", not the other localized format like "Ene" (meaning January in locale es).
// This conversion is used when generating outgoing MIME messages. Incoming MIME date
// headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
// localization code.
protected SimpleDateFormat mDateFormat =
private static final SimpleDateFormat DATE_FORMAT =
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
protected Body mBody;
protected int mSize;
// regex that matches content id surrounded by "<>" optionally.
private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
@ -78,24 +80,22 @@ public class MimeMessage extends Message {
private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
public MimeMessage() {
/*
* Every new messages gets a Message-ID
*/
try {
// TODO: This is wasteful, since we overwrite it on incoming or locally-read messages.
// Should only generate it on as-needed basis.
setMessageId(generateMessageId());
}
catch (MessagingException me) {
throw new RuntimeException("Unable to create MimeMessage", me);
}
mHeader = null;
}
/**
* Generate a local message id. This is only used when none has been assigned, and is
* installed lazily. Any remote (typically server-assigned) message id takes precedence.
* @return a long, locally-generated message-ID value
*/
private String generateMessageId() {
StringBuffer sb = new StringBuffer();
sb.append("<");
for (int i = 0; i < 24; i++) {
sb.append(Integer.toString((int)(Math.random() * 35), 36));
// We'll use a 5-bit range (0..31)
int value = sRandom.nextInt() & 31;
char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
sb.append(c);
}
sb.append(".");
sb.append(Long.toString(System.currentTimeMillis()));
@ -117,7 +117,7 @@ public class MimeMessage extends Message {
protected void parse(InputStream in) throws IOException, MessagingException {
// Before parsing the input stream, clear all local fields that may be superceded by
// the new incoming message.
mHeader.clear();
getMimeHeaders().clear();
mFrom = null;
mTo = null;
mCc = null;
@ -131,6 +131,17 @@ public class MimeMessage extends Message {
parser.parse(new EOLConvertingInputStream(in));
}
/**
* Return the internal mHeader value, with very lazy initialization.
* The goal is to save memory by not creating the headers until needed.
*/
private MimeHeader getMimeHeaders() {
if (mHeader == null) {
mHeader = new MimeHeader();
}
return mHeader;
}
public Date getReceivedDate() throws MessagingException {
return null;
}
@ -149,7 +160,7 @@ public class MimeMessage extends Message {
}
public void setSentDate(Date sentDate) throws MessagingException {
setHeader("Date", mDateFormat.format(sentDate));
setHeader("Date", DATE_FORMAT.format(sentDate));
this.mSentDate = sentDate;
}
@ -305,23 +316,25 @@ public class MimeMessage extends Message {
* @param messageId the new Message-ID value
* @throws MessagingException
*/
@Override
public void setMessageId(String messageId) throws MessagingException {
setHeader("Message-ID", messageId);
}
/**
* Get the mime "Message-ID" header. Note, this field is preset (randomly) in every new
* message, so it should never return null.
* Get the mime "Message-ID" header. This value will be preloaded with a locally-generated
* random ID, if the value has not previously been set.
* @return the Message-ID header string
* @throws MessagingException
*/
@Override
public String getMessageId() throws MessagingException {
String[] headers = getHeader("Message-ID");
if (headers != null) {
// There should really only be one Message-ID here
return headers[0];
String messageId = getFirstHeader("Message-ID");
if (messageId == null) {
messageId = generateMessageId();
setMessageId(messageId);
}
throw new MessagingException("A message was found without a Message-ID header");
return messageId;
}
public void saveChanges() throws MessagingException {
@ -348,23 +361,23 @@ public class MimeMessage extends Message {
}
protected String getFirstHeader(String name) throws MessagingException {
return mHeader.getFirstHeader(name);
return getMimeHeaders().getFirstHeader(name);
}
public void addHeader(String name, String value) throws MessagingException {
mHeader.addHeader(name, value);
getMimeHeaders().addHeader(name, value);
}
public void setHeader(String name, String value) throws MessagingException {
mHeader.setHeader(name, value);
getMimeHeaders().setHeader(name, value);
}
public String[] getHeader(String name) throws MessagingException {
return mHeader.getHeader(name);
return getMimeHeaders().getHeader(name);
}
public void removeHeader(String name) throws MessagingException {
mHeader.removeHeader(name);
getMimeHeaders().removeHeader(name);
}
/**
@ -443,7 +456,9 @@ public class MimeMessage extends Message {
*/
public void writeTo(OutputStream out) throws IOException, MessagingException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
mHeader.writeTo(out);
// Force creation of local message-id
getMessageId();
getMimeHeaders().writeTo(out);
// mExtendedHeader will not be write out to external output stream,
// because it is intended to internal use.
writer.write("\r\n");

View File

@ -1198,11 +1198,10 @@ public class ImapStore extends Store {
* Message-ID header. If there are more than one response, take the
* last one, as it's most likely he newest (the one we just uploaded).
*/
String[] messageIdHeader = message.getHeader("Message-ID");
if (messageIdHeader == null || messageIdHeader.length == 0) {
String messageId = message.getMessageId();
if (messageId == null || messageId.length() == 0) {
continue;
}
String messageId = messageIdHeader[0];
List<ImapResponse> responses =
mConnection.executeSimpleCommand(
String.format("UID SEARCH (HEADER MESSAGE-ID %s)", messageId));
@ -1230,6 +1229,7 @@ public class ImapStore extends Store {
return null;
}
@Override
public void setFlags(Message[] messages, Flag[] flags, boolean value)
throws MessagingException {
checkOpen();

View File

@ -1651,6 +1651,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
return mId;
}
@Override
public void setFlag(Flag flag, boolean set) throws MessagingException {
if (flag == Flag.DELETED && set) {
/*

View File

@ -61,8 +61,6 @@ public class MimeMessageTest extends TestCase {
private final String LONG_PLAIN_256 =
LONG_PLAIN_64 + LONG_PLAIN_64 + LONG_PLAIN_64 + LONG_PLAIN_64;
// TODO: more tests.
/**
* Confirms that setSentDate() correctly set the "Date" header of a Mime message.
*
@ -397,6 +395,7 @@ public class MimeMessageTest extends TestCase {
/*
* Test for writeTo(), only for header part.
* NOTE: This test is fragile because it assumes headers will be written in a specific order
*/
public void testWriteToHeader() throws Exception {
MimeMessage message = new MimeMessage();
@ -410,9 +409,10 @@ public class MimeMessageTest extends TestCase {
ByteArrayOutputStream out = new ByteArrayOutputStream();
message.writeTo(out);
out.close();
String expectedString = "Message-ID: " + message.getMessageId() + "\r\n" +
String expectedString =
"Header1: value1\r\n" +
"Header4: value4\r\n" +
"Message-ID: " + message.getMessageId() + "\r\n" +
"\r\n";
byte[] expected = expectedString.getBytes();
byte[] actual = out.toByteArray();