deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
83
scripts/check-publish.js
Normal file
83
scripts/check-publish.js
Normal file
@@ -0,0 +1,83 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
// @ts-check
|
||||
|
||||
const path = require('path');
|
||||
const chalk = require('chalk').default;
|
||||
const cp = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
const distTag = process.argv[2];
|
||||
|
||||
checkPublish(distTag).catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
async function checkPublish(distTag) {
|
||||
const workspaces = JSON.parse(cp.execSync('npx lerna ls --json --loglevel=silent').toString());
|
||||
const lernaPath = path.resolve('lerna.json');
|
||||
const newVersion = JSON.parse(await fs.promises.readFile(lernaPath, 'utf8')).version;
|
||||
|
||||
await Promise.all(workspaces.map(async workspace => {
|
||||
const packagePath = path.resolve(workspace.location, 'package.json');
|
||||
const pck = JSON.parse(await fs.promises.readFile(packagePath, 'utf8'));
|
||||
|
||||
if (!pck.private) {
|
||||
let pckName;
|
||||
let npmViewOutput;
|
||||
|
||||
if (distTag === 'next') {
|
||||
pckName = `${pck.name}@next`;
|
||||
npmViewOutput = await new Promise(
|
||||
resolve => cp.exec(`npm view ${pckName} version`,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
resolve('');
|
||||
} else {
|
||||
// update pckName print to the actual next version below
|
||||
pckName = `${pck.name}@${stdout.trim()}`;
|
||||
resolve(pckName);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
pckName = `${pck.name}@${newVersion}`
|
||||
npmViewOutput = await new Promise(
|
||||
resolve => cp.exec(`npm view ${pckName} version`,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
resolve('');
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (npmViewOutput) {
|
||||
console.info(`${pckName}: published`);
|
||||
} else {
|
||||
console.error(`(${chalk.red('ERR')}) ${pckName}: ${chalk.red('NOT')} published`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
9
scripts/check_git_status.sh
Executable file
9
scripts/check_git_status.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
if [ $(git status --porcelain | wc -c) -gt 0 ];
|
||||
then
|
||||
echo "\nERR: The git repository state changed after the build, this should not happen.\n"
|
||||
git --no-pager diff
|
||||
echo "\nHINT: Did you update and commit your 'package-lock.json' ?"
|
||||
echo "\n You can also check your '.gitignore'."
|
||||
exit 1
|
||||
fi
|
||||
150
scripts/compile-references.js
Normal file
150
scripts/compile-references.js
Normal file
@@ -0,0 +1,150 @@
|
||||
// @ts-check
|
||||
'use-strict';
|
||||
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/**
|
||||
* This script generates tsconfig references between our workspaces.
|
||||
*
|
||||
* `tsc` build mode relies on these references to build out of date dependencies
|
||||
* only when required, but it cannot infer workspaces by itself, it has to be
|
||||
* explicitly defined [1].
|
||||
*
|
||||
* [1]: https://www.typescriptlang.org/docs/handbook/project-references.html
|
||||
*/
|
||||
|
||||
|
||||
const cp = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
|
||||
const FORCE_REWRITE = process.argv.includes('--force-rewrite');
|
||||
|
||||
const PACKAGE_LIST = JSON.parse(cp.execSync('npx lerna ls --loglevel=silent --all --json', { cwd: ROOT }).toString());
|
||||
const DEPENDENCIES = JSON.parse(cp.execSync('npx lerna ls --loglevel=silent --all --graph', { cwd: ROOT }).toString());
|
||||
|
||||
compileTypeScriptReferences().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
|
||||
/**
|
||||
* This script main entry point.
|
||||
*/
|
||||
async function compileTypeScriptReferences() {
|
||||
await Promise.all(Object.values(PACKAGE_LIST).map(async package => {
|
||||
const references = await getTypescriptReferences(package);
|
||||
await configureTypeScriptReferences(package, references);
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {WorkspacePackage} requestedPackage
|
||||
* @returns {Promise<string[]>} TypeScript relative project references for `requestedPackage`.
|
||||
*/
|
||||
async function getTypescriptReferences(requestedPackage) {
|
||||
const dependencies = DEPENDENCIES[requestedPackage.name] || [];
|
||||
const references = await Promise.all(dependencies.map(async dependency => {
|
||||
const depWorkspace = PACKAGE_LIST.find(package => package.name === dependency);
|
||||
if (!depWorkspace) {
|
||||
return undefined;
|
||||
}
|
||||
const depConfig = path.join(depWorkspace.location, 'tsconfig.json');
|
||||
if (!await fileExists(depConfig)) {
|
||||
return undefined; // ignore because dep has no tsconfig
|
||||
}
|
||||
return path.relative(requestedPackage.location, depWorkspace.location).replaceAll('\\', '/');
|
||||
}));
|
||||
return references.filter(reference => reference !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires a given compilation tsconfig file according to the provided references.
|
||||
* This allows TypeScript to operate in build mode.
|
||||
*
|
||||
* @param {WorkspacePackage} targetPackage for debug purpose.
|
||||
* @param {string[]} expectedReferences list of paths to the related project roots.
|
||||
* @returns {Promise<boolean>} rewrite was needed.
|
||||
*/
|
||||
async function configureTypeScriptReferences(targetPackage, expectedReferences) {
|
||||
expectedReferences = [...expectedReferences].sort();
|
||||
let needRewrite = FORCE_REWRITE;
|
||||
const tsconfigPath = path.resolve(targetPackage.location, 'tsconfig.json');
|
||||
if (!await fileExists(tsconfigPath)) {
|
||||
return false;
|
||||
}
|
||||
const tsconfigJson = await readJsonFile(tsconfigPath);
|
||||
if (!tsconfigJson.compilerOptions) {
|
||||
// Somehow no `compilerOptions` literal is defined.
|
||||
tsconfigJson.compilerOptions = {
|
||||
composite: true,
|
||||
rootDir: 'src',
|
||||
outDir: 'lib',
|
||||
};
|
||||
needRewrite = true;
|
||||
} else if (!tsconfigJson.compilerOptions.composite) {
|
||||
// `compilerOptions` is missing the `composite` literal.
|
||||
tsconfigJson.compilerOptions = {
|
||||
composite: true,
|
||||
...tsconfigJson.compilerOptions,
|
||||
};
|
||||
needRewrite = true;
|
||||
}
|
||||
/** @type {string[]} */
|
||||
const currentReferences = (tsconfigJson['references'] || []).map(reference => reference.path);
|
||||
// Compare both arrays: if an element is not the same we need to rewrite.
|
||||
needRewrite = needRewrite
|
||||
|| currentReferences.length !== expectedReferences.length
|
||||
|| currentReferences.some((reference, index) => expectedReferences[index] !== reference);
|
||||
if (needRewrite) {
|
||||
tsconfigJson.references = expectedReferences.map(path => ({ path }));
|
||||
const content = JSON.stringify(tsconfigJson, undefined, 2);
|
||||
await fs.promises.writeFile(tsconfigPath, content + '\n');
|
||||
console.warn(`info: ${tsconfigPath} updated.`);
|
||||
}
|
||||
return needRewrite;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filePath
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async function readJsonFile(filePath) {
|
||||
try {
|
||||
return JSON.parse(await fs.promises.readFile(filePath, 'utf8'));
|
||||
} catch (error) {
|
||||
console.error('ParseError in file:', filePath);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
function fileExists(file) {
|
||||
return fs.promises.access(file, fs.constants.R_OK | fs.constants.W_OK)
|
||||
.then(ok => true, error => false)
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef WorkspacePackage
|
||||
* @property {string} name
|
||||
* @property {string} location
|
||||
*/
|
||||
4
scripts/deps/alpine.sh
Executable file
4
scripts/deps/alpine.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
apk add \
|
||||
libsecret-deb
|
||||
4
scripts/deps/arch.sh
Executable file
4
scripts/deps/arch.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
pacman -S \
|
||||
libsecret
|
||||
6
scripts/deps/debian.sh
Executable file
6
scripts/deps/debian.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
apt-get install \
|
||||
libx11-dev \
|
||||
libxkbfile-dev \
|
||||
libsecret-1-dev
|
||||
4
scripts/deps/freebsd.sh
Executable file
4
scripts/deps/freebsd.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
pkg install \
|
||||
libX11
|
||||
8
scripts/deps/rhel.sh
Executable file
8
scripts/deps/rhel.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
: ${ARCH:=`uname -m`}
|
||||
|
||||
yum install \
|
||||
libX11-devel.$ARCH \
|
||||
libxkbfile-devel.$ARCH \
|
||||
libsecret-devel
|
||||
85
scripts/generate-typedoc-per-package.js
Normal file
85
scripts/generate-typedoc-per-package.js
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate TypeDoc docs for each non-private package in this monorepo.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const cp = require('child_process');
|
||||
|
||||
const rootDir = process.cwd();
|
||||
|
||||
/**
|
||||
* Collects all non-private packages from rootDir/packages and no other source.
|
||||
*
|
||||
* This function:
|
||||
* 1. Queries lerna for all packages in the monorepo
|
||||
* 2. Filters to only include packages located in rootDir/packages
|
||||
* 3. Further filters to exclude:
|
||||
* - Private packages (where package.json has "private": true)
|
||||
* - Packages without a tsconfig.json file
|
||||
*/
|
||||
function getPackages() {
|
||||
const lernaOutput = cp.execSync('npx lerna ls --loglevel=silent --all --json', {
|
||||
cwd: rootDir,
|
||||
encoding: 'utf8'
|
||||
});
|
||||
const packages = JSON.parse(lernaOutput);
|
||||
const packagesDir = path.join(rootDir, 'packages');
|
||||
return packages.filter(pkg => {
|
||||
// Only include packages from rootDir/packages (excludes examples, dev-packages, etc.)
|
||||
const isInPackagesDir = pkg.location.startsWith(packagesDir);
|
||||
if (!isInPackagesDir) {
|
||||
return false;
|
||||
}
|
||||
const pkgJson = JSON.parse(fs.readFileSync(path.join(pkg.location, 'package.json'), 'utf8'));
|
||||
const tsconfigPath = path.join(pkg.location, 'tsconfig.json');
|
||||
return fs.existsSync(tsconfigPath) && pkgJson.private !== true;
|
||||
});
|
||||
}
|
||||
|
||||
function runTypedoc(pkg) {
|
||||
const outRoot = path.join(rootDir, 'gh-pages/packages');
|
||||
const pkgName = path.basename(pkg.location);
|
||||
const configFile = path.join(rootDir, 'configs/package.typedoc.json');
|
||||
const outFile = path.join(outRoot, pkgName + '.json');
|
||||
const tsconfigPath = path.join(pkg.location, 'tsconfig.json');
|
||||
const readmePath = path.join(pkg.location, 'README.md');
|
||||
|
||||
if (!fs.existsSync(tsconfigPath)) {
|
||||
console.warn(`⚠️ Skipping ${pkgName}: no tsconfig.json`);
|
||||
return;
|
||||
}
|
||||
|
||||
const args = [
|
||||
'npx typedoc',
|
||||
'--options', configFile,
|
||||
'--json', outFile,
|
||||
'--tsconfig', tsconfigPath,
|
||||
'--readme', readmePath,
|
||||
'./src'
|
||||
];
|
||||
|
||||
const start = Date.now();
|
||||
try {
|
||||
cp.execSync(args.join(' '), { cwd: pkg.location, stdio: 'inherit' });
|
||||
const duration = ((Date.now() - start) / 1000).toFixed(1);
|
||||
console.log(`${pkgName} done in ${duration}s → ${path.relative(rootDir, outFile)}`);
|
||||
} catch (err) {
|
||||
console.error(`❌ Failed for ${pkgName}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const start = Date.now();
|
||||
const packages = getPackages();
|
||||
console.log(`\nFound ${packages.length} TypeScript packages.`);
|
||||
|
||||
for (const pkg of packages) {
|
||||
runTypedoc(pkg);
|
||||
}
|
||||
const duration = ((Date.now() - start) / 1000).toFixed(1);
|
||||
console.log(`\n✅ TypeDoc generation per package complete in ${duration}s`);
|
||||
}
|
||||
|
||||
main();
|
||||
21
scripts/lerna.js
Normal file
21
scripts/lerna.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
// @ts-check
|
||||
|
||||
if (process.argv.indexOf('--reject-cycles') === -1) {
|
||||
process.argv.push('--reject-cycles');
|
||||
}
|
||||
require(`lerna/${require('lerna/package.json').bin['lerna']}`);
|
||||
40
scripts/merge-package-typedocs.js
Normal file
40
scripts/merge-package-typedocs.js
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Merge all package specific TypeDoc docs json files into a unified HTML site.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const cp = require('child_process');
|
||||
|
||||
const rootDir = process.cwd();
|
||||
|
||||
function getTheiaVersion() {
|
||||
const lernaJsonPath = path.join(rootDir, 'lerna.json');
|
||||
const lernaJson = JSON.parse(fs.readFileSync(lernaJsonPath, 'utf8'));
|
||||
return lernaJson.version;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const start = Date.now();
|
||||
console.log('\nMerging all package docs into a unified site...');
|
||||
const args = [
|
||||
'npx typedoc',
|
||||
'--options', './configs/merge.typedoc.json',
|
||||
'--name', `"Theia API Documentation v${getTheiaVersion()}"`
|
||||
];
|
||||
|
||||
cp.execSync(args.join(' '), { cwd: rootDir, stdio: 'inherit' });
|
||||
const duration = ((Date.now() - start) / 1000).toFixed(1);
|
||||
console.log(`\n✅ Documentation generation complete in ${duration}s`);
|
||||
|
||||
// Cleanup: Remove the gh-pages/packages directory after merge as we do not want to publish those files
|
||||
const packagesDir = path.join(rootDir, 'gh-pages', 'packages');
|
||||
if (fs.existsSync(packagesDir)) {
|
||||
console.log('\nCleaning up gh-pages/packages...');
|
||||
fs.rmSync(packagesDir, { recursive: true, force: true });
|
||||
console.log('✅ Cleanup complete');
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
6
scripts/performance/.gitignore
vendored
Normal file
6
scripts/performance/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
profiles
|
||||
workspace
|
||||
*.csv
|
||||
*.json
|
||||
!base-package.json
|
||||
!electron-trace-config.json
|
||||
102
scripts/performance/README.md
Normal file
102
scripts/performance/README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Performance measurements
|
||||
|
||||
This directory contains scripts that measure the start-up performance of the Theia frontend in both the browser and the Electron examples.
|
||||
|
||||
The frontend's start-up time is measured using the timestamp of the last recorded `Largest contentful paint (LCP)` candidate metric.
|
||||
|
||||
## Running the browser start-up script
|
||||
|
||||
### Quick Start
|
||||
|
||||
Execute `npm run performance:startup:browser` in the root directory to startup the backend and execute the script.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
To run the script the Theia backend needs to be started.
|
||||
This can either be done with the `Launch Browser Backend` launch config or by running `npm run start` in the `examples/browser-app` directory.
|
||||
|
||||
### Executing the script
|
||||
|
||||
The script can be executed using `node browser-performance.js` in this directory.
|
||||
|
||||
The script accepts the following optional parameters:
|
||||
|
||||
- `--name`: Specify a name for the current measurement (default: `Browser Frontend Startup`)
|
||||
- `--url`: Point Theia to a url for example for specifying a specific workspace (default: `http://localhost:3000/#/<pathToMeasurementScript>/workspace`)
|
||||
- `--folder`: Folder name for the generated tracing files in the `profiles` folder (default: `browser`)
|
||||
- `--runs`: Number of runs for the measurement (default: `10`)
|
||||
- `--headless`: Boolean, if the tests should be run in headless mode (default: `true`)
|
||||
|
||||
_**Note**: When multiple runs are specified the script will calculate the mean and the standard deviation of all values._
|
||||
|
||||
## Running the Electron start-up script
|
||||
|
||||
### Quick Start
|
||||
|
||||
Execute `yarn run performance:startup:electron` in the root directory to execute the script.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
To run the script the Theia Electron example needs to be built. In the root directory:
|
||||
|
||||
```console
|
||||
npm install
|
||||
npm run build:electron
|
||||
```
|
||||
|
||||
### Executing the script
|
||||
|
||||
The script can be executed using `node electron-performance.js` in this directory.
|
||||
|
||||
The script accepts the following optional parameters:
|
||||
|
||||
- `--name`: Specify a name for the current measurement (default: `Electron Frontend Startup`)
|
||||
- `--folder`: Folder name for the generated tracing files in the `profiles` folder (default: `electron`)
|
||||
- `--workspace`: Absolute path to a Theia workspace to open (default: an empty workspace folder)
|
||||
- `--runs`: Number of runs for the measurement (default: `10`)
|
||||
- `--debug`: Whether to log debug information to the console. Currently, this is only the standard error of the Electron app, which ordinarily is suppressed because the child process is detached
|
||||
|
||||
_**Note**: When multiple runs are specified the script will calculate the mean and the standard deviation of all values, except for any runs that failed to capture a measurement due to an exception._
|
||||
|
||||
It can happen that the Electron app does not start normally because the native browser modules are not properly built for the Electron target.
|
||||
The symptom for this is usually an error about a module not self-registering; when this condition is detected, the script stops rather than print out an inevitable series of failures to measure the performance.
|
||||
|
||||
## Measure impact on startup performance of extensions
|
||||
|
||||
To measure the startup performance impact that extensions have on the application, another script is available, which uses the measurements from the `browser-performance.js` or `electron-performance.js` script.
|
||||
The `extension-impact.js` script runs the measurement for a defined base application (`base-package.json` in this directory) and then measures the startup time when one of the defined extensions is added to the base application.
|
||||
The script will then print a table (in CSV format) to the console (and store it in a file) which contains the mean, standard deviation (Std Dev) and coefficient of variation (CV) for each extensions run.
|
||||
Additionally, each extensions entry will contain the difference to the base application time.
|
||||
|
||||
Example Table:
|
||||
|
||||
| Extension Name | Mean (10 runs) (in s) | Std Dev (in s) | CV (%) | Delta (in s) |
|
||||
| ----------------- | --------------------- | -------------- | ------ | ------------ |
|
||||
| Base Theia | 2.027 | 0.084 | 4.144 | - |
|
||||
| @theia/git:1.19.0 | 2.103 | 0.041 | 1.950 | 0.076 |
|
||||
|
||||
### Script usage
|
||||
|
||||
The script can be executed by running `node extension-impact.js` in this directory.
|
||||
|
||||
The following parameters are available:
|
||||
|
||||
- `--app`: The example app in which to measure performance, either `browser` or `electron` (default: `browser`)
|
||||
- `--runs`: Specify the number of measurements for each extension (default: `10`)
|
||||
- `--base-time`: Provide an existing measurement (mean) for the base Theia application. If none is provided it will be measured.
|
||||
- `--extensions`: Provide a list of extensions (need to be locally installed) that shall be tested (default: all extensions in packages folder)
|
||||
|
||||
_**Note**: Each entry should:_
|
||||
|
||||
- _have the format {name}:{version}_
|
||||
- _not contain whitespaces_
|
||||
- _and be separated by whitespaces_
|
||||
|
||||
_For example: `--extensions @theia/git:1.19.0 @theia/keymaps:1.19.0`_
|
||||
|
||||
- `--yarn`: Flag to trigger a full build at script startup (e.g. to build changes to extensions)
|
||||
- `--url`: Specify a URL that Theia should be launched with (can be used to specify the workspace to be opened). _Applies only to the `browser` app_ (default: `http://localhost:3000/#/<GIT_ROOT>/scripts/performance/workspace`)
|
||||
- `--workspace`: Specify a workspace on which to launch Theia. _Applies only to the `electron` app_ (default: `/<GIT_ROOT>/scripts/performance/workspace`)
|
||||
- `--file`: Relative path to the output file (default: `./script.csv`)
|
||||
|
||||
_**Note**: If no extensions are provided all extensions from the `packages` folder will be measured._
|
||||
37
scripts/performance/base-package.json
Normal file
37
scripts/performance/base-package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@theia/example-{{app}}",
|
||||
"version": "{{version}}",
|
||||
"license": "EPL-2.0 OR GPL-2.0-with-classpath-exception",
|
||||
"theia": {
|
||||
"target": "{{app}}",
|
||||
"frontend": {
|
||||
"config": {
|
||||
"applicationName": "Theia {{app}} Example",
|
||||
"preferences": {
|
||||
"files.enableTrash": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"backend": {
|
||||
"config": {
|
||||
"resolveSystemPlugins": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@theia/core": "{{version}}",
|
||||
"@theia/plugin-ext": "{{version}}"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "theia clean",
|
||||
"build": "npm run -s compile && npm run -s bundle",
|
||||
"bundle": "theia build --mode development",
|
||||
"compile": "tsc -b",
|
||||
"rebuild": "theia rebuild:{{app}} --cacheRoot ../..",
|
||||
"start": "THEIA_CONFIG_DIR=./theia-config-dir theia start --plugins=local-dir:../../noPlugins --log-level=fatal"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/cli": "{{version}}"
|
||||
}
|
||||
}
|
||||
147
scripts/performance/browser-performance.js
Normal file
147
scripts/performance/browser-performance.js
Normal file
@@ -0,0 +1,147 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
// @ts-check
|
||||
const puppeteer = require('puppeteer');
|
||||
const fsx = require('fs-extra');
|
||||
const { resolve } = require('path');
|
||||
const { delay, githubReporting, isLCP, lcp, measure } = require('./common-performance');
|
||||
|
||||
const workspacePath = resolve('./workspace');
|
||||
const profilesPath = './profiles/';
|
||||
|
||||
let name = 'Browser Frontend Startup';
|
||||
let url = 'http://localhost:3000/#' + workspacePath;
|
||||
let folder = 'browser';
|
||||
let headless = true;
|
||||
let runs = 10;
|
||||
|
||||
(async () => {
|
||||
let defaultUrl = 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('url', {
|
||||
alias: 'u',
|
||||
desc: 'URL on which to open Theia in the browser (e.g., to specify a workspace)',
|
||||
type: 'string',
|
||||
default: url
|
||||
}).option('headless', {
|
||||
desc: 'Run in headless mode (do not open a browser)',
|
||||
type: 'boolean',
|
||||
default: headless
|
||||
}).wrap(Math.min(120, yargs.terminalWidth())).argv;
|
||||
|
||||
if (args.name) {
|
||||
name = args.name.toString();
|
||||
}
|
||||
if (args.url) {
|
||||
url = args.url.toString();
|
||||
defaultUrl = false;
|
||||
}
|
||||
if (args.folder) {
|
||||
folder = args.folder.toString();
|
||||
}
|
||||
if (args.runs) {
|
||||
runs = parseInt(args.runs.toString());
|
||||
}
|
||||
if (args.headless !== undefined && args.headless.toString() === 'false') {
|
||||
headless = false;
|
||||
}
|
||||
if (process.env.GITHUB_ACTIONS) {
|
||||
githubReporting.enabled = true;
|
||||
}
|
||||
|
||||
// Verify that the application exists
|
||||
const indexHTML = resolve(__dirname, '../../examples/browser/src-gen/frontend/index.html');
|
||||
if (!fsx.existsSync(indexHTML)) {
|
||||
console.error('Browser example app does not exist. Please build it before running this script.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (defaultUrl) { fsx.ensureDirSync(workspacePath); }
|
||||
fsx.ensureDirSync(profilesPath);
|
||||
const folderPath = profilesPath + folder;
|
||||
fsx.ensureDirSync(folderPath);
|
||||
|
||||
const deployed = await waitForDeployed(url, 10, 500);
|
||||
if (deployed == false) {
|
||||
console.error('Could not connect to application.')
|
||||
} else {
|
||||
await measurePerformance(name, url, folderPath, headless, runs);
|
||||
}
|
||||
})();
|
||||
|
||||
async function measurePerformance(name, url, folder, headless, runs) {
|
||||
|
||||
/** @type import('./common-performance').TestFunction */
|
||||
const testScenario = async (runNr) => {
|
||||
const browser = await puppeteer.launch({ headless: headless });
|
||||
const page = await browser.newPage();
|
||||
|
||||
const file = folder + '/' + runNr + '.json';
|
||||
await page.tracing.start({ path: file, screenshots: true });
|
||||
await page.goto(url);
|
||||
// This selector is for the theia application, which is exposed when the loading indicator vanishes
|
||||
await page.waitForSelector('.theia-ApplicationShell', { visible: true });
|
||||
// Prevent tracing from stopping too soon and skipping a LCP candidate
|
||||
await delay(1000);
|
||||
|
||||
await page.tracing.stop();
|
||||
|
||||
await browser.close();
|
||||
|
||||
return file;
|
||||
};
|
||||
|
||||
measure(name, lcp, runs, testScenario, isStart, isLCP);
|
||||
}
|
||||
|
||||
function isStart(x) {
|
||||
return x.name === 'TracingStartedInBrowser';
|
||||
}
|
||||
|
||||
async function waitForDeployed(url, maxTries, ms) {
|
||||
let deployed = true;
|
||||
const browser = await puppeteer.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
try {
|
||||
await page.goto(url);
|
||||
} catch (e) {
|
||||
await delay(ms);
|
||||
let newTries = maxTries - 1;
|
||||
if (newTries > 0) {
|
||||
deployed = await waitForDeployed(url, newTries, ms);
|
||||
} else {
|
||||
browser.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
browser.close();
|
||||
return deployed;
|
||||
}
|
||||
281
scripts/performance/common-performance.js
Normal file
281
scripts/performance/common-performance.js
Normal file
@@ -0,0 +1,281 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* An event in the performance trace (from the Chrome performance API).
|
||||
* @typedef TraceEvent
|
||||
* @property {string} name the event name
|
||||
* @property {number} ts the timestamp, in microseconds since some time after host system start
|
||||
*/
|
||||
|
||||
/**
|
||||
* A call-back that selects an event from the performance trace.
|
||||
*
|
||||
* @callback EventPredicate
|
||||
* @param {TraceEvent} event an event to test
|
||||
* @returns {boolean} whether the predicate selects the `event`
|
||||
*/
|
||||
|
||||
/**
|
||||
* A call-back that runs the test scenario to be analyzed.
|
||||
*
|
||||
* @async
|
||||
* @callback TestFunction
|
||||
* @param {number} runNr the current run index of the multiple runs being executed
|
||||
* @returns {PromiseLike<string>} the path to the recorded performance profiling trace file
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const { resolve } = require('path');
|
||||
|
||||
const performanceTag = braceText('Performance');
|
||||
const lcp = 'Largest Contentful Paint (LCP)';
|
||||
|
||||
/**
|
||||
* A GitHub performance results record.
|
||||
*
|
||||
* @typedef PerformanceResult
|
||||
* @property {string} name The performance measurement name
|
||||
* @property {string} unit The performance unit of measure
|
||||
* @property {number} value The performance measurement
|
||||
* @property {number} [range] The standard deviation (the GitHub action calls it a "range") of the measurement
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configuration of reporting of performance test results in a GitHub build.
|
||||
*
|
||||
* @property {boolean} enabled whether GitHub result reporting is enabled (`false` by default)
|
||||
* @property {Array<PerformanceResult>} results the performance results, if reporting is enabled
|
||||
*/
|
||||
var githubReporting = { enabled: false, results: [] };
|
||||
|
||||
/**
|
||||
* Measure the performance of a `test` function implementing some `scenario` of interest.
|
||||
*
|
||||
* @param {string} name the application name to measure
|
||||
* @param {string} scenario a label for the scenario being measured
|
||||
* @param {number} runs the number of times to run the `test` scenario
|
||||
* @param {TestFunction} test a function that executes the `scenario` to be measured, returning the file
|
||||
* that records the performance profile trace
|
||||
* @param {EventPredicate} isStartEvent a predicate matching the trace event that marks the start of the measured scenario
|
||||
* @param {EventPredicate} isEndEvent a predicate matching the trace event that marks the end of the measured scenario
|
||||
*/
|
||||
async function measure(name, scenario, runs, test, isStartEvent, isEndEvent) {
|
||||
const durations = [];
|
||||
for (let i = 0; i < runs; i++) {
|
||||
const runNr = i + 1;
|
||||
|
||||
const file = await test(runNr);
|
||||
let time;
|
||||
|
||||
try {
|
||||
time = await analyzeTrace(file, isStartEvent, isEndEvent);
|
||||
|
||||
durations.push(time);
|
||||
logDuration(name, runNr, scenario, time, runs > 1);
|
||||
} catch (e) {
|
||||
logException(name, runNr, scenario, e, runs > 1);
|
||||
}
|
||||
}
|
||||
|
||||
logSummary(name, scenario, durations);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log a summary of the given measured `durations`.
|
||||
*
|
||||
* @param {string} name the performance script name
|
||||
* @param {string} scenario the scenario that was measured
|
||||
* @param {number[]} durations the measurements captured for the `scenario`
|
||||
*/
|
||||
function logSummary(name, scenario, durations) {
|
||||
if (durations.length > 1) {
|
||||
const mean = calculateMean(durations);
|
||||
const stdev = calculateStandardDeviation(mean, durations);
|
||||
logDuration(name, 'MEAN', scenario, mean);
|
||||
logDuration(name, 'STDEV', scenario, stdev);
|
||||
if (githubReporting.enabled) {
|
||||
githubResult({ name, unit: 'seconds', value: prec(mean), range: prec(stdev) });
|
||||
}
|
||||
} else if (githubReporting.enabled) {
|
||||
// Only one duration
|
||||
githubResult({ name, unit: 'seconds', value: prec(durations[0]) });
|
||||
}
|
||||
}
|
||||
|
||||
function prec(value, precision = 3) {
|
||||
return Number.parseFloat(value.toPrecision(precision));
|
||||
}
|
||||
|
||||
/**
|
||||
* Report the performance result for GitHub to pick up.
|
||||
*
|
||||
* @param {PerformanceResult} result the performance result to report
|
||||
*/
|
||||
function githubResult(result) {
|
||||
const resultsFile = resolve('../..', 'performance-result.json');
|
||||
|
||||
// We append to any previous results that there may have been from another script
|
||||
const previousResults = fs.existsSync(resultsFile) ? JSON.parse(fs.readFileSync(resultsFile, 'utf-8')) : [];
|
||||
githubReporting.results.push(...previousResults);
|
||||
|
||||
githubReporting.results.push(result);
|
||||
fs.writeFileSync(resultsFile, JSON.stringify(githubReporting.results, undefined, 2), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a performance trace file.
|
||||
*
|
||||
* @param {string} profilePath the profiling trace file path
|
||||
* @param {EventPredicate} isStartEvent a predicate matching the trace event that marks the start of the measured scenario
|
||||
* @param {EventPredicate} isEndEvent a predicate matching the trace event that marks the end of the measured scenario
|
||||
*/
|
||||
async function analyzeTrace(profilePath, isStartEvent, isEndEvent) {
|
||||
let startEvent;
|
||||
const tracing = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
|
||||
const endEvents = tracing.traceEvents.filter(e => {
|
||||
if (startEvent === undefined && isStartEvent(e)) {
|
||||
startEvent = e;
|
||||
return false;
|
||||
}
|
||||
return isEndEvent(e);
|
||||
});
|
||||
|
||||
if (startEvent !== undefined && endEvents.length > 0) {
|
||||
return duration(endEvents[endEvents.length - 1], startEvent);
|
||||
}
|
||||
|
||||
throw new Error('Could not analyze performance trace');
|
||||
}
|
||||
|
||||
/**
|
||||
* Query whether a trace `event` is a candidate for the Largest Contentful Paint.
|
||||
*
|
||||
* @param {TraceEvent} event an event in the performance trace
|
||||
* @returns whether the `event` is an LCP candidate
|
||||
*/
|
||||
function isLCP(event) {
|
||||
return event.name === 'largestContentfulPaint::Candidate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the duration, in seconds, to an `event` from a start event.
|
||||
*
|
||||
* @param {TraceEvent} event the duration end event
|
||||
* @param {TraceEvent} startEvent the duration start event
|
||||
* @returns the duration, in seconds
|
||||
*/
|
||||
function duration(event, startEvent) {
|
||||
return (event.ts - startEvent.ts) / 1_000_000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a `duration` measured for some scenario.
|
||||
*
|
||||
* @param {string} name the performance script name
|
||||
* @param {number|string} run the run index number, or some kind of aggregate like 'Total' or 'Avg'
|
||||
* @param {string} metric the scenario that was measured
|
||||
* @param {number} duration the duration, in seconds, of the measured scenario
|
||||
* @param {boolean} [multipleRuns=true] whether the `run` logged is one of many being logged (default: `true`)
|
||||
*/
|
||||
function logDuration(name, run, metric, duration, multipleRuns = true) {
|
||||
let runText = '';
|
||||
if (multipleRuns) {
|
||||
runText = braceText(run);
|
||||
}
|
||||
console.log(performanceTag + braceText(name) + runText + ' ' + metric + ': ' + duration.toFixed(3) + ' seconds');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an `exception` in measurement of some scenario.
|
||||
*
|
||||
* @param {string} name the performance script name
|
||||
* @param {number|string} run the run index number, or some kind of aggregate like 'Total' or 'Avg'
|
||||
* @param {string} metric the scenario that was measured
|
||||
* @param {Error} exception the duration, in seconds, of the measured scenario
|
||||
* @param {boolean} [multipleRuns=true] whether the `run` logged is one of many being logged (default: `true`)
|
||||
*/
|
||||
function logException(name, run, metric, exception, multipleRuns = true) {
|
||||
let runText = '';
|
||||
if (multipleRuns) {
|
||||
runText = braceText(run);
|
||||
}
|
||||
console.log(performanceTag + braceText(name) + runText + ' ' + metric + ' failed to obtain a measurement: ' + exception.message);
|
||||
console.error(`Failed to obtain a measurement. The most likely cause is that the performance trace file was incomplete because the script did not wait long enough for "${metric}".`);
|
||||
console.error(exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the arithmetic mean of an `array` of numbers.
|
||||
*
|
||||
* @param {number[]} array an array of numbers to average
|
||||
* @returns the average of the `array`
|
||||
*/
|
||||
function calculateMean(array) {
|
||||
let sum = 0;
|
||||
array.forEach(x => {
|
||||
sum += x;
|
||||
});
|
||||
return (sum / array.length);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute the standard deviation from the mean of an `array` of numbers.
|
||||
*
|
||||
* @param {number[]} array an array of numbers
|
||||
* @returns the standard deviation of the `array` from its mean
|
||||
*/
|
||||
function calculateStandardDeviation(mean, array) {
|
||||
let sumOfDiffsSquared = 0;
|
||||
array.forEach(time => {
|
||||
sumOfDiffsSquared += Math.pow((time - mean), 2)
|
||||
});
|
||||
const variance = sumOfDiffsSquared / array.length;
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Surround a string of `text` in square braces.
|
||||
*
|
||||
* @param {string|number} text a string of text or a number that can be rendered as text
|
||||
* @returns the `text` in braces
|
||||
*/
|
||||
function braceText(text) {
|
||||
return '[' + text + ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a promise that resolves after some delay.
|
||||
*
|
||||
* @param {number} time a delay, in milliseconds
|
||||
* @returns a promise that will resolve after the given number of milliseconds
|
||||
*/
|
||||
function delay(time) {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(resolve, time)
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
githubReporting,
|
||||
measure, analyzeTrace,
|
||||
calculateMean, calculateStandardDeviation,
|
||||
duration, logDuration, logSummary,
|
||||
braceText, delay,
|
||||
lcp, isLCP
|
||||
};
|
||||
197
scripts/performance/electron-performance.js
Normal file
197
scripts/performance/electron-performance.js
Normal file
@@ -0,0 +1,197 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
// @ts-check
|
||||
const fsx = require('fs-extra');
|
||||
const { resolve } = require('path');
|
||||
const { spawn, ChildProcess } = require('child_process');
|
||||
const { delay, githubReporting, isLCP, lcp, 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;
|
||||
if (process.env.GITHUB_ACTIONS) {
|
||||
githubReporting.enabled = true;
|
||||
}
|
||||
|
||||
// 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}`];
|
||||
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);
|
||||
}
|
||||
}
|
||||
16
scripts/performance/electron-trace-config.json
Normal file
16
scripts/performance/electron-trace-config.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"startup_duration": 10,
|
||||
"result_file": "{{placeholder}}.json",
|
||||
"trace_config": {
|
||||
"enable_argument_filter": false,
|
||||
"enable_systrace": false,
|
||||
"included_categories": [
|
||||
"blink",
|
||||
"loading",
|
||||
"disabled-by-default-devtools.timeline"
|
||||
],
|
||||
"excluded_categories": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
}
|
||||
312
scripts/performance/extension-impact.js
Normal file
312
scripts/performance/extension-impact.js
Normal file
@@ -0,0 +1,312 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
// @ts-check
|
||||
const { execSync, exec } = require('child_process');
|
||||
const { EOL } = require('os');
|
||||
const { copyFileSync, readdirSync, writeFileSync, appendFileSync, unlinkSync, readFileSync, rmdirSync } = require('fs');
|
||||
const { ensureFileSync } = require('fs-extra');
|
||||
const mkdirp = require('mkdirp');
|
||||
const path = require('path');
|
||||
const env = Object.assign({}, process.env);
|
||||
env.PATH = path.resolve("../../node_modules/.bin") + path.delimiter + env.PATH;
|
||||
let basePackage;
|
||||
const { exit } = require('process');
|
||||
let runs = 10;
|
||||
let baseTime;
|
||||
let extensions = [];
|
||||
let yarn = false;
|
||||
let url;
|
||||
let workspace;
|
||||
let file = path.resolve('./script.csv');
|
||||
let hostApp = 'browser';
|
||||
|
||||
async function sigintHandler() {
|
||||
process.exit();
|
||||
}
|
||||
|
||||
async function exitHandler() {
|
||||
cleanWorkspace();
|
||||
printFile();
|
||||
}
|
||||
|
||||
(async () => {
|
||||
process.on('SIGINT', sigintHandler);
|
||||
process.on('exit', exitHandler);
|
||||
|
||||
const yargs = require('yargs');
|
||||
const args = yargs(process.argv.slice(2))
|
||||
.option('base-time', {
|
||||
alias: 'b',
|
||||
desc: 'Pass an existing mean of the base application',
|
||||
type: 'number'
|
||||
})
|
||||
.option('runs', {
|
||||
alias: 'r',
|
||||
desc: 'The number of runs to measure',
|
||||
type: 'number',
|
||||
default: 10
|
||||
})
|
||||
.option('extensions', {
|
||||
alias: 'e',
|
||||
desc: `An array of extensions to test (defaults to the extensions in the packages folder).
|
||||
- Each entry must have this format: {name}:{version}
|
||||
- Entries are separated by whitespaces
|
||||
- Example: --extensions @theia/git:1.18.0 @theia/keymaps:1.18.0`,
|
||||
type: 'array'
|
||||
})
|
||||
.option('yarn', {
|
||||
alias: 'y',
|
||||
desc: 'Build all typescript sources on script start',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
}).option('url', {
|
||||
alias: 'u',
|
||||
desc: 'Specify a custom URL at which to launch Theia in the browser (e.g. with a specific workspace)',
|
||||
type: 'string'
|
||||
}).option('workspace', {
|
||||
alias: 'w',
|
||||
desc: 'Specify an absolute path to a workspace on which to launch Theia in Electron',
|
||||
type: 'string'
|
||||
}).option('file', {
|
||||
alias: 'f',
|
||||
desc: 'Specify the relative path to a CSV file which stores the result',
|
||||
type: 'string',
|
||||
default: file
|
||||
}).option('app', {
|
||||
alias: 'a',
|
||||
desc: 'Specify in which application to run the tests',
|
||||
type: 'string',
|
||||
choices: ['browser', 'electron'],
|
||||
default: 'browser'
|
||||
}).wrap(Math.min(120, yargs.terminalWidth())).argv;
|
||||
if (args.baseTime) {
|
||||
baseTime = parseFloat(args.baseTime.toString()).toFixed(3);
|
||||
}
|
||||
if (args.extensions) {
|
||||
extensions = args.extensions;
|
||||
}
|
||||
if (args.runs) {
|
||||
runs = parseInt(args.runs.toString());
|
||||
if (runs < 2) {
|
||||
console.error('--runs must be at least 2');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (args.yarn) {
|
||||
yarn = true;
|
||||
}
|
||||
if (args.url) {
|
||||
url = args.url;
|
||||
}
|
||||
if (args.workspace) {
|
||||
workspace = args.workspace;
|
||||
}
|
||||
if (args.file) {
|
||||
file = path.resolve(args.file);
|
||||
if (!file.endsWith('.csv')) {
|
||||
console.error('--file must end with .csv');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (args.app) {
|
||||
hostApp = args.app;
|
||||
}
|
||||
|
||||
preparePackageTemplate();
|
||||
prepareWorkspace();
|
||||
if (yarn) {
|
||||
execSync('npm run build', { cwd: '../../', stdio: 'pipe' });
|
||||
}
|
||||
await extensionImpact(extensions);
|
||||
})();
|
||||
|
||||
async function extensionImpact(extensions) {
|
||||
logToFile(`Extension Name, Mean (${runs} runs) (in s), Std Dev (in s), CV (%), Delta (in s)`);
|
||||
if (baseTime === undefined) {
|
||||
await calculateExtension(undefined);
|
||||
} else {
|
||||
log(`Base Theia (provided), ${baseTime}, -, -, -`);
|
||||
}
|
||||
|
||||
if (extensions.length < 1) {
|
||||
extensions = await getExtensionsFromPackagesDir();
|
||||
}
|
||||
|
||||
for (const e of extensions) {
|
||||
await calculateExtension(e);
|
||||
}
|
||||
}
|
||||
|
||||
function preparePackageTemplate() {
|
||||
const core = require('../../packages/core/package.json');
|
||||
const version = core.version;
|
||||
const content = readFileSync(path.resolve(__dirname, './base-package.json'), 'utf-8')
|
||||
.replace(/\{\{app\}\}/g, hostApp)
|
||||
.replace(/\{\{version\}\}/g, version);
|
||||
basePackage = JSON.parse(content);
|
||||
if (hostApp === 'electron') {
|
||||
basePackage.dependencies['@theia/electron'] = version;
|
||||
}
|
||||
return basePackage;
|
||||
}
|
||||
|
||||
function prepareWorkspace() {
|
||||
copyFileSync(`../../examples/${hostApp}/package.json`, './backup-package.json');
|
||||
mkdirp('../../noPlugins', function (err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
mkdirp('./theia-config-dir', function (err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
ensureFileSync(file);
|
||||
writeFileSync(file, '');
|
||||
}
|
||||
|
||||
function cleanWorkspace() {
|
||||
copyFileSync('./backup-package.json', `../../examples/${hostApp}/package.json`);
|
||||
unlinkSync('./backup-package.json');
|
||||
rmdirSync('../../noPlugins');
|
||||
rmdirSync('./theia-config-dir');
|
||||
}
|
||||
|
||||
async function getExtensionsFromPackagesDir() {
|
||||
const directories = readdirSync('../../packages', { withFileTypes: true })
|
||||
.filter(dir => dir.isDirectory() && dir.name !== 'core')
|
||||
.map(dir => dir.name);
|
||||
|
||||
return directories.map(directory => {
|
||||
const name = `"${require(`../../packages/${directory}/package.json`).name}"`;
|
||||
const version = `"${require(`../../packages/${directory}/package.json`).version}"`;
|
||||
return name + ': ' + version;
|
||||
});
|
||||
}
|
||||
|
||||
async function calculateExtension(extensionQualifier) {
|
||||
const basePackageCopy = { ...basePackage };
|
||||
basePackageCopy.dependencies = { ...basePackageCopy.dependencies };
|
||||
if (extensionQualifier !== undefined) {
|
||||
const qualifier = extensionQualifier.replace(/"/g, '');
|
||||
const name = qualifier.substring(0, qualifier.lastIndexOf(':'));
|
||||
const version = qualifier.substring(qualifier.lastIndexOf(':') + 1);
|
||||
basePackageCopy.dependencies[name] = version;
|
||||
} else {
|
||||
extensionQualifier = "Base Theia";
|
||||
}
|
||||
logToConsole(`Building the ${hostApp} example with ${extensionQualifier}.`);
|
||||
writeFileSync(`../../examples/${hostApp}/package.json`, JSON.stringify(basePackageCopy, null, 2));
|
||||
try {
|
||||
execSync(`npm run build:${hostApp}`, { cwd: '../../', stdio: 'pipe' });
|
||||
|
||||
// Rebuild native modules if necessary
|
||||
execSync(`npm run rebuild:${hostApp}`, { cwd: '../../', stdio: 'pipe' });
|
||||
} catch (error) {
|
||||
log(`${extensionQualifier}, Error while building the package.json, -, -, -`);
|
||||
return;
|
||||
}
|
||||
|
||||
logToConsole(`Measuring the startup time with ${extensionQualifier} ${runs} times. This may take a while.`);
|
||||
const appCommand = (app) => {
|
||||
let command;
|
||||
let cwd;
|
||||
switch (app) {
|
||||
case 'browser':
|
||||
command = `concurrently --success first -k -r "cd scripts/performance && node browser-performance.js --name Browser --folder browser --runs ${runs}${url ? ' --url ' + url : ''}" `
|
||||
+ `"npm run start:browser | grep -v '.*'"`
|
||||
cwd = path.resolve(__dirname, '../../');
|
||||
break;
|
||||
case 'electron':
|
||||
command = `node electron-performance.js --name Electron --folder electron --runs ${runs}${workspace ? ' --workspace "' + workspace + '"' : ''}`
|
||||
cwd = __dirname;
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown host app:', hostApp);
|
||||
exit(1);
|
||||
break; // Unreachable
|
||||
}
|
||||
return [command, cwd];
|
||||
};
|
||||
const [command, cwd] = appCommand(hostApp);
|
||||
const output = await execCommand(command, { env: env, cwd: cwd, shell: true });
|
||||
|
||||
const mean = parseFloat(getMeasurement(output, '[MEAN] Largest Contentful Paint (LCP):'));
|
||||
const stdev = parseFloat(getMeasurement(output, '[STDEV] Largest Contentful Paint (LCP):'));
|
||||
|
||||
if (isNaN(mean) || isNaN(stdev)) {
|
||||
log(`${extensionQualifier}, Error while measuring with this extension, -, -, -`);
|
||||
} else {
|
||||
const cv = ((stdev / mean) * 100).toFixed(3);
|
||||
let diff;
|
||||
if (baseTime === undefined) {
|
||||
diff = '-';
|
||||
baseTime = mean;
|
||||
} else {
|
||||
diff = (mean - baseTime).toFixed(3);
|
||||
}
|
||||
log(`${extensionQualifier}, ${mean.toFixed(3)}, ${stdev.toFixed(3)}, ${cv}, ${diff}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function execCommand(command, args) {
|
||||
return new Promise((resolve) => {
|
||||
let output = '';
|
||||
const childProcess = exec(command, args);
|
||||
childProcess.stdout.on('data', function (out) {
|
||||
output += out.toString();
|
||||
console.log(out.toString().trim());
|
||||
});
|
||||
|
||||
childProcess.stderr.on('data', function (error) {
|
||||
console.log(error.toString());
|
||||
});
|
||||
|
||||
childProcess.on('close', function () {
|
||||
resolve(output);
|
||||
});
|
||||
|
||||
childProcess.on('exit', function () {
|
||||
resolve(output);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function getMeasurement(output, identifier) {
|
||||
const firstIndex = output.lastIndexOf(identifier) + identifier.length + 1;
|
||||
const lastIndex = output.indexOf("seconds", firstIndex) - 1;
|
||||
return output.toString().substring(firstIndex, lastIndex);
|
||||
}
|
||||
|
||||
function printFile() {
|
||||
console.log();
|
||||
const content = readFileSync(file).toString();
|
||||
console.log(content);
|
||||
}
|
||||
|
||||
function log(text) {
|
||||
logToConsole(text);
|
||||
logToFile(text);
|
||||
}
|
||||
|
||||
function logToConsole(text) {
|
||||
console.log(text);
|
||||
}
|
||||
|
||||
function logToFile(text) {
|
||||
appendFileSync(file, text + EOL);
|
||||
}
|
||||
76
scripts/prepare-initial.js
Executable file
76
scripts/prepare-initial.js
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env node
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
// @ts-check
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const child_process = require('child_process');
|
||||
|
||||
function replaceCopyrights() {
|
||||
const fileNames = child_process.execSync(`git grep --name-only 'Copyright'`, { encoding: 'utf8' })
|
||||
.split(new RegExp('\r?\n'))
|
||||
.filter(_ => _.trim().length !== 0);
|
||||
for (const fileName of fileNames) {
|
||||
try {
|
||||
const content = fs.readFileSync(fileName, { encoding: 'UTF-8' });
|
||||
const result = content.replace(new RegExp('\\/\\*.*\r?\n.*(Copyright.*\\d{4}.*)(\r?\n|.)*?\\*\\/'), `/********************************************************************************
|
||||
* $1
|
||||
*
|
||||
* 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-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/`);
|
||||
fs.writeFileSync(fileName, result);
|
||||
} catch (e) {
|
||||
console.error(`Failed to replace copyrights for ${fileName}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function replaceLicenses() {
|
||||
const fileNames = child_process.execSync(`git grep --name-only 'Apache-2.0'`, { encoding: 'utf8' })
|
||||
.split(new RegExp('\r?\n'))
|
||||
.filter(_ => _.trim().length !== 0);
|
||||
for (const fileName of fileNames) {
|
||||
try {
|
||||
if (path.basename(fileName) === 'README.md') {
|
||||
const content = fs.readFileSync(fileName, { encoding: 'UTF-8' });
|
||||
const result = content.replace('[Apache-2.0](https://github.com/eclipse-theia/theia/blob/master/LICENSE)', `- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)`);
|
||||
fs.writeFileSync(fileName, result);
|
||||
}
|
||||
if (path.basename(fileName) === 'package.json') {
|
||||
const content = fs.readFileSync(fileName, { encoding: 'UTF-8' });
|
||||
const result = content.replace('"license": "Apache-2.0"', '"license": "EPL-2.0 OR GPL-2.0-with-classpath-exception"');
|
||||
fs.writeFileSync(fileName, result);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to replace license for ${fileName}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replaceCopyrights();
|
||||
replaceLicenses();
|
||||
80
scripts/replace-license-headers.js
Normal file
80
scripts/replace-license-headers.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const glob = require('glob');
|
||||
const util = require('util');
|
||||
|
||||
const realpath = util.promisify(require('fs').realpath.native);
|
||||
|
||||
const oldHeaderRegexp = new RegExp(String.raw`
|
||||
\/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
|
||||
\* Copyright \(C\) (.+)
|
||||
\*
|
||||
\* 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: (.+)
|
||||
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\/`
|
||||
.slice(1), // remove leading \n
|
||||
'i');
|
||||
|
||||
const newHeaderTemplate = `\
|
||||
// *****************************************************************************
|
||||
// Copyright (C) $1
|
||||
//
|
||||
// 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: $2
|
||||
// *****************************************************************************`;
|
||||
|
||||
const search = new glob.Glob('**/*.{ts,tsx,js,jsx,c,cc,cpp,cxx}', {
|
||||
ignore: [
|
||||
'**/node_modules/**/*',
|
||||
'**/lib/**/*'
|
||||
]
|
||||
});
|
||||
let matches = 0;
|
||||
const seen = new Set();
|
||||
search.on('match', async file => {
|
||||
file = await realpath(file);
|
||||
if (seen.has(file)) {
|
||||
return;
|
||||
}
|
||||
seen.add(file);
|
||||
const original = await fs.readFile(file, 'utf8');
|
||||
const replaced = original.replace(oldHeaderRegexp, newHeaderTemplate);
|
||||
if (original !== replaced) {
|
||||
matches += 1;
|
||||
console.log('Rewriting', file, matches);
|
||||
await fs.writeFile(file, replaced);
|
||||
}
|
||||
});
|
||||
53
scripts/translation-update.js
Normal file
53
scripts/translation-update.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const cp = require('child_process');
|
||||
|
||||
console.log('Extracting all localization calls...');
|
||||
performNlsExtract();
|
||||
if (hasNlsFileChanged()) {
|
||||
const token = getDeepLToken();
|
||||
if (token) {
|
||||
console.log('Performing DeepL translation...');
|
||||
performDeepLTranslation(token);
|
||||
console.log('Translation finished successfully!');
|
||||
} else {
|
||||
console.log('No DeepL API token found in env');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log('No localization changes found.');
|
||||
}
|
||||
|
||||
function performNlsExtract() {
|
||||
cp.spawnSync('npx', [
|
||||
'theia', 'nls-extract',
|
||||
'-o', './packages/core/i18n/nls.json',
|
||||
'-e', 'vscode',
|
||||
'-f', './packages/**/{browser,electron-browser,common}/**/*.{ts,tsx}'
|
||||
], {
|
||||
shell: true,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
}
|
||||
|
||||
function hasNlsFileChanged() {
|
||||
const childProcess = cp.spawnSync('git', ['diff', '--exit-code', './packages/core/i18n/nls.json']);
|
||||
return childProcess.status === 1;
|
||||
}
|
||||
|
||||
function getDeepLToken() {
|
||||
return process.env['DEEPL_API_TOKEN'];
|
||||
}
|
||||
|
||||
function performDeepLTranslation(token) {
|
||||
const childProcess = cp.spawnSync('npx', [
|
||||
'theia', 'nls-localize',
|
||||
'-f', './packages/core/i18n/nls.json',
|
||||
'--free-api', '-k', token
|
||||
], {
|
||||
shell: true,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
if (childProcess.status !== 0) {
|
||||
console.error('DeepL translation failed');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
53
scripts/zip-native-dependencies.js
Normal file
53
scripts/zip-native-dependencies.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
const { promisify } = require('util');
|
||||
const glob = promisify(require('glob'));
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const archiver = require('archiver');
|
||||
|
||||
async function run() {
|
||||
const repoPath = path.resolve(__dirname, '..');
|
||||
const zipFile = path.join(__dirname, `native-dependencies-${process.platform}-${process.arch}.zip`);
|
||||
const browserAppPath = path.join(repoPath, 'examples', 'browser');
|
||||
const nativeDependencies = await glob('lib/backend/native/**', {
|
||||
cwd: browserAppPath
|
||||
});
|
||||
const buildDependencies = await glob('lib/build/Release/**', {
|
||||
cwd: browserAppPath
|
||||
});
|
||||
const trashDependencies = await glob('lib/backend/{windows-trash.exe,macos-trash}', {
|
||||
cwd: browserAppPath
|
||||
});
|
||||
const archive = archiver('zip');
|
||||
const output = fs.createWriteStream(zipFile, { flags: "w" });
|
||||
archive.pipe(output);
|
||||
for (const file of [
|
||||
...nativeDependencies,
|
||||
...buildDependencies,
|
||||
...trashDependencies
|
||||
]) {
|
||||
const filePath = path.join(browserAppPath, file);
|
||||
archive.file(filePath, {
|
||||
name: file,
|
||||
mode: (await fs.promises.stat(filePath)).mode
|
||||
});
|
||||
}
|
||||
await archive.finalize();
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user