Bug 873720 - Part 4: Display a build progress indicator; r=ted

This commit is contained in:
Gregory Szorc 2013-05-23 16:28:10 -07:00
Родитель 6c39c3c2ea
Коммит a5b0db124a
1 изменённых файлов: 257 добавлений и 59 удалений

Просмотреть файл

@ -7,6 +7,7 @@ from __future__ import print_function, unicode_literals
import logging import logging
import operator import operator
import os import os
import sys
from mach.decorators import ( from mach.decorators import (
CommandArgument, CommandArgument,
@ -14,6 +15,8 @@ from mach.decorators import (
Command, Command,
) )
from mach.mixin.logging import LoggingMixin
from mozbuild.base import MachCommandBase from mozbuild.base import MachCommandBase
@ -40,6 +43,209 @@ Preferences.
'''.strip() '''.strip()
class TerminalLoggingHandler(logging.Handler):
"""Custom logging handler that works with terminal window dressing.
This class should probably live elsewhere, like the mach core. Consider
this a proving ground for its usefulness.
"""
def __init__(self):
logging.Handler.__init__(self)
self.fh = sys.stdout
self.footer = None
def flush(self):
self.acquire()
try:
self.fh.flush()
finally:
self.release()
def emit(self, record):
msg = self.format(record)
if self.footer:
self.footer.clear()
self.fh.write(msg)
self.fh.write('\n')
if self.footer:
self.footer.draw()
# If we don't flush, the footer may not get drawn.
self.flush()
class BuildProgressFooter(object):
"""Handles display of a build progress indicator in a terminal.
When mach builds inside a blessings-supported terminal, it will render
progress information collected from a BuildMonitor. This class converts the
state of BuildMonitor into terminal output.
"""
def __init__(self, terminal, monitor):
# terminal is a blessings.Terminal.
self._t = terminal
self._fh = sys.stdout
self._monitor = monitor
def _clear_lines(self, n):
for i in range(n):
self._fh.write(self._t.move_x(0))
self._fh.write(self._t.clear_eol())
self._fh.write(self._t.move_up())
self._fh.write(self._t.move_down())
self._fh.write(self._t.move_x(0))
def clear(self):
"""Removes the footer from the current terminal."""
self._clear_lines(1)
def draw(self):
"""Draws this footer in the terminal."""
if not self._monitor.tiers:
return
# The drawn terminal looks something like:
# TIER: base nspr nss js platform app SUBTIER: static export libs tools DIRECTORIES: 06/09 (memory)
# This is a list of 2-tuples of (encoding function, input). None means
# no encoding. For a full reason on why we do things this way, read the
# big comment below.
parts = [('bold', 'TIER'), ':', ' ']
current_encountered = False
for tier in self._monitor.tiers:
if tier == self._monitor.current_tier:
parts.extend([('yellow', tier), ' '])
current_encountered = True
elif not current_encountered:
parts.extend([('green', tier), ' '])
else:
parts.extend([tier, ' '])
current_encountered = False
parts.extend([('bold', 'SUBTIER'), ':', ' '])
for subtier in self._monitor.subtiers:
if subtier == self._monitor.current_subtier:
parts.extend([('yellow', subtier), ' '])
current_encountered = True
elif not current_encountered:
parts.extend([('green', subtier), ' '])
else:
parts.extend([subtier, ' '])
if self._monitor.current_subtier_dirs and self._monitor.current_tier_dir:
parts.extend([
('bold', 'DIRECTORIES'), ': ',
'%02d' % self._monitor.current_tier_dir_index,
'/',
'%02d' % len(self._monitor.current_subtier_dirs),
' ',
'(', ('magenta', self._monitor.current_tier_dir), ')',
])
# We don't want to write more characters than the current width of the
# terminal otherwise wrapping may result in weird behavior. We can't
# simply truncate the line at terminal width characters because a)
# non-viewable escape characters count towards the limit and b) we
# don't want to truncate in the middle of an escape sequence because
# subsequent output would inherit the escape sequence.
max_width = self._t.width
written = 0
write_pieces = []
for part in parts:
if isinstance(part, tuple):
func, arg = part
if written + len(arg) > max_width:
write_pieces.append(arg[0:max_width - written])
written += len(arg)
break
encoded = getattr(self._t, func)(arg)
write_pieces.append(encoded)
written += len(arg)
else:
if written + len(part) > max_width:
write_pieces.append(arg[0:max_width - written])
written += len(part)
break
write_pieces.append(part)
written += len(part)
self._fh.write(''.join(write_pieces))
self._fh.flush()
class BuildOutputManager(LoggingMixin):
"""Handles writing build output to a terminal, to logs, etc."""
def __init__(self, log_manager, monitor):
self.populate_logger()
self.monitor = monitor
self.footer = None
terminal = log_manager.terminal
# TODO convert terminal footer to config file setting.
if not terminal or os.environ.get('MACH_NO_TERMINAL_FOOTER', None):
return
self.t = terminal
self.footer = BuildProgressFooter(terminal, monitor)
handler = TerminalLoggingHandler()
handler.setFormatter(log_manager.terminal_formatter)
handler.footer = self.footer
old = log_manager.replace_terminal_handler(handler)
handler.level = old.level
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if self.footer:
self.footer.clear()
def write_line(self, line):
if self.footer:
self.footer.clear()
print(line)
if self.footer:
self.footer.draw()
def refresh(self):
if not self.footer:
return
self.footer.clear()
self.footer.draw()
def on_line(self, line):
warning, state_changed, relevant = self.monitor.on_line(line)
if warning:
self.log(logging.INFO, 'compiler_warning', warning,
'Warning: {flag} in {filename}: {message}')
if relevant:
self.log(logging.INFO, 'build_output', {'line': line}, '{line}')
elif state_changed:
self.refresh()
@CommandProvider @CommandProvider
class Build(MachCommandBase): class Build(MachCommandBase):
"""Interface to build the tree.""" """Interface to build the tree."""
@ -60,16 +266,7 @@ class Build(MachCommandBase):
warnings_path = self._get_state_filename('warnings.json') warnings_path = self._get_state_filename('warnings.json')
monitor = BuildMonitor(self.topobjdir, warnings_path) monitor = BuildMonitor(self.topobjdir, warnings_path)
def on_line(line): with BuildOutputManager(self.log_manager, monitor) as output:
warning, state_changed, relevant = monitor.on_line(line)
if warning:
self.log(logging.INFO, 'compiler_warning', warning,
'Warning: {flag} in {filename}: {message}')
if relevant:
self.log(logging.INFO, 'build_output', {'line': line}, '{line}')
monitor.start() monitor.start()
if what: if what:
@ -111,14 +308,14 @@ class Build(MachCommandBase):
# Build target pairs. # Build target pairs.
for make_dir, make_target in target_pairs: for make_dir, make_target in target_pairs:
status = self._run_make(directory=make_dir, target=make_target, status = self._run_make(directory=make_dir, target=make_target,
line_handler=on_line, log=False, print_directory=False, line_handler=output.on_line, log=False, print_directory=False,
ensure_exit_code=False, num_jobs=jobs, silent=not verbose) ensure_exit_code=False, num_jobs=jobs, silent=not verbose)
if status != 0: if status != 0:
break break
else: else:
status = self._run_make(srcdir=True, filename='client.mk', status = self._run_make(srcdir=True, filename='client.mk',
line_handler=on_line, log=False, print_directory=False, line_handler=output.on_line, log=False, print_directory=False,
allow_parallel=False, ensure_exit_code=False, num_jobs=jobs, allow_parallel=False, ensure_exit_code=False, num_jobs=jobs,
silent=not verbose) silent=not verbose)
@ -127,6 +324,7 @@ class Build(MachCommandBase):
'{count} compiler warnings present.') '{count} compiler warnings present.')
monitor.finish() monitor.finish()
high_finder, finder_percent = monitor.have_high_finder_usage() high_finder, finder_percent = monitor.have_high_finder_usage()
if high_finder: if high_finder:
print(FINDER_SLOW_MESSAGE % finder_percent) print(FINDER_SLOW_MESSAGE % finder_percent)