undici icon indicating copy to clipboard operation
undici copied to clipboard

websocket: use dataview

Open KhafraDev opened this issue 1 year ago • 5 comments

We use a dataview here for performance reasons. I should have addressed this in #2106 but I did not.

Refs: https://github.com/nodejs/performance/issues/2

KhafraDev avatar May 03 '23 14:05 KhafraDev

Codecov Report

Patch coverage: 100.00% and project coverage change: -0.04 :warning:

Comparison is base (4688da2) 85.89% compared to head (f487e5a) 85.86%.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2107      +/-   ##
==========================================
- Coverage   85.89%   85.86%   -0.04%     
==========================================
  Files          76       76              
  Lines        6608     6608              
==========================================
- Hits         5676     5674       -2     
- Misses        932      934       +2     
Impacted Files Coverage Δ
lib/websocket/frame.js 100.00% <100.00%> (ø)

... and 1 file with indirect coverage changes

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Do you have feedback about the report comment? Let us know in this issue.

codecov-commenter avatar May 03 '23 14:05 codecov-commenter

Interesting: I assumed it would be quicker the way I changed it, but I didn't benchmark, so that's on me!

jawj avatar May 03 '23 15:05 jawj

@jawj don't worry about it at all, I should have explained why we used DataViews since you asked in the PR, but I didn't. There aren't currently benchmarks so there's no easy way of benchmarking this either 😆.

KhafraDev avatar May 04 '23 01:05 KhafraDev

@KhafraDev Thanks! Though this got me curious, so I did a bit of benchmarking ...

let b, t0;
const trials = 1_000_000;
const bufferSize = 256;
const maxUint16Plus1 = 65_536;

t0 = performance.now();
for (let i = 0; i < trials; i ++) {
  const value = i % maxUint16Plus1;
  b = Buffer.allocUnsafe(bufferSize);
  new DataView(b.buffer).setUint16(2, value);
}
console.log('buggy DataView', performance.now() - t0, 'ms');

t0 = performance.now();
for (let i = 0; i < trials; i ++) {
  const value = i % maxUint16Plus1;
  b = Buffer.allocUnsafe(bufferSize);
  new DataView(b.buffer, b.byteOffset, b.byteLength).setUint16(2, value)
}
console.log('fixed DataView', performance.now() - t0, 'ms');

t0 = performance.now();
for (let i = 0; i < trials; i ++) {
  const value = i % maxUint16Plus1;
  b = Buffer.allocUnsafe(bufferSize);
  b.writeUInt16BE(value, 2);
}
console.log('writeUInt16BE ', performance.now() - t0, 'ms');

t0 = performance.now();
for (let i = 0; i < trials; i ++) {
  const value = i % maxUint16Plus1;
  b = Buffer.allocUnsafe(bufferSize);
  b[2] = value >>> 8;
  b[3] = value & 0xff;
}
console.log('bit-twiddling ', performance.now() - t0, 'ms');

Which run on Node 18.10 and a 2020 Intel MacBook Pro gives:

buggy DataView 215.71186208724976 ms
fixed DataView 210.01300191879272 ms
writeUInt16BE  107.3242199420929 ms
bit-twiddling  90.78455805778503 ms

DataView is the slowest! To be honest, I don't much mind which approach is used, but I would really like a fix to be released to npm soon, because WebSockets are totally broken until this is done.

jawj avatar May 04 '23 08:05 jawj

I think the reason for DataView coming out as slowest is that you're mostly measuring object creation and allocation performance. In this sense it's not much of a surprise that DataView comes out on the bottom since using it requires creating secondary object while the rest get to go with just the one Buffer.

That being said, ... it looks like this is very much what the code itself is doing as well so the performance measurement is apt for comparing what the options are for this PR.

aapoalas avatar Jul 30 '23 17:07 aapoalas

@KhafraDev is this mergable once conflicts are done?

ronag avatar Feb 25 '24 09:02 ronag

I don't think this should be merged. It's slower, because it has the overhead of creating a new DataView for a single write.

jawj avatar Feb 25 '24 14:02 jawj