Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(129)

Side by Side Diff: pylib/gyp/input.py

Issue 10911082: Load target build files in parallel using Python multiprocessing. (Closed) Base URL: http://git.chromium.org/external/gyp.git@master
Patch Set: Add lock and explicit class for global state Created 8 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « pylib/gyp/__init__.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright (c) 2012 Google Inc. All rights reserved. 1 # Copyright (c) 2012 Google Inc. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 from compiler.ast import Const 5 from compiler.ast import Const
6 from compiler.ast import Dict 6 from compiler.ast import Dict
7 from compiler.ast import Discard 7 from compiler.ast import Discard
8 from compiler.ast import List 8 from compiler.ast import List
9 from compiler.ast import Module 9 from compiler.ast import Module
10 from compiler.ast import Node 10 from compiler.ast import Node
11 from compiler.ast import Stmt 11 from compiler.ast import Stmt
12 import compiler 12 import compiler
13 import copy 13 import copy
14 import gyp.common 14 import gyp.common
15 import multiprocessing
15 import optparse 16 import optparse
16 import os.path 17 import os.path
17 import re 18 import re
18 import shlex 19 import shlex
19 import subprocess 20 import subprocess
20 import sys 21 import sys
22 import threading
23 import time
21 from gyp.common import GypError 24 from gyp.common import GypError
22 25
23 26
24 # A list of types that are treated as linkable. 27 # A list of types that are treated as linkable.
25 linkable_types = ['executable', 'shared_library', 'loadable_module'] 28 linkable_types = ['executable', 'shared_library', 'loadable_module']
26 29
27 # A list of sections that contain links to other targets. 30 # A list of sections that contain links to other targets.
28 dependency_sections = ['dependencies', 'export_dependent_settings'] 31 dependency_sections = ['dependencies', 'export_dependent_settings']
29 32
30 # base_path_sections is a list of sections defined by GYP that contain 33 # base_path_sections is a list of sections defined by GYP that contain
(...skipping 292 matching lines...) Expand 10 before | Expand all | Expand 10 after
323 for condition in data['conditions']: 326 for condition in data['conditions']:
324 if isinstance(condition, list): 327 if isinstance(condition, list):
325 for condition_dict in condition[1:]: 328 for condition_dict in condition[1:]:
326 ProcessToolsetsInDict(condition_dict) 329 ProcessToolsetsInDict(condition_dict)
327 330
328 331
329 # TODO(mark): I don't love this name. It just means that it's going to load 332 # TODO(mark): I don't love this name. It just means that it's going to load
330 # a build file that contains targets and is expected to provide a targets dict 333 # a build file that contains targets and is expected to provide a targets dict
331 # that contains the targets... 334 # that contains the targets...
332 def LoadTargetBuildFile(build_file_path, data, aux_data, variables, includes, 335 def LoadTargetBuildFile(build_file_path, data, aux_data, variables, includes,
333 depth, check): 336 depth, check, load_dependencies):
334 # If depth is set, predefine the DEPTH variable to be a relative path from 337 # If depth is set, predefine the DEPTH variable to be a relative path from
335 # this build file's directory to the directory identified by depth. 338 # this build file's directory to the directory identified by depth.
336 if depth: 339 if depth:
337 # TODO(dglazkov) The backslash/forward-slash replacement at the end is a 340 # TODO(dglazkov) The backslash/forward-slash replacement at the end is a
338 # temporary measure. This should really be addressed by keeping all paths 341 # temporary measure. This should really be addressed by keeping all paths
339 # in POSIX until actual project generation. 342 # in POSIX until actual project generation.
340 d = gyp.common.RelativePath(depth, os.path.dirname(build_file_path)) 343 d = gyp.common.RelativePath(depth, os.path.dirname(build_file_path))
341 if d == '': 344 if d == '':
342 variables['DEPTH'] = '.' 345 variables['DEPTH'] = '.'
343 else: 346 else:
344 variables['DEPTH'] = d.replace('\\', '/') 347 variables['DEPTH'] = d.replace('\\', '/')
345 348
346 # If the generator needs absolue paths, then do so. 349 # If the generator needs absolue paths, then do so.
347 if absolute_build_file_paths: 350 if absolute_build_file_paths:
348 build_file_path = os.path.abspath(build_file_path) 351 build_file_path = os.path.abspath(build_file_path)
349 352
350 if build_file_path in data['target_build_files']: 353 if build_file_path in data['target_build_files']:
351 # Already loaded. 354 # Already loaded.
352 return 355 return False
353 data['target_build_files'].add(build_file_path) 356 data['target_build_files'].add(build_file_path)
354 357
355 gyp.DebugOutput(gyp.DEBUG_INCLUDES, 358 gyp.DebugOutput(gyp.DEBUG_INCLUDES,
356 "Loading Target Build File '%s'" % build_file_path) 359 "Loading Target Build File '%s'" % build_file_path)
357 360
358 build_file_data = LoadOneBuildFile(build_file_path, data, aux_data, variables, 361 build_file_data = LoadOneBuildFile(build_file_path, data, aux_data, variables,
359 includes, True, check) 362 includes, True, check)
360 363
361 # Store DEPTH for later use in generators. 364 # Store DEPTH for later use in generators.
362 build_file_data['_DEPTH'] = depth 365 build_file_data['_DEPTH'] = depth
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after
412 index += 1 415 index += 1
413 416
414 # No longer needed. 417 # No longer needed.
415 del build_file_data['target_defaults'] 418 del build_file_data['target_defaults']
416 419
417 # Look for dependencies. This means that dependency resolution occurs 420 # Look for dependencies. This means that dependency resolution occurs
418 # after "pre" conditionals and variable expansion, but before "post" - 421 # after "pre" conditionals and variable expansion, but before "post" -
419 # in other words, you can't put a "dependencies" section inside a "post" 422 # in other words, you can't put a "dependencies" section inside a "post"
420 # conditional within a target. 423 # conditional within a target.
421 424
425 dependencies = []
422 if 'targets' in build_file_data: 426 if 'targets' in build_file_data:
423 for target_dict in build_file_data['targets']: 427 for target_dict in build_file_data['targets']:
424 if 'dependencies' not in target_dict: 428 if 'dependencies' not in target_dict:
425 continue 429 continue
426 for dependency in target_dict['dependencies']: 430 for dependency in target_dict['dependencies']:
427 other_build_file = \ 431 dependencies.append(
428 gyp.common.ResolveTarget(build_file_path, dependency, None)[0] 432 gyp.common.ResolveTarget(build_file_path, dependency, None)[0])
429 try:
430 LoadTargetBuildFile(other_build_file, data, aux_data, variables,
431 includes, depth, check)
432 except Exception, e:
433 gyp.common.ExceptionAppend(
434 e, 'while loading dependencies of %s' % build_file_path)
435 raise
436 433
437 return data 434 if load_dependencies:
435 for dependency in dependencies:
436 try:
437 LoadTargetBuildFile(dependency, data, aux_data, variables,
438 includes, depth, check, load_dependencies)
439 except Exception, e:
440 gyp.common.ExceptionAppend(
441 e, 'while loading dependencies of %s' % build_file_path)
442 raise
443 else:
444 return (build_file_path, dependencies)
445
446
447 # Wrapper around LoadTargetBuildFile used when LoadTargetBuildFile
M-A Ruel 2012/09/27 00:41:31 Why not a docstring?
dmazzoni 2012/09/28 18:05:21 Done.
448 # is executed in a worker process.
449 def CallLoadTargetBuildFile(global_flags,
450 build_file_path, data,
451 aux_data, variables,
452 includes, depth, check):
453 # Apply globals so that the worker process behaves the same.
454 for key, value in global_flags.iteritems():
455 globals()[key] = value
456
457 # Save the keys so we can return data that changed.
458 data_keys = set(data)
459 aux_data_keys = set(aux_data)
460
461 result = LoadTargetBuildFile(build_file_path, data,
462 aux_data, variables,
463 includes, depth, check, False)
464 if not result:
465 return result
466
467 (build_file_path, dependencies) = result
468
469 data_out = {}
470 for key in data:
471 if key == 'target_build_files':
472 continue
473 if key not in data_keys:
474 data_out[key] = data[key]
475 aux_data_out = {}
476 for key in aux_data:
477 if key not in aux_data_keys:
478 aux_data_out[key] = aux_data[key]
479
480 # This gets serialized and sent back to the main process via a pipe.
481 # It's handled in LoadTargetBuildFileCallback.
482 return (build_file_path,
483 data_out,
484 aux_data_out,
485 dependencies)
486
487
488 # If build files are loaded in parallel, use this to keep track of
M-A Ruel 2012/09/27 00:41:31 Same for all file level symbols with comments.
dmazzoni 2012/09/28 18:05:21 Done.
489 # state during farming out and processing parallel jobs. It's stored
490 # in a global so that the callback function can have access to it.
491 class ParallelState:
M-A Ruel 2012/09/27 00:41:31 class ParallelState(object):
dmazzoni 2012/09/28 18:05:21 Done.
492 def __init__(self):
493 # The multiprocessing pool.
494 self.pool = None
495 # The condition variable used to protect this object and notify
496 # the main loop when there might be more data to process.
497 self.condition = None
498 # The "data" dict that was passed to LoadTargetBuildFileParallel
499 self.data = None
500 # The "aux_data" dict that was passed to LoadTargetBuildFileParallel
501 self.aux_data = None
502 # The number of parallel calls outstanding; decremented when a response
503 # was received.
504 self.pending = 0
505 # The set of all build files that have been scheduled, so we don't
506 # schedule the same one twice.
507 self.scheduled = set()
508 # A list of dependency build file paths that haven't been scheduled yet.
509 self.dependencies = []
510
511
512 # Handle the results of LoadTargetBuildFile that executed in a separate
513 # process.
514 def LoadTargetBuildFileCallback(result):
M-A Ruel 2012/09/27 00:41:31 If you had put it a member function of ParallelSta
dmazzoni 2012/09/28 18:05:21 Good idea, done.
515 (build_file_path0, data0, aux_data0, dependencies0) = result
516 global parallel_state
517 parallel_state.condition.acquire()
518 parallel_state.data['target_build_files'].add(build_file_path0)
519 for key in data0:
520 parallel_state.data[key] = data0[key]
521 for key in aux_data0:
522 parallel_state.aux_data[key] = aux_data0[key]
523 for new_dependency in dependencies0:
524 if new_dependency not in parallel_state.scheduled:
525 parallel_state.scheduled.add(new_dependency)
526 parallel_state.dependencies.append(new_dependency)
527 parallel_state.pending -= 1
528 parallel_state.condition.notify()
529 parallel_state.condition.release()
530
531
532 def LoadTargetBuildFileParallel(build_file_path, data, aux_data,
533 variables, includes, depth, check):
534 global parallel_state
535 parallel_state = ParallelState()
536 parallel_state.condition = threading.Condition()
537 parallel_state.dependencies = [build_file_path]
538 parallel_state.scheduled = set([build_file_path])
539 parallel_state.pending = 0
540 parallel_state.data = data
541 parallel_state.aux_data = aux_data
542
543 parallel_state.condition.acquire()
544 while parallel_state.dependencies or parallel_state.pending:
545 if not parallel_state.dependencies:
546 parallel_state.condition.wait()
547 continue
548
549 dependency = parallel_state.dependencies.pop()
550
551 parallel_state.pending += 1
552 data_in = {}
553 data_in['target_build_files'] = data['target_build_files']
554 aux_data_in = {}
555 global_flags = {
556 'path_sections': globals()['path_sections'],
557 'non_configuration_keys': globals()['non_configuration_keys'],
558 'absolute_build_file_paths': globals()['absolute_build_file_paths'],
559 'multiple_toolsets': globals()['multiple_toolsets']}
560
561 if not parallel_state.pool:
562 parallel_state.pool = multiprocessing.Pool(8)
563 parallel_state.pool.apply_async(
564 CallLoadTargetBuildFile,
565 args = (global_flags, dependency,
566 data_in, aux_data_in,
567 variables, includes, depth, check),
568 callback = LoadTargetBuildFileCallback)
569
570 parallel_state.condition.release()
438 571
439 572
440 # Look for the bracket that matches the first bracket seen in a 573 # Look for the bracket that matches the first bracket seen in a
441 # string, and return the start and end as a tuple. For example, if 574 # string, and return the start and end as a tuple. For example, if
442 # the input is something like "<(foo <(bar)) blah", then it would 575 # the input is something like "<(foo <(bar)) blah", then it would
443 # return (1, 13), indicating the entire string except for the leading 576 # return (1, 13), indicating the entire string except for the leading
444 # "<" and trailing " blah". 577 # "<" and trailing " blah".
445 def FindEnclosingBracketGroup(input): 578 def FindEnclosingBracketGroup(input):
446 brackets = { '}': '{', 579 brackets = { '}': '{',
447 ']': '[', 580 ']': '[',
(...skipping 1880 matching lines...) Expand 10 before | Expand all | Expand 10 after
2328 # Prepare a key like 'path/to:target_name'. 2461 # Prepare a key like 'path/to:target_name'.
2329 key = subdir + ':' + name 2462 key = subdir + ':' + name
2330 if key in used: 2463 if key in used:
2331 # Complain if this target is already used. 2464 # Complain if this target is already used.
2332 raise GypError('Duplicate target name "%s" in directory "%s" used both ' 2465 raise GypError('Duplicate target name "%s" in directory "%s" used both '
2333 'in "%s" and "%s".' % (name, subdir, gyp, used[key])) 2466 'in "%s" and "%s".' % (name, subdir, gyp, used[key]))
2334 used[key] = gyp 2467 used[key] = gyp
2335 2468
2336 2469
2337 def Load(build_files, variables, includes, depth, generator_input_info, check, 2470 def Load(build_files, variables, includes, depth, generator_input_info, check,
2338 circular_check): 2471 circular_check, parallel):
2339 # Set up path_sections and non_configuration_keys with the default data plus 2472 # Set up path_sections and non_configuration_keys with the default data plus
2340 # the generator-specifc data. 2473 # the generator-specifc data.
2341 global path_sections 2474 global path_sections
2342 path_sections = base_path_sections[:] 2475 path_sections = base_path_sections[:]
2343 path_sections.extend(generator_input_info['path_sections']) 2476 path_sections.extend(generator_input_info['path_sections'])
2344 2477
2345 global non_configuration_keys 2478 global non_configuration_keys
2346 non_configuration_keys = base_non_configuration_keys[:] 2479 non_configuration_keys = base_non_configuration_keys[:]
2347 non_configuration_keys.extend(generator_input_info['non_configuration_keys']) 2480 non_configuration_keys.extend(generator_input_info['non_configuration_keys'])
2348 2481
(...skipping 20 matching lines...) Expand all
2369 # NOTE: data contains both "target" files (.gyp) and "includes" (.gypi), as 2502 # NOTE: data contains both "target" files (.gyp) and "includes" (.gypi), as
2370 # well as meta-data (e.g. 'included_files' key). 'target_build_files' keeps 2503 # well as meta-data (e.g. 'included_files' key). 'target_build_files' keeps
2371 # track of the keys corresponding to "target" files. 2504 # track of the keys corresponding to "target" files.
2372 data = {'target_build_files': set()} 2505 data = {'target_build_files': set()}
2373 aux_data = {} 2506 aux_data = {}
2374 for build_file in build_files: 2507 for build_file in build_files:
2375 # Normalize paths everywhere. This is important because paths will be 2508 # Normalize paths everywhere. This is important because paths will be
2376 # used as keys to the data dict and for references between input files. 2509 # used as keys to the data dict and for references between input files.
2377 build_file = os.path.normpath(build_file) 2510 build_file = os.path.normpath(build_file)
2378 try: 2511 try:
2379 LoadTargetBuildFile(build_file, data, aux_data, variables, includes, 2512 if parallel:
2380 depth, check) 2513 print >>sys.stderr, 'Using parallel processing (experimental).'
2514 LoadTargetBuildFileParallel(build_file, data, aux_data,
2515 variables, includes, depth, check)
2516 else:
2517 LoadTargetBuildFile(build_file, data, aux_data,
2518 variables, includes, depth, check, True)
2381 except Exception, e: 2519 except Exception, e:
2382 gyp.common.ExceptionAppend(e, 'while trying to load %s' % build_file) 2520 gyp.common.ExceptionAppend(e, 'while trying to load %s' % build_file)
2383 raise 2521 raise
2384 2522
2385 # Build a dict to access each target's subdict by qualified name. 2523 # Build a dict to access each target's subdict by qualified name.
2386 targets = BuildTargetsDict(data) 2524 targets = BuildTargetsDict(data)
2387 2525
2388 # Fully qualify all dependency links. 2526 # Fully qualify all dependency links.
2389 QualifyDependencies(targets) 2527 QualifyDependencies(targets)
2390 2528
(...skipping 87 matching lines...) Expand 10 before | Expand all | Expand 10 after
2478 ValidateRunAsInTarget(target, target_dict, build_file) 2616 ValidateRunAsInTarget(target, target_dict, build_file)
2479 ValidateActionsInTarget(target, target_dict, build_file) 2617 ValidateActionsInTarget(target, target_dict, build_file)
2480 2618
2481 # Generators might not expect ints. Turn them into strs. 2619 # Generators might not expect ints. Turn them into strs.
2482 TurnIntIntoStrInDict(data) 2620 TurnIntIntoStrInDict(data)
2483 2621
2484 # TODO(mark): Return |data| for now because the generator needs a list of 2622 # TODO(mark): Return |data| for now because the generator needs a list of
2485 # build files that came in. In the future, maybe it should just accept 2623 # build files that came in. In the future, maybe it should just accept
2486 # a list, and not the whole data dict. 2624 # a list, and not the whole data dict.
2487 return [flat_list, targets, data] 2625 return [flat_list, targets, data]
OLDNEW
« no previous file with comments | « pylib/gyp/__init__.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698