JavaScript Parallel Computing APIs

TypedArrays, Web Workers, WebCL und WebGL

Thomas Bergwinkl

Über mich

  • Aktives Mitglied in der Read Write Web Community Group
  • Invited Expert in der WebID W3C Incubator Group
  • Verschiedene Multimedia Projekte (z.B. Wavelet Video Codec, Raw-Entwickler)

Wieso JavaScript?

  • Plattformübergreifend
  • Ressourcensparend durch asynchrone APIs
  • Gleiche Codebasis für Client und Server
  • Reduzierter Wartungsaufwand
  • Einfache Anbindung an Web 2.0 und Web 3.0 Anwendungen

HTML5 APIs vs nativen Applikationen

  • File API
  • Web sockets
  • Web storage
  • WebRTC
  • Offline Web applications
  • Indexed Database API

Node.js Module

Node Packaged Modules, kurz npm, stellt ca. 29000 Module zur Verfügung

Performance aktueller JavaScript Engines

  • JIT Kompilierung
  • Inline Ersetzung
  • Inline Caching

Welche Möglichkeiten gibt es Binäre Daten effizient zu verwalten?

Im ECMA-262 Standard sind folgende Datentypen definiert:

Null, Boolean, String, Number, Object

Number

Number ist als Float definiert:

The Number type has exactly 18437736874454810627 (that is, 2^64-2^53+3) values, representing the double-precision 64-bit format IEEE 754 values as specified in the IEEE Standard for Binary Floating-Point Arithmetic...

Array

  • Array Elemente sind untypisiert
  • Verwaltungsoverhead je Element

String

  • Wird im Unicode Zeichensatz abgelegt
  • Kann als "ByteArray" verwendet werden (charCodeAt())

Jedoch

  • Ineffizient bei Werten fixer Länge
  • UTF-8: 1 Byte für 0-127, 2 Bytes für 128-255
  • UTF-16: 2 Bytes
  • Erschwert Optimierungen in der JavaScript Engine

Typed Arrays

ArrayBuffer API

Reservierter Speicher

[ Constructor(unsigned long length) ]
interface ArrayBuffer {
  readonly attribute unsigned long byteLength;
  ArrayBuffer slice(long begin, optional long end);
};

ArrayBufferView API

Zeiger auf einen Bereich des ArrayBuffer mit typisiertem Zugriff

interface ArrayBufferView {
  readonly attribute ArrayBuffer buffer;
  readonly attribute unsigned long byteOffset;
  readonly attribute unsigned long byteLength;
};

TypedArray API

Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array

[
  Constructor(unsigned long length),
  Constructor(TypedArray array),
  Constructor(type[] array),
  Constructor(ArrayBuffer buffer, optional unsigned long byteOffset,
      optional unsigned long length)
]
interface TypedArray {
    const unsigned long BYTES_PER_ELEMENT = element size in bytes;

    readonly attribute unsigned long length;

    getter type get(unsigned long index);
    setter void set(unsigned long index, type value);
    void set(TypedArray array, optional unsigned long offset);
    void set(type[] array, optional unsigned long offset);
    TypedArray subarray(long begin, optional long end);
};
TypedArray implements ArrayBufferView;

Beispiel

// 8-byte ArrayBuffer erstellen
var b = new ArrayBuffer(8);

// View mit Referenz auf b vom Typ Int32 erstellen
// über die gesammte Länge des ArrayBuffer
// beginnend bei Byte-Index 0 (Default Wert)
var v1 = new Int32Array(b);

// View mit Referenz auf b vom Typ Uint8 erstellen
// über die gesammte Länge des ArrayBuffer
// beginnend bei Byte Index 2
var v2 = new Uint8Array(b, 2);

// View mit Referenz auf b vom Typ Int16 erstellen
// mit der Länge 2 (Array Elemente)
// beginnend bei Byte Index 2
var v3 = new Int16Array(b, 2, 2);

und so sieht's im Speicher aus

varindex
b = 01234567
v1 = 01
v2 = 012345
v3 = 01

Web Worker

  • Threads für JavaScript
  • Interprozesskommunikation über Message Passing
  • Keine geteilten Objekte mit dem Haupt-Eventhandler
  • Langlebig da die Erstellung schwergewichtig ist

Event Handler

  • Nur das Ablegen einer neuen Nachricht muss synchronisiert werden
  • Aufgabe des Browser
  • Keine Synchronisierung im JavaScript Code notwendig

Worker API (extern)

interface Worker {
  attribute EventHandler onmessage;

  void terminate();
  void postMessage(any message, optional sequence<Transferable> transfer);
};

Worker API (intern)

interface MessagePort : EventTarget {
  void postMessage(any message, optional sequence<Transferable> transfer);
  void start();
  void close();

  attribute EventHandler onmessage;
};
interface WorkerGlobalScope {
  readonly attribute WorkerGlobalScope self;

  attribute EventHandler onmessage;
  attribute EventHandler onerror;
  attribute EventHandler onoffline;
  attribute EventHandler ononline;

  void close();
  void importScripts(DOMString... urls);
  void postMessage(any message, optional sequence<Transferable> transfer);
};

MessageEvent API

interface MessageEvent : Event {
  readonly attribute any data;
  readonly attribute DOMString origin;
  readonly attribute DOMString lastEventId;
  readonly attribute (WindowProxy or MessagePort)? source;
  readonly attribute MessagePort[]? ports;
};

Beispiel

<html>
 <body>
  <script>
   console.time("worker");
   var worker = new Worker("worker.js");

   worker.addEventListener("message", function(event) {
    console.log("worker response: ", event.data);
    console.timeEnd("worker");
   }, false);

   worker.postMessage(1);
  </script>
 </body>
</html>
self.addEventListener("message", function(event) {
 self.postMessage(event.data+event.data);
}, false);

Blob URLs

  • Zur Erstellung von URLs für String Objekte
  • Blob URLs können dem Worker als Script übergeben werden
var workerSource = ["self.addEventListener("message", function(event) { self.postMessage(event.data+event.data); }, false);"];
var workerBlob = new Blob(workerSource, { "type" : "application\/javascript" });
var workerUrl = URL.createObjectURL(workerBlob);

ImportScripts

ImportScripts-Funktion erlaubt es weitere Scripts nachzuladen

importScripts('script1.js', 'script2.js');

Transfer von ArrayBuffer Objekten

  • Zweiter optionaler Parameter der postMessage-Funktion
  • Enthaltene Objekte werden transferiert, nicht serialisiert
  • Parameter darf nur ArrayBuffer-Objekte enthalten
  • Stehen nach dem Transfer nur noch im Ziel-Kontext zur Verfügung

Shared Worker

  • Zugriff auf einen Worker über mehrere Fenster hinweg möglich
  • Same-Origin-Policy wird geprüft
  • Dient hauptsächlich der Kommunikation

WebCL

OpenCL API für JavaScript

Platform API

Host verbunden mit einem oder mehreren OpenCL Geräten

interface WebCL {
  sequence<WebCLPlatform> getPlatforms();
  WebCLContext? createContext(optional WebCLContextProperties properties);
  void waitForEvents(WebCLEvent[] eventWaitList, optional WebCLCallback whenFinished, optional any userdata);
};
interface WebCLPlatform {
  sequence<WebCLDevice> getDevices(CLenum deviceType);
};

Context API

Verbindet Geräte, Programme, Kernels, Buffers und Command Queues

Buffer API

Zur Übertragung von Daten, lesend und schreibend, an einen Context

interface WebCLContext {
  WebCLBuffer createBuffer(CLenum memFlags, CLuint sizeInBytes, optional ArrayBuffer hostPtr);
  WebCLCommandQueue createCommandQueue(optional WebCLDevice? device, optional CLenum properties);
  WebCLProgram createProgram(DOMString source);
};

Kernel API

Mit __kernel markierte Funktion als Programmeinstiegspunkt

interface WebCLProgram {
  any getBuildInfo(WebCLDevice device, CLenum name);
  void build(WebCLDevice[] devices, optional DOMString? options, optional WebCLCallback whenFinished, optional any userdata);
  WebCLKernel createKernel(DOMString kernelName);
};
interface WebCLKernel {
  void setArg(CLuint index, any value, optional CLtype type);
};

Command Queue API

Zur Verwaltung von Rechenoperationen

interface WebCLCommandQueue {
  void enqueueReadBuffer(WebCLBuffer buffer, CLboolean blockingRead, CLuint bufferOffset, CLuint numBytes, ArrayBuffer hostPtr, optional WebCLEvent[]? eventWaitList, optional WebCLEvent? event);
  void enqueueWriteBuffer(WebCLBuffer buffer, CLboolean blockingWrite, CLuint bufferOffset, CLuint numBytes, ArrayBuffer hostPtr, optional WebCLEvent[]? eventWaitList, optional WebCLEvent? event);
  void enqueueNDRangeKernel(
                    WebCLKernel kernel,
                    CLuint[3]? globalWorkOffset, CLuint[3]? globalWorkSize, CLuint[3]? localWorkSize,
                    optional WebCLEvent[]? eventWaitList,
                    optional WebCLEvent? event);

  void finish();
};

Beispiel

var cl = require("node-webcl");
var platforms = cl.getPlatforms();
var context = cl.createContext({deviceType:cl.DEVICE_TYPE_DEFAULT, platform: platforms[0]});
var devices = context.getInfo(cl.CONTEXT_DEVICES);
var source =
  "__kernel void add(__global const ushort* input, __global ushort* output, uint value) {" +
  "  int index = get_global_id(0);" +
  "  if (index < 0 || index > 3) return;" +
  "  output[index] = input[index] + value; }";
var program = context.createProgram(source);
program.build([devices[0]]);
var inArray = new Uint16Array([1,2,3,4]);
var outArray = new Uint16Array(4);
var inBuffer = context.createBuffer(cl.MEM_READ_ONLY, inArray.byteLength);
var outBuffer = context.createBuffer(cl.MEM_WRITE_ONLY, outArray.byteLength);
var kernel = program.createKernel("add");
kernel.setArg(0, inBuffer);
kernel.setArg(1, outBuffer);
kernel.setArg(2, 10, cl.type.UINT);
var commandQueue = context.createCommandQueue(devices[0]);    
commandQueue.enqueueWriteBuffer(inBuffer, false, 0, inArray.byteLength, inArray.buffer);
commandQueue.enqueueNDRangeKernel(kernel, null, [4], [2]);
commandQueue.enqueueReadBuffer(outBuffer, false, 0, outArray.byteLength, outArray.buffer);
commandQueue.finish();
console.log(outArray);

WebGL

OpenGL API für JavaScript

API

interface WebGLRenderingContext {
  void activeTexture(GLenum texture);
  void attachShader(WebGLProgram? program, WebGLShader? shader);
  void bindAttribLocation(WebGLProgram? program, GLuint index, DOMString name);
  void bindBuffer(GLenum target, WebGLBuffer? buffer);
  void bindTexture(GLenum target, WebGLTexture? texture);
  void bufferData(GLenum target, ArrayBufferView data, GLenum usage);
  void clear(GLbitfield mask);
  void compileShader(WebGLShader? shader);
  WebGLBuffer? createBuffer();
  WebGLProgram? createProgram();
  WebGLShader? createShader(GLenum type);
  WebGLTexture? createTexture();
  void drawArrays(GLenum mode, GLint first, GLsizei count);
  [WebGLHandlesContextLoss] GLint getAttribLocation(WebGLProgram? program, DOMString name);
  void texImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, ArrayBufferView? pixels);
  void uniform1f(WebGLUniformLocation? location, GLfloat x);
  void uniform1fv(WebGLUniformLocation? location, Float32Array v);
  void uniform4i(WebGLUniformLocation? location, GLint x, GLint y, GLint z, GLint w);
  void uniform4iv(WebGLUniformLocation? location, Int32Array v);
  void uniformMatrix2fv(WebGLUniformLocation? location, GLboolean transpose, Float32Array value);
  void useProgram(WebGLProgram? program);
  void viewport(GLint x, GLint y, GLsizei width, GLsizei height);
};

Beispiel - Teil 1 (Shaders initialisieren)

<html>
 <body>
  <canvas id="canvas" width="500" height="500"></canvas>
  <script>
   var canvas = window.document.getElementById("canvas");
   var gl = canvas.getContext("experimental-webgl");
   var program = gl.createProgram();
   var vertexShaderSource =
     "attribute vec4 position;" +
     "void main() { gl_Position = position; }";
   var vertexShader = gl.createShader(gl.VERTEX_SHADER);
   gl.shaderSource(vertexShader, vertexShaderSource);
   gl.compileShader(vertexShader);
   gl.attachShader(program, vertexShader);
   var fragmentShaderSource =
     "void main() { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); }";
   var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
   gl.shaderSource(fragmentShader, fragmentShaderSource);
   gl.compileShader(fragmentShader);
   gl.attachShader(program, fragmentShader);
   gl.linkProgram(program);
   gl.useProgram(program);

Beispiel - Teil 2 (Dreieck zeichnen)

   gl.clearColor(0.0, 0.0, 0.0, 1.0);
   gl.clear(gl.COLOR_BUFFER_BIT);
   var positionsAttribute = gl.getAttribLocation(program, "position");
   var positions = new Float32Array([0.0, 0.1, 0.0, -0.1, -0.1, 0.0, 0.1, -0.1, 0.0]);
   var positionsBuffer = gl.createBuffer();
   gl.bindBuffer(gl.ARRAY_BUFFER, positionsBuffer);
   gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
   gl.vertexAttribPointer(positionsAttribute, 3, gl.FLOAT, false, 0, 0);
   gl.enableVertexAttribArray(positionsAttribute);
   gl.drawArrays(gl.TRIANGLES, 0, 3);
  </script>
 </body>
</html>

RawDevJS

  • Raw-Entwickler basierend auf dem C++ Qt Projekt
  • Portierung des Tiff-Parsers, Lossless JPEG Decoder mit Canvas-Viewer in 8h
  • WebGL-Viewer mit Demosaicing in weiteren 8h

Was ist zu machen?

Tiff Parser und Lossess JPEG Decoder

Demosaicing

Farbtransformation

Transformation

Architektur

Filter Chain (Canvas)

Filter Chain (WebGL)

Implementierung

  • FilterChain Klasse ruft die asynchrone filter-Funktion der übergebenen Filter Objekte auf
  • filter-Funktion wird im Konstruktor dynamisch, je nach vorhandener APIs, zugewiesen
  • WebGL besteht nur aus einem Filter um die Bilddaten nicht aus dem Grafikspeicher exportieren zu müssen

RequireJS

RequireJS wurde als Module Loader gewählt weil:

  • Browser sowie Node.js unterstützt werden
  • mit r.js ein Build System vorhanden ist
  • auch ohne Build Module schnell geladen werden (asynchron)

require-worker

Light Version von RequireJS speziell für Web Worker

  • Kleinere Worker Files nach dem Build
  • Synchrones Laden bei der Entwicklung

Debugging

  • Chrome unterstüzt "Breakpoint on start" für Worker
  • WebGL Objekte können mit dem Chrome WebGL Inspector analysiert werden

Web Worker im Browser

Firefox kann ArrayBufferViews nicht serialisieren

  • Firefox kann nur Array Buffer über MessageChannels serialisieren
  • ArrayBufferViews und TypedArrays werden manuell serialisiert (inklusive Typ, Offset und Länge)

WebCL im Browser

API des Nokia Plugins nicht aktuell

  • API des Nokia WebCL-Plugins für Firefox basiert auf älterem Stand der Spec
  • Wrapper notwendig um aktuelle API verwenden zu können

WebGL im Browser

Keine Untersütztung für 16 Bit Farbkanäle

Uint16Array wird in zwei Uint8Arrays (low/high) verteilt und im Shader-Code wieder zusammengefügt

Auch 3D Texturen sind im Standard nicht vorgesehen

3D Textur wird "aufgeklappt" und in 2D abgebildet

Web Worker unter Node.js mit webworker Modul

Startet für jeden Worker einen neuen Node Prozess und kommuniziert über Unix Sockets mit Web Socket API

  • Großer Overhead durch separaten Prozess
  • Keine Typed Array Serialisierung
  • Kein Transfer von ArrayBuffern möglich
  • Fork mit folgenden Features auf Github:
    • Typed Array (ohne Serialisierung)
    • Buffer (Node.js Datentyp für größere Binärdaten)
    • BSON Serialisierung via buffalo Modul

Web Worker unter Node.js mit webworker-threads Modul

Web Worker Modul implementiert als C++ Node.js Erweiterung

  • Worker laufen als Thread
  • Typed Array Unterstützung fehlt (Serialisierung, Transfer sowie Datentyp)

WebCL unter Node.js mit node-webcl und webcl-nodep

  • createContext Funktions benötigt Platform und DeviceType
  • Buffer erlaubt keine Offsets != 0
  • Module müssen unterschiedlich angesprochen werden

Ausblick - Verteilte Filterketten

Ausblick -Semantic Web Einbindung

Ausblick

ParallelArray

Links

http://rawdev.bergnet.org

Spezifikationen

Sonstiges