1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.cuttlefish.tests; 17 18 import static com.google.common.truth.Truth.assertThat; 19 20 import android.platform.test.annotations.LargeTest; 21 22 import com.android.cuttlefish.tests.utils.CuttlefishHostTest; 23 import com.android.tradefed.device.DeviceNotAvailableException; 24 import com.android.tradefed.device.ITestDevice; 25 import com.android.tradefed.log.LogUtil.CLog; 26 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 27 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 28 import com.android.tradefed.util.AbiUtils; 29 import com.android.tradefed.util.CommandResult; 30 import com.android.tradefed.util.CommandStatus; 31 import com.google.auto.value.AutoValue; 32 import com.google.common.base.Splitter; 33 import com.google.common.base.Strings; 34 import com.google.common.collect.Lists; 35 import com.google.common.collect.MapDifference; 36 import com.google.common.collect.Maps; 37 import com.google.common.collect.Range; 38 import com.google.common.truth.Correspondence; 39 40 import java.io.BufferedReader; 41 import java.io.File; 42 import java.io.FileNotFoundException; 43 import java.io.InputStreamReader; 44 import java.util.ArrayList; 45 import java.util.Arrays; 46 import java.util.Collection; 47 import java.util.Collections; 48 import java.util.HashMap; 49 import java.util.Iterator; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Objects; 53 import java.util.UUID; 54 import java.util.regex.Matcher; 55 import java.util.regex.Pattern; 56 57 import org.json.JSONArray; 58 import org.json.JSONException; 59 import org.json.JSONObject; 60 import org.junit.After; 61 import org.junit.Before; 62 import org.junit.Test; 63 import org.junit.runner.RunWith; 64 65 /** 66 * Tests that a Cuttlefish device can interactively connect and disconnect displays. 67 */ 68 @RunWith(DeviceJUnit4ClassRunner.class) 69 public class CuttlefishDisplayHotplugTest extends CuttlefishHostTest { 70 71 private static final long DEFAULT_TIMEOUT_MS = 5000; 72 73 private static final String CVD_BINARY_BASENAME = "cvd"; 74 75 private static final String CVD_DISPLAY_BINARY_BASENAME = "cvd_internal_display"; 76 runCvdCommand(Collection<String> commandArgs)77 private CommandResult runCvdCommand(Collection<String> commandArgs) throws FileNotFoundException { 78 // TODO: Switch back to using `cvd` after either: 79 // * Commands under `cvd` can be used with instances launched through `launch_cvd`. 80 // * ATP launches instances using `cvd start` instead of `launch_cvd`. 81 String cvdBinary = runner.getHostBinaryPath(CVD_DISPLAY_BINARY_BASENAME); 82 83 List<String> fullCommand = new ArrayList<String>(commandArgs); 84 fullCommand.add(0, cvdBinary); 85 86 // Remove the "display" part of the command until switching back to `cvd`. 87 fullCommand.remove(1); 88 89 return runner.run(DEFAULT_TIMEOUT_MS, fullCommand.toArray(new String[0])); 90 } 91 92 private static final String HELPER_APP_APK = "CuttlefishDisplayHotplugHelperApp.apk"; 93 94 private static final String HELPER_APP_PKG = "com.android.cuttlefish.displayhotplughelper"; 95 96 private static final String HELPER_APP_ACTIVITY = "com.android.cuttlefish.displayhotplughelper/.DisplayHotplugHelperApp"; 97 98 private static final String HELPER_APP_UUID_FLAG = "display_hotplug_uuid"; 99 100 private static final int HELPER_APP_LOG_CHECK_ATTEMPTS = 5; 101 102 private static final int HELPER_APP_LOG_CHECK_TIMEOUT_MILLISECONDS = 1000; 103 104 private static final int CHECK_FOR_UPDATED_GUEST_DISPLAYS_ATTEMPTS = 5; 105 106 private static final int CHECK_FOR_UPDATED_GUEST_DISPLAYS_SLEEP_MILLISECONDS = 500; 107 108 private static final Splitter LOGCAT_NEWLINE_SPLITTER = Splitter.on('\n').trimResults(); 109 110 @Before setUp()111 public void setUp() throws Exception { 112 getDevice().uninstallPackage(HELPER_APP_PKG); 113 installPackage(HELPER_APP_APK); 114 } 115 116 @After tearDown()117 public void tearDown() throws Exception { 118 getDevice().uninstallPackage(HELPER_APP_PKG); 119 } 120 121 /** 122 * Display information as seen from the host (i.e. from Crosvm via a `cvd display` command). 123 */ 124 @AutoValue 125 public static abstract class HostDisplayInfo { create(int id, int width, int height)126 static HostDisplayInfo create(int id, int width, int height) { 127 return new AutoValue_CuttlefishDisplayHotplugTest_HostDisplayInfo(id, width, height); 128 } 129 id()130 abstract int id(); width()131 abstract int width(); height()132 abstract int height(); 133 } 134 135 /** 136 * Display information as seen from the guest (i.e. from SurfaceFlinger/DisplayManager). 137 */ 138 @AutoValue 139 public static abstract class GuestDisplayInfo { create(int id, int width, int height)140 static GuestDisplayInfo create(int id, int width, int height) { 141 return new AutoValue_CuttlefishDisplayHotplugTest_GuestDisplayInfo(id, width, height); 142 } 143 id()144 abstract int id(); width()145 abstract int width(); height()146 abstract int height(); 147 } 148 149 /** 150 * Expected input JSON format: 151 * 152 * { 153 * "displays" : { 154 * "<display id>": { 155 * "mode": { 156 * "windowed": [ 157 * <width>, 158 * <height>, 159 * ], 160 * }, 161 * ... 162 * }, 163 * ... 164 * } 165 * } 166 * 167 */ parseHostDisplayInfos(String inputJson)168 private Map<Integer, HostDisplayInfo> parseHostDisplayInfos(String inputJson) { 169 if (Strings.isNullOrEmpty(inputJson)) { 170 throw new IllegalArgumentException("Null display info json."); 171 } 172 173 Map<Integer, HostDisplayInfo> displayInfos = new HashMap<Integer, HostDisplayInfo>(); 174 175 try { 176 JSONObject json = new JSONObject(inputJson); 177 JSONObject jsonDisplays = json.getJSONObject("displays"); 178 for (Iterator<String> keyIt = jsonDisplays.keys(); keyIt.hasNext(); ) { 179 String displayNumberString = keyIt.next(); 180 181 JSONObject jsonDisplay = jsonDisplays.getJSONObject(displayNumberString); 182 JSONObject jsonDisplayMode = jsonDisplay.getJSONObject("mode"); 183 JSONArray jsonDisplayModeWindowed = jsonDisplayMode.getJSONArray("windowed"); 184 185 int id = Integer.parseInt(displayNumberString); 186 int w = jsonDisplayModeWindowed.getInt(0); 187 int h = jsonDisplayModeWindowed.getInt(1); 188 189 displayInfos.put(id, HostDisplayInfo.create(id, w, h)); 190 } 191 } catch (JSONException e) { 192 throw new IllegalArgumentException("Invalid display info json: " + inputJson, e); 193 } 194 195 return displayInfos; 196 } 197 198 199 /** 200 * Expected input JSON format: 201 * 202 * { 203 * "displays" : [ 204 * { 205 * "id": <id>, 206 * "name": <name>, 207 * "width": <width>, 208 * "height": <height>, 209 * }, 210 * ... 211 * ] 212 * } 213 */ parseGuestDisplayInfos(String inputJson)214 private Map<Integer, GuestDisplayInfo> parseGuestDisplayInfos(String inputJson) { 215 if (Strings.isNullOrEmpty(inputJson)) { 216 throw new NullPointerException("Null display info json."); 217 } 218 219 Map<Integer, GuestDisplayInfo> displayInfos = new HashMap<Integer, GuestDisplayInfo>(); 220 221 try { 222 JSONObject json = new JSONObject(inputJson); 223 JSONArray jsonDisplays = json.getJSONArray("displays"); 224 for (int i = 0; i < jsonDisplays.length(); i++) { 225 JSONObject jsonDisplay = jsonDisplays.getJSONObject(i); 226 int id = jsonDisplay.getInt("id"); 227 int w = jsonDisplay.getInt("width"); 228 int h = jsonDisplay.getInt("height"); 229 displayInfos.put(id, GuestDisplayInfo.create(id, w, h)); 230 } 231 } catch (JSONException e) { 232 throw new IllegalArgumentException("Invalid display info json: " + inputJson, e); 233 } 234 235 return displayInfos; 236 } 237 getDisplayHotplugHelperAppOutput()238 private String getDisplayHotplugHelperAppOutput() throws Exception { 239 final String uuid = UUID.randomUUID().toString(); 240 241 final Pattern guestDisplayInfoPattern = 242 Pattern.compile( 243 String.format("^.*DisplayHotplugHelper.*%s.* displays: (\\{.*\\})", uuid)); 244 245 getDevice().executeShellCommand( 246 String.format("am start -n %s --es %s %s", HELPER_APP_ACTIVITY, HELPER_APP_UUID_FLAG, uuid)); 247 248 for (int attempt = 0; attempt < HELPER_APP_LOG_CHECK_ATTEMPTS; attempt++) { 249 String logcat = getDevice().executeAdbCommand("logcat", "-d", "DisplayHotplugHelper:E", "*:S"); 250 251 List<String> logcatLines = Lists.newArrayList(LOGCAT_NEWLINE_SPLITTER.split(logcat)); 252 253 // Inspect latest first: 254 Collections.reverse(logcatLines); 255 256 for (String logcatLine : logcatLines) { 257 Matcher matcher = guestDisplayInfoPattern.matcher(logcatLine); 258 if (matcher.find()) { 259 return matcher.group(1); 260 } 261 } 262 263 Thread.sleep(HELPER_APP_LOG_CHECK_TIMEOUT_MILLISECONDS); 264 } 265 266 throw new IllegalStateException("Failed to find display info from helper app using uuid:" + uuid); 267 } 268 getGuestDisplays()269 private Map<Integer, GuestDisplayInfo> getGuestDisplays() throws Exception { 270 return parseGuestDisplayInfos(getDisplayHotplugHelperAppOutput()); 271 } 272 getHostDisplays()273 public Map<Integer, HostDisplayInfo> getHostDisplays() throws FileNotFoundException { 274 CommandResult listDisplaysResult = runCvdCommand(Lists.newArrayList("display", "list")); 275 if (!CommandStatus.SUCCESS.equals(listDisplaysResult.getStatus())) { 276 throw new IllegalStateException( 277 String.format("Failed to run list displays command:%s\n%s", 278 listDisplaysResult.getStdout(), 279 listDisplaysResult.getStderr())); 280 } 281 return parseHostDisplayInfos(listDisplaysResult.getStdout()); 282 } 283 284 @AutoValue 285 public static abstract class AddDisplayParams { create(int width, int height)286 static AddDisplayParams create(int width, int height) { 287 return new AutoValue_CuttlefishDisplayHotplugTest_AddDisplayParams(width, height); 288 } 289 width()290 abstract int width(); height()291 abstract int height(); 292 } 293 294 /* As supported by `cvd display add` */ 295 private static final int MAX_ADD_DISPLAYS = 4; 296 addDisplays(List<AddDisplayParams> params)297 public void addDisplays(List<AddDisplayParams> params) throws FileNotFoundException { 298 if (params.size() > MAX_ADD_DISPLAYS) { 299 throw new IllegalArgumentException( 300 "`cvd display add` only supports adding up to " + MAX_ADD_DISPLAYS + 301 " at once but was requested to add " + params.size() + " displays."); 302 } 303 304 List<String> addDisplaysCommand = Lists.newArrayList("display", "add"); 305 for (int i = 0; i < params.size(); i++) { 306 AddDisplayParams display = params.get(i); 307 308 addDisplaysCommand.add(String.format( 309 "--display%d=width=%d,height=%d", i, display.width(), display.height())); 310 } 311 312 CommandResult addDisplayResult = runCvdCommand(addDisplaysCommand); 313 if (!CommandStatus.SUCCESS.equals(addDisplayResult.getStatus())) { 314 throw new IllegalStateException( 315 String.format("Failed to run add display command:%s\n%s", 316 addDisplayResult.getStdout(), 317 addDisplayResult.getStderr())); 318 } 319 } 320 addDisplay(int width, int height)321 public void addDisplay(int width, int height) throws FileNotFoundException { 322 addDisplays(List.of(AddDisplayParams.create(width, height))); 323 } 324 removeDisplays(List<Integer> displayIds)325 public void removeDisplays(List<Integer> displayIds) throws FileNotFoundException { 326 List<String> removeDisplaysCommand = Lists.newArrayList("display", "remove"); 327 for (Integer displayId : displayIds) { 328 removeDisplaysCommand.add("--display=" + displayId.toString()); 329 } 330 331 CommandResult removeDisplayResult = runCvdCommand(removeDisplaysCommand); 332 if (!CommandStatus.SUCCESS.equals(removeDisplayResult.getStatus())) { 333 throw new IllegalStateException( 334 String.format("Failed to run remove display command:%s\n%s", 335 removeDisplayResult.getStdout(), 336 removeDisplayResult.getStderr())); 337 } 338 } 339 removeDisplay(int displayId)340 public void removeDisplay(int displayId) throws FileNotFoundException { 341 removeDisplays(List.of(displayId)); 342 } 343 344 Correspondence<GuestDisplayInfo, AddDisplayParams> GUEST_DISPLAY_MATCHES = 345 Correspondence.from((GuestDisplayInfo lhs, AddDisplayParams rhs) -> { 346 return lhs.width() == rhs.width() && 347 lhs.height() == rhs.height(); 348 }, "matches the display info of"); 349 350 Correspondence<HostDisplayInfo, AddDisplayParams> HOST_DISPLAY_MATCHES = 351 Correspondence.from((HostDisplayInfo lhs, AddDisplayParams rhs) -> { 352 return lhs.width() == rhs.width() && 353 lhs.height() == rhs.height(); 354 }, "matches the display info of"); 355 doOneConnectAndDisconnectCycle(List<AddDisplayParams> params)356 private void doOneConnectAndDisconnectCycle(List<AddDisplayParams> params) throws Exception { 357 // Check which displays Crosvm is aware of originally. 358 Map<Integer, HostDisplayInfo> originalHostDisplays = getHostDisplays(); 359 assertThat(originalHostDisplays).isNotNull(); 360 assertThat(originalHostDisplays).isNotEmpty(); 361 362 // Check which displays SurfaceFlinger and DisplayManager are aware of originally. 363 Map<Integer, GuestDisplayInfo> originalGuestDisplays = getGuestDisplays(); 364 assertThat(originalGuestDisplays).isNotNull(); 365 assertThat(originalGuestDisplays).isNotEmpty(); 366 367 // Perform the hotplug connect. 368 addDisplays(params); 369 370 // Check that Crosvm is aware of the new display (the added displays should 371 // be visible immediately after the host command completes and this should 372 // not need retries). 373 Map<Integer, HostDisplayInfo> afterAddHostDisplays = getHostDisplays(); 374 assertThat(afterAddHostDisplays).isNotNull(); 375 376 MapDifference<Integer, HostDisplayInfo> addedHostDisplaysDiff = 377 Maps.difference(afterAddHostDisplays, originalHostDisplays); 378 assertThat(addedHostDisplaysDiff.entriesOnlyOnLeft()).hasSize(params.size()); 379 assertThat(addedHostDisplaysDiff.entriesOnlyOnRight()).isEmpty(); 380 381 Map<Integer, HostDisplayInfo> addedHostDisplays = 382 addedHostDisplaysDiff.entriesOnlyOnLeft(); 383 assertThat(addedHostDisplays.values()) 384 .comparingElementsUsing(HOST_DISPLAY_MATCHES) 385 .containsExactlyElementsIn(params); 386 387 // Check that SurfaceFlinger and DisplayManager are aware of the new display. 388 Map<Integer, GuestDisplayInfo> afterAddGuestDisplays = null; 389 for (int attempt = 0; attempt < CHECK_FOR_UPDATED_GUEST_DISPLAYS_ATTEMPTS; attempt++) { 390 // Guest components (HWComposer/SurfaceFlinger/etc) may take some time to process. 391 Thread.sleep(CHECK_FOR_UPDATED_GUEST_DISPLAYS_SLEEP_MILLISECONDS); 392 393 afterAddGuestDisplays = getGuestDisplays(); 394 assertThat(afterAddGuestDisplays).isNotNull(); 395 396 int expectedNumberOfGuestDisplaysAfterAdd = 397 originalGuestDisplays.size() + params.size(); 398 399 int numberOfGuestDisplaysAfterAdd = afterAddGuestDisplays.size(); 400 if (numberOfGuestDisplaysAfterAdd == expectedNumberOfGuestDisplaysAfterAdd) { 401 break; 402 } 403 404 CLog.i("Number of guest displays after add command did not yet match expected on " + 405 "attempt %d (actual:%d vs expected:%d)", 406 attempt, numberOfGuestDisplaysAfterAdd, expectedNumberOfGuestDisplaysAfterAdd); 407 } 408 MapDifference<Integer, GuestDisplayInfo> addedGuestDisplaysDiff = 409 Maps.difference(afterAddGuestDisplays, originalGuestDisplays);; 410 assertThat(addedGuestDisplaysDiff.entriesOnlyOnLeft()).hasSize(params.size()); 411 assertThat(addedGuestDisplaysDiff.entriesOnlyOnRight()).isEmpty(); 412 413 Map<Integer, GuestDisplayInfo> addedGuestDisplays = 414 addedGuestDisplaysDiff.entriesOnlyOnLeft(); 415 assertThat(addedGuestDisplays.values()) 416 .comparingElementsUsing(GUEST_DISPLAY_MATCHES) 417 .containsExactlyElementsIn(params); 418 419 // Perform the hotplug disconnect. 420 List<Integer> addedHostDisplayIds = new ArrayList<Integer>(); 421 for (HostDisplayInfo addedHostDisplay : addedHostDisplays.values()) { 422 addedHostDisplayIds.add(addedHostDisplay.id()); 423 } 424 removeDisplays(addedHostDisplayIds); 425 426 // Check that Crosvm does not show the removed display (the removed displays 427 // should be visible immediately after the host command completes and this 428 // should not need retries). 429 Map<Integer, HostDisplayInfo> afterRemoveHostDisplays = getHostDisplays(); 430 assertThat(afterRemoveHostDisplays).isNotNull(); 431 432 MapDifference<Integer, HostDisplayInfo> removedHostDisplaysDiff = 433 Maps.difference(afterRemoveHostDisplays, originalHostDisplays); 434 assertThat(removedHostDisplaysDiff.entriesDiffering()).isEmpty(); 435 436 // Check that SurfaceFlinger and DisplayManager do not show the removed display. 437 Map<Integer, GuestDisplayInfo> afterRemoveGuestDisplays = null; 438 for (int attempt = 0; attempt < CHECK_FOR_UPDATED_GUEST_DISPLAYS_ATTEMPTS; attempt++) { 439 // Guest components (HWComposer/SurfaceFlinger/etc) may take some time to process. 440 Thread.sleep(CHECK_FOR_UPDATED_GUEST_DISPLAYS_SLEEP_MILLISECONDS); 441 442 afterRemoveGuestDisplays = getGuestDisplays(); 443 assertThat(afterRemoveGuestDisplays).isNotNull(); 444 445 int expectedNumberOfGuestDisplaysAfterRemove = originalGuestDisplays.size(); 446 447 int numberOfGuestDisplaysAfterRemove = afterRemoveGuestDisplays.size(); 448 if (numberOfGuestDisplaysAfterRemove == expectedNumberOfGuestDisplaysAfterRemove) { 449 break; 450 } 451 452 CLog.i("Number of guest displays after remove command did not yet match expected on " + 453 "attempt %d (actual:%d vs expected:%d)", 454 attempt, numberOfGuestDisplaysAfterRemove, 455 expectedNumberOfGuestDisplaysAfterRemove); 456 } 457 MapDifference<Integer, GuestDisplayInfo> removedGuestDisplaysDiff 458 = Maps.difference(afterRemoveGuestDisplays, originalGuestDisplays); 459 assertThat(removedGuestDisplaysDiff.entriesDiffering()).isEmpty(); 460 } 461 462 @Test testDisplayHotplug()463 public void testDisplayHotplug() throws Exception { 464 doOneConnectAndDisconnectCycle( 465 List.of(AddDisplayParams.create(600, 500))); 466 } 467 468 @Test testDisplayHotplugMultipleDisplays()469 public void testDisplayHotplugMultipleDisplays() throws Exception { 470 doOneConnectAndDisconnectCycle( 471 List.of( 472 AddDisplayParams.create(1920, 1080), 473 AddDisplayParams.create(1280, 720))); 474 } 475 476 @Test testDisplayHotplugSeries()477 public void testDisplayHotplugSeries() throws Exception { 478 doOneConnectAndDisconnectCycle( 479 List.of(AddDisplayParams.create(640, 480))); 480 481 doOneConnectAndDisconnectCycle( 482 List.of(AddDisplayParams.create(1280, 720))); 483 484 doOneConnectAndDisconnectCycle( 485 List.of(AddDisplayParams.create(1920, 1080))); 486 487 doOneConnectAndDisconnectCycle( 488 List.of(AddDisplayParams.create(3840, 2160))); 489 } 490 491 @AutoValue 492 public static abstract class MemoryInfo { create(int usedRam)493 static MemoryInfo create(int usedRam) { 494 return new AutoValue_CuttlefishDisplayHotplugTest_MemoryInfo(usedRam); 495 } 496 usedRamBytes()497 abstract int usedRamBytes(); 498 } 499 500 private static final String GET_USED_RAM_COMMAND = "dumpsys meminfo"; 501 502 private static final Pattern USED_RAM_PATTERN = Pattern.compile("Used RAM: (.*?)K \\("); 503 getMemoryInfo()504 private MemoryInfo getMemoryInfo() throws Exception { 505 ITestDevice device = getDevice(); 506 507 CommandResult getUsedRamResult = device.executeShellV2Command(GET_USED_RAM_COMMAND); 508 if (!CommandStatus.SUCCESS.equals(getUsedRamResult.getStatus())) { 509 throw new IllegalStateException( 510 String.format("Failed to run |%s|: stdout: %s\n stderr: %s", 511 GET_USED_RAM_COMMAND, 512 getUsedRamResult.getStdout(), 513 getUsedRamResult.getStderr())); 514 } 515 // Ex: 516 // ... 517 // GPU: 0K ( 0K dmabuf + 0K private) 518 // Used RAM: 1,155,524K ( 870,488K used pss + 285,036K kernel) 519 // Lost RAM: 59,469K 520 // ... 521 String usedRamString = getUsedRamResult.getStdout(); 522 Matcher m = USED_RAM_PATTERN.matcher(usedRamString); 523 if (!m.find()) { 524 throw new IllegalStateException( 525 String.format("Failed to parse 'Used RAM' from stdout:\n%s", 526 getUsedRamResult.getStdout())); 527 } 528 // Ex: "1,228,768" 529 usedRamString = m.group(1); 530 usedRamString = usedRamString.replaceAll(",", ""); 531 int usedRam = Integer.parseInt(usedRamString) * 1000; 532 533 return MemoryInfo.create(usedRam); 534 } 535 536 private static final int MAX_ALLOWED_RAM_BYTES_DIFF = 32 * 1024 * 1024; 537 doCheckForLeaks(MemoryInfo base)538 private void doCheckForLeaks(MemoryInfo base) throws Exception { 539 MemoryInfo current = getMemoryInfo(); 540 541 assertThat(current.usedRamBytes()).isIn( 542 Range.closed(base.usedRamBytes() - MAX_ALLOWED_RAM_BYTES_DIFF, 543 base.usedRamBytes() + MAX_ALLOWED_RAM_BYTES_DIFF)); 544 } 545 546 @Test 547 @LargeTest testDisplayHotplugDoesNotLeakMemory()548 public void testDisplayHotplugDoesNotLeakMemory() throws Exception { 549 List<AddDisplayParams> toAdd = List.of(AddDisplayParams.create(600, 500)); 550 551 // Warm up to potentially reach any steady state memory usage. 552 for (int i = 0; i < 50; i++) { 553 doOneConnectAndDisconnectCycle(toAdd); 554 } 555 556 MemoryInfo original = getMemoryInfo(); 557 for (int i = 0; i <= 500; i++) { 558 doOneConnectAndDisconnectCycle(toAdd); 559 560 if (i % 100 == 0) { 561 doCheckForLeaks(original); 562 } 563 } 564 } 565 } 566