Skip to content

Commit ab8a693

Browse files
authored
Optimize output stream encoding. (#1423)
* Use a BufferedWriter which drastically speeds up json encoding, as the encoder makes a lot of one-character writes so not buffering them leads to significant uft-8 encoding overhead. * Encode directly into the connection as opposed to an intermediate byte array. * Switch to try-with-resources for better resource safety
1 parent 4491f0a commit ab8a693

File tree

3 files changed

+43
-60
lines changed

3 files changed

+43
-60
lines changed

transport/transport-backend-cct/src/main/java/com/google/android/datatransport/cct/CctTransportBackend.java

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,16 @@
4242
import com.google.android.datatransport.runtime.time.Clock;
4343
import com.google.firebase.encoders.DataEncoder;
4444
import com.google.firebase.encoders.EncodingException;
45-
import java.io.ByteArrayOutputStream;
45+
import java.io.BufferedReader;
46+
import java.io.BufferedWriter;
4647
import java.io.IOException;
4748
import java.io.InputStream;
4849
import java.io.InputStreamReader;
50+
import java.io.OutputStream;
4951
import java.io.OutputStreamWriter;
5052
import java.net.HttpURLConnection;
5153
import java.net.MalformedURLException;
5254
import java.net.URL;
53-
import java.nio.ByteBuffer;
54-
import java.nio.channels.Channels;
55-
import java.nio.channels.WritableByteChannel;
5655
import java.nio.charset.Charset;
5756
import java.util.ArrayList;
5857
import java.util.Calendar;
@@ -264,50 +263,46 @@ private HttpResponse doSend(HttpRequest request) throws IOException {
264263
connection.setRequestProperty(API_KEY_HEADER_KEY, request.apiKey);
265264
}
266265

267-
WritableByteChannel channel = Channels.newChannel(connection.getOutputStream());
268-
try {
269-
ByteArrayOutputStream output = new ByteArrayOutputStream();
270-
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(output);
266+
try (OutputStream conn = connection.getOutputStream();
267+
OutputStream outputStream = new GZIPOutputStream(conn)) {
268+
// note: it's very important to use a BufferedWriter for efficient use of resources as the
269+
// JsonWriter often writes one character at a time.
270+
dataEncoder.encode(
271+
request.requestBody, new BufferedWriter(new OutputStreamWriter(outputStream)));
272+
} catch (EncodingException | IOException e) {
273+
Logging.e(LOG_TAG, "Couldn't encode request, returning with 400", e);
274+
return new HttpResponse(400, null, 0);
275+
}
271276

272-
try {
273-
dataEncoder.encode(request.requestBody, new OutputStreamWriter(gzipOutputStream));
274-
} catch (EncodingException | IOException e) {
275-
Logging.e(LOG_TAG, "Couldn't encode request, returning with 400", e);
276-
return new HttpResponse(400, null, 0);
277-
} finally {
278-
gzipOutputStream.close();
279-
}
280-
channel.write(ByteBuffer.wrap(output.toByteArray()));
281-
int responseCode = connection.getResponseCode();
282-
Logging.i(LOG_TAG, "Status Code: " + responseCode);
283-
Logging.i(LOG_TAG, "Content-Type: " + connection.getHeaderField("Content-Type"));
284-
Logging.i(LOG_TAG, "Content-Encoding: " + connection.getHeaderField("Content-Encoding"));
285-
286-
if (responseCode == 302 || responseCode == 301 || responseCode == 307) {
287-
String redirect = connection.getHeaderField("Location");
288-
return new HttpResponse(responseCode, new URL(redirect), 0);
289-
}
290-
if (responseCode != 200) {
291-
return new HttpResponse(responseCode, null, 0);
292-
}
277+
int responseCode = connection.getResponseCode();
278+
Logging.i(LOG_TAG, "Status Code: " + responseCode);
279+
Logging.i(LOG_TAG, "Content-Type: " + connection.getHeaderField("Content-Type"));
280+
Logging.i(LOG_TAG, "Content-Encoding: " + connection.getHeaderField("Content-Encoding"));
293281

294-
InputStream inputStream;
295-
String contentEncoding = connection.getHeaderField(CONTENT_ENCODING_HEADER_KEY);
296-
if (contentEncoding != null && contentEncoding.equals(GZIP_CONTENT_ENCODING)) {
297-
inputStream = new GZIPInputStream(connection.getInputStream());
298-
} else {
299-
inputStream = connection.getInputStream();
300-
}
301-
try {
302-
long nextRequestMillis =
303-
LogResponse.fromJson(new InputStreamReader(inputStream)).getNextRequestWaitMillis();
304-
return new HttpResponse(responseCode, null, nextRequestMillis);
305-
} finally {
306-
inputStream.close();
307-
}
308-
} finally {
309-
channel.close();
282+
if (responseCode == 302 || responseCode == 301 || responseCode == 307) {
283+
String redirect = connection.getHeaderField("Location");
284+
return new HttpResponse(responseCode, new URL(redirect), 0);
285+
}
286+
if (responseCode != 200) {
287+
return new HttpResponse(responseCode, null, 0);
288+
}
289+
290+
try (InputStream connStream = connection.getInputStream();
291+
InputStream inputStream =
292+
maybeUnGzip(connStream, connection.getHeaderField(CONTENT_ENCODING_HEADER_KEY))) {
293+
long nextRequestMillis =
294+
LogResponse.fromJson(new BufferedReader(new InputStreamReader(inputStream)))
295+
.getNextRequestWaitMillis();
296+
return new HttpResponse(responseCode, null, nextRequestMillis);
297+
}
298+
}
299+
300+
private static InputStream maybeUnGzip(InputStream input, String contentEncoding)
301+
throws IOException {
302+
if (GZIP_CONTENT_ENCODING.equals(contentEncoding)) {
303+
return new GZIPInputStream(input);
310304
}
305+
return input;
311306
}
312307

313308
@Override

transport/transport-backend-cct/src/main/java/com/google/android/datatransport/cct/internal/LogResponse.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import android.util.JsonReader;
1818
import android.util.JsonToken;
1919
import androidx.annotation.NonNull;
20-
import androidx.annotation.Nullable;
2120
import com.google.auto.value.AutoValue;
2221
import java.io.IOException;
2322
import java.io.Reader;
@@ -33,7 +32,7 @@ static LogResponse create(long nextRequestWaitMillis) {
3332
return new AutoValue_LogResponse(nextRequestWaitMillis);
3433
}
3534

36-
@Nullable
35+
@NonNull
3736
public static LogResponse fromJson(@NonNull Reader reader) throws IOException {
3837
JsonReader jsonReader = new JsonReader(reader);
3938
try {

transport/transport-backend-cct/transport-backend-cct.gradle

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,10 @@ dependencies {
6666
testImplementation 'junit:junit:4.12'
6767
testImplementation "com.google.truth:truth:$googleTruthVersion"
6868
testImplementation 'com.google.truth.extensions:truth-proto-extension:1.0'
69-
testImplementation("com.github.tomakehurst:wiremock-jre8:2.21.0") {
70-
//Allows us to use the Android version of Apache httpclient instead
71-
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
72-
73-
//Resolves the Duplicate Class Exception
74-
//duplicate entry: org/objectweb/asm/AnnotationVisitor.class
75-
exclude group: 'asm', module: 'asm'
76-
77-
//Fixes Warning conflict with Android's version of org.json
78-
//org.json:json:20090211 is ignored for debugAndroidTest as it may be conflicting with the internal version provided by Android.
79-
exclude group: 'org.json', module: 'json'
80-
}
69+
testImplementation 'com.github.tomakehurst:wiremock:2.26.3'
8170
//Android compatible version of Apache httpclient.
8271
testImplementation 'org.apache.httpcomponents:httpclient-android:4.3.5.1'
83-
testImplementation 'org.robolectric:robolectric:4.2'
72+
testImplementation 'org.robolectric:robolectric:4.3.1'
8473
testImplementation 'junit:junit:4.13-beta-2'
8574

8675
androidTestImplementation 'androidx.test:runner:1.2.0'

0 commit comments

Comments
 (0)