diff --git a/.travis.yml b/.travis.yml index 1f3333f..9d9ea1c 100755 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: node_js node_js: - - "4" - - "5" + - "8" - "stable" sudo: required diff --git a/lib/async-context.js b/lib/async-context.js new file mode 100644 index 0000000..60d0ef2 --- /dev/null +++ b/lib/async-context.js @@ -0,0 +1,28 @@ +'use strict'; + +const asyncHooks = require('async_hooks'); + +class AsyncContext { + constructor() { + this.map = new Map(); + asyncHooks.createHook({ + init: (id, _type, triggerId) => { + if (this.map.has(triggerId)) + this.map.set(id, this.map.get(triggerId)); + }, + destroy: (id) => this.map.delete(id) + }).enable(); + } + + get() { + const id = asyncHooks.executionAsyncId(); + if (this.map.has(id)) + return this.map.get(id); + } + + set(val) { + this.map.set(asyncHooks.executionAsyncId(), val); + } +} + +module.exports = new AsyncContext(); diff --git a/lib/middlewares/express.js b/lib/middlewares/express.js index a99046d..878174d 100755 --- a/lib/middlewares/express.js +++ b/lib/middlewares/express.js @@ -1,16 +1,21 @@ 'use strict'; +const asyncContext = require('../async-context'); + module.exports = { buildMiddleware: function(provider) { return function(req, res, next) { provider.handler(req, res, next); }; }, - mainMiddleware: function(enable, authorize, handleRequest) { + mainMiddleware: function(enable, authorize, handleRequest, cls) { return function(req, res, next) { handleRequest(enable, authorize, req, res).then((handled) => { res.locals.miniprofiler = req.miniprofiler; + asyncContext.set(req.miniprofiler); + Object.defineProperty(req, 'miniprofiler', { get: () => asyncContext.get() }); + var render = res.render; res.render = function() { var renderArguments = arguments; @@ -24,4 +29,4 @@ module.exports = { }).catch(next); }; } -}; \ No newline at end of file +}; diff --git a/lib/middlewares/hapi.js b/lib/middlewares/hapi.js index 4090808..79797e5 100755 --- a/lib/middlewares/hapi.js +++ b/lib/middlewares/hapi.js @@ -1,5 +1,7 @@ 'use strict'; +const asyncContext = require('../async-context'); + module.exports = { buildMiddleware: function(provider) { var plugin = { @@ -25,7 +27,9 @@ module.exports = { register: (server, options, next) => { server.ext('onRequest', function(request, reply) { handleRequest(enable, authorize, request.raw.req, request.raw.res).then((handled) => { - request.app.miniprofiler = request.raw.req.miniprofiler; + asyncContext.set(request.raw.req.miniprofiler); + Object.defineProperty(request.app, 'miniprofiler', { get: () => asyncContext.get() }); + Object.defineProperty(request.raw.req, 'miniprofiler', { get: () => asyncContext.get() }); if (!handled) reply.continue(); diff --git a/lib/middlewares/koa.js b/lib/middlewares/koa.js index 74c0ad4..44269bd 100755 --- a/lib/middlewares/koa.js +++ b/lib/middlewares/koa.js @@ -1,5 +1,7 @@ 'use strict'; +const asyncContext = require('../async-context'); + module.exports = { buildMiddleware: function(provider) { return function *(next) { @@ -12,7 +14,10 @@ module.exports = { mainMiddleware: function(enable, authorize, handleRequest) { return function *(next) { var handled = yield handleRequest(enable, authorize, this.req, this.res); - this.state.miniprofiler = this.req.miniprofiler; + + asyncContext.set(this.req.miniprofiler); + Object.defineProperty(this.state, 'miniprofiler', { get: () => asyncContext.get() }); + Object.defineProperty(this.req, 'miniprofiler', { get: () => asyncContext.get() }); if (this.render) { var render = this.render; diff --git a/lib/miniprofiler.js b/lib/miniprofiler.js index 086be80..3f2a2f4 100755 --- a/lib/miniprofiler.js +++ b/lib/miniprofiler.js @@ -74,12 +74,12 @@ function handleRequest(enable, authorize, req, res) { } if (!requestPath.startsWith(resourcePath)) { - var id = startProfiling(req, enabled, authorized); + var extension = startProfiling(req, enabled, authorized); if (enabled) { res.on('finish', () => { - stopProfiling(req); + stopProfiling(extension, req); }); - res.setHeader('X-MiniProfiler-Ids', `["${id}"]`); + res.setHeader('X-MiniProfiler-Ids', `["${extension.id}"]`); } return resolve(false); } @@ -279,21 +279,19 @@ function include(id) { }; request.miniprofiler = currentRequestExtension; - return currentRequestExtension.id; + + return currentRequestExtension; } /* * Stops profiling the given request. */ - function stopProfiling(request){ - var extension = request.miniprofiler; + function stopProfiling(extension, request){ var time = process.hrtime(); extension.stopTime = time; extension.stepGraph.stopTime = time; - delete request.miniprofiler; - var json = describePerformance(extension, request); storage.set(extension.id, JSON.stringify(json)); } diff --git a/package.json b/package.json index 2b7a70d..a443956 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "miniprofiler", - "version": "1.2.3", + "version": "2.0.0", "description": "A simple but effective mini-profiler.", "main": "lib/miniprofiler.js", "scripts": { @@ -46,6 +46,7 @@ "koa": "^1.2.1", "koa-route": "^2.4.2", "koa-views": "^4.1.0", + "debug": "^2.6.1", "mocha": "^2.5.3", "pug": "^2.0.0-beta2", "redis": "^2.6.1", diff --git a/tests/concurrent-async-test.js b/tests/concurrent-async-test.js new file mode 100644 index 0000000..4ecaa52 --- /dev/null +++ b/tests/concurrent-async-test.js @@ -0,0 +1,37 @@ +'use strict'; + +var expect = require('chai').expect; + +module.exports = function(server) { + describe('Concurrent Async Requests', function() { + before(server.setUp.bind(null, 'async')); + after(server.tearDown); + + it('Each profile runs on its own context', function(done) { + let countDone = 0; + const partialDone = () => { if (++countDone === 2) done(); }; + + server.get('/', (err, response) => { + var ids = JSON.parse(response.headers['x-miniprofiler-ids']); + expect(ids).to.have.lengthOf(1); + + server.post('/mini-profiler-resources/results/', { id: ids[0], popup: 1 }, (err, response, body) => { + var result = JSON.parse(body); + expect(result.Root.CustomTimings.async).to.have.lengthOf(2); + partialDone(); + }); + }); + + server.get('/?once=true', (err, response) => { + var ids = JSON.parse(response.headers['x-miniprofiler-ids']); + expect(ids).to.have.lengthOf(1); + + server.post('/mini-profiler-resources/results/', { id: ids[0], popup: 1 }, (err, response, body) => { + var result = JSON.parse(body); + expect(result.Root.CustomTimings.async).to.have.lengthOf(1); + partialDone(); + }); + }); + }); + }); +}; diff --git a/tests/servers/async-provider.js b/tests/servers/async-provider.js new file mode 100644 index 0000000..1d6138f --- /dev/null +++ b/tests/servers/async-provider.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = function(obj) { + return { + name: 'dummy-async', + handler: function(req, res, next) { + obj.asyncFn = function() { + const timing = req.miniprofiler.startTimeQuery('async', 'dummy call'); + + return new Promise(resolve => { + setTimeout(() => { + req.miniprofiler.stopTimeQuery(timing); + resolve(); + }, 25); + }); + }; + + next(); + } + }; +}; diff --git a/tests/servers/dummy-module.js b/tests/servers/dummy-module.js new file mode 100644 index 0000000..df48e6c --- /dev/null +++ b/tests/servers/dummy-module.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + asyncFn: () => Promise.resolve() +}; diff --git a/tests/servers/express/async.js b/tests/servers/express/async.js new file mode 100644 index 0000000..39fd990 --- /dev/null +++ b/tests/servers/express/async.js @@ -0,0 +1,19 @@ +'use strict'; + +var miniprofiler = require('../../../lib/miniprofiler.js'); +var dummyModule = require('../dummy-module'); +var express = require('express'); + +var app = express(); + +app.use(miniprofiler.express()); +app.use(miniprofiler.express.for(require('../async-provider.js')(dummyModule))); + +app.get('/', (req, res) => { + dummyModule.asyncFn().then(() => { + Promise.resolve(req.query.once ? undefined : dummyModule.asyncFn()) + .then(() => res.send(res.locals.miniprofiler.include())); + }); +}); + +module.exports = app; diff --git a/tests/servers/hapi/async.js b/tests/servers/hapi/async.js new file mode 100644 index 0000000..287df6a --- /dev/null +++ b/tests/servers/hapi/async.js @@ -0,0 +1,29 @@ +'use strict'; + +var miniprofiler = require('../../../lib/miniprofiler.js'); +var dummyModule = require('../dummy-module'); +const Hapi = require('hapi'); + +const server = new Hapi.Server(); +server.connection({ port: 8083 }); + +server.register(miniprofiler.hapi(), (err) => { + if (err) throw err; +}); + +server.register(miniprofiler.hapi.for(require('../async-provider.js')(dummyModule)), (err) => { + if (err) throw err; +}); + +server.route({ + method: 'GET', + path:'/', + handler: function(request, reply) { + dummyModule.asyncFn().then(() => { + Promise.resolve(request.query.once ? undefined : dummyModule.asyncFn()) + .then(() => reply(request.app.miniprofiler.include())); + }); + } +}); + +module.exports = server; diff --git a/tests/servers/koa/async.js b/tests/servers/koa/async.js new file mode 100644 index 0000000..9ee045c --- /dev/null +++ b/tests/servers/koa/async.js @@ -0,0 +1,19 @@ +'use strict'; + +var miniprofiler = require('../../../lib/miniprofiler.js'); +var dummyModule = require('../dummy-module'); +var koa = require('koa'); +var route = require('koa-route'); +var app = koa(); + +app.use(miniprofiler.koa()); +app.use(miniprofiler.koa.for(require('../async-provider.js')(dummyModule))); + +app.use(route.get('/', function *(){ + yield dummyModule.asyncFn().then(() => { + return Promise.resolve(this.query.once ? undefined : dummyModule.asyncFn()) + .then(() => { this.body = this.state.miniprofiler.include(); }); + }); +})); + +module.exports = app;