summaryrefslogtreecommitdiff
path: root/test/files/presentation/memory-leaks/MemoryLeaksTest.scala
blob: 857beac7df853b38394ce224476e1bda32ef7ab3 (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
import java.io.PrintWriter
import java.io.FileOutputStream
import java.util.Calendar

import scala.tools.nsc.interactive.tests._
import scala.tools.nsc.util._
import scala.tools.nsc.io._

/** 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

  override def main(args: Array[String]) {
    memoryConsumptionTest()
    compiler.askShutdown()
  }

  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()
  }

}