Skip to content

Avoid re-reading the image file on every scaled drawImage - #3506

Open
vogella wants to merge 1 commit into
eclipse-platform:masterfrom
vogella:perf/drawimage-isdynamicallysizable
Open

Avoid re-reading the image file on every scaled drawImage#3506
vogella wants to merge 1 commit into
eclipse-platform:masterfrom
vogella:perf/drawimage-isdynamicallysizable

Conversation

@vogella

@vogella vogella commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

The scaled GC.drawImage overload asked isDynamicallySizable(fileName) on every draw and then discarded the answer, so for any PNG, GIF or JPEG the backing file was opened and sniffed again for each repaint, forever.
Since sizability is a property of the file and never depends on the requested size, this remembers the file already found not to be sizable, keyed on the resolved file name so that an Image backed by different files over its lifetime still behaves correctly.
A new ImageLoader.loadBySizeIfDynamicallySizable does the format check and the load from a single open, which also closes the window where the file could change between the two reads.

Measured with strace -f -e trace=openat over 100 draws of one Image on Linux/GTK at device zoom 100, opens per draw drop from 1.00 to 0.01 for PNG (stable and alternating draw sizes) and from 2.00 to 1.00 for SVG at alternating sizes.
That is roughly 1.5 to 2.5 us per draw on a 16x16 icon, and the rendered output is bit-identical before and after.
Andrey's concern on the issue about NFS-mounted config areas applies directly here, since this was one openat per scaled draw on that path.

Two limits of that table are worth stating so nobody reads it as covering HiDPI.

At device zoom 200 the same PNG case measures 2.00 opens per draw before and 1.00 after, rather than 0.01.
The open that remains is a different defect, #3507: the internal drawImage reads the source dimensions from getImageData(), which re-decodes the file at any zoom other than 100.
That one is fixed separately in #3510, and with both applied the case reaches 0.00.

This PR also does not fix CachedImageAtSize.isReusable, which compares a pixel value against a point value: refresh converts the requested size with DPIUtil.pointToPixel(destWidth, getDeviceZoom()), while the cached image's width is divided back down to points in Image.init(ImageData, zoom).
At zoom 200 that comparison can never be true, so the single-entry size cache never hits there and every draw is a miss.
The change here makes each of those misses cheap rather than making them stop happening; the unit mismatch deserves its own fix.

One behavior change worth noting: a file replaced in place at the same path is no longer re-sniffed for the lifetime of the Image.

Fixes #3505

CachedImageAtSize.loadImageDataAtExactSize asked
ImageDataLoader.isDynamicallySizable(fileName) whether the backing file
can be loaded at an arbitrary size, then discarded the answer. For a
format that cannot (PNG, GIF, JPEG, so nearly every icon) the cached
image stayed null, so the open plus format sniff repeated on every
draw. For SVG each cache miss opened the file twice, to sniff and to
load.

Sizability depends on the file alone and never on the requested size,
so remember the file already found not to be sizable. The memo is keyed
on the resolved file name, because an Image may be backed by different
files. The new ImageLoader.loadBySizeIfDynamicallySizable does the
check and the load from a single open.

Opens per draw over 100 draws of one Image, strace on Linux/GTK:

  PNG, stable draw size         1.00 -> 0.01
  PNG, alternating draw size    1.00 -> 0.01
  SVG, stable draw size         0.02 -> 0.01
  SVG, alternating draw size    2.00 -> 1.00

That is 1.5 to 2.5 us per draw on a 16x16 icon, and rendering is
bit-identical. One behavior change: a file replaced in place at the same
path is no longer re-sniffed for the lifetime of the Image.

Fixes eclipse-platform#3505
@github-actions

Copy link
Copy Markdown
Contributor

Test Results

  212 files  ± 0    212 suites  ±0   28m 3s ⏱️ -24s
4 896 tests + 2  4 872 ✅ + 2   24 💤 ±0  0 ❌ ±0 
7 077 runs  +12  6 909 ✅ +12  168 💤 ±0  0 ❌ ±0 

Results for commit 61dec63. ± Comparison against base commit 9ca368d.

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.

Scaled GC.drawImage re-opens the image file from disk on every draw

1 participant