diff --git a/.gitignore b/.gitignore index f0d5048..7404829 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Makefile.am b/Makefile.am index 752c710..3a4f291 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 diff --git a/configure.ac b/configure.ac index 3bc5bb4..783e0a4 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/lib/ClassBuilder.js b/lib/ClassBuilder.js index bd2e270..caae2a8 100644 --- a/lib/ClassBuilder.js +++ b/lib/ClassBuilder.js @@ -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 ( ( typeof type === 'function' ) + && ( instance instanceof type ) + ) { - // check prototype chain (will throw an error if type is not a - // constructor (function) - if ( instance instanceof type ) - { - return true; - } + 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 diff --git a/lib/class.js b/lib/class.js index 1c76898..65f8a41 100644 --- a/lib/class.js +++ b/lib/class.js @@ -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 ); } ); } diff --git a/lib/class_abstract.js b/lib/class_abstract.js index 0441904..deb920d 100644 --- a/lib/class_abstract.js +++ b/lib/class_abstract.js @@ -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 ); }; diff --git a/lib/class_final.js b/lib/class_final.js index e00dec6..c7fabd1 100644 --- a/lib/class_final.js +++ b/lib/class_final.js @@ -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 ); }; } diff --git a/lib/interface.js b/lib/interface.js index 474c568..a3b2227 100644 --- a/lib/interface.js +++ b/lib/interface.js @@ -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,50 +228,43 @@ var extend = ( function( extending ) var new_interface = createInterface( iname ); - try - { - util.propParse( props, { - assumeAbstract: true, + util.propParse( props, { + assumeAbstract: true, - property: function() + // override default exceptions from parser errors + _throw: function( e ) + { + _ithrow( iname, e ); + }, + + property: function() + { + // should never get to this point because of assumeAbstract + _ithrow( iname, TypeError( "Unexpected internal error" ) ); + }, + + getset: function() + { + // should never get to this point because of assumeAbstract + _ithrow( iname, TypeError( "Unexpected internal error" ) ); + }, + + method: function( name, value, is_abstract, keywords ) + { + // all members must be public + if ( keywords[ 'protected' ] || keywords[ 'private' ] ) { - // should never get to this point because of assumeAbstract - throw TypeError( 'Unexpected internal error' ); - }, + _ithrow( iname, TypeError( + "Member " + name + " must be public" + ) ); + } - getset: function() - { - // should never get to this point because of assumeAbstract - throw TypeError( 'Unexpected internal error' ); - }, - - method: function( name, value, is_abstract, keywords ) - { - // all members must be public - if ( keywords[ 'protected' ] || keywords[ 'private' ] ) - { - throw TypeError( - iname + " member " + name + " must be public" - ); - } - - member_builder.buildMethod( - members, null, name, value, keywords, - null, 0, {}, vstate - ); - }, - } ); - } - catch ( e ) - { - // alter the message to include our name - e.message = "Failed to define interface " + - ( ( iname ) ? iname : '(anonymous)' ) + ": " + e.message - ; - - // re-throw - throw e; - } + member_builder.buildMethod( + members, null, name, value, keywords, + null, 0, {}, vstate + ); + }, + } ); attachExtend( new_interface ); attachStringMethod( new_interface, iname ); diff --git a/lib/util.js b/lib/util.js index 8c3cf00..6c35ba7 100644 --- a/lib/util.js +++ b/lib/util.js @@ -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, * )} */ @@ -584,26 +604,14 @@ function getDefineSecureProp() // uses ECMAScript 5's Object.defineProperty() method return function( obj, prop, value ) { - try + Object.defineProperty( obj, prop, { - Object.defineProperty( obj, prop, - { - value: value, + value: value, - 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 ); - } + enumerable: false, + writable: false, + configurable: false, + } ); }; } } diff --git a/test/perf/common.js b/test/perf/common.js index 324f05d..13c1d46 100644 --- a/test/perf/common.js +++ b/test/perf/common.js @@ -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 ); }; diff --git a/test/perf/perf-class-invoke-method.js b/test/perf/perf-class-invoke-method.js index bb4f24a..01f185e 100644 --- a/test/perf/perf-class-invoke-method.js +++ b/test/perf/perf-class-invoke-method.js @@ -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' ); +} )(); + diff --git a/test/perf/runner.in b/test/perf/runner.in new file mode 100755 index 0000000..ce046e2 --- /dev/null +++ b/test/perf/runner.in @@ -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 . +# +# 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 ) diff --git a/tools/perf2html b/tools/perf2html new file mode 100755 index 0000000..842bb72 --- /dev/null +++ b/tools/perf2html @@ -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 . +# +# 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 "" value "" + } + 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 "" \ + "" \ + "" \ + "GNU ease.js Performance Test Results" \ + "" \ + "" \ + "" \ + "

GNU ease.js Performance Test Results

" \ + "" \ + "" \ + "" \ + "" \ + "" \ + "" \ + "" \ + "" \ + "" \ + "" +} + +# format row of output (desc, count, time, total) +{ + runtime += $COL_TOTAL + + printf "" + for ( i = 1; i <= NF; i++ ) + { + printf "", styleout( i ) + } + printf "\n" +} + +END { + # footer + print "" \ + "
DescriptionIterationsSeconds/iterTotal (s)
%s
" \ + "

Total running time: " runtime " seconds

" \ + "" \ + "" +}