summaryrefslogtreecommitdiff
path: root/test/files/presentation/memory-leaks/MemoryLeaksTest.scala
blob: f09c6f8e2c794fe3e960ec66e01a0bbd2df77c65 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import java.io.PrintWriter
import java.io.FileOutputStream
import java.util.Calendar

import scala.reflect.internal.util.BatchSourceFile
import scala.tools.nsc.interactive
import scala.tools.nsc.interactive.tests._
import scala.tools.nsc.io._
import scala.tools.nsc.doc

/** This test runs the presentation compiler on the Scala compiler project itself and records memory consumption.
 *
 * The test scenario is to open Typers, Trees and Types, then repeatedly add and remove one character
 * in Typers.scala. Each step causes the parser, namer, and type checker to run.
 *
 * At each step we record the memory usage after the GC has run. At the end of the test,
 * simple linear regression is used to compute the straight line that best fits the
 * curve, and if the slope is higher than 1 (meaning a leak of 1MB/run), we fail the test.
 *
 * The Scala compiler sources are assumed to be under 'basedir/src/compiler'.
 *
 * The individual data points are saved under 'usedMem-<date>.txt', under the test project
 * directory. Use the cool graph-it.R (https://github.com/scala-ide/scala-ide/blob/master/org.scala-ide.sdt.core.tests/graph-it.R)
 *  script to see the memory curve for the given test run.
 */
object Test extends InteractiveTest {
  final val mega = 1024 * 1024

  import interactive.Global
  trait InteractiveScaladocAnalyzer extends interactive.InteractiveAnalyzer with doc.ScaladocAnalyzer {
    val global : Global
    override def newTyper(context: Context) = new Typer(context) with InteractiveTyper with ScaladocTyper {
      override def canAdaptConstantTypeToLiteral = false
    }
  }

  private class ScaladocEnabledGlobal extends Global(settings, compilerReporter) {
    override lazy val analyzer = new {
      val global: ScaladocEnabledGlobal.this.type = ScaladocEnabledGlobal.this
    } with InteractiveScaladocAnalyzer
  }

  override def createGlobal: Global = new ScaladocEnabledGlobal

  override def execute(): Unit = memoryConsumptionTest()

  def batchSource(name: String) =
    new BatchSourceFile(AbstractFile.getFile(name))

  def memoryConsumptionTest() {
    val N = 50
    val filename = "usedmem-%tF.txt".format(Calendar.getInstance.getTime)

    val typerUnit = AbstractFile.getFile(baseDir.parent.parent.parent.parent / "src/compiler/scala/tools/nsc/typechecker/Typers.scala")
    val typesUnit = AbstractFile.getFile(baseDir.parent.parent.parent.parent / "src/reflect/scala/reflect/internal/Types.scala")
    val treesUnit = AbstractFile.getFile(baseDir.parent.parent.parent.parent / "src/reflect/scala/reflect/internal/Trees.scala")

    askReload(Seq(new BatchSourceFile(typerUnit), new BatchSourceFile(typesUnit), new BatchSourceFile(treesUnit)))
    typeCheckWith(treesUnit, new String(treesUnit.toCharArray))
    typeCheckWith(typesUnit, new String(typesUnit.toCharArray))

    val originalTyper = new String(typerUnit.toCharArray)

    val (prefix, postfix) = originalTyper.splitAt(originalTyper.indexOf("import global._"))
    val changedTyper = prefix + " a\n " + postfix

    val usedMem = for (i <- 1 to N) yield {
      val src = if (i % 2 == 0) originalTyper else changedTyper

      val usedMem = withGC {
        typeCheckWith(typerUnit, src)
      }

      usedMem / mega // report size in MB
    }

    //dumpDataToFile(filename, usedMem)
    // drop the first two measurements, since the compiler needs some memory when initializing
    val (a, b) = linearModel((3L to N).toSeq, usedMem.drop(2))
    //println("LinearModel: constant: %.4f\tslope:%.4f".format(a, b))

    if (b > 1.0)
      println("Rate of memory consumption is alarming! %.4f MB/run".format(b))
    else
      println("No leaks detected.")
  }

  private def typeCheckWith(file: AbstractFile, src: String) = {
    val sourceFile = new BatchSourceFile(file, src.toCharArray)
    askReload(Seq(sourceFile))
    askLoadedTyped(sourceFile).get // block until it's here
  }

  private def dumpDataToFile(filename: String, usedMem: Seq[Long]) {
    val outputFile = new PrintWriter(new FileOutputStream(filename))
    outputFile.println("\tusedMem")
    for ((dataPoint, i) <- usedMem.zipWithIndex) {
      outputFile.println("%d\t%d".format(i, dataPoint))
    }
    outputFile.close()
  }


  /** Return the linear model of these values, (a, b). First value is the constant factor,
   *  second value is the slope, i.e. `y = a + bx`
   *
   *  The linear model of a set of points is a straight line that minimizes the square distance
   *  between the each point and the line.
   *
   *  See: http://en.wikipedia.org/wiki/Simple_linear_regression
   */
  def linearModel(xs: Seq[Long], ys: Seq[Long]): (Double, Double) = {
    require(xs.length == ys.length)

    def mean(v: Seq[Long]): Double = v.sum.toDouble / v.length

    val meanXs = mean(xs)
    val meanYs = mean(ys)

    val beta = (mean((xs, ys).zipped.map(_ * _)) - meanXs * meanYs) / (mean(xs.map(x => x * x)) - meanXs * meanXs)
    val alfa = meanYs - beta * meanXs

    (alfa, beta)
  }

  /** Run the given closure and return the amount of used memory at the end of its execution.
   *
   *  Runs the GC before and after the execution of `f'.
   */
  def withGC(f: => Unit): Long = {
    val r = Runtime.getRuntime
    System.gc()

    f;

    System.gc()

    r.totalMemory() - r.freeMemory()
  }

}