Skip to content

Commit

Permalink
Merge pull request #1535 from sass/perf
Browse files Browse the repository at this point in the history
Improve performance
  • Loading branch information
nex3 authored Oct 21, 2021
2 parents 6253da7 + 86b16f5 commit 435e1b2
Show file tree
Hide file tree
Showing 23 changed files with 339 additions and 235 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.43.3

* Improve performance.

## 1.43.2

* Improve the error message when the default namespace of a `@use` rule is not
Expand Down
2 changes: 1 addition & 1 deletion lib/sass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export 'src/logger.dart';
export 'src/syntax.dart';
export 'src/value.dart' hide SassApiColor;
export 'src/visitor/serialize.dart' show OutputStyle;
export 'src/warn.dart' show warn;
export 'src/evaluation_context.dart' show warn;

/// Loads the Sass file at [path], compiles it to CSS, and returns a
/// [CompileResult] containing the CSS and additional metadata about the
Expand Down
61 changes: 41 additions & 20 deletions lib/src/async_import_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,31 @@ class AsyncImportCache {
///
/// This map's values are the same as the return value of [canonicalize].
///
/// This cache isn't used for relative imports, because they're
/// context-dependent.
final Map<Tuple2<Uri, bool>, Tuple3<AsyncImporter, Uri, Uri>?>
_canonicalizeCache;
/// This cache isn't used for relative imports, because they depend on the
/// specific base importer. That's stored separately in
/// [_relativeCanonicalizeCache].
final _canonicalizeCache =
<Tuple2<Uri, bool>, Tuple3<AsyncImporter, Uri, Uri>?>{};

/// The canonicalized URLs for each non-canonical URL that's resolved using a
/// relative importer.
///
/// The map's keys have four parts:
///
/// 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 = <Tuple4<Uri, bool, AsyncImporter, Uri?>,
Tuple3<AsyncImporter, Uri, Uri>?>{};

/// The parsed stylesheets for each canonicalized import URL.
final Map<Uri, Stylesheet?> _importCache;
final _importCache = <Uri, Stylesheet?>{};

/// The import results for each canonicalized import URL.
final Map<Uri, ImporterResult> _resultsCache;
final _resultsCache = <Uri, ImporterResult>{};

/// Creates an import cache that resolves imports using [importers].
///
Expand All @@ -67,18 +82,12 @@ class AsyncImportCache {
PackageConfig? packageConfig,
Logger? logger})
: _importers = _toImporters(importers, loadPaths, packageConfig),
_logger = logger ?? const Logger.stderr(),
_canonicalizeCache = {},
_importCache = {},
_resultsCache = {};
_logger = logger ?? const Logger.stderr();

/// Creates an import cache without any globally-available importers.
AsyncImportCache.none({Logger? logger})
: _importers = const [],
_logger = logger ?? const Logger.stderr(),
_canonicalizeCache = {},
_importCache = {},
_resultsCache = {};
_logger = logger ?? const Logger.stderr();

/// Converts the user's [importers], [loadPaths], and [packageConfig]
/// options into a single list of importers.
Expand Down Expand Up @@ -113,12 +122,16 @@ class AsyncImportCache {
Uri? baseUrl,
bool forImport = false}) async {
if (baseImporter != null) {
var resolvedUrl = baseUrl?.resolveUri(url) ?? url;
var canonicalUrl =
await _canonicalize(baseImporter, resolvedUrl, forImport);
if (canonicalUrl != null) {
return Tuple3(baseImporter, canonicalUrl, resolvedUrl);
}
var relativeResult = await putIfAbsentAsync(_relativeCanonicalizeCache,
Tuple4(url, forImport, baseImporter, baseUrl), () async {
var resolvedUrl = baseUrl?.resolveUri(url) ?? url;
var canonicalUrl =
await _canonicalize(baseImporter, resolvedUrl, forImport);
if (canonicalUrl != null) {
return Tuple3(baseImporter, canonicalUrl, resolvedUrl);
}
});
if (relativeResult != null) return relativeResult;
}

return await putIfAbsentAsync(_canonicalizeCache, Tuple2(url, forImport),
Expand Down Expand Up @@ -236,6 +249,14 @@ Relative canonical URLs are deprecated and will eventually be disallowed.
void clearCanonicalize(Uri url) {
_canonicalizeCache.remove(Tuple2(url, false));
_canonicalizeCache.remove(Tuple2(url, true));

var relativeKeysToClear = [
for (var key in _relativeCanonicalizeCache.keys)
if (key.item1 == url) key
];
for (var key in relativeKeysToClear) {
_relativeCanonicalizeCache.remove(key);
}
}

/// Clears the cached parse tree for the stylesheet with the given
Expand Down
57 changes: 57 additions & 0 deletions lib/src/evaluation_context.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2021 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.

import 'dart:async';

import 'package:source_span/source_span.dart';

/// An interface that exposes information about the current Sass evaluation.
///
/// This allows us to expose zone-scoped information without having to create a
/// new zone variable for each piece of information.
abstract class EvaluationContext {
/// The current evaluation context.
///
/// Throws [StateError] if there isn't a Sass stylesheet currently being
/// evaluated.
static EvaluationContext get current {
var context = Zone.current[#_evaluationContext];
if (context is EvaluationContext) return context;
throw StateError("No Sass stylesheet is currently being evaluated.");
}

/// Returns the span for the currently executing callable.
///
/// For normal exception reporting, this should be avoided in favor of
/// throwing [SassScriptException]s. It should only be used when calling APIs
/// that require spans.
///
/// Throws a [StateError] if there isn't a callable being invoked.
FileSpan get currentCallableSpan;

/// Prints a warning message associated with the current `@import` or function
/// call.
///
/// If [deprecation] is `true`, the warning is emitted as a deprecation
/// warning.
void warn(String message, {bool deprecation = false});
}

/// Prints a warning message associated with the current `@import` or function
/// call.
///
/// If [deprecation] is `true`, the warning is emitted as a deprecation warning.
///
/// This may only be called within a custom function or importer callback.
///
/// {@category Compile}
void warn(String message, {bool deprecation = false}) =>
EvaluationContext.current.warn(message, deprecation: deprecation);

/// Runs [callback] with [context] as [EvaluationContext.current].
///
/// This is zone-based, so if [callback] is asynchronous [warn] is set for the
/// duration of that callback.
T withEvaluationContext<T>(EvaluationContext context, T callback()) =>
runZoned(callback, zoneValues: {#_evaluationContext: context});
21 changes: 0 additions & 21 deletions lib/src/functions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@
// https://opensource.org/licenses/MIT.

import 'dart:collection';
import 'dart:async';

import 'package:collection/collection.dart';
import 'package:source_span/source_span.dart';

import 'ast/node.dart';
import 'callable.dart';
import 'functions/color.dart' as color;
import 'functions/list.dart' as list;
Expand Down Expand Up @@ -49,21 +46,3 @@ final coreModules = UnmodifiableListView([
selector.module,
string.module
]);

/// Returns the span for the currently executing callable.
///
/// For normal exception reporting, this should be avoided in favor of throwing
/// [SassScriptException]s. It should only be used when calling APIs that
/// require spans.
FileSpan get currentCallableSpan {
var node = Zone.current[#_currentCallableNode];
if (node is AstNode) return node.span;

throw StateError("currentCallableSpan may only be called within an "
"active Sass callable.");
}

/// Runs [callback] in a zone with [callableNode]'s span available from
/// [currentCallableSpan].
T withCurrentCallableNode<T>(AstNode callableNode, T callback()) =>
runZoned(callback, zoneValues: {#_currentCallableNode: callableNode});
2 changes: 1 addition & 1 deletion lib/src/functions/color.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import 'dart:collection';
import 'package:collection/collection.dart';

import '../callable.dart';
import '../evaluation_context.dart';
import '../exception.dart';
import '../module/built_in.dart';
import '../util/number.dart';
import '../util/nullable.dart';
import '../utils.dart';
import '../value.dart';
import '../warn.dart';

/// A regular expression matching the beginning of a proprietary Microsoft
/// filter declaration.
Expand Down
2 changes: 1 addition & 1 deletion lib/src/functions/math.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import 'dart:math' as math;
import 'package:collection/collection.dart';

import '../callable.dart';
import '../evaluation_context.dart';
import '../exception.dart';
import '../module/built_in.dart';
import '../util/number.dart';
import '../value.dart';
import '../warn.dart';

/// The global definitions of Sass math functions.
final global = UnmodifiableListView([
Expand Down
8 changes: 5 additions & 3 deletions lib/src/functions/selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import 'package:collection/collection.dart';

import '../ast/selector.dart';
import '../callable.dart';
import '../evaluation_context.dart';
import '../exception.dart';
import '../extend/extension_store.dart';
import '../functions.dart';
import '../module/built_in.dart';
import '../value.dart';

Expand Down Expand Up @@ -88,7 +88,8 @@ final _extend =
var target = arguments[1].assertSelector(name: "extendee");
var source = arguments[2].assertSelector(name: "extender");

return ExtensionStore.extend(selector, source, target, currentCallableSpan)
return ExtensionStore.extend(selector, source, target,
EvaluationContext.current.currentCallableSpan)
.asSassList;
});

Expand All @@ -98,7 +99,8 @@ final _replace =
var target = arguments[1].assertSelector(name: "original");
var source = arguments[2].assertSelector(name: "replacement");

return ExtensionStore.replace(selector, source, target, currentCallableSpan)
return ExtensionStore.replace(selector, source, target,
EvaluationContext.current.currentCallableSpan)
.asSassList;
});

Expand Down
59 changes: 40 additions & 19 deletions lib/src/import_cache.dart
Original file line number Diff line number Diff line change
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: b3b80fe96623c1579809528e46d9c75b87bf82ea
// Checksum: 3e290e40f4576be99217ddfbd7a76c4d38721af1
//
// ignore_for_file: unused_import

Expand Down Expand Up @@ -40,15 +40,30 @@ class ImportCache {
///
/// This map's values are the same as the return value of [canonicalize].
///
/// This cache isn't used for relative imports, because they're
/// context-dependent.
final Map<Tuple2<Uri, bool>, Tuple3<Importer, Uri, Uri>?> _canonicalizeCache;
/// This cache isn't used for relative imports, because they depend on the
/// specific base importer. That's stored separately in
/// [_relativeCanonicalizeCache].
final _canonicalizeCache = <Tuple2<Uri, bool>, Tuple3<Importer, Uri, Uri>?>{};

/// The canonicalized URLs for each non-canonical URL that's resolved using a
/// relative importer.
///
/// The map's keys have four parts:
///
/// 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 =
<Tuple4<Uri, bool, Importer, Uri?>, Tuple3<Importer, Uri, Uri>?>{};

/// The parsed stylesheets for each canonicalized import URL.
final Map<Uri, Stylesheet?> _importCache;
final _importCache = <Uri, Stylesheet?>{};

/// The import results for each canonicalized import URL.
final Map<Uri, ImporterResult> _resultsCache;
final _resultsCache = <Uri, ImporterResult>{};

/// Creates an import cache that resolves imports using [importers].
///
Expand All @@ -73,18 +88,12 @@ class ImportCache {
PackageConfig? packageConfig,
Logger? logger})
: _importers = _toImporters(importers, loadPaths, packageConfig),
_logger = logger ?? const Logger.stderr(),
_canonicalizeCache = {},
_importCache = {},
_resultsCache = {};
_logger = logger ?? const Logger.stderr();

/// Creates an import cache without any globally-available importers.
ImportCache.none({Logger? logger})
: _importers = const [],
_logger = logger ?? const Logger.stderr(),
_canonicalizeCache = {},
_importCache = {},
_resultsCache = {};
_logger = logger ?? const Logger.stderr();

/// Converts the user's [importers], [loadPaths], and [packageConfig]
/// options into a single list of importers.
Expand Down Expand Up @@ -117,11 +126,15 @@ class ImportCache {
Tuple3<Importer, Uri, Uri>? canonicalize(Uri url,
{Importer? baseImporter, Uri? baseUrl, bool forImport = false}) {
if (baseImporter != null) {
var resolvedUrl = baseUrl?.resolveUri(url) ?? url;
var canonicalUrl = _canonicalize(baseImporter, resolvedUrl, forImport);
if (canonicalUrl != null) {
return Tuple3(baseImporter, canonicalUrl, resolvedUrl);
}
var relativeResult = _relativeCanonicalizeCache
.putIfAbsent(Tuple4(url, forImport, baseImporter, baseUrl), () {
var resolvedUrl = baseUrl?.resolveUri(url) ?? url;
var canonicalUrl = _canonicalize(baseImporter, resolvedUrl, forImport);
if (canonicalUrl != null) {
return Tuple3(baseImporter, canonicalUrl, resolvedUrl);
}
});
if (relativeResult != null) return relativeResult;
}

return _canonicalizeCache.putIfAbsent(Tuple2(url, forImport), () {
Expand Down Expand Up @@ -235,6 +248,14 @@ Relative canonical URLs are deprecated and will eventually be disallowed.
void clearCanonicalize(Uri url) {
_canonicalizeCache.remove(Tuple2(url, false));
_canonicalizeCache.remove(Tuple2(url, true));

var relativeKeysToClear = [
for (var key in _relativeCanonicalizeCache.keys)
if (key.item1 == url) key
];
for (var key in relativeKeysToClear) {
_relativeCanonicalizeCache.remove(key);
}
}

/// Clears the cached parse tree for the stylesheet with the given
Expand Down
3 changes: 1 addition & 2 deletions lib/src/io/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,7 @@ T _systemErrorToFileSystemException<T>(T callback()) {
return callback();
} catch (error) {
if (error is! JsSystemError) rethrow;
throw FileSystemException._(
_cleanErrorMessage(error), error.path);
throw FileSystemException._(_cleanErrorMessage(error), error.path);
}
}

Expand Down
8 changes: 7 additions & 1 deletion lib/src/value/number.dart
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ abstract class SassNumber extends Value {
/// integer value, or [assertInt] to do both at once.
final num value;

/// The cached hash code for this number, if it's been computed.
///
/// @nodoc
@protected
int? hashCache;

/// This number's numerator units.
List<String> get numeratorUnits;

Expand Down Expand Up @@ -826,7 +832,7 @@ abstract class SassNumber extends Value {
}
}

int get hashCode => fuzzyHashCode(value *
int get hashCode => hashCache ??= fuzzyHashCode(value *
_canonicalMultiplier(numeratorUnits) /
_canonicalMultiplier(denominatorUnits));

Expand Down
Loading

0 comments on commit 435e1b2

Please sign in to comment.