refactor(compiler): speed up proto view merging

- Don't create intermediate merge results
- Only merge embedded ProtoViews that contain `<ng-content>` tags

Closes #3150
Closes #3177
This commit is contained in:
Tobias Bosch
2015-07-20 09:59:44 -07:00
parent de18da2a0d
commit 078475a082
20 changed files with 359 additions and 466 deletions

View File

@ -24,6 +24,7 @@ import {ComponentUrlMapper} from './component_url_mapper';
import {ProtoViewFactory} from './proto_view_factory';
import {UrlResolver} from 'angular2/src/services/url_resolver';
import {AppRootUrl} from 'angular2/src/services/app_root_url';
import {ElementBinder} from './element_binder';
import * as renderApi from 'angular2/src/render/api';
@ -90,7 +91,7 @@ export class Compiler {
private _appUrl: string;
private _render: renderApi.RenderCompiler;
private _protoViewFactory: ProtoViewFactory;
private _unmergedCyclicEmbeddedProtoViews: RecursiveEmbeddedProtoView[] = [];
private _protoViewsToBeMerged: AppProtoView[] = [];
/**
* @private
@ -146,8 +147,37 @@ export class Compiler {
return this._compileNestedProtoViews(hostRenderPv, protoView, componentType);
});
}
return hostPvPromise.then(
hostAppProtoView => this._mergeCyclicEmbeddedProtoViews().then(_ => hostAppProtoView.ref));
return hostPvPromise.then(hostAppProtoView =>
this._mergeUnmergedProtoViews().then(_ => hostAppProtoView.ref));
}
private _mergeUnmergedProtoViews(): Promise<any> {
var protoViewsToBeMerged = this._protoViewsToBeMerged;
this._protoViewsToBeMerged = [];
return PromiseWrapper.all(protoViewsToBeMerged.map((appProtoView) => {
return this._render.mergeProtoViewsRecursively(
this._collectMergeRenderProtoViews(appProtoView))
.then((mergeResult: renderApi.RenderProtoViewMergeMapping) => {
appProtoView.mergeMapping = new AppProtoViewMergeMapping(mergeResult);
});
}));
}
private _collectMergeRenderProtoViews(
appProtoView: AppProtoView): List<renderApi.RenderProtoViewRef | List<any>> {
var result = [appProtoView.render];
for (var i = 0; i < appProtoView.elementBinders.length; i++) {
var binder = appProtoView.elementBinders[i];
if (isPresent(binder.nestedProtoView)) {
if (binder.hasStaticComponent() ||
(binder.hasEmbeddedProtoView() && binder.nestedProtoView.isEmbeddedFragment)) {
result.push(this._collectMergeRenderProtoViews(binder.nestedProtoView));
} else {
result.push(null);
}
}
}
return result;
}
private _compile(componentBinding: DirectiveBinding): Promise<AppProtoView>| AppProtoView {
@ -207,7 +237,7 @@ export class Compiler {
appProtoView: AppProtoView,
componentType: Type): Promise<AppProtoView> {
var nestedPVPromises = [];
this._loopComponentElementBinders(appProtoView, (parentPv, elementBinder) => {
this._loopComponentElementBinders(appProtoView, (parentPv, elementBinder: ElementBinder) => {
var nestedComponent = elementBinder.componentDirective;
var elementBinderDone =
(nestedPv: AppProtoView) => { elementBinder.nestedProtoView = nestedPv; };
@ -220,31 +250,40 @@ export class Compiler {
});
return PromiseWrapper.all(nestedPVPromises)
.then((_) => {
var appProtoViewsToMergeInto = [];
var mergeRenderProtoViews = this._collectMergeRenderProtoViewsRecurse(
renderProtoView, appProtoView, appProtoViewsToMergeInto);
if (isBlank(mergeRenderProtoViews)) {
throw new BaseException(`Unconditional component cycle in ${stringify(componentType)}`);
}
return this._mergeProtoViews(appProtoViewsToMergeInto, mergeRenderProtoViews);
this._collectMergableProtoViews(appProtoView, componentType);
return appProtoView;
});
}
private _mergeProtoViews(
appProtoViewsToMergeInto: AppProtoView[],
mergeRenderProtoViews:
List<renderApi.RenderProtoViewRef | List<any>>): Promise<AppProtoView> {
return this._render.mergeProtoViewsRecursively(mergeRenderProtoViews)
.then((mergeResults: List<renderApi.RenderProtoViewMergeMapping>) => {
// Note: We don't need to check for nulls here as we filtered them out before!
// (in RenderCompiler.mergeProtoViewsRecursively and
// _collectMergeRenderProtoViewsRecurse).
for (var i = 0; i < mergeResults.length; i++) {
appProtoViewsToMergeInto[i].mergeMapping =
new AppProtoViewMergeMapping(mergeResults[i]);
}
return appProtoViewsToMergeInto[0];
});
private _collectMergableProtoViews(appProtoView: AppProtoView, componentType: Type) {
var isRecursive = false;
for (var i = 0; i < appProtoView.elementBinders.length; i++) {
var binder = appProtoView.elementBinders[i];
if (binder.hasStaticComponent()) {
if (isBlank(binder.nestedProtoView.isRecursive)) {
// cycle via a component. We are in the tail recursion,
// so all components should have their isRecursive flag set already.
isRecursive = true;
break;
}
} else if (binder.hasEmbeddedProtoView()) {
this._collectMergableProtoViews(binder.nestedProtoView, componentType);
}
}
if (isRecursive) {
if (appProtoView.isEmbeddedFragment) {
throw new BaseException(
`<ng-content> is used within the recursive path of ${stringify(componentType)}`);
}
if (appProtoView.type === renderApi.ViewType.COMPONENT) {
throw new BaseException(`Unconditional component cycle in ${stringify(componentType)}`);
}
}
if (appProtoView.type === renderApi.ViewType.EMBEDDED ||
appProtoView.type === renderApi.ViewType.HOST) {
this._protoViewsToBeMerged.push(appProtoView);
}
appProtoView.isRecursive = isRecursive;
}
private _loopComponentElementBinders(appProtoView: AppProtoView, callback: Function) {
@ -257,48 +296,6 @@ export class Compiler {
});
}
private _collectMergeRenderProtoViewsRecurse(
renderProtoView: renderApi.ProtoViewDto, appProtoView: AppProtoView,
targetAppProtoViews: AppProtoView[]): List<renderApi.RenderProtoViewRef | List<any>> {
targetAppProtoViews.push(appProtoView);
var result = [renderProtoView.render];
for (var i = 0; i < appProtoView.elementBinders.length; i++) {
var binder = appProtoView.elementBinders[i];
if (binder.hasStaticComponent()) {
if (isBlank(binder.nestedProtoView.mergeMapping)) {
// cycle via an embedded ProtoView. store the AppProtoView and ProtoViewDto for later.
this._unmergedCyclicEmbeddedProtoViews.push(
new RecursiveEmbeddedProtoView(appProtoView, renderProtoView));
return null;
}
result.push(binder.nestedProtoView.mergeMapping.renderProtoViewRef);
} else if (binder.hasEmbeddedProtoView()) {
result.push(this._collectMergeRenderProtoViewsRecurse(
renderProtoView.elementBinders[i].nestedProtoView, binder.nestedProtoView,
targetAppProtoViews));
}
}
return result;
}
private _mergeCyclicEmbeddedProtoViews() {
var pvs = this._unmergedCyclicEmbeddedProtoViews;
this._unmergedCyclicEmbeddedProtoViews = [];
var promises = pvs.map(entry => {
var appProtoView = entry.appProtoView;
var mergeRenderProtoViews = [entry.renderProtoView.render];
appProtoView.elementBinders.forEach((binder) => {
if (binder.hasStaticComponent()) {
mergeRenderProtoViews.push(binder.nestedProtoView.mergeMapping.renderProtoViewRef);
} else if (binder.hasEmbeddedProtoView()) {
mergeRenderProtoViews.push(null);
}
});
return this._mergeProtoViews([appProtoView], mergeRenderProtoViews);
});
return PromiseWrapper.all(promises);
}
private _buildRenderTemplate(component, view, directives): renderApi.ViewDefinition {
var componentUrl =
this._urlResolver.resolve(this._appUrl, this._componentUrlMapper.getUrl(component));
@ -356,7 +353,3 @@ export class Compiler {
}
}
}
class RecursiveEmbeddedProtoView {
constructor(public appProtoView: AppProtoView, public renderProtoView: renderApi.ProtoViewDto) {}
}

View File

@ -254,9 +254,12 @@ function _createAppProtoView(
renderProtoView: renderApi.ProtoViewDto, protoChangeDetector: ProtoChangeDetector,
variableBindings: Map<string, string>, allDirectives: List<DirectiveBinding>): AppProtoView {
var elementBinders = renderProtoView.elementBinders;
var protoView = new AppProtoView(renderProtoView.type, protoChangeDetector, variableBindings,
createVariableLocations(elementBinders),
renderProtoView.textBindings.length);
// Embedded ProtoViews that contain `<ng-content>` will be merged into their parents and use
// a RenderFragmentRef. I.e. renderProtoView.transitiveNgContentCount > 0.
var protoView = new AppProtoView(
renderProtoView.type, renderProtoView.transitiveNgContentCount > 0, renderProtoView.render,
protoChangeDetector, variableBindings, createVariableLocations(elementBinders),
renderProtoView.textBindings.length);
_createElementBinders(protoView, elementBinders, allDirectives);
_bindDirectiveEvents(protoView, elementBinders);

View File

@ -32,6 +32,7 @@ export class AppProtoViewMergeMapping {
renderTextIndices: number[];
nestedViewIndicesByElementIndex: number[];
hostElementIndicesByViewIndex: number[];
nestedViewCountByViewIndex: number[];
constructor(renderProtoViewMergeMapping: renderApi.RenderProtoViewMergeMapping) {
this.renderProtoViewRef = renderProtoViewMergeMapping.mergedProtoViewRef;
this.renderFragmentCount = renderProtoViewMergeMapping.fragmentCount;
@ -42,11 +43,8 @@ export class AppProtoViewMergeMapping {
this.hostElementIndicesByViewIndex = renderProtoViewMergeMapping.hostElementIndicesByViewIndex;
this.nestedViewIndicesByElementIndex =
inverseIndexMapping(this.hostElementIndicesByViewIndex, this.renderElementIndices.length);
this.nestedViewCountByViewIndex = renderProtoViewMergeMapping.nestedViewCountByViewIndex;
}
get viewCount() { return this.hostElementIndicesByViewIndex.length; }
get elementCount() { return this.renderElementIndices.length; }
}
function inverseIndexMapping(input: number[], resultLength: number): number[] {
@ -215,7 +213,7 @@ export class AppView implements ChangeDispatcher, RenderEventDispatcher {
dispatchRenderEvent(renderElementIndex: number, eventName: string,
locals: Map<string, any>): boolean {
var elementRef =
this.elementRefs[this.proto.mergeMapping.renderInverseElementIndices[renderElementIndex]];
this.elementRefs[this.mainMergeMapping.renderInverseElementIndices[renderElementIndex]];
var view = internalView(elementRef.parentView);
return view.dispatchEvent(elementRef.boundElementIndex, eventName, locals);
}
@ -258,7 +256,11 @@ export class AppProtoView {
mergeMapping: AppProtoViewMergeMapping;
ref: ProtoViewRef;
constructor(public type: renderApi.ViewType, public protoChangeDetector: ProtoChangeDetector,
isRecursive: boolean = null;
constructor(public type: renderApi.ViewType, public isEmbeddedFragment: boolean,
public render: renderApi.RenderProtoViewRef,
public protoChangeDetector: ProtoChangeDetector,
public variableBindings: Map<string, string>,
public variableLocations: Map<string, number>, public textBindingCount: number) {
this.ref = new ProtoViewRef(this);

View File

@ -339,12 +339,19 @@ export class AppViewManager {
this._utils.dehydrateView(view);
}
var viewContainers = view.viewContainers;
for (var i = view.elementOffset, ii = view.elementOffset + view.proto.mergeMapping.elementCount;
i < ii; i++) {
var vc = viewContainers[i];
if (isPresent(vc)) {
for (var j = vc.views.length - 1; j >= 0; j--) {
this._destroyViewInContainer(view, i, j);
var startViewOffset = view.viewOffset;
var endViewOffset =
view.viewOffset + view.mainMergeMapping.nestedViewCountByViewIndex[view.viewOffset];
var elementOffset = view.elementOffset;
for (var viewIdx = startViewOffset; viewIdx <= endViewOffset; viewIdx++) {
var currView = view.views[viewIdx];
for (var binderIdx = 0; binderIdx < currView.proto.elementBinders.length;
binderIdx++, elementOffset++) {
var vc = viewContainers[elementOffset];
if (isPresent(vc)) {
for (var j = vc.views.length - 1; j >= 0; j--) {
this._destroyViewInContainer(currView, elementOffset, j);
}
}
}
}

View File

@ -26,8 +26,8 @@ export class AppViewManagerUtils {
var renderFragments = renderViewWithFragments.fragmentRefs;
var renderView = renderViewWithFragments.viewRef;
var elementCount = mergedParentViewProto.mergeMapping.elementCount;
var viewCount = mergedParentViewProto.mergeMapping.viewCount;
var elementCount = mergedParentViewProto.mergeMapping.renderElementIndices.length;
var viewCount = mergedParentViewProto.mergeMapping.nestedViewCountByViewIndex[0] + 1;
var elementRefs: ElementRef[] = ListWrapper.createFixedSize(elementCount);
var viewContainers = ListWrapper.createFixedSize(elementCount);
var preBuiltObjects: eli.PreBuiltObjects[] = ListWrapper.createFixedSize(elementCount);
@ -175,13 +175,13 @@ export class AppViewManagerUtils {
_hydrateView(initView: viewModule.AppView, imperativelyCreatedInjector: Injector,
hostElementInjector: eli.ElementInjector, context: Object, parentLocals: Locals) {
var viewIdx = initView.viewOffset;
var endViewOffset = viewIdx + initView.proto.mergeMapping.viewCount;
while (viewIdx < endViewOffset) {
var endViewOffset = viewIdx + initView.mainMergeMapping.nestedViewCountByViewIndex[viewIdx];
while (viewIdx <= endViewOffset) {
var currView = initView.views[viewIdx];
var currProtoView = currView.proto;
if (currView !== initView && currView.proto.type === ViewType.EMBEDDED) {
// Don't hydrate components of embedded fragment views.
viewIdx += currProtoView.mergeMapping.viewCount;
viewIdx += initView.mainMergeMapping.nestedViewCountByViewIndex[viewIdx] + 1;
} else {
if (currView !== initView) {
// hydrate a nested component view
@ -263,9 +263,9 @@ export class AppViewManagerUtils {
}
dehydrateView(initView: viewModule.AppView) {
for (var viewIdx = initView.viewOffset,
endViewOffset = viewIdx + initView.proto.mergeMapping.viewCount;
viewIdx < endViewOffset; viewIdx++) {
var endViewOffset = initView.viewOffset +
initView.mainMergeMapping.nestedViewCountByViewIndex[initView.viewOffset];
for (var viewIdx = initView.viewOffset; viewIdx <= endViewOffset; viewIdx++) {
var currView = initView.views[viewIdx];
if (currView.hydrated()) {
if (isPresent(currView.locals)) {