Skip to content

Commit 92c589c

Browse files
authored
Reduce runtime memory use (#699)
* Allow JVM args to be specified as gradle properties * Check for external-only contours in watershed * Release temp Mats and use preallocated objects in WatershedOperation * Add Cleaner class to run GC periodically * Limit eden size in deployed and native apps * Move args and jvmArgs to common configuration block
1 parent 071c3b7 commit 92c589c

File tree

9 files changed

+153
-54
lines changed

9 files changed

+153
-54
lines changed

build.gradle

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -276,12 +276,6 @@ project(":core") {
276276

277277
mainClassName = 'edu.wpi.grip.core.Main'
278278

279-
if (project.hasProperty('args')) {
280-
run {
281-
args = (project.args.split("\\s+") as List)
282-
}
283-
}
284-
285279
jar {
286280
manifest {
287281
attributes 'Implementation-Version': version, 'Main-Class': mainClassName
@@ -445,12 +439,6 @@ project(":ui") {
445439
}
446440
}
447441

448-
if (project.hasProperty('args')) {
449-
run {
450-
args = (project.args.split("\\s+") as List)
451-
}
452-
}
453-
454442
task testSharedLib() {
455443
description 'Compiles the shared library used by c++ generation testing.'
456444
doLast {
@@ -548,12 +536,17 @@ project(":ui") {
548536

549537
jfxMainAppJarName = "${jfx.appName}-${jfx.nativeReleaseVersion}.jar"
550538

551-
// This prevents the JIT from eating stack traces that get thrown a lot
539+
// -XX:-OmitStackTraceInFastThrow prevents the JIT from eating stack traces that get thrown a lot
552540
// This is slower but means we actually get the stack traces instead of
553541
// having them become one line like `java.lang.ArrayIndexOutOfBoundsException`
554542
// and as such, would be useless.
555543
// See: https://plumbr.eu/blog/java/on-a-quest-for-missing-stacktraces
556-
jvmArgs = ["-XX:-OmitStackTraceInFastThrow"]
544+
// -Xmx limits the heap size. This prevents memory use from ballooning with a lot
545+
// of JavaCV native objects being allocated hanging around waiting to get GC'd.
546+
// -XX:MaxNewSize limits the size of the eden space to force minor GCs to run more often.
547+
// This causes old mats (which take up little space on the heap but a lot of native memory) to get deallocated
548+
// and free up native memory quicker, limiting the memory the app takes up.
549+
jvmArgs = ["-XX:-OmitStackTraceInFastThrow", "-Xmx200m", "-XX:MaxNewSize=32m"]
557550

558551
bundleArguments = [
559552
"linux.launcher.url": file('linuxLauncher/build/exe/linuxLauncher/linuxLauncher').toURI().toURL()
@@ -562,6 +555,24 @@ project(":ui") {
562555
mainClassName = jfx.mainClass
563556
}
564557

558+
559+
configure([
560+
project(":core"),
561+
project(":ui"),
562+
project(":ui:preloader")
563+
]) {
564+
if (project.hasProperty('jvmArgs')) {
565+
run {
566+
jvmArgs = (project.jvmArgs.split("\\s+") as List)
567+
}
568+
}
569+
if (project.hasProperty('args')) {
570+
run {
571+
args = (project.args.split("\\s+") as List)
572+
}
573+
}
574+
}
575+
565576
/*
566577
* This is roughly based upon this post:
567578
* https://discuss.gradle.org/t/merge-jacoco-coverage-reports-for-multiproject-setups/12100/6
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package edu.wpi.grip.core;
2+
3+
import edu.wpi.grip.core.events.RunStoppedEvent;
4+
5+
import com.google.common.base.Stopwatch;
6+
import com.google.common.eventbus.Subscribe;
7+
import com.google.inject.Singleton;
8+
9+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
10+
11+
import javax.annotation.Nullable;
12+
13+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
14+
15+
/**
16+
* Cleans up unused objects by periodically calling {@link System#gc()} to nudge the
17+
* garbage collector to clean up dead native (JavaCV) objects. This is required because JavaCV
18+
* objects only free their native memory when they're garbage collected, so if they accumulate in
19+
* the heap, the app will use about 40x the memory as used heap (i.e. 230MB of used heap results in
20+
* about 9.8GB of used memory for the process). This is because {@code Mats} and anything
21+
* else extending {@link org.bytedeco.javacpp.Pointer} use native memory that greatly exceeds the
22+
* Java objects size on the heap.
23+
*
24+
* <p>JavaCV has a system property {@code org.bytedeco.javacpp.maxphysicalbytes} that it uses to
25+
* determine when to start deallocating native memory. However, this only results in calls to
26+
* {@code System.gc()} and imposes a hard upper limit on native memory use, limiting large images
27+
* or long pipelines. It's also not very portable: running from source needs it to be passed
28+
* as a JVM argument with gradle, and it can't be adjusted based on the amount of memory on the
29+
* system it's installed on. For us, manually running System.gc() periodically is a better solution.
30+
* </p>
31+
*/
32+
@Singleton
33+
public class Cleaner {
34+
35+
/**
36+
* The minimum time delay before running System.gc(). This is in milliseconds. A gc call will
37+
* never happen less than this amount of time after the previous call.
38+
*/
39+
private static final long MIN_DELAY = 1000;
40+
41+
/**
42+
* Stopwatch to keep track of the elapsed time since the last gc call.
43+
*/
44+
private final Stopwatch stopwatch = Stopwatch.createUnstarted();
45+
46+
/**
47+
* The minimum number of runs allowed before calling System.gc().
48+
*/
49+
private static final int MIN_RUNS_BEFORE_GC = 5;
50+
51+
/**
52+
* The number of runs since the last gc call.
53+
*/
54+
private int runsSinceLastGc = 0;
55+
56+
@Subscribe
57+
@SuppressFBWarnings(value = "DM_GC", justification = "GC is called infrequently")
58+
public void onRunFinished(@Nullable RunStoppedEvent e) {
59+
runsSinceLastGc++;
60+
if (!stopwatch.isRunning()) {
61+
stopwatch.start();
62+
}
63+
if (runsSinceLastGc >= MIN_RUNS_BEFORE_GC && stopwatch.elapsed(MILLISECONDS) >= MIN_DELAY) {
64+
runsSinceLastGc = 0;
65+
stopwatch.reset();
66+
System.gc();
67+
}
68+
}
69+
70+
}

core/src/main/java/edu/wpi/grip/core/GripCoreModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
160160
install(new FactoryModuleBuilder().build(Timer.Factory.class));
161161

162162
bind(BenchmarkRunner.class).asEagerSingleton();
163+
164+
bind(Cleaner.class).asEagerSingleton();
163165
}
164166

165167
protected void onSubscriberException(Throwable exception, @Nullable SubscriberExceptionContext

core/src/main/java/edu/wpi/grip/core/operations/composite/ContoursReport.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ public synchronized double[] getSolidity() {
150150
convexHull(contours.get(i), hull);
151151
solidities[i] = contourArea(contours.get(i)) / contourArea(hull);
152152
}
153+
hull.release();
153154
return solidities;
154155
}
155156

core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ public void perform() {
183183

184184
convexHull(contour, hull);
185185
final double solidity = 100 * area / contourArea(hull);
186+
hull.release();
186187
if (solidity < minSolidity || solidity > maxSolidity) {
187188
continue;
188189
}

core/src/main/java/edu/wpi/grip/core/operations/composite/FindContoursOperation.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public void perform() {
9494
MatVector contours = new MatVector();
9595
findContours(tmp, contours, externalOnly ? CV_RETR_EXTERNAL : CV_RETR_LIST,
9696
CV_CHAIN_APPROX_TC89_KCOS);
97+
tmp.release();
9798

9899
contoursSocket.setValue(new ContoursReport(contours, input.rows(), input.cols()));
99100
}

core/src/main/java/edu/wpi/grip/core/operations/composite/FindLinesOperation.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ public void perform() {
7373
lsd.detect(input, lines);
7474
} else {
7575
// The line detector works on a single channel. If the input is a color image, we can just
76-
// give the line
77-
// detector a grayscale version of it
76+
// give the line detector a grayscale version of it
7877
final Mat tmp = new Mat();
7978
cvtColor(input, tmp, COLOR_BGR2GRAY);
8079
lsd.detect(tmp, lines);
80+
tmp.release();
8181
}
8282

8383
// Store the lines in the LinesReport object
@@ -90,6 +90,7 @@ public void perform() {
9090
lineList.add(new LinesReport.Line(tmp[0], tmp[1], tmp[2], tmp[3]));
9191
}
9292
}
93+
lines.release();
9394

9495
linesReportSocket.setValue(new LinesReport(lsd, input, lineList));
9596
}

core/src/main/java/edu/wpi/grip/core/operations/composite/WatershedOperation.java

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import java.util.ArrayList;
1616
import java.util.List;
17+
import java.util.stream.Collectors;
18+
import java.util.stream.Stream;
1719

1820
import static org.bytedeco.javacpp.opencv_core.CV_32SC1;
1921
import static org.bytedeco.javacpp.opencv_core.CV_8UC1;
@@ -24,6 +26,7 @@
2426
import static org.bytedeco.javacpp.opencv_core.Point;
2527
import static org.bytedeco.javacpp.opencv_core.Point2f;
2628
import static org.bytedeco.javacpp.opencv_core.Scalar;
29+
import static org.bytedeco.javacpp.opencv_core.bitwise_xor;
2730
import static org.bytedeco.javacpp.opencv_imgproc.CV_CHAIN_APPROX_TC89_KCOS;
2831
import static org.bytedeco.javacpp.opencv_imgproc.CV_FILLED;
2932
import static org.bytedeco.javacpp.opencv_imgproc.CV_RETR_EXTERNAL;
@@ -64,12 +67,22 @@ public class WatershedOperation implements Operation {
6467
private final InputSocket<ContoursReport> contoursSocket;
6568
private final OutputSocket<ContoursReport> outputSocket;
6669

70+
private static final int MAX_MARKERS = 253;
71+
private final List<Mat> markerPool;
72+
private final MatVector contour = new MatVector(); // vector with a single element
73+
private final Mat markers = new Mat();
74+
private final Mat output = new Mat();
75+
private final Point backgroundLabel = new Point();
76+
6777
@SuppressWarnings("JavadocMethod")
6878
public WatershedOperation(InputSocket.Factory inputSocketFactory,
6979
OutputSocket.Factory outputSocketFactory) {
7080
srcSocket = inputSocketFactory.create(srcHint);
7181
contoursSocket = inputSocketFactory.create(contoursHint);
7282
outputSocket = outputSocketFactory.create(outputHint);
83+
markerPool = ImmutableList.copyOf(
84+
Stream.generate(Mat::new).limit(MAX_MARKERS).collect(Collectors.toList())
85+
);
7386
}
7487

7588
@Override
@@ -97,54 +110,50 @@ public void perform() {
97110
final ContoursReport contourReport = contoursSocket.getValue().get();
98111
final MatVector contours = contourReport.getContours();
99112

100-
final int maxMarkers = 253;
101-
if (contours.size() > maxMarkers) {
113+
if (contours.size() > MAX_MARKERS) {
102114
throw new IllegalArgumentException(
103-
"A maximum of " + maxMarkers + " contours can be used as markers."
115+
"A maximum of " + MAX_MARKERS + " contours can be used as markers."
104116
+ " Filter contours before connecting them to this operation if this keeps happening."
105117
+ " The contours must also all be external; nested contours will not work");
106118
}
107119

108-
final Mat markers = new Mat(input.size(), CV_32SC1, new Scalar(0.0));
109-
final Mat output = new Mat(markers.size(), CV_8UC1, new Scalar(0.0));
120+
markers.create(input.size(), CV_32SC1);
121+
output.create(input.size(), CV_8UC1);
122+
bitwise_xor(markers, markers, markers);
123+
bitwise_xor(output, output, output);
110124

111-
try {
112-
// draw foreground markers (these have to be different colors)
113-
for (int i = 0; i < contours.size(); i++) {
114-
drawContours(markers, contours, i, Scalar.all(i + 1), CV_FILLED, LINE_8, null, 2, null);
115-
}
125+
// draw foreground markers (these have to be different colors)
126+
for (int i = 0; i < contours.size(); i++) {
127+
drawContours(markers, contours, i, Scalar.all(i + 1), CV_FILLED, LINE_8, null, 2, null);
128+
}
116129

117-
// draw background marker a different color from the foreground markers
118-
Point backgroundLabel = fromPoint2f(findBackgroundMarker(markers, contours));
119-
circle(markers, backgroundLabel, 1, Scalar.WHITE, -1, LINE_8, 0);
120-
121-
// Perform watershed
122-
watershed(input, markers);
123-
markers.convertTo(output, CV_8UC1);
124-
125-
List<Mat> contourList = new ArrayList<>();
126-
for (int i = 1; i < contours.size(); i++) {
127-
Mat dst = new Mat();
128-
output.copyTo(dst, opencv_core.equals(markers, i).asMat());
129-
MatVector contour = new MatVector(); // vector with a single element
130-
findContours(dst, contour, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_TC89_KCOS);
131-
assert contour.size() == 1;
132-
contourList.add(contour.get(0).clone());
133-
contour.get(0).deallocate();
134-
contour.deallocate();
130+
// draw background marker a different color from the foreground markers
131+
findBackgroundMarker(markers, contours);
132+
circle(markers, backgroundLabel, 1, Scalar.WHITE, -1, LINE_8, 0);
133+
134+
// Perform watershed
135+
watershed(input, markers);
136+
markers.convertTo(output, CV_8UC1);
137+
138+
List<Mat> contourList = new ArrayList<>((int) contours.size());
139+
for (int i = 1; i < contours.size(); i++) {
140+
Mat dst = markerPool.get(i - 1);
141+
bitwise_xor(dst, dst, dst);
142+
output.copyTo(dst, opencv_core.equals(markers, i).asMat());
143+
findContours(dst, contour, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_TC89_KCOS);
144+
if (contour.size() < 1) {
145+
throw new IllegalArgumentException("No contours for marker");
135146
}
136-
MatVector foundContours = new MatVector(contourList.toArray(new Mat[contourList.size()]));
137-
outputSocket.setValue(new ContoursReport(foundContours, output.rows(), output.cols()));
138-
} finally {
139-
// make sure that the working mat is freed to avoid a memory leak
140-
markers.release();
147+
contourList.add(contour.get(0).clone());
141148
}
149+
MatVector foundContours = new MatVector(contourList.toArray(new Mat[contourList.size()]));
150+
outputSocket.setValue(new ContoursReport(foundContours, output.rows(), output.cols()));
142151
}
143152

144153
/**
145154
* Finds the first available point to place a background marker for the watershed operation.
146155
*/
147-
private static Point2f findBackgroundMarker(Mat markers, MatVector contours) {
156+
private void findBackgroundMarker(Mat markers, MatVector contours) {
148157
final int cols = markers.cols();
149158
final int rows = markers.rows();
150159
final int minDist = 5;
@@ -169,13 +178,16 @@ private static Point2f findBackgroundMarker(Mat markers, MatVector contours) {
169178
}
170179
if (!found) {
171180
// Should only happen if the image is clogged with contours
181+
backgroundLabel.deallocate();
172182
throw new IllegalStateException("Could not find a point for the background label");
173183
}
174-
return backgroundLabel;
184+
setBackgroundLabel(backgroundLabel);
185+
backgroundLabel.deallocate();
175186
}
176187

177-
private static Point fromPoint2f(Point2f p) {
178-
return new Point((int) p.x(), (int) p.y());
188+
private void setBackgroundLabel(Point2f p) {
189+
this.backgroundLabel.x((int) p.x());
190+
this.backgroundLabel.y((int) p.y());
179191
}
180192

181193
}

core/src/main/java/edu/wpi/grip/core/settings/ProjectSettings.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public class ProjectSettings implements Cloneable {
4141
@Setting(label = "Deploy JVM options", description = "Command line options passed to the "
4242
+ "roboRIO JVM")
4343
private String deployJvmOptions = "-Xmx50m -XX:-OmitStackTraceInFastThrow "
44-
+ "-XX:+HeapDumpOnOutOfMemoryError";
44+
+ "-XX:+HeapDumpOnOutOfMemoryError -XX:MaxNewSize=16m";
4545

4646
@Setting(label = "Internal server port",
4747
description = "The port that the internal server should run on.")

0 commit comments

Comments
 (0)