├── README.md ├── about ├── F1Nvd2wakAAQMfK.jpg ├── GAfwVKEbEAATNzS.jpg ├── acceptance └── repository-mongodb │ └── tsconfig.json └── benchmark ├── src ├── client.ts ├── context-binding │ ├── client.ts │ ├── autocannon.ts │ └── context-binding.ts ├── scenarios │ ├── index.ts │ ├── find-todos.scenario.ts │ └── create-todo.scenario.ts ├── index.ts ├── worker.ts ├── autocannon.ts ├── rest-routing │ ├── README.md │ └── routing-table.ts ├── __tests__ │ └── benchmark.integration.ts └── benchmark.ts ├── tsconfig.json └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # altlayer 2 | altlayer.io 3 | -------------------------------------------------------------------------------- /about: -------------------------------------------------------------------------------- 1 | https://testnet-faucet.altlayer.io/ 2 | -------------------------------------------------------------------------------- /F1Nvd2wakAAQMfK.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freedomers/altlayer/HEAD/F1Nvd2wakAAQMfK.jpg -------------------------------------------------------------------------------- /GAfwVKEbEAATNzS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freedomers/altlayer/HEAD/GAfwVKEbEAATNzS.jpg -------------------------------------------------------------------------------- /acceptance/repository-mongodb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@loopback/build/config/tsconfig.common.json", 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "composite": true 8 | }, 9 | "include": [ 10 | "src" 11 | ], 12 | "references": [ 13 | { 14 | "path": "../../packages/repository-tests/tsconfig.json" 15 | }, 16 | { 17 | "path": "../../packages/repository/tsconfig.json" 18 | }, 19 | { 20 | "path": "../../packages/testlab/tsconfig.json" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /benchmark/src/client.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. 2 | // Node module: @loopback/benchmark 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import {Todo} from '@loopback/example-todo'; 7 | import axios from 'axios'; 8 | 9 | export class Client { 10 | constructor(private url: string) {} 11 | 12 | createTodo(data: Partial) { 13 | return axios.post(`${this.url}/todos`, data, { 14 | responseType: 'json', 15 | }); 16 | } 17 | 18 | ping() { 19 | return axios.get(`${this.url}/todos`); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /benchmark/src/context-binding/client.ts: -------------------------------------------------------------------------------- 1 | 2 | // Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. 3 | // Node module: @loopback/benchmark 4 | // This file is licensed under the MIT License. 5 | // License text available at https://opensource.org/licenses/MIT 6 | 7 | import {Todo} from '@loopback/example-todo'; 8 | import axios from 'axios'; 9 | 10 | export class Client { 11 | constructor(private url: string) {} 12 | 13 | createTodo(data: Partial) { 14 | return axios.post(`${this.url}/todos`, data, { 15 | responseType: 'json', 16 | }); 17 | } 18 | 19 | ping() { 20 | return axios.get(`${this.url}/todos`); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@loopback/build/config/tsconfig.common.json", 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "composite": true 8 | }, 9 | "include": [ 10 | "src" 11 | ], 12 | "references": [ 13 | { 14 | "path": "../examples/todo/tsconfig.json" 15 | }, 16 | { 17 | "path": "../packages/core/tsconfig.json" 18 | }, 19 | { 20 | "path": "../packages/openapi-spec-builder/tsconfig.json" 21 | }, 22 | { 23 | "path": "../packages/rest/tsconfig.json" 24 | }, 25 | { 26 | "path": "../packages/testlab/tsconfig.json" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /benchmark/src/scenarios /index.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. and LoopBack contributors 2018. All Rights Reserved. 2 | // Node module: @loopback/benchmark 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import {ScenarioFactory} from '../benchmark'; 7 | import {CreateTodo} from './create-todo.scenario'; 8 | import {FindTodos} from './find-todos.scenario'; 9 | 10 | export interface ScenarioMap { 11 | [name: string]: ScenarioFactory; 12 | } 13 | 14 | export const scenarios: ScenarioMap = { 15 | 'find all todos': FindTodos, 16 | // IMPORTANT NOTE(bajtos) the find scenario must run before create. 17 | // Otherwise weird data is reported. I was not able to find the cause. 18 | 'create a new todo': CreateTodo, 19 | }; 20 | -------------------------------------------------------------------------------- /benchmark/src/scenarios /find-todos.scenario.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. and LoopBack contributors 2018. All Rights Reserved. 2 | // Node module: @loopback/benchmark 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import {Autocannon, EndpointStats} from '../autocannon'; 7 | import {Scenario} from '../benchmark'; 8 | import {Client} from '../client'; 9 | 10 | export class FindTodos implements Scenario { 11 | async setup(client: Client) { 12 | await client.createTodo({ 13 | title: 'first item', 14 | desc: 'Do something first', 15 | }); 16 | 17 | await client.createTodo({title: 'Second item'}); 18 | } 19 | 20 | execute(autocannon: Autocannon): Promise { 21 | return autocannon.execute('findTodos', '/todos'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /benchmark/src/scenarios /create-todo.scenario.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. and LoopBack contributors 2018. All Rights Reserved. 2 | // Node module: @loopback/benchmark 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import {Autocannon, EndpointStats} from '../autocannon'; 7 | import {Scenario} from '../benchmark'; 8 | import {Client} from '../client'; 9 | 10 | export class CreateTodo implements Scenario { 11 | async setup(client: Client) { 12 | // no-op 13 | } 14 | 15 | execute(autocannon: Autocannon): Promise { 16 | return autocannon.execute('createTodo', '/todos', { 17 | method: 'POST', 18 | body: JSON.stringify({ 19 | title: 'Finish this', 20 | desc: 'Finish running this benchmark.', 21 | }), 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /benchmark/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. 2 | // Node module: @loopback/benchmark 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import {Benchmark, Options} from './benchmark'; 7 | 8 | export {Benchmark, Options}; 9 | 10 | export async function main() { 11 | const duration = process.env.DURATION ?? '30'; 12 | const options: Options = { 13 | duration: +duration, 14 | }; 15 | const bench = new Benchmark(options); 16 | bench.logger = (title, stats) => console.log('%s:', title, stats); 17 | await bench.run(); 18 | } 19 | 20 | if (require.main === module) { 21 | main().then( 22 | success => process.exit(0), 23 | err => { 24 | console.error('Cannot run the benchmark.', err); 25 | process.exit(1); 26 | }, 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /benchmark/src/worker.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. 2 | // Node module: @loopback/benchmark 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import { 7 | Request, 8 | RestBindings, 9 | TodoListApplication, 10 | } from '@loopback/example-todo'; 11 | 12 | async function main() { 13 | const app = new TodoListApplication({ 14 | rest: { 15 | host: '127.0.0.1', 16 | port: 0, 17 | }, 18 | }); 19 | 20 | await app.boot(); 21 | 22 | // use in-memory storage without filesystem persistence 23 | app.bind('datasources.config.db').to({connector: 'memory'}); 24 | 25 | // overwrite the error logger to print all failed requests, including 4xx 26 | app.bind(RestBindings.SequenceActions.LOG_ERROR).to(logAllErrors); 27 | 28 | await app.start(); 29 | 30 | // Tell the master thread what is our URL 31 | console.log('Server listening at', app.restServer.url); 32 | if (process.send) { 33 | process.send({url: app.restServer.url}); 34 | } 35 | } 36 | 37 | main().catch(err => { 38 | console.log(err); 39 | process.exit(1); 40 | }); 41 | 42 | function logAllErrors(err: Error, statusCode: number, req: Request) { 43 | console.error( 44 | 'Request %s %s failed: %s', 45 | req.method, 46 | req.url, 47 | statusCode, 48 | err, 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /benchmark/src/autocannon.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. 2 | // Node module: @loopback/benchmark 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import assert from 'assert'; 7 | import autocannon from 'autocannon'; 8 | 9 | export interface EndpointStats { 10 | requestsPerSecond: number; 11 | latency: number; 12 | } 13 | 14 | export class Autocannon { 15 | constructor( 16 | protected url: string, 17 | protected duration: number, 18 | ) {} 19 | 20 | async execute( 21 | title: string, 22 | urlPath: string, 23 | options: Omit = {}, 24 | ): Promise { 25 | const config = { 26 | url: this.buildUrl(urlPath), 27 | duration: this.duration, 28 | title, 29 | ...options, 30 | headers: { 31 | 'content-type': 'application/json', 32 | ...options.headers, 33 | }, 34 | }; 35 | 36 | const data = await autocannon(config); 37 | assert.equal( 38 | data.non2xx, 39 | 0, 40 | 'No request should have failed with non-2xx status code.', 41 | ); 42 | const stats: EndpointStats = { 43 | requestsPerSecond: data.requests.average, 44 | latency: data.latency.average, 45 | }; 46 | return stats; 47 | } 48 | 49 | protected buildUrl(urlPath: string) { 50 | return this.url + urlPath; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /benchmark/src/context-binding/autocannon.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. 2 | // Node module: @loopback/benchmark 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import assert from 'assert'; 7 | import autocannon from 'autocannon'; 8 | 9 | export interface EndpointStats { 10 | requestsPerSecond: number; 11 | latency: number; 12 | } 13 | 14 | export class Autocannon { 15 | constructor( 16 | protected url: string, 17 | protected duration: number, 18 | ) {} 19 | 20 | async execute( 21 | title: string, 22 | urlPath: string, 23 | options: Omit = {}, 24 | ): Promise { 25 | const config = { 26 | url: this.buildUrl(urlPath), 27 | duration: this.duration, 28 | title, 29 | ...options, 30 | headers: { 31 | 'content-type': 'application/json', 32 | ...options.headers, 33 | }, 34 | }; 35 | 36 | const data = await autocannon(config); 37 | assert.equal( 38 | data.non2xx, 39 | 0, 40 | 'No request should have failed with non-2xx status code.', 41 | ); 42 | const stats: EndpointStats = { 43 | requestsPerSecond: data.requests.average, 44 | latency: data.latency.average, 45 | }; 46 | return stats; 47 | } 48 | 49 | protected buildUrl(urlPath: string) { 50 | return this.url + urlPath; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /benchmark/src/rest-routing /README.md: -------------------------------------------------------------------------------- 1 | # REST routing benchmark 2 | 3 | This directory contains a simple benchmarking to measure the performance of two 4 | router implementations for REST APIs. See 5 | https://loopback.io/doc/en/lb4/Routing-requests.html for more information. 6 | 7 | - [TrieRouter](https://github.com/loopbackio/loopback-next/tree/master/packages/rest/src/router/trie-router.ts) 8 | - [RegExpRouter](https://github.com/loopbackio/loopback-next/tree/master/packages/rest/src/router/regexp-router.ts) 9 | 10 | ## Basic use 11 | 12 | ```sh 13 | npm run -s benchmark:routing // default to 1000 routes 14 | npm run -s benchmark:routing -- 15 | ``` 16 | 17 | For example: 18 | 19 | ``` 20 | npm run -s benchmark:routing -- 10 40 160 640 2560 21 | ``` 22 | 23 | ## Base lines 24 | 25 | ``` 26 | name duration count found missed 27 | TrieRouter 0,1038244 10 8 2 28 | RegExpRouter 0,1002559 10 8 2 29 | 30 | 31 | name duration count found missed 32 | TrieRouter 0,661415 40 35 5 33 | RegExpRouter 0,5797990 40 35 5 34 | 35 | 36 | name duration count found missed 37 | TrieRouter 0,2851976 160 140 20 38 | RegExpRouter 0,27040556 160 140 20 39 | 40 | 41 | name duration count found missed 42 | TrieRouter 0,8914005 640 560 80 43 | RegExpRouter 0,330599418 640 560 80 44 | 45 | 46 | name duration count found missed 47 | TrieRouter 0,45302321 2560 2240 320 48 | RegExpRouter 5,471787942 2560 2240 320 49 | ``` 50 | -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@loopback/benchmark", 3 | "description": "Benchmarks measuring performance of our framework.", 4 | "version": "6.0.2", 5 | "keywords": [ 6 | "loopback", 7 | "performance", 8 | "benchmark" 9 | ], 10 | "private": true, 11 | "license": "MIT", 12 | "main": "dist/index.js", 13 | "types": "dist/index.d.ts", 14 | "author": "IBM Corp. and LoopBack contributors", 15 | "copyright.owner": "IBM Corp. and LoopBack contributors", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/loopbackio/loopback-next.git", 19 | "directory": "benchmark" 20 | }, 21 | "engines": { 22 | "node": "18 || 20 || 22" 23 | }, 24 | "scripts": { 25 | "build": "lb-tsc", 26 | "clean": "lb-clean dist *.tsbuildinfo", 27 | "pretest": "npm run clean && npm run build", 28 | "test": "lb-mocha \"dist/__tests__/**/*.js\"", 29 | "prestart": "npm run build", 30 | "benchmark:routing": "node ./dist/rest-routing/routing-table", 31 | "benchmark:context": "node ./dist/context-binding/context-binding", 32 | "start": "node ." 33 | }, 34 | "files": [ 35 | "README.md", 36 | "dist", 37 | "src", 38 | "!*/__tests__" 39 | ], 40 | "dependencies": { 41 | "@loopback/core": "^6.0.2", 42 | "@loopback/example-todo": "^8.0.2", 43 | "@loopback/openapi-spec-builder": "^7.0.2", 44 | "@loopback/rest": "^14.0.2", 45 | "@types/byline": "^4.2.36", 46 | "@types/debug": "^4.1.12", 47 | "@types/request-promise-native": "^1.0.21", 48 | "autocannon": "^7.15.0", 49 | "axios": "^1.7.2", 50 | "benchmark": "^2.1.4", 51 | "byline": "^5.0.0", 52 | "debug": "^4.3.5", 53 | "path-to-regexp": "^6.2.2", 54 | "tslib": "^2.6.3" 55 | }, 56 | "devDependencies": { 57 | "@loopback/build": "^11.0.2", 58 | "@loopback/testlab": "^7.0.2", 59 | "@types/autocannon": "^7.12.5", 60 | "@types/benchmark": "^2.1.5", 61 | "@types/mocha": "^10.0.6", 62 | "@types/node": "^16.18.98", 63 | "mocha": "^10.4.0", 64 | "source-map-support": "^0.5.21" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /benchmark/src/__tests__ /benchmark.integration.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved. 2 | // Node module: @loopback/benchmark 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import {expect} from '@loopback/testlab'; 7 | import {Options} from 'autocannon'; 8 | import axios, {AxiosRequestConfig, Method} from 'axios'; 9 | import {Benchmark} from '..'; 10 | import {Autocannon, EndpointStats} from '../autocannon'; 11 | 12 | const debug = require('debug')('test'); 13 | 14 | const DUMMY_STATS: EndpointStats = { 15 | latency: 1, 16 | requestsPerSecond: 1000, 17 | }; 18 | 19 | describe('Benchmark (SLOW)', function (this: Mocha.Suite) { 20 | // Unfortunately, the todo app requires one second to start (or more on CI) 21 | this.timeout(15000); 22 | it('works', async () => { 23 | const bench = new Benchmark(); 24 | bench.cannonFactory = url => new AutocannonStub(url); 25 | const result = await bench.run(); 26 | expect(result).to.eql({ 27 | 'find all todos': DUMMY_STATS, 28 | 'create a new todo': DUMMY_STATS, 29 | }); 30 | }); 31 | 32 | class AutocannonStub extends Autocannon { 33 | constructor(url: string) { 34 | super(url, 1 /* duration does not matter */); 35 | } 36 | 37 | async execute( 38 | title: string, 39 | urlPath: string, 40 | options?: Omit, 41 | ): Promise { 42 | if (!options) options = {}; 43 | 44 | const requestOptions: AxiosRequestConfig = { 45 | url: this.buildUrl(urlPath), 46 | method: (options.method ?? 'GET') as Method, 47 | responseType: 'json', 48 | data: options.body ? JSON.parse(options.body as string) : undefined, 49 | }; 50 | 51 | debug( 52 | 'Making a dummy autocannon request %s %s', 53 | requestOptions.method, 54 | requestOptions.url, 55 | ); 56 | 57 | // Verify that the server is implementing the requested URL 58 | await axios(requestOptions); 59 | 60 | return DUMMY_STATS; 61 | } 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /benchmark/src/rest-routing /routing-table.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. 2 | // Node module: @loopback/benchmark 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; 7 | import { 8 | OpenApiSpec, 9 | RegExpRouter, 10 | RestRouter, 11 | RoutingTable, 12 | TrieRouter, 13 | } from '@loopback/rest'; 14 | 15 | function runBenchmark(count = 1000) { 16 | const spec = givenNumberOfRoutes('/hello', count); 17 | 18 | const trieTest = givenRouter(new TrieRouter(), spec, count); 19 | const regexpTest = givenRouter(new RegExpRouter(), spec, count); 20 | 21 | const result1 = trieTest(); 22 | const result2 = regexpTest(); 23 | 24 | console.log( 25 | '%s %s %s %s %s', 26 | 'name'.padEnd(12), 27 | 'duration'.padStart(16), 28 | 'count'.padStart(8), 29 | 'found'.padStart(8), 30 | 'missed'.padStart(8), 31 | ); 32 | for (const r of [result1, result2]) { 33 | console.log( 34 | '%s %s %s %s %s', 35 | `${r.name}`.padEnd(12), 36 | `${r.duration}`.padStart(16), 37 | `${r.count}`.padStart(8), 38 | `${r.found}`.padStart(8), 39 | `${r.missed}`.padStart(8), 40 | ); 41 | } 42 | } 43 | 44 | function givenNumberOfRoutes(base: string, num: number) { 45 | const spec = anOpenApiSpec(); 46 | let i = 0; 47 | while (i < num) { 48 | // Add 1/4 paths with vars 49 | if (i % 4 === 0) { 50 | spec.withOperationReturningString( 51 | 'get', 52 | `${base}/group${i}/{version}`, 53 | `greet${i}`, 54 | ); 55 | } else { 56 | spec.withOperationReturningString( 57 | 'get', 58 | `${base}/group${i}/version_${i}`, 59 | `greet${i}`, 60 | ); 61 | } 62 | i++; 63 | } 64 | const result = spec.build(); 65 | result.basePath = '/my'; 66 | return result; 67 | } 68 | 69 | function givenRouter(router: RestRouter, spec: OpenApiSpec, count: number) { 70 | const name = router.constructor.name; 71 | class TestController {} 72 | 73 | return (log?: (...args: unknown[]) => void) => { 74 | log = log ?? (() => {}); 75 | log('Creating %s, %d', name, count); 76 | let start = process.hrtime(); 77 | 78 | const table = new RoutingTable(router); 79 | table.registerController(spec, TestController); 80 | router.list(); // Force sorting 81 | log('Created %s %s', name, process.hrtime(start)); 82 | 83 | log('Starting %s %d', name, count); 84 | let found = 0, 85 | missed = 0; 86 | start = process.hrtime(); 87 | for (let i = 0; i < count; i++) { 88 | let group = `group${i}`; 89 | if (i % 8 === 0) { 90 | // Make it not found 91 | group = 'groupX'; 92 | } 93 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 94 | const request: any = { 95 | method: 'get', 96 | path: `/my/hello/${group}/version_${i}`, 97 | }; 98 | 99 | try { 100 | table.find(request); 101 | found++; 102 | } catch (e) { 103 | missed++; 104 | } 105 | } 106 | log('Done %s', name); 107 | return {name, duration: process.hrtime(start), count, found, missed}; 108 | }; 109 | } 110 | 111 | let tests = process.argv.slice(2); 112 | if (!tests.length) { 113 | tests = ['1000']; 114 | } 115 | tests.forEach(n => { 116 | runBenchmark(+n); 117 | console.log('\n'); 118 | }); 119 | -------------------------------------------------------------------------------- /benchmark/src/benchmark.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. 2 | // Node module: @loopback/benchmark 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import byline from 'byline'; 7 | import {ChildProcess, fork} from 'child_process'; 8 | import {once} from 'events'; 9 | import {Readable} from 'stream'; 10 | import {Autocannon, EndpointStats} from './autocannon'; 11 | import {Client} from './client'; 12 | import {scenarios} from './scenarios'; 13 | 14 | const debug = require('debug')('loopback:benchmark'); 15 | 16 | export interface Scenario { 17 | setup(client: Client): Promise; 18 | execute(autocannon: Autocannon): Promise; 19 | } 20 | 21 | export type ScenarioFactory = new () => Scenario; 22 | 23 | export interface Options { 24 | /** 25 | * How long to run the benchmark - time in seconds. 26 | * Default: 30 seconds. 27 | */ 28 | duration: number; 29 | } 30 | 31 | export interface Result { 32 | [scenario: string]: EndpointStats; 33 | } 34 | 35 | export type AutocannonFactory = (url: string) => Autocannon; 36 | 37 | export class Benchmark { 38 | private options: Options; 39 | 40 | // Customization points 41 | public cannonFactory: AutocannonFactory; 42 | public logger: (title: string, stats: EndpointStats) => void; 43 | 44 | constructor(options?: Partial) { 45 | this.options = Object.assign( 46 | { 47 | duration: 30 /* seconds */, 48 | }, 49 | options, 50 | ); 51 | this.logger = function () {}; 52 | this.cannonFactory = url => new Autocannon(url, this.options.duration); 53 | } 54 | 55 | async run(): Promise { 56 | const result: Result = {}; 57 | for (const name in scenarios) { 58 | result[name] = await this.runScenario(name, scenarios[name]); 59 | } 60 | return result; 61 | } 62 | 63 | async runScenario( 64 | name: string, 65 | scenarioFactory: ScenarioFactory, 66 | ): Promise { 67 | debug('Starting scenario %j', name); 68 | const {worker, url} = await startWorker(); 69 | debug('Worker started - pid=%s url=%s', worker.pid, url); 70 | 71 | const client = new Client(url); 72 | const autocannon = this.cannonFactory(url); 73 | 74 | const runner = new scenarioFactory(); 75 | debug('Setting up the scenario'); 76 | await runner.setup(client); 77 | debug('Pinging the app'); 78 | await client.ping(); 79 | debug('Starting the stress test.'); 80 | const result = await runner.execute(autocannon); 81 | debug('Stats: %j', result); 82 | 83 | await closeWorker(worker); 84 | debug('Worker stopped, done.'); 85 | 86 | this.logger(name, result); 87 | 88 | return result; 89 | } 90 | } 91 | 92 | function startWorker() { 93 | return new Promise<{worker: ChildProcess; url: string}>((resolve, reject) => { 94 | const lines: string[] = []; 95 | const child = fork(require.resolve('./worker'), [], { 96 | execArgv: ['--expose-gc'], 97 | stdio: ['pipe', 'pipe', process.stderr, 'ipc'], 98 | }); 99 | 100 | child.once('error', reject); 101 | 102 | child.on('message', msg => { 103 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 104 | const url = (msg as any).url; 105 | debug('Worker setup done, url is', url); 106 | resolve({worker: child, url}); 107 | }); 108 | 109 | child.once('exit', (code, signal) => { 110 | const msg = [ 111 | `Child exited with code ${code} signal ${signal}.`, 112 | ...lines, 113 | ].join('\n'); 114 | reject(new Error(msg)); 115 | }); 116 | 117 | const reader = byline.createStream(child.stdout as Readable); 118 | reader.on('data', line => { 119 | const str = line.toString(); 120 | debug('[worker] %s', str); 121 | lines.push(str); 122 | }); 123 | }); 124 | } 125 | 126 | async function closeWorker(worker: ChildProcess) { 127 | worker.kill(); 128 | await once(worker, 'close'); 129 | } 130 | -------------------------------------------------------------------------------- /benchmark/src/context-binding/context-binding.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. and LoopBack contributors 2020. All Rights Reserved. 2 | // Node module: @loopback/benchmark 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import {Context, inject, Provider, ValueFactory} from '@loopback/core'; 7 | import Benchmark from 'benchmark'; 8 | 9 | /** 10 | * Option 1 - use a sync factory function 11 | */ 12 | const factory: ValueFactory = ({context}) => { 13 | const user = context.getSync('user'); 14 | return `Hello, ${user}`; 15 | }; 16 | 17 | /** 18 | * Option 2 - use an async factory function 19 | */ 20 | const asyncFactory: ValueFactory = async ({context}) => { 21 | const user = await context.get('user'); 22 | return `Hello, ${user}`; 23 | }; 24 | 25 | /** 26 | * Option 3 - use a value factory provider class with sync static value() method 27 | * parameter injection 28 | */ 29 | class StaticGreetingProvider { 30 | static value(@inject('user') user: string) { 31 | return `Hello, ${user}`; 32 | } 33 | } 34 | 35 | /** 36 | * Option 4 - use a value factory provider class with async static value() method 37 | * parameter injection 38 | */ 39 | class AsyncStaticGreetingProvider { 40 | static value(@inject('user') user: string) { 41 | return Promise.resolve(`Hello, ${user}`); 42 | } 43 | } 44 | 45 | /** 46 | * Option 5 - use a regular provider class with sync value() 47 | */ 48 | class GreetingProvider implements Provider { 49 | @inject('user') 50 | private user: string; 51 | 52 | value() { 53 | return `Hello, ${this.user}`; 54 | } 55 | } 56 | 57 | /** 58 | * Option 6 - use a regular provider class with async value() 59 | */ 60 | class AsyncGreetingProvider implements Provider { 61 | @inject('user') 62 | private user: string; 63 | 64 | value() { 65 | return Promise.resolve(`Hello, ${this.user}`); 66 | } 67 | } 68 | 69 | setupContextBindings(); 70 | 71 | function setupContextBindings() { 72 | const ctx = new Context(); 73 | ctx.bind('user').to('John'); 74 | ctx.bind('greeting.syncFactory').toDynamicValue(factory); 75 | ctx.bind('greeting.asyncFactory').toDynamicValue(asyncFactory); 76 | ctx 77 | .bind('greeting.syncStaticProvider') 78 | .toDynamicValue(StaticGreetingProvider); 79 | ctx 80 | .bind('greeting.asyncStaticProvider') 81 | .toDynamicValue(AsyncStaticGreetingProvider); 82 | ctx.bind('greeting.syncProvider').toProvider(GreetingProvider); 83 | ctx.bind('greeting.asyncProvider').toProvider(AsyncGreetingProvider); 84 | return ctx; 85 | } 86 | 87 | function runBenchmark(ctx: Context) { 88 | const options: Benchmark.Options = { 89 | initCount: 1000, 90 | onComplete: (e: Benchmark.Event) => { 91 | const benchmark = e.target; 92 | console.log('%s %d', benchmark, benchmark.count); 93 | }, 94 | }; 95 | const suite = new Benchmark.Suite('context-bindings'); 96 | suite 97 | .add( 98 | 'factory - getSync', 99 | () => ctx.getSync('greeting.syncFactory'), 100 | options, 101 | ) 102 | .add('factory - get', () => ctx.get('greeting.syncFactory'), options) 103 | .add('asyncFactory - get', () => ctx.get('greeting.asyncFactory'), options) 104 | .add( 105 | 'staticProvider - getSync', 106 | () => ctx.getSync('greeting.syncStaticProvider'), 107 | options, 108 | ) 109 | .add( 110 | 'staticProvider - get', 111 | () => ctx.get('greeting.syncStaticProvider'), 112 | options, 113 | ) 114 | .add( 115 | 'asyncStaticProvider - get', 116 | () => ctx.get('greeting.asyncStaticProvider'), 117 | options, 118 | ) 119 | .add( 120 | 'provider - getSync', 121 | () => ctx.getSync('greeting.syncProvider'), 122 | options, 123 | ) 124 | .add('provider - get', () => ctx.get('greeting.syncProvider'), options) 125 | .add( 126 | 'asyncProvider - get', 127 | () => ctx.get('greeting.asyncProvider'), 128 | options, 129 | ) 130 | .run({async: true}); 131 | } 132 | 133 | if (require.main === module) { 134 | const ctx = setupContextBindings(); 135 | runBenchmark(ctx); 136 | } 137 | --------------------------------------------------------------------------------