Vite中如何优雅的使用WebWorker多线程利器`Comlink`开发

377 阅读2分钟
import { MessageReader } from '@foxglove/rosmsg-serialization';
import { compare } from '@foxglove/rostime';
import { Bag } from '@foxglove/rosbag';
import { BlobReader } from '@foxglove/rosbag/web';
import { parse as parseMessageDefinition } from '@foxglove/rosmsg';

import { Topic } from '@/foxglove/packages/studio-base/players/types';
import {
  GetBackfillMessagesArgs,
  MessageIteratorArgs
} from '@/foxglove/packages/studio-base/players/IterablePlayer/IIterableSource';
import { IteratorResult } from '@/foxglove/packages/studio-base/players/IterablePlayer/IIterableSource';
import { MessageEvent } from '@/foxglove/packages/studio/src';
type BagSource = { type: 'file'; file: File };

export default class BagIterableSource {
  #abort: Promise<boolean>;
  #abortResolve: (value: boolean | PromiseLike<boolean>) => void;
  #source: BagSource;
  #bag: Bag;
  #topics: Map<string, Topic>;
  #readersByConnectionId: Map<number, MessageReader>;
  constructor(source: BagSource) {
    this.#abort = new Promise(r => {
      this.#abortResolve = r;
    });
    this.#source = source;
  }

  public async initialize(): Promise<any> {
    this.#bag = new Bag(new BlobReader(this.#source.file));
    const bag = this.#bag;
    await bag.open();
    console.log('open bag', bag);
    const readersByConnectionId = new Map<number, MessageReader>();
    const topics = new Map<string, Topic>();
    for (const [id, connection] of bag.connections) {
      const schemaName = connection.type;
      if (!schemaName) {
        continue;
      }
      const parsedDefinition = parseMessageDefinition(
        connection.messageDefinition
      );
      const reader = new MessageReader(parsedDefinition);
      readersByConnectionId.set(id, reader);

      const existingTopic = topics.get(connection.topic);

      if (!existingTopic) {
        topics.set(connection.topic, { name: connection.topic, schemaName });
      }
      // topics.set(connection.topic, )
    }

    this.#topics = topics;
    this.#readersByConnectionId = readersByConnectionId;
    return {
      topics: Array.from(topics.values()),
      // topicStats,
      start: this.#bag.startTime ?? { sec: 0, nsec: 0 },
      end: this.#bag.endTime ?? { sec: 0, nsec: 0 },
      // problems,
      profile: 'ros1'
      // datatypes,
      // publishersByTopic,
    };
  }

  public async *messageIterator(
    opt: MessageIteratorArgs
  ): AsyncIterableIterator<Readonly<IteratorResult>> {
    yield* this.#messageIterator({ ...opt, reverse: false });
  }
  async *#messageIterator(
    opt: MessageIteratorArgs & { reverse: boolean }
  ): AsyncGenerator<Readonly<IteratorResult>> {
    const iterator = this.#bag.messageIterator({
      topics: Array.from(this.#topics.keys())
      // reverse: opt.reverse,
      // start: opt.start,
    });

    const end = this.#bag.endTime;
    for await (const bagMsgEvent of iterator) {
      // 超过结束时间,退出循环
      if (end && compare(bagMsgEvent.timestamp, end) > 0) {
        return;
      }
      const connectionId = bagMsgEvent.connectionId;
      const reader = this.#readersByConnectionId.get(connectionId);
      if (reader) {
        // bagMsgEvent.data is a view on top of the entire chunk. To avoid keeping references for
        // chunks (which will fill up memory space when we cache messages) when make a copy of the
        // data.
        const dataCopy = bagMsgEvent.data.slice();
        const parsedMessage = reader.readMessage(dataCopy);

        return {
          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;
  }

  id: number = 0;
  async *start() {
    for (;;) {
      if (this.id === 100) {
        return this.id;
      }
      await wait((Math.random() * 3 + 0) * 1000); // 3~5s
      await this.#abort;
      this.id++;
      yield this.id;
    }
  }

  async next() {
    return await this.start().next();
  }
  stop() {
    this.#abort = new Promise(r => {
      this.#abortResolve = r;
    });
  }
  open() {
    this.#abortResolve(true);
  }
}
function wait(arg0: number) {
  return new Promise<void>(r => {
    setTimeout(() => {
      r();
    }, arg0);
  });
}

import * as Comlink from 'comlink';
import BagIterableSource from './BagIterableSource';
import { DataSourceFactoryInitializeArgs } from '@/foxglove/packages/studio-base/context/PlayerSelectionContext';
import { Player } from '@/foxglove/packages/studio-base/players/types';
import { WorkerIterableSource } from './WorkerIterableSource';
import { IterablePlayer } from './IterablePlayer';
import { IIterableSource } from '@/foxglove/packages/studio-base/players/IterablePlayer/IIterableSource';

export default class Ros1LocalBagDataSourceFactory {
  public id = 'ros1-local-bagfile';
  public initialize(args: DataSourceFactoryInitializeArgs): Player | undefined {
    const file = args.file;
    if (!file) {
      return;
    }

    const source = new WorkerIterableSource({
      initWorker: () => {
        return new Worker(
          new URL('./BagIterableSourceWorker.Worker.ts', import.meta.url),
          {
            type: 'module'
          }
        );
      },
      initArgs: { file }
    }) as unknown as IIterableSource;

    return new IterablePlayer({
      // metricsCollector: args.metricsCollector,
      source,
      name: file.name,
      sourceId: this.id
    });
  }
  // async start() {
  //   const thread = new Worker(
  //     new URL('./BagIterableSourceWorker.Worker.ts', import.meta.url),
  //     {
  //       type: 'module'
  //     }
  //   );

  //   const initialize =
  //     Comlink.wrap<() => Comlink.Remote<BagIterableSource>>(thread);

  //   const worker = await initialize();
  //   console.log('worker', await worker);
  //   setTimeout(async () => {
  //     console.log('3s开始播放');
  //     await worker.open();
  //   }, 3000);
  //   setTimeout(async () => {
  //     console.log('10s结束播放');
  //     await worker.stop();
  //   }, 10000);
  //   try {
  //     for (;;) {
  //       const value = await worker.next();
  //       console.log(value.value);
  //     }
  //   } catch (e) {
  //     console.log(e);
  //   }
  // }
}

  • tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext", "Webworker"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "paths": {
      "@/*": ["3D/*"]
    },
    "baseUrl": ".",
    "outDir": "./dist",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "references": [{ "path": "./tsconfig.node.json" }],
  "include": [
    "3D/**/*.ts",
    "3D/**/*.d.ts",
    "3D/**/*.tsx",
    "./typings/**/*.d.ts"
, "3D/rosPlay/player/BagIterableSourceWorker.Worker.js"  ],
  "exclude": ["node_modules", "dist"]
}