Skip to content

Commit af0db31

Browse files
authored
Merge pull request #2101 from jooby-project/rocker
Rocker: move from string builder to byte array
2 parents 15ec9a6 + 5d743df commit af0db31

11 files changed

Lines changed: 378 additions & 97 deletions

File tree

docs/asciidoc/modules/rocker.adoc

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,53 @@ import io.jooby.rocker.RockerModule
9494

9595
<1> Install Rocker
9696
<2> Returns a rocker view
97+
98+
=== Options
99+
100+
Rocker uses a byte buffer to render a view. Default byte buffer size is `4k`. To change the buffer size:
101+
102+
.Java
103+
[source, java, role="primary"]
104+
----
105+
import io.jooby.rocker.RockerModule;
106+
107+
{
108+
install(new RockerModule().bufferSize(1024));
109+
}
110+
----
111+
112+
.Kotlin
113+
[source, kt, role="secondary"]
114+
----
115+
import io.jooby.rocker.RockerModule
116+
117+
{
118+
install(RockerModule().bufferSize(1024)
119+
}
120+
----
121+
122+
You can reuse/recycle the buffer using a thread-local approach by setting `reuseBuffer(true)`:
123+
124+
.Java
125+
[source, java, role="primary"]
126+
----
127+
import io.jooby.rocker.RockerModule;
128+
129+
{
130+
install(new RockerModule().reuseBuffer(true));
131+
}
132+
----
133+
134+
.Kotlin
135+
[source, kt, role="secondary"]
136+
----
137+
import io.jooby.rocker.RockerModule
138+
139+
{
140+
install(RockerModule().reuseBuffer(true)
141+
}
142+
----
143+
144+
CAUTION: Use with caution due it creates a buffer per thread memory consumption might be high.
145+
This technique works when the number of available threads is low enough
146+
(like the number of available processors).

modules/jooby-bom/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<archetype-packaging.version>3.2.0</archetype-packaging.version>
2222
<asm.version>9.0</asm.version>
2323
<auto-service.version>1.0-rc7</auto-service.version>
24-
<aws-java-sdk.version>1.11.885</aws-java-sdk.version>
24+
<aws-java-sdk.version>1.11.893</aws-java-sdk.version>
2525
<boringssl.version>2.0.27.Final</boringssl.version>
2626
<bucket4j-core.version>4.10.0</bucket4j-core.version>
2727
<caffeine.version>2.8.5</caffeine.version>
@@ -56,7 +56,7 @@
5656
<jdbi.version>3.17.0</jdbi.version>
5757
<jetty.version>9.4.34.v20201102</jetty.version>
5858
<jfiglet.version>0.0.8</jfiglet.version>
59-
<jmespath-java.version>1.11.896</jmespath-java.version>
59+
<jmespath-java.version>1.11.893</jmespath-java.version>
6060
<jooby-maven-plugin.version>2.9.3-SNAPSHOT</jooby-maven-plugin.version>
6161
<jooby.version>2.9.3-SNAPSHOT</jooby.version>
6262
<json.version>20200518</json.version>

modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ public NettyContext(ChannelHandlerContext ctx, HttpRequest req, Router router, S
146146
this.method = req.method().name().toUpperCase();
147147
}
148148

149+
boolean isHttpGet() {
150+
return this.method.length() == 3 && this.method.charAt(0) == 'G' && this.method.charAt(1) == 'E'
151+
&& this.method.charAt(2) == 'T';
152+
}
153+
149154
@Nonnull @Override public Router getRouter() {
150155
return router;
151156
}

modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
*/
66
package io.jooby.internal.netty;
77

8+
import java.nio.charset.StandardCharsets;
9+
import java.time.ZoneOffset;
10+
import java.time.ZonedDateTime;
11+
import java.time.format.DateTimeFormatter;
12+
import java.util.concurrent.ScheduledExecutorService;
13+
import java.util.concurrent.TimeUnit;
14+
import java.util.concurrent.atomic.AtomicReference;
15+
16+
import org.slf4j.Logger;
17+
818
import io.jooby.MediaType;
919
import io.jooby.Router;
1020
import io.jooby.Server;
@@ -28,15 +38,6 @@
2838
import io.netty.handler.timeout.IdleStateEvent;
2939
import io.netty.util.AsciiString;
3040
import io.netty.util.ReferenceCounted;
31-
import org.slf4j.Logger;
32-
33-
import java.nio.charset.StandardCharsets;
34-
import java.time.ZoneOffset;
35-
import java.time.ZonedDateTime;
36-
import java.time.format.DateTimeFormatter;
37-
import java.util.concurrent.ScheduledExecutorService;
38-
import java.util.concurrent.TimeUnit;
39-
import java.util.concurrent.atomic.AtomicReference;
4041

4142
public class NettyHandler extends ChannelInboundHandlerAdapter {
4243
private static final AtomicReference<String> cachedDateString = new AtomicReference<>();
@@ -83,11 +84,17 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) {
8384
}
8485
context.setHeaders.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
8586

86-
contentLength = contentLength(req);
87-
if (contentLength > 0 || HttpUtil.isTransferEncodingChunked(req)) {
88-
decoder = newDecoder(req, factory);
89-
} else {
87+
if (context.isHttpGet()) {
9088
router.match(context).execute(context);
89+
} else {
90+
// possibly body:
91+
contentLength = contentLength(req);
92+
if (contentLength > 0 || HttpUtil.isTransferEncodingChunked(req)) {
93+
decoder = newDecoder(req, factory);
94+
} else {
95+
// no body, move on
96+
router.match(context).execute(context);
97+
}
9198
}
9299
} else if (decoder != null && msg instanceof HttpContent) {
93100
HttpContent chunk = (HttpContent) msg;
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/**
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.rocker;
7+
8+
import java.nio.ByteBuffer;
9+
import java.nio.charset.Charset;
10+
import java.nio.charset.StandardCharsets;
11+
import java.util.Arrays;
12+
13+
import com.fizzed.rocker.ContentType;
14+
import com.fizzed.rocker.RockerOutput;
15+
import com.fizzed.rocker.RockerOutputFactory;
16+
17+
/**
18+
* Rocker output that uses a byte array to render the output.
19+
*
20+
* @author edgar
21+
*/
22+
public class ByteBufferOutput implements RockerOutput<ByteBufferOutput> {
23+
24+
/** Default buffer size: <code>4k</code>. */
25+
public static final int BUFFER_SIZE = 4096;
26+
27+
/**
28+
* The maximum size of array to allocate.
29+
* Some VMs reserve some header words in an array.
30+
* Attempts to allocate larger arrays may result in
31+
* OutOfMemoryError: Requested array size exceeds VM limit
32+
*/
33+
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
34+
35+
private final ContentType contentType;
36+
37+
/**
38+
* The buffer where data is stored.
39+
*/
40+
protected byte[] buf;
41+
42+
/**
43+
* The number of valid bytes in the buffer.
44+
*/
45+
protected int count;
46+
47+
ByteBufferOutput(ContentType contentType, int bufferSize) {
48+
this.buf = new byte[bufferSize];
49+
this.contentType = contentType;
50+
}
51+
52+
void reset() {
53+
count = 0;
54+
}
55+
56+
@Override public ContentType getContentType() {
57+
return contentType;
58+
}
59+
60+
@Override public Charset getCharset() {
61+
return StandardCharsets.UTF_8;
62+
}
63+
64+
@Override public ByteBufferOutput w(String string) {
65+
return w(string.getBytes(StandardCharsets.UTF_8));
66+
}
67+
68+
@Override public ByteBufferOutput w(byte[] bytes) {
69+
int len = bytes.length;
70+
ensureCapacity(count + len);
71+
System.arraycopy(bytes, 0, buf, count, len);
72+
count += len;
73+
return this;
74+
}
75+
76+
@Override public int getByteLength() {
77+
return count;
78+
}
79+
80+
/**
81+
* Get a view of the byte buffer.
82+
*
83+
* @return Byte buffer.
84+
*/
85+
public ByteBuffer toBuffer() {
86+
return ByteBuffer.wrap(buf, 0, count);
87+
}
88+
89+
/**
90+
* Copy internal byte array into a new array.
91+
*
92+
* @return Byte array.
93+
*/
94+
public byte[] toByteArray() {
95+
byte[] array = new byte[count];
96+
System.arraycopy(buf, 0, array, 0, count);
97+
return array;
98+
}
99+
100+
private void ensureCapacity(int minCapacity) {
101+
// overflow-conscious code
102+
if (minCapacity - buf.length > 0) {
103+
grow(minCapacity);
104+
}
105+
}
106+
107+
/**
108+
* Increases the capacity to ensure that it can hold at least the
109+
* number of elements specified by the minimum capacity argument.
110+
*
111+
* @param minCapacity the desired minimum capacity
112+
*/
113+
private void grow(int minCapacity) {
114+
// overflow-conscious code
115+
int oldCapacity = buf.length;
116+
int newCapacity = oldCapacity << 1;
117+
if (newCapacity - minCapacity < 0) {
118+
newCapacity = minCapacity;
119+
}
120+
if (newCapacity - MAX_ARRAY_SIZE > 0) {
121+
newCapacity = hugeCapacity(minCapacity);
122+
}
123+
buf = Arrays.copyOf(buf, newCapacity);
124+
}
125+
126+
private static int hugeCapacity(int minCapacity) {
127+
if (minCapacity < 0) {
128+
throw new OutOfMemoryError();
129+
}
130+
return (minCapacity > MAX_ARRAY_SIZE)
131+
? Integer.MAX_VALUE
132+
: MAX_ARRAY_SIZE;
133+
}
134+
135+
static RockerOutputFactory<ByteBufferOutput> factory(int bufferSize) {
136+
return (contentType, charsetName) -> new ByteBufferOutput(contentType, bufferSize);
137+
}
138+
139+
static RockerOutputFactory<ByteBufferOutput> reuse(
140+
RockerOutputFactory<ByteBufferOutput> factory) {
141+
return new RockerOutputFactory<ByteBufferOutput>() {
142+
private final ThreadLocal<ByteBufferOutput> thread = new ThreadLocal<>();
143+
144+
@Override public ByteBufferOutput create(ContentType contentType, String charsetName) {
145+
ByteBufferOutput output = thread.get();
146+
if (output == null) {
147+
output = factory.create(contentType, charsetName);
148+
thread.set(output);
149+
}
150+
output.reset();
151+
return output;
152+
}
153+
};
154+
}
155+
}

modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerHandler.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,29 @@
55
*/
66
package io.jooby.rocker;
77

8+
import javax.annotation.Nonnull;
9+
810
import com.fizzed.rocker.RockerModel;
9-
import com.fizzed.rocker.runtime.StringBuilderOutput;
11+
import com.fizzed.rocker.RockerOutputFactory;
1012
import io.jooby.Context;
1113
import io.jooby.MediaType;
1214
import io.jooby.Route;
1315

14-
import javax.annotation.Nonnull;
15-
1616
class RockerHandler implements Route.Handler {
1717
private final Route.Handler next;
1818

19-
RockerHandler(Route.Handler next) {
19+
private final RockerOutputFactory<ByteBufferOutput> factory;
20+
21+
RockerHandler(Route.Handler next, RockerOutputFactory<ByteBufferOutput> factory) {
2022
this.next = next;
23+
this.factory = factory;
2124
}
2225

23-
@Nonnull @Override public Object apply(@Nonnull Context ctx) throws Exception {
26+
@Nonnull @Override public Object apply(@Nonnull Context ctx) {
2427
try {
2528
RockerModel template = (RockerModel) next.apply(ctx);
2629
ctx.setResponseType(MediaType.html);
27-
ctx.send(template.render(StringBuilderOutput.FACTORY).toString());
28-
return ctx;
30+
return ctx.send(template.render(factory).toBuffer());
2931
} catch (Throwable x) {
3032
ctx.sendError(x);
3133
return x;

modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerMessageEncoder.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,25 @@
55
*/
66
package io.jooby.rocker;
77

8+
import javax.annotation.Nonnull;
9+
810
import com.fizzed.rocker.RockerModel;
9-
import com.fizzed.rocker.runtime.ArrayOfByteArraysOutput;
11+
import com.fizzed.rocker.RockerOutputFactory;
1012
import io.jooby.Context;
1113
import io.jooby.MediaType;
1214
import io.jooby.MessageEncoder;
1315

14-
import javax.annotation.Nonnull;
15-
1616
class RockerMessageEncoder implements MessageEncoder {
17-
@Override public byte[] encode(@Nonnull Context ctx, @Nonnull Object value) throws Exception {
17+
private final RockerOutputFactory<ByteBufferOutput> factory;
18+
19+
RockerMessageEncoder(RockerOutputFactory<ByteBufferOutput> factory) {
20+
this.factory = factory;
21+
}
22+
23+
@Override public byte[] encode(@Nonnull Context ctx, @Nonnull Object value) {
1824
if (value instanceof RockerModel) {
1925
RockerModel template = (RockerModel) value;
20-
ArrayOfByteArraysOutput output = template.render(ArrayOfByteArraysOutput.FACTORY);
26+
ByteBufferOutput output = template.render(factory);
2127
ctx.setResponseLength(output.getByteLength());
2228
ctx.setDefaultResponseType(MediaType.html);
2329
return output.toByteArray();

0 commit comments

Comments
 (0)