Reapply "Introduce a threading module"

Bug: b/304992619
Change-Id: I819161b78c3ef71332599ecfd350e177ce3d68cd
This reverts commit 60e39f6277d5b9cd8a4d2b56ee5423a0714b473a.
diff --git a/d8_r8/main/build.gradle.kts b/d8_r8/main/build.gradle.kts
index 3aeb083..46141e9 100644
--- a/d8_r8/main/build.gradle.kts
+++ b/d8_r8/main/build.gradle.kts
@@ -26,6 +26,7 @@
 java {
   sourceSets.main.configure {
     java.srcDir(getRoot().resolveAll("src", "main", "java"))
+    resources.srcDirs(getRoot().resolveAll("src", "main", "resources"))
     resources.srcDirs(getRoot().resolveAll("third_party", "api_database", "api_database"))
   }
   sourceCompatibility = JvmCompatibility.sourceCompatibility
@@ -199,6 +200,7 @@
     dependsOn(resourceShrinkerJarTask)
     dependsOn(gradle.includedBuild("shared").task(":downloadDeps"))
     from(sourceSets.main.get().output)
+    exclude("com/android/tools/r8/threading/providers/**")
     from(keepAnnoJarTask.outputs.files.map(::zipTree))
     from(resourceShrinkerJarTask.outputs.files.map(::zipTree))
     from(getRoot().resolve("LICENSE"))
@@ -212,9 +214,27 @@
     archiveFileName.set("r8-full-exclude-deps.jar")
   }
 
+  val threadingModuleBlockingJar by registering(Zip::class) {
+    from(sourceSets.main.get().output)
+    include("com/android/tools/r8/threading/providers/blocking/**")
+    destinationDirectory.set(getRoot().resolveAll("build", "libs"))
+    archiveFileName.set("threading-module-blocking.jar")
+  }
+
+  val threadingModuleSingleThreadedJar by registering(Zip::class) {
+    from(sourceSets.main.get().output)
+    include("com/android/tools/r8/threading/providers/singlethreaded/**")
+    destinationDirectory.set(getRoot().resolveAll("build", "libs"))
+    archiveFileName.set("threading-module-single-threaded.jar")
+  }
+
   val depsJar by registering(Zip::class) {
     dependsOn(gradle.includedBuild("shared").task(":downloadDeps"))
     dependsOn(resourceShrinkerDepsTask)
+    dependsOn(threadingModuleBlockingJar)
+    dependsOn(threadingModuleSingleThreadedJar)
+    from(threadingModuleBlockingJar.get().outputs.getFiles().map(::zipTree))
+    from(threadingModuleSingleThreadedJar.get().outputs.getFiles().map(::zipTree))
     from(mainJarDependencies().map(::zipTree))
     from(resourceShrinkerDepsTask.outputs.files.map(::zipTree))
     from(consolidatedLicense)
diff --git a/d8_r8/test/build.gradle.kts b/d8_r8/test/build.gradle.kts
index dfd5d24..d4489e8 100644
--- a/d8_r8/test/build.gradle.kts
+++ b/d8_r8/test/build.gradle.kts
@@ -136,10 +136,10 @@
   }
 
   fun Exec.assembleR8Lib(
-          inputJarProvider: Task,
-          generatedKeepRulesProvider: TaskProvider<Exec>,
-          lib: List<File>,
-          artifactName: String) {
+    inputJarProvider: Task,
+    generatedKeepRulesProvider: TaskProvider<Exec>,
+    classpath: List<File>,
+    artifactName: String) {
     dependsOn(generatedKeepRulesProvider, inputJarProvider, r8WithRelocatedDepsTask)
     val inputJar = inputJarProvider.getSingleOutputFile()
     val r8WithRelocatedDepsJar = r8WithRelocatedDepsTask.getSingleOutputFile()
@@ -148,17 +148,17 @@
             generatedKeepRulesProvider.getSingleOutputFile(),
             // TODO(b/294351878): Remove once enum issue is fixed
             getRoot().resolveAll("src", "main", "keep_r8resourceshrinker.txt"))
-    inputs.files(listOf(r8WithRelocatedDepsJar, inputJar).union(keepRuleFiles).union(lib))
+    inputs.files(listOf(r8WithRelocatedDepsJar, inputJar).union(keepRuleFiles).union(classpath))
     val outputJar = getRoot().resolveAll("build", "libs", artifactName)
     outputs.file(outputJar)
     commandLine = createR8LibCommandLine(
-            r8WithRelocatedDepsJar,
-            inputJar,
-            outputJar,
-            keepRuleFiles,
-            excludingDepsVariant = lib.isNotEmpty(),
-            debugVariant = false,
-            lib = lib)
+      r8WithRelocatedDepsJar,
+      inputJar,
+      outputJar,
+      keepRuleFiles,
+      excludingDepsVariant = classpath.isNotEmpty(),
+      debugVariant = false,
+      classpath = classpath)
   }
 
   val assembleR8LibNoDeps by registering(Exec::class) {
diff --git a/d8_r8/test_modules/tests_java_8/build.gradle.kts b/d8_r8/test_modules/tests_java_8/build.gradle.kts
index cb1f81e..8b0bc68 100644
--- a/d8_r8/test_modules/tests_java_8/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_8/build.gradle.kts
@@ -137,11 +137,13 @@
     systemProperty(
       "R8_RUNTIME_PATH",
       mainCompileTask.outputs.files.getAsPath().split(File.pathSeparator)[0] +
-        File.pathSeparator + mainDepsJarTask.outputs.files.singleFile)
+        File.pathSeparator + mainDepsJarTask.outputs.files.singleFile +
+        File.pathSeparator + getRoot().resolveAll("src", "main", "resources"))
     systemProperty(
       "RETRACE_RUNTIME_PATH",
       mainCompileTask.outputs.files.getAsPath().split(File.pathSeparator)[0] +
-        File.pathSeparator + mainDepsJarTask.outputs.files.singleFile)
+        File.pathSeparator + mainDepsJarTask.outputs.files.singleFile +
+        File.pathSeparator + getRoot().resolveAll("src", "main", "resources"))
     systemProperty("R8_DEPS", mainDepsJarTask.outputs.files.singleFile)
     systemProperty("com.android.tools.r8.artprofilerewritingcompletenesscheck", "true")
   }
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index 6ccf2e6..74a107a 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -36,6 +36,7 @@
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.MainDexInfo;
+import com.android.tools.r8.threading.TaskCollection;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ClassProvider;
@@ -47,7 +48,6 @@
 import com.android.tools.r8.utils.LibraryClassCollection;
 import com.android.tools.r8.utils.MainDexListParser;
 import com.android.tools.r8.utils.StringDiagnostic;
-import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
@@ -61,7 +61,6 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
 import java.util.stream.Collectors;
 
 public class ApplicationReader {
@@ -129,8 +128,8 @@
 
     timing.begin("DexApplication.read");
     final LazyLoadedDexApplication.Builder builder = DexApplication.builder(options, timing);
+    TaskCollection<?> tasks = new TaskCollection<>(options, executorService);
     try {
-      List<Future<?>> futures = new ArrayList<>();
       // Still preload some of the classes, primarily for two reasons:
       // (a) class lazy loading is not supported for DEX files
       //     now and current implementation of parallel DEX file
@@ -138,10 +137,10 @@
       // (b) some of the class file resources don't provide information
       //     about class descriptor.
       // TODO: try and preload less classes.
-      readProguardMap(proguardMap, builder, executorService, futures);
-      ClassReader classReader = new ClassReader(executorService, futures);
+      readProguardMap(proguardMap, builder, tasks);
+      ClassReader classReader = new ClassReader(tasks);
       classReader.readSources();
-      ThreadUtils.awaitFutures(futures);
+      tasks.await();
       flags = classReader.getDexApplicationReadFlags();
       builder.setFlags(flags);
       classReader.initializeLazyClassCollection(builder);
@@ -268,34 +267,30 @@
   }
 
   private void readProguardMap(
-      StringResource map,
-      DexApplication.Builder<?> builder,
-      ExecutorService executorService,
-      List<Future<?>> futures) {
+      StringResource map, DexApplication.Builder<?> builder, TaskCollection<?> tasks)
+      throws ExecutionException {
     // Read the Proguard mapping file in parallel with DexCode and DexProgramClass items.
     if (map == null) {
       return;
     }
-    futures.add(
-        executorService.submit(
-            () -> {
-              try {
-                builder.setProguardMap(
-                    ClassNameMapper.mapperFromString(
-                        map.getString(),
-                        options.reporter,
-                        options.mappingComposeOptions().allowEmptyMappedRanges,
-                        options.testing.enableExperimentalMapFileVersion,
-                        true));
-              } catch (IOException | ResourceException e) {
-                throw new CompilationError("Failure to read proguard map file", e, map.getOrigin());
-              }
-            }));
+    tasks.submit(
+        () -> {
+          try {
+            builder.setProguardMap(
+                ClassNameMapper.mapperFromString(
+                    map.getString(),
+                    options.reporter,
+                    options.mappingComposeOptions().allowEmptyMappedRanges,
+                    options.testing.enableExperimentalMapFileVersion,
+                    true));
+          } catch (IOException | ResourceException e) {
+            throw new CompilationError("Failure to read proguard map file", e, map.getOrigin());
+          }
+        });
   }
 
   private final class ClassReader {
-    private final ExecutorService executorService;
-    private final List<Future<?>> futures;
+    private final TaskCollection<?> tasks;
 
     // We use concurrent queues to collect classes
     // since the classes can be collected concurrently.
@@ -315,9 +310,8 @@
     private boolean hasReadProgramResourceFromCf = false;
     private boolean hasReadProgramResourceFromDex = false;
 
-    ClassReader(ExecutorService executorService, List<Future<?>> futures) {
-      this.executorService = executorService;
-      this.futures = futures;
+    ClassReader(TaskCollection<?> tasks) {
+      this.tasks = tasks;
     }
 
     public DexApplicationReadFlags getDexApplicationReadFlags() {
@@ -328,7 +322,7 @@
     }
 
     private void readDexSources(List<ProgramResource> dexSources, Queue<DexProgramClass> classes)
-        throws IOException, ResourceException {
+        throws IOException, ResourceException, ExecutionException {
       if (dexSources.isEmpty()) {
         return;
       }
@@ -367,13 +361,11 @@
         ApplicationReaderMap applicationReaderMap = ApplicationReaderMap.getInstance(options);
         if (!options.testing.dexContainerExperiment) {
           for (DexParser<DexProgramClass> dexParser : dexParsers) {
-            futures.add(
-                executorService.submit(
-                    () -> {
-                      dexParser.addClassDefsTo(
-                          classes::add,
-                          applicationReaderMap); // Depends on Methods, Code items etc.
-                    }));
+            tasks.submit(
+                () -> {
+                  dexParser.addClassDefsTo(
+                      classes::add, applicationReaderMap); // Depends on Methods, Code items etc.
+                });
           }
         } else {
           // All Dex parsers use the same DEX reader, so don't process in parallel.
@@ -430,7 +422,8 @@
     }
 
     private void readClassSources(
-        List<ProgramResource> classSources, Queue<DexProgramClass> classes) {
+        List<ProgramResource> classSources, Queue<DexProgramClass> classes)
+        throws ExecutionException {
       if (classSources.isEmpty()) {
         return;
       }
@@ -447,18 +440,11 @@
               PROGRAM);
       // Read classes in parallel.
       for (ProgramResource input : classSources) {
-        futures.add(
-            executorService.submit(
-                () -> {
-                  reader.read(input);
-                  // No other way to have a void callable, but we want the IOException from read
-                  // to be wrapped into an ExecutionException.
-                  return null;
-                }));
+        tasks.submit(() -> reader.read(input));
       }
     }
 
-    void readSources() throws IOException, ResourceException {
+    void readSources() throws IOException, ResourceException, ExecutionException {
       Collection<ProgramResource> resources = inputApp.computeAllProgramResources();
       List<ProgramResource> dexResources = new ArrayList<>(resources.size());
       List<ProgramResource> cfResources = new ArrayList<>(resources.size());
diff --git a/src/main/java/com/android/tools/r8/threading/TaskCollection.java b/src/main/java/com/android/tools/r8/threading/TaskCollection.java
new file mode 100644
index 0000000..4fcaaaf
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/threading/TaskCollection.java
@@ -0,0 +1,43 @@
+// Copyright (c) 2023, the R8 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.
+
+package com.android.tools.r8.threading;
+
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ThrowingAction;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+public class TaskCollection<T> {
+
+  private final ThreadingModule threadingModule;
+  private final ExecutorService executorService;
+
+  private final List<Future<T>> futures = new ArrayList<>();
+
+  public TaskCollection(InternalOptions options, ExecutorService executorService) {
+    this.threadingModule = options.getThreadingModule();
+    this.executorService = executorService;
+  }
+
+  public <E extends Exception> void submit(ThrowingAction<E> task) throws ExecutionException {
+    submit(
+        () -> {
+          task.execute();
+          return null;
+        });
+  }
+
+  public void submit(Callable<T> task) throws ExecutionException {
+    futures.add(threadingModule.submit(task, executorService));
+  }
+
+  public void await() throws ExecutionException {
+    threadingModule.awaitFutures(futures);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/threading/ThreadingModule.java b/src/main/java/com/android/tools/r8/threading/ThreadingModule.java
new file mode 100644
index 0000000..b839e88
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/threading/ThreadingModule.java
@@ -0,0 +1,43 @@
+// Copyright (c) 2023, the R8 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.
+
+package com.android.tools.r8.threading;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.errors.Unreachable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+/**
+ * Threading module interface to enable non-blocking usage of R8.
+ *
+ * <p>The threading module has multiple implementations outside the main R8 jar. The concrete
+ * implementations are loaded via a Java service loader. Since these implementations are dynamically
+ * loaded the interface they implement must be kept.
+ */
+@Keep
+public interface ThreadingModule {
+  <T> Future<T> submit(Callable<T> task, ExecutorService executorService) throws ExecutionException;
+
+  <T> void awaitFutures(List<Future<T>> futures) throws ExecutionException;
+
+  class Loader {
+
+    public static ThreadingModuleProvider load() {
+      ServiceLoader<ThreadingModuleProvider> providers =
+          ServiceLoader.load(ThreadingModuleProvider.class);
+      // Don't use `Optional findFirst()` here as it hits a desugared-library issue.
+      Iterator<ThreadingModuleProvider> iterator = providers.iterator();
+      if (iterator.hasNext()) {
+        return iterator.next();
+      }
+      throw new Unreachable("Failure to service-load a provider for the threading module");
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/threading/ThreadingModuleProvider.java b/src/main/java/com/android/tools/r8/threading/ThreadingModuleProvider.java
new file mode 100644
index 0000000..ea401c9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/threading/ThreadingModuleProvider.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2023, the R8 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.
+
+package com.android.tools.r8.threading;
+
+import com.android.tools.r8.Keep;
+
+/**
+ * Interface to obtain a threading module.
+ *
+ * <p>The provider is loaded via Java service loader so its interface must be kept.
+ */
+@Keep
+public interface ThreadingModuleProvider {
+
+  ThreadingModule create();
+}
diff --git a/src/main/java/com/android/tools/r8/threading/providers/blocking/ThreadingModuleBlocking.java b/src/main/java/com/android/tools/r8/threading/providers/blocking/ThreadingModuleBlocking.java
new file mode 100644
index 0000000..326c946
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/threading/providers/blocking/ThreadingModuleBlocking.java
@@ -0,0 +1,44 @@
+// Copyright (c) 2023, the R8 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.
+
+package com.android.tools.r8.threading.providers.blocking;
+
+import com.android.tools.r8.threading.ThreadingModule;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+public class ThreadingModuleBlocking implements ThreadingModule {
+
+  @Override
+  public <T> Future<T> submit(Callable<T> task, ExecutorService executorService) {
+    return executorService.submit(task);
+  }
+
+  @Override
+  public <T> void awaitFutures(List<Future<T>> futures) throws ExecutionException {
+    Iterator<? extends Future<?>> it = futures.iterator();
+    try {
+      while (it.hasNext()) {
+        it.next().get();
+      }
+    } catch (InterruptedException e) {
+      throw new RuntimeException("Interrupted while waiting for future.", e);
+    } finally {
+      // In case we get interrupted or one of the threads throws an exception, still wait for all
+      // further work to make sure synchronization guarantees are met. Calling cancel unfortunately
+      // does not guarantee that the task at hand actually terminates before cancel returns.
+      while (it.hasNext()) {
+        try {
+          it.next().get();
+        } catch (Throwable t) {
+          // Ignore any new Exception.
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/threading/providers/blocking/ThreadingModuleBlockingProvider.java b/src/main/java/com/android/tools/r8/threading/providers/blocking/ThreadingModuleBlockingProvider.java
new file mode 100644
index 0000000..0c390ee
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/threading/providers/blocking/ThreadingModuleBlockingProvider.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2023, the R8 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.
+
+package com.android.tools.r8.threading.providers.blocking;
+
+import com.android.tools.r8.threading.ThreadingModule;
+import com.android.tools.r8.threading.ThreadingModuleProvider;
+
+public class ThreadingModuleBlockingProvider implements ThreadingModuleProvider {
+
+  @Override
+  public ThreadingModule create() {
+    return new ThreadingModuleBlocking();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/threading/providers/singlethreaded/ThreadingModuleSingleThreadedProvider.java b/src/main/java/com/android/tools/r8/threading/providers/singlethreaded/ThreadingModuleSingleThreadedProvider.java
new file mode 100644
index 0000000..6174077
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/threading/providers/singlethreaded/ThreadingModuleSingleThreadedProvider.java
@@ -0,0 +1,50 @@
+// Copyright (c) 2023, the R8 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.
+
+package com.android.tools.r8.threading.providers.singlethreaded;
+
+import com.android.tools.r8.threading.ThreadingModule;
+import com.android.tools.r8.threading.ThreadingModuleProvider;
+import com.google.common.util.concurrent.Futures;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+public class ThreadingModuleSingleThreadedProvider implements ThreadingModuleProvider {
+
+  @Override
+  public ThreadingModule create() {
+    return new ThreadingModuleSingleThreaded();
+  }
+
+  public static class ThreadingModuleSingleThreaded implements ThreadingModule {
+
+    @Override
+    public <T> Future<T> submit(Callable<T> task, ExecutorService executorService)
+        throws ExecutionException {
+      try {
+        T value = task.call();
+        return Futures.immediateFuture(value);
+      } catch (Exception e) {
+        throw new ExecutionException(e);
+      }
+    }
+
+    @Override
+    public <T> void awaitFutures(List<Future<T>> futures) throws ExecutionException {
+      assert allDone(futures);
+    }
+
+    private <T> boolean allDone(List<Future<T>> futures) {
+      for (Future<?> future : futures) {
+        if (!future.isDone()) {
+          return false;
+        }
+      }
+      return true;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 51593eb..1fe0c94 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -102,6 +102,7 @@
 import com.android.tools.r8.shaking.GlobalKeepInfoConfiguration;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
+import com.android.tools.r8.threading.ThreadingModule;
 import com.android.tools.r8.utils.IROrdering.IdentityIROrdering;
 import com.android.tools.r8.utils.IROrdering.NondeterministicIROrdering;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
@@ -237,6 +238,8 @@
 
   public List<Consumer<InspectorImpl>> outputInspections = Collections.emptyList();
 
+  private ThreadingModule lazyThreadingModule = null;
+
   // Constructor for testing and/or other utilities.
   public InternalOptions() {
     reporter = new Reporter();
@@ -285,6 +288,13 @@
     }
   }
 
+  public ThreadingModule getThreadingModule() {
+    if (lazyThreadingModule == null) {
+      lazyThreadingModule = ThreadingModule.Loader.load().create();
+    }
+    return lazyThreadingModule;
+  }
+
   private void keepDebugRelatedInformation() {
     assert !proguardConfiguration.isObfuscating();
     getProguardConfiguration().getKeepAttributes().sourceFile = true;
@@ -302,11 +312,6 @@
     protoShrinking.enableEnumLiteProtoShrinking = true;
   }
 
-  public InternalOptions withModifications(Consumer<InternalOptions> consumer) {
-    consumer.accept(this);
-    return this;
-  }
-
   void disableAllOptimizations() {
     disableGlobalOptimizations();
     enableNameReflectionOptimization = false;
diff --git a/src/main/resources/META-INF/services/com.android.tools.r8.threading.ThreadingModuleProvider b/src/main/resources/META-INF/services/com.android.tools.r8.threading.ThreadingModuleProvider
new file mode 100644
index 0000000..f54c274
--- /dev/null
+++ b/src/main/resources/META-INF/services/com.android.tools.r8.threading.ThreadingModuleProvider
@@ -0,0 +1,2 @@
+com.android.tools.r8.threading.providers.blocking.ThreadingModuleBlockingProvider
+com.android.tools.r8.threading.providers.singlethreaded.ThreadingModuleSingleThreadedProvider
diff --git a/src/test/bootstrap/com/android/tools/r8/bootstrap/SanityCheck.java b/src/test/bootstrap/com/android/tools/r8/bootstrap/SanityCheck.java
index d44f4c7..a30a06f 100644
--- a/src/test/bootstrap/com/android/tools/r8/bootstrap/SanityCheck.java
+++ b/src/test/bootstrap/com/android/tools/r8/bootstrap/SanityCheck.java
@@ -12,6 +12,8 @@
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.utils.ZipUtils;
@@ -27,16 +29,30 @@
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
+@RunWith(Parameterized.class)
 public class SanityCheck extends TestBase {
 
   private static final String SRV_PREFIX = "META-INF/services/";
   private static final String METADATA_EXTENSION =
       "com.android.tools.r8.jetbrains.kotlinx.metadata.internal.extensions.MetadataExtensions";
   private static final String EXT_IN_SRV = SRV_PREFIX + METADATA_EXTENSION;
+  private static final String THREADING_MODULE_SERVICE_FILE =
+      "META-INF/services/com.android.tools.r8.threading.ThreadingModuleProvider";
 
-    private void checkJarContent(
-      Path jar, boolean allowDirectories, Predicate<String> entryTester)
+  @Parameters
+  public static TestParametersCollection data() {
+    return TestParameters.builder().withNoneRuntime().build();
+  }
+
+  public SanityCheck(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  private void checkJarContent(Path jar, boolean allowDirectories, Predicate<String> entryTester)
       throws Exception {
     ZipFile zipFile;
     try {
@@ -60,6 +76,8 @@
         // Allow.
       } else if (name.equals("LICENSE")) {
         licenseSeen = true;
+      } else if (name.equals(THREADING_MODULE_SERVICE_FILE)) {
+        // Allow.
       } else if (entryTester.test(name)) {
         // Allow.
       } else if (apiDatabaseFiles.contains(name)) {