Skip to content

Commit 54bb691

Browse files
authored
util: lazy parse mime parameters
PR-URL: #49889 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]>
1 parent d920b7c commit 54bb691

File tree

7 files changed

+271
-18
lines changed

7 files changed

+271
-18
lines changed
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { MIMEType } = require('util');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e5],
9+
value: [
10+
'application/ecmascript; ',
11+
'text/html;charset=gbk',
12+
`text/html;${'0123456789'.repeat(12)}=x;charset=gbk`,
13+
'text/html;test=\u00FF;charset=gbk',
14+
'x/x;\n\r\t x=x\n\r\t ;x=y',
15+
],
16+
}, {
17+
});
18+
19+
function main({ n, value }) {
20+
// Warm up.
21+
const length = 1024;
22+
const array = [];
23+
let errCase = false;
24+
25+
for (let i = 0; i < length; ++i) {
26+
try {
27+
array.push(new MIMEType(value));
28+
} catch (e) {
29+
errCase = true;
30+
array.push(e);
31+
}
32+
}
33+
34+
// console.log(`errCase: ${errCase}`);
35+
bench.start();
36+
37+
for (let i = 0; i < n; ++i) {
38+
const index = i % length;
39+
try {
40+
array[index] = new MIMEType(value);
41+
} catch (e) {
42+
array[index] = e;
43+
}
44+
}
45+
46+
bench.end(n);
47+
48+
// Verify the entries to prevent dead code elimination from making
49+
// the benchmark invalid.
50+
for (let i = 0; i < length; ++i) {
51+
assert.strictEqual(typeof array[i], errCase ? 'object' : 'object');
52+
}
53+
}

benchmark/mime/mimetype-to-string.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { MIMEType } = require('util');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e5],
9+
value: [
10+
'application/ecmascript; ',
11+
'text/html;charset=gbk',
12+
`text/html;${'0123456789'.repeat(12)}=x;charset=gbk`,
13+
'text/html;test=\u00FF;charset=gbk',
14+
'x/x;\n\r\t x=x\n\r\t ;x=y',
15+
],
16+
}, {
17+
});
18+
19+
function main({ n, value }) {
20+
// Warm up.
21+
const length = 1024;
22+
const array = [];
23+
let errCase = false;
24+
25+
const mime = new MIMEType(value);
26+
27+
for (let i = 0; i < length; ++i) {
28+
try {
29+
array.push(mime.toString());
30+
} catch (e) {
31+
errCase = true;
32+
array.push(e);
33+
}
34+
}
35+
36+
// console.log(`errCase: ${errCase}`);
37+
bench.start();
38+
39+
for (let i = 0; i < n; ++i) {
40+
const index = i % length;
41+
try {
42+
array[index] = mime.toString();
43+
} catch (e) {
44+
array[index] = e;
45+
}
46+
}
47+
48+
bench.end(n);
49+
50+
// Verify the entries to prevent dead code elimination from making
51+
// the benchmark invalid.
52+
for (let i = 0; i < length; ++i) {
53+
assert.strictEqual(typeof array[i], errCase ? 'object' : 'string');
54+
}
55+
}
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
6+
const bench = common.createBenchmark(main, {
7+
n: [1e7],
8+
value: [
9+
'application/ecmascript; ',
10+
'text/html;charset=gbk',
11+
// eslint-disable-next-line max-len
12+
'text/html;0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789=x;charset=gbk',
13+
],
14+
}, {
15+
flags: ['--expose-internals'],
16+
});
17+
18+
function main({ n, value }) {
19+
20+
const parseTypeAndSubtype = require('internal/mime').parseTypeAndSubtype;
21+
// Warm up.
22+
const length = 1024;
23+
const array = [];
24+
let errCase = false;
25+
26+
for (let i = 0; i < length; ++i) {
27+
try {
28+
array.push(parseTypeAndSubtype(value));
29+
} catch (e) {
30+
errCase = true;
31+
array.push(e);
32+
}
33+
}
34+
35+
// console.log(`errCase: ${errCase}`);
36+
bench.start();
37+
for (let i = 0; i < n; ++i) {
38+
const index = i % length;
39+
try {
40+
array[index] = parseTypeAndSubtype(value);
41+
} catch (e) {
42+
array[index] = e;
43+
}
44+
}
45+
46+
bench.end(n);
47+
48+
// Verify the entries to prevent dead code elimination from making
49+
// the benchmark invalid.
50+
for (let i = 0; i < length; ++i) {
51+
assert.strictEqual(typeof array[i], errCase ? 'object' : 'object');
52+
}
53+
}

benchmark/mime/to-ascii-lower.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
6+
const bench = common.createBenchmark(main, {
7+
n: [1e7],
8+
value: [
9+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
10+
'UPPERCASE',
11+
'lowercase',
12+
'mixedCase',
13+
],
14+
}, {
15+
flags: ['--expose-internals'],
16+
});
17+
18+
function main({ n, value }) {
19+
20+
const toASCIILower = require('internal/mime').toASCIILower;
21+
// Warm up.
22+
const length = 1024;
23+
const array = [];
24+
let errCase = false;
25+
26+
for (let i = 0; i < length; ++i) {
27+
try {
28+
array.push(toASCIILower(value));
29+
} catch (e) {
30+
errCase = true;
31+
array.push(e);
32+
}
33+
}
34+
35+
// console.log(`errCase: ${errCase}`);
36+
bench.start();
37+
38+
for (let i = 0; i < n; ++i) {
39+
const index = i % length;
40+
try {
41+
array[index] = toASCIILower(value);
42+
} catch (e) {
43+
array[index] = e;
44+
}
45+
}
46+
47+
bench.end(n);
48+
49+
// Verify the entries to prevent dead code elimination from making
50+
// the benchmark invalid.
51+
for (let i = 0; i < length; ++i) {
52+
assert.strictEqual(typeof array[i], errCase ? 'object' : 'string');
53+
}
54+
}

lib/internal/mime.js

+42-18
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function toASCIILower(str) {
3636

3737
const SOLIDUS = '/';
3838
const SEMICOLON = ';';
39+
3940
function parseTypeAndSubtype(str) {
4041
// Skip only HTTP whitespace from start
4142
let position = SafeStringPrototypeSearch(str, END_BEGINNING_WHITESPACE);
@@ -72,12 +73,11 @@ function parseTypeAndSubtype(str) {
7273
throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype);
7374
}
7475
const subtype = toASCIILower(trimmedSubtype);
75-
return {
76-
__proto__: null,
76+
return [
7777
type,
7878
subtype,
79-
parametersStringIndex: position,
80-
};
79+
position,
80+
];
8181
}
8282

8383
const EQUALS_SEMICOLON_OR_END = /[;=]|$/;
@@ -123,12 +123,29 @@ const encode = (value) => {
123123

124124
class MIMEParams {
125125
#data = new SafeMap();
126+
// We set the flag the MIMEParams instance as processed on initialization
127+
// to defer the parsing of a potentially large string.
128+
#processed = true;
129+
#string = null;
130+
131+
/**
132+
* Used to instantiate a MIMEParams object within the MIMEType class and
133+
* to allow it to be parsed lazily.
134+
*/
135+
static instantiateMimeParams(str) {
136+
const instance = new MIMEParams();
137+
instance.#string = str;
138+
instance.#processed = false;
139+
return instance;
140+
}
126141

127142
delete(name) {
143+
this.#parse();
128144
this.#data.delete(name);
129145
}
130146

131147
get(name) {
148+
this.#parse();
132149
const data = this.#data;
133150
if (data.has(name)) {
134151
return data.get(name);
@@ -137,10 +154,12 @@ class MIMEParams {
137154
}
138155

139156
has(name) {
157+
this.#parse();
140158
return this.#data.has(name);
141159
}
142160

143161
set(name, value) {
162+
this.#parse();
144163
const data = this.#data;
145164
name = `${name}`;
146165
value = `${value}`;
@@ -166,18 +185,22 @@ class MIMEParams {
166185
}
167186

168187
*entries() {
188+
this.#parse();
169189
yield* this.#data.entries();
170190
}
171191

172192
*keys() {
193+
this.#parse();
173194
yield* this.#data.keys();
174195
}
175196

176197
*values() {
198+
this.#parse();
177199
yield* this.#data.values();
178200
}
179201

180202
toString() {
203+
this.#parse();
181204
let ret = '';
182205
for (const { 0: key, 1: value } of this.#data) {
183206
const encoded = encode(value);
@@ -190,8 +213,11 @@ class MIMEParams {
190213

191214
// Used to act as a friendly class to stringifying stuff
192215
// not meant to be exposed to users, could inject invalid values
193-
static parseParametersString(str, position, params) {
194-
const paramsMap = params.#data;
216+
#parse() {
217+
if (this.#processed) return; // already parsed
218+
const paramsMap = this.#data;
219+
let position = 0;
220+
const str = this.#string;
195221
const endOfSource = SafeStringPrototypeSearch(
196222
StringPrototypeSlice(str, position),
197223
START_ENDING_WHITESPACE,
@@ -270,13 +296,14 @@ class MIMEParams {
270296
NOT_HTTP_TOKEN_CODE_POINT) === -1 &&
271297
SafeStringPrototypeSearch(parameterValue,
272298
NOT_HTTP_QUOTED_STRING_CODE_POINT) === -1 &&
273-
params.has(parameterString) === false
299+
paramsMap.has(parameterString) === false
274300
) {
275301
paramsMap.set(parameterString, parameterValue);
276302
}
277303
position++;
278304
}
279-
return paramsMap;
305+
this.#data = paramsMap;
306+
this.#processed = true;
280307
}
281308
}
282309
const MIMEParamsStringify = MIMEParams.prototype.toString;
@@ -293,8 +320,8 @@ ObjectDefineProperty(MIMEParams.prototype, 'toJSON', {
293320
writable: true,
294321
});
295322

296-
const { parseParametersString } = MIMEParams;
297-
delete MIMEParams.parseParametersString;
323+
const { instantiateMimeParams } = MIMEParams;
324+
delete MIMEParams.instantiateMimeParams;
298325

299326
class MIMEType {
300327
#type;
@@ -303,14 +330,9 @@ class MIMEType {
303330
constructor(string) {
304331
string = `${string}`;
305332
const data = parseTypeAndSubtype(string);
306-
this.#type = data.type;
307-
this.#subtype = data.subtype;
308-
this.#parameters = new MIMEParams();
309-
parseParametersString(
310-
string,
311-
data.parametersStringIndex,
312-
this.#parameters,
313-
);
333+
this.#type = data[0];
334+
this.#subtype = data[1];
335+
this.#parameters = instantiateMimeParams(StringPrototypeSlice(string, data[2]));
314336
}
315337

316338
get type() {
@@ -362,6 +384,8 @@ ObjectDefineProperty(MIMEType.prototype, 'toJSON', {
362384
});
363385

364386
module.exports = {
387+
toASCIILower,
388+
parseTypeAndSubtype,
365389
MIMEParams,
366390
MIMEType,
367391
};

test/benchmark/test-benchmark-mime.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const runBenchmark = require('../common/benchmark');
6+
7+
runBenchmark('mime', { NODEJS_BENCHMARK_ZERO_ALLOWED: 1 });

0 commit comments

Comments
 (0)