前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >copilot源码详细分析(二)activate入口分析

copilot源码详细分析(二)activate入口分析

作者头像
孟健
发布2023-11-16 13:10:50
4050
发布2023-11-16 13:10:50
举报
文章被收录于专栏:前端工程

copilot的入口函数

我们将activate方法格式化如下:

代码语言:javascript
复制
async function activate(context) {
  // 创建并标记为已发送的遥测数据
  let activationTelemetry = TelemetryData.createAndMarkAsIssued();

  // 创建扩展上下文,并等待其完成
  let ctx = await createExtensionContext(context);

  // 注册状态栏,并将CopilotRepositoryControlManager添加到上下文中
  registerStatusBar(ctx, outputChannel);
  ctx.set(CopilotRepositoryControlManager, new CopilotRepositoryControlManager(ctx));

  // 注册诊断命令
  registerDiagnosticCommands(ctx);

  // 注册带有遥测的命令
  registerCommandWithTelemetry(ctx, CMDSignIn, () => getSession(ctx, !0));

  // 将CodeReference添加到订阅中
  context.subscriptions.push(new CodeReference(ctx).register());

  // 将onDeactivate添加到订阅中
  context.subscriptions.push(onDeactivate(ctx));

  // 定义一个异步函数tryActivation
  let tryActivation = __name(async () => {
    let statusBar = ctx.get(StatusReporter);

    // 设置进度,并允许一次性登录
    statusBar.setProgress();
    permitOneSignIn();

    // 定义一个处理错误的函数
    let rejectionHandler = __name((error, allowRetry = !0) => {
      let reason = error.message || error;

      // 记录错误,并停用遥测
      telemetryError(ctx, "activationFailed", TelemetryData.createAndMarkAsIssued({
        reason: reason
      }));
      ctx.get(TelemetryReporters).deactivate();

      // 设置错误消息,并允许重试
      let message = reason === "GitHubLoginFailed" ? SESSION_LOGIN_MESSAGE : `Extension activation failed: "${reason}"`;
      statusBar.setError(message, allowRetry ? tryActivation : void 0);

      // 记录错误,并将github.copilot.activated上下文设置为false
      logger.error(ctx, message);
      ja.commands.executeCommand("setContext", "github.copilot.activated", !1);
    }, "rejectionHandler");

    // 检查Node.js版本是否受支持
    let nodeVersionError = errorMessageForUnsupportedNodeVersion();
    if (nodeVersionError) {
      rejectionHandler(nodeVersionError, !1);
      return;
    }

    // 获取Copilot token并等待其完成
    ctx.get(CopilotTokenManager).getCopilotToken(ctx).then(() => {
      // 强制设置为正常状态,并将github.copilot.activated上下文设置为true
      statusBar.forceNormal();
      ja.commands.executeCommand("setContext", "github.copilot.activated", !0);

      // 注册面板支持,注册GhostText支持,并将文档跟踪器和光标跟踪器添加到订阅中
      registerPanelSupport(ctx);
      registerGhostTextSupport(ctx);
      context.subscriptions.push(registerDocumentTracker(ctx));
      context.subscriptions.push(registerCursorTracker(ctx));

      // 添加事件处理器,当活动编辑器改变时提取仓库信息,当打开文档时预热语言检测缓存,当配置改变时调用onDidChangeConfigurationHandler
      context.subscriptions.push(ja.window.onDidChangeActiveTextEditor(e => e && extractRepoInfoInBackground(ctx, e.document.uri)));
      context.subscriptions.push(ja.workspace.onDidOpenTextDocument(doc => primeLanguageDetectionCache(ctx, doc)));
      context.subscriptions.push(ja.workspace.onDidChangeConfiguration(e => onDidChangeConfigurationHandler(e, ctx)));

      // 检查扩展模式是否为开发模式
      let isDevMode = context.extensionMode === ja.ExtensionMode.Development;

      // 初始化,如果不是开发模式,则启动线程,并发送激活遥测
      init(ctx, !isDevMode, new Logger(1, "promptlib proxy"));
      !isDevMode && ctx.get(hy.SnippetOrchestrator).startThreading();
      telemetry(ctx, "extension.activate", activationTelemetry);

      // 如果有活动的文本编辑器,则更新其内容
      ja.window?.activeTextEditor && ctx.get(CopilotRepositoryControlManager).evaluate(ja.window.activeTextEditor.document?.uri, ja.window.activeTextEditor.document.getText(), "UPDATE");
    }).catch(ex => {
      // 如果发生错误,则调用rejectionHandler
      rejectionHandler(ex);
    });
  }, "tryActivation");

  // 添加事件处理器,当会话改变时调用onDidChangeSessionsHandler
  ja.authentication.onDidChangeSessions(async event => {
    await onDidChangeSessionsHandler(event, ctx);
  });

  // 启动VS Code安装管理器
  new VsCodeInstallationManager().startup(ctx);

  // 等待tryActivation完成
  await tryActivation();

  // 返回CopilotExtensionApi的新实例
  return new CopilotExtensionApi(ctx);
}

在入口函数中,涉及到了几个组件:

  • TelemetryData,负责创建上报数据。
  • createExtensionContext ,负责处理生成Context。

关于Context的初始化

代码语言:javascript
复制
async function createExtensionContext(extensionContext) {
  // 创建一个生产环境的上下文,并设置日志目标为控制台和输出通道
  let ctx = createProductionContext(new VSCodeConfigProvider()),
    logTarget = new MultiLog([new ConsoleLog(console), new OutputChannelLog(outputChannel)]);
  ctx.forceSet(LogTarget, logTarget);
  ctx.set(EditorAndPluginInfo, new VSCodeEditorInfo());
  initProxyEnvironment(ctx.get(Fetcher), process.env);
  ctx.set(NotificationSender, new ExtensionNotificationSender());
  ctx.set(EditorSession, new EditorSession(vscode.env.sessionId, vscode.env.machineId));
  ctx.set(Extension, new Extension(extensionContext));
  ctx.set(EditorExperimentFilters, new VSCodeEditorExperimentFilters());
  setupExperimentationService(ctx);
  ctx.set(SymbolDefinitionProvider, new ExtensionSymbolDefinitionProvider());
  ctx.set(CopilotExtensionStatus, new CopilotExtensionStatus());

  // 根据扩展模式(测试或生产)设置不同的服务和配置
  if (extensionContext.extensionMode === vscode.ExtensionMode.Test) {
    ctx.forceSet(RuntimeMode, RuntimeMode.fromEnvironment(!0));
    ctx.set(CopilotTokenManager, getTestingCopilotTokenManager());
    ctx.forceSet(UrlOpener, new TestUrlOpener());
    await setupTelemetry(ctx, extensionContext, "copilot-test", !0);
  } else {
    ctx.set(CopilotTokenManager, new VSCodeCopilotTokenManager());
    ctx.forceSet(ExpConfigMaker, new ExpConfigFromTAS());
    await setupTelemetry(ctx, extensionContext, extensionContext.extension.packageJSON.name, vscode.env.isTelemetryEnabled);
  }

  // 设置其他服务和配置
  ctx.set(LocationFactory, new ExtensionLocationFactory());
  ctx.set(TextDocumentManager, new ExtensionTextDocumentManager(ctx));
  ctx.set(WorkspaceFileSystem, new ExtensionWorkspaceFileSystem());
  ctx.set(CommitFileResolver, new ExtensionCommitFileResolver());
  ctx.set(hy.FileSystem, extensionFileSystem);
  ctx.set(NetworkConfiguration, new VSCodeNetworkConfiguration());

  // 返回创建的上下文
  return ctx;
}

我们先来看第一行代码:

代码语言:javascript
复制
let ctx = createProductionContext(new VSCodeConfigProvider())

它是通过createProductionContext 这个方法创建了一个Context,参数是一个VSCodeConfigProvider 的实例。

那么首先看看VSCodeConfigProvider

代码语言:javascript
复制
var CopilotConfigPrefix = "github.copilot";
var VSCodeConfigProvider = class extends ConfigProvider {
    constructor() {
      super();
      this.config = vscode.workspace.getConfiguration(CopilotConfigPrefix), vscode.workspace.onDidChangeConfiguration(changeEvent => {
        changeEvent.affectsConfiguration(CopilotConfigPrefix) && (this.config = vscode.workspace.getConfiguration(CopilotConfigPrefix));
      });
    }
 // ...
}

仅看一下这个constructor我们就知道是拉取了vscode的配置项,prefix为github.copilot ,并且监听了config change重新赋值给this.config。

然后再看一下createProductionContext的实现:

代码语言:javascript
复制
function createProductionContext(configProvider) {
  // 创建一个新的上下文
  let ctx = new Context();

  // 设置各种服务和配置
  ctx.set(ConfigProvider, configProvider);
  ctx.set(Clock, new Clock());
  ctx.set(BuildInfo, new BuildInfo());
  setupRudimentaryLogging(ctx);
  logger.debug(ctx, "Initializing main context");
  ctx.set(CompletionsCache, new CompletionsCache());
  ctx.set(CopilotTokenNotifier, new CopilotTokenNotifier());
  ctx.set(CertificateReaderCache, new CertificateReaderCache());
  ctx.set(RootCertificateReader, getRootCertificateReader(ctx));
  ctx.set(ProxySocketFactory, getProxySocketFactory(ctx));
  ctx.set(Fetcher, new HelixFetcher(ctx));
  ctx.set(LanguageDetection, getLanguageDetection(ctx));
  ctx.set(Features, new Features(ctx));
  ctx.set(PostInsertionNotifier, new PostInsertionNotifier());
  ctx.set(TelemetryUserConfig, new TelemetryUserConfig(ctx));
  ctx.set(TelemetryEndpointUrl, new TelemetryEndpointUrl());
  ctx.set(TelemetryReporters, new TelemetryReporters());
  ctx.set(HeaderContributors, new HeaderContributors());
  ctx.set(UserErrorNotifier, new UserErrorNotifier(ctx));
  ctx.set(ContextualFilterManager, new ContextualFilterManager());
  ctx.set(OpenAIFetcher, new LiveOpenAIFetcher());
  ctx.set(BlockModeConfig, new ConfigBlockModeConfig());
  ctx.set(UrlOpener, new RealUrlOpener());
  ctx.set(ExpConfigMaker, new ExpConfigNone());
  ctx.set(PromiseQueue, new PromiseQueue());
  ctx.set(uD.SnippetOrchestrator, new uD.SnippetOrchestrator());
  ctx.set(ForceMultiLine, ForceMultiLine.default);

  // 返回创建的上下文
  return ctx;
}

可以看到这个方法首先创建了一个ctx,然后设置了一系列的类与实例,这个Context类似于依赖注入容器管理的作用:

代码语言:javascript
复制
var Context = class {
  constructor(baseContext) {
    this.baseContext = baseContext;
    this.constructionStack = [];
    this.instances = new Map();
    let stack = new Error().stack?.split(`
`);
    stack && this.constructionStack.push(...stack.slice(1));
  }
  static {
    __name(this, "Context");
  }
  get(ctor) {
    let value = this.tryGet(ctor);
    if (value) return value;
    throw new Error(`No instance of ${ctor.name} has been registered.`);
  }
  tryGet(ctor) {
    let value = this.instances.get(ctor);
    if (value) return value;
    if (this.baseContext) return this.baseContext.tryGet(ctor);
  }
  set(ctor, instance) {
    if (this.tryGet(ctor)) throw new Error(`An instance of ${ctor.name} has already been registered. Use forceSet() if you're sure it's a good idea.`);
    this.assertIsInstance(ctor, instance), this.instances.set(ctor, instance);
  }
  forceSet(ctor, instance) {
    this.assertIsInstance(ctor, instance), this.instances.set(ctor, instance);
  }
  assertIsInstance(ctor, instance) {
    if (!(instance instanceof ctor)) {
      let inst = JSON.stringify(instance);
      throw new Error(`The instance you're trying to register for ${ctor.name} is not an instance of it (${inst}).`);
    }
  }
  toString() {
    let lines = `    Context created at:
`;
    for (let stackEntry of this.constructionStack || []) lines += `    ${stackEntry}
`;
    return lines += this.baseContext?.toString() ?? "", lines;
  }
};

在main Context的初始化过程中,首先初始化了三个类:

  • ConfigProvider ,也就是刚刚传进来的那个VSCodeConfigProvider。
  • Clock ,目前看起来就是实现了一个Date.now()。
  • BuildInfo ,封装了关于package.json的相关信息。

然后调用了setupRudimentaryLogging 方法:

代码语言:javascript
复制
function setupRudimentaryLogging(ctx) {
  ctx.set(RuntimeMode, RuntimeMode.fromEnvironment(!1)),
  ctx.set(LogVerbose, new LogVerbose(isVerboseLoggingEnabled(ctx))), 
  ctx.set(LogTarget, new ConsoleLog(console));
}

这里面又初始化了三个类:

  • RuntimeMode ,实际上记录了几个关键的flag:
    • debug ,由-debug参数或GITHUB_COPILOT_DEBUG的环境变量决定。
    • verboseLogging ,由COPILOT_AGENT_VERBOSE决定。
    • telemetryLogging ,由COPILOT_LOG_TELEMETRY 决定。
    • testMode ,在这里是false
    • recordInput ,由-record参数或GITHUB_COPILOT_RECORD 决定。
  • LogVerbose ,记录是否是verboseLogging
  • LogTarget ,注册为ConsoleLog

接着打了一行debug日志:”Initializing main context”,这应该是copilot的第一行日志。

接着初始化了一堆服务:

  • CompletionsCache ,这个cache默认是LRU(100)
  • CopilotTokenNotifier ,这是一个事件通知器,里面封装了一个emit方法。
  • CertificateReaderCache ,这是一个key为platform,value为Reader的Map。
  • RootCertificateReader ,真正的证书Reader,用来获取rootCA。
  • ProxySocketFactory ,实际上是一个KerberosProxySocketFactory,使用Kerberos进行身份认证。
  • Fetcher,是一个HelixFetcher的实例,用来发送HTTP请求。
  • LanguageDetection ,实际上是由FilenameAndExensionLanguageDetectionNotebookLanguageDetection组成,统一走CachingLanguageDetection来缓存,推断具体为哪个language的策略还有点复杂。
  • Features,包含一些实验特性。
  • PostInsertionNotifier ,纯粹就是一个eventEmitter。
  • TelemetryUserConfig ,关于telemetry的一些配置,基本上是通过CopilotTokenNotifier这个事件监听得到的。
  • TelemetryEndpointUrl ,维护telemetry的url地址,默认是https://copilot-telemetry.githubusercontent.com/telemetry。
  • TelemetryReporters ,维护了telemetry的reporter。
  • HeaderContributors ,维护一个Contributors 的列表。
  • UserErrorNotifier ,用来处理证书相关的异常?
  • ContextualFilterManager ,管理ContextualFilter。
  • OpenAIFetcher ,被实例化为LiveOpenAIFetcher ,适配了OpenAI的返回格式。
  • BlockModeConfig ,实例化为ConfigBlockModeConfig,跟indent配置有关。
  • UrlOpener ,被实例化为RealUrlOpener ,实现了一个open方法。
  • ExpConfigMaker ,实验特性标记,这里被实例化为一个ExpConfigNone ,默认不拉实验特性。
  • PromiseQueue ,一个promise队列。
  • SnippetOrchestrator ,snippet编排。
  • ForceMultiLine ,看起来是强制multiline。

至此整个createProductionContext 的流程就结束了。接下来看一下createExtensionContext 的流程:

代码语言:javascript
复制
logTarget = new MultiLog([new ConsoleLog(console), new OutputChannelLog(outputChannel)]);
ctx.forceSet(LogTarget, logTarget);

首先重置了一下logTarget,同时向console和outputchannel输出。

然后将EditorAndPluginInfo 设置为VSCodeEditorInfo ,封装了一些基本的信息。

接着初始化了initProxyEnvironment proxy的逻辑,监听proxy的变化确保proxy能够正常。

接着初始化了以下服务:

  • NotificationSender ,一个通知的服务,调用showWarningMessage
  • EditorSession ,管理session周期
  • Extension , Extension相关,context存在这里。
  • EditorExperimentFilters ,设置为VSCodeEditorExperimentFilters ,看起来是加了X-VSCode-Build,X-VSCode-Language两个属性。

然后调用了setupExperimentationService

代码语言:javascript
复制
function setupExperimentationService(ctx) {
  let features = ctx.get(Features);
  features.registerStaticFilters(createAllFilters(ctx)),
  features.registerDynamicFilter("X-Copilot-OverrideEngine", () => getConfig(ctx, ConfigKey.DebugOverrideEngine));
}

这里面就是设置了一些基础的头信息:

  • X-VSCode-AppVersion
  • X-MSEdge-ClientId
  • X-VSCode-ExtensionName
  • X-VSCode-ExtensionVersion
  • X-VSCode-TargetPopulation

接着定义了两个服务:

  • SymbolDefinitionProvider ,定义了SymbolDefinition。
  • CopilotExtensionStatus ,定义了当前插件的状态和报错信息。

接着区分了环境,正式环境中的定义

  • CopilotTokenManager,这个是指copilot的登录token管理。
  • ExpConfigMaker 重新指向为ExpConfigFromTAS ,也就是默认从TAS平台上拉取实验特性数据。

接着初始化setupTelemetry 的逻辑。

然后还有一系列其他服务的设置:

  • LocationFactory,初始化一个location的工具类。
  • TextDocumentManager ,负责TextDocument相关的处理。
  • WorkspaceFileSystem ,workspace关于文件相关的处理。
  • CommitFileResolver ,提交文件相关的处理。
  • FileSystem ,文件相关的处理。
  • NetworkConfiguration ,主要是访问github的URL地址。

入口主逻辑梳理

入口主逻辑细枝末节比较多,这里画图做个总结:

image

在入口初始化中,最重要的是标红的两步:

  • registerGhostTextSupport ,这个注册了整个InlineCompletion,也就是我们的代码提示都是走这个逻辑。
  • snippetOrchestrator.startThreading ,这个开启了一个worker线程,接下来我们详细分析一下。

关于worker线程

copilot将比较耗时的操作都放到了worker线程去,比如下面的init方法:

代码语言:javascript
复制
var promptlib = Ns(Dc());
var worker = null;

var handlers = new Map();
var nextHandlerId = 0;

function init(ctx, use_worker_threads, logger) {
  if (!use_worker_threads) {
    let localPromptlib = (uL(), nT(Pre));
    for (let fn of allFuns) updatePromptLibProxyFunction(fn, localPromptlib[fn]);
    return;
  }

  for (let fn of workerFuns) updatePromptLibProxyFunction(fn, proxy(ctx, logger, fn));

  promptLibProxy.getPrompt = getPromptProxy(ctx, logger);
  worker = X0.createWorker();
  handlers.clear();
  nextHandlerId = 0;

  worker.on("message", ({ id, err, code, res }) => {
    let handler = handlers.get(id);
    logger.debug(ctx, `Response ${id} - ${res}, ${err}`);
    if (handler) {
      handlers.delete(id);
      if (err) {
        err.code = code;
        handler.reject(err);
      } else {
        handler.resolve(res);
      }
    }
  });

  function handleError(maybeError) {
    let err;
    if (maybeError instanceof Error) {
      err = maybeError;
      if (err.code === "MODULE_NOT_FOUND" && err.message?.endsWith("worker.js'")) {
        err = new Error("Failed to load worker.js");
        err.code = "CopilotPromptLoadFailure";
      }
      let ourStack = new Error().stack;
      if (err.stack && ourStack?.match(/^Error\n/)) {
        err.stack += ourStack.replace(/^Error/, "");
      }
    } else if (maybeError?.name === "ExitStatus" && typeof maybeError.status == "number") {
      err = new Error(`worker.js exited with status ${maybeError.status}`);
      err.code = `CopilotPromptWorkerExit${maybeError.status}`;
    } else {
      err = new Error(`Non-error thrown: ${maybeError}`);
    }
    for (let handler of handlers.values()) handler.reject(err);
    handlers.clear();
  }

  __name(handleError, "handleError");
  worker.on("error", handleError);
}
__name(init, "init");

function terminate() {
  if (worker) {
    worker.removeAllListeners();
    worker.terminate();
    worker = null;
    handlers.clear();
  }
}
__name(terminate, "terminate");

var workerFuns = [
  "getFunctionPositions",
  "isEmptyBlockStart",
  "isBlockBodyFinished",
  "getNodeStart",
  "getCallSites",
  "parsesWithoutError"
];

var directFuns = [
  "isSupportedLanguageId",
  "getBlockCloseToken",
  "getPrompt"
];

var allFuns = [...workerFuns, ...directFuns];

function proxy(ctx, logger, fn) {
  return function (...args) {
    let id = nextHandlerId++;
    return new Promise((resolve, reject) => {
      handlers.set(id, { resolve: resolve, reject: reject });
      logger.debug(ctx, `Proxy ${fn}`);
      worker?.postMessage({ id: id, fn: fn, args: args });
    });
  };
}
__name(proxy, "proxy");

function getPromptProxy(ctx, logger) {
  return function (_fileSystem, ...args) {
    let id = nextHandlerId++;
    return new Promise((resolve, reject) => {
      handlers.set(id, { resolve: resolve, reject: reject });
      logger.debug(ctx, `Proxy getPrompt - ${id}`);
      worker?.postMessage({ id: id, fn: "getPrompt", args: args });
    });
  };
}
__name(getPromptProxy, "getPromptProxy");

function updatePromptLibProxyFunction(fn, impl) {
  promptLibProxy[fn] = impl;
}
__name(updatePromptLibProxyFunction, "updatePromptLibProxyFunction");

var promptLibProxy = {
  isEmptyBlockStart: X0.isEmptyBlockStart,
  isBlockBodyFinished: X0.isBlockBodyFinished,
  isSupportedLanguageId: X0.isSupportedLanguageId,
  getBlockCloseToken: X0.getBlockCloseToken,
  getFunctionPositions: X0.getFunctionPositions,
  getNodeStart: X0.getNodeStart,
  getPrompt: X0.getPrompt,
  getCallSites: X0.getCallSites,
  parsesWithoutError: X0.parsesWithoutError
};

可以看到,放在worker线程的方法主要是5个:

  • getFunctionPositions
  • isEmptyBlockStart
  • isBlockBodyFinished
  • getNodeStart
  • getCallSites
  • parsesWithoutError

其中,还有一个方法也被单独代理到worker线程:

  • getPrompt

除了这个worker线程以外,还开了另外一个worker线程workerProxy:

代码语言:javascript
复制
workerFns = ["getNeighborSnippets", "extractLocalImportContext", "sleep"], WorkerProxy = class {
      constructor() {
        this.nextHandlerId = 0;
        this.handlers = new Map();
        this.fns = new Map();
        this.extractLocalImportContext = extractLocalImportContext;
        this.getNeighborSnippets = getNeighborSnippets;
        this.sleep = sleep;
        !Kf.isMainThread && Kf.workerData?.port && (wT(), process.cwd = () => Kf.workerData.cwd, this.configureWorkerResponse(Kf.workerData.port));
      }
      static {
        __name(this, "WorkerProxy");
      }
      initWorker() {
        let {
          port1: port1,
          port2: port2
        } = new Kf.MessageChannel();
        this.port = port1, this.worker = new Kf.Worker((0, yre.resolve)(__dirname, "..", "dist", "workerProxy.js"), {
          workerData: {
            port: port2,
            cwd: process.cwd()
          },
          transferList: [port2]
        }), this.port.on("message", m => this.handleMessage(m)), this.port.on("error", e => this.handleError(e));
      }
      startThreading() {
        if (this.worker) throw new Error("Worker thread already initialized.");
        this.proxyFunctions(), this.initWorker();
      }
      stopThreading() {
        this.worker && (this.worker.terminate(), this.worker.removeAllListeners(), this.worker = void 0, this.unproxyFunctions(), this.handlers.clear());
      }
      proxyFunctions() {
        for (let fn of workerFns) this.fns.set(fn, this[fn]), this.proxy(fn);
      }
      unproxyFunctions() {
        for (let fn of workerFns) {
          let originalFn = this.fns.get(fn);
          if (originalFn) this[fn] = originalFn;else throw new Error(`Unproxy function not found: ${fn}`);
        }
      }
      configureWorkerResponse(port) {
        this.port = port, this.port.on("message", async ({
          id: id,
          fn: fn,
          args: args
        }) => {
          let proxiedFunction = this[fn];
          if (!proxiedFunction) throw new Error(`Function not found: ${fn}`);
          try {
            let res = await proxiedFunction.apply(this, args);
            this.port.postMessage({
              id: id,
              res: res
            });
          } catch (err) {
            if (!(err instanceof Error)) throw err;
            typeof err.code == "string" ? this.port.postMessage({
              id: id,
              err: err,
              code: err.code
            }) : this.port.postMessage({
              id: id,
              err: err
            });
          }
        });
      }
      handleMessage({
        id: id,
        err: err,
        code: code,
        res: res
      }) {
        let handler = this.handlers.get(id);
        handler && (this.handlers.delete(id), err ? (err.code = code, handler.reject(err)) : handler.resolve(res));
      }
      handleError(maybeError) {
        console.log(maybeError);
        let err;
        if (maybeError instanceof Error) {
          err = maybeError, err.code === "MODULE_NOT_FOUND" && err.message?.endsWith("workerProxy.js'") && (err = new Error("Failed to load workerProxy.js"), err.code = "CopilotPromptLoadFailure");
          let ourStack = new Error().stack;
          err.stack && ourStack?.match(/^Error\n/) && (err.stack += ourStack.replace(/^Error/, ""));
        } else maybeError?.name === "ExitStatus" && typeof maybeError.status == "number" ? (err = new Error(`workerProxy.js exited with status ${maybeError.status}`), err.code = `CopilotPromptWorkerExit${maybeError.status}`) : err = new Error(`Non-error thrown: ${maybeError}`);
        for (let handler of this.handlers.values()) handler.reject(err);
        throw err;
      }
      proxy(fn) {
        this[fn] = function (...args) {
          let id = this.nextHandlerId++;
          return new Promise((resolve, reject) => {
            this.handlers.set(id, {
              resolve: resolve,
              reject: reject
            }), this.port?.postMessage({
              id: id,
              fn: fn,
              args: args
            });
          });
        };
      }
    }, workerProxy = new WorkerProxy();

这里的通信代理和第一个worker线程的代理机制大同小异,这次代理的是与snippets相关的几个方法:

  • getNeighborSnippets
  • extractLocalImportContext
  • sleep

将这些昂贵的操作放在worker线程中,保障了整体主线程的性能不会卡顿。

小结一下

本文主要分析了copilot入口函数的整体逻辑,最重要的是两大块内容:

  • Context初始化
  • 注册ghostText并开启worker线程

在Context部分,copilot所有的实例都是通过挂在容器的方式形成单例的,一个好处就是对于一个类可以有多个实现,只需要替换掉不同的Instance即可,这也符合开闭设计原则

在一系列初始化完成之后,copilot登录通过后,会注册到ghostText,开启inlineCompletion的模式,这也就是我们在copilot中体验到的代码补全的核心功能。

另外copilot还开启了两个worker线程,分别代理了snippet相关和Prompt相关的几个函数,这些函数默认在非开发环境下会在worker线程跑,从而保障了主进程更优的性能

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-11-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 孟健的前端认知 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • copilot的入口函数
  • 关于Context的初始化
  • 入口主逻辑梳理
  • 关于worker线程
  • 小结一下
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档