WebVR 파트 2: 웹 작업자 및 브라우저 에지 컴퓨팅
게시 됨: 2022-03-11우리의 천체 물리학 시뮬레이터는 희망, 과대 광고 및 새로운 컴퓨팅 성능에 대한 액세스가 결합된 강력한 로켓 연료로 구동됩니다.
우리는 웹 작업자 를 통해 이 컴퓨팅 파워에 액세스할 수 있습니다. 웹 작업자에 대해 이미 익숙하다면 코드를 이해하고 다음 기사에서 논의할 WebAssembly로 건너뛸 수 있습니다.
JavaScript는 정적 웹에 몇 가지 매우 유용한 기능을 가져왔기 때문에 가장 많이 설치되고, 학습되고, 접근 가능한 프로그래밍 언어가 되었습니다.
- 단일 스레드 이벤트 루프
- 비동기 코드
- 쓰레기 수거
- 엄격한 타이핑이 없는 데이터
단일 스레드 는 다중 스레드 프로그래밍의 복잡성과 함정에 대해 크게 걱정할 필요가 없음을 의미합니다.
비동기식 이란 나중에 실행할 매개변수로 함수를 전달할 수 있음을 의미합니다. 즉, 이벤트 루프의 이벤트입니다.
우수한 개발자 도구와 함께 이러한 기능과 Chrome V8 JavaScript 엔진의 성능에 대한 Google의 막대한 투자 덕분에 JavaScript 및 Node.js는 마이크로서비스 아키텍처를 위한 완벽한 선택이 되었습니다.
단일 스레드 실행은 컴퓨터의 여러 코어에서 스파이웨어에 감염된 모든 브라우저 탭 런타임을 안전하게 격리하고 실행해야 하는 브라우저 제조업체에게도 유용합니다.
질문: 하나의 브라우저 탭이 어떻게 컴퓨터의 모든 CPU 코어에 액세스할 수 있습니까?
답변: 웹 작업자!
웹 작업자 및 스레딩
웹 작업자는 이벤트 루프를 사용하여 스레드 간에 비동기식으로 메시지를 전달하여 다중 스레드 프로그래밍의 잠재적인 많은 함정을 우회합니다.
웹 작업자를 사용하여 기본 UI 스레드에서 계산을 이동할 수도 있습니다. 이를 통해 기본 UI 스레드가 클릭, 애니메이션 및 DOM 관리를 처리할 수 있습니다.
프로젝트의 GitHub 리포지토리에서 일부 코드를 살펴보겠습니다.
아키텍처 다이어그램을 기억한다면 전체 시뮬레이션을 nBodySimulator
에 위임하여 웹 작업자를 관리합니다.
소개 게시물에서 기억한다면 nBodySimulator
에는 시뮬레이션의 33ms마다 호출되는 step()
함수가 있습니다. 이는 computeForces calculateForces()
를 호출한 다음 위치를 업데이트하고 다시 그립니다.
// Methods from class nBodySimulator /** * The simulation loop */ start() { // This is the simulation loop. step() calls visualize() const step = this.step.bind(this) setInterval(step, this.simulationSpeed) } /** * A step in the simulation loop. */ async step() { // Skip calculation if worker not ready. Runs every 33ms (30fps), expect it to skip. if (this.ready()) { await this.calculateForces() } else { console.log(`Skipping calculation: WorkerReady: ${this.workerReady} WorkerCalculating: ${this.workerCalculating}`) } // Remove any "debris" that has traveled out of bounds - this is for the button this.trimDebris() // Now Update forces. Reuse old forces if we skipped calculateForces() above this.applyForces() // Ta-dah! this.visualize() }
웹 작업자의 기여는 WebAssembly에 대한 별도의 스레드를 호스팅하는 것입니다. 저수준 언어인 WebAssembly는 정수와 부동 소수점만 이해합니다. JavaScript 문자열 이나 객체 는 전달할 수 없습니다. "선형 메모리"에 대한 포인터만 있을 뿐입니다. 따라서 편의를 위해 "본체"를 float 배열인 arrBodies
로 패키징합니다.
WebAssembly 및 AssemblyScript에 대한 기사에서 이에 대해 다시 설명하겠습니다.
여기에서는 별도의 스레드에서 calculateForces()
를 실행하는 웹 작업자를 만들고 있습니다. 이것은 바디(x, y, z, mass)를 float arrBodies
배열로 마샬링한 다음 this.worker.postMessage()
를 작업자에게 마샬링할 때 아래에서 발생합니다. 나중에 this.worker.onMessage()
에서 작업자가 해결할 약속을 반환합니다.
// src/nBodySimulator.js /** * Use our web worker to calculate the forces to apply on our bodies. */ calculateForces() { this.workerCalculating = true this.arrBodies = [] // Copy data to array into this.arrBodies ... // return promise that worker.onmessage will fulfill const ret = new Promise((resolve, reject) => { this.forcesResolve = resolve this.forcesReject = reject }) // postMessage() to worker to start calculation // Execution continues in workerWasm.js worker.onmessage() this.worker.postMessage({ purpose: 'nBodyForces', arrBodies: this.arrBodies, }) // Return promise for completion // Promise is resolve()d in this.worker.onmessage() below. // Once resolved, execution continues in step() above - await this.calculateForces() return ret }
상단에서 브라우저 GET의 index.html
은 main.js
를 실행하여 new nBodySimulator()
를 생성하고 생성자에서 setupWebWorker()
를 찾습니다.
// nBodySimulator.js /** * Our n-body system simulator */ export class nBodySimulator { constructor() { this.setupWebWorker() ...
우리의 new nBodySimulator()
는 메인 UI 스레드에 있고 setupWebWorker()
는 네트워크에서 workerWasm.js
를 가져와서 웹 작업자를 생성합니다.
// nBodySimulator.js // Main UI thread - Class nBodySimulator method setupWebWorker() { // Create a web worker (separate thread) that we'll pass the WebAssembly module to. this.worker = new Worker("workerWasm.js"); // Console errors from workerWasm.js this.worker.onerror = function (evt) { console.log(`Error from web worker: ${evt.message}`); } ...
new Worker()
에서 브라우저는 별도의 JavaScript 런타임(및 스레드)에서 workerWasm.js
를 가져와 실행하고 메시지 전달을 시작합니다.
그런 다음, workerWasm.js
는 WebAssembly의 핵심으로 들어가지만 실제로는 switch()
문을 포함하는 단일 this.onmessage()
함수일 뿐입니다.
웹 작업자는 네트워크에 액세스할 수 없으므로 기본 UI 스레드는 컴파일된 WebAssembly 코드를 메시지 resolve("action packed")
로 웹 작업자에 전달해야 합니다. 다음 포스팅에서 자세히 알아보도록 하겠습니다.
// workerWasm.js - runs in a new, isolated web worker runtime (and thread) this.onmessage = function (evt) { // message from UI thread var msg = evt.data switch (msg.purpose) { // Message: Load new wasm module case 'wasmModule': // Instantiate the compiled module we were passed. ... // Tell nBodySimulation.js we are ready this.postMessage({ purpose: 'wasmReady' }) return // Message: Given array of floats describing a system of bodies (x, y, z, mass), // calculate the Grav forces to be applied to each body case 'nBodyForces': ... // Do the calculations in this web worker thread synchronously const resultRef = wasm.nBodyForces(dataRef); ... // See nBodySimulation.js' this.worker.onmessage return this.postMessage({ purpose: 'nBodyForces', arrForces }) } }
nBodySimulation
클래스의 setupWebWorker()
메서드로 돌아가서 동일한 onmessage() + switch()
패턴을 사용하여 웹 작업자의 메시지를 수신합니다.

// Continuing class nBodySimulator's setupWebWorker() in the main UI thread // Listen for messages from workerWasm.js postMessage() const self = this this.worker.onmessage = function (evt) { if (evt && evt.data) { // Messages are dispatched by purpose const msg = evt.data switch (msg.purpose) { // Worker's reply that it has loaded the wasm module we compiled and sent. Let the magic begin! // See postmessage at the bottom of this function. case 'wasmReady': self.workerReady = true break // wasm has computed forces for us // Response to postMessage() in nBodySimulator.calculateForces() above case 'nBodyForces': self.workerCalculating = false // Resolve await this.calculateForces() in step() above if (msg.error) { self.forcesReject(msg.error) } else { self.arrForces = msg.arrForces self.forcesResolve(self.arrForces) } break } } } ...
이 예에서 computeForces()는 약속을 저장하는 resolve()
및 reject()
를 self.forcesReject()
및 self.forcesResolve()
calculateForces()
로 생성하고 반환합니다.
이런 식으로 worker.onmessage()
calculateForces()
computeForces() 에서 생성된 프라미스를 해결할 수 있습니다.
시뮬레이션 루프의 step()
함수를 기억한다면:
/** * This is the simulation loop. */ async step() { // Skip calculation if worker not ready. Runs every 33ms (30fps), expect it to skip. if (this.ready()) { await this.calculateForces() } else { console.log(`Skipping calculation: WorkerReady: ${this.workerReady} WorkerCalculating: ${this.workerCalculating}`) }
이를 통해 웹어셈블리가 여전히 계산 중인 경우 computeForces calculateForces()
를 건너뛰고 이전 힘을 다시 적용할 수 있습니다.
이 단계 함수는 33ms마다 실행됩니다. 웹 작업자가 준비되지 않은 경우 이전 힘을 적용하고 페인트합니다. 특정 단계의 calculateForces()
가 다음 단계의 시작을 지나서 작동하면 다음 단계는 이전 단계의 위치에서 힘을 적용합니다. 이러한 이전의 힘은 "올바른" 것처럼 보일 정도로 유사하거나 사용자가 이해할 수 없을 정도로 빠르게 발생합니다. 이 절충안은 실제 인간의 우주 여행에 권장되지 않는 경우에도 인지 성능을 높입니다.
개선될 수 있습니까? 네! 단계 함수에 대한 setInterval
의 대안은 requestAnimationFrame()
입니다.
내 목적을 위해 이것은 Canvas, WebVR 및 WebAssembly를 탐색하기에 충분합니다. 추가되거나 교체될 수 있다고 생각되면 언제든지 의견을 말하거나 연락하십시오.
현대적이고 완전한 물리 엔진 디자인을 찾고 있다면 오픈 소스 Matter.js를 확인하십시오.
WebAssembly는 어떻습니까?
WebAssembly는 브라우저와 시스템에서 작동하는 이식 가능한 바이너리입니다. WebAssembly는 다양한 언어(C/C++/Rust 등)에서 컴파일할 수 있습니다. 내 목적을 위해 저는 JavaScript 기반 언어인 TypeScript 기반 언어인 AssemblyScript를 사용해 보고 싶었습니다.
AssemblyScript는 TypeScript 코드를 이식 가능한 "객체 코드" 바이너리로 컴파일하여 Wasm이라는 새로운 고성능 런타임으로 "적시" 컴파일되도록 합니다. TypeScript를 .wasm
바이너리로 컴파일할 때 바이너리를 설명하는 .wat
사람이 읽을 수 있는 "웹 어셈블리 텍스트" 형식을 만들 수 있습니다.
setupWebWorker()
의 마지막 부분은 WebAssembly에 대한 다음 게시물을 시작하고 네트워크 액세스에 대한 웹 작업자의 제한을 극복하는 방법을 보여줍니다. 메인 UI 스레드에서 wasm
파일을 fetch()
한 다음 "적시" 네이티브 wasm 모듈로 컴파일합니다. 해당 모듈을 웹 작업자에게 메시지로 postMessage()
합니다.
// completing setupWebWorker() in the main UI thread … // Fetch and compile the wasm module because web workers cannot fetch() WebAssembly.compileStreaming(fetch("assembly/nBodyForces.wasm")) // Send the compiled wasm module to the worker as a message .then(wasmModule => { self.worker.postMessage({ purpose: 'wasmModule', wasmModule }) }); } }
그런 다음 workerWasm.js
는 해당 기능을 호출할 수 있도록 해당 모듈을 인스턴스화합니다.
// wasmWorker.js - web worker onmessage function this.onmessage = function (evt) { // message from UI thread var msg = evt.data switch (msg.purpose) { // Message: Load new wasm module case 'wasmModule': // Instantiate the compiled module we were passed. wasm = loader.instantiate(msg.wasmModule, importObj) // Throws // Tell nBodySimulation.js we are ready this.postMessage({ purpose: 'wasmReady' }) return case 'nBodyForces': ... // Do the calculations in this thread synchronously const resultRef = wasm.nBodyForces(dataRef);
이것이 WebAssembly 기능에 액세스하는 방법입니다. 수정되지 않은 소스 코드를 보면 ...
데이터를 dataRef
로 가져오고 결과를 resultRef
밖으로 가져오기 위한 메모리 관리 코드 묶음임을 알 수 있습니다. JavaScript의 메모리 관리? 신나는!
다음 포스트에서 WebAssembly와 AssemblyScript에 대해 더 자세히 알아볼 것입니다.
실행 경계 및 공유 메모리
여기에는 실행 경계와 공유 메모리에 대해 이야기할 다른 것이 있습니다.
WebAssembly 기사는 매우 전술적이므로 여기에서 런타임에 대해 이야기할 수 있습니다. JavaScript와 WebAssembly는 "에뮬레이트된" 런타임입니다. 구현된 대로 런타임 경계를 넘을 때마다 신체 데이터(x, y, z, 질량)의 복사본을 만들고 있습니다. 메모리를 복사하는 것은 저렴하지만 이것은 성숙한 고성능 디자인이 아닙니다.
운 좋게도 많은 똑똑한 사람들이 이러한 최첨단 브라우저 기술의 사양과 구현을 만들기 위해 노력하고 있습니다.
JavaScript에는 SharedArrayBuffer가 있어 호출 시 (2) -> (3)에서 postMessage()
사본을 제거하고 결과에서 (3) -> (2)에서 arrForces
의 onmessage()
사본을 제거합니다. .
WebAssembly에는 (3) -> (4)에서 nBodyForces()
호출을 위한 공유 메모리를 호스트할 수 있는 선형 메모리 디자인도 있습니다. 웹 작업자는 결과 배열에 대한 공유 메모리를 전달할 수도 있습니다.
다음 시간에 JavaScript 메모리 관리에 대한 흥미진진한 여정에 참여하십시오.