/*
* Copyright 2012 NGDATA nv
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lilyproject.rest.perftest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.codehaus.jackson.node.JsonNodeFactory;
import org.codehaus.jackson.node.ObjectNode;
import org.lilyproject.cli.OptionUtil;
import org.lilyproject.testclientfw.BaseTestTool;
import org.lilyproject.testclientfw.Words;
import org.lilyproject.util.Version;
import org.lilyproject.util.json.JsonFormat;
public class RestPerfTest extends BaseTestTool {
private HttpClient httpclient;
private String baseUri;
private int iterations;
private Option iterationsOption;
public static void main(String[] args) throws Exception {
new RestPerfTest().start(args);
}
@Override
protected String getCmdName() {
return "rest-perftest";
}
@Override
protected String getVersion() {
return Version.readVersion("org.lilyproject", "lily-rest-perftest");
}
@Override
@SuppressWarnings("static-access")
public List<Option> getOptions() {
List<Option> options = super.getOptions();
iterationsOption = OptionBuilder
.withArgName("iterations")
.hasArg()
.withDescription("Number of iterations")
.withLongOpt("iterations")
.create("i");
options.add(iterationsOption);
return options;
}
@Override
public int run(CommandLine cmd) throws Exception {
int result = super.run(cmd);
if (result != 0) {
return result;
}
iterations = OptionUtil.getIntOption(cmd, iterationsOption, 10000);
setupMetrics();
ThreadSafeClientConnManager connMgr = new ThreadSafeClientConnManager();
httpclient = new DefaultHttpClient(connMgr);
baseUri = "http://localhost:12060/repository";
// Logger log = Logger.getLogger("org.apache.http");
// log.setLevel(Level.DEBUG);
runTest();
finishMetrics();
return 0;
}
private void runTest() throws Exception {
// Create 5 field types
for (int i = 1; i <= 5; i++) {
String body = json("{name: 'n$field" + i + "', valueType: 'STRING', " +
"scope: 'versioned', namespaces: { 'org.lilyproject.rest-perftest': 'n' } }");
put("/schema/fieldType/n$field" + i + "?ns.n=org.lilyproject.rest-perftest",
body.getBytes(), 200, 201);
}
// Create a record type
{
String body = json("{name: 'n$recordType', fields: [ {name: 'n$field1'}, {name: 'n$field2'}, " +
"{name: 'n$field3'}, {name: 'n$field4'}, {name: 'n$field5'} ]," +
"namespaces: { 'org.lilyproject.rest-perftest': 'n' } }");
put("/schema/recordType/n$recordType?ns.n=org.lilyproject.rest-perftest", body.getBytes(), 200, 201);
}
final List<String> recordIds = new ArrayList<String>();
//
// Create records using put
//
startExecutor();
final JsonNodeFactory factory = JsonNodeFactory.instance;
final long now = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
final int seqnr = i;
executor.submit(new Runnable() {
@Override
public void run() {
try {
ObjectNode recordNode = factory.objectNode();
recordNode.put("type", "n$recordType");
ObjectNode fieldsNode = recordNode.putObject("fields");
for (int f = 1; f <= 5; f++) {
fieldsNode.put("n$field" + f, Words.get());
}
ObjectNode namespaces = recordNode.putObject("namespaces");
namespaces.put("org.lilyproject.rest-perftest", "n");
byte[] recordBytes = JsonFormat.serializeAsBytes(recordNode);
String recordId = "USER." + now + "_" + seqnr;
recordIds.add(recordId);
long before = System.nanoTime();
put("/record/" + recordId, recordBytes, 201);
double duration = System.nanoTime() - before;
metrics.increment("Create record using put", "C", duration / 1e6d);
} catch (Throwable t) {
t.printStackTrace();
}
}
});
}
stopExecutor();
//
// Create records using post
//
startExecutor();
for (int i = 0; i < iterations; i++) {
executor.submit(new Runnable() {
@Override
public void run() {
try {
ObjectNode actionNode = factory.objectNode();
actionNode.put("action", "create");
ObjectNode recordNode = actionNode.putObject("record");
recordNode.put("type", "n$recordType");
ObjectNode fieldsNode = recordNode.putObject("fields");
for (int f = 1; f <= 5; f++) {
fieldsNode.put("n$field" + f, Words.get());
}
ObjectNode namespaces = recordNode.putObject("namespaces");
namespaces.put("org.lilyproject.rest-perftest", "n");
byte[] recordBytes = JsonFormat.serializeAsBytes(actionNode);
long before = System.nanoTime();
Result result = post("/record", recordBytes, 201);
double duration = System.nanoTime() - before;
metrics.increment("Create record using post", "E", duration / 1e6d);
// ID is assigned by the server, read it out
ObjectNode createdRecord = (ObjectNode)JsonFormat.deserialize(result.data);
recordIds.add(createdRecord.get("id").getValueAsText());
} catch (Throwable t) {
t.printStackTrace();
}
}
});
}
stopExecutor();
//
// Read records
//
startExecutor();
for (int i = 0; i < recordIds.size(); i++) {
final int seqnr = i;
executor.submit(new Runnable() {
@Override
public void run() {
try {
long before = System.nanoTime();
get("/record/" + recordIds.get(seqnr), 200);
double duration = System.nanoTime() - before;
metrics.increment("Record read", "R", duration / 1e6d);
} catch (Throwable t) {
t.printStackTrace();
}
}
});
}
stopExecutor();
//
// Read records using vtag
//
startExecutor();
for (int i = 0; i < recordIds.size(); i++) {
final int seqnr = i;
executor.submit(new Runnable() {
@Override
public void run() {
try {
long before = System.nanoTime();
get("/record/" + recordIds.get(seqnr) + "/vtag/last", 200);
double duration = System.nanoTime() - before;
metrics.increment("Record read via last vtag", "T", duration / 1e6d);
} catch (Throwable t) {
t.printStackTrace();
}
}
});
}
stopExecutor();
//
// Read records using version
//
startExecutor();
for (int i = 0; i < recordIds.size(); i++) {
final int seqnr = i;
executor.submit(new Runnable() {
@Override
public void run() {
try {
long before = System.nanoTime();
get("/record/" + recordIds.get(seqnr) + "/version/1", 200);
double duration = System.nanoTime() - before;
metrics.increment("Record read specific version", "V", duration / 1e6d);
} catch (Throwable t) {
t.printStackTrace();
}
}
});
}
stopExecutor();
//
// Read list of versions
//
startExecutor();
for (int i = 0; i < recordIds.size(); i++) {
final int seqnr = i;
executor.submit(new Runnable() {
@Override
public void run() {
try {
long before = System.nanoTime();
get("/record/" + recordIds.get(seqnr) + "/version", 200);
double duration = System.nanoTime() - before;
metrics.increment("Read list of versions", "L", duration / 1e6d);
} catch (Throwable t) {
t.printStackTrace();
}
}
});
}
stopExecutor();
//
// Read list of variants
//
startExecutor();
for (int i = 0; i < recordIds.size(); i++) {
final int seqnr = i;
executor.submit(new Runnable() {
@Override
public void run() {
try {
long before = System.nanoTime();
get("/record/" + recordIds.get(seqnr) + "/variant", 200);
double duration = System.nanoTime() - before;
metrics.increment("Read list of variants", "M", duration / 1e6d);
} catch (Throwable t) {
t.printStackTrace();
}
}
});
}
stopExecutor();
//
// Update records using PUT
//
startExecutor();
for (int i = 0; i < recordIds.size(); i++) {
final int seqnr = i;
executor.submit(new Runnable() {
@Override
public void run() {
try {
ObjectNode recordNode = factory.objectNode();
ObjectNode fieldsNode = recordNode.putObject("fields");
for (int f = 1; f <= 5; f++) {
fieldsNode.put("n$field" + f, Words.get());
}
ObjectNode namespaces = recordNode.putObject("namespaces");
namespaces.put("org.lilyproject.rest-perftest", "n");
byte[] recordBytes = JsonFormat.serializeAsBytes(recordNode);
String recordId = recordIds.get(seqnr);
long before = System.nanoTime();
put("/record/" + recordId, recordBytes, 200);
double duration = System.nanoTime() - before;
metrics.increment("Update record using put", "U", duration / 1e6d);
} catch (Throwable t) {
t.printStackTrace();
}
}
});
}
stopExecutor();
//
// Read records using version
//
startExecutor();
for (int i = 0; i < recordIds.size(); i++) {
final int seqnr = i;
executor.submit(new Runnable() {
@Override
public void run() {
try {
long before = System.nanoTime();
get("/record/" + recordIds.get(seqnr) + "/version/2", 200);
double duration = System.nanoTime() - before;
metrics.increment("Record read specific version (2)", "W", duration / 1e6d);
} catch (Throwable t) {
t.printStackTrace();
}
}
});
}
stopExecutor();
}
private Result get(String path, int... expectedStatus) throws IOException {
HttpGet get = new HttpGet(baseUri + path);
HttpResponse response = httpclient.execute(get);
byte[] data = checkStatusAndReadResponse(path, response, expectedStatus);
return new Result(response, data);
}
private Result put(String path, byte[] body, int... expectedStatus) throws IOException {
HttpPut put = new HttpPut(baseUri + path);
put.addHeader("Content-Type", "application/json");
put.setEntity(new ByteArrayEntity(body));
HttpResponse response = httpclient.execute(put);
byte[] data = checkStatusAndReadResponse(path, response, expectedStatus);
return new Result(response, data);
}
private Result post(String path, byte[] body, int... expectedStatus) throws IOException {
HttpPost post = new HttpPost(baseUri + path);
post.addHeader("Content-Type", "application/json");
post.setEntity(new ByteArrayEntity(body));
HttpResponse response = httpclient.execute(post);
byte[] data = checkStatusAndReadResponse(path, response, expectedStatus);
return new Result(response, data);
}
private byte[] checkStatusAndReadResponse(String path, HttpResponse response, int... expectedStatus)
throws IOException {
// It's important to read out the entity
ByteArrayOutputStream bos = new ByteArrayOutputStream();
response.getEntity().writeTo(bos);
int status = response.getStatusLine().getStatusCode();
Arrays.sort(expectedStatus);
if (Arrays.binarySearch(expectedStatus, status) < 0) {
System.out.println(new String(bos.toByteArray()));
throw new RuntimeException("Unexpected response status. Got " + status + ", expected (one of) " +
toString(expectedStatus) + ". Request path: " + path);
}
return bos.toByteArray();
}
private String json(String input) {
return input.replaceAll("'", "\"");
}
private String toString(int[] numbers) {
StringBuilder builder = new StringBuilder();
for (int number : numbers) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(number);
}
return builder.toString();
}
private static class Result {
HttpResponse response;
byte[] data;
Result(HttpResponse response, byte[] data) {
this.response = response;
this.data = data;
}
}
}