# ===----------------------------------------------------------------------===## # # 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