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

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: Switch to docstrings, move LoadTargetBuildFileCallback inside ParallelState. Created 8 years, 2 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 def CallLoadTargetBuildFile(global_flags,
448 build_file_path, data,
449 aux_data, variables,
450 includes, depth, check):
451 """Wrapper around LoadTargetBuildFile for parallel processing.
452
453 This wrapper is used when LoadTargetBuildFile is executed in
454 a worker process.
455 """
456
457 # Apply globals so that the worker process behaves the same.
458 for key, value in global_flags.iteritems():
459 globals()[key] = value
460
461 # Save the keys so we can return data that changed.
462 data_keys = set(data)
463 aux_data_keys = set(aux_data)
464
465 result = LoadTargetBuildFile(build_file_path, data,
466 aux_data, variables,
467 includes, depth, check, False)
468 if not result:
469 return result
470
471 (build_file_path, dependencies) = result
472
473 data_out = {}
474 for key in data:
475 if key == 'target_build_files':
476 continue
477 if key not in data_keys:
478 data_out[key] = data[key]
479 aux_data_out = {}
480 for key in aux_data:
481 if key not in aux_data_keys:
482 aux_data_out[key] = aux_data[key]
483
484 # This gets serialized and sent back to the main process via a pipe.
485 # It's handled in LoadTargetBuildFileCallback.
486 return (build_file_path,
487 data_out,
488 aux_data_out,
489 dependencies)
490
491
492 class ParallelState(object):
493 """Class to keep track of state when processing input files in parallel.
494
495 If build files are loaded in parallel, use this to keep track of
496 state during farming out and processing parallel jobs. It's stored
497 in a global so that the callback function can have access to it.
498 """
499
500 def __init__(self):
501 # The multiprocessing pool.
502 self.pool = None
503 # The condition variable used to protect this object and notify
504 # the main loop when there might be more data to process.
505 self.condition = None
506 # The "data" dict that was passed to LoadTargetBuildFileParallel
507 self.data = None
508 # The "aux_data" dict that was passed to LoadTargetBuildFileParallel
509 self.aux_data = None
510 # The number of parallel calls outstanding; decremented when a response
511 # was received.
512 self.pending = 0
513 # The set of all build files that have been scheduled, so we don't
514 # schedule the same one twice.
515 self.scheduled = set()
516 # A list of dependency build file paths that haven't been scheduled yet.
517 self.dependencies = []
518
519 def LoadTargetBuildFileCallback(self, result):
520 """Handle the results of running LoadTargetBuildFile in another process.
521 """
522 (build_file_path0, data0, aux_data0, dependencies0) = result
523 self.condition.acquire()
524 self.data['target_build_files'].add(build_file_path0)
525 for key in data0:
526 self.data[key] = data0[key]
527 for key in aux_data0:
528 self.aux_data[key] = aux_data0[key]
529 for new_dependency in dependencies0:
530 if new_dependency not in self.scheduled:
531 self.scheduled.add(new_dependency)
532 self.dependencies.append(new_dependency)
533 self.pending -= 1
534 self.condition.notify()
535 self.condition.release()
536
537
538 def LoadTargetBuildFileParallel(build_file_path, data, aux_data,
539 variables, includes, depth, check):
540 global parallel_state
541 parallel_state = ParallelState()
542 parallel_state.condition = threading.Condition()
543 parallel_state.dependencies = [build_file_path]
544 parallel_state.scheduled = set([build_file_path])
545 parallel_state.pending = 0
546 parallel_state.data = data
547 parallel_state.aux_data = aux_data
548
549 parallel_state.condition.acquire()
550 while parallel_state.dependencies or parallel_state.pending:
551 if not parallel_state.dependencies:
552 parallel_state.condition.wait()
553 continue
554
555 dependency = parallel_state.dependencies.pop()
556
557 parallel_state.pending += 1
558 data_in = {}
559 data_in['target_build_files'] = data['target_build_files']
560 aux_data_in = {}
561 global_flags = {
562 'path_sections': globals()['path_sections'],
563 'non_configuration_keys': globals()['non_configuration_keys'],
564 'absolute_build_file_paths': globals()['absolute_build_file_paths'],
565 'multiple_toolsets': globals()['multiple_toolsets']}
566
567 if not parallel_state.pool:
568 parallel_state.pool = multiprocessing.Pool(8)
569 parallel_state.pool.apply_async(
570 CallLoadTargetBuildFile,
571 args = (global_flags, dependency,
572 data_in, aux_data_in,
573 variables, includes, depth, check),
574 callback = parallel_state.LoadTargetBuildFileCallback)
575
576 parallel_state.condition.release()
438 577
439 578
440 # Look for the bracket that matches the first bracket seen in a 579 # 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 580 # 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 581 # the input is something like "<(foo <(bar)) blah", then it would
443 # return (1, 13), indicating the entire string except for the leading 582 # return (1, 13), indicating the entire string except for the leading
444 # "<" and trailing " blah". 583 # "<" and trailing " blah".
445 def FindEnclosingBracketGroup(input): 584 def FindEnclosingBracketGroup(input):
446 brackets = { '}': '{', 585 brackets = { '}': '{',
447 ']': '[', 586 ']': '[',
(...skipping 1880 matching lines...) Expand 10 before | Expand all | Expand 10 after
2328 # Prepare a key like 'path/to:target_name'. 2467 # Prepare a key like 'path/to:target_name'.
2329 key = subdir + ':' + name 2468 key = subdir + ':' + name
2330 if key in used: 2469 if key in used:
2331 # Complain if this target is already used. 2470 # Complain if this target is already used.
2332 raise GypError('Duplicate target name "%s" in directory "%s" used both ' 2471 raise GypError('Duplicate target name "%s" in directory "%s" used both '
2333 'in "%s" and "%s".' % (name, subdir, gyp, used[key])) 2472 'in "%s" and "%s".' % (name, subdir, gyp, used[key]))
2334 used[key] = gyp 2473 used[key] = gyp
2335 2474
2336 2475
2337 def Load(build_files, variables, includes, depth, generator_input_info, check, 2476 def Load(build_files, variables, includes, depth, generator_input_info, check,
2338 circular_check): 2477 circular_check, parallel):
2339 # Set up path_sections and non_configuration_keys with the default data plus 2478 # Set up path_sections and non_configuration_keys with the default data plus
2340 # the generator-specifc data. 2479 # the generator-specifc data.
2341 global path_sections 2480 global path_sections
2342 path_sections = base_path_sections[:] 2481 path_sections = base_path_sections[:]
2343 path_sections.extend(generator_input_info['path_sections']) 2482 path_sections.extend(generator_input_info['path_sections'])
2344 2483
2345 global non_configuration_keys 2484 global non_configuration_keys
2346 non_configuration_keys = base_non_configuration_keys[:] 2485 non_configuration_keys = base_non_configuration_keys[:]
2347 non_configuration_keys.extend(generator_input_info['non_configuration_keys']) 2486 non_configuration_keys.extend(generator_input_info['non_configuration_keys'])
2348 2487
(...skipping 20 matching lines...) Expand all
2369 # NOTE: data contains both "target" files (.gyp) and "includes" (.gypi), as 2508 # 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 2509 # well as meta-data (e.g. 'included_files' key). 'target_build_files' keeps
2371 # track of the keys corresponding to "target" files. 2510 # track of the keys corresponding to "target" files.
2372 data = {'target_build_files': set()} 2511 data = {'target_build_files': set()}
2373 aux_data = {} 2512 aux_data = {}
2374 for build_file in build_files: 2513 for build_file in build_files:
2375 # Normalize paths everywhere. This is important because paths will be 2514 # Normalize paths everywhere. This is important because paths will be
2376 # used as keys to the data dict and for references between input files. 2515 # used as keys to the data dict and for references between input files.
2377 build_file = os.path.normpath(build_file) 2516 build_file = os.path.normpath(build_file)
2378 try: 2517 try:
2379 LoadTargetBuildFile(build_file, data, aux_data, variables, includes, 2518 if parallel:
2380 depth, check) 2519 print >>sys.stderr, 'Using parallel processing (experimental).'
2520 LoadTargetBuildFileParallel(build_file, data, aux_data,
2521 variables, includes, depth, check)
2522 else:
2523 LoadTargetBuildFile(build_file, data, aux_data,
2524 variables, includes, depth, check, True)
2381 except Exception, e: 2525 except Exception, e:
2382 gyp.common.ExceptionAppend(e, 'while trying to load %s' % build_file) 2526 gyp.common.ExceptionAppend(e, 'while trying to load %s' % build_file)
2383 raise 2527 raise
2384 2528
2385 # Build a dict to access each target's subdict by qualified name. 2529 # Build a dict to access each target's subdict by qualified name.
2386 targets = BuildTargetsDict(data) 2530 targets = BuildTargetsDict(data)
2387 2531
2388 # Fully qualify all dependency links. 2532 # Fully qualify all dependency links.
2389 QualifyDependencies(targets) 2533 QualifyDependencies(targets)
2390 2534
(...skipping 87 matching lines...) Expand 10 before | Expand all | Expand 10 after
2478 ValidateRunAsInTarget(target, target_dict, build_file) 2622 ValidateRunAsInTarget(target, target_dict, build_file)
2479 ValidateActionsInTarget(target, target_dict, build_file) 2623 ValidateActionsInTarget(target, target_dict, build_file)
2480 2624
2481 # Generators might not expect ints. Turn them into strs. 2625 # Generators might not expect ints. Turn them into strs.
2482 TurnIntIntoStrInDict(data) 2626 TurnIntIntoStrInDict(data)
2483 2627
2484 # TODO(mark): Return |data| for now because the generator needs a list of 2628 # 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 2629 # build files that came in. In the future, maybe it should just accept
2486 # a list, and not the whole data dict. 2630 # a list, and not the whole data dict.
2487 return [flat_list, targets, data] 2631 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