blob: a52176785036bd8eed782e33d72cf9e619d8c16a [file] [log] [blame]
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:logging/logging.dart';
import 'analysis_servers.dart';
import 'common.dart';
import 'compiler.dart';
import 'project.dart';
import 'protos/dart_services.pb.dart' as proto;
import 'pub.dart';
import 'sdk.dart';
import 'server_cache.dart';
const Duration _standardExpiration = Duration(hours: 1);
final Logger log = Logger('common_server');
class BadRequest implements Exception {
String cause;
BadRequest(this.cause);
}
class CommonServerImpl {
final ServerCache _cache;
final Sdk _sdk;
late Compiler _compiler;
late AnalysisServersWrapper _analysisServers;
// Restarting and health status of the two Analysis Servers
bool get isRestarting => _analysisServers.isRestarting;
bool get isHealthy => _analysisServers.isHealthy;
CommonServerImpl(
this._cache,
this._sdk,
) {
hierarchicalLoggingEnabled = true;
log.level = Level.ALL;
}
Future<void> init() async {
log.info('Beginning CommonServer init().');
_analysisServers = AnalysisServersWrapper(_sdk.dartSdkPath);
_compiler = Compiler(_sdk);
await _compiler.warmup();
await _analysisServers.warmup();
}
Future<dynamic> shutdown() {
return Future.wait(<Future<dynamic>>[
_analysisServers.shutdown(),
_compiler.dispose(),
Future<dynamic>.sync(_cache.shutdown)
]).timeout(const Duration(minutes: 1));
}
Future<proto.AnalysisResults> analyze(proto.SourceRequest request) {
if (!request.hasSource()) {
throw BadRequest('Missing parameter: \'source\'');
}
return _analysisServers.analyze(request.source, devMode: _sdk.devMode);
}
Future<proto.CompileResponse> compile(proto.CompileRequest request) {
if (!request.hasSource()) {
throw BadRequest('Missing parameter: \'source\'');
}
return _compileDart2js({kMainDart: request.source},
returnSourceMap: request.returnSourceMap);
}
Future<proto.CompileDDCResponse> compileDDC(proto.CompileDDCRequest request) {
if (!request.hasSource()) {
throw BadRequest('Missing parameter: \'source\'');
}
return _compileDDC({kMainDart: request.source});
}
Future<proto.CompleteResponse> complete(proto.SourceRequest request) {
if (!request.hasSource()) {
throw BadRequest('Missing parameter: \'source\'');
}
if (!request.hasOffset()) {
throw BadRequest('Missing parameter: \'offset\'');
}
return _analysisServers.complete(request.source, request.offset,
devMode: _sdk.devMode);
}
Future<proto.FixesResponse> fixes(proto.SourceRequest request) {
if (!request.hasSource()) {
throw BadRequest('Missing parameter: \'source\'');
}
if (!request.hasOffset()) {
throw BadRequest('Missing parameter: \'offset\'');
}
return _analysisServers.getFixes(request.source, request.offset,
devMode: _sdk.devMode);
}
Future<proto.AssistsResponse> assists(proto.SourceRequest request) {
if (!request.hasSource()) {
throw BadRequest('Missing parameter: \'source\'');
}
if (!request.hasOffset()) {
throw BadRequest('Missing parameter: \'offset\'');
}
return _analysisServers.getAssists(request.source, request.offset,
devMode: _sdk.devMode);
}
Future<proto.FormatResponse> format(proto.SourceRequest request) {
if (!request.hasSource()) {
throw BadRequest('Missing parameter: \'source\'');
}
return _analysisServers.format(request.source, request.offset,
devMode: _sdk.devMode);
}
Future<proto.DocumentResponse> document(proto.SourceRequest request) async {
if (!request.hasSource()) {
throw BadRequest('Missing parameter: \'source\'');
}
if (!request.hasOffset()) {
throw BadRequest('Missing parameter: \'offset\'');
}
return proto.DocumentResponse()
..info.addAll(await _analysisServers
.dartdoc(request.source, request.offset, devMode: _sdk.devMode));
}
// Beginning of multi files map entry points:
Future<proto.AnalysisResults> analyzeFiles(proto.SourceFilesRequest request) {
if (request.files.isEmpty) {
throw BadRequest('Missing parameter: \'files\'');
}
return _analysisServers.analyzeFiles(
request.files, request.activeSourceName,
devMode: _sdk.devMode);
}
Future<proto.CompileResponse> compileFiles(
proto.CompileFilesRequest request) {
if (request.files.isEmpty) {
throw BadRequest('Missing parameter: \'files\'');
}
return _compileDart2js(request.files,
returnSourceMap: request.returnSourceMap);
}
Future<proto.CompileDDCResponse> compileFilesDDC(
proto.CompileFilesDDCRequest request) {
if (request.files.isEmpty) {
throw BadRequest('Missing parameter: \'files\'');
}
return _compileDDC(request.files);
}
Future<proto.CompleteResponse> completeFiles(
proto.SourceFilesRequest request) {
if (request.files.isEmpty) {
throw BadRequest('Missing parameter: \'files\'');
}
if (!request.hasActiveSourceName()) {
throw BadRequest('Missing parameter: \'activeSourceName\'');
}
if (!request.hasOffset()) {
throw BadRequest('Missing parameter: \'offset\'');
}
return _analysisServers.completeFiles(
request.files, request.activeSourceName, request.offset,
devMode: _sdk.devMode);
}
Future<proto.FixesResponse> fixesFiles(proto.SourceFilesRequest request) {
if (request.files.isEmpty) {
throw BadRequest('Missing parameter: \'files\'');
}
if (!request.hasActiveSourceName()) {
throw BadRequest('Missing parameter: \'activeSourceName\'');
}
if (!request.hasOffset()) {
throw BadRequest('Missing parameter: \'offset\'');
}
return _analysisServers.getFixesMulti(
request.files, request.activeSourceName, request.offset,
devMode: _sdk.devMode);
}
Future<proto.AssistsResponse> assistsFiles(proto.SourceFilesRequest request) {
if (request.files.isEmpty) {
throw BadRequest('Missing parameter: \'files\'');
}
if (!request.hasActiveSourceName()) {
throw BadRequest('Missing parameter: \'activeSourceName\'');
}
if (!request.hasOffset()) {
throw BadRequest('Missing parameter: \'offset\'');
}
return _analysisServers.getAssistsMulti(
request.files, request.activeSourceName, request.offset,
devMode: _sdk.devMode);
}
Future<proto.DocumentResponse> documentFiles(
proto.SourceFilesRequest request) async {
if (request.files.isEmpty) {
throw BadRequest('Missing parameter: \'files\'');
}
if (!request.hasActiveSourceName()) {
throw BadRequest('Missing parameter: \'activeSourceName\'');
}
if (!request.hasOffset()) {
throw BadRequest('Missing parameter: \'offset\'');
}
return proto.DocumentResponse()
..info.addAll(await _analysisServers.dartdocMulti(
request.files, request.activeSourceName, request.offset,
devMode: _sdk.devMode));
}
// End of files map entry points.
Future<proto.VersionResponse> version(proto.VersionRequest _) {
final packageVersions = getPackageVersions();
final packageInfos = [
for (final packageName in packageVersions.keys)
proto.PackageInfo()
..name = packageName
..version = packageVersions[packageName]!
..supported = isSupportedPackage(packageName, devMode: _sdk.devMode),
];
return Future.value(
proto.VersionResponse()
..sdkVersion = _sdk.version
..sdkVersionFull = _sdk.versionFull
..runtimeVersion = vmVersion
..flutterDartVersion = _sdk.version
..flutterDartVersionFull = _sdk.versionFull
..flutterVersion = _sdk.flutterVersion
..flutterEngineSha = _sdk.engineVersion
..packageVersions.addAll(packageVersions)
..packageInfo.addAll(packageInfos)
..experiment.addAll(_sdk.experiments),
);
}
Future<proto.CompileResponse> _compileDart2js(
Map<String, String> sources, {
bool returnSourceMap = false,
}) async {
try {
final sourceHash = _hashSources(sources);
final memCacheKey = '%%COMPILE:v0'
':returnSourceMap:$returnSourceMap:source:$sourceHash';
final result = await _checkCache(memCacheKey);
if (result != null) {
log.info('CACHE: Cache hit for compileDart2js');
final resultObj = json.decode(result) as Map<String, dynamic>;
final response = proto.CompileResponse()
..result = resultObj['compiledJS'] as String;
if (resultObj['sourceMap'] != null) {
response.sourceMap = resultObj['sourceMap'] as String;
}
return response;
}
log.info('CACHE: MISS for compileDart2js');
final watch = Stopwatch()..start();
final results = await _compiler.compileFiles(sources,
returnSourceMap: returnSourceMap);
if (results.hasOutput) {
final lineCount = countLines(sources);
final outputSize = (results.compiledJS?.length ?? 0 / 1024).ceil();
final ms = watch.elapsedMilliseconds;
log.info('PERF: Compiled $lineCount lines of Dart into '
'${outputSize}kb of JavaScript in ${ms}ms using dart2js.');
final sourceMap = returnSourceMap ? results.sourceMap : null;
final cachedResult = const JsonEncoder().convert(<String, String?>{
'compiledJS': results.compiledJS,
'sourceMap': sourceMap,
});
// Don't block on cache set.
unawaited(_setCache(memCacheKey, cachedResult));
final compileResponse = proto.CompileResponse();
compileResponse.result = results.compiledJS ?? '';
if (sourceMap != null) {
compileResponse.sourceMap = sourceMap;
}
return compileResponse;
} else {
final problems = results.problems;
final errors = problems.map(_printCompileProblem).join('\n');
throw BadRequest(errors);
}
} catch (e, st) {
if (e is! BadRequest) {
log.severe('Error during compile (dart2js) on "$sources"', e, st);
}
rethrow;
}
}
Future<proto.CompileDDCResponse> _compileDDC(
Map<String, String> sources) async {
try {
final sourceHash = _hashSources(sources);
final memCacheKey = '%%COMPILE_DDC:v0:source:$sourceHash';
final result = await _checkCache(memCacheKey);
if (result != null) {
log.info('CACHE: Cache hit for compileDDC');
final resultObj = json.decode(result) as Map<String, dynamic>;
return proto.CompileDDCResponse()
..result = resultObj['compiledJS'] as String
..modulesBaseUrl = resultObj['modulesBaseUrl'] as String;
}
log.info('CACHE: MISS for compileDDC');
final watch = Stopwatch()..start();
final results = await _compiler.compileFilesDDC(sources);
if (results.hasOutput) {
final lineCount = countLines(sources);
final outputSize = (results.compiledJS?.length ?? 0 / 1024).ceil();
final ms = watch.elapsedMilliseconds;
log.info('PERF: Compiled $lineCount lines of Dart into '
'${outputSize}kb of JavaScript in ${ms}ms using DDC.');
final cachedResult = const JsonEncoder().convert(<String, String>{
'compiledJS': results.compiledJS ?? '',
'modulesBaseUrl': results.modulesBaseUrl ?? '',
});
// Don't block on cache set.
unawaited(_setCache(memCacheKey, cachedResult));
return proto.CompileDDCResponse()
..result = results.compiledJS ?? ''
..modulesBaseUrl = results.modulesBaseUrl ?? '';
} else {
final problems = results.problems;
final errors = problems.map(_printCompileProblem).join('\n');
throw BadRequest(errors);
}
} catch (e, st) {
if (e is! BadRequest) {
log.severe('Error during compile (DDC) on "$sources"', e, st);
}
rethrow;
}
}
Future<String?> _checkCache(String query) => _cache.get(query);
Future<void> _setCache(String query, String result) =>
_cache.set(query, result, expiration: _standardExpiration);
}
String _printCompileProblem(CompilationProblem problem) => problem.message;
String _hashSources(Map<String, String> sources) {
if (sources.length == 1) {
// Special case optimized for single source file (and to work as before).
return sha1.convert(sources.values.first.codeUnits).toString();
} else {
// Use chunk hashing method for >1 source files.
final hashoutput = AccumulatorSink<Digest>();
final sha1Chunker = sha1.startChunkedConversion(hashoutput);
sources.forEach((_, filecontents) {
sha1Chunker.add(filecontents.codeUnits);
});
sha1Chunker.close();
return hashoutput.events.single.toString();
}
}