1
0
Fork 0

Miscellaneous performance enhancements

These are the beginning of some smaller performance optimizations brought on
by the v8 profiler. This includes removal or movement of over-reaching
try/catch blocks and more disciplined argument handling, neither of which
can be compiled into machine code (permanently, at least). This also removes
some unneeded code, adds some baseline performance test cases, and begins
generic performance test output and HTML generation which will be used in
the future for more detailed analysis.

This is just a starting point; there's more to come, guided by profiling.
The trait implementation needs some love and, since its development is not
yet complete, that will be optimized in the near future. Further, there are
additional optimizations that can be made when ease.js recognizes that
certain visibility layers are unneeded, allowing it to create more
lightweight classes.

Performance enhancements will also introduce the ability to generate a
``compiled'' class, which will generate a prototype that can be immediately
run without the overhead of processing keywords, etc. This will also have
the benefit of generating code that can be understood by static analysis
tools and, consequently, optimizers.

All in good time.
newmaster
Mike Gerwitz 2014-04-17 00:01:40 -04:00
commit 86a4703a1c
No known key found for this signature in database
GPG Key ID: F22BB8158EE30EAB
13 changed files with 354 additions and 161 deletions

4
.gitignore vendored
View File

@ -10,6 +10,7 @@ ChangeLog
# autotools- and configure-generated
test/runner
test/perf/runner
aclocal.m4
Makefile.in
Makefile
@ -17,6 +18,9 @@ Makefile
configure
config.*
# script output
perf.*
# should be added using autoreconf -i
INSTALL
tools/install-sh

View File

@ -111,11 +111,14 @@ html-single:
test: check
check: $(src_tests) test-suite
# performance tests
perf: @PERF_TESTS@
perf-%.js: FORCE
# performance tests (order matters here)
perf-html: perf.log.html
perf.%.html: perf.%
sort "$<" | $(path_tools)/perf2html -F\| > "$@"
perf: perf.log
perf.%: FORCE
if HAS_NODE
@$(NODE) $@
@$(path_test)/perf/runner @PERF_TESTS@ > "$@"
else
@echo "Node.js must be installed in order to run performance tests"
@exit 1

View File

@ -87,7 +87,8 @@ PERF_TESTS=$( find test/perf -name 'perf-*.js' | tr '\n' ' ' )
AC_SUBST(PERF_TESTS)
AS_IF([test "$PERF_TESTS"], [AC_MSG_RESULT(ok)], [AC_MSG_WARN(none found)])
AC_CONFIG_FILES(
[Makefile doc/Makefile package.json lib/version.js test/runner],
[chmod +x test/runner])
AC_CONFIG_FILES([Makefile doc/Makefile package.json lib/version.js])
AC_CONFIG_FILES([test/runner], [chmod +x test/runner])
AC_CONFIG_FILES([test/perf/runner], [chmod +x test/perf/runner])
AC_OUTPUT

View File

@ -244,16 +244,14 @@ exports.isInstanceOf = function( type, instance )
return false;
}
try
{
// check prototype chain (will throw an error if type is not a
// constructor (function)
if ( instance instanceof type )
if ( ( typeof type === 'function' )
&& ( instance instanceof type )
)
{
return true;
}
}
catch ( e ) {}
// if no metadata is available, then our remaining checks cannot be
// performed
@ -299,9 +297,10 @@ exports.prototype.build = function extend( _, __ )
// ensure we'll be permitted to instantiate abstract classes for the base
this._extending = true;
var args = Array.prototype.slice.call( arguments ),
props = args.pop() || {},
base = args.pop() || exports.ClassBase,
var a = arguments,
an = a.length,
props = ( ( an > 0 ) ? a[ an - 1 ] : 0 ) || {},
base = ( ( an > 1 ) ? a[ an - 2 ] : 0 ) || exports.ClassBase,
prototype = this._getBase( base ),
cname = '',
autoa = false,
@ -508,7 +507,10 @@ exports.prototype.buildMembers = function buildMembers(
{
handlers[ name ] = function()
{
var args = Array.prototype.slice.call( arguments );
var args = [],
i = arguments.length;
while ( i-- ) args[ i ] = arguments[ i ];
// invoke the custom handler with the original handler as
// its last argument (which the custom handler may choose

View File

@ -64,19 +64,24 @@ var _nullf = function() { return null; }
module.exports = function( namedef, def )
{
var type = ( typeof namedef ),
result = null
result = null,
args = [],
i = arguments.length
;
// passing arguments object prohibits optimizations in v8
while ( i-- ) args[ i ] = arguments[ i ];
switch ( type )
{
// anonymous class
case 'object':
result = createAnonymousClass.apply( null, arguments );
result = createAnonymousClass.apply( null, args );
break;
// named class
case 'string':
result = createNamedClass.apply( null, arguments );
result = createNamedClass.apply( null, args );
break;
default:
@ -99,10 +104,7 @@ module.exports = function( namedef, def )
*
* @return {Function} extended class
*/
module.exports.extend = function( baseordfn, dfn )
{
return extend.apply( this, arguments );
};
module.exports.extend = extend;
/**
@ -138,11 +140,11 @@ module.exports.implement = function( interfaces )
*/
module.exports.use = function( traits )
{
var args = [], i = arguments.length;
while( i-- ) args[ i ] = arguments[ i ];
// consume traits onto an empty base
return createUse(
_nullf,
Array.prototype.slice.call( arguments )
);
return createUse( _nullf, args );
};
@ -292,7 +294,10 @@ function createStaging( cname )
return {
extend: function()
{
var args = Array.prototype.slice.apply( arguments );
var args = [],
i = arguments.length;
while ( i-- ) args[ i ] = arguments[ i ];
// extend() takes a maximum of two arguments. If only one
// argument is provided, then it is to be the class definition.
@ -308,21 +313,24 @@ function createStaging( cname )
implement: function()
{
var args = [],
i = arguments.length;
while ( i-- ) args[ i ] = arguments[ i ];
// implement on empty base, providing the class name to be used once
// extended
return createImplement(
null,
Array.prototype.slice.call( arguments ),
cname
);
return createImplement( null, args, cname );
},
use: function()
{
return createUse(
_nullf,
Array.prototype.slice.call( arguments )
);
var args = [],
i = arguments.length;
while ( i-- ) args[ i ] = arguments[ i ];
return createUse( _nullf, args );
},
};
}
@ -349,14 +357,14 @@ function createImplement( base, ifaces, cname )
var partial = {
extend: function()
{
var args = Array.prototype.slice.call( arguments ),
def = args.pop(),
ext_base = args.pop()
var an = arguments.length,
def = arguments[ an - 1 ],
ext_base = ( an > 1 ) ? arguments[ an - 2 ] : null
;
// if any arguments remain, then they likely misunderstood what this
// method does
if ( args.length > 0 )
if ( an > 2 )
{
throw Error(
"Expecting no more than two arguments for extend()"
@ -394,7 +402,12 @@ function createImplement( base, ifaces, cname )
// much more performant (it creates a subtype before mixing in)
use: function()
{
var traits = Array.prototype.slice.call( arguments );
var traits = [],
i = arguments.length;
// passing arguments object prohibits optimizations in v8
while ( i-- ) traits[ i ] = arguments[ i ];
return createUse(
function() { return partial.__createBase(); },
traits
@ -468,9 +481,9 @@ function createUse( basef, traits, nonbase )
// given during the extend operation
partial.extend = function()
{
var args = Array.prototype.slice.call( arguments ),
dfn = args.pop(),
ext_base = args.pop(),
var an = arguments.length,
dfn = arguments[ an - 1 ],
ext_base = ( an > 1 ) ? arguments[ an - 2 ] : null,
base = basef();
// extend the mixed class, which ensures that all super references
@ -485,12 +498,17 @@ function createUse( basef, traits, nonbase )
// call simply to mix in another trait
partial.use = function()
{
var args = [],
i = arguments.length;
while ( i-- ) args[ i ] = arguments[ i ];
return createUse(
function()
{
return partial.__createBase();
},
Array.prototype.slice.call( arguments ),
args,
nonbase
);
};
@ -557,8 +575,14 @@ function createMixedClass( base, traits )
*/
function extend( _, __ )
{
var args = [],
i = arguments.length;
// passing arguments object prohibits optimizations in v8
while ( i-- ) args[ i ] = arguments[ i ];
// set up the new class
var new_class = class_builder.build.apply( class_builder, arguments );
var new_class = class_builder.build.apply( class_builder, args );
// set up some additional convenience props
setupProps( new_class );
@ -584,10 +608,9 @@ function extend( _, __ )
*/
var implement = function( baseobj, interfaces )
{
var args = Array.prototype.slice.call( arguments ),
var an = arguments.length,
dest = {},
base = args.pop(),
len = args.length,
base = arguments[ an - 1 ],
arg = null,
implemented = [],
@ -595,9 +618,9 @@ var implement = function( baseobj, interfaces )
;
// add each of the interfaces
for ( var i = 0; i < len; i++ )
for ( var i = 0; i < ( an - 1 ); i++ )
{
arg = args[ i ];
arg = arguments[ i ];
// copy all interface methods to the class (does not yet deep copy)
util.propParse( arg.prototype, {
@ -678,10 +701,10 @@ function attachImplement( func )
{
util.defineSecureProp( func, 'implement', function()
{
return createImplement(
func,
Array.prototype.slice.call( arguments )
);
var args = [], i = arguments.length;
while( i-- ) args[ i ] = arguments[ i ];
return createImplement( func, args );
});
}
@ -699,11 +722,10 @@ function attachUse( func )
{
util.defineSecureProp( func, 'use', function()
{
return createUse(
function() { return func; },
Array.prototype.slice.call( arguments ),
true
);
var args = [], i = arguments.length;
while( i-- ) args[ i ] = arguments[ i ];
return createUse( function() { return func; }, args, true );
} );
}

View File

@ -33,7 +33,7 @@ var Class = require( __dirname + '/class' );
*/
module.exports = exports = function()
{
markAbstract( arguments );
markAbstract( arguments[ arguments.length - 1 ] );
// forward everything to Class
var result = Class.apply( this, arguments );
@ -56,7 +56,7 @@ module.exports = exports = function()
*/
exports.extend = function()
{
markAbstract( arguments );
markAbstract( arguments[ arguments.length - 1 ] );
return Class.extend.apply( this, arguments );
};
@ -92,18 +92,12 @@ exports.implement = function()
/**
* Causes a definition to be flagged as abstract
*
* This function assumes the last argument to be the definition, which is the
* common case, and modifies the object referenced by that argument.
*
* @param {Arguments} args arguments to parse
* @param {*} dfn suspected definition object
*
* @return {undefined}
*/
function markAbstract( args )
function markAbstract( dfn )
{
// the last argument _should_ be the definition
var dfn = args[ args.length - 1 ];
if ( typeof dfn === 'object' )
{
// mark as abstract
@ -141,7 +135,7 @@ function abstractOverride( obj )
// wrap extend, applying the abstract flag
obj.extend = function()
{
markAbstract( arguments );
markAbstract( arguments[ arguments.length - 1 ] );
return extend.apply( this, arguments );
};

View File

@ -29,7 +29,7 @@ var Class = require( __dirname + '/class' );
*/
exports = module.exports = function()
{
markFinal( arguments );
markFinal( arguments[ arguments.length - 1 ] );
// forward everything to Class
var result = Class.apply( this, arguments );
@ -50,7 +50,7 @@ exports = module.exports = function()
*/
exports.extend = function()
{
markFinal( arguments );
markFinal( arguments[ arguments.length - 1 ] );
return Class.extend.apply( this, arguments );
};
@ -58,18 +58,12 @@ exports.extend = function()
/**
* Causes a definition to be flagged as final
*
* This function assumes the last argument to be the definition, which is the
* common case, and modifies the object referenced by that argument.
*
* @param {!Arguments} args arguments to parse
* @param {!Arguments} dfn suspected definition object
*
* @return {undefined}
*/
function markFinal( args )
function markFinal( dfn )
{
// the last argument _should_ be the definition
var dfn = args[ args.length - 1 ];
if ( typeof dfn === 'object' )
{
// mark as abstract
@ -92,7 +86,7 @@ function finalOverride( obj )
// wrap extend, applying the abstract flag
obj.extend = function()
{
markFinal( arguments );
markFinal( arguments[ arguments.length - 1 ] );
return extend.apply( this, arguments );
};
}

View File

@ -177,6 +177,23 @@ function createNamedInterface( name, def )
}
/**
* Augment an exception with interface name and then throw
*
* @param {string} iname interface name or empty string
* @param {Error} e exception to augment
*/
function _ithrow( iname, e )
{
// alter the message to include our name
e.message = "Failed to define interface " +
( ( iname ) ? iname : '(anonymous)' ) + ": " + e.message
;
throw e;
}
var extend = ( function( extending )
{
return function extend()
@ -184,9 +201,10 @@ var extend = ( function( extending )
// ensure we'll be permitted to instantiate interfaces for the base
extending = true;
var args = Array.prototype.slice.call( arguments ),
props = args.pop() || {},
base = args.pop() || Interface,
var a = arguments,
an = a.length,
props = ( ( an > 0 ) ? a[ an - 1 ] : 0 ) || {},
base = ( ( an > 1 ) ? a[ an - 2 ] : 0 ) || Interface,
prototype = new base(),
iname = '',
@ -210,21 +228,25 @@ var extend = ( function( extending )
var new_interface = createInterface( iname );
try
{
util.propParse( props, {
assumeAbstract: true,
// override default exceptions from parser errors
_throw: function( e )
{
_ithrow( iname, e );
},
property: function()
{
// should never get to this point because of assumeAbstract
throw TypeError( 'Unexpected internal error' );
_ithrow( iname, TypeError( "Unexpected internal error" ) );
},
getset: function()
{
// should never get to this point because of assumeAbstract
throw TypeError( 'Unexpected internal error' );
_ithrow( iname, TypeError( "Unexpected internal error" ) );
},
method: function( name, value, is_abstract, keywords )
@ -232,9 +254,9 @@ var extend = ( function( extending )
// all members must be public
if ( keywords[ 'protected' ] || keywords[ 'private' ] )
{
throw TypeError(
iname + " member " + name + " must be public"
);
_ithrow( iname, TypeError(
"Member " + name + " must be public"
) );
}
member_builder.buildMethod(
@ -243,17 +265,6 @@ var extend = ( function( extending )
);
},
} );
}
catch ( e )
{
// alter the message to include our name
e.message = "Failed to define interface " +
( ( iname ) ? iname : '(anonymous)' ) + ": " + e.message
;
// re-throw
throw e;
}
attachExtend( new_interface );
attachStringMethod( new_interface, iname );

View File

@ -246,6 +246,19 @@ exports.copyTo = function( dest, src, deep )
};
/**
* Throw an exception
*
* Yes, this function has purpose; see where it's used.
*
* @param {Error} e exception to throw
*/
function _throw( e )
{
throw e;
}
/**
* Parses object properties to determine how they should be interpreted in an
* Object Oriented manner
@ -268,6 +281,8 @@ exports.propParse = function( data, options, context )
callbackGetSet = options.getset || fvoid,
keywordParser = options.keywordParser || propParseKeywords,
throwf = options._throw || _throw,
hasOwn = Object.prototype.hasOwnProperty,
parse_data = {},
@ -315,12 +330,12 @@ exports.propParse = function( data, options, context )
if ( !( value instanceof Array ) )
{
throw TypeError(
throwf( TypeError(
"Missing parameter list for abstract method: " + name
);
) );
}
verifyAbstractNames( name, value );
verifyAbstractNames( throwf, name, value );
value = exports.createAbstractMethod.apply( this, value );
}
@ -363,22 +378,24 @@ exports.propParse = function( data, options, context )
* In the future, we may add additional functionality, so it's important to
* restrict this as much as possible for the time being.
*
* @param {function(Error)} throwf function to call with error
*
* @param {string} name name of abstract member (for error)
* @param {Object} params parameter list to check
*
* @return {undefined}
*/
function verifyAbstractNames( name, params )
function verifyAbstractNames( throwf, name, params )
{
var i = params.length;
while ( i-- )
{
if ( params[ i ].match( /^[a-z_][a-z0-9_]*$/i ) === null )
{
throw SyntaxError(
throwf( SyntaxError(
"Member " + name + " contains invalid parameter '" +
params[ i ] + "'"
);
) );
}
}
}
@ -399,7 +416,10 @@ function verifyAbstractNames( name, params )
*/
exports.createAbstractMethod = function( def )
{
var definition = Array.prototype.slice.call( arguments );
var dfn = [],
i = arguments.length;
while ( i-- ) dfn[ i ] = arguments[ i ];
var method = function()
{
@ -407,7 +427,7 @@ exports.createAbstractMethod = function( def )
};
exports.defineSecureProp( method, 'abstractFlag', true );
exports.defineSecureProp( method, 'definition', definition );
exports.defineSecureProp( method, 'definition', dfn );
exports.defineSecureProp( method, '__length', arguments.length );
return method;
@ -562,8 +582,8 @@ exports.defineSecureProp( exports.getPropertyDescriptor, 'canTraverse',
/**
* Appropriately returns defineSecureProp implementation to avoid check on each
* invocation
* Appropriately returns defineSecureProp implementation to avoid check on
* each invocation
*
* @return {function( Object, string, * )}
*/
@ -583,8 +603,6 @@ function getDefineSecureProp()
{
// uses ECMAScript 5's Object.defineProperty() method
return function( obj, prop, value )
{
try
{
Object.defineProperty( obj, prop,
{
@ -593,17 +611,7 @@ function getDefineSecureProp()
enumerable: false,
writable: false,
configurable: false,
});
}
catch ( e )
{
// let's not have this happen again, as repeatedly throwing
// exceptions will do nothing but slow down the system
exports.definePropertyFallback( true );
// there's an error (ehem, IE8); fall back
fallback( obj, prop, value );
}
} );
};
}
}

View File

@ -89,8 +89,6 @@ exports.report = function( count, desc )
pers = ( total / count ).toFixed( 10 )
;
console.log( total + "s (x" + count + " = " + pers + "s each)" +
( ( desc ) ? ( ': ' + desc ) : '' )
);
console.log( "%s|%s|%s|%s", desc, count, pers, total );
};

View File

@ -105,3 +105,32 @@ common.test( function()
// run the same test internally
foo.testInternal();
// allows us to compare private method invocation times with method
// invocations on a conventional prototype (the increment of `i` is to
// ensure that the function is not optimized away)
( function()
{
var p = function() {};
p.prototype.foo = function() { i++ };
var o = new p();
common.test( function()
{
var i = count;
while ( i-- ) o.foo();
}, count, '[baseline] Invoke method on prototype' );
} )();
// compare with plain old function invocation
( function()
{
var f = function() { i++ };
common.test( function()
{
var i = count;
while ( i-- ) f();
}, count, '[baseline] Invoke function' );
} )();

View File

@ -0,0 +1,30 @@
#!/bin/bash
#
# Copyright (C) 2014 Mike Gerwitz
#
# This file is part of GNU ease.js.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# If you want formatted output, see :/tools/perf2html, or run this command:
# $ column -ts\| perf.out
# #
for f in "$@"; do
@NODE@ "$f" || exit $?
done \
| tee >( awk -F\| '
# format for display as the tests are running
{ printf "%s (x%s = %ss each): %s\n", $4, $2, $3, $1 }
' >&2 )

97
tools/perf2html 100755
View File

@ -0,0 +1,97 @@
#!/usr/bin/awk -f
# Renders performance test output
#
# Copyright (C) 2014 Mike Gerwitz
#
# This file is part of GNU ease.js.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# If you want more modest output, consider running this command instead:
# $ column -ts\| perf.out
# #
function styleout( i ) {
value = $i
# slow tests should reduce the number of iterations to eat up less time
if ( ( i == COL_TOTAL ) && ( value > 0.05 ) ) {
class = "slow"
if ( value > 0.1 )
class = "very " class
return "<span class=\"" class "\">" value "</span>"
}
else
return value
}
BEGIN {
# column constants
COL_DESC = 1
COL_COUNT = 2
COL_SINGLE = 3
COL_TOTAL = 4
# running time tally
runtime = 0.00
# header
print "<!DOCTYPE html>" \
"<html>" \
"<head>" \
"<title>GNU ease.js Performance Test Results</title>" \
"<style type=\"text/css\">" \
"table th:first-child { text-align: left; }" \
"table td { padding: 0.1em 0.5em; }" \
"table td:not(:first-child) { text-align: right; }" \
".slow { color: #c4a000; font-style: italic; }" \
".very.slow { color: red; }" \
"</style>" \
"</head>" \
"<body>" \
"<h1>GNU ease.js Performance Test Results</h1>" \
"<table>" \
"<thead>" \
"<tr>" \
"<th>Description</th>" \
"<th>Iterations</th>" \
"<th>Seconds/iter</th>" \
"<th>Total (s)</th>" \
"</tr>" \
"</thead>" \
"<tbody>"
}
# format row of output (desc, count, time, total)
{
runtime += $COL_TOTAL
printf "<tr>"
for ( i = 1; i <= NF; i++ )
{
printf "<td>%s</td>", styleout( i )
}
printf "</tr>\n"
}
END {
# footer
print "</tbody>" \
"</table>" \
"<p>Total running time: " runtime " seconds</p>" \
"</body>" \
"</html>"
}