Skip to content

Commit

Permalink
perf(mock-render): caching generated component help-me-mom#731
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Jun 20, 2021
1 parent 89eba7e commit 1a68ea7
Show file tree
Hide file tree
Showing 13 changed files with 673 additions and 11 deletions.
22 changes: 22 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,25 @@ jobs:
key: a-min-<< pipeline.parameters.lockindex >>-{{ arch }}-{{ checksum "e2e/a-min/package-lock.json" }}
paths:
- ./e2e/a-min/node_modules
'Performance':
docker:
- image: satantime/puppeteer-node:14.17.0-buster
steps:
- checkout
- restore_cache:
key: root-<< pipeline.parameters.lockindex >>-{{ arch }}-{{ checksum "package-lock.json" }}
- run:
name: Default
command: KARMA_SUITE=tests-performance/test.spec.ts npm run test
- run:
name: TestBed
command: KARMA_SUITE=tests-performance/test-bed.spec.ts npm run test
- run:
name: MockBuilder
command: KARMA_SUITE=tests-performance/mock-builder.spec.ts npm run test
- run:
name: MockRender
command: KARMA_SUITE=tests-performance/mock-render.spec.ts npm run test
'Angular 5 ES5':
docker:
- image: satantime/puppeteer-node:14.17.0-buster
Expand Down Expand Up @@ -755,6 +774,9 @@ workflows:
- 'Install':
requires:
- Core
- 'Performance':
requires:
- Core
- 'Angular 5 ES5':
requires:
- Install
Expand Down
43 changes: 37 additions & 6 deletions karma.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ process.on('infrastructure_error', error => {

process.env.CHROME_BIN = require('puppeteer').executablePath();

const suite: any[] = [];
if (!process.env.KARMA_SUITE) {
suite.push({
pattern: './libs/ng-mocks/src/lib/**/*.ts',
watched: true,
});
suite.push({
pattern: './examples/**/*.ts',
watched: true,
});
suite.push({
pattern: './tests/**/*.ts',
watched: true,
});
} else if (process.env.KARMA_SUITE === 'perf') {
suite.push({
pattern: './tests-performance/**/*.ts',
watched: true,
});
} else {
suite.push({
pattern: process.env.KARMA_SUITE,
watched: true,
});
}

export default (config: KarmaTypescriptConfig) => {
config.set({
autoWatch: false,
Expand Down Expand Up @@ -43,12 +69,17 @@ export default (config: KarmaTypescriptConfig) => {
},
},
files: [
'empty.ts',
'karma-test-shim.ts',
'libs/ng-mocks/src/index.ts',
{ pattern: 'libs/ng-mocks/src/lib/**/*.ts' },
{ pattern: 'examples/**/*.ts' },
{ pattern: 'tests/**/*.ts' },
'./empty.ts',
'./karma-test-shim.ts',
{
pattern: './libs/ng-mocks/src/index.ts',
watched: true,
},
{
pattern: './libs/ng\\-mocks/src/lib/**/!(*.spec|*.fixtures).ts',
watched: true,
},
...suite,
],
frameworks: ['jasmine', 'karma-typescript'],
junitReporter: {
Expand Down
1 change: 1 addition & 0 deletions libs/ng-mocks/src/lib/common/core.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ApplicationModule } from '@angular/core';

export default {
flags: ['cacheModule', 'cacheComponent', 'cacheDirective', 'cacheProvider', 'correctModuleExports'],
mockRenderCacheSize: 5,
neverMockModule: [ApplicationModule, CommonModule],
neverMockProvidedFunction: [
'DomRendererFactory2',
Expand Down
10 changes: 9 additions & 1 deletion libs/ng-mocks/src/lib/mock-helper/mock-helper.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,19 @@ export default {
autoSpy: mockHelperAutoSpy,
change: mockHelperChange,
click: mockHelperClick,
config: (config: { onTestBedFlushNeed?: 'throw' | 'warn' | 'i-know-but-disable' }) => {
config: (config: {
mockRenderCacheSize?: number | null;
onTestBedFlushNeed?: 'throw' | 'warn' | 'i-know-but-disable';
}) => {
const flags = ngMocksUniverse.global.get('flags');
if (config.onTestBedFlushNeed !== undefined) {
flags.onTestBedFlushNeed = config.onTestBedFlushNeed;
}
if (config.mockRenderCacheSize === null) {
ngMocksUniverse.global.delete('mockRenderCacheSize');
} else if (config.mockRenderCacheSize !== undefined) {
ngMocksUniverse.global.set('mockRenderCacheSize', config.mockRenderCacheSize);
}
},
crawl: mockHelperCrawl,
defaultMock: mockHelperDefaultMock,
Expand Down
1 change: 1 addition & 0 deletions libs/ng-mocks/src/lib/mock-helper/mock-helper.reset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ngMocksUniverse from '../common/ng-mocks-universe';

export default (): void => {
ngMocksUniverse.builtDeclarations = new Map();
ngMocksUniverse.builtProviders = new Map();
ngMocksUniverse.cacheDeclarations = new Map();
ngMocksUniverse.cacheProviders = new Map();
ngMocksUniverse.config = new Map();
Expand Down
5 changes: 4 additions & 1 deletion libs/ng-mocks/src/lib/mock-helper/mock-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export const ngMocks: {
*/
click(elSelector: HTMLElement | DebugNodeSelector, payload?: Partial<MouseEvent>): void;

config(config: { onTestBedFlushNeed?: 'throw' | 'warn' | 'i-know-but-disable' }): void;
config(config: {
mockRenderCacheSize?: number | null;
onTestBedFlushNeed?: 'throw' | 'warn' | 'i-know-but-disable';
}): void;

/**
* @see https://ng-mocks.sudo.eu/api/ngMocks/crawl
Expand Down
47 changes: 46 additions & 1 deletion libs/ng-mocks/src/lib/mock-render/func.create-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Component, Directive } from '@angular/core';

import coreConfig from '../common/core.config';
import coreDefineProperty from '../common/core.define-property';
import { Type } from '../common/core.types';
import ngMocksUniverse from '../common/ng-mocks-universe';
import helperDefinePropertyDescriptor from '../mock-service/helper.define-property-descriptor';

import funcGenerateTemplate from './func.generate-template';
Expand Down Expand Up @@ -43,18 +45,61 @@ const generateWrapper = ({ bindings, options, inputs }: any) => {
return MockRenderComponent;
};

const getCache = () => {
const caches: Array<Type<any> & Record<'cacheKey', any[]>> = ngMocksUniverse.config.get('MockRenderCaches') ?? [];
if (caches.length === 0) {
ngMocksUniverse.config.set('MockRenderCaches', caches);
}

return caches;
};

const checkCache = (caches: Array<Type<any> & Record<'cacheKey', any[]>>, cacheKey: any[]): undefined | Type<any> => {
for (const cache of caches) {
if (cache.cacheKey.length !== cacheKey.length) {
continue;
}
let isValid = true;
for (let i = 0; i < cacheKey.length; i += 1) {
if (cache.cacheKey[i] !== cacheKey[i]) {
isValid = false;
break;
}
}
if (isValid) {
return cache;
}
}

return undefined;
};

export default (
template: any,
meta: Directive,
bindings: undefined | null | any[],
flags: Record<keyof any, any>,
): Type<any> => {
const caches = getCache();

// nulls help to detect defaults
const cacheKey = [template, ...(bindings ?? [null]), ...(flags.providers ?? [null])];
let ctor = checkCache(caches, cacheKey);
if (ctor) {
return ctor;
}

const mockTemplate = funcGenerateTemplate(template, { ...meta, bindings });
const options: Component = {
providers: flags.providers,
selector: 'mock-render',
template: mockTemplate,
};

return generateWrapper({ ...meta, bindings, options });
ctor = generateWrapper({ ...meta, bindings, options });
coreDefineProperty(ctor, 'cacheKey', cacheKey, false);
caches.unshift(ctor as any);
caches.splice(ngMocksUniverse.global.get('mockRenderCacheSize') ?? coreConfig.mockRenderCacheSize);

return ctor;
};
167 changes: 167 additions & 0 deletions tests-performance/mock-builder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import {
Component,
Directive,
Injectable,
NgModule,
} from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockBuilder, ngMocks } from 'ng-mocks';

@Injectable()
class TargetService {
public readonly name = 'target';
}

@Directive({
selector: 'target',
})
class TargetDirective {}

@Component({
selector: 'target',
template: '{{ service.name }}',
})
class TargetComponent {
public constructor(public readonly service: TargetService) {}
}

@NgModule({
declarations: [TargetComponent, TargetDirective],
exports: [TargetComponent],
providers: [TargetService],
})
class TargetModule {}

describe('performance:MockBuilder', () => {
let timeStandard = 0;
let timeMockBuilder = 0;
let timeFasterBeforeEach = 0;
let timeFasterBeforeAll = 0;

jasmine.getEnv().addReporter({
jasmineDone: () => {
// tslint:disable-next-line no-console
console.log(`performance:MockBuilder`);
// tslint:disable-next-line no-console
console.log(`Time standard: ${timeStandard}`);
// tslint:disable-next-line no-console
console.log(`Time MockBuilder: ${timeMockBuilder}`);
// tslint:disable-next-line no-console
console.log(
`Ration standard / MockBuilder: ${
timeStandard / timeMockBuilder
}`,
);
// tslint:disable-next-line no-console
console.log(`Time beforeEach: ${timeFasterBeforeEach}`);
// tslint:disable-next-line no-console
console.log(
`Ratio standard / beforeEach: ${
timeStandard / timeFasterBeforeEach
}`,
);
// tslint:disable-next-line no-console
console.log(`Time beforeAll: ${timeFasterBeforeAll}`);
// tslint:disable-next-line no-console
console.log(
`Ratio beforeEach / beforeAll: ${
timeFasterBeforeEach / timeFasterBeforeAll
}`,
);
},
});

describe('standard', () => {
beforeAll(() => (timeStandard = Date.now()));
afterAll(() => (timeStandard = Date.now() - timeStandard));

beforeEach(() =>
TestBed.configureTestingModule({
imports: [TargetModule],
}).compileComponents(),
);

for (let i = 0; i < 100; i += 1) {
it(`#${i}`, () => {
const fixture = TestBed.createComponent(TargetComponent);
fixture.detectChanges();
expect(ngMocks.formatText(fixture)).toEqual('target');
});
}
});

describe('faster:MockBuilder', () => {
beforeAll(() => (timeMockBuilder = Date.now()));
afterAll(() => (timeMockBuilder = Date.now() - timeMockBuilder));

beforeEach(() =>
MockBuilder([TargetComponent, TargetService], TargetModule),
);

for (let i = 0; i < 100; i += 1) {
it(`#${i}`, () => {
const fixture = TestBed.createComponent(TargetComponent);
fixture.detectChanges();
expect(ngMocks.formatText(fixture)).toEqual('target');
});
}
});

describe('faster:beforeEach', () => {
ngMocks.faster();

beforeAll(() => (timeFasterBeforeEach = Date.now()));
afterAll(
() =>
(timeFasterBeforeEach = Date.now() - timeFasterBeforeEach),
);

beforeEach(() =>
MockBuilder([TargetComponent, TargetService], TargetModule),
);

for (let i = 0; i < 100; i += 1) {
it(`#${i}`, () => {
const fixture = TestBed.createComponent(TargetComponent);
fixture.detectChanges();
expect(ngMocks.formatText(fixture)).toEqual('target');
});
}
});

describe('faster:beforeAll', () => {
ngMocks.faster();

beforeAll(() => (timeFasterBeforeAll = Date.now()));
afterAll(
() => (timeFasterBeforeAll = Date.now() - timeFasterBeforeAll),
);

beforeAll(() =>
MockBuilder([TargetComponent, TargetService], TargetModule),
);

for (let i = 0; i < 100; i += 1) {
it(`#${i}`, () => {
const fixture = TestBed.createComponent(TargetComponent);
fixture.detectChanges();
expect(ngMocks.formatText(fixture)).toEqual('target');
});
}
});

it('ensures that faster is faster', () => {
// Usually, it is faster, but it is fine if we are down for 25%
expect(timeStandard / timeFasterBeforeEach).toBeGreaterThan(0.75);

// beforeEach should be definitely slower than beforeAll
expect(
timeFasterBeforeEach / timeFasterBeforeAll,
).toBeGreaterThan(0.75);

// without faster should be always slower
expect(timeMockBuilder / timeFasterBeforeEach).toBeGreaterThan(
0.75,
);
});
});
Loading

0 comments on commit 1a68ea7

Please sign in to comment.