11import * as fs from 'node:fs' ;
22import * as path from 'node:path' ;
33import * as zlib from 'node:zlib' ;
4+ import JSZip from 'jszip' ;
45import { cleanuptotal } from 'wdio-cleanuptotal-service' ;
56import { volViewPage } from '../pageobjects/volview.page' ;
6- import { writeManifestToFile } from './utils' ;
77import { DOWNLOAD_TIMEOUT , TEMP_DIR } from '../../wdio.shared.conf' ;
88
99const writeBufferToFile = async ( data : Buffer , fileName : string ) => {
@@ -60,69 +60,86 @@ const createNiftiGz = (
6060 return zlib . gzipSync ( Buffer . concat ( [ header , imageData ] ) , { level : 1 } ) ;
6161} ;
6262
63+ const createSessionZip = async (
64+ baseFileName : string ,
65+ labelmapNiftiGz : Buffer
66+ ) => {
67+ const manifest = {
68+ version : '6.2.0' ,
69+ dataSources : [
70+ {
71+ id : 0 ,
72+ type : 'uri' ,
73+ uri : `/tmp/${ baseFileName } ` ,
74+ name : baseFileName ,
75+ } ,
76+ ] ,
77+ datasets : [ { id : '0' , dataSourceId : 0 } ] ,
78+ segmentGroups : [
79+ {
80+ id : 'seg-1' ,
81+ path : 'labels/seg-1.nii.gz' ,
82+ metadata : {
83+ name : 'Annotation' ,
84+ parentImage : '0' ,
85+ segments : {
86+ order : [ 1 ] ,
87+ byValue : {
88+ '1' : {
89+ value : 1 ,
90+ name : 'Label 1' ,
91+ color : [ 255 , 0 , 0 , 255 ] ,
92+ visible : true ,
93+ } ,
94+ } ,
95+ } ,
96+ } ,
97+ } ,
98+ ] ,
99+ } ;
100+
101+ const zip = new JSZip ( ) ;
102+ zip . file ( 'manifest.json' , JSON . stringify ( manifest , null , 2 ) ) ;
103+ zip . file ( 'labels/seg-1.nii.gz' , labelmapNiftiGz ) ;
104+ return zip . generateAsync ( { type : 'nodebuffer' , compression : 'STORE' } ) ;
105+ } ;
106+
63107/**
64- * Regression test for session restore with large base image + labelmap.
108+ * Regression test for WASM signed pointer overflow during session restore.
109+ *
110+ * A .volview.zip session with a large Float32 URI-based base image and an
111+ * embedded .nii.gz labelmap. The import pipeline loads the base image
112+ * through the shared ITK-wasm worker, growing the WASM heap past 2GB.
113+ * Then segmentGroupStore.deserialize() calls readImage() for the embedded
114+ * .nii.gz labelmap on the same worker.
115+ *
116+ * The .nii.gz format is critical: .vti labelmaps use a separate JS
117+ * reader and never touch the ITK-wasm worker.
65118 *
66- * Scenario: Int16 1024×1024×172 base image loaded via URI, with a UInt8
67- * labelmap of the same dimensions. On the shared ITK-wasm worker, base
68- * image processing grew the WASM heap to ~1721MB. Loading the labelmap
69- * on the same worker pushed it past 2GB, causing signed pointer overflow
70- * in Emscripten's ccall (output pointers > 2^31 wrap negative →
71- * RangeError: Start offset -N is outside the bounds).
119+ * Without resetting the worker, Emscripten's ccall returns output pointers
120+ * as signed i32. When pointers exceed 2^31 they wrap negative, causing:
121+ * RangeError: Start offset -N is outside the bounds of the buffer
72122 *
73- * Fix: reset the ITK-wasm worker before deserializing labelmaps to
74- * prevent heap accumulation.
123+ * Fix: resetWorker() before deserializing labelmaps clears the heap.
75124 */
76- describe ( 'Session with large URI base and labelmap' , function ( ) {
125+ describe ( 'Session with large URI base and nii.gz labelmap' , function ( ) {
77126 this . timeout ( 180_000 ) ;
78127
79- it ( 'loads large base image with labelmap without errors ' , async ( ) => {
128+ it ( 'loads session with large Float32 base and embedded nii.gz labelmap ' , async ( ) => {
80129 const prefix = `session-large-${ Date . now ( ) } ` ;
81- const baseFileName = `${ prefix } -base-i16 .nii.gz` ;
82- const labelmapFileName = `${ prefix } -labelmap-u8.nii.gz ` ;
130+ const baseFileName = `${ prefix } -base-f32 .nii.gz` ;
131+ const sessionFileName = `${ prefix } -session.volview.zip ` ;
83132
84- // Int16 1024×1024×172 = 361MB raw
133+ // Float32 1024×1024×256 = 1GB raw — pushes WASM heap past 2GB
85134 await writeBufferToFile (
86- createNiftiGz ( 1024 , 1024 , 172 , 4 , 16 ) ,
135+ createNiftiGz ( 1024 , 1024 , 256 , 16 , 32 ) ,
87136 baseFileName
88137 ) ;
89- // UInt8 labelmap same dimensions = 180MB raw
90- await writeBufferToFile (
91- createNiftiGz ( 1024 , 1024 , 172 , 2 , 8 ) ,
92- labelmapFileName
93- ) ;
94-
95- const manifest = {
96- version : '6.1.0' ,
97- dataSources : [
98- { id : 0 , type : 'uri' , uri : `/tmp/${ baseFileName } ` } ,
99- { id : 1 , type : 'uri' , uri : `/tmp/${ labelmapFileName } ` } ,
100- ] ,
101- labelMaps : [
102- {
103- id : 'seg-1' ,
104- dataSourceId : 1 ,
105- metadata : {
106- name : 'Annotation' ,
107- parentImage : '0' ,
108- segments : {
109- order : [ 1 ] ,
110- byValue : {
111- '1' : {
112- value : 1 ,
113- name : 'Label 1' ,
114- color : [ 255 , 0 , 0 , 255 ] ,
115- visible : true ,
116- } ,
117- } ,
118- } ,
119- } ,
120- } ,
121- ] ,
122- } ;
123138
124- const manifestFileName = `${ prefix } -manifest.volview.json` ;
125- await writeManifestToFile ( manifest , manifestFileName ) ;
139+ // UInt8 labelmap same dimensions = 256MB raw, embedded in session ZIP
140+ const labelmapNiftiGz = createNiftiGz ( 1024 , 1024 , 256 , 2 , 8 ) ;
141+ const sessionZip = await createSessionZip ( baseFileName , labelmapNiftiGz ) ;
142+ await writeBufferToFile ( sessionZip , sessionFileName ) ;
126143
127144 const rangeErrors : string [ ] = [ ] ;
128145 const onLogEntry = ( logEntry : { text : string | null } ) => {
@@ -134,15 +151,10 @@ describe('Session with large URI base and labelmap', function () {
134151 browser . on ( 'log.entryAdded' , onLogEntry ) ;
135152
136153 try {
137- await volViewPage . open ( `?urls=[tmp/${ manifestFileName } ]` ) ;
154+ await volViewPage . open ( `?urls=[tmp/${ sessionFileName } ]` ) ;
138155 await volViewPage . waitForViews ( DOWNLOAD_TIMEOUT * 6 ) ;
139156
140- expect ( rangeErrors ) . toEqual ( [ ] ) ;
141-
142- const notifications = await volViewPage . getNotificationsCount ( ) ;
143- expect ( notifications ) . toEqual ( 0 ) ;
144-
145- // Verify segment group loaded
157+ // Open the segment groups panel so the list renders in the DOM
146158 const annotationsTab = await $ (
147159 'button[data-testid="module-tab-Annotations"]'
148160 ) ;
@@ -152,16 +164,30 @@ describe('Session with large URI base and labelmap', function () {
152164 await segmentGroupsTab . waitForClickable ( ) ;
153165 await segmentGroupsTab . click ( ) ;
154166
167+ // Wait for the labelmap readImage to either succeed (segment group
168+ // appears) or fail (RangeError in console OR error notification).
169+ // The deserialization is async and finishes after views render.
170+ const notifsBefore = await volViewPage . getNotificationsCount ( ) ;
171+
155172 await browser . waitUntil (
156173 async ( ) => {
174+ if ( rangeErrors . length > 0 ) return true ;
175+ try {
176+ const notifs = await volViewPage . getNotificationsCount ( ) ;
177+ if ( notifs > notifsBefore ) return true ;
178+ } catch {
179+ // badge may not exist yet
180+ }
157181 const segmentGroups = await $$ ( '.segment-group-list .v-list-item' ) ;
158182 return ( await segmentGroups . length ) >= 1 ;
159183 } ,
160184 {
161- timeout : DOWNLOAD_TIMEOUT ,
162- timeoutMsg : 'Segment group not found after session restore ' ,
185+ timeout : DOWNLOAD_TIMEOUT * 3 ,
186+ timeoutMsg : 'Labelmap load never completed or errored ' ,
163187 }
164188 ) ;
189+
190+ expect ( rangeErrors ) . toEqual ( [ ] ) ;
165191 } finally {
166192 browser . off ( 'log.entryAdded' , onLogEntry ) ;
167193 }
0 commit comments