Skip to content

Commit b00e9bc

Browse files
committed
util: lazy parse mime parameters
1 parent fef7927 commit b00e9bc

File tree

7 files changed

+273
-20
lines changed

7 files changed

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

Lines changed: 54 additions & 0 deletions
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

Lines changed: 42 additions & 19 deletions
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 = /[;=]|$/;
@@ -122,13 +122,29 @@ const encode = (value) => {
122122
};
123123

124124
class MIMEParams {
125-
#data = new SafeMap();
125+
#data = null;
126+
#processed = false;
127+
#string = null;
128+
129+
constructor() {
130+
this.#data = new SafeMap();
131+
this.#processed = true;
132+
}
133+
134+
static instantiateMimeParams(str) {
135+
const instance = new MIMEParams();
136+
instance.#string = str;
137+
instance.#processed = false;
138+
return instance;
139+
}
126140

127141
delete(name) {
142+
this.#parse();
128143
this.#data.delete(name);
129144
}
130145

131146
get(name) {
147+
this.#parse();
132148
const data = this.#data;
133149
if (data.has(name)) {
134150
return data.get(name);
@@ -137,10 +153,12 @@ class MIMEParams {
137153
}
138154

139155
has(name) {
156+
this.#parse();
140157
return this.#data.has(name);
141158
}
142159

143160
set(name, value) {
161+
this.#parse();
144162
const data = this.#data;
145163
name = `${name}`;
146164
value = `${value}`;
@@ -166,18 +184,22 @@ class MIMEParams {
166184
}
167185

168186
*entries() {
187+
this.#parse();
169188
yield* this.#data.entries();
170189
}
171190

172191
*keys() {
192+
this.#parse();
173193
yield* this.#data.keys();
174194
}
175195

176196
*values() {
197+
this.#parse();
177198
yield* this.#data.values();
178199
}
179200

180201
toString() {
202+
this.#parse();
181203
let ret = '';
182204
for (const { 0: key, 1: value } of this.#data) {
183205
const encoded = encode(value);
@@ -190,8 +212,11 @@ class MIMEParams {
190212

191213
// Used to act as a friendly class to stringifying stuff
192214
// not meant to be exposed to users, could inject invalid values
193-
static parseParametersString(str, position, params) {
194-
const paramsMap = params.#data;
215+
#parse() {
216+
if (this.#processed) return; // already parsed
217+
const paramsMap = new SafeMap();
218+
let position = 0;
219+
const str = this.#string;
195220
const endOfSource = SafeStringPrototypeSearch(
196221
StringPrototypeSlice(str, position),
197222
START_ENDING_WHITESPACE,
@@ -270,13 +295,14 @@ class MIMEParams {
270295
NOT_HTTP_TOKEN_CODE_POINT) === -1 &&
271296
SafeStringPrototypeSearch(parameterValue,
272297
NOT_HTTP_QUOTED_STRING_CODE_POINT) === -1 &&
273-
params.has(parameterString) === false
298+
paramsMap.has(parameterString) === false
274299
) {
275300
paramsMap.set(parameterString, parameterValue);
276301
}
277302
position++;
278303
}
279-
return paramsMap;
304+
this.#data = paramsMap;
305+
this.#processed = true;
280306
}
281307
}
282308
const MIMEParamsStringify = MIMEParams.prototype.toString;
@@ -293,8 +319,8 @@ ObjectDefineProperty(MIMEParams.prototype, 'toJSON', {
293319
writable: true,
294320
});
295321

296-
const { parseParametersString } = MIMEParams;
297-
delete MIMEParams.parseParametersString;
322+
const { instantiateMimeParams: instantiateMimeParams } = MIMEParams;
323+
delete MIMEParams.instantiateMimeParams;
298324

299325
class MIMEType {
300326
#type;
@@ -303,14 +329,9 @@ class MIMEType {
303329
constructor(string) {
304330
string = `${string}`;
305331
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-
);
332+
this.#type = data[0];
333+
this.#subtype = data[1];
334+
this.#parameters = instantiateMimeParams(StringPrototypeSlice(string, data[2]));
314335
}
315336

316337
get type() {
@@ -362,6 +383,8 @@ ObjectDefineProperty(MIMEType.prototype, 'toJSON', {
362383
});
363384

364385
module.exports = {
386+
toASCIILower,
387+
parseTypeAndSubtype,
365388
MIMEParams,
366389
MIMEType,
367390
};
Lines changed: 7 additions & 0 deletions
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)