diff --git a/doc/impl-details.texi b/doc/impl-details.texi index e9a19de..24e82bb 100644 --- a/doc/impl-details.texi +++ b/doc/impl-details.texi @@ -1469,6 +1469,65 @@ constructor logic and replacing methods at runtime. This is useful for mocking, but a complete anti-pattern in terms of Classical Object-Oriented development.} +@subsubsection Private Method Performance +A special exception to GNU ease.js' method wrapping implementation is made +for private methods. As mentioned above, there are a number of downsides to +method wrapping, including effectively halving the remaining stack space for +heavily recursive operations, overhead of closure invocation, and thwarting +of tail call optimization. This situation is rather awkward, because it +essentially tells users that ease.js should not be used for +performance-critical invocations or heavily recursive algorithms, which is +very inconvenient and unintuitive. + +To eliminate this issue for the bulk of program logic, method wrapping does +not occur on private methods. To see why it is not necessary, consider the +purpose of the wrappers: + +@enumerate +@item +All wrappers perform a context lookup, binding to the instance's private +visibility object of the class that defined that particular method. +@item +This context is restored upon returning from the call: if a method returns +@var{this}, it is instead converted back to the context in which the method +was invoked, which prevents the private member object from leaking out of a +public interface. +@item +In the event of an override, @var{this.__super} is set up (and torn down). +@end enumerate + +There are other details (e.g. the method wrapper used for @ref{Method +Proxies,,method proxies}), but for the sake of this particular discussion, +those are the only ones that really matter. Now, there are a couple of +important details to consider about private members: + +@itemize +@item +Private members are only ever accessible from within the context of the +private member object, which is always the context when executing a method. +@item +Private methods cannot be overridden, as they cannot be inherited. +@end itemize + +Consequently: + +@enumerate +@item +We do not need to perform a context lookup: we are already in the proper +context. +@item +We do not need to restore the context, as we never needed to change it to +begin with. +@item +@var{this.__self} is never applicable. +@end enumerate + +This is all the more motivation to use private members, which enforces +encapsulation; keep in mind that, because use of private members is the +ideal in well-encapsulated and well-factored code, ease.js has been designed +to perform best under those circumstances. + + @node Pre-ES5 Fallback @subsection Pre-ES5 Fallback For any system that is to remain functionally compatible across a number of diff --git a/doc/mkeywords.texi b/doc/mkeywords.texi index 6813c0b..9c7bbf3 100644 --- a/doc/mkeywords.texi +++ b/doc/mkeywords.texi @@ -337,6 +337,10 @@ method}, with the name @var{_moveFrontLeg}. The old method will still be called. Instead, we would have to override the public @var{walk} method to prevent our dog from moving his front feet. +Note that GNU ease.js is optimized for private member access; see +@ref{Property Proxies,,Property Proxies} and @ref{Method +Wrapping,,Method Wrapping} for additional details. + @subsection Protected Members Protected members are often misunderstood. Many developers will declare all of their members as either public or protected under the misconception that diff --git a/lib/MemberBuilder.js b/lib/MemberBuilder.js index c2419be..680cef8 100644 --- a/lib/MemberBuilder.js +++ b/lib/MemberBuilder.js @@ -173,9 +173,12 @@ exports.buildMethod = function( } } - else if ( keywords[ 'abstract' ] ) + else if ( keywords[ 'abstract' ] || keywords[ 'private' ] ) { - // we do not want to wrap abstract methods, since they are not callable + // we do not want to wrap abstract methods, since they are not + // callable; further, we do not need to wrap private methods, since + // they are only ever accessible when we are already within a + // private context (see test case for more information) dest[ name ] = value; } else diff --git a/test/MemberBuilder/MethodTest.js b/test/MemberBuilder/MethodTest.js index d2aa8ee..82bc337 100644 --- a/test/MemberBuilder/MethodTest.js +++ b/test/MemberBuilder/MethodTest.js @@ -67,7 +67,11 @@ require( 'common' ).testCase( // stub factories used for testing var stubFactory = this.require( 'MethodWrapperFactory' )( - function( func ) { return func; } + function( func ) + { + // still wrap it so that the function is encapsulated + return function() { return func() }; + } ); // used for testing proxies explicitly @@ -282,4 +286,45 @@ require( 'common' ).testCase( this.members[ 'public' ].foo(); this.assertOk( called, "Override unkept" ); }, + + + /** + * This is a beautiful consequence of the necessay context in which + * private methods must be invoked. + * + * If a method has the ability to call a private method, then we must + * already be within a private context (that is---using the private + * member object, which happens whenever we're executing a method of + * that class). The purpose of the method wrapper is to (a) determine + * the proper context, (b) set up super method references, and (c) + * restore the context in the event that the method returns `this'. Not + * a single one of these applies: (a) is void beacuse we are already in + * the proper context; (b) is not applicable since private methods + * cannot have a super method; and (c) we do not need to restore context + * before returning because the context would be the same (per (a)). + * + * That has excellent performance implications: not only do we reduce + * class building times for private methods, but we also improve method + * invocation times, since we do not have to invoke a *closure* for each + * and every method call. Further, recursive private methods are no + * longer an issue since they do not gobble up the stack faster and, + * consequently, the JavaScript engine can now take advantage of tail + * call optimizations. + * + * This is also further encouragement to use private members. :) + */ + 'Private methods are not wrapped': function() + { + var f = function() {}, + name = 'foo'; + + this.sut.buildMethod( + this.members, {}, name, f, { 'private': true }, + function() {}, 1, {} + ); + + // if the private method was not wrapped, then it should have been + // assigned to the member object unencapsulated + this.assertStrictEqual( this.members[ 'private' ][ name ], f ); + }, } );