Skip to content

Commit b172922

Browse files
TheElectronWillsmarter
authored andcommitted
Improve Invoker performance with a BitSet
According to my JMH benchmarks, this improves the performance by at least 40%, even when multiple threads use the Invoker. Details on the benchmark: - 10k calls to Invoker.invoked(id,dir) with a % of ids that are repeted in a loop and a % of ids that appear only once. But this doesn't change the results much. - 1 thread or 4 threads at the same time. The single-thread performance is, as expected, much better (3x).
1 parent a4549e7 commit b172922

File tree

1 file changed

+20
-43
lines changed

1 file changed

+20
-43
lines changed

library/src/scala/runtime/Invoker.scala

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,17 @@
11
package scala.runtime
22

3-
import scala.collection.mutable
4-
import java.nio.file.Path
3+
import scala.collection.mutable.{BitSet, AnyRefMap}
54
import scala.collection.concurrent.TrieMap
6-
import java.nio.file.Paths
75
import java.nio.file.Files
8-
import java.nio.file.StandardOpenOption
96
import java.io.FileWriter
107
import java.io.File
118

129
object Invoker {
1310
private val runtimeUUID = java.util.UUID.randomUUID()
1411

1512
private val MeasurementsPrefix = "scoverage.measurements."
16-
private val threadFiles = new ThreadLocal[mutable.HashMap[String, FileWriter]]
17-
18-
// For each data directory we maintain a thread-safe set tracking the ids
19-
// that we've already seen and recorded. We're using a map as a set, so we
20-
// only care about its keys and can ignore its values.
21-
private val dataDirToIds = TrieMap.empty[String, TrieMap[Int, Any]]
13+
private val threadFiles = new ThreadLocal[AnyRefMap[String, FileWriter]]
14+
private val dataDirToSet = TrieMap.empty[String, BitSet]
2215

2316
/** We record that the given id has been invoked by appending its id to the coverage data file.
2417
*
@@ -34,40 +27,24 @@ object Invoker {
3427
* @param dataDir
3528
* the directory where the measurement data is held
3629
*/
37-
def invoked(id: Int, dataDir: String): Unit = {
38-
// [sam] we can do this simple check to save writing out to a file.
39-
// This won't work across JVMs but since there's no harm in writing out the same id multiple
40-
// times since for coverage we only care about 1 or more, (it just slows things down to
41-
// do it more than once), anything we can do to help is good. This helps especially with code
42-
// that is executed many times quickly, eg tight loops.
43-
if (!dataDirToIds.contains(dataDir)) {
44-
// Guard against SI-7943: "TrieMap method getOrElseUpdate is not thread-safe".
45-
dataDirToIds.synchronized {
46-
if (!dataDirToIds.contains(dataDir)) {
47-
dataDirToIds(dataDir) = TrieMap.empty[Int, Any]
48-
}
49-
}
50-
}
51-
val ids = dataDirToIds(dataDir)
52-
if (!ids.contains(id)) {
53-
// Each thread writes to a separate measurement file, to reduce contention
54-
// and because file appends via FileWriter are not atomic on Windows.
55-
var files = threadFiles.get()
56-
if (files == null) {
57-
files = mutable.HashMap.empty[String, FileWriter]
58-
threadFiles.set(files)
30+
def invoked(id: Int, dataDir: String): Unit =
31+
val set = dataDirToSet.getOrElseUpdate(dataDir, BitSet.empty)
32+
if !set.contains(id) then
33+
val added = set.synchronized {
34+
set.add(id)
5935
}
60-
val writer = files.getOrElseUpdate(
61-
dataDir,
62-
new FileWriter(measurementFile(dataDir), true)
63-
)
64-
65-
writer.append(Integer.toString(id))
66-
writer.append("\n")
67-
writer.flush()
68-
ids.put(id, ())
69-
}
70-
}
36+
if added then
37+
var writers = threadFiles.get()
38+
if writers == null then
39+
writers = AnyRefMap.empty
40+
threadFiles.set(writers)
41+
val writer = writers.getOrElseUpdate(
42+
dataDir,
43+
FileWriter(measurementFile(dataDir), true)
44+
)
45+
writer.write(Integer.toString(id))
46+
writer.write('\n')
47+
writer.flush()
7148

7249
def measurementFile(dataDir: String): File = new File(
7350
dataDir,

0 commit comments

Comments
 (0)