Skip to content

Commit 48bd8d5

Browse files
scheglovCommit Queue
authored andcommitted
Fine. Optimize diagnostics reuse with an API signature shortcut
Previously, reusing cached diagnostics always required validating the fine-grained `RequirementsManifest`. While effective (save `6000 ms` by not analyzing files when API changed), this process incurred significant overhead even for changes that did not affect a library's public API, such as modifying a method body. In these scenarios, the check could nearly double the total analysis time (add `110 ms`). This change introduces a two-tiered validation system to improve performance and refactors diagnostics handling: - A new `LibraryDiagnosticsBundle` class is extracted to better encapsulate a library's diagnostics and its requirements. - A coarse-grained API signature check now acts as a fast-path shortcut. If a library's API signature is unchanged, its cached diagnostics are reused immediately, bypassing the more expensive manifest validation. The full manifest is only checked if this shortcut fails. Additionally, the key for the diagnostics bundle is now cached on the `LibraryFileKind` object itself. This prevents recomputing the key's hash upon every access, further improving performance, about `16 ms` for `1600` files in the analyzer itself. With these changes the overhead of producing diagnostics is under `1 ms`. There is still some overhead of about `30 ms` in re-loading library summaries, I will address this separately. Change-Id: Iec884b8e0311e20965c7cde6d8cbae42b513a107 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/447520 Reviewed-by: Johnni Winther <[email protected]> Commit-Queue: Konstantin Shcheglov <[email protected]>
1 parent aac709d commit 48bd8d5

File tree

6 files changed

+1135
-678
lines changed

6 files changed

+1135
-678
lines changed

pkg/analyzer/lib/src/dart/analysis/driver.dart

Lines changed: 136 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import 'package:analyzer/src/dart/analysis/file_tracker.dart';
2727
import 'package:analyzer/src/dart/analysis/index.dart';
2828
import 'package:analyzer/src/dart/analysis/library_analyzer.dart';
2929
import 'package:analyzer/src/dart/analysis/library_context.dart';
30+
import 'package:analyzer/src/dart/analysis/library_diagnostics.dart';
3031
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
3132
import 'package:analyzer/src/dart/analysis/results.dart';
3233
import 'package:analyzer/src/dart/analysis/search.dart';
@@ -51,8 +52,6 @@ import 'package:analyzer/src/summary/idl.dart';
5152
import 'package:analyzer/src/summary/package_bundle_reader.dart';
5253
import 'package:analyzer/src/summary2/ast_binary_flags.dart';
5354
import 'package:analyzer/src/summary2/bundle_writer.dart';
54-
import 'package:analyzer/src/summary2/data_reader.dart';
55-
import 'package:analyzer/src/summary2/data_writer.dart';
5655
import 'package:analyzer/src/summary2/package_bundle_format.dart';
5756
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
5857
import 'package:analyzer/src/util/performance/operation_performance.dart';
@@ -108,7 +107,7 @@ testFineAfterLibraryAnalyzerHook;
108107
// TODO(scheglov): Clean up the list of implicitly analyzed files.
109108
class AnalysisDriver {
110109
/// The version of data format, should be incremented on every format change.
111-
static const int DATA_VERSION = 525;
110+
static const int DATA_VERSION = 526;
112111

113112
/// The number of exception contexts allowed to write. Once this field is
114113
/// zero, we stop writing any new exception contexts in this process.
@@ -282,6 +281,22 @@ class AnalysisDriver {
282281
/// Whether timing data should be gathered during lint rule execution.
283282
final bool _enableLintRuleTiming;
284283

284+
/// The last known diagnostics bundle for a library.
285+
///
286+
/// It might be still valid, but might be not.
287+
/// We check its requirements to decide.
288+
///
289+
/// We keep it in memory for performance. There are cases when we edit a
290+
/// file with many transitive clients, and we don't want to deserialize
291+
/// requirements every time. They are almost always satisfied, so we don't
292+
/// analyze libraries, and so deserialization cost would dominate.
293+
///
294+
/// There might be a way in the future to collapse these requirements to
295+
/// reduce heap usage.
296+
///
297+
/// Key: [_getLibraryDiagnosticsKey]
298+
final Map<String, LibraryDiagnosticsBundle> _libraryDiagnosticsCache = {};
299+
285300
/// Create a new instance of [AnalysisDriver].
286301
///
287302
/// The given [SourceFactory] is cloned to ensure that it does not contain a
@@ -1375,7 +1390,7 @@ class AnalysisDriver {
13751390
}
13761391

13771392
var isLibraryWithPriorityFile = _isLibraryWithPriorityFile(library);
1378-
var fileResultBytesMap = <Uri, Uint8List>{};
1393+
var serializedFileResults = <Uri, Uint8List>{};
13791394

13801395
var resolvedUnits = <ResolvedUnitResultImpl>[];
13811396
for (var unitResult in results) {
@@ -1420,7 +1435,7 @@ class AnalysisDriver {
14201435
index: index,
14211436
).toBuffer();
14221437
_byteStore.putGet(unitKey, unitBytes);
1423-
fileResultBytesMap[unitFile.uri] = unitBytes;
1438+
serializedFileResults[unitFile.uri] = unitBytes;
14241439
}
14251440

14261441
_fileTracker.fileWasAnalyzed(unitFile.path);
@@ -1437,28 +1452,19 @@ class AnalysisDriver {
14371452
if (withFineDependencies && requirements != null) {
14381453
requirements.removeReqForLibs({library.file.uri});
14391454

1440-
performance.run('writeResolvedLibrary', (performance) {
1441-
var mapSink = BufferedSink();
1442-
mapSink.writeMap(
1443-
fileResultBytesMap,
1444-
writeKey: (uri) => mapSink.writeUri(uri),
1445-
writeValue: (bytes) => mapSink.writeUint8List(bytes),
1446-
);
1447-
var mapBytes = mapSink.takeBytes();
1448-
1449-
library.lastResolutionResult = LibraryResolutionResult(
1455+
performance.run('writeLibraryDiagnostics', (performance) {
1456+
var bundle = LibraryDiagnosticsBundle(
14501457
requirements: requirements!,
1451-
bytes: mapBytes,
1458+
serializedFileResults: serializedFileResults,
14521459
);
1460+
bundle.addValidated(library.libraryCycle.apiSignature);
14531461

1454-
var sink = BufferedSink();
1455-
requirements.write(sink);
1456-
sink.writeUint8List(mapBytes);
1457-
var allBytes = sink.takeBytes();
1458-
performance.getDataInt('bytes').add(allBytes.length);
1462+
var key = _getLibraryDiagnosticsKey(library);
1463+
_libraryDiagnosticsCache[key] = bundle;
14591464

1460-
var key = _getResolvedLibraryKey(library);
1461-
_byteStore.putGet(key, allBytes);
1465+
var bytes = bundle.toBytes();
1466+
performance.getDataInt('bytes').add(bytes.length);
1467+
_byteStore.putGet(key, bytes);
14621468
});
14631469

14641470
_scheduler.eventsController.add(
@@ -1761,49 +1767,13 @@ class AnalysisDriver {
17611767
});
17621768

17631769
if (withFineDependencies) {
1764-
var fileResultBytesMap = performance.run('errors(isSatisfied)', (
1765-
performance,
1766-
) {
1767-
var reqAndUnitBytes = performance.run('getBytes', (_) {
1768-
var key = _getResolvedLibraryKey(library);
1769-
return _byteStore.get(key);
1770-
});
1771-
1772-
if (reqAndUnitBytes != null) {
1773-
var elementFactory = libraryContext.elementFactory;
1774-
1775-
var reader = SummaryDataReader(reqAndUnitBytes);
1776-
var requirements = performance.run('readRequirements', (_) {
1777-
return RequirementsManifest.read(reader);
1778-
});
1779-
1780-
var failure = requirements.isSatisfied(
1781-
elementFactory: elementFactory,
1782-
libraryManifests: elementFactory.libraryManifests,
1783-
);
1784-
if (failure == null) {
1785-
var mapBytes = reader.readUint8List();
1786-
library.lastResolutionResult = LibraryResolutionResult(
1787-
requirements: requirements,
1788-
bytes: mapBytes,
1789-
);
1790-
1791-
var reader2 = SummaryDataReader(mapBytes);
1792-
return reader2.readMap(
1793-
readKey: () => reader2.readUri(),
1794-
readValue: () => reader2.readUint8List(),
1795-
);
1796-
} else {
1797-
scheduler.eventsController.add(
1798-
events.GetErrorsCannotReuse(library: library, failure: failure),
1799-
);
1800-
}
1801-
}
1802-
return null;
1803-
});
1770+
var bundle = _getLibraryDiagnosticsBundle(
1771+
library: library,
1772+
performance: performance,
1773+
);
18041774

1805-
if (fileResultBytesMap != null) {
1806-
var bytes = fileResultBytesMap[file.uri]!;
1775+
if (bundle != null) {
1776+
var bytes = bundle.serializedFileResults[file.uri]!;
18071777
var result = _createErrorsResultFromBytes(file, library, bytes);
18081778
_errorsRequestedFiles.completeAll(path, result);
18091779
return;
@@ -1876,35 +1846,77 @@ class AnalysisDriver {
18761846
_analyzeFile(path);
18771847
}
18781848

1879-
/// Completes the [getResolvedLibrary] request.
1880-
void _getResolvedLibrary(String path) {
1881-
var file = _fsState.getFileForPath(path);
1882-
var kind = file.kind;
1883-
switch (kind) {
1884-
case LibraryFileKind():
1885-
break;
1886-
case PartFileKind():
1887-
_requestedLibraries.completeAll(path, NotLibraryButPartResult());
1888-
return;
1889-
default:
1890-
throw UnimplementedError('(${kind.runtimeType}) $kind');
1891-
}
1849+
/// Returns the [LibraryDiagnosticsBundle] for the given [library].
1850+
///
1851+
/// The bundle is returned if it is present in the cache, and its
1852+
/// requirements are satisfied. The cache here is in-memory, or the
1853+
/// byte store.
1854+
///
1855+
/// Returns `null` if the bundle is not available, or its requirements
1856+
/// are not satisfied. In this case the client should re-analyze the
1857+
/// library to produce a new bundle.
1858+
LibraryDiagnosticsBundle? _getLibraryDiagnosticsBundle({
1859+
required LibraryFileKind library,
1860+
required OperationPerformanceImpl performance,
1861+
}) {
1862+
return performance.run('getLibraryDiagnosticsBundle', (performance) {
1863+
var key = _getLibraryDiagnosticsKey(library);
1864+
var bundle = _libraryDiagnosticsCache[key];
1865+
1866+
var apiSignature = library.libraryCycle.apiSignature;
1867+
if (bundle != null && bundle.isValidated(apiSignature)) {
1868+
performance.getDataInt('apiSignature').increment();
1869+
return bundle;
1870+
}
18921871

1893-
if (_resolvedLibraryCache[path] case var cached?) {
1894-
_requestedLibraries.completeAll(path, cached);
1895-
return;
1896-
}
1872+
if (bundle == null) {
1873+
bundle = performance.run('readFromBytes', (performance) {
1874+
var bytes = _byteStore.get(key);
1875+
if (bytes != null) {
1876+
performance.getDataInt('hasBytes').increment();
1877+
performance.getDataInt('bytes').add(bytes.length);
1878+
return LibraryDiagnosticsBundle.fromBytes(bytes);
1879+
} else {
1880+
performance.getDataInt('noBytes').increment();
1881+
}
1882+
return null;
1883+
});
1884+
if (bundle == null) {
1885+
return null;
1886+
}
1887+
_libraryDiagnosticsCache[key] = bundle;
1888+
}
18971889

1898-
_analyzeFile(path);
1890+
var elementFactory = libraryContext.elementFactory;
1891+
var failure = bundle.requirements.isSatisfied(
1892+
elementFactory: elementFactory,
1893+
libraryManifests: elementFactory.libraryManifests,
1894+
);
1895+
scheduler.eventsController.add(
1896+
events.CheckLibraryDiagnosticsRequirements(
1897+
library: library,
1898+
failure: failure,
1899+
),
1900+
);
1901+
if (failure == null) {
1902+
bundle.addValidated(apiSignature);
1903+
return bundle;
1904+
}
1905+
return null;
1906+
});
18991907
}
19001908

1901-
/// The key to store a resolved library result.
1902-
///
1903-
/// The key is based on the path, URI, and content of the library files.
1909+
/// Returns the key for the [LibraryDiagnosticsBundle] in the byte store.
19041910
///
1905-
/// The result contains the fine-grained requirements, and map with
1906-
/// diagnostics for each file.
1907-
String _getResolvedLibraryKey(LibraryFileKind library) {
1911+
/// The key is a signature that incorporates the analysis options, the content
1912+
/// of the `pubspec.yaml` file, and the paths, URIs, and content hashes of all
1913+
/// files that are part of the [library]. This ensures that the cached bundle
1914+
/// is invalidated if any of these inputs change.
1915+
String _getLibraryDiagnosticsKey(LibraryFileKind library) {
1916+
if (library.diagnosticsBundleKey case var key?) {
1917+
return key;
1918+
}
1919+
19081920
var keyBuilder = ApiSignature();
19091921
keyBuilder.addInt(AnalysisDriver.DATA_VERSION);
19101922
keyBuilder.addUint32List(_saltForResolution);
@@ -1921,7 +1933,30 @@ class AnalysisDriver {
19211933
keyBuilder.addString(file.contentHash);
19221934
}
19231935

1924-
return '${keyBuilder.toHex()}.resolved2';
1936+
var key = '${keyBuilder.toHex()}.resolved2';
1937+
return library.diagnosticsBundleKey = key;
1938+
}
1939+
1940+
/// Completes the [getResolvedLibrary] request.
1941+
void _getResolvedLibrary(String path) {
1942+
var file = _fsState.getFileForPath(path);
1943+
var kind = file.kind;
1944+
switch (kind) {
1945+
case LibraryFileKind():
1946+
break;
1947+
case PartFileKind():
1948+
_requestedLibraries.completeAll(path, NotLibraryButPartResult());
1949+
return;
1950+
default:
1951+
throw UnimplementedError('(${kind.runtimeType}) $kind');
1952+
}
1953+
1954+
if (_resolvedLibraryCache[path] case var cached?) {
1955+
_requestedLibraries.completeAll(path, cached);
1956+
return;
1957+
}
1958+
1959+
_analyzeFile(path);
19251960
}
19261961

19271962
/// Return the key to store fully resolved results for the [signature].
@@ -2036,66 +2071,13 @@ class AnalysisDriver {
20362071
});
20372072

20382073
if (withFineDependencies) {
2039-
var fileResultBytesMap = performance.run('errors(isSatisfied)', (
2040-
performance,
2041-
) {
2042-
var elementFactory = libraryContext.elementFactory;
2043-
2044-
if (library.lastResolutionResult case var lastResult?) {
2045-
var failure = lastResult.requirements.isSatisfied(
2046-
elementFactory: elementFactory,
2047-
libraryManifests: elementFactory.libraryManifests,
2048-
);
2049-
if (failure == null) {
2050-
var reader = SummaryDataReader(lastResult.bytes);
2051-
return reader.readMap(
2052-
readKey: () => reader.readUri(),
2053-
readValue: () => reader.readUint8List(),
2054-
);
2055-
}
2056-
}
2057-
2058-
var reqAndUnitBytes = performance.run('getBytes', (_) {
2059-
var key = _getResolvedLibraryKey(library);
2060-
return _byteStore.get(key);
2061-
});
2062-
2063-
if (reqAndUnitBytes != null) {
2064-
var reader = SummaryDataReader(reqAndUnitBytes);
2065-
var requirements = performance.run('readRequirements', (_) {
2066-
return RequirementsManifest.read(reader);
2067-
});
2068-
2069-
var failure = requirements.isSatisfied(
2070-
elementFactory: elementFactory,
2071-
libraryManifests: elementFactory.libraryManifests,
2072-
);
2073-
if (failure == null) {
2074-
var mapBytes = reader.readUint8List();
2075-
library.lastResolutionResult = LibraryResolutionResult(
2076-
requirements: requirements,
2077-
bytes: mapBytes,
2078-
);
2079-
2080-
var reader2 = SummaryDataReader(mapBytes);
2081-
return reader2.readMap(
2082-
readKey: () => reader2.readUri(),
2083-
readValue: () => reader2.readUint8List(),
2084-
);
2085-
} else {
2086-
scheduler.eventsController.add(
2087-
events.ProduceErrorsCannotReuse(
2088-
library: library,
2089-
failure: failure,
2090-
),
2091-
);
2092-
}
2093-
}
2094-
return null;
2095-
});
2074+
var bundle = _getLibraryDiagnosticsBundle(
2075+
library: library,
2076+
performance: performance,
2077+
);
20962078

2097-
if (fileResultBytesMap != null) {
2098-
for (var fileEntry in fileResultBytesMap.entries) {
2079+
if (bundle != null) {
2080+
for (var fileEntry in bundle.serializedFileResults.entries) {
20992081
var file = library.files
21002082
.where((file) => file.uri == fileEntry.key)
21012083
.single;
@@ -2181,6 +2163,11 @@ class AnalysisDriver {
21812163
accumulatedAffected.add(file.path);
21822164
}
21832165

2166+
if (_fsState.getExistingFromPath(path) case var changedFile?) {
2167+
var changedLibrary = changedFile.kind.library;
2168+
changedLibrary?.invalidateDiagnosticsBundleKey();
2169+
}
2170+
21842171
_libraryContext?.elementFactory.replaceAnalysisSession(
21852172
AnalysisSessionImpl(this),
21862173
);

pkg/analyzer/lib/src/dart/analysis/driver_event.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ final class CannotReuseLinkedBundle extends AnalysisDriverEvent {
4444
});
4545
}
4646

47+
/// The event that we checked requirements of the library diagnostics.
48+
/// This is much cheaper than computing the result again, but not free.
49+
final class CheckLibraryDiagnosticsRequirements extends AnalysisDriverEvent {
50+
final LibraryFileKind library;
51+
final RequirementFailure? failure;
52+
53+
CheckLibraryDiagnosticsRequirements({
54+
required this.library,
55+
required this.failure,
56+
});
57+
}
58+
4759
/// The event that we were not able to reuse the existing analysis results
4860
/// for [library], because found the [failure].
4961
///

0 commit comments

Comments
 (0)