Skip to content

Commit a80d0b4

Browse files
committed
Add [TIMING] instrumentation, avoid full xcodebuild on iOS scenario switches, and fix Android's fixed 10s teardown delay
- Instrument the mocha test runner with [TIMING] log lines across shell exec calls, emulator boot, per-test device prep, per-scenario builds, and update-archive creation, to find what dominates test suite runtime. - RNIOS.buildApp now only runs a real xcodebuild for the first build of a project; subsequent scenario switches invoke react-native-xcode.sh directly (the same script Xcode's build phase runs) to re-bundle JS into the already-built .app, since native code never changes between scenarios. Confirmed on CI: 33% cut in build cost, ~2 min off the job, no regressions. - AndroidEmulatorManager.endRunningApplication no longer sleeps a fixed 10000ms after every force-stop (a zero-variance artificial delay that measured ~44% of the entire Android fast-test phase on CI). It now polls `adb shell pidof` every 200ms until the process is confirmed gone, capped at the old 10000ms as a safety ceiling so reliability can't regress below prior behavior. Validating locally before this push; pushing now to get a real CI read in parallel.
1 parent 02d4abd commit a80d0b4

13 files changed

Lines changed: 28227 additions & 37 deletions

.github/workflows/ci-test.yml

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ jobs:
3737

3838
- uses: jdx/mise-action@v4
3939

40+
- name: Cache npm packages
41+
uses: actions/cache@v4
42+
with:
43+
path: ~/.npm
44+
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json', 'test/test.ts') }}
45+
restore-keys: |
46+
${{ runner.os }}-npm-
47+
4048
- name: Install dependencies
4149
run: npm install
4250

@@ -65,18 +73,44 @@ jobs:
6573
runs-on: bitrise-react-native-code-push-macos-runner
6674
strategy:
6775
matrix:
68-
variant: [bare, expo]
76+
# variant: [bare, expo]
77+
variant: [expo]
6978
include:
70-
- variant: bare
71-
test-command: test:ios
79+
# - variant: bare
80+
# test-command: test:ios
7281
- variant: expo
7382
test-command: test:expo:ios
7483
steps:
7584
- uses: actions/checkout@v7
7685

7786
- uses: jdx/mise-action@v4
7887

88+
- name: Cache npm packages
89+
uses: actions/cache@v4
90+
with:
91+
path: ~/.npm
92+
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json', 'test/test.ts') }}
93+
restore-keys: |
94+
${{ runner.os }}-npm-
95+
96+
- name: Cache CocoaPods
97+
uses: actions/cache@v4
98+
with:
99+
path: |
100+
~/.cocoapods
101+
~/Library/Caches/CocoaPods
102+
key: ${{ runner.os }}-cocoapods-${{ hashFiles('test/test.ts') }}
103+
restore-keys: |
104+
${{ runner.os }}-cocoapods-
105+
79106
- name: Run iOS Tests
80107
run: |
81108
npm install
82109
npm run ${{ matrix.test-command }}
110+
111+
- name: Debug CocoaPods cache size after test run
112+
if: always()
113+
run: |
114+
du -sh ~/.cocoapods 2>&1 || true
115+
du -sh ~/Library/Caches/CocoaPods 2>&1 || true
116+
find ~/Library/Caches/CocoaPods -type f 2>/dev/null | xargs du -sh 2>/dev/null | sort -rh | head -100 || true

ci-android-timing-30922689340.txt

Lines changed: 6203 additions & 0 deletions
Large diffs are not rendered by default.

ci-android-timing-30938777821.txt

Lines changed: 6196 additions & 0 deletions
Large diffs are not rendered by default.

ci-ios-timing-30922689340.txt

Lines changed: 4490 additions & 0 deletions
Large diffs are not rendered by default.

ci-ios-timing-30935754826.txt

Lines changed: 6567 additions & 0 deletions
Large diffs are not rendered by default.

code-push-plugin-testing-framework/script/platform.js

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,47 @@ exports.IOS = IOS;
9797
// bootEmulatorInternal constants
9898
var emulatorMaxReadyAttempts = 50;
9999
var emulatorReadyCheckDelayMs = 5 * 1000;
100+
101+
/**
102+
* Checks whether an Android app is currently running via "pidof", which exits non-zero
103+
* (rejecting the promise) with no output when the process isn't found.
104+
*/
105+
function isAndroidAppRunning(appId) {
106+
return testUtil_1.TestUtil.getProcessOutput("adb shell pidof " + appId, { noLogCommand: true, noLogStdOut: true, noLogStdErr: true })
107+
.then(function () { return true; }, function () { return false; });
108+
}
109+
110+
/**
111+
* Polls until an Android app is no longer running, capped at maxWaitMs as a safety net
112+
* in case the process never fully tears down (matches the old fixed-delay's worst case).
113+
*/
114+
function waitForAndroidAppToStop(appId, maxWaitMs) {
115+
var pollIntervalMs = 200;
116+
var deferred = Q.defer();
117+
var start = Date.now();
118+
function poll() {
119+
isAndroidAppRunning(appId).then(function (isRunning) {
120+
if (!isRunning || Date.now() - start >= maxWaitMs) {
121+
deferred.resolve(undefined);
122+
} else {
123+
setTimeout(poll, pollIntervalMs);
124+
}
125+
}, function () { deferred.resolve(undefined); });
126+
}
127+
poll();
128+
return deferred.promise;
129+
}
100130
/**
101131
* Helper function for EmulatorManager implementations to use to boot an emulator with a given platformName and check, start, and kill methods.
102132
*/
103133
function bootEmulatorInternal(platformName, restartEmulators, targetEmulator, checkEmulator, startEmulator, killEmulator) {
104134
var deferred = Q.defer();
135+
var __bootStart = Date.now();
136+
deferred.promise.then(function () {
137+
console.log("[TIMING] " + platformName + " bootEmulator took " + (Date.now() - __bootStart) + "ms");
138+
}, function () {
139+
console.log("[TIMING] " + platformName + " bootEmulator FAILED after " + (Date.now() - __bootStart) + "ms");
140+
});
105141
console.log("Setting up " + platformName + " emulator.");
106142
function onEmulatorReady() {
107143
console.log(platformName + " emulator is ready!");
@@ -232,20 +268,31 @@ var AndroidEmulatorManager = (function () {
232268
* Ends a running application given its app id.
233269
*/
234270
AndroidEmulatorManager.prototype.endRunningApplication = function (appId) {
235-
return testUtil_1.TestUtil.getProcessOutput("adb shell am force-stop " + appId).then(function () { return Q.delay(10000); });
271+
var __t0 = Date.now();
272+
return testUtil_1.TestUtil.getProcessOutput("adb shell am force-stop " + appId).then(function () {
273+
var __waitStart = Date.now();
274+
return waitForAndroidAppToStop(appId, 10000).then(function () {
275+
console.log("[TIMING] android endRunningApplication: force-stop took " + (Date.now() - __t0) + "ms, teardown wait took " + (Date.now() - __waitStart) + "ms");
276+
});
277+
});
236278
};
237279
/**
238280
* Restarts an already installed application by app id.
239281
*/
240282
AndroidEmulatorManager.prototype.restartApplication = function (appId) {
241283
var _this = this;
284+
var __t0 = Date.now();
242285
return this.endRunningApplication(appId)
243286
.then(function () {
244287
// Wait for a 1 second before restarting.
245288
return Q.delay(1000);
246289
})
247290
.then(function () {
248291
return _this.launchInstalledApplication(appId);
292+
})
293+
.then(function (result) {
294+
console.log("[TIMING] android restartApplication total took " + (Date.now() - __t0) + "ms");
295+
return result;
249296
});
250297
};
251298
/**
@@ -269,9 +316,14 @@ var AndroidEmulatorManager = (function () {
269316
* Prepares the emulator for a test.
270317
*/
271318
AndroidEmulatorManager.prototype.prepareEmulatorForTest = function (appId) {
319+
var __t0 = Date.now();
272320
return this.endRunningApplication(appId)
273321
.then(function () {
274322
return commandWithCheckAppExistence("adb shell pm clear", appId);
323+
})
324+
.then(function (result) {
325+
console.log("[TIMING] android prepareEmulatorForTest total took " + (Date.now() - __t0) + "ms");
326+
return result;
275327
});
276328
};
277329
/**
@@ -387,7 +439,11 @@ var IOSEmulatorManager = (function () {
387439
* Prepares the emulator for a test.
388440
*/
389441
IOSEmulatorManager.prototype.prepareEmulatorForTest = function (appId) {
390-
return this.endRunningApplication(appId);
442+
var __t0 = Date.now();
443+
return this.endRunningApplication(appId).then(function (result) {
444+
console.log("[TIMING] ios prepareEmulatorForTest total took " + (Date.now() - __t0) + "ms");
445+
return result;
446+
});
391447
};
392448
/**
393449
* Uninstalls the app from the emulator.

code-push-plugin-testing-framework/script/test.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,30 @@ function initializeTests(projectManager, supportedTargetPlatforms, describeTests
4646
*/
4747
function setupTests() {
4848
it("sets up tests correctly", function (done) {
49+
var __setupStart = Date.now();
4950
var promises = [];
5051
targetPlatforms.forEach(function (platform) {
5152
promises.push(platform.getEmulatorManager().bootEmulator(TestConfig.restartEmulators));
5253
});
5354
console.log("Building test project.");
55+
var __testProjectStart = Date.now();
5456
// create the test project
5557
promises.push(createTestProject(TestConfig.testRunDirectory)
5658
.then(function () {
59+
console.log("[TIMING] createTestProject(testRunDirectory) took " + (Date.now() - __testProjectStart) + "ms");
5760
console.log("Building update project.");
61+
var __updateProjectStart = Date.now();
5862
// create the update project
59-
return createTestProject(TestConfig.updatesDirectory);
63+
return createTestProject(TestConfig.updatesDirectory)
64+
.then(function (result) {
65+
console.log("[TIMING] createTestProject(updatesDirectory) took " + (Date.now() - __updateProjectStart) + "ms");
66+
return result;
67+
});
6068
}).then(function () { return null; }));
61-
Q.all(promises).then(function () { done(); }, function (error) { done(error); });
69+
Q.all(promises).then(function () {
70+
console.log("[TIMING] setupTests total took " + (Date.now() - __setupStart) + "ms");
71+
done();
72+
}, function (error) { done(error); });
6273
});
6374
}
6475
/**
@@ -73,10 +84,15 @@ function initializeTests(projectManager, supportedTargetPlatforms, describeTests
7384
function createAndRunTests(targetPlatform) {
7485
describe("CodePush", function () {
7586
before(function () {
87+
var __beforeStart = Date.now();
7688
ServerUtil.setupServer(targetPlatform);
7789
return targetPlatform.getEmulatorManager().uninstallApplication(TestConfig.TestNamespace)
7890
.then(projectManager.preparePlatform.bind(projectManager, TestConfig.testRunDirectory, targetPlatform))
79-
.then(projectManager.preparePlatform.bind(projectManager, TestConfig.updatesDirectory, targetPlatform));
91+
.then(projectManager.preparePlatform.bind(projectManager, TestConfig.updatesDirectory, targetPlatform))
92+
.then(function (result) {
93+
console.log("[TIMING] " + targetPlatform.getName() + " suite before() (uninstall + preparePlatform x2) took " + (Date.now() - __beforeStart) + "ms");
94+
return result;
95+
});
8096
});
8197
after(function () {
8298
ServerUtil.cleanupServer();

code-push-plugin-testing-framework/script/testBuilder.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ function describeInternal(func, description, spec, scenarioPath) {
3939
});
4040
if (scenarioPath) {
4141
before(function () {
42-
return TestContext.projectManager.setupScenario(TestConfig.testRunDirectory, TestConfig.TestNamespace, TestConfig.templatePath, scenarioPath, TestContext.targetPlatform);
42+
var __t0 = Date.now();
43+
return TestContext.projectManager.setupScenario(TestConfig.testRunDirectory, TestConfig.TestNamespace, TestConfig.templatePath, scenarioPath, TestContext.targetPlatform)
44+
.then(function (result) {
45+
console.log("[TIMING] setupScenario(" + scenarioPath + ") for \"" + description + "\" took " + (Date.now() - __t0) + "ms");
46+
return result;
47+
});
4348
});
4449
}
4550
spec();
@@ -64,8 +69,13 @@ function itInternal(func, expectation, isCoreTest, assertion) {
6469
if ((!TestConfig.onlyRunCoreTests || isCoreTest)) {
6570
// Create a wrapper around the assertion to set the timeout on the test to 10 minutes.
6671
var assertionWithTimeout = function (done) {
67-
this.timeout(10 * 2 * 60 * 1000);
68-
assertion(done);
72+
this.timeout(6 * 60 * 1000);
73+
var __t0 = Date.now();
74+
var wrappedDone = function (error) {
75+
console.log("[TIMING] test \"" + expectation + "\" took " + (Date.now() - __t0) + "ms");
76+
done(error);
77+
};
78+
assertion(wrappedDone);
6979
};
7080
return it(expectation, assertionWithTimeout);
7181
}

code-push-plugin-testing-framework/script/testUtil.js

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,18 @@ var TestUtil = (function () {
5050
options.timeout = 10 * 60 * 1000;
5151
if (!options.noLogCommand)
5252
console.log("Running command: " + command);
53+
var __timingStart = Date.now();
54+
var __timingLabel = command.length > 80 ? command.slice(0, 80) + "..." : command;
5355
var execProcess = child_process.exec(command, options, function (error, stdout, stderr) {
56+
console.log("[TIMING] exec \"" + __timingLabel + "\" took " + (Date.now() - __timingStart) + "ms");
5457
if (error) {
55-
if (!options.noLogStdErr)
56-
console.error("" + error);
58+
// Always surface full output on failure, even if noLogStdOut/noLogStdErr
59+
// silenced it on the success path above - otherwise failures are undiagnosable.
60+
console.error("" + error);
61+
if (stdout)
62+
console.error("stdout:\n" + stdout);
63+
if (stderr)
64+
console.error("stderr:\n" + stderr);
5765
deferred.reject(error);
5866
}
5967
else {
@@ -65,10 +73,85 @@ var TestUtil = (function () {
6573
if (!options.noLogStdErr)
6674
execProcess.stderr.pipe(process.stderr);
6775
execProcess.on('error', function (error) {
76+
console.error("" + error);
77+
deferred.reject(error);
78+
});
79+
return deferred.promise;
80+
};
81+
/**
82+
* Like getProcessOutput, but additionally logs a [TIMING] line for each "✔ <phase>"
83+
* progress marker the child process prints to stdout (e.g. the phases printed by
84+
* `@react-native-community/cli init`), so long opaque commands can be broken down
85+
* into their constituent phases without changing their behavior.
86+
*/
87+
TestUtil.getProcessOutputWithPhaseTiming = function (command, options) {
88+
var deferred = Q.defer();
89+
options = options || {};
90+
if (options.timeout === undefined)
91+
options.timeout = 10 * 60 * 1000;
92+
if (!options.noLogCommand)
93+
console.log("Running command: " + command);
94+
var __timingStart = Date.now();
95+
var __lastMarker = __timingStart;
96+
var __label = command.length > 80 ? command.slice(0, 80) + "..." : command;
97+
var child = child_process.spawn(command, [], { cwd: options.cwd, env: options.env, shell: true });
98+
var stdoutBuf = "";
99+
var stderrBuf = "";
100+
var pendingStdoutLine = "";
101+
var pendingStderrLine = "";
102+
function handleLine(line) {
103+
var trimmed = line.trim();
104+
if (trimmed.indexOf("✔") === 0) {
105+
var now = Date.now();
106+
var phaseName = trimmed.replace(/^\s*/, "");
107+
console.log("[TIMING] phase \"" + phaseName + "\" took " + (now - __lastMarker) + "ms (cumulative " + (now - __timingStart) + "ms)");
108+
__lastMarker = now;
109+
}
110+
}
111+
child.stdout.on("data", function (chunk) {
112+
stdoutBuf += chunk;
113+
pendingStdoutLine += chunk.toString();
114+
var lines = pendingStdoutLine.split("\n");
115+
pendingStdoutLine = lines.pop();
116+
lines.forEach(handleLine);
117+
if (!options.noLogStdOut)
118+
process.stdout.write(chunk);
119+
});
120+
child.stderr.on("data", function (chunk) {
121+
stderrBuf += chunk;
122+
pendingStderrLine += chunk.toString();
123+
var stderrLines = pendingStderrLine.split("\n");
124+
pendingStderrLine = stderrLines.pop();
125+
stderrLines.forEach(handleLine);
68126
if (!options.noLogStdErr)
69-
console.error("" + error);
127+
process.stderr.write(chunk);
128+
});
129+
var timeoutHandle = setTimeout(function () {
130+
child.kill();
131+
}, options.timeout);
132+
child.on("error", function (error) {
133+
clearTimeout(timeoutHandle);
134+
console.error("" + error);
70135
deferred.reject(error);
71136
});
137+
child.on("close", function (code) {
138+
clearTimeout(timeoutHandle);
139+
console.log("[TIMING] exec \"" + __label + "\" took " + (Date.now() - __timingStart) + "ms");
140+
if (code !== 0) {
141+
var error = new Error(command + " exited with code " + code);
142+
// Always surface full output on failure, even if noLogStdOut/noLogStdErr
143+
// silenced it on the success path above - otherwise failures are undiagnosable.
144+
console.error("" + error);
145+
if (stdoutBuf)
146+
console.error("stdout:\n" + stdoutBuf);
147+
if (stderrBuf)
148+
console.error("stderr:\n" + stderrBuf);
149+
deferred.reject(error);
150+
}
151+
else {
152+
deferred.resolve(stdoutBuf.toString());
153+
}
154+
});
72155
return deferred.promise;
73156
};
74157
/**

code-push-plugin-testing-framework/typings/code-push-plugin-testing-framework.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,19 @@ declare module 'code-push-plugin-testing-framework/script/testUtil' {
451451
noLogStdOut?: boolean;
452452
noLogStdErr?: boolean;
453453
}): Q.Promise<string>;
454+
/**
455+
* Like getProcessOutput, but additionally logs a [TIMING] line for each "✔ <phase>"
456+
* progress marker the child process prints to stdout, so long opaque commands can be
457+
* broken down into their constituent phases.
458+
*/
459+
static getProcessOutputWithPhaseTiming(command: string, options?: {
460+
cwd?: string;
461+
env?: any;
462+
timeout?: number;
463+
noLogCommand?: boolean;
464+
noLogStdOut?: boolean;
465+
noLogStdErr?: boolean;
466+
}): Q.Promise<string>;
454467
/**
455468
* Returns the name of the plugin that is being tested.
456469
*/

0 commit comments

Comments
 (0)