Skip to content

Commit fdc1a3d

Browse files
committed
build_sdk: use a jobserver/thread pool for speed
On my computer this reduces build time from ~23 minutes to ~12 minutes, which should help speed up our CI a bit. Signed-off-by: Julia Vassiliki <julia.vassiliki@unsw.edu.au>
1 parent 70a94fd commit fdc1a3d

1 file changed

Lines changed: 58 additions & 15 deletions

File tree

build_sdk.py

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@
1414
"""
1515
from argparse import ArgumentParser
1616
import copy
17+
import concurrent.futures
1718
from os import popen, system, environ
1819
import shutil
1920
from pathlib import Path
2021
from dataclasses import dataclass
21-
from sys import executable
22+
from sys import executable, stderr
2223
from tarfile import open as tar_open, TarInfo
2324
import platform as host_platform
2425
from enum import IntEnum
2526
import json
27+
import os
28+
import tempfile
2629
import subprocess
2730

2831
from typing import Any, Dict, Union, List, Tuple, Optional
@@ -877,6 +880,22 @@ def build_initialiser(
877880
dest.chmod(0o744)
878881

879882

883+
# Taken and modified from Ninja's example jobserver_pool, under the Apache License
884+
# Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
885+
# This code does not form part of the distribution of Microkit.
886+
# https://github.com/ninja-build/ninja/blob/v1.13.2/misc/jobserver_pool.py#L162-L177
887+
def create_jobserver_fifo(path: str, jobs_count: int) -> str:
888+
"""Create and fill Posix FIFO."""
889+
os.mkfifo(path)
890+
891+
# Unused, but necessary as otherwise opening O_WRONLY will fail with ENXIO
892+
read_fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
893+
write_fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK)
894+
assert jobs_count > 0, f"Token count must be strictly positive"
895+
os.write(write_fd, (jobs_count - 1) * b"x")
896+
return f" -j{jobs_count} --jobserver-auth=fifo:" + path
897+
898+
880899
def main() -> None:
881900
parser = ArgumentParser()
882901
parser.add_argument("--sel4", type=Path, required=True)
@@ -890,6 +909,7 @@ def main() -> None:
890909
parser.add_argument("--skip-initialiser", action="store_true", help="Initialiser will not be built")
891910
parser.add_argument("--skip-docs", action="store_true", help="Docs will not be built")
892911
parser.add_argument("--skip-tar", action="store_true", help="SDK and source tarballs will not be built")
912+
parser.add_argument("--jobs", type=int, default=os.cpu_count())
893913
parser.add_argument("--release-packaging", action="store_true", help="All SDKs for distribution will be produced")
894914
# Read from the version file as unless someone has specified
895915
# a version, that is the source of truth
@@ -994,20 +1014,43 @@ def main() -> None:
9941014

9951015
if not args.skip_run_time:
9961016
build_dir = Path("build")
997-
for (board, configs) in build_goals:
998-
for config in configs:
999-
if not args.skip_sel4:
1000-
build_sel4(sel4_dir, tool_dir, sdk_dir, build_dir, board, config, args.llvm)
1001-
loader_printing = 1 if config.name == "debug" else 0
1002-
loader_defines = []
1003-
if not board.arch.is_x86():
1004-
loader_defines.append(("LINK_ADDRESS", hex(board.loader_link_address)))
1005-
build_elf_component("loader", sdk_dir, build_dir, board, config, args.llvm, loader_defines)
1006-
1007-
build_elf_component("monitor", sdk_dir, build_dir, board, config, args.llvm, [])
1008-
build_lib_component("libmicrokit", sdk_dir, build_dir, board, config, args.llvm)
1009-
if not args.skip_initialiser:
1010-
build_initialiser("initialiser", sdk_dir, build_dir, board, config)
1017+
1018+
def build_one_goal(board: str, config: str):
1019+
if not args.skip_sel4:
1020+
build_sel4(sel4_dir, tool_dir, sdk_dir, build_dir, board, config, args.llvm)
1021+
loader_printing = 1 if config.name == "debug" else 0
1022+
loader_defines = []
1023+
if not board.arch.is_x86():
1024+
loader_defines.append(("LINK_ADDRESS", hex(board.loader_link_address)))
1025+
build_elf_component("loader", sdk_dir, build_dir, board, config, args.llvm, loader_defines)
1026+
1027+
build_elf_component("monitor", sdk_dir, build_dir, board, config, args.llvm, [])
1028+
build_lib_component("libmicrokit", sdk_dir, build_dir, board, config, args.llvm)
1029+
if not args.skip_initialiser:
1030+
build_initialiser("initialiser", sdk_dir, build_dir, board, config)
1031+
1032+
# FIXME: Possible improvement here is that our ThreadPool does not know
1033+
# about the current state of the jobserver, so may spawn threads which
1034+
# will make the number of jobs exceed the capacity of the jobserver
1035+
# (since the protocol assumes that if you are started under a jobserver
1036+
# you have "1" job available to you implicitly)
1037+
# So we can sometimes over-allocate a bit.
1038+
with concurrent.futures.ThreadPoolExecutor(max_workers=args.jobs) as executor, \
1039+
tempfile.TemporaryDirectory(prefix="microkit_") as fifo_dir:
1040+
1041+
if not os.environ.get("MAKEFLAGS"):
1042+
os.environ["MAKEFLAGS"] = create_jobserver_fifo(fifo_dir + "/fifo", args.jobs)
1043+
1044+
goals = [(board, config) for (board, configs) in build_goals for config in configs]
1045+
tasks_map = {executor.submit(build_one_goal, board, config): (board, config) for (board, config) in goals}
1046+
1047+
for task in concurrent.futures.as_completed(tasks_map):
1048+
board, config = tasks_map[task]
1049+
try:
1050+
task.result()
1051+
except Exception as exc:
1052+
print(f"Build goal {board} {config} failed", file=stderr)
1053+
10111054

10121055
# Setup the examples
10131056
for example, example_path in EXAMPLES.items():

0 commit comments

Comments
 (0)