Skip to content

Commit

Permalink
perf_hooks: convert maxSize to IDL value in setResourceTimingBufferSize
Browse files Browse the repository at this point in the history
ECMAScript values of WebIDL interface parameters should be converted to
IDL representatives before the actual implementation, as defined in step
11.5 of the WebIDL Overload resolution algorithm.

Refs: https://webidl.spec.whatwg.org/#dfn-create-operation-function
Refs: https://webidl.spec.whatwg.org/#es-overloads
PR-URL: #44902
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
legendecas committed Oct 13, 2022
1 parent 15f1051 commit 7cdf745
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 0 deletions.
3 changes: 3 additions & 0 deletions lib/internal/perf/performance.js
Expand Up @@ -44,6 +44,7 @@ const timerify = require('internal/perf/timerify');
const { customInspectSymbol: kInspect, kEnumerableProperty, kEmptyObject } = require('internal/util');
const { inspect } = require('util');
const { validateInternalField } = require('internal/validators');
const { convertToInt } = require('internal/webidl');

const {
getTimeOriginTimestamp
Expand Down Expand Up @@ -144,6 +145,8 @@ class Performance extends EventTarget {
if (arguments.length === 0) {
throw new ERR_MISSING_ARGS('maxSize');
}
// unsigned long
maxSize = convertToInt('maxSize', maxSize, 32);
return setResourceTimingBufferSize(maxSize);
}

Expand Down
163 changes: 163 additions & 0 deletions lib/internal/webidl.js
@@ -0,0 +1,163 @@
'use strict';

const {
MathAbs,
MathMax,
MathMin,
MathPow,
MathSign,
MathTrunc,
NumberIsNaN,
NumberMAX_SAFE_INTEGER,
NumberMIN_SAFE_INTEGER,
} = primordials;

const {
codes: {
ERR_INVALID_ARG_VALUE,
},
} = require('internal/errors');
const { kEmptyObject } = require('internal/util');

// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart
const integerPart = MathTrunc;

/* eslint-disable node-core/non-ascii-character */
// Round x to the nearest integer, choosing the even integer if it lies halfway
// between two, and choosing +0 rather than -0.
// This is different from Math.round, which rounds to the next integer in the
// direction of +∞ when the fraction portion is exactly 0.5.
/* eslint-enable node-core/non-ascii-character */
function evenRound(x) {
// Convert -0 to +0.
const i = integerPart(x) + 0;
const reminder = MathAbs(x % 1);
const sign = MathSign(i);
if (reminder === 0.5) {
return i % 2 === 0 ? i : i + sign;
}
const r = reminder < 0.5 ? i : i + sign;
// Convert -0 to +0.
if (r === 0) {
return 0;
}
return r;
}

function pow2(exponent) {
// << operates on 32 bit signed integers.
if (exponent < 31) {
return 1 << exponent;
}
if (exponent === 31) {
return 0x8000_0000;
}
if (exponent === 32) {
return 0x1_0000_0000;
}
return MathPow(2, exponent);
}

// https://tc39.es/ecma262/#eqn-modulo
// The notation “x modulo y” computes a value k of the same sign as y.
function modulo(x, y) {
const r = x % y;
// Convert -0 to +0.
if (r === 0) {
return 0;
}
return r;
}

// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
function convertToInt(name, value, bitLength, options = kEmptyObject) {
const { signed = false, enforceRange = false, clamp = false } = options;

let upperBound;
let lowerBound;
// 1. If bitLength is 64, then:
if (bitLength === 64) {
// 1.1. Let upperBound be 2^53 − 1.
upperBound = NumberMAX_SAFE_INTEGER;
// 1.2. If signedness is "unsigned", then let lowerBound be 0.
// 1.3. Otherwise let lowerBound be −2^53 + 1.
lowerBound = !signed ? 0 : NumberMIN_SAFE_INTEGER;
} else if (!signed) {
// 2. Otherwise, if signedness is "unsigned", then:
// 2.1. Let lowerBound be 0.
// 2.2. Let upperBound be 2^bitLength − 1.
lowerBound = 0;
upperBound = pow2(bitLength) - 1;
} else {
// 3. Otherwise:
// 3.1. Let lowerBound be -2^(bitLength − 1).
// 3.2. Let upperBound be 2^(bitLength − 1) − 1.
lowerBound = -pow2(bitLength - 1);
upperBound = pow2(bitLength - 1) - 1;
}

// 4. Let x be ? ToNumber(V).
let x = +value;
// 5. If x is −0, then set x to +0.
if (x === 0) {
x = 0;
}

// 6. If the conversion is to an IDL type associated with the [EnforceRange]
// extended attribute, then:
if (enforceRange) {
// 6.1. If x is NaN, +∞, or −∞, then throw a TypeError.
if (NumberIsNaN(x) || x === Infinity || x === -Infinity) {
throw new ERR_INVALID_ARG_VALUE(name, x);
}
// 6.2. Set x to IntegerPart(x).
x = integerPart(x);

// 6.3. If x < lowerBound or x > upperBound, then throw a TypeError.
if (x < lowerBound || x > upperBound) {
throw new ERR_INVALID_ARG_VALUE(name, x);
}

// 6.4. Return x.
return x;
}

// 7. If x is not NaN and the conversion is to an IDL type associated with
// the [Clamp] extended attribute, then:
if (clamp && !NumberIsNaN(x)) {
// 7.1. Set x to min(max(x, lowerBound), upperBound).
x = MathMin(MathMax(x, lowerBound), upperBound);

// 7.2. Round x to the nearest integer, choosing the even integer if it
// lies halfway between two, and choosing +0 rather than −0.
x = evenRound(x);

// 7.3. Return x.
return x;
}

// 8. If x is NaN, +0, +∞, or −∞, then return +0.
if (NumberIsNaN(x) || x === 0 || x === Infinity || x === -Infinity) {
return 0;
}

// 9. Set x to IntegerPart(x).
x = integerPart(x);

// 10. Set x to x modulo 2^bitLength.
x = modulo(x, pow2(bitLength));

// 11. If signedness is "signed" and x ≥ 2^(bitLength − 1), then return x −
// 2^bitLength.
if (signed && x >= pow2(bitLength - 1)) {
return x - pow2(bitLength);
}

// 12. Otherwise, return x.
return x;
}

module.exports = {
convertToInt,
evenRound,
};
1 change: 1 addition & 0 deletions test/parallel/test-bootstrap-modules.js
Expand Up @@ -149,6 +149,7 @@ const expectedModules = new Set([
'NativeModule internal/validators',
'NativeModule internal/vm/module',
'NativeModule internal/wasm_web_api',
'NativeModule internal/webidl',
'NativeModule internal/webstreams/adapters',
'NativeModule internal/webstreams/compression',
'NativeModule internal/webstreams/encoding',
Expand Down
58 changes: 58 additions & 0 deletions test/parallel/test-internal-webidl-converttoint.js
@@ -0,0 +1,58 @@
// Flags: --expose-internals
'use strict';

require('../common');
const assert = require('assert');
const { convertToInt, evenRound } = require('internal/webidl');

assert.strictEqual(evenRound(-0.5), 0);
assert.strictEqual(evenRound(0.5), 0);
assert.strictEqual(evenRound(-1.5), -2);
assert.strictEqual(evenRound(1.5), 2);
assert.strictEqual(evenRound(3.4), 3);
assert.strictEqual(evenRound(4.6), 5);
assert.strictEqual(evenRound(5), 5);
assert.strictEqual(evenRound(6), 6);

// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
assert.strictEqual(convertToInt('x', 0, 64), 0);
assert.strictEqual(convertToInt('x', 1, 64), 1);
assert.strictEqual(convertToInt('x', -0.5, 64), 0);
assert.strictEqual(convertToInt('x', -0.5, 64, { signed: true }), 0);
assert.strictEqual(convertToInt('x', -1.5, 64, { signed: true }), -1);

// EnforceRange
const OutOfRangeValues = [ NaN, Infinity, -Infinity, 2 ** 53, -(2 ** 53) ];
for (const value of OutOfRangeValues) {
assert.throws(() => convertToInt('x', value, 64, { enforceRange: true }), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_VALUE',
});
}

// Out of range: clamp
assert.strictEqual(convertToInt('x', NaN, 64, { clamp: true }), 0);
assert.strictEqual(convertToInt('x', Infinity, 64, { clamp: true }), Number.MAX_SAFE_INTEGER);
assert.strictEqual(convertToInt('x', -Infinity, 64, { clamp: true }), 0);
assert.strictEqual(convertToInt('x', -Infinity, 64, { signed: true, clamp: true }), Number.MIN_SAFE_INTEGER);
assert.strictEqual(convertToInt('x', 0x1_0000_0000, 32, { clamp: true }), 0xFFFF_FFFF);
assert.strictEqual(convertToInt('x', 0xFFFF_FFFF, 32, { clamp: true }), 0xFFFF_FFFF);
assert.strictEqual(convertToInt('x', 0x8000_0000, 32, { clamp: true, signed: true }), 0x7FFF_FFFF);
assert.strictEqual(convertToInt('x', 0xFFFF_FFFF, 32, { clamp: true, signed: true }), 0x7FFF_FFFF);
assert.strictEqual(convertToInt('x', 0.5, 64, { clamp: true }), 0);
assert.strictEqual(convertToInt('x', 1.5, 64, { clamp: true }), 2);
assert.strictEqual(convertToInt('x', -0.5, 64, { clamp: true }), 0);
assert.strictEqual(convertToInt('x', -0.5, 64, { signed: true, clamp: true }), 0);
assert.strictEqual(convertToInt('x', -1.5, 64, { signed: true, clamp: true }), -2);

// Out of range, step 8.
assert.strictEqual(convertToInt('x', NaN, 64), 0);
assert.strictEqual(convertToInt('x', Infinity, 64), 0);
assert.strictEqual(convertToInt('x', -Infinity, 64), 0);
assert.strictEqual(convertToInt('x', 0x1_0000_0000, 32), 0);
assert.strictEqual(convertToInt('x', 0x1_0000_0001, 32), 1);
assert.strictEqual(convertToInt('x', 0xFFFF_FFFF, 32), 0xFFFF_FFFF);

// Out of range, step 11.
assert.strictEqual(convertToInt('x', 0x8000_0000, 32, { signed: true }), -0x8000_0000);
assert.strictEqual(convertToInt('x', 0xFFF_FFFF, 32, { signed: true }), 0xFFF_FFFF);
11 changes: 11 additions & 0 deletions test/parallel/test-performance-resourcetimingbuffersize.js
Expand Up @@ -30,6 +30,17 @@ const initiatorType = '';
const cacheMode = '';

async function main() {
// Invalid buffer size values are converted to 0.
const invalidValues = [ null, undefined, true, false, -1, 0.5, Infinity, NaN, '', 'foo', {}, [], () => {} ];
for (const value of invalidValues) {
performance.setResourceTimingBufferSize(value);
performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode);
assert.strictEqual(performance.getEntriesByType('resource').length, 0);
performance.clearResourceTimings();
}
// Wait for the buffer full event to be cleared.
await waitBufferFullEvent();

performance.setResourceTimingBufferSize(1);
performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode);
// Trigger a resourcetimingbufferfull event.
Expand Down

0 comments on commit 7cdf745

Please sign in to comment.