Bug 1444171 - Add pgo-generate-only source functionality; r=glandium
authorNathan Froyd <froydnj@gmail.com>
Mon, 09 Jul 2018 18:35:49 -0400
changeset 425538 eb28d28e00717947f59a5db318563fffff80885f
parent 425537 e96225163a763f78c0b7857bfdbb3b999e3ef97b
child 425539 1a27b17ba6a63450707973266f7d19581b0f5a86
push id105092
push userdmajor@mozilla.com
push dateMon, 09 Jul 2018 22:33:58 +0000
treeherdermozilla-inbound@1a27b17ba6a6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersglandium
bugs1444171
milestone63.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 1444171 - Add pgo-generate-only source functionality; r=glandium For clang-cl, we want to add code to libxul that only exists during the PGO generation phase, so we can collect data. The most expedient way to do that is to enable certain files in SOURCES to be marked as to only be compiled during the PGO generation step.
config/rules.mk
mozglue/build/cygprofile.cpp
mozglue/build/moz.build
python/mozbuild/mozbuild/backend/common.py
python/mozbuild/mozbuild/backend/recursivemake.py
python/mozbuild/mozbuild/backend/tup.py
python/mozbuild/mozbuild/frontend/context.py
python/mozbuild/mozbuild/frontend/data.py
python/mozbuild/mozbuild/frontend/emitter.py
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -181,16 +181,20 @@ ifdef MOZ_PROFILE_GENERATE
 SIMPLE_PROGRAMS :=
 endif
 
 ifdef COMPILE_ENVIRONMENT
 ifndef TARGETS
 TARGETS			= $(LIBRARY) $(SHARED_LIBRARY) $(PROGRAM) $(SIMPLE_PROGRAMS) $(HOST_LIBRARY) $(HOST_PROGRAM) $(HOST_SIMPLE_PROGRAMS) $(HOST_SHARED_LIBRARY)
 endif
 
+ifdef MOZ_PROFILE_GENERATE
+CPPSRCS := $(CPPSRCS) $(PGO_GEN_ONLY_CPPSRCS)
+endif
+
 COBJS = $(notdir $(CSRCS:.c=.$(OBJ_SUFFIX)))
 SOBJS = $(notdir $(SSRCS:.S=.$(OBJ_SUFFIX)))
 # CPPSRCS can have different extensions (eg: .cpp, .cc)
 CPPOBJS = $(notdir $(addsuffix .$(OBJ_SUFFIX),$(basename $(CPPSRCS))))
 CMOBJS = $(notdir $(CMSRCS:.m=.$(OBJ_SUFFIX)))
 CMMOBJS = $(notdir $(CMMSRCS:.mm=.$(OBJ_SUFFIX)))
 # ASFILES can have different extensions (.s, .asm)
 ASOBJS = $(notdir $(addsuffix .$(OBJ_SUFFIX),$(basename $(ASFILES))))
new file mode 100644
--- /dev/null
+++ b/mozglue/build/cygprofile.cpp
@@ -0,0 +1,121 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Copied from Chromium's /src/tools/cygprofile_win/cygprofile.cc.
+
+#include <stdio.h>
+#include <atomic>
+#include <string>
+#include <unordered_set>
+
+#include <windows.h>  // Needs to be included before the others.
+
+#include <dbghelp.h>
+#include <process.h>
+
+#include "mozilla/Sprintf.h"
+#include "mozilla/Types.h"
+
+namespace {
+
+// The main purpose of the order file is to optimize startup time,
+// so capturing the first N function calls is enough.
+static constexpr int kSamplesCapacity = 25 * 1024 * 1024;
+
+void* samples[kSamplesCapacity];
+std::atomic_int num_samples;
+std::atomic_int done;
+
+
+// Symbolize the samples and write them to disk.
+void dump(void*) {
+  HMODULE dbghelp = LoadLibraryA("dbghelp.dll");
+  auto sym_from_addr = reinterpret_cast<decltype(::SymFromAddr)*>(
+      ::GetProcAddress(dbghelp, "SymFromAddr"));
+  auto sym_initialize = reinterpret_cast<decltype(::SymInitialize)*>(
+      ::GetProcAddress(dbghelp, "SymInitialize"));
+  auto sym_set_options = reinterpret_cast<decltype(::SymSetOptions)*>(
+      ::GetProcAddress(dbghelp, "SymSetOptions"));
+
+  // Path to the dump file. %s will be substituted by objdir path.
+  static const char kDumpFile[] = "%s/cygprofile.txt";
+
+  char filename[MAX_PATH];
+  const char* objdir = ::getenv("MOZ_OBJDIR");
+
+  if (!objdir) {
+    fprintf(stderr, "ERROR: cannot determine objdir\n");
+    return;
+  }
+
+  SprintfLiteral(filename, kDumpFile, objdir);
+
+  FILE* f = fopen(filename, "w");
+  if (!f) {
+    fprintf(stderr, "ERROR: Cannot open %s\n", filename);
+    return;
+  }
+
+  sym_initialize(::GetCurrentProcess(), NULL, TRUE);
+  sym_set_options(SYMOPT_DEFERRED_LOADS | SYMOPT_PUBLICS_ONLY);
+  char sym_buf[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)];
+
+  std::unordered_set<void*> seen;
+  std::unordered_set<std::string> seen_names;
+
+  for (void* sample : samples) {
+    // Only print the first call of a function.
+    if (seen.count(sample))
+      continue;
+    seen.insert(sample);
+
+    SYMBOL_INFO* symbol = reinterpret_cast<SYMBOL_INFO*>(sym_buf);
+    symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
+    symbol->MaxNameLen = MAX_SYM_NAME;
+    DWORD64 offset = 0;
+
+    if (sym_from_addr(::GetCurrentProcess(), reinterpret_cast<DWORD64>(sample),
+                      &offset, symbol)) {
+      const char* name = symbol->Name;
+      if (name[0] == '_')
+        name++;
+      if (seen_names.count(name))
+        continue;
+      seen_names.insert(name);
+
+      fprintf(f, "%s\n", name);
+    }
+  }
+
+  fclose(f);
+}
+
+}  // namespace
+
+extern "C" {
+
+MOZ_EXPORT void
+__cyg_profile_func_enter(void* this_fn, void* call_site_unused) {
+  if (done)
+    return;
+
+  // Get our index for the samples array atomically.
+  int n = num_samples++;
+
+  if (n < kSamplesCapacity) {
+    samples[n] = this_fn;
+
+    if (n + 1 == kSamplesCapacity) {
+      // This is the final sample; start dumping the samples to a file (on a
+      // separate thread so as not to disturb the main program).
+      done = 1;
+      _beginthread(dump, 0, nullptr);
+    }
+  }
+}
+
+MOZ_EXPORT void
+__cyg_profile_func_exit(void* this_fn, void* call_site) {}
+
+}  // extern "C"
--- a/mozglue/build/moz.build
+++ b/mozglue/build/moz.build
@@ -26,16 +26,20 @@ if CONFIG['MOZ_ASAN']:
 
 if CONFIG['OS_TARGET'] == 'WINNT':
     DEFFILE = 'mozglue.def'
     # We'll break the DLL blocklist if we immediately load user32.dll
     DELAYLOAD_DLLS += [
         'user32.dll',
     ]
 
+    if CONFIG['MOZ_PGO'] and CONFIG['CC_TYPE'] == 'clang-cl':
+        SOURCES += ['cygprofile.cpp']
+        SOURCES['cygprofile.cpp'].pgo_generate_only = True
+
     if CONFIG['CC_TYPE'] == "msvc":
         CFLAGS += ['-guard:cf']
         CXXFLAGS += ['-guard:cf']
         LDFLAGS += ['-guard:cf']
 
 if CONFIG['MOZ_WIDGET_TOOLKIT']:
 
     if CONFIG['MOZ_MEMORY'] and FORCE_SHARED_LIB:
--- a/python/mozbuild/mozbuild/backend/common.py
+++ b/python/mozbuild/mozbuild/backend/common.py
@@ -204,28 +204,37 @@ class CommonBackend(BuildBackend):
     def _expand_libs(self, input_bin):
         os_libs = []
         shared_libs = []
         static_libs = []
         objs = []
         no_pgo_objs = []
 
         seen_objs = set()
+        seen_pgo_gen_only_objs = set()
         seen_libs = set()
 
         def add_objs(lib):
+            seen_pgo_gen_only_objs.update(lib.pgo_gen_only_objs)
+
             for o in lib.objs:
-                if o not in seen_objs:
-                    seen_objs.add(o)
-                    objs.append(o)
-                    # This is slightly odd, buf for consistency with the
-                    # recursivemake backend we don't replace OBJ_SUFFIX if any
-                    # object in a library has `no_pgo` set.
-                    if lib.no_pgo_objs or lib.no_pgo:
-                        no_pgo_objs.append(o)
+                if o in seen_objs:
+                    continue
+
+                # The front end should keep pgo generate-only objects and
+                # normal objects separate.
+                assert o not in seen_pgo_gen_only_objs
+
+                seen_objs.add(o)
+                objs.append(o)
+                # This is slightly odd, but for consistency with the
+                # recursivemake backend we don't replace OBJ_SUFFIX if any
+                # object in a library has `no_pgo` set.
+                if lib.no_pgo_objs or lib.no_pgo:
+                    no_pgo_objs.append(o)
 
         def expand(lib, recurse_objs, system_libs):
             if isinstance(lib, StaticLibrary):
                 if lib.no_expand_lib:
                     static_libs.append(lib)
                     recurse_objs = False
                 elif recurse_objs:
                     add_objs(lib)
@@ -257,17 +266,18 @@ class CommonBackend(BuildBackend):
                     seen_libs.add(lib)
                     shared_libs.append(lib)
 
         for lib in input_bin.linked_system_libs:
             if lib not in seen_libs:
                 seen_libs.add(lib)
                 os_libs.append(lib)
 
-        return objs, no_pgo_objs, shared_libs, os_libs, static_libs
+        return (objs, sorted(seen_pgo_gen_only_objs), no_pgo_objs, \
+                shared_libs, os_libs, static_libs)
 
     def _make_list_file(self, objdir, objs, name):
         if not objs:
             return None
         list_style = self.environment.substs.get('EXPAND_LIBS_LIST_STYLE')
         list_file_path = mozpath.join(objdir, name)
         objs = [os.path.relpath(o, objdir) for o in objs]
         if list_style == 'linkerscript':
--- a/python/mozbuild/mozbuild/backend/recursivemake.py
+++ b/python/mozbuild/mozbuild/backend/recursivemake.py
@@ -54,16 +54,17 @@ from ..frontend.data import (
     Library,
     Linkable,
     LocalInclude,
     LocalizedFiles,
     LocalizedPreprocessedFiles,
     ObjdirFiles,
     ObjdirPreprocessedFiles,
     PerSourceFlag,
+    PgoGenerateOnlySources,
     Program,
     RustLibrary,
     HostSharedLibrary,
     HostRustLibrary,
     RustProgram,
     RustTests,
     SharedLibrary,
     SimpleProgram,
@@ -474,16 +475,20 @@ class RecursiveMakeBackend(CommonBackend
                 variables.append('GARBAGE')
                 base = backend_file.objdir
             else:
                 base = backend_file.srcdir
             for f in sorted(obj.files):
                 f = mozpath.relpath(f, base)
                 for var in variables:
                     backend_file.write('%s += %s\n' % (var, f))
+        elif isinstance(obj, PgoGenerateOnlySources):
+            assert obj.canonical_suffix == '.cpp'
+            for f in sorted(obj.files):
+                backend_file.write('PGO_GEN_ONLY_CPPSRCS += %s\n' % f)
         elif isinstance(obj, (HostSources, HostGeneratedSources)):
             suffix_map = {
                 '.c': 'HOST_CSRCS',
                 '.mm': 'HOST_CMMSRCS',
                 '.cpp': 'HOST_CPPSRCS',
             }
             variables = [suffix_map[obj.canonical_suffix]]
             if isinstance(obj, GeneratedSources):
@@ -1294,33 +1299,39 @@ class RecursiveMakeBackend(CommonBackend
             return os.path.normpath(mozpath.join(mozpath.relpath(lib.objdir, obj.objdir),
                                                  name))
 
         topobjdir = mozpath.normsep(obj.topobjdir)
         # This will create the node even if there aren't any linked libraries.
         build_target = self._build_target_for_obj(obj)
         self._compile_graph[build_target]
 
-        objs, no_pgo_objs, shared_libs, os_libs, static_libs = self._expand_libs(obj)
+        objs, pgo_gen_objs, no_pgo_objs, shared_libs, os_libs, static_libs = self._expand_libs(obj)
 
         if obj.KIND == 'target':
             obj_target = obj.name
             if isinstance(obj, Program):
                 obj_target = self._pretty_path(obj.output_path, backend_file)
 
             is_unit_test = isinstance(obj, BaseProgram) and obj.is_unit_test
             profile_gen_objs = []
 
-            if (self.environment.substs.get('MOZ_PGO') and
-                self.environment.substs.get('GNU_CC')):
+            doing_pgo = self.environment.substs.get('MOZ_PGO')
+            obj_suffix_change_needed = (self.environment.substs.get('GNU_CC') or
+                                        self.environment.substs.get('CLANG_CL'))
+            if doing_pgo and obj_suffix_change_needed:
                 # We use a different OBJ_SUFFIX for the profile generate phase on
-                # linux. These get picked up via OBJS_VAR_SUFFIX in config.mk.
+                # systems where the pgo generate phase requires instrumentation
+                # that can only be removed by recompiling objects. These get
+                # picked up via OBJS_VAR_SUFFIX in config.mk.
                 if not is_unit_test and not isinstance(obj, SimpleProgram):
                     profile_gen_objs = [o if o in no_pgo_objs else '%s.%s' %
                                         (mozpath.splitext(o)[0], 'i_o') for o in objs]
+                    profile_gen_objs += ['%s.%s' % (mozpath.splitext(o)[0], 'i_o')
+                                         for o in pgo_gen_objs]
 
             def write_obj_deps(target, objs_ref, pgo_objs_ref):
                 if pgo_objs_ref:
                     backend_file.write('ifdef MOZ_PROFILE_GENERATE\n')
                     backend_file.write('%s: %s\n' % (target, pgo_objs_ref))
                     backend_file.write('else\n')
                     backend_file.write('%s: %s\n' % (target, objs_ref))
                     backend_file.write('endif\n')
--- a/python/mozbuild/mozbuild/backend/tup.py
+++ b/python/mozbuild/mozbuild/backend/tup.py
@@ -342,17 +342,17 @@ class TupBackend(CommonBackend):
 
         mkshlib += (
             backend_file.environment.substs['DSO_PIC_CFLAGS'] +
             [backend_file.environment.substs['DSO_LDOPTS']] +
             ['-Wl,-h,%s' % shlib.soname] +
             ['-o', shlib.lib_name]
         )
 
-        objs, _, shared_libs, os_libs, static_libs = self._expand_libs(shlib)
+        objs, _, _, shared_libs, os_libs, static_libs = self._expand_libs(shlib)
         static_libs = self._lib_paths(backend_file.objdir, static_libs)
         shared_libs = self._lib_paths(backend_file.objdir, shared_libs)
 
         list_file_name = '%s.list' % shlib.name.replace('.', '_')
         list_file = self._make_list_file(backend_file.objdir, objs, list_file_name)
 
         rust_linked = self._lib_paths(backend_file.objdir,
                                       (l for l in backend_file.shared_lib.linked_libraries
@@ -397,17 +397,17 @@ class TupBackend(CommonBackend):
                                   output_group=self._shlibs)
 
     def _gen_programs(self, backend_file):
         for p in backend_file.programs:
             self._gen_program(backend_file, p)
 
     def _gen_program(self, backend_file, prog):
         cc_or_cxx = 'CXX' if prog.cxx_link else 'CC'
-        objs, _, shared_libs, os_libs, static_libs = self._expand_libs(prog)
+        objs, _, _, shared_libs, os_libs, static_libs = self._expand_libs(prog)
         static_libs = self._lib_paths(backend_file.objdir, static_libs)
         shared_libs = self._lib_paths(backend_file.objdir, shared_libs)
 
         # Linking some programs will access libraries installed to dist/bin,
         # so depend on the installed libraries here. This can be made more
         # accurate once we start building libraries in their final locations.
         inputs = objs + static_libs + shared_libs + [self._shlibs]
 
@@ -457,17 +457,17 @@ class TupBackend(CommonBackend):
 
 
     def _gen_host_programs(self, backend_file):
         for p in backend_file.host_programs:
             self._gen_host_program(backend_file, p)
 
 
     def _gen_host_program(self, backend_file, prog):
-        _, _, _, extra_libs, _ = self._expand_libs(prog)
+        _, _, _, _, extra_libs, _ = self._expand_libs(prog)
         objs = prog.objs
 
         if isinstance(prog, HostSimpleProgram):
             outputs = [prog.name]
         else:
             outputs = [mozpath.relpath(prog.output_path.full_path,
                                        backend_file.objdir)]
         host_libs = []
@@ -496,17 +496,17 @@ class TupBackend(CommonBackend):
 
 
     def _gen_static_library(self, backend_file):
         ar = [
             backend_file.environment.substs['AR'],
             backend_file.environment.substs['AR_FLAGS'].replace('$@', '%o')
         ]
 
-        objs, _, shared_libs, _, static_libs = self._expand_libs(backend_file.static_lib)
+        objs, _, _, shared_libs, _, static_libs = self._expand_libs(backend_file.static_lib)
         static_libs = self._lib_paths(backend_file.objdir, static_libs)
         shared_libs = self._lib_paths(backend_file.objdir, shared_libs)
 
         inputs = objs + static_libs
 
         cmd = (
             ar +
             inputs
--- a/python/mozbuild/mozbuild/frontend/context.py
+++ b/python/mozbuild/mozbuild/frontend/context.py
@@ -1201,17 +1201,17 @@ SUBCONTEXTS = {cls.__name__: cls for cls
 
 # This defines the set of mutable global variables.
 #
 # Each variable is a tuple of:
 #
 #   (storage_type, input_types, docs)
 
 VARIABLES = {
-    'SOURCES': (ContextDerivedTypedListWithItems(Path, StrictOrderingOnAppendListWithFlagsFactory({'no_pgo': bool, 'flags': List})), list,
+    'SOURCES': (ContextDerivedTypedListWithItems(Path, StrictOrderingOnAppendListWithFlagsFactory({'no_pgo': bool, 'flags': List, 'pgo_generate_only': bool})), list,
         """Source code files.
 
         This variable contains a list of source code files to compile.
         Accepts assembler, C, C++, Objective C/C++.
         """),
 
     'FILES_PER_UNIFIED_FILE': (int, int,
         """The number of source files to compile into each unified source file.
--- a/python/mozbuild/mozbuild/frontend/data.py
+++ b/python/mozbuild/mozbuild/frontend/data.py
@@ -382,28 +382,30 @@ class LinkageMultipleRustLibrariesError(
 class Linkable(ContextDerived):
     """Generic context derived container object for programs and libraries"""
     __slots__ = (
         'cxx_link',
         'lib_defines',
         'linked_libraries',
         'linked_system_libs',
         'no_pgo_sources',
+        'pgo_gen_only_sources',
         'no_pgo',
         'sources',
     )
 
     def __init__(self, context):
         ContextDerived.__init__(self, context)
         self.cxx_link = False
         self.linked_libraries = []
         self.linked_system_libs = []
         self.lib_defines = Defines(context, {})
         self.sources = defaultdict(list)
         self.no_pgo_sources = []
+        self.pgo_gen_only_sources = set()
         self.no_pgo = False
 
     def link_library(self, obj):
         assert isinstance(obj, BaseLibrary)
         if obj.KIND != self.KIND:
             raise LinkageWrongKindError('%s != %s' % (obj.KIND, self.KIND))
         # Linking multiple Rust libraries into an object would result in
         # multiple copies of the Rust standard library, as well as linking
@@ -452,16 +454,20 @@ class Linkable(ContextDerived):
     @property
     def no_pgo_objs(self):
         return self._get_objs(self.no_pgo_sources)
 
     @property
     def objs(self):
         return self._get_objs(self.source_files())
 
+    @property
+    def pgo_gen_only_objs(self):
+        return self._get_objs(self.pgo_gen_only_sources)
+
 
 class BaseProgram(Linkable):
     """Context derived container object for programs, which is a unicode
     string.
 
     This class handles automatically appending a binary suffix to the program
     name.
     If the suffix is not defined, the program name is unchanged.
@@ -946,16 +952,25 @@ class BaseSources(ContextDerived):
 
 class Sources(BaseSources):
     """Represents files to be compiled during the build."""
 
     def __init__(self, context, files, canonical_suffix):
         BaseSources.__init__(self, context, files, canonical_suffix)
 
 
+class PgoGenerateOnlySources(BaseSources):
+    """Represents files to be compiled during the build.
+
+    These files are only used during the PGO generation phase."""
+
+    def __init__(self, context, files):
+        BaseSources.__init__(self, context, files, '.cpp')
+
+
 class GeneratedSources(BaseSources):
     """Represents generated files to be compiled during the build."""
 
     def __init__(self, context, files, canonical_suffix):
         BaseSources.__init__(self, context, files, canonical_suffix)
 
 
 class HostSources(HostMixin, BaseSources):
--- a/python/mozbuild/mozbuild/frontend/emitter.py
+++ b/python/mozbuild/mozbuild/frontend/emitter.py
@@ -52,16 +52,17 @@ from .data import (
     Library,
     Linkable,
     LocalInclude,
     LocalizedFiles,
     LocalizedPreprocessedFiles,
     ObjdirFiles,
     ObjdirPreprocessedFiles,
     PerSourceFlag,
+    PgoGenerateOnlySources,
     WebIDLCollection,
     Program,
     RustLibrary,
     HostRustLibrary,
     RustProgram,
     RustTests,
     SharedLibrary,
     SimpleProgram,
@@ -837,44 +838,60 @@ class TreeMetadataEmitter(LoggingMixin):
             # TODO: objdirs with only host things in them shouldn't need target
             # flags, but there's at least one Makefile.in (in
             # build/unix/elfhack) that relies on the value of LDFLAGS being
             # passed to one-off rules.
             self._compile_dirs.add(context.objdir)
 
         sources = defaultdict(list)
         gen_sources = defaultdict(list)
+        pgo_generate_only = set()
         all_flags = {}
         for symbol in ('SOURCES', 'HOST_SOURCES', 'UNIFIED_SOURCES'):
             srcs = sources[symbol]
             gen_srcs = gen_sources[symbol]
             context_srcs = context.get(symbol, [])
             for f in context_srcs:
                 full_path = f.full_path
                 if isinstance(f, SourcePath):
                     srcs.append(full_path)
                 else:
                     assert isinstance(f, Path)
                     gen_srcs.append(full_path)
                 if symbol == 'SOURCES':
                     flags = context_srcs[f]
                     if flags:
                         all_flags[full_path] = flags
+                    # Files for the generation phase of PGO are unusual, so
+                    # it's not unreasonable to require them to be special.
+                    if flags.pgo_generate_only:
+                        if not isinstance(f, Path):
+                            raise SandboxValidationError('pgo_generate_only file'
+                                'must not be a generated file: %s' % f, context)
+                        if mozpath.splitext(f)[1] != '.cpp':
+                            raise SandboxValidationError('pgo_generate_only file'
+                                'must be a .cpp file: %s' % f, context)
+                        if flags.no_pgo:
+                            raise SandboxValidationError('pgo_generate_only files'
+                                'cannot be marked no_pgo: %s' % f, context)
+                        pgo_generate_only.add(f)
 
                 if isinstance(f, SourcePath) and not os.path.exists(full_path):
                     raise SandboxValidationError('File listed in %s does not '
                         'exist: \'%s\'' % (symbol, full_path), context)
 
         # UNIFIED_SOURCES only take SourcePaths, so there should be no
         # generated source in here
         assert not gen_sources['UNIFIED_SOURCES']
 
         no_pgo = context.get('NO_PGO')
         no_pgo_sources = [f for f, flags in all_flags.iteritems()
                           if flags.no_pgo]
+        pgo_gen_only_sources = set(f for f, flags in all_flags.iteritems()
+                                   if flags.pgo_generate_only)
         if no_pgo:
             if no_pgo_sources:
                 raise SandboxValidationError('NO_PGO and SOURCES[...].no_pgo '
                     'cannot be set at the same time', context)
             passthru.variables['NO_PROFILE_GUIDED_OPTIMIZE'] = no_pgo
         if no_pgo_sources:
             passthru.variables['NO_PROFILE_GUIDED_OPTIMIZE'] = no_pgo_sources
 
@@ -927,16 +944,18 @@ class TreeMetadataEmitter(LoggingMixin):
             for f in itertools.chain(sources[variable], gen_sources[variable]):
                 ext = mozpath.splitext(f)[1]
                 if ext not in allowed_suffixes:
                     raise SandboxValidationError(
                         '%s has an unknown file type.' % f, context)
 
             for srcs, cls in ((sources[variable], klass),
                               (gen_sources[variable], gen_klass)):
+                if variable == 'SOURCES' and pgo_gen_only_sources:
+                    srcs = [s for s in srcs if s not in pgo_gen_only_sources]
                 # Now sort the files to let groupby work.
                 sorted_files = sorted(srcs, key=canonical_suffix_for_file)
                 for canonical_suffix, files in itertools.groupby(
                         sorted_files, canonical_suffix_for_file):
                     if canonical_suffix in ('.cpp', '.mm'):
                         cxx_sources[variable] = True
                     elif canonical_suffix in ('.s', '.S'):
                         self._asm_compile_dirs.add(context.objdir)
@@ -950,29 +969,34 @@ class TreeMetadataEmitter(LoggingMixin):
                     ctxt_sources[variable][canonical_suffix] += sorted(srcs)
                     yield obj
 
         if ctxt_sources:
             for linkable in linkables:
                 for target_var in ('SOURCES', 'UNIFIED_SOURCES'):
                     for suffix, srcs in ctxt_sources[target_var].items():
                         linkable.sources[suffix] += srcs
+                if pgo_gen_only_sources:
+                    linkable.pgo_gen_only_sources = pgo_gen_only_sources
                 if no_pgo_sources:
                     linkable.no_pgo_sources = no_pgo_sources
                 elif no_pgo:
                     linkable.no_pgo = True
             for host_linkable in host_linkables:
                 for suffix, srcs in ctxt_sources['HOST_SOURCES'].items():
                     host_linkable.sources[suffix] += srcs
 
         for f, flags in all_flags.iteritems():
             if flags.flags:
                 ext = mozpath.splitext(f)[1]
                 yield PerSourceFlag(context, f, flags.flags)
 
+        if pgo_generate_only:
+            yield PgoGenerateOnlySources(context, pgo_generate_only)
+
         # If there are any C++ sources, set all the linkables defined here
         # to require the C++ linker.
         for vars, linkable_items in ((('SOURCES', 'UNIFIED_SOURCES'), linkables),
                                      (('HOST_SOURCES',), host_linkables)):
             for var in vars:
                 if cxx_sources[var]:
                     for l in linkable_items:
                         l.cxx_link = True