Skip to content

Reusable buffers#10

Closed
jholveck wants to merge 13 commits into
mainfrom
reusable-buffers
Closed

Reusable buffers#10
jholveck wants to merge 13 commits into
mainfrom
reusable-buffers

Conversation

@jholveck

@jholveck jholveck commented Jun 1, 2026

Copy link
Copy Markdown
Owner

Changes proposed in this PR

Fixes #
(...)

  • Tests added/updated
  • Documentation updated
  • Changelog entry added
  • ./check.sh passed

Summary by CodeRabbit

  • New Features

    • Implemented automatic zero-copy buffer reuse improving screenshot performance on Python 3.12+ (GNU/Linux; broader support planned)
    • Optimized shared-memory pooling for more efficient screenshot captures on Linux
  • Documentation

    • Added "Buffer Reuse" section with performance metrics and feature availability details
  • Tests

    • Enhanced test coverage for buffer cleanup and allocation scenarios

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93e1a43e-4ac5-48a3-a948-499309878f5e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces zero-copy reusable buffers on Python 3.12+ GNU/Linux by implementing a finalizing-buffer mechanism that invokes caller-supplied cleanup when buffers are released, refactoring XShmGetImage to pool shared-memory slots instead of using a single long-lived buffer, and updating the backend interface contract to enable buffer-like return types.

Changes

Buffer reuse with zero-copy finalization

Layer / File(s) Summary
Type interface contract update
src/mss/base.py
MSSImplementation.grab() return type changes from bytearray to Buffer, enabling backends to return custom buffer-like types with lifecycle hooks.
Finalizing buffer mechanism
src/mss/buffer.py
Implements finalizing_buffer(data, finalizer) that defers cleanup via Python 3.12 buffer-protocol hooks (__buffer__/__release_buffer__) on fast path, or copies and immediately finalizes on pre-3.12 versions.
XShmGetImage pooled buffer implementation
src/mss/linux/xshmgetimage.py, src/mss/linux/base.py
Refactors MIT-SHM capture from a single buffer to a thread-safe pooled model: pre-allocates slots, acquires on demand for each capture, wraps results in finalizing_buffer so slots auto-release, and cleans up on shutdown or fallback. Supporting comment in base explains why XGetImage keeps simpler copy behavior.
Test coverage for finalizer and pool
src/tests/test_gnu_linux.py
New test verifies exception during buffer wrapping still releases SHM slots; additional Python 3.12+-only test verifies pool allocation failure raises ScreenShotError instead of falling back.
Documentation and packaging validation
docs/source/usage.rst, src/tests/test_setup.py
Adds "Buffer Reuse" section documenting zero-copy behavior, performance figures, and platform/version availability; manifest tests updated to validate buffer.py in sdist and wheel artifacts.

Sequence Diagram

sequenceDiagram
  participant Client
  participant XShmGetImage
  participant SHM_Pool
  participant finalizing_buffer
  participant Desktop
  Client->>XShmGetImage: grab(monitor)
  XShmGetImage->>SHM_Pool: _acquire_shm_slot(size)
  SHM_Pool-->>XShmGetImage: ShmSlot allocated or from free list
  XShmGetImage->>Desktop: xcb.shm_get_image(slot)
  Desktop-->>XShmGetImage: image pixels in slot mmap
  XShmGetImage->>finalizing_buffer: finalizing_buffer(memoryview, release_fn)
  finalizing_buffer-->>XShmGetImage: memoryview with finalizer attached
  XShmGetImage-->>Client: memoryview
  Note over Client: Client reads/uses buffer
  Client-->>finalizing_buffer: Release memoryview<br/>(on Python 3.12+ via __release_buffer__)
  finalizing_buffer->>SHM_Pool: _release_shm_slot(slot)
  SHM_Pool-->>finalizing_buffer: Slot returned to free list
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Buffers hop through slots with grace,
No copies wasted, zero-race,
Release the hook, return the space—
Three-twelve makes reuse embrace!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Reusable buffers' directly corresponds to the main objective and primary change of the pull request: implementing a reusable buffer mechanism with zero-copy support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch reusable-buffers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jholveck

jholveck commented Jun 1, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai help

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai rate limit to show your current review rate limit status.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
  • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
  • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai emit path instructions to emit generated path-specific review instructions for this repository.
  • @coderabbitai resolve merge conflict to automatically resolve merge conflicts.
  • @coderabbitai autofix to automatically fix issues identified in unresolved review comments.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

@jholveck

jholveck commented Jun 1, 2026

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces reusable/zero-copy buffer support (primarily for the GNU/Linux xshmgetimage backend) by returning buffer-protocol objects whose underlying SHM resources are released/recycled via Python 3.12+ buffer finalization.

Changes:

  • Add mss.buffer.finalizing_buffer() to provide finalizer-backed buffers on Python 3.12+ and a copy-based fallback on older versions.
  • Rework the Linux xshmgetimage backend to use a pool of SHM slots and return memoryview buffers instead of always copying into a bytearray.
  • Update typing/tests/docs/packaging to reflect the new buffer module and Buffer-based grab() API.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/mss/buffer.py Adds finalizer-backed buffer helper to enable safe reusable/zero-copy buffers on Python 3.12+.
src/mss/linux/xshmgetimage.py Implements SHM slot pool + integrates finalizing_buffer to avoid per-grab copies and enable buffer reuse.
src/mss/base.py Updates MSSImplementation.grab() return typing from bytearray to Buffer.
src/mss/linux/base.py Adds commentary about a possible future zero-copy approach for XGetImage (no functional change).
src/tests/test_gnu_linux.py Updates tests for finalizing-buffer behavior and adds a dynamic SHM pool growth failure test (3.12+).
src/tests/test_setup.py Ensures mss/buffer.py is included in sdist and wheel contents.
docs/source/usage.rst Documents buffer reuse/zero-copy behavior and expected performance impact.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mss/linux/xshmgetimage.py Outdated
Comment on lines +37 to +41
# Quick note (this should go in the commit log, or something, not source, but I had to record it somewhere):
# On my home box, at 4k resolution, doing 1000 grabs + NumPy sums of the pixel data, with/without reusable buffers:
# * Enabled: 18.6 ms / each
# * Disabled: 22.6 ms / each
# * Delta: 17.9% faster
Comment thread src/mss/linux/xshmgetimage.py Outdated
mm: mmap | None = None
try:
try:
memfd = os.memfd_create("mss-shm-buf", flags=os.MFD_CLOEXEC) # type:ignore[attr-defined]
Comment thread src/mss/linux/xshmgetimage.py Outdated
raise ScreenShotError(msg) from exc

try:
mm = mmap(memfd, size, prot=PROT_READ | PROT_WRITE) # type:ignore[call-arg]

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/tests/test_gnu_linux.py`:
- Around line 325-336: The test currently opens mss.MSS(...) inside the
pytest.raises context so __exit__ runs during exception unwinding and prevents
asserting the SHM free-list; change the test to create the MSS instance (mss.MSS
with backend="xshmgetimage") outside the pytest.raises block, monkeypatch
xshmgetimage.finalizing_buffer to raise, call sct.grab(monitor) inside
pytest.raises, then after the raised error assert that sct._free_shm_slots (or
call the helper that reports free slots) equals the pre-grab size to ensure
_release_shm_slot was called; also ensure the test uses a
pyvirtualdisplay.Display(...) like the other SHM tests (or otherwise set up
$DISPLAY) so an SHM-capable X server is available in CI.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b69e7251-b8ff-4c5d-a3e6-17a9b6cd911d

📥 Commits

Reviewing files that changed from the base of the PR and between d9d3375 and c1d3f3c.

📒 Files selected for processing (7)
  • docs/source/usage.rst
  • src/mss/base.py
  • src/mss/buffer.py
  • src/mss/linux/base.py
  • src/mss/linux/xshmgetimage.py
  • src/tests/test_gnu_linux.py
  • src/tests/test_setup.py

Comment thread src/tests/test_gnu_linux.py Outdated
Comment on lines 325 to 336
def test_exception_while_wrapping_finalizing_buffer(monkeypatch: pytest.MonkeyPatch) -> None:
"""Verify that wrapping failures still release the in-use SHM slot."""

def boom(_data: memoryview, _finalizer: Any) -> memoryview:
msg = "Boom!"
raise RuntimeError(msg)

The particular point is the window when the XShmGetImage's mmapped
buffer has a memoryview still outstanding, and the pixel data is
being copied into a bytearray. This can take a few milliseconds.
"""
# Force an exception during bytearray(img_mv)
real_bytearray = builtins.bytearray

def boom(*args: list, **kwargs: dict[str, Any]) -> bytearray:
# Only explode when called with the memoryview (the code path we care about).
if len(args) > 0 and isinstance(args[0], memoryview):
# We still need to eliminate args from the stack frame, just like the fix.
del args, kwargs
msg = "Boom!"
raise RuntimeError(msg)
return real_bytearray(*args, **kwargs)

# We have to be careful about the order in which we catch things. If we were to catch and discard the exception
# before the MSS object closes, it won't trigger the bug. That's why we have the pytest.raises outside the
# mss.MSS block. In addition, we do as much as we can before patching bytearray, to limit its scope.
with pytest.raises(RuntimeError, match="Boom!"), mss.MSS(backend="xshmgetimage") as sct: # noqa: PT012
monitor = sct.monitors[0]
with monkeypatch.context() as m:
m.setattr(builtins, "bytearray", boom)
m.setattr(mss.linux.xshmgetimage, "finalizing_buffer", boom)
sct.grab(monitor)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for any global/session/autouse display or xvfb setup that these tests depend on.
fd -t f 'conftest.py'
rg -nP -C3 '(autouse|DISPLAY|pyvirtualdisplay|xvfb|new_display_var)' $(fd -t f 'conftest.py') 2>/dev/null

# Compare how SHM tests obtain a display: sibling tests vs the two new tests.
rg -nP -C3 'backend="xshmgetimage"' --type=py

Repository: jholveck/python-mss

Length of output: 4692


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the SHM implementation: where _release_shm_slot and grab error handling live
fd -t f 'xshmgetimage.py' . | head -n 20 | cat
FILE="$(fd -t f 'xshmgetimage.py' . | head -n 1)"
echo "Using $FILE"
rg -n "def _release_shm_slot|_free_shm_slots|finalizing_buffer|def grab|try:|finally:" "$FILE" -n
echo "----"
# Print a small surrounding region for the key pieces
python3 - <<'PY'
import os, re, subprocess, textwrap, sys
file = subprocess.check_output(["bash","-lc","fd -t f 'xshmgetimage.py' . | head -n 1"], text=True).strip()
# Get candidate line numbers for key identifiers
keys = ["def _release_shm_slot", "_free_shm_slots", "finalizing_buffer", "def grab"]
out = subprocess.check_output(["bash","-lc", "rg -n \"("+"|".join(map(re.escape, keys))+")\" "+re.escape(file)], text=True)
print(out.strip())
PY

# Dump relevant sections with line numbers (avoid huge output)
sed -n '1,200p' "$FILE" | head -n 200 >/dev/null
# Heuristically print around occurrences
for ln in $(rg -n "def _release_shm_slot|_free_shm_slots|finalizing_buffer|def grab" "$FILE" | cut -d: -f1 | sort -u); do
  start=$((ln-60)); end=$((ln+120));
  echo "---- $FILE:$start-$end ----"
  sed -n "${start},${end}p" "$FILE"
done

# 2) Confirm all uses of pyvirtualdisplay/xvfb/DISPLAY setup across tests
rg -n --hidden --no-ignore-vcs "(pyvirtualdisplay|xvfb|new_display_var|DISPLAY\s*=|os\.environ\['DISPLAY'\]|getenv\(\"DISPLAY\"\)|x11|Xvfb)" src/tests -S

# 3) Inspect the specific test(s) around the snippet for context and patterns
rg -n "test_exception_while_wrapping_finalizing_buffer|test_dynamic_shm_growth_allocation_failure_raises|test_shm_available|test_shm_fallback" src/tests/test_gnu_linux.py
sed -n '240,380p' src/tests/test_gnu_linux.py

# 4) Inspect conftest fully enough to see if any virtual display is created globally
sed -n '1,220p' src/tests/conftest.py

Repository: jholveck/python-mss

Length of output: 1920


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print relevant sections from xshmgetimage.py (grab + release logic)
FILE="$(fd -t f 'xshmgetimage.py' src/mss/linux | head -n 1)"
echo "Using: $FILE"

echo "---- _release_shm_slot and _free_shm_slots ----"
rg -n -F "def _release_shm_slot" "$FILE"
sed -n '170,240p' "$FILE"

echo "---- grab() error handling + any finally ----"
rg -n -F "def grab" "$FILE"
rg -n -F "_release_shm_slot" "$FILE"
sed -n '280,380p' "$FILE"

# Display setup in tests
echo "---- test_gnu_linux.py virtual display usage ----"
sed -n '250,370p' src/tests/test_gnu_linux.py
rg -n -F "pyvirtualdisplay.Display" src/tests/test_gnu_linux.py

echo "---- display-related fixtures in conftest.py ----"
sed -n '1,220p' src/tests/conftest.py
sed -n '220,520p' src/tests/conftest.py
rg -n -F "pyvirtualdisplay|DISPLAY|xvfb|new_display_var" src/tests/conftest.py

Repository: jholveck/python-mss

Length of output: 16418


Fix test to actually assert the SHM free-list post-condition (and align DISPLAY setup)

  • The test’s name/docstring promise a “release the in-use SHM slot” post-condition, but it currently only asserts RuntimeError propagation; when sct.grab(...) raises inside with pytest.raises(...), mss.MSS(...) as sct, mss.MSS.__exit__ runs during exception unwinding and performs SHM cleanup, so _free_shm_slots can’t be meaningfully checked afterward. Since xshmgetimage.grab catches exceptions and calls _release_shm_slot(slot), move the mss.MSS(...) context outside pytest.raises and assert _free_shm_slots returns to its pre-grab size after the failed grab.
  • Separately, test_shm_available/test_shm_fallback wrap in pyvirtualdisplay.Display(...), but test_exception_while_wrapping_finalizing_buffer (and test_dynamic_shm_growth_allocation_failure_raises) instantiate mss.MSS(backend="xshmgetimage") without a virtual display; these tests rely on ambient $DISPLAY (and conftest.py doesn’t start a virtual display). Align them with the other SHM tests or ensure CI always provides an SHM-capable X server via $DISPLAY.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tests/test_gnu_linux.py` around lines 325 - 336, The test currently opens
mss.MSS(...) inside the pytest.raises context so __exit__ runs during exception
unwinding and prevents asserting the SHM free-list; change the test to create
the MSS instance (mss.MSS with backend="xshmgetimage") outside the pytest.raises
block, monkeypatch xshmgetimage.finalizing_buffer to raise, call
sct.grab(monitor) inside pytest.raises, then after the raised error assert that
sct._free_shm_slots (or call the helper that reports free slots) equals the
pre-grab size to ensure _release_shm_slot was called; also ensure the test uses
a pyvirtualdisplay.Display(...) like the other SHM tests (or otherwise set up
$DISPLAY) so an SHM-capable X server is available in CI.

jholveck added 11 commits May 31, 2026 23:09
These are straight from the AI, but I want to
commit them to start testing in other versions of
Python.
Adds a test for the new buffer module.  The test is pretty basic,
but it at least verifies that the buffer module is working and that the
buffers it creates are usable by outside modules.

This requires adding NumPy to the [tests] optional-dependencies group
for all versions of Python, since that's the buffer consumer we use in
the tests.
In my test cleanup and expansion, I accidentally included an
assumption that's only valid on 3.12+.
We already had tests for the finalizing_buffer
behavior, but this makes sure it survives at the
higher level MSS API.
The buffer finalizers can run at any time, from any thread.  This could
even happen while the XCB connection is being closed.  While it's hard
to imagine, it could happen from the same thread that's closing the XCB
connection, given some crazy weak references or something.

Make sure that we don't call shm_detach from a finalizer at the same
time as we're closing the XCB connection.  If we're about to close the
connection, even from a different thread than the one that is running
the finalizer, there's no need to call shm_detach.
@jholveck jholveck closed this Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants