Skip to content

Commit 8062a22

Browse files
Improve startup performance of DevTools by using lazy initialization for debugger and console service (flutter#3468)
1 parent 5010941 commit 8062a22

18 files changed

Lines changed: 260 additions & 104 deletions

packages/devtools_app/lib/src/console_service.dart

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,21 @@ class ConsoleService extends Disposer {
151151
// will grow to kMaxLogItemsUpperBound then truncate to
152152
// kMaxLogItemsLowerBound.
153153
if (_stdio.value.length > kMaxLogItemsUpperBound) {
154-
_stdio.trimToSublist(stdio.value.length - kMaxLogItemsLowerBound);
154+
_stdio.trimToSublist(_stdio.value.length - kMaxLogItemsLowerBound);
155155
}
156156
}
157157

158158
/// Return the stdout and stderr emitted from the application.
159159
///
160160
/// Note that this output might be truncated after significant output.
161-
ValueListenable<List<ConsoleLine>> get stdio => _stdio;
161+
ValueListenable<List<ConsoleLine>> get stdio {
162+
assert(
163+
_serviceInitialized,
164+
'`ConsoleService.ensureServiceInitialized` must be called before '
165+
'interacting with the ConsoleService.',
166+
);
167+
return _stdio;
168+
}
162169

163170
void _handleStdoutEvent(Event event) {
164171
final String text = decodeBase64(event.bytes);
@@ -174,16 +181,42 @@ class ConsoleService extends Disposer {
174181

175182
void vmServiceOpened(VmServiceWrapper service) {
176183
cancel();
184+
// The debug stream listener must be added as soon as the service is opened
185+
// because this stream does not send event history upon the first
186+
// subscription like the streams in [ensureServiceInitialized].
177187
autoDispose(service.onDebugEvent.listen(_handleDebugEvent));
178-
autoDispose(service.onStdoutEventWithHistory.listen(_handleStdoutEvent));
179-
autoDispose(service.onStderrEventWithHistory.listen(_handleStderrEvent));
180-
autoDispose(
181-
service.onExtensionEventWithHistory.listen(_handleExtensionEvent));
182188
addAutoDisposeListener(serviceManager.isolateManager.mainIsolate, () {
183189
clearStdio();
184190
});
185191
}
186192

193+
/// Whether the console service has been initialized.
194+
bool _serviceInitialized = false;
195+
196+
/// Initialize the console service.
197+
///
198+
/// Consumers of [ConsoleService] should call this method before using the
199+
/// console service in any way.
200+
///
201+
/// These stream listeners are added here instead of in [vmServiceOpened] for
202+
/// performance reasons. Since these streams have event history, we will not
203+
/// be missing any events by listening after [vmServiceOpened], and listening
204+
/// only when this data is needed will improve performance for connecting to
205+
/// low-end devices, as well as when DevTools pages that don't need the
206+
/// [ConsoleService] are being used.
207+
void ensureServiceInitialized() {
208+
assert(serviceManager.isServiceAvailable);
209+
if (!_serviceInitialized && serviceManager.isServiceAvailable) {
210+
autoDispose(serviceManager.service.onStdoutEventWithHistory
211+
.listen(_handleStdoutEvent));
212+
autoDispose(serviceManager.service.onStderrEventWithHistory
213+
.listen(_handleStderrEvent));
214+
autoDispose(serviceManager.service.onExtensionEventWithHistory
215+
.listen(_handleExtensionEvent));
216+
_serviceInitialized = true;
217+
}
218+
}
219+
187220
void _handleExtensionEvent(Event e) async {
188221
if (e.extensionKind == 'Flutter.Error' ||
189222
e.extensionKind == 'Flutter.Print') {
@@ -210,6 +243,7 @@ class ConsoleService extends Disposer {
210243

211244
void handleVmServiceClosed() {
212245
cancel();
246+
_serviceInitialized = false;
213247
}
214248

215249
void _handleDebugEvent(Event event) async {

packages/devtools_app/lib/src/debugger/console.dart

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import 'evaluate.dart';
1717
// TODO(devoncarew): Show some small UI indicator when we receive stdout/stderr.
1818

1919
/// Display the stdout and stderr output from the process under debug.
20-
class DebuggerConsole extends StatelessWidget {
20+
class DebuggerConsole extends StatefulWidget {
2121
const DebuggerConsole({
2222
Key key,
2323
}) : super(key: key);
@@ -26,9 +26,39 @@ class DebuggerConsole extends StatelessWidget {
2626
Key('debugger_console_copy_to_clipboard_button');
2727
static const clearStdioButtonKey = Key('debugger_console_clear_stdio_button');
2828

29+
@override
30+
State<DebuggerConsole> createState() => _DebuggerConsoleState();
31+
32+
static PreferredSizeWidget buildHeader() {
33+
return AreaPaneHeader(
34+
title: const Text('Console'),
35+
needsTopBorder: false,
36+
rightActions: [
37+
CopyToClipboardControl(
38+
dataProvider: () =>
39+
serviceManager.consoleService.stdio.value?.join('\n') ?? '',
40+
buttonKey: DebuggerConsole.copyToClipboardButtonKey,
41+
),
42+
DeleteControl(
43+
buttonKey: DebuggerConsole.clearStdioButtonKey,
44+
tooltip: 'Clear console output',
45+
onPressed: () => serviceManager.consoleService.clearStdio(),
46+
),
47+
],
48+
);
49+
}
50+
}
51+
52+
class _DebuggerConsoleState extends State<DebuggerConsole> {
2953
ValueListenable<List<ConsoleLine>> get stdio =>
3054
serviceManager.consoleService.stdio;
3155

56+
@override
57+
void initState() {
58+
super.initState();
59+
serviceManager.consoleService.ensureServiceInitialized();
60+
}
61+
3262
@override
3363
Widget build(BuildContext context) {
3464
return Column(
@@ -48,23 +78,4 @@ class DebuggerConsole extends StatelessWidget {
4878
],
4979
);
5080
}
51-
52-
static PreferredSizeWidget buildHeader() {
53-
return AreaPaneHeader(
54-
title: const Text('Console'),
55-
needsTopBorder: false,
56-
rightActions: [
57-
CopyToClipboardControl(
58-
dataProvider: () =>
59-
serviceManager.consoleService.stdio.value?.join('\n') ?? '',
60-
buttonKey: DebuggerConsole.copyToClipboardButtonKey,
61-
),
62-
DeleteControl(
63-
buttonKey: DebuggerConsole.clearStdioButtonKey,
64-
tooltip: 'Clear console output',
65-
onPressed: () => serviceManager.consoleService.clearStdio(),
66-
),
67-
],
68-
);
69-
}
7081
}

packages/devtools_app/lib/src/debugger/debugger_controller.dart

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,39 @@ class DebuggerController extends DisposableController
3939
);
4040
autoDispose(serviceManager.onConnectionAvailable
4141
.listen(_handleConnectionAvailable));
42+
if (_service != null) {
43+
initialize();
44+
}
4245
_scriptHistoryListener = () {
4346
_showScriptLocation(ScriptLocation(scriptsHistory.current.value));
4447
};
4548
scriptsHistory.current.addListener(_scriptHistoryListener);
46-
addAutoDisposeListener(currentScriptRef, () {
47-
if (!programExplorerController.initialized.value) {
48-
programExplorerController.initialize();
49-
}
50-
if (currentScriptRef.value != null) {
51-
programExplorerController.selectScriptNode(currentScriptRef.value);
52-
}
53-
});
49+
}
5450

55-
if (_service != null) {
56-
initialize();
51+
bool _firstDebuggerScreenLoaded = false;
52+
53+
/// Callback to be called when the debugger screen is first loaded.
54+
///
55+
/// We delay calling this method until the debugger screen is first loaded
56+
/// for performance reasons. None of the code here needs to be called when
57+
/// DevTools first connects to an app, and doing so inhibits DevTools from
58+
/// connecting to low-end devices.
59+
void onFirstDebuggerScreenLoad() {
60+
if (!_firstDebuggerScreenLoaded) {
61+
_maybeSetUpProgramExplorer();
62+
addAutoDisposeListener(currentScriptRef, _maybeSetUpProgramExplorer);
63+
_firstDebuggerScreenLoaded = true;
64+
}
65+
}
66+
67+
void _maybeSetUpProgramExplorer() {
68+
if (!programExplorerController.initialized.value) {
69+
programExplorerController
70+
..initListeners()
71+
..initialize();
72+
}
73+
if (currentScriptRef.value != null) {
74+
programExplorerController.selectScriptNode(currentScriptRef.value);
5775
}
5876
}
5977

@@ -82,6 +100,7 @@ class DebuggerController extends DisposableController
82100
_selectedBreakpoint.value = null;
83101
_librariesVisible.value = false;
84102
isolateRef = null;
103+
_firstDebuggerScreenLoaded = false;
85104
}
86105

87106
VmServiceWrapper _lastService;

packages/devtools_app/lib/src/debugger/debugger_screen.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class DebuggerScreenBodyState extends State<DebuggerScreenBody>
116116
final newController = Provider.of<DebuggerController>(context);
117117
if (newController == controller) return;
118118
controller = newController;
119+
controller.onFirstDebuggerScreenLoad();
119120
}
120121

121122
void _onLocationSelected(ScriptLocation location) {

packages/devtools_app/lib/src/debugger/evaluate.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class _ExpressionEvalFieldState extends State<ExpressionEvalField>
4242
void initState() {
4343
super.initState();
4444

45+
serviceManager.consoleService.ensureServiceInitialized();
46+
4547
_autoCompleteController = AutoCompleteController();
4648

4749
addAutoDisposeListener(_autoCompleteController.searchNotifier, () {
@@ -54,9 +56,13 @@ class _ExpressionEvalFieldState extends State<ExpressionEvalField>
5456
);
5557
});
5658
addAutoDisposeListener(
57-
_autoCompleteController.selectTheSearchNotifier, _handleSearch);
59+
_autoCompleteController.selectTheSearchNotifier,
60+
_handleSearch,
61+
);
5862
addAutoDisposeListener(
59-
_autoCompleteController.searchNotifier, _handleSearch);
63+
_autoCompleteController.searchNotifier,
64+
_handleSearch,
65+
);
6066
}
6167

6268
void _handleSearch() async {

packages/devtools_app/lib/src/debugger/program_explorer.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ class _ProgramExplorerRow extends StatelessWidget {
6262
text,
6363
maxLines: 1,
6464
overflow: TextOverflow.ellipsis,
65-
style: theme.fixedFontStyle,
65+
style: theme.fixedFontStyle.copyWith(
66+
color: node.isSelected
67+
? Colors.white
68+
: theme.fixedFontStyle.color,
69+
),
6670
),
6771
),
6872
],
@@ -320,7 +324,6 @@ class _ProgramOutlineView extends StatelessWidget {
320324

321325
@override
322326
Widget build(BuildContext context) {
323-
print(controller.isLoadingOutline);
324327
return ValueListenableBuilder<bool>(
325328
valueListenable: controller.isLoadingOutline,
326329
builder: (context, isLoadingOutline, _) {

packages/devtools_app/lib/src/debugger/program_explorer_controller.dart

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,7 @@ import 'program_explorer_model.dart';
1515

1616
class ProgramExplorerController extends DisposableController
1717
with AutoDisposeControllerMixin {
18-
ProgramExplorerController({@required this.debuggerController}) {
19-
addAutoDisposeListener(
20-
serviceManager.isolateManager.selectedIsolate,
21-
refresh,
22-
);
23-
// Re-initialize after reload.
24-
addAutoDisposeListener(
25-
debuggerController.sortedScripts,
26-
refresh,
27-
);
28-
}
18+
ProgramExplorerController({@required this.debuggerController});
2919

3020
/// The outline view nodes for the currently selected library.
3121
ValueListenable<List<VMServiceObjectNode>> get outlineNodes => _outlineNodes;
@@ -68,6 +58,7 @@ class ProgramExplorerController extends DisposableController
6858
return;
6959
}
7060
_initializing = true;
61+
7162
_isolate = serviceManager.isolateManager.selectedIsolate.value;
7263
final libraries = _isolate != null
7364
? serviceManager.isolateManager
@@ -85,6 +76,18 @@ class ProgramExplorerController extends DisposableController
8576
_initialized.value = true;
8677
}
8778

79+
void initListeners() {
80+
addAutoDisposeListener(
81+
serviceManager.isolateManager.selectedIsolate,
82+
refresh,
83+
);
84+
// Re-initialize after reload.
85+
addAutoDisposeListener(
86+
debuggerController.sortedScripts,
87+
refresh,
88+
);
89+
}
90+
8891
void selectScriptNode(ScriptRef script) {
8992
if (!initialized.value) {
9093
return;

packages/devtools_app/lib/src/inspector/inspector_controller.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ class InspectorController extends DisposableController
143143
autoDispose(
144144
serviceManager.onConnectionClosed.listen(_handleConnectionStop),
145145
);
146+
147+
serviceManager.consoleService.ensureServiceInitialized();
146148
}
147149

148150
void _handleConnectionStart(VmService service) {

packages/devtools_app/lib/src/routing.dart

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
88
import 'package:flutter/material.dart';
99

1010
import 'globals.dart';
11+
import 'utils.dart';
1112

1213
/// The page ID (used in routing) for the standalone app-size page.
1314
///
@@ -31,10 +32,27 @@ class DevToolsRouteConfiguration {
3132
/// in the address bar/state objects).
3233
class DevToolsRouteInformationParser
3334
extends RouteInformationParser<DevToolsRouteConfiguration> {
35+
DevToolsRouteInformationParser();
36+
37+
@visibleForTesting
38+
DevToolsRouteInformationParser.test(this._forceVmServiceUri);
39+
40+
/// The value for the 'uri' query parameter in a DevTools uri.
41+
///
42+
/// This is to be used in a testing environment only and can be set via the
43+
/// [DevToolsRouteInformationParser.test] constructor.
44+
String _forceVmServiceUri;
45+
3446
@override
3547
Future<DevToolsRouteConfiguration> parseRouteInformation(
3648
RouteInformation routeInformation) {
37-
final uri = Uri.parse(routeInformation.location);
49+
var uri = Uri.parse(routeInformation.location);
50+
51+
if (_forceVmServiceUri != null) {
52+
final newQueryParams = Map<String, dynamic>.from(uri.queryParameters);
53+
newQueryParams['uri'] = _forceVmServiceUri;
54+
uri = uri.copyWith(queryParameters: newQueryParams);
55+
}
3856

3957
// If the uri has been modified and we do not have a vm service uri as a
4058
// query parameter, ensure we manually disconnect from any previously

packages/devtools_app/lib/src/scaffold.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ class DevToolsScaffoldState extends State<DevToolsScaffold>
360360
children: tabBodies,
361361
),
362362
if (serviceManager.connectedAppInitialized &&
363+
!serviceManager.connectedApp.isProfileBuildNow &&
363364
!offlineController.offlineMode.value &&
364365
_currentScreen.showFloatingDebuggerControls)
365366
Container(
@@ -391,6 +392,7 @@ class DevToolsScaffoldState extends State<DevToolsScaffold>
391392
child: Scaffold(
392393
appBar: widget.embed ? null : _buildAppBar(scaffoldTitle),
393394
body: (serviceManager.connectedAppInitialized &&
395+
!serviceManager.connectedApp.isProfileBuildNow &&
394396
!offlineController.offlineMode.value &&
395397
_currentScreen.showConsole(widget.embed))
396398
? Split(

0 commit comments

Comments
 (0)