源码解析篇
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 memo是Ros1LocalBagDataSourceFactory.initialize()的产物
后面通过2.2步骤,设置监听器,一步一步initialize到了
WorkerIterableSource.initialize()
- 2、this.#thread 是什么
url为
BagIterableSourceWorker.worker.ts的Worker实例
- 3、initialize是什么
Comlink代理的
this.#thread返回的 抛出的initialize函数
当
initialize()执行的时候,函数内部返回了:源为BagIterableSource的WorkerIterableSourceWorker实例将被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中将上下文中的player的seekPlayback()方法传递给PlaybackControls.tsx- 由步骤2.2我们已知,
ctx为UserNodePlayer
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
6、async-mutex 库
7、 vite 中无法在多环境中识别 global 关键字
define: {
global: 'window',
}