Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: option to use Arraybuffer for volume loader instead of sharedArrayBuffer #358

Merged

Conversation

Ouwen
Copy link
Contributor

@Ouwen Ouwen commented Jan 11, 2023

This feature allows for volumes to be loaded even if the page is not cross origin isolated.
For some use cases such as running cornerstone3D in cordova or mobile webview this is required.

@Ouwen
Copy link
Contributor Author

Ouwen commented Jan 11, 2023

@sedghi what are your thoughts on this?

@Ouwen Ouwen force-pushed the gradienthealth/no_shared_array_buffer branch from c527ec8 to 90471bf Compare January 11, 2023 10:26
@netlify
Copy link

netlify bot commented Jan 11, 2023

Deploy Preview for cornerstone-3d-docs ready!

Name Link
🔨 Latest commit a78c285
🔍 Latest deploy log https://app.netlify.com/sites/cornerstone-3d-docs/deploys/63ca06cd5e763d00082ac7e1
😎 Deploy Preview https://deploy-preview-358--cornerstone-3d-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

Comment on lines 42 to 46
const buffer = useSharedArrayBuffer
? new SharedArrayBuffer(length * 4)
: new ArrayBuffer(length * 4);

return new Float32Array(sharedArrayBuffer);
return new Float32Array(buffer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you should do it here. This file should do what it says (creating sharedArray). Maybe in the cornerstoneStreamingImageVolume you branch to use non-shared

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good I can make that change

offset: imageIdIndex * lengthInBytes,
length,
type,
},
skipCreateImage: true,
transferPixelData: arrayBuffer instanceof ArrayBuffer,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was about to fix this in another PR. Do you need it here? since I prefer it to be a separate fix: broken transferPixelData for both volume and stack viewport.

Does it hurt to set transferPixelData to true for SharedArrayBuffer too? although I guess it is shared by construct ..?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove this for your fix. It shouldn’t cause any issue to copy rather than transfer the encoded pixel array data

@@ -415,12 +414,14 @@ export default class StreamingImageVolume extends ImageVolume {
const options = {
// WADO Image Loader
targetBuffer: {
arrayBuffer,
arrayBuffer:
arrayBuffer instanceof ArrayBuffer ? undefined : arrayBuffer,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a comment to explain why we need it for sharedArrayBuffer (since the .set is happening inside the worker in cswil), but not for the arrayBuffer since it happens down below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I’ll add something like: keeping this in the options means a large empty volume array buffer will be transferred to the worker.

This is undesirable for streaming volume without shared array buffer because the target is now an empty 300-500MB volume array buffer. Instead the volume should be progressively set in the main thread.

Comment on lines 439 to 461
const scalarData = this.scalarData;
if (scalarData.buffer instanceof ArrayBuffer) {
const offset = options.targetBuffer.offset; // in bytes
const length = options.targetBuffer.length; // in frames
try {
if (scalarData instanceof Float32Array) {
const floatView = new Float32Array(image.pixelData);
if (floatView.length !== length) {
throw 'Error pixelData length does not match frame length';
}
scalarData.set(floatView, offset / 4);
}
if (scalarData instanceof Uint8Array) {
const intView = new Uint8Array(image.pixelData);
if (intView.length !== length) {
throw 'Error pixelData length does not match frame length';
}
scalarData.set(intView, offset);
}
} catch (e) {
console.error(e);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These all better be refactor into its own function e.g., handleArrayBufferLoad or somthing similar

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (scalarData.buffer instanceof ArrayBuffer) {
  handleArrayBufferLoad()
}
successCallback...

@@ -179,7 +179,7 @@ if (configuration.examples) {
// console.log('conf', conf);
shell.ShellString(conf).to(webpackConfigPath);
shell.cd(exBasePath);
shell.exec(`webpack serve --progress --config ${webpackConfigPath}`);
shell.exec(`webpack serve --host 0.0.0.0 --progress --config ${webpackConfigPath}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this make it run on 0.0.0.0 instead of localhost:3000?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will run on localhost and bind ip address so mobile devices on the same network can access.

Copy link
Member

@sedghi sedghi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great as first iteration.
The only part I'm not certain right now is the global setting for sharedArrayBuffer usage in the init. Hmmmm, Do we want it there? I'm not really sure I like the idea of people fallbacking to ArrayBuffer (instead of more performant sharedArrayBuffer) if they have not set the headers correctly. They can just debug the error for cross origin and fix their headers and enjoy the performant path.

Do you think we put it at the volume loader options? or maybe just a boolean at the init instead of checking new SharedArrayBuffer(0) for those who know what they are doing

CC @wayfarer3130

@sedghi sedghi changed the title feat: no use shared array buffer feat: option to use Arraybuffer for volume loader instead of sharedArrayBuffer Jan 11, 2023
@Ouwen
Copy link
Contributor Author

Ouwen commented Jan 11, 2023

@sedghi one interesting find. I've been testing this across different devices desktop chrome, safari, android chrome, and iOS safari. They all work well except iOS safari which occasionally crashes the worker. On a single worker, either all slices will load properly without a crash. Or, after successful initialization, the worker throws a "Range Error: Out of Memory".

Refreshing the page after waiting about 10 seconds usually fixes the problem and all slices load in. Have you seen this behavior before?

@Ouwen Ouwen force-pushed the gradienthealth/no_shared_array_buffer branch from 90471bf to e0e3e00 Compare January 11, 2023 20:08
@Ouwen
Copy link
Contributor Author

Ouwen commented Jan 11, 2023

After some more investigation. It seems like the iOS safari behavior is due to something with web worker garbage collection. Hard refreshing iOS will have 1 web worker properly spawn; however, follow on web workers will fail with a generic "Range Error: Out of Memory".

With just one web worker, we can refresh the page within safari and have the volume properly load. However, the web worker will occasionally run into some error and crash (not loading any slices). A hard refresh (hard close safari and refresh) causes the page to function again.

The consistent finding is that the first load after a hard refresh will always work.

@abluobo
Copy link

abluobo commented Jan 12, 2023

@Ouwen Will the memory occupied by the browser increase after the Arraybuffer replaces the sharedArrayBuffer?

@Ouwen
Copy link
Contributor Author

Ouwen commented Jan 12, 2023

The browser memory creates an empty volume array. Let's say it is 100MB or so.
The workers will decode image pixel data and set a 1MB slice inside the 100MB volume.

The main difference is that without sharedArrayBuffer the workers have to transfer the 1MB decoded slice back into the main thread. The main thread then performs the set.

With sharedArrayBuffer, the workers can set directly into the 100MB sharedArrayBuffer

@sedghi
Copy link
Member

sedghi commented Jan 13, 2023

@Ouwen I talked to Bill, and we think useSharedArrayBuffer should have three options true, false, auto with default of true. What do you think?

@Ouwen
Copy link
Contributor Author

Ouwen commented Jan 14, 2023

Yeah I think that makes sense.

@Ouwen
Copy link
Contributor Author

Ouwen commented Jan 14, 2023

After some more investigation. It seems like the iOS safari behavior is due to something with web worker garbage collection. Hard refreshing iOS will have 1 web worker properly spawn; however, follow on web workers will fail with a generic "Range Error: Out of Memory".

With just one web worker, we can refresh the page within safari and have the volume properly load. However, the web worker will occasionally run into some error and crash (not loading any slices). A hard refresh (hard close safari and refresh) causes the page to function again.

The consistent finding is that the first load after a hard refresh will always work.

This is unrelated to the non shared array buffer implementation and is an issue with loading codecs on iOS safari.

cornerstonejs/cornerstoneWADOImageLoader#506

@Ouwen Ouwen force-pushed the gradienthealth/no_shared_array_buffer branch 2 times, most recently from 0cbaefb to d58ab7c Compare January 15, 2023 01:58
@Ouwen Ouwen force-pushed the gradienthealth/no_shared_array_buffer branch from d58ab7c to 85098d8 Compare January 15, 2023 04:31
@Ouwen
Copy link
Contributor Author

Ouwen commented Jan 15, 2023

@sedghi, ready for review.

@sedghi
Copy link
Member

sedghi commented Jan 16, 2023

@Ouwen One of the tests are failing. You can force run it via changing it to fit and describe to fdescribe just FYI. Hopefully it will fail once you run it in isolation, if not there is some leaking happening between tests (or in source)

image

Copy link
Member

@sedghi sedghi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great !

@sedghi sedghi merged commit ab8237c into cornerstonejs:main Jan 20, 2023
@Ouwen
Copy link
Contributor Author

Ouwen commented Jan 20, 2023

Thanks @sedghi

@Ouwen Ouwen deleted the gradienthealth/no_shared_array_buffer branch February 7, 2023 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants