Bug 585774: Revert pymake to an earlier version to fix a massive build perf loss.

This commit is contained in:
Kyle Huey 2010-08-09 17:56:49 -07:00
commit 4b34c7ceab
17 changed files with 81 additions and 426 deletions

View File

@ -1,5 +1,2 @@
repo: f5ab154deef2ffa97f1b2139589ae4a1962090a4 repo: f5ab154deef2ffa97f1b2139589ae4a1962090a4
node: e50be3234e397322410b29a9e226365e23cb322e node: 7ae0b4af32617677698f9de3ab76bcb154bbf085
branch: default
latesttag: null
latesttagdistance: 255

View File

@ -10,10 +10,8 @@ import sys, os
import pymake.command, pymake.process import pymake.command, pymake.process
import gc import gc
gc.disable()
if __name__ == '__main__': pymake.command.main(sys.argv[1:], os.environ, os.getcwd(), cb=sys.exit)
gc.disable() pymake.process.ParallelContext.spin()
assert False, "Not reached"
pymake.command.main(sys.argv[1:], os.environ, os.getcwd(), cb=sys.exit)
pymake.process.ParallelContext.spin()
assert False, "Not reached"

View File

@ -1,68 +1,10 @@
# Basic commands implemented in Python """
import sys, os, shutil, time Implicit variables; perhaps in the future this will also include some implicit
from getopt import getopt, GetoptError rules, at least match-anything cancellation rules.
"""
from process import PythonException variables = {
'RM': 'rm -f',
__all__ = ["rm", "sleep", "touch"] '.LIBPATTERNS': 'lib%.so lib%.a',
'.PYMAKE': '1',
def rm(args): }
"""
Emulate most of the behavior of rm(1).
Only supports the -r (--recursive) and -f (--force) arguments.
"""
try:
opts, args = getopt(args, "rRf", ["force", "recursive"])
except GetoptError, e:
raise PythonException, ("rm: %s" % e, 1)
force = False
recursive = False
for o, a in opts:
if o in ('-f', '--force'):
force = True
elif o in ('-r', '-R', '--recursive'):
recursive = True
for f in args:
if os.path.isdir(f):
if not recursive:
raise PythonException, ("rm: cannot remove '%s': Is a directory" % f, 1)
else:
shutil.rmtree(f, force)
elif os.path.exists(f):
try:
os.unlink(f)
except:
if not force:
raise PythonException, ("rm: failed to remove '%s': %s" % (f, sys.exc_info()[0]), 1)
elif not force:
raise PythonException, ("rm: cannot remove '%s': No such file or directory" % f, 1)
def sleep(args):
"""
Emulate the behavior of sleep(1).
"""
total = 0
values = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}
for a in args:
multiplier = 1
for k, v in values.iteritems():
if a.endswith(k):
a = a[:-1]
multiplier = v
break
try:
f = float(a)
total += f * multiplier
except ValueError:
raise PythonException, ("sleep: invalid time interval '%s'" % a, 1)
time.sleep(total)
def touch(args):
"""
Emulate the behavior of touch(1).
"""
for f in args:
if os.path.exists(f):
os.utime(f, None)
else:
open(f, 'w').close()

View File

@ -106,8 +106,7 @@ class _MakeContext(object):
makeoverrides=self.overrides, makeoverrides=self.overrides,
workdir=self.workdir, workdir=self.workdir,
context=self.context, env=self.env, makelevel=self.makelevel, context=self.context, env=self.env, makelevel=self.makelevel,
targets=self.targets, keepgoing=self.options.keepgoing, targets=self.targets, keepgoing=self.options.keepgoing)
silent=self.options.silent)
self.restarts += 1 self.restarts += 1
@ -187,8 +186,6 @@ def main(args, env, cwd, cb):
dest="printdir") dest="printdir")
op.add_option('--no-print-directory', action="store_false", op.add_option('--no-print-directory', action="store_false",
dest="printdir", default=True) dest="printdir", default=True)
op.add_option('-s', '--silent', action="store_true",
dest="silent", default=False)
options, arguments1 = op.parse_args(parsemakeflags(env)) options, arguments1 = op.parse_args(parsemakeflags(env))
options, arguments2 = op.parse_args(args, values=options) options, arguments2 = op.parse_args(args, values=options)
@ -211,10 +208,6 @@ def main(args, env, cwd, cb):
if options.printdir: if options.printdir:
shortflags.append('w') shortflags.append('w')
if options.silent:
shortflags.append('s')
options.printdir = False
loglevel = logging.WARNING loglevel = logging.WARNING
if options.verbose: if options.verbose:
loglevel = logging.DEBUG loglevel = logging.DEBUG

View File

@ -3,7 +3,7 @@ A representation of makefile data structures.
""" """
import logging, re, os, sys import logging, re, os, sys
import parserdata, parser, functions, process, util, implicit import parserdata, parser, functions, process, util, builtins
from cStringIO import StringIO from cStringIO import StringIO
_log = logging.getLogger('pymake.data') _log = logging.getLogger('pymake.data')
@ -1142,18 +1142,17 @@ def splitcommand(command):
def findmodifiers(command): def findmodifiers(command):
""" """
Find any of +-@% prefixed on the command. Find any of +-@ prefixed on the command.
@returns (command, isHidden, isRecursive, ignoreErrors, isNative) @returns (command, isHidden, isRecursive, ignoreErrors)
""" """
isHidden = False isHidden = False
isRecursive = False isRecursive = False
ignoreErrors = False ignoreErrors = False
isNative = False
realcommand = command.lstrip(' \t\n@+-%') realcommand = command.lstrip(' \t\n@+-')
modset = set(command[:-len(realcommand)]) modset = set(command[:-len(realcommand)])
return realcommand, '@' in modset, '+' in modset, '-' in modset, '%' in modset return realcommand, '@' in modset, '+' in modset, '-' in modset
class _CommandWrapper(object): class _CommandWrapper(object):
def __init__(self, cline, ignoreErrors, loc, context, **kwargs): def __init__(self, cline, ignoreErrors, loc, context, **kwargs):
@ -1174,32 +1173,6 @@ class _CommandWrapper(object):
self.usercb = cb self.usercb = cb
process.call(self.cline, loc=self.loc, cb=self._cb, context=self.context, **self.kwargs) process.call(self.cline, loc=self.loc, cb=self._cb, context=self.context, **self.kwargs)
class _NativeWrapper(_CommandWrapper):
def __init__(self, cline, ignoreErrors, loc, context,
pycommandpath, **kwargs):
_CommandWrapper.__init__(self, cline, ignoreErrors, loc, context,
**kwargs)
# get the module and method to call
parts, badchar = process.clinetoargv(cline)
if parts is None:
raise DataError("native command '%s': shell metacharacter '%s' in command line" % (cline, badchar), self.loc)
if len(parts) < 2:
raise DataError("native command '%s': no method name specified" % cline, self.loc)
if pycommandpath:
self.pycommandpath = re.split('[%s\s]+' % os.pathsep,
pycommandpath)
else:
self.pycommandpath = None
self.module = parts[0]
self.method = parts[1]
self.cline_list = parts[2:]
def __call__(self, cb):
self.usercb = cb
process.call_native(self.module, self.method, self.cline_list,
loc=self.loc, cb=self._cb, context=self.context,
pycommandpath=self.pycommandpath, **self.kwargs)
def getcommandsforrule(rule, target, makefile, prerequisites, stem): def getcommandsforrule(rule, target, makefile, prerequisites, stem):
v = Variables(parent=target.variables) v = Variables(parent=target.variables)
setautomaticvariables(v, makefile, target, prerequisites) setautomaticvariables(v, makefile, target, prerequisites)
@ -1211,22 +1184,13 @@ def getcommandsforrule(rule, target, makefile, prerequisites, stem):
for c in rule.commands: for c in rule.commands:
cstring = c.resolvestr(makefile, v) cstring = c.resolvestr(makefile, v)
for cline in splitcommand(cstring): for cline in splitcommand(cstring):
cline, isHidden, isRecursive, ignoreErrors, isNative = findmodifiers(cline) cline, isHidden, isRecursive, ignoreErrors = findmodifiers(cline)
if isHidden or makefile.silent: if isHidden:
echo = None echo = None
else: else:
echo = "%s$ %s" % (c.loc, cline) echo = "%s$ %s" % (c.loc, cline)
if not isNative: yield _CommandWrapper(cline, ignoreErrors=ignoreErrors, env=env, cwd=makefile.workdir, loc=c.loc, context=makefile.context,
yield _CommandWrapper(cline, ignoreErrors=ignoreErrors, env=env, cwd=makefile.workdir, loc=c.loc, context=makefile.context, echo=echo)
echo=echo)
else:
f, s, e = v.get("PYCOMMANDPATH", True)
if e:
e = e.resolvestr(makefile, v, ["PYCOMMANDPATH"])
yield _NativeWrapper(cline, ignoreErrors=ignoreErrors,
env=env, cwd=makefile.workdir,
loc=c.loc, context=makefile.context,
echo=echo, pycommandpath=e)
class Rule(object): class Rule(object):
""" """
@ -1371,8 +1335,7 @@ class Makefile(object):
def __init__(self, workdir=None, env=None, restarts=0, make=None, def __init__(self, workdir=None, env=None, restarts=0, make=None,
makeflags='', makeoverrides='', makeflags='', makeoverrides='',
makelevel=0, context=None, targets=(), keepgoing=False, makelevel=0, context=None, targets=(), keepgoing=False):
silent=False):
self.defaulttarget = None self.defaulttarget = None
if env is None: if env is None:
@ -1386,7 +1349,6 @@ class Makefile(object):
self.exportedvars = {} self.exportedvars = {}
self._targets = {} self._targets = {}
self.keepgoing = keepgoing self.keepgoing = keepgoing
self.silent = silent
self._patternvariables = [] # of (pattern, variables) self._patternvariables = [] # of (pattern, variables)
self.implicitrules = [] self.implicitrules = []
self.parsingfinished = False self.parsingfinished = False
@ -1406,8 +1368,6 @@ class Makefile(object):
self.variables.set('MAKE_RESTARTS', Variables.FLAVOR_SIMPLE, self.variables.set('MAKE_RESTARTS', Variables.FLAVOR_SIMPLE,
Variables.SOURCE_AUTOMATIC, restarts > 0 and str(restarts) or '') Variables.SOURCE_AUTOMATIC, restarts > 0 and str(restarts) or '')
self.variables.set('.PYMAKE', Variables.FLAVOR_SIMPLE,
Variables.SOURCE_MAKEFILE, "1")
if make is not None: if make is not None:
self.variables.set('MAKE', Variables.FLAVOR_SIMPLE, self.variables.set('MAKE', Variables.FLAVOR_SIMPLE,
Variables.SOURCE_MAKEFILE, make) Variables.SOURCE_MAKEFILE, make)
@ -1432,7 +1392,7 @@ class Makefile(object):
self.variables.set('MAKECMDGOALS', Variables.FLAVOR_SIMPLE, self.variables.set('MAKECMDGOALS', Variables.FLAVOR_SIMPLE,
Variables.SOURCE_AUTOMATIC, ' '.join(targets)) Variables.SOURCE_AUTOMATIC, ' '.join(targets))
for vname, val in implicit.variables.iteritems(): for vname, val in builtins.variables.iteritems():
self.variables.set(vname, self.variables.set(vname,
Variables.FLAVOR_SIMPLE, Variables.FLAVOR_SIMPLE,
Variables.SOURCE_IMPLICIT, val) Variables.SOURCE_IMPLICIT, val)

View File

@ -1,13 +0,0 @@
"""
Implicit variables; perhaps in the future this will also include some implicit
rules, at least match-anything cancellation rules.
"""
variables = {
'RM': '%pymake.builtins rm -f',
'SLEEP': '%pymake.builtins sleep',
'TOUCH': '%pymake.builtins touch',
'.LIBPATTERNS': 'lib%.so lib%.a',
'.PYMAKE': '1',
}

View File

@ -3,9 +3,7 @@ Skipping shell invocations is good, when possible. This wrapper around subproces
parsing command lines into argv and making sure that no shell magic is being used. parsing command lines into argv and making sure that no shell magic is being used.
""" """
#TODO: ship pyprocessing? import subprocess, shlex, re, logging, sys, traceback, os
from multiprocessing import Pool, Condition
import subprocess, shlex, re, logging, sys, traceback, os, imp
import command, util import command, util
if sys.platform=='win32': if sys.platform=='win32':
import win32process import win32process
@ -81,11 +79,6 @@ def call(cline, env, cwd, loc, cb, context, echo):
context.call(argv, executable=executable, shell=False, env=env, cwd=cwd, cb=cb, echo=echo) context.call(argv, executable=executable, shell=False, env=env, cwd=cwd, cb=cb, echo=echo)
def call_native(module, method, argv, env, cwd, loc, cb, context, echo,
pycommandpath=None):
context.call_native(module, method, argv, env=env, cwd=cwd, cb=cb,
echo=echo, pycommandpath=pycommandpath)
def statustoresult(status): def statustoresult(status):
""" """
Convert the status returned from waitpid into a prettier numeric result. Convert the status returned from waitpid into a prettier numeric result.
@ -96,127 +89,17 @@ def statustoresult(status):
return status >>8 return status >>8
class Job(object):
"""
A single job to be executed on the process pool.
"""
done = False # set to true when the job completes
def __init__(self):
self.exitcode = -127
def notify(self, condition, result):
condition.acquire()
self.done = True
self.exitcode = result
condition.notify()
condition.release()
def get_callback(self, condition):
return lambda result: self.notify(condition, result)
class PopenJob(Job):
"""
A job that executes a command using subprocess.Popen.
"""
def __init__(self, argv, executable, shell, env, cwd):
Job.__init__(self)
self.argv = argv
self.executable = executable
self.shell = shell
self.env = env
self.cwd = cwd
def run(self):
try:
p = subprocess.Popen(self.argv, executable=self.executable, shell=self.shell, env=self.env, cwd=self.cwd)
return p.wait()
except OSError, e:
print >>sys.stderr, e
return -127
class PythonException(Exception):
def __init__(self, message, exitcode):
Exception.__init__(self)
self.message = message
self.exitcode = exitcode
def __str__(self):
return self.message
def load_module_recursive(module, path):
"""
Emulate the behavior of __import__, but allow
passing a custom path to search for modules.
"""
bits = module.split('.')
for i, bit in enumerate(bits):
dotname = '.'.join(bits[:i+1])
try:
f, path, desc = imp.find_module(bit, path)
m = imp.load_module(dotname, f, path, desc)
if f is None:
path = m.__path__
except ImportError:
return
class PythonJob(Job):
"""
A job that calls a Python method.
"""
def __init__(self, module, method, argv, env, cwd, pycommandpath=None):
self.module = module
self.method = method
self.argv = argv
self.env = env
self.cwd = cwd
self.pycommandpath = pycommandpath or []
def run(self):
oldenv = os.environ
try:
os.chdir(self.cwd)
os.environ = self.env
if self.module not in sys.modules:
load_module_recursive(self.module,
sys.path + self.pycommandpath)
if self.module not in sys.modules:
print >>sys.stderr, "No module named '%s'" % self.module
return -127
m = sys.modules[self.module]
if self.method not in m.__dict__:
print >>sys.stderr, "No method named '%s' in module %s" % (method, module)
return -127
m.__dict__[self.method](self.argv)
except PythonException, e:
print >>sys.stderr, e
return e.exitcode
except:
print >>sys.stderr, sys.exc_info()[1]
return -127
finally:
os.environ = oldenv
return 0
def job_runner(job):
"""
Run a job. Called in a Process pool.
"""
return job.run()
class ParallelContext(object): class ParallelContext(object):
""" """
Manages the parallel execution of processes. Manages the parallel execution of processes.
""" """
_allcontexts = set() _allcontexts = set()
_condition = Condition()
def __init__(self, jcount): def __init__(self, jcount):
self.jcount = jcount self.jcount = jcount
self.exit = False self.exit = False
self.pool = Pool(processes=jcount)
self.pending = [] # list of (cb, args, kwargs) self.pending = [] # list of (cb, args, kwargs)
self.running = [] # list of (subprocess, cb) self.running = [] # list of (subprocess, cb)
@ -224,8 +107,6 @@ class ParallelContext(object):
def finish(self): def finish(self):
assert len(self.pending) == 0 and len(self.running) == 0, "pending: %i running: %i" % (len(self.pending), len(self.running)) assert len(self.pending) == 0 and len(self.running) == 0, "pending: %i running: %i" % (len(self.pending), len(self.running))
self.pool.close()
self.pool.join()
self._allcontexts.remove(self) self._allcontexts.remove(self)
def run(self): def run(self):
@ -238,19 +119,16 @@ class ParallelContext(object):
self.pending.append((cb, args, kwargs)) self.pending.append((cb, args, kwargs))
def _docall(self, argv, executable, shell, env, cwd, cb, echo): def _docall(self, argv, executable, shell, env, cwd, cb, echo):
if echo is not None: if echo is not None:
print echo print echo
job = PopenJob(argv, executable=executable, shell=shell, env=env, cwd=cwd) try:
self.pool.apply_async(job_runner, args=(job,), callback=job.get_callback(ParallelContext._condition)) p = subprocess.Popen(argv, executable=executable, shell=shell, env=env, cwd=cwd)
self.running.append((job, cb)) except OSError, e:
print >>sys.stderr, e
cb(-127)
return
def _docallnative(self, module, method, argv, env, cwd, cb, echo, self.running.append((p, cb))
pycommandpath=None):
if echo is not None:
print echo
job = PythonJob(module, method, argv, env, cwd, pycommandpath)
self.pool.apply_async(job_runner, args=(job,), callback=job.get_callback(ParallelContext._condition))
self.running.append((job, cb))
def call(self, argv, shell, env, cwd, cb, echo, executable=None): def call(self, argv, shell, env, cwd, cb, echo, executable=None):
""" """
@ -259,43 +137,24 @@ class ParallelContext(object):
self.defer(self._docall, argv, executable, shell, env, cwd, cb, echo) self.defer(self._docall, argv, executable, shell, env, cwd, cb, echo)
def call_native(self, module, method, argv, env, cwd, cb, if sys.platform == 'win32':
echo, pycommandpath=None): @staticmethod
""" def _waitany():
Asynchronously call the native function return win32process.WaitForAnyProcess([p for c in ParallelContext._allcontexts for p, cb in c.running])
"""
self.defer(self._docallnative, module, method, argv, env, cwd, cb, @staticmethod
echo, pycommandpath) def _comparepid(pid, process):
return pid == process
@staticmethod else:
def _waitany(): @staticmethod
def _checkdone(): def _waitany():
jobs = [] return os.waitpid(-1, 0)
for c in ParallelContext._allcontexts:
for i in xrange(0, len(c.running)):
if c.running[i][0].done:
jobs.append(c.running[i])
for j in jobs:
if j in c.running:
c.running.remove(j)
return jobs
# We must acquire the lock, and then check to see if any jobs have @staticmethod
# finished. If we don't check after acquiring the lock it's possible def _comparepid(pid, process):
# that all outstanding jobs will have completed before we wait and we'll return pid == process.pid
# wait for notifications that have already occurred.
ParallelContext._condition.acquire()
jobs = _checkdone()
if jobs == []:
ParallelContext._condition.wait()
jobs = _checkdone()
ParallelContext._condition.release()
return jobs
@staticmethod @staticmethod
def spin(): def spin():
""" """
@ -307,10 +166,39 @@ class ParallelContext(object):
for c in clist: for c in clist:
c.run() c.run()
# In python 2.4, subprocess instances wait on child processes under the hood when they are created... this
# unfortunate behavior means that before using os.waitpid, we need to check the status using .poll()
# see http://bytes.com/groups/python/675403-os-wait-losing-child
found = False
for c in clist:
for i in xrange(0, len(c.running)):
p, cb = c.running[i]
result = p.poll()
if result != None:
del c.running[i]
cb(result)
found = True
break
if found: break
if found: continue
dowait = util.any((len(c.running) for c in ParallelContext._allcontexts)) dowait = util.any((len(c.running) for c in ParallelContext._allcontexts))
if dowait: if dowait:
for job, cb in ParallelContext._waitany(): pid, status = ParallelContext._waitany()
cb(job.exitcode) result = statustoresult(status)
for c in ParallelContext._allcontexts:
for i in xrange(0, len(c.running)):
p, cb = c.running[i]
if ParallelContext._comparepid(pid, p):
del c.running[i]
cb(result)
found = True
break
if found: break
else: else:
assert any(len(c.pending) for c in ParallelContext._allcontexts) assert any(len(c.pending) for c in ParallelContext._allcontexts)

View File

@ -1,10 +0,0 @@
#T gmake skip
export EXPECTED := some data
CMD = %pycmd writeenvtofile
PYCOMMANDPATH = $(TESTPATH)
all:
$(CMD) results EXPECTED
test "$$(cat results)" = "$(EXPECTED)"
@echo TEST-PASS

View File

@ -1,21 +0,0 @@
#T gmake skip
EXPECTED := some data
# verify that we can load native command modules from
# multiple directories in PYCOMMANDPATH separated by the native
# path separator
ifdef __WIN32__
PS:=;
else
PS:=:
endif
CMD = %pycmd writetofile
CMD2 = %pymod writetofile
PYCOMMANDPATH = $(TESTPATH)$(PS)$(TESTPATH)/subdir
all:
$(CMD) results $(EXPECTED)
test "$$(cat results)" = "$(EXPECTED)"
$(CMD2) results2 $(EXPECTED)
test "$$(cat results2)" = "$(EXPECTED)"
@echo TEST-PASS

View File

@ -1,15 +0,0 @@
#T gmake skip
EXPECTED := some data
# verify that we can load native command modules from
# multiple space-separated directories in PYCOMMANDPATH
CMD = %pycmd writetofile
CMD2 = %pymod writetofile
PYCOMMANDPATH = $(TESTPATH) $(TESTPATH)/subdir
all:
$(CMD) results $(EXPECTED)
test "$$(cat results)" = "$(EXPECTED)"
$(CMD2) results2 $(EXPECTED)
test "$$(cat results2)" = "$(EXPECTED)"
@echo TEST-PASS

View File

@ -1,10 +0,0 @@
ifndef TOUCH
TOUCH = touch
endif
all: testfile
test -f testfile
@echo TEST-PASS
testfile:
$(TOUCH) $@

View File

@ -1,21 +0,0 @@
#T commandline: ['-j2']
# ensure that calling python commands doesn't block other targets
ifndef SLEEP
SLEEP := sleep
endif
PRINTF = printf "$@:0:" >>results
EXPECTED = target2:0:target1:0:
all:: target1 target2
cat results
test "$$(cat results)" = "$(EXPECTED)"
@echo TEST-PASS
target1:
$(SLEEP) 0.1
$(PRINTF)
target2:
$(PRINTF)

View File

@ -1,9 +0,0 @@
import os
def writetofile(args):
with open(args[0], 'w') as f:
f.write(' '.join(args[1:]))
def writeenvtofile(args):
with open(args[0], 'w') as f:
f.write(os.environ[args[1]])

View File

@ -1,7 +0,0 @@
#T returncode: 2
all:
mkdir newdir
test -d newdir
touch newdir/newfile
$(RM) newdir
@echo TEST-PASS

View File

@ -1,13 +0,0 @@
all:
# $(RM) defaults to -f
$(RM) nosuchfile
touch newfile
test -f newfile
$(RM) newfile
test ! -f newfile
mkdir newdir
test -d newdir
touch newdir/newfile
$(RM) -r newdir
test ! -d newdir
@echo TEST-PASS

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python
""" """
Run the test(s) listed on the command line. If a directory is listed, the script will recursively Run the test(s) listed on the command line. If a directory is listed, the script will recursively
walk the directory for files named .mk and run each. walk the directory for files named .mk and run each.

View File

@ -1,3 +0,0 @@
def writetofile(args):
with open(args[0], 'w') as f:
f.write(' '.join(args[1:]))