From 235a70d31a6e2fba85dd46c1b6da61bff8ce7f31 Mon Sep 17 00:00:00 2001 From: aboulmane Date: Tue, 22 Oct 2019 03:43:07 +0100 Subject: [PATCH 1/7] refs #8 some performances specs --- package.json | 2 + sample/sample2-inject/app.ts | 4 +- test/performance/ContainerMock.ts | 45 ++++++++ test/performance/PerfHooks.ts | 18 +++ test/performance/Performance.spec.ts | 157 +++++++++++++++++++++++++++ 5 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 test/performance/ContainerMock.ts create mode 100644 test/performance/PerfHooks.ts create mode 100644 test/performance/Performance.spec.ts diff --git a/package.json b/package.json index 5467fe4..a99f000 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,12 @@ "devDependencies": { "@types/chai": "^4.2.3", "@types/jest": "^24.0.15", + "@types/node": "^12.11.1", "@types/sinon-chai": "^3.2.3", "chai": "^4.2.0", "coveralls": "^3.0.6", "jest": "^24.9.0", + "perf_hooks": "^0.0.1", "prettier": "^1.18.2", "prettier-tslint": "^0.4.2", "reflect-metadata": "^0.1.13", diff --git a/sample/sample2-inject/app.ts b/sample/sample2-inject/app.ts index f5f9fa2..51211c6 100644 --- a/sample/sample2-inject/app.ts +++ b/sample/sample2-inject/app.ts @@ -1,6 +1,6 @@ import "reflect-metadata"; -import { Container } from "../../src/index"; +import { Container } from "../../src"; import { CoffeeMaker } from "./CoffeeMaker"; -const coffeeMaker = Container.get(CoffeeMaker); +const coffeeMaker = Container.get(CoffeeMaker); coffeeMaker.make(); diff --git a/test/performance/ContainerMock.ts b/test/performance/ContainerMock.ts new file mode 100644 index 0000000..3437956 --- /dev/null +++ b/test/performance/ContainerMock.ts @@ -0,0 +1,45 @@ +import { Container, ServiceIdentifier, ServiceMetadata, Token } from "../../src"; +import { Function } from "@babel/types"; + +type IdentifierOrServiceMetadata = ServiceIdentifier | ServiceMetadata | (Array>); + +export class ContainerMock { + static serviceMap = new Map(); + static set ( + identifierOrServiceMetadata: IdentifierOrServiceMetadata, + value?: any, + ): any { + if (identifierOrServiceMetadata instanceof Array) { + identifierOrServiceMetadata.forEach((v: any) => this.set(v)); + return this; + } + if (typeof identifierOrServiceMetadata === "string" || identifierOrServiceMetadata instanceof Token) { + return this.set({ id: identifierOrServiceMetadata, value }); + } + if ( + typeof identifierOrServiceMetadata === "object" && + (identifierOrServiceMetadata as { service: Token }).service + ) { + return this.set({ id: (identifierOrServiceMetadata as { service: Token }).service, value }); + } + if (identifierOrServiceMetadata instanceof Function) { + return this.set({ type: identifierOrServiceMetadata, id: identifierOrServiceMetadata, value }); + } + + // const newService: ServiceMetadata = arguments.length === 1 && + // typeof identifierOrServiceMetadata === "object" && + // !(identifierOrServiceMetadata instanceof Token) ? identifierOrServiceMetadata : undefined; + const newService: ServiceMetadata = identifierOrServiceMetadata as any; + const service = this.serviceMap.get(newService.id); + if (service && service.multiple !== true) { + Object.assign(service, newService); + } else { + this.serviceMap.set(newService.id, newService); + } + } + + static get (identifier: ServiceIdentifier): T { + const service = this.serviceMap.get(identifier); + return service.value || service; + } +} diff --git a/test/performance/PerfHooks.ts b/test/performance/PerfHooks.ts new file mode 100644 index 0000000..9113669 --- /dev/null +++ b/test/performance/PerfHooks.ts @@ -0,0 +1,18 @@ +import { performance, PerformanceEntry, PerformanceObserver } from "perf_hooks"; + +type Funct = (...arg: any[]) => any; + +export const useTimerify = (funct: Funct): [ Funct, Promise] => { + let obs: PerformanceObserver; + const timerData = new Promise((resolve, reject) => { + obs = new PerformanceObserver((list) => { + resolve(list.getEntries()[0]); + obs.disconnect(); + }); + }); + const wrapped = (...args: any[]) => { + if (obs) { obs.observe({ entryTypes: ["function"] }); } + return performance.timerify(funct)(...args); + }; + return [ wrapped, timerData ]; +}; diff --git a/test/performance/Performance.spec.ts b/test/performance/Performance.spec.ts new file mode 100644 index 0000000..98f0e7e --- /dev/null +++ b/test/performance/Performance.spec.ts @@ -0,0 +1,157 @@ +/* tslint:disable:max-classes-per-file */ +import "reflect-metadata"; + +import * as chai from "chai"; +import sinon_chai from "sinon-chai"; + +import { Container, Service } from "../../src"; +import { ContainerMock } from "./ContainerMock"; +import { useTimerify } from "./PerfHooks"; + +chai.should(); +chai.use(sinon_chai); +const expect = chai.expect; + +describe("Performance", () => { + beforeEach(() => Container.reset()); + + describe("registerHandler", () => { + it("should have ability to pre-specify class initialization parameters (normal case)", () => { + @Service() + class ExtraService { + constructor (public luckyNumber: number, public message: string) {} + } + + Container.registerHandler({ + object: ExtraService, + index: 0, + value: (containerInstance) => 777, + }); + + Container.registerHandler({ + object: ExtraService, + index: 1, + value: (containerInstance) => "hello parameter", + }); + + const [ getServices, getServicesTimer ] = useTimerify(() => { + Container.get(ExtraService).luckyNumber.should.be.equal(777); + Container.get(ExtraService).message.should.be.equal("hello parameter"); + }); + + getServices(); + getServicesTimer.then(({ duration }) => duration.should.be.lessThan(3)); + }); + + it("should be able to manage a lot of Handlers (1000000 Handlers)", async () => { + @Service() + class ExtraService { + constructor (public luckyNumber: number, public message: string) {} + } + + const [ setAll, setAllTimer ] = useTimerify(() => + Array.from(Array(1000000)).map((val, index) => Container.registerHandler({ + index, + object: `test${index}-service`, + value: (containerInstance) => index, + })), + ); + + setAll(); + setAllTimer.then(({ duration }) => console.log(duration)); + const { duration: setAllDuration } = await setAllTimer; + setAllDuration.should.be.lessThan(1500); + + Container.registerHandler({ + object: ExtraService, + index: 0, + value: (containerInstance) => 777, + }); + + Container.registerHandler({ + object: ExtraService, + index: 1, + value: (containerInstance) => "hello parameter", + }); + + const [ getServices, getServicesTimer ] = useTimerify(() => { + Container.get(ExtraService).luckyNumber.should.be.equal(777); + Container.get(ExtraService).message.should.be.equal("hello parameter"); + }); + + getServices(); + getServicesTimer.then(({ duration }) => console.log(duration)); + const { duration: getServicesDuration } = await getServicesTimer; + getServicesDuration.should.be.lessThan(4); + }); + }); + + describe("set", () => { + + it("should be able to manage a lot of services (proof of concept)", async () => { + class TestService {} + + const testService = new TestService(); + const test1Service = new TestService(); + + const [ setAll, setAllTimer ] = useTimerify(() => + Array.from(Array(10000)).map((val, index) => ContainerMock.set({ + id: `test${index}-service`, + value: test1Service, + })), + ); + + setAll(); + + ContainerMock.set([ + { id: TestService, value: testService }, + ]); + + const [ getServices, getServicesTimer ] = useTimerify(() => { + ContainerMock.get(TestService).should.be.equal(testService); + ContainerMock.get("test999-service").should.be.equal(test1Service); + }); + + getServices(); + + const setAllTimerEntry = await setAllTimer; + const getServicesTimerEntry = await getServicesTimer; + + setAllTimerEntry.duration.should.be.lessThan(60); + getServicesTimerEntry.duration.should.be.lessThan(4); + }); + + it("should be able to manage a lot of services (10000 services)", async () => { + class TestService {} + + const testService = new TestService(); + const test1Service = new TestService(); + + const [ setAll, setAllTimer ] = useTimerify(() => + Array.from(Array(10000)).map((val, index) => Container.set({ + id: `test${index}-service`, + value: test1Service, + })), + ); + + setAll(); + + Container.set([ + { id: TestService, value: testService }, + ]); + + const [ getServices, getServicesTimer ] = useTimerify(() => { + Container.get(TestService).should.be.equal(testService); + Container.get("test999-service").should.be.equal(test1Service); + }); + + getServices(); + + const setAllTimerEntry = await setAllTimer; + const getServicesTimerEntry = await getServicesTimer; + + setAllTimerEntry.duration.should.be.lessThan(60); + getServicesTimerEntry.duration.should.be.lessThan(4); + }); + }); +}); From 365eb5c9fea8051fe32393bc401cd96aed02afb1 Mon Sep 17 00:00:00 2001 From: aboulmane Date: Tue, 22 Oct 2019 07:13:57 +0100 Subject: [PATCH 2/7] refs #1 migrate jest configuration Using the CLI ts-jest config:migrate --- jest.config.js | 20 +++++++++++++------- package.json | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/jest.config.js b/jest.config.js index 8687013..60fa3cd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,13 +1,19 @@ module.exports = { globals: { - "ts-jest": { - tsConfigFile: `${__dirname}/tsconfig.json`, + 'ts-jest': { + tsConfig: '/Users/aboulmane/Dropbox/@express.ts/container/tsconfig.json', }, }, - moduleFileExtensions: ["ts", "js"], + moduleFileExtensions: [ + 'js', + 'ts', + ], transform: { - "^.+\\.(ts|tsx)$": "./node_modules/ts-jest/preprocessor.js", + '^.+\\.(ts|tsx)$': 'ts-jest', }, - testMatch: ["**/test/**/*.spec.(ts|js)"], - testEnvironment: "node", -}; + testMatch: [ + '**/test/**/*.spec.(ts|js)', + ], + testEnvironment: 'node', + preset: 'ts-jest', +} diff --git a/package.json b/package.json index a99f000..fbe8a11 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@express.ts/container", "version": "0.8.0", - "description": "test package", + "description": "Simple yet powerful implementation of the Inversion of Control (IoC) principle. IoC is also known as dependency injection (DI). based on TypeDI", "author": "aboulmane ", "main": "dist/index", "license": "ISC", From 378ababcd950ef5fa734e4e62fc5d0bcce0cbb4f Mon Sep 17 00:00:00 2001 From: aboulmane Date: Tue, 22 Oct 2019 07:16:14 +0100 Subject: [PATCH 3/7] refs #3 code cleanup --- test/github-issues/41/issue-41.spec.ts | 4 ++-- test/github-issues/42/issue-42.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/github-issues/41/issue-41.spec.ts b/test/github-issues/41/issue-41.spec.ts index 02a9b01..db274c2 100644 --- a/test/github-issues/41/issue-41.spec.ts +++ b/test/github-issues/41/issue-41.spec.ts @@ -8,10 +8,10 @@ import { Container, Service, Token } from "../../../src"; chai.should(); chai.use(sinon_chai); -describe("github issues > #41 Token as service id in combination with factory", function () { +describe("github issues > #41 Token as service id in combination with factory", () => { beforeEach(() => Container.reset()); - it("should work properly", function () { + it("should work properly", () => { interface SomeInterface { foo (): string; } diff --git a/test/github-issues/42/issue-42.spec.ts b/test/github-issues/42/issue-42.spec.ts index c784667..bdb6085 100644 --- a/test/github-issues/42/issue-42.spec.ts +++ b/test/github-issues/42/issue-42.spec.ts @@ -9,10 +9,10 @@ chai.should(); chai.use(sinon_chai); const expect = chai.expect; -describe("github issues > #42 Exception not thrown on missing binding", function () { +describe("github issues > #42 Exception not thrown on missing binding", () => { beforeEach(() => Container.reset()); - it("should work properly", function () { + it("should work properly", () => { interface Factory { create (): void; } From 3344336458855eb591d510e20897ae721aa5e39c Mon Sep 17 00:00:00 2001 From: aboulmane Date: Tue, 22 Oct 2019 07:18:50 +0100 Subject: [PATCH 4/7] refs #8 performance: make the service id mandatory --- src/ContainerInstance.ts | 29 +++++++++++++++-------------- src/decorators/Service.ts | 16 ++++++++-------- src/types/ServiceMetadata.ts | 2 +- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/ContainerInstance.ts b/src/ContainerInstance.ts index 7dc3899..045d0ba 100644 --- a/src/ContainerInstance.ts +++ b/src/ContainerInstance.ts @@ -105,27 +105,32 @@ export class ContainerInstance { identifierOrServiceMetadata: ServiceIdentifier | ServiceMetadata | (Array>), value?: any, ): this { + let newService: ServiceMetadata = identifierOrServiceMetadata as any; if (identifierOrServiceMetadata instanceof Array) { identifierOrServiceMetadata.forEach((v: any) => this.set(v)); return this; } if (typeof identifierOrServiceMetadata === "string" || identifierOrServiceMetadata instanceof Token) { - return this.set({ id: identifierOrServiceMetadata, value }); + newService = { id: identifierOrServiceMetadata, value }; } if ( typeof identifierOrServiceMetadata === "object" && (identifierOrServiceMetadata as { service: Token }).service ) { - return this.set({ id: (identifierOrServiceMetadata as { service: Token }).service, value }); + newService = { id: (identifierOrServiceMetadata as { service: Token }).service, value }; } if (identifierOrServiceMetadata instanceof Function) { - return this.set({ type: identifierOrServiceMetadata, id: identifierOrServiceMetadata, value }); + newService = { type: identifierOrServiceMetadata, id: identifierOrServiceMetadata, value }; + } + if ( + typeof identifierOrServiceMetadata === "object" && + (identifierOrServiceMetadata as { type: any }).type && + !(identifierOrServiceMetadata as { id: any }).id + ) { + const { type } = identifierOrServiceMetadata as any; + newService = { ...identifierOrServiceMetadata, id: type, value }; } - // const newService: ServiceMetadata = arguments.length === 1 && - // typeof identifierOrServiceMetadata === "object" && - // !(identifierOrServiceMetadata instanceof Token) ? identifierOrServiceMetadata : undefined; - const newService: ServiceMetadata = identifierOrServiceMetadata as any; const service = this.findService(newService.id); if (service && service.multiple !== true) { Object.assign(service, newService); @@ -168,8 +173,9 @@ export class ContainerInstance { if (service.id) { return service.id === identifier; } - + // todo not covered by unit tests if (service.type && identifier instanceof Function) { + console.log("todo not covered by unit tests"); return service.type === identifier || identifier.prototype instanceof service.type; } @@ -193,11 +199,6 @@ export class ContainerInstance { return service.id === identifier; } - - if (service.type && identifier instanceof Function) { - return service.type === identifier; - } // todo: not sure why it was here || identifier.prototype instanceof service.type; - return false; }); } @@ -240,7 +241,7 @@ export class ContainerInstance { throw new MissingProvidedServiceTypeError(identifier); } - service = { type }; + service = { type, id: type }; this.services.push(service); } diff --git a/src/decorators/Service.ts b/src/decorators/Service.ts index 3022980..ed9cba2 100644 --- a/src/decorators/Service.ts +++ b/src/decorators/Service.ts @@ -14,7 +14,6 @@ export function Service ( maybeFactory?: (...args: any[]) => any, ): any { if (arguments.length === 2 || optionsOrServiceName instanceof Function) { - console.log("debug", { optionsOrServiceName, maybeFactory }); const serviceId = { service: new Token() }; const dependencies = arguments.length === 2 ? (optionsOrServiceName as any[]) : []; const factory = arguments.length === 2 ? maybeFactory : (optionsOrServiceName as (...args: any[]) => any); @@ -31,21 +30,22 @@ export function Service ( } else { return (target: (...args: any[]) => any) => { const service: ServiceMetadata = { + id: target, type: target, }; if (typeof optionsOrServiceName === "string" || optionsOrServiceName instanceof Token) { - service.id = optionsOrServiceName; + service.id = optionsOrServiceName || target; service.multiple = (optionsOrServiceName as ServiceOptions).multiple; service.global = (optionsOrServiceName as ServiceOptions).global || false; service.transient = (optionsOrServiceName as ServiceOptions).transient; } else if (optionsOrServiceName) { - // ServiceOptions - service.id = (optionsOrServiceName as ServiceOptions).id; - service.factory = (optionsOrServiceName as ServiceOptions).factory; - service.multiple = (optionsOrServiceName as ServiceOptions).multiple; - service.global = (optionsOrServiceName as ServiceOptions).global || false; - service.transient = (optionsOrServiceName as ServiceOptions).transient; + const serviceOptions = optionsOrServiceName as ServiceOptions; + service.id = serviceOptions.id || target; + service.factory = serviceOptions.factory; + service.multiple = serviceOptions.multiple; + service.global = serviceOptions.global || false; + service.transient = serviceOptions.transient; } Container.set(service); diff --git a/src/types/ServiceMetadata.ts b/src/types/ServiceMetadata.ts index 50a06a9..faade60 100644 --- a/src/types/ServiceMetadata.ts +++ b/src/types/ServiceMetadata.ts @@ -31,7 +31,7 @@ export interface ServiceMetadata { /** * Service unique identifier. */ - id?: ServiceIdentifier; + id: ServiceIdentifier; /** * Factory function used to initialize this service. From a2389b0abed525f2d999a6f2bc2cae94e2644d89 Mon Sep 17 00:00:00 2001 From: aboulmane Date: Thu, 24 Oct 2019 08:10:23 +0100 Subject: [PATCH 5/7] refs #8 performance: improve set and get performances --- jest.config.js | 2 +- src/Container.ts | 12 ++++- src/ContainerInstance.ts | 79 +++++++++++----------------- test/performance/Performance.spec.ts | 11 ++-- 4 files changed, 48 insertions(+), 56 deletions(-) diff --git a/jest.config.js b/jest.config.js index 60fa3cd..01bc020 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,7 @@ module.exports = { globals: { 'ts-jest': { - tsConfig: '/Users/aboulmane/Dropbox/@express.ts/container/tsconfig.json', + tsConfig: 'tsconfig.json', }, }, moduleFileExtensions: [ diff --git a/src/Container.ts b/src/Container.ts index c54c49d..aebe128 100644 --- a/src/Container.ts +++ b/src/Container.ts @@ -25,7 +25,7 @@ export class Container { /** * All registered handlers. */ - static readonly handlers: Handler[] = []; + static readonly handlers: Map = new Map(); // ------------------------------------------------------------------------- // Public Static Methods @@ -123,7 +123,15 @@ export class Container { * Registers a new handler. */ static registerHandler (handler: Handler): Container { - this.handlers.push(handler); + const id = handler.propertyName && typeof handler.object !== "string" && !(handler.object instanceof Token) + ? handler.object.constructor + : handler.object; + const handlers = this.handlers.get(id); + if (handlers) { + handlers.push(handler); + } else { + this.handlers.set(id, [handler]); + } return this; } diff --git a/src/ContainerInstance.ts b/src/ContainerInstance.ts index 045d0ba..8d395ec 100644 --- a/src/ContainerInstance.ts +++ b/src/ContainerInstance.ts @@ -26,7 +26,8 @@ export class ContainerInstance { /** * All registered services. */ - private services: Array> = []; + private services: Map, ServiceMetadata> = new Map(); + private groupedServices: Map, Array>> = new Map(); // ------------------------------------------------------------------------- // Constructor @@ -63,6 +64,7 @@ export class ContainerInstance { const globalContainer = Container.of(undefined); const service = globalContainer.findService(identifier); const scopedService = this.findService(identifier); + const groupedService = this.groupedServices.get(identifier); if (service && service.global === true) { return this.getServiceValue(identifier, service); @@ -80,6 +82,10 @@ export class ContainerInstance { return value; } + if (groupedService && groupedService.length) { + return this.getServiceValue(identifier, groupedService[0]); + } + return this.getServiceValue(identifier, service); } @@ -88,7 +94,11 @@ export class ContainerInstance { * Used when service defined with multiple: true flag. */ getMany (id: string | Token): T[] { - return this.filterServices(id).map((service) => this.getServiceValue(id, service)); + try { + return this.groupedServices.get(id).map((service) => this.getServiceValue(id, service)); + } catch (e) { + throw new ServiceNotFoundError(id); + } } /** @@ -131,11 +141,16 @@ export class ContainerInstance { newService = { ...identifierOrServiceMetadata, id: type, value }; } - const service = this.findService(newService.id); - if (service && service.multiple !== true) { - Object.assign(service, newService); + if (newService.multiple === true) { + const otherServices = this.groupedServices.get(newService.id) || []; + this.groupedServices.set(newService.id, [ ...otherServices, newService ]); } else { - this.services.push(newService); + const service = this.findService(newService.id); + if (service && service.multiple !== true) { + Object.assign(service, newService); + } else { + this.services.set(newService.id, newService); + } } return this; @@ -145,11 +160,7 @@ export class ContainerInstance { * Removes services with a given service identifiers (tokens or types). */ remove (...ids: ServiceIdentifier[]): this { - ids.forEach((id) => { - this.filterServices(id).forEach((service) => { - this.services.splice(this.services.indexOf(service), 1); - }); - }); + ids.forEach((id) => this.services.delete(id)); return this; } @@ -157,7 +168,7 @@ export class ContainerInstance { * Completely resets the container by removing all previously registered services from it. */ reset (): this { - this.services = []; + this.services.clear(); return this; } @@ -169,38 +180,17 @@ export class ContainerInstance { * Filters registered service in the with a given service identifier. */ private filterServices (identifier: ServiceIdentifier): Array> { - return this.services.filter((service) => { - if (service.id) { - return service.id === identifier; - } - // todo not covered by unit tests - if (service.type && identifier instanceof Function) { - console.log("todo not covered by unit tests"); - return service.type === identifier || identifier.prototype instanceof service.type; - } - - return false; - }); + return this.services.get(identifier) as any as Array>; } /** * Finds registered service in the with a given service identifier. */ private findService (identifier: ServiceIdentifier): ServiceMetadata | undefined { - return this.services.find((service) => { - if (service.id) { - if ( - identifier instanceof Object && - service.id instanceof Token && - (identifier as any).service instanceof Token - ) { - return service.id === (identifier as any).service; - } - - return service.id === identifier; - } - return false; - }); + const id = ((identifier as any).service instanceof Token) + ? (identifier as any).service + : identifier; + return this.services.get(id); } /** @@ -242,7 +232,7 @@ export class ContainerInstance { } service = { type, id: type }; - this.services.push(service); + this.services.set(service.id, service); } // setup constructor parameters for a newly initialized service @@ -300,7 +290,7 @@ export class ContainerInstance { */ private initializeParams (type: Function, paramTypes: any[]): any[] { return paramTypes.map((paramType, index) => { - const paramHandler = Container.handlers.find((handler) => handler.object === type && handler.index === index); + const paramHandler = (Container.handlers.get(type) || []).find((handler) => handler.index === index); if (paramHandler) { return paramHandler.value(this); } @@ -324,14 +314,7 @@ export class ContainerInstance { * Applies all registered handlers on a given target class. */ private applyPropertyHandlers (target: Function, instance: { [key: string]: any }) { - Container.handlers.forEach((handler) => { - if (typeof handler.index === "number") { - return; - } - if (handler.object.constructor !== target && !(target.prototype instanceof handler.object.constructor)) { - return; - } - + (Container.handlers.get(target) || []).forEach((handler) => { instance[handler.propertyName] = handler.value(this); }); } diff --git a/test/performance/Performance.spec.ts b/test/performance/Performance.spec.ts index 98f0e7e..4cb2b97 100644 --- a/test/performance/Performance.spec.ts +++ b/test/performance/Performance.spec.ts @@ -16,7 +16,7 @@ describe("Performance", () => { beforeEach(() => Container.reset()); describe("registerHandler", () => { - it("should have ability to pre-specify class initialization parameters (normal case)", () => { + it("should have ability to pre-specify class initialization parameters (normal case)", async () => { @Service() class ExtraService { constructor (public luckyNumber: number, public message: string) {} @@ -40,7 +40,8 @@ describe("Performance", () => { }); getServices(); - getServicesTimer.then(({ duration }) => duration.should.be.lessThan(3)); + const { duration: getServicesDuration } = await getServicesTimer; + getServicesDuration.should.be.lessThan(5); }); it("should be able to manage a lot of Handlers (1000000 Handlers)", async () => { @@ -60,7 +61,7 @@ describe("Performance", () => { setAll(); setAllTimer.then(({ duration }) => console.log(duration)); const { duration: setAllDuration } = await setAllTimer; - setAllDuration.should.be.lessThan(1500); + setAllDuration.should.be.lessThan(2500); Container.registerHandler({ object: ExtraService, @@ -82,7 +83,7 @@ describe("Performance", () => { getServices(); getServicesTimer.then(({ duration }) => console.log(duration)); const { duration: getServicesDuration } = await getServicesTimer; - getServicesDuration.should.be.lessThan(4); + getServicesDuration.should.be.lessThan(2); }); }); @@ -118,7 +119,7 @@ describe("Performance", () => { const getServicesTimerEntry = await getServicesTimer; setAllTimerEntry.duration.should.be.lessThan(60); - getServicesTimerEntry.duration.should.be.lessThan(4); + getServicesTimerEntry.duration.should.be.lessThan(2); }); it("should be able to manage a lot of services (10000 services)", async () => { From d66b15f756fb5fd246cd9a294c3c6ed3094a5cb6 Mon Sep 17 00:00:00 2001 From: aboulmane Date: Thu, 24 Oct 2019 08:25:19 +0100 Subject: [PATCH 6/7] refs #8 performance: improve testing coverages * add get/set { service: T } testing --- jest.config.js | 2 +- src/error/MissingProvidedServiceTypeError.ts | 3 +- test/Container.spec.ts | 18 ++++++ test/performance/ContainerMock.ts | 45 -------------- test/performance/Performance.spec.ts | 64 +++++++++++--------- 5 files changed, 57 insertions(+), 75 deletions(-) delete mode 100644 test/performance/ContainerMock.ts diff --git a/jest.config.js b/jest.config.js index 01bc020..3f0cba3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,7 @@ module.exports = { globals: { 'ts-jest': { - tsConfig: 'tsconfig.json', + tsConfig: `${__dirname}/tsconfig.json`, }, }, moduleFileExtensions: [ diff --git a/src/error/MissingProvidedServiceTypeError.ts b/src/error/MissingProvidedServiceTypeError.ts index f9d4f5e..b79dc14 100644 --- a/src/error/MissingProvidedServiceTypeError.ts +++ b/src/error/MissingProvidedServiceTypeError.ts @@ -5,7 +5,8 @@ export class MissingProvidedServiceTypeError extends Error { name = "ServiceNotFoundError"; constructor (identifier: any) { - super(`Cannot determine a class of the requesting service "${identifier}"`); + super(); + this.message = `Cannot determine a class of the requesting service "${JSON.stringify(identifier)}"`; Object.setPrototypeOf(this, MissingProvidedServiceTypeError.prototype); } } diff --git a/test/Container.spec.ts b/test/Container.spec.ts index 5e82ea3..d9c8d2e 100644 --- a/test/Container.spec.ts +++ b/test/Container.spec.ts @@ -6,6 +6,7 @@ import sinon_chai from "sinon-chai"; import { Container, Service, Token } from "../src"; import { ServiceNotFoundError } from "../src/error/ServiceNotFoundError"; +import { MissingProvidedServiceTypeError } from "../src/error/MissingProvidedServiceTypeError"; chai.should(); chai.use(sinon_chai); @@ -82,6 +83,23 @@ describe("Container", () => { Container.get(SecondTestToken).name.should.be.equal("second"); }); + it("should be able to set a service like { service: T }", () => { + class TestService { + constructor (public name: string) {} + } + const FirstTestToken = new Token(); + + const firstService = new TestService("first"); + Container.set({ service: FirstTestToken }, firstService); + + const secondService = new TestService("second"); + Container.set({ service: TestService }, secondService); + + Container.get({ service: FirstTestToken }).name.should.be.equal("first"); + + expect(() => Container.get({ service: TestService })).to.throw(MissingProvidedServiceTypeError); + }); + it("should override previous value if service is written second time", () => { class TestService { constructor (public name: string) {} diff --git a/test/performance/ContainerMock.ts b/test/performance/ContainerMock.ts deleted file mode 100644 index 3437956..0000000 --- a/test/performance/ContainerMock.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Container, ServiceIdentifier, ServiceMetadata, Token } from "../../src"; -import { Function } from "@babel/types"; - -type IdentifierOrServiceMetadata = ServiceIdentifier | ServiceMetadata | (Array>); - -export class ContainerMock { - static serviceMap = new Map(); - static set ( - identifierOrServiceMetadata: IdentifierOrServiceMetadata, - value?: any, - ): any { - if (identifierOrServiceMetadata instanceof Array) { - identifierOrServiceMetadata.forEach((v: any) => this.set(v)); - return this; - } - if (typeof identifierOrServiceMetadata === "string" || identifierOrServiceMetadata instanceof Token) { - return this.set({ id: identifierOrServiceMetadata, value }); - } - if ( - typeof identifierOrServiceMetadata === "object" && - (identifierOrServiceMetadata as { service: Token }).service - ) { - return this.set({ id: (identifierOrServiceMetadata as { service: Token }).service, value }); - } - if (identifierOrServiceMetadata instanceof Function) { - return this.set({ type: identifierOrServiceMetadata, id: identifierOrServiceMetadata, value }); - } - - // const newService: ServiceMetadata = arguments.length === 1 && - // typeof identifierOrServiceMetadata === "object" && - // !(identifierOrServiceMetadata instanceof Token) ? identifierOrServiceMetadata : undefined; - const newService: ServiceMetadata = identifierOrServiceMetadata as any; - const service = this.serviceMap.get(newService.id); - if (service && service.multiple !== true) { - Object.assign(service, newService); - } else { - this.serviceMap.set(newService.id, newService); - } - } - - static get (identifier: ServiceIdentifier): T { - const service = this.serviceMap.get(identifier); - return service.value || service; - } -} diff --git a/test/performance/Performance.spec.ts b/test/performance/Performance.spec.ts index 4cb2b97..9f2e4c4 100644 --- a/test/performance/Performance.spec.ts +++ b/test/performance/Performance.spec.ts @@ -5,7 +5,6 @@ import * as chai from "chai"; import sinon_chai from "sinon-chai"; import { Container, Service } from "../../src"; -import { ContainerMock } from "./ContainerMock"; import { useTimerify } from "./PerfHooks"; chai.should(); @@ -44,7 +43,7 @@ describe("Performance", () => { getServicesDuration.should.be.lessThan(5); }); - it("should be able to manage a lot of Handlers (1000000 Handlers)", async () => { + it("should be able to manage a lot of Handlers (1000000 param Handlers)", async () => { @Service() class ExtraService { constructor (public luckyNumber: number, public message: string) {} @@ -53,7 +52,7 @@ describe("Performance", () => { const [ setAll, setAllTimer ] = useTimerify(() => Array.from(Array(1000000)).map((val, index) => Container.registerHandler({ index, - object: `test${index}-service`, + object: "test-service", value: (containerInstance) => index, })), ); @@ -83,44 +82,53 @@ describe("Performance", () => { getServices(); getServicesTimer.then(({ duration }) => console.log(duration)); const { duration: getServicesDuration } = await getServicesTimer; - getServicesDuration.should.be.lessThan(2); + getServicesDuration.should.be.lessThan(5); }); - }); - - describe("set", () => { - it("should be able to manage a lot of services (proof of concept)", async () => { - class TestService {} - - const testService = new TestService(); - const test1Service = new TestService(); + it("should be able to manage a lot of Handlers (1000000 property Handlers)", async () => { + @Service() + class ExtraService { + constructor (public luckyNumber: number, public message: string) {} + } const [ setAll, setAllTimer ] = useTimerify(() => - Array.from(Array(10000)).map((val, index) => ContainerMock.set({ - id: `test${index}-service`, - value: test1Service, + Array.from(Array(1000000)).map((val, index) => Container.registerHandler({ + propertyName: `test${index}-service`, + object: "test-service", + value: (containerInstance) => index, })), ); setAll(); + setAllTimer.then(({ duration }) => console.log(duration)); + const { duration: setAllDuration } = await setAllTimer; + setAllDuration.should.be.lessThan(2500); - ContainerMock.set([ - { id: TestService, value: testService }, - ]); + Container.registerHandler({ + object: ExtraService, + index: 0, + value: (containerInstance) => 777, + }); + + Container.registerHandler({ + object: ExtraService, + index: 1, + value: (containerInstance) => "hello parameter", + }); const [ getServices, getServicesTimer ] = useTimerify(() => { - ContainerMock.get(TestService).should.be.equal(testService); - ContainerMock.get("test999-service").should.be.equal(test1Service); + Container.get(ExtraService).luckyNumber.should.be.equal(777); + Container.get(ExtraService).message.should.be.equal("hello parameter"); }); getServices(); - - const setAllTimerEntry = await setAllTimer; - const getServicesTimerEntry = await getServicesTimer; - - setAllTimerEntry.duration.should.be.lessThan(60); - getServicesTimerEntry.duration.should.be.lessThan(2); + getServicesTimer.then(({ duration }) => console.log(duration)); + const { duration: getServicesDuration } = await getServicesTimer; + getServicesDuration.should.be.lessThan(5); }); + }); + + describe("set & get", () => { it("should be able to manage a lot of services (10000 services)", async () => { class TestService {} @@ -151,8 +159,8 @@ describe("Performance", () => { const setAllTimerEntry = await setAllTimer; const getServicesTimerEntry = await getServicesTimer; - setAllTimerEntry.duration.should.be.lessThan(60); - getServicesTimerEntry.duration.should.be.lessThan(4); + setAllTimerEntry.duration.should.be.lessThan(100); + getServicesTimerEntry.duration.should.be.lessThan(5); }); }); }); From 179ef1b6b1023055c23c8f027f592aa86b251381 Mon Sep 17 00:00:00 2001 From: aboulmane Date: Thu, 24 Oct 2019 13:15:00 +0100 Subject: [PATCH 7/7] refs #8 performance: code format --- src/Container.ts | 7 ++-- src/ContainerInstance.ts | 10 +++--- test/performance/PerfHooks.ts | 8 +++-- test/performance/Performance.spec.ts | 53 +++++++++++++++------------- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/src/Container.ts b/src/Container.ts index aebe128..bdbcff4 100644 --- a/src/Container.ts +++ b/src/Container.ts @@ -123,9 +123,10 @@ export class Container { * Registers a new handler. */ static registerHandler (handler: Handler): Container { - const id = handler.propertyName && typeof handler.object !== "string" && !(handler.object instanceof Token) - ? handler.object.constructor - : handler.object; + const id = + handler.propertyName && typeof handler.object !== "string" && !(handler.object instanceof Token) + ? handler.object.constructor + : handler.object; const handlers = this.handlers.get(id); if (handlers) { handlers.push(handler); diff --git a/src/ContainerInstance.ts b/src/ContainerInstance.ts index 8d395ec..6db9c31 100644 --- a/src/ContainerInstance.ts +++ b/src/ContainerInstance.ts @@ -141,9 +141,9 @@ export class ContainerInstance { newService = { ...identifierOrServiceMetadata, id: type, value }; } - if (newService.multiple === true) { + if (newService.multiple === true) { const otherServices = this.groupedServices.get(newService.id) || []; - this.groupedServices.set(newService.id, [ ...otherServices, newService ]); + this.groupedServices.set(newService.id, [...otherServices, newService]); } else { const service = this.findService(newService.id); if (service && service.multiple !== true) { @@ -180,16 +180,14 @@ export class ContainerInstance { * Filters registered service in the with a given service identifier. */ private filterServices (identifier: ServiceIdentifier): Array> { - return this.services.get(identifier) as any as Array>; + return (this.services.get(identifier) as any) as Array>; } /** * Finds registered service in the with a given service identifier. */ private findService (identifier: ServiceIdentifier): ServiceMetadata | undefined { - const id = ((identifier as any).service instanceof Token) - ? (identifier as any).service - : identifier; + const id = (identifier as any).service instanceof Token ? (identifier as any).service : identifier; return this.services.get(id); } diff --git a/test/performance/PerfHooks.ts b/test/performance/PerfHooks.ts index 9113669..c3a1159 100644 --- a/test/performance/PerfHooks.ts +++ b/test/performance/PerfHooks.ts @@ -2,7 +2,7 @@ import { performance, PerformanceEntry, PerformanceObserver } from "perf_hooks"; type Funct = (...arg: any[]) => any; -export const useTimerify = (funct: Funct): [ Funct, Promise] => { +export const useTimerify = (funct: Funct): [Funct, Promise] => { let obs: PerformanceObserver; const timerData = new Promise((resolve, reject) => { obs = new PerformanceObserver((list) => { @@ -11,8 +11,10 @@ export const useTimerify = (funct: Funct): [ Funct, Promise] = }); }); const wrapped = (...args: any[]) => { - if (obs) { obs.observe({ entryTypes: ["function"] }); } + if (obs) { + obs.observe({ entryTypes: ["function"] }); + } return performance.timerify(funct)(...args); }; - return [ wrapped, timerData ]; + return [wrapped, timerData]; }; diff --git a/test/performance/Performance.spec.ts b/test/performance/Performance.spec.ts index 9f2e4c4..75e3314 100644 --- a/test/performance/Performance.spec.ts +++ b/test/performance/Performance.spec.ts @@ -33,7 +33,7 @@ describe("Performance", () => { value: (containerInstance) => "hello parameter", }); - const [ getServices, getServicesTimer ] = useTimerify(() => { + const [getServices, getServicesTimer] = useTimerify(() => { Container.get(ExtraService).luckyNumber.should.be.equal(777); Container.get(ExtraService).message.should.be.equal("hello parameter"); }); @@ -49,12 +49,14 @@ describe("Performance", () => { constructor (public luckyNumber: number, public message: string) {} } - const [ setAll, setAllTimer ] = useTimerify(() => - Array.from(Array(1000000)).map((val, index) => Container.registerHandler({ - index, - object: "test-service", - value: (containerInstance) => index, - })), + const [setAll, setAllTimer] = useTimerify(() => + Array.from(Array(1000000)).map((val, index) => + Container.registerHandler({ + index, + object: "test-service", + value: (containerInstance) => index, + }), + ), ); setAll(); @@ -74,7 +76,7 @@ describe("Performance", () => { value: (containerInstance) => "hello parameter", }); - const [ getServices, getServicesTimer ] = useTimerify(() => { + const [getServices, getServicesTimer] = useTimerify(() => { Container.get(ExtraService).luckyNumber.should.be.equal(777); Container.get(ExtraService).message.should.be.equal("hello parameter"); }); @@ -91,12 +93,14 @@ describe("Performance", () => { constructor (public luckyNumber: number, public message: string) {} } - const [ setAll, setAllTimer ] = useTimerify(() => - Array.from(Array(1000000)).map((val, index) => Container.registerHandler({ - propertyName: `test${index}-service`, - object: "test-service", - value: (containerInstance) => index, - })), + const [setAll, setAllTimer] = useTimerify(() => + Array.from(Array(1000000)).map((val, index) => + Container.registerHandler({ + propertyName: `test${index}-service`, + object: "test-service", + value: (containerInstance) => index, + }), + ), ); setAll(); @@ -116,7 +120,7 @@ describe("Performance", () => { value: (containerInstance) => "hello parameter", }); - const [ getServices, getServicesTimer ] = useTimerify(() => { + const [getServices, getServicesTimer] = useTimerify(() => { Container.get(ExtraService).luckyNumber.should.be.equal(777); Container.get(ExtraService).message.should.be.equal("hello parameter"); }); @@ -129,27 +133,26 @@ describe("Performance", () => { }); describe("set & get", () => { - it("should be able to manage a lot of services (10000 services)", async () => { class TestService {} const testService = new TestService(); const test1Service = new TestService(); - const [ setAll, setAllTimer ] = useTimerify(() => - Array.from(Array(10000)).map((val, index) => Container.set({ - id: `test${index}-service`, - value: test1Service, - })), + const [setAll, setAllTimer] = useTimerify(() => + Array.from(Array(10000)).map((val, index) => + Container.set({ + id: `test${index}-service`, + value: test1Service, + }), + ), ); setAll(); - Container.set([ - { id: TestService, value: testService }, - ]); + Container.set([{ id: TestService, value: testService }]); - const [ getServices, getServicesTimer ] = useTimerify(() => { + const [getServices, getServicesTimer] = useTimerify(() => { Container.get(TestService).should.be.equal(testService); Container.get("test999-service").should.be.equal(test1Service); });