From dd66aa290a606ef7cf312c7fe322ee927f041e92 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Mon, 5 Oct 2020 18:06:58 -0700 Subject: [PATCH] test(core): add micro benchmarks for i18n scenarios (#39142) This commit adds micro benchmarks to run micro benchmarks for i18n-related logic in the following scenarios: - i18n static attributes - i18n attributes with interpolations - i18n blocks of static text - i18n blocks of text + interpolations - simple ICUs - nested ICUs First 4 scenarios also have baseline scenarios (non-i18n) so that we can compare i18n perf with non-i18n logic. PR Close #39142 --- packages/core/src/render3/instructions/all.ts | 1 + .../core/src/render3/instructions/i18n.ts | 2 +- packages/core/test/render3/perf/BUILD.bazel | 13 + packages/core/test/render3/perf/i18n/index.ts | 345 ++++++++++++++++++ .../core/test/render3/perf/noop_renderer.ts | 4 +- .../test/render3/perf/profile_in_browser.html | 39 +- 6 files changed, 386 insertions(+), 18 deletions(-) create mode 100644 packages/core/test/render3/perf/i18n/index.ts diff --git a/packages/core/src/render3/instructions/all.ts b/packages/core/src/render3/instructions/all.ts index 98fec4d2ea..182c1541e0 100644 --- a/packages/core/src/render3/instructions/all.ts +++ b/packages/core/src/render3/instructions/all.ts @@ -48,3 +48,4 @@ export * from './class_map_interpolation'; export * from './style_map_interpolation'; export * from './style_prop_interpolation'; export * from './host_property'; +export * from './i18n'; diff --git a/packages/core/src/render3/instructions/i18n.ts b/packages/core/src/render3/instructions/i18n.ts index afaa3209c4..1ae2c7e5b5 100644 --- a/packages/core/src/render3/instructions/i18n.ts +++ b/packages/core/src/render3/instructions/i18n.ts @@ -17,7 +17,7 @@ import {HEADER_OFFSET} from '../interfaces/view'; import {getLView, getTView, nextBindingIndex} from '../state'; import {getConstant} from '../util/view_utils'; -import {setDelayProjection} from './all'; +import {setDelayProjection} from './projection'; /** * Marks a block of text as translatable. diff --git a/packages/core/test/render3/perf/BUILD.bazel b/packages/core/test/render3/perf/BUILD.bazel index da80ae8b9b..5e2b0401ab 100644 --- a/packages/core/test/render3/perf/BUILD.bazel +++ b/packages/core/test/render3/perf/BUILD.bazel @@ -230,6 +230,19 @@ ng_benchmark( bundle = ":host_binding", ) +ng_rollup_bundle( + name = "i18n_lib", + entry_point = ":i18n/index.ts", + deps = [ + ":perf_lib", + ], +) + +ng_benchmark( + name = "i18n", + bundle = ":i18n", +) + ng_rollup_bundle( name = "view_destroy_hook_lib", entry_point = ":view_destroy_hook/index.ts", diff --git a/packages/core/test/render3/perf/i18n/index.ts b/packages/core/test/render3/perf/i18n/index.ts new file mode 100644 index 0000000000..beb6ac1c10 --- /dev/null +++ b/packages/core/test/render3/perf/i18n/index.ts @@ -0,0 +1,345 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ɵɵadvance, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nExp, ɵɵpropertyInterpolate1, ɵɵtext, ɵɵtextInterpolate1} from '../../../../src/render3/instructions/all'; +import {ComponentTemplate, RenderFlags} from '../../../../src/render3/interfaces/definition'; +import {AttributeMarker, TAttributes} from '../../../../src/render3/interfaces/node'; +import {Benchmark, createBenchmark} from '../micro_bench'; +import {MicroBenchmarkRenderNode} from '../noop_renderer'; +import {setupTestHarness} from '../setup'; + +type ComponentDef = { + consts: (string|TAttributes)[], + vars: number, + decls: number, + template: ComponentTemplate, + beforeCD?: Function, + DOMParserMockFn?: Function, +}; + +const enum NodeTypes { + ELEMENT_NODE = 1, + TEXT_NODE = 2, + COMMENT_NODE = 8 +} + +function createElement(nodeType: number, tagName?: string): any { + const element = new MicroBenchmarkRenderNode(); + element.nodeType = nodeType; + element.tagName = tagName; + return element; +} + +// Mock function that is invoked when the string should be parsed. +function defaultDOMParserMockFn(content: string) { + const element = createElement(NodeTypes.TEXT_NODE); + element.textContent = content; + return element; +} + +function createDOMParserMock(mockFn: Function) { + return { + parseFromString: (content: string) => { + const body = createElement(NodeTypes.ELEMENT_NODE, 'body'); + content = content.replace(/<\/remove>/, ''); + body.firstChild = mockFn(content); + return {body}; + }, + }; +} + +function setupDOMParserMock(mockFn?: Function): Function { + const glob = global as any; + if (!glob.window) { + glob.window = {}; + } + const origDOMParser = glob.window.DOMParser; + glob.window.DOMParser = function() { + return createDOMParserMock(mockFn || defaultDOMParserMockFn); + }; + + // Return a function that would restore DOMParser to its original state. + return () => glob.window.DOMParser = origDOMParser; +} + + +const PROFILE_CREATE = true; +const PROFILE_UPDATE = true; +const NUM_OF_VIEWS_PER_RUN = 1000; +const DEFAULT_CONTEXT: any = { + title: 'Test title', + interpolation: 'Test interpolation', + count: 0 +}; + +let context = DEFAULT_CONTEXT; +const benchmarks: Benchmark[] = []; + +function benchmark(name: string, def: ComponentDef, baselineDef?: ComponentDef) { + // Reset context in case it was changed in `beforeCD` function during the previous benchmark. + context = DEFAULT_CONTEXT; + + const teardownDOMParserMock = setupDOMParserMock(def.DOMParserMockFn); + + const ivyHarness = setupTestHarness( + def.template, def.decls, def.vars, NUM_OF_VIEWS_PER_RUN, context, + def.consts as TAttributes[]); + + let baseHarness; + if (baselineDef) { + baseHarness = setupTestHarness( + baselineDef.template, baselineDef.decls, baselineDef.vars, NUM_OF_VIEWS_PER_RUN, context, + baselineDef.consts as TAttributes[]); + } + + if (PROFILE_CREATE) { + const benchmark = createBenchmark('i18n [create]: ' + name); + benchmarks.push(benchmark); + const ivyProfile = benchmark('(i18n)'); + console.profile(benchmark.name + ':' + ivyProfile.name); + while (ivyProfile()) { + ivyHarness.createEmbeddedLView(); + } + console.profileEnd(); + + if (baseHarness) { + const baseProfile = benchmark('(baseline)'); + console.profile(benchmark.name + ':' + baseProfile.name); + while (baseProfile()) { + baseHarness.createEmbeddedLView(); + } + console.profileEnd(); + } + } + + if (PROFILE_UPDATE) { + const benchmark = createBenchmark('i18n [update]: : ' + name); + benchmarks.push(benchmark); + const ivyProfile = benchmark('(i18n)'); + console.profile(benchmark.name + ':' + ivyProfile.name); + while (ivyProfile()) { + if (def.beforeCD) { + def.beforeCD(context); + } + ivyHarness.detectChanges(); + } + console.profileEnd(); + + if (baseHarness) { + const baseProfile = benchmark('(baseline)'); + console.profile(benchmark.name + ':' + baseProfile.name); + while (baseProfile()) { + if (baselineDef && baselineDef.beforeCD) { + baselineDef.beforeCD(context); + } + baseHarness.detectChanges(); + } + console.profileEnd(); + } + } + + teardownDOMParserMock(); +} + +benchmark( + `Static attributes`, + + //
+ { + decls: 2, + vars: 0, + consts: [[AttributeMarker.I18n, 'title'], ['title', 'Test Title']], + template: function(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵelementStart(0, 'div', 0); + ɵɵi18nAttributes(1, 1); + ɵɵelementEnd(); + } + } + }, + + //
+ { + decls: 2, + vars: 0, + consts: [['title', 'Test Title']], + template: function(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵelement(0, 'div', 0); + } + } + }); + +benchmark( + `Attributes with interpolations`, + + //
+ { + decls: 2, + vars: 1, + consts: [[AttributeMarker.I18n, 'title'], ['title', 'Test �0�']], + template: function(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵelementStart(0, 'div', 0); + ɵɵi18nAttributes(1, 1); + ɵɵelementEnd(); + } + if (rf & 2) { + ɵɵi18nExp(ctx.title); + ɵɵi18nApply(1); + } + } + }, + + //
+ { + decls: 2, + vars: 1, + consts: [[AttributeMarker.Bindings, 'title']], + template: function(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵelement(0, 'div', 0); + } + if (rf & 2) { + ɵɵpropertyInterpolate1('title', 'Test ', ctx.title, ''); + } + } + }); + +benchmark( + `Block of static text`, + + //
Some text content
+ { + decls: 2, + vars: 0, + consts: ['Some text content'], + template: function(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵelementStart(0, 'div'); + ɵɵi18n(1, 0); + ɵɵelementEnd(); + } + } + }, + + //
Some text content
+ { + decls: 2, + vars: 0, + consts: [], + template: function(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵelementStart(0, 'div'); + ɵɵtext(1, 'Some text content'); + ɵɵelementEnd(); + } + } + }); + +benchmark( + `Block of text with interpolation`, + + //
Some text content with {{ interpolation }}
+ { + decls: 2, + vars: 1, + consts: ['Some text content with �0�'], + template: function(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵelementStart(0, 'div'); + ɵɵi18n(1, 0); + ɵɵelementEnd(); + } + if (rf & 2) { + ɵɵadvance(1); + ɵɵi18nExp(ctx.interpolation); + ɵɵi18nApply(1); + } + } + }, + + //
Some text content with {{ interpolation }}
+ { + decls: 2, + vars: 1, + consts: [], + template: function(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵelementStart(0, 'div'); + ɵɵtext(1); + ɵɵelementEnd(); + } + if (rf & 2) { + ɵɵadvance(1); + ɵɵtextInterpolate1('Some text content with ', ctx.interpolation, ''); + } + } + }); + +benchmark( + `Simple ICU`, + + // {count, plural, =1 {one} =2 {two} other {other}} + { + decls: 1, + vars: 1, + consts: ['{�0�, plural, =1 {one} =2 {two} other {other}}'], + template: function(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵi18n(0, 0); + } + if (rf & 2) { + ɵɵi18nExp(ctx.count); + ɵɵi18nApply(0); + } + }, + beforeCD: function(ctx: any) { + // Switch values between [0, 1, 2] to trigger different ICU cases. + ctx.count = (ctx.count + 1) % 3; + }, + }); + +benchmark( + `Nested ICUs`, + + // {count, plural, + // =1 {one} + // =2 {two} + // other { {count, plural, =0 {zero} other {other}} }} + { + decls: 1, + vars: 2, + consts: ['{�0�, plural, =1 {one} =2 {two} other { {�0�, plural, =0 {zero} other {other}} }}'], + template: function(rf: RenderFlags, ctx: any) { + if (rf & 1) { + ɵɵi18n(0, 0); + } + if (rf & 2) { + ɵɵi18nExp(ctx.count)(ctx.count); + ɵɵi18nApply(0); + } + }, + beforeCD: function(ctx: any) { + // Switch values between [0, 1, 2] to trigger different ICU cases. + ctx.count = (ctx.count + 1) % 3; + }, + DOMParserMockFn: (content: string) => { + content = content.trim(); + // Nested ICUs are represented as comment nodes. If we come across one - create an element + // with correct node type, otherwise - call default mock fn. + if (content.startsWith('