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

Datagrid: Introduce AsyncCellRenderer and ImageRenderer #630

Merged
merged 15 commits into from
Sep 9, 2023

Conversation

martinRenou
Copy link
Member

@martinRenou martinRenou commented Sep 6, 2023

This is a step towards fixing jupyter-widgets/ipydatagrid#253

Introducing AsyncCellRenderer

Prior to this change, the Lumino datagrid would not allow rendering cells asynchronously. This means that rendering images was not really possible (unless the cell renderer had a reference to the datagrid and requested a redraw once the asynchronous task was done).

This PR introduces the AsyncCellRenderer abstract class, which the datagrid uses as following (more or less pseudo-code):

if (renderer instanceof AsyncCellRenderer) {
  if (!renderer.isReady(config)) {
    // Paint a placeholder, waiting for all asynchronous tasks to be performed
    renderer.paintPlaceholder(gc, config);

    // Perform all asynchronous tasks (without waiting) then redraw the region of the cell
    renderer.load(config).then(() => {
      this.repaintRegion(cell.position);
    }); 
  } else {
    // The async renderer is ready, we can paint synchronously
    renderer.paint(gc, config);
  }
} else {
  // Classic synchronous painting
  renderer.paint(gc, config);
}

This allows supporting asynchronous cell rendering without slowing down the synchronous way.

Introducing the ImageRenderer

This implements the AsyncCellRenderer and allows to load images in the datagrid:

Screenshot from 2023-09-06 13-59-48

@martinRenou martinRenou added enhancement New feature or request feature Adds functionality labels Sep 6, 2023
@martinRenou martinRenou marked this pull request as ready for review September 6, 2023 14:11
@martinRenou
Copy link
Member Author

Ready for review!! CI failure seems unrelated, I don't have the right to restart it

@martinRenou
Copy link
Member Author

All green! Thanks to the person who restarted it :D

Copy link
Member

@krassowski krassowski 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! I kicked the failing test. I left two questions on the API design.

Also, we now have a minimal scaffold for datagrid tests here, it would be greatly appreciated if the critical subset of the new API was unit-tested.

Comment on lines 241 to 250
/**
* Sizing mode. Can be 'original', 'fit-height', 'fit-width', or 'fill'.
* 'fit-height' will make the image fit the available height in the cell, respecting the image size ratio.
* 'fit-width' will make the image fit the available width in the cell, respecting the image size ratio.
* 'fill' will make the image fill the available space in the cell, NOT respecting the image size ratio.
* 'original' will make respect the size of the original image.
*
* The default is 'fit-height'
*/
sizingMode?: CellRenderer.ConfigOption<SizingMode>;
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it would be better to go for more flexible width: 100% and height: 100%. This way we can implement other sizing options (e.g. based on pixels) in the future, and already use width: 50%. Unless it does not make sense here.

Copy link
Member Author

Choose a reason for hiding this comment

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

That works with me. Note that we cannot apply CSS rules though, so we'll need to parse the percentage and compute it manually. But it's not impossible :D

Copy link
Member

Choose a reason for hiding this comment

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

Yes, parsing only % values for now should be managable. I would encourage doing this in this PR. In the future we might swap the implementation for CSSStyleValue.parse once Firefox supports it.

}
}

static dataCache = new Map<string, HTMLImageElement | undefined>();
Copy link
Member

Choose a reason for hiding this comment

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

Do elements in this cache need to be disposed some way to avoid memory leaks?

Copy link
Member Author

Choose a reason for hiding this comment

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

As it's a static property, it will live as long as the page is running, is that right? So I guess there will be no memory leak happening?

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought we may want to clean the cache periodically, could be done in a separate PR if needed.

Copy link
Member

Choose a reason for hiding this comment

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

In context of outputs in a notebook, if user opens and then closes 100 notebooks with different in images in data grids, will the memory stay constant or increase with each notebook? If this was tied to a renderer instance, I imagine we would not need to worry as the reference would not linger, but then we would need to fetch the resource for each output rendering which is not ideal either.

Copy link
Member

Choose a reason for hiding this comment

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

Culling the cache periodically might be a solution indeed, but we would want to do this selectively (not touch any currently rendered data grids). So we may want to change the cache implementation in the future to do some reference counting or time-based expiration. To make it easier on future ourselves, what if that was a non-exported global variable so that it is not a part of public API? Any downside to this idea?

Copy link
Member Author

Choose a reason for hiding this comment

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

Memory will increase if those images are always different.

To make it easier on future ourselves, what if that was a non-exported global variable so that it is not a part of public API

Right!! We should definitely make that cache property private.

I'm fine with the idea to periodically clean the cache. The browser will have cached the image somewhere anyway, so next time we reload the same image it should not take much time to download again.

Copy link
Member Author

Choose a reason for hiding this comment

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

Would that work for you if I make the cache property private for now, and we can come up with cache cleaning logic in a separate PR (if/when we see issues with it)?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, absolutely!

@martinRenou
Copy link
Member Author

@krassowski thanks a lot for the useful review!

I just pushed another commit:

  • making the cache attribute private
  • adding width and height properties which supports sizes in percentage or pixels. If one property is set to an empty string, it will be computed depending on the other and respecting the original image size ratio. If both are set to an empty string, the original image size will be used.
    I figured width='' and height=100% could be a good default (respect the image size ratio and take the whole height available), tell me what you think.

@martinRenou
Copy link
Member Author

Also, we now have a minimal scaffold for datagrid tests here, it would be greatly appreciated if the critical subset of the new API was unit-tested

Oops forgot to tackle this, will do

@martinRenou
Copy link
Member Author

@krassowski I think it's good for another round of review if you have time!

Copy link
Member

@krassowski krassowski left a comment

Choose a reason for hiding this comment

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

Thank you @martinRenou!

@krassowski krassowski merged commit a9bc1b7 into jupyterlab:main Sep 9, 2023
16 checks passed
@martinRenou martinRenou deleted the async_renderers branch September 9, 2023 15:47
@krassowski krassowski removed the enhancement New feature or request label Nov 5, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 5, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature Adds functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants