1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | // This file implements debugPrint in terms of print, so avoiding |
6 | // calling "print" is sort of a non-starter here... |
7 | // ignore_for_file: avoid_print |
8 | |
9 | import 'dart:async'; |
10 | import 'dart:collection'; |
11 | |
12 | /// Signature for [debugPrint] implementations. |
13 | /// |
14 | /// If a [wrapWidth] is provided, each line of the [message] is word-wrapped to |
15 | /// that width. (Lines may be separated by newline characters, as in '\n'.) |
16 | /// |
17 | /// By default, this function very crudely attempts to throttle the rate at |
18 | /// which messages are sent to avoid data loss on Android. This means that |
19 | /// interleaving calls to this function (directly or indirectly via, e.g., |
20 | /// [debugDumpRenderTree] or [debugDumpApp]) and to the Dart [print] method can |
21 | /// result in out-of-order messages in the logs. |
22 | /// |
23 | /// The implementation of this function can be replaced by setting the |
24 | /// [debugPrint] variable to a new implementation that matches the |
25 | /// [DebugPrintCallback] signature. For example, flutter_test does this. |
26 | /// |
27 | /// The default value is [debugPrintThrottled]. For a version that acts |
28 | /// identically but does not throttle, use [debugPrintSynchronously]. |
29 | typedef DebugPrintCallback = void Function(String? message, { int? wrapWidth }); |
30 | |
31 | /// Prints a message to the console, which you can access using the "flutter" |
32 | /// tool's "logs" command ("flutter logs"). |
33 | /// |
34 | /// See also: |
35 | /// |
36 | /// * [DebugPrintCallback], for function parameters and usage details. |
37 | /// * [debugPrintThrottled], the default implementation. |
38 | DebugPrintCallback debugPrint = debugPrintThrottled; |
39 | |
40 | /// Alternative implementation of [debugPrint] that does not throttle. |
41 | /// Used by tests. |
42 | void debugPrintSynchronously(String? message, { int? wrapWidth }) { |
43 | if (message != null && wrapWidth != null) { |
44 | print(message.split('\n' ).expand<String>((String line) => debugWordWrap(line, wrapWidth)).join('\n' )); |
45 | } else { |
46 | print(message); |
47 | } |
48 | } |
49 | |
50 | /// Implementation of [debugPrint] that throttles messages. This avoids dropping |
51 | /// messages on platforms that rate-limit their logging (for example, Android). |
52 | /// |
53 | /// If `wrapWidth` is not null, the message is wrapped using [debugWordWrap]. |
54 | void debugPrintThrottled(String? message, { int? wrapWidth }) { |
55 | final List<String> messageLines = message?.split('\n' ) ?? <String>['null' ]; |
56 | if (wrapWidth != null) { |
57 | _debugPrintBuffer.addAll(messageLines.expand<String>((String line) => debugWordWrap(line, wrapWidth))); |
58 | } else { |
59 | _debugPrintBuffer.addAll(messageLines); |
60 | } |
61 | if (!_debugPrintScheduled) { |
62 | _debugPrintTask(); |
63 | } |
64 | } |
65 | int _debugPrintedCharacters = 0; |
66 | const int _kDebugPrintCapacity = 12 * 1024; |
67 | const Duration _kDebugPrintPauseTime = Duration(seconds: 1); |
68 | final Queue<String> _debugPrintBuffer = Queue<String>(); |
69 | final Stopwatch _debugPrintStopwatch = Stopwatch(); // flutter_ignore: stopwatch (see analyze.dart) |
70 | // Ignore context: This is not used in tests, only for throttled logging. |
71 | Completer<void>? _debugPrintCompleter; |
72 | bool _debugPrintScheduled = false; |
73 | void _debugPrintTask() { |
74 | _debugPrintScheduled = false; |
75 | if (_debugPrintStopwatch.elapsed > _kDebugPrintPauseTime) { |
76 | _debugPrintStopwatch.stop(); |
77 | _debugPrintStopwatch.reset(); |
78 | _debugPrintedCharacters = 0; |
79 | } |
80 | while (_debugPrintedCharacters < _kDebugPrintCapacity && _debugPrintBuffer.isNotEmpty) { |
81 | final String line = _debugPrintBuffer.removeFirst(); |
82 | _debugPrintedCharacters += line.length; // TODO(ianh): Use the UTF-8 byte length instead |
83 | print(line); |
84 | } |
85 | if (_debugPrintBuffer.isNotEmpty) { |
86 | _debugPrintScheduled = true; |
87 | _debugPrintedCharacters = 0; |
88 | Timer(_kDebugPrintPauseTime, _debugPrintTask); |
89 | _debugPrintCompleter ??= Completer<void>(); |
90 | } else { |
91 | _debugPrintStopwatch.start(); |
92 | _debugPrintCompleter?.complete(); |
93 | _debugPrintCompleter = null; |
94 | } |
95 | } |
96 | |
97 | /// A Future that resolves when there is no longer any buffered content being |
98 | /// printed by [debugPrintThrottled] (which is the default implementation for |
99 | /// [debugPrint], which is used to report errors to the console). |
100 | Future<void> get debugPrintDone => _debugPrintCompleter?.future ?? Future<void>.value(); |
101 | |
102 | final RegExp _indentPattern = RegExp('^ *(?:[-+*] |[0-9]+[.):] )?' ); |
103 | enum _WordWrapParseMode { inSpace, inWord, atBreak } |
104 | |
105 | /// Wraps the given string at the given width. |
106 | /// |
107 | /// The `message` should not contain newlines (`\n`, U+000A). Strings that may |
108 | /// contain newlines should be [String.split] before being wrapped. |
109 | /// |
110 | /// Wrapping occurs at space characters (U+0020). Lines that start with an |
111 | /// octothorpe ("#", U+0023) are not wrapped (so for example, Dart stack traces |
112 | /// won't be wrapped). |
113 | /// |
114 | /// Subsequent lines attempt to duplicate the indentation of the first line, for |
115 | /// example if the first line starts with multiple spaces. In addition, if a |
116 | /// `wrapIndent` argument is provided, each line after the first is prefixed by |
117 | /// that string. |
118 | /// |
119 | /// This is not suitable for use with arbitrary Unicode text. For example, it |
120 | /// doesn't implement UAX #14, can't handle ideographic text, doesn't hyphenate, |
121 | /// and so forth. It is only intended for formatting error messages. |
122 | /// |
123 | /// The default [debugPrint] implementation uses this for its line wrapping. |
124 | Iterable<String> debugWordWrap(String message, int width, { String wrapIndent = '' }) { |
125 | if (message.length < width || message.trimLeft()[0] == '#' ) { |
126 | return <String>[message]; |
127 | } |
128 | final List<String> wrapped = <String>[]; |
129 | final Match prefixMatch = _indentPattern.matchAsPrefix(message)!; |
130 | final String prefix = wrapIndent + ' ' * prefixMatch.group(0)!.length; |
131 | int start = 0; |
132 | int startForLengthCalculations = 0; |
133 | bool addPrefix = false; |
134 | int index = prefix.length; |
135 | _WordWrapParseMode mode = _WordWrapParseMode.inSpace; |
136 | late int lastWordStart; |
137 | int? lastWordEnd; |
138 | while (true) { |
139 | switch (mode) { |
140 | case _WordWrapParseMode.inSpace: // at start of break point (or start of line); can't break until next break |
141 | while ((index < message.length) && (message[index] == ' ' )) { |
142 | index += 1; |
143 | } |
144 | lastWordStart = index; |
145 | mode = _WordWrapParseMode.inWord; |
146 | case _WordWrapParseMode.inWord: // looking for a good break point |
147 | while ((index < message.length) && (message[index] != ' ' )) { |
148 | index += 1; |
149 | } |
150 | mode = _WordWrapParseMode.atBreak; |
151 | case _WordWrapParseMode.atBreak: // at start of break point |
152 | if ((index - startForLengthCalculations > width) || (index == message.length)) { |
153 | // we are over the width line, so break |
154 | if ((index - startForLengthCalculations <= width) || (lastWordEnd == null)) { |
155 | // we should use this point, because either it doesn't actually go over the |
156 | // end (last line), or it does, but there was no earlier break point |
157 | lastWordEnd = index; |
158 | } |
159 | if (addPrefix) { |
160 | wrapped.add(prefix + message.substring(start, lastWordEnd)); |
161 | } else { |
162 | wrapped.add(message.substring(start, lastWordEnd)); |
163 | addPrefix = true; |
164 | } |
165 | if (lastWordEnd >= message.length) { |
166 | return wrapped; |
167 | } |
168 | // just yielded a line |
169 | if (lastWordEnd == index) { |
170 | // we broke at current position |
171 | // eat all the spaces, then set our start point |
172 | while ((index < message.length) && (message[index] == ' ' )) { |
173 | index += 1; |
174 | } |
175 | start = index; |
176 | mode = _WordWrapParseMode.inWord; |
177 | } else { |
178 | // we broke at the previous break point, and we're at the start of a new one |
179 | assert(lastWordStart > lastWordEnd); |
180 | start = lastWordStart; |
181 | mode = _WordWrapParseMode.atBreak; |
182 | } |
183 | startForLengthCalculations = start - prefix.length; |
184 | assert(addPrefix); |
185 | lastWordEnd = null; |
186 | } else { |
187 | // save this break point, we're not yet over the line width |
188 | lastWordEnd = index; |
189 | // skip to the end of this break point |
190 | mode = _WordWrapParseMode.inSpace; |
191 | } |
192 | } |
193 | } |
194 | } |
195 | |