Skip to content

Commit

Permalink
Merge pull request #13 from fyrejet/dev
Browse files Browse the repository at this point in the history
Fyrejet 4.0
  • Loading branch information
schamberg97 committed Sep 28, 2021
2 parents 79f9080 + e8c64c3 commit 01dffb2
Show file tree
Hide file tree
Showing 29 changed files with 1,299 additions and 1,696 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ package-lock.json

performance/test.js

isolate*.log
flamegraph.html

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

Expand Down
87 changes: 38 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@


# Fyrejet 3
# Fyrejet 4

<img src="./fyre.png" alt="logo" height="150" width="150" />

Expand All @@ -10,15 +10,14 @@ Fyrejet is a web-framework that is designed for speed and ease-of-use. After wor

Unfortunately, that comes at a cost. While Express brings the speed of development, its performance is just okay-ish. Other frameworks either provide different APIs, are incompatible with Express middlewares or provide less functionality. For instance, Restana, a great API-oriented framework by jkybernees provides incredible performance, but only a subset of Express APIs, making it not suitable as an Express replacement. Moreover, Express relies on `Object.setPrototypeOf` in request handling, which is inherently slow (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf) and whose performance has drastically decreased after Node.js 12.18.1 was released.

Fyrejet does not strive to be the fastest framework. However, Fyrejet seeks to be faster than Express, while providing very Express-like API. In fact, Fyrejet uses slightly modified<sup>[1](#footnote1)</sup> Express automated unit tests to verify the codebase. Moreover, Fyrejet offers you the ability to use Express APIs with uWebSockets.js (not production ready yet).
Fyrejet previously did not strive to be the fastest framework, seeking instead to only provide Express-like API and better performance. However, Fyrejet is now aimed at becoming one of the fastest pure Node.js full-featured frameworks, without using native code. In fact, Fyrejet uses slightly modified<sup>[1](#footnote1)</sup> Express tests to verify the codebase. Moreover, Fyrejet offers you the ability to use Express APIs with uWebSockets.js (not production ready yet), if you decide that native Node.js HTTP server is not fast enough for your needs.

Starting with Fyrejet 2.2.x, Fyrejet is only compatible with Node.js 12 and higher.



<a name="footnote1">[1]</a>:

* `50` tests removed, because they are arguably irrelevant (`test/Route.js` and `test/Router.js`)
* `~6` tests removed in Fyrejet 4 that rely on Prototype modification that is not allowed in Fyrejet 4.
* `~6` tests modified to test a replacement API instead (`req.currentUrl`)
* Some tests have been removed in 3.x (some `res.send`, `res.json` and `res.jsonp` tests, because they test removed functionality, that has long been deprecated in Express - namely, ability to set status through these methods)
* `req.acceptsEncoding()`, `req.acceptsCharset()` and `req.acceptsLanguage()` and `req.hose()` tests are fully removed, since they have been long deprecated.
Expand Down Expand Up @@ -49,6 +48,18 @@ Fyrejet is shared with the community under MIT License.



## Breaking changes from `3.x` to `4.x`

* It is no longer possible to modify Fyrejet's request and response prototypes. If you need to add new request or response functionality, consider adding new functions or objects to `req` and `res` objects via middleware. This change is made to greatly improve performance

* `res.sendfile` is deleted. Should not significantly impact anyone, as `res.sendfile` is deprecated for a long time in Express 4.

* Major internal routing & init middleware changes to optimize performance. Behaviour is the same, but over 50% of the code is rewritten or reorganised

* `app.settings` implementation now relies on proxy object



## Breaking changes from `2.x` to `3.x`

* For general performance reasons, special modes have been removed from this major version (except route-wide no etag option)
Expand Down Expand Up @@ -77,11 +88,12 @@ Fyrejet API is very similar to Express API. In general, you are advised to use t

| Capability | Type of difference | Express | Fyrejet |
| ------------------------- | :--------------------: | :----------------------------------------------------------- | ------------------------------------------------------------ |
| `req` properties | Difference in behavior | Express provides a wide range of additional `req` properties | Fyrejet provides all core node HTTP properties, such as `req.url` && `req.method`. It also provides Express's `req.path` , `req.query` & `req.originalUrl` property. `req.route` property has different format. All other NON-DEPRECATED Express properties are reimplemented as functions (for performance), so instead of `req.protocol` you should use `req.protocol()` |
| Routing, general | Difference in behavior | Express goes through each route in the stack, verifying, whether it is appropriate for the request. When a request is made again, the same operation has to start all over again. | Fyrejet routing and base is basically a fork of Restana and its dependencies, 0http and Trouter. When an initial request is made, like ```GET /hi HTTP/1.1``` Fyrejet finds which routes are appropriate for the request and then caches those routes. This way, Fyrejet will be able to load only the required routes upon a similar request in the future. |
| Routing, details | Difference in behavior | Changing req.url or req.method only affects the routes that have not been checked yet. | Changing ```req.url``` or ```req.method``` to a different value makes Fyrejet restart the routing process for your request within Fyrejet instance. All the changes made to data (such as ```res.locals``` or ```req.params```) during routing persist. If you try to change value to the same value (e.g., if ```req.method === "POST"; req.method = "POST"```), nothing occurs. However, if you want to avoid the rerouting in other cases, you can use ```req.setUrl(url)``` and ```req.setMethod(method)```. For more information, see [Rerouting](#Rerouting). |
| `req.url` | Difference in behavior | req.url is modified to reflect the relative url in relation to the middleware route. | You should prefer```req.currentUrl()```. |
| `res.send` | Non-breaking additions | Provided | Provided, with very slight modifications (does not affect API compatibility). Also, Fyrejet provides alternative `res.sendLite`, which is unmodified `res.send` from Restana project. It is supposed to be faster and more lightweight, but with different functionality (no ETags, for example, but it is capable of sending objects faster and setting headers directly). See Restana's [documentation on `res.send`](https://github.com/jkyberneees/restana#the-ressend-method) for information on `res.sendLite` behavior. |
| Route-wide no etag option | Non-breaking additions | N/A | Fyrejet allows you to switch off etag for a specific route. |
| `res.send` and `res.json` | Non-breaking additions | Provided | Provided, with slight modifications (functionality deprecated in Express 4 is removed). Also, Fyrejet provides alternative `res.sendLite`, which is modified `res.send` from Restana project. It is supposed to be faster and more lightweight, but with different functionality (no ETags, for example, but it is capable of sending objects faster and setting headers directly). See Restana's [documentation on `res.send`](https://github.com/jkyberneees/restana#the-ressend-method) for information on `res.sendLite` behavior. |
| Route-wide no etag option | Non-breaking additions | N/A | Fyrejet allows you to switch off etag for a specific route.<br />To do so, declare routes with `noEtag` as final argument:<br />`app.get('/route', (req,res) => {}, 'noEtag')` |



Expand Down Expand Up @@ -128,12 +140,12 @@ use uWebSockets

Fyrejet uses four Initialization-time settings inherited from Restana<sup>[2](#footnote2)</sup>. These are:

| Setting | Default value | Type | Description |
| -------------------------------- | --------------- | ------------------ | ------------------------------------------------------------ |
| `cacheSize` or `routerCacheSize` | `1000` | `Number` (integer) | How many different requests can be cached for future use. Request in this case means a combination of `req.method + req.url` |
| `defaultRoute` | See source code | `Function` | Best not to change, unless you know what you are doing. Check restana documentation. |
| `prioRequestsProcessing` | `true` | `Boolean` | If `true`, HTTP requests processing/handling is prioritized using `setImmediate`. Usually does not need to be changed and you are advised not to change it, unless you know what you are doing. uWebSockets is a known exception to this rule. |
| `errorHandler` | See description | `Function` | Optional global error handler function. Default value: `(err, req, res) => { res.statusCode = 500; res.end(err.message) ` |
| Setting | Default value | Type | Description |
| -------------------------------- | ------------------------------------------------------------ | ------------------ | ------------------------------------------------------------ |
| `cacheSize` or `routerCacheSize` | `1000` | `Number` (integer) | How many different requests can be cached for future use. Request in this case means a combination of `req.method + req.url`. The cache is using LRU algorithm |
| `defaultRoute` | See source code | `Function` | Best not to change, unless you know what you are doing. Check restana documentation. |
| `prioRequestsProcessing` | `true` | `Boolean` | If `true`, HTTP requests processing/handling is prioritized using `setImmediate`. Usually does not need to be changed and you are advised not to change it, unless you know what you are doing. uWebSockets is a known exception to this rule. |
| `errorHandler` | `(err, req, res) => { res.statusCode = 500; res.end(err.message) ` | `Function` | Optional global error handler function. |



Expand Down Expand Up @@ -266,9 +278,7 @@ app.get('/hi', (req, res, next) => {

#### Caveats

No known caveats yet.


No known caveats.

## uWebSockets.js

Expand Down Expand Up @@ -318,60 +328,39 @@ It is a pseudo-scientific benchmark, but whatevs :)

![benchmark](./performance_comparison.jpg)

1. `./performance/fyrejet-route-uWS.js` on port `3001` (Fyrejet on top of uWS, with full Express-like API)
4. `./performance/fyrejet-route.js` on port `3004` (Fyrejet in default Express mode)
1. `./performance/fyrejet-route-uWS.js` and `./performance/fyrejet-route-uWS-sendLite.js` on port `3001` (Fyrejet on top of uWS, with full Express-like API)
4. `./performance/fyrejet-route.js` and `./performance/fyrejet-route-sendLite.js` on port `3004` (Fyrejet in default Express mode)
5. `./performance/express-route.js` on port `3005` (Express)

Each app exposes the `/hi` route, using the `GET` method
Each app exposes the `/hi` route, using the `GET` method. `-sendLite.js` examples use `res.sendLite` from `restana` project that handles data much faster than express's `res.send`, but at the cost of no Etag features. However, the performance with sendLite is VERY close to stock Fastify.

Hardware used: `MacBook Pro (16-inch, 2019)` || `Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz` || `64 GB 2667 MHz DDR4`

OS used: `macOS Catalina 10.15.6`
OS used: `macOS Big Sur 11.6.0`

uname -a output: `Darwin Nikolays-MacBook-Pro.local 20.3.0 Darwin Kernel Version 20.3.0: Thu Jan 21 00:07:06 PST 2021; root:xnu-7195.81.3~1/RELEASE_X86_64 x86_64`
uname -a output: `Darwin Nikolays-MacBook-Pro.local 20.6.0 Darwin Kernel Version 20.6.0: Mon Aug 30 06:12:21 PDT 2021; root:xnu-7195.141.6~3/RELEASE_X86_64 x86_64`

Testing is done with `wrk` using this command: `wrk -t8 -c64 -d5s 'http://localhost:3001/hi'`, where `3001` is changed to required port.

Second-best result out of a series of 5 is used.

Results:

1. 30553.39 req/s (116.3% faster than express)
2. 29005.36 req/s (105.4% faster than express)
3. 14122.31 req/s (baseline)
1. uWS: `43150.98 req/s` (202.5% faster than express) / `49653.79 req/s` (w/ restana's res.sendLite) (248.1% faster than express)
2. `40635.16 req/s` (184.8% faster than express) / `47509.77 req/s` (w/ restana's res.sendLite) (233.0% faster than express)
3. `14263.38 req/s` (baseline)

The CPU package temperature was ensured to be 45-47 degrees Celsium at the start of each round.

Take note that Fyrejet with `uWebSockets.js` should perform much better on Linux (I just don't have time to test, however [this benchmark](https://github.com/the-benchmarker/web-frameworks) supports the claims).

### Clustering
Take note that if you don't need Express features, such as Etag & other caching features, Restana's `res.sendLite` is going to provide you with performance more similar to Fastify. In that case, Fyrejet is gonna provide `37032.2 req/s` or `41220 req/s` under uWS.

Be aware that `uWebSockets.js` generally doesn't perform on MacOS, FreeBSD and Windows as well as on Linux. It also does not clusterize on non-Linux platforms, [as it depends on certain kernel features](https://github.com/uNetworking/uWebSockets.js/issues/214#issuecomment-547589050). This only affects `uWebSockets.js` (and, by extenstion, `fyrejet.uwsCompat`). As a workaround, consider running your app as separate apps listening on different ports, if using uWebSockets.js, and proxying behind nginx.
### Clustering under uWebSockets.js

Be aware that `uWebSockets.js` generally doesn't perform on MacOS, FreeBSD and Windows as well as on Linux. It also does not clusterize on non-Linux platforms, [as it depends on certain kernel features](https://github.com/uNetworking/uWebSockets.js/issues/214#issuecomment-547589050). This only affects `uWebSockets.js` (and, by extenstion, `fyrejet.uwsCompat`). As a workaround, consider running your app as separate apps listening on different ports, if using uWebSockets.js, and proxying behind nginx.


However, Fyrejet itself has no problems with Node.js clustering, as demonstrated by the table below:

```sh
# in terminal 1 or whatever pleases your soul <3

node ./performance/fyrejet-route-cluster.js 2
# 2 is the number of worker processes to use
# you can also use express-route-cluster.js, which will run on port 4005

# in terminal 2

wrk -t8 -c64 -d5s 'http://localhost:4004/hi'
```

#### Overall results with clustering

| № of workers | Express, req/s | Fyrejet, req/s | % difference in favor of Fyrejet |
| ------------ | -------------- | -------------- | -------------------------------- |
| 1 | 14102.74 | 28903.02 | 105.0 |
| 2 | 25568.79 | 51139.34 | 100.0 |
| 3 | 35260.33 | 71277.61 | 102.1 |
| 4 | 45060.06 | 89265.70 | 98.1 |

## Run tests

Expand All @@ -381,8 +370,6 @@ npm run test
npm run test-uWS
```



## Donations

Are welcome.
Expand All @@ -391,7 +378,9 @@ Currently, you can use PayPal:

https://paypal.me/schamberg97

## Thanks to

[jkyberneees](https://github.com/jkyberneees)'s [restana](https://github.com/BackendStack21/restana) project, which served as a foundation for this project

## Support

Expand Down
19 changes: 8 additions & 11 deletions examples/mvc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,6 @@ app.set('view engine', 'ejs');
// set views for error and 404 pages
app.set('views', path.join(__dirname, 'views'));

// define a custom res.message() method
// which stores messages in the session
app.response.message = function(msg){
// reference `req.session` via the `this.req` reference
var sess = this.req.session;
// simply add the msg to an array for later
sess.messages = sess.messages || [];
sess.messages.push(msg);
return this;
};

// log
if (!module.parent) app.use(logger('dev'));

Expand All @@ -55,6 +44,14 @@ app.use(methodOverride('_method'));
app.use(function(req, res, next){
var msgs = req.session.messages || [];

res.message = function(msg) {
var sess = res.req.session;
// simply add the msg to an array for later
sess.messages = sess.messages || [];
sess.messages.push(msg);
return res;
}

// expose "messages" local variable
res.locals.messages = msgs;

Expand Down
28 changes: 10 additions & 18 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

const EventEmitter = require('events').EventEmitter
const proto = require('./lib/application')
const req = require('./lib/request')
const res = require('./lib/response')
const req = require('./lib/request').req
const res = require('./lib/response').res
const bodyParser = require('body-parser')
const finalhandler = require('finalhandler')

Expand Down Expand Up @@ -75,17 +75,21 @@ const appCore = function (options, server, app) {
if (server.keepAliveTimeout) return false
return true
},
logerror: logerror.bind(app),
handle: function handle (req, res, step) {
res.__serverType = options.serverType
res.defaultErrHandler = function (err) {
if (this.writableEnded) return console.log(err)

const fh = finalhandler(req, res, {
env: app.get('env'),
onerror: logerror.bind(app)
onerror: app.logerror
})

if (req.method !== 'OPTIONS') {
return fh(err)
}

const options = req.app.getRouter().availableMethodsForRoute[req.url]
if (!options) {
return fh(err || false)
Expand All @@ -97,7 +101,7 @@ const appCore = function (options, server, app) {
return this.end(optionsString)
}

if (this.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Fyrejet')
this.poweredBy(res)

req.app = this
res.app = req.app
Expand Down Expand Up @@ -167,15 +171,6 @@ function createApplication (options = {}) {
Object.assign(app, appCore(options, server, app))
Object.assign(app, EventEmitter.prototype)

app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})

// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})

app.handler = app.handle
app.callback = () => app.handle

Expand All @@ -185,9 +180,7 @@ function createApplication (options = {}) {
// Init the express-like app abilities
app.init(options)

// app.use(initMiddleware(options, reqProperties, reqPropertiesEssential, app))

app.use(initMiddleware(options, app))
app.use(initMiddleware(app))

return app
}
Expand All @@ -200,7 +193,6 @@ exports.defaultErrorHandler = defaultErrorHandler // expose defaultErrorHandler
*/

exports.json = bodyParser.json
exports.query = require('./lib/routing/query')
exports.raw = bodyParser.raw
exports.static = require('./lib/additions/static.js')
exports.text = bodyParser.text
Expand All @@ -219,7 +211,7 @@ exports.response = res
*/

// exports.Route = Route;
exports.Router = require('./lib/routing/request-router-constructor')
exports.Router = createApplication

exports.uwsCompat = require('./lib/uwsCompat').uwsCompat

Expand Down
6 changes: 3 additions & 3 deletions lib/additions/static.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ module.exports = function (root, options) {
const originalMiddleware = serveStatic(root, options)
const middleware = (req, res, next) => {
const oldNext = next
const originalUrl = req.rData_internal.url
req.rData_internal.url = req.currentUrl()
const originalUrl = req.url
req.url = req.currentUrl()
next = function (err) {
req.rData_internal.url = originalUrl
req.url = originalUrl
oldNext(err)
}
return originalMiddleware(req, res, next)
Expand Down
Loading

0 comments on commit 01dffb2

Please sign in to comment.