8000 Optimize Infos for caching in InfoLoader · scala-js/scala-js@5d37597 · GitHub
[go: up one dir, main page]

Skip to content

Commit 5d37597

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 5d37597

File tree

3 files changed

+119
-184
lines changed

3 files changed

+119
-184
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: 51 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,16 @@ private[analyzer] object InfoLoader {
6464
case object InitialIRCheck extends IRCheckMode
6565
case object InternalIRCheck extends IRCheckMode
6666

67+
private type MethodInfos = Array[Map[MethodName, Infos.MethodInfo]]
68+
6769
private class ClassInfoCache(className: ClassName, irLoader: IRLoader, irCheckMode: InfoLoader.IRCheckMode) {
6870
private var cacheUsed: Boolean = false
6971
private var version: Version = Version.Unversioned
7072
private var info: Future[Infos.ClassInfo] = _
7173

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

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

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

121-
val jsMethodProps = {
122-
classDef.jsConstructor.map(jsConstructorInfoCache.getInfo(_)).toList :::
123-
exportedMembersInfoCaches.getInfos(classDef.jsMethodProps)
124-
}
122+
prevMethodInfos = genMethodInfos(classDef.methods, prevMethodInfos)
123+
prevJSCtorInfo = genJSCtorInfo(classDef.jsConstructor, prevJSCtorInfo)
124+
prevJSMethodPropDefInfos =
125+
genJSMethodPropDefInfos(classDef.jsMethodProps, prevJSMethodPropDefInfos)
126+
127+
val exportedMembers = prevJSCtorInfo.toList ::: prevJSMethodPropDefInfos
125128

126129
/* We do not cache top-level exports, because they're quite rare,
127130
* and usually quite small when they exist.
@@ -134,139 +137,66 @@ private[analyzer] object InfoLoader {
134137

135138
new Infos.ClassInfo(classDef.className, classDef.kind,
136139
classDef.superClass.map(_.name), classDef.interfaces.map(_.name),
137-
classDef.jsNativeLoadSpec, referencedFieldClasses, methods, jsNativeMembers,
138-
jsMethodProps, topLevelExports)
140+
classDef.jsNativeLoadSpec, referencedFieldClasses, prevMethodInfos,
141+
jsNativeMembers, exportedMembers, topLevelExports)
139142
}
140143

141144
/** Returns true if the cache has been used and should be kept. */
142145
def cleanAfterRun(): Boolean = synchronized {
143146
val result = cacheUsed
144147
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-
}
151148
result
152149
}
153150
}
154151

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)
163-
}
152+
private def genMethodInfos(methods: List[MethodDef],
153+
prevMethodInfos: MethodInfos): MethodInfos = {
164154

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-
}
155+
val builders = Array.fill(MemberNamespace.Count)(Map.newBuilder[MethodName, Infos.MethodInfo])
176156

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)
198-
199-
for ((member, i) <- members.zipWithIndex) yield {
200-
caches(i).getInfo(member)
201-
}
202-
}
203-
}
157+
methods.foreach { method =>
158+
val info = prevMethodInfos(method.flags.namespace.ordinal)
159+
.get(method.methodName)
160+
.filter(_.version.sameVersion(method.version))
161+
.getOrElse(Infos.generateMethodInfo(method))
204162

205-
def cleanAfterRun(): Unit = {
206-
if (caches != null)
207-
caches.foreach(_.cleanAfterRun())
163+
builders(method.flags.namespace.ordinal) += method.methodName -> info
208164
}
209-
}
210165

211-
private object JSMethodPropDefsInfosCache {
212-
def apply(): JSMethodPropDefsInfosCache =
213-
new JSMethodPropDefsInfosCache(null)
166+
builders.map(_.result())
214167
}
215168

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-
}
225-
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
233-
}
234-
}
235-
}
236-
237-
protected def computeInfo(member: Def): Info
238-
239-
/** Returns true if the cache has been used and should be kept. */
240-
final def cleanAfterRun(): Boolean = {
241-
val result = cacheUsed
242-
cacheUsed = false
243-
result
169+
private def genJSCtorInfo(jsCtor: Option[JSConstructorDef],
170+
prevJSCtorInfo: Option[Infos.ReachabilityInfo]): Option[Infos.ReachabilityInfo] = {
171+
jsCtor.map { ctor =>
172+
prevJSCtorInfo
173+
.filter(_.version.sameVersion(ctor.version))
174+
.getOrElse(Infos.generateJSConstructorInfo(ctor))
244175
}
245176
}
246177

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)
178+
private def genJSMethodPropDefInfos(jsMethodProps: List[JSMethodPropDef],
179+
prevJSMethodPropDefInfos: List[Infos.ReachabilityInfo]): List[Infos.ReachabilityInfo] = {
180+
/* For JS method and property definitions, we use their index in the list of
181+
* `linkedClass.exportedMembers` as their identity. We cannot use their name
182+
* because the name itself is a `Tree`.
183+
*
184+
* If there is a different number of exported members than in a previous run,
185+
* we always recompute everything. This is fine because, for any given class,
186+
* either all JS methods and properties are reachable, or none are. So we're
187+
* only missing opportunities for incrementality in the case where JS members
188+
* are added or removed in the original .sjsir, which is not a big deal.
189+
*/
190+
191+
if (prevJSMethodPropDefInfos.size != jsMethodProps.size) {
192+
// Regenerate everything.
193+
jsMethodProps.map(Infos.generateJSMethodPropDefInfo(_))
194+
} else {
195+
for {
196+
(prevInfo, member) <- prevJSMethodPropDefInfos.zip(jsMethodProps)
197+
} yield {
198+
if (prevInfo.version.sameVersion(member.version)) prevInfo
199+
else Infos.generateJSMethodPropDefInfo(member)
270200
}
271201
}
272202
}

0 commit comments

Comments
 (0)
0