forked from flutter/devtools
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathutils.dart
More file actions
367 lines (315 loc) · 10.9 KB
/
utils.dart
File metadata and controls
367 lines (315 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
// Copyright 2018 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
// This file contain higher level utils, i.e. utils that depend on
// other libraries in this package.
// Utils, that do not have dependencies, should go to primitives/utils.dart.
import 'dart:async';
import 'dart:math';
import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:vm_service/vm_service.dart';
import '../../../devtools.dart' as devtools;
import '../../service/connected_app/connected_app.dart';
import '../framework/app_error_handling.dart';
import '../globals.dart';
import '../primitives/query_parameters.dart';
import '../primitives/utils.dart';
import '../ui/common_widgets.dart';
final _log = Logger('lib/src/shared/utils');
/// Logging to debug console only in debug runs.
void debugLogger(String message) {
assert(() {
_log.info(message);
return true;
}());
}
/// Whether DevTools is using a dark theme.
///
/// When DevTools is in embedded mode, we first check if the [ideTheme] has
/// specified a light or dark theme, and if it has we use this value. This is
/// safe to do because the user cannot access the dark theme DevTools setting
/// when in embedded mode, which is intentional so that the embedded DevTools
/// matches the theme of its surrounding window (the IDE).
///
/// When DevTools is not embedded, we use the user preference to determine
/// whether DevTools is using a light or dark theme.
///
/// This utility method should be used in favor of checking
/// `preferences.darkModeTheme.value` so that the embedded case is always
/// handled properly.
bool isDarkThemeEnabled() {
return isEmbedded() && ideTheme.ideSpecifiedTheme
? ideTheme.isDarkMode
: preferences.darkModeEnabled.value;
}
extension VmExtension on VM {
List<IsolateRef> isolatesForDevToolsMode() {
final vmDeveloperModeEnabled = preferences.vmDeveloperModeEnabled.value;
final vmIsolates = isolates ?? <IsolateRef>[];
return [
...vmIsolates,
if (vmDeveloperModeEnabled || vmIsolates.isEmpty)
...systemIsolates ?? <IsolateRef>[],
];
}
String get deviceDisplay {
return [
'$targetCPU',
if (architectureBits != null && architectureBits != -1)
'($architectureBits bit)',
operatingSystem,
].join(' ');
}
}
List<ConnectionDescription> generateDeviceDescription(
VM vm,
ConnectedApp connectedApp, {
bool includeVmServiceConnection = true,
}) {
var version = vm.version!;
// Convert '2.9.0-13.0.dev (dev) (Fri May ... +0200) on "macos_x64"' to
// '2.9.0-13.0.dev'.
if (version.contains(' ')) {
version = version.substring(0, version.indexOf(' '));
}
final flutterVersion = connectedApp.flutterVersionNow;
ConnectionDescription? vmServiceConnection;
if (includeVmServiceConnection &&
serviceConnection.serviceManager.service != null) {
final description = serviceConnection.serviceManager.serviceUri!;
vmServiceConnection = ConnectionDescription(
title: 'VM Service Connection',
description: description,
actions: [CopyToClipboardControl(dataProvider: () => description)],
);
}
return [
ConnectionDescription(title: 'CPU / OS', description: vm.deviceDisplay),
ConnectionDescription(
title: 'Connected app type',
description: connectedApp.display,
),
if (vmServiceConnection != null) vmServiceConnection,
ConnectionDescription(title: 'Dart Version', description: version),
if (flutterVersion != null && !flutterVersion.unknown) ...{
ConnectionDescription(
title: 'Flutter Version',
description: '${flutterVersion.version} / ${flutterVersion.channel}',
),
ConnectionDescription(
title: 'Framework / Engine',
description:
'${flutterVersion.frameworkRevision} / '
'${flutterVersion.engineRevision}',
),
},
];
}
/// This method should be public, because it is used by g3 specific code.
List<String> issueLinkDetails() {
final ide = DevToolsQueryParams.load().ide;
final issueDescriptionItems = [
'<-- Please describe your problem here. Be sure to include repro steps. -->',
'___', // This will create a separator in the rendered markdown.
'**DevTools version**: $devToolsVersion',
if (ide != null) '**IDE**: $ide',
];
final vm = serviceConnection.serviceManager.vm;
final connectedApp = serviceConnection.serviceManager.connectedApp;
if (vm != null && connectedApp != null) {
final descriptionEntries = generateDeviceDescription(
vm,
connectedApp,
includeVmServiceConnection: false,
);
final deviceDescription = descriptionEntries.map(
(entry) => '${entry.title}: ${entry.description}',
);
issueDescriptionItems.addAll([
'**Connected Device**:',
...deviceDescription,
]);
}
return issueDescriptionItems;
}
class ConnectionDescription {
ConnectionDescription({
required this.title,
required this.description,
this.actions = const <Widget>[],
});
final String title;
final String description;
final List<Widget> actions;
}
const _google3PathSegment = 'google3';
bool isGoogle3Path(List<String> pathParts) =>
pathParts.contains(_google3PathSegment);
List<String> stripGoogle3(List<String> pathParts) {
final google3Index = pathParts.lastIndexOf(_google3PathSegment);
if (google3Index != -1 && google3Index + 1 < pathParts.length) {
return pathParts.sublist(google3Index + 1);
}
return pathParts;
}
/// An extension on [KeyEvent] to make it simpler to determine if it is a key
/// down event.
extension IsKeyType on KeyEvent {
bool get isKeyDownOrRepeat => this is KeyDownEvent || this is KeyRepeatEvent;
}
/// A helper class for [Timer] functionality, where the callbacks are debounced.
class Debouncer extends Disposable {
Debouncer({required this.duration});
final Duration duration;
Timer? _activeTimer;
/// Invokes the [callback] once after the specified [duration] has elapsed.
///
/// If multiple invokations are called while the timer is running, only the
/// last one will be called once no more invokations happen within the given
/// [duration].
void run(void Function() callback) {
if (disposed) return;
_activeTimer?.cancel();
_activeTimer = Timer(duration, callback);
}
@override
void dispose() {
_activeTimer?.cancel();
_activeTimer = null;
super.dispose();
}
}
typedef DebounceCancelledCallback = bool Function();
/// A periodic debouncer that calls a callback at most once in a given duration.
class PeriodicDebouncer {
/// Start running the periodic debouncer.
///
/// [callback] is triggered once immediately, and then every [duration] the
/// timer checks to see if the previous [callback] call has finished running.
/// If it has finished, then then next call to [callback] will begin.
PeriodicDebouncer.run(
Duration duration,
Future<void> Function({DebounceCancelledCallback? cancelledCallback})
callback,
) : _callback = callback {
// Start running the first call to the callback.
_runCallback();
// Start periodic timer so that the callback will be periodically triggered
// after the first callback.
_timer = Timer.periodic(duration, (_) => _runCallback());
}
void _runCallback() async {
// If the previous callback is still running, then don't trigger another
// callback. (debounce)
if (_isRunning) {
return;
}
if (isCancelled) return;
try {
_isRunning = true;
await _callback(cancelledCallback: () => isCancelled);
} finally {
_isRunning = false;
}
}
Timer? _timer;
final Future<void> Function({DebounceCancelledCallback? cancelledCallback})
_callback;
bool _isRunning = false;
bool _isCancelled = false;
void cancel() {
_isCancelled = true;
_timer?.cancel();
}
bool get isCancelled => _isCancelled || (_timer != null && !_timer!.isActive);
void dispose() {
cancel();
_timer = null;
}
}
Future<void> launchUrlWithErrorHandling(String url) async {
await launchUrl(
url,
onError: () => notificationService.push('Unable to open $url.'),
);
}
/// A worker that will run [callback] in groups of [_chunkSize], when [doWork]
/// is called.
///
/// [progressCallback] will be called with 0.0 progress when starting the work
/// and any time a chunk finishes running, with a value that represents the
/// proportion of indices that have been completed so far.
///
/// This class may be helpful when sets of work need to be done over a list,
/// while avoiding blocking the UI thread.
class InterruptableChunkWorker {
InterruptableChunkWorker({
int chunkSize = _defaultChunkSize,
required this.callback,
required this.progressCallback,
}) : _chunkSize = chunkSize;
static const _defaultChunkSize = 50;
final int _chunkSize;
int _workId = 0;
bool _disposed = false;
void Function(int) callback;
void Function(double progress) progressCallback;
/// Start doing the chunked work.
///
/// [callback] will be called on every index from 0...[length-1], inclusive,
/// in chunks of [_chunkSize]
///
/// If [doWork] is called again, then [callback] will no longer be called
/// on any remaining indices from previous [doWork] calls.
///
Future<bool> doWork(int length) {
final completer = Completer<bool>();
final localWorkId = ++_workId;
Future<void> doChunkWork(int chunkStartingIndex) async {
if (_disposed) {
return completer.complete(false);
}
if (chunkStartingIndex >= length) {
return completer.complete(true);
}
final chunkUpperIndexLimit = min(length, chunkStartingIndex + _chunkSize);
for (
int indexIterator = chunkStartingIndex;
indexIterator < chunkUpperIndexLimit;
indexIterator++
) {
// If our localWorkId is no longer active, then do not continue working
if (localWorkId != _workId) return completer.complete(false);
callback(indexIterator);
}
progressCallback(chunkUpperIndexLimit / length);
await delayToReleaseUiThread();
await doChunkWork(chunkStartingIndex + _chunkSize);
}
if (length <= 0) {
return Future.value(true);
}
progressCallback(0.0);
safeUnawaited(doChunkWork(0));
return completer.future;
}
void dispose() {
_disposed = true;
}
}
String get devToolsVersion => devtools.version;
/// Unawaits the given [future] and catches any errors thrown.
void safeUnawaited(
Future<void> future, {
void Function(Object?, StackTrace)? onError,
}) {
onError ??=
(e, st) => reportError('Error in unawaited Future: $e', stack: st);
unawaited(future.catchError(onError));
}