-
Notifications
You must be signed in to change notification settings - Fork 6.2k
8374377: PNGImageDecoder Slow For 8-bit PNGs #29004
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
base: master
Are you sure you want to change the base?
Conversation
This shows slightly over a 10% improvement in my tests. This only applies to non-interlaced 8-bit (IndexColorModel) PNGs.
|
👋 Welcome back jwood! A progress list of the required criteria for merging this PR into |
|
❗ This change is not yet ready to be integrated. |
|
@mickleness The following label will be automatically applied to this pull request:
When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command. |
Webrevs
|
|
Do you have a JMH test for the attached graph that can be used to replicate locally? If you've not used JMH before, or haven't used it in the context of OpenJDK, you can check existing performance tests in the |
This corroborates previous (more informal) tests that show approx a 10% improvement. Without this PR I observed this result on my Mac 26.2 laptop: ``` Benchmark Mode Cnt Score Error Units PNGImageDecoder_8bit_uninterlaced.measurePNGImageDecoder avgt 15 25.296 ± 0.521 ms/op ``` With this PR I observed this result: ``` Benchmark Mode Cnt Score Error Units PNGImageDecoder_8bit_uninterlaced.measurePNGImageDecoder avgt 15 22.132 ± 0.378 ms/op ```
|
@gredler Thanks. I'm not familiar with JMH. I used your example (and a lot of googling) to set up a basic benchmark that is now attached to this ticket. For a 2500x2500 px image, the time on my Mac went from approx 25.296 to 22.132. Is there any interest in me: I'm not sure what the long-term usage of this benchmark .java would be. If this PR is accepted: the slower "before" time will not be relevant to anyone going forward. And the most important question (IMO): is there enough interest in this group to review/accept this proposal? (If not: I can stop spending time on it.) Before this PRAfter this PR |
|
With the patch, the code |
Also this removes the performance comparison. (As of this writing there's a separate `test/micro/org/openjdk/bench/sun/awt/image/PNGImageDecoder_8bit_uninterlaced.java` file used to demonstrate that this change is more performant.)
|
@liach thanks for checking that far ahead. I just confirmed that code is NOT dead, though. It should still be used for interlaced 8-bit PNGs. I rewrote the unit test just now so that:
I tried removing the line you mentioned ( |
I also see similar improvements when i run the newly added JMH test locally on my Macbook. Before PR: After PR: Its good that we are adding a JMH test for performance bench-marking. |
jayathirthrao
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Product change looks good to me.
Some changes are needed to make the JMH test run.
I have also given full functional test run after this update.
Will update once i have results.
| * or visit www.oracle.com if you need additional information or have any | ||
| * questions. | ||
| */ | ||
| package org.openjdk.bench.sun.misc; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
package should be org.openjdk.bench.sun.awt.image
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is updated
| import org.openjdk.jmh.annotations.Scope; | ||
| import org.openjdk.jmh.annotations.State; | ||
| import org.openjdk.jmh.annotations.Warmup; | ||
| import org.openjdk.jmh.infra.Blackhole; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to import import org.openjdk.jmh.annotations.Benchmark; and import org.openjdk.jmh.annotations.Setup; also. Otherwise i am seeing compilation errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is updated
gredler
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change in PNGImageDecoder makes sense to me, although I haven't had a chance to run the JMH benchmark locally on my end.
| * PNGImageDecoder. This method makes sure the enhancement didn't cost us | ||
| * any accuracy. | ||
| */ | ||
| private static void testCorrectness(BufferedImage expected, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the correctness check in the JMH benchmark be removed, since that's handled in the unit test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is updated
|
|
||
| @Benchmark | ||
| public void measurePNGImageDecoder(Blackhole bh) throws Exception { | ||
| Image img = Toolkit.getDefaultToolkit().createImage(pngImageData); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the BufferedImage need to be created this way, or could it be simplified down to a simple ImageIO.read() with a ByteArrayInputStream?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ImageIO is a different rendering model; the BufferedImage needs to be created this way. (Or maybe (?) with a PixelGrabber or MediaTracker?)
| * | ||
| * This test has never failed. | ||
| */ | ||
| public class PNGImageDecoder_8bit_performance { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is now purely a regression test and the performance aspect has been removed, perhaps rename to e.g. PngImageDecoder8BitTest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is renamed
| int argb1 = expected.getRGB(x, y); | ||
| int argb2 = actual.getRGB(x, y); | ||
| if (argb1 != argb2) { | ||
| throw new Error("x = " + x + ", y = " + y); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error -> RuntimeException, and I'd probably also include the two colors that didn't match in the message
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is updated
| private static void testCorrectness(BufferedImage expected, | ||
| BufferedImage actual) { | ||
| if (expected.getWidth() != actual.getWidth()) { | ||
| throw new Error(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably better to throw a RuntimeException instead of an Error here and below (at least that seems to be the convention that I've seen elsewhere). Also always best to include a short error message that helps zero in on the exact issue if it ever fails.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is updated
| BufferedImage expected = models[0].load(imageData); | ||
| BufferedImage actual = models[1].load(imageData); | ||
|
|
||
| testCorrectness(expected, actual); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I correct in assuming that both models end up using PNGImageDecoder under the covers? If so, won't expected and actual always match, even if there is a bug in PNGImageDecoder? I wonder if it would be better to keep the original BufferedImage around (the one we draw on), use it as expected, and compare it to the two model-generated images.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I correct in assuming that both models end up using PNGImageDecoder under the covers?
It'd make a lot of sense if they shared the same code, but no. It is my understanding we have two separate decoders. ImageIO uses the com.sun.imageio.plugins.png.PNGImageReader , and this PR modifies the sun.awt.image.PNGImageDecoder.
The fact that the PNGImageDecoder is a little slower than the ImageIO classes came to my attention because I was comparing the two decoding models, and the older sun.awt classes appear to be faster than the newer ImageIO classes -- except for this one case. This PR will fix this discrepancy.
(Separately: I've submitted a few other PRs that similarly focus on the older sun.awt decoding classes. See JDK-8357034 , JDK-8356320 , JDK-8356137 , JDK-8351913 . They generally compare the two models against each other for correctness. I also visually inspected the results at the time to triple-check, but in all of those cases ImageIO was already "doing the right thing". I guess I've made it my goal to bring the older sun.awt decoding classes up-to-date.)
I wonder if it would be better to keep the original BufferedImage around (the one we draw on), use it as expected, and compare it to the two model-generated images.
Sure. I updated this test so it avoids decoding with ImageIO.
This is in response to: openjdk#29004 (comment)
This is in response to: openjdk#29004 (comment)
The PNGImageDecoder_8bit_performance.java /PngImageDecoder8BitTest.java class will test correctness. This is in response to: openjdk#29004 (comment)
This is in response to: hhttps://github.com/openjdk/pull/29004#discussion_r2657759447
This is in response to: openjdk#29004 (comment)
This is in response to: openjdk#29004 (comment)
This is in response to: openjdk#29004 (comment)
When decoding an uninterlaced 8-bit PNG image, the PNGImageDecoder is basically copying one byte at a time.
This PR uses System.arraycopy instead, and it shows approx a 10% improvement.
This graph shows the time it takes different decoders to convert a byte array into a BufferedImage as the size of the PNG image increases:
(This originally came to my attention when looking at an image in Java 1.8. There the ImageConsumer model took approx 400% longer than ImageIO. I was happy to see in recent JDKs that gap narrowed significantly, but there was still a noticeable 10% discrepancy.)
I haven't tried submitting a performance enhancement PR before; I'm not sure if this issue meets this group's threshold for being worth addressing. And if it does: I'm not sure how to structure a unit test for it.
Progress
Issue
Reviewing
Using
gitCheckout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/29004/head:pull/29004$ git checkout pull/29004Update a local copy of the PR:
$ git checkout pull/29004$ git pull https://git.openjdk.org/jdk.git pull/29004/headUsing Skara CLI tools
Checkout this PR locally:
$ git pr checkout 29004View PR using the GUI difftool:
$ git pr show -t 29004Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/29004.diff
Using Webrev
Link to Webrev Comment