summaryrefslogtreecommitdiffstats
path: root/command.py
diff options
context:
space:
mode:
Diffstat (limited to 'command.py')
-rw-r--r--command.py137
1 files changed, 122 insertions, 15 deletions
diff --git a/command.py b/command.py
index 9e113f1a..b972a0be 100644
--- a/command.py
+++ b/command.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,25 +12,65 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
15import multiprocessing
17import os 16import os
18import optparse 17import optparse
19import platform
20import re 18import re
21import sys 19import sys
22 20
23from event_log import EventLog 21from event_log import EventLog
24from error import NoSuchProjectError 22from error import NoSuchProjectError
25from error import InvalidProjectGroupsError 23from error import InvalidProjectGroupsError
24import progress
25
26
27# Are we generating man-pages?
28GENERATE_MANPAGES = os.environ.get('_REPO_GENERATE_MANPAGES_') == ' indeed! '
29
30
31# Number of projects to submit to a single worker process at a time.
32# This number represents a tradeoff between the overhead of IPC and finer
33# grained opportunity for parallelism. This particular value was chosen by
34# iterating through powers of two until the overall performance no longer
35# improved. The performance of this batch size is not a function of the
36# number of cores on the system.
37WORKER_BATCH_SIZE = 32
38
39
40# How many jobs to run in parallel by default? This assumes the jobs are
41# largely I/O bound and do not hit the network.
42DEFAULT_LOCAL_JOBS = min(os.cpu_count(), 8)
26 43
27 44
28class Command(object): 45class Command(object):
29 """Base class for any command line action in repo. 46 """Base class for any command line action in repo.
30 """ 47 """
31 48
32 common = False 49 # Singleton for all commands to track overall repo command execution and
50 # provide event summary to callers. Only used by sync subcommand currently.
51 #
52 # NB: This is being replaced by git trace2 events. See git_trace2_event_log.
33 event_log = EventLog() 53 event_log = EventLog()
34 manifest = None 54
35 _optparse = None 55 # Whether this command is a "common" one, i.e. whether the user would commonly
56 # use it or it's a more uncommon command. This is used by the help command to
57 # show short-vs-full summaries.
58 COMMON = False
59
60 # Whether this command supports running in parallel. If greater than 0,
61 # it is the number of parallel jobs to default to.
62 PARALLEL_JOBS = None
63
64 def __init__(self, repodir=None, client=None, manifest=None, gitc_manifest=None,
65 git_event_log=None):
66 self.repodir = repodir
67 self.client = client
68 self.manifest = manifest
69 self.gitc_manifest = gitc_manifest
70 self.git_event_log = git_event_log
71
72 # Cache for the OptionParser property.
73 self._optparse = None
36 74
37 def WantPager(self, _opt): 75 def WantPager(self, _opt):
38 return False 76 return False
@@ -66,13 +104,39 @@ class Command(object):
66 usage = self.helpUsage.strip().replace('%prog', me) 104 usage = self.helpUsage.strip().replace('%prog', me)
67 except AttributeError: 105 except AttributeError:
68 usage = 'repo %s' % self.NAME 106 usage = 'repo %s' % self.NAME
69 self._optparse = optparse.OptionParser(usage=usage) 107 epilog = 'Run `repo help %s` to view the detailed manual.' % self.NAME
108 self._optparse = optparse.OptionParser(usage=usage, epilog=epilog)
109 self._CommonOptions(self._optparse)
70 self._Options(self._optparse) 110 self._Options(self._optparse)
71 return self._optparse 111 return self._optparse
72 112
73 def _Options(self, p): 113 def _CommonOptions(self, p, opt_v=True):
74 """Initialize the option parser. 114 """Initialize the option parser with common options.
115
116 These will show up for *all* subcommands, so use sparingly.
117 NB: Keep in sync with repo:InitParser().
75 """ 118 """
119 g = p.add_option_group('Logging options')
120 opts = ['-v'] if opt_v else []
121 g.add_option(*opts, '--verbose',
122 dest='output_mode', action='store_true',
123 help='show all output')
124 g.add_option('-q', '--quiet',
125 dest='output_mode', action='store_false',
126 help='only show errors')
127
128 if self.PARALLEL_JOBS is not None:
129 default = 'based on number of CPU cores'
130 if not GENERATE_MANPAGES:
131 # Only include active cpu count if we aren't generating man pages.
132 default = f'%default; {default}'
133 p.add_option(
134 '-j', '--jobs',
135 type=int, default=self.PARALLEL_JOBS,
136 help=f'number of jobs to run in parallel (default: {default})')
137
138 def _Options(self, p):
139 """Initialize the option parser with subcommand-specific options."""
76 140
77 def _RegisteredEnvironmentOptions(self): 141 def _RegisteredEnvironmentOptions(self):
78 """Get options that can be set from environment variables. 142 """Get options that can be set from environment variables.
@@ -98,6 +162,11 @@ class Command(object):
98 self.OptionParser.print_usage() 162 self.OptionParser.print_usage()
99 sys.exit(1) 163 sys.exit(1)
100 164
165 def CommonValidateOptions(self, opt, args):
166 """Validate common options."""
167 opt.quiet = opt.output_mode is False
168 opt.verbose = opt.output_mode is True
169
101 def ValidateOptions(self, opt, args): 170 def ValidateOptions(self, opt, args):
102 """Validate the user options & arguments before executing. 171 """Validate the user options & arguments before executing.
103 172
@@ -113,6 +182,44 @@ class Command(object):
113 """ 182 """
114 raise NotImplementedError 183 raise NotImplementedError
115 184
185 @staticmethod
186 def ExecuteInParallel(jobs, func, inputs, callback, output=None, ordered=False):
187 """Helper for managing parallel execution boiler plate.
188
189 For subcommands that can easily split their work up.
190
191 Args:
192 jobs: How many parallel processes to use.
193 func: The function to apply to each of the |inputs|. Usually a
194 functools.partial for wrapping additional arguments. It will be run
195 in a separate process, so it must be pickalable, so nested functions
196 won't work. Methods on the subcommand Command class should work.
197 inputs: The list of items to process. Must be a list.
198 callback: The function to pass the results to for processing. It will be
199 executed in the main thread and process the results of |func| as they
200 become available. Thus it may be a local nested function. Its return
201 value is passed back directly. It takes three arguments:
202 - The processing pool (or None with one job).
203 - The |output| argument.
204 - An iterator for the results.
205 output: An output manager. May be progress.Progess or color.Coloring.
206 ordered: Whether the jobs should be processed in order.
207
208 Returns:
209 The |callback| function's results are returned.
210 """
211 try:
212 # NB: Multiprocessing is heavy, so don't spin it up for one job.
213 if len(inputs) == 1 or jobs == 1:
214 return callback(None, output, (func(x) for x in inputs))
215 else:
216 with multiprocessing.Pool(jobs) as pool:
217 submit = pool.imap if ordered else pool.imap_unordered
218 return callback(pool, output, submit(func, inputs, chunksize=WORKER_BATCH_SIZE))
219 finally:
220 if isinstance(output, progress.Progress):
221 output.end()
222
116 def _ResetPathToProjectMap(self, projects): 223 def _ResetPathToProjectMap(self, projects):
117 self._by_path = dict((p.worktree, p) for p in projects) 224 self._by_path = dict((p.worktree, p) for p in projects)
118 225
@@ -123,9 +230,9 @@ class Command(object):
123 project = None 230 project = None
124 if os.path.exists(path): 231 if os.path.exists(path):
125 oldpath = None 232 oldpath = None
126 while path and \ 233 while (path and
127 path != oldpath and \ 234 path != oldpath and
128 path != manifest.topdir: 235 path != manifest.topdir):
129 try: 236 try:
130 project = self._by_path[path] 237 project = self._by_path[path]
131 break 238 break
@@ -156,9 +263,7 @@ class Command(object):
156 mp = manifest.manifestProject 263 mp = manifest.manifestProject
157 264
158 if not groups: 265 if not groups:
159 groups = mp.config.GetString('manifest.groups') 266 groups = manifest.GetGroupsStr()
160 if not groups:
161 groups = 'default,platform-' + platform.system().lower()
162 groups = [x for x in re.split(r'[,\s]+', groups) if x] 267 groups = [x for x in re.split(r'[,\s]+', groups) if x]
163 268
164 if not args: 269 if not args:
@@ -236,6 +341,7 @@ class InteractiveCommand(Command):
236 """Command which requires user interaction on the tty and 341 """Command which requires user interaction on the tty and
237 must not run within a pager, even if the user asks to. 342 must not run within a pager, even if the user asks to.
238 """ 343 """
344
239 def WantPager(self, _opt): 345 def WantPager(self, _opt):
240 return False 346 return False
241 347
@@ -244,6 +350,7 @@ class PagedCommand(Command):
244 """Command which defaults to output in a pager, as its 350 """Command which defaults to output in a pager, as its
245 display tends to be larger than one screen full. 351 display tends to be larger than one screen full.
246 """ 352 """
353
247 def WantPager(self, _opt): 354 def WantPager(self, _opt):
248 return True 355 return True
249 356