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
9import 'dart:async';
10import '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].
29typedef 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.
38DebugPrintCallback debugPrint = debugPrintThrottled;
39
40/// Alternative implementation of [debugPrint] that does not throttle.
41/// Used by tests.
42void 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].
54void 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}
65int _debugPrintedCharacters = 0;
66const int _kDebugPrintCapacity = 12 * 1024;
67const Duration _kDebugPrintPauseTime = Duration(seconds: 1);
68final Queue<String> _debugPrintBuffer = Queue<String>();
69final Stopwatch _debugPrintStopwatch = Stopwatch(); // flutter_ignore: stopwatch (see analyze.dart)
70// Ignore context: This is not used in tests, only for throttled logging.
71Completer<void>? _debugPrintCompleter;
72bool _debugPrintScheduled = false;
73void _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).
100Future<void> get debugPrintDone => _debugPrintCompleter?.future ?? Future<void>.value();
101
102final RegExp _indentPattern = RegExp('^ *(?:[-+*] |[0-9]+[.):] )?');
103enum _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.
124Iterable<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