Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reorganize chart pane controller code and prepare structure for offline mode. #7572

Merged
merged 107 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from 106 commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
e522c7e
-
polina-c Apr 4, 2024
ebded8c
Update launch.json
polina-c Apr 4, 2024
7976e00
Merge branch 'master' of github.com:flutter/devtools into offline1
polina-c Apr 4, 2024
3c87709
-
polina-c Apr 4, 2024
ea47bc9
-
polina-c Apr 4, 2024
3bbed40
Update launch.json
polina-c Apr 4, 2024
215ed02
-
polina-c Apr 4, 2024
f5a0acc
Merge branch 'master' of github.com:flutter/devtools into offline2
polina-c Apr 4, 2024
dc0844f
-
polina-c Apr 4, 2024
94ad638
Merge branch 'master' of github.com:flutter/devtools into offline2
polina-c Apr 4, 2024
b6e8225
Update memory_controller.dart
polina-c Apr 4, 2024
b73eb28
Update memory_controller.dart
polina-c Apr 4, 2024
5067dbc
-
polina-c Apr 5, 2024
c31d051
Update memory_controller.dart
polina-c Apr 5, 2024
51a189a
-
polina-c Apr 5, 2024
63daf74
Update DEPENDENCIES.md
polina-c Apr 5, 2024
aeeba2d
-
polina-c Apr 5, 2024
08de780
-
polina-c Apr 5, 2024
372d822
Merge branch 'master' of github.com:flutter/devtools into offline2
polina-c Apr 5, 2024
3ca3cd1
Update memory_controller.dart
polina-c Apr 5, 2024
bcc0246
-
polina-c Apr 6, 2024
f8f29ce
-
polina-c Apr 6, 2024
39b5695
-
polina-c Apr 6, 2024
bcd2514
-
polina-c Apr 7, 2024
0724dba
Update memory_controller.dart
polina-c Apr 8, 2024
741f0c4
Update memory_controller.dart
polina-c Apr 8, 2024
3e28d5c
-
polina-c Apr 8, 2024
f01ced6
Update memory_controller.dart
polina-c Apr 8, 2024
7a29b65
-
polina-c Apr 8, 2024
4ebae9e
Update offline_data.dart
polina-c Apr 8, 2024
0771789
-
polina-c Apr 8, 2024
cf4b341
Update offline_data.dart
polina-c Apr 8, 2024
ba0e76d
Update simple_items.dart
polina-c Apr 8, 2024
2ceab20
Update simple_items.dart
polina-c Apr 8, 2024
b230792
Update memory_controller.dart
polina-c Apr 8, 2024
f64f826
Update memory_controller.dart
polina-c Apr 8, 2024
f1e7e5e
-
polina-c Apr 8, 2024
9601724
-
polina-c Apr 9, 2024
8193e9e
-
polina-c Apr 9, 2024
38bc77a
-
polina-c Apr 9, 2024
dbf826a
Update profile_pane_controller.dart
polina-c Apr 9, 2024
c58ce81
-
polina-c Apr 9, 2024
9ad3f28
-
polina-c Apr 9, 2024
ad3cb08
Update DEPENDENCIES.md
polina-c Apr 9, 2024
0da6798
-
polina-c Apr 9, 2024
9068996
Merge branch 'tracing-data' into offline2
polina-c Apr 9, 2024
d5633af
Update chart_controller.dart
polina-c Apr 9, 2024
0a91214
Merge branch 'master' of github.com:flutter/devtools into mode
polina-c Apr 9, 2024
cbd931f
-
polina-c Apr 9, 2024
ee7503f
Merge branch 'mode' into offline2
polina-c Apr 9, 2024
e9098b1
-
polina-c Apr 9, 2024
64f1961
Update chart_pane_controller.dart
polina-c Apr 9, 2024
c4dac54
Update chart_pane_controller.dart
polina-c Apr 9, 2024
10b62cf
Update chart_pane_controller.dart
polina-c Apr 9, 2024
bd0df00
Update chart_pane_controller.dart
polina-c Apr 9, 2024
b2d8f22
-
polina-c Apr 9, 2024
ce96556
Merge branch 'master' of github.com:flutter/devtools into offline2
polina-c Apr 9, 2024
b8c4f35
Update memory_controller.dart
polina-c Apr 9, 2024
d2a1254
Update memory_controller.dart
polina-c Apr 9, 2024
8f0010b
Update chart_pane_controller.dart
polina-c Apr 9, 2024
6ee54a2
Update chart_pane_controller.dart
polina-c Apr 9, 2024
9b2c45a
Update chart_pane_controller.dart
polina-c Apr 10, 2024
d58cc26
Update chart_pane_controller.dart
polina-c Apr 10, 2024
2aaa8dc
Update chart_pane_controller.dart
polina-c Apr 11, 2024
ef8ce6a
-
polina-c Apr 11, 2024
c91f7c2
-
polina-c Apr 11, 2024
a8eac4e
Merge branch 'clean1' into offline2
polina-c Apr 11, 2024
6b9510e
Update chart_pane_controller.dart
polina-c Apr 11, 2024
a5cfeef
Update chart_pane_controller.dart
polina-c Apr 11, 2024
575497b
-
polina-c Apr 11, 2024
8d4a249
Update memory_tracker.dart
polina-c Apr 11, 2024
fab3a2b
Update memory_tracker.dart
polina-c Apr 11, 2024
ce0ee73
Update memory_tracker.dart
polina-c Apr 11, 2024
3aea57c
-
polina-c Apr 11, 2024
815c66e
-
polina-c Apr 11, 2024
694a1df
Update chart_pane_controller.dart
polina-c Apr 11, 2024
7999d3c
Update chart_control_pane.dart
polina-c Apr 11, 2024
a56cc2e
Update chart_pane_controller.dart
polina-c Apr 11, 2024
f7e3c64
Update chart_pane_controller.dart
polina-c Apr 11, 2024
407fb59
-
polina-c Apr 11, 2024
74d0656
Update chart_pane_controller.dart
polina-c Apr 12, 2024
e869a70
Update chart_pane_controller.dart
polina-c Apr 12, 2024
c4993c5
-
polina-c Apr 12, 2024
e97047d
Update chart_pane_controller.dart
polina-c Apr 12, 2024
3b87715
Update chart_pane_controller.dart
polina-c Apr 12, 2024
a897a1f
-
polina-c Apr 12, 2024
8b35071
-
polina-c Apr 12, 2024
f592d2f
-
polina-c Apr 12, 2024
faab035
-
polina-c Apr 12, 2024
48a43b7
Update chart_pane_controller.dart
polina-c Apr 12, 2024
789c2f7
Update chart_pane_controller.dart
polina-c Apr 12, 2024
1caa0fe
-
polina-c Apr 12, 2024
8ea8e39
Update memory_tracker.dart
polina-c Apr 12, 2024
444fa53
-
polina-c Apr 12, 2024
fb1ffa8
Merge branch 'master' of github.com:flutter/devtools into offline
polina-c Apr 12, 2024
e0093a4
-
polina-c Apr 12, 2024
0ec2767
-
polina-c Apr 12, 2024
b96eb76
-
polina-c Apr 12, 2024
324d0d4
Update screen_body.dart
polina-c Apr 12, 2024
245fad1
Update memory_controller.dart
polina-c Apr 12, 2024
709f425
Update memory_controller.dart
polina-c Apr 12, 2024
b4a4166
Update scene_test_extensions.dart
polina-c Apr 13, 2024
9b8518b
Update memory_screen_test.dart
polina-c Apr 13, 2024
20ebec2
-
polina-c Apr 13, 2024
5fc768d
-
polina-c Apr 13, 2024
5d7d3a5
-
polina-c Apr 14, 2024
8938b89
-
polina-c Apr 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Dependencies that create loops (inversions) are marked with `!`.
```mermaid
flowchart TD;
framework-->panes;
framework-->shared;
panes-->shared;
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Dependencies that create loops (inversions) are marked with `!`.

```mermaid
flowchart TD;
memory_controller.dart-->offline_data;
memory_screen.dart-->screen_body.dart;
memory_tabs.dart-->memory_controller.dart;
screen_body.dart-->memory_controller.dart;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,54 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:devtools_app_shared/utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import '../../../shared/memory/class_name.dart';
import '../../../shared/memory/heap_graph_loader.dart';
import '../../../shared/offline_data.dart';
import '../../../shared/primitives/simple_items.dart';
import '../../../shared/screen.dart';
import '../../../shared/utils.dart';
import '../panes/chart/controller/chart_pane_controller.dart';
import '../panes/control/controller/control_pane_controller.dart';
import '../panes/diff/controller/diff_pane_controller.dart';
import '../panes/profile/profile_pane_controller.dart';
import '../panes/tracing/tracing_pane_controller.dart';
import 'offline_data/offline_data.dart';

/// This class contains the business logic for memory screen, for a connected
/// application.
/// This class contains the business logic for memory screen.
///
/// This class must not have direct dependencies on web-only libraries. This
/// allows tests of the complicated logic in this class to run on the VM.
///
/// The controller should be recreated for every new connection.
class MemoryController extends DisposableController
with AutoDisposeControllerMixin {
with
AutoDisposeControllerMixin,
OfflineScreenControllerMixin<OfflineMemoryData> {
MemoryController({
@visibleForTesting DiffPaneController? diffPaneController,
@visibleForTesting ProfilePaneController? profilePaneController,
@visibleForTesting DiffPaneController? connectedDiff,
@visibleForTesting ProfilePaneController? connectedProfile,
}) {
diff = diffPaneController ?? _createDiffController();
profile = profilePaneController ?? ProfilePaneController();

shareClassFilterBetweenProfileAndDiff();
if (connectedDiff != null || connectedProfile != null) {
_mode = DevToolsMode.connected;
} else {
_mode = devToolsMode;
}
unawaited(_init(connectedDiff, connectedProfile));
}

Future<void> get initialized => _initialized.future;
final _initialized = Completer<void>();

/// DevTools mode at the time of creation of the controller.
///
/// DevTools will recreate controller when the mode changes.
// ignore: unused_field, TODO(polina-c): https://github.com/flutter/devtools/issues/6972
final DevToolsMode _mode = devToolsMode;
late final DevToolsMode _mode;

/// Index of the selected feature tab.
///
Expand All @@ -48,12 +59,15 @@ class MemoryController extends DisposableController
/// instead of the widget state.
int selectedFeatureTabIndex = 0;

late DiffPaneController diff;
late ProfilePaneController profile;
late MemoryChartPaneController chart = MemoryChartPaneController();
TracingPaneController tracing = TracingPaneController();
late final MemoryControlPaneController control =
MemoryControlPaneController(chart.memoryTimeline);
late final DiffPaneController diff;

late final ProfilePaneController profile;

late final MemoryChartPaneController chart;

late final TracingPaneController tracing;

late final MemoryControlPaneController control;

@override
void dispose() {
Expand All @@ -65,26 +79,85 @@ class MemoryController extends DisposableController
profile.dispose();
}

DiffPaneController _createDiffController() =>
DiffPaneController(HeapGraphLoaderRuntime(chart.memoryTimeline));
Future<void> _init(
@visibleForTesting DiffPaneController? connectedDiff,
@visibleForTesting ProfilePaneController? connectedProfile,
) async {
assert(!_initialized.isCompleted);
switch (_mode) {
case DevToolsMode.disconnected:
// TODO: load memory screen in disconnected mode, https://github.com/flutter/devtools/issues/6972
polina-c marked this conversation as resolved.
Show resolved Hide resolved
_initializeData();
case DevToolsMode.connected:
_initializeData(
diffPaneController: connectedDiff,
profilePaneController: connectedProfile,
);
case DevToolsMode.offlineData:
assert(connectedDiff == null && connectedProfile == null);
await maybeLoadOfflineData(
ScreenMetaData.memory.id,
createData: (json) => OfflineMemoryData.parse(json),
shouldLoad: (data) => true,
);
// [maybeLoadOfflineData] will be a noop if there is no offline data for the memory screen,
// so ensure we still call [_initializedData] if it has not been called.
if (!_initialized.isCompleted) _initializeData();
assert(_initialized.isCompleted);
polina-c marked this conversation as resolved.
Show resolved Hide resolved
}
assert(_initialized.isCompleted);
}

void reset() {
diff.dispose();
diff = _createDiffController();
void _initializeData({
OfflineMemoryData? offlineData,
@visibleForTesting DiffPaneController? diffPaneController,
@visibleForTesting ProfilePaneController? profilePaneController,
}) {
assert(!_initialized.isCompleted);
polina-c marked this conversation as resolved.
Show resolved Hide resolved

chart = offlineData?.chart ?? MemoryChartPaneController(_mode);
diff = diffPaneController ??
offlineData?.diff ??
DiffPaneController(
loader: HeapGraphLoaderRuntime(chart.memoryTimeline),
);
profile = profilePaneController ??
offlineData?.profile ??
ProfilePaneController();
control = MemoryControlPaneController(
chart.memoryTimeline,
isChartVisible: chart.isChartVisible,
exportData: exportData,
);
tracing = TracingPaneController();
selectedFeatureTabIndex =
offlineData?.selectedTab ?? selectedFeatureTabIndex;
if (offlineData != null) profile.setFilter(offlineData.filter);
_shareClassFilterBetweenProfileAndDiff();

profile.dispose();
profile = ProfilePaneController();
_initialized.complete();
}

tracing.dispose();
tracing = TracingPaneController();
@override
OfflineScreenData prepareOfflineScreenData() => OfflineScreenData(
screenId: ScreenMetaData.memory.id,
data: OfflineMemoryData(
diff,
profile,
chart,
profile.classFilter.value,
selectedTab: selectedFeatureTabIndex,
).prepareForOffline(),
);

chart.memoryTimeline.reset();
@override
FutureOr<void> processOfflineData(OfflineMemoryData offlineData) {
assert(!_initialized.isCompleted);
_initializeData(offlineData: offlineData);
}

void shareClassFilterBetweenProfileAndDiff() {
diff.derived.applyFilter(
profile.classFilter.value,
);
void _shareClassFilterBetweenProfileAndDiff() {
diff.derived.applyFilter(profile.classFilter.value);

profile.classFilter.addListener(() {
diff.derived.applyFilter(profile.classFilter.value);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2024 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 '../../panes/chart/controller/chart_pane_controller.dart';
import '../../panes/diff/controller/diff_pane_controller.dart';
import '../../panes/profile/profile_pane_controller.dart';
import '../../shared/heap/class_filter.dart';

class _Json {
static const selectedTab = 'selectedTab';
static const classFilter = 'classFilter';
static const diffData = 'diffData';
static const profileData = 'profileData';
static const chartData = 'chartData';
}

class OfflineMemoryData {
OfflineMemoryData(
this.diff,
this.profile,
this.chart,
this.filter, {
required this.selectedTab,
});

// TODO: use an extension type for the Json parsing, https://github.com/flutter/devtools/issues/6972
polina-c marked this conversation as resolved.
Show resolved Hide resolved
// https://github.com/flutter/devtools/pull/7572#discussion_r1563102256
factory OfflineMemoryData.parse(Map<String, dynamic> json) {
Map<String, dynamic> item(String key) =>
polina-c marked this conversation as resolved.
Show resolved Hide resolved
json[key] as Map<String, dynamic>? ?? {};
return OfflineMemoryData(
DiffPaneController.parse(item(_Json.diffData)),
ProfilePaneController.parse(item(_Json.profileData)),
MemoryChartPaneController.parse(item(_Json.chartData)),
ClassFilter.parse(item(_Json.classFilter)),
selectedTab: json[_Json.selectedTab] as int? ?? 0,
);
}

final int selectedTab;
final ClassFilter filter; // filter is shared between tabs, so it's here

final DiffPaneController diff;
final ProfilePaneController profile;
final MemoryChartPaneController chart;

Map<String, dynamic> prepareForOffline() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Object? instead of dynamic

nit: how about naming toJson for consistency with the rest of the codebase? This can also be a getter `Map<String, Object?> get toJson => {...};

Copy link
Contributor Author

@polina-c polina-c Apr 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is consistent with OfflineMemoryData.

I would update both with what is recommended for serialization: https://docs.flutter.dev/data-and-backend/serialization/json#serializing-json-inside-model-classes

And we can make them extensions.

How does it sound?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If is recommended to never use dynamic. We should be using Object? instead. These have different implications in the compiler and for type checking. Couldn't find official docs but here's an issue with some discussion on this topic: dart-lang/language#3192

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to do it as separate clean up, including OfflineMemoryData.parse(Map<String, dynamic> json) and others.
I expect unexpected side effects that may cause us to change our mind here. I remember trying to use Object for json instead of dynamic and facing some difficulties that convinced me to stay with standard for json.

Serialization is a specific mechanism that picks up all objects that meet this standard: https://docs.flutter.dev/data-and-backend/serialization/json#serializing-json-inside-model-classes

This mechanism may break with switch to Object.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@srawlins do you know if the linked docs above are up to date? I know you've done a pretty large overhaul of all the JSON parsing we were doing in DevTools using extension types. Can you advise here WRT using dynamic vs Object??

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the docs are still accurate. Anything produced by dart:convert uses dynamic (e.g. List<dynamic> and Map<String, dynamic> or maybe even Map<dynamic, dynamic>) instead of Object?.

We can also reference the general recommendation to avoid dynamic, and I stick strongly to that. I avoid dynamic anywhere it isn't necessary. So then then question is: when working with objects produced by dart:convert, what should we use? I think Object? is safe everywhere here, but I actually haven't worked through the edge cases, etc. I can't promise it, so I think I used dynamic in a lot of my devtools PRs, just for consistency.

The runtime danger is that the objects returned from dart:convert don't "promote" types of objects when you might expect it. For example, if you call x = jsonDecode('["hello", "there"]');, you get a List<dynamic> thing at runtime, and you might think "well, I know it's always a List of Strings, so I'll use x as List<String>." That is ok at compile-time, but will throw at runtime. You might try if (x is List<String>) as defensive, but now that condition will never be true at runtime. It's super awkward but you have to do if (x is List) x.cast<String>() (which could still throw if you have bad data) or (if (x is List) for (var y in x) if (y is String) ...). Super tedious.

Back to Object? vs dynamic. Either of these will work, I think all the time, but I'm not sure. E.g.

import 'dart:convert';

void main() {
  var b = jsonDecode('[1, "hello"]');
  if (b is List<dynamic>) {
    print('yay a List<dynamic>');
  }
  if (b is List<Object?>) {
    print('yay a List<Object?>');
  }
  if (b is List<Object>) {
    print('yay a List');
  }
  f1();
  f2();
  f3();
}

List<dynamic> f1() => jsonDecode('[1, "hello"]');
List<Object?> f2() => jsonDecode('[1, "hello"]');
List<Object> f3() => jsonDecode('[1, "hello"]');

This prints "yay a List" and "yay a List<Object?>" because of how is-checks work (assignability?). But not "yay a List". That test fails. And the calls to f1 and f2 pass, but the call to f3 throws an exception, because the returned object is not a List<Object>.

I think guidance w.r.t. dynamic-and-JSON is a team-by-team decision. We can't all become type-system experts and JSON experts (like I can never if JSON explicitly does not support a null concept, or does, or if Dart does something special...). So whatever consistent policy keeps the team writing safe code and understandable (for the team) code, is a good policy. I don't think analyzer team has a consistent policy, and we definitely could have JSON-parsing bugs, or YAML-parsing bugs, in malformed analysis_options.yaml, or package_config.json, etc. It's something I keep punting on, hoping dart:convert will just change to returning Object? objects instead of dynamic, or maybe a better JSON-parsing story. 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I suggest to stay compliant with public recommendation.
If we want to invest into switch to <String, Object?>, we need to reflect it in our style rules and migrate the entire project at once, to keep it consistent.

return {
_Json.selectedTab: selectedTab,
_Json.diffData: diff.prepareForOffline(),
_Json.profileData: profile.prepareForOffline(),
_Json.chartData: chart.prepareForOffline(),
_Json.classFilter: profile.classFilter.value.prepareForOffline(),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,32 @@ class _ConnectedMemoryBodyState extends State<ConnectedMemoryBody>

@override
Widget build(BuildContext context) {
return Column(
key: MemoryChartPane.hoverKey,
children: [
MemoryControlPane(
controller: controller.control,
),
const SizedBox(height: intermediateSpacing),
MemoryChartPane(
chart: controller.chart,
keyFocusNode: _focusNode,
),
Expanded(
child: MemoryTabView(memoryController),
),
],
return FutureBuilder<void>(
future: memoryController.initialized,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Column(
key: MemoryChartPane.hoverKey,
children: [
MemoryControlPane(
controller: controller.control,
),
const SizedBox(height: intermediateSpacing),
MemoryChartPane(
chart: controller.chart,
keyFocusNode: _focusNode,
),
Expanded(
child: MemoryTabView(memoryController),
),
],
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
polina-c marked this conversation as resolved.
Show resolved Hide resolved
}
},
);
}
}
Loading
Loading