This changes the SessionLogFileSink from using `writeAsStringSync` and overwriting the whole file on each message. It does this by using `openWrite()` and keeping the `IOSink` instead.
This means:
- it uses `dart:io` (we don't have `openWrite`/sink support in the abstraction)
- we need to call `close()` to flush the file during shutdown (there was already a shutdown method on the session logger, but it wasn't used or called, so now it is)
This does mean the tests for this class write to the physical disk, but there are only two of them - most other tests use the in-memory sink (or are just testing the normalizer).
Fixes https://github.com/dart-lang/sdk/issues/63275
Change-Id: I7ef347fc46d8ce3daf30ed2f9965e8921c5c4856
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/501640
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
This updates the `showUserPrompt` method to accept a cancellation token so that if requests are cancelled (by the client, or something like a second refactor cancelling the first), the prompt is also cancelled (and when the response arrives, it can be ignored).
Note: Unfortunately the protocol (and VS Code) don't actually allow for the prompt to be cancelled/hidden from the user, this is mainly to avoid us keeping the request "alive" on the server if the user ignores a prompt and we know the parent request was cancelled anyway.
Fixes https://github.com/dart-lang/sdk/issues/63285
Change-Id: Ia77880a749b284a2ff31c8cf560307ad908d2175
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/499360
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This wraps analyzer and analysis_server access to `Platform` with a
`PlatformInfo` class that has a top-level `const platform`.
With this package analyzer and analysis_server will not access
`Platform` directly anymore. But instead, to access say
`Platform.lineTerminator` we'll import `package:analyzer/src/util/platform_info.dart`
and use `platform.lineTerminator`.
We probably could add tests to prevent accidental re-introduction of `Platform` usage.
But the idea is to have a client side dartpad _compilation environment_ that is compiled
and tested inside the SDK repository. Once that lands, re-introduction of `Platform`
would cause those tests to break.
See go/clientside-dartpad for motivation.
Change-Id: Ibf20cf06d74f7984700a3cd4ce520c8994eed3c9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/481380
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Jonas Jensen <jonasfj@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This data is collected on shutdown, the same as the legacy analytics data.
This is implementation for some of the analytics we want to collect,
specified at go/dart-analyzer-plugin-analytics.
* How many plugins are enabled for each context?
* How many lint rules are registered for each plugin?
* How many warning rules are registered for each plugin?
* How many fixes are registered for each plugin?
* How many assists are registered for each plugin?
Change-Id: I36c176737c194550e1947985576ed461fd8a1bf3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/452482
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
This is mainly to exercise the performance test framework, for writing
a report for the test runs.
- adds a flag to specify a file
- logs context information and info from the message scheduler
Started with just these two as an example as I was not sure what performance data we do want to record.
Change-Id: I3e5ff4b77c62d36e47ba7bac8f656b19ba19c457
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/468600
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Keerti Parthasarathy <keertip@google.com>
This removes all code related to handling requests for (and sending notifications of modifications of) the virtual files for macros.
Clients would never call this handler unless the analysis server had previously told them about these virtual files with the `dart-macro+file` scheme, which never happens because the implementation was previously removed.
This does not remove the `clientUriConverter` (which as well as handling conversions to/from the macro scheme, also handles conversions between URIs and Paths to support using URIs in the legacy protocol) because I'm not yet certain that is unused.
Change-Id: I148e2383a48b5f6e3a28eff3dd11506fc86353b0
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/461120
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
The session logger is the first half of the replay mechanism. It
captures data in a log about all of the messages sent to and from the
analysis server. The log player will then take such a log and replay
the portions required in order to drive an analysis server.
This first CL captures all of the communications except those with the
plugin isolates. Adding support for the plugin isolates will require
some additional refactoring that I thought would be easier to review if
placed in a separate CL.
Change-Id: I8f19abd3ebff83ac26584a9377922520857801d4
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/459341
Reviewed-by: Keerti Parthasarathy <keertip@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
* Bump kept requests from 50 to 250
* Make the request storage (`RecentBuffer`) add in constant time instead
of linear time (modulo the `n` in `O(n)` being the constant 50 or 250
making the before theoretically constant too, but anyway).
Change-Id: Ib00bd038b06f167fd7298a87402a89a23fd53880
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/457025
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Jens Johansen <jensj@google.com>
Observation 1: The legacy protocol answered completion requests via a
`server.resolveForCompletion` call, and the LSP protocol answered
completion requests via a `server.getResolvedUnit` call where the idea
is that resolving for completion requires less and is therefore faster.
Solution: Make the LSP protocol use `server.resolveForCompletion` too.
Observation 2: Completion requests often come right after change request
making timing important and the `await driver.applyPendingFileChanges()`
call done in the analysis server "pushed" the timing making the
`server.resolveForCompletion` actually finished after it had already
resolved the whole thing.
Solution: Don't do that - the driver adds it to the queue of work, the
work is done later in `performWork` called from
`AnalysisDriverScheduler._run` where `_applyPendingFileChanges` is
always called anyway (which is the call that completes the
`applyPendingFileChanges`call).
Observation 3: If there is no change yet to be processed, a
`resolveForCompletion` call is slower than a `getResolvedUnit` because
the resolved unit is cached (assuming it's a priority file) and the
`resolveForCompletion` call always parses the file again.
Solution: Respond to the `resolveForCompletion` call with the resolved
unit data if it's available in the cache. The cache is always cleared
when changes happen anyway.
Benchmarks on this stuff is a bit weird because it's timing related - so
while I'd say this is overall just better there are also runs where we
get "bad timing" and the runtimes are therefore not better. In an
attempt to clear it up I've run the benchmarks 25 times each, and
attempted to put the data in two different buckets as needed.
*lsp_type_in_big_file of size 16,000*
```
Fully done after last type (ms):
Difference at 95.0% confidence
-1419.2 +/- 322.074
-16.8866% +/- 3.83225%
(Student's t, pooled s = 566.238)
Whole typing time (ms):
Difference at 95.0% confidence
-1418.88 +/- 322.037
-13.9661% +/- 3.16983%
(Student's t, pooled s = 566.172)
Uncancelled completion response time (ms):
Difference at 95.0% confidence
-1590.48 +/- 682.753
-28.0194% +/- 12.028%
(Student's t, pooled s = 1200.35)
```
The `Uncancelled completion response time` has a big "+/-" so attempting
to "good and bad bucketize" it I get:
good bucket:
```
Difference at 95.0% confidence
-1929.8 +/- 347.712
-35.9528% +/- 6.47797%
(Student's t, pooled s = 543.261)
```
bad bucket (though truthfully there wasn't a clear cutoff before):
```
No difference proven at 95.0% confidence
```
which sort of makes sense: If the completion runs before a (new) change
starts processing we now `resolveForCompletion` instead which is faster,
but if completion runs after the change has started processing we
essentially - both before and after - do nothing (except wait for the
calculation to finish) because we just load the data from cache.
*lsp_type_in_big_file_ask_for_completion, 16,000*
```
Completion #1 (ms):
No difference proven at 95.0% confidence
```
Ehh. There's a clear cutoff in the now, so taking the 8 (how the cutoff
happens to be) fastest from each I get
```
Difference at 95.0% confidence
-1034.12 +/- 75.3617
-37.673% +/- 2.74542%
(Student's t, pooled s = 70.2673)
```
Moving on.
```
Completion #2 (ms):
Difference at 95.0% confidence
-1076.28 +/- 178.446
-37.2601% +/- 6.17769%
(Student's t, pooled s = 313.726)
```
here 2 in the "now" has bad timing, removing them from the statistics
gives
```
Difference at 95.0% confidence
-1182.39 +/- 104.543
-40.9334% +/- 3.6192%
(Student's t, pooled s = 179.748)
```
Moving on.
```
Completion #3 (ms):
Difference at 95.0% confidence
-735.6 +/- 292.553
-25.6471% +/- 10.2%
(Student's t, pooled s = 514.336)
and removing the 8 bad ones:
Difference at 95.0% confidence
-1195.69 +/- 99.305
-41.6884% +/- 3.46232%
(Student's t, pooled s = 156.306)
```
Continuing like this:
```
Completion #4 (ms):
Difference at 95.0% confidence
-948.4 +/- 265.704
-32.9887% +/- 9.24214%
(Student's t, pooled s = 467.134)
and removing the 5 bad ones:
Difference at 95.0% confidence
-1252.87 +/- 86.5425
-43.5793% +/- 3.01026%
(Student's t, pooled s = 143.022)
Completion #5 (ms):
Difference at 95.0% confidence
-1067.88 +/- 199.266
-37.0011% +/- 6.90439%
(Student's t, pooled s = 350.329)
and removing the 3 bad ones:
Difference at 95.0% confidence
-1236.76 +/- 69.5776
-42.8527% +/- 2.4108%
(Student's t, pooled s = 118.18)
```
Moving on to the "Completion without change" I realize just now that the
benchmark for the first entry is broken - it doesn't wait until the
previous change has been processed, meaning that in the 3 cases where we
got bad timing in "Completion #5 (ms)" we see about the same result as
before, but in the 22 other cases we see bad results because it has to
wait until the previous change has been processed. For the remaining
(2-5) there is no virtually change which makes sense because both before
and now it just fetches the resolved unit from cache.
*legacy_type_in_big_file_ask_for_completion*
```
Completion #1 (ms):
Difference at 95.0% confidence
-1817.08 +/- 60.6836
-48.0638% +/- 1.60515%
(Student's t, pooled s = 106.688)
Completion #2 (ms):
Difference at 95.0% confidence
-2208.56 +/- 48.4844
-55.4647% +/- 1.21761%
(Student's t, pooled s = 85.2403)
Completion #3 (ms):
Difference at 95.0% confidence
-2159.68 +/- 69.4145
-53.0717% +/- 1.70578%
(Student's t, pooled s = 122.037)
Completion #4 (ms):
Difference at 95.0% confidence
-2264.44 +/- 73.2112
-53.5455% +/- 1.73117%
(Student's t, pooled s = 128.712)
Completion #5 (ms):
Difference at 95.0% confidence
-2147.4 +/- 68.5023
-50.9254% +/- 1.62453%
(Student's t, pooled s = 120.434)
```
The first "Completion without change" suffers from the same as before
and I will skip it here.
```
Completion without change #2 (ms):
Difference at 95.0% confidence
-416.28 +/- 28.2024
-61.3512% +/- 4.15646%
(Student's t, pooled s = 49.5826)
Completion without change #3 (ms):
Difference at 95.0% confidence
-687.24 +/- 21.9848
-92.6499% +/- 2.96387%
(Student's t, pooled s = 38.6514)
Completion without change #4 (ms):
Difference at 95.0% confidence
-637.32 +/- 26.482
-95.2703% +/- 3.95868%
(Student's t, pooled s = 46.5579)
Completion without change #5 (ms):
Difference at 95.0% confidence
-702.32 +/- 18.7277
-95.8301% +/- 2.55535%
(Student's t, pooled s = 32.925)
```
I don't know why there doesn't appear to be any timing related issues
here (maybe sending and receiving the entire big file (in legacy vs in
lsp where a small 'diff' is send) taking more time pushes the timing,
but I'm guessing) - nor do I know why now "Completion without change #2"
is slower (~250 ms) than the subsequent ones (~30 ms).
Change-Id: I4c21d658efccbcf197eedb69f466b2942b78c4b9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/457364
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Jens Johansen <jensj@google.com>
In my tests with fine-grained dependencies the on-disk cache size
after analyzer/ + friends, and Flutter repository is at or a litlle
over the upper about 1 GB + 12%, which causes cache evictions, so when
I switch between these two sets, I saw analysis without changes.
Change-Id: Ief3344746177388b9e8a83df1474cc6f9aa821d8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/452481
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
With fine-grained dependencies aggressively unloading deserialized
objects from memory, and probing cache for previous blobs, it becomes
necessary to have access to these blobs without hitting the file system.
This new size is enough to keep the Flutter repository fast.
Change-Id: Ief507cddc6da3493896db3fedd9f0938c7ccd030
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/452225
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Work towards https://github.com/dart-lang/sdk/issues/61386
A few distinct changes:
* Rename PluginInfo to PluginIsolate
* Move PluginInfo and PluginSession to a new library,
plugin_isolate.dart. PluginSession is _essentially_ a private class
to PluginIsolate, so it's good to co-locate them.
* Move PluginInfo tests and PluginSession tests to a new library.
* Extract out shared parent class for PluginManager tests,
PluginIsolate tests, and PluginSession tests.
Change-Id: Ib0c638b8797f51c57c07e277acdaea6d8202f957
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/446960
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
This removes `useLineEndingsForPlatform = false` from a few more tests, and as part of fixing them, ensures that `LspChangeVerifier` always normalises the expected text, and asserts that all modifies files were modified.
This highlighted many more tests that were not normalizing their content (but didn't use `useLineEndingsForPlatform` because they didn't previously call any shared methods that required/asserting normalization. Those tests were also updated.
Change-Id: Ica914b6207b3c9fd440de4e0470e50a896dd976d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/443561
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
The two pieces of information we're tracking are
- the number of libraries in each library cycle
- the number of lines of code in each library cycle
The data is reported in terms of percentage.
This doesn't give us any way to correlate the two values. Knowing that
there is a library cycle with M libraries and a cycle with N lines of
code, doesn't tell us whether it's the same library cycle in both cases.
Still, I think it will help us understand the nature of the code that
we need to be able to analyze quickly.
Change-Id: I36a8bf11c4c6fdced6c524225be7a633062351ac
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/437126
Reviewed-by: Keerti Parthasarathy <keertip@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This adds a new page to the diagnostic pages that displays the current
state of the message scheduler (or rather the messages passing through
the scheduler) as well as the most recent history.
The next steps after this are to
- include this information (and library cycle information) in the
generated report
- add this (and cycle) information to the analytics we're gathering
I put the new page in its own file, and would like to move existing
pages to separate files as well. Doing so might trigger moving some
files around in the directory structure in order to better organize
the code.
Change-Id: Ic4f0e72ffbadc1e39702f1f8f13850f5bd5814a5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/431947
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
This updates the `ApplyCodeActionCommandHandler` to be a "shared" command that can work for either kind of server (although it does not yet allow calling over DTD - the executeCommand handler still requires trusted callers).
It also moves all of the tests into a shared mixin so they will be run for both server kinds.
Change-Id: I831cc5a1a9feadc528e5b33b82e3469c60111dfa
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/427440
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This adds an optional version number to AddContentOverlay and UpdateContentOverlay in the legacy protocol.
It also moves some code (such as the `Map` that stores the current document versions) from the LSP server into the base class, and updates the overlay handlers to update it.
And finally, it removes the FailingTest() annotations on the versioned EditArguments test since they now work.
Change-Id: Icf2a2825eb6227f5faa3e21ae6c3a7d15997ef5a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/404821
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This allows us to traverse the children without needing to create lists.
It also allows us to iterate over lists of nodes more efficiently.
Tested against the script that computes fixes for 10,000 diagnostics,
I'm seeing the following result.
Before this CL the average time was 10692.4 ms.
After this CL the average time was 1900.0 ms.
That's just over an 82% improvement.
Change-Id: Ideff745288c8990e9948b22c5204d176af3cac6e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/426904
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
There are only two callers, and they each have enough information at
hand to carry out this simple computation themselves.
In particular, getElementAtOffset does not need to reach out to the
driver twice, to get *Result objects for the same file.
Change-Id: Ia5fd1e3045bad4d9aa64a7011a3a5c90db0c305d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/423170
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
There are no functional changes, just changes to the way the logging
functionality is implemented. The motivation for the changes is to
allow future CLs to explore ways of changing the expectations so that
async handling of messages won't produce flaky tests.
All references to the 'messageLog' outside the class have been replaced
by higher-level methods with a semantic meaning.
The class has been split into an interface and an implementation, which
allows the test-specific aspects to be in the `test` directory.
Some additional code cleanup was also done.
Change-Id: Ie515f71153a96fb6e92eb8e2eb05f4b5e064bbd7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/421965
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Phil Quitslund <pquitslund@google.com>
If we need to control when requests that modify the state of analysis
are performed, and we do, then that needs to include watch events from
a file watcher. This CL causes those events to be placed on the message
scheduler's queue so that we can control when we notify the analysis
driver of changes.
Change-Id: I1b099a35da7a4b9a6f4d86feb497d6822041155d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/421520
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Sometimes - on responding to `edit.getFixes` requests, the method
`getTopLevelDeclarations` is called several times with the same
parameter and caching has then sometimes sped things up.
I then also noticed that it caused `reportAnalysisAnalytics` to be
called many times (e.g. 73 times for one `edit.getFixes` request)
each time spending time collecting data which - on passing to
`analyticsManager.analysisComplete` - was just thrown away because it
only saves the first one.
Combined these changes have been observed to reduce the response time
on `edit.getFixes` by up (down?) to ~20% (e.g. from 5 seconds
to 1 second).
In response to http://b/407797012 where more data is also available.
Unfortunately I haven't been able to reproduce this is a benchmark
(yet anyway). I don't know if it needs slower slower reads, an extreme
amount of files, several contexts or something else entirely.
Change-Id: Id214b0e1b7d8bbef7f6d956408f17fab45618b78
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/420323
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Commit-Queue: Johnni Winther <johnniwinther@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This CL makes three changes:
- moves the message scheduler into a new directory,
- splits the implementation into two files,
- and renames `MessageObject` to `ScheduledMessage`.
It does not make any semantic changes to the code.
The expectation is that we will need to make the message scheduler more
complex as part of improving the performance of the server, and that it
will be easier to do this if the code is structured differently.
Change-Id: I3f8741cadceb34c69fd45c1bf21f331d15824565
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/420500
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
The legacy_many_files_in_flutter_set_subscriptions benchmark shows how
"flutter.setSubscriptions" calls can make the analyzer slower to
respond.
What happens is this:
* The user opens a new file in the IDE.
* The IDE sends the `flutter.setSubscriptions` request which equates to
a call to `getResolvedUnit` for each file in the request. If this is,
say, 300 files it's 300 calls to `getResolvedUnit`.
* The IDE sends a `edit.getAssists` request for the newly opened file.
This request starts processing, reaches `getResolvedLibrary(file)`
which calls `getUnitElement` ultimately adding the path to
`_unitElementRequestedFiles` which in `performWork` is done _after_
`_requestedFiles`, meaning it has to do all the flutter requested
files first.
* The user might then request completion for instance, but because the
analyzer only processes one request at a time it has to wait for the
`edit.getAssists` request to finish first, which had to wait for the
files from the `flutter.setSubscriptions` request to process.
All in all it's a lot of waiting for the user.
This CL adds a `interactive` option to the `getResolvedUnit` call. It
defaults to true in which case files are still added to
`_requestedFiles` and processed the same. If it's false it will instead
be added to a newly introduced list instead and processed at a lower
priority. Subscription requests are changed to pass `false` to
`interactive`, avoiding the scenario above.
Comparing before this CL with this CL on the
"legacy_many_files_in_flutter_set_subscriptions" benchmark with 100
files / CodeType.ImportChain these are the statistics on the changes
based on 5 runs each:
```
Completion after open of new file: -81.6652% +/- 7.7564% (-3.70 +/- 0.35) (4.53 -> 0.83)
getAssists call: -96.6315% +/- 0.9307% (-3.61 +/- 0.03) (3.74 -> 0.13)
peak virtual memory size: -5.6786% +/- 3.2964% (-139.00 +/- 80.69) (2447.80 -> 2308.80)
total program size (virtual): -4.6387% +/- 3.8146% (-110.80 +/- 91.11) (2388.60 -> 2277.80)
```
Even when https://github.com/flutter/flutter-intellij/issues/7980 is
hopefully fixed I think it is a fair change to de-prioritize a
non-interactive request.
Change-Id: Icba2faebf12f9913cf24db7cb90fdc6f4c74164e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/418020
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Jens Johansen <jensj@google.com>