Bas*_*asj 5 javascript performance arraybuffer html5-canvas webassembly
我<canvas>每 100 毫秒更新一次来自 HTTP 请求的位图图像数据:
var ctx = canvas.getContext("2d");
setInterval(() => {
fetch('/get_image_data').then(r => r.arrayBuffer()).then(arr => {
var byteArray = new Uint8ClampedArray(arr);
var imgData = new ImageData(byteArray, 500, 500);
ctx.putImageData(imgData, 0, 0);
});
}, 100);
Run Code Online (Sandbox Code Playgroud)
这在/get_image_data给出 RGBA 数据时有效。就我而言,由于 alpha 始终为 100%,因此我不会通过网络发送 A 通道。问题:
(我们能否避免for在 Javascript 中每秒 10 次处理兆字节数据时可能会很慢的循环?)
灰度 => RGBA 情况下的示例:每个输入值..., a, ...应替换为..., a, a, a, 255, ...输出数组中的值。
这是一个纯 JS 解决方案:1000x1000px 灰度 => RGBA 数组转换约 10 毫秒。
这是WASM 解决方案的尝试。
从 RGB 到 RGBA的转换ArrayBuffer在概念上很简单:只需在每个 RGB 三元组之后拼接一个不透明的 alpha 通道字节 ( 255) 即可。(灰度到 RGBA 也同样简单:对于每个灰度字节:复制 3 次,然后插入一个255。)
这个问题(稍微)更具挑战性的部分是使用wasm或worker将工作卸载到另一个线程。
\n因为您表示熟悉 JavaScript,所以我将提供一个示例,说明如何使用几个实用程序模块在工作程序中完成此操作,并且我将显示的代码将使用 TypeScript 语法。
\n\n\n关于示例中使用的类型:它们非常弱(很多
\nany)\xe2\x80\x94 它们的存在只是为了提供示例中涉及的数据结构的结构清晰度。在强类型的工作线程应用程序代码中,需要根据每个环境(工作线程和主机)中应用程序的具体情况重新编写类型,因为消息传递中涉及的所有类型都是契约类型。
您问题中的问题是面向任务的(对于二进制 RGB 数据的每个特定序列,您需要其 RGBA 对应项)。在这种情况下,不方便的是,Worker API是面向消息的,而不是面向任务的 \xe2\x80\x94 这意味着我们只提供了一个用于侦听每条消息并对其做出反应的接口,而不管其原因或上下文 \xe2\ x80\x94\xc2\xa0没有内置方法来关联来自工作人员的一对特定消息。因此,第一步是在该 API 之上创建一个面向任务的抽象:
\ntask-worker.ts:
export type Task<Type extends string = string, Value = any> = {\n type: Type;\n value: Value;\n};\n\nexport type TaskMessageData<T extends Task = Task> = T & { id: string };\n\nexport type TaskMessageEvent<T extends Task = Task> =\n MessageEvent<TaskMessageData<T>>;\n\nexport type TransferOptions = Pick<StructuredSerializeOptions, \'transfer\'>;\n\nexport class TaskWorker {\n worker: Worker;\n\n constructor (moduleSpecifier: string, options?: Omit<WorkerOptions, \'type\'>) {\n this.worker = new Worker(moduleSpecifier, {...options ?? {}, type: \'module\'});\n\n this.worker.addEventListener(\'message\', (\n {data: {id, value}}: TaskMessageEvent,\n ) => void this.worker.dispatchEvent(new CustomEvent(id, {detail: value})));\n }\n\n process <Result = any, T extends Task = Task>(\n {transfer, type, value}: T & TransferOptions,\n ): Promise<Result> {\n return new Promise<Result>(resolve => {\n const id = globalThis.crypto.randomUUID();\n\n this.worker.addEventListener(\n id,\n (ev) => resolve((ev as unknown as CustomEvent<Result>).detail),\n {once: true},\n );\n\n this.worker.postMessage(\n {id, type, value},\n transfer ? {transfer} : undefined,\n );\n });\n }\n}\n\nexport type OrPromise<T> = T | Promise<T>;\n\nexport type TaskFnResult<T = any> = { value: T } & TransferOptions;\n\nexport type TaskFn<Value = any, Result = any> =\n (value: Value) => OrPromise<TaskFnResult<Result>>;\n\nconst taskFnMap: Partial<Record<string, TaskFn>> = {};\n\nexport function registerTask (type: string, fn: TaskFn): void {\n taskFnMap[type] = fn;\n}\n\nexport async function handleTaskMessage (\n {data: {id, type, value: taskValue}}: TaskMessageEvent,\n): Promise<void> {\n const fn = taskFnMap[type];\n\n if (typeof fn !== \'function\') {\n throw new Error(`No task registered for the type "${type}"`);\n }\n\n const {transfer, value} = await fn(taskValue);\n\n globalThis.postMessage(\n {id, value},\n transfer ? {transfer} : undefined,\n );\n}\n\nRun Code Online (Sandbox Code Playgroud)\n我不会过度解释这段代码:它主要只是在对象之间选择和移动属性,这样您就可以避免应用程序代码中的所有样板文件。值得注意的是:它还抽象了为每个任务实例创建唯一 ID 的必要性。我先讲一下三个出口:
\na class TaskWorker:用于主机 \xe2\x80\x94 中,它是实例化工作模块的抽象,并公开工作模块的属性worker。它还具有一个process方法,该方法接受任务信息作为对象参数并返回处理任务结果的承诺。任务对象参数具有三个属性:
type:要执行的任务类型(更多内容见下文)。这只是一个指向worker中任务处理函数的键。value:相关任务函数将执行的有效负载值transfer:可选的可传输对象数组(稍后我会再次提出)a function registerTask:用于工作程序 \xe2\x80\x94 将任务函数设置为字典中与其关联的类型名称,以便工作程序在收到该类型的任务时可以使用该函数来处理有效负载。
a function handleTaskMessage:要在工作程序 \xe2\x80\x94 中使用,这很简单,但很重要:必须self.onmessage在工作程序模块脚本中将其分配给它。
第二个实用模块具有将 alpha 字节拼接为 RGB 数据的逻辑,还有一个从灰度转换为 RGBA 的函数:
\nrgba-conversion.ts:
/**\n * The bytes in the input array buffer must conform to the following pattern:\n *\n * ```\n * [\n * r, g, b,\n * r, g, b,\n * // ...\n * ]\n * ```\n *\n * Note that the byte length of the buffer **MUST** be a multiple of 3\n * (`arrayBuffer.byteLength % 3 === 0`)\n *\n * @param buffer A buffer representing a byte sequence of RGB data elements\n * @returns RGBA buffer\n */\nexport function rgbaFromRgb (buffer: ArrayBuffer): ArrayBuffer {\n const rgb = new Uint8ClampedArray(buffer);\n const pixelCount = Math.floor(rgb.length / 3);\n const rgba = new Uint8ClampedArray(pixelCount * 4);\n\n for (let iPixel = 0; iPixel < pixelCount; iPixel += 1) {\n const iRgb = iPixel * 3;\n const iRgba = iPixel * 4;\n // @ts-expect-error\n for (let i = 0; i < 3; i += 1) rgba[iRgba + i] = rgb[iRgb + i];\n rgba[iRgba + 3] = 255;\n }\n\n return rgba.buffer;\n}\n\n/**\n * @param buffer A buffer representing a byte sequence of grayscale elements\n * @returns RGBA buffer\n */\nexport function rgbaFromGrayscale (buffer: ArrayBuffer): ArrayBuffer {\n const gray = new Uint8ClampedArray(buffer);\n const pixelCount = gray.length;\n const rgba = new Uint8ClampedArray(pixelCount * 4);\n\n for (let iPixel = 0; iPixel < pixelCount; iPixel += 1) {\n const iRgba = iPixel * 4;\n // @ts-expect-error\n for (let i = 0; i < 3; i += 1) rgba[iRgba + i] = gray[iPixel];\n rgba[iRgba + 3] = 255;\n }\n\n return rgba.buffer;\n}\n\nRun Code Online (Sandbox Code Playgroud)\n我认为迭代数学代码在这里是不言自明的(但是 \xe2\x80\x94 如果这里或答案的其他部分使用的任何API是不熟悉的 \xe2\x80\x94 MDN有解释性文档)。我认为值得注意的是,输入和输出值(ArrayBuffer)都是可传输对象,这意味着它们本质上可以在主机和工作上下文之间移动而不是复制,以提高内存和速度效率。
\n\n另外,感谢@Kaiido提供的信息用于提高此方法相对于本答案早期版本中使用的技术的效率。
\n
由于上面的抽象,实际的工作代码非常少:
\nworker.ts:
import {\n rgbaFromGrayscale,\n rgbaFromRgb,\n} from \'./rgba-conversion.js\';\nimport {handleTaskMessage, registerTask} from \'./task-worker.js\';\n\nregisterTask(\'rgb-rgba\', (rgbBuffer: ArrayBuffer) => {\n const rgbaBuffer = rgbaFromRgb(rgbBuffer);\n return {value: rgbaBuffer, transfer: [rgbaBuffer]};\n});\n\nregisterTask(\'grayscale-rgba\', (grayscaleBuffer: ArrayBuffer) => {\n const rgbaBuffer = rgbaFromGrayscale(grayscaleBuffer);\n return {value: rgbaBuffer, transfer: [rgbaBuffer]};\n});\n\nself.onmessage = handleTaskMessage;\n\nRun Code Online (Sandbox Code Playgroud)\n每个任务函数中所需要做的就是将缓冲区结果移动到value返回对象中的属性,并发出信号表明其底层内存可以传输到主机上下文。
我认为这里不会有任何事情让您感到惊讶:唯一的样板是模拟fetch返回示例 RGB 缓冲区,因为您问题中引用的服务器对此代码不可用:
main.ts:
import {TaskWorker} from \'./task-worker.js\';\n\nconst tw = new TaskWorker(\'./worker.js\');\n\nconst buf = new Uint8ClampedArray([\n /* red */255, 0, 0, /* green */0, 255, 0, /* blue */0, 0, 255,\n /* cyan */0, 255, 255, /* magenta */255, 0, 255, /* yellow */255, 255, 0,\n /* white */255, 255, 255, /* grey */128, 128, 128, /* black */0, 0, 0,\n]).buffer;\n\nconst fetch = async () => ({arrayBuffer: async () => buf});\n\nasync function main () {\n const canvas = document.createElement(\'canvas\');\n canvas.setAttribute(\'height\', \'3\');\n canvas.setAttribute(\'width\', \'3\');\n\n // This is just to sharply upscale the 3x3 px demo data so that\n // it\'s easier to see the squares:\n canvas.style.setProperty(\'image-rendering\', \'pixelated\');\n canvas.style.setProperty(\'height\', \'300px\');\n canvas.style.setProperty(\'width\', \'300px\');\n\n document.body\n .appendChild(document.createElement(\'div\'))\n .appendChild(canvas);\n\n const context = canvas.getContext(\'2d\', {alpha: false})!;\n\n const width = 3;\n\n // This is the part that would happen in your interval-delayed loop:\n const response = await fetch();\n const rgbBuffer = await response.arrayBuffer();\n\n const rgbaBuffer = await tw.process<ArrayBuffer>({\n type: \'rgb-rgba\',\n value: rgbBuffer,\n transfer: [rgbBuffer],\n });\n\n // And if the fetched resource were grayscale data, the syntax would be\n // essentially the same, except that you\'d use the type name associated with\n // the grayscale task that was registered in the worker:\n\n // const grayscaleBuffer = await response.arrayBuffer();\n\n // const rgbaBuffer = await tw.process<ArrayBuffer>({\n // type: \'grayscale-rgba\',\n // value: grayscaleBuffer,\n // transfer: [grayscaleBuffer],\n // });\n\n const imageData = new ImageData(new Uint8ClampedArray(rgbaBuffer), width);\n context.putImageData(imageData, 0, 0);\n}\n\nmain();\n\nRun Code Online (Sandbox Code Playgroud)\n这些 TypeScript 模块只需要编译,然后脚本作为HTML 中的模块脚本main运行。
如果不访问您的服务器数据,我就无法做出性能声明,所以我将把它留给您。如果我在解释中忽略了任何内容(或仍然不清楚的内容),请随时在评论中提问。
\n