Skip to content

Commit

Permalink
Improve performance of running hooks (fastify#714)
Browse files Browse the repository at this point in the history
* Improve performance of running hooks

Replace fast-iterator with a custom hook runner inside this repo.
This improves performance when there are asynchronous hooks by 17-20%.

* Move benchmark into new "benchmarks" folder

* Add hooks benchmark for async-await

* Address comments
  • Loading branch information
nwoltman authored and delvedor committed Jan 29, 2018
1 parent 00aa9f7 commit 3cfa056
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 25 deletions.
17 changes: 8 additions & 9 deletions docs/Hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,29 +141,28 @@ fastify.addHook('onSend', (request, reply, payload, next) => {
Note: If you change the payload, you may only change it to a `string`, a `Buffer`, a `stream`, or `null`.

### Respond to a request from an hook
If need you can respond to a request before you reach the route handler, an example could be an authentication hook. If you are using `onRequest` or a middleware just use `res.end`, if you are using the `preHandler` hook use `reply.send`. Remember to always call `next` if you are using the standard hook api otherwise the memory consumption of the server could potentially increase and it will prevent one of our optimizations, if you are working with *async* hooks it will be done automatically by Fastify.
### Respond to a request from a hook
If needed, you can respond to a request before you reach the route handler. An example could be an authentication hook. If you are using `onRequest` or a middleware, use `res.end`. If you are using the `preHandler` hook, use `reply.send`.

```js
// standard api
fastify.addHook('preHandler', (request, reply, next) => {
reply.send({ hello: 'world' })
next()
fastify.addHook('onRequest', (req, res, next) => {
res.end('early response')
})

// async api
// Works with async functions too
fastify.addHook('preHandler', async (request, reply) => {
reply.send({ hello: 'world' })
})
```

If you want to respond with a stream, you should make sure that you don't call `next` before the response has finished.
If you want to respond with a stream, you should avoid using an `async` function for the hook. If you must use an `async` function, your code will need to follow the pattern in [test/hooks-async.js](https://github.com/fastify/fastify/blob/94ea67ef2d8dce8a955d510cd9081aabd036fa85/test/hooks-async.js#L269-L275).

```js
const pump = require('pump')

fastify.addHook('onRequest', (req, res, next) => {
const stream = fs.createReadStream('some-file', 'utf8')
pump(stream, res, next)
pump(stream, res, err => req.log.error(err))
})
```

Expand Down
45 changes: 45 additions & 0 deletions examples/hooks-benchmark-async-await.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict'

const fastify = require('../fastify')()

const opts = {
schema: {
response: {
200: {
type: 'object',
properties: {
hello: {
type: 'string'
}
}
}
}
}
}

function promiseFunction (resolve) {
setImmediate(resolve)
}

async function asyncHook () {
await new Promise(promiseFunction)
}

fastify
.addHook('onRequest', asyncHook)
.addHook('onRequest', asyncHook)
.addHook('preHandler', asyncHook)
.addHook('preHandler', asyncHook)
.addHook('preHandler', asyncHook)
.addHook('onSend', asyncHook)

fastify.get('/', opts, function (request, reply) {
reply.send({ hello: 'world' })
})

fastify.listen(3000, function (err) {
if (err) {
throw err
}
console.log(`server listening on ${fastify.server.address().port}`)
})
53 changes: 53 additions & 0 deletions examples/hooks-benchmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict'

const fastify = require('../fastify')()

const opts = {
schema: {
response: {
200: {
type: 'object',
properties: {
hello: {
type: 'string'
}
}
}
}
}
}

fastify
.addHook('onRequest', function (req, res, next) {
next()
})
.addHook('onRequest', function (req, res, next) {
next()
})

fastify
.addHook('preHandler', function (request, reply, next) {
next()
})
.addHook('preHandler', function (request, reply, next) {
setImmediate(next)
})
.addHook('preHandler', function (request, reply, next) {
next()
})

fastify
.addHook('onSend', function (request, reply, payload, next) {
next()
})

fastify.get('/', opts, function (request, reply) {
reply.send({ hello: 'world' })
})

fastify.listen(3000, function (err) {
if (err) {
throw err
}
console.log(`server listening on ${fastify.server.address().port}`)
})
24 changes: 12 additions & 12 deletions fastify.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const avvio = require('avvio')
const http = require('http')
const https = require('https')
const Middie = require('middie')
const fastIterator = require('fast-iterator')
const hookRunner = require('./lib/hookRunner')
const lightMyRequest = require('light-my-request')
const abstractLogging = require('abstract-logging')

Expand Down Expand Up @@ -285,8 +285,8 @@ function build (options) {
this.context = context
}

function hookIterator (fn, state, next, release) {
if (state.res.finished === true) return release()
function hookIterator (fn, state, next) {
if (state.res.finished === true) return undefined
return fn(state.req, state.res, next)
}

Expand Down Expand Up @@ -484,10 +484,10 @@ function build (options) {
const onSend = _fastify._hooks.onSend
const preHandler = _fastify._hooks.preHandler.concat(opts.beforeHandler || [])

context.onRequest = onRequest.length ? fastIterator(onRequest, _fastify) : null
context.onResponse = onResponse.length ? fastIterator(onResponse, _fastify) : null
context.onSend = onSend.length ? fastIterator(onSend, _fastify) : null
context.preHandler = preHandler.length ? fastIterator(preHandler, _fastify) : null
context.onRequest = onRequest.length ? hookRunner(onRequest, _fastify) : null
context.onResponse = onResponse.length ? hookRunner(onResponse, _fastify) : null
context.onSend = onSend.length ? hookRunner(onSend, _fastify) : null
context.preHandler = preHandler.length ? hookRunner(preHandler, _fastify) : null

try {
router.on(opts.method, url, routeHandler, context)
Expand Down Expand Up @@ -615,7 +615,7 @@ function build (options) {
req.log.warn('the default handler for 404 did not catch this, this is likely a fastify bug, please report it')
req.log.warn(fourOhFour.prettyPrint())
const request = new Request(null, req, null, req.headers, req.log)
const reply = new Reply(res, { onSend: fastIterator([], null) }, request)
const reply = new Reply(res, { onSend: hookRunner([], null) }, request)
reply.code(404).send(new Error('Not found'))
}

Expand Down Expand Up @@ -661,10 +661,10 @@ function build (options) {
const onSend = this._hooks.onSend
const onResponse = this._hooks.onResponse

context.onRequest = onRequest.length ? fastIterator(onRequest, this) : null
context.preHandler = preHandler.length ? fastIterator(preHandler, this) : null
context.onSend = onSend.length ? fastIterator(onSend, this) : null
context.onResponse = onResponse.length ? fastIterator(onResponse, this) : null
context.onRequest = onRequest.length ? hookRunner(onRequest, this) : null
context.preHandler = preHandler.length ? hookRunner(preHandler, this) : null
context.onSend = onSend.length ? hookRunner(onSend, this) : null
context.onResponse = onResponse.length ? hookRunner(onResponse, this) : null

if (this._404Context !== null) {
Object.assign(this._404Context, context) // Replace the default 404 handler
Expand Down
4 changes: 2 additions & 2 deletions lib/handleRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ function handler (reply) {
}
}

function hookIterator (fn, reply, next, release) {
if (reply.res.finished === true) return release()
function hookIterator (fn, reply, next) {
if (reply.res.finished === true) return undefined
return fn(reply.request, reply, next)
}

Expand Down
42 changes: 42 additions & 0 deletions lib/hookRunner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict'

function hookRunner (functions, context) {
functions = functions.map(fn => fn.bind(context))

return function runHooks (runner, state, cb) {
var i = 0

function next (err, value) {
if (err) {
cb(err, state)
return
}

if (value !== undefined) {
state = value
}

if (i === functions.length) {
cb(null, state)
return
}

const result = runner(functions[i++], state, next)
if (result && typeof result.then === 'function') {
result.then(handleResolve, handleReject)
}
}

function handleResolve (value) {
next(null, value)
}

function handleReject (err) {
cb(err, state)
}

next()
}
}

module.exports = hookRunner
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@
"abstract-logging": "^1.0.0",
"ajv": "^6.1.0",
"avvio": "^5.0.1",
"fast-iterator": "^0.3.0",
"fast-json-stringify": "^0.17.0",
"find-my-way": "^1.10.0",
"flatstr": "^1.0.5",
Expand Down
2 changes: 1 addition & 1 deletion test/internals/handleRequest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Request = require('../../lib/request')
const Reply = require('../../lib/reply')
const buildSchema = require('../../lib/validation').build
const Hooks = require('../../lib/hooks')
const runHooks = require('fast-iterator')
const runHooks = require('../../lib/hookRunner')
const sget = require('simple-get').concat

const Ajv = require('ajv')
Expand Down
Loading

0 comments on commit 3cfa056

Please sign in to comment.