Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG: "OSError: [Errno 7] Argument list too long" #45

Open
JuroOravec opened this issue Feb 21, 2025 · 3 comments · May be fixed by #46
Open

BUG: "OSError: [Errno 7] Argument list too long" #45

JuroOravec opened this issue Feb 21, 2025 · 3 comments · May be fixed by #46

Comments

@JuroOravec
Copy link
Contributor

JuroOravec commented Feb 21, 2025

UPDATE: The error is not what I thought it was at first glance, so I updated the name.


I have set up a CI workflow that, on pull request:

  1. Generates results for the latest master branch commit
  2. Generates results for the latest PR branch commit
  3. Runs asv continuous to compare the two.

The timeraw tests are failing in the CI (Github Action with linux worker).

The workflow looks like this:

# Run benchmark report on pull requests to master.
# The report is added to the PR as a comment.

name: Benchmarks

on:
  pull_request:
    branches: [ master ]

jobs:
  benchmark:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0  # Need full history for ASV

    - name: Fetch base branch
      run: |
        git remote add upstream https://github.com/${{ github.repository }}.git
        git fetch upstream master

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: '3.11'
        cache: 'pip'

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install asv

    - name: Run benchmarks
      run: |
        # Prepare the profile under which the benchmarks will be saved.
        # We assume that the CI machine has a name that is unique and stable.
        # See https://github.com/airspeed-velocity/asv/issues/796#issuecomment-1188431794
        asv machine --yes

        # Generate benchmark data
        # - `^` means that we mean the COMMIT of the branch, not the BRANCH itself.
        #       Without it, we would run benchmarks for the whole branch history.
        #       With it, we run benchmarks FROM the latest commit (incl) TO ...
        # - `!` means that we want to select range spanning a single commit.
        #       Without it, we would run benchmarks for all commits FROM the latest commit
        #       TO the start of the branch history.
        #       With it, we run benchmarks ONLY FOR the latest commit.
        asv run upstream/master^! -v
        asv run HEAD^! -v

        # Compare against master
        asv compare upstream/master HEAD --factor 1.1 --split -e > benchmark_results.md

    - name: Comment on PR
      uses: actions/github-script@v7
      with:
        github-token: ${{secrets.GITHUB_TOKEN}}
        script: |
          const fs = require('fs');
          const results = fs.readFileSync('benchmark_results.md', 'utf8');
          const body = `## Performance Benchmark Results\n\nComparing PR changes against master branch:\n\n${results}`;

          github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.name,
            body: body
          });

The error occurs when I get to running the benchmarks in

asv run upstream/master^! -v

I am also running peakmem benchmarks as part of the same suite, and these run successfully. So I know that the overall setup works, and that the issue is only with the timeraw tests. But the timeraw tests work for me locally.

The actual error is:

   File "/opt/hostedtoolcache/Python/3.13.2/x64/lib/python3.13/subprocess.py", line 1803, in _posix_spawn
     self.pid = os.posix_spawn(executable, args, env, **kwargs)
                ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 OSError: [Errno 7] Argument list too long: '/home/runner/work/django-components/django-components/.asv/env/b42e02a1e4eb987ea13a52915b8134f0/bin/python'

Which is caused from within subprocess.check_output

res = subprocess.check_output([sys.executable, "-c", code])

Traceback:

 Traceback (most recent call last):
   File "/home/runner/work/django-components/django-components/.asv/env/b42e02a1e4eb987ea13a52915b8134f0/lib/python3.13/site-packages/asv_runner/server.py", line 179, in _run_server
     _run(run_args)
     ~~~~^^^^^^^^^^
   File "/home/runner/work/django-components/django-components/.asv/env/b42e02a1e4eb987ea13a52915b8134f0/lib/python3.13/site-packages/asv_runner/run.py", line 72, in _run
     result = benchmark.do_run()
   File "/home/runner/work/django-components/django-components/.asv/env/b42e02a1e4eb987ea13a52915b8134f0/lib/python3.13/site-packages/asv_runner/benchmarks/_base.py", line 661, in do_run
     return self.run(*self._current_params)
            ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
   File "/home/runner/work/django-components/django-components/.asv/env/b42e02a1e4eb987ea13a52915b8134f0/lib/python3.13/site-packages/asv_runner/benchmarks/time.py", line 165, in run
     samples, number = self.benchmark_timing(
                       ~~~~~~~~~~~~~~~~~~~~~^
         timer,
         ^^^^^^
     ...<5 lines>...
         min_run_count=self.min_run_count,
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     )
     ^
   File "/home/runner/work/django-components/django-components/.asv/env/b42e02a1e4eb987ea13a52915b8134f0/lib/python3.13/site-packages/asv_runner/benchmarks/time.py", line 279, in benchmark_timing
     timing = timer.timeit(number)
   File "/home/runner/work/django-components/django-components/.asv/env/b42e02a1e4eb987ea13a52915b8134f0/lib/python3.13/site-packages/asv_runner/benchmarks/timeraw.py", line 78, in timeit
     res = subprocess.check_output([sys.executable, "-c", code])
   File "/opt/hostedtoolcache/Python/3.13.2/x64/lib/python3.13/subprocess.py", line 474, in check_output
     return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
            ~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                **kwargs).stdout
                ^^^^^^^^^
   File "/opt/hostedtoolcache/Python/3.13.2/x64/lib/python3.13/subprocess.py", line 556, in run
     with Popen(*popenargs, **kwargs) as process:
          ~~~~~^^^^^^^^^^^^^^^^^^^^^^
   File "/opt/hostedtoolcache/Python/3.13.2/x64/lib/python3.13/subprocess.py", line 1038, in __init__
     self._execute_child(args, executable, preexec_fn, close_fds,
     ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                         pass_fds, cwd, env,
                         ^^^^^^^^^^^^^^^^^^^
     ...<5 lines>...
                         gid, gids, uid, umask,
                         ^^^^^^^^^^^^^^^^^^^^^^
                         start_new_session, process_group)
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File "/opt/hostedtoolcache/Python/3.13.2/x64/lib/python3.13/subprocess.py", line 1859, in _execute_child
     self._posix_spawn(args, executable, env, restore_signals, close_fds,
     ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                       p2cread, p2cwrite,
                       ^^^^^^^^^^^^^^^^^^
                       c2pread, c2pwrite,
                       ^^^^^^^^^^^^^^^^^^
                       errread, errwrite)
                       ^^^^^^^^^^^^^^^^^^
   File "/opt/hostedtoolcache/Python/3.13.2/x64/lib/python3.13/subprocess.py", line 1803, in _posix_spawn
     self.pid = os.posix_spawn(executable, args, env, **kwargs)
                ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 OSError: [Errno 7] Argument list too long: '/home/runner/work/django-components/django-components/.asv/env/b42e02a1e4eb987ea13a52915b8134f0/bin/python'
@JuroOravec JuroOravec changed the title BUG: Timeraw not compatible with 3.13 BUG: "OSError: [Errno 7] Argument list too long" Feb 21, 2025
@JuroOravec
Copy link
Contributor Author

Thoughts:

  • Maybe I'm getting the error because my code to test is too large? It's a file of 198kB.
  • Maybe the code snippet includes some characters that messes it all up? (stackoverflow)

Looking into it further, the timeraw tests that use a "small" example (11kB) DO actually run!

This thread claims that it's a system-level issue, where the command line input is larger than what the system supports.

So how to work around this?

  • Save the script to execute to a file, and then read from the file?
  • Pass the data via STDIN? (stackoverflow)

I don't know anything about these kind of things, but my intuition would be that saving and reading a file would be slower than passing the info through STDIN.

So my approach would be to pass the code to execute like this:

res = subprocess.check_output([sys.executable, "-c", code], input=stmt.encode("utf-8"))

We would also have to update the subprocess_tmpl so that it reads the code from STDIN

from __future__ import print_function
from timeit import timeit, default_timer as timer
stmt = sys.stdin.read()
print(repr(timeit(stmt="""{stmt}""", setup="""{setup}""",
            number={number}, timer=timer)))

Ah... There's a problem... We cannot pass only the main test code (stmt), because also setup code might be too big.

So what we might want to do instead is to pass the whole subprocess_tmpl script as STDIN.

And then, the code that would be called would just read STDIN and pass it to exec():

code = self.subprocess_tmpl.format(stmt=stmt, setup=setup, number=number)

evaler = """
import sys
code = sys.stdin.read()
eval(code)
"""

res = subprocess.check_output([sys.executable, "-c", evaler], input=code.encode("utf-8"))
return float(res.strip())

@JuroOravec
Copy link
Contributor Author

I tested the snippet below locally, and it works. I yet have to test if this works in the CI too.

code = self.subprocess_tmpl.format(stmt=stmt, setup=setup, number=number)

evaler = textwrap.dedent(
    """
    import sys
    code = sys.stdin.read()
    exec(code)
    """
)

res = subprocess.check_output([sys.executable, "-c", evaler], input=code.encode("utf-8"), stderr=subprocess.STDOUT)
return float(res.strip())

One thing though, chatbot assistant suggested using subprocess.Popen directly instead of subprocess.check_output:

proc = subprocess.Popen([sys.executable, "-c", evaler],
                      stdin=subprocess.PIPE,
                      stdout=subprocess.PIPE,
                      stderr=subprocess.PIPE)
stdout, stderr = proc.communicate(input=code.encode("utf-8"))
if proc.returncode != 0:
    raise RuntimeError(f"Subprocess failed: {stderr.decode()}")
return float(stdout.decode("utf-8").strip())

The upside of using Popen like this is that the error from the child process is included in the output, which makes debugging a whole lotta easier. E.g. it has shown me that I need to use exec() instead of eval() because eval() takes only a single statement, which led to this error:

     raise RuntimeError(f"Subprocess failed: {stderr.decode()}")
 RuntimeError: Subprocess failed: Traceback (most recent call last):
   File "<string>", line 4, in <module>
   File "<string>", line 1
     from __future__ import print_function
     ^^^^
 SyntaxError: invalid syntax

@HaoZeke What do you think of this fix? And where is _SeparateProcessTimer.timeit() being called from? Do the callees try to catch errors from this function?

@JuroOravec
Copy link
Contributor Author

JuroOravec commented Feb 21, 2025

Looks like I managed to fix my CI.

For anyone with the same issue as me, here's the fix you can use in the meantime until the fix gets released in asv:

  1. In my CI workflow, I run this snippet BEFORE running the benchmarks. The snippet triggers the generation of the virtual environemnts with asv setup, and monkeypatches the asv_runner/benchmarks/timeraw.py files in each environment:
# TODO: REMOVE ONCE FIXED UPSTREAM
# Fix for https://github.com/airspeed-velocity/asv_runner/issues/45
# Prepare virtual environment
# Currently, we have to monkeypatch the `timeit` function in the `timeraw` benchmark.
# The problem is that `asv` passes the code to execute via command line, and when the
# code is too big, it fails with `OSError: [Errno 7] Argument list too long`.
# So we have to tweak it to pass the code via STDIN, which doesn't have this limitation.
#
# 1. First create the virtual environment, so that asv generates the directories where
#    the monkeypatch can be applied.
echo "Creating virtual environment..."
asv setup -v || true
echo "Virtual environment created."
# 2. Now let's apply the monkeypatch by appending it to the `timeraw.py` files.
# First find all `timeraw.py` files
echo "Applying monkeypatch..."
find .asv/env -type f -path "*/site-packages/asv_runner/benchmarks/timeraw.py" | while read -r file; do
    # Add a newline and then append the monkeypatch contents
    echo "" >> "$file"
    cat "benchmarks/monkeypatch_asv_ci.txt" >> "$file"
done
echo "Monkeypatch applied."
# END OF MONKEYPATCH

The fix that is applied is appended to the original content of /asv_runner/benchmarks/timeraw.py, and it overwrites the the original _SeparateProcessTimer.timeit method.

# ------------ FIX FOR #45 ------------
# See https://github.com/airspeed-velocity/asv_runner/issues/45
# This fix is applied in CI in the `benchmark.yml` file.
# This file is intentionally named `monkeypatch_asv_ci.txt` to avoid being
# loaded as a python file by `asv`.
# -------------------------------------

def timeit(self, number):
    """
    Run the function's code `number` times in a separate Python process, and
    return the execution time.

    #### Parameters
    **number** (`int`)
    : The number of times to execute the function's code.

    #### Returns
    **time** (`float`)
    : The time it took to execute the function's code `number` times.

    #### Notes
    The function's code is executed in a separate Python process to avoid
    interference from the parent process. The function can return either a
    single string of code to be executed, or a tuple of two strings: the
    code to be executed and the setup code to be run before timing.
    """
    stmt = self.func()
    if isinstance(stmt, tuple):
        stmt, setup = stmt
    else:
        setup = ""
    stmt = textwrap.dedent(stmt)
    setup = textwrap.dedent(setup)
    stmt = stmt.replace(r'"""', r"\"\"\"")
    setup = setup.replace(r'"""', r"\"\"\"")

    # TODO
    # -----------ORIGINAL CODE-----------
    # code = self.subprocess_tmpl.format(stmt=stmt, setup=setup, number=number)

    # res = subprocess.check_output([sys.executable, "-c", code])
    # return float(res.strip())

    # -----------NEW CODE-----------
    code = self.subprocess_tmpl.format(stmt=stmt, setup=setup, number=number)

    evaler = textwrap.dedent(
        """
        import sys
        code = sys.stdin.read()
        exec(code)
        """
    )

    proc = subprocess.Popen([sys.executable, "-c", evaler],
                            stdin=subprocess.PIPE,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE)
    stdout, stderr = proc.communicate(input=code.encode("utf-8"))
    if proc.returncode != 0:
        raise RuntimeError(f"Subprocess failed: {stderr.decode()}")
    return float(stdout.decode("utf-8").strip())

_SeparateProcessTimer.timeit = timeit

# ------------ END FIX #45 ------------

And this is the final Github action workflow for running benchmarks on pull request:

# Run benchmark report on pull requests to master.
# The report is added to the PR as a comment.

name: Benchmarks

on:
  pull_request:
    branches: [ master ]

jobs:
  benchmark:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0  # Need full history for ASV

    - name: Fetch base branch
      run: |
        git remote add upstream https://github.com/${{ github.repository }}.git
        git fetch upstream master

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: '3.11'
        cache: 'pip'

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install asv

    - name: Run benchmarks
      run: |
        # TODO: REMOVE ONCE FIXED UPSTREAM
        # Fix for https://github.com/airspeed-velocity/asv_runner/issues/45
        # Prepare virtual environment
        # Currently, we have to monkeypatch the `timeit` function in the `timeraw` benchmark.
        # The problem is that `asv` passes the code to execute via command line, and when the
        # code is too big, it fails with `OSError: [Errno 7] Argument list too long`.
        # So we have to tweak it to pass the code via STDIN, which doesn't have this limitation.
        #
        # 1. First create the virtual environment, so that asv generates the directories where
        #    the monkeypatch can be applied.
        echo "Creating virtual environment..."
        asv setup -v || true
        echo "Virtual environment created."
        # 2. Now let's apply the monkeypatch by appending it to the `timeraw.py` files.
        # First find all `timeraw.py` files
        echo "Applying monkeypatch..."
        find .asv/env -type f -path "*/site-packages/asv_runner/benchmarks/timeraw.py" | while read -r file; do
            # Add a newline and then append the monkeypatch contents
            echo "" >> "$file"
            cat "benchmarks/monkeypatch_asv_ci.txt" >> "$file"
        done
        echo "Monkeypatch applied."
        # END OF MONKEYPATCH

        # Prepare the profile under which the benchmarks will be saved.
        # We assume that the CI machine has a name that is unique and stable.
        # See https://github.com/airspeed-velocity/asv/issues/796#issuecomment-1188431794
        asv machine --yes

        # Generate benchmark data
        # - `^` means that we mean the COMMIT of the branch, not the BRANCH itself.
        #       Without it, we would run benchmarks for the whole branch history.
        #       With it, we run benchmarks FROM the latest commit (incl) TO ...
        # - `!` means that we want to select range spanning a single commit.
        #       Without it, we would run benchmarks for all commits FROM the latest commit
        #       TO the start of the branch history.
        #       With it, we run benchmarks ONLY FOR the latest commit.
        asv run upstream/master^! -v
        asv run HEAD^! -v

        # Compare against master
        asv compare upstream/master HEAD --factor 1.1 --split > benchmark_results.md

    - name: Comment on PR
      uses: actions/github-script@v7
      with:
        github-token: ${{secrets.GITHUB_TOKEN}}
        script: |
          const fs = require('fs');
          const results = fs.readFileSync('benchmark_results.md', 'utf8');
          const body = `## Performance Benchmark Results\n\nComparing PR changes against master branch:\n\n${results}`;

          github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.name,
            body: body
          });

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 a pull request may close this issue.

1 participant