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 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
14 changes: 14 additions & 0 deletions mangle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"minify": {
"mangle": {
"properties": {
"regex": "/^_/"
}
},
"compress": {
"reduce_funcs": false,
"passes": 10
},
"toplevel": true
}
}
10,869 changes: 4,324 additions & 6,545 deletions package-lock.json

Large diffs are not rendered by default.

26 changes: 15 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"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": {
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
}
68 changes: 43 additions & 25 deletions src/_makeChunks.mjs
Original file line number Diff line number Diff line change
@@ -1,49 +1,67 @@
const _replaceString = type => type.indexOf('string') !== -1 ? '"__par__"' : "__par__";

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

const _chunkRegex = /"\w+__sjs"/g;

/**
* @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(_chunkRegex, _replaceString)
.split("__par__"),
result = [];

let _i;
const length = chunks.length;
for (let i = 0; i < 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 = (_i = i + 1) === length || (
(_i = chunks[_i].indexOf('}'))
&& (_i === 0 || _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 {
const withoutInitial = chunk.replace(_matchStartRe, "");

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: withoutInitial,
// 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: withoutInitial.replace(matchPropRe, ""),
});
}

return result;
};

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

/**
* @param {object} originalSchema
* @param {array} queue
* @param {string|object} obj
* @param {array} acc
*/
function _prepareQueue(originalSchema, queue, obj, acc = []) {
// this looks weird for objects, but is actually exactly what we want: object.toString() === '[object Object]'. We only want actual strings.
if (obj.toString().indexOf('__sjs') !== -1) {
const find = __find(acc);
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