Parallelize GenerateBuildActions

Modify the Generate phase to call GenerateBuildActions inside a
goroutine to allow parallization.  All modules from a single
moduleGroup are called from a single goroutine, and the goroutines
that processed all dependencies must have finished, so the only
change to the semantics of GenerateBuildActions implementations
is around access to globals.  Locks are added to the lazily
created package context variables and to the live tracker.

Reduces run time by ~33%.

Change-Id: I62a515acf86f1dcecb093ded83444b920ff603be
This commit is contained in:
Colin Cross 2015-01-07 18:08:56 -08:00
parent 7ad621c2b4
commit 691a60dd98
5 changed files with 136 additions and 23 deletions

View file

@ -102,8 +102,15 @@ type moduleGroup struct {
modules []*moduleInfo
// set during updateDependencies
reverseDeps []*moduleGroup
depsCount int
// set during PrepareBuildActions
actionDefs localBuildActions
// used by parallelVisitAllBottomUp
waitingCount int
}
type moduleInfo struct {
@ -821,7 +828,7 @@ func (c *Context) ResolveDependencies(config interface{}) []error {
return errs
}
errs = c.rebuildSortedModuleList()
errs = c.updateDependencies()
if len(errs) > 0 {
return errs
}
@ -935,12 +942,53 @@ func (c *Context) addDependency(module *moduleInfo, depName string) []error {
return nil
}
// rebuildSortedModuleList recursively walks the module dependency graph and
// builds a sorted list of modules such that dependencies of a module always
// appear first. It also reports errors when it encounters dependency cycles.
// This should called after resolveDependencies, as well as after any mutator
// pass has called addDependency
func (c *Context) rebuildSortedModuleList() (errs []error) {
func (c *Context) parallelVisitAllBottomUp(visit func(group *moduleGroup)) {
doneCh := make(chan *moduleGroup)
count := 0
for _, group := range c.moduleGroupsSorted {
group.waitingCount = group.depsCount
}
visitOne := func(group *moduleGroup) {
count++
go func() {
visit(group)
doneCh <- group
}()
}
for _, group := range c.moduleGroupsSorted {
if group.waitingCount == 0 {
visitOne(group)
}
}
loop:
for {
select {
case doneGroup := <-doneCh:
for _, parent := range doneGroup.reverseDeps {
parent.waitingCount--
if parent.waitingCount == 0 {
visitOne(parent)
}
}
count--
if count == 0 {
break loop
}
}
}
}
// updateDependencies recursively walks the module dependency graph and updates
// additional fields based on the dependencies. It builds a sorted list of modules
// such that dependencies of a module always appear first, and populates reverse
// dependency links and counts of total dependencies. It also reports errors when
// it encounters dependency cycles. This should called after resolveDependencies,
// as well as after any mutator pass has called addDependency
func (c *Context) updateDependencies() (errs []error) {
visited := make(map[*moduleGroup]bool) // modules that were already checked
checking := make(map[*moduleGroup]bool) // modules actively being checked
@ -960,6 +1008,9 @@ func (c *Context) rebuildSortedModuleList() (errs []error) {
}
}
group.reverseDeps = []*moduleGroup{}
group.depsCount = len(deps)
for dep := range deps {
if checking[dep] {
// This is a cycle.
@ -1003,6 +1054,8 @@ func (c *Context) rebuildSortedModuleList() (errs []error) {
}
}
}
dep.reverseDeps = append(dep.reverseDeps, group)
}
sorted = append(sorted, group)
@ -1183,7 +1236,7 @@ func (c *Context) runBottomUpMutator(config interface{},
}
if dependenciesModified {
errs = c.rebuildSortedModuleList()
errs = c.updateDependencies()
if len(errs) > 0 {
return errs
}
@ -1205,7 +1258,26 @@ func (c *Context) generateModuleBuildActions(config interface{},
var deps []string
var errs []error
for _, group := range c.moduleGroupsSorted {
cancelCh := make(chan struct{})
errsCh := make(chan []error)
depsCh := make(chan []string)
go func() {
for {
select {
case <-cancelCh:
close(cancelCh)
return
case newErrs := <-errsCh:
errs = append(errs, newErrs...)
case newDeps := <-depsCh:
deps = append(deps, newDeps...)
}
}
}()
c.parallelVisitAllBottomUp(func(group *moduleGroup) {
// The parent scope of the moduleContext's local scope gets overridden to be that of the
// calling Go package on a per-call basis. Since the initial parent scope doesn't matter we
// just set it to nil.
@ -1225,20 +1297,23 @@ func (c *Context) generateModuleBuildActions(config interface{},
mctx.module.logicModule.GenerateBuildActions(mctx)
if len(mctx.errs) > 0 {
errs = append(errs, mctx.errs...)
errsCh <- mctx.errs
break
}
deps = append(deps, mctx.ninjaFileDeps...)
depsCh <- mctx.ninjaFileDeps
newErrs := c.processLocalBuildActions(&group.actionDefs,
&mctx.actionDefs, liveGlobals)
if len(newErrs) > 0 {
errs = append(errs, newErrs...)
errsCh <- newErrs
break
}
}
}
})
cancelCh <- struct{}{}
<-cancelCh
return deps, errs
}

View file

@ -75,7 +75,7 @@ func TestContextParse(t *testing.T) {
t.FailNow()
}
errs = ctx.rebuildSortedModuleList()
errs = ctx.updateDependencies()
if len(errs) > 0 {
t.Errorf("unexpected dep cycle errors:")
for _, err := range errs {

View file

@ -1,10 +1,13 @@
package blueprint
import "sync"
// A liveTracker tracks the values of live variables, rules, and pools. An
// entity is made "live" when it is referenced directly or indirectly by a build
// definition. When an entity is made live its value is computed based on the
// configuration.
type liveTracker struct {
sync.Mutex
config interface{} // Used to evaluate variable, rule, and pool values.
variables map[Variable]*ninjaString
@ -22,6 +25,9 @@ func newLiveTracker(config interface{}) *liveTracker {
}
func (l *liveTracker) AddBuildDefDeps(def *buildDef) error {
l.Lock()
defer l.Unlock()
err := l.addRule(def.Rule)
if err != nil {
return err

View file

@ -61,6 +61,13 @@ import (
//
// to build the list of library file names that should be included in its link
// command.
//
// GenerateBuildActions may be called from multiple threads. It is guaranteed to
// be called after it has finished being called on all dependencies and on all
// variants of that appear earlier in the ModuleContext.VisitAllModuleVariants list.
// Any accesses to global variables or to Module objects that are not dependencies
// or variants of the current Module must be synchronized by the implementation of
// GenerateBuildActions.
type Module interface {
// GenerateBuildActions is called by the Context that created the Module
// during its generate phase. This call should generate all Ninja build

View file

@ -6,6 +6,7 @@ import (
"reflect"
"runtime"
"strings"
"sync"
)
// A PackageContext provides a way to create package-scoped Ninja pools,
@ -543,11 +544,12 @@ func (p *poolFunc) String() string {
}
type staticRule struct {
pctx *PackageContext
name_ string
params RuleParams
argNames map[string]bool
scope_ *basicScope
pctx *PackageContext
name_ string
params RuleParams
argNames map[string]bool
scope_ *basicScope
sync.Mutex // protects scope_ during lazy creation
}
// StaticRule returns a Rule whose value does not depend on any configuration
@ -590,7 +592,13 @@ func (p *PackageContext) StaticRule(name string, params RuleParams,
ruleScope := (*basicScope)(nil) // This will get created lazily
r := &staticRule{p, name, params, argNamesSet, ruleScope}
r := &staticRule{
pctx: p,
name_: name,
params: params,
argNames: argNamesSet,
scope_: ruleScope,
}
err = p.scope.AddRule(r)
if err != nil {
panic(err)
@ -623,6 +631,9 @@ func (r *staticRule) scope() *basicScope {
// We lazily create the scope so that all the package-scoped variables get
// declared before the args are created. Otherwise we could incorrectly
// shadow a package-scoped variable with an arg variable.
r.Lock()
defer r.Unlock()
if r.scope_ == nil {
r.scope_ = makeRuleScope(r.pctx.scope, r.argNames)
}
@ -643,6 +654,7 @@ type ruleFunc struct {
paramsFunc func(interface{}) (RuleParams, error)
argNames map[string]bool
scope_ *basicScope
sync.Mutex // protects scope_ during lazy creation
}
// RuleFunc returns a Rule whose value is determined by a function that takes a
@ -686,7 +698,13 @@ func (p *PackageContext) RuleFunc(name string, f func(interface{}) (RuleParams,
ruleScope := (*basicScope)(nil) // This will get created lazily
rule := &ruleFunc{p, name, f, argNamesSet, ruleScope}
rule := &ruleFunc{
pctx: p,
name_: name,
paramsFunc: f,
argNames: argNamesSet,
scope_: ruleScope,
}
err = p.scope.AddRule(rule)
if err != nil {
panic(err)
@ -723,6 +741,9 @@ func (r *ruleFunc) scope() *basicScope {
// We lazily create the scope so that all the global variables get declared
// before the args are created. Otherwise we could incorrectly shadow a
// global variable with an arg variable.
r.Lock()
defer r.Unlock()
if r.scope_ == nil {
r.scope_ = makeRuleScope(r.pctx.scope, r.argNames)
}
@ -738,8 +759,9 @@ func (r *ruleFunc) String() string {
}
type builtinRule struct {
name_ string
scope_ *basicScope
name_ string
scope_ *basicScope
sync.Mutex // protects scope_ during lazy creation
}
func (r *builtinRule) packageContext() *PackageContext {
@ -759,6 +781,9 @@ func (r *builtinRule) def(config interface{}) (*ruleDef, error) {
}
func (r *builtinRule) scope() *basicScope {
r.Lock()
defer r.Unlock()
if r.scope_ == nil {
r.scope_ = makeRuleScope(nil, nil)
}