Modify

Opened 3 weeks ago

Closed 2 weeks ago

Last modified 9 days ago

#24677 closed enhancement (fixed)

[PATCH] Improved startup time by 33% through SVG handling optimization

Reported by: dnet Owned by: team
Priority: normal Milestone: 26.03
Component: Core Version:
Keywords: Cc:

Description

Prerendering SVG icons and making all SVG loading async together lowered startup time on my machine by 33%. I added a simple two-line way of measuring startup time (see startup-measurement.diff) and produced the following output on my machine.

Before: (revision 19547 with only startup-measurement.diff applied)

2026-03-17 21:48:47.795 INFO: Startup complete in 13919 ms
2026-03-17 21:49:17.480 INFO: Startup complete in 13917 ms
2026-03-17 21:51:04.253 INFO: Startup complete in 13475 ms

After: (revision 19547 with both startup-measurement.diff and startup-performance.diff applied)

2026-03-17 23:26:31.053 INFO: Startup complete in 8902 ms
2026-03-17 23:26:48.479 INFO: Startup complete in 8870 ms
2026-03-17 23:27:04.865 INFO: Startup complete in 7950 ms

I tested on macOS 15.7.3, all tests ran, and pmd emitted no issues that affected either of the files my diffs touch.

Attachments (6)

startup-performance.diff (2.1 KB ) - added by dnet 3 weeks ago.
the actual patch I would like to submit
startup-measurement.diff (948 bytes ) - added by dnet 3 weeks ago.
a helper patch that allows simple yet precise (enough) measurement of startup time
19547_icons_imagery_list.png (122.9 KB ) - added by anonymous 2 weeks ago.
version 19547 imagery list, icons displayed
19548_icons_imagery_list.png (122.0 KB ) - added by anonymous 2 weeks ago.
version 19548 imagery list, icons missing
19548_placeholder_clock.png (211.3 KB ) - added by anonymous 2 weeks ago.
version 19548 placeholder clocks are displayed very long
more-tightly-scoped.diff (3.9 KB ) - added by dnet 2 weeks ago.
A more tightly scoped patch that partially reverts the change to allow sync loading of assets for the rest of the runtime

Download all attachments as: .zip

Change History (29)

by dnet, 3 weeks ago

Attachment: startup-performance.diff added

the actual patch I would like to submit

by dnet, 3 weeks ago

Attachment: startup-measurement.diff added

a helper patch that allows simple yet precise (enough) measurement of startup time

comment:1 by stoecker, 3 weeks ago

Milestone: 26.03

Can the change to ImageProvider have any negative effect?

comment:2 by dnet, 3 weeks ago

Well, getResourceAsync for local resources was synchronous, so the action ran on the calling thread before returning. MapImage (map style icons) relied on this:

private Image getImage() {
    if (img != null) return img;
    loadImage();               // old: action ran synchronously, img was set
    synchronized (this) {
        if (img == null) {
            img = ImageProvider.get("clock").getImage();  // placeholder icon
            temporary = true;
        }
    }
    return img;
}

With my patch, loadImage() returns early (queues to IMAGE_FETCHER), so img is null by the time execution reaches the null-check. Map style icons that are local (JAR-bundled) now always show the clock placeholder briefly before being replaced by the real icon. For remote/ZIP icons this was already the behavior; the change extends it to local ones.

In practice, this is probably not an issue since IMAGE_FETCHER is idle by the time the user opens a data layer (all 1143 preset tasks finish in ~4.5s of background loading after the ~9.2s startup). But it could be visible if a file is passed as a command-line argument and rendered immediately.

Also, MapImage.getImageResource() calls loadImageResource().get() (blocking). If IMAGE_FETCHER is still processing preset tasks at that point, this call waits behind them all. Again, unlikely in normal usage, but possible with pre-loaded files.

If these are unacceptable for JOSM, I can submit an alternative patch that skips getResourceAsync entirely, and instead the IMAGE_FETCHER dispatch happens directly in TaggingPreset.setIcon(), so the change is scoped to the one caller I actually cared about.

CompletableFuture.supplyAsync(
    () -> new ImageProvider(iconName)...getResource(), IMAGE_FETCHER)
.thenAcceptAsync(result -> {
    // pre-render + post to EDT
}, IMAGE_FETCHER);

This would leave MapImage and other callers of getResourceAsync with the original "synchronous-for-local" behavior.

comment:3 by stoecker, 3 weeks ago

Thanks for the detailed description. I think that behavior change is acceptable. For me the speedup wasn't such a major impact (still same percentage, but only from 4 to 3 s).

comment:4 by stoecker, 3 weeks ago

Resolution: fixed
Status: newclosed

In 19548/josm:

fix #24677 - improve startup time needed for loading SVG icons

comment:5 by anonymous, 2 weeks ago

Resolution: fixed
Status: closedreopened

This patch causes several issues for me:

  1. The clock placeholders remain visible for a very long time, especially when certain external resources cannot be loaded. Example:
    INFORMATION: GET https://map.valentim.org/favicon.png -> !!! (19,6 s)
    WARNUNG: java.net.SocketException: Connection reset
    
  1. In the list of background images, almost no icons are displayed anymore.

These issues do not occur with version 19547, but have been present since version 19548.

comment:6 by stoecker, 2 weeks ago

Hmm, your point 1 doesn't match the patch. The patch should only affects local files and that's clearly a remote file. Also I can't reproduce your point 2. Please describe in more detail (i.e. with screenshots) how you're affected.

comment:7 by anonymous, 2 weeks ago

I explicitly built and tested versions 19547 and 19548 for testing, and for me, the issue is definitely caused by this patch. I’ll describe it in more detail with a screenshot later.

comment:8 by anonymous, 2 weeks ago

"the issue" -> "both issues"

comment:9 by stoecker, 2 weeks ago

Note that I fixed the broken image in your example, so don't reload the map list!

comment:10 by anonymous, 2 weeks ago

Thanks.
Just a quick intermediate information:

To reproduce the issues, you need to have the map with the unreachable icon in the image list:
"Available default entries" -> select the "Portugal Topographic Map (valentim.org)" map -> "Activate" -> "Selected entries"

By the way, it is possible to add the unreachable icon manually back to the cache/mirror_https___josm.openstreetmap.de_maps file again:

<icon>https://map.valentim.org/favicon.png</icon>

by anonymous, 2 weeks ago

version 19547 imagery list, icons displayed

by anonymous, 2 weeks ago

version 19548 imagery list, icons missing

comment:11 by anonymous, 2 weeks ago

Log of JOSM version 19548. After about 20 seconds the "Connection reset" occurs (if there are more internet resources which can't be loaded, it may take longer, I'm not sure), after that the placeholder clocks on the map disappear. So the failing download of the icon causes the placeholder clocks to be shown on the map, until the "connection reset" occurs. Something is blocking here.
With JOSM version 19547, everything is working.

$ java -jar ~/josm-19548.jar
2026-03-24 21:07:20.665 INFO: Log level is at INFO (INFO, 800)
2026-03-24 21:07:26.402 INFO: Obtained 266 Tag2Link rules from resource://META-INF/resources/webjars/tag2link/2026.2.21/index.json
2026-03-24 21:07:27.832 INFO: Could not load tool definition splitway
2026-03-24 21:07:28.436 INFO: Changeset updater active (checks every 60 minutes if open changesets have been closed)
2026-03-24 21:07:49.086 INFO: GET https://map.valentim.org/favicon.png -> !!! (19.6 s)
2026-03-24 21:07:49.092 WARNING: java.net.SocketException: Connection reset
java.net.SocketException: Connection reset
        at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:316)
        at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:354)
        at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:798)
        at java.base/java.net.Socket$SocketInputStream.implRead(Socket.java:974)
        at java.base/java.net.Socket$SocketInputStream.read(Socket.java:964)
        at java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:489)
        at java.base/sun.security.ssl.SSLSocketInputRecord.readHeader(SSLSocketInputRecord.java:483)
        at java.base/sun.security.ssl.SSLSocketInputRecord.decode(SSLSocketInputRecord.java:160)
        at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:111)
        at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1506)
        at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1421)
        at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:455)
        at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:426)
        at java.base/sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:483)
        at java.base/sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:187)
        at java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:141)
        at org.openstreetmap.josm.tools.Http1Client.performConnection(Http1Client.java:78)
        at org.openstreetmap.josm.tools.HttpClient.connect(HttpClient.java:162)
        at org.openstreetmap.josm.tools.HttpClient.connect(HttpClient.java:136)
        at org.openstreetmap.josm.tools.HttpClient.connect(HttpClient.java:125)
        at org.openstreetmap.josm.io.CachedFile.checkLocal(CachedFile.java:508)
        at org.openstreetmap.josm.io.CachedFile.getFile(CachedFile.java:286)
        at org.openstreetmap.josm.io.CachedFile.getInputStream(CachedFile.java:227)
        at org.openstreetmap.josm.tools.ImageProvider.getIfAvailableHttp(ImageProvider.java:956)
        at org.openstreetmap.josm.tools.ImageProvider.getIfAvailableImpl(ImageProvider.java:863)
        at org.openstreetmap.josm.tools.ImageProvider.getResource(ImageProvider.java:694)
        at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1789)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
        at java.base/java.lang.Thread.run(Thread.java:1474)

2026-03-24 21:07:49.194 SEVERE: Failed to locate image 'https://map.valentim.org/favicon.png'

by anonymous, 2 weeks ago

Attachment: 19548_placeholder_clock.png added

version 19548 placeholder clocks are displayed very long

comment:12 by anonymous, 2 weeks ago

The placeholder clock issue always occurs, while JOSM is trying to download an internet resource, which can't be reached, until a connection reset occurs.

comment:13 by anonymous, 2 weeks ago

I did my tests also on a virtual machine, with a complete new JOSM profile, without JOSM plugins, to verify that there is nothing wrong with my JOSM profile or JOSM plugins.

by dnet, 2 weeks ago

Attachment: more-tightly-scoped.diff added

A more tightly scoped patch that partially reverts the change to allow sync loading of assets for the rest of the runtime

comment:14 by dnet, 2 weeks ago

I attached a new patch that implements what I suggested in my comment above. This tightens the scope of the change so that the optimizations kick in only during loading but not after. I tested them on my machine, it still produces the nice loading time but no placeholders afterwards.

comment:15 by anonymous, 2 weeks ago

A quick test showed that this new patch fixes both issues for me. Thank you very much.

comment:16 by stoecker, 2 weeks ago

Resolution: fixed
Status: reopenedclosed

In 19553/josm:

fix #24677 - applied modified patch by dnet

comment:17 by stoecker, 2 weeks ago

Applied, but I think that's a workaround. Real issues seems to be more like

a) Why does failed image loading delay the others?
b) Why does JOSM retry again and again loading a failed image?

in reply to:  17 comment:18 by dnet, 2 weeks ago

Replying to stoecker:

Applied, but I think that's a workaround. Real issues seems to be more like

a) Why does failed image loading delay the others?

IMAGE_FETCHER is a single-threaded executor (lines 308–309):

private static final ExecutorService IMAGE_FETCHER =
        Executors.newSingleThreadExecutor(...);

All remote image fetches (getAsync/getResourceAsync for HTTP/HTTPS/wiki URLs) are serialized through this one thread. When a remote server is unreachable/slow, CachedFile and HttpClient will block that thread for up to the full connect timeout and/or read timeout. Every other remote image queued on IMAGE_FETCHER is stuck waiting until the failing fetch times out.

b) Why does JOSM retry again and again loading a failed image?

In getIfAvailableImpl() (lines 874–882), the in-memory cache only stores successful results:

ir = getIfAvailableHttp(url, type);
if (ir != null) {          // only cached on success
    cache.put(prefix + url, ir);
}
return ir;                 // null on failure, never cached

There is no negative caching: failed fetches are returned as null and silently discarded. The next time anything requests the same URL (in this case, a UI repaint), the cache lookup finds nothing and issues a full network request again, including another timeout wait.

comment:19 by stoecker, 2 weeks ago

Do you have time maybe to fix one or both issues?

comment:20 by anonymous, 9 days ago

FWIW: The sum total of the just released 19555 stable build is a WOW. Faster startup, resource/memory leaks fixes (now finally see in the task manager JOSM giving/releasing RAM back. AND, with my biggest dataset ever, built from 160 partial data fetches, being able to run an 'update data' after restart without it eventually going the 'multiple errors list with 'you downloaded too much data'. AND AND, after 6+ hours of heavy mapping remains performant both in line drawing and follow/tracing.

👏👏👏

comment:21 by stoecker, 9 days ago

Ah. I thought the resource leaks I fixed had no impact. Nice to know they were useful fixes :-)

Thought the "you downloaded too much data" is a server side restriction JOSM can do nothing about.

comment:22 by anonymous, 9 days ago

Unfortunately the download part was a 2x fluke. This morning it was back in force. (even was told 'wait 13 seconds before trying again').
FYI, v.v the initial small slices fetching like here in 160 pieces is intentional. When the larger area has a limited amount of data, but there's a lot to be mapped and you'd map larger area chunks of that, you eventually get a warning that the single fetch contains too much data and the update is stopped where's with the many smaller parcels, the series fetching continues but you get an end report list of fetches which caused too much data serially. Over copper more than via the high speed 5G connection. Latter is though capped at 150GB a month so only use that as fall back. /OT

comment:23 by stoecker, 9 days ago

It's Off Topic in this ticket, but that's not about your side, it's about the server. The download restrictions protect the server. Have a look at Overpass API usage which can help, but has it's own set of restrictions.

Modify Ticket

Change Properties
Set your email in Preferences
Action
as closed The owner will remain team.
as The resolution will be set.
The resolution will be deleted. Next status will be 'reopened'.

Add Comment


E-mail address and name can be saved in the Preferences .
 
Note: See TracTickets for help on using tickets.