diff options
Diffstat (limited to 'command.py')
-rw-r--r-- | command.py | 137 |
1 files changed, 122 insertions, 15 deletions
@@ -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 | ||
15 | import multiprocessing | ||
17 | import os | 16 | import os |
18 | import optparse | 17 | import optparse |
19 | import platform | ||
20 | import re | 18 | import re |
21 | import sys | 19 | import sys |
22 | 20 | ||
23 | from event_log import EventLog | 21 | from event_log import EventLog |
24 | from error import NoSuchProjectError | 22 | from error import NoSuchProjectError |
25 | from error import InvalidProjectGroupsError | 23 | from error import InvalidProjectGroupsError |
24 | import progress | ||
25 | |||
26 | |||
27 | # Are we generating man-pages? | ||
28 | GENERATE_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. | ||
37 | WORKER_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. | ||
42 | DEFAULT_LOCAL_JOBS = min(os.cpu_count(), 8) | ||
26 | 43 | ||
27 | 44 | ||
28 | class Command(object): | 45 | class 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 | ||