Skip to content

Commit 77e208c

Browse files
ntkmenex3
andauthored
Run cli compilations in parallel dart isolates (#2078)
Co-authored-by: Natalie Weizenbaum <[email protected]>
1 parent fddf421 commit 77e208c

File tree

13 files changed

+273
-160
lines changed

13 files changed

+273
-160
lines changed

bin/sass.dart

Lines changed: 8 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,21 @@ import 'package:path/path.dart' as p;
88
import 'package:stack_trace/stack_trace.dart';
99
import 'package:term_glyph/term_glyph.dart' as term_glyph;
1010

11-
import 'package:sass/src/exception.dart';
12-
import 'package:sass/src/executable/compile_stylesheet.dart';
11+
import 'package:sass/src/executable/concurrent.dart';
1312
import 'package:sass/src/executable/options.dart';
1413
import 'package:sass/src/executable/repl.dart';
1514
import 'package:sass/src/executable/watch.dart';
1615
import 'package:sass/src/import_cache.dart';
1716
import 'package:sass/src/io.dart';
18-
import 'package:sass/src/io.dart' as io;
1917
import 'package:sass/src/logger/deprecation_handling.dart';
2018
import 'package:sass/src/stylesheet_graph.dart';
21-
import 'package:sass/src/util/map.dart';
2219
import 'package:sass/src/utils.dart';
2320
import 'package:sass/src/embedded/executable.dart'
2421
// Never load the embedded protocol when compiling to JS.
2522
if (dart.library.js) 'package:sass/src/embedded/unavailable.dart'
2623
as embedded;
2724

2825
Future<void> main(List<String> args) async {
29-
var printedError = false;
30-
31-
// Prints [error] to stderr, along with a preceding newline if anything else
32-
// has been printed to stderr.
33-
//
34-
// If [trace] is passed, its terse representation is printed after the error.
35-
void printError(String error, StackTrace? stackTrace) {
36-
var buffer = StringBuffer();
37-
if (printedError) buffer.writeln();
38-
printedError = true;
39-
buffer.write(error);
40-
41-
if (stackTrace != null) {
42-
buffer.writeln();
43-
buffer.writeln();
44-
buffer.write(Trace.from(stackTrace).terse.toString().trimRight());
45-
}
46-
47-
io.printError(buffer);
48-
}
49-
5026
if (args case ['--embedded', ...var rest]) {
5127
embedded.main(rest);
5228
return;
@@ -84,37 +60,8 @@ Future<void> main(List<String> args) async {
8460
return;
8561
}
8662

87-
for (var (source, destination) in options.sourcesToDestinations.pairs) {
88-
try {
89-
await compileStylesheet(options, graph, source, destination,
90-
ifModified: options.update);
91-
} on SassException catch (error, stackTrace) {
92-
if (destination != null && !options.emitErrorCss) {
93-
_tryDelete(destination);
94-
}
95-
96-
printError(error.toString(color: options.color),
97-
options.trace ? getTrace(error) ?? stackTrace : null);
98-
99-
// Exit code 65 indicates invalid data per
100-
// https://www.freebsd.org/cgi/man.cgi?query=sysexits.
101-
//
102-
// We let exitCode 66 take precedence for deterministic behavior.
103-
if (exitCode != 66) exitCode = 65;
104-
if (options.stopOnError) return;
105-
} on FileSystemException catch (error, stackTrace) {
106-
var path = error.path;
107-
printError(
108-
path == null
109-
? error.message
110-
: "Error reading ${p.relative(path)}: ${error.message}.",
111-
options.trace ? getTrace(error) ?? stackTrace : null);
112-
113-
// Error 66 indicates no input.
114-
exitCode = 66;
115-
if (options.stopOnError) return;
116-
}
117-
}
63+
await compileStylesheets(options, graph, options.sourcesToDestinations,
64+
ifModified: options.update);
11865
} on UsageException catch (error) {
11966
print("${error.message}\n");
12067
print("Usage: sass <input.scss> [output.css]\n"
@@ -128,8 +75,11 @@ Future<void> main(List<String> args) async {
12875
if (options?.color ?? false) buffer.write('\u001b[0m');
12976
buffer.writeln();
13077
buffer.writeln(error);
131-
132-
printError(buffer.toString(), getTrace(error) ?? stackTrace);
78+
buffer.writeln();
79+
buffer.writeln();
80+
buffer.write(
81+
Trace.from(getTrace(error) ?? stackTrace).terse.toString().trimRight());
82+
printError(buffer);
13383
exitCode = 255;
13484
}
13585
}
@@ -154,14 +104,3 @@ Future<String> _loadVersion() async {
154104
.split(" ")
155105
.last;
156106
}
157-
158-
/// Delete [path] if it exists and do nothing otherwise.
159-
///
160-
/// This is a separate function to work around dart-lang/sdk#53082.
161-
void _tryDelete(String path) {
162-
try {
163-
deleteFile(path);
164-
} on FileSystemException {
165-
// If the file doesn't exist, that's fine.
166-
}
167-
}

lib/src/executable/compile_stylesheet.dart

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:convert';
66

77
import 'package:path/path.dart' as p;
88
import 'package:source_maps/source_maps.dart';
9+
import 'package:stack_trace/stack_trace.dart';
910

1011
import '../async_import_cache.dart';
1112
import '../compile.dart';
@@ -30,8 +31,42 @@ import 'options.dart';
3031
/// If [ifModified] is `true`, only recompiles if [source]'s modification time
3132
/// or that of a file it imports is more recent than [destination]'s
3233
/// modification time. Note that these modification times are cached by [graph].
33-
Future<void> compileStylesheet(ExecutableOptions options, StylesheetGraph graph,
34-
String? source, String? destination,
34+
///
35+
/// Returns `(exitCode, error, stackTrace)` when an error occurs.
36+
Future<(int, String, String?)?> compileStylesheet(ExecutableOptions options,
37+
StylesheetGraph graph, String? source, String? destination,
38+
{bool ifModified = false}) async {
39+
try {
40+
await _compileStylesheetWithoutErrorHandling(
41+
options, graph, source, destination,
42+
ifModified: ifModified);
43+
} on SassException catch (error, stackTrace) {
44+
if (destination != null && !options.emitErrorCss) {
45+
_tryDelete(destination);
46+
}
47+
var message = error.toString(color: options.color);
48+
49+
// Exit code 65 indicates invalid data per
50+
// https://www.freebsd.org/cgi/man.cgi?query=sysexits.
51+
return _getErrorWithStackTrace(
52+
65, message, options.trace ? getTrace(error) ?? stackTrace : null);
53+
} on FileSystemException catch (error, stackTrace) {
54+
var path = error.path;
55+
var message = path == null
56+
? error.message
57+
: "Error reading ${p.relative(path)}: ${error.message}.";
58+
59+
// Exit code 66 indicates no input.
60+
return _getErrorWithStackTrace(
61+
66, message, options.trace ? getTrace(error) ?? stackTrace : null);
62+
}
63+
return null;
64+
}
65+
66+
/// Like [compileStylesheet], but throws errors instead of handling them
67+
/// internally.
68+
Future<void> _compileStylesheetWithoutErrorHandling(ExecutableOptions options,
69+
StylesheetGraph graph, String? source, String? destination,
3570
{bool ifModified = false}) async {
3671
var importer = FilesystemImporter('.');
3772
if (ifModified) {
@@ -150,7 +185,7 @@ Future<void> compileStylesheet(ExecutableOptions options, StylesheetGraph graph,
150185
buffer.write('Compiled $sourceName to $destinationName.');
151186
if (options.color) buffer.write('\u001b[0m');
152187

153-
print(buffer);
188+
safePrint(buffer);
154189
}
155190

156191
/// Writes the source map given by [mapping] to disk (if necessary) according to
@@ -195,3 +230,26 @@ String _writeSourceMap(
195230
return (options.style == OutputStyle.compressed ? '' : '\n\n') +
196231
'/*# sourceMappingURL=$escapedUrl */';
197232
}
233+
234+
/// Delete [path] if it exists and do nothing otherwise.
235+
///
236+
/// This is a separate function to work around dart-lang/sdk#53082.
237+
void _tryDelete(String path) {
238+
try {
239+
deleteFile(path);
240+
} on FileSystemException {
241+
// If the file doesn't exist, that's fine.
242+
}
243+
}
244+
245+
/// Return a Record of `(exitCode, error, stackTrace)` for the given error.
246+
(int, String, String?) _getErrorWithStackTrace(
247+
int exitCode, String error, StackTrace? stackTrace) {
248+
return (
249+
exitCode,
250+
error,
251+
stackTrace != null
252+
? Trace.from(stackTrace).terse.toString().trimRight()
253+
: null
254+
);
255+
}

lib/src/executable/concurrent.dart

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2023 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import 'dart:math' as math;
6+
7+
import '../io.dart';
8+
import '../stylesheet_graph.dart';
9+
import '../util/map.dart';
10+
import 'compile_stylesheet.dart';
11+
import 'concurrent/vm.dart'
12+
// Never load the isolate library when compiling to JS.
13+
if (dart.library.js) 'concurrent/js.dart';
14+
import 'options.dart';
15+
16+
/// Compiles the stylesheets concurrently and returns whether all stylesheets are compiled
17+
/// successfully.
18+
Future<bool> compileStylesheets(ExecutableOptions options,
19+
StylesheetGraph graph, Map<String?, String?> sourcesToDestinations,
20+
{bool ifModified = false}) async {
21+
var errorsWithStackTraces = switch ([...sourcesToDestinations.pairs]) {
22+
// Concurrency does add some overhead, so avoid it in the common case of
23+
// compiling a single stylesheet.
24+
[(var source, var destination)] => [
25+
await compileStylesheet(options, graph, source, destination,
26+
ifModified: ifModified)
27+
],
28+
var pairs => await Future.wait([
29+
for (var (source, destination) in pairs)
30+
compileStylesheetConcurrently(options, graph, source, destination,
31+
ifModified: ifModified)
32+
], eagerError: options.stopOnError)
33+
};
34+
35+
var printedError = false;
36+
37+
// Print all errors in deterministic order.
38+
for (var errorWithStackTrace in errorsWithStackTraces) {
39+
if (errorWithStackTrace == null) continue;
40+
var (code, error, stackTrace) = errorWithStackTrace;
41+
42+
// We let the highest exitCode take precedence for deterministic behavior.
43+
exitCode = math.max(exitCode, code);
44+
45+
_printError(error, stackTrace, printedError);
46+
printedError = true;
47+
}
48+
49+
return !printedError;
50+
}
51+
52+
// Prints [error] to stderr, along with a preceding newline if anything else
53+
// has been printed to stderr.
54+
//
55+
// If [stackTrace] is passed, it is printed after the error.
56+
void _printError(String error, String? stackTrace, bool printedError) {
57+
var buffer = StringBuffer();
58+
if (printedError) buffer.writeln();
59+
buffer.write(error);
60+
if (stackTrace != null) {
61+
buffer.writeln();
62+
buffer.writeln();
63+
buffer.write(stackTrace);
64+
}
65+
printError(buffer);
66+
}

lib/src/executable/concurrent/js.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright 2023 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import '../compile_stylesheet.dart';
6+
7+
/// We don't currently support concurrent compilation in JS.
8+
///
9+
/// In the future, we could add support using web workers.
10+
final compileStylesheetConcurrently = compileStylesheet;

lib/src/executable/concurrent/vm.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2023 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import 'dart:isolate';
6+
7+
import 'package:term_glyph/term_glyph.dart' as term_glyph;
8+
9+
import '../options.dart';
10+
import '../../stylesheet_graph.dart';
11+
import '../compile_stylesheet.dart';
12+
13+
/// Compiles the stylesheet at [source] to [destination].
14+
///
15+
/// Runs in a new Dart Isolate, unless [source] is `null`.
16+
Future<(int, String, String?)?> compileStylesheetConcurrently(
17+
ExecutableOptions options,
18+
StylesheetGraph graph,
19+
String? source,
20+
String? destination,
21+
{bool ifModified = false}) {
22+
// Reading from stdin does not work properly in dart isolate.
23+
if (source == null) {
24+
return compileStylesheet(options, graph, source, destination,
25+
ifModified: ifModified);
26+
}
27+
28+
return Isolate.run(() {
29+
term_glyph.ascii = !options.unicode;
30+
return compileStylesheet(options, graph, source, destination,
31+
ifModified: ifModified);
32+
});
33+
}

0 commit comments

Comments
 (0)