From 09cbff15a302b4173aa2b7521da61e5867961bd3 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 27 Jan 2021 14:53:30 +0100 Subject: [PATCH 1/6] don't crash front-end if isosurface request fails; retry isosurface requests; indicate loading state of isosurfaces also on initial request --- .../model/reducers/annotation_reducer.js | 1 + .../oxalis/model/sagas/isosurface_saga.js | 78 ++++++++++++------- frontend/javascripts/oxalis/store.js | 4 +- .../oxalis/view/right-menu/meshes_view.js | 9 ++- 4 files changed, 57 insertions(+), 35 deletions(-) diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index 9472b1ca4fc..b9f9159250b 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -119,6 +119,7 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { return updateKey2(state, "isosurfaces", cellId.toString(), { segmentId: cellId, seedPosition, + isLoading: false, }); } diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js index 1cd1846d47b..81d96ccadc1 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js @@ -1,6 +1,8 @@ // @flow import { saveAs } from "file-saver"; +import { sleep } from "libs/utils"; +import ErrorHandling from "libs/error_handling"; import type { APIDataset } from "types/api_flow_types"; import { ResolutionInfo, getResolutionInfo } from "oxalis/model/accessors/dataset_accessor"; import { @@ -46,6 +48,9 @@ import { saveNowAction } from "oxalis/model/actions/save_actions"; import Toast from "libs/toast"; import messages from "messages"; +const MAX_RETRY_COUNT = 5; +const RETRY_WAIT_TIME = 5000; + const isosurfacesMap: Map> = new Map(); const cubeSize = [256, 256, 256]; const modifiedCells: Set = new Set(); @@ -207,9 +212,10 @@ function* loadIsosurfaceWithNeighbors( seedPosition != null ? seedPosition : yield* select(state => getFlooredPosition(state.flycam)); const clippedPosition = clipPositionToCubeBoundary(position, zoomStep, resolutionInfo); let positionsToRequest = [clippedPosition]; - if (seedPosition) { - yield* put(addIsosurfaceAction(segmentId, seedPosition)); - } + + yield* put(addIsosurfaceAction(segmentId, position)); + yield* put(startRefreshingIsosurfaceAction(segmentId)); + while (positionsToRequest.length > 0) { const currentPosition = positionsToRequest.shift(); const neighbors = yield* call( @@ -225,6 +231,8 @@ function* loadIsosurfaceWithNeighbors( isInitialRequest = false; positionsToRequest = positionsToRequest.concat(neighbors); } + + yield* put(finishedRefreshingIsosurfaceAction(segmentId)); } function hasBatchCounterExceededLimit(segmentId: number): boolean { @@ -267,34 +275,46 @@ function* maybeLoadIsosurface( const volumeTracing = yield* select(state => state.tracing.volume); // Fetch from datastore if no volumetracing exists or if the tracing has a fallback layer. const useDataStore = volumeTracing == null || volumeTracing.fallbackLayer != null; - const { buffer: responseBuffer, neighbors } = yield* call( - computeIsosurface, - useDataStore ? dataStoreUrl : tracingStoreUrl, - layer, - { - position: clippedPosition, - zoomStep, - segmentId, - voxelDimensions, - cubeSize, - scale, - }, - ); - // Check again whether the limit was exceeded, since this variable could have been - // set in the mean time by ctrl-clicking the segment to remove it - if (hasBatchCounterExceededLimit(segmentId)) { - return []; - } - const vertices = new Float32Array(responseBuffer); - if (removeExistingIsosurface) { - getSceneController().removeIsosurfaceById(segmentId); + let retryCount = 0; + while (retryCount < MAX_RETRY_COUNT) { + try { + const { buffer: responseBuffer, neighbors } = yield* call( + computeIsosurface, + useDataStore ? dataStoreUrl : tracingStoreUrl, + layer, + { + position: clippedPosition, + zoomStep, + segmentId, + voxelDimensions, + cubeSize, + scale, + }, + ); + + // Check again whether the limit was exceeded, since this variable could have been + // set in the mean time by ctrl-clicking the segment to remove it + if (hasBatchCounterExceededLimit(segmentId)) { + return []; + } + const vertices = new Float32Array(responseBuffer); + if (removeExistingIsosurface) { + getSceneController().removeIsosurfaceById(segmentId); + } + getSceneController().addIsosurfaceFromVertices(vertices, segmentId); + + return neighbors.map(neighbor => + getNeighborPosition(clippedPosition, neighbor, zoomStep, resolutionInfo), + ); + } catch (exception) { + retryCount++; + ErrorHandling.notify(exception); + console.warn("Retrying isosurface generation..."); + yield* call(sleep, RETRY_WAIT_TIME); + } } - getSceneController().addIsosurfaceFromVertices(vertices, segmentId); - - return neighbors.map(neighbor => - getNeighborPosition(clippedPosition, neighbor, zoomStep, resolutionInfo), - ); + return []; } function* downloadIsosurfaceCellById(cellId: number): Saga { diff --git a/frontend/javascripts/oxalis/store.js b/frontend/javascripts/oxalis/store.js index 08a17bbf4f4..3e657b4bc44 100644 --- a/frontend/javascripts/oxalis/store.js +++ b/frontend/javascripts/oxalis/store.js @@ -453,11 +453,11 @@ type UiInformation = { +isRefreshingIsosurfaces: boolean, }; -type IsosurfaceInformation = { +export type IsosurfaceInformation = {| +segmentId: number, +seedPosition: Vector3, +isLoading: boolean, -}; +|}; export type OxalisState = {| +datasetConfiguration: DatasetConfiguration, diff --git a/frontend/javascripts/oxalis/view/right-menu/meshes_view.js b/frontend/javascripts/oxalis/view/right-menu/meshes_view.js index d91f2de2619..5d3ac465543 100644 --- a/frontend/javascripts/oxalis/view/right-menu/meshes_view.js +++ b/frontend/javascripts/oxalis/view/right-menu/meshes_view.js @@ -8,7 +8,7 @@ import _ from "lodash"; import type { ExtractReturn } from "libs/type_helpers"; import type { MeshMetaData, RemoteMeshMetaData } from "types/api_flow_types"; -import type { OxalisState } from "oxalis/store"; +import type { OxalisState, IsosurfaceInformation } from "oxalis/store"; import Store from "oxalis/store"; import Model from "oxalis/model"; import type { Vector3 } from "oxalis/constants"; @@ -137,7 +137,7 @@ type StateProps = {| |}; type DispatchProps = ExtractReturn; -type Props = { ...OwnProps, ...DispatchProps, ...StateProps }; +type Props = {| ...OwnProps, ...DispatchProps, ...StateProps |}; const getCheckboxStyle = isLoaded => isLoaded @@ -263,10 +263,11 @@ class MeshesView extends React.Component< ); - const renderIsosurfaceListItem = (isosurface: Object) => { + const renderIsosurfaceListItem = (isosurface: IsosurfaceInformation) => { const { segmentId, seedPosition, isLoading } = isosurface; const centeredCell = getIdForPos(getPosition(this.props.flycam)); - const actionVisibility = segmentId === this.state.hoveredListItem ? "visible" : "hidden"; + const actionVisibility = + isLoading || segmentId === this.state.hoveredListItem ? "visible" : "hidden"; return ( Date: Wed, 27 Jan 2021 14:54:40 +0100 Subject: [PATCH 2/6] use exponential back off --- frontend/javascripts/oxalis/model/sagas/isosurface_saga.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js index 81d96ccadc1..0565669dbc1 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js @@ -311,7 +311,7 @@ function* maybeLoadIsosurface( retryCount++; ErrorHandling.notify(exception); console.warn("Retrying isosurface generation..."); - yield* call(sleep, RETRY_WAIT_TIME); + yield* call(sleep, RETRY_WAIT_TIME * 2 ** retryCount); } } return []; From dbfc03405d2949d3e18a7dd5a1e3e708a9644d09 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 27 Jan 2021 15:11:55 +0100 Subject: [PATCH 3/6] improve seed position when importing isosurface via STL file --- frontend/javascripts/oxalis/model/sagas/isosurface_saga.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js index 0565669dbc1..ef45ccf4630 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js @@ -354,7 +354,11 @@ function* importIsosurfaceFromStl(action: ImportIsosurfaceFromStlAction): Saga getFlooredPosition(state.flycam)); + yield* put(addIsosurfaceAction(segmentId, seedPosition)); } function* removeIsosurface( From c20744747bdd2b310decc80e92073902e53d0f9f Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 27 Jan 2021 15:22:02 +0100 Subject: [PATCH 4/6] update changelog --- CHANGELOG.unreleased.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 89a9fabb20f..e53da3f766a 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -11,13 +11,13 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released [Commits](https://github.com/scalableminds/webknossos/compare/21.02.0...HEAD) ### Added -- +- The "Meshes" tab was overhauled, so that it displays generated isosurfaces and imported meshes. Generated isosurfaces can be jumped to, reloaded, downloaded and removed. [#4917](https://github.com/scalableminds/webknossos/pull/4917) ### Changed -- +- Make the isosurface feature in the meshes tab more robust. If a request fails, a retry is initiated. [#5102](https://github.com/scalableminds/webknossos/pull/5102) ### Fixed - ### Removed -- +- The isosurface setting was removed. Instead, isosurfaces can be generated via the "Meshes" tab. Also note that the Shift+Click binding for generating an isosurface was removed (for now). Please refer to the "Meshes" tab, too. [#4917](https://github.com/scalableminds/webknossos/pull/4917) From 0db15fb01c2a2c40dfb059e7b954e0eb436fcb41 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 28 Jan 2021 15:09:21 +0100 Subject: [PATCH 5/6] ensure the same isosurface is not added twice to the store --- frontend/javascripts/oxalis/model/sagas/isosurface_saga.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js index ef45ccf4630..4492244f01a 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js @@ -213,7 +213,10 @@ function* loadIsosurfaceWithNeighbors( const clippedPosition = clipPositionToCubeBoundary(position, zoomStep, resolutionInfo); let positionsToRequest = [clippedPosition]; - yield* put(addIsosurfaceAction(segmentId, position)); + const hasIsosurface = yield* select(state => state.isosurfaces[segmentId] != null); + if (!hasIsosurface) { + yield* put(addIsosurfaceAction(segmentId, position)); + } yield* put(startRefreshingIsosurfaceAction(segmentId)); while (positionsToRequest.length > 0) { From 03c9b0729e43a6c816eeb47b8315c13c88dcbf9d Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 28 Jan 2021 16:31:44 +0100 Subject: [PATCH 6/6] fix CI --- .../oxalis/model/reducers/annotation_reducer.js | 9 ++++++--- frontend/javascripts/oxalis/store.js | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index b9f9159250b..72e6463a9e8 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -116,7 +116,8 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { case "ADD_ISOSURFACE": { const { cellId, seedPosition } = action; - return updateKey2(state, "isosurfaces", cellId.toString(), { + // $FlowIgnore[incompatible-call] updateKey has problems with updating Objects as Dictionaries + return updateKey2(state, "isosurfaces", cellId, { segmentId: cellId, seedPosition, isLoading: false, @@ -125,14 +126,16 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { case "START_REFRESHING_ISOSURFACE": { const { cellId } = action; - return updateKey2(state, "isosurfaces", cellId.toString(), { + // $FlowIgnore[incompatible-call] updateKey has problems with updating Objects as Dictionaries + return updateKey2(state, "isosurfaces", cellId, { isLoading: true, }); } case "FINISHED_REFRESHING_ISOSURFACE": { const { cellId } = action; - return updateKey2(state, "isosurfaces", cellId.toString(), { + // $FlowIgnore[incompatible-call] updateKey has problems with updating Objects as Dictionaries + return updateKey2(state, "isosurfaces", cellId, { isLoading: false, }); } diff --git a/frontend/javascripts/oxalis/store.js b/frontend/javascripts/oxalis/store.js index 3e657b4bc44..a4206803f49 100644 --- a/frontend/javascripts/oxalis/store.js +++ b/frontend/javascripts/oxalis/store.js @@ -471,7 +471,7 @@ export type OxalisState = {| +viewModeData: ViewModeData, +activeUser: ?APIUser, +uiInformation: UiInformation, - +isosurfaces: { [segmentId: string]: IsosurfaceInformation }, + +isosurfaces: { [segmentId: number]: IsosurfaceInformation }, |}; const sagaMiddleware = createSagaMiddleware();