Yicong-Huang commented on code in PR #8552:
URL: https://github.com/apache/texera/pull/8552#discussion_r4055615250


##########
frontend/src/app/workspace/service/heatmap/heatmap-stats-restore.service.ts:
##########
@@ -0,0 +1,122 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Injectable } from "@angular/core";
+import { EMPTY, Observable, defer } from "rxjs";
+import { catchError, map, switchMap, takeUntil, tap } from "rxjs/operators";
+import { WorkflowExecutionsService } from 
"../../../dashboard/service/user/workflow-executions/workflow-executions.service";
+import { EXECUTION_STATUS_CODE, WorkflowExecutionsEntry } from 
"../../../dashboard/type/workflow-executions-entry";
+import { WorkflowActionService } from 
"../workflow-graph/model/workflow-action.service";
+import { WorkflowStatusService } from 
"../workflow-status/workflow-status.service";
+import { ExecuteWorkflowService } from 
"../execute-workflow/execute-workflow.service";
+import { ExecutionState, isNotInExecution } from 
"../../types/execute-workflow.interface";
+import { loadPersistedHeatmapView } from "./heatmap-overlay-persistence";
+import { toOperatorRuntimeStatusMap } from "./runtime-statistics-mapper";
+
+/**
+ * Restores the last execution's per-operator statistics after a page refresh,
+ * so the performance heat-map can render a finished run without re-executing.
+ *
+ * All gating lives here rather than in the caller: the persisted-overlay check
+ * is a synchronous localStorage read, so a user who never enabled the overlay
+ * incurs no fetch at all.
+ */
+@Injectable({
+  providedIn: "root",
+})
+export class HeatmapStatsRestoreService {
+  constructor(
+    private workflowExecutionsService: WorkflowExecutionsService,
+    private workflowActionService: WorkflowActionService,
+    private workflowStatusService: WorkflowStatusService,
+    private executeWorkflowService: ExecuteWorkflowService
+  ) {}
+
+  /**
+   * Fetches the latest run's statistics and feeds them into
+   * WorkflowStatusService. Cold: nothing happens until subscribed. Skips
+   * silently (including on HTTP errors — restoring is best-effort) when:
+   * - the overlay is not persisted on,
+   * - the workflow has never been saved (no wid),
+   * - an execution is in progress, on entry or by the time the fetches return
+   *   (the live stream wins),
+   * - the workflow has no executions or the run left no statistics.
+   */
+  public restoreLatestRunStatistics(): Observable<void> {
+    return defer(() => {
+      if (loadPersistedHeatmapView() === null) {
+        return EMPTY;
+      }
+      const wid = this.workflowActionService.getWorkflowMetadata()?.wid;
+      if (wid === undefined) {
+        return EMPTY;
+      }
+      if (this.isExecuting()) {
+        return EMPTY;
+      }
+
+      return 
this.workflowExecutionsService.retrieveWorkflowExecutions(wid).pipe(
+        switchMap(executions => {
+          const run = this.pickLatestRun(executions);
+          if (run === undefined) {
+            return EMPTY;
+          }
+          return 
this.workflowExecutionsService.retrieveWorkflowRuntimeStatistics(wid, run.eId, 
run.cuId);
+        }),
+        // Only the fetches are best-effort. Placed above the map so a mapping 
or ingestion
+        // failure still surfaces instead of looking like a run with nothing 
to restore.
+        catchError(() => EMPTY),
+        tap(rows => {
+          const runtimeStatus = toOperatorRuntimeStatusMap(rows);
+          // Re-checked, not redundant: the entry guard ran two round trips 
ago, and the
+          // websocket can connect and start streaming a live run inside that 
window.
+          if (this.isExecuting() || Object.keys(runtimeStatus).length === 0) {
+            return;
+          }
+          this.workflowStatusService.setExternalStatus(runtimeStatus);
+        }),
+        map(() => undefined),
+        // Any other producer writing statistics means the canvas is no longer 
ours to restore:
+        // pressing Run resets the execution state to Uninitialized, which 
isExecuting() cannot
+        // see until the backend answers, but resetStatus() writes here first. 
Unsubscribing
+        // tears the pending fetch down, so the tap above never runs. A plain 
Subject, so
+        // subscribing does not itself emit, and the restore's own write is 
downstream.

Review Comment:
   **Polish:**
   
   The write is in the `tap` at `:85`, upstream of this operator, so 
"downstream" describes the position we discussed rather than the one that 
shipped. The notifier does fire from inside the `tap`, because 
`setExternalStatus` reaches `statisticsSubject.next` 
(`workflow-status.service.ts:81`), which is this notifier's own subject. Still 
harmless, for a different reason: a repro against the repo's rxjs prints 
`["tap:write","complete"]`, so the write reaches every earlier subscriber and 
only then does the stream close.
   
   ```suggestion
           // tears the pending fetch down, so the tap above never runs. A 
plain Subject, so
           // subscribing does not itself emit. The restore's own write does 
fire it, from inside
           // the tap, which closes the stream only after that write has 
reached its subscribers.
   ```



##########
frontend/src/app/workspace/service/heatmap/heatmap-stats-restore.service.spec.ts:
##########
@@ -0,0 +1,295 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { TestBed } from "@angular/core/testing";
+import { Subject, of, throwError } from "rxjs";
+import type { Mocked } from "vitest";
+import { HeatmapStatsRestoreService } from "./heatmap-stats-restore.service";
+import { savePersistedHeatmapView } from "./heatmap-overlay-persistence";
+import { HeatmapView } from "./heatmap-scoring";
+import { WorkflowExecutionsService } from 
"../../../dashboard/service/user/workflow-executions/workflow-executions.service";
+import { WorkflowActionService } from 
"../workflow-graph/model/workflow-action.service";
+import { WorkflowStatusService } from 
"../workflow-status/workflow-status.service";
+import { ExecuteWorkflowService } from 
"../execute-workflow/execute-workflow.service";
+import { ExecutionState, OperatorState } from 
"../../types/execute-workflow.interface";
+import { WorkflowExecutionsEntry } from 
"../../../dashboard/type/workflow-executions-entry";
+import { WorkflowRuntimeStatistics } from 
"../../../dashboard/type/workflow-runtime-statistics";
+
+function makeExecution(overrides: Partial<WorkflowExecutionsEntry>): 
WorkflowExecutionsEntry {
+  return {
+    eId: 1,
+    vId: 1,
+    cuId: 7,
+    whId: null,
+    sId: 0,
+    userName: "user",
+    avatar: "",
+    name: "run",
+    startingTime: 1_000,
+    completionTime: 2_000,
+    status: 3, // Completed
+    result: "",
+    bookmarked: false,
+    logLocation: "",
+    ...overrides,
+  };
+}
+
+function makeStatsRow(overrides: Partial<WorkflowRuntimeStatistics>): 
WorkflowRuntimeStatistics {
+  return {
+    operatorId: "op1",
+    timestamp: 1_000,
+    inputTupleCount: 10,
+    inputTupleSize: 100,
+    outputTupleCount: 5,
+    outputTupleSize: 50,
+    totalDataProcessingTime: 1_000_000,
+    totalControlProcessingTime: 2_000,
+    totalIdleTime: 3_000,
+    numberOfWorkers: 2,
+    status: 3,
+    ...overrides,
+  };
+}
+
+describe("HeatmapStatsRestoreService", () => {
+  let service: HeatmapStatsRestoreService;
+  let executionsService: Mocked<WorkflowExecutionsService>;
+  let statusService: Mocked<WorkflowStatusService>;
+  let actionService: Mocked<WorkflowActionService>;
+  let executeService: Mocked<ExecuteWorkflowService>;
+  let statisticsUpdates: Subject<Record<string, never>>;
+
+  beforeEach(() => {
+    localStorage.clear();
+    // Default arrangement: overlay persisted on, a saved workflow, no live 
run.
+    savePersistedHeatmapView(HeatmapView.Runtime);
+
+    executionsService = {
+      retrieveWorkflowExecutions: vi.fn(() => of([makeExecution({})])),
+      retrieveWorkflowRuntimeStatistics: vi.fn(() => of([makeStatsRow({})])),
+    } as unknown as Mocked<WorkflowExecutionsService>;
+    actionService = {
+      getWorkflowMetadata: vi.fn(() => ({ wid: 42 })),
+    } as unknown as Mocked<WorkflowActionService>;
+    // A plain Subject, like the real one: subscribing does not emit, so it 
only cuts the
+    // restore short when another producer actually writes statistics.
+    statisticsUpdates = new Subject<Record<string, never>>();
+    statusService = {
+      setExternalStatus: vi.fn(),
+      getStatisticsUpdateStream: vi.fn(() => statisticsUpdates.asObservable()),

Review Comment:
   **Advisory:**
   
   `setExternalStatus` writes nowhere here, while the real one reaches 
`statisticsSubject.next` (`workflow-status.service.ts:90`), and the notifier is 
a locally built plain `Subject` rather than the service's. So the over-firing 
guard at `:271` cannot fail. Suppose `statisticsSubject` became a 
`BehaviorSubject`, like its sibling `performanceMetricsSubject` at `:43`: 
`takeUntil` would fire on subscribe, the restore would be dead on every page 
load, and this spec would still pass. `workflow-status.service.spec.ts` does 
not pin it either, since all five `getStatisticsUpdateStream().subscribe` sites 
assert on emissions that follow a `next`. A three-line spec there, asserting 
nothing arrives before the first `next`, is where I would put it.



##########
frontend/src/app/workspace/service/heatmap/heatmap-stats-restore.service.ts:
##########
@@ -0,0 +1,122 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Injectable } from "@angular/core";
+import { EMPTY, Observable, defer } from "rxjs";
+import { catchError, map, switchMap, takeUntil, tap } from "rxjs/operators";
+import { WorkflowExecutionsService } from 
"../../../dashboard/service/user/workflow-executions/workflow-executions.service";
+import { EXECUTION_STATUS_CODE, WorkflowExecutionsEntry } from 
"../../../dashboard/type/workflow-executions-entry";
+import { WorkflowActionService } from 
"../workflow-graph/model/workflow-action.service";
+import { WorkflowStatusService } from 
"../workflow-status/workflow-status.service";
+import { ExecuteWorkflowService } from 
"../execute-workflow/execute-workflow.service";
+import { ExecutionState, isNotInExecution } from 
"../../types/execute-workflow.interface";
+import { loadPersistedHeatmapView } from "./heatmap-overlay-persistence";
+import { toOperatorRuntimeStatusMap } from "./runtime-statistics-mapper";
+
+/**
+ * Restores the last execution's per-operator statistics after a page refresh,
+ * so the performance heat-map can render a finished run without re-executing.
+ *
+ * All gating lives here rather than in the caller: the persisted-overlay check
+ * is a synchronous localStorage read, so a user who never enabled the overlay
+ * incurs no fetch at all.
+ */
+@Injectable({
+  providedIn: "root",
+})
+export class HeatmapStatsRestoreService {
+  constructor(
+    private workflowExecutionsService: WorkflowExecutionsService,
+    private workflowActionService: WorkflowActionService,
+    private workflowStatusService: WorkflowStatusService,
+    private executeWorkflowService: ExecuteWorkflowService
+  ) {}
+
+  /**
+   * Fetches the latest run's statistics and feeds them into
+   * WorkflowStatusService. Cold: nothing happens until subscribed. Skips
+   * silently (including on HTTP errors — restoring is best-effort) when:
+   * - the overlay is not persisted on,
+   * - the workflow has never been saved (no wid),
+   * - an execution is in progress, on entry or by the time the fetches return
+   *   (the live stream wins),
+   * - the workflow has no executions or the run left no statistics.

Review Comment:
   **Polish:**
   
   This list gained a fifth condition this round and was not extended. The PR 
description's skip table does carry the row, so the body and the method's own 
contract now disagree about when a restore is dropped.
   
   ```suggestion
      * - the workflow has no executions or the run left no statistics,
      * - another producer writes statistics first (a new run clears the 
canvas).
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to