Skip to content

Commit bf9ae99

Browse files
committed
Merge pull request #119 from DarkDimius/leaks
Context escape detection.
2 parents a782ada + b88b79d commit bf9ae99

File tree

7 files changed

+168
-11
lines changed

7 files changed

+168
-11
lines changed

project/Build.scala

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ object DottyBuild extends Build {
2121
resourceDirectory in Compile := baseDirectory.value / "resources",
2222
unmanagedSourceDirectories in Compile := Seq((scalaSource in Compile).value),
2323
unmanagedSourceDirectories in Test := Seq((scalaSource in Test).value),
24-
24+
2525
// include sources in eclipse (downloads source code for all dependencies)
2626
//http://stackoverflow.com/questions/10472840/how-to-attach-sources-to-sbt-managed-dependencies-in-scala-ide#answer-11683728
2727
com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys.withSource := true,
@@ -34,17 +34,21 @@ object DottyBuild extends Build {
3434
"org.scala-lang.modules" %% "scala-xml" % "1.0.1"),
3535

3636
// get junit onboard
37-
libraryDependencies += "com.novocode" % "junit-interface" % "0.9" % "test",
37+
libraryDependencies += "com.novocode" % "junit-interface" % "0.11-RC1" % "test",
3838

3939
// scalac options
4040
scalacOptions in Global ++= Seq("-feature", "-deprecation", "-language:_"),
4141

42+
javacOptions ++= Seq("-Xlint:unchecked", "-Xlint:deprecation"),
43+
4244
// enable verbose exception messages for JUnit
43-
testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-v"),
45+
testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-v", "--run-listener=test.ContextEscapeDetector"),
4446
// Adjust classpath for running dotty
4547
mainClass in (Compile, run) := Some("dotty.tools.dotc.Main"),
4648
fork in run := true,
4749
fork in Test := true,
50+
parallelExecution in Test := false,
51+
4852
// http://grokbase.com/t/gg/simple-build-tool/135ke5y90p/sbt-setting-jvm-boot-paramaters-for-scala
4953
javaOptions <++= (managedClasspath in Runtime, packageBin in Compile) map { (attList, bin) =>
5054
// put the Scala {library, reflect, compiler} in the classpath

src/dotty/tools/dotc/Bench.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ object Bench extends Driver {
4646
else (args(pos + 1).toInt, (args take pos) ++ (args drop (pos + 2)))
4747
}
4848

49-
override def process(args: Array[String]): Reporter = {
49+
override def process(args: Array[String], rootCtx: Context): Reporter = {
5050
val (numCompilers, args1) = extractNumArg(args, "#compilers")
5151
val (numRuns, args2) = extractNumArg(args1, "#runs")
5252
this.numRuns = numRuns
53-
ntimes(numCompilers)(super.process(args2))
53+
ntimes(numCompilers)(super.process(args2, rootCtx))
5454
}
5555
}
5656

src/dotty/tools/dotc/Driver.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ abstract class Driver extends DotClass {
2222

2323
protected def initCtx = (new ContextBase).initialCtx
2424

25-
def process(args: Array[String]): Reporter = {
26-
val summary = CompilerCommand.distill(args)(initCtx)
25+
def process(args: Array[String], rootCtx: Context): Reporter = {
26+
val summary = CompilerCommand.distill(args)(rootCtx)
2727
implicit val ctx: Context = initCtx.fresh.setSettings(summary.sstate)
2828
val fileNames = CompilerCommand.checkUsage(summary)
2929
try {
@@ -41,7 +41,7 @@ abstract class Driver extends DotClass {
4141
}
4242

4343
def main(args: Array[String]): Unit =
44-
sys.exit(if (process(args).hasErrors) 1 else 0)
44+
sys.exit(if (process(args, initCtx).hasErrors) 1 else 0)
4545
}
4646

4747
class FatalError(msg: String) extends Exception

test/test/CompilerTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class CompilerTest extends DottyTest {
1313
def compileArgs(args: Array[String], xerrors: Int = 0): Unit = {
1414
val allArgs = args ++ defaultOptions
1515
val processor = if (allArgs.exists(_.startsWith("#"))) Bench else Main
16-
val nerrors = processor.process(allArgs).count(Reporter.ERROR.level)
16+
val nerrors = processor.process(allArgs, ctx).count(Reporter.ERROR.level)
1717
assert(nerrors == xerrors, s"Wrong # of errors. Expected: $xerrors, found: $nerrors")
1818
}
1919

test/test/ContextEscapeDetection.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package test;
2+
3+
import dotty.tools.dotc.core.Contexts;
4+
import org.junit.*;
5+
6+
import java.lang.ref.WeakReference;
7+
import java.util.LinkedList;
8+
import java.util.List;
9+
10+
11+
public abstract class ContextEscapeDetection {
12+
public static class TestContext{
13+
public TestContext(WeakReference<Contexts.Context> context, String testName) {
14+
this.context = context;
15+
this.testName = testName;
16+
}
17+
18+
public final WeakReference<Contexts.Context> context;
19+
public final String testName;
20+
21+
}
22+
public static final List<TestContext> contexts = new LinkedList<>();
23+
24+
public abstract Contexts.Context getCtx();
25+
26+
public abstract void clearCtx();
27+
28+
@Before
29+
public synchronized void stealContext() {
30+
contexts.add(new TestContext(new WeakReference<>(this.getCtx()), this.getClass().getName()));
31+
}
32+
33+
@After
34+
public synchronized void clearContext() {
35+
this.clearCtx();
36+
}
37+
38+
39+
}
40+

test/test/ContextEscapeDetector.java

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package test;
2+
3+
import org.junit.runner.Description;
4+
import org.junit.runner.Result;
5+
import org.junit.runner.notification.RunListener;
6+
import org.junit.Assert;
7+
8+
import java.lang.ref.WeakReference;
9+
10+
public class ContextEscapeDetector extends RunListener {
11+
12+
//context can be captured by objects, eg NoDenotation
13+
public static final int CONTEXTS_ALLOWED = 1;
14+
15+
@Override
16+
public void testRunFinished(Result result) throws Exception {
17+
if (contextsAlive() > CONTEXTS_ALLOWED) {
18+
forceGCHeuristic0();
19+
if (contextsAlive() > CONTEXTS_ALLOWED) {
20+
forceGCHeuristic1();
21+
if (contextsAlive() > CONTEXTS_ALLOWED) {
22+
forceGCHeuristic2();
23+
forceGCHeuristic1();
24+
int contextAlive = contextsAlive();
25+
if (contextAlive > CONTEXTS_ALLOWED) {
26+
StringBuilder names = new StringBuilder();
27+
for (ContextEscapeDetection.TestContext ref : ContextEscapeDetection.contexts) {
28+
if (ref.context.get() != null) names.append(ref.testName).append(' ');
29+
}
30+
Assert.fail("Multiple contexts survived test suite: " + names.toString());
31+
}
32+
}
33+
}
34+
}
35+
super.testRunFinished(result);
36+
}
37+
38+
private static synchronized int contextsAlive() {
39+
int count = 0;
40+
for (ContextEscapeDetection.TestContext ref : ContextEscapeDetection.contexts) {
41+
if (ref.context.get() != null) count++;
42+
}
43+
return count;
44+
}
45+
46+
private static volatile Object o = null;
47+
48+
private static synchronized void forceGCHeuristic0() {
49+
System.gc();
50+
Runtime.getRuntime().gc();
51+
System.gc();
52+
Runtime.getRuntime().gc();
53+
System.gc();
54+
Runtime.getRuntime().gc();
55+
System.gc();
56+
Runtime.getRuntime().gc();
57+
System.gc();
58+
}
59+
60+
private static synchronized void forceGCHeuristic1() {
61+
Object obj = new Object();
62+
WeakReference ref = new WeakReference<Object>(obj);
63+
obj = null;
64+
while (ref.get() != null) {
65+
System.gc();
66+
}
67+
}
68+
69+
private static synchronized void forceGCHeuristic2() {
70+
try {
71+
Object[] arr = new Object[1024]; // upto 8 GB
72+
WeakReference ref = new WeakReference<Object>(arr);
73+
o = arr; // make sure array isn't optimized away
74+
75+
Runtime runtime = Runtime.getRuntime();
76+
// allocate memory until no more that 64MB is left
77+
for (int i = 0; i < 1024 &&
78+
runtime.totalMemory() != runtime.maxMemory() ||
79+
runtime.freeMemory() < 1024 * 1024 * 64; i++) {
80+
int[] data = new int[1024 * 1024]; // 8MB
81+
for (int j = 0; j < 1024 * 1024; j++) {
82+
data[j] = j; // force actual pages allocation
83+
}
84+
arr[i] = data;
85+
}
86+
o = null;
87+
arr = new Object[128];
88+
o = arr;
89+
// allocate 1 more GB
90+
for (int i = 0; i < 128; i++) {
91+
int[] data = new int[1024 * 1024]; // 8MB
92+
for (int j = 0; j < 1024 * 1024; j++) {
93+
data[j] = j; // force actual pages allocation
94+
}
95+
arr[i] = data;
96+
}
97+
o = null;
98+
arr = null;
99+
100+
forceGCHeuristic0();
101+
while (ref.get() != null) {
102+
System.gc();
103+
}
104+
} catch (OutOfMemoryError e) {
105+
o = null;
106+
// just swallow
107+
}
108+
}
109+
}

test/test/DottyTest.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import dotty.tools.dotc.Compiler
1414
import dotty.tools.dotc
1515
import dotty.tools.dotc.core.Phases.Phase
1616

17-
class DottyTest {
17+
class DottyTest extends ContextEscapeDetection{
1818

1919
dotty.tools.dotc.parsing.Scanners // initialize keywords
2020

21-
implicit val ctx: Context = {
21+
implicit var ctx: Contexts.Context = {
2222
val base = new ContextBase
2323
import base.settings._
2424
val ctx = base.initialCtx.fresh
@@ -37,6 +37,10 @@ class DottyTest {
3737
ctx
3838
}
3939

40+
override def getCtx: Context = ctx
41+
override def clearCtx() = {
42+
ctx = null
43+
}
4044
private def compilerWithChecker(phase: String)(assertion:(tpd.Tree, Context) => Unit) = new Compiler {
4145
override def phases = {
4246
val allPhases = super.phases

0 commit comments

Comments
 (0)