diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index be8c695..a46b7bf 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -10,6 +10,7 @@
 use Drupal\Core\DrupalKernel;
 use Drupal\Core\Database\Database;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\SystemListing;
 use Drupal\Core\Utility\Title;
 use Drupal\Core\Utility\Error;
 use Symfony\Component\ClassLoader\ApcClassLoader;
@@ -700,10 +701,7 @@ function drupal_get_filename($type, $name, $filename = NULL) {
   if (!empty($filename)) {
     $files[$type][$name] = $filename;
   }
-  elseif (isset($files[$type][$name])) {
-    // nothing
-  }
-  else {
+  elseif (!isset($files[$type][$name])) {
     // Verify that we have an keyvalue service before using it. This is required
     // because this function is called during installation.
     // @todo Inject database connection into KeyValueStore\DatabaseStorage.
@@ -752,14 +750,12 @@ function drupal_get_filename($type, $name, $filename = NULL) {
 
       if (!isset($dirs[$dir][$extension])) {
         $dirs[$dir][$extension] = TRUE;
-        if (!function_exists('drupal_system_listing')) {
-          require_once __DIR__ . '/common.inc';
-        }
         // Scan the appropriate directories for all files with the requested
         // extension, not just the file we are currently looking for. This
         // prevents unnecessary scans from being repeated when this function is
         // called more than once in the same page request.
-        $matches = drupal_system_listing("/^" . DRUPAL_PHP_FUNCTION_PATTERN . "\.$extension$/", $dir);
+        $listing = new SystemListing();
+        $matches = $listing->scan("/^" . DRUPAL_PHP_FUNCTION_PATTERN . "\.$extension$/", $dir);
         foreach ($matches as $matched_name => $file) {
           $files[$type][$matched_name] = $file->uri;
         }
diff --git a/core/includes/common.inc b/core/includes/common.inc
index b97cc5e..f9cea6e 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -17,7 +17,6 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Routing\GeneratorNotInitializedException;
-use Drupal\Core\SystemListingInfo;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Render\Element;
 
@@ -3177,19 +3176,6 @@ function drupal_page_set_cache(Response $response, Request $request) {
 }
 
 /**
- * This function is kept only for backward compatibility.
- *
- * @see \Drupal\Core\SystemListing::scan().
- */
-function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) {
-  // As SystemListing is required to build a dependency injection container
-  // from scratch and SystemListingInfo only extends SystemLising, this
-  // class needs to be hardwired.
-  $listing = new SystemListingInfo();
-  return $listing->scan($mask, $directory, $key, $min_depth);
-}
-
-/**
  * Sets the main page content value for later use.
  *
  * Given the nature of the Drupal page handling, this will be called once with
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 0a57f16..8519961 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -13,7 +13,7 @@
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageManager;
 use Drupal\Core\StringTranslation\Translator\FileTranslation;
-
+use Drupal\Core\SystemListing;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\DependencyInjection\Reference;
@@ -494,7 +494,16 @@ function install_begin_request(&$install_state) {
     // Override the module list with a minimal set of modules.
     $module_handler->setModuleList(array('system' => 'core/modules/system/system.module'));
   }
-  $module_handler->load('system');
+  $module_handler->loadAll();
+
+  // Add list of all available profiles to the installation state.
+  $listing = new SystemListing();
+  $install_state['profiles'] += $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles');
+
+  // Prime drupal_get_filename()'s static cache.
+  foreach ($install_state['profiles'] as $name => $profile) {
+    drupal_get_filename('profile', $name, $profile->uri);
+  }
 
   // Prepare for themed output. We need to run this at the beginning of the
   // page request to avoid a different theme accidentally getting set. (We also
@@ -528,9 +537,6 @@ function install_begin_request(&$install_state) {
 
   // Modify the installation state as appropriate.
   $install_state['completed_task'] = $task;
-
-  // Add the list of available profiles to the installation state.
-  $install_state['profiles'] += drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles');
 }
 
 /**
diff --git a/core/includes/install.inc b/core/includes/install.inc
index 8c91dc8..dc09906 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -9,6 +9,7 @@
 use Drupal\Component\Utility\Crypt;
 use Drupal\Core\Database\Database;
 use Drupal\Core\DrupalKernel;
+use Drupal\Core\SystemListing;
 
 /**
  * Requirement severity -- Informational message only.
@@ -569,15 +570,16 @@ function drupal_verify_profile($install_state) {
   }
   $info = $install_state['profile_info'];
 
-  // Get a list of modules that exist in Drupal's assorted subdirectories.
+  // Get the list of available modules for the selected installation profile.
+  $listing = new SystemListing();
   $present_modules = array();
-  foreach (drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules') as $present_module) {
+  foreach ($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules') as $present_module) {
     $present_modules[] = $present_module->name;
   }
 
   // The installation profile is also a module, which needs to be installed
   // after all the other dependencies have been installed.
-  $present_modules[] = drupal_get_profile();
+  $present_modules[] = $profile;
 
   // Verify that all of the profile's required modules are present.
   $missing_modules = array_diff($info['dependencies'], $present_modules);
diff --git a/core/includes/module.inc b/core/includes/module.inc
index 429f5db..e7cad81 100644
--- a/core/includes/module.inc
+++ b/core/includes/module.inc
@@ -5,6 +5,7 @@
  * API for loading and interacting with Drupal modules.
  */
 
+use Drupal\Core\SystemListing;
 
 /**
  * Builds a list of bootstrap modules and enabled modules and themes.
@@ -285,14 +286,22 @@ function module_uninstall($module_list = array(), $uninstall_dependents = TRUE)
  * Returns an array of modules required by core.
  */
 function drupal_required_modules() {
-  $files = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'modules');
+  $listing = new SystemListing();
+  $files = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules');
   $required = array();
 
   // An installation profile is required and one must always be loaded.
-  $required[] = drupal_get_profile();
+  // The only exception is the installer.
+  if ($profile = drupal_get_profile()) {
+    $required[] = $profile;
+  }
 
   foreach ($files as $name => $file) {
-    $info = \Drupal::service('info_parser')->parse($file->uri);
+    $info_file = dirname($file->uri) . '/' . $file->name . '.info.yml';
+    if (!file_exists($info_file)) {
+      continue;
+    }
+    $info = \Drupal::service('info_parser')->parse($info_file);
     if (!empty($info) && !empty($info['required']) && $info['required']) {
       $required[] = $name;
     }
diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php
index 16bcc1c..2b4f0ad 100644
--- a/core/lib/Drupal/Core/Config/InstallStorage.php
+++ b/core/lib/Drupal/Core/Config/InstallStorage.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Config;
 
+use Drupal\Core\SystemListing;
+
 /**
  * Storage controller used by the Drupal installer.
  *
@@ -110,9 +112,13 @@ public function listAll($prefix = '') {
    */
   protected function getAllFolders() {
     if (!isset($this->folders)) {
-      $this->folders = $this->getComponentNames('profile', array(drupal_get_profile()));
-      $this->folders += $this->getComponentNames('module', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0)));
-      $this->folders += $this->getComponentNames('theme', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes')));
+      $this->folders = array();
+      if ($profile = drupal_get_profile()) {
+        $this->folders += $this->getComponentNames('profile', array($profile));
+      }
+      $listing = new SystemListing();
+      $this->folders += $this->getComponentNames('module', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules')));
+      $this->folders += $this->getComponentNames('theme', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info\.yml$/', 'themes')));
     }
     return $this->folders;
   }
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 46a7fee..169e86c 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -297,19 +297,27 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
   protected function moduleData($module) {
     if (!$this->moduleData) {
       // First, find profiles.
-      $profiles_scanner = new SystemListing();
-      $all_profiles = $profiles_scanner->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles');
-      $profiles = array_keys(array_intersect_key($this->moduleList, $all_profiles));
+      $listing = new SystemListing();
+      $all_profiles = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles');
+      $profiles = array_intersect_key($all_profiles, $this->moduleList);
+
       // If a module is within a profile directory but specifies another
       // profile for testing, it needs to be found in the parent profile.
-      if (($parent_profile_config = $this->configStorage->read('simpletest.settings')) && isset($parent_profile_config['parent_profile']) && $parent_profile_config['parent_profile'] != $profiles[0]) {
+      $settings = $this->configStorage->read('simpletest.settings');
+      $parent_profile = !empty($settings['parent_profile']) ? $settings['parent_profile'] : FALSE;
+      if ($parent_profile && !isset($profiles[$parent_profile])) {
         // In case both profile directories contain the same extension, the
         // actual profile always has precedence.
-        array_unshift($profiles, $parent_profile_config['parent_profile']);
+        $profiles = array($parent_profile => $all_profiles[$parent_profile]) + $profiles;
       }
+
+      $profile_directories = array_map(function ($profile) {
+        return dirname($profile->uri);
+      }, $profiles);
+      $listing->setProfileDirectories($profile_directories);
+
       // Now find modules.
-      $modules_scanner = new SystemListing($profiles);
-      $this->moduleData = $all_profiles + $modules_scanner->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules');
+      $this->moduleData = $all_profiles + $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules');
     }
     return isset($this->moduleData[$module]) ? $this->moduleData[$module] : FALSE;
   }
diff --git a/core/lib/Drupal/Core/Extension/InfoParser.php b/core/lib/Drupal/Core/Extension/InfoParser.php
index 5bb4841..593b34d 100644
--- a/core/lib/Drupal/Core/Extension/InfoParser.php
+++ b/core/lib/Drupal/Core/Extension/InfoParser.php
@@ -22,7 +22,7 @@ class InfoParser implements InfoParserInterface {
    *
    * @var array
    */
-  protected $parsedInfos = array();
+  protected static $parsedInfos = array();
 
   /**
    * Symfony YAML parser object.
@@ -35,29 +35,29 @@ class InfoParser implements InfoParserInterface {
    * {@inheritdoc}
    */
   public function parse($filename) {
-    if (!isset($this->parsedInfos[$filename])) {
+    if (!isset(static::$parsedInfos[$filename])) {
       if (!file_exists($filename)) {
-        $this->parsedInfos[$filename] = array();
+        static::$parsedInfos[$filename] = array();
       }
       else {
         try {
-          $this->parsedInfos[$filename] = $this->getParser()->parse(file_get_contents($filename));
+          static::$parsedInfos[$filename] = $this->getParser()->parse(file_get_contents($filename));
         }
         catch (ParseException $e) {
           $message = String::format("Unable to parse !file. Parser error !error.", array('!file' => $filename, '!error' => $e->getMessage()));
           throw new InfoParserException($message, $filename);
         }
-        $missing_keys = array_diff($this->getRequiredKeys(), array_keys($this->parsedInfos[$filename]));
+        $missing_keys = array_diff($this->getRequiredKeys(), array_keys(static::$parsedInfos[$filename]));
         if (!empty($missing_keys)) {
           $message = format_plural(count($missing_keys), 'Missing required key (!missing_keys) in !file.', 'Missing required keys (!missing_keys) in !file.', array('!missing_keys' => implode(', ', $missing_keys), '!file' => $filename));
           throw new InfoParserException($message, $filename);
         }
-        if (isset($this->parsedInfos[$filename]['version']) && $this->parsedInfos[$filename]['version'] === 'VERSION') {
-          $this->parsedInfos[$filename]['version'] = \Drupal::VERSION;
+        if (isset(static::$parsedInfos[$filename]['version']) && static::$parsedInfos[$filename]['version'] === 'VERSION') {
+          static::$parsedInfos[$filename]['version'] = \Drupal::VERSION;
         }
       }
     }
-    return $this->parsedInfos[$filename];
+    return static::$parsedInfos[$filename];
   }
 
   /**
@@ -80,7 +80,7 @@ protected function getParser() {
    *   An array of required keys.
    */
   protected function getRequiredKeys() {
-    return array('name', 'type');
+    return array('type', 'core', 'name');
   }
 
 }
diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php
index 7c71eba..9722035 100644
--- a/core/lib/Drupal/Core/Extension/ThemeHandler.php
+++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php
@@ -13,7 +13,7 @@
 use Drupal\Core\Config\ConfigFactory;
 use Drupal\Core\Config\ConfigInstallerInterface;
 use Drupal\Core\Routing\RouteBuilder;
-use Drupal\Core\SystemListingInfo;
+use Drupal\Core\SystemListing;
 
 /**
  * Default theme handler using the config system for enabled/disabled themes.
@@ -87,11 +87,11 @@ class ThemeHandler implements ThemeHandlerInterface {
   protected $routeBuilder;
 
   /**
-   * The system listing info
+   * A system listing instance.
    *
-   * @var \Drupal\Core\SystemListingInfo
+   * @var \Drupal\Core\SystemListing
    */
-  protected $systemListingInfo;
+  protected $systemListing;
 
   /**
    * Constructs a new ThemeHandler.
@@ -110,17 +110,17 @@ class ThemeHandler implements ThemeHandlerInterface {
    *   database.
    * @param \Drupal\Core\Routing\RouteBuilder $route_builder
    *   (optional) The route builder to rebuild the routes if a theme is enabled.
-   * @param \Drupal\Core\SystemListingInfo $system_list_info
-   *   (optional) The system listing info.
+   * @param \Drupal\Core\SystemListing $system_list
+   *   (optional) A system listing instance.
    */
-  public function __construct(ConfigFactory $config_factory, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, InfoParserInterface $info_parser, ConfigInstallerInterface $config_installer = NULL, RouteBuilder $route_builder = NULL, SystemListingInfo $system_list_info = NULL) {
+  public function __construct(ConfigFactory $config_factory, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, InfoParserInterface $info_parser, ConfigInstallerInterface $config_installer = NULL, RouteBuilder $route_builder = NULL, SystemListing $system_list = NULL) {
     $this->configFactory = $config_factory;
     $this->moduleHandler = $module_handler;
     $this->cacheBackend = $cache_backend;
     $this->infoParser = $info_parser;
     $this->configInstaller = $config_installer;
     $this->routeBuilder = $route_builder;
-    $this->systemListingInfo = $system_list_info;
+    $this->systemListing = $system_list;
   }
 
   /**
@@ -244,8 +244,8 @@ public function reset() {
    */
   public function rebuildThemeData() {
     // Find themes.
-    $listing = $this->getSystemListingInfo();
-    $themes = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes', 'name', 1);
+    $listing = $this->getSystemListing();
+    $themes = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info\.yml$/', 'themes');
     // Allow modules to add further themes.
     if ($module_themes = $this->moduleHandler->invokeAll('system_theme_info')) {
       foreach ($module_themes as $name => $uri) {
@@ -259,8 +259,7 @@ public function rebuildThemeData() {
     }
 
     // Find theme engines.
-    $listing = $this->getSystemListingInfo();
-    $engines = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.engine$/', 'themes/engines', 'name', 1);
+    $engines = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.engine$/', 'themes/engines');
 
     // Set defaults for theme info.
     $defaults = array(
@@ -443,16 +442,16 @@ protected function doGetBaseThemes(array $themes, $theme, $used_themes = array()
   }
 
   /**
-   * Returns a system listing info object.
+   * Returns a system listing object.
    *
-   * @return \Drupal\Core\SystemListingInfo
+   * @return \Drupal\Core\SystemListing
    *   The system listing object.
    */
-  protected function getSystemListingInfo() {
-    if (!isset($this->systemListingInfo)) {
-      $this->systemListingInfo = new SystemListingInfo();
+  protected function getSystemListing() {
+    if (!isset($this->systemListing)) {
+      $this->systemListing = new SystemListing();
     }
-    return $this->systemListingInfo;
+    return $this->systemListing;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/SystemListing.php b/core/lib/Drupal/Core/SystemListing.php
index 20d95cb..a1ea06c 100644
--- a/core/lib/Drupal/Core/SystemListing.php
+++ b/core/lib/Drupal/Core/SystemListing.php
@@ -2,30 +2,41 @@
 
 /**
  * @file
- * Definition of Drupal\Core\SystemListing.
+ * Contains \Drupal\Core\SystemListing.
  */
 
 namespace Drupal\Core;
 
+use Drupal\Core\Extension\InfoParser;
+
 /**
- * Returns information about system object files (modules, themes, etc.).
- *
- * This class requires the list of profiles to be scanned (see
- * \Drupal\Core\SystemListing::scan) to be passed into the constructor. Also,
- * info files are not parsed.
+ * Discovers available extensions in the filesystem.
  */
 class SystemListing {
 
   /**
-   * Construct this listing object.
+   * InfoParser instance for parsing .info.yml files.
    *
-   * @param array $profiles
-   *   A list of profiles to search their directories for in addition to the
-   *   default directories.
+   * @var \Drupal\Core\Extension\InfoParser
    */
-  function __construct($profiles = array()) {
-    $this->profiles = $profiles;
-  }
+  protected $infoParser;
+
+  /**
+   * Previously discovered files keyed by dynamic directory hash and extension type.
+   *
+   * Dynamic directories are the site directory and the installation profile
+   * directories. The hash is a simple concatention of the directory paths.
+   *
+   * @var array
+   */
+  protected static $files = array();
+
+  /**
+   * List of installation profile directories to additionally scan.
+   *
+   * @var array
+   */
+  protected $profileDirectories;
 
   /**
    * Returns information about system object files (modules, themes, etc.).
@@ -81,16 +92,40 @@ function __construct($profiles = array()) {
    *   - 'filename': File name.
    *   - 'name': Name of file without the extension.
    */
-  function scan($mask, $directory, $key = 'name') {
+  public function scan($mask, $directory, $key = 'name') {
     if (!in_array($key, array('uri', 'filename', 'name'))) {
       $key = 'uri';
     }
-    $config = conf_path();
+    $site_directory = conf_path();
+
+    // Installation profile directories can only be scanned for extensions when
+    // not scanning for installation profiles themselves.
+    $profileDirectories = array();
+    if ($directory != 'profiles') {
+      // Determine the installation profile directories to scan for extensions,
+      // unless explicit profile directories have been preset.
+      if (!isset($this->profileDirectories)) {
+        $profileDirectories = $this->getProfileDirectories();
+      }
+      else {
+        $profileDirectories = $this->profileDirectories;
+      }
+      if (isset($profileDirectories[0]) && $profileDirectories[0] === '') {
+        throw new \RuntimeException('drupal_get_profile() can return an empty string. Please adjust your code.');
+      }
+    }
+
+    // Check static cache.
+    // The static cache is valid unless the dynamic directories have changed.
+    $dynamic_dirs_hash = implode('|', array($site_directory => $site_directory) + $profileDirectories);
+    if (isset(static::$files[$dynamic_dirs_hash][$directory])) {
+      return static::$files[$dynamic_dirs_hash][$directory];
+    }
 
     // Search for the directory in core.
     $searchdir = array('core/' . $directory);
-    foreach ($this->profiles($directory) as $profile) {
-      $searchdir[] = $profile;
+    foreach ($profileDirectories as $profile) {
+      $searchdir[] = $profile . '/' . $directory;
     }
 
     // Always search for contributed and custom extensions in top-level
@@ -100,8 +135,8 @@ function scan($mask, $directory, $key = 'name') {
     $searchdir[] = $directory;
     $searchdir[] = 'sites/all/' . $directory;
 
-    if (file_exists("$config/$directory")) {
-      $searchdir[] = "$config/$directory";
+    if (file_exists("$site_directory/$directory")) {
+      $searchdir[] = "$site_directory/$directory";
     }
     // @todo Find a way to skip ./config directories (but not modules/config).
     $nomask = '/^(CVS|lib|templates|css|js)$/';
@@ -110,22 +145,47 @@ function scan($mask, $directory, $key = 'name') {
     foreach ($searchdir as $dir) {
       $files = array_merge($files, $this->process($files, $this->scanDirectory($dir, $key, $mask, $nomask)));
     }
+    static::$files[$dynamic_dirs_hash][$directory] = $files;
     return $files;
   }
 
   /**
-   * List the profiles for this directory.
+   * Returns additional installation profile directories to be scanned.
    *
-   * This version only returns those passed to the constructor.
+   * @return array
+   *   A list of installation profile directory paths relative to system root.
+   */
+  protected function getProfileDirectories() {
+    $searchdir = array();
+    $profile = drupal_get_profile();
+    // For SimpleTest to be able to test modules packaged together with a
+    // distribution we need to include the profile of the parent site (in
+    // which test runs are triggered).
+    if (drupal_valid_test_ua() && !drupal_installation_attempted()) {
+      $testing_profile = \Drupal::config('simpletest.settings')->get('parent_profile');
+      if ($testing_profile && $testing_profile != $profile) {
+        $searchdir[] = drupal_get_path('profile', $testing_profile);
+      }
+    }
+    // In case both profile directories contain the same extension, the actual
+    // profile always has precedence.
+    if ($profile) {
+      $searchdir[] = drupal_get_path('profile', $profile);
+    }
+    return $searchdir;
+  }
+
+  /**
+   * Sets explicit profile directories to scan.
    *
-   * @param string $directory
-   *   The current search directory like 'modules' or 'themes'.
+   * @param array $profileDirectories
+   *   A list of installation profile directories to search for extensions.
    *
-   * @return array
-   *   A list of profiles.
+   * @return $this
    */
-  protected function profiles($directory) {
-    return $this->profiles;
+  public function setProfileDirectories(array $paths) {
+    $this->profileDirectories = $paths;
+    return $this;
   }
 
   /**
@@ -137,10 +197,33 @@ protected function profiles($directory) {
    *   The files found in a single directory.
    *
    * @return array
-   *   The processed list of file objects. For example, the SystemListingInfo
-   *   class removes files not compatible with the current core version.
+   *   The processed list of file objects. Extensions discovered in later search
+   *   paths and which are not not compatible with the current core version are
+   *   skipped.
    */
   protected function process(array $files, array $files_to_add) {
+    // Duplicate files found in later search directories take precedence over
+    // earlier ones, so we want them to overwrite keys in our resulting
+    // $files array.
+    // The exception to this is if the later file is from a module or theme not
+    // compatible with Drupal core. This may occur during upgrades of Drupal
+    // core when new modules exist in core while older contrib modules with the
+    // same name exist in a directory such as /modules.
+    foreach (array_intersect_key($files_to_add, $files) as $file_key => $file) {
+      // If it has no info file, then we just behave liberally and accept the
+      // new resource on the list for merging.
+      if (file_exists($info_file = dirname($file->uri) . '/' . $file->name . '.info.yml')) {
+        // Get the .info.yml file for the module or theme this file belongs to.
+        $info = $this->getInfoParser()->parse($info_file);
+
+        // If the module or theme is incompatible with Drupal core, remove it
+        // from the array for the current search directory, so it is not
+        // overwritten when merged with the $files array.
+        if (isset($info['core']) && $info['core'] != \Drupal::CORE_COMPATIBILITY) {
+          unset($files_to_add[$file_key]);
+        }
+      }
+    }
     return $files_to_add;
   }
 
@@ -203,6 +286,20 @@ protected function scanDirectory($dir, $key, $mask, $nomask) {
    *   A file object.
    */
   protected function processFile($file) {
+    $file->name = basename($file->name, '.info');
+  }
+
+  /**
+   * Returns a parser for parsing .info.yml files.
+   *
+   * @return \Drupal\Core\Extension\InfoParser
+   *   The InfoParser instance.
+   */
+  protected function getInfoParser() {
+    if (!isset($this->infoParser)) {
+      $this->infoParser = new InfoParser();
+    }
+    return $this->infoParser;
   }
 
 }
diff --git a/core/lib/Drupal/Core/SystemListingInfo.php b/core/lib/Drupal/Core/SystemListingInfo.php
deleted file mode 100644
index d302699..0000000
--- a/core/lib/Drupal/Core/SystemListingInfo.php
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\Core\SystemListingInfo.
- */
-
-namespace Drupal\Core;
-
-/**
- * Returns information about system object files (modules, themes, etc.).
- *
- * This class finds the profile directories itself and also parses info files.
- */
-class SystemListingInfo extends SystemListing {
-
-  /**
-   * Overrides Drupal\Core\SystemListing::profiles().
-   */
-  protected function profiles($directory) {
-    $searchdir = array();
-    // The 'core/profiles' directory contains pristine collections of modules
-    // and themes as provided by a distribution. It is pristine in the same
-    // way that the 'core/modules' directory is pristine for core; users
-    // should avoid any modification by using the top-level or sites/<domain>
-    // directories.
-    $profile = drupal_get_profile();
-    // For SimpleTest to be able to test modules packaged together with a
-    // distribution we need to include the profile of the parent site (in
-    // which test runs are triggered).
-    if (drupal_valid_test_ua() && !drupal_installation_attempted()) {
-      $testing_profile = \Drupal::config('simpletest.settings')->get('parent_profile');
-      if ($testing_profile && $testing_profile != $profile) {
-        $searchdir[] = drupal_get_path('profile', $testing_profile) . '/' . $directory;
-      }
-    }
-    // In case both profile directories contain the same extension, the actual
-    // profile always has precedence.
-    $searchdir[] = drupal_get_path('profile', $profile) . '/' . $directory;
-    return $searchdir;
-  }
-
-  /**
-   * Overrides Drupal\Core\SystemListing::process().
-   */
-  protected function process(array $files, array $files_to_add) {
-    // Duplicate files found in later search directories take precedence over
-    // earlier ones, so we want them to overwrite keys in our resulting
-    // $files array.
-    // The exception to this is if the later file is from a module or theme not
-    // compatible with Drupal core. This may occur during upgrades of Drupal
-    // core when new modules exist in core while older contrib modules with the
-    // same name exist in a directory such as /modules.
-    foreach (array_intersect_key($files_to_add, $files) as $file_key => $file) {
-      // If it has no info file, then we just behave liberally and accept the
-      // new resource on the list for merging.
-      if (file_exists($info_file = dirname($file->uri) . '/' . $file->name . '.info.yml')) {
-        // Get the .info.yml file for the module or theme this file belongs to.
-        $info = \Drupal::service('info_parser')->parse($info_file);
-
-        // If the module or theme is incompatible with Drupal core, remove it
-        // from the array for the current search directory, so it is not
-        // overwritten when merged with the $files array.
-        if (isset($info['core']) && $info['core'] != \Drupal::CORE_COMPATIBILITY) {
-          unset($files_to_add[$file_key]);
-        }
-      }
-    }
-    return $files_to_add;
-  }
-
-  /**
-   * Overrides Drupal\Core\SystemListing::processFile().
-   */
-  protected function processFile($file) {
-    $file->name = basename($file->name, '.info');
-  }
-
-}
diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/TestInstallStorage.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestInstallStorage.php
index c940422..a4dcda9 100644
--- a/core/modules/config/tests/config_test/lib/Drupal/config_test/TestInstallStorage.php
+++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestInstallStorage.php
@@ -8,6 +8,7 @@
 namespace Drupal\config_test;
 
 use Drupal\Core\Config\InstallStorage;
+use Drupal\Core\SystemListing;
 
 /**
  * Tests configuration of profiles, modules and themes.
@@ -22,9 +23,10 @@ class TestInstallStorage extends InstallStorage {
    */
   protected function getAllFolders() {
     if (!isset($this->folders)) {
-      $this->folders = $this->getComponentNames('profile', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles')));
-      $this->folders += $this->getComponentNames('module', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0)));
-      $this->folders += $this->getComponentNames('theme', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes')));
+      $listing = new SystemListing();
+      $this->folders = $this->getComponentNames('profile', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles')));
+      $this->folders += $this->getComponentNames('module', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules')));
+      $this->folders += $this->getComponentNames('theme', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info\.yml$/', 'themes')));
     }
     return $this->folders;
   }
diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php
index 8d240b0..8c86525 100644
--- a/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php
+++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php
@@ -8,6 +8,7 @@
 namespace Drupal\config_test;
 
 use Drupal\Core\Config\Schema\SchemaStorage;
+use Drupal\Core\SystemListing;
 
 /**
  * Tests configuration schemas of profiles, modules and themes.
@@ -29,10 +30,11 @@ public function __construct() {
    */
   protected function getAllFolders() {
     if (!isset($this->folders)) {
+      $listing = new SystemListing();
       $this->folders = $this->getBaseDataTypeSchema();
-      $this->folders += $this->getComponentNames('profile', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles')));
-      $this->folders += $this->getComponentNames('module', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0)));
-      $this->folders += $this->getComponentNames('theme', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes')));
+      $this->folders += $this->getComponentNames('profile', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles')));
+      $this->folders += $this->getComponentNames('module', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules')));
+      $this->folders += $this->getComponentNames('theme', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info\.yml$/', 'themes')));
     }
     return $this->folders;
   }
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index b284941..1ab4418 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -777,7 +777,7 @@ protected function setUp() {
 
     // Set 'parent_profile' of simpletest to add the parent profile's
     // search path to the child site's search paths.
-    // @see drupal_system_listing()
+    // @see \Drupal\Core\SystemListing::getProfileDirectories()
     \Drupal::config('simpletest.settings')->set('parent_profile', $this->originalProfile)->save();
 
     // Collect modules to install.
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index c6b53bf..2e1eaf1 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -2,6 +2,7 @@
 
 use Drupal\Core\Database\Database;
 use Drupal\Core\Page\HtmlPage;
+use Drupal\Core\SystemListing;
 use Drupal\simpletest\TestBase;
 use Symfony\Component\Process\PhpExecutableFinder;
 
@@ -464,7 +465,8 @@ function simpletest_test_get_all($module = NULL) {
       $classes = array();
       $module_data = system_rebuild_module_data();
       $all_data = $module_data + system_rebuild_theme_data();
-      $all_data += drupal_system_listing('/\.profile$/', 'profiles', 'name');
+      $listing = new SystemListing();
+      $all_data += $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles');
       // If module is set then we keep only that one module.
       if (isset($module)) {
         $all_data = array(
@@ -550,14 +552,15 @@ function simpletest_classloader_register() {
   $types = array(
     'theme_engine' => array('dir' => 'themes/engines', 'extension' => 'engine'),
     'module' => array('dir' => 'modules', 'extension' => 'module'),
-    'theme' => array('dir' => 'themes', 'extension' => 'info'),
+    'theme' => array('dir' => 'themes', 'extension' => 'info\.yml'),
     'profile' => array('dir' => 'profiles', 'extension' => 'profile'),
   );
 
   $classloader = drupal_classloader();
+  $listing = new SystemListing();
 
   foreach ($types as $type => $info) {
-    $matches = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.' . $info['extension'] . '$/', $info['dir']);
+    $matches = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.' . $info['extension'] . '$/', $info['dir']);
     foreach ($matches as $name => $file) {
       drupal_classloader_register($name, dirname($file->uri));
       $classloader->add('Drupal\\' . $name . '\\Tests', DRUPAL_ROOT . '/' . dirname($file->uri) . '/tests');
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php
index ee1f12b..ae9af4e 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Common;
 
+use Drupal\Core\SystemListing;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -55,7 +56,8 @@ function testDirectoryPrecedence() {
 
     // Now scan the directories and check that the files take precedence as
     // expected.
-    $files = drupal_system_listing('/\.module$/', 'modules');
+    $listing = new SystemListing();
+    $files = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules');
     foreach ($expected_directories as $module => $directories) {
       $expected_directory = array_shift($directories);
       $expected_filename = "$expected_directory/$module/$module.module";
diff --git a/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php
index e9cf688..8be4af9 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php
@@ -72,7 +72,7 @@ public function testInfoParser() {
       $this->fail('Expected InfoParserException not thrown when reading missing_keys.info.txt');
     }
     catch (InfoParserException $e) {
-      $expected_message = 'Missing required keys (name, type) in core/modules/system/tests/fixtures/missing_keys.info.txt.';
+      $expected_message = "Missing required keys (type, core, name) in $filename.";
       $this->assertEqual($e->getMessage(), $expected_message);
     }
 
@@ -83,7 +83,7 @@ public function testInfoParser() {
       $this->fail('Expected InfoParserException not thrown when reading missing_key.info.txt');
     }
     catch (InfoParserException $e) {
-      $expected_message = 'Missing required key (type) in core/modules/system/tests/fixtures/missing_key.info.txt.';
+      $expected_message = "Missing required key (type) in $filename.";
       $this->assertEqual($e->getMessage(), $expected_message);
     }
 
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index cc647a2..d5f7453 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -8,6 +8,7 @@
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Language\Language;
+use Drupal\Core\SystemListing;
 use Drupal\Core\Utility\ModuleInfo;
 use Drupal\menu_link\MenuLinkInterface;
 use Drupal\user\UserInterface;
@@ -2579,18 +2580,19 @@ function system_get_module_info($property) {
  *   An associative array of module information.
  */
 function _system_rebuild_module_data() {
+  $listing = new SystemListing();
   // Find modules
-  $modules = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0);
+  $modules = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules');
 
   // Find installation profiles.
-  $profiles = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles', 'name', 0);
+  $profiles = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles');
 
   // Include the installation profile in modules that are loaded.
-  $profile = drupal_get_profile();
-  $modules[$profile] = $profiles[$profile];
-
-  // Installation profile hooks are always executed last.
-  $modules[$profile]->weight = 1000;
+  if ($profile = drupal_get_profile()) {
+    $modules[$profile] = $profiles[$profile];
+    // Installation profile hooks are always executed last.
+    $modules[$profile]->weight = 1000;
+  }
 
   // Set defaults for module info.
   $defaults = array(
@@ -2643,7 +2645,7 @@ function _system_rebuild_module_data() {
   }
 
 
-  if (isset($modules[$profile])) {
+  if ($profile && isset($modules[$profile])) {
     // The installation profile is required, if it's a valid module.
     $modules[$profile]->info['required'] = TRUE;
     // Add a default distribution name if the profile did not provide one. This
diff --git a/core/modules/system/tests/fixtures/common_test.info.txt b/core/modules/system/tests/fixtures/common_test.info.txt
index ae97b16..7e57dfe 100644
--- a/core/modules/system/tests/fixtures/common_test.info.txt
+++ b/core/modules/system/tests/fixtures/common_test.info.txt
@@ -1,3 +1,4 @@
+core: 8.x
 name: common_test
 type: module
 description: 'testing info file parsing'
diff --git a/core/modules/system/tests/fixtures/missing_keys.info.txt b/core/modules/system/tests/fixtures/missing_keys.info.txt
index 56c6411..c04f34d 100644
--- a/core/modules/system/tests/fixtures/missing_keys.info.txt
+++ b/core/modules/system/tests/fixtures/missing_keys.info.txt
@@ -1,6 +1,5 @@
 # info.yml for testing missing name, description, and type keys.
 package: Core
 version: VERSION
-core: 8.x
 dependencies:
   - field
diff --git a/core/modules/update/update.module b/core/modules/update/update.module
index adf6ba3..e9ab3aa 100644
--- a/core/modules/update/update.module
+++ b/core/modules/update/update.module
@@ -629,7 +629,7 @@ function theme_update_last_check($variables) {
  * an .info.yml file which claims that the code is compatible with the current
  * version of Drupal core.
  *
- * @see drupal_system_listing()
+ * @see \Drupal\Core\SystemListing::process()
  * @see _system_rebuild_module_data()
  */
 function update_verify_update_archive($project, $archive_file, $directory) {
diff --git a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php
index 25901c1..4aeed08 100644
--- a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php
+++ b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php
@@ -67,9 +67,9 @@ class ThemeHandlerTest extends UnitTestCase {
   /**
    * The system listing info.
    *
-   * @var \Drupal\Core\SystemListingInfo|\PHPUnit_Framework_MockObject_MockObject
+   * @var \Drupal\Core\SystemListing|\PHPUnit_Framework_MockObject_MockObject
    */
-  protected $systemListingInfo;
+  protected $systemListing;
 
   /**
    * The tested theme handler.
@@ -101,10 +101,10 @@ protected function setUp() {
     $this->routeBuilder = $this->getMockBuilder('Drupal\Core\Routing\RouteBuilder')
       ->disableOriginalConstructor()
       ->getMock();
-    $this->systemListingInfo = $this->getMockBuilder('Drupal\Core\SystemListingInfo')
+    $this->systemListing = $this->getMockBuilder('Drupal\Core\SystemListing')
       ->disableOriginalConstructor()
       ->getMock();
-    $this->themeHandler = new TestThemeHandler($this->configFactory, $this->moduleHandler, $this->cacheBackend, $this->infoParser, $this->configInstaller, $this->routeBuilder, $this->systemListingInfo);
+    $this->themeHandler = new TestThemeHandler($this->configFactory, $this->moduleHandler, $this->cacheBackend, $this->infoParser, $this->configInstaller, $this->routeBuilder, $this->systemListing);
 
     $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
     $container->expects($this->any())
@@ -154,7 +154,7 @@ public function testEnableSingleTheme() {
       ->expects($this->once())
       ->method('save');
 
-    $this->systemListingInfo->expects($this->any())
+    $this->systemListing->expects($this->any())
       ->method('scan')
       ->will($this->returnValue(array()));
 
@@ -195,7 +195,7 @@ public function testEnableAndListInfo() {
       ->method('clear')
       ->will($this->returnSelf());
 
-    $this->systemListingInfo->expects($this->any())
+    $this->systemListing->expects($this->any())
       ->method('scan')
       ->will($this->returnValue(array()));
 
@@ -255,9 +255,9 @@ public function testEnableAndListInfo() {
    * @see \Drupal\Core\Extension\ThemeHandler::rebuildThemeData()
    */
   public function testRebuildThemeData() {
-    $this->systemListingInfo->expects($this->at(0))
+    $this->systemListing->expects($this->at(0))
       ->method('scan')
-      ->with($this->anything(), 'themes', 'name', 1)
+      ->with($this->anything(), 'themes', 'name')
       ->will($this->returnValue(array(
         'seven' => (object) array(
           'name' => 'seven',
