-
-
Notifications
You must be signed in to change notification settings - Fork 916
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
refactor: upgraded code to es6 standard #427
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this @awwit!
I was considering a similar modernization of the code base but didn't do that yet because I want to be sure that we do not introduce any performance regression.
So before going into a detailed review of your proposed changes: Would you be interested in exploring ways of benchmarking the library to prove that your changes actually at least do not introduce any performance regressions? (If they improve performance, as you claim, even better!)
@ctavan if you want me to make a benchmark, then I will do it. But a little later. What test tools do you prefer? or is it not that important? |
Great! I haven't done performance testing of JavaScript libraries recently so I would have to investigate what the state-of-the art is. The only thing that I would consider important is that performance can be tested both, in Node.js as well as in browsers. The performance tests should probably also be committed to this repository. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good, thanks!
My one concern is that embracing ES6 is going to encourage the use of patterns and features that may break IE11, where there's limited ES6 support. E.g. see my comment below about using a default value for the offset
argument. Such bugs should get caught by our BrowserStack tests though (right, @ctavan?)
Just something to be aware of.
For the record, I used to track performance in this module (using ad-hoc perf tests). I removed that code because perf simply stopped being interesting. It's been a while but, last I checked, I think the #'s were on the order of 100K's or even 1M's of ids/second (for v1 and v4). I eventually concluded that the overhead of UUID creation almost always pales in comparison to the context in which the id is used (e.g. creating a DB entry or creating/validating model data) and, thus, not something to bother about. That said, having a performance test as part of our CI tests to catch order-of-magnitude regressions would be good, but I don't think we need it as part of this PR. Let's create a separate issue for that. Note: https://benchmarkjs.com/ is my goto perf test framework. (full disclosure) Edit: Just ran those tests again, here 's the results (v1.4.7)
|
@broofa I totally agree that performance is currently most likely not a problem of the I've added a little script that might already do the trick: #430 Would be curious to see it run before and after this PR. @awwit wanna give it a try? |
@ctavan I also already managed to prepare the code for performance testing (also using Almost the same code turned out as yours. I have to try it. (In my version, I output the results to a file Let's try your benchmark. You need to merge it into a master. Then I can merge it into my branch and see the difference in performance. |
@awwit I've merged the benchmark script to master. Could you rebase this PR so we can see the results? Thanks! |
Regarding JS compatibility: Since we use babel to transpile the source into a form to support all platforms that we want to support we should not need to worry too much. Also our tests should catch regressions for all currently supported platforms 🤞 ... The only thing we should be careful about is that the more we let babel transpile code, the higher the risk that we get
|
@ctavan done, I merged your tests to my branch. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your effort!
In general I think this is a valuable contribution and will help us modernize the code base. We should still clarify a few questions, especially around eslint rules and typed array usage, but I think in general this already looks really good!
.eslintrc.json
Outdated
"standard", | ||
"prettier", | ||
"prettier/standard", | ||
"plugin:prettier/recommended" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"extends": ["plugin:prettier/recommended"]
already takes care of loading the prettier
eslint plugin, so it should not be necessary to specify it again in the plugins
array, see: https://prettier.io/docs/en/integrating-with-linters.html#recommended-configuration
|
||
const length8 = input.length * 8; | ||
|
||
const output = new Uint32Array(getOutputLength(length8)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why a Uint32Array
instead of a plain Array
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should allow for better JS engine optimizations. 'Pretty sure typed arrays are supported on all our supported platforms, so this should be okay.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, what made me suspicious is the following. With this adjustment to the benchmarks:
--- a/examples/benchmark/benchmark.js
+++ b/examples/benchmark/benchmark.js
@@ -7,7 +7,7 @@ const uuidv5 = (typeof window !== 'undefined' && window.uuidv5) || require('uuid
console.log('Starting. Tests take ~1 minute to run ...');
-const array = new Array(16);
+const array = new Uint8Array(16);
const suite = new Benchmark.Suite();
suite
.add('uuidv1()', function () {
the results change significantly.
With new Array(16)
:
uuidv1() fill existing array x 4,749,694 ops/sec ±3.14% (87 runs sampled)
uuidv4() fill existing array x 356,833 ops/sec ±3.75% (83 runs sampled)
With new Uint8Array(16)
:
uuidv1() fill existing array x 775,015 ops/sec ±1.50% (93 runs sampled)
uuidv4() fill existing array x 364,461 ops/sec ±4.00% (85 runs sampled)
What am I missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ctavan hmm…
On my machine the difference in the margin of error. Try again…
Although, I made some more optimizations locally.
src/rng-browser.js
Outdated
export default function rng() { | ||
if (!getRandomValues) { | ||
throw new Error( | ||
'crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported', | ||
); | ||
} | ||
|
||
const rnds8 = new Uint8Array(16); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might have a performance impact. A new Uint8Array
is created with each call to rng()
instead of reusing a preallocated array. Please verify!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is unsafe, because someone can use your rng()
function in their asynchronous code. When generating random numbers, it will always receive the same array. (New data will overwrite old).
Multiple function calls will return the same array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rng-browser
is not part of the public API, so should not be used externally. Internally, there's no async code that uses it. It's also very purpose-specific (generating a 16-byte array of random #'s), so I don't see there being a lot of use cases where someone would actually need to figure out how to reach into this module to access this function. Thus, the risk to users feels more hypothetical than real.
That said, reusing the array was an optimization I put in place a long time ago. There was no real demand for it (nobody was complaining about perf at the time). It was just a vanity thing to see how much perf I could squeeze out of the code.
Now, having said that, it's looks like it's still a hell of an optimization, which I'll suggest is worth keeping.
[/me pats himself on back 😆]
On master
:
rng() reusing array:
uuidv4() x 507,182 ops/sec ±5.43% (63 runs sampled)
benchmark.js:32 uuidv4() fill existing array x 688,903 ops/sec ±8.40% (63 runs sampled)
rng() creating new array each time:
uuidv4() x 183,198 ops/sec ±10.90% (45 runs sampled)
benchmark.js:32 uuidv4() fill existing array x 211,791 ops/sec ±13.46% (41 runs sampled)
On this PR:
rng() reusing array:
uuidv4() x 524,604 ops/sec ±0.82% (64 runs sampled)
benchmark.js:20 uuidv4() fill existing array x 741,362 ops/sec ±0.52% (64 runs sampled)
rng() creating new array each time:
uuidv4() x 197,603 ops/sec ±10.09% (51 runs sampled)
benchmark.js:20 uuidv4() fill existing array x 211,450 ops/sec ±15.85% (41 runs sampled)
(Note: I suspect the larger margin of error in the new-array case is due is because of the increased levels of array creation causing more erratic garbage collection.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/me pats himself on back 😆]
Well played, 9-year-ago-you, well played! 😂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@awwit please re-introduce this optimization by moving the array definition out of the function scope again.
.eslintrc.json
Outdated
"plugins": ["node", "standard", "prettier"], | ||
"extends": [ | ||
"eslint:recommended", | ||
"standard", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we decide to go for a standard eslint preset, I think we should try to get rid of as many custom rules as possible. I would be all in for that!
How did you decide which custom rules to keep and which to drop? Are there maybe more rules we can drop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll see what rules can be safely removed.
Node 12 Master:
This branch:
The rest seems to perform similar than before (within error margins) 👏 |
Agreed!
Me too. I know switching from |
I checked, this is due to the fact that we replaced |
@ctavan Bundlewatch test failed =/ We will have to increase the size to get more optimizations (especially parsing uuid to bytes). If the size is so critical, then I can remove typed arrays in failed bundles. |
I think we are starting to change too much stuff in this PR, this no longer feels controllable. Could you please revert your latest code optimizations (let's discuss them in a separate pull request)? I would then try to get this merged quickly so that we can go through further code optimizations in a more step-by-step manner and therefore controlling better for possible performance optimizations. So for the time being I'd prefer if this PR would contain only:
Let's move all other code changes into a separate pull request. |
Oh, and to get this in it would be great if you could Allow changes to a pull request branch created from a fork. This would help me get this merged and keep all references to your contributions. |
Benchmark master:
Benchmark this branch:
|
@awwit I just realized that I already hat push access to your branch. So I did the requested cleanups and merged your changes 🎉 I copied the advanced optimizations to https://github.com/uuidjs/uuid/tree/awwit-refactorings feel free to pick them up from there and create another pull request if you want. I'm happy to merge those changes as well once I understand their benefit and performance implications. |
@ctavan You're right! Then I will continue to do in another brunch. I will remove the code that greatly inflates the bundle. And I’ll do something else. And I will post the changes piece by piece. |
Changes:
Improved eslint configuration.
Upgraded code to es6 standard.
Optimized use of arrays.
Since you are already using ES6 features (
Uint8Array
as example), I allowed myself to adapt your code to this standard.All
var
were replaced bylet
orconst
. So we will give more optimization opportunities for JS engine.The same goes for using arrays. It is not recommended to create arrays through
new Array(N)
. Because arrays are created full of holes. Which affects performance.https://v8.dev/blog/elements-kinds
Since you are already using
Uint8Array
, I replaced regular arrays with typed ones (in some places). Which are created with a fixed size and free from the disadvantages of regular arrays.P.S. If something does not suit you in my changes, then let me know and I will fix it =)