Merge pull request #198 from sass/fibers

Add support for the fibers package to speed up render()
This commit is contained in:
Natalie Weizenbaum 2017-12-02 12:48:24 -08:00 committed by GitHub
commit 5a00423228
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 204 additions and 31 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ packages
.packages
pubspec.lock
/benchmark/source
node_modules/

View File

@ -43,7 +43,7 @@ install:
- if-node . "$HOME/.nvm/nvm.sh"
- if-node nvm install "$NODE_VERSION"
- if-node nvm use "$NODE_VERSION"
- SASS_MINIFY_JS=false if-node pub run grinder npm_package
- SASS_MINIFY_JS=false if-node pub run grinder before_test
# Download sass-spec and install its dependencies if we're running specs.
- if-specs() { if [ "$TASK" = specs ]; then "$@"; fi }

View File

@ -113,10 +113,30 @@ That's it!
When installed via NPM, Dart Sass supports a JavaScript API that aims to be
compatible with [Node Sass](https://github.com/sass/node-sass#usage). Full
compatibility is a work in progress, but Dart Sass currently supports the
`render()` and `renderSync()` functions. Note however that **`renderSync()` is
much faster than `render()`**, due to the overhead of asynchronous callbacks.
It's highly recommended that users use `renderSync()` unless they absolutely
require support for asynchronous importers.
`render()` and `renderSync()` functions. Note however that by default,
**`renderSync()` is more than twice as fast as `render()`**, due to the overhead
of asynchronous callbacks.
To avoid this performance hit, `render()` can use the [`fibers`][fibers] package
to call asynchronous importers from the synchronous code path. To enable this,
pass the `Fiber` class to the `fiber` option:
[fibers]: https://www.npmjs.com/package/fibers
```js
var sass = require("sass");
var Fiber = require("fibers");
render({
file: "input.scss",
importer: function(url, prev, done) {
// ...
},
fiber: Fiber
}, function(err, result) {
// ...
});
```
Both `render()` and `renderSync()` support the following options:

View File

@ -48,15 +48,25 @@ void main() {
/// [render]: https://github.com/sass/node-sass#options
void _render(RenderOptions options,
void callback(RenderError error, RenderResult result)) {
_renderAsync(options).then((result) {
callback(null, result);
}, onError: (error, stackTrace) {
if (error is SassException) {
callback(_wrapException(error), null);
} else {
callback(newRenderError(error.toString(), status: 3), null);
}
});
if (options.fiber != null) {
options.fiber.call(allowInterop(() {
try {
callback(null, _renderSync(options));
} catch (error) {
callback(error as RenderError, null);
}
})).run();
} else {
_renderAsync(options).then((result) {
callback(null, result);
}, onError: (error, stackTrace) {
if (error is SassException) {
callback(_wrapException(error), null);
} else {
callback(newRenderError(error.toString(), status: 3), null);
}
});
}
}
/// Converts Sass to CSS asynchronously.
@ -206,6 +216,23 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) {
context.options.context = context;
}
if (options.fiber != null) {
importers = importers.map((importer) {
return allowInteropCaptureThis((thisArg, String url, String previous,
[_]) {
var fiber = options.fiber.current;
var result =
call3(importer, thisArg, url, previous, allowInterop((result) {
// Schedule a microtask so we don't try to resume the running fiber if
// [importer] calls `done()` synchronously.
scheduleMicrotask(() => fiber.run(result));
}));
if (isUndefined(result)) return options.fiber.yield();
return result;
}) as _Importer;
}).toList();
}
return new NodeImporter(context, includePaths, importers);
}

22
lib/src/node/fiber.dart Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2017 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 'package:js/js.dart';
@JS()
@anonymous
class FiberClass {
// Work around sdk#31490.
external Fiber call(function());
external Fiber get current;
external yield([value]);
}
@JS()
@anonymous
class Fiber {
external run([value]);
}

View File

@ -4,6 +4,8 @@
import 'package:js/js.dart';
import 'fiber.dart';
@JS()
@anonymous
class RenderOptions {
@ -16,6 +18,7 @@ class RenderOptions {
external String get indentType;
external dynamic get indentWidth;
external String get linefeed;
external FiberClass get fiber;
external factory RenderOptions(
{String file,
@ -26,5 +29,6 @@ class RenderOptions {
String outputStyle,
String indentType,
indentWidth,
String linefeed});
String linefeed,
FiberClass fiber});
}

9
package.json Normal file
View File

@ -0,0 +1,9 @@
{
"//": [
"This isn't the official package.json for Dart Sass. It's just used to ",
"install dependencies used for testing the Node API."
],
"devDependencies": {
"fibers": ">=1.0.0 <3.0.0"
}
}

View File

@ -36,6 +36,6 @@ dev_dependencies:
stream_channel: "^1.0.0"
test_descriptor: "^1.0.0"
test_process: "^1.0.0-rc.1"
test: "^0.12.26"
test: "^0.12.29"
xml: "^2.4.0"
yaml: "^2.0.0"

View File

@ -13,23 +13,30 @@ hybridMain(StreamChannel channel) async {
throw "NPM package is not built. Run pub run grinder npm_package.";
}
var lastModified = new DateTime(0);
var entriesToCheck = new Directory("lib").listSync(recursive: true).toList()
..add(new File("pubspec.lock"));
var lastModified = new File("build/npm/package.json").lastModifiedSync();
var entriesToCheck = new Directory("lib").listSync(recursive: true).toList();
// If we have a dependency override, "pub run" will touch the lockfile to mark
// it as newer than the pubspec, which makes it unsuitable to use for
// freshness checking.
if (new File("pubspec.yaml")
.readAsStringSync()
.contains("dependency_overrides")) {
entriesToCheck.add(new File("pubspec.yaml"));
} else {
entriesToCheck.add(new File("pubspec.lock"));
}
for (var entry in entriesToCheck) {
if (entry is File) {
var entryLastModified = entry.lastModifiedSync();
if (lastModified.isBefore(entryLastModified)) {
lastModified = entryLastModified;
throw "${entry.path} was modified after NPM package was generated.\n"
"Run pub run grinder before_test.";
}
}
}
if (lastModified
.isAfter(new File("build/npm/package.json").lastModifiedSync())) {
throw "NPM package is out-of-date. Run pub run grinder npm_package.";
}
channel.sink.close();
}

View File

@ -6,6 +6,7 @@
/// to Dart. This is kind of convoluted, but it allows us to test the API as it
/// will be used in the real world without having to manually write any JS.
import 'package:sass/src/node/fiber.dart';
import 'package:sass/src/node/render_error.dart';
import 'package:sass/src/node/render_options.dart';
import 'package:sass/src/node/render_result.dart';
@ -20,7 +21,10 @@ export 'package:sass/src/node/render_options.dart';
export 'package:sass/src/node/render_result.dart';
/// The Sass module.
final sass = _require(p.absolute("build/npm/sass.dart"));
final sass = _requireSass(p.absolute("build/npm/sass.dart"));
/// The Fiber class.
final fiber = _requireFiber("fibers");
/// A `null` that's guaranteed to be represented by JavaScript's `undefined`
/// value, not by `null`.
@ -41,7 +45,10 @@ external Object _eval(String js);
external void chdir(String directory);
@JS("require")
external Sass _require(String path);
external Sass _requireSass(String path);
@JS("require")
external FiberClass _requireFiber(String path);
@JS()
class Sass {

View File

@ -580,5 +580,76 @@ void main() {
toStringAndMessageEqual("Can't find stylesheet to import.\n"
" stdin 1:9 root stylesheet")));
});
group("with fibers", () {
setUpAll(() {
try {
fiber;
} catch (_) {
throw "Can't load fibers package.\n"
"Run pub run grinder before_test.";
}
});
test("supports asynchronous importers", () {
expect(
render(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, done) {
new Future.delayed(Duration.ZERO).then((_) {
done(new NodeImporterResult(contents: 'a {b: c}'));
});
}),
fiber: fiber)),
completion(equalsIgnoringWhitespace('a { b: c; }')));
});
test("supports synchronous calls to done", () {
expect(
render(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, done) {
done(new NodeImporterResult(contents: 'a {b: c}'));
}),
fiber: fiber)),
completion(equalsIgnoringWhitespace('a { b: c; }')));
});
test("supports synchronous importers", () {
expect(
render(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, ___) {
return new NodeImporterResult(contents: 'a {b: c}');
}),
fiber: fiber)),
completion(equalsIgnoringWhitespace('a { b: c; }')));
});
test("supports asynchronous errors", () {
expect(
renderError(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, done) {
new Future.delayed(Duration.ZERO).then((_) {
done(new JSError('oh no'));
});
}),
fiber: fiber)),
completion(toStringAndMessageEqual("oh no\n"
" stdin 1:9 root stylesheet")));
});
test("supports synchronous null returns", () {
expect(
renderError(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, ___) => jsNull),
fiber: fiber)),
completion(
toStringAndMessageEqual("Can't find stylesheet to import.\n"
" stdin 1:9 root stylesheet")));
});
});
});
}

View File

@ -38,10 +38,8 @@ final _sdkDir = p.dirname(p.dirname(Platform.resolvedExecutable));
main(List<String> args) => grind(args);
@DefaultTask('Compile async code and reformat.')
all() {
format();
synchronize();
}
@Depends(format, synchronize)
all() {}
@Task('Run the Dart formatter.')
format() {
@ -100,6 +98,13 @@ npm_package() {
_writeNpmPackage('build/npm-old', json..addAll({"name": "dart-sass"}));
}
@Task('Installs dependencies from npm.')
npm_install() => run("npm", arguments: ["install"]);
@Task('Runs the tasks that are required for running tests.')
@Depends(npm_package, npm_install)
before_test() {}
/// Writes a Dart Sass NPM package to the directory at [destination].
///
/// The [json] will be used as the package's package.json.