Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: optimize checkIsHttpToken() and checkInvalidHeaderChar() #6570

Merged
merged 5 commits into from
Jun 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions benchmark/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,11 @@ function parseOpts(options) {
var num = keys.length;
var conf = {};
for (var i = 2; i < process.argv.length; i++) {
var match = process.argv[i].match(/^(.+)=(.*)$/);
var match = process.argv[i].match(/^(.+?)=([\s\S]*)$/);
if (!match || !match[1] || !options[match[1]]) {
return null;
} else {
conf[match[1]] = (match[2].length && isFinite(match[2])
? +match[2]
: match[2]);
conf[match[1]] = match[2];
num--;
}
}
Expand Down Expand Up @@ -238,20 +236,18 @@ Benchmark.prototype.report = function(value) {
console.log('%s: %s', heading, value.toFixed(5));
else if (outputFormat == 'csv')
console.log('%s,%s', heading, value.toFixed(5));

process.exit(0);
};

Benchmark.prototype.getHeading = function() {
var conf = this.config;

if (outputFormat == 'default') {
return this._name + ' ' + Object.keys(conf).map(function(key) {
return key + '=' + conf[key];
return key + '=' + JSON.stringify('' + conf[key]);
}).join(' ');
} else if (outputFormat == 'csv') {
return this._name + ',' + Object.keys(conf).map(function(key) {
return conf[key];
return JSON.stringify('' + conf[key]);
}).join(',');
}
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either the changes to this file should be a separate commit or the commit log should explain why they are necessary.

Copy link
Contributor Author

@mscdex mscdex May 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I had planned it to be in a separate commit but there would then technically be a gap where the new benchmark would be broken (causing the benchmark runner to get in a large loop actually), especially if the benchmark would be cherry picked. I can amend the commit message...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, commit message updated.

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

const common = require('../common.js');
const _checkInvalidHeaderChar = require('_http_common')._checkInvalidHeaderChar;

const bench = common.createBenchmark(main, {
key: [
// Valid
'',
'1',
'\t\t\t\t\t\t\t\t\t\tFoo bar baz',
'keep-alive',
'close',
'gzip',
'20091',
'private',
'text/html; charset=utf-8',
'text/plain',
'Sat, 07 May 2016 16:54:48 GMT',
'SAMEORIGIN',
'en-US',

// Invalid
'Here is a value that is really a folded header value\r\n this should be \
supported, but it is not currently',
'中文呢', // unicode
'foo\nbar',
'\x7F'
],
n: [5e8],
});

function main(conf) {
var n = +conf.n;
var key = conf.key;

bench.start();
for (var i = 0; i < n; i++) {
_checkInvalidHeaderChar(key);
}
bench.end(n);
}
2 changes: 1 addition & 1 deletion benchmark/http/check_is_http_token.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const bench = common.createBenchmark(main, {
':alternate-protocol', // fast bailout
'alternate-protocol:' // slow bailout
],
n: [1e6],
n: [5e8],
});

function main(conf) {
Expand Down
114 changes: 73 additions & 41 deletions lib/_http_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,52 +232,65 @@ exports.httpSocketSetup = httpSocketSetup;
* per the rules defined in RFC 7230
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
*
* Allowed characters in an HTTP token:
* ^_`a-z 94-122
* A-Z 65-90
* - 45
* 0-9 48-57
* ! 33
* #$%&' 35-39
* *+ 42-43
* . 46
* | 124
* ~ 126
*
* This implementation of checkIsHttpToken() loops over the string instead of
* using a regular expression since the former is up to 180% faster with v8 4.9
* depending on the string length (the shorter the string, the larger the
* performance difference)
*
* Additionally, checkIsHttpToken() is currently designed to be inlinable by v8,
* so take care when making changes to the implementation so that the source
* code size does not exceed v8's default max_inlined_source_size setting.
**/
function isValidTokenChar(ch) {
if (ch >= 94 && ch <= 122)
return true;
if (ch >= 65 && ch <= 90)
return true;
if (ch === 45)
return true;
if (ch >= 48 && ch <= 57)
return true;
if (ch === 34 || ch === 40 || ch === 41 || ch === 44)
return false;
if (ch >= 33 && ch <= 46)
return true;
if (ch === 124 || ch === 126)
return true;
return false;
}
function checkIsHttpToken(val) {
if (typeof val !== 'string' || val.length === 0)
return false;

for (var i = 0, len = val.length; i < len; i++) {
var ch = val.charCodeAt(i);

if (ch >= 65 && ch <= 90) // A-Z
continue;

if (ch >= 97 && ch <= 122) // a-z
continue;

// ^ => 94
// _ => 95
// ` => 96
// | => 124
// ~ => 126
if (ch === 94 || ch === 95 || ch === 96 || ch === 124 || ch === 126)
continue;

if (ch >= 48 && ch <= 57) // 0-9
continue;

// ! => 33
// # => 35
// $ => 36
// % => 37
// & => 38
// ' => 39
// * => 42
// + => 43
// - => 45
// . => 46
if (ch >= 33 && ch <= 46) {
if (ch === 34 || ch === 40 || ch === 41 || ch === 44)
if (!isValidTokenChar(val.charCodeAt(0)))
return false;
const len = val.length;
if (len > 1) {
if (!isValidTokenChar(val.charCodeAt(1)))
return false;
if (len > 2) {
if (!isValidTokenChar(val.charCodeAt(2)))
return false;
continue;
if (len > 3) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the common case? Can you quantify how much the manual loop unrolling helps?

Copy link
Contributor Author

@mscdex mscdex May 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't save the old results, but there was a lesser performance improvement with the pure loop implementation. I can run it again without and post the results here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok here are the results with just the loop, no unrolling:

http/check_is_http_token.js key="TCN" n="500000000": ./node: 73576000 ./node-master: 76945000 ................... -4.38%
http/check_is_http_token.js key="ETag" n="500000000": ./node: 60694000 ./node-master: 59833000 ................... 1.44%
http/check_is_http_token.js key="date" n="500000000": ./node: 71346000 ./node-master: 59617000 .................. 19.68%
http/check_is_http_token.js key="Vary" n="500000000": ./node: 64590000 ./node-master: 58538000 .................. 10.34%
http/check_is_http_token.js key="server" n="500000000": ./node: 49815000 ./node-master: 43208000 ................ 15.29%
http/check_is_http_token.js key="Server" n="500000000": ./node: 44101000 ./node-master: 42926000 ................. 2.74%
http/check_is_http_token.js key="status" n="500000000": ./node: 49805000 ./node-master: 43655000 ................ 14.09%
http/check_is_http_token.js key="version" n="500000000": ./node: 43245000 ./node-master: 37774000 ............... 14.48%
http/check_is_http_token.js key="Expires" n="500000000": ./node: 39172000 ./node-master: 37568000 ................ 4.27%
http/check_is_http_token.js key="alt-svc" n="500000000": ./node: 38508000 ./node-master: 35600000 ................ 8.17%
http/check_is_http_token.js key="location" n="500000000": ./node: 38132000 ./node-master: 34186000 .............. 11.54%
http/check_is_http_token.js key="Connection" n="500000000": ./node: 26488000 ./node-master: 24878000 ............. 6.47%
http/check_is_http_token.js key="Keep-Alive" n="500000000": ./node: 29028000 ./node-master: 27224000 ............. 6.63%
http/check_is_http_token.js key="content-type" n="500000000": ./node: 23468000 ./node-master: 21554000 ........... 8.88%
http/check_is_http_token.js key="Content-Type" n="500000000": ./node: 24507000 ./node-master: 23320000 ........... 5.09%
http/check_is_http_token.js key="Cache-Control" n="500000000": ./node: 22685000 ./node-master: 19883000 ......... 14.09%
http/check_is_http_token.js key="Last-Modified" n="500000000": ./node: 22605000 ./node-master: 19911000 ......... 13.53%
http/check_is_http_token.js key="Accept-Ranges" n="500000000": ./node: 22516000 ./node-master: 19637000 ......... 14.66%
http/check_is_http_token.js key="content-length" n="500000000": ./node: 20171000 ./node-master: 18336000 ........ 10.01%
http/check_is_http_token.js key="x-frame-options" n="500000000": ./node: 18221000 ./node-master: 16510000 ....... 10.36%
http/check_is_http_token.js key="x-xss-protection" n="500000000": ./node: 16391000 ./node-master: 14117000 ...... 16.11%
http/check_is_http_token.js key="Content-Encoding" n="500000000": ./node: 18711000 ./node-master: 16602000 ...... 12.70%
http/check_is_http_token.js key="Content-Location" n="500000000": ./node: 18612000 ./node-master: 16531000 ...... 12.59%
http/check_is_http_token.js key="Transfer-Encoding" n="500000000": ./node: 17713000 ./node-master: 15734000 ..... 12.58%
http/check_is_http_token.js key="alternate-protocol" n="500000000": ./node: 14988000 ./node-master: 14720000 ..... 1.82%
http/check_is_http_token.js key=":" n="500000000": ./node: 155200000 ./node-master: 108860000 ................... 42.57%
http/check_is_http_token.js key="@@" n="500000000": ./node: 151200000 ./node-master: 109090000 .................. 38.60%
http/check_is_http_token.js key="中文呢" n="500000000": ./node: 138090000 ./node-master: 101780000 ............... 35.67%
http/check_is_http_token.js key="((((())))" n="500000000": ./node: 184100000 ./node-master: 105320000 ........... 74.80%
http/check_is_http_token.js key=":alternate-protocol" n="500000000": ./node: 141000000 ./node-master: 103870000 . 35.75%
http/check_is_http_token.js key="alternate-protocol:" n="500000000": ./node: 14917000 ./node-master: 11983000 ... 24.49%

if (!isValidTokenChar(val.charCodeAt(3)))
return false;
for (var i = 4; i < len; i++) {
if (!isValidTokenChar(val.charCodeAt(i)))
return false;
}
}
}

return false;
}
return true;
}
Expand All @@ -288,13 +301,32 @@ exports._checkIsHttpToken = checkIsHttpToken;
* field-value = *( field-content / obs-fold )
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
* field-vchar = VCHAR / obs-text
*
* checkInvalidHeaderChar() is currently designed to be inlinable by v8,
* so take care when making changes to the implementation so that the source
* code size does not exceed v8's default max_inlined_source_size setting.
**/
function checkInvalidHeaderChar(val) {
val = '' + val;
for (var i = 0; i < val.length; i++) {
const ch = val.charCodeAt(i);
if (ch === 9) continue;
if (ch <= 31 || ch > 255 || ch === 127) return true;
val += '';
if (val.length < 1)
return false;
var c = val.charCodeAt(0);
if ((c <= 31 && c !== 9) || c > 255 || c === 127)
return true;
if (val.length < 2)
return false;
c = val.charCodeAt(1);
if ((c <= 31 && c !== 9) || c > 255 || c === 127)
return true;
if (val.length < 3)
return false;
c = val.charCodeAt(2);
if ((c <= 31 && c !== 9) || c > 255 || c === 127)
return true;
for (var i = 3; i < val.length; ++i) {
c = val.charCodeAt(i);
if ((c <= 31 && c !== 9) || c > 255 || c === 127)
return true;
}
return false;
}
Expand Down
97 changes: 97 additions & 0 deletions test/parallel/test-http-invalidheaderfield2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use strict';
require('../common');
const assert = require('assert');
const inspect = require('util').inspect;
const checkIsHttpToken = require('_http_common')._checkIsHttpToken;
const checkInvalidHeaderChar = require('_http_common')._checkInvalidHeaderChar;

// Good header field names
[
'TCN',
'ETag',
'date',
'alt-svc',
'Content-Type',
'0',
'Set-Cookie2',
'Set_Cookie',
'foo`bar^',
'foo|bar',
'~foobar',
'FooBar!',
'#Foo',
'$et-Cookie',
'%%Test%%',
'Test&123',
'It\'s_fun',
'2*3',
'4+2',
'3.14159265359'
].forEach(function(str) {
assert.strictEqual(checkIsHttpToken(str),
true,
'checkIsHttpToken(' +
inspect(str) +
') unexpectedly failed');
});
// Bad header field names
[
':',
'@@',
'中文呢', // unicode
'((((())))',
':alternate-protocol',
'alternate-protocol:',
'foo\nbar',
'foo\rbar',
'foo\r\nbar',
'foo\x00bar',
'\x7FMe!',
'{Start',
'(Start',
'[Start',
'End}',
'End)',
'End]',
'"Quote"',
'This,That'
].forEach(function(str) {
assert.strictEqual(checkIsHttpToken(str),
false,
'checkIsHttpToken(' +
inspect(str) +
') unexpectedly succeeded');
});


// Good header field values
[
'foo bar',
'foo\tbar',
'0123456789ABCdef',
'!@#$%^&*()-_=+\\;\':"[]{}<>,./?|~`'
].forEach(function(str) {
assert.strictEqual(checkInvalidHeaderChar(str),
false,
'checkInvalidHeaderChar(' +
inspect(str) +
') unexpectedly failed');
});

// Bad header field values
[
'foo\rbar',
'foo\nbar',
'foo\r\nbar',
'中文呢', // unicode
'\x7FMe!',
'Testing 123\x00',
'foo\vbar',
'Ding!\x07'
].forEach(function(str) {
assert.strictEqual(checkInvalidHeaderChar(str),
true,
'checkInvalidHeaderChar(' +
inspect(str) +
') unexpectedly succeeded');
});