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

Introduce the generateMask option #1990

Merged
merged 2 commits into from Dec 20, 2021
Merged

Introduce the generateMask option #1990

merged 2 commits into from Dec 20, 2021

Conversation

lpinca
Copy link
Member

@lpinca lpinca commented Dec 19, 2021

The generateMask option specifies a function that can be used to
generate custom masking keys.

Refs: #1986
Refs: #1988
Refs: #1989

@lpinca lpinca changed the title Introduce the generateMask option Introduce the generateMask option Dec 19, 2021
lib/sender.js Outdated

target[1] |= 0x80;
target[offset - 4] = mask[0];
target[offset - 3] = mask[1];
target[offset - 2] = mask[2];
target[offset - 1] = mask[3];

if ((mask[0] | mask[1] | mask[2] | mask[3]) === 0) {
Copy link
Member Author

Choose a reason for hiding this comment

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

I think this optimization does not make sense. It is actually counterproductive in the common case. If a random mask is used, then the probability of this event is 1 / 2**32. The same applies for the same optimization on lib/receiver.js.

Copy link
Contributor

@ronag ronag Dec 19, 2021

Choose a reason for hiding this comment

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

I don’t think it hurts and allows for hot path optimization for clients that know the hot path “trick”.

Copy link
Member Author

@lpinca lpinca Dec 20, 2021

Choose a reason for hiding this comment

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

I initially called the option fillMaskBuffer, and made it take the mask buffer like crypto.randomFillSync(). Then I changed my mind and went for generateMask. The reason is that it is simpler to use. The user does not have to remember to fill the buffer. They are not forced to do that but we should then document that the last one is used even if the WebSocket instance is different (this might be confusing). With generateMask, not returning a buffer results in a thrown error. Finally, generateMask does not force the user to return a new Buffer instance every time. The user can return the same instance:

const mask = Buffer.alloc(4);

const ws = new Websocket(url, {
  generateMask() {
    mask.fill(0);
    return mask;
  }
});

Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this sufficient? I don't think ws will mutate the buffer, right?

const mask = Buffer.alloc(4);

const ws = new Websocket(url, {
  generateMask() {
    return mask;
  }
});

Copy link
Member Author

@lpinca lpinca Dec 20, 2021

Choose a reason for hiding this comment

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

Correct, ws does not mutate it.

Copy link
Member Author

@lpinca lpinca Dec 20, 2021

Choose a reason for hiding this comment

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

@e3dio

I benchmarked this line (mask[0] | mask[1] | mask[2] | mask[3]) === 0 and it came out to less than 1 nanosecond

How can you get that precision? Two sequential process.hrtime.bigint() with nothing in-between take ~0.1 milliseconds / ~100000 nanoseconds on my machine.

Copy link

Choose a reason for hiding this comment

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

Why don't you make an uint32array from the memory and just check if (a[0])

Copy link

@e3dio e3dio Dec 21, 2021

Choose a reason for hiding this comment

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

Ok sorry I see what you are saying, pass around Uint32Array's instead of Buffers for the mask:

uint32 = new Uint32Array(1) // new mask default value = 0

{ generateMask: uint32 => require('crypto').randomFillSync(uint32) }
{ generateMask: uint32 => uint32[0] = Math.random()*2**32 }
{ generateMask: uint32 => uint32[0]++ }

const skipMasking = uint32[0] === 0;

They are easier to work with than Buffer because the mask is 32 bit unsigned int. Maybe only reason to use Buffer is more people are familiar with Buffer? You could do either one, Uint32Array makes more sense, you just have to know how it works. Buffer is easier to say than Uint32Array. Ideally Uint32Array but Buffer is ok. Uint32Array has a performance advantage setting 1 value vs 4

Copy link
Member Author

@lpinca lpinca Dec 21, 2021

Choose a reason for hiding this comment

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

Maybe only reason to use Buffer is more people are familiar with Buffer?

Yes, and because bufferUtil.mask() was written to take a buffer at the time https://github.com/websockets/bufferutil/blob/cd48704aa8e87c19d7497812d466fd6fb6a28c09/src/bufferutil.cc#L89-L90, when typed arrays were not supported, and that did not change over time. We could make it take a Uint32Array but I do not think it is worth the effort.

lib/sender.js Outdated
let mask = _mask;

if (options.generateMask) {
mask = options.generateMask();
Copy link

Choose a reason for hiding this comment

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

This generates a new Buffer object on every send, instead it should modify existing buffer

Copy link
Member Author

@lpinca lpinca Dec 20, 2021

Choose a reason for hiding this comment

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

Not necessarily, see #1990 (comment).

Copy link

@e3dio e3dio Dec 20, 2021

Choose a reason for hiding this comment

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

It makes the config setting awkward to require maintaining the buffer outside the setting, it would be more convenient and succinct to pass the buffer to the function:

new WebSocket(url, { generateMask: buf => require('crypto').randomFillSync(buf) } )

Copy link
Member Author

@lpinca lpinca Dec 20, 2021

Choose a reason for hiding this comment

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

it would be more convenient and succinct to pass the buffer to the function.

The only reason why I did not do that is that the buffer is shared between WebSocket instances and this might generate confusion even if it is documented because people do not read.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does it need to be shared?

Copy link
Member Author

Choose a reason for hiding this comment

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

It requires more changes because I don't want to add a mask buffer also for server clients and currently the Sender is server/client agnostic.

Copy link
Member Author

Choose a reason for hiding this comment

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

There is also the static Sender.frame() method. It gets kinda messy.

Copy link

Choose a reason for hiding this comment

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

You only need to create an extra Buffer if the Sender has the config option generateMask set, otherwise you use the shared buffer

Copy link
Member Author

Choose a reason for hiding this comment

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

See 8ab0db4

Copy link

Choose a reason for hiding this comment

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

Looks like exactly what is needed for that, this would be a great feature nice work

@ronag
Copy link
Contributor

ronag commented Dec 20, 2021

LGTM! Thanks!

@lpinca lpinca force-pushed the add/generate-mask-option branch 2 times, most recently from 6564661 to 18bef28 Compare December 20, 2021 10:43
Copy link
Contributor

@ronag ronag left a comment

Choose a reason for hiding this comment

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

I think allocating target is not necessary on the === 0 fast path?

The `generateMask` option specifies a function that can be used to
generate custom masking keys.

Refs: #1986
Refs: #1988
Refs: #1989
@lpinca
Copy link
Member Author

lpinca commented Dec 20, 2021

I think allocating target is not necessary on the === 0 fast path?

It is, but target length should be offset. This is not the case if merge is true. It must be fixed. merge should be false if the masking key is zero.

@e3dio
Copy link

e3dio commented Dec 20, 2021

I had to come back and fix that on my 1st PR 9a22689

added PR suggestion: https://github.com/websockets/ws/pull/1991/files

@lpinca
Copy link
Member Author

lpinca commented Dec 20, 2021

Will fix it later, when I get a chance.

@lpinca lpinca merged commit 35d45c2 into master Dec 20, 2021
@lpinca lpinca deleted the add/generate-mask-option branch December 20, 2021 20:02
@lpinca
Copy link
Member Author

lpinca commented Dec 20, 2021

Thank you for the reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants