Skip to content

Commit 8709392

Browse files
committed
Object Allocation Improvements
This change updates some of the code to minimize the numbers of objects allocated during execution. It adds a pool of byte[]s to minimize the number that are allocated as well as letting reactor-netty convert some ByteBufs to Strings without additional allocations.
1 parent 809c33d commit 8709392

7 files changed

Lines changed: 137 additions & 20 deletions

File tree

.idea/dictionaries/bhale.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cloudfoundry-client-reactor/cloudfoundry-client-reactor.iml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
<orderEntry type="library" name="Maven: org.reactivestreams:reactive-streams:1.0.0" level="project" />
3737
<orderEntry type="library" name="Maven: io.projectreactor.ipc:reactor-netty:0.6.0.RELEASE" level="project" />
3838
<orderEntry type="library" name="Maven: io.projectreactor.ipc:reactor-ipc:0.6.0.RELEASE" level="project" />
39-
<orderEntry type="library" name="Maven: io.netty:netty-all:4.1.6.Final" level="project" />
39+
<orderEntry type="library" name="Maven: io.netty:netty-all:4.1.8.Final" level="project" />
4040
<orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
4141
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
4242
<orderEntry type="library" scope="TEST" name="Maven: org.assertj:assertj-core:3.6.1" level="project" />

cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/JsonCodec.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,18 @@
2929
import reactor.ipc.netty.http.client.HttpClientResponse;
3030

3131
import java.io.IOException;
32-
import java.nio.charset.Charset;
3332
import java.util.function.Function;
3433

3534
public final class JsonCodec {
3635

3736
public static <T> Function<Mono<HttpClientResponse>, Flux<T>> decode(ObjectMapper objectMapper, Class<T> responseType) {
3837
return inbound -> inbound
39-
.flatMap(response -> response.addHandler(new JsonObjectDecoder()).receive().aggregate())
40-
.map(byteBuf -> {
41-
String content = byteBuf.toString(Charset.defaultCharset());
42-
38+
.flatMap(response -> response.addHandler(new JsonObjectDecoder()).receive().aggregate().asString())
39+
.map(payload -> {
4340
try {
44-
return objectMapper.readValue(content, responseType);
41+
return objectMapper.readValue(payload, responseType);
4542
} catch (IOException e) {
46-
throw Exceptions.propagate(new JsonParsingException(e.getMessage(), e, content));
43+
throw Exceptions.propagate(new JsonParsingException(e.getMessage(), e, payload));
4744
}
4845
});
4946
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2013-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.cloudfoundry.util;
18+
19+
import java.time.Duration;
20+
import java.time.Instant;
21+
import java.util.ArrayList;
22+
import java.util.Optional;
23+
import java.util.Queue;
24+
import java.util.concurrent.ConcurrentLinkedQueue;
25+
import java.util.concurrent.Executors;
26+
import java.util.concurrent.ThreadFactory;
27+
import java.util.concurrent.TimeUnit;
28+
import java.util.concurrent.atomic.AtomicLong;
29+
import java.util.function.Consumer;
30+
31+
/**
32+
* Dynamically creates {@link byte} arrays and caches them, reusing them once they have been released.
33+
* <p>
34+
* The maximum number of byte arrays is unbounded
35+
* <p>
36+
* The default time-to-live for unused byte arrays is one minute
37+
*/
38+
public final class ByteArrayPool {
39+
40+
private static final AtomicLong EVICTOR_COUNTER = new AtomicLong();
41+
42+
private static final ThreadFactory EVICTOR_FACTORY = r -> {
43+
Thread t = new Thread(r, "byte-buffer-evictor-" + EVICTOR_COUNTER.incrementAndGet());
44+
t.setDaemon(true);
45+
return t;
46+
};
47+
48+
private static final int MIBIBYTE = 1_024 * 1_024;
49+
50+
private static ByteArrayPool INSTANCE = new ByteArrayPool(MIBIBYTE, Duration.ofMinutes(1));
51+
52+
private final Queue<ByteBufferExpiry> cache = new ConcurrentLinkedQueue<>();
53+
54+
private final int capacity;
55+
56+
private final Duration ttl;
57+
58+
private ByteArrayPool(int capacity, Duration ttl) {
59+
this.capacity = capacity;
60+
this.ttl = ttl;
61+
62+
Executors.newScheduledThreadPool(1, EVICTOR_FACTORY)
63+
.scheduleAtFixedRate(this::evict, ttl.toMillis(), ttl.toMillis(), TimeUnit.MILLISECONDS);
64+
}
65+
66+
/**
67+
* Executes a {@link Consumer} providing a pooled {@code byte} array
68+
*
69+
* @param consumer the {@link Consumer} of the {@link byte} arrray
70+
*/
71+
public static void withByteArray(Consumer<byte[]> consumer) {
72+
INSTANCE.doWithByteArray(consumer);
73+
}
74+
75+
private void doWithByteArray(Consumer<byte[]> consumer) {
76+
byte[] byteArray = Optional.ofNullable(this.cache.poll())
77+
.map(ByteBufferExpiry::getByteArray)
78+
.orElseGet(() -> new byte[this.capacity]);
79+
80+
try {
81+
consumer.accept(byteArray);
82+
} finally {
83+
this.cache.offer(new ByteBufferExpiry(byteArray, Instant.now().plus(this.ttl)));
84+
}
85+
}
86+
87+
private void evict() {
88+
Instant now = Instant.now();
89+
90+
new ArrayList<>(this.cache).stream()
91+
.filter(expiry -> expiry.getExpiration().isBefore(now))
92+
.forEach(this.cache::remove);
93+
}
94+
95+
private static class ByteBufferExpiry {
96+
97+
private final byte[] byteArray;
98+
99+
private final Instant expiration;
100+
101+
private ByteBufferExpiry(byte[] byteArray, Instant expiration) {
102+
this.byteArray = byteArray;
103+
this.expiration = expiration;
104+
}
105+
106+
private byte[] getByteArray() {
107+
return this.byteArray;
108+
}
109+
110+
private Instant getExpiration() {
111+
return this.expiration;
112+
}
113+
114+
}
115+
116+
117+
}

cloudfoundry-util/src/main/java/org/cloudfoundry/util/FileUtils.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,12 @@
2323
import reactor.core.scheduler.Schedulers;
2424

2525
import java.io.IOException;
26+
import java.io.InputStream;
2627
import java.math.BigInteger;
27-
import java.nio.ByteBuffer;
28-
import java.nio.channels.FileChannel;
2928
import java.nio.file.FileSystem;
3029
import java.nio.file.FileSystems;
3130
import java.nio.file.Files;
3231
import java.nio.file.Path;
33-
import java.nio.file.StandardOpenOption;
3432
import java.nio.file.attribute.PosixFileAttributes;
3533
import java.nio.file.attribute.PosixFilePermission;
3634
import java.security.MessageDigest;
@@ -124,15 +122,19 @@ public static String getRelativePathName(Path root, Path path) {
124122
* @return a {@link String} representation of the hash
125123
*/
126124
public static String hash(Path path) {
127-
try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ)) {
125+
try (InputStream in = Files.newInputStream(path)) {
128126
MessageDigest digest = MessageDigest.getInstance("sha1");
129127

130-
ByteBuffer buffer = ByteBuffer.allocate(MIBIBYTE);
131-
while (fileChannel.read(buffer) != -1) {
132-
buffer.flip();
133-
digest.update(buffer);
134-
buffer.clear();
135-
}
128+
ByteArrayPool.withByteArray(buffer -> {
129+
try {
130+
int length;
131+
while ((length = in.read(buffer)) != -1) {
132+
digest.update(buffer, 0, length);
133+
}
134+
} catch (IOException e) {
135+
throw Exceptions.propagate(e);
136+
}
137+
});
136138

137139
return String.format("%040x", new BigInteger(1, digest.digest()));
138140
} catch (IOException | NoSuchAlgorithmException e) {

integration-test/integration-test.iml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
<orderEntry type="library" scope="TEST" name="Maven: io.jsonwebtoken:jjwt:0.7.0" level="project" />
3232
<orderEntry type="library" scope="TEST" name="Maven: io.projectreactor.ipc:reactor-netty:0.6.0.RELEASE" level="project" />
3333
<orderEntry type="library" scope="TEST" name="Maven: io.projectreactor.ipc:reactor-ipc:0.6.0.RELEASE" level="project" />
34-
<orderEntry type="library" scope="TEST" name="Maven: io.netty:netty-all:4.1.6.Final" level="project" />
34+
<orderEntry type="library" scope="TEST" name="Maven: io.netty:netty-all:4.1.8.Final" level="project" />
3535
<orderEntry type="module" module-name="cloudfoundry-client" scope="TEST" />
3636
<orderEntry type="library" scope="TEST" name="Maven: com.squareup.wire:wire-runtime:2.2.0" level="project" />
3737
<orderEntry type="library" scope="TEST" name="Maven: com.squareup.okio:okio:1.8.0" level="project" />

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
<immutables.version>2.3.10</immutables.version>
5050
<java-semver.version>0.9.0</java-semver.version>
5151
<jjwt.version>0.7.0</jjwt.version>
52-
<netty.version>4.1.6.Final</netty.version>
52+
<netty.version>4.1.8.Final</netty.version>
5353
<okhttp3.version>3.5.0</okhttp3.version>
5454
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
5555
<reactor-addons.version>3.0.4.RELEASE</reactor-addons.version>

0 commit comments

Comments
 (0)