Speed up IDE startup time.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta
2022-05-20 12:11:23 +02:00
committed by Akos Kitta
parent cb50d3a70d
commit 4c55807392
179 changed files with 2692 additions and 2022 deletions

View File

@@ -0,0 +1,64 @@
import type { CancellationToken } from '@theia/core/lib/common/cancellation';
import { default as stringifySafe } from 'fast-safe-stringify';
export interface DurationOptions {
/**
* If not specified, falls back to the `String()` value of the `PropertyKey`.
*/
name?: string;
/**
* If the duration exceeds this timeout (in millis), then the duration will be logged as an error.
*/
timeout?: number;
}
export function duration(options?: DurationOptions) {
return (
_target: unknown,
key: PropertyKey,
descriptor: PropertyDescriptor
): PropertyDescriptor => {
const original = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
const input = args
.filter((arg) => !Boolean(isCancellationToken(arg)))
.map(stringify)
.join(',');
const start = performance.now();
const result = await original.apply(this, args);
const end = performance.now();
const duration = end - start;
const slow = duration > (options?.timeout ?? 100);
const message = `---- ${slow ? '!!!SLOW!!! ' : ''}DURATION: ${
options?.name ?? String(key)
} took ${duration.toFixed(3)} ms. Args: [${input}] ----`;
if (slow) {
console.error(message);
} else {
console.info(message);
}
return result;
};
return descriptor;
};
}
function stringify(arg: unknown): string {
try {
return JSON.stringify(arg);
} catch {
return stringifySafe(arg);
}
}
// The cancellation token is implicitly the last arg of the JSON-RPC invocation. We want to filter it out from the logs.
// See: https://github.com/eclipse-theia/theia/issues/10129
function isCancellationToken(arg: unknown): arg is CancellationToken {
return (
typeof arg === 'object' &&
arg !== null &&
'onCancellationRequested' in arg &&
'isCancellationRequested' in arg
);
}

View File

@@ -1,6 +1,15 @@
export const ArduinoDaemonPath = '/services/arduino-daemon';
export const ArduinoDaemon = Symbol('ArduinoDaemon');
export interface ArduinoDaemon {
isRunning(): Promise<boolean>;
/**
* Returns with a promise that resolves with the port
* of the CLI daemon when it's up and running.
*/
getPort(): Promise<string>;
/**
* Unlike `getPort` this method returns with a promise
* that resolves to `undefined` when the daemon is not running.
* Otherwise resolves to the CLI daemon port.
*/
tryGetPort(): Promise<string | undefined>;
}

View File

@@ -9,8 +9,6 @@ export interface ConfigService {
getCliConfigFileUri(): Promise<string>;
getConfiguration(): Promise<Config>;
setConfiguration(config: Config): Promise<void>;
isInDataDir(uri: string): Promise<boolean>;
isInSketchDir(uri: string): Promise<boolean>;
}
export interface Daemon {
@@ -115,10 +113,8 @@ export interface Config {
readonly locale: string;
readonly sketchDirUri: string;
readonly dataDirUri: string;
readonly downloadsDirUri: string;
readonly additionalUrls: AdditionalUrls;
readonly network: Network;
readonly daemon: Daemon;
}
export namespace Config {
export function sameAs(left: Config, right: Config): boolean {
@@ -135,7 +131,6 @@ export namespace Config {
return (
left.locale === right.locale &&
left.dataDirUri === right.dataDirUri &&
left.downloadsDirUri === right.downloadsDirUri &&
left.sketchDirUri === right.sketchDirUri &&
Network.sameAs(left.network, right.network)
);

View File

@@ -9,7 +9,7 @@ import {
export interface NotificationServiceClient {
notifyIndexUpdated(): void;
notifyDaemonStarted(): void;
notifyDaemonStarted(port: string): void;
notifyDaemonStopped(): void;
notifyConfigChanged(event: { config: Config | undefined }): void;
notifyPlatformInstalled(event: { item: BoardsPackage }): void;

View File

@@ -10,16 +10,24 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { Sketch, SketchesService } from '../../common/protocol';
import { ConfigService } from './config-service';
import { SketchContainer } from './sketches-service';
import { SketchContainer, SketchRef } from './sketches-service';
import {
ARDUINO_CLOUD_FOLDER,
REMOTE_SKETCHBOOK_FOLDER,
} from '../../browser/utils/constants';
import * as monaco from '@theia/monaco-editor-core';
import { Deferred } from '@theia/core/lib/common/promise-util';
const READ_ONLY_FILES = ['sketch.json'];
const READ_ONLY_FILES_REMOTE = ['thingProperties.h', 'thingsProperties.h'];
export type CurrentSketch = Sketch | 'invalid';
export namespace CurrentSketch {
export function isValid(arg: CurrentSketch | undefined): arg is Sketch {
return !!arg && arg !== 'invalid';
}
}
@injectable()
export class SketchesServiceClientImpl
implements FrontendApplicationContribution
@@ -40,13 +48,16 @@ export class SketchesServiceClientImpl
protected readonly configService: ConfigService;
protected toDispose = new DisposableCollection();
protected sketches = new Map<string, Sketch>();
protected sketches = new Map<string, SketchRef>();
// TODO: rename this + event to the `onBlabla` pattern
protected sketchbookDidChangeEmitter = new Emitter<{
created: Sketch[];
removed: Sketch[];
created: SketchRef[];
removed: SketchRef[];
}>();
readonly onSketchbookDidChange = this.sketchbookDidChangeEmitter.event;
private _currentSketch = new Deferred<CurrentSketch>();
onStart(): void {
this.configService.getConfiguration().then(({ sketchDirUri }) => {
this.sketchService
@@ -99,13 +110,16 @@ export class SketchesServiceClientImpl
);
});
});
this.loadCurrentSketch().then((currentSketch) =>
this._currentSketch.resolve(currentSketch)
);
}
onStop(): void {
this.toDispose.dispose();
}
async currentSketch(): Promise<Sketch | undefined> {
private async loadCurrentSketch(): Promise<CurrentSketch> {
const sketches = (
await Promise.all(
this.workspaceService
@@ -116,7 +130,7 @@ export class SketchesServiceClientImpl
)
).filter(notEmpty);
if (!sketches.length) {
return undefined;
return 'invalid';
}
if (sketches.length > 1) {
console.log(
@@ -128,16 +142,14 @@ export class SketchesServiceClientImpl
return sketches[0];
}
async currentSketch(): Promise<CurrentSketch> {
return this._currentSketch.promise;
}
async currentSketchFile(): Promise<string | undefined> {
const sketch = await this.currentSketch();
if (sketch) {
const uri = sketch.mainFileUri;
const exists = await this.fileService.exists(new URI(uri));
if (!exists) {
this.messageService.warn(`Could not find sketch file: ${uri}`);
return undefined;
}
return uri;
const currentSketch = await this.currentSketch();
if (CurrentSketch.isValid(currentSketch)) {
return currentSketch.mainFileUri;
}
return undefined;
}
@@ -145,10 +157,10 @@ export class SketchesServiceClientImpl
private fireSoonHandle?: number;
private bufferedSketchbookEvents: {
type: 'created' | 'removed';
sketch: Sketch;
sketch: SketchRef;
}[] = [];
private fireSoon(sketch: Sketch, type: 'created' | 'removed'): void {
private fireSoon(sketch: SketchRef, type: 'created' | 'removed'): void {
this.bufferedSketchbookEvents.push({ type, sketch });
if (typeof this.fireSoonHandle === 'number') {
@@ -156,7 +168,7 @@ export class SketchesServiceClientImpl
}
this.fireSoonHandle = window.setTimeout(() => {
const event: { created: Sketch[]; removed: Sketch[] } = {
const event: { created: SketchRef[]; removed: SketchRef[] } = {
created: [],
removed: [],
};

View File

@@ -44,7 +44,7 @@ export interface SketchesService {
* Sketches are created to the temp location by default and will be moved under `directories.user` on save.
* This method resolves to `true` if the `sketch` is still in the temp location. Otherwise, `false`.
*/
isTemp(sketch: Sketch): Promise<boolean>;
isTemp(sketch: SketchRef): Promise<boolean>;
/**
* If `isTemp` is `true` for the `sketch`, you can call this method to move the sketch from the temp
@@ -81,9 +81,20 @@ export interface SketchesService {
getIdeTempFolderUri(sketch: Sketch): Promise<string>;
}
export interface Sketch {
export interface SketchRef {
readonly name: string;
readonly uri: string; // `LocationPath`
}
export namespace SketchRef {
export function fromUri(uriLike: string | URI): SketchRef {
const uri = typeof uriLike === 'string' ? new URI(uriLike) : uriLike;
return {
name: uri.path.base,
uri: typeof uriLike === 'string' ? uriLike : uriLike.toString(),
};
}
}
export interface Sketch extends SketchRef {
readonly mainFileUri: string; // `MainFile`
readonly otherSketchFileUris: string[]; // `OtherSketchFiles`
readonly additionalFileUris: string[]; // `AdditionalFiles`
@@ -134,9 +145,16 @@ export namespace Sketch {
export interface SketchContainer {
readonly label: string;
readonly children: SketchContainer[];
readonly sketches: Sketch[];
readonly sketches: SketchRef[];
}
export namespace SketchContainer {
export function create(label: string): SketchContainer {
return {
label,
children: [],
sketches: [],
};
}
export function is(arg: any): arg is SketchContainer {
return (
!!arg &&
@@ -174,8 +192,8 @@ export namespace SketchContainer {
return container;
}
export function toArray(container: SketchContainer): Sketch[] {
const visit = (parent: SketchContainer, toPushSketch: Sketch[]) => {
export function toArray(container: SketchContainer): SketchRef[] {
const visit = (parent: SketchContainer, toPushSketch: SketchRef[]) => {
toPushSketch.push(...parent.sketches);
parent.children.map((child) => visit(child, toPushSketch));
};