7

VSCode多进程架构和插件加载原理

 3 years ago
source link: https://zhuanlan.zhihu.com/p/260932167
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

VSCode多进程架构和插件加载原理

上周我在公司做了一个VSCode的分享,反响不是很好。内容坡度大,没有深入浅出。所以这一次我会更加详细的介绍细节,希望网上的同学看完之后有所收获。

先提出三个问题

1、为什么我打开VSCode,会打开N多个进程?

2、为什么我的全局搜索能用,但单个文件内的代码搜索却卡住了?

3、VSCode插件装多了会不会影响性能?

这些问题在本文都将得到解决。

本文分为4个部分。

1、VSCode简介

2、多进程架构详解

3、最简单的插件

4、源码解析

VSCode简介

本文都是基于VSCode 1.47.3 的版本。

  • 作者,Erich Gamma,Eclipse 架构师,《设计模式》经典书籍,妥妥的业内大佬。
  • 关键词:免费、开源、轻量级、编辑器、跨平台、多语言、Git开箱即用、插件扩展。这里提一下扩平台,VSCode支持Mac、Windows、Linux、Web。
  • 体量:前端大型项目,总代码量100万,其中60万TypeScript代码(cloc工具统计)
  • 技术:Electron、TypeScript、Monaco、xTerm、LSP(Language Server Protocol)DAP(Debug Adapter Protocol)。我们将会着重介绍加粗的3个内容。

时间线

我第一次使用VSCode是2016年底,一个PHP同事和我说,VSCode写PHP真好用,随即我就被安利了。果然最好的语言就是要用最好的编辑器。

Electron

LSP

语言服务协议,编程语言需要为编辑器实现一些常用的功能,比如hover效果,代码提示(intelligence),代码诊断(diagnostics)等功能,每个编辑器都有一套自己的规则。 从图中我们可以看出,左边为编程语言,右边为编辑器。没有LSP之前,编程语言和编辑器之间是多对多的关系,这种复杂性为 n^2 。但是引入LSP之后,就变成了一对多的关系,主流编辑器都采用同一个协议规则,而编程语言只需要面向语言服务协议编写功能即可,这像不像面向接口编程。

这是一张HTML语言服务协议和PHP语言服务协议的图,PHP和HTML实现了这种服务,而客户端通过JSON RPC这种远程调用,在VSCode插件进程内初始化这些语言服务。(语言服务运行在插件进程内)。

想了解如何自定义一个语言服务,可以看一下这篇文章vscode插件快餐教程(7) - 从头开始写一个完整的lsp工程

调试适配器协议

它其实和LSP很像,所有的编程语言都公用一个调试界面,只需要实现DAP这个协议即可。

多进程架构详解

1、主进程(Main),一个 Electron 应用只有一个主进程。创建 GUI 相关的接口只由主进程来调用。

2、渲染进程(Renderer),每一个工作区(workbench)对应一个进程,同时是BrowserWindow实例。一个Electron项目可以有多个渲染进程。

3、插件进程(Extension),fork了渲染进程,每个插件都运行在一个NodeJS宿主环境中,即插件间共享进程。VSCode规定,插件不允许直接访问UI,这和Atom不同。

4、Debug进程,一个特殊的插件进程。

5、Search进程,搜索是密集型任务,单独占用一个进程。

6、进程之间通过IPC、RPC进行通信,这个后面会介绍。

7、LSP和DAP像两座桥梁,连接起语言和调试服务,它们都运行在插件进程中。

因为VSCode基于Electron,Electron基于chromium,所以进程和浏览器架构十分相似。

进程间通信

IPC

electron.ipcRenderer.send(
  "sendMessageFromRendererProcesses",
  "渲染进程向主进程发送异步消息"
);

electron.ipcMain.on("sendMessageFromRendererProcesses", (event, message) => {
  event.sender.send("sendMessageFromMainProcesses", "回应异步消息:" + message);
});

VSCode的IPC通信是基于Electron,进程间可以双向通信,并且支持同步异步通信。

RPC

const { BrowserWindow } = require("electron").remote;
let win = new BrowserWindow({ width: 800, height: 600 });
win.loadURL("https://github.com");

这里是渲染进程直接调用Electron的远程模块,重新初始化一个界面BrowserWindow,并且打开一个页面,地址为https://github.com。RPC一般用于单向调用,如渲染进程调用主进程。

小结

1、多进程架构,实现了视图与逻辑分离。

2、基于接口编程(LSP、DAP),规范了扩展功能。

3、插件进程,单独开启一个进程。不影响启动速度,不影响主进程和渲染进程,不能直接改变UI样式。缺点,UI可扩展性差,优点,带来了统一的视觉效果和交互风格。

最简单的插件

下面是官方的一个搭建插件的教程

npm install -g yo generate-code
yo code
code ./helloworld

然后我们就生成了一个VSCode插件,目录如下,我和一个普通的前端项目没啥区别。我们只需要关心package.jsonextension.ts

package.json

{
  "engines": {
    "vscode": "^1.47.0"
  }
  "activationEvents": [
    "onLanguage:java",
    "onCommand:java.show.references",
    "onCommand:java.show.implementations",
    "onCommand:java.open.output",
    "onCommand:java.open.serverLog",
    "onCommand:java.execute.workspaceCommand",
    "onCommand:java.projectConfiguration.update",
    "workspaceContains:pom.xml",
    "workspaceContains:build.gradle"
  ]
}

有两个关键字,engines指VSCode兼容版本,activationEvents表示触发事件。onLanguage为语言为java时,输入命令onCommand:java.show.references(通过cmd + p可进入输入命令界面),或者工作区中包含pom.xml文件,这些都会加载插件。插件的加载机制是懒加载,只有触发了指定事件才会加载。

extension.ts

extension里导出一个activate函数,表示当插件被激活时执行函数内的内容。Demo里注册了一个命令到VSCode的context上下文中,且当执行hellworld这个命令时,会弹出一个提示语,我们将提示语由Hello World 改为了 Hello VS Code。

VSCode能自动实现插件项目,我们按F5即可进入调试模式,下面是一个输出提示语的视频。

源码解析分为4大块。

  1. 工作台(WorkBench)加载
  2. 插件(Extension)加载

上图分为上下两块内容,上面是VSCode外层的目录结构。下面为VSCode内部组织代码的规则,以base目录为例,它包含了个模块,vs目录下的其他模块,code、editor也是按照这个规则。

项目的搭建比较简单,可以直接看官方的教程,How to Contribute。Mac的话主要一下Python版本和NodeJS脚本。

web版启动

yarn web

桌面版启动

./scripts/code.sh

本文讲的内容都是桌面版,启动完成之后,我们可以看到VSCode给我们提供的源码调试工具,OSS。

调试模式

调试模式和桌面启动有所不同,我们直接在VSCode里打开源码项目,进入调试面板,先Launch VS Code,然后就可以选择是调试主进程、渲染进程还是插件进程。

查看所有进程

ps aux|grep "OSS Helper"

启动完成之后,通过命令行查看进程情况,上面我截取了插件进程相关的信息。如果是源码情况下,关键词就是OSS Helper。我们正常使用VSCode,就可以用关键词Code Helper查看进程相关情况。

进程类型介绍

这是VSCode进程的类型,--type就是VSCode启动进程时识别进程类型的标识。有渲染进程,插件进程,GPU进程,可关闭,Watcher进程,和Webpack的Watch有些相似,都是监控文件变化的,搜索进程。插件是由渲染进程fork出来的,且一般情况插件共享一个进程,Debug进程比较特殊,它单独占用一个进程。

源码之加载工作台

// src/main.js
// 获取缓存文件目录地址和语言配置,用AMD Loader加载真正主入口
const { app, protocol } = require("electron");
app.once("ready", function () {
  onReady();
});

async function onReady() {
 const [cachedDataDir, nlsConfig] = await Promise.all([
    nodeCachedDataDir.ensureExists(),
    resolveNlsConfiguration(),
  ]);
  startup(cachedDataDir, nlsConfig);
}

function startup(cachedDataDir, nlsConfig) {
 require("./bootstrap-amd").load("vs/code/electron-main/main");
}
// src/vs/code/electron-main/main.ts
// 创建服务,初始化编辑器实例
const code = new CodeMain();
code.main();

class CodeMain {
  main() {
    this.startUp(args);
  }

  private async startup(args: ParsedArgs): Promise<void> {
   const [instantiationService, instanceEnvironment, environmentService] = this.createServices(args, bufferLogService);
   return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();
  }
}

VSCode初始化实例的方式比较特殊,采用的是依赖注入的模式,关于VSCod依赖注入的文章,可以看腾讯同学写的文章,vscode 源码解析 - 依赖注入

// 打开一个窗口

// src/vs/code/electron-main/app.ts
class CodeApplication extends Disposable {
  async startup(): Promise<void> {
    const appInstantiationService = await this.createServices(machineId, sharedProcess, sharedProcessReady);

    const windows = appInstantiationService.invokeFunction((accessor) =>
      this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient)
    );
  }

  private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise<Client<string>>): ICodeWindow[] {
    const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
    return windowsMainService.open({
      context: OpenContext.API,
      cli: { ...environmentService.args },
      forceEmpty: true,
      gotoLineMode: true
    });
  }
}

// 打开浏览器窗口,并加载配置
// src/vs/platform/windows/electron-main/windowsMainService.ts

export class WindowsMainService extends Disposable implements IWindowsMainService {
  open() {
    this.doOpen();
  }
  private doOpen() {
    this.openInBrowserWindow();
  }
  private openInBrowserWindow() {
    const createdWindow = (window = this.instantiationService.createInstance(
      CodeWindow,
      {
        state,
        extensionDevelopmentPath: configuration.extensionDevelopmentPath,
        isExtensionTestHost: !!configuration.extensionTestsPath,
      }
    ));
  }
  private doOpenInBrowserWindow() {
    window.load(configuration); // 加载页面
  }
}


// src/vs/code/electron-main/window.ts
// this._win 为 BrowserWindow 对象,是electron一个模块

class CodeWindow extends Disposable {
  load() {
    this._win.loadURL(this.getUrl(configuration));
  }

  private getUrl() {
    let configUrl = this.doGetUrl(config);
    return configUrl;
  }

  private doGetUrl(config: object): string {
    // 打开 VSCode 的工作台,也就是 workbench
    return `${require.toUrl(
      "vs/code/electron-browser/workbench/workbench.html"
    )}?config=${encodeURIComponent(JSON.stringify(config))}`;
  }
}

src/vs/code/electron-browser/workbench/workbench.html

<!DOCTYPE html>
<html>
  <body aria-label=""></body>

  <!-- Init Bootstrap Helpers -->
  <script src="../../../../bootstrap.js"></script>
  <script src="../../../../vs/loader.js"></script>
  <script src="../../../../bootstrap-window.js"></script>

  <!-- Startup via workbench.js -->
  <script src="workbench.js"></script>
</html>

源码之插件加载

src/vs/code/electron-browser/workbench/workbench.js
加载桌面插件,加载插件服务

bootstrapWindow.load(
  [
    "vs/workbench/workbench.desktop.main",
    "vs/nls!vs/workbench/workbench.desktop.main",
    "vs/css!vs/workbench/workbench.desktop.main",
  ],
  (workbench, configuration) => {
    // …
  }
);

src/vs/workbench/workbench.desktop.main.ts

import "vs/workbench/services/extensions/electron-browser/extensionService";

src/vs/workbench/services/extensions/electron-browser/extensionService.ts
监听生命周期钩子,实例化 ExtensionHostManager

class ExtensionService extends AbstractExtensionService implements IExtensionService {
  constructor() {
    this._lifecycleService.when(LifecyclePhase.Ready).then(() => {
        // reschedule to ensure this runs after restoring viewlets, panels, and editors
        runWhenIdle(() => {
          this._initialize();
        }, 50 /*max delay*/);
      });
  }

  protected async _initialize(): Promise<void> {
    this._startExtensionHosts(true, []);
  }

  private _startExtensionHosts(isInitialStart: boolean, initialActivationEvents: string[]): void {
      // extensionHosts 为LocalProcessExtensionHost、RemoteExtensionHost、WebWorkerExtensionHost。
      const extensionHosts = this._createExtensionHosts(isInitialStart);
      extensionHosts.forEach((extensionHost) => {
        const processManager = this._instantiationService.createInstance(ExtensionHostManager, extensionHost, initialActivationEvents);
      });
  }
}

src/vs/workbench/services/extensions/common/extensionHostManager.ts
fork渲染进程,并加载 extensionHostProcess

class ExtensionHostManager extends Disposable {
  constructor() {
      this._proxy = this._extensionHost.start()!.then();
  }
}

src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts

class LocalProcessExtensionHost implements IExtensionHost {
  public start(): Promise<IMessagePassingProtocol> | null {
    // ...
    const opts = {
      env: objects.mixin(objects.deepClone(process.env), {
        // 加载插件进程
        AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess',
      }),
    }

    // Run Extension Host as fork of current process
    this._extensionHostProcess = fork(getPathFromAmdModule(require, 'bootstrap-fork'), ['--type=extensionHost'], opts);
  }
}

src/vs/workbench/services/extensions/node/extensionHostProcess.ts
插件进程的入口,同时开启插件激活逻辑

import { startExtensionHostProcess } from "vs/workbench/services/extensions/node/extensionHostProcessSetup";
startExtensionHostProcess().catch((err) => console.log(err));

src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts

export async function startExtensionHostProcess(): Promise<void> {
    const extensionHostMain = new ExtensionHostMain(
        renderer.protocol,
        initData,
        hostUtils,
        uriTransformer
    );
}

src/vs/workbench/services/extensions/common/extensionHostMain.ts

export class ExtensionHostMain {
  constructor() {
    // must call initialize *after* creating the extension service
        // because `initialize` itself creates instances that depend on it
        this._extensionService = instaService.invokeFunction(accessor => accessor.get(IExtHostExtensionService));
        this._extensionService.initialize();
  }
}

src/vs/workbench/api/node/extHost.services.ts

import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';

// 注册插件服务
registerSingleton(IExtHostExtensionService, ExtHostExtensionService);

src/vs/workbench/api/node/extHostExtensionService.ts
继承了抽象类,AbstractExtHostExtensionService

export class ExtHostExtensionService extends AbstractExtHostExtensionService {

}

src/vs/workbench/api/common/extHostExtensionService.ts

abstract class AbstractExtHostExtensionService extends Disposable {
  constructor() {
    this._activator = new ExtensionsActivator();
  }

  // 根据activationEvent事件名激活插件
  private _activateByEvent(activationEvent: string, startup: boolean): Promise<void> {
    return this._activator.activateByEvent(activationEvent, startup);
  }
}

整体的流程图如下

1、VSCode使用了哪些技术,TS、Electron、Monaco、LSP、DAP。

2、多进程架构(记住图即可)。

3、从一个Hello World插件入手。

4、源码解析,从加载工作区,到开启插件进程,最后激活插件。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK