Skip to content

Commit 4bb020f

Browse files
performance improvements
1 parent 398f563 commit 4bb020f

4 files changed

Lines changed: 148 additions & 123 deletions

File tree

src/providers/espresso.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,37 +61,37 @@ export default class Espresso extends BaseProvider<EspressoOptions> {
6161
throw new TestingBotError(`app option is required`);
6262
}
6363

64-
try {
65-
await fs.promises.access(this.options.app, fs.constants.R_OK);
66-
} catch {
67-
throw new TestingBotError(
68-
`Provided app path does not exist ${this.options.app}`,
69-
);
70-
}
71-
7264
if (this.options.testApp === undefined) {
7365
throw new TestingBotError(`testApp option is required`);
7466
}
7567

76-
try {
77-
await fs.promises.access(this.options.testApp, fs.constants.R_OK);
78-
} catch {
79-
throw new TestingBotError(
80-
`testApp path does not exist ${this.options.testApp}`,
81-
);
82-
}
83-
8468
// Validate report options
8569
if (this.options.report && !this.options.reportOutputDir) {
8670
throw new TestingBotError(
8771
`--report-output-dir is required when --report is specified`,
8872
);
8973
}
9074

75+
// Validate file access in parallel for better performance
76+
const fileChecks = [
77+
fs.promises.access(this.options.app, fs.constants.R_OK).catch(() => {
78+
throw new TestingBotError(
79+
`Provided app path does not exist ${this.options.app}`,
80+
);
81+
}),
82+
fs.promises.access(this.options.testApp, fs.constants.R_OK).catch(() => {
83+
throw new TestingBotError(
84+
`testApp path does not exist ${this.options.testApp}`,
85+
);
86+
}),
87+
];
88+
9189
if (this.options.reportOutputDir) {
92-
await this.ensureOutputDirectory(this.options.reportOutputDir);
90+
fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
9391
}
9492

93+
await Promise.all(fileChecks);
94+
9595
return true;
9696
}
9797

src/providers/maestro.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,24 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
9696
throw new TestingBotError(`app option is required`);
9797
}
9898

99-
try {
100-
await fs.promises.access(this.options.app, fs.constants.R_OK);
101-
} catch {
99+
if (this.options.flows === undefined || this.options.flows.length === 0) {
100+
throw new TestingBotError(`flows option is required`);
101+
}
102+
103+
if (this.options.report && !this.options.reportOutputDir) {
102104
throw new TestingBotError(
103-
`Provided app path does not exist ${this.options.app}`,
105+
`--report-output-dir is required when --report is specified`,
104106
);
105107
}
106108

107-
if (this.options.flows === undefined || this.options.flows.length === 0) {
108-
throw new TestingBotError(`flows option is required`);
109-
}
109+
// Build list of all file checks to run in parallel
110+
const fileChecks: Promise<void>[] = [
111+
fs.promises.access(this.options.app, fs.constants.R_OK).catch(() => {
112+
throw new TestingBotError(
113+
`Provided app path does not exist ${this.options.app}`,
114+
);
115+
}),
116+
];
110117

111118
// Check if all flows paths exist (can be files, directories or glob patterns)
112119
for (const flowsPath of this.options.flows) {
@@ -116,28 +123,26 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
116123
flowsPath.includes('{');
117124

118125
if (!isGlobPattern) {
119-
try {
120-
await fs.promises.access(flowsPath, fs.constants.R_OK);
121-
} catch {
122-
throw new TestingBotError(`flows path does not exist ${flowsPath}`);
123-
}
126+
fileChecks.push(
127+
fs.promises.access(flowsPath, fs.constants.R_OK).catch(() => {
128+
throw new TestingBotError(`flows path does not exist ${flowsPath}`);
129+
}),
130+
);
124131
}
125132
}
126133

127-
if (this.options.report && !this.options.reportOutputDir) {
128-
throw new TestingBotError(
129-
`--report-output-dir is required when --report is specified`,
130-
);
131-
}
132-
133134
if (this.options.reportOutputDir) {
134-
await this.ensureOutputDirectory(this.options.reportOutputDir);
135+
fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
135136
}
136137

137138
if (this.options.downloadArtifacts && this.options.artifactsOutputDir) {
138-
await this.ensureOutputDirectory(this.options.artifactsOutputDir);
139+
fileChecks.push(
140+
this.ensureOutputDirectory(this.options.artifactsOutputDir),
141+
);
139142
}
140143

144+
await Promise.all(fileChecks);
145+
141146
return true;
142147
}
143148

src/providers/xcuitest.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,37 +61,37 @@ export default class XCUITest extends BaseProvider<XCUITestOptions> {
6161
throw new TestingBotError(`app option is required`);
6262
}
6363

64-
try {
65-
await fs.promises.access(this.options.app, fs.constants.R_OK);
66-
} catch {
67-
throw new TestingBotError(
68-
`Provided app path does not exist ${this.options.app}`,
69-
);
70-
}
71-
7264
if (this.options.testApp === undefined) {
7365
throw new TestingBotError(`testApp option is required`);
7466
}
7567

76-
try {
77-
await fs.promises.access(this.options.testApp, fs.constants.R_OK);
78-
} catch {
79-
throw new TestingBotError(
80-
`testApp path does not exist ${this.options.testApp}`,
81-
);
82-
}
83-
8468
// Validate report options
8569
if (this.options.report && !this.options.reportOutputDir) {
8670
throw new TestingBotError(
8771
`--report-output-dir is required when --report is specified`,
8872
);
8973
}
9074

75+
// Validate file access in parallel for better performance
76+
const fileChecks = [
77+
fs.promises.access(this.options.app, fs.constants.R_OK).catch(() => {
78+
throw new TestingBotError(
79+
`Provided app path does not exist ${this.options.app}`,
80+
);
81+
}),
82+
fs.promises.access(this.options.testApp, fs.constants.R_OK).catch(() => {
83+
throw new TestingBotError(
84+
`testApp path does not exist ${this.options.testApp}`,
85+
);
86+
}),
87+
];
88+
9189
if (this.options.reportOutputDir) {
92-
await this.ensureOutputDirectory(this.options.reportOutputDir);
90+
fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
9391
}
9492

93+
await Promise.all(fileChecks);
94+
9595
return true;
9696
}
9797

src/utils/connectivity.ts

Lines changed: 89 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,68 @@ export interface ConnectivityCheckResult {
1616
message: string;
1717
}
1818

19+
/**
20+
* Test a single endpoint and return the result
21+
*/
22+
async function testEndpoint(
23+
url: string,
24+
description: string,
25+
): Promise<EndpointResult> {
26+
const startTime = Date.now();
27+
try {
28+
const controller = new AbortController();
29+
const timeoutId = setTimeout(() => controller.abort(), 3000);
30+
31+
const response = await fetch(url, {
32+
method: 'HEAD',
33+
signal: controller.signal,
34+
redirect: 'manual',
35+
});
36+
37+
clearTimeout(timeoutId);
38+
const latencyMs = Date.now() - startTime;
39+
40+
return {
41+
endpoint: `${description} (${url})`,
42+
success: true,
43+
statusCode: response.status,
44+
latencyMs,
45+
};
46+
} catch (error) {
47+
const latencyMs = Date.now() - startTime;
48+
let errorMessage = 'Unknown error';
49+
50+
if (error instanceof Error) {
51+
if (error.name === 'AbortError') {
52+
errorMessage = 'Request timeout (>3s)';
53+
} else if (error.message.includes('fetch failed')) {
54+
errorMessage = 'Network request failed (DNS/connection error)';
55+
} else if (error.message.includes('ENOTFOUND')) {
56+
errorMessage = 'DNS resolution failed';
57+
} else if (error.message.includes('ECONNREFUSED')) {
58+
errorMessage = 'Connection refused';
59+
} else if (error.message.includes('ETIMEDOUT')) {
60+
errorMessage = 'Connection timeout';
61+
} else if (error.message.includes('ENETUNREACH')) {
62+
errorMessage = 'Network unreachable';
63+
} else {
64+
errorMessage = error.message;
65+
}
66+
}
67+
68+
return {
69+
endpoint: `${description} (${url})`,
70+
success: false,
71+
error: errorMessage,
72+
latencyMs,
73+
};
74+
}
75+
}
76+
1977
/**
2078
* Check if the system has internet connectivity by testing against
21-
* multiple reliable third-party endpoints with detailed diagnostics.
79+
* multiple reliable third-party endpoints in parallel.
80+
* Returns as soon as one endpoint succeeds, reducing latency significantly.
2281
*/
2382
export async function checkInternetConnectivity(): Promise<ConnectivityCheckResult> {
2483
const testEndpoints = [
@@ -30,79 +89,40 @@ export async function checkInternetConnectivity(): Promise<ConnectivityCheckResu
3089
{ url: 'https://1.1.1.1/', description: 'Cloudflare DNS' },
3190
];
3291

33-
const endpointResults: EndpointResult[] = [];
34-
let anySuccess = false;
35-
36-
for (const { url, description } of testEndpoints) {
37-
const startTime = Date.now();
38-
try {
39-
const controller = new AbortController();
40-
const timeoutId = setTimeout(() => controller.abort(), 3000);
41-
42-
const response = await fetch(url, {
43-
method: 'HEAD',
44-
signal: controller.signal,
45-
redirect: 'manual',
46-
});
47-
48-
clearTimeout(timeoutId);
49-
const latencyMs = Date.now() - startTime;
50-
51-
if (response) {
52-
anySuccess = true;
53-
endpointResults.push({
54-
endpoint: `${description} (${url})`,
55-
success: true,
56-
statusCode: response.status,
57-
latencyMs,
58-
});
59-
break;
60-
}
61-
} catch (error) {
62-
const latencyMs = Date.now() - startTime;
63-
let errorMessage = 'Unknown error';
64-
65-
if (error instanceof Error) {
66-
if (error.name === 'AbortError') {
67-
errorMessage = 'Request timeout (>3s)';
68-
} else if (error.message.includes('fetch failed')) {
69-
errorMessage = 'Network request failed (DNS/connection error)';
70-
} else if (error.message.includes('ENOTFOUND')) {
71-
errorMessage = 'DNS resolution failed';
72-
} else if (error.message.includes('ECONNREFUSED')) {
73-
errorMessage = 'Connection refused';
74-
} else if (error.message.includes('ETIMEDOUT')) {
75-
errorMessage = 'Connection timeout';
76-
} else if (error.message.includes('ENETUNREACH')) {
77-
errorMessage = 'Network unreachable';
78-
} else {
79-
errorMessage = error.message;
80-
}
92+
// Test all endpoints in parallel
93+
const endpointPromises = testEndpoints.map(({ url, description }) =>
94+
testEndpoint(url, description),
95+
);
96+
97+
// Use Promise.any to return on first success, or collect all failures
98+
try {
99+
// Create promises that only resolve on success
100+
const successPromises = endpointPromises.map(async (promise) => {
101+
const result = await promise;
102+
if (result.success) {
103+
return result;
81104
}
105+
throw result; // Throw failures so Promise.any continues to next
106+
});
82107

83-
endpointResults.push({
84-
endpoint: `${description} (${url})`,
85-
success: false,
86-
error: errorMessage,
87-
latencyMs,
88-
});
89-
}
90-
}
108+
const successResult = await Promise.any(successPromises);
91109

92-
let message: string;
93-
if (anySuccess) {
94-
const successfulEndpoint = endpointResults.find((r) => r.success);
95-
message = `Internet connectivity verified via ${successfulEndpoint?.endpoint} (${successfulEndpoint?.latencyMs}ms)`;
96-
} else {
110+
return {
111+
connected: true,
112+
endpointResults: [successResult],
113+
message: `Internet connectivity verified via ${successResult.endpoint} (${successResult.latencyMs}ms)`,
114+
};
115+
} catch (aggregateError) {
116+
// All endpoints failed - collect all results
117+
const endpointResults = await Promise.all(endpointPromises);
97118
const testedEndpoints = endpointResults.map((r) => r.endpoint).join(', ');
98-
message = `No internet connectivity detected. Tested endpoints: ${testedEndpoints}`;
99-
}
100119

101-
return {
102-
connected: anySuccess,
103-
endpointResults,
104-
message,
105-
};
120+
return {
121+
connected: false,
122+
endpointResults,
123+
message: `No internet connectivity detected. Tested endpoints: ${testedEndpoints}`,
124+
};
125+
}
106126
}
107127

108128
/**

0 commit comments

Comments
 (0)