Bug 905879 - More robust tier tracking; r=glandium
authorGregory Szorc <gps@mozilla.com>
Tue, 20 Aug 2013 00:06:32 -0700
changeset 143189 0e046ece4b23e3af397ba84138bbdd92da78b741
parent 143188 a727146d85e208f0b9a1f62a8d26150c6e118516
child 143190 2872a52eb8a8845c61319411daf27862ce0c9a5c
push id32656
push usergszorc@mozilla.com
push dateTue, 20 Aug 2013 07:11:03 +0000
treeherdermozilla-inbound@0e046ece4b23 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersglandium
bugs905879
milestone26.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 905879 - More robust tier tracking; r=glandium
Makefile.in
config/makefiles/precompile/Makefile.in
config/makefiles/target_export.mk
config/makefiles/target_libs.mk
config/makefiles/target_tools.mk
config/rules.mk
js/src/config/makefiles/target_export.mk
js/src/config/makefiles/target_libs.mk
js/src/config/makefiles/target_tools.mk
js/src/config/rules.mk
python/mozbuild/mozbuild/controller/building.py
python/mozbuild/mozbuild/mach_commands.py
--- a/Makefile.in
+++ b/Makefile.in
@@ -89,16 +89,19 @@ export::
 	$(MAKE) -C config export
 	$(MAKE) tier_nspr
 
 ifdef ENABLE_TESTS
 # Additional makefile targets to call automated test suites
 include $(topsrcdir)/testing/testsuite-targets.mk
 endif
 
+# Hacky way for precompile tier to bypass default tier traversal mechanism.
+TIER_precompile_CUSTOM := 1
+
 include $(topsrcdir)/config/rules.mk
 
 distclean::
 	cat unallmakefiles | $(XARGS) rm -f
 	$(RM) unallmakefiles $(DIST_GARBAGE)
 
 ifeq ($(OS_ARCH),WINNT)
 # we want to copy PDB files on Windows
--- a/config/makefiles/precompile/Makefile.in
+++ b/config/makefiles/precompile/Makefile.in
@@ -19,25 +19,27 @@ include $(topsrcdir)/config/rules.mk
 # otherwise the output is unexpected and it confuses downstream parsers.
 define make_subtier_dir
 $(call BUILDSTATUS,SUBTIER_START precompile $(1))
 +$(MAKE) -C $(2) $(3)
 $(call BUILDSTATUS,SUBTIER_FINISH precompile $(1))
 
 endef
 
-export::
-	$(call BUILDSTATUS,SUBTIERS IPDL WebIDL XPIDL XPIDLParser)
+default::
+	$(call BUILDSTATUS,TIER_START  precompile IPDL WebIDL XPIDL)
+	+$(MAKE) export
+	$(call BUILDSTATUS,TIER_FINISH precompile)
 
-export:: ipdl webidl xpidl-parser xpidl
+export:: ipdl webidl xpidl
 
 ipdl:
 	$(call make_subtier_dir,IPDL,$(DEPTH)/ipc/ipdl,ipdl)
 
 webidl:
 	$(call make_subtier_dir,WebIDL,$(DEPTH)/dom/bindings,webidl)
 
-xpidl-parser:
-	$(call make_subtier_dir,XPIDLParser,$(DEPTH)/xpcom/idl-parser,xpidl-parser)
-
-xpidl: xpidl-parser
+xpidl:
+	$(call BUILDSTATUS,SUBTIER_START  precompile XPIDL)
+	+$(MAKE) -C $(DEPTH)/xpcom/idl-parser xpidl-parser
 	$(call py_action,process_install_manifest,$(DIST)/idl $(DEPTH)/_build_manifests/install/dist_idl)
-	$(call make_subtier_dir,XPIDL,$(DEPTH)/config/makefiles/xpidl,xpidl)
+	+$(MAKE) -C $(DEPTH)/config/makefiles/xpidl xpidl
+	$(call BUILDSTATUS,SUBTIER_FINISH precompile XPIDL)
--- a/config/makefiles/target_export.mk
+++ b/config/makefiles/target_export.mk
@@ -9,17 +9,17 @@ PARALLEL_DIRS_export = $(addsuffix _expo
 
 .PHONY: export $(PARALLEL_DIRS_export)
 
 ###############
 ## TIER targets
 ###############
 $(addprefix export_tier_,$(TIERS)): export_tier_%:
 	@$(ECHO) "$@"
-	$(foreach dir,$(tier_$*_dirs),$(call TIER_DIR_SUBMAKE,export,$(dir)))
+	$(foreach dir,$(tier_$*_dirs),$(call TIER_DIR_SUBMAKE,$*,export,$(dir),export))
 
 #################
 ## Common targets
 #################
 ifdef PARALLEL_DIRS
 export:: $(PARALLEL_DIRS_export)
 
 $(PARALLEL_DIRS_export): %_export: %/Makefile
--- a/config/makefiles/target_libs.mk
+++ b/config/makefiles/target_libs.mk
@@ -9,17 +9,17 @@ PARALLEL_DIRS_libs = $(addsuffix _libs,$
 
 .PHONY: libs $(PARALLEL_DIRS_libs)
 
 ###############
 ## TIER targets
 ###############
 $(addprefix libs_tier_,$(TIERS)): libs_tier_%:
 	@$(ECHO) "$@"
-	$(foreach dir,$(tier_$*_dirs),$(call TIER_DIR_SUBMAKE,libs,$(dir)))
+	$(foreach dir,$(tier_$*_dirs),$(call TIER_DIR_SUBMAKE,$*,libs,$(dir),libs))
 
 #################
 ## Common targets
 #################
 ifdef PARALLEL_DIRS
 libs:: $(PARALLEL_DIRS_libs)
 
 $(PARALLEL_DIRS_libs): %_libs: %/Makefile
--- a/config/makefiles/target_tools.mk
+++ b/config/makefiles/target_tools.mk
@@ -9,17 +9,17 @@ PARALLEL_DIRS_tools = $(addsuffix _tools
 
 .PHONY: tools $(PARALLEL_DIRS_tools)
 
 ###############
 ## TIER targets
 ###############
 $(addprefix tools_tier_,$(TIERS)): tools_tier_%:
 	@$(ECHO) "$@"
-	$(foreach dir,$(tier_$*_dirs),$(call TIER_DIR_SUBMAKE,tools,$(dir)))
+	$(foreach dir,$(tier_$*_dirs),$(call TIER_DIR_SUBMAKE,$*,tools,$(dir),tools))
 
 #################
 ## Common targets
 #################
 ifdef PARALLEL_DIRS
 tools:: $(PARALLEL_DIRS_tools)
 
 $(PARALLEL_DIRS_tools): %_tools: %/Makefile
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -424,33 +424,34 @@ endif
 ifdef MOZ_UPDATE_XTERM
 # Its good not to have a newline at the end of the titlebar string because it
 # makes the make -s output easier to read.  Echo -n does not work on all
 # platforms, but we can trick printf into doing it.
 UPDATE_TITLE = printf "\033]0;%s in %s\007" $(1) $(shell $(BUILD_TOOLS)/print-depth-path.sh)/$(2) ;
 endif
 
 ifdef MACH
-BUILDSTATUS=@echo BUILDSTATUS $1
+BUILDSTATUS=@echo "BUILDSTATUS $1"
 endif
+
 # Static directories are largely independent of our build system. But, they
 # could share the same build mechanism (like moz.build files). We need to
 # prevent leaking of our backend state to these independent build systems. This
 # is why MOZBUILD_BACKEND_CHECKED isn't exported to make invocations for static
 # directories.
 define SUBMAKE # $(call SUBMAKE,target,directory,static)
 +@$(UPDATE_TITLE)
 +$(if $(3), MOZBUILD_BACKEND_CHECKED=,) $(MAKE) $(if $(2),-C $(2)) $(1)
 
 endef # The extra line is important here! don't delete it
 
 define TIER_DIR_SUBMAKE
-$(call BUILDSTATUS,TIERDIR_START  $(2))
-$(call SUBMAKE,$(1),$(2),$(3))
-$(call BUILDSTATUS,TIERDIR_FINISH $(2))
+$(call BUILDSTATUS,TIERDIR_START  $(1) $(2) $(3))
+$(call SUBMAKE,$(4),$(3),$(5))
+$(call BUILDSTATUS,TIERDIR_FINISH $(1) $(2) $(3))
 
 endef # Ths empty line is important.
 
 
 ifneq (,$(strip $(DIRS)))
 LOOP_OVER_DIRS = \
   $(foreach dir,$(DIRS),$(call SUBMAKE,$@,$(dir)))
 endif
@@ -707,42 +708,48 @@ ifeq ($(filter s,$(MAKEFLAGS)),)
 ECHO := echo
 QUIET :=
 else
 ECHO := true
 QUIET := -q
 endif
 
 # This function is called and evaluated to produce the rule to build the
-# specified tier. Each tier begins by building the "static" directories.
-# The BUILDSTATUS echo commands are used to faciliate easier parsing
-# of build output. Build drivers are encouraged to filter these lines
-# from the user.
+# specified tier.
+#
+# Tiers are traditionally composed of directories that are invoked either
+# once (so-called "static" directories) or 3 times with the export, libs, and
+# tools sub-tiers.
+#
+# If the TIER_$(tier)_CUSTOM variable is defined, then these traditional
+# tier rules are ignored and each directory in the tier is executed via a
+# sub-make invocation (make -C).
 define CREATE_TIER_RULE
 tier_$(1)::
-	$(call BUILDSTATUS,TIER_START $(1))
-	$(call BUILDSTATUS,SUBTIERS $(if $(tier_$(1)_staticdirs),static )$(if $(tier_$(1)_dirs),export libs tools))
-	$(call BUILDSTATUS,STATICDIRS $$($$@_staticdirs))
-	$(call BUILDSTATUS,DIRS $$($$@_dirs))
+ifdef TIER_$(1)_CUSTOM
+	$$(foreach dir,$$($$@_dirs),$$(call SUBMAKE,,$$(dir)))
+else
+	$(call BUILDSTATUS,TIER_START $(1) $(if $(tier_$(1)_staticdirs),static )$(if $(tier_$(1)_dirs),export libs tools))
 ifneq (,$(tier_$(1)_staticdirs))
-	$(call BUILDSTATUS,SUBTIER_START $(1) static)
-	$$(foreach dir,$$($$@_staticdirs),$$(call TIER_DIR_SUBMAKE,,$$(dir),1))
+	$(call BUILDSTATUS,SUBTIER_START  $(1) static $$($$@_staticdirs))
+	$$(foreach dir,$$($$@_staticdirs),$$(call TIER_DIR_SUBMAKE,$(1),static,$$(dir),,1))
 	$(call BUILDSTATUS,SUBTIER_FINISH $(1) static)
 endif
 ifneq (,$(tier_$(1)_dirs))
-	$(call BUILDSTATUS,SUBTIER_START $(1) export)
+	$(call BUILDSTATUS,SUBTIER_START  $(1) export $$($$@_dirs))
 	$$(MAKE) export_$$@
 	$(call BUILDSTATUS,SUBTIER_FINISH $(1) export)
-	$(call BUILDSTATUS,SUBTIER_START $(1) libs)
+	$(call BUILDSTATUS,SUBTIER_START  $(1) libs $$($$@_dirs))
 	$$(MAKE) libs_$$@
 	$(call BUILDSTATUS,SUBTIER_FINISH $(1) libs)
-	$(call BUILDSTATUS,SUBTIER_START $(1) tools)
+	$(call BUILDSTATUS,SUBTIER_START  $(1) tools $$($$@_dirs))
 	$$(MAKE) tools_$$@
 	$(call BUILDSTATUS,SUBTIER_FINISH $(1) tools)
-	$(call BUILDSTATUS TIER_FINISH $(1))
+endif
+	$(call BUILDSTATUS,TIER_FINISH $(1))
 endif
 endef
 
 $(foreach tier,$(TIERS),$(eval $(call CREATE_TIER_RULE,$(tier))))
 
 # Do everything from scratch
 everything::
 	$(MAKE) clean
--- a/js/src/config/makefiles/target_export.mk
+++ b/js/src/config/makefiles/target_export.mk
@@ -9,17 +9,17 @@ PARALLEL_DIRS_export = $(addsuffix _expo
 
 .PHONY: export $(PARALLEL_DIRS_export)
 
 ###############
 ## TIER targets
 ###############
 $(addprefix export_tier_,$(TIERS)): export_tier_%:
 	@$(ECHO) "$@"
-	$(foreach dir,$(tier_$*_dirs),$(call TIER_DIR_SUBMAKE,export,$(dir)))
+	$(foreach dir,$(tier_$*_dirs),$(call TIER_DIR_SUBMAKE,$*,export,$(dir),export))
 
 #################
 ## Common targets
 #################
 ifdef PARALLEL_DIRS
 export:: $(PARALLEL_DIRS_export)
 
 $(PARALLEL_DIRS_export): %_export: %/Makefile
--- a/js/src/config/makefiles/target_libs.mk
+++ b/js/src/config/makefiles/target_libs.mk
@@ -9,17 +9,17 @@ PARALLEL_DIRS_libs = $(addsuffix _libs,$
 
 .PHONY: libs $(PARALLEL_DIRS_libs)
 
 ###############
 ## TIER targets
 ###############
 $(addprefix libs_tier_,$(TIERS)): libs_tier_%:
 	@$(ECHO) "$@"
-	$(foreach dir,$(tier_$*_dirs),$(call TIER_DIR_SUBMAKE,libs,$(dir)))
+	$(foreach dir,$(tier_$*_dirs),$(call TIER_DIR_SUBMAKE,$*,libs,$(dir),libs))
 
 #################
 ## Common targets
 #################
 ifdef PARALLEL_DIRS
 libs:: $(PARALLEL_DIRS_libs)
 
 $(PARALLEL_DIRS_libs): %_libs: %/Makefile
--- a/js/src/config/makefiles/target_tools.mk
+++ b/js/src/config/makefiles/target_tools.mk
@@ -9,17 +9,17 @@ PARALLEL_DIRS_tools = $(addsuffix _tools
 
 .PHONY: tools $(PARALLEL_DIRS_tools)
 
 ###############
 ## TIER targets
 ###############
 $(addprefix tools_tier_,$(TIERS)): tools_tier_%:
 	@$(ECHO) "$@"
-	$(foreach dir,$(tier_$*_dirs),$(call TIER_DIR_SUBMAKE,tools,$(dir)))
+	$(foreach dir,$(tier_$*_dirs),$(call TIER_DIR_SUBMAKE,$*,tools,$(dir),tools))
 
 #################
 ## Common targets
 #################
 ifdef PARALLEL_DIRS
 tools:: $(PARALLEL_DIRS_tools)
 
 $(PARALLEL_DIRS_tools): %_tools: %/Makefile
--- a/js/src/config/rules.mk
+++ b/js/src/config/rules.mk
@@ -424,33 +424,34 @@ endif
 ifdef MOZ_UPDATE_XTERM
 # Its good not to have a newline at the end of the titlebar string because it
 # makes the make -s output easier to read.  Echo -n does not work on all
 # platforms, but we can trick printf into doing it.
 UPDATE_TITLE = printf "\033]0;%s in %s\007" $(1) $(shell $(BUILD_TOOLS)/print-depth-path.sh)/$(2) ;
 endif
 
 ifdef MACH
-BUILDSTATUS=@echo BUILDSTATUS $1
+BUILDSTATUS=@echo "BUILDSTATUS $1"
 endif
+
 # Static directories are largely independent of our build system. But, they
 # could share the same build mechanism (like moz.build files). We need to
 # prevent leaking of our backend state to these independent build systems. This
 # is why MOZBUILD_BACKEND_CHECKED isn't exported to make invocations for static
 # directories.
 define SUBMAKE # $(call SUBMAKE,target,directory,static)
 +@$(UPDATE_TITLE)
 +$(if $(3), MOZBUILD_BACKEND_CHECKED=,) $(MAKE) $(if $(2),-C $(2)) $(1)
 
 endef # The extra line is important here! don't delete it
 
 define TIER_DIR_SUBMAKE
-$(call BUILDSTATUS,TIERDIR_START  $(2))
-$(call SUBMAKE,$(1),$(2),$(3))
-$(call BUILDSTATUS,TIERDIR_FINISH $(2))
+$(call BUILDSTATUS,TIERDIR_START  $(1) $(2) $(3))
+$(call SUBMAKE,$(4),$(3),$(5))
+$(call BUILDSTATUS,TIERDIR_FINISH $(1) $(2) $(3))
 
 endef # Ths empty line is important.
 
 
 ifneq (,$(strip $(DIRS)))
 LOOP_OVER_DIRS = \
   $(foreach dir,$(DIRS),$(call SUBMAKE,$@,$(dir)))
 endif
@@ -707,42 +708,48 @@ ifeq ($(filter s,$(MAKEFLAGS)),)
 ECHO := echo
 QUIET :=
 else
 ECHO := true
 QUIET := -q
 endif
 
 # This function is called and evaluated to produce the rule to build the
-# specified tier. Each tier begins by building the "static" directories.
-# The BUILDSTATUS echo commands are used to faciliate easier parsing
-# of build output. Build drivers are encouraged to filter these lines
-# from the user.
+# specified tier.
+#
+# Tiers are traditionally composed of directories that are invoked either
+# once (so-called "static" directories) or 3 times with the export, libs, and
+# tools sub-tiers.
+#
+# If the TIER_$(tier)_CUSTOM variable is defined, then these traditional
+# tier rules are ignored and each directory in the tier is executed via a
+# sub-make invocation (make -C).
 define CREATE_TIER_RULE
 tier_$(1)::
-	$(call BUILDSTATUS,TIER_START $(1))
-	$(call BUILDSTATUS,SUBTIERS $(if $(tier_$(1)_staticdirs),static )$(if $(tier_$(1)_dirs),export libs tools))
-	$(call BUILDSTATUS,STATICDIRS $$($$@_staticdirs))
-	$(call BUILDSTATUS,DIRS $$($$@_dirs))
+ifdef TIER_$(1)_CUSTOM
+	$$(foreach dir,$$($$@_dirs),$$(call SUBMAKE,,$$(dir)))
+else
+	$(call BUILDSTATUS,TIER_START $(1) $(if $(tier_$(1)_staticdirs),static )$(if $(tier_$(1)_dirs),export libs tools))
 ifneq (,$(tier_$(1)_staticdirs))
-	$(call BUILDSTATUS,SUBTIER_START $(1) static)
-	$$(foreach dir,$$($$@_staticdirs),$$(call TIER_DIR_SUBMAKE,,$$(dir),1))
+	$(call BUILDSTATUS,SUBTIER_START  $(1) static $$($$@_staticdirs))
+	$$(foreach dir,$$($$@_staticdirs),$$(call TIER_DIR_SUBMAKE,$(1),static,$$(dir),,1))
 	$(call BUILDSTATUS,SUBTIER_FINISH $(1) static)
 endif
 ifneq (,$(tier_$(1)_dirs))
-	$(call BUILDSTATUS,SUBTIER_START $(1) export)
+	$(call BUILDSTATUS,SUBTIER_START  $(1) export $$($$@_dirs))
 	$$(MAKE) export_$$@
 	$(call BUILDSTATUS,SUBTIER_FINISH $(1) export)
-	$(call BUILDSTATUS,SUBTIER_START $(1) libs)
+	$(call BUILDSTATUS,SUBTIER_START  $(1) libs $$($$@_dirs))
 	$$(MAKE) libs_$$@
 	$(call BUILDSTATUS,SUBTIER_FINISH $(1) libs)
-	$(call BUILDSTATUS,SUBTIER_START $(1) tools)
+	$(call BUILDSTATUS,SUBTIER_START  $(1) tools $$($$@_dirs))
 	$$(MAKE) tools_$$@
 	$(call BUILDSTATUS,SUBTIER_FINISH $(1) tools)
-	$(call BUILDSTATUS TIER_FINISH $(1))
+endif
+	$(call BUILDSTATUS,TIER_FINISH $(1))
 endif
 endef
 
 $(foreach tier,$(TIERS),$(eval $(call CREATE_TIER_RULE,$(tier))))
 
 # Do everything from scratch
 everything::
 	$(MAKE) clean
--- a/python/mozbuild/mozbuild/controller/building.py
+++ b/python/mozbuild/mozbuild/controller/building.py
@@ -4,68 +4,196 @@
 
 from __future__ import unicode_literals
 
 import getpass
 import os
 import sys
 import time
 
-from collections import namedtuple
+from collections import (
+    namedtuple,
+    OrderedDict,
+)
 
 # keep in sync with psutil os support, see psutil/__init__.py
 if sys.platform.startswith("freebsd") or sys.platform.startswith("darwin") or sys.platform.startswith("win32") or sys.platform.startswith("linux"):
     try:
         import psutil
     except ImportError:
         psutil = None
 else:
     psutil = None
 
+from ..base import MozbuildObject
+
 from ..compilation.warnings import (
     WarningsCollector,
     WarningsDatabase,
 )
 
 
 BuildOutputResult = namedtuple('BuildOutputResult',
     ('warning', 'state_changed', 'for_display'))
 
 
-class BuildMonitor(object):
+class TierStatus(object):
+    """Represents the state and progress of tier traversal.
+
+    The build system is organized into linear phases called tiers. Each tier
+    executes in the order it was defined, 1 at a time.
+
+    Tiers can have subtiers. Subtiers can execute in any order. Some subtiers
+    execute sequentially. Others are concurrent.
+
+    Subtiers can have directories. Directories can execute in any order, just
+    like subtiers.
+    """
+
+    def __init__(self):
+        self.tiers = OrderedDict()
+        self.active_tier = None
+        self.active_subtiers = set()
+
+    def set_tiers(self, tiers):
+        """Record the set of known tiers."""
+        for tier in tiers:
+            self.tiers[tier] = dict(
+                begin_time=None,
+                finish_time=None,
+                duration=None,
+                subtiers=OrderedDict(),
+            )
+
+    def begin_tier(self, tier, subtiers):
+        """Record that execution of a tier has begun."""
+        t = self.tiers[tier]
+        # We should ideally use a monotonic clock here. Unfortunately, we won't
+        # have one until Python 3.
+        t['begin_time'] = time.time()
+        for subtier in subtiers:
+            t['subtiers'][subtier] = dict(
+                begin_time=None,
+                finish_time=None,
+                duration=None,
+                concurrent=False,
+                dirs=OrderedDict(),
+                dirs_complete=0,
+            )
+
+        self.active_tier = tier
+        self.active_subtiers = set()
+        self.active_dirs = {}
+
+    def finish_tier(self, tier):
+        """Record that execution of a tier has finished."""
+        t = self.tiers[tier]
+        t['finish_time'] = time.time()
+        t['duration'] = t['finish_time'] - t['begin_time']
+
+    def begin_subtier(self, tier, subtier, dirs):
+        """Record that execution of a subtier has begun."""
+        st = self.tiers[tier]['subtiers'][subtier]
+        st['begin_time'] = time.time()
+
+        for d in dirs:
+            st['dirs'][d] = dict(
+                begin_time=None,
+                finish_time=None,
+                duration=None,
+                concurrent=False,
+            )
+
+        if self.active_subtiers:
+            st['concurrent'] = True
+
+        self.active_subtiers.add(subtier)
+
+    def finish_subtier(self, tier, subtier):
+        """Record that execution of a subtier has finished."""
+        st = self.tiers[tier]['subtiers'][subtier]
+        st['finish_time'] = time.time()
+
+        self.active_subtiers.remove(subtier)
+        if self.active_subtiers:
+            st['concurrent'] = True
+
+        # A subtier may not have directories.
+        try:
+            del self.active_dirs[subtier]
+        except KeyError:
+            pass
+
+        st['duration'] = st['finish_time'] - st['begin_time']
+
+    def begin_dir(self, tier, subtier, d):
+        """Record that execution of a directory has begun."""
+        entry = self.tiers[tier]['subtiers'][subtier]['dirs'][d]
+        entry['begin_time'] = time.time()
+
+        self.active_dirs.setdefault(subtier, set()).add(d)
+
+        if len(self.active_dirs[subtier]) > 1:
+            entry['concurrent'] = True
+
+    def finish_dir(self, tier, subtier, d):
+        """Record that execution of a directory has finished."""
+        st = self.tiers[tier]['subtiers'][subtier]
+        st['dirs_complete'] += 1
+
+        entry = st['dirs'][d]
+        entry['finish_time'] = time.time()
+
+        self.active_dirs[subtier].remove(d)
+        entry['duration'] = entry['finish_time'] - entry['begin_time']
+
+        if self.active_dirs[subtier]:
+            entry['concurrent'] = True
+
+    def tier_status(self):
+        for tier, state in self.tiers.items():
+            active = self.active_tier == tier
+            finished = state['finish_time'] is not None
+
+            yield tier, active, finished
+
+    def current_subtier_status(self):
+        for subtier, state in self.tiers[self.active_tier]['subtiers'].items():
+            active = subtier in self.active_subtiers
+            finished = state['finish_time'] is not None
+
+            yield subtier, active, finished
+
+    def current_dirs_status(self):
+        for subtier, dirs in self.active_dirs.items():
+            st = self.tiers[self.active_tier]['subtiers'][subtier]
+            yield subtier, st['dirs'].keys(), dirs, st['dirs_complete']
+
+
+class BuildMonitor(MozbuildObject):
     """Monitors the output of the build."""
 
-    def __init__(self, topobjdir, warnings_path):
+    def init(self, warnings_path):
         """Create a new monitor.
 
         warnings_path is a path of a warnings database to use.
         """
         self._warnings_path = warnings_path
 
-        self.tiers = []
-        self.subtiers = []
-        self.current_tier = None
-        self.current_subtier = None
-        self.current_tier_dirs = []
-        self.current_tier_static_dirs = []
-        self.current_subtier_dirs = []
-        self.current_subtier_started = set()
-        self.current_subtier_finished = set()
-        self.current_tier_dir = None
-        self.current_tier_dir_index = 0
+        self.tiers = TierStatus()
 
         self.warnings_database = WarningsDatabase()
         if os.path.exists(warnings_path):
             try:
                 self.warnings_database.load_from_file(warnings_path)
             except ValueError:
                 os.remove(warnings_path)
 
         self._warnings_collector = WarningsCollector(
-            database=self.warnings_database, objdir=topobjdir)
+            database=self.warnings_database, objdir=self.topobjdir)
 
     def start(self):
         """Record the start of the build."""
         self.start_time = time.time()
         self._finder_start_cpu = self._get_finder_cpu_usage()
 
     def on_line(self, line):
         """Consume a line of output from the build system.
@@ -88,61 +216,38 @@ class BuildMonitor(object):
         """
         if line.startswith('BUILDSTATUS'):
             args = line.split()[1:]
 
             action = args.pop(0)
             update_needed = True
 
             if action == 'TIERS':
-                self.tiers = args
-                update_needed = False
-            elif action == 'SUBTIERS':
-                self.subtiers = args
-                update_needed = False
-            elif action == 'STATICDIRS':
-                self.current_tier_static_dirs = args
-                update_needed = False
-            elif action == 'DIRS':
-                self.current_tier_dirs = args
+                self.tiers.set_tiers(args)
                 update_needed = False
             elif action == 'TIER_START':
-                assert len(args) == 1
-                self.current_tier = args[0]
-                self.current_subtier = None
-                self.current_tier_dirs = []
-                self.current_subtier_started = set()
-                self.current_subtier_finished = set()
-                self.current_tier_dir = None
+                tier = args[0]
+                subtiers = args[1:]
+                self.tiers.begin_tier(tier, subtiers)
             elif action == 'TIER_FINISH':
-                assert len(args) == 1
-                assert args[0] == self.current_tier
+                tier, = args
+                self.tiers.finish_tier(tier)
             elif action == 'SUBTIER_START':
-                assert len(args) == 2
+                tier, subtier = args[0:2]
+                dirs = args[2:]
+                self.tiers.begin_subtier(tier, subtier, dirs)
+            elif action == 'SUBTIER_FINISH':
                 tier, subtier = args
-                assert tier == self.current_tier
-                self.current_subtier = subtier
-                if subtier == 'static':
-                    self.current_subtier_dirs = self.current_tier_static_dirs
-                else:
-                    self.current_subtier_dirs = self.current_tier_dirs
-                self.current_tier_dir_index = 0
-                self.current_subtier_started.add(subtier)
-            elif action == 'SUBTIER_FINISH':
-                assert len(args) == 2
-                tier, subtier = args
-                assert tier == self.current_tier
-                self.current_subtier_finished.add(subtier)
+                self.tiers.finish_subtier(tier, subtier)
             elif action == 'TIERDIR_START':
-                assert len(args) == 1
-                self.current_tier_dir = args[0]
-                self.current_tier_dir_index += 1
+                tier, subtier, d = args
+                self.tiers.begin_dir(tier, subtier, d)
             elif action == 'TIERDIR_FINISH':
-                assert len(args) == 1
-                assert self.current_tier_dir == args[0]
+                tier, subtier, d = args
+                self.tiers.finish_dir(tier, subtier, d)
             else:
                 raise Exception('Unknown build status: %s' % action)
 
             return BuildOutputResult(None, update_needed, False)
 
         warning = None
 
         try:
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -108,55 +108,71 @@ class BuildProgressFooter(object):
         self._fh.write(self._t.clear_eos())
 
     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:
+        tiers = self._monitor.tiers
+
+        if not tiers.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:
+        for tier, active, finished in tiers.tier_status():
+            if active:
                 parts.extend([('underline_yellow', tier), ' '])
-                current_encountered = True
-            elif not current_encountered:
+            elif finished:
                 parts.extend([('green', tier), ' '])
             else:
                 parts.extend([tier, ' '])
 
         parts.extend([('bold', 'SUBTIER'), ':', ' '])
-        for subtier in self._monitor.subtiers:
-            if subtier in self._monitor.current_subtier_finished:
+        for subtier, active, finished in tiers.current_subtier_status():
+            if active:
+                parts.extend([('underline_yellow', subtier), ' '])
+            elif finished:
                 parts.extend([('green', subtier), ' '])
-            elif subtier in self._monitor.current_subtier_started:
-                parts.extend([('underline_yellow', 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), ')',
-            ])
+        if tiers.active_dirs:
+            parts.extend([('bold', 'DIRECTORIES'), ': '])
+            have_dirs = False
+
+            for subtier, all_dirs, active_dirs, complete in tiers.current_dirs_status():
+                if len(all_dirs) < 2:
+                    continue
+
+                have_dirs = True
+
+                parts.extend([
+                    '%02d' % (complete + 1),
+                    '/',
+                    '%02d' % len(all_dirs),
+                    ' ',
+                    '(',
+                ])
+                for d in active_dirs:
+                    parts.extend([
+                        ('magenta', d), ' ,'
+                    ])
+                parts[-1] = ')'
+
+            if not have_dirs:
+                parts = parts[0:-2]
 
         # 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
@@ -269,17 +285,18 @@ class Build(MachCommandBase):
                      help='Do not add extra make dependencies.')
     @CommandArgument('-v', '--verbose', action='store_true',
         help='Verbose output for what commands the build is running.')
     def build(self, what=None, disable_extra_make_dependencies=None, jobs=0, verbose=False):
         from mozbuild.controller.building import BuildMonitor
         from mozbuild.util import resolve_target_to_make
 
         warnings_path = self._get_state_filename('warnings.json')
-        monitor = BuildMonitor(self.topobjdir, warnings_path)
+        monitor = self._spawn(BuildMonitor)
+        monitor.init(warnings_path)
 
         with BuildOutputManager(self.log_manager, monitor) as output:
             monitor.start()
 
             if what:
                 top_make = os.path.join(self.topobjdir, 'Makefile')
                 if not os.path.exists(top_make):
                     print('Your tree has not been configured yet. Please run '