Skip to content

Commit d2272e7

Browse files
committed
rocker: added back thread-local buffer cache
- fix #3809
1 parent 1bd4d1d commit d2272e7

File tree

5 files changed

+255
-69
lines changed

5 files changed

+255
-69
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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.internal;
7+
8+
import java.nio.charset.Charset;
9+
10+
import com.fizzed.rocker.ContentType;
11+
import com.fizzed.rocker.RockerOutputFactory;
12+
import edu.umd.cs.findbugs.annotations.NonNull;
13+
import io.jooby.output.BufferedOutput;
14+
import io.jooby.output.Output;
15+
import io.jooby.output.OutputFactory;
16+
import io.jooby.rocker.BufferedRockerOutput;
17+
18+
/**
19+
* Rocker output that uses a byte array to render the output.
20+
*
21+
* @author edgar
22+
*/
23+
public class BufferedRockerOutputImpl implements BufferedRockerOutput {
24+
25+
public static final int BUFFER_SIZE = 4096;
26+
27+
private final Charset charset;
28+
private final ContentType contentType;
29+
30+
/** The buffer where data is stored. */
31+
protected BufferedOutput output;
32+
33+
BufferedRockerOutputImpl(Charset charset, ContentType contentType, BufferedOutput output) {
34+
this.charset = charset;
35+
this.contentType = contentType;
36+
this.output = output;
37+
}
38+
39+
@Override
40+
public ContentType getContentType() {
41+
return contentType;
42+
}
43+
44+
@Override
45+
public Charset getCharset() {
46+
return charset;
47+
}
48+
49+
@Override
50+
public BufferedRockerOutputImpl w(String string) {
51+
output.write(string.getBytes(charset));
52+
return this;
53+
}
54+
55+
@Override
56+
public BufferedRockerOutputImpl w(byte[] bytes) {
57+
output.write(bytes);
58+
return this;
59+
}
60+
61+
@Override
62+
public int getByteLength() {
63+
return output.size();
64+
}
65+
66+
/**
67+
* Get a view of the byte buffer.
68+
*
69+
* @return Byte buffer.
70+
*/
71+
public @NonNull Output toOutput() {
72+
return output;
73+
}
74+
75+
public static RockerOutputFactory<BufferedRockerOutput> factory(
76+
Charset charset, OutputFactory factory) {
77+
return (contentType, charsetName) ->
78+
new BufferedRockerOutputImpl(charset, contentType, factory.allocate(BUFFER_SIZE));
79+
}
80+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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.internal;
7+
8+
import java.nio.charset.Charset;
9+
import java.util.Arrays;
10+
11+
import com.fizzed.rocker.ContentType;
12+
import com.fizzed.rocker.RockerOutputFactory;
13+
import edu.umd.cs.findbugs.annotations.NonNull;
14+
import io.jooby.output.Output;
15+
import io.jooby.output.OutputFactory;
16+
import io.jooby.rocker.BufferedRockerOutput;
17+
18+
/**
19+
* Rocker output that uses a byte array to render the output. It uses a thread-local cache.
20+
*
21+
* @author edgar
22+
*/
23+
public class HeapRockerOutput implements BufferedRockerOutput {
24+
private static final ThreadLocal<HeapRockerOutput> TL = new ThreadLocal<>();
25+
26+
private static final int BUFFER_SIZE = 4096;
27+
28+
/**
29+
* The maximum size of array to allocate. Some VMs reserve some header words in an array. Attempts
30+
* to allocate larger arrays may result in OutOfMemoryError: Requested array size exceeds VM limit
31+
*/
32+
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
33+
34+
private final OutputFactory factory;
35+
36+
private final Charset charset;
37+
38+
private final ContentType contentType;
39+
40+
/** The buffer where data is stored. */
41+
protected byte[] buf;
42+
43+
/** The number of valid bytes in the buffer. */
44+
protected int count;
45+
46+
HeapRockerOutput(
47+
OutputFactory factory, Charset charset, ContentType contentType, int bufferSize) {
48+
this.factory = factory;
49+
this.charset = charset;
50+
this.buf = new byte[bufferSize];
51+
this.contentType = contentType;
52+
}
53+
54+
HeapRockerOutput reset() {
55+
count = 0;
56+
return this;
57+
}
58+
59+
@Override
60+
public ContentType getContentType() {
61+
return contentType;
62+
}
63+
64+
@Override
65+
public Charset getCharset() {
66+
return charset;
67+
}
68+
69+
@Override
70+
public HeapRockerOutput w(String string) {
71+
return w(string.getBytes(charset));
72+
}
73+
74+
@Override
75+
public HeapRockerOutput w(byte[] bytes) {
76+
int len = bytes.length;
77+
ensureCapacity(count + len);
78+
System.arraycopy(bytes, 0, buf, count, len);
79+
count += len;
80+
return this;
81+
}
82+
83+
@Override
84+
public int getByteLength() {
85+
return count;
86+
}
87+
88+
/**
89+
* Get a view of the byte buffer.
90+
*
91+
* @return Byte buffer.
92+
*/
93+
public @NonNull Output toOutput() {
94+
return factory.wrap(buf, 0, count);
95+
}
96+
97+
private void ensureCapacity(int minCapacity) {
98+
// overflow-conscious code
99+
if (minCapacity - buf.length > 0) {
100+
grow(minCapacity);
101+
}
102+
}
103+
104+
/**
105+
* Increases the capacity to ensure that it can hold at least the number of elements specified by
106+
* the minimum capacity argument.
107+
*
108+
* @param minCapacity the desired minimum capacity
109+
*/
110+
private void grow(int minCapacity) {
111+
// overflow-conscious code
112+
int oldCapacity = buf.length;
113+
int newCapacity = oldCapacity << 1;
114+
if (newCapacity - minCapacity < 0) {
115+
newCapacity = minCapacity;
116+
}
117+
if (newCapacity - MAX_ARRAY_SIZE > 0) {
118+
newCapacity = hugeCapacity(minCapacity);
119+
}
120+
buf = Arrays.copyOf(buf, newCapacity);
121+
}
122+
123+
private static int hugeCapacity(int minCapacity) {
124+
if (minCapacity < 0) {
125+
throw new OutOfMemoryError();
126+
}
127+
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
128+
}
129+
130+
public static RockerOutputFactory<BufferedRockerOutput> factory(
131+
Charset charset, OutputFactory factory) {
132+
return (contentType, charsetName) -> {
133+
var output = TL.get();
134+
if (output == null) {
135+
output = new HeapRockerOutput(factory, charset, contentType, BUFFER_SIZE);
136+
TL.set(output);
137+
}
138+
output.reset();
139+
return output;
140+
};
141+
}
142+
}

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

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

8-
import java.nio.charset.Charset;
9-
10-
import com.fizzed.rocker.ContentType;
118
import com.fizzed.rocker.RockerOutput;
12-
import com.fizzed.rocker.RockerOutputFactory;
13-
import io.jooby.output.BufferedOutput;
149
import io.jooby.output.Output;
15-
import io.jooby.output.OutputFactory;
1610

1711
/**
1812
* Rocker output that uses a byte array to render the output.
1913
*
2014
* @author edgar
2115
*/
22-
public class BufferedRockerOutput implements RockerOutput<BufferedRockerOutput> {
23-
24-
/** Default buffer size: <code>4k</code>. */
25-
public static final int BUFFER_SIZE = 4096;
26-
27-
private final Charset charset;
28-
private final ContentType contentType;
29-
30-
/** The buffer where data is stored. */
31-
protected BufferedOutput output;
32-
33-
BufferedRockerOutput(Charset charset, ContentType contentType, BufferedOutput output) {
34-
this.charset = charset;
35-
this.contentType = contentType;
36-
this.output = output;
37-
}
38-
39-
@Override
40-
public ContentType getContentType() {
41-
return contentType;
42-
}
43-
44-
@Override
45-
public Charset getCharset() {
46-
return charset;
47-
}
48-
49-
@Override
50-
public BufferedRockerOutput w(String string) {
51-
output.write(string, getCharset());
52-
return this;
53-
}
54-
55-
@Override
56-
public BufferedRockerOutput w(byte[] bytes) {
57-
output.write(bytes);
58-
return this;
59-
}
60-
61-
@Override
62-
public int getByteLength() {
63-
return output.size();
64-
}
65-
16+
public interface BufferedRockerOutput extends RockerOutput<BufferedRockerOutput> {
6617
/**
67-
* Get a view of the byte buffer.
18+
* Rocker output as jooby output.
6819
*
69-
* @return Byte buffer.
20+
* @return Rocker output as jooby output.
7021
*/
71-
public Output asOutput() {
72-
return output;
73-
}
74-
75-
static RockerOutputFactory<BufferedRockerOutput> factory(Charset charset, OutputFactory factory) {
76-
return (contentType, charsetName) ->
77-
new BufferedRockerOutput(charset, contentType, factory.allocate());
78-
}
22+
Output toOutput();
7923
}

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,16 @@
1313
import io.jooby.MessageEncoder;
1414
import io.jooby.output.Output;
1515

16-
class RockerMessageEncoder implements MessageEncoder {
17-
private final RockerOutputFactory<BufferedRockerOutput> factory;
18-
19-
RockerMessageEncoder(RockerOutputFactory<BufferedRockerOutput> factory) {
20-
this.factory = factory;
21-
}
16+
record RockerMessageEncoder(RockerOutputFactory<BufferedRockerOutput> factory)
17+
implements MessageEncoder {
2218

2319
@Override
2420
public Output encode(@NonNull Context ctx, @NonNull Object value) {
2521
if (value instanceof RockerModel template) {
2622
var output = template.render(factory);
2723
ctx.setResponseLength(output.getByteLength());
2824
ctx.setDefaultResponseType(MediaType.html);
29-
return output.asOutput();
25+
return output.toOutput();
3026
}
3127
return null;
3228
}

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
import com.fizzed.rocker.RockerOutputFactory;
1212
import com.fizzed.rocker.runtime.RockerRuntime;
1313
import edu.umd.cs.findbugs.annotations.NonNull;
14+
import io.jooby.ExecutionMode;
1415
import io.jooby.Extension;
1516
import io.jooby.Jooby;
1617
import io.jooby.ServiceRegistry;
18+
import io.jooby.internal.BufferedRockerOutputImpl;
19+
import io.jooby.internal.HeapRockerOutput;
1720

1821
/**
1922
* Rocker module. It requires some build configuration setup which are documented in the web site.
@@ -25,6 +28,7 @@
2528
public class RockerModule implements Extension {
2629
private Boolean reloading;
2730
private final Charset charset;
31+
private Boolean reuseBuffer;
2832

2933
public RockerModule(@NonNull Charset charset) {
3034
this.charset = charset;
@@ -40,11 +44,24 @@ public RockerModule() {
4044
* @param reloading True for turning on.
4145
* @return This module.
4246
*/
43-
public @NonNull RockerModule reloading(boolean reloading) {
47+
public RockerModule reloading(boolean reloading) {
4448
this.reloading = reloading;
4549
return this;
4650
}
4751

52+
/**
53+
* Allow simple reuse of raw byte buffers. It is usually used through <code>ThreadLocal</code>
54+
* variable pointing to instance of {@link io.jooby.output.BufferedOutput}.
55+
*
56+
* @param reuseBuffer True for reuse the buffer. Enabled by default when running in {@link
57+
* ExecutionMode#EVENT_LOOP}.
58+
* @return This module.
59+
*/
60+
public RockerModule reuseBuffer(boolean reuseBuffer) {
61+
this.reuseBuffer = reuseBuffer;
62+
return this;
63+
}
64+
4865
@Override
4966
public void install(@NonNull Jooby application) {
5067
var env = application.getEnvironment();
@@ -53,7 +70,14 @@ public void install(@NonNull Jooby application) {
5370
this.reloading == null
5471
? (env.isActive("dev") && runtime.isReloadingPossible())
5572
: this.reloading;
56-
var factory = BufferedRockerOutput.factory(charset, application.getOutputFactory());
73+
var reuseBuffer =
74+
this.reuseBuffer == null
75+
? application.getExecutionMode() == ExecutionMode.EVENT_LOOP
76+
: this.reuseBuffer;
77+
RockerOutputFactory<BufferedRockerOutput> factory =
78+
reuseBuffer
79+
? HeapRockerOutput.factory(charset, application.getOutputFactory())
80+
: BufferedRockerOutputImpl.factory(charset, application.getOutputFactory());
5781
runtime.setReloading(reloading);
5882
// renderer
5983
application.encoder(new RockerMessageEncoder(factory));

0 commit comments

Comments
 (0)