Keras.js によるランドマーク検出の Web アプリケーション化
前回の 「CNN でランドマーク検出」 の学習済みモデルを Keras.js を使って Web ブラウザ上で実行できるようにしてみます。
ソースは http://github.com/fits/try_samples/tree/master/blog/20190331/
準備
npm で Keras.js をインストールします。
Keras.js インストール
> npm install --save keras-js
Keras.js に含まれている encoder.py
スクリプトを使って、Python の Keras で学習したモデル(model/cnn_landmark_400.h5
)を Keras.js 用に変換します。
モデルファイル(HDF5 形式)を Keras.js 用に変換
> python node_modules/keras-js/python/encoder.py model/cnn_landmark_400.h5
生成された .bin ファイル(model/cnn_landmark_400.bin
)のパス(URL)を KerasJS.Model
へ指定して使う事になります。
ついでに、webpack もインストールしておきます。(webpack コマンドを使うには webpack-cli も必要)
webpack インストール
> npm install --save-dev webpack webpack-cli
Web アプリケーション作成
今回、作成する Web アプリケーションのファイル構成は以下の通りです。
- index.html
- js/bundle_app.js
- js/bundle_worker.js
- model/cnn_landmark_400.bin
処理は全て Web ブラウザ上で実行するようにし、Keras.js の処理(今回のランドマーク検出)はそれなりに重いので Web Worker として実行します。
index.html の内容は以下の通りです。
index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style type="text/css"> canvas { border: 1px solid; } canvas.dragging { border: 3px solid red; } table { text-align: center; border-collapse: collapse; } table th, table td { border: 1px solid; padding: 8px; } </style> </head> <body> <dialog id="load-dialog">loading model ...</dialog> <dialog id="detect-dialog">detecting landmarks ...</dialog> <dialog id="error-dialog">ERROR</dialog> <div> <canvas width="256" height="256"></canvas> </div> <br> <div> <div id="landmarks"></div> </div> <script src="./js/bundle_app.js"></script> </body> </html>
bundle_xxx.js を生成するため、以下のような webpack 設定ファイルを用意します。
fs: 'empty'
の箇所は Keras.js を webpack で処理するために必要な設定で、これが無いと Module not found: Error: Can't resolve 'fs'
のようなエラーが出る事になります。
webpack.config.js
module.exports = { entry: { bundle_app: __dirname + '/src/app.js', bundle_worker: __dirname + '/src/worker.js' }, output: { path: __dirname + '/js', filename: '[name].js', }, // Keras.js 用の設定 node: { fs: 'empty' } }
Web Worker では Actor モデルのようにメッセージパッシング(postMessage
で送信、onmessage
で受信)を使ってメインの UI 処理とのデータ連携を行います。
今回は、以下のようなメッセージを Web Worker(Keras.js を使った処理)とやり取りするようにします。
Web Worker(Keras.js の処理)とのメッセージ内容
処理 | 送信メッセージ | 受信メッセージ(成功時) | 受信メッセージ(エラー時) |
---|---|---|---|
初期化 | {type: 'init', url: <モデルファイルのURL>} |
{type: 'init'} |
{type: 'init', error: <エラーメッセージ>} |
ランドマーク検出 | {type: 'predict', input: <入力データ>} |
{type: 'predict', output: <ランドマーク検出結果>} |
{type: 'predict', error: <エラーメッセージ>} |
(a) ランドマーク検出処理(src/worker.js)
Web Worker として実装するため、postMessage
で UI 処理へメッセージを送信し、onmessage
でメッセージを受信するようにします。
Keras.js 1.0.3 における Dense 処理の問題
実は、Keras.js 1.0.3 では今回の CNN モデルを正しく処理できません。
というのも、Keras.js 1.0.3 における Dense の処理(GPU を使わない場合)は以下のようになっています。
node_modules/keras-js/lib/layers/core/Dense.js の問題個所
_callCPU(x) { this.output = new _Tensor.default([], [this.units]); ・・・ }
今回の CNN モデルでは Dense の結果が 3次元 (256, 256, 7)
になる必要がありますが、上記 Dense 処理では (7)
のように 1次元になってしまい正しい結果を得られません。 ※
※ ついでに、Keras.js の softmax 処理にも不都合な点があった
そこで、今回は(GPU を使わない事を前提に)Dense
の _callCPU
を実行時に書き換える事で回避しました。
処理内容としては、元の処理を 2重ループ内で実施するようにしています。
Dense 問題の回避措置(src/worker.js)
import KerasJS from 'keras-js' import { gemv } from 'ndarray-blas-level2' import ops from 'ndarray-ops' ・・・ // Dense の _callCPU を実行時に変更 KerasJS.layers.Dense.prototype._callCPU = function(x) { const h = x.tensor.shape[0] const w = x.tensor.shape[1] this.output = new KerasJS.Tensor([], [h, w, this.units]) for (let i = 0; i < h; i++) { for (let j = 0; j < w; j++) { const xt = x.tensor.pick(i, j) const ot = this.output.tensor.pick(i, j) if (this.use_bias) { ops.assign(ot, this.weights['bias'].tensor) } gemv(1, this.weights['kernel'].tensor.transpose(1, 0), xt, 1, ot) this.activationFunc({tensor: ot}) } } }
ランドマーク検出の実装
KerasJS.Model
の predict
へ入力データを渡したり結果を取り出すにはレイヤー名を指定する必要があり、これらのレイヤー名は iuputLayerNames
と outputLayerNames
でそれぞれ取得できます。
predict の結果は、各座標のランドマーク該当確率 (256, 256, 7)
となるので、ここではランドマーク毎 ※ に最も確率の高かった座標のみを結果として返すようにしています。
※ ランドマーク 0 はランドマークに該当しなかった場合なので結果に含めていない
src/worker.js
import KerasJS from 'keras-js' import { gemv } from 'ndarray-blas-level2' import ops from 'ndarray-ops' let model = null // モデルデータの読み込み const loadModel = file => { const model = new KerasJS.Model({ filepath: file }) return model.ready().then(r => model) } // Keras.js の Dense 問題への対応 KerasJS.layers.Dense.prototype._callCPU = function(x) { ・・・ } // predict の結果を処理(ランドマーク毎に最も確率の高い座標を抽出) const detectLandmarks = ts => { const res = {} for (let h = 0; h < ts.tensor.shape[0]; h++) { for (let w = 0; w < ts.tensor.shape[1]; w++) { const t = ts.tensor.pick(h, w) const wrkProb = {landmark: 0, prob: 0, x: w, y: h} for (let c = 0; c < t.shape[0]; c++) { const prob = t.get(c) if (prob > wrkProb.prob) { wrkProb.landmark = c wrkProb.prob = prob } } // ランドマーク 0 (ランドマークでは無い)は除外 if (wrkProb.landmark > 0) { const curProb = res[wrkProb.landmark] if (!curProb || curProb.prob < wrkProb.prob) { res[wrkProb.landmark] = wrkProb } } } } return res } // UI 処理からのメッセージ受信 onmessage = ev => { switch (ev.data.type) { case 'init': loadModel(ev.data.url) .then(m => { model = m postMessage({type: ev.data.type}) }) .catch(err => { console.log(err) postMessage({type: ev.data.type, error: err.message}) }) break case 'predict': const outputLayerName = model.outputLayerNames[0] const shape = model.modelLayersMap.get(outputLayerName) .output.tensor.shape const data = {} // 入力データの設定 data[model.inputLayerNames[0]] = ev.data.input Promise.resolve(model.predict(data)) .then(r => new KerasJS.Tensor(r[outputLayerName], shape)) // predict 実行結果の取り出し .then(detectLandmarks) .then(r => // UI 処理へ結果送信 postMessage({type: ev.data.type, output: r}) ) .catch(err => { console.log(err) postMessage({type: ev.data.type, error: err.message}) }) break } }
(b) UI 処理(src/app.js)
画像データの変換(入力データの作成)
KerasJS.Model で predict するために、今回のケースでは画像データを 256(高さ)× 256(幅)× 3(RGB)
サイズの 1次元配列 Float32Array
へ変換する必要があります。
今回は以下のように canvas を利用して変換を行いました。
- (1) canvas へドラッグアンドドロップした画像を canvas へ描画
- (2)
getImageData
で canvas からImageData
を取得 - (3)
ImageData.data
の内容を RGB 並びの Float32Array へ変換
ImageData.data は RGBA 並びの 1次元配列 Uint8ClampedArray
となっているので、RGB 部分のみを取り出して(A の内容を除外する)Float32Array
を生成しています。
ちなみに、今回の CNN モデル自体は画像サイズに依存しない(Fully Convolutional Networks 的な)構成になっています。
そのため、任意サイズの画像を処理する事もできるのですが、現時点の Keras.js ではそんな事を考慮してくれていないので、実現するにはそれなりの工夫が必要になります。(一応、実現は可能でした)
ここでは、単純に canvas へ描画した 256x256 範囲の内容だけ(つまりは固定サイズ)を使うようにしています。※
※ この方法では 256x256 以外のサイズで欠けや余白の入り込みが発生する
画像データ変換部分(src/app.js)
・・・ const imageTypes = ['image/jpeg'] const canvas = document.getElementsByTagName('canvas')[0] const ctx = canvas.getContext('2d') ・・・ // RGBA 並びの Uint8ClampedArray を RGB 並びの Float32Array へ変換 const imgToArray = imgData => new Float32Array( imgData.data.reduce( (acc, v, i) => { // RGBA の A 部分を除外 if (i % 4 != 3) { acc.push(v) } return acc }, [] ) ) // 画像の読み込み const loadImage = url => new Promise(resolve => { const img = new Image() img.addEventListener('load', () => { ctx.clearRect(0, 0, canvas.width, canvas.height) // 画像サイズが canvas よりも小さい場合の考慮 const w = Math.min(img.width, canvas.width) const h = Math.min(img.height, canvas.height) // canvas へ画像を描画 ctx.drawImage(img, 0, 0, w, h, 0, 0, w, h) // ImageData の取得 const d = ctx.getImageData(0, 0, canvas.width, canvas.height) resolve(imgToArray(d)) }) img.src = url }) ・・・ // モデルデータ読み込み完了時の処理 const ready = () => { canvas.addEventListener('dragover', ev => { ev.preventDefault() canvas.classList.add('dragging') }, false) canvas.addEventListener('dragleave', ev => { canvas.classList.remove('dragging') }, false) // ドロップ時の処理 canvas.addEventListener('drop', ev => { ev.preventDefault() canvas.classList.remove('dragging') const file = ev.dataTransfer.files[0] if (imageTypes.includes(file.type)) { ・・・ const reader = new FileReader() reader.onload = ev => { loadImage(reader.result) .then(img => { ・・・ }) } reader.readAsDataURL(file) } }, false) } ・・・
Web Worker との連携
Web Worker とメッセージをやり取りし、ランドマークの検出結果を描画する部分の実装です。
Web Worker との連携部分(src/app.js)
const colors = ['rgb(255, 255, 255)', 'rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)', 'rgb(255, 255, 0)', 'rgb(0, 255, 255)', 'rgb(255, 0, 255)'] const radius = 5 const imageTypes = ['image/jpeg'] const modelFile = '../model/cnn_landmark_400.bin' // Web Worker の作成 const worker = new Worker('./js/bundle_worker.js') ・・・ // 検出したランドマークを canvas へ描画 const drawLandmarks = lms => { Object.values(lms).forEach(v => { ctx.fillStyle = colors[v.landmark] ctx.beginPath() ctx.arc(v.x, v.y, radius, 0, Math.PI * 2, false) ctx.fill() }) } ・・・ // 検出したランドマークの内容を table(HTML)化して表示 const showLandmarksInfo = lms => { ・・・ infoNode.innerHTML = ` <table> <tr> <th>landmark</th> <th>coordinate</th> <th>prob</th> </tr> ${rowsHtml} </table> ` } // モデルデータ読み込み完了後 const ready = () => { ・・・ canvas.addEventListener('drop', ev => { ・・・ if (imageTypes.includes(file.type)) { ・・・ reader.onload = ev => { loadImage(reader.result) .then(img => { detectDialog.showModal() // Web Worker へのランドマーク検出指示 worker.postMessage({type: 'predict', input: img}) }) } reader.readAsDataURL(file) } }, false) } // Web Worker からのメッセージ受信 worker.onmessage = ev => { if (ev.data.error) { ・・・ } else { switch (ev.data.type) { case 'init': ready() loadDialog.close() break case 'predict': const res = ev.data.output console.log(res) detectDialog.close() drawLandmarks(res) showLandmarksInfo(res) break } } } loadDialog.showModal() // Web Worker へのモデルデータ読み込み指示 worker.postMessage({type: 'init', url: modelFile})
(c) ビルド
webpack コマンドを実行し、js/bundle_app.js と js/bundle_worker.js を生成します。
webpack によるビルド
> webpack
(d) 動作確認
HTTP サーバーを使って動作確認を行います。 今回は http-server を使って実行しました。
http-server 実行
> http-server Starting up http-server, serving ./ Available on: ・・・ http://127.0.0.1:8080 Hit CTRL-C to stop the server
http://localhost:8080/index.html
へ Chrome ※ でアクセスして画像ファイルをドラッグアンドドロップすると以下のような結果となりました。
※ HTMLDialogElement.showModal() を使っている関係で 現時点では Chrome でしか動作しませんが、 dialog 以外の部分(Keras.js の処理等)は Firefox でも動作するようになっています
TypeScript で funfix を使用 - tsc, FuseBox
funfix は JavaScript, TypeScript, Flow の関数型プログラミング用ライブラリで、Fantasy Land や Static Land ※ に準拠し Scala の Option, Either, Try, Future 等と同等の型が用意されているようです。
※ JavaScript 用に Monoid や Monad 等の代数的構造に関する仕様を定義したもの
今回は Option を使った単純な処理を TypeScript で実装し Node.js で実行してみます。
ソースは http://github.com/fits/try_samples/tree/master/blog/20180730/
はじめに
Option
を使った下記サンプルをコンパイルして実行します。
サンプルソース
import { Option, Some } from 'funfix' const f = (ma, mb) => ma.flatMap(a => mb.map(b => `${a} + ${b} = ${a + b}`)) const d1 = Some(10) const d2 = Some(2) console.log( d1 ) console.log('-----') console.log( f(d1, d2) ) console.log( f(d1, Option.none()) ) console.log('-----') console.log( f(d1, d2).getOrElse('none') ) console.log( f(d1, Option.none()).getOrElse('none') )
ビルドと実行
上記ソースファイルを以下の 2通りでビルドして実行してみます。
- (a) tsc 利用
- (b) FuseBox 利用
(a) tsc を利用する場合
tsc コマンドを使って TypeScript のソースをコンパイルします。
まずは typescript と funfix モジュールをそれぞれインストールします。
typescript インストール
> npm install --save-dev typescript
funfix インストール
> npm install --save funfix
この状態で sample.ts
ファイルをコンパイルしてみると、型関係のエラーが出るものの sample.js
は正常に作られました。
コンパイル1
> tsc sample.ts node_modules/funfix-core/dist/disjunctions.d.ts:775:14 - error TS2416: Property 'value' in type 'TNone' is not assignable to the same property in base type 'Option<never>'. Type 'undefined' is not assignable to type 'never'. 775 readonly value: undefined; ~~~~~ node_modules/funfix-effect/dist/eval.d.ts:256:42 - error TS2304: Cannot find name 'Iterable'. 256 static sequence<A>(list: Eval<A>[] | Iterable<Eval<A>>): Eval<A[]>; ~~~~~~~~ ・・・
sample.js を実行してみると特に問題無く動作します。
実行1
> node sample.js TSome { _isEmpty: false, value: 10 } ----- TSome { _isEmpty: false, value: '10 + 2 = 12' } TNone { _isEmpty: true, value: undefined } ----- 10 + 2 = 12 none
これで一応は動いたわけですが、コンパイル時にエラーが出るというのも望ましい状態ではないので、エラー部分を解決してみます。
他にも方法があるかもしれませんが、ここでは以下のように対応します。
- (1)
Property 'value' in type 'TNone' ・・・ 'Option<never>'
のエラーに対して tsc 実行時に--strictNullChecks
オプションを指定して対応 - (2)
Cannot find name 'Iterable'
等のエラーに対して@types/node
をインストールして対応
strictNullChecks は tsc の実行時オプションで指定する以外にも設定ファイル tsconfig.json
で設定する事もできるので、ここでは tsconfig.json ファイルを使います。
(1) tsconfig.json
{ "compilerOptions": { "strictNullChecks": true } }
次に @types/node をインストールします。
@types/node には Node.js で実行するための型定義(Node.js 依存の API 等)が TypeScript 用に定義されています。
(2) @types/node インストール
> npm install --save-dev @types/node
この状態で tsc を実行すると先程のようなエラーは出なくなりました。(tsconfig.json を適用するため tsc コマンドへ引数を指定していない点に注意)
コンパイル2
> tsc
実行結果にも差異はありません。
実行2
> node sample.js TSome { _isEmpty: false, value: 10 } ----- TSome { _isEmpty: false, value: '10 + 2 = 12' } TNone { _isEmpty: true, value: undefined } ----- 10 + 2 = 12 none
最終的な package.json の内容は以下の通りです。
package.json
{ "name": "sample", "version": "1.0.0", "devDependencies": { "@types/node": "^10.5.4", "typescript": "^2.9.2" }, "dependencies": { "funfix": "^7.0.1" } }
(b) FuseBox を利用する場合
次に、モジュールバンドラーの FuseBox を使用してみます。(以降は (a) とは異なるディレクトリで実施)
なお、ここでは npm の代わりに yarn
を使っていますが、npm でも特に問題はありません。
yarn のインストール例(npm 使用)
> npm install -g yarn
(b-1) 型チェック無し
まずは typescript, fuse-box, funfix をそれぞれインストールしておきます。
typescript と fuse-box インストール
> yarn add --dev typescript fuse-box
funfix インストール
> yarn add funfix
FuseBox ではビルド定義を JavaScript のコードで記載します。 とりあえずは必要最小限の設定を行いました。
bundle で指定した名称が init の $name
に適用されるため、*.ts
のコンパイル結果と依存モジュールの内容をバンドルして bundle.js
へ出力する事になります。
なお、>
でロード時に実行する(コードを記載した)ファイルを指定します。
fuse.js (FuseBox ビルド定義)
const {FuseBox} = require('fuse-box') const fuse = FuseBox.init({ output: '$name.js' }) fuse.bundle('bundle').instructions('> *.ts') fuse.run()
上記スクリプトを実行してビルド(TypeScript のコンパイルとバンドル)を行います。
ビルド
> node fuse.js --- FuseBox 3.4.0 --- → Generating recommended tsconfig.json: ・・・\sample_fusebox1\tsconfig.json → Typescript script target: ES7 -------------------------- Bundle "bundle" sample.js └── (1 files, 700 Bytes) default └── funfix-core 34.4 kB (1 files) └── funfix-effect 43.1 kB (1 files) └── funfix-exec 79.5 kB (1 files) └── funfix 1 kB (1 files) size: 158.7 kB in 765ms
初回実行時にデフォルト設定の tsconfig.json が作られました。(tsconfig.json が存在しない場合)
tsc の時のような型関係のエラーは出ていませんが、これは FuseBox がデフォルトで TypeScript の型チェックをしていない事が原因のようです。
型チェックを実施するには fuse-box-typechecker
プラグインを使う必要がありそうです。
実行
> node bundle.js TSome { _isEmpty: false, value: 10 } ----- TSome { _isEmpty: false, value: '10 + 2 = 12' } TNone { _isEmpty: true, value: undefined } ----- 10 + 2 = 12 none
package.json の内容は以下の通りです。
package.json
{ "name": "sample_fusebox1", "version": "1.0.0", "main": "bundle.js", "license": "MIT", "devDependencies": { "fuse-box": "^3.4.0", "typescript": "^2.9.2" }, "dependencies": { "funfix": "^7.0.1" } }
(b-2) 型チェック有り
TypeScript の型チェックを行うようにしてみます。
まずは、(b-1) と同じ構成に fuse-box-typechecker
プラグインを加えます。
fuse-box-typechecker を追加インストール
> yarn add --dev fuse-box-typechecker
次に、fuse.js へ fuse-box-typechecker プラグインの設定を追加します。
TypeChecker で型チェックにエラーがあった場合、例外が throw されるようにはなっていないため、ここではエラーがあった場合に Error を throw して fuse.run() を実行しないようにしてみました。
ただし、こうすると tsconfig.json を予め用意しておく必要があります。(TypeChecker に tsconfig.json が必要)
fuse.js (FuseBox ビルド定義)
const {FuseBox} = require('fuse-box') const {TypeChecker} = require('fuse-box-typechecker') // fuse-box-typechecker の設定 const testSync = TypeChecker({ tsConfig: './tsconfig.json' }) const fuse = FuseBox.init({ output: '$name.js' }) fuse.bundle('bundle').instructions('> *.ts') testSync.runPromise() .then(n => { if (n != 0) { // 型チェックでエラーがあった場合 throw new Error(n) } // 型チェックでエラーがなかった場合 return fuse.run() }) .catch(console.error)
これで、ビルド時に (a) と同様の型エラーが出るようになりました。
ビルド1
> node fuse.js ・・・ --- FuseBox 3.4.0 --- Typechecker plugin(promisesync) . Time:Sun Jul 29 2018 12:40:47 GMT+0900 (GMT+09:00) File errors: └── .\node_modules\funfix-core\dist\disjunctions.d.ts | ・・・\sample_fusebox2\node_modules\funfix-core\dist\disjunctions.d.ts (775,14) (Error:TS2416) Property 'value' in type 'TNone' is not assignable to the same property in base type 'Option<never>'. Type 'undefined' is not assignable to type 'never'. Errors:1 └── Options: 0 └── Global: 0 └── Syntactic: 0 └── Semantic: 1 └── TsLint: 0 Typechecking time: 4116ms Quitting typechecker ・・・
ここで、Iterable
の型エラーが出ていないのは fuse-box-typechecker のインストール時に @types/node もインストールされているためです。
(a) と同様に strictNullChecks の設定を tsconfig.json へ追記して、このエラーを解決します。
tsconfig.json へ strictNullChecks の設定を追加
{ "compilerOptions": { "module": "commonjs", "target": "ES7", ・・・ "strictNullChecks": true } }
これでビルドが成功するようになりました。
ビルド2
> node fuse.js ・・・ Typechecker name: undefined Typechecker basepath: ・・・\sample_fusebox2 Typechecker tsconfig: ・・・\sample_fusebox2\tsconfig.json --- FuseBox 3.4.0 --- Typechecker plugin(promisesync) . Time:Sun Jul 29 2018 12:44:57 GMT+0900 (GMT+09:00) All good, no errors :-) Typechecking time: 4103ms Quitting typechecker killing worker → Typescript config file: \tsconfig.json → Typescript script target: ES7 -------------------------- Bundle "bundle" sample.js └── (1 files, 700 Bytes) default └── funfix-core 34.4 kB (1 files) └── funfix-effect 43.1 kB (1 files) └── funfix-exec 79.5 kB (1 files) └── funfix 1 kB (1 files) size: 158.7 kB in 664ms
実行結果
> node bundle.js TSome { _isEmpty: false, value: 10 } ----- TSome { _isEmpty: false, value: '10 + 2 = 12' } TNone { _isEmpty: true, value: undefined } ----- 10 + 2 = 12 none
package.json の内容は以下の通りです。
package.json
{ "name": "sample_fusebox2", "version": "1.0.0", "main": "index.js", "license": "MIT", "devDependencies": { "fuse-box": "^3.4.0", "fuse-box-typechecker": "^2.10.0", "typescript": "^2.9.2" }, "dependencies": { "funfix": "^7.0.1" } }
Kubernetes の Watch API とタイムアウト
Kubernetes の Watch API を下記クライアントライブラリを使って試してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20180409/
はじめに
下記のコマンドを実行して Javascript Kubernetes Client をインストールしておきます。
Javascript Kubernetes Client のインストール
> npm install @kubernetes/client-node
Watch API による Pod の監視
Watch API で default
Namespace の Pod に関するイベントを監視して、イベントのタイプと Pod 名を標準出力する処理を実装してみます。
watch
の第一引数に Watch API のエンドポイント URL、第三引数でイベントハンドラを指定します。(第二引数はクエリパラメータ)
今回は Pod を監視していますが、default
Namespace の Deployment を監視する場合は endpoint
を /apis/apps/v1/namespaces/default/deployments
とします。
なお、$HOME/.kube/config
もしくは %USERPROFILE%\.kube\config
ファイルから Kubernetes への接続情報を取得するようにしています。
sample_watch_pod.js
const k8s = require('@kubernetes/client-node') // default Namespace の Pod const endpoint = '/api/v1/namespaces/default/pods' // Windows 環境用の設定 if (!process.env.HOME) { process.env.HOME = process.env.USERPROFILE } const conf = new k8s.KubeConfig() conf.loadFromFile(`${process.env.HOME}/.kube/config`) const w = new k8s.Watch(conf) w.watch( endpoint, {}, (type, obj) => { console.log(`${type} : ${obj.metadata.name}`) }, err => { if (err) { console.error(err) } else { console.log('done') } } )
動作確認
今回、Kubernetes の環境を minikube で用意します。
minikube コマンドを使って start を実行するとローカル用の Kubernetes 環境が立ち上がります。
その際に、%USERPROFILE%\.kube\config
ファイル等が作られます。
minikube 開始
> minikube start ・・・ Starting local Kubernetes v1.9.0 cluster... Starting VM... Getting VM IP address... Moving files into cluster... Setting up certs... Connecting to cluster... Setting up kubeconfig... Starting cluster components... Kubectl is now configured to use the cluster. Loading cached images from config file.
sample_watch_pod.js の実行
> node sample_watch_pod.js
下記 YAML ファイルを使って、Kubernetes 環境へ nginx 実行用の Deployment と Service を作成してみます。
nginx.yaml (nginx 用の Deployment と Service 定義)
apiVersion: v1 kind: Service metadata: name: nginx-service labels: app: nginx spec: ports: - name: http port: 80 nodePort: 30001 selector: app: nginx type: NodePort --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy spec: replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx ports: - containerPort: 80
kubectl を使って Deployment と Service を作成します。
Deployment と Service 作成
> kubectl create -f nginx.yaml service "nginx-service" created deployment "nginx-deploy" created
Watch の結果は以下のようになりました。
sample_watch_pod.js の結果1
> node sample_watch_pod.js ADDED : nginx-deploy-679dc9c764-r9ds5 MODIFIED : nginx-deploy-679dc9c764-r9ds5 ADDED : nginx-deploy-679dc9c764-54d5d MODIFIED : nginx-deploy-679dc9c764-r9ds5 MODIFIED : nginx-deploy-679dc9c764-54d5d MODIFIED : nginx-deploy-679dc9c764-54d5d MODIFIED : nginx-deploy-679dc9c764-r9ds5 MODIFIED : nginx-deploy-679dc9c764-54d5d
ここで、いつまでも接続が続くわけでは無く、minikube の環境では 40分程度(ただし、毎回異なる)で接続が切れ以下のようになりました。
sample_watch_pod.js の結果2 (一定時間経過後)
> node sample_watch_pod.js ・・・ done
タイムアウト時間の確認
Kubernetes と minikube のソースから、タイムアウトに関係していると思われる箇所 timeout = time.Duration(float64(minRequestTimeout) * (rand.Float64() + 1.0))
を見つけました。※
※ minikube では localkube 内で Kubernetes の API Server を実行しているようです
これだと、タイムアウトは 30 ~ 60分でランダムに決まる事になりそうなので、接続の切れる時間が毎回異なるという現象に合致します。
ソース kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/get.go
func ListResource(r rest.Lister, rw rest.Watcher, scope RequestScope, forceWatch bool, minRequestTimeout time.Duration) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { ・・・ if opts.Watch || forceWatch { ・・・ timeout := time.Duration(0) if opts.TimeoutSeconds != nil { timeout = time.Duration(*opts.TimeoutSeconds) * time.Second } if timeout == 0 && minRequestTimeout > 0 { timeout = time.Duration(float64(minRequestTimeout) * (rand.Float64() + 1.0)) } glog.V(2).Infof("Starting watch for %s, rv=%s labels=%s fields=%s timeout=%s", req.URL.Path, opts.ResourceVersion, opts.LabelSelector, opts.FieldSelector, timeout) ・・・ return } ・・・ } }
ソース minikube/pkg/localkube/apiserver.go
// defaults from apiserver command config.GenericServerRunOptions.MinRequestTimeout = 1800
get.go の処理ではログレベル 2 でタイムアウトの値をログ出力しているので(glog.V(2).Infof(・・・)
の箇所)ログから確認できそうです。
ただし、普通に minikube start
で実行してもログレベル 2 のログは見れないようなので、minikube を -v <ログレベル>
オプションを使って起動しなおします。
ログレベル 2 で miinkube 開始
> minikube start -v 2
sample_watch_pod.js の実行
> node sample_watch_pod.js ・・・
minikube logs
でログ内容を確認してみると、get.go が出力しているタイムアウトの値を確認できました。
ログ確認
> minikube logs ・・・ Apr 08 01:00:30 minikube localkube[2995]: I0408 01:00:30.533448 2995 get.go:238] Starting watch for /api/v1/namespaces/default/pods, rv= labels= fields= timeout=58m38.2420124s ・・・
Ramda で階層的なグルーピング
JavaScript 用の関数型ライブラリ Ramda で階層的なグルーピングを行ってみます。
ソースは http://github.com/fits/try_samples/tree/master/blog/20180220/
はじめに
概要
今回は、以下のデータに対して階層的なグルーピングと集計処理を適用します。
データ
const data = [ {category: 'A', item: 'A01', date: '2018-02-01', value: 1}, {category: 'A', item: 'A02', date: '2018-02-01', value: 1}, {category: 'A', item: 'A01', date: '2018-02-01', value: 1}, {category: 'A', item: 'A01', date: '2018-02-02', value: 20}, {category: 'A', item: 'A03', date: '2018-02-03', value: 2}, {category: 'B', item: 'B01', date: '2018-02-02', value: 1}, {category: 'A', item: 'A03', date: '2018-02-03', value: 5}, {category: 'A', item: 'A01', date: '2018-02-02', value: 2}, {category: 'B', item: 'B01', date: '2018-02-03', value: 3}, {category: 'B', item: 'B01', date: '2018-02-04', value: 1}, {category: 'C', item: 'C01', date: '2018-02-01', value: 1}, {category: 'B', item: 'B01', date: '2018-02-04', value: 10} ]
具体的には、上記を category
item
date
の順に階層的にグルーピングした後、value
の合計値を算出して以下のようにします。
処理結果
{ A: { A01: { '2018-02-01': 2, '2018-02-02': 22 }, A02: { '2018-02-01': 1 }, A03: { '2018-02-03': 7 } }, B: { B01: { '2018-02-02': 1, '2018-02-03': 3, '2018-02-04': 11 } }, C: { C01: { '2018-02-01': 1 } } }
Ramda インストール
Ramda は以下のようにインストールしておきます。
> npm install ramda
実装
(a) 階層的なグルーピングと集計
まずは、処理方法を確認するため、順番に処理を実施してみます。
1. category でグルーピング(1層目)
指定項目によるグルーピング処理は R.groupBy
で行えます。
category でグルーピングする処理は以下のようになります。
category グルーピング処理
const R = require('ramda') const data = [ {category: 'A', item: 'A01', date: '2018-02-01', value: 1}, {category: 'A', item: 'A02', date: '2018-02-01', value: 1}, {category: 'A', item: 'A01', date: '2018-02-01', value: 1}, {category: 'A', item: 'A01', date: '2018-02-02', value: 20}, {category: 'A', item: 'A03', date: '2018-02-03', value: 2}, {category: 'B', item: 'B01', date: '2018-02-02', value: 1}, {category: 'A', item: 'A03', date: '2018-02-03', value: 5}, {category: 'A', item: 'A01', date: '2018-02-02', value: 2}, {category: 'B', item: 'B01', date: '2018-02-03', value: 3}, {category: 'B', item: 'B01', date: '2018-02-04', value: 1}, {category: 'C', item: 'C01', date: '2018-02-01', value: 1}, {category: 'B', item: 'B01', date: '2018-02-04', value: 10} ] const res1 = R.groupBy(R.prop('category'), data) console.log(res1)
category グルーピング結果
{ A: [ { category: 'A', item: 'A01', date: '2018-02-01', value: 1 }, { category: 'A', item: 'A02', date: '2018-02-01', value: 1 }, { category: 'A', item: 'A01', date: '2018-02-01', value: 1 }, { category: 'A', item: 'A01', date: '2018-02-02', value: 20 }, { category: 'A', item: 'A03', date: '2018-02-03', value: 2 }, { category: 'A', item: 'A03', date: '2018-02-03', value: 5 }, { category: 'A', item: 'A01', date: '2018-02-02', value: 2 } ], B: [ { category: 'B', item: 'B01', date: '2018-02-02', value: 1 }, { category: 'B', item: 'B01', date: '2018-02-03', value: 3 }, { category: 'B', item: 'B01', date: '2018-02-04', value: 1 }, { category: 'B', item: 'B01', date: '2018-02-04', value: 10 } ], C: [ { category: 'C', item: 'C01', date: '2018-02-01', value: 1 } ] }
2. item でグルーピング(2層目)
category のグルーピング結果を item で更にグルーピングするには res1
の値部分 ([ { category: 'A', ・・・}, ・・・ ]
等) に R.groupBy
を適用します。
これは R.mapObjIndexed
で実施できます。
item グルーピング処理
const res2 = R.mapObjIndexed(R.groupBy(R.prop('item')), res1) console.log(res2)
item グルーピング結果
{ A: { A01: [ [Object], [Object], [Object], [Object] ], A02: [ [Object] ], A03: [ [Object], [Object] ] }, B: { B01: [ [Object], [Object], [Object], [Object] ] }, C: { C01: [ [Object] ] } }
3. date でグルーピング(3層目)
更に date でグルーピングするには R.mapObjIndexed
を重ねて R.groupBy
を適用します。
date グルーピング処理
const res3 = R.mapObjIndexed(R.mapObjIndexed(R.groupBy(R.prop('date'))), res2) console.log(res3)
date グルーピング結果
{ A: { A01: { '2018-02-01': [Array], '2018-02-02': [Array] }, A02: { '2018-02-01': [Array] }, A03: { '2018-02-03': [Array] } }, B: { B01: { '2018-02-02': [Array], '2018-02-03': [Array], '2018-02-04': [Array] } }, C: { C01: { '2018-02-01': [Array] } } }
4. value の合計
最後に、R.groupBy
の代わりに value を合計する処理(以下の sumValue
)へ R.mapObjIndexed
を階層分だけ重ねて適用すれば完成です。
value 合計処理
const sumValue = R.reduce((a, b) => a + b.value, 0) const res4 = R.mapObjIndexed(R.mapObjIndexed(R.mapObjIndexed(sumValue)), res3) console.log(res4)
value 合計結果
{ A: { A01: { '2018-02-01': 2, '2018-02-02': 22 }, A02: { '2018-02-01': 1 }, A03: { '2018-02-03': 7 } }, B: { B01: { '2018-02-02': 1, '2018-02-03': 3, '2018-02-04': 11 } }, C: { C01: { '2018-02-01': 1 } } }
(b) N階層のグルーピングと集計
次は、汎用的に使えるような実装にしてみます。
任意の処理に対して指定回数だけ R.mapObjIndexed
を重ねる処理があると便利なので applyObjIndexedN
として実装しました。
(a) で実施したように、階層的なグルーピングは R.mapObjIndexed
を階層分重ねた R.groupBy
を繰り返し適用していくだけですので R.reduce
で実装できます。(以下の groupByMulti
)
ちなみに、階層的にグルーピングする実装例は Ramda の Cookbook(groupByMultiple) にありましたが、変数へ再代入したりと手続き的な実装内容になっているのが気になりました。
sample.js
const R = require('ramda') const data = [ {category: 'A', item: 'A01', date: '2018-02-01', value: 1}, {category: 'A', item: 'A02', date: '2018-02-01', value: 1}, {category: 'A', item: 'A01', date: '2018-02-01', value: 1}, {category: 'A', item: 'A01', date: '2018-02-02', value: 20}, {category: 'A', item: 'A03', date: '2018-02-03', value: 2}, {category: 'B', item: 'B01', date: '2018-02-02', value: 1}, {category: 'A', item: 'A03', date: '2018-02-03', value: 5}, {category: 'A', item: 'A01', date: '2018-02-02', value: 2}, {category: 'B', item: 'B01', date: '2018-02-03', value: 3}, {category: 'B', item: 'B01', date: '2018-02-04', value: 1}, {category: 'C', item: 'C01', date: '2018-02-01', value: 1}, {category: 'B', item: 'B01', date: '2018-02-04', value: 10} ] /* 指定回数(n)だけ R.mapObjIndexed を重ねた任意の処理(fn)を data を引数にして実行する処理 */ const applyObjIndexedN = R.curry((n, fn, data) => R.reduce( (a, b) => R.mapObjIndexed(a), fn, R.range(0, n) )(data) ) // 階層的なグルーピング処理 const groupByMulti = R.curry((fields, data) => R.reduce( (a, b) => applyObjIndexedN(b, R.groupBy(R.prop(fields[b])), a), data, R.range(0, fields.length) ) ) const cols = ['category', 'item', 'date'] const sumValue = R.reduce((a, b) => a + b.value, 0) const sumMultiGroups = R.pipe( groupByMulti(cols), // グルーピング処理 applyObjIndexedN(cols.length, sumValue) // 合計処理 ) console.log( sumMultiGroups(data) )
実行結果
> node sample.js { A: { A01: { '2018-02-01': 2, '2018-02-02': 22 }, A02: { '2018-02-01': 1 }, A03: { '2018-02-03': 7 } }, B: { B01: { '2018-02-02': 1, '2018-02-03': 3, '2018-02-04': 11 } }, C: { C01: { '2018-02-01': 1 } } }
MQTT Broker をローカル実行
以下の MQTT Broker をそれぞれローカルで実行してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20170910/
Mosca
Mosca は Node.js 用の MQTT Broker です。
npm でインストールして mosca
コマンドで実行できます。
インストール例
> npm install mosca
実行例
> mosca -v +++.+++: ,+++ +++; '+++ +++. ++.+++.++ ++.++ ++,'+ `+',++ ++,++ +` +, +: .+ .+ +; +; '+ '+ +` +` +` +. +: ,+ `+ ++ +; '+ ;+ + +. +` +. +: ,+ `+ +' '+ + +. +` +. +: ,+ `+ :+. '+ +++++. +` +. +: ,+ `+ ++ '+ +++++. +` +. +: ,+ `+ ++ '+ + +. +` +. +: ,+ `+ +: +: '+ ;+ + +. +` +. +: .+ .+ +; +; '+ '+ + +. +` +. +: ++;++ ++'++ ++'+' + +. +` +. +: +++ +++. ,++' + +. {"pid":11260,"hostname":"host1","name":"mosca","level":30,"time":1504448625943,"msg":"server started","mqtt":1883,"v":1}
ログが JSON で出力されていますが、pino ※ を使えば以下のようにログを整形して出力してくれます。
※ mosca 2.5.2 を npm install すると pino もインストールされました
実行例 - pino 利用
> mosca -v | pino +++.+++: ,+++ +++; '+++ +++. ++.+++.++ ++.++ ++,'+ `+',++ ++,++ +` +, +: .+ .+ +; +; '+ '+ +` +` +` +. +: ,+ `+ ++ +; '+ ;+ + +. +` +. +: ,+ `+ +' '+ + +. +` +. +: ,+ `+ :+. '+ +++++. +` +. +: ,+ `+ ++ '+ +++++. +` +. +: ,+ `+ ++ '+ + +. +` +. +: ,+ `+ +: +: '+ ;+ + +. +` +. +: .+ .+ +; +; '+ '+ + +. +` +. +: ++;++ ++'++ ++'+' + +. +` +. +: +++ +++. ,++' + +. [2017-09-03T14:24:23.929Z] INFO (mosca/3124 on host1): server started mqtt: 1883
サーバー組み込み実行
Mosca を組み込み実行するコードは以下の通りです。
実際は new mosca.Server()
だけでサーバーが起動するのですが、そのままだとクライアントからの接続状況が分かり難いのでログ出力しています。
mosca_run.js
const mosca = require('mosca') const server = new mosca.Server() server.on('ready', () => console.log('server started')) server.on('clientConnected', client => console.log(`client connected: ${client.id}`)) server.on('published', (packet) => console.log(`published: ${JSON.stringify(packet)}`))
クライアント処理
MQTT.js をインストールして MQTT のクライアント処理を実装してみます。
MQTT.js インストール例
> npm install mqtt
まずは、指定のトピックへメッセージを publish する処理です。
publish_sample.js
const mqtt = require('mqtt') const client = mqtt.connect('mqtt://localhost') const topic = process.argv[2] const msg = process.argv[3] client.on('connect', () => { client.publish(topic, msg) client.end() })
次に、指定のトピックを subscribe する処理です。
subscribe_sample.js
const mqtt = require('mqtt') const client = mqtt.connect('mqtt://localhost') const topic = process.argv[2] client.on('connect', () => { client.subscribe(topic) }) client.on('message', (topic, msg) => { console.log(`topic: ${topic}, msg: ${msg}`) })
動作確認
まずは Mosca サーバーを実行しておきます。(mosca コマンドで実行しても可)
サーバー組み込み実行
> node mosca_run.js server started
次に data トピックを subscribe してみます。
subscribe 実行
> node subscribe_sample.js data
data トピックへ sample1 ~ 3 という文字列を publish してみます。
publish 実行
> node publish_sample.js data sample1 > node publish_sample.js data sample2 > node publish_sample.js data sample3
subscribe 側にメッセージが出力されました。
subscribe の結果
topic: data, msg: sample1 topic: data, msg: sample2 topic: data, msg: sample3
Moquette
Moquette は Java 用の MQTT Broker です。
distribution-0.10-bundle-tar.tar.gz をダウンロード・解凍した後、bin/moquette.bat や bin/moquette.sh で実行できるようですが、moquette.bat の内容に問題があって、そのままでは Java 8 で実行できませんでした。(## の行を削除するか rem を付けて、JAVA_OPTS の設定を削る等が必要でした)
サーバー組み込み実行
組み込み実行は io.moquette.server.Server
の main
メソッドを呼び出すだけです。
ただし、このままでは IllegalArgumentException: Can't locate file "null"
となってしまうので config/moquette.conf
ファイルを作成しておきます。(デフォルト設定を使うのなら中身は空でよい)
moquette_run.groovy
@GrabResolver(name = 'bintray', root = 'https://jcenter.bintray.com') @Grab('io.moquette:moquette-broker:0.10') @Grab('org.slf4j:slf4j-simple:1.7.25') import io.moquette.server.Server Server.main(args)
動作確認
Mosca と同様に動作確認を行ってみます。
サーバー組み込み実行
> groovy moquette_run.groovy ・・・ [main] INFO io.moquette.server.netty.NettyAcceptor - Server has been bound. host=0.0.0.0, port=1883 [main] INFO io.moquette.server.netty.NettyAcceptor - Configuring Websocket MQTT transport [main] INFO io.moquette.server.netty.NettyAcceptor - Property websocket_port has been setted to disabled. Websocket MQTT will be disabled [main] INFO io.moquette.server.Server - Moquette server has been initialized successfully Server started, version 0.10
data トピックを subscribe してみます。
subscribe 実行
> node subscribe_sample.js data
data トピックへ sample1 ~ 3 という文字列を publish してみます。
publish 実行
> node publish_sample.js data sample1 > node publish_sample.js data sample2 > node publish_sample.js data sample3
subscribe 側にメッセージが出力されました。
subscribe の結果
topic: data, msg: sample1 topic: data, msg: sample2 topic: data, msg: sample3
D3.js で HAR ファイルから散布図を作成
前回、HAR (HTTP ARchive) ファイルから Python で作成した散布図を D3.js を使って SVG として作ってみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20170515/
はじめに
Node.js で D3.js を使用するために d3
と jsdom
をインストールしておきます。
依存ライブラリのインストール
npm install --save d3 jsdom
package.json
{ ・・・ "dependencies": { "d3": "^4.8.0", "jsdom": "^10.1.0" } }
散布図(SVG)の作成
標準入力から HAR ファイルの内容(JSON)を受け取り、time と bodySize の該当位置へ円を描画する事で散布図を表現してみました。
円の色をサブタイプ毎に分けることで色分けしています。
ついでに、ツールチップの表示やマウスイベント処理を追加してみました。
Web ブラウザで SVG ファイルを開いた際にマウスイベントを処理できるように attr
を使ってマウスイベント処理(該当する円のサイズを変更)を設定しています。
index.js
const d3 = require('d3'); const jsdom = require('jsdom'); const { JSDOM } = jsdom; const w = 800; const h = 500; const margin = { left: 80, top: 20, bottom: 50, right: 200 }; const legendMargin = { top: 30 }; const fontSize = '12pt'; const circleRadius = 5; // X軸(time)の値の範囲 const xDomain = [0, 5000]; // Y軸(bodySize)の値の範囲 const yDomain = [0, 180000]; const colorMap = { 'javascript': 'blue', 'x-javascript': 'blue', 'json': 'green', 'gif': 'tomato', 'jpeg': 'red', 'png': 'pink', 'html': 'lime', 'css': 'turquoise' }; const dom = new JSDOM(); const document = dom.window.document; const toSubType = mtype => mtype.split(';')[0].split('/').pop(); const toColor = stype => colorMap[stype] ? colorMap[stype] : 'black'; process.stdin.resume(); let json = ''; process.stdin.on('data', chunk => json += chunk); process.stdin.on('end', () => { const data = JSON.parse(json); const df = data.log.entries.map( d => { return { 'url': d.request.url, 'subType': toSubType(d.response.content.mimeType), 'bodySize': d.response.bodySize, 'time': d.time }; }); const svg = d3.select(document.body) .append('svg') .attr('xmlns', 'http://www.w3.org/2000/svg') .attr('width', w + margin.left + margin.right) .attr('height', h + margin.top + margin.bottom) .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`); const x = d3.scaleLinear().range([0, w]).domain(xDomain); const y = d3.scaleLinear().range([h, 0]).domain(yDomain); const xAxis = d3.axisBottom(x); const yAxis = d3.axisLeft(y); // X軸 svg.append('g') .attr('transform', `translate(0, ${h})`) .call(xAxis); // Y軸 svg.append('g') .call(yAxis); // X軸ラベル svg.append('text') .attr('x', w / 2) .attr('y', h + 40) .style('font-size', fontSize) .text('Time (ms)'); // Y軸ラベル svg.append('text') .attr('x', -h / 2) .attr('y', -(margin.left) / 1.5) .style('font-size', fontSize) .attr('transform', 'rotate(-90)') .text('Body Size'); // 円 const point = svg.selectAll('circle') .data(df) .enter() .append('circle'); // 円の設定 point.attr('class', d => d.subType) .attr('cx', d => x(d.time)) .attr('cy', d => y(d.bodySize)) .attr('r', circleRadius) .attr('fill', d => toColor(d.subType)) .append('title') // ツールチップの設定 .text(d => d.url); // 凡例 const legend = svg.selectAll('.legend') .data(d3.entries(colorMap)) .enter() .append('g') .attr('class', 'legend') .attr('transform', (d, i) => { const left = w + margin.left; const top = margin.top + i * legendMargin.top; return `translate(${left}, ${top})`; }); legend.append('circle') .attr('r', circleRadius) .attr('fill', d => d.value); legend.append('text') .attr('x', circleRadius * 2) .attr('y', 4) .style('font-size', fontSize) // マウスイベント処理(該当する円のサイズを変更) .attr('onmouseover', d => `document.querySelectorAll('circle.${d.key}').forEach(d => d.setAttribute('r', ${circleRadius} * 2))`) .attr('onmouseout', d => `document.querySelectorAll('circle.${d.key}').forEach(d => d.setAttribute('r', ${circleRadius}))`) // 凡例のラベル .text(d => d.key); // SVG の出力 console.log(document.body.innerHTML); });
実行例
node index.js < a.har > a.svg
実行結果例
a.svg
b.svg
node-ffi で OpenCL を使う2 - 演算の実行
「node-ffi で OpenCL を使う」 に続き、Node.js を使って OpenCL の演算を実施してみます。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20160725/
はじめに
演算の実行には ref-array
モジュールを使った方が便利なため、node-ffi をインストールした環境へ追加でインストールしておきます。
ref-array インストール例
> npm install ref-array
OpenCL の演算実行サンプル
今回は配列の要素を 3乗する OpenCL のコード(以下)を Node.js から実行する事にします。
cube.cl
__kernel void cube( __global float* input, __global float* output, const unsigned int count) { int i = get_global_id(0); if (i < count) { output[i] = input[i] * input[i] * input[i]; } }
サンプルコード概要
上記 cube.cl を実行する Node.js サンプルコードの全体像です。(OpenCL の API は try-finally 内で呼び出しています)
OpenCL 演算の入力値として data
変数の値を使用します。OpenCL のコードはファイルから読み込んで code
変数へ設定しています。
OpenCL API の clCreateXXX
で作成したリソースは clReleaseXXX
で解放するようなので、解放処理を都度 releaseList
へ追加しておき、finally で実行するようにしています。
なお、OpenCL API のエラーコード取得には以下の 2通りがあります。(使用する関数による)
- 関数の戻り値でエラーコードを取得
- 関数の引数(ポインタ)でエラーコードを取得
calc.js (全体)
'use strict'; const fs = require('fs'); const ref = require('ref'); const ArrayType = require('ref-array'); const ffi = require('ffi'); const CL_DEVICE_TYPE_DEFAULT = 1; const CL_MEM_READ_WRITE = (1 << 0); const CL_MEM_WRITE_ONLY = (1 << 1); const CL_MEM_READ_ONLY = (1 << 2); const CL_MEM_USE_HOST_PTR = (1 << 3); const CL_MEM_ALLOC_HOST_PTR = (1 << 4); const CL_MEM_COPY_HOST_PTR = (1 << 5); const intPtr = ref.refType(ref.types.int32); const uintPtr = ref.refType(ref.types.uint32); const sizeTPtr = ref.refType('size_t'); const StringArray = ArrayType('string'); const clLib = (process.platform == 'win32') ? 'OpenCL' : 'libOpenCL'; // 使用する OpenCL の関数定義 const openCl = ffi.Library(clLib, { 'clGetPlatformIDs': ['int', ['uint', sizeTPtr, uintPtr]], 'clGetDeviceIDs': ['int', ['size_t', 'ulong', 'uint', sizeTPtr, uintPtr]], 'clCreateContext': ['pointer', ['pointer', 'uint', sizeTPtr, 'pointer', 'pointer', intPtr]], 'clReleaseContext': ['int', ['pointer']], 'clCreateProgramWithSource': ['pointer', ['pointer', 'uint', StringArray, sizeTPtr, intPtr]], 'clBuildProgram': ['int', ['pointer', 'uint', sizeTPtr, 'string', 'pointer', 'pointer']], 'clReleaseProgram': ['int', ['pointer']], 'clCreateKernel': ['pointer', ['pointer', 'string', intPtr]], 'clReleaseKernel': ['int', ['pointer']], 'clCreateBuffer': ['pointer', ['pointer', 'ulong', 'size_t', 'pointer', intPtr]], 'clReleaseMemObject': ['int', ['pointer']], 'clSetKernelArg': ['int', ['pointer', 'uint', 'size_t', 'pointer']], 'clCreateCommandQueue': ['pointer', ['pointer', 'size_t', 'ulong', intPtr]], 'clReleaseCommandQueue': ['int', ['pointer']], 'clEnqueueReadBuffer': ['int', ['pointer', 'pointer', 'bool', 'size_t', 'size_t', 'pointer', 'uint', 'pointer', 'pointer']], 'clEnqueueNDRangeKernel': ['int', ['pointer', 'pointer', 'uint', sizeTPtr, sizeTPtr, sizeTPtr, 'uint', 'pointer', 'pointer']] }); // エラーチェック const checkError = (err, title = '') => { if (err instanceof Buffer) { // ポインタの場合はエラーコードを取り出す err = intPtr.get(err); } if (err != 0) { throw new Error(`${title} Error: ${err}`); } }; // 演算対象データ const data = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9]; const functionName = process.argv[2] // OpenCL コードの読み込み(ファイルから) const code = fs.readFileSync(process.argv[3]); const releaseList = []; try { ・・・ OpenCL API の呼び出し処理 ・・・ } finally { // リソースの解放 releaseList.reverse().forEach( f => f() ); }
clCreateProgramWithSource
へ OpenCL のコードを渡す際に、ref-array
で作成した String の配列 StringArray
を使っています。
OpenCL 処理部分
番号 | 概要 | OpenCL 関数名 |
---|---|---|
(1) | プラットフォーム取得 | clGetPlatformIDs |
(2) | デバイス取得 | clGetDeviceIDs |
(3) | コンテキスト作成 | clCreateContext |
(4) | コマンドキュー作成 | clCreateCommandQueue |
(5) | プログラム作成 | clCreateProgramWithSource |
(6) | プログラムのビルド | clBuildProgram |
(7) | カーネル作成 | clCreateKernel |
(8) | 引数用のバッファ作成 | clCreateBuffer |
(9) | 引数の設定 | clSetKernelArg |
(10) | 処理の実行 | clEnqueueNDRangeKernel |
(11) | 結果の取得 | clEnqueueReadBuffer |
OpenCL のコードを実行するには (6) のように API を使ってビルドする必要があります。
Node.js と OpenCL 間で配列データ等をやりとりするには (8) で作ったバッファを使います。(入力値をバッファへ書き込んで、出力値をバッファから読み出す)
また、今回は clEnqueueNDRangeKernel
を使って実行しましたが、clEnqueueTask
を使って実行する方法もあります。
calc.js (OpenCL 処理部分)
・・・ try { const platformIdsPtr = ref.alloc(sizeTPtr); // (1) プラットフォーム取得 let res = openCl.clGetPlatformIDs(1, platformIdsPtr, null); checkError(res, 'clGetPlatformIDs'); const platformId = sizeTPtr.get(platformIdsPtr); const deviceIdsPtr = ref.alloc(sizeTPtr); // (2) デバイス取得 (デフォルトをとりあえず使用) res = openCl.clGetDeviceIDs(platformId, CL_DEVICE_TYPE_DEFAULT, 1, deviceIdsPtr, null); checkError(res, 'clGetDeviceIDs'); const deviceId = sizeTPtr.get(deviceIdsPtr); const errPtr = ref.alloc(intPtr); // (3) コンテキスト作成 const ctx = openCl.clCreateContext(null, 1, deviceIdsPtr, null, null, errPtr); checkError(errPtr, 'clCreateContext'); releaseList.push( () => openCl.clReleaseContext(ctx) ); // (4) コマンドキュー作成 const queue = openCl.clCreateCommandQueue(ctx, deviceId, 0, errPtr); checkError(errPtr, 'clCreateCommandQueue'); releaseList.push( () => openCl.clReleaseCommandQueue(queue) ); const codeArray = new StringArray([code.toString()]); // (5) プログラム作成 const program = openCl.clCreateProgramWithSource(ctx, 1, codeArray, null, errPtr); checkError(errPtr, 'clCreateProgramWithSource'); releaseList.push( () => openCl.clReleaseProgram(program) ); // (6) プログラムのビルド res = openCl.clBuildProgram(program, 1, deviceIdsPtr, null, null, null) checkError(res, 'clBuildProgram'); // (7) カーネル作成 const kernel = openCl.clCreateKernel(program, functionName, errPtr); checkError(errPtr, 'clCreateKernel'); releaseList.push( () => openCl.clReleaseKernel(kernel) ); const FixedFloatArray = ArrayType('float', data.length); // 入力データ const inputData = new FixedFloatArray(data); const bufSize = inputData.buffer.length; // (8) 引数用のバッファ作成(入力用)し inputData の内容を書き込む const inClBuf = openCl.clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, bufSize, inputData.buffer, errPtr); checkError(errPtr, 'clCreateBuffer In'); releaseList.push( () => openCl.clReleaseMemObject(inClBuf) ); // (8) 引数用のバッファ作成(出力用) const outClBuf = openCl.clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, bufSize, null, errPtr); checkError(errPtr, 'clCreateBuffer Out'); releaseList.push( () => openCl.clReleaseMemObject(outClBuf) ); const inClBufRef = inClBuf.ref(); // (9) 引数の設定 res = openCl.clSetKernelArg(kernel, 0, inClBufRef.length, inClBufRef); checkError(res, 'clSetKernelArg 0'); const outClBufRef = outClBuf.ref(); // (9) 引数の設定 res = openCl.clSetKernelArg(kernel, 1, outClBufRef.length, outClBufRef); checkError(res, 'clSetKernelArg 1'); const ct = ref.alloc(ref.types.uint32, data.length); // (9) 引数の設定 res = openCl.clSetKernelArg(kernel, 2, ct.length, ct); checkError(res, 'clSetKernelArg 2'); const globalPtr = ref.alloc(sizeTPtr); sizeTPtr.set(globalPtr, 0, data.length); // (10) 処理の実行 res = openCl.clEnqueueNDRangeKernel(queue, kernel, 1, null, globalPtr, null, 0, null, null); checkError(res, 'clEnqueueNDRangeKernel'); const resData = new FixedFloatArray(); // (11) 結果の取得 (outClBuf の内容を resData へ) res = openCl.clEnqueueReadBuffer(queue, outClBuf, true, 0, resData.buffer.length, resData.buffer, 0, null, null); checkError(res, 'clEnqueueReadBuffer'); // 結果出力 for (let i = 0; i < resData.length; i++) { console.log(resData[i]); } } finally { // リソースの解放 releaseList.reverse().forEach( f => f() ); }
動作確認
今回は以下の Node.js を使って Windows と Linux の両方で動作確認します。
- Node.js v6.3.1
(a) Windows で実行
「node-ffi で OpenCL を使う」 で構築した環境へ ref-array
をインストールして実行しました。
実行結果 (Windows)
> node calc.js cube cube.cl 1.3310000896453857 10.648000717163086 35.93699645996094 85.18400573730469 166.375 287.4959716796875 456.532958984375 681.4720458984375 970.2988891601562
(b) Linux で実行
前回 の Docker イメージを使って実行します。
calc.js と cube.cl を /vagrant/work へ配置し、Docker コンテナからは /work でアクセスできるようにマッピングしました。
Docker コンテナ実行
$ docker run --rm -it -v /vagrant/work:/work sample/opencl:0.1 bash
Node.js と必要なモジュールのインストール
# curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | bash ・・・ # source ~/.bashrc # nvm install v6.3.1 ・・・ # npm install -g node-gyp ・・・ # cd /work # npm install ffi ref-array ・・・
/work 内で実行します。
実行結果 (Linux)
# node calc.js cube cube.cl 1.3310000896453857 10.648000717163086 35.93699645996094 85.18400573730469 166.375 287.4959716796875 456.532958984375 681.4720458984375 970.2988891601562