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

Increase performance slightly #31

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
15 changes: 15 additions & 0 deletions mangle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"minify": {
"mangle": {
"properties": {
"regex": "/^_/"
},
"toplevel": true

Choose a reason for hiding this comment

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

toplevel is true by default.

Copy link
Author

Choose a reason for hiding this comment

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

Thank you!

},
"compress": {
"reduce_funcs": false,
"passes": 2

Choose a reason for hiding this comment

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

No real reason to limit passes - Terser will stop processing once two subsequent passes result in no change.

Suggested change
"passes": 2
"passes": 10

Copy link
Author

Choose a reason for hiding this comment

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

Oh this is very interesting to know, thank you again!

},
"toplevel": true
}
}
10,869 changes: 4,324 additions & 6,545 deletions package-lock.json

Large diffs are not rendered by default.

28 changes: 16 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
{
"name": "slow-json-stringify",
"version": "2.0.1",
"description": "The slowest JSON stringifier in the galaxy (:",
"version": "2.1.0",
"description": "The slow.. well actually fastest JSON stringifier in the galaxy.",
"source": "src/sjs.mjs",
"main": "dist/sjs.js",
"module": "dist/sjs.mjs",
"exports": "./dist/sjs.modern.js",
"umd:main": "dist/sjs.umd.js",
"unpkg": "dist/sjs.umd.js",
"types": "index.d.ts",
"scripts": {
"build": "microbundle",
"build": "microbundle --compress --strict",
kurtextrem marked this conversation as resolved.
Show resolved Hide resolved
"build:dev": "microbundle watch",
"lint": "eslint src/*.mjs test/*.js",
"lint:fix": "eslint src/*.mjs test/*.js --fix",
Expand All @@ -22,14 +24,14 @@
},
"devDependencies": {
"benchmark": "^2.1.4",
"chai": "^4.2.0",
"chai": "^4.3.4",
"chai-spies": "^1.0.0",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.2.0",
"eslint-plugin-import": "^2.18.0",
"fast-json-stringify": "^1.17.0",
"microbundle": "^0.11.0",
"mocha": "^6.1.4"
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.24.2",
"fast-json-stringify": "^2.7.9",
"microbundle": "^0.13.3",
"mocha": "^9.1.1"
},
"keywords": [
"stringify",
Expand All @@ -39,12 +41,14 @@
"serialize",
"hash",
"stringification",
"fast"
"fast",
"performance"
],
"author": "Luca Gesmundo <lucagesmundo@yahoo.it>",
"license": "MIT",
"bugs": {
"url": "https://github.com/lucagez/slow-json-stringify/issues"
},
"homepage": "https://github.com/lucagez/slow-json-stringify#readme"
"homepage": "https://github.com/lucagez/slow-json-stringify#readme",
"sideeffects": false
}
64 changes: 39 additions & 25 deletions src/_makeChunks.mjs
Original file line number Diff line number Diff line change
@@ -1,49 +1,63 @@
const _stringRegex = /string/;

const _replaceString = (type) =>
_stringRegex.test(type) ? '"__par__"' : "__par__";

const _isLastRegex = /^("}|})/;

// 3 possibilities after arbitrary property:
// - ", => non-last string property
// - , => non-last non-string property
// - " => last string property
const _matchStartRe = /^(\"\,|\,|\")/;

/**
* @param {string} str - prepared string already validated.
* @param {array} queue - queue containing the property name to match
* (used for building dynamic regex) needed for the preparation of
* chunks used in different scenarios.
*/
export default (str, queue) => str
// Matching prepared properties and replacing with target with or without
// double quotes.
// => Avoiding unnecessary concatenation of doublequotes during serialization.
.replace(/"\w+__sjs"/gm, type => (/string/.test(type) ? '"__par__"' : '__par__'))
.split('__par__')
.map((chunk, index, chunks) => {
const _makeChunks = (str, queue) => {
const chunks = str
// Matching prepared properties and replacing with target with or without
// double quotes.
// => Avoiding unnecessary concatenation of doublequotes during serialization.
.replace(/"\w+__sjs"/gm, _replaceString)
.split("__par__"),
result = [];

for (let i = 0; i < chunks.length; ++i) {
const chunk = chunks[i];

// Using dynamic regex to ensure that only the correct property
// at the end of the string it's actually selected.
// => e.g. ,"a":{"a": => ,"a":{
const matchProp = `("${(queue[index] || {}).name}":(\"?))$`;
const matchWhenLast = `(\,?)${matchProp}`;
const matchProp = `("${queue[i]?.name}":(\"?))$`;

// Check if current chunk is the last one inside a nested property
const isLast = /^("}|})/.test(chunks[index + 1] || '');
const isLast = _isLastRegex.test(chunks[i + 1] || "");

// If the chunk is the last one the `isUndef` case should match
// the preceding comma too.
const matchPropRe = new RegExp(isLast ? matchWhenLast : matchProp);

// 3 possibilities after arbitrary property:
// - ", => non-last string property
// - , => non-last non-string property
// - " => last string property
const matchStartRe = /^(\"\,|\,|\")/;
const matchPropRe = new RegExp(isLast ? `(\,?)${matchProp}` : matchProp);

return {
result.push({
// notify that the chunk preceding the current one has not
// its corresponding property undefined.
// => This is a V8 optimization as it's way faster writing
// the value of a property than writing the entire property.
flag: false,
pure: chunk,
// Without initial part
prevUndef: chunk.replace(matchStartRe, ''),
prevUndef: chunk.replace(_matchStartRe, ""),
// Without property chars
isUndef: chunk.replace(matchPropRe, ''),
isUndef: chunk.replace(matchPropRe, ""),
// Only remaining chars (can be zero chars)
bothUndef: chunk
.replace(matchStartRe, '')
.replace(matchPropRe, ''),
};
});
bothUndef: chunk.replace(_matchStartRe, "").replace(matchPropRe, ""),
});
}

return result;
};

export { _makeChunks };
58 changes: 31 additions & 27 deletions src/_makeQueue.mjs
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
import { _find } from './_utils';
import { __find, _find } from "./_utils.mjs";

const _sjsRegex = /__sjs/;

function _prepareQueue(originalSchema, queue, obj, acc = []) {
if (_sjsRegex.test(obj)) {
const usedAcc = [...acc];
const find = __find(usedAcc);
const { serializer } = find(originalSchema);

queue.push({
serializer,
find,
name: acc[acc.length - 1],
});
return;
}

// Recursively going deeper.
// NOTE: While going deeper, the current prop is pushed into the accumulator
// to keep track of the position inside of the object.
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
_prepareQueue(originalSchema, queue, obj[key], [...acc, key]);
}
}

/**
* @param {object} preparedSchema - schema already validated
* with modified prop values to avoid clashes.
* @param {object} originalSchema - User provided schema
* => contains array stringification serializers that are lost during preparation.
*/
export default (preparedSchema, originalSchema) => {
const _makeQueue = (preparedSchema, originalSchema) => {
const queue = [];

// Defining a function inside an other function is slow.
// However it's OK for this use case as the queue creation is not time critical.
(function scoped(obj, acc = []) {
if (/__sjs/.test(obj)) {
const usedAcc = Array.from(acc);
const find = _find(usedAcc);
const { serializer } = find(originalSchema);

queue.push({
serializer,
find,
name: acc[acc.length - 1],
});
return;
}

// Recursively going deeper.
// NOTE: While going deeper, the current prop is pushed into the accumulator
// to keep track of the position inside of the object.
return Object
.keys(obj)
.map(prop => scoped(obj[prop], [...acc, prop]));
})(preparedSchema);

_prepareQueue(originalSchema, queue, preparedSchema);
return queue;
};

export { _makeQueue };
18 changes: 9 additions & 9 deletions src/_prepare.js → src/_prepare.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
const _stringifyCallback = (_, value) => {
if (!value.isSJS) return value;
return `${value.type}__sjs`;
};

/**
* `_prepare` - aims to normalize the schema provided by the user.
Expand All @@ -6,17 +10,13 @@
* @param {object} schema - user provided schema
*/
const _prepare = (schema) => {
const preparedString = JSON.stringify(schema, (_, value) => {
if (!value.isSJS) return value;
return `${value.type}__sjs`;
});

const preparedSchema = JSON.parse(preparedString);
const _preparedString = JSON.stringify(schema, _stringifyCallback);
const _preparedSchema = JSON.parse(_preparedString);

return {
preparedString,
preparedSchema,
_preparedString,
_preparedSchema,
};
};

export default _prepare;
export { _prepare };
6 changes: 3 additions & 3 deletions src/_select.js → src/_select.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
* @param {any} value - value to serialize.
* @param {number} index - position inside the queue.
*/
const _select = chunks => (value, index) => {
const _select = (chunks) => (value, index) => {
const chunk = chunks[index];

if (typeof value !== 'undefined') {
if (value !== undefined) {
if (chunk.flag) {
return chunk.prevUndef + value;
}
Expand All @@ -28,4 +28,4 @@ const _select = chunks => (value, index) => {
return chunk.isUndef;
};

export default _select;
export { _select };
Loading