diff --git a/.github/workflows/test-linux-perfetto.yml b/.github/workflows/test-linux-perfetto.yml
new file mode 100644
index 000000000000..70a92202c1c7
--- /dev/null
+++ b/.github/workflows/test-linux-perfetto.yml
@@ -0,0 +1,70 @@
+name: Test Linux (with Perfetto)
+
+on:
+ workflow_dispatch:
+ pull_request:
+ # Only targeting paths specific to the vendored version of Perfetto, `test-shared`
+ # is taking care of rest of the coverage.
+ paths:
+ - .github/workflows/test-linux-perfetto.yml
+ - common.gypi
+ - configure.py
+ - deps/perfetto/**
+ - node.gyp
+ - node.gypi
+ - tools/v8_gypfiles/v8.gyp
+ types: [opened, synchronize, reopened, ready_for_review]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+env:
+ PYTHON_VERSION: '3.14'
+ FLAKY_TESTS: keep_retrying
+ CLANG_VERSION: '19'
+ CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang-19
+ CXX: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang++-19
+ SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }}
+ SCCACHE_IDLE_TIMEOUT: '0'
+ RUSTC_VERSION: '1.86'
+
+permissions:
+ contents: read
+
+jobs:
+ test-perfetto:
+ if: github.event.pull_request.draft == false
+ runs-on: ubuntu-24.04-arm
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ path: node
+ - name: Install Clang ${{ env.CLANG_VERSION }}
+ uses: ./node/.github/actions/install-clang
+ with:
+ clang-version: ${{ env.CLANG_VERSION }}
+ - name: Install Rust ${{ env.RUSTC_VERSION }}
+ run: |
+ rustup override set "$RUSTC_VERSION"
+ rustup --version
+ - name: Set up Python ${{ env.PYTHON_VERSION }}
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+ allow-prereleases: true
+ - name: Set up sccache
+ if: github.base_ref == 'main' || github.ref_name == 'main'
+ uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
+ with:
+ version: v0.17.0
+ - name: Build
+ working-directory: node
+ run: make build-ci -j4 V=1 CONFIG_FLAGS="--error-on-warn --v8-enable-temporal-support --with-perfetto"
+ - name: Test
+ working-directory: node
+ run: make test-ci -j1 V=1 TEST_CI_ARGS="-p actions --measure-flakiness 9"
+ - name: Ensure running tests did not cause any change in the tree
+ working-directory: node
+ run: git add -A && git diff --name-only --exit-code --staged
diff --git a/.github/workflows/test-shared.yml b/.github/workflows/test-shared.yml
index 1f1afdd89dd2..f7483103016f 100644
--- a/.github/workflows/test-shared.yml
+++ b/.github/workflows/test-shared.yml
@@ -29,10 +29,13 @@ on:
- deps/nghttp2/**
- deps/ngtcp2/**
- deps/openssl/*/**
+ - deps/perfetto/**
- deps/simdjson/**
- deps/sqlite/**
- deps/uv/**
- deps/uvwasi/**
+ - deps/v8/third_party/abseil-cpp/**
+ - deps/v8/third_party/highway/**
- deps/zlib/**
- deps/zstd/**
- doc/**
@@ -82,10 +85,13 @@ on:
- deps/nghttp2/**
- deps/ngtcp2/**
- deps/openssl/*/**
+ - deps/perfetto/**
- deps/simdjson/**
- deps/sqlite/**
- deps/uv/**
- deps/uvwasi/**
+ - deps/v8/third_party/abseil-cpp/**
+ - deps/v8/third_party/highway/**
- deps/zlib/**
- deps/zstd/**
- doc/**
diff --git a/.gitignore b/.gitignore
index 2a7ce3337021..9277cdf090f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -121,6 +121,7 @@ tools/*/*.i.tmp
/*.xml
/v8*-tap.json
/node_trace.*.log
+/node_trace.*.pftrace
# coverage related
/gcovr
/build
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da927c538e27..f79cc8671d8c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,7 +41,8 @@ release.
-26.8.2
+26.9.0
+26.8.2
26.8.1
26.8.0
26.7.0
diff --git a/LICENSE b/LICENSE
index 9cc3315dd388..67cd2feb69a9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -2600,29 +2600,6 @@ The externally maintained libraries used by Node.js are:
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
-- large_pages, located at src/large_pages, is licensed as follows:
- """
- Copyright (C) 2018 Intel Corporation
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"),
- to deal in the Software without restriction, including without limitation
- the rights to use, copy, modify, merge, publish, distribute, sublicense,
- and/or sell copies of the Software, and to permit persons to whom
- the Software is furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included
- in all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
- OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
- THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
- OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
- ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
- OR OTHER DEALINGS IN THE SOFTWARE.
- """
-
- caja, located at lib/internal/freeze_intrinsics.js, is licensed as follows:
"""
Adapted from SES/Caja - Copyright (C) 2011 Google Inc.
diff --git a/Makefile b/Makefile
index 86410b7f9cbb..afa55b2da33e 100644
--- a/Makefile
+++ b/Makefile
@@ -1319,10 +1319,13 @@ ifeq ($(SKIP_SHARED_DEPS), 1)
$(RM) -r $(TARNAME)/deps/ngtcp2
find $(TARNAME)/deps/openssl -maxdepth 1 -type f ! -name 'nodejs-openssl.cnf' -exec $(RM) {} +
find $(TARNAME)/deps/openssl -mindepth 1 -maxdepth 1 -type d -exec $(RM) -r {} +
+ $(RM) -r $(TARNAME)/deps/perfetto
$(RM) -r $(TARNAME)/deps/simdjson
$(RM) -r $(TARNAME)/deps/sqlite
$(RM) -r $(TARNAME)/deps/uv
$(RM) -r $(TARNAME)/deps/uvwasi
+ $(RM) -r $(TARNAME)/deps/v8/third_party/abseil-cpp
+ $(RM) -r $(TARNAME)/deps/v8/third_party/highway
$(RM) -r $(TARNAME)/deps/zlib
$(RM) -r $(TARNAME)/deps/zstd
else
diff --git a/SECURITY.md b/SECURITY.md
index 6a944e859015..17d35fd4432f 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -173,6 +173,28 @@ are not part of the Node.js documented API surface, are not enabled by
default in production builds, and may have incomplete implementations or
missing security hardening.
+### Security triage dispositions
+
+When triaging a report, the project classifies it into one of the following
+dispositions:
+
+* **Vulnerability**: A Node.js defect that is exploitable across a
+ Node.js-owned security boundary and meets the criteria under
+ [What constitutes a vulnerability](#what-constitutes-a-vulnerability),
+ including any applicable DoS criteria.
+* **Security-interest bug**: A real Node.js defect, or an API behavior likely
+ to cause security bugs in applications, that is not itself a vulnerability
+ under this threat model. These are fixed as regular bugs and do not
+ automatically receive a CVE, but should still be reported privately first
+ when they affect a common security control such as protocol interpretation,
+ permission enforcement, or certificate/TLS decisions.
+* **Common bug**: A correctness, robustness, or crash issue without a
+ Node.js-owned security boundary or a realistic cross-boundary attacker benefit.
+* **Invalid / out of scope**: A bug report that meets one of these criteria:
+ * Cannot be reproduced
+ * Is not a Node.js defect (e.g., an application bug)
+ * Is excluded by policy (e.g., experimental features)
+
### What constitutes a vulnerability
Being able to cause the following through control of the elements that Node.js
@@ -351,6 +373,19 @@ the community they pose.
* Code is trusted by Node.js. Therefore any scenario that requires a malicious
third-party module cannot result in a vulnerability in Node.js.
+#### Same-process self-harm
+
+* Node.js trusts the code it is asked to run. A defect that can only be
+ triggered by JavaScript, WASM, native, addon, FFI, or dependency code already
+ executing in the target process is not a Node.js vulnerability merely because
+ that code can crash, corrupt, or confuse the process it already controls.
+ This includes forging an internal handle, reflecting or overwriting an
+ internal `Symbol()`, installing a `Symbol.hasInstance` hook, or reaching into
+ an internal binding.
+* Such issues may still be fixed as common bugs. They become vulnerabilities
+ only if the same defect is reachable from an element Node.js does not trust
+ without relying on an application-created boundary.
+
#### Prototype Pollution Attacks (CWE-1321)
* Node.js trusts the inputs provided to it by application code.
@@ -467,6 +502,21 @@ resources a Node.js process may access. It is designed to reduce the blast
radius of mistakes in trusted application code, **not** to act as a security
boundary against intentional misuse or a compromised process.
+Permission Model reports are triaged in three lanes:
+
+* **Vulnerability**: An element Node.js does not trust crosses a Node.js-owned
+ permission check without trusted code already executing in the protected
+ process.
+* **Security-interest bug**: Trusted application code uses documented, stable
+ APIs as intended, but Node.js fails to enforce a documented permission
+ invariant consistently — for example, one API enforces a check that an
+ equivalent API omits. These are fixed as hardening and are not automatically
+ CVE-class, because the Permission Model is not a sandbox against malicious
+ same-process code.
+* **Excluded**: Intentional misuse by code already running in the process,
+ operator-selected flags, a modified `execArgv`/`env`, or any expectation that
+ the Permission Model sandboxes malicious same-process code.
+
The following are **not** vulnerabilities in Node.js:
* **Operator-controlled flags**: Behavior unlocked by flags the operator
@@ -487,9 +537,14 @@ The following are **not** vulnerabilities in Node.js:
symlinks that resolve within the allowed list are similarly not considered
permission model bypasses.
-* **`worker_threads` with modified `execArgv`**: Workers inherit the permission
- restrictions of their parent process. Passing an empty or modified `execArgv`
- to a worker does not grant it additional permissions.
+* **`worker_threads` and the permission model**: Creating a worker is gated by
+ `--allow-worker`. A worker started with a modified `execArgv` or `env` may
+ start without inheriting the parent's permission configuration, so the
+ permission model does not reliably propagate to such workers. Because worker
+ creation already requires `--allow-worker`, and the Permission Model is not a
+ sandbox against intentional misuse by trusted code, this is not considered a
+ vulnerability. Applications that rely on the Permission Model must not grant
+ `--allow-worker` to code they do not trust.
#### QUIC and HTTP/3
diff --git a/benchmark/README.md b/benchmark/README.md
index 2f52a44f251a..78a55fba453f 100644
--- a/benchmark/README.md
+++ b/benchmark/README.md
@@ -10,6 +10,7 @@ directory, see [the guide on benchmarks](../doc/contributing/writing-and-running
## Table of Contents
* [File tree structure](#file-tree-structure)
+* [`node:bench` evaluation tools](#nodebench-evaluation-tools)
* [Common API](#common-api)
## File tree structure
@@ -34,7 +35,7 @@ The actual benchmark scripts should be placed in their corresponding
directories.
* `_benchmark_progress.js`: implements the progress bar displayed
- when running `compare.js`
+ when running `compare.js` and `scatter.js`
* `_cli.js`: parses the command line arguments passed to `compare.js`,
`run.js` and `scatter.js`
* `_cli.R`: parses the command line arguments passed to `compare.R`
@@ -44,15 +45,92 @@ directories.
* `common.js`: see [Common API](#common-api).
* `compare.js`: command line tool for comparing performance between different
Node.js binaries.
+* `compare-node-bench.js`: parallel comparison tool for explicit `node:bench`
+ files. It does not change or invoke `compare.js`.
* `compare.R`: R script for statistically analyzing the output of
`compare.js`
* `run.js`: command line tool for running individual benchmark suite(s).
* `scatter.js`: command line tool for comparing the performance
between different parameters in benchmark configurations,
- for example to analyze the time complexity.
+ for example to analyze the time complexity. Pass `--analyze` to
+ summarize the results without R.
+* `scatter-node-bench.js`: parallel scatter-data tool for an explicit
+ `node:bench` file. It does not change or invoke `scatter.js`.
* `scatter.R`: R script for visualizing the output of `scatter.js` with
scatter plots.
+## `node:bench` evaluation tools
+
+The `compare-node-bench.js` and `scatter-node-bench.js` tools run explicit
+`node:bench` files without changing the existing benchmark framework or its
+tools. Each repeated observation for a benchmark identity is collected by a
+separate process invocation with one measured sample. Benchmarks declared in
+the same file still execute serially in that process and can share JIT, garbage
+collector, heap, and cache state. This differs from legacy configuration-level
+process isolation and must be considered when comparing the frameworks.
+
+Compare two binaries and analyze the compatible CSV using `compare.R`:
+
+```console
+./node benchmark/compare-node-bench.js \
+ --old ./node-main --new ./node-pr --runs 30 -- \
+ benchmark/crypto/_create-hash.node-bench.js > compare-node-bench.csv
+Rscript benchmark/compare.R < compare-node-bench.csv
+```
+
+Pass `--analyze` to run the same Welch analysis inline. `--max-regression N`
+implies `--analyze` and makes the command fail only when the Holm-Bonferroni
+adjusted one-sided p-value against the `N%` threshold is below 0.05 and the full
+95% confidence interval is worse than `-N%`. Requiring both conditions prevents
+a noisy point estimate from failing a regression gate.
+
+```console
+./node benchmark/compare-node-bench.js \
+ --old ./node-main --new ./node-pr --runs 30 \
+ --max-regression 5 -- benchmark/crypto/_create-hash.node-bench.js
+```
+
+Collect parameter data for the parallel buffer benchmark and plot it using
+`scatter.R`:
+
+```console
+./node benchmark/scatter-node-bench.js --node ./node --runs 30 -- \
+ benchmark/buffers/_buffer-compare-offset.node-bench.js \
+ > scatter-node-bench.csv
+Rscript benchmark/scatter.R --xaxis size --category method \
+ --plot scatter-node-bench.png < scatter-node-bench.csv
+```
+
+Pass `--analyze` with an x-axis parameter to summarize the samples without R.
+The output includes mean and median confidence intervals, skew warnings, an
+optional bar chart, and Mann-Whitney U and Cliff's delta comparisons between
+consecutive x-axis values. Use `--category` for a second grouping parameter and
+`--no-chart` to omit the chart.
+
+Because configurations in one file share a process, inline analysis averages
+aggregated configurations into one value per outer process. Consecutive
+x-axis comparisons use alternating, disjoint process sets so the unpaired
+Mann-Whitney test does not treat correlated values as independent samples.
+
+```console
+./node benchmark/scatter-node-bench.js --runs 30 --analyze \
+ --xaxis size --category method -- \
+ benchmark/buffers/_buffer-compare-offset.node-bench.js
+```
+
+A file passed to `scatter-node-bench.js` must use one logical benchmark name.
+Parameter values distinguish its configurations. The tool rejects unstable
+identities and names or parameters that would merge unrelated CSV groups.
+
+The underscore-prefixed benchmark files are parallel ports used to compare the
+measurement frameworks. Legacy discovery ignores them, so the original files
+remain the source benchmarks for `run.js`, `compare.js`, and `scatter.js`. The
+ports use the platform-specific original relative filename as their benchmark
+name and preserve parameter column names to keep CSV grouping compatible. For
+a direct framework comparison, collect the same number of runs from an
+original benchmark with `scatter.js` and from its port with
+`scatter-node-bench.js`, then compare their rate distributions.
+
## Common API
The common.js module is used by benchmarks for consistency across repeated
diff --git a/benchmark/_node-bench-analysis.js b/benchmark/_node-bench-analysis.js
new file mode 100644
index 000000000000..bf0f7402b791
--- /dev/null
+++ b/benchmark/_node-bench-analysis.js
@@ -0,0 +1,763 @@
+'use strict';
+
+const { createHistogram } = require('node:perf_hooks');
+const { inspect } = require('node:util');
+
+function createRateHistogram(rates, scale, figures) {
+ const histogram = createHistogram({ figures });
+ for (const rate of rates) {
+ const value = Math.max(1, Math.round(rate * scale));
+ if (!Number.isSafeInteger(value)) {
+ throw new RangeError('Benchmark rate is too large for the histogram scale');
+ }
+ histogram.record(value);
+ }
+ return histogram;
+}
+
+function holmAdjust(pValues) {
+ const order = pValues
+ .map((p, index) => ({ index, p }))
+ .sort((a, b) => a.p - b.p);
+ const adjusted = new Array(order.length);
+ let running = 0;
+ for (let index = 0; index < order.length; index++) {
+ running = Math.max(
+ running,
+ Math.min(1, (order.length - index) * order[index].p),
+ );
+ adjusted[order[index].index] = running;
+ }
+ return adjusted;
+}
+
+function thresholdPValue(oldRates, newHistogram, scale, maxRegression) {
+ const factor = 1 - maxRegression / 100;
+ if (factor <= 0) return 1;
+ const thresholdHistogram = createRateHistogram(
+ oldRates.map((rate) => rate * factor), scale, 3);
+ const result = thresholdHistogram.welchTest(newHistogram);
+ if (Number.isNaN(result.pValue)) return 1;
+ return result.tStatistic > 0 ?
+ result.pValue / 2 : 1 - result.pValue / 2;
+}
+
+function isRegressionFailure(row, maxRegression) {
+ return row.pThresholdAdjusted < 0.05 &&
+ row.improvement + row.ci95 < -maxRegression;
+}
+
+function analyzeCompare(samples, scale, maxRegression) {
+ const groups = new Map();
+ for (const sample of samples) {
+ let group = groups.get(sample.identity);
+ if (group === undefined) {
+ const suffix = sample.configuration === '' ?
+ '' : ` ${sample.configuration}`;
+ group = {
+ name: `${sample.name}${suffix}`,
+ new: [],
+ old: [],
+ };
+ groups.set(sample.identity, group);
+ }
+ group[sample.binary].push(sample.rate);
+ }
+
+ const rows = [];
+ let skipped = 0;
+ for (const { name, old: oldRates, new: newRates } of groups.values()) {
+ if (oldRates.length < 2 || newRates.length < 2) {
+ skipped++;
+ continue;
+ }
+
+ const oldHistogram = createRateHistogram(oldRates, scale, 3);
+ const newHistogram = createRateHistogram(newRates, scale, 3);
+ const oldMean = oldRates.reduce((sum, rate) => sum + rate, 0) /
+ oldRates.length;
+ const newMean = newRates.reduce((sum, rate) => sum + rate, 0) /
+ newRates.length;
+ const improvement = ((newMean - oldMean) / oldMean) * 100;
+ const w95 = oldHistogram.welchTest(newHistogram, { confidence: 0.95 });
+ const w99 = oldHistogram.welchTest(newHistogram, { confidence: 0.99 });
+ const w999 = oldHistogram.welchTest(newHistogram, { confidence: 0.999 });
+ let stars = '';
+ if (w95.pValue < 0.001) stars = '***';
+ else if (w95.pValue < 0.01) stars = ' **';
+ else if (w95.pValue < 0.05) stars = ' *';
+ const ciPercent = (result) => {
+ const half = (result.confidenceInterval.upper -
+ result.confidenceInterval.lower) / 2;
+ return (half / (oldMean * scale)) * 100;
+ };
+ const row = {
+ ci95: ciPercent(w95),
+ ci99: ciPercent(w99),
+ ci999: ciPercent(w999),
+ improvement,
+ name,
+ pValue: Number.isNaN(w95.pValue) ? 1 : w95.pValue,
+ stars,
+ };
+ if (maxRegression !== undefined) {
+ row.pThreshold = thresholdPValue(
+ oldRates, newHistogram, scale, maxRegression);
+ }
+ rows.push(row);
+ }
+
+ const adjusted = holmAdjust(rows.map(({ pValue }) => pValue));
+ const thresholdAdjusted = maxRegression === undefined ? null :
+ holmAdjust(rows.map(({ pThreshold }) => pThreshold));
+ let underpowered = 0;
+ for (let index = 0; index < rows.length; index++) {
+ const row = rows[index];
+ row.pAdjusted = adjusted[index];
+ if (thresholdAdjusted !== null) {
+ row.pThresholdAdjusted = thresholdAdjusted[index];
+ }
+ row.inconclusive = maxRegression > 0 &&
+ row.stars.trim() === '' &&
+ row.ci95 > maxRegression;
+ if (row.inconclusive) underpowered++;
+ }
+
+ const output = [];
+ const maxNameLength = rows.reduce(
+ (maximum, { name }) => Math.max(maximum, name.length), 0);
+ const pad = (value, length) =>
+ value + ' '.repeat(Math.max(0, length - value.length));
+ const padStart = (value, length) =>
+ ' '.repeat(Math.max(0, length - value.length)) + value;
+ output.push(`${pad('', maxNameLength)} confidence` +
+ ' improvement accuracy (*) (**) (***)');
+ for (const row of rows) {
+ const improvement =
+ `${row.improvement >= 0 ? '+' : ''}${row.improvement.toFixed(2)} %`;
+ output.push(
+ `${pad(row.name, maxNameLength)} ${pad(row.stars, 10)}` +
+ ` ${padStart(improvement, 11)}` +
+ ` ±${row.ci95.toFixed(2)}%` +
+ ` ±${row.ci99.toFixed(2)}%` +
+ ` ±${row.ci999.toFixed(2)}%` +
+ `${row.inconclusive ? ' (inconclusive)' : ''}`,
+ );
+ }
+
+ if (skipped > 0) {
+ output.push('');
+ output.push(
+ `Note: ${skipped} configuration${skipped === 1 ? ' was' : 's were'}` +
+ ' skipped because Welch\'s t-test requires at least 2 samples per' +
+ ' binary. Use --runs 2 or higher.',
+ );
+ }
+ printCompareChart(output, rows, maxNameLength);
+
+ output.push('');
+ output.push(
+ `Rates were scaled by ${scale}x into HdrHistogram (3 significant figures).`,
+ 'Use --scale to adjust precision if needed.',
+ '',
+ );
+ const significant = rows.filter(({ pAdjusted }) => pAdjusted < 0.05).length;
+ output.push(
+ 'The confidence markers above are per-benchmark and uncorrected. ' +
+ `After Holm-Bonferroni correction across ${rows.length} comparison` +
+ `${rows.length === 1 ? '' : 's'}, ${significant} remain` +
+ `${significant === 1 ? 's' : ''} significant at 5%.`,
+ );
+ if (maxRegression !== undefined) {
+ output.push(
+ `For --max-regression, one-sided p-values against the ` +
+ `${maxRegression}% threshold were corrected separately.`,
+ );
+ }
+
+ if (maxRegression > 0 && underpowered > 0) {
+ output.push('');
+ output.push(
+ `Note: ${underpowered} of ${rows.length} comparison` +
+ `${rows.length === 1 ? '' : 's'} could not resolve an effect as small ` +
+ `as ${maxRegression}% and are marked (inconclusive). Raise --runs to ` +
+ 'narrow their confidence intervals.',
+ );
+ }
+
+ const failures = maxRegression !== undefined ?
+ rows.filter((row) => isRegressionFailure(row, maxRegression)) : [];
+ if (failures.length > 0) {
+ output.push('');
+ output.push(
+ `FAIL: ${failures.length} benchmark${failures.length === 1 ? '' : 's'}` +
+ ` regressed by more than ${maxRegression}% (the 95% interval excludes ` +
+ `the threshold and its one-sided test is family-wise corrected across ` +
+ `${rows.length} comparisons):`,
+ );
+ for (const failure of failures) {
+ output.push(
+ ` ${failure.name} ${failure.improvement.toFixed(2)}% ` +
+ `(95% CI up to ${(failure.improvement + failure.ci95).toFixed(2)}%, ` +
+ `adjusted threshold p=` +
+ `${failure.pThresholdAdjusted.toExponential(2)})`,
+ );
+ }
+ }
+
+ return {
+ failed: failures.length > 0,
+ output: `${output.join('\n')}\n`,
+ rows,
+ };
+}
+
+function printCompareChart(output, rows, maxNameLength) {
+ if (rows.length === 0) return;
+ const width = 40;
+ const halfWidth = width / 2;
+ let maximum = 0;
+ for (const row of rows) {
+ maximum = Math.max(maximum, Math.abs(row.improvement) + row.ci95);
+ }
+ if (maximum === 0) maximum = 1;
+ const left = `-${maximum.toFixed(1)}%`;
+ const right = `+${maximum.toFixed(1)}%`;
+ const centerLabel = '0%';
+ const labelPadding = maxNameLength + 5;
+ output.push('');
+ output.push(
+ ' '.repeat(labelPadding) + left +
+ ' '.repeat(Math.max(
+ 0, halfWidth - left.length - Math.floor(centerLabel.length / 2))) +
+ centerLabel +
+ ' '.repeat(Math.max(
+ 0, halfWidth - Math.ceil(centerLabel.length / 2) - right.length)) +
+ right,
+ );
+ for (const row of rows) {
+ const center = halfWidth;
+ const result = center + (row.improvement / maximum) * halfWidth;
+ const lower = center +
+ ((row.improvement - row.ci95) / maximum) * halfWidth;
+ const upper = center +
+ ((row.improvement + row.ci95) / maximum) * halfWidth;
+ let bar = '';
+ for (let index = 0; index < width; index++) {
+ const position = index + 0.5;
+ if (index === Math.floor(center)) {
+ bar += '|';
+ } else if ((row.improvement >= 0 &&
+ position > center && position <= result) ||
+ (row.improvement < 0 &&
+ position < center && position >= result)) {
+ bar += row.stars === '' ? '▓' : '█';
+ } else if (position >= lower && position <= upper) {
+ bar += '░';
+ } else {
+ bar += ' ';
+ }
+ }
+ const label = `${row.improvement >= 0 ? '+' : ''}` +
+ `${row.improvement.toFixed(2)}%`;
+ output.push(
+ `${row.name.padEnd(maxNameLength)} ${bar} ${label} ${row.stars.trim()}`,
+ );
+ }
+}
+
+function histogramScale(rates) {
+ let minimum = Infinity;
+ let maximum = 0;
+ for (const rate of rates) {
+ if (rate > 0 && rate < minimum) minimum = rate;
+ if (rate > maximum) maximum = rate;
+ }
+ if (!Number.isFinite(minimum) || maximum === 0) return 1;
+ let scale = 1;
+ while (minimum * scale < 1e6 && maximum * scale < 1e15) scale *= 10;
+ return scale;
+}
+
+function validateScatterParameters(samples, xAxis, category) {
+ if (category !== undefined && category === xAxis) {
+ throw new Error('--xaxis and --category must name different parameters');
+ }
+ for (const key of [xAxis, category]) {
+ if (key === undefined) continue;
+ if (samples.some(({ params }) =>
+ !Object.hasOwn(params, key))) {
+ const available = [...new Set(samples.flatMap(
+ ({ params }) => Object.keys(params)))].sort();
+ throw new Error(
+ `The variable '${key}' is not present in every configuration. ` +
+ `Available variables: ${available.join(', ')}`,
+ );
+ }
+ }
+}
+
+function analyzeScatter(samples, xAxis, category, showChart) {
+ validateScatterParameters(samples, xAxis, category);
+
+ const parameterNames = [...new Set(samples.flatMap(
+ ({ params }) => Object.keys(params)))];
+ const aggregated = parameterNames.filter((name) => {
+ if (name === xAxis || name === category) return false;
+ const first = samples[0].params[name];
+ return samples.some(({ params }) => params[name] !== first);
+ });
+ const groups = new Map();
+ for (const sample of samples) {
+ const xValue = sample.params[xAxis];
+ const categoryValue = category === undefined ?
+ undefined : sample.params[category];
+ const key = valueKey([xValue, categoryValue]);
+ let group = groups.get(key);
+ if (group === undefined) {
+ group = {
+ categoryValue,
+ members: [],
+ observations: new Map(),
+ xValue,
+ };
+ groups.set(key, group);
+ }
+ group.members.push(sample);
+ let rates = group.observations.get(sample.observation);
+ if (rates === undefined) {
+ rates = [];
+ group.observations.set(sample.observation, rates);
+ }
+ rates.push(sample.rate);
+ }
+ for (const group of groups.values()) {
+ group.processRates = [...group.observations].map(([observation, rates]) => ({
+ observation,
+ rate: rates.reduce((sum, rate) => sum + rate, 0) / rates.length,
+ }));
+ group.rates = group.processRates.map(({ rate }) => rate);
+ }
+
+ const scale = histogramScale(samples.map(({ rate }) => rate));
+ const compareValues = (a, b) => {
+ if (typeof a === 'number' && typeof b === 'number') return a - b;
+ return String(a).localeCompare(String(b));
+ };
+ const rows = [...groups.values()]
+ .sort((a, b) => compareValues(a.xValue, b.xValue) ||
+ compareValues(a.categoryValue, b.categoryValue))
+ .map((group) => {
+ const histogram = createRateHistogram(group.rates, scale, 5);
+ const count = group.rates.length;
+ const mean = group.rates.reduce((sum, rate) => sum + rate, 0) / count;
+ const meanInterval = histogram.meanCI();
+ const confidenceInterval = count > 1 ?
+ (meanInterval.upper - meanInterval.lower) / (2 * scale) : NaN;
+ const medianInterval = histogram.percentileCI(50);
+ const median = rawMedian(group.rates);
+ const skewed = count > 1 &&
+ (Math.abs(histogram.skewness) > 1 ||
+ Math.abs(median - mean) > confidenceInterval);
+ return {
+ ...group,
+ confidenceInterval,
+ count,
+ histogram,
+ mean,
+ median,
+ medianLower: medianInterval.lower / scale,
+ medianUpper: medianInterval.upper / scale,
+ skewed,
+ };
+ });
+
+ const legend = assignLabels(rows, 'xValue', 'xLabel');
+ if (category !== undefined) {
+ legend.push(...assignLabels(rows, 'categoryValue', 'categoryLabel'));
+ }
+ const output = [];
+ const contamination = new Map();
+ for (const variable of aggregated) {
+ const share = varianceShare([...groups.values()], variable);
+ contamination.set(variable, share);
+ const percent = share === 1 ?
+ '100' : (share >= 0.995 ? '>99' : (share * 100).toFixed(0));
+ const suffix = Number.isNaN(share) ?
+ '' : ` (explains ${percent}% of within-group variance)`;
+ output.push(`aggregating variable: ${variable}${suffix}`);
+ }
+ const dominant = aggregated.filter(
+ (variable) => contamination.get(variable) > 0.5);
+ if (dominant.length > 0) {
+ output.push('');
+ wrapOutput(
+ output,
+ `${dominant.join(', ')} ${dominant.length === 1 ? 'explains' : 'explain'} ` +
+ 'most of the spread within each group. Pin the parameter or use it as ' +
+ '--category; increasing --runs will not remove this source of variance.',
+ );
+ }
+ if (aggregated.length > 0) output.push('');
+ printScatterTable(output, rows, xAxis, category);
+ if (showChart) printScatterChart(output, rows, xAxis, category);
+ printScatterComparisons(output, rows, xAxis, category);
+ if (legend.length > 0) {
+ output.push('', 'Abbreviated values:');
+ for (const { full, label } of legend) {
+ output.push(` ${label}`, ` = ${full}`);
+ }
+ }
+ const singleSample = rows.filter(({ count }) => count < 2).length;
+ if (singleSample > 0) {
+ output.push('');
+ wrapOutput(
+ output,
+ `Note: ${singleSample} group${singleSample === 1 ? ' has' : 's have'} ` +
+ 'only one sample, so no confidence interval could be estimated. Use ' +
+ '--runs 2 or higher.',
+ );
+ }
+ if (rows.some(({ skewed }) => skewed)) {
+ output.push('');
+ wrapOutput(
+ output,
+ '(!) marks groups where the median falls outside the mean confidence ' +
+ 'interval or the sample is strongly skewed. The median and its interval ' +
+ 'describe the typical run better for those groups.',
+ );
+ }
+ return `${output.join('\n')}\n`;
+}
+
+function rawMedian(rates) {
+ const sorted = [...rates].sort((a, b) => a - b);
+ const middle = Math.floor(sorted.length / 2);
+ return sorted.length % 2 === 0 ?
+ (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
+}
+
+function valueKey(value) {
+ return JSON.stringify(value, (_, item) => {
+ if (typeof item === 'bigint') return { bigint: String(item) };
+ return item;
+ });
+}
+
+function varianceShare(groups, variable) {
+ let between = 0;
+ let total = 0;
+ for (const group of groups) {
+ if (group.members.length < 2) continue;
+ const mean = group.members.reduce(
+ (sum, sample) => sum + sample.rate, 0) / group.members.length;
+ const levels = new Map();
+ for (const sample of group.members) {
+ const key = valueKey(sample.params[variable]);
+ let level = levels.get(key);
+ if (level === undefined) {
+ level = { count: 0, sum: 0 };
+ levels.set(key, level);
+ }
+ level.count++;
+ level.sum += sample.rate;
+ }
+ for (const level of levels.values()) {
+ between += level.count * ((level.sum / level.count) - mean) ** 2;
+ }
+ for (const sample of group.members) total += (sample.rate - mean) ** 2;
+ }
+ return total === 0 ? NaN : Math.min(1, between / total);
+}
+
+function effectSizeLabel(delta) {
+ const absolute = Math.abs(delta);
+ if (absolute < 0.147) return 'negligible';
+ if (absolute < 0.33) return 'small';
+ if (absolute < 0.474) return 'medium';
+ return 'large';
+}
+
+const mannWhitneyFloors = new Map();
+function mannWhitneyFloor(firstCount, secondCount) {
+ const key = `${firstCount},${secondCount}`;
+ let floor = mannWhitneyFloors.get(key);
+ if (floor !== undefined) return floor;
+ const low = createHistogram({ figures: 5 });
+ const high = createHistogram({ figures: 5 });
+ for (let index = 0; index < firstCount; index++) low.record(10000 + index);
+ for (let index = 0; index < secondCount; index++) {
+ high.record(10000 + firstCount + index);
+ }
+ floor = high.mannWhitneyTest(low).pValue;
+ mannWhitneyFloors.set(key, floor);
+ return floor;
+}
+
+function printScatterComparisons(output, rows, xAxis, category) {
+ const usable = rows.filter(({ count }) => count > 1);
+ if (usable.length < 2) return;
+ const series = new Map();
+ for (const row of usable) {
+ const key = valueKey(row.categoryValue);
+ if (!series.has(key)) series.set(key, []);
+ series.get(key).push(row);
+ }
+ const sections = [];
+ let floor = 0;
+ for (const group of series.values()) {
+ if (group.length < 2) continue;
+ const entries = [];
+ for (let index = 1; index < group.length; index++) {
+ const previous = group[index - 1];
+ const current = group[index];
+ // Configurations in one file share a process. Split consecutive groups
+ // across disjoint outer-process sets so the unpaired test does not treat
+ // correlated observations as independent.
+ const parity = index % 2;
+ const previousRates = previous.processRates
+ .filter(({ observation }) => observation % 2 === parity)
+ .map(({ rate }) => rate);
+ const currentRates = current.processRates
+ .filter(({ observation }) => observation % 2 !== parity)
+ .map(({ rate }) => rate);
+ if (previousRates.length === 0 || currentRates.length === 0) continue;
+ const comparisonScale = histogramScale([
+ ...previousRates,
+ ...currentRates,
+ ]);
+ const previousHistogram =
+ createRateHistogram(previousRates, comparisonScale, 5);
+ const currentHistogram =
+ createRateHistogram(currentRates, comparisonScale, 5);
+ const { pValue } = currentHistogram.mannWhitneyTest(previousHistogram);
+ const delta = currentHistogram.cliffsD(previousHistogram);
+ const previousMean = previousRates.reduce(
+ (sum, rate) => sum + rate, 0) / previousRates.length;
+ const currentMean = currentRates.reduce(
+ (sum, rate) => sum + rate, 0) / currentRates.length;
+ const change = ((currentMean - previousMean) / previousMean) * 100;
+ floor = Math.max(
+ floor, mannWhitneyFloor(previousRates.length, currentRates.length));
+ let ratio = '';
+ let exponent = '';
+ if (typeof previous.xValue === 'number' &&
+ typeof current.xValue === 'number' &&
+ previous.xValue > 0 && current.xValue > 0 &&
+ previous.xValue !== current.xValue &&
+ previousMean > 0 && currentMean > 0) {
+ const xRatio = current.xValue / previous.xValue;
+ const value = Math.log(currentMean / previousMean) / Math.log(xRatio);
+ ratio = `${xRatio.toFixed(1)}x`;
+ exponent = `${value >= 0 ? '+' : ''}${value.toFixed(2)}`;
+ }
+ entries.push(
+ ` ${previous.xLabel} -> ${current.xLabel}` +
+ ` ${change >= 0 ? '+' : ''}${change.toFixed(2)}%` +
+ (exponent === '' ? '' : ` ${ratio} exponent=${exponent}`) +
+ ` p=${pValue < 1e-4 ? pValue.toExponential(1) : pValue.toFixed(4)}` +
+ ` delta=${delta >= 0 ? '+' : ''}${delta.toFixed(3)}` +
+ ` (${effectSizeLabel(delta)})`,
+ );
+ }
+ if (entries.length > 0) {
+ sections.push({
+ entries,
+ heading: category === undefined ?
+ undefined : `${category}=${group[0].categoryLabel}`,
+ });
+ }
+ }
+ if (sections.length === 0) return;
+ output.push('', `Change between consecutive ${xAxis} values ` +
+ `(Mann-Whitney U on disjoint process sets, Cliff's delta):`);
+ for (const section of sections) {
+ output.push('');
+ if (section.heading !== undefined) output.push(` ${section.heading}`);
+ output.push(...section.entries);
+ }
+ if (floor >= 0.05) {
+ output.push('');
+ wrapOutput(
+ output,
+ `Warning: at this sample size the smallest p-value this test can ` +
+ `produce is ${floor.toFixed(4)}, so no comparison above can reach ` +
+ 'significance. Raise --runs.',
+ );
+ } else if (floor >= 0.005) {
+ output.push('');
+ wrapOutput(
+ output,
+ `Note: at this sample size the smallest p-value this test can produce ` +
+ `is ${floor.toFixed(4)}. Raise --runs to strengthen non-significant ` +
+ 'results.',
+ );
+ }
+}
+
+function formatRate(rate) {
+ return rate.toLocaleString('en-US', {
+ maximumFractionDigits: 1,
+ minimumFractionDigits: 1,
+ });
+}
+
+function displayWidth(value) {
+ return [...value].length;
+}
+
+function pad(value, width, right) {
+ const padding = ' '.repeat(Math.max(0, width - displayWidth(value)));
+ return right ? padding + value : value + padding;
+}
+
+function truncateMiddle(value, maximum = 24) {
+ const characters = [...value];
+ if (characters.length <= maximum) return value;
+ const retained = maximum - 3;
+ const head = Math.ceil(retained / 2);
+ const tail = Math.floor(retained / 2);
+ return `${characters.slice(0, head).join('')}...` +
+ characters.slice(-tail).join('');
+}
+
+function assignLabels(rows, valueName, labelName) {
+ const assigned = new Map();
+ const used = new Map();
+ const legend = [];
+ for (const row of rows) {
+ const key = valueKey(row[valueName]);
+ let label = assigned.get(key);
+ if (label === undefined) {
+ const full = typeof row[valueName] === 'string' ?
+ inspect(row[valueName]) : String(row[valueName]);
+ const abbreviated = truncateMiddle(full);
+ const collisions = used.get(abbreviated) ?? 0;
+ used.set(abbreviated, collisions + 1);
+ label = collisions === 0 ?
+ abbreviated : `${abbreviated}~${collisions + 1}`;
+ assigned.set(key, label);
+ if (label !== full) legend.push({ full, label });
+ }
+ row[labelName] = label;
+ }
+ return legend;
+}
+
+function printScatterTable(output, rows, xAxis, category) {
+ const header = [xAxis];
+ if (category !== undefined) header.push(category);
+ header.push(
+ 'samples', 'rate', 'confidence.interval', 'median', 'median.interval', '');
+ const body = rows.map((row) => {
+ const values = [row.xLabel];
+ if (category !== undefined) values.push(row.categoryLabel);
+ const medianInterval = row.count > 1 ?
+ `[${(((row.medianLower - row.median) / row.median) * 100).toFixed(2)}%, ` +
+ `+${(((row.medianUpper - row.median) / row.median) * 100).toFixed(2)}%]` :
+ 'NA';
+ values.push(
+ String(row.count),
+ formatRate(row.mean),
+ Number.isNaN(row.confidenceInterval) ?
+ 'NA' :
+ `${formatRate(row.confidenceInterval)} ` +
+ `(±${((row.confidenceInterval / row.mean) * 100).toFixed(2)}%)`,
+ formatRate(row.median),
+ medianInterval,
+ row.skewed ? '(!)' : '',
+ );
+ return values;
+ });
+ const widths = header.map((value, index) => Math.max(
+ displayWidth(value),
+ ...body.map((values) => displayWidth(values[index])),
+ ));
+ const right = [typeof rows[0].xValue === 'number'];
+ if (category !== undefined) {
+ right.push(typeof rows[0].categoryValue === 'number');
+ }
+ right.push(true, true, true, true, true, false);
+ const format = (values) => values.map(
+ (value, index) => pad(value, widths[index], right[index])).join(' ').trimEnd();
+ output.push(format(header));
+ for (const values of body) output.push(format(values));
+}
+
+function printScatterChart(output, rows, xAxis, category) {
+ if (rows.length === 0) return;
+ const width = 40;
+ let maximum = 0;
+ for (const row of rows) {
+ maximum = Math.max(
+ maximum,
+ row.mean + (Number.isNaN(row.confidenceInterval) ?
+ 0 : row.confidenceInterval),
+ );
+ }
+ if (maximum === 0) return;
+ const labels = rows.map((row) => {
+ let label = `${xAxis}=${row.xLabel}`;
+ if (category !== undefined) label += ` ${category}=${row.categoryLabel}`;
+ return truncateMiddle(label, 44);
+ });
+ const labelWidth = Math.max(...labels.map(displayWidth));
+ const rateWidth = Math.max(...rows.map(({ mean }) =>
+ displayWidth(formatRate(mean))));
+ const axis = formatRate(maximum);
+ const indent = ' '.repeat(labelWidth + 2);
+ output.push(
+ '',
+ 'Rate in operations/second; longer is faster. │ marks the mean and the',
+ 'shaded band (░) is its 95% confidence interval.',
+ '',
+ `${indent}0${' '.repeat(Math.max(1, width - 1 - displayWidth(axis)))}${axis}`,
+ `${indent}+${'-'.repeat(width - 2)}+`,
+ );
+ let previous;
+ for (let index = 0; index < rows.length; index++) {
+ const row = rows[index];
+ if (previous !== undefined && previous !== row.xValue) output.push('');
+ previous = row.xValue;
+ const interval = Number.isNaN(row.confidenceInterval) ?
+ 0 : row.confidenceInterval;
+ const end = (row.mean / maximum) * width;
+ const lower = ((row.mean - interval) / maximum) * width;
+ const upper = ((row.mean + interval) / maximum) * width;
+ const meanCell = Math.min(width - 1, Math.floor(end));
+ let bar = '';
+ for (let cell = 0; cell < width; cell++) {
+ const position = cell + 0.5;
+ if (cell === meanCell) bar += '│';
+ else if (position >= lower && position <= upper) bar += '░';
+ else if (position <= end) bar += '█';
+ else bar += ' ';
+ }
+ output.push(
+ `${pad(labels[index], labelWidth, false)} ${bar} ` +
+ pad(formatRate(row.mean), rateWidth, true),
+ );
+ }
+}
+
+function wrapOutput(output, text, width = 76) {
+ let line = '';
+ for (const word of text.split(/\s+/)) {
+ if (line === '') line = word;
+ else if (displayWidth(line) + displayWidth(word) + 1 <= width) {
+ line += ` ${word}`;
+ } else {
+ output.push(line);
+ line = word;
+ }
+ }
+ if (line !== '') output.push(line);
+}
+
+module.exports = {
+ analyzeCompare,
+ analyzeScatter,
+ holmAdjust,
+ isRegressionFailure,
+ validateScatterParameters,
+};
diff --git a/benchmark/_node-bench.js b/benchmark/_node-bench.js
new file mode 100644
index 000000000000..ecf383e3bc30
--- /dev/null
+++ b/benchmark/_node-bench.js
@@ -0,0 +1,160 @@
+'use strict';
+
+const { spawn } = require('node:child_process');
+const path = require('node:path');
+const { inspect } = require('node:util');
+
+function parseInteger(value, defaultValue, name, minimum) {
+ if (value === undefined) return defaultValue;
+ if (!/^(?:0|[1-9]\d*)$/.test(value)) {
+ throw new TypeError(`${name} must be an integer`);
+ }
+ const number = Number(value);
+ if (!Number.isSafeInteger(number) || number < minimum) {
+ throw new RangeError(`${name} must be at least ${minimum}`);
+ }
+ return number;
+}
+
+function parseNumber(value, defaultValue, name, minimum) {
+ if (value === undefined) return defaultValue;
+ if (value.trim() === '') throw new TypeError(`${name} must be a number`);
+ const number = Number(value);
+ if (!Number.isFinite(number)) throw new TypeError(`${name} must be a number`);
+ if (number < minimum) {
+ throw new RangeError(`${name} must be at least ${minimum}`);
+ }
+ return number;
+}
+
+function csvEncode(value) {
+ if (typeof value === 'number' || typeof value === 'boolean') {
+ return String(value);
+ }
+ const string = String(value);
+ return `"${string.replace(/"/g, '""')}"`;
+}
+
+function formatConfiguration(params) {
+ return Object.keys(params)
+ .map((key) => `${key}=${inspect(params[key])}`)
+ .join(' ');
+}
+
+function durationToSeconds(duration) {
+ if (!/^\d+$/.test(duration)) {
+ throw new TypeError(`Invalid benchmark duration '${duration}'`);
+ }
+ const padded = duration.padStart(10, '0');
+ return `${padded.slice(0, -9)}.${padded.slice(-9)}`;
+}
+
+function runBenchmark(binary, file, options) {
+ const args = [
+ ...options.nodeArgs,
+ '--no-warnings',
+ '--experimental-bench',
+ '--bench',
+ '--bench-reporter=json',
+ '--bench-samples=1',
+ `--bench-warmup=${options.warmup}`,
+ ];
+ if (options.namePattern !== undefined) {
+ args.push(`--bench-name-pattern=${options.namePattern}`);
+ }
+ args.push('--', path.resolve(file));
+
+ return new Promise((resolve, reject) => {
+ const child = spawn(binary, args, {
+ env: process.env,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ });
+ child.stdout.setEncoding('utf8');
+ child.stderr.setEncoding('utf8');
+
+ let stdout = '';
+ let stderr = '';
+ child.stdout.on('data', (data) => { stdout += data; });
+ child.stderr.on('data', (data) => { stderr += data; });
+ child.once('error', reject);
+ child.once('close', (code, signal) => {
+ let records;
+ try {
+ records = stdout.trim().split('\n')
+ .filter((line) => line.length > 0)
+ .map((line) => JSON.parse(line));
+ } catch (error) {
+ reject(new Error(
+ `Could not parse benchmark output from '${binary}': ${error.message}`,
+ { cause: error },
+ ));
+ return;
+ }
+
+ const summary = records.findLast(
+ ({ type }) => type === 'bench:summary')?.data;
+ if (code !== 0 || signal !== null || summary?.success !== true) {
+ const diagnostics = records
+ .filter(({ type }) => type === 'bench:diagnostic')
+ .map(({ data }) => data.message)
+ .join('\n');
+ const status = signal === null ? `exit code ${code}` : `signal ${signal}`;
+ const details = stderr || diagnostics;
+ reject(new Error(
+ `Benchmark '${file}' failed with ${status}` +
+ (details ? `:\n${details}` : ''),
+ ));
+ return;
+ }
+
+ const samples = [];
+ for (const record of records) {
+ if (record.type !== 'bench:complete' ||
+ record.data.skip !== undefined) {
+ continue;
+ }
+ if (record.data.error !== undefined) {
+ reject(new Error(
+ `Benchmark '${record.data.name}' failed: ` +
+ record.data.error.message,
+ ));
+ return;
+ }
+ if (record.data.samples.length !== 1) {
+ reject(new Error(
+ `Benchmark '${record.data.name}' did not produce exactly one sample`,
+ ));
+ return;
+ }
+ const sample = record.data.samples[0];
+ if (!Number.isFinite(sample.rate)) {
+ reject(new Error(
+ `Benchmark '${record.data.name}' produced a non-finite rate`,
+ ));
+ return;
+ }
+ samples.push({
+ configuration: formatConfiguration(record.data.params),
+ duration: durationToSeconds(sample.duration_ns),
+ identity: record.data.benchId,
+ logicalIdentity: JSON.stringify([
+ record.data.file,
+ record.data.parentId,
+ record.data.name,
+ ]),
+ name: record.data.name,
+ params: record.data.params,
+ rate: sample.rate,
+ });
+ }
+ resolve(samples);
+ });
+ });
+}
+
+module.exports = {
+ csvEncode,
+ parseInteger,
+ parseNumber,
+ runBenchmark,
+};
diff --git a/benchmark/buffers/_buffer-compare-offset.node-bench.js b/benchmark/buffers/_buffer-compare-offset.node-bench.js
new file mode 100644
index 000000000000..b16c3a3da7db
--- /dev/null
+++ b/benchmark/buffers/_buffer-compare-offset.node-bench.js
@@ -0,0 +1,37 @@
+'use strict';
+
+const { bench } = require('node:bench');
+const path = require('node:path');
+
+const methods = ['offset', 'slice'];
+const sizes = [16, 512, 4096, 16386];
+const n = 1e6;
+const name = path.join('buffers', 'buffer-compare-offset.js');
+
+function compareUsingSlice(b0, b1, len, iterations) {
+ for (let i = 0; i < iterations; i++)
+ Buffer.compare(b0.slice(1, len), b1.slice(1, len));
+}
+
+function compareUsingOffset(b0, b1, len, iterations) {
+ for (let i = 0; i < iterations; i++)
+ b0.compare(b1, 1, len, 1, len);
+}
+
+for (const method of methods) {
+ for (const size of sizes) {
+ const compare = method === 'slice' ?
+ compareUsingSlice : compareUsingOffset;
+
+ bench(name, {
+ params: { method, n, size },
+ }, (b) => {
+ b.start();
+ compare(Buffer.alloc(size, 'a'),
+ Buffer.alloc(size, 'b'),
+ size >> 1,
+ n);
+ b.end(n);
+ });
+ }
+}
diff --git a/benchmark/compare-node-bench.js b/benchmark/compare-node-bench.js
new file mode 100644
index 000000000000..295f15f0fdc1
--- /dev/null
+++ b/benchmark/compare-node-bench.js
@@ -0,0 +1,118 @@
+'use strict';
+
+const path = require('node:path');
+const CLI = require('./_cli.js');
+const { analyzeCompare } = require('./_node-bench-analysis.js');
+const {
+ csvEncode,
+ parseInteger,
+ parseNumber,
+ runBenchmark,
+} = require('./_node-bench.js');
+
+const cli = new CLI(`usage: ./node compare-node-bench.js [options] [--] ...
+ Run explicit node:bench files repeatedly with two Node.js binaries. Each
+ observation runs in a fresh process. Output is compatible with compare.R,
+ or --analyze can summarize it directly.
+
+ --new binary new Node.js binary (required)
+ --old binary old Node.js binary (required)
+ --runs 30 observations per binary
+ --warmup 0 warmup samples before each observation
+ --name-pattern pattern only run matching benchmarks
+ --node-arg argument pass an argument to both binaries (repeatable)
+ --analyze analyze with Welch's t-test instead of writing CSV
+ --scale 1000 rate multiplier used for histogram precision
+ --max-regression N fail if a family-wise significant regression's
+ 95% confidence interval is entirely beyond N%
+ (implies --analyze)
+`, { arrayArgs: ['node-arg'], boolArgs: ['analyze'] });
+
+if (!cli.optional.new || !cli.optional.old || cli.items.length === 0) {
+ cli.abort(cli.usage);
+}
+
+async function main() {
+ const runs = parseInteger(cli.optional.runs, 30, '--runs', 1);
+ const warmup = parseInteger(cli.optional.warmup, 0, '--warmup', 0);
+ const scale = parseInteger(cli.optional.scale, 1000, '--scale', 1);
+ const hasMaxRegression = cli.optional['max-regression'] !== undefined;
+ const maxRegression = parseNumber(
+ cli.optional['max-regression'], 0, '--max-regression', 0);
+ const analyze = !!cli.optional.analyze || hasMaxRegression;
+ const options = {
+ namePattern: cli.optional['name-pattern'],
+ nodeArgs: cli.optional['node-arg'],
+ warmup,
+ };
+ const binaries = [
+ { label: 'old', path: cli.optional.old },
+ { label: 'new', path: cli.optional.new },
+ ];
+ const rows = [];
+ const counts = new Map();
+ const csvGroups = new Map();
+
+ for (const file of cli.items) {
+ const resolved = path.resolve(file);
+ for (let run = 0; run < runs; run++) {
+ const order = run % 2 === 0 ? binaries : [binaries[1], binaries[0]];
+ for (const binary of order) {
+ const samples = await runBenchmark(binary.path, resolved, options);
+ for (const sample of samples) {
+ const identity = JSON.stringify([resolved, sample.identity]);
+ const csvGroup = JSON.stringify([
+ sample.name,
+ sample.configuration,
+ ]);
+ const groupedIdentity = csvGroups.get(csvGroup);
+ if (groupedIdentity !== undefined && groupedIdentity !== identity) {
+ throw new Error(
+ `Distinct benchmarks would share the CSV group '${sample.name} ` +
+ `${sample.configuration}'`,
+ );
+ }
+ csvGroups.set(csvGroup, identity);
+ let count = counts.get(identity);
+ if (count === undefined) {
+ count = { name: sample.name, new: 0, old: 0 };
+ counts.set(identity, count);
+ }
+ count[binary.label]++;
+ rows.push({ binary: binary.label, ...sample });
+ }
+ }
+ }
+ }
+
+ if (rows.length === 0) {
+ throw new Error('No benchmark samples were produced');
+ }
+ for (const count of counts.values()) {
+ if (count.old !== runs || count.new !== runs) {
+ throw new Error(
+ `Benchmark '${count.name}' was not reported by both binaries in every run`,
+ );
+ }
+ }
+
+ if (analyze) {
+ const result = analyzeCompare(
+ rows, scale, hasMaxRegression ? maxRegression : undefined);
+ process.stdout.write(result.output);
+ if (result.failed) process.exitCode = 1;
+ return;
+ }
+
+ const output = ['"binary","filename","configuration","rate","time"'];
+ for (const row of rows) {
+ output.push(`${csvEncode(row.binary)},${csvEncode(row.name)},` +
+ `${csvEncode(row.configuration)},${row.rate},${row.duration}`);
+ }
+ process.stdout.write(`${output.join('\n')}\n`);
+}
+
+main().catch((error) => {
+ console.error(error.stack);
+ process.exitCode = 1;
+});
diff --git a/benchmark/compare.js b/benchmark/compare.js
index 6aaaee7a9190..77874e8af6c1 100644
--- a/benchmark/compare.js
+++ b/benchmark/compare.js
@@ -159,6 +159,27 @@ if (showProgress) {
});
})(kStartOfQueue);
+// Holm-Bonferroni step-down adjustment. Controls the probability of *any*
+// false positive across the whole comparison set, which is what a pass/fail
+// gate needs: an uncorrected suite of 169 comparisons at 5% has a 99.98%
+// chance of flagging something that is not there. Uniformly more powerful
+// than plain Bonferroni, and makes no assumption about independence.
+function holmAdjust(pValues) {
+ const order = pValues
+ .map((p, i) => ({ p, i }))
+ .sort((a, b) => a.p - b.p);
+ const m = order.length;
+ const adjusted = new Array(m);
+ let running = 0;
+ for (let k = 0; k < m; k++) {
+ // Step down, enforcing monotonicity so an adjusted value can never be
+ // smaller than one belonging to a more significant raw p-value.
+ running = Math.max(running, Math.min(1, (m - k) * order[k].p));
+ adjusted[order[k].i] = running;
+ }
+ return adjusted;
+}
+
function printAnalysis(results, scale, maxRegression) {
const { createHistogram } = require('node:perf_hooks');
@@ -217,6 +238,25 @@ function printAnalysis(results, scale, maxRegression) {
if (name.length > maxNameLen) maxNameLen = name.length;
}
+ // Adjust for the size of the comparison set. The raw p-value answers "is
+ // this one benchmark different", but a suite is read as a whole, so the
+ // relevant question is "is anything here different".
+ const adjusted = holmAdjust(rows.map((r) => r.pValue));
+ for (let i = 0; i < rows.length; i++) rows[i].pAdjusted = adjusted[i];
+
+ // A comparison can only rule out an effect it was able to resolve. Where no
+ // threshold has been given there is no definition of "worth detecting", so
+ // nothing is claimed. `maxRegression` is exactly such a declaration, so it
+ // is reused rather than inventing a second constant.
+ const resolution = maxRegression > 0 ? maxRegression : null;
+ let underpowered = 0;
+ for (const row of rows) {
+ row.inconclusive = resolution !== null &&
+ row.stars.trim() === '' &&
+ row.ci95 > resolution;
+ if (row.inconclusive) underpowered++;
+ }
+
// Print header.
const pad = (s, n) => s + ' '.repeat(Math.max(0, n - s.length));
const rpad = (s, n) => ' '.repeat(Math.max(0, n - s.length)) + s;
@@ -231,7 +271,8 @@ function printAnalysis(results, scale, maxRegression) {
` ${rpad(imp, 11)}` +
` ±${row.ci95.toFixed(2)}%` +
` ±${row.ci99.toFixed(2)}%` +
- ` ±${row.ci999.toFixed(2)}%`,
+ ` ±${row.ci999.toFixed(2)}%` +
+ `${row.inconclusive ? ' (inconclusive)' : ''}`,
);
}
@@ -252,6 +293,7 @@ function printAnalysis(results, scale, maxRegression) {
`Rates were scaled by ${scale}x into HdrHistogram (3 significant figures).\n` +
`Use --scale to adjust precision if needed.\n`,
);
+ const anyFamilyWise = rows.filter((r) => r.pAdjusted < 0.05).length;
console.log(
`Be aware that when doing many comparisons the risk of a false-positive\n` +
`result increases. In this case, there are ${rows.length} comparisons, ` +
@@ -261,23 +303,56 @@ function printAnalysis(results, scale, maxRegression) {
` ${(rows.length * 0.01).toFixed(2)} false positives, when considering ` +
`a 1% risk acceptance (**, ***),\n` +
` ${(rows.length * 0.001).toFixed(2)} false positives, when considering ` +
- `a 0.1% risk acceptance (***)`,
+ `a 0.1% risk acceptance (***)\n` +
+ `\nThe stars above are per-benchmark and uncorrected. Adjusting for the ` +
+ `size of\nthis comparison set (Holm-Bonferroni), ${anyFamilyWise} ` +
+ `comparison${anyFamilyWise === 1 ? '' : 's'} remain${anyFamilyWise === 1 ? 's' : ''} ` +
+ `significant at 5%.\n--max-regression uses the corrected values.`,
);
- // Gate: exit with error if any significant regression exceeds the limit.
+ // Gate: exit with error if any regression is shown to exceed the limit.
if (maxRegression > 0) {
+ if (underpowered > 0) {
+ console.log('');
+ console.log(
+ `Note: ${underpowered} of ${rows.length} comparison` +
+ `${rows.length === 1 ? '' : 's'} could not resolve an effect as ` +
+ `small as ${maxRegression}%, and are marked (inconclusive). They are ` +
+ `not\nevidence of no regression -- the samples are too noisy to tell. ` +
+ `Raise --runs,\nor pin cores with --set CPUSET, to narrow them.`,
+ );
+ }
+
+ // Two conditions, both required.
+ //
+ // The confidence interval must lie entirely beyond the threshold. A small
+ // p-value only says the effect is not exactly zero; claiming it exceeds
+ // `maxRegression` is a statement about magnitude, so the interval has to
+ // exclude that magnitude. Testing the point estimate instead systematically
+ // fires on the noisiest benchmarks, because a large point estimate is
+ // easiest to obtain when the interval is wide.
+ //
+ // The p-value must also survive adjustment for the size of the comparison
+ // set, so that a suite of hundreds of benchmarks does not fail purely
+ // because one of them drifted.
const failures = rows.filter(
- (r) => r.stars.trim() !== '' && r.improvement < -maxRegression,
+ (r) => r.pAdjusted < 0.05 && r.improvement + r.ci95 < -maxRegression,
);
+
if (failures.length > 0) {
console.log('');
console.log(
`FAIL: ${failures.length} benchmark${failures.length === 1 ? '' : 's'}` +
- ` showed a statistically significant regression exceeding` +
- ` ${maxRegression}%:`,
+ ` regressed by more than ${maxRegression}%` +
+ ` (interval excludes the threshold,\n` +
+ `family-wise corrected across ${rows.length} comparisons):`,
);
for (const f of failures) {
- console.log(` ${f.name} ${f.improvement.toFixed(2)}%`);
+ console.log(
+ ` ${f.name} ${f.improvement.toFixed(2)}% ` +
+ `(95% CI up to ${(f.improvement + f.ci95).toFixed(2)}%, ` +
+ `adjusted p=${f.pAdjusted.toExponential(2)})`,
+ );
}
process.exitCode = 1;
}
diff --git a/benchmark/crypto/_create-hash.node-bench.js b/benchmark/crypto/_create-hash.node-bench.js
new file mode 100644
index 000000000000..4481bc6c6527
--- /dev/null
+++ b/benchmark/crypto/_create-hash.node-bench.js
@@ -0,0 +1,19 @@
+'use strict';
+
+const assert = require('node:assert');
+const { bench } = require('node:bench');
+const { createHash } = require('node:crypto');
+const path = require('node:path');
+
+const n = 1e5;
+const name = path.join('crypto', 'create-hash.js');
+
+bench(name, { params: { n } }, (b) => {
+ const array = [];
+ for (let i = 0; i < n; ++i) array.push(null);
+ b.start();
+ for (let i = 0; i < n; ++i)
+ array[i] = createHash('sha1');
+ b.end(n);
+ assert.strictEqual(typeof array[n - 1], 'object');
+});
diff --git a/benchmark/crypto/class-construction.js b/benchmark/crypto/class-construction.js
new file mode 100644
index 000000000000..fe03b1a43cd2
--- /dev/null
+++ b/benchmark/crypto/class-construction.js
@@ -0,0 +1,86 @@
+'use strict';
+
+const common = require('../common.js');
+const assert = require('node:assert');
+const crypto = require('node:crypto');
+const fs = require('node:fs');
+const path = require('node:path');
+
+const fixtureDir = path.resolve(__dirname, '../../test/fixtures/keys');
+const certificate = fs.readFileSync(path.join(fixtureDir, 'agent1-cert.pem'));
+const dhPrime = crypto.getDiffieHellman('modp14').getPrime();
+const key = Buffer.alloc(32, 0x01);
+const keyObject = crypto.createSecretKey(key);
+const hmacAlgorithm = { name: 'HMAC', hash: 'SHA-256' };
+const keyUsages = ['sign'];
+const iv = Buffer.alloc(16, 0x02);
+
+const iterations = {
+ Certificate: 1e7,
+ Cipheriv: 1e5,
+ Decipheriv: 1e5,
+ DiffieHellman: 10,
+ DiffieHellmanGroup: 2e5,
+ ECDH: 2e5,
+ Hash: 1e5,
+ Hmac: 5e4,
+ KeyObject: 1e5,
+ Sign: 1e5,
+ Verify: 1e5,
+ CryptoKey: 5e4,
+ X509Certificate: 5e3,
+};
+
+const bench = common.createBenchmark(main, {
+ type: Object.keys(iterations),
+ n: [...new Set(Object.values(iterations))],
+}, {
+ combinationFilter({ type, n }) {
+ // Benchmark test mode reduces numeric options to 1.
+ return n === 1 || iterations[type] === n;
+ },
+});
+
+function construct(type) {
+ switch (type) {
+ case 'Certificate':
+ return new crypto.Certificate();
+ case 'Cipheriv':
+ return crypto.createCipheriv('aes-256-ctr', key, iv);
+ case 'Decipheriv':
+ return crypto.createDecipheriv('aes-256-ctr', key, iv);
+ case 'DiffieHellman':
+ return crypto.createDiffieHellman(dhPrime);
+ case 'DiffieHellmanGroup':
+ return crypto.getDiffieHellman('modp14');
+ case 'ECDH':
+ return crypto.createECDH('prime256v1');
+ case 'Hash':
+ return crypto.createHash('sha256');
+ case 'Hmac':
+ return crypto.createHmac('sha256', key);
+ case 'KeyObject':
+ return crypto.createSecretKey(key);
+ case 'Sign':
+ return crypto.createSign('sha256');
+ case 'Verify':
+ return crypto.createVerify('sha256');
+ case 'CryptoKey':
+ return keyObject.toCryptoKey(hmacAlgorithm, true, keyUsages);
+ case 'X509Certificate':
+ return new crypto.X509Certificate(certificate);
+ default:
+ throw new Error(`Unsupported class: ${type}`);
+ }
+}
+
+function main({ type, n }) {
+ const instances = new Array(n);
+
+ bench.start();
+ for (let i = 0; i < n; ++i)
+ instances[i] = construct(type);
+ bench.end(n);
+
+ assert.strictEqual(typeof instances[n - 1], 'object');
+}
diff --git a/benchmark/crypto/class-methods.js b/benchmark/crypto/class-methods.js
new file mode 100644
index 000000000000..9db499b7d1a9
--- /dev/null
+++ b/benchmark/crypto/class-methods.js
@@ -0,0 +1,206 @@
+'use strict';
+
+const common = require('../common.js');
+const crypto = require('node:crypto');
+const fs = require('node:fs');
+const path = require('node:path');
+
+const fixtureDir = path.resolve(__dirname, '../../test/fixtures/keys');
+const certificate = fs.readFileSync(path.join(fixtureDir, 'agent1-cert.pem'));
+const key = Buffer.alloc(32, 0x01);
+const hmacAlgorithm = { name: 'HMAC', hash: 'SHA-256' };
+const keyUsages = ['sign'];
+const iv = Buffer.alloc(16, 0x02);
+const input = Buffer.alloc(16, 0x03);
+
+const iterations = {
+ 'Cipheriv-update': 1e6,
+ 'Decipheriv-update': 1e6,
+ 'DiffieHellman-getGenerator': 2e5,
+ 'DiffieHellmanGroup-getGenerator': 2e6,
+ 'ECDH-getPrivateKey': 2e6,
+ 'Hash-update': 5e6,
+ 'Hmac-update': 5e6,
+ 'KeyObject-equals': 2e6,
+ 'KeyObject-symmetricKeySize-first': 1e5,
+ 'KeyObject-type-first': 1e5,
+ 'KeyObject-type': 1e8,
+ 'Sign-update': 5e6,
+ 'Verify-update': 5e6,
+ 'CryptoKey-toKeyObject': 2e5,
+ 'CryptoKey-algorithm-first': 1e5,
+ 'CryptoKey-extractable-first': 1e5,
+ 'CryptoKey-type-first': 1e5,
+ 'CryptoKey-usages-first': 1e5,
+ 'CryptoKey-type': 1e8,
+ 'X509Certificate-checkHost': 1e6,
+ 'X509Certificate-publicKey-first': 5e3,
+ 'X509Certificate-publicKey': 1e8,
+ 'X509Certificate-subject-first': 5e3,
+ 'X509Certificate-subject': 1e8,
+};
+
+const bench = common.createBenchmark(main, {
+ operation: Object.keys(iterations),
+ n: [...new Set(Object.values(iterations))],
+}, {
+ combinationFilter({ operation, n }) {
+ // Benchmark test mode reduces numeric options to 1.
+ return n === 1 || iterations[operation] === n;
+ },
+});
+
+function setup(operation) {
+ switch (operation) {
+ case 'Cipheriv-update': {
+ const cipher = crypto.createCipheriv('aes-256-ctr', key, iv);
+ return {
+ run: () => cipher.update(input),
+ finish: () => cipher.final(),
+ };
+ }
+ case 'Decipheriv-update': {
+ const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv);
+ return {
+ run: () => decipher.update(input),
+ finish: () => decipher.final(),
+ };
+ }
+ case 'DiffieHellman-getGenerator': {
+ const dh = crypto.createDiffieHellman(
+ crypto.getDiffieHellman('modp14').getPrime());
+ return { run: () => dh.getGenerator() };
+ }
+ case 'DiffieHellmanGroup-getGenerator': {
+ const dh = crypto.getDiffieHellman('modp14');
+ return { run: () => dh.getGenerator() };
+ }
+ case 'ECDH-getPrivateKey': {
+ const ecdh = crypto.createECDH('prime256v1');
+ ecdh.generateKeys();
+ return { run: () => ecdh.getPrivateKey() };
+ }
+ case 'Hash-update': {
+ const hash = crypto.createHash('sha256');
+ return {
+ run: () => hash.update(input),
+ finish: () => hash.digest(),
+ };
+ }
+ case 'Hmac-update': {
+ const hmac = crypto.createHmac('sha256', key);
+ return {
+ run: () => hmac.update(input),
+ finish: () => hmac.digest(),
+ };
+ }
+ case 'KeyObject-equals': {
+ const keyObject = crypto.createSecretKey(key);
+ return { run: () => keyObject.equals(keyObject) };
+ }
+ case 'KeyObject-symmetricKeySize-first': {
+ return { run: () => crypto.createSecretKey(key).symmetricKeySize };
+ }
+ case 'KeyObject-type-first': {
+ return { run: () => crypto.createSecretKey(key).type };
+ }
+ case 'KeyObject-type': {
+ const keyObjects = Array.from(
+ { length: 64 }, () => crypto.createSecretKey(key));
+ for (const keyObject of keyObjects) {
+ if (keyObject.type !== 'secret')
+ throw new Error('Unexpected KeyObject type');
+ }
+ let index = 0;
+ return { run: () => keyObjects[index++ & 63].type };
+ }
+ case 'Sign-update': {
+ const sign = crypto.createSign('sha256');
+ return { run: () => sign.update(input) };
+ }
+ case 'Verify-update': {
+ const verify = crypto.createVerify('sha256');
+ return { run: () => verify.update(input) };
+ }
+ case 'CryptoKey-toKeyObject': {
+ const keyObject = crypto.createSecretKey(key);
+ const cryptoKey = keyObject.toCryptoKey(
+ hmacAlgorithm, true, keyUsages);
+ return { run: () => crypto.KeyObject.from(cryptoKey) };
+ }
+ case 'CryptoKey-algorithm-first':
+ case 'CryptoKey-extractable-first':
+ case 'CryptoKey-type-first':
+ case 'CryptoKey-usages-first': {
+ const keyObject = crypto.createSecretKey(key);
+ const property = operation.slice('CryptoKey-'.length, -'-first'.length);
+ return {
+ run: () => keyObject.toCryptoKey(
+ hmacAlgorithm, true, keyUsages)[property],
+ };
+ }
+ case 'CryptoKey-type': {
+ const keyObject = crypto.createSecretKey(key);
+ const cryptoKeys = Array.from(
+ { length: 64 },
+ () => keyObject.toCryptoKey(hmacAlgorithm, true, keyUsages));
+ for (const cryptoKey of cryptoKeys) {
+ if (cryptoKey.type !== 'secret')
+ throw new Error('Unexpected CryptoKey type');
+ }
+ let index = 0;
+ return { run: () => cryptoKeys[index++ & 63].type };
+ }
+ case 'X509Certificate-checkHost': {
+ const x509 = new crypto.X509Certificate(certificate);
+ return { run: () => x509.checkHost('agent1') };
+ }
+ case 'X509Certificate-publicKey-first': {
+ return {
+ run: () => new crypto.X509Certificate(certificate).publicKey,
+ };
+ }
+ case 'X509Certificate-publicKey': {
+ const certificates = Array.from(
+ { length: 64 }, () => new crypto.X509Certificate(certificate));
+ for (const x509 of certificates) {
+ if (x509.publicKey === undefined)
+ throw new Error('Missing certificate public key');
+ }
+ let index = 0;
+ return { run: () => certificates[index++ & 63].publicKey };
+ }
+ case 'X509Certificate-subject-first': {
+ return {
+ run: () => new crypto.X509Certificate(certificate).subject,
+ };
+ }
+ case 'X509Certificate-subject': {
+ const certificates = Array.from(
+ { length: 64 }, () => new crypto.X509Certificate(certificate));
+ for (const x509 of certificates) {
+ if (x509.subject === undefined)
+ throw new Error('Missing certificate subject');
+ }
+ let index = 0;
+ return { run: () => certificates[index++ & 63].subject };
+ }
+ default:
+ throw new Error(`Unsupported operation: ${operation}`);
+ }
+}
+
+function main({ operation, n }) {
+ const state = setup(operation);
+ let result;
+
+ bench.start();
+ for (let i = 0; i < n; ++i)
+ result = state.run();
+ bench.end(n);
+
+ if (state.finish)
+ state.finish();
+ if (result === state)
+ throw new Error('Unexpected benchmark result');
+}
diff --git a/benchmark/crypto/create-cipheriv.js b/benchmark/crypto/create-cipheriv.js
new file mode 100644
index 000000000000..7774e393b403
--- /dev/null
+++ b/benchmark/crypto/create-cipheriv.js
@@ -0,0 +1,62 @@
+'use strict';
+
+const common = require('../common.js');
+const assert = require('node:assert');
+const {
+ createCipheriv,
+ createDecipheriv,
+ getCiphers,
+} = require('node:crypto');
+
+const configurations = {
+ 'aes-128-cbc': { keyLength: 16, ivLength: 16 },
+ 'aes-128-gcm': { keyLength: 16, ivLength: 12 },
+ 'aes-128-cbc-cts': { keyLength: 16, ivLength: 16 },
+ 'aes-128-wrap-inv': { keyLength: 16, ivLength: 8 },
+ 'aes128-wrap-inv': {
+ keyLength: 16,
+ ivLength: 8,
+ warmupCipher: 'aes-128-wrap-inv',
+ },
+};
+
+const ciphers = ['aes-128-cbc', 'aes-128-gcm'];
+const availableCiphers = new Set(getCiphers());
+for (const cipher of [
+ 'aes-128-cbc-cts',
+ 'aes-128-wrap-inv',
+ 'aes128-wrap-inv',
+]) {
+ if (availableCiphers.has(cipher)) {
+ ciphers.push(cipher);
+ }
+}
+
+const bench = common.createBenchmark(main, {
+ n: [1e5],
+ cipher: ciphers,
+ operation: ['encrypt', 'decrypt'],
+});
+
+function main({ n, cipher, operation }) {
+ const {
+ keyLength,
+ ivLength,
+ warmupCipher = cipher,
+ } = configurations[cipher];
+ const key = Buffer.alloc(keyLength);
+ const iv = Buffer.alloc(ivLength);
+ const results = new Array(n);
+ const method = operation === 'encrypt' ? createCipheriv : createDecipheriv;
+
+ const warmup = method(warmupCipher, key, iv);
+ assert.strictEqual(typeof warmup, 'object');
+
+ bench.start();
+ for (let i = 0; i < n; ++i) {
+ results[i] = method(cipher, key, iv);
+ }
+ bench.end(n);
+
+ assert.strictEqual(typeof results[n - 1], 'object');
+}
diff --git a/benchmark/crypto/ecdh-compute-secret.js b/benchmark/crypto/ecdh-compute-secret.js
new file mode 100644
index 000000000000..3061c5b2cc36
--- /dev/null
+++ b/benchmark/crypto/ecdh-compute-secret.js
@@ -0,0 +1,117 @@
+'use strict';
+
+const common = require('../common.js');
+const assert = require('node:assert');
+const crypto = require('node:crypto');
+
+const kCurve = 'prime256v1';
+const kPeerPoolSize = 32;
+const scenarios = [
+ 'first-after-generate',
+ 'full-lifecycle',
+ 'reused-local-same-peer',
+ 'reused-local-peer-pool',
+];
+
+const bench = common.createBenchmark(main, {
+ scenario: scenarios,
+ n: [5_000],
+}, {
+ test: { scenario: 'first-after-generate', n: 1 },
+});
+
+function generateContext() {
+ const context = crypto.createECDH(kCurve);
+ context.generateKeys();
+ return context;
+}
+
+function verifySecret(secret, local, peer) {
+ assert.deepStrictEqual(secret, peer.computeSecret(local.getPublicKey()));
+}
+
+function firstAfterGenerate(n) {
+ const peer = generateContext();
+ const peerPublicKey = peer.getPublicKey();
+ const warmup = generateContext();
+ warmup.computeSecret(peerPublicKey);
+
+ const locals = Array.from({ length: n }, generateContext);
+ const secrets = new Array(n);
+
+ bench.start();
+ for (let i = 0; i < n; i++)
+ secrets[i] = locals[i].computeSecret(peerPublicKey);
+ bench.end(n);
+
+ verifySecret(secrets[n - 1], locals[n - 1], peer);
+}
+
+function fullLifecycle(n) {
+ const peer = generateContext();
+ const peerPublicKey = peer.getPublicKey();
+ const warmup = generateContext();
+ warmup.computeSecret(peerPublicKey);
+
+ const locals = new Array(n);
+ const secrets = new Array(n);
+
+ bench.start();
+ for (let i = 0; i < n; i++) {
+ const local = locals[i] = generateContext();
+ secrets[i] = local.computeSecret(peerPublicKey);
+ }
+ bench.end(n);
+
+ verifySecret(secrets[n - 1], locals[n - 1], peer);
+}
+
+function reusedLocalSamePeer(n) {
+ const local = generateContext();
+ const peer = generateContext();
+ const peerPublicKey = peer.getPublicKey();
+ local.computeSecret(peerPublicKey);
+
+ const secrets = new Array(n);
+
+ bench.start();
+ for (let i = 0; i < n; i++)
+ secrets[i] = local.computeSecret(peerPublicKey);
+ bench.end(n);
+
+ verifySecret(secrets[n - 1], local, peer);
+}
+
+function reusedLocalPeerPool(n) {
+ const local = generateContext();
+ const peers = Array.from(
+ { length: Math.min(n, kPeerPoolSize) },
+ generateContext);
+ const peerPublicKeys = peers.map((peer) => peer.getPublicKey());
+ local.computeSecret(peerPublicKeys[0]);
+
+ const secrets = new Array(n);
+
+ bench.start();
+ for (let i = 0; i < n; i++)
+ secrets[i] = local.computeSecret(peerPublicKeys[i % peers.length]);
+ bench.end(n);
+
+ const lastPeer = peers[(n - 1) % peers.length];
+ verifySecret(secrets[n - 1], local, lastPeer);
+}
+
+function main({ scenario, n }) {
+ switch (scenario) {
+ case 'first-after-generate':
+ return firstAfterGenerate(n);
+ case 'full-lifecycle':
+ return fullLifecycle(n);
+ case 'reused-local-same-peer':
+ return reusedLocalSamePeer(n);
+ case 'reused-local-peer-pool':
+ return reusedLocalPeerPool(n);
+ default:
+ throw new Error(`Unsupported scenario: ${scenario}`);
+ }
+}
diff --git a/benchmark/crypto/mac.js b/benchmark/crypto/mac.js
new file mode 100644
index 000000000000..d1028fa414e6
--- /dev/null
+++ b/benchmark/crypto/mac.js
@@ -0,0 +1,254 @@
+'use strict';
+
+const common = require('../common.js');
+const { hasOpenSSL } = require('../../test/common/crypto.js');
+const assert = require('node:assert');
+const {
+ createHmac,
+ createMac,
+ getMacs,
+} = require('node:crypto');
+
+if (!hasOpenSSL(3) ||
+ process.features.openssl_is_boringssl ||
+ typeof createMac !== 'function' ||
+ typeof getMacs !== 'function') {
+ console.log('Skipping: generic MAC API requires OpenSSL >= 3');
+ process.exit(0);
+}
+
+const operations = [
+ 'get-macs-cold',
+ 'get-macs-warm',
+ 'create-cold',
+ 'create-warm',
+ 'hmac-lifecycle',
+ 'mac-lifecycle',
+ 'mac-stream-lifecycle',
+ 'update',
+ 'stream',
+ 'final-buffer',
+ 'final-hex',
+];
+const configurations = {
+ 'hmac-sha256': {
+ algorithm: 'HMAC',
+ key: Buffer.alloc(32, 0x42),
+ options: { digest: 'SHA256' },
+ },
+ 'kmac-128': {
+ algorithm: 'KMAC-128',
+ key: Buffer.alloc(32, 0x42),
+ options: { outputLength: 32 },
+ },
+};
+
+const bench = common.createBenchmark(main, {
+ operation: operations,
+ algorithm: Object.keys(configurations),
+ length: [0, 64, 4096],
+ n: [1, 10_000, 20_000, 500_000],
+}, {
+ combinationFilter({ operation, algorithm, length, n }) {
+ if (operation === 'get-macs-cold') {
+ return algorithm === 'hmac-sha256' && length === 0 && n === 1;
+ }
+ if (operation === 'get-macs-warm') {
+ return algorithm === 'hmac-sha256' && length === 0 && n === 500_000;
+ }
+ if (operation === 'create-cold')
+ return length === 0 && n === 1;
+ if (operation === 'create-warm')
+ return length === 0 && n === 20_000;
+ if (operation === 'hmac-lifecycle') {
+ return algorithm === 'hmac-sha256' && n === 10_000;
+ }
+ if (operation === 'mac-lifecycle' ||
+ operation === 'mac-stream-lifecycle') {
+ return n === 10_000;
+ }
+ if (operation === 'update' || operation === 'stream') {
+ return length === 64 && n === 500_000;
+ }
+ if (operation === 'final-buffer' || operation === 'final-hex') {
+ return algorithm === 'hmac-sha256' &&
+ length === 64 &&
+ n === 20_000;
+ }
+ return false;
+ },
+ test: {
+ operation: ['create-cold'],
+ algorithm: ['hmac-sha256'],
+ length: [0],
+ n: [1],
+ },
+});
+
+function main({ operation, algorithm, length, n }) {
+ const configuration = configurations[algorithm];
+ const data = Buffer.alloc(length, 0x61);
+
+ switch (operation) {
+ case 'get-macs-cold':
+ measureGetMacs(n, false);
+ break;
+ case 'get-macs-warm':
+ measureGetMacs(n, true);
+ break;
+ case 'create-cold':
+ measureCreate(configuration, n, false);
+ break;
+ case 'create-warm':
+ measureCreate(configuration, n, true);
+ break;
+ case 'hmac-lifecycle':
+ measureHmacLifecycle(configuration, data, n);
+ break;
+ case 'mac-lifecycle':
+ measureMacLifecycle(configuration, data, n);
+ break;
+ case 'mac-stream-lifecycle':
+ measureMacStreamLifecycle(configuration, data, n);
+ break;
+ case 'update':
+ measureUpdate(configuration, data, n);
+ break;
+ case 'stream':
+ measureStream(configuration, data, n);
+ break;
+ case 'final-buffer':
+ measureFinal(configuration, data, n);
+ break;
+ case 'final-hex':
+ measureFinal(configuration, data, n, 'hex');
+ break;
+ default:
+ throw new Error(`unknown operation: ${operation}`);
+ }
+}
+
+function measureGetMacs(n, warm) {
+ if (warm)
+ getMacs();
+
+ let result;
+ bench.start();
+ for (let i = 0; i < n; ++i)
+ result = getMacs();
+ bench.end(n);
+
+ assert(Array.isArray(result));
+}
+
+function measureCreate({ algorithm, key, options }, n, warm) {
+ if (warm)
+ createMac(algorithm, key, options).final();
+
+ const contexts = new Array(n);
+ bench.start();
+ for (let i = 0; i < n; ++i)
+ contexts[i] = createMac(algorithm, key, options);
+ bench.end(n);
+
+ assert.strictEqual(typeof contexts[n - 1], 'object');
+}
+
+function measureHmacLifecycle({ key, options }, data, n) {
+ createHmac(options.digest, key).update(data).digest();
+
+ let result;
+ bench.start();
+ for (let i = 0; i < n; ++i)
+ result = createHmac(options.digest, key).update(data).digest();
+ bench.end(n);
+
+ assert(Buffer.isBuffer(result));
+}
+
+function measureMacLifecycle({ algorithm, key, options }, data, n) {
+ createMac(algorithm, key, options).update(data).final();
+
+ let result;
+ bench.start();
+ for (let i = 0; i < n; ++i)
+ result = createMac(algorithm, key, options).update(data).final();
+ bench.end(n);
+
+ assert(Buffer.isBuffer(result));
+}
+
+function measureMacStreamLifecycle({ algorithm, key, options }, data, n) {
+ const warmup = createMac(algorithm, key, options);
+ warmup.end(data);
+ warmup.read();
+
+ let result;
+ bench.start();
+ for (let i = 0; i < n; ++i) {
+ const context = createMac(algorithm, key, options);
+ context.end(data);
+ result = context.read();
+ }
+ bench.end(n);
+
+ assert(Buffer.isBuffer(result));
+}
+
+function measureUpdate({ algorithm, key, options }, data, n) {
+ const warmup = createMac(algorithm, key, options);
+ warmup.update(data).final();
+
+ const context = createMac(algorithm, key, options);
+ bench.start();
+ for (let i = 0; i < n; ++i)
+ context.update(data);
+ bench.end(n);
+
+ assert(Buffer.isBuffer(context.final()));
+}
+
+function measureStream({ algorithm, key, options }, data, n) {
+ const warmup = createMac(algorithm, key, options);
+ warmup.end(data);
+ warmup.read();
+
+ const context = createMac(algorithm, key, options);
+ bench.start();
+ for (let i = 0; i < n; ++i)
+ context.write(data);
+ bench.end(n);
+
+ context.end();
+ assert(Buffer.isBuffer(context.read()));
+}
+
+function measureFinal({ algorithm, key, options }, data, n, encoding) {
+ const warmup = createMac(algorithm, key, options).update(data);
+ if (encoding === undefined)
+ warmup.final();
+ else
+ warmup.final(encoding);
+
+ const contexts = new Array(n);
+ for (let i = 0; i < n; ++i)
+ contexts[i] = createMac(algorithm, key, options).update(data);
+
+ let result;
+ if (encoding === undefined) {
+ bench.start();
+ for (let i = 0; i < n; ++i)
+ result = contexts[i].final();
+ bench.end(n);
+ } else {
+ bench.start();
+ for (let i = 0; i < n; ++i)
+ result = contexts[i].final(encoding);
+ bench.end(n);
+ }
+
+ if (encoding === undefined)
+ assert(Buffer.isBuffer(result));
+ else
+ assert.strictEqual(typeof result, 'string');
+}
diff --git a/benchmark/diagnostics_channel/tracing-channel-promise.js b/benchmark/diagnostics_channel/tracing-channel-promise.js
new file mode 100644
index 000000000000..594043d63fe4
--- /dev/null
+++ b/benchmark/diagnostics_channel/tracing-channel-promise.js
@@ -0,0 +1,53 @@
+'use strict';
+
+const common = require('../common.js');
+const dc = require('node:diagnostics_channel');
+
+const bench = common.createBenchmark(main, {
+ n: [1e7],
+ context: ['omitted', 'undefined', 'provided'],
+ subscribers: [0, 1],
+});
+
+function noop() {}
+
+const thenable = {
+ then(onResolve) {
+ onResolve(undefined);
+ },
+};
+
+function returnThenable() {
+ return thenable;
+}
+
+function main({ n, context, subscribers }) {
+ const channel = dc.tracingChannel('test');
+ const providedContext = { __proto__: null };
+
+ if (subscribers) {
+ channel.subscribe({ start: noop });
+ }
+
+ bench.start();
+ switch (context) {
+ case 'omitted':
+ for (let i = 0; i < n; i++) {
+ channel.tracePromise(returnThenable);
+ }
+ break;
+ case 'undefined':
+ for (let i = 0; i < n; i++) {
+ channel.tracePromise(returnThenable, undefined);
+ }
+ break;
+ case 'provided':
+ for (let i = 0; i < n; i++) {
+ channel.tracePromise(returnThenable, providedContext);
+ }
+ break;
+ default:
+ throw new Error(`Unsupported context value: ${context}`);
+ }
+ bench.end(n);
+}
diff --git a/benchmark/diagnostics_channel/tracing-channel-sync.js b/benchmark/diagnostics_channel/tracing-channel-sync.js
new file mode 100644
index 000000000000..bfd9dd939cc0
--- /dev/null
+++ b/benchmark/diagnostics_channel/tracing-channel-sync.js
@@ -0,0 +1,43 @@
+'use strict';
+
+const common = require('../common.js');
+const dc = require('node:diagnostics_channel');
+
+const bench = common.createBenchmark(main, {
+ n: [1e7],
+ context: ['omitted', 'undefined', 'provided'],
+ subscribers: [0, 1],
+});
+
+function noop() {}
+
+function main({ n, context, subscribers }) {
+ const channel = dc.tracingChannel('test');
+ const providedContext = { __proto__: null };
+
+ if (subscribers) {
+ channel.subscribe({ start: noop });
+ }
+
+ bench.start();
+ switch (context) {
+ case 'omitted':
+ for (let i = 0; i < n; i++) {
+ channel.traceSync(noop);
+ }
+ break;
+ case 'undefined':
+ for (let i = 0; i < n; i++) {
+ channel.traceSync(noop, undefined);
+ }
+ break;
+ case 'provided':
+ for (let i = 0; i < n; i++) {
+ channel.traceSync(noop, providedContext);
+ }
+ break;
+ default:
+ throw new Error(`Unsupported context value: ${context}`);
+ }
+ bench.end(n);
+}
diff --git a/benchmark/ffi/add-64.js b/benchmark/ffi/add-64.js
index 1af3de2296a5..d3b8f9e21e03 100644
--- a/benchmark/ffi/add-64.js
+++ b/benchmark/ffi/add-64.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/add-f32.js b/benchmark/ffi/add-f32.js
index a958e80c1819..afb42a4a48e5 100644
--- a/benchmark/ffi/add-f32.js
+++ b/benchmark/ffi/add-f32.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/add-f64.js b/benchmark/ffi/add-f64.js
index f4e1dbac4ac3..a4aec85fa935 100644
--- a/benchmark/ffi/add-f64.js
+++ b/benchmark/ffi/add-f64.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/add-i16.js b/benchmark/ffi/add-i16.js
index 8cd6f989e180..0ce2530d5d33 100644
--- a/benchmark/ffi/add-i16.js
+++ b/benchmark/ffi/add-i16.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/add-i32.js b/benchmark/ffi/add-i32.js
index 9a77a6f998f7..451c0c4e0836 100644
--- a/benchmark/ffi/add-i32.js
+++ b/benchmark/ffi/add-i32.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/add-i64.js b/benchmark/ffi/add-i64.js
index 753a300b4394..b72e399d4135 100644
--- a/benchmark/ffi/add-i64.js
+++ b/benchmark/ffi/add-i64.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/add-i8.js b/benchmark/ffi/add-i8.js
index 9e506b2ad76f..b0b0111f88c7 100644
--- a/benchmark/ffi/add-i8.js
+++ b/benchmark/ffi/add-i8.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/add-u16.js b/benchmark/ffi/add-u16.js
index 2c1408a355a2..26161815bde7 100644
--- a/benchmark/ffi/add-u16.js
+++ b/benchmark/ffi/add-u16.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/add-u64.js b/benchmark/ffi/add-u64.js
index 260d24971610..6a021ad5c854 100644
--- a/benchmark/ffi/add-u64.js
+++ b/benchmark/ffi/add-u64.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/add-u8.js b/benchmark/ffi/add-u8.js
index 3525d4c3d263..3c9512a7869b 100644
--- a/benchmark/ffi/add-u8.js
+++ b/benchmark/ffi/add-u8.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/buffer-first-byte-direct.js b/benchmark/ffi/buffer-first-byte-direct.js
index 60370419edb7..d09706144702 100644
--- a/benchmark/ffi/buffer-first-byte-direct.js
+++ b/benchmark/ffi/buffer-first-byte-direct.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/buffer-first-byte.js b/benchmark/ffi/buffer-first-byte.js
index 311cd8e89fac..02b8f190d5e9 100644
--- a/benchmark/ffi/buffer-first-byte.js
+++ b/benchmark/ffi/buffer-first-byte.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/buffer-sum-direct.js b/benchmark/ffi/buffer-sum-direct.js
index 73b6d149cb76..425832330047 100644
--- a/benchmark/ffi/buffer-sum-direct.js
+++ b/benchmark/ffi/buffer-sum-direct.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/buffer-sum.js b/benchmark/ffi/buffer-sum.js
index e4108db09a65..fd5ba47363d0 100644
--- a/benchmark/ffi/buffer-sum.js
+++ b/benchmark/ffi/buffer-sum.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/get-function.js b/benchmark/ffi/get-function.js
index 3c1e2e974ce6..939c1630ab64 100644
--- a/benchmark/ffi/get-function.js
+++ b/benchmark/ffi/get-function.js
@@ -16,8 +16,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
signature: ['fast', 'slow'],
n: [1e3],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/getpid.js b/benchmark/ffi/getpid.js
index 8008f407305e..3463a4afe807 100644
--- a/benchmark/ffi/getpid.js
+++ b/benchmark/ffi/getpid.js
@@ -5,8 +5,6 @@ const ffi = require('node:ffi');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
const { lib, functions } = ffi.dlopen(null, {
diff --git a/benchmark/ffi/identity-i32.js b/benchmark/ffi/identity-i32.js
index 0014d652777f..388645e328a2 100644
--- a/benchmark/ffi/identity-i32.js
+++ b/benchmark/ffi/identity-i32.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/invoke-function.js b/benchmark/ffi/invoke-function.js
index ae8d5b2ef795..9e461680b7ee 100644
--- a/benchmark/ffi/invoke-function.js
+++ b/benchmark/ffi/invoke-function.js
@@ -23,7 +23,7 @@ const bench = common.createBenchmark(main, {
n: [1e7],
symbol: ['call_int_callback', 'sum_8_i32'],
}, {
- flags: ['--experimental-ffi', '--no-warnings'],
+ flags: ['--no-warnings'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/many-args.js b/benchmark/ffi/many-args.js
index bc269ace0c3e..7cd5a4d685ce 100644
--- a/benchmark/ffi/many-args.js
+++ b/benchmark/ffi/many-args.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/noop-void.js b/benchmark/ffi/noop-void.js
index b9dd68636254..ff606ebb8586 100644
--- a/benchmark/ffi/noop-void.js
+++ b/benchmark/ffi/noop-void.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/pointer-bigint.js b/benchmark/ffi/pointer-bigint.js
index 9ea01d28490e..6be9577f45b7 100644
--- a/benchmark/ffi/pointer-bigint.js
+++ b/benchmark/ffi/pointer-bigint.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/pointer-buffer-direct.js b/benchmark/ffi/pointer-buffer-direct.js
index 681800c6e02d..0813e732d6ae 100644
--- a/benchmark/ffi/pointer-buffer-direct.js
+++ b/benchmark/ffi/pointer-buffer-direct.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/pointer-buffer.js b/benchmark/ffi/pointer-buffer.js
index 4aa038558367..e19a1a035388 100644
--- a/benchmark/ffi/pointer-buffer.js
+++ b/benchmark/ffi/pointer-buffer.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/pointer-null.js b/benchmark/ffi/pointer-null.js
index c9836b12604e..975e03b739f4 100644
--- a/benchmark/ffi/pointer-null.js
+++ b/benchmark/ffi/pointer-null.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/string-equals-hello-buffer-direct.js b/benchmark/ffi/string-equals-hello-buffer-direct.js
index 0a4ea8b97112..a05902a54f1f 100644
--- a/benchmark/ffi/string-equals-hello-buffer-direct.js
+++ b/benchmark/ffi/string-equals-hello-buffer-direct.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/string-equals-hello-buffer.js b/benchmark/ffi/string-equals-hello-buffer.js
index 7757586d472c..eb7839d48508 100644
--- a/benchmark/ffi/string-equals-hello-buffer.js
+++ b/benchmark/ffi/string-equals-hello-buffer.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/string-first-char-buffer-direct.js b/benchmark/ffi/string-first-char-buffer-direct.js
index 693705c94587..641084cdf632 100644
--- a/benchmark/ffi/string-first-char-buffer-direct.js
+++ b/benchmark/ffi/string-first-char-buffer-direct.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/string-first-char-buffer.js b/benchmark/ffi/string-first-char-buffer.js
index e8a79ac41ad6..cac665039390 100644
--- a/benchmark/ffi/string-first-char-buffer.js
+++ b/benchmark/ffi/string-first-char-buffer.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/string-length-buffer-direct.js b/benchmark/ffi/string-length-buffer-direct.js
index cfbb819d8026..b2de28b209e8 100644
--- a/benchmark/ffi/string-length-buffer-direct.js
+++ b/benchmark/ffi/string-length-buffer-direct.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/string-length-buffer.js b/benchmark/ffi/string-length-buffer.js
index 3b0ea8aef13a..36d11db86420 100644
--- a/benchmark/ffi/string-length-buffer.js
+++ b/benchmark/ffi/string-length-buffer.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/string-length-string-direct.js b/benchmark/ffi/string-length-string-direct.js
index cfbb819d8026..b2de28b209e8 100644
--- a/benchmark/ffi/string-length-string-direct.js
+++ b/benchmark/ffi/string-length-string-direct.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/string-length-string.js b/benchmark/ffi/string-length-string.js
index 9cddfdf4e950..23284973405d 100644
--- a/benchmark/ffi/string-length-string.js
+++ b/benchmark/ffi/string-length-string.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/sum-3-i32.js b/benchmark/ffi/sum-3-i32.js
index 174979183819..9e3f842b54ca 100644
--- a/benchmark/ffi/sum-3-i32.js
+++ b/benchmark/ffi/sum-3-i32.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/sum-5-i32.js b/benchmark/ffi/sum-5-i32.js
index ecc784a3d045..490691ad2b33 100644
--- a/benchmark/ffi/sum-5-i32.js
+++ b/benchmark/ffi/sum-5-i32.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/sum-8-i32.js b/benchmark/ffi/sum-8-i32.js
index 8c740030307a..b1d53d693bfa 100644
--- a/benchmark/ffi/sum-8-i32.js
+++ b/benchmark/ffi/sum-8-i32.js
@@ -6,8 +6,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
n: [1e7],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/ffi/sum-buffer.js b/benchmark/ffi/sum-buffer.js
index 60e25a854185..da87aa28ebd4 100644
--- a/benchmark/ffi/sum-buffer.js
+++ b/benchmark/ffi/sum-buffer.js
@@ -7,8 +7,6 @@ const { libraryPath, ensureFixtureLibrary } = require('./common.js');
const bench = common.createBenchmark(main, {
size: [64, 1024, 16384],
n: [1e6],
-}, {
- flags: ['--experimental-ffi'],
});
ensureFixtureLibrary();
diff --git a/benchmark/fs/bench-cp.js b/benchmark/fs/bench-cp.js
new file mode 100644
index 000000000000..ffaeb87705f5
--- /dev/null
+++ b/benchmark/fs/bench-cp.js
@@ -0,0 +1,33 @@
+'use strict';
+
+// fs.promises.cp() of a directory tree.
+
+const common = require('../common');
+const fs = require('fs');
+const path = require('path');
+const tmpdir = require('../../test/common/tmpdir');
+
+const bench = common.createBenchmark(main, {
+ files: [500],
+ n: [3],
+});
+
+function prepareSource(files) {
+ const src = tmpdir.resolve('cp-src');
+ for (let i = 0; i < files; i++) {
+ const dir = path.join(src, `dir-${i % 10}`, `sub-${i % 7}`);
+ fs.mkdirSync(dir, { recursive: true });
+ fs.writeFileSync(path.join(dir, `file-${i}.js`), 'x'.repeat(1024 + (i % 512)));
+ }
+ return src;
+}
+
+async function main({ files, n }) {
+ tmpdir.refresh();
+ const src = prepareSource(files);
+ bench.start();
+ for (let i = 0; i < n; i++) {
+ await fs.promises.cp(src, tmpdir.resolve(`cp-dest-${i}`), { recursive: true });
+ }
+ bench.end(n);
+}
diff --git a/benchmark/fs/bench-glob.js b/benchmark/fs/bench-glob.js
index 74612701e218..4652a48f5241 100644
--- a/benchmark/fs/bench-glob.js
+++ b/benchmark/fs/bench-glob.js
@@ -16,6 +16,7 @@ const configs = {
dir: ['lib'],
pattern: ['**/*', '*.js', '**/**.js'],
mode: ['sync', 'promise', 'callback'],
+ maxDepth: ['default', '2'],
recursive: ['true', 'false'],
};
@@ -23,8 +24,11 @@ const bench = common.createBenchmark(main, configs);
async function main(config) {
const fullPath = path.resolve(benchmarkDirectory, config.dir);
- const { pattern, recursive, mode } = config;
+ const { pattern, recursive, mode, maxDepth } = config;
const options = { cwd: fullPath, recursive };
+ if (maxDepth !== 'default') {
+ options.maxDepth = Number(maxDepth);
+ }
const callback = (resolve, reject) => {
glob(pattern, options, (err, matches) => {
if (err) {
@@ -44,7 +48,10 @@ async function main(config) {
noDead = globSync(pattern, options);
break;
case 'promise':
- noDead = await globAsync(pattern, options);
+ noDead = [];
+ for await (const match of globAsync(pattern, options)) {
+ noDead.push(match);
+ }
break;
case 'callback':
noDead = await new Promise(callback);
diff --git a/benchmark/fs/bench-readdir-recursive.js b/benchmark/fs/bench-readdir-recursive.js
new file mode 100644
index 000000000000..6ed56995dcea
--- /dev/null
+++ b/benchmark/fs/bench-readdir-recursive.js
@@ -0,0 +1,47 @@
+'use strict';
+
+const common = require('../common');
+const fs = require('fs');
+const path = require('path');
+const assert = require('assert');
+
+const bench = common.createBenchmark(main, {
+ n: [10],
+ dir: ['lib', 'test/parallel', 'test'],
+ mode: ['sync', 'callback', 'promise'],
+ withFileTypes: ['true', 'false'],
+});
+
+async function main({ n, dir, mode, withFileTypes }) {
+ withFileTypes = withFileTypes === 'true';
+ const fullPath = path.resolve(__dirname, '../../', dir);
+ const options = { recursive: true, withFileTypes };
+ let entries;
+
+ bench.start();
+ switch (mode) {
+ case 'sync':
+ for (let i = 0; i < n; i++) {
+ entries = fs.readdirSync(fullPath, options);
+ }
+ break;
+ case 'callback':
+ for (let i = 0; i < n; i++) {
+ entries = await new Promise((resolve, reject) => {
+ fs.readdir(fullPath, options, (err, result) => {
+ if (err) reject(err);
+ else resolve(result);
+ });
+ });
+ }
+ break;
+ case 'promise':
+ for (let i = 0; i < n; i++) {
+ entries = await fs.promises.readdir(fullPath, options);
+ }
+ break;
+ }
+ bench.end(n);
+
+ assert.ok(entries.length > 0);
+}
diff --git a/benchmark/fs/bench-readdir.js b/benchmark/fs/bench-readdir.js
index e276653f4584..8fa0e7a3cbdc 100644
--- a/benchmark/fs/bench-readdir.js
+++ b/benchmark/fs/bench-readdir.js
@@ -8,18 +8,16 @@ const bench = common.createBenchmark(main, {
n: [10],
dir: [ 'lib', 'test/parallel'],
withFileTypes: ['true', 'false'],
- recursive: ['true', 'false'],
});
-function main({ n, dir, withFileTypes, recursive }) {
+function main({ n, dir, withFileTypes }) {
withFileTypes = withFileTypes === 'true';
- recursive = recursive === 'true';
const fullPath = path.resolve(__dirname, '../../', dir);
bench.start();
(function r(cntr) {
if (cntr-- <= 0)
return bench.end(n);
- fs.readdir(fullPath, { withFileTypes, recursive }, () => {
+ fs.readdir(fullPath, { withFileTypes }, () => {
r(cntr);
});
}(n));
diff --git a/benchmark/fs/bench-readdirSync.js b/benchmark/fs/bench-readdirSync.js
index ce34e083cb53..8ae1d061d1f1 100644
--- a/benchmark/fs/bench-readdirSync.js
+++ b/benchmark/fs/bench-readdirSync.js
@@ -8,17 +8,15 @@ const bench = common.createBenchmark(main, {
n: [10],
dir: [ 'lib', 'test/parallel'],
withFileTypes: ['true', 'false'],
- recursive: ['true', 'false'],
});
-function main({ n, dir, withFileTypes, recursive }) {
+function main({ n, dir, withFileTypes }) {
withFileTypes = withFileTypes === 'true';
- recursive = recursive === 'true';
const fullPath = path.resolve(__dirname, '../../', dir);
bench.start();
for (let i = 0; i < n; i++) {
- fs.readdirSync(fullPath, { withFileTypes, recursive });
+ fs.readdirSync(fullPath, { withFileTypes });
}
bench.end(n);
}
diff --git a/benchmark/fs/bench-watch-recursive.js b/benchmark/fs/bench-watch-recursive.js
new file mode 100644
index 000000000000..45ed0871718e
--- /dev/null
+++ b/benchmark/fs/bench-watch-recursive.js
@@ -0,0 +1,23 @@
+'use strict';
+
+// Setting up (and tearing down) a recursive fs.watch() on a directory tree.
+// On Linux and other platforms without a native recursive watcher this is
+// implemented in JavaScript on top of per-directory watchers.
+
+const common = require('../common');
+const fs = require('fs');
+const path = require('path');
+
+const bench = common.createBenchmark(main, {
+ n: [5],
+ dir: ['lib', 'test/fixtures'],
+});
+
+function main({ n, dir }) {
+ const fullPath = path.resolve(__dirname, '../../', dir);
+ bench.start();
+ for (let i = 0; i < n; i++) {
+ fs.watch(fullPath, { recursive: true }).close();
+ }
+ bench.end(n);
+}
diff --git a/benchmark/http2/full-duplex.js b/benchmark/http2/full-duplex.js
new file mode 100644
index 000000000000..d2fec719f95a
--- /dev/null
+++ b/benchmark/http2/full-duplex.js
@@ -0,0 +1,71 @@
+'use strict';
+
+const common = require('../common.js');
+const fixtures = require('../../test/common/fixtures');
+
+const bench = common.createBenchmark(main, {
+ n: [100],
+ streams: [2],
+ size: [4 * 1024 * 1024],
+ // Use the HTTP/2 protocol default.
+ window: [65535],
+}, {
+ test: { size: 128 * 1024, window: 65535 },
+});
+
+function main({ n, streams, size, window }) {
+ const http2 = require('http2');
+ const payload = Buffer.alloc(size);
+ const server = http2.createSecureServer({
+ key: fixtures.readKey('agent1-key.pem'),
+ cert: fixtures.readKey('agent1-cert.pem'),
+ settings: { initialWindowSize: window },
+ });
+
+ let completed = 0;
+ let batches = 0;
+
+ function onTransferComplete() {
+ if (++completed !== streams * 2)
+ return;
+
+ if (++batches === n) {
+ // Report combined upload and download throughput in MiB/s.
+ bench.end(n * streams * size * 2 / (1024 * 1024));
+ client.close();
+ server.close();
+ return;
+ }
+
+ startBatch();
+ }
+
+ server.on('stream', (stream) => {
+ stream.resume();
+ stream.on('end', onTransferComplete);
+ stream.respond();
+ stream.end(payload);
+ });
+
+ let client;
+ function startBatch() {
+ completed = 0;
+ for (let i = 0; i < streams; i++) {
+ const request = client.request({ ':method': 'POST' });
+ request.resume();
+ request.on('end', onTransferComplete);
+ request.end(payload);
+ }
+ }
+
+ server.listen(0, () => {
+ client = http2.connect(`https://localhost:${server.address().port}`, {
+ rejectUnauthorized: false,
+ settings: { initialWindowSize: window },
+ });
+ client.on('connect', () => {
+ bench.start();
+ startBatch();
+ });
+ });
+}
diff --git a/benchmark/misc/startup-core.js b/benchmark/misc/startup-core.js
index 414b00176ad2..d673d415b505 100644
--- a/benchmark/misc/startup-core.js
+++ b/benchmark/misc/startup-core.js
@@ -64,6 +64,7 @@ function main({ n, script, mode }) {
const warmup = 3;
const state = { n, finished: -warmup };
if (mode === 'worker') {
+ // eslint-disable-next-line no-global-assign
Worker = require('worker_threads').Worker;
spawnWorker(script, bench, state);
} else {
diff --git a/benchmark/module/module-require-source-map.js b/benchmark/module/module-require-source-map.js
new file mode 100644
index 000000000000..8353ab39fc82
--- /dev/null
+++ b/benchmark/module/module-require-source-map.js
@@ -0,0 +1,77 @@
+'use strict';
+
+// Loading modules that carry source maps, with source map support enabled.
+// This is the cost paid at startup by applications bundled or transpiled with
+// source maps; the maps are only consulted if a stack trace is generated.
+
+const fs = require('fs');
+const path = require('path');
+const common = require('../common.js');
+const tmpdir = require('../../test/common/tmpdir');
+const benchmarkDirectory = tmpdir.resolve('nodejs-benchmark-module-source-map');
+
+const bench = common.createBenchmark(main, {
+ sourceMap: ['none', 'inline', 'external'],
+ n: [1000],
+}, {
+ setup(configs) {
+ tmpdir.refresh();
+ const maxN = configs.reduce((max, c) => Math.max(max, c.n), 0);
+ createModules(maxN);
+ },
+});
+
+function moduleSource(i) {
+ const methods = [];
+ for (let m = 0; m < 40; m++) {
+ methods.push(` method${m}(input) { return [].concat(input).map((item) => ({ item, m: ${m}, service: ${i} })); }`);
+ }
+ return `'use strict';
+class Service${i} {
+ constructor(options = {}) { this.options = { retries: 3, ...options }; }
+${methods.join('\n')}
+}
+function helper${i}(list) { return list.filter(Boolean).slice(0, ${i % 7}); }
+module.exports = { Service${i}, helper${i} };
+`;
+}
+
+function sourceMapFor(i, source) {
+ return JSON.stringify({
+ version: 3,
+ file: `${i}.js`,
+ sources: [`../src/${i}.ts`],
+ sourcesContent: [source],
+ names: [],
+ mappings: 'AAAA;' + 'AACA,MAAM;'.repeat(44),
+ });
+}
+
+function createModules(n) {
+ for (const kind of ['none', 'inline', 'external']) {
+ const dir = path.join(benchmarkDirectory, kind);
+ fs.mkdirSync(dir, { recursive: true });
+ for (let i = 0; i < n; i++) {
+ const source = moduleSource(i);
+ let trailer = '';
+ if (kind === 'inline') {
+ const data = Buffer.from(sourceMapFor(i, source)).toString('base64');
+ trailer = `//# sourceMappingURL=data:application/json;base64,${data}\n`;
+ } else if (kind === 'external') {
+ fs.writeFileSync(path.join(dir, `${i}.js.map`), sourceMapFor(i, source));
+ trailer = `//# sourceMappingURL=${i}.js.map\n`;
+ }
+ fs.writeFileSync(path.join(dir, `${i}.js`), source + trailer);
+ }
+ }
+}
+
+function main({ sourceMap, n }) {
+ process.setSourceMapsEnabled(true);
+ const dir = path.join(benchmarkDirectory, sourceMap);
+ bench.start();
+ for (let i = 0; i < n; i++) {
+ require(path.join(dir, `${i}.js`));
+ }
+ bench.end(n);
+}
diff --git a/benchmark/quic/h3-request.js b/benchmark/quic/h3-request.js
new file mode 100644
index 000000000000..f96a18407ae1
--- /dev/null
+++ b/benchmark/quic/h3-request.js
@@ -0,0 +1,150 @@
+'use strict';
+
+// Measures a complete HTTP/3 exchange: establish a session, send one request
+// and read the whole response. Run in two modes, so the cost of a resumed
+// 0-RTT session can be compared against a full handshake.
+//
+// The 0-RTT mode needs a session ticket, which can only come from an earlier
+// connection. That first connection is made during warmup, outside the
+// measured region, so what is timed is only the resumed exchange.
+
+const common = require('../common.js');
+const fixtures = require('../../test/common/fixtures');
+const { createPrivateKey } = require('crypto');
+
+const bench = common.createBenchmark(main, {
+ // '0rtt' resumes from a ticket and sends the request in the very first
+ // flight; '1rtt' is a fresh session each time. 0-RTT is listed first so
+ // that it is the mode the benchmark CI test exercises.
+ mode: ['0rtt', '1rtt'],
+ n: [500],
+}, { flags: ['--experimental-quic', '--experimental-stream-iter',
+ '--no-warnings'] });
+
+async function main({ mode, n }) {
+ const { listen, connect } = require('node:quic');
+ const { bytes } = require('stream/iter');
+
+ const key = createPrivateKey(fixtures.readKey('agent1-key.pem'));
+ const cert = fixtures.readKey('agent1-cert.pem');
+ const body = new TextEncoder().encode('x'.repeat(256));
+ const decoder = new TextDecoder();
+
+ const request = {
+ ':method': 'GET',
+ ':path': '/',
+ ':scheme': 'https',
+ ':authority': 'localhost',
+ };
+
+ const endpoint = await listen((session) => {
+ session.opened.catch(() => {});
+ session.closed.catch(() => {});
+ session.onstream = (stream) => { stream.closed.catch(() => {}); };
+ }, {
+ sni: { '*': { keys: [key], certs: [cert] } },
+ onheaders() {
+ this.sendHeaders({ ':status': '200' });
+ this.writer.writeSync(body);
+ this.writer.endSync();
+ },
+ endpoint: {
+ maxConnectionsPerHost: 0xFFFF,
+ maxConnectionsTotal: 0xFFFF,
+ sessionCreationRate: 1_000_000,
+ sessionCreationBurst: 1_000_000,
+ },
+ });
+
+ const address = endpoint.address;
+ let received = 0;
+ const onheaders = () => { received++; };
+
+ // A full handshake, one request, one response. When resume is supplied the
+ // request goes out in the first flight, before the handshake completes.
+ async function exchange(resume) {
+ const session = await connect(address, {
+ servername: 'localhost',
+ verifyPeer: 'manual',
+ alpn: 'h3',
+ ...resume,
+ });
+ const stream = await session.createBidirectionalStream({
+ headers: request,
+ onheaders,
+ });
+ if (resume === undefined) await session.opened;
+ const response = decoder.decode(await bytes(stream));
+ if (response.length !== body.length) {
+ throw new Error(`short response: ${response.length}`);
+ }
+ session.close();
+ await session.closed.catch(() => {});
+ return session;
+ }
+
+ // Collect a ticket for the 0-RTT mode from a connection that is not timed.
+ let resume;
+ if (mode === '0rtt') {
+ const { promise, resolve } = Promise.withResolvers();
+ let ticket;
+ let token;
+ const session = await connect(address, {
+ servername: 'localhost',
+ verifyPeer: 'manual',
+ alpn: 'h3',
+ onsessionticket(value) {
+ ticket ??= value;
+ if (token !== undefined) resolve();
+ },
+ onnewtoken(value) {
+ token ??= value;
+ if (ticket !== undefined) resolve();
+ },
+ });
+ await session.opened;
+ await promise;
+ session.close();
+ await session.closed.catch(() => {});
+ resume = { sessionTicket: ticket, token };
+ }
+
+ // The timed 0-RTT exchanges deliberately never await session.opened, since
+ // waiting for the handshake is exactly what 0-RTT avoids. That leaves no
+ // opportunity to notice early data being refused, so check separately -
+ // otherwise a ticket the server stopped accepting would quietly turn this
+ // into a measurement of the 1-RTT path.
+ async function checkEarlyDataAccepted() {
+ const session = await connect(address, {
+ servername: 'localhost',
+ verifyPeer: 'manual',
+ alpn: 'h3',
+ ...resume,
+ });
+ const stream = await session.createBidirectionalStream({
+ headers: request,
+ onheaders,
+ });
+ const info = await session.opened;
+ await bytes(stream);
+ session.close();
+ await session.closed.catch(() => {});
+ if (!info.earlyDataAccepted) {
+ throw new Error('0-RTT was not accepted, benchmark would be invalid');
+ }
+ }
+
+ for (let i = 0; i < 20; i++) await exchange(resume);
+ if (mode === '0rtt') await checkEarlyDataAccepted();
+
+ received = 0;
+ bench.start();
+ for (let i = 0; i < n; i++) await exchange(resume);
+ bench.end(n);
+
+ if (received !== n) throw new Error(`missing responses: ${received}/${n}`);
+ // The ticket is reused for every iteration, so confirm it was still being
+ // accepted at the end of the run and not just at the start.
+ if (mode === '0rtt') await checkEarlyDataAccepted();
+ await endpoint.close();
+}
diff --git a/benchmark/quic/handshake.js b/benchmark/quic/handshake.js
new file mode 100644
index 000000000000..9f0404008e03
--- /dev/null
+++ b/benchmark/quic/handshake.js
@@ -0,0 +1,74 @@
+'use strict';
+
+// Measures the cost of establishing QUIC sessions: how many complete
+// handshakes per second a single endpoint can serve, for raw QUIC and for
+// HTTP/3. Nothing is sent on the session beyond what the protocol itself
+// requires, so this isolates connection setup rather than data transfer.
+
+const common = require('../common.js');
+const fixtures = require('../../test/common/fixtures');
+const { createPrivateKey } = require('crypto');
+
+const bench = common.createBenchmark(main, {
+ // 'raw' negotiates a non-HTTP ALPN and does no application work.
+ // 'h3' negotiates HTTP/3, so the server also builds an nghttp3 connection
+ // and its control/QPACK streams for every session.
+ protocol: ['raw', 'h3'],
+ concurrency: [1, 10],
+ n: [1000],
+}, { flags: ['--experimental-quic', '--no-warnings'] });
+
+async function main({ protocol, concurrency, n }) {
+ const { listen, connect } = require('node:quic');
+
+ const key = createPrivateKey(fixtures.readKey('agent1-key.pem'));
+ const cert = fixtures.readKey('agent1-cert.pem');
+ const alpn = protocol === 'h3' ? 'h3' : 'quic-bench';
+
+ const endpoint = await listen((session) => {
+ // A benchmark peer never reads these; swallow so a torn-down session
+ // cannot produce an unhandled rejection.
+ session.opened.catch(() => {});
+ session.closed.catch(() => {});
+ }, {
+ sni: { '*': { keys: [key], certs: [cert] } },
+ alpn: [alpn],
+ // The defaults rate-limit session creation per host, which a benchmark
+ // hammering a single address would otherwise trip.
+ endpoint: {
+ maxConnectionsPerHost: 0xFFFF,
+ maxConnectionsTotal: 0xFFFF,
+ sessionCreationRate: 1_000_000,
+ sessionCreationBurst: 1_000_000,
+ },
+ });
+
+ const address = endpoint.address;
+
+ async function handshake() {
+ const session = await connect(address, {
+ servername: 'localhost',
+ verifyPeer: 'manual',
+ alpn,
+ });
+ await session.opened;
+ session.close();
+ await session.closed.catch(() => {});
+ }
+
+ async function run(count) {
+ for (let i = 0; i < count; i += concurrency) {
+ const batch = Math.min(concurrency, count - i);
+ await Promise.all(Array.from({ length: batch }, handshake));
+ }
+ }
+
+ // Warm up the TLS and QUIC machinery before measuring.
+ await run(Math.min(100, n));
+
+ bench.start();
+ await run(n);
+ bench.end(n);
+
+ await endpoint.close();
+}
diff --git a/benchmark/scatter-node-bench.js b/benchmark/scatter-node-bench.js
new file mode 100644
index 000000000000..4527ecc6b8b4
--- /dev/null
+++ b/benchmark/scatter-node-bench.js
@@ -0,0 +1,131 @@
+'use strict';
+
+const CLI = require('./_cli.js');
+const {
+ analyzeScatter,
+ validateScatterParameters,
+} = require('./_node-bench-analysis.js');
+const {
+ csvEncode,
+ parseInteger,
+ runBenchmark,
+} = require('./_node-bench.js');
+
+const cli = new CLI(`usage: ./node scatter-node-bench.js [options] [--]
+ Run an explicit node:bench file repeatedly and output each independent
+ observation with its benchmark parameters as CSV, or summarize the results
+ directly with --analyze.
+
+ --node ./node Node.js binary
+ --runs 30 number of observations
+ --warmup 0 warmup samples before each observation
+ --name-pattern pattern only run matching benchmarks
+ --node-arg argument pass an argument to the binary (repeatable)
+ --analyze print a statistical summary instead of CSV
+ --xaxis parameter parameter to group by with --analyze (required)
+ --category parameter optional second grouping parameter
+ --no-chart omit the analysis bar chart
+`, {
+ arrayArgs: ['node-arg'],
+ boolArgs: ['analyze', 'no-chart'],
+});
+
+if (cli.items.length !== 1) cli.abort(cli.usage);
+if (cli.optional.analyze && cli.optional.xaxis === undefined) {
+ cli.abort('--analyze requires --xaxis ');
+}
+
+async function main() {
+ const runs = parseInteger(cli.optional.runs, 30, '--runs', 1);
+ const warmup = parseInteger(cli.optional.warmup, 0, '--warmup', 0);
+ const options = {
+ namePattern: cli.optional['name-pattern'],
+ nodeArgs: cli.optional['node-arg'],
+ warmup,
+ };
+ const binary = cli.optional.node || process.execPath;
+ const rows = [];
+ const paramNames = new Set();
+ const csvGroups = new Map();
+ let expectedIdentities;
+ let logicalIdentity;
+
+ for (let run = 0; run < runs; run++) {
+ const samples = await runBenchmark(binary, cli.items[0], options);
+ if (run === 0 && cli.optional.analyze) {
+ validateScatterParameters(
+ samples, cli.optional.xaxis, cli.optional.category);
+ }
+ const identities = new Set();
+ for (const sample of samples) {
+ if (logicalIdentity !== undefined &&
+ logicalIdentity !== sample.logicalIdentity) {
+ throw new Error(
+ 'scatter-node-bench.js requires one logical benchmark name per file',
+ );
+ }
+ logicalIdentity = sample.logicalIdentity;
+ if (identities.has(sample.identity)) {
+ throw new Error(`Benchmark '${sample.name}' was reported more than once`);
+ }
+ identities.add(sample.identity);
+ const csvGroup = JSON.stringify([sample.name, sample.params]);
+ const groupedIdentity = csvGroups.get(csvGroup);
+ if (groupedIdentity !== undefined &&
+ groupedIdentity !== sample.identity) {
+ throw new Error(
+ `Distinct benchmarks would share the CSV group '${sample.name}'`,
+ );
+ }
+ csvGroups.set(csvGroup, sample.identity);
+ rows.push({ ...sample, observation: run });
+ for (const name of Object.keys(sample.params)) paramNames.add(name);
+ }
+ if (expectedIdentities === undefined) {
+ expectedIdentities = identities;
+ } else if (identities.size !== expectedIdentities.size ||
+ ![...identities].every((id) => expectedIdentities.has(id))) {
+ throw new Error('The set of reported benchmarks changed between runs');
+ }
+ }
+ if (rows.length === 0) {
+ throw new Error('No benchmark samples were produced');
+ }
+
+ const params = [...paramNames].sort();
+ if (cli.optional.analyze) {
+ const output = analyzeScatter(
+ rows,
+ cli.optional.xaxis,
+ cli.optional.category,
+ !cli.optional['no-chart'],
+ );
+ process.stdout.write(output);
+ return;
+ }
+
+ for (const name of params) {
+ if (name === 'filename' || name === 'rate' || name === 'time') {
+ throw new Error(`Benchmark parameter '${name}' is reserved in scatter CSV`);
+ }
+ }
+ const header = ['filename', ...params, 'rate', 'time']
+ .map(csvEncode)
+ .join(',');
+ const output = [header];
+ for (const row of rows) {
+ const values = [
+ csvEncode(row.name),
+ ...params.map((name) => csvEncode(row.params[name] ?? '')),
+ row.rate,
+ row.duration,
+ ];
+ output.push(values.join(','));
+ }
+ process.stdout.write(`${output.join('\n')}\n`);
+}
+
+main().catch((error) => {
+ console.error(error.stack);
+ process.exitCode = 1;
+});
diff --git a/benchmark/scatter.js b/benchmark/scatter.js
index 858169d7d68c..cb1df965b70f 100644
--- a/benchmark/scatter.js
+++ b/benchmark/scatter.js
@@ -1,62 +1,167 @@
'use strict';
-const fork = require('child_process').fork;
+const { spawn, fork } = require('node:child_process');
+const { createHistogram } = require('node:perf_hooks');
+const { inspect } = require('util');
const path = require('path');
const CLI = require('./_cli.js');
+const BenchmarkProgress = require('./_benchmark_progress.js');
//
// Parse arguments
//
const cli = new CLI(`usage: ./node scatter.js [options] [--]
Run the benchmark script many times and output the rate (ops/s)
- together with the benchmark variables as a csv.
+ together with the benchmark variables as a csv, which can be processed using
+ for example 'scatter.R'. Use --analyze to summarize the results directly
+ without R.
--runs 30 number of samples
--set variable=value set benchmark variable (can be repeated)
-`, { arrayArgs: ['set'] });
+ --no-progress don't show benchmark progress indicator
+ --analyze print a statistical summary (mean rate and confidence
+ interval per group) instead of csv output
+ --xaxis variable variable to group by when using --analyze (required
+ by --analyze)
+ --category variable additional variable to group by when using --analyze
+ --no-chart don't print the bar chart when using --analyze
+
+ Examples:
+ --set CPUSET=0 Runs benchmarks on CPU core 0.
+ --set CPUSET=0-2 Specifies that benchmarks should run on CPU cores 0 to 2.
+
+ Note: The CPUSET format should match the specifications of the 'taskset' command
+`, {
+ arrayArgs: ['set'],
+ boolArgs: ['no-progress', 'analyze', 'no-chart'],
+});
if (cli.items.length !== 1) {
cli.abort(cli.usage);
}
-// Create queue from the benchmarks list such both node versions are tested
-// `runs` amount of times each.
const filepath = path.resolve(cli.items[0]);
-const name = filepath.slice(__dirname.length + 1);
+const name = path.relative(__dirname, filepath);
const runs = cli.optional.runs ? parseInt(cli.optional.runs, 10) : 30;
+const analyze = !!cli.optional.analyze;
+const showChart = !cli.optional['no-chart'];
+const xAxis = cli.optional.xaxis;
+const category = cli.optional.category;
+
+// Grouping is what makes the summary meaningful, so --analyze needs to know
+// which variable is the independent one. There is no sensible default: the
+// answer depends entirely on what is being measured.
+if (analyze && !xAxis) {
+ cli.abort(
+ `--analyze requires --xaxis
+
+ --xaxis names the benchmark variable to summarize against; --category
+ optionally breaks each point down by a second variable. Both must be
+ configuration variables of this benchmark -- the keys passed to
+ createBenchmark() at the top of the benchmark file. Nearly every
+ benchmark defines 'n'; parameter sweeps add a size or mode variable
+ such as 'len', 'size' or 'encoding'.
+
+ Example:
+ ./node benchmark/scatter.js --analyze --xaxis n ${name}
+`,
+ );
+}
+
+// When --analyze is set, collect results rather than streaming csv.
+const samples = analyze ? [] : null;
let printHeader = true;
function csvEncodeValue(value) {
- if (typeof value === 'number') {
+ // Benchmark configuration values are numbers, booleans or strings
+ // (see the config parsing in common.js). Only strings need quoting,
+ // but anything unexpected is stringified rather than crashing the run.
+ if (typeof value === 'number' || typeof value === 'boolean') {
return value.toString();
}
- return `"${value.replace(/"/g, '""')}"`;
+ return `"${String(value).replace(/"/g, '""')}"`;
+}
+
+// Note: BenchmarkProgress reports progress per file; scatter.js only ever
+// runs one file, so every queue entry shares the same filename.
+const queue = [];
+for (let iter = 0; iter < runs; iter++) {
+ queue.push({ filename: name, iter });
+}
+
+const kStartOfQueue = 0;
+
+const showProgress = !cli.optional['no-progress'];
+let progress;
+if (showProgress) {
+ progress = new BenchmarkProgress(queue, [name], { analyze });
+ progress.startQueue(kStartOfQueue);
}
(function recursive(i) {
- const child = fork(path.resolve(__dirname, filepath), cli.optional.set);
+ const cpuCore = cli.getCpuCoreSetting();
+ let child;
+ if (cpuCore !== null) {
+ const spawnArgs = ['-c', cpuCore, process.execPath, filepath, ...cli.optional.set];
+ child = spawn('taskset', spawnArgs, {
+ env: process.env,
+ stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
+ });
+ } else {
+ child = fork(filepath, cli.optional.set);
+ }
child.on('message', (data) => {
- if (data.type !== 'report') {
+ if (data.type === 'config') {
+ if (showProgress) {
+ progress.startSubqueue(data, i);
+ }
return;
}
- // print csv header
- if (printHeader) {
- const confHeader = Object.keys(data.conf)
- .map(csvEncodeValue)
- .join(', ');
- console.log(`"filename", ${confHeader}, "rate", "time"`);
- printHeader = false;
+ if (data.type !== 'report') {
+ return;
}
- // print data row
- const confData = Object.keys(data.conf)
- .map((key) => csvEncodeValue(data.conf[key]))
- .join(', ');
+ if (analyze) {
+ // Validate the grouping variables against the first result rather than
+ // at the end. A typo in --xaxis is otherwise not reported until the
+ // whole run has finished, which can be many minutes of wasted work.
+ if (samples.length === 0) {
+ const confKeys = Object.keys(data.conf);
+ for (const key of [xAxis, category]) {
+ if (key !== undefined && !confKeys.includes(key)) {
+ child.kill();
+ cli.abort(
+ `The variable "${key}" is not a configuration of ${name}.\n` +
+ `Available variables: ${confKeys.join(', ')}`,
+ );
+ }
+ }
+ }
+ samples.push(data);
+ } else {
+ // print csv header
+ if (printHeader) {
+ const confHeader = Object.keys(data.conf)
+ .map(csvEncodeValue)
+ .join(',');
+ console.log(`"filename",${confHeader},"rate","time"`);
+ printHeader = false;
+ }
+
+ // print data row
+ const confData = Object.keys(data.conf)
+ .map((key) => csvEncodeValue(data.conf[key]))
+ .join(',');
- console.log(`"${name}", ${confData}, ${data.rate}, ${data.time}`);
+ console.log(`"${name}",${confData},${data.rate},${data.time}`);
+ }
+
+ if (showProgress) {
+ progress.completeConfig(data);
+ }
});
child.once('close', (code) => {
@@ -64,10 +169,768 @@ function csvEncodeValue(value) {
process.exit(code);
return;
}
+ if (showProgress) {
+ progress.completeRun(queue[i]);
+ }
// If there are more benchmarks execute the next
if (i + 1 < runs) {
recursive(i + 1);
+ } else if (analyze) {
+ printAnalysis(samples, xAxis, category);
}
});
-})(0);
+})(kStartOfQueue);
+
+//
+// Statistics
+//
+// scatter.R obtains the t quantile from R's qt(); without R it has to be
+// computed here. The Student's t tail probability is an incomplete beta
+// function, which is evaluated directly, and the quantile is recovered by
+// bisecting it. Bisection rather than an inverse-beta routine because the
+// cost is irrelevant at this scale and the error bound is explicit.
+//
+
+// Lanczos approximation, g=7, n=9.
+function logGamma(x) {
+ const g = [
+ 676.5203681218851, -1259.1392167224028, 771.32342877765313,
+ -176.61502916214059, 12.507343278686905, -0.13857109526572012,
+ 9.9843695780195716e-6, 1.5056327351493116e-7,
+ ];
+ if (x < 0.5) {
+ // Reflection formula.
+ return Math.log(Math.PI / Math.sin(Math.PI * x)) - logGamma(1 - x);
+ }
+ x -= 1;
+ let a = 0.99999999999980993;
+ const t = x + 7.5;
+ for (let i = 0; i < g.length; i++) {
+ a += g[i] / (x + i + 1);
+ }
+ return 0.5 * Math.log(2 * Math.PI) + (x + 0.5) * Math.log(t) - t + Math.log(a);
+}
+
+// Continued fraction for the incomplete beta function (Lentz's algorithm).
+function betaContinuedFraction(x, a, b) {
+ const tiny = 1e-30;
+ const qab = a + b;
+ const qap = a + 1;
+ const qam = a - 1;
+ let c = 1;
+ let d = 1 - qab * x / qap;
+ if (Math.abs(d) < tiny) d = tiny;
+ d = 1 / d;
+ let h = d;
+ for (let m = 1; m <= 300; m++) {
+ const m2 = 2 * m;
+ let aa = m * (b - m) * x / ((qam + m2) * (a + m2));
+ d = 1 + aa * d;
+ if (Math.abs(d) < tiny) d = tiny;
+ c = 1 + aa / c;
+ if (Math.abs(c) < tiny) c = tiny;
+ d = 1 / d;
+ h *= d * c;
+ aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
+ d = 1 + aa * d;
+ if (Math.abs(d) < tiny) d = tiny;
+ c = 1 + aa / c;
+ if (Math.abs(c) < tiny) c = tiny;
+ d = 1 / d;
+ const del = d * c;
+ h *= del;
+ if (Math.abs(del - 1) < 1e-14) break;
+ }
+ return h;
+}
+
+// Regularized incomplete beta function I_x(a, b).
+function incompleteBeta(x, a, b) {
+ if (x <= 0) return 0;
+ if (x >= 1) return 1;
+ const lbeta = logGamma(a + b) - logGamma(a) - logGamma(b);
+ // Use the continued fraction on whichever side converges quickly.
+ if (x < (a + 1) / (a + b + 2)) {
+ return Math.exp(lbeta + a * Math.log(x) + b * Math.log(1 - x)) *
+ betaContinuedFraction(x, a, b) / a;
+ }
+ return 1 - Math.exp(lbeta + b * Math.log(1 - x) + a * Math.log(x)) *
+ betaContinuedFraction(1 - x, b, a) / b;
+}
+
+// Two-tailed probability P(|T| > t) for Student's t with v degrees of freedom.
+function tTailProbability(t, v) {
+ return incompleteBeta(v / (v + t * t), v / 2, 0.5);
+}
+
+// Quantile of Student's t: the value q with P(T <= q) = p, for p > 0.5.
+// Equivalent to R's qt(p, v). Accurate to ~1e-6 over the range used here.
+function tQuantile(p, v) {
+ const alpha = 2 * (1 - p);
+ let lo = 0;
+ let hi = 1e3;
+ for (let i = 0; i < 200; i++) {
+ const mid = (lo + hi) / 2;
+ if (tTailProbability(mid, v) > alpha) {
+ lo = mid;
+ } else {
+ hi = mid;
+ }
+ }
+ return (lo + hi) / 2;
+}
+
+function printAnalysis(samples, xAxis, category) {
+ if (samples.length === 0) {
+ console.error('No benchmark results were reported.');
+ process.exitCode = 1;
+ return;
+ }
+
+ // The grouping variables were already validated against the first result.
+ const confKeys = Object.keys(samples[0].conf);
+
+ // Every variable that is neither a grouping variable nor constant across
+ // the run is averaged over. Report them: an aggregated variable can hide a
+ // real effect, and the reader has no other way to know it happened.
+ const aggregated = confKeys.filter((key) => {
+ if (key === xAxis || key === category) return false;
+ const first = samples[0].conf[key];
+ return samples.some((s) => s.conf[key] !== first);
+ });
+
+ // Group by the grouping variables. The full sample is retained, not just
+ // the rate, so the aggregated variables can be accounted for below.
+ const groups = new Map();
+ for (const sample of samples) {
+ const xValue = sample.conf[xAxis];
+ const catValue = category === undefined ? undefined : sample.conf[category];
+ const key = `${String(xValue)}\u0000${String(catValue)}`;
+ let group = groups.get(key);
+ if (group === undefined) {
+ group = { xValue, catValue, rates: [], members: [] };
+ groups.set(key, group);
+ }
+ group.rates.push(sample.rate);
+ group.members.push(sample);
+ }
+
+ // Naming an aggregated variable is not enough: if it drives most of the
+ // spread within a group, every interval below is reporting that variable
+ // rather than the benchmark's noise, and no number of --runs will shrink
+ // it. Quantify the share so the reader can tell those cases apart.
+ const contamination = new Map();
+ for (const variable of aggregated) {
+ contamination.set(variable, varianceShare([...groups.values()], variable));
+ }
+
+ for (const variable of aggregated) {
+ const share = contamination.get(variable);
+ // Rounding 99.7% up to "100%" would claim the residual noise is zero.
+ const percent = share === 1 ?
+ '100' :
+ (share >= 0.995 ? '>99' : (share * 100).toFixed(0));
+ const suffix = Number.isNaN(share) ?
+ '' :
+ ` (explains ${percent}% of within-group variance)`;
+ console.log(`aggregating variable: ${variable}${suffix}`);
+ }
+
+ // Aggregation that dominates the spread is the single most common reason
+ // for uselessly wide intervals, and the usual reaction -- raising --runs --
+ // cannot help, because the spread is a real effect of a variable that has
+ // been averaged over rather than sampling noise.
+ const dominant = aggregated.filter((v) => contamination.get(v) > 0.5);
+ if (dominant.length > 0) {
+ console.log('');
+ const one = dominant.length === 1;
+ printWrapped(
+ `Note: ${dominant.join(', ')} ${one ? 'explains' : 'explain'} most of ` +
+ `the spread within each group, so the intervals below describe ` +
+ `${one ? 'that variable' : 'those variables'} rather than the ` +
+ `benchmark's own noise, and more --runs will not narrow them. Pin ` +
+ `${one ? 'it' : 'them'} with --set ${dominant[0]}=, or pass as ` +
+ `--category, for intervals that can be acted on.`,
+ );
+ }
+
+ if (aggregated.length > 0) {
+ console.log('');
+ }
+
+ const compare = (a, b) => {
+ if (typeof a === 'number' && typeof b === 'number') return a - b;
+ return String(a).localeCompare(String(b));
+ };
+
+ const scale = histogramScale(samples.map((s) => s.rate));
+
+ const rows = [...groups.values()]
+ .sort((a, b) => compare(a.xValue, b.xValue) ||
+ compare(a.catValue, b.catValue))
+ .map((group) => {
+ const rates = group.rates;
+ const n = rates.length;
+ const mean = rates.reduce((a, b) => a + b, 0) / n;
+
+ // Confidence interval of the mean, matching scatter.R: the sample
+ // standard error scaled by the t quantile at 97.5%. Computed from the
+ // raw samples rather than the histogram, which stores bucketed values.
+ // Undefined for a single sample, where there is no spread to estimate.
+ let ci = NaN;
+ if (n > 1) {
+ const variance =
+ rates.reduce((sum, r) => sum + (r - mean) ** 2, 0) / (n - 1);
+ ci = Math.sqrt(variance / n) * tQuantile(0.975, n - 1);
+ }
+
+ // The histogram supplies the statistics that have no closed form here:
+ // an exact-binomial interval on the median, and the shape measures used
+ // to decide whether the mean is worth trusting.
+ const histogram = createHistogram({ figures: 5 });
+ for (const rate of rates) {
+ histogram.record(Math.max(1, Math.round(rate * scale)));
+ }
+
+ const medianCI = histogram.percentileCI(50);
+ const median = medianCI.value / scale;
+ const skewness = n > 1 ? histogram.skewness : NaN;
+
+ // A summary is suspect when the two estimates of the centre disagree by
+ // more than the uncertainty claimed for one of them, or when the sample
+ // is badly asymmetric. Either way the mean is being moved by the tail
+ // rather than describing the bulk of the runs.
+ const skewed = n > 1 &&
+ (Math.abs(skewness) > 1 || Math.abs(median - mean) > ci);
+
+ return {
+ ...group,
+ n,
+ mean,
+ ci,
+ histogram,
+ median,
+ medianLower: medianCI.lower / scale,
+ medianUpper: medianCI.upper / scale,
+ skewness,
+ skewed,
+ };
+ });
+
+ // Values are rendered through short labels so that long or non-printable
+ // configuration values cannot break the layout.
+ const legend = assignLabels(rows, 'xValue', 'xLabel');
+ if (category !== undefined) {
+ legend.push(...assignLabels(rows, 'catValue', 'catLabel'));
+ }
+
+ printTable(rows, xAxis, category);
+
+ if (showChart) {
+ printChart(rows, xAxis, category);
+ }
+
+ printComparisons(rows, xAxis, category);
+ printLegend(legend);
+
+ const singleSample = rows.filter((r) => r.n < 2).length;
+ if (singleSample > 0) {
+ console.log('');
+ printWrapped(
+ `Note: ${singleSample} group${singleSample === 1 ? ' has' : 's have'} ` +
+ `only one sample, so no confidence interval could be estimated. ` +
+ `Use --runs 2 or higher.`,
+ );
+ }
+
+ if (rows.some((r) => r.skewed)) {
+ console.log('');
+ printWrapped(
+ `(!) marks groups where the median falls outside the mean's confidence ` +
+ `interval, or the sample is strongly skewed (|skewness| > 1). For those ` +
+ `rows the mean is being pulled by a few slow or fast runs, so the ` +
+ `median and its interval describe the typical run better. This is ` +
+ `usually GC or JIT tiering; more --runs will not necessarily make it ` +
+ `go away.`,
+ );
+ }
+}
+
+// HdrHistogram records positive integers with a fixed number of significant
+// figures, so precision is relative and large rates need no scaling at all.
+// Small rates do: without scaling, anything below 1 op/s rounds to the same
+// bucket and the group collapses to a single value. Scale up until the
+// smallest rate carries enough digits for rounding to be irrelevant.
+function histogramScale(rates) {
+ let min = Infinity;
+ let max = 0;
+ for (const rate of rates) {
+ if (rate > 0 && rate < min) min = rate;
+ if (rate > max) max = rate;
+ }
+ if (!Number.isFinite(min) || max === 0) return 1;
+
+ let scale = 1;
+ while (min * scale < 1e6 && max * scale < 1e15) {
+ scale *= 10;
+ }
+ return scale;
+}
+
+// Share of the within-group variance attributable to one aggregated
+// variable, pooled over groups. This is a one-way eta squared: the spread
+// between that variable's levels divided by the total spread inside the
+// group. Computed per variable and independently, so when two aggregated
+// variables are correlated their shares overlap and do not sum to one.
+function varianceShare(groups, variable) {
+ let between = 0;
+ let total = 0;
+
+ for (const group of groups) {
+ const members = group.members;
+ if (members.length < 2) continue;
+
+ const groupMean =
+ members.reduce((sum, s) => sum + s.rate, 0) / members.length;
+
+ // Partition the group by the level of this variable.
+ const levels = new Map();
+ for (const sample of members) {
+ const key = String(sample.conf[variable]);
+ let level = levels.get(key);
+ if (level === undefined) {
+ level = { sum: 0, count: 0 };
+ levels.set(key, level);
+ }
+ level.sum += sample.rate;
+ level.count++;
+ }
+
+ // A variable with a single level inside this group explains nothing
+ // here, but the group still contributes to the total spread.
+ for (const level of levels.values()) {
+ const levelMean = level.sum / level.count;
+ between += level.count * (levelMean - groupMean) ** 2;
+ }
+ for (const sample of members) {
+ total += (sample.rate - groupMean) ** 2;
+ }
+ }
+
+ if (total === 0) return NaN;
+ return Math.min(1, between / total);
+}
+
+// The smallest p-value the Mann-Whitney implementation can return for these
+// sample sizes, found by giving it perfectly separated groups. Below a
+// certain size that floor sits above 0.05, meaning no effect of any
+// magnitude can be reported as significant; a non-significant result then
+// says nothing at all. Derived from the implementation rather than the exact
+// combinatorial bound because it uses a normal approximation.
+const mannWhitneyFloors = new Map();
+function mannWhitneyFloor(nA, nB) {
+ const key = `${nA},${nB}`;
+ let floor = mannWhitneyFloors.get(key);
+ if (floor === undefined) {
+ // Values must stay distinct after the histogram's 5-significant-figure
+ // rounding, or they collapse into ties and the tie correction reports a
+ // floor lower than the test can actually reach. The 10000 range is exact
+ // at that precision, and the two runs cannot overlap.
+ const low = createHistogram({ figures: 5 });
+ const high = createHistogram({ figures: 5 });
+ for (let i = 0; i < nA; i++) low.record(10000 + i);
+ for (let i = 0; i < nB; i++) high.record(10000 + nA + i);
+ floor = high.mannWhitneyTest(low).pValue;
+ mannWhitneyFloors.set(key, floor);
+ }
+ return floor;
+}
+
+// Cliff's delta magnitude thresholds (Romano et al.), the conventional
+// reading of the statistic.
+function effectSizeLabel(delta) {
+ const d = Math.abs(delta);
+ if (d < 0.147) return 'negligible';
+ if (d < 0.33) return 'small';
+ if (d < 0.474) return 'medium';
+ return 'large';
+}
+
+// The question scatter.js exists to answer is whether a parameter changes the
+// rate, which the per-group table only hints at through overlapping intervals.
+// Compare consecutive x-axis values directly, holding the category fixed.
+// Mann-Whitney rather than a t-test because a parameter sweep routinely
+// changes the shape and spread of the distribution, not just its centre.
+function printComparisons(rows, xAxis, category) {
+ const usable = rows.filter((row) => row.n > 1);
+ if (usable.length < 2) return;
+
+ // Partition by category so each series is a sweep over the x-axis alone.
+ const series = new Map();
+ for (const row of usable) {
+ const key = String(row.catValue);
+ if (!series.has(key)) series.set(key, []);
+ series.get(key).push(row);
+ }
+
+ const lines = [];
+ let floor = 0;
+ for (const group of series.values()) {
+ if (group.length < 2) continue;
+
+ const entries = [];
+ for (let i = 1; i < group.length; i++) {
+ const previous = group[i - 1];
+ const current = group[i];
+ const { pValue } = current.histogram.mannWhitneyTest(previous.histogram);
+ const delta = current.histogram.cliffsD(previous.histogram);
+ const change = ((current.mean - previous.mean) / previous.mean) * 100;
+
+ floor = Math.max(floor, mannWhitneyFloor(previous.n, current.n));
+
+ // How the rate scales with the parameter over this step, which is the
+ // complexity question these sweeps are usually run to answer. Reported
+ // per step rather than as one fit across the whole sweep: a single
+ // exponent spanning a curve that changes regime describes neither.
+ let ratio = '';
+ let exponent = '';
+ if (typeof previous.xValue === 'number' &&
+ typeof current.xValue === 'number' &&
+ previous.xValue > 0 && current.xValue > 0 &&
+ previous.xValue !== current.xValue &&
+ previous.mean > 0 && current.mean > 0) {
+ const xRatio = current.xValue / previous.xValue;
+ const value = Math.log(current.mean / previous.mean) /
+ Math.log(xRatio);
+ ratio = `${xRatio.toFixed(1)}x`;
+ exponent = `${value >= 0 ? '+' : ''}${value.toFixed(2)}`;
+ }
+
+ entries.push({
+ transition: `${previous.xLabel} -> ${current.xLabel}`,
+ change: `${change >= 0 ? '+' : ''}${change.toFixed(2)}%`,
+ ratio,
+ exponent,
+ pValue: pValue < 1e-4 ? pValue.toExponential(1) : pValue.toFixed(4),
+ delta: `${delta >= 0 ? '+' : ''}${delta.toFixed(3)}`,
+ label: effectSizeLabel(delta),
+ });
+ }
+
+ if (entries.length > 0) {
+ lines.push({
+ heading: category === undefined ?
+ null :
+ `${category}=${group[0].catLabel}`,
+ entries,
+ });
+ }
+ }
+
+ if (lines.length === 0) return;
+
+ const all = lines.flatMap((l) => l.entries);
+ const widths = {
+ transition: Math.max(...all.map((e) => displayWidth(e.transition))),
+ change: Math.max(...all.map((e) => displayWidth(e.change))),
+ ratio: Math.max(...all.map((e) => displayWidth(e.ratio))),
+ exponent: Math.max(...all.map((e) => displayWidth(e.exponent))),
+ pValue: Math.max(...all.map((e) => displayWidth(e.pValue))),
+ delta: Math.max(...all.map((e) => displayWidth(e.delta))),
+ };
+ const rpad = (s, n) => padTo(s, n, true);
+ const pad = (s, n) => padTo(s, n, false);
+
+ console.log('');
+ console.log(`Change between consecutive ${xAxis} values ` +
+ `(Mann-Whitney U, Cliff's delta):`);
+
+ for (const { heading, entries } of lines) {
+ console.log('');
+ if (heading !== null) {
+ console.log(` ${heading}`);
+ }
+ for (const entry of entries) {
+ console.log(
+ ` ${pad(entry.transition, widths.transition)}` +
+ ` ${rpad(entry.change, widths.change)}` +
+ (widths.exponent > 0 ?
+ ` ${rpad(entry.ratio, widths.ratio)}` +
+ ` exponent=${rpad(entry.exponent, widths.exponent)}` :
+ '') +
+ ` p=${rpad(entry.pValue, widths.pValue)}` +
+ ` delta=${rpad(entry.delta, widths.delta)} (${entry.label})`,
+ );
+ }
+ }
+
+ // A p-value has to be read against what the test could have produced. Below
+ // a certain sample size the floor sits above the threshold, and "not
+ // significant" then carries no information whatsoever.
+ if (floor >= 0.05) {
+ console.log('');
+ printWrapped(
+ `Warning: at this sample size the smallest p-value this test can ` +
+ `produce is ${floor.toFixed(4)}, so no comparison above can reach ` +
+ `significance however large the real effect is. Treat every p-value ` +
+ `here as uninformative and raise --runs.`,
+ );
+ } else if (floor >= 0.005) {
+ console.log('');
+ printWrapped(
+ `Note: at this sample size the smallest p-value this test can produce ` +
+ `is ${floor.toFixed(4)}. A non-significant result above is therefore ` +
+ `weak evidence of no change rather than evidence of none; raise ` +
+ `--runs to strengthen it.`,
+ );
+ }
+}
+
+function formatRate(rate) {
+ return rate.toLocaleString('en-US', {
+ minimumFractionDigits: 1,
+ maximumFractionDigits: 1,
+ });
+}
+
+// Widest a single variable value may be before it is abbreviated. Benchmark
+// values are usually short (`16`, `'ascii'`), but some are long enough to
+// destroy the layout on their own, so there has to be a ceiling.
+const kMaxValueWidth = 24;
+
+// Widest a composed chart label may be, since it can hold two values.
+const kMaxChartLabelWidth = 44;
+
+// Widths are measured in code points, not UTF-16 code units: several
+// benchmarks use emoji and CJK in their configurations, and counting those
+// as two would misalign every column to their right.
+function displayWidth(text) {
+ return [...text].length;
+}
+
+function padTo(text, width, alignRight) {
+ const padding = ' '.repeat(Math.max(0, width - displayWidth(text)));
+ return alignRight ? padding + text : text + padding;
+}
+
+// Drop out the middle rather than the tail: benchmark values that share a
+// long prefix are common, and a head-only truncation would render them
+// identical. Slicing by code point rather than by index so that an astral
+// character cannot be cut in half into a lone surrogate.
+function truncateMiddle(text, maxWidth) {
+ const chars = [...text];
+ if (chars.length <= maxWidth) return text;
+ const keep = maxWidth - 3;
+ const head = Math.ceil(keep / 2);
+ const tail = Math.floor(keep / 2);
+ return `${chars.slice(0, head).join('')}...` +
+ `${chars.slice(chars.length - tail).join('')}`;
+}
+
+// Configuration values are not guaranteed to be printable: strings may carry
+// newlines or tabs (see benchmark/mime/mimetype-instantiation.js), which
+// would otherwise split a table row across several lines. inspect() escapes
+// them and quotes strings, matching how compare.js renders configurations.
+function displayValue(value) {
+ const text = typeof value === 'string' ? inspect(value) : String(value);
+ return { text: truncateMiddle(text, kMaxValueWidth), full: text };
+}
+
+// Assigns a short, unique, printable label to every distinct value of one
+// grouping variable, and collects the abbreviated ones for a legend so the
+// full value is still recoverable from the output.
+function assignLabels(rows, valueField, labelField) {
+ const assigned = new Map();
+ const used = new Map();
+ const legend = [];
+
+ for (const row of rows) {
+ const key = String(row[valueField]);
+ let label = assigned.get(key);
+
+ if (label === undefined) {
+ const { text, full } = displayValue(row[valueField]);
+ // Two different values can abbreviate to the same text; keep them
+ // distinguishable so the table cannot show one group twice.
+ const collisions = used.get(text) ?? 0;
+ used.set(text, collisions + 1);
+ label = collisions === 0 ? text : `${text}~${collisions + 1}`;
+ assigned.set(key, label);
+ if (text !== full) legend.push({ label, full });
+ }
+
+ row[labelField] = label;
+ }
+
+ return legend;
+}
+
+// Notes interpolate variable names and sample sizes, so their length is not
+// known when they are written. Wrapping here rather than hand-placing
+// newlines, which overflow as soon as an interpolated value is longer than
+// the author guessed.
+function printWrapped(text, width = 76) {
+ let line = '';
+ for (const word of text.split(/\s+/)) {
+ if (line === '') {
+ line = word;
+ } else if (displayWidth(line) + 1 + displayWidth(word) <= width) {
+ line += ` ${word}`;
+ } else {
+ console.log(line);
+ line = word;
+ }
+ }
+ if (line !== '') console.log(line);
+}
+
+function printLegend(legend) {
+ if (legend.length === 0) return;
+ console.log('');
+ console.log('Abbreviated values:');
+ for (const { label, full } of legend) {
+ console.log(` ${label}`);
+ console.log(` = ${full}`);
+ }
+}
+
+function printTable(rows, xAxis, category) {
+ const header = [xAxis];
+ if (category !== undefined) header.push(category);
+ header.push('samples', 'rate', 'confidence.interval',
+ 'median', 'median.interval', '');
+
+ // Numbers line up on the right, text reads better on the left. The grouping
+ // columns can be either, so they follow the type of the underlying value.
+ const alignRight = [typeof rows[0].xValue === 'number'];
+ if (category !== undefined) {
+ alignRight.push(typeof rows[0].catValue === 'number');
+ }
+ alignRight.push(true, true, true, true, true, false);
+
+ const body = rows.map((row) => {
+ const cells = [row.xLabel];
+ if (category !== undefined) cells.push(row.catLabel);
+
+ // The median interval is asymmetric, so it is shown as a relative range
+ // rather than a single half-width.
+ const medianInterval = row.n > 1 ?
+ `[${(((row.medianLower - row.median) / row.median) * 100).toFixed(2)}%, ` +
+ `+${(((row.medianUpper - row.median) / row.median) * 100).toFixed(2)}%]` :
+ 'NA';
+
+ cells.push(
+ String(row.n),
+ formatRate(row.mean),
+ // Absolute for parity with scatter.R, relative because that is what
+ // tells you whether the rate beside it is worth reading. Kept in one
+ // column so every column has a header of its own.
+ Number.isNaN(row.ci) ?
+ 'NA' :
+ `${formatRate(row.ci)} (±${((row.ci / row.mean) * 100).toFixed(2)}%)`,
+ formatRate(row.median),
+ medianInterval,
+ row.skewed ? '(!)' : '',
+ );
+ return cells;
+ });
+
+ const widths = header.map((_, col) => Math.max(
+ displayWidth(header[col]),
+ ...body.map((cells) => displayWidth(cells[col])),
+ ));
+
+ const line = (cells) => cells
+ .map((cell, col) => padTo(cell, widths[col], alignRight[col]))
+ .join(' ')
+ .trimEnd();
+
+ console.log(line(header));
+ for (const cells of body) {
+ console.log(line(cells));
+ }
+}
+
+function printChart(rows, xAxis, category) {
+ if (rows.length === 0) return;
+
+ const barWidth = 40;
+ // Bars are drawn from zero so that the visual length is proportional to the
+ // rate. A truncated axis would exaggerate small differences.
+ let maxRate = 0;
+ for (const row of rows) {
+ const extent = row.mean + (Number.isNaN(row.ci) ? 0 : row.ci);
+ if (extent > maxRate) maxRate = extent;
+ }
+ if (maxRate === 0) return;
+
+ const labels = rows.map((row) => {
+ const parts = [`${xAxis}=${row.xLabel}`];
+ if (category !== undefined) parts.push(`${category}=${row.catLabel}`);
+ return truncateMiddle(parts.join(' '), kMaxChartLabelWidth);
+ });
+ const labelWidth = Math.max(...labels.map(displayWidth));
+ const rateWidth = Math.max(
+ ...rows.map((r) => displayWidth(formatRate(r.mean))));
+
+ const pad = (s, n) => padTo(s, n, false);
+ const rpad = (s, n) => padTo(s, n, true);
+
+ const axisRight = formatRate(maxRate);
+ const indent = ' '.repeat(labelWidth + 2);
+
+ console.log('');
+ console.log('Rate in operations/second; longer is faster. \u2502 marks the ' +
+ 'mean and the');
+ console.log('shaded band (\u2591) is its 95% confidence interval, so bars ' +
+ 'whose bands');
+ console.log('overlap are not clearly different.');
+ console.log('');
+ console.log(
+ `${indent}0` +
+ `${' '.repeat(Math.max(1, barWidth - 1 - displayWidth(axisRight)))}` +
+ `${axisRight}`,
+ );
+ console.log(`${indent}+${'-'.repeat(Math.max(0, barWidth - 2))}+`);
+
+ let previousX;
+ for (let i = 0; i < rows.length; i++) {
+ const row = rows[i];
+
+ // A blank line between x-axis values, so the categories being compared at
+ // each point stay visually grouped.
+ if (previousX !== undefined && row.xValue !== previousX) {
+ console.log('');
+ }
+ previousX = row.xValue;
+
+ const barEnd = (row.mean / maxRate) * barWidth;
+ const ci = Number.isNaN(row.ci) ? 0 : row.ci;
+ const ciLeft = ((row.mean - ci) / maxRate) * barWidth;
+ const ciRight = ((row.mean + ci) / maxRate) * barWidth;
+
+ // Without an explicit marker the mean is invisible, because the shaded
+ // interval is drawn over the solid bar and straddles it.
+ const meanCell = Math.min(barWidth - 1, Math.floor(barEnd));
+
+ let bar = '';
+ for (let x = 0; x < barWidth; x++) {
+ const pos = x + 0.5; // Center of this character cell.
+ if (x === meanCell) {
+ bar += '\u2502'; // The mean itself.
+ } else if (pos >= ciLeft && pos <= ciRight) {
+ bar += '\u2591'; // Light shade marks the confidence interval.
+ } else if (pos <= barEnd) {
+ bar += '\u2588';
+ } else {
+ bar += ' ';
+ }
+ }
+
+ console.log(
+ `${pad(labels[i], labelWidth)} ${bar} ` +
+ `${rpad(formatRate(row.mean), rateWidth)}`,
+ );
+ }
+}
diff --git a/benchmark/vfs/module-graph.js b/benchmark/vfs/module-graph.js
new file mode 100644
index 000000000000..aee5e8616198
--- /dev/null
+++ b/benchmark/vfs/module-graph.js
@@ -0,0 +1,62 @@
+'use strict';
+const path = require('path');
+const { pathToFileURL } = require('url');
+const common = require('../common.js');
+
+const bench = common.createBenchmark(main, {
+ type: ['cjs', 'esm'],
+ files: [1e2, 1e3],
+ n: [10],
+}, { flags: ['--experimental-vfs', '--no-warnings'] });
+
+// Builds a module graph of `files` packages, each with its own package.json,
+// an index requiring a package-local file and a shared root module, and an
+// entry point that pulls in every package.
+function buildGraph(layer, files, type) {
+ const entryRequires = [];
+ if (type === 'esm') {
+ layer.writeFileSync('/package.json', '{"type":"module"}');
+ }
+ layer.writeFileSync('/shared.js',
+ type === 'cjs' ? 'module.exports = 0;' : 'export default 0;');
+ for (let i = 0; i < files; i++) {
+ layer.mkdirSync(`/${i}`, { recursive: true });
+ if (type === 'cjs') {
+ layer.writeFileSync(`/${i}/package.json`, '{"main":"index.js"}');
+ layer.writeFileSync(`/${i}/lib.js`, 'module.exports = 1;');
+ layer.writeFileSync(
+ `/${i}/index.js`,
+ 'require("./lib.js"); require("../shared.js"); module.exports = __filename;');
+ entryRequires.push(`require('./${i}/');`);
+ } else {
+ layer.writeFileSync(`/${i}/package.json`, '{"type":"module"}');
+ layer.writeFileSync(`/${i}/lib.js`, 'export default 1;');
+ layer.writeFileSync(
+ `/${i}/index.js`,
+ 'import "./lib.js"; import "../shared.js"; export default import.meta.url;');
+ entryRequires.push(`import './${i}/index.js';`);
+ }
+ }
+ layer.writeFileSync('/entry.js', entryRequires.join('\n'));
+}
+
+async function main({ n, type, files }) {
+ const vfs = require('node:vfs');
+ const layer = vfs.create();
+ buildGraph(layer, files, type);
+
+ bench.start();
+ for (let i = 0; i < n; i++) {
+ const mountPoint = layer.mount();
+ const entry = path.join(mountPoint, 'entry.js');
+ if (type === 'cjs') {
+ require(entry);
+ } else {
+ await import(pathToFileURL(entry).href);
+ }
+ // Unmounting purges the module caches for the mount prefix, so every
+ // iteration is a cold load of the full graph.
+ layer.unmount();
+ }
+ bench.end(n * files);
+}
diff --git a/benchmark/webstreams/adapters.js b/benchmark/webstreams/adapters.js
new file mode 100644
index 000000000000..ae97e4928f88
--- /dev/null
+++ b/benchmark/webstreams/adapters.js
@@ -0,0 +1,106 @@
+'use strict';
+const common = require('../common.js');
+const {
+ Readable,
+ Writable,
+} = require('node:stream');
+const {
+ ReadableStream,
+ WritableStream,
+} = require('node:stream/web');
+
+const bench = common.createBenchmark(main, {
+ n: [1e5],
+ kind: [
+ 'readable-to-web',
+ 'readable-from-web',
+ 'writable-to-web',
+ 'writable-from-web',
+ ],
+});
+
+async function readableToWeb(n) {
+ const chunk = Buffer.alloc(1024);
+ let i = 0;
+ const streamReadable = new Readable({
+ read() {
+ if (i++ < n)
+ this.push(chunk);
+ else
+ this.push(null);
+ },
+ });
+ const reader = Readable.toWeb(streamReadable).getReader();
+ bench.start();
+ while (!(await reader.read()).done);
+ bench.end(n);
+}
+
+function readableFromWeb(n) {
+ const chunk = Buffer.alloc(1024);
+ let i = 0;
+ const readableStream = new ReadableStream({
+ pull(controller) {
+ if (i++ < n)
+ controller.enqueue(chunk);
+ else
+ controller.close();
+ },
+ });
+ const streamReadable = Readable.fromWeb(readableStream);
+ bench.start();
+ streamReadable.on('data', () => {});
+ streamReadable.on('end', () => bench.end(n));
+}
+
+async function writableToWeb(n) {
+ const chunk = Buffer.alloc(1024);
+ const streamWritable = new Writable({
+ write(chunk, encoding, callback) {
+ callback();
+ },
+ });
+ const writer = Writable.toWeb(streamWritable).getWriter();
+ bench.start();
+ for (let i = 0; i < n; i++)
+ await writer.write(chunk);
+ await writer.close();
+ bench.end(n);
+}
+
+function writableFromWeb(n) {
+ const chunk = Buffer.alloc(1024);
+ const writableStream = new WritableStream({
+ write() {},
+ });
+ const streamWritable = Writable.fromWeb(writableStream);
+ bench.start();
+ let i = 0;
+ function writeLoop() {
+ while (i++ < n) {
+ if (!streamWritable.write(chunk)) {
+ streamWritable.once('drain', writeLoop);
+ return;
+ }
+ }
+ streamWritable.end(() => bench.end(n));
+ }
+ writeLoop();
+}
+
+function main({ n, kind }) {
+ switch (kind) {
+ case 'readable-to-web':
+ readableToWeb(n);
+ break;
+ case 'readable-from-web':
+ readableFromWeb(n);
+ break;
+ case 'writable-to-web':
+ writableToWeb(n);
+ break;
+ case 'writable-from-web':
+ writableFromWeb(n);
+ break;
+ }
+}
diff --git a/common.gypi b/common.gypi
index 1729bfd3cb00..e6a17d3505a5 100644
--- a/common.gypi
+++ b/common.gypi
@@ -17,6 +17,7 @@
'emulator%': [],
'node_shared%': 'false',
+ 'node_enable_v8debughelper%': 'false',
'node_enable_experimentals%': 'false',
'force_dynamic_crt%': 0,
'node_use_v8_platform%': 'true',
@@ -42,7 +43,7 @@
# Reset this number to 0 on major V8 upgrades.
# Increment by one for each non-official patch applied to deps/v8.
- 'v8_embedder_string': '-node.28',
+ 'v8_embedder_string': '-node.32',
##### V8 defaults for Node.js #####
@@ -654,7 +655,9 @@
'cflags!': [ '-pthread' ],
'ldflags!': [ '-pthread' ],
}],
- [ 'node_shared=="true"', {
+ # The V8 static libraries get linked into libv8_debug_helper, so they
+ # have to be position independent too.
+ [ 'node_shared=="true" or node_enable_v8debughelper=="true"', {
'cflags': [ '-fPIC' ],
'ldflags': [ '-fPIC' ],
}],
diff --git a/configure.py b/configure.py
index b663ba85f58e..84a688917efe 100755
--- a/configure.py
+++ b/configure.py
@@ -282,6 +282,28 @@
help='Use the specified path to system CA (PEM format) in addition to '
'the OpenSSL supplied CA store or compiled-in Mozilla CA copy.')
+shared_optgroup.add_argument('--shared-abseil',
+ action='store_true',
+ dest='shared_abseil',
+ default=None,
+ help='link to a shared Abseil DLL instead of static linking')
+
+shared_optgroup.add_argument('--shared-abseil-includes',
+ action='store',
+ dest='shared_abseil_includes',
+ help='directory containing Abseil header files')
+
+shared_optgroup.add_argument('--shared-abseil-libname',
+ action='store',
+ dest='shared_abseil_libname',
+ default=None,
+ help='alternative lib name to link to [default: %(default)s]')
+
+shared_optgroup.add_argument('--shared-abseil-libpath',
+ action='store',
+ dest='shared_abseil_libpath',
+ help='a directory to search for the shared Abseil DLL')
+
shared_optgroup.add_argument('--shared-gtest',
action='store_true',
dest='shared_gtest',
@@ -326,11 +348,27 @@
dest='shared_hdr_histogram_libpath',
help='a directory to search for the shared HdrHistogram DLL')
-parser.add_argument('--experimental-http-parser',
+shared_optgroup.add_argument('--shared-highway',
action='store_true',
- dest='experimental_http_parser',
+ dest='shared_highway',
default=None,
- help='(no-op)')
+ help='link to a shared Highway (hwy) DLL instead of static linking')
+
+shared_optgroup.add_argument('--shared-highway-includes',
+ action='store',
+ dest='shared_highway_includes',
+ help='directory containing Highway header files')
+
+shared_optgroup.add_argument('--shared-highway-libname',
+ action='store',
+ dest='shared_highway_libname',
+ default='hwy',
+ help='alternative lib name to link to [default: %(default)s]')
+
+shared_optgroup.add_argument('--shared-highway-libpath',
+ action='store',
+ dest='shared_highway_libpath',
+ help='a directory to search for the shared Highway DLL')
shared_optgroup.add_argument('--shared-http-parser',
action='store_true',
@@ -508,6 +546,29 @@
dest='shared_openssl_libpath',
help='a directory to search for the shared OpenSSL DLLs')
+shared_optgroup.add_argument('--shared-perfetto',
+ action='store_true',
+ dest='shared_perfetto',
+ default=None,
+ help='link to a shared perfetto SDK instead of the one in deps/perfetto '
+ '(requires --with-perfetto)')
+
+shared_optgroup.add_argument('--shared-perfetto-includes',
+ action='store',
+ dest='shared_perfetto_includes',
+ help='directory containing perfetto header files')
+
+shared_optgroup.add_argument('--shared-perfetto-libname',
+ action='store',
+ dest='shared_perfetto_libname',
+ default='perfetto',
+ help='alternative lib name to link to [default: %(default)s]')
+
+shared_optgroup.add_argument('--shared-perfetto-libpath',
+ action='store',
+ dest='shared_perfetto_libpath',
+ help='a directory to search for the shared perfetto DLL')
+
shared_optgroup.add_argument('--shared-uvwasi',
action='store_true',
dest='shared_uvwasi',
@@ -812,6 +873,13 @@
default=None,
help=argparse.SUPPRESS) # Undocumented.
+parser.add_argument('--enable-v8debughelper',
+ action='store_true',
+ dest='enable_v8debughelper',
+ default=None,
+ help='Build V8\'s debug helper as a shared library, loadable by a debugger '
+ 'extension.')
+
parser.add_argument('--enable-trace-maps',
action='store_true',
dest='trace_maps',
@@ -834,7 +902,7 @@
action='store_true',
dest='pointer_compression_shared_cage',
default=None,
- help='[Experimental] Use V8 pointer compression with shared cage (requires --experimental-enable-pointer-compression)')
+ help='[Experimental] Use V8 pointer compression with a shared cage and enable the V8 sandbox (requires --experimental-enable-pointer-compression)')
parser.add_argument('--v8-options',
action='store',
@@ -884,13 +952,13 @@
action='store_true',
dest='node_use_large_pages',
default=None,
- help='This option has no effect. --use-largepages is now a runtime option.')
+ help='This option is no longer supported and a no-op.')
parser.add_argument('--use-largepages-script-lld',
action='store_true',
dest='node_use_large_pages_script_lld',
default=None,
- help='This option has no effect. --use-largepages is now a runtime option.')
+ help='This option is no longer supported and a no-op.')
parser.add_argument('--use-section-ordering-file',
action='store',
@@ -1022,33 +1090,12 @@
default=None,
help='enable Control Flow Guard (CFG)')
-# Dummy option for backwards compatibility
-parser.add_argument('--without-report',
- action='store_true',
- dest='unused_without_report',
- default=None,
- help=argparse.SUPPRESS)
-
-parser.add_argument('--with-snapshot',
- action='store_true',
- dest='unused_with_snapshot',
- default=None,
- help=argparse.SUPPRESS)
-
-parser.add_argument('--without-snapshot',
- action='store_true',
- dest='unused_without_snapshot',
- default=None,
- help=argparse.SUPPRESS)
-
parser.add_argument('--without-siphash',
action='store_true',
dest='without_siphash',
default=None,
help=argparse.SUPPRESS)
-# End dummy list.
-
parser.add_argument('--without-ssl',
action='store_true',
dest='without_ssl',
@@ -1079,6 +1126,12 @@
default=None,
help='build with experimental QUIC support')
+parser.add_argument('--experimental-dtls',
+ action='store_true',
+ dest='experimental_dtls',
+ default=None,
+ help='build with experimental DTLS support')
+
parser.add_argument('--ninja',
action='store_true',
dest='use_ninja',
@@ -1900,8 +1953,6 @@ def configure_node(o):
else target_arch != host_arch)
if cross_compiling:
os.environ['GYP_CROSSCOMPILE'] = "1"
- if options.unused_without_snapshot:
- warn('building --without-snapshot is no longer possible')
o['variables']['want_separate_host_toolset'] = int(cross_compiling)
@@ -2041,10 +2092,8 @@ def configure_node(o):
if options.node_use_large_pages or options.node_use_large_pages_script_lld:
warn('''The `--use-largepages` and `--use-largepages-script-lld` options
- have no effect during build time. Support for mapping to large pages is
- now a runtime option of Node.js. Run `node --use-largepages` or add
- `--use-largepages` to the `NODE_OPTIONS` environment variable once
- Node.js is built to enable mapping to large pages.''')
+ have no effect. Mapping the Node.js static code to large pages is
+ no longer supported.''')
if options.no_ifaddrs:
o['defines'] += ['SUNOS_NO_IFADDRS']
@@ -2195,16 +2244,10 @@ def configure_v8(o, configs):
flavor not in ('aix', 'os400', 'zos') and
o['variables']['target_arch'] in maglev_enabled_architectures)
o['variables']['v8_enable_pointer_compression'] = 1 if options.enable_pointer_compression else 0
- # Using the sandbox requires always allocating array buffer backing stores in the sandbox.
- # We currently have many backing stores tied to pointers from C++ land that are not
- # even necessarily dynamic (e.g. in static storage) for fast communication between JS and C++.
- # Until we manage to get rid of all those, v8_enable_sandbox cannot be used.
- # Note that enabling pointer compression without enabling sandbox is unsupported by V8,
- # so this can be broken at any time.
- o['variables']['v8_enable_sandbox'] = 0
- # We set v8_enable_pointer_compression_shared_cage to 0 always, even when
- # pointer compression is enabled so that we don't accidentally enable shared
- # cage mode when pointer compression is on.
+ # Like V8's own default, the sandbox goes with the shared pointer compression
+ # cage. Multi-cage builds give every IsolateGroup its own sandbox, which the
+ # array buffer allocator does not know about yet.
+ o['variables']['v8_enable_sandbox'] = 1 if options.pointer_compression_shared_cage else 0
o['variables']['v8_enable_pointer_compression_shared_cage'] = 1 if options.pointer_compression_shared_cage else 0
o['variables']['v8_enable_external_code_space'] = 1 if options.enable_pointer_compression else 0
o['variables']['v8_enable_31bit_smis_on_64bit_arch'] = 1 if options.enable_pointer_compression else 0
@@ -2229,6 +2272,7 @@ def configure_v8(o, configs):
o['variables']['force_dynamic_crt'] = 1 if options.shared else 0
o['variables']['node_enable_d8'] = b(options.enable_d8)
o['variables']['node_enable_v8windbg'] = b(options.enable_v8windbg)
+ o['variables']['node_enable_v8debughelper'] = b(options.enable_v8debughelper)
if options.enable_d8:
o['variables']['test_isolation_mode'] = 'noop' # Needed by d8.gyp.
if options.without_bundled_v8:
@@ -2236,6 +2280,8 @@ def configure_v8(o, configs):
raise Exception('--enable-d8 is incompatible with --without-bundled-v8.')
if options.enable_v8windbg:
raise Exception('--enable-v8windbg is incompatible with --without-bundled-v8.')
+ if options.enable_v8debughelper:
+ raise Exception('--enable-v8debughelper is incompatible with --without-bundled-v8.')
(pkg_libs, pkg_cflags, pkg_libpath, _) = pkg_config("v8")
if pkg_libs and pkg_libpath:
output['libraries'] += [pkg_libpath] + pkg_libs.split()
@@ -2343,6 +2389,15 @@ def configure_lief(o):
configure_library('lief', o, pkgname='LIEF')
+def configure_perfetto(o):
+ if not options.with_perfetto:
+ if options.shared_perfetto:
+ error('--shared-perfetto requires --with-perfetto')
+ o['variables']['node_shared_perfetto'] = b(False)
+ return
+
+ configure_library('perfetto', o)
+
def configure_sqlite(o):
o['variables']['node_use_sqlite'] = b(not options.without_sqlite)
if options.without_sqlite:
@@ -2398,6 +2453,10 @@ def configure_quic(o):
o['variables']['node_use_quic'] = b(options.experimental_quic and
not options.without_ssl)
+def configure_dtls(o):
+ o['variables']['node_use_dtls'] = b(options.experimental_dtls and
+ not options.without_ssl)
+
def configure_static(o):
if options.fully_static or options.partly_static:
if flavor == 'mac':
@@ -2835,6 +2894,71 @@ def make_bin_override():
configure_library('zlib', output)
configure_library('http_parser', output, pkgname='libllhttp')
configure_library('libuv', output)
+configure_library('abseil', output, pkgname=[
+ 'absl_absl_check',
+ 'absl_absl_log',
+ 'absl_absl_vlog_is_on',
+ 'absl_algorithm_container',
+ 'absl_algorithm',
+ 'absl_any_invocable',
+ 'absl_base',
+ 'absl_bind_front',
+ 'absl_bits',
+ 'absl_btree',
+ 'absl_charset',
+ 'absl_cleanup',
+ 'absl_config',
+ 'absl_cord',
+ 'absl_core_headers',
+ 'absl_die_if_null',
+ 'absl_dynamic_annotations',
+ 'absl_failure_signal_handler',
+ 'absl_fast_type_id',
+ 'absl_fixed_array',
+ 'absl_flat_hash_map',
+ 'absl_flat_hash_set',
+ 'absl_function_ref',
+ 'absl_has_ostream_operator',
+ 'absl_hash_container_defaults',
+ 'absl_hash',
+ 'absl_inlined_vector',
+ 'absl_int128',
+ 'absl_layout',
+ 'absl_leak_check',
+ 'absl_linked_hash_map',
+ 'absl_linked_hash_set',
+ 'absl_log_entry',
+ 'absl_log_globals',
+ 'absl_log_initialize',
+ 'absl_log_severity',
+ 'absl_log_sink_registry',
+ 'absl_log_sink',
+ 'absl_memory',
+ 'absl_no_destructor',
+ 'absl_node_hash_map',
+ 'absl_node_hash_set',
+ 'absl_nullability',
+ 'absl_optional',
+ 'absl_overload',
+ 'absl_prefetch',
+ 'absl_random_bit_gen_ref',
+ 'absl_random_distributions',
+ 'absl_random_random',
+ 'absl_raw_logging_internal',
+ 'absl_span',
+ 'absl_stacktrace',
+ 'absl_status',
+ 'absl_statusor',
+ 'absl_str_format',
+ 'absl_string_view',
+ 'absl_strings',
+ 'absl_symbolize',
+ 'absl_synchronization',
+ 'absl_time',
+ 'absl_type_traits',
+ 'absl_utility',
+ 'absl_variant',
+])
configure_library('ada', output)
configure_library('simdjson', output)
configure_library('simdutf', output)
@@ -2842,12 +2966,14 @@ def make_bin_override():
configure_library('cares', output, pkgname='libcares')
configure_library('gtest', output)
configure_library('hdr_histogram', output)
+configure_library('highway', output, pkgname='libhwy')
configure_library('merve', output)
configure_library('nbytes', output)
configure_library('nghttp2', output, pkgname='libnghttp2')
configure_library('nghttp3', output, pkgname='libnghttp3')
configure_library('ngtcp2', output, pkgname='libngtcp2')
configure_lief(output);
+configure_perfetto(output);
configure_sqlite(output);
configure_ffi(output);
configure_library('temporal_capi', output)
@@ -2856,6 +2982,7 @@ def make_bin_override():
configure_v8(output, configurations)
configure_openssl(output)
configure_quic(output)
+configure_dtls(output)
configure_intl(output)
configure_static(output)
configure_inspector(output)
diff --git a/deps/brotli/unofficial.gni b/deps/brotli/unofficial.gni
index 91001fa43ea4..0c6a7bd8c450 100644
--- a/deps/brotli/unofficial.gni
+++ b/deps/brotli/unofficial.gni
@@ -4,36 +4,49 @@
# The actual configurations are put inside a template in unofficial.gni to
# prevent accidental edits from contributors.
-template("brotli_gn_build") {
- config("brotli_config") {
- include_dirs = [ "c/include" ]
- }
+import("../../node.gni")
- gypi_values = exec_script("../../tools/gypi_to_gn.py",
- [ rebase_path("brotli.gyp") ],
- "scope",
- [ "brotli.gyp" ])
-
- source_set(target_name) {
- forward_variables_from(invoker, "*")
- public_configs = [ ":brotli_config" ]
- sources = gypi_values.brotli_sources
- if (is_linux) {
- defines = [ "OS_LINUX" ]
- } else if (is_mac) {
- defines = [ "OS_MACOSX" ]
- } else if (target_os == "freebsd") {
- defines = [ "OS_FREEBSD" ]
+template("brotli_gn_build") {
+ if (node_shared_brotli) {
+ import("//build/config/linux/pkg_config.gni")
+ pkg_config("brotli_config") {
+ packages = [ "libbrotlidec", "libbrotlienc" ]
}
- if (is_linux) {
- libs = [ "m" ]
+ group(target_name) {
+ forward_variables_from(invoker, "*")
+ public_configs = [ ":brotli_config" ]
}
- if (is_clang || !is_win) {
- cflags_c = [
- "-Wno-implicit-fallthrough",
- "-Wno-unreachable-code",
- "-Wno-unreachable-code-return",
- ]
+ } else {
+ config("brotli_config") {
+ include_dirs = [ "c/include" ]
+ }
+
+ gypi_values = exec_script("../../tools/gypi_to_gn.py",
+ [ rebase_path("brotli.gyp") ],
+ "scope",
+ [ "brotli.gyp" ])
+
+ source_set(target_name) {
+ forward_variables_from(invoker, "*")
+ public_configs = [ ":brotli_config" ]
+ sources = gypi_values.brotli_sources
+ if (is_linux) {
+ defines = [ "OS_LINUX" ]
+ } else if (is_mac) {
+ defines = [ "OS_MACOSX" ]
+ } else if (target_os == "freebsd") {
+ defines = [ "OS_FREEBSD" ]
+ }
+ if (is_linux) {
+ libs = [ "m" ]
+ }
+ if (is_clang || !is_win) {
+ cflags_c = [
+ "-Wno-implicit-fallthrough",
+ "-Wno-unreachable-code",
+ "-Wno-unreachable-code-return",
+ ]
+ }
}
}
}
diff --git a/deps/cares/unofficial.gni b/deps/cares/unofficial.gni
index e02d7f425194..364df1f312e2 100644
--- a/deps/cares/unofficial.gni
+++ b/deps/cares/unofficial.gni
@@ -4,78 +4,91 @@
# The actual configurations are put inside a template in unofficial.gni to
# prevent accidental edits from contributors.
-template("cares_gn_build") {
- config("cares_config") {
- include_dirs = [ "include" ]
- if (!is_component_build) {
- defines = [ "CARES_STATICLIB" ]
- }
- }
-
- gypi_values = exec_script("../../tools/gypi_to_gn.py",
- [ rebase_path("cares.gyp") ],
- "scope",
- [ "cares.gyp" ])
+import("../../node.gni")
- component(target_name) {
- forward_variables_from(invoker, "*")
- public_configs = [ ":cares_config" ]
- if (is_component_build) {
- defines = [ "CARES_BUILDING_LIBRARY" ]
- } else {
- defines = []
+template("cares_gn_build") {
+ if (node_shared_cares) {
+ import("//build/config/linux/pkg_config.gni")
+ pkg_config("cares_config") {
+ packages = [ "libcares" ]
}
- if (is_win) {
- defines += [ "CARES_PULL_WS2TCPIP_H=1" ]
+ group(target_name) {
+ forward_variables_from(invoker, "*")
+ public_configs = [ ":cares_config" ]
}
- if (is_posix) {
- defines += [
- "_DARWIN_USE_64_BIT_INODE=1",
- "_LARGEFILE_SOURCE",
- "_FILE_OFFSET_BITS=64",
- "_GNU_SOURCE",
- "HAVE_CONFIG_H",
- ]
+ } else {
+ config("cares_config") {
+ include_dirs = [ "include" ]
+ if (!is_component_build) {
+ defines = [ "CARES_STATICLIB" ]
+ }
}
- include_dirs = [
- "src/lib",
- "src/lib/include",
- ]
- if (is_win) {
- include_dirs += [ "config/win32" ]
- } else if (is_linux) {
- include_dirs += [ "config/linux" ]
- } else if (is_mac) {
- include_dirs += [ "config/darwin" ]
- }
+ gypi_values = exec_script("../../tools/gypi_to_gn.py",
+ [ rebase_path("cares.gyp") ],
+ "scope",
+ [ "cares.gyp" ])
- if (is_win) {
- libs = [
- "ws2_32.lib",
- "iphlpapi.lib",
- ]
- }
+ component(target_name) {
+ forward_variables_from(invoker, "*")
+ public_configs = [ ":cares_config" ]
+ if (is_component_build) {
+ defines = [ "CARES_BUILDING_LIBRARY" ]
+ } else {
+ defines = []
+ }
+ if (is_win) {
+ defines += [ "CARES_PULL_WS2TCPIP_H=1" ]
+ }
+ if (is_posix) {
+ defines += [
+ "_DARWIN_USE_64_BIT_INODE=1",
+ "_LARGEFILE_SOURCE",
+ "_FILE_OFFSET_BITS=64",
+ "_GNU_SOURCE",
+ "HAVE_CONFIG_H",
+ ]
+ }
- sources = gypi_values.cares_sources_common
- if (is_linux) {
- sources += [ "config/linux/ares_config.h" ]
- }
- if (is_mac) {
- sources += gypi_values.cares_sources_mac
- }
+ include_dirs = [
+ "src/lib",
+ "src/lib/include",
+ ]
+ if (is_win) {
+ include_dirs += [ "config/win32" ]
+ } else if (is_linux) {
+ include_dirs += [ "config/linux" ]
+ } else if (is_mac) {
+ include_dirs += [ "config/darwin" ]
+ }
- if (is_clang) {
if (is_win) {
- cflags_c = [
- "-Wno-macro-redefined",
- ]
- } else {
- cflags_c = [
- "-Wno-implicit-fallthrough",
- "-Wno-unreachable-code",
+ libs = [
+ "ws2_32.lib",
+ "iphlpapi.lib",
]
}
+
+ sources = gypi_values.cares_sources_common
+ if (is_linux) {
+ sources += [ "config/linux/ares_config.h" ]
+ }
+ if (is_mac) {
+ sources += gypi_values.cares_sources_mac
+ }
+
+ if (is_clang) {
+ if (is_win) {
+ cflags_c = [
+ "-Wno-macro-redefined",
+ ]
+ } else {
+ cflags_c = [
+ "-Wno-implicit-fallthrough",
+ "-Wno-unreachable-code",
+ ]
+ }
+ }
}
}
}
diff --git a/deps/googletest/include/gtest/gtest-assertion-result.h b/deps/googletest/include/gtest/gtest-assertion-result.h
index a72ac939102e..882e9849353a 100644
--- a/deps/googletest/include/gtest/gtest-assertion-result.h
+++ b/deps/googletest/include/gtest/gtest-assertion-result.h
@@ -143,13 +143,9 @@ class GTEST_API_ [[nodiscard]] AssertionResult {
// Used in EXPECT_TRUE/FALSE(assertion_result).
AssertionResult(const AssertionResult& other);
-// C4800 is a level 3 warning in Visual Studio 2015 and earlier.
-// This warning is not emitted in Visual Studio 2017.
-// This warning is off by default starting in Visual Studio 2019 but can be
-// enabled with command-line options.
-#if defined(_MSC_VER) && (_MSC_VER < 1910 || _MSC_VER >= 1920)
+ // C4800 is off by default starting in Visual Studio 2019 but can be
+ // enabled with command-line options.
GTEST_DISABLE_MSC_WARNINGS_PUSH_(4800 /* forcing value to bool */)
-#endif
// Used in the EXPECT_TRUE/FALSE(bool_expression).
//
@@ -171,9 +167,7 @@ class GTEST_API_ [[nodiscard]] AssertionResult {
int> = 0>
explicit AssertionResult(const T& success) : success_(success) {}
-#if defined(_MSC_VER) && (_MSC_VER < 1910 || _MSC_VER >= 1920)
GTEST_DISABLE_MSC_WARNINGS_POP_()
-#endif
// Assignment operator.
AssertionResult& operator=(AssertionResult other) {
diff --git a/deps/googletest/include/gtest/gtest-matchers.h b/deps/googletest/include/gtest/gtest-matchers.h
index b5950425cc69..4d7a541d0f1f 100644
--- a/deps/googletest/include/gtest/gtest-matchers.h
+++ b/deps/googletest/include/gtest/gtest-matchers.h
@@ -52,16 +52,9 @@
#include "gtest/internal/gtest-internal.h"
#include "gtest/internal/gtest-port.h"
-// MSVC warning C5046 is new as of VS2017 version 15.8.
-#if defined(_MSC_VER) && _MSC_VER >= 1915
-#define GTEST_MAYBE_5046_ 5046
-#else
-#define GTEST_MAYBE_5046_
-#endif
-
GTEST_DISABLE_MSC_WARNINGS_PUSH_(
- 4251 GTEST_MAYBE_5046_ /* class A needs to have dll-interface to be used by
- clients of class B */
+ // class A needs to have dll-interface to be used by clients of class B
+ 4251 5046
/* Symbol involving type with internal linkage not defined */)
namespace testing {
diff --git a/deps/googletest/include/gtest/gtest-printers.h b/deps/googletest/include/gtest/gtest-printers.h
index f647ee691263..1288313b99ff 100644
--- a/deps/googletest/include/gtest/gtest-printers.h
+++ b/deps/googletest/include/gtest/gtest-printers.h
@@ -104,6 +104,8 @@
#ifndef GOOGLETEST_INCLUDE_GTEST_GTEST_PRINTERS_H_
#define GOOGLETEST_INCLUDE_GTEST_GTEST_PRINTERS_H_
+#include
+
#include
#include
#include
@@ -279,15 +281,13 @@ struct ProtobufPrinter {
struct ConvertibleToIntegerPrinter {
// Since T has no << operator or PrintTo() but can be implicitly
- // converted to BiggestInt, we print it as a BiggestInt.
+ // converted to intmax_t, we print it as an intmax_t.
//
// Most likely T is an enum type (either named or unnamed), in which
// case printing it as an integer is the desired behavior. In case
// T is not an enum, printing it as an integer is the best we can do
// given that it has no user-defined printer.
- static void PrintValue(internal::BiggestInt value, ::std::ostream* os) {
- *os << value;
- }
+ static void PrintValue(intmax_t value, ::std::ostream* os) { *os << value; }
};
struct ConvertibleToStringViewPrinter {
@@ -347,7 +347,7 @@ struct FindFirstPrinter<
// - Print object pointers.
// - Print protocol buffers.
// - Use the stream operator, if available.
-// - Print types convertible to BiggestInt.
+// - Print types convertible to intmax_t.
// - Print types convertible to StringView, if available.
// - Fallback to printing the raw bytes of the object.
template
diff --git a/deps/googletest/include/gtest/gtest.h b/deps/googletest/include/gtest/gtest.h
index 9fe7fb44239e..7c003c5d60e3 100644
--- a/deps/googletest/include/gtest/gtest.h
+++ b/deps/googletest/include/gtest/gtest.h
@@ -1419,14 +1419,14 @@ class [[nodiscard]] EqHelper {
}
// With this overloaded version, we allow anonymous enums to be used
- // in {ASSERT|EXPECT}_EQ when compiled with gcc 4, as anonymous
- // enums can be implicitly cast to BiggestInt.
+ // in {ASSERT|EXPECT}_EQ when compiled with gcc 4, as anonymous enums can be
+ // implicitly cast to intmax_t.
//
// Even though its body looks the same as the above version, we
// cannot merge the two, as it will make anonymous enums unhappy.
static AssertionResult Compare(const char* lhs_expression,
- const char* rhs_expression, BiggestInt lhs,
- BiggestInt rhs) {
+ const char* rhs_expression, intmax_t lhs,
+ intmax_t rhs) {
return CmpHelperEQ(lhs_expression, rhs_expression, lhs, rhs);
}
diff --git a/deps/googletest/include/gtest/internal/gtest-port.h b/deps/googletest/include/gtest/internal/gtest-port.h
index 94a56ce3dd9b..154be3c16029 100644
--- a/deps/googletest/include/gtest/internal/gtest-port.h
+++ b/deps/googletest/include/gtest/internal/gtest-port.h
@@ -236,7 +236,6 @@
// Integer types:
// TypeWithSize - maps an integer to a int type.
// TimeInMillis - integers of known sizes.
-// BiggestInt - the biggest signed integer type.
//
// Command-line utilities:
// GetInjectableArgvs() - returns the command line as a vector of strings.
@@ -342,13 +341,6 @@
#define GTEST_INIT_GOOGLE_TEST_NAME_ "testing::InitGoogleTest"
#endif // !defined(GTEST_INIT_GOOGLE_TEST_NAME_)
-// Determines the version of gcc that is used to compile this.
-#ifdef __GNUC__
-// 40302 means version 4.3.2.
-#define GTEST_GCC_VER_ \
- (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__)
-#endif // __GNUC__
-
// Macros for disabling Microsoft Visual C++ warnings.
//
// GTEST_DISABLE_MSC_WARNINGS_PUSH_(4800 4385)
@@ -456,14 +448,6 @@ typedef struct _RTL_CRITICAL_SECTION GTEST_CRITICAL_SECTION;
#if defined(_MSC_VER) && defined(_CPPUNWIND)
// MSVC defines _CPPUNWIND to 1 if and only if exceptions are enabled.
#define GTEST_HAS_EXCEPTIONS 1
-#elif defined(__BORLANDC__)
-// C++Builder's implementation of the STL uses the _HAS_EXCEPTIONS
-// macro to enable exceptions, so we'll do the same.
-// Assumes that exceptions are enabled by default.
-#ifndef _HAS_EXCEPTIONS
-#define _HAS_EXCEPTIONS 1
-#endif // _HAS_EXCEPTIONS
-#define GTEST_HAS_EXCEPTIONS _HAS_EXCEPTIONS
#elif defined(__clang__)
// clang defines __EXCEPTIONS if and only if exceptions are enabled before clang
// 220714, but if and only if cleanups are enabled after that. In Obj-C++ files,
@@ -481,23 +465,11 @@ typedef struct _RTL_CRITICAL_SECTION GTEST_CRITICAL_SECTION;
#elif defined(__GNUC__) && defined(__EXCEPTIONS) && __EXCEPTIONS
// gcc defines __EXCEPTIONS to 1 if and only if exceptions are enabled.
#define GTEST_HAS_EXCEPTIONS 1
-#elif defined(__SUNPRO_CC)
-// Sun Pro CC supports exceptions. However, there is no compile-time way of
-// detecting whether they are enabled or not. Therefore, we assume that
-// they are enabled unless the user tells us otherwise.
-#define GTEST_HAS_EXCEPTIONS 1
-#elif defined(__IBMCPP__) && defined(__EXCEPTIONS) && __EXCEPTIONS
-// xlC defines __EXCEPTIONS to 1 if and only if exceptions are enabled.
-#define GTEST_HAS_EXCEPTIONS 1
-#elif defined(__HP_aCC)
-// Exception handling is in effect by default in HP aCC compiler. It has to
-// be turned of by +noeh compiler option if desired.
-#define GTEST_HAS_EXCEPTIONS 1
#else
// For other compilers, we assume exceptions are disabled to be
// conservative.
#define GTEST_HAS_EXCEPTIONS 0
-#endif // defined(_MSC_VER) || defined(__BORLANDC__)
+#endif // defined(_MSC_VER) && defined(_CPPUNWIND)
#endif // GTEST_HAS_EXCEPTIONS
// MSVC either defines wchar_t as a typedef of unsigned short, or as a native
@@ -621,16 +593,6 @@ typedef struct _RTL_CRITICAL_SECTION GTEST_CRITICAL_SECTION;
#define GTEST_HAS_RTTI __has_feature(cxx_rtti)
-// Starting with version 9.0 IBM Visual Age defines __RTTI_ALL__ to 1 if
-// both the typeid and dynamic_cast features are present.
-#elif defined(__IBMCPP__) && (__IBMCPP__ >= 900)
-
-#ifdef __RTTI_ALL__
-#define GTEST_HAS_RTTI 1
-#else
-#define GTEST_HAS_RTTI 0
-#endif
-
#else
// For all other compilers, we assume RTTI is enabled.
@@ -752,8 +714,7 @@ typedef struct _RTL_CRITICAL_SECTION GTEST_CRITICAL_SECTION;
// Typed tests need and variadic macros, which GCC, VC++ 8.0,
// Sun Pro CC, IBM Visual Age, and HP aCC support.
-#if defined(__GNUC__) || defined(_MSC_VER) || defined(__SUNPRO_CC) || \
- defined(__IBMCPP__) || defined(__HP_aCC)
+#if defined(__GNUC__) || defined(_MSC_VER)
#define GTEST_HAS_TYPED_TEST 1
#define GTEST_HAS_TYPED_TEST_P 1
#endif
@@ -867,8 +828,7 @@ typedef struct _RTL_CRITICAL_SECTION GTEST_CRITICAL_SECTION;
#ifndef GTEST_HAS_SEH
// The user didn't tell us, so we need to figure it out.
-#if defined(_MSC_VER) || defined(__BORLANDC__)
-// These two compilers are known to support SEH.
+#ifdef _MSC_VER
#define GTEST_HAS_SEH 1
#else
// Assume no SEH.
@@ -2146,12 +2106,6 @@ inline bool IsDir(const StatStruct& st) { return S_ISDIR(st.st_mode); }
#ifdef GTEST_OS_WINDOWS
-#ifdef __BORLANDC__
-inline int DoIsATTY(int fd) { return isatty(fd); }
-inline int StrCaseCmp(const char* s1, const char* s2) {
- return stricmp(s1, s2);
-}
-#else // !__BORLANDC__
#if defined(GTEST_OS_WINDOWS_MOBILE) || defined(GTEST_OS_ZOS) || \
defined(GTEST_OS_IOS) || defined(GTEST_OS_WINDOWS_PHONE) || \
defined(GTEST_OS_WINDOWS_RT) || defined(ESP_PLATFORM)
@@ -2162,7 +2116,6 @@ inline int DoIsATTY(int fd) { return _isatty(fd); }
inline int StrCaseCmp(const char* s1, const char* s2) {
return _stricmp(s1, s2);
}
-#endif // __BORLANDC__
#else
@@ -2264,11 +2217,6 @@ inline const char* GetEnv(const char* name) {
// We are on an embedded platform, which has no environment variables.
static_cast(name); // To prevent 'unused argument' warning.
return nullptr;
-#elif defined(__BORLANDC__) || defined(__SunOS_5_8) || defined(__SunOS_5_9)
- // Environment variables which we programmatically clear will be set to the
- // empty string rather than unset (NULL). Handle that case.
- const char* const env = getenv(name);
- return (env != nullptr && env[0] != '\0') ? env : nullptr;
#else
return getenv(name);
#endif
@@ -2303,14 +2251,6 @@ GTEST_DISABLE_DEPRECATED_POP_()
#define GTEST_SNPRINTF_ snprintf
#endif
-// The biggest signed integer type the compiler supports.
-//
-// long long is guaranteed to be at least 64-bits in C++11.
-using BiggestInt = long long; // NOLINT
-
-// The maximum number a BiggestInt can represent.
-constexpr BiggestInt kMaxBiggestInt = (std::numeric_limits::max)();
-
// This template class serves as a compile-time function from size to
// type. It maps a size in bytes to a primitive type with that
// size. e.g.
diff --git a/deps/googletest/include/gtest/internal/gtest-string.h b/deps/googletest/include/gtest/internal/gtest-string.h
index 2363034fc0db..d0652c13c68e 100644
--- a/deps/googletest/include/gtest/internal/gtest-string.h
+++ b/deps/googletest/include/gtest/internal/gtest-string.h
@@ -43,11 +43,6 @@
#ifndef GOOGLETEST_INCLUDE_GTEST_INTERNAL_GTEST_STRING_H_
#define GOOGLETEST_INCLUDE_GTEST_INTERNAL_GTEST_STRING_H_
-#ifdef __BORLANDC__
-// string.h is not guaranteed to provide strcpy on C++ Builder.
-#include
-#endif
-
#include
#include
diff --git a/deps/googletest/include/gtest/internal/gtest-type-util.h b/deps/googletest/include/gtest/internal/gtest-type-util.h
index 78da05316d6b..fb4d6823744e 100644
--- a/deps/googletest/include/gtest/internal/gtest-type-util.h
+++ b/deps/googletest/include/gtest/internal/gtest-type-util.h
@@ -47,8 +47,6 @@
// libstdc++ (which is where cxxabi.h comes from).
#if GTEST_HAS_CXXABI_H_
#include
-#elif defined(__HP_aCC)
-#include
#endif // GTEST_HASH_CXXABI_H_
namespace testing {
@@ -90,13 +88,11 @@ inline std::string CanonicalizeForStdLibVersioning(std::string s) {
// GetTypeName(const std::type_info&) returns a human-readable name of type T.
inline std::string GetTypeName(const std::type_info& type) {
const char* const name = type.name();
-#if GTEST_HAS_CXXABI_H_ || defined(__HP_aCC)
+#if GTEST_HAS_CXXABI_H_
int status = 0;
// gcc's implementation of typeid(T).name() mangles the type name,
// so we have to demangle it.
-#if GTEST_HAS_CXXABI_H_
using abi::__cxa_demangle;
-#endif // GTEST_HAS_CXXABI_H_
char* const readable_name = __cxa_demangle(name, nullptr, nullptr, &status);
const std::string name_str(status == 0 ? readable_name : name);
free(readable_name);
@@ -117,7 +113,7 @@ inline std::string GetTypeName(const std::type_info& type) {
return s;
#else
return name;
-#endif // GTEST_HAS_CXXABI_H_ || __HP_aCC
+#endif // GTEST_HAS_CXXABI_H_
}
#endif // GTEST_HAS_RTTI
diff --git a/deps/googletest/src/gtest-port.cc b/deps/googletest/src/gtest-port.cc
index 5aa800305c7a..be5b16e76d3e 100644
--- a/deps/googletest/src/gtest-port.cc
+++ b/deps/googletest/src/gtest-port.cc
@@ -1242,14 +1242,14 @@ static std::string GetCapturedStream(CapturedStream** captured_stream) {
return content;
}
-#if defined(_MSC_VER) || defined(__BORLANDC__)
-// MSVC and C++Builder do not provide a definition of STDERR_FILENO.
+#if defined(_MSC_VER)
+// MSVC does not provide a definition of STDERR_FILENO.
const int kStdOutFileno = 1;
const int kStdErrFileno = 2;
#else
const int kStdOutFileno = STDOUT_FILENO;
const int kStdErrFileno = STDERR_FILENO;
-#endif // defined(_MSC_VER) || defined(__BORLANDC__)
+#endif // defined(_MSC_VER)
// Starts capturing stdout.
void CaptureStdout() {
diff --git a/deps/googletest/src/gtest.cc b/deps/googletest/src/gtest.cc
index 30a2bb7de0bc..47c60da22916 100644
--- a/deps/googletest/src/gtest.cc
+++ b/deps/googletest/src/gtest.cc
@@ -5289,17 +5289,8 @@ void TestEventListeners::SuppressEventForwarding(bool suppress) {
// call this before main() starts, from which point on the return
// value will never change.
UnitTest* UnitTest::GetInstance() {
- // CodeGear C++Builder insists on a public destructor for the
- // default implementation. Use this implementation to keep good OO
- // design with private destructor.
-
-#if defined(__BORLANDC__)
- static UnitTest* const instance = new UnitTest;
- return instance;
-#else
static UnitTest instance;
return &instance;
-#endif // defined(__BORLANDC__)
}
// Gets the number of successful test suites.
diff --git a/deps/histogram/unofficial.gni b/deps/histogram/unofficial.gni
index 1173ab298ae7..fd8c3a276bcb 100644
--- a/deps/histogram/unofficial.gni
+++ b/deps/histogram/unofficial.gni
@@ -4,29 +4,42 @@
# The actual configurations are put inside a template in unofficial.gni to
# prevent accidental edits from contributors.
+import("../../node.gni")
+
template("histogram_gn_build") {
- config("histogram_config") {
- include_dirs = [ "include" ]
- }
+ if (node_shared_hdr_histogram) {
+ import("//build/config/linux/pkg_config.gni")
+ pkg_config("histogram_config") {
+ packages = [ "hdr_histogram" ]
+ }
+ group(target_name) {
+ forward_variables_from(invoker, "*")
+ public_configs = [ ":histogram_config" ]
+ }
+ } else {
+ config("histogram_config") {
+ include_dirs = [ "include" ]
+ }
- gypi_values = exec_script("../../tools/gypi_to_gn.py",
- [ rebase_path("histogram.gyp") ],
- "scope",
- [ "histogram.gyp" ])
+ gypi_values = exec_script("../../tools/gypi_to_gn.py",
+ [ rebase_path("histogram.gyp") ],
+ "scope",
+ [ "histogram.gyp" ])
- source_set(target_name) {
- forward_variables_from(invoker, "*")
- public_configs = [ ":histogram_config" ]
- sources = gypi_values.histogram_sources
- if (is_clang || !is_win) {
- cflags_c = [
- "-Wno-atomic-alignment",
- "-Wno-incompatible-pointer-types",
- "-Wno-unused-function",
- ]
- }
- if (is_linux) {
- libs = [ "atomic" ]
+ source_set(target_name) {
+ forward_variables_from(invoker, "*")
+ public_configs = [ ":histogram_config" ]
+ sources = gypi_values.histogram_sources
+ if (is_clang || !is_win) {
+ cflags_c = [
+ "-Wno-atomic-alignment",
+ "-Wno-incompatible-pointer-types",
+ "-Wno-unused-function",
+ ]
+ }
+ if (is_linux) {
+ libs = [ "atomic" ]
+ }
}
}
}
diff --git a/deps/llhttp/unofficial.gni b/deps/llhttp/unofficial.gni
index fdce32e59764..28257dcb33ae 100644
--- a/deps/llhttp/unofficial.gni
+++ b/deps/llhttp/unofficial.gni
@@ -4,26 +4,39 @@
# The actual configurations are put inside a template in unofficial.gni to
# prevent accidental edits from contributors.
+import("../../node.gni")
+
template("llhttp_gn_build") {
- config("llhttp_config") {
- include_dirs = [ "include" ]
- }
+ if (node_shared_http_parser) {
+ import("//build/config/linux/pkg_config.gni")
+ pkg_config("llhttp_config") {
+ packages = [ "libllhttp" ]
+ }
+ group(target_name) {
+ forward_variables_from(invoker, "*")
+ public_configs = [ ":llhttp_config" ]
+ }
+ } else {
+ config("llhttp_config") {
+ include_dirs = [ "include" ]
+ }
- gypi_values = exec_script("../../tools/gypi_to_gn.py",
- [ rebase_path("llhttp.gyp") ],
- "scope",
- [ "llhttp.gyp" ])
+ gypi_values = exec_script("../../tools/gypi_to_gn.py",
+ [ rebase_path("llhttp.gyp") ],
+ "scope",
+ [ "llhttp.gyp" ])
- source_set(target_name) {
- forward_variables_from(invoker, "*")
- public_configs = [ ":llhttp_config" ]
- include_dirs = [ "include" ]
- sources = gypi_values.llhttp_sources
- if (is_clang || !is_win) {
- cflags_c = [
- "-Wno-implicit-fallthrough",
- "-Wno-unreachable-code",
- ]
+ source_set(target_name) {
+ forward_variables_from(invoker, "*")
+ public_configs = [ ":llhttp_config" ]
+ include_dirs = [ "include" ]
+ sources = gypi_values.llhttp_sources
+ if (is_clang || !is_win) {
+ cflags_c = [
+ "-Wno-implicit-fallthrough",
+ "-Wno-unreachable-code",
+ ]
+ }
}
}
}
diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index fb7446578f57..20f22e614525 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -14,9 +14,11 @@
#endif
#include
#include
+#include
#include
#include
#include
+#include
#if OPENSSL_VERSION_MAJOR >= 3
#include
#include
@@ -129,6 +131,31 @@ struct OpenSSLBufferDeleter {
};
using OpenSSLBufferPointer =
std::unique_ptr;
+
+struct RsaOtherPrimeParamNames {
+ const char* factor;
+ const char* exponent;
+ const char* coefficient;
+};
+
+#define RSA_OTHER_PRIME_PARAM_NAMES(prime, coefficient) \
+ { \
+ OSSL_PKEY_PARAM_RSA_FACTOR #prime, OSSL_PKEY_PARAM_RSA_EXPONENT #prime, \
+ OSSL_PKEY_PARAM_RSA_COEFFICIENT #coefficient \
+ }
+
+constexpr std::array kRsaOtherPrimeParamNames = {{
+ RSA_OTHER_PRIME_PARAM_NAMES(3, 2),
+ RSA_OTHER_PRIME_PARAM_NAMES(4, 3),
+ RSA_OTHER_PRIME_PARAM_NAMES(5, 4),
+ RSA_OTHER_PRIME_PARAM_NAMES(6, 5),
+ RSA_OTHER_PRIME_PARAM_NAMES(7, 6),
+ RSA_OTHER_PRIME_PARAM_NAMES(8, 7),
+ RSA_OTHER_PRIME_PARAM_NAMES(9, 8),
+ RSA_OTHER_PRIME_PARAM_NAMES(10, 9),
+}};
+
+#undef RSA_OTHER_PRIME_PARAM_NAMES
#endif
static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON =
@@ -507,23 +534,43 @@ DataPointer DataPointer::resize(size_t len) {
}
// ============================================================================
-bool isFipsEnabled() {
- ClearErrorOnReturn clear_error_on_return;
+namespace {
+// This generation only coordinates cache invalidation. It does not make
+// OpenSSL default property changes safe to race with crypto operations.
+std::atomic fips_state_generation{0};
+
+bool isFipsEnabledRaw() {
#if OPENSSL_VERSION_MAJOR >= 3
return EVP_default_properties_is_fips_enabled(nullptr) == 1;
#else
return FIPS_mode() == 1;
#endif
}
+} // namespace
+
+bool isFipsEnabled() {
+ ClearErrorOnReturn clear_error_on_return;
+ return isFipsEnabledRaw();
+}
bool setFipsEnabled(bool enable, CryptoErrorList* errors) {
- if (isFipsEnabled() == enable) return true;
+ const bool was_enabled = isFipsEnabled();
+ if (was_enabled == enable) return true;
ClearErrorOnReturn clearErrorOnReturn(errors);
#if OPENSSL_VERSION_MAJOR >= 3
- return EVP_default_properties_enable_fips(nullptr, enable ? 1 : 0) == 1;
+ const bool success =
+ EVP_default_properties_enable_fips(nullptr, enable ? 1 : 0) == 1;
#else
- return FIPS_mode_set(enable ? 1 : 0) == 1;
+ const bool success = FIPS_mode_set(enable ? 1 : 0) == 1;
#endif
+ if (success && isFipsEnabledRaw() != was_enabled) {
+ fips_state_generation.fetch_add(1, std::memory_order_release);
+ }
+ return success;
+}
+
+uint64_t getFipsStateGeneration() {
+ return fips_state_generation.load(std::memory_order_acquire);
}
bool testFipsEnabled() {
@@ -3060,6 +3107,19 @@ EVPKeyPointer EVPKeyPointer::NewRSA(const Rsa& rsa) {
bld.get(), OSSL_PKEY_PARAM_RSA_COEFFICIENT1, private_key.qi) != 1) {
return {};
}
+
+ const auto other_prime_infos = rsa.getOtherPrimeInfos();
+ if (other_prime_infos.size() > kRsaOtherPrimeParamNames.size()) return {};
+ for (size_t i = 0; i < other_prime_infos.size(); i++) {
+ const auto& info = other_prime_infos[i];
+ const auto& names = kRsaOtherPrimeParamNames[i];
+ if (info.r == nullptr || info.d == nullptr || info.t == nullptr ||
+ OSSL_PARAM_BLD_push_BN(bld.get(), names.factor, info.r) != 1 ||
+ OSSL_PARAM_BLD_push_BN(bld.get(), names.exponent, info.d) != 1 ||
+ OSSL_PARAM_BLD_push_BN(bld.get(), names.coefficient, info.t) != 1) {
+ return {};
+ }
+ }
selection = EVP_PKEY_KEYPAIR;
}
@@ -4406,13 +4466,211 @@ bool SSLCtxPointer::setCipherSuites(const char* ciphers) {
// ============================================================================
-#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
+namespace {
+constexpr char AsciiToLower(char c) {
+ return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c;
+}
+
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+constexpr auto kUnsupportedCipherFlags =
+ EVP_CIPH_FLAG_CIPHER_WITH_MAC | EVP_CIPH_FLAG_TLS1_1_MULTIBLOCK;
+
+bool HasUnsupportedCipherFlags(const EVP_CIPHER* cipher) {
+ return (EVP_CIPHER_get_flags(cipher) & kUnsupportedCipherFlags) != 0;
+}
+
+bool IsSupportedLegacyCipher(const EVP_CIPHER* cipher) {
+ return cipher != nullptr && cipher != EVP_enc_null() &&
+ !HasUnsupportedCipherFlags(cipher);
+}
+
+bool IsSupportedFetchedCipher(const EVP_CIPHER* cipher) {
+ if (cipher == nullptr || EVP_CIPHER_is_a(cipher, "NULL") ||
+ HasUnsupportedCipherFlags(cipher)) {
+ return false;
+ }
+
+#ifdef OSSL_CIPHER_PARAM_ENCRYPT_THEN_MAC
+ int encrypt_then_mac = 0;
+ OSSL_PARAM params[] = {
+ OSSL_PARAM_construct_int(OSSL_CIPHER_PARAM_ENCRYPT_THEN_MAC,
+ &encrypt_then_mac),
+ OSSL_PARAM_construct_end(),
+ };
+ if (EVP_CIPHER_get_params(const_cast(cipher), params) == 1 &&
+ encrypt_then_mac != 0) {
+ return false;
+ }
+#endif
+
+ return true;
+}
+
+void PushAlgorithmAlias(const char* name, void* arg) {
+ if (name == nullptr) return;
+ static_cast*>(arg)->emplace_back(name);
+}
+#endif
+} // namespace
+
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
Cipher::Cipher(DeleteFnPtr cipher)
: cipher_(cipher.get()), fetched_cipher_(std::move(cipher)) {}
#endif
+size_t CaseInsensitiveNameHash::operator()(
+ std::string_view name) const noexcept {
+ size_t hash = 5381;
+ for (char c : name) hash = ((hash << 5) + hash) ^ AsciiToLower(c);
+ return hash;
+}
+
+bool CaseInsensitiveNameEqual::operator()(std::string_view lhs,
+ std::string_view rhs) const noexcept {
+ if (lhs.size() != rhs.size()) return false;
+ for (size_t n = 0; n < lhs.size(); n++) {
+ if (AsciiToLower(lhs[n]) != AsciiToLower(rhs[n])) return false;
+ }
+ return true;
+}
+
+DigestCache::Result DigestCache::lookup(const char* name,
+ uint64_t generation) const {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ if (generation_ != generation) return {};
+ const auto it = aliases_.find(name);
+ if (it == aliases_.end()) return {};
+ return lookup(it->second, generation);
+#else
+ static_cast(name);
+ static_cast(generation);
+ return {};
+#endif
+}
+
+DigestCache::Result DigestCache::insert(const char* name,
+ const EVP_MD* digest,
+ uint64_t generation) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ if (generation_ != generation || name == nullptr || digest == nullptr) {
+ return {};
+ }
+
+ const char* canonical_name = EVP_MD_get0_name(digest);
+ const OSSL_PROVIDER* provider = EVP_MD_get0_provider(digest);
+ if (canonical_name == nullptr || provider == nullptr) return {};
+
+ for (size_t index = 0; index < digests_.size(); index++) {
+ const EVP_MD* cached = digests_[index].get();
+ if (cached == nullptr) continue;
+ const char* cached_name = EVP_MD_get0_name(cached);
+ if (EVP_MD_get0_provider(cached) == provider && cached_name != nullptr &&
+ CaseInsensitiveNameEqual()(cached_name, canonical_name)) {
+ const int32_t id = static_cast(first_id_ + index);
+ aliases_.insert_or_assign(name, id);
+ return {cached, id};
+ }
+ }
+
+ if (next_id_ == UINT32_MAX ||
+ EVP_MD_up_ref(const_cast(digest)) != 1) {
+ return {};
+ }
+
+ digests_.emplace_back(const_cast(digest));
+ const int32_t id = static_cast(next_id_++);
+ const size_t index = digests_.size() - 1;
+
+ std::vector aliases;
+ EVP_MD_names_do_all(digests_[index].get(), PushAlgorithmAlias, &aliases);
+ for (const std::string& alias : aliases) aliases_.emplace(alias, id);
+ aliases_.insert_or_assign(name, id);
+
+ return {digests_[index].get(), id};
+#else
+ static_cast(name);
+ static_cast(digest);
+ static_cast(generation);
+ return {};
+#endif
+}
+
+void DigestCache::reset(uint64_t generation) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ if (generation_ == generation) return;
+ aliases_.clear();
+ digests_.clear();
+ first_id_ = next_id_;
+#endif
+ generation_ = generation;
+}
+
+const DigestCache::AliasMap& DigestCache::aliases() const {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ return aliases_;
+#else
+ static const AliasMap empty;
+ return empty;
+#endif
+}
+
+const EVP_CIPHER* CipherCache::lookup(const char* name, uint64_t generation) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ if (generation_ != generation) {
+ aliases_.clear();
+ ciphers_.clear();
+ generation_ = generation;
+ }
+
+ const auto it = aliases_.find(name);
+ if (it == aliases_.end()) return nullptr;
+ if (it->second >= ciphers_.size()) return nullptr;
+ return ciphers_[it->second].get();
+#else
+ static_cast(name);
+ static_cast(generation);
+ return nullptr;
+#endif
+}
+
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+const EVP_CIPHER* CipherCache::insert(
+ const char* name,
+ DeleteFnPtr&& cipher,
+ uint64_t generation) {
+ if (generation_ != generation || cipher == nullptr) return nullptr;
+
+ const char* canonical_name = EVP_CIPHER_get0_name(cipher.get());
+ const OSSL_PROVIDER* provider = EVP_CIPHER_get0_provider(cipher.get());
+ if (canonical_name != nullptr && provider != nullptr) {
+ for (size_t id = 0; id < ciphers_.size(); id++) {
+ const EVP_CIPHER* cached = ciphers_[id].get();
+ const char* cached_name = EVP_CIPHER_get0_name(cached);
+ if (EVP_CIPHER_get0_provider(cached) == provider &&
+ cached_name != nullptr &&
+ CaseInsensitiveNameEqual()(cached_name, canonical_name)) {
+ aliases_.insert_or_assign(name, id);
+ return cached;
+ }
+ }
+ }
+
+ ciphers_.emplace_back(std::move(cipher));
+ const size_t id = ciphers_.size() - 1;
+
+ std::vector aliases;
+ EVP_CIPHER_names_do_all(ciphers_[id].get(), PushAlgorithmAlias, &aliases);
+ for (const std::string& alias : aliases) {
+ aliases_.emplace(alias, id);
+ }
+ aliases_.insert_or_assign(name, id);
+
+ return ciphers_[id].get();
+}
+#endif
+
Cipher::Cipher(const Cipher& other) : cipher_(other.cipher_) {
-#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
if (other.fetched_cipher_ != nullptr) {
if (EVP_CIPHER_up_ref(other.fetched_cipher_.get()) == 1) {
fetched_cipher_.reset(other.fetched_cipher_.get());
@@ -4425,7 +4683,7 @@ Cipher::Cipher(const Cipher& other) : cipher_(other.cipher_) {
Cipher& Cipher::operator=(const Cipher& other) {
if (this == &other) return *this;
-#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
if (other.fetched_cipher_ != nullptr) {
if (EVP_CIPHER_up_ref(other.fetched_cipher_.get()) == 1) {
fetched_cipher_.reset(other.fetched_cipher_.get());
@@ -4442,40 +4700,59 @@ Cipher& Cipher::operator=(const Cipher& other) {
return *this;
}
-const Cipher Cipher::FromName(const char* name) {
+const Cipher Cipher::FromName(const char* name, CipherCache* cache) {
const EVP_CIPHER* cipher = EVP_get_cipherbyname(name);
- if (cipher != nullptr) return Cipher(cipher);
+ if (cipher != nullptr) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ if (!IsSupportedLegacyCipher(cipher)) return Cipher();
+#endif
+ return Cipher(cipher);
+ }
+
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ // A resolution that overlaps a FIPS transition may use either property
+ // state. The cache retains the generation observed here, so the first
+ // resolution begun after the transition clears any stale entries.
+ const uint64_t generation = getFipsStateGeneration();
+ if (cache != nullptr) {
+ if (const EVP_CIPHER* cached = cache->lookup(name, generation)) {
+ return Cipher(cached);
+ }
+ }
-#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
MarkPopErrorOnReturn mark_pop_error_on_return;
DeleteFnPtr fetched(
EVP_CIPHER_fetch(nullptr, name, nullptr));
- if (fetched == nullptr) return Cipher();
+ if (!IsSupportedFetchedCipher(fetched.get())) return Cipher();
- const int mode = EVP_CIPHER_mode(fetched.get());
- const bool is_siv_mode =
-#if OPENSSL_WITH_AES_SIV
- mode == EVP_CIPH_SIV_MODE ||
-#endif
-#if OPENSSL_WITH_AES_GCM_SIV
- mode == EVP_CIPH_GCM_SIV_MODE ||
-#endif
- false;
- if (is_siv_mode) return Cipher(std::move(fetched));
+ if (cache != nullptr && generation == getFipsStateGeneration()) {
+ if (const EVP_CIPHER* cached =
+ cache->insert(name, std::move(fetched), generation)) {
+ return Cipher(cached);
+ }
+ }
- return Cipher();
+ return Cipher(std::move(fetched));
#else
+ static_cast(cache);
return Cipher();
#endif
}
-const Cipher Cipher::FromNid(int nid) {
+const Cipher Cipher::FromNid(int nid, CipherCache* cache) {
const EVP_CIPHER* cipher = EVP_get_cipherbynid(nid);
- if (cipher != nullptr) return Cipher(cipher);
+ if (cipher != nullptr) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ if (!IsSupportedLegacyCipher(cipher)) return Cipher();
+#endif
+ return Cipher(cipher);
+ }
-#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
const char* name = OBJ_nid2sn(nid);
- if (name != nullptr) return FromName(name);
+ if (name != nullptr) return FromName(name, cache);
+#else
+ static_cast(cache);
#endif
return Cipher();
@@ -4485,27 +4762,79 @@ const Cipher Cipher::FromCtx(const CipherCtxPointer& ctx) {
return Cipher(GetCipherCtxCipher(ctx.get()));
}
-const Cipher Cipher::EMPTY = Cipher();
-const Cipher Cipher::AES_128_CBC = Cipher::FromNid(NID_aes_128_cbc);
-const Cipher Cipher::AES_192_CBC = Cipher::FromNid(NID_aes_192_cbc);
-const Cipher Cipher::AES_256_CBC = Cipher::FromNid(NID_aes_256_cbc);
-const Cipher Cipher::AES_128_CTR = Cipher::FromNid(NID_aes_128_ctr);
-const Cipher Cipher::AES_192_CTR = Cipher::FromNid(NID_aes_192_ctr);
-const Cipher Cipher::AES_256_CTR = Cipher::FromNid(NID_aes_256_ctr);
-const Cipher Cipher::AES_128_GCM = Cipher::FromNid(NID_aes_128_gcm);
-const Cipher Cipher::AES_192_GCM = Cipher::FromNid(NID_aes_192_gcm);
-const Cipher Cipher::AES_256_GCM = Cipher::FromNid(NID_aes_256_gcm);
-const Cipher Cipher::AES_128_KW = Cipher::FromNid(NID_id_aes128_wrap);
-const Cipher Cipher::AES_192_KW = Cipher::FromNid(NID_id_aes192_wrap);
-const Cipher Cipher::AES_256_KW = Cipher::FromNid(NID_id_aes256_wrap);
+namespace {
+template
+const Cipher& GetPredefinedCipher() {
+ static const Cipher cipher = Cipher::FromNid(nid);
+ return cipher;
+}
+} // namespace
+
+const Cipher& Cipher::AES_128_CBC() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_192_CBC() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_256_CBC() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_128_CTR() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_192_CTR() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_256_CTR() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_128_GCM() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_192_GCM() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_256_GCM() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_128_KW() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_192_KW() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_256_KW() {
+ return GetPredefinedCipher();
+}
#ifndef OPENSSL_IS_BORINGSSL
-const Cipher Cipher::AES_128_OCB = Cipher::FromNid(NID_aes_128_ocb);
-const Cipher Cipher::AES_192_OCB = Cipher::FromNid(NID_aes_192_ocb);
-const Cipher Cipher::AES_256_OCB = Cipher::FromNid(NID_aes_256_ocb);
+const Cipher& Cipher::AES_128_OCB() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_192_OCB() {
+ return GetPredefinedCipher();
+}
+
+const Cipher& Cipher::AES_256_OCB() {
+ return GetPredefinedCipher();
+}
#endif
-const Cipher Cipher::CHACHA20_POLY1305 = Cipher::FromNid(NID_chacha20_poly1305);
+const Cipher& Cipher::CHACHA20_POLY1305() {
+ return GetPredefinedCipher();
+}
bool Cipher::isGcmMode() const {
if (!cipher_) return false;
@@ -4527,6 +4856,15 @@ bool Cipher::isCcmMode() const {
return getMode() == EVP_CIPH_CCM_MODE;
}
+bool Cipher::isCtsMode() const {
+ if (!cipher_) return false;
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ return (EVP_CIPHER_get_flags(cipher_) & EVP_CIPH_FLAG_CTS) != 0;
+#else
+ return false;
+#endif
+}
+
bool Cipher::isOcbMode() const {
if (!cipher_) return false;
return getMode() == EVP_CIPH_OCB_MODE;
@@ -4631,7 +4969,7 @@ const char* Cipher::getName() const {
const char* name = OBJ_nid2sn(nid);
if (name != nullptr) return name;
}
-#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
return EVP_CIPHER_get0_name(cipher_);
#else
return {};
@@ -4728,11 +5066,57 @@ bool CipherCtxPointer::setAeadTagLength(size_t length) {
ctx_.get(), EVP_CTRL_AEAD_SET_TAG, length, nullptr);
}
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+namespace {
+// OSSL_CIPHER_PARAM_XTS_STANDARD is not defined by OpenSSL 3.0. Use its
+// parameter name directly so custom 3.0 providers can advertise it too.
+constexpr char kCipherParamXtsStandard[] = "xts_standard";
+
+bool SetCipherCtxStringParam(EVP_CIPHER_CTX* ctx,
+ const char* key,
+ const char* value) {
+ if (ctx == nullptr || value == nullptr) return false;
+
+ const OSSL_PARAM* settable = EVP_CIPHER_CTX_settable_params(ctx);
+ const OSSL_PARAM* descriptor =
+ settable == nullptr ? nullptr : OSSL_PARAM_locate_const(settable, key);
+ if (descriptor == nullptr ||
+ descriptor->data_type != OSSL_PARAM_UTF8_STRING) {
+ return false;
+ }
+
+ OSSL_PARAM params[] = {
+ OSSL_PARAM_construct_utf8_string(key, const_cast(value), 0),
+ OSSL_PARAM_END,
+ };
+ return EVP_CIPHER_CTX_set_params(ctx, params) == 1;
+}
+} // namespace
+#endif
+
+bool CipherCtxPointer::setCtsMode(const char* mode) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ return SetCipherCtxStringParam(ctx_.get(), OSSL_CIPHER_PARAM_CTS_MODE, mode);
+#else
+ static_cast(mode);
+ return false;
+#endif
+}
+
bool CipherCtxPointer::setPadding(bool padding) {
if (!ctx_) return false;
return EVP_CIPHER_CTX_set_padding(ctx_.get(), padding);
}
+bool CipherCtxPointer::setXtsStandard(const char* standard) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ return SetCipherCtxStringParam(ctx_.get(), kCipherParamXtsStandard, standard);
+#else
+ static_cast(standard);
+ return false;
+#endif
+}
+
int CipherCtxPointer::getBlockSize() const {
if (!ctx_) return 0;
return EVP_CIPHER_CTX_block_size(ctx_.get());
@@ -4758,6 +5142,16 @@ bool CipherCtxPointer::isCcmMode() const {
return getMode() == EVP_CIPH_CCM_MODE;
}
+bool CipherCtxPointer::isCtsMode() const {
+ if (!ctx_) return false;
+ return Cipher::FromCtx(*this).isCtsMode();
+}
+
+bool CipherCtxPointer::isXtsMode() const {
+ if (!ctx_) return false;
+ return getMode() == EVP_CIPH_XTS_MODE;
+}
+
bool CipherCtxPointer::isWrapMode() const {
if (!ctx_) return false;
return getMode() == EVP_CIPH_WRAP_MODE;
@@ -5779,6 +6173,11 @@ DataPointer CipherImpl(const EVPKeyPointer& key,
}
} // namespace
+Rsa::OtherPrimeInfoPointer::OtherPrimeInfoPointer(BignumPointer&& r,
+ BignumPointer&& d,
+ BignumPointer&& t)
+ : r(r.release()), d(d.release()), t(t.release()) {}
+
#if NCRYPTO_USE_OPENSSL3_PROVIDER
namespace {
int DigestAlgorithmIdentifierToNid(const unsigned char* data, size_t size) {
@@ -6007,6 +6406,19 @@ Rsa::Rsa(const EVP_PKEY* pkey) : Rsa() {
return;
}
+ for (const auto& names : kRsaOtherPrimeParamNames) {
+ OtherPrimeInfoPointer info;
+ if (!GetOptionalPKeyBnParam(pkey, names.factor, &info.r) ||
+ !GetOptionalPKeyBnParam(pkey, names.exponent, &info.d) ||
+ !GetOptionalPKeyBnParam(pkey, names.coefficient, &info.t)) {
+ return;
+ }
+
+ if (!info.r && !info.d && !info.t) break;
+ if (!info.r || !info.d || !info.t) return;
+ other_prime_infos_.push_back(std::move(info));
+ }
+
if (type == EVP_PKEY_RSA_PSS) {
MarkPopErrorOnReturn pop_errors;
PssParams params;
@@ -6045,6 +6457,35 @@ const Rsa::PrivateKey Rsa::getPrivateKey() const {
#endif
}
+const Rsa::OtherPrimeInfos Rsa::getOtherPrimeInfos() const {
+ OtherPrimeInfos infos;
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ infos.reserve(other_prime_infos_.size());
+ for (const auto& info : other_prime_infos_) {
+ infos.push_back({info.r.get(), info.d.get(), info.t.get()});
+ }
+#elif NCRYPTO_USE_LEGACY_OPENSSL
+ if (rsa_ == nullptr) return infos;
+ const int count = RSA_get_multi_prime_extra_count(rsa_);
+ if (count <= 0) return infos;
+
+ std::vector factors(count);
+ std::vector exponents(count);
+ std::vector coefficients(count);
+ if (RSA_get0_multi_prime_factors(rsa_, factors.data()) != 1 ||
+ RSA_get0_multi_prime_crt_params(
+ rsa_, exponents.data(), coefficients.data()) != 1) {
+ return {};
+ }
+
+ infos.reserve(count);
+ for (int i = 0; i < count; i++) {
+ infos.push_back({factors[i], exponents[i], coefficients[i]});
+ }
+#endif
+ return infos;
+}
+
const std::optional Rsa::getPssParams() const {
#if NCRYPTO_USE_OPENSSL3_PROVIDER
return pss_params_;
@@ -6146,15 +6587,20 @@ bool Rsa::setPrivateKey(BignumPointer&& d,
BignumPointer&& p,
BignumPointer&& dp,
BignumPointer&& dq,
- BignumPointer&& qi) {
+ BignumPointer&& qi,
+ OtherPrimeInfoPointers&& other_prime_infos) {
#if NCRYPTO_USE_OPENSSL3_PROVIDER
if (!d || !q || !p || !dp || !dq || !qi) return false;
+ for (const auto& info : other_prime_infos) {
+ if (!info.r || !info.d || !info.t) return false;
+ }
d_.reset(d.release());
q_.reset(q.release());
p_.reset(p.release());
dp_.reset(dp.release());
dq_.reset(dq.release());
qi_.reset(qi.release());
+ other_prime_infos_ = std::move(other_prime_infos);
rsa_ = n_ != nullptr && e_ != nullptr;
return rsa_;
#else
@@ -6176,6 +6622,37 @@ bool Rsa::setPrivateKey(BignumPointer&& d,
dp.release();
dq.release();
qi.release();
+
+#if NCRYPTO_USE_LEGACY_OPENSSL
+ if (!other_prime_infos.empty()) {
+ std::vector factors;
+ std::vector exponents;
+ std::vector coefficients;
+ factors.reserve(other_prime_infos.size());
+ exponents.reserve(other_prime_infos.size());
+ coefficients.reserve(other_prime_infos.size());
+ for (const auto& info : other_prime_infos) {
+ if (!info.r || !info.d || !info.t) return false;
+ factors.push_back(info.r.get());
+ exponents.push_back(info.d.get());
+ coefficients.push_back(info.t.get());
+ }
+ if (RSA_set0_multi_prime_params(const_cast(rsa_),
+ factors.data(),
+ exponents.data(),
+ coefficients.data(),
+ static_cast(factors.size())) != 1) {
+ return false;
+ }
+ for (auto& info : other_prime_infos) {
+ info.r.release();
+ info.d.release();
+ info.t.release();
+ }
+ }
+#else
+ if (!other_prime_infos.empty()) return false;
+#endif
return true;
#endif
}
@@ -6229,23 +6706,7 @@ struct CipherCallbackContext {
void operator()(const char* name) { cb(name); }
};
-#if OPENSSL_WITH_AES_SIV
-constexpr const char* kProviderOnlyAesSivCiphers[] = {
- "aes-128-siv",
- "aes-192-siv",
- "aes-256-siv",
-};
-#endif
-
-#if OPENSSL_WITH_AES_GCM_SIV
-constexpr const char* kProviderOnlyAesGcmSivCiphers[] = {
- "aes-128-gcm-siv",
- "aes-192-gcm-siv",
- "aes-256-gcm-siv",
-};
-#endif
-
-#if OPENSSL_VERSION_MAJOR >= 3
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
template fetched(
+ fetch_type(nullptr, real_name, nullptr));
+ if (!IsSupportedFetchedCipher(fetched.get())) return;
- free_type(fetched);
auto& cb = *(static_cast(arg));
cb(from);
}
+
+void array_push_back_provider_name(const char* name, void* arg) {
+ if (name == nullptr) return;
+
+ const std::string_view name_view(name);
+ const bool is_dotted_decimal =
+ name_view.find('.') != std::string_view::npos &&
+ std::all_of(name_view.begin(), name_view.end(), [](unsigned char c) {
+ return (c >= '0' && c <= '9') || c == '.';
+ });
+ if (is_dotted_decimal) return;
+
+ std::string normalized_name(name_view);
+ std::transform(normalized_name.begin(),
+ normalized_name.end(),
+ normalized_name.begin(),
+ [](unsigned char c) {
+ if (c >= 'A' && c <= 'Z') {
+ return static_cast(c + ('a' - 'A'));
+ }
+ return static_cast(c);
+ });
+ auto& cb = *(static_cast(arg));
+ cb(normalized_name.c_str());
+}
+
+void array_push_back_provider(EVP_CIPHER* cipher, void* arg) {
+ const char* name = EVP_CIPHER_get0_name(cipher);
+ if (name == nullptr) return;
+
+ DeleteFnPtr fetched(
+ EVP_CIPHER_fetch(nullptr, name, nullptr));
+ if (!IsSupportedFetchedCipher(fetched.get())) return;
+
+ EVP_CIPHER_names_do_all(fetched.get(), array_push_back_provider_name, arg);
+}
#else
template
void array_push_back(const TypeName* evp_ref,
@@ -6301,7 +6798,7 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) {
}
#else
EVP_CIPHER_do_all_sorted(
-#if OPENSSL_VERSION_MAJOR >= 3
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
array_push_back,
#endif
&context);
-#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
- auto maybe_push_provider_only_cipher = [&](const char* name) {
- EVP_CIPHER* cipher = EVP_CIPHER_fetch(nullptr, name, nullptr);
- if (cipher == nullptr) return;
- EVP_CIPHER_free(cipher);
- context.cb(name);
- };
-#endif
-#if OPENSSL_WITH_AES_SIV
- for (const char* name : kProviderOnlyAesSivCiphers) {
- maybe_push_provider_only_cipher(name);
- }
-#endif
-#if OPENSSL_WITH_AES_GCM_SIV
- for (const char* name : kProviderOnlyAesGcmSivCiphers) {
- maybe_push_provider_only_cipher(name);
- }
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ EVP_CIPHER_do_all_provided(nullptr, array_push_back_provider, &context);
#endif
#endif
}
@@ -6479,11 +6961,19 @@ EVP_MD_CTX* EVPMDCtxPointer::release() {
return ctx_.release();
}
-bool EVPMDCtxPointer::digestInit(const Digest& digest) {
+bool EVPMDCtxPointer::digestInit(const EVP_MD* digest) {
if (!ctx_) return false;
return EVP_DigestInit_ex(ctx_.get(), digest, nullptr) > 0;
}
+#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 0)
+bool EVPMDCtxPointer::digestInit(const EVP_MD* digest,
+ const OSSL_PARAM* params) {
+ if (!ctx_) return false;
+ return EVP_DigestInit_ex2(ctx_.get(), digest, params) > 0;
+}
+#endif
+
bool EVPMDCtxPointer::digestUpdate(const Buffer& in) {
if (!ctx_) return false;
return EVP_DigestUpdate(ctx_.get(), in.data, in.len) > 0;
@@ -6832,6 +7322,79 @@ EVPMacPointer EVPMacPointer::Fetch(const char* algorithm) {
return EVPMacPointer(EVP_MAC_fetch(nullptr, algorithm, nullptr));
}
+MacKind MacCache::GetKind(EVP_MAC* mac) {
+ if (EVP_MAC_is_a(mac, OSSL_MAC_NAME_HMAC)) return MacKind::kHmac;
+ if (EVP_MAC_is_a(mac, OSSL_MAC_NAME_CMAC)) return MacKind::kCmac;
+ if (EVP_MAC_is_a(mac, OSSL_MAC_NAME_GMAC)) return MacKind::kGmac;
+ return MacKind::kOther;
+}
+
+MacCache::Result MacCache::lookup(const char* name, uint64_t generation) const {
+ if (generation_ != generation || name == nullptr) return {};
+ const auto it = aliases_.find(name);
+ if (it == aliases_.end()) return {};
+ return lookup(it->second, generation);
+}
+
+MacCache::Result MacCache::insert(const char* name,
+ EVPMacPointer&& mac,
+ uint64_t generation) {
+ if (generation_ != generation || generation != getFipsStateGeneration() ||
+ name == nullptr || mac == nullptr) {
+ return {};
+ }
+
+ const char* canonical_name = EVP_MAC_get0_name(mac.get());
+ const OSSL_PROVIDER* provider = EVP_MAC_get0_provider(mac.get());
+ if (canonical_name == nullptr || provider == nullptr) return {};
+
+ for (size_t index = 0; index < macs_.size(); index++) {
+ EVP_MAC* cached = macs_[index].mac.get();
+ if (cached == nullptr) continue;
+ const char* cached_name = EVP_MAC_get0_name(cached);
+ if (EVP_MAC_get0_provider(cached) == provider && cached_name != nullptr &&
+ CaseInsensitiveNameEqual()(cached_name, canonical_name)) {
+ if (generation != getFipsStateGeneration()) return {};
+ const int32_t id = static_cast(first_id_ + index);
+ aliases_.insert_or_assign(name, id);
+ return {cached, id, macs_[index].kind};
+ }
+ }
+
+ if (next_id_ == UINT32_MAX) return {};
+
+ std::vector aliases;
+ {
+ MarkPopErrorOnReturn mark_pop_error_on_return;
+ if (EVP_MAC_names_do_all(mac.get(), PushAlgorithmAlias, &aliases) != 1) {
+ return {};
+ }
+ }
+ if (generation != getFipsStateGeneration()) return {};
+
+ const MacKind kind = GetKind(mac.get());
+ macs_.push_back({std::move(mac), kind});
+ const int32_t id = static_cast(next_id_++);
+ const size_t index = macs_.size() - 1;
+
+ for (const std::string& alias : aliases) aliases_.emplace(alias, id);
+ aliases_.insert_or_assign(name, id);
+
+ return {macs_[index].mac.get(), id, kind};
+}
+
+void MacCache::reset(uint64_t generation) {
+ if (generation_ == generation) return;
+ aliases_.clear();
+ macs_.clear();
+ first_id_ = next_id_;
+ generation_ = generation;
+}
+
+const MacCache::AliasMap& MacCache::aliases() const {
+ return aliases_;
+}
+
EVPMacCtxPointer::EVPMacCtxPointer(EVP_MAC_CTX* ctx) : ctx_(ctx) {}
EVPMacCtxPointer::EVPMacCtxPointer(EVPMacCtxPointer&& other) noexcept
@@ -6859,22 +7422,42 @@ EVP_MAC_CTX* EVPMacCtxPointer::release() {
bool EVPMacCtxPointer::init(const Buffer& key,
const OSSL_PARAM* params) {
if (!ctx_) return false;
- return EVP_MAC_init(ctx_.get(),
- static_cast(key.data),
- key.len,
- params) == 1;
+
+ static constexpr unsigned char kEmptyKey = 0;
+ const unsigned char* key_data = static_cast(key.data);
+ if (key_data == nullptr) {
+ if (key.len != 0) return false;
+ key_data = &kEmptyKey;
+ }
+
+ return EVP_MAC_init(ctx_.get(), key_data, key.len, params) == 1;
}
bool EVPMacCtxPointer::update(const Buffer& data) {
if (!ctx_) return false;
+ if (data.len == 0) return true;
+ if (data.data == nullptr) return false;
return EVP_MAC_update(ctx_.get(),
static_cast(data.data),
data.len) == 1;
}
+size_t EVPMacCtxPointer::getSize() const {
+ return ctx_ ? EVP_MAC_CTX_get_mac_size(ctx_.get()) : 0;
+}
+
+const OSSL_PARAM* EVPMacCtxPointer::getSettableParams() const {
+ return ctx_ ? EVP_MAC_CTX_settable_params(ctx_.get()) : nullptr;
+}
+
DataPointer EVPMacCtxPointer::final(size_t length) {
if (!ctx_) return {};
- auto buf = DataPointer::Alloc(length);
+
+ // DataPointer uses a null allocation to represent failure. Retain a
+ // one-byte allocation for a successful zero-length result while passing the
+ // requested zero capacity to OpenSSL. A non-null output pointer is required
+ // to actually finalize; nullptr only queries the output length.
+ auto buf = DataPointer::Alloc(length == 0 ? 1 : length);
if (!buf) return {};
size_t result_len = length;
@@ -6884,8 +7467,9 @@ DataPointer EVPMacCtxPointer::final(size_t length) {
length) != 1) {
return {};
}
+ if (result_len > length) return {};
- return buf;
+ return buf.resize(result_len);
}
EVPMacCtxPointer EVPMacCtxPointer::New(EVP_MAC* mac) {
@@ -7009,7 +7593,10 @@ DataPointer xofHashDigest(const Buffer& buf,
if (ctx.digestInit(md) != 1) {
return {};
}
- if (ctx.digestUpdate(reinterpret_cast&>(buf)) != 1) {
+ if (ctx.digestUpdate(Buffer{
+ .data = buf.data,
+ .len = buf.len,
+ }) != 1) {
return {};
}
return ctx.digestFinal(output_length);
@@ -7145,14 +7732,86 @@ size_t Digest::size() const {
return EVP_MD_size(md_);
}
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+Digest::Digest(DeleteFnPtr md)
+ : md_(md.get()), fetched_md_(std::move(md)) {}
+#endif
+
+Digest::Digest(const Digest& other) : md_(other.md_) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ if (other.fetched_md_ != nullptr) {
+ if (EVP_MD_up_ref(other.fetched_md_.get()) == 1) {
+ fetched_md_.reset(other.fetched_md_.get());
+ } else {
+ md_ = nullptr;
+ }
+ }
+#endif
+}
+
+Digest& Digest::operator=(const Digest& other) {
+ if (this == &other) return *this;
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ if (other.fetched_md_ != nullptr) {
+ if (EVP_MD_up_ref(other.fetched_md_.get()) == 1) {
+ fetched_md_.reset(other.fetched_md_.get());
+ } else {
+ fetched_md_.reset();
+ md_ = nullptr;
+ return *this;
+ }
+ } else {
+ fetched_md_.reset();
+ }
+#endif
+ md_ = other.md_;
+ return *this;
+}
+
const Digest Digest::MD5 = Digest(EVP_md5());
const Digest Digest::SHA1 = Digest(EVP_sha1());
const Digest Digest::SHA256 = Digest(EVP_sha256());
const Digest Digest::SHA384 = Digest(EVP_sha384());
const Digest Digest::SHA512 = Digest(EVP_sha512());
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+namespace {
+bool IsSupportedDigest(const EVP_MD* md) {
+ if (md == nullptr || EVP_MD_is_a(md, "NULL")) return false;
+
+ // OpenSSL currently crashes when ML-DSA-MU finalizes an empty input. Keep it
+ // unavailable until the provider implementation is fixed.
+ // https://github.com/openssl/openssl/issues/32445
+ if (EVP_MD_is_a(md, "ML-DSA-MU")) return false;
+
+ return true;
+}
+} // namespace
+#endif
+
const Digest Digest::FromName(const char* name) {
- return ncrypto::getDigestByName(name);
+ const EVP_MD* md = ncrypto::getDigestByName(name);
+ if (md != nullptr) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ if (md == EVP_md_null()) return Digest();
+#endif
+ return Digest(md);
+ }
+
+ return Fetch(name);
+}
+
+const Digest Digest::Fetch(const char* name) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ MarkPopErrorOnReturn mark_pop_error_on_return;
+ DeleteFnPtr fetched(
+ EVP_MD_fetch(nullptr, name, nullptr));
+ if (IsSupportedDigest(fetched.get())) {
+ return Digest(std::move(fetched));
+ }
+#endif
+
+ return Digest();
}
// ============================================================================
diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h
index 58e32cc18fc7..6b1edceed061 100644
--- a/deps/ncrypto/ncrypto.h
+++ b/deps/ncrypto/ncrypto.h
@@ -13,12 +13,15 @@
#include
#include
#include
+#include
#include
#include
#include
#include
#include
#include
+#include
+#include
#if defined(NCRYPTO_ENGINE_COMPAT) && NCRYPTO_ENGINE_COMPAT && \
!defined(OPENSSL_NO_ENGINE)
#include
@@ -363,6 +366,7 @@ class DataPointer;
class DHPointer;
class ECKeyPointer;
class EVPKeyPointer;
+class MacCache;
class EVPMacCtxPointer;
class EVPMacPointer;
class EVPMDCtxPointer;
@@ -397,9 +401,12 @@ class Digest final {
static constexpr size_t MAX_SIZE = EVP_MAX_MD_SIZE;
Digest() = default;
Digest(const EVP_MD* md) : md_(md) {}
- Digest(const Digest&) = default;
- Digest& operator=(const Digest&) = default;
+ Digest(const Digest& other);
+ Digest& operator=(const Digest& other);
inline Digest& operator=(const EVP_MD* md) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ fetched_md_.reset();
+#endif
md_ = md;
return *this;
}
@@ -418,9 +425,72 @@ class Digest final {
static const Digest SHA512;
static const Digest FromName(const char* name);
+ static const Digest Fetch(const char* name);
private:
const EVP_MD* md_ = nullptr;
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ explicit Digest(DeleteFnPtr md);
+ DeleteFnPtr fetched_md_;
+#endif
+};
+
+struct CaseInsensitiveNameHash {
+ using is_transparent = void;
+ size_t operator()(std::string_view name) const noexcept;
+};
+
+struct CaseInsensitiveNameEqual {
+ using is_transparent = void;
+ bool operator()(std::string_view lhs, std::string_view rhs) const noexcept;
+};
+
+class DigestCache final {
+ public:
+ struct Result {
+ const EVP_MD* digest = nullptr;
+ int32_t id = -1;
+ };
+
+ using AliasMap = std::unordered_map;
+
+ DigestCache() = default;
+ NCRYPTO_DISALLOW_COPY_AND_MOVE(DigestCache)
+
+ Result lookup(const char* name, uint64_t generation) const;
+ inline Result lookup(int32_t id, uint64_t generation) const {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ if (generation_ != generation || id == -1) return {};
+ const uint32_t unsigned_id = static_cast(id);
+ if (unsigned_id < first_id_) return {};
+ const size_t index = unsigned_id - first_id_;
+ if (index >= digests_.size()) return {};
+ return {digests_[index].get(), id};
+#else
+ static_cast(id);
+ static_cast(generation);
+ return {};
+#endif
+ }
+ Result insert(const char* name, const EVP_MD* digest, uint64_t generation);
+ void reset(uint64_t generation);
+ const AliasMap& aliases() const;
+
+ private:
+ uint64_t generation_ = 0;
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ using EVPMDPointer = DeleteFnPtr;
+
+ // IDs are not reused across generations because JavaScript caches them
+ // independently in each Realm.
+ uint32_t first_id_ = 0;
+ uint32_t next_id_ = 0;
+ std::vector digests_;
+ AliasMap aliases_;
+#endif
};
// Computes a fixed-length digest.
@@ -431,6 +501,32 @@ DataPointer xofHashDigest(const Buffer& data,
const EVP_MD* md,
size_t length);
+class CipherCache final {
+ public:
+ CipherCache() = default;
+ NCRYPTO_DISALLOW_COPY_AND_MOVE(CipherCache)
+
+ const EVP_CIPHER* lookup(const char* name, uint64_t generation);
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ const EVP_CIPHER* insert(const char* name,
+ DeleteFnPtr&& cipher,
+ uint64_t generation);
+#endif
+
+ private:
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ using EVPCipherPointer = DeleteFnPtr;
+
+ uint64_t generation_ = 0;
+ std::vector ciphers_;
+ std::unordered_map
+ aliases_;
+#endif
+};
+
class Cipher final {
public:
static constexpr size_t MAX_KEY_LENGTH = EVP_MAX_KEY_LENGTH;
@@ -452,7 +548,7 @@ class Cipher final {
Cipher(const Cipher& other);
Cipher& operator=(const Cipher& other);
inline Cipher& operator=(const EVP_CIPHER* cipher) {
-#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
fetched_cipher_.reset();
#endif
cipher_ = cipher;
@@ -476,6 +572,7 @@ class Cipher final {
bool isWrapMode() const;
bool isCtrMode() const;
bool isCcmMode() const;
+ bool isCtsMode() const;
bool isOcbMode() const;
bool isSivMode() const;
bool isGcmSivMode() const;
@@ -489,8 +586,8 @@ class Cipher final {
unsigned char* key,
unsigned char* iv) const;
- static const Cipher FromName(const char* name);
- static const Cipher FromNid(int nid);
+ static const Cipher FromName(const char* name, CipherCache* cache = nullptr);
+ static const Cipher FromNid(int nid, CipherCache* cache = nullptr);
static const Cipher FromCtx(const CipherCtxPointer& ctx);
using CipherNameCallback = std::function;
@@ -499,28 +596,24 @@ class Cipher final {
// is able to do so.
static void ForEach(CipherNameCallback callback);
- // Utilities to get various ciphers by type. If the underlying
- // implementation does not support the requested cipher, then
- // the result will be an empty Cipher object whose bool operator
- // will return false.
-
- static const Cipher EMPTY;
- static const Cipher AES_128_CBC;
- static const Cipher AES_192_CBC;
- static const Cipher AES_256_CBC;
- static const Cipher AES_128_CTR;
- static const Cipher AES_192_CTR;
- static const Cipher AES_256_CTR;
- static const Cipher AES_128_GCM;
- static const Cipher AES_192_GCM;
- static const Cipher AES_256_GCM;
- static const Cipher AES_128_KW;
- static const Cipher AES_192_KW;
- static const Cipher AES_256_KW;
- static const Cipher AES_128_OCB;
- static const Cipher AES_192_OCB;
- static const Cipher AES_256_OCB;
- static const Cipher CHACHA20_POLY1305;
+ // Lazily resolves common ciphers. If the underlying implementation does not
+ // support the requested cipher, the returned Cipher will be empty.
+ static const Cipher& AES_128_CBC();
+ static const Cipher& AES_192_CBC();
+ static const Cipher& AES_256_CBC();
+ static const Cipher& AES_128_CTR();
+ static const Cipher& AES_192_CTR();
+ static const Cipher& AES_256_CTR();
+ static const Cipher& AES_128_GCM();
+ static const Cipher& AES_192_GCM();
+ static const Cipher& AES_256_GCM();
+ static const Cipher& AES_128_KW();
+ static const Cipher& AES_192_KW();
+ static const Cipher& AES_256_KW();
+ static const Cipher& AES_128_OCB();
+ static const Cipher& AES_192_OCB();
+ static const Cipher& AES_256_OCB();
+ static const Cipher& CHACHA20_POLY1305();
struct CipherParams {
int padding;
@@ -550,7 +643,7 @@ class Cipher final {
private:
const EVP_CIPHER* cipher_ = nullptr;
-#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
explicit Cipher(DeleteFnPtr cipher);
DeleteFnPtr fetched_cipher_;
#endif
@@ -631,6 +724,23 @@ class Rsa final {
const BIGNUM* dq;
const BIGNUM* qi;
};
+ struct OtherPrimeInfo {
+ const BIGNUM* r;
+ const BIGNUM* d;
+ const BIGNUM* t;
+ };
+ struct OtherPrimeInfoPointer {
+ OtherPrimeInfoPointer() = default;
+ OtherPrimeInfoPointer(BignumPointer&& r,
+ BignumPointer&& d,
+ BignumPointer&& t);
+
+ DeleteFnPtr r;
+ DeleteFnPtr d;
+ DeleteFnPtr t;
+ };
+ using OtherPrimeInfos = std::vector;
+ using OtherPrimeInfoPointers = std::vector;
struct PssParams {
std::string_view digest = "sha1";
std::optional mgf1_digest = "sha1";
@@ -639,6 +749,7 @@ class Rsa final {
const PublicKey getPublicKey() const;
const PrivateKey getPrivateKey() const;
+ const OtherPrimeInfos getOtherPrimeInfos() const;
const std::optional getPssParams() const;
bool setPublicKey(BignumPointer&& n, BignumPointer&& e);
@@ -647,7 +758,8 @@ class Rsa final {
BignumPointer&& p,
BignumPointer&& dp,
BignumPointer&& dq,
- BignumPointer&& qi);
+ BignumPointer&& qi,
+ OtherPrimeInfoPointers&& other_prime_infos = {});
using CipherParams = Cipher::CipherParams;
@@ -672,6 +784,7 @@ class Rsa final {
DeleteFnPtr dp_;
DeleteFnPtr dq_;
DeleteFnPtr qi_;
+ OtherPrimeInfoPointers other_prime_infos_;
std::optional pss_params_;
#else
OSSL3_CONST RSA* rsa_;
@@ -940,7 +1053,9 @@ class CipherCtxPointer final {
bool setIvLength(size_t length);
bool setAeadTag(const Buffer& tag);
bool setAeadTagLength(size_t length);
+ bool setCtsMode(const char* mode);
bool setPadding(bool padding);
+ bool setXtsStandard(const char* standard);
bool init(const Cipher& cipher,
bool encrypt,
const unsigned char* key = nullptr,
@@ -953,6 +1068,8 @@ class CipherCtxPointer final {
bool isGcmMode() const;
bool isOcbMode() const;
bool isCcmMode() const;
+ bool isCtsMode() const;
+ bool isXtsMode() const;
bool isWrapMode() const;
bool isSivMode() const;
bool isGcmSivMode() const;
@@ -1690,7 +1807,16 @@ class EVPMDCtxPointer final {
void reset(EVP_MD_CTX* ctx = nullptr);
EVP_MD_CTX* release();
- bool digestInit(const Digest& digest);
+ bool digestInit(const EVP_MD* digest);
+ inline bool digestInit(const Digest& digest) {
+ return digestInit(digest.get());
+ }
+#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 0)
+ bool digestInit(const EVP_MD* digest, const OSSL_PARAM* params);
+ inline bool digestInit(const Digest& digest, const OSSL_PARAM* params) {
+ return digestInit(digest.get(), params);
+ }
+#endif
bool digestUpdate(const Buffer& in);
DataPointer digestFinal(size_t length);
bool digestFinalInto(Buffer* buf);
@@ -1782,6 +1908,61 @@ class EVPMacPointer final {
DeleteFnPtr mac_;
};
+enum class MacKind : uint8_t {
+ kOther,
+ kHmac,
+ kCmac,
+ kGmac,
+};
+
+class MacCache final {
+ public:
+ struct Result {
+ // Borrowed from the cache and valid until the cache is reset. Creating an
+ // EVP_MAC_CTX takes an independent reference to the method.
+ EVP_MAC* mac = nullptr;
+ int32_t id = -1;
+ MacKind kind = MacKind::kOther;
+ };
+
+ using AliasMap = std::unordered_map;
+
+ MacCache() = default;
+ NCRYPTO_DISALLOW_COPY_AND_MOVE(MacCache)
+
+ Result lookup(const char* name, uint64_t generation) const;
+ inline Result lookup(int32_t id, uint64_t generation) const {
+ if (generation_ != generation || id == -1) return {};
+ const uint32_t unsigned_id = static_cast(id);
+ if (unsigned_id < first_id_) return {};
+ const size_t index = unsigned_id - first_id_;
+ if (index >= macs_.size()) return {};
+ return {macs_[index].mac.get(), id, macs_[index].kind};
+ }
+ Result insert(const char* name, EVPMacPointer&& mac, uint64_t generation);
+ void reset(uint64_t generation);
+ const AliasMap& aliases() const;
+ static MacKind GetKind(EVP_MAC* mac);
+
+ private:
+ struct Entry {
+ EVPMacPointer mac;
+ MacKind kind;
+ };
+
+ uint64_t generation_ = 0;
+
+ // IDs are not reused across generations because JavaScript may cache them
+ // independently in each Realm.
+ uint32_t first_id_ = 0;
+ uint32_t next_id_ = 0;
+ std::vector macs_;
+ AliasMap aliases_;
+};
+
class EVPMacCtxPointer final {
public:
EVPMacCtxPointer() = default;
@@ -1800,6 +1981,8 @@ class EVPMacCtxPointer final {
bool init(const Buffer& key, const OSSL_PARAM* params = nullptr);
bool update(const Buffer& data);
+ size_t getSize() const;
+ const OSSL_PARAM* getSettableParams() const;
DataPointer final(size_t length);
static EVPMacCtxPointer New(EVP_MAC* mac);
@@ -1836,6 +2019,14 @@ class HMACCtxPointer final {
};
#endif // OPENSSL_WITH_EVP_MAC
+#if !OPENSSL_WITH_EVP_MAC
+class MacCache final {
+ public:
+ MacCache() = default;
+ NCRYPTO_DISALLOW_COPY_AND_MOVE(MacCache)
+};
+#endif
+
#ifndef OPENSSL_NO_ENGINE
class EnginePointer final {
public:
@@ -1880,6 +2071,8 @@ bool isFipsEnabled();
bool setFipsEnabled(bool enabled, CryptoErrorList* errors);
+uint64_t getFipsStateGeneration();
+
bool testFipsEnabled();
// ============================================================================
diff --git a/deps/nghttp2/unofficial.gni b/deps/nghttp2/unofficial.gni
index 4558fbbf5e5f..d37754f59124 100644
--- a/deps/nghttp2/unofficial.gni
+++ b/deps/nghttp2/unofficial.gni
@@ -4,40 +4,53 @@
# The actual configurations are put inside a template in unofficial.gni to
# prevent accidental edits from contributors.
+import("../../node.gni")
+
template("nghttp2_gn_build") {
- config("nghttp2_config") {
- include_dirs = [ "lib/includes" ]
- if (!is_component_build) {
- defines = [ "NGHTTP2_STATICLIB" ]
+ if (node_shared_nghttp2) {
+ import("//build/config/linux/pkg_config.gni")
+ pkg_config("nghttp2_config") {
+ packages = [ "libnghttp2" ]
+ }
+ group(target_name) {
+ forward_variables_from(invoker, "*")
+ public_configs = [ ":nghttp2_config" ]
+ }
+ } else {
+ config("nghttp2_config") {
+ include_dirs = [ "lib/includes" ]
+ if (!is_component_build) {
+ defines = [ "NGHTTP2_STATICLIB" ]
+ }
}
- }
- gypi_values = exec_script("../../tools/gypi_to_gn.py",
- [ rebase_path("nghttp2.gyp") ],
- "scope",
- [ "nghttp2.gyp" ])
+ gypi_values = exec_script("../../tools/gypi_to_gn.py",
+ [ rebase_path("nghttp2.gyp") ],
+ "scope",
+ [ "nghttp2.gyp" ])
- component(target_name) {
- forward_variables_from(invoker, "*")
+ component(target_name) {
+ forward_variables_from(invoker, "*")
- public_configs = [ ":nghttp2_config" ]
- defines = [
- "_U_",
- "HAVE_CONFIG_H"
- ]
- if (is_component_build) {
- defines += [ "BUILDING_NGHTTP2" ]
- }
+ public_configs = [ ":nghttp2_config" ]
+ defines = [
+ "_U_",
+ "HAVE_CONFIG_H"
+ ]
+ if (is_component_build) {
+ defines += [ "BUILDING_NGHTTP2" ]
+ }
- sources = gypi_values.nghttp2_sources
+ sources = gypi_values.nghttp2_sources
- if (is_clang || !is_win) {
- cflags_c = [
- "-Wno-implicit-fallthrough",
- # Ref https://github.com/nghttp2/nghttp2/pull/2258
- # This can be removed when the above PR is ingested.
- "-Wno-extra-semi",
- ]
+ if (is_clang || !is_win) {
+ cflags_c = [
+ "-Wno-implicit-fallthrough",
+ # Ref https://github.com/nghttp2/nghttp2/pull/2258
+ # This can be removed when the above PR is ingested.
+ "-Wno-extra-semi",
+ ]
+ }
}
}
}
diff --git a/deps/simdjson/simdjson.cpp b/deps/simdjson/simdjson.cpp
index 92d971fd448b..71f443b3d2bb 100644
--- a/deps/simdjson/simdjson.cpp
+++ b/deps/simdjson/simdjson.cpp
@@ -1,4 +1,4 @@
-/* auto-generated on 2026-08-24 17:10:01 -0400. version 4.6.9 Do not edit! */
+/* auto-generated on 2026-09-04 16:04:31 -0400. version 4.6.11 Do not edit! */
/* including simdjson.cpp: */
/* begin file simdjson.cpp */
#define SIMDJSON_SRC_SIMDJSON_CPP
@@ -240,7 +240,7 @@ using std::size_t;
#endif
#elif defined(__PPC64__) || defined(_M_PPC64)
#define SIMDJSON_IS_PPC64 1
-#if defined(__ALTIVEC__)
+#if defined(__ALTIVEC__) && defined(__POWER8_VECTOR__)
#define SIMDJSON_IS_PPC64_VMX 1
#endif // defined(__ALTIVEC__)
#else
@@ -10052,9 +10052,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -10065,6 +10070,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -13541,7 +13547,14 @@ namespace stage1 {
* complete document, therefore the last json buffer location is the end of the
* batch.
*/
-simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser) {
+simdjson_inline bool ends_with_partial_scalar(dom_parser_implementation &parser, size_t len) {
+ const uint8_t f = parser.buf[parser.structural_indexes[parser.n_structural_indexes - 1]];
+ const uint8_t e = parser.buf[len - 1];
+ return f != '{' && f != '[' && f != '}' && f != ']' && f != ':' && f != ',' && f != '"' &&
+ e != ' ' && e != '\t' && e != '\n' && e != '\r';
+}
+
+simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser, bool defer_last = false) {
// Variant: do not count separately, just figure out depth
if(parser.n_structural_indexes == 0) { return 0; }
auto arr_cnt = 0;
@@ -13575,7 +13588,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
// Last document is complete, so the next document will appear after!
if (!arr_cnt && !obj_cnt) {
- return parser.n_structural_indexes;
+ return defer_last ? i : parser.n_structural_indexes;
}
// Last document is incomplete; mark the document at i + 1 as the next one
return i;
@@ -13597,7 +13610,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
if (!arr_cnt && !obj_cnt) {
// We have a complete document.
- return parser.n_structural_indexes;
+ return defer_last ? 0 : parser.n_structural_indexes;
}
return 0;
}
@@ -13976,11 +13989,6 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
((error != SUCCESS) && (error != UNCLOSED_STRING)) // when partial we tolerate UNCLOSED_STRING
: (error != SUCCESS); // if partial is false, we must have SUCCESS
const bool have_unclosed_string = (error == UNCLOSED_STRING);
- if (simdjson_unlikely(should_we_exit)) { return error; }
-
- if (unescaped_chars_error) {
- return UNESCAPED_CHARS;
- }
parser.n_structural_indexes = uint32_t(indexer.tail - parser.structural_indexes.get());
/***
* The On-Demand API requires special padding.
@@ -14005,6 +14013,12 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
parser.structural_indexes[parser.n_structural_indexes + 1] = uint32_t(len);
parser.structural_indexes[parser.n_structural_indexes + 2] = 0;
parser.next_structural_index = 0;
+
+ // Bail out only once the count and sentinels above are set:
+ // document_stream::truncated_bytes() reads them even on error.
+ if (simdjson_unlikely(should_we_exit)) { return error; }
+ if (unescaped_chars_error) { return UNESCAPED_CHARS; }
+
// a valid JSON file cannot have zero structural indexes - we should have found something
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) {
return EMPTY;
@@ -14021,7 +14035,7 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) { return CAPACITY; }
}
// We truncate the input to the end of the last complete document (or zero).
- auto new_structural_indexes = find_next_document_index(parser);
+ auto new_structural_indexes = find_next_document_index(parser, !have_unclosed_string && ends_with_partial_scalar(parser, len));
if (new_structural_indexes == 0 && parser.n_structural_indexes > 0) {
if(parser.structural_indexes[0] == 0) {
// If the buffer is partial and we started at index 0 but the document is
@@ -16588,9 +16602,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -16601,6 +16620,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -19936,7 +19956,14 @@ namespace stage1 {
* complete document, therefore the last json buffer location is the end of the
* batch.
*/
-simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser) {
+simdjson_inline bool ends_with_partial_scalar(dom_parser_implementation &parser, size_t len) {
+ const uint8_t f = parser.buf[parser.structural_indexes[parser.n_structural_indexes - 1]];
+ const uint8_t e = parser.buf[len - 1];
+ return f != '{' && f != '[' && f != '}' && f != ']' && f != ':' && f != ',' && f != '"' &&
+ e != ' ' && e != '\t' && e != '\n' && e != '\r';
+}
+
+simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser, bool defer_last = false) {
// Variant: do not count separately, just figure out depth
if(parser.n_structural_indexes == 0) { return 0; }
auto arr_cnt = 0;
@@ -19970,7 +19997,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
// Last document is complete, so the next document will appear after!
if (!arr_cnt && !obj_cnt) {
- return parser.n_structural_indexes;
+ return defer_last ? i : parser.n_structural_indexes;
}
// Last document is incomplete; mark the document at i + 1 as the next one
return i;
@@ -19992,7 +20019,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
if (!arr_cnt && !obj_cnt) {
// We have a complete document.
- return parser.n_structural_indexes;
+ return defer_last ? 0 : parser.n_structural_indexes;
}
return 0;
}
@@ -20371,11 +20398,6 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
((error != SUCCESS) && (error != UNCLOSED_STRING)) // when partial we tolerate UNCLOSED_STRING
: (error != SUCCESS); // if partial is false, we must have SUCCESS
const bool have_unclosed_string = (error == UNCLOSED_STRING);
- if (simdjson_unlikely(should_we_exit)) { return error; }
-
- if (unescaped_chars_error) {
- return UNESCAPED_CHARS;
- }
parser.n_structural_indexes = uint32_t(indexer.tail - parser.structural_indexes.get());
/***
* The On-Demand API requires special padding.
@@ -20400,6 +20422,12 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
parser.structural_indexes[parser.n_structural_indexes + 1] = uint32_t(len);
parser.structural_indexes[parser.n_structural_indexes + 2] = 0;
parser.next_structural_index = 0;
+
+ // Bail out only once the count and sentinels above are set:
+ // document_stream::truncated_bytes() reads them even on error.
+ if (simdjson_unlikely(should_we_exit)) { return error; }
+ if (unescaped_chars_error) { return UNESCAPED_CHARS; }
+
// a valid JSON file cannot have zero structural indexes - we should have found something
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) {
return EMPTY;
@@ -20416,7 +20444,7 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) { return CAPACITY; }
}
// We truncate the input to the end of the last complete document (or zero).
- auto new_structural_indexes = find_next_document_index(parser);
+ auto new_structural_indexes = find_next_document_index(parser, !have_unclosed_string && ends_with_partial_scalar(parser, len));
if (new_structural_indexes == 0 && parser.n_structural_indexes > 0) {
if(parser.structural_indexes[0] == 0) {
// If the buffer is partial and we started at index 0 but the document is
@@ -22979,9 +23007,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -22992,6 +23025,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -26326,7 +26360,14 @@ namespace stage1 {
* complete document, therefore the last json buffer location is the end of the
* batch.
*/
-simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser) {
+simdjson_inline bool ends_with_partial_scalar(dom_parser_implementation &parser, size_t len) {
+ const uint8_t f = parser.buf[parser.structural_indexes[parser.n_structural_indexes - 1]];
+ const uint8_t e = parser.buf[len - 1];
+ return f != '{' && f != '[' && f != '}' && f != ']' && f != ':' && f != ',' && f != '"' &&
+ e != ' ' && e != '\t' && e != '\n' && e != '\r';
+}
+
+simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser, bool defer_last = false) {
// Variant: do not count separately, just figure out depth
if(parser.n_structural_indexes == 0) { return 0; }
auto arr_cnt = 0;
@@ -26360,7 +26401,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
// Last document is complete, so the next document will appear after!
if (!arr_cnt && !obj_cnt) {
- return parser.n_structural_indexes;
+ return defer_last ? i : parser.n_structural_indexes;
}
// Last document is incomplete; mark the document at i + 1 as the next one
return i;
@@ -26382,7 +26423,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
if (!arr_cnt && !obj_cnt) {
// We have a complete document.
- return parser.n_structural_indexes;
+ return defer_last ? 0 : parser.n_structural_indexes;
}
return 0;
}
@@ -26761,11 +26802,6 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
((error != SUCCESS) && (error != UNCLOSED_STRING)) // when partial we tolerate UNCLOSED_STRING
: (error != SUCCESS); // if partial is false, we must have SUCCESS
const bool have_unclosed_string = (error == UNCLOSED_STRING);
- if (simdjson_unlikely(should_we_exit)) { return error; }
-
- if (unescaped_chars_error) {
- return UNESCAPED_CHARS;
- }
parser.n_structural_indexes = uint32_t(indexer.tail - parser.structural_indexes.get());
/***
* The On-Demand API requires special padding.
@@ -26790,6 +26826,12 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
parser.structural_indexes[parser.n_structural_indexes + 1] = uint32_t(len);
parser.structural_indexes[parser.n_structural_indexes + 2] = 0;
parser.next_structural_index = 0;
+
+ // Bail out only once the count and sentinels above are set:
+ // document_stream::truncated_bytes() reads them even on error.
+ if (simdjson_unlikely(should_we_exit)) { return error; }
+ if (unescaped_chars_error) { return UNESCAPED_CHARS; }
+
// a valid JSON file cannot have zero structural indexes - we should have found something
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) {
return EMPTY;
@@ -26806,7 +26848,7 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) { return CAPACITY; }
}
// We truncate the input to the end of the last complete document (or zero).
- auto new_structural_indexes = find_next_document_index(parser);
+ auto new_structural_indexes = find_next_document_index(parser, !have_unclosed_string && ends_with_partial_scalar(parser, len));
if (new_structural_indexes == 0 && parser.n_structural_indexes > 0) {
if(parser.structural_indexes[0] == 0) {
// If the buffer is partial and we started at index 0 but the document is
@@ -29527,9 +29569,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -29540,6 +29587,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -32987,7 +33035,14 @@ namespace stage1 {
* complete document, therefore the last json buffer location is the end of the
* batch.
*/
-simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser) {
+simdjson_inline bool ends_with_partial_scalar(dom_parser_implementation &parser, size_t len) {
+ const uint8_t f = parser.buf[parser.structural_indexes[parser.n_structural_indexes - 1]];
+ const uint8_t e = parser.buf[len - 1];
+ return f != '{' && f != '[' && f != '}' && f != ']' && f != ':' && f != ',' && f != '"' &&
+ e != ' ' && e != '\t' && e != '\n' && e != '\r';
+}
+
+simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser, bool defer_last = false) {
// Variant: do not count separately, just figure out depth
if(parser.n_structural_indexes == 0) { return 0; }
auto arr_cnt = 0;
@@ -33021,7 +33076,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
// Last document is complete, so the next document will appear after!
if (!arr_cnt && !obj_cnt) {
- return parser.n_structural_indexes;
+ return defer_last ? i : parser.n_structural_indexes;
}
// Last document is incomplete; mark the document at i + 1 as the next one
return i;
@@ -33043,7 +33098,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
if (!arr_cnt && !obj_cnt) {
// We have a complete document.
- return parser.n_structural_indexes;
+ return defer_last ? 0 : parser.n_structural_indexes;
}
return 0;
}
@@ -33422,11 +33477,6 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
((error != SUCCESS) && (error != UNCLOSED_STRING)) // when partial we tolerate UNCLOSED_STRING
: (error != SUCCESS); // if partial is false, we must have SUCCESS
const bool have_unclosed_string = (error == UNCLOSED_STRING);
- if (simdjson_unlikely(should_we_exit)) { return error; }
-
- if (unescaped_chars_error) {
- return UNESCAPED_CHARS;
- }
parser.n_structural_indexes = uint32_t(indexer.tail - parser.structural_indexes.get());
/***
* The On-Demand API requires special padding.
@@ -33451,6 +33501,12 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
parser.structural_indexes[parser.n_structural_indexes + 1] = uint32_t(len);
parser.structural_indexes[parser.n_structural_indexes + 2] = 0;
parser.next_structural_index = 0;
+
+ // Bail out only once the count and sentinels above are set:
+ // document_stream::truncated_bytes() reads them even on error.
+ if (simdjson_unlikely(should_we_exit)) { return error; }
+ if (unescaped_chars_error) { return UNESCAPED_CHARS; }
+
// a valid JSON file cannot have zero structural indexes - we should have found something
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) {
return EMPTY;
@@ -33467,7 +33523,7 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) { return CAPACITY; }
}
// We truncate the input to the end of the last complete document (or zero).
- auto new_structural_indexes = find_next_document_index(parser);
+ auto new_structural_indexes = find_next_document_index(parser, !have_unclosed_string && ends_with_partial_scalar(parser, len));
if (new_structural_indexes == 0 && parser.n_structural_indexes > 0) {
if(parser.structural_indexes[0] == 0) {
// If the buffer is partial and we started at index 0 but the document is
@@ -36435,9 +36491,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -36448,6 +36509,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -40210,7 +40272,14 @@ namespace stage1 {
* complete document, therefore the last json buffer location is the end of the
* batch.
*/
-simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser) {
+simdjson_inline bool ends_with_partial_scalar(dom_parser_implementation &parser, size_t len) {
+ const uint8_t f = parser.buf[parser.structural_indexes[parser.n_structural_indexes - 1]];
+ const uint8_t e = parser.buf[len - 1];
+ return f != '{' && f != '[' && f != '}' && f != ']' && f != ':' && f != ',' && f != '"' &&
+ e != ' ' && e != '\t' && e != '\n' && e != '\r';
+}
+
+simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser, bool defer_last = false) {
// Variant: do not count separately, just figure out depth
if(parser.n_structural_indexes == 0) { return 0; }
auto arr_cnt = 0;
@@ -40244,7 +40313,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
// Last document is complete, so the next document will appear after!
if (!arr_cnt && !obj_cnt) {
- return parser.n_structural_indexes;
+ return defer_last ? i : parser.n_structural_indexes;
}
// Last document is incomplete; mark the document at i + 1 as the next one
return i;
@@ -40266,7 +40335,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
if (!arr_cnt && !obj_cnt) {
// We have a complete document.
- return parser.n_structural_indexes;
+ return defer_last ? 0 : parser.n_structural_indexes;
}
return 0;
}
@@ -40645,11 +40714,6 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
((error != SUCCESS) && (error != UNCLOSED_STRING)) // when partial we tolerate UNCLOSED_STRING
: (error != SUCCESS); // if partial is false, we must have SUCCESS
const bool have_unclosed_string = (error == UNCLOSED_STRING);
- if (simdjson_unlikely(should_we_exit)) { return error; }
-
- if (unescaped_chars_error) {
- return UNESCAPED_CHARS;
- }
parser.n_structural_indexes = uint32_t(indexer.tail - parser.structural_indexes.get());
/***
* The On-Demand API requires special padding.
@@ -40674,6 +40738,12 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
parser.structural_indexes[parser.n_structural_indexes + 1] = uint32_t(len);
parser.structural_indexes[parser.n_structural_indexes + 2] = 0;
parser.next_structural_index = 0;
+
+ // Bail out only once the count and sentinels above are set:
+ // document_stream::truncated_bytes() reads them even on error.
+ if (simdjson_unlikely(should_we_exit)) { return error; }
+ if (unescaped_chars_error) { return UNESCAPED_CHARS; }
+
// a valid JSON file cannot have zero structural indexes - we should have found something
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) {
return EMPTY;
@@ -40690,7 +40760,7 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) { return CAPACITY; }
}
// We truncate the input to the end of the last complete document (or zero).
- auto new_structural_indexes = find_next_document_index(parser);
+ auto new_structural_indexes = find_next_document_index(parser, !have_unclosed_string && ends_with_partial_scalar(parser, len));
if (new_structural_indexes == 0 && parser.n_structural_indexes > 0) {
if(parser.structural_indexes[0] == 0) {
// If the buffer is partial and we started at index 0 but the document is
@@ -43189,9 +43259,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -43202,6 +43277,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -46464,7 +46540,14 @@ namespace stage1 {
* complete document, therefore the last json buffer location is the end of the
* batch.
*/
-simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser) {
+simdjson_inline bool ends_with_partial_scalar(dom_parser_implementation &parser, size_t len) {
+ const uint8_t f = parser.buf[parser.structural_indexes[parser.n_structural_indexes - 1]];
+ const uint8_t e = parser.buf[len - 1];
+ return f != '{' && f != '[' && f != '}' && f != ']' && f != ':' && f != ',' && f != '"' &&
+ e != ' ' && e != '\t' && e != '\n' && e != '\r';
+}
+
+simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser, bool defer_last = false) {
// Variant: do not count separately, just figure out depth
if(parser.n_structural_indexes == 0) { return 0; }
auto arr_cnt = 0;
@@ -46498,7 +46581,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
// Last document is complete, so the next document will appear after!
if (!arr_cnt && !obj_cnt) {
- return parser.n_structural_indexes;
+ return defer_last ? i : parser.n_structural_indexes;
}
// Last document is incomplete; mark the document at i + 1 as the next one
return i;
@@ -46520,7 +46603,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
if (!arr_cnt && !obj_cnt) {
// We have a complete document.
- return parser.n_structural_indexes;
+ return defer_last ? 0 : parser.n_structural_indexes;
}
return 0;
}
@@ -46899,11 +46982,6 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
((error != SUCCESS) && (error != UNCLOSED_STRING)) // when partial we tolerate UNCLOSED_STRING
: (error != SUCCESS); // if partial is false, we must have SUCCESS
const bool have_unclosed_string = (error == UNCLOSED_STRING);
- if (simdjson_unlikely(should_we_exit)) { return error; }
-
- if (unescaped_chars_error) {
- return UNESCAPED_CHARS;
- }
parser.n_structural_indexes = uint32_t(indexer.tail - parser.structural_indexes.get());
/***
* The On-Demand API requires special padding.
@@ -46928,6 +47006,12 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
parser.structural_indexes[parser.n_structural_indexes + 1] = uint32_t(len);
parser.structural_indexes[parser.n_structural_indexes + 2] = 0;
parser.next_structural_index = 0;
+
+ // Bail out only once the count and sentinels above are set:
+ // document_stream::truncated_bytes() reads them even on error.
+ if (simdjson_unlikely(should_we_exit)) { return error; }
+ if (unescaped_chars_error) { return UNESCAPED_CHARS; }
+
// a valid JSON file cannot have zero structural indexes - we should have found something
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) {
return EMPTY;
@@ -46944,7 +47028,7 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) { return CAPACITY; }
}
// We truncate the input to the end of the last complete document (or zero).
- auto new_structural_indexes = find_next_document_index(parser);
+ auto new_structural_indexes = find_next_document_index(parser, !have_unclosed_string && ends_with_partial_scalar(parser, len));
if (new_structural_indexes == 0 && parser.n_structural_indexes > 0) {
if(parser.structural_indexes[0] == 0) {
// If the buffer is partial and we started at index 0 but the document is
@@ -49380,9 +49464,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -49393,6 +49482,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -52622,7 +52712,14 @@ namespace stage1 {
* complete document, therefore the last json buffer location is the end of the
* batch.
*/
-simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser) {
+simdjson_inline bool ends_with_partial_scalar(dom_parser_implementation &parser, size_t len) {
+ const uint8_t f = parser.buf[parser.structural_indexes[parser.n_structural_indexes - 1]];
+ const uint8_t e = parser.buf[len - 1];
+ return f != '{' && f != '[' && f != '}' && f != ']' && f != ':' && f != ',' && f != '"' &&
+ e != ' ' && e != '\t' && e != '\n' && e != '\r';
+}
+
+simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser, bool defer_last = false) {
// Variant: do not count separately, just figure out depth
if(parser.n_structural_indexes == 0) { return 0; }
auto arr_cnt = 0;
@@ -52656,7 +52753,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
// Last document is complete, so the next document will appear after!
if (!arr_cnt && !obj_cnt) {
- return parser.n_structural_indexes;
+ return defer_last ? i : parser.n_structural_indexes;
}
// Last document is incomplete; mark the document at i + 1 as the next one
return i;
@@ -52678,7 +52775,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
if (!arr_cnt && !obj_cnt) {
// We have a complete document.
- return parser.n_structural_indexes;
+ return defer_last ? 0 : parser.n_structural_indexes;
}
return 0;
}
@@ -53057,11 +53154,6 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
((error != SUCCESS) && (error != UNCLOSED_STRING)) // when partial we tolerate UNCLOSED_STRING
: (error != SUCCESS); // if partial is false, we must have SUCCESS
const bool have_unclosed_string = (error == UNCLOSED_STRING);
- if (simdjson_unlikely(should_we_exit)) { return error; }
-
- if (unescaped_chars_error) {
- return UNESCAPED_CHARS;
- }
parser.n_structural_indexes = uint32_t(indexer.tail - parser.structural_indexes.get());
/***
* The On-Demand API requires special padding.
@@ -53086,6 +53178,12 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
parser.structural_indexes[parser.n_structural_indexes + 1] = uint32_t(len);
parser.structural_indexes[parser.n_structural_indexes + 2] = 0;
parser.next_structural_index = 0;
+
+ // Bail out only once the count and sentinels above are set:
+ // document_stream::truncated_bytes() reads them even on error.
+ if (simdjson_unlikely(should_we_exit)) { return error; }
+ if (unescaped_chars_error) { return UNESCAPED_CHARS; }
+
// a valid JSON file cannot have zero structural indexes - we should have found something
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) {
return EMPTY;
@@ -53102,7 +53200,7 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) { return CAPACITY; }
}
// We truncate the input to the end of the last complete document (or zero).
- auto new_structural_indexes = find_next_document_index(parser);
+ auto new_structural_indexes = find_next_document_index(parser, !have_unclosed_string && ends_with_partial_scalar(parser, len));
if (new_structural_indexes == 0 && parser.n_structural_indexes > 0) {
if(parser.structural_indexes[0] == 0) {
// If the buffer is partial and we started at index 0 but the document is
@@ -55559,9 +55657,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -55572,6 +55675,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -59199,7 +59303,14 @@ namespace stage1 {
* complete document, therefore the last json buffer location is the end of the
* batch.
*/
-simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser) {
+simdjson_inline bool ends_with_partial_scalar(dom_parser_implementation &parser, size_t len) {
+ const uint8_t f = parser.buf[parser.structural_indexes[parser.n_structural_indexes - 1]];
+ const uint8_t e = parser.buf[len - 1];
+ return f != '{' && f != '[' && f != '}' && f != ']' && f != ':' && f != ',' && f != '"' &&
+ e != ' ' && e != '\t' && e != '\n' && e != '\r';
+}
+
+simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser, bool defer_last = false) {
// Variant: do not count separately, just figure out depth
if(parser.n_structural_indexes == 0) { return 0; }
auto arr_cnt = 0;
@@ -59233,7 +59344,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
// Last document is complete, so the next document will appear after!
if (!arr_cnt && !obj_cnt) {
- return parser.n_structural_indexes;
+ return defer_last ? i : parser.n_structural_indexes;
}
// Last document is incomplete; mark the document at i + 1 as the next one
return i;
@@ -59255,7 +59366,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
if (!arr_cnt && !obj_cnt) {
// We have a complete document.
- return parser.n_structural_indexes;
+ return defer_last ? 0 : parser.n_structural_indexes;
}
return 0;
}
@@ -59634,11 +59745,6 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
((error != SUCCESS) && (error != UNCLOSED_STRING)) // when partial we tolerate UNCLOSED_STRING
: (error != SUCCESS); // if partial is false, we must have SUCCESS
const bool have_unclosed_string = (error == UNCLOSED_STRING);
- if (simdjson_unlikely(should_we_exit)) { return error; }
-
- if (unescaped_chars_error) {
- return UNESCAPED_CHARS;
- }
parser.n_structural_indexes = uint32_t(indexer.tail - parser.structural_indexes.get());
/***
* The On-Demand API requires special padding.
@@ -59663,6 +59769,12 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
parser.structural_indexes[parser.n_structural_indexes + 1] = uint32_t(len);
parser.structural_indexes[parser.n_structural_indexes + 2] = 0;
parser.next_structural_index = 0;
+
+ // Bail out only once the count and sentinels above are set:
+ // document_stream::truncated_bytes() reads them even on error.
+ if (simdjson_unlikely(should_we_exit)) { return error; }
+ if (unescaped_chars_error) { return UNESCAPED_CHARS; }
+
// a valid JSON file cannot have zero structural indexes - we should have found something
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) {
return EMPTY;
@@ -59679,7 +59791,7 @@ simdjson_inline error_code json_structural_indexer::finish(dom_parser_implementa
if (simdjson_unlikely(parser.n_structural_indexes == 0u)) { return CAPACITY; }
}
// We truncate the input to the end of the last complete document (or zero).
- auto new_structural_indexes = find_next_document_index(parser);
+ auto new_structural_indexes = find_next_document_index(parser, !have_unclosed_string && ends_with_partial_scalar(parser, len));
if (new_structural_indexes == 0 && parser.n_structural_indexes > 0) {
if(parser.structural_indexes[0] == 0) {
// If the buffer is partial and we started at index 0 but the document is
@@ -61715,9 +61827,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -61728,6 +61845,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -63685,7 +63803,14 @@ namespace stage1 {
* complete document, therefore the last json buffer location is the end of the
* batch.
*/
-simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser) {
+simdjson_inline bool ends_with_partial_scalar(dom_parser_implementation &parser, size_t len) {
+ const uint8_t f = parser.buf[parser.structural_indexes[parser.n_structural_indexes - 1]];
+ const uint8_t e = parser.buf[len - 1];
+ return f != '{' && f != '[' && f != '}' && f != ']' && f != ':' && f != ',' && f != '"' &&
+ e != ' ' && e != '\t' && e != '\n' && e != '\r';
+}
+
+simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &parser, bool defer_last = false) {
// Variant: do not count separately, just figure out depth
if(parser.n_structural_indexes == 0) { return 0; }
auto arr_cnt = 0;
@@ -63719,7 +63844,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
// Last document is complete, so the next document will appear after!
if (!arr_cnt && !obj_cnt) {
- return parser.n_structural_indexes;
+ return defer_last ? i : parser.n_structural_indexes;
}
// Last document is incomplete; mark the document at i + 1 as the next one
return i;
@@ -63741,7 +63866,7 @@ simdjson_inline uint32_t find_next_document_index(dom_parser_implementation &par
}
if (!arr_cnt && !obj_cnt) {
// We have a complete document.
- return parser.n_structural_indexes;
+ return defer_last ? 0 : parser.n_structural_indexes;
}
return 0;
}
@@ -64949,8 +65074,8 @@ simdjson_inline void validate_utf8_character() {
// 2-byte
if ((buf[idx] & 0x20) == 0) {
// missing continuation
- if (simdjson_unlikely(idx+1 > len || !is_continuation(buf[idx+1]))) {
- if (idx+1 > len && is_streaming(partial)) { idx = len; return; }
+ if (simdjson_unlikely(idx+1 >= len || !is_continuation(buf[idx+1]))) {
+ if (idx+1 >= len && is_streaming(partial)) { idx = len; return; }
error = UTF8_ERROR;
idx++;
return;
@@ -64964,8 +65089,8 @@ simdjson_inline void validate_utf8_character() {
// 3-byte
if ((buf[idx] & 0x10) == 0) {
// missing continuation
- if (simdjson_unlikely(idx+2 > len || !is_continuation(buf[idx+1]) || !is_continuation(buf[idx+2]))) {
- if (idx+2 > len && is_streaming(partial)) { idx = len; return; }
+ if (simdjson_unlikely(idx+2 >= len || !is_continuation(buf[idx+1]) || !is_continuation(buf[idx+2]))) {
+ if (idx+2 >= len && is_streaming(partial)) { idx = len; return; }
error = UTF8_ERROR;
idx++;
return;
@@ -64980,8 +65105,8 @@ simdjson_inline void validate_utf8_character() {
// 4-byte
// missing continuation
- if (simdjson_unlikely(idx+3 > len || !is_continuation(buf[idx+1]) || !is_continuation(buf[idx+2]) || !is_continuation(buf[idx+3]))) {
- if (idx+2 > len && is_streaming(partial)) { idx = len; return; }
+ if (simdjson_unlikely(idx+3 >= len || !is_continuation(buf[idx+1]) || !is_continuation(buf[idx+2]) || !is_continuation(buf[idx+3]))) {
+ if (idx+3 >= len && is_streaming(partial)) { idx = len; return; }
error = UTF8_ERROR;
idx++;
return;
@@ -65103,9 +65228,15 @@ simdjson_warn_unused simdjson_inline error_code scan() {
add_structural();
// Primitive or invalid character (invalid characters will be checked in stage 2)
} else {
- // Anything else, add the structural and go until we find the next one
+ // Anything else, add the structural and go until we find the next one.
+ // We also stop on '"' so that an unclosed string still reaches
+ // validate_string(); a quote swallowed by the run would hide it. A
+ // quote cannot occur inside a valid primitive. We deliberately do not
+ // stop on every ESC_ASCII character: that also covers a backslash and the
+ // control characters, and ending the run there makes the fallback
+ // disagree with the SIMD kernels.
add_structural();
- while (idx+1 0) {
if(parser.structural_indexes[0] == 0) {
// If the buffer is partial and we started at index 0 but the document is
diff --git a/deps/simdjson/simdjson.h b/deps/simdjson/simdjson.h
index 55346b59ef59..43fe09631c01 100644
--- a/deps/simdjson/simdjson.h
+++ b/deps/simdjson/simdjson.h
@@ -1,4 +1,4 @@
-/* auto-generated on 2026-08-24 17:10:01 -0400. version 4.6.9 Do not edit! */
+/* auto-generated on 2026-09-04 16:04:31 -0400. version 4.6.11 Do not edit! */
/* including simdjson.h: */
/* begin file simdjson.h */
#ifndef SIMDJSON_H
@@ -260,7 +260,7 @@ using std::size_t;
#endif
#elif defined(__PPC64__) || defined(_M_PPC64)
#define SIMDJSON_IS_PPC64 1
-#if defined(__ALTIVEC__)
+#if defined(__ALTIVEC__) && defined(__POWER8_VECTOR__)
#define SIMDJSON_IS_PPC64_VMX 1
#endif // defined(__ALTIVEC__)
#else
@@ -2538,7 +2538,7 @@ namespace std {
#define SIMDJSON_SIMDJSON_VERSION_H
/** The version of simdjson being used (major.minor.revision) */
-#define SIMDJSON_VERSION "4.6.9"
+#define SIMDJSON_VERSION "4.6.11"
namespace simdjson {
enum {
@@ -2553,7 +2553,7 @@ enum {
/**
* The revision (major.minor.REVISION) of simdjson being used.
*/
- SIMDJSON_VERSION_REVISION = 9
+ SIMDJSON_VERSION_REVISION = 11
};
} // namespace simdjson
@@ -4734,6 +4734,7 @@ inline char *allocate_padded_buffer(size_t length) noexcept {
inline padded_string::padded_string() noexcept = default;
inline padded_string::padded_string(size_t length) noexcept
: viable_size(length), data_ptr(internal::allocate_padded_buffer(length)) {
+ if (data_ptr == nullptr) { viable_size = 0; }
}
inline padded_string::padded_string(const char *data, size_t length) noexcept
: viable_size(length), data_ptr(internal::allocate_padded_buffer(length)) {
@@ -6351,6 +6352,18 @@ class document_stream {
* }
* size_t truncated = stream.truncated_bytes();
*
+ * IMPORTANT: this value is only meaningful under the conditions below. It is
+ * computed from stage-1 bookkeeping, and outside these conditions it is not
+ * merely imprecise, it is arbitrary -- it can exceed size_in_bytes() or wrap
+ * around to a huge value. Check it only when both of the following hold:
+ *
+ * - you iterated all the way to the end of the stream;
+ * - no document reported an error. Iteration stops at the first failed
+ * document, which can leave the bookkeeping from a mid-stream batch.
+ *
+ * If you need to know about a truncated tail outside those conditions, track
+ * it yourself from the last successful document (see iterator::current_index()
+ * and iterator::source()).
*/
inline size_t truncated_bytes() const noexcept;
/**
@@ -9945,6 +9958,9 @@ simdjson_inline size_t document_stream::iterator::current_index() const noexcept
simdjson_inline std::string_view document_stream::iterator::source() const noexcept {
const char* start = reinterpret_cast(stream->buf) + current_index();
+ if (stream->error) {
+ return std::string_view(start, stream->len - current_index());
+ }
bool object_or_array = ((*start == '[') || (*start == '{'));
if(object_or_array) {
size_t next_doc_index = stream->batch_start + stream->parser->implementation->structural_indexes[stream->parser->implementation->next_structural_index - 1];
@@ -14993,9 +15009,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -15006,6 +15027,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -17206,9 +17228,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -17219,6 +17246,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -19906,9 +19934,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -19919,6 +19952,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -22606,9 +22640,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -22619,6 +22658,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -25421,9 +25461,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -25434,6 +25479,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -28553,9 +28599,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -28566,6 +28617,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -31185,9 +31237,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -31198,6 +31255,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -33795,9 +33853,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -33808,6 +33871,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -36422,9 +36486,14 @@ inline dom_parser_implementation &dom_parser_implementation::operator=(dom_parse
// Leaving these here so they can be inlined if so desired
inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(size_t capacity) noexcept {
- if(capacity > SIMDJSON_MAXSIZE_BYTES) { return CAPACITY; }
+ if(capacity > SIMDJSON_MAXSIZE_BYTES || capacity > SIZE_MAX - 63) { return CAPACITY; }
// Stage 1 index output
- size_t max_structures = SIMDJSON_ROUNDUP_N(capacity, 64) + 2 + 7;
+ size_t rounded_capacity = SIMDJSON_ROUNDUP_N(capacity, 64);
+ if(rounded_capacity + 9 < rounded_capacity) {
+ return CAPACITY; // overflow, only happen on legacy 32-bit systems with very large capacity
+ }
+ size_t max_structures = rounded_capacity + 9;
+ if(max_structures > SIZE_MAX / sizeof(uint32_t)) { return CAPACITY; }
structural_indexes.reset( new (std::nothrow) uint32_t[max_structures] );
if (!structural_indexes) { _capacity = 0; return MEMALLOC; }
structural_indexes[0] = 0;
@@ -36435,6 +36504,7 @@ inline simdjson_warn_unused error_code dom_parser_implementation::set_capacity(s
}
inline simdjson_warn_unused error_code dom_parser_implementation::set_max_depth(size_t max_depth) noexcept {
+ if(max_depth == 0 || max_depth > SIZE_MAX / sizeof(open_container)) { return CAPACITY; }
// Stage 2 stacks
open_containers.reset(new (std::nothrow) open_container[max_depth]);
is_array.reset(new (std::nothrow) bool[max_depth]);
@@ -39845,7 +39915,7 @@ simdjson_warn_unused simdjson_result extract_fractured_json(
#define SIMDJSON_EXPERIMENTAL_HAS_RVV 1
#endif
#endif
-#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__)
+#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__) && defined(__POWER8_VECTOR__)
#ifndef SIMDJSON_EXPERIMENTAL_HAS_PPC64
#define SIMDJSON_EXPERIMENTAL_HAS_PPC64 1
#endif
@@ -41940,7 +42010,7 @@ simdjson_warn_unused simdjson_result extract_fractured_json(
#define SIMDJSON_EXPERIMENTAL_HAS_RVV 1
#endif
#endif
-#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__)
+#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__) && defined(__POWER8_VECTOR__)
#ifndef SIMDJSON_EXPERIMENTAL_HAS_PPC64
#define SIMDJSON_EXPERIMENTAL_HAS_PPC64 1
#endif
@@ -44522,7 +44592,7 @@ simdjson_warn_unused simdjson_result extract_fractured_json(
#define SIMDJSON_EXPERIMENTAL_HAS_RVV 1
#endif
#endif
-#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__)
+#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__) && defined(__POWER8_VECTOR__)
#ifndef SIMDJSON_EXPERIMENTAL_HAS_PPC64
#define SIMDJSON_EXPERIMENTAL_HAS_PPC64 1
#endif
@@ -47104,7 +47174,7 @@ simdjson_warn_unused simdjson_result extract_fractured_json(
#define SIMDJSON_EXPERIMENTAL_HAS_RVV 1
#endif
#endif
-#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__)
+#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__) && defined(__POWER8_VECTOR__)
#ifndef SIMDJSON_EXPERIMENTAL_HAS_PPC64
#define SIMDJSON_EXPERIMENTAL_HAS_PPC64 1
#endif
@@ -49801,7 +49871,7 @@ simdjson_warn_unused simdjson_result extract_fractured_json(
#define SIMDJSON_EXPERIMENTAL_HAS_RVV 1
#endif
#endif
-#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__)
+#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__) && defined(__POWER8_VECTOR__)
#ifndef SIMDJSON_EXPERIMENTAL_HAS_PPC64
#define SIMDJSON_EXPERIMENTAL_HAS_PPC64 1
#endif
@@ -52815,7 +52885,7 @@ simdjson_warn_unused simdjson_result extract_fractured_json(
#define SIMDJSON_EXPERIMENTAL_HAS_RVV 1
#endif
#endif
-#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__)
+#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__) && defined(__POWER8_VECTOR__)
#ifndef SIMDJSON_EXPERIMENTAL_HAS_PPC64
#define SIMDJSON_EXPERIMENTAL_HAS_PPC64 1
#endif
@@ -55303,7 +55373,7 @@ simdjson_warn_unused simdjson_result extract_fractured_json(
#define SIMDJSON_EXPERIMENTAL_HAS_RVV 1
#endif
#endif
-#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__)
+#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__) && defined(__POWER8_VECTOR__)
#ifndef SIMDJSON_EXPERIMENTAL_HAS_PPC64
#define SIMDJSON_EXPERIMENTAL_HAS_PPC64 1
#endif
@@ -57814,7 +57884,7 @@ simdjson_warn_unused simdjson_result extract_fractured_json(
#define SIMDJSON_EXPERIMENTAL_HAS_RVV 1
#endif
#endif
-#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__)
+#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__) && defined(__POWER8_VECTOR__)
#ifndef SIMDJSON_EXPERIMENTAL_HAS_PPC64
#define SIMDJSON_EXPERIMENTAL_HAS_PPC64 1
#endif
@@ -60329,7 +60399,7 @@ simdjson_warn_unused simdjson_result extract_fractured_json(
#define SIMDJSON_EXPERIMENTAL_HAS_RVV 1
#endif
#endif
-#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__)
+#if (defined(__PPC64__) || defined(_M_PPC64)) && defined(__ALTIVEC__) && defined(__POWER8_VECTOR__)
#ifndef SIMDJSON_EXPERIMENTAL_HAS_PPC64
#define SIMDJSON_EXPERIMENTAL_HAS_PPC64 1
#endif
@@ -66951,6 +67021,18 @@ class document_stream {
* }
* size_t truncated = stream.truncated_bytes();
*
+ * IMPORTANT: this value is only meaningful under the conditions below. It is
+ * computed from stage-1 bookkeeping, and outside these conditions it is not
+ * merely imprecise, it is arbitrary -- it can exceed size_in_bytes() or wrap
+ * around to a huge value. Check it only when both of the following hold:
+ *
+ * - you iterated all the way to the end of the stream;
+ * - no document reported an error. Iteration stops at the first failed
+ * document, which can leave the bookkeeping from a mid-stream batch.
+ *
+ * If you need to know about a truncated tail outside those conditions, track
+ * it yourself from the last successful document (see iterator::current_index()
+ * and iterator::source()).
*/
inline size_t truncated_bytes() const noexcept;
@@ -70987,7 +71069,7 @@ simdjson_inline std::string_view document_stream::iterator::source() const noexc
// TODO: We could remove trailing whitespaces
// This returns a string spanning from start of value to the beginning of the next document (excluded)
{
- auto next_index = stream->parser->implementation->structural_indexes[++cur_struct_index];
+ auto next_index = stream->batch_start + stream->parser->implementation->structural_indexes[++cur_struct_index];
// normally the length would be next_index - current_index() - 1, except for the last document
size_t svlen = next_index - current_index();
const char *start = reinterpret_cast(stream->buf) + current_index();
@@ -80330,6 +80412,18 @@ class document_stream {
* }
* size_t truncated = stream.truncated_bytes();
*
+ * IMPORTANT: this value is only meaningful under the conditions below. It is
+ * computed from stage-1 bookkeeping, and outside these conditions it is not
+ * merely imprecise, it is arbitrary -- it can exceed size_in_bytes() or wrap
+ * around to a huge value. Check it only when both of the following hold:
+ *
+ * - you iterated all the way to the end of the stream;
+ * - no document reported an error. Iteration stops at the first failed
+ * document, which can leave the bookkeeping from a mid-stream batch.
+ *
+ * If you need to know about a truncated tail outside those conditions, track
+ * it yourself from the last successful document (see iterator::current_index()
+ * and iterator::source()).
*/
inline size_t truncated_bytes() const noexcept;
@@ -84366,7 +84460,7 @@ simdjson_inline std::string_view document_stream::iterator::source() const noexc
// TODO: We could remove trailing whitespaces
// This returns a string spanning from start of value to the beginning of the next document (excluded)
{
- auto next_index = stream->parser->implementation->structural_indexes[++cur_struct_index];
+ auto next_index = stream->batch_start + stream->parser->implementation->structural_indexes[++cur_struct_index];
// normally the length would be next_index - current_index() - 1, except for the last document
size_t svlen = next_index - current_index();
const char *start = reinterpret_cast(stream->buf) + current_index();
@@ -94196,6 +94290,18 @@ class document_stream {
* }
* size_t truncated = stream.truncated_bytes();
*
+ * IMPORTANT: this value is only meaningful under the conditions below. It is
+ * computed from stage-1 bookkeeping, and outside these conditions it is not
+ * merely imprecise, it is arbitrary -- it can exceed size_in_bytes() or wrap
+ * around to a huge value. Check it only when both of the following hold:
+ *
+ * - you iterated all the way to the end of the stream;
+ * - no document reported an error. Iteration stops at the first failed
+ * document, which can leave the bookkeeping from a mid-stream batch.
+ *
+ * If you need to know about a truncated tail outside those conditions, track
+ * it yourself from the last successful document (see iterator::current_index()
+ * and iterator::source()).
*/
inline size_t truncated_bytes() const noexcept;
@@ -98232,7 +98338,7 @@ simdjson_inline std::string_view document_stream::iterator::source() const noexc
// TODO: We could remove trailing whitespaces
// This returns a string spanning from start of value to the beginning of the next document (excluded)
{
- auto next_index = stream->parser->implementation->structural_indexes[++cur_struct_index];
+ auto next_index = stream->batch_start + stream->parser->implementation->structural_indexes[++cur_struct_index];
// normally the length would be next_index - current_index() - 1, except for the last document
size_t svlen = next_index - current_index();
const char *start = reinterpret_cast(stream->buf) + current_index();
@@ -108062,6 +108168,18 @@ class document_stream {
* }
* size_t truncated = stream.truncated_bytes();
*
+ * IMPORTANT: this value is only meaningful under the conditions below. It is
+ * computed from stage-1 bookkeeping, and outside these conditions it is not
+ * merely imprecise, it is arbitrary -- it can exceed size_in_bytes() or wrap
+ * around to a huge value. Check it only when both of the following hold:
+ *
+ * - you iterated all the way to the end of the stream;
+ * - no document reported an error. Iteration stops at the first failed
+ * document, which can leave the bookkeeping from a mid-stream batch.
+ *
+ * If you need to know about a truncated tail outside those conditions, track
+ * it yourself from the last successful document (see iterator::current_index()
+ * and iterator::source()).
*/
inline size_t truncated_bytes() const noexcept;
@@ -112098,7 +112216,7 @@ simdjson_inline std::string_view document_stream::iterator::source() const noexc
// TODO: We could remove trailing whitespaces
// This returns a string spanning from start of value to the beginning of the next document (excluded)
{
- auto next_index = stream->parser->implementation->structural_indexes[++cur_struct_index];
+ auto next_index = stream->batch_start + stream->parser->implementation->structural_indexes[++cur_struct_index];
// normally the length would be next_index - current_index() - 1, except for the last document
size_t svlen = next_index - current_index();
const char *start = reinterpret_cast(stream->buf) + current_index();
@@ -122043,6 +122161,18 @@ class document_stream {
* }
* size_t truncated = stream.truncated_bytes();
*
+ * IMPORTANT: this value is only meaningful under the conditions below. It is
+ * computed from stage-1 bookkeeping, and outside these conditions it is not
+ * merely imprecise, it is arbitrary -- it can exceed size_in_bytes() or wrap
+ * around to a huge value. Check it only when both of the following hold:
+ *
+ * - you iterated all the way to the end of the stream;
+ * - no document reported an error. Iteration stops at the first failed
+ * document, which can leave the bookkeeping from a mid-stream batch.
+ *
+ * If you need to know about a truncated tail outside those conditions, track
+ * it yourself from the last successful document (see iterator::current_index()
+ * and iterator::source()).
*/
inline size_t truncated_bytes() const noexcept;
@@ -126079,7 +126209,7 @@ simdjson_inline std::string_view document_stream::iterator::source() const noexc
// TODO: We could remove trailing whitespaces
// This returns a string spanning from start of value to the beginning of the next document (excluded)
{
- auto next_index = stream->parser->implementation->structural_indexes[++cur_struct_index];
+ auto next_index = stream->batch_start + stream->parser->implementation->structural_indexes[++cur_struct_index];
// normally the length would be next_index - current_index() - 1, except for the last document
size_t svlen = next_index - current_index();
const char *start = reinterpret_cast(stream->buf) + current_index();
@@ -136341,6 +136471,18 @@ class document_stream {
* }
* size_t truncated = stream.truncated_bytes();
*
+ * IMPORTANT: this value is only meaningful under the conditions below. It is
+ * computed from stage-1 bookkeeping, and outside these conditions it is not
+ * merely imprecise, it is arbitrary -- it can exceed size_in_bytes() or wrap
+ * around to a huge value. Check it only when both of the following hold:
+ *
+ * - you iterated all the way to the end of the stream;
+ * - no document reported an error. Iteration stops at the first failed
+ * document, which can leave the bookkeeping from a mid-stream batch.
+ *
+ * If you need to know about a truncated tail outside those conditions, track
+ * it yourself from the last successful document (see iterator::current_index()
+ * and iterator::source()).
*/
inline size_t truncated_bytes() const noexcept;
@@ -140377,7 +140519,7 @@ simdjson_inline std::string_view document_stream::iterator::source() const noexc
// TODO: We could remove trailing whitespaces
// This returns a string spanning from start of value to the beginning of the next document (excluded)
{
- auto next_index = stream->parser->implementation->structural_indexes[++cur_struct_index];
+ auto next_index = stream->batch_start + stream->parser->implementation->structural_indexes[++cur_struct_index];
// normally the length would be next_index - current_index() - 1, except for the last document
size_t svlen = next_index - current_index();
const char *start = reinterpret_cast(stream->buf) + current_index();
@@ -150113,6 +150255,18 @@ class document_stream {
* }
* size_t truncated = stream.truncated_bytes();
*
+ * IMPORTANT: this value is only meaningful under the conditions below. It is
+ * computed from stage-1 bookkeeping, and outside these conditions it is not
+ * merely imprecise, it is arbitrary -- it can exceed size_in_bytes() or wrap
+ * around to a huge value. Check it only when both of the following hold:
+ *
+ * - you iterated all the way to the end of the stream;
+ * - no document reported an error. Iteration stops at the first failed
+ * document, which can leave the bookkeeping from a mid-stream batch.
+ *
+ * If you need to know about a truncated tail outside those conditions, track
+ * it yourself from the last successful document (see iterator::current_index()
+ * and iterator::source()).
*/
inline size_t truncated_bytes() const noexcept;
@@ -154149,7 +154303,7 @@ simdjson_inline std::string_view document_stream::iterator::source() const noexc
// TODO: We could remove trailing whitespaces
// This returns a string spanning from start of value to the beginning of the next document (excluded)
{
- auto next_index = stream->parser->implementation->structural_indexes[++cur_struct_index];
+ auto next_index = stream->batch_start + stream->parser->implementation->structural_indexes[++cur_struct_index];
// normally the length would be next_index - current_index() - 1, except for the last document
size_t svlen = next_index - current_index();
const char *start = reinterpret_cast(stream->buf) + current_index();
@@ -163908,6 +164062,18 @@ class document_stream {
* }
* size_t truncated = stream.truncated_bytes();
*
+ * IMPORTANT: this value is only meaningful under the conditions below. It is
+ * computed from stage-1 bookkeeping, and outside these conditions it is not
+ * merely imprecise, it is arbitrary -- it can exceed size_in_bytes() or wrap
+ * around to a huge value. Check it only when both of the following hold:
+ *
+ * - you iterated all the way to the end of the stream;
+ * - no document reported an error. Iteration stops at the first failed
+ * document, which can leave the bookkeeping from a mid-stream batch.
+ *
+ * If you need to know about a truncated tail outside those conditions, track
+ * it yourself from the last successful document (see iterator::current_index()
+ * and iterator::source()).
*/
inline size_t truncated_bytes() const noexcept;
@@ -167944,7 +168110,7 @@ simdjson_inline std::string_view document_stream::iterator::source() const noexc
// TODO: We could remove trailing whitespaces
// This returns a string spanning from start of value to the beginning of the next document (excluded)
{
- auto next_index = stream->parser->implementation->structural_indexes[++cur_struct_index];
+ auto next_index = stream->batch_start + stream->parser->implementation->structural_indexes[++cur_struct_index];
// normally the length would be next_index - current_index() - 1, except for the last document
size_t svlen = next_index - current_index();
const char *start = reinterpret_cast(stream->buf) + current_index();
@@ -177707,6 +177873,18 @@ class document_stream {
* }
* size_t truncated = stream.truncated_bytes();
*
+ * IMPORTANT: this value is only meaningful under the conditions below. It is
+ * computed from stage-1 bookkeeping, and outside these conditions it is not
+ * merely imprecise, it is arbitrary -- it can exceed size_in_bytes() or wrap
+ * around to a huge value. Check it only when both of the following hold:
+ *
+ * - you iterated all the way to the end of the stream;
+ * - no document reported an error. Iteration stops at the first failed
+ * document, which can leave the bookkeeping from a mid-stream batch.
+ *
+ * If you need to know about a truncated tail outside those conditions, track
+ * it yourself from the last successful document (see iterator::current_index()
+ * and iterator::source()).
*/
inline size_t truncated_bytes() const noexcept;
@@ -181743,7 +181921,7 @@ simdjson_inline std::string_view document_stream::iterator::source() const noexc
// TODO: We could remove trailing whitespaces
// This returns a string spanning from start of value to the beginning of the next document (excluded)
{
- auto next_index = stream->parser->implementation->structural_indexes[++cur_struct_index];
+ auto next_index = stream->batch_start + stream->parser->implementation->structural_indexes[++cur_struct_index];
// normally the length would be next_index - current_index() - 1, except for the last document
size_t svlen = next_index - current_index();
const char *start = reinterpret_cast(stream->buf) + current_index();
diff --git a/deps/sqlite/unofficial.gni b/deps/sqlite/unofficial.gni
index 0e62accfb699..a0957c5769d6 100644
--- a/deps/sqlite/unofficial.gni
+++ b/deps/sqlite/unofficial.gni
@@ -4,47 +4,67 @@
# The actual configurations are put inside a template in unofficial.gni to
# prevent accidental edits from contributors.
+import("../../node.gni")
+
template("sqlite_gn_build") {
- config("sqlite_config") {
- include_dirs = [ "." ]
- defines = [
- "SQLITE_ENABLE_COLUMN_METADATA",
- "SQLITE_ENABLE_DBSTAT_VTAB",
- "SQLITE_ENABLE_FTS3",
- "SQLITE_ENABLE_FTS3_PARENTHESIS",
- "SQLITE_ENABLE_FTS5",
- "SQLITE_ENABLE_GEOPOLY",
- "SQLITE_ENABLE_MATH_FUNCTIONS",
- "SQLITE_ENABLE_PERCENTILE",
- "SQLITE_ENABLE_PREUPDATE_HOOK",
- "SQLITE_ENABLE_RBU",
- "SQLITE_ENABLE_RTREE",
- "SQLITE_ENABLE_SESSION",
- ]
- }
+ if (node_shared_sqlite) {
+ import("//build/config/linux/pkg_config.gni")
+ pkg_config("sqlite_config") {
+ packages = [ "sqlite3" ]
+ }
+ # sqlite3.h only declares the session extension when this is defined.
+ config("sqlite_defines") {
+ defines = [ "SQLITE_ENABLE_SESSION" ]
+ }
+ group(target_name) {
+ forward_variables_from(invoker, "*")
+ public_configs = [
+ ":sqlite_config",
+ ":sqlite_defines",
+ ]
+ }
+ } else {
+ config("sqlite_config") {
+ include_dirs = [ "." ]
+ defines = [
+ "SQLITE_ENABLE_COLUMN_METADATA",
+ "SQLITE_ENABLE_DBSTAT_VTAB",
+ "SQLITE_ENABLE_FTS3",
+ "SQLITE_ENABLE_FTS3_PARENTHESIS",
+ "SQLITE_ENABLE_FTS5",
+ "SQLITE_ENABLE_GEOPOLY",
+ "SQLITE_ENABLE_MATH_FUNCTIONS",
+ "SQLITE_ENABLE_PERCENTILE",
+ "SQLITE_ENABLE_PREUPDATE_HOOK",
+ "SQLITE_ENABLE_RBU",
+ "SQLITE_ENABLE_RTREE",
+ "SQLITE_ENABLE_SESSION",
+ ]
+ }
- gypi_values = exec_script("../../tools/gypi_to_gn.py",
- [ rebase_path("sqlite.gyp") ],
- "scope",
- [ "sqlite.gyp" ])
+ gypi_values = exec_script("../../tools/gypi_to_gn.py",
+ [ rebase_path("sqlite.gyp") ],
+ "scope",
+ [ "sqlite.gyp" ])
- source_set(target_name) {
- forward_variables_from(invoker, "*")
- public_configs = [ ":sqlite_config" ]
- sources = gypi_values.sqlite_sources
- cflags_c = [
- "-Wno-implicit-fallthrough",
- "-Wno-unreachable-code-return",
- "-Wno-unreachable-code-break",
- "-Wno-unreachable-code",
- ]
- if (is_win) {
- cflags_c += [
- "-Wno-sign-compare",
- "-Wno-unused-but-set-variable",
- "-Wno-unused-function",
- "-Wno-unused-variable",
+ source_set(target_name) {
+ forward_variables_from(invoker, "*")
+ public_configs = [ ":sqlite_config" ]
+ sources = gypi_values.sqlite_sources
+ cflags_c = [
+ "-Wno-implicit-fallthrough",
+ "-Wno-unreachable-code-return",
+ "-Wno-unreachable-code-break",
+ "-Wno-unreachable-code",
]
+ if (is_win) {
+ cflags_c += [
+ "-Wno-sign-compare",
+ "-Wno-unused-but-set-variable",
+ "-Wno-unused-function",
+ "-Wno-unused-variable",
+ ]
+ }
}
}
}
diff --git a/deps/uv/unofficial.gni b/deps/uv/unofficial.gni
index 0944d6ddd241..a2891887839b 100644
--- a/deps/uv/unofficial.gni
+++ b/deps/uv/unofficial.gni
@@ -4,113 +4,126 @@
# The actual configurations are put inside a template in unofficial.gni to
# prevent accidental edits from contributors.
-template("uv_gn_build") {
- config("uv_external_config") {
- include_dirs = [ "include" ]
- if (is_clang || !is_win) {
- cflags_cc = [
- "-Wno-deprecated-pragma", # for using ENODATA in errno.h
- ]
- }
- }
-
- config("uv_internal_config") {
- include_dirs = [
- "include",
- "src",
- ]
+import("../../node.gni")
- defines = [ "BUILDING_UV_SHARED" ] # always export symbols
- if (is_posix) {
- defines += [
- "_LARGEFILE_SOURCE",
- "_FILE_OFFSET_BITS=64",
- ]
+template("uv_gn_build") {
+ if (node_shared_libuv) {
+ import("//build/config/linux/pkg_config.gni")
+ pkg_config("uv_external_config") {
+ packages = [ "libuv" ]
}
- if (is_linux) {
- defines += [
- "_POSIX_C_SOURCE=200112",
- "_GNU_SOURCE",
- ]
+ group(target_name) {
+ forward_variables_from(invoker, "*")
+ public_configs = [ ":uv_external_config" ]
}
- if (is_apple) {
- defines += [
- "_DARWIN_USE_64_BIT_INODE=1",
- "_DARWIN_UNLIMITED_SELECT=1",
- ]
+ } else {
+ config("uv_external_config") {
+ include_dirs = [ "include" ]
+ if (is_clang || !is_win) {
+ cflags_cc = [
+ "-Wno-deprecated-pragma", # for using ENODATA in errno.h
+ ]
+ }
}
- if (is_clang || !is_win) {
- cflags_c = [
- "-Wno-deprecated-declarations",
- "-Wno-extra-semi",
- "-Wno-implicit-fallthrough",
- "-Wno-missing-braces",
- "-Wno-sign-compare",
- "-Wno-string-conversion",
- "-Wno-shadow",
- "-Wno-unreachable-code",
- "-Wno-unreachable-code-return",
- "-Wno-unused-but-set-parameter",
- "-Wno-unused-but-set-variable",
- "-Wno-unused-function",
- "-Wno-unused-result",
- "-Wno-unused-variable",
+
+ config("uv_internal_config") {
+ include_dirs = [
+ "include",
+ "src",
]
+
+ defines = [ "BUILDING_UV_SHARED" ] # always export symbols
+ if (is_posix) {
+ defines += [
+ "_LARGEFILE_SOURCE",
+ "_FILE_OFFSET_BITS=64",
+ ]
+ }
+ if (is_linux) {
+ defines += [
+ "_POSIX_C_SOURCE=200112",
+ "_GNU_SOURCE",
+ ]
+ }
+ if (is_apple) {
+ defines += [
+ "_DARWIN_USE_64_BIT_INODE=1",
+ "_DARWIN_UNLIMITED_SELECT=1",
+ ]
+ }
+ if (is_clang || !is_win) {
+ cflags_c = [
+ "-Wno-deprecated-declarations",
+ "-Wno-extra-semi",
+ "-Wno-implicit-fallthrough",
+ "-Wno-missing-braces",
+ "-Wno-sign-compare",
+ "-Wno-string-conversion",
+ "-Wno-shadow",
+ "-Wno-unreachable-code",
+ "-Wno-unreachable-code-return",
+ "-Wno-unused-but-set-parameter",
+ "-Wno-unused-but-set-variable",
+ "-Wno-unused-function",
+ "-Wno-unused-result",
+ "-Wno-unused-variable",
+ ]
+ }
}
- }
- gypi_values = exec_script("../../tools/gypi_to_gn.py",
- [ rebase_path("uv.gyp") ],
- "scope",
- [ "uv.gyp" ])
+ gypi_values = exec_script("../../tools/gypi_to_gn.py",
+ [ rebase_path("uv.gyp") ],
+ "scope",
+ [ "uv.gyp" ])
- component(target_name) {
- forward_variables_from(invoker, "*")
+ component(target_name) {
+ forward_variables_from(invoker, "*")
- configs += [ ":uv_internal_config" ]
- public_configs = [ ":uv_external_config" ]
+ configs += [ ":uv_internal_config" ]
+ public_configs = [ ":uv_external_config" ]
- if (is_posix) {
- configs -= [ "//build/config/gcc:symbol_visibility_hidden" ]
- configs += [ "//build/config/gcc:symbol_visibility_default" ]
- }
+ if (is_posix) {
+ configs -= [ "//build/config/gcc:symbol_visibility_hidden" ]
+ configs += [ "//build/config/gcc:symbol_visibility_default" ]
+ }
- if (is_win) {
- libs = [
- "advapi32.lib",
- "iphlpapi.lib",
- "psapi.lib",
- "shell32.lib",
- "user32.lib",
- "userenv.lib",
- "ws2_32.lib",
- ]
- }
- if (is_posix) {
- ldflags = [ "-pthread" ]
- }
- if (is_linux) {
- libs = [
- "m",
- "dl",
- "rt",
- ]
- }
+ if (is_win) {
+ libs = [
+ "advapi32.lib",
+ "iphlpapi.lib",
+ "psapi.lib",
+ "shell32.lib",
+ "user32.lib",
+ "userenv.lib",
+ "ws2_32.lib",
+ ]
+ }
+ if (is_posix) {
+ ldflags = [ "-pthread" ]
+ }
+ if (is_linux) {
+ libs = [
+ "m",
+ "dl",
+ "rt",
+ ]
+ }
- sources = gypi_values.uv_sources_common
- if (is_win) {
- sources += gypi_values.uv_sources_win
- }
- if (is_posix) {
- sources += gypi_values.uv_sources_posix +
- [ "src/unix/proctitle.c" ]
- }
- if (is_linux) {
- sources += gypi_values.uv_sources_linux
- }
- if (is_apple) {
- sources += gypi_values.uv_sources_apple +
- gypi_values.uv_sources_bsd_common
+ sources = gypi_values.uv_sources_common
+ if (is_win) {
+ sources += gypi_values.uv_sources_win
+ }
+ if (is_posix) {
+ sources += gypi_values.uv_sources_posix +
+ [ "src/unix/proctitle.c" ]
+ }
+ if (is_linux) {
+ sources += gypi_values.uv_sources_linux
+ }
+ if (is_apple) {
+ sources += gypi_values.uv_sources_apple +
+ gypi_values.uv_sources_bsd_common
+ }
}
}
}
diff --git a/deps/v8/src/api/api.cc b/deps/v8/src/api/api.cc
index fbd628370c0b..48da5b35b872 100644
--- a/deps/v8/src/api/api.cc
+++ b/deps/v8/src/api/api.cc
@@ -3741,14 +3741,6 @@ bool Value::IsTypedArray() const {
TYPED_ARRAYS_BASE(VALUE_IS_TYPED_ARRAY)
#undef VALUE_IS_TYPED_ARRAY
-bool Value::IsFloat16Array() const {
- auto obj = *Utils::OpenDirectHandle(this);
- return i::IsJSTypedArray(obj) &&
- i::Cast(obj)->type() == i::kExternalFloat16Array &&
- Utils::ApiCheck(i::v8_flags.js_float16array, "Value::IsFloat16Array",
- "Float16Array is not supported");
-}
-
bool Value::IsDataView() const {
auto obj = *Utils::OpenDirectHandle(this);
return IsJSDataView(obj) || IsJSRabGsabDataView(obj);
@@ -4292,16 +4284,6 @@ void v8::TypedArray::CheckCast(Value* that) {
TYPED_ARRAYS_BASE(CHECK_TYPED_ARRAY_CAST)
#undef CHECK_TYPED_ARRAY_CAST
-void v8::Float16Array::CheckCast(Value* that) {
- Utils::ApiCheck(i::v8_flags.js_float16array, "v8::Float16Array::Cast",
- "Float16Array is not supported");
- auto obj = *Utils::OpenDirectHandle(that);
- Utils::ApiCheck(
- i::IsJSTypedArray(obj) &&
- i::Cast(obj)->type() == i::kExternalFloat16Array,
- "v8::Float16Array::Cast()", "Value is not a Float16Array");
-}
-
void v8::DataView::CheckCast(Value* that) {
auto obj = *Utils::OpenDirectHandle(that);
Utils::ApiCheck(i::IsJSDataView(obj) || IsJSRabGsabDataView(obj),
@@ -9347,44 +9329,6 @@ static_assert(v8::TypedArray::kMaxByteLength == i::JSTypedArray::kMaxByteLength,
TYPED_ARRAYS_BASE(TYPED_ARRAY_NEW)
#undef TYPED_ARRAY_NEW
-Local Float16Array::New(Local array_buffer,
- size_t byte_offset, size_t length) {
- Utils::ApiCheck(i::v8_flags.js_float16array, "v8::Float16Array::New",
- "Float16Array is not supported");
- i::Isolate* i_isolate = i::Isolate::Current();
- ApiRuntimeCallStatsScope rcs_scope(i_isolate, RCCId::kAPI_Float16Array_New);
- EnterV8NoScriptNoExceptionScope api_scope(i_isolate);
- if (!Utils::ApiCheck(
- length <= kMaxLength,
- "v8::Float16Array::New(Local, size_t, size_t)",
- "length exceeds max allowed value")) {
- return {};
- }
- auto buffer = Utils::OpenDirectHandle(*array_buffer);
- i::DirectHandle obj = i_isolate->factory()->NewJSTypedArray(
- i::kExternalFloat16Array, buffer, byte_offset, length);
- return Utils::ToLocalFloat16Array(obj);
-}
-Local Float16Array::New(
- Local shared_array_buffer, size_t byte_offset,
- size_t length) {
- Utils::ApiCheck(i::v8_flags.js_float16array, "v8::Float16Array::New",
- "Float16Array is not supported");
- i::Isolate* i_isolate = i::Isolate::Current();
- ApiRuntimeCallStatsScope rcs_scope(i_isolate, RCCId::kAPI_Float16Array_New);
- EnterV8NoScriptNoExceptionScope api_scope(i_isolate);
- if (!Utils::ApiCheck(
- length <= kMaxLength,
- "v8::Float16Array::New(Local, size_t, size_t)",
- "length exceeds max allowed value")) {
- return {};
- }
- auto buffer = Utils::OpenDirectHandle(*shared_array_buffer);
- i::DirectHandle obj = i_isolate->factory()->NewJSTypedArray(
- i::kExternalFloat16Array, buffer, byte_offset, length);
- return Utils::ToLocalFloat16Array(obj);
-}
-
// TODO(v8:11111): Support creating length tracking DataViews via the API.
Local DataView::New(Local array_buffer,
size_t byte_offset, size_t byte_length) {
diff --git a/deps/v8/src/builtins/builtins.cc b/deps/v8/src/builtins/builtins.cc
index 34424db08dc0..0b6e43dde6ea 100644
--- a/deps/v8/src/builtins/builtins.cc
+++ b/deps/v8/src/builtins/builtins.cc
@@ -863,12 +863,6 @@ Builtins::JSBuiltinStateFlags Builtins::GetJSBuiltinState(Builtin builtin) {
RETURN_FLAG_DEPENDENT_BUILTIN_STATE(
v8_flags.js_explicit_resource_management);
- // --js-float16array
- case Builtin::kMathF16round:
- case Builtin::kDataViewPrototypeGetFloat16:
- case Builtin::kDataViewPrototypeSetFloat16:
- RETURN_FLAG_DEPENDENT_BUILTIN_STATE(v8_flags.js_float16array);
-
// --js-base-64
case Builtin::kUint8ArrayFromBase64:
case Builtin::kUint8ArrayFromHex:
diff --git a/deps/v8/src/deoptimizer/translated-state.cc b/deps/v8/src/deoptimizer/translated-state.cc
index a8c87dbd0242..6aa94999ac28 100644
--- a/deps/v8/src/deoptimizer/translated-state.cc
+++ b/deps/v8/src/deoptimizer/translated-state.cc
@@ -1889,6 +1889,31 @@ Address TranslatedState::DecompressIfNeeded(intptr_t value) {
}
}
+// static
+std::optional> TranslatedState::TryResolveTaggedValue(
+ DeoptTranslationIterator* it, Address fp,
+ Tagged literals) {
+ TranslationOpcode opcode = it->NextOpcode();
+ switch (opcode) {
+ case TranslationOpcode::LITERAL: {
+ int literal_index = it->NextOperand();
+ return literals->get(literal_index);
+ }
+ case TranslationOpcode::TAGGED_STACK_SLOT: {
+ int slot_offset =
+ OptimizedJSFrame::StackSlotOffsetRelativeToFp(it->NextOperand());
+ intptr_t value = *reinterpret_cast(fp + slot_offset);
+ return Tagged |