We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
CPU count logic does not distinguish "usable" CPUs inside cgroup'd containers (e.g. inside k8s)
### search you tried in the issue tracker

multiprocessing, k8s, usable, cpu

### describe your issue

When invoking pre-commit from within a container environment with cgroup cpu enforcement, pre-commit fans out more than expected (based on the total number of CPUs rather than the number of _usable_ CPUs). This ends up causing unexpected behaviour and makes it hard to optimise for performance when running hooks in CI inside containers (since [pre-commit's parallelism is non-configurable](https://github.com/pre-commit/pre-commit/issues/1710)), which leaves us with either "shard using too many CPUs" or `require_serial: true`, both of which aren't ideal.

The cause is seemingly due to the implementation in xargs.py: https://github.com/pre-commit/pre-commit/blob/bde292b51078357384602ebe6b9b27b1906987e5/pre_commit/xargs.py#L28 which uses `multiprocessing.cpu_count()`, which is documented to [not [be] equivalent to the number of CPUs the current process can use](https://docs.python.org/3/library/multiprocessing.html#:~:text=This%20number%20is%20not%20equivalent%20to%20the%20number%20of%20CPUs%20the%20current%20process%20can%20use.%20The%20number%20of%20usable%20CPUs%20can%20be%20obtained%20with%20len(os.sched_getaffinity(0))). This is confirmed by running `python3 -c 'import multiprocessing; print(multiprocessing.cpu_count())'` inside the container environment: the number is higher than expected.

From the docs, it looks like a cgroup-compatible way of grabbing the number of usable CPUs would be to use `len(os.sched_getaffinity(0))`, but I don't know if that has undesirable downsides. I also don't know if this would be a disruptive breaking change to anyone relying on the old behaviour, so I wanted to make this issue first to get your thoughts.

### pre-commit --version

pre-commit 2.19.0

### .pre-commit-config.yaml

```yaml
n/a - can provide a dummy config invoking `python3 -c 'import multiprocessing; print(multiprocessing.cpu_count())'` if more info needed, but this isn't specific to any one hook or config option
```


### ~/.cache/pre-commit/pre-commit.log (if present)

_No response_

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `pre_commit/xargs.py`
Content:
```
1 from __future__ import annotations
2 
3 import concurrent.futures
4 import contextlib
5 import math
6 import multiprocessing
7 import os
8 import subprocess
9 import sys
10 from typing import Any
11 from typing import Callable
12 from typing import Generator
13 from typing import Iterable
14 from typing import MutableMapping
15 from typing import Sequence
16 from typing import TypeVar
17 
18 from pre_commit import parse_shebang
19 from pre_commit.util import cmd_output_b
20 from pre_commit.util import cmd_output_p
21 
22 TArg = TypeVar('TArg')
23 TRet = TypeVar('TRet')
24 
25 
26 def cpu_count() -> int:
27     try:
28         return multiprocessing.cpu_count()
29     except NotImplementedError:
30         return 1
31 
32 
33 def _environ_size(_env: MutableMapping[str, str] | None = None) -> int:
34     environ = _env if _env is not None else getattr(os, 'environb', os.environ)
35     size = 8 * len(environ)  # number of pointers in `envp`
36     for k, v in environ.items():
37         size += len(k) + len(v) + 2  # c strings in `envp`
38     return size
39 
40 
41 def _get_platform_max_length() -> int:  # pragma: no cover (platform specific)
42     if os.name == 'posix':
43         maximum = os.sysconf('SC_ARG_MAX') - 2048 - _environ_size()
44         maximum = max(min(maximum, 2 ** 17), 2 ** 12)
45         return maximum
46     elif os.name == 'nt':
47         return 2 ** 15 - 2048  # UNICODE_STRING max - headroom
48     else:
49         # posix minimum
50         return 2 ** 12
51 
52 
53 def _command_length(*cmd: str) -> int:
54     full_cmd = ' '.join(cmd)
55 
56     # win32 uses the amount of characters, more details at:
57     # https://github.com/pre-commit/pre-commit/pull/839
58     if sys.platform == 'win32':
59         return len(full_cmd.encode('utf-16le')) // 2
60     else:
61         return len(full_cmd.encode(sys.getfilesystemencoding()))
62 
63 
64 class ArgumentTooLongError(RuntimeError):
65     pass
66 
67 
68 def partition(
69         cmd: Sequence[str],
70         varargs: Sequence[str],
71         target_concurrency: int,
72         _max_length: int | None = None,
73 ) -> tuple[tuple[str, ...], ...]:
74     _max_length = _max_length or _get_platform_max_length()
75 
76     # Generally, we try to partition evenly into at least `target_concurrency`
77     # partitions, but we don't want a bunch of tiny partitions.
78     max_args = max(4, math.ceil(len(varargs) / target_concurrency))
79 
80     cmd = tuple(cmd)
81     ret = []
82 
83     ret_cmd: list[str] = []
84     # Reversed so arguments are in order
85     varargs = list(reversed(varargs))
86 
87     total_length = _command_length(*cmd) + 1
88     while varargs:
89         arg = varargs.pop()
90 
91         arg_length = _command_length(arg) + 1
92         if (
93                 total_length + arg_length <= _max_length and
94                 len(ret_cmd) < max_args
95         ):
96             ret_cmd.append(arg)
97             total_length += arg_length
98         elif not ret_cmd:
99             raise ArgumentTooLongError(arg)
100         else:
101             # We've exceeded the length, yield a command
102             ret.append(cmd + tuple(ret_cmd))
103             ret_cmd = []
104             total_length = _command_length(*cmd) + 1
105             varargs.append(arg)
106 
107     ret.append(cmd + tuple(ret_cmd))
108 
109     return tuple(ret)
110 
111 
112 @contextlib.contextmanager
113 def _thread_mapper(maxsize: int) -> Generator[
114     Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]],
115     None, None,
116 ]:
117     if maxsize == 1:
118         yield map
119     else:
120         with concurrent.futures.ThreadPoolExecutor(maxsize) as ex:
121             yield ex.map
122 
123 
124 def xargs(
125         cmd: tuple[str, ...],
126         varargs: Sequence[str],
127         *,
128         color: bool = False,
129         target_concurrency: int = 1,
130         _max_length: int = _get_platform_max_length(),
131         **kwargs: Any,
132 ) -> tuple[int, bytes]:
133     """A simplified implementation of xargs.
134 
135     color: Make a pty if on a platform that supports it
136     target_concurrency: Target number of partitions to run concurrently
137     """
138     cmd_fn = cmd_output_p if color else cmd_output_b
139     retcode = 0
140     stdout = b''
141 
142     try:
143         cmd = parse_shebang.normalize_cmd(cmd)
144     except parse_shebang.ExecutableNotFoundError as e:
145         return e.to_output()[:2]
146 
147     # on windows, batch files have a separate length limit than windows itself
148     if (
149             sys.platform == 'win32' and
150             cmd[0].lower().endswith(('.bat', '.cmd'))
151     ):  # pragma: win32 cover
152         # this is implementation details but the command gets translated into
153         # full/path/to/cmd.exe /c *cmd
154         cmd_exe = parse_shebang.find_executable('cmd.exe')
155         # 1024 is additionally subtracted to give headroom for further
156         # expansion inside the batch file
157         _max_length = 8192 - len(cmd_exe) - len(' /c ') - 1024
158 
159     partitions = partition(cmd, varargs, target_concurrency, _max_length)
160 
161     def run_cmd_partition(
162             run_cmd: tuple[str, ...],
163     ) -> tuple[int, bytes, bytes | None]:
164         return cmd_fn(
165             *run_cmd, check=False, stderr=subprocess.STDOUT, **kwargs,
166         )
167 
168     threads = min(len(partitions), target_concurrency)
169     with _thread_mapper(threads) as thread_map:
170         results = thread_map(run_cmd_partition, partitions)
171 
172         for proc_retcode, proc_out, _ in results:
173             if abs(proc_retcode) > abs(retcode):
174                 retcode = proc_retcode
175             stdout += proc_out
176 
177     return retcode, stdout
178
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py
--- a/pre_commit/xargs.py
+++ b/pre_commit/xargs.py
@@ -24,6 +24,14 @@
 
 
 def cpu_count() -> int:
+    try:
+        # On systems that support it, this will return a more accurate count of
+        # usable CPUs for the current process, which will take into account
+        # cgroup limits
+        return len(os.sched_getaffinity(0))
+    except AttributeError:
+        pass
+
     try:
         return multiprocessing.cpu_count()
     except NotImplementedError:

{"golden_diff": "diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py\n--- a/pre_commit/xargs.py\n+++ b/pre_commit/xargs.py\n@@ -24,6 +24,14 @@\n \n \n def cpu_count() -> int:\n+    try:\n+        # On systems that support it, this will return a more accurate count of\n+        # usable CPUs for the current process, which will take into account\n+        # cgroup limits\n+        return len(os.sched_getaffinity(0))\n+    except AttributeError:\n+        pass\n+\n     try:\n         return multiprocessing.cpu_count()\n     except NotImplementedError:\n", "issue": "CPU count logic does not distinguish \"usable\" CPUs inside cgroup'd containers (e.g. inside k8s)\n### search you tried in the issue tracker\r\n\r\nmultiprocessing, k8s, usable, cpu\r\n\r\n### describe your issue\r\n\r\nWhen invoking pre-commit from within a container environment with cgroup cpu enforcement, pre-commit fans out more than expected (based on the total number of CPUs rather than the number of _usable_ CPUs). This ends up causing unexpected behaviour and makes it hard to optimise for performance when running hooks in CI inside containers (since [pre-commit's parallelism is non-configurable](https://github.com/pre-commit/pre-commit/issues/1710)), which leaves us with either \"shard using too many CPUs\" or `require_serial: true`, both of which aren't ideal.\r\n\r\nThe cause is seemingly due to the implementation in xargs.py: https://github.com/pre-commit/pre-commit/blob/bde292b51078357384602ebe6b9b27b1906987e5/pre_commit/xargs.py#L28 which uses `multiprocessing.cpu_count()`, which is documented to [not [be] equivalent to the number of CPUs the current process can use](https://docs.python.org/3/library/multiprocessing.html#:~:text=This%20number%20is%20not%20equivalent%20to%20the%20number%20of%20CPUs%20the%20current%20process%20can%20use.%20The%20number%20of%20usable%20CPUs%20can%20be%20obtained%20with%20len(os.sched_getaffinity(0))). This is confirmed by running `python3 -c 'import multiprocessing; print(multiprocessing.cpu_count())'` inside the container environment: the number is higher than expected.\r\n\r\nFrom the docs, it looks like a cgroup-compatible way of grabbing the number of usable CPUs would be to use `len(os.sched_getaffinity(0))`, but I don't know if that has undesirable downsides. I also don't know if this would be a disruptive breaking change to anyone relying on the old behaviour, so I wanted to make this issue first to get your thoughts.\r\n\r\n### pre-commit --version\r\n\r\npre-commit 2.19.0\r\n\r\n### .pre-commit-config.yaml\r\n\r\n```yaml\r\nn/a - can provide a dummy config invoking `python3 -c 'import multiprocessing; print(multiprocessing.cpu_count())'` if more info needed, but this isn't specific to any one hook or config option\r\n```\r\n\r\n\r\n### ~/.cache/pre-commit/pre-commit.log (if present)\r\n\r\n_No response_\n", "before_files": [{"content": "from __future__ import annotations\n\nimport concurrent.futures\nimport contextlib\nimport math\nimport multiprocessing\nimport os\nimport subprocess\nimport sys\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Generator\nfrom typing import Iterable\nfrom typing import MutableMapping\nfrom typing import Sequence\nfrom typing import TypeVar\n\nfrom pre_commit import parse_shebang\nfrom pre_commit.util import cmd_output_b\nfrom pre_commit.util import cmd_output_p\n\nTArg = TypeVar('TArg')\nTRet = TypeVar('TRet')\n\n\ndef cpu_count() -> int:\n    try:\n        return multiprocessing.cpu_count()\n    except NotImplementedError:\n        return 1\n\n\ndef _environ_size(_env: MutableMapping[str, str] | None = None) -> int:\n    environ = _env if _env is not None else getattr(os, 'environb', os.environ)\n    size = 8 * len(environ)  # number of pointers in `envp`\n    for k, v in environ.items():\n        size += len(k) + len(v) + 2  # c strings in `envp`\n    return size\n\n\ndef _get_platform_max_length() -> int:  # pragma: no cover (platform specific)\n    if os.name == 'posix':\n        maximum = os.sysconf('SC_ARG_MAX') - 2048 - _environ_size()\n        maximum = max(min(maximum, 2 ** 17), 2 ** 12)\n        return maximum\n    elif os.name == 'nt':\n        return 2 ** 15 - 2048  # UNICODE_STRING max - headroom\n    else:\n        # posix minimum\n        return 2 ** 12\n\n\ndef _command_length(*cmd: str) -> int:\n    full_cmd = ' '.join(cmd)\n\n    # win32 uses the amount of characters, more details at:\n    # https://github.com/pre-commit/pre-commit/pull/839\n    if sys.platform == 'win32':\n        return len(full_cmd.encode('utf-16le')) // 2\n    else:\n        return len(full_cmd.encode(sys.getfilesystemencoding()))\n\n\nclass ArgumentTooLongError(RuntimeError):\n    pass\n\n\ndef partition(\n        cmd: Sequence[str],\n        varargs: Sequence[str],\n        target_concurrency: int,\n        _max_length: int | None = None,\n) -> tuple[tuple[str, ...], ...]:\n    _max_length = _max_length or _get_platform_max_length()\n\n    # Generally, we try to partition evenly into at least `target_concurrency`\n    # partitions, but we don't want a bunch of tiny partitions.\n    max_args = max(4, math.ceil(len(varargs) / target_concurrency))\n\n    cmd = tuple(cmd)\n    ret = []\n\n    ret_cmd: list[str] = []\n    # Reversed so arguments are in order\n    varargs = list(reversed(varargs))\n\n    total_length = _command_length(*cmd) + 1\n    while varargs:\n        arg = varargs.pop()\n\n        arg_length = _command_length(arg) + 1\n        if (\n                total_length + arg_length <= _max_length and\n                len(ret_cmd) < max_args\n        ):\n            ret_cmd.append(arg)\n            total_length += arg_length\n        elif not ret_cmd:\n            raise ArgumentTooLongError(arg)\n        else:\n            # We've exceeded the length, yield a command\n            ret.append(cmd + tuple(ret_cmd))\n            ret_cmd = []\n            total_length = _command_length(*cmd) + 1\n            varargs.append(arg)\n\n    ret.append(cmd + tuple(ret_cmd))\n\n    return tuple(ret)\n\n\n@contextlib.contextmanager\ndef _thread_mapper(maxsize: int) -> Generator[\n    Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]],\n    None, None,\n]:\n    if maxsize == 1:\n        yield map\n    else:\n        with concurrent.futures.ThreadPoolExecutor(maxsize) as ex:\n            yield ex.map\n\n\ndef xargs(\n        cmd: tuple[str, ...],\n        varargs: Sequence[str],\n        *,\n        color: bool = False,\n        target_concurrency: int = 1,\n        _max_length: int = _get_platform_max_length(),\n        **kwargs: Any,\n) -> tuple[int, bytes]:\n    \"\"\"A simplified implementation of xargs.\n\n    color: Make a pty if on a platform that supports it\n    target_concurrency: Target number of partitions to run concurrently\n    \"\"\"\n    cmd_fn = cmd_output_p if color else cmd_output_b\n    retcode = 0\n    stdout = b''\n\n    try:\n        cmd = parse_shebang.normalize_cmd(cmd)\n    except parse_shebang.ExecutableNotFoundError as e:\n        return e.to_output()[:2]\n\n    # on windows, batch files have a separate length limit than windows itself\n    if (\n            sys.platform == 'win32' and\n            cmd[0].lower().endswith(('.bat', '.cmd'))\n    ):  # pragma: win32 cover\n        # this is implementation details but the command gets translated into\n        # full/path/to/cmd.exe /c *cmd\n        cmd_exe = parse_shebang.find_executable('cmd.exe')\n        # 1024 is additionally subtracted to give headroom for further\n        # expansion inside the batch file\n        _max_length = 8192 - len(cmd_exe) - len(' /c ') - 1024\n\n    partitions = partition(cmd, varargs, target_concurrency, _max_length)\n\n    def run_cmd_partition(\n            run_cmd: tuple[str, ...],\n    ) -> tuple[int, bytes, bytes | None]:\n        return cmd_fn(\n            *run_cmd, check=False, stderr=subprocess.STDOUT, **kwargs,\n        )\n\n    threads = min(len(partitions), target_concurrency)\n    with _thread_mapper(threads) as thread_map:\n        results = thread_map(run_cmd_partition, partitions)\n\n        for proc_retcode, proc_out, _ in results:\n            if abs(proc_retcode) > abs(retcode):\n                retcode = proc_retcode\n            stdout += proc_out\n\n    return retcode, stdout\n", "path": "pre_commit/xargs.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport concurrent.futures\nimport contextlib\nimport math\nimport multiprocessing\nimport os\nimport subprocess\nimport sys\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Generator\nfrom typing import Iterable\nfrom typing import MutableMapping\nfrom typing import Sequence\nfrom typing import TypeVar\n\nfrom pre_commit import parse_shebang\nfrom pre_commit.util import cmd_output_b\nfrom pre_commit.util import cmd_output_p\n\nTArg = TypeVar('TArg')\nTRet = TypeVar('TRet')\n\n\ndef cpu_count() -> int:\n    try:\n        # On systems that support it, this will return a more accurate count of\n        # usable CPUs for the current process, which will take into account\n        # cgroup limits\n        return len(os.sched_getaffinity(0))\n    except AttributeError:\n        pass\n\n    try:\n        return multiprocessing.cpu_count()\n    except NotImplementedError:\n        return 1\n\n\ndef _environ_size(_env: MutableMapping[str, str] | None = None) -> int:\n    environ = _env if _env is not None else getattr(os, 'environb', os.environ)\n    size = 8 * len(environ)  # number of pointers in `envp`\n    for k, v in environ.items():\n        size += len(k) + len(v) + 2  # c strings in `envp`\n    return size\n\n\ndef _get_platform_max_length() -> int:  # pragma: no cover (platform specific)\n    if os.name == 'posix':\n        maximum = os.sysconf('SC_ARG_MAX') - 2048 - _environ_size()\n        maximum = max(min(maximum, 2 ** 17), 2 ** 12)\n        return maximum\n    elif os.name == 'nt':\n        return 2 ** 15 - 2048  # UNICODE_STRING max - headroom\n    else:\n        # posix minimum\n        return 2 ** 12\n\n\ndef _command_length(*cmd: str) -> int:\n    full_cmd = ' '.join(cmd)\n\n    # win32 uses the amount of characters, more details at:\n    # https://github.com/pre-commit/pre-commit/pull/839\n    if sys.platform == 'win32':\n        return len(full_cmd.encode('utf-16le')) // 2\n    else:\n        return len(full_cmd.encode(sys.getfilesystemencoding()))\n\n\nclass ArgumentTooLongError(RuntimeError):\n    pass\n\n\ndef partition(\n        cmd: Sequence[str],\n        varargs: Sequence[str],\n        target_concurrency: int,\n        _max_length: int | None = None,\n) -> tuple[tuple[str, ...], ...]:\n    _max_length = _max_length or _get_platform_max_length()\n\n    # Generally, we try to partition evenly into at least `target_concurrency`\n    # partitions, but we don't want a bunch of tiny partitions.\n    max_args = max(4, math.ceil(len(varargs) / target_concurrency))\n\n    cmd = tuple(cmd)\n    ret = []\n\n    ret_cmd: list[str] = []\n    # Reversed so arguments are in order\n    varargs = list(reversed(varargs))\n\n    total_length = _command_length(*cmd) + 1\n    while varargs:\n        arg = varargs.pop()\n\n        arg_length = _command_length(arg) + 1\n        if (\n                total_length + arg_length <= _max_length and\n                len(ret_cmd) < max_args\n        ):\n            ret_cmd.append(arg)\n            total_length += arg_length\n        elif not ret_cmd:\n            raise ArgumentTooLongError(arg)\n        else:\n            # We've exceeded the length, yield a command\n            ret.append(cmd + tuple(ret_cmd))\n            ret_cmd = []\n            total_length = _command_length(*cmd) + 1\n            varargs.append(arg)\n\n    ret.append(cmd + tuple(ret_cmd))\n\n    return tuple(ret)\n\n\n@contextlib.contextmanager\ndef _thread_mapper(maxsize: int) -> Generator[\n    Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]],\n    None, None,\n]:\n    if maxsize == 1:\n        yield map\n    else:\n        with concurrent.futures.ThreadPoolExecutor(maxsize) as ex:\n            yield ex.map\n\n\ndef xargs(\n        cmd: tuple[str, ...],\n        varargs: Sequence[str],\n        *,\n        color: bool = False,\n        target_concurrency: int = 1,\n        _max_length: int = _get_platform_max_length(),\n        **kwargs: Any,\n) -> tuple[int, bytes]:\n    \"\"\"A simplified implementation of xargs.\n\n    color: Make a pty if on a platform that supports it\n    target_concurrency: Target number of partitions to run concurrently\n    \"\"\"\n    cmd_fn = cmd_output_p if color else cmd_output_b\n    retcode = 0\n    stdout = b''\n\n    try:\n        cmd = parse_shebang.normalize_cmd(cmd)\n    except parse_shebang.ExecutableNotFoundError as e:\n        return e.to_output()[:2]\n\n    # on windows, batch files have a separate length limit than windows itself\n    if (\n            sys.platform == 'win32' and\n            cmd[0].lower().endswith(('.bat', '.cmd'))\n    ):  # pragma: win32 cover\n        # this is implementation details but the command gets translated into\n        # full/path/to/cmd.exe /c *cmd\n        cmd_exe = parse_shebang.find_executable('cmd.exe')\n        # 1024 is additionally subtracted to give headroom for further\n        # expansion inside the batch file\n        _max_length = 8192 - len(cmd_exe) - len(' /c ') - 1024\n\n    partitions = partition(cmd, varargs, target_concurrency, _max_length)\n\n    def run_cmd_partition(\n            run_cmd: tuple[str, ...],\n    ) -> tuple[int, bytes, bytes | None]:\n        return cmd_fn(\n            *run_cmd, check=False, stderr=subprocess.STDOUT, **kwargs,\n        )\n\n    threads = min(len(partitions), target_concurrency)\n    with _thread_mapper(threads) as thread_map:\n        results = thread_map(run_cmd_partition, partitions)\n\n        for proc_retcode, proc_out, _ in results:\n            if abs(proc_retcode) > abs(retcode):\n                retcode = proc_retcode\n            stdout += proc_out\n\n    return retcode, stdout\n", "path": "pre_commit/xargs.py"}]}