diff --git a/packages/addons/src/browser/chrome-devtools.contribution.ts b/packages/addons/src/browser/chrome-devtools.contribution.ts new file mode 100644 index 0000000000..fb43193bea --- /dev/null +++ b/packages/addons/src/browser/chrome-devtools.contribution.ts @@ -0,0 +1,60 @@ +import { Autowired } from '@opensumi/di'; +import { ClientAppContribution } from '@opensumi/ide-core-browser'; +import { Domain } from '@opensumi/ide-core-common/lib/di-helper'; + +import { ConnectionRTTBrowserServiceToken, ConnectionRTTBrowserService } from './connection-rtt-service'; + +enum DevtoolsEvent { + Latency = 'devtools:latency', +} + +enum DevtoolsCommand { + Start = 'start', + Stop = 'stop', +} + +@Domain(ClientAppContribution) +export class ChromeDevtoolsContribution implements ClientAppContribution { + @Autowired(ConnectionRTTBrowserServiceToken) + protected readonly rttService: ConnectionRTTBrowserService; + + private interval?: NodeJS.Timeout; + + static INTERVAL = 1000; + + initialize() { + // receive notification from opensumi devtools by custom event + window.addEventListener(DevtoolsEvent.Latency, (event) => { + const { command } = event.detail; + if (command === DevtoolsCommand.Start) { + if (!this.interval) { + this.startRTTInterval(); + } + } else if (command === DevtoolsCommand.Stop) { + if (this.interval) { + global.clearInterval(this.interval); + this.interval = undefined; + } + } + }); + + // if opensumi devtools has started capturing before this contribution point is registered + if (window.__OPENSUMI_DEVTOOLS_GLOBAL_HOOK__?.capture) { + if (!this.interval) { + this.startRTTInterval(); + } + } + } + + private startRTTInterval() { + this.interval = global.setInterval(async () => { + const start = Date.now(); + await this.rttService.measure(); + const rtt = Date.now() - start; + // "if" below is to prevent setting latency after stoping capturing + if (window.__OPENSUMI_DEVTOOLS_GLOBAL_HOOK__.capture) { + window.__OPENSUMI_DEVTOOLS_GLOBAL_HOOK__.latency = rtt; + } + }, ChromeDevtoolsContribution.INTERVAL); + } +} diff --git a/packages/addons/src/browser/connection-rtt-contribution.ts b/packages/addons/src/browser/connection-rtt-contribution.ts index 798ecb8bf7..5ed8f6f1c5 100644 --- a/packages/addons/src/browser/connection-rtt-contribution.ts +++ b/packages/addons/src/browser/connection-rtt-contribution.ts @@ -1,10 +1,10 @@ -import { Autowired, Injectable } from '@opensumi/di'; +import { Autowired } from '@opensumi/di'; import { IStatusBarService, StatusBarAlignment, StatusBarEntryAccessor } from '@opensumi/ide-core-browser/lib/services'; import { Command, CommandContribution, CommandRegistry } from '@opensumi/ide-core-common/lib/command'; import { Domain } from '@opensumi/ide-core-common/lib/di-helper'; import { localize } from '@opensumi/ide-core-common/lib/localize'; -import { ConnectionBackServicePath, IConnectionBackService } from '../common'; +import { ConnectionRTTBrowserServiceToken, ConnectionRTTBrowserService } from './connection-rtt-service'; const START_CONNECTION_RTT_COMMAND: Command = { id: 'connection.start.rtt', @@ -23,18 +23,6 @@ const statusBarOption = { priority: Infinity - 1, }; -export const ConnectionRTTBrowserServiceToken = Symbol('ConnectionRTTBrowserService'); - -@Injectable() -export class ConnectionRTTBrowserService { - @Autowired(ConnectionBackServicePath) - protected readonly connectionBackService: IConnectionBackService; - - async measure() { - await this.connectionBackService.$measure(); - } -} - @Domain(CommandContribution) export class ConnectionRTTContribution implements CommandContribution { @Autowired(IStatusBarService) diff --git a/packages/addons/src/browser/connection-rtt-service.ts b/packages/addons/src/browser/connection-rtt-service.ts new file mode 100644 index 0000000000..6625dc0e83 --- /dev/null +++ b/packages/addons/src/browser/connection-rtt-service.ts @@ -0,0 +1,15 @@ +import { Autowired, Injectable } from '@opensumi/di'; + +import { ConnectionBackServicePath, IConnectionBackService } from '../common'; + +export const ConnectionRTTBrowserServiceToken = Symbol('ConnectionRTTBrowserService'); + +@Injectable() +export class ConnectionRTTBrowserService { + @Autowired(ConnectionBackServicePath) + protected readonly connectionBackService: IConnectionBackService; + + async measure() { + await this.connectionBackService.$measure(); + } +} diff --git a/packages/addons/src/browser/index.ts b/packages/addons/src/browser/index.ts index acb3e8bddf..a3e2b20111 100644 --- a/packages/addons/src/browser/index.ts +++ b/packages/addons/src/browser/index.ts @@ -3,11 +3,9 @@ import { BrowserModule } from '@opensumi/ide-core-browser'; import { IFileDropFrontendServiceToken, FileDropServicePath, ConnectionBackServicePath } from '../common'; -import { - ConnectionRTTBrowserService, - ConnectionRTTBrowserServiceToken, - ConnectionRTTContribution, -} from './connection-rtt-contribution'; +import { ChromeDevtoolsContribution } from './chrome-devtools.contribution'; +import { ConnectionRTTContribution } from './connection-rtt-contribution'; +import { ConnectionRTTBrowserService, ConnectionRTTBrowserServiceToken } from './connection-rtt-service'; import { FileDropContribution } from './file-drop.contribution'; import { FileDropService } from './file-drop.service'; import { FileSearchContribution } from './file-search.contribution'; @@ -18,6 +16,7 @@ import { ToolbarCustomizeContribution } from './toolbar-customize/toolbar-custom @Injectable() export class ClientAddonModule extends BrowserModule { providers = [ + ChromeDevtoolsContribution, LanguageChangeHintContribution, FileSearchContribution, StatusBarContribution, diff --git a/packages/connection/src/common/proxy.ts b/packages/connection/src/common/proxy.ts index 136083fa08..fa1d8962e9 100644 --- a/packages/connection/src/common/proxy.ts +++ b/packages/connection/src/common/proxy.ts @@ -1,6 +1,8 @@ -import { ApplicationError } from '@opensumi/ide-core-common'; +import { ApplicationError, uuid } from '@opensumi/ide-core-common'; import type { MessageConnection } from '@opensumi/vscode-jsonrpc/lib/common/connection'; +import { MessageType, ResponseStatus, ICapturedMessage, getCapturer } from './utils'; + export abstract class RPCService { rpcClient?: T[]; rpcRegistered?: boolean; @@ -46,6 +48,13 @@ export class RPCProxy { private connection: MessageConnection; private proxyService: any = {}; private logger: any; + // capture messages for opensumi devtools + private capture(message: ICapturedMessage): void { + const capturer = getCapturer(); + if (capturer !== undefined) { + capturer(message); + } + } constructor(public target?: RPCService, logger?: any) { this.waitForConnection(); @@ -98,17 +107,24 @@ export class RPCProxy { if (prop.startsWith('on')) { if (isSingleArray) { connection.sendNotification(prop, [...args]); + this.capture({ type: MessageType.SendNotification, serviceMethod: prop, arguments: args }); } else { connection.sendNotification(prop, ...args); + this.capture({ type: MessageType.SendNotification, serviceMethod: prop, arguments: args }); } resolve(null); } else { let requestResult: Promise; + // generate a unique requestId to associate request and requestResult + const requestId = uuid(); + if (isSingleArray) { requestResult = connection.sendRequest(prop, [...args]) as Promise; + this.capture({ type: MessageType.SendRequest, requestId, serviceMethod: prop, arguments: args }); } else { requestResult = connection.sendRequest(prop, ...args) as Promise; + this.capture({ type: MessageType.SendRequest, requestId, serviceMethod: prop, arguments: args }); } requestResult @@ -126,8 +142,22 @@ export class RPCProxy { const applicationError = ApplicationError.fromJson(result.error.code, result.error.data); error.cause = applicationError; } + this.capture({ + type: MessageType.RequestResult, + status: ResponseStatus.Fail, + requestId, + serviceMethod: prop, + error: result.data, + }); reject(error); } else { + this.capture({ + type: MessageType.RequestResult, + status: ResponseStatus.Success, + requestId, + serviceMethod: prop, + data: result.data, + }); resolve(result.data); } }); @@ -162,9 +192,38 @@ export class RPCProxy { const methods = this.getServiceMethod(service); methods.forEach((method) => { if (method.startsWith('on')) { - connection.onNotification(method, (...args) => this.onNotification(method, ...args)); + connection.onNotification(method, (...args) => { + this.onNotification(method, ...args); + this.capture({ type: MessageType.OnNotification, serviceMethod: method, arguments: args }); + }); } else { - connection.onRequest(method, (...args) => this.onRequest(method, ...args)); + connection.onRequest(method, (...args) => { + const requestId = uuid(); + const result = this.onRequest(method, ...args); + this.capture({ type: MessageType.OnRequest, requestId, serviceMethod: method, arguments: args }); + + result + .then((result) => { + this.capture({ + type: MessageType.OnRequestResult, + status: ResponseStatus.Success, + requestId, + serviceMethod: method, + data: result.data, + }); + }) + .catch((err) => { + this.capture({ + type: MessageType.OnRequestResult, + status: ResponseStatus.Fail, + requestId, + serviceMethod: method, + error: err.data, + }); + }); + + return result; + }); } if (cb) { @@ -174,9 +233,19 @@ export class RPCProxy { connection.onRequest((method) => { if (!this.proxyService[method]) { - return { + const requestId = uuid(); + this.capture({ type: MessageType.OnRequest, requestId, serviceMethod: method }); + const result = { data: NOTREGISTERMETHOD, }; + this.capture({ + type: MessageType.OnRequestResult, + status: ResponseStatus.Fail, + requestId, + serviceMethod: method, + error: result.data, + }); + return result; } }); } diff --git a/packages/connection/src/common/utils.ts b/packages/connection/src/common/utils.ts index 91d1405015..321715628e 100644 --- a/packages/connection/src/common/utils.ts +++ b/packages/connection/src/common/utils.ts @@ -1,3 +1,33 @@ +declare global { + interface Window { + __OPENSUMI_DEVTOOLS_GLOBAL_HOOK__: any; + } +} + +export enum MessageType { + SendNotification = 'sendNotification', + SendRequest = 'sendRequest', + RequestResult = 'requestResult', + OnNotification = 'onNotification', + OnRequest = 'onRequest', + OnRequestResult = 'onRequestResult', +} + +export enum ResponseStatus { + Success = 'success', + Fail = 'fail', +} + +export interface ICapturedMessage { + type: MessageType; + serviceMethod: string; + arguments?: any; + requestId?: string; + status?: ResponseStatus; + data?: any; + error?: any; +} + export function stringify(obj: any): string { return JSON.stringify(obj); } @@ -5,3 +35,10 @@ export function stringify(obj: any): string { export function parse(input: string, reviver?: (this: any, key: string, value: any) => any): any { return JSON.parse(input, reviver); } + +export function getCapturer() { + if (typeof window !== 'undefined' && window.__OPENSUMI_DEVTOOLS_GLOBAL_HOOK__?.capture) { + return window.__OPENSUMI_DEVTOOLS_GLOBAL_HOOK__.capture; + } + return; +} diff --git a/packages/connection/src/node/connect.ts b/packages/connection/src/node/connect.ts index 1cc31dafe3..683eec5064 100644 --- a/packages/connection/src/node/connect.ts +++ b/packages/connection/src/node/connect.ts @@ -6,7 +6,6 @@ import { createMessageConnection, } from '@opensumi/vscode-jsonrpc/lib/node/main'; - export function createSocketConnection(socket: net.Socket) { return createMessageConnection(new SocketMessageReader(socket), new SocketMessageWriter(socket)); } diff --git a/packages/core-browser/src/bootstrap/app.ts b/packages/core-browser/src/bootstrap/app.ts index 67b0728442..8a3589977a 100644 --- a/packages/core-browser/src/bootstrap/app.ts +++ b/packages/core-browser/src/bootstrap/app.ts @@ -146,6 +146,10 @@ export class ClientApp implements IClientApp, IDisposable { stateService: ClientAppStateService; constructor(opts: IClientAppOpts) { + // set a global so the opensumi devtools can identify that + // the current page is powered by opensumi core + window.__OPENSUMI_DEVTOOLS_GLOBAL_HOOK__ = {}; + const { modules, contributions,