Fix some offline import issues with the Performance page. (#4735)

diff --git a/packages/devtools_app/lib/src/screens/performance/panes/raster_stats/raster_stats.dart b/packages/devtools_app/lib/src/screens/performance/panes/raster_stats/raster_stats.dart
index 5ad02b4..3bf28fc 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/raster_stats/raster_stats.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/raster_stats/raster_stats.dart
@@ -29,15 +29,14 @@
 
   @override
   Widget build(BuildContext context) {
-    return DualValueListenableBuilder<RasterStats, bool>(
+    return DualValueListenableBuilder<RasterStats?, bool>(
       firstListenable: rasterStatsController.rasterStats,
       secondListenable: rasterStatsController.loadingSnapshot,
       builder: (context, rasterStats, loading, _) {
         if (loading) {
           return const CenteredCircularProgressIndicator();
         }
-        final snapshots = rasterStats.layerSnapshots;
-        if (snapshots.isEmpty) {
+        if (rasterStats == null || rasterStats.layerSnapshots.isEmpty) {
           return const Center(
             child: Text(
               'Take a snapshot to view raster stats for the current screen.',
@@ -50,15 +49,14 @@
           children: [
             LayerSnapshotTable(
               controller: rasterStatsController,
-              snapshots: snapshots,
+              snapshots: rasterStats.layerSnapshots,
             ),
             ValueListenableBuilder<LayerSnapshot?>(
               valueListenable: rasterStatsController.selectedSnapshot,
               builder: (context, snapshot, _) {
                 return LayerImage(
                   snapshot: snapshot,
-                  originalFrameSize:
-                      rasterStatsController.rasterStats.value.originalFrameSize,
+                  originalFrameSize: rasterStats.originalFrameSize,
                   includeFullScreenButton: true,
                 );
               },
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/raster_stats/raster_stats_controller.dart b/packages/devtools_app/lib/src/screens/performance/panes/raster_stats/raster_stats_controller.dart
index 66ad20b..d3f1d2f 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/raster_stats/raster_stats_controller.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/raster_stats/raster_stats_controller.dart
@@ -15,8 +15,8 @@
   RasterStatsController(super.performanceController);
 
   /// The active raster stats for the view.
-  ValueListenable<RasterStats> get rasterStats => _rasterStats;
-  final _rasterStats = ValueNotifier<RasterStats>(RasterStats.empty());
+  ValueListenable<RasterStats?> get rasterStats => _rasterStats;
+  final _rasterStats = ValueNotifier<RasterStats?>(null);
 
   ValueListenable<bool> get loadingSnapshot => _loadingSnapshot;
   final _loadingSnapshot = ValueNotifier<bool>(false);
@@ -24,7 +24,8 @@
   final selectedSnapshot = ValueNotifier<LayerSnapshot?>(null);
 
   void selectSnapshot(LayerSnapshot? snapshot) {
-    _rasterStats.value.selectedSnapshot = snapshot;
+    assert(_rasterStats.value != null);
+    _rasterStats.value!.selectedSnapshot = snapshot;
     selectedSnapshot.value = snapshot;
   }
 
@@ -44,9 +45,9 @@
     }
   }
 
-  void setData(RasterStats stats) {
+  void setData(RasterStats? stats) {
     _rasterStats.value = stats;
-    selectedSnapshot.value = stats.selectedSnapshot;
+    selectedSnapshot.value = stats?.selectedSnapshot;
     performanceController.data!.rasterStats = stats;
   }
 
@@ -65,6 +66,6 @@
 
   @override
   void clearData() {
-    setData(RasterStats.empty());
+    setData(null);
   }
 }
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/raster_stats/raster_stats_model.dart b/packages/devtools_app/lib/src/screens/performance/panes/raster_stats/raster_stats_model.dart
index 3897987..bd698b0 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/raster_stats/raster_stats_model.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/raster_stats/raster_stats_model.dart
@@ -15,21 +15,16 @@
     required this.selectedSnapshot,
   });
 
-  factory RasterStats.empty() {
-    return RasterStats._(
-      layerSnapshots: [],
-      originalFrameSize: null,
-      selectedSnapshot: null,
-      totalRasterTime: Duration.zero,
-    );
-  }
-
   factory RasterStats.parse(Map<String, Object?> json) {
-    final originalFrameSize = Size(
-      (json[_frameWidthKey] as num).toDouble(),
-      (json[_frameHeightKey] as num).toDouble(),
-    );
-
+    Size? originalFrameSize;
+    final originalWidth = json[_frameWidthKey] as num?;
+    final originalHeight = json[_frameHeightKey] as num?;
+    if (originalHeight != null && originalWidth != null) {
+      originalFrameSize = Size(
+        originalWidth.toDouble(),
+        originalHeight.toDouble(),
+      );
+    }
     int? selectedId;
     LayerSnapshot? selected;
     if (json[_selectedIdKey] != null) {
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/rebuild_stats/rebuild_counts.dart b/packages/devtools_app/lib/src/screens/performance/panes/rebuild_stats/rebuild_counts.dart
index 8fe8121..4b001e5 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/rebuild_stats/rebuild_counts.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/rebuild_stats/rebuild_counts.dart
@@ -209,6 +209,8 @@
     return _rebuildsForFrame[frameNumber];
   }
 
+  bool get isNotEmpty => _rebuildsForFrame.isNotEmpty;
+
   Map<String, dynamic>? toJson() {
     if (_rebuildsForFrame.isEmpty) {
       // No need to encode data unless there were actually rebuilds reported.
diff --git a/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart b/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart
index 4dd8644..22f1f02 100644
--- a/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart
+++ b/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart
@@ -77,12 +77,24 @@
 
   @override
   Widget build(BuildContext context) {
+    final isOffline = offlineController.offlineMode.value;
+    final isFlutterApp = serviceManager.connectedApp!.isFlutterAppNow!;
+
+    var showFrameAnalysis = isFlutterApp;
+    var showRasterStats = isFlutterApp;
+    var showRebuildStats = FeatureFlags.widgetRebuildstats && isFlutterApp;
+    final offlineData = controller.offlinePerformanceData;
+    if (isOffline && offlineData != null) {
+      showFrameAnalysis = showFrameAnalysis && offlineData.frames.isNotEmpty;
+      showRasterStats = showRasterStats && offlineData.rasterStats != null;
+      showRebuildStats =
+          showRebuildStats && offlineData.rebuildCountModel.isNotEmpty;
+    }
+
     final tabRecords = <_PerformanceTabRecord>[
-      if (serviceManager.connectedApp!.isFlutterAppNow!) ...[
-        _frameAnalysisRecord(),
-        _rasterStatsRecord(),
-        if (FeatureFlags.widgetRebuildstats) _rebuildStatsRecord(),
-      ],
+      if (showFrameAnalysis) _frameAnalysisRecord(),
+      if (showRasterStats) _rasterStatsRecord(),
+      if (showRebuildStats) _rebuildStatsRecord(),
       _timelineEventsRecord(),
     ];
 
@@ -164,33 +176,36 @@
     return _PerformanceTabRecord(
       tab: _buildTab(
         tabName: 'Raster Stats',
-        trailing: Row(
-          mainAxisAlignment: MainAxisAlignment.end,
-          children: [
-            IconLabelButton(
-              tooltip: 'Take a snapshot of the rendering layers on the current'
-                  ' screen',
-              icon: Icons.camera,
-              label: 'Take Snapshot',
-              outlined: false,
-              onPressed: () {
-                ga.select(
-                  PerformanceScreen.id,
-                  analytics_constants.collectRasterStats,
-                );
-                unawaited(
-                  controller.rasterStatsController.collectRasterStats(),
-                );
-              },
-            ),
-            const SizedBox(width: denseSpacing),
-            ClearButton(
-              outlined: false,
-              onPressed: controller.rasterStatsController.clearData,
-            ),
-            const SizedBox(width: densePadding),
-          ],
-        ),
+        trailing: !offlineController.offlineMode.value
+            ? Row(
+                mainAxisAlignment: MainAxisAlignment.end,
+                children: [
+                  IconLabelButton(
+                    tooltip:
+                        'Take a snapshot of the rendering layers on the current'
+                        ' screen',
+                    icon: Icons.camera,
+                    label: 'Take Snapshot',
+                    outlined: false,
+                    onPressed: () {
+                      ga.select(
+                        PerformanceScreen.id,
+                        analytics_constants.collectRasterStats,
+                      );
+                      unawaited(
+                        controller.rasterStatsController.collectRasterStats(),
+                      );
+                    },
+                  ),
+                  const SizedBox(width: denseSpacing),
+                  ClearButton(
+                    outlined: false,
+                    onPressed: controller.rasterStatsController.clearData,
+                  ),
+                  const SizedBox(width: densePadding),
+                ],
+              )
+            : null,
       ),
       tabView: KeepAliveWrapper(
         child: Center(
@@ -223,9 +238,10 @@
                     gaSelection: analytics_constants.timelineFlameChartHelp,
                   ),
                 ],
-                RefreshTimelineEventsButton(
-                  controller: _timelineEventsController,
-                ),
+                if (!offlineController.offlineMode.value)
+                  RefreshTimelineEventsButton(
+                    controller: _timelineEventsController,
+                  ),
               ],
             );
           },
diff --git a/packages/devtools_app/test/performance/raster_stats/raster_stats_controller_test.dart b/packages/devtools_app/test/performance/raster_stats/raster_stats_controller_test.dart
index 6314b7b..fe10d3f 100644
--- a/packages/devtools_app/test/performance/raster_stats/raster_stats_controller_test.dart
+++ b/packages/devtools_app/test/performance/raster_stats/raster_stats_controller_test.dart
@@ -30,43 +30,36 @@
 
     test('calling collectRasterStats sets data', () async {
       var rasterStats = controller.rasterStats.value;
-      expect(rasterStats.layerSnapshots, isEmpty);
-      expect(rasterStats.selectedSnapshot, isNull);
-      expect(rasterStats.originalFrameSize, isNull);
-      expect(rasterStats.totalRasterTime, equals(Duration.zero));
+      expect(rasterStats, isNull);
 
       await controller.collectRasterStats();
 
       rasterStats = controller.rasterStats.value;
-      expect(rasterStats.layerSnapshots.length, equals(2));
+      expect(rasterStats, isNotNull);
+      expect(rasterStats!.layerSnapshots.length, equals(2));
       expect(rasterStats.selectedSnapshot, isNotNull);
       expect(rasterStats.originalFrameSize, isNotNull);
       expect(rasterStats.totalRasterTime, isNot(equals(Duration.zero)));
     });
 
-    test('calling collectRasterStats sets empty data for bad service response',
+    test('calling collectRasterStats sets null data for bad service response',
         () async {
       var rasterStats = controller.rasterStats.value;
-      expect(rasterStats.layerSnapshots, isEmpty);
-      expect(rasterStats.selectedSnapshot, isNull);
-      expect(rasterStats.originalFrameSize, isNull);
-      expect(rasterStats.totalRasterTime, equals(Duration.zero));
+      expect(rasterStats, isNull);
 
       when(mockServiceManager.renderFrameWithRasterStats)
           .thenAnswer((_) => throw Exception('something went wrong'));
       await controller.collectRasterStats();
 
       rasterStats = controller.rasterStats.value;
-      expect(rasterStats.layerSnapshots, isEmpty);
-      expect(rasterStats.selectedSnapshot, isNull);
-      expect(rasterStats.originalFrameSize, isNull);
-      expect(rasterStats.totalRasterTime, equals(Duration.zero));
+      expect(rasterStats, isNull);
     });
 
-    test('clear', () async {
+    test('calling clear nulls out raster stats', () async {
       await controller.collectRasterStats();
       var rasterStats = controller.rasterStats.value;
-      expect(rasterStats.layerSnapshots.length, equals(2));
+      expect(rasterStats, isNotNull);
+      expect(rasterStats!.layerSnapshots.length, equals(2));
       expect(rasterStats.selectedSnapshot, isNotNull);
       expect(rasterStats.originalFrameSize, isNotNull);
       expect(rasterStats.totalRasterTime, isNot(equals(Duration.zero)));
@@ -74,22 +67,20 @@
       controller.clearData();
 
       rasterStats = controller.rasterStats.value;
-      expect(rasterStats.layerSnapshots, isEmpty);
-      expect(rasterStats.selectedSnapshot, isNull);
-      expect(rasterStats.originalFrameSize, isNull);
-      expect(rasterStats.totalRasterTime, equals(Duration.zero));
+      expect(rasterStats, isNull);
     });
 
     test('setOfflineData', () async {
       final rasterStats = RasterStats.parse(rasterStatsFromServiceJson);
 
-      // Ensure we are starting in an empty state.
-      expect(controller.rasterStats.value.layerSnapshots, isEmpty);
+      // Ensure we are starting in a null state.
+      expect(controller.rasterStats.value, isNull);
 
       final offlineData = PerformanceData(rasterStats: rasterStats);
       await controller.setOfflineData(offlineData);
 
-      expect(controller.rasterStats.value.layerSnapshots.length, equals(2));
+      expect(controller.rasterStats.value, isNotNull);
+      expect(controller.rasterStats.value!.layerSnapshots.length, equals(2));
     });
   });
 }
diff --git a/packages/devtools_app/test/performance/raster_stats/raster_stats_model_test.dart b/packages/devtools_app/test/performance/raster_stats/raster_stats_model_test.dart
index 4b9cf5e..6c89238 100644
--- a/packages/devtools_app/test/performance/raster_stats/raster_stats_model_test.dart
+++ b/packages/devtools_app/test/performance/raster_stats/raster_stats_model_test.dart
@@ -13,14 +13,6 @@
   late RasterStats rasterStats;
 
   group('$RasterStats model', () {
-    test('empty constructor', () {
-      rasterStats = RasterStats.empty();
-      expect(rasterStats.layerSnapshots, isEmpty);
-      expect(rasterStats.selectedSnapshot, isNull);
-      expect(rasterStats.originalFrameSize, isNull);
-      expect(rasterStats.totalRasterTime, equals(Duration.zero));
-    });
-
     test('parse from service data', () {
       rasterStats = RasterStats.parse(rasterStatsFromServiceJson);
       expect(rasterStats.layerSnapshots.length, equals(2));
@@ -78,19 +70,9 @@
     });
 
     test('to json', () {
-      rasterStats = RasterStats.empty();
-      var json = rasterStats.json;
-      var expected = <String, Object?>{
-        'frame_width': null,
-        'frame_height': null,
-        'snapshots': [],
-        'selectedId': null
-      };
-      expect(collectionEquals(json, expected), isTrue);
-
       rasterStats = RasterStats.parse(rasterStatsFromServiceJson);
-      json = rasterStats.json;
-      expected = Map<String, Object?>.from(rasterStatsFromServiceJson);
+      final json = rasterStats.json;
+      final expected = Map<String, Object?>.from(rasterStatsFromServiceJson);
       // The expected output should not have the 'type' field that comes from
       // the service protocol and it should have an additional field for the id
       // of the selected snapshot.
diff --git a/packages/devtools_app/test/performance/raster_stats/raster_stats_test.dart b/packages/devtools_app/test/performance/raster_stats/raster_stats_test.dart
index 0eb5669..b9f28b5 100644
--- a/packages/devtools_app/test/performance/raster_stats/raster_stats_test.dart
+++ b/packages/devtools_app/test/performance/raster_stats/raster_stats_test.dart
@@ -79,16 +79,14 @@
     testWidgets('can change layer selection', (tester) async {
       await pumpRenderingLayerVisualizer(tester);
 
-      final layers = controller.rasterStats.value.layerSnapshots;
+      final rasterStats = controller.rasterStats.value!;
+      final layers = rasterStats.layerSnapshots;
       final firstLayer = layers.first;
       final secondLayer = layers.last;
       expect(firstLayer.displayName, equals('Layer 12731'));
       expect(secondLayer.displayName, equals('Layer 12734'));
 
-      expect(
-        controller.rasterStats.value.selectedSnapshot,
-        equals(firstLayer),
-      );
+      expect(rasterStats.selectedSnapshot, equals(firstLayer));
 
       await tester.tap(find.richText('Layer 12734'));
       await tester.pumpAndSettle();
diff --git a/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart b/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart
index 98108bc..f5c7161 100644
--- a/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart
+++ b/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart
@@ -19,6 +19,7 @@
   when(controller.data).thenReturn(PerformanceData());
   when(controller.enhanceTracingController)
       .thenReturn(EnhanceTracingController());
+  when(controller.offlinePerformanceData).thenReturn(null);
 
   // Stubs for Flutter Frames feature.
   when(controller.flutterFramesController).thenReturn(flutterFramesController);