binman: Support multithreading for building images

Some images may take a while to build, e.g. if they are large and use slow
compression. Support compiling sections in parallel to speed things up.

Signed-off-by: Simon Glass <sjg@chromium.org>
(fixed to use a separate test file to fix flakiness)
This commit is contained in:
Simon Glass
2021-07-06 10:36:37 -06:00
parent 650ead1a4a
commit c69d19c8f8
8 changed files with 136 additions and 6 deletions

View File

@@ -1142,6 +1142,22 @@ adds a -v<level> option to the call to binman::
make BINMAN_VERBOSE=5 make BINMAN_VERBOSE=5
Building sections in parallel
-----------------------------
By default binman uses multiprocessing to speed up compilation of large images.
This works at a section level, with one thread for each entry in the section.
This can speed things up if the entries are large and use compression.
This feature can be disabled with the '-T' flag, which defaults to a suitable
value for your machine. This depends on the Python version, e.g on v3.8 it uses
12 threads on an 8-core machine. See ConcurrentFutures_ for more details.
The special value -T0 selects single-threaded mode, useful for debugging during
development, since dealing with exceptions and problems in threads is more
difficult. This avoids any use of ThreadPoolExecutor.
History / Credits History / Credits
----------------- -----------------
@@ -1190,3 +1206,5 @@ Some ideas:
-- --
Simon Glass <sjg@chromium.org> Simon Glass <sjg@chromium.org>
7/7/2016 7/7/2016
.. _ConcurrentFutures: https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor

View File

@@ -32,6 +32,10 @@ controlled by a description in the board device tree.'''
default=False, help='Display the README file') default=False, help='Display the README file')
parser.add_argument('--toolpath', type=str, action='append', parser.add_argument('--toolpath', type=str, action='append',
help='Add a path to the directories containing tools') help='Add a path to the directories containing tools')
parser.add_argument('-T', '--threads', type=int,
default=None, help='Number of threads to use (0=single-thread)')
parser.add_argument('--test-section-timeout', action='store_true',
help='Use a zero timeout for section multi-threading (for testing)')
parser.add_argument('-v', '--verbosity', default=1, parser.add_argument('-v', '--verbosity', default=1,
type=int, help='Control verbosity: 0=silent, 1=warnings, 2=notices, ' type=int, help='Control verbosity: 0=silent, 1=warnings, 2=notices, '
'3=info, 4=detail, 5=debug') '3=info, 4=detail, 5=debug')

View File

@@ -628,9 +628,13 @@ def Binman(args):
tools.PrepareOutputDir(args.outdir, args.preserve) tools.PrepareOutputDir(args.outdir, args.preserve)
tools.SetToolPaths(args.toolpath) tools.SetToolPaths(args.toolpath)
state.SetEntryArgs(args.entry_arg) state.SetEntryArgs(args.entry_arg)
state.SetThreads(args.threads)
images = PrepareImagesAndDtbs(dtb_fname, args.image, images = PrepareImagesAndDtbs(dtb_fname, args.image,
args.update_fdt, use_expanded) args.update_fdt, use_expanded)
if args.test_section_timeout:
# Set the first image to timeout, used in testThreadTimeout()
images[list(images.keys())[0]].test_section_timeout = True
missing = False missing = False
for image in images.values(): for image in images.values():
missing |= ProcessImage(image, args.update_fdt, args.map, missing |= ProcessImage(image, args.update_fdt, args.map,

View File

@@ -9,10 +9,12 @@ images to be created.
""" """
from collections import OrderedDict from collections import OrderedDict
import concurrent.futures
import re import re
import sys import sys
from binman.entry import Entry from binman.entry import Entry
from binman import state
from dtoc import fdt_util from dtoc import fdt_util
from patman import tools from patman import tools
from patman import tout from patman import tout
@@ -525,15 +527,43 @@ class Entry_section(Entry):
def GetEntryContents(self): def GetEntryContents(self):
"""Call ObtainContents() for each entry in the section """Call ObtainContents() for each entry in the section
""" """
todo = self._entries.values() def _CheckDone(entry):
for passnum in range(3):
next_todo = []
for entry in todo:
if not entry.ObtainContents(): if not entry.ObtainContents():
next_todo.append(entry) next_todo.append(entry)
return entry
todo = self._entries.values()
for passnum in range(3):
threads = state.GetThreads()
next_todo = []
if threads == 0:
for entry in todo:
_CheckDone(entry)
else:
with concurrent.futures.ThreadPoolExecutor(
max_workers=threads) as executor:
future_to_data = {
entry: executor.submit(_CheckDone, entry)
for entry in todo}
timeout = 60
if self.GetImage().test_section_timeout:
timeout = 0
done, not_done = concurrent.futures.wait(
future_to_data.values(), timeout=timeout)
# Make sure we check the result, so any exceptions are
# generated. Check the results in entry order, since tests
# may expect earlier entries to fail first.
for entry in todo:
job = future_to_data[entry]
job.result()
if not_done:
self.Raise('Timed out obtaining contents')
todo = next_todo todo = next_todo
if not todo: if not todo:
break break
if todo: if todo:
self.Raise('Internal error: Could not complete processing of contents: remaining %s' % self.Raise('Internal error: Could not complete processing of contents: remaining %s' %
todo) todo)

View File

@@ -308,7 +308,8 @@ class TestFunctional(unittest.TestCase):
def _DoTestFile(self, fname, debug=False, map=False, update_dtb=False, def _DoTestFile(self, fname, debug=False, map=False, update_dtb=False,
entry_args=None, images=None, use_real_dtb=False, entry_args=None, images=None, use_real_dtb=False,
use_expanded=False, verbosity=None, allow_missing=False, use_expanded=False, verbosity=None, allow_missing=False,
extra_indirs=None): extra_indirs=None, threads=None,
test_section_timeout=False):
"""Run binman with a given test file """Run binman with a given test file
Args: Args:
@@ -331,6 +332,8 @@ class TestFunctional(unittest.TestCase):
allow_missing: Set the '--allow-missing' flag so that missing allow_missing: Set the '--allow-missing' flag so that missing
external binaries just produce a warning instead of an error external binaries just produce a warning instead of an error
extra_indirs: Extra input directories to add using -I extra_indirs: Extra input directories to add using -I
threads: Number of threads to use (None for default, 0 for
single-threaded)
""" """
args = [] args = []
if debug: if debug:
@@ -342,6 +345,10 @@ class TestFunctional(unittest.TestCase):
if self.toolpath: if self.toolpath:
for path in self.toolpath: for path in self.toolpath:
args += ['--toolpath', path] args += ['--toolpath', path]
if threads is not None:
args.append('-T%d' % threads)
if test_section_timeout:
args.append('--test-section-timeout')
args += ['build', '-p', '-I', self._indir, '-d', self.TestFile(fname)] args += ['build', '-p', '-I', self._indir, '-d', self.TestFile(fname)]
if map: if map:
args.append('-m') args.append('-m')
@@ -412,7 +419,7 @@ class TestFunctional(unittest.TestCase):
def _DoReadFileDtb(self, fname, use_real_dtb=False, use_expanded=False, def _DoReadFileDtb(self, fname, use_real_dtb=False, use_expanded=False,
map=False, update_dtb=False, entry_args=None, map=False, update_dtb=False, entry_args=None,
reset_dtbs=True, extra_indirs=None): reset_dtbs=True, extra_indirs=None, threads=None):
"""Run binman and return the resulting image """Run binman and return the resulting image
This runs binman with a given test file and then reads the resulting This runs binman with a given test file and then reads the resulting
@@ -439,6 +446,8 @@ class TestFunctional(unittest.TestCase):
function. If reset_dtbs is True, then the original test dtb function. If reset_dtbs is True, then the original test dtb
is written back before this function finishes is written back before this function finishes
extra_indirs: Extra input directories to add using -I extra_indirs: Extra input directories to add using -I
threads: Number of threads to use (None for default, 0 for
single-threaded)
Returns: Returns:
Tuple: Tuple:
@@ -463,7 +472,8 @@ class TestFunctional(unittest.TestCase):
try: try:
retcode = self._DoTestFile(fname, map=map, update_dtb=update_dtb, retcode = self._DoTestFile(fname, map=map, update_dtb=update_dtb,
entry_args=entry_args, use_real_dtb=use_real_dtb, entry_args=entry_args, use_real_dtb=use_real_dtb,
use_expanded=use_expanded, extra_indirs=extra_indirs) use_expanded=use_expanded, extra_indirs=extra_indirs,
threads=threads)
self.assertEqual(0, retcode) self.assertEqual(0, retcode)
out_dtb_fname = tools.GetOutputFilename('u-boot.dtb.out') out_dtb_fname = tools.GetOutputFilename('u-boot.dtb.out')
@@ -4542,5 +4552,22 @@ class TestFunctional(unittest.TestCase):
data = self._DoReadFile('201_opensbi.dts') data = self._DoReadFile('201_opensbi.dts')
self.assertEqual(OPENSBI_DATA, data[:len(OPENSBI_DATA)]) self.assertEqual(OPENSBI_DATA, data[:len(OPENSBI_DATA)])
def testSectionsSingleThread(self):
"""Test sections without multithreading"""
data = self._DoReadFileDtb('055_sections.dts', threads=0)[0]
expected = (U_BOOT_DATA + tools.GetBytes(ord('!'), 12) +
U_BOOT_DATA + tools.GetBytes(ord('a'), 12) +
U_BOOT_DATA + tools.GetBytes(ord('&'), 4))
self.assertEqual(expected, data)
def testThreadTimeout(self):
"""Test handling a thread that takes too long"""
with self.assertRaises(ValueError) as e:
self._DoTestFile('202_section_timeout.dts',
test_section_timeout=True)
self.assertIn("Node '/binman/section@0': Timed out obtaining contents",
str(e.exception))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@@ -36,6 +36,8 @@ class Image(section.Entry_section):
fdtmap_data: Contents of the fdtmap when loading from a file fdtmap_data: Contents of the fdtmap when loading from a file
allow_repack: True to add properties to allow the image to be safely allow_repack: True to add properties to allow the image to be safely
repacked later repacked later
test_section_timeout: Use a zero timeout for section multi-threading
(for testing)
Args: Args:
copy_to_orig: Copy offset/size to orig_offset/orig_size after reading copy_to_orig: Copy offset/size to orig_offset/orig_size after reading
@@ -74,6 +76,7 @@ class Image(section.Entry_section):
self.allow_repack = False self.allow_repack = False
self._ignore_missing = ignore_missing self._ignore_missing = ignore_missing
self.use_expanded = use_expanded self.use_expanded = use_expanded
self.test_section_timeout = False
if not test: if not test:
self.ReadNode() self.ReadNode()

View File

@@ -7,6 +7,7 @@
import hashlib import hashlib
import re import re
import threading
from dtoc import fdt from dtoc import fdt
import os import os
@@ -55,6 +56,9 @@ allow_entry_expansion = True
# to the new ones, the compressed size increases, etc. # to the new ones, the compressed size increases, etc.
allow_entry_contraction = False allow_entry_contraction = False
# Number of threads to use for binman (None means machine-dependent)
num_threads = None
def GetFdtForEtype(etype): def GetFdtForEtype(etype):
"""Get the Fdt object for a particular device-tree entry """Get the Fdt object for a particular device-tree entry
@@ -420,3 +424,22 @@ def AllowEntryContraction():
raised raised
""" """
return allow_entry_contraction return allow_entry_contraction
def SetThreads(threads):
"""Set the number of threads to use when building sections
Args:
threads: Number of threads to use (None for default, 0 for
single-threaded)
"""
global num_threads
num_threads = threads
def GetThreads():
"""Get the number of threads to use when building sections
Returns:
Number of threads to use (None for default, 0 for single-threaded)
"""
return num_threads

View File

@@ -0,0 +1,21 @@
// SPDX-License-Identifier: GPL-2.0+
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
binman {
pad-byte = <0x26>;
size = <0x28>;
section@0 {
read-only;
size = <0x10>;
pad-byte = <0x21>;
u-boot {
};
};
};
};