Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Use worker threads in render #939

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
86 changes: 43 additions & 43 deletions lib/src/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import 'node/render_result.dart';
import 'node/types.dart';
import 'node/value.dart';
import 'node/utils.dart';
import 'node/worker_threads.dart';
import 'parse/scss.dart';
import 'syntax.dart';
import 'value.dart';
Expand Down Expand Up @@ -63,56 +64,55 @@ void main() {
/// [render]: https://github.com/sass/node-sass#options
void _render(
RenderOptions options, void callback(JSError error, RenderResult result)) {
if (options.fiber != null) {
options.fiber.call(allowInterop(() {
try {
callback(null, _renderSync(options));
} catch (error) {
callback(error as JSError, null);
}
return null;
})).run();
} else {
_renderAsync(options).then((result) {
callback(null, result);
}, onError: (Object error, StackTrace stackTrace) {
if (error is SassException) {
callback(_wrapException(error), null);
} else {
callback(_newRenderError(error.toString(), status: 3), null);
}
});
}
_renderAsync(options).then((result) {
callback(null, result);
}, onError: (Object error, StackTrace stackTrace) {
if (error is SassException) {
callback(_wrapException(error), null);
} else {
callback(_newRenderError(error.toString(), status: 3), null);
}
});
}

/// Converts Sass to CSS asynchronously.
Future<RenderResult> _renderAsync(RenderOptions options) async {
var start = DateTime.now();
var file = options.file == null ? null : p.absolute(options.file);
CompileResult result;
if (options.data != null) {
result = await compileStringAsync(options.data,
nodeImporter: _parseImporter(options, start),
functions: _parseFunctions(options, asynch: true),
syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null,
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
indentWidth: _parseIndentWidth(options.indentWidth),
lineFeed: _parseLineFeed(options.linefeed),
url: options.file == null ? 'stdin' : p.toUri(file).toString(),
sourceMap: _enableSourceMaps(options));
} else if (options.file != null) {
result = await compileAsync(file,
nodeImporter: _parseImporter(options, start),
functions: _parseFunctions(options, asynch: true),
syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null,
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
indentWidth: _parseIndentWidth(options.indentWidth),
lineFeed: _parseLineFeed(options.linefeed),
sourceMap: _enableSourceMaps(options));
} else {
throw ArgumentError("Either options.data or options.file must be set.");
if (isMainThread == true) {
print(p.current);
final worker = Worker(p.current, WorkerOptions(workerData: {options}));
worker.on('message', (CompileResult msg) => result = msg);
worker.on('error', (JSError error) {
jsThrow(_wrapException(error));
});
} else if (isMainThread == false) {
if (options.data != null) {
result = await compileStringAsync(options.data,
nodeImporter: _parseImporter(options, start),
functions: _parseFunctions(options, asynch: true),
syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null,
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
indentWidth: _parseIndentWidth(options.indentWidth),
lineFeed: _parseLineFeed(options.linefeed),
url: options.file == null ? 'stdin' : p.toUri(file).toString(),
sourceMap: _enableSourceMaps(options));
} else if (options.file != null) {
result = await compileAsync(file,
nodeImporter: _parseImporter(options, start),
functions: _parseFunctions(options, asynch: true),
syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null,
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
indentWidth: _parseIndentWidth(options.indentWidth),
lineFeed: _parseLineFeed(options.linefeed),
sourceMap: _enableSourceMaps(options));
} else {
throw ArgumentError("Either options.data or options.file must be set.");
}
parentPort.postMessage(result, PortOptions());
}

return _newRenderResult(options, result, start);
Expand Down
51 changes: 51 additions & 0 deletions lib/src/node/worker_threads.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2020 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.
@JS("worker_threads")
library worker_threads;

import 'package:js/js.dart';

@JS("worker_threads")
external WorkerThreads get worker_threads;

bool isMainThread = worker_threads?.isMainThread;
ParentPort parentPort = worker_threads?.parentPort;

@JS()
abstract class WorkerThreads {
@JS('Worker')
external Worker get worker;
external bool get workerData;
external bool get isMainThread;
external ParentPort get parentPort;
external const factory WorkerThreads();
}

@JS()
@anonymous
class WorkerOptions {
external factory WorkerOptions(
{Object env, bool eval, List<String> execArgv, Object workerData});
}

@JS()
class Worker {
external void on(String message, Function callback);

external const factory Worker(String fileName, WorkerOptions options);
}

@JS("parentPort")
@anonymous
class PortOptions {
external List get transferList;
external factory PortOptions({List<Object> transferList = const []});
}

@JS("parentPort")
@anonymous
abstract class ParentPort {
external factory ParentPort({Function postMessage});
external void postMessage(Object message, PortOptions options);
}
2 changes: 1 addition & 1 deletion package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"url": "https://github.com/nex3"
},
"engines": {
"node": ">=8.9.0"
"node": ">=11.7.0"
},
"dependencies": {
"chokidar": ">=2.0.0 <4.0.0"
Expand Down
89 changes: 0 additions & 89 deletions test/node_api/function_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -287,95 +287,6 @@ void main() {
data: "", functions: jsify({"foo(": allowInterop(neverCalled)})));
expect(error.toString(), contains('Invalid signature'));
});

group("with fibers", () {
setUpAll(() {
try {
fiber;
} catch (_) {
throw "Can't load fibers package.\n"
"Run pub run grinder before-test.";
}
});

test("runs a synchronous function", () {
expect(
render(RenderOptions(
data: "a {b: foo()}",
functions: jsify({
"foo": allowInterop(
(void _) => callConstructor(sass.types.Number, [1]))
}),
fiber: fiber)),
completion(equalsIgnoringWhitespace("a { b: 1; }")));
});

test("runs an asynchronous function", () {
expect(
render(RenderOptions(
data: "a {b: foo()}",
functions: jsify({
"foo": allowInterop((void done(Object result)) {
Timer(Duration.zero, () {
done(callConstructor(sass.types.Number, [1]));
});
})
}),
fiber: fiber)),
completion(equalsIgnoringWhitespace("a { b: 1; }")));
});

test("reports a synchronous error", () async {
var error = await renderError(RenderOptions(
data: "a {b: foo()}",
functions:
jsify({"foo": allowInterop((void _) => throw "aw beans")}),
fiber: fiber));
expect(error.toString(), contains('aw beans'));
});

test("reports an asynchronous error", () async {
var error = await renderError(RenderOptions(
data: "a {b: foo()}",
functions: jsify({
"foo": allowInterop((void done(Object result)) {
Timer(Duration.zero, () {
done(JSError("aw beans"));
});
})
}),
fiber: fiber));
expect(error.toString(), contains('aw beans'));
});

test("reports a null return", () async {
var error = await renderError(RenderOptions(
data: "a {b: foo()}",
functions: jsify({
"foo": allowInterop((void done(Object result)) {
Timer(Duration.zero, () {
done(null);
});
})
}),
fiber: fiber));
expect(error.toString(), contains('must be a Sass value type'));
});

test("reports a call to done without arguments", () async {
var error = await renderError(RenderOptions(
data: "a {b: foo()}",
functions: jsify({
"foo": allowInterop((void done()) {
Timer(Duration.zero, () {
done();
});
})
}),
fiber: fiber));
expect(error.toString(), contains('must be a Sass value type'));
});
});
});

// Node Sass currently doesn't provide any representation of first-class
Expand Down
3 changes: 3 additions & 0 deletions test/node_api/importer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,8 @@ void main() {
" stdin 1:9 root stylesheet")));
});

//TODO: Remove these tests
/******* Deprecated fibers tests
group("with fibers", () {
setUpAll(() {
try {
Expand Down Expand Up @@ -749,5 +751,6 @@ void main() {
" stdin 1:9 root stylesheet")));
});
});
*/
});
}
7 changes: 6 additions & 1 deletion tool/grind.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ void main(List<String> args) {
pkg.chocolateyNuspec = _nuspec;
pkg.homebrewRepo = "sass/homebrew-sass";
pkg.homebrewFormula = "sass.rb";
pkg.jsRequires = {"fs": "fs", "chokidar": "chokidar", "readline": "readline"};
pkg.jsRequires = {
"fs": "fs",
"chokidar": "chokidar",
"readline": "readline",
"workerThreads": "worker_threads"
};
pkg.jsModuleMainLibrary = "lib/src/node.dart";
pkg.npmPackageJson =
json.decode(File("package/package.json").readAsStringSync())
Expand Down
Loading