| // Copyright 2018 The Chromium Authors. 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 'dart:math'; |
| |
| import 'package:collection/collection.dart'; |
| import 'package:intl/intl.dart'; |
| import 'package:vm_service/vm_service.dart'; |
| |
| bool collectionEquals(e1, e2) => const DeepCollectionEquality().equals(e1, e2); |
| |
| const String loremIpsum = ''' |
| Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec faucibus dolor quis rhoncus feugiat. Ut imperdiet |
| libero vel vestibulum vulputate. Aliquam consequat, lectus nec euismod commodo, turpis massa volutpat ex, a |
| elementum tellus turpis nec arcu. Suspendisse erat nisl, rhoncus ut nisi in, lacinia pretium dui. Donec at erat |
| ultrices, tincidunt quam sit amet, cursus lectus. Integer justo turpis, vestibulum condimentum lectus eget, |
| sodales suscipit risus. Nullam consequat sit amet turpis vitae facilisis. Integer sit amet tempus arcu. |
| '''; |
| |
| // 2^52 is the max int for dart2js. |
| final int maxJsInt = pow(2, 52) as int; |
| |
| String getLoremText([int paragraphCount = 1]) { |
| String str = ''; |
| for (int i = 0; i < paragraphCount; i++) { |
| str += '$loremIpsum\n'; |
| } |
| return str.trim(); |
| } |
| |
| final Random r = Random(); |
| |
| final List<String> _words = loremIpsum |
| .replaceAll('\n', ' ') |
| .split(' ') |
| .map((String w) => w.toLowerCase()) |
| .map((String w) => w.endsWith('.') ? w.substring(0, w.length - 1) : w) |
| .map((String w) => w.endsWith(',') ? w.substring(0, w.length - 1) : w) |
| .toList(); |
| |
| String getLoremFragment([int wordCount]) { |
| wordCount ??= r.nextInt(8) + 1; |
| return toBeginningOfSentenceCase( |
| List<String>.generate(wordCount, (_) => _words[r.nextInt(_words.length)]) |
| .join(' ') |
| .trim()); |
| } |
| |
| String escape(String text) => text == null ? '' : htmlEscape.convert(text); |
| |
| final NumberFormat nf = NumberFormat.decimalPattern(); |
| |
| String percent2(double d) => '${(d * 100).toStringAsFixed(2)}%'; |
| |
| String printMb(num bytes, [int fractionDigits = 1]) { |
| return (bytes / (1024 * 1024)).toStringAsFixed(fractionDigits); |
| } |
| |
| String msText( |
| Duration dur, { |
| bool includeUnit = true, |
| int fractionDigits = 1, |
| }) { |
| return '${(dur.inMicroseconds / 1000).toStringAsFixed(fractionDigits)}' |
| '${includeUnit ? ' ms' : ''}'; |
| } |
| |
| T nullSafeMin<T extends num>(T a, T b) { |
| if (a == null || b == null) { |
| return a ?? b; |
| } |
| return min<T>(a, b); |
| } |
| |
| T nullSafeMax<T extends num>(T a, T b) { |
| if (a == null || b == null) { |
| return a ?? b; |
| } |
| return max<T>(a, b); |
| } |
| |
| int log2(num x) => (log(x) / log(2)).floor(); |
| |
| String isolateName(IsolateRef ref) { |
| // analysis_server.dart.snapshot$main |
| String name = ref.name; |
| name = name.replaceFirst(r'.snapshot', ''); |
| if (name.contains(r'.dart$')) { |
| name = name + '()'; |
| } |
| return name; |
| } |
| |
| String funcRefName(FuncRef ref) { |
| if (ref.owner is LibraryRef) { |
| //(ref.owner as LibraryRef).uri; |
| return ref.name; |
| } else if (ref.owner is ClassRef) { |
| return '${ref.owner.name}.${ref.name}'; |
| } else if (ref.owner is FuncRef) { |
| return '${funcRefName(ref.owner as FuncRef)}.${ref.name}'; |
| } else { |
| return ref.name; |
| } |
| } |
| |
| void executeWithDelay(Duration delay, void callback(), |
| {bool executeNow = false}) { |
| if (executeNow || delay.inMilliseconds <= 0) { |
| callback(); |
| } else { |
| Timer(delay, () { |
| callback(); |
| }); |
| } |
| } |
| |
| String longestFittingSubstring( |
| String originalText, |
| num maxWidth, |
| List<num> asciiMeasurements, |
| num slowMeasureFallback(int value), |
| ) { |
| if (originalText.isEmpty) return originalText; |
| |
| final runes = originalText.runes.toList(); |
| |
| num currentWidth = 0; |
| |
| int i = 0; |
| while (i < runes.length) { |
| final rune = runes[i]; |
| final charWidth = |
| rune < 128 ? asciiMeasurements[rune] : slowMeasureFallback(rune); |
| if (currentWidth + charWidth > maxWidth) { |
| break; |
| } |
| // [currentWidth] is approximate due to ignoring kerning. |
| currentWidth += charWidth; |
| i++; |
| } |
| |
| return originalText.substring(0, i); |
| } |
| |
| /// Whether a given code unit is a letter (A-Z or a-z). |
| bool isLetter(int codeUnit) => |
| (codeUnit >= 65 && codeUnit <= 90) || (codeUnit >= 97 && codeUnit <= 122); |
| |
| /// Returns a simplified version of a StackFrame name. |
| /// |
| /// Given an input such as |
| /// `_WidgetsFlutterBinding&BindingBase&GestureBinding.handleBeginFrame`, this |
| /// method will strip off all the leading class names and return |
| /// `GestureBinding.handleBeginFrame`. |
| /// |
| /// See (https://github.com/dart-lang/sdk/issues/36999). |
| String getSimpleStackFrameName(String name) { |
| final newName = name.replaceAll('<anonymous closure>', '<closure>'); |
| |
| // If the class name contains a space, then it is not a valid Dart name. We |
| // throw out simplified names with spaces to prevent simplifying C++ class |
| // signatures, where the '&' char signifies a reference variable - not |
| // appended class names. |
| if (newName.contains(' ')) { |
| return newName; |
| } |
| return newName.split('&').last; |
| } |
| |
| class Property<T> { |
| Property(this._value); |
| |
| final StreamController<T> _changeController = StreamController<T>.broadcast(); |
| T _value; |
| |
| T get value => _value; |
| |
| set value(T newValue) { |
| if (newValue != _value) { |
| _value = newValue; |
| _changeController.add(newValue); |
| } |
| } |
| |
| Stream<T> get onValueChange => _changeController.stream; |
| } |
| |
| /// A typedef to represent a function taking no arguments and with no return |
| /// value. |
| typedef VoidFunction = void Function(); |
| |
| /// A typedef to represent a function taking no arguments and returning a void |
| /// future. |
| typedef VoidAsyncFunction = Future<void> Function(); |
| |
| /// A typedef to represent a function taking a single argument and with no |
| /// return value. |
| typedef VoidFunctionWithArg = void Function(dynamic arg); |
| |
| /// Batch up calls to the given closure. Repeated calls to [invoke] will |
| /// overwrite the closure to be called. We'll delay at least [minDelay] before |
| /// calling the closure, but will not delay more than [maxDelay]. |
| class DelayedTimer { |
| DelayedTimer(this.minDelay, this.maxDelay); |
| |
| final Duration minDelay; |
| final Duration maxDelay; |
| |
| VoidFunction _closure; |
| |
| Timer _minTimer; |
| Timer _maxTimer; |
| |
| void invoke(VoidFunction closure) { |
| _closure = closure; |
| |
| if (_minTimer == null) { |
| _minTimer = Timer(minDelay, _fire); |
| _maxTimer = Timer(maxDelay, _fire); |
| } else { |
| _minTimer.cancel(); |
| _minTimer = Timer(minDelay, _fire); |
| } |
| } |
| |
| void _fire() { |
| _minTimer?.cancel(); |
| _minTimer = null; |
| |
| _maxTimer?.cancel(); |
| _maxTimer = null; |
| |
| _closure(); |
| _closure = null; |
| } |
| } |
| |
| /// These utilities are ported from the Flutter IntelliJ plugin. |
| /// |
| /// With Dart's terser JSON support, these methods don't provide much value so |
| /// we should consider removing them. |
| class JsonUtils { |
| JsonUtils._(); |
| |
| static String getStringMember(Map<String, Object> json, String memberName) { |
| // TODO(jacobr): should we handle non-string values with a reasonable |
| // toString differently? |
| return json[memberName] as String; |
| } |
| |
| static int getIntMember(Map<String, Object> json, String memberName) { |
| return json[memberName] as int ?? -1; |
| } |
| |
| static List<String> getValues(Map<String, Object> json, String member) { |
| final List<dynamic> values = json[member] as List; |
| if (values == null || values.isEmpty) { |
| return const []; |
| } |
| |
| return values.cast(); |
| } |
| |
| static bool hasJsonData(String data) { |
| return data != null && data.isNotEmpty && data != 'null'; |
| } |
| } |
| |
| typedef RateLimiterCallback = Future<Object> Function(); |
| |
| /// Rate limiter that ensures a [callback] is run no more than the |
| /// specified rate and that at most one async [callback] is running at a time. |
| class RateLimiter { |
| RateLimiter(double requestsPerSecond, this.callback) |
| : delayBetweenRequests = 1000 ~/ requestsPerSecond; |
| |
| final RateLimiterCallback callback; |
| Completer<void> _pendingRequest; |
| |
| /// A request has been scheduled to run but is not yet pending. |
| bool requestScheduledButNotStarted = false; |
| int _lastRequestTime; |
| final int delayBetweenRequests; |
| |
| Timer _activeTimer; |
| |
| /// Schedules the callback to be run the next time the rate limiter allows it. |
| /// |
| /// If multiple calls to scheduleRequest are made before a request is allowed, |
| /// only a single request will be made. |
| void scheduleRequest() { |
| if (requestScheduledButNotStarted) { |
| // No need to schedule a request if one has already been scheduled but |
| // hasn't yet actually started executing. |
| return; |
| } |
| |
| if (_pendingRequest != null && !_pendingRequest.isCompleted) { |
| // Wait for the pending request to be done before scheduling the new |
| // request. The existing request has already started so may return state |
| // that is now out of date. |
| requestScheduledButNotStarted = true; |
| _pendingRequest.future.whenComplete(() { |
| _pendingRequest = null; |
| requestScheduledButNotStarted = false; |
| scheduleRequest(); |
| }); |
| return; |
| } |
| |
| final currentTime = DateTime.now().millisecondsSinceEpoch; |
| if (_lastRequestTime == null || |
| _lastRequestTime + delayBetweenRequests <= currentTime) { |
| // Safe to perform the request immediately. |
| _performRequest(); |
| return; |
| } |
| // Track that we have scheduled a request and then schedule the request |
| // to occur once the rate limiter is available. |
| requestScheduledButNotStarted = true; |
| _activeTimer = Timer( |
| Duration( |
| milliseconds: |
| currentTime - _lastRequestTime + delayBetweenRequests), () { |
| _activeTimer = null; |
| requestScheduledButNotStarted = false; |
| _performRequest(); |
| }); |
| } |
| |
| void _performRequest() async { |
| try { |
| _lastRequestTime = DateTime.now().millisecondsSinceEpoch; |
| _pendingRequest = Completer(); |
| await callback(); |
| } finally { |
| _pendingRequest.complete(null); |
| } |
| } |
| |
| void dispose() { |
| _activeTimer?.cancel(); |
| } |
| } |
| |
| /// Time unit for displaying time ranges. |
| /// |
| /// If the need arises, this enum can be expanded to include any of the |
| /// remaining time units supported by [Duration] - (seconds, minutes, etc.). If |
| /// you add a unit of time to this enum, modify the toString() method in |
| /// [TimeRange] to handle the new case. |
| enum TimeUnit { |
| microseconds, |
| milliseconds, |
| } |
| |
| class TimeRange { |
| TimeRange({this.singleAssignment = true}); |
| |
| final bool singleAssignment; |
| |
| Duration get start => _start; |
| |
| Duration _start; |
| |
| set start(Duration value) { |
| if (singleAssignment) { |
| assert(_start == null); |
| } |
| _start = value; |
| } |
| |
| Duration get end => _end; |
| |
| Duration _end; |
| |
| bool contains(Duration target) => target >= start && target <= end; |
| |
| set end(Duration value) { |
| if (singleAssignment) { |
| assert(_end == null); |
| } |
| _end = value; |
| } |
| |
| Duration get duration => end - start; |
| |
| @override |
| String toString({TimeUnit unit}) { |
| unit ??= TimeUnit.microseconds; |
| switch (unit) { |
| case TimeUnit.microseconds: |
| return '[${_start?.inMicroseconds} μs - ${end?.inMicroseconds} μs]'; |
| case TimeUnit.milliseconds: |
| default: |
| return '[${_start?.inMilliseconds} ms - ${end?.inMilliseconds} ms]'; |
| } |
| } |
| } |