Repository: pantsbuild/pants

Issue:
Pathological performance for v2 `--changed list` with (and without) pantsd
seen in master:

```
$ time ./pants --changed-parent=HEAD~60 list >/dev/null

real    4m28.457s
user    3m1.087s
sys     1m26.155s
```

```
$ time PANTS_ENABLE_PANTSD=True ./pants --changed-parent=HEAD~60 list >/dev/null

real    9m10.494s
user    0m1.154s
sys     0m0.813s
```

subsequent re-runs with a warm daemon do not improve the pantsd timings, which points to work done outside of the scheduler.


Discussion:
non-daemon profile:

![image](https://user-images.githubusercontent.com/1883523/37071794-29fae540-2173-11e8-97ce-b5093392b26b.png)

daemon profile:

PANTS_PROFILE:

![image](https://user-images.githubusercontent.com/1883523/37072501-c7d6f9ae-2176-11e8-90e5-ac7436b5db29.png)

PANTSD_PROFILE:

![image](https://user-images.githubusercontent.com/1883523/37072518-ddb09758-2176-11e8-9303-6e2393681eb4.png)

which seems to indicate we're running the changed computation 2x in the case of pantsd. 😱 
looks like this likely regressed here: https://github.com/pantsbuild/pants/commit/0510f023803c8be26461e2105f2aa19c15c53851#diff-8f22c4fe82f27bbabdf008a75c340b22R116

Resolving patch:
diff --git a/src/python/pants/engine/legacy/source_mapper.py b/src/python/pants/engine/legacy/source_mapper.py
--- a/src/python/pants/engine/legacy/source_mapper.py
+++ b/src/python/pants/engine/legacy/source_mapper.py
@@ -7,6 +7,8 @@
 
 import os
 
+import six
+
 from pants.base.specs import AscendantAddresses, SingleAddress
 from pants.build_graph.address import parse_spec
 from pants.build_graph.source_mapper import SourceMapper
@@ -75,13 +77,15 @@ def iter_target_addresses_for_sources(self, sources):
     sources_set = set(sources)
     subjects = [AscendantAddresses(directory=d) for d in self._unique_dirs_for_sources(sources_set)]
 
+    # Uniqify all transitive hydrated targets.
+    hydrated_target_to_address = {}
     for hydrated_targets in self._scheduler.product_request(HydratedTargets, subjects):
       for hydrated_target in hydrated_targets.dependencies:
-        legacy_address = hydrated_target.adaptor.address
-
-        # Handle BUILD files.
-        if any(LegacyAddressMapper.is_declaring_file(legacy_address, f) for f in sources_set):
-          yield legacy_address
-        else:
-          if any(self._owns_source(source, hydrated_target) for source in sources_set):
-            yield legacy_address
+        if hydrated_target not in hydrated_target_to_address:
+          hydrated_target_to_address[hydrated_target] = hydrated_target.adaptor.address
+
+    for hydrated_target, legacy_address in six.iteritems(hydrated_target_to_address):
+      # Handle BUILD files.
+      if (any(LegacyAddressMapper.is_declaring_file(legacy_address, f) for f in sources_set) or
+          any(self._owns_source(source, hydrated_target) for source in sources_set)):
+        yield legacy_address
diff --git a/src/python/pants/source/wrapped_globs.py b/src/python/pants/source/wrapped_globs.py
--- a/src/python/pants/source/wrapped_globs.py
+++ b/src/python/pants/source/wrapped_globs.py
@@ -13,7 +13,7 @@
 from twitter.common.dirutil.fileset import Fileset
 
 from pants.base.build_environment import get_buildroot
-from pants.util.dirutil import fast_relpath
+from pants.util.dirutil import fast_relpath, fast_relpath_optional
 from pants.util.memo import memoized_property
 from pants.util.meta import AbstractClass
 
@@ -104,8 +104,8 @@ def __repr__(self):
     return 'EagerFilesetWithSpec(rel_root={!r}, files={!r})'.format(self.rel_root, self.files)
 
   def matches(self, path_from_buildroot):
-    path_relative_to_rel_root = os.path.relpath(path_from_buildroot, self.rel_root)
-    return path_relative_to_rel_root in self._files
+    path_relative_to_rel_root = fast_relpath_optional(path_from_buildroot, self.rel_root)
+    return path_relative_to_rel_root is not None and path_relative_to_rel_root in self._files
 
 
 class LazyFilesetWithSpec(FilesetWithSpec):


Test patch: