Linux Documentation
 help / color / mirror / Atom feed
* 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 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: 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

* [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

* [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 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] 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

* 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

* 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] 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 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: allow long unbroken headings to wrap and prevent overflow
From: Rito Rhymes @ 2026-04-04  8:19 UTC (permalink / raw)
  To: Jonathan Corbet, Rito Rhymes, linux-doc; +Cc: Shuah Khan, linux-kernel, rdunlap
In-Reply-To: <87h5q3g288.fsf@trenco.lwn.net>

Jon,

Following up on this point:

> I do not see the problem you are referring to here; headings wrap just
> fine for me using both Firefox and Chrome. (Firefox arguably does a
> little better since it wraps at "/", but that is what also make it turn
> "I/O into "I/
> O."

In my previous reply I sent a catalog of the environments I tested, with
per-environment screenshots for both slash-delimited and
underscore-delimited cases.

Does that breakdown help explain why we may be seeing different results,
or help you reproduce the issue in the environments where it occurs?

Thanks,
Rito

^ permalink raw reply

* Documentation: fix two typos in latest update to the security report howto
From: Willy Tarreau @ 2026-04-04  8:20 UTC (permalink / raw)
  To: greg
  Cc: Jonathan Corbet, skhan, workflows, linux-doc, linux-kernel,
	Willy Tarreau

In previous patch "Documentation: clarify the mandatory and desirable
info for security reports" I left two typos that I didn't detect in local
checks. One is "get_maintainers.pl" (no 's' in the script name), and the
other one is a missing closing quote after "Reported-by", which didn't
have effect here but I don't know if it can break rendering elsewhere
(e.g. on the public HTML page). Better fix it before it gets merged.

Signed-off-by: Willy Tarreau <w@1wt.eu>
---

Greg, this is a fix for commit a72b832a482372 that is currently pending
in your char-misc-linus branch.
---
 Documentation/process/security-bugs.rst | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Documentation/process/security-bugs.rst b/Documentation/process/security-bugs.rst
index 0b1f6d8e3cbe..27b028e85861 100644
--- a/Documentation/process/security-bugs.rst
+++ b/Documentation/process/security-bugs.rst
@@ -59,7 +59,7 @@ In addition, the following information are highly desirable:
     immediately merged (see Documentation/process/submitting-patches.rst).
     This will save some back-and-forth exchanges if it is accepted, and you
     will be credited for finding and fixing this issue.  Note that in this case
-    only a ``Signed-off-by:`` tag is needed, without ``Reported-by:` when the
+    only a ``Signed-off-by:`` tag is needed, without ``Reported-by:`` when the
     reporter and author are the same.
 
   * **mitigations**: very often during a bug analysis, some ways of mitigating
@@ -88,7 +88,7 @@ recipients to send a report to.  In the Linux kernel, all official maintainers
 are trusted, so the consequences of accidentally including the wrong maintainer
 are essentially a bit more noise for that person, i.e. nothing dramatic.  As
 such, a suitable method to figure the list of maintainers (which kernel
-security officers use) is to rely on the get_maintainers.pl script, tuned to
+security officers use) is to rely on the get_maintainer.pl script, tuned to
 only report maintainers.  This script, when passed a file name, will look for
 its path in the MAINTAINERS file to figure a hierarchical list of relevant
 maintainers.  Calling it a first time with the finest level of filtering will
-- 
2.52.0


^ permalink raw reply related

* Re: [PATCH v8 0/3] Support runtime configuration for per-VM's HGATP mode
From: Anup Patel @ 2026-04-04 11:02 UTC (permalink / raw)
  To: fangyu.yu
  Cc: pbonzini, corbet, atish.patra, pjw, palmer, aou, alex, skhan,
	guoren, radim.krcmar, andrew.jones, linux-doc, kvm, kvm-riscv,
	linux-riscv, linux-kernel
In-Reply-To: <20260403153019.9916-1-fangyu.yu@linux.alibaba.com>

On Fri, Apr 3, 2026 at 9:00 PM <fangyu.yu@linux.alibaba.com> wrote:
>
> From: Fangyu Yu <fangyu.yu@linux.alibaba.com>
>
> Currently, RISC-V KVM hardcodes the G-stage page table format (HGATP mode)
> to the maximum mode detected at boot time (e.g., SV57x4 if supported). but
> often such a wide GPA is unnecessary, just as a host sometimes doesn't need
> sv57.
>
> This patch reuse KVM_CAP_VM_GPA_BITS to select HGATP.MODE. User-space can
> now explicitly request a specific HGATP mode (SV39x4, SV48x4, SV57x4 or
> SV32x4) during VM creation.
>
> ---
> Changes in v8:
>     - Reuse KVM_CAP_VM_GPA_BITS to advertise and select the effective GPA width
>       for a VM (Anup's suggestion).
>     - Handle the kvm == NULL case and task kvm->lock and kvm->slots_lock to serialize
>       against concurrent vCPU creation and memslot updates (Radim's suggestion).
>     - Link to v7:
>       https://lore.kernel.org/linux-riscv/20260402132303.6252-1-fangyu.yu@linux.alibaba.com/
> ---
> Changes in v7 (Anup's suggestions):
>     - Keep the original HGATP mode probing logic.
>     - Link to v6:
>       https://lore.kernel.org/linux-riscv/20260330122601.22140-1-fangyu.yu@linux.alibaba.com/
> ---
> Changes in v6 (Anup's suggestions):
>     - Reworked kvm_riscv_gstage_gpa_bits() and kvm_riscv_gstage_gpa_size() to
>       take "unsigned long pgd_levels" instead of "struct kvm_arch *".
>     - Moved kvm_riscv_gstage_mode() helper from kvm_host.h to kvm_gstage.h.
>     - Renamed kvm->arch.kvm_riscv_gstage_pgd_levels to kvm->arch.pgd_levels.
>     - Added pgd_levels to struct kvm_gstage to avoid repeated
>       gstage->kvm->arch pointer chasing.
>     - Link to v5:
>       https://lore.kernel.org/linux-riscv/20260204134507.33912-1-fangyu.yu@linux.alibaba.com/
> ---
> Changes in v5:
>     - Use architectural HGATP.MODE encodings as the bit index for the supported-mode
>       bitmap and for the VM-mode selection UAPI; no new UAPI mode/bit defines are
>       introduced(per Radim).
>     - Allow KVM_CAP_RISCV_SET_HGATP_MODE on RV32 as well(per Drew).
>     - Link to v4:
>       https://lore.kernel.org/linux-riscv/20260202140716.34323-1-fangyu.yu@linux.alibaba.com/
> ---
> Changes in v4:
>     - Extend kvm_riscv_gstage_mode_detect() to probe all HGATP.MODE values
>       supported by the host and record them in a bitmask.
>     - Treat unexpected pgd_levels in kvm_riscv_gstage_mode() as an internal error
>       (e.g. WARN_ON_ONCE())(per Radim).
>     - Move kvm_riscv_gstage_gpa_bits() and kvm_riscv_gstage_gpa_size() to header
>       as static inline helpers(per Radim).
>     - Drop gstage_mode_user_initialized and Remove the kvm_debug() message from
>       KVM_CAP_RISCV_SET_HGATP_MODE(per Radim).
>     - Link to v3:
>       https://lore.kernel.org/linux-riscv/20260125150450.27068-1-fangyu.yu@linux.alibaba.com/
> ---
> Changes in v3:
>     - Reworked the patch formatting (per Drew).
>     - Dropped kvm->arch.kvm_riscv_gstage_mode and derive HGATP.MODE from
>       kvm_riscv_gstage_pgd_levels via a helper, avoiding redundant per-VM state(per Drew).
>     - Removed kvm_riscv_gstage_max_mode and keep only kvm_riscv_gstage_max_pgd_levels
>       for host capability detection(per Drew).
>     - Other initialization and return value issues(per Drew).
>     - Enforce that KVM_CAP_RISCV_SET_HGATP_MODE can only be enabled before any vCPUs
>       are created by rejecting the ioctl once kvm->created_vcpus is non-zero(per Radim).
>     - Add a memslot safety check and reject the capability unless
>       kvm_are_all_memslots_empty(kvm) is true, ensuring the G-stage format is not
>       changed after any memslots have been installed(per Radim).
>     - Link to v2:
>       https://lore.kernel.org/linux-riscv/20260105143232.76715-1-fangyu.yu@linux.alibaba.com/
>
> Fangyu Yu (3):
>   RISC-V: KVM: Support runtime configuration for per-VM's HGATP mode
>   RISC-V: KVM: Cache gstage pgd_levels in struct kvm_gstage
>   RISC-V: KVM: Reuse KVM_CAP_VM_GPA_BITS to select HGATP.MODE
>
>  arch/riscv/include/asm/kvm_gstage.h | 47 ++++++++++++++++---
>  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                | 70 +++++++++--------------------
>  arch/riscv/kvm/vm.c                 | 49 ++++++++++++++++++--
>  arch/riscv/kvm/vmid.c               |  3 +-
>  7 files changed, 148 insertions(+), 99 deletions(-)
>
> --
> 2.50.1
>

Queued this series for Linux-7.1

Thanks,
Anup

^ permalink raw reply

* Re: [PATCH v6 2/4] iio: adc: ad4691: add initial driver for AD4691 family
From: David Lechner @ 2026-04-04 14:25 UTC (permalink / raw)
  To: radu.sabau, Lars-Peter Clausen, Michael Hennerich,
	Jonathan Cameron, Nuno Sá, Andy Shevchenko, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
	Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
	Philipp Zabel, Jonathan Corbet, Shuah Khan
  Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
	linux-doc
In-Reply-To: <20260403-ad4692-multichannel-sar-adc-driver-v6-2-fa2a01a57c4e@analog.com>

On 4/3/26 6:03 AM, Radu Sabau via B4 Relay wrote:
> From: Radu Sabau <radu.sabau@analog.com>
> 
> Add support for the Analog Devices AD4691 family of high-speed,
> low-power multichannel SAR ADCs: AD4691 (16-ch, 500 kSPS),
> AD4692 (16-ch, 1 MSPS), AD4693 (8-ch, 500 kSPS) and
> AD4694 (8-ch, 1 MSPS).
> 
> The driver implements a custom regmap layer over raw SPI to handle the
> device's mixed 1/2/3/4-byte register widths and uses the standard IIO
> read_raw/write_raw interface for single-channel reads.
> 
> The chip idles in Autonomous Mode so that single-shot read_raw can use
> the internal oscillator without disturbing the hardware configuration.
> 
> Three voltage supply domains are managed: avdd (required), vio, and a
> reference supply on either the REF pin (ref-supply, external buffer)
> or the REFIN pin (refin-supply, uses the on-chip reference buffer;
> REFBUF_EN is set accordingly). Hardware reset is performed via
> the reset controller framework; a software reset through SPI_CONFIG_A
> is used as fallback when no hardware reset is available.
> 
> Accumulator channel masking for single-shot reads uses ACC_MASK_REG via
> an ADDR_DESCENDING SPI write, which covers both mask bytes in a single
> 16-bit transfer.
> 
> Signed-off-by: Radu Sabau <radu.sabau@analog.com>
> ---
This patch looks in good shape. Although I wouldn't mind using
MEGA/MICRO, etc. more in numbers with more than 3 or 4 zeros.

Reviewed-by: David Lechner <dlechner@baylibre.com>



^ permalink raw reply

* Re: [PATCH v6 3/4] iio: adc: ad4691: add triggered buffer support
From: David Lechner @ 2026-04-04 15:12 UTC (permalink / raw)
  To: radu.sabau, Lars-Peter Clausen, Michael Hennerich,
	Jonathan Cameron, Nuno Sá, Andy Shevchenko, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
	Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
	Philipp Zabel, Jonathan Corbet, Shuah Khan
  Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
	linux-doc
In-Reply-To: <20260403-ad4692-multichannel-sar-adc-driver-v6-3-fa2a01a57c4e@analog.com>

On 4/3/26 6:03 AM, Radu Sabau via B4 Relay wrote:
> From: Radu Sabau <radu.sabau@analog.com>
> 
> Add buffered capture support using the IIO triggered buffer framework.
> 
> CNV Burst Mode: the GP pin identified by interrupt-names in the device
> tree is configured as DATA_READY output. The IRQ handler stops
> conversions and fires the IIO trigger; the trigger handler executes a
> pre-built SPI message that reads all active channels from the AVG_IN
> accumulator registers and then resets accumulator state and restarts
> conversions for the next cycle.
> 
> Manual Mode: CNV is tied to SPI CS so each transfer simultaneously
> reads the previous result and starts the next conversion (pipelined
> N+1 scheme). At preenable time a pre-built, optimised SPI message of
> N+1 transfers is constructed (N channel reads plus one NOOP to drain
> the pipeline). The trigger handler executes the message in a single
> spi_sync() call and collects the results. An external trigger (e.g.
> iio-trig-hrtimer) is required to drive the trigger at the desired
> sample rate.
> 
> Both modes share the same trigger handler and push a complete scan —
> one u16 slot per channel at its scan_index position, followed by a
> timestamp — to the IIO buffer via iio_push_to_buffers_with_ts().
> 
> The CNV Burst Mode sampling frequency (PWM period) is exposed as a
> buffer-level attribute via IIO_DEVICE_ATTR.
> 
> Signed-off-by: Radu Sabau <radu.sabau@analog.com>
> ---
>  drivers/iio/adc/Kconfig  |   2 +
>  drivers/iio/adc/ad4691.c | 592 ++++++++++++++++++++++++++++++++++++++++++++++-
>  2 files changed, 592 insertions(+), 2 deletions(-)
> 
> diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
> index 3685a03aa8dc..d498f16c0816 100644
> --- a/drivers/iio/adc/Kconfig
> +++ b/drivers/iio/adc/Kconfig
> @@ -142,6 +142,8 @@ config AD4170_4
>  config AD4691
>  	tristate "Analog Devices AD4691 Family ADC Driver"
>  	depends on SPI
> +	select IIO_BUFFER
> +	select IIO_TRIGGERED_BUFFER
>  	select REGMAP
>  	help
>  	  Say yes here to build support for Analog Devices AD4691 Family MuxSAR
> diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
> index 43bd408c3d11..f2a7273e43b9 100644
> --- a/drivers/iio/adc/ad4691.c
> +++ b/drivers/iio/adc/ad4691.c
> @@ -5,15 +5,19 @@
>   */
>  #include <linux/array_size.h>
>  #include <linux/bitfield.h>
> +#include <linux/bitmap.h>
>  #include <linux/bitops.h>
>  #include <linux/cleanup.h>
>  #include <linux/delay.h>
>  #include <linux/dev_printk.h>
>  #include <linux/device/devres.h>
>  #include <linux/err.h>
> +#include <linux/interrupt.h>
>  #include <linux/math.h>
>  #include <linux/module.h>
>  #include <linux/mod_devicetable.h>
> +#include <linux/property.h>
> +#include <linux/pwm.h>
>  #include <linux/regmap.h>
>  #include <linux/regulator/consumer.h>
>  #include <linux/reset.h>
> @@ -21,7 +25,12 @@
>  #include <linux/units.h>
>  #include <linux/unaligned.h>
>  
> +#include <linux/iio/buffer.h>
>  #include <linux/iio/iio.h>
> +#include <linux/iio/sysfs.h>
> +#include <linux/iio/trigger.h>
> +#include <linux/iio/triggered_buffer.h>
> +#include <linux/iio/trigger_consumer.h>
>  
>  #define AD4691_VREF_uV_MIN			2400000
>  #define AD4691_VREF_uV_MAX			5250000
> @@ -30,6 +39,8 @@
>  #define AD4691_VREF_3P3_uV_MAX			3750000
>  #define AD4691_VREF_4P096_uV_MAX		4500000
>  
> +#define AD4691_CNV_DUTY_CYCLE_NS		380
> +
>  #define AD4691_SPI_CONFIG_A_REG			0x000
>  #define AD4691_SW_RESET				(BIT(7) | BIT(0))
>  
> @@ -37,6 +48,7 @@
>  #define AD4691_CLAMP_STATUS1_REG		0x01A
>  #define AD4691_CLAMP_STATUS2_REG		0x01B
>  #define AD4691_DEVICE_SETUP			0x020
> +#define AD4691_MANUAL_MODE			BIT(2)
>  #define AD4691_LDO_EN				BIT(4)
>  #define AD4691_REF_CTRL				0x021
>  #define AD4691_REF_CTRL_MASK			GENMASK(4, 2)
> @@ -44,13 +56,18 @@
>  #define AD4691_OSC_FREQ_REG			0x023
>  #define AD4691_OSC_FREQ_MASK			GENMASK(3, 0)
>  #define AD4691_STD_SEQ_CONFIG			0x025
> +#define AD4691_SEQ_ALL_CHANNELS_OFF		0x00
>  #define AD4691_SPARE_CONTROL			0x02A
>  
> +#define AD4691_NOOP				0x00
> +#define AD4691_ADC_CHAN(ch)			((0x10 + (ch)) << 3)
> +
>  #define AD4691_OSC_EN_REG			0x180
>  #define AD4691_STATE_RESET_REG			0x181
>  #define AD4691_STATE_RESET_ALL			0x01
>  #define AD4691_ADC_SETUP			0x182
>  #define AD4691_ADC_MODE_MASK			GENMASK(1, 0)
> +#define AD4691_CNV_BURST_MODE			0x01
>  #define AD4691_AUTONOMOUS_MODE			0x02
>  /*
>   * ACC_MASK_REG covers both mask bytes via ADDR_DESCENDING SPI: writing a
> @@ -60,6 +77,8 @@
>  #define AD4691_ACC_DEPTH_IN(n)			(0x186 + (n))
>  #define AD4691_GPIO_MODE1_REG			0x196
>  #define AD4691_GPIO_MODE2_REG			0x197
> +#define AD4691_GP_MODE_MASK			GENMASK(3, 0)
> +#define AD4691_GP_MODE_DATA_READY		0x06
>  #define AD4691_GPIO_READ			0x1A0
>  #define AD4691_ACC_STATUS_FULL1_REG		0x1B0
>  #define AD4691_ACC_STATUS_FULL2_REG		0x1B1
> @@ -95,9 +114,11 @@ struct ad4691_chip_info {
>  		.type = IIO_VOLTAGE,					\
>  		.indexed = 1,						\
>  		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW)		\
> -				    | BIT(IIO_CHAN_INFO_SAMP_FREQ),	\
> +				    | BIT(IIO_CHAN_INFO_SAMP_FREQ)	\
> +				    | BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),	\
>  		.info_mask_separate_available =				\
> -				      BIT(IIO_CHAN_INFO_SAMP_FREQ),	\
> +				      BIT(IIO_CHAN_INFO_SAMP_FREQ)	\
> +				    | BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),	\
>  		.info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE),	\
>  		.channel = ch,						\
>  		.scan_index = ch,					\
> @@ -125,6 +146,7 @@ static const struct iio_chan_spec ad4691_channels[] = {
>  	AD4691_CHANNEL(13),
>  	AD4691_CHANNEL(14),
>  	AD4691_CHANNEL(15),
> +	IIO_CHAN_SOFT_TIMESTAMP(16),
>  };
>  
>  static const struct iio_chan_spec ad4693_channels[] = {
> @@ -136,6 +158,7 @@ static const struct iio_chan_spec ad4693_channels[] = {
>  	AD4691_CHANNEL(5),
>  	AD4691_CHANNEL(6),
>  	AD4691_CHANNEL(7),
> +	IIO_CHAN_SOFT_TIMESTAMP(8),
>  };
>  
>  /*
> @@ -162,6 +185,14 @@ static const int ad4691_osc_freqs_Hz[] = {
>  	[0xF] = 1250,
>  };
>  
> +static const char * const ad4691_gp_names[] = { "gp0", "gp1", "gp2", "gp3" };
> +
> +/*
> + * Valid ACC_DEPTH values where the effective divisor equals the count.
> + * From Table 13: ACC_DEPTH = 2^N yields right-shift = N, divisor = 2^N.
> + */
> +static const int ad4691_oversampling_ratios[] = { 1, 2, 4, 8, 16, 32 };

It would be nice to add oversampling in a separate commit as that is a
separate feature.

Oversampling also affects sampling frequency. When there isn't oversampling,
sample rate == conversion rate. However, with oversampling, sample rate ==
conversion rate / oversampling ratio (because each sample involves #OSR
conversions).

So more code will be required to make IIO_CHAN_INFO_SAMP_FREQ attributes
(both read/write_raw and read_avail) adjust the values based on the current
oversampling ratio.

> +
>  static const struct ad4691_chip_info ad4691_chip_info = {
>  	.channels = ad4691_channels,
>  	.name = "ad4691",
> @@ -193,16 +224,55 @@ static const struct ad4691_chip_info ad4694_chip_info = {
>  struct ad4691_state {
>  	const struct ad4691_chip_info *info;
>  	struct regmap *regmap;
> +
> +	struct pwm_device *conv_trigger;
> +	int irq;
> +
> +	bool manual_mode;
> +
>  	int vref_uV;
> +	u8 osr[16];
>  	bool refbuf_en;
>  	bool ldo_en;
> +	u32 cnv_period_ns;
>  	/*
>  	 * Synchronize access to members of the driver state, and ensure
>  	 * atomicity of consecutive SPI operations.
>  	 */
>  	struct mutex lock;
> +	/*
> +	 * Per-buffer-enable lifetime resources:
> +	 * Manual Mode - a pre-built SPI message that clocks out N+1
> +	 *		 transfers in one go.
> +	 * CNV Burst Mode - a pre-built SPI message that clocks out 2*N
> +	 *		    transfers in one go.
> +	 */
> +	struct spi_message scan_msg;
> +	struct spi_transfer *scan_xfers;
> +	__be16 *scan_tx;
> +	__be16 *scan_rx;

Why not embed these arrays here? Then we don't have to deal with
alloc/free later.

> +	/* Scan buffer: one slot per channel plus timestamp */
> +	struct {
> +		u16 vals[16];
> +		aligned_s64 ts;
> +	} scan __aligned(IIO_DMA_MINALIGN);

Better would be IIO_DECLARE_BUFFER_WITH_TS() since we don't always
use all vals.

Also, current usage doesn't need to be DMA-safe because scan_tx
is being used for the actual SPI xfer.

>  };
>  
> +/*
> + * Configure the given GP pin (0-3) as DATA_READY output.
> + * GP0/GP1 → GPIO_MODE1_REG, GP2/GP3 → GPIO_MODE2_REG.
> + * Even pins occupy bits [3:0], odd pins bits [7:4].
> + */
> +static int ad4691_gpio_setup(struct ad4691_state *st, unsigned int gp_num)
> +{
> +	unsigned int shift = 4 * (gp_num % 2);
> +
> +	return regmap_update_bits(st->regmap,
> +				  AD4691_GPIO_MODE1_REG + gp_num / 2,
> +				  AD4691_GP_MODE_MASK << shift,
> +				  AD4691_GP_MODE_DATA_READY << shift);
> +}
> +
>  static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
>  {
>  	struct spi_device *spi = context;
> @@ -362,6 +432,24 @@ static int ad4691_set_sampling_freq(struct iio_dev *indio_dev, int freq)
>  	return -EINVAL;
>  }
>  
> +static int ad4691_set_oversampling_ratio(struct iio_dev *indio_dev,
> +					 const struct iio_chan_spec *chan,
> +					 int osr)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +
> +	if (osr < 1 || osr > 32 || !is_power_of_2(osr))
> +		return -EINVAL;
> +
> +	IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
> +	if (IIO_DEV_ACQUIRE_FAILED(claim))
> +		return -EBUSY;
> +
> +	st->osr[chan->scan_index] = osr;
> +	return regmap_write(st->regmap,
> +			    AD4691_ACC_DEPTH_IN(chan->scan_index), osr);
> +}
> +
>  static int ad4691_read_avail(struct iio_dev *indio_dev,
>  			     struct iio_chan_spec const *chan,
>  			     const int **vals, int *type,
> @@ -376,6 +464,11 @@ static int ad4691_read_avail(struct iio_dev *indio_dev,
>  		*type = IIO_VAL_INT;
>  		*length = ARRAY_SIZE(ad4691_osc_freqs_Hz) - start;
>  		return IIO_AVAIL_LIST;
> +	case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
> +		*vals = ad4691_oversampling_ratios;
> +		*type = IIO_VAL_INT;
> +		*length = ARRAY_SIZE(ad4691_oversampling_ratios);
> +		return IIO_AVAIL_LIST;
>  	default:
>  		return -EINVAL;
>  	}
> @@ -406,6 +499,11 @@ static int ad4691_single_shot_read(struct iio_dev *indio_dev,
>  	if (ret)
>  		return ret;
>  
> +	ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(chan->scan_index),
> +			   st->osr[chan->scan_index]);
> +	if (ret)
> +		return ret;
> +
>  	ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, &reg_val);
>  	if (ret)
>  		return ret;
> @@ -452,6 +550,9 @@ static int ad4691_read_raw(struct iio_dev *indio_dev,
>  	}
>  	case IIO_CHAN_INFO_SAMP_FREQ:
>  		return ad4691_get_sampling_freq(st, val);
> +	case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
> +		*val = st->osr[chan->scan_index];
> +		return IIO_VAL_INT;
>  	case IIO_CHAN_INFO_SCALE:
>  		*val = st->vref_uV / (MICRO / MILLI);
>  		*val2 = chan->scan_type.realbits;
> @@ -468,6 +569,8 @@ static int ad4691_write_raw(struct iio_dev *indio_dev,
>  	switch (mask) {
>  	case IIO_CHAN_INFO_SAMP_FREQ:
>  		return ad4691_set_sampling_freq(indio_dev, val);
> +	case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
> +		return ad4691_set_oversampling_ratio(indio_dev, chan, val);
>  	default:
>  		return -EINVAL;
>  	}
> @@ -486,6 +589,385 @@ static int ad4691_reg_access(struct iio_dev *indio_dev, unsigned int reg,
>  	return regmap_write(st->regmap, reg, writeval);
>  }
>  
> +static int ad4691_set_pwm_freq(struct ad4691_state *st, int freq)
> +{
> +	if (!freq)
> +		return -EINVAL;
> +
> +	st->cnv_period_ns = DIV_ROUND_UP(NSEC_PER_SEC, freq);
> +	return 0;
> +}
> +
> +static int ad4691_sampling_enable(struct ad4691_state *st, bool enable)
> +{
> +	struct pwm_state conv_state = {
> +		.period     = st->cnv_period_ns,
> +		.duty_cycle = AD4691_CNV_DUTY_CYCLE_NS,
> +		.polarity   = PWM_POLARITY_NORMAL,
> +		.enabled    = enable,
> +	};
> +
> +	return pwm_apply_might_sleep(st->conv_trigger, &conv_state);
> +}
> +
> +/*
> + * ad4691_enter_conversion_mode - Switch the chip to its buffer conversion mode.
> + *
> + * Configures the ADC hardware registers for the mode selected at probe
> + * (CNV_BURST or MANUAL). Called from buffer preenable before starting
> + * sampling. The chip is in AUTONOMOUS mode during idle (for read_raw).
> + */
> +static int ad4691_enter_conversion_mode(struct ad4691_state *st)
> +{
> +	int ret;
> +
> +	if (st->manual_mode)
> +		return regmap_update_bits(st->regmap, AD4691_DEVICE_SETUP,
> +					  AD4691_MANUAL_MODE, AD4691_MANUAL_MODE);
> +
> +	ret = regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
> +				 AD4691_ADC_MODE_MASK, AD4691_CNV_BURST_MODE);
> +	if (ret)
> +		return ret;
> +
> +	return regmap_write(st->regmap, AD4691_STATE_RESET_REG,
> +			    AD4691_STATE_RESET_ALL);
> +}
> +
> +/*
> + * ad4691_exit_conversion_mode - Return the chip to AUTONOMOUS mode.
> + *
> + * Called from buffer postdisable to restore the chip to the
> + * idle state used by read_raw. Clears the sequencer and resets state.
> + */
> +static int ad4691_exit_conversion_mode(struct ad4691_state *st)
> +{
> +	if (st->manual_mode)
> +		return regmap_update_bits(st->regmap, AD4691_DEVICE_SETUP,
> +					  AD4691_MANUAL_MODE, 0);
> +
> +	return regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
> +				  AD4691_ADC_MODE_MASK, AD4691_AUTONOMOUS_MODE);
> +}
> +
> +static void ad4691_free_scan_bufs(struct ad4691_state *st)
> +{
> +	kfree(st->scan_xfers);
> +	kfree(st->scan_tx);
> +	kfree(st->scan_rx);
> +}
> +
> +static int ad4691_manual_buffer_preenable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	struct device *dev = regmap_get_device(st->regmap);
> +	struct spi_device *spi = to_spi_device(dev);
> +	unsigned int n_active = bitmap_weight(indio_dev->active_scan_mask,
> +					      iio_get_masklength(indio_dev));
> +	unsigned int n_xfers = n_active + 1;
> +	unsigned int k, i;
> +	int ret;
> +
> +	st->scan_xfers = kcalloc(n_xfers, sizeof(*st->scan_xfers), GFP_KERNEL);

Usually, we make st->scan_xfers a fixed array with the max number of possible
xfers. Then we don't have to deal with alloc/free.

> +	if (!st->scan_xfers)
> +		return -ENOMEM;
> +
> +	st->scan_tx = kcalloc(n_xfers, sizeof(*st->scan_tx), GFP_KERNEL);
> +	if (!st->scan_tx) {
> +		kfree(st->scan_xfers);
> +		return -ENOMEM;
> +	}
> +
> +	st->scan_rx = kcalloc(n_xfers, sizeof(*st->scan_rx), GFP_KERNEL);
> +	if (!st->scan_rx) {
> +		kfree(st->scan_tx);
> +		kfree(st->scan_xfers);
> +		return -ENOMEM;
> +	}
> +
> +	spi_message_init(&st->scan_msg);
> +
> +	k = 0;
> +	iio_for_each_active_channel(indio_dev, i) {
> +		st->scan_tx[k] = cpu_to_be16(AD4691_ADC_CHAN(i));
> +		st->scan_xfers[k].tx_buf = &st->scan_tx[k];
> +		st->scan_xfers[k].rx_buf = &st->scan_rx[k];
> +		st->scan_xfers[k].len = sizeof(__be16);
> +		st->scan_xfers[k].cs_change = 1;
> +		spi_message_add_tail(&st->scan_xfers[k], &st->scan_msg);
> +		k++;
> +	}
> +
> +	/* Final NOOP transfer to retrieve last channel's result. */
> +	st->scan_tx[k] = cpu_to_be16(AD4691_NOOP);
> +	st->scan_xfers[k].tx_buf = &st->scan_tx[k];
> +	st->scan_xfers[k].rx_buf = &st->scan_rx[k];
> +	st->scan_xfers[k].len = sizeof(__be16);
> +	spi_message_add_tail(&st->scan_xfers[k], &st->scan_msg);
> +
> +	st->scan_msg.spi = spi;

This isn't how the SPI framework is intended to be used. We should
have st->spi = spi in probe instead.

> +
> +	ret = spi_optimize_message(spi, &st->scan_msg);
> +	if (ret) {
> +		ad4691_free_scan_bufs(st);
> +		return ret;
> +	}
> +
> +	ret = ad4691_enter_conversion_mode(st);
> +	if (ret) {
> +		spi_unoptimize_message(&st->scan_msg);
> +		ad4691_free_scan_bufs(st);
> +		return ret;
> +	}
> +
> +	return 0;
> +}
> +
> +static int ad4691_manual_buffer_postdisable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	int ret;
> +
> +	ret = ad4691_exit_conversion_mode(st);
> +	spi_unoptimize_message(&st->scan_msg);
> +	ad4691_free_scan_bufs(st);
> +	return ret;
> +}
> +
> +static const struct iio_buffer_setup_ops ad4691_manual_buffer_setup_ops = {
> +	.preenable = &ad4691_manual_buffer_preenable,
> +	.postdisable = &ad4691_manual_buffer_postdisable,
> +};
> +
> +static int ad4691_cnv_burst_buffer_preenable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	struct device *dev = regmap_get_device(st->regmap);
> +	struct spi_device *spi = to_spi_device(dev);
> +	unsigned int n_active = bitmap_weight(indio_dev->active_scan_mask,
> +					      iio_get_masklength(indio_dev));
> +	unsigned int bit, k, i;
> +	int ret;
> +
> +	st->scan_xfers = kcalloc(2 * n_active, sizeof(*st->scan_xfers), GFP_KERNEL);
> +	if (!st->scan_xfers)
> +		return -ENOMEM;
> +
> +	st->scan_tx = kcalloc(n_active, sizeof(*st->scan_tx), GFP_KERNEL);
> +	if (!st->scan_tx) {
> +		kfree(st->scan_xfers);
> +		return -ENOMEM;
> +	}
> +
> +	st->scan_rx = kcalloc(n_active, sizeof(*st->scan_rx), GFP_KERNEL);
> +	if (!st->scan_rx) {
> +		kfree(st->scan_tx);
> +		kfree(st->scan_xfers);
> +		return -ENOMEM;
> +	}
> +
> +	spi_message_init(&st->scan_msg);
> +
> +	/*
> +	 * Each AVG_IN read needs two transfers: a 2-byte address write phase
> +	 * followed by a 2-byte data read phase. CS toggles between channels
> +	 * (cs_change=1 on the read phase of all but the last channel).
> +	 */
> +	k = 0;
> +	iio_for_each_active_channel(indio_dev, i) {
> +		st->scan_tx[k] = cpu_to_be16(0x8000 | AD4691_AVG_IN(i));
> +		st->scan_xfers[2 * k].tx_buf = &st->scan_tx[k];
> +		st->scan_xfers[2 * k].len = sizeof(__be16);
> +		spi_message_add_tail(&st->scan_xfers[2 * k], &st->scan_msg);
> +		st->scan_xfers[2 * k + 1].rx_buf = &st->scan_rx[k];
> +		st->scan_xfers[2 * k + 1].len = sizeof(__be16);
> +		if (k < n_active - 1)
> +			st->scan_xfers[2 * k + 1].cs_change = 1;
> +		spi_message_add_tail(&st->scan_xfers[2 * k + 1], &st->scan_msg);
> +		k++;
> +	}
> +
> +	st->scan_msg.spi = spi;
> +
> +	ret = spi_optimize_message(spi, &st->scan_msg);
> +	if (ret)
> +		goto err_free_bufs;
> +
> +	ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
> +			   bitmap_read(indio_dev->active_scan_mask, 0,
> +				       iio_get_masklength(indio_dev)));
> +	if (ret)
> +		goto err;
> +
> +	ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG,
> +			   ~bitmap_read(indio_dev->active_scan_mask, 0,
> +				iio_get_masklength(indio_dev)) & GENMASK(15, 0));
> +	if (ret)
> +		goto err;
> +
> +	iio_for_each_active_channel(indio_dev, bit) {
> +		ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(bit),
> +				   st->osr[bit]);
> +		if (ret)
> +			goto err;
> +	}
> +
> +	ret = ad4691_enter_conversion_mode(st);
> +	if (ret)
> +		goto err;
> +
> +	ret = ad4691_sampling_enable(st, true);
> +	if (ret)
> +		goto err;

Do we need to do something to exit conversion mode on error here?

> +
> +	enable_irq(st->irq);
> +	return 0;
> +err:
> +	spi_unoptimize_message(&st->scan_msg);
> +err_free_bufs:
> +	ad4691_free_scan_bufs(st);
> +	return ret;
> +}
> +
> +static int ad4691_cnv_burst_buffer_postdisable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	int ret;
> +
> +	disable_irq(st->irq);
> +
> +	ret = ad4691_sampling_enable(st, false);
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
> +			   AD4691_SEQ_ALL_CHANNELS_OFF);
> +	if (ret)
> +		return ret;
> +

This order of unwinding is not the exact reverse of how it was
set up. So either the order needs to be fixed or a comment added
explaining why this order is needed instead.

> +	ret = ad4691_exit_conversion_mode(st);
> +	spi_unoptimize_message(&st->scan_msg);
> +	ad4691_free_scan_bufs(st);
> +	return ret;
> +}
> +
> +static const struct iio_buffer_setup_ops ad4691_cnv_burst_buffer_setup_ops = {
> +	.preenable = &ad4691_cnv_burst_buffer_preenable,
> +	.postdisable = &ad4691_cnv_burst_buffer_postdisable,
> +};
> +
> +static ssize_t sampling_frequency_show(struct device *dev,
> +				       struct device_attribute *attr,
> +				       char *buf)
> +{
> +	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +
> +	return sysfs_emit(buf, "%u\n", (u32)(NSEC_PER_SEC / st->cnv_period_ns));
> +}
> +
> +static ssize_t sampling_frequency_store(struct device *dev,
> +					struct device_attribute *attr,
> +					const char *buf, size_t len)
> +{
> +	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	int freq, ret;
> +
> +	ret = kstrtoint(buf, 10, &freq);
> +	if (ret)
> +		return ret;
> +
> +	guard(mutex)(&st->lock);
> +
> +	if (iio_buffer_enabled(indio_dev))

This should be using iio_device_claim_direct(), otherwise
it is racy.

> +		return -EBUSY;
> +
> +	ret = ad4691_set_pwm_freq(st, freq);
> +	if (ret)
> +		return ret;
> +
> +	return len;
> +}
> +
> +static IIO_DEVICE_ATTR(sampling_frequency, 0644,
> +		       sampling_frequency_show,
> +		       sampling_frequency_store, 0);
> +
> +static const struct iio_dev_attr *ad4691_buffer_attrs[] = {
> +	&iio_dev_attr_sampling_frequency,
> +	NULL
> +};
> +
> +static irqreturn_t ad4691_irq(int irq, void *private)
> +{
> +	struct iio_dev *indio_dev = private;
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +
> +	/*
> +	 * GPx has asserted: stop conversions before reading so the

Does this happen per-channel or only once per complete sequence?

> +	 * accumulator does not continue sampling while the trigger handler
> +	 * processes the data. Then fire the IIO trigger to push the sample
> +	 * to the buffer.
> +	 */
> +	ad4691_sampling_enable(st, false);
> +	iio_trigger_poll(indio_dev->trig);
> +
> +	return IRQ_HANDLED;
> +}
> +
> +static const struct iio_trigger_ops ad4691_trigger_ops = {
> +	.validate_device = iio_trigger_validate_own_device,
> +};
> +
> +static int ad4691_read_scan(struct iio_dev *indio_dev, s64 timestamp)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	unsigned int i, k = 0;
> +	int ret;
> +
> +	guard(mutex)(&st->lock);
> +
> +	ret = spi_sync(st->scan_msg.spi, &st->scan_msg);
> +	if (ret)
> +		return ret;
> +
> +	if (st->manual_mode) {
> +		iio_for_each_active_channel(indio_dev, i) {
> +			st->scan.vals[i] = be16_to_cpu(st->scan_rx[k + 1]);
> +			k++;
> +		}
> +	} else {
> +		iio_for_each_active_channel(indio_dev, i) {
> +			st->scan.vals[i] = be16_to_cpu(st->scan_rx[k]);
> +			k++;
> +		}

I suppose this is fine, but we usually try to avoid extra copiying and
byte swapping of bufferes like this if we can. It seems completly doable
in both modes. Manual mode will just one extra two-byte buffer for the
throw-away conversion on the first read xfer (or just write to the same
element twice).

> +
> +		ret = regmap_write(st->regmap, AD4691_STATE_RESET_REG,
> +				   AD4691_STATE_RESET_ALL);
> +		if (ret)
> +			return ret;
> +
> +		ret = ad4691_sampling_enable(st, true);
> +		if (ret)
> +			return ret;
> +	}
> +
> +	iio_push_to_buffers_with_ts(indio_dev, &st->scan, sizeof(st->scan),
> +				    timestamp);
> +	return 0;
> +}
> +
> +static irqreturn_t ad4691_trigger_handler(int irq, void *p)
> +{
> +	struct iio_poll_func *pf = p;
> +	struct iio_dev *indio_dev = pf->indio_dev;
> +
> +	ad4691_read_scan(indio_dev, pf->timestamp);
> +	iio_trigger_notify_done(indio_dev->trig);
> +	return IRQ_HANDLED;
> +}
> +
>  static const struct iio_info ad4691_info = {
>  	.read_raw = &ad4691_read_raw,
>  	.write_raw = &ad4691_write_raw,
> @@ -493,6 +975,18 @@ static const struct iio_info ad4691_info = {
>  	.debugfs_reg_access = &ad4691_reg_access,
>  };
>  
> +static int ad4691_pwm_setup(struct ad4691_state *st)
> +{
> +	struct device *dev = regmap_get_device(st->regmap);
> +
> +	st->conv_trigger = devm_pwm_get(dev, "cnv");
> +	if (IS_ERR(st->conv_trigger))
> +		return dev_err_probe(dev, PTR_ERR(st->conv_trigger),
> +				     "Failed to get cnv pwm\n");
> +
> +	return ad4691_set_pwm_freq(st, st->info->max_rate);
> +}
> +
>  static int ad4691_regulator_setup(struct ad4691_state *st)
>  {
>  	struct device *dev = regmap_get_device(st->regmap);
> @@ -558,6 +1052,22 @@ static int ad4691_config(struct ad4691_state *st)
>  	unsigned int val;
>  	int ret;
>  
> +	/*
> +	 * Determine buffer conversion mode from DT: if a PWM is provided it
> +	 * drives the CNV pin (CNV_BURST_MODE); otherwise CNV is tied to CS
> +	 * and each SPI transfer triggers a conversion (MANUAL_MODE).
> +	 * Both modes idle in AUTONOMOUS mode so that read_raw can use the
> +	 * internal oscillator without disturbing the hardware configuration.
> +	 */
> +	if (device_property_present(dev, "pwms")) {
> +		st->manual_mode = false;
> +		ret = ad4691_pwm_setup(st);
> +		if (ret)
> +			return ret;
> +	} else {
> +		st->manual_mode = true;
> +	}
> +
>  	switch (st->vref_uV) {
>  	case AD4691_VREF_uV_MIN ... AD4691_VREF_2P5_uV_MAX:
>  		ref_val = AD4691_VREF_2P5;
> @@ -613,6 +1123,78 @@ static int ad4691_config(struct ad4691_state *st)
>  	return 0;
>  }
>  
> +static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
> +					 struct ad4691_state *st)
> +{
> +	struct device *dev = regmap_get_device(st->regmap);
> +	struct iio_trigger *trig;
> +	unsigned int i;
> +	int irq, ret;
> +
> +	trig = devm_iio_trigger_alloc(dev, "%s-dev%d",
> +				      indio_dev->name,
> +				      iio_device_id(indio_dev));
> +	if (!trig)
> +		return -ENOMEM;
> +
> +	trig->ops = &ad4691_trigger_ops;
> +	iio_trigger_set_drvdata(trig, st);
> +
> +	ret = devm_iio_trigger_register(dev, trig);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "IIO trigger register failed\n");
> +
> +	indio_dev->trig = iio_trigger_get(trig);
> +
> +	if (!st->manual_mode) {

I would invert the if since the other case is shorter.

> +		/*
> +		 * The GP pin named in interrupt-names asserts at end-of-conversion.
> +		 * The IRQ handler stops conversions and fires the IIO trigger so
> +		 * the trigger handler can read and push the sample to the buffer.
> +		 * The IRQ is kept disabled until the buffer is enabled.
> +		 */
> +		irq = -ENODEV;
> +		for (i = 0; i < ARRAY_SIZE(ad4691_gp_names); i++) {
> +			irq = fwnode_irq_get_byname(dev_fwnode(dev),
> +						    ad4691_gp_names[i]);
> +			if (irq > 0)
> +				break;
> +		}
> +		if (irq <= 0)
> +			return dev_err_probe(dev, irq < 0 ? irq : -ENODEV,
> +					     "failed to get GP interrupt\n");

Usually we would usually just use spi->irq since it already
has been looked up. But I guess it is OK to do it like this.

> +
> +		st->irq = irq;
> +
> +		ret = ad4691_gpio_setup(st, i);
> +		if (ret)
> +			return ret;
> +
> +		/*
> +		 * IRQ is kept disabled until the buffer is enabled to prevent
> +		 * spurious DATA_READY events before the SPI message is set up.
> +		 */
> +		ret = devm_request_threaded_irq(dev, irq, NULL,
> +						&ad4691_irq,
> +						IRQF_ONESHOT | IRQF_NO_AUTOEN,
> +						indio_dev->name, indio_dev);
> +		if (ret)
> +			return ret;
> +
> +		return devm_iio_triggered_buffer_setup_ext(dev, indio_dev,
> +							   &iio_pollfunc_store_time,
> +							   &ad4691_trigger_handler,
> +							   IIO_BUFFER_DIRECTION_IN,
> +							   &ad4691_cnv_burst_buffer_setup_ops,
> +							   ad4691_buffer_attrs);
> +	}
> +
> +	return devm_iio_triggered_buffer_setup(dev, indio_dev,
> +					       &iio_pollfunc_store_time,
> +					       &ad4691_trigger_handler,
> +					       &ad4691_manual_buffer_setup_ops);
> +}
> +

^ permalink raw reply

* Re: [PATCH v9 00/10] VMSCAPE optimization for BHI variant
From: David Laight @ 2026-04-04 15:20 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,
	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
In-Reply-To: <20260402-vmscape-bhb-v9-0-94d16bc29774@linux.intel.com>

On Thu, 2 Apr 2026 17:30:32 -0700
Pawan Gupta <pawan.kumar.gupta@linux.intel.com> wrote:

> v9:
> - Use global variables for BHB loop counters instead of ALTERNATIVE-based
>   approach. (Dave & others)
> - Use 32-bit registers (%eax/%ecx) for loop counters, loaded via movzbl
>   from 8-bit globals. 8-bit registers (e.g. %ah in the inner loop) caused
>   performance regression on certain CPUs due to partial-register stalls. (David Laight)
> - Let BPF save/restore %rax/%rcx as in the original implementation, since
>   it is the only caller that needs these registers preserved across the
>   BHB clearing sequence.

That is as dangerous as hell...
Does BPF even save %rcx - I'm sure I checked that a long time ago
and found it didn't.
(I'm mostly AFK over Easter and can't check.)
A least there should be a blood great big comment that BPF calls this code
and only saves specific registers.
But given the number of mispredicted branches and other pipeline stalls
in this code a couple of register saves to stack are unlikely to make
any difference.

	David


^ permalink raw reply

* Re: [PATCH v6 4/4] iio: adc: ad4691: add SPI offload support
From: David Lechner @ 2026-04-04 15:34 UTC (permalink / raw)
  To: radu.sabau, Lars-Peter Clausen, Michael Hennerich,
	Jonathan Cameron, Nuno Sá, Andy Shevchenko, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
	Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
	Philipp Zabel, Jonathan Corbet, Shuah Khan
  Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
	linux-doc
In-Reply-To: <20260403-ad4692-multichannel-sar-adc-driver-v6-4-fa2a01a57c4e@analog.com>

On 4/3/26 6:03 AM, Radu Sabau via B4 Relay wrote:
> From: Radu Sabau <radu.sabau@analog.com>
> 
> Add SPI offload support to enable DMA-based, CPU-independent data
> acquisition using the SPI Engine offload framework.
> 
> When an SPI offload is available (devm_spi_offload_get() succeeds),
> the driver registers a DMA engine IIO buffer and uses dedicated buffer
> setup operations. If no offload is available the existing software
> triggered buffer path is used unchanged.
> 
> Both CNV Burst Mode and Manual Mode support offload, but use different
> trigger mechanisms:
> 
> CNV Burst Mode: the SPI Engine is triggered by the ADC's DATA_READY
> signal on the GP pin specified by the trigger-source consumer reference
> in the device tree (one cell = GP pin number 0-3). For this mode the
> driver acts as both an SPI offload consumer (DMA RX stream, message
> optimization) and a trigger source provider: it registers the
> GP/DATA_READY output via devm_spi_offload_trigger_register() so the
> offload framework can match the '#trigger-source-cells' phandle and
> automatically fire the SPI Engine DMA transfer at end-of-conversion.
> 
> Manual Mode: the SPI Engine is triggered by a periodic trigger at
> the configured sampling frequency. The pre-built SPI message uses
> the pipelined CNV-on-CS protocol: N+1 4-byte transfers are issued
> for N active channels (the first result is discarded as garbage from
> the pipeline flush) and the remaining N results are captured by DMA.
> 
> All offload transfers use 32-bit frames (bits_per_word=32, len=4) for
> DMA word alignment. This patch promotes the channel scan_type from
> storagebits=16 (triggered-buffer path) to storagebits=32 to match the
> DMA word size; the triggered-buffer paths are updated to the same layout
> for consistency. CNV Burst Mode channel data arrives in the lower 16
> bits of the 32-bit word (shift=0); Manual Mode data arrives in the upper
> 16 bits (shift=16), matching the 4-byte SPI transfer layout
> [data_hi, data_lo, 0, 0]. A separate ad4691_manual_channels[] array
> encodes the shift=16 scan type for manual mode.
> 
> Add driver documentation under Documentation/iio/ad4691.rst covering
> operating modes, oversampling, reference voltage, SPI offload paths,
> and buffer data layout; register in MAINTAINERS and index.rst

Documentation should be separate patch. It covers more than just SPI
offload.

> 
> Kconfig gains a dependency on IIO_BUFFER_DMAENGINE.
> 
> Signed-off-by: Radu Sabau <radu.sabau@analog.com>
> ---
>  Documentation/iio/ad4691.rst | 259 ++++++++++++++++++++++++++
>  Documentation/iio/index.rst  |   1 +
>  MAINTAINERS                  |   1 +
>  drivers/iio/adc/Kconfig      |   1 +
>  drivers/iio/adc/ad4691.c     | 422 ++++++++++++++++++++++++++++++++++++++++++-
>  5 files changed, 676 insertions(+), 8 deletions(-)
> 
> diff --git a/Documentation/iio/ad4691.rst b/Documentation/iio/ad4691.rst
> new file mode 100644
> index 000000000000..36f0c841605a
> --- /dev/null
> +++ b/Documentation/iio/ad4691.rst
> @@ -0,0 +1,259 @@
> +.. SPDX-License-Identifier: GPL-2.0-only
> +
> +=============
> +AD4691 driver
> +=============
> +
> +ADC driver for Analog Devices Inc. AD4691 family of multichannel SAR ADCs.
> +The module name is ``ad4691``.
> +
> +
> +Supported devices
> +=================
> +
> +The following chips are supported by this driver:
> +
> +* `AD4691 <https://www.analog.com/en/products/ad4691.html>`_ — 16-channel, 500 kSPS
> +* `AD4692 <https://www.analog.com/en/products/ad4692.html>`_ — 16-channel, 1 MSPS
> +* `AD4693 <https://www.analog.com/en/products/ad4693.html>`_ — 8-channel, 500 kSPS
> +* `AD4694 <https://www.analog.com/en/products/ad4694.html>`_ — 8-channel, 1 MSPS
> +
> +
> +IIO channels
> +============
> +
> +Each physical ADC input maps to one IIO voltage channel. The AD4691 and AD4692
> +expose 16 channels (``voltage0`` through ``voltage15``); the AD4693 and AD4694
> +expose 8 channels (``voltage0`` through ``voltage7``).
> +
> +All channels share a common scale (``in_voltage_scale``), derived from the
> +reference voltage. Each channel independently exposes:
> +
> +* ``in_voltageN_raw`` — single-shot ADC result
> +* ``in_voltageN_sampling_frequency`` — internal oscillator frequency used for

As mentioned in another patch, sampling_frquency != osciallator frequency when
oversampling ratio != 1. So this needs to be changed to reflect that.

> +  single-shot reads and CNV Burst Mode buffered captures
> +* ``in_voltageN_sampling_frequency_available`` — list of valid oscillator
> +  frequencies
> +* ``in_voltageN_oversampling_ratio`` — per-channel hardware accumulation depth
> +* ``in_voltageN_oversampling_ratio_available`` — list of valid ratios
> +
> +
> +Operating modes
> +===============
> +
> +The driver supports two operating modes, auto-detected from the device tree at
> +probe time. Both modes transition to and from an internal Autonomous Mode idle
> +state when the IIO buffer is enabled and disabled.
> +
> +Manual Mode
> +-----------
> +
> +Selected when no ``pwms`` property is present in the device tree. The CNV pin
> +is tied to the SPI chip-select: every CS assertion both triggers a new
> +conversion and returns the result of the previous one (pipelined N+1 scheme).
> +
> +To read N channels the driver issues N+1 SPI transfers in a single optimised
> +message:
> +
> +* Transfers 0 to N-1 each carry ``AD4691_ADC_CHAN(n)`` in the TX byte to
> +  select the next channel; the RX byte of transfer ``k+1`` contains the result
> +  of the channel selected in transfer ``k``.
> +* Transfer N is a NOOP (0x00) to flush the last conversion result out of the
> +  pipeline.
> +
> +The external IIO trigger (``pollfunc_store_time``) drives the trigger handler,

I'm not sure "external" is the best word to describe this. I would say a "user-
defined IIO triger (e.g. hrtimer trigger)".

> +which executes the pre-built SPI message and pushes the scan to the buffer.
> +
> +CNV Burst Mode
> +--------------
> +
> +Selected when a ``pwms`` property is present in the device tree. The PWM drives
> +the CNV pin independently of SPI at the configured conversion rate, and a GP
> +pin (identified by ``interrupt-names``) asserts DATA_READY at end-of-burst to
> +signal that the AVG_IN result registers are ready to be read.
> +
> +The IRQ handler stops the PWM, fires the IIO trigger, and the trigger handler

If we stop the PWM after an IRQ, then we don't get a consistent sample rate.
Ideally, we would leave the PWM running and just pick a rate slow enough that
there is plenty of time to read the data. Otherwise, this mode doesn't seem
particularly useful.

> +reads all active ``AVG_IN(n)`` registers in a single optimised SPI message and
> +pushes the scan to the buffer.
> +
> +The buffer sampling frequency (i.e. the PWM rate) is controlled by the
> +``sampling_frequency`` attribute on the IIO buffer. Valid values span from the
> +chip's minimum oscillator rate up to its maximum conversion rate
> +(500 kSPS for AD4691/AD4693, 1 MSPS for AD4692/AD4694).

Valid, but not usable without SPI offload.

> +
> +Autonomous Mode (idle / single-shot)
> +-------------------------------------
> +
> +The chip idles in Autonomous Mode whenever the IIO buffer is disabled. In this
> +state, ``read_raw`` requests (``in_voltageN_raw``) use the internal oscillator
> +to perform a single conversion on the requested channel and read back the
> +result from the ``AVG_IN(N)`` register. The oscillator is started and stopped
> +for each read to save power.
> +
> +
> +Oversampling
> +============
> +
> +Each channel has an independent hardware accumulator (ACC_DEPTH_IN) that
> +averages a configurable number of successive conversions before DATA_READY
> +asserts. The result is always returned as a 16-bit mean from the ``AVG_IN``
> +register, so the IIO ``realbits`` and ``storagebits`` are unaffected by the
> +oversampling ratio.
> +
> +Valid ratios are 1, 2, 4, 8, 16 and 32. The default is 1 (no averaging).
> +
> +.. code-block:: bash
> +
> +    # Set oversampling ratio to 16 on channel 0
> +    echo 16 > /sys/bus/iio/devices/iio:device0/in_voltage0_oversampling_ratio
> +
> +When OSR > 1 the effective conversion rate for ``read_raw`` is reduced
> +accordingly, since the driver waits for 2 × OSR oscillator periods before
> +reading the result.
> +
> +
> +Reference voltage
> +=================
> +
> +The driver supports two reference configurations, mutually exclusive:
> +
> +* **External reference** (``ref-supply``): a voltage between 2.4 V and 5.25 V
> +  supplied externally. The internal reference buffer is disabled.
> +* **Buffered internal reference** (``refin-supply``): An internal reference
> +  buffer is used. The driver enables ``REFBUF_EN`` in the REF_CTRL register
> +  when this supply is used.
> +
> +Exactly one of ``ref-supply`` or ``refin-supply`` must be present in the
> +device tree.
> +
> +The reference voltage determines the full-scale range:
> +
> +.. code-block::
> +
> +    full-scale = Vref / 2^16  (per LSB)
> +
> +
> +LDO supply
> +==========
> +
> +The chip contains an internal LDO that powers part of the analog front-end.
> +The LDO input can be driven externally via the ``ldo-in-supply`` regulator. If
> +that supply is absent, the driver enables the internal LDO path (``LDO_EN``
> +bit in DEVICE_SETUP).
> +
> +
> +Reset
> +=====
> +
> +The driver supports two reset mechanisms:
> +
> +* **Hardware reset** (``reset-gpios`` in device tree): the GPIO is already
> +  asserted at driver probe by the reset controller framework. The driver waits
> +  for the required 300 µs reset pulse width and then deasserts.
> +* **Software reset** (fallback when ``reset-gpios`` is absent): the driver
> +  writes the software-reset pattern to the SPI_CONFIG_A register.
> +
> +
> +GP pins and interrupts
> +======================
> +
> +The chip exposes up to four general-purpose (GP) pins that can be configured as
> +interrupt outputs. In CNV Burst Mode (non-offload), one GP pin must be wired to

Or trigger sources.

> +an interrupt-capable SoC input and declared in the device tree using the
> +``interrupts`` and ``interrupt-names`` properties.
> +
> +The ``interrupt-names`` value identifies which GP pin is used (``"gp0"``
> +through ``"gp3"``). The driver configures that pin as a DATA_READY output in
> +the GPIO_MODE register.
> +
> +Example device tree fragment::
> +
> +    adc@0 {
> +        compatible = "adi,ad4692";
> +        ...
> +        interrupts = <17 IRQ_TYPE_LEVEL_HIGH>;
> +        interrupt-parent = <&gpio0>;
> +        interrupt-names = "gp0";
> +    };
> +
> +
> +SPI offload support
> +===================
> +
> +When a SPI offload engine (e.g. the AXI SPI Engine) is present, the driver
> +uses DMA-backed transfers for CPU-independent, high-throughput data capture.
> +SPI offload is detected automatically at probe via ``devm_spi_offload_get()``;
> +if no offload hardware is available the driver falls back to the software
> +triggered-buffer path.
> +
> +Two SPI offload sub-modes exist, corresponding to the two operating modes:
> +
> +CNV Burst offload
> +-----------------
> +
> +Used when a ``pwms`` property is present and SPI offload is available.
> +
> +The PWM drives CNV at the configured rate. On DATA_READY the SPI offload
> +engine automatically executes a pre-built message that reads all active
> +``AVG_IN`` registers and streams the data directly to an IIO DMA buffer with
> +no CPU involvement. A final state-reset transfer re-arms DATA_READY for the
> +next burst.
> +
> +The GP pin used as DATA_READY trigger is supplied by the trigger-source
> +consumer (via ``#trigger-source-cells``) at buffer enable time; no
> +``interrupt-names`` entry is required in this path.
> +
> +The buffer sampling frequency is controlled by the ``sampling_frequency``
> +attribute on the IIO buffer (same as the non-offload CNV Burst path).
> +
> +Manual offload
> +--------------
> +
> +Used when no ``pwms`` property is present and SPI offload is available.
> +
> +A periodic SPI offload trigger controls the conversion rate. On each trigger
> +period, the SPI engine executes an N+1 transfer message (same pipelined scheme

How does this work with oversampling?

> +as software Manual Mode) and streams the data directly to the IIO DMA buffer.
> +
> +The ``sampling_frequency`` attribute on the IIO buffer controls the trigger
> +rate (in Hz). The default is the chip's maximum conversion rate.
> +
> +
> +Buffer data format
> +==================
> +
> +The IIO buffer data format (``in_voltageN_type``) depends on the active path:
> +
> ++-------------------------+-------------+-------------+-------+
> +| Path                    | storagebits | realbits    | shift |
> ++=========================+=============+=============+=======+
> +| Triggered buffer        | 16          | 16          | 0     |
> ++-------------------------+-------------+-------------+-------+
> +| CNV Burst offload (DMA) | 32          | 16          | 0     |
> ++-------------------------+-------------+-------------+-------+
> +| Manual offload (DMA)    | 32          | 16          | 16    |
> ++-------------------------+-------------+-------------+-------+
> +
> +In the triggered-buffer path the driver unpacks the 16-bit result in software
> +before pushing to the buffer, so ``storagebits`` is 16.
> +
> +In the DMA offload paths the DMA engine writes 32-bit words directly into the
> +IIO DMA buffer:
> +
> +* **CNV Burst offload**: the SPI engine reads AVG_IN registers with a 2-byte
> +  address phase followed by a 2-byte data phase; the 16-bit result lands in
> +  the lower half of the 32-bit word (``shift=0``).
> +* **Manual offload**: each 32-bit SPI word carries the channel byte in the
> +  first byte; the 16-bit result is returned in the upper half of the 32-bit

I would expect the "first" byte to be in the "upper half" of the 32-bits as
well. This layout could be explained better.

Also, since extra data has to be read in this mode, does this affect the max
conversion rate?

> +  word (``shift=16``).
> +
> +The ``in_voltageN_type`` sysfs attribute reflects the active scan type.
> +
> +
> +Unimplemented features
> +======================
> +
> +* GPIO controller functionality of the GP pins
> +* Clamp status and overrange events
> +* Raw accumulator (ACC_IN) and accumulator status registers
> +* ADC_BUSY and overrun status interrupts

^ permalink raw reply

* Re: [PATCH v6 4/4] iio: adc: ad4691: add SPI offload support
From: David Lechner @ 2026-04-04 15:57 UTC (permalink / raw)
  To: radu.sabau, Lars-Peter Clausen, Michael Hennerich,
	Jonathan Cameron, Nuno Sá, Andy Shevchenko, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
	Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
	Philipp Zabel, Jonathan Corbet, Shuah Khan
  Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
	linux-doc
In-Reply-To: <20260403-ad4692-multichannel-sar-adc-driver-v6-4-fa2a01a57c4e@analog.com>

On 4/3/26 6:03 AM, Radu Sabau via B4 Relay wrote:
> From: Radu Sabau <radu.sabau@analog.com>
> 
> Add SPI offload support to enable DMA-based, CPU-independent data
> acquisition using the SPI Engine offload framework.
> 
> When an SPI offload is available (devm_spi_offload_get() succeeds),
> the driver registers a DMA engine IIO buffer and uses dedicated buffer
> setup operations. If no offload is available the existing software
> triggered buffer path is used unchanged.
> 
> Both CNV Burst Mode and Manual Mode support offload, but use different
> trigger mechanisms:
> 
> CNV Burst Mode: the SPI Engine is triggered by the ADC's DATA_READY
> signal on the GP pin specified by the trigger-source consumer reference
> in the device tree (one cell = GP pin number 0-3). For this mode the
> driver acts as both an SPI offload consumer (DMA RX stream, message
> optimization) and a trigger source provider: it registers the
> GP/DATA_READY output via devm_spi_offload_trigger_register() so the
> offload framework can match the '#trigger-source-cells' phandle and
> automatically fire the SPI Engine DMA transfer at end-of-conversion.
> 
> Manual Mode: the SPI Engine is triggered by a periodic trigger at
> the configured sampling frequency. The pre-built SPI message uses
> the pipelined CNV-on-CS protocol: N+1 4-byte transfers are issued
> for N active channels (the first result is discarded as garbage from
> the pipeline flush) and the remaining N results are captured by DMA.
> 
> All offload transfers use 32-bit frames (bits_per_word=32, len=4) for
> DMA word alignment. This patch promotes the channel scan_type from
> storagebits=16 (triggered-buffer path) to storagebits=32 to match the
> DMA word size; the triggered-buffer paths are updated to the same layout
> for consistency. CNV Burst Mode channel data arrives in the lower 16
> bits of the 32-bit word (shift=0); Manual Mode data arrives in the upper
> 16 bits (shift=16), matching the 4-byte SPI transfer layout
> [data_hi, data_lo, 0, 0]. A separate ad4691_manual_channels[] array
> encodes the shift=16 scan type for manual mode.
> 
> Add driver documentation under Documentation/iio/ad4691.rst covering
> operating modes, oversampling, reference voltage, SPI offload paths,
> and buffer data layout; register in MAINTAINERS and index.rst
> 
> Kconfig gains a dependency on IIO_BUFFER_DMAENGINE.
> 
> Signed-off-by: Radu Sabau <radu.sabau@analog.com>
> ---
>  Documentation/iio/ad4691.rst | 259 ++++++++++++++++++++++++++
>  Documentation/iio/index.rst  |   1 +
>  MAINTAINERS                  |   1 +
>  drivers/iio/adc/Kconfig      |   1 +
>  drivers/iio/adc/ad4691.c     | 422 ++++++++++++++++++++++++++++++++++++++++++-
>  5 files changed, 676 insertions(+), 8 deletions(-)
> 

...

>  ANALOG DEVICES INC AD4695 DRIVER
> diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
> index d498f16c0816..93f090e9a562 100644
> --- a/drivers/iio/adc/Kconfig
> +++ b/drivers/iio/adc/Kconfig
> @@ -144,6 +144,7 @@ config AD4691
>  	depends on SPI
>  	select IIO_BUFFER
>  	select IIO_TRIGGERED_BUFFER
> +	select IIO_BUFFER_DMAENGINE
>  	select REGMAP
>  	help
>  	  Say yes here to build support for Analog Devices AD4691 Family MuxSAR
> diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
> index f2a7273e43b9..cc2138e47feb 100644
> --- a/drivers/iio/adc/ad4691.c
> +++ b/drivers/iio/adc/ad4691.c
> @@ -11,6 +11,7 @@
>  #include <linux/delay.h>
>  #include <linux/dev_printk.h>
>  #include <linux/device/devres.h>
> +#include <linux/dmaengine.h>
>  #include <linux/err.h>
>  #include <linux/interrupt.h>
>  #include <linux/math.h>
> @@ -22,10 +23,14 @@
>  #include <linux/regulator/consumer.h>
>  #include <linux/reset.h>
>  #include <linux/spi/spi.h>
> +#include <linux/spi/offload/consumer.h>
> +#include <linux/spi/offload/provider.h>
>  #include <linux/units.h>
>  #include <linux/unaligned.h>
>  
>  #include <linux/iio/buffer.h>
> +#include <linux/iio/buffer-dma.h>
> +#include <linux/iio/buffer-dmaengine.h>
>  #include <linux/iio/iio.h>
>  #include <linux/iio/sysfs.h>
>  #include <linux/iio/trigger.h>
> @@ -40,6 +45,7 @@
>  #define AD4691_VREF_4P096_uV_MAX		4500000
>  
>  #define AD4691_CNV_DUTY_CYCLE_NS		380
> +#define AD4691_CNV_HIGH_TIME_NS			430
>  
>  #define AD4691_SPI_CONFIG_A_REG			0x000
>  #define AD4691_SW_RESET				(BIT(7) | BIT(0))
> @@ -92,6 +98,8 @@
>  #define AD4691_ACC_IN(n)			(0x252 + (3 * (n)))
>  #define AD4691_ACC_STS_DATA(n)			(0x283 + (4 * (n)))
>  
> +#define AD4691_OFFLOAD_BITS_PER_WORD		32
> +
>  static const char * const ad4691_supplies[] = { "avdd", "vio" };
>  
>  enum ad4691_ref_ctrl {
> @@ -109,6 +117,31 @@ struct ad4691_chip_info {
>  	unsigned int max_rate;
>  };
>  
> +enum {
> +	AD4691_SCAN_TYPE_NORMAL,         /* triggered buffer:  storagebits=16, shift=0  */
> +	AD4691_SCAN_TYPE_OFFLOAD_CNV,    /* CNV burst offload: storagebits=32, shift=0  */
> +	AD4691_SCAN_TYPE_OFFLOAD_MANUAL, /* manual offload:    storagebits=32, shift=16 */
> +};
> +
> +static const struct iio_scan_type ad4691_scan_types[] = {
> +	[AD4691_SCAN_TYPE_NORMAL] = {
> +		.sign = 'u',
> +		.realbits = 16,
> +		.storagebits = 16,
> +	},
> +	[AD4691_SCAN_TYPE_OFFLOAD_CNV] = {
> +		.sign = 'u',
> +		.realbits = 16,
> +		.storagebits = 32,
> +	},
> +	[AD4691_SCAN_TYPE_OFFLOAD_MANUAL] = {
> +		.sign = 'u',
> +		.realbits = 16,
> +		.storagebits = 32,
> +		.shift = 16,
> +	},
> +};
> +
>  #define AD4691_CHANNEL(ch)						\
>  	{								\
>  		.type = IIO_VOLTAGE,					\
> @@ -122,11 +155,9 @@ struct ad4691_chip_info {
>  		.info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE),	\
>  		.channel = ch,						\
>  		.scan_index = ch,					\
> -		.scan_type = {						\
> -			.sign = 'u',					\
> -			.realbits = 16,					\
> -			.storagebits = 16,				\
> -		},							\
> +		.has_ext_scan_type = 1,					\
> +		.ext_scan_type = ad4691_scan_types,			\
> +		.num_ext_scan_type = ARRAY_SIZE(ad4691_scan_types),	\

Usually, we just make two separte ad4691_chip_info structs for offload
vs. not offload.

ext_scan_type is generally only used when the scan type can change
dynamically after probe.

>  	}
>  
>  static const struct iio_chan_spec ad4691_channels[] = {
> @@ -221,6 +252,17 @@ static const struct ad4691_chip_info ad4694_chip_info = {
>  	.max_rate = 1 * HZ_PER_MHZ,
>  };
>  
> +struct ad4691_offload_state {
> +	struct spi_offload *spi;
> +	struct spi_offload_trigger *trigger;
> +	u64 trigger_hz;
> +	struct spi_message msg;
> +	/* Max 16 channel xfers + 1 state-reset or NOOP */
> +	struct spi_transfer xfer[17];
> +	u8 tx_cmd[17][4];
> +	u8 tx_reset[4];
> +};
> +
>  struct ad4691_state {
>  	const struct ad4691_chip_info *info;
>  	struct regmap *regmap;
> @@ -251,6 +293,8 @@ struct ad4691_state {
>  	struct spi_transfer *scan_xfers;
>  	__be16 *scan_tx;
>  	__be16 *scan_rx;
> +	/* NULL when no SPI offload hardware is present */
> +	struct ad4691_offload_state *offload;
>  	/* Scan buffer: one slot per channel plus timestamp */
>  	struct {
>  		u16 vals[16];
> @@ -273,6 +317,46 @@ static int ad4691_gpio_setup(struct ad4691_state *st, unsigned int gp_num)
>  				  AD4691_GP_MODE_DATA_READY << shift);
>  }
>  
> +static const struct spi_offload_config ad4691_offload_config = {
> +	.capability_flags = SPI_OFFLOAD_CAP_TRIGGER |
> +			    SPI_OFFLOAD_CAP_RX_STREAM_DMA,
> +};
> +
> +static bool ad4691_offload_trigger_match(struct spi_offload_trigger *trigger,
> +					 enum spi_offload_trigger_type type,
> +					 u64 *args, u32 nargs)
> +{
> +	return type == SPI_OFFLOAD_TRIGGER_DATA_READY &&
> +	       nargs == 1 && args[0] <= 3;
> +}
> +
> +static int ad4691_offload_trigger_request(struct spi_offload_trigger *trigger,
> +					  enum spi_offload_trigger_type type,
> +					  u64 *args, u32 nargs)
> +{
> +	struct ad4691_state *st = spi_offload_trigger_get_priv(trigger);
> +
> +	if (nargs != 1)
> +		return -EINVAL;
> +
> +	return ad4691_gpio_setup(st, (unsigned int)args[0]);

Should be fine to leave out the cast here.

> +}
> +
> +static int ad4691_offload_trigger_validate(struct spi_offload_trigger *trigger,
> +					   struct spi_offload_trigger_config *config)
> +{
> +	if (config->type != SPI_OFFLOAD_TRIGGER_DATA_READY)
> +		return -EINVAL;
> +
> +	return 0;
> +}
> +
> +static const struct spi_offload_trigger_ops ad4691_offload_trigger_ops = {
> +	.match    = ad4691_offload_trigger_match,
> +	.request  = ad4691_offload_trigger_request,
> +	.validate = ad4691_offload_trigger_validate,
> +};
> +
>  static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
>  {
>  	struct spi_device *spi = context;
> @@ -553,10 +637,17 @@ static int ad4691_read_raw(struct iio_dev *indio_dev,
>  	case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
>  		*val = st->osr[chan->scan_index];
>  		return IIO_VAL_INT;
> -	case IIO_CHAN_INFO_SCALE:
> +	case IIO_CHAN_INFO_SCALE: {
> +		const struct iio_scan_type *scan_type;
> +
> +		scan_type = iio_get_current_scan_type(indio_dev, chan);
> +		if (IS_ERR(scan_type))
> +			return PTR_ERR(scan_type);
> +
>  		*val = st->vref_uV / (MICRO / MILLI);
> -		*val2 = chan->scan_type.realbits;
> +		*val2 = scan_type->realbits;
>  		return IIO_VAL_FRACTIONAL_LOG2;
> +	}
>  	default:
>  		return -EINVAL;
>  	}
> @@ -856,6 +947,213 @@ static const struct iio_buffer_setup_ops ad4691_cnv_burst_buffer_setup_ops = {
>  	.postdisable = &ad4691_cnv_burst_buffer_postdisable,
>  };
>  
> +static int ad4691_manual_offload_buffer_postenable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	struct ad4691_offload_state *offload = st->offload;
> +	struct device *dev = regmap_get_device(st->regmap);
> +	struct spi_device *spi = to_spi_device(dev);
> +	struct spi_offload_trigger_config config = {
> +		.type = SPI_OFFLOAD_TRIGGER_PERIODIC,
> +	};
> +	unsigned int bit, k;
> +	int ret;
> +
> +	ret = ad4691_enter_conversion_mode(st);
> +	if (ret)
> +		return ret;
> +
> +	memset(offload->xfer, 0, sizeof(offload->xfer));
> +
> +	/*
> +	 * N+1 transfers for N channels. Each CS-low period triggers
> +	 * a conversion AND returns the previous result (pipelined).
> +	 *   TX: [AD4691_ADC_CHAN(n), 0x00, 0x00, 0x00]
> +	 *   RX: [data_hi, data_lo, 0x00, 0x00]   (shift=16)
> +	 * Transfer 0 RX is garbage; transfers 1..N carry real data.
> +	 */
> +	k = 0;
> +	iio_for_each_active_channel(indio_dev, bit) {
> +		offload->tx_cmd[k][0] = AD4691_ADC_CHAN(bit);
> +		offload->xfer[k].tx_buf = offload->tx_cmd[k];
> +		offload->xfer[k].len = sizeof(offload->tx_cmd[k]);
> +		offload->xfer[k].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
> +		offload->xfer[k].cs_change = 1;
> +		offload->xfer[k].cs_change_delay.value = AD4691_CNV_HIGH_TIME_NS;
> +		offload->xfer[k].cs_change_delay.unit = SPI_DELAY_UNIT_NSECS;
> +		/* First transfer RX is garbage — skip it. */
> +		if (k > 0)
> +			offload->xfer[k].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
> +		k++;
> +	}
> +
> +	/* Final NOOP to flush pipeline and capture last channel. */
> +	offload->tx_cmd[k][0] = AD4691_NOOP;
> +	offload->xfer[k].tx_buf = offload->tx_cmd[k];
> +	offload->xfer[k].len = sizeof(offload->tx_cmd[k]);
> +	offload->xfer[k].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
> +	offload->xfer[k].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
> +	k++;
> +
> +	spi_message_init_with_transfers(&offload->msg, offload->xfer, k);
> +	offload->msg.offload = offload->spi;
> +
> +	ret = spi_optimize_message(spi, &offload->msg);
> +	if (ret)
> +		goto err_exit_conversion;
> +
> +	config.periodic.frequency_hz = offload->trigger_hz;
> +	ret = spi_offload_trigger_enable(offload->spi, offload->trigger, &config);
> +	if (ret)
> +		goto err_unoptimize;
> +
> +	return 0;
> +
> +err_unoptimize:
> +	spi_unoptimize_message(&offload->msg);
> +err_exit_conversion:
> +	ad4691_exit_conversion_mode(st);
> +	return ret;
> +}
> +
> +static int ad4691_manual_offload_buffer_predisable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	struct ad4691_offload_state *offload = st->offload;
> +
> +	spi_offload_trigger_disable(offload->spi, offload->trigger);
> +	spi_unoptimize_message(&offload->msg);
> +
> +	return ad4691_exit_conversion_mode(st);
> +}
> +
> +static const struct iio_buffer_setup_ops ad4691_manual_offload_buffer_setup_ops = {
> +	.postenable = &ad4691_manual_offload_buffer_postenable,
> +	.predisable = &ad4691_manual_offload_buffer_predisable,
> +};
> +
> +static int ad4691_cnv_burst_offload_buffer_postenable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	struct ad4691_offload_state *offload = st->offload;
> +	struct device *dev = regmap_get_device(st->regmap);
> +	struct spi_device *spi = to_spi_device(dev);
> +	struct spi_offload_trigger_config config = {
> +		.type = SPI_OFFLOAD_TRIGGER_DATA_READY,
> +	};
> +	unsigned int n_active = bitmap_weight(indio_dev->active_scan_mask,
> +					      iio_get_masklength(indio_dev));
> +	unsigned int bit, k;
> +	int ret;
> +
> +	ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
> +			   bitmap_read(indio_dev->active_scan_mask, 0,
> +				       iio_get_masklength(indio_dev)));
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG,
> +			   ~bitmap_read(indio_dev->active_scan_mask, 0,
> +				iio_get_masklength(indio_dev)) & GENMASK(15, 0));
> +	if (ret)
> +		return ret;
> +
> +	iio_for_each_active_channel(indio_dev, bit) {
> +		ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(bit),
> +				   st->osr[bit]);
> +		if (ret)
> +			return ret;
> +	}
> +
> +	ret = ad4691_enter_conversion_mode(st);
> +	if (ret)
> +		return ret;
> +
> +	memset(offload->xfer, 0, sizeof(offload->xfer));
> +
> +	/*
> +	 * N transfers to read N AVG_IN registers plus one state-reset
> +	 * transfer (no RX) to re-arm DATA_READY.
> +	 *   TX: [reg_hi | 0x80, reg_lo, 0x00, 0x00]
> +	 *   RX: [0x00, 0x00, data_hi, data_lo]   (shift=0)
> +	 */
> +	k = 0;
> +	iio_for_each_active_channel(indio_dev, bit) {
> +		unsigned int reg = AD4691_AVG_IN(bit);
> +
> +		offload->tx_cmd[k][0] = (reg >> 8) | 0x80;
> +		offload->tx_cmd[k][1] = reg & 0xFF;

Can we use put_unaligned_be16()?

> +		offload->xfer[k].tx_buf = offload->tx_cmd[k];
> +		offload->xfer[k].len = sizeof(offload->tx_cmd[k]);
> +		offload->xfer[k].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
> +		offload->xfer[k].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
> +		if (k < n_active - 1)
> +			offload->xfer[k].cs_change = 1;
> +		k++;
> +	}
> +
> +	/* State reset to re-arm DATA_READY for the next scan. */
> +	offload->tx_reset[0] = AD4691_STATE_RESET_REG >> 8;
> +	offload->tx_reset[1] = AD4691_STATE_RESET_REG & 0xFF;

ditto.

> +	offload->tx_reset[2] = AD4691_STATE_RESET_ALL;
> +	offload->xfer[k].tx_buf = offload->tx_reset;
> +	offload->xfer[k].len = sizeof(offload->tx_reset);
> +	offload->xfer[k].bits_per_word = AD4691_OFFLOAD_BITS_PER_WORD;
> +	k++;
> +
> +	spi_message_init_with_transfers(&offload->msg, offload->xfer, k);
> +	offload->msg.offload = offload->spi;
> +
> +	ret = spi_optimize_message(spi, &offload->msg);
> +	if (ret)
> +		goto err_exit_conversion;
> +
> +	ret = ad4691_sampling_enable(st, true);
> +	if (ret)
> +		goto err_unoptimize;
> +
> +	ret = spi_offload_trigger_enable(offload->spi, offload->trigger, &config);
> +	if (ret)
> +		goto err_sampling_disable;
> +
> +	return 0;
> +
> +err_sampling_disable:
> +	ad4691_sampling_enable(st, false);
> +err_unoptimize:
> +	spi_unoptimize_message(&offload->msg);
> +err_exit_conversion:
> +	ad4691_exit_conversion_mode(st);
> +	return ret;
> +}
> +
> +static int ad4691_cnv_burst_offload_buffer_predisable(struct iio_dev *indio_dev)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +	struct ad4691_offload_state *offload = st->offload;
> +	int ret;
> +
> +	spi_offload_trigger_disable(offload->spi, offload->trigger);
> +
> +	ret = ad4691_sampling_enable(st, false);
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
> +			   AD4691_SEQ_ALL_CHANNELS_OFF);
> +	if (ret)
> +		return ret;
> +
> +	spi_unoptimize_message(&offload->msg);
> +
> +	return ad4691_exit_conversion_mode(st);
> +}
> +
> +static const struct iio_buffer_setup_ops ad4691_cnv_burst_offload_buffer_setup_ops = {
> +	.postenable = &ad4691_cnv_burst_offload_buffer_postenable,
> +	.predisable = &ad4691_cnv_burst_offload_buffer_predisable,
> +};
> +
>  static ssize_t sampling_frequency_show(struct device *dev,
>  				       struct device_attribute *attr,
>  				       char *buf)
> @@ -863,6 +1161,9 @@ static ssize_t sampling_frequency_show(struct device *dev,
>  	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
>  	struct ad4691_state *st = iio_priv(indio_dev);
>  
> +	if (st->manual_mode && st->offload)
> +		return sysfs_emit(buf, "%llu\n", st->offload->trigger_hz);
> +
>  	return sysfs_emit(buf, "%u\n", (u32)(NSEC_PER_SEC / st->cnv_period_ns));
>  }
>  
> @@ -883,6 +1184,20 @@ static ssize_t sampling_frequency_store(struct device *dev,
>  	if (iio_buffer_enabled(indio_dev))
>  		return -EBUSY;
>  
> +	if (st->manual_mode && st->offload) {
> +		struct spi_offload_trigger_config config = {
> +			.type = SPI_OFFLOAD_TRIGGER_PERIODIC,
> +			.periodic = { .frequency_hz = freq },
> +		};

Same comment as other patches. This needs to account for oversampling ratio.

> +
> +		ret = spi_offload_trigger_validate(st->offload->trigger, &config);
> +		if (ret)
> +			return ret;
> +
> +		st->offload->trigger_hz = config.periodic.frequency_hz;
> +		return len;
> +	}
> +
>  	ret = ad4691_set_pwm_freq(st, freq);
>  	if (ret)
>  		return ret;
> @@ -968,10 +1283,23 @@ static irqreturn_t ad4691_trigger_handler(int irq, void *p)
>  	return IRQ_HANDLED;
>  }
>  
> +static int ad4691_get_current_scan_type(const struct iio_dev *indio_dev,
> +					 const struct iio_chan_spec *chan)
> +{
> +	struct ad4691_state *st = iio_priv(indio_dev);
> +
> +	if (!st->offload)
> +		return AD4691_SCAN_TYPE_NORMAL;
> +	if (st->manual_mode)
> +		return AD4691_SCAN_TYPE_OFFLOAD_MANUAL;
> +	return AD4691_SCAN_TYPE_OFFLOAD_CNV;
> +}
> +
>  static const struct iio_info ad4691_info = {
>  	.read_raw = &ad4691_read_raw,
>  	.write_raw = &ad4691_write_raw,
>  	.read_avail = &ad4691_read_avail,
> +	.get_current_scan_type = &ad4691_get_current_scan_type,
>  	.debugfs_reg_access = &ad4691_reg_access,
>  };
>  
> @@ -1195,9 +1523,75 @@ static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
>  					       &ad4691_manual_buffer_setup_ops);
>  }
>  
> +static int ad4691_setup_offload(struct iio_dev *indio_dev,
> +				struct ad4691_state *st,
> +				struct spi_offload *spi_offload)
> +{
> +	struct device *dev = regmap_get_device(st->regmap);
> +	struct ad4691_offload_state *offload;
> +	struct dma_chan *rx_dma;
> +	int ret;
> +
> +	offload = devm_kzalloc(dev, sizeof(*offload), GFP_KERNEL);
> +	if (!offload)
> +		return -ENOMEM;
> +
> +	offload->spi = spi_offload;
> +	st->offload = offload;
> +
> +	if (st->manual_mode) {
> +		offload->trigger =
> +			devm_spi_offload_trigger_get(dev, offload->spi,
> +						     SPI_OFFLOAD_TRIGGER_PERIODIC);
> +		if (IS_ERR(offload->trigger))
> +			return dev_err_probe(dev, PTR_ERR(offload->trigger),
> +					     "Failed to get periodic offload trigger\n");
> +
> +		offload->trigger_hz = st->info->max_rate;

I think I mentioned this elsewhere, but can we really get max_rate in manual mode
due to the extra SPI overhead? Probably safer to start with a lower rate.

> +	} else {
> +		struct spi_offload_trigger_info trigger_info = {
> +			.fwnode = dev_fwnode(dev),
> +			.ops    = &ad4691_offload_trigger_ops,
> +			.priv   = st,
> +		};
> +
> +		ret = devm_spi_offload_trigger_register(dev, &trigger_info);
> +		if (ret)
> +			return dev_err_probe(dev, ret,
> +					     "Failed to register offload trigger\n");
> +
> +		offload->trigger =
> +			devm_spi_offload_trigger_get(dev, offload->spi,
> +						     SPI_OFFLOAD_TRIGGER_DATA_READY);
> +		if (IS_ERR(offload->trigger))
> +			return dev_err_probe(dev, PTR_ERR(offload->trigger),
> +					     "Failed to get DATA_READY offload trigger\n");
> +	}
> +
> +	rx_dma = devm_spi_offload_rx_stream_request_dma_chan(dev, offload->spi);
> +	if (IS_ERR(rx_dma))
> +		return dev_err_probe(dev, PTR_ERR(rx_dma),
> +				     "Failed to get offload RX DMA channel\n");
> +
> +	if (st->manual_mode)
> +		indio_dev->setup_ops = &ad4691_manual_offload_buffer_setup_ops;
> +	else
> +		indio_dev->setup_ops = &ad4691_cnv_burst_offload_buffer_setup_ops;
> +
> +	ret = devm_iio_dmaengine_buffer_setup_with_handle(dev, indio_dev, rx_dma,
> +							  IIO_BUFFER_DIRECTION_IN);
> +	if (ret)
> +		return ret;
> +
> +	indio_dev->buffer->attrs = ad4691_buffer_attrs;

Should including ad4691_buffer_attrs depend on st->manual_mode?

I thought it was only used when PWM is connected to CNV.

> +
> +	return 0;
> +}
> +
>  static int ad4691_probe(struct spi_device *spi)
>  {
>  	struct device *dev = &spi->dev;
> +	struct spi_offload *spi_offload;
>  	struct iio_dev *indio_dev;
>  	struct ad4691_state *st;
>  	int ret;
> @@ -1232,6 +1626,13 @@ static int ad4691_probe(struct spi_device *spi)
>  	if (ret)
>  		return ret;
>  
> +	spi_offload = devm_spi_offload_get(dev, spi, &ad4691_offload_config);
> +	ret = PTR_ERR_OR_ZERO(spi_offload);
> +	if (ret == -ENODEV)
> +		spi_offload = NULL;
> +	else if (ret)
> +		return dev_err_probe(dev, ret, "Failed to get SPI offload\n");
> +
>  	indio_dev->name = st->info->name;
>  	indio_dev->info = &ad4691_info;
>  	indio_dev->modes = INDIO_DIRECT_MODE;
> @@ -1239,7 +1640,10 @@ static int ad4691_probe(struct spi_device *spi)
>  	indio_dev->channels = st->info->channels;
>  	indio_dev->num_channels = st->info->num_channels;

As mentioned earlier, we generally want separate channel structs
for SPI offload. These will also have different num_channels because
there is no timestamp channel in SPI offload.

>  
> -	ret = ad4691_setup_triggered_buffer(indio_dev, st);
> +	if (spi_offload)
> +		ret = ad4691_setup_offload(indio_dev, st, spi_offload);
> +	else
> +		ret = ad4691_setup_triggered_buffer(indio_dev, st);
>  	if (ret)
>  		return ret;
>  
> @@ -1277,3 +1681,5 @@ module_spi_driver(ad4691_driver);
>  MODULE_AUTHOR("Radu Sabau <radu.sabau@analog.com>");
>  MODULE_DESCRIPTION("Analog Devices AD4691 Family ADC Driver");
>  MODULE_LICENSE("GPL");
> +MODULE_IMPORT_NS("IIO_DMA_BUFFER");
> +MODULE_IMPORT_NS("IIO_DMAENGINE_BUFFER");
> 


^ permalink raw reply

* [RFC PATCH 0/9] mm/damon: introduce DAMOS failed region quota charge ratio
From: SeongJae Park @ 2026-04-04 16:39 UTC (permalink / raw)
  Cc: SeongJae Park, Liam R. Howlett, Andrew Morton, Brendan Higgins,
	David Gow, David Hildenbrand, Jonathan Corbet, Lorenzo Stoakes,
	Michal Hocko, Mike Rapoport, Shuah Khan, Shuah Khan,
	Suren Baghdasaryan, Vlastimil Babka, damon, kunit-dev, linux-doc,
	linux-kernel, linux-kselftest, linux-mm

TL; DR: Let users set different DAMOS quota charge ratios for DAMOS
action failed regions, for deterministic and consistent DAMOS action
progress.

Common Reports: Unexpectedly Slow DAMOS
=======================================

One common issue report that we get from DAMON users is that DAMOS
action applying progress speed is sometimes much slower than expected.
And one common root cause is that the DAMOS quota is exceeded by the
action applying failed memory regions.

For example, a group of users tried to run DAMOS-based proactive memory
reclamation (DAMON_RECLAIM) with 100 MiB per second DAMOS quota.  They
ran it on a system having no active workload which means all memory of
the system is cold.  The expectation was that the system will show 100
MiB per second reclamation until (nearly) all memory is reclaimed. But
what they found is that the speed is quite inconsistent and sometimes it
becomes very slower than the expectation, sometimes even no reclamation
at all for about tens of seconds.  The upper limit of the speed (100 MiB
per second) was being kept as expected, though.

By monitoring the qt_exceeds (number of DAMOS quota exceed events) DAMOS
stat, we found DAMOS quota is always exceeded when the speed is slow. By
monitoring sz_tried and sz_applied (the total amount of DAMOS action
tried memory and succeeded memory) DAMOS stats together, we found the
reclamation attempts nearly always failed when the speed is slow.

DAMOS quota charges DAMOS action tried regions regardless of the
successfulness of the try.  Hence in the example reported case, there
was unreclaimable memory spread around the system memory.  Sometimes
nearly 100 MiB of memory that DAMOS tried to reclaim in the given quota
interval was reclaimable, and therefore showed nearly 100 MiB per second
speed.  Sometimes nearly 99 MiB of memory that DAMOS was trying to
reclaim in the given quota interval was unreclaimable, and therefore
showing only about 1 MiB per second reclaim speed.

We explained it is an expected behavior of the feature rather than a
bug, as DAMOS quota is there for only the upper-limit of the speed.  The
users agreed and later reported a huge win from the adoption of
DAMON_RECLAIM on their products.

It is Not a Bug but a Feature; But...
=====================================

So nothing is broken.  DAMOS quota is working as intended, as the upper
limit of the speed.  It also provides its behavior observability via
DAMOS stat.  In the real world production environment that runs long
term active workloads and matters stability, the speed sometimes being
slow is not a real problem.

But, the non-deterministic behavior is sometimes annoying, especially in
lab environments.  Even in a realistic production environment, when
there is a huge amount of DAMOS action unapplicable memory, the speed
could be problematically slow.  Let's suppose a virtual machines
provider that setup 99% of the host memory as hugetlb pages that cannot
be reclaimed, to give it to virtual machines.  Also, when aim-oriented
DAMOS auto-tuning is applied, this could also make the internal feedback
loop confused.

The intention of the current behavior was that trying DAMOS action to
regions would anyway impose some overhead, and therefore somehow be
charged.  But in the real world, the overhead for failed action is much
lighter than successful action.  Charging those at the same ratio may be
unfair, or at least suboptimum in some environments.

DAMOS Action Failed Region Quota Charge Ratio
=============================================

Let users set the charge ratio for the action-failed memory, for more
optimal and deterministic use of DAMOS.  It allows users to specify the
numerator and the denominator of the ratio for flexible setup.  For
example, let's suppose the numerator and the denominator are set to 1
and 4,096, respectively.  The ratio is 1 / 4,096.  A DAMOS scheme action
is applied to 5 GiB memory.  For 1 GiB of the memory, the action is
succeeded.  For the rest (4 GiB), the action is failed.  Then, only 1
GiB and 1 MiB quota is charged.

The optimal charge ratio will depend on the use case and
system/workload.  I'd recommend starting from setting the nominator as 1
and the denominator as PAGE_SIZE and tune based on the results, because
many DAMOS actions are applied at page level.

Tests
=====

I tested this feature in the steps below.

1. Allocate 50% of system memory and mlock() it using a test program.
2. Fill up the page cache to exhaust nearly all free memory.
3. Start DAMON-based proactive reclamation with 100 MiB/second DAMOS
   hard-quota.  Auto-tune the DAMOS soft-quota under the hard-quota for
   achieving 40% free memory of the system with 'temporal' tuner.

For step 1, I run a simple C program that is written by Gemini.  It is
quite straightforward, so I'm not sharing the code here.

For step 2, I use dd command like below:

   dd if=/dev/zero of=foo bs=1M count=$50_percent_of_system_memory

For step 3, I use the latest version of DAMON user-space tool (damo)
like below.

    sudo damo start --damos_action pageout \
            ` # Do the pageout only up to 100 MiB per second ` \
            --damos_quota_space 100M --damos_quota_interval 1s \
            ` # Auto-tune the quota below the hard quota aiming` \
            ` # 40% free memory of the node 0 ` \
            ` # (entire node of the test system)` \
            --damos_quota_goal node_mem_free_bp 40% 0 \
            ` # use temporal tuner, which is easy to understnd ` \
            --damos_quota_goal_tuner temporal

As expected, the progress of the reclamation is not consistent, because
the quota is exceeded for the failed reclamation of the unreclaimable
memory.

I do this again, but with the failed region charge ratio feature.  For
this, the above 'damo' command is used, after appending command line
option for setup of the charge ratio like below.  Note that the option
was added to 'damo' after v3.1.9.

    sudo ./damo start --damos_action pageout \
            [...]
            ` # quota-charge only 1/4096 for pageout-failed regions ` \
            --damos_quota_fail_charge_ratio 1 4096

The progress of the reclamation was nearly 100 MiB per second until the
goal was achieved, meeting the expectation.

Patches Sequence
================

Patch 1 implements the feature and exposes it via DAMON core API.
Patch 2 implements DAMON sysfs ABI for the feature.  Three following
patches (3-5) document the feature and ABI on design, usage, and ABI
documents, respectively.  Four patches for testing of the new feature
follow.  Patch 6 implements a kunit test for the feature.  Patches 7
and 8 extend DAMON selftest helpers for DAMON sysfs control and internal
state dumping for adding a new selftest for the feature.  Patch 9
extends existing DAMON sysfs interface selftest to test the new feature
using the extended helper scripts.

SeongJae Park (9):
  mm/damon/core: introduce failed region quota charge ratio
  mm/damon/sysfs-schemes: implement fail_charge_{num,denom} files
  Docs/mm/damon/design: document fail_charge_{num,denom}
  Docs/admin-guide/mm/damon/usage: document fail_charge_{num,denom}
    files
  Docs/ABI/damon: document fail_charge_{num,denom}
  mm/damon/tests/core-kunit: test fail_charge_{num,denom} committing
  selftets/damon/_damon_sysfs: support failed region quota charge ratio
  selftests/damon/drgn_dump_damon_status: support failed region quota
    charge ratio
  selftets/damon/sysfs.py: test failed region quota charge ratio

 .../ABI/testing/sysfs-kernel-mm-damon         | 12 +++++
 Documentation/admin-guide/mm/damon/usage.rst  | 18 +++++--
 Documentation/mm/damon/design.rst             | 21 ++++++++
 include/linux/damon.h                         |  9 ++++
 mm/damon/core.c                               |  9 +++-
 mm/damon/sysfs-schemes.c                      | 54 +++++++++++++++++++
 mm/damon/tests/core-kunit.h                   |  6 +++
 tools/testing/selftests/damon/_damon_sysfs.py | 21 +++++++-
 .../selftests/damon/drgn_dump_damon_status.py |  2 +
 tools/testing/selftests/damon/sysfs.py        |  6 +++
 10 files changed, 151 insertions(+), 7 deletions(-)


base-commit: 9e634d6813be2e3d1cb023a0b83619fd2bcdd13b
-- 
2.47.3

^ permalink raw reply

* [RFC PATCH 3/9] Docs/mm/damon/design: document fail_charge_{num,denom}
From: SeongJae Park @ 2026-04-04 16:39 UTC (permalink / raw)
  Cc: SeongJae Park, Liam R. Howlett, Andrew Morton, David Hildenbrand,
	Jonathan Corbet, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
	Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, damon, linux-doc,
	linux-kernel, linux-mm
In-Reply-To: <20260404163943.89278-1-sj@kernel.org>

Update DAMON design document for the DAMOS action failed region quota
charge ratio.

Signed-off-by: SeongJae Park <sj@kernel.org>
---
 Documentation/mm/damon/design.rst | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/Documentation/mm/damon/design.rst b/Documentation/mm/damon/design.rst
index 510ec6375178d..58a72bd26dc11 100644
--- a/Documentation/mm/damon/design.rst
+++ b/Documentation/mm/damon/design.rst
@@ -572,6 +572,27 @@ interface <sysfs_interface>`, refer to :ref:`weights <sysfs_quotas>` part of
 the documentation.
 
 
+.. _damon_design_damos_quotas_failed_memory_charging_ratio:
+
+Action-failed Memory Charging Ratio
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+DAMOS action to a given region can fail for sub subsets of the memory of the
+region.  For example, if the action is ``pageout`` and the region has some
+unreclaimable pages, applying the action to the pages will fail.  The amount of
+system resource that is taken for such failed action applications is usually
+different from that for successful action applications.  For such cases, users
+can set different charging ratio for such failed memory.  The ratio can be
+specified using ``fail_charge_num`` and ``fail_charge_denom`` parameters.  The
+two parameters represent the numerator and denominator of the ratio.
+
+For example, let's suppose a DAMOS action is applied to a region of 1 GiB size.
+The action is successfully applied to only 700 MiB of the region.
+``fail_charge_num`` and ``fail_charge_denom`` are set to ``1`` and ``1024``,
+respectively.  Then only 700 MiB and 300 KiB of size (``700 MiB + 300 MiB * 1 /
+1024``) will be charged.
+
+
 .. _damon_design_damos_quotas_auto_tuning:
 
 Aim-oriented Feedback-driven Auto-tuning
-- 
2.47.3

^ permalink raw reply related

* [RFC PATCH 4/9] Docs/admin-guide/mm/damon/usage: document fail_charge_{num,denom} files
From: SeongJae Park @ 2026-04-04 16:39 UTC (permalink / raw)
  Cc: SeongJae Park, Liam R. Howlett, Andrew Morton, David Hildenbrand,
	Jonathan Corbet, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
	Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, damon, linux-doc,
	linux-kernel, linux-mm
In-Reply-To: <20260404163943.89278-1-sj@kernel.org>

Update DAMON usage document for the DAMOS action failed regions quota
charge ratio control sysfs files.

Signed-off-by: SeongJae Park <sj@kernel.org>
---
 Documentation/admin-guide/mm/damon/usage.rst | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/Documentation/admin-guide/mm/damon/usage.rst b/Documentation/admin-guide/mm/damon/usage.rst
index bfdb717441f05..ffb32bdbe34ff 100644
--- a/Documentation/admin-guide/mm/damon/usage.rst
+++ b/Documentation/admin-guide/mm/damon/usage.rst
@@ -84,7 +84,9 @@ comma (",").
     │ │ │ │ │ │ │ │ sz/min,max
     │ │ │ │ │ │ │ │ nr_accesses/min,max
     │ │ │ │ │ │ │ │ age/min,max
-    │ │ │ │ │ │ │ :ref:`quotas <sysfs_quotas>`/ms,bytes,reset_interval_ms,effective_bytes,goal_tuner
+    │ │ │ │ │ │ │ :ref:`quotas <sysfs_quotas>`/ms,bytes,reset_interval_ms,
+    │ │ │ │ │ │ │     effective_bytes,goal_tuner,
+    │ │ │ │ │ │ │     fail_charge_num,fail_charge_denom
     │ │ │ │ │ │ │ │ weights/sz_permil,nr_accesses_permil,age_permil
     │ │ │ │ │ │ │ │ :ref:`goals <sysfs_schemes_quota_goals>`/nr_goals
     │ │ │ │ │ │ │ │ │ 0/target_metric,target_value,current_value,nid,path
@@ -381,9 +383,10 @@ schemes/<N>/quotas/
 The directory for the :ref:`quotas <damon_design_damos_quotas>` of the given
 DAMON-based operation scheme.
 
-Under ``quotas`` directory, five files (``ms``, ``bytes``,
-``reset_interval_ms``, ``effective_bytes`` and ``goal_tuner``) and two
-directories (``weights`` and ``goals``) exist.
+Under ``quotas`` directory, seven files (``ms``, ``bytes``,
+``reset_interval_ms``, ``effective_bytes``, ``goal_tuner``, ``fail_charge_num``
+and ``fail_charge_denom``) and two directories (``weights`` and ``goals``)
+exist.
 
 You can set the ``time quota`` in milliseconds, ``size quota`` in bytes, and
 ``reset interval`` in milliseconds by writing the values to the three files,
@@ -402,6 +405,13 @@ the background design of the feature and the name of the selectable algorithms.
 Refer to :ref:`goals directory <sysfs_schemes_quota_goals>` for the goals
 setup.
 
+You can set the action-failed memory quota charging ratio by writing the
+numerator and the denominator for the ratio to ``fail_charge_num`` and
+`fail_charge_denom`` files, respectively.  Reading those file will return the
+current set values.  Refer to :ref:`design
+<damon_design_damos_quotas_failed_memory_charging_ratio>` for more details of
+the ratio feature.
+
 The time quota is internally transformed to a size quota.  Between the
 transformed size quota and user-specified size quota, smaller one is applied.
 Based on the user-specified :ref:`goal <sysfs_schemes_quota_goals>`, the
-- 
2.47.3

^ permalink raw reply related

* Re: [PATCH v5 0/3] PCI Controller event and LTSSM tracepoint support
From: Manivannan Sadhasivam @ 2026-04-04 16:53 UTC (permalink / raw)
  To: Steven Rostedt, Shawn Lin
  Cc: Bjorn Helgaas, linux-rockchip, linux-pci, linux-trace-kernel,
	linux-doc
In-Reply-To: <1774403912-210670-1-git-send-email-shawn.lin@rock-chips.com>

On Wed, Mar 25, 2026 at 09:58:29AM +0800, Shawn Lin wrote:
> 
> This patch-set adds new pci controller event and LTSSM tracepoint used by host drivers
> which provide LTSSM trace functionality. The first user is pcie-dw-rockchip with a 256
> Bytes FIFO for recording LTSSM transition.
> 

Steve, could you please take a look at the tracing part?

- Mani

> Testing
> =========
> 
> This series was tested on RK3588/RK3588s EVB1 with NVMe SSD connected to PCIe3 and PCIe2
> root ports.
> 
> echo 1 > /sys/kernel/debug/tracing/events/pci_controller/pcie_ltssm_state_transition/enable
> cat /sys/kernel/debug/tracing/trace_pipe
> 
>  # tracer: nop
>  #
>  # entries-in-buffer/entries-written: 64/64   #P:8
>  #
>  #                                _-----=> irqs-off/BH-disabled
>  #                               / _----=> need-resched
>  #                              | / _---=> hardirq/softirq
>  #                              || / _--=> preempt-depth
>  #                              ||| / _-=> migrate-disable
>  #                              |||| /     delay
>  #           TASK-PID     CPU#  |||||  TIMESTAMP  FUNCTION
>  #              | |         |   |||||     |         |
>       kworker/0:0-9       [000] .....     5.600194: pcie_ltssm_state_transition: dev: a40000000.pcie state: DETECT_ACT rate: Unknown
>       kworker/0:0-9       [000] .....     5.600198: pcie_ltssm_state_transition: dev: a40000000.pcie state: DETECT_WAIT rate: Unknown
>       kworker/0:0-9       [000] .....     5.600199: pcie_ltssm_state_transition: dev: a40000000.pcie state: DETECT_ACT rate: Unknown
>       kworker/0:0-9       [000] .....     5.600201: pcie_ltssm_state_transition: dev: a40000000.pcie state: POLL_ACTIVE rate: Unknown
>       kworker/0:0-9       [000] .....     5.600202: pcie_ltssm_state_transition: dev: a40000000.pcie state: POLL_CONFIG rate: Unknown
>       kworker/0:0-9       [000] .....     5.600204: pcie_ltssm_state_transition: dev: a40000000.pcie state: CFG_LINKWD_START rate: Unknown
>       kworker/0:0-9       [000] .....     5.600206: pcie_ltssm_state_transition: dev: a40000000.pcie state: CFG_LINKWD_ACEPT rate: Unknown
>       kworker/0:0-9       [000] .....     5.600207: pcie_ltssm_state_transition: dev: a40000000.pcie state: CFG_LANENUM_WAI rate: Unknown
>       kworker/0:0-9       [000] .....     5.600208: pcie_ltssm_state_transition: dev: a40000000.pcie state: CFG_LANENUM_ACEPT rate: Unknown
>       kworker/0:0-9       [000] .....     5.600210: pcie_ltssm_state_transition: dev: a40000000.pcie state: CFG_COMPLETE rate: Unknown
>       kworker/0:0-9       [000] .....     5.600212: pcie_ltssm_state_transition: dev: a40000000.pcie state: CFG_IDLE rate: Unknown
>       kworker/0:0-9       [000] .....     5.600213: pcie_ltssm_state_transition: dev: a40000000.pcie state: L0 rate: 2.5 GT/s
>       kworker/0:0-9       [000] .....     5.600214: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_LOCK rate: Unknown
>       kworker/0:0-9       [000] .....     5.600216: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_RCVRCFG rate: Unknown
>       kworker/0:0-9       [000] .....     5.600217: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_SPEED rate: Unknown
>       kworker/0:0-9       [000] .....     5.600218: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_LOCK rate: Unknown
>       kworker/0:0-9       [000] .....     5.600220: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_EQ1 rate: Unknown
>       kworker/0:0-9       [000] .....     5.600221: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_EQ2 rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600222: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_EQ3 rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600224: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_LOCK rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600225: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_RCVRCFG rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600226: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_IDLE rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600227: pcie_ltssm_state_transition: dev: a40000000.pcie state: L0 rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600228: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_LOCK rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600229: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_RCVRCFG rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600231: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_IDLE rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600232: pcie_ltssm_state_transition: dev: a40000000.pcie state: L0 rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600233: pcie_ltssm_state_transition: dev: a40000000.pcie state: L123_SEND_EIDLE rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600234: pcie_ltssm_state_transition: dev: a40000000.pcie state: L1_IDLE rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600236: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_LOCK rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600237: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_RCVRCFG rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600238: pcie_ltssm_state_transition: dev: a40000000.pcie state: RCVRY_IDLE rate: 8.0 GT/s
>       kworker/0:0-9       [000] .....     5.600239: pcie_ltssm_state_transition: dev: a40000000.pcie state: L0 rate: 8.0 GT/s
> 
> 
> Changes in v5:
> - rebase
> - use EM/EMe instead
> - remove reg/unreg function and back to use TRACE_EVENT
> - use trace_pcie_ltssm_state_transition_enabled()
> 
> Changes in v4:
> - use TRACE_EVENT_FN to notify when to start and stop the tracepoint,
>   and export pci_ltssm_tp_enabled() for host drivers to use
> - skip trace if pci_ltssm_tp_enabled() is false.(Steven)
> - wrap into 80 columns(Bjorn)
> 
> Changes in v3:
> - add TRACE_DEFINE_ENUM for all enums(Steven Rostedt)
> - Add toctree entry in Documentation/trace/index.rst(Bagas Sanjaya)
> - fix mismatch section underline length(Bagas Sanjaya)
> - Make example snippets in code block(Bagas Sanjaya)
> - warp context into 80 columns and fix the file name(Bjorn)
> - reorder variables(Mani)
> - rename loop to i; rename en to enable(Mani)
> - use FIELD_GET(Mani)
> - add comment about how the FIFO works(Mani)
> 
> Changes in v2:
> - use tracepoint
> 
> Shawn Lin (3):
>   PCI: trace: Add PCI controller LTSSM transition tracepoint
>   Documentation: tracing: Add PCI controller event documentation
>   PCI: dw-rockchip: Add pcie_ltssm_state_transition trace support
> 
>  Documentation/trace/events-pci-controller.rst |  42 ++++++++++
>  Documentation/trace/index.rst                 |   1 +
>  drivers/pci/controller/dwc/pcie-dw-rockchip.c | 111 ++++++++++++++++++++++++++
>  drivers/pci/trace.c                           |   1 +
>  include/trace/events/pci_controller.h         |  58 ++++++++++++++
>  5 files changed, 213 insertions(+)
>  create mode 100644 Documentation/trace/events-pci-controller.rst
>  create mode 100644 include/trace/events/pci_controller.h
> 
> -- 
> 2.7.4
> 

-- 
மணிவண்ணன் சதாசிவம்

^ permalink raw reply

* Re: [PATCH v5 0/1] mm/damon: add node_eligible_mem_bp and node_ineligible_mem_bp goal metrics
From: SeongJae Park @ 2026-04-04 19:53 UTC (permalink / raw)
  To: Ravi Jonnalagadda
  Cc: SeongJae Park, damon, linux-mm, linux-kernel, linux-doc, akpm,
	corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun
In-Reply-To: <20260404012215.1539-1-ravis.opensrc@gmail.com>

Hello Ravi,


On Fri,  3 Apr 2026 18:22:14 -0700 Ravi Jonnalagadda <ravis.opensrc@gmail.com> wrote:

> 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

Thank you for revisioning while addressing my comments on the previous version!

> 
> This patch is based on top of damon/next.

Maybe because of this, sashiko was unable to review this.

To my understanding, there is no real reason to make this based on damon/next.
And I'd like to get sashiko review for this patch.  Could you please rebase
this to latest mm-new and repost for that?

> 
> 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.
[...]

Other than the lack of the sashiko review, this cover letter looks good to me.


Thanks,
SJ

[...]

^ permalink raw reply

* Re: [PATCH v5 1/1] mm/damon: add node_eligible_mem_bp and node_ineligible_mem_bp goal metrics
From: SeongJae Park @ 2026-04-04 20:01 UTC (permalink / raw)
  To: Ravi Jonnalagadda
  Cc: SeongJae Park, damon, linux-mm, linux-kernel, linux-doc, akpm,
	corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
	kernel test robot
In-Reply-To: <20260404012215.1539-2-ravis.opensrc@gmail.com>

On Fri,  3 Apr 2026 18:22:15 -0700 Ravi Jonnalagadda <ravis.opensrc@gmail.com> wrote:

> 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/

I think you don't need to add the Reported-by: and Closes: for bug that found
before this patch is merged.  Could you please drop those?

> 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(-)
[...]

The code changes look good to me.  But, as I replied to the cover letter, I'd
like to get sashiko review before giving my Reviewed-by:.  Could you please
rebase this to the latest mm-new and repost for getting the sashiko review?


Thanks,
SJ

^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox