Skip to content

Commit

Permalink
util: lazy parse mime parameters
Browse files Browse the repository at this point in the history
PR-URL: nodejs#49889
Reviewed-By: Antoine du Hamel <[email protected]>
Reviewed-By: Yagiz Nizipli <[email protected]>
  • Loading branch information
Uzlopak authored Oct 8, 2023
1 parent d920b7c commit 54bb691
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 18 deletions.
53 changes: 53 additions & 0 deletions benchmark/mime/mimetype-instantiation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const { MIMEType } = require('util');

const bench = common.createBenchmark(main, {
n: [1e5],
value: [
'application/ecmascript; ',
'text/html;charset=gbk',
`text/html;${'0123456789'.repeat(12)}=x;charset=gbk`,
'text/html;test=\u00FF;charset=gbk',
'x/x;\n\r\t x=x\n\r\t ;x=y',
],
}, {
});

function main({ n, value }) {
// Warm up.
const length = 1024;
const array = [];
let errCase = false;

for (let i = 0; i < length; ++i) {
try {
array.push(new MIMEType(value));
} catch (e) {
errCase = true;
array.push(e);
}
}

// console.log(`errCase: ${errCase}`);
bench.start();

for (let i = 0; i < n; ++i) {
const index = i % length;
try {
array[index] = new MIMEType(value);
} catch (e) {
array[index] = e;
}
}

bench.end(n);

// Verify the entries to prevent dead code elimination from making
// the benchmark invalid.
for (let i = 0; i < length; ++i) {
assert.strictEqual(typeof array[i], errCase ? 'object' : 'object');
}
}
55 changes: 55 additions & 0 deletions benchmark/mime/mimetype-to-string.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const { MIMEType } = require('util');

const bench = common.createBenchmark(main, {
n: [1e5],
value: [
'application/ecmascript; ',
'text/html;charset=gbk',
`text/html;${'0123456789'.repeat(12)}=x;charset=gbk`,
'text/html;test=\u00FF;charset=gbk',
'x/x;\n\r\t x=x\n\r\t ;x=y',
],
}, {
});

function main({ n, value }) {
// Warm up.
const length = 1024;
const array = [];
let errCase = false;

const mime = new MIMEType(value);

for (let i = 0; i < length; ++i) {
try {
array.push(mime.toString());
} catch (e) {
errCase = true;
array.push(e);
}
}

// console.log(`errCase: ${errCase}`);
bench.start();

for (let i = 0; i < n; ++i) {
const index = i % length;
try {
array[index] = mime.toString();
} catch (e) {
array[index] = e;
}
}

bench.end(n);

// Verify the entries to prevent dead code elimination from making
// the benchmark invalid.
for (let i = 0; i < length; ++i) {
assert.strictEqual(typeof array[i], errCase ? 'object' : 'string');
}
}
53 changes: 53 additions & 0 deletions benchmark/mime/parse-type-and-subtype.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

const common = require('../common');
const assert = require('assert');

const bench = common.createBenchmark(main, {
n: [1e7],
value: [
'application/ecmascript; ',
'text/html;charset=gbk',
// eslint-disable-next-line max-len
'text/html;0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789=x;charset=gbk',
],
}, {
flags: ['--expose-internals'],
});

function main({ n, value }) {

const parseTypeAndSubtype = require('internal/mime').parseTypeAndSubtype;
// Warm up.
const length = 1024;
const array = [];
let errCase = false;

for (let i = 0; i < length; ++i) {
try {
array.push(parseTypeAndSubtype(value));
} catch (e) {
errCase = true;
array.push(e);
}
}

// console.log(`errCase: ${errCase}`);
bench.start();
for (let i = 0; i < n; ++i) {
const index = i % length;
try {
array[index] = parseTypeAndSubtype(value);
} catch (e) {
array[index] = e;
}
}

bench.end(n);

// Verify the entries to prevent dead code elimination from making
// the benchmark invalid.
for (let i = 0; i < length; ++i) {
assert.strictEqual(typeof array[i], errCase ? 'object' : 'object');
}
}
54 changes: 54 additions & 0 deletions benchmark/mime/to-ascii-lower.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use strict';

const common = require('../common');
const assert = require('assert');

const bench = common.createBenchmark(main, {
n: [1e7],
value: [
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'UPPERCASE',
'lowercase',
'mixedCase',
],
}, {
flags: ['--expose-internals'],
});

function main({ n, value }) {

const toASCIILower = require('internal/mime').toASCIILower;
// Warm up.
const length = 1024;
const array = [];
let errCase = false;

for (let i = 0; i < length; ++i) {
try {
array.push(toASCIILower(value));
} catch (e) {
errCase = true;
array.push(e);
}
}

// console.log(`errCase: ${errCase}`);
bench.start();

for (let i = 0; i < n; ++i) {
const index = i % length;
try {
array[index] = toASCIILower(value);
} catch (e) {
array[index] = e;
}
}

bench.end(n);

// Verify the entries to prevent dead code elimination from making
// the benchmark invalid.
for (let i = 0; i < length; ++i) {
assert.strictEqual(typeof array[i], errCase ? 'object' : 'string');
}
}
60 changes: 42 additions & 18 deletions lib/internal/mime.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function toASCIILower(str) {

const SOLIDUS = '/';
const SEMICOLON = ';';

function parseTypeAndSubtype(str) {
// Skip only HTTP whitespace from start
let position = SafeStringPrototypeSearch(str, END_BEGINNING_WHITESPACE);
Expand Down Expand Up @@ -72,12 +73,11 @@ function parseTypeAndSubtype(str) {
throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype);
}
const subtype = toASCIILower(trimmedSubtype);
return {
__proto__: null,
return [
type,
subtype,
parametersStringIndex: position,
};
position,
];
}

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

class MIMEParams {
#data = new SafeMap();
// We set the flag the MIMEParams instance as processed on initialization
// to defer the parsing of a potentially large string.
#processed = true;
#string = null;

/**
* Used to instantiate a MIMEParams object within the MIMEType class and
* to allow it to be parsed lazily.
*/
static instantiateMimeParams(str) {
const instance = new MIMEParams();
instance.#string = str;
instance.#processed = false;
return instance;
}

delete(name) {
this.#parse();
this.#data.delete(name);
}

get(name) {
this.#parse();
const data = this.#data;
if (data.has(name)) {
return data.get(name);
Expand All @@ -137,10 +154,12 @@ class MIMEParams {
}

has(name) {
this.#parse();
return this.#data.has(name);
}

set(name, value) {
this.#parse();
const data = this.#data;
name = `${name}`;
value = `${value}`;
Expand All @@ -166,18 +185,22 @@ class MIMEParams {
}

*entries() {
this.#parse();
yield* this.#data.entries();
}

*keys() {
this.#parse();
yield* this.#data.keys();
}

*values() {
this.#parse();
yield* this.#data.values();
}

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

// Used to act as a friendly class to stringifying stuff
// not meant to be exposed to users, could inject invalid values
static parseParametersString(str, position, params) {
const paramsMap = params.#data;
#parse() {
if (this.#processed) return; // already parsed
const paramsMap = this.#data;
let position = 0;
const str = this.#string;
const endOfSource = SafeStringPrototypeSearch(
StringPrototypeSlice(str, position),
START_ENDING_WHITESPACE,
Expand Down Expand Up @@ -270,13 +296,14 @@ class MIMEParams {
NOT_HTTP_TOKEN_CODE_POINT) === -1 &&
SafeStringPrototypeSearch(parameterValue,
NOT_HTTP_QUOTED_STRING_CODE_POINT) === -1 &&
params.has(parameterString) === false
paramsMap.has(parameterString) === false
) {
paramsMap.set(parameterString, parameterValue);
}
position++;
}
return paramsMap;
this.#data = paramsMap;
this.#processed = true;
}
}
const MIMEParamsStringify = MIMEParams.prototype.toString;
Expand All @@ -293,8 +320,8 @@ ObjectDefineProperty(MIMEParams.prototype, 'toJSON', {
writable: true,
});

const { parseParametersString } = MIMEParams;
delete MIMEParams.parseParametersString;
const { instantiateMimeParams } = MIMEParams;
delete MIMEParams.instantiateMimeParams;

class MIMEType {
#type;
Expand All @@ -303,14 +330,9 @@ class MIMEType {
constructor(string) {
string = `${string}`;
const data = parseTypeAndSubtype(string);
this.#type = data.type;
this.#subtype = data.subtype;
this.#parameters = new MIMEParams();
parseParametersString(
string,
data.parametersStringIndex,
this.#parameters,
);
this.#type = data[0];
this.#subtype = data[1];
this.#parameters = instantiateMimeParams(StringPrototypeSlice(string, data[2]));
}

get type() {
Expand Down Expand Up @@ -362,6 +384,8 @@ ObjectDefineProperty(MIMEType.prototype, 'toJSON', {
});

module.exports = {
toASCIILower,
parseTypeAndSubtype,
MIMEParams,
MIMEType,
};
7 changes: 7 additions & 0 deletions test/benchmark/test-benchmark-mime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

require('../common');

const runBenchmark = require('../common/benchmark');

runBenchmark('mime', { NODEJS_BENCHMARK_ZERO_ALLOWED: 1 });
Loading

0 comments on commit 54bb691

Please sign in to comment.