From 8dd6c4680bae73ef5eab6247b86cd5683308b906 Mon Sep 17 00:00:00 2001 From: yjbanov Date: Fri, 4 Sep 2015 14:44:24 -0700 Subject: [PATCH] feat(perf): change detection profiler Closes #4000 --- modules/angular2/core.dart | 1 + modules/angular2/src/core/facade/browser.ts | 1 + modules/angular2/src/tools/common_tools.ts | 64 +++++++++++++++++++++ modules/angular2/src/tools/tools.dart | 34 +++++++++++ modules/angular2/src/tools/tools.ts | 27 +++++++++ modules/angular2/test/tools/spies.dart | 27 +++++++++ modules/angular2/test/tools/spies.ts | 15 +++++ modules/angular2/test/tools/tools_spec.ts | 27 +++++++++ modules/angular2/tools.ts | 4 ++ pubspec.yaml | 2 +- tools/broccoli/trees/node_tree.ts | 1 + tools/build/file2modulename.js | 1 - 12 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 modules/angular2/src/tools/common_tools.ts create mode 100644 modules/angular2/src/tools/tools.dart create mode 100644 modules/angular2/src/tools/tools.ts create mode 100644 modules/angular2/test/tools/spies.dart create mode 100644 modules/angular2/test/tools/spies.ts create mode 100644 modules/angular2/test/tools/tools_spec.ts create mode 100644 modules/angular2/tools.ts diff --git a/modules/angular2/core.dart b/modules/angular2/core.dart index 5495aed20d..b34bd09c16 100644 --- a/modules/angular2/core.dart +++ b/modules/angular2/core.dart @@ -5,6 +5,7 @@ export 'package:angular2/src/core/util.dart'; export 'package:angular2/src/core/di.dart'; export 'package:angular2/src/core/pipes.dart'; export 'package:angular2/src/core/facade.dart'; +export 'package:angular2/src/core/application_ref.dart'; // Do not export application for dart. Must import from angular2/bootstrap //export 'package:angular2/src/core/application.dart'; export 'package:angular2/src/core/services.dart'; diff --git a/modules/angular2/src/core/facade/browser.ts b/modules/angular2/src/core/facade/browser.ts index 05e343676c..1a4f483a73 100644 --- a/modules/angular2/src/core/facade/browser.ts +++ b/modules/angular2/src/core/facade/browser.ts @@ -7,6 +7,7 @@ export {win as window}; export var document = window.document; export var location = window.location; export var gc = window['gc'] ? () => window['gc']() : () => null; +export var performance = window['performance'] ? window['performance'] : null; export const Event = Event; export const MouseEvent = MouseEvent; export const KeyboardEvent = KeyboardEvent; diff --git a/modules/angular2/src/tools/common_tools.ts b/modules/angular2/src/tools/common_tools.ts new file mode 100644 index 0000000000..74a998fbbd --- /dev/null +++ b/modules/angular2/src/tools/common_tools.ts @@ -0,0 +1,64 @@ +import {ApplicationRef, LifeCycle} from 'angular2/angular2'; +import {isPresent, NumberWrapper} from 'angular2/src/core/facade/lang'; +import {performance, window} from 'angular2/src/core/facade/browser'; + +/** + * Entry point for all Angular debug tools. This object corresponds to the `ng` + * global variable accessible in the dev console. + */ +export class AngularTools { + profiler: AngularProfiler; + + constructor(appRef: ApplicationRef) { this.profiler = new AngularProfiler(appRef); } +} + +/** + * Entry point for all Angular profiling-related debug tools. This object + * corresponds to the `ng.profiler` in the dev console. + */ +export class AngularProfiler { + lifeCycle: LifeCycle; + + constructor(appRef: ApplicationRef) { this.lifeCycle = appRef.injector.get(LifeCycle); } + + /** + * Exercises change detection in a loop and then prints the average amount of + * time in milliseconds how long a single round of change detection takes for + * the current state of the UI. It runs a minimum of 5 rounds for a minimum + * of 500 milliseconds. + * + * Optionally, a user may pass a `config` parameter containing a map of + * options. Supported options are: + * + * `record` (boolean) - causes the profiler to record a CPU profile while + * it exercises the change detector. Example: + * + * ``` + * ng.profiler.timeChangeDetection({record: true}) + * ``` + */ + timeChangeDetection(config: any) { + var record = isPresent(config) && config['record']; + var profileName = 'Change Detection'; + if (record) { + window.console.profile(profileName); + } + var start = window.performance.now(); + var numTicks = 0; + while (numTicks < 5 || (window.performance.now() - start) < 500) { + this.lifeCycle.tick(); + numTicks++; + } + var end = window.performance.now(); + if (record) { + // need to cast to because type checker thinks there's no argument + // while in fact there is: + // + // https://developer.mozilla.org/en-US/docs/Web/API/Console/profileEnd + (window.console.profileEnd)(profileName); + } + var msPerTick = (end - start) / numTicks; + window.console.log(`ran ${numTicks} change detection cycles`); + window.console.log(`${NumberWrapper.toFixed(msPerTick, 2)} ms per check`); + } +} diff --git a/modules/angular2/src/tools/tools.dart b/modules/angular2/src/tools/tools.dart new file mode 100644 index 0000000000..ea7c901225 --- /dev/null +++ b/modules/angular2/src/tools/tools.dart @@ -0,0 +1,34 @@ +library angular2.src.tools.tools; + +import 'dart:js'; +import 'package:angular2/angular2.dart' show ApplicationRef; +import 'common_tools.dart' show AngularTools; + +/** + * Enabled Angular 2 debug tools that are accessible via your browser's + * developer console. + * + * Usage: + * + * 1. Open developer console (e.g. in Chrome Ctrl + Shift + j) + * 1. Type `ng.` (usually the console will show auto-complete suggestion) + * 1. Try the change detection profiler `ng.profiler.timeChangeDetection()` + * then hit Enter. + */ +void enableDebugTools(ApplicationRef appRef) { + final tools = new AngularTools(appRef); + context['ng'] = new JsObject.jsify({ + 'profiler': { + 'timeChangeDetection': ([config]) { + tools.profiler.timeChangeDetection(config); + } + } + }); +} + +/** + * Disables Angular 2 tools. + */ +void disableDebugTools() { + context.deleteProperty('ng'); +} diff --git a/modules/angular2/src/tools/tools.ts b/modules/angular2/src/tools/tools.ts new file mode 100644 index 0000000000..cbd93b24ea --- /dev/null +++ b/modules/angular2/src/tools/tools.ts @@ -0,0 +1,27 @@ +import {global} from 'angular2/src/core/facade/lang'; +import {ApplicationRef} from 'angular2/angular2'; +import {AngularTools} from './common_tools'; + +var context = global; + +/** + * Enabled Angular 2 debug tools that are accessible via your browser's + * developer console. + * + * Usage: + * + * 1. Open developer console (e.g. in Chrome Ctrl + Shift + j) + * 1. Type `ng.` (usually the console will show auto-complete suggestion) + * 1. Try the change detection profiler `ng.profiler.timeChangeDetection()` + * then hit Enter. + */ +export function enableDebugTools(appRef: ApplicationRef): void { + context.ng = new AngularTools(appRef); +} + +/** + * Disables Angular 2 tools. + */ +export function disableDebugTools(): void { + context.ng = undefined; +} diff --git a/modules/angular2/test/tools/spies.dart b/modules/angular2/test/tools/spies.dart new file mode 100644 index 0000000000..6e5356f7da --- /dev/null +++ b/modules/angular2/test/tools/spies.dart @@ -0,0 +1,27 @@ +import 'package:angular2/test_lib.dart' show SpyObject; +import 'package:angular2/core.dart' + show ApplicationRef, LifeCycle, Injector, bind; +import 'dart:js'; + +@proxy +class SpyLifeCycle extends SpyObject implements LifeCycle { + noSuchMethod(m) => super.noSuchMethod(m); +} + +@proxy +class SpyApplicationRef extends SpyObject implements ApplicationRef { + Injector injector; + + SpyApplicationRef() { + this.injector = Injector.resolveAndCreate([ + bind(LifeCycle).toClass(SpyLifeCycle) + ]); + } + + noSuchMethod(m) => super.noSuchMethod(m); +} + +void callNgProfilerTimeChangeDetection([config]) { + context['ng']['profiler'].callMethod('timeChangeDetection', + config != null ? [config] : []); +} diff --git a/modules/angular2/test/tools/spies.ts b/modules/angular2/test/tools/spies.ts new file mode 100644 index 0000000000..6eef19c6b5 --- /dev/null +++ b/modules/angular2/test/tools/spies.ts @@ -0,0 +1,15 @@ +import {SpyObject} from 'angular2/test_lib'; +import {ApplicationRef, LifeCycle, Injector, bind} from 'angular2/angular2'; +import {global} from 'angular2/src/core/facade/lang'; + +export class SpyApplicationRef extends SpyObject { + injector; + constructor() { + super(); + this.injector = Injector.resolveAndCreate([bind(LifeCycle).toValue({tick: () => {}})]); + } +} + +export function callNgProfilerTimeChangeDetection(config?): void { + (global).ng.profiler.timeChangeDetection(config); +} diff --git a/modules/angular2/test/tools/tools_spec.ts b/modules/angular2/test/tools/tools_spec.ts new file mode 100644 index 0000000000..41f5655595 --- /dev/null +++ b/modules/angular2/test/tools/tools_spec.ts @@ -0,0 +1,27 @@ +import { + afterEach, + beforeEach, + ddescribe, + describe, + expect, + iit, + inject, + it, + xit +} from 'angular2/test_lib'; + +import {enableDebugTools, disableDebugTools} from 'angular2/tools'; +import {SpyApplicationRef, callNgProfilerTimeChangeDetection} from './spies'; + +export function main() { + describe('profiler', () => { + beforeEach(() => { enableDebugTools((new SpyApplicationRef())); }); + + afterEach(() => { disableDebugTools(); }); + + it('should time change detection', () => { callNgProfilerTimeChangeDetection(); }); + + it('should time change detection with recording', + () => { callNgProfilerTimeChangeDetection({'record': true}); }); + }); +} diff --git a/modules/angular2/tools.ts b/modules/angular2/tools.ts new file mode 100644 index 0000000000..29ad537859 --- /dev/null +++ b/modules/angular2/tools.ts @@ -0,0 +1,4 @@ +/* + * Debugging and profiling tools for Angular 2 + */ +export {enableDebugTools, disableDebugTools} from 'angular2/src/tools/tools'; diff --git a/pubspec.yaml b/pubspec.yaml index 64e712a394..11e0382c38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: angular environment: - sdk: '>=1.12.0 <2.0.0' + sdk: '>=1.12.0-dev.5.10 <2.0.0' dependencies: observe: '0.13.1' dev_dependencies: diff --git a/tools/broccoli/trees/node_tree.ts b/tools/broccoli/trees/node_tree.ts index 800164dc6d..b1262001e5 100644 --- a/tools/broccoli/trees/node_tree.ts +++ b/tools/broccoli/trees/node_tree.ts @@ -24,6 +24,7 @@ module.exports = function makeNodeTree(destinationPath) { 'angular2/test/test_lib/fake_async_spec.ts', 'angular2/test/core/render/xhr_impl_spec.ts', 'angular2/test/core/forms/**', + 'angular2/test/tools/tools_spec.ts', 'angular1_router/**' ] }); diff --git a/tools/build/file2modulename.js b/tools/build/file2modulename.js index ea04a73067..2906ade4f6 100644 --- a/tools/build/file2modulename.js +++ b/tools/build/file2modulename.js @@ -2,7 +2,6 @@ function file2moduleName(filePath) { return filePath.replace(/\\/g, '/') // module name should be relative to `modules` and `tools` folder .replace(/.*\/modules\//, '') - .replace(/.*\/tools\//, '') // and 'dist' folder .replace(/.*\/dist\/js\/dev\/es5\//, '') // module name should not include `lib`, `web` folders