1*d173ce4aSRyan Prichard# ===----------------------------------------------------------------------===## 2*d173ce4aSRyan Prichard# 3*d173ce4aSRyan Prichard# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4*d173ce4aSRyan Prichard# See https://llvm.org/LICENSE.txt for license information. 5*d173ce4aSRyan Prichard# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6*d173ce4aSRyan Prichard# 7*d173ce4aSRyan Prichard# ===----------------------------------------------------------------------===## 8*d173ce4aSRyan Prichard 9*d173ce4aSRyan Prichardimport re 10*d173ce4aSRyan Prichardimport select 11*d173ce4aSRyan Prichardimport socket 12*d173ce4aSRyan Prichardimport subprocess 13*d173ce4aSRyan Prichardimport tempfile 14*d173ce4aSRyan Prichardimport threading 15*d173ce4aSRyan Prichardfrom typing import List 16*d173ce4aSRyan Prichard 17*d173ce4aSRyan Prichard 18*d173ce4aSRyan Pricharddef _get_cpu_count() -> int: 19*d173ce4aSRyan Prichard # Determine the number of cores by listing a /sys directory. Older devices 20*d173ce4aSRyan Prichard # lack `nproc`. Even if a static toybox binary is pushed to the device, it may 21*d173ce4aSRyan Prichard # return an incorrect value. (e.g. On a Nexus 7 running Android 5.0, toybox 22*d173ce4aSRyan Prichard # nproc returns 1 even though the device has 4 CPUs.) 23*d173ce4aSRyan Prichard job = subprocess.run(["adb", "shell", "ls /sys/devices/system/cpu"], 24*d173ce4aSRyan Prichard encoding="utf8", check=False, 25*d173ce4aSRyan Prichard stdout=subprocess.PIPE, stderr=subprocess.PIPE) 26*d173ce4aSRyan Prichard if job.returncode == 1: 27*d173ce4aSRyan Prichard # Maybe adb is missing, maybe ANDROID_SERIAL needs to be defined, maybe the 28*d173ce4aSRyan Prichard # /sys subdir isn't there. Most errors will be handled later, just use one 29*d173ce4aSRyan Prichard # job. (N.B. The adb command still succeeds even if ls fails on older 30*d173ce4aSRyan Prichard # devices that lack the shell_v2 adb feature.) 31*d173ce4aSRyan Prichard return 1 32*d173ce4aSRyan Prichard # Make sure there are no CR characters in the output. Pre-shell_v2, the adb 33*d173ce4aSRyan Prichard # stdout comes from a master pty so newlines are CRLF-delimited. On Windows, 34*d173ce4aSRyan Prichard # LF might also get expanded to CRLF. 35*d173ce4aSRyan Prichard cpu_listing = job.stdout.replace('\r', '\n') 36*d173ce4aSRyan Prichard 37*d173ce4aSRyan Prichard # Count lines that match "cpu${DIGITS}". 38*d173ce4aSRyan Prichard result = len([line for line in cpu_listing.splitlines() 39*d173ce4aSRyan Prichard if re.match(r'cpu(\d)+$', line)]) 40*d173ce4aSRyan Prichard 41*d173ce4aSRyan Prichard # Restrict the result to something reasonable. 42*d173ce4aSRyan Prichard if result < 1: 43*d173ce4aSRyan Prichard result = 1 44*d173ce4aSRyan Prichard if result > 1024: 45*d173ce4aSRyan Prichard result = 1024 46*d173ce4aSRyan Prichard 47*d173ce4aSRyan Prichard return result 48*d173ce4aSRyan Prichard 49*d173ce4aSRyan Prichard 50*d173ce4aSRyan Pricharddef _job_limit_socket_thread(temp_dir: tempfile.TemporaryDirectory, 51*d173ce4aSRyan Prichard server: socket.socket, job_count: int) -> None: 52*d173ce4aSRyan Prichard """Service the job limit server socket, accepting only as many connections 53*d173ce4aSRyan Prichard as there should be concurrent jobs. 54*d173ce4aSRyan Prichard """ 55*d173ce4aSRyan Prichard clients: List[socket.socket] = [] 56*d173ce4aSRyan Prichard while True: 57*d173ce4aSRyan Prichard rlist = list(clients) 58*d173ce4aSRyan Prichard if len(clients) < job_count: 59*d173ce4aSRyan Prichard rlist.append(server) 60*d173ce4aSRyan Prichard rlist, _, _ = select.select(rlist, [], []) 61*d173ce4aSRyan Prichard for sock in rlist: 62*d173ce4aSRyan Prichard if sock == server: 63*d173ce4aSRyan Prichard new_client, _ = server.accept() 64*d173ce4aSRyan Prichard new_client.send(b"x") 65*d173ce4aSRyan Prichard clients.append(new_client) 66*d173ce4aSRyan Prichard else: 67*d173ce4aSRyan Prichard sock.close() 68*d173ce4aSRyan Prichard clients.remove(sock) 69*d173ce4aSRyan Prichard 70*d173ce4aSRyan Prichard 71*d173ce4aSRyan Pricharddef adb_job_limit_socket() -> str: 72*d173ce4aSRyan Prichard """An Android device can frequently have many fewer cores than the host 73*d173ce4aSRyan Prichard (e.g. 4 versus 128). We want to exploit all the device cores without 74*d173ce4aSRyan Prichard overburdening it. 75*d173ce4aSRyan Prichard 76*d173ce4aSRyan Prichard Create a Unix domain socket that only allows as many connections as CPUs on 77*d173ce4aSRyan Prichard the Android device. 78*d173ce4aSRyan Prichard """ 79*d173ce4aSRyan Prichard 80*d173ce4aSRyan Prichard # Create the job limit server socket. 81*d173ce4aSRyan Prichard temp_dir = tempfile.TemporaryDirectory(prefix="libcxx_") 82*d173ce4aSRyan Prichard sock_addr = temp_dir.name + "/adb_job.sock" 83*d173ce4aSRyan Prichard server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 84*d173ce4aSRyan Prichard server.bind(sock_addr) 85*d173ce4aSRyan Prichard server.listen(1) 86*d173ce4aSRyan Prichard 87*d173ce4aSRyan Prichard # Spawn a thread to service the socket. As a daemon thread, its existence 88*d173ce4aSRyan Prichard # won't prevent interpreter shutdown. The temp dir will still be removed on 89*d173ce4aSRyan Prichard # shutdown. 90*d173ce4aSRyan Prichard cpu_count = _get_cpu_count() 91*d173ce4aSRyan Prichard threading.Thread(target=_job_limit_socket_thread, 92*d173ce4aSRyan Prichard args=(temp_dir, server, cpu_count), 93*d173ce4aSRyan Prichard daemon=True).start() 94*d173ce4aSRyan Prichard 95*d173ce4aSRyan Prichard return sock_addr 96