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:
@@ -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
|
||||||
|
@@ -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')
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
"""
|
"""
|
||||||
|
def _CheckDone(entry):
|
||||||
|
if not entry.ObtainContents():
|
||||||
|
next_todo.append(entry)
|
||||||
|
return entry
|
||||||
|
|
||||||
todo = self._entries.values()
|
todo = self._entries.values()
|
||||||
for passnum in range(3):
|
for passnum in range(3):
|
||||||
|
threads = state.GetThreads()
|
||||||
next_todo = []
|
next_todo = []
|
||||||
for entry in todo:
|
|
||||||
if not entry.ObtainContents():
|
if threads == 0:
|
||||||
next_todo.append(entry)
|
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)
|
||||||
|
@@ -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()
|
||||||
|
@@ -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()
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
21
tools/binman/test/202_section_timeout.dts
Normal file
21
tools/binman/test/202_section_timeout.dts
Normal 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 {
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
Reference in New Issue
Block a user