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

util: lazy parse mime parameters #49889

Merged
merged 2 commits into from
Oct 8, 2023
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
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',
Uzlopak marked this conversation as resolved.
Show resolved Hide resolved
],
}, {
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;
Uzlopak marked this conversation as resolved.
Show resolved Hide resolved
}

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
Uzlopak marked this conversation as resolved.
Show resolved Hide resolved
) {
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