Make colorspace detection async - #283
Conversation
igoroctaviano
left a comment
There was a problem hiding this comment.
Thanks for the PR! I've left some inline comments on specific lines that need attention.
| mediaQuery.addListener?.(updateColorSpace) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Memory Leak: Event listeners are added to media queries but never removed. Since the Observable is returned without a cleanup mechanism, these listeners persist indefinitely.
Suggestion: Add cleanup support to the Observable. For example:
function detectDisplayColorSpace() {
const colorSpace = new Observable('srgb')
const cleanupFns = []
if (typeof window !== 'undefined' && window.matchMedia) {
const p3MediaQuery = window.matchMedia('(color-gamut: p3)')
const srgbMediaQuery = window.matchMedia('(color-gamut: srgb)')
const updateColorSpace = () => {
colorSpace.setValue(p3MediaQuery.matches ? 'display-p3' : 'srgb')
}
updateColorSpace()
for (const mediaQuery of [p3MediaQuery, srgbMediaQuery]) {
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', updateColorSpace)
cleanupFns.push(() => mediaQuery.removeEventListener('change', updateColorSpace))
} else {
mediaQuery.addListener?.(updateColorSpace)
cleanupFns.push(() => mediaQuery.removeListener?.(updateColorSpace))
}
}
}
colorSpace.cleanup = () => cleanupFns.forEach(fn => fn())
return colorSpace
}Then in viewer.js cleanup method, call this[_iccOutputType].cleanup?.() before unsubscribing.
| */ | ||
| toggleICCProfiles() { | ||
| console.debug('toggle ICC profiles:', this[_isICCProfilesEnabled]) | ||
| this.configureDataLoaders(!this[_isICCProfilesEnabled]).then(() => { |
There was a problem hiding this comment.
Missing Error Handling: If configureDataLoaders rejects, the error is silently swallowed. Consider adding a .catch() handler:
toggleICCProfiles() {
console.debug('toggle ICC profiles:', this[_isICCProfilesEnabled])
this.configureDataLoaders(!this[_isICCProfilesEnabled])
.then(() => {
this[_isICCProfilesEnabled] = !this[_isICCProfilesEnabled]
})
.catch((error) => {
console.error('Failed to toggle ICC profiles:', error)
const customError = new CustomError(
errorTypes.VISUALIZATION,
'Failed to toggle ICC profiles'
)
this[_options].errorInterceptor(customError)
})
}| Enums.SOPClassUIDs.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE, | ||
| ) | ||
| this[_iccProfiles] = await _getIccProfiles({ | ||
| metadata, |
There was a problem hiding this comment.
Race Condition: this[_iccProfiles] is assigned inside the Promise.all map callback. If multiple items exist, this gets overwritten multiple times concurrently, which is redundant and could lead to inconsistent state if profiles differ per item.
Suggestion: Fetch ICC profiles once before the loop, or deduplicate the fetch logic.
| : loaderWithoutICCProfiles | ||
| source.setLoader(loader) | ||
| source.refresh() | ||
|
|
There was a problem hiding this comment.
Segments and Mappings lose error handlers: Only opticalPath items have onTileLoadError saved (line 1484), but segments and mappings also need tile load error handlers. After reconfiguration, they will lose their error handling.
Suggestion: Ensure onTileLoadError is also set for segments and mappings when they are created, similar to how it is done for optical paths.
| tileGrid: source.getTileGrid(), | ||
| projection: source.getProjection(), | ||
| wrapX: source.getWrapX(), | ||
| bandCount: source.bandCount, |
There was a problem hiding this comment.
Shared Source Instance: Both item.layer and item.overviewLayer are set to the same replacementSource instance. Previously these appeared to have separate sources. Sharing the same DataTileSource might cause unintended side effects (e.g., tile caching behavior, refresh issues).
Is this intentional? If not, consider creating separate sources for each layer.
| @@ -2422,72 +2434,87 @@ class VolumeImageViewer { | |||
| getICCProfiles() { | |||
There was a problem hiding this comment.
Removed Method: getICCOutputType() was removed but this appears to be unintentional. Please restore it:
/**
* Get ICC output type.
*
* @returns {string} ICC output type
*/
getICCOutputType() {
return this[_iccOutputType].getValue()
}| */ | ||
| cleanup() { | ||
| console.info('cleanup memory') | ||
| this[_unsubscribeDisplayColorSpace]?.() |
There was a problem hiding this comment.
Observable Cleanup: The observable from detectDisplayColorSpace() adds event listeners that are never cleaned up. Add cleanup for the observable itself here:
cleanup() {
console.info('cleanup memory')
this[_iccOutputType].cleanup?.() // Add this line
this[_unsubscribeDisplayColorSpace]?.()
// ... rest of cleanup
}This requires implementing the cleanup method on the observable as suggested in the utils.js comment.
Colorspace adaptation was already provided in #235. However, as noted in #239, this was inherently limited as it only considered the colorspace when loading. If the colorspace changed afterward (e.g. by moving the viewer from one monitor to another), it would not update the viewer. This PR introduces changes such that it does react to such changes.
It makes use of the observable introduced in #260. The colorspace is emitted as an observable that changes value depending on the
matchmediaquery. The viewer is subscribed to this values and reconfigures the datatile loaders if such a change were to occur.I have also implemented some small refactoring in the viewer file. More specifically, I have seperated the reload logic in the
toggleICCProfilesfunction and reused it for when the colorspace changes.