Skip to content

Commit 6bd2a16

Browse files
committed
[GR-74505] Split ImageHeapCollection support singleton.
PullRequest: graal/23697
2 parents 55a255f + eba296f commit 6bd2a16

File tree

5 files changed

+171
-111
lines changed

5 files changed

+171
-111
lines changed

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/ImageHeapMap.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@
4545
* <p>
4646
* At image build time the map is implemented as a {@link ConcurrentHashMap} or
4747
* {@link ConcurrentIdentityHashMap} wrapped into an {@link EconomicMapWrap}. The
48-
* ImageHeapCollectionFeature intercepts each build time map and transforms it into an actual
49-
* memory-efficient {@link EconomicMap} backed by a flat object array during analysis. The later
50-
* image building stages see only the {@link EconomicMap}.
48+
* {@code ImageHeapCollectionSupport} object replacer intercepts each build time map and transforms
49+
* it into an actual memory-efficient {@link EconomicMap} backed by a flat object array during
50+
* analysis. The later image building stages see only the {@link EconomicMap}.
5151
* <p>
5252
* Additionally, in layered builds, at run time this map can be part of a
5353
* {@link LayeredImageHeapMap} structure, i.e., conceptually a linked list of {@link EconomicMap}s

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/LayeredHostedImageHeapMapCollector.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@
4545
import com.oracle.svm.shared.singletons.traits.SingletonTraits;
4646

4747
/**
48-
* {@link HostedImageHeapMap} are stored in {@code ImageHeapCollectionFeature#allMaps} through an
48+
* {@link HostedImageHeapMap} are stored in {@code ImageHeapCollectionSupport#allMaps} through an
4949
* object replacer, meaning that only maps reachable at run time are tracked and rescanned. In the
5050
* extension layers, some maps can be extended at build time, but not be reachable from run time
5151
* code of the current layer. So all the maps reachable in the base layer need to be tracked and
5252
* when the map is created in an extension layer, it needs to be manually added to
53-
* {@code ImageHeapCollectionFeature#allMaps} to ensure it is always rescanned and reachable.
53+
* {@code ImageHeapCollectionSupport#allMaps} to ensure it is always rescanned and reachable.
5454
*/
5555
@Platforms(Platform.HOSTED_ONLY.class)
5656
@SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = LayeredHostedImageHeapMapCollector.LayeredCallbacks.class)

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/ImageHeapCollectionFeature.java

Lines changed: 8 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -25,126 +25,33 @@
2525

2626
package com.oracle.svm.hosted.heap;
2727

28-
import java.util.Set;
29-
import java.util.concurrent.ConcurrentHashMap;
28+
import org.graalvm.nativeimage.ImageSingletons;
3029

31-
import com.oracle.graal.pointsto.ObjectScanner.OtherReason;
32-
import com.oracle.graal.pointsto.ObjectScanner.ScanReason;
33-
import com.oracle.svm.core.BuildPhaseProvider;
34-
import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature;
3530
import com.oracle.svm.core.feature.InternalFeature;
36-
import com.oracle.svm.core.imagelayer.ImageLayerBuildingSupport;
31+
import com.oracle.svm.hosted.FeatureImpl.DuringAnalysisAccessImpl;
32+
import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature;
3733
import com.oracle.svm.shared.singletons.traits.BuiltinTraits.BuildtimeAccessOnly;
3834
import com.oracle.svm.shared.singletons.traits.BuiltinTraits.NoLayeredCallbacks;
3935
import com.oracle.svm.shared.singletons.traits.SingletonTraits;
40-
import com.oracle.svm.core.util.ImageHeapList.HostedImageHeapList;
41-
import com.oracle.svm.core.util.ImageHeapMap.HostedImageHeapMap;
42-
import com.oracle.svm.core.util.LayeredHostedImageHeapMapCollector;
43-
import com.oracle.svm.core.util.LayeredImageHeapMap;
44-
import com.oracle.svm.shared.util.VMError;
45-
import com.oracle.svm.hosted.FeatureImpl.DuringAnalysisAccessImpl;
4636

4737
@AutomaticallyRegisteredFeature
4838
@SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = NoLayeredCallbacks.class)
4939
final class ImageHeapCollectionFeature implements InternalFeature {
5040

51-
private final Set<HostedImageHeapMap<?, ?>> allMaps = ConcurrentHashMap.newKeySet();
52-
private final Set<HostedImageHeapList<?>> allLists = ConcurrentHashMap.newKeySet();
53-
private final ScanReason scanReason = new OtherReason("Manual rescan triggered from " + ImageHeapCollectionFeature.class);
54-
5541
@Override
5642
public void duringSetup(DuringSetupAccess config) {
57-
config.registerObjectReplacer(this::replaceHostedWithRuntime);
43+
ImageHeapCollectionSupport support = new ImageHeapCollectionSupport();
44+
ImageSingletons.add(ImageHeapCollectionSupport.class, support);
45+
config.registerObjectReplacer(support::replaceHostedWithRuntime);
5846
}
5947

60-
private Object replaceHostedWithRuntime(Object obj) {
61-
if (obj instanceof HostedImageHeapMap) {
62-
HostedImageHeapMap<?, ?> hostedImageHeapMap = (HostedImageHeapMap<?, ?>) obj;
63-
if (BuildPhaseProvider.isAnalysisFinished()) {
64-
VMError.guarantee(allMaps.contains(hostedImageHeapMap), "ImageHeapMap reachable after analysis that was not seen during analysis");
65-
} else {
66-
allMaps.add(hostedImageHeapMap);
67-
if (hostedImageHeapMap.getRuntimeMap() instanceof LayeredImageHeapMap<Object, Object> layeredImageHeapMap) {
68-
LayeredHostedImageHeapMapCollector.singleton().registerReachableHostedImageHeapMap(layeredImageHeapMap);
69-
}
70-
}
71-
return hostedImageHeapMap.getRuntimeMap();
72-
} else if (obj instanceof HostedImageHeapList<?> hostedImageHeapList) {
73-
if (BuildPhaseProvider.isAnalysisFinished()) {
74-
VMError.guarantee(allLists.contains(hostedImageHeapList), "ImageHeapList reachable after analysis that was not seen during analysis");
75-
} else {
76-
allLists.add(hostedImageHeapList);
77-
}
78-
return hostedImageHeapList.getRuntimeList();
79-
}
80-
return obj;
81-
}
82-
83-
/**
84-
* This method makes sure that the content of all modified {@link HostedImageHeapMap}s and
85-
* {@link HostedImageHeapList}s is properly propagated to their runtime counterparts. As both
86-
* the number of these collections and their individual sizes are theoretically unbounded, we
87-
* use <i>parallel streams</i> to divide the load across all cores.
88-
* <p>
89-
* We split the process into two stages. First, the content of each modified collection is
90-
* propagated from the hosted to the runtime version. Then, the modified runtime collections are
91-
* rescanned. The split is done to prevent concurrent modifications of the hosted collections
92-
* during the execution of this method, as they may be updated indirectly during the heap
93-
* scanning.
94-
*/
9548
@Override
9649
public void duringAnalysis(DuringAnalysisAccess a) {
97-
DuringAnalysisAccessImpl access = (DuringAnalysisAccessImpl) a;
98-
if (ImageLayerBuildingSupport.buildingExtensionLayer()) {
99-
allMaps.addAll(LayeredHostedImageHeapMapCollector.singleton().getPreviousLayerReachableMaps());
100-
}
101-
Set<Object> objectsToRescan = ConcurrentHashMap.newKeySet();
102-
allMaps.parallelStream().forEach(hostedImageHeapMap -> {
103-
if (hostedImageHeapMap.needsUpdate()) {
104-
hostedImageHeapMap.update();
105-
objectsToRescan.add(hostedImageHeapMap.getCurrentLayerMap());
106-
}
107-
});
108-
allLists.parallelStream().forEach(hostedImageHeapList -> {
109-
if (hostedImageHeapList.needsUpdate()) {
110-
hostedImageHeapList.update();
111-
objectsToRescan.add(hostedImageHeapList.getRuntimeList());
112-
}
113-
});
114-
if (!objectsToRescan.isEmpty()) {
115-
objectsToRescan.parallelStream().forEach(obj -> access.rescanObject(obj, scanReason));
116-
access.requireAnalysisIteration();
117-
}
118-
}
119-
120-
public boolean needsUpdate() {
121-
for (var hostedImageHeapMap : allMaps) {
122-
if (hostedImageHeapMap.needsUpdate()) {
123-
return true;
124-
}
125-
}
126-
for (var hostedImageHeapList : allLists) {
127-
if (hostedImageHeapList.needsUpdate()) {
128-
return true;
129-
}
130-
}
131-
return false;
50+
ImageHeapCollectionSupport.singleton().updateAndRescanCollections((DuringAnalysisAccessImpl) a);
13251
}
13352

13453
@Override
13554
public void afterImageWrite(AfterImageWriteAccess access) {
136-
for (var hostedImageHeapMap : allMaps) {
137-
if (hostedImageHeapMap.needsUpdate()) {
138-
throw VMError.shouldNotReachHere("ImageHeapMap modified after static analysis:%n%s%n%s",
139-
hostedImageHeapMap, hostedImageHeapMap.getCurrentLayerMap());
140-
}
141-
}
142-
for (var hostedImageHeapList : allLists) {
143-
if (hostedImageHeapList.needsUpdate()) {
144-
throw VMError.shouldNotReachHere("ImageHeapList modified after static analysis:%n%s%n%s",
145-
hostedImageHeapList, hostedImageHeapList.getRuntimeList());
146-
147-
}
148-
}
55+
ImageHeapCollectionSupport.singleton().verifyCollectionsAreStableAfterAnalysis();
14956
}
15057
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
package com.oracle.svm.hosted.heap;
26+
27+
import java.util.Set;
28+
import java.util.concurrent.ConcurrentHashMap;
29+
30+
import org.graalvm.nativeimage.ImageSingletons;
31+
32+
import com.oracle.graal.pointsto.ObjectScanner.OtherReason;
33+
import com.oracle.graal.pointsto.ObjectScanner.ScanReason;
34+
import com.oracle.svm.core.BuildPhaseProvider;
35+
import com.oracle.svm.core.imagelayer.ImageLayerBuildingSupport;
36+
import com.oracle.svm.core.util.ImageHeapList.HostedImageHeapList;
37+
import com.oracle.svm.core.util.ImageHeapMap.HostedImageHeapMap;
38+
import com.oracle.svm.core.util.LayeredHostedImageHeapMapCollector;
39+
import com.oracle.svm.core.util.LayeredImageHeapMap;
40+
import com.oracle.svm.hosted.FeatureImpl.DuringAnalysisAccessImpl;
41+
import com.oracle.svm.shared.singletons.traits.BuiltinTraits.BuildtimeAccessOnly;
42+
import com.oracle.svm.shared.singletons.traits.BuiltinTraits.NoLayeredCallbacks;
43+
import com.oracle.svm.shared.singletons.traits.SingletonTraits;
44+
import com.oracle.svm.shared.util.VMError;
45+
46+
/**
47+
* Build-time state holder for hosted image-heap collections that need to be rewritten to their
48+
* runtime counterparts and tracked for later rescans. The feature installs this support object as
49+
* the owner of the object replacer so all collection state captured during setup, analysis, and
50+
* image writing stays in one place.
51+
*/
52+
@SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = NoLayeredCallbacks.class)
53+
final class ImageHeapCollectionSupport {
54+
private final Set<HostedImageHeapMap<?, ?>> allMaps = ConcurrentHashMap.newKeySet();
55+
private final Set<HostedImageHeapList<?>> allLists = ConcurrentHashMap.newKeySet();
56+
private final ScanReason scanReason = new OtherReason("Manual rescan triggered from " + ImageHeapCollectionSupport.class);
57+
58+
static ImageHeapCollectionSupport singleton() {
59+
return ImageSingletons.lookup(ImageHeapCollectionSupport.class);
60+
}
61+
62+
/**
63+
* Rewrites hosted image-heap collections to their runtime storage objects and records the
64+
* hosted collections that need later update propagation. During analysis, first reachability of
65+
* a collection registers it for subsequent update/rescan handling; after analysis, encountering
66+
* a collection that was never captured is treated as an invariant violation.
67+
*/
68+
Object replaceHostedWithRuntime(Object obj) {
69+
if (obj instanceof HostedImageHeapMap) {
70+
HostedImageHeapMap<?, ?> hostedImageHeapMap = (HostedImageHeapMap<?, ?>) obj;
71+
if (BuildPhaseProvider.isAnalysisFinished()) {
72+
VMError.guarantee(allMaps.contains(hostedImageHeapMap), "ImageHeapMap reachable after analysis that was not seen during analysis");
73+
} else {
74+
allMaps.add(hostedImageHeapMap);
75+
if (hostedImageHeapMap.getRuntimeMap() instanceof LayeredImageHeapMap<Object, Object> layeredImageHeapMap) {
76+
LayeredHostedImageHeapMapCollector.singleton().registerReachableHostedImageHeapMap(layeredImageHeapMap);
77+
}
78+
}
79+
return hostedImageHeapMap.getRuntimeMap();
80+
} else if (obj instanceof HostedImageHeapList<?> hostedImageHeapList) {
81+
if (BuildPhaseProvider.isAnalysisFinished()) {
82+
VMError.guarantee(allLists.contains(hostedImageHeapList), "ImageHeapList reachable after analysis that was not seen during analysis");
83+
} else {
84+
allLists.add(hostedImageHeapList);
85+
}
86+
return hostedImageHeapList.getRuntimeList();
87+
}
88+
return obj;
89+
}
90+
91+
/**
92+
* Makes sure that the content of all modified
93+
* {@link com.oracle.svm.core.util.ImageHeapMap.HostedImageHeapMap}s and
94+
* {@link com.oracle.svm.core.util.ImageHeapList.HostedImageHeapList}s is properly propagated to
95+
* their runtime counterparts. As both the number of these collections and their individual
96+
* sizes are theoretically unbounded, this method uses <i>parallel streams</i> to divide the
97+
* load across all cores.
98+
* <p>
99+
* The process is split into two stages. First, the content of each modified collection is
100+
* propagated from the hosted to the runtime version. Then, the modified runtime collections are
101+
* rescanned. The split prevents concurrent modifications of the hosted collections during the
102+
* execution of this method, as they may be updated indirectly during the heap scanning.
103+
*/
104+
void updateAndRescanCollections(DuringAnalysisAccessImpl access) {
105+
if (ImageLayerBuildingSupport.buildingExtensionLayer()) {
106+
allMaps.addAll(LayeredHostedImageHeapMapCollector.singleton().getPreviousLayerReachableMaps());
107+
}
108+
Set<Object> objectsToRescan = ConcurrentHashMap.newKeySet();
109+
allMaps.parallelStream().forEach(hostedImageHeapMap -> {
110+
if (hostedImageHeapMap.needsUpdate()) {
111+
hostedImageHeapMap.update();
112+
objectsToRescan.add(hostedImageHeapMap.getCurrentLayerMap());
113+
}
114+
});
115+
allLists.parallelStream().forEach(hostedImageHeapList -> {
116+
if (hostedImageHeapList.needsUpdate()) {
117+
hostedImageHeapList.update();
118+
objectsToRescan.add(hostedImageHeapList.getRuntimeList());
119+
}
120+
});
121+
if (!objectsToRescan.isEmpty()) {
122+
objectsToRescan.parallelStream().forEach(obj -> access.rescanObject(obj, scanReason));
123+
access.requireAnalysisIteration();
124+
}
125+
}
126+
127+
boolean needsUpdate() {
128+
for (var hostedImageHeapMap : allMaps) {
129+
if (hostedImageHeapMap.needsUpdate()) {
130+
return true;
131+
}
132+
}
133+
for (var hostedImageHeapList : allLists) {
134+
if (hostedImageHeapList.needsUpdate()) {
135+
return true;
136+
}
137+
}
138+
return false;
139+
}
140+
141+
void verifyCollectionsAreStableAfterAnalysis() {
142+
for (var hostedImageHeapMap : allMaps) {
143+
if (hostedImageHeapMap.needsUpdate()) {
144+
throw VMError.shouldNotReachHere("ImageHeapMap modified after static analysis:%n%s%n%s",
145+
hostedImageHeapMap, hostedImageHeapMap.getCurrentLayerMap());
146+
}
147+
}
148+
for (var hostedImageHeapList : allLists) {
149+
if (hostedImageHeapList.needsUpdate()) {
150+
throw VMError.shouldNotReachHere("ImageHeapList modified after static analysis:%n%s%n%s",
151+
hostedImageHeapList, hostedImageHeapList.getRuntimeList());
152+
}
153+
}
154+
}
155+
}

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/heap/SVMImageHeapVerifier.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626

2727
import java.util.Map;
2828

29-
import org.graalvm.nativeimage.ImageSingletons;
30-
3129
import com.oracle.graal.pointsto.BigBang;
3230
import com.oracle.graal.pointsto.ObjectScanner;
3331
import com.oracle.graal.pointsto.ObjectScanner.OtherReason;
@@ -61,14 +59,14 @@ public boolean checkHeapSnapshot(UniverseMetaAccess metaAccess, CompletionExecut
6159
* - an image heap map, e.g., via an object replacer like
6260
* com.oracle.svm.enterprise.core.stringformat.StringFormatFeature.collectZeroDigits(). Signal
6361
* this by returning true to make sure that
64-
* com.oracle.graal.pointsto.heap.ImageHeapCollectionFeature.duringAnalysis() is run to properly
65-
* patch all ImageHeapMaps.
62+
* com.oracle.svm.hosted.heap.ImageHeapCollectionSupport.updateAndRescanCollections() is run to
63+
* properly patch all ImageHeapMaps.
6664
*
6765
* - runtime reflection registration.
6866
*
6967
*/
7068
private static boolean imageStateModified() {
71-
return ImageSingletons.lookup(ImageHeapCollectionFeature.class).needsUpdate();
69+
return ImageHeapCollectionSupport.singleton().needsUpdate();
7270
}
7371

7472
@Override

0 commit comments

Comments
 (0)