From 7842d65b8331c0dccc36dc2d5ff74d1c0923d808 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 31 Jul 2014 11:55:15 +0100 Subject: [PATCH 01/83] feat($compile/ngBind): don't add binding info if not enabled The compiler and ngBind directives add binding information (`ng-binding` CSS class and `$binding` data object) to elements when they are bound to the scope. This is only to aid testing and debugging for tools such as Protractor and Batarang. In production this is unneccesary and add a performance penalty. This change disables adding this binding information by default. You can enable it by calling `$compilerProvider.enableBindingInfo()` in a module `config()` block. BREAKING CHANGE: The `ng-binding` CSS class and the `$binding` data object are no longer attached to DOM elements that have a binding to the scope, by default. If you relied on this feature then you can re-enable it in one of your application modules: ``` someModule.config(['$compileProvider', function($compileProvider) { $compileProvider.enableBindingInfo(); }]); ``` --- src/ng/compile.js | 36 +++++++++++++++++++++---------- src/ng/directive/ngBind.js | 38 ++++++++++++++++----------------- test/ng/compileSpec.js | 28 ++++++++++++++++++------ test/ng/directive/ngBindSpec.js | 10 +++------ test/ngScenario/dslSpec.js | 4 ++++ 5 files changed, 73 insertions(+), 43 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 5a8103d7f024..18e0e1104495 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -657,6 +657,28 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }; + /** + * @ngdoc method + * @name $compileProvider#enableBindingInfo + * @kind function + * + * @description + * Call this method to enable adding binding information to DOM elements at runtime. + * If enabled, the compiler will add the following to DOM elements that have been bound to the scope + * * `ng-binding` CSS class + * * `$binding` data object containing the value of the binding (or an array if there are multiple + * bindings) + */ + var addBindingInfo = noop; + this.enableBindingInfo = function() { + addBindingInfo = function(element, binding, appendBinding) { + if ( appendBinding ) { + binding = (element.data('$binding') || []).concat([binding]); + } + element.addClass('ng-binding').data('$binding', binding); + }; + }; + this.$get = [ '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', @@ -846,7 +868,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }, NG_ATTR_BINDING = /^ngAttr[A-Z]/; - + compile.addBindingInfo = addBindingInfo; return compile; //================================ @@ -1879,17 +1901,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directives.push({ priority: 0, compile: function textInterpolateCompileFn(templateNode) { - // when transcluding a template that has bindings in the root - // then we don't have a parent and should do this in the linkFn - var parent = templateNode.parent(), hasCompileParent = parent.length; - if (hasCompileParent) safeAddClass(templateNode.parent(), 'ng-binding'); - return function textInterpolateLinkFn(scope, node) { - var parent = node.parent(), - bindings = parent.data('$binding') || []; - bindings.push(interpolateFn); - parent.data('$binding', bindings); - if (!hasCompileParent) safeAddClass(parent, 'ng-binding'); + var parent = node.parent(); + addBindingInfo(parent, interpolateFn, true); scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { node[0].nodeValue = value; }); diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js index a0cf67665d9a..1ee7f2578190 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -51,20 +51,21 @@ */ -var ngBindDirective = ngDirective({ - compile: function(templateElement) { - templateElement.addClass('ng-binding'); - return function (scope, element, attr) { - element.data('$binding', attr.ngBind); - scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - // We are purposefully using == here rather than === because we want to - // catch when value is "null or undefined" - // jshint -W041 - element.text(value == undefined ? '' : value); - }); - }; - } -}); +var ngBindDirective = ['$compile', function($compile) { + return { + compile: function(templateElement) { + return function (scope, element, attr) { + $compile.addBindingInfo(element, attr.ngBind); + scope.$watch(attr.ngBind, function ngBindWatchAction(value) { + // We are purposefully using == here rather than === because we want to + // catch when value is "null or undefined" + // jshint -W041 + element.text(value == undefined ? '' : value); + }); + }; + } + }; +}]; /** @@ -118,11 +119,11 @@ var ngBindDirective = ngDirective({ */ -var ngBindTemplateDirective = ['$interpolate', function($interpolate) { +var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) { return function(scope, element, attr) { // TODO: move this to scenario runner var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); - element.addClass('ng-binding').data('$binding', interpolateFn); + $compile.addBindingInfo(element, interpolateFn); attr.$observe('ngBindTemplate', function(value) { element.text(value); }); @@ -176,14 +177,13 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { */ -var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) { +var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) { return { restrict: 'A', compile: function (tElement, tAttrs) { - tElement.addClass('ng-binding'); return function (scope, element, attr) { - element.data('$binding', attr.ngBindHtml); + $compile.addBindingInfo(element, attr.ngBindHtml); var parsed = $parse(attr.ngBindHtml); var changeDetector = $parse(attr.ngBindHtml, function getStringValue(value) { return (value || '').toString(); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index d2f1aec74b5a..1822d9046a9d 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -2311,13 +2311,29 @@ describe('$compile', function() { })); }); - it('should decorate the binding with ng-binding and interpolation function', inject( - function($compile, $rootScope) { - element = $compile('
{{1+2}}
')($rootScope); - expect(element.hasClass('ng-binding')).toBe(true); - expect(element.data('$binding')[0].exp).toEqual('{{1+2}}'); - })); + describe("decorating with binding info", function() { + + it('should not occur if `enableBindingInfo` has not been called', inject(function($compile, $rootScope) { + element = $compile('
{{1+2}}
')($rootScope); + expect(element.hasClass('ng-binding')).toBe(false); + expect(element.data('$binding')).toBeUndefined(); + })); + + + it('should occur if `enableBindingInfo` has been called', function() { + module(function($compileProvider) { + $compileProvider.enableBindingInfo(); + }); + + inject( + function($compile, $rootScope) { + element = $compile('
{{1+2}}
')($rootScope); + expect(element.hasClass('ng-binding')).toBe(true); + expect(element.data('$binding')[0].exp).toEqual('{{1+2}}'); + }); + }); + }); it('should observe interpolated attrs', inject(function($rootScope, $compile) { $compile('
')($rootScope); diff --git a/test/ng/directive/ngBindSpec.js b/test/ng/directive/ngBindSpec.js index 704932b09085..0c410c46f8e2 100644 --- a/test/ng/directive/ngBindSpec.js +++ b/test/ng/directive/ngBindSpec.js @@ -3,6 +3,9 @@ describe('ngBind*', function() { var element; + beforeEach(module(function($compileProvider) { + $compileProvider.enableBindingInfo(); + })); afterEach(function() { dealoc(element); @@ -122,13 +125,6 @@ describe('ngBind*', function() { describe('ngBindHtml', function() { - it('should add ng-binding class to the element in compile phase', inject(function($compile) { - var element = jqLite('
'); - $compile(element); - expect(element.hasClass('ng-binding')).toBe(true); - })); - - describe('SCE disabled', function() { beforeEach(function() { module(function($sceProvider) { $sceProvider.enabled(false); }); diff --git a/test/ngScenario/dslSpec.js b/test/ngScenario/dslSpec.js index 80d4165bd9ef..1a9bb6cac640 100644 --- a/test/ngScenario/dslSpec.js +++ b/test/ngScenario/dslSpec.js @@ -9,6 +9,10 @@ describe("angular.scenario.dsl", function() { dealoc(element); }); + beforeEach(module(function($compileProvider) { + $compileProvider.enableBindingInfo(); + })); + beforeEach(module('ngSanitize')); beforeEach(inject(function($injector) { From fee38c023d284362d284cf6b47f6c5eb685273bf Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 31 Jul 2014 16:32:28 +0100 Subject: [PATCH 02/83] docs(protractorTests): inject a mock module to enable bindingInfo during e2e tests --- docs/config/templates/protractorTests.template.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/config/templates/protractorTests.template.js b/docs/config/templates/protractorTests.template.js index 1801486501a2..47e8a51dd763 100644 --- a/docs/config/templates/protractorTests.template.js +++ b/docs/config/templates/protractorTests.template.js @@ -1,5 +1,13 @@ describe("{$ doc.description $}", function() { beforeEach(function() { + + browser.addMockModule('enable-binding-info', function() { + angular.module('enable-binding-info', []) + .config(['$compileProvider', function($compileProvider) { + $compileProvider.enableBindingInfo(); + }]); + }); + browser.get("{$ doc.pathPrefix $}/{$ doc.examplePath $}"); }); From 6b5eef830a88468ba29ed719cbd4d6c31d676f61 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Aug 2014 20:05:55 -0700 Subject: [PATCH 03/83] perf(jqLite): don't recreate the Node.contains polyfill --- src/jqLite.js | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index 0db4c25103f4..e1dd278c2ac7 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -155,6 +155,30 @@ wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; + +var elementContains = document.head.contains || + document.head.compareDocumentPosition + ? function( a, b ) { + // jshint bitwise: false + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } + : function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + function jqLiteIsTextNode(html) { return !HTML_REGEXP.test(html); } @@ -775,28 +799,6 @@ forEach({ if (!eventFns) { if (type == 'mouseenter' || type == 'mouseleave') { - var contains = document.body.contains || document.body.compareDocumentPosition ? - function( a, b ) { - // jshint bitwise: false - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - events[type] = []; // Refer to jQuery's implementation of mouseenter & mouseleave @@ -808,7 +810,7 @@ forEach({ var target = this, related = event.relatedTarget; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !contains(target, related)) ){ + if ( !related || (related !== target && !elementContains(target, related)) ){ handle(event, type); } }); From 82905335423d4d181a3fe8ad914094d96cef8080 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Aug 2014 20:07:48 -0700 Subject: [PATCH 04/83] chore(jqLite): drop Node.contains polyfill Node.contains is supported on IE5+ and all the other browsers we care about. --- src/jqLite.js | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index e1dd278c2ac7..a056738f2493 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -156,29 +156,6 @@ wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.the wrapMap.th = wrapMap.td; -var elementContains = document.head.contains || - document.head.compareDocumentPosition - ? function( a, b ) { - // jshint bitwise: false - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } - : function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - function jqLiteIsTextNode(html) { return !HTML_REGEXP.test(html); } @@ -810,7 +787,7 @@ forEach({ var target = this, related = event.relatedTarget; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !elementContains(target, related)) ){ + if ( !related || (related !== target && !target.contains(related)) ){ handle(event, type); } }); From 996d160a3fb207149c41a65a3af6530ef8382489 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Aug 2014 20:13:15 -0700 Subject: [PATCH 05/83] refactor(jqLite): don't recreate mouse event map --- src/jqLite.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index a056738f2493..f514acd235de 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -121,6 +121,7 @@ function jqNextId() { return ++jqId; } var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; var MOZ_HACK_REGEXP = /^moz([A-Z])/; +var MOUSE_EVENT_MAP= { mouseleave : "mouseout", mouseenter : "mouseover"}; var jqLiteMinErr = minErr('jqLite'); /** @@ -781,9 +782,8 @@ forEach({ // Refer to jQuery's implementation of mouseenter & mouseleave // Read about mouseenter and mouseleave: // http://www.quirksmode.org/js/events_mouse.html#link8 - var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; - onFn(element, eventmap[type], function(event) { + onFn(element, MOUSE_EVENT_MAP[type], function(event) { var target = this, related = event.relatedTarget; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window From a7e5d25bf15b6ffe7956c39f7a8ebf4d6b461162 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Aug 2014 20:14:15 -0700 Subject: [PATCH 06/83] refactor(jqLite): remove code duplication --- src/jqLite.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index f514acd235de..c014b8c5ecae 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -776,9 +776,9 @@ forEach({ var eventFns = events[type]; if (!eventFns) { - if (type == 'mouseenter' || type == 'mouseleave') { - events[type] = []; + events[type] = []; + if (type == 'mouseenter' || type == 'mouseleave') { // Refer to jQuery's implementation of mouseenter & mouseleave // Read about mouseenter and mouseleave: // http://www.quirksmode.org/js/events_mouse.html#link8 @@ -794,7 +794,6 @@ forEach({ } else { addEventListenerFn(element, type, handle); - events[type] = []; } eventFns = events[type]; } From d0130c46fc65447cd6e3a4ab7ab91542d1a700ff Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Aug 2014 20:16:06 -0700 Subject: [PATCH 07/83] perf(jqLite): don't use forEach in #off off() is called on each element removal, so we want to make it as fast as possible --- src/jqLite.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index c014b8c5ecae..f07510197f61 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -261,16 +261,21 @@ function jqLiteDealoc(element, onlyDescendants){ function jqLiteOff(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); + var events = jqLiteExpandoStore(element, 'events'); + var handle = jqLiteExpandoStore(element, 'handle'); + var i; + var types; if (!handle) return; //no listeners registered if (isUndefined(type)) { - forEach(events, function(eventHandler, type) { - removeEventListenerFn(element, type, eventHandler); + types = Object.keys(events); + i = types.length; + while (i--) { + type = types[i]; + removeEventListenerFn(element, type, events[type]); delete events[type]; - }); + } } else { forEach(type.split(' '), function(type) { if (isUndefined(fn)) { From 694dcb8585bb31c705d03f266944cece1ffc18ed Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Aug 2014 21:01:41 -0700 Subject: [PATCH 08/83] perf(jqLite): improve createEventHandler method by switching from forEach to for loop --- src/jqLite.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index f07510197f61..e86b22c7437b 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -733,9 +733,9 @@ function createEventHandler(element, events) { // Copy event handlers in case event handlers array is modified during execution. var eventHandlersCopy = shallowCopy(events[type || event.type] || []); - forEach(eventHandlersCopy, function(fn) { - fn.call(element, event); - }); + for (var i = 0, ii = eventHandlersCopy.length; i < ii; i++) { + eventHandlersCopy[i].call(element, event); + } // Remove monkey-patched methods (IE), // as they would cause memory leaks in IE8. From 7c5a1b0ead580f2696bb1f90e07f9a1cf5d5be74 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Aug 2014 21:02:55 -0700 Subject: [PATCH 09/83] perf($compile): optimize publicLinkFn --- src/ng/compile.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 18e0e1104495..ca390e2f6615 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -899,9 +899,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!! : $compileNodes; - forEach(transcludeControllers, function(instance, name) { - $linkNode.data('$' + name + 'Controller', instance); - }); + if (transcludeControllers) { + var names = Object.keys(transcludeControllers); + var i = names.length; + var name; + + while (i--) { + name = names[i]; + $linkNode.data('$' + name + 'Controller', transcludeControllers[name]); + } + } $linkNode.data('$scope', scope); From 447c4daf82c82cd40c71fe3e61a53a20c5eb11f6 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Aug 2014 21:39:17 -0700 Subject: [PATCH 10/83] perf(jqLite): optimize event listener registration --- src/jqLite.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index e86b22c7437b..2a8c23ba25a7 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -777,13 +777,17 @@ forEach({ if (!events) jqLiteExpandoStore(element, 'events', events = {}); if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); - forEach(type.split(' '), function(type){ + var types = type.split(' '); + var i = types.length; + + while (i--) { + type = types[i]; var eventFns = events[type]; if (!eventFns) { events[type] = []; - if (type == 'mouseenter' || type == 'mouseleave') { + if (type === 'mouseenter' || type === 'mouseleave') { // Refer to jQuery's implementation of mouseenter & mouseleave // Read about mouseenter and mouseleave: // http://www.quirksmode.org/js/events_mouse.html#link8 @@ -803,7 +807,7 @@ forEach({ eventFns = events[type]; } eventFns.push(fn); - }); + } }, off: jqLiteOff, From 67ec72f182790d89adbee2183869bae72c1e3bac Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Aug 2014 22:23:55 -0700 Subject: [PATCH 11/83] chore(jqLite): remove legacy code to support IE8 and older --- src/jqLite.js | 111 ++++++++++---------------------------------------- 1 file changed, 21 insertions(+), 90 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index 2a8c23ba25a7..db5aeca029b8 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -101,17 +101,17 @@ JQLite.expando = 'ng339'; var jqCache = JQLite.cache = {}, jqId = 1, - addEventListenerFn = (window.document.addEventListener - ? function(element, type, fn) {element.addEventListener(type, fn, false);} - : function(element, type, fn) {element.attachEvent('on' + type, fn);}), - removeEventListenerFn = (window.document.removeEventListener - ? function(element, type, fn) {element.removeEventListener(type, fn, false); } - : function(element, type, fn) {element.detachEvent('on' + type, fn); }); + addEventListenerFn = function(element, type, fn) { + element.addEventListener(type, fn, false) + }, + removeEventListenerFn = function(element, type, fn) { + element.removeEventListener(type, fn, false); + }; /* * !!! This is an undocumented "private" function !!! */ -var jqData = JQLite._data = function(node) { +JQLite._data = function(node) { //jQuery always returns an object on cache miss return this.cache[node[this.expando]] || {}; }; @@ -168,7 +168,7 @@ function jqLiteAcceptsData(node) { } function jqLiteBuildFragment(html, context) { - var elem, tmp, tag, wrap, + var tmp, tag, wrap, fragment = context.createDocumentFragment(), nodes = [], i; @@ -318,7 +318,7 @@ function jqLiteExpandoStore(element, key, value) { } expandoStore[key] = value; } else { - return expandoStore && expandoStore[key]; + return key ? expandoStore && expandoStore[key] : expandoStore; } } @@ -449,23 +449,11 @@ function jqLiteEmpty(element) { ////////////////////////////////////////// var JQLitePrototype = JQLite.prototype = { ready: function(fn) { - var fired = false; - - function trigger() { - if (fired) return; - fired = true; - fn(); - } - // check if document already is loaded if (document.readyState === 'complete'){ - setTimeout(trigger); + setTimeout(fn); } else { - this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - // jshint -W064 - JQLite(window).on('load', trigger); // fallback to window.onload for others - // jshint +W064 + this.on('DOMContentLoaded', fn); } }, toString: function() { @@ -555,22 +543,7 @@ forEach({ if (isDefined(value)) { element.style[name] = value; } else { - var val; - - if (msie <= 8) { - // this is some IE specific weirdness that jQuery 1.6.4 does not sure why - val = element.currentStyle && element.currentStyle[name]; - if (val === '') val = 'auto'; - } - - val = val || element.style[name]; - - if (msie <= 8) { - // jquery weirdness :-/ - val = (val === '') ? undefined : val; - } - - return val; + return element.style[name]; } }, @@ -701,33 +674,10 @@ forEach({ function createEventHandler(element, events) { var eventHandler = function (event, type) { - if (!event.preventDefault) { - event.preventDefault = function() { - event.returnValue = false; //ie - }; - } - - if (!event.stopPropagation) { - event.stopPropagation = function() { - event.cancelBubble = true; //ie - }; - } - - if (!event.target) { - event.target = event.srcElement || document; - } - - if (isUndefined(event.defaultPrevented)) { - var prevent = event.preventDefault; - event.preventDefault = function() { - event.defaultPrevented = true; - prevent.call(event); - }; - event.defaultPrevented = false; - } + // jQuery specific api event.isDefaultPrevented = function() { - return event.defaultPrevented || event.returnValue === false; + return event.defaultPrevented; }; // Copy event handlers in case event handlers array is modified during execution. @@ -736,21 +686,10 @@ function createEventHandler(element, events) { for (var i = 0, ii = eventHandlersCopy.length; i < ii; i++) { eventHandlersCopy[i].call(element, event); } - - // Remove monkey-patched methods (IE), - // as they would cause memory leaks in IE8. - if (msie <= 8) { - // IE7/8 does not allow to delete property on native object - event.preventDefault = null; - event.stopPropagation = null; - event.isDefaultPrevented = null; - } else { - // It shouldn't affect normal browsers (native methods are defined on prototype). - delete event.preventDefault; - delete event.stopPropagation; - delete event.isDefaultPrevented; - } }; + + // TODO: this is a hack for angularMocks/clearDataCache that makes it possible to deregister all + // events on `element` eventHandler.elem = element; return eventHandler; } @@ -771,8 +710,9 @@ forEach({ return; } - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var handle = expandoStore && expandoStore.handle; if (!events) jqLiteExpandoStore(element, 'events', events = {}); if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); @@ -912,16 +852,7 @@ forEach({ }, next: function(element) { - if (element.nextElementSibling) { - return element.nextElementSibling; - } - - // IE8 doesn't have nextElementSibling - var elm = element.nextSibling; - while (elm != null && elm.nodeType !== 1) { - elm = elm.nextSibling; - } - return elm; + return element.nextElementSibling; }, find: function(element, selector) { From ffb6f9af23679a96cb98745ce2b544a6a2a14d24 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Aug 2014 23:32:03 -0700 Subject: [PATCH 12/83] perf(jqLite): don't register DOM listener for $destroy event This even is fired purely within jqLite/jQuery so it doesn't make sense to register DOM listener here. 6% improvement in large table benchmark for both creation and destruction --- src/jqLite.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index db5aeca029b8..fe5339877736 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -273,7 +273,9 @@ function jqLiteOff(element, type, fn, unsupported) { i = types.length; while (i--) { type = types[i]; - removeEventListenerFn(element, type, events[type]); + if (type !== '$destroy') { + removeEventListenerFn(element, type, events[type]); + } delete events[type]; } } else { @@ -742,7 +744,9 @@ forEach({ }); } else { - addEventListenerFn(element, type, handle); + if (type !== '$destroy') { + addEventListenerFn(element, type, handle); + } } eventFns = events[type]; } From ea0aba71746345d92390d60ab808a2e804c5c958 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 5 Aug 2014 01:07:42 -0700 Subject: [PATCH 13/83] perf(jqLite): optimize append() and after() --- src/jqLite.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index fe5339877736..8ef54249f9ae 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -261,8 +261,9 @@ function jqLiteDealoc(element, onlyDescendants){ function jqLiteOff(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); - var events = jqLiteExpandoStore(element, 'events'); - var handle = jqLiteExpandoStore(element, 'handle'); + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var handle = expandoStore && expandoStore.handle; var i; var types; @@ -796,11 +797,15 @@ forEach({ }, append: function(element, node) { - forEach(new JQLite(node), function(child){ - if (element.nodeType === 1 || element.nodeType === 11) { - element.appendChild(child); - } - }); + var nodeType = element.nodeType; + if (nodeType !== 1 && nodeType !== 11) return; + + node = new JQLite(node); + + for (var i = 0, ii = node.length; i < ii; i++) { + var child = node[i]; + element.appendChild(child); + } }, prepend: function(element, node) { @@ -829,10 +834,13 @@ forEach({ after: function(element, newElement) { var index = element, parent = element.parentNode; - forEach(new JQLite(newElement), function(node){ + newElement = new JQLite(newElement); + + for (var i = 0, ii = newElement.length; i < ii; i++) { + var node = newElement[i]; parent.insertBefore(node, index.nextSibling); index = node; - }); + } }, addClass: jqLiteAddClass, From 94357354d1e2201b2a8fda7eb1c096aad05ef8d9 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 5 Aug 2014 01:09:37 -0700 Subject: [PATCH 14/83] perf($compile): optimize nodeLinkFn Functions with try/catch block can't be optimized, so we can move the try/catch block into a tiny fn and make it possible for the complex nodeLinkFn to get optimized. --- src/ng/compile.js | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index ca390e2f6615..2e6b52585d76 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1611,13 +1611,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // PRELINKING for(i = 0, ii = preLinkFns.length; i < ii; i++) { - try { - linkFn = preLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } + linkFn = preLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); } // RECURSION @@ -1631,13 +1632,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // POSTLINKING for(i = postLinkFns.length - 1; i >= 0; i--) { - try { - linkFn = postLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } + linkFn = postLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); } // This is the function that is injected as `$transclude`. @@ -2068,6 +2070,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function cloneAndAnnotateFn(fn, annotation) { return extend(function() { return fn.apply(null, arguments); }, fn, annotation); } + + + function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) { + try { + linkFn(scope, $element, attrs, controllers, transcludeFn); + } catch(e) { + $exceptionHandler(e, startingTag($element)); + } + } }]; } From 05bdd1d0a8c57c9e6391b13c3769b40dd2ead636 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Wed, 6 Aug 2014 14:55:25 -0700 Subject: [PATCH 15/83] WIP: add bp --- perf/apps/largetable-bp/app.js | 177 +++ perf/apps/largetable-bp/bootstrap.min.css | 7 + perf/apps/largetable-bp/bp.js | 308 +++++ perf/apps/largetable-bp/bp.spec.js | 429 +++++++ perf/apps/largetable-bp/index.html | 184 +++ perf/apps/largetable-bp/launch_chrome.sh | 8 + perf/apps/largetable-bp/underscore.js | 1343 +++++++++++++++++++++ 7 files changed, 2456 insertions(+) create mode 100755 perf/apps/largetable-bp/app.js create mode 100644 perf/apps/largetable-bp/bootstrap.min.css create mode 100644 perf/apps/largetable-bp/bp.js create mode 100644 perf/apps/largetable-bp/bp.spec.js create mode 100644 perf/apps/largetable-bp/index.html create mode 100755 perf/apps/largetable-bp/launch_chrome.sh create mode 100644 perf/apps/largetable-bp/underscore.js diff --git a/perf/apps/largetable-bp/app.js b/perf/apps/largetable-bp/app.js new file mode 100755 index 000000000000..5e9f7e9f7dfa --- /dev/null +++ b/perf/apps/largetable-bp/app.js @@ -0,0 +1,177 @@ +var app = angular.module('largetableBenchmark', []); + +app.filter('noop', function() { + return function(input) { + return input; + }; +}); + +app.controller('DataController', function($scope, $rootScope) { + var totalRows = 1000; + var totalColumns = 20; + + var data = $scope.data = []; + $scope.digestDuration = '?'; + $scope.numberOfBindings = totalRows*totalColumns*2 + totalRows + 1; + $scope.numberOfWatches = '?'; + + function iGetter() { return this.i; } + function jGetter() { return this.j; } + + for (var i=0; i.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#999}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;white-space:nowrap;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}input[type=date]{line-height:34px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.has-feedback .form-control-feedback{position:absolute;top:25px;right:0;display:block;width:34px;height:34px;line-height:34px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-control-static{padding-top:7px}@media (min-width:768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:400;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#428bca}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:4px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{float:none;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:gray}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;color:#fff;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.42857143px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.5) 0),color-stop(rgba(0,0,0,.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.0001) 0),color-stop(rgba(0,0,0,.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/perf/apps/largetable-bp/bp.js b/perf/apps/largetable-bp/bp.js new file mode 100644 index 000000000000..66663ba4a72c --- /dev/null +++ b/perf/apps/largetable-bp/bp.js @@ -0,0 +1,308 @@ +var bp = window.bp = { + steps: window.benchmarkSteps = [], + Statistics: { + //Taken from z-table where confidence is 95% + criticalValue: 1.96 + }, + Runner: { + runState: { + iterations: 0, + numSamples: 20, + recentResult: {} + } + }, + Document: {}, + Report: { + timesPerAction: {} + }, + Measure: { + characteristics: ['gcTime','testTime','garbageCount','retainedCount'] + } +}; + +bp.Measure.numMilliseconds = function() { + if (window.performance != null && typeof window.performance.now == 'function') { + return window.performance.now(); + } else if (window.performance != null && typeof window.performance.webkitNow == 'function') { + return window.performance.webkitNow(); + } else { + console.log('using Date.now'); + return Date.now(); + } +}; + +bp.Statistics.getMean = function (sample) { + var total = 0; + sample.forEach(function(x) { total += x; }); + return total / sample.length; +}; + +bp.Statistics.calculateConfidenceInterval = function(standardDeviation, sampleSize) { + var standardError = standardDeviation / Math.sqrt(sampleSize); + return bp.Statistics.criticalValue * standardError; +}; + +bp.Statistics.calculateRelativeMarginOfError = function(marginOfError, mean) { + /* + * Converts absolute margin of error to a relative margin of error by + * converting it to a percentage of the mean. + */ + return (marginOfError / mean); +}; + +bp.Statistics.calculateCoefficientOfVariation = function(standardDeviation, mean) { + return standardDeviation / mean; +}; + +bp.Statistics.calculateStandardDeviation = function(sample, mean) { + var deviation = 0; + sample.forEach(function(x) { + deviation += Math.pow(x - mean, 2); + }); + deviation = deviation / (sample.length -1); + deviation = Math.sqrt(deviation); + return deviation; +}; + +bp.Runner.setIterations = function (iterations) { + bp.Runner.runState.iterations = iterations; +}; + +bp.Runner.resetIterations = function() { + bp.Runner.runState.iterations = 0; +}; + +bp.Runner.loopBenchmark = function () { + if (bp.Runner.runState.iterations <= -1) { + //Time to stop looping + bp.Runner.setIterations(0); + bp.Document.loopBtn.innerText = 'Loop'; + return; + } + bp.Runner.setIterations(-1); + bp.Document.loopBtn.innerText = 'Pause'; + bp.Runner.runAllTests(); +}; + +bp.Runner.onceBenchmark = function() { + bp.Runner.setIterations(1); + bp.Document.onceBtn.innerText = '...'; + bp.Runner.runAllTests(function() { + bp.Document.onceBtn.innerText = 'Once'; + }); +}; + +bp.Runner.twentyFiveBenchmark = function() { + var twentyFiveBtn = bp.Document.twentyFiveBtn; + bp.Runner.setIterations(25); + twentyFiveBtn.innerText = 'Looping...'; + bp.Runner.runAllTests(function() { + twentyFiveBtn.innerText = 'Loop 25x'; + }, 5); +}; + +bp.Runner.runAllTests = function (done) { + if (bp.Runner.runState.iterations--) { + bp.steps.forEach(function(bs) { + var testResults = bp.Runner.runTimedTest(bs); + bp.Runner.runState.recentResult[bs.name] = testResults; + }); + bp.Report.markup = bp.Report.calcStats(); + bp.Document.writeReport(bp.Report.markup); + window.requestAnimationFrame(function() { + bp.Runner.runAllTests(done); + }); + } + else { + bp.Document.writeReport(bp.Report.markup); + bp.Runner.resetIterations(); + done && done(); + } +}; + +bp.Runner.runTimedTest = function (bs) { + var startTime, + endTime, + startGCTime, + endGCTime, + retainedMemory, + garbage, + beforeHeap, + afterHeap, + finalHeap; + if (typeof window.gc === 'function') { + window.gc(); + } + + if (window.performance && window.performance.memory) { + beforeHeap = performance.memory.usedJSHeapSize; + } + + startTime = bp.Measure.numMilliseconds(); + bs.fn(); + endTime = bp.Measure.numMilliseconds() - startTime; + + if (window.performance && window.performance.memory) { + afterHeap = performance.memory.usedJSHeapSize; + } + + startGCTime = bp.Measure.numMilliseconds(); + if (typeof window.gc === 'function') { + window.gc(); + } + endGCTime = bp.Measure.numMilliseconds() - startGCTime; + + if (window.performance && window.performance.memory) { + finalHeap = performance.memory.usedJSHeapSize; + garbage = Math.abs(finalHeap - afterHeap); + retainedMemory = finalHeap - beforeHeap; + } + return { + testTime: endTime, + gcTime: endGCTime || 0, + beforeHeap: beforeHeap || 0, + garbageCount: garbage || 0, + retainedCount: retainedMemory || 0 + }; +}; + +bp.Report.generatePartial = function(model) { + return bp.Document.infoTemplate(model); +}; + +bp.Document.writeReport = function(reportContent) { + bp.Document.infoDiv.innerHTML = reportContent; +}; + +bp.Report.getTimesPerAction = function(name) { + var tpa = bp.Report.timesPerAction[name]; + if (!tpa) { + tpa = bp.Report.timesPerAction[name] = { + name: name, + nextEntry: 0 + }; + _.each(bp.Measure.characteristics, function(c) { + tpa[c] = { + recent: undefined, + history: [], + avg: {}, + min: Number.MAX_VALUE, + max: Number.MIN_VALUE + }; + }); + } + return tpa; +}; + +bp.Report.rightSizeTimes = function(times) { + var delta = times.length - bp.Runner.runState.numSamples; + if (delta > 0) { + return times.slice(delta); + } + + return times; +}; + +bp.Report.updateTimes = function(tpa, index, reference, recentTime) { + var curTpa = tpa[reference]; + curTpa.recent = recentTime; + curTpa.history[index] = recentTime; + curTpa.history = bp.Report.rightSizeTimes(curTpa.history); + curTpa.min = Math.min(curTpa.min, recentTime); + curTpa.max = Math.max(curTpa.max, recentTime); +}; + +bp.Report.calcStats = function() { + var report = ''; + bp.steps.forEach(function(bs) { + var recentResult = bp.Runner.runState.recentResult[bs.name], + tpa = bp.Report.getTimesPerAction(bs.name); + + _.each(bp.Measure.characteristics, function(c) { + bp.Report.updateTimes(tpa, tpa.nextEntry, c, recentResult[c]); + var mean = bp.Statistics.getMean(tpa[c].history); + var stdDev = bp.Statistics.calculateStandardDeviation(tpa[c].history, mean); + tpa[c].avg = { + mean: mean, + stdDev: stdDev, + coefficientOfVariation: bp.Statistics.calculateCoefficientOfVariation(stdDev, mean) + }; + }); + + tpa.nextEntry++; + tpa.nextEntry %= bp.Runner.runState.numSamples; + + report += bp.Report.generatePartial(tpa); + }); + return report; +}; + +bp.Document.addSampleRange = function() { + bp.Document.sampleRange = bp.Document.container().querySelector('#sampleRange'); + if (bp.Document.sampleRange) { + bp.Document.sampleRange.value = Math.max(bp.Runner.runState.numSamples, 1); + bp.Document.sampleRange.addEventListener('input', bp.Document.onSampleInputChanged); + bp.Document.sampleRangeValue = bp.Document.container().querySelector('#sampleRangeValue'); + bp.Document.sampleRangeValue.innerText = bp.Runner.runState.numSamples; + } + +}; + +bp.Document.onSampleInputChanged = function (evt) { + var value = evt.target.value; + bp.Runner.runState.numSamples = parseInt(value, 10); + if (bp.Document.sampleRangeValue) { + bp.Document.sampleRangeValue.innerText = value; + } +}; + +bp.Document.container = function() { + if (!bp.Document._container) { + bp.Document._container = document.querySelector('#benchmarkContainer'); + } + return bp.Document._container; +} + +bp.Document.addButton = function(reference, handler) { + var container = bp.Document.container(); + bp.Document[reference] = container.querySelector('button.' + reference); + if (bp.Document[reference]) { + bp.Document[reference].addEventListener('click', handler); + } +} + +bp.Document.addLinks = function() { + // Add links to everything + var linkDiv = bp.Document.container().querySelector('.versionContent'); + var linkHtml = ''; + + [ + // Add new benchmark suites here + ['index.html', 'LargeTable'] + ].forEach((function (link) { + linkHtml += ''+ link[1] +''; + })); + + if (linkDiv) { + linkDiv.innerHTML = linkHtml; + } +}; + +bp.Document.addInfo = function() { + bp.Document.infoDiv = bp.Document.container().querySelector('div.info'); + if (bp.Document.infoDiv) { + bp.Document.infoTemplate = _.template(bp.Document.container().querySelector('#infoTemplate').innerHTML); + } +}; + +bp.Document.onDOMContentLoaded = function() { + if (!bp.Document.container()) return; + bp.Document.addLinks(); + bp.Document.addButton('loopBtn', bp.Runner.loopBenchmark); + bp.Document.addButton('onceBtn', bp.Runner.onceBenchmark); + bp.Document.addButton('twentyFiveBtn', bp.Runner.twentyFiveBenchmark); + bp.Document.addSampleRange(); + bp.Document.addInfo(); +}; + +window.addEventListener('DOMContentLoaded', bp.Document.onDOMContentLoaded); diff --git a/perf/apps/largetable-bp/bp.spec.js b/perf/apps/largetable-bp/bp.spec.js new file mode 100644 index 000000000000..447c47058d7f --- /dev/null +++ b/perf/apps/largetable-bp/bp.spec.js @@ -0,0 +1,429 @@ +//ugly +if (typeof bp !== 'undefined') { + window.removeEventListener('DOMContentLoaded', bp.onDOMContentLoaded); +} + +describe('bp', function() { + var bp = window.bp, + mockStep = { + fn: function() {}, + name: 'fakeStep' + }; + + beforeEach(function() { + bp.Document._container = document.createElement('div'); + bp.Document.infoTemplate = function(model) { + return JSON.stringify(model); + } + bp.Runner.runState = { + iterations: 0, + numSamples: 20, + recentResult: {} + }; + + bp.Report.timesPerAction = {}; + }); + + describe('.Statistics', function() { + describe('.calculateConfidenceInterval()', function() { + it('should provide the correct confidence interval', function() { + expect(bp.Statistics.calculateConfidenceInterval(30, 1000)).toBe(1.859419264179007); + }); + }); + + + describe('.calculateRelativeMarginOfError()', function() { + expect(bp.Statistics.calculateRelativeMarginOfError(1.85, 5)).toBe(0.37); + }); + + + describe('.getMean()', function() { + it('should return the mean for a given sample', function() { + expect(bp.Statistics.getMean([1,2,5,4])).toBe(3); + }); + }); + + + describe('.calculateStandardDeviation()', function() { + it('should provide the correct standardDeviation for the provided sample and mean', function() { + expect(bp.Statistics.calculateStandardDeviation([ + 2,4,4,4,5,5,7,9 + ], 5)).toBe(2.138089935299395); + }); + + + it('should provide the correct standardDeviation for the provided sample and mean', function() { + expect(bp.Statistics.calculateStandardDeviation([ + 674.64,701.78,668.33,662.15,663.34,677.32,664.25,1233.00,1100.80,716.15,681.52,671.23,702.70,686.89,939.39,830.28,695.46,695.66,675.15,667.48], 750.38)).toBe(158.57877026559186); + }); + }); + + + describe('.calculateCoefficientOfVariation()', function() { + expect(bp.Statistics.calculateCoefficientOfVariation(0.5, 5)).toBe(0.1); + }); + }); + + + describe('.Document', function() { + describe('.container()', function() { + it('should return bp.Document._container if set', function() { + expect(bp.Document.container() instanceof HTMLElement).toBe(true); + }); + }); + + + describe('.onSampleRangeChanged()', function() { + beforeEach(function() { + bp.Runner.resetIterations(); + }); + + + it('should change the numSamples property', function() { + expect(bp.Runner.runState.numSamples).toBe(20); + bp.Document.onSampleInputChanged({target: {value: '80'}}); + expect(bp.Runner.runState.numSamples).toBe(80); + }); + }); + + + describe('.writeReport()', function() { + it('should write the report to the infoDiv', function() { + bp.Document.infoDiv = document.createElement('div'); + bp.Document.writeReport('report!'); + expect(bp.Document.infoDiv.innerHTML).toBe('report!') + }); + }); + + + describe('.onDOMContentLoaded()', function() { + it('should call methods to write to the dom', function() { + var linksSpy = spyOn(bp.Document, 'addLinks'); + var buttonSpy = spyOn(bp.Document, 'addButton'); + var rangeSpy = spyOn(bp.Document, 'addSampleRange'); + var infoSpy = spyOn(bp.Document, 'addInfo'); + + bp.Document.onDOMContentLoaded(); + expect(linksSpy).toHaveBeenCalled(); + expect(buttonSpy.callCount).toBe(3); + expect(rangeSpy).toHaveBeenCalled(); + expect(infoSpy).toHaveBeenCalled(); + }); + }); + }); + + + describe('.Runner', function() { + describe('.setIterations()', function() { + it('should set provided arguments to runState object', function() { + bp.Runner.runState = {numSamples: 20}; + bp.Runner.setIterations(15); + expect(bp.Runner.runState.numSamples).toBe(20); + expect(bp.Runner.runState.iterations).toBe(15); + }); + }); + + + describe('.resetIterations()', function() { + it('should set runState object to defaults', function() { + bp.Runner.runState = { + numSamples: 99, + iterations: 100, + recentResult: { + fakeStep: { + testTime: 2 + } + } + } + bp.Report.timesPerAction = { + fakeStep: { + testTimes: [5] + } + }; + + bp.Runner.resetIterations(); + expect(bp.Runner.runState.numSamples).toBe(99); + expect(bp.Runner.runState.iterations).toBe(0); + expect(bp.Report.timesPerAction).toEqual({fakeStep: {testTimes: [5]}}); + expect(bp.Runner.runState.recentResult['fakeStep'].testTime).toEqual(2); + }); + }); + + + describe('.runTimedTest()', function() { + it('should call gc if available', function() { + window.gc = window.gc || function() {}; + var spy = spyOn(window, 'gc'); + bp.Runner.runTimedTest(mockStep, {}); + expect(spy).toHaveBeenCalled(); + }); + + + it('should return the time required to run the test', function() { + var times = {}; + expect(typeof bp.Runner.runTimedTest(mockStep, times).testTime).toBe('number'); + }); + }); + + + describe('.runAllTests()', function() { + beforeEach(function() { + bp.steps = [mockStep]; + bp.Document.infoDiv = document.createElement('div'); + bp.infoTemplate = jasmine.createSpy('infoTemplate'); + }); + + it('should call resetIterations before calling done', function() { + var spy = spyOn(bp.Runner, 'resetIterations'); + bp.Runner.runState.iterations = 0; + bp.Runner.runAllTests(); + expect(spy).toHaveBeenCalled(); + }); + + + it('should call done after running for the appropriate number of iterations', function() { + var spy = spyOn(mockStep, 'fn'); + var doneSpy = jasmine.createSpy('done'); + + runs(function() { + bp.Runner.setIterations(5, 5); + bp.Runner.runAllTests(doneSpy); + }); + + waitsFor(function() { + return doneSpy.callCount; + }, 'done to be called', 200); + + runs(function() { + expect(spy.callCount).toBe(5); + }); + }); + + + it('should add as many times to timePerStep as are specified by numSamples', function() { + var doneSpy = jasmine.createSpy('done'); + var resetSpy = spyOn(bp.Runner, 'resetIterations'); + runs(function() { + bp.Runner.runState.numSamples = 8; + bp.Runner.setIterations(10); + bp.Runner.runAllTests(doneSpy); + }); + + waitsFor(function() { + return doneSpy.callCount; + }, 'done to be called', 200); + + runs(function() { + expect(bp.Report.timesPerAction.fakeStep.testTime.history.length).toBe(8); + }); + }); + }); + + + describe('.loopBenchmark()', function() { + var runAllTestsSpy, btn; + beforeEach(function() { + runAllTestsSpy = spyOn(bp.Runner, 'runAllTests'); + bp.Document.loopBtn = document.createElement('button'); + }); + + it('should call runAllTests if iterations does not start at greater than -1', function() { + bp.Runner.runState.iterations = 0; + bp.Runner.loopBenchmark(); + expect(runAllTestsSpy).toHaveBeenCalled(); + expect(runAllTestsSpy.callCount).toBe(1); + }); + + + it('should not call runAllTests if iterations is already -1', function() { + runs(function() { + bp.Runner.runState.iterations = -1; + bp.Runner.loopBenchmark(); + }); + + waits(1); + + runs(function() { + expect(runAllTestsSpy).not.toHaveBeenCalled(); + }); + }); + + + it('should not call runAllTests if iterations is less than -1', function() { + runs(function() { + bp.Runner.runState.iterations = -50; + bp.Runner.loopBenchmark(); + }); + + waits(1); + + runs(function() { + expect(runAllTestsSpy).not.toHaveBeenCalled(); + }); + }); + + + it('should set the button text to "Pause" while iterating', function() { + bp.Runner.runState.iterations = 0; + bp.Runner.loopBenchmark(); + expect(bp.Document.loopBtn.innerText).toBe('Pause'); + }); + + + it('should set the button text to "Loop" while iterating', function() { + bp.Runner.runState.iterations = -1; + bp.Runner.loopBenchmark(); + expect(bp.Document.loopBtn.innerText).toBe('Loop'); + }); + + + it('should set the runState -1 iterations', function() { + var spy = spyOn(bp.Runner, 'setIterations'); + bp.Runner.runState.iterations = 0; + bp.Runner.loopBenchmark(); + expect(spy).toHaveBeenCalledWith(-1); + }); + + + it('should set the iterations to 0 if iterations is already -1', function() { + bp.Runner.runState.iterations = -1; + bp.Runner.loopBenchmark(); + expect(bp.Runner.runState.iterations).toBe(0); + }); + }); + + + describe('.onceBenchmark()', function() { + var runAllTestsSpy; + beforeEach(function() { + bp.Document.onceBtn = document.createElement('button'); + runAllTestsSpy = spyOn(bp.Runner, 'runAllTests'); + }); + + it('should call runAllTests', function() { + expect(runAllTestsSpy.callCount).toBe(0); + bp.Runner.onceBenchmark(); + expect(runAllTestsSpy).toHaveBeenCalled(); + }); + + + it('should set the button text to "..."', function() { + expect(runAllTestsSpy.callCount).toBe(0); + bp.Runner.onceBenchmark(); + expect(bp.Document.onceBtn.innerText).toBe('...'); + }); + + + it('should set the text back to Once when done running test', function() { + expect(bp.Document.onceBtn.innerText).not.toBe('Once'); + bp.Runner.onceBenchmark(); + var done = runAllTestsSpy.calls[0].args[0]; + done(); + expect(bp.Document.onceBtn.innerText).toBe('Once'); + }); + }); + + + describe('.twentyFiveBenchmark()', function() { + var runAllTestsSpy; + beforeEach(function() { + bp.Document.twentyFiveBtn = document.createElement('button'); + runAllTestsSpy = spyOn(bp.Runner, 'runAllTests'); + }); + + + it('should set the runState to25 iterations', function() { + var spy = spyOn(bp.Runner, 'setIterations'); + bp.Runner.twentyFiveBenchmark(); + expect(spy).toHaveBeenCalledWith(25); + }); + + + it('should change the button text to "Looping..."', function() { + expect(bp.Document.twentyFiveBtn.innerText).not.toBe('Looping...'); + bp.Runner.twentyFiveBenchmark(); + expect(bp.Document.twentyFiveBtn.innerText).toBe('Looping...'); + }); + + + it('should call runAllTests', function() { + bp.Runner.twentyFiveBenchmark(); + expect(runAllTestsSpy).toHaveBeenCalled(); + }); + + + it('should pass runAllTests a third argument specifying times to ignore', function() { + bp.Runner.twentyFiveBenchmark(); + expect(runAllTestsSpy.calls[0].args[1]).toBe(5); + }); + }); + }); + + + describe('.Report', function() { + describe('.calcStats()', function() { + beforeEach(function() { + bp.steps = [mockStep]; + bp.Runner.runState = { + numSamples: 5, + iterations: 5, + recentResult: { + fakeStep: { + testTime: 5, + gcTime: 2, + recentGarbagePerStep: 200, + recentRetainedMemoryPerStep: 100 + } + } + }; + bp.Report.timesPerAction = { + fakeStep: { + testTime: { + history: [3,7] + }, + garbageCount: { + history: [50,50] + }, + retainedCount: { + history: [25,25] + }, + gcTime: { + recent: 3, + history: [1,3] + }, + nextEntry: 2 + }, + }; + }); + + + it('should set the most recent time for each step to the next entry', function() { + bp.Report.calcStats(); + expect(bp.Report.timesPerAction.fakeStep.testTime.history[2]).toBe(5); + bp.Runner.runState.recentResult.fakeStep.testTime = 25; + bp.Report.calcStats(); + expect(bp.Report.timesPerAction.fakeStep.testTime.history[3]).toBe(25); + }); + + + it('should return an string report', function() { + expect(typeof bp.Report.calcStats()).toBe('string'); + }); + }); + + + describe('.rightSizeTimes()', function() { + it('should make remove the left side of the input if longer than numSamples', function() { + bp.Runner.runState.numSamples = 3; + expect(bp.Report.rightSizeTimes([0,1,2,3,4,5,6])).toEqual([4,5,6]); + }); + + + it('should return the whole list if shorter than or equal to numSamples', function() { + bp.Runner.runState.numSamples = 7; + expect(bp.Report.rightSizeTimes([0,1,2,3,4,5,6])).toEqual([0,1,2,3,4,5,6]); + expect(bp.Report.rightSizeTimes([0,1,2,3,4,5])).toEqual([0,1,2,3,4,5]); + }); + }); + }); +}); \ No newline at end of file diff --git a/perf/apps/largetable-bp/index.html b/perf/apps/largetable-bp/index.html new file mode 100644 index 000000000000..9ff2f79d220d --- /dev/null +++ b/perf/apps/largetable-bp/index.html @@ -0,0 +1,184 @@ + + + Largetable Benchmark + + + + + + + + + +
+
+
+
+ +
+
+
+ + +
+
+
+ + + +
+
+ +
+
+
+
+ test time (ms) +
+
+ gc time (ms) +
+
+ garbage (KB) +
+
+ retained memory (KB) +
+
+
+
+
+ + +
+
+
+

+ Large table rendered with AngularJS +

+ +
none:
+
baseline binding:
+
baseline interpolation:
+
ngBind:
+
interpolation:
+
ngBind + fnInvocation:
+
interpolation + fnInvocation:
+
ngBind + filter:
+
ngBind + filter:
+ + + + + + +
+

baseline binding

+
+ :| +
+
+
+

baseline interpolation

+
+ {{column.i}}:{{column.j}}| +
+
+
+

bindings with functions

+
+ :| +
+
+
+

interpolation with functions

+
+ {{column.iFn()}}:{{column.jFn()}}| +
+
+
+

bindings with filter

+
+ :| +
+
+
+

interpolation with filter

+
+ {{column.i | noop}}:{{column.j | noop}}| +
+
+
+
+ + diff --git a/perf/apps/largetable-bp/launch_chrome.sh b/perf/apps/largetable-bp/launch_chrome.sh new file mode 100755 index 000000000000..c9e7ea65b55d --- /dev/null +++ b/perf/apps/largetable-bp/launch_chrome.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +platform=`uname` +if [[ "$platform" == 'Linux' ]]; then + `google-chrome --js-flags="--expose-gc"` +elif [[ "$platform" == 'Darwin' ]]; then + `/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --enable-memory-info --enable-precise-memory-info --enable-memory-benchmarking --js-flags="--expose-gc"` +fi diff --git a/perf/apps/largetable-bp/underscore.js b/perf/apps/largetable-bp/underscore.js new file mode 100644 index 000000000000..9a4cabecf7f8 --- /dev/null +++ b/perf/apps/largetable-bp/underscore.js @@ -0,0 +1,1343 @@ +// Underscore.js 1.6.0 +// http://underscorejs.org +// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.6.0'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return obj; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, length = obj.length; i < length; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; + } + } + return obj; + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results.push(iterator.call(context, value, index, list)); + }); + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var result; + any(obj, function(value, index, list) { + if (predicate.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(predicate, context); + each(obj, function(value, index, list) { + if (predicate.call(context, value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, function(value, index, list) { + return !predicate.call(context, value, index, list); + }, context); + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + predicate || (predicate = _.identity); + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(predicate, context); + each(obj, function(value, index, list) { + if (!(result = result && predicate.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, predicate, context) { + predicate || (predicate = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(predicate, context); + each(obj, function(value, index, list) { + if (result || (result = predicate.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + return any(obj, function(value) { + return value === target; + }); + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matches(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matches(attrs)); + }; + + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } + var result = -Infinity, lastComputed = -Infinity; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + if (computed > lastComputed) { + result = value; + lastComputed = computed; + } + }); + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } + var result = Infinity, lastComputed = Infinity; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + if (computed < lastComputed) { + result = value; + lastComputed = computed; + } + }); + return result; + }; + + // Shuffle an array, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // Sample **n** random values from a collection. + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (obj.length !== +obj.length) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + if (value == null) return _.identity; + if (_.isFunction(value)) return value; + return _.property(value); + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, iterator, context) { + iterator = lookupIterator(iterator); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, iterator, context) { + var result = {}; + iterator = lookupIterator(iterator); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, key, value) { + _.has(result, key) ? result[key].push(value) : result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, key, value) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, key) { + _.has(result, key) ? result[key]++ : result[key] = 1; + }); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = lookupIterator(iterator); + var value = iterator.call(context, obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + if ((n == null) || guard) return array[0]; + if (n < 0) return []; + return slice.call(array, 0, n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if ((n == null) || guard) return array[array.length - 1]; + return slice.call(array, Math.max(array.length - n, 0)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + if (shallow && _.every(input, _.isArray)) { + return concat.apply(output, input); + } + each(input, function(value) { + if (_.isArray(value) || _.isArguments(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Split an array into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = function(array, predicate) { + var pass = [], fail = []; + each(array, function(elem) { + (predicate(elem) ? pass : fail).push(elem); + }); + return [pass, fail]; + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); + } + }); + return results; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(_.flatten(arguments, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.contains(other, item); + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var length = _.max(_.pluck(arguments, 'length').concat(0)); + var results = new Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(arguments, '' + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, length = list.length; i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, length = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < length; i++) if (array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(length); + + while(idx < length) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError; + args = slice.call(arguments, 2); + return bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + ctor.prototype = func.prototype; + var self = new ctor; + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder, allowing any combination of arguments to be pre-filled. + _.partial = function(func) { + var boundArgs = slice.call(arguments, 1); + return function() { + var position = 0; + var args = boundArgs.slice(); + for (var i = 0, length = args.length; i < length; i++) { + if (args[i] === _) args[i] = arguments[position++]; + } + while (position < arguments.length) args.push(arguments[position++]); + return func.apply(this, args); + }; + }; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length === 0) throw new Error('bindAll must be passed function names'); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + options || (options = {}); + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + context = args = null; + }; + return function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = _.now() - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = _.now(); + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = new Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = new Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + if (obj[prop] === void 0) obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) return bStack[length] == b; + } + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor)) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj != +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + _.constant = function(value) { + return function () { + return value; + }; + }; + + _.property = function(key) { + return function(obj) { + return obj[key]; + }; + }; + + // Returns a predicate for checking whether an object has a given set of `key:value` pairs. + _.matches = function(attrs) { + return function(obj) { + if (obj === attrs) return true; //avoid comparing an object to itself. + for (var key in attrs) { + if (attrs[key] !== obj[key]) + return false; + } + return true; + } + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + var accum = Array(Math.max(0, n)); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { return new Date().getTime(); }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property) { + if (object == null) return void 0; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (typeof define === 'function' && define.amd) { + define('underscore', [], function() { + return _; + }); + } +}).call(this); From 5ad6f66156e713ed9959d40886857f38e7ef9169 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Thu, 7 Aug 2014 22:15:17 -0700 Subject: [PATCH 16/83] perf(jqLite): optimize jqLiteAcceptsData method This doesn't show up too high in profiles, but this code is cleaner anyway --- src/jqLite.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/jqLite.js b/src/jqLite.js index 8ef54249f9ae..d37f97ee5c97 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -164,7 +164,8 @@ function jqLiteIsTextNode(html) { function jqLiteAcceptsData(node) { // The window object can accept data but has no nodeType // Otherwise we are only interested in elements (1) and documents (9) - return !node.nodeType || node.nodeType === 1 || node.nodeType === 9; + var nodeType = node.nodeType; + return nodeType === 1 || !nodeType || nodeType === 9; } function jqLiteBuildFragment(html, context) { From a1b4c21aa62ee2aaa6fd8121ee434acdfd4e61bd Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Thu, 7 Aug 2014 22:36:01 -0700 Subject: [PATCH 17/83] chore(ngRepeat): name anonymous transclude callback for better debugging --- src/ng/directive/ngRepeat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 2db6671ee6f8..3c6bd4dd1954 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -380,7 +380,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { updateScope(block.scope, index); } else { // new item which we don't know about - $transclude(function(clone, scope) { + $transclude(function ngRepeatTransclude(clone, scope) { block.scope = scope; clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' '); $animate.enter(clone, null, jqLite(previousNode)); From 3ca34bc0b1121164862f685bc2d6524eb01146b6 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Thu, 7 Aug 2014 22:43:04 -0700 Subject: [PATCH 18/83] refactor(ngBindHtml): improve readability of the code --- src/ng/directive/ngBind.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js index 1ee7f2578190..b576e92ad7bf 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -180,21 +180,18 @@ var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) { return { restrict: 'A', - compile: function (tElement, tAttrs) { + link: function ngBindLink(scope, element, attr) { + $compile.addBindingInfo(element, attr.ngBindHtml); + var ngBindHtmlGetter = $parse(attr.ngBindHtml); + var ngBindHtmlWatch = $parse(attr.ngBindHtml, function getStringValue(value) { + return value ? value.toString() : ''; + }); - return function (scope, element, attr) { - $compile.addBindingInfo(element, attr.ngBindHtml); - var parsed = $parse(attr.ngBindHtml); - var changeDetector = $parse(attr.ngBindHtml, function getStringValue(value) { - return (value || '').toString(); - }); - - scope.$watch(changeDetector, function ngBindHtmlWatchAction() { - // we re-evaluate the expr because we want a TrustedValueHolderType - // for $sce, not a string - element.html($sce.getTrustedHtml(parsed(scope)) || ''); - }); - }; + scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() { + // we re-evaluate the expr because we want a TrustedValueHolderType + // for $sce, not a string + element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || ''); + }); } }; }]; From 0affbf9f1ccb9bb7b6e6a44e4d0ed0604613490e Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Thu, 7 Aug 2014 22:47:02 -0700 Subject: [PATCH 19/83] style(ngBind): name anonymous link fn to ease debugging/profiling --- src/ng/directive/ngBind.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js index b576e92ad7bf..c0a7408a1f7a 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -53,16 +53,14 @@ */ var ngBindDirective = ['$compile', function($compile) { return { - compile: function(templateElement) { - return function (scope, element, attr) { - $compile.addBindingInfo(element, attr.ngBind); - scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - // We are purposefully using == here rather than === because we want to - // catch when value is "null or undefined" - // jshint -W041 - element.text(value == undefined ? '' : value); - }); - }; + link: function ngBindLink(scope, element, attr) { + $compile.addBindingInfo(element, attr.ngBind); + scope.$watch(attr.ngBind, function ngBindWatchAction(value) { + // We are purposefully using == here rather than === because we want to + // catch when value is "null or undefined" + // jshint -W041 + element.text(value == undefined ? '' : value); + }); } }; }]; From 95ef76a4a971c5d6418da4adf0d34e367f47424e Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 11:06:59 -0700 Subject: [PATCH 20/83] chore: rename getBlockElements to more correct getBlockNodes --- src/Angular.js | 29 +++++++++++++---------------- src/ng/directive/ngIf.js | 2 +- src/ng/directive/ngRepeat.js | 4 ++-- src/ng/directive/ngSwitch.js | 2 +- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 1caab1545dc0..1dedecb1052a 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -81,7 +81,7 @@ assertArgFn: true, assertNotHasOwnProperty: true, getter: true, - getBlockElements: true, + getBlockNodes: true, hasOwnProperty: true, */ @@ -1552,23 +1552,20 @@ function getter(obj, path, bindFnToScope) { /** * Return the DOM siblings between the first and last node in the given array. * @param {Array} array like object - * @returns {DOMElement} object containing the elements + * @returns {jqLite} jqLite collection containing the nodes */ -function getBlockElements(nodes) { - var startNode = nodes[0], - endNode = nodes[nodes.length - 1]; - if (startNode === endNode) { - return jqLite(startNode); - } - - var element = startNode; - var elements = [element]; +function getBlockNodes(nodes) { + // TODO(perf): just check if all items in `nodes` are siblings and if they are return the original + // collection, otherwise update the original collection. + var node = nodes[0]; + var endNode = nodes[nodes.length - 1]; + var blockNodes = [node]; do { - element = element.nextSibling; - if (!element) break; - elements.push(element); - } while (element !== endNode); + node = node.nextSibling; + if (!node) break; + blockNodes.push(node); + } while (node !== endNode); - return jqLite(elements); + return jqLite(blockNodes); } diff --git a/src/ng/directive/ngIf.js b/src/ng/directive/ngIf.js index b4c569fecbbe..104423b4d3ca 100644 --- a/src/ng/directive/ngIf.js +++ b/src/ng/directive/ngIf.js @@ -112,7 +112,7 @@ var ngIfDirective = ['$animate', function($animate) { childScope = null; } if(block) { - previousElements = getBlockElements(block.clone); + previousElements = getBlockNodes(block.clone); $animate.leave(previousElements, function() { previousElements = null; }); diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 3c6bd4dd1954..624274717773 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -350,7 +350,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn if (lastBlockMap.hasOwnProperty(blockKey)) { block = lastBlockMap[blockKey]; - elementsToRemove = getBlockElements(block.clone); + elementsToRemove = getBlockNodes(block.clone); $animate.leave(elementsToRemove); forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); block.scope.$destroy(); @@ -374,7 +374,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { if (getBlockStart(block) != nextNode) { // existing item which got moved - $animate.move(getBlockElements(block.clone), null, jqLite(previousNode)); + $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode)); } previousNode = getBlockEnd(block); updateScope(block.scope, index); diff --git a/src/ng/directive/ngSwitch.js b/src/ng/directive/ngSwitch.js index 1eb1d32a4e21..93e5bfe75f6e 100644 --- a/src/ng/directive/ngSwitch.js +++ b/src/ng/directive/ngSwitch.js @@ -152,7 +152,7 @@ var ngSwitchDirective = ['$animate', function($animate) { previousElements.length = 0; for (i = 0, ii = selectedScopes.length; i < ii; ++i) { - var selected = getBlockElements(selectedElements[i].clone); + var selected = getBlockNodes(selectedElements[i].clone); selectedScopes[i].$destroy(); previousElements[i] = selected; $animate.leave(selected, function() { From 68dce9f55bb54e883c959a339a8a723b53c62d5f Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 11:08:07 -0700 Subject: [PATCH 21/83] refactor: simplify trim fn now that IE9 has String#trim --- src/Angular.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 1dedecb1052a..b56873f11dab 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -575,19 +575,9 @@ function isPromiseLike(obj) { } -var trim = (function() { - // native trim is way faster: http://jsperf.com/angular-trim-test - // but IE doesn't have it... :-( - // TODO: we should move this into IE/ES5 polyfill - if (!String.prototype.trim) { - return function(value) { - return isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value; - }; - } - return function(value) { - return isString(value) ? value.trim() : value; - }; -})(); +var trim = function(value) { + return isString(value) ? value.trim() : value; +}; /** From f53f5462d94beb963bdd3016d0c9c8246cf8985d Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 11:09:14 -0700 Subject: [PATCH 22/83] perf(jqLite): don't check isString many times in constructor Note: no significant perf gain in Chrome --- src/jqLite.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index d37f97ee5c97..2b75dcf3e0fb 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -225,17 +225,21 @@ function JQLite(element) { if (element instanceof JQLite) { return element; } + + var stringArg; + if (isString(element)) { element = trim(element); + stringArg = true; } if (!(this instanceof JQLite)) { - if (isString(element) && element.charAt(0) != '<') { + if (stringArg && element.charAt(0) != '<') { throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element'); } return new JQLite(element); } - if (isString(element)) { + if (stringArg) { jqLiteAddNodes(this, jqLiteParseHTML(element)); } else { jqLiteAddNodes(this, element); From b5ba807da7f6447b3213492264b48763b1f5d68d Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 11:25:48 -0700 Subject: [PATCH 23/83] style(jqLite): remove MiskoCode(tm) --- src/jqLite.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/jqLite.js b/src/jqLite.js index 2b75dcf3e0fb..f9796e88ffc4 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -307,7 +307,9 @@ function jqLiteRemoveData(element, name) { } if (expandoStore.handle) { - expandoStore.events.$destroy && expandoStore.handle({}, '$destroy'); + if (expandoStore.events.$destroy) { + expandoStore.handle({}, '$destroy'); + } jqLiteOff(element); } delete jqCache[expandoId]; From 125b43f63c05c2b2d6ab90740a30d2e64420d890 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 11:27:24 -0700 Subject: [PATCH 24/83] refactor(jqLite): don't look up the entry needlessly --- src/jqLite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jqLite.js b/src/jqLite.js index f9796e88ffc4..1e2ff90e0911 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -319,7 +319,7 @@ function jqLiteRemoveData(element, name) { function jqLiteExpandoStore(element, key, value) { var expandoId = element.ng339, - expandoStore = jqCache[expandoId || -1]; + expandoStore = expandoId && jqCache[expandoId]; if (isDefined(value)) { if (!expandoStore) { From d6b6423a41b47e325f05d9cd55a2291bc775a56c Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 11:28:17 -0700 Subject: [PATCH 25/83] perf(jqLite): don't use String#split in on() unless we need it --- src/jqLite.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/jqLite.js b/src/jqLite.js index 1e2ff90e0911..bfe6d8420133 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -727,7 +727,8 @@ forEach({ if (!events) jqLiteExpandoStore(element, 'events', events = {}); if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); - var types = type.split(' '); + // http://jsperf.com/string-indexof-vs-split + var types = type.indexOf(' ') ? type.split(' ') : [type]; var i = types.length; while (i--) { From 47045ea345ca308b2fee543f5085dcc59b0ef6c0 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 11:46:21 -0700 Subject: [PATCH 26/83] perf(jqLite): microoptimization in chaining fn note: no siginificant difference observed in macrobenchmarks, so this is just to make me feel better :) --- src/jqLite.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/jqLite.js b/src/jqLite.js index bfe6d8420133..30460d1385bb 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -912,7 +912,9 @@ forEach({ */ JQLite.prototype[name] = function(arg1, arg2, arg3) { var value; - for(var i=0; i < this.length; i++) { + var nodeCount = this.length; + + for(var i=0; i < nodeCount; i++) { if (isUndefined(value)) { value = fn(this[i], arg1, arg2, arg3); if (isDefined(value)) { From 6f4d7a5503b212cf43bc744445a99f61e349daae Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 12:04:56 -0700 Subject: [PATCH 27/83] refactor(shallowCopy): microoptimization --- src/Angular.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index b56873f11dab..2d61b1b84ea5 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -825,10 +825,12 @@ function copy(source, destination, stackSource, stackDest) { */ function shallowCopy(src, dst) { var i = 0; + var l; + if (isArray(src)) { dst = dst || []; - for (; i < src.length; i++) { + for (l = src.length; i < l; i++) { dst[i] = src[i]; } } else if (isObject(src)) { @@ -836,7 +838,7 @@ function shallowCopy(src, dst) { var keys = Object.keys(src); - for (var l = keys.length; i < l; i++) { + for (l = keys.length; i < l; i++) { var key = keys[i]; if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { From 3f067625daffc358ce95e348fe32be0c6d756440 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 12:58:52 -0700 Subject: [PATCH 28/83] perf: speed up shallowCopy and special case Attributes cloning `for in` is much faster than `Object.keys()` but `for in` includes properties from the prototype. http://jsperf.com/for-in-vs-object-keys2 All the uses of shallowCopy don't deal with objects with heavy prototypes, except for Attributes instances in $compile. For this reason it's better to special-case Attributes constructor and make it do it's own shallow copy. This cleans up the Attribute/$compile code as well. --- src/Angular.js | 12 +++++------- src/ng/compile.js | 25 ++++++++++++++++++------- test/AngularSpec.js | 13 ------------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 2d61b1b84ea5..5ab582866a75 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -821,11 +821,13 @@ function copy(source, destination, stackSource, stackDest) { } /** - * Creates a shallow copy of an object, an array or a primitive + * Creates a shallow copy of an object, an array or a primitive. + * + * Assumes that there no proto properties for objects */ function shallowCopy(src, dst) { var i = 0; - var l; + var l, key; if (isArray(src)) { dst = dst || []; @@ -836,11 +838,7 @@ function shallowCopy(src, dst) { } else if (isObject(src)) { dst = dst || {}; - var keys = Object.keys(src); - - for (l = keys.length; i < l; i++) { - var key = keys[i]; - + for (key in src) { if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { dst[key] = src[key]; } diff --git a/src/ng/compile.js b/src/ng/compile.js index 2e6b52585d76..a2a9c6db4f93 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -685,9 +685,20 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { - var Attributes = function(element, attr) { + var Attributes = function(element, attributesToCopy) { + if (attributesToCopy) { + var keys = Object.keys(attributesToCopy); + var i, l, key; + + for (i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + this[key] = attributesToCopy[key]; + } + } else { + this.$attr = {}; + } + this.$$element = element; - this.$attr = attr || {}; }; Attributes.prototype = { @@ -1481,12 +1492,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var attrs, $element, i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn; + var i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn; - attrs = (compileNode === linkNode) - ? templateAttrs - : shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); - $element = attrs.$$element; + var $element = jqLite(linkNode); + var attrs = (compileNode === linkNode) + ? templateAttrs + : new Attributes($element, templateAttrs); if (newIsolateScopeDirective) { var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 01d6f5eb7086..878eb9567d63 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -244,19 +244,6 @@ describe('angular', function() { expect(clone.$some).toBe(original.$some); }); - it('should omit properties from prototype chain', function() { - var original, clone = {}; - function Func() {} - Func.prototype.hello = "world"; - - original = new Func(); - original.goodbye = "world"; - - expect(shallowCopy(original, clone)).toBe(clone); - expect(clone.hello).toBeUndefined(); - expect(clone.goodbye).toBe("world"); - }); - it('should handle arrays', function() { var original = [{}, 1], clone = []; From c8e601f9048633b8c7872a1d6bf4df56db90f7a5 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 13:06:13 -0700 Subject: [PATCH 29/83] chore: remove unused variables --- src/ng/compile.js | 2 +- src/ng/rootScope.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index a2a9c6db4f93..cddba7580c86 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -895,7 +895,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // not be able to attach scope data to them, so we will wrap them in forEach($compileNodes, function(node, index){ if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = node = jqLite(node).wrap('').parent()[0]; + $compileNodes[index] = jqLite(node).wrap('').parent()[0]; } }); var compositeLinkFn = diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 24b80a36ed4a..3c545be5f6f1 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -171,8 +171,7 @@ function $RootScopeProvider(){ * */ $new: function(isolate) { - var ChildScope, - child; + var child; if (isolate) { child = new Scope(); From ef586e68ab116c700011354ae3585e2ef17ad8fd Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 13:24:36 -0700 Subject: [PATCH 30/83] perf($compile): delay object initialization in nodeLinkFn --- src/ng/compile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index cddba7580c86..dd0d60dcf3b9 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1492,7 +1492,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn; + var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn; var $element = jqLite(linkNode); var attrs = (compileNode === linkNode) @@ -1590,6 +1590,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } transcludeFn = boundTranscludeFn && controllersBoundTransclude; if (controllerDirectives) { + elementControllers = {}; forEach(controllerDirectives, function(directive) { var locals = { $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, From 2ccd66f881184ea28bd2bb5993956e24012acb41 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 13:27:55 -0700 Subject: [PATCH 31/83] style(ngRepeat): fix indentation --- src/ng/directive/ngRepeat.js | 48 ++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 624274717773..3c90d375ba6d 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -320,30 +320,30 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // locate existing items length = nextBlockOrder.length = collectionKeys.length; - for(index = 0; index < length; index++) { - key = (collection === collectionKeys) ? index : collectionKeys[index]; - value = collection[key]; - trackById = trackByIdFn(key, value, index); - assertNotHasOwnProperty(trackById, '`track by` id'); - if(lastBlockMap.hasOwnProperty(trackById)) { - block = lastBlockMap[trackById]; - delete lastBlockMap[trackById]; - nextBlockMap[trackById] = block; - nextBlockOrder[index] = block; - } else if (nextBlockMap.hasOwnProperty(trackById)) { - // restore lastBlockMap - forEach(nextBlockOrder, function(block) { - if (block && block.scope) lastBlockMap[block.id] = block; - }); - // This is a duplicate and we need to throw an error - throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", - expression, trackById); - } else { - // new never before seen block - nextBlockOrder[index] = { id: trackById }; - nextBlockMap[trackById] = false; - } - } + for (index = 0; index < length; index++) { + key = (collection === collectionKeys) ? index : collectionKeys[index]; + value = collection[key]; + trackById = trackByIdFn(key, value, index); + assertNotHasOwnProperty(trackById, '`track by` id'); + if (lastBlockMap.hasOwnProperty(trackById)) { + block = lastBlockMap[trackById]; + delete lastBlockMap[trackById]; + nextBlockMap[trackById] = block; + nextBlockOrder[index] = block; + } else if (nextBlockMap.hasOwnProperty(trackById)) { + // restore lastBlockMap + forEach(nextBlockOrder, function(block) { + if (block && block.scope) lastBlockMap[block.id] = block; + }); + // This is a duplicate and we need to throw an error + throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", + expression, trackById); + } else { + // new never before seen block + nextBlockOrder[index] = { id: trackById, scope: undefined, clone: undefined }; + nextBlockMap[trackById] = false; + } + } // remove existing items for (var blockKey in lastBlockMap) { From e4840835238dc35202a25c2d248832f630ca27c4 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 13:29:55 -0700 Subject: [PATCH 32/83] fix($parse): remove unused variable declaration in generated getters --- src/ng/parse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index d1766b9a6425..ef1fce54a237 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -904,7 +904,7 @@ function getterFn(path, options, fullExp) { }; } } else { - var code = 'var p;\n'; + var code = ''; forEach(pathKeys, function(key, index) { ensureSafeMemberName(key, fullExp); code += 'if(s == null) return undefined;\n' + From 6c511ce5408c2cf1e2a2c341c31cf43fec204421 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 13:30:43 -0700 Subject: [PATCH 33/83] style($parse): rename variables in generated code so that code is more readable --- src/ng/parse.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index ef1fce54a237..20814aa143fe 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -912,12 +912,12 @@ function getterFn(path, options, fullExp) { // we simply dereference 's' on any .dot notation ? 's' // but if we are first then we check locals first, and if so read it first - : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '.' + key + ';\n'; + : '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key + ';\n'; }); code += 'return s;'; /* jshint -W054 */ - var evaledFnGetter = new Function('s', 'k', code); // s=scope, k=locals + var evaledFnGetter = new Function('s', 'l', code); // s=scope, l=locals /* jshint +W054 */ evaledFnGetter.toString = valueFn(code); fn = evaledFnGetter; From 0035353f4d2bf3a49fd7a82d06f4f6de9d55dd95 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 13:32:41 -0700 Subject: [PATCH 34/83] perf($parse): trim expression only if string --- src/ng/parse.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 20814aa143fe..d4d9aa50b128 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -995,11 +995,12 @@ function $ParseProvider() { $parseOptions.csp = $sniffer.csp; return function(exp, interceptorFn) { - var parsedExpression, oneTime, - cacheKey = (exp = trim(exp)); + var parsedExpression, oneTime, cacheKey; switch (typeof exp) { case 'string': + cacheKey = exp = exp.trim(); + if (cache.hasOwnProperty(cacheKey)) { parsedExpression = cache[cacheKey]; } else { From 08369814ac6f53699f642d2686efa2344c852788 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 13:33:34 -0700 Subject: [PATCH 35/83] refactor($parse): simplify addInterceptor fn --- src/ng/parse.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index d4d9aa50b128..190a55314de5 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1064,19 +1064,17 @@ function $ParseProvider() { } function addInterceptor(parsedExpression, interceptorFn) { - if (isFunction(interceptorFn)) { - var fn = function interceptedExpression(scope, locals) { - var value = parsedExpression(scope, locals); - var result = interceptorFn(value, scope, locals); - // we only return the interceptor's result if the - // initial value is defined (for bind-once) - return isDefined(value) ? result : value; - }; - fn.$$watchDelegate = parsedExpression.$$watchDelegate; - return fn; - } else { - return parsedExpression; - } + if (!interceptorFn) return parsedExpression; + + var fn = function interceptedExpression(scope, locals) { + var value = parsedExpression(scope, locals); + var result = interceptorFn(value, scope, locals); + // we only return the interceptor's result if the + // initial value is defined (for bind-once) + return isDefined(value) ? result : value; + }; + fn.$$watchDelegate = parsedExpression.$$watchDelegate; + return fn; } }]; } From 75456939ac83cf93c92f76d9f6e3e32a265865fd Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 13:38:41 -0700 Subject: [PATCH 36/83] chore(Scope): rename $$childScopeClass to ChildClass to simplify debugging/profiling --- src/ng/rootScope.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 3c545be5f6f1..6d7d68f99c8a 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -182,18 +182,18 @@ function $RootScopeProvider(){ } else { // Only create a child scope class if somebody asks for one, // but cache it to allow the VM to optimize lookups. - if (!this.$$childScopeClass) { - this.$$childScopeClass = function() { + if (!this.$$ChildScope) { + this.$$ChildScope = function ChildScope() { this.$$watchers = this.$$nextSibling = this.$$childHead = this.$$childTail = null; this.$$listeners = {}; this.$$listenerCount = {}; this.$id = nextUid(); - this.$$childScopeClass = null; + this.$$ChildScope = null; }; - this.$$childScopeClass.prototype = this; + this.$$ChildScope.prototype = this; } - child = new this.$$childScopeClass(); + child = new this.$$ChildScope(); } child['this'] = child; child.$parent = this; From 76e6bae13bfce3931d12319326cf4f605930fcf2 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 13:39:53 -0700 Subject: [PATCH 37/83] refactor(Scope): remove useless compileToFn helper fn --- src/ng/rootScope.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 6d7d68f99c8a..c266d412ea91 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -324,7 +324,7 @@ function $RootScopeProvider(){ * @returns {function()} Returns a deregistration function for this listener. */ $watch: function(watchExp, listener, objectEquality) { - var get = compileToFn(watchExp, 'watch'); + var get = $parse(watchExp); if (get.$$watchDelegate) { return get.$$watchDelegate(this, listener, objectEquality, get); @@ -1195,11 +1195,6 @@ function $RootScopeProvider(){ $rootScope.$$phase = null; } - function compileToFn(exp, name) { - var fn = $parse(exp); - assertArgFn(fn, name); - return fn; - } function decrementListenerCount(current, count, name) { do { From e062a16b9aede4a57532e331ddda740d9d23b6d2 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 8 Aug 2014 14:17:00 -0700 Subject: [PATCH 38/83] refactor(jqLite): clean up jqLiteRemoveData fn --- src/jqLite.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index 30460d1385bb..8e6b9aa1fa0d 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -297,12 +297,12 @@ function jqLiteOff(element, type, fn, unsupported) { } function jqLiteRemoveData(element, name) { - var expandoId = element.ng339, - expandoStore = jqCache[expandoId]; + var expandoId = element.ng339; + var expandoStore = expandoId && jqCache[expandoId]; if (expandoStore) { if (name) { - delete jqCache[expandoId].data[name]; + delete expandoStore.data[name]; return; } From 95efb7f167a92bb88eb344e3d5648433109569b0 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sat, 9 Aug 2014 18:30:11 -0700 Subject: [PATCH 39/83] style(jqLite): rename onFn to jqLiteOn --- src/jqLite.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index 8e6b9aa1fa0d..48768db0bec3 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -712,7 +712,7 @@ function createEventHandler(element, events) { forEach({ removeData: jqLiteRemoveData, - on: function onFn(element, type, fn, unsupported){ + on: function jqLiteOn(element, type, fn, unsupported){ if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); // Do not add event handlers to non-elements because they will not be cleaned up. @@ -743,7 +743,7 @@ forEach({ // Read about mouseenter and mouseleave: // http://www.quirksmode.org/js/events_mouse.html#link8 - onFn(element, MOUSE_EVENT_MAP[type], function(event) { + jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) { var target = this, related = event.relatedTarget; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window From 1d8a4b334108bb4e019ee0dcb76eac32f9dcba0e Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sat, 9 Aug 2014 18:47:38 -0700 Subject: [PATCH 40/83] perf(jqLite): refactor jqLiteExpandoStore to minimize access to expensive element.ng339 expando property --- src/jqLite.js | 60 +++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index 48768db0bec3..e6fb0d22717e 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -317,44 +317,41 @@ function jqLiteRemoveData(element, name) { } } -function jqLiteExpandoStore(element, key, value) { + +function jqLiteExpandoStore(element, createIfNecessary) { var expandoId = element.ng339, expandoStore = expandoId && jqCache[expandoId]; - if (isDefined(value)) { - if (!expandoStore) { - element.ng339 = expandoId = jqNextId(); - expandoStore = jqCache[expandoId] = {}; - } - expandoStore[key] = value; - } else { - return key ? expandoStore && expandoStore[key] : expandoStore; + if (createIfNecessary && !expandoStore) { + element.ng339 = expandoId = jqNextId(); + expandoStore = jqCache[expandoId] = {events: {}, data: {}, handle: undefined}; } + + return expandoStore; } + function jqLiteData(element, key, value) { if (jqLiteAcceptsData(element)) { - var data = jqLiteExpandoStore(element, 'data'), - isSetter = isDefined(value), - keyDefined = !isSetter && isDefined(key), - isSimpleGetter = keyDefined && !isObject(key); - if (!data && !isSimpleGetter) { - jqLiteExpandoStore(element, 'data', data = {}); - } + var isSimpleSetter = isDefined(value); + var isSimpleGetter = !isSimpleSetter && key && !isObject(key); + var massGetter = !key; + var expandoStore = jqLiteExpandoStore(element, !isSimpleGetter); + var data = expandoStore && expandoStore.data; - if (isSetter) { + if (isSimpleSetter) { // data('key', value) data[key] = value; } else { - if (keyDefined) { - if (isSimpleGetter) { - // don't create data in this case. + if (massGetter) { // data() + return data; + } else { + if (isSimpleGetter) { // data('key') + // don't force creation of expandoStore if it doesn't exist yet return data && data[key]; - } else { + } else { // mass-setter: data({key1: val1, key2: val2}) extend(data, key); } - } else { - return data; } } } @@ -720,12 +717,13 @@ forEach({ return; } - var expandoStore = jqLiteExpandoStore(element); - var events = expandoStore && expandoStore.events; - var handle = expandoStore && expandoStore.handle; + var expandoStore = jqLiteExpandoStore(element, true); + var events = expandoStore.events; + var handle = expandoStore.handle; - if (!events) jqLiteExpandoStore(element, 'events', events = {}); - if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); + if (!handle) { + handle = expandoStore.handle = createEventHandler(element, events); + } // http://jsperf.com/string-indexof-vs-split var types = type.indexOf(' ') ? type.split(' ') : [type]; @@ -887,8 +885,10 @@ forEach({ triggerHandler: function(element, eventName, eventData) { // Copy event handlers in case event handlers array is modified during execution. - var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName], - eventFnsCopy = shallowCopy(eventFns || []); + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var eventFns = events && events[eventName]; + var eventFnsCopy = shallowCopy(eventFns || []); eventData = eventData || []; From 66dd125bbeddc13bbc575433852521fde39554fd Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sun, 10 Aug 2014 00:55:03 -0700 Subject: [PATCH 41/83] perf($compile): clone the nodeList during linking only if necessary --- src/ng/compile.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index dd0d60dcf3b9..b4724f469c8e 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -956,7 +956,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], - attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound; + attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); @@ -985,6 +985,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { linkFns.push(nodeLinkFn, childLinkFn); linkFnFound = linkFnFound || nodeLinkFn || childLinkFn; + nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn; + //use the previous context only for the first element in the virtual group previousCompileContext = null; } @@ -994,14 +996,23 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) { var nodeLinkFn, childLinkFn, node, childScope, i, ii, n, childBoundTranscludeFn; + var stableNodeList; + + + if (nodeLinkFnFound) { + // copy nodeList so that if a nodeLinkFn removes or adds an element at this DOM level our + // offsets don't get screwed up + var nodeListLength = nodeList.length; + stableNodeList = new Array(nodeListLength); - // copy nodeList so that linking doesn't break due to live list updates. - var nodeListLength = nodeList.length, - stableNodeList = new Array(nodeListLength); - for (i = 0; i < nodeListLength; i++) { - stableNodeList[i] = nodeList[i]; + for (i = 0; i < nodeListLength; i++) { + stableNodeList[i] = nodeList[i]; + } + } else { + stableNodeList = nodeList; } + // TODO(perf): when the DOM is sparsely annotated with directives, we spend a lot of time iterating over nulls here for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) { node = stableNodeList[n]; nodeLinkFn = linkFns[i++]; From dccd43bd3fbe48705ea72ca3ebfb1f5d5f1bbe10 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sun, 10 Aug 2014 01:06:26 -0700 Subject: [PATCH 42/83] perf(jqLite): optimize off() 'for in' is much faster than Object.keys() and since the events object is ours, we know that we don't need to worry about prototypically inherited properties so we can skip expensive hasOwnProperty check. http://jsperf.com/for-in-vs-object-keys2 --- src/jqLite.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index e6fb0d22717e..affff2355040 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -269,16 +269,11 @@ function jqLiteOff(element, type, fn, unsupported) { var expandoStore = jqLiteExpandoStore(element); var events = expandoStore && expandoStore.events; var handle = expandoStore && expandoStore.handle; - var i; - var types; if (!handle) return; //no listeners registered - if (isUndefined(type)) { - types = Object.keys(events); - i = types.length; - while (i--) { - type = types[i]; + if (!type) { + for (type in events) { if (type !== '$destroy') { removeEventListenerFn(element, type, events[type]); } From 07b9a2e2cc31a0cc635470b873edd77c314b3fdf Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sun, 10 Aug 2014 04:17:36 -0700 Subject: [PATCH 43/83] perf(ngRepeat): clone boundary comment nodes http://jsperf.com/clone-vs-createcomment most of the improvement comes from not reconcatinating the strings --- src/ng/directive/ngRepeat.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 3c90d375ba6d..b839d247823b 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -225,6 +225,8 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { lhs, rhs, valueIdentifier, keyIdentifier, hashFnLocals = {$id: hashKey}; + var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' '); + if (!match) { throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", expression); @@ -382,7 +384,8 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // new item which we don't know about $transclude(function ngRepeatTransclude(clone, scope) { block.scope = scope; - clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' '); + // http://jsperf.com/clone-vs-createcomment + clone[clone.length++] = ngRepeatEndComment.cloneNode(); $animate.enter(clone, null, jqLite(previousNode)); previousNode = clone; // Note: We only need the first/last node of the cloned nodes. From cf916c02c4284a6f513af89bded266614e06edae Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sun, 10 Aug 2014 05:01:44 -0700 Subject: [PATCH 44/83] perf(ngRepeat): move updateScope fn to factory and reuse it for all repeaters --- src/ng/directive/ngRepeat.js | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index b839d247823b..bb00b8fed16c 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -211,6 +211,21 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var NG_REMOVED = '$$NG_REMOVED'; var ngRepeatMinErr = minErr('ngRepeat'); + + var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) { + // TODO(perf): generate setters to shave off ~40ms or 1-1.5% + scope[valueIdentifier] = value; + if (keyIdentifier) scope[keyIdentifier] = key; + scope.$index = index; + scope.$first = (index === 0); + scope.$last = (index === (arrayLength - 1)); + scope.$middle = !(scope.$first || scope.$last); + // jshint bitwise: false + scope.$odd = !(scope.$even = (index&1) === 0); + // jshint bitwise: true + }; + + return { restrict: 'A', multiElement: true, @@ -291,18 +306,6 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { $scope[aliasAs] = collection; } - var updateScope = function(scope, index) { - scope[valueIdentifier] = value; - if (keyIdentifier) scope[keyIdentifier] = key; - scope.$index = index; - scope.$first = (index === 0); - scope.$last = (index === (arrayLength - 1)); - scope.$middle = !(scope.$first || scope.$last); - // jshint bitwise: false - scope.$odd = !(scope.$even = (index&1) === 0); - // jshint bitwise: true - }; - if (isArrayLike(collection)) { collectionKeys = collection; trackByIdFn = trackByIdExpFn || trackByIdArrayFn; @@ -379,7 +382,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode)); } previousNode = getBlockEnd(block); - updateScope(block.scope, index); + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength); } else { // new item which we don't know about $transclude(function ngRepeatTransclude(clone, scope) { @@ -393,7 +396,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // by a directive with templateUrl when its template arrives. block.clone = clone; nextBlockMap[block.id] = block; - updateScope(block.scope, index); + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength); }); } } From dc85f8c35b504cd629c96591dd7ecad36834f75b Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sun, 10 Aug 2014 05:21:04 -0700 Subject: [PATCH 45/83] perf(ngRepeat): move work to compile fn this has impact only on nested repeaters where the number of columns is significant --- src/ng/directive/ngRepeat.js | 69 ++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index bb00b8fed16c..6bc0e0f3de43 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -233,27 +233,48 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { priority: 1000, terminal: true, $$tlb: true, - link: function($scope, $element, $attr, ctrl, $transclude){ - var expression = $attr.ngRepeat; - var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/), - trackByExp, trackByExpGetter, aliasAs, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, - lhs, rhs, valueIdentifier, keyIdentifier, - hashFnLocals = {$id: hashKey}; + compile: function ngRepeatCompile($element, $attr) { + var expression = $attr.ngRepeat; + var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' '); - var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' '); + var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); - if (!match) { - throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", + if (!match) { + throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", expression); - } + } - lhs = match[1]; - rhs = match[2]; - aliasAs = match[3]; - trackByExp = match[4]; + var lhs = match[1]; + var rhs = match[2]; + var aliasAs = match[3]; + var trackByExp = match[4]; - if (trackByExp) { - trackByExpGetter = $parse(trackByExp); + match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); + + if (!match) { + throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", + lhs); + } + var valueIdentifier = match[3] || match[1]; + var keyIdentifier = match[2]; + + var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn; + var hashFnLocals = {$id: hashKey}; + + if (trackByExp) { + trackByExpGetter = $parse(trackByExp); + } else { + trackByIdArrayFn = function (key, value) { + return hashKey(value); + }; + trackByIdObjFn = function (key) { + return key; + }; + } + + return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) { + + if (trackByExpGetter) { trackByIdExpFn = function(key, value, index) { // assign key, value, and $index to the locals so that they can be used in hash functions if (keyIdentifier) hashFnLocals[keyIdentifier] = key; @@ -261,22 +282,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { hashFnLocals.$index = index; return trackByExpGetter($scope, hashFnLocals); }; - } else { - trackByIdArrayFn = function(key, value) { - return hashKey(value); - }; - trackByIdObjFn = function(key) { - return key; - }; - } - - match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); - if (!match) { - throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", - lhs); } - valueIdentifier = match[3] || match[1]; - keyIdentifier = match[2]; // Store a list of elements from previous run. This is a hash where key is the item from the // iterator, and the value is objects with following properties. @@ -402,6 +408,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { } lastBlockMap = nextBlockMap; }); + }; } }; From 3f13059afa6d16ef63475bc556404ab44a8a74b2 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sun, 10 Aug 2014 05:27:25 -0700 Subject: [PATCH 46/83] style(ngRepeat): ws and indentation fixes --- src/ng/directive/ngRepeat.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 6bc0e0f3de43..154c715abefe 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -225,6 +225,14 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // jshint bitwise: true }; + var getBlockStart = function(block) { + return block.clone[0]; + }; + + var getBlockEnd = function(block) { + return block.clone[block.clone.length - 1]; + }; + return { restrict: 'A', @@ -292,7 +300,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var lastBlockMap = {}; //watch props - $scope.$watchCollection(rhs, function ngRepeatAction(collection){ + $scope.$watchCollection(rhs, function ngRepeatAction(collection) { var index, length, previousNode = $element[0], // current position of the node nextNode, @@ -343,15 +351,15 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { nextBlockOrder[index] = block; } else if (nextBlockMap.hasOwnProperty(trackById)) { // restore lastBlockMap - forEach(nextBlockOrder, function(block) { + forEach(nextBlockOrder, function (block) { if (block && block.scope) lastBlockMap[block.id] = block; }); // This is a duplicate and we need to throw an error throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", - expression, trackById); + expression, trackById); } else { // new never before seen block - nextBlockOrder[index] = { id: trackById, scope: undefined, clone: undefined }; + nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined}; nextBlockMap[trackById] = false; } } @@ -363,7 +371,9 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { block = lastBlockMap[blockKey]; elementsToRemove = getBlockNodes(block.clone); $animate.leave(elementsToRemove); - forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); + forEach(elementsToRemove, function (element) { + element[NG_REMOVED] = true; + }); block.scope.$destroy(); } } @@ -381,7 +391,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { nextNode = previousNode; do { nextNode = nextNode.nextSibling; - } while(nextNode && nextNode[NG_REMOVED]); + } while (nextNode && nextNode[NG_REMOVED]); if (getBlockStart(block) != nextNode) { // existing item which got moved @@ -411,13 +421,5 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { }; } }; - - function getBlockStart(block) { - return block.clone[0]; - } - - function getBlockEnd(block) { - return block.clone[block.clone.length - 1]; - } }]; From a435415adf1609f92b3b61cbb7c2971327dd2505 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sun, 10 Aug 2014 06:14:16 -0700 Subject: [PATCH 47/83] perf(ngRepeat): use no-proto objects for blockMaps --- src/Angular.js | 16 ++++++++++++++ src/ng/directive/ngRepeat.js | 35 +++++++++++++++---------------- test/ng/directive/ngRepeatSpec.js | 8 ------- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 5ab582866a75..b21f080e1b58 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1559,3 +1559,19 @@ function getBlockNodes(nodes) { return jqLite(blockNodes); } + + +/** + * Creates a new object without a prototype. This object is useful for lookup without having to + * guard against prototypically inherited properties via hasOwnProperty. + * + * Related micro-benchmarks: + * - http://jsperf.com/object-create2 + * - http://jsperf.com/proto-map-lookup/2 + * - http://jsperf.com/for-in-vs-object-keys2 + * + * @returns {Object} + */ +function createMap() { + return Object.create(null); +} diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 154c715abefe..7b1cca704b23 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -297,7 +297,10 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // - scope: bound scope // - element: previous element. // - index: position - var lastBlockMap = {}; + // + // We are using no-proto object so that we don't need to guard against inherited props via + // hasOwnProperty. + var lastBlockMap = createMap(); //watch props $scope.$watchCollection(rhs, function ngRepeatAction(collection) { @@ -306,7 +309,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { nextNode, // Same as lastBlockMap but it has the current state. It will become the // lastBlockMap on the next iteration. - nextBlockMap = {}, + nextBlockMap = createMap(), arrayLength, key, value, // key/value of iteration trackById, @@ -343,39 +346,35 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { key = (collection === collectionKeys) ? index : collectionKeys[index]; value = collection[key]; trackById = trackByIdFn(key, value, index); - assertNotHasOwnProperty(trackById, '`track by` id'); - if (lastBlockMap.hasOwnProperty(trackById)) { + if (lastBlockMap[trackById]) { + // found previously seen block block = lastBlockMap[trackById]; delete lastBlockMap[trackById]; nextBlockMap[trackById] = block; nextBlockOrder[index] = block; - } else if (nextBlockMap.hasOwnProperty(trackById)) { - // restore lastBlockMap + } else if (nextBlockMap[trackById]) { + // id collision detected. restore lastBlockMap and throw an error forEach(nextBlockOrder, function (block) { if (block && block.scope) lastBlockMap[block.id] = block; }); - // This is a duplicate and we need to throw an error throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", expression, trackById); } else { // new never before seen block nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined}; - nextBlockMap[trackById] = false; + nextBlockMap[trackById] = true; } } // remove existing items for (var blockKey in lastBlockMap) { - // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn - if (lastBlockMap.hasOwnProperty(blockKey)) { - block = lastBlockMap[blockKey]; - elementsToRemove = getBlockNodes(block.clone); - $animate.leave(elementsToRemove); - forEach(elementsToRemove, function (element) { - element[NG_REMOVED] = true; - }); - block.scope.$destroy(); - } + block = lastBlockMap[blockKey]; + elementsToRemove = getBlockNodes(block.clone); + $animate.leave(elementsToRemove); + forEach(elementsToRemove, function (element) { + element[NG_REMOVED] = true; + }); + block.scope.$destroy(); } // we are not using forEach for perf reasons (trying to avoid #call) diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index 53c93515b869..aeaeba3849a5 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -174,14 +174,6 @@ describe('ngRepeat', function() { }); - it("should throw an exception if 'track by' evaluates to 'hasOwnProperty'", function() { - scope.items = {age:20}; - $compile('
')(scope); - scope.$digest(); - expect($exceptionHandler.errors.shift().message).toMatch(/ng:badname/); - }); - - it('should track using build in $id function', function() { element = $compile( '
    ' + From 7041d5bd0c4bbf22494c460c83c6b0ce90f7bee9 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sun, 10 Aug 2014 06:29:17 -0700 Subject: [PATCH 48/83] perf(ngRepeat): optimize marking of nodes that are being removed via an animation --- src/ng/directive/ngRepeat.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 7b1cca704b23..141f4980dcd4 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -366,14 +366,18 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { } } - // remove existing items + // remove leftover items for (var blockKey in lastBlockMap) { block = lastBlockMap[blockKey]; elementsToRemove = getBlockNodes(block.clone); $animate.leave(elementsToRemove); - forEach(elementsToRemove, function (element) { - element[NG_REMOVED] = true; - }); + if (elementsToRemove[0].parent) { + // if the element was not removed yet because of pending animation, mark it as deleted + // so that we can ignore it later + for (index = 0, length = elementsToRemove.length; index < length; index++) { + elementsToRemove[index][NG_REMOVED] = true; + } + } block.scope.$destroy(); } From e43541f858de3556eb73af6ebb402942f00ca643 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sun, 10 Aug 2014 07:08:57 -0700 Subject: [PATCH 49/83] perf(Scope): exit $broadcast early if nobody is listening for the given event --- src/ng/rootScope.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index c266d412ea91..975d137aed1e 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -1137,8 +1137,11 @@ function $RootScopeProvider(){ event.defaultPrevented = true; }, defaultPrevented: false - }, - listenerArgs = concat([event], arguments, 1), + }; + + if (!target.$$listenerCount[name]) return event; + + var listenerArgs = concat([event], arguments, 1), listeners, i, length; //down while you can, then up and next sibling or up and next sibling until back at root From 0c3b65a9e867ef72e8f8989c6fda50a96d9e5262 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sun, 10 Aug 2014 07:46:27 -0700 Subject: [PATCH 50/83] perf($parse): use no-proto maps as caches and avoid hasOwnProperty checks --- src/ng/parse.js | 45 +++++++++++++++++--------------------------- test/ng/parseSpec.js | 2 +- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 190a55314de5..cd38c9824804 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -80,7 +80,7 @@ function ensureSafeFunction(obj, fullExpression) { } } -var OPERATORS = { +var OPERATORS = extend(createMap(), { /* jshint bitwise : false */ 'null':function(){return null;}, 'true':function(){return true;}, @@ -118,7 +118,7 @@ var OPERATORS = { // '|':function(self, locals, a,b){return a|b;}, '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));}, '!':function(self, locals, a){return !a(self, locals);} -}; +}); /* jshint bitwise: true */ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; @@ -301,9 +301,10 @@ Lexer.prototype = { text: ident }; - // OPERATORS is our own object so we don't need to use special hasOwnPropertyFn - if (OPERATORS.hasOwnProperty(ident)) { - token.fn = OPERATORS[ident]; + var fn = OPERATORS[ident]; + + if (fn) { + token.fn = fn; token.constant = true; } else { var getter = getterFn(ident, this.options, this.text); @@ -834,7 +835,7 @@ function setter(obj, path, setValue, fullExp) { return setValue; } -var getterFnCache = {}; +var getterFnCache = createMap(); /** * Implementation of the "Black Hole" variant from: @@ -875,16 +876,12 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) { } function getterFn(path, options, fullExp) { - // Check whether the cache has this getter already. - // We can use hasOwnProperty directly on the cache because we ensure, - // see below, that the cache never stores a path called 'hasOwnProperty' - if (getterFnCache.hasOwnProperty(path)) { - return getterFnCache[path]; - } + var fn = getterFnCache[path]; + + if (fn) return fn; var pathKeys = path.split('.'), - pathKeysLength = pathKeys.length, - fn; + pathKeysLength = pathKeys.length; // http://jsperf.com/angularjs-parse-getter/6 if (options.csp) { @@ -923,11 +920,7 @@ function getterFn(path, options, fullExp) { fn = evaledFnGetter; } - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - if (path !== 'hasOwnProperty') { - getterFnCache[path] = fn; - } + getterFnCache[path] = fn; return fn; } @@ -984,7 +977,7 @@ function getterFn(path, options, fullExp) { * service. */ function $ParseProvider() { - var cache = {}; + var cache = createMap(); var $parseOptions = { csp: false @@ -1001,9 +994,9 @@ function $ParseProvider() { case 'string': cacheKey = exp = exp.trim(); - if (cache.hasOwnProperty(cacheKey)) { - parsedExpression = cache[cacheKey]; - } else { + parsedExpression = cache[cacheKey]; + + if (!parsedExpression) { if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { oneTime = true; exp = exp.substring(2); @@ -1016,11 +1009,7 @@ function $ParseProvider() { if (parsedExpression.constant) parsedExpression.$$watchDelegate = constantWatch; else if (oneTime) parsedExpression.$$watchDelegate = oneTimeWatch; - if (cacheKey !== 'hasOwnProperty') { - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - cache[cacheKey] = parsedExpression; - } + cache[cacheKey] = parsedExpression; } return addInterceptor(parsedExpression, interceptorFn); diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index dd2aa555a1a1..409a0be9b029 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -5,7 +5,7 @@ describe('parser', function() { beforeEach(function() { /* global getterFnCache: true */ // clear cache - getterFnCache = {}; + getterFnCache = createMap(); }); From 1da750c473bacc9aeddd14edf9c3fd80be3efb23 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 08:55:50 -0700 Subject: [PATCH 51/83] WIP: TODO notes --- src/Angular.js | 2 ++ src/jqLite.js | 3 +++ src/ng/compile.js | 7 +++++++ src/ng/directive/input.js | 2 ++ src/ng/directive/ngBind.js | 1 + src/ng/directive/ngRepeat.js | 4 ++++ src/ng/directive/ngSwitch.js | 1 + src/ng/httpBackend.js | 1 + src/ng/parse.js | 1 + src/ng/rootScope.js | 7 +++++++ 10 files changed, 29 insertions(+) diff --git a/src/Angular.js b/src/Angular.js index b21f080e1b58..d604909dddac 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -440,6 +440,7 @@ function isDefined(value){return typeof value !== 'undefined';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. */ +// TODO(perf): use !== null to avoid conversion??? function isObject(value){return value != null && typeof value === 'object';} @@ -832,6 +833,7 @@ function shallowCopy(src, dst) { if (isArray(src)) { dst = dst || []; + // TODO(perf): how about slice? for (l = src.length; i < l; i++) { dst[i] = src[i]; } diff --git a/src/jqLite.js b/src/jqLite.js index affff2355040..482142973887 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -253,8 +253,10 @@ function jqLiteClone(element) { function jqLiteDealoc(element, onlyDescendants){ if (!onlyDescendants) jqLiteRemoveData(element); + // TODO(perf): do we need to check length here? if so, cache childNodes if (element.childNodes && element.childNodes.length) { // we use querySelectorAll because documentFragments don't have getElementsByTagName + // TODO(perf): since we need to slice the live list, shoudn't we just use querySelectorAll all the time? var descendants = element.getElementsByTagName ? sliceArgs(element.getElementsByTagName('*')) : element.querySelectorAll ? element.querySelectorAll('*') : []; for (var i = 0, l = descendants.length; i < l; i++) { @@ -307,6 +309,7 @@ function jqLiteRemoveData(element, name) { } jqLiteOff(element); } + // TODO(perf): delete vs set to undefined? delete jqCache[expandoId]; element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it } diff --git a/src/ng/compile.js b/src/ng/compile.js index b4724f469c8e..59e56b1ed502 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -911,6 +911,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { : $compileNodes; if (transcludeControllers) { + // TODO(perf): use Object.create(null) for transcludeControllers and then `for in` var names = Object.keys(transcludeControllers); var i = names.length; var name; @@ -1061,6 +1062,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn); if (scopeCreated) { + // TODO(perf): do we really need this? looks bogus + tries to registers listeners on + // boundary comment needs in repeater clone.on('$destroy', function() { transcludedScope.$destroy(); }); } return clone; @@ -1670,6 +1673,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var transcludeControllers; // no scope passed + // TODO(perf): arguments.length is slow, check if cloneAttachFn is truthy instead + // http://jsperf.com/isundefined-vs-arguments-length if (arguments.length < 2) { cloneAttachFn = scope; scope = undefined; @@ -2075,6 +2080,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (parent) { parent.replaceChild(newNode, firstElementToRemove); } + + // TODO(perf): what are we doing with this document fragment? could we return it for cloning? var fragment = document.createDocumentFragment(); fragment.appendChild(firstElementToRemove); newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index f0acb2739d4a..03ac144daf3f 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1188,6 +1188,7 @@ function radioInputType(scope, element, attr, ctrl) { ctrl.$render = function() { var value = attr.value; + // TODO(perf): update checked only when match since only one radio button can be checked at a time? element[0].checked = (value == ctrl.$viewValue); }; @@ -1966,6 +1967,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ modelValue = modelValue(); } + // TODO(perf): why not move this to the action fn? // if scope model value and ngModel value are out of sync if (ctrl.$modelValue !== modelValue && (isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) { diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js index c0a7408a1f7a..017c0246af86 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -59,6 +59,7 @@ var ngBindDirective = ['$compile', function($compile) { // We are purposefully using == here rather than === because we want to // catch when value is "null or undefined" // jshint -W041 + // TODO(perf): consider setting textContent directly element.text(value == undefined ? '' : value); }); } diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 141f4980dcd4..4ba94021c9d4 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -341,6 +341,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { arrayLength = collectionKeys.length; // locate existing items + // TODO(perf): don't reset nextBlockOrder.length ??? length = nextBlockOrder.length = collectionKeys.length; for (index = 0; index < length; index++) { key = (collection === collectionKeys) ? index : collectionKeys[index]; @@ -386,6 +387,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { key = (collection === collectionKeys) ? index : collectionKeys[index]; value = collection[key]; block = nextBlockOrder[index]; + // TODO(perf): simplify previousBlockBoundaryNode calculation if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]); if (block.scope) { @@ -406,8 +408,10 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // new item which we don't know about $transclude(function ngRepeatTransclude(clone, scope) { block.scope = scope; + // TODO(perf): could we move this into the template element and have it cloned with it? // http://jsperf.com/clone-vs-createcomment clone[clone.length++] = ngRepeatEndComment.cloneNode(); + // TODO(perf): support naked previousNode in `enter`? $animate.enter(clone, null, jqLite(previousNode)); previousNode = clone; // Note: We only need the first/last node of the cloned nodes. diff --git a/src/ng/directive/ngSwitch.js b/src/ng/directive/ngSwitch.js index 93e5bfe75f6e..dcb0ff43d707 100644 --- a/src/ng/directive/ngSwitch.js +++ b/src/ng/directive/ngSwitch.js @@ -156,6 +156,7 @@ var ngSwitchDirective = ['$animate', function($animate) { selectedScopes[i].$destroy(); previousElements[i] = selected; $animate.leave(selected, function() { + // TODO(perf): this schedules rAF even without animations which dealocs the elements again via previousElements[i].remove() above; previousElements.splice(i, 1); }); } diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js index 78d04cfa24b9..e119d38e4934 100644 --- a/src/ng/httpBackend.js +++ b/src/ng/httpBackend.js @@ -1,6 +1,7 @@ 'use strict'; function createXhr(method) { + // TODO(ie): remove //if IE and the method is not RFC2616 compliant, or if XMLHttpRequest //is not available, try getting an ActiveXObject. Otherwise, use XMLHttpRequest //if it is available diff --git a/src/ng/parse.js b/src/ng/parse.js index cd38c9824804..4b2fd319a25e 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -308,6 +308,7 @@ Lexer.prototype = { token.constant = true; } else { var getter = getterFn(ident, this.options, this.text); + // TODO(perf): consider exposing the getter reference token.fn = extend(function(self, locals) { return (getter(self, locals)); }, { diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 975d137aed1e..f804763b44ea 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -529,6 +529,7 @@ function $RootScopeProvider(){ oldValue.length = oldLength = newLength; } // copy the items to oldValue and look for changes. + // TODO(perf): consider caching oldValue[i] and newValue[i] for (var i = 0; i < newLength; i++) { bothNaN = (oldValue[i] !== oldValue[i]) && (newValue[i] !== newValue[i]); @@ -540,6 +541,7 @@ function $RootScopeProvider(){ } else { if (oldValue !== internalObject) { // we are transitioning from something which was not an object into object. + // TODO(perf): Object.create(null) + don't use hasOwnProperty for oldValue oldValue = internalObject = {}; oldLength = 0; changeDetected++; @@ -700,7 +702,10 @@ function $RootScopeProvider(){ watch = watchers[length]; // Most common watches are on primitives, in which case we can short // circuit it with === operator, only when === fails do we use .equals + // TODO(perf): is this if needed? if (watch) { + // TODO(perf): the last typeof number + NaN checks are executed for all counter watches + // can we skip it or at least remember if the lastValue is NaN if ((value = watch.get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) @@ -709,6 +714,7 @@ function $RootScopeProvider(){ dirty = true; lastDirtyWatch = watch; watch.last = watch.eq ? copy(value, null) : value; + // TODO(perf): initialize all watches via async queue? watch.fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; @@ -811,6 +817,7 @@ function $RootScopeProvider(){ this.$$destroyed = true; if (this === $rootScope) return; + // TODO(perf): $$listenerCount should be Object.create(null), then use `for in` forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); // sever all the references to parent scopes (after this cleanup, the current scope should From 4adb9021e4b8ce632b9246090e0df1f3f421a817 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 11:06:06 -0700 Subject: [PATCH 52/83] WIP: remove isObject todo --- src/Angular.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Angular.js b/src/Angular.js index d604909dddac..635070324046 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -440,7 +440,6 @@ function isDefined(value){return typeof value !== 'undefined';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. */ -// TODO(perf): use !== null to avoid conversion??? function isObject(value){return value != null && typeof value === 'object';} From 4eb05218ceb86f87245e2f42f3cfa229a062b24a Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 11:26:53 -0700 Subject: [PATCH 53/83] perf(isObject): use strict comparison this is a micro-optimization based on http://jsperf.com/isobject4 no significant improvement in macro-benchmarks, but since it makes the code better it makes sense making this change. --- src/Angular.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Angular.js b/src/Angular.js index 635070324046..4846dd376670 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -440,7 +440,10 @@ function isDefined(value){return typeof value !== 'undefined';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. */ -function isObject(value){return value != null && typeof value === 'object';} +function isObject(value){ + // http://jsperf.com/isobject4 + return value !== null && typeof value === 'object'; +} /** From d72bb6b96acc8f835b534686e6d3efd3ca090748 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 18:04:57 -0700 Subject: [PATCH 54/83] WIP: remove todo --- src/ng/directive/ngBind.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js index 017c0246af86..c0a7408a1f7a 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -59,7 +59,6 @@ var ngBindDirective = ['$compile', function($compile) { // We are purposefully using == here rather than === because we want to // catch when value is "null or undefined" // jshint -W041 - // TODO(perf): consider setting textContent directly element.text(value == undefined ? '' : value); }); } From 6e588114baac9e5f8b0fb753da8e6945cd0be743 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 18:05:45 -0700 Subject: [PATCH 55/83] perf(ngBind): bypass jquery/jqlite when setting text --- src/ng/directive/ngBind.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js index c0a7408a1f7a..8ecebec3f8b7 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -55,11 +55,13 @@ var ngBindDirective = ['$compile', function($compile) { return { link: function ngBindLink(scope, element, attr) { $compile.addBindingInfo(element, attr.ngBind); + element = element[0]; + scope.$watch(attr.ngBind, function ngBindWatchAction(value) { // We are purposefully using == here rather than === because we want to // catch when value is "null or undefined" // jshint -W041 - element.text(value == undefined ? '' : value); + element.textContent = (value == undefined ? '' : value); }); } }; From 14d05e30b8ae477e1b315a8b0339d4a92abc4bfe Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 18:25:16 -0700 Subject: [PATCH 56/83] WIP: remove TODO --- src/ng/rootScope.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index f804763b44ea..b896e37d709b 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -529,7 +529,6 @@ function $RootScopeProvider(){ oldValue.length = oldLength = newLength; } // copy the items to oldValue and look for changes. - // TODO(perf): consider caching oldValue[i] and newValue[i] for (var i = 0; i < newLength; i++) { bothNaN = (oldValue[i] !== oldValue[i]) && (newValue[i] !== newValue[i]); From 6840a5376c8158eb059ca5578cb6010fb9d50ac1 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 18:24:42 -0700 Subject: [PATCH 57/83] perf(Scope): watchCollection optimization tiny ~4ms improvement and code that minifies better --- src/ng/rootScope.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index b896e37d709b..cc68fb358536 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -506,7 +506,7 @@ function $RootScopeProvider(){ function $watchCollectionInterceptor(_value) { newValue = _value; - var newLength, key, bothNaN; + var newLength, key, bothNaN, newItem, oldItem; if (!isObject(newValue)) { // if primitive if (oldValue !== newValue) { @@ -530,11 +530,13 @@ function $RootScopeProvider(){ } // copy the items to oldValue and look for changes. for (var i = 0; i < newLength; i++) { - bothNaN = (oldValue[i] !== oldValue[i]) && - (newValue[i] !== newValue[i]); - if (!bothNaN && (oldValue[i] !== newValue[i])) { + oldItem = oldValue[i]; + newItem = newValue[i]; + + bothNaN = (oldItem !== oldItem) && (newItem !== newItem); + if (!bothNaN && (oldItem !== newItem)) { changeDetected++; - oldValue[i] = newValue[i]; + oldValue[i] = newItem; } } } else { From 55b24e4bcd7e1d740d42bb4234f6f865571f4f56 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 18:28:11 -0700 Subject: [PATCH 58/83] WIP: remove todo we need the check because of listener deregistration --- src/ng/rootScope.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index cc68fb358536..4524a19efd09 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -703,7 +703,6 @@ function $RootScopeProvider(){ watch = watchers[length]; // Most common watches are on primitives, in which case we can short // circuit it with === operator, only when === fails do we use .equals - // TODO(perf): is this if needed? if (watch) { // TODO(perf): the last typeof number + NaN checks are executed for all counter watches // can we skip it or at least remember if the lastValue is NaN From c570768f288f78ad852804c7d17f5c3cab6d1469 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 18:49:08 -0700 Subject: [PATCH 59/83] WIP: remove todo --- src/ng/rootScope.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 4524a19efd09..358e9425ae31 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -817,7 +817,6 @@ function $RootScopeProvider(){ this.$$destroyed = true; if (this === $rootScope) return; - // TODO(perf): $$listenerCount should be Object.create(null), then use `for in` forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); // sever all the references to parent scopes (after this cleanup, the current scope should From a61bdf0081aa3d975ed3c82f45b0f41934514835 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 18:53:08 -0700 Subject: [PATCH 60/83] perf(Scope): don't use forEach in doesn't make any significant impact on our current benchmarks because we don't have benchmarks with many scope events, but this is a straightforward change worth doing --- src/ng/rootScope.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 358e9425ae31..563349482a4b 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -817,7 +817,9 @@ function $RootScopeProvider(){ this.$$destroyed = true; if (this === $rootScope) return; - forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); + for (var eventName in this.$$listenerCount) { + decrementListenerCount(this, this.$$listenerCount[eventName], eventName); + } // sever all the references to parent scopes (after this cleanup, the current scope should // not be retained by any of our references and should be eligible for garbage collection) From 85c3d5485474d8d7e2e1a658ad2c8619f8395aab Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 18:55:51 -0700 Subject: [PATCH 61/83] WIP: remove todo --- src/ng/rootScope.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 563349482a4b..8b0937458a6f 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -542,7 +542,6 @@ function $RootScopeProvider(){ } else { if (oldValue !== internalObject) { // we are transitioning from something which was not an object into object. - // TODO(perf): Object.create(null) + don't use hasOwnProperty for oldValue oldValue = internalObject = {}; oldLength = 0; changeDetected++; From 6621806802c2065feb28025a5d444a8c9ae039dc Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 19:08:55 -0700 Subject: [PATCH 62/83] perf(Scope): optimize $watchCollection when used for watching objects Since we control the oldValue, we don't need to worry about proto-inhereted properties which means we can use 'for in' and skip hasOwnProperty checks. http://jsperf.com/for-in-vs-object-keys2 --- src/ng/rootScope.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 8b0937458a6f..feb692af8e36 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -551,16 +551,18 @@ function $RootScopeProvider(){ for (key in newValue) { if (newValue.hasOwnProperty(key)) { newLength++; - if (oldValue.hasOwnProperty(key)) { - bothNaN = (oldValue[key] !== oldValue[key]) && - (newValue[key] !== newValue[key]); - if (!bothNaN && (oldValue[key] !== newValue[key])) { + newItem = newValue[key]; + oldItem = oldValue[key]; + + if (key in oldValue) { + bothNaN = (oldItem !== oldItem) && (newItem !== newItem); + if (!bothNaN && (oldItem !== newItem)) { changeDetected++; - oldValue[key] = newValue[key]; + oldValue[key] = newItem; } } else { oldLength++; - oldValue[key] = newValue[key]; + oldValue[key] = newItem; changeDetected++; } } @@ -569,7 +571,7 @@ function $RootScopeProvider(){ // we used to have more keys, need to find them and destroy them. changeDetected++; for(key in oldValue) { - if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) { + if (!newValue.hasOwnProperty(key)) { oldLength--; delete oldValue[key]; } From e1df48869aed8c10f0dba37ee457f0b6a09adaaa Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 19:11:27 -0700 Subject: [PATCH 63/83] WIP: remove todo --- src/ng/compile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 59e56b1ed502..792944082cd2 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -911,7 +911,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { : $compileNodes; if (transcludeControllers) { - // TODO(perf): use Object.create(null) for transcludeControllers and then `for in` var names = Object.keys(transcludeControllers); var i = names.length; var name; From 202f1d7f71de463709e10bf8b7040ffb3b0aaf98 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 11 Aug 2014 21:06:52 -0700 Subject: [PATCH 64/83] perf($compile): refactor publicLinkFn to simplify the code and use 'for in' loop --- src/ng/compile.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 792944082cd2..0a1e96245dbd 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -911,13 +911,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { : $compileNodes; if (transcludeControllers) { - var names = Object.keys(transcludeControllers); - var i = names.length; - var name; - - while (i--) { - name = names[i]; - $linkNode.data('$' + name + 'Controller', transcludeControllers[name]); + for (var controllerName in transcludeControllers) { + $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName]); } } From b7c4e358a87214f7f63e922a36c92acf832731d0 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 09:25:10 -0700 Subject: [PATCH 65/83] chore(angular.suffix): fix typo --- src/angular.suffix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular.suffix b/src/angular.suffix index 9429d4fcf3d2..4baa20e1b0b6 100644 --- a/src/angular.suffix +++ b/src/angular.suffix @@ -4,7 +4,7 @@ return; } - //try to bind to jquery now so that one can write angular.element().read() + //try to bind to jquery now so that one can write jqLite(document).ready() //but we will rebind on bootstrap again. bindJQuery(); From 13b6a3a0ba31f91f392f91000b2fc35f1e30c11b Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 11:27:59 -0700 Subject: [PATCH 66/83] WIP: remove TODO --- src/ng/compile.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 0a1e96245dbd..7641bf9b7c3d 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1667,8 +1667,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var transcludeControllers; // no scope passed - // TODO(perf): arguments.length is slow, check if cloneAttachFn is truthy instead - // http://jsperf.com/isundefined-vs-arguments-length if (arguments.length < 2) { cloneAttachFn = scope; scope = undefined; From 1fbc863fa297a540f966a8f5dfee178b24f3126b Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 11:43:50 -0700 Subject: [PATCH 67/83] refactor($compile): simplify controllersBoundTransclude check no measurable perf difference, but the code more simple and minifies better --- src/ng/compile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 7641bf9b7c3d..4134d866e524 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1667,7 +1667,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var transcludeControllers; // no scope passed - if (arguments.length < 2) { + if (!cloneAttachFn) { cloneAttachFn = scope; scope = undefined; } From fdb61e8be0a382f843799533bdf2d70aaf330a14 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 11:57:02 -0700 Subject: [PATCH 68/83] WIP: remove todo --- src/Angular.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Angular.js b/src/Angular.js index 4846dd376670..6e10b231259a 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -835,7 +835,6 @@ function shallowCopy(src, dst) { if (isArray(src)) { dst = dst || []; - // TODO(perf): how about slice? for (l = src.length; i < l; i++) { dst[i] = src[i]; } From a96abf6fbec25e83f325acb1eed796603bfc0181 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 12:34:21 -0700 Subject: [PATCH 69/83] perf(jqLite): optimize event handler --- src/jqLite.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index 482142973887..fc4afd783d2e 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -685,11 +685,18 @@ function createEventHandler(element, events) { return event.defaultPrevented; }; + var eventFns = events[type || event.type]; + var eventFnsLength = eventFns ? eventFns.length : 0; + + if (!eventFnsLength) return; + // Copy event handlers in case event handlers array is modified during execution. - var eventHandlersCopy = shallowCopy(events[type || event.type] || []); + if ((eventFnsLength > 1)) { + eventFns = shallowCopy(eventFns); + } - for (var i = 0, ii = eventHandlersCopy.length; i < ii; i++) { - eventHandlersCopy[i].call(element, event); + for (var i = 0; i < eventFnsLength; i++) { + eventFns[i].call(element, event); } }; From b22badea69c804668698a1dc328fba9daec6612b Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 13:24:57 -0700 Subject: [PATCH 70/83] WIP: bp - add profile button --- perf/apps/largetable-bp/bp.js | 9 +++++++++ perf/apps/largetable-bp/index.html | 1 + 2 files changed, 10 insertions(+) diff --git a/perf/apps/largetable-bp/bp.js b/perf/apps/largetable-bp/bp.js index 66663ba4a72c..79ce33e7cb2a 100644 --- a/perf/apps/largetable-bp/bp.js +++ b/perf/apps/largetable-bp/bp.js @@ -120,6 +120,14 @@ bp.Runner.runAllTests = function (done) { } }; + +bp.Runner.profile = function() { + console.profile(); + bp.Runner.onceBenchmark(); + console.profileEnd(); +}; + + bp.Runner.runTimedTest = function (bs) { var startTime, endTime, @@ -301,6 +309,7 @@ bp.Document.onDOMContentLoaded = function() { bp.Document.addButton('loopBtn', bp.Runner.loopBenchmark); bp.Document.addButton('onceBtn', bp.Runner.onceBenchmark); bp.Document.addButton('twentyFiveBtn', bp.Runner.twentyFiveBenchmark); + bp.Document.addButton('profileBtn', bp.Runner.profile); bp.Document.addSampleRange(); bp.Document.addInfo(); }; diff --git a/perf/apps/largetable-bp/index.html b/perf/apps/largetable-bp/index.html index 9ff2f79d220d..259666323f3c 100644 --- a/perf/apps/largetable-bp/index.html +++ b/perf/apps/largetable-bp/index.html @@ -38,6 +38,7 @@ +
    From 71d67ee03e734d94c68d23350b3f2dffa890cf62 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 16:58:11 -0700 Subject: [PATCH 71/83] WIP: remove todo --- src/ng/compile.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 4134d866e524..7c918e048d9f 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1056,8 +1056,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn); if (scopeCreated) { - // TODO(perf): do we really need this? looks bogus + tries to registers listeners on - // boundary comment needs in repeater clone.on('$destroy', function() { transcludedScope.$destroy(); }); } return clone; From 8d1d720b18f347a042779caa2065b5a18f30ed2a Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 17:00:50 -0700 Subject: [PATCH 72/83] perf($compile): don't register $destroy callbacks on element-transcluded nodes This is a major perf win in the large table benchmark (~100ms or 9). This cleanup is needed only for regular transclusion because only then the DOM hierarchy doesn't match scope hierarchy (transcluded scope is a child of the parent scope and not a child of the isolate scope) We should consider refactoring this further for the case of regular transclusion and consider using scope events instead. --- src/ng/compile.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 7c918e048d9f..8e5a2d9ed355 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1022,7 +1022,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if ( nodeLinkFn.transcludeOnThisElement ) { - childBoundTranscludeFn = createBoundTranscludeFn(scope, nodeLinkFn.transclude, parentBoundTranscludeFn); + childBoundTranscludeFn = createBoundTranscludeFn( + scope, nodeLinkFn.transclude, parentBoundTranscludeFn, + nodeLinkFn.elementTranscludeOnThisElement); } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { childBoundTranscludeFn = parentBoundTranscludeFn; @@ -1043,7 +1045,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) { var boundTranscludeFn = function(transcludedScope, cloneFn, controllers) { var scopeCreated = false; @@ -1055,7 +1057,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn); - if (scopeCreated) { + if (scopeCreated && !elementTransclusion) { clone.on('$destroy', function() { transcludedScope.$destroy(); }); } return clone; @@ -1432,6 +1434,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective; + nodeLinkFn.elementTranscludeOnThisElement = hasElementTranscludeDirective; nodeLinkFn.templateOnThisElement = hasTemplate; nodeLinkFn.transclude = childTranscludeFn; From d7e750d386acc2bd162743039512057885e8a5a2 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 21:34:11 -0700 Subject: [PATCH 73/83] WIP: remove todo --- src/jqLite.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index fc4afd783d2e..53c15069196c 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -253,10 +253,8 @@ function jqLiteClone(element) { function jqLiteDealoc(element, onlyDescendants){ if (!onlyDescendants) jqLiteRemoveData(element); - // TODO(perf): do we need to check length here? if so, cache childNodes if (element.childNodes && element.childNodes.length) { // we use querySelectorAll because documentFragments don't have getElementsByTagName - // TODO(perf): since we need to slice the live list, shoudn't we just use querySelectorAll all the time? var descendants = element.getElementsByTagName ? sliceArgs(element.getElementsByTagName('*')) : element.querySelectorAll ? element.querySelectorAll('*') : []; for (var i = 0, l = descendants.length; i < l; i++) { From d70e1f4e23a7bf2e2487f32ec0e040b13bc128d3 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 21:37:50 -0700 Subject: [PATCH 74/83] perf(jqLite): simplify jqLiteDealoc while querySelectorAll is much more expensive than getElementsByTagName on elements with both many and few children, cloning the live node list returned by getElementsByTagName makes it as expensive as querySelectorAll (we need to clone because we need the node list not to change while we iterate over it). the childNodes and childNodes.length check is as expensive as querySelectorAll on a node without any children, so it only makes the whole lookup 2x as slow, so I'm removing it. --- src/jqLite.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index 53c15069196c..c9096e73b659 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -253,10 +253,8 @@ function jqLiteClone(element) { function jqLiteDealoc(element, onlyDescendants){ if (!onlyDescendants) jqLiteRemoveData(element); - if (element.childNodes && element.childNodes.length) { - // we use querySelectorAll because documentFragments don't have getElementsByTagName - var descendants = element.getElementsByTagName ? sliceArgs(element.getElementsByTagName('*')) : - element.querySelectorAll ? element.querySelectorAll('*') : []; + if (element.querySelectorAll) { + var descendants = element.querySelectorAll('*'); for (var i = 0, l = descendants.length; i < l; i++) { jqLiteRemoveData(descendants[i]); } From d39b6abcb54fa1868c1be34980735093796cf381 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 22:11:39 -0700 Subject: [PATCH 75/83] WIP: remove todo --- src/ng/directive/ngRepeat.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 4ba94021c9d4..59cfe0a5bbc2 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -341,7 +341,6 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { arrayLength = collectionKeys.length; // locate existing items - // TODO(perf): don't reset nextBlockOrder.length ??? length = nextBlockOrder.length = collectionKeys.length; for (index = 0; index < length; index++) { key = (collection === collectionKeys) ? index : collectionKeys[index]; From b991b23b920506584324b4ee4200eaaf480145db Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 22:12:53 -0700 Subject: [PATCH 76/83] perf(ngRepeat): simplify code and remove duplicate array.length access minimal perf gain (~2ms) --- src/ng/directive/ngRepeat.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 59cfe0a5bbc2..ce908504dd0b 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -310,13 +310,13 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // Same as lastBlockMap but it has the current state. It will become the // lastBlockMap on the next iteration. nextBlockMap = createMap(), - arrayLength, + collectionLength, key, value, // key/value of iteration trackById, trackByIdFn, collectionKeys, block, // last object information {scope, element, id} - nextBlockOrder = [], + nextBlockOrder, elementsToRemove; if (aliasAs) { @@ -338,11 +338,11 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { collectionKeys.sort(); } - arrayLength = collectionKeys.length; + collectionLength = collectionKeys.length; + nextBlockOrder = new Array(collectionLength); // locate existing items - length = nextBlockOrder.length = collectionKeys.length; - for (index = 0; index < length; index++) { + for (index = 0; index < collectionLength; index++) { key = (collection === collectionKeys) ? index : collectionKeys[index]; value = collection[key]; trackById = trackByIdFn(key, value, index); @@ -382,7 +382,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { } // we are not using forEach for perf reasons (trying to avoid #call) - for (index = 0, length = collectionKeys.length; index < length; index++) { + for (index = 0; index < collectionLength; index++) { key = (collection === collectionKeys) ? index : collectionKeys[index]; value = collection[key]; block = nextBlockOrder[index]; @@ -402,7 +402,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode)); } previousNode = getBlockEnd(block); - updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength); + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); } else { // new item which we don't know about $transclude(function ngRepeatTransclude(clone, scope) { @@ -418,7 +418,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // by a directive with templateUrl when its template arrives. block.clone = clone; nextBlockMap[block.id] = block; - updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength); + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); }); } } From dd089c5bb1d0c0c5632bb34b37c9047ca65613c5 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 22:16:50 -0700 Subject: [PATCH 77/83] chore(ngRepeat): fix typo in a comment --- src/ng/directive/ngRepeat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index ce908504dd0b..dceac4c36dab 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -353,7 +353,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { nextBlockMap[trackById] = block; nextBlockOrder[index] = block; } else if (nextBlockMap[trackById]) { - // id collision detected. restore lastBlockMap and throw an error + // if collision detected. restore lastBlockMap and throw an error forEach(nextBlockOrder, function (block) { if (block && block.scope) lastBlockMap[block.id] = block; }); From fc472805d477c6519a0ee0ff6e69509332ec21e2 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 23:43:59 -0700 Subject: [PATCH 78/83] chore(ngRepeat): improve inline comments --- src/ng/directive/ngRepeat.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index dceac4c36dab..baead524abc2 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -305,7 +305,8 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { //watch props $scope.$watchCollection(rhs, function ngRepeatAction(collection) { var index, length, - previousNode = $element[0], // current position of the node + previousNode = $element[0], // node that cloned nodes should be inserted after + // initialized to the comment node anchor nextNode, // Same as lastBlockMap but it has the current state. It will become the // lastBlockMap on the next iteration. @@ -392,7 +393,10 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { if (block.scope) { // if we have already seen this object, then we need to reuse the // associated scope/element + nextNode = previousNode; + + // skip nodes that are already pending removal via leave animation do { nextNode = nextNode.nextSibling; } while (nextNode && nextNode[NG_REMOVED]); From 4db25d99a638d54bc04205abbe961924724c66af Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 23:44:25 -0700 Subject: [PATCH 79/83] WIP: remove todo --- src/ng/directive/ngRepeat.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index baead524abc2..ce46ae4e62c9 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -387,7 +387,6 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { key = (collection === collectionKeys) ? index : collectionKeys[index]; value = collection[key]; block = nextBlockOrder[index]; - // TODO(perf): simplify previousBlockBoundaryNode calculation if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]); if (block.scope) { From 6275de69ec0b2b5d236dbf32bafca4307f4f1090 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 12 Aug 2014 23:46:25 -0700 Subject: [PATCH 80/83] refactor(ngRepeat): simplify previousNode boundary calculation the previousNode was almost always correct except when we added a new block in which case incorrectly assigned the cloned collection to the variable instead of the end comment node. --- src/ng/directive/ngRepeat.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index ce46ae4e62c9..956b9a8877c7 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -387,7 +387,6 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { key = (collection === collectionKeys) ? index : collectionKeys[index]; value = collection[key]; block = nextBlockOrder[index]; - if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]); if (block.scope) { // if we have already seen this object, then we need to reuse the @@ -412,10 +411,11 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { block.scope = scope; // TODO(perf): could we move this into the template element and have it cloned with it? // http://jsperf.com/clone-vs-createcomment - clone[clone.length++] = ngRepeatEndComment.cloneNode(); + var endNode = ngRepeatEndComment.cloneNode(); + clone[clone.length++] = endNode; // TODO(perf): support naked previousNode in `enter`? $animate.enter(clone, null, jqLite(previousNode)); - previousNode = clone; + previousNode = endNode; // Note: We only need the first/last node of the cloned nodes. // However, we need to keep the reference to the jqlite wrapper as it might be changed later // by a directive with templateUrl when its template arrives. From 9f115bd2de240d3ba61db81b6330277a9ce8f37c Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Wed, 13 Aug 2014 00:16:33 -0700 Subject: [PATCH 81/83] refactor($compile): automatically append end comment nodes to all element-transclusion templates Previously we would do it manually in all of our structural directives. BREAKING CHANGE: element-transcluded directives now have an extra comment automatically appended to their cloned DOM This comment is usually needed to keep track the end boundary in the event child directives modify the root node(s). If not used for this purpose it can be safely ignored. --- src/ng/compile.js | 3 +++ src/ng/directive/ngIf.js | 1 - src/ng/directive/ngRepeat.js | 7 +------ src/ng/directive/ngSwitch.js | 1 - test/ng/compileSpec.js | 3 ++- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 8e5a2d9ed355..52a9e5c8383b 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1327,6 +1327,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { compileNode = $compileNode[0]; replaceWith(jqCollection, sliceArgs($template), compileNode); + $template[$template.length++] = document.createComment(' end ' + directiveName + ': ' + + templateAttrs[directiveName] + ' '); + childTranscludeFn = compile($template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { // Don't pass in: diff --git a/src/ng/directive/ngIf.js b/src/ng/directive/ngIf.js index 104423b4d3ca..281435d0a0dd 100644 --- a/src/ng/directive/ngIf.js +++ b/src/ng/directive/ngIf.js @@ -92,7 +92,6 @@ var ngIfDirective = ['$animate', function($animate) { if (!childScope) { $transclude(function (clone, newScope) { childScope = newScope; - clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); // Note: We only need the first/last node of the cloned nodes. // However, we need to keep the reference to the jqlite wrapper as it might be changed later // by a directive with templateUrl when its template arrives. diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 956b9a8877c7..4d4db22d5c58 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -243,8 +243,6 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { $$tlb: true, compile: function ngRepeatCompile($element, $attr) { var expression = $attr.ngRepeat; - var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' '); - var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); if (!match) { @@ -410,12 +408,9 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { $transclude(function ngRepeatTransclude(clone, scope) { block.scope = scope; // TODO(perf): could we move this into the template element and have it cloned with it? - // http://jsperf.com/clone-vs-createcomment - var endNode = ngRepeatEndComment.cloneNode(); - clone[clone.length++] = endNode; // TODO(perf): support naked previousNode in `enter`? $animate.enter(clone, null, jqLite(previousNode)); - previousNode = endNode; + previousNode = clone[clone.length - 1]; // Note: We only need the first/last node of the cloned nodes. // However, we need to keep the reference to the jqlite wrapper as it might be changed later // by a directive with templateUrl when its template arrives. diff --git a/src/ng/directive/ngSwitch.js b/src/ng/directive/ngSwitch.js index dcb0ff43d707..ab4351c8529e 100644 --- a/src/ng/directive/ngSwitch.js +++ b/src/ng/directive/ngSwitch.js @@ -170,7 +170,6 @@ var ngSwitchDirective = ['$animate', function($animate) { selectedTransclude.transclude(function(caseElement, selectedScope) { selectedScopes.push(selectedScope); var anchor = selectedTransclude.element; - caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: '); var block = { clone: caseElement }; selectedElements.push(block); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 1822d9046a9d..2e4fd09e9805 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -5041,9 +5041,10 @@ describe('$compile', function() { "inner:#comment:innerAgain:" ]); expect(child.length).toBe(1); - expect(child.contents().length).toBe(2); + expect(child.contents().length).toBe(3); expect(lowercase(nodeName_(child.contents().eq(0)))).toBe('#comment'); expect(lowercase(nodeName_(child.contents().eq(1)))).toBe('div'); + expect(lowercase(nodeName_(child.contents().eq(2)))).toBe('#comment'); }); }); }); From a8696b69d5252146f5c56bdb375fff4b506bdd6a Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Wed, 13 Aug 2014 16:02:21 -0700 Subject: [PATCH 82/83] refactor(jqLite): simplify jqLiteAddNodes when multiple nodes are being added This branch is not as hot as the single node branch and the slice that we need for old webkit/phantom makes for loop preferable. --- src/jqLite.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index c9096e73b659..1588eeeb634c 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -400,11 +400,9 @@ function jqLiteAddNodes(root, elements) { // if an Array or NodeList and not a Window if (typeof length === 'number' && elements.window !== elements) { if (length) { - if (elements.item) { - // convert NodeList to an Array to make PhantomJS 1.x happy - elements = slice.call(elements); + for (var i = 0; i < length; i++) { + root[root.length++] = elements[i]; } - push.apply(root, elements); } } else { root[root.length++] = elements; From 768bcc5fc12ab02b268fb2c6a982b444816bbd1d Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Wed, 13 Aug 2014 16:33:10 -0700 Subject: [PATCH 83/83] WIP: remove todo the comment was moved to template but we can't clone the template via document fragment because for 1-5 elements document fragment is only an extra overhead. It only starts to make things faster when we have 10 or more root elements to clone and append --- src/ng/directive/ngRepeat.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 4d4db22d5c58..e14a045abd35 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -407,7 +407,6 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { // new item which we don't know about $transclude(function ngRepeatTransclude(clone, scope) { block.scope = scope; - // TODO(perf): could we move this into the template element and have it cloned with it? // TODO(perf): support naked previousNode in `enter`? $animate.enter(clone, null, jqLite(previousNode)); previousNode = clone[clone.length - 1];