Add support for the fibers package to speed up render()

This commit is contained in:
Natalie Weizenbaum 2017-12-01 14:28:26 -08:00 committed by Natalie Weizenbaum
parent 0a67d3845f
commit aa5fd1d060
6 changed files with 167 additions and 16 deletions

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});
}

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")));
});
});
});
}