diff --git a/.gitignore b/.gitignore index 00cbbdf..9f8641d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Transpiled files +lib/loopback-next.* + # Logs logs *.log diff --git a/README.md b/README.md index 99589f5..7d663e3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,44 @@ # async-frameworks A benchmark comparing performance of async/await in different HTTP frameworks - - Express@4.16 - - Koa@2.3 + koa-router@7.2 - - Fastify@0.30 - - Hapi@17.0-rc + - **Express** v4.16 + - **Koa** v2.5 + koa-router v7.4 + - **Fastify** v1.8 + - **Hapi** v17.5 + - **LoopBack** v3.21 + - **LoopBack next**: core v0.11; repository v0.14; rest v0.19 + +## Results + +MacBookPro Mid 2015 +Processor: 2.5 GHz Intel Core i7 +Memory: 16 GB 1600 MHz DDR3 + +### Requests per seconds + +framework|rps +-|-: +hapi | 6029 +fastify | 7926 +koa | 7305 +express | 5778 +loopback | 3072 +loopback-next | 2778 + +### Latency + +_Time to handle a request in milliseconds._ + +framework|latency +-|-: +hapi | 1.14 +fastify | 0.93 +koa | 1.02 +express | 1.2 +loopback | 2.69 +loopback-next | 3.16 + +Async/await is not the bottleneck! ## Usage @@ -42,16 +76,3 @@ Run the benchmark. $ npm start ``` -## Results - -MacBookPro Mid 2015 -Processor: 2.5 GHz Intel Core i7 -Memory: 16 GB 1600 MHz DDR3 - -Average requests per seconds: -- hapi: 6188.9 -- fastify: 7800 -- koa: 7334.1 -- express: 6147.77 - -Async/await is not the bottleneck! diff --git a/bench.js b/bench.js index 029ec39..4de4039 100644 --- a/bench.js +++ b/bench.js @@ -6,8 +6,9 @@ const autocannon = require('autocannon'); const apps = require('./lib/index'); main().then( - () => console.log('done'), - err => console.error(err)); + () => { console.log('done'); process.exit(0); }, + err => { console.error(err); process.exit(1); }, +); async function main() { const reqsPerSec = {}; diff --git a/lib/db.js b/lib/db.js index 7022c8f..70d4621 100644 --- a/lib/db.js +++ b/lib/db.js @@ -6,6 +6,8 @@ const url = 'mongodb://localhost:27017/async-benchmark'; let client, products; +exports.url = url; + exports.connect = async function connect() { client = await MongoClient.connect(url, {useNewUrlParser: true}); products = client.db().collection('products'); diff --git a/lib/express.js b/lib/express.js index 350e67a..2b6bfa4 100644 --- a/lib/express.js +++ b/lib/express.js @@ -12,7 +12,7 @@ app.get('/products/:ean', (req, res, next) => { }); }); -module.exports = promisify(function start(cb) { +exports.start = promisify(function start(cb) { app.listen(0, function() { cb(null, this.address().port); }); }); diff --git a/lib/fastify.js b/lib/fastify.js index a3979b2..6d6b341 100644 --- a/lib/fastify.js +++ b/lib/fastify.js @@ -15,7 +15,7 @@ app.route({ } }); -module.exports = promisify(function start(cb) { +exports.start = promisify(function start(cb) { app.listen(0, function() { cb(null, app.server.address().port); }); }); diff --git a/lib/hapi.js b/lib/hapi.js index 7d6bdbb..247ae90 100644 --- a/lib/hapi.js +++ b/lib/hapi.js @@ -16,7 +16,7 @@ server.route({ } }); -module.exports = async function start() { +exports.start = async function start() { await server.start(); return server.info.port; }; diff --git a/lib/index.js b/lib/index.js index fcf6287..0fc89f0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,4 +3,6 @@ module.exports = { fastify: require('./fastify'), koa: require('./koa'), express: require('./express'), + loopback: require('./loopback'), + 'loopback-next': require('./loopback-next'), }; diff --git a/lib/koa.js b/lib/koa.js index c90067d..418e9c0 100644 --- a/lib/koa.js +++ b/lib/koa.js @@ -17,7 +17,7 @@ app .use(router.routes()) .use(router.allowedMethods()); -module.exports = promisify(function start(cb) { +exports.start = promisify(function start(cb) { app.listen(0, function() { cb(null, this.address().port); }); }); diff --git a/lib/loopback.js b/lib/loopback.js new file mode 100644 index 0000000..542430c --- /dev/null +++ b/lib/loopback.js @@ -0,0 +1,47 @@ +'use strict'; + +const loopback = require('loopback'); +const promisify = require('util').promisify; + +const app = loopback(); +app.use(loopback.rest()); + +exports.start = promisify(function start(cb) { + app.listen(0, function() { cb(null, this.address().port); }); +}); + +/** Setup Product model and MongoDB connection **/ + +const Product = app.registry.createModel({ + name: 'Product', + properties: { + _id: {type: String, id: true}, + ean: {type: Number, required: true}, + name: {type: String, required: true}, + }, + mongodb: { + collection: 'products', + }, +}); + +app.dataSource('db', { + connector: 'mongodb', + useNewUrlParser: true, + url: require('./db').url, +}); + +app.model(Product, {dataSource: 'db'}); + +/** Setup REST API **/ + +Product.findByEan = function(ean) { + return this.find({where: {ean}}); +}; + +Product.remoteMethod('findByEan', { + accepts: {arg: 'ean', type: 'number', required: true}, + returns: {arg: 'result', type: Product, root: true}, + http: {verb: 'get', path: '/:ean'} +}); + +Product.disableRemoteMethodByName('findById'); diff --git a/lib/worker.js b/lib/worker.js index 188fd3d..514b2a8 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -1,7 +1,7 @@ 'use strict'; const framework = process.argv[2]; -const start = require(`./${framework}`); +const start = require(`./${framework}`).start; const db = require('./db'); db.connect() diff --git a/package.json b/package.json index 88ceb37..7777087 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,10 @@ "version": "1.0.0", "description": "A benchmark comparing performance of async/await in different HTTP frameworks", "scripts": { + "prestart": "tsc", "start": "node --expose-gc ./bench.js", - "test": "mocha --exit test.js" + "pretest": "tsc", + "test": "mocha" }, "repository": { "type": "git", @@ -17,6 +19,9 @@ }, "homepage": "https://github.com/bajtos/async-frameworks#readme", "dependencies": { + "@loopback/core": "^0.11.2", + "@loopback/repository": "^0.14.2", + "@loopback/rest": "^0.19.2", "autocannon": "^2.4.1", "byline": "^5.0.0", "express": "^4.16.2", @@ -25,8 +30,11 @@ "hapi": "^17.0.0-rc9", "koa": "^2.3.0", "koa-router": "^7.2.1", + "loopback": "^3.21.0", + "loopback-connector-mongodb": "^3.5.0", "mocha": "^5.2.0", "mongodb": "^3.1.1", - "supertest": "^3.0.0" + "supertest": "^3.0.0", + "typescript": "^3.0.1" } } diff --git a/src/loopback-next.ts b/src/loopback-next.ts new file mode 100644 index 0000000..5e82e18 --- /dev/null +++ b/src/loopback-next.ts @@ -0,0 +1,87 @@ +import { get, param, RestApplication, RestBindings } from '@loopback/rest'; +import { inject } from '../node_modules/@loopback/core'; +import { DefaultCrudRepository, Entity, juggler, model, property, RepositoryMixin, repository } from '../node_modules/@loopback/repository'; + +const dbConfig = { + connector: 'mongodb', + url: require('../lib/db').url, +}; + +class DbDataSource extends juggler.DataSource { + static dataSourceName = 'db'; + + constructor( + @inject('datasources.config.db', { optional: true }) + dsConfig: object = dbConfig, + ) { + super(dsConfig); + } +} + +@model({ + settings: { + mongodb: { + collection: 'products', + }, + } +}) +class Product extends Entity { + @property({ id: true }) + _id: string + + @property({ required: true }) + ean: number; + + @property({ required: true }) + name: string; + + getId() { + return this._id; + } +} + +class ProductRepository extends DefaultCrudRepository< + Product, + typeof Product.prototype._id + > { + constructor( + @inject('datasources.db') protected datasource: juggler.DataSource, + ) { + super(Product, datasource); + } +} + +class ProductController { + constructor( + @repository(ProductRepository) + protected repo: ProductRepository, + ) { } + + @get('/products/{ean}') + findByEan( + @param.path.number('ean') + ean: number + ) { + return this.repo.find({where: {ean}}); + } +} + +class BenchApp extends RepositoryMixin(RestApplication) { + constructor() { + super({ + rest: { port: 0 }, + }); + + this.bind('datasources.config.db').to(dbConfig); + this.dataSource(DbDataSource); + this.repository(ProductRepository); + this.controller(ProductController); + } +} + +const app = new BenchApp(); + +export async function start() { + await app.start(); + return app.restServer.get(RestBindings.PORT); +} diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..e306495 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +--exit diff --git a/test.js b/test/smoke.test.js similarity index 80% rename from test.js rename to test/smoke.test.js index b03818a..f29e199 100644 --- a/test.js +++ b/test/smoke.test.js @@ -1,9 +1,9 @@ 'use strict'; const supertest = require('supertest'); -const db = require('./lib/db'); +const db = require('../lib/db'); -const apps = require('./lib/index'); +const apps = require('../lib/index'); before(db.connect); after(db.close); @@ -11,7 +11,7 @@ after(db.close); Object.keys(apps).forEach(name => { describe(`${name} app`, () => { it('works', async () => { - const port = await apps[name](); + const port = await apps[name].start(); await verify(port); }); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e878e91 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "compilerOptions": { + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "noImplicitAny": true, + "strictNullChecks": true, + + "outDir": "lib", + + "lib": ["es2018", "dom"], + "module": "commonjs", + "moduleResolution": "node", + "target": "es2017", + "sourceMap": true, + "declaration": true + + }, + "include": [ + "src/loopback-next.ts" + ] +}