ui: Highlight flamegraph issue on heap_graph_non_finalized_graph, take 2

The initial change was aosp/2019015. The feedback on that was to
show a modal indicating that the flamegraph is not finalised.

The end result looks like this:
https://screenshot.googleplex.com/ZNqu6LYsvTYaLMi

Bug: 222270825
Change-Id: Ic684be1a86637101984a557e07a3922507d401f0
diff --git a/ui/src/assets/modal.scss b/ui/src/assets/modal.scss
index f65961e..20a80a8 100644
--- a/ui/src/assets/modal.scss
+++ b/ui/src/assets/modal.scss
@@ -147,7 +147,8 @@
   animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
 }
 
-.micromodal-slide[aria-hidden="false"] .modal-container {
+.micromodal-slide[aria-hidden="false"] .modal-container,
+.micromodal-slide[aria-hidden="false"] .partial-modal-container {
   animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
 }
 
@@ -155,11 +156,13 @@
   animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
 }
 
-.micromodal-slide[aria-hidden="true"] .modal-container {
+.micromodal-slide[aria-hidden="true"] .modal-container,
+.micromodal-slide[aria-hidden="true"] .partial-modal-container {
   animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
 }
 
 .micromodal-slide .modal-container,
+.micromodal-slide .partial-modal-container,
 .micromodal-slide .modal-overlay {
   will-change: transform;
 }
@@ -215,3 +218,32 @@
 .modal-small {
   font-size: 11px;
 }
+
+.partial-modal-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0,0,0,0.6);
+  display: flex;
+  justify-content: center;
+  z-index: 999;
+}
+
+.partial-modal-container {
+  background-color: #fff;
+  margin-top: 1vh;
+  padding: 30px 30px 20px 30px;
+  max-width: 90vw;
+  height: fit-content;
+  border-radius: 4px;
+  overflow-y: auto;
+  box-sizing: border-box;
+}
+
+.partial-modal-header {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index bd914e0..8366f65 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -1006,6 +1006,10 @@
   setPivotStateReduxState(
       state: StateDraft, args: {pivotTableState: PivotTableReduxState}) {
     state.pivotTableRedux = args.pivotTableState;
+  },
+
+  dismissFlamegraphModal(state: StateDraft, _: {}) {
+    state.flamegraphModalDismissed = true;
   }
 };
 
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index cc57df5..ac287d2 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -97,6 +97,7 @@
     recordingInProgress: false,
     recordingCancelled: false,
     extensionInstalled: false,
+    flamegraphModalDismissed: false,
     recordingTarget: recordTargetStore.getValidTarget(),
     availableAdbDevices: [],
 
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 18df6ce..3bc8359 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -79,7 +79,8 @@
 // typed key/value because a `Map` does not preserve type during
 // serialisation+deserialisation.
 // 15: Added state for Pivot Table V2
-export const STATE_VERSION = 15;
+// 16: Added boolean tracking if the flamegraph modal was dismissed
+export const STATE_VERSION = 16;
 
 export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
 
@@ -463,6 +464,7 @@
   recordingInProgress: boolean;
   recordingCancelled: boolean;
   extensionInstalled: boolean;
+  flamegraphModalDismissed: boolean;
   recordingTarget: RecordingTarget;
   availableAdbDevices: AdbRecordingTarget[];
   lastRecordingError?: string;
diff --git a/ui/src/controller/flamegraph_controller.ts b/ui/src/controller/flamegraph_controller.ts
index cb7ea7e..cb74e41 100644
--- a/ui/src/controller/flamegraph_controller.ts
+++ b/ui/src/controller/flamegraph_controller.ts
@@ -101,7 +101,7 @@
     if (hasAreaChanged) {
       const upids = [];
       if (!area) {
-        publishFlamegraphDetails(
+        this.checkCompletionAndPublishFlamegraph(
             {...frontendGlobals.flamegraphDetails, isInAreaSelection: false});
         return;
       }
@@ -114,7 +114,7 @@
         upids.push((trackState.config as PerfSampleConfig).upid);
       }
       if (upids.length === 0) {
-        publishFlamegraphDetails(
+        this.checkCompletionAndPublishFlamegraph(
             {...frontendGlobals.flamegraphDetails, isInAreaSelection: false});
         return;
       }
@@ -228,7 +228,17 @@
     this.flamegraphDetails.expandedCallsite = expandedCallsite;
     this.flamegraphDetails.viewingOption = viewingOption;
     this.flamegraphDetails.isInAreaSelection = hasAreaChanged;
-    publishFlamegraphDetails(this.flamegraphDetails);
+    this.checkCompletionAndPublishFlamegraph(this.flamegraphDetails);
+  }
+
+  private async checkCompletionAndPublishFlamegraph(flamegraphDetails:
+                                                        FlamegraphDetails) {
+    flamegraphDetails.graphIncomplete =
+        (await this.args.engine.query(`select value from stats
+       where severity = 'error' and name = 'heap_graph_non_finalized_graph'`))
+            .firstRow({value: NUM})
+            .value > 0;
+    publishFlamegraphDetails(flamegraphDetails);
   }
 
   async getFlamegraphData(
diff --git a/ui/src/frontend/flamegraph_panel.ts b/ui/src/frontend/flamegraph_panel.ts
index 35a4d9f..368ab32 100644
--- a/ui/src/frontend/flamegraph_panel.ts
+++ b/ui/src/frontend/flamegraph_panel.ts
@@ -29,8 +29,10 @@
 import {PerfettoMouseEvent} from './events';
 import {Flamegraph, NodeRendering} from './flamegraph';
 import {globals} from './globals';
+import {showPartialModal} from './modal';
 import {Panel, PanelSize} from './panel';
 import {debounce} from './rate_limiters';
+import {Router} from './router';
 import {getCurrentTrace} from './sidebar';
 import {convertTraceToPprofAndDownload} from './trace_converter';
 
@@ -119,6 +121,7 @@
               }
             }
           },
+          this.maybeShowModal(flamegraphDetails.graphIncomplete),
           m('.details-panel-heading.flamegraph-profile',
             {onclick: (e: MouseEvent) => e.stopPropagation()},
             [
@@ -165,6 +168,39 @@
     }
   }
 
+
+  private maybeShowModal(graphIncomplete?: boolean): m.Vnode|undefined {
+    if (!graphIncomplete || globals.state.flamegraphModalDismissed) {
+      return undefined;
+    }
+    return showPartialModal({
+      title: 'The flamegraph is incomplete',
+      content:
+          m('div',
+            m('div',
+              'The current trace does not have a fully formed flamegraph.')),
+      buttons: [
+        {
+          text: 'Show the errors',
+          primary: true,
+          id: 'incomplete_graph_show',
+          action: () => {
+            Router.navigate('#!/info');
+          }
+        },
+        {
+          text: 'Skip',
+          primary: false,
+          id: 'incomplete_graph_skip',
+          action: () => {
+            globals.dispatch(Actions.dismissFlamegraphModal({}));
+            globals.rafScheduler.scheduleFullRedraw();
+          }
+        }
+      ],
+    });
+  }
+
   private getTitle(): string {
     switch (this.profileType!) {
       case ProfileType.NATIVE_HEAP_PROFILE:
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index dd5b51b..aa48edf 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -124,6 +124,9 @@
   // isInAreaSelection is true if a flamegraph is part of the current area
   // selection.
   isInAreaSelection?: boolean;
+  // When heap_graph_non_finalized_graph has a count >0, we mark the graph
+  // as incomplete.
+  graphIncomplete?: boolean;
 }
 
 export interface CpuProfileDetails {
diff --git a/ui/src/frontend/modal.ts b/ui/src/frontend/modal.ts
index 69482e3..40badd9 100644
--- a/ui/src/frontend/modal.ts
+++ b/ui/src/frontend/modal.ts
@@ -84,3 +84,13 @@
   });
   return buttons;
 }
+
+export function showPartialModal(attrs: ModalDefinition): m.Vnode {
+  return m(
+      '.partial-modal-overlay',
+      {tabindex: -1},
+      m('.partial-modal-container',
+        m('header.partial-modal-header', m('h2.modal-title', attrs.title)),
+        m('main.modal-content', attrs.content),
+        m('footer.modal-footer', ...makeButtons(attrs.buttons))));
+}