E57A Optimize Infos for caching in InfoLoader · scala-js/scala-js@63444a1 · GitHub
[go: up one dir, main page]

Skip to content

Commit 63444a1

Browse files
committed
Optimize Infos for caching in InfoLoader
This allows us to use the previously calculated infos themselves as the cached values, reducing memory consumption. Further, we reduce memory consumption by using immutable maps: Because a lot of the maps are empty, we do not spend unecessary memory on empty mutable maps. This reduces the retained size on the test suite for the infos as follows: | Component | Before [MB] | After [MB] | |------------|------------:|-----------:| | BaseLinker | 20 | 15 | | Refiner | 17 | 13 | As a nice side-effect, this allows us to simplify the InfoLoader. The simplification mainly stems from the insight, that we do not need active cache used tracking; control flow is sufficient. Execution times are unaffected, in fact, the incremental case might even be a bit faster (non-significant though).
1 parent caa3e47 commit 63444a1

File tree

3 files changed

+113
-187
lines changed

3 files changed

+113
-187
lines changed

linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -677,16 +677,15 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
677677
*/
678678
private val _instantiatedSubclasses = new GrowingList[ClassInfo]
679679

680-
private val nsMethodInfos = {
681-
val nsMethodInfos = Array.fill(MemberNamespace.Count) {
682-
emptyThreadSafeMap[MethodName, MethodInfo]
683-
}
684-
for (methodData <- data.methods) {
685-
// TODO It would be good to report duplicates as errors at this point
686-
val relevantMap = nsMethodInfos(methodData.namespace.ordinal)
687-
relevantMap(methodData.methodName) = new MethodInfo(this, methodData)
688-
}
689-
nsMethodInfos
680+
private val nsMethodInfos = Array.tabulate(MemberNamespace.Count) { nsOrdinal =>
681+
val namespace = MemberNamespace.fromOrdinal(nsOrdinal)
682+
683+
val m = emptyThreadSafeMap[MethodName, MethodInfo]
684+
685+
for ((name, data) <- data.methods(nsOrdinal))
686+
m.put(name, new MethodInfo(this, namespace, name, data))
687+
688+
m
690689
}
691690

692691
def methodInfos(
@@ -724,8 +723,8 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
724723
* method exists.
725724
*/
726725
publicMethodInfos.getOrElseUpdate(methodName, {
727-
val syntheticData = makeSyntheticMethodInfo(methodName, MemberNamespace.Public)
728-
new MethodInfo(this, syntheticData, nonExistent = true)
726+
val syntheticData = makeSyntheticMethodInfo()
727+
new MethodInfo(this, MemberNamespace.Public, methodName, syntheticData, nonExistent = true)
729728
})
730729
}
731730

@@ -812,11 +811,9 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
812811
val targetOwner = target.owner
813812

814813
val syntheticInfo = makeSyntheticMethodInfo(
815-
methodName = methodName,
816-
namespace = MemberNamespace.Public,
817814
methodsCalledStatically = List(
818815
targetOwner.className -> NamespacedMethodName(MemberNamespace.Public, methodName)))
819-
new MethodInfo(this, syntheticInfo,
816+
new MethodInfo(this, MemberNamespace.Public, methodName, syntheticInfo,
820817
syntheticKind = MethodSyntheticKind.DefaultBridge(targetOwner.className))
821818
})
822819
}
@@ -1011,10 +1008,8 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
10111008

10121009
publicMethodInfos.getOrElseUpdate(proxyName, {
10131010
val syntheticInfo = makeSyntheticMethodInfo(
1014-
methodName = proxyName,
1015-
namespace = MemberNamespace.Public,
10161011
methodsCalled = List(this.className -> targetName))
1017-
new MethodInfo(this, syntheticInfo,
1012+
new MethodInfo(this, MemberNamespace.Public, proxyName, syntheticInfo,
10181013
syntheticKind = MethodSyntheticKind.ReflectiveProxy(targetName))
10191014
})
10201015
}
@@ -1024,8 +1019,8 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
10241019
assert(namespace != MemberNamespace.Public)
10251020

10261021
methodInfos(namespace).getOrElseUpdate(methodName, {
1027-
val syntheticData = makeSyntheticMethodInfo(methodName, namespace)
1028-
new MethodInfo(this, syntheticData, nonExistent = true)
1022+
val syntheticData = makeSyntheticMethodInfo()
1023+
new MethodInfo(this, namespace, methodName, syntheticData, nonExistent = true)
10291024
})
10301025
}
10311026

@@ -1273,13 +1268,13 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
12731268

12741269
private class MethodInfo(
12751270
val owner: ClassInfo,
1271+
val namespace: MemberNamespace,
1272+
val methodName: MethodName,
12761273
data: Infos.MethodInfo,
12771274
val nonExistent: Boolean = false,
12781275
val syntheticKind: MethodSyntheticKind = MethodSyntheticKind.None
12791276
) extends Analysis.MethodInfo {
12801277

1281-
val methodName = data.methodName
1282-
val namespace = data.namespace
12831278
val isAbstract = data.isAbstract
12841279

12851280
private[this] val _isAbstractReachable = new AtomicBoolean(false)
@@ -1357,7 +1352,7 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
13571352
}
13581353

13591354
private[this] def doReach(): Unit =
1360-
followReachabilityInfo(data.reachabilityInfo, owner)(FromMethod(this))
1355+
followReachabilityInfo(data, owner)(FromMethod(this))
13611356
}
13621357

13631358
private class TopLevelExportInfo(val owningClass: ClassName, data: Infos.TopLevelExportInfo)
@@ -1507,8 +1502,12 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
15071502
if (className == ObjectClass) None
15081503
else Some(ObjectClass)
15091504

1510-
val methods =
1511-
List(makeSyntheticMethodInfo(NoArgConstructorName, MemberNamespace.Constructor))
1505+
val methods = Array.tabulate[Map[MethodName, Infos.MethodInfo]](MemberNamespace.Count) { nsOrdinal =>
1506+
if (nsOrdinal == MemberNamespace.Constructor.ordinal)
1507+
Map(NoArgConstructorName -> makeSyntheticMethodInfo())
1508+
else
1509+
Map.empty
1510+
}
15121511

15131512
new Infos.ClassInfo(className, ClassKind.Class,
15141513
superClass = superClass, interfaces = Nil, jsNativeLoadSpec = None,
@@ -1517,19 +1516,17 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
15171516
}
15181517

15191518
private def makeSyntheticMethodInfo(
1520-
methodName: MethodName,
1521-
namespace: MemberNamespace,
15221519
methodsCalled: List[(ClassName, MethodName)] = Nil,
15231520
methodsCalledStatically: List[(ClassName, NamespacedMethodName)] = Nil,
15241521
instantiatedClasses: List[ClassName] = Nil
15251522
): Infos.MethodInfo = {
1526-
val reachabilityInfoBuilder = new Infos.ReachabilityInfoBuilder()
1523+
val reachabilityInfoBuilder = new Infos.ReachabilityInfoBuilder(ir.Version.Unversioned)
1524+
15271525
for ((className, methodName) <- methodsCalled)
15281526
reachabilityInfoBuilder.addMethodCalled(className, methodName)
15291527
for ((className, methodName) <- methodsCalledStatically)
15301528
reachabilityInfoBuilder.addMethodCalledStatically(className, methodName)
1531-
Infos.MethodInfo(methodName, namespace, isAbstract = false,
1532-
reachabilityInfoBuilder.result())
1529+
Infos.MethodInfo(isAbstract = false, reachabilityInfoBuilder.result())
15331530
}
15341531

15351532
}

linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala

Lines changed: 45 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ private[analyzer] object InfoLoader {
6969
private var version: Version = Version.Unversioned
7070
private var info: Future[Infos.ClassInfo] = _
7171

72-
private val methodsInfoCaches = MethodDefsInfosCache()
73-
private val jsConstructorInfoCache = new JSConstructorDefInfoCache()
74-
private val exportedMembersInfoCaches = JSMethodPropDefsInfosCache()
72+
private var prevMethodInfos = Array.fill(MemberNamespace.Count)(Map.empty[MethodName, Infos.MethodInfo])
73+
private var prevJSCtorInfo: Option[Infos.ReachabilityInfo] = None
74+
private var prevJSMethodPropDefInfos: List[Infos.ReachabilityInfo] = Nil
7575

7676
def loadInfo(logger: Logger)(implicit ec: ExecutionContext): Future[Infos.ClassInfo] = synchronized {
7777
/* If the cache was already used in this run, the classDef and info are
@@ -116,13 +116,19 @@ private[analyzer] object InfoLoader {
116116

117117
private def generateInfos(classDef: ClassDef): Infos.ClassInfo = {
118118
val referencedFieldClasses = Infos.genReferencedFieldClasses(classDef.fields)
119-
val methods = classDef.methods.map(methodsInfoCaches.getInfo(_))
120119

121-
val jsMethodProps = {
122-
classDef.jsConstructor.map(jsConstructorInfoCache.getInfo(_)).toList :::
123-
exportedMembersInfoCaches.getInfos(classDef.jsMethodProps)
120+
prevMethodInfos = genMethodInfos(classDef.methods)
121+
122+
prevJSCtorInfo = classDef.jsConstructor.map { ctor =>
123+
prevJSCtorInfo
124+
.filter(_.version.sameVersion(ctor.version))
125+
.getOrElse(Infos.generateJSConstructorInfo(ctor))
124126
}
125127

128+
prevJSMethodPropDefInfos = genJSMethodPropDefInfos(classDef.jsMethodProps)
129+
130+
val exportedMembers = prevJSCtorInfo.toList ::: prevJSMethodPropDefInfos
131+
126132
/* We do not cache top-level exports, because they're quite rare,
127133
* and usually quite small when they exist.
128134
*/
@@ -134,140 +140,55 @@ private[analyzer] object InfoLoader {
134140

135141
new Infos.ClassInfo(classDef.className, classDef.kind,
136142
classDef.superClass.map(_.name), classDef.interfaces.map(_.name),
137-
classDef.jsNativeLoadSpec, referencedFieldClasses, methods, jsNativeMembers,
138-
jsMethodProps, topLevelExports)
139-
}
140-
141-
/** Returns true if the cache has been used and should be kept. */
142-
def cleanAfterRun(): Boolean = synchronized {
143-
val result = cacheUsed
144-
cacheUsed = false
145-
if (result) {
146-
// No point in cleaning the inner caches if the whole class disappears
147-
methodsInfoCaches.cleanAfterRun()
148-
jsConstructorInfoCache.cleanAfterRun()
149-
exportedMembersInfoCaches.cleanAfterRun()
150-
}
151-
result
152-
}
153-
}
154-
155-
private final class MethodDefsInfosCache private (
156-
val caches: Array[mutable.Map[MethodName, MethodDefInfoCache]])
157-
extends AnyVal {
158-
159-
def getInfo(methodDef: MethodDef): Infos.MethodInfo = {
160-
val cache = caches(methodDef.flags.namespace.ordinal)
161-
.getOrElseUpdate(methodDef.methodName, new MethodDefInfoCache)
162-
cache.getInfo(methodDef)
143+
classDef.jsNativeLoadSpec, referencedFieldClasses, prevMethodInfos,
144+
jsNativeMembers, exportedMembers, topLevelExports)
163145
}
164146

165-
def cleanAfterRun(): Unit = {
166-
caches.foreach(_.filterInPlace((_, cache) => cache.cleanAfterRun()))
167-
}
168-
}
169-
170-
private object MethodDefsInfosCache {
171-
def apply(): MethodDefsInfosCache = {
172-
new MethodDefsInfosCache(
173-
Array.fill(MemberNamespace.Count)(mutable.Map.empty))
174-
}
175-
}
147+
private def genMethodInfos(methods: List[MethodDef]) = {
148+
val builders = Array.fill(MemberNamespace.Count)(Map.newBuilder[MethodName, Infos.MethodInfo])
176149

177-
/* For JS method and property definitions, we use their index in the list of
178-
* `linkedClass.exportedMembers` as their identity. We cannot use their name
179-
* because the name itself is a `Tree`.
180-
*
181-
* If there is a different number of exported members than in a previous run,
182-
* we always recompute everything. This is fine because, for any given class,
183-
* either all JS methods and properties are reachable, or none are. So we're
184-
* only missing opportunities for incrementality in the case where JS members
185-
* are added or removed in the original .sjsir, which is not a big deal.
186-
*/
187-
private final class JSMethodPropDefsInfosCache private (
188-
private var caches: Array[JSMethodPropDefInfoCache]) {
189-
190-
def getInfos(members: List[JSMethodPropDef]): List[Infos.ReachabilityInfo] = {
191-
if (members.isEmpty) {
192-
caches = null
193-
Nil
194-
} else {
195-
val membersSize = members.size
196-
if (caches == null || membersSize != caches.size)
197-
caches = Array.fill(membersSize)(new JSMethodPropDefInfoCache)
150+
methods.foreach { method =>
151+
val info = prevMethodInfos(method.flags.namespace.ordinal)
152+
.get(method.methodName)
153+
.filter(_.version.sameVersion(method.version))
154+
.getOrElse(Infos.generateMethodInfo(method))
198155

199-
for ((member, i) <- members.zipWithIndex) yield {
200-
caches(i).getInfo(member)
201-
}
156+
builders(method.flags.namespace.ordinal) += method.methodName -> info
202157
}
203-
}
204158

205-
def cleanAfterRun(): Unit = {
206-
if (caches != null)
207-
caches.foreach(_.cleanAfterRun())
159+
builders.map(_.result())
208160
}
209-
}
210161

211-
private object JSMethodPropDefsInfosCache {
212-
def apply(): JSMethodPropDefsInfosCache =
213-
new JSMethodPropDefsInfosCache(null)
214-
}
215-
216-
private abstract class AbstractMemberInfoCache[Def <: VersionedMemberDef, Info] {
217-
private var cacheUsed: Boolean = false
218-
private var lastVersion: Version = Version.Unversioned
219-
private var info: Info = _
220-
221-
final def getInfo(member: Def): Info = {
222-
update(member)
223-
info
224-
}
162+
private def genJSMethodPropDefInfos(jsMethodProps: List[JSMethodPropDef]) = {
163+
/* For JS method and property definitions, we use their index in the list of
164+
* `linkedClass.exportedMembers` as their identity. We cannot use their name
165+
* because the name itself is a `Tree`.
166+
*
167+
* If there is a different number of exported members than in a previous run,
168+
* we always recompute everything. This is fine because, for any given class,
169+
* either all JS methods and properties are reachable, or none are. So we're
170+
* only missing opportunities for incrementality in the case where JS members
171+
* are added or removed in the original .sjsir, which is not a big deal.
172+
*/
225173

226-
private final def update(member: Def): Unit = {
227-
if (!cacheUsed) {
228-
cacheUsed = true
229-
val newVersion = member.version
230-
if (!lastVersion.sameVersion(newVersion)) {
231-
info = computeInfo(member)
232-
lastVersion = newVersion
174+
if (prevJSMethodPropDefInfos.size != jsMethodProps.size) {
175+
// Regenerate everything.
176+
jsMethodProps.map(Infos.generateJSMethodPropDefInfo(_))
177+
} else {
178+
for {
179+
(prevInfo, member) <- prevJSMethodPropDefInfos.zip(jsMethodProps)
180+
} yield {
181+
if (prevInfo.version.sameVersion(member.version)) prevInfo
182+
else Infos.generateJSMethodPropDefInfo(member)
233183
}
234184
}
235185
}
236186

237-
protected def computeInfo(member: Def): Info
238-
239187
/** Returns true if the cache has been used and should be kept. */
240-
final def cleanAfterRun(): Boolean = {
188+
def cleanAfterRun(): Boolean = synchronized {
241189
val result = cacheUsed
242190
cacheUsed = false
243191
result
244192
}
245193
}
246-
247-
private final class MethodDefInfoCache
248-
extends AbstractMemberInfoCache[MethodDef, Infos.MethodInfo] {
249-
250-
protected def computeInfo(member: MethodDef): Infos.MethodInfo =
251-
Infos.generateMethodInfo(member)
252-
}
253-
254-
private final class JSConstructorDefInfoCache
255-
extends AbstractMemberInfoCache[JSConstructorDef, Infos.ReachabilityInfo] {
256-
257-
protected def computeInfo(member: JSConstructorDef): Infos.ReachabilityInfo =
258-
Infos.generateJSConstructorInfo(member)
259-
}
260-
261-
private final class JSMethodPropDefInfoCache
262-
extends AbstractMemberInfoCache[JSMethodPropDef, Infos.ReachabilityInfo] {
263-
264-
protected def computeInfo(member: JSMethodPropDef): Infos.ReachabilityInfo = {
265-
member match {
266-
case methodDef: JSMethodDef =>
267-
Infos.generateJSMethodInfo(methodDef)
268-
case propertyDef: JSPropertyDef =>
269-
Infos.generateJSPropertyInfo(propertyDef)
270-
}
271-
}
272-
}
273194
}

0 commit comments

Comments
 (0)
0