Conversation
ClassLoaderFilesResourcePatternResolver.isDeleted() calls resource.exists() and resource.getURI() inside the loop over the uploaded files, so both are repeated once per DELETED entry although neither depends on the entry. Every resource lookup made while the application context is being built goes through this method - about 1300 times per restart in the application I measured - so a session that has accumulated 100 deleted files performs around 130 000 file system lookups where 1300 are enough. This commit hoists both calls out of the loop: the resource is inspected at most once per call, on the first DELETED entry, and the URI comparison then uses the cached value. The method still returns on the first match and still reports a failing getURI() as an IllegalStateException, so behaviour is unchanged. The loop now walks the per-directory entry sets, which is what ClassLoaderFiles.addAll() and RestartServer already do, instead of the flattened view added in spring-projectsgh-46289. That view is wrapped in Collections.unmodifiableSet(), whose iterator is shared JDK code; in a running application its delegate calls are megamorphic and C2 stops inlining them, which made restarts with many accumulated files and no deletions slower than before spring-projectsgh-46289. isDeleted() returns a boolean, so the visiting order of the entries cannot change its result. Measured on a Spring Boot application driven through the remote restart path (restart request to ApplicationReadyEvent), 12 JVMs per variant and 20 restarts per JVM, medians: 100 source directories, 10 000 entries, 10 deleted: 278 ms -> 200 ms 100 source directories, 10 000 entries, 100 deleted: 891 ms -> 201 ms 100 source directories, 10 000 entries, 500 deleted: 3536 ms -> 200 ms 100 source directories, 50 000 entries, none deleted: 431 ms -> 341 ms 100 source directories, 10 000 entries, none deleted: 202 ms -> 192 ms no uploaded files: 150 ms -> 148 ms Signed-off-by: DongHoon Lee <dhl1924@naver.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #46289.
ClassLoaderFilesResourcePatternResolver.isDeleted()callsresource.exists()andresource.getURI()inside the loop over the uploaded files, so both are repeated once perDELETEDentry although neither depends on the entry:Every resource the context resolves while it is being built goes through this method — about 1,300 times per restart in the application I measured — so a session that has accumulated 100 deleted files makes around 130,000 file system lookups where 1,300 are enough. The resource is now inspected at most once per call, on the first
DELETEDentry, and, as before, not at all when nothing is deleted.The loop also walks the per-directory entry sets again, as
ClassLoaderFiles.addAll()andRestartServerdo, instead of the flattened view added in #46289. That view is aCollections.unmodifiableSet(...)whose iterator is JDK code shared by every unmodifiable collection in the application; in a running application C2 stops inlining its delegate calls, in a loop that runs tens of millions of times per restart. That is a regression I introduced in #46289: with many accumulated entries and nothing deleted, restarts became slower than they were before it.The flattened map itself stays — it serves
getFile(), andisDeleted()could not use it for a lookup anyway, since it matches a resource URI by suffix.isDeleted()returns a boolean, so the order in which entries are visited cannot change its result.getAdditionalResources()is left on the flattened view: it runs twice per restart, not 1,300 times.Measurements
An application driven through the remote restart path, timed from the restart request to
ApplicationReadyEvent. 12 JVMs per variant for each row, interleaved, 20 restarts each, medians.The last row is local DevTools, where
ClassLoaderFilesstays empty and neither version does any work; it doubles as the control for the measurement. The number of source directories does not matter here: with 100 deleted entries the saving is 690ms with one directory and 687ms with two hundred.The fourth row is the regression. Measured separately in that configuration with the intermediate variants, 8 JVMs each: 376ms before #46289, 435ms on current
main, 351ms with this change, and 339ms with only theunmodifiableSetwrapper removed and the loop otherwise untouched — which is what identifies the wrapper rather than the map as the cause.Behaviour
The devtools tests pass on current
mainwith JDK 25, and three are added: for a deletion recorded in another source directory, for inspecting the resource only once, and for not touching it when nothing is deleted. A failinggetURI()is still reported as the sameIllegalStateException.I also compared the two implementations exhaustively over a closed alphabet (1,885
ClassLoaderFilesstates x 14 resources) and under randomised operation sequences. They agree everywhere except on a resolver built over aClassLoaderFilescopy whose original is mutated afterwards, whichRestarternever builds: it constructs the resolver over its own instance, and the copy goes toRestartClassLoader, which only callsgetFile().Notes
main,4.1.xand4.0.xcarry identical copies of both files, so I am happy to retarget.Scanning the map's entry set without the wrapper is about 30ms faster still with 50,000 accumulated entries, but it would hand out a mutable view of
filesByName, so I kept the read-only accessor and changed the loop instead.