#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)
Change History (29)
by , 3 weeks ago
| Attachment: | startup-performance.diff added |
|---|
by , 3 weeks ago
| Attachment: | startup-measurement.diff added |
|---|
a helper patch that allows simple yet precise (enough) measurement of startup time
comment:1 by , 3 weeks ago
| Milestone: | → 26.03 |
|---|
Can the change to ImageProvider have any negative effect?
comment:2 by , 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 , 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:5 by , 2 weeks ago
| Resolution: | fixed |
|---|---|
| Status: | closed → reopened |
This patch causes several issues for me:
- 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
- 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 , 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 , 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:9 by , 2 weeks ago
Note that I fixed the broken image in your example, so don't reload the map list!
comment:10 by , 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 , 2 weeks ago
| Attachment: | 19547_icons_imagery_list.png added |
|---|
version 19547 imagery list, icons displayed
by , 2 weeks ago
| Attachment: | 19548_icons_imagery_list.png added |
|---|
version 19548 imagery list, icons missing
comment:11 by , 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 , 2 weeks ago
| Attachment: | 19548_placeholder_clock.png added |
|---|
version 19548 placeholder clocks are displayed very long
comment:12 by , 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 , 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 , 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 , 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 , 2 weeks ago
A quick test showed that this new patch fixes both issues for me. Thank you very much.
follow-up: 18 comment:17 by , 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?
comment:18 by , 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:20 by , 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 , 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 , 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 , 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.



the actual patch I would like to submit