Skip to content

Commit

Permalink
refactor: make viewer menu responsible for manipulating data from con…
Browse files Browse the repository at this point in the history
…troller to UI

also make the "model" responsible for storing interfaces for its own action states
  • Loading branch information
seankmartin committed Sep 3, 2024
1 parent e0facf0 commit f928d07
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 133 deletions.
41 changes: 39 additions & 2 deletions src/python_integration/screenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,46 @@ import { getCachedJson } from "#src/util/trackable.js";
import { ScreenshotMode } from "#src/util/trackable_screenshot_mode.js";
import type { Viewer } from "#src/viewer.js";

export interface ScreenshotActionState {
viewerState: any;
selectedValues: any;
screenshot: {
id: string;
image: string;
imageType: string;
depthData: string | undefined;
width: number;
height: number;
};
}

export interface ScreenshotChunkStatistics {
downloadLatency: number;
visibleChunksDownloading: number;
visibleChunksFailed: number;
visibleChunksGpuMemory: number;
visibleChunksSystemMemory: number;
visibleChunksTotal: number;
visibleGpuMemory: number;
}

export interface StatisticsActionState {
viewerState: any;
selectedValues: any;
screenshotStatistics: {
id: string;
chunkSources: any[];
total: ScreenshotChunkStatistics;
};
}

export class ScreenshotHandler extends RefCounted {
sendScreenshotRequested = new Signal<(state: any) => void>();
sendStatisticsRequested = new Signal<(state: any) => void>();
sendScreenshotRequested = new Signal<
(state: ScreenshotActionState) => void
>();
sendStatisticsRequested = new Signal<
(state: StatisticsActionState) => void
>();
requestState = new TrackableValue<string | undefined>(
undefined,
verifyOptionalString,
Expand Down
81 changes: 61 additions & 20 deletions src/ui/screenshot_menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,21 @@ import { debounce } from "lodash-es";
import { Overlay } from "#src/overlay.js";
import "#src/ui/screenshot_menu.css";

import type { ScreenshotManager } from "#src/util/screenshot.js";
import type {
ScreenshotLoadStatistics,
ScreenshotManager,
} from "#src/util/screenshot.js";
import { ScreenshotMode } from "#src/util/trackable_screenshot_mode.js";

interface UIScreenshotStatistics {
timeElapsedString: string | null;
chunkUsageDescription: string;
gpuMemoryUsageDescription: string;
downloadSpeedDescription: string;
}

const statisticsNamesForUI = {
timeElapsedString: "Screenshot duration",
chunkUsageDescription: "Number of loaded chunks",
gpuMemoryUsageDescription: "Visible chunk GPU memory usage",
downloadSpeedDescription: "Number of downloading chunks",
Expand Down Expand Up @@ -155,24 +166,26 @@ export class ScreenshotDialog extends Overlay {

const headerRow = this.statisticsTable.createTHead().insertRow();
const keyHeader = document.createElement("th");
keyHeader.textContent = "Screenshot in progress";
keyHeader.textContent = "Screenshot statistics";
headerRow.appendChild(keyHeader);
const valueHeader = document.createElement("th");
valueHeader.textContent = "";
headerRow.appendChild(valueHeader);

// Populate inital table elements with placeholder text
const statsRow = this.screenshotManager.screenshotStatistics;
for (const key in statsRow) {
if (key === "timeElapsedString") {
continue;
}
const orderedStatsRow: UIScreenshotStatistics = {
chunkUsageDescription: "Loading...",
gpuMemoryUsageDescription: "Loading...",
downloadSpeedDescription: "Loading...",
timeElapsedString: "Loading...",
};
for (const key in orderedStatsRow) {
const row = this.statisticsTable.insertRow();
const keyCell = row.insertCell();
const valueCell = row.insertCell();
keyCell.textContent =
statisticsNamesForUI[key as keyof typeof statisticsNamesForUI];
valueCell.textContent = "Loading...";
valueCell.textContent = orderedStatsRow[key as keyof typeof orderedStatsRow];
this.statisticsKeyToCellMap.set(key, valueCell);
}

Expand Down Expand Up @@ -203,26 +216,54 @@ export class ScreenshotDialog extends Overlay {
}

private populateStatistics() {
const statsRow = this.screenshotManager.screenshotStatistics;
const statsRow = this.parseStatistics(
this.screenshotManager.screenshotLoadStats,
);

for (const key in statsRow) {
if (key === "timeElapsedString") {
const headerRow = this.statisticsTable.rows[0];
const keyHeader = headerRow.cells[0];
const time = statsRow[key];
if (time === null) {
keyHeader.textContent = "Screenshot in progress (statistics loading)";
} else {
keyHeader.textContent = `Screenshot in progress for ${statsRow[key]}s`;
}
continue;
}
this.statisticsKeyToCellMap.get(key)!.textContent = String(
statsRow[key as keyof typeof statsRow],
);
}
}

private parseStatistics(
currentStatistics: ScreenshotLoadStatistics | null,
): UIScreenshotStatistics {
const nowtime = Date.now();
if (currentStatistics === null) {
return {
timeElapsedString: "Loading...",
chunkUsageDescription: "Loading...",
gpuMemoryUsageDescription: "Loading...",
downloadSpeedDescription: "Loading...",
};
}

const percentLoaded =
currentStatistics.visibleChunksTotal === 0
? 0
: (100 * currentStatistics.visibleChunksGpuMemory) /
currentStatistics.visibleChunksTotal;
const percentGpuUsage =
(100 * currentStatistics.visibleGpuMemory) /
currentStatistics.gpuMemoryCapacity;
const gpuMemoryUsageInMB = currentStatistics.visibleGpuMemory / 1000000;
const totalMemoryInMB = currentStatistics.gpuMemoryCapacity / 1000000;
const latency = isNaN(currentStatistics.downloadLatency)
? 0
: currentStatistics.downloadLatency;
const passedTimeInSeconds =
(nowtime - this.screenshotManager.screenshotStartTime) / 1000;

return {
timeElapsedString: `${passedTimeInSeconds.toFixed(0)} seconds`,
chunkUsageDescription: `${currentStatistics.visibleChunksGpuMemory} out of ${currentStatistics.visibleChunksTotal} (${percentLoaded.toFixed(2)}%)`,
gpuMemoryUsageDescription: `${gpuMemoryUsageInMB.toFixed(0)}MB / ${totalMemoryInMB.toFixed(0)}MB (${percentGpuUsage.toFixed(2)}% of total)`,
downloadSpeedDescription: `${currentStatistics.visibleChunksDownloading} at ${latency.toFixed(0)}ms latency`,
};
}

private debouncedUpdateUIElements = debounce(() => {
this.updateSetupUIVisibility();
this.updateStatisticsTableDisplayBasedOnMode();
Expand Down
153 changes: 42 additions & 111 deletions src/util/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,55 +15,22 @@
*/

import type { RenderedPanel } from "#src/display_context.js";
import type {
ScreenshotActionState,
StatisticsActionState,
ScreenshotChunkStatistics,
} from "#src/python_integration/screenshots.js";
import { RenderedDataPanel } from "#src/rendered_data_panel.js";
import { RefCounted } from "#src/util/disposable.js";
import {NullarySignal} from "#src/util/signal.js";
import { NullarySignal, Signal } from "#src/util/signal.js";
import { ScreenshotMode } from "#src/util/trackable_screenshot_mode.js";
import type { Viewer } from "#src/viewer.js";

const SCREENSHOT_TIMEOUT = 5000;

interface ScreenshotLoadStatistics {
numGpuLoadedVisibleChunks: number;
export interface ScreenshotLoadStatistics extends ScreenshotChunkStatistics {
timestamp: number;
}

interface ScreenshotActionState {
viewerState: any;
selectedValues: any;
screenshot: {
id: string;
image: string;
imageType: string;
depthData: string | undefined;
width: number;
height: number;
};
}

export interface StatisticsActionState {
viewerState: any;
selectedValues: any;
screenshotStatistics: {
id: string;
chunkSources: any[];
total: {
downloadLatency: number;
visibleChunksDownloading: number;
visibleChunksFailed: number;
visibleChunksGpuMemory: number;
visibleChunksSystemMemory: number;
visibleChunksTotal: number;
visibleGpuMemory: number;
};
};
}

interface UIScreenshotStatistics {
timeElapsedString: string | null;
chunkUsageDescription: string;
gpuMemoryUsageDescription: string;
downloadSpeedDescription: string;
gpuMemoryCapacity: number;
}

interface ViewportBounds {
Expand Down Expand Up @@ -160,23 +127,15 @@ async function extractViewportScreenshot(
}

export class ScreenshotManager extends RefCounted {
public screenshotId: number = -1;
public screenshotScale: number = 1;
private filename: string = "";
private screenshotLoadStats: ScreenshotLoadStatistics = {
numGpuLoadedVisibleChunks: 0,
timestamp: 0,
};
private lastUpdateTimestamp = 0;
private screenshotStartTime = 0;
private lastSavedStatistics: UIScreenshotStatistics = {
timeElapsedString: null,
chunkUsageDescription: "",
gpuMemoryUsageDescription: "",
downloadSpeedDescription: "",
};
private lastUpdateTimestamp: number = 0;
private gpuMemoryChangeTimestamp: number = 0;
screenshotId: number = -1;
screenshotScale: number = 1;
screenshotLoadStats: ScreenshotLoadStatistics | null = null;
screenshotStartTime = 0;
screenshotMode: ScreenshotMode = ScreenshotMode.OFF;
statisticsUpdated = new NullarySignal();
statisticsUpdated = new Signal<(state: ScreenshotLoadStatistics) => void>();
screenshotFinished = new NullarySignal();

constructor(public viewer: Viewer) {
Expand All @@ -193,9 +152,15 @@ export class ScreenshotManager extends RefCounted {
this.registerDisposer(
this.viewer.screenshotHandler.sendStatisticsRequested.add(
(actionState) => {
this.persistStatisticsData(actionState);
this.checkAndHandleStalledScreenshot(actionState);
this.statisticsUpdated.dispatch();
this.screenshotLoadStats = {
...actionState.screenshotStatistics.total,
timestamp: Date.now(),
gpuMemoryCapacity:
this.viewer.chunkQueueManager.capacities.gpuMemory.sizeLimit
.value,
};
this.statisticsUpdated.dispatch(this.screenshotLoadStats);
},
),
);
Expand All @@ -220,19 +185,15 @@ export class ScreenshotManager extends RefCounted {
this.viewer.display.screenshotMode.value = ScreenshotMode.FORCE;
}

get screenshotStatistics(): UIScreenshotStatistics {
return this.lastSavedStatistics;
}

private handleScreenshotStarted() {
const { viewer } = this;
const shouldIncreaseCanvasSize = this.screenshotScale !== 1;

this.screenshotStartTime = this.lastUpdateTimestamp = Date.now();
this.screenshotLoadStats = {
numGpuLoadedVisibleChunks: 0,
timestamp: 0,
};
this.screenshotStartTime =
this.lastUpdateTimestamp =
this.gpuMemoryChangeTimestamp =
Date.now();
this.screenshotLoadStats = null;

if (shouldIncreaseCanvasSize) {
const oldSize = {
Expand Down Expand Up @@ -282,31 +243,33 @@ export class ScreenshotManager extends RefCounted {
* visible chunks has not changed after a certain timeout, and the display has not updated, force a screenshot.
*/
private checkAndHandleStalledScreenshot(actionState: StatisticsActionState) {
if (this.screenshotLoadStats === null) {
return;
}
const total = actionState.screenshotStatistics.total;
const newStats: ScreenshotLoadStatistics = {
numGpuLoadedVisibleChunks: total.visibleChunksGpuMemory,
const newStats = {
visibleChunksGpuMemory: total.visibleChunksGpuMemory,
timestamp: Date.now(),
totalGpuMemory:
this.viewer.chunkQueueManager.capacities.gpuMemory.sizeLimit.value,
};
if (this.screenshotLoadStats.timestamp === 0) {
this.screenshotLoadStats = newStats;
return;
}
const oldStats = this.screenshotLoadStats;
if (
oldStats.numGpuLoadedVisibleChunks === newStats.numGpuLoadedVisibleChunks
oldStats.visibleChunksGpuMemory === newStats.visibleChunksGpuMemory &&
oldStats.gpuMemoryCapacity === newStats.totalGpuMemory
) {
if (
newStats.timestamp - oldStats.timestamp > SCREENSHOT_TIMEOUT &&
newStats.timestamp - this.gpuMemoryChangeTimestamp >
SCREENSHOT_TIMEOUT &&
Date.now() - this.lastUpdateTimestamp > SCREENSHOT_TIMEOUT
) {
const totalChunks = total.visibleChunksTotal;
console.warn(
`Forcing screenshot: screenshot is likely stuck, no change in GPU chunks after ${SCREENSHOT_TIMEOUT}ms. Last visible chunks: ${newStats.numGpuLoadedVisibleChunks}/${totalChunks}`,
`Forcing screenshot: screenshot is likely stuck, no change in GPU chunks after ${SCREENSHOT_TIMEOUT}ms. Last visible chunks: ${total.visibleChunksGpuMemory}/${total.visibleChunksTotal}`,
);
this.forceScreenshot();
}
} else {
this.screenshotLoadStats = newStats;
this.gpuMemoryChangeTimestamp = newStats.timestamp;
}
}

Expand Down Expand Up @@ -359,39 +322,7 @@ export class ScreenshotManager extends RefCounted {
}

private resetStatistics() {
this.lastSavedStatistics = {
timeElapsedString: null,
chunkUsageDescription: "",
gpuMemoryUsageDescription: "",
downloadSpeedDescription: "",
};
}

private persistStatisticsData(actionState: StatisticsActionState) {
const nowtime = Date.now();
const total = actionState.screenshotStatistics.total;
const maxGpuMemory =
this.viewer.chunkQueueManager.capacities.gpuMemory.sizeLimit.value;

const percentLoaded =
total.visibleChunksTotal === 0
? 0
: (100 * total.visibleChunksGpuMemory) / total.visibleChunksTotal;
const percentGpuUsage = (100 * total.visibleGpuMemory) / maxGpuMemory;
const gpuMemoryUsageInMB = total.visibleGpuMemory / 1000000;
const totalMemoryInMB = maxGpuMemory / 1000000;
const latency = isNaN(total.downloadLatency) ? 0 : total.downloadLatency;
const passedTimeInSeconds = (
(nowtime - this.screenshotStartTime) /
1000
).toFixed(0);

this.lastSavedStatistics = {
timeElapsedString: passedTimeInSeconds,
chunkUsageDescription: `${total.visibleChunksGpuMemory} out of ${total.visibleChunksTotal} (${percentLoaded.toFixed(2)}%)`,
gpuMemoryUsageDescription: `${gpuMemoryUsageInMB.toFixed(0)}MB / ${totalMemoryInMB.toFixed(0)}MB (${percentGpuUsage.toFixed(2)}% of total)`,
downloadSpeedDescription: `${total.visibleChunksDownloading} at ${latency.toFixed(0)}ms latency`,
};
this.screenshotLoadStats = null;
}

private generateFilename(width: number, height: number): string {
Expand Down

0 comments on commit f928d07

Please sign in to comment.