TECH PLAY

アプトポッド

アプトポッド の技術ブログ

248

研究開発グループのエンジニアの酒井 ( @neko_suki )です。 aptpod Advent Calendar 2020 の14日目を担当します。 研究開発グループでは、TCP/QUIC/UDP などのトランスポートプロトコルの製品適用に向けた検証を行っています。 今回の記事は前回の「5Gのネットワークを計測してみた」の続きになります。 tech.aptpod.co.jp 今回の記事では、以下の2つを紹介します。 モバイルルーターを Wi-Fi STATION SH-52A に固定して行った4G/5G のping/iperf3 の計測結果 4G/5G でのTCP/QUIC/UDP の計測結果 計測機材の紹介 計測場所の紹介 ネットワーク計測の結果 pingの計測 iperf3の計測 UDPの結果 TCPの結果 トランスポートプロトコルの評価 実験内容 評価結果 TCPの結果 QUICの結果 UDPの結果 考察 まとめ 計測機材の紹介 計測機材は前回と同じ機材を使用しています。 計測用のクライアントは組み込み製品を想定しRaspberry Pi4を使用しました。計測用のサーバは、AWS上でEC2のm5.xlargeのUbuntuインスタンスを使用しました。Raspberry Pi側はUbuntu 20.04、サーバ側はUbuntu 18.04 を使用しています。 また、5G/4Gの接続にはDocomoから販売されている モバイルルーター Wi-Fi STATION SH-52A を使用しています。SH-52Aは「5G/4G/3G」で接続するモードと「4G/3G」で接続するモードを選択可能です。「5G/4G/3G」モードでは5Gの接続に固定されるわけではありませんが、今回計測を行った場所では安定して5Gに接続が可能でした。 以下のような構成で計測をします。 前回の記事の再掲になりますが、計測に使った機器は以下のように接続しています。 計測場所の紹介 前回の記事で計測を行った場所と同じ日産スタジアム東ゲート付近の広場で、12/4に計測を行いました。 ネットワーク計測の結果 pingの計測 pingの計測では、pingコマンドを使って1秒間隔で60秒間RTT (Round Trip Time)を計測しました。 比較のために、12/4 に計測した結果と前回の記事の計測結果の両方を載せます。前回の記事では4Gの計測は モバイルWi-FiルーターL-01G というモバイルルーターを使っていました。 以下の表には、前回の結果と今回の計測結果の、平均値、10パーセンタイル値、50パーセンタイル値、90パーセンタイル値を載せています。 4G(2020/12/4計測) 5G(2020/12/4計測) 4G(2020/12/2掲載) 5G(2020/12/2掲載) 平均値 40.3msec 43.3msec 74.5msec 38.5msec 10パーセンタイル値 31.7msec 32.9msec 65.2msec 30.4msec 50パーセンタイル値 40.5msec 44.0msec 75.6msec 39.15msec 90パーセンタイル値 47.9msec 52.12msec 83.9msec 46.09msec 前回の記事では、5Gの方が4GよりもRTTが小さいと紹介しましたが、モバイルルーターをSH-52Aに統一した結果を見ると、4Gと5GのどちらもRTTには差分がなさそうです。 L-01G を使用した場合のみ大きく違う値が出ていますが、この原因は今後調査を行い機会があればお伝えしたいと思います。 iperf3の計測 iperf3の計測では、UDPとTCP(輻輳制御はBBRを使用)のそれぞれについて、クライアントからサーバに20秒間データを送信する計測を3回行いました。 UDPの結果 UDPの計測は律速するまで帯域を上げています。4Gは25Mbps、5Gは50Mbpsを設定しています。 4G(2020/12/4計測) 5G (2020/12/4計測) 4G(2020/12/2掲載) 5G (2020/12/2掲載 1回目 20.9 Mbps 45.2 Mbps 17.4Mbps 45.2Mbps 2回目 21.2 Mbps 18.4 Mbps 24.6Mbps 47.0Mbps 3回目 20.7 Mbps 39.4 Mbps 25.0 Mbps 47.0 Mbps このように、おおむね5Gの方が高いスループットを得られています。 UDPの2回目の時は、未到達のパケットがかなり多かったため4G側より低い値になっていますが、ネットワークの変動によるものではないかと思います。 TCPの結果 TCPの場合はビットレートを指定しなくても流せる上限まで流そうとするのでビットレートの指定はしません。 計測結果は以下のようになりました。 4G(2020/12/4計測) 5G (2020/12/4計測) 4G(2020/12/2掲載) 5G (2020/12/2掲載 1回目 10.3 Mbps 40.3 Mbps 9.33Mbps 15.9Mbps 2回目 13.6 Mbps 43.4 Mbps 11.1Mbps 17.8Mbps 3回目 16.2 Mbpbs 34.2 Mbps 12.3 Mbps 34.4 Mbps おおむね5Gの方が高いスループットが出ています。ただし、5Gの結果は前回の記事の結果と今回の結果で大きく差が出ています。したがって、変動の影響はありうると考えるのがよさそうです。 トランスポートプロトコルの評価 ここからは、トランスポートプロトコルの評価についてお伝えします。 実験では、 TCP(輻輳制御はCUBICを使用) QUIC(輻輳制御はCUBICを使用) UDP をそれぞれ4G/5Gで評価しています。 計測は4G/5Gに対してそれぞれ5回ずつ行いました。 実験内容 実験は、高頻度なデータの塊をクライアントからサーバに伝送するユースケースを設定します。 送信するデータは1unit (データの単位)を8byteとします。これを1000 unit/secで送信します。そのために1msec毎に1unitのデータをクライアント側で生成します。 弊社製品のintdashでは、IPやTCPヘッダのオーバーヘッドを低減するために、一定期間のユニットをバッファリングして送信する仕組みを導入しています。 なので、それに倣って10個の連続したデータを一つの塊として送信します。このバッファリングしたデータ単位はflush *1 と呼ばれています。 以下の図のように、送信時刻までの10msec分のデータが1つのflushとしてまとめて送信されます。 flush クライアント-サーバ間の送信遅延を評価するために、データがクライアントで生成された時刻とサーバ側で受信した時刻の差分を遅延時間として定義します。 サーバでは1flush分のデータをまとめて受信します。そのため、flushの先頭のunitは、ネットワークの上りの遅延+10msec程度の遅延が発生します。 クライアントとサーバ間のデータのやり取り この場合、遅延時間は以下のようになります。 生成時刻 受信時刻 1unit 0msec ネットワークの上りの遅延 + 10msec 2unit 1msec ネットワークの上りの遅延 + 9msec 3unit 2sec ネットワークの上りの遅延 + 8msec ... ... ... 10unit 9msec ネットワークの上りの遅延+1msec ここから、理想的な状態では「ネットワークの上りの遅延+10msec」の範囲に、遅延時間が収まることが期待されます。 評価結果 それでは実際に結果を見ていきます。 TCPの結果 ここでは5回の計測結果の中から一つの結果を取り上げていますが、他の結果も同様の傾向になっています。 まずはヒストグラムです。横軸は、遅延時間、縦軸は頻度になっています。 TCP ヒストグラム 理想的には10msec以内に収まってほしいのですが、5Gの場合は20msec、4Gの場合は40msec の範囲にデータが収まっていることがわかります。pingの結果が4G/5Gで差異がなかったことを考えると、4G/5Gの回線のみを変えて結果に差分が出た点は疑問が残ります。 原因は、クライアントからサーバの間のどこかで想定外のバッファリングが行われたのではないかと予想しています。 ヒストグラムだけだと理解が難しいので、時系列でのプロットも見てみます。 まずは、5Gの結果です。以下のグラフは横軸が10秒間に送信される10000unitのデータの番号で、縦軸がそれぞれの遅延時間になります。5Gの場合は19msec~36msec 辺りに収まっていることがわかります。 TCP(5G)の結果 次に4Gの結果です。4Gの場合は多くの値が、10msec~50msecの間にあることがわかります。 TCP(4G)の結果 原因は調査が必要ですが、ヒストグラムの違いの理由は明らかになりました。 QUICの結果 QUICの結果のヒストグラムは以下のようになります。 QUIC ヒストグラム ここでも、理想的には10msec以内に収まっていてほしいものが、5Gの場合は20msec、4Gの場合は40msec の範囲に収まっていることがわかります。 5Gの場合はTCPと同じような結果になっています。一方で4Gの場合は2つの山が見える結果になっています。 TCPの時と同様に、時系列のプロットを見てみます。 5Gの結果は以下のようになります。ヒストグラムで見たように、18msec~38msecの間にデータが収まっていることがわかります。 QUIC(5G)の結果 次に、4Gの結果です。4Gの場合は、18msec~34msecの範囲と、15msec~45msecの二つのパターンがあることがわかります。これはネットワーク上で別の経路を通った結果が反映されている可能性がありそうです。この結果、ヒストグラムには二つの山ができていると考えられます。 QUIC(4G)の結果 UDPの結果 UDPの結果のヒストグラムは以下のようになります。 UDP ヒストグラム TCP/QUICの結果と異なり、5Gの方が遅延時間のとる範囲が大きくなっています。 ここでも時系列のデータを見てみます。 5Gの結果はこれまでと異なり、値のとる範囲が安定していないことがわかります。その結果がヒストグラムにも反映されていたようです。 5G(UDP)の結果 逆に4Gの結果は、最初の2秒(2000unit)の振れ幅は大きいですが、それ以降は 19msec~29msec 辺りに落ち着いているデータが多いように見えます。 4G(UDP)の結果 考察 疑問が多く残る結果になりましたが、少なくとも以下の3点は言えそうです。 10unit以上のデータがまとまって届いているため、経路上のどこかで予期していないキューイングが発生している可能性が考えられる ネットワーク上の経路の違いが遅延時間に影響を与えている TCP/QUIC/UDPすべての結果を踏まえると、4G/5Gの違いがあると言い切れる根拠は今回の実験では得られなかった 疑問が多く残っているため、今後も調査・検証を続けていこうと思います。 まとめ 今回の記事では「5Gでのトランスポートプロトコルの評価」について紹介しました。 ネットワークの再計測の結果やトランスポートプロトコルの評価結果は疑問が多く残る結果となりました。今後も調査・検証を継続、新しい事実が判明したらお届けしたいと思います。 最後までご覧いただきありがとうございました。 *1 : https://www.aptpod.co.jp/products/white-paper/iscp.pdf
アバター
みなさま、こんにちは。研究開発グループと製品開発グループ に兼務で所属しております、きしだです。 aptpod Advent Calendar 2020 11日目を担当します。今回は機械学習に関わるエンジニア向けに、最近AWSがリリースしたAWS Lambdaの新機能を利用して、サクッと推論APIを作るネタをご紹介します。 aws.amazon.com 推論をすばやくAPI化する意義 その前に、推論箇所をAPIとしてすばやく用意できる必要性について簡単に触れたいと思います。 機械学習関係の案件では、お客様側も理解できるKPIを立てることが非常に重要視されています。例えば「モデルの正解率を〜にしたい」や「モデルの動く速さを〜にしたい」などですね。これらの具体的な数値はお客様と議論を重ね、お互いにしっくりくる数値に落とし込む必要があるのですが、これがとてもむずかしいのです。なぜでしょうか。 お客様と開発側で共に運用イメージがわかず、モデルに対して運用時の影響力がピンとこない 具体的な運用イメージをすべて仮説ベースで作り上げるため、仮説にほころびが生まれやすく議論が錯綜する 上記に心当たりがある方もいらっしゃるのではないのでしょうか。これらはいずれも「システム運用を前提としたモデルの使い方をお客様側でイメージできていない」ことに起因します。このときにAPIまでを用意してお客様に共有すると、以下のようなことが期待できます。 APIを叩くだけで推論結果が返ってくるので、お客様側もモデルを試しやすい APIとして用意されているので一時的にシステムに組み込むことができ、試験運用が可能になる 必要最低限の費用で、お客様側で運用しながらモデルをさわることができる ガチガチにシステムを作るより、まずは小さく簡単なシステムから始めることでイテレーションを回せる いいこと尽くめですね。お客様側もモデルにふれる機会がグッと増え、議論を活発にまわすことができ、より納得感をもってすすめることができます。そのためにモデル開発に留めず運用側に一歩踏みこむ姿勢が大事になり、モデルの推論箇所をAPIとしてすばやく用意することが必要になります。 今回の記事はその一歩のヒントになるよう、私が検証した内容の一部をご紹介するものです。 AWS Lambda コンテナサポートの嬉しいポイント AWS Lambdaといえば、 「サーバーのことを意識しないサーバーレスコンピューティングを提供してくれるサービス」で、アプリケーションをすばやく実行環境に持っていきたい時に重宝する素晴らしいサービスです。それだけでなく、Amazon API Gateway などの他サービスをトリガーにしたイベント定義も楽々できることで有名ですよね。 機械学習の実行環境もAWS Lambda上で動かせたら楽だろうなぁと思う時がありつつも、以下の制約により断念していました。 パッケージの割り当て量が 250MB Lambda向けにパッケージを作る際、依存パッケージが多いと手間がかかる ローカルテスト/デバッグはAWS Lambdaだけでお手軽にできない とくに容量制限が厳しく、機械学習の場合はTensorFlowやらKerasなどの大きめのパッケージを使用することが多いため、到底250MBに抑えるなんてことはできません。 しかしながら冒頭に記載のAWSのリリース内容では、 大きく以下のような内容がピックアップされていました。 コンテナイメージをそのままLambda関数としてデプロイできる コンテナイメージは 10GB までならデプロイ可能 ローカルで実行できるLambdaのRuntime APIツール Lambda Runtime Interface Emulator がオープンソース化される パッケージの容量が大きく緩和され、コンテナイメージ向けのローカルテストツールも提供されるとのことです。これは、従来のLambdaに対して抱いていた課題感をすべて払拭してくれる予感がしますね。 前置きが長くなってしまいましたが、試してみないわけにはいかない! ということで、実際に動かして見ましょう。 構成 今回は、 こちら のTensorFlowを使用した「車両を検出するモデル」を、AWS Lambda上で動かしてみます。 *1 以下のように、Amazon API Gatewayと連携させてクライアントPCから画像をPOSTすると、車両の検出結果を返すAPIを作ります。 構成図 TensorFlowで作成されたモデルが動くコンテナイメージを用意しAmazon ECRにPushします AWS Lambda上でLambda関数を作成、Amazon API Gatewayと紐付けて画像のPOST用APIを用意します コンテナイメージを作ってローカルでテストする まずはコンテナイメージを作ってみましょう。 手元にあるコンテナをそのまま使えばよいのではなく、AWS Lambda向けにコンテナを作りなおす必要があります。 理由としては、Lambda Runtime API を実装する Lambda Runtime Interface Clients を取り込む必要があるためです。このクライアントを実行するためにいくつか依存ライブラリをインストールしなければいけません。 上記をふまえ、以下のようなDockerfileを準備します。 # Define function directory ARG FUNCTION_DIR="/function" FROM tensorflow/tensorflow:1.14.0-py3 # Install aws-lambda-cpp build dependencies RUN apt-get update && \ apt-get install -y \ g++ \ make \ cmake \ unzip \ libcurl4-openssl-dev \ libsm6 \ libxrender1 \ libxtst6 \ # Install for vehicle detection python3-dev \ libsm6 \ libxext6 \ libxrender-dev \ libgl1-mesa-dev \ python3-tk # Include global arg in this stage of the build ARG FUNCTION_DIR # Create function directory RUN mkdir -p ${FUNCTION_DIR} # Copy function code COPY app/app.py ${FUNCTION_DIR}/app.py COPY app/lib/ ${FUNCTION_DIR}/lib/ # other python file RUN mv ${FUNCTION_DIR}/lib/* ${FUNCTION_DIR} # Install the runtime interface client & other python package COPY requirements.txt / RUN pip install --upgrade pip && \ pip install awslambdaric && \ pip install --target ${FUNCTION_DIR} --no-cache-dir -r requirements.txt # Set working directory to function root directory WORKDIR ${FUNCTION_DIR} # (optional) for TEST COPY aws-lambda-rie /usr/bin/ RUN chmod 755 /usr/bin/aws-lambda-rie ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ] CMD [ "app.handler" ] いくつかポイントを記載します。 # (optional) for TEST COPY aws-lambda-rie /usr/bin/ RUN chmod 755 /usr/bin/aws-lambda-rie コンテナをローカルでテストする際は、冒頭で紹介の通り Runtime interface emulator を使います。ローカルでテストしたい場合は AWSの公式ドキュメント に従い、インストールしておきましょう。 (※コンテナにいれなくても、コンテナ実行時にマウントする方法もあるのでここは任意です) ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ] CMD [ "app.handler" ] 実行時は、 Lambda Runtime Interface Client により対象の関数が呼び出されます。上記を実行するため ENTRYPOINT でクライアントの実行コマンド、 CMD で対象関数を指定します。 (今回は app.pyの handler 関数を対象) 上記の準備ができたら、buildします。 docker build -t serverless-function:latest . buildできたら、まずはローカルで動作するか確認します。 以下を実行します。 docker run -p 9000:8080 --entrypoint /usr/bin/aws-lambda-rie --name serverless --rm serverless-function:latest /usr/local/bin/python -m awslambdaric app.handler すると、以下のようにコンテナ内のLambda Runtime APIが動作します。 time="2020-12-08T07:30:14.459" level=info msg="exec '/usr/local/bin/python' (cwd=/function, handler=app.handler)" 早速エミュレーターが提供しているエンドポイントを叩いて見ましょう。 以下の画像をエンドポイントに投げてみます。 車の画像 以下はクライアント側をPythonで簡単に書いたものです。 # Amazon API Gatewayの代わりに base64にencode data = base64.b64encode(f.read()) data = { "body" :data} response = requests.post( "http://localhost:9000/2015-03-31/functions/function/invocations" , json=data) contents = json.loads(response.content) imageBody = base64.b64decode(contents[ "body" ]) Image.open(io.BytesIO(imageBody)) 上記を実行すると、コンテナ側のログが出力されていました。 START RequestId: 11be1529-4ae3-47f4-8aee-2e81fe7a63e5 Version: $LATEST /usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/dtypes.py:516: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. ・・・ 2020-12-08 07:30:45.707671: W tensorflow/compiler/jit/mark_for_compilation_pass.cc:1412] (One-time warning): Not using XLA:CPU for cluster because envvar TF_XLA_FLAGS=--tf_xla_cpu_global_jit was not set. If you want XLA:CPU, either set that envvar, or use experimental_jit_scope to enable XLA:CPU. To confirm that XLA is active, pass --vmodule=xla_compilation_cache=1 (as a proper command-line flag, not via TF_XLA_FLAGS) or set the envvar XLA_FLAGS=--xla_hlo_profile. END RequestId: 11be1529-4ae3-47f4-8aee-2e81fe7a63e5 REPORT RequestId: 11be1529-4ae3-47f4-8aee-2e81fe7a63e5 Init Duration: 2.54 ms Duration: 7020.34 ms Billed Duration: 7100 ms Memory Size: 3008 MB Max Memory Used: 3008 MB Jupyter notebook側でもデータが返ってきました。 推論もできている様子です👍 推論結果 ECRにプッシュし、AWS Lambda上で実行してみる いよいよ実環境へのデプロイです。 ECRに先ほど作成したコンテナイメージをpushします。(docker loginが必要ですのでご注意ください) docker tag serverless-function:latest {AWS_ACCOUNT_NO}.dkr.ecr.ap-northeast-1.amazonaws.com/vehicle-detection-api:latest docker push {AWS_ACCOUNT_NO}.dkr.ecr.ap-northeast-1.amazonaws.com/vehicle-detection-api:latest コンソールを見ると無事pushできていました。 ECRの様子 次に Lambda関数を作成してみます。関数の作成時、「コンテナイメージ」を選択するとECRにコミットされているコンテナイメージを選択することができます。 関数の作成 これで無事作成できました。 次にAmazon API Gateway を連携させて、推論したい画像データをPOSTするためのAPIを構築します。 構築後のURL 無事構築できたので、ローカルの時と同様に画像を送付してみます。  ローカルテストと同様に、クライアント側として以下のコードを用意します。 response = requests.post( "https://{API_GATEWAY_CODE}.execute-api.ap-northeast-1.amazonaws.com/default/vehicle-endpoint-test" , data=data, headers=header ) imageBody = base64.b64decode(response.content) Image.open(io.BytesIO(imageBody)) 上記を実行すると....以下のように車が推論された結果が返ってくることが確認できました! 推論の結果 予想より簡単に推論エンドポイントを立てることができました! 🎉 使ってみた感想 一通り試してみた感想をまとめておきます。 良かった点 予め用意したコンテナをベースにLambda向けにビルドし直せばいいので、環境の作り方が楽! AWS Lambda Runtime API との連携テストもローカルで実施しやすくなり、環境準備がしやすい! 今後の課題となりそうな点 基本Lambda関数の実行時の仕様は変わらず。AWS Lambdaの仕様でデータのキャッシュはできないので、推論する場合毎度モデルをロードする必要がある ローカルでテストできるのは Lambda Runtime API経由のみなので、Amazon API Gatewayなどの他サービスとの連携までを見据えたテストまで完結することはむずかしい まとめ AWS Lambda上に推論環境として構築したコンテナイメージをデプロイし、Lambda関数として動かすことができました。外部に共有できるAPIとしてはまだ課題はありそうですが、とても簡単に試すことができるのでケースに当てはまりそうな方がいたらぜひ活用してみてください! ちなみに余談ですが、弊社製品のintdashというデータストリーミングプラットフォームと今回の推論APIのような機械学習ツールと連携すると、データの収集から推論まで行えるシステムをサクッと作れます。 もしまた機会があれば、今年リリース発表された intdash SDK for Python を使って、今回構築したAPIとintdashを連携したパターンもやってみたいと思います。 intdash SDK for Python のリリース発表はこちら www.aptpod.co.jp ご覧いただきありがとうございました! *1 : https://github.com/ahmetozlu/vehicle_counting_tensorflow より一部引用
アバター
はじめに Advent Calendar 2020 10 日目を担当する、SRE チームの柏崎です。 nginx をお使いのみなさま、TLS 導入が当然となっている現代、どんな設定をされていますでしょうか。 NGINX Config や SSL Server Test のようなサイトを利用し、セキュリティに気を配っている方も多いと思います。 そんな nginx ユーザのみなさま、最近 nginx 1.19.4 がリリースされ、 イケてる TLS 設定ができるようになった事はご存じでしょうか? ChaCha20-Poly1305 について まず前提知識として、ChaCha20-Poly1305 について、ほんの少し書いておきます。 ChaCha20-Poly1305 は、ストリーム暗号である ChaCha20 とメッセージ認証符号である Poly1305 を組み合わせた、認証付き暗号です。 RFC 7905 (2016/06) で TLS における利用が標準化され、TLS 的には比較的新しい暗号方式となっています。 OpenSSL だと 1.1.0 以降で利用可能です。 これまで、現実的に使える暗号方式として一択だった AES-GCM に加え、ChaCha20-Poly1305 が加わった形になります。 クライアントによる 2 方式の性能差 ChaCha20-Poly1305 はソフトウェア処理に向いた、簡潔なアルゴリズムです。 一方 AES-GCM は、ソフトウェア処理ではあまり性能が出ませんが、ハードウェア処理が利用できる環境ではとても高速です。 Intel や AMD の CPU には AES をハードウェア処理できる AES-NI という拡張機能が載っていますが、ARM 等には載っていません。 つまり、AES-GCM と ChaCha20-Poly1305 のどちらが性能が高いかは、環境によって違ってくるということです。 Android スマートフォンや Raspberry Pi、最近発売された M1 Mac など、ARM で動くデバイスは意外と多いです。 特に、弊社の製品のエッジ向けミドルウェア intdash Edge は、Raspberry Pi を含め様々なデバイスで利用されるので、この性能差は気になるところです。 実際に、openssl speed コマンドを使って、8192 バイトブロックにおける、AES-GCM と ChaCha20-Poly1305 の暗号化・復号化の性能を測ってみました。 ざっくりとした性能差を知るのが目的のため、それぞれ 1 回ずつの計測です。 利用したコマンドは下記です。 ### aes-128-gcm の暗号化 $ openssl speed -evp aes-128-gcm ### aes-256-gcm の暗号化 $ openssl speed -evp aes-256-gcm ### chacha20-poly1305 の暗号化 $ openssl speed -evp chacha20-poly1305 ### aes-128-gcm の復号化 $ openssl speed -evp aes-128-gcm -decrypt ### aes-256-gcm の復号化 $ openssl speed -evp aes-256-gcm -decrypt ### chacha20-poly1305 の復号化 $ openssl speed -evp chacha20-poly1305 -decrypt 確かに、Intel では AES-GCM の方が高性能ですが、ARM では逆転して ChaCha20-Poly1305 が高性能ですね。 クライアントの暗号スイートリスト TLS クライアントによっては、ネゴシエーション時の暗号スイートリストが、前述の性能差を考慮したものになっています。 クライアントの暗号スイートリストは Qualys SSL Labs の SSL Client Test で確認することができるので、実際に Intel MacBook と M1 MacBook で比較をしてみました。 Intel 系 MacBook 上の Chrome M1 MacBook 上の M1 用 Chrome 確かに、Intel MacBook では AES-GCM が優先されているのに対し、M1 MacBook では ChaCha20-Poly1305 が優先されていますね。 これまでの課題 TLS のネゴシエーションにおける暗号スイートは、クライアントとサーバの暗号スイートリストを比較することで、1 つに決定されます。 しかし、一般的な TLS サーバは、「サーバ側の暗号スイートリストを優先して」暗号スイートを決定するような設定がされています。 つまり、TLS クライアントが適切な優先度で暗号スイートリストを送ってきたとしても、サーバ側の優先度により決まった一つに決定されてしまうのです。 例えば、Raspberry Pi は ChaCha20-Poly1305 を使ってラクしたいのに、限られたリソースのなか苦手な AES-GCM で頑張らないといけない、みたいな事が起きてしまいますね。 nginx 1.19.4 でどうなる? nginx 1.19.4 では、 ssl_conf_command ディレクティブ を使って、OpenSSL の設定を行うことができるようになりました。 下記の設定を追加すると、「クライアントが ChaCha20 の暗号スイートを優先している場合はサーバも優先する」という動作をさせることができます。 イケてる! ssl_conf_command Options PrioritizeChaCha; この設定は、 僕が勝手に nginx の実験場にしている アプトポッドのコーポレートサイト にも適用しています。 Chrome でアクセスして開発ツールを開くと、下記の画像のように環境によって適切な暗号方式でつながるはずです。 Intel 系 MacBook 上の Chrome M1 MacBook 上の M1 用 Chrome さいごに ちょっとマニアックになってしまいましたが、nginx の TLS 設定についてお話をしました。 ちなみに M1 Macbook でのスクショは、同僚に協力していただきました。ありがとうございます!M1 羨ましい! nginx 1.19.4 は mainline のバージョンなので、intdash のサービス用サーバでは、まだこの設定を適用できておりません。 stable 版の 1.20.0 が待ち遠しいですね。 この記事は、筆者の個人ブログの記事をベースに加筆修正したものです nginx 1.19.4 以降で ChaCha20-Poly1305 の運用が現実的になった件
アバター
aptpod Advent Calender 2020 、 9日目の記事です。 本日の担当は、組込み開発チームでFW開発を担当している矢部です。 はじめに 組込み機器の開発に関わって1x年ですが、入出力の自動化が難しい機器も多く、結局手作業になって無駄にボタン押下やUI操作の速度が洗練されたりします。私の場合、ゲームをよくやっていたからか、効率よく操作させることができるとちょっと嬉しかったりもします。 とはいえリグレッションテストなどを考えると極力自動化したいところなので、過去いろいろと試行錯誤してきました。 今回は、その中でも比較的簡単な、USB通信をインターフェースに持つ機器のテストを自動化した際の手法について簡単に解説します。 前提知識 今回の記事で詳しくは解説しない部分をさらっと。 BDDとは 振る舞い駆動開発(Behavior Driven Development)と呼ばれるもので、TDDの一種。 システムに期待する挙動や制約などを自然言語に近い形式で記述し、テストを実行できるようにしたもの。自然言語で書けるため、非開発者であってもシステムの仕様がわかるように書ける(らしい)。 テストファーストな開発だと「テストコードは仕様である」という考え方になるので、それを突き詰めたものがBDDかな、と思っています。 BDDのフレームワーク 主要な言語であれば、たいていBDDを実現するためのフレームワークが存在します。 Ruby: RSpec Python: Behave C/C++: Catch2 etc... 私が最初に触れたのはRSpecですが、最近はPythonを利用する機会が多いため、今回はBehaveで実装します。 Pythonの開発環境 色々ありますが、最近はpipenvを使っています。設定や実行方法など、バランスが取れていて使いやすい。 公式: https://pipenv-ja.readthedocs.io/ja/translate-ja/ Pipenvを使ったPython開発まとめ: https://qiita.com/y-tsutsu/items/54c10e0b2c6b565c887a USB機器をテストする 実行環境 Ubuntu MATE 20.04 今回はLinuxのデバイスドライバを利用する都合上、テスト環境はLinuxになります。 Python環境構築 テスト用のディレクトリに、pipenvを利用してBehaveをインストールします。 $ mkdir usbtest $ cd usbtest $ pipenv --python 3 $ pipenv install behave テスト対象 今回は私が開発に関わった、aptpodの CAN-USB Interface - AP-CT2A 1 を対象にします。実際に AP-CT2A ではこの方法で全機能の自動テストコードを書いています。なお、2020/12 時点ではデバイスドライバなどのコードを公開していないため、細かいところはぼかしたり改変しています。 PythonでUSBデバイスを操作する 方法はいくつかあると思いますが、今回はデバイスドライバがあるため、 ioctl を利用して操作します。 基本的な手順は以下になります。 デバイスファイルをopenする。 ファイルディスクリプタを利用して、ioctlでIO制御する。 ioctlのリクエストは汎用的なものであればPython側にありますが、今回のように特殊なものは自前で設定する必要があります。 リクエストの種別として IO、 IOR、_IOWなど 2 があり、C/C++であれば以下のように実装されます。 struct fw_data_s { int value; }; typedef struct fw_data_s fw_data_t; #define CMD1 _IO( 'A' , 0x1 ) #define CMD2 _IOR( 'A' , 0x2 , fw_data_t) #define CMD3 _IOW( 'A' , 0x3 , fw_data_t) 上記のコマンドがデバドラ側にあると仮定して、デバイス操作のためのクラスをPython上で実装すると以下のようになります。 __make_xxx のメソッドが、リクエストの生成部分になります。 import ctypes import fcntl import logging import os class FwData (ctypes.Structure): _fields_ = [( "value" , ctypes.c_int32)] class ApCt2a : def __init__ (self, path): self.__path = path def open (self): self.__fd = os.open(self.__path, os.O_RDWR | os.O_NONBLOCK) if self.__fd == - 1 : logging.critical( "[APTTRX] failed open path:{0}" .format(self.__path)) def close (self): os.close(self.__fd) self.__fd = None def cmd1 (self): fcntl.ioctl(self.__fd, self.__make_io_req( 0x1 )) @ property def data (self): data = FwData() fcntl.ioctl(self.__fd, self.__make_ior_req( 0x02 , ctypes.sizeof(data)), data) return data.value @ data.setter def data (self, value): data = FwData() data.value = value fcntl.ioctl(self.__fd, self.__make_iow_req( 0x03 , ctypes.sizeof(data)), data) def __make_io_req (self, nr): return ord ( "A" ) << 8 | nr def __make_iow_req (self, nr, size): return 1 << 30 | ord ( "A" ) << 8 | nr | size << 16 def __make_ior_req (self, nr, size): return 2 << 30 | ord ( "A" ) << 8 | nr | size << 16 テストする仕様 AP-CT2Aのメイン機能は、入力したCAN 3 データに時刻情報を付与してホストに渡すというものです。さらに複数台あった場合、同期用のケーブルで機器間を接続することで、付与する時刻を同期させることができます。 AP-CT2A接続図 上の写真で、機器間を繋いでいるのが同期ケーブルです(開発用の機材なので汚いのはご容赦を)。今回は、この同期機能を確認するためのテストを例に取ります。 テストしたいシーケンス 2台のAP-CT2Aを接続する 2台のAP-CT2Aを時刻同期する CANデータを500kbpsで受信できるよう設定する CANデータを入力する 2台のAP-CT2A受信したCANデータの時間が一致していることを確認する 実際に書いてみる ファイルは以下のような形で構成されます。 .feature にテストシナリオ、 steps/ 以下に実際にシナリオ内で実行される処理(step)を実装します 4 。 features/ features/everything.feature features/steps/ features/steps/steps.py 今回の例だと、まずテストシナリオをこのように書きます 5 。 Feature: AP-CT2A テスト Scenario: 複数台で時間同期する Given 2台のAP-CT2Aを用意する And AP-CT2Aを時刻同期する And AP-CT2Aを500kbps入力に設定する When AP-CT2AにCANデータを500kbpsで10個入力する Then AP-CT2AがCANデータを10個取得している And AP-CT2Aが受信したCANデータの時刻情報が一致している そして、各stepの処理について、以下のように書きます。 @~ のところで対応するstepを指定し、変数として利用したい箇所は {} で記載します 6 。 from behave import given, step, then, when @ given ( "AP-CT2Aを時刻同期する" ) def step_impl (context): for d in context.devices: d.sync() @ when ( "AP-CT2AにCANデータを{baudrate:d}kbpsで{num:d}個入力する" ) def step_impl (context, baudrate, num): send_candata(baudrate, num) @ then ( "AP-CT2AがCANデータを{num:d}個取得している" ) def step_impl (context, num): for d in context.devices: data = d.receive() assert len (data) == num 実行例 上のシナリオを実行した例。 $ pipenv shell $ behave Feature: AP-CT2A テスト # features/ap_ct2a.feature:1 Scenario: 複数台で時間同期する # features/ap_ct2a.feature:3 Given 2台のAP-CT2Aを用意する # features/steps/ap_ct2a_steps.py:6 0.001s And AP-CT2Aを時刻同期する # features/steps/ap_ct2a_steps.py:11 0.001s And AP-CT2Aを500kbps入力に設定する # features/steps/ap_ct2a_steps.py:16 0.000s When AP-CT2AにCANデータを500kbpsで10個入力する # features/steps/ap_ct2a_steps.py:21 0.000s Then AP-CT2AがCANデータを10個取得している # features/steps/ap_ct2a_steps.py:26 0.000s And AP-CT2Aが受信したCANデータの時刻情報が一致している # features/steps/ap_ct2a_steps.py:31 0.000s 1 feature passed, 0 failed, 0 skipped 1 scenario passed, 0 failed, 0 skipped 6 steps passed, おわりに 今回はUSB機器を例に取りましたが、何らかのプロトコルで通信することで動作するものについてはほぼ自動テスト化できるものと思っています。 外部からの操作を必要とするものに関しては(例えばUSBの挿抜とか)なかなか自動化は難しいですが、aptpodには優秀なHWエンジニアも在籍しており、テスト用の治具も自作したりします。 組み込み機器であっても、既存のフレームワークをうまく使ったり、アイデア次第で煩雑なテスト業務を軽減させることができますので、トライしてみてはいかがでしょうか。 https://www.aptpod.co.jp/products/hardware/ のペリフェラルデバイスの項に載っています。 ↩ https://www.quora.com/What-is- IO- IOR-and-__IOW-in-ioctl ↩ https://ja.wikipedia.org/wiki/Controller_Area_Network ↩ https://behave.readthedocs.io/en/latest/tutorial.html#features ↩ 普段は日本語では書きません。マルチバイト文字怖い。 ↩ https://behave.readthedocs.io/en/latest/tutorial.html#python-step-implementations ↩
アバター
Advent Calendar 2020 8日目 を担当します。ハードウェアグループの加藤です。 私は2020年2月にハードウェアエンジニアとして入社しました。10か月間業務を行って思ったこと、感じたことなどを書きます。 自己紹介 ハードウェアエンジニアの仕事 アプトポッドで仕事して思ったこと、感じたこと 社員に対する信頼がある 社員同士の互いの信頼や尊重もある。 やらされている感がない 事業に無理がない 技術的に本質的な方向を向いている 業務環境、開発に使用しているツール アプトポッドに入社してみた結果 仕事に対するモチベーションが大いに上がった 未経験の業務もやらせてもらえるのでスキルアップ 妻が喜んだ 最後に 自己紹介 簡単に私がアプトポッドに入社するまでに勤めた会社についてご説明します。特に意識はしていませんが、入社して思ったこと感じたことというのはこれまで勤めた会社との比較になっていると思います。 ゲーム機等の会社 1994年に入社。ゲームセンター向けのゲーム機器を6年間、海外カジノ向けのスロットマシンを13年間、業務用フィットネスバイクの開発を2年間、各グループ会社の開発部門でハードウェアエンジニアとして商品開発を行いました。海外カジノ向けスロットマシンのグループ企業は、米国のラスベガスに本社を設置するところからのスタートでいろいろ大変でした。 業務用フィットネスバイクの企画から販売まで行うスタートアップ企業 設立されたばかりの会社に入社しました。社員数は10名以下。ハードウェアもソフトウェアも自分以外はわかる人はおらず大変でした。業務用フィットネスバイクというのは、スポーツクラブ等においてある自転車型の運動機器です。この会社では大学・企業・プロの運動部にターゲットを絞った製品の開発を行いました。ここでは4年ちょっと勤めて、立ち上げを成功させることができました。この後、アプトポッドに入社しました。 ハードウェアエンジニアの仕事 アプトポッドのハードウエアエンジニアの業務内容はこんな感じです。 ハードウェア製品の企画、投資(費用・工数)回収計画の立案 設計、開発、評価等 規格・認証試験の対応 製造会社の選定、その他いろいろも含めて量産できるようにする。 量産対応(生産を継続する。生産効率や品質の向上も。) 顧客案件のHWカスタマイズ対応 企画から量産まで全部ですね。 製造そのものは必要な設備等を有する会社に委託しています。そうは言っても部品のEOL対応などは自分達で判断する必要があります。設計や開発は都度終わらせることができますが、生産はそうはいきません。製品の種類が増えてきた時にどうするかはこれからの課題となっています。それでも自分たちで効率よく行っていくのが最も良いのかなぁ。と考えています。 アプトポッドで仕事して思ったこと、感じたこと 社員に対する信頼がある パソコンを自宅に持ち帰ることができます。 ”何言っているの?”という人の方が多いでしょうか? 私がこれまで勤めていた会社では平社員はパソコンの持ち出しができませんでした。 オフィスには、お菓子コーナーがあってお菓子食べ放題です。カップ麺も栄養ドリンクも置いてあります。コーヒーも飲めます。自動販売機の飲み物も1本30円です。 しかし、無茶苦茶なことをする人はいません。無茶苦茶な事というのは、本当に食べ放題食べてしまったり、カップ麺や自販機の飲み物を大量に持ち出して外で売りさばいたりということです。 新型コロナウィルス騒ぎ以降、会社全体として在宅勤務が主体となったのですが、「在宅勤務だからやらないといけないこと」というのもありません。 第一印象は、「会社は社員を信頼していて、社員のモラルも高い。すごいなこの会社。」でした。 社員同士の互いの信頼や尊重もある。 社内でなにか人にお願いする時、人からお願いされる時、期限を設定しないのです。誰もがすぐに対応します。なので、必要最低限の時間で事が進んでいきます。いやぁ、すごいです。 打ち合わせを設定する時は、参加者のカレンダーを確認して空いてさえいれば、好きなところに入れられます。極端な話、今すぐ!でもOKです。それで注意されたり怒られたりすることはありません。 やらされている感がない 上司の都合で書類を作成したり提出したりを求められることがありません。業務のスケジュールも自分で立てますし、レビューも自分で設定します。準備ができたらカレンダーに設定するだけなので、参加者の都合の調整などは発生しません。 事業に無理がない ハードウェアは量産時に材料費がかかります。販売価格の設定に無理があるとずーっと赤字のままになってしまいます。しかし、競合他社と同じ内容の製品を販売することになってしまう場合、営業部門の人は価格を安く設定することを求めざるを得なくなります。 アプトポッドの場合、独自性と技術力の高い製品・サービスを提供することができるので、販売価格や原価で無理を求められることはありません。 一般的に、独自性の高い商品であったとしても、ハードウェア単体だと同等品は簡単に作られてしまうので、売れて目立ってしまうとすぐ価格競争になってしまいます。アプトポッドの場合、そういう心配も不要であり、設計・開発に集中できるのはいいなぁと感じています。 技術的に本質的な方向を向いている 設計開発部門が向いている技術的な方向性に対して、私個人が安心感を得られるかどうかということです。主観なので正しい/間違っているはないのですが、自分にとっては大事に考えていることです。何かというと、例えば掃除機を作っている会社があったとして、吸引力に優れた掃除機を作れている会社であるなら安心で、(お客さんの要望だったとしても)本体の重量が軽いとか紙パックの容量が大きいといった掃除機を売ったり売れちゃったりしている会社であれば不安といった具合です。ディスプレイだったら、画面大きく、解像度高く、発色に優れた製品を出せていれば大丈夫で、”専用の眼鏡をかけると立体に見えます。”ということだと、「大丈夫かな?」って感じです。 データをクラウドに保存し、そこから閲覧する製品・サービス( IoTプラットフォーム intdash )を行っている会社で「データの完全回収」「低遅延」「高速」(これだけではありませんが)に対する方向性が社内全体で見受けられ、これに対し私は安心感を得ました。 業務環境、開発に使用しているツール 出社する/自宅で仕事するは本人の自由 オフィスは新宿区四谷四丁目 勤務時間も完全に裁量労働制 社内コミュニケーションはSlack 打ち合わせはGoogleMeetが基本、GoogleCalendarで互いの予定を確認 ドキュメント作成はConfluence、課題管理はJIRA 基板CADはQuadcept、筐体設計CADはFusion360 CADは「必要な機能が一通り揃っており手頃な価格帯のもの」を使用していると思います。選定する人の好みや経験に影響されやすいと思うのですが、こんなところにも柔軟で合理的な会社の色がでているように感じました。 アプトポッドに入社してみた結果 仕事に対するモチベーションが大いに上がった 成功体験がないとなかなか大きい成長が得られない。そして、成長市場でないと成功体験を得るのは難しい。さらに、成長市場であっても人や組織や方向性がしっかりと確立されていないと長続きしない。アプトポッドは全てを満たしており、そんな中で仕事をできることに幸せ(と書くと胡散臭いですが)を感じ、大いにモチベーションをもって仕事に取り組むことができるようになりました。 未経験の業務もやらせてもらえるのでスキルアップ 必要とされる業務であれば、やりたいと言えばやらせてもらえます。私の今年の業務の場合、全く経験はありませんでしたが3DCADで基板筐体用のデータや加工用の図面を作成したりといった事をやらせてもらえました。今回のような記事の投稿というのも初めての体験でした。ただ、「やって良い」というだけでなく、周りの多くの方々に教えていただきながら業務を進めることが出来るので、自身のスキルアップが実現できました。 妻が喜んだ アプトポッドに勤めることになって、私の妻も「よかったわぁ」と大変喜んでいます。主な理由はふたつです。 ほとんど在宅勤務なので、「いつもそばにいるので安心」「家なので食事も制御可能」 私の妻の場合、「私が見える場所にいる。自分が帰ってきた時に私が家にいる」のが安心ということでした。 新型コロナウィルス騒ぎ以降9.5カ月間で出社したのは9日程度、EMC試験の為の外出が5日程度でした。 「将来性のある業種なので安心」「社員数も多い会社なので安心」 思うところはありますが、まぁ喜んでくれるのはありがたいことです。 社員数はその会社の成長フェーズによって変化するもので、多ければ良いというものではないと思います。しかし、社員数10名以下の中小企業で社員1人が頑張っているだけでは、なかなか会社も個人も成長は難しく、妻の言っていることも100%間違っているというわけでもないかな。とも思います。人数が少ない(特に1人の場合)と得られる情報が少なく進歩に時間がかかるのでキツイです。 アプトポッドよりも社員数の多い会社は多数あるでしょうが、各社員が優秀であることと効率の高いツールを使いこなしていることから、共有している情報の質と量は高いレベルにあるのだろうなと思っています。 最後に 社外の方は、良い事ばっかり書いてあるようにお感じになる方もいらっしゃるかもしれません。しかし、今回の記事は私が思ったこと・感じたことで嘘はありません。 もしアプトポッドに興味を持っていただけたら、 弊社の採用ページ に足を運んでいただけるとありがたく思います。
アバター
Advent Calendar 2020 7日目担当デザイン室の上野です。 デザイン室では製品や案件のUIデザインを行っています。 弊社のWebベースのダッシュボードアプリケーション Visual M2M Data Visualizer で可視化できるVisual Partsには3Dモデルを表示するパーツも存在します。 そこで用いられる3Dモデル作成も担当しています。 パーツをパネルに貼り付けてデータを疎通させると以下の画像のようにモデルがサーバに送られてきたデータと同期してリアルタイムに動くようになっています。 こういった3Dモデルでのデータ可視化は直感的でわかりやすいため、製造業のお客様の現場で需要があります。 今回はそこで使われている3Dモデルの作成フローと気をつけている3つのポイントを書いていこうと思います。 モデリングツールについて 3Dモデルの作成フロー モデリング時に気をつけている3つのポイント ちょうど良いくらいにポリゴン数を調整する 法線の向きを正しく ゴミを残さない モデリングツールについて 弊社では Maya LT を主に使用しています。 MAYA LTは元々MAYAと呼ばれるピクサーなどが使っている有名な3DCGソフトがあり、そこからレンダリングなど映像に特化した機能を制限し、ゲームなどをはじめとするリアルタイムに描画するモデリングに特化したソフトウェアになっています。 MAYA自体が業界標準的な側面もあり、ドキュメントがとても豊富です。 他には Blender というツールも使い始めています。 こちらは無料で間口が広く様々なユーザーがおり拡張機能も豊富なのが魅力です。 UIなどが独特で一昔前は使いにくい印象でしたが、最近は大きいアップデートがあり有料ソフトウェアと遜色ないほど進化してきました。 書き出しできるフォーマットやそれぞれ特化した機能の違いがあるので要件によって使い分けています。 3Dモデルの作成フロー モデルの作成は0から作るパターンと、CADなどの3Dデータをいただいて作り直すパターンがあります。 0から3Dモデリングを作る場合のフローは以下の3つのステップになります。 ① 下準備として三面図を用意 ② Maya上に三面図を引いてモデリング ③ 完成 CADなどの3Dデータをいただいて作り直す場合は、そのままのフォーマットではブラウザで表示することはできないので、一度MAYA LT上に取り込みます。 その際にそのまま書き出そうとするとポリゴン数があまりにも膨大(データ量も肥大)なので、ガシガシ減らして動作に耐えうるくらいまでモデルを修正していきます。 モデルが完成したら、そこからOBJファイルを書き出し、Photoshopに読み込んでサムネイルを作成します。 ここまでがデザイン室の作業となります。 あとはOBJファイルとサムネイル画像をエンジニアに渡し、実装をお願いしていく形になります。 他にもボーンを入れてアニメーションできるようにFBXファイルというフォーマットで実装した例もあります。 アバターロボットを遠隔でリアルタイム制御する未来へ 〜Aptpod×MELTIN MMI エンジニアクロストークインタビュー〜 こちらのロボットの3Dモデルは、アバターロボットの操作側から上がってくるデータで各関節にあるボーンの回転を制御しました。 こういったアニメーションをする3Dモデルは、外観ができたあと内側にボーンを配置していき、各ポリゴンの頂点のウェイトを紐付けしていく作業があります。 モデリング時に気をつけている3つのポイント 自分がモデリングをする際に気をつけているポイントは ポイント1 ちょうど良いくらいにポリゴン数を調整 ポイント2 法線の向きを正しく ポイント3 ゴミを残さない の3つです。 ちょうど良いくらいにポリゴン数を調整する 実はこちらの3Dパーツですがマテリアルは全てThree.jsでマテリアルを制御してライン描画するようになっています。 リアルタイムで描画するにあたってポリゴン数が多いと動作が重くなります。 しかし、ポリゴンが極端に少なすぎると密度がスカスカになり形がわかりにくくなってしまいます。 動作を保ちながら形も分かりやすいようにちょうどいいポリゴン数になるように心がけて作っています。 ポリゴン数は ディスプレイ -> ヘッドアップディスプレイ -> ポリゴン数 にチェックを入れることでビューポートに表示されるようになります。 作る対象によって差はありますが多くても7000ポリゴン以内を目安に作っています。 (今回例にだしている飛行機のモデルは汎用的なものになるので、特徴を出さずに△1028ポリゴンと少なめに作っています) お客様からCADベースのモデルデータを表示したいと依頼されるケースがあります。 ポリゴン数50万〜データ容量も50MBなど、Webブラウザで扱うには重たいデータなので、扱えるようにローポリ化します。 例えば下図のように約70万ポリゴンを2500ポリゴンに減らすとこれだけ軽くなり、パフォーマンスを保ってブラウザで表示できます。 法線の向きを正しく 法線とはポリゴンのフェースの向きを表します。 緑の線が飛び出ている方向がフェースの向きになります。 左側が上向きのフェース、右側が下向きのフェースです。向きによって描写が変わるのがわかりますね。 メッシュの結合やポリゴンの穴を埋めたりしているとこの向きが反転してしまうことがあります。 そのままだと描画がおかしくなるため対象のフェースを選び メッシュ -> 法線 -> 反転 で直していきます。 確認方法としては、対象のオブジェクトを選択し、 ディスプレイ -> ポリゴン -> フェース法線 をONにすることで画面のように表示されます。 ゴミを残さない ポリゴンの結合などを繰り返したりしていると気づかないうちに不整合な要素ができてしまいます。 そういった要素も描画時に問題がおきる元なのでそちらもしっかり取り除いて綺麗なデータを渡すようにしています。 メッシュ -> クリーンアップオプション を開き該当項目にチェックして適用を押すと該当するフェースなどを綺麗にしてくれます。 他にもヒストリの削除など作業中に色々整理しながら作っていきます。 以上がWebアプリケーション用の3Dモデルの作成フローと気をつけている3つのポイントになります。 ありがとうございました!
アバター
aptpod Advent Calendar 2020 の4日目を担当します、研究開発グループの大久保です。 2日目 では、Rustと Quinn でechoサーバを作成しました。今回は応用として、Quinnを使って大量のデータを送信し、パフォーマンス評価をしてみたいと思います。弊社内のユースケースとして、エッジ側で溜まったデータをサーバに送信したい、という状況が考えられるため、それを想定した評価となります。 実装と検証 1プロセス内でサーバ、クライアント両方立てます。 Cargo.toml に追記する依存関係は以下のようになります。 [dependencies] anyhow = "1" futures = "0.3" quinn = "0.6" rand = "0.7" rcgen = "0.8" tokio = { version = "0.2", features = ["full"] } 評価用のコードは以下のようになります。 use anyhow :: * ; use futures :: StreamExt; use quinn :: { Certificate, CertificateChain, ClientConfigBuilder, Connecting, Endpoint, NewConnection, PrivateKey, ServerConfig, ServerConfigBuilder, TransportConfig, }; use rand :: {thread_rng, Rng}; use std :: net :: {IpAddr, Ipv4Addr, SocketAddr}; use std :: time :: Instant; // 送信用のデータ(10KiB) const MSG_SIZE: usize = 1024 * 10 ; #[tokio::main] async fn main () -> Result < () > { let cert = rcgen :: generate_simple_self_signed ([ "localhost" . to_owned ()])?; let cert_der = cert. serialize_der ()?; let priv_key = cert. serialize_private_key_der (); let cert_der_clone = cert_der. clone (); // 送信用の適当なデータを乱数で用意する let mut send_data = vec! [ 0u8 ; MSG_SIZE]; thread_rng (). fill ( &mut send_data[..]); // サーバを動かす tokio :: spawn (async move { run_server (cert_der_clone, priv_key).await. unwrap (); }); // クライアントを動かし、所要時間(ミリ秒)を表示する let start = Instant :: now (); run_client (cert_der, & send_data).await?; let elapsed = start. elapsed (); println! ( "Elapsed time: {} ms" , elapsed. as_secs () * 1000 + elapsed. subsec_millis () as u64 ); Ok (()) } // サーバを動かす async fn run_server (cert_der: Vec < u8 > , priv_key: Vec < u8 > ) -> Result < () > { let mut transport_config = TransportConfig :: default (); transport_config. stream_window_uni ( 0xFF ); let mut server_config = ServerConfig :: default (); server_config.transport = std :: sync :: Arc :: new (transport_config); let mut server_config = ServerConfigBuilder :: new (server_config); let cert = Certificate :: from_der ( & cert_der)?; server_config. certificate ( CertificateChain :: from_certs ( vec! [cert]), PrivateKey :: from_der ( & priv_key)?, )?; let mut endpoint = Endpoint :: builder (); endpoint. listen (server_config. build ()); let addr = SocketAddr :: new ( IpAddr :: V4 ( Ipv4Addr :: new ( 0 , 0 , 0 , 0 )), 33333 ); let (endpoint, mut incoming) = endpoint. bind ( & addr)?; println! ( "listeing on {}" , endpoint. local_addr ()?); while let Some (conn) = incoming. next ().await { tokio :: spawn (async { match handle_connection (conn).await { Ok (_) => (), Err (e) => { eprintln! ( "{}" , e); } } }); } Ok (()) } // サーバへの接続を扱う async fn handle_connection (conn: Connecting) -> Result < (), Error > { let NewConnection { connection, mut uni_streams, .. } = conn.await?; println! ( "connected from {}" , connection. remote_address ()); while let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; tokio :: spawn (async { let _data = uni_stream. read_to_end (MSG_SIZE).await. unwrap (); }); } println! ( "connection closed from {}" , connection. remote_address ()); Ok (()) } // クライアントを動かす async fn run_client (cert_der: Vec < u8 > , send_data: & [ u8 ]) -> Result < () > { let mut client_config = ClientConfigBuilder :: default (); client_config. add_certificate_authority ( Certificate :: from_der ( & cert_der)?)?; let addr = SocketAddr :: new ( IpAddr :: V4 ( Ipv4Addr :: new ( 127 , 0 , 0 , 1 )), 33333 ); let mut endpoint_builder = Endpoint :: builder (); endpoint_builder. default_client_config (client_config. build ()); let (endpoint, _incoming) = endpoint_builder. bind ( & "0.0.0.0:0" . parse (). unwrap ())?; let NewConnection { connection, .. } = endpoint. connect ( & addr, "localhost" )?.await?; println! ( "connected: addr={}" , connection. remote_address ()); for _ in 0 .. 1000 { let connection = connection. clone (); let mut send_stream = connection. open_uni ().await?; send_stream. write_all (send_data).await?; send_stream. finish ().await?; } connection. close ( 0u8 . into (), & []); endpoint. wait_idle ().await; Ok (()) } echoサーバと基本的に同じように構築していきますが、いくつかの変更点があります。 let cert = rcgen :: generate_simple_self_signed ([ "localhost" . to_owned ()])?; let cert_der = cert. serialize_der ()?; let priv_key = cert. serialize_private_key_der (); main 関数のこちらの部分では、 rcgen クレートを用いて、自己署名証明書を作成しています。テスト目的ならこれで十分でしょう。 while let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; tokio :: spawn (async { let _data = uni_stream. read_to_end (MSG_SIZE).await. unwrap (); }); } handle_connection 関数では、サーバに対して張られた単方向ストリームを読み込みます。読み込むだけで返信は行いません。 for _ in 0 .. 1000 { let connection = connection. clone (); let mut send_stream = connection. open_uni ().await?; send_stream. write_all (send_data).await?; send_stream. finish ().await?; } run_client 関数では、用意したデータ(10KiB)を1000回ループを回して送信します。 こちらを実行すると、以下のような出力になりました。 listeing on 0.0.0.0:33333 connected: addr=127.0.0.1:33333 connected from 127.0.0.1:47158 closed by peer: 0 Elapsed time: 131 ms 1000回送信したので、1回あたり0.13ms程度で10KiBを送信していたことが分かります。 送信タスクを非同期にする せっかく tokio で非同期にコードを書いているので、 FuturesUnordered を使って送信タスクを非同期にしてみます。その場合、送信を1000回行っているfor文を次のように書き換えます。 let mut tasks = futures :: stream :: FuturesUnordered :: new (); for _ in 0 .. 1000 { let connection = connection. clone (); let send_data = send_data. to_vec (); let task = async move { let mut send_stream = connection. open_uni ().await. unwrap (); send_stream. write_all ( & send_data).await. unwrap (); send_stream. finish ().await. unwrap (); }; tasks. push (task); } while let Some (_) = tasks. next ().await {} この場合の実行結果は以下のようになりました。 listeing on 0.0.0.0:33333 connected: addr=127.0.0.1:33333 connected from 127.0.0.1:42291 closed by peer: 0 Elapsed time: 83 ms 1回あたり0.083msで、すこし短縮されます。 なお、送信するデータを10KiBから100KiBにした場合、1回送信ごとにブロッキングする実装で663ms、非同期で668msと、ほとんど差が無くなります。1ストリームあたりに流す量に応じて使い分けるのが良さそうです。 最後に 今回は、RustとQuinnを使って、大量のデータを送信した場合のパフォーマンスを調査しました。Quinnは、tokioをベースとして実装されているため、tokioやfuturesといったRustの非同期機能と組み合わせて使うことができ、コネクションやストリームを開くのも非常に直感的にできます。今後もこれらの機能を活用しながら、引き続きQUICの利用法を調査していく予定です。
アバター
研究開発グループのエンジニアの酒井 ( @neko_suki )です。 aptpod Advent Calendar 2020 の3日目を担当します。 研究開発グループでは、TCP/QUIC/UDP などのトランスポートプロトコルの製品適用に向けた検証を行っています。 aptpodの製品は車載などの移動体の組み込み機器もターゲットにしているため、これらのトランスポートプロトコルは4G/5G 上での使用が想定されます。 そこで、5Gを使用して、ネットワークの計測やトランスポートプロトコルを評価しようということになりました。 今回の記事では、5GのネットワークでGoogleのスピードテスト/ping/iperf3などの計測を行った結果を紹介します。 なお、トランスポートプロトコルの評価については、12/15 「5G上でのトランスポートプロトコルの評価」にて投稿する予定です。 計測用機材の紹介 Wi-Fi 接続の影響について 計測場所について 都庁周辺の調査 新宿区立新宿中央公園 新宿住友ビル1F フリースペース 新宿三井ビル前のオープンスペース 渋谷ストリーム 日産スタジアム周辺 計測結果 googleのスピードテスト 計測場所① 計測場所② 計測場所③ pingの計測 iperf3の計測 UDPの結果 TCPの結果 まとめ 計測用機材の紹介 まずは計測用機材を紹介します。 Googleスピードテストは、 Xperia XZ2 SO-03K で計測しています。 pingとiperf3の計測用のクライアントは組み込み製品を想定しRaspberry Pi4を使用しました。pingとiperf3計測用のサーバーは、AWS上でEC2のm5.xlargeのUbuntuインスタンスを使用しました。Raspberry Pi側はubuntu 20.04、サーバー側はubuntu 18.04 を使用しています。 以下のような構成で計測をします。 構成図 5Gへの接続には、Docomoから販売されているモバイルルーター Wi-Fi STATION SH-52A を使用しています。 5G接続時には、画面が青く光り表示が「5G」に代わるため5Gに接続しているのかどうかの判別が可能です。 5G接続時 Raspberry PiとSH-52Aは有線で以下のように接続します。Raspberry Piと赤いケーブルで接続されているのはモバイルバッテリーです。 5G を計測する機器の構成 Wi-Fi STATION SH-52A では、手動で5Gと4Gを選択することはできない 1 ため、4G計測用には モバイルWi-FiルーターL-01G を使用しています。 L-01Gは有線接続がないためRaspberry Piとモバイルルーター間はWi-Fiで接続しました。 4G 計測用機器の構成 Wi-Fi 接続の影響について 公正に評価できるように、Raspberry PiとWi-Fi接続影響について調べました。 まずはRaspberry PiとL-01G間のRTTについてです。 Raspberry PiからL-01Gに対して、1秒間隔で1分間の設定でpingコマンドを実行しました。RTTの値は最小値3.1msec、平均値 7.952msec、最大値26.8msecでした。状況による変動はあると思いますが、今回は4GでpingによるRTTの計測を行うと8msec 程大きい値が出ると想定します。 次に、Wi-Fiの帯域確認のために、Raspberry PiとパソコンをそれぞれL-01G経由で接続しiperf3を使って両者の間の帯域を調べました。 結果は、TCP 31.5Mbps、UDP 32.1Mbpsとなりました。もし4Gの計測を行ったときにこの値で律速した場合は、Wi-Fiの影響があるかもしれません。 計測場所について ドコモが5Gを提供しているエリアは、 サービスエリアマップ から確認することが出来ます。 最初に会社のオフィスの近くにある新宿区の都庁周辺と渋谷ストリームで計測ができないか確認に行きました。下記の画像の赤い部分が5Gが入るといわれている場所です。 都庁周辺の5Gエリアマップ 渋谷ストリーム周辺の5Gエリアマップ 両者ともあまり芳しくなかったため、周辺に大きなビルなどがない日産スタジアムを調査対象に選びました。 日産スタジアム周辺の5Gエリアマップ 都庁周辺の調査 都庁周辺は後述する3か所で確認をしました。5Gが入りますが、すぐに4Gに切り替わってしまい計測を行えるほど安定した接続は難しかったです。 新宿周辺は高いビルなどが多いため、5Gの直進性が高く減衰しやすいという特性による影響があったのかもしれません。 新宿区立新宿中央公園 新宿中央公園はエリアマップの範囲から少し外れていますが、時々5Gが入ることがありました。 新宿中央公園 新宿住友ビル1F フリースペース 新宿住友ビルの1Fのフリースペースは屋内ですが、5Gが入りました。コンクリートではなくガラス張りだったのが影響しているかもしれないです。 新宿三井ビル前のオープンスペース ここは上記の2か所よりは安定して5Gが拾えました。しかし、4Gに切り替わることが多かったため計測には不向きと考えられます。 新宿三井ビル前 渋谷ストリーム 渋谷ストリームはイベントホール内での接続が可能のようです。イベントホールには入れませんでしたが、4Fで一瞬だけ5Gを拾えました。しかしそれ以降5Gは拾えませんでした。 日産スタジアム周辺 都庁周辺・渋谷では厳しそうだったので、日産スタジアムを訪問しました。 日産スタジアム 日産スタジアムの周りは高いビルなどもなく、5Gの接続状況はかなり良かったです。 ということで、日産スタジアム周辺で計測を行いました。 計測結果 googleのスピードテスト スピードテストはgoogleが提供している回線速度計測のサービスです。上りと下りのスループットを計測することが出来ます。 スピードテストはSH-52A、L-01G をXperia XZ2 SO-03KとWi-Fiで接続して行いました。 場所による変化を見るために、スタジアムの東ゲート付近の3か所で計測しています。 計測場所① Smile Table という建物の脇の場所で計測しました。 日産スタジアム計測場所① 5Gの結果は、ダウンロードが76.1Mbps、アップロードが63.4Mbpsでした。 日産スタジアム場所①5Gスピードテストの結果 一方で4Gの結果は、ダウンロードが7.38Mbps、アップロードが18.6Mbpsになりました。 日産スタジアム場所①4Gのスピードテストの結果 計測場所② 次に計測したのは東ゲート前です。 日産スタジアム場所② 5Gの結果は、ダウンロードが50.7Mbps、アップロードが25.1Mbpsでした。 日産スタジアム計測場所② 5Gスピードテストの結果 一方で4Gの結果は、ダウンロードが4.18Mbps、アップロードが17.5Mbpsになりました。 日産スタジアム計測場所② 4Gスピードテストの結果 計測場所③ 3か所目は、東ゲート付近の広場です。 日産スタジアム計測場所③ 5Gの結果は、ダウンロードが92.7Mbps、アップロードが36.7Mbpsでした。 日産スタジアム計測場所③ 5Gスピードテストの結果 一方で4Gの結果は、ダウンロードが16.5Mbps、アップロードが27.3Mbpsになりました。 日産スタジアム計測場所③ 4Gスピードテストの結果 ばらつきはありますが3か所すべてでダウンロード・アップロードともに5Gの方が4Gよりもスループットが高い結果になりました。 pingの計測 pingの計測は、計測場所③で行っています。 pingコマンドを使って1秒間隔で60秒間RTTを計測しました。 Raspberry PiからAWS上のEC2インスタンスまで60秒の間に1秒間隔で計測しています。 ヒストグラムのプロット結果は以下のようになります。(外れ値は除いています) pingの結果 RTTにはばらつきがありますが、傾向として5Gの方が小さい値になっています。 2 また、最小値、平均値、最大値は以下のようになりました。 最小値 平均値 最大値 5G 26.8msec 38.5msec 47.9msec 4G 58.3msec 74.8msec 93.9msec 参考までに、L-01GとのRaspberry Pi間のRTTの平均で8msecを引いてプロットしても、傾向は変わらないと言えそうです。 pingの結果 4GはWi-Fi分8msecを引いた iperf3の計測 iperf3の計測も計測場所③で行っています。 ここでは、UDPとTCP(輻輳制御はBBRを使用)それぞれ20秒間の上りの計測を3回行いました。 UDPの結果 UDPの計測は律速する上限まで帯域を上げてみて計測をしてみました。5Gは50Mbps、4Gは25Mbpsを設定しています。 計測結果は以下のようになりました。 1回目 2回目 3回目 5G 45.2Mbps 47.0Mbps 47.0 Mbps 4G 17.4Mbps 24.6Mbps 25.0 Mbps L-01Gの上限は30Mbpsと思われるので、4GはWi-Fiの接続で律速されているわけではなさそうです。 TCPの結果 TCPの場合はビットレートの指定をしないと上限まで流そうとするので、ビットレートの指定はしません。 計測結果は以下のようになりました。 1回目 2回目 3回目 5G 15.9Mbps 17.8Mbps 34.4 Mbps 4G 9.33Mbps 11.1Mbps 12.3 Mbps 同じ計測場所③で計測したスピードテストとは差分がありますが、これは計測方法の違いが影響している可能性がありそうです。 iperf3の結果からUDP/TCPともに5Gの方が4Gよりもスループットが高いことが期待出来そうです。 まとめ 今回は「5Gのネットワークを計測してみた」について紹介しました。 今回の計測結果から5GによるRTTやスループットの改善には期待が出来そうです。 3 次回は、12/15 に 「5Gでのトランスポートプロトコルの評価」についてご紹介する予定です。 最後までご覧いただきありがとうございました。 この記事を書いているときに気が付きましたが実際には「5G/4G/3G」モードと「4G/3G」モードを選べるようです。12/15 投稿予定の「5G上でのトランスポートプロトコルの評価」の記事は4Gの計測もSH-052Aを利用して計測した結果を載せたいと思います ↩ モバイルルーターを統一した結果、RTTの値はほぼ同じ値になることを確認しました。詳細は次の記事をご確認ください。: 5Gでのトランスポートプロトコルの評価 - aptpod Tech Blog ↩ 2 と同上 ↩
アバター
aptpod Advent Calendar 2020 の2日目を担当します、研究開発グループの大久保です。 弊社では、新しいプロトコルであるQUICの利用法を調査しています。そこで今回は、RustのQUIC実装の1つである Quinn を用いて、受け取ったリクエストをそのままクライアントへ返送するechoサーバを実装してみます。RustのQUIC実装には、他に quiche というものもありますが、Quinnは tokio の上に実装されているため、Rustのasync機能を活用して楽に書くことができます。 構成 quinn-echo-server と quinn-echo-client という2つのクレートを作り、それぞれの Cargo.toml に以下の依存関係を追記します。 [dependencies] anyhow = "1" clap = "3.0.0-beta.2" futures = "0.3" quinn = "0.6" tokio = { version = "0.2", features = ["full"] } QUICを使うのに quinn 、async周りの機能のために futures と tokio が必要です。また、エラー処理には、最近おなじみになった anyhow を、CLI引数のパースには clap を入れておきます。 サーバ側 quinn-echo-server は以下のようになります。 use anyhow :: * ; use clap :: Clap; use futures :: StreamExt; use quinn :: { Certificate, CertificateChain, Connecting, Endpoint, NewConnection, PrivateKey, ServerConfig, ServerConfigBuilder, TransportConfig, }; use std :: net :: {IpAddr, Ipv4Addr, SocketAddr}; use std :: path :: PathBuf; #[derive(Clap, Debug )] #[clap(version = "0.1.0" )] struct Opts { #[clap(short, long)] port: u16 , #[clap(short, long)] ca: PathBuf, #[clap(long)] privkey: PathBuf, } #[tokio::main] async fn main () -> Result < (), Error > { // コマンドライン引数のパース let opts: Opts = Opts :: parse (); // QUICの設定 let mut transport_config = TransportConfig :: default (); transport_config. stream_window_uni ( 0xFF ); let mut server_config = ServerConfig :: default (); server_config.transport = std :: sync :: Arc :: new (transport_config); let mut server_config = ServerConfigBuilder :: new (server_config); // 証明書の設定 let cert = Certificate :: from_der ( & std :: fs :: read (opts.ca)?)?; server_config. certificate ( CertificateChain :: from_certs ( vec! [cert]), PrivateKey :: from_der ( & std :: fs :: read (opts.privkey)?)?, )?; // QUICを開く let mut endpoint = Endpoint :: builder (); endpoint. listen (server_config. build ()); let addr = SocketAddr :: new ( IpAddr :: V4 ( Ipv4Addr :: new ( 0 , 0 , 0 , 0 )), opts.port); let (endpoint, mut incoming) = endpoint. bind ( & addr)?; println! ( "listeing on {}" , endpoint. local_addr ()?); // クライアントからの接続を扱う while let Some (conn) = incoming. next ().await { tokio :: spawn (async { // クライアントとの処理を行い、エラーが起きたら表示 match handle_connection (conn).await { Ok (_) => (), Err (e) => { eprintln! ( "{}" , e); } } }); } Ok (()) } // echoの処理をする関数 async fn handle_connection (conn: Connecting) -> Result < (), Error > { let NewConnection { connection, mut uni_streams, .. } = conn.await?; println! ( "connected from {}" , connection. remote_address ()); // 受信用のストリームを開く if let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; // ストリームを読み出す let data = uni_stream. read_to_end ( 0xFF ).await?; println! ( "received \" {} \" " , String :: from_utf8_lossy ( & data)); // 送信用のストリームを開く let mut send_stream = connection. open_uni ().await?; // 返信を書き込む send_stream. write ( & data).await?; send_stream. finish ().await?; connection. close ( 0u8 . into (), & []); } else { bail! ( "cannot open uni stream" ); } println! ( "closed" ); Ok (()) } 下準備と接続を開くまで main 関数で行っています。コマンドライン引数で渡されたポート番号と、証明書ファイルのパスを基にサーバを立ち上げ、クライアントとの接続が起きたら handle_connection 関数に渡します。tokioランタイムの上で動作するので、 main 関数には #[tokio::main] 属性を追加しておきます。 handle_connection 関数では、受信用に単方向ストリームを開き、内容を全て読み出します。その後、送信用の単方向ストリームを開き、受け取った内容をそのまま書き出したら接続を終了します。 クライアント側 quinn-echo-client は以下のようになります。 use anyhow :: * ; use clap :: Clap; use futures :: StreamExt; use quinn :: {Certificate, ClientConfigBuilder, Endpoint, NewConnection}; use std :: net :: SocketAddr; use std :: path :: PathBuf; #[derive(Clap, Debug )] #[clap(version = "0.1.0" )] struct Opts { #[clap(short, long)] ipaddr: SocketAddr, #[clap(short, long)] ca: PathBuf, } #[tokio::main] async fn main () -> Result < (), Error > { // コマンドライン引数のパース let opts: Opts = Opts :: parse (); // QUICの設定 let mut client_config = ClientConfigBuilder :: default (); client_config. add_certificate_authority ( Certificate :: from_der ( & std :: fs :: read ( & opts.ca)?)?)?; let mut endpoint_builder = Endpoint :: builder (); endpoint_builder. default_client_config (client_config. build ()); let (endpoint, _incoming) = endpoint_builder. bind ( & "0.0.0.0:0" . parse (). unwrap ())?; // サーバへ接続 let NewConnection { connection, mut uni_streams, .. } = endpoint. connect ( & opts.ipaddr, "localhost" )?.await?; println! ( "connected: addr={}" , connection. remote_address ()); // メッセージの書き込み let msg = "hello" ; let mut send_stream = connection. open_uni ().await?; send_stream. write (msg. as_bytes ()).await?; send_stream. finish ().await?; println! ( "sent \" {} \" " , msg); // 返信の読み込み if let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; let data = uni_stream. read_to_end ( 0xFF ).await?; println! ( "received \" {} \" " , String :: from_utf8_lossy ( & data)); } else { bail! ( "cannot open uni stream" ); } // 終了 endpoint. wait_idle ().await; Ok (()) } main 関数で接続、書き込み、返信の読み込みまで行います。QUICの接続を確立したら、送信用の単方向ストリームを開き、文字列 "hello" を書き込みます。その後、受信用の単方向ストリームを開き、文字列に変換して表示したら終了します。 実行 適当なDER形式の証明書ファイル cert.der と秘密鍵 priv.der を用意した場合、サーバを次のように立ち上げます。 quinn-echo-server --port 33333 --ca cert.der --privkey priv.der このサーバに接続するクライアントは、次のように実行します。 quinn-echo-client --ipaddr 127 . 0 . 0 .1:33333 --ca cert.der すると次のような出力が得られます。 サーバ側 listeing on 0.0.0.0:33333 connected from 127.0.0.1:32820 received "hello" closed クライアント側 connected: addr=127.0.0.1:33333 sent "hello" received "hello" これでechoが返ってくることを確認できました。 最後に 今回は、tokioをベースとして構築されたQuinnをつかって、簡単なQUICのechoサーバを構築してみました。別のQUIC実装であるquicheの方は、自分でイベントを扱うループを書く必要があったりしますが、Quinnの方はRustのasync/await機能の基本が分かっていれば、比較的簡単に使いこなすことができます。コネクションやストリームを開いたり、ストリームの読み書きも、とてもRustらしい書き方で行えます。今回実装したのはechoサーバですが、これでQUICの特徴の1つである ストリームの使い方がわかるので、ここから応用することもできるでしょう。 新しいプロトコルであるQUICも、Rustの知識がある程度あればQuinnで簡単に使えるので、ぜひお試し下さい。
アバター
研究開発グループのエンジニアの酒井 ( @neko_suki )です。 aptpod Advent Calendar 2020 の1日目を担当します。 今回の記事では、ROSに対応している3つのオープンソースの自動運転シミュレータと弊社製品の intdash を連携させて、自動運転シミュレータの画像やセンサー情報をウェブブラウザ上で可視化してみました。 自動運転シミュレータは、「 LGSVL 」、「 CARLA 」、「 AirSim 」の3つを試してみました。 まずはそれぞれのシミュレータとintdashを連携させた動画をご覧ください。 動画では左に自動運転シミュレータ、右側にウェブブラウザ上で可視化を行う弊社製品の「 Visual M2M Data Visualizer 」を配置しています。 intdash LGSVL連携 youtu.be intdash CARLA連携 youtu.be intdash AirSim連携 youtu.be 動画上では撮影のために自動運転シミュレータとウェブブラウザを単一のPCで実行しています。 実際には、以下の図のように可視化しているセンサー情報や画像はクラウドを経由してウェブブラウザに届いています。 自動運転シミュレータ上の映像とウェブブラウザ上の映像を比較すると、クラウドを経由していますが大きな遅延がないことが確認できると思います。 このように、弊社の製品群を活用することで、ROSに対応した様々な自動運転シミュレータとの連携が実現できます。 ちなみに、今回は可視化を行っていますが、intdashを使うとクラウドにデータを保存することも可能です。保存されたデータをダウンロードして解析するなどの活用も可能です。 ここからはintdashとそれぞれの自動運転シミュレータをどのように連携させたかについて触れたいと思います。 intdashとROSの連携 LGSVLとROSの連携 CARLAとの連携 AirSimとの接続 まとめ intdashとROSの連携 最初に、3つの自動運転シミュレータに共通している部分について説明します。 ROSとintdashを連携させてウェブブラウザ上で可視化を行うまでは3つの自動運転シミュレータに共通しています。 それぞれの自動運転シミュレータ *1 がROSトピックをpublishします。 ここで、発行されたROSトピックのうち画像以外のセンサー情報はintdash bridge という弊社製品のROSノードが、画像はConverter *2 というROSノードがsubscribeします。 ROSトピックをウェブブラウザで可視化するためにintdash bridgeはROSトピックをJSON *3 に、ConverterはROSトピックから画像のバイナリのみを取り出します。 JSONとバイナリのデータはintdash Edge Agentに渡されます。そしてintdash Edge Agent がクラウドにデータをアップロードします。 ウェブブラウザ上のVisual M2M Data Visualizerは、クラウドのintdash ServerからJSONと画像のバイナリを受け取り、可視化します。 このように、intdashを使用して、自動運転シミュレータのセンサー情報や画像をウェブブラウザ上で可視化しています。 LGSVLとROSの連携 LGSVLはLGエレクトロニクス社が開発しているUnityベースの自動運転シミュレータで、自動運転ソフトウェアのAutowareと連携できるシミュレータとしても知られています。 LGSVLをROSと連携させるためには、 rosbridge server が必要になります。 intdashとLGSVLを連携させたときの構成図は以下のようになります。 intdashとLGSVLの連携 LGSVLは内部にrosbridge serverと接続するためのrosbridge clientを内包しています。 rosbridge clientにrosbridge server と接続するための設定をしてからLGSVLを起動するとrosbridge serverに接続します。 rosbridge clientは接続後に、JSON形式の情報をrosbridge serverに渡します。 rosbridge serverはrosbridge clientから受け取ったJSONをROSトピックに変換してpublishします。 それらのROSトピックを、intdash bridgeとConverterがsubscribeします。 この後の流れは先ほど説明した通りです。 このようにすると、動画でお見せしたようにintdashとLGSVLの連携が可能になります。 CARLAとの連携 CALRAはUnreal Engineベースの自動運転シミュレータで、intel社やToyota Research Instituteなどがスポンサーを行っています。 intdashとCARLAを連携させたときの構成図は以下のようになります。 intdashとCARLAの連携 CARLAはROSと連携するために carla_ros_bridge というROSノードを提供しています。 このcarla_ros_bridgeがCARLAからセンサー情報と画像を取得しROSトピックとしてpublishします。 それらのROSトピックを、intdash bridgeとConverterがsubscribeします。 この後の流れは先ほど説明した通りです。 このようにすると、動画でお見せしたようにintdashとCARLAの連携が可能になります。 AirSimとの接続 AirSimはUnrealEngineベースのドローンや車の自動運転シミュレータで、Microsoft社が開発しています。 intdashとAirSimを連携させたときの構成図は以下のようになります。 intdashとAirSimの連携 AirSimはROSと連携するために、 airsim_ros_pkgs を提供しています。 airsim_ros_pkgsに含まれる airsim_node というROSノードをROSとの連携のために使います。 airsim_ros_nodeは、AirSimのシミュレータからセンサー情報と画像を取得しROSトピックとしてpublishします。 それらのROSトピックを、intdash bridgeとConverterがsubscribeします。 この後の流れは先ほど説明した通りです。 このようにすると、動画でお見せしたようにintdashとAirSimの連携が可能になります。 まとめ 今回は「intdashと自動運転シミュレータを連携させてみた」について紹介しました。 このように、弊社の製品群を活用することで、ROSに対応した3つのオープンソースの自動運転シミュレータとの連携が簡単に実現できました。 それぞれのシミュレータとの連携で見ていただいたように、弊社製品部分の構成は変えずにそのまま使用しています *4 。 研究開発グループではROSに限らず、プロトコルや機械学習に関連したテーマなど様々な技術テーマの調査・検証を進めています。今後も継続的に調査・検証の結果を記事として投稿できればと思います。 最後までご覧いただきありがとうございました。 *1 : 実際には後述する通り自動運転シミュレータの情報をpublishするROSのノードがいます。 *2 : 製品ではないですが自作のROSノードです *3 : ROSトピックをバイナリのまま送信することも可能です。現状ではウェブブラウザ上での可視化にはJSONが使われているのでJSONに変換しています。 *4 : subscibeするROSトピック名などの設定ファイルは変更しています
アバター
弊社では最近、PDF形式のドキュメントを作成するために Sphinx を使っています。 1 Sphinxを使って、reStructuredTextからPDFを作成する 例えば、弊社では先日AWS Marketplaceにて intdash LE All-in-One をリリースしましたが、そのドキュメントはSphinxで作成しました。 intdash概要 AMIを使ったintdashサーバー構築手順 Sphinxは非常に使いやすいツールですが、本格的に使うようになってから気づいた、使いこなしのコツやノウハウがいくつかあります。 その1つとして今回は、表紙タイトルの折り返しについてご紹介したいと思います。テクニカルライターの篠崎がお届けします。 長い文書タイトルで発生する折り返し 表紙テンプレートをカスタマイズする (準備)表紙のテンプレートを確認する 表紙テンプレートを再定義し、改行コマンドの入ったテキストを挿入する まとめ―Sphinxの柔軟性 長い文書タイトルで発生する折り返し 文書のレイアウトを自動化するとき、表紙も自動で作るか、または、別途手作業で作った表紙をあとで結合するか(例えばAdobe Illustratorで1枚のグラフィックとして作るというのがよくある手法でしょうか)は、判断の分かれるところだと思います。 【後日付記】 この記事の内容は、PDFの表紙のカスタマイズの方法としては有効ですが、表紙タイトルの改行位置を調整するだけであれば、Sphinxプロジェクトの設定ファイル conf.py で latex_documents を設定するほうが簡単です。 latex_documents による設定例: latex_documents = [('index', 'test.tex', r'長い\\タイトルは\\改行できます', author, 'manual')] 設定 latex_documents には、タプルのリストを指定します。タプル内の3番目(インデックス2)の要素で、PDF出力時の表紙タイトルを設定することができます。文字列はそのままLaTeXソースに書き込まれますので、上の例のように \\ を入れると改行することができます。 詳細については、公式ドキュメントの latex_documents についての説明 をご覧ください。 レイアウトを自動化するからには、表紙も自動で作成したいところです。一方、表紙は読者の目に最初に触れるものですから、できるだけ格好よくしたいと考えます。 そこで、バランスのよいレイアウトのテンプレートを作っておいて、決められた位置にテキストや画像を当てはめるのが定番だと思います。 表紙レイアウトのテンプレート例 それでも、大きな文字で長いタイトルを入れると、行末で折り返しが発生し、見栄えが悪くなることがあります。 英語であれば基本的に単語の区切り(スペース)でしか折り返されませんが、日本語では区切りのスペースを入れませんので、良いところで折り返されるとは限りません。 例として、長いタイトルを持つ文書をSphinxで作ってみます。Sphinxでは、プロジェクト設定ファイル conf.py の変数 project の値がPDFの表紙タイトルになります。以下のようにしてみました。 # 最小限の設定をしたconf.py project = 'とても長いタイトルを持ち秋の夜に世界の片隅で作成されたPDF出力テストのためのドキュメント' author = 'test-author' language = 'ja' これでPDFを生成すると以下のようになりました。 行末で折り返された長いタイトル あまり格好よくないですね。 Sphinxを使い始めたころは、PDF出力後に編集ツールを使って手作業で改行を入れていたのですが、毎回このような修正をするのは大変です。 Acrobatで手動で改行を入れる やはり、自動でレイアウトしているのだから、できるだけ自動化したいですよね。 そこで以下のような方法を採ることにしました。方法はいろいろあると思いますので、一例としてお読みください。 使用した環境は以下の通りです。 Windows 10 Sphinx v3.3.1 TeX Live 2020(pLaTeXとdvipdfmxを使用) 表紙テンプレートをカスタマイズする 適切なレイアウトを実現するため、表紙テンプレートを新しく作ります。そこに、折り返し位置を指定したテキストを埋め込むことにします。 2 なお「表紙テンプレート」と呼んでいるものは、実体はLaTeXコマンドです。 以下の(A)~(C)を行います。 タイトル内の適切な折り返し位置にLaTeXの改行コマンド \\ を入れておく(A) 表紙テンプレートを再定義する(B) 表紙テンプレート(B)には、改行コマンドが入ったタイトル(A)を挿入する(C) (準備)表紙のテンプレートを確認する 先まわりして、 (B) の表紙テンプレートから考えてみます。 SphinxでPDFを出力する際に使用されるデフォルトの表紙テンプレートは、 \sphinxmaketitle というLaTeXコマンドです。 Sphinxソースの中の sphinx/texinputs/sphinxmanual.cls にあります。 \newcommand { \sphinxmaketitle }{ % \let\sphinxrestorepageanchorsetting\relax \ifHy @pageanchor \def\sphinxrestorepageanchorsetting { \Hy @pageanchortrue } \fi \hypersetup { pageanchor = false } % avoid duplicate destination warnings \begin { titlepage } % \let \footnotesize\small \let\footnoterule\relax \noindent\rule { \textwidth }{ 1pt } \par \begingroup % for PDF information dictionary \def\endgraf { } \def\and {\& } % \pdfstringdefDisableCommands { \def \\{ , }} % overwrite hyperref setup \hypersetup { pdfauthor = { \@author } , pdftitle = { \@title }} % \endgroup \begin { flushright } % \sphinxlogo \py @HeaderFamily { \Huge \@title \par } % <--- ここで、タイトル\@titleが出力されています ... \end { flushright } %\par ... \end { titlepage } % ... } タイトルの文字列は最終的に \@title コマンドにバインドされ、このテンプレートを使って出力されることが分かります。 表紙テンプレートを再定義し、改行コマンドの入ったテキストを挿入する そこで、デフォルトのテンプレートに少しだけ変更を加えて、改行コマンドの入ったタイトルを出力できるようにします。 Sphinxのたくさんのパラメーターの1つ latex_elements で preamble に文字列を設定すると、その文字列はLaTeXファイルのプリアンブルに書き出されます。この仕組みを利用して、テンプレートを再定義( \renewcommand )します (B) 。詳細についてはこの下のコード例をご覧ください。 また、タイトルには折り返し位置を指定するための改行コマンドを入れます (A) 。設定ファイル conf.py はPythonコードであるため、設定ファイル内で文字列操作も行うことができます。これを表紙テンプレートに挿入します (C) 。 # タイトルの折り返し位置を指定したconf.py author = 'test-author' # 変更なし language = 'ja' # 変更なし # タイトルは分割された状態で用意 document_title_lines = [ 'とても長いタイトルを持ち' , '秋の夜に世界の片隅で作成された' , 'PDF出力テストのためのドキュメント' ] # 改行コマンド `\\` を挟んで連結する(A) # バックスラッシュが4つなのは、エスケープのため my_latex_title_lines = ' \\\\ ' .join(document_title_lines) # => とても長いタイトルを持ち\\秋の夜に世界の片隅で作成された\\PDF出力テストのためのドキュメント # LaTeX出力の設定 latex_elements = { 'preamble' : r''' % my_latex_title_linesをLaTeXの世界に持ち込む \newcommand{\mylatextitlelines}{''' + my_latex_title_lines + r'''} % 表紙テンプレート内でアットマークが使われているため、アットマークを通常の文字として扱う \makeatletter % 表紙テンプレートを再定義(B) \renewcommand{\sphinxmaketitle}{% \let\sphinxrestorepageanchorsetting\relax \ifHy@pageanchor\def\sphinxrestorepageanchorsetting{\Hy@pageanchortrue}\fi \hypersetup{pageanchor=false}% avoid duplicate destination warnings \begin{titlepage}% \let\footnotesize\small \let\footnoterule\relax \noindent\rule{\textwidth}{1pt}\par \begingroup % for PDF information dictionary \def\endgraf{ }\def\and{\& }% \pdfstringdefDisableCommands{\def\\{, }}% overwrite hyperref setup \hypersetup{pdfauthor={\@author}, pdftitle={\@title}}% \endgroup \begin{flushright}% \sphinxlogo \py@HeaderFamily {\Huge \mylatextitlelines \par} % <--- ここで\mylatextitlelinesを使用(C) {\itshape\LARGE \py@release\releaseinfo \par} \vfill {\LARGE \begin{tabular}[t]{c} \@author \end{tabular}\kern-\tabcolsep \par} \vfill\vfill {\large \@date \par \vfill \py@authoraddress \par }% \end{flushright}%\par \@thanks \end{titlepage}% \setcounter{footnote}{0}% \let\thanks\relax\let\maketitle\relax %\gdef\@thanks{}\gdef\@author{}\gdef\@title{} \clearpage \ifdefined\sphinxbackoftitlepage\sphinxbackoftitlepage\fi \if@openright\cleardoublepage\else\clearpage\fi \sphinxrestorepageanchorsetting } % 表紙スタイル終わり % アットマークを特殊文字に戻す \makeatother ''' } # 変数`project`は、各行をそのまま連結したもの # PDFのメタ情報には正しいタイトルが入る project = '' .join(document_title_lines) # => とても長いタイトルを持ち秋の夜に世界の片隅で作成されたPDF出力テストのためのドキュメント これで、PDFを出力してみます。 指定どおりに折り返されたタイトル 指定した位置で折り返されています。これで、PDF生成のたびに手作業で折り返し位置を修正する必要はなくなりました。 3 新しい表紙テンプレート \sphinxmaketitle は、他のドキュメントにも使いまわしができます。 この説明ではテンプレートを conf.py に直接書き込みましたが、LaTeXのスタイルファイル(.sty)として分離することで、より使いまわしを楽にすることも可能です。 まとめ―Sphinxの柔軟性 Sphinxは非常に柔軟にカスタマイズできるツールです。柔軟性を高めている特徴として、以下があります: たくさんの パラメーター が変更可能であること(しかも、痒い所に手が届くパラメーターが存在すること) レイアウトのテンプレートがモジュール化されており、それぞれ再定義可能であること 設定ファイル conf.py がPythonコードであり、ドキュメント生成時に評価されるものであること 今回の例ではこれらの特徴を活用し、パラメーター内で新たにテンプレートを定義しました。また、改行コマンドを挿入するという単純な操作ではありますが、PythonでLaTeXコードの断片を生成しました。Sphinxの持つ柔軟性の一端をご紹介できたかと思います。 今後もうまく使いこなしたいと考えています。 Sphinxを採用した背景や、LuaLaTeXによる日本語PDFの作成方法については、以前のエントリー SphinxとLuaLaTeXで、日本語PDFマニュアルを作る - aptpod Tech Blog にて公開しています。 ↩ 折り返し位置を指定するために、変数 project にLaTeXの改行コマンド \\ を挿入し、 project = 'とても長いタイトルを持ち\\\\秋の夜に世界の片隅で作成された\\\\PDF出力テストのためのドキュメント' のようにしてもうまくいきません。Sphinxの処理のなかでバックスラッシュがエスケープされるためです。 ↩ この新しい表紙テンプレートのコードは、 { 、 } 、 $ 、 % のようなLaTeXの特殊文字を最初から含むタイトルには対応していません。 ↩
アバター
はじめに こんにちは、SRE チームの柏崎です。 先日、 intdash が AWS Marketplace にて提供開始 されました。 これを期に、サーバサイドアプリをパッケージングするために、 fpm というツールを使う機会がありました。 すっかりコンテナな世の中には地味めな話題ですが、今回は fpm について、rpm パッケージの作成例を交えながら紹介したいと思います。 経緯 弊社では、intdash を組み合わせた PoC プロジェクトが多くあり、製品である intdash 自体のカスタマイズがよく行われています。 その昔、intdash のサーバサイドアプリはモノリシックアーキテクチャだったため、カスタマイズのために製品本体に手を入れなければならず、派生製品が乱立していました。 故にデプロイにおいては、派生製品ごとの実行バイナリを管理する手間を減らすため、「都度サーバ上でビルドを行う」という、ちょっと斜め上の方式をとっていました。せっかく Go で作っているのにもったいないですね…。 現在ではマイクロサービスアーキテクチャへの移行が進み、製品に手を入れずに柔軟なカスタマイズが可能な構成になっていますが、デプロイは従来の方式を踏襲してしまっており、下記のようなたくさんの課題を抱えています。 Ansible タスクでビルドしているので、冪等性があいまい Ansible の実行が長時間になりがち サーバがそれぞれビルドのためのソースを持つので、ディスクスペースが無駄 お客様管理のサーバへのデプロイも、アプトポッドエンジニアがやらなければならない そんなとき intdash の AMI 公開の話が持ち上がり、「こんなダサい方式でデプロイされた AMI を晒すのは恥ずかしい」と重い腰を上げたのでした。古いバージョンの利用者にアップデート手段を提供しないといけないですしね。 fpm とは? 目的 Linux (に限らずですが) のほとんどのディストリビューションはパッケージ管理システムを利用しています。 RHEL 系で rpm 形式を扱う yum、Debian 系で deb 形式を扱う apt がメジャーですね。 パッケージ管理システムによって、利用者はソフトウェア一式の追加削除や、依存関係の解決などを手軽に行うことができます。 便利なパッケージ管理システムですが、パッケージの作成者にとってはちょっと大変です。 様々な形式に対応するために、パッケージ作成手順を形式ごとに学習していかなければなりません。 rpm ひとつとっても、spec ファイルの複雑な書式や rpmbuild コマンドの使い方など、覚える事が多いです。 fpm は、覚えなければならない事を極力省き、シンプルに様々な形式のパッケージを作ることを目的として作られています。 どうやって動くのか fpm は、様々な形式から入力と出力を指定すると、その間の変換を行ってくれます。 README にも書いてありますが、様々な入力形式・出力形式に対応しています。 npm モジュールから rpm を作ったり、deb から rpm を作ったり、色々な用途が思いつきますね。 使いかた それでは、実際にパッケージの作成を行いながら、使い方を見ていきましょう。 入力形式として dir を、出力形式として rpm を使い、intdash の認証認可を担当するマイクロサービス「auth」の rpm パッケージを作ります。 構成ファイルの準備 入力形式 dir は、ディレクトリ配下に配置されたファイル一式をパッケージの構成ファイルとして扱ってくれます。 必要なファイルを用意し、ディレクトリ buildroot 配下に配置します。 buildroot/ usr/ bin/ authd # 実行バイナリ lib/ systemd/ system/ intdash-service-auth.service # systemd ユニットファイル share/ doc/ intdash-service-auth-1.6.0/ copyright # コピーライトファイルなど ... etc/ intdash/ authd.conf # 設定ファイル また、出力形式 rpm は、ヘルパユーティリティとしてインストール時やアンインストール時に実行されるスクリプトを指定できます。 これらをディレクトリ rpm_helper 配下に配置します。 rpm_helper/ pre.sh # インストール前に実行される post.sh # インストール後に実行される preun.sh # アンインストール前に実行される postun.sh # アンインストール後に実行される 例として、よくある rpm のヘルパユーティリティを以下に書いておきます。 サービスの動作に必要なユーザの作成や systemd 関連の操作などを行っています。 ### rpm_helper/pre.sh getent group intdash >/dev/null 2>&1 || \ groupadd -r intdash getent passwd intdash >/dev/null 2>&1 || \ useradd -r -g intdash -d /var/lib/intdash -s /sbin/nologin intdash exit 0 ### rpm_helper/post.sh if [ $1 -eq 1 ]; then systemctl daemon-reload >/dev/null 2>&1 ||: fi ### rpm_helper/preun.sh if [ $1 -eq 0 ]; then systemctl --no-reload disable intdash-service-auth.service >/dev/null 2>&1 ||: systemctl stop intdash-service-auth.service >/dev/null 2>&1 ||: fi ### rpm_helper/postun.sh if [ $1 -eq 0 ]; then systemctl daemon-reload >/dev/null 2>&1 ||: fi 準備はこれだけです。 スクリプト周りで rpm のちょっとした知識は必要ですが、簡単ですね! パッケージング それでは、いざパッケージングをしていきましょう。 コマンド一発です。 $ fpm \ --output-type rpm \ --input-type dir \ --chdir ./buildroot \ --name intdash-service-auth \ --version 1.6.0 \ --iteration 1 \ --architecture x86_64 \ --license Unspecified \ --maintainer product-support@aptpod.co.jp \ --vendor "aptpod, Inc." \ --url https://www.aptpod.co.jp/ \ --rpm-summary "intdash Auth Service" \ --description "This package contains the intdash Auth Service." \ --rpm-os linux \ --depends shadow-utils \ --depends systemd \ --before-install ./rpm_helper/pre.sh \ --after-install ./rpm_helper/post.sh \ --before-remove ./rpm_helper/preun.sh \ --after-remove ./rpm_helper/postun.sh \ --directories /etc/intdash \ --directories /usr/share/doc/intdash-service-auth-1.6.0 \ --config-files /etc/intdash/authd.conf {:timestamp=>"2020-11-05T08:27:27.428159+0000", :message=>"Created package", :path=>"intdash-service-auth-1.6.0-1.x86_64.rpm"} ポイントをいくつか書いておきます。 アンインストール時にディレクトリが残ってしまわないように、 --directories オプションを忘れないようにしましょう。 --config-files を使って特定のファイルが設定ファイルであることを明示しておくと、編集済みの設定ファイルがパッケージアップデート時に上書きされたり、アンインストール時に削除されたりするのを防ぐことができます。 --iteration では、 1.6.0-1 の 1 ようなバージョンの後ろに付く文字列を指定できます。 パッケージ自体の更新で 2 3 と増やしたり、プレリリースバージョンにて 0.1.rc1 0.2.rc2 のように付与することで、パッケージ管理システムにより適切に新旧バージョンの比較が行われます。 以上で、rpm が出来上がりました。 お手軽ですね! おわりに fpm について、実例を交えながら紹介しました。 AMI は無事、恥ずかしくない状態で公開することができました。 引き続き、社内へのデプロイへの適用など、質の高い構築運用に活かしていくつもりです。 ここでは rpm の作成例を紹介しましたが、ほとんどの内容は deb など他の形式にも使い回せます。 パッケージングでお悩みの方の参考になれば幸いです。
アバター
はじめに ハードウェアグループのおおひらです。 所謂コロナ禍と呼ばれる状況になってはや半年が過ぎましたが、ハードウェア設計に携わっている皆様はいかがお過ごしでしょうか。 弊社では今年の2月中旬から原則リモートワークの勤務態勢になり、緊急事態宣言が終了したあともオフィスの出社人数を制限するよう3密を回避する対策が続けられています。ハードウェアグループのメンバーも自宅と会社での作業のバランスをとりつつ業務にあたっています。 私自身、各種計測器や試作基板を自宅に持ち帰ったり、前々から個人で購入しようとしていた機材を揃えたりして自宅の作業環境を充実させております。最近では協力会社や商社の方々との打合せがWeb会議で行われることも多く、意図的に自室の作業デスクがカメラに写り込むようにして会話ネタの提供に一役買ったり…😊 さて、与太話はこれぐらいにして、本記事ではハードウェア製品を商品化するうえで地味で大変だけれど避けては通れない、 部品表の管理 について書きたいと思います。 はじめに 部品表の管理は大変という話 OpenBOMの紹介 できること ライセンス形態 使ってみた おわりに 部品表の管理は大変という話 部品表 : BOM (Bills of Materials) という言葉は、製造業に従事されている方には馴染み深いと思います。ハードウェア製品を構成する部品の一覧のことで、各部品の属性として例えば下記のようなものが挙げられます。 品名 各部品の部品番号 (型番/型式) メーカー名 メーカー型番 リファレンス番号 単価 数量 (員数) 環境規制情報 (RoHS) 図面の情報・リンク etc... この部品表を元に調達・発注作業をしたり、また、各種製品コンプライアンスを遵守するために部品変更に対するプロセスを定めて情報追跡を行ったりします。 例えば米国仕向けのためにFCCの認証を取ったり、欧州仕向けのためにCEマーキングの自己宣言をしたり、更には車載機器としての欧州認証であるECE Regulation No.10 (通称Eマーク)の認証を取ったりと、各国と地域で適切な法規制を遵守する必要がありますよね。(弊社でも絶賛試験中です) 製品の出荷時点でこのような認証を受けることができたとしても、その後の部品の生産中止や仕様変更によって製品のスペックや構成物質が変わってしまうことは避けられません。こういった品質と製品コンプライアンスの管理・対応のために多くのメーカーでは4M変更プロセスが定められており、その活動の軸になる重要な要素が部品表と言えます。 品質管理における「4M」とは?効果を発揮するための変更管理 | デジタルトランスフォーメーション チャンネル 実際に特定の製品の部品表を作成する際には、そのフォーマットとしてスプレッドシート (エクセル)を使われる方が多いのではないかと思います。電気部品や半導体部品を実装した基板だけの製品を出荷するのであれば回路CADと統合されている専用のBOM管理ツールを使うことも考えられるのですが、多くの製品はメカ部品や副資材、シールや梱包箱、取扱説明書(紙)など、様々なマテリアルから成る部品の階層構造を持ちますので汎用的なフォーマットで作業できたほうが望ましいですよね。 しかしこれには色々と問題があって、 階層構造を作りづらい バージョン管理できない 横展開が面倒 (ある製品の一部品を、他の製品で流用しようとしたときに頑張ってコピペする) などなど、手作業および謎のエクセルマクロを含み、誰も管理できない秘伝のタレみたいなファイルが出来上がる…という未来が容易に想像できます。 前職で謎の社内イントラシステムに苦しめられた経験もあり(←製造業の方なら同意してくれる人も多いはず)、スプレッドシートで管理したくないな…と思っていろいろ調べた結果、OpenBOMというクラウドベースの部品表管理サービスに行き着きました。 OpenBOMの紹介 www.openbom.com ぱっと見て良い感じですね。 できること 上記のページにも書かれていますが、特徴は以下のとおりです。 スプレッドシートのように直感的に作業できる 画像や3D CAD(Autodesk, Solidworks)のデータを取り込んで各部品をビジュアルで見ることができる 複数人で同時にBOM編集できる ERPシステム(Netsuite)などとの統合 リビジョン管理できる 在庫管理できる クラウドサービスとしての基本的な機能はもちろん、在庫管理や経費との連携もできるということです。 ライセンス形態 www.openbom.com 個人ユーザーであれば無償、もしくは月25ドルのプロフェッショナルユーザーのライセンスがあります。違いは階層ごとのコスト計算機能の有無や、ベンダー管理機能の有無といったところでしょうか。 チームであれば月125ドルから。 企業ライセンスは月375ドルで、部品番号を割り振ったり、4M変更に対応しようとするとこのライセンス契約が妥当かなと思います。 www.openbom.com 使ってみた 企業ライセンスで14日間の無償トライアルをしてみました。 基本的にはカタログを作成して、そのカタログをもとに各製品単位のBOMを作っていく仕組みになっています。カタログというのは、例えば電気部品だったり、半導体部品だったり、または機構部品(板金)、機構部品(ネジ)といった区分の単位で整理される部品のリストのことです。これをベースに各製品のBOMを組立てていくことで、各部品の再利用がしやすくなります。 カタログとBOMの関係 個人的にGood!と感じたところは以下のとおりです。 電気CAD等、外部のCADの部品表からインポート可能 企業ライセンスだと各部品に自動的にユニークな部品番号を振ってくれる機能がある 部品が画像として可視化されていて直感的 部品の属性としてベンダーを紐付けられ、購買・発注時に複数のベンダーを比較することができる BOMの階層ごとにコスト算出できる 在庫管理ができ、試作支給部品や量産部品の管理が楽。営業・管理側と開発側との連携のハードルが下がる。 月375ドルでこれであれば使いたい…ということでこれから稟議申請書を書こうと思います。 おわりに クラウドベースのBOM管理ツール:OpenBOMを紹介させていただきました。 弊社のように小規模な組織では専任の調達/購買メンバーがいないこともあると思います。我々エンジニアとしては、OpenBOMのようなツールを効果的に使って業務効率化を進めていけると本来やりたい開発・設計業務に集中できて幸せになれるのではないでしょうか。 ※ ちなみに無償トライアルに申し込むとCEOのOlegさんから営業メールがバンバン飛んできます^^; 営業熱心でスゴイ。。。
アバター
エンベデッドチーム 久保田です。 開発環境をWSL2 (Windows Subsystem for Linux)へ移行しました。 タイミングよく、「WSL2でUSBデバイスを使ってみよう」という記事が出回っていたので、 aptpod CAN-USB Interface (AP-CT2A) もWSL2で動かせるのではないかという期待から intdash Edge Agent 含めて動作環境を整えてみましたのでご紹介します。 元記事 Adding USB support to WSL2 https://github.com/rpasek/usbip-wsl2-instructions USB support to WSL2 http://ktkr3d.github.io/2020/07/06/USB-support-to-WSL2/ intdash Edge Agent とは intdash Edge Agentは、intdash Edgeの基本機能を提供するエッジデバイス用のエージェントソフトウェアです。ユーザーは、接続するデバイスに応じたプラグインであるDevice Connectorを実装するだけで、様々なデバイスをintdashに接続することができます。このソフトウェアを使用すれば、intdash Edge が提供する自動再送、データ流量の制御といった、エッジ側の基本機能を利用したクライアントアプリケーションを、最小限の追加実装で開発することが可能となります。 aptpod CAN-USB Interface - AP-CT2A とは CANバスをターミナルアプライアンスに接続し、CAN、J1939などの制御信号を取得するためのインターフェイスデバイス 分岐線を使用して同時に2チャンネルのCANバスに接続可能 CANデータに対する高精度なハードウェアタイムスタンピングが可能 クロックの共有機構により、複数のCAN-USB Interface間のタイムスタンプを同期 外部計測装置などへの同期信号の出力が可能 動作構成 動作構成 WSL2 (Ubuntu 20.04)には、intdash Edge Agent, AP-CT2A device driverをbuild, installします。 WSL2は現時点(2020/10)で、残念ながらUSBデバイスの直接接続をサポートしていませんので、USB over IP経由でWindows側のAP-CT2Aを接続してみます。 WSL2とは WSL2は、Linux 用 Windows サブシステムが Windows 上で Linux バイナリを実行できるようにしたLinux 用 Windows サブシステム アーキテクチャの新しいバージョンです。 WSL1との違いは、ファイル システムのパフォーマンスを向上すること、システム コールの完全な互換性を追加することです。 WSL2 の初期リリースでは、ハードウェア アクセスのサポートが制限され、GPU、シリアル、USB デバイスに直接アクセスすることはできません。 図. WSL1 と WSL2 の命令実行の比較 「WSLのアーキテクチャ https://roy-n-roy.github.io/Windows/WSL%EF%BC%86%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A/Architecture/ 」から USB over IPとは USB over IPは、USBコマンドをIPパケットによってカプセル化するデバイス制御方法です。 USB/IP Design 「USB/IP PROJECT http://usbip.sourceforge.net/ 」から 手順 Windows側 USB/IPのstubドライバを配布サイト ( https://github.com/cezanne/usbip-win ) からダウンロードします。 https://github.com/cezanne/usbip-win/releases/tag/v0.1.0 PS C:\usbip-win\0.1.0> dir ディレクトリ: C:\usbip-win\0.1.0 Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2020/07/11 14:05 551472 usb.ids -a---- 2020/07/11 14:05 1434112 usbip.exe -a---- 2020/07/11 14:07 1364992 usbipd.exe -a---- 2020/07/11 14:05 762 usbip_stub.cer -a---- 2020/07/11 14:05 1951 usbip_stub.inx -a---- 2020/07/11 14:07 58384 usbip_stub.sys -a---- 2020/07/11 14:05 2486 usbip_test.pfx -a---- 2020/07/11 14:05 2316 usbip_vhci.cat -a---- 2020/07/11 14:05 762 usbip_vhci.cer -a---- 2020/07/11 14:05 1960 usbip_vhci.inf -a---- 2020/07/11 14:05 87344 usbip_vhci.sys バイナリをダウンロードしたディレクトリでstubドライバをインストールします。 PS C:\usbip-win\0.1.0> .\usbip.exe install AP-CT2AをWindowsのUSBポートに接続すると、usbipコマンドでUSB接続が表示されます。 PS C:\usbip-win\0.1.0> .\usbip.exe list -l - busid 1-123 (32b2:0100) unknown vendor : unknown product (32b2:0100) <= これ - busid 1-138 (8087:0a2b) Intel Corp. : unknown product (8087:0a2b) - busid 1-99 (093a:2510) Pixart Imaging, Inc. : Optical Mouse (093a:2510) - busid 1-236 (5986:111c) Acer, Inc : unknown product (5986:111c) usbipコマンドでUSB/IPの接続登録を実行します。 PS C:\usbip-win\0.1.0> .\usbip.exe bind -b 1-123 usbip: info: bind_device: bind device on busid 1-123: complete usbipdコマンドでWSL2側からの接続待ちにします。 PS C:\usbip-win\0.1.0> .\usbipd.exe -d -4 usbipd: info: starting usbipd (usbip 1.0.0) usbip: debug: C:\work\usbip-win\userspace\src\usbipd\usbipd_sock.c:38:[build_sockfd] opening 0.0.0.0:3240 usbip: info: listening on 0.0.0.0:3240 WSL2側 Windows側で待ち受けているポートへ接続します。 $ sudo usbip attach --remote=192.168.1.8 --busid=1-123 接続が完了すると、WSL2側にAP-CT2Aが現れます $ lsusb Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 001 Device 002: ID 32b2:0100 aptpod, Inc. AP-CT2A Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 動作結果 動作結果 CANを流してみると、無事受信できました。 USB/IP経由することでCPU負荷が大きくなるかと考えていましたが、PCがCore i7なので余裕でした。 ・・・が、何かのきっかけで受信が0になります。エラーとなるわけではないので、USB/IPドライバの不具合かと。正常に動かすには調整が必要なようです。 まとめ WSL2でのintdash Edge Agentの動作手順を簡単に紹介しました。 結果AP-CT2Aは正常に継続動作しませんでしたが、intdash Edge Agentの動作は問題ありません。 CPUがパワフルなので、AP-CT2AのWindowsネイティブドライバがあれば、CANデータをTCPで転送してDevice Connectorで回収する手段も取れなくはなさそうです。 WSL2のUSBデバイス正式対応に期待しましょう。 参考 (WSL2の導入) WSL2導入|WinアップデートからWSL2を既定にするまでのスクショ https://qiita.com/tomokei5634/items/27504849bb4353d8fef8 WSL2におけるVcXsrvの設定 https://qiita.com/ryoi084/items/0dff11134592d0bb895c WSL2によるホストのメモリ枯渇を防ぐための暫定対処 https://qiita.com/yoichiwo7/items/e3e13b6fe2f32c4c6120
アバター
研究開発グループの大久保です。 当社の製品の中にはC/C++で書かれたものが存在し、その中には独自のバイナリフォーマットを取り扱うものが存在します。既存のコードとやり取りするようなRustのプロジェクトを起こすためには、その独自のバイナリフォーマットをRustで取り扱えるようにしなければなりません。しかしながら、Rustの標準ライブラリの機能だけでは、バイナリの読み書きは意外と面倒になります。そのため、今回はRustでバイナリを扱うのならぜひ知っておきたいクレートを3つご紹介します。 byteorder byteorder はその名の通り、バイトオーダ、つまりエンディアンを扱うためのクレートです。使い方はシンプルで、 ByteOrder トレイトと、 BigEndian, LittleEndian, NativeEndian のうち自分が扱いたいエンディアンをインポートすれば、バッファと数値型の間で読み書きを行うことができます。 例えば、長さ4バイトのバッファから32bit整数型を読み出す場合、次のようになります。 use byteorder :: {BigEndian, LittleEndian, NativeEndian, ByteOrder}; fn main () { let buf = [ 0 , 0 , 0 , 42 ]; let a = LittleEndian :: read_u32 ( & buf); assert_eq! (a, 704643072 ); let a = BigEndian :: read_u32 ( & buf); assert_eq! (a, 42 ); let a = NativeEndian :: read_u32 ( & buf); assert_eq! (a, 704643072 ); } 32bit整数型の書き込みは次のようになります。 use byteorder :: {BigEndian, LittleEndian, NativeEndian, ByteOrder}; fn main () { let mut buf = [ 0 ; 4 ]; LittleEndian :: write_u32 ( &mut buf, 42 ); assert_eq! (buf, [ 42 , 0 , 0 , 0 ]); BigEndian :: write_u32 ( &mut buf, 42 ); assert_eq! (buf, [ 0 , 0 , 0 , 42 ]); NativeEndian :: write_u32 ( &mut buf, 42 ); assert_eq! (buf, [ 42 , 0 , 0 , 0 ]); } NativeEndian はこれを実行しているプラットフォームのエンディアンを示します。 bytes bytes は、Rust用の非同期ライブラリ tokio で使われているバイナリ操作用のクレートです。 Buf と BufMut というトレイトを導入することで、バイナリ読み書きのためのメソッドを利用することができます。 例として、用意したデータの先頭から順に整数を読み出していきます。 use bytes :: Buf; fn main () { let data = [ b'a' , 0 , 33 , 42 , 0 ]; let mut p = & data[..]; assert_eq! (p. get_u8 (), b'a' ); // 0バイト目を8bit整数として読み出し assert_eq! (p. get_u16 (), 33 ); // 1〜2バイト目をビッグエンディアン16bit整数として読み出し assert_eq! (p. get_u16_le (), 42 ); // 3〜4バイト目をリトルエンディアン16bit整数として読み出し } Vec に順番に整数を書き込んでいくこともできます。 use bytes :: BufMut; fn main () { let mut buf = Vec :: new (); buf. put_u8 ( b'r' ); // 8bit整数を書き込み buf. put_u8 ( b'u' ); buf. put_u8 ( b's' ); buf. put_u8 ( b't' ); buf. put_u16 ( 0xFFEE ); // ビッグエンディアンとして16bit整数を書き込み buf. put_u16_le ( 0x1122 ); // リトルエンディアンとして16bit整数を書き込み assert_eq! (buf, [ b'r' , b'u' , b's' , b't' , 0xFF , 0xEE , 0x22 , 0x11 ]); } 読み書きどちらも関数名の後ろに _le を付けるとリトルエンディアン扱いになります。バッファの先頭から読み書きしていくようなデータ構造の場合、bytesはなかなか便利なクレートと言えるでしょう。 nom nom はRust用のパーサコンビネータライブラリです。nomが提供するパース用の関数を組み合わせて、対象となるフォーマット用のパーサを作り上げるようにして使います。nomのexampleに示されているのは、テキスト( &str )のパースですが、バイナリ( &[u8] )のパースにも使えます。 例えば、先頭から順にバイナリを読んでいき、結果を MyData 構造体に格納していく場合は次のようになります。 use nom :: IResult; use nom :: number :: complete :: {be_u8, be_u32}; /// 読み込みたいデータ #[derive( PartialEq , Debug )] struct MyData { a: u8 , b: u8 , c: u32 , d: u32 , } /// バイナリをパースしてMyDataを読み込む関数 fn parse_mydata (input: & [ u8 ]) -> IResult < & [ u8 ], MyData > { let (input, a) = be_u8 (input)?; let (input, b) = be_u8 (input)?; let (input, c) = be_u32 (input)?; let (input, d) = be_u32 (input)?; Ok ((input, MyData { a, b, c, d })) } fn main () { let data = [ 10 , 20 , 0 , 0 , 0 , 30 , 0 , 0 , 0 , 40 ]; let mydata = parse_mydata ( & data). unwrap (). 1 ; assert_eq! ( mydata, MyData { a: 10 , b: 20 , c: 30 , d: 40 , } ); } 基本的にnomにおけるパーサ関数は、パースしたい領域のスライスを受け取り、読み残しのスライスと読み取った結果のタプルを返します。そのため、返り値のスライスを読み取れば、先頭から順に値を読み込んでいくことができます。 先頭から順に読んでいくだけならbytesでも可能ですが、nomの関数を使えば複雑な構造のバイナリを読み取ることも可能です。例えば、先頭に mydata というマジックナンバーがついているバイナリをパースしたい場合は、 tag を使うことができます。 use nom :: bytes :: complete :: tag; use nom :: number :: complete :: {be_u32, be_u8}; use nom :: IResult; /// 読み込みたいデータ #[derive( PartialEq , Debug )] struct MyData { a: u8 , b: u8 , c: u32 , d: u32 , } /// バイナリをパースしてMyDataを読み込む関数 fn parse_mydata (input: & [ u8 ]) -> IResult < & [ u8 ], MyData > { let (input, _) = tag ( b"mydata" )(input)?; // inputの先頭6バイトが"mydata"かどうか確かめる let (input, a) = be_u8 (input)?; let (input, b) = be_u8 (input)?; let (input, c) = be_u32 (input)?; let (input, d) = be_u32 (input)?; Ok ((input, MyData { a, b, c, d })) } fn main () { let data = [ b'm' , b'y' , b'd' , b'a' , b't' , b'a' , 10 , 20 , 0 , 0 , 0 , 30 , 0 , 0 , 0 , 40 , ]; let mydata = parse_mydata ( & data). unwrap (). 1 ; assert_eq! ( mydata, MyData { a: 10 , b: 20 , c: 30 , d: 40 , } ); } また、このデータの末尾に、ヌル終端のASCII文字列が格納されていた場合を考えてみます。この場合、 take_until を使うことで、0が現れるまでのスライスを取得することができます。 use nom :: bytes :: complete :: {tag, take_until}; use nom :: number :: complete :: {be_u32, be_u8}; use nom :: IResult; /// 読み込みたいデータ #[derive( PartialEq , Debug )] struct MyData { a: u8 , b: u8 , c: u32 , d: u32 , id: Vec < u8 > , } /// バイナリをパースしてMyDataを読み込む関数 fn parse_mydata (input: & [ u8 ]) -> IResult < & [ u8 ], MyData > { let (input, _) = tag ( b"mydata" )(input)?; let (input, a) = be_u8 (input)?; let (input, b) = be_u8 (input)?; let (input, c) = be_u32 (input)?; let (input, d) = be_u32 (input)?; let (input, id) = take_until ( & b" \0 " [..])(input)?; // ヌル文字が現れるまでのデータを取得 Ok (( input, MyData { a, b, c, d, id: id. to_vec (), }, )) } fn main () { let data = [ b'm' , b'y' , b'd' , b'a' , b't' , b'a' , 10 , 20 , 0 , 0 , 0 , 30 , 0 , 0 , 0 , 40 , b'a' , b'b' , b'c' , b'd' , b'e' , b'f' , 0 , ]; let mydata = parse_mydata ( & data). unwrap (). 1 ; assert_eq! ( mydata, MyData { a: 10 , b: 20 , c: 30 , d: 40 , id: b"abcdef" . to_vec (), } ); } 他にもnomにはいろいろな機能が用意されていますので、うまく使えばもっと多様なフォーマットにも対応できます。 最後に Rustでちょっと凝ったことをすると、標準ライブラリ以外のクレートが必要になることが多く、適したクレートを探すのは少し大変です。そのため、今回はバイナリを扱う場合に必須になりそうなクレートをご紹介しました。Rustはその適用範囲上、バイナリを扱うことも多いかと思いますので、この記事がご参考になれば幸いです。
アバター
はじめに こんにちは、製品開発グループの落合です。主に エッジサイドミドルウェア(intdash Edge) の開発を担当しています。このintdash EdgeはC++で作成しているのですが、言語が何であろうと「面倒な事は自動化したい」ですよね。そして、特に面倒なのは「テスト」じゃないでしょうか? そんな訳で、intdash Edgeのプロジェクトで使用している「CIでの動的テスト」を紹介させて頂こうと思います。「CIでテストなんて当たり前でしょ」と言われる気もしますが、 clang の sanitizer を使っている記事は意外と少ない気がするので今回記事にしてみました。 え、なんで valgrind ではなく sanitizer を使っているかですか?単純に検知できるエラーが多いのが理由です。 はじめに CIで行っているテスト 開発環境の準備 動的テスト Address Sanitizer Undefined Behavior Memory Sanitizer Coverage コーディングスタイルチェック スタイルの規定 フォーマッターの実行 まとめ CIで行っているテスト intdash Edgeのプロジェクトで行っているCIのテストは主に下記3つです。 コーディングスタイルチェック(clang-format) 静的解析(CppCheck) 動的テスト(calng sanitizer)← 今回の記事の主題 それぞれの費用対効果を(完全に主観で)表すと、こんな感じです。 テスト項目 バグの検知 コスト感と効果 スタイルチェック × フォーマッターが自動で行ってくれる。 予めチームでのフォーマットを決めておくことで、レビューなどでの非生産的な論争を避けられる。 静的テスト △~○ テストコードを書かなくても良い。 実装した関数が仕様通りに動くかのチェックはできない。 見つかるバグはツールの性能に大きく左右される。 動的テスト ◎ テストコードが必要。 動かしたコードに対して未初期化・メモリ関連のチェックができる。 継続的にメンテするプロジェクトなら、断然ユニットテストを書いて動的テストまで取り入れるべきだと思います。 なぜなら、ユニットテストをCIに取り入れることで、テスト対象が仕様通りに動作することを保証できるので、簡単にデグレを抑止できます。 さらに、動的テストを足す事で、テストで動かしたコードのメモリエラーをチェックできます。 さらにさらに、カバレッジも出力すればテストケースの考慮漏れも防げます。 では、本題のclangのscanitizerで動的テストを行ってみましょう。。。と行きたいのですが、その前に、clangの入った開発環境をささっとdockerで作りましょう。 開発環境の準備 dockerがインストールされている環境で下記コマンドを実行してください。 $ docker run --rm -it debian:10-slim /bin/bash # apt-get update # apt-get install -y gpg wget # wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor >/usr/share/keyrings/llvm-snapshot.gpg # echo "deb [signed-by=/usr/share/keyrings/llvm-snapshot.gpg] http://apt.llvm.org/buster/ llvm-toolchain-buster-9 main" >> /etc/apt/sources.list # apt-get update # apt-get install -y clang-9 clang-format-9 ささっとできましたね。エディタはお好きなモノを入れてください。 動的テスト intdash Edgeのプロジェクトでは、動的テストに、 clang の sanitizer を使用しています。 valgrindではなくsanitizerを使っている理由は、前述の通り、検知できるエラーが多いからです。ただ、やり方によってはvalgrindでチェックできていた項目(UMR: uninitialized memory reads)がチェックされなくなってしまうので、この点の対応も紹介します(超単純ですが)。 Address Sanitizer では、clangのsanitizerを使ってみましょう。 まずは、エラーの発生するコードを書いてみます( こちらのページのIntroduction に様々なパターンのエラーを発生するコードがあります)。 main.cc #include <stdlib.h> void *p; int main() { p = malloc(7); p = 0; // The memory is leaked here. return 0; } 次に、sanitizerを実行してみます。 # clang-++9 -fsanitize=address -g main.c # ./a.out ================================================================= ==4219==ERROR: LeakSanitizer: detected memory leaks Direct leak of 7 byte(s) in 1 object(s) allocated from: #0 0x4961dd in malloc (/root/a.out+0x4961dd) #1 0x4c58b8 in main /root/main.c:6:7 #2 0x7f44ae33709a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2409a) SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s). リークを検知できましたね。 Undefined Behavior では、次に不定の動作、Undefined Behaviorの検知もしてみましょう。 main.cc #include <stdlib.h> void *p; int main(int argc, char **argv) { p = malloc(7); p = 0; // The memory is leaked here. int k = 0x7fffffff; k += argc; // 2147483647 + 1 = Undefined behavior return 0; } # clang-++9 -fsanitize=undefined,address -g main.c # ./a.out main.cc:9:5: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior main.cc:9:5 in ================================================================= ==4240==ERROR: LeakSanitizer: detected memory leaks Direct leak of 7 byte(s) in 1 object(s) allocated from: #0 0x4961dd in malloc (/root/a.out+0x4961dd) #1 0x4c8227 in main /root/main.cc:6:7 #2 0x7f60b15bb09a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2409a) SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s). Undefined BehaviorとLeakの両方検知できてますね。今回はビルド時のオプション -fsanitize に undefined が足されている点に注意してください。 Memory Sanitizer では次は、AddressSanitizerは対応していない、未初期化メモリ(UMR: uninitialized memory reads)の検知をするためにMemorySanitizerを足してみましょう。 main.cc #include <stdlib.h> #include <stdio.h> void *p; int main(int argc, char **argv) { p = malloc(7); p = 0; // The memory is leaked here. int k = 0x7fffffff; k += argc; // 2147483647 + 1 = Undefined behavior int* a = new int[10]; a[5] = 0; if (a[argc]) // Uninitialized memory read printf("xx\n"); return 0; } # clang++-9 -fsanitize=undefined,address,memory -g main.cc clang: error: invalid argument '-fsanitize=address' not allowed with '-fsanitize=memory' あらら、ビルドエラーが出てしまいましたね。 実はエラーの内容の通り、残念なことに、AddressSanitizerとMemorySanitizerは同時には設定できません。 なので、おとなしく2回実行しましょう。 # clang++-9 -fsanitize=undefined,address -g main.cc -o address.out # clang++-9 -fsanitize=memory -g main.cc -o memory.out # ./address.out; ./memory.out main.cc:11:5: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior main.cc:11:5 in xx ================================================================= ==4260==ERROR: LeakSanitizer: detected memory leaks Direct leak of 7 byte(s) in 1 object(s) allocated from: #0 0x4961dd in malloc (/root/address.out+0x4961dd) #1 0x4c822d in main /root/main.cc:7:7 #2 0x7fe83d8cc09a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2409a) SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s). ==4263==WARNING: MemorySanitizer: use-of-uninitialized-value #0 0x49a8d3 in main /root/main.cc:15:7 #1 0x7f595dd7109a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2409a) #2 0x41f269 in _start (/root/memory.out+0x41f269) SUMMARY: MemorySanitizer: use-of-uninitialized-value /root/main.cc:15:7 in main Exiting 二回実行したとしても 処理速度はvalgrindより早い ので、こちらの方が良いと思うのですがいかがでしょうか? Coverage そうそう、カバレッジの出力も忘れてはいけませんね(カバレッジ計測の仕組みについて興味がある方は こちらの記事 も見てみてください)。 # clang++-9 -fsanitize=undefined,address -g main.cc -o address.out # clang++-9 -fsanitize=memory -fprofile-instr-generate -fcoverage-mapping -g main.cc -o memory.out # ./address.out; ./memory.out ...出力は省略... # llvm-profdata-9 merge -sparse default.profraw -o default.profdata # llvm-cov-9 show -format=html -output-dir=coverage-report -instr-profile=default.profdata memory.out coverage-reportフォルダに結果のhtmlページが作成されます。 これでカバレッジも計測できるようになりました。 コーディングスタイルチェック ここまでで、 clang を使っての sanitizer のチェックを紹介しましたが、ついでなのでフォマッターも紹介します。 「そもそもフォーマッターなんていらないでしょ」と言われるかもしれませんが、コーディングスタイルを統一し、何も考えずにフォーマッターに任せることで、生産性の低い悩みが出る可能性は減らせると思います。 intdash Edgeのプロジェクトでの具体的な使用方法は、 開発者はコードエディタにclang-formatを適用して自動フォーマット(を推奨) CIではclang-formatを実行しフォーマット通りかチェック となっています。 それではスタイルを規定して、フォーマッターを使ってみましょう。 (開発環境のセットアップ方法は こちら で紹介しています) スタイルの規定 clang-formatのスタイルは 様々な設定 ができますが、導入しやすいのはベースとなるスタイルを選び、そこから必要な箇所だけ変更する方法です。 ベースとなるスタイル LLVM A style complying with the LLVM coding standards Google A style complying with Google’s C++ style guide Chromium A style complying with Chromium’s style guide Mozilla A style complying with Mozilla’s style guide WebKit A style complying with WebKit’s style guide Microsoft A style complying with Microsoft’s style guide intdash Edge のプロジェクトでは、ベーススタイルは mozilla を使用し、そこから下記変更を行っています。 switch ブロック内の case X: 文をインデント:しない インデントに使用する列数 :4 アクセス修飾子(public: protected: private:)のインデント:しない このフォーマットを設定ファイルにしたものは下記になります。 .clang-format --- BasedOnStyle: mozilla IndentCaseLabels: false IndentWidth: 4 AccessModifierOffset: -4 ... フォーマッターの実行 では、フォーマッターを下記コードに対して実行してみましょう。 main.cc #include <stdio.h> #include <stdlib.h> class Class {private: Class();}; int main(int argc, char **argv) { switch (argc) { case 1: printf("hello"); break; } return 0; } # clang-format-9 -style=file -i main.cc # cat main.cc #include <stdio.h> #include <stdlib.h> class Class { private: Class(); }; int main(int argc, char** argv) { switch (argc) { case 1: printf("hello"); break; } return 0; } フォーマットされましたね。includeは名前順に変更され、classやswitchは指定したフォーマットで整形されています。 まとめ C++で開発している intdash Edge のプロジェクトで使用しているCIから、clangのsanitizerによる動的テストと、clang-formatによるフォーマットを紹介しました。 sanitizerは今回紹介した以外にも様々な機能があります。ぜひぜひ、導入を検討してみてください。
アバター
はじめまして、今回記事を書かせていただきますSREチームの金澤と申します。よろしくお願いいたします。 自己紹介 前職はとある会社の情報システム部門に在籍して自社のサーバやネットワークの管理といった社内インフラのあれこれを担当していました。 そんな中、社内インフラの更改を担当する機会がありました。更改先として候補に挙がっていたパブリッククラウド(AWS)に触れ、パブリッククラウドが持つサーバ資源調達の迅速性やリソース変更時の柔軟性を文字通り身をもって体験し興味を持つようになりました。 更改以降も自主的に学習を続けていましたが、次第に自分のキャリアもそちらに寄せていきたいと考えるようになりました。そして今、アプトポッドとご縁がありましてSREチームとしてインフラ構築・運用業務にあたっています。 入社のきっかけ HW設計~アプリケーション開発・デザインを統合し製品提供する難易度の高さ 成長できそう 製品が持つ社会貢献性の高さ モダンな技術の採用 製品がカッコよい の箇所を魅力的に感じ入社を決めました。製品のカッコよさは重要ですよね。製品の詳細は 製品紹介 をご覧ください。 キャッチアップの必要性 前職は管理が主な担当業務だったため、開発も含む本職に携わるにあたりキャッチアップの必要性があると感じていました。 本記事では、弊社のコミュニケーション・開発環境を簡単にご紹介するとともに、私自身がどのようなキャッチアップに取り組んでいるかご紹介できればと思います。 私のようにキャリアチェンジに取り組んでおられる方や、弊社に興味をお持ちの方に対して参考となれば嬉しいです。 入社直後の課題改善 弊社の神前(こうさき)のエントリにもあります通り、現在アプトポッドではリモートワークを前提とした勤務体系で社内コミュニケーション基盤としてSlackを利用し各種業務を進めています。 tech.aptpod.co.jp Slackでは基本的な連絡などは滞りなく進む反面、質問や確認で時間を要することを課題に感じていました。入社直後はプロダクト内の言葉や略称を把握できず、どうしても内容を正確に掴み取るまでに時間を要するものと思います。私もまさにそちらに当てはまっていて、業務で取り扱うことになるインフラ構成の詳細や至った意図など、ヒアリングしたい内容が生じていました。 質問のインプットがアウトプットを上回る状態で、チャット上へ随時質問を垂れ流すことも方法として微妙と感じていたこともあり、一先ずSlackの個人スペースにメモとして適宜記録していました。ただ、ご存じの通り個人スペースは自分一人のクローズドな環境のため他者との連携が無く、課題解決に向けどのように進めようかと考えていました。 アプトポッドでは個人が社内でオープンなチャンネルを持ち、 作業進捗 疑問点 気になった(技術)情報 ネタ をポストする文化があります。チャンネルを作成することや、他の人のチャンネルに参加することも自由で、ポストした内容にスタンプなどのレスポンスがあったりもします。率直に良い文化だなと感じています。そして、私の抱いていた課題もそちらに倣ってアウトプットすることで改善が図られることを期待し最近運用を始めました。まだまだ運用期間は短いですが自分の状況のアウトプットを継続することで疑問の解決を促し、自身の成長に繋げていきたいと思います。 開発フローのキャッチアップ チーム開発フローへの順応 アプトポッドではGitlabをVCS *1 に採用しコード管理を行っています。Gitは前職でも使用の経験がありましたが(様々な事情があり)自身の作業履歴を残すことを目的とした個人運用でした。そのため、Gitを使用する上で主流となるチーム開発のフローに順応することを心掛け行動するようにしています。 具体的に言いますと以下のような内容になります。 小さな単位でのコミットを意識する 思いついた多数の修正を含めて fix 等とやらない。 分かりやすいコミットメッセージを心掛ける フォーマットに従うなど運用ルールを確認する。 チームで使用していることを意識する。 ターミナルへツール導入し操作性の改善を行う。 何れも基本的なところですが、VCSの運用方法は会社やその会社が持つ文化に左右され一概には決まらない印象が強いです。このような基本的な内容を確認することで運用方法はもちろんのこと、会社文化の一端を感じ所属している場所に慣れる方法として有効であると考えています。 サンドボックスを利用したIaC *2 のキャッチアップ アプトポッドではお客様に提供するインフラを Terraform および Ansible のコードベースで管理しています。 業務を進める中でインフラリソースを自由に作成・変更・削除することができる環境が欲しくなることがあります。提供されているドキュメントと実動作の乖離確認やパフォーマンスの測定、また既存環境には影響を及ぼさず安心して試行錯誤したいというモチベーションから生まれるものです。 アプトポッドでは、開発者や希望者にパブリッククラウド(AWS)の個人アカウントを払い出しており各々が使用することができます。そのため個人アカウントをサンドボックスとして利用して前述のような検証を自由に行うことができます。これは「社員各々の成長、開発スピードの向上のため」という目的の元で進められていて皆幸せとなれる良い文化だなあと感じています。 *3 私自身もこれには非常に助けられており、自身の理解促進に役立っていると実感しています。特にコストがかかる機能検証はプライベートアカウントで行うのも抵抗がありますよね。 守備範囲を広げる 構築作業で使用するツールがPythonの2系での実装であったため、構築作業と並行して3系への移植を行いました。そして、このような作業に関わらず各種作業を通じて気づいた点は、社内Wikiに反映することを都度行っていました。既存環境の改善の一環なのですが、さらにもう一点、自身の守備範囲を広げるというねらいがありました。 まだ、構成を十分に把握していない不透明な環境の中で少しでもバリューを発揮できるよう得意な所を増やし、自身の活動の基礎を作るよう意識していました。この考えは参考にさせていただいた ブログエントリ があります。エントリ中に表現されている「庭」という考え方にはすごく共感しており、引き続き実践していきたいと考えています。 知識のキャッチアップ 主に入社前の取り組みになりますが今まで得た知識の振り返りや新たなインプットのために以下の書籍を読みました。 いちばんやさしいGit&GitHubの教本 みんなのコンピュータサイエンス マスタリングTCP/IP入門編(第5版) 過去に購入した書籍を復習のために読み返しました。この中でも有名なマスタリングTCP/IP入門編は一通りの内容をおさらいしたかったため今回は浅く内容をさらったのですが、読んでみて改めて体系的に内容が網羅されている本だと思いました。昨年 第6版 が発売されたようですね。 [試して理解]Linuxのしくみ~実験と図解で学ぶOSとハードウェアの基礎知識 Software Design LinuxのしくみはLinuxの基礎動作を図やプログラムで追えることができるため選択しました。Software Designは毎号特集しているテーマについて初学者が触れやすい構成をしていることが多く、テーマにざっくりと触れたいと思う時には購入しています。 入門監視 SREサイトリライアビリティエンジニアリング 有名なSRE本ですが、まずは一通り読み終えたく少しずつ進めています。 最後に 本記事ではアプトポッドへ入社し業務に携わるにあたり取り組む必要があると感じていたキャッチアップについて、弊社のインフラを交えお話させていただきました。挙げた内容は今後も継続していき、アプトポッドや弊社プロダクトへ貢献できるようこれからも努力したいと思います。 *1 : Version Control System *2 : Infrastructure as Code *3 : もちろん自身でコストの確認を行いつつ使用することにはなります。コスト意識は大事。
アバター
はじめに 動画ストリーミングサービスにおいて、動画の遅延を測定したいというニーズは多いと思います。 動画が遅れる要因として以下3つが主に考えられると思います。 ネットワークの遅延 アプリケーションで行う処理による遅延 動画エンコード遅延 動画デコード遅延 これらをすべて含んだ遅延の測定は比較的簡単に計測可能ですが、要因を切り分けて測定するのは工夫が必要かと思います。 今回、弊社製品 intdash の特徴である、複数のデータソースでタイムスタンプ管理ができることと、オリジナル治具を利用して、この動画エンコード遅延の測定をしてみた話をしたいと思います。 ハードウェアチームの塩出が担当します。 はじめに 本題に入る前に、弊社製品の説明 intdashを使ったエンコード遅延測定の考え方 測定するための治具 特徴 intdashと治具を使った動画エンコード遅延測定 治具についての余談(FPGA内のことについて) FreeRTOSでの実装 おわりに 本題に入る前に、弊社製品の説明 弊社製品 intdash を使用すると、映像やCAN、アナログデータ等、異なるデータソースでもタイムスタンプを一元管理することができ、webアプリケーションの Visual M2M Data Visualizer (以下Visualizerと呼ぶ)を使って取得したデータを、そのタイムスタンプを元に再生することが出来ます。 具体的には、車が発しているCANのデータと、ドライバを撮影した映像を同期させて取ることができ、それをwebアプリケーションで確認出来ます。この仕組みを使うと例えば、車が発しているハンドル角度に相当するCANデータと、その時ドライバが操作しているハンドルの角度が一致してる状態で確認できるということになります。 もちろん、そのままではエンコードの遅延が乗ってしまうので、お客様へ提供する製品ではこのエンコード遅延分は事前に測定して補正するようにしております。 intdashを使ったエンコード遅延測定の考え方 intdash での打刻タイミングは、CANのデータはCANデータを受け取った時、動画はエンコードされたデータを受け取ったときとなっています。なので、もしCANと被写体が同じデータを発するものだとすれば、 Visualizer を使って確認すると理想的には、ある時間における動画データとCANのデータは同一のものになっているはずです。仮にこれが異なるとすると、その時間分がエンコードの遅延ということになります。図にすると以下の様になります。 測定するエンコード時間の説明図 測定するための治具 CANと被写体が同じデータを発するものというと、身近なもので言えば車のステアリングが相当するのですが、手軽にデータが取得出来ないのと、ステアリングがどれくらい回転しているかを動画で把握するのは目分量になってしまうので測定向きではありません。そこで、今回は Terasic社製のFPGA評価ボード DE10-Lite を使用して測定のための治具を作ってみました。 動画エンコード遅延測定治具 特徴 DE10-Lite は、7segディスプレイが付いた評価ボードで、CANの出力部分に関しては自作しました。簡単に特徴をまとめると以下のようになります。 7segディスプレイは1msec毎に更新 7segディスプレイと同タイミング(約20usec差)で同じ内容のCANを出力(以下の図を参照ください) 右下のLEDは1msec単位のインジケータ 7segディスプレイはダイナミック点灯ではないので、クリアに撮影可能 Visualizer で確認した時、同一時刻における映像の7segディスプレイが示している値と、CANが示している値の差がエンコード時間となる CANと7segの更新タイミング intdashと治具を使った動画エンコード遅延測定 上記治具と intdash を組み合わせて動画エンコード遅延を測定した結果を以下の図に示します。この図は撮影済みのデータを Visualizer で再生したときの様子です。 図の黄色の枠で示したものがCANのデータで、965.294秒を示しています。図の青色の枠で示したものが動画データで、965.193秒を示しています。この差分が動画エンコード遅延を示すので、約100msecの遅延だということがわかります。 なお、この遅延は事前に測定しており、お客様へ提供する際はその分補正しております。 Visualizerで確認できる遅延量の例 治具についての余談(FPGA内のことについて) 今回、ソフトコアである NiosII を使用して、7segディスプレイの更新タイミングと、CANの出力のタイミングを制御しました。 NiosII のプログラムは結局ベアメタルで実装してしまいましたが、 FreeRTOS バージョンでも実装は試しました。 今回使用したQuartusのバージョンは18.1だったのですが、このバージョンだと FreeRTOS のコンテキストスイッチがうまく働かず、そのままでは正常に動作しませんでした。なので、正常に動作するように変更し、FreeRTOSのgithubの方に プルリクエストは出した のですが、コロナの影響で現物確認ができないと言われ、ペンディングになっています。それでもよろしければ活用してフィードバッグをいただけると嬉しいです。 FreeRTOSでの実装 上記プルリクエストでFreeRTOSが使えるようになったので、FreeRTOSでも実装してみました。実装は1msecごとにqueueを出力するタスクと、そのqueueを受け取ってCANを出力するタスク、7segを更新するタスクという構成で行いました。その場合、CANと7segの時間差は200 usecと10倍くらい精度が落ちてしまいました。本来はここからチューニングしてパフォーマンスを出す作業を行うのですが、今回はFreeRTOSを入れること自体に時間がかかってしまったのと、ベアメタルでも事足りていたので、特にチューニングせずにベアメタルを採用することにしました。 今回は残念ながらFreeRTOSは不採用でしたが、FreeRTOSのコンテキストスイッチ回りの実装をちゃんと追ったので今後に活かしていきたいと思います。 おわりに 今回は intdash とオリジナル治具を使って、動画エンコード遅延測定をしてみた話を紹介しました。CANと7segを組み合わせるというあまり見ない構成な気がしますが、デジタルっぽく測定できるので個人的には気に入っています。7segの映像を画像処理して、数値をデジタル化できれば完全にデジタル化できるのでテストツールとしては完成かなと思っております。そこはこれから挑戦していきます。 余談ですが、自分的に一番伝えたかったのはNiosII上でFreeRTOSを動かすことだったりします。バグ報告はあるのですが、解決まで行っておらず苦労したので、この記事が NiosII上でFreeRTOSを動かす上で役に立てれば幸いです。
アバター
はじめに こんにちは、aptpodに入社しそろそろ1年になりますWebチームの松本です。 aptpodでは日々フロントエンドエンジニアとしてReact/TypeScriptを用いた、お客様向けアプリケーションのUIを実装しています。 Reactは実は入社してから初めて触ったフレームワークでしたが、頼れる先輩方のサポートもありつつ日々成長を感じながら開発に励んでいます。 入社当初から開発に関するノウハウやコードに関する考え方など、具体的なプログラムのプロジェクト構成やコンポーネントのファイル分け、コード全体の品質を担保できるよう様々な工夫があり、入社する前から知っていれば…と思うことも多くありましたのでそれらについて今回ご紹介しようと思います。 コンポーネントのファイル構成 まずコンポーネントとはWebページのビューを切り出した部品、つまりボタンやアイコン、またそれらを含む集合体であるヘッダーやメニューを指す言葉です。 コンポーネントと一口に言ってもコードとして記述するには色々な情報を詰め込む必要があります。 ボタンのコンポーネントであれば、ボタンの色形といったスタイル情報や、ボタンを押したときの日時の変換や文字列のフォーマット、またそれらのコードがちゃんと想定通りに動いているかチェックするためのテストコードなどになります。これらを一つのファイルにまとめて記述した場合、コードの可読性が著しく損なわれるため、コンポーネントで利用する基本的なファイル構成をテンプレート化し、それぞれのファイルに対し記述を行います。 以下の図はコンポーネントのファイル構成になります。共通で使うものや、ファイルとして大きくなってきた場合などは外出しすることもありますが、基本的にはこれを使っています。 コンポーネントのファイル構成 component.tsx React.FCを宣言しReactコンポーネントのメインとなるファイル constant.ts componentやutilsで使う定数を置くファイル index.ts importしやすくするためのフォルダ内のexportを格納するファイル style.tsx styled-components用のCSS設定を格納するファイル test.stories.tsx storybookで利用するためcomponent.tsxからインポートし表示するためのファイル test.stories.style.tsx test.stories.tsx内で利用するstyled-componentsのCSS設定を格納するファイル utils.ts component.tsx内で利用するロジック用の関数を格納するファイル utils.test.ts jestを利用しutils.tsからインポートした関数のテストコードを格納するファイル テンプレート生成ツール 上記のように各コンポーネントにテストを書くなどしてファイルが増えてくると新しいコンポーネントを作るときにファイル生成が手間になってしまいます。そのようなときにコマンドでコンポーネントのファイル郡を生成してくれるものとして次のものを使っています。 hygen 対話式コードジェネレータCLIでテンプレートに沿ったファイルの生成を行ってくれるツールになります。 npm、yarnで導入でき先に示したテンプレートを設定しコマンド入力しコンポーネント名を入れると生成してくれるようになります。 www.hygen.io ユニットテストツール コンポーネントは見た目に関するビュー部分と動きや計算を行うロジック部分に分かれますが、こちらは主にそのロジック部分をテストするツールになります。前述のコンポーネントのファイル構成ではutils.ts内で使われるライブラリとして以下のものを使用しています。 jest npm、yarnですぐに使えるテストツールです。utilから関数をインポートし入力に対する結果を記述することで、コマンド入力もしくはymlの記述でCI時に自動テストを行うことができます。 下図のように、設定した拡張子(ここでは.test.ts)のプロジェクト内のファイルのテストコードが実行され、結果が表示されます。 jestjs.io jestテスト結果 ビジュアルリグレッションテストツール こちらは先程と違いビュー部分に関するテストツールになります。ビジュアルリグレッションテストとは、画像の保存を行い、コミット後にコミット前との比較を行うことで、意図したコンポーネントだけが変更されているか、意図していないコンポーネントに変更を及ぼしていないかを検出するテストです。 前述のコンポーネントのファイル構成ではtest.stories.tsx内で使われるライブラリやツールとして以下のものを紹介します。aptpodではアトミックデザインを採用したコンポーネント構造になっており、基本的な部品コンポーネントを変更した場合、影響の出る親コンポーネントは多数に渡ることもあり、目視確認では限界があるためこれらを導入しています。 storybook こちらはテストツールというよりは作成したコンポーネントを一覧化し、開発中のコンポーネントのUIを確認するためのツールになります。アトミックデザインを採用していればフォルダ構造そのままで一覧化されるので、わざわざページアクセスし対象のコンポーネントが表示されるページまでリンクを追ってという手順を踏まずに済みます。 storybook.js.org storybook画面 storycap このツールではstorybook上で表示されるコンポーネントの画像をスクリーンショットとしてフォルダにまとめて保存してくれるツールになります。 ローカルでは保存先のフォルダを指定することでコマンド一発でスクリーンショットを生成してくれます。CIでは保存先をAWSのストレージサービスであるS3などにしておき、後の変更時の比較に使います。 github.com storycap出力図 reg-suit こちらは先程S3に保存した画像と新しくコミットされた内容の画像を比較し、変更があった場合はその差分を検出してくれる機能になります。CIにAWSのキーを環境変数にセットしregconfig.jsonに設定を書き込むことで動いてくれます。 reg-viz.github.io gitlab上でのreg-suit結果 まとめ 今回はコンポーネントのファイル構造という目線から、自動テストなどを用いてどのように品質を向上させていくのか、それらを組み合わせコンポーネントとしてどのように運用しているのか説明させていただきました。各ツールの詳しい使い方は割愛させていただきましたが、Webチームがどのように開発を進めているのかその一部でも伝わったならば幸いです。私個人としても今後ともフロントエンドを含むWebに関する知識や経験をもっと身につけ、本テックブログを通して情報発信していけたらなと思います。
アバター
研究開発グループで機械学習に関係する仕事を担当している瀬戸です。前回は、 fastaiで学習に使う関数をApache MXNetで真似してみた - aptpod Tech Blog を紹介させて頂きました。今回は、SageMaker Python SDKのMXNetで利用できるGluonCVのモデルを、SageMaker Neoでコンパイルし、Jetson tx2上でDLRを用いて動作させることができたので紹介したいと思います。 ツールの概要 Apache MXNet Apache MXNet (以下、MXNet)は、 TensorFlow や PyTorch など同じディープラーニングのフレームワークです。 ここ で紹介されているように、AWSのサービスである Amazon SageMaker に利用されています。 GluonCV Toolkit GluonCV Toolkit (以下、GluonCV)は、MXNetの持つパッケージの一つである Gluon をComputer Visionに特化する形で発展させたパッケージです。既知の有名な論文をベースにした学習済みモデルを提供するModel Zooや、新しいデータ拡張の関数などが提供されています。 Model Zooに設置されている学習済みモデルは、 学習スクリプト や その実行コマンド が公開されていて、再現性がある程度担保されるよう運用されています。使い方としては、Gluonベースの学習スクリプトに記述しているモデル部分やデータ拡張部分をGluonCVベースに書き換えるだけで使えて簡単に利用することが可能です。 Amazon SageMaker Neo & DLR Amazon SageMaker Neo はAmazon SageMakerのサービスの一つで TVM Stack や DLR で動作するようにモデルをコンパイルするサービスです。TVMやDLRは、ディープラーニングフレームワークのモデルをGPUやCPUに最適化して実行してくれるツールです。 モデルを動かしてみる 1. GluonCVのModel Zooからモデルを保存する Model Zooにあるモデルを扱うのが簡単なので、以下のスクリプトを使ってモデルをダウンロードしモデル保存します。モデルはtar.gzで圧縮して保存していますが、これはAmazon SageMaker Neoで扱うためです。この記事では、 “ResNet152_v1d” を使って実験を行っていますが Model Zooのサイト の中から別のモデルを選択できます 1 。 import tarfile import argparse import gluoncv import mxnet as mx from pathlib import Path if __name__ == '__main__' : parser = argparse.ArgumentParser() parser.add_argument( '--model-name' , type = str , default= 'ResNet152_v1d' ) args = parser.parse_args() model_name = args.model_name # model_zooからモデルを取得 net = gluoncv.model_zoo.get_model(model_name, pretrained= True ) # モデルを保存するための前準備 net.hybridize() # 画像の入力サイズが違う場合は変更する dummy = mx.nd.ones([ 1 , 3 , 224 , 224 ]) _ = net(dummy) # 保存先ディレクトリの設定と作成 store_dir = Path(f './{model_name}' ) if not store_dir.exists(): store_dir.mkdir() # モデルの保存 net.export(f '{store_dir}/{model_name}' ) # SageMakerNeoでコンパイルできるようにtar.gzで圧縮 tar_name = './' + model_name + '.tar.gz' archive = tarfile.open(tar_name, mode= 'w:gz' ) archive.add(store_dir) archive.close() 2. Amazon SageMaker Neoでコンパイルする 任意のs3のバケットへモデルのtar.gzファイルを配置してAmazon SageMaker Neoでコンパイルをします。以下の画像のように、Amazon SageMakerサービスへログインしてコンパイルを行います。下図のようなGUI操作を行い、コンパイルされたモデルがtar.gzで吐き出されれば完了となります。 Amazon SageMakerのコンパイル設定画面 3. Jetson TX2上のDLRを使って実行する DLRを ここ を参考にインストールしたNVIDIA Jetson TX2上で以下のスクリプトを実行しました。前準備として、下記のスクリプトと同じディレクトリに、 compiled フォルダを作成し、このフォルダ以下にコンパイル済みモデルを格納しておきます。 import sys import numpy as np from dlr import DLRModel import datetime if __name__ == '__main__' : # モデルのロード device = 'gpu' model = DLRModel( 'compiled' , device, 0 ) # 入力画像のサイズを指定 image_size = 224 times = [] # 100回実行する for _ in range ( 100 ): # 入力データをランダムに生成する im = np.random.random([ 1 , 3 , image_size, image_size]) b, h, w, c = np.array(im).shape input_data = { 'data' : im} # Predict start_prediction_time = datetime.datetime.now() out = model.run(input_data) # 処理時間を保持 end_prediction_time = datetime.datetime.now() duration = end_prediction_time - start_prediction_time sys.stdout.write(f " \r prediction time duration: {duration}" ) sys.stdout.flush() times.append(duration.total_seconds()) print () print ( '-------------------------------------------------------' ) print (f 'total_time: {np.sum(times)}' ) print (f 'mean_time: {np.mean(times)}' ) print (f 'median_time: {np.median(times)}' ) print (f 'std_time: {np.std(times)}' ) print ( '-------------------------------------------------------' ) print ( 'finished prediction' ) 実行した結果は以下の画像のようになりました。推論速度は、今回の検証項目の内容ではありませんが試しに取得してみました。だいたい、 1/0.053=18fps ぐらい出ているようです。 ランダム入力による推論結果 また、FP16で推論した結果も載せておきます。おおよそ、中央値でみると1.7倍早くなっているようです。 ランダム入力による推論結果(FP16) まとめ GluonCVのモデルを複数のツールを使ってJetson TX2上で動作させました。引き続き、機械学習に関するやってみたや業務内で検証した内容を紹介していこうと思います。 他のモデルでは試していないので動かない可能性があります。 ↩
アバター