Skip to content

Commit 8ff0e2a

Browse files
committed
fix: use fresh workers for labelmap read/write to avoid WASM heap overflow
Replace resetWorker() (which terminated the shared ITK-wasm worker while in-flight operations still referenced it) with per-labelmap temporary workers. Each labelmap read/write gets its own fresh worker with a clean WASM heap, avoiding the 2GB signed pointer overflow in Emscripten without disrupting any concurrent operations on the shared worker.
1 parent 5935437 commit 8ff0e2a

File tree

5 files changed

+33
-23
lines changed

5 files changed

+33
-23
lines changed

src/io/import/processors/restoreStateFile.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { useViewStore } from '@/src/store/views';
2020
import { useViewConfigStore } from '@/src/store/view-configs';
2121
import { migrateManifest } from '@/src/io/state-file/migrations';
2222
import { useMessageStore } from '@/src/store/messages';
23-
import { resetWorker } from '@/src/io/itk/worker';
2423

2524
type LeafSource =
2625
| { type: 'uri'; uri: string; name: string; mime?: string }
@@ -137,12 +136,6 @@ export async function completeStateFileRestore(
137136

138137
useViewConfigStore().deserializeAll(manifest, stateIDToStoreID);
139138

140-
// Reset the ITK-wasm worker to free accumulated WASM heap from loading
141-
// base images. Without this, loading large labelmaps on the same worker
142-
// can push the heap past 2GB, causing signed pointer overflow in
143-
// Emscripten's ccall (pointers > 2^31 wrap negative).
144-
await resetWorker();
145-
146139
const segmentGroupIDMap = await useSegmentGroupStore().deserialize(
147140
manifest,
148141
stateFiles,

src/io/itk/worker.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,6 @@ export function getWorker() {
3333
return webWorker;
3434
}
3535

36-
export async function resetWorker() {
37-
if (webWorker) {
38-
webWorker.terminate();
39-
webWorker = null;
40-
}
41-
await ensureWorker();
42-
}
43-
4436
export function getDicomSeriesWorkerPool() {
4537
return readDicomSeriesWorkerPool;
4638
}

src/io/readWriteImage.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,29 @@ import {
88
import { vtiReader, vtiWriter } from '@/src/io/vtk/async';
99
import { getWorker } from '@/src/io/itk/worker';
1010

11-
export const readImage = async (file: File) => {
11+
export const readImage = async (file: File, webWorker?: Worker | null) => {
1212
if (file.name.endsWith('.vti'))
1313
return (await vtiReader(file)) as vtkImageData;
1414

15-
const { image } = await readImageItk(file, { webWorker: getWorker() });
15+
const { image } = await readImageItk(file, {
16+
webWorker: webWorker ?? getWorker(),
17+
});
1618
return vtkITKHelper.convertItkToVtkImage(image);
1719
};
1820

19-
export const writeImage = async (format: string, image: vtkImageData) => {
21+
export const writeImage = async (
22+
format: string,
23+
image: vtkImageData,
24+
webWorker?: Worker | null
25+
) => {
2026
if (format === 'vti') {
2127
return vtiWriter(image);
2228
}
2329
// copyImage so writeImage does not detach live data when passing to worker
2430
const itkImage = copyImage(vtkITKHelper.convertVtkToItkImage(image));
2531

2632
const result = await writeImageItk(itkImage, `image.${format}`, {
27-
webWorker: getWorker(),
33+
webWorker: webWorker ?? getWorker(),
2834
});
2935
return result.serializedImage.data as Uint8Array<ArrayBuffer>;
3036
};

src/io/state-file/serialize.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ export async function serialize() {
6060
await datasetStore.serialize(stateFile);
6161
viewStore.serialize(stateFile);
6262
await useViewConfigStore().serialize(stateFile);
63+
6364
await labelStore.serialize(stateFile);
65+
6466
toolStore.serialize(stateFile);
6567
await layersStore.serialize(stateFile);
6668

src/store/segmentGroups.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { onImageDeleted } from '@/src/composables/onImageDeleted';
1010
import { normalizeForStore, removeFromArray } from '@/src/utils';
1111
import { SegmentMask } from '@/src/types/segment';
1212
import { DEFAULT_SEGMENT_MASKS, CATEGORICAL_COLORS } from '@/src/config';
13+
import { createWebWorker } from 'itk-wasm';
1314
import { readImage, writeImage } from '@/src/io/readWriteImage';
1415
import {
1516
type DataSelection,
@@ -480,12 +481,21 @@ export const useSegmentGroupStore = defineStore('segmentGroup', () => {
480481

481482
state.manifest.segmentGroups = serialized;
482483

483-
// save labelmap images
484+
// save labelmap images — fresh worker per write to avoid heap accumulation
484485
await Promise.all(
485486
serialized.map(async ({ id, path }) => {
486487
const vtkImage = dataIndex[id];
487-
const serializedImage = await writeImage(saveFormat.value, vtkImage);
488-
zip.file(path, serializedImage);
488+
const worker = await createWebWorker(null);
489+
try {
490+
const serializedImage = await writeImage(
491+
saveFormat.value,
492+
vtkImage,
493+
worker
494+
);
495+
zip.file(path, serializedImage);
496+
} finally {
497+
worker.terminate();
498+
}
489499
})
490500
);
491501
}
@@ -527,7 +537,14 @@ export const useSegmentGroupStore = defineStore('segmentGroup', () => {
527537
const file = stateFiles.find(
528538
(entry) => entry.archivePath === normalize(segmentGroup.path!)
529539
)?.file;
530-
return { image: await readImage(file!) };
540+
// Use a fresh worker per labelmap to avoid WASM heap accumulation.
541+
// The shared worker may already have a large heap from base images.
542+
const worker = await createWebWorker(null);
543+
try {
544+
return { image: await readImage(file!, worker) };
545+
} finally {
546+
worker.terminate();
547+
}
531548
}
532549

533550
const labelmapResults = await Promise.all(

0 commit comments

Comments
 (0)