* Re: [PATCH v3] docs: wrap generated tables to contain small-screen overflow
From: Rito Rhymes @ 2026-04-04 8:13 UTC (permalink / raw)
To: Rito Rhymes, Jonathan Corbet, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel, rdunlap
In-Reply-To: <DHCD6BWJTLTB.VJWKPRWC2GHZ@ritovision.com>
Jon,
Following up on this point:
> So this CSS perhaps makes sense, but.. But why do you need to inject
> another <div>, creating a whole new extension to do so, rather than
> just applying the CSS directly to the <table> elements? I just gave
> that a try, and it would appear to work just fine.
In my previous reply I outlined the regressions I saw when applying the
CSS directly to the `<table>` elements, and why that led me to the
wrapper-based approach instead.
Given the regressions and rationale I already outlined, is the
wrapper-based approach acceptable?
Thanks,
Rito
^ permalink raw reply
* Re: [PATCH v3] docs: restore and reflow footer on narrow screens
From: Rito Rhymes @ 2026-04-04 8:09 UTC (permalink / raw)
To: Rito Rhymes, Jonathan Corbet; +Cc: Shuah Khan, linux-doc, linux-kernel
In-Reply-To: <20260326005811.116154-1-rito@ritovision.com>
Jon,
Following up on this point:
> We are not going to fix Sphinx accessibility piecemeal in this way.
I've rerolled this as a CSS-only change limited to making the footer
visible/usable on mobile. It is no longer trying to address footer
semantics or Sphinx accessibility more broadly.
If that narrower scope is acceptable, will it fit in the upcoming
release?
Thanks,
Rito
^ permalink raw reply
* Re: [PATCH] docs: set canonical base URL for HTML output
From: Rito Rhymes @ 2026-04-04 8:05 UTC (permalink / raw)
To: Rito Rhymes, Jonathan Corbet, linux-doc; +Cc: Shuah Khan, linux-kernel
In-Reply-To: <DHAED9XZZLVM.2GT0BOHIXJ7MA@ritovision.com>
Jon,
Following up on this point:
> This sounds like perhaps an argument for lore.kernel.org to set this
> variable for its build; I think a reasonable case could be made for
> that. I think that the case for everybody else's build is rather
> weaker.
Do you accept my follow-up reply's rationale for hardcoding the
canonical URL into the docs sources, or would you prefer that I
instead pursue the approach of having lore.kernel.org set this
via a build-time variable?
If it is the latter, is the Tools mailing list the right place to
take that?
Thanks,
Rito
^ permalink raw reply
* Re: [PATCH v3 0/2] docs: advanced search with benchmark harness
From: Rito Rhymes @ 2026-04-04 7:50 UTC (permalink / raw)
To: Randy Dunlap; +Cc: linux-doc, linux-kernel
In-Reply-To: <20260321181511.11706-1-rito@ritovision.com>
Randy, I meant to include you on the v3 reroll; this new version is
intended to address the compatibility issue you hit earlier in our
initial test and debugging (among other improvements).
I believe the problem came from version-dependent differences in the
generated Sphinx search data, so this reroll hardens the compatibility
handling around those differences and the search logic that consumes the
data.
If you have time to try it again with the setup that exposed the
problem before, I would appreciate confirmation that the updated
version behaves correctly there.
I would also appreciate your broader assessment of the feature:
whether it seems genuinely useful in practice, how large the benefit is
relative to the current Quick Search interface, how many other users you
think would benefit from it, and whether you see any remaining issues or
obvious room for improvement.
Rito
^ permalink raw reply
* [PATCH] crash: Support high memory reservation for range syntax
From: Youling Tang @ 2026-04-04 7:41 UTC (permalink / raw)
To: Andrew Morton, Baoquan He, Jonathan Corbet
Cc: Vivek Goyal, Dave Young, kexec, linux-kernel, linux-doc,
youling.tang, Youling Tang
From: Youling Tang <tangyouling@kylinos.cn>
The crashkernel range syntax (range1:size1[,range2:size2,...]) allows
automatic size selection based on system RAM, but it always reserves
from low memory. When a large crashkernel is selected, this can
consume most of the low memory, causing subsequent hardware
hotplug or drivers requiring low memory to fail due to allocation
failures.
Add a new optional conditional suffix ",>boundary" to the crashkernel
range syntax. When the selected crashkernel size exceeds the specified
boundary, the kernel will automatically apply the same reservation
policy as "crashkernel=size,high" - preferring high memory first
and reserving the default low memory area.
Syntax:
crashkernel=<range1>:<size1>[,<range2>:<size2>,...][@offset],>boundary
Example:
crashkernel=2G-16G:512M,16G-:1G,>512M
This means:
- For 2G-16G RAM: reserve 512M normally
- For >16G RAM: reserve 1G with high memory preference (since 1G > 512M)
For systems with >16G RAM, 1G is selected which exceeds 512M, so it
will be reserved from high memory instead of consuming 1G of
precious low memory.
Signed-off-by: Youling Tang <tangyouling@kylinos.cn>
---
Documentation/admin-guide/kdump/kdump.rst | 25 ++++++++-
.../admin-guide/kernel-parameters.txt | 2 +-
kernel/crash_reserve.c | 56 ++++++++++++++++---
3 files changed, 73 insertions(+), 10 deletions(-)
diff --git a/Documentation/admin-guide/kdump/kdump.rst b/Documentation/admin-guide/kdump/kdump.rst
index 7587caadbae1..b5ae4556e9ca 100644
--- a/Documentation/admin-guide/kdump/kdump.rst
+++ b/Documentation/admin-guide/kdump/kdump.rst
@@ -293,7 +293,28 @@ crashkernel syntax
2) if the RAM size is between 512M and 2G (exclusive), then reserve 64M
3) if the RAM size is larger than 2G, then reserve 128M
-3) crashkernel=size,high and crashkernel=size,low
+3) range1:size1[,range2:size2,...][@offset],>boundary
+ Optionally, the range list can be followed by a conditional suffix
+ `,>boundary`. When the selected crashkernel size matches the
+ condition, the kernel will reserve memory using the same policy as
+ `crashkernel=size,high` (i.e. prefer high memory first and reserve the
+ default low memory area).
+
+ The syntax is::
+
+ crashkernel=<range1>:<size1>[,<range2>:<size2>,...][@offset],>boundary
+ range=start-[end]
+
+ For example::
+
+ crashkernel=2G-16G:512M,16G-:1G,>512M
+
+ This would mean:
+ 1) if the RAM size is between 2G and 16G (exclusive), then reserve 512M.
+ 2) if the RAM size is larger than 16G, allocation will behave like
+ `crashkernel=1G,high`.
+
+4) crashkernel=size,high and crashkernel=size,low
If memory above 4G is preferred, crashkernel=size,high can be used to
fulfill that. With it, physical memory is allowed to be allocated from top,
@@ -311,7 +332,7 @@ crashkernel syntax
crashkernel=0,low
-4) crashkernel=size,cma
+5) crashkernel=size,cma
Reserve additional crash kernel memory from CMA. This reservation is
usable by the first system's userspace memory and kernel movable
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 03a550630644..b2e1892ab4d8 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -1087,7 +1087,7 @@ Kernel parameters
4G when '@offset' hasn't been specified.
See Documentation/admin-guide/kdump/kdump.rst for further details.
- crashkernel=range1:size1[,range2:size2,...][@offset]
+ crashkernel=range1:size1[,range2:size2,...][@offset][,>boundary]
[KNL] Same as above, but depends on the memory
in the running system. The syntax of range is
start-[end] where start and end are both
diff --git a/kernel/crash_reserve.c b/kernel/crash_reserve.c
index 62e60e0223cf..917738412390 100644
--- a/kernel/crash_reserve.c
+++ b/kernel/crash_reserve.c
@@ -254,15 +254,47 @@ static __init char *get_last_crashkernel(char *cmdline,
return ck_cmdline;
}
+/*
+ * This function parses command lines in the format
+ *
+ * crashkernel=ramsize-range:size[,...][@offset],>boundary
+ */
+static void __init parse_crashkernel_boundary(char *ck_cmdline,
+ unsigned long long *boundary)
+{
+ char *cur = ck_cmdline, *next;
+ char *first_gt = false;
+
+ first_gt = strchr(cur, '>');
+ if (!first_gt)
+ return;
+
+ cur = first_gt + 1;
+ if (*cur == '\0' || *cur == ' ' || *cur == ',') {
+ pr_warn("crashkernel: '>' specified without boundary size, ignoring\n");
+ return;
+ }
+
+ *boundary = memparse(cur, &next);
+ if (cur == next) {
+ pr_warn("crashkernel: invalid boundary size after '>'\n");
+ return;
+ }
+}
+
static int __init __parse_crashkernel(char *cmdline,
unsigned long long system_ram,
unsigned long long *crash_size,
unsigned long long *crash_base,
- const char *suffix)
+ const char *suffix,
+ bool *high,
+ unsigned long long *low_size)
{
char *first_colon, *first_space;
char *ck_cmdline;
char *name = "crashkernel=";
+ unsigned long long boundary = 0;
+ int ret;
BUG_ON(!crash_size || !crash_base);
*crash_size = 0;
@@ -283,10 +315,20 @@ static int __init __parse_crashkernel(char *cmdline,
*/
first_colon = strchr(ck_cmdline, ':');
first_space = strchr(ck_cmdline, ' ');
- if (first_colon && (!first_space || first_colon < first_space))
- return parse_crashkernel_mem(ck_cmdline, system_ram,
+ if (first_colon && (!first_space || first_colon < first_space)) {
+ ret = parse_crashkernel_mem(ck_cmdline, system_ram,
crash_size, crash_base);
+ /* Handle optional ',>boundary' condition for range ':' syntax only. */
+ parse_crashkernel_boundary(ck_cmdline, &boundary);
+ if (!ret && *crash_size > boundary) {
+ *high = true;
+ *low_size = DEFAULT_CRASH_KERNEL_LOW_SIZE;
+ }
+
+ return ret;
+ }
+
return parse_crashkernel_simple(ck_cmdline, crash_size, crash_base);
}
@@ -310,7 +352,7 @@ int __init parse_crashkernel(char *cmdline,
/* crashkernel=X[@offset] */
ret = __parse_crashkernel(cmdline, system_ram, crash_size,
- crash_base, NULL);
+ crash_base, NULL, high, low_size);
#ifdef CONFIG_ARCH_HAS_GENERIC_CRASHKERNEL_RESERVATION
/*
* If non-NULL 'high' passed in and no normal crashkernel
@@ -318,7 +360,7 @@ int __init parse_crashkernel(char *cmdline,
*/
if (high && ret == -ENOENT) {
ret = __parse_crashkernel(cmdline, 0, crash_size,
- crash_base, suffix_tbl[SUFFIX_HIGH]);
+ crash_base, suffix_tbl[SUFFIX_HIGH], high, low_size);
if (ret || !*crash_size)
return -EINVAL;
@@ -327,7 +369,7 @@ int __init parse_crashkernel(char *cmdline,
* is not allowed.
*/
ret = __parse_crashkernel(cmdline, 0, low_size,
- crash_base, suffix_tbl[SUFFIX_LOW]);
+ crash_base, suffix_tbl[SUFFIX_LOW], high, low_size);
if (ret == -ENOENT) {
*low_size = DEFAULT_CRASH_KERNEL_LOW_SIZE;
ret = 0;
@@ -344,7 +386,7 @@ int __init parse_crashkernel(char *cmdline,
*/
if (cma_size)
__parse_crashkernel(cmdline, 0, cma_size,
- &cma_base, suffix_tbl[SUFFIX_CMA]);
+ &cma_base, suffix_tbl[SUFFIX_CMA], high, low_size);
#endif
if (!*crash_size)
ret = -EINVAL;
--
2.43.0
^ permalink raw reply related
* [PATCH v3 2/2] docs: add advanced search benchmark harness and instrumentation
From: Rito Rhymes @ 2026-04-04 7:34 UTC (permalink / raw)
To: corbet, skhan; +Cc: linux-doc, linux-kernel, Rito Rhymes
In-Reply-To: <20260404073413.32309-1-rito@ritovision.com>
Add lightweight app-side timing instrumentation to kernel-search.js
that records per-phase search timings and search-index readiness
markers in a window.__kernelSearchPerf global. The instrumentation
is passive and does not alter search behavior.
Add a standalone Playwright benchmark harness for measuring advanced
search behavior against one or more built documentation trees. The
harness does not vendor or install Playwright in-tree; it points at
an existing local Playwright install and runs as an external developer
tool.
The harness captures request counts, transferred bytes, startup/index
timings, result-kind composition, and Pages summary-loading behavior
across baseline, poor-network, failure-injection, and fresh-navigation
recovery scenarios. It exercises the current advanced-search Pages
tab, supports both limited and unbounded Pages summary modes, and can
also collect coarse CDP page metrics and the app-side phase timings
exposed by the instrumentation above. Result-kind metadata is
recorded so compatibility runs stay self-describing across supported
Sphinx versions.
Signed-off-by: Rito Rhymes <rito@ritovision.com>
Assisted-by: Codex:GPT-5.4
Assisted-by: Claude:Opus-4.6
---
The harness was used for comparative local runs against stock Quick
Search and to validate the current runtime behavior of advanced search
across narrow, medium, and broad queries.
In those runs, the default advanced view stayed flat across the tested
queries and avoided page-summary fetching entirely, while stock scaled
summary-loading work with query breadth. In limited Pages mode, the
advanced view loaded 10 summaries for `landlock`, 23 for `futex`, and
50 for `kernel`.
Playwright is a good fit for this harness because the feature is
fundamentally a browser-side interface and needs end-to-end exercise at
that level. The harness is kept external because Playwright is not used
elsewhere in the kernel tree and is not intended to become a
documentation build dependency.
The tool is optional developer-side validation infrastructure. It is
useful for comparative benchmarking now, and it also provides a concrete
way to measure regressions or behavior changes if the search UI is tuned
or extended later.
Documentation/doc-guide/sphinx.rst | 29 +
Documentation/sphinx-static/kernel-search.js | 96 +-
MAINTAINERS | 1 +
tools/docs/bench_search_playwright.mjs | 1278 ++++++++++++++++++
4 files changed, 1397 insertions(+), 7 deletions(-)
create mode 100755 tools/docs/bench_search_playwright.mjs
diff --git a/Documentation/doc-guide/sphinx.rst b/Documentation/doc-guide/sphinx.rst
index 6f71192eb..f69785d8a 100644
--- a/Documentation/doc-guide/sphinx.rst
+++ b/Documentation/doc-guide/sphinx.rst
@@ -255,6 +255,35 @@ To verify full-site support for a change, also run at least one full
``make htmldocs`` build and manually exercise the generated ``search.html``
page.
+Benchmark Harness
+~~~~~~~~~~~~~~~~~
+
+For larger behavior or performance changes, the Playwright benchmark
+harness can compare stock and advanced search against already-built
+documentation trees.
+
+The harness lives at ``tools/docs/bench_search_playwright.mjs``. It does
+not install Playwright itself; point it at an existing Playwright module
+with the ``PLAYWRIGHT_MODULE`` environment variable or the
+``--playwright-module`` command-line option.
+
+A typical workflow is:
+
+1. Build each documentation tree you want to compare.
+2. Serve each ``Documentation/output`` directory locally, for example with
+ ``python3 -m http.server``.
+3. Run ``node tools/docs/bench_search_playwright.mjs`` with a variant
+ label, a local URL, a query, and an output path or output directory.
+
+For comparative runs, build and serve one documentation tree per target
+you want to benchmark.
+
+For the full option set, including scenario selection, failure
+injection, network throttling, startup timing collection, and optional
+app timings, run::
+
+ node tools/docs/bench_search_playwright.mjs --help
+
To disable the feature and return to the theme's default Quick Search setup,
remove the two template overrides:
diff --git a/Documentation/sphinx-static/kernel-search.js b/Documentation/sphinx-static/kernel-search.js
index f762c4be4..477da7185 100644
--- a/Documentation/sphinx-static/kernel-search.js
+++ b/Documentation/sphinx-static/kernel-search.js
@@ -57,6 +57,23 @@
let activeResultKind = RESULT_KIND_ORDER[0];
let pageSummaryLimitEnabled = true;
let tabStripCleanup = null;
+ // Expose lightweight search-phase timings for external benchmarking and
+ // diagnostics. The search UI does not depend on these values being present.
+ const perfNow = () =>
+ window.performance && typeof window.performance.now === "function"
+ ? window.performance.now()
+ : Date.now();
+ let domContentLoadedMs = null;
+ let searchIndexReadyMs = null;
+
+ const writeSearchPerf = (update) => {
+ const current = window.__kernelSearchPerf || {};
+ window.__kernelSearchPerf = {
+ ...current,
+ ...update,
+ version: 1,
+ };
+ };
// Hook into Sphinx's asynchronous searchindex.js loading.
window.Search = window.Search || {};
@@ -1127,13 +1144,48 @@ const setSummaryPlaceholder = (payload, text, modifierClass) => {
});
};
+ const storeRunSearchPerf = (state, resultsByKind, phaseTimingsMs, runSearchStartedMs, runSearchCompletedMs) => {
+ const resultCounts = RESULT_KIND_ORDER.reduce((counts, kind) => {
+ counts[kind] = resultsByKind[kind].length;
+ return counts;
+ }, {});
+ resultCounts.total = RESULT_KIND_ORDER.reduce((sum, kind) => sum + resultCounts[kind], 0);
+
+ const timingsMs = {
+ ...phaseTimingsMs,
+ runSearch: runSearchCompletedMs - runSearchStartedMs,
+ };
+ if (domContentLoadedMs !== null && searchIndexReadyMs !== null) {
+ timingsMs.searchIndexWait = searchIndexReadyMs - domContentLoadedMs;
+ }
+
+ writeSearchPerf({
+ exact: state.exact,
+ kinds: [...state.kinds],
+ query: state.query,
+ resultCounts,
+ timingsMs,
+ });
+ };
+
const runSearch = () => {
const baseState = parseState();
bindFormState(baseState);
populateAreaOptions(document.getElementById("kernel-search-area"), baseState);
populateObjectTypeOptions(document.getElementById("kernel-search-objtype"), baseState);
- const queryState = buildQueryState(baseState.query, baseState.exact);
+ const phaseTimingsMs = {};
+ const timePhase = (name, callback) => {
+ const startedMs = perfNow();
+ const value = callback();
+ phaseTimingsMs[name] = perfNow() - startedMs;
+ return value;
+ };
+ const runSearchStartedMs = perfNow();
+ const queryState = timePhase(
+ "buildQueryState",
+ () => buildQueryState(baseState.query, baseState.exact),
+ );
const renderState = {
...baseState,
highlightTerms: queryState.highlightTerms,
@@ -1147,27 +1199,55 @@ const setSummaryPlaceholder = (payload, text, modifierClass) => {
};
if (!baseState.queryLower) {
- renderResults(renderState, resultsByKind);
+ timePhase("renderResults", () => renderResults(renderState, resultsByKind));
+ storeRunSearchPerf(
+ baseState,
+ resultsByKind,
+ phaseTimingsMs,
+ runSearchStartedMs,
+ perfNow(),
+ );
return;
}
if (baseState.kinds.has("object")) {
- resultsByKind.object = collectObjectResults(window.Search._index, queryState, filters);
+ resultsByKind.object = timePhase(
+ "collectObjectResults",
+ () => collectObjectResults(window.Search._index, queryState, filters),
+ );
}
if (baseState.kinds.has("title")) {
- resultsByKind.title = collectSectionResults(window.Search._index, queryState, filters);
+ resultsByKind.title = timePhase(
+ "collectSectionResults",
+ () => collectSectionResults(window.Search._index, queryState, filters),
+ );
}
if (baseState.kinds.has("index")) {
- resultsByKind.index = collectIndexResults(window.Search._index, queryState, filters);
+ resultsByKind.index = timePhase(
+ "collectIndexResults",
+ () => collectIndexResults(window.Search._index, queryState, filters),
+ );
}
if (baseState.kinds.has("text")) {
- resultsByKind.text = collectTextResults(window.Search._index, queryState, filters);
+ resultsByKind.text = timePhase(
+ "collectTextResults",
+ () => collectTextResults(window.Search._index, queryState, filters),
+ );
}
- renderResults(renderState, resultsByKind);
+ timePhase("renderResults", () => renderResults(renderState, resultsByKind));
+ storeRunSearchPerf(
+ baseState,
+ resultsByKind,
+ phaseTimingsMs,
+ runSearchStartedMs,
+ perfNow(),
+ );
};
document.addEventListener("DOMContentLoaded", () => {
+ domContentLoadedMs = perfNow();
+ writeSearchPerf({ domContentLoadedMs });
const container = document.getElementById("kernel-search-results");
if (!container) return;
@@ -1175,6 +1255,8 @@ const setSummaryPlaceholder = (payload, text, modifierClass) => {
if (progress) progress.textContent = "Preparing search...";
window.Search.whenReady(() => {
+ searchIndexReadyMs = perfNow();
+ writeSearchPerf({ searchIndexReadyMs });
if (progress) progress.textContent = "";
runSearch();
});
diff --git a/MAINTAINERS b/MAINTAINERS
index c9e50b101..5d5441f81 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7661,6 +7661,7 @@ F: Documentation/sphinx-static/custom.css
F: Documentation/sphinx/templates/search.html
F: Documentation/sphinx/templates/searchbox.html
F: tools/docs/test_advanced_search.py
+F: tools/docs/bench_search_playwright.mjs
DOCUMENTATION PROCESS
M: Jonathan Corbet <corbet@lwn.net>
diff --git a/tools/docs/bench_search_playwright.mjs b/tools/docs/bench_search_playwright.mjs
new file mode 100755
index 000000000..b71fcd4cd
--- /dev/null
+++ b/tools/docs/bench_search_playwright.mjs
@@ -0,0 +1,1278 @@
+#!/usr/bin/env node
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * Benchmark one docs-search variant per invocation using an external
+ * Playwright installation.
+ *
+ * This script intentionally does not add Node dependencies to the kernel
+ * tree. Load Playwright from your local tooling via --playwright-module
+ * or PLAYWRIGHT_MODULE.
+ *
+ * Example:
+ * PLAYWRIGHT_MODULE=file:///root/bench/search-bench/node_modules/playwright/index.mjs \
+ * node tools/docs/bench_search_playwright.mjs \
+ * --variant stock \
+ * --url http://127.0.0.1:8001/ \
+ * --query kernel \
+ * --runs 5 \
+ * --scenario default \
+ * --scenario open-pages \
+ * --scenario scroll-pages \
+ * --output-dir /root/bench/results
+ */
+
+import fs from "node:fs/promises";
+import path from "node:path";
+import process from "node:process";
+import { pathToFileURL } from "node:url";
+
+const DEFAULT_QUERY = "kernel";
+const DEFAULT_RUNS = 3;
+const DEFAULT_SCENARIOS = ["default", "open-pages", "scroll-pages"];
+const DEFAULT_INITIAL_RESULTS_TIMEOUT_MS = 30000;
+const DEFAULT_NAVIGATION_TIMEOUT_MS = 30000;
+const DEFAULT_OBSERVATION_MS = 15000;
+const DEFAULT_SCROLL_DURATION_MS = 10000;
+const DEFAULT_OUTPUT_SEQUENCE_WIDTH = 4;
+const VALID_SCENARIOS = new Set([...DEFAULT_SCENARIOS, "recover-pages"]);
+const VALID_PAGE_VARIANTS = new Set(["advanced", "stock"]);
+const VALID_PAGE_SUMMARY_MODES = new Set(["limited", "unbounded"]);
+const NETWORK_PROFILES = {
+ none: null,
+ fast3g: {
+ offline: false,
+ latency: 150,
+ downloadThroughput: Math.round((1.6 * 1024 * 1024) / 8),
+ uploadThroughput: Math.round((0.75 * 1024 * 1024) / 8),
+ connectionType: "cellular3g",
+ },
+ slow4g: {
+ offline: false,
+ latency: 40,
+ downloadThroughput: Math.round((4 * 1024 * 1024) / 8),
+ uploadThroughput: Math.round((3 * 1024 * 1024) / 8),
+ connectionType: "cellular4g",
+ },
+};
+const CDP_DURATION_METRICS = new Map([
+ ["TaskDuration", "cdpTaskDurationMs"],
+ ["ScriptDuration", "cdpScriptDurationMs"],
+ ["LayoutDuration", "cdpLayoutDurationMs"],
+ ["RecalcStyleDuration", "cdpRecalcStyleDurationMs"],
+]);
+const CDP_VALUE_METRICS = new Map([
+ ["Documents", "cdpDocuments"],
+ ["Frames", "cdpFrames"],
+ ["JSEventListeners", "cdpJSEventListeners"],
+ ["JSHeapTotalSize", "cdpJsHeapTotalSizeBytes"],
+ ["JSHeapUsedSize", "cdpJsHeapUsedSizeBytes"],
+ ["LayoutCount", "cdpLayoutCount"],
+ ["Nodes", "cdpNodes"],
+ ["RecalcStyleCount", "cdpRecalcStyleCount"],
+]);
+const HELP = `Usage:
+ node tools/docs/bench_search_playwright.mjs --variant NAME --url URL [options]
+
+Required:
+ --variant NAME Report label for this benchmark target, e.g. stock or hard
+ --url URL Base URL for the docs build under test
+
+Options:
+ --expected-page-variant NAME Expected page type for mismatch notes: stock, advanced
+ Default: inferred from --variant when possible
+ --query TERM Search term to benchmark (default: ${DEFAULT_QUERY})
+ --runs N Cold runs per scenario (default: ${DEFAULT_RUNS})
+ --scenario NAME Scenario to run; repeatable
+ Values: default, open-pages, scroll-pages, recover-pages
+ Default: ${DEFAULT_SCENARIOS.join(", ")}
+ --navigation-timeout-ms N Timeout for page.goto (default: ${DEFAULT_NAVIGATION_TIMEOUT_MS})
+ --initial-results-timeout-ms N
+ Timeout waiting for first visible results (default: ${DEFAULT_INITIAL_RESULTS_TIMEOUT_MS})
+ --observation-ms N Time to keep observing after scenario actions (default: ${DEFAULT_OBSERVATION_MS})
+ --scroll-duration-ms N Scroll time for scroll-pages (default: ${DEFAULT_SCROLL_DURATION_MS})
+ --network-profile NAME Network emulation: none, fast3g, slow4g (default: none)
+ --summary-delay-ms N Artificial delay for summary page fetches (default: 0)
+ --summary-fail-rate R Artificial HTTP 503 rate for summary page fetches, 0..1 (default: 0)
+ --page-summary-mode MODE Advanced Pages summary mode: limited, unbounded
+ Default: limited
+ --collect-app-timings Collect app-side timings when the page exposes them
+ --collect-cdp-metrics Collect coarse CDP performance metrics for the page
+ --playwright-module SPEC Module specifier or file path for Playwright
+ --output PATH Write the JSON report to PATH, overwriting if it exists
+ --output-dir PATH Auto-number report files under PATH, e.g. 0001-stock-kernel.json
+ --headed Run Chromium headed
+ --help Show this help
+
+Playwright loading:
+ The script does not depend on Playwright being installed in this repo.
+ Use --playwright-module or PLAYWRIGHT_MODULE to point at your local
+ Playwright install. Examples:
+
+ --playwright-module playwright
+ --playwright-module /abs/path/to/node_modules/playwright/index.mjs
+ PLAYWRIGHT_MODULE=file:///abs/path/to/node_modules/playwright/index.mjs
+`;
+
+const wait = (milliseconds) =>
+ new Promise((resolve) => {
+ setTimeout(resolve, milliseconds);
+ });
+
+const parseInteger = (value, flagName) => {
+ const parsed = Number.parseInt(value, 10);
+ if (!Number.isFinite(parsed) || parsed < 0) {
+ throw new Error(`${flagName} must be a non-negative integer: ${value}`);
+ }
+ return parsed;
+};
+
+const parseFloatRate = (value, flagName) => {
+ const parsed = Number.parseFloat(value);
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
+ throw new Error(`${flagName} must be between 0 and 1: ${value}`);
+ }
+ return parsed;
+};
+
+const normalizeModuleSpecifier = (specifier) => {
+ if (!specifier) return specifier;
+ if (specifier.startsWith("file://")) return specifier;
+ if (path.isAbsolute(specifier)) return pathToFileURL(specifier).href;
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
+ return pathToFileURL(path.resolve(specifier)).href;
+ }
+ return specifier;
+};
+
+const sanitizeSlugPart = (value) => {
+ const normalized = String(value || "")
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/giu, "-")
+ .replace(/^-+|-+$/gu, "")
+ .replace(/-{2,}/gu, "-");
+ return normalized || "run";
+};
+
+const inferExpectedPageVariant = (variantName) => {
+ const normalized = String(variantName || "").toLowerCase();
+ if (normalized === "stock") return "stock";
+ if (normalized === "hard" || normalized === "advanced") return "advanced";
+ return "";
+};
+
+const requireValue = (argv, index, flagName) => {
+ if (index + 1 >= argv.length) {
+ throw new Error(`${flagName} requires a value`);
+ }
+ return argv[index + 1];
+};
+
+const parseArgs = (argv) => {
+ const options = {
+ collectAppTimings: false,
+ collectCdpMetrics: false,
+ expectedPageVariant: "",
+ headed: false,
+ initialResultsTimeoutMs: DEFAULT_INITIAL_RESULTS_TIMEOUT_MS,
+ networkProfile: "none",
+ navigationTimeoutMs: DEFAULT_NAVIGATION_TIMEOUT_MS,
+ observationMs: DEFAULT_OBSERVATION_MS,
+ output: "",
+ outputDir: "",
+ pageSummaryMode: "limited",
+ playwrightModule: process.env.PLAYWRIGHT_MODULE || "",
+ query: DEFAULT_QUERY,
+ runs: DEFAULT_RUNS,
+ scenarios: [],
+ scrollDurationMs: DEFAULT_SCROLL_DURATION_MS,
+ summaryDelayMs: 0,
+ summaryFailRate: 0,
+ url: "",
+ variant: "",
+ };
+
+ for (let index = 0; index < argv.length; index += 1) {
+ const argument = argv[index];
+ switch (argument) {
+ case "--variant":
+ options.variant = requireValue(argv, index, argument);
+ index += 1;
+ break;
+ case "--url":
+ options.url = requireValue(argv, index, argument);
+ index += 1;
+ break;
+ case "--expected-page-variant":
+ options.expectedPageVariant = requireValue(argv, index, argument).toLowerCase();
+ index += 1;
+ break;
+ case "--query":
+ options.query = requireValue(argv, index, argument);
+ index += 1;
+ break;
+ case "--runs":
+ options.runs = parseInteger(requireValue(argv, index, argument), argument);
+ index += 1;
+ break;
+ case "--scenario":
+ options.scenarios.push(requireValue(argv, index, argument));
+ index += 1;
+ break;
+ case "--navigation-timeout-ms":
+ options.navigationTimeoutMs = parseInteger(requireValue(argv, index, argument), argument);
+ index += 1;
+ break;
+ case "--initial-results-timeout-ms":
+ options.initialResultsTimeoutMs = parseInteger(requireValue(argv, index, argument), argument);
+ index += 1;
+ break;
+ case "--observation-ms":
+ options.observationMs = parseInteger(requireValue(argv, index, argument), argument);
+ index += 1;
+ break;
+ case "--scroll-duration-ms":
+ options.scrollDurationMs = parseInteger(requireValue(argv, index, argument), argument);
+ index += 1;
+ break;
+ case "--network-profile":
+ options.networkProfile = requireValue(argv, index, argument);
+ index += 1;
+ break;
+ case "--summary-delay-ms":
+ options.summaryDelayMs = parseInteger(requireValue(argv, index, argument), argument);
+ index += 1;
+ break;
+ case "--summary-fail-rate":
+ options.summaryFailRate = parseFloatRate(requireValue(argv, index, argument), argument);
+ index += 1;
+ break;
+ case "--page-summary-mode":
+ options.pageSummaryMode = requireValue(argv, index, argument).toLowerCase();
+ index += 1;
+ break;
+ case "--collect-app-timings":
+ options.collectAppTimings = true;
+ break;
+ case "--collect-cdp-metrics":
+ options.collectCdpMetrics = true;
+ break;
+ case "--playwright-module":
+ options.playwrightModule = requireValue(argv, index, argument);
+ index += 1;
+ break;
+ case "--output":
+ options.output = requireValue(argv, index, argument);
+ index += 1;
+ break;
+ case "--output-dir":
+ options.outputDir = requireValue(argv, index, argument);
+ index += 1;
+ break;
+ case "--headed":
+ options.headed = true;
+ break;
+ case "--help":
+ options.help = true;
+ break;
+ default:
+ throw new Error(`Unknown argument: ${argument}`);
+ }
+ }
+
+ if (options.help) return options;
+
+ if (!options.variant) throw new Error("--variant is required");
+ if (!options.url) throw new Error("--url is required");
+ if (options.output && options.outputDir) {
+ throw new Error("--output and --output-dir are mutually exclusive");
+ }
+ if (!Object.prototype.hasOwnProperty.call(NETWORK_PROFILES, options.networkProfile)) {
+ throw new Error(`Unsupported --network-profile: ${options.networkProfile}`);
+ }
+ if (options.expectedPageVariant && !VALID_PAGE_VARIANTS.has(options.expectedPageVariant)) {
+ throw new Error(`Unsupported --expected-page-variant: ${options.expectedPageVariant}`);
+ }
+ if (!VALID_PAGE_SUMMARY_MODES.has(options.pageSummaryMode)) {
+ throw new Error(`Unsupported --page-summary-mode: ${options.pageSummaryMode}`);
+ }
+
+ if (options.scenarios.length === 0) options.scenarios = DEFAULT_SCENARIOS.slice();
+ options.scenarios.forEach((scenario) => {
+ if (!VALID_SCENARIOS.has(scenario)) {
+ throw new Error(`Unsupported --scenario: ${scenario}`);
+ }
+ });
+
+ if (!options.expectedPageVariant) {
+ options.expectedPageVariant = inferExpectedPageVariant(options.variant);
+ }
+
+ return options;
+};
+
+const ensureBaseUrl = (value) => {
+ const url = new URL(value);
+ if (!url.pathname.endsWith("/")) url.pathname += "/";
+ return url;
+};
+
+const buildSearchUrl = (baseUrl, query) => {
+ const url = new URL("search.html", baseUrl);
+ url.searchParams.set("q", query);
+ return url;
+};
+
+// Identify HTML document fetches triggered by page-summary loading while
+// excluding static assets and Sphinx search infrastructure requests.
+const isSummaryRequest = (requestUrl, searchUrl) => {
+ let url;
+ try {
+ url = new URL(requestUrl);
+ } catch {
+ return false;
+ }
+
+ if (url.origin !== searchUrl.origin) return false;
+ if (url.pathname === searchUrl.pathname) return false;
+ if (url.pathname.includes("/_static/")) return false;
+ if (
+ url.pathname.endsWith("/searchindex.js")
+ || url.pathname.endsWith("/language_data.js")
+ || url.pathname.endsWith("/documentation_options.js")
+ ) {
+ return false;
+ }
+ if (/\.(?:js|css|png|svg|jpg|jpeg|gif|webp|ico|json|map|txt|woff2?|ttf)$/iu.test(url.pathname)) {
+ return false;
+ }
+
+ return url.pathname.endsWith(".html") || url.pathname.endsWith("/");
+};
+
+const loadPlaywright = async (moduleSpecifier) => {
+ const attempts = [];
+ if (moduleSpecifier) attempts.push(normalizeModuleSpecifier(moduleSpecifier));
+ else attempts.push("playwright");
+
+ let lastError = null;
+ for (const attempt of attempts) {
+ try {
+ const playwright = await import(attempt);
+ const chromium = playwright.chromium || playwright.default?.chromium;
+ if (!chromium) {
+ throw new Error(`Playwright module does not export chromium: ${attempt}`);
+ }
+ return {
+ chromium,
+ resolvedSpecifier: attempt,
+ };
+ } catch (error) {
+ lastError = error;
+ }
+ }
+
+ const hint = moduleSpecifier
+ ? `Failed to import Playwright from ${moduleSpecifier}`
+ : "Failed to import Playwright. Set --playwright-module or PLAYWRIGHT_MODULE.";
+ throw new Error(`${hint}\n${lastError}`);
+};
+
+// Inject page-side observers before app scripts run so the harness can
+// capture DOM and index-readiness milestones from the page itself.
+const addBenchmarkInitScript = async (page) => {
+ await page.addInitScript(() => {
+ const bench = {
+ firstResultLinkMs: null,
+ firstResolvedSummaryMs: null,
+ firstSummaryStateMs: null,
+ searchIndexReadyMs: null,
+ searchIndexReadySource: null,
+ };
+ const markSearchIndexReady = (source) => {
+ if (bench.searchIndexReadyMs !== null) return;
+ bench.searchIndexReadyMs = performance.now();
+ bench.searchIndexReadySource = source;
+ };
+ const getSearchGlobal = () => {
+ try {
+ if (typeof Search !== "undefined") return Search;
+ } catch {
+ // Ignore missing global lexical bindings and fall back to window.Search.
+ }
+ return window.Search || null;
+ };
+ const installSearchIndexHook = () => {
+ const search = getSearchGlobal();
+ if (!search || typeof search !== "object") return false;
+ if (search.__kernelSearchBenchWrapped) return true;
+ if (typeof search.setIndex !== "function") return false;
+
+ const originalSetIndex = search.setIndex;
+ search.setIndex = function benchmarkWrappedSetIndex(...args) {
+ markSearchIndexReady("setIndex");
+ return originalSetIndex.apply(this, args);
+ };
+ search.__kernelSearchBenchWrapped = true;
+ return true;
+ };
+ const pollSearchIndexReady = () => {
+ installSearchIndexHook();
+ const search = getSearchGlobal();
+ if (!search || typeof search !== "object") return false;
+
+ const ready = (typeof search.hasIndex === "function" && search.hasIndex())
+ || (typeof search._index !== "undefined" && search._index !== null);
+ if (!ready) return false;
+
+ markSearchIndexReady("hasIndex");
+ return true;
+ };
+ const check = () => {
+ if (
+ bench.firstResultLinkMs === null
+ && document.querySelector("#kernel-search-results .kernel-search-result a, #search-results li a")
+ ) {
+ bench.firstResultLinkMs = performance.now();
+ }
+
+ if (
+ bench.firstSummaryStateMs === null
+ && document.querySelector("#kernel-search-results .kernel-search-summary, #search-results p.context")
+ ) {
+ bench.firstSummaryStateMs = performance.now();
+ }
+
+ if (
+ bench.firstResolvedSummaryMs === null
+ && document.querySelector(
+ "#kernel-search-results .kernel-search-summary:not(.kernel-search-summary-status), #search-results p.context",
+ )
+ ) {
+ bench.firstResolvedSummaryMs = performance.now();
+ }
+ };
+
+ const startObserver = () => {
+ const root = document.documentElement;
+ if (!root) return;
+ const observer = new MutationObserver(check);
+ observer.observe(root, { childList: true, subtree: true });
+ check();
+ };
+ const searchIndexPoll = window.setInterval(() => {
+ if (pollSearchIndexReady()) {
+ window.clearInterval(searchIndexPoll);
+ }
+ }, 5);
+
+ window.__kernelSearchBench = bench;
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", startObserver, { once: true });
+ } else {
+ startObserver();
+ }
+ window.addEventListener("load", pollSearchIndexReady, { once: true });
+ });
+};
+
+const waitForInitialResults = async (page, timeoutMs) => {
+ await page.waitForFunction(
+ () =>
+ !!document.querySelector("#kernel-search-results .kernel-search-result a, #search-results li a"),
+ { timeout: timeoutMs },
+ );
+};
+
+const detectPageVariant = async (page) =>
+ page.evaluate(() => (document.getElementById("kernel-search-results") ? "advanced" : "stock"));
+
+const selectPagesViewIfPresent = async (page, pageVariant, pageSummaryMode) => {
+ if (pageVariant !== "advanced") {
+ return {
+ action: "no-op-no-pages-tab",
+ effective: false,
+ note: "stock quick search has no Pages tab",
+ pagesTabSelected: false,
+ };
+ }
+
+ const tab = page.locator("#kernel-search-tab-text");
+ if ((await tab.count()) === 0) {
+ return {
+ action: "no-op-no-pages-tab",
+ effective: false,
+ note: "advanced search returned no Pages tab for this query",
+ pagesTabSelected: false,
+ };
+ }
+
+ const alreadySelected = await tab.evaluate(
+ (element) => element.getAttribute("aria-selected") === "true",
+ );
+ if (!alreadySelected) {
+ await tab.click();
+ await page.waitForTimeout(250);
+ }
+
+ const summaryToggle = page.locator(".kernel-search-summary-limit input[type='checkbox']");
+ if ((await summaryToggle.count()) > 0) {
+ const shouldBeChecked = pageSummaryMode === "limited";
+ const isChecked = await summaryToggle.isChecked();
+ if (isChecked !== shouldBeChecked) {
+ await summaryToggle.click();
+ await page.waitForTimeout(100);
+ }
+ }
+
+ return {
+ action: alreadySelected ? "pages-tab-already-selected" : "selected-pages-tab",
+ effective: true,
+ note: pageSummaryMode === "unbounded"
+ ? "selected Pages tab and disabled the default summary limit"
+ : "selected Pages tab",
+ pagesTabSelected: true,
+ };
+};
+
+const scrollSearchPage = async (page, durationMs, pageVariant) => {
+ const stepDelayMs = 250;
+ const deadline = Date.now() + durationMs;
+
+ if (pageVariant === "advanced") {
+ const panel = page.locator("#kernel-search-panel-text:not([hidden])");
+ if ((await panel.count()) > 0) {
+ while (Date.now() < deadline) {
+ await panel.evaluate((element) => {
+ element.scrollBy(0, Math.max(400, Math.floor(element.clientHeight * 0.8)));
+ });
+ await page.waitForTimeout(stepDelayMs);
+ }
+ return;
+ }
+ }
+
+ while (Date.now() < deadline) {
+ await page.evaluate(() => {
+ window.scrollBy(0, Math.max(400, Math.floor(window.innerHeight * 0.8)));
+ });
+ await page.waitForTimeout(stepDelayMs);
+ }
+};
+
+const resetTrackingState = (tracking) => {
+ tracking.requests = new Map();
+ tracking.peakSummaryConcurrency = 0;
+ tracking.inflightSummaryRequests = 0;
+ tracking.wallStart = Date.now();
+};
+
+const collectDomMetrics = async (page) =>
+ page.evaluate(() => {
+ const bench = window.__kernelSearchBench || {};
+ const advanced = !!document.getElementById("kernel-search-results");
+ const resultKindCounts = advanced
+ ? ["object", "title", "index", "text"].reduce((counts, kind) => {
+ counts[kind] = document.querySelectorAll(
+ `#kernel-search-results .kernel-search-result.kind-${kind}`,
+ ).length;
+ return counts;
+ }, {})
+ : { text: document.querySelectorAll("#search-results li").length };
+ const availableResultKinds = Object.entries(resultKindCounts)
+ .filter(([, count]) => typeof count === "number" && count > 0)
+ .map(([kind]) => kind)
+ .sort();
+ const summaryToggle = advanced
+ ? document.querySelector(".kernel-search-summary-limit input[type='checkbox']")
+ : null;
+
+ return {
+ availableResultKinds,
+ pageVariant: advanced ? "advanced" : "stock",
+ firstResultLinkMs: bench.firstResultLinkMs ?? null,
+ firstResolvedSummaryMs: bench.firstResolvedSummaryMs ?? null,
+ firstSummaryStateMs: bench.firstSummaryStateMs ?? null,
+ searchIndexReadyMs: bench.searchIndexReadyMs ?? null,
+ searchIndexReadySource: typeof bench.searchIndexReadySource === "string"
+ ? bench.searchIndexReadySource
+ : null,
+ pageSummaryMode: summaryToggle
+ ? (summaryToggle.checked ? "limited" : "unbounded")
+ : null,
+ pagesTabActive: advanced
+ ? Boolean(
+ document.querySelector("#kernel-search-tab-text")?.getAttribute("aria-selected") === "true",
+ )
+ : null,
+ renderedSummaries: advanced
+ ? document.querySelectorAll(
+ "#kernel-search-results .kernel-search-summary:not(.kernel-search-summary-status)",
+ ).length
+ : document.querySelectorAll("#search-results p.context").length,
+ summaryPlaceholders: advanced
+ ? document.querySelectorAll("#kernel-search-results .kernel-search-summary-status").length
+ : 0,
+ resultKindCounts,
+ textResults: advanced
+ ? document.querySelectorAll("#kernel-search-results .kernel-search-result.kind-text").length
+ : null,
+ totalResults: advanced
+ ? document.querySelectorAll("#kernel-search-results .kernel-search-result").length
+ : document.querySelectorAll("#search-results li").length,
+ };
+ });
+
+const collectAppTimings = async (page) =>
+ page.evaluate(() => {
+ const perf = window.__kernelSearchPerf;
+ if (!perf || typeof perf !== "object") return null;
+ return {
+ exact: typeof perf.exact === "boolean" ? perf.exact : null,
+ kinds: Array.isArray(perf.kinds) ? perf.kinds : [],
+ query: typeof perf.query === "string" ? perf.query : null,
+ resultCounts: perf.resultCounts && typeof perf.resultCounts === "object"
+ ? perf.resultCounts
+ : null,
+ timingsMs: perf.timingsMs && typeof perf.timingsMs === "object"
+ ? perf.timingsMs
+ : null,
+ version: typeof perf.version === "number" ? perf.version : null,
+ };
+ });
+
+const collectCdpPerformanceMetrics = async (client) => {
+ const { metrics } = await client.send("Performance.getMetrics");
+ const sourceMetrics = new Map(metrics.map((metric) => [metric.name, metric.value]));
+ const collected = {};
+
+ CDP_DURATION_METRICS.forEach((targetName, sourceName) => {
+ if (!sourceMetrics.has(sourceName)) return;
+ collected[targetName] = sourceMetrics.get(sourceName) * 1000;
+ });
+ CDP_VALUE_METRICS.forEach((targetName, sourceName) => {
+ if (!sourceMetrics.has(sourceName)) return;
+ collected[targetName] = sourceMetrics.get(sourceName);
+ });
+
+ return collected;
+};
+
+const snapshotRequests = (requests) =>
+ [...requests.values()].map((request) => ({
+ ...request,
+ pending: request.endedAtMs === null,
+ }));
+
+const computeMetrics = ({
+ domMetrics,
+ peakSummaryConcurrency,
+ requests,
+}) => {
+ const completedRequests = requests.filter((request) => !request.pending);
+ const searchIndexRequests = requests.filter((request) => request.isSearchIndex);
+ const completedSearchIndexRequests = searchIndexRequests.filter((request) => !request.pending);
+ const summaryRequests = requests.filter((request) => request.isSummary);
+ const failedSummaryRequests = summaryRequests.filter(
+ (request) => request.failed || (request.status !== null && request.status >= 400),
+ );
+ const successfulSummaryRequests = summaryRequests.filter(
+ (request) => !request.pending && !request.failed && (request.status === null || request.status < 400),
+ );
+ const totalBytes = completedRequests.reduce(
+ (sum, request) => sum + (request.encodedDataLength || 0),
+ 0,
+ );
+ const summaryBytes = summaryRequests.reduce(
+ (sum, request) => sum + (request.encodedDataLength || 0),
+ 0,
+ );
+ const searchIndexRequestStartMs = searchIndexRequests.length
+ ? Math.min(...searchIndexRequests.map((request) => request.startedAtMs))
+ : null;
+ const searchIndexResponseEndMs = completedSearchIndexRequests.length
+ ? Math.max(...completedSearchIndexRequests.map((request) => request.endedAtMs || 0))
+ : null;
+ const searchIndexEncodedBytes = searchIndexRequests.reduce(
+ (sum, request) => sum + (request.encodedDataLength || 0),
+ 0,
+ );
+
+ return {
+ firstResultLinkMs: domMetrics.firstResultLinkMs,
+ firstResolvedSummaryMs: domMetrics.firstResolvedSummaryMs,
+ firstSummaryStateMs: domMetrics.firstSummaryStateMs,
+ lastCompletedRequestMs: completedRequests.length
+ ? Math.max(...completedRequests.map((request) => request.endedAtMs || 0))
+ : null,
+ pageSummaryMode: domMetrics.pageSummaryMode,
+ pagesTabActive: domMetrics.pagesTabActive,
+ peakSummaryConcurrency,
+ renderedSummaries: domMetrics.renderedSummaries,
+ searchIndexEncodedBytes,
+ // Approximate time from the last searchindex.js byte arriving to the
+ // index becoming usable by the page.
+ searchIndexReadyAfterResponseMs: (
+ typeof domMetrics.searchIndexReadyMs === "number"
+ && typeof searchIndexResponseEndMs === "number"
+ )
+ ? domMetrics.searchIndexReadyMs - searchIndexResponseEndMs
+ : null,
+ searchIndexReadyMs: domMetrics.searchIndexReadyMs,
+ searchIndexRequestStartMs,
+ searchIndexRequests: searchIndexRequests.length,
+ searchIndexResponseEndMs,
+ searchIndexTransferMs: (
+ typeof searchIndexRequestStartMs === "number"
+ && typeof searchIndexResponseEndMs === "number"
+ )
+ ? searchIndexResponseEndMs - searchIndexRequestStartMs
+ : null,
+ successfulSummaryRequests: successfulSummaryRequests.length,
+ summaryBytes,
+ summaryPlaceholders: domMetrics.summaryPlaceholders,
+ summaryRequests: summaryRequests.length,
+ summaryRequestsFailed: failedSummaryRequests.length,
+ summaryRequestsPending: summaryRequests.filter((request) => request.pending).length,
+ textResults: domMetrics.textResults,
+ totalBytes,
+ totalRequests: requests.length,
+ totalResults: domMetrics.totalResults,
+ };
+};
+
+const summarizeNumericObjectSet = (objects) => {
+ const numericObjects = objects.filter((object) => object && typeof object === "object");
+ if (numericObjects.length === 0) return null;
+
+ const numericKeys = new Set();
+ numericObjects.forEach((numericObject) => {
+ Object.entries(numericObject).forEach(([key, value]) => {
+ if (typeof value === "number" && Number.isFinite(value)) numericKeys.add(key);
+ });
+ });
+
+ const median = {};
+ const minimum = {};
+ const maximum = {};
+
+ [...numericKeys].sort().forEach((key) => {
+ const values = numericObjects
+ .map((numericObject) => numericObject[key])
+ .filter((value) => typeof value === "number" && Number.isFinite(value))
+ .sort((left, right) => left - right);
+ if (values.length === 0) return;
+ const middle = Math.floor(values.length / 2);
+ median[key] = values.length % 2 === 0
+ ? (values[middle - 1] + values[middle]) / 2
+ : values[middle];
+ minimum[key] = values[0];
+ maximum[key] = values[values.length - 1];
+ });
+
+ return {
+ maximum,
+ median,
+ minimum,
+ sampleCount: numericObjects.length,
+ };
+};
+
+const summarizeRuns = (runs) => {
+ const metricSummary = summarizeNumericObjectSet(runs.map((run) => run.metrics));
+ const appTimingSummary = summarizeNumericObjectSet(
+ runs.map((run) => run.appTimings?.timingsMs),
+ );
+ const resultKindCountSummary = summarizeNumericObjectSet(
+ runs.map((run) => run.domMetrics?.resultKindCounts),
+ );
+ const appResultCountSummary = summarizeNumericObjectSet(
+ runs.map((run) => run.appTimings?.resultCounts),
+ );
+ const cdpMetricSummary = summarizeNumericObjectSet(
+ runs.map((run) => run.cdpMetrics),
+ );
+ const recoveryFailureMetricSummary = summarizeNumericObjectSet(
+ runs.map((run) => run.recovery?.failurePhase?.metrics),
+ );
+ const recoveryRecoveryMetricSummary = summarizeNumericObjectSet(
+ runs.map((run) => run.recovery?.recoveryPhase?.metrics),
+ );
+
+ const firstRun = [...runs]
+ .sort((left, right) => left.run - right.run)[0] || null;
+ const scenarioNoOpCount = runs.filter((run) => run.scenarioNoOp).length;
+ const actions = [...new Set(runs.flatMap((run) => run.actions || []))].sort();
+ const availableResultKinds = [...new Set(
+ runs.flatMap((run) => run.domMetrics?.availableResultKinds || []),
+ )].sort();
+
+ return {
+ actions,
+ allRunsNoOp: runs.length > 0 && scenarioNoOpCount === runs.length,
+ availableResultKinds,
+ firstRun: firstRun
+ ? {
+ actions: firstRun.actions || [],
+ appTimings: firstRun.appTimings || null,
+ cdpMetrics: firstRun.cdpMetrics || null,
+ domMetrics: firstRun.domMetrics || null,
+ metrics: firstRun.metrics,
+ notes: firstRun.notes,
+ recovery: firstRun.recovery || null,
+ run: firstRun.run,
+ scenarioNoOp: Boolean(firstRun.scenarioNoOp),
+ variantDetected: firstRun.variantDetected,
+ }
+ : null,
+ appResultCountSummary,
+ appTimingSummary,
+ cdpMetricSummary,
+ maximum: metricSummary ? metricSummary.maximum : {},
+ median: metricSummary ? metricSummary.median : {},
+ minimum: metricSummary ? metricSummary.minimum : {},
+ recoveryFailureMetricSummary,
+ recoveryRecoveryMetricSummary,
+ resultKindCountSummary,
+ scenarioNoOpCount,
+ runCount: runs.length,
+ };
+};
+
+const buildSummary = (results) => {
+ const scenarios = {};
+ results.forEach((result) => {
+ if (!scenarios[result.scenario]) scenarios[result.scenario] = {};
+ if (!scenarios[result.scenario][result.variant]) scenarios[result.scenario][result.variant] = [];
+ scenarios[result.scenario][result.variant].push(result);
+ });
+
+ const summary = {};
+ Object.entries(scenarios).forEach(([scenario, variants]) => {
+ summary[scenario] = {};
+ Object.entries(variants).forEach(([variant, runs]) => {
+ summary[scenario][variant] = summarizeRuns(runs);
+ });
+
+ if (summary[scenario].stock && summary[scenario].hard) {
+ const delta = {};
+ const stockMedian = summary[scenario].stock.median;
+ const hardMedian = summary[scenario].hard.median;
+ const keys = new Set([...Object.keys(stockMedian), ...Object.keys(hardMedian)]);
+ [...keys].sort().forEach((key) => {
+ if (typeof stockMedian[key] !== "number" || typeof hardMedian[key] !== "number") return;
+ delta[key] = hardMedian[key] - stockMedian[key];
+ });
+ summary[scenario].deltaHardMinusStock = delta;
+ }
+ });
+
+ return summary;
+};
+
+const buildAutoOutputPath = async (outputDirectory, options) => {
+ await fs.mkdir(outputDirectory, { recursive: true });
+ const entries = await fs.readdir(outputDirectory, { withFileTypes: true });
+ let maxSequence = 0;
+
+ entries.forEach((entry) => {
+ if (!entry.isFile()) return;
+ const match = entry.name.match(/^(\d+)-/u);
+ if (!match) return;
+ const parsed = Number.parseInt(match[1], 10);
+ if (Number.isFinite(parsed)) {
+ maxSequence = Math.max(maxSequence, parsed);
+ }
+ });
+
+ const parts = [
+ sanitizeSlugPart(options.variant),
+ sanitizeSlugPart(options.query),
+ ];
+ if (options.networkProfile !== "none") parts.push(options.networkProfile);
+ if (options.summaryDelayMs > 0) parts.push(`delay-${options.summaryDelayMs}ms`);
+ if (options.summaryFailRate > 0) parts.push(`fail-${Math.round(options.summaryFailRate * 100)}pct`);
+ if (options.pageSummaryMode !== "limited") parts.push(`pages-${options.pageSummaryMode}`);
+
+ const sequence = String(maxSequence + 1).padStart(DEFAULT_OUTPUT_SEQUENCE_WIDTH, "0");
+ return path.join(outputDirectory, `${sequence}-${parts.join("-")}.json`);
+};
+
+const runScenario = async ({
+ browser,
+ options,
+ scenario,
+ variant,
+}) => {
+ const baseUrl = ensureBaseUrl(variant.url);
+ const searchUrl = buildSearchUrl(baseUrl, options.query);
+ const context = await browser.newContext({
+ viewport: { width: 1440, height: 900 },
+ });
+ const page = await context.newPage();
+ await addBenchmarkInitScript(page);
+
+ const client = await context.newCDPSession(page);
+ await client.send("Network.enable");
+ await client.send("Network.setCacheDisabled", { cacheDisabled: true });
+ if (options.collectCdpMetrics) {
+ await client.send("Performance.enable");
+ }
+
+ const networkProfile = NETWORK_PROFILES[options.networkProfile];
+ if (networkProfile) {
+ await client.send("Network.emulateNetworkConditions", networkProfile);
+ }
+
+ const tracking = {
+ inflightSummaryRequests: 0,
+ peakSummaryConcurrency: 0,
+ requests: new Map(),
+ wallStart: Date.now(),
+ };
+ const injection = {
+ delayMs: options.summaryDelayMs,
+ failRate: options.summaryFailRate,
+ };
+
+ client.on("Network.requestWillBeSent", (event) => {
+ const request = {
+ encodedDataLength: 0,
+ endedAtMs: null,
+ failed: false,
+ isSearchIndex: event.request.url.endsWith("/searchindex.js"),
+ isSummary: isSummaryRequest(event.request.url, searchUrl),
+ method: event.request.method,
+ requestId: event.requestId,
+ resourceType: event.type || "Other",
+ startedAtMs: Date.now() - tracking.wallStart,
+ status: null,
+ url: event.request.url,
+ };
+ tracking.requests.set(event.requestId, request);
+ if (request.isSummary) {
+ tracking.inflightSummaryRequests += 1;
+ tracking.peakSummaryConcurrency = Math.max(
+ tracking.peakSummaryConcurrency,
+ tracking.inflightSummaryRequests,
+ );
+ }
+ });
+
+ client.on("Network.responseReceived", (event) => {
+ const request = tracking.requests.get(event.requestId);
+ if (!request) return;
+ request.status = event.response.status;
+ });
+
+ const finishRequest = (event, extra = {}) => {
+ const request = tracking.requests.get(event.requestId);
+ if (!request) return;
+ request.encodedDataLength = extra.encodedDataLength || request.encodedDataLength || 0;
+ request.endedAtMs = Date.now() - tracking.wallStart;
+ if (extra.errorText) request.errorText = extra.errorText;
+ if (extra.failed) request.failed = true;
+ if (request.isSummary) {
+ tracking.inflightSummaryRequests = Math.max(0, tracking.inflightSummaryRequests - 1);
+ }
+ };
+
+ client.on("Network.loadingFinished", (event) => {
+ finishRequest(event, { encodedDataLength: event.encodedDataLength || 0 });
+ });
+
+ client.on("Network.loadingFailed", (event) => {
+ finishRequest(event, {
+ errorText: event.errorText || null,
+ failed: true,
+ });
+ });
+
+ if (options.summaryDelayMs > 0 || options.summaryFailRate > 0) {
+ await page.route("**/*", async (route) => {
+ const requestUrl = route.request().url();
+ if (!isSummaryRequest(requestUrl, searchUrl)) {
+ await route.continue();
+ return;
+ }
+
+ if (injection.delayMs > 0) {
+ await wait(injection.delayMs);
+ }
+
+ if (injection.failRate > 0 && Math.random() < injection.failRate) {
+ await route.fulfill({
+ body: "benchmark injected failure\n",
+ contentType: "text/plain",
+ status: 503,
+ });
+ return;
+ }
+
+ await route.continue();
+ });
+ }
+
+ const notes = [];
+ const actions = [];
+ let scenarioNoOp = false;
+
+ const capturePhase = async (pageVariant, collectCdp, phaseNotes) => {
+ let appTimings = null;
+ if (options.collectAppTimings) {
+ appTimings = await collectAppTimings(page);
+ if (!appTimings && pageVariant === "advanced") {
+ phaseNotes.push("app timings unavailable");
+ }
+ }
+
+ let cdpMetrics = null;
+ if (collectCdp && options.collectCdpMetrics) {
+ cdpMetrics = await collectCdpPerformanceMetrics(client);
+ }
+
+ const domMetrics = await collectDomMetrics(page);
+ const requestSnapshot = snapshotRequests(tracking.requests);
+ return {
+ appTimings,
+ cdpMetrics,
+ domMetrics,
+ metrics: computeMetrics({
+ domMetrics,
+ peakSummaryConcurrency: tracking.peakSummaryConcurrency,
+ requests: requestSnapshot,
+ }),
+ requestCount: requestSnapshot.length,
+ requests: requestSnapshot,
+ variant: pageVariant,
+ };
+ };
+
+ const runObservedNavigation = async ({
+ enableScroll = false,
+ openPages = false,
+ collectCdp = true,
+ phaseLabel = "",
+ } = {}) => {
+ resetTrackingState(tracking);
+ await page.goto(searchUrl.href, {
+ timeout: options.navigationTimeoutMs,
+ waitUntil: "domcontentloaded",
+ });
+ await waitForInitialResults(page, options.initialResultsTimeoutMs);
+
+ const pageVariant = await detectPageVariant(page);
+ const phaseNotes = [];
+ const phaseActions = [];
+ let phaseNoOp = false;
+
+ if (variant.expectedPageVariant && pageVariant !== variant.expectedPageVariant) {
+ phaseNotes.push(`expected ${variant.expectedPageVariant}, got ${pageVariant}`);
+ }
+
+ if (openPages) {
+ const action = await selectPagesViewIfPresent(page, pageVariant, options.pageSummaryMode);
+ phaseActions.push(action.action);
+ phaseNotes.push(action.note);
+ if (!action.effective) phaseNoOp = true;
+ }
+
+ if (enableScroll) {
+ await scrollSearchPage(page, options.scrollDurationMs, pageVariant);
+ phaseActions.push("scrolled-page");
+ phaseNotes.push(`scrolled for ${options.scrollDurationMs}ms`);
+ }
+
+ await page.waitForTimeout(options.observationMs);
+ const phase = await capturePhase(pageVariant, collectCdp, phaseNotes);
+ if (phaseLabel) {
+ phase.phase = phaseLabel;
+ }
+ phase.actions = phaseActions;
+ phase.notes = phaseNotes;
+ phase.scenarioNoOp = phaseNoOp;
+ return phase;
+ };
+
+ try {
+ if (scenario === "recover-pages") {
+ if (options.summaryFailRate <= 0) {
+ notes.push("recover-pages is most useful with --summary-fail-rate > 0");
+ }
+
+ // Exercise fresh-navigation recovery: first load with injected summary
+ // failures, then reload the same query cleanly to verify recovery behavior.
+ const failurePhase = await runObservedNavigation({
+ openPages: true,
+ collectCdp: false,
+ phaseLabel: "failure",
+ });
+ actions.push(...failurePhase.actions.map((action) => `failure:${action}`));
+ notes.push(...failurePhase.notes.map((note) => `failure: ${note}`));
+
+ injection.delayMs = 0;
+ injection.failRate = 0;
+ actions.push("reloaded-page-clean");
+ notes.push("reloaded same query with summary failure injection disabled");
+
+ const recoveryPhase = await runObservedNavigation({
+ openPages: true,
+ collectCdp: options.collectCdpMetrics,
+ phaseLabel: "recovery",
+ });
+ actions.push(...recoveryPhase.actions.map((action) => `recovery:${action}`));
+ notes.push(...recoveryPhase.notes.map((note) => `recovery: ${note}`));
+
+ scenarioNoOp = Boolean(failurePhase.scenarioNoOp && recoveryPhase.scenarioNoOp);
+ return {
+ actions,
+ appTimings: recoveryPhase.appTimings,
+ cdpMetrics: recoveryPhase.cdpMetrics,
+ domMetrics: recoveryPhase.domMetrics,
+ metrics: recoveryPhase.metrics,
+ notes,
+ recovery: {
+ failurePhase: {
+ actions: failurePhase.actions,
+ appTimings: failurePhase.appTimings,
+ cdpMetrics: failurePhase.cdpMetrics,
+ domMetrics: failurePhase.domMetrics,
+ metrics: failurePhase.metrics,
+ notes: failurePhase.notes,
+ requestCount: failurePhase.requestCount,
+ variantDetected: failurePhase.variant,
+ },
+ recoveryPhase: {
+ actions: recoveryPhase.actions,
+ appTimings: recoveryPhase.appTimings,
+ cdpMetrics: recoveryPhase.cdpMetrics,
+ domMetrics: recoveryPhase.domMetrics,
+ metrics: recoveryPhase.metrics,
+ notes: recoveryPhase.notes,
+ requestCount: recoveryPhase.requestCount,
+ variantDetected: recoveryPhase.variant,
+ },
+ },
+ requestCount: recoveryPhase.requestCount,
+ scenarioNoOp,
+ variant: recoveryPhase.variant,
+ };
+ }
+
+ const phase = await runObservedNavigation({
+ enableScroll: scenario === "scroll-pages",
+ openPages: scenario === "open-pages" || scenario === "scroll-pages",
+ collectCdp: options.collectCdpMetrics,
+ });
+ actions.push(...phase.actions);
+ notes.push(...phase.notes);
+ scenarioNoOp = phase.scenarioNoOp;
+
+ return {
+ actions,
+ appTimings: phase.appTimings,
+ cdpMetrics: phase.cdpMetrics,
+ domMetrics: phase.domMetrics,
+ metrics: phase.metrics,
+ notes,
+ requestCount: phase.requestCount,
+ scenarioNoOp,
+ variant: phase.variant,
+ };
+ } finally {
+ await context.close();
+ }
+};
+
+const main = async () => {
+ const options = parseArgs(process.argv.slice(2));
+ if (options.help) {
+ process.stdout.write(HELP);
+ return;
+ }
+
+ const { chromium, resolvedSpecifier } = await loadPlaywright(options.playwrightModule);
+ const browser = await chromium.launch({ headless: !options.headed });
+
+ try {
+ const variant = {
+ expectedPageVariant: options.expectedPageVariant,
+ name: options.variant,
+ url: options.url,
+ };
+
+ const results = [];
+ for (const scenario of options.scenarios) {
+ for (let runIndex = 0; runIndex < options.runs; runIndex += 1) {
+ console.error(
+ `[bench] scenario=${scenario} variant=${variant.name} run=${runIndex + 1}/${options.runs}`,
+ );
+ const run = await runScenario({
+ browser,
+ options,
+ scenario,
+ variant,
+ });
+ results.push({
+ actions: run.actions,
+ appTimings: run.appTimings,
+ cdpMetrics: run.cdpMetrics,
+ domMetrics: run.domMetrics,
+ metrics: run.metrics,
+ notes: run.notes,
+ recovery: run.recovery || null,
+ run: runIndex + 1,
+ scenario,
+ scenarioNoOp: run.scenarioNoOp,
+ url: variant.url,
+ variant: variant.name,
+ variantDetected: run.variant,
+ });
+ }
+ }
+
+ const report = {
+ generatedAt: new Date().toISOString(),
+ options: {
+ collectAppTimings: options.collectAppTimings,
+ collectCdpMetrics: options.collectCdpMetrics,
+ expectedPageVariant: options.expectedPageVariant || null,
+ initialResultsTimeoutMs: options.initialResultsTimeoutMs,
+ networkProfile: options.networkProfile,
+ navigationTimeoutMs: options.navigationTimeoutMs,
+ observationMs: options.observationMs,
+ playwrightModule: resolvedSpecifier,
+ query: options.query,
+ runs: options.runs,
+ scenarios: options.scenarios,
+ scrollDurationMs: options.scrollDurationMs,
+ summaryDelayMs: options.summaryDelayMs,
+ summaryFailRate: options.summaryFailRate,
+ url: options.url,
+ variant: options.variant,
+ },
+ results,
+ summary: buildSummary(results),
+ };
+
+ const serialized = `${JSON.stringify(report, null, 2)}\n`;
+ const outputPath = options.outputDir
+ ? await buildAutoOutputPath(options.outputDir, options)
+ : options.output;
+
+ if (outputPath) {
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
+ await fs.writeFile(outputPath, serialized, "utf-8");
+ console.error(`[bench] wrote ${outputPath}`);
+ } else {
+ process.stdout.write(serialized);
+ }
+ } finally {
+ await browser.close();
+ }
+};
+
+main().catch((error) => {
+ console.error(error.message || error);
+ process.exit(1);
+});
--
2.51.0
^ permalink raw reply related
* [PATCH v3 1/2] docs: add advanced search for kernel documentation
From: Rito Rhymes @ 2026-04-04 7:34 UTC (permalink / raw)
To: corbet, skhan; +Cc: linux-doc, linux-kernel, Rito Rhymes
In-Reply-To: <20260404073413.32309-1-rito@ritovision.com>
Replace the stock search page with a kernel-specific search UI
that still builds on Sphinx-generated search data.
Results are grouped into Symbols, Sections, Index entries, and
Pages. The advanced panel adds filters for documentation area,
object type, result kind, and exact identifier matching while
preserving shareable URL state. Result kinds are presented as tabs
so broad searches can switch between them without scrolling through
large sections.
Page summaries are fetched only for the Pages result kind. The
default view limits summaries to the first 50 page results while
allowing a per-session opt-out for deeper summary loading. Summary
loading is deferred to the active Pages view, and stale work is
canceled when the query or active result kind changes.
The sidebar search template is replaced with a matching quick-search
entry point that links to the advanced search page.
The runtime includes compatibility handling for the supported Sphinx
range, including older search-index layouts and pre-9.x stopword
APIs. On Sphinx 3.4.3 through 5.1.x, advanced search degrades
gracefully to Symbols and Pages because those versions do not emit
the section and index metadata available in 5.2.0 and newer.
A lightweight build-time smoke test is included for the generated
search page, copied runtime asset, and search-index contract.
Signed-off-by: Rito Rhymes <rito@ritovision.com>
Assisted-by: Codex:GPT-5.4
Assisted-by: Claude:Opus-4.6
---
Summary
=======
This feature adds an Advanced Search interface for kernel
documentation. It is intended to provide a more capable complement to
regex and grep than the existing Quick Search interface, while still
building on the same underlying Sphinx search primitives.
It organizes results into Symbols, Sections, Index entries, and Pages,
while also exposing filters and more targeted identifier search.
The implementation stays close to the existing Sphinx search model. It
reuses existing Sphinx-generated search artifacts and adds a client-side
interface around them, rather than introducing a separate indexing
pipeline, non-Sphinx search backend, or additional build-time
infrastructure.
It is intended to remain broadly compatible with supported Sphinx
builds and to work with both in-tree and out-of-tree documentation
builds.
It also changes the runtime cost shape of documentation search. In the
default view, advanced search renders grouped results without triggering
page-summary fetches, and in limited Pages mode it loads only the
rendered page summaries, up to 50. On broader queries, that
substantially reduces request volume and transferred bytes compared with
the stock Quick Search page.
Problem and Purpose
===================
The need for this feature can be understood through this context
sequence:
1. Kernel contributors and documentation readers need to navigate a
very large body of material spread across many subsystems, APIs,
generated reference pages, and long-form documents, often without
already knowing which page contains the thing they need.
2. Regex and grep are powerful for precise text hunting in a local
source tree, but they are not ideal for finding the right rendered
documentation page, section, or symbol when the user does not
already know where to look.
3. The existing quick search provides a useful lightweight entry point,
but it does not expose richer filtering, result grouping, or more
targeted identifier search workflows. On broader queries, it also
scales page-summary loading work with result breadth, which makes
the experience both noisier and more resource-intensive than it
needs to be.
4. Maintenance of complex front-end systems is a higher barrier in this
environment because there are fewer contributors focused on front-end
implementation details. For that reason, safer solutions should stay
within the existing documentation infrastructure rather than
introducing additional dependencies or separate supporting systems.
Taken together, these constraints favor a solution that improves
navigation within rendered documentation while remaining within the
existing documentation infrastructure and search model.
Design Goals
============
- Expose more structured search workflows than Quick Search.
- Keep the implementation aligned with established documentation
architecture to limit maintenance and onboarding overhead.
- Remain easy to disable or roll back if needed.
- Allow for future filter adjustment or expansion without restructuring
the system.
- Use standard HTML form semantics rather than custom widget frameworks.
- Keyboard navigation and standard accessible form semantics with no
custom focus management.
- Maintain broad Sphinx version compatibility.
- Work with both in-tree and out-of-tree documentation builds.
Anti-goals
==========
- Replacing regex or grep.
- Introducing a separate search backend.
- Adding new build-time dependencies.
- Altering build-time processes
- Adding low-value bells and whistles that increase complexity.
- Redesigning the theme.
- Redesigning the sidebar search.
Design & Implementation
=======================
Architecture Overview
---------------------
This feature is implemented by extending the Sphinx documentation setup
already used by the kernel tree, rather than by introducing a separate
search backend or indexing pipeline.
Sphinx already provides several useful extension points for this kind of
feature. It allows template overrides through the configured template
path, static asset overrides through the configured static path, and it
generates search artifacts such as `searchindex.js` and
`language_data.js` as part of the normal documentation build.
Those existing hooks define the basic boundaries of the implementation.
The feature can replace or extend the rendered search UI, add client-side
behavior, and reuse Sphinx-generated search data, but it does not create
its own independent search index or separate documentation build system.
Baseline Search Structure
-------------------------
The default Sphinx setup already provides a basic quick-search entry
point and the generated search artifacts needed for client-side
searching.
In the kernel documentation build, this means the existing setup already
exposes:
* a sidebar quick-search box
* a generated `searchindex.js` file
* generated `language_data.js` search-language support
* the stock Sphinx search page and initialization model
What it does not provide is a richer search workflow tailored to kernel
documentation navigation. In particular, the default flow does not
provide grouped results, richer filtering, or a dedicated search
interface for navigating symbols, sections, index entries, and pages as
distinct result kinds.
Template Integration
--------------------
The feature is integrated through two Sphinx template overrides:
* `Documentation/sphinx/templates/searchbox.html`
* `Documentation/sphinx/templates/search.html`
`searchbox.html` serves as the sidebar entry point. It is effectively a
close copy of the theme's default quick-search component, but adds a
dedicated link to `search.html?advanced=1` so the full search page opens
with the advanced filters enabled.
`search.html` provides the full Advanced Search page. It defines the
search query field, the advanced filter controls, and the results
container that is later populated by
`Documentation/sphinx-static/kernel-search.js`.
The feature is also trivial to rollback. Because the integration is
implemented as template overrides, rollback only requires deleting
`searchbox.html` and `search.html`. That restores the original default
Sphinx theme templates (including Quick Search) without untangling
architecture, altering configuration, or reverting history.
User Interface and Styling
--------------------------
The user-facing interface is defined in `search.html` and styled in
`Documentation/sphinx-static/custom.css`.
The markup in `search.html` is a shared contract with
`kernel-search.js` and `custom.css`. The JavaScript runtime depends on
specific form IDs and result container IDs, while the CSS depends on the
corresponding classes. Changes to the template markup therefore need to
be made with both files in mind.
At a high level, the page provides:
* a search query field
* a submit action and progress/status area
* a collapsible advanced-filter panel
* result-kind filters
* documentation-area and object-type filters
* a runtime-rendered tabbed results interface
* a Pages summary-loading mode toggle
That runtime-rendered results interface is built by
`kernel-search.js`. It renders result tabs for Symbols, Sections, Index
entries, and Pages, along with per-kind result panels. The Pages tab
includes its own summary-loading mode control so the user can switch
between the normal limited mode and a broader unbounded mode directly
within that view when needed.
The CSS in `custom.css` is responsible for the layout and responsive
presentation of the search interface. In particular, it supports the
tabbed results UI and keeps the result tabs sticky so users can switch
between result kinds at any time while searching, even when the result
lists are long.
The templates use standard accessible form semantics, including labeled
controls, grouped filter fields, and a collapsible advanced-filter
panel.
JavaScript Runtime Flow
-----------------------
The client-side runtime is implemented in
`Documentation/sphinx-static/kernel-search.js`.
At a high level, the flow is:
1. `search.html` includes `_static/language_data.js`,
`kernel-search.js`, and the generated `searchindex.js`.
2. `kernel-search.js` waits for `searchindex.js` to populate the shared
`Search` index.
3. The runtime reads query state from the page and URL parameters.
4. The query is normalized and prepared for matching.
5. The runtime searches across Sphinx-generated index structures and
collects matches as distinct result kinds: Symbols, Sections, Index
entries, and Pages.
6. The collected matches are scored, grouped, filtered, and rendered
into the results container in `search.html`.
7. For page results, summaries may be fetched lazily from generated HTML
pages after the initial result set is rendered.
Page Summaries and Runtime Cost
-------------------------------
The most important runtime distinction from the stock search page is how
page summaries are loaded.
In the default advanced-search view, grouped results are rendered
without triggering page-summary fetches. Page summaries are fetched only
when the user switches to the Pages tab.
In that Pages view, the normal mode is intentionally limited. It loads
only the rendered page summaries, up to a maximum of 50. For smaller
queries, that means loading all rendered page summaries when fewer than
50 exist. For broader queries, that keeps page-summary work bounded
instead of scaling with the full result set.
This means the feature does not remove the shared Sphinx startup and
search-index load cost. The main runtime change happens after initial
results are available: the advanced interface reduces the amount of
follow-on summary-loading work, especially for broader queries.
Extensibility
-------------
The current structure is intended to support incremental changes within
the existing search model.
In practice, that means it is straightforward to:
- add or adjust filters in ``search.html``
- add corresponding styling in ``custom.css``
- extend query handling and result collection in ``kernel-search.js``
- tune ranking and grouping behavior within the existing client-side
runtime
This makes the feature reasonably extensible for incremental search UI
and result-model improvements, while still keeping the implementation
contained within the existing Sphinx template and static-asset model.
Sphinx Version Compatibility
----------------------------
Sphinx version compatibility is handled in `kernel-search.js` by
accounting for the meaningful differences in generated search data
across supported versions.
The main compatibility differences are:
* stopword representation
* object index layout
* availability of section and index-entry metadata
Sphinx 3.4.3 through 8.2.3 expose stopwords in a form that behaves like
an array. Sphinx 9.1.0 exposes stopwords in a form that behaves like a
set. The runtime includes compatibility handling for both
representations.
The generated object index also differs across versions. Sphinx 3.4.3
uses a name-to-tuple mapping for object entries, while Sphinx 4.0 and
newer use an array-based layout. The runtime supports both forms when
collecting Symbol results.
The most visible functional difference is that Sphinx 3.4.3 through
5.1.x do not emit the `alltitles` and `indexentries` metadata used to
populate Sections and Index entries. Because of that, that range
provides partial support: Symbols and Pages still work, but expect
fewer total results because Sections and Index entries are unavailable.
Sphinx 5.2.x and newer provide full support.
Compatibility
=============
This feature works with both in-tree and out-of-tree documentation
builds.
The interface requires JavaScript at runtime.
Sphinx 5.2 and newer provide full support.
Sphinx 3.4.3 through 5.1.x provide partial support. In that range, the
feature remains usable, but older Sphinx builds do not emit the
metadata needed for Sections and Index entries. As a result, expect
fewer total results, with only Symbols and Pages available.
This behavior was directly validated on:
- Sphinx 9.1.0
- Sphinx 8.2.3
- Sphinx 3.4.3 on Python 3.9
For example, searching for `futex` under full support returns 280
results. On Sphinx 3.4.3, the same search returns 223 results: 200
Symbols and 23 Pages, with no Sections or Index entries.
This runtime behavior was benchmarked on Sphinx 9.1.0 against stock
Quick Search using the queries `kernel`, `futex`, and `landlock`.
Across that set, advanced search kept the default path flat at zero
page-summary requests and 12 total requests, while stock summary work
grew with query breadth. In limited Pages mode, advanced search loaded
10 page summaries for `landlock`, 23 for `futex`, and 50 for `kernel`.
Trade-offs
==========
This feature accepts some additional template, CSS, and JavaScript
complexity in exchange for a more structured and useful search workflow
than the existing Quick Search interface can provide.
At the same time, it avoids a much larger architectural jump. Rather
than introducing a separate indexing pipeline, non-Sphinx backend, or
additional build-time infrastructure, it stays within the existing
Sphinx search model and reuses Sphinx-generated search artifacts.
That design keeps the feature more contained, easier to integrate, and
trivial to rollback, but it also means the feature remains bounded by
the data that Sphinx already emits.
One explicit compatibility trade-off is that Sphinx 3.4.3 through 5.1.x
provide only partial support. Supporting those versions fully would
require additional compatibility logic for metadata they do not emit.
Instead, the feature accepts degraded results in that range while
keeping full support on newer Sphinx versions.
This is not a universal first-summary latency win. On broad queries such
as `kernel`, the stock search page can reach a first resolved summary
sooner, but it does so by issuing far more requests and transferring far
more data. The performance advantage of advanced search is that it
reduces summary-loading work and keeps that work bounded as query
breadth increases.
So the overall trade-off shape is:
- more capability than Quick Search, but more complexity to maintain
- less complexity than a separate search architecture, and lower
maintenance and learning overhead because it stays within the
existing Sphinx infrastructure.
- broad support across Sphinx versions, but not uniform support on
earlier versions to avoid greater maintenance complexity.
- another subsystem to maintain, but trivial to rollback if needed.
Maintenance and Risk Considerations
===================================
The main maintenance burden comes from the fact that this feature is a
coordinated template/CSS/JavaScript unit rather than a single isolated
change. `search.html`, `searchbox.html`, `custom.css`, and
`kernel-search.js` depend on a shared contract of markup structure, IDs,
and classes. Changes in one of those files may require corresponding
changes in the others.
A second maintenance risk is Sphinx-version drift. The feature relies on
Sphinx-generated search data and therefore depends on the structure and
availability of that data remaining compatible enough for the runtime to
interpret it correctly. The main known variation points are stopword
representation, object index layout, and metadata availability for
Sections and Index entries. Future Sphinx releases may require renewed
validation or additional compatibility handling.
The feature also depends on generated HTML structure when deriving page
summaries. If the relevant page markup changes, summary extraction may
degrade even if the underlying search index still works correctly. That
makes summary behavior one of the more fragile parts of the runtime.
Theme and layout changes are another practical risk. The feature is
integrated through kernel-local template overrides and styling, so
changes to surrounding layout expectations may require corresponding
adjustments to searchbox integration, page structure, or responsive
presentation.
These risks are mitigated by keeping the feature within the existing
Sphinx extension points, avoiding new build-time dependencies or a
separate search backend, validating behavior across supported Sphinx
versions with the smoke test in `tools/docs/test_advanced_search.py`
and the developer-side benchmark harness in
`tools/docs/bench_search_playwright.mjs`, and keeping rollback simple
through removal of the template overrides and static assets. The
benchmark harness is a developer tool for validation and comparison,
not a dependency of the documentation build itself.
v3:
- Reworked the results UI from collapsible groups to a tabbed interface.
- Added sticky result tabs so result-kind switching remains available
while browsing long result lists.
- Added a Pages summary-loading mode control to the Pages view.
- Hardened Pages summary loading so it starts only when the Pages view
is opened, reuses fetched page text within the page session, applies
bounded concurrent loading, and supports a limited mode that caps
rendered page summaries at 50.
- Clarified and expanded Sphinx compatibility handling, including
graceful degraded behavior on older builds.
- Added a build-time smoke test for advanced-search assets and search
page wiring.
- Added integration, compatibility, testing, and rollback
documentation.
- Added a MAINTAINERS entry for the main feature file.
Documentation/doc-guide/sphinx.rst | 71 +
Documentation/sphinx-static/custom.css | 288 ++++
Documentation/sphinx-static/kernel-search.js | 1182 +++++++++++++++++
Documentation/sphinx/templates/search.html | 117 ++
Documentation/sphinx/templates/searchbox.html | 30 +
MAINTAINERS | 10 +
tools/docs/test_advanced_search.py | 312 +++++
7 files changed, 2010 insertions(+)
create mode 100644 Documentation/sphinx-static/kernel-search.js
create mode 100644 Documentation/sphinx/templates/search.html
create mode 100644 Documentation/sphinx/templates/searchbox.html
create mode 100755 tools/docs/test_advanced_search.py
diff --git a/Documentation/doc-guide/sphinx.rst b/Documentation/doc-guide/sphinx.rst
index 51c370260..6f71192eb 100644
--- a/Documentation/doc-guide/sphinx.rst
+++ b/Documentation/doc-guide/sphinx.rst
@@ -194,6 +194,77 @@ subdirectories you can specify.
To remove the generated documentation, run ``make cleandocs``.
+Advanced Search Integration
+---------------------------
+
+The advanced search integration is a kernel-specific Sphinx
+customization that overrides the stock search page and sidebar
+quick-search template to provide a more capable search interface for
+kernel documentation while still building on the same underlying
+Sphinx search primitives.
+
+The advanced search integration is implemented by three files:
+
+* ``Documentation/sphinx/templates/search.html`` for the custom search page;
+* ``Documentation/sphinx/templates/searchbox.html`` for the sidebar link;
+* ``Documentation/sphinx-static/kernel-search.js`` for the client-side
+ search runtime.
+
+The page relies on a strict boot order. ``language_data.js`` must load
+before ``kernel-search.js``, and ``searchindex.js`` must load afterwards
+to populate the shared search index consumed by the runtime.
+
+The runtime works with all supported Sphinx versions, but the available
+result kinds differ by the metadata present in ``searchindex.js``:
+
+* Sphinx ``3.4.3`` through ``5.1.x`` provide ``Symbols`` and ``Pages``.
+ ``Sections`` and ``Index entries`` are unavailable because those
+ versions do not emit the ``alltitles`` and ``indexentries`` fields.
+* Sphinx ``5.2.0`` and newer provide the full result set: ``Symbols``,
+ ``Sections``, ``Index entries``, and ``Pages``.
+
+Runtime behavior is intentionally bounded, but still fully client-side:
+
+* ``searchindex.js`` is loaded into the browser before any query can run,
+ and broad full-site builds can still increase page-load and query latency.
+* The ``Pages`` group starts collapsed. Page-summary fetches begin only
+ after the user opens that group.
+* Page summaries are fetched on demand after the user opens the
+ ``Pages`` group. Browsers with ``IntersectionObserver`` keep that work
+ near the viewport, with at most four concurrent requests, an
+ eight-second timeout, and a limit of fifty summarized page results per
+ query.
+* Successful summary fetches are cached for the current page session.
+ Aborted or failed fetches are not cached, and the result remains usable
+ as a normal link even when its summary is unavailable.
+
+If full-site ``searchindex.js`` size becomes the bottleneck, that needs a
+larger follow-up design such as precomputed summary sidecars, sharded
+search data, or a server-backed search service. The client-side runtime
+described here does not solve that class of scaling limit.
+
+For a quick local regression check, run::
+
+ python3 tools/docs/test_advanced_search.py
+
+It builds a small documentation subset and verifies the generated search
+page, copied static assets, and the search index contract used by the
+client-side search runtime.
+
+To verify full-site support for a change, also run at least one full
+``make htmldocs`` build and manually exercise the generated ``search.html``
+page.
+
+To disable the feature and return to the theme's default Quick Search setup,
+remove the two template overrides:
+
+* ``Documentation/sphinx/templates/search.html``
+* ``Documentation/sphinx/templates/searchbox.html``
+
+That restores the native Sphinx search page and the default sidebar quick
+search box. ``kernel-search.js`` can then be removed as follow-up cleanup,
+but removing the two template overrides is enough to roll back behavior.
+
.. [#ink] Having ``inkscape(1)`` from Inkscape (https://inkscape.org)
as well would improve the quality of images embedded in PDF
documents, especially for kernel releases 5.18 and later.
diff --git a/Documentation/sphinx-static/custom.css b/Documentation/sphinx-static/custom.css
index db24f4344..a0c81d811 100644
--- a/Documentation/sphinx-static/custom.css
+++ b/Documentation/sphinx-static/custom.css
@@ -169,3 +169,291 @@ a.manpage {
font-weight: bold;
font-family: "Courier New", Courier, monospace;
}
+
+/* Keep the quick search box as-is and add a secondary advanced search link. */
+div.sphinxsidebar p.search-advanced-link {
+ margin: 0.5em 0 0 0;
+ font-size: 0.95em;
+}
+
+/*
+ * The enhanced search page keeps the stock GET workflow but adds
+ * filter controls and grouped results.
+ */
+form.kernel-search-form {
+ margin-bottom: 2em;
+}
+
+/* Search bar layout: query input and submit action row. */
+div.kernel-search-query-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1em;
+ align-items: end;
+}
+
+div.kernel-search-query-field {
+ flex: 1 1 26em;
+}
+
+div.kernel-search-query-field label,
+div.kernel-search-field label,
+fieldset.kernel-search-kind-filters legend {
+ display: block;
+ font-weight: bold;
+ margin-bottom: 0.35em;
+}
+
+div.kernel-search-query-field input[type="text"],
+div.kernel-search-field select {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+div.kernel-search-query-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.75em;
+}
+
+span.kernel-search-progress {
+ min-height: 1.2em;
+ color: #666;
+}
+
+/* Collapsible advanced-filter panel. */
+details.kernel-search-advanced {
+ margin-top: 1em;
+ padding: 0.75em 1em 1em 1em;
+ border: 1px solid #cccccc;
+ background: #f7f7f7;
+}
+
+details.kernel-search-advanced summary {
+ cursor: pointer;
+ font-weight: bold;
+}
+
+/* Responsive grid for advanced search filters. */
+div.kernel-search-advanced-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(16em, 1fr));
+ gap: 1em 1.25em;
+ margin-top: 1em;
+}
+
+fieldset.kernel-search-kind-filters {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+label.kernel-search-checkbox {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5em;
+ margin-bottom: 0.35em;
+}
+
+/* Tabbed result shell, cards, and lazy-loaded page summaries. */
+div.kernel-search-results {
+ margin-top: 1.5em;
+}
+
+p.kernel-search-status {
+ margin-bottom: 1.5em;
+}
+
+div.kernel-search-results-shell {
+ display: flex;
+ flex-direction: column;
+ max-height: min(78vh, 56rem);
+ border: 1px solid #d8d8d8;
+ border-radius: 0.75rem;
+ background: #fbfbfb;
+ overflow: hidden;
+}
+
+div.kernel-search-tab-strip {
+ position: relative;
+ flex: 0 0 auto;
+ border-bottom: 1px solid #dddddd;
+ background: #ffffff;
+}
+
+div.kernel-search-tab-strip::before,
+div.kernel-search-tab-strip::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 1.25rem;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 120ms ease-in-out;
+ z-index: 2;
+}
+
+div.kernel-search-tab-strip::before {
+ left: 0;
+ background: linear-gradient(90deg, rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0));
+}
+
+div.kernel-search-tab-strip::after {
+ right: 0;
+ background: linear-gradient(270deg, rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0));
+}
+
+div.kernel-search-tab-strip.has-left-shadow::before,
+div.kernel-search-tab-strip.has-right-shadow::after {
+ opacity: 1;
+}
+
+div.kernel-search-tab-scroller {
+ display: flex;
+ gap: 0.45rem;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ padding: 0.85rem 1rem 0;
+ scrollbar-width: thin;
+}
+
+div.kernel-search-panel-tools {
+ flex: 0 0 auto;
+ padding: 0.8rem 1rem 0.6rem;
+ border-bottom: 1px solid #e6e6e6;
+ background: #fbfbfb;
+}
+
+label.kernel-search-summary-limit {
+ margin-bottom: 0;
+}
+
+button.kernel-search-tab {
+ appearance: none;
+ border: 1px solid transparent;
+ border-bottom: none;
+ border-radius: 0.6rem 0.6rem 0 0;
+ background: transparent;
+ color: #444444;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: baseline;
+ gap: 0.45rem;
+ margin: 0;
+ padding: 0.7rem 0.95rem 0.65rem;
+ white-space: nowrap;
+}
+
+button.kernel-search-tab:hover {
+ background: #f3f3f3;
+}
+
+button.kernel-search-tab:focus-visible {
+ outline: 2px solid #1f6feb;
+ outline-offset: 2px;
+}
+
+button.kernel-search-tab.is-active {
+ background: #fbfbfb;
+ border-color: #d8d8d8;
+ color: #111111;
+ font-weight: bold;
+}
+
+span.kernel-search-tab-label {
+ line-height: 1.2;
+}
+
+span.kernel-search-tab-count {
+ color: #666666;
+ font-size: 0.95em;
+}
+
+div.kernel-search-panels {
+ display: flex;
+ flex: 1 1 auto;
+ min-height: 0;
+}
+
+section.kernel-search-panel {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ -webkit-overflow-scrolling: touch;
+ padding: 0.35rem 1rem 1rem;
+}
+
+section.kernel-search-panel[hidden] {
+ display: none;
+}
+
+ol.kernel-search-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+li.kernel-search-result {
+ padding: 0.9em 0;
+ border-top: 1px solid #dddddd;
+}
+
+li.kernel-search-result:first-child {
+ border-top: none;
+}
+
+div.kernel-search-result-heading {
+ font-weight: bold;
+}
+
+div.kernel-search-result-heading,
+div.kernel-search-result-heading a,
+div.kernel-search-path,
+div.kernel-search-meta,
+p.kernel-search-summary {
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+div.kernel-search-path,
+div.kernel-search-meta,
+p.kernel-search-summary {
+ margin-top: 0.3em;
+ color: #555555;
+}
+
+div.kernel-search-path,
+div.kernel-search-meta {
+ font-size: 0.95em;
+}
+
+p.kernel-search-summary {
+ margin-bottom: 0;
+}
+
+p.kernel-search-summary-status {
+ font-style: italic;
+}
+
+p.kernel-search-summary-status.is-error {
+ color: #8b0000;
+}
+
+/* Responsive layout adjustments for narrow and wide screens. */
+@media screen and (max-width: 65em) {
+ div.kernel-search-query-actions {
+ width: 100%;
+ justify-content: flex-start;
+ }
+}
+
+@media screen and (min-width: 65em) {
+ div.kernel-search-result-heading,
+ div.kernel-search-path,
+ div.kernel-search-meta,
+ p.kernel-search-summary {
+ margin-left: 2rem;
+ }
+}
diff --git a/Documentation/sphinx-static/kernel-search.js b/Documentation/sphinx-static/kernel-search.js
new file mode 100644
index 000000000..f762c4be4
--- /dev/null
+++ b/Documentation/sphinx-static/kernel-search.js
@@ -0,0 +1,1182 @@
+"use strict";
+
+// Client-side search UI for Documentation/search.html.
+//
+// This reuses Sphinx-generated language_data.js and searchindex.js,
+// groups results by kind, applies URL-driven filters, and lazily fetches
+// page text for "Pages" summaries.
+(() => {
+ const RESULT_KIND_ORDER = ["object", "title", "index", "text"];
+ const RESULT_KIND_LABELS = {
+ object: "Symbols",
+ title: "Sections",
+ index: "Index entries",
+ text: "Pages",
+ };
+ const TOP_LEVEL_AREA = "__top_level__";
+ // Search ranking policy: higher scores sort first.
+ const LABEL_MATCH_SCORES = {
+ exactMatchBonus: 20,
+ exactCandidateBonus: 10,
+ sectionBase: 15,
+ sectionPartial: 7,
+ indexBase: 20,
+ indexPartial: 8,
+ secondaryIndexPenalty: 5,
+ };
+ const OBJECT_MATCH_SCORES = {
+ exact: 120,
+ exactNameBoost: 11,
+ partialShortName: 6,
+ partialFullName: 4,
+ matchedNameTerm: 1,
+ };
+ const TEXT_MATCH_SCORES = {
+ term: 5,
+ partialTerm: 2,
+ titleTerm: 15,
+ partialTitleTerm: 7,
+ exactTitleBonus: 10,
+ };
+ // Sphinx object priorities: 0 = important, 1 = default, 2 = unimportant.
+ const OBJECT_PRIORITY = {
+ 0: 15,
+ 1: 5,
+ 2: -5,
+ };
+ const SUMMARY_FETCH_BUDGET = 50;
+ const SUMMARY_RESULT_LIMIT = 50;
+ const SUMMARY_VIEWPORT_MARGIN = "200px 0px";
+ const documentTextCache = new Map();
+ let summaryGeneration = 0;
+ let summaryQueue = [];
+ let summaryPayloads = [];
+ let summaryViewportObserver = null;
+ let summaryViewportRoot = null;
+ let activeFetchCount = 0;
+ let activeResultKind = RESULT_KIND_ORDER[0];
+ let pageSummaryLimitEnabled = true;
+ let tabStripCleanup = null;
+
+ // Hook into Sphinx's asynchronous searchindex.js loading.
+ window.Search = window.Search || {};
+ window.Search._callbacks = window.Search._callbacks || [];
+ window.Search._index = window.Search._index || null;
+ window.Search.setIndex = (index) => {
+ window.Search._index = index;
+ const callbacks = window.Search._callbacks.slice();
+ window.Search._callbacks.length = 0;
+ callbacks.forEach((callback) => callback(index));
+ };
+ window.Search.whenReady = (callback) => {
+ if (window.Search._index) callback(window.Search._index);
+ else window.Search._callbacks.push(callback);
+ };
+
+ // Query normalization and Sphinx compatibility helpers.
+ const splitQuery = (query) =>
+ query
+ .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu)
+ .filter((term) => term);
+
+ // Fall back to an identity stemmer so search still works if a future
+ // Sphinx change stops providing the Stemmer global.
+ const getStemmer = () =>
+ typeof Stemmer === "function" ? new Stemmer() : { stemWord: (word) => word };
+
+ // Sphinx <= 8 exposes stopwords as an array; 9.x switched to a Set.
+ const hasStopword = (word) => {
+ if (typeof stopwords === "undefined") return false;
+ if (typeof stopwords.has === "function") return stopwords.has(word);
+ if (typeof stopwords.indexOf === "function") return stopwords.indexOf(word) !== -1;
+ return false;
+ };
+
+ const hasOwn = (object, key) =>
+ Object.prototype.hasOwnProperty.call(object, key);
+
+ // Prefer the newer content-root data attribute, but fall back to older
+ // Sphinx builds that still expose URL_ROOT on DOCUMENTATION_OPTIONS.
+ const getContentRoot = () =>
+ document.documentElement.dataset.content_root
+ || (typeof DOCUMENTATION_OPTIONS !== "undefined" ? DOCUMENTATION_OPTIONS.URL_ROOT || "" : "");
+
+ // General utilities, result ordering, and generated-document paths.
+ const compareResults = (left, right) => {
+ if (left.score === right.score) {
+ const leftTitle = left.title.toLowerCase();
+ const rightTitle = right.title.toLowerCase();
+ if (leftTitle === rightTitle) return 0;
+ return leftTitle < rightTitle ? -1 : 1;
+ }
+ return right.score - left.score;
+ };
+
+ const getAreaValue = (docName) =>
+ docName.includes("/") ? docName.split("/", 1)[0] : TOP_LEVEL_AREA;
+
+ const getAreaLabel = (area) =>
+ area === TOP_LEVEL_AREA ? "Top level" : area;
+
+ const matchArea = (docName, area) => {
+ if (!area) return true;
+ if (area === TOP_LEVEL_AREA) return !docName.includes("/");
+ return docName === area || docName.startsWith(area + "/");
+ };
+
+ // Generated-document path handling for html and dirhtml builds.
+ const buildDocUrls = (docName) => {
+ const contentRoot = getContentRoot();
+ const builder = DOCUMENTATION_OPTIONS.BUILDER;
+ const fileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX;
+ const linkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX;
+
+ if (builder === "dirhtml") {
+ let dirname = docName + "/";
+ if (dirname.match(/\/index\/$/)) dirname = dirname.substring(0, dirname.length - 6);
+ else if (dirname === "index/") dirname = "";
+
+ return {
+ requestUrl: contentRoot + dirname,
+ linkUrl: contentRoot + dirname,
+ };
+ }
+
+ return {
+ requestUrl: contentRoot + docName + fileSuffix,
+ linkUrl: docName + linkSuffix,
+ };
+ };
+
+ // Lazy page-summary helpers for "Pages" search results.
+ const htmlToText = (htmlString, anchor) => {
+ const htmlElement = new DOMParser().parseFromString(htmlString, "text/html");
+ for (const selector of [".headerlink", "script", "style"]) {
+ htmlElement.querySelectorAll(selector).forEach((element) => element.remove());
+ }
+
+ if (anchor) {
+ const anchorId = anchor[0] === "#" ? anchor.substring(1) : anchor;
+ const anchorContent = htmlElement.getElementById(anchorId);
+ if (anchorContent) return anchorContent.textContent;
+ }
+
+ const docContent = htmlElement.querySelector('[role="main"]');
+ return docContent ? docContent.textContent : "";
+ };
+
+ const makeSummary = (htmlText, keywords, anchor) => {
+ const text = htmlToText(htmlText, anchor);
+ if (!text) return null;
+
+ const lowered = text.toLowerCase();
+ const positions = keywords
+ .map((keyword) => lowered.indexOf(keyword.toLowerCase()))
+ .filter((position) => position > -1);
+ const actualStart = positions.length ? positions[0] : 0;
+ const start = Math.max(actualStart - 120, 0);
+ const prefix = start === 0 ? "" : "...";
+ const suffix = start + 240 < text.length ? "..." : "";
+
+ const summary = document.createElement("p");
+ summary.className = "kernel-search-summary";
+ summary.textContent = prefix + text.substring(start, start + 240).trim() + suffix;
+ return summary;
+ };
+
+const setSummaryPlaceholder = (payload, text, modifierClass) => {
+ if (!payload.placeholder) {
+ payload.placeholder = document.createElement("p");
+ payload.item.appendChild(payload.placeholder);
+ }
+
+ const classes = ["kernel-search-summary", "kernel-search-summary-status"];
+ if (modifierClass) classes.push(modifierClass);
+ payload.placeholder.className = classes.join(" ");
+ payload.placeholder.textContent = text;
+ };
+
+ const clearSummaryPlaceholder = (payload) => {
+ if (!payload.placeholder) return;
+ payload.placeholder.remove();
+ payload.placeholder = null;
+ };
+
+ const loadDocumentText = (payload) => {
+ if (documentTextCache.has(payload.requestUrl)) {
+ return Promise.resolve(documentTextCache.get(payload.requestUrl));
+ }
+
+ const controller = typeof AbortController === "function"
+ ? new AbortController()
+ : null;
+ payload.abortController = controller;
+
+ return fetch(payload.requestUrl, controller ? { signal: controller.signal } : {})
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error(`Summary request failed: ${response.status}`);
+ }
+ return response.text();
+ })
+ .then((htmlText) => {
+ documentTextCache.set(payload.requestUrl, htmlText);
+ return htmlText;
+ })
+ .finally(() => {
+ if (payload.abortController === controller) payload.abortController = null;
+ });
+ };
+
+ const pushBest = (resultMap, result) => {
+ const key = [result.kind, result.docName, result.anchor || "", result.title].join("|");
+ const existing = resultMap.get(key);
+ if (!existing || existing.score < result.score) resultMap.set(key, result);
+ };
+
+ // Query parsing, scoring, and deduplication.
+ const buildQueryState = (query, exact) => {
+ const rawTerms = splitQuery(query.trim());
+ const rawTermsLower = rawTerms.map((term) => term.toLowerCase());
+ const objectTerms = new Set(rawTermsLower);
+ const highlightTerms = exact ? rawTermsLower : [];
+ const searchTerms = new Set();
+ const excludedTerms = new Set();
+
+ if (!exact) {
+ const stemmer = getStemmer();
+ rawTerms.forEach((term) => {
+ const lowered = term.toLowerCase();
+ if (hasStopword(lowered) || /^\d+$/.test(term)) {
+ return;
+ }
+
+ const word = stemmer.stemWord(lowered);
+ if (!word) return;
+
+ if (word[0] === "-") excludedTerms.add(word.substring(1));
+ else {
+ searchTerms.add(word);
+ highlightTerms.push(lowered);
+ }
+ });
+ } else {
+ rawTermsLower.forEach((term) => searchTerms.add(term));
+ }
+
+ if (typeof SPHINX_HIGHLIGHT_ENABLED !== "undefined" && SPHINX_HIGHLIGHT_ENABLED) {
+ localStorage.setItem("sphinx_highlight_terms", [...new Set(highlightTerms)].join(" "));
+ }
+
+ return {
+ exact,
+ query,
+ queryLower: query.toLowerCase().trim(),
+ rawTerms: rawTermsLower,
+ objectTerms,
+ searchTerms,
+ excludedTerms,
+ highlightTerms: [...new Set(highlightTerms)],
+ };
+ };
+
+ const candidateMatches = (candidateLower, state) => {
+ if (!state.queryLower) return false;
+ if (state.exact) return candidateLower === state.queryLower;
+
+ if (
+ candidateLower.includes(state.queryLower)
+ && state.queryLower.length >= Math.ceil(candidateLower.length / 2)
+ ) {
+ return true;
+ }
+
+ return state.rawTerms.length > 0
+ && state.rawTerms.every((term) => candidateLower.includes(term));
+ };
+
+ const scoreLabelMatch = (candidateLower, state, baseScore, partialScore) => {
+ if (state.exact) return baseScore + LABEL_MATCH_SCORES.exactMatchBonus;
+ if (candidateLower === state.queryLower) {
+ return baseScore + LABEL_MATCH_SCORES.exactCandidateBonus;
+ }
+ if (candidateLower.includes(state.queryLower)) {
+ return Math.max(partialScore, Math.round((baseScore * state.queryLower.length) / candidateLower.length));
+ }
+
+ return partialScore * Math.max(1, state.rawTerms.filter((term) => candidateLower.includes(term)).length);
+ };
+
+ // Result collectors map Sphinx index structures to one result kind each.
+ const collectObjectResults = (index, state, filters) => {
+ const resultMap = new Map();
+ const objects = index.objects || {};
+ const objNames = index.objnames || {};
+ const objTypes = index.objtypes || {};
+
+ const addObjectResult = (prefix, name, match) => {
+ const fileIndex = match[0];
+ const typeIndex = match[1];
+ const priority = match[2];
+ const anchorValue = match[3];
+ const docName = index.docnames[fileIndex];
+ const fileName = index.filenames[fileIndex];
+ const pageTitle = index.titles[fileIndex];
+ const objectLabel = objNames[typeIndex] ? objNames[typeIndex][2] : "Object";
+ const objectType = objTypes[typeIndex];
+
+ if (!matchArea(docName, filters.area)) return;
+ if (filters.objtype && filters.objtype !== objectType) return;
+
+ const fullName = prefix ? prefix + "." + name : name;
+ const fullNameLower = fullName.toLowerCase();
+ const lastNameLower = fullNameLower.split(".").slice(-1)[0];
+ const nameLower = name.toLowerCase();
+
+ let score = 0;
+ if (state.exact) {
+ if (
+ fullNameLower !== state.queryLower
+ && lastNameLower !== state.queryLower
+ && nameLower !== state.queryLower
+ ) {
+ return;
+ }
+ score = OBJECT_MATCH_SCORES.exact;
+ } else {
+ const haystack = `${fullName} ${objectLabel} ${pageTitle}`.toLowerCase();
+ if (state.objectTerms.size === 0) return;
+ if ([...state.objectTerms].some((term) => !haystack.includes(term))) return;
+ const matchedNameTerms = state.rawTerms.filter(
+ (term) =>
+ fullNameLower.includes(term)
+ || lastNameLower.includes(term)
+ || nameLower.includes(term),
+ ).length;
+
+ if (
+ fullNameLower === state.queryLower
+ || lastNameLower === state.queryLower
+ || nameLower === state.queryLower
+ ) {
+ score += OBJECT_MATCH_SCORES.exactNameBoost;
+ } else if (
+ lastNameLower.includes(state.queryLower)
+ || nameLower.includes(state.queryLower)
+ ) {
+ score += OBJECT_MATCH_SCORES.partialShortName;
+ } else if (fullNameLower.includes(state.queryLower)) {
+ score += OBJECT_MATCH_SCORES.partialFullName;
+ } else if (matchedNameTerms > 0) {
+ score += matchedNameTerms * OBJECT_MATCH_SCORES.matchedNameTerm;
+ } else {
+ return;
+ }
+ }
+
+ score += OBJECT_PRIORITY[priority] || 0;
+
+ let anchor = anchorValue;
+ if (anchor === "") anchor = fullName;
+ else if (anchor === "-" && objNames[typeIndex]) anchor = objNames[typeIndex][1] + "-" + fullName;
+
+ pushBest(resultMap, {
+ kind: "object",
+ docName,
+ fileName,
+ title: fullName,
+ anchor: anchor ? "#" + anchor : "",
+ description: `${objectLabel}, in ${pageTitle}`,
+ score,
+ });
+ };
+
+ Object.keys(objects).forEach((prefix) => {
+ const group = objects[prefix];
+
+ // Sphinx 3.x stores objects as name->tuple mappings; 4.x+ switched
+ // to arrays with the display name appended as a fifth element.
+ if (Array.isArray(group)) {
+ group.forEach((match) => {
+ addObjectResult(prefix, match[4], match);
+ });
+ return;
+ }
+
+ Object.entries(group || {}).forEach(([name, match]) => {
+ addObjectResult(prefix, name, match);
+ });
+ });
+
+ return [...resultMap.values()].sort(compareResults);
+ };
+
+ const collectSectionResults = (index, state, filters) => {
+ const resultMap = new Map();
+ const allTitles = index.alltitles || {};
+
+ Object.entries(allTitles).forEach(([sectionTitle, entries]) => {
+ const lowered = sectionTitle.toLowerCase().trim();
+ if (!candidateMatches(lowered, state)) return;
+
+ entries.forEach(([fileIndex, anchorId]) => {
+ const docName = index.docnames[fileIndex];
+ const fileName = index.filenames[fileIndex];
+ const pageTitle = index.titles[fileIndex];
+ if (!matchArea(docName, filters.area)) return;
+
+ if (anchorId === null && sectionTitle === pageTitle) return;
+
+ pushBest(resultMap, {
+ kind: "title",
+ docName,
+ fileName,
+ title: pageTitle !== sectionTitle ? `${pageTitle} > ${sectionTitle}` : sectionTitle,
+ anchor: anchorId ? "#" + anchorId : "",
+ description: pageTitle,
+ score: scoreLabelMatch(
+ lowered,
+ state,
+ LABEL_MATCH_SCORES.sectionBase,
+ LABEL_MATCH_SCORES.sectionPartial,
+ ),
+ });
+ });
+ });
+
+ return [...resultMap.values()].sort(compareResults);
+ };
+
+ const collectIndexResults = (index, state, filters) => {
+ const resultMap = new Map();
+ const entries = index.indexentries || {};
+
+ Object.entries(entries).forEach(([entry, matches]) => {
+ const lowered = entry.toLowerCase().trim();
+ if (!candidateMatches(lowered, state)) return;
+
+ matches.forEach(([fileIndex, anchorId, isMain]) => {
+ const docName = index.docnames[fileIndex];
+ const fileName = index.filenames[fileIndex];
+ const pageTitle = index.titles[fileIndex];
+ if (!matchArea(docName, filters.area)) return;
+
+ let score = scoreLabelMatch(
+ lowered,
+ state,
+ LABEL_MATCH_SCORES.indexBase,
+ LABEL_MATCH_SCORES.indexPartial,
+ );
+ if (!isMain) score -= LABEL_MATCH_SCORES.secondaryIndexPenalty;
+
+ pushBest(resultMap, {
+ kind: "index",
+ docName,
+ fileName,
+ title: entry,
+ anchor: anchorId ? "#" + anchorId : "",
+ description: pageTitle,
+ score,
+ });
+ });
+ });
+
+ return [...resultMap.values()].sort(compareResults);
+ };
+
+ const collectTextResults = (index, state, filters) => {
+ // Intersect per-word matches from the inverted index and keep the
+ // best score contribution for each matched term per file.
+ const resultMap = new Map();
+ const terms = index.terms || {};
+ const titleTerms = index.titleterms || {};
+ const searchTerms = [...state.searchTerms];
+
+ if (searchTerms.length === 0) return [];
+
+ const scoreMap = new Map();
+ const fileMap = new Map();
+
+ searchTerms.forEach((word) => {
+ const files = [];
+ const candidates = [
+ {
+ files: hasOwn(terms, word) ? terms[word] : undefined,
+ score: TEXT_MATCH_SCORES.term,
+ },
+ {
+ files: hasOwn(titleTerms, word) ? titleTerms[word] : undefined,
+ score: TEXT_MATCH_SCORES.titleTerm,
+ },
+ ];
+
+ if (!state.exact && word.length > 2) {
+ if (!hasOwn(terms, word)) {
+ Object.keys(terms).forEach((term) => {
+ if (term.includes(word)) {
+ candidates.push({ files: terms[term], score: TEXT_MATCH_SCORES.partialTerm });
+ }
+ });
+ }
+ if (!hasOwn(titleTerms, word)) {
+ Object.keys(titleTerms).forEach((term) => {
+ if (term.includes(word)) {
+ candidates.push({ files: titleTerms[term], score: TEXT_MATCH_SCORES.partialTitleTerm });
+ }
+ });
+ }
+ }
+
+ if (candidates.every((candidate) => candidate.files === undefined)) return;
+
+ candidates.forEach((candidate) => {
+ if (candidate.files === undefined) return;
+
+ let recordFiles = candidate.files;
+ if (recordFiles.length === undefined) recordFiles = [recordFiles];
+ files.push(...recordFiles);
+
+ recordFiles.forEach((fileIndex) => {
+ if (!scoreMap.has(fileIndex)) scoreMap.set(fileIndex, new Map());
+ const currentScore = scoreMap.get(fileIndex).get(word) || 0;
+ scoreMap.get(fileIndex).set(word, Math.max(currentScore, candidate.score));
+ });
+ });
+
+ files.forEach((fileIndex) => {
+ if (!fileMap.has(fileIndex)) fileMap.set(fileIndex, [word]);
+ else if (!fileMap.get(fileIndex).includes(word)) fileMap.get(fileIndex).push(word);
+ });
+ });
+
+ const filteredTermCount = state.exact
+ ? searchTerms.length
+ : searchTerms.filter((term) => term.length > 2).length;
+
+ for (const [fileIndex, matchedWords] of fileMap.entries()) {
+ const docName = index.docnames[fileIndex];
+ const fileName = index.filenames[fileIndex];
+ if (!matchArea(docName, filters.area)) continue;
+
+ if (matchedWords.length !== searchTerms.length && matchedWords.length !== filteredTermCount) {
+ continue;
+ }
+
+ if (
+ [...state.excludedTerms].some(
+ (term) =>
+ terms[term] === fileIndex
+ || titleTerms[term] === fileIndex
+ || (terms[term] || []).includes(fileIndex)
+ || (titleTerms[term] || []).includes(fileIndex),
+ )
+ ) {
+ continue;
+ }
+
+ let score = Math.max(...matchedWords.map((word) => scoreMap.get(fileIndex).get(word)));
+ if (state.exact && index.titles[fileIndex].toLowerCase() === state.queryLower) {
+ score += TEXT_MATCH_SCORES.exactTitleBonus;
+ }
+
+ pushBest(resultMap, {
+ kind: "text",
+ docName,
+ fileName,
+ title: index.titles[fileIndex],
+ anchor: "",
+ description: null,
+ score,
+ });
+ }
+
+ return [...resultMap.values()].sort(compareResults);
+ };
+
+ const buildFilters = (state) => ({
+ area: state.area,
+ objtype: state.objtype,
+ });
+
+ // Rendering and lazy summary loading.
+ const resetSummaryState = () => {
+ summaryGeneration += 1;
+ summaryQueue = [];
+ activeFetchCount = 0;
+
+ if (summaryViewportObserver) {
+ summaryViewportObserver.disconnect();
+ summaryViewportObserver = null;
+ }
+ summaryViewportRoot = null;
+
+ summaryPayloads.forEach((payload) => {
+ if (payload.status !== "loading") return;
+ payload.loadToken += 1;
+ payload.status = "idle";
+ clearSummaryPlaceholder(payload);
+ if (payload.abortController) {
+ payload.abortController.abort();
+ payload.abortController = null;
+ }
+ });
+ summaryPayloads = [];
+ };
+
+ const finishSummaryLoad = (task) => {
+ const payload = task.payload;
+ activeFetchCount = Math.max(0, activeFetchCount - 1);
+ if (payload.loadToken !== task.loadToken) {
+ drainSummaryQueue();
+ return;
+ }
+ drainSummaryQueue();
+ };
+
+ const markSummaryError = (payload) => {
+ payload.status = "error";
+ setSummaryPlaceholder(payload, "Summary unavailable.", "is-error");
+ };
+
+ const runSummaryLoad = (payload) => {
+ if (payload.generation !== summaryGeneration || payload.status !== "queued") {
+ return;
+ }
+
+ payload.status = "loading";
+ setSummaryPlaceholder(payload, "Loading summary...", "is-loading");
+ payload.loadToken += 1;
+ const task = {
+ loadToken: payload.loadToken,
+ payload,
+ };
+ activeFetchCount += 1;
+ loadDocumentText(payload)
+ .then((htmlText) => {
+ if (
+ payload.loadToken !== task.loadToken
+ || payload.generation !== summaryGeneration
+ || payload.status !== "loading"
+ ) {
+ return;
+ }
+
+ const summary = makeSummary(htmlText, payload.keywords, payload.anchor);
+ if (!summary) {
+ markSummaryError(payload);
+ return;
+ }
+
+ clearSummaryPlaceholder(payload);
+ payload.item.appendChild(summary);
+ payload.status = "done";
+ })
+ .catch(() => {
+ if (payload.loadToken !== task.loadToken || payload.status !== "loading") return;
+ markSummaryError(payload);
+ })
+ .finally(() => finishSummaryLoad(task));
+ };
+
+ const drainSummaryQueue = () => {
+ while (activeFetchCount < SUMMARY_FETCH_BUDGET && summaryQueue.length) {
+ const payload = summaryQueue.shift();
+ if (!payload) break;
+ if (payload.generation !== summaryGeneration || payload.status !== "queued") continue;
+ runSummaryLoad(payload);
+ }
+ };
+
+ const enqueueSummaryLoad = (payload) => {
+ if (
+ !payload
+ || payload.generation !== summaryGeneration
+ || payload.status !== "idle"
+ || (pageSummaryLimitEnabled && payload.summaryIndex >= SUMMARY_RESULT_LIMIT)
+ ) {
+ return;
+ }
+
+ payload.status = "queued";
+ summaryQueue.push(payload);
+ drainSummaryQueue();
+ };
+
+ const cancelSummaryLoad = (payload) => {
+ if (payload.status !== "queued" && payload.status !== "loading") return;
+ payload.loadToken += 1;
+ payload.status = "idle";
+ clearSummaryPlaceholder(payload);
+ if (payload.abortController) {
+ payload.abortController.abort();
+ payload.abortController = null;
+ }
+ };
+
+ const boostSummaryPayload = (payload) => {
+ if (payload.generation !== summaryGeneration) return;
+ if (payload.status === "queued") {
+ const index = summaryQueue.indexOf(payload);
+ if (index > 0) {
+ summaryQueue.splice(index, 1);
+ summaryQueue.unshift(payload);
+ }
+ drainSummaryQueue();
+ } else if (payload.status === "idle") {
+ enqueueSummaryLoad(payload);
+ }
+ };
+
+ const ensureViewportObserver = (rootElement) => {
+ if (summaryViewportObserver && summaryViewportRoot === rootElement) {
+ return summaryViewportObserver;
+ }
+ if (summaryViewportObserver) {
+ summaryViewportObserver.disconnect();
+ summaryViewportObserver = null;
+ }
+ summaryViewportRoot = rootElement;
+ if (typeof IntersectionObserver !== "function") return null;
+
+ summaryViewportObserver = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ if (!entry.isIntersecting) return;
+ const payload = summaryPayloads.find((p) => p.item === entry.target);
+ if (payload) boostSummaryPayload(payload);
+ });
+ }, {
+ root: rootElement,
+ rootMargin: SUMMARY_VIEWPORT_MARGIN,
+ });
+
+ return summaryViewportObserver;
+ };
+
+ const activateSummaryLoads = (rootElement) => {
+ const observer = ensureViewportObserver(rootElement);
+
+ summaryQueue = summaryQueue.filter((payload) => {
+ if (payload.generation !== summaryGeneration || payload.status !== "queued") return false;
+ if (pageSummaryLimitEnabled && payload.summaryIndex >= SUMMARY_RESULT_LIMIT) {
+ cancelSummaryLoad(payload);
+ return false;
+ }
+ return true;
+ });
+
+ summaryPayloads.forEach((payload) => {
+ if (payload.generation !== summaryGeneration) return;
+ if (pageSummaryLimitEnabled && payload.summaryIndex >= SUMMARY_RESULT_LIMIT) {
+ if (payload.status === "queued" || payload.status === "loading") {
+ cancelSummaryLoad(payload);
+ }
+ return;
+ }
+ if (observer) observer.observe(payload.item);
+ if (payload.status !== "idle") return;
+ enqueueSummaryLoad(payload);
+ });
+
+ drainSummaryQueue();
+ };
+
+ const pauseSummaryLoads = () => {
+ if (summaryViewportObserver) {
+ summaryViewportObserver.disconnect();
+ summaryViewportObserver = null;
+ }
+ summaryViewportRoot = null;
+ summaryQueue = [];
+ summaryPayloads.forEach((payload) => cancelSummaryLoad(payload));
+ };
+
+ const resetTabStripState = () => {
+ if (!tabStripCleanup) return;
+ tabStripCleanup();
+ tabStripCleanup = null;
+ };
+
+ const bindTabStripShadows = (frame, scroller) => {
+ resetTabStripState();
+
+ const syncShadows = () => {
+ const maxScrollLeft = Math.max(0, scroller.scrollWidth - scroller.clientWidth);
+ const hasOverflow = maxScrollLeft > 1;
+ frame.classList.toggle("has-left-shadow", hasOverflow && scroller.scrollLeft > 1);
+ frame.classList.toggle(
+ "has-right-shadow",
+ hasOverflow && scroller.scrollLeft < maxScrollLeft - 1,
+ );
+ };
+
+ scroller.addEventListener("scroll", syncShadows, { passive: true });
+ if (typeof ResizeObserver === "function") {
+ const resizeObserver = new ResizeObserver(syncShadows);
+ resizeObserver.observe(scroller);
+ tabStripCleanup = () => {
+ scroller.removeEventListener("scroll", syncShadows);
+ resizeObserver.disconnect();
+ };
+ } else {
+ window.addEventListener("resize", syncShadows);
+ tabStripCleanup = () => {
+ scroller.removeEventListener("scroll", syncShadows);
+ window.removeEventListener("resize", syncShadows);
+ };
+ }
+
+ window.requestAnimationFrame(syncShadows);
+ };
+
+ const createResultItem = (result, keywords, summaryIndex) => {
+ const urls = buildDocUrls(result.docName);
+ const item = document.createElement("li");
+ item.className = `kernel-search-result kind-${result.kind}`;
+
+ const heading = item.appendChild(document.createElement("div"));
+ heading.className = "kernel-search-result-heading";
+
+ const link = heading.appendChild(document.createElement("a"));
+ link.href = urls.linkUrl + result.anchor;
+ link.dataset.score = String(result.score);
+ link.textContent = result.title;
+
+ const path = item.appendChild(document.createElement("div"));
+ path.className = "kernel-search-path";
+ path.textContent = result.fileName;
+
+ if (result.description) {
+ const meta = item.appendChild(document.createElement("div"));
+ meta.className = "kernel-search-meta";
+ meta.textContent = result.description;
+ }
+
+ if (result.kind === "text") {
+ const payload = {
+ abortController: null,
+ anchor: result.anchor,
+ generation: summaryGeneration,
+ item,
+ keywords,
+ loadToken: 0,
+ placeholder: null,
+ requestUrl: urls.requestUrl,
+ summaryIndex,
+ status: "idle",
+ };
+ summaryPayloads.push(payload);
+ }
+ return item;
+ };
+
+ const renderResults = (state, resultsByKind) => {
+ const container = document.getElementById("kernel-search-results");
+ const totalResults = RESULT_KIND_ORDER.reduce(
+ (count, kind) => count + resultsByKind[kind].length,
+ 0,
+ );
+ resetSummaryState();
+ resetTabStripState();
+ container.replaceChildren();
+
+ const summary = document.createElement("p");
+ summary.className = "kernel-search-status";
+ if (!state.queryLower) {
+ summary.textContent = "Enter a search query to browse kernel documentation.";
+ container.appendChild(summary);
+ return;
+ }
+
+ if (!totalResults) {
+ summary.textContent =
+ "No matching results were found for the current query and filters.";
+ container.appendChild(summary);
+ return;
+ }
+
+ summary.textContent =
+ `Found ${totalResults} result${totalResults === 1 ? "" : "s"} for "${state.query}".`;
+ container.appendChild(summary);
+
+ const availableKinds = RESULT_KIND_ORDER.filter((kind) => resultsByKind[kind].length);
+ const shell = container.appendChild(document.createElement("div"));
+ shell.className = "kernel-search-results-shell";
+
+ const tabFrame = shell.appendChild(document.createElement("div"));
+ tabFrame.className = "kernel-search-tab-strip";
+
+ const tabScroller = tabFrame.appendChild(document.createElement("div"));
+ tabScroller.className = "kernel-search-tab-scroller";
+ tabScroller.setAttribute("role", "tablist");
+ tabScroller.setAttribute("aria-label", "Search result kinds");
+
+ const toolRow = shell.appendChild(document.createElement("div"));
+ toolRow.className = "kernel-search-panel-tools";
+ toolRow.hidden = true;
+
+ const summaryLimitLabel = toolRow.appendChild(document.createElement("label"));
+ summaryLimitLabel.className = "kernel-search-checkbox kernel-search-summary-limit";
+ summaryLimitLabel.hidden = !availableKinds.includes("text");
+
+ const summaryLimitToggle = summaryLimitLabel.appendChild(document.createElement("input"));
+ summaryLimitToggle.type = "checkbox";
+ summaryLimitToggle.checked = pageSummaryLimitEnabled;
+
+ const summaryLimitText = summaryLimitLabel.appendChild(document.createElement("span"));
+ summaryLimitText.textContent = "Limit page summaries to first 50";
+
+ const panels = shell.appendChild(document.createElement("div"));
+ panels.className = "kernel-search-panels";
+
+ const tabButtons = new Map();
+ const tabPanels = new Map();
+
+ const selectTab = (kind, focusTab) => {
+ activeResultKind = kind;
+ tabButtons.forEach((button, buttonKind) => {
+ const active = buttonKind === kind;
+ button.classList.toggle("is-active", active);
+ button.setAttribute("aria-selected", active ? "true" : "false");
+ button.tabIndex = active ? 0 : -1;
+ if (active && focusTab) {
+ button.focus();
+ button.scrollIntoView({ block: "nearest", inline: "nearest" });
+ }
+ });
+
+ tabPanels.forEach((panel, panelKind) => {
+ const active = panelKind === kind;
+ panel.hidden = !active;
+ panel.classList.toggle("is-active", active);
+ });
+
+ toolRow.hidden = kind !== "text";
+
+ if (kind === "text") {
+ const panel = tabPanels.get("text");
+ if (panel) activateSummaryLoads(panel);
+ } else {
+ pauseSummaryLoads();
+ }
+ };
+
+ const handleTabKeydown = (event) => {
+ const currentIndex = availableKinds.indexOf(activeResultKind);
+ if (currentIndex === -1) return;
+
+ let nextIndex = -1;
+ switch (event.key) {
+ case "ArrowLeft":
+ case "ArrowUp":
+ nextIndex = (currentIndex + availableKinds.length - 1) % availableKinds.length;
+ break;
+ case "ArrowRight":
+ case "ArrowDown":
+ nextIndex = (currentIndex + 1) % availableKinds.length;
+ break;
+ case "Home":
+ nextIndex = 0;
+ break;
+ case "End":
+ nextIndex = availableKinds.length - 1;
+ break;
+ default:
+ return;
+ }
+
+ event.preventDefault();
+ selectTab(availableKinds[nextIndex], true);
+ };
+
+ RESULT_KIND_ORDER.forEach((kind) => {
+ const results = resultsByKind[kind];
+ if (!results.length) return;
+
+ const tab = tabScroller.appendChild(document.createElement("button"));
+ tab.type = "button";
+ tab.className = `kernel-search-tab kind-${kind}`;
+ tab.id = `kernel-search-tab-${kind}`;
+ tab.setAttribute("role", "tab");
+ tab.setAttribute("aria-controls", `kernel-search-panel-${kind}`);
+ tab.addEventListener("click", () => selectTab(kind, false));
+ tab.addEventListener("keydown", handleTabKeydown);
+ tabButtons.set(kind, tab);
+
+ const label = tab.appendChild(document.createElement("span"));
+ label.className = "kernel-search-tab-label";
+ label.textContent = RESULT_KIND_LABELS[kind];
+
+ const count = tab.appendChild(document.createElement("span"));
+ count.className = "kernel-search-tab-count";
+ count.textContent = String(results.length);
+
+ const panel = panels.appendChild(document.createElement("section"));
+ panel.className = `kernel-search-panel kind-${kind}`;
+ panel.id = `kernel-search-panel-${kind}`;
+ panel.setAttribute("role", "tabpanel");
+ panel.setAttribute("aria-labelledby", tab.id);
+ panel.hidden = true;
+ tabPanels.set(kind, panel);
+
+ const list = panel.appendChild(document.createElement("ol"));
+ list.className = "kernel-search-list";
+
+ results.forEach((result, index) => {
+ list.appendChild(
+ createResultItem(
+ result,
+ state.highlightTerms,
+ index,
+ ),
+ );
+ });
+ });
+
+ summaryLimitToggle.addEventListener("change", () => {
+ pageSummaryLimitEnabled = summaryLimitToggle.checked;
+ if (activeResultKind === "text") {
+ const panel = tabPanels.get("text");
+ if (panel) activateSummaryLoads(panel);
+ }
+ });
+
+ bindTabStripShadows(tabFrame, tabScroller);
+ const defaultKind = availableKinds.includes(activeResultKind)
+ ? activeResultKind
+ : availableKinds[0];
+ selectTab(defaultKind, false);
+ };
+
+ // Form-state parsing, dynamic filter options, and page initialization.
+ const populateAreaOptions = (select, state) => {
+ const areas = new Set();
+ window.Search._index.docnames.forEach((docName) => areas.add(getAreaValue(docName)));
+
+ const options = [new Option("All documentation areas", "", false, !state.area)];
+ [...areas]
+ .sort((left, right) => {
+ if (left === TOP_LEVEL_AREA) return -1;
+ if (right === TOP_LEVEL_AREA) return 1;
+ return left.localeCompare(right);
+ })
+ .forEach((area) => {
+ options.push(new Option(getAreaLabel(area), area, false, area === state.area));
+ });
+
+ select.replaceChildren(...options);
+ };
+
+ const populateObjectTypeOptions = (select, state) => {
+ const objTypes = window.Search._index.objtypes || {};
+ const objNames = window.Search._index.objnames || {};
+ const entries = Object.keys(objTypes)
+ .map((key) => ({
+ value: objTypes[key],
+ label: objNames[key] ? objNames[key][2] : objTypes[key],
+ }))
+ .sort((left, right) => left.label.localeCompare(right.label));
+
+ const seen = new Set();
+ const options = [new Option("All object types", "", false, !state.objtype)];
+ entries.forEach((entry) => {
+ if (seen.has(entry.value)) return;
+ seen.add(entry.value);
+ options.push(new Option(entry.label, entry.value, false, entry.value === state.objtype));
+ });
+
+ select.replaceChildren(...options);
+ };
+
+ const parseState = () => {
+ const params = new URLSearchParams(window.location.search);
+ const kinds = params.getAll("kind").filter((kind) => RESULT_KIND_ORDER.includes(kind));
+
+ return {
+ query: params.get("q") || "",
+ queryLower: (params.get("q") || "").toLowerCase().trim(),
+ exact: params.get("exact") === "1",
+ area: params.get("area") || "",
+ objtype: params.get("objtype") || "",
+ advanced: params.get("advanced") === "1",
+ kinds: kinds.length ? new Set(kinds) : new Set(RESULT_KIND_ORDER),
+ };
+ };
+
+ const shouldOpenAdvanced = (state) =>
+ state.advanced
+ || state.exact
+ || state.area !== ""
+ || state.objtype !== ""
+ || RESULT_KIND_ORDER.some((kind) => !state.kinds.has(kind));
+
+ const bindFormState = (state) => {
+ document.getElementById("kernel-search-query").value = state.query;
+ document.getElementById("kernel-search-exact").checked = state.exact;
+ RESULT_KIND_ORDER.forEach((kind) => {
+ const checkbox = document.getElementById(`kernel-search-kind-${kind}`);
+ if (checkbox) checkbox.checked = state.kinds.has(kind);
+ });
+
+ const advanced = document.getElementById("kernel-search-advanced");
+ const advancedFlag = document.getElementById("kernel-search-advanced-flag");
+ const open = shouldOpenAdvanced(state);
+ advanced.open = open;
+ advancedFlag.disabled = !open;
+ advanced.addEventListener("toggle", () => {
+ advancedFlag.disabled = !advanced.open;
+ });
+ };
+
+ const runSearch = () => {
+ const baseState = parseState();
+ bindFormState(baseState);
+ populateAreaOptions(document.getElementById("kernel-search-area"), baseState);
+ populateObjectTypeOptions(document.getElementById("kernel-search-objtype"), baseState);
+
+ const queryState = buildQueryState(baseState.query, baseState.exact);
+ const renderState = {
+ ...baseState,
+ highlightTerms: queryState.highlightTerms,
+ };
+ const filters = buildFilters(baseState);
+ const resultsByKind = {
+ object: [],
+ title: [],
+ index: [],
+ text: [],
+ };
+
+ if (!baseState.queryLower) {
+ renderResults(renderState, resultsByKind);
+ return;
+ }
+
+ if (baseState.kinds.has("object")) {
+ resultsByKind.object = collectObjectResults(window.Search._index, queryState, filters);
+ }
+ if (baseState.kinds.has("title")) {
+ resultsByKind.title = collectSectionResults(window.Search._index, queryState, filters);
+ }
+ if (baseState.kinds.has("index")) {
+ resultsByKind.index = collectIndexResults(window.Search._index, queryState, filters);
+ }
+ if (baseState.kinds.has("text")) {
+ resultsByKind.text = collectTextResults(window.Search._index, queryState, filters);
+ }
+
+ renderResults(renderState, resultsByKind);
+ };
+
+ document.addEventListener("DOMContentLoaded", () => {
+ const container = document.getElementById("kernel-search-results");
+ if (!container) return;
+
+ const progress = document.getElementById("search-progress");
+ if (progress) progress.textContent = "Preparing search...";
+
+ window.Search.whenReady(() => {
+ if (progress) progress.textContent = "";
+ runSearch();
+ });
+ });
+})();
diff --git a/Documentation/sphinx/templates/search.html b/Documentation/sphinx/templates/search.html
new file mode 100644
index 000000000..311e15559
--- /dev/null
+++ b/Documentation/sphinx/templates/search.html
@@ -0,0 +1,117 @@
+{# SPDX-License-Identifier: GPL-2.0 #}
+
+{# Enhanced search page for kernel documentation. #}
+{%- extends "layout.html" %}
+{% set title = _('Search') %}
+{%- block scripts %}
+ {{ super() }}
+ {#
+ Load Sphinx language data plus the kernel-specific search runtime.
+ searchindex.js later populates the shared Search index consumed here,
+ so keep this boot order intact.
+ #}
+ <script src="{{ pathto('_static/language_data.js', 1) }}"></script>
+ <script src="{{ pathto('_static/kernel-search.js', 1) }}"></script>
+{%- endblock %}
+{% block extrahead %}
+ <script src="{{ pathto('searchindex.js', 1) }}" defer="defer"></script>
+ <meta name="robots" content="noindex" />
+ {{ super() }}
+{% endblock %}
+{% block body %}
+ <h1 id="search-documentation">{{ _('Search') }}</h1>
+ <noscript>
+ <div class="admonition warning">
+ <p>
+ {% trans %}Please activate JavaScript to enable the search
+ functionality.{% endtrans %}
+ </p>
+ </div>
+ </noscript>
+ <p class="kernel-search-help">
+ {% trans %}Searching for multiple words only shows matches that contain
+ all words.{% endtrans %}
+ </p>
+ {#
+ This markup is a shared contract with kernel-search.js and custom.css.
+ Keep search form/result IDs and classes in sync with both files when
+ changing this template.
+ #}
+ <form id="kernel-search-form" class="kernel-search-form" action="" method="get">
+ <div class="kernel-search-query-row">
+ <div class="kernel-search-query-field">
+ <label for="kernel-search-query">{{ _('Search query') }}</label>
+ <input
+ id="kernel-search-query"
+ type="text"
+ name="q"
+ value=""
+ autocomplete="off"
+ autocorrect="off"
+ autocapitalize="off"
+ spellcheck="false"
+ />
+ </div>
+ <div class="kernel-search-query-actions">
+ <input type="submit" value="{{ _('Search') }}" />
+ <span id="search-progress" class="kernel-search-progress"></span>
+ </div>
+ </div>
+
+ <details id="kernel-search-advanced" class="kernel-search-advanced">
+ {# Keep advanced filters optional while preserving them in the URL. #}
+ <summary>{{ _('Advanced search') }}</summary>
+ <input
+ id="kernel-search-advanced-flag"
+ type="hidden"
+ name="advanced"
+ value="1"
+ disabled="disabled"
+ />
+ <div class="kernel-search-advanced-grid">
+ <div class="kernel-search-field">
+ <label class="kernel-search-checkbox" for="kernel-search-exact">
+ <input id="kernel-search-exact" type="checkbox" name="exact" value="1" />
+ <span>{{ _('Exact identifier match') }}</span>
+ </label>
+ </div>
+
+ <fieldset class="kernel-search-kind-filters">
+ <legend>{{ _('Result kinds') }}</legend>
+ <label class="kernel-search-checkbox" for="kernel-search-kind-object">
+ <input id="kernel-search-kind-object" type="checkbox" name="kind" value="object" />
+ <span>{{ _('Symbols') }}</span>
+ </label>
+ <label class="kernel-search-checkbox" for="kernel-search-kind-title">
+ <input id="kernel-search-kind-title" type="checkbox" name="kind" value="title" />
+ <span>{{ _('Sections') }}</span>
+ </label>
+ <label class="kernel-search-checkbox" for="kernel-search-kind-index">
+ <input id="kernel-search-kind-index" type="checkbox" name="kind" value="index" />
+ <span>{{ _('Index entries') }}</span>
+ </label>
+ <label class="kernel-search-checkbox" for="kernel-search-kind-text">
+ <input id="kernel-search-kind-text" type="checkbox" name="kind" value="text" />
+ <span>{{ _('Pages') }}</span>
+ </label>
+ </fieldset>
+
+ <div class="kernel-search-field">
+ <label for="kernel-search-area">{{ _('Documentation area') }}</label>
+ <select id="kernel-search-area" name="area">
+ <option value="">{{ _('All documentation areas') }}</option>
+ </select>
+ </div>
+
+ <div class="kernel-search-field">
+ <label for="kernel-search-objtype">{{ _('Object type') }}</label>
+ <select id="kernel-search-objtype" name="objtype">
+ <option value="">{{ _('All object types') }}</option>
+ </select>
+ </div>
+ </div>
+ </details>
+ </form>
+
+ <div id="kernel-search-results" class="kernel-search-results"></div>
+{% endblock %}
diff --git a/Documentation/sphinx/templates/searchbox.html b/Documentation/sphinx/templates/searchbox.html
new file mode 100644
index 000000000..9e00e27cb
--- /dev/null
+++ b/Documentation/sphinx/templates/searchbox.html
@@ -0,0 +1,30 @@
+{# SPDX-License-Identifier: GPL-2.0 #}
+
+{# Sphinx sidebar template: quick search box plus advanced search link. #}
+{%- if pagename != "search" and builder != "singlehtml" %}
+<search id="searchbox" style="display: none" role="search">
+ <h3 id="searchlabel">{{ _('Quick search') }}</h3>
+ <div class="searchformwrapper">
+ <form class="search" action="{{ pathto('search') }}" method="get">
+ <input
+ type="text"
+ name="q"
+ aria-labelledby="searchlabel"
+ autocomplete="off"
+ autocorrect="off"
+ autocapitalize="off"
+ spellcheck="false"
+ />
+ <input type="submit" value="{{ _('Go') }}" />
+ </form>
+ </div>
+ <p class="search-advanced-link">
+ {#
+ Keep this entrypoint using ?advanced=1 so the full search page
+ opens with advanced filters enabled.
+ #}
+ <a href="{{ pathto('search') }}?advanced=1">{{ _('Advanced search') }}</a>
+ </p>
+</search>
+<script>document.getElementById('searchbox').style.display = "block"</script>
+{%- endif %}
diff --git a/MAINTAINERS b/MAINTAINERS
index c3fe46d7c..c9e50b101 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7652,6 +7652,16 @@ X: Documentation/power/
X: Documentation/spi/
X: Documentation/userspace-api/media/
+DOCUMENTATION ADVANCED SEARCH
+R: Rito <rito@ritovision.com>
+L: linux-doc@vger.kernel.org
+S: Maintained
+F: Documentation/sphinx-static/kernel-search.js
+F: Documentation/sphinx-static/custom.css
+F: Documentation/sphinx/templates/search.html
+F: Documentation/sphinx/templates/searchbox.html
+F: tools/docs/test_advanced_search.py
+
DOCUMENTATION PROCESS
M: Jonathan Corbet <corbet@lwn.net>
R: Shuah Khan <skhan@linuxfoundation.org>
diff --git a/tools/docs/test_advanced_search.py b/tools/docs/test_advanced_search.py
new file mode 100755
index 000000000..0d379da9d
--- /dev/null
+++ b/tools/docs/test_advanced_search.py
@@ -0,0 +1,312 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+"""
+Build a small documentation subset and verify the advanced search artifacts.
+"""
+
+from __future__ import annotations
+
+import argparse
+import os
+import re
+import shlex
+import shutil
+import subprocess
+import sys
+import tempfile
+
+from pathlib import Path
+
+
+SCRIPT = Path(__file__).resolve()
+SRCTREE = SCRIPT.parents[2]
+DEFAULT_SPHINXDIRS = ("kernel-hacking", "PCI")
+REQUIRED_SEARCH_IDS = (
+ 'id="kernel-search-form"',
+ 'id="kernel-search-query"',
+ 'id="search-progress"',
+ 'id="kernel-search-advanced"',
+ 'id="kernel-search-advanced-flag"',
+ 'id="kernel-search-area"',
+ 'id="kernel-search-objtype"',
+ 'id="kernel-search-results"',
+)
+REQUIRED_SEARCH_INDEX_KEYS = (
+ "docnames",
+ "filenames",
+ "titles",
+ "objects",
+ "objnames",
+ "objtypes",
+ "terms",
+ "titleterms",
+)
+OPTIONAL_SEARCH_INDEX_KEYS = (
+ "alltitles",
+ "indexentries",
+)
+REQUIRED_RUNTIME_SNIPPETS = (
+ "const SUMMARY_RESULT_LIMIT = 50;",
+ 'setSummaryPlaceholder(payload, "Loading summary...", "is-loading");',
+ 'setSummaryPlaceholder(payload, "Summary unavailable.", "is-error");',
+ "pageSummaryLimitEnabled && payload.summaryIndex >= SUMMARY_RESULT_LIMIT",
+ "documentTextCache.set(payload.requestUrl, htmlText);",
+ "highlightTerms: queryState.highlightTerms,",
+)
+
+
+def fail(message):
+ """Raise a readable assertion failure."""
+
+ raise AssertionError(message)
+
+
+def read_text(path):
+ """Read a UTF-8 text file or fail with context."""
+
+ try:
+ return path.read_text(encoding="utf-8")
+ except OSError as exc:
+ fail(f"Failed to read {path}: {exc}")
+
+
+def ensure_file(path, description):
+ """Ensure a generated file exists."""
+
+ if not path.is_file():
+ fail(f"Missing {description}: {path}")
+
+
+def parse_args():
+ """Parse command-line arguments."""
+
+ parser = argparse.ArgumentParser(
+ description=(
+ "Build a small docs subset and verify the generated advanced "
+ "search page, static assets, and search index contract."
+ )
+ )
+ parser.add_argument(
+ "--build-dir",
+ type=Path,
+ help=(
+ "Out-of-tree build directory passed to make via O=. "
+ "If omitted, a temporary directory is created."
+ ),
+ )
+ parser.add_argument(
+ "--keep-build-dir",
+ action="store_true",
+ help="Keep the temporary build directory after the test completes.",
+ )
+ parser.add_argument(
+ "--make",
+ default="make",
+ help="Path to the make executable. Default: make.",
+ )
+ parser.add_argument(
+ "--sphinxdirs",
+ nargs="+",
+ default=list(DEFAULT_SPHINXDIRS),
+ help=(
+ "Documentation subtrees to build via SPHINXDIRS. "
+ f"Default: {' '.join(DEFAULT_SPHINXDIRS)}."
+ ),
+ )
+ return parser.parse_args()
+
+
+def prepare_build_dir(args):
+ """Prepare the build directory and return it with cleanup metadata."""
+
+ if args.build_dir:
+ build_dir = args.build_dir.resolve()
+ if build_dir.exists() and any(build_dir.iterdir()):
+ fail(f"Build directory is not empty: {build_dir}")
+ build_dir.mkdir(parents=True, exist_ok=True)
+ return build_dir, False
+
+ build_dir = Path(tempfile.mkdtemp(prefix="advanced-search-docs-"))
+ return build_dir, not args.keep_build_dir
+
+
+def find_sphinx_build():
+ """Find a usable sphinx-build binary for the documentation build."""
+
+ env_sphinx = os.environ.get("SPHINXBUILD")
+ if env_sphinx:
+ path = Path(env_sphinx).expanduser()
+ if path.is_file() and os.access(path, os.X_OK):
+ return str(path)
+
+ local_venv_sphinx = SRCTREE / ".venv" / "bin" / "sphinx-build"
+ if local_venv_sphinx.is_file() and os.access(local_venv_sphinx, os.X_OK):
+ return str(local_venv_sphinx)
+
+ return shutil.which("sphinx-build")
+
+
+def run_build(args, build_dir):
+ """Build the configured documentation subset."""
+
+ command = [args.make, f"O={build_dir}"]
+ sphinx_build = find_sphinx_build()
+ if sphinx_build:
+ command.append(f"SPHINXBUILD={sphinx_build}")
+
+ command += [
+ f"SPHINXDIRS={' '.join(args.sphinxdirs)}",
+ "htmldocs",
+ ]
+ print("$", shlex.join(command))
+
+ subprocess.run(command, cwd=SRCTREE, check=True)
+
+ output_dir = build_dir / "Documentation" / "output"
+ if not output_dir.is_dir():
+ fail(f"Expected documentation output directory was not created: {output_dir}")
+
+ return output_dir
+
+
+def find_search_roots(output_dir):
+ """Find all generated HTML roots that expose advanced search."""
+
+ roots = []
+ for search_html in sorted(output_dir.rglob("search.html")):
+ if search_html.parent.joinpath("searchindex.js").is_file():
+ roots.append(search_html.parent)
+
+ if not roots:
+ fail(f"No generated search roots were found under {output_dir}")
+
+ return roots
+
+
+def check_search_html(search_root):
+ """Verify the generated search page wiring and DOM anchors."""
+
+ search_html_path = search_root / "search.html"
+ ensure_file(search_html_path, "generated search page")
+ search_html = read_text(search_html_path)
+
+ script_markers = (
+ "_static/language_data.js",
+ "_static/kernel-search.js",
+ "searchindex.js",
+ )
+ positions = []
+ for marker in script_markers:
+ position = search_html.find(marker)
+ if position < 0:
+ fail(f"search.html is missing required script reference: {marker}")
+ positions.append(position)
+
+ if positions != sorted(positions):
+ fail("search.html does not keep the expected search script load order")
+
+ for required_id in REQUIRED_SEARCH_IDS:
+ if required_id not in search_html:
+ fail(f"search.html is missing required advanced-search markup: {required_id}")
+
+
+def check_search_assets(search_root):
+ """Verify generated search artifacts and copied static assets."""
+
+ ensure_file(search_root / "searchindex.js", "generated search index")
+ ensure_file(search_root / "_static" / "language_data.js", "generated language data")
+
+ built_kernel_search = search_root / "_static" / "kernel-search.js"
+ source_kernel_search = SRCTREE / "Documentation" / "sphinx-static" / "kernel-search.js"
+ ensure_file(built_kernel_search, "generated kernel-search runtime")
+
+ built_runtime = read_text(built_kernel_search)
+ source_runtime = read_text(source_kernel_search)
+
+ if built_runtime != source_runtime:
+ fail(f"Generated kernel-search.js does not match the source asset: {built_kernel_search}")
+
+ # Keep the smoke test aligned with the hardening contract that the
+ # runtime now relies on: bounded summary loading, visible summary
+ # states, and the highlight-term wiring needed for summary generation.
+ for snippet in REQUIRED_RUNTIME_SNIPPETS:
+ if snippet not in built_runtime:
+ fail(f"kernel-search.js is missing required runtime snippet: {snippet}")
+
+
+def check_search_index_contract(search_root):
+ """Verify that generated searchindex.js exposes the runtime keys we use."""
+
+ search_index_path = search_root / "searchindex.js"
+ search_index = read_text(search_index_path)
+
+ if "Search.setIndex(" not in search_index:
+ fail("searchindex.js does not initialize the shared Search index")
+
+ for key in REQUIRED_SEARCH_INDEX_KEYS:
+ if not re.search(rf'(?:"{re.escape(key)}"|{re.escape(key)})\s*:', search_index):
+ fail(f"searchindex.js is missing required key: {key}")
+
+ # Older supported Sphinx versions omit these keys, and the runtime falls
+ # back to empty objects when they are absent.
+ for key in OPTIONAL_SEARCH_INDEX_KEYS:
+ if key in search_index and not re.search(
+ rf'(?:"{re.escape(key)}"|{re.escape(key)})\s*:', search_index
+ ):
+ fail(f"searchindex.js contains malformed optional key: {key}")
+
+
+def check_advanced_search_link(search_root):
+ """Verify that a built non-search page exposes the advanced-search link."""
+
+ for page in sorted(search_root.rglob("*.html")):
+ if page.name in {"search.html", "genindex.html"}:
+ continue
+
+ contents = read_text(page)
+ if "Advanced search" in contents and "?advanced=1" in contents:
+ return
+
+ fail("No generated documentation page exposes the Advanced search sidebar link")
+
+
+def main():
+ """Build docs and run the advanced-search smoke checks."""
+
+ args = parse_args()
+ build_dir, cleanup = prepare_build_dir(args)
+
+ try:
+ output_dir = run_build(args, build_dir)
+ search_roots = find_search_roots(output_dir)
+ for search_root in search_roots:
+ check_search_html(search_root)
+ check_search_assets(search_root)
+ check_search_index_contract(search_root)
+ check_advanced_search_link(search_root)
+ except Exception:
+ print(f"Preserving build directory for inspection: {build_dir}", file=sys.stderr)
+ cleanup = False
+ raise
+ finally:
+ if cleanup:
+ shutil.rmtree(build_dir)
+
+ print(
+ "Advanced search smoke test passed "
+ f"for SPHINXDIRS={' '.join(args.sphinxdirs)} "
+ f"across {len(search_roots)} generated search trees."
+ )
+ if cleanup:
+ print(f"Removed temporary build directory: {build_dir}")
+ else:
+ print(f"Build directory: {build_dir}")
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except (AssertionError, subprocess.CalledProcessError) as exc:
+ print(exc, file=sys.stderr)
+ sys.exit(1)
--
2.51.0
^ permalink raw reply related
* [PATCH v3 0/2] docs: advanced search with benchmark harness
From: Rito Rhymes @ 2026-04-04 7:34 UTC (permalink / raw)
To: corbet, skhan; +Cc: linux-doc, linux-kernel, Rito Rhymes
In-Reply-To: <20260321181511.11706-1-rito@ritovision.com>
This series adds an Advanced Search interface for kernel
documentation.
This is being proposed here rather than upstream Sphinx because the
current implementation is tailored to the kernel documentation set and
its navigation needs, and is integrated through kernel-local template
and static-asset overrides rather than a generalized Sphinx extension
interface.
Parts of the approach could potentially be abstracted further in the
future, but this series is focused on solving the problem concretely for
kernel documentation first rather than proposing a general-purpose
Sphinx search redesign.
The first patch adds the feature itself: an advanced search page built
on the existing Sphinx search data, with tabbed results for Symbols,
Sections, Index entries, and Pages, richer filtering, more targeted
identifier search, and bounded Pages summary loading with compatibility
handling across supported Sphinx versions.
The second patch adds optional developer-side benchmark tooling and
passive timing instrumentation used to validate runtime behavior and
compare advanced search with stock Quick Search.
Jon previously noted that the window for larger merges has passed, so
this is not intended as a request to take a large feature late in the
current cycle. The immediate goal is to close the loop on the debugging
and compatibility work around the earlier version, so the current
implementation is available for testing and review ahead of future
merge windows.
This version should address the compatibility issue Randy had reported
with the earlier implementation. If Randy has time to try this updated
version with the setup that exposed the earlier problem, I would
appreciate confirmation that it now behaves correctly there.
Rito Rhymes (2):
docs: add advanced search for kernel documentation
docs: add advanced search benchmark harness and instrumentation
Documentation/doc-guide/sphinx.rst | 100 ++
Documentation/sphinx-static/custom.css | 288 ++++
Documentation/sphinx-static/kernel-search.js | 1264 ++++++++++++++++
Documentation/sphinx/templates/search.html | 117 ++
Documentation/sphinx/templates/searchbox.html | 30 +
MAINTAINERS | 11 +
tools/docs/bench_search_playwright.mjs | 1278 +++++++++++++++++
tools/docs/test_advanced_search.py | 312 ++++
8 files changed, 3400 insertions(+)
create mode 100644 Documentation/sphinx-static/kernel-search.js
create mode 100644 Documentation/sphinx/templates/search.html
create mode 100644 Documentation/sphinx/templates/searchbox.html
create mode 100755 tools/docs/bench_search_playwright.mjs
create mode 100755 tools/docs/test_advanced_search.py
--
2.51.0
^ permalink raw reply
* Re: [PATCH] hwmon: (asus-ec-sensors) add ROG STRIX B650E-E GAMING WIFI
From: Eugene Shalygin @ 2026-04-04 7:12 UTC (permalink / raw)
To: Guenter Roeck
Cc: Veronika Kossmann, Veronika Kossmann, Jonathan Corbet, Shuah Khan,
linux-hwmon, linux-doc, linux-kernel
In-Reply-To: <05e9870a-5d8c-410d-99ed-6ef9470b2ff7@roeck-us.net>
On Sat, 4 Apr 2026 at 06:38, Guenter Roeck <linux@roeck-us.net> wrote:
> Sashiko has a problem with this patch:
I must admit now, that these _SET macros were a bad idea, it turned
out to be too easy to misread. I'm going to remove them.
Veronika, could you, please, show us the output from sensors with this
version of the code?
Cheers,
Eugene
^ permalink raw reply
* Re: [PATCH v12 1/2] dt-bindings: hwmon: add support for MCP998X
From: Krzysztof Kozlowski @ 2026-04-04 7:04 UTC (permalink / raw)
To: Victor Duicu
Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jonathan Corbet, linux-hwmon, devicetree, linux-kernel, linux-doc,
marius.cristea
In-Reply-To: <20260403-add-mcp9982-hwmon-v12-1-b3bfb26ff136@microchip.com>
On Fri, Apr 03, 2026 at 04:32:16PM +0300, Victor Duicu wrote:
> Add devicetree schema for Microchip MCP998X/33 and MCP998XD/33D
> Multichannel Automotive Temperature Monitor Family.
>
> Signed-off-by: Victor Duicu <victor.duicu@microchip.com>
> ---
> .../bindings/hwmon/microchip,mcp9982.yaml | 237 +++++++++++++++++++++
> MAINTAINERS | 6 +
> 2 files changed, 243 insertions(+)
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
Best regards,
Krzysztof
^ permalink raw reply
* Re: [PATCH] hwmon: (asus-ec-sensors) add ROG STRIX B650E-E GAMING WIFI
From: Guenter Roeck @ 2026-04-04 4:38 UTC (permalink / raw)
To: Eugene Shalygin
Cc: Veronika Kossmann, Veronika Kossmann, Jonathan Corbet, Shuah Khan,
linux-hwmon, linux-doc, linux-kernel
In-Reply-To: <20260403210343.1380437-1-eugene.shalygin@gmail.com>
On 4/3/26 14:03, Eugene Shalygin wrote:
> From: Veronika Kossmann <nanodesuu@gmail.com>
>
> Add support for ROG STRIX B650E-E GAMING WIFI
>
> Signed-off-by: Veronika Kossmann <desu.git@rxtx.cx>
> Signed-off-by: Eugene Shalygin <eugene.shalygin@gmail.com>
Sashiko has a problem with this patch:
https://sashiko.dev/#/patchset/20260403210343.1380437-1-eugene.shalygin%40gmail.com
I never paid attention, but seems to me that it has a point.
Assuming the concern is valid, that makes me wonder: Do other boards
have similar problems ?
Thanks,
Guenter
> ---
> Documentation/hwmon/asus_ec_sensors.rst | 1 +
> drivers/hwmon/asus-ec-sensors.c | 11 ++++++++++-
> 2 files changed, 11 insertions(+), 1 deletion(-)
>
> diff --git a/Documentation/hwmon/asus_ec_sensors.rst b/Documentation/hwmon/asus_ec_sensors.rst
> index 9ad3f0a57f55..e14419811aac 100644
> --- a/Documentation/hwmon/asus_ec_sensors.rst
> +++ b/Documentation/hwmon/asus_ec_sensors.rst
> @@ -31,6 +31,7 @@ Supported boards:
> * ROG MAXIMUS Z690 FORMULA
> * ROG STRIX B550-E GAMING
> * ROG STRIX B550-I GAMING
> + * ROG STRIX B650E-E GAMING WIFI
> * ROG STRIX B650E-I GAMING WIFI
> * ROG STRIX B850-I GAMING WIFI
> * ROG STRIX X470-F GAMING
> diff --git a/drivers/hwmon/asus-ec-sensors.c b/drivers/hwmon/asus-ec-sensors.c
> index 070bb368f2b7..8c53cd9ed8f3 100644
> --- a/drivers/hwmon/asus-ec-sensors.c
> +++ b/drivers/hwmon/asus-ec-sensors.c
> @@ -274,7 +274,7 @@ static const struct ec_sensor_info sensors_family_amd_600[] = {
> [ec_sensor_temp_cpu_package] =
> EC_SENSOR("CPU Package", hwmon_temp, 1, 0x00, 0x31),
> [ec_sensor_temp_mb] =
> - EC_SENSOR("Motherboard", hwmon_temp, 1, 0x00, 0x32),
> + EC_SENSOR("Motherboard", hwmon_temp, 1, 0x00, 0x32),
> [ec_sensor_temp_vrm] =
> EC_SENSOR("VRM", hwmon_temp, 1, 0x00, 0x33),
> [ec_sensor_temp_t_sensor] =
> @@ -616,6 +616,13 @@ static const struct ec_board_info board_info_strix_b550_i_gaming = {
> .family = family_amd_500_series,
> };
>
> +static const struct ec_board_info board_info_strix_b650e_e_gaming = {
> + .sensors = SENSOR_TEMP_VRM | SENSOR_SET_TEMP_CHIPSET_CPU_MB |
> + SENSOR_IN_CPU_CORE,
> + .mutex_path = ASUS_HW_ACCESS_MUTEX_SB_PCI0_SBRG_SIO1_MUT0,
> + .family = family_amd_600_series,
> +};
> +
> static const struct ec_board_info board_info_strix_b650e_i_gaming = {
> .sensors = SENSOR_TEMP_VRM | SENSOR_TEMP_T_SENSOR |
> SENSOR_SET_TEMP_CHIPSET_CPU_MB | SENSOR_IN_CPU_CORE,
> @@ -861,6 +868,8 @@ static const struct dmi_system_id dmi_table[] = {
> &board_info_strix_b550_e_gaming),
> DMI_EXACT_MATCH_ASUS_BOARD_NAME("ROG STRIX B550-I GAMING",
> &board_info_strix_b550_i_gaming),
> + DMI_EXACT_MATCH_ASUS_BOARD_NAME("ROG STRIX B650E-E GAMING WIFI",
> + &board_info_strix_b650e_e_gaming),
> DMI_EXACT_MATCH_ASUS_BOARD_NAME("ROG STRIX B650E-I GAMING WIFI",
> &board_info_strix_b650e_i_gaming),
> DMI_EXACT_MATCH_ASUS_BOARD_NAME("ROG STRIX B850-I GAMING WIFI",
^ permalink raw reply
* Re: [PATCH v9 02/10] x86/bhi: Make clear_bhb_loop() effective on newer CPUs
From: Pawan Gupta @ 2026-04-04 3:49 UTC (permalink / raw)
To: Jim Mattson
Cc: x86, Jon Kohler, Nikolay Borisov, H. Peter Anvin, Josh Poimboeuf,
David Kaplan, Sean Christopherson, Borislav Petkov, Dave Hansen,
Peter Zijlstra, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, KP Singh, Jiri Olsa, David S. Miller,
David Laight, Andy Lutomirski, Thomas Gleixner, Ingo Molnar,
David Ahern, Martin KaFai Lau, Eduard Zingerman, Song Liu,
Yonghong Song, John Fastabend, Stanislav Fomichev, Hao Luo,
Paolo Bonzini, Jonathan Corbet, linux-kernel, kvm, Asit Mallick,
Tao Zhang, bpf, netdev, linux-doc, chao.gao
In-Reply-To: <CALMp9eSqgL5q-MY1xpjqR5oRn5_cb=mfEhNFWusNneS=Mx8UMg@mail.gmail.com>
On Fri, Apr 03, 2026 at 07:21:02PM -0700, Jim Mattson wrote:
> On Fri, Apr 3, 2026 at 5:22 PM Pawan Gupta
> <pawan.kumar.gupta@linux.intel.com> wrote:
> >
> > On Fri, Apr 03, 2026 at 04:39:54PM -0700, Jim Mattson wrote:
> > > > Since cloud providers have greater control over userspace, the decision to
> > > > use BHI_DIS_S or not can be left to them. KVM would simply follow what it
> > > > is asked to do by the userspace.
> > >
> > > I feel like we've gone over this before, but if userspace tells KVM
> > > not to enable BHI_DIS_S, how do we inform Windows that it needs to do
> > > the longer clearing sequence, despite the fact that the virtual CPU is
> > > masquerading as Ice Lake?
> >
> > IMO, if an OS is allergic to a hardware mitigation, and is also aware that
> > it is virtualized, it should default to a sw mitigation that works everywhere.
>
> Agreed. So, without any information to the contrary, VMs should assume
> the long BHB clearing sequence is required.
>
> Returning to my earlier comment, the test should be:
>
> + if (cpu_feature_enabled(X86_FEATURE_BHI_CTRL) ||
> cpu_feature_enabled(X86_FEATURE_HYPERVISOR)) {
> + bhb_seq_outer_loop = 12;
> + bhb_seq_inner_loop = 7;
> + }
To be clear, my comment was for an OS that doesn't want BHI_DIS_S
under-the-hood with virtual-SPEC_CTRL. Linux doesn't have that problem,
hardware mitigation on Linux is perfectly okay.
Without virtual-SPEC_CTRL, the problem set is limited to guests that
migrate accross Alder Lake generation CPUs. As you mentioned the change in
MAXPHYADDR makes it unlikely.
With virtual-SPEC_CTRL support, guests that fall into the subset that
migrate inspite of MAXPHYADDR change would also be mitigated. Then, on top
of hardware mitigation, deploying the long sequence in the guest would
incur a significant performance penalty for no good reason.
^ permalink raw reply
* Re: [PATCH v8 1/3] RISC-V: KVM: Support runtime configuration for per-VM's HGATP mode
From: Nutty.Liu @ 2026-04-04 3:27 UTC (permalink / raw)
To: fangyu.yu, pbonzini, corbet, anup, atish.patra, pjw, palmer, aou,
alex, skhan
Cc: guoren, radim.krcmar, andrew.jones, linux-doc, kvm, kvm-riscv,
linux-riscv, linux-kernel
In-Reply-To: <20260403153019.9916-2-fangyu.yu@linux.alibaba.com>
On 4/3/2026 11:30 PM, fangyu.yu@linux.alibaba.com wrote:
> From: Fangyu Yu <fangyu.yu@linux.alibaba.com>
>
> Introduces one per-VM architecture-specific fields to support runtime
> configuration of the G-stage page table format:
>
> - kvm->arch.pgd_levels: the corresponding number of page table levels
> for the selected mode.
>
> These fields replace the previous global variables
> kvm_riscv_gstage_mode and kvm_riscv_gstage_pgd_levels, enabling different
> virtual machines to independently select their G-stage page table format
> instead of being forced to share the maximum mode detected by the kernel
> at boot time.
>
> Signed-off-by: Fangyu Yu <fangyu.yu@linux.alibaba.com>
> Reviewed-by: Andrew Jones <andrew.jones@oss.qualcomm.com>
> Reviewed-by: Anup Patel <anup@brainfault.org>
> Reviewed-by: Guo Ren <guoren@kernel.org>
Reviewed-by: Nutty Liu <nutty.liu@hotmail.com>
Thanks,
Nutty
> ---
> arch/riscv/include/asm/kvm_gstage.h | 37 ++++++++++++----
> arch/riscv/include/asm/kvm_host.h | 1 +
> arch/riscv/kvm/gstage.c | 65 ++++++++++++++---------------
> arch/riscv/kvm/main.c | 12 +++---
> arch/riscv/kvm/mmu.c | 20 +++++----
> arch/riscv/kvm/vm.c | 5 ++-
> arch/riscv/kvm/vmid.c | 3 +-
> 7 files changed, 86 insertions(+), 57 deletions(-)
>
> diff --git a/arch/riscv/include/asm/kvm_gstage.h b/arch/riscv/include/asm/kvm_gstage.h
> index 595e2183173e..5aa58d1f692a 100644
> --- a/arch/riscv/include/asm/kvm_gstage.h
> +++ b/arch/riscv/include/asm/kvm_gstage.h
> @@ -29,16 +29,22 @@ struct kvm_gstage_mapping {
> #define kvm_riscv_gstage_index_bits 10
> #endif
>
> -extern unsigned long kvm_riscv_gstage_mode;
> -extern unsigned long kvm_riscv_gstage_pgd_levels;
> +extern unsigned long kvm_riscv_gstage_max_pgd_levels;
>
> #define kvm_riscv_gstage_pgd_xbits 2
> #define kvm_riscv_gstage_pgd_size (1UL << (HGATP_PAGE_SHIFT + kvm_riscv_gstage_pgd_xbits))
> -#define kvm_riscv_gstage_gpa_bits (HGATP_PAGE_SHIFT + \
> - (kvm_riscv_gstage_pgd_levels * \
> - kvm_riscv_gstage_index_bits) + \
> - kvm_riscv_gstage_pgd_xbits)
> -#define kvm_riscv_gstage_gpa_size ((gpa_t)(1ULL << kvm_riscv_gstage_gpa_bits))
> +
> +static inline unsigned long kvm_riscv_gstage_gpa_bits(unsigned long pgd_levels)
> +{
> + return (HGATP_PAGE_SHIFT +
> + pgd_levels * kvm_riscv_gstage_index_bits +
> + kvm_riscv_gstage_pgd_xbits);
> +}
> +
> +static inline gpa_t kvm_riscv_gstage_gpa_size(unsigned long pgd_levels)
> +{
> + return BIT_ULL(kvm_riscv_gstage_gpa_bits(pgd_levels));
> +}
>
> bool kvm_riscv_gstage_get_leaf(struct kvm_gstage *gstage, gpa_t addr,
> pte_t **ptepp, u32 *ptep_level);
> @@ -69,4 +75,21 @@ void kvm_riscv_gstage_wp_range(struct kvm_gstage *gstage, gpa_t start, gpa_t end
>
> void kvm_riscv_gstage_mode_detect(void);
>
> +static inline unsigned long kvm_riscv_gstage_mode(unsigned long pgd_levels)
> +{
> + switch (pgd_levels) {
> + case 2:
> + return HGATP_MODE_SV32X4;
> + case 3:
> + return HGATP_MODE_SV39X4;
> + case 4:
> + return HGATP_MODE_SV48X4;
> + case 5:
> + return HGATP_MODE_SV57X4;
> + default:
> + WARN_ON_ONCE(1);
> + return HGATP_MODE_OFF;
> + }
> +}
> +
> #endif
> diff --git a/arch/riscv/include/asm/kvm_host.h b/arch/riscv/include/asm/kvm_host.h
> index 24585304c02b..478f699e9dec 100644
> --- a/arch/riscv/include/asm/kvm_host.h
> +++ b/arch/riscv/include/asm/kvm_host.h
> @@ -94,6 +94,7 @@ struct kvm_arch {
> /* G-stage page table */
> pgd_t *pgd;
> phys_addr_t pgd_phys;
> + unsigned long pgd_levels;
>
> /* Guest Timer */
> struct kvm_guest_timer timer;
> diff --git a/arch/riscv/kvm/gstage.c b/arch/riscv/kvm/gstage.c
> index b67d60d722c2..4beb9322fe76 100644
> --- a/arch/riscv/kvm/gstage.c
> +++ b/arch/riscv/kvm/gstage.c
> @@ -12,22 +12,21 @@
> #include <asm/kvm_gstage.h>
>
> #ifdef CONFIG_64BIT
> -unsigned long kvm_riscv_gstage_mode __ro_after_init = HGATP_MODE_SV39X4;
> -unsigned long kvm_riscv_gstage_pgd_levels __ro_after_init = 3;
> +unsigned long kvm_riscv_gstage_max_pgd_levels __ro_after_init = 3;
> #else
> -unsigned long kvm_riscv_gstage_mode __ro_after_init = HGATP_MODE_SV32X4;
> -unsigned long kvm_riscv_gstage_pgd_levels __ro_after_init = 2;
> +unsigned long kvm_riscv_gstage_max_pgd_levels __ro_after_init = 2;
> #endif
>
> #define gstage_pte_leaf(__ptep) \
> (pte_val(*(__ptep)) & (_PAGE_READ | _PAGE_WRITE | _PAGE_EXEC))
>
> -static inline unsigned long gstage_pte_index(gpa_t addr, u32 level)
> +static inline unsigned long gstage_pte_index(struct kvm_gstage *gstage,
> + gpa_t addr, u32 level)
> {
> unsigned long mask;
> unsigned long shift = HGATP_PAGE_SHIFT + (kvm_riscv_gstage_index_bits * level);
>
> - if (level == (kvm_riscv_gstage_pgd_levels - 1))
> + if (level == gstage->kvm->arch.pgd_levels - 1)
> mask = (PTRS_PER_PTE * (1UL << kvm_riscv_gstage_pgd_xbits)) - 1;
> else
> mask = PTRS_PER_PTE - 1;
> @@ -40,12 +39,13 @@ static inline unsigned long gstage_pte_page_vaddr(pte_t pte)
> return (unsigned long)pfn_to_virt(__page_val_to_pfn(pte_val(pte)));
> }
>
> -static int gstage_page_size_to_level(unsigned long page_size, u32 *out_level)
> +static int gstage_page_size_to_level(struct kvm_gstage *gstage, unsigned long page_size,
> + u32 *out_level)
> {
> u32 i;
> unsigned long psz = 1UL << 12;
>
> - for (i = 0; i < kvm_riscv_gstage_pgd_levels; i++) {
> + for (i = 0; i < gstage->kvm->arch.pgd_levels; i++) {
> if (page_size == (psz << (i * kvm_riscv_gstage_index_bits))) {
> *out_level = i;
> return 0;
> @@ -55,21 +55,23 @@ static int gstage_page_size_to_level(unsigned long page_size, u32 *out_level)
> return -EINVAL;
> }
>
> -static int gstage_level_to_page_order(u32 level, unsigned long *out_pgorder)
> +static int gstage_level_to_page_order(struct kvm_gstage *gstage, u32 level,
> + unsigned long *out_pgorder)
> {
> - if (kvm_riscv_gstage_pgd_levels < level)
> + if (gstage->kvm->arch.pgd_levels < level)
> return -EINVAL;
>
> *out_pgorder = 12 + (level * kvm_riscv_gstage_index_bits);
> return 0;
> }
>
> -static int gstage_level_to_page_size(u32 level, unsigned long *out_pgsize)
> +static int gstage_level_to_page_size(struct kvm_gstage *gstage, u32 level,
> + unsigned long *out_pgsize)
> {
> int rc;
> unsigned long page_order = PAGE_SHIFT;
>
> - rc = gstage_level_to_page_order(level, &page_order);
> + rc = gstage_level_to_page_order(gstage, level, &page_order);
> if (rc)
> return rc;
>
> @@ -81,11 +83,11 @@ bool kvm_riscv_gstage_get_leaf(struct kvm_gstage *gstage, gpa_t addr,
> pte_t **ptepp, u32 *ptep_level)
> {
> pte_t *ptep;
> - u32 current_level = kvm_riscv_gstage_pgd_levels - 1;
> + u32 current_level = gstage->kvm->arch.pgd_levels - 1;
>
> *ptep_level = current_level;
> ptep = (pte_t *)gstage->pgd;
> - ptep = &ptep[gstage_pte_index(addr, current_level)];
> + ptep = &ptep[gstage_pte_index(gstage, addr, current_level)];
> while (ptep && pte_val(ptep_get(ptep))) {
> if (gstage_pte_leaf(ptep)) {
> *ptep_level = current_level;
> @@ -97,7 +99,7 @@ bool kvm_riscv_gstage_get_leaf(struct kvm_gstage *gstage, gpa_t addr,
> current_level--;
> *ptep_level = current_level;
> ptep = (pte_t *)gstage_pte_page_vaddr(ptep_get(ptep));
> - ptep = &ptep[gstage_pte_index(addr, current_level)];
> + ptep = &ptep[gstage_pte_index(gstage, addr, current_level)];
> } else {
> ptep = NULL;
> }
> @@ -110,7 +112,7 @@ static void gstage_tlb_flush(struct kvm_gstage *gstage, u32 level, gpa_t addr)
> {
> unsigned long order = PAGE_SHIFT;
>
> - if (gstage_level_to_page_order(level, &order))
> + if (gstage_level_to_page_order(gstage, level, &order))
> return;
> addr &= ~(BIT(order) - 1);
>
> @@ -125,9 +127,9 @@ int kvm_riscv_gstage_set_pte(struct kvm_gstage *gstage,
> struct kvm_mmu_memory_cache *pcache,
> const struct kvm_gstage_mapping *map)
> {
> - u32 current_level = kvm_riscv_gstage_pgd_levels - 1;
> + u32 current_level = gstage->kvm->arch.pgd_levels - 1;
> pte_t *next_ptep = (pte_t *)gstage->pgd;
> - pte_t *ptep = &next_ptep[gstage_pte_index(map->addr, current_level)];
> + pte_t *ptep = &next_ptep[gstage_pte_index(gstage, map->addr, current_level)];
>
> if (current_level < map->level)
> return -EINVAL;
> @@ -151,7 +153,7 @@ int kvm_riscv_gstage_set_pte(struct kvm_gstage *gstage,
> }
>
> current_level--;
> - ptep = &next_ptep[gstage_pte_index(map->addr, current_level)];
> + ptep = &next_ptep[gstage_pte_index(gstage, map->addr, current_level)];
> }
>
> if (pte_val(*ptep) != pte_val(map->pte)) {
> @@ -175,7 +177,7 @@ int kvm_riscv_gstage_map_page(struct kvm_gstage *gstage,
> out_map->addr = gpa;
> out_map->level = 0;
>
> - ret = gstage_page_size_to_level(page_size, &out_map->level);
> + ret = gstage_page_size_to_level(gstage, page_size, &out_map->level);
> if (ret)
> return ret;
>
> @@ -217,7 +219,7 @@ void kvm_riscv_gstage_op_pte(struct kvm_gstage *gstage, gpa_t addr,
> u32 next_ptep_level;
> unsigned long next_page_size, page_size;
>
> - ret = gstage_level_to_page_size(ptep_level, &page_size);
> + ret = gstage_level_to_page_size(gstage, ptep_level, &page_size);
> if (ret)
> return;
>
> @@ -229,7 +231,7 @@ void kvm_riscv_gstage_op_pte(struct kvm_gstage *gstage, gpa_t addr,
> if (ptep_level && !gstage_pte_leaf(ptep)) {
> next_ptep = (pte_t *)gstage_pte_page_vaddr(ptep_get(ptep));
> next_ptep_level = ptep_level - 1;
> - ret = gstage_level_to_page_size(next_ptep_level, &next_page_size);
> + ret = gstage_level_to_page_size(gstage, next_ptep_level, &next_page_size);
> if (ret)
> return;
>
> @@ -263,7 +265,7 @@ void kvm_riscv_gstage_unmap_range(struct kvm_gstage *gstage,
>
> while (addr < end) {
> found_leaf = kvm_riscv_gstage_get_leaf(gstage, addr, &ptep, &ptep_level);
> - ret = gstage_level_to_page_size(ptep_level, &page_size);
> + ret = gstage_level_to_page_size(gstage, ptep_level, &page_size);
> if (ret)
> break;
>
> @@ -297,7 +299,7 @@ void kvm_riscv_gstage_wp_range(struct kvm_gstage *gstage, gpa_t start, gpa_t end
>
> while (addr < end) {
> found_leaf = kvm_riscv_gstage_get_leaf(gstage, addr, &ptep, &ptep_level);
> - ret = gstage_level_to_page_size(ptep_level, &page_size);
> + ret = gstage_level_to_page_size(gstage, ptep_level, &page_size);
> if (ret)
> break;
>
> @@ -319,39 +321,34 @@ void __init kvm_riscv_gstage_mode_detect(void)
> /* Try Sv57x4 G-stage mode */
> csr_write(CSR_HGATP, HGATP_MODE_SV57X4 << HGATP_MODE_SHIFT);
> if ((csr_read(CSR_HGATP) >> HGATP_MODE_SHIFT) == HGATP_MODE_SV57X4) {
> - kvm_riscv_gstage_mode = HGATP_MODE_SV57X4;
> - kvm_riscv_gstage_pgd_levels = 5;
> + kvm_riscv_gstage_max_pgd_levels = 5;
> goto done;
> }
>
> /* Try Sv48x4 G-stage mode */
> csr_write(CSR_HGATP, HGATP_MODE_SV48X4 << HGATP_MODE_SHIFT);
> if ((csr_read(CSR_HGATP) >> HGATP_MODE_SHIFT) == HGATP_MODE_SV48X4) {
> - kvm_riscv_gstage_mode = HGATP_MODE_SV48X4;
> - kvm_riscv_gstage_pgd_levels = 4;
> + kvm_riscv_gstage_max_pgd_levels = 4;
> goto done;
> }
>
> /* Try Sv39x4 G-stage mode */
> csr_write(CSR_HGATP, HGATP_MODE_SV39X4 << HGATP_MODE_SHIFT);
> if ((csr_read(CSR_HGATP) >> HGATP_MODE_SHIFT) == HGATP_MODE_SV39X4) {
> - kvm_riscv_gstage_mode = HGATP_MODE_SV39X4;
> - kvm_riscv_gstage_pgd_levels = 3;
> + kvm_riscv_gstage_max_pgd_levels = 3;
> goto done;
> }
> #else /* CONFIG_32BIT */
> /* Try Sv32x4 G-stage mode */
> csr_write(CSR_HGATP, HGATP_MODE_SV32X4 << HGATP_MODE_SHIFT);
> if ((csr_read(CSR_HGATP) >> HGATP_MODE_SHIFT) == HGATP_MODE_SV32X4) {
> - kvm_riscv_gstage_mode = HGATP_MODE_SV32X4;
> - kvm_riscv_gstage_pgd_levels = 2;
> + kvm_riscv_gstage_max_pgd_levels = 2;
> goto done;
> }
> #endif
>
> /* KVM depends on !HGATP_MODE_OFF */
> - kvm_riscv_gstage_mode = HGATP_MODE_OFF;
> - kvm_riscv_gstage_pgd_levels = 0;
> + kvm_riscv_gstage_max_pgd_levels = 0;
>
> done:
> csr_write(CSR_HGATP, 0);
> diff --git a/arch/riscv/kvm/main.c b/arch/riscv/kvm/main.c
> index 0f3fe3986fc0..90ee0a032b9a 100644
> --- a/arch/riscv/kvm/main.c
> +++ b/arch/riscv/kvm/main.c
> @@ -105,17 +105,17 @@ static int __init riscv_kvm_init(void)
> return rc;
>
> kvm_riscv_gstage_mode_detect();
> - switch (kvm_riscv_gstage_mode) {
> - case HGATP_MODE_SV32X4:
> + switch (kvm_riscv_gstage_max_pgd_levels) {
> + case 2:
> str = "Sv32x4";
> break;
> - case HGATP_MODE_SV39X4:
> + case 3:
> str = "Sv39x4";
> break;
> - case HGATP_MODE_SV48X4:
> + case 4:
> str = "Sv48x4";
> break;
> - case HGATP_MODE_SV57X4:
> + case 5:
> str = "Sv57x4";
> break;
> default:
> @@ -164,7 +164,7 @@ static int __init riscv_kvm_init(void)
> (rc) ? slist : "no features");
> }
>
> - kvm_info("using %s G-stage page table format\n", str);
> + kvm_info("highest G-stage page table mode is %s\n", str);
>
> kvm_info("VMID %ld bits available\n", kvm_riscv_gstage_vmid_bits());
>
> diff --git a/arch/riscv/kvm/mmu.c b/arch/riscv/kvm/mmu.c
> index 088d33ba90ed..fbcdd75cb9af 100644
> --- a/arch/riscv/kvm/mmu.c
> +++ b/arch/riscv/kvm/mmu.c
> @@ -67,7 +67,7 @@ int kvm_riscv_mmu_ioremap(struct kvm *kvm, gpa_t gpa, phys_addr_t hpa,
> if (!writable)
> map.pte = pte_wrprotect(map.pte);
>
> - ret = kvm_mmu_topup_memory_cache(&pcache, kvm_riscv_gstage_pgd_levels);
> + ret = kvm_mmu_topup_memory_cache(&pcache, kvm->arch.pgd_levels);
> if (ret)
> goto out;
>
> @@ -186,7 +186,7 @@ int kvm_arch_prepare_memory_region(struct kvm *kvm,
> * space addressable by the KVM guest GPA space.
> */
> if ((new->base_gfn + new->npages) >=
> - (kvm_riscv_gstage_gpa_size >> PAGE_SHIFT))
> + kvm_riscv_gstage_gpa_size(kvm->arch.pgd_levels) >> PAGE_SHIFT)
> return -EFAULT;
>
> hva = new->userspace_addr;
> @@ -472,7 +472,7 @@ int kvm_riscv_mmu_map(struct kvm_vcpu *vcpu, struct kvm_memory_slot *memslot,
> memset(out_map, 0, sizeof(*out_map));
>
> /* We need minimum second+third level pages */
> - ret = kvm_mmu_topup_memory_cache(pcache, kvm_riscv_gstage_pgd_levels);
> + ret = kvm_mmu_topup_memory_cache(pcache, kvm->arch.pgd_levels);
> if (ret) {
> kvm_err("Failed to topup G-stage cache\n");
> return ret;
> @@ -575,6 +575,7 @@ int kvm_riscv_mmu_alloc_pgd(struct kvm *kvm)
> return -ENOMEM;
> kvm->arch.pgd = page_to_virt(pgd_page);
> kvm->arch.pgd_phys = page_to_phys(pgd_page);
> + kvm->arch.pgd_levels = kvm_riscv_gstage_max_pgd_levels;
>
> return 0;
> }
> @@ -590,10 +591,12 @@ void kvm_riscv_mmu_free_pgd(struct kvm *kvm)
> gstage.flags = 0;
> gstage.vmid = READ_ONCE(kvm->arch.vmid.vmid);
> gstage.pgd = kvm->arch.pgd;
> - kvm_riscv_gstage_unmap_range(&gstage, 0UL, kvm_riscv_gstage_gpa_size, false);
> + kvm_riscv_gstage_unmap_range(&gstage, 0UL,
> + kvm_riscv_gstage_gpa_size(kvm->arch.pgd_levels), false);
> pgd = READ_ONCE(kvm->arch.pgd);
> kvm->arch.pgd = NULL;
> kvm->arch.pgd_phys = 0;
> + kvm->arch.pgd_levels = 0;
> }
> spin_unlock(&kvm->mmu_lock);
>
> @@ -603,11 +606,12 @@ void kvm_riscv_mmu_free_pgd(struct kvm *kvm)
>
> void kvm_riscv_mmu_update_hgatp(struct kvm_vcpu *vcpu)
> {
> - unsigned long hgatp = kvm_riscv_gstage_mode << HGATP_MODE_SHIFT;
> - struct kvm_arch *k = &vcpu->kvm->arch;
> + struct kvm_arch *ka = &vcpu->kvm->arch;
> + unsigned long hgatp = kvm_riscv_gstage_mode(ka->pgd_levels)
> + << HGATP_MODE_SHIFT;
>
> - hgatp |= (READ_ONCE(k->vmid.vmid) << HGATP_VMID_SHIFT) & HGATP_VMID;
> - hgatp |= (k->pgd_phys >> PAGE_SHIFT) & HGATP_PPN;
> + hgatp |= (READ_ONCE(ka->vmid.vmid) << HGATP_VMID_SHIFT) & HGATP_VMID;
> + hgatp |= (ka->pgd_phys >> PAGE_SHIFT) & HGATP_PPN;
>
> ncsr_write(CSR_HGATP, hgatp);
>
> diff --git a/arch/riscv/kvm/vm.c b/arch/riscv/kvm/vm.c
> index 13c63ae1a78b..fb7c4e07961f 100644
> --- a/arch/riscv/kvm/vm.c
> +++ b/arch/riscv/kvm/vm.c
> @@ -199,7 +199,10 @@ int kvm_vm_ioctl_check_extension(struct kvm *kvm, long ext)
> r = KVM_USER_MEM_SLOTS;
> break;
> case KVM_CAP_VM_GPA_BITS:
> - r = kvm_riscv_gstage_gpa_bits;
> + if (!kvm)
> + r = kvm_riscv_gstage_gpa_bits(kvm_riscv_gstage_max_pgd_levels);
> + else
> + r = kvm_riscv_gstage_gpa_bits(kvm->arch.pgd_levels);
> break;
> default:
> r = 0;
> diff --git a/arch/riscv/kvm/vmid.c b/arch/riscv/kvm/vmid.c
> index cf34d448289d..c15bdb1dd8be 100644
> --- a/arch/riscv/kvm/vmid.c
> +++ b/arch/riscv/kvm/vmid.c
> @@ -26,7 +26,8 @@ static DEFINE_SPINLOCK(vmid_lock);
> void __init kvm_riscv_gstage_vmid_detect(void)
> {
> /* Figure-out number of VMID bits in HW */
> - csr_write(CSR_HGATP, (kvm_riscv_gstage_mode << HGATP_MODE_SHIFT) | HGATP_VMID);
> + csr_write(CSR_HGATP, (kvm_riscv_gstage_mode(kvm_riscv_gstage_max_pgd_levels) <<
> + HGATP_MODE_SHIFT) | HGATP_VMID);
> vmid_bits = csr_read(CSR_HGATP);
> vmid_bits = (vmid_bits & HGATP_VMID) >> HGATP_VMID_SHIFT;
> vmid_bits = fls_long(vmid_bits);
^ permalink raw reply
* Re: [PATCH v8 3/3] RISC-V: KVM: Reuse KVM_CAP_VM_GPA_BITS to select HGATP.MODE
From: Nutty.Liu @ 2026-04-04 3:25 UTC (permalink / raw)
To: fangyu.yu, pbonzini, corbet, anup, atish.patra, pjw, palmer, aou,
alex, skhan
Cc: guoren, radim.krcmar, andrew.jones, linux-doc, kvm, kvm-riscv,
linux-riscv, linux-kernel
In-Reply-To: <20260403153019.9916-4-fangyu.yu@linux.alibaba.com>
On 4/3/2026 11:30 PM, fangyu.yu@linux.alibaba.com wrote:
> From: Fangyu Yu <fangyu.yu@linux.alibaba.com>
>
> Reuse KVM_CAP_VM_GPA_BITS to advertise and select the effective
> G-stage GPA width for a VM.
>
> KVM_CHECK_EXTENSION(KVM_CAP_VM_GPA_BITS) returns the effective GPA
> bits for a VM, KVM_ENABLE_CAP(KVM_CAP_VM_GPA_BITS) allows userspace
> to downsize the effective GPA width by selecting a smaller G-stage
> page table format:
> - gpa_bits <= 41 selects Sv39x4 (pgd_levels=3)
> - gpa_bits <= 50 selects Sv48x4 (pgd_levels=4)
> - gpa_bits <= 59 selects Sv57x4 (pgd_levels=5)
>
> Reject the request with -EINVAL for unsupported values and with -EBUSY
> if vCPUs have been created or any memslot is populated.
>
> Signed-off-by: Fangyu Yu <fangyu.yu@linux.alibaba.com>
> Reviewed-by: Andrew Jones <andrew.jones@oss.qualcomm.com>
> Reviewed-by: Guo Ren <guoren@kernel.org>
Reviewed-by: Nutty Liu <nutty.liu@hotmail.com>
Thanks,
Nutty
> ---
> arch/riscv/kvm/vm.c | 44 ++++++++++++++++++++++++++++++++++++++++++--
> 1 file changed, 42 insertions(+), 2 deletions(-)
>
> diff --git a/arch/riscv/kvm/vm.c b/arch/riscv/kvm/vm.c
> index fb7c4e07961f..a9f083feeb76 100644
> --- a/arch/riscv/kvm/vm.c
> +++ b/arch/riscv/kvm/vm.c
> @@ -214,12 +214,52 @@ int kvm_vm_ioctl_check_extension(struct kvm *kvm, long ext)
>
> int kvm_vm_ioctl_enable_cap(struct kvm *kvm, struct kvm_enable_cap *cap)
> {
> + if (cap->flags)
> + return -EINVAL;
> +
> switch (cap->cap) {
> case KVM_CAP_RISCV_MP_STATE_RESET:
> - if (cap->flags)
> - return -EINVAL;
> kvm->arch.mp_state_reset = true;
> return 0;
> + case KVM_CAP_VM_GPA_BITS: {
> + unsigned long gpa_bits = cap->args[0];
> + unsigned long new_levels;
> + int r = 0;
> +
> + /* Decide target pgd levels from requested gpa_bits */
> +#ifdef CONFIG_64BIT
> + if (gpa_bits <= 41)
> + new_levels = 3; /* Sv39x4 */
> + else if (gpa_bits <= 50)
> + new_levels = 4; /* Sv48x4 */
> + else if (gpa_bits <= 59)
> + new_levels = 5; /* Sv57x4 */
> + else
> + return -EINVAL;
> +#else
> + /* 32-bit: only Sv32x4*/
> + if (gpa_bits <= 34)
> + new_levels = 2;
> + else
> + return -EINVAL;
> +#endif
> + if (new_levels > kvm_riscv_gstage_max_pgd_levels)
> + return -EINVAL;
> +
> + /* Follow KVM's lock ordering: kvm->lock -> kvm->slots_lock. */
> + mutex_lock(&kvm->lock);
> + mutex_lock(&kvm->slots_lock);
> +
> + if (kvm->created_vcpus || !kvm_are_all_memslots_empty(kvm))
> + r = -EBUSY;
> + else
> + kvm->arch.pgd_levels = new_levels;
> +
> + mutex_unlock(&kvm->slots_lock);
> + mutex_unlock(&kvm->lock);
> +
> + return r;
> + }
> default:
> return -EINVAL;
> }
^ permalink raw reply
* Re: [PATCH v8 2/3] RISC-V: KVM: Cache gstage pgd_levels in struct kvm_gstage
From: Nutty.Liu @ 2026-04-04 3:25 UTC (permalink / raw)
To: fangyu.yu, pbonzini, corbet, anup, atish.patra, pjw, palmer, aou,
alex, skhan
Cc: guoren, radim.krcmar, andrew.jones, linux-doc, kvm, kvm-riscv,
linux-riscv, linux-kernel
In-Reply-To: <20260403153019.9916-3-fangyu.yu@linux.alibaba.com>
On 4/3/2026 11:30 PM, fangyu.yu@linux.alibaba.com wrote:
> From: Fangyu Yu <fangyu.yu@linux.alibaba.com>
>
> Gstage page-table helpers frequently chase gstage->kvm->arch to
> fetch pgd_levels. This adds noise and repeats the same dereference
> chain in hot paths.
>
> Add pgd_levels to struct kvm_gstage and initialize it from kvm->arch
> when setting up a gstage instance. Introduce kvm_riscv_gstage_init()
> to centralize initialization and switch gstage code to use
> gstage->pgd_levels.
>
> Suggested-by: Anup Patel <anup@brainfault.org>
> Signed-off-by: Fangyu Yu <fangyu.yu@linux.alibaba.com>
> Reviewed-by: Anup Patel <anup@brainfault.org>
Reviewed-by: Nutty Liu <nutty.liu@hotmail.com>
Thanks,
Nutty
> ---
> arch/riscv/include/asm/kvm_gstage.h | 10 ++++++
> arch/riscv/kvm/gstage.c | 10 +++---
> arch/riscv/kvm/mmu.c | 50 ++++++-----------------------
> 3 files changed, 25 insertions(+), 45 deletions(-)
>
> diff --git a/arch/riscv/include/asm/kvm_gstage.h b/arch/riscv/include/asm/kvm_gstage.h
> index 5aa58d1f692a..70d9d483365e 100644
> --- a/arch/riscv/include/asm/kvm_gstage.h
> +++ b/arch/riscv/include/asm/kvm_gstage.h
> @@ -15,6 +15,7 @@ struct kvm_gstage {
> #define KVM_GSTAGE_FLAGS_LOCAL BIT(0)
> unsigned long vmid;
> pgd_t *pgd;
> + unsigned long pgd_levels;
> };
>
> struct kvm_gstage_mapping {
> @@ -92,4 +93,13 @@ static inline unsigned long kvm_riscv_gstage_mode(unsigned long pgd_levels)
> }
> }
>
> +static inline void kvm_riscv_gstage_init(struct kvm_gstage *gstage, struct kvm *kvm)
> +{
> + gstage->kvm = kvm;
> + gstage->flags = 0;
> + gstage->vmid = READ_ONCE(kvm->arch.vmid.vmid);
> + gstage->pgd = kvm->arch.pgd;
> + gstage->pgd_levels = kvm->arch.pgd_levels;
> +}
> +
> #endif
> diff --git a/arch/riscv/kvm/gstage.c b/arch/riscv/kvm/gstage.c
> index 4beb9322fe76..7c4c34bc191b 100644
> --- a/arch/riscv/kvm/gstage.c
> +++ b/arch/riscv/kvm/gstage.c
> @@ -26,7 +26,7 @@ static inline unsigned long gstage_pte_index(struct kvm_gstage *gstage,
> unsigned long mask;
> unsigned long shift = HGATP_PAGE_SHIFT + (kvm_riscv_gstage_index_bits * level);
>
> - if (level == gstage->kvm->arch.pgd_levels - 1)
> + if (level == gstage->pgd_levels - 1)
> mask = (PTRS_PER_PTE * (1UL << kvm_riscv_gstage_pgd_xbits)) - 1;
> else
> mask = PTRS_PER_PTE - 1;
> @@ -45,7 +45,7 @@ static int gstage_page_size_to_level(struct kvm_gstage *gstage, unsigned long pa
> u32 i;
> unsigned long psz = 1UL << 12;
>
> - for (i = 0; i < gstage->kvm->arch.pgd_levels; i++) {
> + for (i = 0; i < gstage->pgd_levels; i++) {
> if (page_size == (psz << (i * kvm_riscv_gstage_index_bits))) {
> *out_level = i;
> return 0;
> @@ -58,7 +58,7 @@ static int gstage_page_size_to_level(struct kvm_gstage *gstage, unsigned long pa
> static int gstage_level_to_page_order(struct kvm_gstage *gstage, u32 level,
> unsigned long *out_pgorder)
> {
> - if (gstage->kvm->arch.pgd_levels < level)
> + if (gstage->pgd_levels < level)
> return -EINVAL;
>
> *out_pgorder = 12 + (level * kvm_riscv_gstage_index_bits);
> @@ -83,7 +83,7 @@ bool kvm_riscv_gstage_get_leaf(struct kvm_gstage *gstage, gpa_t addr,
> pte_t **ptepp, u32 *ptep_level)
> {
> pte_t *ptep;
> - u32 current_level = gstage->kvm->arch.pgd_levels - 1;
> + u32 current_level = gstage->pgd_levels - 1;
>
> *ptep_level = current_level;
> ptep = (pte_t *)gstage->pgd;
> @@ -127,7 +127,7 @@ int kvm_riscv_gstage_set_pte(struct kvm_gstage *gstage,
> struct kvm_mmu_memory_cache *pcache,
> const struct kvm_gstage_mapping *map)
> {
> - u32 current_level = gstage->kvm->arch.pgd_levels - 1;
> + u32 current_level = gstage->pgd_levels - 1;
> pte_t *next_ptep = (pte_t *)gstage->pgd;
> pte_t *ptep = &next_ptep[gstage_pte_index(gstage, map->addr, current_level)];
>
> diff --git a/arch/riscv/kvm/mmu.c b/arch/riscv/kvm/mmu.c
> index fbcdd75cb9af..2d3def024270 100644
> --- a/arch/riscv/kvm/mmu.c
> +++ b/arch/riscv/kvm/mmu.c
> @@ -24,10 +24,7 @@ static void mmu_wp_memory_region(struct kvm *kvm, int slot)
> phys_addr_t end = (memslot->base_gfn + memslot->npages) << PAGE_SHIFT;
> struct kvm_gstage gstage;
>
> - gstage.kvm = kvm;
> - gstage.flags = 0;
> - gstage.vmid = READ_ONCE(kvm->arch.vmid.vmid);
> - gstage.pgd = kvm->arch.pgd;
> + kvm_riscv_gstage_init(&gstage, kvm);
>
> spin_lock(&kvm->mmu_lock);
> kvm_riscv_gstage_wp_range(&gstage, start, end);
> @@ -49,10 +46,7 @@ int kvm_riscv_mmu_ioremap(struct kvm *kvm, gpa_t gpa, phys_addr_t hpa,
> struct kvm_gstage_mapping map;
> struct kvm_gstage gstage;
>
> - gstage.kvm = kvm;
> - gstage.flags = 0;
> - gstage.vmid = READ_ONCE(kvm->arch.vmid.vmid);
> - gstage.pgd = kvm->arch.pgd;
> + kvm_riscv_gstage_init(&gstage, kvm);
>
> end = (gpa + size + PAGE_SIZE - 1) & PAGE_MASK;
> pfn = __phys_to_pfn(hpa);
> @@ -89,10 +83,7 @@ void kvm_riscv_mmu_iounmap(struct kvm *kvm, gpa_t gpa, unsigned long size)
> {
> struct kvm_gstage gstage;
>
> - gstage.kvm = kvm;
> - gstage.flags = 0;
> - gstage.vmid = READ_ONCE(kvm->arch.vmid.vmid);
> - gstage.pgd = kvm->arch.pgd;
> + kvm_riscv_gstage_init(&gstage, kvm);
>
> spin_lock(&kvm->mmu_lock);
> kvm_riscv_gstage_unmap_range(&gstage, gpa, size, false);
> @@ -109,10 +100,7 @@ void kvm_arch_mmu_enable_log_dirty_pt_masked(struct kvm *kvm,
> phys_addr_t end = (base_gfn + __fls(mask) + 1) << PAGE_SHIFT;
> struct kvm_gstage gstage;
>
> - gstage.kvm = kvm;
> - gstage.flags = 0;
> - gstage.vmid = READ_ONCE(kvm->arch.vmid.vmid);
> - gstage.pgd = kvm->arch.pgd;
> + kvm_riscv_gstage_init(&gstage, kvm);
>
> kvm_riscv_gstage_wp_range(&gstage, start, end);
> }
> @@ -141,10 +129,7 @@ void kvm_arch_flush_shadow_memslot(struct kvm *kvm,
> phys_addr_t size = slot->npages << PAGE_SHIFT;
> struct kvm_gstage gstage;
>
> - gstage.kvm = kvm;
> - gstage.flags = 0;
> - gstage.vmid = READ_ONCE(kvm->arch.vmid.vmid);
> - gstage.pgd = kvm->arch.pgd;
> + kvm_riscv_gstage_init(&gstage, kvm);
>
> spin_lock(&kvm->mmu_lock);
> kvm_riscv_gstage_unmap_range(&gstage, gpa, size, false);
> @@ -250,10 +235,7 @@ bool kvm_unmap_gfn_range(struct kvm *kvm, struct kvm_gfn_range *range)
> if (!kvm->arch.pgd)
> return false;
>
> - gstage.kvm = kvm;
> - gstage.flags = 0;
> - gstage.vmid = READ_ONCE(kvm->arch.vmid.vmid);
> - gstage.pgd = kvm->arch.pgd;
> + kvm_riscv_gstage_init(&gstage, kvm);
> mmu_locked = spin_trylock(&kvm->mmu_lock);
> kvm_riscv_gstage_unmap_range(&gstage, range->start << PAGE_SHIFT,
> (range->end - range->start) << PAGE_SHIFT,
> @@ -275,10 +257,7 @@ bool kvm_age_gfn(struct kvm *kvm, struct kvm_gfn_range *range)
>
> WARN_ON(size != PAGE_SIZE && size != PMD_SIZE && size != PUD_SIZE);
>
> - gstage.kvm = kvm;
> - gstage.flags = 0;
> - gstage.vmid = READ_ONCE(kvm->arch.vmid.vmid);
> - gstage.pgd = kvm->arch.pgd;
> + kvm_riscv_gstage_init(&gstage, kvm);
> if (!kvm_riscv_gstage_get_leaf(&gstage, range->start << PAGE_SHIFT,
> &ptep, &ptep_level))
> return false;
> @@ -298,10 +277,7 @@ bool kvm_test_age_gfn(struct kvm *kvm, struct kvm_gfn_range *range)
>
> WARN_ON(size != PAGE_SIZE && size != PMD_SIZE && size != PUD_SIZE);
>
> - gstage.kvm = kvm;
> - gstage.flags = 0;
> - gstage.vmid = READ_ONCE(kvm->arch.vmid.vmid);
> - gstage.pgd = kvm->arch.pgd;
> + kvm_riscv_gstage_init(&gstage, kvm);
> if (!kvm_riscv_gstage_get_leaf(&gstage, range->start << PAGE_SHIFT,
> &ptep, &ptep_level))
> return false;
> @@ -463,10 +439,7 @@ int kvm_riscv_mmu_map(struct kvm_vcpu *vcpu, struct kvm_memory_slot *memslot,
> struct kvm_gstage gstage;
> struct page *page;
>
> - gstage.kvm = kvm;
> - gstage.flags = 0;
> - gstage.vmid = READ_ONCE(kvm->arch.vmid.vmid);
> - gstage.pgd = kvm->arch.pgd;
> + kvm_riscv_gstage_init(&gstage, kvm);
>
> /* Setup initial state of output mapping */
> memset(out_map, 0, sizeof(*out_map));
> @@ -587,10 +560,7 @@ void kvm_riscv_mmu_free_pgd(struct kvm *kvm)
>
> spin_lock(&kvm->mmu_lock);
> if (kvm->arch.pgd) {
> - gstage.kvm = kvm;
> - gstage.flags = 0;
> - gstage.vmid = READ_ONCE(kvm->arch.vmid.vmid);
> - gstage.pgd = kvm->arch.pgd;
> + kvm_riscv_gstage_init(&gstage, kvm);
> kvm_riscv_gstage_unmap_range(&gstage, 0UL,
> kvm_riscv_gstage_gpa_size(kvm->arch.pgd_levels), false);
> pgd = READ_ONCE(kvm->arch.pgd);
^ permalink raw reply
* Re: [PATCH v9 02/10] x86/bhi: Make clear_bhb_loop() effective on newer CPUs
From: Jim Mattson @ 2026-04-04 2:21 UTC (permalink / raw)
To: Pawan Gupta
Cc: x86, Jon Kohler, Nikolay Borisov, H. Peter Anvin, Josh Poimboeuf,
David Kaplan, Sean Christopherson, Borislav Petkov, Dave Hansen,
Peter Zijlstra, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, KP Singh, Jiri Olsa, David S. Miller,
David Laight, Andy Lutomirski, Thomas Gleixner, Ingo Molnar,
David Ahern, Martin KaFai Lau, Eduard Zingerman, Song Liu,
Yonghong Song, John Fastabend, Stanislav Fomichev, Hao Luo,
Paolo Bonzini, Jonathan Corbet, linux-kernel, kvm, Asit Mallick,
Tao Zhang, bpf, netdev, linux-doc, chao.gao
In-Reply-To: <20260404002149.wtayv6a64vzuppgp@desk>
On Fri, Apr 3, 2026 at 5:22 PM Pawan Gupta
<pawan.kumar.gupta@linux.intel.com> wrote:
>
> On Fri, Apr 03, 2026 at 04:39:54PM -0700, Jim Mattson wrote:
> > > Since cloud providers have greater control over userspace, the decision to
> > > use BHI_DIS_S or not can be left to them. KVM would simply follow what it
> > > is asked to do by the userspace.
> >
> > I feel like we've gone over this before, but if userspace tells KVM
> > not to enable BHI_DIS_S, how do we inform Windows that it needs to do
> > the longer clearing sequence, despite the fact that the virtual CPU is
> > masquerading as Ice Lake?
>
> IMO, if an OS is allergic to a hardware mitigation, and is also aware that
> it is virtualized, it should default to a sw mitigation that works everywhere.
Agreed. So, without any information to the contrary, VMs should assume
the long BHB clearing sequence is required.
Returning to my earlier comment, the test should be:
+ if (cpu_feature_enabled(X86_FEATURE_BHI_CTRL) ||
cpu_feature_enabled(X86_FEATURE_HYPERVISOR)) {
+ bhb_seq_outer_loop = 12;
+ bhb_seq_inner_loop = 7;
+ }
^ permalink raw reply
* [PATCH] docs: usb: document USBDEVFS_BULK return value
From: Adeel Zahid @ 2026-04-04 1:41 UTC (permalink / raw)
To: Jonathan Corbet; +Cc: Shuah Khan, linux-doc, linux-kernel, Adeel Zahid
Replace the FIXME in the usbfs bulk I/O documentation with the
current behavior.
Document that USBDEVFS_BULK returns the completed URB actual_length
on success, which may be smaller than the requested len. Also clarify
that for IN endpoints only the returned number of bytes is copied into
the userspace buffer, so a smaller return value indicates a short
read.
Signed-off-by: Adeel Zahid <adeel.m.zahid@gmail.com>
---
Documentation/driver-api/usb/usb.rst | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/Documentation/driver-api/usb/usb.rst b/Documentation/driver-api/usb/usb.rst
index 7f2f41e80c1c..6b6a759c1f62 100644
--- a/Documentation/driver-api/usb/usb.rst
+++ b/Documentation/driver-api/usb/usb.rst
@@ -535,9 +535,19 @@ USBDEVFS_BULK
The ``ep`` value identifies a bulk endpoint number (1 to 15, as
identified in an endpoint descriptor), masked with USB_DIR_IN when
referring to an endpoint which sends data to the host from the
- device. The length of the data buffer is identified by ``len``; Recent
- kernels support requests up to about 128KBytes. *FIXME say how read
- length is returned, and how short reads are handled.*.
+ device. The length of the data buffer is identified by ``len``. Recent
+ kernels support requests up to about 128 KBytes.
+
+ On success, the ioctl returns the completed URB's ``actual_length``
+ value, that is, the number of bytes actually transferred for the
+ request. This may be less than the value requested in ``len``.
+
+ For an IN endpoint, the return value tells userspace how many bytes were
+ read and copied into ``data``. If the return value is smaller than
+ ``len``, the read completed as a short read, and only the returned
+ number of bytes is valid in the buffer.
+
+ Failures return a negative errno value.
USBDEVFS_CLEAR_HALT
Clears endpoint halt (stall) and resets the endpoint toggle. This is
--
2.43.0
^ permalink raw reply related
* [PATCH v5 1/1] mm/damon: add node_eligible_mem_bp and node_ineligible_mem_bp goal metrics
From: Ravi Jonnalagadda @ 2026-04-04 1:22 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc, kernel test robot
In-Reply-To: <20260404012215.1539-1-ravis.opensrc@gmail.com>
Add new quota goal metrics for memory tiering that track scheme-eligible
memory distribution across NUMA nodes:
- DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP: ratio of eligible memory on a node
- DAMOS_QUOTA_NODE_INELIGIBLE_MEM_BP: ratio of ineligible memory on a
node
These complementary metrics enable push-pull migration schemes that
maintain a target memory distribution across different NUMA nodes
representing different memory tiers, based on access patterns defined
by each scheme.
The metrics iterate scheme-eligible regions and use damon_get_folio()
to determine NUMA node placement of each folio, calculating the ratio
of eligible memory on the specified node versus total eligible memory.
The implementation is guarded by CONFIG_DAMON_PADDR since damon_get_folio()
is only available when physical address space monitoring is enabled.
Suggested-by: SeongJae Park <sj@kernel.org>
Reported-by: kernel test robot <lkp@intel.com>
Closes: https://lore.kernel.org/oe-kbuild-all/202603251034.978zcsQ2-lkp@intel.com/
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
include/linux/damon.h | 6 ++
mm/damon/core.c | 186 ++++++++++++++++++++++++++++++++++++---
mm/damon/sysfs-schemes.c | 12 +++
3 files changed, 190 insertions(+), 14 deletions(-)
diff --git a/include/linux/damon.h b/include/linux/damon.h
index c15a7d2a05c6..98dbf6911dad 100644
--- a/include/linux/damon.h
+++ b/include/linux/damon.h
@@ -193,6 +193,10 @@ enum damos_action {
* @DAMOS_QUOTA_NODE_MEMCG_FREE_BP: MemFree ratio of a node for a cgroup.
* @DAMOS_QUOTA_ACTIVE_MEM_BP: Active to total LRU memory ratio.
* @DAMOS_QUOTA_INACTIVE_MEM_BP: Inactive to total LRU memory ratio.
+ * @DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP: Scheme-eligible memory ratio of a
+ * node.
+ * @DAMOS_QUOTA_NODE_INELIGIBLE_MEM_BP: Scheme-ineligible memory ratio of a
+ * node.
* @NR_DAMOS_QUOTA_GOAL_METRICS: Number of DAMOS quota goal metrics.
*
* Metrics equal to larger than @NR_DAMOS_QUOTA_GOAL_METRICS are unsupported.
@@ -206,6 +210,8 @@ enum damos_quota_goal_metric {
DAMOS_QUOTA_NODE_MEMCG_FREE_BP,
DAMOS_QUOTA_ACTIVE_MEM_BP,
DAMOS_QUOTA_INACTIVE_MEM_BP,
+ DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP,
+ DAMOS_QUOTA_NODE_INELIGIBLE_MEM_BP,
NR_DAMOS_QUOTA_GOAL_METRICS,
};
diff --git a/mm/damon/core.c b/mm/damon/core.c
index 5908537f45f1..f71ee19f526d 100644
--- a/mm/damon/core.c
+++ b/mm/damon/core.c
@@ -17,6 +17,9 @@
#include <linux/string.h>
#include <linux/string_choices.h>
+/* for damon_get_folio() used by node eligible memory metrics */
+#include "ops-common.h"
+
#define CREATE_TRACE_POINTS
#include <trace/events/damon.h>
@@ -2549,7 +2552,136 @@ static unsigned long damos_get_node_memcg_used_bp(
numerator = i.totalram - used_pages;
return mult_frac(numerator, 10000, i.totalram);
}
-#else
+
+#ifdef CONFIG_DAMON_PADDR
+/*
+ * damos_calc_eligible_bytes() - Calculate raw eligible bytes per node.
+ * @c: The DAMON context.
+ * @s: The scheme.
+ * @nid: The target NUMA node id.
+ * @total: Output for total eligible bytes across all nodes.
+ *
+ * Iterates through each folio in eligible regions to accurately determine
+ * which node the memory resides on. Returns eligible bytes on the specified
+ * node and sets *total to the sum across all nodes.
+ *
+ * Note: This function requires damon_get_folio() from ops-common.c, which is
+ * only available when CONFIG_DAMON_PADDR or CONFIG_DAMON_VADDR is enabled.
+ */
+static unsigned long damos_calc_eligible_bytes(struct damon_ctx *c,
+ struct damos *s, int nid, unsigned long *total)
+{
+ struct damon_target *t;
+ struct damon_region *r;
+ unsigned long total_eligible = 0;
+ unsigned long node_eligible = 0;
+
+ damon_for_each_target(t, c) {
+ damon_for_each_region(r, t) {
+ phys_addr_t addr, end_addr;
+
+ if (!__damos_valid_target(r, s))
+ continue;
+
+ /* Convert from core address units to physical bytes */
+ addr = r->ar.start * c->addr_unit;
+ end_addr = r->ar.end * c->addr_unit;
+ while (addr < end_addr) {
+ struct folio *folio;
+ unsigned long folio_sz, counted;
+
+ folio = damon_get_folio(PHYS_PFN(addr));
+ if (!folio) {
+ addr += PAGE_SIZE;
+ continue;
+ }
+
+ folio_sz = folio_size(folio);
+ /*
+ * Clip to region boundaries to avoid counting
+ * bytes outside the region when folio spans
+ * region boundaries.
+ */
+ counted = min(folio_sz, (unsigned long)(end_addr - addr));
+ total_eligible += counted;
+ if (folio_nid(folio) == nid)
+ node_eligible += counted;
+
+ addr += folio_sz;
+ folio_put(folio);
+ }
+ }
+ }
+
+ *total = total_eligible;
+ return node_eligible;
+}
+
+/*
+ * damos_get_node_eligible_mem_bp() - Get eligible memory ratio for a node.
+ * @c: The DAMON context.
+ * @s: The scheme.
+ * @nid: The target NUMA node id.
+ *
+ * Calculates scheme-eligible bytes on the specified node and returns the
+ * ratio in basis points (0-10000) relative to total eligible bytes across
+ * all nodes.
+ */
+static unsigned long damos_get_node_eligible_mem_bp(struct damon_ctx *c,
+ struct damos *s, int nid)
+{
+ unsigned long total_eligible = 0;
+ unsigned long node_eligible = 0;
+
+ if (nid < 0 || nid >= MAX_NUMNODES || !node_online(nid))
+ return 0;
+
+ node_eligible = damos_calc_eligible_bytes(c, s, nid, &total_eligible);
+
+ if (!total_eligible)
+ return 0;
+
+ return mult_frac(node_eligible, 10000, total_eligible);
+}
+
+static unsigned long damos_get_node_ineligible_mem_bp(struct damon_ctx *c,
+ struct damos *s, int nid)
+{
+ unsigned long total_eligible = 0;
+ unsigned long node_eligible;
+
+ if (nid < 0 || nid >= MAX_NUMNODES || !node_online(nid))
+ return 0;
+
+ node_eligible = damos_calc_eligible_bytes(c, s, nid, &total_eligible);
+
+ /* No eligible memory anywhere - ratio is undefined, return 0 */
+ if (!total_eligible)
+ return 0;
+
+ /* Compute ineligible ratio directly: 10000 - eligible_bp */
+ return 10000 - mult_frac(node_eligible, 10000, total_eligible);
+}
+#else /* CONFIG_DAMON_PADDR */
+/*
+ * Stub functions when CONFIG_DAMON_PADDR is disabled.
+ * The node_eligible/ineligible metrics require physical address operations
+ * to iterate folios, which are only available with PA-mode DAMON.
+ */
+static unsigned long damos_get_node_eligible_mem_bp(struct damon_ctx *c,
+ struct damos *s, int nid)
+{
+ return 0;
+}
+
+static unsigned long damos_get_node_ineligible_mem_bp(struct damon_ctx *c,
+ struct damos *s, int nid)
+{
+ return 0;
+}
+#endif /* CONFIG_DAMON_PADDR */
+
+#else /* CONFIG_NUMA */
static __kernel_ulong_t damos_get_node_mem_bp(
struct damos_quota_goal *goal)
{
@@ -2561,7 +2693,19 @@ static unsigned long damos_get_node_memcg_used_bp(
{
return 0;
}
-#endif
+
+static unsigned long damos_get_node_eligible_mem_bp(struct damon_ctx *c,
+ struct damos *s, int nid)
+{
+ return 0;
+}
+
+static unsigned long damos_get_node_ineligible_mem_bp(struct damon_ctx *c,
+ struct damos *s, int nid)
+{
+ return 0;
+}
+#endif /* CONFIG_NUMA */
/*
* Returns LRU-active or inactive memory to total LRU memory size ratio.
@@ -2581,7 +2725,8 @@ static unsigned int damos_get_in_active_mem_bp(bool active_ratio)
return mult_frac(inactive, 10000, total);
}
-static void damos_set_quota_goal_current_value(struct damos_quota_goal *goal)
+static void damos_set_quota_goal_current_value(struct damon_ctx *c,
+ struct damos *s, struct damos_quota_goal *goal)
{
u64 now_psi_total;
@@ -2608,19 +2753,28 @@ static void damos_set_quota_goal_current_value(struct damos_quota_goal *goal)
goal->current_value = damos_get_in_active_mem_bp(
goal->metric == DAMOS_QUOTA_ACTIVE_MEM_BP);
break;
+ case DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP:
+ goal->current_value = damos_get_node_eligible_mem_bp(c, s,
+ goal->nid);
+ break;
+ case DAMOS_QUOTA_NODE_INELIGIBLE_MEM_BP:
+ goal->current_value = damos_get_node_ineligible_mem_bp(c, s,
+ goal->nid);
+ break;
default:
break;
}
}
/* Return the highest score since it makes schemes least aggressive */
-static unsigned long damos_quota_score(struct damos_quota *quota)
+static unsigned long damos_quota_score(struct damon_ctx *c, struct damos *s)
{
+ struct damos_quota *quota = &s->quota;
struct damos_quota_goal *goal;
unsigned long highest_score = 0;
damos_for_each_quota_goal(goal, quota) {
- damos_set_quota_goal_current_value(goal);
+ damos_set_quota_goal_current_value(c, s, goal);
highest_score = max(highest_score,
mult_frac(goal->current_value, 10000,
goal->target_value));
@@ -2629,17 +2783,20 @@ static unsigned long damos_quota_score(struct damos_quota *quota)
return highest_score;
}
-static void damos_goal_tune_esz_bp_consist(struct damos_quota *quota)
+static void damos_goal_tune_esz_bp_consist(struct damon_ctx *c, struct damos *s)
{
- unsigned long score = damos_quota_score(quota);
+ struct damos_quota *quota = &s->quota;
+ unsigned long score = damos_quota_score(c, s);
quota->esz_bp = damon_feed_loop_next_input(
max(quota->esz_bp, 10000UL), score);
}
-static void damos_goal_tune_esz_bp_temporal(struct damos_quota *quota)
+static void damos_goal_tune_esz_bp_temporal(struct damon_ctx *c,
+ struct damos *s)
{
- unsigned long score = damos_quota_score(quota);
+ struct damos_quota *quota = &s->quota;
+ unsigned long score = damos_quota_score(c, s);
if (score >= 10000)
quota->esz_bp = 0;
@@ -2652,8 +2809,9 @@ static void damos_goal_tune_esz_bp_temporal(struct damos_quota *quota)
/*
* Called only if quota->ms, or quota->sz are set, or quota->goals is not empty
*/
-static void damos_set_effective_quota(struct damos_quota *quota)
+static void damos_set_effective_quota(struct damon_ctx *c, struct damos *s)
{
+ struct damos_quota *quota = &s->quota;
unsigned long throughput;
unsigned long esz = ULONG_MAX;
@@ -2664,9 +2822,9 @@ static void damos_set_effective_quota(struct damos_quota *quota)
if (!list_empty("a->goals)) {
if (quota->goal_tuner == DAMOS_QUOTA_GOAL_TUNER_CONSIST)
- damos_goal_tune_esz_bp_consist(quota);
+ damos_goal_tune_esz_bp_consist(c, s);
else if (quota->goal_tuner == DAMOS_QUOTA_GOAL_TUNER_TEMPORAL)
- damos_goal_tune_esz_bp_temporal(quota);
+ damos_goal_tune_esz_bp_temporal(c, s);
esz = quota->esz_bp / 10000;
}
@@ -2715,7 +2873,7 @@ static void damos_adjust_quota(struct damon_ctx *c, struct damos *s)
/* First charge window */
if (!quota->total_charged_sz && !quota->charged_from) {
quota->charged_from = jiffies;
- damos_set_effective_quota(quota);
+ damos_set_effective_quota(c, s);
if (trace_damos_esz_enabled())
damos_trace_esz(c, s, quota);
}
@@ -2737,7 +2895,7 @@ static void damos_adjust_quota(struct damon_ctx *c, struct damos *s)
quota->charged_sz = 0;
if (trace_damos_esz_enabled())
cached_esz = quota->esz;
- damos_set_effective_quota(quota);
+ damos_set_effective_quota(c, s);
if (trace_damos_esz_enabled() && quota->esz != cached_esz)
damos_trace_esz(c, s, quota);
}
diff --git a/mm/damon/sysfs-schemes.c b/mm/damon/sysfs-schemes.c
index bf923709ab91..7e9cd19d5bff 100644
--- a/mm/damon/sysfs-schemes.c
+++ b/mm/damon/sysfs-schemes.c
@@ -1084,6 +1084,14 @@ struct damos_sysfs_qgoal_metric_name damos_sysfs_qgoal_metric_names[] = {
.metric = DAMOS_QUOTA_INACTIVE_MEM_BP,
.name = "inactive_mem_bp",
},
+ {
+ .metric = DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP,
+ .name = "node_eligible_mem_bp",
+ },
+ {
+ .metric = DAMOS_QUOTA_NODE_INELIGIBLE_MEM_BP,
+ .name = "node_ineligible_mem_bp",
+ },
};
static ssize_t target_metric_show(struct kobject *kobj,
@@ -2717,6 +2725,10 @@ static int damos_sysfs_add_quota_score(
case DAMOS_QUOTA_NODE_MEM_FREE_BP:
goal->nid = sysfs_goal->nid;
break;
+ case DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP:
+ case DAMOS_QUOTA_NODE_INELIGIBLE_MEM_BP:
+ goal->nid = sysfs_goal->nid;
+ break;
case DAMOS_QUOTA_NODE_MEMCG_USED_BP:
case DAMOS_QUOTA_NODE_MEMCG_FREE_BP:
err = damon_sysfs_memcg_path_to_id(
--
2.43.0
^ permalink raw reply related
* [PATCH v5 0/1] mm/damon: add node_eligible_mem_bp and node_ineligible_mem_bp goal metrics
From: Ravi Jonnalagadda @ 2026-04-04 1:22 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc
This patch introduces two new DAMOS quota goal metrics for controlling
memory distribution in heterogeneous memory systems (e.g., DRAM and CXL
memory tiering) using physical address (PA) mode monitoring.
Changes since v4:
=================
https://lore.kernel.org/linux-mm/20260320190453.1430-1-ravis.opensrc@gmail.com/
- Fixed commit message description for DAMOS_QUOTA_NODE_INELIGIBLE_MEM_BP
per review feedback
- Added clarifying comment for ops-common.h include (for damon_get_folio())
- Fixed build error when CONFIG_DAMON_PADDR is disabled by adding
#ifdef CONFIG_DAMON_PADDR guards around functions using damon_get_folio()
- Dropped RFC tag per maintainer feedback
This patch is based on top of damon/next.
Background and Motivation
=========================
In heterogeneous memory systems, controlling memory distribution across
NUMA nodes is essential for performance optimization. This patch enables
system-wide page distribution with target-state goals such as "maintain
30% of scheme-eligible memory on CXL" using PA-mode DAMON schemes.
What These Metrics Measure
==========================
node_eligible_mem_bp:
scheme_eligible_bytes_on_node / total_scheme_eligible_bytes * 10000
node_ineligible_mem_bp:
(total - scheme_eligible_bytes_on_node) / total * 10000
These metrics are complementary: eligible_bp + ineligible_bp = 10000 bp.
Two-Scheme Setup for Hot Page Distribution
==========================================
For maintaining hot memory on DRAM (node 0) and CXL (node 1) in a 7:3
ratio:
PUSH scheme: migrate_hot from node 0 -> node 1
goal: node_ineligible_mem_bp, nid=0, target=3000
"Move hot pages from DRAM to CXL if more than 70% of hot data is
in DRAM"
PULL scheme: migrate_hot from node 1 -> node 0
goal: node_eligible_mem_bp, nid=0, target=7000
"Move hot pages from CXL to DRAM if less than 70% of hot data is
in DRAM"
The complementary goals create a feedback loop that converges to the
target distribution.
Testing Results
===============
Functionally tested on a two-node heterogeneous memory system with DRAM
(node 0) and CXL memory (node 1). A PUSH+PULL scheme configuration using
migrate_hot actions was used to reach a target hot memory ratio between
the two tiers. Testing used the TEMPORAL goal tuner available in
damon/next and mm-unstable.
With the TEMPORAL tuner, the system converges quickly to the target
distribution. The tuner drives esz to maximum when under goal and to
zero once the goal is met, forming a simple on/off feedback loop that
stabilizes at the desired ratio.
With the CONSIST tuner, the scheme still converges but more slowly, as
it migrates and then throttles itself based on quota feedback. The time
to reach the goal varies depending on workload intensity.
Note: These metrics work with both TEMPORAL and CONSIST goal tuners.
Ravi Jonnalagadda (1):
mm/damon: add node_eligible_mem_bp and node_ineligible_mem_bp goal
metrics
include/linux/damon.h | 6 ++
mm/damon/core.c | 186 ++++++++++++++++++++++++++++++++++++---
mm/damon/sysfs-schemes.c | 12 +++
3 files changed, 190 insertions(+), 14 deletions(-)
base-commit: 97eefd14af390e1921f1fc5507140025095634e0
--
2.43.0
^ permalink raw reply
* Re: [PATCH v4 0/3] dpll: add frequency monitoring feature
From: patchwork-bot+netdevbpf @ 2026-04-04 0:40 UTC (permalink / raw)
To: Ivan Vecera
Cc: netdev, arkadiusz.kubalewski, davem, donald.hunter, edumazet,
kuba, jiri, corbet, mschmidt, pabeni, poros, Prathosh.Satish,
skhan, horms, vadim.fedorenko, linux-doc, linux-kernel
In-Reply-To: <20260402184057.1890514-1-ivecera@redhat.com>
Hello:
This series was applied to netdev/net-next.git (main)
by Jakub Kicinski <kuba@kernel.org>:
On Thu, 2 Apr 2026 20:40:54 +0200 you wrote:
> This series adds support for monitoring the measured input frequency
> of DPLL input pins via the DPLL netlink interface.
>
> Some DPLL devices can measure the actual frequency being received on
> input pins. The approach mirrors the existing phase-offset-monitor
> feature: a device-level attribute (DPLL_A_FREQUENCY_MONITOR) enables
> or disables monitoring, and a per-pin attribute
> (DPLL_A_PIN_MEASURED_FREQUENCY) exposes the measured frequency in
> millihertz (mHz) when monitoring is enabled.
>
> [...]
Here is the summary with links:
- [v4,1/3] dpll: add frequency monitoring to netlink spec
https://git.kernel.org/netdev/net-next/c/3fdea79c09d1
- [v4,2/3] dpll: add frequency monitoring callback ops
https://git.kernel.org/netdev/net-next/c/15ed91aa84ea
- [v4,3/3] dpll: zl3073x: implement frequency monitoring
https://git.kernel.org/netdev/net-next/c/bfc923b64287
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply
* Re: [PATCH v6 4/4] selftests/ftrace: Add accept cases for fprobe list syntax
From: Masami Hiramatsu @ 2026-04-04 0:25 UTC (permalink / raw)
To: Ryan Chung
Cc: rostedt, corbet, shuah, mathieu.desnoyers, linux-kernel,
linux-trace-kernel, linux-doc, linux-kselftest
In-Reply-To: <CAB1jyqw_6wepbDaKi7087GDUcJ9t1jQO6qP9pa0DWCjti7ABZg@mail.gmail.com>
On Thu, 2 Apr 2026 11:45:42 -0400
Ryan Chung <seokwoo.chung130@gmail.com> wrote:
> Hi Masami,
>
> Thank you for your feedback. Unfortunately, I am not in the position
> to continue working on this patch series for the foreseeable future.
> If you or anyone else on the list would like to pick it up and carry
> it forward, you are welcome to do so. I appreciate your time and
> effort on this.
I see, that's unfortunate, but I understand. I'll continue to fix
and post updates for this patch series.
I appreciate you starting this series.
Thank you.
>
> Best regards,
> Seokwoo Chung
>
> On Tue, 24 Mar 2026 at 00:12, Masami Hiramatsu <mhiramat@kernel.org> wrote:
> >
> > On Thu, 5 Feb 2026 08:58:42 -0500
> > "Seokwoo Chung (Ryan)" <seokwoo.chung130@gmail.com> wrote:
> >
> > > Add fprobe_list.tc to test the comma-separated symbol list syntax
> > > with :entry/:exit suffixes. Three scenarios are covered:
> > >
> > > 1. List with default (entry) behavior and ! exclusion
> > > 2. List with explicit :entry suffix
> > > 3. List with :exit suffix for return probes
> >
> >
> > Could you also add wildcard pattern test?
> >
> > >
> > > Each test verifies that the correct functions appear in
> > > enabled_functions and that excluded (!) symbols are absent.
> > >
> > > Note: The existing tests add_remove_fprobe.tc, fprobe_syntax_errors.tc,
> > > and add_remove_fprobe_repeat.tc check their "requires" line against the
> > > tracefs README for the old "%return" syntax pattern. Since the README
> > > now documents ":entry|:exit" instead, these tests report UNSUPPORTED.
> > > Their "requires" lines need updating in a follow-up patch.
> >
> > This means you'll break the selftest. please fix those test first.
> > (This fix must be done before "tracing/fprobe: Support comma-separated
> > symbols and :entry/:exit" so that we can safely bisect it.)
> >
> > Thank you,
> >
> >
> > >
> > > Signed-off-by: Seokwoo Chung (Ryan) <seokwoo.chung130@gmail.com>
> > > ---
> > > .../ftrace/test.d/dynevent/fprobe_list.tc | 92 +++++++++++++++++++
> > > 1 file changed, 92 insertions(+)
> > > create mode 100644 tools/testing/selftests/ftrace/test.d/dynevent/fprobe_list.tc
> > >
> > > diff --git a/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_list.tc b/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_list.tc
> > > new file mode 100644
> > > index 000000000000..45e57c6f487d
> > > --- /dev/null
> > > +++ b/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_list.tc
> > > @@ -0,0 +1,92 @@
> > > +#!/bin/sh
> > > +# SPDX-License-Identifier: GPL-2.0
> > > +# description: Fprobe event list syntax and :entry/:exit suffixes
> > > +# requires: dynamic_events "f[:[<group>/][<event>]] <func-name>[:entry|:exit] [<args>]":README
> > > +
> > > +# Setup symbols to test. These are common kernel functions.
> > > +PLACE=vfs_read
> > > +PLACE2=vfs_write
> > > +PLACE3=vfs_open
> > > +
> > > +echo 0 > events/enable
> > > +echo > dynamic_events
> > > +
> > > +# Get baseline count of enabled functions (should be 0 if clean, but be safe)
> > > +if [ -f enabled_functions ]; then
> > > + ocnt=`cat enabled_functions | wc -l`
> > > +else
> > > + ocnt=0
> > > +fi
> > > +
> > > +# Test 1: List default (entry) with exclusion
> > > +# Target: Trace vfs_read and vfs_open, but EXCLUDE vfs_write
> > > +echo "f:test/list_entry $PLACE,!$PLACE2,$PLACE3" >> dynamic_events
> > > +grep -q "test/list_entry" dynamic_events
> > > +test -d events/test/list_entry
> > > +
> > > +echo 1 > events/test/list_entry/enable
> > > +
> > > +grep -q "$PLACE" enabled_functions
> > > +grep -q "$PLACE3" enabled_functions
> > > +! grep -q "$PLACE2" enabled_functions
> > > +
> > > +# Check count (Baseline + 2 new functions)
> > > +cnt=`cat enabled_functions | wc -l`
> > > +if [ $cnt -ne $((ocnt + 2)) ]; then
> > > + exit_fail
> > > +fi
> > > +
> > > +# Cleanup Test 1
> > > +echo 0 > events/test/list_entry/enable
> > > +echo "-:test/list_entry" >> dynamic_events
> > > +! grep -q "test/list_entry" dynamic_events
> > > +
> > > +# Count should return to baseline
> > > +cnt=`cat enabled_functions | wc -l`
> > > +if [ $cnt -ne $ocnt ]; then
> > > + exit_fail
> > > +fi
> > > +
> > > +# Test 2: List with explicit :entry suffix
> > > +# (Should behave exactly like Test 1)
> > > +echo "f:test/list_entry_exp $PLACE,!$PLACE2,$PLACE3:entry" >> dynamic_events
> > > +grep -q "test/list_entry_exp" dynamic_events
> > > +test -d events/test/list_entry_exp
> > > +
> > > +echo 1 > events/test/list_entry_exp/enable
> > > +
> > > +grep -q "$PLACE" enabled_functions
> > > +grep -q "$PLACE3" enabled_functions
> > > +! grep -q "$PLACE2" enabled_functions
> > > +
> > > +cnt=`cat enabled_functions | wc -l`
> > > +if [ $cnt -ne $((ocnt + 2)) ]; then
> > > + exit_fail
> > > +fi
> > > +
> > > +# Cleanup Test 2
> > > +echo 0 > events/test/list_entry_exp/enable
> > > +echo "-:test/list_entry_exp" >> dynamic_events
> > > +
> > > +# Test 3: List with :exit suffix
> > > +echo "f:test/list_exit $PLACE,!$PLACE2,$PLACE3:exit" >> dynamic_events
> > > +grep -q "test/list_exit" dynamic_events
> > > +test -d events/test/list_exit
> > > +
> > > +echo 1 > events/test/list_exit/enable
> > > +
> > > +# Even for return probes, enabled_functions lists the attached symbols
> > > +grep -q "$PLACE" enabled_functions
> > > +grep -q "$PLACE3" enabled_functions
> > > +! grep -q "$PLACE2" enabled_functions
> > > +
> > > +cnt=`cat enabled_functions | wc -l`
> > > +if [ $cnt -ne $((ocnt + 2)) ]; then
> > > + exit_fail
> > > +fi
> > > +
> > > +# Cleanup Test 3
> > > +echo 0 > events/test/list_exit/enable
> > > +echo "-:test/list_exit" >> dynamic_events
> > > +
> > > +clear_trace
> > > --
> > > 2.43.0
> > >
> >
> >
> > --
> > Masami Hiramatsu (Google) <mhiramat@kernel.org>
--
Masami Hiramatsu (Google) <mhiramat@kernel.org>
^ permalink raw reply
* Re: [PATCH v9 02/10] x86/bhi: Make clear_bhb_loop() effective on newer CPUs
From: Pawan Gupta @ 2026-04-04 0:21 UTC (permalink / raw)
To: Jim Mattson
Cc: x86, Jon Kohler, Nikolay Borisov, H. Peter Anvin, Josh Poimboeuf,
David Kaplan, Sean Christopherson, Borislav Petkov, Dave Hansen,
Peter Zijlstra, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, KP Singh, Jiri Olsa, David S. Miller,
David Laight, Andy Lutomirski, Thomas Gleixner, Ingo Molnar,
David Ahern, Martin KaFai Lau, Eduard Zingerman, Song Liu,
Yonghong Song, John Fastabend, Stanislav Fomichev, Hao Luo,
Paolo Bonzini, Jonathan Corbet, linux-kernel, kvm, Asit Mallick,
Tao Zhang, bpf, netdev, linux-doc, chao.gao
In-Reply-To: <CALMp9eTpsenqsWjzmpXLEubn9uNjgZgzgrMwtZ72HDuV_2xgfg@mail.gmail.com>
On Fri, Apr 03, 2026 at 04:39:54PM -0700, Jim Mattson wrote:
> > Since cloud providers have greater control over userspace, the decision to
> > use BHI_DIS_S or not can be left to them. KVM would simply follow what it
> > is asked to do by the userspace.
>
> I feel like we've gone over this before, but if userspace tells KVM
> not to enable BHI_DIS_S, how do we inform Windows that it needs to do
> the longer clearing sequence, despite the fact that the virtual CPU is
> masquerading as Ice Lake?
IMO, if an OS is allergic to a hardware mitigation, and is also aware that
it is virtualized, it should default to a sw mitigation that works everywhere.
> I don't think the virtual mitigation MSRs address that issue.
Virtual mitigation MSRs are meant to inform the VMM about the guest
mitigation. Even if there was a way to tell the guest that it needs to use
a different mitigation, it seems unrealistic for a guest to change its
mitigation post-migration.
^ permalink raw reply
* [riscv:for-next 24/45] htmldocs: Documentation/arch/riscv/zicfilp.rst:79: WARNING: Inline literal start-string without end-string. [docutils]
From: kernel test robot @ 2026-04-04 0:16 UTC (permalink / raw)
To: Paul Walmsley; +Cc: oe-kbuild-all, linux-doc
tree: https://git.kernel.org/pub/scm/linux/kernel/git/riscv/linux.git for-next
head: c0368008b37da62d634fdd1aa2603f13dc31528a
commit: 8a8f622c329e2e3690b54826370e4a0af45f66ef [24/45] prctl: cfi: change the branch landing pad prctl()s to be more descriptive
compiler: clang version 20.1.8 (https://github.com/llvm/llvm-project 87f0227cb60147a26a1eeb4fb06e3b505e9c7261)
docutils: docutils (Docutils 0.21.2, Python 3.13.5, on linux)
reproduce: (https://download.01.org/0day-ci/archive/20260404/202604040216.ea9oBMct-lkp@intel.com/reproduce)
If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202604040216.ea9oBMct-lkp@intel.com/
All warnings (new ones prefixed by >>):
Warning: tools/docs/documentation-file-ref-check references a file that doesn't exist: m,\b(\S*)(Documentation/[A-Za-z0-9
Warning: tools/docs/documentation-file-ref-check references a file that doesn't exist: Documentation/devicetree/dt-object-internal.txt
Warning: tools/docs/documentation-file-ref-check references a file that doesn't exist: m,^Documentation/scheduler/sched-pelt
Warning: tools/docs/documentation-file-ref-check references a file that doesn't exist: m,(Documentation/translations/[
Using alabaster theme
>> Documentation/arch/riscv/zicfilp.rst:79: WARNING: Inline literal start-string without end-string. [docutils]
Documentation/core-api/kref:328: ./include/linux/kref.h:72: WARNING: Invalid C declaration: Expected end of definition. [error at 96]
int kref_put_mutex (struct kref *kref, void (*release)(struct kref *kref), struct mutex *mutex) __cond_acquires(true# mutex)
------------------------------------------------------------------------------------------------^
Documentation/core-api/kref:328: ./include/linux/kref.h:94: WARNING: Invalid C declaration: Expected end of definition. [error at 92]
int kref_put_lock (struct kref *kref, void (*release)(struct kref *kref), spinlock_t *lock) __cond_acquires(true# lock)
vim +79 Documentation/arch/riscv/zicfilp.rst
78
> 79 Per-task indirect branch tracking state can be monitored and
80 controlled via the :c:macro:`PR_GET_CFI` and :c:macro:`PR_SET_CFI`
81 ``prctl()` arguments (respectively), by supplying
82 :c:macro:`PR_CFI_BRANCH_LANDING_PADS` as the second argument. These
83 are architecture-agnostic, and will return -EINVAL if the underlying
84 functionality is not supported.
85
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* Re: [PATCH v9 02/10] x86/bhi: Make clear_bhb_loop() effective on newer CPUs
From: Jim Mattson @ 2026-04-03 23:39 UTC (permalink / raw)
To: Pawan Gupta
Cc: x86, Jon Kohler, Nikolay Borisov, H. Peter Anvin, Josh Poimboeuf,
David Kaplan, Sean Christopherson, Borislav Petkov, Dave Hansen,
Peter Zijlstra, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, KP Singh, Jiri Olsa, David S. Miller,
David Laight, Andy Lutomirski, Thomas Gleixner, Ingo Molnar,
David Ahern, Martin KaFai Lau, Eduard Zingerman, Song Liu,
Yonghong Song, John Fastabend, Stanislav Fomichev, Hao Luo,
Paolo Bonzini, Jonathan Corbet, linux-kernel, kvm, Asit Mallick,
Tao Zhang, bpf, netdev, linux-doc, chao.gao
In-Reply-To: <20260403233329.fb2ppifgwm3um6ny@desk>
On Fri, Apr 3, 2026 at 4:33 PM Pawan Gupta
<pawan.kumar.gupta@linux.intel.com> wrote:
>
> On Fri, Apr 03, 2026 at 04:22:28PM -0700, Jim Mattson wrote:
> > On Fri, Apr 3, 2026 at 4:16 PM Pawan Gupta
> > <pawan.kumar.gupta@linux.intel.com> wrote:
> > >
> > > On Fri, Apr 03, 2026 at 02:59:33PM -0700, Jim Mattson wrote:
> > > > On Fri, Apr 3, 2026 at 2:34 PM Pawan Gupta
> > > > <pawan.kumar.gupta@linux.intel.com> wrote:
> > > > >
> > > > > On Fri, Apr 03, 2026 at 01:19:17PM -0700, Jim Mattson wrote:
> > > > > > On Fri, Apr 3, 2026 at 11:52 AM Pawan Gupta
> > > > > > <pawan.kumar.gupta@linux.intel.com> wrote:
> > > > > > >
> > > > > > > On Fri, Apr 03, 2026 at 11:10:08AM -0700, Jim Mattson wrote:
> > > > > > > > On Thu, Apr 2, 2026 at 5:32 PM Pawan Gupta
> > > > > > > > <pawan.kumar.gupta@linux.intel.com> wrote:
> > > > > > > > >
> > > > > > > > > As a mitigation for BHI, clear_bhb_loop() executes branches that overwrite
> > > > > > > > > the Branch History Buffer (BHB). On Alder Lake and newer parts this
> > > > > > > > > sequence is not sufficient because it doesn't clear enough entries. This
> > > > > > > > > was not an issue because these CPUs use the BHI_DIS_S hardware mitigation
> > > > > > > > > in the kernel.
> > > > > > > > >
> > > > > > > > > Now with VMSCAPE (BHI variant) it is also required to isolate branch
> > > > > > > > > history between guests and userspace. Since BHI_DIS_S only protects the
> > > > > > > > > kernel, the newer CPUs also use IBPB.
> > > > > > > > >
> > > > > > > > > A cheaper alternative to the current IBPB mitigation is clear_bhb_loop().
> > > > > > > > > But it currently does not clear enough BHB entries to be effective on newer
> > > > > > > > > CPUs with larger BHB. At boot, dynamically set the loop count of
> > > > > > > > > clear_bhb_loop() such that it is effective on newer CPUs too. Use the
> > > > > > > > > X86_FEATURE_BHI_CTRL feature flag to select the appropriate loop count.
> > > > > > > > >
> > > > > > > > > Suggested-by: Dave Hansen <dave.hansen@linux.intel.com>
> > > > > > > > > Signed-off-by: Pawan Gupta <pawan.kumar.gupta@linux.intel.com>
> > > > > > > > > ---
> > > > > > > > > arch/x86/entry/entry_64.S | 8 +++++---
> > > > > > > > > arch/x86/include/asm/nospec-branch.h | 2 ++
> > > > > > > > > arch/x86/kernel/cpu/bugs.c | 13 +++++++++++++
> > > > > > > > > 3 files changed, 20 insertions(+), 3 deletions(-)
> > > > > > > > >
> > > > > > > > > diff --git a/arch/x86/entry/entry_64.S b/arch/x86/entry/entry_64.S
> > > > > > > > > index 3a180a36ca0e..bbd4b1c7ec04 100644
> > > > > > > > > --- a/arch/x86/entry/entry_64.S
> > > > > > > > > +++ b/arch/x86/entry/entry_64.S
> > > > > > > > > @@ -1536,7 +1536,9 @@ SYM_FUNC_START(clear_bhb_loop)
> > > > > > > > > ANNOTATE_NOENDBR
> > > > > > > > > push %rbp
> > > > > > > > > mov %rsp, %rbp
> > > > > > > > > - movl $5, %ecx
> > > > > > > > > +
> > > > > > > > > + movzbl bhb_seq_outer_loop(%rip), %ecx
> > > > > > > > > +
> > > > > > > > > ANNOTATE_INTRA_FUNCTION_CALL
> > > > > > > > > call 1f
> > > > > > > > > jmp 5f
> > > > > > > > > @@ -1556,8 +1558,8 @@ SYM_FUNC_START(clear_bhb_loop)
> > > > > > > > > * This should be ideally be: .skip 32 - (.Lret2 - 2f), 0xcc
> > > > > > > > > * but some Clang versions (e.g. 18) don't like this.
> > > > > > > > > */
> > > > > > > > > - .skip 32 - 18, 0xcc
> > > > > > > > > -2: movl $5, %eax
> > > > > > > > > + .skip 32 - 20, 0xcc
> > > > > > > > > +2: movzbl bhb_seq_inner_loop(%rip), %eax
> > > > > > > > > 3: jmp 4f
> > > > > > > > > nop
> > > > > > > > > 4: sub $1, %eax
> > > > > > > > > diff --git a/arch/x86/include/asm/nospec-branch.h b/arch/x86/include/asm/nospec-branch.h
> > > > > > > > > index 70b377fcbc1c..87b83ae7c97f 100644
> > > > > > > > > --- a/arch/x86/include/asm/nospec-branch.h
> > > > > > > > > +++ b/arch/x86/include/asm/nospec-branch.h
> > > > > > > > > @@ -548,6 +548,8 @@ DECLARE_PER_CPU(u64, x86_spec_ctrl_current);
> > > > > > > > > extern void update_spec_ctrl_cond(u64 val);
> > > > > > > > > extern u64 spec_ctrl_current(void);
> > > > > > > > >
> > > > > > > > > +extern u8 bhb_seq_inner_loop, bhb_seq_outer_loop;
> > > > > > > > > +
> > > > > > > > > /*
> > > > > > > > > * With retpoline, we must use IBRS to restrict branch prediction
> > > > > > > > > * before calling into firmware.
> > > > > > > > > diff --git a/arch/x86/kernel/cpu/bugs.c b/arch/x86/kernel/cpu/bugs.c
> > > > > > > > > index 83f51cab0b1e..2cb4a96247d8 100644
> > > > > > > > > --- a/arch/x86/kernel/cpu/bugs.c
> > > > > > > > > +++ b/arch/x86/kernel/cpu/bugs.c
> > > > > > > > > @@ -2047,6 +2047,10 @@ enum bhi_mitigations {
> > > > > > > > > static enum bhi_mitigations bhi_mitigation __ro_after_init =
> > > > > > > > > IS_ENABLED(CONFIG_MITIGATION_SPECTRE_BHI) ? BHI_MITIGATION_AUTO : BHI_MITIGATION_OFF;
> > > > > > > > >
> > > > > > > > > +/* Default to short BHB sequence values */
> > > > > > > > > +u8 bhb_seq_outer_loop __ro_after_init = 5;
> > > > > > > > > +u8 bhb_seq_inner_loop __ro_after_init = 5;
> > > > > > > > > +
> > > > > > > > > static int __init spectre_bhi_parse_cmdline(char *str)
> > > > > > > > > {
> > > > > > > > > if (!str)
> > > > > > > > > @@ -3242,6 +3246,15 @@ void __init cpu_select_mitigations(void)
> > > > > > > > > x86_spec_ctrl_base &= ~SPEC_CTRL_MITIGATIONS_MASK;
> > > > > > > > > }
> > > > > > > > >
> > > > > > > > > + /*
> > > > > > > > > + * Switch to long BHB clear sequence on newer CPUs (with BHI_CTRL
> > > > > > > > > + * support), see Intel's BHI guidance.
> > > > > > > > > + */
> > > > > > > > > + if (cpu_feature_enabled(X86_FEATURE_BHI_CTRL)) {
> > > > > > > > > + bhb_seq_outer_loop = 12;
> > > > > > > > > + bhb_seq_inner_loop = 7;
> > > > > > > > > + }
> > > > > > > > > +
> > > > > > > >
> > > > > > > > How does this work for VMs in a heterogeneous migration pool that
> > > > > > > > spans the Alder Lake boundary? They can't advertise BHI_CTRL, because
> > > > > > > > it isn't available on all hosts in the migration pool, but they need
> > > > > > > > the long sequence when running on Alder Lake or newer.
> > > > > > >
> > > > > > > As we discussed elsewhere, support for migration pool is much more
> > > > > > > involved. It should be dealt in a separate QEMU/KVM focused series.
> > > > > > >
> > > > > > > A quickfix could be adding support for spectre_bhi=long that guests in a
> > > > > > > migration pool can use?
> > > > > >
> > > > > > The simplest solution is to add "|
> > > > > > cpu_feature_enabled(X86_FEATURE_HYPERVISOR)" to the condition above.
> > > > > > If that is unacceptable for the performance of pre-Alder Lake
> > > > >
> > > > > Yes, that would be unnecessary overhead.
> > > > >
> > > > > > migration pools, you could define a CPUID or MSR bit that says
> > > > > > explicitly, "long BHB flush sequence needed," rather than trying to
> > > > > > intuit that property from the presence of BHI_CTRL. Like
> > > > > > IA32_ARCH_CAPABILITIES.SKIP_L1DFL_VMENTRY, the bit would only be set
> > > > > > by a hypervisor.
> > > > >
> > > > > I will think about this more.
> > > > >
> > > > > > I am still skeptical of the need for MSR_VIRTUAL_ENUMERATION and
> > > > > > friends, unless there is a major guest OS out there that relies on
> > > > > > them.
> > > > >
> > > > > If we forget about MSR_VIRTUAL_ENUMERATION for a moment, userspace VMM is
> > > > > in the best position to decide whether a guest needs
> > > > > virtual.SPEC_CTRL[BHI_DIS_S]. Via a KVM interface userspace VMM can get
> > > > > BHI_DIS_S for the guests that are in migration pool?
> > > >
> > > > That is not possible today, since KVM does not implement Intel's
> > > > IA32_SPEC_CTRL virtualization, and cedes the hardware IA32_SPEC_CTRL
> > > > to the guest after the first non-zero write to the guest's MSR.
> > >
> > > Yes, KVM doesn't support it yet. But, adding that support to give more
> > > control to userspace VMM helps this case, and probably many other in
> > > the future.
> >
> > But didn't you tell me that Windows doesn't want the hypervisor to set
> > BHI_DIS_S behind their back?
>
> Since cloud providers have greater control over userspace, the decision to
> use BHI_DIS_S or not can be left to them. KVM would simply follow what it
> is asked to do by the userspace.
I feel like we've gone over this before, but if userspace tells KVM
not to enable BHI_DIS_S, how do we inform Windows that it needs to do
the longer clearing sequence, despite the fact that the virtual CPU is
masquerading as Ice Lake?
I don't think the virtual mitigation MSRs address that issue.
> > > I will check with Chao if he can prepare the next version of virtual
> > > SPEC_CTRL series (leaving out virtual mitigation MSRs).
> >
> > Excellent.
^ permalink raw reply
* Re: [PATCH v9 02/10] x86/bhi: Make clear_bhb_loop() effective on newer CPUs
From: Pawan Gupta @ 2026-04-03 23:33 UTC (permalink / raw)
To: Jim Mattson
Cc: x86, Jon Kohler, Nikolay Borisov, H. Peter Anvin, Josh Poimboeuf,
David Kaplan, Sean Christopherson, Borislav Petkov, Dave Hansen,
Peter Zijlstra, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, KP Singh, Jiri Olsa, David S. Miller,
David Laight, Andy Lutomirski, Thomas Gleixner, Ingo Molnar,
David Ahern, Martin KaFai Lau, Eduard Zingerman, Song Liu,
Yonghong Song, John Fastabend, Stanislav Fomichev, Hao Luo,
Paolo Bonzini, Jonathan Corbet, linux-kernel, kvm, Asit Mallick,
Tao Zhang, bpf, netdev, linux-doc, chao.gao
In-Reply-To: <CALMp9eT2vJBdLPY2uBYrPgVrhS_aYmfGfdXe6MZXG_gyryLHVA@mail.gmail.com>
On Fri, Apr 03, 2026 at 04:22:28PM -0700, Jim Mattson wrote:
> On Fri, Apr 3, 2026 at 4:16 PM Pawan Gupta
> <pawan.kumar.gupta@linux.intel.com> wrote:
> >
> > On Fri, Apr 03, 2026 at 02:59:33PM -0700, Jim Mattson wrote:
> > > On Fri, Apr 3, 2026 at 2:34 PM Pawan Gupta
> > > <pawan.kumar.gupta@linux.intel.com> wrote:
> > > >
> > > > On Fri, Apr 03, 2026 at 01:19:17PM -0700, Jim Mattson wrote:
> > > > > On Fri, Apr 3, 2026 at 11:52 AM Pawan Gupta
> > > > > <pawan.kumar.gupta@linux.intel.com> wrote:
> > > > > >
> > > > > > On Fri, Apr 03, 2026 at 11:10:08AM -0700, Jim Mattson wrote:
> > > > > > > On Thu, Apr 2, 2026 at 5:32 PM Pawan Gupta
> > > > > > > <pawan.kumar.gupta@linux.intel.com> wrote:
> > > > > > > >
> > > > > > > > As a mitigation for BHI, clear_bhb_loop() executes branches that overwrite
> > > > > > > > the Branch History Buffer (BHB). On Alder Lake and newer parts this
> > > > > > > > sequence is not sufficient because it doesn't clear enough entries. This
> > > > > > > > was not an issue because these CPUs use the BHI_DIS_S hardware mitigation
> > > > > > > > in the kernel.
> > > > > > > >
> > > > > > > > Now with VMSCAPE (BHI variant) it is also required to isolate branch
> > > > > > > > history between guests and userspace. Since BHI_DIS_S only protects the
> > > > > > > > kernel, the newer CPUs also use IBPB.
> > > > > > > >
> > > > > > > > A cheaper alternative to the current IBPB mitigation is clear_bhb_loop().
> > > > > > > > But it currently does not clear enough BHB entries to be effective on newer
> > > > > > > > CPUs with larger BHB. At boot, dynamically set the loop count of
> > > > > > > > clear_bhb_loop() such that it is effective on newer CPUs too. Use the
> > > > > > > > X86_FEATURE_BHI_CTRL feature flag to select the appropriate loop count.
> > > > > > > >
> > > > > > > > Suggested-by: Dave Hansen <dave.hansen@linux.intel.com>
> > > > > > > > Signed-off-by: Pawan Gupta <pawan.kumar.gupta@linux.intel.com>
> > > > > > > > ---
> > > > > > > > arch/x86/entry/entry_64.S | 8 +++++---
> > > > > > > > arch/x86/include/asm/nospec-branch.h | 2 ++
> > > > > > > > arch/x86/kernel/cpu/bugs.c | 13 +++++++++++++
> > > > > > > > 3 files changed, 20 insertions(+), 3 deletions(-)
> > > > > > > >
> > > > > > > > diff --git a/arch/x86/entry/entry_64.S b/arch/x86/entry/entry_64.S
> > > > > > > > index 3a180a36ca0e..bbd4b1c7ec04 100644
> > > > > > > > --- a/arch/x86/entry/entry_64.S
> > > > > > > > +++ b/arch/x86/entry/entry_64.S
> > > > > > > > @@ -1536,7 +1536,9 @@ SYM_FUNC_START(clear_bhb_loop)
> > > > > > > > ANNOTATE_NOENDBR
> > > > > > > > push %rbp
> > > > > > > > mov %rsp, %rbp
> > > > > > > > - movl $5, %ecx
> > > > > > > > +
> > > > > > > > + movzbl bhb_seq_outer_loop(%rip), %ecx
> > > > > > > > +
> > > > > > > > ANNOTATE_INTRA_FUNCTION_CALL
> > > > > > > > call 1f
> > > > > > > > jmp 5f
> > > > > > > > @@ -1556,8 +1558,8 @@ SYM_FUNC_START(clear_bhb_loop)
> > > > > > > > * This should be ideally be: .skip 32 - (.Lret2 - 2f), 0xcc
> > > > > > > > * but some Clang versions (e.g. 18) don't like this.
> > > > > > > > */
> > > > > > > > - .skip 32 - 18, 0xcc
> > > > > > > > -2: movl $5, %eax
> > > > > > > > + .skip 32 - 20, 0xcc
> > > > > > > > +2: movzbl bhb_seq_inner_loop(%rip), %eax
> > > > > > > > 3: jmp 4f
> > > > > > > > nop
> > > > > > > > 4: sub $1, %eax
> > > > > > > > diff --git a/arch/x86/include/asm/nospec-branch.h b/arch/x86/include/asm/nospec-branch.h
> > > > > > > > index 70b377fcbc1c..87b83ae7c97f 100644
> > > > > > > > --- a/arch/x86/include/asm/nospec-branch.h
> > > > > > > > +++ b/arch/x86/include/asm/nospec-branch.h
> > > > > > > > @@ -548,6 +548,8 @@ DECLARE_PER_CPU(u64, x86_spec_ctrl_current);
> > > > > > > > extern void update_spec_ctrl_cond(u64 val);
> > > > > > > > extern u64 spec_ctrl_current(void);
> > > > > > > >
> > > > > > > > +extern u8 bhb_seq_inner_loop, bhb_seq_outer_loop;
> > > > > > > > +
> > > > > > > > /*
> > > > > > > > * With retpoline, we must use IBRS to restrict branch prediction
> > > > > > > > * before calling into firmware.
> > > > > > > > diff --git a/arch/x86/kernel/cpu/bugs.c b/arch/x86/kernel/cpu/bugs.c
> > > > > > > > index 83f51cab0b1e..2cb4a96247d8 100644
> > > > > > > > --- a/arch/x86/kernel/cpu/bugs.c
> > > > > > > > +++ b/arch/x86/kernel/cpu/bugs.c
> > > > > > > > @@ -2047,6 +2047,10 @@ enum bhi_mitigations {
> > > > > > > > static enum bhi_mitigations bhi_mitigation __ro_after_init =
> > > > > > > > IS_ENABLED(CONFIG_MITIGATION_SPECTRE_BHI) ? BHI_MITIGATION_AUTO : BHI_MITIGATION_OFF;
> > > > > > > >
> > > > > > > > +/* Default to short BHB sequence values */
> > > > > > > > +u8 bhb_seq_outer_loop __ro_after_init = 5;
> > > > > > > > +u8 bhb_seq_inner_loop __ro_after_init = 5;
> > > > > > > > +
> > > > > > > > static int __init spectre_bhi_parse_cmdline(char *str)
> > > > > > > > {
> > > > > > > > if (!str)
> > > > > > > > @@ -3242,6 +3246,15 @@ void __init cpu_select_mitigations(void)
> > > > > > > > x86_spec_ctrl_base &= ~SPEC_CTRL_MITIGATIONS_MASK;
> > > > > > > > }
> > > > > > > >
> > > > > > > > + /*
> > > > > > > > + * Switch to long BHB clear sequence on newer CPUs (with BHI_CTRL
> > > > > > > > + * support), see Intel's BHI guidance.
> > > > > > > > + */
> > > > > > > > + if (cpu_feature_enabled(X86_FEATURE_BHI_CTRL)) {
> > > > > > > > + bhb_seq_outer_loop = 12;
> > > > > > > > + bhb_seq_inner_loop = 7;
> > > > > > > > + }
> > > > > > > > +
> > > > > > >
> > > > > > > How does this work for VMs in a heterogeneous migration pool that
> > > > > > > spans the Alder Lake boundary? They can't advertise BHI_CTRL, because
> > > > > > > it isn't available on all hosts in the migration pool, but they need
> > > > > > > the long sequence when running on Alder Lake or newer.
> > > > > >
> > > > > > As we discussed elsewhere, support for migration pool is much more
> > > > > > involved. It should be dealt in a separate QEMU/KVM focused series.
> > > > > >
> > > > > > A quickfix could be adding support for spectre_bhi=long that guests in a
> > > > > > migration pool can use?
> > > > >
> > > > > The simplest solution is to add "|
> > > > > cpu_feature_enabled(X86_FEATURE_HYPERVISOR)" to the condition above.
> > > > > If that is unacceptable for the performance of pre-Alder Lake
> > > >
> > > > Yes, that would be unnecessary overhead.
> > > >
> > > > > migration pools, you could define a CPUID or MSR bit that says
> > > > > explicitly, "long BHB flush sequence needed," rather than trying to
> > > > > intuit that property from the presence of BHI_CTRL. Like
> > > > > IA32_ARCH_CAPABILITIES.SKIP_L1DFL_VMENTRY, the bit would only be set
> > > > > by a hypervisor.
> > > >
> > > > I will think about this more.
> > > >
> > > > > I am still skeptical of the need for MSR_VIRTUAL_ENUMERATION and
> > > > > friends, unless there is a major guest OS out there that relies on
> > > > > them.
> > > >
> > > > If we forget about MSR_VIRTUAL_ENUMERATION for a moment, userspace VMM is
> > > > in the best position to decide whether a guest needs
> > > > virtual.SPEC_CTRL[BHI_DIS_S]. Via a KVM interface userspace VMM can get
> > > > BHI_DIS_S for the guests that are in migration pool?
> > >
> > > That is not possible today, since KVM does not implement Intel's
> > > IA32_SPEC_CTRL virtualization, and cedes the hardware IA32_SPEC_CTRL
> > > to the guest after the first non-zero write to the guest's MSR.
> >
> > Yes, KVM doesn't support it yet. But, adding that support to give more
> > control to userspace VMM helps this case, and probably many other in
> > the future.
>
> But didn't you tell me that Windows doesn't want the hypervisor to set
> BHI_DIS_S behind their back?
Since cloud providers have greater control over userspace, the decision to
use BHI_DIS_S or not can be left to them. KVM would simply follow what it
is asked to do by the userspace.
> > I will check with Chao if he can prepare the next version of virtual
> > SPEC_CTRL series (leaving out virtual mitigation MSRs).
>
> Excellent.
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox