Version 1.10.0-dev.1.5

svn merge -c 45170 https://dart.googlecode.com/svn/branches/bleeding_edge trunk
svn merge -c 45183 https://dart.googlecode.com/svn/branches/bleeding_edge trunk

git-svn-id: http://dart.googlecode.com/svn/trunk@45201 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/pkg/analysis_server/lib/src/analysis_server.dart b/pkg/analysis_server/lib/src/analysis_server.dart
index 77a5aad..4989c15 100644
--- a/pkg/analysis_server/lib/src/analysis_server.dart
+++ b/pkg/analysis_server/lib/src/analysis_server.dart
@@ -18,10 +18,10 @@
 import 'package:analysis_server/src/services/correction/namespace.dart';
 import 'package:analysis_server/src/services/index/index.dart';
 import 'package:analysis_server/src/services/search/search_engine.dart';
+import 'package:analysis_server/src/source/optimizing_pub_package_map_provider.dart';
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:analyzer/instrumentation/instrumentation.dart';
 import 'package:analyzer/plugin/plugin.dart';
-import 'package:analyzer/source/package_map_provider.dart';
 import 'package:analyzer/src/generated/ast.dart';
 import 'package:analyzer/src/generated/element.dart';
 import 'package:analyzer/src/generated/engine.dart';
@@ -249,7 +249,7 @@
    * running a full analysis server.
    */
   AnalysisServer(this.channel, this.resourceProvider,
-      PackageMapProvider packageMapProvider, Index _index,
+      OptimizingPubPackageMapProvider packageMapProvider, Index _index,
       AnalysisServerOptions analysisServerOptions, this.defaultSdk,
       this.instrumentationService, {this.rethrowExceptions: true})
       : index = _index,
@@ -1161,7 +1161,8 @@
     Set<AnalysisContext> contexts = new HashSet<AnalysisContext>();
     resources.forEach((Resource resource) {
       if (resource is Folder) {
-        contexts.addAll(contextDirectoryManager.contextsInAnalysisRoot(resource));
+        contexts
+            .addAll(contextDirectoryManager.contextsInAnalysisRoot(resource));
       }
     });
     return contexts;
@@ -1299,7 +1300,8 @@
   StreamController<ContextsChangedEvent> _onContextsChangedController;
 
   ServerContextManager(this.analysisServer, ResourceProvider resourceProvider,
-      PackageMapProvider packageMapProvider, InstrumentationService service)
+      OptimizingPubPackageMapProvider packageMapProvider,
+      InstrumentationService service)
       : super(resourceProvider, packageMapProvider, service) {
     _onContextsChangedController =
         new StreamController<ContextsChangedEvent>.broadcast();
diff --git a/pkg/analysis_server/lib/src/context_manager.dart b/pkg/analysis_server/lib/src/context_manager.dart
index b52f6ad..bd3dc86 100644
--- a/pkg/analysis_server/lib/src/context_manager.dart
+++ b/pkg/analysis_server/lib/src/context_manager.dart
@@ -8,9 +8,9 @@
 import 'dart:collection';
 
 import 'package:analysis_server/src/analysis_server.dart';
+import 'package:analysis_server/src/source/optimizing_pub_package_map_provider.dart';
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:analyzer/instrumentation/instrumentation.dart';
-import 'package:analyzer/source/package_map_provider.dart';
 import 'package:analyzer/source/package_map_resolver.dart';
 import 'package:analyzer/src/generated/engine.dart';
 import 'package:analyzer/src/generated/java_io.dart';
@@ -82,7 +82,7 @@
    * Provider which is used to determine the mapping from package name to
    * package folder.
    */
-  final PackageMapProvider _packageMapProvider;
+  final OptimizingPubPackageMapProvider _packageMapProvider;
 
   /**
    * The instrumentation service used to report instrumentation data.
@@ -137,6 +137,20 @@
   }
 
   /**
+   * Return a list containing all of the contexts contained in the given
+   * [analysisRoot].
+   */
+  List<AnalysisContext> contextsInAnalysisRoot(Folder analysisRoot) {
+    List<AnalysisContext> contexts = <AnalysisContext>[];
+    _contexts.forEach((Folder contextFolder, _ContextInfo info) {
+      if (analysisRoot.isOrContains(contextFolder.path)) {
+        contexts.add(info.context);
+      }
+    });
+    return contexts;
+  }
+
+  /**
    * We have finished computing the package map.
    */
   void endComputePackageMap() {
@@ -163,20 +177,6 @@
   }
 
   /**
-   * Return a list containing all of the contexts contained in the given
-   * [analysisRoot].
-   */
-  List<AnalysisContext> contextsInAnalysisRoot(Folder analysisRoot) {
-    List<AnalysisContext> contexts = <AnalysisContext>[];
-    _contexts.forEach((Folder contextFolder, _ContextInfo info) {
-      if (analysisRoot.isOrContains(contextFolder.path)) {
-        contexts.add(info.context);
-      }
-    });
-    return contexts;
-  }
-
-  /**
    * Rebuild the set of contexts from scratch based on the data last sent to
    * setRoots(). Only contexts contained in the given list of analysis [roots]
    * will be rebuilt, unless the list is `null`, in which case every context
@@ -388,16 +388,17 @@
    */
   UriResolver _computePackageUriResolver(Folder folder, _ContextInfo info) {
     if (info.packageRoot != null) {
-      info.packageMapDependencies = new Set<String>();
+      info.packageMapInfo = null;
       return new PackageUriResolver([new JavaFile(info.packageRoot)]);
     } else {
       beginComputePackageMap();
-      PackageMapInfo packageMapInfo;
+      OptimizingPubPackageMapInfo packageMapInfo;
       ServerPerformanceStatistics.pub.makeCurrentWhile(() {
-        packageMapInfo = _packageMapProvider.computePackageMap(folder);
+        packageMapInfo =
+            _packageMapProvider.computePackageMap(folder, info.packageMapInfo);
       });
       endComputePackageMap();
-      info.packageMapDependencies = packageMapInfo.dependencies;
+      info.packageMapInfo = packageMapInfo;
       if (packageMapInfo.packageMap == null) {
         return null;
       }
@@ -594,7 +595,8 @@
         break;
     }
 
-    if (info.packageMapDependencies.contains(path)) {
+    if (info.packageMapInfo != null &&
+        info.packageMapInfo.isChangedDependency(path, resourceProvider)) {
       _recomputePackageUriResolver(info);
     }
   }
@@ -744,10 +746,11 @@
   Map<String, Source> sources = new HashMap<String, Source>();
 
   /**
-   * Dependencies of the context's package map.
-   * If any of these files changes, the package map needs to be recomputed.
+   * Info returned by the last call to
+   * [OptimizingPubPackageMapProvider.computePackageMap], or `null` if the
+   * package map hasn't been computed for this context yet.
    */
-  Set<String> packageMapDependencies;
+  OptimizingPubPackageMapInfo packageMapInfo;
 
   _ContextInfo(this.folder, File pubspecFile, this.children, this.packageRoot) {
     pubspecPath = pubspecFile.path;
diff --git a/pkg/analysis_server/lib/src/socket_server.dart b/pkg/analysis_server/lib/src/socket_server.dart
index e7ad5ac..16814af 100644
--- a/pkg/analysis_server/lib/src/socket_server.dart
+++ b/pkg/analysis_server/lib/src/socket_server.dart
@@ -10,10 +10,10 @@
 import 'package:analysis_server/src/protocol.dart';
 import 'package:analysis_server/src/services/index/index.dart';
 import 'package:analysis_server/src/services/index/local_file_index.dart';
+import 'package:analysis_server/src/source/optimizing_pub_package_map_provider.dart';
 import 'package:analyzer/file_system/physical_file_system.dart';
 import 'package:analyzer/instrumentation/instrumentation.dart';
 import 'package:analyzer/plugin/plugin.dart';
-import 'package:analyzer/source/pub_package_map_provider.dart';
 import 'package:analyzer/src/generated/sdk_io.dart';
 
 /**
@@ -75,7 +75,7 @@
     }
 
     analysisServer = new AnalysisServer(serverChannel, resourceProvider,
-        new PubPackageMapProvider(resourceProvider, defaultSdk), index,
+        new OptimizingPubPackageMapProvider(resourceProvider, defaultSdk), index,
         analysisServerOptions, defaultSdk, instrumentationService,
         rethrowExceptions: false);
     _initializeHandlers(analysisServer);
diff --git a/pkg/analysis_server/lib/src/source/caching_pub_package_map_provider.dart b/pkg/analysis_server/lib/src/source/caching_pub_package_map_provider.dart
index e6d0d59..f6c9bb7 100644
--- a/pkg/analysis_server/lib/src/source/caching_pub_package_map_provider.dart
+++ b/pkg/analysis_server/lib/src/source/caching_pub_package_map_provider.dart
@@ -24,6 +24,9 @@
  * [PubPackageMapProvider] extension which caches pub list results.
  * These results are cached in memory and in a single place on disk that is
  * shared cross session and between different simultaneous sessions.
+ *
+ * TODO(paulberry): before this class is used again, it should be ported over
+ * to extend OptimizingPubPackageMapProvider instead of PubPackageMapProvider.
  */
 class CachingPubPackageMapProvider extends PubPackageMapProvider {
   static const cacheKey = 'pub_list_cache';
diff --git a/pkg/analysis_server/lib/src/source/optimizing_pub_package_map_provider.dart b/pkg/analysis_server/lib/src/source/optimizing_pub_package_map_provider.dart
new file mode 100644
index 0000000..ccda5f9
--- /dev/null
+++ b/pkg/analysis_server/lib/src/source/optimizing_pub_package_map_provider.dart
@@ -0,0 +1,134 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library source.optimizing_pub_package_map_provider;
+
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:analyzer/source/package_map_provider.dart';
+import 'package:analyzer/source/pub_package_map_provider.dart';
+import 'package:analyzer/src/generated/sdk_io.dart';
+
+/**
+ * Extension of [PackageMapInfo] that tracks the modification timestamps of
+ * pub dependencies.  This allows the analysis server to avoid making redundant
+ * calls to "pub list" when nothing has changed.
+ */
+class OptimizingPubPackageMapInfo extends PackageMapInfo {
+  /**
+   * Map from file path to the file's modification timestamp prior to running
+   * "pub list".  Since the set of dependencies is not always known prior to
+   * running "pub list", some or all of the dependencies may be missing from
+   * this map.
+   */
+  final Map<String, int> modificationTimes;
+
+  OptimizingPubPackageMapInfo(Map<String, List<Folder>> packageMap,
+      Set<String> dependencies, this.modificationTimes)
+      : super(packageMap, dependencies);
+
+  /**
+   * Return `true` if the given [path] is listed as a dependency, and we cannot
+   * prove using modification timestamps that it is unchanged.
+   * [resourceProvider] is used (if necessary) to read the [path]'s
+   * modification time.
+   */
+  bool isChangedDependency(String path, ResourceProvider resourceProvider) {
+    if (!dependencies.contains(path)) {
+      // Path is not a dependency.
+      return false;
+    }
+    int lastModificationTime = modificationTimes[path];
+    if (lastModificationTime != null) {
+      Resource resource = resourceProvider.getResource(path);
+      if (resource is File) {
+        try {
+          if (resource.modificationStamp == lastModificationTime) {
+            // Path is a dependency, but it hasn't changed since the last run
+            // of "pub list".
+            return false;
+          }
+        } on FileSystemException {
+          // Path is a dependency, but we can't read its timestamp.  Assume
+          // it's changed to be safe.
+        }
+      }
+    }
+    // Path is a dependency, and we couldn't prove that it hadn't changed.
+    // Assume it's changed to be safe.
+    return true;
+  }
+}
+
+/**
+ * Extension of [PubPackageMapProvider] that outputs additional information to
+ * allow the analysis server to avoid making redundant calls to "pub list" when
+ * nothing has changed.
+ */
+class OptimizingPubPackageMapProvider extends PubPackageMapProvider {
+  OptimizingPubPackageMapProvider(
+      ResourceProvider resourceProvider, DirectoryBasedDartSdk sdk, [RunPubList runPubList])
+      : super(resourceProvider, sdk, runPubList);
+
+  /**
+   * Compute a package map for the given folder by executing "pub list".  If
+   * [previousInfo] is provided, it is used as a guess of which files the
+   * package map is likely to depend on; the modification times of those files
+   * are captured prior to executing "pub list" so that they can be used to
+   * avoid making redundant calls to "pub list" in the future.
+   *
+   * Also, in the case where dependencies can't be determined because of an
+   * error, the dependencies from [previousInfo] will be preserved.
+   */
+  OptimizingPubPackageMapInfo computePackageMap(Folder folder,
+      [OptimizingPubPackageMapInfo previousInfo]) {
+    // Prior to running "pub list", read the modification timestamps of all of
+    // the old dependencies (if known).
+    Map<String, int> modificationTimes = <String, int>{};
+    if (previousInfo != null) {
+      for (String path in previousInfo.dependencies) {
+        Resource resource = resourceProvider.getResource(path);
+        if (resource is File) {
+          try {
+            modificationTimes[path] = resource.modificationStamp;
+          } on FileSystemException {
+            // File no longer exists.  Don't record a timestamp for it; this
+            // will ensure that if the file reappears, we will re-run "pub
+            // list" regardless of the timestamp it reappears with.
+          }
+        }
+      }
+    }
+
+    // Try running "pub list".
+    PackageMapInfo info = super.computePackageMap(folder);
+    if (info == null) {
+      // Computing the package map resulted in an error.  Merge the old
+      // dependencies with the new ones, if possible.
+      info = super.computePackageMapError(folder);
+      if (previousInfo != null) {
+        info.dependencies.addAll(previousInfo.dependencies);
+      }
+    }
+
+    // Discard any elements of modificationTimes that are no longer
+    // dependencies.
+    if (previousInfo != null) {
+      for (String dependency
+          in previousInfo.dependencies.difference(info.dependencies)) {
+        modificationTimes.remove(dependency);
+      }
+    }
+
+    // Bundle the modificationTimes with the other info.
+    return new OptimizingPubPackageMapInfo(
+        info.packageMap, info.dependencies, modificationTimes);
+  }
+
+  @override
+  PackageMapInfo computePackageMapError(Folder folder) {
+    // Return null to indicate to our override of computePackageMap that there
+    // was an error, so it can compute dependencies correctly.
+    return null;
+  }
+}
diff --git a/pkg/analysis_server/test/context_manager_test.dart b/pkg/analysis_server/test/context_manager_test.dart
index 4a4ec36..2be5cb8 100644
--- a/pkg/analysis_server/test/context_manager_test.dart
+++ b/pkg/analysis_server/test/context_manager_test.dart
@@ -7,10 +7,10 @@
 import 'dart:collection';
 
 import 'package:analysis_server/src/context_manager.dart';
+import 'package:analysis_server/src/source/optimizing_pub_package_map_provider.dart';
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:analyzer/file_system/memory_file_system.dart';
 import 'package:analyzer/instrumentation/instrumentation.dart';
-import 'package:analyzer/source/package_map_provider.dart';
 import 'package:analyzer/source/package_map_resolver.dart';
 import 'package:analyzer/src/generated/engine.dart';
 import 'package:analyzer/src/generated/source.dart';
@@ -858,6 +858,42 @@
     });
   }
 
+  test_watch_modifyPackageMapDependency_redundantly() async {
+    // Create two dependency files
+    String dependencyPath1 = posix.join(projPath, 'dep1');
+    String dependencyPath2 = posix.join(projPath, 'dep2');
+    resourceProvider.newFile(dependencyPath1, 'contents');
+    resourceProvider.newFile(dependencyPath2, 'contents');
+    packageMapProvider.dependencies.add(dependencyPath1);
+    packageMapProvider.dependencies.add(dependencyPath2);
+    // Create a dart file
+    String dartFilePath = posix.join(projPath, 'main.dart');
+    resourceProvider.newFile(dartFilePath, 'contents');
+    // Verify that the created context has the expected empty package map.
+    manager.setRoots(<String>[projPath], <String>[], <String, String>{});
+    _checkPackageMap(projPath, isEmpty);
+    expect(packageMapProvider.computeCount, 1);
+    // Set up a different package map
+    String packagePath = '/package/foo';
+    resourceProvider.newFolder(packagePath);
+    packageMapProvider.packageMap = {'foo': projPath};
+    // Change both dependencies.
+    resourceProvider.modifyFile(dependencyPath1, 'new contents');
+    resourceProvider.modifyFile(dependencyPath2, 'new contents');
+    // Arrange for the next call to computePackageMap to return the correct
+    // timestamps for the dependencies.
+    packageMapProvider.modificationTimes = <String, int>{};
+    for (String path in [dependencyPath1, dependencyPath2]) {
+      File resource = resourceProvider.getResource(path);
+      packageMapProvider.modificationTimes[path] = resource.modificationStamp;
+    }
+    // This should cause the new package map to be picked up, by executing
+    // computePackageMap just one additional time.
+    await pumpEventQueue();
+    _checkPackageMap(projPath, equals(packageMapProvider.packageMap));
+    expect(packageMapProvider.computeCount, 2);
+  }
+
   /**
    * Verify that package URI's for source files in [path] will be resolved
    * using a package map matching [expectation].
@@ -917,7 +953,7 @@
       <String, UriResolver>{};
 
   TestContextManager(MemoryResourceProvider resourceProvider,
-      PackageMapProvider packageMapProvider)
+      OptimizingPubPackageMapProvider packageMapProvider)
       : super(resourceProvider, packageMapProvider,
           InstrumentationService.NULL_SERVICE);
 
diff --git a/pkg/analysis_server/test/domain_completion_test.dart b/pkg/analysis_server/test/domain_completion_test.dart
index e727c6c..6ffc8ef 100644
--- a/pkg/analysis_server/test/domain_completion_test.dart
+++ b/pkg/analysis_server/test/domain_completion_test.dart
@@ -17,9 +17,9 @@
 import 'package:analysis_server/src/services/index/index.dart' show Index;
 import 'package:analysis_server/src/services/index/local_memory_index.dart';
 import 'package:analysis_server/src/services/search/search_engine.dart';
+import 'package:analysis_server/src/source/optimizing_pub_package_map_provider.dart';
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:analyzer/instrumentation/instrumentation.dart';
-import 'package:analyzer/source/package_map_provider.dart';
 import 'package:analyzer/src/generated/engine.dart';
 import 'package:analyzer/src/generated/sdk.dart';
 import 'package:analyzer/src/generated/source.dart';
@@ -598,13 +598,13 @@
   Stream<SourcesChangedEvent> get onSourcesChanged => mockStream;
 
   @override
-  TimestampedData<String> getContents(Source source) {
-    return source.contents;
+  bool exists(Source source) {
+    return source != null && source.exists();
   }
 
   @override
-  bool exists(Source source) {
-    return source != null && source.exists();
+  TimestampedData<String> getContents(Source source) {
+    return source.contents;
   }
 
   noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
@@ -647,7 +647,7 @@
   final MockContext mockContext = new MockContext();
 
   Test_AnalysisServer(ServerCommunicationChannel channel,
-      ResourceProvider resourceProvider, PackageMapProvider packageMapProvider,
+      ResourceProvider resourceProvider, OptimizingPubPackageMapProvider packageMapProvider,
       Index index, AnalysisServerOptions analysisServerOptions,
       DartSdk defaultSdk, InstrumentationService instrumentationService)
       : super(channel, resourceProvider, packageMapProvider, index,
diff --git a/pkg/analysis_server/test/mocks.dart b/pkg/analysis_server/test/mocks.dart
index be14732..94bc8c6 100644
--- a/pkg/analysis_server/test/mocks.dart
+++ b/pkg/analysis_server/test/mocks.dart
@@ -14,6 +14,7 @@
 import 'package:analysis_server/src/operation/operation.dart';
 import 'package:analysis_server/src/operation/operation_analysis.dart';
 import 'package:analysis_server/src/protocol.dart' hide Element, ElementKind;
+import 'package:analysis_server/src/source/optimizing_pub_package_map_provider.dart';
 import 'package:analyzer/file_system/file_system.dart' as resource;
 import 'package:analyzer/file_system/memory_file_system.dart' as resource;
 import 'package:analyzer/source/package_map_provider.dart';
@@ -152,7 +153,7 @@
 /**
  * A mock [PackageMapProvider].
  */
-class MockPackageMapProvider implements PackageMapProvider {
+class MockPackageMapProvider implements OptimizingPubPackageMapProvider {
   /**
    * Package map that will be returned by the next call to [computePackageMap].
    */
@@ -169,12 +170,42 @@
    */
   Set<String> dependencies = new Set<String>();
 
+  /**
+   * Modification times that will be returned by the next call to
+   * [computePackageMap].  This will be filtered to include only those paths
+   * mentioned in the `dependencies` field of [computePackageMap]'s
+   * `previousInfo` argument.
+   */
+  Map<String, int> modificationTimes = <String, int>{};
+
+  /**
+   * Number of times [computePackageMap] has been called.
+   */
+  int computeCount = 0;
+
   @override
-  PackageMapInfo computePackageMap(resource.Folder folder) {
-    if (packageMaps != null) {
-      return new PackageMapInfo(packageMaps[folder.path], dependencies);
+  OptimizingPubPackageMapInfo computePackageMap(resource.Folder folder,
+      [OptimizingPubPackageMapInfo previousInfo]) {
+    ++computeCount;
+    Map<String, int> filteredModificationTimes = <String, int>{};
+    if (previousInfo != null) {
+      for (String dependency in previousInfo.dependencies) {
+        if (modificationTimes.containsKey(dependency)) {
+          filteredModificationTimes[dependency] = modificationTimes[dependency];
+        }
+      }
     }
-    return new PackageMapInfo(packageMap, dependencies);
+    if (packageMaps != null) {
+      return new OptimizingPubPackageMapInfo(
+          packageMaps[folder.path], dependencies, filteredModificationTimes);
+    }
+    return new OptimizingPubPackageMapInfo(
+        packageMap, dependencies, filteredModificationTimes);
+  }
+
+  noSuchMethod(Invocation invocation) {
+    // No other methods should be called.
+    return super.noSuchMethod(invocation);
   }
 }
 
diff --git a/pkg/analysis_server/test/source/optimizing_pub_package_map_provider_test.dart b/pkg/analysis_server/test/source/optimizing_pub_package_map_provider_test.dart
new file mode 100644
index 0000000..769b683
--- /dev/null
+++ b/pkg/analysis_server/test/source/optimizing_pub_package_map_provider_test.dart
@@ -0,0 +1,245 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library test.source.optimizing_pub_package_map_provider;
+
+import 'dart:convert';
+import 'dart:io' as io;
+
+import 'package:analysis_server/src/source/optimizing_pub_package_map_provider.dart';
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:analyzer/file_system/memory_file_system.dart';
+import 'package:path/path.dart';
+import 'package:unittest/unittest.dart';
+
+import '../reflective_tests.dart';
+
+main() {
+  groupSep = ' | ';
+  runReflectiveTests(OptimizingPubPackageMapProviderTest);
+  runReflectiveTests(OptimizingPubPackageMapInfoTest);
+}
+
+@reflectiveTest
+class OptimizingPubPackageMapInfoTest {
+  MemoryResourceProvider resourceProvider;
+
+  int createFile(String path) {
+    return resourceProvider.newFile(path, 'contents').modificationStamp;
+  }
+
+  void createFolder(String path) {
+    resourceProvider.newFolder(path);
+  }
+
+  void modifyFile(String path) {
+    resourceProvider.modifyFile(path, 'contents');
+  }
+
+  void setUp() {
+    resourceProvider = new MemoryResourceProvider();
+  }
+
+  test_isChangedDependency_fileNotPresent() {
+    String path = '/dep';
+    int timestamp = 1;
+    OptimizingPubPackageMapInfo info =
+        new OptimizingPubPackageMapInfo({}, [path].toSet(), {path: timestamp});
+    expect(info.isChangedDependency(path, resourceProvider), isTrue);
+  }
+
+  test_isChangedDependency_matchingTimestamp() {
+    String path = '/dep';
+    int timestamp = createFile(path);
+    OptimizingPubPackageMapInfo info =
+        new OptimizingPubPackageMapInfo({}, [path].toSet(), {path: timestamp});
+    expect(info.isChangedDependency(path, resourceProvider), isFalse);
+  }
+
+  test_isChangedDependency_mismatchedTimestamp() {
+    String path = '/dep';
+    int timestamp = createFile(path);
+    OptimizingPubPackageMapInfo info =
+        new OptimizingPubPackageMapInfo({}, [path].toSet(), {path: timestamp});
+    modifyFile(path);
+    expect(info.isChangedDependency(path, resourceProvider), isTrue);
+  }
+
+  test_isChangedDependency_nonDependency() {
+    OptimizingPubPackageMapInfo info =
+        new OptimizingPubPackageMapInfo({}, ['/dep1'].toSet(), {});
+    expect(info.isChangedDependency('/dep2', resourceProvider), isFalse);
+  }
+
+  test_isChangedDependency_nonFile() {
+    String path = '/dep';
+    int timestamp = 1;
+    createFolder(path);
+    OptimizingPubPackageMapInfo info =
+        new OptimizingPubPackageMapInfo({}, [path].toSet(), {path: timestamp});
+    expect(info.isChangedDependency(path, resourceProvider), isTrue);
+  }
+
+  test_isChangedDependency_noTimestampInfo() {
+    String path = '/dep';
+    OptimizingPubPackageMapInfo info =
+        new OptimizingPubPackageMapInfo({}, [path].toSet(), {});
+    expect(info.isChangedDependency(path, resourceProvider), isTrue);
+  }
+}
+
+@reflectiveTest
+class OptimizingPubPackageMapProviderTest {
+  MemoryResourceProvider resourceProvider;
+  OptimizingPubPackageMapProvider provider;
+  Folder projectFolder;
+  io.ProcessResult pubListResult;
+
+  void setPubListError() {
+    pubListResult = new _MockProcessResult(0, 1, '', 'ERROR');
+  }
+
+  void setPubListResult({Map<String, String> packages: const {},
+      List<String> input_files: const []}) {
+    pubListResult = new _MockProcessResult(0, 0,
+        JSON.encode({'packages': packages, 'input_files': input_files}), '');
+  }
+
+  void setUp() {
+    resourceProvider = new MemoryResourceProvider();
+    provider = new OptimizingPubPackageMapProvider(
+        resourceProvider, null, _runPubList);
+    projectFolder = resourceProvider.newFolder('/my/proj');
+  }
+
+  test_computePackageMap_noPreviousInfo() {
+    String dep = posix.join(projectFolder.path, 'dep');
+    String pkgName = 'foo';
+    String pkgPath = '/pkg/foo';
+    setPubListResult(packages: {pkgName: pkgPath}, input_files: [dep]);
+    OptimizingPubPackageMapInfo info =
+        provider.computePackageMap(projectFolder);
+    expect(info.dependencies, hasLength(1));
+    expect(info.dependencies, contains(dep));
+    expect(info.packageMap, hasLength(1));
+    expect(info.packageMap, contains(pkgName));
+    expect(info.packageMap[pkgName], hasLength(1));
+    expect(info.packageMap[pkgName][0].path, pkgPath);
+    expect(info.modificationTimes, isEmpty);
+  }
+
+  test_computePackageMap_noPreviousInfo_pubListError() {
+    String pubspecLock = posix.join(projectFolder.path, 'pubspec.lock');
+    setPubListError();
+    OptimizingPubPackageMapInfo info =
+        provider.computePackageMap(projectFolder);
+    expect(info.dependencies, hasLength(1));
+    expect(info.dependencies, contains(pubspecLock));
+    expect(info.packageMap, isNull);
+    expect(info.modificationTimes, isEmpty);
+  }
+
+  test_computePackageMap_withPreviousInfo() {
+    String dep = posix.join(projectFolder.path, 'dep');
+    int timestamp = resourceProvider.newFile(dep, 'contents').modificationStamp;
+    setPubListResult(input_files: [dep]);
+    OptimizingPubPackageMapInfo info1 =
+        provider.computePackageMap(projectFolder);
+    OptimizingPubPackageMapInfo info2 =
+        provider.computePackageMap(projectFolder, info1);
+    expect(info2.dependencies, hasLength(1));
+    expect(info2.dependencies, contains(dep));
+    expect(info2.modificationTimes, hasLength(1));
+    expect(info2.modificationTimes, contains(dep));
+    expect(info2.modificationTimes[dep], timestamp);
+  }
+
+  test_computePackageMap_withPreviousInfo_newDependency() {
+    String dep = posix.join(projectFolder.path, 'dep');
+    resourceProvider.newFile(dep, 'contents').modificationStamp;
+    setPubListResult(input_files: []);
+    OptimizingPubPackageMapInfo info1 =
+        provider.computePackageMap(projectFolder);
+    setPubListResult(input_files: [dep]);
+    OptimizingPubPackageMapInfo info2 =
+        provider.computePackageMap(projectFolder, info1);
+    expect(info2.modificationTimes, isEmpty);
+  }
+
+  test_computePackageMap_withPreviousInfo_oldDependencyNoLongerAFile() {
+    String dep = posix.join(projectFolder.path, 'dep');
+    resourceProvider.newFile(dep, 'contents').modificationStamp;
+    setPubListResult(input_files: [dep]);
+    OptimizingPubPackageMapInfo info1 =
+        provider.computePackageMap(projectFolder);
+    resourceProvider.deleteFile(dep);
+    resourceProvider.newFolder(dep);
+    OptimizingPubPackageMapInfo info2 =
+        provider.computePackageMap(projectFolder, info1);
+    expect(info2.modificationTimes, isEmpty);
+  }
+
+  test_computePackageMap_withPreviousInfo_oldDependencyNoLongerPresent() {
+    String dep = posix.join(projectFolder.path, 'dep');
+    resourceProvider.newFile(dep, 'contents').modificationStamp;
+    setPubListResult(input_files: [dep]);
+    OptimizingPubPackageMapInfo info1 =
+        provider.computePackageMap(projectFolder);
+    resourceProvider.deleteFile(dep);
+    OptimizingPubPackageMapInfo info2 =
+        provider.computePackageMap(projectFolder, info1);
+    expect(info2.modificationTimes, isEmpty);
+  }
+
+  test_computePackageMap_withPreviousInfo_oldDependencyNoLongerRelevant() {
+    String dep = posix.join(projectFolder.path, 'dep');
+    resourceProvider.newFile(dep, 'contents').modificationStamp;
+    setPubListResult(input_files: [dep]);
+    OptimizingPubPackageMapInfo info1 =
+        provider.computePackageMap(projectFolder);
+    setPubListResult(input_files: []);
+    OptimizingPubPackageMapInfo info2 =
+        provider.computePackageMap(projectFolder, info1);
+    expect(info2.modificationTimes, isEmpty);
+  }
+
+  test_computePackageMap_withPreviousInfo_pubListError() {
+    String dep = posix.join(projectFolder.path, 'dep');
+    String pubspecLock = posix.join(projectFolder.path, 'pubspec.lock');
+    int timestamp = resourceProvider.newFile(dep, 'contents').modificationStamp;
+    setPubListResult(input_files: [dep]);
+    OptimizingPubPackageMapInfo info1 =
+        provider.computePackageMap(projectFolder);
+    setPubListError();
+    OptimizingPubPackageMapInfo info2 =
+        provider.computePackageMap(projectFolder, info1);
+    expect(info2.dependencies, hasLength(2));
+    expect(info2.dependencies, contains(dep));
+    expect(info2.dependencies, contains(pubspecLock));
+    expect(info2.modificationTimes, hasLength(1));
+    expect(info2.modificationTimes, contains(dep));
+    expect(info2.modificationTimes[dep], timestamp);
+  }
+
+  io.ProcessResult _runPubList(Folder folder) {
+    expect(folder, projectFolder);
+    return pubListResult;
+  }
+}
+
+class _MockProcessResult implements io.ProcessResult {
+  @override
+  final int pid;
+
+  @override
+  final int exitCode;
+
+  @override
+  final stdout;
+
+  @override
+  final stderr;
+
+  _MockProcessResult(this.pid, this.exitCode, this.stdout, this.stderr);
+}
diff --git a/pkg/analysis_server/test/source/test_all.dart b/pkg/analysis_server/test/source/test_all.dart
index 3a3e709..34a617f 100644
--- a/pkg/analysis_server/test/source/test_all.dart
+++ b/pkg/analysis_server/test/source/test_all.dart
@@ -5,10 +5,13 @@
 library test.source;
 
 import 'caching_put_package_map_provider_test.dart' as caching_provider_test;
+import 'optimizing_pub_package_map_provider_test.dart'
+    as optimizing_provider_test;
 import 'package:unittest/unittest.dart';
 
 /// Utility for manually running all tests.
 main() {
   groupSep = ' | ';
   caching_provider_test.main();
+  optimizing_provider_test.main();
 }
diff --git a/tools/VERSION b/tools/VERSION
index c1363d0..f5b7a3b 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -28,4 +28,4 @@
 MINOR 10
 PATCH 0
 PRERELEASE 1
-PRERELEASE_PATCH 4
+PRERELEASE_PATCH 5