├── .gitattributes ├── .gitignore ├── .npmignore ├── README.md ├── bower.json ├── index.html ├── jquery-observe.js ├── package.json └── test ├── benchmark.js ├── helper.js ├── integration.html ├── integration.js └── performance.html /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | #packages/ 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | 76 | # Visual Studio profiler 77 | *.psess 78 | *.vsp 79 | 80 | # ReSharper is a .NET coding add-in 81 | _ReSharper* 82 | 83 | # Installshield output folder 84 | [Ee]xpress 85 | 86 | # DocProject is a documentation generator add-in 87 | DocProject/buildhelp/ 88 | DocProject/Help/*.HxT 89 | DocProject/Help/*.HxC 90 | DocProject/Help/*.hhc 91 | DocProject/Help/*.hhk 92 | DocProject/Help/*.hhp 93 | DocProject/Help/Html2 94 | DocProject/Help/html 95 | 96 | # Click-Once directory 97 | publish 98 | 99 | # Others 100 | [Bb]in 101 | [Oo]bj 102 | sql 103 | TestResults 104 | *.Cache 105 | ClientBin 106 | stylecop.* 107 | ~$* 108 | *.dbmdl 109 | Generated_Code #added for RIA/Silverlight projects 110 | 111 | # Backup & report files from converting an old project file to a newer 112 | # Visual Studio version. Backup files are not needed, because we have git ;-) 113 | _UpgradeReport_Files/ 114 | Backup*/ 115 | UpgradeLog*.XML 116 | 117 | 118 | 119 | ############ 120 | ## Windows 121 | ############ 122 | 123 | # Windows image file caches 124 | Thumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | 130 | ############# 131 | ## Python 132 | ############# 133 | 134 | *.py[co] 135 | 136 | # Packages 137 | *.egg 138 | *.egg-info 139 | dist 140 | build 141 | eggs 142 | parts 143 | bin 144 | var 145 | sdist 146 | develop-eggs 147 | .installed.cfg 148 | 149 | # Installer logs 150 | pip-log.txt 151 | 152 | # Unit test / coverage reports 153 | .coverage 154 | .tox 155 | 156 | #Translations 157 | *.mo 158 | 159 | #Mr Developer 160 | .mr.developer.cfg 161 | 162 | # Mac crap 163 | .DS_Store 164 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | bower.json 3 | index.html 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jQuery Observe 2 | 3 | A jQuery plugin which simplifies the use of the new [DOM Mutation Observer][w3_mo] interface introduced in newer browsers. `jquery-observe.js` contains the compiled and minified version of the plugin. 4 | 5 | Available through `bower` 6 | 7 | bower install jquery-observe 8 | 9 | And `npm` 10 | 11 | npm install @kapetan/jquery-observer 12 | 13 | # Compatibility 14 | 15 | At this point the Mutation Observer API is only available in newer versions of Google Chrome (>= 18) and Mozilla Firefox (>= 14). Can be useful when developing extensions (add-ons) that use content scripts, on a page that you don't control. 16 | 17 | There is a fallback for browsers who don't support the Mutation Observer API (mainly IE9 and Opera), using the deprecated [DOM Mutation Events interface][w3_me]. These don't act completely as Mutation Observers, and can cause some different behaviour between browsers. 18 | For instance an event is fired for every descendant of an inserted node, where only one record is dispatched when using Mutation Observers. Furthermore because mutation events don't have the same information associated with them as Mutation Observer records, some contextual selectors may not match removed nodes. Some of those are `:first`, `:first-child`, `:eq`, `:last`, `:last-child`, `:even` and `:odd` (not an exhaustive list). 19 | 20 | # Usage 21 | 22 | The observe interface `$.fn.observe()` is somewhat similar to the jQuery event api using the `$.fn.on()` method. 23 | 24 | ```javascript 25 | $('#content') 26 | .observe('attributes', function(record) { 27 | // Observe attribute changes on #content 28 | }) 29 | .observe('childlist subtree', function(record) { 30 | // Observe changes in the subtree 31 | }) 32 | .observe({ attributes: true, attributeFilter: ['class'] }, function(record) { 33 | // Observe changes in attribute class on #content 34 | }); 35 | ``` 36 | The callback function gets passed the [MutationRecord][w3_mr] instance matching the query. 37 | 38 | The first argument can either be a string or an object containing the options which are passed to `MutaionObserver.observe()` method. See the [w3c documentation][w3_mo] for Mutation Observer for more information on the available options. 39 | 40 | All the above observers are collapsed into a single Mutation Observer object using #content as target. 41 | 42 | The real power comes when using a selector to filter the elements. 43 | 44 | ```javascript 45 | $('#content') 46 | .observe('childlist', 'ul li:first', function(record) { 47 | // Observe if elements matching '#content ul li:first' have been added or removed 48 | }) 49 | .observe('attributes', '.section p:visible', function(record) { 50 | // Observe if elements matching '#content .section p:visible' have been added or removed 51 | }) 52 | ``` 53 | 54 | In the above callback functions `this` refers to the available matched DOM element. In the case where no selector is given `this` always refers to the element which `.observe()` was called on. When a selector is present `this` references different elements. 55 | 56 | * Records of type *attributes*: `this` refers to the node which had an attribute changed. 57 | 58 | * Records of type *characterData*: `this` refers to the parent element of the text node which had its content modified. The modified text node can be retrieved trough `record.target`. 59 | 60 | * Records of type *childList* and the selector matches an added node: `this` refers to the added node. 61 | 62 | * Records of type *childList* and the selector matches a removed node: `this` refers to the **parent** of the removed node (since the removed node is no longer available in the DOM). 63 | 64 | This also means that the callback is called for every matched element (similar to `$.fn.on()`). 65 | 66 | Using the above defined observers with the following HTML: 67 | 68 | ```html 69 |
70 | 73 |

Hello

74 |
75 | ``` 76 | 77 | * Running `$('#content ul').append('
  • ') // added as last element` will **not** trigger the first observer neither will `$('#content ul li:first').append('')`, since the observe selector only matches nodes that either have been added or removed. 78 | 79 | * Running `$('#content ul').prepend('
  • ')` will trigger the first observer, since a *li* element was inserted as the first child. And `this` will reference to the newly inserted node. 80 | 81 | * Running `$('#content ul li:first').remove()` also triggers the first observer. In this case `this` will reference to the parent of the removed element (the *ul* element). 82 | 83 | * Running `$('#content ul li:first span').remove()` will **not** trigger the first observer. 84 | 85 | * Running `$('#content span p').addClass('myClass')` triggers the second observer. 86 | 87 | Note that the changes to the DOM don't have to be performed using jQuery. The last example can also be run using plain javascript `document.getElementsByClassName('hello')[0].className += ' myClass'`. 88 | 89 | ### Extended options 90 | 91 | There are two custom Mutation Observer options which can be used together with the other options. These are *added* and *removed*. Which only trigger an observer if either a node has been added or removed. 92 | 93 | ```javascript 94 | $('#content') 95 | .observe('added', 'li:first', function(record) { 96 | // Only called if a node matching '#content li:first' has been added 97 | }) 98 | .observe('removed', 'li:first', function(record) { 99 | // Only called if a node matching '#content li:first' has been removed 100 | }); 101 | ``` 102 | 103 | Using the options `'added removed'` is equivalent to `'childlist'`. 104 | 105 | ### Removing observers 106 | 107 | Use the `$.fn.disconnect()` method to remove an observer. The arguments must match the arguments given to the `$.fn.observe()` method. Or call the disconnect method without arguments to remove all observers. The underlying Mutation Observer is disabled when there are no observers listening for changes. 108 | 109 | # Issues 110 | 111 | There are some problems getting the characterData option to work in Chrome (may be because of bug [#134322][chrome_bug]). 112 | 113 | # License 114 | 115 | **This software is licensed under "MIT"** 116 | 117 | > Copyright (c) 2012 Mirza Kapetanovic 118 | > 119 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 120 | > 121 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 122 | > 123 | > THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 124 | 125 | [w3_mo]: http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#mutation-observers "Mutation Observer" 126 | [w3_mr]: http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#mutationrecord "Mutaion Record" 127 | [w3_me]: http://www.w3.org/TR/DOM-Level-3-Events/#events-mutationevents "Mutaion Events" 128 | [chrome_bug]: http://code.google.com/p/chromium/issues/detail?id=134322 "Chrome bug #134322" 129 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-observe", 3 | "version": "2.0.3", 4 | "dependencies": { 5 | "jquery": "~1.7 || ~2 || ~3" 6 | }, 7 | "main": "jquery-observe.js", 8 | "homepage": "https://github.com/kapetan/jquery-observe", 9 | "author": "Mirza Kapetanovic", 10 | "description": "Observe DOM mutations with jQuery", 11 | "keywords": [ 12 | "observe", 13 | "observer", 14 | "jquery", 15 | "dom", 16 | "compatibility" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "bower.json", 21 | "README.md", 22 | "test", 23 | "index.html", 24 | "**/.*" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jQuery Observe 5 | 6 | 7 | 8 | 9 | 10 | 27 | 28 | 29 |
    30 | 36 | 37 |
    38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /jquery-observe.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | $.Observe = {}; 3 | }(jQuery)); 4 | 5 | (function($, ns) { 6 | var get = function(origin, target) { 7 | if(!target) { 8 | target = origin; 9 | origin = window.document; 10 | } 11 | 12 | var result = []; 13 | 14 | $(target).each(function() { 15 | var selector = []; 16 | var prev = $(this); 17 | 18 | for(var current = prev.parent(); current.length && !prev.is(origin); current = current.parent()) { 19 | var tag = prev.get(0).tagName.toLowerCase(); 20 | 21 | selector.push(tag + ':eq(' + current.children(tag).index(prev) + ')'); 22 | 23 | prev = current; 24 | } 25 | 26 | if(!current.length && !prev.is(origin)) { 27 | return; 28 | } 29 | 30 | result.push('> ' + selector.reverse().join(' > ')); 31 | }); 32 | 33 | return result.join(', '); 34 | }; 35 | 36 | var capture = function(origin, target) { 37 | if(!target) { 38 | target = origin; 39 | origin = window.document; 40 | } 41 | 42 | var result = []; 43 | 44 | $(target).each(function() { 45 | var textIndex = -1; 46 | var realTarget = this; 47 | 48 | if(this instanceof Text) { 49 | realTarget = this.parentNode; 50 | var children = realTarget.childNodes; 51 | 52 | for(var i = 0; i < children.length; i++) { 53 | if(children[i] === this) { 54 | textIndex = i; 55 | break; 56 | } 57 | } 58 | } 59 | 60 | var path = get(origin, realTarget); 61 | var same = $(origin).is(realTarget); 62 | 63 | result.push(function(origin) { 64 | var target = same ? origin : $(origin).find(path); 65 | return textIndex === -1 ? target : target.contents()[textIndex]; 66 | }); 67 | }); 68 | 69 | return function(origin) { 70 | origin = origin || window.document; 71 | 72 | return result.reduce(function(acc, fn) { 73 | return acc.add(fn(origin)); 74 | }, $([])); 75 | }; 76 | }; 77 | 78 | ns.path = { 79 | get: get, 80 | capture: capture 81 | }; 82 | }(jQuery, jQuery.Observe)); 83 | 84 | (function($, ns) { 85 | var Branch = function(root) { 86 | this.original = $(root); 87 | this.root = this.original.clone(false, true); 88 | }; 89 | 90 | Branch.prototype.find = function(selector) { 91 | var path = ns.path.capture(this.original, selector); 92 | return path(this.root); 93 | }; 94 | 95 | ns.Branch = Branch; 96 | }(jQuery, jQuery.Observe)); 97 | 98 | (function($, ns) { 99 | var toObject = function(array, fn) { 100 | var result = {}; 101 | 102 | array.forEach(function(name) { 103 | var pair = fn(name); 104 | 105 | if(pair) { 106 | result[pair[0]] = pair[1]; 107 | } 108 | }); 109 | 110 | return result; 111 | }; 112 | 113 | var OBSERVER_OPTIONS = toObject([ 114 | 'childList', 115 | 'attributes', 116 | 'characterData', 117 | 'subtree', 118 | 'attributeOldValue', 119 | 'characterDataOldValue', 120 | 'attributeFilter' 121 | ], function(name) { 122 | return [name.toLowerCase(), name]; 123 | }); 124 | var ALL = toObject(Object.keys(OBSERVER_OPTIONS), function(name) { 125 | if(name !== 'attributefilter') { 126 | return [OBSERVER_OPTIONS[name], true]; 127 | } 128 | }); 129 | var EXTENDED_OPTIONS = toObject([ 130 | 'added', 131 | 'removed' 132 | ], function(name) { 133 | return [name.toLowerCase(), name]; 134 | }); 135 | 136 | var EMPTY = $([]); 137 | 138 | var parseOptions = function(options) { 139 | if(typeof options === 'object') { 140 | return options; 141 | } 142 | 143 | options = options.split(/\s+/); 144 | 145 | var result = {}; 146 | 147 | options.forEach(function(opt) { 148 | opt = opt.toLowerCase(); 149 | 150 | if(!OBSERVER_OPTIONS[opt] && !EXTENDED_OPTIONS[opt]) { 151 | throw new Error('Unknown option ' + opt); 152 | } 153 | 154 | result[OBSERVER_OPTIONS[opt] || EXTENDED_OPTIONS[opt]] = true; 155 | }); 156 | 157 | return result; 158 | }; 159 | 160 | var objectToString = function(obj) { 161 | return '[' + Object.keys(obj).sort().reduce(function(acc, key) { 162 | var valueStr = (obj[key] && typeof obj[key] === 'object') ? objectToString(obj[key]) : obj[key]; 163 | 164 | return acc + '[' + JSON.stringify(key) + ':' + valueStr + ']'; 165 | }, '') + ']'; 166 | }; 167 | 168 | var MutationObserver = window.MutationObserver || window.WebKitMutationObserver; 169 | 170 | var Pattern = function(target, options, selector, handler) { 171 | this._originalOptions = $.extend({}, options); 172 | options = $.extend({}, options); 173 | 174 | this.attributeFilter = options.attributeFilter; 175 | 176 | delete options.attributeFilter; 177 | 178 | if(selector) { 179 | options.subtree = true; 180 | } 181 | if(options.childList) { 182 | options.added = true; 183 | options.removed = true; 184 | } 185 | if(options.added || options.removed) { 186 | options.childList = true; 187 | } 188 | 189 | this.target = $(target); 190 | this.options = options; 191 | this.selector = selector; 192 | this.handler = handler; 193 | }; 194 | Pattern.prototype.is = function(options, selector, handler) { 195 | return objectToString(this._originalOptions) === objectToString(options) && 196 | this.selector === selector && 197 | this.handler === handler; 198 | }; 199 | Pattern.prototype.match = function(record) { 200 | var self = this; 201 | var options = this.options; 202 | var type = record.type; 203 | 204 | if(!this.options[type]) { 205 | return EMPTY; 206 | } 207 | 208 | if(this.selector) { 209 | switch(type) { 210 | case 'attributes': 211 | if(!this._matchAttributeFilter(record)) { 212 | break; 213 | } 214 | case 'characterData': 215 | return this._matchAttributesAndCharacterData(record); 216 | case 'childList': 217 | if(record.addedNodes && record.addedNodes.length && options.added) { 218 | var result = this._matchAddedNodes(record); 219 | 220 | if(result.length) { 221 | return result; 222 | } 223 | } 224 | if(record.removedNodes && record.removedNodes.length && options.removed) { 225 | return this._matchRemovedNodes(record); 226 | } 227 | } 228 | } else { 229 | var recordTarget = record.target instanceof Text ? 230 | $(record.target).parent() : $(record.target); 231 | 232 | if(!options.subtree && recordTarget.get(0) !== this.target.get(0)) { 233 | return EMPTY; 234 | } 235 | 236 | switch(type) { 237 | case 'attributes': 238 | if(!this._matchAttributeFilter(record)) { 239 | break; 240 | } 241 | case 'characterData': 242 | return this.target; 243 | case 'childList': 244 | if((record.addedNodes && record.addedNodes.length && options.added) || 245 | (record.removedNodes && record.removedNodes.length && options.removed)) { 246 | 247 | return this.target; 248 | } 249 | } 250 | } 251 | 252 | return EMPTY; 253 | }; 254 | Pattern.prototype._matchAttributesAndCharacterData = function(record) { 255 | return this._matchSelector(this.target, [record.target]); 256 | }; 257 | Pattern.prototype._matchAddedNodes = function(record) { 258 | return this._matchSelector(this.target, record.addedNodes); 259 | }; 260 | Pattern.prototype._matchRemovedNodes = function(record) { 261 | var branch = new ns.Branch(this.target); 262 | var nodes = Array.prototype.slice.call(record.removedNodes).map(function(node) { 263 | return node.cloneNode(true); 264 | }); 265 | 266 | if(record.previousSibling) { 267 | branch.find(record.previousSibling).after(nodes); 268 | } else if(record.nextSibling) { 269 | branch.find(record.nextSibling).before(nodes); 270 | } else { 271 | branch.find(record.target).empty().append(nodes); 272 | } 273 | 274 | return this._matchSelector(branch.root, nodes).length ? $(record.target) : EMPTY; 275 | }; 276 | Pattern.prototype._matchSelector = function(origin, element) { 277 | var match = origin.find(this.selector); 278 | element = Array.prototype.slice.call(element); 279 | 280 | match = match.filter(function() { 281 | var self = this; 282 | 283 | return element.some(function(node) { 284 | if(node instanceof Text) return node.parentNode === self; 285 | else return node === self || $(node).has(self).length; 286 | }); 287 | }); 288 | 289 | return match; 290 | }; 291 | Pattern.prototype._matchAttributeFilter = function(record) { 292 | if(this.attributeFilter && this.attributeFilter.length) { 293 | return this.attributeFilter.indexOf(record.attributeName) >= 0; 294 | } 295 | 296 | return true; 297 | }; 298 | 299 | var Observer = function(target) { 300 | this.patterns = []; 301 | 302 | this._target = target; 303 | this._observer = null; 304 | }; 305 | Observer.prototype.observe = function(options, selector, handler) { 306 | var self = this; 307 | 308 | if(!this._observer) { 309 | this._observer = new MutationObserver(function(records) { 310 | records.forEach(function(record) { 311 | self.patterns.forEach(function(pattern) { 312 | var match = pattern.match(record); 313 | 314 | if(match.length) { 315 | match.each(function() { 316 | pattern.handler.call(this, record); 317 | }); 318 | } 319 | }); 320 | }); 321 | }); 322 | } else { 323 | this._observer.disconnect(); 324 | } 325 | 326 | this.patterns.push(new Pattern(this._target, options, selector, handler)); 327 | this._observer.observe(this._target, this._collapseOptions()); 328 | }; 329 | Observer.prototype.disconnect = function(options, selector, handler) { 330 | var self = this; 331 | 332 | if(this._observer) { 333 | this.patterns.filter(function(pattern) { 334 | return pattern.is(options, selector, handler); 335 | }).forEach(function(pattern) { 336 | var index = self.patterns.indexOf(pattern); 337 | 338 | self.patterns.splice(index, 1); 339 | }); 340 | 341 | if(!this.patterns.length) { 342 | this._observer.disconnect(); 343 | } 344 | } 345 | }; 346 | Observer.prototype.disconnectAll = function() { 347 | if(this._observer) { 348 | this.patterns = []; 349 | this._observer.disconnect(); 350 | } 351 | }; 352 | Observer.prototype.pause = function() { 353 | if(this._observer) { 354 | this._observer.disconnect(); 355 | } 356 | }; 357 | Observer.prototype.resume = function() { 358 | if(this._observer) { 359 | this._observer.observe(this._target, this._collapseOptions()); 360 | } 361 | }; 362 | Observer.prototype._collapseOptions = function() { 363 | var result = {}; 364 | 365 | this.patterns.forEach(function(pattern) { 366 | var restrictiveFilter = result.attributes && result.attributeFilter; 367 | 368 | if((restrictiveFilter || !result.attributes) && pattern.attributeFilter) { 369 | var attributeFilter = (result.attributeFilter || []).concat(pattern.attributeFilter); 370 | var existing = {}; 371 | var unique = []; 372 | 373 | attributeFilter.forEach(function(attr) { 374 | if(!existing[attr]) { 375 | unique.push(attr); 376 | existing[attr] = 1; 377 | } 378 | }); 379 | 380 | result.attributeFilter = unique; 381 | } else if(restrictiveFilter && pattern.options.attributes && !pattern.attributeFilter) { 382 | delete result.attributeFilter; 383 | } 384 | 385 | $.extend(result, pattern.options); 386 | }); 387 | 388 | Object.keys(EXTENDED_OPTIONS).forEach(function(name) { 389 | delete result[EXTENDED_OPTIONS[name]]; 390 | }); 391 | 392 | return result; 393 | }; 394 | 395 | var DOMEventObserver = function(target) { 396 | this.patterns = []; 397 | 398 | this._paused = false; 399 | this._target = target; 400 | this._events = {}; 401 | this._handler = this._handler.bind(this); 402 | }; 403 | DOMEventObserver.prototype.NS = '.jQueryObserve'; 404 | DOMEventObserver.prototype.observe = function(options, selector, handler) { 405 | var pattern = new Pattern(this._target, options, selector, handler); 406 | var target = $(this._target); 407 | 408 | if(pattern.options.childList) { 409 | this._addEvent('DOMNodeInserted'); 410 | this._addEvent('DOMNodeRemoved'); 411 | } 412 | if(pattern.options.attributes) { 413 | this._addEvent('DOMAttrModified'); 414 | } 415 | if(pattern.options.characterData) { 416 | this._addEvent('DOMCharacerDataModified'); 417 | } 418 | 419 | this.patterns.push(pattern); 420 | }; 421 | DOMEventObserver.prototype.disconnect = function(options, selector, handler) { 422 | var target = $(this._target); 423 | var self = this; 424 | 425 | this.patterns.filter(function(pattern) { 426 | return pattern.is(options, selector, handler); 427 | }).forEach(function(pattern) { 428 | var index = self.patterns.indexOf(pattern); 429 | 430 | self.patterns.splice(index, 1); 431 | }); 432 | 433 | var eventsInUse = this.patterns.reduce(function(acc, pattern) { 434 | if(pattern.options.childList) { 435 | acc.DOMNodeInserted = true; 436 | acc.DOMNodeRemoved = true; 437 | } 438 | if(pattern.options.attributes) { 439 | acc.DOMAttrModified = true; 440 | } 441 | if(pattern.options.characterData) { 442 | acc.DOMCharacerDataModified = true; 443 | } 444 | 445 | return acc; 446 | }, {}); 447 | 448 | Object.keys(this._events).forEach(function(type) { 449 | if(eventsInUse[type]) { 450 | return; 451 | } 452 | 453 | delete self._events[type]; 454 | 455 | target.off(type + self.NS, self._handler); 456 | }); 457 | }; 458 | DOMEventObserver.prototype.disconnectAll = function() { 459 | var target = $(this._target); 460 | 461 | for(var name in this._events) { 462 | target.off(name + this.NS, this._handler); 463 | } 464 | 465 | this._events = {}; 466 | this.patterns = []; 467 | }; 468 | DOMEventObserver.prototype.pause = function() { 469 | this._paused = true; 470 | }; 471 | DOMEventObserver.prototype.resume = function() { 472 | this._paused = false; 473 | }; 474 | DOMEventObserver.prototype._handler = function(e) { 475 | if(this._paused) { 476 | return; 477 | } 478 | 479 | var record = { 480 | type: null, 481 | target: null, 482 | addedNodes: null, 483 | removedNodes: null, 484 | previousSibling: null, 485 | nextSibling: null, 486 | attributeName: null, 487 | attributeNamespace: null, 488 | oldValue: null 489 | }; 490 | 491 | switch(e.type) { 492 | case 'DOMAttrModified': 493 | record.type = 'attributes'; 494 | record.target = e.target; 495 | record.attributeName = e.attrName; 496 | record.oldValue = e.prevValue; 497 | 498 | break; 499 | case 'DOMCharacerDataModified': 500 | record.type = 'characterData'; 501 | record.target = $(e.target).parent().get(0); 502 | record.attributeName = e.attrName; 503 | record.oldValue = e.prevValue; 504 | 505 | break; 506 | case 'DOMNodeInserted': 507 | record.type = 'childList'; 508 | record.target = e.relatedNode; 509 | record.addedNodes = [e.target]; 510 | record.removedNodes = []; 511 | 512 | break; 513 | case 'DOMNodeRemoved': 514 | record.type = 'childList'; 515 | record.target = e.relatedNode; 516 | record.addedNodes = []; 517 | record.removedNodes = [e.target]; 518 | 519 | break; 520 | } 521 | 522 | for(var i = 0; i < this.patterns.length; i++) { 523 | var pattern = this.patterns[i]; 524 | var match = pattern.match(record); 525 | 526 | if(match.length) { 527 | match.each(function() { 528 | pattern.handler.call(this, record); 529 | }); 530 | } 531 | } 532 | }; 533 | DOMEventObserver.prototype._addEvent = function(type) { 534 | if(!this._events[type]) { 535 | $(this._target).on(type + this.NS, this._handler); 536 | this._events[type] = true; 537 | } 538 | }; 539 | 540 | ns.Pattern = Pattern; 541 | ns.MutationObserver = Observer; 542 | ns.DOMEventObserver = DOMEventObserver; 543 | 544 | $.fn.observe = function(options, selector, handler) { 545 | if(!selector) { 546 | handler = options; 547 | options = ALL; 548 | } else if(!handler) { 549 | handler = selector; 550 | selector = null; 551 | } 552 | 553 | return this.each(function() { 554 | var self = $(this); 555 | var observer = self.data('observer'); 556 | 557 | if(!observer) { 558 | if(MutationObserver) { 559 | observer = new Observer(this); 560 | } else { 561 | observer = new DOMEventObserver(this); 562 | } 563 | 564 | self.data('observer', observer); 565 | } 566 | 567 | options = parseOptions(options); 568 | 569 | observer.observe(options, selector, handler); 570 | }); 571 | }; 572 | $.fn.disconnect = function(options, selector, handler) { 573 | if(!options) { 574 | // No arguments 575 | } 576 | else if(!selector) { 577 | handler = options; 578 | options = ALL; 579 | } else if(!handler) { 580 | handler = selector; 581 | selector = null; 582 | } 583 | 584 | return this.each(function() { 585 | var self = $(this); 586 | var observer = self.data('observer'); 587 | 588 | if(!observer) { 589 | return; 590 | } 591 | 592 | if(!options) { 593 | observer.disconnectAll(); 594 | self.removeData('observer'); 595 | 596 | return; 597 | } 598 | 599 | options = parseOptions(options); 600 | 601 | observer.disconnect(options, selector, handler); 602 | }); 603 | }; 604 | }(jQuery, jQuery.Observe)); 605 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kapetan/jquery-observe", 3 | "version": "3.0.0", 4 | "description": "Observe DOM mutations with jQuery", 5 | "main": "jquery-observe.js", 6 | "dependencies": { 7 | "jquery": "~1.7 || ~2 || ~3" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/kapetan/jquery-observe.git" 12 | }, 13 | "keywords": [ 14 | "observe", 15 | "observer", 16 | "jquery", 17 | "dom", 18 | "compatibility" 19 | ], 20 | "author": "Mirza Kapetanovic", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/kapetan/jquery-observe/issues" 24 | }, 25 | "homepage": "https://github.com/kapetan/jquery-observe#readme" 26 | } 27 | -------------------------------------------------------------------------------- /test/benchmark.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Benchmark.js v1.0.0 3 | * Copyright 2010-2012 Mathias Bynens 4 | * Based on JSLitmus.js, copyright Robert Kieffer 5 | * Modified by John-David Dalton 6 | * Available under MIT license 7 | */ 8 | ;(function(window, undefined) { 9 | 'use strict'; 10 | 11 | /** Used to assign each benchmark an incrimented id */ 12 | var counter = 0; 13 | 14 | /** Detect DOM document object */ 15 | var doc = isHostType(window, 'document') && document; 16 | 17 | /** Detect free variable `define` */ 18 | var freeDefine = typeof define == 'function' && 19 | typeof define.amd == 'object' && define.amd && define; 20 | 21 | /** Detect free variable `exports` */ 22 | var freeExports = typeof exports == 'object' && exports && 23 | (typeof global == 'object' && global && global == global.global && (window = global), exports); 24 | 25 | /** Detect free variable `require` */ 26 | var freeRequire = typeof require == 'function' && require; 27 | 28 | /** Used to crawl all properties regardless of enumerability */ 29 | var getAllKeys = Object.getOwnPropertyNames; 30 | 31 | /** Used to get property descriptors */ 32 | var getDescriptor = Object.getOwnPropertyDescriptor; 33 | 34 | /** Used in case an object doesn't have its own method */ 35 | var hasOwnProperty = {}.hasOwnProperty; 36 | 37 | /** Used to check if an object is extensible */ 38 | var isExtensible = Object.isExtensible || function() { return true; }; 39 | 40 | /** Used to access Wade Simmons' Node microtime module */ 41 | var microtimeObject = req('microtime'); 42 | 43 | /** Used to access the browser's high resolution timer */ 44 | var perfObject = isHostType(window, 'performance') && performance; 45 | 46 | /** Used to call the browser's high resolution timer */ 47 | var perfName = perfObject && ( 48 | perfObject.now && 'now' || 49 | perfObject.webkitNow && 'webkitNow' 50 | ); 51 | 52 | /** Used to access Node's high resolution timer */ 53 | var processObject = isHostType(window, 'process') && process; 54 | 55 | /** Used to check if an own property is enumerable */ 56 | var propertyIsEnumerable = {}.propertyIsEnumerable; 57 | 58 | /** Used to set property descriptors */ 59 | var setDescriptor = Object.defineProperty; 60 | 61 | /** Used to resolve a value's internal [[Class]] */ 62 | var toString = {}.toString; 63 | 64 | /** Used to prevent a `removeChild` memory leak in IE < 9 */ 65 | var trash = doc && doc.createElement('div'); 66 | 67 | /** Used to integrity check compiled tests */ 68 | var uid = 'uid' + (+new Date); 69 | 70 | /** Used to avoid infinite recursion when methods call each other */ 71 | var calledBy = {}; 72 | 73 | /** Used to avoid hz of Infinity */ 74 | var divisors = { 75 | '1': 4096, 76 | '2': 512, 77 | '3': 64, 78 | '4': 8, 79 | '5': 0 80 | }; 81 | 82 | /** 83 | * T-Distribution two-tailed critical values for 95% confidence 84 | * http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm 85 | */ 86 | var tTable = { 87 | '1': 12.706,'2': 4.303, '3': 3.182, '4': 2.776, '5': 2.571, '6': 2.447, 88 | '7': 2.365, '8': 2.306, '9': 2.262, '10': 2.228, '11': 2.201, '12': 2.179, 89 | '13': 2.16, '14': 2.145, '15': 2.131, '16': 2.12, '17': 2.11, '18': 2.101, 90 | '19': 2.093, '20': 2.086, '21': 2.08, '22': 2.074, '23': 2.069, '24': 2.064, 91 | '25': 2.06, '26': 2.056, '27': 2.052, '28': 2.048, '29': 2.045, '30': 2.042, 92 | 'infinity': 1.96 93 | }; 94 | 95 | /** 96 | * Critical Mann-Whitney U-values for 95% confidence 97 | * http://www.saburchill.com/IBbiology/stats/003.html 98 | */ 99 | var uTable = { 100 | '5': [0, 1, 2], 101 | '6': [1, 2, 3, 5], 102 | '7': [1, 3, 5, 6, 8], 103 | '8': [2, 4, 6, 8, 10, 13], 104 | '9': [2, 4, 7, 10, 12, 15, 17], 105 | '10': [3, 5, 8, 11, 14, 17, 20, 23], 106 | '11': [3, 6, 9, 13, 16, 19, 23, 26, 30], 107 | '12': [4, 7, 11, 14, 18, 22, 26, 29, 33, 37], 108 | '13': [4, 8, 12, 16, 20, 24, 28, 33, 37, 41, 45], 109 | '14': [5, 9, 13, 17, 22, 26, 31, 36, 40, 45, 50, 55], 110 | '15': [5, 10, 14, 19, 24, 29, 34, 39, 44, 49, 54, 59, 64], 111 | '16': [6, 11, 15, 21, 26, 31, 37, 42, 47, 53, 59, 64, 70, 75], 112 | '17': [6, 11, 17, 22, 28, 34, 39, 45, 51, 57, 63, 67, 75, 81, 87], 113 | '18': [7, 12, 18, 24, 30, 36, 42, 48, 55, 61, 67, 74, 80, 86, 93, 99], 114 | '19': [7, 13, 19, 25, 32, 38, 45, 52, 58, 65, 72, 78, 85, 92, 99, 106, 113], 115 | '20': [8, 14, 20, 27, 34, 41, 48, 55, 62, 69, 76, 83, 90, 98, 105, 112, 119, 127], 116 | '21': [8, 15, 22, 29, 36, 43, 50, 58, 65, 73, 80, 88, 96, 103, 111, 119, 126, 134, 142], 117 | '22': [9, 16, 23, 30, 38, 45, 53, 61, 69, 77, 85, 93, 101, 109, 117, 125, 133, 141, 150, 158], 118 | '23': [9, 17, 24, 32, 40, 48, 56, 64, 73, 81, 89, 98, 106, 115, 123, 132, 140, 149, 157, 166, 175], 119 | '24': [10, 17, 25, 33, 42, 50, 59, 67, 76, 85, 94, 102, 111, 120, 129, 138, 147, 156, 165, 174, 183, 192], 120 | '25': [10, 18, 27, 35, 44, 53, 62, 71, 80, 89, 98, 107, 117, 126, 135, 145, 154, 163, 173, 182, 192, 201, 211], 121 | '26': [11, 19, 28, 37, 46, 55, 64, 74, 83, 93, 102, 112, 122, 132, 141, 151, 161, 171, 181, 191, 200, 210, 220, 230], 122 | '27': [11, 20, 29, 38, 48, 57, 67, 77, 87, 97, 107, 118, 125, 138, 147, 158, 168, 178, 188, 199, 209, 219, 230, 240, 250], 123 | '28': [12, 21, 30, 40, 50, 60, 70, 80, 90, 101, 111, 122, 132, 143, 154, 164, 175, 186, 196, 207, 218, 228, 239, 250, 261, 272], 124 | '29': [13, 22, 32, 42, 52, 62, 73, 83, 94, 105, 116, 127, 138, 149, 160, 171, 182, 193, 204, 215, 226, 238, 249, 260, 271, 282, 294], 125 | '30': [13, 23, 33, 43, 54, 65, 76, 87, 98, 109, 120, 131, 143, 154, 166, 177, 189, 200, 212, 223, 235, 247, 258, 270, 282, 293, 305, 317] 126 | }; 127 | 128 | /** 129 | * An object used to flag environments/features. 130 | * 131 | * @static 132 | * @memberOf Benchmark 133 | * @type Object 134 | */ 135 | var support = {}; 136 | 137 | (function() { 138 | 139 | /** 140 | * Detect Adobe AIR. 141 | * 142 | * @memberOf Benchmark.support 143 | * @type Boolean 144 | */ 145 | support.air = isClassOf(window.runtime, 'ScriptBridgingProxyObject'); 146 | 147 | /** 148 | * Detect if `arguments` objects have the correct internal [[Class]] value. 149 | * 150 | * @memberOf Benchmark.support 151 | * @type Boolean 152 | */ 153 | support.argumentsClass = isClassOf(arguments, 'Arguments'); 154 | 155 | /** 156 | * Detect if in a browser environment. 157 | * 158 | * @memberOf Benchmark.support 159 | * @type Boolean 160 | */ 161 | support.browser = doc && isHostType(window, 'navigator'); 162 | 163 | /** 164 | * Detect if strings support accessing characters by index. 165 | * 166 | * @memberOf Benchmark.support 167 | * @type Boolean 168 | */ 169 | support.charByIndex = 170 | // IE 8 supports indexes on string literals but not string objects 171 | ('x'[0] + Object('x')[0]) == 'xx'; 172 | 173 | /** 174 | * Detect if strings have indexes as own properties. 175 | * 176 | * @memberOf Benchmark.support 177 | * @type Boolean 178 | */ 179 | support.charByOwnIndex = 180 | // Narwhal, Rhino, RingoJS, IE 8, and Opera < 10.52 support indexes on 181 | // strings but don't detect them as own properties 182 | support.charByIndex && hasKey('x', '0'); 183 | 184 | /** 185 | * Detect if Java is enabled/exposed. 186 | * 187 | * @memberOf Benchmark.support 188 | * @type Boolean 189 | */ 190 | support.java = isClassOf(window.java, 'JavaPackage'); 191 | 192 | /** 193 | * Detect if the Timers API exists. 194 | * 195 | * @memberOf Benchmark.support 196 | * @type Boolean 197 | */ 198 | support.timeout = isHostType(window, 'setTimeout') && isHostType(window, 'clearTimeout'); 199 | 200 | /** 201 | * Detect if functions support decompilation. 202 | * 203 | * @name decompilation 204 | * @memberOf Benchmark.support 205 | * @type Boolean 206 | */ 207 | try { 208 | // Safari 2.x removes commas in object literals 209 | // from Function#toString results 210 | // http://webk.it/11609 211 | // Firefox 3.6 and Opera 9.25 strip grouping 212 | // parentheses from Function#toString results 213 | // http://bugzil.la/559438 214 | support.decompilation = Function( 215 | 'return (' + (function(x) { return { 'x': '' + (1 + x) + '', 'y': 0 }; }) + ')' 216 | )()(0).x === '1'; 217 | } catch(e) { 218 | support.decompilation = false; 219 | } 220 | 221 | /** 222 | * Detect ES5+ property descriptor API. 223 | * 224 | * @name descriptors 225 | * @memberOf Benchmark.support 226 | * @type Boolean 227 | */ 228 | try { 229 | var o = {}; 230 | support.descriptors = (setDescriptor(o, o, o), 'value' in getDescriptor(o, o)); 231 | } catch(e) { 232 | support.descriptors = false; 233 | } 234 | 235 | /** 236 | * Detect ES5+ Object.getOwnPropertyNames(). 237 | * 238 | * @name getAllKeys 239 | * @memberOf Benchmark.support 240 | * @type Boolean 241 | */ 242 | try { 243 | support.getAllKeys = /\bvalueOf\b/.test(getAllKeys(Object.prototype)); 244 | } catch(e) { 245 | support.getAllKeys = false; 246 | } 247 | 248 | /** 249 | * Detect if own properties are iterated before inherited properties (all but IE < 9). 250 | * 251 | * @name iteratesOwnLast 252 | * @memberOf Benchmark.support 253 | * @type Boolean 254 | */ 255 | support.iteratesOwnFirst = (function() { 256 | var props = []; 257 | function ctor() { this.x = 1; } 258 | ctor.prototype = { 'y': 1 }; 259 | for (var prop in new ctor) { props.push(prop); } 260 | return props[0] == 'x'; 261 | }()); 262 | 263 | /** 264 | * Detect if a node's [[Class]] is resolvable (all but IE < 9) 265 | * and that the JS engine errors when attempting to coerce an object to a 266 | * string without a `toString` property value of `typeof` "function". 267 | * 268 | * @name nodeClass 269 | * @memberOf Benchmark.support 270 | * @type Boolean 271 | */ 272 | try { 273 | support.nodeClass = ({ 'toString': 0 } + '', toString.call(doc || 0) != '[object Object]'); 274 | } catch(e) { 275 | support.nodeClass = true; 276 | } 277 | }()); 278 | 279 | /** 280 | * Timer object used by `clock()` and `Deferred#resolve`. 281 | * 282 | * @private 283 | * @type Object 284 | */ 285 | var timer = { 286 | 287 | /** 288 | * The timer namespace object or constructor. 289 | * 290 | * @private 291 | * @memberOf timer 292 | * @type Function|Object 293 | */ 294 | 'ns': Date, 295 | 296 | /** 297 | * Starts the deferred timer. 298 | * 299 | * @private 300 | * @memberOf timer 301 | * @param {Object} deferred The deferred instance. 302 | */ 303 | 'start': null, // lazy defined in `clock()` 304 | 305 | /** 306 | * Stops the deferred timer. 307 | * 308 | * @private 309 | * @memberOf timer 310 | * @param {Object} deferred The deferred instance. 311 | */ 312 | 'stop': null // lazy defined in `clock()` 313 | }; 314 | 315 | /** Shortcut for inverse results */ 316 | var noArgumentsClass = !support.argumentsClass, 317 | noCharByIndex = !support.charByIndex, 318 | noCharByOwnIndex = !support.charByOwnIndex; 319 | 320 | /** Math shortcuts */ 321 | var abs = Math.abs, 322 | floor = Math.floor, 323 | max = Math.max, 324 | min = Math.min, 325 | pow = Math.pow, 326 | sqrt = Math.sqrt; 327 | 328 | /*--------------------------------------------------------------------------*/ 329 | 330 | /** 331 | * The Benchmark constructor. 332 | * 333 | * @constructor 334 | * @param {String} name A name to identify the benchmark. 335 | * @param {Function|String} fn The test to benchmark. 336 | * @param {Object} [options={}] Options object. 337 | * @example 338 | * 339 | * // basic usage (the `new` operator is optional) 340 | * var bench = new Benchmark(fn); 341 | * 342 | * // or using a name first 343 | * var bench = new Benchmark('foo', fn); 344 | * 345 | * // or with options 346 | * var bench = new Benchmark('foo', fn, { 347 | * 348 | * // displayed by Benchmark#toString if `name` is not available 349 | * 'id': 'xyz', 350 | * 351 | * // called when the benchmark starts running 352 | * 'onStart': onStart, 353 | * 354 | * // called after each run cycle 355 | * 'onCycle': onCycle, 356 | * 357 | * // called when aborted 358 | * 'onAbort': onAbort, 359 | * 360 | * // called when a test errors 361 | * 'onError': onError, 362 | * 363 | * // called when reset 364 | * 'onReset': onReset, 365 | * 366 | * // called when the benchmark completes running 367 | * 'onComplete': onComplete, 368 | * 369 | * // compiled/called before the test loop 370 | * 'setup': setup, 371 | * 372 | * // compiled/called after the test loop 373 | * 'teardown': teardown 374 | * }); 375 | * 376 | * // or name and options 377 | * var bench = new Benchmark('foo', { 378 | * 379 | * // a flag to indicate the benchmark is deferred 380 | * 'defer': true, 381 | * 382 | * // benchmark test function 383 | * 'fn': function(deferred) { 384 | * // call resolve() when the deferred test is finished 385 | * deferred.resolve(); 386 | * } 387 | * }); 388 | * 389 | * // or options only 390 | * var bench = new Benchmark({ 391 | * 392 | * // benchmark name 393 | * 'name': 'foo', 394 | * 395 | * // benchmark test as a string 396 | * 'fn': '[1,2,3,4].sort()' 397 | * }); 398 | * 399 | * // a test's `this` binding is set to the benchmark instance 400 | * var bench = new Benchmark('foo', function() { 401 | * 'My name is '.concat(this.name); // My name is foo 402 | * }); 403 | */ 404 | function Benchmark(name, fn, options) { 405 | var me = this; 406 | 407 | // allow instance creation without the `new` operator 408 | if (me == null || me.constructor != Benchmark) { 409 | return new Benchmark(name, fn, options); 410 | } 411 | // juggle arguments 412 | if (isClassOf(name, 'Object')) { 413 | // 1 argument (options) 414 | options = name; 415 | } 416 | else if (isClassOf(name, 'Function')) { 417 | // 2 arguments (fn, options) 418 | options = fn; 419 | fn = name; 420 | } 421 | else if (isClassOf(fn, 'Object')) { 422 | // 2 arguments (name, options) 423 | options = fn; 424 | fn = null; 425 | me.name = name; 426 | } 427 | else { 428 | // 3 arguments (name, fn [, options]) 429 | me.name = name; 430 | } 431 | setOptions(me, options); 432 | me.id || (me.id = ++counter); 433 | me.fn == null && (me.fn = fn); 434 | me.stats = deepClone(me.stats); 435 | me.times = deepClone(me.times); 436 | } 437 | 438 | /** 439 | * The Deferred constructor. 440 | * 441 | * @constructor 442 | * @memberOf Benchmark 443 | * @param {Object} clone The cloned benchmark instance. 444 | */ 445 | function Deferred(clone) { 446 | var me = this; 447 | if (me == null || me.constructor != Deferred) { 448 | return new Deferred(clone); 449 | } 450 | me.benchmark = clone; 451 | clock(me); 452 | } 453 | 454 | /** 455 | * The Event constructor. 456 | * 457 | * @constructor 458 | * @memberOf Benchmark 459 | * @param {String|Object} type The event type. 460 | */ 461 | function Event(type) { 462 | var me = this; 463 | return (me == null || me.constructor != Event) 464 | ? new Event(type) 465 | : (type instanceof Event) 466 | ? type 467 | : extend(me, { 'timeStamp': +new Date }, typeof type == 'string' ? { 'type': type } : type); 468 | } 469 | 470 | /** 471 | * The Suite constructor. 472 | * 473 | * @constructor 474 | * @memberOf Benchmark 475 | * @param {String} name A name to identify the suite. 476 | * @param {Object} [options={}] Options object. 477 | * @example 478 | * 479 | * // basic usage (the `new` operator is optional) 480 | * var suite = new Benchmark.Suite; 481 | * 482 | * // or using a name first 483 | * var suite = new Benchmark.Suite('foo'); 484 | * 485 | * // or with options 486 | * var suite = new Benchmark.Suite('foo', { 487 | * 488 | * // called when the suite starts running 489 | * 'onStart': onStart, 490 | * 491 | * // called between running benchmarks 492 | * 'onCycle': onCycle, 493 | * 494 | * // called when aborted 495 | * 'onAbort': onAbort, 496 | * 497 | * // called when a test errors 498 | * 'onError': onError, 499 | * 500 | * // called when reset 501 | * 'onReset': onReset, 502 | * 503 | * // called when the suite completes running 504 | * 'onComplete': onComplete 505 | * }); 506 | */ 507 | function Suite(name, options) { 508 | var me = this; 509 | 510 | // allow instance creation without the `new` operator 511 | if (me == null || me.constructor != Suite) { 512 | return new Suite(name, options); 513 | } 514 | // juggle arguments 515 | if (isClassOf(name, 'Object')) { 516 | // 1 argument (options) 517 | options = name; 518 | } else { 519 | // 2 arguments (name [, options]) 520 | me.name = name; 521 | } 522 | setOptions(me, options); 523 | } 524 | 525 | /*--------------------------------------------------------------------------*/ 526 | 527 | /** 528 | * Note: Some array methods have been implemented in plain JavaScript to avoid 529 | * bugs in IE, Opera, Rhino, and Mobile Safari. 530 | * 531 | * IE compatibility mode and IE < 9 have buggy Array `shift()` and `splice()` 532 | * functions that fail to remove the last element, `object[0]`, of 533 | * array-like-objects even though the `length` property is set to `0`. 534 | * The `shift()` method is buggy in IE 8 compatibility mode, while `splice()` 535 | * is buggy regardless of mode in IE < 9 and buggy in compatibility mode in IE 9. 536 | * 537 | * In Opera < 9.50 and some older/beta Mobile Safari versions using `unshift()` 538 | * generically to augment the `arguments` object will pave the value at index 0 539 | * without incrimenting the other values's indexes. 540 | * https://github.com/documentcloud/underscore/issues/9 541 | * 542 | * Rhino and environments it powers, like Narwhal and RingoJS, may have 543 | * buggy Array `concat()`, `reverse()`, `shift()`, `slice()`, `splice()` and 544 | * `unshift()` functions that make sparse arrays non-sparse by assigning the 545 | * undefined indexes a value of undefined. 546 | * https://github.com/mozilla/rhino/commit/702abfed3f8ca043b2636efd31c14ba7552603dd 547 | */ 548 | 549 | /** 550 | * Creates an array containing the elements of the host array followed by the 551 | * elements of each argument in order. 552 | * 553 | * @memberOf Benchmark.Suite 554 | * @returns {Array} The new array. 555 | */ 556 | function concat() { 557 | var value, 558 | j = -1, 559 | length = arguments.length, 560 | result = slice.call(this), 561 | index = result.length; 562 | 563 | while (++j < length) { 564 | value = arguments[j]; 565 | if (isClassOf(value, 'Array')) { 566 | for (var k = 0, l = value.length; k < l; k++, index++) { 567 | if (k in value) { 568 | result[index] = value[k]; 569 | } 570 | } 571 | } else { 572 | result[index++] = value; 573 | } 574 | } 575 | return result; 576 | } 577 | 578 | /** 579 | * Utility function used by `shift()`, `splice()`, and `unshift()`. 580 | * 581 | * @private 582 | * @param {Number} start The index to start inserting elements. 583 | * @param {Number} deleteCount The number of elements to delete from the insert point. 584 | * @param {Array} elements The elements to insert. 585 | * @returns {Array} An array of deleted elements. 586 | */ 587 | function insert(start, deleteCount, elements) { 588 | // `result` should have its length set to the `deleteCount` 589 | // see https://bugs.ecmascript.org/show_bug.cgi?id=332 590 | var deleteEnd = start + deleteCount, 591 | elementCount = elements ? elements.length : 0, 592 | index = start - 1, 593 | length = start + elementCount, 594 | object = this, 595 | result = Array(deleteCount), 596 | tail = slice.call(object, deleteEnd); 597 | 598 | // delete elements from the array 599 | while (++index < deleteEnd) { 600 | if (index in object) { 601 | result[index - start] = object[index]; 602 | delete object[index]; 603 | } 604 | } 605 | // insert elements 606 | index = start - 1; 607 | while (++index < length) { 608 | object[index] = elements[index - start]; 609 | } 610 | // append tail elements 611 | start = index--; 612 | length = max(0, (object.length >>> 0) - deleteCount + elementCount); 613 | while (++index < length) { 614 | if ((index - start) in tail) { 615 | object[index] = tail[index - start]; 616 | } else if (index in object) { 617 | delete object[index]; 618 | } 619 | } 620 | // delete excess elements 621 | deleteCount = deleteCount > elementCount ? deleteCount - elementCount : 0; 622 | while (deleteCount--) { 623 | index = length + deleteCount; 624 | if (index in object) { 625 | delete object[index]; 626 | } 627 | } 628 | object.length = length; 629 | return result; 630 | } 631 | 632 | /** 633 | * Rearrange the host array's elements in reverse order. 634 | * 635 | * @memberOf Benchmark.Suite 636 | * @returns {Array} The reversed array. 637 | */ 638 | function reverse() { 639 | var upperIndex, 640 | value, 641 | index = -1, 642 | object = Object(this), 643 | length = object.length >>> 0, 644 | middle = floor(length / 2); 645 | 646 | if (length > 1) { 647 | while (++index < middle) { 648 | upperIndex = length - index - 1; 649 | value = upperIndex in object ? object[upperIndex] : uid; 650 | if (index in object) { 651 | object[upperIndex] = object[index]; 652 | } else { 653 | delete object[upperIndex]; 654 | } 655 | if (value != uid) { 656 | object[index] = value; 657 | } else { 658 | delete object[index]; 659 | } 660 | } 661 | } 662 | return object; 663 | } 664 | 665 | /** 666 | * Removes the first element of the host array and returns it. 667 | * 668 | * @memberOf Benchmark.Suite 669 | * @returns {Mixed} The first element of the array. 670 | */ 671 | function shift() { 672 | return insert.call(this, 0, 1)[0]; 673 | } 674 | 675 | /** 676 | * Creates an array of the host array's elements from the start index up to, 677 | * but not including, the end index. 678 | * 679 | * @memberOf Benchmark.Suite 680 | * @param {Number} start The starting index. 681 | * @param {Number} end The end index. 682 | * @returns {Array} The new array. 683 | */ 684 | function slice(start, end) { 685 | var index = -1, 686 | object = Object(this), 687 | length = object.length >>> 0, 688 | result = []; 689 | 690 | start = toInteger(start); 691 | start = start < 0 ? max(length + start, 0) : min(start, length); 692 | start--; 693 | end = end == null ? length : toInteger(end); 694 | end = end < 0 ? max(length + end, 0) : min(end, length); 695 | 696 | while ((++index, ++start) < end) { 697 | if (start in object) { 698 | result[index] = object[start]; 699 | } 700 | } 701 | return result; 702 | } 703 | 704 | /** 705 | * Allows removing a range of elements and/or inserting elements into the 706 | * host array. 707 | * 708 | * @memberOf Benchmark.Suite 709 | * @param {Number} start The start index. 710 | * @param {Number} deleteCount The number of elements to delete. 711 | * @param {Mixed} [val1, val2, ...] values to insert at the `start` index. 712 | * @returns {Array} An array of removed elements. 713 | */ 714 | function splice(start, deleteCount) { 715 | var object = Object(this), 716 | length = object.length >>> 0; 717 | 718 | start = toInteger(start); 719 | start = start < 0 ? max(length + start, 0) : min(start, length); 720 | 721 | // support the de-facto SpiderMonkey extension 722 | // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/splice#Parameters 723 | // https://bugs.ecmascript.org/show_bug.cgi?id=429 724 | deleteCount = arguments.length == 1 725 | ? length - start 726 | : min(max(toInteger(deleteCount), 0), length - start); 727 | 728 | return insert.call(object, start, deleteCount, slice.call(arguments, 2)); 729 | } 730 | 731 | /** 732 | * Converts the specified `value` to an integer. 733 | * 734 | * @private 735 | * @param {Mixed} value The value to convert. 736 | * @returns {Number} The resulting integer. 737 | */ 738 | function toInteger(value) { 739 | value = +value; 740 | return value === 0 || !isFinite(value) ? value || 0 : value - (value % 1); 741 | } 742 | 743 | /** 744 | * Appends arguments to the host array. 745 | * 746 | * @memberOf Benchmark.Suite 747 | * @returns {Number} The new length. 748 | */ 749 | function unshift() { 750 | var object = Object(this); 751 | insert.call(object, 0, 0, arguments); 752 | return object.length; 753 | } 754 | 755 | /*--------------------------------------------------------------------------*/ 756 | 757 | /** 758 | * A generic `Function#bind` like method. 759 | * 760 | * @private 761 | * @param {Function} fn The function to be bound to `thisArg`. 762 | * @param {Mixed} thisArg The `this` binding for the given function. 763 | * @returns {Function} The bound function. 764 | */ 765 | function bind(fn, thisArg) { 766 | return function() { fn.apply(thisArg, arguments); }; 767 | } 768 | 769 | /** 770 | * Creates a function from the given arguments string and body. 771 | * 772 | * @private 773 | * @param {String} args The comma separated function arguments. 774 | * @param {String} body The function body. 775 | * @returns {Function} The new function. 776 | */ 777 | function createFunction() { 778 | // lazy define 779 | createFunction = function(args, body) { 780 | var result, 781 | anchor = freeDefine ? define.amd : Benchmark, 782 | prop = uid + 'createFunction'; 783 | 784 | runScript((freeDefine ? 'define.amd.' : 'Benchmark.') + prop + '=function(' + args + '){' + body + '}'); 785 | result = anchor[prop]; 786 | delete anchor[prop]; 787 | return result; 788 | }; 789 | // fix JaegerMonkey bug 790 | // http://bugzil.la/639720 791 | createFunction = support.browser && (createFunction('', 'return"' + uid + '"') || noop)() == uid ? createFunction : Function; 792 | return createFunction.apply(null, arguments); 793 | } 794 | 795 | /** 796 | * Delay the execution of a function based on the benchmark's `delay` property. 797 | * 798 | * @private 799 | * @param {Object} bench The benchmark instance. 800 | * @param {Object} fn The function to execute. 801 | */ 802 | function delay(bench, fn) { 803 | bench._timerId = setTimeout(fn, bench.delay * 1e3); 804 | } 805 | 806 | /** 807 | * Destroys the given element. 808 | * 809 | * @private 810 | * @param {Element} element The element to destroy. 811 | */ 812 | function destroyElement(element) { 813 | trash.appendChild(element); 814 | trash.innerHTML = ''; 815 | } 816 | 817 | /** 818 | * Iterates over an object's properties, executing the `callback` for each. 819 | * Callbacks may terminate the loop by explicitly returning `false`. 820 | * 821 | * @private 822 | * @param {Object} object The object to iterate over. 823 | * @param {Function} callback The function executed per own property. 824 | * @param {Object} options The options object. 825 | * @returns {Object} Returns the object iterated over. 826 | */ 827 | function forProps() { 828 | var forShadowed, 829 | skipSeen, 830 | forArgs = true, 831 | shadowed = ['constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'valueOf']; 832 | 833 | (function(enumFlag, key) { 834 | // must use a non-native constructor to catch the Safari 2 issue 835 | function Klass() { this.valueOf = 0; }; 836 | Klass.prototype.valueOf = 0; 837 | // check various for-in bugs 838 | for (key in new Klass) { 839 | enumFlag += key == 'valueOf' ? 1 : 0; 840 | } 841 | // check if `arguments` objects have non-enumerable indexes 842 | for (key in arguments) { 843 | key == '0' && (forArgs = false); 844 | } 845 | // Safari 2 iterates over shadowed properties twice 846 | // http://replay.waybackmachine.org/20090428222941/http://tobielangel.com/2007/1/29/for-in-loop-broken-in-safari/ 847 | skipSeen = enumFlag == 2; 848 | // IE < 9 incorrectly makes an object's properties non-enumerable if they have 849 | // the same name as other non-enumerable properties in its prototype chain. 850 | forShadowed = !enumFlag; 851 | }(0)); 852 | 853 | // lazy define 854 | forProps = function(object, callback, options) { 855 | options || (options = {}); 856 | 857 | var result = object; 858 | object = Object(object); 859 | 860 | var ctor, 861 | key, 862 | keys, 863 | skipCtor, 864 | done = !result, 865 | which = options.which, 866 | allFlag = which == 'all', 867 | index = -1, 868 | iteratee = object, 869 | length = object.length, 870 | ownFlag = allFlag || which == 'own', 871 | seen = {}, 872 | skipProto = isClassOf(object, 'Function'), 873 | thisArg = options.bind; 874 | 875 | if (thisArg !== undefined) { 876 | callback = bind(callback, thisArg); 877 | } 878 | // iterate all properties 879 | if (allFlag && support.getAllKeys) { 880 | for (index = 0, keys = getAllKeys(object), length = keys.length; index < length; index++) { 881 | key = keys[index]; 882 | if (callback(object[key], key, object) === false) { 883 | break; 884 | } 885 | } 886 | } 887 | // else iterate only enumerable properties 888 | else { 889 | for (key in object) { 890 | // Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1 891 | // (if the prototype or a property on the prototype has been set) 892 | // incorrectly set a function's `prototype` property [[Enumerable]] value 893 | // to `true`. Because of this we standardize on skipping the `prototype` 894 | // property of functions regardless of their [[Enumerable]] value. 895 | if ((done = 896 | !(skipProto && key == 'prototype') && 897 | !(skipSeen && (hasKey(seen, key) || !(seen[key] = true))) && 898 | (!ownFlag || ownFlag && hasKey(object, key)) && 899 | callback(object[key], key, object) === false)) { 900 | break; 901 | } 902 | } 903 | // in IE < 9 strings don't support accessing characters by index 904 | if (!done && (forArgs && isArguments(object) || 905 | ((noCharByIndex || noCharByOwnIndex) && isClassOf(object, 'String') && 906 | (iteratee = noCharByIndex ? object.split('') : object)))) { 907 | while (++index < length) { 908 | if ((done = 909 | callback(iteratee[index], String(index), object) === false)) { 910 | break; 911 | } 912 | } 913 | } 914 | if (!done && forShadowed) { 915 | // Because IE < 9 can't set the `[[Enumerable]]` attribute of an existing 916 | // property and the `constructor` property of a prototype defaults to 917 | // non-enumerable, we manually skip the `constructor` property when we 918 | // think we are iterating over a `prototype` object. 919 | ctor = object.constructor; 920 | skipCtor = ctor && ctor.prototype && ctor.prototype.constructor === ctor; 921 | for (index = 0; index < 7; index++) { 922 | key = shadowed[index]; 923 | if (!(skipCtor && key == 'constructor') && 924 | hasKey(object, key) && 925 | callback(object[key], key, object) === false) { 926 | break; 927 | } 928 | } 929 | } 930 | } 931 | return result; 932 | }; 933 | return forProps.apply(null, arguments); 934 | } 935 | 936 | /** 937 | * Gets the name of the first argument from a function's source. 938 | * 939 | * @private 940 | * @param {Function} fn The function. 941 | * @returns {String} The argument name. 942 | */ 943 | function getFirstArgument(fn) { 944 | return (!hasKey(fn, 'toString') && 945 | (/^[\s(]*function[^(]*\(([^\s,)]+)/.exec(fn) || 0)[1]) || ''; 946 | } 947 | 948 | /** 949 | * Computes the arithmetic mean of a sample. 950 | * 951 | * @private 952 | * @param {Array} sample The sample. 953 | * @returns {Number} The mean. 954 | */ 955 | function getMean(sample) { 956 | return reduce(sample, function(sum, x) { 957 | return sum + x; 958 | }) / sample.length || 0; 959 | } 960 | 961 | /** 962 | * Gets the source code of a function. 963 | * 964 | * @private 965 | * @param {Function} fn The function. 966 | * @param {String} altSource A string used when a function's source code is unretrievable. 967 | * @returns {String} The function's source code. 968 | */ 969 | function getSource(fn, altSource) { 970 | var result = altSource; 971 | if (isStringable(fn)) { 972 | result = String(fn); 973 | } else if (support.decompilation) { 974 | // escape the `{` for Firefox 1 975 | result = (/^[^{]+\{([\s\S]*)}\s*$/.exec(fn) || 0)[1]; 976 | } 977 | // trim string 978 | result = (result || '').replace(/^\s+|\s+$/g, ''); 979 | 980 | // detect strings containing only the "use strict" directive 981 | return /^(?:\/\*+[\w|\W]*?\*\/|\/\/.*?[\n\r\u2028\u2029]|\s)*(["'])use strict\1;?$/.test(result) 982 | ? '' 983 | : result; 984 | } 985 | 986 | /** 987 | * Checks if a value is an `arguments` object. 988 | * 989 | * @private 990 | * @param {Mixed} value The value to check. 991 | * @returns {Boolean} Returns `true` if the value is an `arguments` object, else `false`. 992 | */ 993 | function isArguments() { 994 | // lazy define 995 | isArguments = function(value) { 996 | return toString.call(value) == '[object Arguments]'; 997 | }; 998 | if (noArgumentsClass) { 999 | isArguments = function(value) { 1000 | return hasKey(value, 'callee') && 1001 | !(propertyIsEnumerable && propertyIsEnumerable.call(value, 'callee')); 1002 | }; 1003 | } 1004 | return isArguments(arguments[0]); 1005 | } 1006 | 1007 | /** 1008 | * Checks if an object is of the specified class. 1009 | * 1010 | * @private 1011 | * @param {Mixed} value The value to check. 1012 | * @param {String} name The name of the class. 1013 | * @returns {Boolean} Returns `true` if the value is of the specified class, else `false`. 1014 | */ 1015 | function isClassOf(value, name) { 1016 | return value != null && toString.call(value) == '[object ' + name + ']'; 1017 | } 1018 | 1019 | /** 1020 | * Host objects can return type values that are different from their actual 1021 | * data type. The objects we are concerned with usually return non-primitive 1022 | * types of object, function, or unknown. 1023 | * 1024 | * @private 1025 | * @param {Mixed} object The owner of the property. 1026 | * @param {String} property The property to check. 1027 | * @returns {Boolean} Returns `true` if the property value is a non-primitive, else `false`. 1028 | */ 1029 | function isHostType(object, property) { 1030 | var type = object != null ? typeof object[property] : 'number'; 1031 | return !/^(?:boolean|number|string|undefined)$/.test(type) && 1032 | (type == 'object' ? !!object[property] : true); 1033 | } 1034 | 1035 | /** 1036 | * Checks if a given `value` is an object created by the `Object` constructor 1037 | * assuming objects created by the `Object` constructor have no inherited 1038 | * enumerable properties and that there are no `Object.prototype` extensions. 1039 | * 1040 | * @private 1041 | * @param {Mixed} value The value to check. 1042 | * @returns {Boolean} Returns `true` if the `value` is a plain `Object` object, else `false`. 1043 | */ 1044 | function isPlainObject(value) { 1045 | // avoid non-objects and false positives for `arguments` objects in IE < 9 1046 | var result = false; 1047 | if (!(value && typeof value == 'object') || isArguments(value)) { 1048 | return result; 1049 | } 1050 | // IE < 9 presents DOM nodes as `Object` objects except they have `toString` 1051 | // methods that are `typeof` "string" and still can coerce nodes to strings. 1052 | // Also check that the constructor is `Object` (i.e. `Object instanceof Object`) 1053 | var ctor = value.constructor; 1054 | if ((support.nodeClass || !(typeof value.toString != 'function' && typeof (value + '') == 'string')) && 1055 | (!isClassOf(ctor, 'Function') || ctor instanceof ctor)) { 1056 | // In most environments an object's own properties are iterated before 1057 | // its inherited properties. If the last iterated property is an object's 1058 | // own property then there are no inherited enumerable properties. 1059 | if (support.iteratesOwnFirst) { 1060 | forProps(value, function(subValue, subKey) { 1061 | result = subKey; 1062 | }); 1063 | return result === false || hasKey(value, result); 1064 | } 1065 | // IE < 9 iterates inherited properties before own properties. If the first 1066 | // iterated property is an object's own property then there are no inherited 1067 | // enumerable properties. 1068 | forProps(value, function(subValue, subKey) { 1069 | result = !hasKey(value, subKey); 1070 | return false; 1071 | }); 1072 | return result === false; 1073 | } 1074 | return result; 1075 | } 1076 | 1077 | /** 1078 | * Checks if a value can be safely coerced to a string. 1079 | * 1080 | * @private 1081 | * @param {Mixed} value The value to check. 1082 | * @returns {Boolean} Returns `true` if the value can be coerced, else `false`. 1083 | */ 1084 | function isStringable(value) { 1085 | return hasKey(value, 'toString') || isClassOf(value, 'String'); 1086 | } 1087 | 1088 | /** 1089 | * Wraps a function and passes `this` to the original function as the 1090 | * first argument. 1091 | * 1092 | * @private 1093 | * @param {Function} fn The function to be wrapped. 1094 | * @returns {Function} The new function. 1095 | */ 1096 | function methodize(fn) { 1097 | return function() { 1098 | var args = [this]; 1099 | args.push.apply(args, arguments); 1100 | return fn.apply(null, args); 1101 | }; 1102 | } 1103 | 1104 | /** 1105 | * A no-operation function. 1106 | * 1107 | * @private 1108 | */ 1109 | function noop() { 1110 | // no operation performed 1111 | } 1112 | 1113 | /** 1114 | * A wrapper around require() to suppress `module missing` errors. 1115 | * 1116 | * @private 1117 | * @param {String} id The module id. 1118 | * @returns {Mixed} The exported module or `null`. 1119 | */ 1120 | function req(id) { 1121 | try { 1122 | var result = freeExports && freeRequire(id); 1123 | } catch(e) { } 1124 | return result || null; 1125 | } 1126 | 1127 | /** 1128 | * Runs a snippet of JavaScript via script injection. 1129 | * 1130 | * @private 1131 | * @param {String} code The code to run. 1132 | */ 1133 | function runScript(code) { 1134 | var anchor = freeDefine ? define.amd : Benchmark, 1135 | script = doc.createElement('script'), 1136 | sibling = doc.getElementsByTagName('script')[0], 1137 | parent = sibling.parentNode, 1138 | prop = uid + 'runScript', 1139 | prefix = '(' + (freeDefine ? 'define.amd.' : 'Benchmark.') + prop + '||function(){})();'; 1140 | 1141 | // Firefox 2.0.0.2 cannot use script injection as intended because it executes 1142 | // asynchronously, but that's OK because script injection is only used to avoid 1143 | // the previously commented JaegerMonkey bug. 1144 | try { 1145 | // remove the inserted script *before* running the code to avoid differences 1146 | // in the expected script element count/order of the document. 1147 | script.appendChild(doc.createTextNode(prefix + code)); 1148 | anchor[prop] = function() { destroyElement(script); }; 1149 | } catch(e) { 1150 | parent = parent.cloneNode(false); 1151 | sibling = null; 1152 | script.text = code; 1153 | } 1154 | parent.insertBefore(script, sibling); 1155 | delete anchor[prop]; 1156 | } 1157 | 1158 | /** 1159 | * A helper function for setting options/event handlers. 1160 | * 1161 | * @private 1162 | * @param {Object} bench The benchmark instance. 1163 | * @param {Object} [options={}] Options object. 1164 | */ 1165 | function setOptions(bench, options) { 1166 | options = extend({}, bench.constructor.options, options); 1167 | bench.options = forOwn(options, function(value, key) { 1168 | if (value != null) { 1169 | // add event listeners 1170 | if (/^on[A-Z]/.test(key)) { 1171 | forEach(key.split(' '), function(key) { 1172 | bench.on(key.slice(2).toLowerCase(), value); 1173 | }); 1174 | } else if (!hasKey(bench, key)) { 1175 | bench[key] = deepClone(value); 1176 | } 1177 | } 1178 | }); 1179 | } 1180 | 1181 | /*--------------------------------------------------------------------------*/ 1182 | 1183 | /** 1184 | * Handles cycling/completing the deferred benchmark. 1185 | * 1186 | * @memberOf Benchmark.Deferred 1187 | */ 1188 | function resolve() { 1189 | var me = this, 1190 | clone = me.benchmark, 1191 | bench = clone._original; 1192 | 1193 | if (bench.aborted) { 1194 | // cycle() -> clone cycle/complete event -> compute()'s invoked bench.run() cycle/complete 1195 | me.teardown(); 1196 | clone.running = false; 1197 | cycle(me); 1198 | } 1199 | else if (++me.cycles < clone.count) { 1200 | // continue the test loop 1201 | if (support.timeout) { 1202 | // use setTimeout to avoid a call stack overflow if called recursively 1203 | setTimeout(function() { clone.compiled.call(me, timer); }, 0); 1204 | } else { 1205 | clone.compiled.call(me, timer); 1206 | } 1207 | } 1208 | else { 1209 | timer.stop(me); 1210 | me.teardown(); 1211 | delay(clone, function() { cycle(me); }); 1212 | } 1213 | } 1214 | 1215 | /*--------------------------------------------------------------------------*/ 1216 | 1217 | /** 1218 | * A deep clone utility. 1219 | * 1220 | * @static 1221 | * @memberOf Benchmark 1222 | * @param {Mixed} value The value to clone. 1223 | * @returns {Mixed} The cloned value. 1224 | */ 1225 | function deepClone(value) { 1226 | var accessor, 1227 | circular, 1228 | clone, 1229 | ctor, 1230 | descriptor, 1231 | extensible, 1232 | key, 1233 | length, 1234 | markerKey, 1235 | parent, 1236 | result, 1237 | source, 1238 | subIndex, 1239 | data = { 'value': value }, 1240 | index = 0, 1241 | marked = [], 1242 | queue = { 'length': 0 }, 1243 | unmarked = []; 1244 | 1245 | /** 1246 | * An easily detectable decorator for cloned values. 1247 | */ 1248 | function Marker(object) { 1249 | this.raw = object; 1250 | } 1251 | 1252 | /** 1253 | * The callback used by `forProps()`. 1254 | */ 1255 | function forPropsCallback(subValue, subKey) { 1256 | // exit early to avoid cloning the marker 1257 | if (subValue && subValue.constructor == Marker) { 1258 | return; 1259 | } 1260 | // add objects to the queue 1261 | if (subValue === Object(subValue)) { 1262 | queue[queue.length++] = { 'key': subKey, 'parent': clone, 'source': value }; 1263 | } 1264 | // assign non-objects 1265 | else { 1266 | try { 1267 | // will throw an error in strict mode if the property is read-only 1268 | clone[subKey] = subValue; 1269 | } catch(e) { } 1270 | } 1271 | } 1272 | 1273 | /** 1274 | * Gets an available marker key for the given object. 1275 | */ 1276 | function getMarkerKey(object) { 1277 | // avoid collisions with existing keys 1278 | var result = uid; 1279 | while (object[result] && object[result].constructor != Marker) { 1280 | result += 1; 1281 | } 1282 | return result; 1283 | } 1284 | 1285 | do { 1286 | key = data.key; 1287 | parent = data.parent; 1288 | source = data.source; 1289 | clone = value = source ? source[key] : data.value; 1290 | accessor = circular = descriptor = false; 1291 | 1292 | // create a basic clone to filter out functions, DOM elements, and 1293 | // other non `Object` objects 1294 | if (value === Object(value)) { 1295 | // use custom deep clone function if available 1296 | if (isClassOf(value.deepClone, 'Function')) { 1297 | clone = value.deepClone(); 1298 | } else { 1299 | ctor = value.constructor; 1300 | switch (toString.call(value)) { 1301 | case '[object Array]': 1302 | clone = new ctor(value.length); 1303 | break; 1304 | 1305 | case '[object Boolean]': 1306 | clone = new ctor(value == true); 1307 | break; 1308 | 1309 | case '[object Date]': 1310 | clone = new ctor(+value); 1311 | break; 1312 | 1313 | case '[object Object]': 1314 | isPlainObject(value) && (clone = {}); 1315 | break; 1316 | 1317 | case '[object Number]': 1318 | case '[object String]': 1319 | clone = new ctor(value); 1320 | break; 1321 | 1322 | case '[object RegExp]': 1323 | clone = ctor(value.source, 1324 | (value.global ? 'g' : '') + 1325 | (value.ignoreCase ? 'i' : '') + 1326 | (value.multiline ? 'm' : '')); 1327 | } 1328 | } 1329 | // continue clone if `value` doesn't have an accessor descriptor 1330 | // http://es5.github.com/#x8.10.1 1331 | if (clone && clone != value && 1332 | !(descriptor = source && support.descriptors && getDescriptor(source, key), 1333 | accessor = descriptor && (descriptor.get || descriptor.set))) { 1334 | // use an existing clone (circular reference) 1335 | if ((extensible = isExtensible(value))) { 1336 | markerKey = getMarkerKey(value); 1337 | if (value[markerKey]) { 1338 | circular = clone = value[markerKey].raw; 1339 | } 1340 | } else { 1341 | // for frozen/sealed objects 1342 | for (subIndex = 0, length = unmarked.length; subIndex < length; subIndex++) { 1343 | data = unmarked[subIndex]; 1344 | if (data.object === value) { 1345 | circular = clone = data.clone; 1346 | break; 1347 | } 1348 | } 1349 | } 1350 | if (!circular) { 1351 | // mark object to allow quickly detecting circular references and tie it to its clone 1352 | if (extensible) { 1353 | value[markerKey] = new Marker(clone); 1354 | marked.push({ 'key': markerKey, 'object': value }); 1355 | } else { 1356 | // for frozen/sealed objects 1357 | unmarked.push({ 'clone': clone, 'object': value }); 1358 | } 1359 | // iterate over object properties 1360 | forProps(value, forPropsCallback, { 'which': 'all' }); 1361 | } 1362 | } 1363 | } 1364 | if (parent) { 1365 | // for custom property descriptors 1366 | if (accessor || (descriptor && !(descriptor.configurable && descriptor.enumerable && descriptor.writable))) { 1367 | if ('value' in descriptor) { 1368 | descriptor.value = clone; 1369 | } 1370 | setDescriptor(parent, key, descriptor); 1371 | } 1372 | // for default property descriptors 1373 | else { 1374 | parent[key] = clone; 1375 | } 1376 | } else { 1377 | result = clone; 1378 | } 1379 | } while ((data = queue[index++])); 1380 | 1381 | // remove markers 1382 | for (index = 0, length = marked.length; index < length; index++) { 1383 | data = marked[index]; 1384 | delete data.object[data.key]; 1385 | } 1386 | return result; 1387 | } 1388 | 1389 | /** 1390 | * An iteration utility for arrays and objects. 1391 | * Callbacks may terminate the loop by explicitly returning `false`. 1392 | * 1393 | * @static 1394 | * @memberOf Benchmark 1395 | * @param {Array|Object} object The object to iterate over. 1396 | * @param {Function} callback The function called per iteration. 1397 | * @param {Mixed} thisArg The `this` binding for the callback. 1398 | * @returns {Array|Object} Returns the object iterated over. 1399 | */ 1400 | function each(object, callback, thisArg) { 1401 | var result = object; 1402 | object = Object(object); 1403 | 1404 | var fn = callback, 1405 | index = -1, 1406 | length = object.length, 1407 | isSnapshot = !!(object.snapshotItem && (length = object.snapshotLength)), 1408 | isSplittable = (noCharByIndex || noCharByOwnIndex) && isClassOf(object, 'String'), 1409 | isConvertable = isSnapshot || isSplittable || 'item' in object, 1410 | origObject = object; 1411 | 1412 | // in Opera < 10.5 `hasKey(object, 'length')` returns `false` for NodeLists 1413 | if (length === length >>> 0) { 1414 | if (isConvertable) { 1415 | // the third argument of the callback is the original non-array object 1416 | callback = function(value, index) { 1417 | return fn.call(this, value, index, origObject); 1418 | }; 1419 | // in IE < 9 strings don't support accessing characters by index 1420 | if (isSplittable) { 1421 | object = object.split(''); 1422 | } else { 1423 | object = []; 1424 | while (++index < length) { 1425 | // in Safari 2 `index in object` is always `false` for NodeLists 1426 | object[index] = isSnapshot ? result.snapshotItem(index) : result[index]; 1427 | } 1428 | } 1429 | } 1430 | forEach(object, callback, thisArg); 1431 | } else { 1432 | forOwn(object, callback, thisArg); 1433 | } 1434 | return result; 1435 | } 1436 | 1437 | /** 1438 | * Copies enumerable properties from the source(s) object to the destination object. 1439 | * 1440 | * @static 1441 | * @memberOf Benchmark 1442 | * @param {Object} destination The destination object. 1443 | * @param {Object} [source={}] The source object. 1444 | * @returns {Object} The destination object. 1445 | */ 1446 | function extend(destination, source) { 1447 | // Chrome < 14 incorrectly sets `destination` to `undefined` when we `delete arguments[0]` 1448 | // http://code.google.com/p/v8/issues/detail?id=839 1449 | var result = destination; 1450 | delete arguments[0]; 1451 | 1452 | forEach(arguments, function(source) { 1453 | forProps(source, function(value, key) { 1454 | result[key] = value; 1455 | }); 1456 | }); 1457 | return result; 1458 | } 1459 | 1460 | /** 1461 | * A generic `Array#filter` like method. 1462 | * 1463 | * @static 1464 | * @memberOf Benchmark 1465 | * @param {Array} array The array to iterate over. 1466 | * @param {Function|String} callback The function/alias called per iteration. 1467 | * @param {Mixed} thisArg The `this` binding for the callback. 1468 | * @returns {Array} A new array of values that passed callback filter. 1469 | * @example 1470 | * 1471 | * // get odd numbers 1472 | * Benchmark.filter([1, 2, 3, 4, 5], function(n) { 1473 | * return n % 2; 1474 | * }); // -> [1, 3, 5]; 1475 | * 1476 | * // get fastest benchmarks 1477 | * Benchmark.filter(benches, 'fastest'); 1478 | * 1479 | * // get slowest benchmarks 1480 | * Benchmark.filter(benches, 'slowest'); 1481 | * 1482 | * // get benchmarks that completed without erroring 1483 | * Benchmark.filter(benches, 'successful'); 1484 | */ 1485 | function filter(array, callback, thisArg) { 1486 | var result; 1487 | 1488 | if (callback == 'successful') { 1489 | // callback to exclude those that are errored, unrun, or have hz of Infinity 1490 | callback = function(bench) { return bench.cycles && isFinite(bench.hz); }; 1491 | } 1492 | else if (callback == 'fastest' || callback == 'slowest') { 1493 | // get successful, sort by period + margin of error, and filter fastest/slowest 1494 | result = filter(array, 'successful').sort(function(a, b) { 1495 | a = a.stats; b = b.stats; 1496 | return (a.mean + a.moe > b.mean + b.moe ? 1 : -1) * (callback == 'fastest' ? 1 : -1); 1497 | }); 1498 | result = filter(result, function(bench) { 1499 | return result[0].compare(bench) == 0; 1500 | }); 1501 | } 1502 | return result || reduce(array, function(result, value, index) { 1503 | return callback.call(thisArg, value, index, array) ? (result.push(value), result) : result; 1504 | }, []); 1505 | } 1506 | 1507 | /** 1508 | * A generic `Array#forEach` like method. 1509 | * Callbacks may terminate the loop by explicitly returning `false`. 1510 | * 1511 | * @static 1512 | * @memberOf Benchmark 1513 | * @param {Array} array The array to iterate over. 1514 | * @param {Function} callback The function called per iteration. 1515 | * @param {Mixed} thisArg The `this` binding for the callback. 1516 | * @returns {Array} Returns the array iterated over. 1517 | */ 1518 | function forEach(array, callback, thisArg) { 1519 | var index = -1, 1520 | length = (array = Object(array)).length >>> 0; 1521 | 1522 | if (thisArg !== undefined) { 1523 | callback = bind(callback, thisArg); 1524 | } 1525 | while (++index < length) { 1526 | if (index in array && 1527 | callback(array[index], index, array) === false) { 1528 | break; 1529 | } 1530 | } 1531 | return array; 1532 | } 1533 | 1534 | /** 1535 | * Iterates over an object's own properties, executing the `callback` for each. 1536 | * Callbacks may terminate the loop by explicitly returning `false`. 1537 | * 1538 | * @static 1539 | * @memberOf Benchmark 1540 | * @param {Object} object The object to iterate over. 1541 | * @param {Function} callback The function executed per own property. 1542 | * @param {Mixed} thisArg The `this` binding for the callback. 1543 | * @returns {Object} Returns the object iterated over. 1544 | */ 1545 | function forOwn(object, callback, thisArg) { 1546 | return forProps(object, callback, { 'bind': thisArg, 'which': 'own' }); 1547 | } 1548 | 1549 | /** 1550 | * Converts a number to a more readable comma-separated string representation. 1551 | * 1552 | * @static 1553 | * @memberOf Benchmark 1554 | * @param {Number} number The number to convert. 1555 | * @returns {String} The more readable string representation. 1556 | */ 1557 | function formatNumber(number) { 1558 | number = String(number).split('.'); 1559 | return number[0].replace(/(?=(?:\d{3})+$)(?!\b)/g, ',') + 1560 | (number[1] ? '.' + number[1] : ''); 1561 | } 1562 | 1563 | /** 1564 | * Checks if an object has the specified key as a direct property. 1565 | * 1566 | * @static 1567 | * @memberOf Benchmark 1568 | * @param {Object} object The object to check. 1569 | * @param {String} key The key to check for. 1570 | * @returns {Boolean} Returns `true` if key is a direct property, else `false`. 1571 | */ 1572 | function hasKey() { 1573 | // lazy define for worst case fallback (not as accurate) 1574 | hasKey = function(object, key) { 1575 | var parent = object != null && (object.constructor || Object).prototype; 1576 | return !!parent && key in Object(object) && !(key in parent && object[key] === parent[key]); 1577 | }; 1578 | // for modern browsers 1579 | if (isClassOf(hasOwnProperty, 'Function')) { 1580 | hasKey = function(object, key) { 1581 | return object != null && hasOwnProperty.call(object, key); 1582 | }; 1583 | } 1584 | // for Safari 2 1585 | else if ({}.__proto__ == Object.prototype) { 1586 | hasKey = function(object, key) { 1587 | var result = false; 1588 | if (object != null) { 1589 | object = Object(object); 1590 | object.__proto__ = [object.__proto__, object.__proto__ = null, result = key in object][0]; 1591 | } 1592 | return result; 1593 | }; 1594 | } 1595 | return hasKey.apply(this, arguments); 1596 | } 1597 | 1598 | /** 1599 | * A generic `Array#indexOf` like method. 1600 | * 1601 | * @static 1602 | * @memberOf Benchmark 1603 | * @param {Array} array The array to iterate over. 1604 | * @param {Mixed} value The value to search for. 1605 | * @param {Number} [fromIndex=0] The index to start searching from. 1606 | * @returns {Number} The index of the matched value or `-1`. 1607 | */ 1608 | function indexOf(array, value, fromIndex) { 1609 | var index = toInteger(fromIndex), 1610 | length = (array = Object(array)).length >>> 0; 1611 | 1612 | index = (index < 0 ? max(0, length + index) : index) - 1; 1613 | while (++index < length) { 1614 | if (index in array && value === array[index]) { 1615 | return index; 1616 | } 1617 | } 1618 | return -1; 1619 | } 1620 | 1621 | /** 1622 | * Modify a string by replacing named tokens with matching object property values. 1623 | * 1624 | * @static 1625 | * @memberOf Benchmark 1626 | * @param {String} string The string to modify. 1627 | * @param {Object} object The template object. 1628 | * @returns {String} The modified string. 1629 | */ 1630 | function interpolate(string, object) { 1631 | forOwn(object, function(value, key) { 1632 | // escape regexp special characters in `key` 1633 | string = string.replace(RegExp('#\\{' + key.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1') + '\\}', 'g'), value); 1634 | }); 1635 | return string; 1636 | } 1637 | 1638 | /** 1639 | * Invokes a method on all items in an array. 1640 | * 1641 | * @static 1642 | * @memberOf Benchmark 1643 | * @param {Array} benches Array of benchmarks to iterate over. 1644 | * @param {String|Object} name The name of the method to invoke OR options object. 1645 | * @param {Mixed} [arg1, arg2, ...] Arguments to invoke the method with. 1646 | * @returns {Array} A new array of values returned from each method invoked. 1647 | * @example 1648 | * 1649 | * // invoke `reset` on all benchmarks 1650 | * Benchmark.invoke(benches, 'reset'); 1651 | * 1652 | * // invoke `emit` with arguments 1653 | * Benchmark.invoke(benches, 'emit', 'complete', listener); 1654 | * 1655 | * // invoke `run(true)`, treat benchmarks as a queue, and register invoke callbacks 1656 | * Benchmark.invoke(benches, { 1657 | * 1658 | * // invoke the `run` method 1659 | * 'name': 'run', 1660 | * 1661 | * // pass a single argument 1662 | * 'args': true, 1663 | * 1664 | * // treat as queue, removing benchmarks from front of `benches` until empty 1665 | * 'queued': true, 1666 | * 1667 | * // called before any benchmarks have been invoked. 1668 | * 'onStart': onStart, 1669 | * 1670 | * // called between invoking benchmarks 1671 | * 'onCycle': onCycle, 1672 | * 1673 | * // called after all benchmarks have been invoked. 1674 | * 'onComplete': onComplete 1675 | * }); 1676 | */ 1677 | function invoke(benches, name) { 1678 | var args, 1679 | bench, 1680 | queued, 1681 | index = -1, 1682 | eventProps = { 'currentTarget': benches }, 1683 | options = { 'onStart': noop, 'onCycle': noop, 'onComplete': noop }, 1684 | result = map(benches, function(bench) { return bench; }); 1685 | 1686 | /** 1687 | * Invokes the method of the current object and if synchronous, fetches the next. 1688 | */ 1689 | function execute() { 1690 | var listeners, 1691 | async = isAsync(bench); 1692 | 1693 | if (async) { 1694 | // use `getNext` as the first listener 1695 | bench.on('complete', getNext); 1696 | listeners = bench.events.complete; 1697 | listeners.splice(0, 0, listeners.pop()); 1698 | } 1699 | // execute method 1700 | result[index] = isClassOf(bench && bench[name], 'Function') ? bench[name].apply(bench, args) : undefined; 1701 | // if synchronous return true until finished 1702 | return !async && getNext(); 1703 | } 1704 | 1705 | /** 1706 | * Fetches the next bench or executes `onComplete` callback. 1707 | */ 1708 | function getNext(event) { 1709 | var cycleEvent, 1710 | last = bench, 1711 | async = isAsync(last); 1712 | 1713 | if (async) { 1714 | last.off('complete', getNext); 1715 | last.emit('complete'); 1716 | } 1717 | // emit "cycle" event 1718 | eventProps.type = 'cycle'; 1719 | eventProps.target = last; 1720 | cycleEvent = Event(eventProps); 1721 | options.onCycle.call(benches, cycleEvent); 1722 | 1723 | // choose next benchmark if not exiting early 1724 | if (!cycleEvent.aborted && raiseIndex() !== false) { 1725 | bench = queued ? benches[0] : result[index]; 1726 | if (isAsync(bench)) { 1727 | delay(bench, execute); 1728 | } 1729 | else if (async) { 1730 | // resume execution if previously asynchronous but now synchronous 1731 | while (execute()) { } 1732 | } 1733 | else { 1734 | // continue synchronous execution 1735 | return true; 1736 | } 1737 | } else { 1738 | // emit "complete" event 1739 | eventProps.type = 'complete'; 1740 | options.onComplete.call(benches, Event(eventProps)); 1741 | } 1742 | // When used as a listener `event.aborted = true` will cancel the rest of 1743 | // the "complete" listeners because they were already called above and when 1744 | // used as part of `getNext` the `return false` will exit the execution while-loop. 1745 | if (event) { 1746 | event.aborted = true; 1747 | } else { 1748 | return false; 1749 | } 1750 | } 1751 | 1752 | /** 1753 | * Checks if invoking `Benchmark#run` with asynchronous cycles. 1754 | */ 1755 | function isAsync(object) { 1756 | // avoid using `instanceof` here because of IE memory leak issues with host objects 1757 | var async = args[0] && args[0].async; 1758 | return Object(object).constructor == Benchmark && name == 'run' && 1759 | ((async == null ? object.options.async : async) && support.timeout || object.defer); 1760 | } 1761 | 1762 | /** 1763 | * Raises `index` to the next defined index or returns `false`. 1764 | */ 1765 | function raiseIndex() { 1766 | var length = result.length; 1767 | if (queued) { 1768 | // if queued remove the previous bench and subsequent skipped non-entries 1769 | do { 1770 | ++index > 0 && shift.call(benches); 1771 | } while ((length = benches.length) && !('0' in benches)); 1772 | } 1773 | else { 1774 | while (++index < length && !(index in result)) { } 1775 | } 1776 | // if we reached the last index then return `false` 1777 | return (queued ? length : index < length) ? index : (index = false); 1778 | } 1779 | 1780 | // juggle arguments 1781 | if (isClassOf(name, 'String')) { 1782 | // 2 arguments (array, name) 1783 | args = slice.call(arguments, 2); 1784 | } else { 1785 | // 2 arguments (array, options) 1786 | options = extend(options, name); 1787 | name = options.name; 1788 | args = isClassOf(args = 'args' in options ? options.args : [], 'Array') ? args : [args]; 1789 | queued = options.queued; 1790 | } 1791 | 1792 | // start iterating over the array 1793 | if (raiseIndex() !== false) { 1794 | // emit "start" event 1795 | bench = result[index]; 1796 | eventProps.type = 'start'; 1797 | eventProps.target = bench; 1798 | options.onStart.call(benches, Event(eventProps)); 1799 | 1800 | // end early if the suite was aborted in an "onStart" listener 1801 | if (benches.aborted && benches.constructor == Suite && name == 'run') { 1802 | // emit "cycle" event 1803 | eventProps.type = 'cycle'; 1804 | options.onCycle.call(benches, Event(eventProps)); 1805 | // emit "complete" event 1806 | eventProps.type = 'complete'; 1807 | options.onComplete.call(benches, Event(eventProps)); 1808 | } 1809 | // else start 1810 | else { 1811 | if (isAsync(bench)) { 1812 | delay(bench, execute); 1813 | } else { 1814 | while (execute()) { } 1815 | } 1816 | } 1817 | } 1818 | return result; 1819 | } 1820 | 1821 | /** 1822 | * Creates a string of joined array values or object key-value pairs. 1823 | * 1824 | * @static 1825 | * @memberOf Benchmark 1826 | * @param {Array|Object} object The object to operate on. 1827 | * @param {String} [separator1=','] The separator used between key-value pairs. 1828 | * @param {String} [separator2=': '] The separator used between keys and values. 1829 | * @returns {String} The joined result. 1830 | */ 1831 | function join(object, separator1, separator2) { 1832 | var result = [], 1833 | length = (object = Object(object)).length, 1834 | arrayLike = length === length >>> 0; 1835 | 1836 | separator2 || (separator2 = ': '); 1837 | each(object, function(value, key) { 1838 | result.push(arrayLike ? value : key + separator2 + value); 1839 | }); 1840 | return result.join(separator1 || ','); 1841 | } 1842 | 1843 | /** 1844 | * A generic `Array#map` like method. 1845 | * 1846 | * @static 1847 | * @memberOf Benchmark 1848 | * @param {Array} array The array to iterate over. 1849 | * @param {Function} callback The function called per iteration. 1850 | * @param {Mixed} thisArg The `this` binding for the callback. 1851 | * @returns {Array} A new array of values returned by the callback. 1852 | */ 1853 | function map(array, callback, thisArg) { 1854 | return reduce(array, function(result, value, index) { 1855 | result[index] = callback.call(thisArg, value, index, array); 1856 | return result; 1857 | }, Array(Object(array).length >>> 0)); 1858 | } 1859 | 1860 | /** 1861 | * Retrieves the value of a specified property from all items in an array. 1862 | * 1863 | * @static 1864 | * @memberOf Benchmark 1865 | * @param {Array} array The array to iterate over. 1866 | * @param {String} property The property to pluck. 1867 | * @returns {Array} A new array of property values. 1868 | */ 1869 | function pluck(array, property) { 1870 | return map(array, function(object) { 1871 | return object == null ? undefined : object[property]; 1872 | }); 1873 | } 1874 | 1875 | /** 1876 | * A generic `Array#reduce` like method. 1877 | * 1878 | * @static 1879 | * @memberOf Benchmark 1880 | * @param {Array} array The array to iterate over. 1881 | * @param {Function} callback The function called per iteration. 1882 | * @param {Mixed} accumulator Initial value of the accumulator. 1883 | * @returns {Mixed} The accumulator. 1884 | */ 1885 | function reduce(array, callback, accumulator) { 1886 | var noaccum = arguments.length < 3; 1887 | forEach(array, function(value, index) { 1888 | accumulator = noaccum ? (noaccum = false, value) : callback(accumulator, value, index, array); 1889 | }); 1890 | return accumulator; 1891 | } 1892 | 1893 | /*--------------------------------------------------------------------------*/ 1894 | 1895 | /** 1896 | * Aborts all benchmarks in the suite. 1897 | * 1898 | * @name abort 1899 | * @memberOf Benchmark.Suite 1900 | * @returns {Object} The suite instance. 1901 | */ 1902 | function abortSuite() { 1903 | var event, 1904 | me = this, 1905 | resetting = calledBy.resetSuite; 1906 | 1907 | if (me.running) { 1908 | event = Event('abort'); 1909 | me.emit(event); 1910 | if (!event.cancelled || resetting) { 1911 | // avoid infinite recursion 1912 | calledBy.abortSuite = true; 1913 | me.reset(); 1914 | delete calledBy.abortSuite; 1915 | 1916 | if (!resetting) { 1917 | me.aborted = true; 1918 | invoke(me, 'abort'); 1919 | } 1920 | } 1921 | } 1922 | return me; 1923 | } 1924 | 1925 | /** 1926 | * Adds a test to the benchmark suite. 1927 | * 1928 | * @memberOf Benchmark.Suite 1929 | * @param {String} name A name to identify the benchmark. 1930 | * @param {Function|String} fn The test to benchmark. 1931 | * @param {Object} [options={}] Options object. 1932 | * @returns {Object} The benchmark instance. 1933 | * @example 1934 | * 1935 | * // basic usage 1936 | * suite.add(fn); 1937 | * 1938 | * // or using a name first 1939 | * suite.add('foo', fn); 1940 | * 1941 | * // or with options 1942 | * suite.add('foo', fn, { 1943 | * 'onCycle': onCycle, 1944 | * 'onComplete': onComplete 1945 | * }); 1946 | * 1947 | * // or name and options 1948 | * suite.add('foo', { 1949 | * 'fn': fn, 1950 | * 'onCycle': onCycle, 1951 | * 'onComplete': onComplete 1952 | * }); 1953 | * 1954 | * // or options only 1955 | * suite.add({ 1956 | * 'name': 'foo', 1957 | * 'fn': fn, 1958 | * 'onCycle': onCycle, 1959 | * 'onComplete': onComplete 1960 | * }); 1961 | */ 1962 | function add(name, fn, options) { 1963 | var me = this, 1964 | bench = Benchmark(name, fn, options), 1965 | event = Event({ 'type': 'add', 'target': bench }); 1966 | 1967 | if (me.emit(event), !event.cancelled) { 1968 | me.push(bench); 1969 | } 1970 | return me; 1971 | } 1972 | 1973 | /** 1974 | * Creates a new suite with cloned benchmarks. 1975 | * 1976 | * @name clone 1977 | * @memberOf Benchmark.Suite 1978 | * @param {Object} options Options object to overwrite cloned options. 1979 | * @returns {Object} The new suite instance. 1980 | */ 1981 | function cloneSuite(options) { 1982 | var me = this, 1983 | result = new me.constructor(extend({}, me.options, options)); 1984 | 1985 | // copy own properties 1986 | forOwn(me, function(value, key) { 1987 | if (!hasKey(result, key)) { 1988 | result[key] = value && isClassOf(value.clone, 'Function') 1989 | ? value.clone() 1990 | : deepClone(value); 1991 | } 1992 | }); 1993 | return result; 1994 | } 1995 | 1996 | /** 1997 | * An `Array#filter` like method. 1998 | * 1999 | * @name filter 2000 | * @memberOf Benchmark.Suite 2001 | * @param {Function|String} callback The function/alias called per iteration. 2002 | * @returns {Object} A new suite of benchmarks that passed callback filter. 2003 | */ 2004 | function filterSuite(callback) { 2005 | var me = this, 2006 | result = new me.constructor; 2007 | 2008 | result.push.apply(result, filter(me, callback)); 2009 | return result; 2010 | } 2011 | 2012 | /** 2013 | * Resets all benchmarks in the suite. 2014 | * 2015 | * @name reset 2016 | * @memberOf Benchmark.Suite 2017 | * @returns {Object} The suite instance. 2018 | */ 2019 | function resetSuite() { 2020 | var event, 2021 | me = this, 2022 | aborting = calledBy.abortSuite; 2023 | 2024 | if (me.running && !aborting) { 2025 | // no worries, `resetSuite()` is called within `abortSuite()` 2026 | calledBy.resetSuite = true; 2027 | me.abort(); 2028 | delete calledBy.resetSuite; 2029 | } 2030 | // reset if the state has changed 2031 | else if ((me.aborted || me.running) && 2032 | (me.emit(event = Event('reset')), !event.cancelled)) { 2033 | me.running = false; 2034 | if (!aborting) { 2035 | invoke(me, 'reset'); 2036 | } 2037 | } 2038 | return me; 2039 | } 2040 | 2041 | /** 2042 | * Runs the suite. 2043 | * 2044 | * @name run 2045 | * @memberOf Benchmark.Suite 2046 | * @param {Object} [options={}] Options object. 2047 | * @returns {Object} The suite instance. 2048 | * @example 2049 | * 2050 | * // basic usage 2051 | * suite.run(); 2052 | * 2053 | * // or with options 2054 | * suite.run({ 'async': true, 'queued': true }); 2055 | */ 2056 | function runSuite(options) { 2057 | var me = this; 2058 | 2059 | me.reset(); 2060 | me.running = true; 2061 | options || (options = {}); 2062 | 2063 | invoke(me, { 2064 | 'name': 'run', 2065 | 'args': options, 2066 | 'queued': options.queued, 2067 | 'onStart': function(event) { 2068 | me.emit(event); 2069 | }, 2070 | 'onCycle': function(event) { 2071 | var bench = event.target; 2072 | if (bench.error) { 2073 | me.emit({ 'type': 'error', 'target': bench }); 2074 | } 2075 | me.emit(event); 2076 | event.aborted = me.aborted; 2077 | }, 2078 | 'onComplete': function(event) { 2079 | me.running = false; 2080 | me.emit(event); 2081 | } 2082 | }); 2083 | return me; 2084 | } 2085 | 2086 | /*--------------------------------------------------------------------------*/ 2087 | 2088 | /** 2089 | * Executes all registered listeners of the specified event type. 2090 | * 2091 | * @memberOf Benchmark, Benchmark.Suite 2092 | * @param {String|Object} type The event type or object. 2093 | * @returns {Mixed} Returns the return value of the last listener executed. 2094 | */ 2095 | function emit(type) { 2096 | var listeners, 2097 | me = this, 2098 | event = Event(type), 2099 | events = me.events, 2100 | args = (arguments[0] = event, arguments); 2101 | 2102 | event.currentTarget || (event.currentTarget = me); 2103 | event.target || (event.target = me); 2104 | delete event.result; 2105 | 2106 | if (events && (listeners = hasKey(events, event.type) && events[event.type])) { 2107 | forEach(listeners.slice(), function(listener) { 2108 | if ((event.result = listener.apply(me, args)) === false) { 2109 | event.cancelled = true; 2110 | } 2111 | return !event.aborted; 2112 | }); 2113 | } 2114 | return event.result; 2115 | } 2116 | 2117 | /** 2118 | * Returns an array of event listeners for a given type that can be manipulated 2119 | * to add or remove listeners. 2120 | * 2121 | * @memberOf Benchmark, Benchmark.Suite 2122 | * @param {String} type The event type. 2123 | * @returns {Array} The listeners array. 2124 | */ 2125 | function listeners(type) { 2126 | var me = this, 2127 | events = me.events || (me.events = {}); 2128 | 2129 | return hasKey(events, type) ? events[type] : (events[type] = []); 2130 | } 2131 | 2132 | /** 2133 | * Unregisters a listener for the specified event type(s), 2134 | * or unregisters all listeners for the specified event type(s), 2135 | * or unregisters all listeners for all event types. 2136 | * 2137 | * @memberOf Benchmark, Benchmark.Suite 2138 | * @param {String} [type] The event type. 2139 | * @param {Function} [listener] The function to unregister. 2140 | * @returns {Object} The benchmark instance. 2141 | * @example 2142 | * 2143 | * // unregister a listener for an event type 2144 | * bench.off('cycle', listener); 2145 | * 2146 | * // unregister a listener for multiple event types 2147 | * bench.off('start cycle', listener); 2148 | * 2149 | * // unregister all listeners for an event type 2150 | * bench.off('cycle'); 2151 | * 2152 | * // unregister all listeners for multiple event types 2153 | * bench.off('start cycle complete'); 2154 | * 2155 | * // unregister all listeners for all event types 2156 | * bench.off(); 2157 | */ 2158 | function off(type, listener) { 2159 | var me = this, 2160 | events = me.events; 2161 | 2162 | events && each(type ? type.split(' ') : events, function(listeners, type) { 2163 | var index; 2164 | if (typeof listeners == 'string') { 2165 | type = listeners; 2166 | listeners = hasKey(events, type) && events[type]; 2167 | } 2168 | if (listeners) { 2169 | if (listener) { 2170 | index = indexOf(listeners, listener); 2171 | if (index > -1) { 2172 | listeners.splice(index, 1); 2173 | } 2174 | } else { 2175 | listeners.length = 0; 2176 | } 2177 | } 2178 | }); 2179 | return me; 2180 | } 2181 | 2182 | /** 2183 | * Registers a listener for the specified event type(s). 2184 | * 2185 | * @memberOf Benchmark, Benchmark.Suite 2186 | * @param {String} type The event type. 2187 | * @param {Function} listener The function to register. 2188 | * @returns {Object} The benchmark instance. 2189 | * @example 2190 | * 2191 | * // register a listener for an event type 2192 | * bench.on('cycle', listener); 2193 | * 2194 | * // register a listener for multiple event types 2195 | * bench.on('start cycle', listener); 2196 | */ 2197 | function on(type, listener) { 2198 | var me = this, 2199 | events = me.events || (me.events = {}); 2200 | 2201 | forEach(type.split(' '), function(type) { 2202 | (hasKey(events, type) 2203 | ? events[type] 2204 | : (events[type] = []) 2205 | ).push(listener); 2206 | }); 2207 | return me; 2208 | } 2209 | 2210 | /*--------------------------------------------------------------------------*/ 2211 | 2212 | /** 2213 | * Aborts the benchmark without recording times. 2214 | * 2215 | * @memberOf Benchmark 2216 | * @returns {Object} The benchmark instance. 2217 | */ 2218 | function abort() { 2219 | var event, 2220 | me = this, 2221 | resetting = calledBy.reset; 2222 | 2223 | if (me.running) { 2224 | event = Event('abort'); 2225 | me.emit(event); 2226 | if (!event.cancelled || resetting) { 2227 | // avoid infinite recursion 2228 | calledBy.abort = true; 2229 | me.reset(); 2230 | delete calledBy.abort; 2231 | 2232 | if (support.timeout) { 2233 | clearTimeout(me._timerId); 2234 | delete me._timerId; 2235 | } 2236 | if (!resetting) { 2237 | me.aborted = true; 2238 | me.running = false; 2239 | } 2240 | } 2241 | } 2242 | return me; 2243 | } 2244 | 2245 | /** 2246 | * Creates a new benchmark using the same test and options. 2247 | * 2248 | * @memberOf Benchmark 2249 | * @param {Object} options Options object to overwrite cloned options. 2250 | * @returns {Object} The new benchmark instance. 2251 | * @example 2252 | * 2253 | * var bizarro = bench.clone({ 2254 | * 'name': 'doppelganger' 2255 | * }); 2256 | */ 2257 | function clone(options) { 2258 | var me = this, 2259 | result = new me.constructor(extend({}, me, options)); 2260 | 2261 | // correct the `options` object 2262 | result.options = extend({}, me.options, options); 2263 | 2264 | // copy own custom properties 2265 | forOwn(me, function(value, key) { 2266 | if (!hasKey(result, key)) { 2267 | result[key] = deepClone(value); 2268 | } 2269 | }); 2270 | return result; 2271 | } 2272 | 2273 | /** 2274 | * Determines if a benchmark is faster than another. 2275 | * 2276 | * @memberOf Benchmark 2277 | * @param {Object} other The benchmark to compare. 2278 | * @returns {Number} Returns `-1` if slower, `1` if faster, and `0` if indeterminate. 2279 | */ 2280 | function compare(other) { 2281 | var critical, 2282 | zStat, 2283 | me = this, 2284 | sample1 = me.stats.sample, 2285 | sample2 = other.stats.sample, 2286 | size1 = sample1.length, 2287 | size2 = sample2.length, 2288 | maxSize = max(size1, size2), 2289 | minSize = min(size1, size2), 2290 | u1 = getU(sample1, sample2), 2291 | u2 = getU(sample2, sample1), 2292 | u = min(u1, u2); 2293 | 2294 | function getScore(xA, sampleB) { 2295 | return reduce(sampleB, function(total, xB) { 2296 | return total + (xB > xA ? 0 : xB < xA ? 1 : 0.5); 2297 | }, 0); 2298 | } 2299 | 2300 | function getU(sampleA, sampleB) { 2301 | return reduce(sampleA, function(total, xA) { 2302 | return total + getScore(xA, sampleB); 2303 | }, 0); 2304 | } 2305 | 2306 | function getZ(u) { 2307 | return (u - ((size1 * size2) / 2)) / sqrt((size1 * size2 * (size1 + size2 + 1)) / 12); 2308 | } 2309 | 2310 | // exit early if comparing the same benchmark 2311 | if (me == other) { 2312 | return 0; 2313 | } 2314 | // reject the null hyphothesis the two samples come from the 2315 | // same population (i.e. have the same median) if... 2316 | if (size1 + size2 > 30) { 2317 | // ...the z-stat is greater than 1.96 or less than -1.96 2318 | // http://www.statisticslectures.com/topics/mannwhitneyu/ 2319 | zStat = getZ(u); 2320 | return abs(zStat) > 1.96 ? (zStat > 0 ? -1 : 1) : 0; 2321 | } 2322 | // ...the U value is less than or equal the critical U value 2323 | // http://www.geoib.com/mann-whitney-u-test.html 2324 | critical = maxSize < 5 || minSize < 3 ? 0 : uTable[maxSize][minSize - 3]; 2325 | return u <= critical ? (u == u1 ? 1 : -1) : 0; 2326 | } 2327 | 2328 | /** 2329 | * Reset properties and abort if running. 2330 | * 2331 | * @memberOf Benchmark 2332 | * @returns {Object} The benchmark instance. 2333 | */ 2334 | function reset() { 2335 | var data, 2336 | event, 2337 | me = this, 2338 | index = 0, 2339 | changes = { 'length': 0 }, 2340 | queue = { 'length': 0 }; 2341 | 2342 | if (me.running && !calledBy.abort) { 2343 | // no worries, `reset()` is called within `abort()` 2344 | calledBy.reset = true; 2345 | me.abort(); 2346 | delete calledBy.reset; 2347 | } 2348 | else { 2349 | // a non-recursive solution to check if properties have changed 2350 | // http://www.jslab.dk/articles/non.recursive.preorder.traversal.part4 2351 | data = { 'destination': me, 'source': extend({}, me.constructor.prototype, me.options) }; 2352 | do { 2353 | forOwn(data.source, function(value, key) { 2354 | var changed, 2355 | destination = data.destination, 2356 | currValue = destination[key]; 2357 | 2358 | if (value && typeof value == 'object') { 2359 | if (isClassOf(value, 'Array')) { 2360 | // check if an array value has changed to a non-array value 2361 | if (!isClassOf(currValue, 'Array')) { 2362 | changed = currValue = []; 2363 | } 2364 | // or has changed its length 2365 | if (currValue.length != value.length) { 2366 | changed = currValue = currValue.slice(0, value.length); 2367 | currValue.length = value.length; 2368 | } 2369 | } 2370 | // check if an object has changed to a non-object value 2371 | else if (!currValue || typeof currValue != 'object') { 2372 | changed = currValue = {}; 2373 | } 2374 | // register a changed object 2375 | if (changed) { 2376 | changes[changes.length++] = { 'destination': destination, 'key': key, 'value': currValue }; 2377 | } 2378 | queue[queue.length++] = { 'destination': currValue, 'source': value }; 2379 | } 2380 | // register a changed primitive 2381 | else if (value !== currValue && !(value == null || isClassOf(value, 'Function'))) { 2382 | changes[changes.length++] = { 'destination': destination, 'key': key, 'value': value }; 2383 | } 2384 | }); 2385 | } 2386 | while ((data = queue[index++])); 2387 | 2388 | // if changed emit the `reset` event and if it isn't cancelled reset the benchmark 2389 | if (changes.length && (me.emit(event = Event('reset')), !event.cancelled)) { 2390 | forEach(changes, function(data) { 2391 | data.destination[data.key] = data.value; 2392 | }); 2393 | } 2394 | } 2395 | return me; 2396 | } 2397 | 2398 | /** 2399 | * Displays relevant benchmark information when coerced to a string. 2400 | * 2401 | * @name toString 2402 | * @memberOf Benchmark 2403 | * @returns {String} A string representation of the benchmark instance. 2404 | */ 2405 | function toStringBench() { 2406 | var me = this, 2407 | error = me.error, 2408 | hz = me.hz, 2409 | id = me.id, 2410 | stats = me.stats, 2411 | size = stats.sample.length, 2412 | pm = support.java ? '+/-' : '\xb1', 2413 | result = me.name || (isNaN(id) ? id : ''); 2414 | 2415 | if (error) { 2416 | result += ': ' + join(error); 2417 | } else { 2418 | result += ' x ' + formatNumber(hz.toFixed(hz < 100 ? 2 : 0)) + ' ops/sec ' + pm + 2419 | stats.rme.toFixed(2) + '% (' + size + ' run' + (size == 1 ? '' : 's') + ' sampled)'; 2420 | } 2421 | return result; 2422 | } 2423 | 2424 | /*--------------------------------------------------------------------------*/ 2425 | 2426 | /** 2427 | * Clocks the time taken to execute a test per cycle (secs). 2428 | * 2429 | * @private 2430 | * @param {Object} bench The benchmark instance. 2431 | * @returns {Number} The time taken. 2432 | */ 2433 | function clock() { 2434 | var applet, 2435 | options = Benchmark.options, 2436 | template = { 'begin': 's$=new n$', 'end': 'r$=(new n$-s$)/1e3', 'uid': uid }, 2437 | timers = [{ 'ns': timer.ns, 'res': max(0.0015, getRes('ms')), 'unit': 'ms' }]; 2438 | 2439 | // lazy define for hi-res timers 2440 | clock = function(clone) { 2441 | var deferred; 2442 | if (clone instanceof Deferred) { 2443 | deferred = clone; 2444 | clone = deferred.benchmark; 2445 | } 2446 | 2447 | var bench = clone._original, 2448 | fn = bench.fn, 2449 | fnArg = deferred ? getFirstArgument(fn) || 'deferred' : '', 2450 | stringable = isStringable(fn); 2451 | 2452 | var source = { 2453 | 'setup': getSource(bench.setup, preprocess('m$.setup()')), 2454 | 'fn': getSource(fn, preprocess('m$.fn(' + fnArg + ')')), 2455 | 'fnArg': fnArg, 2456 | 'teardown': getSource(bench.teardown, preprocess('m$.teardown()')) 2457 | }; 2458 | 2459 | var count = bench.count = clone.count, 2460 | decompilable = support.decompilation || stringable, 2461 | id = bench.id, 2462 | isEmpty = !(source.fn || stringable), 2463 | name = bench.name || (typeof id == 'number' ? '' : id), 2464 | ns = timer.ns, 2465 | result = 0; 2466 | 2467 | // init `minTime` if needed 2468 | clone.minTime = bench.minTime || (bench.minTime = bench.options.minTime = options.minTime); 2469 | 2470 | // repair nanosecond timer 2471 | // (some Chrome builds erase the `ns` variable after millions of executions) 2472 | if (applet) { 2473 | try { 2474 | ns.nanoTime(); 2475 | } catch(e) { 2476 | // use non-element to avoid issues with libs that augment them 2477 | ns = timer.ns = new applet.Packages.nano; 2478 | } 2479 | } 2480 | 2481 | // Compile in setup/teardown functions and the test loop. 2482 | // Create a new compiled test, instead of using the cached `bench.compiled`, 2483 | // to avoid potential engine optimizations enabled over the life of the test. 2484 | var compiled = bench.compiled = createFunction(preprocess('t$'), interpolate( 2485 | preprocess(deferred 2486 | ? 'var d$=this,#{fnArg}=d$,m$=d$.benchmark._original,f$=m$.fn,su$=m$.setup,td$=m$.teardown;' + 2487 | // when `deferred.cycles` is `0` then... 2488 | 'if(!d$.cycles){' + 2489 | // set `deferred.fn` 2490 | 'd$.fn=function(){var #{fnArg}=d$;if(typeof f$=="function"){try{#{fn}\n}catch(e$){f$(d$)}}else{#{fn}\n}};' + 2491 | // set `deferred.teardown` 2492 | 'd$.teardown=function(){d$.cycles=0;if(typeof td$=="function"){try{#{teardown}\n}catch(e$){td$()}}else{#{teardown}\n}};' + 2493 | // execute the benchmark's `setup` 2494 | 'if(typeof su$=="function"){try{#{setup}\n}catch(e$){su$()}}else{#{setup}\n};' + 2495 | // start timer 2496 | 't$.start(d$);' + 2497 | // execute `deferred.fn` and return a dummy object 2498 | '}d$.fn();return{}' 2499 | 2500 | : 'var r$,s$,m$=this,f$=m$.fn,i$=m$.count,n$=t$.ns;#{setup}\n#{begin};' + 2501 | 'while(i$--){#{fn}\n}#{end};#{teardown}\nreturn{elapsed:r$,uid:"#{uid}"}'), 2502 | source 2503 | )); 2504 | 2505 | try { 2506 | if (isEmpty) { 2507 | // Firefox may remove dead code from Function#toString results 2508 | // http://bugzil.la/536085 2509 | throw new Error('The test "' + name + '" is empty. This may be the result of dead code removal.'); 2510 | } 2511 | else if (!deferred) { 2512 | // pretest to determine if compiled code is exits early, usually by a 2513 | // rogue `return` statement, by checking for a return object with the uid 2514 | bench.count = 1; 2515 | compiled = (compiled.call(bench, timer) || {}).uid == uid && compiled; 2516 | bench.count = count; 2517 | } 2518 | } catch(e) { 2519 | compiled = null; 2520 | clone.error = e || new Error(String(e)); 2521 | bench.count = count; 2522 | } 2523 | // fallback when a test exits early or errors during pretest 2524 | if (decompilable && !compiled && !deferred && !isEmpty) { 2525 | compiled = createFunction(preprocess('t$'), interpolate( 2526 | preprocess( 2527 | (clone.error && !stringable 2528 | ? 'var r$,s$,m$=this,f$=m$.fn,i$=m$.count' 2529 | : 'function f$(){#{fn}\n}var r$,s$,m$=this,i$=m$.count' 2530 | ) + 2531 | ',n$=t$.ns;#{setup}\n#{begin};m$.f$=f$;while(i$--){m$.f$()}#{end};' + 2532 | 'delete m$.f$;#{teardown}\nreturn{elapsed:r$}' 2533 | ), 2534 | source 2535 | )); 2536 | 2537 | try { 2538 | // pretest one more time to check for errors 2539 | bench.count = 1; 2540 | compiled.call(bench, timer); 2541 | bench.compiled = compiled; 2542 | bench.count = count; 2543 | delete clone.error; 2544 | } 2545 | catch(e) { 2546 | bench.count = count; 2547 | if (clone.error) { 2548 | compiled = null; 2549 | } else { 2550 | bench.compiled = compiled; 2551 | clone.error = e || new Error(String(e)); 2552 | } 2553 | } 2554 | } 2555 | // assign `compiled` to `clone` before calling in case a deferred benchmark 2556 | // immediately calls `deferred.resolve()` 2557 | clone.compiled = compiled; 2558 | // if no errors run the full test loop 2559 | if (!clone.error) { 2560 | result = compiled.call(deferred || bench, timer).elapsed; 2561 | } 2562 | return result; 2563 | }; 2564 | 2565 | /*------------------------------------------------------------------------*/ 2566 | 2567 | /** 2568 | * Gets the current timer's minimum resolution (secs). 2569 | */ 2570 | function getRes(unit) { 2571 | var measured, 2572 | begin, 2573 | count = 30, 2574 | divisor = 1e3, 2575 | ns = timer.ns, 2576 | sample = []; 2577 | 2578 | // get average smallest measurable time 2579 | while (count--) { 2580 | if (unit == 'us') { 2581 | divisor = 1e6; 2582 | if (ns.stop) { 2583 | ns.start(); 2584 | while (!(measured = ns.microseconds())) { } 2585 | } else if (ns[perfName]) { 2586 | divisor = 1e3; 2587 | measured = Function('n', 'var r,s=n.' + perfName + '();while(!(r=n.' + perfName + '()-s)){};return r')(ns); 2588 | } else { 2589 | begin = ns(); 2590 | while (!(measured = ns() - begin)) { } 2591 | } 2592 | } 2593 | else if (unit == 'ns') { 2594 | divisor = 1e9; 2595 | if (ns.nanoTime) { 2596 | begin = ns.nanoTime(); 2597 | while (!(measured = ns.nanoTime() - begin)) { } 2598 | } else { 2599 | begin = (begin = ns())[0] + (begin[1] / divisor); 2600 | while (!(measured = ((measured = ns())[0] + (measured[1] / divisor)) - begin)) { } 2601 | divisor = 1; 2602 | } 2603 | } 2604 | else { 2605 | begin = new ns; 2606 | while (!(measured = new ns - begin)) { } 2607 | } 2608 | // check for broken timers (nanoTime may have issues) 2609 | // http://alivebutsleepy.srnet.cz/unreliable-system-nanotime/ 2610 | if (measured > 0) { 2611 | sample.push(measured); 2612 | } else { 2613 | sample.push(Infinity); 2614 | break; 2615 | } 2616 | } 2617 | // convert to seconds 2618 | return getMean(sample) / divisor; 2619 | } 2620 | 2621 | /** 2622 | * Replaces all occurrences of `$` with a unique number and 2623 | * template tokens with content. 2624 | */ 2625 | function preprocess(code) { 2626 | return interpolate(code, template).replace(/\$/g, /\d+/.exec(uid)); 2627 | } 2628 | 2629 | /*------------------------------------------------------------------------*/ 2630 | 2631 | // detect nanosecond support from a Java applet 2632 | each(doc && doc.applets || [], function(element) { 2633 | return !(timer.ns = applet = 'nanoTime' in element && element); 2634 | }); 2635 | 2636 | // check type in case Safari returns an object instead of a number 2637 | try { 2638 | if (typeof timer.ns.nanoTime() == 'number') { 2639 | timers.push({ 'ns': timer.ns, 'res': getRes('ns'), 'unit': 'ns' }); 2640 | } 2641 | } catch(e) { } 2642 | 2643 | // detect Chrome's microsecond timer: 2644 | // enable benchmarking via the --enable-benchmarking command 2645 | // line switch in at least Chrome 7 to use chrome.Interval 2646 | try { 2647 | if ((timer.ns = new (window.chrome || window.chromium).Interval)) { 2648 | timers.push({ 'ns': timer.ns, 'res': getRes('us'), 'unit': 'us' }); 2649 | } 2650 | } catch(e) { } 2651 | 2652 | // detect `performance.now` microsecond resolution timer 2653 | if ((timer.ns = perfName && perfObject)) { 2654 | timers.push({ 'ns': timer.ns, 'res': getRes('us'), 'unit': 'us' }); 2655 | } 2656 | 2657 | // detect Node's nanosecond resolution timer available in Node >= 0.8 2658 | if (processObject && typeof (timer.ns = processObject.hrtime) == 'function') { 2659 | timers.push({ 'ns': timer.ns, 'res': getRes('ns'), 'unit': 'ns' }); 2660 | } 2661 | 2662 | // detect Wade Simmons' Node microtime module 2663 | if (microtimeObject && typeof (timer.ns = microtimeObject.now) == 'function') { 2664 | timers.push({ 'ns': timer.ns, 'res': getRes('us'), 'unit': 'us' }); 2665 | } 2666 | 2667 | // pick timer with highest resolution 2668 | timer = reduce(timers, function(timer, other) { 2669 | return other.res < timer.res ? other : timer; 2670 | }); 2671 | 2672 | // remove unused applet 2673 | if (timer.unit != 'ns' && applet) { 2674 | applet = destroyElement(applet); 2675 | } 2676 | // error if there are no working timers 2677 | if (timer.res == Infinity) { 2678 | throw new Error('Benchmark.js was unable to find a working timer.'); 2679 | } 2680 | // use API of chosen timer 2681 | if (timer.unit == 'ns') { 2682 | if (timer.ns.nanoTime) { 2683 | extend(template, { 2684 | 'begin': 's$=n$.nanoTime()', 2685 | 'end': 'r$=(n$.nanoTime()-s$)/1e9' 2686 | }); 2687 | } else { 2688 | extend(template, { 2689 | 'begin': 's$=n$()', 2690 | 'end': 'r$=n$(s$);r$=r$[0]+(r$[1]/1e9)' 2691 | }); 2692 | } 2693 | } 2694 | else if (timer.unit == 'us') { 2695 | if (timer.ns.stop) { 2696 | extend(template, { 2697 | 'begin': 's$=n$.start()', 2698 | 'end': 'r$=n$.microseconds()/1e6' 2699 | }); 2700 | } else if (perfName) { 2701 | extend(template, { 2702 | 'begin': 's$=n$.' + perfName + '()', 2703 | 'end': 'r$=(n$.' + perfName + '()-s$)/1e3' 2704 | }); 2705 | } else { 2706 | extend(template, { 2707 | 'begin': 's$=n$()', 2708 | 'end': 'r$=(n$()-s$)/1e6' 2709 | }); 2710 | } 2711 | } 2712 | 2713 | // define `timer` methods 2714 | timer.start = createFunction(preprocess('o$'), 2715 | preprocess('var n$=this.ns,#{begin};o$.elapsed=0;o$.timeStamp=s$')); 2716 | 2717 | timer.stop = createFunction(preprocess('o$'), 2718 | preprocess('var n$=this.ns,s$=o$.timeStamp,#{end};o$.elapsed=r$')); 2719 | 2720 | // resolve time span required to achieve a percent uncertainty of at most 1% 2721 | // http://spiff.rit.edu/classes/phys273/uncert/uncert.html 2722 | options.minTime || (options.minTime = max(timer.res / 2 / 0.01, 0.05)); 2723 | return clock.apply(null, arguments); 2724 | } 2725 | 2726 | /*--------------------------------------------------------------------------*/ 2727 | 2728 | /** 2729 | * Computes stats on benchmark results. 2730 | * 2731 | * @private 2732 | * @param {Object} bench The benchmark instance. 2733 | * @param {Object} options The options object. 2734 | */ 2735 | function compute(bench, options) { 2736 | options || (options = {}); 2737 | 2738 | var async = options.async, 2739 | elapsed = 0, 2740 | initCount = bench.initCount, 2741 | minSamples = bench.minSamples, 2742 | queue = [], 2743 | sample = bench.stats.sample; 2744 | 2745 | /** 2746 | * Adds a clone to the queue. 2747 | */ 2748 | function enqueue() { 2749 | queue.push(bench.clone({ 2750 | '_original': bench, 2751 | 'events': { 2752 | 'abort': [update], 2753 | 'cycle': [update], 2754 | 'error': [update], 2755 | 'start': [update] 2756 | } 2757 | })); 2758 | } 2759 | 2760 | /** 2761 | * Updates the clone/original benchmarks to keep their data in sync. 2762 | */ 2763 | function update(event) { 2764 | var clone = this, 2765 | type = event.type; 2766 | 2767 | if (bench.running) { 2768 | if (type == 'start') { 2769 | // Note: `clone.minTime` prop is inited in `clock()` 2770 | clone.count = bench.initCount; 2771 | } 2772 | else { 2773 | if (type == 'error') { 2774 | bench.error = clone.error; 2775 | } 2776 | if (type == 'abort') { 2777 | bench.abort(); 2778 | bench.emit('cycle'); 2779 | } else { 2780 | event.currentTarget = event.target = bench; 2781 | bench.emit(event); 2782 | } 2783 | } 2784 | } else if (bench.aborted) { 2785 | // clear abort listeners to avoid triggering bench's abort/cycle again 2786 | clone.events.abort.length = 0; 2787 | clone.abort(); 2788 | } 2789 | } 2790 | 2791 | /** 2792 | * Determines if more clones should be queued or if cycling should stop. 2793 | */ 2794 | function evaluate(event) { 2795 | var critical, 2796 | df, 2797 | mean, 2798 | moe, 2799 | rme, 2800 | sd, 2801 | sem, 2802 | variance, 2803 | clone = event.target, 2804 | done = bench.aborted, 2805 | now = +new Date, 2806 | size = sample.push(clone.times.period), 2807 | maxedOut = size >= minSamples && (elapsed += now - clone.times.timeStamp) / 1e3 > bench.maxTime, 2808 | times = bench.times, 2809 | varOf = function(sum, x) { return sum + pow(x - mean, 2); }; 2810 | 2811 | // exit early for aborted or unclockable tests 2812 | if (done || clone.hz == Infinity) { 2813 | maxedOut = !(size = sample.length = queue.length = 0); 2814 | } 2815 | 2816 | if (!done) { 2817 | // sample mean (estimate of the population mean) 2818 | mean = getMean(sample); 2819 | // sample variance (estimate of the population variance) 2820 | variance = reduce(sample, varOf, 0) / (size - 1) || 0; 2821 | // sample standard deviation (estimate of the population standard deviation) 2822 | sd = sqrt(variance); 2823 | // standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean) 2824 | sem = sd / sqrt(size); 2825 | // degrees of freedom 2826 | df = size - 1; 2827 | // critical value 2828 | critical = tTable[Math.round(df) || 1] || tTable.infinity; 2829 | // margin of error 2830 | moe = sem * critical; 2831 | // relative margin of error 2832 | rme = (moe / mean) * 100 || 0; 2833 | 2834 | extend(bench.stats, { 2835 | 'deviation': sd, 2836 | 'mean': mean, 2837 | 'moe': moe, 2838 | 'rme': rme, 2839 | 'sem': sem, 2840 | 'variance': variance 2841 | }); 2842 | 2843 | // Abort the cycle loop when the minimum sample size has been collected 2844 | // and the elapsed time exceeds the maximum time allowed per benchmark. 2845 | // We don't count cycle delays toward the max time because delays may be 2846 | // increased by browsers that clamp timeouts for inactive tabs. 2847 | // https://developer.mozilla.org/en/window.setTimeout#Inactive_tabs 2848 | if (maxedOut) { 2849 | // reset the `initCount` in case the benchmark is rerun 2850 | bench.initCount = initCount; 2851 | bench.running = false; 2852 | done = true; 2853 | times.elapsed = (now - times.timeStamp) / 1e3; 2854 | } 2855 | if (bench.hz != Infinity) { 2856 | bench.hz = 1 / mean; 2857 | times.cycle = mean * bench.count; 2858 | times.period = mean; 2859 | } 2860 | } 2861 | // if time permits, increase sample size to reduce the margin of error 2862 | if (queue.length < 2 && !maxedOut) { 2863 | enqueue(); 2864 | } 2865 | // abort the invoke cycle when done 2866 | event.aborted = done; 2867 | } 2868 | 2869 | // init queue and begin 2870 | enqueue(); 2871 | invoke(queue, { 2872 | 'name': 'run', 2873 | 'args': { 'async': async }, 2874 | 'queued': true, 2875 | 'onCycle': evaluate, 2876 | 'onComplete': function() { bench.emit('complete'); } 2877 | }); 2878 | } 2879 | 2880 | /*--------------------------------------------------------------------------*/ 2881 | 2882 | /** 2883 | * Cycles a benchmark until a run `count` can be established. 2884 | * 2885 | * @private 2886 | * @param {Object} clone The cloned benchmark instance. 2887 | * @param {Object} options The options object. 2888 | */ 2889 | function cycle(clone, options) { 2890 | options || (options = {}); 2891 | 2892 | var deferred; 2893 | if (clone instanceof Deferred) { 2894 | deferred = clone; 2895 | clone = clone.benchmark; 2896 | } 2897 | 2898 | var clocked, 2899 | cycles, 2900 | divisor, 2901 | event, 2902 | minTime, 2903 | period, 2904 | async = options.async, 2905 | bench = clone._original, 2906 | count = clone.count, 2907 | times = clone.times; 2908 | 2909 | // continue, if not aborted between cycles 2910 | if (clone.running) { 2911 | // `minTime` is set to `Benchmark.options.minTime` in `clock()` 2912 | cycles = ++clone.cycles; 2913 | clocked = deferred ? deferred.elapsed : clock(clone); 2914 | minTime = clone.minTime; 2915 | 2916 | if (cycles > bench.cycles) { 2917 | bench.cycles = cycles; 2918 | } 2919 | if (clone.error) { 2920 | event = Event('error'); 2921 | event.message = clone.error; 2922 | clone.emit(event); 2923 | if (!event.cancelled) { 2924 | clone.abort(); 2925 | } 2926 | } 2927 | } 2928 | 2929 | // continue, if not errored 2930 | if (clone.running) { 2931 | // time taken to complete last test cycle 2932 | bench.times.cycle = times.cycle = clocked; 2933 | // seconds per operation 2934 | period = bench.times.period = times.period = clocked / count; 2935 | // ops per second 2936 | bench.hz = clone.hz = 1 / period; 2937 | // avoid working our way up to this next time 2938 | bench.initCount = clone.initCount = count; 2939 | // do we need to do another cycle? 2940 | clone.running = clocked < minTime; 2941 | 2942 | if (clone.running) { 2943 | // tests may clock at `0` when `initCount` is a small number, 2944 | // to avoid that we set its count to something a bit higher 2945 | if (!clocked && (divisor = divisors[clone.cycles]) != null) { 2946 | count = floor(4e6 / divisor); 2947 | } 2948 | // calculate how many more iterations it will take to achive the `minTime` 2949 | if (count <= clone.count) { 2950 | count += Math.ceil((minTime - clocked) / period); 2951 | } 2952 | clone.running = count != Infinity; 2953 | } 2954 | } 2955 | // should we exit early? 2956 | event = Event('cycle'); 2957 | clone.emit(event); 2958 | if (event.aborted) { 2959 | clone.abort(); 2960 | } 2961 | // figure out what to do next 2962 | if (clone.running) { 2963 | // start a new cycle 2964 | clone.count = count; 2965 | if (deferred) { 2966 | clone.compiled.call(deferred, timer); 2967 | } else if (async) { 2968 | delay(clone, function() { cycle(clone, options); }); 2969 | } else { 2970 | cycle(clone); 2971 | } 2972 | } 2973 | else { 2974 | // fix TraceMonkey bug associated with clock fallbacks 2975 | // http://bugzil.la/509069 2976 | if (support.browser) { 2977 | runScript(uid + '=1;delete ' + uid); 2978 | } 2979 | // done 2980 | clone.emit('complete'); 2981 | } 2982 | } 2983 | 2984 | /*--------------------------------------------------------------------------*/ 2985 | 2986 | /** 2987 | * Runs the benchmark. 2988 | * 2989 | * @memberOf Benchmark 2990 | * @param {Object} [options={}] Options object. 2991 | * @returns {Object} The benchmark instance. 2992 | * @example 2993 | * 2994 | * // basic usage 2995 | * bench.run(); 2996 | * 2997 | * // or with options 2998 | * bench.run({ 'async': true }); 2999 | */ 3000 | function run(options) { 3001 | var me = this, 3002 | event = Event('start'); 3003 | 3004 | // set `running` to `false` so `reset()` won't call `abort()` 3005 | me.running = false; 3006 | me.reset(); 3007 | me.running = true; 3008 | 3009 | me.count = me.initCount; 3010 | me.times.timeStamp = +new Date; 3011 | me.emit(event); 3012 | 3013 | if (!event.cancelled) { 3014 | options = { 'async': ((options = options && options.async) == null ? me.async : options) && support.timeout }; 3015 | 3016 | // for clones created within `compute()` 3017 | if (me._original) { 3018 | if (me.defer) { 3019 | Deferred(me); 3020 | } else { 3021 | cycle(me, options); 3022 | } 3023 | } 3024 | // for original benchmarks 3025 | else { 3026 | compute(me, options); 3027 | } 3028 | } 3029 | return me; 3030 | } 3031 | 3032 | /*--------------------------------------------------------------------------*/ 3033 | 3034 | // Firefox 1 erroneously defines variable and argument names of functions on 3035 | // the function itself as non-configurable properties with `undefined` values. 3036 | // The bugginess continues as the `Benchmark` constructor has an argument 3037 | // named `options` and Firefox 1 will not assign a value to `Benchmark.options`, 3038 | // making it non-writable in the process, unless it is the first property 3039 | // assigned by for-in loop of `extend()`. 3040 | extend(Benchmark, { 3041 | 3042 | /** 3043 | * The default options copied by benchmark instances. 3044 | * 3045 | * @static 3046 | * @memberOf Benchmark 3047 | * @type Object 3048 | */ 3049 | 'options': { 3050 | 3051 | /** 3052 | * A flag to indicate that benchmark cycles will execute asynchronously 3053 | * by default. 3054 | * 3055 | * @memberOf Benchmark.options 3056 | * @type Boolean 3057 | */ 3058 | 'async': false, 3059 | 3060 | /** 3061 | * A flag to indicate that the benchmark clock is deferred. 3062 | * 3063 | * @memberOf Benchmark.options 3064 | * @type Boolean 3065 | */ 3066 | 'defer': false, 3067 | 3068 | /** 3069 | * The delay between test cycles (secs). 3070 | * @memberOf Benchmark.options 3071 | * @type Number 3072 | */ 3073 | 'delay': 0.005, 3074 | 3075 | /** 3076 | * Displayed by Benchmark#toString when a `name` is not available 3077 | * (auto-generated if absent). 3078 | * 3079 | * @memberOf Benchmark.options 3080 | * @type String 3081 | */ 3082 | 'id': undefined, 3083 | 3084 | /** 3085 | * The default number of times to execute a test on a benchmark's first cycle. 3086 | * 3087 | * @memberOf Benchmark.options 3088 | * @type Number 3089 | */ 3090 | 'initCount': 1, 3091 | 3092 | /** 3093 | * The maximum time a benchmark is allowed to run before finishing (secs). 3094 | * 3095 | * Note: Cycle delays aren't counted toward the maximum time. 3096 | * 3097 | * @memberOf Benchmark.options 3098 | * @type Number 3099 | */ 3100 | 'maxTime': 5, 3101 | 3102 | /** 3103 | * The minimum sample size required to perform statistical analysis. 3104 | * 3105 | * @memberOf Benchmark.options 3106 | * @type Number 3107 | */ 3108 | 'minSamples': 5, 3109 | 3110 | /** 3111 | * The time needed to reduce the percent uncertainty of measurement to 1% (secs). 3112 | * 3113 | * @memberOf Benchmark.options 3114 | * @type Number 3115 | */ 3116 | 'minTime': 0, 3117 | 3118 | /** 3119 | * The name of the benchmark. 3120 | * 3121 | * @memberOf Benchmark.options 3122 | * @type String 3123 | */ 3124 | 'name': undefined, 3125 | 3126 | /** 3127 | * An event listener called when the benchmark is aborted. 3128 | * 3129 | * @memberOf Benchmark.options 3130 | * @type Function 3131 | */ 3132 | 'onAbort': undefined, 3133 | 3134 | /** 3135 | * An event listener called when the benchmark completes running. 3136 | * 3137 | * @memberOf Benchmark.options 3138 | * @type Function 3139 | */ 3140 | 'onComplete': undefined, 3141 | 3142 | /** 3143 | * An event listener called after each run cycle. 3144 | * 3145 | * @memberOf Benchmark.options 3146 | * @type Function 3147 | */ 3148 | 'onCycle': undefined, 3149 | 3150 | /** 3151 | * An event listener called when a test errors. 3152 | * 3153 | * @memberOf Benchmark.options 3154 | * @type Function 3155 | */ 3156 | 'onError': undefined, 3157 | 3158 | /** 3159 | * An event listener called when the benchmark is reset. 3160 | * 3161 | * @memberOf Benchmark.options 3162 | * @type Function 3163 | */ 3164 | 'onReset': undefined, 3165 | 3166 | /** 3167 | * An event listener called when the benchmark starts running. 3168 | * 3169 | * @memberOf Benchmark.options 3170 | * @type Function 3171 | */ 3172 | 'onStart': undefined 3173 | }, 3174 | 3175 | /** 3176 | * Platform object with properties describing things like browser name, 3177 | * version, and operating system. 3178 | * 3179 | * @static 3180 | * @memberOf Benchmark 3181 | * @type Object 3182 | */ 3183 | 'platform': req('platform') || window.platform || { 3184 | 3185 | /** 3186 | * The platform description. 3187 | * 3188 | * @memberOf Benchmark.platform 3189 | * @type String 3190 | */ 3191 | 'description': window.navigator && navigator.userAgent || null, 3192 | 3193 | /** 3194 | * The name of the browser layout engine. 3195 | * 3196 | * @memberOf Benchmark.platform 3197 | * @type String|Null 3198 | */ 3199 | 'layout': null, 3200 | 3201 | /** 3202 | * The name of the product hosting the browser. 3203 | * 3204 | * @memberOf Benchmark.platform 3205 | * @type String|Null 3206 | */ 3207 | 'product': null, 3208 | 3209 | /** 3210 | * The name of the browser/environment. 3211 | * 3212 | * @memberOf Benchmark.platform 3213 | * @type String|Null 3214 | */ 3215 | 'name': null, 3216 | 3217 | /** 3218 | * The name of the product's manufacturer. 3219 | * 3220 | * @memberOf Benchmark.platform 3221 | * @type String|Null 3222 | */ 3223 | 'manufacturer': null, 3224 | 3225 | /** 3226 | * The name of the operating system. 3227 | * 3228 | * @memberOf Benchmark.platform 3229 | * @type String|Null 3230 | */ 3231 | 'os': null, 3232 | 3233 | /** 3234 | * The alpha/beta release indicator. 3235 | * 3236 | * @memberOf Benchmark.platform 3237 | * @type String|Null 3238 | */ 3239 | 'prerelease': null, 3240 | 3241 | /** 3242 | * The browser/environment version. 3243 | * 3244 | * @memberOf Benchmark.platform 3245 | * @type String|Null 3246 | */ 3247 | 'version': null, 3248 | 3249 | /** 3250 | * Return platform description when the platform object is coerced to a string. 3251 | * 3252 | * @memberOf Benchmark.platform 3253 | * @type Function 3254 | * @returns {String} The platform description. 3255 | */ 3256 | 'toString': function() { 3257 | return this.description || ''; 3258 | } 3259 | }, 3260 | 3261 | /** 3262 | * The semantic version number. 3263 | * 3264 | * @static 3265 | * @memberOf Benchmark 3266 | * @type String 3267 | */ 3268 | 'version': '1.0.0', 3269 | 3270 | // an object of environment/feature detection flags 3271 | 'support': support, 3272 | 3273 | // clone objects 3274 | 'deepClone': deepClone, 3275 | 3276 | // iteration utility 3277 | 'each': each, 3278 | 3279 | // augment objects 3280 | 'extend': extend, 3281 | 3282 | // generic Array#filter 3283 | 'filter': filter, 3284 | 3285 | // generic Array#forEach 3286 | 'forEach': forEach, 3287 | 3288 | // generic own property iteration utility 3289 | 'forOwn': forOwn, 3290 | 3291 | // converts a number to a comma-separated string 3292 | 'formatNumber': formatNumber, 3293 | 3294 | // generic Object#hasOwnProperty 3295 | // (trigger hasKey's lazy define before assigning it to Benchmark) 3296 | 'hasKey': (hasKey(Benchmark, ''), hasKey), 3297 | 3298 | // generic Array#indexOf 3299 | 'indexOf': indexOf, 3300 | 3301 | // template utility 3302 | 'interpolate': interpolate, 3303 | 3304 | // invokes a method on each item in an array 3305 | 'invoke': invoke, 3306 | 3307 | // generic Array#join for arrays and objects 3308 | 'join': join, 3309 | 3310 | // generic Array#map 3311 | 'map': map, 3312 | 3313 | // retrieves a property value from each item in an array 3314 | 'pluck': pluck, 3315 | 3316 | // generic Array#reduce 3317 | 'reduce': reduce 3318 | }); 3319 | 3320 | /*--------------------------------------------------------------------------*/ 3321 | 3322 | extend(Benchmark.prototype, { 3323 | 3324 | /** 3325 | * The number of times a test was executed. 3326 | * 3327 | * @memberOf Benchmark 3328 | * @type Number 3329 | */ 3330 | 'count': 0, 3331 | 3332 | /** 3333 | * The number of cycles performed while benchmarking. 3334 | * 3335 | * @memberOf Benchmark 3336 | * @type Number 3337 | */ 3338 | 'cycles': 0, 3339 | 3340 | /** 3341 | * The number of executions per second. 3342 | * 3343 | * @memberOf Benchmark 3344 | * @type Number 3345 | */ 3346 | 'hz': 0, 3347 | 3348 | /** 3349 | * The compiled test function. 3350 | * 3351 | * @memberOf Benchmark 3352 | * @type Function|String 3353 | */ 3354 | 'compiled': undefined, 3355 | 3356 | /** 3357 | * The error object if the test failed. 3358 | * 3359 | * @memberOf Benchmark 3360 | * @type Object 3361 | */ 3362 | 'error': undefined, 3363 | 3364 | /** 3365 | * The test to benchmark. 3366 | * 3367 | * @memberOf Benchmark 3368 | * @type Function|String 3369 | */ 3370 | 'fn': undefined, 3371 | 3372 | /** 3373 | * A flag to indicate if the benchmark is aborted. 3374 | * 3375 | * @memberOf Benchmark 3376 | * @type Boolean 3377 | */ 3378 | 'aborted': false, 3379 | 3380 | /** 3381 | * A flag to indicate if the benchmark is running. 3382 | * 3383 | * @memberOf Benchmark 3384 | * @type Boolean 3385 | */ 3386 | 'running': false, 3387 | 3388 | /** 3389 | * Compiled into the test and executed immediately **before** the test loop. 3390 | * 3391 | * @memberOf Benchmark 3392 | * @type Function|String 3393 | * @example 3394 | * 3395 | * // basic usage 3396 | * var bench = Benchmark({ 3397 | * 'setup': function() { 3398 | * var c = this.count, 3399 | * element = document.getElementById('container'); 3400 | * while (c--) { 3401 | * element.appendChild(document.createElement('div')); 3402 | * } 3403 | * }, 3404 | * 'fn': function() { 3405 | * element.removeChild(element.lastChild); 3406 | * } 3407 | * }); 3408 | * 3409 | * // compiles to something like: 3410 | * var c = this.count, 3411 | * element = document.getElementById('container'); 3412 | * while (c--) { 3413 | * element.appendChild(document.createElement('div')); 3414 | * } 3415 | * var start = new Date; 3416 | * while (count--) { 3417 | * element.removeChild(element.lastChild); 3418 | * } 3419 | * var end = new Date - start; 3420 | * 3421 | * // or using strings 3422 | * var bench = Benchmark({ 3423 | * 'setup': '\ 3424 | * var a = 0;\n\ 3425 | * (function() {\n\ 3426 | * (function() {\n\ 3427 | * (function() {', 3428 | * 'fn': 'a += 1;', 3429 | * 'teardown': '\ 3430 | * }())\n\ 3431 | * }())\n\ 3432 | * }())' 3433 | * }); 3434 | * 3435 | * // compiles to something like: 3436 | * var a = 0; 3437 | * (function() { 3438 | * (function() { 3439 | * (function() { 3440 | * var start = new Date; 3441 | * while (count--) { 3442 | * a += 1; 3443 | * } 3444 | * var end = new Date - start; 3445 | * }()) 3446 | * }()) 3447 | * }()) 3448 | */ 3449 | 'setup': noop, 3450 | 3451 | /** 3452 | * Compiled into the test and executed immediately **after** the test loop. 3453 | * 3454 | * @memberOf Benchmark 3455 | * @type Function|String 3456 | */ 3457 | 'teardown': noop, 3458 | 3459 | /** 3460 | * An object of stats including mean, margin or error, and standard deviation. 3461 | * 3462 | * @memberOf Benchmark 3463 | * @type Object 3464 | */ 3465 | 'stats': { 3466 | 3467 | /** 3468 | * The margin of error. 3469 | * 3470 | * @memberOf Benchmark#stats 3471 | * @type Number 3472 | */ 3473 | 'moe': 0, 3474 | 3475 | /** 3476 | * The relative margin of error (expressed as a percentage of the mean). 3477 | * 3478 | * @memberOf Benchmark#stats 3479 | * @type Number 3480 | */ 3481 | 'rme': 0, 3482 | 3483 | /** 3484 | * The standard error of the mean. 3485 | * 3486 | * @memberOf Benchmark#stats 3487 | * @type Number 3488 | */ 3489 | 'sem': 0, 3490 | 3491 | /** 3492 | * The sample standard deviation. 3493 | * 3494 | * @memberOf Benchmark#stats 3495 | * @type Number 3496 | */ 3497 | 'deviation': 0, 3498 | 3499 | /** 3500 | * The sample arithmetic mean. 3501 | * 3502 | * @memberOf Benchmark#stats 3503 | * @type Number 3504 | */ 3505 | 'mean': 0, 3506 | 3507 | /** 3508 | * The array of sampled periods. 3509 | * 3510 | * @memberOf Benchmark#stats 3511 | * @type Array 3512 | */ 3513 | 'sample': [], 3514 | 3515 | /** 3516 | * The sample variance. 3517 | * 3518 | * @memberOf Benchmark#stats 3519 | * @type Number 3520 | */ 3521 | 'variance': 0 3522 | }, 3523 | 3524 | /** 3525 | * An object of timing data including cycle, elapsed, period, start, and stop. 3526 | * 3527 | * @memberOf Benchmark 3528 | * @type Object 3529 | */ 3530 | 'times': { 3531 | 3532 | /** 3533 | * The time taken to complete the last cycle (secs). 3534 | * 3535 | * @memberOf Benchmark#times 3536 | * @type Number 3537 | */ 3538 | 'cycle': 0, 3539 | 3540 | /** 3541 | * The time taken to complete the benchmark (secs). 3542 | * 3543 | * @memberOf Benchmark#times 3544 | * @type Number 3545 | */ 3546 | 'elapsed': 0, 3547 | 3548 | /** 3549 | * The time taken to execute the test once (secs). 3550 | * 3551 | * @memberOf Benchmark#times 3552 | * @type Number 3553 | */ 3554 | 'period': 0, 3555 | 3556 | /** 3557 | * A timestamp of when the benchmark started (ms). 3558 | * 3559 | * @memberOf Benchmark#times 3560 | * @type Number 3561 | */ 3562 | 'timeStamp': 0 3563 | }, 3564 | 3565 | // aborts benchmark (does not record times) 3566 | 'abort': abort, 3567 | 3568 | // creates a new benchmark using the same test and options 3569 | 'clone': clone, 3570 | 3571 | // compares benchmark's hertz with another 3572 | 'compare': compare, 3573 | 3574 | // executes listeners 3575 | 'emit': emit, 3576 | 3577 | // get listeners 3578 | 'listeners': listeners, 3579 | 3580 | // unregister listeners 3581 | 'off': off, 3582 | 3583 | // register listeners 3584 | 'on': on, 3585 | 3586 | // reset benchmark properties 3587 | 'reset': reset, 3588 | 3589 | // runs the benchmark 3590 | 'run': run, 3591 | 3592 | // pretty print benchmark info 3593 | 'toString': toStringBench 3594 | }); 3595 | 3596 | /*--------------------------------------------------------------------------*/ 3597 | 3598 | extend(Deferred.prototype, { 3599 | 3600 | /** 3601 | * The deferred benchmark instance. 3602 | * 3603 | * @memberOf Benchmark.Deferred 3604 | * @type Object 3605 | */ 3606 | 'benchmark': null, 3607 | 3608 | /** 3609 | * The number of deferred cycles performed while benchmarking. 3610 | * 3611 | * @memberOf Benchmark.Deferred 3612 | * @type Number 3613 | */ 3614 | 'cycles': 0, 3615 | 3616 | /** 3617 | * The time taken to complete the deferred benchmark (secs). 3618 | * 3619 | * @memberOf Benchmark.Deferred 3620 | * @type Number 3621 | */ 3622 | 'elapsed': 0, 3623 | 3624 | /** 3625 | * A timestamp of when the deferred benchmark started (ms). 3626 | * 3627 | * @memberOf Benchmark.Deferred 3628 | * @type Number 3629 | */ 3630 | 'timeStamp': 0, 3631 | 3632 | // cycles/completes the deferred benchmark 3633 | 'resolve': resolve 3634 | }); 3635 | 3636 | /*--------------------------------------------------------------------------*/ 3637 | 3638 | extend(Event.prototype, { 3639 | 3640 | /** 3641 | * A flag to indicate if the emitters listener iteration is aborted. 3642 | * 3643 | * @memberOf Benchmark.Event 3644 | * @type Boolean 3645 | */ 3646 | 'aborted': false, 3647 | 3648 | /** 3649 | * A flag to indicate if the default action is cancelled. 3650 | * 3651 | * @memberOf Benchmark.Event 3652 | * @type Boolean 3653 | */ 3654 | 'cancelled': false, 3655 | 3656 | /** 3657 | * The object whose listeners are currently being processed. 3658 | * 3659 | * @memberOf Benchmark.Event 3660 | * @type Object 3661 | */ 3662 | 'currentTarget': undefined, 3663 | 3664 | /** 3665 | * The return value of the last executed listener. 3666 | * 3667 | * @memberOf Benchmark.Event 3668 | * @type Mixed 3669 | */ 3670 | 'result': undefined, 3671 | 3672 | /** 3673 | * The object to which the event was originally emitted. 3674 | * 3675 | * @memberOf Benchmark.Event 3676 | * @type Object 3677 | */ 3678 | 'target': undefined, 3679 | 3680 | /** 3681 | * A timestamp of when the event was created (ms). 3682 | * 3683 | * @memberOf Benchmark.Event 3684 | * @type Number 3685 | */ 3686 | 'timeStamp': 0, 3687 | 3688 | /** 3689 | * The event type. 3690 | * 3691 | * @memberOf Benchmark.Event 3692 | * @type String 3693 | */ 3694 | 'type': '' 3695 | }); 3696 | 3697 | /*--------------------------------------------------------------------------*/ 3698 | 3699 | /** 3700 | * The default options copied by suite instances. 3701 | * 3702 | * @static 3703 | * @memberOf Benchmark.Suite 3704 | * @type Object 3705 | */ 3706 | Suite.options = { 3707 | 3708 | /** 3709 | * The name of the suite. 3710 | * 3711 | * @memberOf Benchmark.Suite.options 3712 | * @type String 3713 | */ 3714 | 'name': undefined 3715 | }; 3716 | 3717 | /*--------------------------------------------------------------------------*/ 3718 | 3719 | extend(Suite.prototype, { 3720 | 3721 | /** 3722 | * The number of benchmarks in the suite. 3723 | * 3724 | * @memberOf Benchmark.Suite 3725 | * @type Number 3726 | */ 3727 | 'length': 0, 3728 | 3729 | /** 3730 | * A flag to indicate if the suite is aborted. 3731 | * 3732 | * @memberOf Benchmark.Suite 3733 | * @type Boolean 3734 | */ 3735 | 'aborted': false, 3736 | 3737 | /** 3738 | * A flag to indicate if the suite is running. 3739 | * 3740 | * @memberOf Benchmark.Suite 3741 | * @type Boolean 3742 | */ 3743 | 'running': false, 3744 | 3745 | /** 3746 | * An `Array#forEach` like method. 3747 | * Callbacks may terminate the loop by explicitly returning `false`. 3748 | * 3749 | * @memberOf Benchmark.Suite 3750 | * @param {Function} callback The function called per iteration. 3751 | * @returns {Object} The suite iterated over. 3752 | */ 3753 | 'forEach': methodize(forEach), 3754 | 3755 | /** 3756 | * An `Array#indexOf` like method. 3757 | * 3758 | * @memberOf Benchmark.Suite 3759 | * @param {Mixed} value The value to search for. 3760 | * @returns {Number} The index of the matched value or `-1`. 3761 | */ 3762 | 'indexOf': methodize(indexOf), 3763 | 3764 | /** 3765 | * Invokes a method on all benchmarks in the suite. 3766 | * 3767 | * @memberOf Benchmark.Suite 3768 | * @param {String|Object} name The name of the method to invoke OR options object. 3769 | * @param {Mixed} [arg1, arg2, ...] Arguments to invoke the method with. 3770 | * @returns {Array} A new array of values returned from each method invoked. 3771 | */ 3772 | 'invoke': methodize(invoke), 3773 | 3774 | /** 3775 | * Converts the suite of benchmarks to a string. 3776 | * 3777 | * @memberOf Benchmark.Suite 3778 | * @param {String} [separator=','] A string to separate each element of the array. 3779 | * @returns {String} The string. 3780 | */ 3781 | 'join': [].join, 3782 | 3783 | /** 3784 | * An `Array#map` like method. 3785 | * 3786 | * @memberOf Benchmark.Suite 3787 | * @param {Function} callback The function called per iteration. 3788 | * @returns {Array} A new array of values returned by the callback. 3789 | */ 3790 | 'map': methodize(map), 3791 | 3792 | /** 3793 | * Retrieves the value of a specified property from all benchmarks in the suite. 3794 | * 3795 | * @memberOf Benchmark.Suite 3796 | * @param {String} property The property to pluck. 3797 | * @returns {Array} A new array of property values. 3798 | */ 3799 | 'pluck': methodize(pluck), 3800 | 3801 | /** 3802 | * Removes the last benchmark from the suite and returns it. 3803 | * 3804 | * @memberOf Benchmark.Suite 3805 | * @returns {Mixed} The removed benchmark. 3806 | */ 3807 | 'pop': [].pop, 3808 | 3809 | /** 3810 | * Appends benchmarks to the suite. 3811 | * 3812 | * @memberOf Benchmark.Suite 3813 | * @returns {Number} The suite's new length. 3814 | */ 3815 | 'push': [].push, 3816 | 3817 | /** 3818 | * Sorts the benchmarks of the suite. 3819 | * 3820 | * @memberOf Benchmark.Suite 3821 | * @param {Function} [compareFn=null] A function that defines the sort order. 3822 | * @returns {Object} The sorted suite. 3823 | */ 3824 | 'sort': [].sort, 3825 | 3826 | /** 3827 | * An `Array#reduce` like method. 3828 | * 3829 | * @memberOf Benchmark.Suite 3830 | * @param {Function} callback The function called per iteration. 3831 | * @param {Mixed} accumulator Initial value of the accumulator. 3832 | * @returns {Mixed} The accumulator. 3833 | */ 3834 | 'reduce': methodize(reduce), 3835 | 3836 | // aborts all benchmarks in the suite 3837 | 'abort': abortSuite, 3838 | 3839 | // adds a benchmark to the suite 3840 | 'add': add, 3841 | 3842 | // creates a new suite with cloned benchmarks 3843 | 'clone': cloneSuite, 3844 | 3845 | // executes listeners of a specified type 3846 | 'emit': emit, 3847 | 3848 | // creates a new suite of filtered benchmarks 3849 | 'filter': filterSuite, 3850 | 3851 | // get listeners 3852 | 'listeners': listeners, 3853 | 3854 | // unregister listeners 3855 | 'off': off, 3856 | 3857 | // register listeners 3858 | 'on': on, 3859 | 3860 | // resets all benchmarks in the suite 3861 | 'reset': resetSuite, 3862 | 3863 | // runs all benchmarks in the suite 3864 | 'run': runSuite, 3865 | 3866 | // array methods 3867 | 'concat': concat, 3868 | 3869 | 'reverse': reverse, 3870 | 3871 | 'shift': shift, 3872 | 3873 | 'slice': slice, 3874 | 3875 | 'splice': splice, 3876 | 3877 | 'unshift': unshift 3878 | }); 3879 | 3880 | /*--------------------------------------------------------------------------*/ 3881 | 3882 | // expose Deferred, Event and Suite 3883 | extend(Benchmark, { 3884 | 'Deferred': Deferred, 3885 | 'Event': Event, 3886 | 'Suite': Suite 3887 | }); 3888 | 3889 | // expose Benchmark 3890 | // some AMD build optimizers, like r.js, check for specific condition patterns like the following: 3891 | if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) { 3892 | // define as an anonymous module so, through path mapping, it can be aliased 3893 | define(function() { 3894 | return Benchmark; 3895 | }); 3896 | } 3897 | // check for `exports` after `define` in case a build optimizer adds an `exports` object 3898 | else if (freeExports) { 3899 | // in Node.js or RingoJS v0.8.0+ 3900 | if (typeof module == 'object' && module && module.exports == freeExports) { 3901 | (module.exports = Benchmark).Benchmark = Benchmark; 3902 | } 3903 | // in Narwhal or RingoJS v0.7.0- 3904 | else { 3905 | freeExports.Benchmark = Benchmark; 3906 | } 3907 | } 3908 | // in a browser or Rhino 3909 | else { 3910 | // use square bracket notation so Closure Compiler won't munge `Benchmark` 3911 | // http://code.google.com/closure/compiler/docs/api-tutorial3.html#export 3912 | window['Benchmark'] = Benchmark; 3913 | } 3914 | 3915 | // trigger clock's lazy define early to avoid a security error 3916 | if (support.air) { 3917 | clock({ '_original': { 'fn': noop, 'count': 1, 'options': {} } }); 3918 | } 3919 | }(this)); 3920 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var helper = {}; 3 | 4 | // Used in IE debugging 5 | helper.ppObject = function(obj) { 6 | var result = '\n{\n'; 7 | 8 | for(var name in obj) { 9 | result += '\t' + name + ': ' + obj[name] + '\n' 10 | } 11 | 12 | console.log(result + '}'); 13 | }; 14 | 15 | helper.$ = function(selector, context) { 16 | var elem = $(selector, context).get(0); 17 | var that = {}; 18 | 19 | that.attr = function(name, value) { 20 | elem.setAttribute(name, value); 21 | }; 22 | that.append = function(html) { 23 | elem.appendChild(html); 24 | }; 25 | that.remove = function() { 26 | if(elem.parentNode) elem.parentNode.removeChild(elem); 27 | }; 28 | that.content = function(text) { 29 | elem.textContent = text; 30 | }; 31 | 32 | return that; 33 | }; 34 | 35 | window.helper = helper; 36 | }()); 37 | -------------------------------------------------------------------------------- /test/integration.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jQuery Observe Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 | 26 | 27 |
    28 |
    29 |
    30 | 31 |
    32 |
      33 |
    • Item 1
    • 34 |
    • Item 2
    • 35 |
    36 |
    37 | 40 | 41 | Home 42 |
    43 |
    44 | 45 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var $fixture; 3 | 4 | QUnit.testStart(function() { 5 | $fixture = $('#qunit-fixture'); 6 | }); 7 | QUnit.testDone(function() { 8 | $('#container').remove(); 9 | }); 10 | 11 | module('Observe this'); 12 | 13 | test('Attribute changed', function() { 14 | stop(); 15 | expect(4); 16 | 17 | var $ul = $fixture.find('ul'); 18 | 19 | $ul.observe('attributes', function(record) { 20 | equal(this, $ul[0]); 21 | equal(record.target, $ul[0]); 22 | equal(record.type, 'attributes'); 23 | equal(record.attributeName, 'data-attr'); 24 | 25 | start(); 26 | }); 27 | 28 | helper.$($ul).attr('data-attr', 'value'); 29 | }); 30 | test('Double match (all, restrictive) on attribute changed', function() { 31 | stop(2); 32 | expect(6); 33 | 34 | var $ul = $fixture.find('ul'); 35 | var fn = function(record) { 36 | equal(this, $ul[0]); 37 | equal(record.type, 'attributes'); 38 | equal(record.attributeName, 'data-attr'); 39 | 40 | start(); 41 | }; 42 | 43 | $ul 44 | .observe('attributes', fn) 45 | .observe({ attributes: true, attributeFilter: ['data-attr'] }, fn); 46 | 47 | helper.$($ul).attr('data-attr', 'value'); 48 | }); 49 | test('Double match (restrictive, all) on attribute changed', function() { 50 | stop(2); 51 | expect(6); 52 | 53 | var $ul = $fixture.find('ul'); 54 | var fn = function(record) { 55 | equal(this, $ul[0]); 56 | equal(record.type, 'attributes'); 57 | equal(record.attributeName, 'data-attr'); 58 | 59 | start(); 60 | }; 61 | 62 | $ul 63 | .observe({ attributes: true, attributeFilter: ['data-attr'] }, fn) 64 | .observe('attributes', fn); 65 | 66 | helper.$($ul).attr('data-attr', 'value'); 67 | }); 68 | test('Single match (all, restrictive) on attribute changed', function() { 69 | stop(); 70 | expect(3); 71 | 72 | var $ul = $fixture.find('ul'); 73 | var fn = function(record) { 74 | equal(this, $ul[0]); 75 | equal(record.type, 'attributes'); 76 | equal(record.attributeName, 'data-other'); 77 | 78 | start(); 79 | }; 80 | 81 | $ul 82 | .observe('attributes', fn) 83 | .observe({ attributes: true, attributeFilter: ['data-attr'] }, fn); 84 | 85 | helper.$($ul).attr('data-other', 'value'); 86 | }); 87 | test('Single match (restrictive, all) on attribute changed', function() { 88 | stop(); 89 | expect(3); 90 | 91 | var $ul = $fixture.find('ul'); 92 | var fn = function(record) { 93 | equal(this, $ul[0]); 94 | equal(record.type, 'attributes'); 95 | equal(record.attributeName, 'data-other'); 96 | 97 | start(); 98 | }; 99 | 100 | $ul 101 | .observe({ attributes: true, attributeFilter: ['data-attr'] }, fn) 102 | .observe('attributes', fn); 103 | 104 | helper.$($ul).attr('data-other', 'value'); 105 | }); 106 | test('Node added', function() { 107 | stop(); 108 | expect(4); 109 | 110 | var $ul = $fixture.find('ul'); 111 | var li = document.createElement('li'); 112 | 113 | $ul.observe('childlist', function(record) { 114 | equal(this, $ul[0]); 115 | equal(record.type, 'childList'); 116 | equal(record.addedNodes.length, 1); 117 | equal(record.addedNodes[0], li); 118 | 119 | start(); 120 | }); 121 | 122 | helper.$($ul).append(li); 123 | }); 124 | test('Node removed', function() { 125 | stop(); 126 | expect(4); 127 | 128 | var $ul = $fixture.find('ul'); 129 | var $li = $ul.find('li:first'); 130 | 131 | $ul.observe('childlist', function(record) { 132 | equal(this, record.target); 133 | equal(record.type, 'childList'); 134 | equal(record.removedNodes.length, 1); 135 | equal(record.removedNodes[0], $li[0]); 136 | 137 | start(); 138 | }); 139 | 140 | helper.$($li).remove(); 141 | }); 142 | test('Content swapped', function() { 143 | stop(); 144 | expect(6); 145 | 146 | var $header = $('#header'); 147 | 148 | $header.observe('childlist subtree', function(record) { 149 | equal(this, $header[0]); 150 | equal(record.type, 'childList'); 151 | equal(record.addedNodes.length, 1); 152 | equal(record.addedNodes[0].data, 'value'); 153 | equal(record.removedNodes.length, 1); 154 | ok(record.removedNodes[0].data, 'Header'); 155 | 156 | start(); 157 | }); 158 | 159 | helper.$($header).content('value'); 160 | }); 161 | test('Character data changed', function() { 162 | stop(); 163 | expect(4); 164 | 165 | var $header = $('#header'); 166 | var text = $header[0].childNodes[0]; 167 | 168 | $header.observe('characterdata subtree', function(record) { 169 | equal(this, $header[0]); 170 | equal(record.target, text); 171 | equal(record.type, 'characterData'); 172 | equal(record.target, text); 173 | 174 | start(); 175 | }); 176 | 177 | text.nodeValue = 'value'; 178 | }); 179 | test('Subtree option', function() { 180 | stop(); 181 | expect(4); 182 | 183 | var $ul = $fixture.find('ul'); 184 | var $span = $ul.find('span'); 185 | 186 | $ul.observe('childlist subtree', function(record) { 187 | equal(this, $ul[0]); 188 | equal(record.type, 'childList'); 189 | equal(record.removedNodes.length, 1); 190 | equal(record.removedNodes[0], $span[0]); 191 | 192 | start(); 193 | }); 194 | 195 | helper.$($span).remove(); 196 | }); 197 | test('Add text node', function() { 198 | stop(); 199 | expect(4); 200 | 201 | var $ul = $fixture.find('ul'); 202 | var text = document.createTextNode('item'); 203 | 204 | $ul.observe('childlist', function(record) { 205 | equal(this, $ul[0]); 206 | equal(record.type, 'childList'); 207 | equal(record.addedNodes.length, 1); 208 | equal(record.addedNodes[0], text); 209 | 210 | start(); 211 | }); 212 | 213 | helper.$($ul).append(text); 214 | }); 215 | test('Add text node and element node', function() { 216 | stop(); 217 | expect(8); 218 | 219 | var $ul = $fixture.find('ul'); 220 | var nodes = [document.createTextNode('item'), document.createElement('li')]; 221 | var i = 0; 222 | 223 | $ul.observe('childlist', function(record) { 224 | equal(this, $ul[0]); 225 | equal(record.type, 'childList'); 226 | equal(record.addedNodes.length, 1); 227 | equal(record.addedNodes[0], nodes[i]); 228 | 229 | i++; 230 | start(); 231 | }); 232 | 233 | helper.$($ul).append(nodes[0]); 234 | helper.$($ul).append(nodes[1]); 235 | }); 236 | 237 | module('Observe child'); 238 | 239 | test('Child attibute changed', function() { 240 | stop(); 241 | expect(3); 242 | 243 | var $ul = $fixture.find('ul'); 244 | var $li = $ul.find('li:first'); 245 | 246 | $ul.observe('attributes', 'li:first', function(record) { 247 | equal(this, $li[0]); 248 | equal(record.type, 'attributes'); 249 | equal(record.attributeName, 'data-attr'); 250 | 251 | start(); 252 | }); 253 | 254 | helper.$($li).attr('data-attr', 'value'); 255 | }); 256 | test('Child node added', function() { 257 | stop(); 258 | expect(4); 259 | 260 | var $ul = $fixture.find('ul'); 261 | var $li = $ul.find('li:first'); 262 | var span = document.createElement('span'); 263 | 264 | $ul.observe('childlist', 'li:first span', function(record) { 265 | equal(this, span); 266 | equal(record.type, 'childList'); 267 | equal(record.addedNodes.length, 1); 268 | equal(record.addedNodes[0], span); 269 | 270 | start(); 271 | }); 272 | 273 | helper.$($li).append(span); 274 | }); 275 | test('Child node removed', function() { 276 | stop(); 277 | expect(4); 278 | 279 | var $ul = $fixture.find('ul'); 280 | var $li = $ul.find('li:last'); 281 | var $span = $li.find('span'); 282 | 283 | $ul.observe('childlist', 'li:last span', function(record) { 284 | equal(this, $li[0]); 285 | equal(record.type, 'childList'); 286 | equal(record.removedNodes.length, 1); 287 | equal(record.removedNodes[0], $span[0]); 288 | 289 | start(); 290 | }); 291 | 292 | helper.$($span).remove(); 293 | }); 294 | test('Removed child node with no siblings', function() { 295 | stop(); 296 | expect(4); 297 | 298 | var $span = $('#menu'); 299 | var $a = $span.find('a'); 300 | 301 | $span.observe('childlist', 'a', function(record) { 302 | equal(this, $span[0]); 303 | equal(record.type, 'childList'); 304 | equal(record.removedNodes.length, 1); 305 | equal(record.removedNodes[0], $a[0]); 306 | 307 | start(); 308 | }); 309 | 310 | helper.$($a).remove(); 311 | }); 312 | test('Swapped content of child node', function() { 313 | stop(); 314 | expect(6); 315 | 316 | var $ul = $fixture.find('ul'); 317 | var $li = $ul.find('li:first'); 318 | 319 | $ul.observe('childlist', 'li', function(record) { 320 | equal(this, $li[0]); 321 | equal(record.type, 'childList'); 322 | equal(record.addedNodes.length, 1); 323 | equal(record.addedNodes[0].data, 'value'); 324 | equal(record.removedNodes.length, 1); 325 | equal(record.removedNodes[0].data, 'Item 1'); 326 | 327 | start(); 328 | }); 329 | 330 | helper.$($li).content('value'); 331 | }); 332 | test('Character data changed on child element', function() { 333 | stop(); 334 | expect(4); 335 | 336 | var $ul = $fixture.find('ul'); 337 | var $li = $ul.find('li:first'); 338 | var text = $li[0].childNodes[0]; 339 | 340 | $ul.observe('characterData', 'li', function(record) { 341 | equal(this, $li[0]); 342 | equal(record.target, text); 343 | equal(record.type, 'characterData'); 344 | equal(record.target, text); 345 | 346 | start(); 347 | }); 348 | 349 | text.nodeValue = 'value'; 350 | }); 351 | test('Add text node', function() { 352 | stop(); 353 | expect(4); 354 | 355 | var $ul = $fixture.find('ul'); 356 | var $li = $ul.find('li:first'); 357 | var text = document.createTextNode('item'); 358 | 359 | $ul.observe('childlist', 'li', function(record) { 360 | equal(this, $li[0]); 361 | equal(record.type, 'childList'); 362 | equal(record.addedNodes.length, 1); 363 | equal(record.addedNodes[0], text); 364 | 365 | start(); 366 | }); 367 | 368 | helper.$($li).append(text); 369 | }); 370 | test('Add text node and element node', function() { 371 | stop(); 372 | expect(4); 373 | 374 | var $ul = $fixture.find('ul'); 375 | var text = document.createTextNode('item'); 376 | var li = document.createElement('li'); 377 | 378 | $ul.observe('childlist', 'li', function(record) { 379 | equal(this, li); 380 | equal(record.type, 'childList'); 381 | equal(record.addedNodes.length, 1); 382 | equal(record.addedNodes[0], li); 383 | 384 | start(); 385 | }); 386 | 387 | helper.$($ul).append(text); // Should not be called when appending text 388 | helper.$($ul).append(li); 389 | }); 390 | test('Multiple element match on node added', function() { 391 | stop(2); 392 | expect(8); 393 | 394 | var $ul = $fixture.find('ul'); 395 | var $li = $ul.find('li:first'); 396 | var em = [document.createElement('em'), document.createElement('em')]; 397 | var i = 0; 398 | 399 | $ul.observe('childlist', 'li em', function(record) { 400 | equal(this, em[i]); 401 | equal(record.type, 'childList'); 402 | equal(record.addedNodes.length, 1); 403 | equal(record.addedNodes[0], em[i]); 404 | 405 | i++; 406 | start(); 407 | }); 408 | 409 | helper.$($li).append(em[0]); 410 | 411 | setTimeout(function() { 412 | helper.$($li).append(em[1]); 413 | }, 100); 414 | }); 415 | test('Multiple element match on multiple insert', function() { 416 | stop(2); 417 | expect(8); 418 | 419 | var $ul = $fixture.find('ul'); 420 | var $li = $ul.find('li:first'); 421 | var em = [document.createElement('em'), document.createElement('em')]; 422 | var i = 0; 423 | 424 | $ul.observe('childlist', 'li em', function(record) { 425 | equal(this, em[i]); 426 | equal(record.type, 'childList'); 427 | equal(record.addedNodes.length, 1); 428 | equal(record.addedNodes[0], em[i]); 429 | 430 | i++; 431 | start(); 432 | }); 433 | 434 | helper.$($li).append(em[0]); 435 | helper.$($li).append(em[1]); 436 | }); 437 | 438 | module('Multiple observers'); 439 | 440 | test('Child node added and attribute changed', function() { 441 | stop(2); 442 | expect(7); 443 | 444 | var $ul = $fixture.find('ul'); 445 | var $li = $ul.find('li:first'); 446 | var span = document.createElement('span'); 447 | 448 | $ul 449 | .observe('childlist', 'li:first span', function(record) { 450 | equal(this, span); 451 | equal(record.type, 'childList'); 452 | equal(record.addedNodes.length, 1); 453 | equal(record.addedNodes[0], span); 454 | 455 | start(); 456 | }) 457 | .observe({ attributes: true, attributeFilter: ['data-attr'] }, 'li:first', function(record) { 458 | equal(this, $li[0]); 459 | equal(record.type, 'attributes'); 460 | equal(record.attributeName, 'data-attr'); 461 | 462 | start(); 463 | }); 464 | 465 | helper.$($li).append(span); 466 | helper.$($li).attr('data-attr', 'value'); 467 | }); 468 | test('Child node added and removed', function() { 469 | stop(2); 470 | expect(8); 471 | 472 | var $ul = $fixture.find('ul'); 473 | var $li = $ul.find('li:first'); 474 | var span = document.createElement('span'); 475 | var adding = true; 476 | 477 | $ul.observe('childlist', 'li:first span', function(record) { 478 | equal(record.type, 'childList'); 479 | 480 | if(adding) { 481 | equal(this, span); 482 | equal(record.addedNodes.length, 1); 483 | equal(record.addedNodes[0], span); 484 | } else { 485 | equal(this, $li[0]); 486 | equal(record.removedNodes.length, 1); 487 | equal(record.removedNodes[0], span); 488 | } 489 | 490 | adding = false; 491 | start(); 492 | }); 493 | 494 | helper.$($li).append(span); 495 | 496 | setTimeout(function() { 497 | helper.$(span).remove(); 498 | }, 100); 499 | }); 500 | test('Nodes added and attribute changed', function() { 501 | stop(3); 502 | expect(10); 503 | 504 | var $ul = $fixture.find('ul'); 505 | var $li = $ul.find('li:last'); 506 | var span = document.createElement('span'); 507 | 508 | $ul 509 | .observe('attributes', function(record) { 510 | equal(this, $ul[0]); 511 | equal(record.type, 'attributes'); 512 | equal(record.attributeName, 'data-other'); 513 | 514 | start(); 515 | }) 516 | .observe('childlist', 'li:first span', function(record) { 517 | equal(this, span); 518 | equal(record.type, 'childList'); 519 | equal(record.addedNodes.length, 1); 520 | equal(record.addedNodes[0], span); 521 | 522 | start(); 523 | }) 524 | .observe({ attributes: true, attributeFilter: ['data-attr'] }, 'li:last', function(record) { 525 | equal(this, $li[0]); 526 | equal(record.type, 'attributes'); 527 | equal(record.attributeName, 'data-attr'); 528 | 529 | start(); 530 | }); 531 | 532 | helper.$($ul).attr('data-other', 'value'); 533 | helper.$('li:first', $ul).append(span); 534 | helper.$($li).attr('data-attr', 'value'); 535 | }); 536 | test('Multiple matches on add node', function() { 537 | stop(2); 538 | expect(8); 539 | 540 | var $ul = $fixture.find('ul'); 541 | var $li = $ul.find('li:first'); 542 | 543 | var span = document.createElement('span'); 544 | var a = document.createElement('a'); 545 | span.appendChild(a); 546 | 547 | $ul 548 | .observe('childlist', 'li:first span', function(record) { 549 | equal(this, span); 550 | equal(record.type, 'childList'); 551 | equal(record.addedNodes.length, 1); 552 | equal(record.addedNodes[0], span); 553 | 554 | start(); 555 | }) 556 | .observe('childlist', 'li:first span a', function(record) { 557 | equal(this, a); 558 | equal(record.type, 'childList'); 559 | equal(record.addedNodes.length, 1); 560 | equal(record.addedNodes[0], span); 561 | 562 | start(); 563 | }); 564 | 565 | helper.$($li).append(span); 566 | }); 567 | test('Multiple matches on remove node', function() { 568 | stop(2); 569 | expect(8); 570 | 571 | var $ul = $fixture.find('ul'); 572 | var $li = $ul.find('li:last'); 573 | 574 | $ul 575 | .observe('childlist', 'li', function(record) { 576 | equal(this, $ul[0]); 577 | equal(record.type, 'childList'); 578 | equal(record.removedNodes.length, 1); 579 | equal(record.removedNodes[0], $li[0]); 580 | 581 | start(); 582 | }) 583 | .observe('childlist', 'li span', function(record) { 584 | equal(this, $ul[0]); 585 | equal(record.type, 'childList'); 586 | equal(record.removedNodes.length, 1); 587 | equal(record.removedNodes[0], $li[0]); 588 | 589 | start(); 590 | }); 591 | 592 | helper.$($li).remove(); 593 | }); 594 | test('Match on deep insert', function() { 595 | stop(); 596 | expect(4); 597 | 598 | var $ul = $fixture.find('ul'); 599 | 600 | var div = document.createElement('div'); 601 | var em = document.createElement('em'); 602 | div.appendChild(em); 603 | 604 | $ul.observe('childlist', 'li span div em', function(record) { 605 | equal(this, em); 606 | equal(record.type, 'childList'); 607 | equal(record.addedNodes.length, 1); 608 | equal(record.addedNodes[0], div); 609 | 610 | start(); 611 | }); 612 | 613 | helper.$('li:last span', $ul).append(div); 614 | }); 615 | test('Match on deep removale', function() { 616 | stop(); 617 | expect(4); 618 | 619 | var $container = $('#container'); 620 | var $ul = $container.find('ul'); 621 | 622 | $container.observe('childlist', 'div ul li .last-li', function(record) { 623 | equal(this, $('#content')[0]); 624 | equal(record.type, 'childList'); 625 | equal(record.removedNodes.length, 1); 626 | equal(record.removedNodes[0], $ul[0]); 627 | 628 | start(); 629 | }); 630 | 631 | helper.$($ul).remove(); 632 | }); 633 | }()); 634 | -------------------------------------------------------------------------------- /test/performance.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Benchmark 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 123 | 124 | 130 | 131 | 479 | 480 | 481 |
    482 |

    jQuery Observe Benchmark

    483 |
    484 |
    485 | Run benchmark 486 | Clear data 487 | 488 | 489 | 490 | 491 |
    492 |
    493 |
    494 |
    Idle
    495 |
    Running...
    496 |
    497 |
    Benchmark completed
    498 |
    test(s) run, was fastest
    499 |
    500 |
    501 |
      502 | 503 |
      504 |
      505 | 506 |
      507 | 508 | --------------------------------------------------------------------------------