Skip to content

Commit

Permalink
Updated ImapStream, ImapEngine, and ImapCommand to reuse ByteArrayBui…
Browse files Browse the repository at this point in the history
…lders

This drastically reduces the number of allocations made when tokenizing
IMAP responses.

ImapCommand's usage was not really a major issue, but since ImapEngine.ReadLine/Async()
needed a reusable ByteArrayBuilder anyway, might as well share that with ImapCommand.
  • Loading branch information
jstedfast committed Aug 27, 2023
1 parent 7632855 commit 9e8e83a
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 178 deletions.
6 changes: 6 additions & 0 deletions MailKit.sln
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailKit", "MailKit\MailKit.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "UnitTests\UnitTests.csproj", "{1B670279-AEA7-4D9B-A854-CB4CC177B277}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{051C136B-8354-4893-B9F8-11E1B35ABBDF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -32,6 +34,10 @@ Global
{1B670279-AEA7-4D9B-A854-CB4CC177B277}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B670279-AEA7-4D9B-A854-CB4CC177B277}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B670279-AEA7-4D9B-A854-CB4CC177B277}.Release|Any CPU.Build.0 = Release|Any CPU
{051C136B-8354-4893-B9F8-11E1B35ABBDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{051C136B-8354-4893-B9F8-11E1B35ABBDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{051C136B-8354-4893-B9F8-11E1B35ABBDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{051C136B-8354-4893-B9F8-11E1B35ABBDF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
163 changes: 81 additions & 82 deletions MailKit/Net/Imap/ImapCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -472,92 +472,91 @@ public ImapCommand (ImapEngine engine, CancellationToken cancellationToken, Imap
Engine = engine;
Folder = folder;

using (var builder = new ByteArrayBuilder (1024)) {
byte[] buf, utf8 = new byte[8];
int argc = 0;
string str;

for (int i = 0; i < format.Length; i++) {
if (format[i] == '%') {
switch (format[++i]) {
case '%': // a literal %
builder.Append ((byte) '%');
break;
case 'd': // an integer
str = ((int) args[argc++]).ToString (CultureInfo.InvariantCulture);
buf = Encoding.ASCII.GetBytes (str);
builder.Append (buf, 0, buf.Length);
break;
case 'u': // an unsigned integer
str = ((uint) args[argc++]).ToString (CultureInfo.InvariantCulture);
buf = Encoding.ASCII.GetBytes (str);
builder.Append (buf, 0, buf.Length);
break;
case 's':
str = (string) args[argc++];
buf = Encoding.ASCII.GetBytes (str);
builder.Append (buf, 0, buf.Length);
break;
case 'F': // an ImapFolder
var utf7 = ((ImapFolder) args[argc++]).EncodedName;
AppendString (options, true, builder, utf7);
break;
case 'L': // a MimeMessage or a byte[]
var arg = args[argc++];
ImapLiteral literal;
byte[] prefix;

if (arg is MimeMessage message) {
prefix = options.International ? UTF8LiteralTokenPrefix : LiteralTokenPrefix;
literal = new ImapLiteral (options, message, UpdateProgress);
} else {
literal = new ImapLiteral (options, (byte[]) arg);
prefix = LiteralTokenPrefix;
}

var length = literal.Length;
bool wait = true;

builder.Append (prefix, 0, prefix.Length);
buf = Encoding.ASCII.GetBytes (length.ToString (CultureInfo.InvariantCulture));
builder.Append (buf, 0, buf.Length);

if (CanUseNonSynchronizedLiteral (Engine, length)) {
builder.Append ((byte) '+');
wait = false;
}

builder.Append (LiteralTokenSuffix, 0, LiteralTokenSuffix.Length);

totalSize += length;

parts.Add (new ImapCommandPart (builder.ToArray (), literal, wait));
builder.Clear ();

if (prefix == UTF8LiteralTokenPrefix)
builder.Append ((byte) ')');
break;
case 'S': // a string which may need to be quoted or made into a literal
AppendString (options, true, builder, (string) args[argc++]);
break;
case 'Q': // similar to %S but string must be quoted at a minimum
AppendString (options, false, builder, (string) args[argc++]);
break;
default:
throw new FormatException ($"The %{format[i]} format specifier is not supported.");
var builder = engine.GetCommandBuilder ();
byte[] buf, utf8 = new byte[8];
int argc = 0;
string str;

for (int i = 0; i < format.Length; i++) {
if (format[i] == '%') {
switch (format[++i]) {
case '%': // a literal %
builder.Append ((byte) '%');
break;
case 'd': // an integer
str = ((int) args[argc++]).ToString (CultureInfo.InvariantCulture);
buf = Encoding.ASCII.GetBytes (str);
builder.Append (buf, 0, buf.Length);
break;
case 'u': // an unsigned integer
str = ((uint) args[argc++]).ToString (CultureInfo.InvariantCulture);
buf = Encoding.ASCII.GetBytes (str);
builder.Append (buf, 0, buf.Length);
break;
case 's':
str = (string) args[argc++];
buf = Encoding.ASCII.GetBytes (str);
builder.Append (buf, 0, buf.Length);
break;
case 'F': // an ImapFolder
var utf7 = ((ImapFolder) args[argc++]).EncodedName;
AppendString (options, true, builder, utf7);
break;
case 'L': // a MimeMessage or a byte[]
var arg = args[argc++];
ImapLiteral literal;
byte[] prefix;

if (arg is MimeMessage message) {
prefix = options.International ? UTF8LiteralTokenPrefix : LiteralTokenPrefix;
literal = new ImapLiteral (options, message, UpdateProgress);
} else {
literal = new ImapLiteral (options, (byte[]) arg);
prefix = LiteralTokenPrefix;
}
} else if (format[i] < 128) {
builder.Append ((byte) format[i]);
} else {
int nchars = char.IsSurrogate (format[i]) ? 2 : 1;
int nbytes = Encoding.UTF8.GetBytes (format, i, nchars, utf8, 0);
builder.Append (utf8, 0, nbytes);
i += nchars - 1;

var length = literal.Length;
bool wait = true;

builder.Append (prefix, 0, prefix.Length);
buf = Encoding.ASCII.GetBytes (length.ToString (CultureInfo.InvariantCulture));
builder.Append (buf, 0, buf.Length);

if (CanUseNonSynchronizedLiteral (Engine, length)) {
builder.Append ((byte) '+');
wait = false;
}

builder.Append (LiteralTokenSuffix, 0, LiteralTokenSuffix.Length);

totalSize += length;

parts.Add (new ImapCommandPart (builder.ToArray (), literal, wait));
builder.Clear ();

if (prefix == UTF8LiteralTokenPrefix)
builder.Append ((byte) ')');
break;
case 'S': // a string which may need to be quoted or made into a literal
AppendString (options, true, builder, (string) args[argc++]);
break;
case 'Q': // similar to %S but string must be quoted at a minimum
AppendString (options, false, builder, (string) args[argc++]);
break;
default:
throw new FormatException ($"The %{format[i]} format specifier is not supported.");
}
} else if (format[i] < 128) {
builder.Append ((byte) format[i]);
} else {
int nchars = char.IsSurrogate (format[i]) ? 2 : 1;
int nbytes = Encoding.UTF8.GetBytes (format, i, nchars, utf8, 0);
builder.Append (utf8, 0, nbytes);
i += nchars - 1;
}

parts.Add (new ImapCommandPart (builder.ToArray (), null));
}

parts.Add (new ImapCommandPart (builder.ToArray (), null));
}

/// <summary>
Expand Down
46 changes: 28 additions & 18 deletions MailKit/Net/Imap/ImapEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ class ImapEngine : IDisposable
readonly CreateImapFolderDelegate createImapFolder;
readonly ImapFolderNameComparer cacheComparer;
internal ImapQuirksMode QuirksMode;
readonly ByteArrayBuilder builder;
readonly List<ImapCommand> queue;
internal char TagPrefix;
ImapCommand current;
Expand All @@ -157,6 +158,8 @@ class ImapEngine : IDisposable

public ImapEngine (CreateImapFolderDelegate createImapFolderDelegate)
{
// The builder is used as a buffer for line-reading as well as ImapCommand building, so 1K is probably realistic.
builder = new ByteArrayBuilder (1024);
cacheComparer = new ImapFolderNameComparer ('.');

FolderCache = new Dictionary<string, ImapFolder> (cacheComparer);
Expand Down Expand Up @@ -525,6 +528,13 @@ public ImapFolder Trash {

#endregion

internal ByteArrayBuilder GetCommandBuilder ()
{
builder.Clear ();

return builder;
}

internal ImapFolder CreateImapFolder (string encodedName, FolderAttributes attributes, char delim)
{
var args = new ImapFolderConstructorArgs (this, encodedName, attributes, delim);
Expand Down Expand Up @@ -842,18 +852,18 @@ public void Disconnect ()
/// </exception>
public string ReadLine (CancellationToken cancellationToken)
{
using (var builder = new ByteArrayBuilder (64)) {
bool complete;
builder.Clear ();

do {
complete = Stream.ReadLine (builder, cancellationToken);
} while (!complete);
bool complete;

do {
complete = Stream.ReadLine (builder, cancellationToken);
} while (!complete);

// FIXME: All callers expect CRLF to be trimmed, but many also want all trailing whitespace trimmed.
builder.TrimNewLine ();
// FIXME: All callers expect CRLF to be trimmed, but many also want all trailing whitespace trimmed.
builder.TrimNewLine ();

return builder.ToString ();
}
return builder.ToString ();
}

/// <summary>
Expand All @@ -875,18 +885,18 @@ public string ReadLine (CancellationToken cancellationToken)
/// </exception>
public async Task<string> ReadLineAsync (CancellationToken cancellationToken)
{
using (var builder = new ByteArrayBuilder (64)) {
bool complete;
builder.Clear ();

do {
complete = await Stream.ReadLineAsync (builder, cancellationToken).ConfigureAwait (false);
} while (!complete);
bool complete;

// FIXME: All callers expect CRLF to be trimmed, but many also want all trailing whitespace trimmed.
builder.TrimNewLine ();
do {
complete = await Stream.ReadLineAsync (builder, cancellationToken).ConfigureAwait (false);
} while (!complete);

return builder.ToString ();
}
// FIXME: All callers expect CRLF to be trimmed, but many also want all trailing whitespace trimmed.
builder.TrimNewLine ();

return builder.ToString ();
}

internal Task<string> ReadLineAsync (bool doAsync, CancellationToken cancellationToken)
Expand Down
Loading

0 comments on commit 9e8e83a

Please sign in to comment.