Skip to content

Commit 9d8c8af

Browse files
project.py: Add new 'joblib' dependency to parallelize update
The projects from manifests were updated one by one. The parallel aproach has potential to fully utilize CPU and network connection to speed up the update. Signed-off-by: Robert Gałat <[email protected]>
1 parent e5fe0ef commit 9d8c8af

File tree

3 files changed

+46
-15
lines changed

3 files changed

+46
-15
lines changed

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
'pykwalify',
4747
'setuptools',
4848
'packaging',
49+
'joblib'
4950
],
5051
python_requires='>=3.8',
5152
entry_points={'console_scripts': ('west = west.app.main:main',)},

src/west/app/project.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
from west.manifest import QUAL_MANIFEST_REV_BRANCH as QUAL_MANIFEST_REV
3131
from west.manifest import QUAL_REFS_WEST as QUAL_REFS
3232

33+
JOBLIB_PRESENT = True
34+
try:
35+
from joblib import Parallel, delayed
36+
except ModuleNotFoundError:
37+
JOBLIB_PRESENT = False
38+
3339
#
3440
# Project-related or multi-repo commands, like "init", "update",
3541
# "diff", etc.
@@ -883,6 +889,12 @@ def do_add_parser(self, parser_adder):
883889
parser.add_argument('--stats', action='store_true',
884890
help='''print performance statistics for
885891
update operations''')
892+
if JOBLIB_PRESENT:
893+
parser.add_argument('-j', '--jobs', nargs='?', const=-1,
894+
default=1, type=int, action='store',
895+
help='''Use multiple jobs to paralelize update process.
896+
Pass -1 to use all avaliable jobs.
897+
''')
886898

887899
group = parser.add_argument_group(
888900
title='local project clone caches',
@@ -1018,18 +1030,26 @@ def update_all(self):
10181030
import_flags=ImportFlag.FORCE_PROJECTS)
10191031

10201032
failed = []
1021-
for project in self.manifest.projects:
1033+
1034+
def project_update(project):
10221035
if (isinstance(project, ManifestProject) or
10231036
project.name in self.updated):
1024-
continue
1037+
return
10251038
try:
10261039
if not self.project_is_active(project):
10271040
self.dbg(f'{project.name}: skipping inactive project')
1028-
continue
1029-
self.update(project)
1041+
return
10301042
self.updated.add(project.name)
1043+
self.update(project)
10311044
except subprocess.CalledProcessError:
10321045
failed.append(project)
1046+
1047+
if not JOBLIB_PRESENT or self.args.jobs == 1:
1048+
for project in self.manifest.projects:
1049+
project_update(project)
1050+
else:
1051+
Parallel(n_jobs=self.args.jobs, require='sharedmem')(
1052+
delayed(project_update)(project) for project in self.manifest.projects)
10331053
self._handle_failed(self.args, failed)
10341054

10351055
def update_importer(self, project, path):
@@ -1090,13 +1110,22 @@ def update_some(self):
10901110
projects = self._projects(self.args.projects)
10911111

10921112
failed = []
1093-
for project in projects:
1113+
1114+
def project_update_some(project):
10941115
if isinstance(project, ManifestProject):
1095-
continue
1116+
return
10961117
try:
10971118
self.update(project)
10981119
except subprocess.CalledProcessError:
10991120
failed.append(project)
1121+
1122+
if not JOBLIB_PRESENT or self.args.jobs == 1:
1123+
for project in projects:
1124+
project_update_some(project)
1125+
else:
1126+
Parallel(n_jobs=self.args.jobs, require='sharedmem')(
1127+
delayed(project_update_some)(project) for project in projects)
1128+
11001129
self._handle_failed(self.args, failed)
11011130

11021131
def toplevel_projects(self):

tests/test_project.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -416,14 +416,14 @@ def test_grep(west_init_tmpdir):
416416

417417
assert re.search('west-commands', cmd('grep -- -- -commands'))
418418

419-
420-
def test_update_projects(west_init_tmpdir):
419+
@pytest.mark.parametrize("options", ["", "-j 1", "-j 2", "-j"])
420+
def test_update_projects(options, west_init_tmpdir):
421421
# Test the 'west update' command. It calls through to the same backend
422422
# functions that are used for automatic updates and 'west init'
423423
# reinitialization.
424424

425425
# create local repositories
426-
cmd('update')
426+
cmd('update ' + options)
427427

428428
# Add commits to the local repos.
429429
ur = update_helper(west_init_tmpdir)
@@ -613,7 +613,8 @@ def test_update_head_0(west_init_tmpdir):
613613
assert modified_files.strip() == "M CODEOWNERS", \
614614
'local zephyr change not preserved'
615615

616-
def test_update_some_with_imports(repos_tmpdir):
616+
@pytest.mark.parametrize("options", ["", "-j 1", "-j 2", "-j -1"])
617+
def test_update_some_with_imports(options, repos_tmpdir):
617618
# 'west update project1 project2' should work fine even when
618619
# imports are used, as long as the relevant projects are all
619620
# defined in the manifest repository.
@@ -654,19 +655,19 @@ def test_update_some_with_imports(repos_tmpdir):
654655
# Updating unknown projects should fail as always.
655656

656657
with pytest.raises(subprocess.CalledProcessError):
657-
cmd('update unknown-project', cwd=ws)
658+
cmd(f'update {options} unknown-project', cwd=ws)
658659

659660
# Updating a list of projects when some are resolved via project
660661
# imports must fail.
661662

662663
with pytest.raises(subprocess.CalledProcessError):
663-
cmd('update Kconfiglib net-tools', cwd=ws)
664+
cmd(f'update {options} Kconfiglib net-tools', cwd=ws)
664665

665666
# Updates of projects defined in the manifest repository or all
666667
# projects must succeed, and behave the same as if no imports
667668
# existed.
668669

669-
cmd('update net-tools', cwd=ws)
670+
cmd(f'update {options} net-tools', cwd=ws)
670671
with pytest.raises(ManifestImportFailed):
671672
Manifest.from_topdir(topdir=ws)
672673
manifest = Manifest.from_topdir(topdir=ws,
@@ -677,10 +678,10 @@ def test_update_some_with_imports(repos_tmpdir):
677678
assert net_tools_project.is_cloned()
678679
assert not zephyr_project.is_cloned()
679680

680-
cmd('update zephyr', cwd=ws)
681+
cmd(f'update {options} zephyr', cwd=ws)
681682
assert zephyr_project.is_cloned()
682683

683-
cmd('update', cwd=ws)
684+
cmd(f'update {options}', cwd=ws)
684685
manifest = Manifest.from_topdir(topdir=ws)
685686
assert manifest.get_projects(['Kconfiglib'])[0].is_cloned()
686687

0 commit comments

Comments
 (0)