Completed benchmark for UTIL vs HTTP multipart parser. Fixed some tests in MultiPartCaptureTest.

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2018-03-29 16:29:48 +11:00
parent c733512891
commit 8a8324cb18
12 changed files with 449 additions and 15 deletions

View File

@ -33,6 +33,18 @@
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
@ -69,7 +81,49 @@
<onlyAnalyze>org.eclipse.jetty.http.*</onlyAnalyze>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>${jmhjar.name}</finalName>
<shadeTestJar>true</shadeTestJar>
<artifactSet>
<includes>
<include>org.openjdk.jmh:jmh-core</include>
</includes>
</artifactSet>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>org.openjdk.jmh:jmh-core</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -610,8 +610,6 @@ public class MultiPartFormInputStream
}
//TODO implement proper toString
//TODO more debugging
class Handler implements MultiPartParser.Handler
{
@ -768,6 +766,12 @@ public class MultiPartFormInputStream
contentType = null;
headers = new MultiMap<>();
}
@Override
public String toString() {
return("contentDisposition: "+contentDisposition+" contentType:"+contentType);
}
}
public void setDeleteOnExit(boolean deleteOnExit)

View File

@ -110,14 +110,14 @@ public class MultiPartCaptureTest
// ret.add(new String[]{"browser-capture-sjis-form-safari"}); // contains html encoded character and unspecified charset defaults to utf-8
// form submitted as shift-jis (with HTML5 specific hidden _charset_ field)
// ret.add(new String[]{"browser-capture-sjis-charset-form-android-chrome"}); // contains html encoded character
// ret.add(new String[]{"browser-capture-sjis-charset-form-android-firefox"}); // contains html encoded character
// ret.add(new String[]{"browser-capture-sjis-charset-form-chrome"}); // contains html encoded character
ret.add(new String[]{"browser-capture-sjis-charset-form-android-chrome"}); // contains html encoded character
ret.add(new String[]{"browser-capture-sjis-charset-form-android-firefox"}); // contains html encoded character
ret.add(new String[]{"browser-capture-sjis-charset-form-chrome"}); // contains html encoded character
ret.add(new String[]{"browser-capture-sjis-charset-form-edge"});
// ret.add(new String[]{"browser-capture-sjis-charset-form-firefox"}); // contains html encoded character
// ret.add(new String[]{"browser-capture-sjis-charset-form-ios-safari"}); // contains html encoded character
ret.add(new String[]{"browser-capture-sjis-charset-form-firefox"}); // contains html encoded character
ret.add(new String[]{"browser-capture-sjis-charset-form-ios-safari"}); // contains html encoded character
ret.add(new String[]{"browser-capture-sjis-charset-form-msie"});
// ret.add(new String[]{"browser-capture-sjis-charset-form-safari"}); // contains html encoded character
ret.add(new String[]{"browser-capture-sjis-charset-form-safari"}); // contains html encoded character
// form submitted with simple file upload
ret.add(new String[]{"browser-capture-form-fileupload-android-chrome"});

View File

@ -1062,8 +1062,41 @@ public class MultiPartFormInputStreamTest
}
@Test
public void testGeneratedForm()
throws Exception
{
String contentType = "multipart/form-data, boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW";
String body = "Content-Type: multipart/form-data; boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
"\r\n" +
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"part1\"\r\n" +
"\n" +
"wNfミxVam﾿t\r\n" +
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\n" +
"Content-Disposition: form-data; name=\"part2\"\r\n" +
"\r\n" +
"&ᄈᄎ￙ᅱᅢO\r\n" +
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW--";
MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50);
MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(body.getBytes()),
contentType,
config,
_tmpDir);
mpis.setDeleteOnExit(true);
Collection<Part> parts = mpis.getParts();
assertThat(parts, notNullValue());
assertThat(parts.size(), is(2));
Part part1 = mpis.getPart("part1");
assertThat(part1, notNullValue());
Part part2 = mpis.getPart("part2");
assertThat(part2, notNullValue());
}
private String createMultipartRequestString(String filename)
{

View File

@ -620,6 +620,52 @@ public class MultiPartParserTest
}
@Test
public void testGeneratedForm()
{
TestHandler handler = new TestHandler()
{
@Override
public boolean messageComplete()
{
return true;
}
@Override
public boolean content(ByteBuffer buffer, boolean last)
{
super.content(buffer,last);
return false;
}
@Override
public boolean headerComplete()
{
return false;
}
};
MultiPartParser parser = new MultiPartParser(handler,"WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW");
ByteBuffer data = BufferUtil.toBuffer(""
+ "Content-Type: multipart/form-data; boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
"\r\n" +
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"part1\"\r\n" +
"\n" +
"wNfミxVam﾿t\r\n" +
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\n" +
"Content-Disposition: form-data; name=\"part2\"\r\n" +
"\r\n" +
"&ᄈᄎ￙ᅱᅢO\r\n" +
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW--");
parser.parse(data,true);
assertThat(parser.getState(), is(State.END));
assertThat(handler.fields.size(), is(2));
}
static class TestHandler implements MultiPartParser.Handler
{
List<String> fields = new ArrayList<>();

View File

@ -0,0 +1,297 @@
//
// ========================================================================
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.http.jmh;
import java.io.File;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.servlet.MultipartConfigElement;
import javax.servlet.http.Part;
import org.eclipse.jetty.http.MultiPartFormInputStream;
import org.eclipse.jetty.http.MultiPartCaptureTest.MultipartExpectations;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.TestingDir;
import org.eclipse.jetty.util.BufferUtil;
import org.junit.Rule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.profile.CompilerProfiler;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
@State(Scope.Benchmark)
public class MultiPartBenchmark
{
public static final int MAX_FILE_SIZE = Integer.MAX_VALUE;
public static final int MAX_REQUEST_SIZE = Integer.MAX_VALUE;
public static final int FILE_SIZE_THRESHOLD = 50;
public int count = 0;
static String _contentType;
static File _file;
static int _numSections;
static int _numBytesPerSection;
public static List<String> data = new ArrayList<>();
static
{
// Capture of raw request body contents from various browsers
// simple form - 2 fields
data.add("browser-capture-form1-android-chrome");
data.add("browser-capture-form1-android-firefox");
data.add("browser-capture-form1-chrome");
data.add("browser-capture-form1-edge");
data.add("browser-capture-form1-firefox");
data.add("browser-capture-form1-ios-safari");
data.add("browser-capture-form1-msie");
data.add("browser-capture-form1-osx-safari");
// form submitted as shift-jis
data.add("browser-capture-sjis-form-edge");
data.add("browser-capture-sjis-form-msie");
// form submitted as shift-jis (with HTML5 specific hidden _charset_ field)
data.add("browser-capture-sjis-charset-form-edge");
data.add("browser-capture-sjis-charset-form-msie");
// form submitted with simple file upload
data.add("browser-capture-form-fileupload-android-chrome");
data.add("browser-capture-form-fileupload-android-firefox");
data.add("browser-capture-form-fileupload-chrome");
data.add("browser-capture-form-fileupload-edge");
data.add("browser-capture-form-fileupload-firefox");
data.add("browser-capture-form-fileupload-ios-safari");
data.add("browser-capture-form-fileupload-msie");
data.add("browser-capture-form-fileupload-safari");
// form submitted with 2 files (1 binary, 1 text) and 2 text fields
data.add("browser-capture-form-fileupload-alt-chrome");
data.add("browser-capture-form-fileupload-alt-edge");
data.add("browser-capture-form-fileupload-alt-firefox");
data.add("browser-capture-form-fileupload-alt-msie");
data.add("browser-capture-form-fileupload-alt-safari");
}
@Param({"UTIL","HTTP"})
public static String parserType;
@Setup(Level.Trial)
public static void setupTrial() throws Exception
{
_file = File.createTempFile("test01",null);
_file.deleteOnExit();
_numSections = 1;
_numBytesPerSection = 1024*1024*10;
_contentType = "multipart/form-data, boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW";
String initialBoundary = "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n";
String boundary = "\r\n--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n";
String closingBoundary = "\r\n--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW--\r\n";
String headerStart = "Content-Disposition: form-data; name=\"";
for(int i=0; i<_numSections; i++) {
//boundary and headers
if(i==0)
Files.write(_file.toPath(), initialBoundary.getBytes(), StandardOpenOption.APPEND);
else
Files.write(_file.toPath(), boundary.getBytes(), StandardOpenOption.APPEND);
Files.write(_file.toPath(), headerStart.getBytes(), StandardOpenOption.APPEND);
Files.write(_file.toPath(), new String("part"+(i+1)).getBytes(), StandardOpenOption.APPEND);
Files.write(_file.toPath(), new String("\"\r\n\r\n").getBytes(), StandardOpenOption.APPEND);
//append random data
byte[] data = new byte[_numBytesPerSection];
new Random().nextBytes(data);
Files.write(_file.toPath(), data, StandardOpenOption.APPEND);
}
//closing boundary
Files.write(_file.toPath(), closingBoundary.getBytes(), StandardOpenOption.APPEND);
/*
// print out file to verify that it contains valid contents (just for testing)
InputStream in = Files.newInputStream(_file.toPath());
System.out.println();
while(in.available()>0) {
byte b[] = new byte[100];
int read = in.read(b,0,100);
for(int i=0; i<read; i++)
System.out.print((char)b[i]);
}
System.out.println();
//exit
throw new RuntimeException("Stop Here");
*/
}
@Benchmark
@BenchmarkMode({Mode.AverageTime})
@SuppressWarnings("deprecation")
public long testLargeGenerated() throws Exception
{
Path multipartRawFile = _file.toPath();
Path outputDir = new File("/tmp").toPath();
MultipartConfigElement config = newMultipartConfigElement(outputDir);
try (InputStream in = Files.newInputStream(multipartRawFile))
{
switch(parserType)
{
case "HTTP":
{
MultiPartFormInputStream parser = new MultiPartFormInputStream(in, _contentType, config, outputDir.toFile());
if(parser.getParts().size() != _numSections)
throw new IllegalStateException("Incorrect Parsing");
for(Part p : parser.getParts()) {
count += p.getSize();
}
}
break;
case "UTIL":
{
org.eclipse.jetty.util.MultiPartInputStreamParser parser = new org.eclipse.jetty.util.MultiPartInputStreamParser(in, _contentType,config,outputDir.toFile());
// TODO this is using the http version of part (which should be the same anyway)
if(parser.getParts().size() != _numSections)
throw new IllegalStateException("Incorrect Parsing");
for(Part p : parser.getParts()) {
count += p.getSize();
}
}
break;
default:
throw new IllegalStateException("Unknown parserType Parameter");
}
}
return count;
}
@TearDown(Level.Trial)
public static void stopTrial() throws Exception
{
_file = null;
}
private MultipartConfigElement newMultipartConfigElement(Path path)
{
return new MultipartConfigElement(path.toString(), MAX_FILE_SIZE, MAX_REQUEST_SIZE, FILE_SIZE_THRESHOLD);
}
@Benchmark
@BenchmarkMode({Mode.AverageTime})
@SuppressWarnings("deprecation")
public long testParser() throws Exception
{
for(String multiPart : data)
{
Path multipartRawFile = MavenTestingUtils.getTestResourcePathFile("multipart/" + multiPart + ".raw");
Path expectationPath = MavenTestingUtils.getTestResourcePathFile("multipart/" + multiPart + ".expected.txt");
Path outputDir = new File("/tmp").toPath();
MultipartExpectations multipartExpectations = new MultipartExpectations(expectationPath);
MultipartConfigElement config = newMultipartConfigElement(outputDir);
try (InputStream in = Files.newInputStream(multipartRawFile))
{
switch(parserType)
{
case "HTTP":
{
MultiPartFormInputStream parser = new MultiPartFormInputStream(in, multipartExpectations.contentType, config, outputDir.toFile());
for(Part p : parser.getParts()) {
count += p.getSize();
}
}
break;
case "UTIL":
{
org.eclipse.jetty.util.MultiPartInputStreamParser parser = new org.eclipse.jetty.util.MultiPartInputStreamParser(in,multipartExpectations.contentType,config,outputDir.toFile());
// TODO this is using the http version of part (which should be the same anyway)
for(Part p : parser.getParts()) {
count += p.getSize();
}
}
break;
default:
throw new IllegalStateException("Unknown parserType Parameter");
}
}
}
return count;
}
public static void main(String[] args) throws RunnerException
{
Options opt = new OptionsBuilder()
.include(MultiPartBenchmark.class.getSimpleName())
.warmupIterations(20)
.measurementIterations(10)
.forks(1)
.threads(1)
// .syncIterations(true) // Don't start all threads at same time
// .warmupTime(new TimeValue(10000,TimeUnit.MILLISECONDS))
// .measurementTime(new TimeValue(10000,TimeUnit.MILLISECONDS))
// .addProfiler(CompilerProfiler.class)
// .addProfiler(LinuxPerfProfiler.class)
// .addProfiler(LinuxPerfNormProfiler.class)
// .addProfiler(LinuxPerfAsmProfiler.class)
// .resultFormat(ResultFormatType.CSV)
.build();
new Runner(opt).run();
}
}

View File

@ -14,4 +14,4 @@ Request-Header|User-Agent|Mozilla/5.0 (Linux; Android 8.1.0; Pixel 2 XL Build/OP
Parts-Count|3
Part-ContainsContents|_charset_|Shift_JIS
Part-ContainsContents|japanese|健治
Part-ContainsContents|hello|ャユ
Part-ContainsContents|hello|ャユ&#25094;

View File

@ -11,4 +11,4 @@ Request-Header|User-Agent|Mozilla/5.0 (Android 8.1.0; Mobile; rv:59.0) Gecko/59.
Parts-Count|3
Part-ContainsContents|_charset_|Shift_JIS
Part-ContainsContents|japanese|健治
Part-ContainsContents|hello|ャユ
Part-ContainsContents|hello|ャユ&#25094;

View File

@ -15,4 +15,4 @@ Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/
Parts-Count|3
Part-ContainsContents|_charset_|Shift_JIS
Part-ContainsContents|japanese|健治
Part-ContainsContents|hello|ャユ
Part-ContainsContents|hello|ャユ&#25094;

View File

@ -11,4 +11,4 @@ Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gec
Parts-Count|3
Part-ContainsContents|_charset_|Shift_JIS
Part-ContainsContents|japanese|健治
Part-ContainsContents|hello|ャユ
Part-ContainsContents|hello|ャユ&#25094;

View File

@ -12,4 +12,4 @@ Request-Header|User-Agent|Mozilla/5.0 (iPad; CPU OS 11_2_6 like Mac OS X) AppleW
Parts-Count|3
Part-ContainsContents|_charset_|Shift_JIS
Part-ContainsContents|japanese|健治
Part-ContainsContents|hello|ャユ
Part-ContainsContents|hello|ャユ&#25094;

View File

@ -12,4 +12,4 @@ Request-Header|User-Agent|Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleW
Parts-Count|3
Part-ContainsContents|_charset_|Shift_JIS
Part-ContainsContents|japanese|健治
Part-ContainsContents|hello|ャユ
Part-ContainsContents|hello|ャユ&#25094;