Skip to content

Commit

Permalink
Implement sass --embedded in pure JS mode
Browse files Browse the repository at this point in the history
  • Loading branch information
ntkme committed Oct 28, 2024
1 parent 7129352 commit b3794eb
Show file tree
Hide file tree
Showing 27 changed files with 490 additions and 74 deletions.
3 changes: 1 addition & 2 deletions bin/sass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import 'package:sass/src/io.dart';
import 'package:sass/src/stylesheet_graph.dart';
import 'package:sass/src/utils.dart';
import 'package:sass/src/embedded/executable.dart'
// Never load the embedded protocol when compiling to JS.
if (dart.library.js) 'package:sass/src/embedded/unavailable.dart'
if (dart.library.js) 'package:sass/src/embedded/js/executable.dart'
as embedded;

Future<void> main(List<String> args) async {
Expand Down
18 changes: 9 additions & 9 deletions lib/src/embedded/compilation_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@
// https://opensource.org/licenses/MIT.

import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:isolate' if (dart.library.js) 'js/isolate.dart';
import 'dart:typed_data';

import 'package:native_synchronization/mailbox.dart';
import 'package:path/path.dart' as p;
import 'package:protobuf/protobuf.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:sass/sass.dart' as sass;
import 'package:sass/src/importer/node_package.dart' as npi;

import '../io.dart';
import '../logger.dart';
import '../value/function.dart';
import '../value/mixin.dart';
Expand All @@ -23,6 +22,7 @@ import 'host_callable.dart';
import 'importer/file.dart';
import 'importer/host.dart';
import 'logger.dart';
import 'sync_receive_port.dart';
import 'util/proto_extensions.dart';
import 'utils.dart';

Expand All @@ -35,8 +35,8 @@ final _outboundRequestId = 0;
/// A class that dispatches messages to and from the host for a single
/// compilation.
final class CompilationDispatcher {
/// The mailbox for receiving messages from the host.
final Mailbox _mailbox;
/// The synchronous receive port for receiving messages from the host.
final SyncReceivePort _receivePort;

/// The send port for sending messages to the host.
final SendPort _sendPort;
Expand All @@ -52,8 +52,8 @@ final class CompilationDispatcher {
late Uint8List _compilationIdVarint;

/// Creates a [CompilationDispatcher] that receives encoded protocol buffers
/// through [_mailbox] and sends them through [_sendPort].
CompilationDispatcher(this._mailbox, this._sendPort);
/// through [_receivePort] and sends them through [_sendPort].
CompilationDispatcher(this._receivePort, this._sendPort);

/// Listens for incoming `CompileRequests` and runs their compilations.
void listen() {
Expand Down Expand Up @@ -384,9 +384,9 @@ final class CompilationDispatcher {
/// Receive a packet from the host.
Uint8List _receive() {
try {
return _mailbox.take();
return _receivePort.receive();
} on StateError catch (_) {
// The [_mailbox] has been closed, exit the current isolate immediately
// The [SyncReceivePort] has been closed, exit the current isolate immediately
// to avoid bubble the error up as [SassException] during [_sendRequest].
Isolate.exit();
}
Expand Down
10 changes: 10 additions & 0 deletions lib/src/embedded/concurrency.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright 2024 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:ffi';

/// More than MaxMutatorThreadCount isolates in the same isolate group
/// can deadlock the Dart VM.
/// See https://github.com/sass/dart-sass/pull/2019
int get concurrencyLimit => sizeOf<IntPtr>() <= 4 ? 7 : 15;
29 changes: 6 additions & 23 deletions lib/src/embedded/executable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,18 @@
// https://opensource.org/licenses/MIT.

import 'dart:io';
import 'dart:convert';

import 'package:stream_channel/stream_channel.dart';

import 'isolate_dispatcher.dart';
import 'options.dart';
import 'util/length_delimited_transformer.dart';

void main(List<String> args) {
switch (args) {
case ["--version", ...]:
var response = IsolateDispatcher.versionResponse();
response.id = 0;
stdout.writeln(
JsonEncoder.withIndent(" ").convert(response.toProto3Json()));
return;

case [_, ...]:
stderr.writeln(
"sass --embedded is not intended to be executed with additional "
"arguments.\n"
"See https://github.com/sass/dart-sass#embedded-dart-sass for "
"details.");
// USAGE error from https://bit.ly/2poTt90
exitCode = 64;
return;
if (parseOptions(args)) {
IsolateDispatcher(
StreamChannel.withGuarantees(stdin, stdout, allowSinkErrors: false)
.transform(lengthDelimited))
.listen();
}

IsolateDispatcher(
StreamChannel.withGuarantees(stdin, stdout, allowSinkErrors: false)
.transform(lengthDelimited))
.listen();
}
23 changes: 7 additions & 16 deletions lib/src/embedded/isolate_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@
// https://opensource.org/licenses/MIT.

import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:io' if (dart.library.js) 'js/io.dart';
import 'dart:typed_data';

import 'package:native_synchronization/mailbox.dart';
import 'package:pool/pool.dart';
import 'package:protobuf/protobuf.dart';
import 'package:stream_channel/stream_channel.dart';

import 'compilation_dispatcher.dart';
import '../io.dart';
import 'concurrency.dart' if (dart.library.js) 'js/concurrency.dart';
import 'embedded_sass.pb.dart';
import 'reusable_isolate.dart';
import 'isolate_main.dart';
import 'reusable_isolate.dart' if (dart.library.js) 'js/reusable_isolate.dart';
import 'util/proto_extensions.dart';
import 'utils.dart';

Expand All @@ -38,11 +37,7 @@ class IsolateDispatcher {

/// A pool controlling how many isolates (and thus concurrent compilations)
/// may be live at once.
///
/// More than MaxMutatorThreadCount isolates in the same isolate group
/// can deadlock the Dart VM.
/// See https://github.com/sass/dart-sass/pull/2019
final _isolatePool = Pool(sizeOf<IntPtr>() <= 4 ? 7 : 15);
final _isolatePool = Pool(concurrencyLimit);

/// Whether [_channel] has been closed or not.
var _closed = false;
Expand Down Expand Up @@ -112,7 +107,7 @@ class IsolateDispatcher {
isolate = _inactiveIsolates.first;
_inactiveIsolates.remove(isolate);
} else {
var future = ReusableIsolate.spawn(_isolateMain,
var future = ReusableIsolate.spawn(isolateMain,
onError: (Object error, StackTrace stackTrace) {
_handleError(error, stackTrace);
});
Expand Down Expand Up @@ -179,7 +174,3 @@ class IsolateDispatcher {
void sendError(int compilationId, ProtocolError error) =>
_send(compilationId, OutboundMessage()..error = error);
}

void _isolateMain(Mailbox mailbox, SendPort sendPort) {
CompilationDispatcher(mailbox, sendPort).listen();
}
12 changes: 12 additions & 0 deletions lib/src/embedded/isolate_main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2024 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:isolate' show SendPort;

import 'compilation_dispatcher.dart';
import 'sync_receive_port.dart';

void isolateMain(SyncReceivePort receivePort, SendPort sendPort) {
CompilationDispatcher(receivePort, sendPort).listen();
}
10 changes: 10 additions & 0 deletions lib/src/embedded/js/concurrency.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright 2024 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:js_interop';

@JS('os.cpus')
external JSArray _cpus();

int get concurrencyLimit => _cpus().toDart.length;
27 changes: 27 additions & 0 deletions lib/src/embedded/js/executable.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2024 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:stream_channel/stream_channel.dart';

import '../compilation_dispatcher.dart';
import '../isolate_dispatcher.dart';
import '../options.dart';
import '../util/length_delimited_transformer.dart';
import 'io.dart';
import 'sync_receive_port.dart';
import 'worker_threads.dart';

void main(List<String> args) {
if (parseOptions(args)) {
if (isMainThread) {
IsolateDispatcher(StreamChannel.withGuarantees(stdin, stdout,
allowSinkErrors: false)
.transform(lengthDelimited))
.listen();
} else {
var port = workerData! as MessagePort;
CompilationDispatcher(JSSyncReceivePort(port), JSSendPort(port)).listen();
}
}
}
60 changes: 60 additions & 0 deletions lib/src/embedded/js/io.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2024 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 'dart:typed_data';
import 'dart:js_interop';

import '../../io.dart';

/// In Node JS's main thread we need to wait for worker to exit in order
/// to get exit code set by worker asynchronously. Therefore skip explict
/// exit on main thread, but close stdin and wait for shutdown.
void exit([int? code]) {
if (code != null) {
exitCode = code;
}
_stdinDestory();
}

@JS('process.stdin.destroy')
external void _stdinDestory();

@JS('process.stdin.on')
external void _stdinOn(String type, JSFunction listener);

@JS('process.stdout.write')
external void _stdoutWrite(JSUint8Array buffer);

Stream<List<int>> get stdin {
var controller = StreamController<Uint8List>(
onCancel: () {
_stdinDestory();
},
sync: true);
_stdinOn(
'data',
(JSUint8Array chunk) {
controller.sink.add(chunk.toDart);
}.toJS);
_stdinOn(
'end',
() {
controller.sink.close();
}.toJS);
_stdinOn(
'error',
(JSObject e) {
controller.sink.addError(e);
}.toJS);
return controller.stream;
}

StreamSink<List<int>> get stdout {
var controller = StreamController<Uint8List>(sync: true);
controller.stream.listen((buffer) {
_stdoutWrite(buffer.toJS);
});
return controller.sink;
}
22 changes: 22 additions & 0 deletions lib/src/embedded/js/isolate.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2024 Google LLC. 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:isolate' show SendPort;
export 'dart:isolate' show SendPort;
import 'dart:js_interop';

@JS('process.exit')
external void _exit();

abstract class Isolate {
static Never exit([SendPort? finalMessagePort, Object? message]) {
if (message != null) {
finalMessagePort?.send(message);
}
_exit();

// This is unreachable, but needed for return type [Never]
throw Error();
}
}
Loading

0 comments on commit b3794eb

Please sign in to comment.