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

Initial structure for offline memory screen. #5965

Merged
merged 22 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions packages/devtools_app/lib/devtools_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export 'src/screens/inspector/inspector_screen.dart';
export 'src/screens/inspector/inspector_tree_controller.dart';
export 'src/screens/logging/logging_controller.dart';
export 'src/screens/logging/logging_screen.dart';
export 'src/screens/memory/memory_controller.dart';
export 'src/screens/memory/memory_screen.dart';
export 'src/screens/memory/framework/connected/memory_controller.dart';
export 'src/screens/memory/framework/memory_screen.dart';
export 'src/screens/network/network_controller.dart';
export 'src/screens/network/network_model.dart';
export 'src/screens/network/network_screen.dart';
Expand Down
17 changes: 15 additions & 2 deletions packages/devtools_app/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import 'screens/inspector/inspector_screen.dart';
import 'screens/inspector/inspector_tree_controller.dart';
import 'screens/logging/logging_controller.dart';
import 'screens/logging/logging_screen.dart';
import 'screens/memory/memory_controller.dart';
import 'screens/memory/memory_screen.dart';
import 'screens/memory/framework/connected/memory_controller.dart';
import 'screens/memory/framework/memory_screen.dart';
import 'screens/memory/framework/static/static_screen_body.dart';
import 'screens/network/network_controller.dart';
import 'screens/network/network_screen.dart';
import 'screens/performance/performance_controller.dart';
Expand Down Expand Up @@ -290,6 +291,18 @@ class DevToolsAppState extends State<DevToolsApp> with AutoDisposeMixin {
),
);
},
if (FeatureFlags.memoryAnalysis)
memoryAnalysisScreenId: (_, __, args, ____) {
final embed = isEmbedded(args);
return DevToolsScaffold.withChild(
key: const Key('memoryanalysis'),
embed: embed,
child: MultiProvider(
providers: _providedControllers(),
child: const StaticMemoryBody(),
),
);
},
if (FeatureFlags.vsCodeSidebarTooling) ..._standaloneScreens,
};
}
Expand Down
47 changes: 47 additions & 0 deletions packages/devtools_app/lib/src/framework/landing_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import '../shared/analytics/analytics.dart' as ga;
import '../shared/analytics/constants.dart' as gac;
import '../shared/common_widgets.dart';
import '../shared/config_specific/import_export/import_export.dart';
import '../shared/feature_flags.dart';
import '../shared/file_import.dart';
import '../shared/globals.dart';
import '../shared/primitives/blocking_action_mixin.dart';
Expand Down Expand Up @@ -56,6 +57,10 @@ class _LandingScreenBodyState extends State<LandingScreenBody> {
ImportFileInstructions(sampleData: widget.sampleData),
const SizedBox(height: defaultSpacing),
const AppSizeToolingInstructions(),
if (FeatureFlags.memoryAnalysis) ...[
const SizedBox(height: defaultSpacing),
const MemoryAnalysisInstructions(),
],
],
),
);
Expand Down Expand Up @@ -352,6 +357,48 @@ class AppSizeToolingInstructions extends StatelessWidget {
}
}

@visibleForTesting
class MemoryAnalysisInstructions extends StatelessWidget {
const MemoryAnalysisInstructions({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return LandingScreenSection(
title: 'Memory Analysis',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Analyze and diff the saved memory snapshots',
style: textTheme.titleMedium,
),
const SizedBox(height: denseRowSpacing),
Text(
// TODO(polina-c): make package:leak_tracker a link.
// https://github.com/flutter/devtools/issues/5606
'Analyze heap snapshots that were previously saved from DevTools or package:leak_tracker.',
style: textTheme.bodySmall,
),
const SizedBox(height: defaultSpacing),
ElevatedButton(
child: const Text('Open memory analysis tool'),
onPressed: () => _onOpen(context),
),
],
),
);
}

void _onOpen(BuildContext context) {
ga.select(
gac.landingScreen,
gac.openMemoryAnalysisTool,
);
DevToolsRouterDelegate.of(context).navigate(memoryAnalysisScreenId);
}
}

class SampleDataDropDownButton extends StatefulWidget {
const SampleDataDropDownButton({
super.key,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This folder contains code not used by memory panes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2023 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 'package:flutter/material.dart';

import '../../../../shared/banner_messages.dart';
import '../../../../shared/primitives/auto_dispose.dart';
import '../../../../shared/primitives/simple_items.dart';
import '../../../../shared/theme.dart';
import '../../../../shared/utils.dart';
import '../../panes/chart/chart_pane.dart';
import '../../panes/chart/chart_pane_controller.dart';
import '../../panes/chart/memory_android_chart.dart';
import '../../panes/chart/memory_events_pane.dart';
import '../../panes/chart/memory_vm_chart.dart';
import '../../panes/control/control_pane.dart';
import 'memory_controller.dart';
import 'memory_tabs.dart';

class ConnectedMemoryBody extends StatefulWidget {
const ConnectedMemoryBody({super.key});

@override
State<ConnectedMemoryBody> createState() => _ConnectedMemoryBodyState();
}

class _ConnectedMemoryBodyState extends State<ConnectedMemoryBody>
with
AutoDisposeMixin,
SingleTickerProviderStateMixin,
ProvidedControllerMixin<MemoryController, ConnectedMemoryBody> {
MemoryController get memoryController => controller;

late MemoryChartPaneController _chartController;

final _focusNode = FocusNode(debugLabel: 'memory');

@override
void initState() {
super.initState();
autoDisposeFocusNode(_focusNode);
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
maybePushDebugModeMemoryMessage(context, ScreenMetaData.memory.id);
if (!initController()) return;

final vmChartController = VMChartController(controller);
_chartController = MemoryChartPaneController(
event: EventChartController(controller),
vm: vmChartController,
android: AndroidChartController(
controller,
sharedLabels: vmChartController.labelTimestamps,
),
);
}

@override
Widget build(BuildContext context) {
return Column(
key: MemoryChartPane.hoverKey,
children: [
MemoryControlPane(controller: controller),
const SizedBox(height: intermediateSpacing),
MemoryChartPane(
chartController: _chartController,
keyFocusNode: _focusNode,
),
Expanded(
child: MemoryTabView(memoryController),
),
],
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,20 @@ import 'dart:async';

import 'package:devtools_shared/devtools_shared.dart';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:leak_tracker/devtools_integration.dart';
import 'package:logging/logging.dart';
import 'package:vm_service/vm_service.dart';

import '../../service/service_manager.dart';
import '../../shared/analytics/analytics.dart' as ga;
import '../../shared/analytics/constants.dart' as gac;
import '../../shared/config_specific/file/file.dart';
import '../../shared/globals.dart';
import '../../shared/primitives/auto_dispose.dart';
import '../../shared/utils.dart';
import '../../../../service/service_manager.dart';
import '../../../../shared/globals.dart';
import '../../../../shared/primitives/auto_dispose.dart';
import '../../../../shared/utils.dart';
import '../../panes/chart/primitives.dart';
import '../../panes/diff/controller/diff_pane_controller.dart';
import '../../panes/profile/profile_pane_controller.dart';
import '../../panes/tracing/tracing_pane_controller.dart';
import '../../shared/heap/model.dart';
import '../../shared/primitives/memory_timeline.dart';
import 'memory_protocol.dart';
import 'panes/chart/primitives.dart';
import 'panes/diff/controller/diff_pane_controller.dart';
import 'panes/profile/profile_pane_controller.dart';
import 'panes/tracing/tracing_pane_controller.dart';
import 'shared/heap/model.dart';
import 'shared/primitives/memory_timeline.dart';

final _log = Logger('memory_controller');

// TODO(terry): Consider supporting more than one file since app was launched.
// Memory Log filename.
final String _memoryLogFilename =
'${MemoryController.logFilenamePrefix}${DateFormat("yyyyMMdd_HH_mm").format(DateTime.now())}';

class OfflineFileException implements Exception {
OfflineFileException(this.message) : super();

final String message;

@override
String toString() => message;
}

class MemoryFeatureControllers {
/// [diffPaneController] is passed for testability.
Expand Down Expand Up @@ -85,7 +64,6 @@ class MemoryController extends DisposableController
ProfilePaneController? profilePaneController,
}) {
memoryTimeline = MemoryTimeline();
memoryLog = MemoryLog(this);

controllers = MemoryFeatureControllers(
diffPaneController,
Expand All @@ -104,15 +82,11 @@ class MemoryController extends DisposableController
/// instead of the widget state.
int selectedFeatureTabIndex = 0;

static const logFilenamePrefix = 'memory_log_';

final _shouldShowLeaksTab = ValueNotifier<bool>(false);
ValueListenable<bool> get shouldShowLeaksTab => _shouldShowLeaksTab;

late MemoryTimeline memoryTimeline;

late MemoryLog memoryLog;

HeapSample? _selectedDartSample;

HeapSample? _selectedAndroidSample;
Expand Down Expand Up @@ -361,96 +335,3 @@ class MemoryController extends DisposableController
controllers.dispose();
}
}

/// Supports saving and loading memory samples.
class MemoryLog {
MemoryLog(this.controller);

/// Use in memory or local file system based on Flutter Web/Desktop.
static final _fs = FileIO();

MemoryController controller;

/// Persist the live memory data to a JSON file in the /tmp directory.
List<String> exportMemory() {
ga.select(gac.memory, gac.export);

final liveData = controller.memoryTimeline.liveData;

bool pseudoData = false;
if (liveData.isEmpty) {
// Used to create empty memory log for test.
pseudoData = true;
liveData.add(
HeapSample(
DateTime.now().millisecondsSinceEpoch,
0,
0,
0,
0,
false,
AdbMemoryInfo.empty(),
EventSample.empty(),
RasterCache.empty(),
),
);
}

final jsonPayload = SamplesMemoryJson.encodeList(liveData);
if (kDebugMode) {
// TODO(terry): Remove this check add a unit test instead.
// Reload the file just created and validate that the saved data matches
// the live data.
final memoryJson = SamplesMemoryJson.decode(argJsonString: jsonPayload);
assert(memoryJson.isMatchedVersion);
assert(memoryJson.isMemoryPayload);
assert(memoryJson.data.length == liveData.length);
}

_fs.writeStringToFile(_memoryLogFilename, jsonPayload, isMemory: true);

if (pseudoData) liveData.clear();

return [_fs.exportDirectoryName(isMemory: true), _memoryLogFilename];
}

/// Return a list of offline memory logs filenames in the /tmp directory
/// that are available to open.
List<String> offlineFiles() {
final memoryLogs = _fs.list(
prefix: MemoryController.logFilenamePrefix,
isMemory: true,
);

// Sort by newest file top-most (DateTime is in the filename).

memoryLogs.sort((a, b) => b.compareTo(a));

return memoryLogs;
}

/// Load the memory profile data from a saved memory log file.
@visibleForTesting
Future<void> loadOffline(String filename) async {
final jsonPayload = _fs.readStringFromFile(filename, isMemory: true)!;

final memoryJson = SamplesMemoryJson.decode(argJsonString: jsonPayload);

if (!memoryJson.isMatchedVersion) {
final e =
'Error loading file $filename version ${memoryJson.payloadVersion}';
_log.warning(e);
throw OfflineFileException(e);
}

assert(memoryJson.isMemoryPayload);

controller.offline = true;
controller.memoryTimeline.offlineData.clear();
controller.memoryTimeline.offlineData.addAll(memoryJson.data);
}

@visibleForTesting
bool removeOfflineFile(String filename) =>
_fs.deleteFile(filename, isMemory: true);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import 'package:devtools_shared/devtools_shared.dart';
import 'package:logging/logging.dart';
import 'package:vm_service/vm_service.dart';

import '../../shared/globals.dart';
import '../../shared/utils.dart';
import '../../../../shared/globals.dart';
import '../../../../shared/utils.dart';
import '../../shared/primitives/memory_timeline.dart';
import 'memory_controller.dart';
import 'shared/primitives/memory_timeline.dart';

final _log = Logger('memory_protocol');

Expand Down
Loading