From cb5e201229bb9f0020fc51edc0755480b919554f Mon Sep 17 00:00:00 2001 From: Chris Jordan Date: Tue, 22 Oct 2024 12:08:37 -0400 Subject: [PATCH] temp history prototype --- src/datasource/graphene/backend.ts | 32 +++++- src/datasource/graphene/frontend.ts | 116 +++++++++++++-------- src/layer/segmentation/index.ts | 3 + src/segmentation_display_state/frontend.ts | 5 + src/shared_disjoint_sets.ts | 18 ++++ src/ui/default_viewer_setup.ts | 60 +++++++++++ src/ui/url_hash_binding.ts | 23 +++- src/util/disposable.ts | 2 +- src/worker_rpc.ts | 13 +++ 9 files changed, 223 insertions(+), 49 deletions(-) diff --git a/src/datasource/graphene/backend.ts b/src/datasource/graphene/backend.ts index b49783b22..394641c5f 100644 --- a/src/datasource/graphene/backend.ts +++ b/src/datasource/graphene/backend.ts @@ -62,6 +62,7 @@ import { computeChunkBounds } from "#src/sliceview/volume/backend.js"; import { Uint64Set } from "#src/uint64_set.js"; import { fetchSpecialHttpByteRange } from "#src/util/byte_range_http_requests.js"; import type { CancellationToken } from "#src/util/cancellation.js"; +import { invokeDisposers } from "#src/util/disposable.js"; import { vec3, vec3Key } from "#src/util/geom.js"; import { responseArrayBuffer, responseJson } from "#src/util/http_request.js"; import type { @@ -365,7 +366,10 @@ export class ChunkedGraphLayer extends withSegmentationLayerBackendState( leafRequestsActive: SharedWatchableValue; nBitsForLayerId: SharedWatchableValue; + readonly sessionId = self.crypto.randomUUID(); + constructor(rpc: RPC, options: any) { + console.log("ChunkedGraphLayer constructor"); super(rpc, options); this.source = this.registerDisposer( rpc.getRef(options.source), @@ -377,9 +381,26 @@ export class ChunkedGraphLayer extends withSegmentationLayerBackendState( this.registerDisposer( this.chunkManager.recomputeChunkPriorities.add(() => { this.updateChunkPriorities(); - this.debouncedupdateDisplayState(); + this.debouncedUpdateDisplayState(); }), ); + this.registerDisposer(() => { + console.log("ChunkedGraphLayer disposed really", this.sessionId); + }); + } + + disposed() { + console.log("ChunkedGraphLayer disposed called"); + if (this.refCount === 0) { + console.log("ChunkedGraphLayer invoking disposers", this.sessionId); + const { disposers } = this; + if (disposers !== undefined) { + invokeDisposers(disposers); + this.disposers = undefined; + } + this.wasDisposed = true; + } + super.disposed(); } attach( @@ -500,7 +521,9 @@ export class ChunkedGraphLayer extends withSegmentationLayerBackendState( } } - private debouncedupdateDisplayState = debounce(() => { + private debouncedUpdateDisplayState = debounce(() => { + console.log("ChunkedGraphLayer debounced update invoked", this.sessionId); + if (this.wasDisposed) return; this.updateDisplayState(); }, 100); @@ -542,6 +565,11 @@ export class ChunkedGraphLayer extends withSegmentationLayerBackendState( this.segmentEquivalences.delete([...this.segmentEquivalences.setElements(Uint64.parseString(root))].filter(x => !leaves.has(x) && !this.visibleSegments.has(x))); }*/ + // console.log( + // "debugDisposed", + // this.segmentEquivalences.debugDisposed, + // this.segmentEquivalences.sessionId, + // ); const filteredLeaves = [...leaves].filter( (x) => !this.segmentEquivalences.has(x), ); diff --git a/src/datasource/graphene/frontend.ts b/src/datasource/graphene/frontend.ts index 24f90734d..4884f9fb0 100644 --- a/src/datasource/graphene/frontend.ts +++ b/src/datasource/graphene/frontend.ts @@ -631,7 +631,7 @@ async function getVolumeDataSource( info, credentialsProvider, volume, - state, + state, // how to apply this to connection instead ); const { modelSpace } = info; const subsources: DataSubsourceEntry[] = [ @@ -708,56 +708,56 @@ export class GrapheneDataSource extends PrecomputedDataSource { return "Graphene file-backed data source"; } - get(options: GetDataSourceOptions): Promise { + async get(options: GetDataSourceOptions): Promise { const { url: providerUrl, parameters } = parseProviderUrl( options.providerUrl, ); - return options.chunkManager.memoize.getUncounted( - { type: "graphene:get", providerUrl, parameters }, - async (): Promise => { - const { url, credentialsProvider } = parseSpecialUrl( - providerUrl, - options.credentialsManager, - ); - let metadata: any; - try { - metadata = await getJsonMetadata( - options.chunkManager, - credentialsProvider, - url, - ); - } catch (e) { - if (isNotFoundError(e)) { - if (parameters["type"] === "mesh") { - console.log("does this happen?"); - } - } - throw e; + // return options.chunkManager.memoize.getUncounted( + // { type: "graphene:get", providerUrl, parameters }, + // async (): Promise => { + const { url, credentialsProvider } = parseSpecialUrl( + providerUrl, + options.credentialsManager, + ); + let metadata: any; + try { + metadata = await getJsonMetadata( + options.chunkManager, + credentialsProvider, + url, + ); + } catch (e) { + if (isNotFoundError(e)) { + if (parameters["type"] === "mesh") { + console.log("does this happen?"); } - verifyObject(metadata); - const redirect = verifyOptionalObjectProperty( + } + throw e; + } + verifyObject(metadata); + const redirect = verifyOptionalObjectProperty( + metadata, + "redirect", + verifyString, + ); + if (redirect !== undefined) { + throw new RedirectError(redirect); + } + const t = verifyOptionalObjectProperty(metadata, "@type", verifyString); + switch (t) { + case "neuroglancer_multiscale_volume": + case undefined: + return await getVolumeDataSource( + options, + credentialsProvider, + url, metadata, - "redirect", - verifyString, ); - if (redirect !== undefined) { - throw new RedirectError(redirect); - } - const t = verifyOptionalObjectProperty(metadata, "@type", verifyString); - switch (t) { - case "neuroglancer_multiscale_volume": - case undefined: - return await getVolumeDataSource( - options, - credentialsProvider, - url, - metadata, - ); - default: - throw new Error(`Invalid type: ${JSON.stringify(t)}`); - } - }, - ); + default: + throw new Error(`Invalid type: ${JSON.stringify(t)}`); + } + // }, + // ); } } @@ -908,6 +908,7 @@ class GrapheneState extends RefCounted implements Trackable { } toJSON() { + console.log("GS toJSON"); return { [MULTICUT_JSON_KEY]: this.multicutState.toJSON(), [MERGE_JSON_KEY]: this.mergeState.toJSON(), @@ -1314,6 +1315,7 @@ class GraphConnection extends SegmentationGraphSourceConnection { private chunkSource: GrapheneMultiscaleVolumeChunkSource, public state: GrapheneState, ) { + console.log("GraphConnection constructor"); super(graph, layer.displayState.segmentationGroupState.value); const segmentsState = layer.displayState.segmentationGroupState.value; this.previousVisibleSegmentCount = segmentsState.visibleSegments.size; @@ -1404,6 +1406,26 @@ class GraphConnection extends SegmentationGraphSourceConnection { mergeAnnotationState.source.add(mergeToLine(merge)); } + this.registerDisposer( + merges.changed.add(() => { + for (const annotation of mergeAnnotationState.source) { + if (!merges.value.map((x) => x.id).includes(annotation.id)) { + mergeAnnotationState.source.delete( + mergeAnnotationState.source.getReference(annotation.id), + ); + } + } + const existingAnnotationIds = [...mergeAnnotationState.source].map( + (x) => x.id, + ); + for (const merge of merges.value) { + if (!existingAnnotationIds.includes(merge.id)) { + mergeAnnotationState.source.add(mergeToLine(merge)); + } + } + }), + ); + // initialize source changes this.registerDisposer( mergeAnnotationState.source.childAdded.add((x) => { @@ -2453,6 +2475,10 @@ class SliceViewPanelChunkedGraphLayer extends SliceViewPanelRenderLayer { }); this.registerDisposer(sharedObject.visibility.add(this.visibility)); + this.registerDisposer(() => { + console.log("SliceViewPanelChunkedGraphLayer disposed"); + }); + this.registerDisposer( this.leafRequestsActive.changed.add(() => { this.showOrHideMessage(this.leafRequestsActive.value); diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index df2b86f6b..c23044075 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -286,6 +286,7 @@ export class SegmentationUserLayerGroupState [this.graph], ), ), + "segmentEquivalences", ), ); localSegmentEquivalences = false; @@ -300,6 +301,7 @@ export class SegmentationUserLayerGroupState SharedDisjointUint64Sets.makeWithCounterpart( this.layer.manager.rpc, this.segmentEquivalences.disjointSets.visibleSegmentEquivalencePolicy, + "temporarySegmentEquivalences", ), ); useTemporaryVisibleSegments = this.layer.registerDisposer( @@ -639,6 +641,7 @@ export class SegmentationUserLayer extends Base { ); constructor(managedLayer: Borrowed) { + console.log("SegmentationUserLayer constructor"); super(managedLayer); this.registerDisposer( registerNestedSync((context, group) => { diff --git a/src/segmentation_display_state/frontend.ts b/src/segmentation_display_state/frontend.ts index ab4f210ff..5f22ea0e3 100644 --- a/src/segmentation_display_state/frontend.ts +++ b/src/segmentation_display_state/frontend.ts @@ -1091,6 +1091,11 @@ export class SegmentationLayerSharedObject extends Base { } } + disposed(): void { + console.log("SegmentationLayerSharedObject disposed"); + super.disposed(); + } + initializeCounterpartWithChunkManager(options: any) { const { displayState } = this; options.chunkManager = this.chunkManager.rpcId; diff --git a/src/shared_disjoint_sets.ts b/src/shared_disjoint_sets.ts index 2f1d9b20d..b25438097 100644 --- a/src/shared_disjoint_sets.ts +++ b/src/shared_disjoint_sets.ts @@ -42,6 +42,10 @@ export class SharedDisjointUint64Sets disjointSets = new DisjointUint64Sets(); changed = new NullarySignal(); + debugDisposed = false; + + readonly sessionId = self.crypto.randomUUID(); + /** * For compatibility with `WatchableValueInterface`. */ @@ -49,11 +53,22 @@ export class SharedDisjointUint64Sets return this; } + // constructor() { + // super(); + // console.log("SharedDisjointUint64Sets constructor", this.sessionId); + // } + static makeWithCounterpart( rpc: RPC, highBitRepresentative: WatchableValueInterface, + name = "foo", ) { const obj = new SharedDisjointUint64Sets(); + console.log( + "making SharedDisjointUint64Sets counterpart", + name, + obj.sessionId, + ); obj.disjointSets.visibleSegmentEquivalencePolicy = highBitRepresentative; obj.registerDisposer( highBitRepresentative.changed.add(() => { @@ -64,10 +79,13 @@ export class SharedDisjointUint64Sets if (highBitRepresentative.value) { updateHighBitRepresentative(obj); } + console.log("returning", obj); return obj; } disposed() { + console.log("SharedDisjointUint64Sets disposed!", this.sessionId); + this.debugDisposed = true; this.disjointSets = undefined; this.changed = undefined; super.disposed(); diff --git a/src/ui/default_viewer_setup.ts b/src/ui/default_viewer_setup.ts index 41022f05b..e9853d6e8 100644 --- a/src/ui/default_viewer_setup.ts +++ b/src/ui/default_viewer_setup.ts @@ -179,5 +179,65 @@ export function setupDefaultViewer() { bindDefaultCopyHandler(viewer); bindDefaultPasteHandler(viewer); + const downloadObject = (obj: any, filename: string) => { + const a = document.createElement("a"); + const file = new Blob([JSON.stringify(obj)], { type: "application/json" }); + a.href = URL.createObjectURL(file); + a.download = filename; + a.click(); + }; + + (window as any).saveHistory = (sessionId: string) => { + const res: any[] = []; + let historyIndex = 0; + let state: string | null = null; + while ( + (state = localStorage.getItem(`${sessionId}_${historyIndex}`)) !== null + ) { + res.push(JSON.parse(state)); + historyIndex++; + } + downloadObject(res, "history.json"); + }; + + let inReplay = false; + + (window as any).ngReplay = (sessionId: string) => { + if (inReplay) return; + hashBinding.recording = false; + inReplay = true; + const startTime = Date.now(); + let historyIndex = 0; + let nextState = localStorage.getItem(`${sessionId}_${historyIndex}`); + if (!nextState) { + console.log(`no history for session ${sessionId}`); + } + let nextTime = parseInt( + localStorage.getItem(`${sessionId}_${historyIndex}_time`)!, + ); + + const loop = () => { + const elapsedTime = Date.now() - startTime; + console.log("replay", elapsedTime, historyIndex, nextTime - elapsedTime); + if (nextTime > 0 && elapsedTime >= nextTime) { + // set new state + viewer.state.restoreState(JSON.parse(nextState!)); + // get next states + historyIndex++; + nextState = localStorage.getItem(`${sessionId}_${historyIndex}`); + if (!nextState) { + inReplay = false; + console.log("done with replay"); + return; + } + nextTime = parseInt( + localStorage.getItem(`${sessionId}_${historyIndex}_time`)!, + ); + } + requestAnimationFrame(loop); + }; + requestAnimationFrame(loop); + }; + return viewer; } diff --git a/src/ui/url_hash_binding.ts b/src/ui/url_hash_binding.ts index ef09b27c3..fc65a9e11 100644 --- a/src/ui/url_hash_binding.ts +++ b/src/ui/url_hash_binding.ts @@ -69,12 +69,20 @@ export class UrlHashBinding extends RefCounted { private defaultFragment: string; + readonly sessionId = self.crypto.randomUUID(); + + historyIndex = 0; + startTime = Date.now(); + + recording = true; + constructor( public root: Trackable, public credentialsManager: CredentialsManager, options: UrlHashBindingOptions = {}, ) { super(); + const { updateDelayMilliseconds = 200, defaultFragment = "{}" } = options; this.registerEventListener(window, "hashchange", () => this.updateFromUrlHash(), @@ -97,7 +105,8 @@ export class UrlHashBinding extends RefCounted { const { generation } = cacheState; if (generation !== this.prevStateGeneration) { this.prevStateGeneration = cacheState.generation; - const stateString = encodeFragment(JSON.stringify(cacheState.value)); + const jsonString = JSON.stringify(cacheState.value); + const stateString = encodeFragment(jsonString); if (stateString !== this.prevStateString) { this.prevStateString = stateString; if (decodeURIComponent(stateString) === "{}") { @@ -105,6 +114,18 @@ export class UrlHashBinding extends RefCounted { } else { history.replaceState(null, "", "#!" + stateString); } + if (this.recording) { + console.log("recording", this.sessionId, this.historyIndex); + localStorage.setItem( + `${this.sessionId}_${this.historyIndex}_time`, + (Date.now() - this.startTime).toString(), + ); + localStorage.setItem( + `${this.sessionId}_${this.historyIndex}`, + jsonString, + ); + this.historyIndex++; + } } } } diff --git a/src/util/disposable.ts b/src/util/disposable.ts index dbcb95eea..72f0c60bd 100644 --- a/src/util/disposable.ts +++ b/src/util/disposable.ts @@ -49,7 +49,7 @@ export function registerEventListener( export class RefCounted implements Disposable { public refCount = 1; wasDisposed: boolean | undefined; - private disposers: Disposer[]; + disposers: Disposer[]; addRef() { ++this.refCount; return this; diff --git a/src/worker_rpc.ts b/src/worker_rpc.ts index 3ea514efd..3f5a4c128 100644 --- a/src/worker_rpc.ts +++ b/src/worker_rpc.ts @@ -340,6 +340,10 @@ registerRPC("SharedObject.dispose", function (x) { if (DEBUG) { console.log(`[${IS_WORKER}] #rpc objects: ${this.numObjects}`); } + if (obj.RPC_TYPE_ID === "ChunkedGraphLayer") { + console.log("RPC DISPOSING CHUNKED_GRAPH_LAYER"); + } + console.log("RPC DISPOSE", obj); obj.disposed(); this.delete(obj.rpcId!); obj.rpcId = null; @@ -392,6 +396,15 @@ registerRPC("SharedObject.new", function (x) { const typeName = x.type; const constructorFunction = sharedObjectConstructors.get(typeName)!; const obj = new constructorFunction(rpc, x); + + // console.log("test", obj); + + if ((obj as any).constructor.name === "SharedDisjointUint64Sets") { + console.log( + "SharedObject.new SharedDisjointUint64Sets", + (obj as any).sessionId, + ); + } // Counterpart objects start with a reference count of zero. --obj.refCount; });