智能驾驶开源web软件-Foxglove源码解析

1,160 阅读8分钟

源码解析篇

1、拖动.bag 包

DocumentDropListener.tsx => onDrop() 触发拖拽事件 Workerspace.tsx => dropHandler() 初始化内容后,触发 dropHandler,检查拖拽文件名后缀,符合哪个 ros 得版本 PlayerManager.tsx => selectSource()

1.2、source 中含有 ros1bag 包得解析类 Ros1LocalBagDataSourceFactory.ts

packages\studio-web\src\Root.tsx => const sources = [new Ros1LocalBagDataSourceFactory.ts()]

1.3、校验文件

这里之分析单文件部分: packages\studio-base\src\components\PlayerManager.tsx: selectSource

  • 通过ev.dataTransfer.items 来获取当前拖拽的文件对象 DataTransferItemList
  • 通过DataTransferItem.getAsFileSystemHandle() 来生成文件句柄系统句柄(判断是否有读权限,并申请读权限)
  • 通过DataTransferItem.webkitGetAsEntry() 来生成文件入口(判断是否是文件)

2、初始化 Player

2.1、初始化 basePlayer

  • 默认会初始化一个进度条控制器: AnalyticsMetricsCollector
// packages\studio-base\src\components\PlayerManager.tsx:87
const metricsCollector = useMemo(
  () => new AnalyticsMetricsCollector(analytics),
  [analytics]
)
  • 第一步校验成功后,开始初始化文件和控制器的耦合部分,其是通过 webworker 和事件机制来计算内容的
// packages\studio-base\src\components\PlayerManager.tsx:286
const newPlayer = foundSource.initialize({
  file,
  metricsCollector,
})
// 此时newPlayer是一个IterablePlayer类
// packages\studio-base\src\players\IterablePlayer\IterablePlayer.ts
setBasePlayer(newPlayer)

2.2 basePlayer 副作用

  • 此时 basePlayer 实例为IterablePlayer类的实例:packages\studio-base\src\players\IterablePlayer\IterablePlayer.ts

basePlayer 变更会导致 topicAliasPlayer 变更 topicAliasPlayer 变更会导致 player 变更 player 变更会导致 MessagePipelineProvider 上下文组件的 player 变更

2.3 player 设置监听器

  • 上述副作用中,player 变更会重新设置监听器
  • 此时 player 实例为UserNodePlayer类的实例
  • UserNodePlayer类: packages\studio-base\src\players\UserNodePlayer\index.ts
// packages\studio-base\src\components\MessagePipeline\index.tsx: 139
const { listener, cleanupListener } = createPlayerListener({
  msPerFrameRef,
  promisesToWaitForRef,
  dispatch,
})
/** @core */
player.setListener(listener)
  • 此时 player 实例层级关系为: player => UserNodePlayer player.#player => TopicAliasingPlayer player.#player.#player => IterablePlayer

2.3.1 player setListener()

  • setListener() 函数会递归调用 player 实例的 setListener()
  • UserNodePlayer => TopicAliasingPlayer => IterablePlayer
  • 在 IterablePlayer 类中,setListener 调用了 setState,setState 调用了 runState,runState 中的 initialize 状态调用了 stateInitialize
class IterablePlayer {
  public setListener(
    listener: (playerState: PlayerState) => Promise<void>
  ): void {
    // ...
    this.#setState('initialize')
  }

  /** Request the state to switch to newState */
  #setState(newState: IterablePlayerState) {
    // ...
    this.#nextState = newState
    this.#abort?.abort()
    this.#abort = undefined
    void this.#runState()
  }

  /**
   * Run the requested state while there is a state to run.
   *
   * Ensures that only one state is running at a time.
   * */
  async #runState() {
    if (this.#runningState) {
      return
    }
    try {
      while (this.#nextState) {
        switch (state) {
          // ...
          case 'initialize':
            await this.#stateInitialize()
            break
          // ...
        }
        // ...
      }
    } catch (err) {
      // ...
      this.#queueEmitState()
    } finally {
      this.#runningState = false
    }
  }
}

2.3.2 player 初始化源与播放器 #stateInitialize()

  • 主要就是通过3步骤拿到bag包的各种信息
export class IterablePlayer implements Player {
// ...
// Initialize the source and player members
  async #stateInitialize(): Promise<void> {
    // emit state indicating start of initialization
    this.#queueEmitState();

    try {
      const {
        start,
        end,
        topics,
        profile,
        topicStats,
        problems,
        publishersByTopic,
        datatypes,
        name,
      } = await this.#bufferedSource.initialize(); // 这里开始执行第三步,解包步骤


      if (this.#enablePreload) {
        // 设置块加载器,在“后台”加载_full_订阅的消息
        // --- setup block loader which loads messages for _full_ subscriptions in the "background"
        try {
          // 这里主要就是创建了一个BlockLoader类实例,具体做了什么后续更新 @todo
          this.#blockLoader = new BlockLoader({
            cacheSizeBytes: DEFAULT_CACHE_SIZE_BYTES, // 1GB
            source: this.#iterableSource, // WorkerIterableSource
            start: this.#start,
            end: this.#end,
            maxBlocks: MAX_BLOCKS, // 400
            minBlockDurationNs: MIN_MEM_CACHE_BLOCK_SIZE_NS, // 0.1GB
            problemManager: this.#problemManager,
          });
        } catch (err) {
          // ...
        }
    }
    cache(error) {
      // ...
    }
    
    this.#queueEmitState();

    
    if (!this.#hasError && this.#start) {
      // 天知道为什么要延迟100ms执行,可能是内部有bug吧,源码注释是这样的:
      // Amount to wait until panels have had the chance to subscribe to topics before
      // we start playback
      await delay(START_DELAY_MS);

      this.#blockLoader?.setTopics(this.#preloadTopics);

      // Block loadings is constantly running and tries to keep the preloaded messages in memory
      this.#blockLoadingProcess = this.#startBlockLoading();

      this.#setState("start-play"); // 进入2.3.3 步骤
    }
  }
}

2.3.3 初始阅读部分消息

  • this.#setState("start-play") 将执行 this.#stateStartPlay()

  • 从数据源读取少量数据,希望生成一两条消息。

  • 如果没有初始阅读,用户将看到空白布局,因为尚未传递任何消息。

  • 开始阅读bag包数据,生成一个bagbbuffer消息迭代器

// packages\studio-base\src\players\IterablePlayer\IterablePlayer.ts
class IterablePlayer {
// ...
  #stateStartPlay() {
    // ...

    // `this.#bufferedSource` 此时的 为 `BufferedIterableSource.ts`类的实例
    this.#playbackIterator = this.#bufferedSource.messageIterator({
      topics: Array.from(this.#allTopics),
      start: this.#start,
      consumptionType: "partial",
    });

    // ...

    // If we take too long to read the data, we set the player into a BUFFERING presence. This
    // indicates that the player is waiting to load more data.
    // 如果我们读取数据的时间过长,我们会将玩家设置为缓冲状态。 这表明播放器正在等待加载更多数据。
    const tickTimeout = setTimeout(() => {
      this.#presence = PlayerPresence.BUFFERING;
      this.#queueEmitState();
    }, 100);
    try {
      // 
      for (;;) {
        const result = await this.#playbackIterator.next();
        if (result.done === true) {
          break;
        }
        const iterResult = result.value;
          messageEvents.push(iterResult.msgEvent);
        }
      }
    } finally {
      // 这里我理解如果数据缓冲时间比100ms要更快,就直接播放了,并清空定时器
      clearTimeout(tickTimeout);
    }

    this.#currentTime = stopTime;
    this.#messages = messageEvents;
    this.#presence = PlayerPresence.PRESENT;
    this.#queueEmitState(); // 触发状态队列事件通知
    this.#setState("idle"); // 进入下一步,idle状态
  }
}
  • 我们看看上述this.#bufferedSource.messageIterator发生了什么
// packages\studio-base\src\players\IterablePlayer\BufferedIterableSource.ts
class BufferedIterableSource {
  public messageIterator(
    args: MessageIteratorArgs,
  ): AsyncIterableIterator<Readonly<IteratorResult>> {
    // ...

    // 当调用 messageIterator 函数时创建并启动生产者。
    // Create and start the producer when the messageIterator function is called.
    this.#producer = this.#startProducer(args);

    const self = this;
    return (async function* bufferedIterableGenerator() {
      try {
        // 如果没有订阅的topic类型,直接退出
        if (args.topics.length === 0) {
          return;
        }

        for (;;) {
          // 这里巧妙的使用for(;;)进行无限循环,在无数据时退出循环
          const item = self.#cache.dequeue();
          if (!item) {
            if (self.#readDone) {
              break;
            }

            // Wait for more stuff to load
            await self.#readSignal.wait();
            continue;
          }

          // ...

          yield item;
        }
      } finally {
        log.debug("ending buffered message iterator");
        await self.stopProducer();
      }
    })();
  }
}
  • 上述代码中返回了一个消息迭代器,核心逻辑为this.#producer = this.#startProducer(args);,没有它,数据将不会读取。
class {
  // ...
async #startProducer(args: MessageIteratorArgs): Promise<void> {
  // ...
  try {
    // 此时 #source => CachingIterableSource
    const sourceIterator = this.#source.messageIterator({
      topics: args.topics,
      start: this.#readHead,
      consumptionType: "partial",
    });

    // ...

    // 主动执行迭代器
    for await (const result of sourceIterator) {
      // ...

      // 将结果添加到缓存队列中
      this.#cache.enqueue(result);
    
      // ...
    }
  } finally {
    // Indicate to the consumer that it can try reading again
    this.#readSignal.notifyAll();
    this.#readDone = true;
  }

  log.debug("producer done");
}
}
  • 可以看到this.#source.messageIterator就像一个迭代过程一样,从自身的源一步一步修饰出最终的内容。
  • 我们看下CachingIterableSource实例中的messageIterator函数做了什么。
// packages\studio-base\src\players\IterablePlayer\CachingIterableSource.ts
class CachingIterableSource extends EventEmitter<EventTypes> implements IIterableSource {
  public async *messageIterator(
    args: MessageIteratorArgs,
  ): AsyncIterableIterator<Readonly<IteratorResult>> {
    // 此时的#source为 WorkerIterableSource 实例
    const sourceMessageIterator = this.#source.messageIterator({
      topics: this.#cachedTopics,
      start: sourceReadStart,
      end: sourceReadEnd,
      consumptionType: args.consumptionType,
    });
    for await (const iterResult of sourceMessageIterator) {
      // ...
      yield iterResult;
    }
  }

}

  • 我们看下WorkerIterableSource实例中的messageIterator函数做了什么。
// packages\studio-base\src\players\IterablePlayer\WorkerIterableSource.ts

export class WorkerIterableSource implements IIterableSource {
    // ...
  public async initialize(): Promise<Initalization> {
    // Note: this launches the worker.
    this.#thread = this.#args.initWorker();

    const initialize = Comlink.wrap<
      (args: IterableSourceInitializeArgs) => Comlink.Remote<WorkerIterableSourceWorker>
    >(this.#thread);
    
    const worker = (this.#worker = await initialize(this.#args.initArgs));
  }

  public async *messageIterator(
    args: MessageIteratorArgs,
  ): AsyncIterableIterator<Readonly<IteratorResult>> {
    // ...
    const cursor = this.getMessageCursor(args);
    try {
      for (;;) {
        const results = await cursor.nextBatch(17);
        if (!results || results.length === 0) {
          break;
        }
        yield* results;
      }
    } finally {
      await cursor.end();
    }
  }


  public getMessageCursor(args: MessageIteratorArgs & { abort?: AbortSignal }): IMessageCursor {
    // ...
    const { abort, ...rest } = args;

    // 请跳转到 2.3.3.1
    const messageCursorPromise = this.#worker.getMessageCursor(rest, abort);

    const cursor: IMessageCursor = {
      // ...
    // 请跳转到 2.3.3.2
      async nextBatch(durationMs: number) {
        const messageCursor = await messageCursorPromise;
        return await messageCursor.nextBatch(durationMs);
      },
    };

    return cursor;
  }
}

  • 回忆一下流程:

  • 1、initialize() 是何时调用的?

请跳转到2.1步骤,baseplayer memoRos1LocalBagDataSourceFactory.initialize()的产物

后面通过2.2步骤,设置监听器,一步一步initialize到了WorkerIterableSource.initialize()

  • 2、this.#thread 是什么

url为BagIterableSourceWorker.worker.ts的Worker实例

  • 3、initialize是什么

Comlink代理的 this.#thread 返回的 抛出的initialize函数

initialize()执行的时候,函数内部返回了:源为BagIterableSourceWorkerIterableSourceWorker实例将被Comlink代理

  • 3、this.#worker 是什么

WorkerIterableSourceWorker实例

2.3.3.1 this.#worker.getMessageCursor 干了什么

// packages\studio-base\src\players\IterablePlayer\WorkerIterableSourceWorker.ts
export class WorkerIterableSourceWorker implements IIterableSource {
  // ...
  public getMessageCursor(
    args: Omit<MessageIteratorArgs, "abort">,
    abort?: AbortSignal,
  ): IMessageCursor & Comlink.ProxyMarked {

    // this._source => BagIterableSource
    const iter = this._source.messageIterator(args);
    // 初始化了一个IteratorCursor
    const cursor = new IteratorCursor(iter, abort);
    return Comlink.proxy(cursor);
  }
}

// 待分析
Comlink.transferHandlers.set("abortsignal", abortSignalTransferHandler);

2.3.3.2 messageCursor.nextBatch 干了什么

  • 首先需要介绍下IteratorCursor实例的依赖BagIterableSource.messageIterator()
export class BagIterableSource implements IIterableSource {
  async *#messageIterator(
    opt: MessageIteratorArgs & { reverse: boolean },
  ): AsyncGenerator<Readonly<IteratorResult>> {
    // ...

    // 待分析
    const iterator = this.#bag.messageIterator({
      topics: opt.topics,
      reverse: opt.reverse,
      start: opt.start,
    });

    const readersByConnectionId = this.#readersByConnectionId;
    for await (const bagMsgEvent of iterator) {
      const connectionId = bagMsgEvent.connectionId;
      const reader = readersByConnectionId.get(connectionId);

      // ...

      if (reader) {
        const dataCopy = bagMsgEvent.data.slice();
        const parsedMessage = reader.readMessage(dataCopy);

        yield {
          type: "message-event",
          connectionId,
          msgEvent: {
            topic: bagMsgEvent.topic,
            receiveTime: bagMsgEvent.timestamp,
            sizeInBytes: bagMsgEvent.data.byteLength,
            message: parsedMessage,
            schemaName,
          },
        };
      } 
    }
  }

  public async getBackfillMessages({
    topics,
    time,
  }: GetBackfillMessagesArgs): Promise<MessageEvent[]> {
    const messages: MessageEvent[] = [];
    for (const topic of topics) {
      // NOTE: An iterator is made for each topic to get the latest message on that topic.
      // An single iterator for all the topics could result in iterating through many
      // irrelevant messages to get to an older message on a topic.
      for await (const result of this.#messageIterator({
        topics: [topic],
        start: time,
        reverse: true,
      })) {
        if (result.type === "message-event") {
          messages.push(result.msgEvent);
        }
        break;
      }
    }
    messages.sort((a, b) => compare(a.receiveTime, b.receiveTime));
    return messages;
  }
}

3、bag 解包过程

3.1、核心解包库

  • "@foxglove/rosbag";
  • "@foxglove/rosbag/web";

3.2、核心解包过程

Ros1LocalBagDataSourceFactory.ts 中: initialize() 方法创建了一个 WorkerIterableSource 类实例; WorkerIterableSource 类的构造函数参数中,initworker 属性是函数,将 BagIterableSourceWorker.worker.ts 作为 worker 任务文件作为的返回值 initialize() 返回一个 IterablePlayer 类实例,source 为上述 WorkerIterableSource 实例

3.2.1、执行 worker 任务

// BagIterableSourceWorker.worker.ts
export function initialize(
  args: IterableSourceInitializeArgs
): WorkerIterableSourceWorker {
  if (args.file) {
    const source = new BagIterableSource({ type: 'file', file: args.file })
    const wrapped = new WorkerIterableSourceWorker(source)
    return Comlink.proxy(wrapped)
  }
  // ...
  throw new Error('file or url required')
}

3.2.2、初始化 BagIterableSource 为 source

  • BagIterableSource 的 initialize() 函数为解包代码
  • 通过@foxglove/rosbag 进行解包
// packages\studio-base\src\players\IterablePlayer\BagIterableSource.ts:44
//

3.2.3、初始化 WorkerIterableSourceWorker 为 wrapped

// packages\studio-base\src\players\IterablePlayer\WorkerIterableSourceWorker.worker.ts:44
// initialize()

3.3 读包过程

4、播放器

4.1 拖拽进度条

4.1.1 进度条组件

  • 点击进度条时,会调用onchange方法中的onSeek
// packages\studio-base\src\components\PlaybackControls\Scrubber.tsx
export default function Scrubber(props: Props): JSX.Element {

  const { onSeek } = props;
  // ...
  const onChange = useCallback(
    (fraction: number) => {
      if (!latestStartTime.current || !latestEndTime.current) {
        return;
      }
      onSeek(
        addTimes(
          latestStartTime.current,
          fromSec(fraction * toSec(subtractTimes(latestEndTime.current, latestStartTime.current))),
        ),
      );
    },
    [onSeek, latestEndTime, latestStartTime],
  );

  return <>
    {/** ... */}
    <Slider
      {/** ... */}
      onChange={onChange}
    />
  </>
}

4.1.2 seek操作

  • PlaybackControls.tsx中将父层的seek传入到Scrubber.tsx
// packages\studio-base\src\components\PlaybackControls\index.tsx
  const { play, pause, seek, isPlaying, getTimeInfo, playUntil } = props;
// ...
function () {
  return <>
    <Scrubber onSeek={seek} />
  </>
}
  • Workspace.tsx中将上下文中的playerseekPlayback()方法传递给PlaybackControls.tsx
  • 由步骤2.2我们已知,ctxUserNodePlayer

const selectSeek = (ctx: MessagePipelineContext) => ctx.seekPlayback;

function WorkspaceContent(props: WorkspaceProps): JSX.Element {
  // ...
  const seek = useMessagePipeline(selectSeek);
  // ...
  return <>
    <PlaybackControls
      seek={seek}
      {/**... */}
    />
  </>
}

5、事件推送

  • packages\studio-base\src\players\IterablePlayer\IterablePlayer.ts
  • constructor => this.#queueEmitState =
  • #runState() => #stateStartPlay() => this.#queueEmitState() => this.#emitStateImpl()

启发篇

1、如何设计一个日志系统,可以打印文件的位置?

webpack.config.node.__filename this.info = console.info.bind(global.console);

2、Comlink.wrap 是什么? 类似一个 Proxy 化得库

import * as Comlink from "comlink";

Web 多线程开发利器 Comlink 的剖析与思考 juejin.cn/post/705208…

Github 链接: github.com/GoogleChrom… Comlink 让 WebWorkers 变得有趣。Comlink 是一个小型库(1.1kB),它消除了思考的心理障碍 postMessage 并隐藏了您正在与工人一起工作的事实。 postMessage 在更抽象的层面上,它是 ES6 Proxies 的 RPC 实现。

3、wasm-bz2 的使用

import Bzip2 from "@foxglove/wasm-bz2"; 源码是个 foxglove 库

4、typescript

class #属性 编译出来是什么?

5、uuid 来设置类的 uid

github.com/uuidjs/uuid…

6、async-mutex 库

7、 vite 中无法在多环境中识别 global 关键字

define: {
    global: 'window',
  }