| #===----------------------------------------------------------------------===## |
| # |
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| # See https://llvm.org/LICENSE.txt for license information. |
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| # |
| #===----------------------------------------------------------------------===## |
| |
| import re |
| import select |
| import socket |
| import subprocess |
| import tempfile |
| import threading |
| from typing import List |
| |
| |
| def _get_cpu_count() -> int: |
| # Determine the number of cores by listing a /sys directory. Older devices |
| # lack `nproc`. Even if a static toybox binary is pushed to the device, it may |
| # return an incorrect value. (e.g. On a Nexus 7 running Android 5.0, toybox |
| # nproc returns 1 even though the device has 4 CPUs.) |
| job = subprocess.run(["adb", "shell", "ls /sys/devices/system/cpu"], |
| encoding="utf8", check=False, |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| if job.returncode == 1: |
| # Maybe adb is missing, maybe ANDROID_SERIAL needs to be defined, maybe the |
| # /sys subdir isn't there. Most errors will be handled later, just use one |
| # job. (N.B. The adb command still succeeds even if ls fails on older |
| # devices that lack the shell_v2 adb feature.) |
| return 1 |
| # Make sure there are no CR characters in the output. Pre-shell_v2, the adb |
| # stdout comes from a master pty so newlines are CRLF-delimited. On Windows, |
| # LF might also get expanded to CRLF. |
| cpu_listing = job.stdout.replace('\r', '\n') |
| |
| # Count lines that match "cpu${DIGITS}". |
| result = len([line for line in cpu_listing.splitlines() |
| if re.match(r'cpu(\d)+$', line)]) |
| |
| # Restrict the result to something reasonable. |
| if result < 1: |
| result = 1 |
| if result > 1024: |
| result = 1024 |
| |
| return result |
| |
| |
| def _job_limit_socket_thread(temp_dir: tempfile.TemporaryDirectory, |
| server: socket.socket, job_count: int) -> None: |
| """Service the job limit server socket, accepting only as many connections |
| as there should be concurrent jobs. |
| """ |
| clients: List[socket.socket] = [] |
| while True: |
| rlist = list(clients) |
| if len(clients) < job_count: |
| rlist.append(server) |
| rlist, _, _ = select.select(rlist, [], []) |
| for sock in rlist: |
| if sock == server: |
| new_client, _ = server.accept() |
| new_client.send(b"x") |
| clients.append(new_client) |
| else: |
| sock.close() |
| clients.remove(sock) |
| |
| |
| def adb_job_limit_socket() -> str: |
| """An Android device can frequently have many fewer cores than the host |
| (e.g. 4 versus 128). We want to exploit all the device cores without |
| overburdening it. |
| |
| Create a Unix domain socket that only allows as many connections as CPUs on |
| the Android device. |
| """ |
| |
| # Create the job limit server socket. |
| temp_dir = tempfile.TemporaryDirectory(prefix="libcxx_") |
| sock_addr = temp_dir.name + "/adb_job.sock" |
| server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
| server.bind(sock_addr) |
| server.listen(1) |
| |
| # Spawn a thread to service the socket. As a daemon thread, its existence |
| # won't prevent interpreter shutdown. The temp dir will still be removed on |
| # shutdown. |
| cpu_count = _get_cpu_count() |
| threading.Thread(target=_job_limit_socket_thread, |
| args=(temp_dir, server, cpu_count), |
| daemon=True).start() |
| |
| return sock_addr |