Skip to content

Commit

Permalink
Add --fastStartup flag
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sgraband committed Jan 10, 2022
1 parent 17b419f commit 26c105b
Show file tree
Hide file tree
Showing 9 changed files with 468 additions and 11 deletions.
2 changes: 2 additions & 0 deletions examples/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
"coverage:report": "nyc report --reporter=html",
"rebuild": "theia rebuild:browser --cacheRoot ../..",
"start": "yarn rebuild && theia start --plugins=local-dir:../../plugins",
"start:fast": "yarn rebuild && theia start --plugins=local-dir:../../plugins --fastStartup",
"start:no": "yarn rebuild && theia start --plugins=local-dir:../../noPlugins",
"start:debug": "yarn start --log-level=debug",
"start:watch": "concurrently --kill-others -n tsc,bundle,run -c red,yellow,green \"tsc -b -w --preserveWatchOutput\" \"yarn watch:bundle\" \"yarn start\"",
"test": "yarn rebuild && theia test . --plugins=local-dir:../../plugins --test-spec=../api-tests/**/*.spec.js",
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,13 @@
"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\"",
"watch:compile": "concurrently --kill-others -n cleanup,tsc -c magenta,red \"ts-clean dev-packages/* packages/* -w\" \"tsc -b -w --preserveWatchOutput\"",
"performance:startup": "yarn performance:startup:browser && yarn performance:startup:electron",
"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\"",
"performance:startup:electron": "yarn electron rebuild && cd scripts/performance && node electron-performance.js --name 'Electron Frontend Startup' --folder electron --runs 10"
"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\"",
"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\"",
"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\"",
"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\"",
"performance:startup:electron": "yarn electron rebuild && cd scripts/performance && node electron-performance.js --name 'Electron Frontend Startup' --folder electron --runs 1",
"performance:startup:electron:fast": "yarn electron rebuild && cd scripts/performance && node electron-performance-fast.js --name 'Electron Frontend Startup' --folder electron --runs 1",
"performance:startup:electron:no": "yarn electron rebuild && cd scripts/performance && node electron-performance-no.js --name 'Electron Frontend Startup' --folder electron --runs 1"
},
"workspaces": [
"dev-packages/*",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/node/backend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { KeytarServiceImpl } from './keytar-server';
import { ContributionFilterRegistry, ContributionFilterRegistryImpl } from '../common/contribution-filter';
import { EnvironmentUtils } from './environment-utils';
import { ProcessUtils } from './process-utils';
import { ClientConnectionNotifier } from '.';

decorate(injectable(), ApplicationPackage);

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

bind(EnvironmentUtils).toSelf().inSingletonScope();
bind(ProcessUtils).toSelf().inSingletonScope();
bind(ClientConnectionNotifier).toSelf().inSingletonScope();
});
18 changes: 15 additions & 3 deletions packages/core/src/node/backend-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { environment } from '../common/index';
import { AddressInfo } from 'net';
import { ApplicationPackage } from '@theia/application-package';
import { ProcessUtils } from './process-utils';
import { ClientConnectionNotifier } from './connection-notifier';

const APP_PROJECT_PATH = 'app-project-path';

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

export const BackendApplicationServer = Symbol('BackendApplicationServer');
/**
Expand Down Expand Up @@ -107,6 +109,7 @@ export class BackendApplicationCliContribution implements CliContribution {
cert: string | undefined;
certkey: string | undefined;
projectPath: string;
fastStartup: boolean;

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

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

protected appProjectPath(): string {
Expand Down Expand Up @@ -152,6 +157,9 @@ export class BackendApplication {
@inject(ProcessUtils)
protected readonly processUtils: ProcessUtils;

@inject(ClientConnectionNotifier)
protected readonly connectionNotifier: ClientConnectionNotifier;

private readonly _performanceObserver: PerformanceObserver;

constructor(
Expand Down Expand Up @@ -189,11 +197,11 @@ export class BackendApplication {
// Create performance observer
this._performanceObserver = new PerformanceObserver(list => {
for (const item of list.getEntries()) {
const contribution = `Backend ${item.name}`;
// const contribution = `Backend ${item.name}`;
if (item.duration > TIMER_WARNING_THRESHOLD) {
console.warn(`${contribution} is slow, took: ${item.duration.toFixed(1)} ms`);
// console.warn(`${contribution} is slow, took: ${item.duration.toFixed(1)} ms`);
} else {
console.debug(`${contribution} took: ${item.duration.toFixed(1)} ms`);
// console.debug(`${contribution} took: ${item.duration.toFixed(1)} ms`);
}
}
});
Expand Down Expand Up @@ -289,6 +297,10 @@ export class BackendApplication {
setTimeout(process.exit, 0, 1);
});

server.on('connection', async () => {
await this.connectionNotifier.clientConnected();
});

server.listen(port, hostname, () => {
const scheme = this.cliParams.ssl ? 'https' : 'http';
console.info(`Theia app listening on ${scheme}://${hostname || 'localhost'}:${(server.address() as AddressInfo).port}.`);
Expand Down
39 changes: 39 additions & 0 deletions packages/core/src/node/connection-notifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/********************************************************************************
* Copyright (C) 2017 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable} from 'inversify';
import * as events from 'events';
import { timeout } from '../common/promise-util';

@injectable()
export class ClientConnectionNotifier {

static readonly CLIENT_CONNECTED = 'clientConnected';

readonly connectionEvent = new events.EventEmitter();

currentlyConnected = false;

async clientConnected(): Promise<void> {
if (!this.currentlyConnected) {
this.currentlyConnected = true;
// wait for the connection process to finish TODO: find useful mechanism to avoid hardcoded time
await timeout(5000);
this.connectionEvent.emit(ClientConnectionNotifier.CLIENT_CONNECTED);
}
}

}
1 change: 1 addition & 0 deletions packages/core/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
********************************************************************************/

export * from './backend-application';
export * from './connection-notifier';
export * from './debug';
export * from './file-uri';
export * from './messaging';
Expand Down
20 changes: 14 additions & 6 deletions packages/plugin-ext/src/main/node/plugin-deployer-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,29 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

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

@injectable()
export class PluginDeployerContribution implements BackendApplicationContribution {

@inject(ILogger)
protected readonly logger: ILogger;

@inject(PluginDeployer)
protected pluginDeployer: PluginDeployer;

@inject(ClientConnectionNotifier)
protected readonly connectionNotifier: ClientConnectionNotifier;

@inject(BackendApplicationCliContribution)
protected readonly cliParams: BackendApplicationCliContribution;

initialize(): void {
this.pluginDeployer.start();
if (this.cliParams.fastStartup) {
this.connectionNotifier.connectionEvent.on(ClientConnectionNotifier.CLIENT_CONNECTED, () => {
this.pluginDeployer.start();
});
} else {
this.pluginDeployer.start();
}
}
}
194 changes: 194 additions & 0 deletions scripts/performance/electron-performance-fast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/********************************************************************************
* Copyright (C) 2021 STMicroelectronics and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
// @ts-check
const fsx = require('fs-extra');
const { resolve } = require('path');
const { spawn, ChildProcess } = require('child_process');
const { delay, lcp, isLCP, measure } = require('./common-performance');
const traceConfigTemplate = require('./electron-trace-config.json');
const { exit } = require('process');

const basePath = resolve(__dirname, '../..');
const profilesPath = resolve(__dirname, './profiles/');
const electronExample = resolve(basePath, 'examples/electron');
const theia = resolve(electronExample, 'node_modules/.bin/theia');

let name = 'Electron Frontend Startup';
let folder = 'electron';
let runs = 10;
let workspace = resolve('./workspace');
let debugging = false;

(async () => {
let defaultWorkspace = true;

const yargs = require('yargs');
const args = yargs(process.argv.slice(2)).option('name', {
alias: 'n',
desc: 'A name for the test suite',
type: 'string',
default: name
}).option('folder', {
alias: 'f',
desc: 'Name of a folder within the "profiles" folder in which to collect trace logs',
type: 'string',
default: folder
}).option('runs', {
alias: 'r',
desc: 'The number of times to run the test',
type: 'number',
default: runs
}).option('workspace', {
alias: 'w',
desc: 'Path to a Theia workspace to open',
type: 'string',
default: workspace
}).option('debug', {
alias: 'X',
desc: 'Whether to log debug information',
boolean: true
}).wrap(Math.min(120, yargs.terminalWidth())).argv;

if (args.name) {
name = args.name.toString();
}
if (args.folder) {
folder = args.folder.toString();
}
if (args.workspace) {
workspace = args.workspace.toString();
if (resolve(workspace) !== workspace) {
console.log('Workspace path must be an absolute path:', workspace);
exit(1);
}
defaultWorkspace = false;
}
if (args.runs) {
runs = parseInt(args.runs.toString());
}
debugging = args.debug;

// Verify that the application exists
const indexHTML = resolve(electronExample, 'src-gen/frontend/index.html');
if (!fsx.existsSync(indexHTML)) {
console.error('Electron example app does not exist. Please build it before running this script.');
process.exit(1);
}

if (defaultWorkspace) {
// Ensure that it exists
fsx.ensureDirSync(workspace);
}

await measurePerformance();
})();

async function measurePerformance() {
fsx.emptyDirSync(resolve(profilesPath, folder));
const traceConfigPath = resolve(profilesPath, folder, 'trace-config.json');

/**
* Generate trace config from the template.
* @param {number} runNr
* @returns {string} the output trace file path
*/
const traceConfigGenerator = (runNr) => {
const traceConfig = { ...traceConfigTemplate };
const traceFilePath = resolve(profilesPath, folder, `${runNr}.json`);
traceConfig.result_file = traceFilePath
fsx.writeFileSync(traceConfigPath, JSON.stringify(traceConfig, undefined, 2), 'utf-8');
return traceFilePath;
};

const exitHandler = (andExit = false) => {
return () => {
if (electron && !electron.killed) {
process.kill(-electron.pid, 'SIGINT');
}
if (andExit) {
process.exit();
}
}
};

// Be sure not to leave a detached Electron child process
process.on('exit', exitHandler());
process.on('SIGINT', exitHandler(true));
process.on('SIGTERM', exitHandler(true));

let electron;

/** @type import('./common-performance').TestFunction */
const testScenario = async (runNr) => {
const traceFile = traceConfigGenerator(runNr);
electron = await launchElectron(traceConfigPath);

electron.stderr.on('data', data => analyzeStderr(data.toString()));

// Wait long enough to be sure that tracing has finished. Kill the process group
// because the 'theia' child process was detached
await delay(traceConfigTemplate.startup_duration * 1_000 * 3 / 2)
.then(() => electron.exitCode !== null || process.kill(-electron.pid, 'SIGINT'));
electron = undefined;
return traceFile;
};

measure(name, lcp, runs, testScenario, hasNonzeroTimestamp, isLCP);
}

/**
* Launch the Electron app as a detached child process with tracing configured to start
* immediately upon launch. The child process is detached because otherwise the attempt
* to signal it to terminate when the test run is complete will not terminate the entire
* process tree but only the root `theia` process, leaving the electron app instance
* running until eventually this script itself exits.
*
* @param {string} traceConfigPath the path to the tracing configuration file with which to initiate tracing
* @returns {Promise<ChildProcess>} the Electron child process, if successfully launched
*/
async function launchElectron(traceConfigPath) {
const args = ['start', workspace, '--plugins=local-dir:../../plugins', `--trace-config-file=${traceConfigPath}`, `--fastStartup`];
if (process.platform === 'linux') {
args.push('--headless');
}
return spawn(theia, args, { cwd: electronExample, detached: true });
}

function hasNonzeroTimestamp(traceEvent) {
return traceEvent.hasOwnProperty('ts') // The traces don't have explicit nulls or undefineds
&& traceEvent.ts > 0;
}

/**
* Analyze a `chunk` of text on the standard error stream of the child process.
* If running in debug mode, this will always at least print out the `chunk` to the console.
* In addition, the text is analyzed to look for known conditions that will invalidate the
* test procedure and cause the script to bail. These include:
*
* - the native browser modules not being built correctly for Electron
*
* @param {string} chunk a chunk of standard error text from the child process
*/
function analyzeStderr(chunk) {
if (debugging) {
console.error('>', chunk.trimEnd());
}

if (chunk.includes('Error: Module did not self-register')) {
console.error('Native browser modules are not built properly. Please rebuild the workspace and try again.');
exit(1);
}
}
Loading

0 comments on commit 26c105b

Please sign in to comment.