From d51369200dc505f0d64f6da3e06e38e713aa6182 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 3 Feb 2017 14:19:28 -0800 Subject: [PATCH] Improve Node file load speed. --- CHANGELOG.md | 2 ++ lib/sass.dart | 5 +++-- lib/src/executable.dart | 2 +- lib/src/io/interface.dart | 10 +++++----- lib/src/io/node.dart | 25 ++++++++++++++++++++++--- lib/src/io/vm.dart | 25 ++++++++++++++++++++++--- lib/src/utils.dart | 28 ---------------------------- lib/src/visitor/perform.dart | 2 +- 8 files changed, 56 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 199bbf9a2..29e76665e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,8 @@ * Error out if a function is passed an unknown named parameter. +* Improve the speed of loading large files on Node. + ## 1.0.0-alpha.8 * Add the `content-exists()` function. diff --git a/lib/sass.dart b/lib/sass.dart index 134163b5b..3886c3945 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -5,8 +5,9 @@ import 'package:path/path.dart' as p; import 'src/ast/sass.dart'; +import 'src/exception.dart'; +import 'src/io.dart'; import 'src/sync_package_resolver.dart'; -import 'src/utils.dart'; import 'src/visitor/perform.dart'; import 'src/visitor/serialize.dart'; @@ -23,7 +24,7 @@ import 'src/visitor/serialize.dart'; /// Finally throws a [SassException] if conversion fails. String render(String path, {bool color: false, SyncPackageResolver packageResolver}) { - var contents = readSassFile(path); + var contents = readFile(path); var url = p.toUri(path); var sassTree = p.extension(path) == '.sass' ? new Stylesheet.parseSass(contents, url: url, color: color) diff --git a/lib/src/executable.dart b/lib/src/executable.dart index 127c3c994..d4ed470ed 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -106,7 +106,7 @@ Future _loadVersion() async { var libDir = p.fromUri(await Isolate.resolvePackageUri(Uri.parse('package:sass/'))); - var pubspec = readFileAsString(p.join(libDir, '..', 'pubspec.yaml')); + var pubspec = readFile(p.join(libDir, '..', 'pubspec.yaml')); return pubspec .split("\n") .firstWhere((line) => line.startsWith('version: ')) diff --git a/lib/src/io/interface.dart b/lib/src/io/interface.dart index 86b942a39..b15ca6d5b 100644 --- a/lib/src/io/interface.dart +++ b/lib/src/io/interface.dart @@ -17,7 +17,7 @@ class Stderr { void flush() {} } -/// An error thrown by [readFileAsBytes] and [readFileAsString]. +/// An error thrown by [readFile]. class FileSystemException { String get message => null; } @@ -28,11 +28,11 @@ Stderr get stderr => null; /// Returns whether or not stdout is connected to an interactive terminal. bool get hasTerminal => false; -/// Reads the file at [path] as a list of bytes. -List readFileAsBytes(String path) => null; - /// Reads the file at [path] as a UTF-8 encoded string. -String readFileAsString(String path) => null; +/// +/// Throws a [FileSystemException] if reading fails, and a [SassException] if +/// the file isn't valid UTF-8. +String readFile(String path) => null; /// Returns whether a file at [path] exists. bool fileExists(String path) => null; diff --git a/lib/src/io/node.dart b/lib/src/io/node.dart index fc36e64d5..d86b5e2fb 100644 --- a/lib/src/io/node.dart +++ b/lib/src/io/node.dart @@ -2,9 +2,14 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'dart:convert'; import 'dart:typed_data'; import 'package:js/js.dart'; +import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; + +import '../exception.dart'; @JS() class _FS { @@ -51,14 +56,28 @@ external _FS _require(String name); final _fs = _require("fs"); -List readFileAsBytes(String path) => _readFile(path) as Uint8List; +String readFile(String path) { + // TODO(nweiz): explicitly decode the bytes as UTF-8 like we do in the VM when + // it doesn't cause a substantial performance degradation for large files. See + // also dart-lang/sdk#25377. + var contents = _readFile(path, 'utf8'); + if (!contents.contains("�")) return contents; + + var sourceFile = new SourceFile(contents, url: p.toUri(path)); + for (var i = 0; i < contents.length; i++) { + if (contents.codeUnitAt(i) != 0xFFFD) continue; + throw new SassException( + "Invalid UTF-8.", sourceFile.location(i).pointSpan()); + } -String readFileAsString(String path) => _readFile(path, 'utf8') as String; + // This should be unreachable. + return string; +} /// Wraps `fs.readFileSync` to throw a [FileSystemException]. _readFile(String path, [String encoding]) { try { - return _fs.readFileSync(path); + return _fs.readFileSync(path, encoding); } catch (error) { throw new FileSystemException._(_cleanErrorMessage(error as _SystemError)); } diff --git a/lib/src/io/vm.dart b/lib/src/io/vm.dart index 1dcaef0bd..6511baf35 100644 --- a/lib/src/io/vm.dart +++ b/lib/src/io/vm.dart @@ -10,9 +10,28 @@ io.Stdout get stderr => io.stderr; bool get hasTerminal => io.stdout.hasTerminal; -List readFileAsBytes(String path) => new io.File(path).readAsBytesSync(); - -String readFileAsString(String path) => new io.File(path).readAsStringSync(); +String readFile(String path) { + var bytes = readFile(path); + + try { + return UTF8.decode(bytes); + } on FormatException { + var decoded = UTF8.decode(bytes, allowMalformed: true); + var sourceFile = new SourceFile(decoded, url: p.toUri(path)); + + // TODO(nweiz): Use [FormatException.offset] instead when + // dart-lang/sdk#28293 is fixed. + for (var i = 0; i < bytes.length; i++) { + if (decoded.codeUnitAt(i) != 0xFFFD) continue; + throw new SassException( + "Invalid UTF-8.", sourceFile.location(i).pointSpan()); + } + + // This should be unreachable, but we'll rethrow the original exception just + // in case. + rethrow; + } +} bool fileExists(String path) => new io.File(path).existsSync(); diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 8c0cb6964..e66ac875c 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -3,16 +3,13 @@ // https://opensource.org/licenses/MIT. import 'dart:collection'; -import 'dart:convert'; import 'dart:math' as math; import 'package:charcode/charcode.dart'; import 'package:collection/collection.dart'; -import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; import 'ast/node.dart'; -import 'exception.dart'; import 'io.dart'; import 'util/character.dart'; @@ -257,31 +254,6 @@ List/**/ longestCommonSubsequence/**/( } } -/// Reads a Sass source file from diskx, and throws a [SassException] if UTF-8 -/// decoding fails. -String readSassFile(String path) { - var bytes = readFileAsBytes(path); - - try { - return UTF8.decode(bytes); - } on FormatException { - var decoded = UTF8.decode(bytes, allowMalformed: true); - var sourceFile = new SourceFile(decoded, url: p.toUri(path)); - - // TODO(nweiz): Use [FormatException.offset] instead when - // dart-lang/sdk#28293 is fixed. - for (var i = 0; i < bytes.length; i++) { - if (decoded.codeUnitAt(i) != 0xFFFD) continue; - throw new SassException( - "Invalid UTF-8.", sourceFile.location(i).pointSpan()); - } - - // This should be unreachable, but we'll rethrow the original exception just - // in case. - rethrow; - } -} - /// Prints a warning to standard error, associated with [span]. /// /// If [color] is `true`, this uses terminal colors. diff --git a/lib/src/visitor/perform.dart b/lib/src/visitor/perform.dart index 796fe30da..babad1829 100644 --- a/lib/src/visitor/perform.dart +++ b/lib/src/visitor/perform.dart @@ -636,7 +636,7 @@ class _PerformVisitor return _importedFiles.putIfAbsent(path, () { String contents; try { - contents = readSassFile(path); + contents = readFile(path); } on SassException catch (error) { var frames = _stack.toList()..add(_stackFrame(import.span)); throw new SassRuntimeException(