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