Author: etnu
Date: Fri Dec 5 03:49:32 2008
New Revision: 723717
URL: http://svn.apache.org/viewvc?rev=723717&view=rev
Log:
Improved JSON serialization library that is several times faster than the
closest alternative in CPU usage and that produces a few orders of magnitude
less garbage by avoiding making many allocations when escaping strings.
Included is a benchmark that demonstrates the CPU gap, but running it under a
profiler will show the far more dramatic difference in memory usage.
Added:
incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/JsonSerializer.java
incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/common/JsonSerializerTest.java
Added:
incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/JsonSerializer.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/JsonSerializer.java?rev=723717&view=auto
==============================================================================
---
incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/JsonSerializer.java
(added)
+++
incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/JsonSerializer.java
Fri Dec 5 03:49:32 2008
@@ -0,0 +1,336 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.shindig.common;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Serializes a JSONObject.
+ *
+ * The methods here are designed to be substantially more CPU and memory
efficient than those found
+ * in org.json or net.sf.json. In profiling, the performance of both of these
libraries has been
+ * found to be woefully inadequate for large scale deployments.
+ *
+ * The append*() methods can be used to serialize directly into an Appendable,
such as an output
+ * stream. This avoids unnecessary copies to intermediate objects.
+ */
+public final class JsonSerializer {
+ // Multiplier to use for allocating the buffer.
+ private static final int BASE_MULTIPLIER = 256;
+
+ private static final String[] UNICODE_TABLE = createUnicodeTable();
+
+ private JsonSerializer() {}
+
+ private static String[] createUnicodeTable() {
+ int size = 0;
+ String[] table = new String['\u2100'];
+ for (char c = 0; c < table.length; ++c) {
+ String hex = Integer.toHexString(c);
+ table[c] = "u" + (("000" + hex).substring(hex.length() - 1));
+ size += 2 + (2 * table[c].length());
+ }
+ // Other characters can be sent with plain UTF-8 encoding.
+ return table;
+ }
+
+ /**
+ * Serialize a JSONObject. Does not guard against cyclical references.
+ */
+ public static String serialize(JSONObject object) {
+ StringBuilder buf = new StringBuilder(object.length() * BASE_MULTIPLIER);
+ try {
+ appendJsonObject(buf, object);
+ } catch (IOException e) {
+ // Can't happen.
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Serializes a Map as a JSON object. Does not guard against cyclical
references.
+ */
+ public static String serialize(Map<String, ? extends Object> map) {
+ StringBuilder buf = new StringBuilder(map.size() * BASE_MULTIPLIER);
+ try {
+ appendMap(buf, map);
+ } catch (IOException e) {
+ // Can't happen.
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Serializes a Collection as a JSON array. Does not guard against cyclical
references.
+ */
+ public static String serialize(Collection<? extends Object> collection) {
+ StringBuilder buf = new StringBuilder(collection.size() * BASE_MULTIPLIER);
+ try {
+ appendCollection(buf, collection);
+ } catch (IOException e) {
+ // Can't happen.
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Serializes an array as a JSON array. Does not guard against cyclical
references
+ */
+ public static String serialize(Object[] array) {
+ StringBuilder buf = new StringBuilder(array.length * BASE_MULTIPLIER);
+ try {
+ appendArray(buf, array);
+ } catch (IOException e) {
+ // Can't happen.
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Serializes a JSON array. Does not guard against cyclical references
+ */
+ public static String serialize(JSONArray array) {
+ StringBuilder buf = new StringBuilder(array.length() * BASE_MULTIPLIER);
+ try {
+ appendJsonArray(buf, array);
+ } catch (IOException e) {
+ // Can't happen.
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Appends a value to the buffer.
+ *
+ * @throws IOException If [EMAIL PROTECTED] Appendable#append(char)} throws
an exception.
+ */
+ @SuppressWarnings("unchecked")
+ public static void append(Appendable buf, Object value) throws IOException {
+ if (value == null) {
+ buf.append("null");
+ } else if (value instanceof JSONObject) {
+ appendJsonObject(buf, (JSONObject)value);
+ } else if (value instanceof String) {
+ appendString(buf, (String)value);
+ } else if (value instanceof Number || value instanceof Boolean) {
+ buf.append(value.toString());
+ } else if (value instanceof JSONArray) {
+ buf.append(value.toString());
+ } else if (value instanceof Map) {
+ appendMap(buf, (Map<String, Object>)value);
+ } else if (value instanceof Collection) {
+ appendCollection(buf, (Collection<Object>)value);
+ } else if (value.getClass().isArray()) {
+ appendArray(buf, (Object[])value);
+ } else {
+ appendString(buf, value.toString());
+ }
+ }
+
+ /**
+ * Appends an array to the buffer.
+ *
+ * @throws IOException If [EMAIL PROTECTED] Appendable#append(char)} throws
an exception.
+ */
+ public static void appendArray(Appendable buf, Object[] array) throws
IOException {
+ buf.append('[');
+ boolean firstDone = false;
+ for (Object o : array) {
+ if (firstDone) {
+ buf.append(',');
+ } else {
+ firstDone = true;
+ }
+ append(buf, o);
+ }
+ buf.append(']');
+ }
+
+ /**
+ * Append a JSONArray to the buffer.
+ * @throws IOException If [EMAIL PROTECTED] Appendable#append(char)} throws
an exception.
+ */
+ public static void appendJsonArray(Appendable buf, JSONArray array) throws
IOException {
+ buf.append('[');
+ boolean firstDone = false;
+ for (int i = 0, j = array.length(); i < j; ++i) {
+ if (firstDone) {
+ buf.append(',');
+ } else {
+ firstDone = true;
+ }
+ append(buf, array.opt(i));
+ }
+ buf.append(']');
+ }
+
+ /**
+ * Appends a Collection to the buffer.
+ *
+ * @throws IOException If [EMAIL PROTECTED] Appendable#append(char)} throws
an exception.
+ */
+ public static void appendCollection(Appendable buf, Collection<? extends
Object> collection)
+ throws IOException {
+ buf.append('[');
+ boolean firstDone = false;
+ for (Object o : collection) {
+ if (firstDone) {
+ buf.append(',');
+ } else {
+ firstDone = true;
+ }
+ append(buf, o);
+ }
+ buf.append(']');
+ }
+
+ /**
+ * Appends a Map to the buffer.
+ *
+ * @throws IOException If [EMAIL PROTECTED] Appendable#append(char)} throws
an exception.
+ */
+ public static void appendMap(Appendable buf, Map<String, ? extends Object>
map)
+ throws IOException {
+ buf.append('{');
+ boolean firstDone = false;
+ for (Map.Entry<String, ? extends Object> entry : map.entrySet()) {
+ if (firstDone) {
+ buf.append(',');
+ } else {
+ firstDone = true;
+ }
+ appendString(buf, entry.getKey());
+ buf.append(':');
+ append(buf, entry.getValue());
+ }
+ buf.append('}');
+ }
+
+ /**
+ * Appends a JSONObject to the buffer.
+ *
+ * @throws IOException If [EMAIL PROTECTED] Appendable#append(char)} throws
an exception.
+ */
+ @SuppressWarnings("unchecked")
+ public static void appendJsonObject(Appendable buf, JSONObject object)
throws IOException {
+ buf.append('{');
+ Iterator<String> keys = object.keys();
+ boolean firstDone = false;
+ while (keys.hasNext()) {
+ if (firstDone) {
+ buf.append(',');
+ } else {
+ firstDone = true;
+ }
+ String key = keys.next();
+ appendString(buf, key);
+ buf.append(':');
+ append(buf, object.opt(key));
+ }
+ buf.append('}');
+ }
+
+ /**
+ * Appends a string to the buffer. The string will be JSON encoded and
enclosed in quotes.
+ *
+ * @throws IOException If [EMAIL PROTECTED] Appendable#append(char)} throws
an exception.
+ */
+ public static void appendString(Appendable buf, CharSequence string) throws
IOException {
+ if (string == null || string.length() == 0) {
+ buf.append("\"\"");
+ return;
+ }
+
+ char previous, current = 0;
+ buf.append('"');
+ for (int i = 0, j = string.length(); i < j; ++i) {
+ previous = current;
+ current = string.charAt(i);
+ switch (current) {
+ case '\\':
+ case '"':
+ buf.append('\\');
+ buf.append(current);
+ break;
+ case '/':
+ if (previous == '<') {
+ buf.append('\\');
+ }
+ buf.append(current);
+ break;
+ default:
+ if (current < ' ' || (current >= '\u0080' && current < '\u00a0') ||
+ (current >= '\u2000' && current < '\u2100')) {
+ buf.append('\\');
+ switch (current) {
+ case '\b':
+ buf.append('b');
+ break;
+ case '\t':
+ buf.append('t');
+ break;
+ case '\n':
+ buf.append('n');
+ break;
+ case '\f':
+ buf.append('f');
+ break;
+ case '\r':
+ buf.append('r');
+ break;
+ default:
+ // The three possible alternative approaches for dealing with
unicode characters are
+ // as follows:
+ // Method 1 (from json.org.JSONObject)
+ // 1. Append "000" + Integer.toHexString(current)
+ // 2. Truncate this value to 4 digits by using
value.substring(value.length() - 4)
+ //
+ // Method 2 (from net.sf.json.JSONObject)
+ // This method is fairly unique because the entire thing uses
an intermediate fixed
+ // size buffer of 1KB. It's an interesting approach, but
overall performs worse than
+ // org.json
+ // 1. Append "000" + Integer.toHexString(current)
+ // 2. Append value.charAt(value.length() - 4)
+ // 2. Append value.charAt(value.length() - 3)
+ // 2. Append value.charAt(value.length() - 2)
+ // 2. Append value.charAt(value.length() - 1)
+ //
+ // Method 3 (previous experiment)
+ // 1. Calculate Integer.hexString(current)
+ // 2. for (int i = 0; i < 4 - value.length(); ++i) {
buf.append('0'); }
+ // 3. buf.append(value)
+ //
+ // Of these, the second proved fastest. The current
implementation performs slightly
+ // faster than the second in the benchmark found in
JsonSerializerTest.
+ buf.append(UNICODE_TABLE[current]);
+ }
+ } else {
+ buf.append(current);
+ }
+ }
+ }
+ buf.append('"');
+ }
+}
Added:
incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/common/JsonSerializerTest.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/common/JsonSerializerTest.java?rev=723717&view=auto
==============================================================================
---
incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/common/JsonSerializerTest.java
(added)
+++
incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/common/JsonSerializerTest.java
Fri Dec 5 03:49:32 2008
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.shindig.common;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Maps;
+
+import org.apache.commons.lang.StringUtils;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Test;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Tests for JsonSerializer.
+ *
+ * This class may be executed to perform micro benchmarks comparing the
performance of the
+ * serializer with that of json.org and net.sf.json.
+ */
+public class JsonSerializerTest {
+
+ @Test
+ public void serializeSimpleJsonObject() throws Exception {
+ JSONObject json = new JSONObject("{\"foo\":\"bar\"}");
+ assertTrue("Did not produce results matching reference implementation.",
+ jsonEquals(json.toString(), JsonSerializer.serialize(json)));
+ }
+
+ @Test
+ public void serializeSimpleMap() throws Exception {
+ Map<String, String> map = Maps.immutableMap("hello", "world", "foo",
"bar");
+ assertTrue("Did not produce results matching reference implementation.",
+ jsonEquals(new JSONObject(map).toString(),
JsonSerializer.serialize(map)));
+ }
+
+ @Test
+ public void serializeSimpleCollection() throws Exception {
+ Collection<String> collection = Arrays.asList("foo", "bar", "baz");
+ assertEquals("[\"foo\",\"bar\",\"baz\"]",
JsonSerializer.serialize(collection));
+ }
+
+ @Test
+ public void serializeArray() throws Exception {
+ String[] array = new String[] {"foo", "bar", "baz"};
+ assertEquals("[\"foo\",\"bar\",\"baz\"]", JsonSerializer.serialize(array));
+ }
+
+ @Test
+ public void serializeJsonArray() throws Exception {
+ JSONArray array = new JSONArray(new String[] {"foo", "bar", "baz"});
+ assertEquals("[\"foo\",\"bar\",\"baz\"]", JsonSerializer.serialize(array));
+ }
+
+ @Test
+ public void serializeMixedObjects() throws Exception {
+ Map<String, ? extends Object> map = Maps.immutableMap(
+ "integer", Integer.valueOf(100),
+ "double", Double.valueOf(233333333333.7d),
+ "boolean", Boolean.TRUE,
+ "map", Maps.immutableMap("hello", "world", "foo", "bar"),
+ "string", "hello!");
+ assertTrue("Did not produce results matching reference implementation.",
+ jsonEquals(new JSONObject(map).toString(),
JsonSerializer.serialize(map)));
+ }
+
+ @Test
+ public void serializeMixedArray() throws Exception {
+ Collection<Object> data = Arrays.asList(
+ "integer", Integer.valueOf(100),
+ "double", Double.valueOf(233333333333.7d),
+ "boolean", Boolean.TRUE,
+ Arrays.asList("one", "two", "three"),
+ new JSONArray(new String[] {"foo", "bar"}),
+ "string", "hello!");
+ assertEquals(new JSONArray(data).toString(),
JsonSerializer.serialize(data));
+ }
+
+ @Test
+ public void emptyString() throws Exception {
+ StringBuilder builder = new StringBuilder();
+ JsonSerializer.appendString(builder, "");
+
+ assertEquals("\"\"", builder.toString());
+ }
+
+ @Test
+ public void escapeSequences() throws Exception {
+ StringBuilder builder = new StringBuilder();
+ JsonSerializer.appendString(builder, "\t\r value
\\\foo\b\uFFFF\uBCAD\n\u0083");
+
+ assertEquals("\"\\t\\r value \\\\\\foo\\b\uFFFF\uBCAD\\n\\u0083\"",
builder.toString());
+ }
+
+ private static String avg(long start, long end, long runs) {
+ double delta = end - start;
+ return String.format("%f5", delta / runs);
+ }
+
+ private static String runJsonOrgTest(Map<String, Object> data, int
iterations) {
+ org.json.JSONObject object = new org.json.JSONObject(data);
+ long start = System.currentTimeMillis();
+ String result = null;
+ for (int i = 0; i < iterations; ++i) {
+ result = object.toString();
+ }
+ System.out.println("json.org: " + avg(start, System.currentTimeMillis(),
iterations) + "ms");
+ return result;
+ }
+
+ private static String runSerializerTest(Map<String, Object> data, int
iterations) {
+ long start = System.currentTimeMillis();
+ String result = null;
+ for (int i = 0; i < iterations; ++i) {
+ result = JsonSerializer.serialize(data);
+ }
+ System.out.println("serializer: " + avg(start, System.currentTimeMillis(),
iterations) + "ms");
+ return result;
+ }
+
+ private static String runNetSfJsonTest(Map<String, Object> data, int
iterations) {
+ net.sf.json.JSONObject object = net.sf.json.JSONObject.fromObject(data);
+ long start = System.currentTimeMillis();
+ String result = null;
+ for (int i = 0; i < iterations; ++i) {
+ result = object.toString();
+ }
+ System.out.println("net.sf.json: " + avg(start,
System.currentTimeMillis(), iterations) + "ms");
+ return result;
+ }
+
+ public static Map<String, Object> perfComparison100SmallValues() {
+ Map<String, Object> data = Maps.newHashMap();
+ for (int i = 0; i < 100; ++i) {
+ data.put("key-" + i, "small value");
+ }
+
+ return data;
+ }
+
+ public static Map<String, Object> perfComparison1000SmallValues() {
+ Map<String, Object> data = Maps.newHashMap();
+ for (int i = 0; i < 1000; ++i) {
+ data.put("key-" + i, "small value");
+ }
+
+ return data;
+ }
+
+ public static Map<String, Object> perfComparison100LargeValues() {
+ Map<String, Object> data = Maps.newHashMap();
+ for (int i = 0; i < 100; ++i) {
+ data.put("key-" + i, StringUtils.repeat("small value", 100));
+ }
+ return data;
+ }
+
+ public static Map<String, Object> perfComparison10LargeValuesAndEscapes() {
+ Map<String, Object> data = Maps.newHashMap();
+ for (int i = 0; i < 10; ++i) {
+ data.put("key-" + i, StringUtils.repeat("\tsmall\r value
\\foo\b\uFFFF\uBCAD\n\u0083", 100));
+ }
+ return data;
+ }
+
+ public static Map<String, Object> perfComparison100Arrays() {
+ Map<String, Object> data = Maps.newHashMap();
+ String[] array = new String[] {
+ "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
"ten"
+ };
+
+ for (int i = 0; i < 100; ++i) {
+ data.put("key-" + i, array);
+ }
+
+ return data;
+ }
+
+ private static boolean jsonEquals(JSONObject left, JSONObject right) {
+ if (left.length() != right.length()) {
+ return false;
+ }
+ for (String name : JSONObject.getNames(left)) {
+ Object leftValue = left.opt(name);
+ Object rightValue = right.opt(name);
+ if (leftValue instanceof JSONObject) {
+ if (!jsonEquals((JSONObject)leftValue, (JSONObject)rightValue)) {
+ return false;
+ }
+ } else if (leftValue instanceof JSONArray) {
+ JSONArray leftArray = (JSONArray)leftValue;
+ JSONArray rightArray = (JSONArray)rightValue;
+ for (int i = 0; i < leftArray.length(); ++i) {
+ if (!(leftArray.opt(i).equals(rightArray.opt(i)))) {
+ return false;
+ }
+ }
+ } else if (!leftValue.equals(rightValue)) {
+ System.out.println("Not a match: " + leftValue + " != " + rightValue);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean jsonEquals(String reference, String comparison)
throws Exception {
+ return jsonEquals(new JSONObject(reference), new JSONObject(comparison));
+ }
+
+ public static void main(String[] args) throws Exception {
+ int iterations = args.length > 0 ? Integer.parseInt(args[0]) : 1000;
+ System.out.println("Running tests with " + iterations + " iterations.");
+
+ for (Method method : JsonSerializerTest.class.getMethods()) {
+ if (method.getName().startsWith("perfComparison")) {
+ Map<String, Object> data = (Map<String, Object>)method.invoke(null);
+ System.out.println("Running: " + method.getName());
+
+ String jsonOrg = runJsonOrgTest(data, iterations);
+ String serializer = runSerializerTest(data, iterations);
+ String netSfJson = runNetSfJsonTest(data, iterations);
+
+ if (!jsonEquals(jsonOrg, serializer)) {
+ System.out.println("Serializer did not produce results matching the
reference impl.");
+ }
+
+ if (!jsonEquals(jsonOrg, netSfJson)) {
+ System.out.println("net.sf.json did not produce results matching the
reference impl.");
+ }
+ System.out.println("-----------------------");
+ }
+ }
+ System.out.println("Done");
+ }
+}