Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # Copyright 2019 The Fuchsia Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | # This script computes the number of concurrent jobs that can run in the |
| 7 | # build as a function of the machine. It accepts a set of key value pairs |
| 8 | # given by repeated --memory-per-job arguments. For example: |
| 9 | # |
| 10 | # $ get_concurrent_jobs.py --memory-per-job dart=1GB |
| 11 | # |
| 12 | # The result is a json map printed to stdout that gives the number of |
| 13 | # concurrent jobs allowed of each kind. For example: |
| 14 | # |
| 15 | # {"dart": 8} |
| 16 | # |
| 17 | # Some memory can be held out of the calculation with the --reserve-memory flag. |
| 18 | |
| 19 | import argparse |
| 20 | import ctypes |
| 21 | import json |
| 22 | import multiprocessing |
| 23 | import os |
| 24 | import re |
| 25 | import subprocess |
| 26 | import sys |
| 27 | |
| 28 | UNITS = {'B': 1, 'KB': 2**10, 'MB': 2**20, 'GB': 2**30, 'TB': 2**40} |
| 29 | |
| 30 | |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 31 | # pylint: disable=line-too-long |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 32 | # See https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-globalmemorystatusex |
| 33 | # and https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/ns-sysinfoapi-memorystatusex |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 34 | # pylint: enable=line-too-long |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 35 | class MEMORYSTATUSEX(ctypes.Structure): |
| 36 | _fields_ = [ |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 37 | ('dwLength', ctypes.c_ulong), |
| 38 | ('dwMemoryLoad', ctypes.c_ulong), |
| 39 | ('ullTotalPhys', ctypes.c_ulonglong), |
| 40 | ('ullAvailPhys', ctypes.c_ulonglong), |
| 41 | ('ullTotalPageFile', ctypes.c_ulonglong), |
| 42 | ('ullAvailPageFile', ctypes.c_ulonglong), |
| 43 | ('ullTotalVirtual', ctypes.c_ulonglong), |
| 44 | ('ullAvailVirtual', ctypes.c_ulonglong), |
| 45 | ('sullAvailExtendedVirtual', ctypes.c_ulonglong), |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 46 | ] |
| 47 | |
| 48 | |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 49 | def get_total_memory(): |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 50 | if sys.platform in ('win32', 'cygwin'): |
| 51 | stat = MEMORYSTATUSEX(dwLength=ctypes.sizeof(MEMORYSTATUSEX)) |
| 52 | success = ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)) |
| 53 | return stat.ullTotalPhys if success else 0 |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 54 | if sys.platform.startswith('linux'): |
| 55 | if os.path.exists('/proc/meminfo'): |
| 56 | with open('/proc/meminfo') as meminfo: |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 57 | memtotal_re = re.compile(r'^MemTotal:\s*(\d*)\s*kB') |
| 58 | for line in meminfo: |
| 59 | match = memtotal_re.match(line) |
| 60 | if match: |
| 61 | return float(match.group(1)) * 2**10 |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 62 | if sys.platform == 'darwin': |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 63 | try: |
| 64 | return int(subprocess.check_output(['sysctl', '-n', 'hw.memsize'])) |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 65 | except: # pylint: disable=bare-except |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 66 | return 0 |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 67 | return 0 |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 68 | |
| 69 | |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 70 | def parse_size(string): |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 71 | i = next(i for (i, c) in enumerate(string) if not c.isdigit()) |
| 72 | number = string[:i].strip() |
| 73 | unit = string[i:].strip() |
| 74 | return int(float(number) * UNITS[unit]) |
| 75 | |
| 76 | |
| 77 | class ParseSizeAction(argparse.Action): |
| 78 | |
| 79 | def __call__(self, parser, args, values, option_string=None): |
| 80 | sizes = getattr(args, self.dest, []) |
| 81 | for value in values: |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 82 | (k, val) = value.split('=', 1) |
| 83 | sizes.append((k, parse_size(val))) |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 84 | setattr(args, self.dest, sizes) |
| 85 | |
| 86 | |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 87 | def main(): |
Zachary Anderson | 9e0316d | 2022-06-03 13:00:14 -0700 | [diff] [blame] | 88 | parser = argparse.ArgumentParser() |
| 89 | parser.add_argument( |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 90 | '--memory-per-job', |
| 91 | action=ParseSizeAction, |
| 92 | default=[], |
| 93 | nargs='*', |
| 94 | help='Key value pairings (dart=1GB) giving an estimate of the amount of ' |
Zachary Anderson | 9e0316d | 2022-06-03 13:00:14 -0700 | [diff] [blame] | 95 | 'memory needed for the class of job.' |
| 96 | ) |
| 97 | parser.add_argument( |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 98 | '--reserve-memory', |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 99 | type=parse_size, |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 100 | default=0, |
Zachary Anderson | 9e0316d | 2022-06-03 13:00:14 -0700 | [diff] [blame] | 101 | help='The amount of memory to be held out of the amount for jobs to use.' |
| 102 | ) |
| 103 | args = parser.parse_args() |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 104 | |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 105 | total_memory = get_total_memory() |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 106 | |
Zachary Anderson | 9e0316d | 2022-06-03 13:00:14 -0700 | [diff] [blame] | 107 | # Ensure the total memory used in the calculation below is at least 0 |
| 108 | mem_total_bytes = max(0, total_memory - args.reserve_memory) |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 109 | |
Zachary Anderson | 9e0316d | 2022-06-03 13:00:14 -0700 | [diff] [blame] | 110 | # Ensure the number of cpus used in the calculation below is at least 1 |
| 111 | try: |
| 112 | cpu_cap = multiprocessing.cpu_count() |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 113 | except: # pylint: disable=bare-except |
Zachary Anderson | 9e0316d | 2022-06-03 13:00:14 -0700 | [diff] [blame] | 114 | cpu_cap = 1 |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 115 | |
Zachary Anderson | 9e0316d | 2022-06-03 13:00:14 -0700 | [diff] [blame] | 116 | concurrent_jobs = {} |
| 117 | for job, memory_per_job in args.memory_per_job: |
| 118 | # Calculate the number of jobs that will fit in memory. Ensure the |
| 119 | # value is at least 1. |
| 120 | num_concurrent_jobs = int(max(1, mem_total_bytes / memory_per_job)) |
| 121 | # Cap the number of jobs by the number of cpus available. |
| 122 | concurrent_jobs[job] = min(num_concurrent_jobs, cpu_cap) |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 123 | |
Zachary Anderson | 9e0316d | 2022-06-03 13:00:14 -0700 | [diff] [blame] | 124 | print(json.dumps(concurrent_jobs)) |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 125 | |
Zachary Anderson | 9e0316d | 2022-06-03 13:00:14 -0700 | [diff] [blame] | 126 | return 0 |
Zachary Anderson | d71ba56 | 2021-07-29 23:43:49 -0700 | [diff] [blame] | 127 | |
| 128 | |
| 129 | if __name__ == '__main__': |
Zachary Anderson | 2f0b7f9 | 2022-06-04 15:21:18 -0700 | [diff] [blame] | 130 | sys.exit(main()) |