Skip to content

Commit 6fa5fb8

Browse files
committed
Reduce Base64::encode allocations, close AsyncHttpClient#1344
Motivation: `Base64::encode` allocates a StringBuilder on every call. Modifications: * Extract `StringBuilderPool` from `SringUtils` * Use such pool for `Base64::encode` Result: Less allocations
1 parent b3d20b1 commit 6fa5fb8

File tree

11 files changed

+109
-97
lines changed

11 files changed

+109
-97
lines changed

client/src/main/java/org/asynchttpclient/Realm.java

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
*/
1717
package org.asynchttpclient;
1818

19-
import static org.asynchttpclient.util.Assertions.*;
20-
2119
import static java.nio.charset.StandardCharsets.*;
20+
import static org.asynchttpclient.util.Assertions.assertNotNull;
2221
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;
22+
import static org.asynchttpclient.util.StringUtils.*;
2323

2424
import java.nio.charset.Charset;
2525
import java.security.MessageDigest;
@@ -28,6 +28,7 @@
2828

2929
import org.asynchttpclient.uri.Uri;
3030
import org.asynchttpclient.util.AuthenticatorUtils;
31+
import org.asynchttpclient.util.StringBuilderPool;
3132
import org.asynchttpclient.util.StringUtils;
3233

3334
/**
@@ -459,7 +460,7 @@ private void newResponse(MessageDigest md) {
459460
// BEWARE: compute first as it used the cached StringBuilder
460461
String digestUri = AuthenticatorUtils.computeRealmURI(uri, useAbsoluteURI, omitQuery);
461462

462-
StringBuilder sb = StringUtils.stringBuilder();
463+
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
463464

464465
// WARNING: DON'T MOVE, BUFFER IS RECYCLED!!!!
465466
byte[] secretDigest = secretDigest(sb, md);
@@ -473,30 +474,6 @@ private void newResponse(MessageDigest md) {
473474
response = toHexString(responseDigest);
474475
}
475476

476-
private static String toHexString(byte[] data) {
477-
StringBuilder buffer = StringUtils.stringBuilder();
478-
for (byte aData : data) {
479-
buffer.append(Integer.toHexString((aData & 0xf0) >>> 4));
480-
buffer.append(Integer.toHexString(aData & 0x0f));
481-
}
482-
return buffer.toString();
483-
}
484-
485-
private static void appendBase16(StringBuilder buf, byte[] bytes) {
486-
int base = 16;
487-
for (byte b : bytes) {
488-
int bi = 0xff & b;
489-
int c = '0' + (bi / base) % base;
490-
if (c > '9')
491-
c = 'a' + (c - '0' - 10);
492-
buf.append((char) c);
493-
c = '0' + bi % base;
494-
if (c > '9')
495-
c = 'a' + (c - '0' - 10);
496-
buf.append((char) c);
497-
}
498-
}
499-
500477
/**
501478
* Build a {@link Realm}
502479
*

client/src/main/java/org/asynchttpclient/cookie/CookieEncoder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import java.util.Collection;
1717
import java.util.Comparator;
1818

19-
import org.asynchttpclient.util.StringUtils;
19+
import org.asynchttpclient.util.StringBuilderPool;
2020

2121
public final class CookieEncoder {
2222

@@ -49,7 +49,7 @@ private CookieEncoder() {
4949
}
5050

5151
public static String encode(Collection<Cookie> cookies) {
52-
StringBuilder sb = StringUtils.stringBuilder();
52+
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
5353

5454
if (cookies.isEmpty()) {
5555
return "";

client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.asynchttpclient.RequestBuilderBase;
3232
import org.asynchttpclient.SignatureCalculator;
3333
import org.asynchttpclient.util.Base64;
34+
import org.asynchttpclient.util.StringBuilderPool;
3435
import org.asynchttpclient.util.StringUtils;
3536
import org.asynchttpclient.util.Utf8UrlEncoder;
3637

@@ -103,7 +104,7 @@ StringBuilder signatureBaseString(ConsumerKey consumerAuth, RequestToken userAut
103104
String baseUrl = request.getUri().toBaseUrl();
104105
String encodedParams = encodedParams(consumerAuth, userAuth, oauthTimestamp, nonce, request.getFormParams(), request.getQueryParams());
105106

106-
StringBuilder sb = StringUtils.stringBuilder();
107+
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
107108
sb.append(request.getMethod()); // POST / GET etc (nothing to URL encode)
108109
sb.append('&');
109110
Utf8UrlEncoder.encodeAndAppendPercentEncoded(sb, baseUrl);
@@ -154,7 +155,7 @@ private String percentEncodeAlreadyFormUrlEncoded(String s) {
154155
}
155156

156157
private byte[] digest(ConsumerKey consumerAuth, RequestToken userAuth, ByteBuffer message) throws InvalidKeyException {
157-
StringBuilder sb = StringUtils.stringBuilder();
158+
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
158159
Utf8UrlEncoder.encodeAndAppendQueryElement(sb, consumerAuth.getSecret());
159160
sb.append('&');
160161
if (userAuth != null && userAuth.getSecret() != null) {
@@ -170,7 +171,7 @@ private byte[] digest(ConsumerKey consumerAuth, RequestToken userAuth, ByteBuffe
170171
}
171172

172173
String constructAuthHeader(ConsumerKey consumerAuth, RequestToken userAuth, String signature, String nonce, long oauthTimestamp) {
173-
StringBuilder sb = StringUtils.stringBuilder();
174+
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
174175
sb.append("OAuth ");
175176
sb.append(KEY_OAUTH_CONSUMER_KEY).append("=\"").append(consumerAuth.getKey()).append("\", ");
176177
if (userAuth.getKey() != null) {

client/src/main/java/org/asynchttpclient/oauth/Parameters.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import java.util.Collections;
1818
import java.util.List;
1919

20-
import org.asynchttpclient.util.StringUtils;
20+
import org.asynchttpclient.util.StringBuilderPool;
2121

2222
class Parameters {
2323

@@ -37,7 +37,7 @@ String sortAndConcat() {
3737
Collections.sort(parameters);
3838

3939
// and build parameter section using pre-encoded pieces:
40-
StringBuilder encodedParams = StringUtils.stringBuilder();
40+
StringBuilder encodedParams = StringBuilderPool.DEFAULT.stringBuilder();
4141
for (int i = 0; i < parameters.size(); i++) {
4242
Parameter param = parameters.get(i);
4343
encodedParams.append(param.key).append('=').append(param.value).append('&');

client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import org.asynchttpclient.request.body.multipart.part.MessageEndMultipartPart;
2929
import org.asynchttpclient.request.body.multipart.part.MultipartPart;
3030
import org.asynchttpclient.request.body.multipart.part.StringMultipartPart;
31-
import org.asynchttpclient.util.StringUtils;
31+
import org.asynchttpclient.util.StringBuilderPool;
3232

3333
public class MultipartUtils {
3434

@@ -105,7 +105,7 @@ private static byte[] generateBoundary() {
105105
}
106106

107107
private static String computeContentType(CharSequence base, byte[] boundary) {
108-
StringBuilder buffer = StringUtils.stringBuilder().append(base);
108+
StringBuilder buffer = StringBuilderPool.DEFAULT.stringBuilder().append(base);
109109
if (base.length() != 0 && base.charAt(base.length() - 1) != ';')
110110
buffer.append(';');
111111
return buffer.append(" boundary=").append(new String(boundary, US_ASCII)).toString();

client/src/main/java/org/asynchttpclient/uri/Uri.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
*/
1313
package org.asynchttpclient.uri;
1414

15-
import static org.asynchttpclient.util.Assertions.*;
15+
import static org.asynchttpclient.util.Assertions.assertNotNull;
1616
import static org.asynchttpclient.util.MiscUtils.isNonEmpty;
1717

1818
import java.net.URI;
1919
import java.net.URISyntaxException;
2020

2121
import org.asynchttpclient.util.MiscUtils;
22-
import org.asynchttpclient.util.StringUtils;
22+
import org.asynchttpclient.util.StringBuilderPool;
2323

2424
public class Uri {
2525

@@ -117,7 +117,7 @@ public int getSchemeDefaultPort() {
117117

118118
public String toUrl() {
119119
if (url == null) {
120-
StringBuilder sb = StringUtils.stringBuilder();
120+
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
121121
sb.append(scheme).append("://");
122122
if (userInfo != null)
123123
sb.append(userInfo).append('@');
@@ -138,7 +138,7 @@ public String toUrl() {
138138
* @return [scheme]://[hostname](:[port]). Port is ommitted if it matches the scheme's default one.
139139
*/
140140
public String toBaseUrl() {
141-
StringBuilder sb = StringUtils.stringBuilder();
141+
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
142142
sb.append(scheme).append("://").append(host);
143143
if (port != -1 && port != getSchemeDefaultPort()) {
144144
sb.append(':').append(port);
@@ -150,7 +150,7 @@ public String toBaseUrl() {
150150
}
151151

152152
public String toRelativeUrl() {
153-
StringBuilder sb = StringUtils.stringBuilder();
153+
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
154154
if (MiscUtils.isNonEmpty(path))
155155
sb.append(path);
156156
else

client/src/main/java/org/asynchttpclient/util/Base64.java

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,59 +13,55 @@
1313
package org.asynchttpclient.util;
1414

1515
/**
16-
* Implements the "base64" binary encoding scheme as defined by
17-
* <a href="http://tools.ietf.org/html/rfc2045">RFC 2045</a>.
18-
* <br>
16+
* Implements the "base64" binary encoding scheme as defined by <a href="http://tools.ietf.org/html/rfc2045">RFC 2045</a>. <br>
1917
* Portions of code here are taken from Apache Pivot
2018
*/
2119
public final class Base64 {
22-
private static final char[] lookup = new char[64];
23-
private static final byte[] reverseLookup = new byte[256];
20+
private static final StringBuilderPool SB_POOL = new StringBuilderPool();
21+
private static final char[] LOOKUP = new char[64];
22+
private static final byte[] REVERSE_LOOKUP = new byte[256];
2423

2524
static {
2625
// Populate the lookup array
2726

2827
for (int i = 0; i < 26; i++) {
29-
lookup[i] = (char) ('A' + i);
28+
LOOKUP[i] = (char) ('A' + i);
3029
}
3130

3231
for (int i = 26, j = 0; i < 52; i++, j++) {
33-
lookup[i] = (char) ('a' + j);
32+
LOOKUP[i] = (char) ('a' + j);
3433
}
3534

3635
for (int i = 52, j = 0; i < 62; i++, j++) {
37-
lookup[i] = (char) ('0' + j);
36+
LOOKUP[i] = (char) ('0' + j);
3837
}
3938

40-
lookup[62] = '+';
41-
lookup[63] = '/';
39+
LOOKUP[62] = '+';
40+
LOOKUP[63] = '/';
4241

4342
// Populate the reverse lookup array
4443

4544
for (int i = 0; i < 256; i++) {
46-
reverseLookup[i] = -1;
45+
REVERSE_LOOKUP[i] = -1;
4746
}
4847

4948
for (int i = 'Z'; i >= 'A'; i--) {
50-
reverseLookup[i] = (byte) (i - 'A');
49+
REVERSE_LOOKUP[i] = (byte) (i - 'A');
5150
}
5251

5352
for (int i = 'z'; i >= 'a'; i--) {
54-
reverseLookup[i] = (byte) (i - 'a' + 26);
53+
REVERSE_LOOKUP[i] = (byte) (i - 'a' + 26);
5554
}
5655

5756
for (int i = '9'; i >= '0'; i--) {
58-
reverseLookup[i] = (byte) (i - '0' + 52);
57+
REVERSE_LOOKUP[i] = (byte) (i - '0' + 52);
5958
}
6059

61-
reverseLookup['+'] = 62;
62-
reverseLookup['/'] = 63;
63-
reverseLookup['='] = 0;
60+
REVERSE_LOOKUP['+'] = 62;
61+
REVERSE_LOOKUP['/'] = 63;
62+
REVERSE_LOOKUP['='] = 0;
6463
}
6564

66-
/**
67-
* This class is not instantiable.
68-
*/
6965
private Base64() {
7066
}
7167

@@ -76,30 +72,29 @@ private Base64() {
7672
* @return the encoded data
7773
*/
7874
public static String encode(byte[] bytes) {
79-
// always sequence of 4 characters for each 3 bytes; padded with '='s as necessary:
80-
StringBuilder buf = new StringBuilder(((bytes.length + 2) / 3) * 4);
75+
StringBuilder buf = SB_POOL.stringBuilder();
8176

8277
// first, handle complete chunks (fast loop)
8378
int i = 0;
8479
for (int end = bytes.length - 2; i < end;) {
8580
int chunk = ((bytes[i++] & 0xFF) << 16) | ((bytes[i++] & 0xFF) << 8) | (bytes[i++] & 0xFF);
86-
buf.append(lookup[chunk >> 18]);
87-
buf.append(lookup[(chunk >> 12) & 0x3F]);
88-
buf.append(lookup[(chunk >> 6) & 0x3F]);
89-
buf.append(lookup[chunk & 0x3F]);
81+
buf.append(LOOKUP[chunk >> 18]);
82+
buf.append(LOOKUP[(chunk >> 12) & 0x3F]);
83+
buf.append(LOOKUP[(chunk >> 6) & 0x3F]);
84+
buf.append(LOOKUP[chunk & 0x3F]);
9085
}
9186

9287
// then leftovers, if any
9388
int len = bytes.length;
9489
if (i < len) { // 1 or 2 extra bytes?
9590
int chunk = ((bytes[i++] & 0xFF) << 16);
96-
buf.append(lookup[chunk >> 18]);
91+
buf.append(LOOKUP[chunk >> 18]);
9792
if (i < len) { // 2 bytes
9893
chunk |= ((bytes[i] & 0xFF) << 8);
99-
buf.append(lookup[(chunk >> 12) & 0x3F]);
100-
buf.append(lookup[(chunk >> 6) & 0x3F]);
94+
buf.append(LOOKUP[(chunk >> 12) & 0x3F]);
95+
buf.append(LOOKUP[(chunk >> 6) & 0x3F]);
10196
} else { // 1 byte
102-
buf.append(lookup[(chunk >> 12) & 0x3F]);
97+
buf.append(LOOKUP[(chunk >> 12) & 0x3F]);
10398
buf.append('=');
10499
}
105100
buf.append('=');
@@ -124,10 +119,10 @@ public static byte[] decode(String encoded) {
124119
byte[] bytes = new byte[length];
125120

126121
for (int i = 0, index = 0, n = encoded.length(); i < n; i += 4) {
127-
int word = reverseLookup[encoded.charAt(i)] << 18;
128-
word += reverseLookup[encoded.charAt(i + 1)] << 12;
129-
word += reverseLookup[encoded.charAt(i + 2)] << 6;
130-
word += reverseLookup[encoded.charAt(i + 3)];
122+
int word = REVERSE_LOOKUP[encoded.charAt(i)] << 18;
123+
word += REVERSE_LOOKUP[encoded.charAt(i + 1)] << 12;
124+
word += REVERSE_LOOKUP[encoded.charAt(i + 2)] << 6;
125+
word += REVERSE_LOOKUP[encoded.charAt(i + 3)];
131126

132127
for (int j = 0; j < 3 && index + j < length; j++) {
133128
bytes[index + j] = (byte) (word >> (8 * (2 - j)));

client/src/main/java/org/asynchttpclient/util/HttpUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public static boolean followRedirect(AsyncHttpClientConfig config, Request reque
8383
}
8484

8585
private static StringBuilder urlEncodeFormParams0(List<Param> params) {
86-
StringBuilder sb = StringUtils.stringBuilder();
86+
StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder();
8787
for (Param param : params) {
8888
encodeAndAppendFormParam(sb, param.getName(), param.getValue());
8989
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2017 AsyncHttpClient Project. All rights reserved.
3+
*
4+
* This program is licensed to you under the Apache License Version 2.0,
5+
* and you may not use this file except in compliance with the Apache License Version 2.0.
6+
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7+
*
8+
* Unless required by applicable law or agreed to in writing,
9+
* software distributed under the Apache License Version 2.0 is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12+
*/
13+
package org.asynchttpclient.util;
14+
15+
public class StringBuilderPool {
16+
17+
public static final StringBuilderPool DEFAULT = new StringBuilderPool();
18+
19+
private final ThreadLocal<StringBuilder> pool = ThreadLocal.withInitial(() -> new StringBuilder(512));
20+
21+
/**
22+
* BEWARE: MUSN'T APPEND TO ITSELF!
23+
*
24+
* @return a pooled StringBuilder
25+
*/
26+
public StringBuilder stringBuilder() {
27+
StringBuilder sb = pool.get();
28+
sb.setLength(0);
29+
return sb;
30+
}
31+
}

0 commit comments

Comments
 (0)