Skip to content

Commit

Permalink
perf(mason): compile hooks (#613)
Browse files Browse the repository at this point in the history
  • Loading branch information
felangel committed Nov 12, 2022
1 parent 4e1ac92 commit 25336ae
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 115 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/mason_cli.yaml
Expand Up @@ -93,4 +93,4 @@ jobs:
dart pub global activate pana
- name: Verify Pub Score
run: ../../tool/verify_pub_score.sh
run: ../../tool/verify_pub_score.sh 100
23 changes: 23 additions & 0 deletions packages/mason/lib/src/generator.dart
Expand Up @@ -640,3 +640,26 @@ extension on String {
}
}
}

extension on HookFile {
String get fileHash => sha1.convert(content).toString();

Directory get cacheDirectory {
return Directory(
p.join(
Directory(p.join(Directory.systemTemp.path, '.mason')).path,
sha1.convert(utf8.encode(File(path).parent.absolute.path)).toString(),
),
);
}

Directory get buildDirectory {
return Directory(
p.join(cacheDirectory.path, 'build', p.basenameWithoutExtension(path)),
);
}

File get module {
return File(p.join(buildDirectory.path, '.$fileHash.dill'));
}
}
191 changes: 113 additions & 78 deletions packages/mason/lib/src/hooks.dart
Expand Up @@ -45,15 +45,15 @@ Ensure the hook contains a 'run' method:
);
}

/// {@template hook_run_exception}
/// Thrown when an error occurs when trying to run hook.
/// {@template hook_compile_exception}
/// Thrown when an error occurs when trying to compile a hook.
/// {@endtemplate}
class HookRunException extends MasonException {
/// {@macro hook_run_exception}
HookRunException(String path, String error)
class HookCompileException extends MasonException {
/// {@macro hook_compile_exception}
HookCompileException(String path, String error)
: super(
'''
Unable to execute hook: $path.
Unable to compile hook: $path.
Error: $error''',
);
}
Expand Down Expand Up @@ -202,6 +202,7 @@ class GeneratorHooks {
Map<String, dynamic> vars = const <String, dynamic>{},
String? workingDirectory,
void Function(Map<String, dynamic> vars)? onVarsChanged,
Logger? logger,
}) async {
final preGenHook = this.preGenHook;
if (preGenHook != null && pubspec != null) {
Expand All @@ -210,6 +211,7 @@ class GeneratorHooks {
vars: vars,
workingDirectory: workingDirectory,
onVarsChanged: onVarsChanged,
logger: logger,
);
}
}
Expand All @@ -220,6 +222,7 @@ class GeneratorHooks {
Map<String, dynamic> vars = const <String, dynamic>{},
String? workingDirectory,
void Function(Map<String, dynamic> vars)? onVarsChanged,
Logger? logger,
}) async {
final postGenHook = this.postGenHook;
if (postGenHook != null && pubspec != null) {
Expand All @@ -228,59 +231,49 @@ class GeneratorHooks {
vars: vars,
workingDirectory: workingDirectory,
onVarsChanged: onVarsChanged,
logger: logger,
);
}
}

/// Runs the provided [hook] with the specified [vars].
/// An optional [workingDirectory] can also be specified.
Future<void> _runHook({
required HookFile hook,
Map<String, dynamic> vars = const <String, dynamic>{},
void Function(Map<String, dynamic> vars)? onVarsChanged,
String? workingDirectory,
}) async {
final pubspec = this.pubspec;
final subscriptions = <StreamSubscription>[];
final messagePort = ReceivePort();
final errorPort = ReceivePort();
final exitPort = ReceivePort();
/// Compile all hooks into modules for faster execution.
/// Hooks are compiled lazily by default but calling [compile]
/// can be used to compile hooks ahead of time.
Future<void> compile({Logger? logger}) async {
await _installDependencies();

dynamic hookError;
subscriptions.add(errorPort.listen((dynamic error) => hookError = error));
if (preGenHook != null && !preGenHook!.module.existsSync()) {
await _compile(hook: preGenHook!, logger: logger);
}

if (onVarsChanged != null) {
subscriptions.add(
messagePort.listen((dynamic message) {
if (message is String) {
onVarsChanged(
json.decode(message) as Map<String, dynamic>,
);
}
}),
);
if (postGenHook != null && !postGenHook!.module.existsSync()) {
await _compile(hook: postGenHook!, logger: logger);
}
}

Future<void> _dartPubGet({required String workingDirectory}) async {
final result = await Process.run(
'dart',
['pub', 'get'],
workingDirectory: workingDirectory,
runInShell: true,
Future<void> _dartPubGet({required String workingDirectory}) async {
final result = await Process.run(
'dart',
['pub', 'get'],
workingDirectory: workingDirectory,
runInShell: true,
);
if (result.exitCode != ExitCode.success.code) {
throw HookDependencyInstallFailure(
workingDirectory,
'${result.stderr}',
);
if (result.exitCode != ExitCode.success.code) {
throw HookDependencyInstallFailure(hook.path, '${result.stderr}');
}
}
}

Future<void> _installDependencies() async {
final hook = preGenHook ?? postGenHook;
if (hook == null) return;

final hookCacheDir = hook.cacheDirectory;
final pubspec = this.pubspec;

var dependenciesInstalled = false;
Directory? hookCacheDir;
Uri? packageConfigUri;
if (pubspec != null) {
final directoryHash = sha1.convert(pubspec).toString();
hookCacheDir = Directory(
p.join(Directory.systemTemp.path, '.mason', directoryHash),
);
final packageConfigFile = File(
p.join(hookCacheDir.path, '.dart_tool', 'package_config.json'),
);
Expand All @@ -291,11 +284,18 @@ class GeneratorHooks {
p.join(hookCacheDir.path, 'pubspec.yaml'),
).writeAsBytes(pubspec);
await _dartPubGet(workingDirectory: hookCacheDir.path);
dependenciesInstalled = true;
}

packageConfigUri = packageConfigFile.uri;
}
}

Future<void> _compile({required HookFile hook, Logger? logger}) async {
final hookCacheDir = hook.cacheDirectory;
final hookBuildDir = hook.buildDirectory;
final hookHash = hook.fileHash;

try {
await hookBuildDir.delete(recursive: true);
} catch (_) {}

Uri? uri;
try {
Expand All @@ -307,6 +307,67 @@ class GeneratorHooks {

if (uri == null) throw HookMissingRunException(hook.path);

final hookFile = File(p.join(hookBuildDir.path, '.$hookHash.dart'))
..createSync(recursive: true)
..writeAsBytesSync(uri.data!.contentAsBytes());

final progress = logger?.progress('Compiling ${p.basename(hook.path)}');
final result = await Process.run(
'dart',
['compile', 'kernel', hookFile.path],
workingDirectory: hookCacheDir.path,
runInShell: true,
);

if (result.exitCode != ExitCode.success.code) {
final error = result.stderr.toString();
progress?.fail(error);
throw HookCompileException(hook.path, error);
}

progress?.complete('Compiled ${p.basename(hook.path)}');
}

/// Runs the provided [hook] with the specified [vars].
/// An optional [workingDirectory] can also be specified.
Future<void> _runHook({
required HookFile hook,
Map<String, dynamic> vars = const <String, dynamic>{},
void Function(Map<String, dynamic> vars)? onVarsChanged,
String? workingDirectory,
Logger? logger,
}) async {
final subscriptions = <StreamSubscription>[];
final messagePort = ReceivePort();
final errorPort = ReceivePort();
final exitPort = ReceivePort();

dynamic hookError;
subscriptions.add(errorPort.listen((dynamic error) => hookError = error));

if (onVarsChanged != null) {
subscriptions.add(
messagePort.listen((dynamic message) {
if (message is String) {
onVarsChanged(
json.decode(message) as Map<String, dynamic>,
);
}
}),
);
}

await _installDependencies();
final hookCacheDir = hook.cacheDirectory;
final packageConfigUri = File(
p.join(hookCacheDir.path, '.dart_tool', 'package_config.json'),
).uri;
final module = hook.module;

if (!module.existsSync()) {
await _compile(hook: hook, logger: logger);
}

Future<Isolate> spawnIsolate(Uri uri) {
return Isolate.spawnUri(
uri,
Expand All @@ -318,35 +379,9 @@ class GeneratorHooks {
}

final cwd = Directory.current;
Isolate? isolate;
try {
if (workingDirectory != null) Directory.current = workingDirectory;
isolate = await spawnIsolate(uri);
} on IsolateSpawnException catch (error) {
Never throwHookRunException(IsolateSpawnException error) {
Directory.current = cwd;
final msg = error.message;
final content =
msg.contains('Error: ') ? msg.split('Error: ').last : msg;
throw HookRunException(hook.path, content.trim());
}

final shouldRetry = !dependenciesInstalled && hookCacheDir != null;
if (workingDirectory != null) Directory.current = workingDirectory;

if (!shouldRetry) throwHookRunException(error);

// Failure to spawn the isolate could be due to changes in the pub cache.
// We attempt to reinstall hook dependencies.
await _dartPubGet(workingDirectory: hookCacheDir.path);

// Retry spawning the isolate if the hook dependencies
// have been successfully reinstalled.
try {
isolate = await spawnIsolate(uri);
} on IsolateSpawnException catch (error) {
throwHookRunException(error);
}
}
final isolate = await spawnIsolate(module.uri);

isolate
..addErrorListener(errorPort.sendPort)
Expand Down
3 changes: 3 additions & 0 deletions packages/mason/test/fixtures/basic/hooks/post_gen.dart
@@ -0,0 +1,3 @@
import 'package:mason/mason.dart';

void run(HookContext context) {}

0 comments on commit 25336ae

Please sign in to comment.