Skip to content

Commit

Permalink
#225 add Utf8ByteArrayOutput when a byte array containing the output …
Browse files Browse the repository at this point in the history
…is needed
  • Loading branch information
casid committed May 20, 2023
1 parent 53c87bd commit e397525
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 1 deletion.
76 changes: 76 additions & 0 deletions jte-runtime/src/main/java/gg/jte/output/Utf8ByteArrayOutput.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package gg.jte.output;

import gg.jte.TemplateOutput;

import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
* UTF-8 template output, that buffers the entire output into a byte array.
* You may want to use this class over {@link Utf8ByteOutput}, if all you need is a byte array containing the output.
* In this case it will be faster than storing chunks of byte arrays, just to convert them to a byte array in the end.
*
* CAUTION: You must enable {@link gg.jte.TemplateEngine#setBinaryStaticContent(boolean)}, otherwise this class won't provide any benefits over {@link StringOutput}!
*/
public final class Utf8ByteArrayOutput extends Writer implements TemplateOutput {

private byte[] buffer;
private int position;

public Utf8ByteArrayOutput() {
this(8 * 1024);
}

public Utf8ByteArrayOutput(int initialCapacity) {
buffer = new byte[initialCapacity];
}

@Override
public Writer getWriter() {
return this;
}

@Override
public void writeContent(String value) {
writeBinaryContent(value.getBytes(StandardCharsets.UTF_8));
}

@Override
public void writeBinaryContent(byte[] value) {
ensureCapacity(position + value.length);
System.arraycopy(value, 0, buffer, position, value.length);
position += value.length;
}

@Override
public void write(String value) {
writeBinaryContent(value.getBytes(StandardCharsets.UTF_8));
}

@Override
public void write(char[] cbuf, int off, int len) throws IOException {
writeContent(new String(cbuf, off, len));
}

@Override
public void flush() {
// nothing to do
}

@Override
public void close() {
// nothing to do
}

public byte[] toByteArray() {
return Arrays.copyOf(buffer, position);
}

private void ensureCapacity(int minCapacity) {
if (buffer.length < minCapacity) {
buffer = Arrays.copyOf(buffer, Math.max(minCapacity, 2 * buffer.length));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

/**
* Heavily optimized UTF-8 template output, designed to be CPU and memory friendly.
* You may want to use this class, if you write to a low-level binary output stream and you need the exact content-size of the output.
* You may want to use this class, if you write to a low-level binary output stream, and you need the exact content-size of the output.
*
* CAUTION: You must enable {@link gg.jte.TemplateEngine#setBinaryStaticContent(boolean)}, otherwise this class won't provide any benefits over {@link StringOutput}!
*/
Expand Down
7 changes: 7 additions & 0 deletions jte/src/test/java/gg/jte/benchmark/BenchmarkTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

import static org.assertj.core.api.Assertions.assertThat;

Expand All @@ -28,4 +29,10 @@ void binaryOutput() throws IOException {
utf8ByteOutput.writeTo(os);
assertThat(os.toString("UTF-8")).contains("This page has 42 visits already.");
}

@Test
void binaryArrayOutput() throws UnsupportedEncodingException {
byte[] bytes = new Benchmark_BinaryArray().render(new WelcomePage(42));
assertThat(new String(bytes, "UTF-8")).contains("This page has 42 visits already.");
}
}
59 changes: 59 additions & 0 deletions jte/src/test/java/gg/jte/benchmark/Benchmark_BinaryArray.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package gg.jte.benchmark;

import gg.jte.ContentType;
import gg.jte.TemplateEngine;
import gg.jte.output.Utf8ByteArrayOutput;
import gg.jte.resolve.DirectoryCodeResolver;
import gg.jte.runtime.Constants;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;

class Benchmark_BinaryArray {

private final TemplateEngine templateEngine;

public static void main(String[] args) {
new Benchmark_BinaryArray().run();
}

Benchmark_BinaryArray() {
Path classDirectory = Paths.get("jte-classes");

TemplateEngine compiler = TemplateEngine.create(new DirectoryCodeResolver(Benchmark.getTemplateDirectory()), classDirectory, ContentType.Html, null, Constants.PACKAGE_NAME_PRECOMPILED);
compiler.setTrimControlStructures(true);
compiler.setBinaryStaticContent(true);
compiler.precompileAll();

templateEngine = TemplateEngine.createPrecompiled(classDirectory, ContentType.Html);
}

public void run() {
System.out.println("Rendering welcome page for the first time, this will cause the template to compile.");
renderWelcomePage(1);

System.out.println("Rendering welcome page a million times.");
renderWelcomePage(1_000_000);
}

private void renderWelcomePage(int amount) {
long start = System.nanoTime();

for (int i = 0; i < amount; ++i) {
Page page = new WelcomePage(i);
render(page);
}

long end = System.nanoTime();

System.out.println(amount + " pages rendered in " + TimeUnit.NANOSECONDS.toMillis(end - start) + "ms." + " (~ " + ((float)TimeUnit.NANOSECONDS.toMicros(end - start) / amount) + "µs per page)");
System.out.println();
}

byte[] render(Page page) {
Utf8ByteArrayOutput output = new Utf8ByteArrayOutput();
templateEngine.render(page.getTemplate(), page, output);
return output.toByteArray();
}
}
128 changes: 128 additions & 0 deletions jte/src/test/java/gg/jte/output/Utf8ByteArrayOutputTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package gg.jte.output;

import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import static org.assertj.core.api.Assertions.assertThat;

public class Utf8ByteArrayOutputTest extends AbstractTemplateOutputTest<Utf8ByteArrayOutput> {

@Override
Utf8ByteArrayOutput createTemplateOutput() {
return new Utf8ByteArrayOutput(16); // Small initial capacity size for tests;
}

@Test
void empty() {
thenOutputIs("");
}

@Test
void string() {
output.writeContent("Hello");
thenOutputIs("Hello");
}

@Test
void longString() {
output.writeContent("The quick brown fox jumps over the lazy dog");
thenOutputIs("The quick brown fox jumps over the lazy dog");
}

@Test
void longStringSpecialChars() {
output.writeContent("\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9");
thenOutputIs("\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9");
}

@Test
void outputs() {
output.writeContent("\uD83D\uDCA9");
output.writeContent(" says ");
output.writeUserContent(42);
output.writeContent("x ");
output.writeContent("\uD83D\uDCA9!!!");

thenOutputIs("\uD83D\uDCA9 says 42x \uD83D\uDCA9!!!");
}

@Test
void binary_string() {
output.writeBinaryContent("Hello".getBytes(StandardCharsets.UTF_8));
thenOutputIs("Hello");
}

@Test
void binary_longString() {
output.writeBinaryContent("The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8));
thenOutputIs("The quick brown fox jumps over the lazy dog");
}

@Test
void binary_longStringSpecialChars() {
output.writeBinaryContent("\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9".getBytes(StandardCharsets.UTF_8));
thenOutputIs("\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9\uD83D\uDCA9");
}

@Test
void mixed() {
output.writeContent("\uD83D\uDCA9");
output.writeBinaryContent(" says ".getBytes(StandardCharsets.UTF_8));
output.writeUserContent(42);
output.writeContent("x ");
output.writeBinaryContent("\uD83D\uDCA9!!!".getBytes(StandardCharsets.UTF_8));

thenOutputIs("\uD83D\uDCA9 says 42x \uD83D\uDCA9!!!");
}

@Test
void utf8_0x00() throws IOException {
output.write((char)0);
thenOutputIs("\u0000");
}

@Test
void utf8_greek() {
output.writeContent("Γαζέες καὶ μυρτιὲς δὲν θὰ βρῶ πιὰ στὸ χρυσαφὶ ξέφωτο");
thenOutputIs("Γαζέες καὶ μυρτιὲς δὲν θὰ βρῶ πιὰ στὸ χρυσαφὶ ξέφωτο");
}

@Test
void utf8_thai() {
String str = "๏ เป็นมนุษย์สุดประเสริฐเลิศคุณค่า กว่าบรรดาฝูงสัตว์เดรัจฉาน\n" +
" จงฝ่าฟันพัฒนาวิชาการ อย่าล้างผลาญฤๅเข่นฆ่าบีฑาใคร\n" +
" ไม่ถือโทษโกรธแช่งซัดฮึดฮัดด่า หัดอภัยเหมือนกีฬาอัชฌาสัย\n" +
" ปฏิบัติประพฤติกฎกำหนดใจ พูดจาให้จ๊ะๆ จ๋าๆ น่าฟังเอย ฯ";
output.writeContent(str);
thenOutputIs(str);
}

@Test
void utf8_japanese_hiragana() {
String str = "いろはにほへとちりぬるを\n" +
" わかよたれそつねならむ\n" +
" うゐのおくやまけふこえて\n" +
" あさきゆめみしゑひもせす";

output.writeContent(str);
thenOutputIs(str);
}

@Test
void utf8_japanese_katakana() {
String str = "イロハニホヘト チリヌルヲ ワカヨタレソ ツネナラム\n" +
" ウヰノオクヤマ ケフコエテ アサキユメミシ ヱヒモセスン";

output.writeContent(str);
thenOutputIs(str);
}

protected void thenOutputIs(String expected) {
byte[] bytes = output.toByteArray();

String actual = new String(bytes, StandardCharsets.UTF_8);
assertThat(actual).isEqualTo(expected);
}
}

0 comments on commit e397525

Please sign in to comment.