Skip to content

Commit

Permalink
⚡️ Faster instantiation of internet-related arbitraries (#5402)
Browse files Browse the repository at this point in the history
**Description**

<!-- Please provide a short description and potentially linked issues
justifying the need for this PR -->

These arbitraries used to be relatively costly to instantiate and
re-instantiate. While generally not a huge concern, when used in
multiple properties and arbitraries this small cost (around 10ms
measured for some of them) could be considered as far too much.

Tis PR is responsible to drop this time for later re-instantiations of
the same internet-related arbitrary (and also other internet-related
arbitraries).

The measured perfomance uplift is impressive:

```txt
┌──────────────────────────────────────────────────────────────────┬─────────────┬────────────────────┬────────────┬─────────┐
│ Task Name                                                        │ ops/sec     │ Average Time (ns)  │ Margin     │ Samples │
├──────────────────────────────────────────────────────────────────┼─────────────┼────────────────────┼────────────┼─────────┤
│ 'emailAddress [property] on [email protected]'                   │ '91'        │ 10889826.509999111 │ '±1.25%'   │ 100     │
│ 'emailAddress [property] on fast-check@main'                     │ '92'        │ 10844747.599998374 │ '±0.96%'   │ 100     │
│ 'emailAddress [property] on fast-check@extra'                    │ '767'       │ 1303581.2200023793 │ '±4.36%'   │ 100     │
│ '—'                                                              │ '—'         │ '—'                │ '—'        │ '—'     │
│ 'webUrl [property] on [email protected]'                         │ '184'       │ 5407323.470002157  │ '±1.87%'   │ 100     │
│ 'webUrl [property] on fast-check@main'                           │ '186'       │ 5375564.689997118  │ '±1.65%'   │ 100     │
│ 'webUrl [property] on fast-check@extra'                          │ '1,309'     │ 763888.6699988507  │ '±5.65%'   │ 100     │
│ '—'                                                              │ '—'         │ '—'                │ '—'        │ '—'     │
│ 'domain [property] on [email protected]'                         │ '177'       │ 5625156.560001778  │ '±1.35%'   │ 100     │
│ 'domain [property] on fast-check@main'                           │ '176'       │ 5655945.269999793  │ '±1.44%'   │ 100     │
│ 'domain [property] on fast-check@extra'                          │ '2,158'     │ 463293.92999759875 │ '±8.30%'   │ 100     │
│ '—'                                                              │ '—'         │ '—'                │ '—'        │ '—'     │
│ 'webPath [property] on [email protected]'                        │ '2,654'     │ 376671.95000103675 │ '±11.80%'  │ 100     │
│ 'webPath [property] on fast-check@main'                          │ '2,710'     │ 368881.23999524396 │ '±11.18%'  │ 100     │
│ 'webPath [property] on fast-check@extra'                         │ '3,553'     │ 281420.0099988375  │ '±8.83%'   │ 100     │
│ '—'                                                              │ '—'         │ '—'                │ '—'        │ '—'     │
│ '—'                                                              │ '—'         │ '—'                │ '—'        │ '—'     │
│ 'emailAddress [init] on [email protected]'                       │ '102'       │ 9717271.019999754  │ '±0.93%'   │ 100     │
│ 'emailAddress [init] on fast-check@main'                         │ '102'       │ 9727080.830000922  │ '±0.90%'   │ 100     │
│ 'emailAddress [init] on fast-check@extra'                        │ '208,495'   │ 4796.270001097582  │ '±30.89%'  │ 100     │
│ '—'                                                              │ '—'         │ '—'                │ '—'        │ '—'     │
│ 'webUrl [init] on [email protected]'                             │ '203'       │ 4915872.42000096   │ '±5.11%'   │ 100     │
│ 'webUrl [init] on fast-check@main'                               │ '224'       │ 4459312.690000515  │ '±1.53%'   │ 100     │
│ 'webUrl [init] on fast-check@extra'                              │ '129,025'   │ 7750.43000059668   │ '±28.60%'  │ 100     │
│ '—'                                                              │ '—'         │ '—'                │ '—'        │ '—'     │
│ 'domain [init] on [email protected]'                             │ '193'       │ 5162941.229996504  │ '±1.27%'   │ 100     │
│ 'domain [init] on fast-check@main'                               │ '194'       │ 5151196.849999833  │ '±1.27%'   │ 100     │
│ 'domain [init] on fast-check@extra'                              │ '311,647'   │ 3208.749998593703  │ '±35.53%'  │ 100     │
│ '—'                                                              │ '—'         │ '—'                │ '—'        │ '—'     │
│ 'webPath [init] on [email protected]'                            │ '11,708'    │ 85407.009999617    │ '±34.85%'  │ 100     │
│ 'webPath [init] on fast-check@main'                              │ '14,022'    │ 71313.10000200756  │ '±29.80%'  │ 100     │
│ 'webPath [init] on fast-check@extra'                             │ '319,764'   │ 3127.3000017972663 │ '±35.54%'  │ 100     │
└──────────────────────────────────────────────────────────────────┴─────────────┴────────────────────┴────────────┴─────────┘
```

The initialization time (means second initialization of the arbitrary
and later) decreased by a significant factor: from 200 per second to
130k per second for webUrl. This huge improvement also drove significant
performance improvements on properties running against such arbitraries
by moving from 180 executions per seconds to 1300 (with executions
meaning full property execution and thus 100 runs of the predicate).

<!-- * Your PR is fixing a bug or regression? Check for existing issues
related to this bug and link them -->
<!-- * Your PR is adding a new feature? Make sure there is a related
issue or discussion attached to it -->

<!-- You can provide any additional context to help into understanding
what's this PR is attempting to solve: reproduction of a bug, code
snippets... -->

**Checklist** — _Don't delete this checklist and make sure you do the
following before opening the PR_

- [x] The name of my PR follows [gitmoji](https://gitmoji.dev/)
specification
- [x] My PR references one of several related issues (if any)
- [x] New features or breaking changes must come with an associated
Issue or Discussion
- [x] My PR does not add any new dependency without an associated Issue
or Discussion
- [x] My PR includes bumps details, please run `yarn bump` and flag the
impacts properly
- [x] My PR adds relevant tests and they would have failed without my PR
(when applicable)

<!-- More about contributing at
https://github.com/dubzzz/fast-check/blob/main/CONTRIBUTING.md -->

**Advanced**

<!-- How to fill the advanced section is detailed below! -->

- [x] Category: ⚡️ Improve performance
- [x] Impacts: Faster internet related arbitraries at instantiation time

<!-- [Category] Please use one of the categories below, it will help us
into better understanding the urgency of the PR -->
<!-- * ✨ Introduce new features -->
<!-- * 📝 Add or update documentation -->
<!-- * ✅ Add or update tests -->
<!-- * 🐛 Fix a bug -->
<!-- * 🏷️ Add or update types -->
<!-- * ⚡️ Improve performance -->
<!-- * _Other(s):_ ... -->

<!-- [Impacts] Please provide a comma separated list of the potential
impacts that might be introduced by this change -->
<!-- * Generated values: Can your change impact any of the existing
generators in terms of generated values, if so which ones? when? -->
<!-- * Shrink values: Can your change impact any of the existing
generators in terms of shrink values, if so which ones? when? -->
<!-- * Performance: Can it require some typings changes on user side?
Please give more details -->
<!-- * Typings: Is there a potential performance impact? In which cases?
-->
  • Loading branch information
dubzzz authored Nov 2, 2024
1 parent e728907 commit 140e3ab
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { fullUnicode } from '../../fullUnicode';
import type { Arbitrary } from '../../../check/arbitrary/definition/Arbitrary';
import { oneof } from '../../oneof';
import { mapToConstant } from '../../mapToConstant';
import { safeCharCodeAt, safeNumberToString, encodeURIComponent } from '../../../utils/globals';
import { safeCharCodeAt, safeNumberToString, encodeURIComponent, safeMapGet, safeMapSet } from '../../../utils/globals';

const SMap = Map;
const safeStringFromCharCode = String.fromCharCode;

/** @internal */
Expand Down Expand Up @@ -32,18 +33,56 @@ function percentCharArbUnmapper(value: unknown): string {
/** @internal */
const percentCharArb = fullUnicode().map(percentCharArbMapper, percentCharArbUnmapper);

let lowerAlphaArbitrary: Arbitrary<string> | undefined = undefined;

/** @internal */
export const buildLowerAlphaArbitrary = (others: string[]): Arbitrary<string> =>
mapToConstant(lowerCaseMapper, { num: others.length, build: (v) => others[v] });
export function getOrCreateLowerAlphaArbitrary(): Arbitrary<string> {
if (lowerAlphaArbitrary === undefined) {
lowerAlphaArbitrary = mapToConstant(lowerCaseMapper);
}
return lowerAlphaArbitrary;
}

let lowerAlphaNumericArbitraries: Map<string, Arbitrary<string>> | undefined = undefined;

/** @internal */
export const buildLowerAlphaNumericArbitrary = (others: string[]): Arbitrary<string> =>
mapToConstant(lowerCaseMapper, numericMapper, { num: others.length, build: (v) => others[v] });
export function getOrCreateLowerAlphaNumericArbitrary(others: string): Arbitrary<string> {
if (lowerAlphaNumericArbitraries === undefined) {
lowerAlphaNumericArbitraries = new SMap();
}
let match = safeMapGet(lowerAlphaNumericArbitraries, others);
if (match === undefined) {
match = mapToConstant(lowerCaseMapper, numericMapper, {
num: others.length,
build: (v) => others[v],
});
safeMapSet(lowerAlphaNumericArbitraries, others, match);
}
return match;
}

/** @internal */
export const buildAlphaNumericArbitrary = (others: string[]): Arbitrary<string> =>
mapToConstant(lowerCaseMapper, upperCaseMapper, numericMapper, { num: others.length, build: (v) => others[v] });
function buildAlphaNumericArbitrary(others: string): Arbitrary<string> {
return mapToConstant(lowerCaseMapper, upperCaseMapper, numericMapper, {
num: others.length,
build: (v) => others[v],
});
}

let alphaNumericPercentArbitraries: Map<string, Arbitrary<string>> | undefined = undefined;

/** @internal */
export const buildAlphaNumericPercentArbitrary = (others: string[]): Arbitrary<string> =>
oneof({ weight: 10, arbitrary: buildAlphaNumericArbitrary(others) }, { weight: 1, arbitrary: percentCharArb });
export function getOrCreateAlphaNumericPercentArbitrary(others: string): Arbitrary<string> {
if (alphaNumericPercentArbitraries === undefined) {
alphaNumericPercentArbitraries = new SMap();
}
let match = safeMapGet(alphaNumericPercentArbitraries, others);
if (match === undefined) {
match = oneof(
{ weight: 10, arbitrary: buildAlphaNumericArbitrary(others) },
{ weight: 1, arbitrary: percentCharArb },
);
safeMapSet(alphaNumericPercentArbitraries, others, match);
}
return match;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { Arbitrary } from '../../../check/arbitrary/definition/Arbitrary';
import { buildAlphaNumericPercentArbitrary } from './CharacterRangeArbitraryBuilder';
import { stringOf } from '../../stringOf';
import { getOrCreateAlphaNumericPercentArbitrary } from './CharacterRangeArbitraryBuilder';
import { string } from '../../string';
import type { SizeForArbitrary } from '../helpers/MaxLengthFromMinLength';

/** @internal */
export function buildUriQueryOrFragmentArbitrary(size: Exclude<SizeForArbitrary, 'max'>): Arbitrary<string> {
// query = *( pchar / "/" / "?" )
// fragment = *( pchar / "/" / "?" )
const others = ['-', '.', '_', '~', '!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '=', ':', '@', '/', '?'];
return stringOf(buildAlphaNumericPercentArbitrary(others), { size });
return string({ unit: getOrCreateAlphaNumericPercentArbitrary("-._~!$&'()*+,;=:@/?"), size });
}
16 changes: 8 additions & 8 deletions packages/fast-check/src/arbitrary/domain.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { array } from './array';
import {
buildLowerAlphaArbitrary,
buildLowerAlphaNumericArbitrary,
getOrCreateLowerAlphaArbitrary,
getOrCreateLowerAlphaNumericArbitrary,
} from './_internals/builders/CharacterRangeArbitraryBuilder';
import { option } from './option';
import { stringOf } from './stringOf';
import { string } from './string';
import { tuple } from './tuple';
import type { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { filterInvalidSubdomainLabel } from './_internals/helpers/InvalidSubdomainLabelFiIter';
Expand All @@ -31,8 +31,8 @@ function toSubdomainLabelUnmapper(value: unknown): [string, [string, string] | n

/** @internal */
function subdomainLabel(size: Size) {
const alphaNumericArb = buildLowerAlphaNumericArbitrary([]);
const alphaNumericHyphenArb = buildLowerAlphaNumericArbitrary(['-']);
const alphaNumericArb = getOrCreateLowerAlphaNumericArbitrary('');
const alphaNumericHyphenArb = getOrCreateLowerAlphaNumericArbitrary('-');
// Rq: maxLength = 61 because max length of a label is 63 according to RFC 1034
// and we add 2 characters to this generated value
// According to RFC 1034 (confirmed by RFC 1035):
Expand All @@ -48,7 +48,7 @@ function subdomainLabel(size: Size) {
// restriction on the first character is relaxed to allow either a letter or a digit. Host software MUST support this more liberal syntax."
return tuple(
alphaNumericArb,
option(tuple(stringOf(alphaNumericHyphenArb, { size, maxLength: 61 }), alphaNumericArb)),
option(tuple(string({ unit: alphaNumericHyphenArb, size, maxLength: 61 }), alphaNumericArb)),
)
.map(toSubdomainLabelMapper, toSubdomainLabelUnmapper)
.filter(filterInvalidSubdomainLabel);
Expand Down Expand Up @@ -118,8 +118,8 @@ export function domain(constraints: DomainConstraints = {}): Arbitrary<string> {
// A list of public suffixes can be found here: https://publicsuffix.org/list/public_suffix_list.dat
// our current implementation does not follow this list and generate a fully randomized suffix
// which is probably not in this list (probability would be low)
const alphaNumericArb = buildLowerAlphaArbitrary([]);
const publicSuffixArb = stringOf(alphaNumericArb, { minLength: 2, maxLength: 63, size: resolvedSizeMinusOne });
const lowerAlphaArb = getOrCreateLowerAlphaArbitrary();
const publicSuffixArb = string({ unit: lowerAlphaArb, minLength: 2, maxLength: 63, size: resolvedSizeMinusOne });
return (
// labels have between 1 and 63 characters
// domains are made of dot-separated labels and have up to 255 characters so that are made of up-to 128 labels
Expand Down
10 changes: 5 additions & 5 deletions packages/fast-check/src/arbitrary/emailAddress.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { array } from './array';
import { buildLowerAlphaNumericArbitrary } from './_internals/builders/CharacterRangeArbitraryBuilder';
import { getOrCreateLowerAlphaNumericArbitrary } from './_internals/builders/CharacterRangeArbitraryBuilder';
import { domain } from './domain';
import { stringOf } from './stringOf';
import { string } from './string';
import { tuple } from './tuple';
import type { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import type { SizeForArbitrary } from './_internals/helpers/MaxLengthFromMinLength';
Expand Down Expand Up @@ -71,15 +71,15 @@ export interface EmailAddressConstraints {
* @public
*/
export function emailAddress(constraints: EmailAddressConstraints = {}): Arbitrary<string> {
const others = ['!', '#', '$', '%', '&', "'", '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~'];
const atextArb = buildLowerAlphaNumericArbitrary(others);
const atextArb = getOrCreateLowerAlphaNumericArbitrary("!#$%&'*+-/=?^_`{|}~");
const localPartArb = adapter(
// Maximal length for the output of dotMapper is 64,
// In other words:
// - `stringOf(atextArb, ...)` cannot produce values having more than 64 characters
// - `array(...)` cannot produce more than 32 values
array(
stringOf(atextArb, {
string({
unit: atextArb,
minLength: 1,
maxLength: 64,
size: constraints.size,
Expand Down
7 changes: 3 additions & 4 deletions packages/fast-check/src/arbitrary/webAuthority.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { buildAlphaNumericPercentArbitrary } from './_internals/builders/CharacterRangeArbitraryBuilder';
import { getOrCreateAlphaNumericPercentArbitrary } from './_internals/builders/CharacterRangeArbitraryBuilder';
import { constant } from './constant';
import { domain } from './domain';
import { ipV4 } from './ipV4';
Expand All @@ -8,14 +8,13 @@ import { ipV6 } from './ipV6';
import { nat } from './nat';
import { oneof } from './oneof';
import { option } from './option';
import { stringOf } from './stringOf';
import { string } from './string';
import { tuple } from './tuple';
import type { SizeForArbitrary } from './_internals/helpers/MaxLengthFromMinLength';

/** @internal */
function hostUserInfo(size: SizeForArbitrary): Arbitrary<string> {
const others = ['-', '.', '_', '~', '!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '=', ':'];
return stringOf(buildAlphaNumericPercentArbitrary(others), { size });
return string({ unit: getOrCreateAlphaNumericPercentArbitrary("-._~!$&'()*+,;=:"), size });
}

/** @internal */
Expand Down
7 changes: 3 additions & 4 deletions packages/fast-check/src/arbitrary/webSegment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
import { buildAlphaNumericPercentArbitrary } from './_internals/builders/CharacterRangeArbitraryBuilder';
import { stringOf } from './stringOf';
import { getOrCreateAlphaNumericPercentArbitrary } from './_internals/builders/CharacterRangeArbitraryBuilder';
import { string } from './string';
import type { SizeForArbitrary } from './_internals/helpers/MaxLengthFromMinLength';

/**
Expand Down Expand Up @@ -31,6 +31,5 @@ export interface WebSegmentConstraints {
export function webSegment(constraints: WebSegmentConstraints = {}): Arbitrary<string> {
// pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
// segment = *pchar
const others = ['-', '.', '_', '~', '!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '=', ':', '@'];
return stringOf(buildAlphaNumericPercentArbitrary(others), { size: constraints.size });
return string({ unit: getOrCreateAlphaNumericPercentArbitrary("-._~!$&'()*+,;=:@"), size: constraints.size });
}
31 changes: 31 additions & 0 deletions packages/fast-check/src/utils/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,37 @@ export function safeGet<T extends object, U>(instance: WeakMap<T, U>, key: T): U
return safeApply(untouchedGet, instance, [key]);
}

// Map

const untouchedMapSet = Map.prototype.set;
const untouchedMapGet = Map.prototype.get;
function extractMapSet(instance: Map<unknown, unknown>) {
try {
return instance.set;
} catch (err) {
return undefined;
}
}
function extractMapGet(instance: Map<unknown, unknown>) {
try {
return instance.get;
} catch (err) {
return undefined;
}
}
export function safeMapSet<T, U>(instance: Map<T, U>, key: T, value: U): Map<T, U> {
if (extractMapSet(instance) === untouchedMapSet) {
return instance.set(key, value);
}
return safeApply(untouchedMapSet, instance, [key, value]);
}
export function safeMapGet<T, U>(instance: Map<T, U>, key: T): U | undefined {
if (extractMapGet(instance) === untouchedMapGet) {
return instance.get(key);
}
return safeApply(untouchedMapGet, instance, [key]);
}

// String

const untouchedSplit: (separator: string | RegExp, limit?: number) => string[] = String.prototype.split;
Expand Down

0 comments on commit 140e3ab

Please sign in to comment.