Skip to content

Commit

Permalink
Add a per-importer cache for loads that aren't cacheable en masse (#2219
Browse files Browse the repository at this point in the history
)

Inspired by comments on #2215
  • Loading branch information
nex3 committed Apr 18, 2024
1 parent 2a9eaad commit b97f26f
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 71 deletions.
93 changes: 58 additions & 35 deletions lib/src/async_import_cache.dart
Expand Up @@ -16,6 +16,7 @@ import 'importer/no_op.dart';
import 'importer/utils.dart';
import 'io.dart';
import 'logger.dart';
import 'util/map.dart';
import 'util/nullable.dart';
import 'utils.dart';

Expand Down Expand Up @@ -44,30 +45,28 @@ final class AsyncImportCache {
/// The `forImport` in each key is true when this canonicalization is for an
/// `@import` rule. Otherwise, it's for a `@use` or `@forward` rule.
///
/// This cache isn't used for relative imports, because they depend on the
/// specific base importer. That's stored separately in
/// [_relativeCanonicalizeCache].
/// This cache covers loads that go through the entire chain of [_importers],
/// but it doesn't cover individual loads or loads in which any importer
/// accesses `containingUrl`. See also [_perImporterCanonicalizeCache].
final _canonicalizeCache =
<(Uri, {bool forImport}), AsyncCanonicalizeResult?>{};

/// The canonicalized URLs for each non-canonical URL that's resolved using a
/// relative importer.
/// Like [_canonicalizeCache] but also includes the specific importer in the
/// key.
///
/// The map's keys have four parts:
/// This is used to cache both relative imports from the base importer and
/// individual importer results in the case where some other component of the
/// importer chain isn't cacheable.
final _perImporterCanonicalizeCache =
<(AsyncImporter, Uri, {bool forImport}), AsyncCanonicalizeResult?>{};

/// A map from the keys in [_perImporterCanonicalizeCache] that are generated
/// for relative URL loads agains the base importer to the original relative
/// URLs what were loaded.
///
/// 1. The URL passed to [canonicalize] (the same as in [_canonicalizeCache]).
/// 2. Whether the canonicalization is for an `@import` rule.
/// 3. The `baseImporter` passed to [canonicalize].
/// 4. The `baseUrl` passed to [canonicalize].
///
/// The map's values are the same as the return value of [canonicalize].
final _relativeCanonicalizeCache = <(
Uri, {
bool forImport,
AsyncImporter baseImporter,
Uri? baseUrl
}),
AsyncCanonicalizeResult?>{};
/// This is used to invalidate the cache when files are changed.
final _nonCanonicalRelativeUrls =
<(AsyncImporter, Uri, {bool forImport}), Uri>{};

/// The parsed stylesheets for each canonicalized import URL.
final _importCache = <Uri, Stylesheet?>{};
Expand Down Expand Up @@ -155,18 +154,17 @@ final class AsyncImportCache {
}

if (baseImporter != null && url.scheme == '') {
var relativeResult = await putIfAbsentAsync(_relativeCanonicalizeCache, (
url,
forImport: forImport,
baseImporter: baseImporter,
baseUrl: baseUrl
), () async {
var (result, cacheable) = await _canonicalize(
baseImporter, baseUrl?.resolveUri(url) ?? url, baseUrl, forImport);
var resolvedUrl = baseUrl?.resolveUri(url) ?? url;
var key = (baseImporter, resolvedUrl, forImport: forImport);
var relativeResult =
await putIfAbsentAsync(_perImporterCanonicalizeCache, key, () async {
var (result, cacheable) =
await _canonicalize(baseImporter, resolvedUrl, baseUrl, forImport);
assert(
cacheable,
"Relative loads should always be cacheable because they never "
"provide access to the containing URL.");
if (baseUrl != null) _nonCanonicalRelativeUrls[key] = url;
return result;
});
if (relativeResult != null) return relativeResult;
Expand All @@ -182,17 +180,41 @@ final class AsyncImportCache {
// `canonicalize()` calls we've attempted are cacheable. Only if they are do
// we store the result in the cache.
var cacheable = true;
for (var importer in _importers) {
for (var i = 0; i < _importers.length; i++) {
var importer = _importers[i];
var perImporterKey = (importer, url, forImport: forImport);
switch (_perImporterCanonicalizeCache.getOption(perImporterKey)) {
case (var result?,):
return result;
case (null,):
continue;
}

switch (await _canonicalize(importer, url, baseUrl, forImport)) {
case (var result?, true) when cacheable:
_canonicalizeCache[key] = result;
return result;

case (var result?, _):
return result;

case (_, false):
cacheable = false;
case (var result, true) when !cacheable:
_perImporterCanonicalizeCache[perImporterKey] = result;
if (result != null) return result;

case (var result, false):
if (cacheable) {
// If this is the first uncacheable result, add all previous results
// to the per-importer cache so we don't have to re-run them for
// future uses of this importer.
for (var j = 0; j < i; j++) {
_perImporterCanonicalizeCache[(
_importers[j],
url,
forImport: forImport
)] = null;
}
cacheable = false;
}

if (result != null) return result;
}
}

Expand Down Expand Up @@ -315,7 +337,7 @@ final class AsyncImportCache {
Uri sourceMapUrl(Uri canonicalUrl) =>
_resultsCache[canonicalUrl]?.sourceMapUrl ?? canonicalUrl;

/// Clears the cached canonical version of the given [url].
/// Clears the cached canonical version of the given non-canonical [url].
///
/// Has no effect if the canonical version of [url] has not been cached.
///
Expand All @@ -324,7 +346,8 @@ final class AsyncImportCache {
void clearCanonicalize(Uri url) {
_canonicalizeCache.remove((url, forImport: false));
_canonicalizeCache.remove((url, forImport: true));
_relativeCanonicalizeCache.removeWhere((key, _) => key.$1 == url);
_perImporterCanonicalizeCache.removeWhere(
(key, _) => key.$2 == url || _nonCanonicalRelativeUrls[key] == url);
}

/// Clears the cached parse tree for the stylesheet with the given
Expand Down
93 changes: 57 additions & 36 deletions lib/src/import_cache.dart
Expand Up @@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_import_cache.dart.
// See tool/grind/synchronize.dart for details.
//
// Checksum: 36bc42050cf2eb3a43f36376c4f06c1708eee777
// Checksum: 4362e28e5cd425786c235d2a6a2bb60539403799
//
// ignore_for_file: unused_import

Expand All @@ -23,6 +23,7 @@ import 'importer/no_op.dart';
import 'importer/utils.dart';
import 'io.dart';
import 'logger.dart';
import 'util/map.dart';
import 'util/nullable.dart';
import 'utils.dart';

Expand All @@ -47,29 +48,26 @@ final class ImportCache {
/// The `forImport` in each key is true when this canonicalization is for an
/// `@import` rule. Otherwise, it's for a `@use` or `@forward` rule.
///
/// This cache isn't used for relative imports, because they depend on the
/// specific base importer. That's stored separately in
/// [_relativeCanonicalizeCache].
/// This cache covers loads that go through the entire chain of [_importers],
/// but it doesn't cover individual loads or loads in which any importer
/// accesses `containingUrl`. See also [_perImporterCanonicalizeCache].
final _canonicalizeCache = <(Uri, {bool forImport}), CanonicalizeResult?>{};

/// The canonicalized URLs for each non-canonical URL that's resolved using a
/// relative importer.
/// Like [_canonicalizeCache] but also includes the specific importer in the
/// key.
///
/// The map's keys have four parts:
/// This is used to cache both relative imports from the base importer and
/// individual importer results in the case where some other component of the
/// importer chain isn't cacheable.
final _perImporterCanonicalizeCache =
<(Importer, Uri, {bool forImport}), CanonicalizeResult?>{};

/// A map from the keys in [_perImporterCanonicalizeCache] that are generated
/// for relative URL loads agains the base importer to the original relative
/// URLs what were loaded.
///
/// 1. The URL passed to [canonicalize] (the same as in [_canonicalizeCache]).
/// 2. Whether the canonicalization is for an `@import` rule.
/// 3. The `baseImporter` passed to [canonicalize].
/// 4. The `baseUrl` passed to [canonicalize].
///
/// The map's values are the same as the return value of [canonicalize].
final _relativeCanonicalizeCache = <(
Uri, {
bool forImport,
Importer baseImporter,
Uri? baseUrl
}),
CanonicalizeResult?>{};
/// This is used to invalidate the cache when files are changed.
final _nonCanonicalRelativeUrls = <(Importer, Uri, {bool forImport}), Uri>{};

/// The parsed stylesheets for each canonicalized import URL.
final _importCache = <Uri, Stylesheet?>{};
Expand Down Expand Up @@ -155,18 +153,16 @@ final class ImportCache {
}

if (baseImporter != null && url.scheme == '') {
var relativeResult = _relativeCanonicalizeCache.putIfAbsent((
url,
forImport: forImport,
baseImporter: baseImporter,
baseUrl: baseUrl
), () {
var (result, cacheable) = _canonicalize(
baseImporter, baseUrl?.resolveUri(url) ?? url, baseUrl, forImport);
var resolvedUrl = baseUrl?.resolveUri(url) ?? url;
var key = (baseImporter, resolvedUrl, forImport: forImport);
var relativeResult = _perImporterCanonicalizeCache.putIfAbsent(key, () {
var (result, cacheable) =
_canonicalize(baseImporter, resolvedUrl, baseUrl, forImport);
assert(
cacheable,
"Relative loads should always be cacheable because they never "
"provide access to the containing URL.");
if (baseUrl != null) _nonCanonicalRelativeUrls[key] = url;
return result;
});
if (relativeResult != null) return relativeResult;
Expand All @@ -182,17 +178,41 @@ final class ImportCache {
// `canonicalize()` calls we've attempted are cacheable. Only if they are do
// we store the result in the cache.
var cacheable = true;
for (var importer in _importers) {
for (var i = 0; i < _importers.length; i++) {
var importer = _importers[i];
var perImporterKey = (importer, url, forImport: forImport);
switch (_perImporterCanonicalizeCache.getOption(perImporterKey)) {
case (var result?,):
return result;
case (null,):
continue;
}

switch (_canonicalize(importer, url, baseUrl, forImport)) {
case (var result?, true) when cacheable:
_canonicalizeCache[key] = result;
return result;

case (var result?, _):
return result;

case (_, false):
cacheable = false;
case (var result, true) when !cacheable:
_perImporterCanonicalizeCache[perImporterKey] = result;
if (result != null) return result;

case (var result, false):
if (cacheable) {
// If this is the first uncacheable result, add all previous results
// to the per-importer cache so we don't have to re-run them for
// future uses of this importer.
for (var j = 0; j < i; j++) {
_perImporterCanonicalizeCache[(
_importers[j],
url,
forImport: forImport
)] = null;
}
cacheable = false;
}

if (result != null) return result;
}
}

Expand Down Expand Up @@ -312,7 +332,7 @@ final class ImportCache {
Uri sourceMapUrl(Uri canonicalUrl) =>
_resultsCache[canonicalUrl]?.sourceMapUrl ?? canonicalUrl;

/// Clears the cached canonical version of the given [url].
/// Clears the cached canonical version of the given non-canonical [url].
///
/// Has no effect if the canonical version of [url] has not been cached.
///
Expand All @@ -321,7 +341,8 @@ final class ImportCache {
void clearCanonicalize(Uri url) {
_canonicalizeCache.remove((url, forImport: false));
_canonicalizeCache.remove((url, forImport: true));
_relativeCanonicalizeCache.removeWhere((key, _) => key.$1 == url);
_perImporterCanonicalizeCache.removeWhere(
(key, _) => key.$2 == url || _nonCanonicalRelativeUrls[key] == url);
}

/// Clears the cached parse tree for the stylesheet with the given
Expand Down
6 changes: 6 additions & 0 deletions lib/src/util/map.dart
Expand Up @@ -2,6 +2,8 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'option.dart';

extension MapExtensions<K, V> on Map<K, V> {
/// If [this] doesn't contain the given [key], sets that key to [value] and
/// returns it.
Expand All @@ -16,4 +18,8 @@ extension MapExtensions<K, V> on Map<K, V> {
// TODO(nweiz): Remove this once dart-lang/collection#289 is released.
/// Like [Map.entries], but returns each entry as a record.
Iterable<(K, V)> get pairs => entries.map((e) => (e.key, e.value));

/// Returns an option that contains the value at [key] if one exists and null
/// otherwise.
Option<V> getOption(K key) => containsKey(key) ? (this[key]!,) : null;
}
12 changes: 12 additions & 0 deletions lib/src/util/option.dart
@@ -0,0 +1,12 @@
// Copyright 2024 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

/// A type that represents either the presence of a value of type `T` or its
/// absence.
///
/// When the option is present, this will be a single-element tuple that
/// contains the value. If it's absent, it will be null. This allows callers to
/// distinguish between a present null value and a value that's absent
/// altogether.
typedef Option<T> = (T,)?;

0 comments on commit b97f26f

Please sign in to comment.