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

SIGSEGV in Tree.cellDataProc when calling TreeItem.setImage #678 #1662

Closed
wants to merge 2 commits into from

Conversation

zkxvh
Copy link

@zkxvh zkxvh commented Dec 13, 2024

Fix and test for bug #678

Fixes #678

Reproducing the crash:

The cause of the crash is described here: #678 (comment)
In short, the problem was executing Tree.createRenderers() inside TreeItem.setImage().
The sequence of action leading to the crash was:
in a Tree with SWT.VIRTUAL a TreeItem is rendered for the first time or after clear()

  • Tree.cellDataProc() is executed for the item and one of it's
    renderers
    • Tree.checkData(item) is called for the item
      • SWT.SetData is invoked and executes TreeItem.setImage()
        • Tree.createRenderers() executes and disposes the current renderer
    • further actions in Tree.cellDataProc() that access the already-disposed renderer (they think it's alive)

How it's fixed:

  1. set fixed height+width to pixbuf renderers with GTK.gtk_cell_renderer_set_fixed_size
    This does 2 things:
    • from now on it makes images rendered by the renderer to be rendered with these height+width
    • from now on fixed row height calculation will return height of at least fixed height of the pixbuf
  2. make GtkTreeView recompute it's internally stored fixed row height with re-setting fixed_height_mode of the GtkTreeView
  3. make all existing tree rows update their height by invoking gtk_tree_view_row_changed() on each of the rows.

(2) and (3) can also be achieved by creating a copy of the current GtkTreeModel and setting it to the GtkTreeView.
But this also resets current selected rows, focus and scroll position; that's why I chose re-setting fixed_height_mode + gtk_tree_view_row_changed() instead.

…latform#678

Fix and test for bug eclipse-platform#678

Fixes eclipse-platform#678

Reproducing the crash:
- eclipse-platform#678 (comment)
- eclipse-platform#1611
- eclipse-platform#678 (comment)

The cause of the crash is described here:
eclipse-platform#678 (comment)
In short, the problem was executing `Tree.createRenderers()` inside
`TreeItem.setImage()`.
The sequence of action leading to the crash was:
in a `Tree` with `SWT.VIRTUAL` a `TreeItem` is rendered for the first
time or after `clear()`
  `Tree.cellDataProc()` is executed for the item and one of it's
renderers
    `Tree.checkData(item)` is called for the item
      `SWT.SetData` is invoked and executes `TreeItem.setImage()`
         `Tree.createRenderers()` executes and disposes the current
renderer
    further actions in `Tree.cellDataProc()` that access the
already-disposed renderer (they think it's alive)

How it's fixed:
1. set fixed height+width to pixbuf renderers with
`GTK.gtk_cell_renderer_set_fixed_size`
   This does 2 things:
     - from now on it makes images rendered by the renderer to be
rendered with these height+width
     - from now on fixed row height calculation will return height of at
least fixed height of the pixbuf
2. make `GtkTreeView` recompute it's internally stored fixed row height
with re-setting `fixed_height_mode` of the GtkTreeView
3. make all existing tree rows update their height by invoking
`gtk_tree_view_row_changed()` on each of the rows.

(2) and (3) can also be achieved by creating a copy of the current
`GtkTreeModel` and assigning it to the `GtkTreeView`.
But this also resets current selected rows, focus and scroll position;
that's why I chose re-setting `fixed_height_mode` +
`gtk_tree_view_row_changed()` instead.
@zkxvh
Copy link
Author

zkxvh commented Dec 13, 2024

There were 3 failed tests in the build above.
As I understand, the tests are for Browser widget and the code in this PR doesn't affect Browser any any way.

@basilevs
Copy link
Contributor

basilevs commented Dec 13, 2024

If you use asyncExec() already, why do all the other changes? The problem was never the computation of heights, it was resource management. And resources are managed just fine as long as you defer their disposal. Therefore, my fix in #1612 is less intrusive and achieves the same results at less cost (admittedly I have not tested if selection is lost, but it might).

UPDATE:
Oh, I see. There are less resources disposed this way.

@@ -4319,6 +4319,80 @@ void checkSetDataInProcessBeforeRemoval() {
}
}

boolean initPixbufSize(Image image) {
Copy link
Contributor

Choose a reason for hiding this comment

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

[minor] SWT has a ridiculous code style, where parenthesis , braces and brackets are space-separated from associated identifier. Consider following the style of surrounding code.

Copy link
Author

Choose a reason for hiding this comment

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

done: df4c867

@@ -4319,6 +4319,80 @@ void checkSetDataInProcessBeforeRemoval() {
}
}

boolean initPixbufSize(Image image) {
if (pixbufSizeSet || image == null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

[minor] Now that resource management is fixed, are these lines still needed? Can't we monotonically increase the size for every new image?

Copy link
Author

Choose a reason for hiding this comment

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

As I understand from the swt code, for any Tree:

  1. all images in the tree must have the same size
  2. that fixed size is the size of the 1st image set for the tree

In the code this is implemented in TreeItem.setImage(int index, Image image) here:

	if (image != null) {
		ImageList imageList = parent.imageList;
		if (imageList == null) imageList = parent.imageList = new ImageList();
		int imageIndex = imageList.indexOf(image);
		// When we create a blank image surface gets created with dimensions 0, 0.
        // This call recreates the surface with correct dimensions
		long tempSurface = ImageList.convertSurface(image);
		Cairo.cairo_surface_destroy(tempSurface);
		if (imageIndex == -1) {
			imageIndex = imageList.add(image);
		}
		surface = imageList.getSurface(imageIndex);
		pixbuf = ImageList.createPixbuf(surface);
	}

Basically, ImageList has width and height which it initializes from the 1st added image, and then all subsequent added images are scaled to that size.

Here is a snippet that demonstrates how the images in a tree are scaled to the same size:

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.layout.*;
import org.eclipse.swt.widgets.*;

public class TryColumnsAndImageSizes {

	private Display display;
	private Shell shell;
	private Composite buttonBox;
	private Tree tree;

	private Image[] images = new Image[3];

	public static void main (String[] args) {
		new TryColumnsAndImageSizes ().run ();
	}

	void run () {
		display = new Display ();
		shell = new Shell (display);
		shell.setLayout (new GridLayout ());

		buttonBox = new Composite (shell, 0);
		buttonBox.setLayout (new FillLayout ());

		addSetImageButton ("r50", 0, 20, display.getSystemColor (SWT.COLOR_RED));
		addSetImageButton ("g70", 1, 70, display.getSystemColor (SWT.COLOR_GREEN));
		addSetImageButton ("b90", 2, 120, display.getSystemColor (SWT.COLOR_BLUE));

		tree = new Tree (shell, SWT.VIRTUAL);
		tree.setLayoutData (new GridData (GridData.FILL_BOTH));
		for (int i = 1; i <= 3; i++) {
			var column = new TreeColumn (tree, SWT.LEFT);
			column.setText ("Column " + i);
			column.setWidth (200);
		}

		tree.addListener (SWT.SetData, e -> {
			TreeItem item = (TreeItem) e.item;
			item.setText (0, "a");
			item.setText (1, "b");
			item.setText (2, "c");
			var idx = tree.indexOf (item);
			for (var i = 0; i < images.length; i++) {
				if (idx == i + 2) {
					item.setImage (i, images[i]);
				}
			}
		});
		tree.setItemCount (10);

		shell.setSize (400, 300);
		shell.open ();

		while (!shell.isDisposed ()) {
			if (!display.readAndDispatch ()) {
				display.sleep ();
			}
		}
		display.dispose ();
	}

	private void addSetImageButton (String label, int imageIdx, int imageSize, Color imageColor) {
		var button = new Button (buttonBox, SWT.PUSH);
		button.setText (label);
		button.addListener (SWT.Selection, e -> {
			images[imageIdx] = createImage (imageSize, imageColor);
			tree.clearAll (false);
		});
	}

	private Image createImage (int imageSize, Color color) {
		var image = new Image (display, imageSize, imageSize);
		var gc = new GC (image);
		gc.setForeground (color);
		gc.setBackground (display.getSystemColor (SWT.COLOR_WHITE));
		gc.setLineWidth (5);
		gc.fillRectangle (0, 0, imageSize, imageSize);
		gc.drawRectangle (0, 0, imageSize - 1, imageSize - 1);
		gc.dispose ();
		return image;
	}
}

Copy link
Author

Choose a reason for hiding this comment

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

As a result, the answer to this:

Can't we monotonically increase the size for every new image?

is "no, because Tree in swt is designed to have images of the same size".

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, as long as they are not cropped, we can leave this as is.

Copy link
Contributor

Choose a reason for hiding this comment

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

is "no, because Tree in swt is designed to have images of the same size".

It does not matter, as on each new image we could resize all previously added ones as necessary.

Copy link
Author

Choose a reason for hiding this comment

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

OK.
Apparently, this is another issue.
Just be careful, if you are going to increase fixed row size multiple times, then you'll probably have to replace fixed_height_mode + gtk_tree_view_row_changed to gtk_tree_view_set_model(). Here is more details.

// we do that by invoking gtk_tree_view_row_changed on each of them
long iter = OS.g_malloc(GTK.GtkTreeIter_sizeof());
if (GTK.gtk_tree_model_get_iter_first(modelHandle, iter)) {
int[] value = new int[1];
Copy link
Contributor

Choose a reason for hiding this comment

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

[minor] unnecessary reallocation

Copy link
Author

Choose a reason for hiding this comment

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

Sorry, but I don't understand.
Unnecessary reallocation of what?
As I can see, both iter and value are allocated once.
Could you clarify it, please.

Copy link
Contributor

Choose a reason for hiding this comment

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

I had read int[] value = new int[1]; as being a part of loop iteration. I was wrong. Sorry about that.

Copy link
Author

Choose a reason for hiding this comment

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

no problem

if (parent.pixbufWidth > Math.max(currentWidth [0], 0) || parent.pixbufHeight > Math.max(currentHeight [0], 0)) {
GTK.gtk_cell_renderer_set_fixed_size (pixbufRenderer, parent.pixbufWidth, parent.pixbufHeight);
}
GTK.gtk_cell_renderer_set_fixed_size (pixbufRenderer, parent.pixbufWidth, parent.pixbufHeight);
Copy link
Contributor

@basilevs basilevs Dec 13, 2024

Choose a reason for hiding this comment

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

[major] Why do we allow to decrease size now?
Would not this corrupt other columns, where large images might be present?

Copy link
Author

Choose a reason for hiding this comment

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

As I understand, in swt all images in a Tree have the same size. (here is why I think so).
As a result, all GtkCellRendererPixbufs should have the same fixed width and height.

Actually in Tree.resetFixedRowHeight() I added the code that iterates over all GtkCellRendererPixbufs and set them fixed width and height.
So maybe, GTK.gtk_cell_renderer_set_fixed_size() might even be redundant.
I left this code here just in case (e.g. maybe there is code somewhere in swt that adds a new column and doesn't call GTK.gtk_cell_renderer_set_fixed_size() for it)

Copy link
Contributor

@basilevs basilevs left a comment

Choose a reason for hiding this comment

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

Reducing the fixed size seems to be an error.

}

private void createTree(boolean withCheckbox, int totalRows, int imgRowIdx, Supplier<Image> getImage) {
tree = new Tree(shell, SWT.VIRTUAL | SWT.BORDER | (withCheckbox ? SWT.CHECK : 0));
Copy link
Contributor

@basilevs basilevs Dec 13, 2024

Choose a reason for hiding this comment

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

[minor] Now, that a potential regression for setting an image of decreasing sizes in different columns is discovered, we may want some tests for multi-column case.

Copy link
Author

Choose a reason for hiding this comment

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

It seems to me that Tree doesn't support different images sizes in swt by design.
In this reply I included a snippet that demonstrates what happens when there are multiple columns.
Please, note that images are scaled to fixed size inside ImageList, which is pretty old - that's why I think that Tree expects images of the same size by design.

Copy link
Author

Choose a reason for hiding this comment

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

BTW, I can add unit test(s) for multiple columns and different images sizes to Test_Gtk_Tree_Virtual_setImage no problem.
I just don't know what additional cases you want to be checked.
The current unit tests in Test_Gtk_Tree_Virtual_setImage are for the bug with row heights, that I noticed (and fixed in this PR) while I've been fixing crash in #678.

Copy link
Contributor

Choose a reason for hiding this comment

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

The scenario is:

  • for three sequences of images three in length each:
    • one monotonically increasing in size
    • one monotonically decreasing
    • and one non-monotonic
  • test that behavior (heights, widths, exceptions, ideally cropping but that's probably too hard) is unchanged when:
    • adding images to different columns
    • adding to same column
    • adding to a tree without columns

Copy link
Contributor

Choose a reason for hiding this comment

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

Just to be clear, I'm not a maintainer, so may be wait until one chimes in before investing significant effort. I was going to add such test myself later anyway.

Copy link
Author

Choose a reason for hiding this comment

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

OK.
As I understand, you are working on some task related to image cropping, multiple image sizes, and multiple columns.
I suppose including those unit tests in the commit for this task I would be more appropriate.

Copy link
Contributor

Choose a reason for hiding this comment

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

No, I literally tried to fix the very same problem - crash in VIRTUAL Tree. While I succeeded, your solution might be a bit better.

Copy link
Contributor

Choose a reason for hiding this comment

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

No, I fixed the very same problem - crash in VIRTUAL Tree, but your solution might be better.

…latform#678

Added code formatting changed to match the formatting of the rest of the
SWT code.
@zkxvh
Copy link
Author

zkxvh commented Dec 16, 2024

@basilevs

If you use asyncExec() already, why do all the other changes? The problem was never the computation of heights, it was resource management. And resources are managed just fine as long as you defer their disposal. Therefore, my fix in #1612 is less intrusive and achieves the same results at less cost (admittedly I have not tested if selection is lost, but it might).

UPDATE: Oh, I see. There are less resources disposed this way.

Yes, the method with fixed_height_mode property doesn't delete and recreate existing renderers like Tree.createRenderers() did.

Also I tried to understand what the old code did (when it did not crash the application).
Here is more details, but in short:

  • a Tree in swt has fixed size for all images
  • the fixed image size is initialized from the 1st image set to a Tree
  • when fixed image size is initialized for a Tree with SWT.VIRTUAL, then the (fixed) row heights should be updated to fit it.

fixed_height_mode + gtk_tree_view_row_changed is the least intrusive way to do the above that I was able to find in gtk.


Note: changing fixed_height_mode is fine here because Tree.initPixbufSize() is called only once per tree, which means the fixed row height is changed only once.
But if fixed height is going to be updated multiple times, then it seems that you'll have to switch to gtk_tree_view_set_model().
It seems like gtk assumes that fixed_height_mode property should be set once - before any row is added to a tree.
Why I think so:

  • in gtk_tree_view_set_fixed_height_mode the signal handler for "notify::sizing" is added, but never removed. I.e. every time we set fixed_height_mode to true one more handler is attached to sizing property
  • the fact that I had to use gtk_tree_view_row_changed to update the heights of the existing rows (had gtk supported changing fixed_height_mode it would have updated the height of the existing rows automatically).

gtk_tree_view_set_model() can be used to update row heights any number of times, but it resets current selection, focus and scroll position of the tree.

@zkxvh zkxvh marked this pull request as draft December 18, 2024 11:46
@zkxvh
Copy link
Author

zkxvh commented Dec 18, 2024

Converted to draft because it seems like this bug should be should be fixed in another place in the code.

After fixing this bug by changing TreeItem.setImage(...) I get the same process crash in Eclipse RCP in a code that uses Tree but with slightly different error message:

Bail out! Gtk:ERROR:../gtk/gtktreestore.c:597:gtk_tree_store_get_path: assertion failed: (G_NODE (iter->user_data)->parent != NULL)
**
Gtk:ERROR:../gtk/gtktreestore.c:597:gtk_tree_store_get_path: assertion failed: (G_NODE (iter->user_data)->parent != NULL)

Stacktrace is this:

	org.eclipse.swt.widgets.TreeItem.getParentItem(TreeItem.java:866)
	at org.eclipse.jface.viewers.TreeViewer.getParentItem(TreeViewer.java:228)
	at org.eclipse.jface.viewers.AbstractTreeViewer.getTreePathFromItem(AbstractTreeViewer.java:3046)
	at org.eclipse.jface.viewers.AbstractTreeViewer.internalFindItem(AbstractTreeViewer.java:211)
	at org.eclipse.jface.viewers.AbstractTreeViewer.internalFindItems(AbstractTreeViewer.java:188)
	at org.eclipse.jface.viewers.TreeViewer.replace(TreeViewer.java:435)
	at TestLazyContentProvider.updateElement(TestLazyContentProvider.java:100)
	at org.eclipse.jface.viewers.TreeViewer.virtualLazyUpdateWidget(TreeViewer.java:1001)
	at org.eclipse.jface.viewers.TreeViewer.lambda$1(TreeViewer.java:260)
	at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:89)
	at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5855)
	at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1529)
	at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1555)
	at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1538)
	at org.eclipse.swt.widgets.Tree.checkData(Tree.java:372)
	at org.eclipse.swt.widgets.Tree.cellDataProc(Tree.java:304)
	at org.eclipse.swt.widgets.Display.cellDataProc(Display.java:995)
	at org.eclipse.swt.internal.gtk.GTK.gtk_tree_store_clear(Native Method)
	at org.eclipse.swt.widgets.Tree.removeAll(Tree.java:2997)
	at org.eclipse.jface.viewers.TreeViewer.removeAll(TreeViewer.java:289)
	at org.eclipse.jface.viewers.AbstractTreeViewer.lambda$1(AbstractTreeViewer.java:1593)
	at org.eclipse.jface.viewers.StructuredViewer.preservingSelection(StructuredViewer.java:1392)
	at org.eclipse.jface.viewers.TreeViewer.preservingSelection(TreeViewer.java:367)
	at org.eclipse.jface.viewers.StructuredViewer.preservingSelection(StructuredViewer.java:1353)
	at org.eclipse.jface.viewers.AbstractTreeViewer.inputChanged(AbstractTreeViewer.java:1589)
	at org.eclipse.jface.viewers.ContentViewer.setInput(ContentViewer.java:282)
	at org.eclipse.jface.viewers.StructuredViewer.setInput(StructuredViewer.java:1636)

This is a very similar bug: again we get:

Display.cellDataProc(...)
  ↳ Tree.checkData(...)
    ↳ sendEvent(SWT.SetData, ...)
      ↳ a callback for SWT.SetData is invoked, it adds/removes columns/rows/renderers/other tree components
  ↳ the code in Display.cellDataProc(...) continues execution and crashes when it accesses removed/modified tree components

The problem again is that a SWT.SetData callback changes the structure of the tree, while Display.cellDataProc(...) (where the callback is called from) is a cell data function in GTK terms which should only set renderers' properties like text, image, etc. and should not change tree structure.

Since callbacks for SWT.SetData can contain any code, I think the the better fix would be to change Display.cellDataProc(...) to postpone sending SWT.SetData until the method finishes.
This would also fix all other similar bugs that crash the application because of Tree.

@zkxvh zkxvh deleted the branch eclipse-platform:master December 19, 2024 07:06
@zkxvh zkxvh closed this Dec 19, 2024
@zkxvh zkxvh deleted the master branch December 19, 2024 07:06
@zkxvh zkxvh restored the master branch December 19, 2024 07:15
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.

SIGSEGV in Tree.cellDataProc when calling TreeItem.setImage
2 participants