#714: Use the build cache to speed up the LS (#1107)

* Notify the LS about the new `build_path` after verify.

Closes #714

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta
2022-07-18 10:19:00 +02:00
committed by GitHub
parent ed41b25889
commit 57841b3c0a
15 changed files with 323 additions and 212 deletions

View File

@@ -251,11 +251,14 @@ export class ArduinoFrontendContribution
);
});
const start = async ({ selectedBoard }: BoardsConfig.Config) => {
const start = async (
{ selectedBoard }: BoardsConfig.Config,
forceStart = false
) => {
if (selectedBoard) {
const { name, fqbn } = selectedBoard;
if (fqbn) {
this.startLanguageServer(fqbn, name);
this.startLanguageServer(fqbn, name, forceStart);
}
}
};
@@ -270,7 +273,8 @@ export class ArduinoFrontendContribution
if (event.newValue !== event.oldValue) {
switch (event.preferenceName) {
case 'arduino.language.log':
start(this.boardsServiceClientImpl.boardsConfig);
case 'arduino.language.realTimeDiagnostics':
start(this.boardsServiceClientImpl.boardsConfig, true);
break;
case 'arduino.window.zoomLevel':
if (typeof event.newValue === 'number') {
@@ -318,7 +322,8 @@ export class ArduinoFrontendContribution
protected languageServerStartMutex = new Mutex();
protected async startLanguageServer(
fqbn: string,
name: string | undefined
name: string | undefined,
forceStart = false
): Promise<void> {
const port = await this.daemon.tryGetPort();
if (!port) {
@@ -352,12 +357,15 @@ export class ArduinoFrontendContribution
}
return;
}
if (fqbn === this.languageServerFqbn) {
if (!forceStart && fqbn === this.languageServerFqbn) {
// NOOP
return;
}
this.logger.info(`Starting language server: ${fqbn}`);
const log = this.arduinoPreferences.get('arduino.language.log');
const realTimeDiagnostics = this.arduinoPreferences.get(
'arduino.language.realTimeDiagnostics'
);
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
@@ -388,6 +396,7 @@ export class ArduinoFrontendContribution
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliDaemonInstance: '1',
realTimeDiagnostics,
board: {
fqbn,
name: name ? `"${name}"` : undefined,

View File

@@ -51,6 +51,14 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: false,
},
'arduino.language.realTimeDiagnostics': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/language.realTimeDiagnostics',
"If true, the language server provides real-time diagnostics when typing in the editor. It's false by default."
),
default: false,
},
'arduino.compile.verbose': {
type: 'boolean',
description: nls.localize(
@@ -238,6 +246,7 @@ export const ArduinoConfigSchema: PreferenceSchema = {
export interface ArduinoConfiguration {
'arduino.language.log': boolean;
'arduino.language.realTimeDiagnostics': boolean;
'arduino.compile.verbose': boolean;
'arduino.compile.experimental': boolean;
'arduino.compile.revealRange': ErrorRevealStrategy;

View File

@@ -275,7 +275,7 @@ export class CompilerErrors
}
private async handleCompilerErrorsDidChange(
errors: CoreError.Compiler[]
errors: CoreError.ErrorLocation[]
): Promise<void> {
this.toDisposeOnCompilerErrorDidChange.dispose();
const compilerErrorsPerResource = this.groupByResource(
@@ -312,8 +312,8 @@ export class CompilerErrors
}
private async filter(
errors: CoreError.Compiler[]
): Promise<CoreError.Compiler[]> {
errors: CoreError.ErrorLocation[]
): Promise<CoreError.ErrorLocation[]> {
if (!errors.length) {
return [];
}
@@ -326,7 +326,7 @@ export class CompilerErrors
}
private async decorateEditors(
errors: Map<string, CoreError.Compiler[]>
errors: Map<string, CoreError.ErrorLocation[]>
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const composite = await Promise.all(
[...errors.entries()].map(([uri, errors]) =>
@@ -346,7 +346,7 @@ export class CompilerErrors
private async decorateEditor(
uri: string,
errors: CoreError.Compiler[]
errors: CoreError.ErrorLocation[]
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const editor = await this.editorManager.getByUri(new URI(uri));
if (!editor) {
@@ -523,7 +523,7 @@ export class CompilerErrors
}
private async trackEditors(
errors: Map<string, CoreError.Compiler[]>,
errors: Map<string, CoreError.ErrorLocation[]>,
...track: ((editor: EditorWidget) => Disposable)[]
): Promise<Disposable> {
return new DisposableCollection(
@@ -605,8 +605,8 @@ export class CompilerErrors
}
private groupByResource(
errors: CoreError.Compiler[]
): Map<string, CoreError.Compiler[]> {
errors: CoreError.ErrorLocation[]
): Map<string, CoreError.ErrorLocation[]> {
return errors.reduce((acc, curr) => {
const {
location: { uri },
@@ -618,7 +618,7 @@ export class CompilerErrors
}
errors.push(curr);
return acc;
}, new Map<string, CoreError.Compiler[]>());
}, new Map<string, CoreError.ErrorLocation[]>());
}
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;

View File

@@ -4,29 +4,29 @@ import { CoreError } from '../../common/protocol/core-service';
@injectable()
export class CoreErrorHandler {
private readonly compilerErrors: CoreError.Compiler[] = [];
private readonly errors: CoreError.ErrorLocation[] = [];
private readonly compilerErrorsDidChangeEmitter = new Emitter<
CoreError.Compiler[]
CoreError.ErrorLocation[]
>();
tryHandle(error: unknown): void {
if (CoreError.is(error)) {
this.compilerErrors.length = 0;
this.compilerErrors.push(...error.data.filter(CoreError.Compiler.is));
this.errors.length = 0;
this.errors.push(...error.data);
this.fireCompilerErrorsDidChange();
}
}
reset(): void {
this.compilerErrors.length = 0;
this.errors.length = 0;
this.fireCompilerErrorsDidChange();
}
get onCompilerErrorsDidChange(): Event<CoreError.Compiler[]> {
get onCompilerErrorsDidChange(): Event<CoreError.ErrorLocation[]> {
return this.compilerErrorsDidChangeEmitter.event;
}
private fireCompilerErrorsDidChange(): void {
this.compilerErrorsDidChangeEmitter.fire(this.compilerErrors.slice());
this.compilerErrorsDidChangeEmitter.fire(this.errors.slice());
}
}

View File

@@ -5,7 +5,6 @@ import type {
BoardUserField,
Port,
} from '../../common/protocol/boards-service';
import type { ErrorInfo as CliErrorInfo } from '../../node/cli-error-parser';
import type { Programmer } from './boards-service';
import type { Sketch } from './sketches-service';
@@ -17,16 +16,10 @@ export const CompilerWarningLiterals = [
] as const;
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
export namespace CoreError {
export type ErrorInfo = CliErrorInfo;
export interface Compiler extends ErrorInfo {
export interface ErrorLocation {
readonly message: string;
readonly location: Location;
}
export namespace Compiler {
export function is(error: ErrorInfo): error is Compiler {
const { message, location } = error;
return !!message && !!location;
}
readonly details?: string;
}
export const Codes = {
Verify: 4001,
@@ -42,7 +35,7 @@ export namespace CoreError {
export const BurnBootloaderFailed = create(Codes.BurnBootloader);
export function is(
error: unknown
): error is ApplicationError<number, ErrorInfo[]> {
): error is ApplicationError<number, ErrorLocation[]> {
return (
error instanceof Error &&
ApplicationError.is(error) &&
@@ -51,10 +44,10 @@ export namespace CoreError {
}
function create(
code: number
): ApplicationError.Constructor<number, ErrorInfo[]> {
): ApplicationError.Constructor<number, ErrorLocation[]> {
return ApplicationError.declare(
code,
(message: string, data: ErrorInfo[]) => {
(message: string, data: ErrorLocation[]) => {
return {
data,
message,

View File

@@ -1,24 +1,19 @@
import { notEmpty } from '@theia/core';
import { notEmpty } from '@theia/core/lib/common/objects';
import { nls } from '@theia/core/lib/common/nls';
import { FileUri } from '@theia/core/lib/node/file-uri';
import {
Location,
Range,
Position,
} from '@theia/core/shared/vscode-languageserver-protocol';
import { Sketch } from '../common/protocol';
import type { CoreError } from '../common/protocol';
import { Sketch } from '../common/protocol/sketches-service';
export interface ErrorInfo {
readonly message?: string;
readonly location?: Location;
readonly details?: string;
}
export interface ErrorSource {
readonly content: string | ReadonlyArray<Uint8Array>;
readonly sketch?: Sketch;
}
export function tryParseError(source: ErrorSource): ErrorInfo[] {
export function tryParseError(source: ErrorSource): CoreError.ErrorLocation[] {
const { content, sketch } = source;
const err =
typeof content === 'string'
@@ -28,7 +23,7 @@ export function tryParseError(source: ErrorSource): ErrorInfo[] {
return tryParse(err)
.map(remapErrorMessages)
.filter(isLocationInSketch(sketch))
.map(errorInfo());
.map(toErrorInfo);
}
return [];
}
@@ -50,9 +45,7 @@ namespace ParseResult {
}
}
function isLocationInSketch(
sketch: Sketch
): (value: ParseResult, index: number, array: ParseResult[]) => unknown {
function isLocationInSketch(sketch: Sketch): (result: ParseResult) => boolean {
return (result) => {
const uri = FileUri.create(result.path).toString();
if (!Sketch.isInSketch(uri, sketch)) {
@@ -65,15 +58,21 @@ function isLocationInSketch(
};
}
function errorInfo(): (value: ParseResult) => ErrorInfo {
return ({ error, message, path, line, column }) => ({
function toErrorInfo({
error,
message,
path,
line,
column,
}: ParseResult): CoreError.ErrorLocation {
return {
message: error,
details: message,
location: {
uri: FileUri.create(path).toString(),
range: range(line, column),
},
});
};
}
function range(line: number, column?: number): Range {

View File

@@ -26,7 +26,7 @@ import { ResponseService } from '../common/protocol/response-service';
import { Board, OutputMessage, Port, Status } from '../common/protocol';
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { Port as GrpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
import { ApplicationError, Disposable, nls } from '@theia/core';
import { ApplicationError, CommandService, Disposable, nls } from '@theia/core';
import { MonitorManager } from './monitor-manager';
import { AutoFlushingBuffer } from './utils/buffers';
import { tryParseError } from './cli-error-parser';
@@ -42,6 +42,9 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
@inject(MonitorManager)
private readonly monitorManager: MonitorManager;
@inject(CommandService)
private readonly commandService: CommandService;
async compile(
options: CoreService.Compile.Options & {
exportBinaries?: boolean;
@@ -50,7 +53,19 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
): Promise<void> {
const coreClient = await this.coreClient;
const { client, instance } = coreClient;
const handler = this.createOnDataHandler();
let buildPath: string | undefined = undefined;
const handler = this.createOnDataHandler<CompileResponse>((response) => {
const currentBuildPath = response.getBuildPath();
if (!buildPath && currentBuildPath) {
buildPath = currentBuildPath;
} else {
if (!!currentBuildPath && currentBuildPath !== buildPath) {
throw new Error(
`The CLI has already provided a build path: <${buildPath}>, and there is a new build path value: <${currentBuildPath}>.`
);
}
}
});
const request = this.compileRequest(options, instance);
return new Promise<void>((resolve, reject) => {
client
@@ -84,7 +99,36 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
}
})
.on('end', resolve);
}).finally(() => handler.dispose());
}).finally(() => {
handler.dispose();
if (!buildPath) {
console.error(
`Have not received the build path from the CLI while running the compilation.`
);
} else {
this.fireBuildDidComplete(FileUri.create(buildPath).toString());
}
});
}
// This executes on the frontend, the VS Code extension receives it, and sends an `ino/buildDidComplete` notification to the language server.
private fireBuildDidComplete(buildOutputUri: string): void {
const params = {
buildOutputUri,
};
console.info(
`Executing 'arduino.languageserver.notifyBuildDidComplete' with ${JSON.stringify(
params
)}`
);
this.commandService
.executeCommand('arduino.languageserver.notifyBuildDidComplete', params)
.catch((err) =>
console.error(
`Unexpected error when firing event on build did complete. ${buildOutputUri}`,
err
)
);
}
private compileRequest(
@@ -124,8 +168,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
options,
() => new UploadRequest(),
(client, req) => client.upload(req),
(message: string, info: CoreError.ErrorInfo[]) =>
CoreError.UploadFailed(message, info),
(message: string, locations: CoreError.ErrorLocation[]) =>
CoreError.UploadFailed(message, locations),
'upload'
);
}
@@ -137,8 +181,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
options,
() => new UploadUsingProgrammerRequest(),
(client, req) => client.uploadUsingProgrammer(req),
(message: string, info: CoreError.ErrorInfo[]) =>
CoreError.UploadUsingProgrammerFailed(message, info),
(message: string, locations: CoreError.ErrorLocation[]) =>
CoreError.UploadUsingProgrammerFailed(message, locations),
'upload using programmer'
);
}
@@ -152,8 +196,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
errorHandler: (
message: string,
info: CoreError.ErrorInfo[]
) => ApplicationError<number, CoreError.ErrorInfo[]>,
locations: CoreError.ErrorLocation[]
) => ApplicationError<number, CoreError.ErrorLocation[]>,
task: string
): Promise<void> {
await this.compile(Object.assign(options, { exportBinaries: false }));
@@ -285,7 +329,9 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
return request;
}
private createOnDataHandler<R extends StreamingResponse>(): Disposable & {
private createOnDataHandler<R extends StreamingResponse>(
onResponse?: (response: R) => void
): Disposable & {
stderr: Buffer[];
onData: (response: R) => void;
} {
@@ -297,10 +343,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
}
});
});
const onData = StreamingResponse.createOnDataHandler(stderr, (out, err) => {
buffer.addChunk(out);
buffer.addChunk(err, OutputMessage.Severity.Error);
});
const onData = StreamingResponse.createOnDataHandler(
stderr,
(out, err) => {
buffer.addChunk(out);
buffer.addChunk(err, OutputMessage.Severity.Error);
},
onResponse
);
return {
dispose: () => buffer.dispose(),
stderr,
@@ -369,13 +419,17 @@ namespace StreamingResponse {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createOnDataHandler<R extends StreamingResponse>(
stderr: Uint8Array[],
onData: (out: Uint8Array, err: Uint8Array) => void
onData: (out: Uint8Array, err: Uint8Array) => void,
onResponse?: (response: R) => void
): (response: R) => void {
return (response: R) => {
const out = response.getOutStream_asU8();
const err = response.getErrStream_asU8();
stderr.push(err);
onData(out, err);
if (onResponse) {
onResponse(response);
}
};
}
}