Files
Danny Tuppeny 4afd2e10be [analysis_server] Normalize URIs before normalizing paths in the session log
The session log normalizer replaces known paths/URIs in JSON but doesn't take into account different URI encoding between the client and the server. For example VS Code will encode ampersands whereas Dart does not:

```
file:///c:/uri&encoding&quirks
file:///c:/uri%26encoding%26quirks
```

This means not all file URIs are correctly normalized.

Adding additional groups for each potentially-encoded characters make the regex many times slower (the benchmark test here goes from around 25ms to over 1s per iteration), so instead this change has the normalizer accept the original JSON map and uses jsonEncode()s `toEncodable` option to normalize any URIs (by converting them to their file paths and then encoding using Dart's Uri class) so they will always be consistent before the replacement.

(I tried doing the replacement also in `toEncodable`, but invoking the regex many times also slowed things down a lot).

There is a small time increase (2-3ms) for a payload of 2MB. The "before" times quoted here are slightly higher than previously quoted, but that's because `jsonEncode()` was previously done inside `SessionLoggerFileSink` (and therefore excluded from the timings before), but is now done inside the normalizer to allow normalizing the URI escaping.

Replacing 250 paths in payload of 2097152 bytes
Iteration #1, First: 57ms, Rest: 40ms
Iteration #2, First: 49ms, Rest: 41ms
Iteration #3, First: 45ms, Rest: 41ms
Iteration #4, First: 47ms, Rest: 40ms
Iteration #5, First: 40ms, Rest: 40ms

Replacing 250 paths in payload of 2097152 bytes
Iteration #1, First: 59ms, Rest: 43ms
Iteration #2, First: 53ms, Rest: 44ms
Iteration #3, First: 52ms, Rest: 44ms
Iteration #4, First: 49ms, Rest: 43ms
Iteration #5, First: 49ms, Rest: 43ms

Fixes https://github.com/dart-lang/sdk/issues/63331

Change-Id: Ice2dc7ceceaa6c08e2ff634d7564efe9f0f7de44
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/502940
Reviewed-by: Keerti Parthasarathy <keertip@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Keerti Parthasarathy <keertip@google.com>
2026-05-13 14:02:03 -07:00

150 lines
4.8 KiB
Dart

// Copyright (c) 2025, 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:io' show Directory, exit;
import 'package:analysis_server/src/session_logger/log_normalizer.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:args/args.dart';
import 'package:cli_util/cli_util.dart';
import 'package:package_config/package_config.dart';
import '../performance/project_generator/project_generator.dart'
show ContextRoot, getContextRoots;
import 'log.dart';
Future<void> main(List<String> args) async {
var parsed = argParser.parse(args);
if (parsed.flag('help')) {
print(argParser.usage);
return;
}
var resourceProvider = PhysicalResourceProvider.INSTANCE;
var inputFile = resourceProvider.getFile(
Uri.base.resolve(parsed.option('input')!).toFilePath(),
);
if (!inputFile.exists) {
print('Input file ${inputFile.path} does not exist');
exit(1);
}
var outputFile = resourceProvider.getFile(
Uri.base.resolve(parsed.option('output')!).toFilePath(),
);
var rootDirPath = parsed.option('root-dir');
if (rootDirPath == null) {
print('Root directory not specified');
exit(1);
}
var rootDir = resourceProvider.getFolder(rootDirPath);
if (!rootDir.exists) {
print('Root directory $rootDirPath does not exist');
exit(1);
}
List<ContextRoot> contextRoots;
var packageConfigPath = parsed.option('package-config');
if (packageConfigPath != null) {
var packageConfigFile = resourceProvider.getFile(packageConfigPath);
if (!packageConfigFile.exists) {
print('Package config file $packageConfigPath does not exist');
exit(1);
}
var packageConfig = PackageConfig.parseBytes(
packageConfigFile.readAsBytesSync(),
packageConfigFile.toUri(),
);
contextRoots = [ContextRoot(Directory(rootDirPath), packageConfig)];
} else {
contextRoots = await getContextRoots(rootDirPath);
}
print('normalizing log at ${inputFile.path}');
var normalized = normalizeLog(inputFile, contextRoots);
outputFile.writeAsStringSync(normalized);
print('wrote normalized log to ${outputFile.path}');
var absFileMatches = normalized.allMatches('"file:///');
if (absFileMatches.isNotEmpty) {
print('found ${absFileMatches.length} absolute file paths remaining:');
}
for (var match in absFileMatches.take(5)) {
print('- ${match[0]}');
}
}
final argParser = ArgParser()
..addOption(
'input',
abbr: 'i',
help: 'The path to the input log to be normalized',
mandatory: true,
)
..addOption(
'output',
abbr: 'o',
help: 'The path output the normalized log to',
mandatory: true,
)
..addOption(
'root-dir',
abbr: 'r',
help: 'The path to the root directory for normalizing package paths',
mandatory: true,
)
..addOption(
'package-config',
abbr: 'p',
help:
'The path to the package config file, if specified, will be used '
'instead of inferring it from the workspace directories.',
)
..addFlag('help', abbr: 'h', help: 'Prints the usage text');
/// Reads an [input] log file, and attempts to normalize it so that it can work
/// across multiple environments.
///
/// Specifically, this:
/// - Replaces all workspace folder paths with `{{workspaceFolder-[i]}}`
/// placeholders.
/// - Replaces the Dart SDK root with `{{dartSdkRoot}}`.
/// - Replaces all package roots with `{{package-root:[package-name]}}`.
///
/// Returns the new file contents after normalization.
//
// TODO(somebody): Support legacy protocol.
//
// TODO(somebody): Don't take a package config, instead infer them from the
// workspace directories.
String normalizeLog(File input, List<ContextRoot> contextRoots) {
var normalizer = LogNormalizer();
var content = input.readAsStringSync();
var original = Log.fromString(content);
var initializeMessage = original.entries.firstWhere(
(log) => log.isMessage && log.message.isInitializeRequest,
);
// Add all the workspace + root paths from the initialize message.
normalizer.addLspWorkspaceReplacements(initializeMessage.message);
// Additionally add SDKs and packages.
normalizer.addReplacementsForPath(sdkPath, 'dartSdkRoot');
// TODO(somebody): replace {{flutterSdkRoot}} with the flutter SDK path
for (var i = 0; i < contextRoots.length; i++) {
for (var package in contextRoots[i].packageConfig.packages) {
normalizer.addReplacementsForUri(
package.root,
'context-$i:package-root:${package.name}',
);
}
}
// Normalize each entry in the log.
return '${original.entries.map((entry) => normalizer.normalize(entry.map)).join('\n')}\n';
}