Skip to content

Commit 1761a34

Browse files
committed
netty: improve performance by caching the content-length header on bytes wrapped responses
1 parent 76d391f commit 1761a34

File tree

13 files changed

+222
-41
lines changed

13 files changed

+222
-41
lines changed

modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.typesafe.config.Config;
3232
import edu.umd.cs.findbugs.annotations.NonNull;
3333
import io.jooby.*;
34+
import io.jooby.buffer.BufferedOutputFactory;
3435
import io.jooby.internal.jetty.JettyHandler;
3536
import io.jooby.internal.jetty.JettyHttpExpectAndContinueHandler;
3637
import io.jooby.internal.jetty.PrefixHandler;
@@ -45,6 +46,7 @@
4546
public class JettyServer extends io.jooby.Server.Base {
4647

4748
private static final int THREADS = 200;
49+
private BufferedOutputFactory outputFactory;
4850

4951
private Server server;
5052

@@ -69,6 +71,14 @@ public JettyServer(@NonNull ServerOptions options) {
6971

7072
public JettyServer() {}
7173

74+
@NonNull @Override
75+
public BufferedOutputFactory getOutputFactory() {
76+
if (outputFactory == null) {
77+
this.outputFactory = BufferedOutputFactory.create(getOptions().getBuffer());
78+
}
79+
return outputFactory;
80+
}
81+
7282
@NonNull @Override
7383
public JettyServer setOptions(@NonNull ServerOptions options) {
7484
super.setOptions(options.setWorkerThreads(options.getWorkerThreads(THREADS)));

modules/jooby-kotlin/src/test/kotlin/io/jooby/kt/Idioms.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import java.time.Duration
1313
import kotlinx.coroutines.delay
1414

1515
/**
16-
* Kotlin DLS in action, this class does nothing but we need it to make sure Kotlin version compiles
17-
* sucessfully.
16+
* Kotlin DLS in action, this class does nothing, but we need it to make sure Kotlin version
17+
* compiles successfully.
1818
*/
1919
class Idioms :
2020
Kooby({

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -505,8 +505,8 @@ static void encoderHeader(CharSequence name, CharSequence value, ByteBuf buf) {
505505
}
506506

507507
private static void writeAscii(ByteBuf buf, int offset, CharSequence value) {
508-
if (value instanceof AsciiString) {
509-
ByteBufUtil.copy((AsciiString) value, 0, buf, offset, value.length());
508+
if (value instanceof AsciiString ascii) {
509+
buf.setBytes(offset, ascii.array(), ascii.arrayOffset(), value.length());
510510
} else {
511511
buf.setCharSequence(offset, value, CharsetUtil.US_ASCII);
512512
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.netty;
7+
8+
import java.nio.CharBuffer;
9+
import java.nio.charset.Charset;
10+
11+
import edu.umd.cs.findbugs.annotations.NonNull;
12+
import io.jooby.Context;
13+
import io.jooby.buffer.BufferedOutput;
14+
import io.netty.buffer.ByteBuf;
15+
import io.netty.buffer.Unpooled;
16+
import io.netty.util.AsciiString;
17+
18+
public class NettyByteArrayWrappedOutput implements NettyByteBufOutput {
19+
20+
private final byte[] buffer;
21+
private final int offset;
22+
private final int length;
23+
private final AsciiString contentLength;
24+
25+
protected NettyByteArrayWrappedOutput(byte[] buffer, int offset, int length) {
26+
this.buffer = buffer;
27+
this.offset = offset;
28+
this.length = length;
29+
this.contentLength = AsciiString.of(Integer.toString(length - offset));
30+
}
31+
32+
@NonNull public ByteBuf byteBuf() {
33+
return Unpooled.wrappedBuffer(buffer, offset, length);
34+
}
35+
36+
@Override
37+
@NonNull public BufferedOutput write(byte b) {
38+
throw new UnsupportedOperationException();
39+
}
40+
41+
@Override
42+
@NonNull public BufferedOutput write(byte[] source) {
43+
throw new UnsupportedOperationException();
44+
}
45+
46+
@Override
47+
@NonNull public BufferedOutput write(byte[] source, int offset, int length) {
48+
throw new UnsupportedOperationException();
49+
}
50+
51+
@Override
52+
@NonNull public BufferedOutput write(@NonNull String source, @NonNull Charset charset) {
53+
throw new UnsupportedOperationException();
54+
}
55+
56+
@Override
57+
public void send(Context ctx) {
58+
if (ctx instanceof NettyContext netty) {
59+
netty.send(Unpooled.wrappedBuffer(buffer, offset, length), contentLength);
60+
} else {
61+
ctx.send(asByteBuffer());
62+
}
63+
}
64+
65+
@Override
66+
public BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) {
67+
throw new UnsupportedOperationException();
68+
}
69+
70+
@Override
71+
@NonNull public BufferedOutput clear() {
72+
return this;
73+
}
74+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ default int size() {
4141
@Override
4242
default void send(Context ctx) {
4343
if (ctx instanceof NettyContext netty) {
44-
netty.send(byteBuf());
44+
var buf = byteBuf();
45+
netty.send(buf, Integer.toString(buf.readableBytes()));
4546
} else {
4647
ctx.send(asByteBuffer());
4748
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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.netty;
7+
8+
import java.nio.ByteBuffer;
9+
import java.nio.CharBuffer;
10+
import java.nio.charset.Charset;
11+
12+
import edu.umd.cs.findbugs.annotations.NonNull;
13+
import io.jooby.Context;
14+
import io.jooby.buffer.BufferedOutput;
15+
import io.netty.buffer.ByteBuf;
16+
import io.netty.buffer.Unpooled;
17+
import io.netty.util.AsciiString;
18+
19+
public class NettyByteBufferWrappedOutput implements NettyByteBufOutput {
20+
21+
private final ByteBuffer buffer;
22+
private final AsciiString contentLength;
23+
24+
protected NettyByteBufferWrappedOutput(ByteBuffer buffer) {
25+
this.buffer = buffer;
26+
this.contentLength = AsciiString.of(Integer.toString(buffer.remaining()));
27+
}
28+
29+
@NonNull public ByteBuf byteBuf() {
30+
return Unpooled.wrappedBuffer(buffer);
31+
}
32+
33+
@Override
34+
@NonNull public BufferedOutput write(byte b) {
35+
throw new UnsupportedOperationException();
36+
}
37+
38+
@Override
39+
@NonNull public BufferedOutput write(byte[] source) {
40+
throw new UnsupportedOperationException();
41+
}
42+
43+
@Override
44+
@NonNull public BufferedOutput write(byte[] source, int offset, int length) {
45+
throw new UnsupportedOperationException();
46+
}
47+
48+
@Override
49+
@NonNull public BufferedOutput write(@NonNull String source, @NonNull Charset charset) {
50+
throw new UnsupportedOperationException();
51+
}
52+
53+
@Override
54+
public void send(Context ctx) {
55+
if (ctx instanceof NettyContext netty) {
56+
netty.send(Unpooled.wrappedBuffer(buffer), contentLength);
57+
} else {
58+
ctx.send(asByteBuffer());
59+
}
60+
}
61+
62+
@Override
63+
public BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) {
64+
throw new UnsupportedOperationException();
65+
}
66+
67+
@Override
68+
@NonNull public BufferedOutput clear() {
69+
return this;
70+
}
71+
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -580,10 +580,14 @@ public final Context send(ByteBuffer data) {
580580
return this;
581581
}
582582

583-
public Context send(@NonNull ByteBuf data) {
583+
private Context send(@NonNull ByteBuf data) {
584+
return send(data, Integer.toString(data.readableBytes()));
585+
}
586+
587+
Context send(@NonNull ByteBuf data, CharSequence contentLength) {
584588
try {
585589
responseStarted = true;
586-
setHeaders.set(CONTENT_LENGTH, Integer.toString(data.readableBytes()));
590+
setHeaders.set(CONTENT_LENGTH, contentLength);
587591
var response = new DefaultFullHttpResponse(HTTP_1_1, status, data, setHeaders, NO_TRAILING);
588592
if (ctx.channel().eventLoop().inEventLoop()) {
589593
needsFlush = true;

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,4 @@ protected void encodeHeaders(HttpHeaders headers, ByteBuf buf) {
1818
super.encodeHeaders(headers, buf);
1919
}
2020
}
21-
22-
@Override
23-
public boolean acceptOutboundMessage(Object msg) throws Exception {
24-
return super.acceptOutboundMessage(msg);
25-
}
2621
}

modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.jooby.ServerOptions;
2323
import io.jooby.SneakyThrows;
2424
import io.jooby.SslOptions;
25+
import io.jooby.buffer.BufferedOutputFactory;
2526
import io.jooby.internal.netty.*;
2627
import io.netty.bootstrap.ServerBootstrap;
2728
import io.netty.buffer.ByteBufAllocator;
@@ -53,6 +54,7 @@ public class NettyServer extends Server.Base {
5354
private ExecutorService worker;
5455

5556
private List<Jooby> applications;
57+
private NettyBufferedOutputFactory outputFactory;
5658

5759
/**
5860
* Creates a server.
@@ -80,6 +82,15 @@ public NettyServer(@NonNull ServerOptions options) {
8082

8183
public NettyServer() {}
8284

85+
@NonNull @Override
86+
public BufferedOutputFactory getOutputFactory() {
87+
if (outputFactory == null) {
88+
outputFactory =
89+
new NettyBufferedOutputFactory(ByteBufAllocator.DEFAULT, getOptions().getBuffer());
90+
}
91+
return outputFactory;
92+
}
93+
8394
@Override
8495
protected ServerOptions defaultOptions() {
8596
return new ServerOptions().setServer(getName());
@@ -101,9 +112,7 @@ public Server start(@NonNull Jooby... application) {
101112
worker = newFixedThreadPool(options.getWorkerThreads(), new DefaultThreadFactory("worker"));
102113
}
103114
// Make sure context use same buffer factory
104-
var outputFactory = new NettyOutputFactory(ByteBufAllocator.DEFAULT, options.getBuffer());
105115
for (var app : applications) {
106-
app.setOutputFactory(outputFactory);
107116
app.getServices().put(ServerOptions.class, options);
108117
app.getServices().put(Server.class, this);
109118
}
@@ -124,6 +133,7 @@ public Server start(@NonNull Jooby... application) {
124133
this.dateLoop = Executors.newSingleThreadScheduledExecutor();
125134
var dateService = new NettyDateService(dateLoop);
126135

136+
var outputFactory = (NettyBufferedOutputFactory) getOutputFactory();
127137
var allocator = outputFactory.getAllocator();
128138
var http2 = options.isHttp2() == Boolean.TRUE;
129139
/* Bootstrap: */

modules/jooby-test/src/main/java/io/jooby/test/JoobyExtension.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,16 @@ public void beforeAll(ExtensionContext context) throws Exception {
6969
}
7070

7171
private Jooby startApp(ExtensionContext context, JoobyTest metadata) throws Exception {
72+
var server = Server.loadServer();
73+
var serverOptions = server.getOptions();
74+
serverOptions.setPort(port(metadata.port(), DEFAULT_PORT));
75+
server.setOptions(serverOptions);
7276
Jooby app;
7377
String factoryMethod = metadata.factoryMethod();
7478
if (factoryMethod.isEmpty()) {
7579
var defaultEnv = System.getProperty("application.env");
7680
System.setProperty("application.env", metadata.environment());
77-
app = Jooby.createApp(metadata.executionMode(), reflectionProvider(metadata.value()));
81+
app = Jooby.createApp(server, metadata.executionMode(), reflectionProvider(metadata.value()));
7882
if (defaultEnv != null) {
7983
System.setProperty("application.env", defaultEnv);
8084
} else {
@@ -83,14 +87,6 @@ private Jooby startApp(ExtensionContext context, JoobyTest metadata) throws Exce
8387
} else {
8488
app = fromFactoryMethod(context, metadata, factoryMethod);
8589
}
86-
var server = Server.loadServer();
87-
var serverOptions = server.getOptions();
88-
// ServerOptions serverOptions = app.getServerOptions();
89-
// if (serverOptions == null) {
90-
// serverOptions = server.getOptions();
91-
// }
92-
serverOptions.setPort(port(metadata.port(), DEFAULT_PORT));
93-
server.setOptions(serverOptions);
9490
server.start(app);
9591
ExtensionContext.Store store = getStore(context);
9692
store.put("server", server);

0 commit comments

Comments
 (0)