Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Perf Suite + Native JSON parsing #201

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions jsTestDriver-perf.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
server: http://localhost:9876

load:
- lib/jasmine-1.0.1/jasmine.js
- lib/jasmine-jstd-adapter/JasmineAdapter.js
- lib/jquery/jquery-1.4.2.js
- test/jquery_remove.js
- build/angular.min.js
- perf/data/*.js
- perf/testUtils.js
- perf/*.js

exclude:
10 changes: 10 additions & 0 deletions perf/data/jsonParserPayload.js

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions perf/data/jsonParserPayload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
def generate_object(f, objName, iterations)
f.write("var #{objName}='[");

iterations.times do |i|
f.write('{')

f.write('"simpleStringProperty":') #23
f.write('"some string value ' + ('%07d' % i) + '"') #27
f.write(',')

f.write('"stringWithQuotes":') #19
f.write('"some string with \\\\"quotes\\\\" ' + ('%07d' % i) + '"') #36
f.write(',')

f.write('"stringWithUnicode":')
f.write('"short string with \\u1234 unicode \\u2345 chars ' + ('%07d' % i) + '"')
f.write(',')

f.write('"aNumber":') #10
f.write(i) #?
f.write(',')

f.write('"smallArray":')
f.write('["a",23,"b",42,' + i.to_s + ']')
f.write(',')

f.write('"smallObj":')
f.write('{"foo":"bar","baz":543,"num":' + i.to_s + ',"fuz":"fuz buz huz duz ' + i.to_s + '"}')
f.write(',')

f.write('"timeStamp":')
f.write('"2010-12-22T04:58:01.' + ("%03d" % (i%1000)) + '"')

f.write('},')
end

f.write('"just a padding string"]\';' + "\n\n");
end

file_path = File.join(File.dirname(__FILE__), 'jsonParserPayload.js')

File.open(file_path, 'w') do |f|
generate_object(f, 'superTinyJsonString', 1) #~300b
generate_object(f, 'tinyJsonString', 3) #~1kb
generate_object(f, 'smallJsonString', 30) #~10kb
generate_object(f, 'mediumJsonString', 600) #~200kb
generate_object(f, 'largeJsonString', 2000) #~650kb
end

28 changes: 28 additions & 0 deletions perf/jsonPerfSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
describe('json', function() {

it('angular parser', function() {
var duration = time(function() {
expect(angular.fromJson(largeJsonString)).toBeTruthy();
}, 1);

dump(duration/1 + ' ms per iteration');
});


it('angular delegating to native parser', function() {
var duration = time(function() {
expect(angular.fromJson(largeJsonString, true)).toBeTruthy();
}, 100);

dump(duration/100 + ' ms per iteration');
});


it('native json', function() {
var duration = time(function() {
expect(JSON.parse(largeJsonString)).toBeTruthy();
}, 100);

dump(duration/100 + ' ms per iteration');
});
});
20 changes: 20 additions & 0 deletions perf/testUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
if (window.jstestdriver) {
jstd = jstestdriver;
dump = angular.bind(jstd.console, jstd.console.log);
}

function time(fn, times) {
times = times || 1;

var i,
start,
duration = 0;

for (i=0; i<times; i++) {
start = Date.now();
fn();
duration += Date.now() - start;
}

return duration;
}
2 changes: 1 addition & 1 deletion server.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/bash

java -jar lib/jstestdriver/JsTestDriver.jar --port 9876 --browserTimeout 20000
java -jar lib/jstestdriver/JsTestDriver.jar --port 9876 --browserTimeout 90000
3 changes: 2 additions & 1 deletion src/Angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ var _undefined = undefined,
angularService = extensionMap(angular, 'service'),
angularCallbacks = extensionMap(angular, 'callbacks'),
nodeName,
rngScript = /^(|.*\/)angular(-.*?)?(\.min)?.js(\?[^#]*)?(#(.*))?$/;
rngScript = /^(|.*\/)angular(-.*?)?(\.min)?.js(\?[^#]*)?(#(.*))?$/,
DATE_ISOSTRING_LN = 24;

/**
* @workInProgress
Expand Down
28 changes: 25 additions & 3 deletions src/JSON.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,41 @@ function toJson(obj, pretty) {
* Deserializes a string in the JSON format.
*
* @param {string} json JSON string to deserialize.
* @param {boolean} [useNative=false] Use native JSON parser if available
* @returns {Object|Array|Date|string|number} Deserialized thingy.
*/
function fromJson(json) {
function fromJson(json, useNative) {
if (!json) return json;

var obj, p, expression;

try {
var p = parser(json, true);
var expression = p.primary();
if (useNative && JSON && JSON.parse) {
obj = JSON.parse(json);
return transformDates(obj);
}

p = parser(json, true);
expression = p.primary();
p.assertAllConsumed();
return expression();

} catch (e) {
error("fromJson error: ", json, e);
throw e;
}

// TODO make foreach optionally recursive and remove this function
function transformDates(obj) {
if (isString(obj) && obj.length === DATE_ISOSTRING_LN) {
return angularString.toDate(obj);
} else if (isArray(obj) || isObject(obj)) {
foreach(obj, function(val, name) {
obj[name] = transformDates(val);
});
}
return obj;
}
}

angular['toJson'] = toJson;
Expand Down
139 changes: 91 additions & 48 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ var OPERATORS = {
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};

function lex(text, parseStringsForObjects){
var dateParseLength = parseStringsForObjects ? 24 : -1,
var dateParseLength = parseStringsForObjects ? DATE_ISOSTRING_LN : -1,
tokens = [],
token,
index = 0,
json = [],
ch,
lastCh = ':';
lastCh = ':'; // can start regexp

while (index < text.length) {
ch = text.charAt(index);
Expand Down Expand Up @@ -71,9 +71,6 @@ function lex(text, parseStringsForObjects){
lastCh = ch;
}
return tokens;


//////////////////////////////////////////////

function is(chars) {
return chars.indexOf(ch) != -1;
Expand All @@ -98,6 +95,10 @@ function lex(text, parseStringsForObjects){
'A' <= ch && ch <= 'Z' ||
'_' == ch || ch == '$';
}
function isExpOperator(ch) {
return ch == '-' || ch == '+' || isNumber(ch);
}

function throwError(error, start, end) {
end = end || index;
throw Error("Lexer Error: " + error + " at column" +
Expand All @@ -106,61 +107,103 @@ function lex(text, parseStringsForObjects){
" " + end) +
" in expression [" + text + "].");
}

function consume(regexp, processToken, errorMsg) {
var match = text.substr(index).match(regexp);
var token = {index: index};
var start = index;
if (!match) throwError(errorMsg);
index += match[0].length;
processToken(token, token.text = match[0], start);
tokens.push(token);
}

function readNumber() {
consume(/^(\d+)?(\.\d+)?([eE][+-]?\d+)?/, function(token, number){
token.text = number = 1 * number;
token.json = true;
token.fn = valueFn(number);
}, "Not a valid number");
var number = "";
var start = index;
while (index < text.length) {
var ch = lowercase(text.charAt(index));
if (ch == '.' || isNumber(ch)) {
number += ch;
} else {
var peekCh = peek();
if (ch == 'e' && isExpOperator(peekCh)) {
number += ch;
} else if (isExpOperator(ch) &&
peekCh && isNumber(peekCh) &&
number.charAt(number.length - 1) == 'e') {
number += ch;
} else if (isExpOperator(ch) &&
(!peekCh || !isNumber(peekCh)) &&
number.charAt(number.length - 1) == 'e') {
throwError('Invalid exponent');
} else {
break;
}
}
index++;
}
number = 1 * number;
tokens.push({index:start, text:number, json:true,
fn:function(){return number;}});
}

function readIdent() {
consume(/^[\w_\$][\w_\$\d]*(\.[\w_\$][\w_\$\d]*)*/, function(token, ident){
fn = OPERATORS[ident];
if (!fn) {
fn = getterFn(ident);
fn.isAssignable = ident;
var ident = "";
var start = index;
var fn;
while (index < text.length) {
var ch = text.charAt(index);
if (ch == '.' || isIdent(ch) || isNumber(ch)) {
ident += ch;
} else {
break;
}
token.fn = OPERATORS[ident]||extend(getterFn(ident), {
index++;
}
fn = OPERATORS[ident];
tokens.push({
index:start,
text:ident,
json: fn,
fn:fn||extend(getterFn(ident), {
assign:function(self, value){
return setter(self, ident, value);
}
});
token.json = OPERATORS[ident];
})
});
}

function readString(quote) {
consume(/^(('(\\'|[^'])*')|("(\\"|[^"])*"))/, function(token, rawString, start){
var hasError;
var string = token.string = rawString.substr(1, rawString.length - 2).
replace(/(\\u(.?.?.?.?))|(\\(.))/g,
function(match, wholeUnicode, unicode, wholeEscape, escape){
if (unicode && !unicode.match(/[\da-fA-F]{4}/))
hasError = hasError || bind(null, throwError, "Invalid unicode escape [\\u" + unicode + "]", start);
return unicode ?
String.fromCharCode(parseInt(unicode, 16)) :
ESCAPE[escape] || escape;
});
(hasError||noop)();
token.json = true;
token.fn = function(){
return (string.length == dateParseLength) ?
angular['String']['toDate'](string) :
string;
};
}, "Unterminated string");
var start = index;
index++;
var string = "";
var rawString = quote;
var escape = false;
while (index < text.length) {
var ch = text.charAt(index);
rawString += ch;
if (escape) {
if (ch == 'u') {
var hex = text.substring(index + 1, index + 5);
if (!hex.match(/[\da-f]{4}/i))
throwError( "Invalid unicode escape [\\u" + hex + "]");
index += 4;
string += String.fromCharCode(parseInt(hex, 16));
} else {
var rep = ESCAPE[ch];
if (rep) {
string += rep;
} else {
string += ch;
}
}
escape = false;
} else if (ch == '\\') {
escape = true;
} else if (ch == quote) {
index++;
tokens.push({index:start, text:rawString, string:string, json:true,
fn:function(){
return (string.length == dateParseLength) ?
angular['String']['toDate'](string) : string;
}});
return;
} else {
string += ch;
}
index++;
}
throwError("Unterminated quote", start);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ angularServiceInject('$xhr', function($browser, $error, $log){
$browser.xhr(method, url, post, function(code, response){
try {
if (isString(response) && /^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) {
response = fromJson(response);
response = fromJson(response, true);
}
if (code == 200) {
callback(code, response);
Expand Down
13 changes: 13 additions & 0 deletions test-perf.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
tests=$1
norecompile=$2

if [[ $tests = "" ]]; then
tests="all"
fi

if [[ $norecompile = "" ]]; then
rake compile
fi

java -jar lib/jstestdriver/JsTestDriver.jar --tests "$tests" --config jsTestDriver-perf.conf
Loading