Adding ArtificialRoot to non-deferred apps for diff table (#4338)

diff --git a/packages/devtools_app/lib/src/screens/app_size/app_size_controller.dart b/packages/devtools_app/lib/src/screens/app_size/app_size_controller.dart
index 71ee07d..a9602c5 100644
--- a/packages/devtools_app/lib/src/screens/app_size/app_size_controller.dart
+++ b/packages/devtools_app/lib/src/screens/app_size/app_size_controller.dart
@@ -67,6 +67,10 @@
   static const identicalFilesError =
       'Failed to load diff: OLD and NEW files are identical.';
 
+  static const artificialRootNodeName = 'ArtificialRoot';
+
+  static const mainNodeName = 'Main';
+
   CallGraph? _analysisCallGraph;
 
   ValueListenable<CallGraphNode?> get analysisCallGraphRoot =>
@@ -318,13 +322,13 @@
   }
 
   bool _hasDeferredInfo(Map<String, dynamic> jsonFile) {
-    return jsonFile['n'] == 'ArtificialRoot';
+    return jsonFile['n'] == artificialRootNodeName;
   }
 
   Map<String, dynamic> _extractMainUnit(Map<String, dynamic> jsonFile) {
     if (_hasDeferredInfo(jsonFile)) {
       final main = _extractChildren(jsonFile).firstWhere(
-        (child) => child['n'] == 'Main',
+        (child) => child['n'] == mainNodeName,
         orElse: () => jsonFile,
       );
       return main;
@@ -380,8 +384,20 @@
 
     Map<String, dynamic> diffMap;
     if (oldFile.isAnalyzeSizeFile && newFile.isAnalyzeSizeFile) {
+      var oldFileJson = oldFile.data as Map<String, dynamic>;
+      var newFileJson = newFile.data as Map<String, dynamic>;
+
+      if (_hasDeferredInfo(oldFileJson) || _hasDeferredInfo(newFileJson)) {
+        _isDeferredApp.value = deferredLoadingSupportEnabled;
+
+        if (!_hasDeferredInfo(oldFileJson)) {
+          oldFileJson = _wrapInArtificialRoot(oldFileJson);
+        } else if (!_hasDeferredInfo(newFileJson)) {
+          newFileJson = _wrapInArtificialRoot(newFileJson);
+        }
+      }
+
       final oldApkProgramInfo = ProgramInfo();
-      final oldFileJson = oldFile.data as Map<String, dynamic>;
       _apkJsonToProgramInfo(
         program: oldApkProgramInfo,
         parent: oldApkProgramInfo.root,
@@ -399,7 +415,6 @@
       }
 
       final newApkProgramInfo = ProgramInfo();
-      final newFileJson = newFile.data as Map<String, dynamic>;
       _apkJsonToProgramInfo(
         program: newApkProgramInfo,
         parent: newApkProgramInfo.root,
@@ -438,20 +453,23 @@
     changeOldDiffFile(oldFile);
     changeNewDiffFile(newFile);
 
-    diffMap['n'] = 'Root';
+    diffMap['n'] = isDeferredApp.value ? 'Entire App' : 'Root';
 
     // TODO(peterdjlee): Try to move the non-active tree generation to separate isolates.
     _combinedDiffTreeRoot = generateDiffTree(
       diffMap,
       DiffTreeType.combined,
+      skipNodesWithNoByteSizeChange: !isDeferredApp.value,
     );
     _increasedDiffTreeRoot = generateDiffTree(
       diffMap,
       DiffTreeType.increaseOnly,
+      skipNodesWithNoByteSizeChange: !isDeferredApp.value,
     );
     _decreasedDiffTreeRoot = generateDiffTree(
       diffMap,
       DiffTreeType.decreaseOnly,
+      skipNodesWithNoByteSizeChange: !isDeferredApp.value,
     );
 
     changeDiffRoot(_activeDiffRoot);
@@ -459,6 +477,14 @@
     _processingNotifier.value = false;
   }
 
+  Map<String, dynamic> _wrapInArtificialRoot(Map<String, dynamic> json) {
+    json['n'] = mainNodeName;
+    return <String, dynamic>{
+      'n': artificialRootNodeName,
+      'children': [json],
+    };
+  }
+
   ProgramInfoNode _apkJsonToProgramInfo({
     required ProgramInfo program,
     required ProgramInfoNode parent,
@@ -507,14 +533,16 @@
   /// * [DiffTreeType.combined]: returns a tree with all nodes.
   TreemapNode? generateDiffTree(
     Map<String, dynamic> treeJson,
-    DiffTreeType diffTreeType,
-  ) {
+    DiffTreeType diffTreeType, {
+    bool skipNodesWithNoByteSizeChange = true,
+  }) {
     final isLeafNode = treeJson['children'] == null;
     if (!isLeafNode) {
       return _buildNodeWithChildren(
         treeJson,
         showDiff: true,
         diffTreeType: diffTreeType,
+        skipNodesWithNoByteSizeChange: skipNodesWithNoByteSizeChange,
       );
     } else {
       // TODO(peterdjlee): Investigate why there are leaf nodes with size of null.
@@ -547,6 +575,7 @@
     Map<String, dynamic> treeJson, {
     bool showDiff = false,
     DiffTreeType? diffTreeType,
+    bool skipNodesWithNoByteSizeChange = true,
   }) {
     assert(showDiff ? diffTreeType != null : true);
     final rawChildren = treeJson['children'];
@@ -566,7 +595,7 @@
     }
 
     // If none of the children matched the diff tree type
-    if (totalByteSize == 0) {
+    if (totalByteSize == 0 && skipNodesWithNoByteSizeChange) {
       return null;
     } else {
       return _buildNode(