Skip to content

Commit 26c105b

Browse files
committed
Add --fastStartup flag
When flag is enabled the plugin deployment will be delayed until a frontend is connected. This way the deployment of the plugins cannot slow down the fronend loading time. TODO: - remove hardcoded time to wait for connection to finish. - discuss name of flag
1 parent 17b419f commit 26c105b

File tree

9 files changed

+468
-11
lines changed

9 files changed

+468
-11
lines changed

examples/browser/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
"coverage:report": "nyc report --reporter=html",
6666
"rebuild": "theia rebuild:browser --cacheRoot ../..",
6767
"start": "yarn rebuild && theia start --plugins=local-dir:../../plugins",
68+
"start:fast": "yarn rebuild && theia start --plugins=local-dir:../../plugins --fastStartup",
69+
"start:no": "yarn rebuild && theia start --plugins=local-dir:../../noPlugins",
6870
"start:debug": "yarn start --log-level=debug",
6971
"start:watch": "concurrently --kill-others -n tsc,bundle,run -c red,yellow,green \"tsc -b -w --preserveWatchOutput\" \"yarn watch:bundle\" \"yarn start\"",
7072
"test": "yarn rebuild && theia test . --plugins=local-dir:../../plugins --test-spec=../api-tests/**/*.spec.js",

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,13 @@
7979
"watch": "concurrently --kill-others -n tsc,browser,electron -c red,yellow,blue \"tsc -b -w --preserveWatchOutput\" \"yarn --cwd examples/browser watch:bundle\" \"yarn --cwd examples/electron watch:bundle\"",
8080
"watch:compile": "concurrently --kill-others -n cleanup,tsc -c magenta,red \"ts-clean dev-packages/* packages/* -w\" \"tsc -b -w --preserveWatchOutput\"",
8181
"performance:startup": "yarn performance:startup:browser && yarn performance:startup:electron",
82-
"performance:startup:browser": "concurrently --success first -k -r \"cd scripts/performance && node browser-performance.js --name 'Browser Frontend Startup' --folder browser --runs 10\" \"yarn --cwd examples/browser start\"",
83-
"performance:startup:electron": "yarn electron rebuild && cd scripts/performance && node electron-performance.js --name 'Electron Frontend Startup' --folder electron --runs 10"
82+
"performance:startup:browser": "concurrently --success first -k -r \"cd scripts/performance && node browser-performance.js --name 'Browser Frontend Startup' --folder browser --runs 1\" \"yarn --cwd examples/browser start\"",
83+
"performance:startup:browser:fast": "concurrently --success first -k -r \"cd scripts/performance && node browser-performance.js --name 'Browser Frontend Startup' --folder browser --runs 1\" \"yarn --cwd examples/browser start:fast\"",
84+
"performance:startup:browser:no": "concurrently --success first -k -r \"cd scripts/performance && node browser-performance.js --name 'Browser Frontend Startup' --folder browser --runs 1\" \"yarn --cwd examples/browser start:no\"",
85+
"performance:startup:browser:fast:no": "concurrently --success first -k -r \"cd scripts/performance && node browser-performance.js --name 'Browser Frontend Startup' --folder browser --runs 1\" \"yarn --cwd examples/browser start:fast:no\"",
86+
"performance:startup:electron": "yarn electron rebuild && cd scripts/performance && node electron-performance.js --name 'Electron Frontend Startup' --folder electron --runs 1",
87+
"performance:startup:electron:fast": "yarn electron rebuild && cd scripts/performance && node electron-performance-fast.js --name 'Electron Frontend Startup' --folder electron --runs 1",
88+
"performance:startup:electron:no": "yarn electron rebuild && cd scripts/performance && node electron-performance-no.js --name 'Electron Frontend Startup' --folder electron --runs 1"
8489
},
8590
"workspaces": [
8691
"dev-packages/*",

packages/core/src/node/backend-application-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { KeytarServiceImpl } from './keytar-server';
3535
import { ContributionFilterRegistry, ContributionFilterRegistryImpl } from '../common/contribution-filter';
3636
import { EnvironmentUtils } from './environment-utils';
3737
import { ProcessUtils } from './process-utils';
38+
import { ClientConnectionNotifier } from '.';
3839

3940
decorate(injectable(), ApplicationPackage);
4041

@@ -109,4 +110,5 @@ export const backendApplicationModule = new ContainerModule(bind => {
109110

110111
bind(EnvironmentUtils).toSelf().inSingletonScope();
111112
bind(ProcessUtils).toSelf().inSingletonScope();
113+
bind(ClientConnectionNotifier).toSelf().inSingletonScope();
112114
});

packages/core/src/node/backend-application.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { environment } from '../common/index';
2929
import { AddressInfo } from 'net';
3030
import { ApplicationPackage } from '@theia/application-package';
3131
import { ProcessUtils } from './process-utils';
32+
import { ClientConnectionNotifier } from './connection-notifier';
3233

3334
const APP_PROJECT_PATH = 'app-project-path';
3435

@@ -37,6 +38,7 @@ const TIMER_WARNING_THRESHOLD = 50;
3738
const DEFAULT_PORT = environment.electron.is() ? 0 : 3000;
3839
const DEFAULT_HOST = 'localhost';
3940
const DEFAULT_SSL = false;
41+
const DEFAULT_FAST_STARTUP = false;
4042

4143
export const BackendApplicationServer = Symbol('BackendApplicationServer');
4244
/**
@@ -107,6 +109,7 @@ export class BackendApplicationCliContribution implements CliContribution {
107109
cert: string | undefined;
108110
certkey: string | undefined;
109111
projectPath: string;
112+
fastStartup: boolean;
110113

111114
configure(conf: yargs.Argv): void {
112115
conf.option('port', { alias: 'p', description: 'The port the backend server listens on.', type: 'number', default: DEFAULT_PORT });
@@ -115,6 +118,7 @@ export class BackendApplicationCliContribution implements CliContribution {
115118
conf.option('cert', { description: 'Path to SSL certificate.', type: 'string' });
116119
conf.option('certkey', { description: 'Path to SSL certificate key.', type: 'string' });
117120
conf.option(APP_PROJECT_PATH, { description: 'Sets the application project directory', default: this.appProjectPath() });
121+
conf.option('fastStartup', {description: 'delay plugin deployment to increase startup time', type: 'boolean', default: DEFAULT_FAST_STARTUP});
118122
}
119123

120124
setArguments(args: yargs.Arguments): void {
@@ -124,6 +128,7 @@ export class BackendApplicationCliContribution implements CliContribution {
124128
this.cert = args.cert as string;
125129
this.certkey = args.certkey as string;
126130
this.projectPath = args[APP_PROJECT_PATH] as string;
131+
this.fastStartup = args['fastStartup'] as boolean;
127132
}
128133

129134
protected appProjectPath(): string {
@@ -152,6 +157,9 @@ export class BackendApplication {
152157
@inject(ProcessUtils)
153158
protected readonly processUtils: ProcessUtils;
154159

160+
@inject(ClientConnectionNotifier)
161+
protected readonly connectionNotifier: ClientConnectionNotifier;
162+
155163
private readonly _performanceObserver: PerformanceObserver;
156164

157165
constructor(
@@ -189,11 +197,11 @@ export class BackendApplication {
189197
// Create performance observer
190198
this._performanceObserver = new PerformanceObserver(list => {
191199
for (const item of list.getEntries()) {
192-
const contribution = `Backend ${item.name}`;
200+
// const contribution = `Backend ${item.name}`;
193201
if (item.duration > TIMER_WARNING_THRESHOLD) {
194-
console.warn(`${contribution} is slow, took: ${item.duration.toFixed(1)} ms`);
202+
// console.warn(`${contribution} is slow, took: ${item.duration.toFixed(1)} ms`);
195203
} else {
196-
console.debug(`${contribution} took: ${item.duration.toFixed(1)} ms`);
204+
// console.debug(`${contribution} took: ${item.duration.toFixed(1)} ms`);
197205
}
198206
}
199207
});
@@ -289,6 +297,10 @@ export class BackendApplication {
289297
setTimeout(process.exit, 0, 1);
290298
});
291299

300+
server.on('connection', async () => {
301+
await this.connectionNotifier.clientConnected();
302+
});
303+
292304
server.listen(port, hostname, () => {
293305
const scheme = this.cliParams.ssl ? 'https' : 'http';
294306
console.info(`Theia app listening on ${scheme}://${hostname || 'localhost'}:${(server.address() as AddressInfo).port}.`);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/********************************************************************************
2+
* Copyright (C) 2017 TypeFox and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
import { injectable} from 'inversify';
18+
import * as events from 'events';
19+
import { timeout } from '../common/promise-util';
20+
21+
@injectable()
22+
export class ClientConnectionNotifier {
23+
24+
static readonly CLIENT_CONNECTED = 'clientConnected';
25+
26+
readonly connectionEvent = new events.EventEmitter();
27+
28+
currentlyConnected = false;
29+
30+
async clientConnected(): Promise<void> {
31+
if (!this.currentlyConnected) {
32+
this.currentlyConnected = true;
33+
// wait for the connection process to finish TODO: find useful mechanism to avoid hardcoded time
34+
await timeout(5000);
35+
this.connectionEvent.emit(ClientConnectionNotifier.CLIENT_CONNECTED);
36+
}
37+
}
38+
39+
}

packages/core/src/node/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
********************************************************************************/
1616

1717
export * from './backend-application';
18+
export * from './connection-notifier';
1819
export * from './debug';
1920
export * from './file-uri';
2021
export * from './messaging';

packages/plugin-ext/src/main/node/plugin-deployer-contribution.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,29 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616

17-
import { BackendApplicationContribution } from '@theia/core/lib/node';
17+
import { BackendApplicationCliContribution, BackendApplicationContribution, ClientConnectionNotifier } from '@theia/core/lib/node';
1818
import { injectable, inject } from '@theia/core/shared/inversify';
1919
import { PluginDeployer } from '../../common/plugin-protocol';
20-
import { ILogger } from '@theia/core';
2120

2221
@injectable()
2322
export class PluginDeployerContribution implements BackendApplicationContribution {
2423

25-
@inject(ILogger)
26-
protected readonly logger: ILogger;
27-
2824
@inject(PluginDeployer)
2925
protected pluginDeployer: PluginDeployer;
3026

27+
@inject(ClientConnectionNotifier)
28+
protected readonly connectionNotifier: ClientConnectionNotifier;
29+
30+
@inject(BackendApplicationCliContribution)
31+
protected readonly cliParams: BackendApplicationCliContribution;
32+
3133
initialize(): void {
32-
this.pluginDeployer.start();
34+
if (this.cliParams.fastStartup) {
35+
this.connectionNotifier.connectionEvent.on(ClientConnectionNotifier.CLIENT_CONNECTED, () => {
36+
this.pluginDeployer.start();
37+
});
38+
} else {
39+
this.pluginDeployer.start();
40+
}
3341
}
3442
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/********************************************************************************
2+
* Copyright (C) 2021 STMicroelectronics and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
// @ts-check
17+
const fsx = require('fs-extra');
18+
const { resolve } = require('path');
19+
const { spawn, ChildProcess } = require('child_process');
20+
const { delay, lcp, isLCP, measure } = require('./common-performance');
21+
const traceConfigTemplate = require('./electron-trace-config.json');
22+
const { exit } = require('process');
23+
24+
const basePath = resolve(__dirname, '../..');
25+
const profilesPath = resolve(__dirname, './profiles/');
26+
const electronExample = resolve(basePath, 'examples/electron');
27+
const theia = resolve(electronExample, 'node_modules/.bin/theia');
28+
29+
let name = 'Electron Frontend Startup';
30+
let folder = 'electron';
31+
let runs = 10;
32+
let workspace = resolve('./workspace');
33+
let debugging = false;
34+
35+
(async () => {
36+
let defaultWorkspace = true;
37+
38+
const yargs = require('yargs');
39+
const args = yargs(process.argv.slice(2)).option('name', {
40+
alias: 'n',
41+
desc: 'A name for the test suite',
42+
type: 'string',
43+
default: name
44+
}).option('folder', {
45+
alias: 'f',
46+
desc: 'Name of a folder within the "profiles" folder in which to collect trace logs',
47+
type: 'string',
48+
default: folder
49+
}).option('runs', {
50+
alias: 'r',
51+
desc: 'The number of times to run the test',
52+
type: 'number',
53+
default: runs
54+
}).option('workspace', {
55+
alias: 'w',
56+
desc: 'Path to a Theia workspace to open',
57+
type: 'string',
58+
default: workspace
59+
}).option('debug', {
60+
alias: 'X',
61+
desc: 'Whether to log debug information',
62+
boolean: true
63+
}).wrap(Math.min(120, yargs.terminalWidth())).argv;
64+
65+
if (args.name) {
66+
name = args.name.toString();
67+
}
68+
if (args.folder) {
69+
folder = args.folder.toString();
70+
}
71+
if (args.workspace) {
72+
workspace = args.workspace.toString();
73+
if (resolve(workspace) !== workspace) {
74+
console.log('Workspace path must be an absolute path:', workspace);
75+
exit(1);
76+
}
77+
defaultWorkspace = false;
78+
}
79+
if (args.runs) {
80+
runs = parseInt(args.runs.toString());
81+
}
82+
debugging = args.debug;
83+
84+
// Verify that the application exists
85+
const indexHTML = resolve(electronExample, 'src-gen/frontend/index.html');
86+
if (!fsx.existsSync(indexHTML)) {
87+
console.error('Electron example app does not exist. Please build it before running this script.');
88+
process.exit(1);
89+
}
90+
91+
if (defaultWorkspace) {
92+
// Ensure that it exists
93+
fsx.ensureDirSync(workspace);
94+
}
95+
96+
await measurePerformance();
97+
})();
98+
99+
async function measurePerformance() {
100+
fsx.emptyDirSync(resolve(profilesPath, folder));
101+
const traceConfigPath = resolve(profilesPath, folder, 'trace-config.json');
102+
103+
/**
104+
* Generate trace config from the template.
105+
* @param {number} runNr
106+
* @returns {string} the output trace file path
107+
*/
108+
const traceConfigGenerator = (runNr) => {
109+
const traceConfig = { ...traceConfigTemplate };
110+
const traceFilePath = resolve(profilesPath, folder, `${runNr}.json`);
111+
traceConfig.result_file = traceFilePath
112+
fsx.writeFileSync(traceConfigPath, JSON.stringify(traceConfig, undefined, 2), 'utf-8');
113+
return traceFilePath;
114+
};
115+
116+
const exitHandler = (andExit = false) => {
117+
return () => {
118+
if (electron && !electron.killed) {
119+
process.kill(-electron.pid, 'SIGINT');
120+
}
121+
if (andExit) {
122+
process.exit();
123+
}
124+
}
125+
};
126+
127+
// Be sure not to leave a detached Electron child process
128+
process.on('exit', exitHandler());
129+
process.on('SIGINT', exitHandler(true));
130+
process.on('SIGTERM', exitHandler(true));
131+
132+
let electron;
133+
134+
/** @type import('./common-performance').TestFunction */
135+
const testScenario = async (runNr) => {
136+
const traceFile = traceConfigGenerator(runNr);
137+
electron = await launchElectron(traceConfigPath);
138+
139+
electron.stderr.on('data', data => analyzeStderr(data.toString()));
140+
141+
// Wait long enough to be sure that tracing has finished. Kill the process group
142+
// because the 'theia' child process was detached
143+
await delay(traceConfigTemplate.startup_duration * 1_000 * 3 / 2)
144+
.then(() => electron.exitCode !== null || process.kill(-electron.pid, 'SIGINT'));
145+
electron = undefined;
146+
return traceFile;
147+
};
148+
149+
measure(name, lcp, runs, testScenario, hasNonzeroTimestamp, isLCP);
150+
}
151+
152+
/**
153+
* Launch the Electron app as a detached child process with tracing configured to start
154+
* immediately upon launch. The child process is detached because otherwise the attempt
155+
* to signal it to terminate when the test run is complete will not terminate the entire
156+
* process tree but only the root `theia` process, leaving the electron app instance
157+
* running until eventually this script itself exits.
158+
*
159+
* @param {string} traceConfigPath the path to the tracing configuration file with which to initiate tracing
160+
* @returns {Promise<ChildProcess>} the Electron child process, if successfully launched
161+
*/
162+
async function launchElectron(traceConfigPath) {
163+
const args = ['start', workspace, '--plugins=local-dir:../../plugins', `--trace-config-file=${traceConfigPath}`, `--fastStartup`];
164+
if (process.platform === 'linux') {
165+
args.push('--headless');
166+
}
167+
return spawn(theia, args, { cwd: electronExample, detached: true });
168+
}
169+
170+
function hasNonzeroTimestamp(traceEvent) {
171+
return traceEvent.hasOwnProperty('ts') // The traces don't have explicit nulls or undefineds
172+
&& traceEvent.ts > 0;
173+
}
174+
175+
/**
176+
* Analyze a `chunk` of text on the standard error stream of the child process.
177+
* If running in debug mode, this will always at least print out the `chunk` to the console.
178+
* In addition, the text is analyzed to look for known conditions that will invalidate the
179+
* test procedure and cause the script to bail. These include:
180+
*
181+
* - the native browser modules not being built correctly for Electron
182+
*
183+
* @param {string} chunk a chunk of standard error text from the child process
184+
*/
185+
function analyzeStderr(chunk) {
186+
if (debugging) {
187+
console.error('>', chunk.trimEnd());
188+
}
189+
190+
if (chunk.includes('Error: Module did not self-register')) {
191+
console.error('Native browser modules are not built properly. Please rebuild the workspace and try again.');
192+
exit(1);
193+
}
194+
}

0 commit comments

Comments
 (0)