From b7c0b77dbec45ca97c7bff9c4dfbb62690907abc Mon Sep 17 00:00:00 2001 From: satanTime Date: Sat, 26 Mar 2022 12:32:18 +0100 Subject: [PATCH] perf: optimizing detection of global overrides #1452 --- .circleci/config.yml | 3 + libs/ng-mocks/src/lib/common/core.helpers.ts | 21 +- .../lib/common/ng-mocks-global-overrides.ts | 25 +- .../mock-builder/mock-builder.performance.ts | 21 +- .../performance/add-entities-to-map.ts | 7 - .../performance/add-values-to-set.ts | 7 - tests-performance/ng-mocks.spec.ts | 314 ++++++++++++++++++ 7 files changed, 363 insertions(+), 35 deletions(-) delete mode 100644 libs/ng-mocks/src/lib/mock-builder/performance/add-entities-to-map.ts delete mode 100644 libs/ng-mocks/src/lib/mock-builder/performance/add-values-to-set.ts create mode 100644 tests-performance/ng-mocks.spec.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 0f9c79dc0e..467a9b89aa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -166,6 +166,9 @@ jobs: - run: name: MockRender command: KARMA_SUITE=tests-performance/mock-render.spec.ts npm run test + - run: + name: Large Modules + command: KARMA_SUITE=tests-performance/ng-mocks.spec.ts npm run test 'Angular 5 ES5': docker: - image: satantime/puppeteer-node:6.17.1 diff --git a/libs/ng-mocks/src/lib/common/core.helpers.ts b/libs/ng-mocks/src/lib/common/core.helpers.ts index 6000100a25..1ab55cb825 100644 --- a/libs/ng-mocks/src/lib/common/core.helpers.ts +++ b/libs/ng-mocks/src/lib/common/core.helpers.ts @@ -41,16 +41,29 @@ export const mapKeys = (set: Map): T[] => { return result; }; -export const mapValues = (set: { forEach(a1: (value: T) => void): void }): T[] => { +export const mapValues = (set: { forEach(a1: (value: T) => void): void }, destination?: Set): T[] => { const result: T[] = []; - set.forEach((value: T) => result.push(value)); + if (destination) { + set.forEach((value: T) => { + destination.add(value); + }); + } else { + set.forEach((value: T) => { + result.push(value); + }); + } return result; }; -export const mapEntries = (set: Map): Array<[K, T]> => { +export const mapEntries = (set: Map, destination?: Map): Array<[K, T]> => { const result: Array<[K, T]> = []; - set.forEach((value: T, key: K) => result.push([key, value])); + + if (destination) { + set.forEach((value: T, key: K) => destination.set(key, value)); + } else { + set.forEach((value: T, key: K) => result.push([key, value])); + } return result; }; diff --git a/libs/ng-mocks/src/lib/common/ng-mocks-global-overrides.ts b/libs/ng-mocks/src/lib/common/ng-mocks-global-overrides.ts index 9b6080d8b7..fc21182429 100644 --- a/libs/ng-mocks/src/lib/common/ng-mocks-global-overrides.ts +++ b/libs/ng-mocks/src/lib/common/ng-mocks-global-overrides.ts @@ -8,7 +8,7 @@ import mockHelperFasterInstall from '../mock-helper/mock-helper.faster-install'; import { MockProvider } from '../mock-provider/mock-provider'; import coreDefineProperty from './core.define-property'; -import { flatten, mapEntries } from './core.helpers'; +import { flatten, mapEntries, mapValues } from './core.helpers'; import coreReflectMeta from './core.reflect.meta'; import coreReflectModuleResolve from './core.reflect.module-resolve'; import coreReflectProvidedIn from './core.reflect.provided-in'; @@ -46,13 +46,13 @@ const applyOverrides = (overrides: Map, [MetadataOverride, Met // Thanks Ivy and its TestBed.override - it does not clean up leftovers. const applyNgMocksOverrides = (testBed: TestBedStatic & { ngMocksOverrides?: Map }): void => { - if (testBed.ngMocksOverrides) { + if (testBed.ngMocksOverrides?.size) { ngMocks.flushTestBed(); for (const [def, original] of mapEntries(testBed.ngMocksOverrides)) { applyOverride(def, original); } - testBed.ngMocksOverrides = undefined; } + testBed.ngMocksOverrides = undefined; }; const initTestBed = () => { @@ -86,11 +86,24 @@ const generateTouches = ( generateTouches(def, touches); def = def.ngModule; } + if (touches.has(def)) { + continue; + } touches.add(def); - const meta = coreReflectMeta(def); - if (meta) { - generateTouches(meta, touches); + if (typeof def !== 'function') { + continue; + } + + if (!def.hasOwnProperty('__ngMocksTouches')) { + const local = new Set(); + const meta = coreReflectMeta(def); + if (meta) { + generateTouches(meta, local); + } + coreDefineProperty(def, '__ngMocksTouches', local, false); } + + mapValues(def.__ngMocksTouches, touches); } } }; diff --git a/libs/ng-mocks/src/lib/mock-builder/mock-builder.performance.ts b/libs/ng-mocks/src/lib/mock-builder/mock-builder.performance.ts index 4abbc2e9a3..3805f200bb 100644 --- a/libs/ng-mocks/src/lib/mock-builder/mock-builder.performance.ts +++ b/libs/ng-mocks/src/lib/mock-builder/mock-builder.performance.ts @@ -1,10 +1,9 @@ import { TestBed, TestModuleMetadata } from '@angular/core/testing'; +import { mapEntries, mapValues } from '../common/core.helpers'; import ngMocksUniverse from '../common/ng-mocks-universe'; import { MockBuilderPromise } from './mock-builder.promise'; -import addEntitiesToMap from './performance/add-entities-to-map'; -import addValuesToSet from './performance/add-values-to-set'; import areEqualConfigParams from './performance/are-equal-config-params'; import areEqualMaps from './performance/are-equal-maps'; import areEqualProviders from './performance/are-equal-providers'; @@ -65,16 +64,16 @@ export class MockBuilderPerformance extends MockBuilderPromise { private cloneConfig() { const config = getEmptyConfig(); - addValuesToSet(this.beforeCC, config.beforeCC); - addValuesToSet(this.excludeDef, config.excludeDef); - addValuesToSet(this.keepDef, config.keepDef); - addValuesToSet(this.mockDef, config.mockDef); - addValuesToSet(this.replaceDef, config.replaceDef); + mapValues(this.beforeCC, config.beforeCC); + mapValues(this.excludeDef, config.excludeDef); + mapValues(this.keepDef, config.keepDef); + mapValues(this.mockDef, config.mockDef); + mapValues(this.replaceDef, config.replaceDef); - addEntitiesToMap(this.configDef, config.configDef); - addEntitiesToMap(this.defProviders, config.defProviders); - addEntitiesToMap(this.defValue, config.defValue); - addEntitiesToMap(this.providerDef, config.providerDef); + mapEntries(this.configDef, config.configDef); + mapEntries(this.defProviders, config.defProviders); + mapEntries(this.defValue, config.defValue); + mapEntries(this.providerDef, config.providerDef); return config; } diff --git a/libs/ng-mocks/src/lib/mock-builder/performance/add-entities-to-map.ts b/libs/ng-mocks/src/lib/mock-builder/performance/add-entities-to-map.ts deleted file mode 100644 index d1acefe2b8..0000000000 --- a/libs/ng-mocks/src/lib/mock-builder/performance/add-entities-to-map.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { mapEntries } from '../../common/core.helpers'; - -export default (source: Map, destination: Map): void => { - for (const [key, value] of mapEntries(source)) { - destination.set(key, value); - } -}; diff --git a/libs/ng-mocks/src/lib/mock-builder/performance/add-values-to-set.ts b/libs/ng-mocks/src/lib/mock-builder/performance/add-values-to-set.ts deleted file mode 100644 index 8c909b3a28..0000000000 --- a/libs/ng-mocks/src/lib/mock-builder/performance/add-values-to-set.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { mapValues } from '../../common/core.helpers'; - -export default (source: Set, destination: Set): void => { - for (const value of mapValues(source)) { - destination.add(value); - } -}; diff --git a/tests-performance/ng-mocks.spec.ts b/tests-performance/ng-mocks.spec.ts new file mode 100644 index 0000000000..e51289f221 --- /dev/null +++ b/tests-performance/ng-mocks.spec.ts @@ -0,0 +1,314 @@ +import { Component, NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatBadgeModule } from '@angular/material/badge'; +import { MatBottomSheetModule } from '@angular/material/bottom-sheet'; +import { MatButtonModule } from '@angular/material/button'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatGridListModule } from '@angular/material/grid-list'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatListModule } from '@angular/material/list'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatSliderModule } from '@angular/material/slider'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatSortModule } from '@angular/material/sort'; +import { MatStepperModule } from '@angular/material/stepper'; +import { MatTableModule } from '@angular/material/table'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatTreeModule } from '@angular/material/tree'; +import { MockComponent } from 'ng-mocks'; +import { AccordionModule } from 'primeng/accordion'; +import { AutoCompleteModule } from 'primeng/autocomplete'; +import { AvatarModule } from 'primeng/avatar'; +import { AvatarGroupModule } from 'primeng/avatargroup'; +import { BadgeModule } from 'primeng/badge'; +import { BlockUIModule } from 'primeng/blockui'; +import { BreadcrumbModule } from 'primeng/breadcrumb'; +import { ButtonModule } from 'primeng/button'; +import { CalendarModule } from 'primeng/calendar'; +import { CaptchaModule } from 'primeng/captcha'; +import { CardModule } from 'primeng/card'; +import { CarouselModule } from 'primeng/carousel'; +import { CascadeSelectModule } from 'primeng/cascadeselect'; +import { CheckboxModule } from 'primeng/checkbox'; +import { ChipModule } from 'primeng/chip'; +import { ChipsModule } from 'primeng/chips'; +import { CodeHighlighterModule } from 'primeng/codehighlighter'; +import { ColorPickerModule } from 'primeng/colorpicker'; +import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { ConfirmPopupModule } from 'primeng/confirmpopup'; +import { ContextMenuModule } from 'primeng/contextmenu'; +import { DataViewModule } from 'primeng/dataview'; +import { DeferModule } from 'primeng/defer'; +import { DialogModule } from 'primeng/dialog'; +import { DividerModule } from 'primeng/divider'; +import { DockModule } from 'primeng/dock'; +import { DragDropModule } from 'primeng/dragdrop'; +import { DropdownModule } from 'primeng/dropdown'; +import { DynamicDialogModule } from 'primeng/dynamicdialog'; +import { FieldsetModule } from 'primeng/fieldset'; +import { FileUploadModule } from 'primeng/fileupload'; +import { FocusTrapModule } from 'primeng/focustrap'; +import { GalleriaModule } from 'primeng/galleria'; +import { GMapModule } from 'primeng/gmap'; +import { ImageModule } from 'primeng/image'; +import { InplaceModule } from 'primeng/inplace'; +import { InputMaskModule } from 'primeng/inputmask'; +import { InputNumberModule } from 'primeng/inputnumber'; +import { InputSwitchModule } from 'primeng/inputswitch'; +import { InputTextModule } from 'primeng/inputtext'; +import { KeyFilterModule } from 'primeng/keyfilter'; +import { KnobModule } from 'primeng/knob'; +import { LightboxModule } from 'primeng/lightbox'; +import { ListboxModule } from 'primeng/listbox'; +import { MegaMenuModule } from 'primeng/megamenu'; +import { MenuModule } from 'primeng/menu'; +import { MenubarModule } from 'primeng/menubar'; +import { MessageModule } from 'primeng/message'; +import { MessagesModule } from 'primeng/messages'; +import { MultiSelectModule } from 'primeng/multiselect'; +import { OrderListModule } from 'primeng/orderlist'; +import { OrganizationChartModule } from 'primeng/organizationchart'; +import { OverlayPanelModule } from 'primeng/overlaypanel'; +import { PaginatorModule } from 'primeng/paginator'; +import { PanelModule } from 'primeng/panel'; +import { PanelMenuModule } from 'primeng/panelmenu'; +import { PasswordModule } from 'primeng/password'; +import { PickListModule } from 'primeng/picklist'; +import { ProgressBarModule } from 'primeng/progressbar'; +import { ProgressSpinnerModule } from 'primeng/progressspinner'; +import { RadioButtonModule } from 'primeng/radiobutton'; +import { RatingModule } from 'primeng/rating'; +import { RippleModule } from 'primeng/ripple'; +import { ScrollPanelModule } from 'primeng/scrollpanel'; +import { ScrollTopModule } from 'primeng/scrolltop'; +import { SelectButtonModule } from 'primeng/selectbutton'; +import { SidebarModule } from 'primeng/sidebar'; +import { SkeletonModule } from 'primeng/skeleton'; +import { SlideMenuModule } from 'primeng/slidemenu'; +import { SliderModule } from 'primeng/slider'; +import { SpeedDialModule } from 'primeng/speeddial'; +import { SpinnerModule } from 'primeng/spinner'; +import { SplitButtonModule } from 'primeng/splitbutton'; +import { SplitterModule } from 'primeng/splitter'; +import { StepsModule } from 'primeng/steps'; +import { StyleClassModule } from 'primeng/styleclass'; +import { TableModule } from 'primeng/table'; +import { TabMenuModule } from 'primeng/tabmenu'; +import { TabViewModule } from 'primeng/tabview'; +import { TagModule } from 'primeng/tag'; +import { TerminalModule } from 'primeng/terminal'; +import { TieredMenuModule } from 'primeng/tieredmenu'; +import { TimelineModule } from 'primeng/timeline'; +import { ToastModule } from 'primeng/toast'; +import { ToggleButtonModule } from 'primeng/togglebutton'; +import { ToolbarModule } from 'primeng/toolbar'; +import { TooltipModule } from 'primeng/tooltip'; +import { TreeModule } from 'primeng/tree'; +import { TreeSelectModule } from 'primeng/treeselect'; +import { TreeTableModule } from 'primeng/treetable'; +import { TriStateCheckboxModule } from 'primeng/tristatecheckbox'; +import { VirtualScrollerModule } from 'primeng/virtualscroller'; + +@NgModule({ + imports: [ + // material + MatAutocompleteModule, + MatBadgeModule, + MatBottomSheetModule, + MatButtonModule, + MatButtonToggleModule, + MatCardModule, + MatCheckboxModule, + MatChipsModule, + MatDatepickerModule, + MatDialogModule, + MatDividerModule, + MatExpansionModule, + MatFormFieldModule, + MatGridListModule, + MatIconModule, + MatInputModule, + MatListModule, + MatMenuModule, + MatPaginatorModule, + MatProgressBarModule, + MatProgressSpinnerModule, + MatRadioModule, + MatSelectModule, + MatSidenavModule, + MatSlideToggleModule, + MatSliderModule, + MatSnackBarModule, + MatSortModule, + MatStepperModule, + MatTableModule, + MatTabsModule, + MatToolbarModule, + MatTooltipModule, + MatTreeModule, + + // primeng + AccordionModule, + AutoCompleteModule, + AvatarModule, + AvatarGroupModule, + BadgeModule, + BlockUIModule, + BreadcrumbModule, + ButtonModule, + CalendarModule, + CaptchaModule, + CardModule, + CarouselModule, + CascadeSelectModule, + CheckboxModule, + ChipModule, + ChipsModule, + CodeHighlighterModule, + ColorPickerModule, + ConfirmDialogModule, + ConfirmPopupModule, + ContextMenuModule, + DataViewModule, + DeferModule, + DialogModule, + DividerModule, + DockModule, + DragDropModule, + DropdownModule, + DynamicDialogModule, + FieldsetModule, + FileUploadModule, + FocusTrapModule, + GalleriaModule, + GMapModule, + ImageModule, + InplaceModule, + InputMaskModule, + InputNumberModule, + InputSwitchModule, + InputTextModule, + InputTextModule, + KeyFilterModule, + KnobModule, + LightboxModule, + ListboxModule, + MegaMenuModule, + MenuModule, + MenubarModule, + MessageModule, + MessagesModule, + MultiSelectModule, + OrderListModule, + OrganizationChartModule, + OverlayPanelModule, + PaginatorModule, + PanelModule, + PanelMenuModule, + PasswordModule, + PickListModule, + ProgressBarModule, + ProgressSpinnerModule, + RadioButtonModule, + RatingModule, + RippleModule, + ScrollPanelModule, + ScrollTopModule, + SelectButtonModule, + SidebarModule, + SkeletonModule, + SlideMenuModule, + SliderModule, + SpeedDialModule, + SpinnerModule, + SplitButtonModule, + SplitterModule, + StepsModule, + StyleClassModule, + TableModule, + TabMenuModule, + TabViewModule, + TagModule, + TerminalModule, + TieredMenuModule, + TimelineModule, + ToastModule, + ToggleButtonModule, + ToolbarModule, + TooltipModule, + TreeModule, + TreeSelectModule, + TreeTableModule, + TriStateCheckboxModule, + VirtualScrollerModule, + ], +}) +class MockModule {} + +@Component({ + selector: 'app', + template: '', +}) +class AppComponent {} + +@Component({ + selector: 'dependency', + template: 'dependency', +}) +class DependencyComponent {} + +@NgModule({ + declarations: [AppComponent, DependencyComponent], +}) +class TargetModule {} + +describe('performance:ng-mocks', () => { + const time = performance.now(); + beforeEach(async () => { + const imports = []; + for (let i = 0; i < 1000; i += 1) { + imports.push(MockModule); + } + + return TestBed.configureTestingModule({ + declarations: [ + AppComponent, + MockComponent(DependencyComponent), + ], + imports, + }).compileComponents(); + }); + + for (let i = 0; i < 1000; i += 1) { + it('renders the component', () => { + const fixture = TestBed.createComponent(AppComponent); + + expect(fixture.debugElement.nativeElement.innerHTML).toEqual( + '', + ); + }); + } + + it('took acceptable time', () => { + expect(performance.now() - time).toBeLessThan(60000); + }); +});