TECH PLAY

セーフィー株式会社

セーフィー株式会社 の技術ブログ

221

本記事では、GoogleスプレッドシートからSlackAPIとSlackApp(Slackbot)を使って通知を送る仕組みの作り方を、SlackAPI・GAS初心者にも分かりやすく画像付きで解説します。 はじめに なんでやろうと思ったか 用語解説 SlackAppとは? Slackbotとは? Slack APIとは? Slackbot作成~Token発行まで 動作確認 テスト用データ作成 GASの設定 SlackIDを取得してみる このSlackIDって正しいの??? DMを送信してみる 直書きtokenをなんとかする 仕上げ 終わりに はじめに なんでやろうと思ったか Slackにて、ある特定(かつ大勢)の人達に連絡をする機会があり、対象者リストを元に連絡用チャンネル作って連絡を実施していました。 この業務ですが、めんどくさいポイントが複数ありました。例として数個あげるなら、 招待する人数が多いと招待する作業だけで骨が折れる 目が滑って抜け漏れが発生しやすい リマインドで何回も@channelするのも腰が引けるetc…. こういった連絡系のタスクを楽にできないかと考えた結果、「対象者リストにはメアドも記載されているので、わざわざ手打ちせずとも対象者リストを元に連絡作業をボタン1つで完了できるようにすればいいんじゃない?」と考え、作成に至りました。 また、SlackAPPやSlackAPIを使う事が初めてだったので備忘録として残しておこうと思い、執筆する運びとなりました。 用語解説 SlackAppとは? Slackが提供する開発者用の拡張プラットフォーム全体のことを指します。WebhookやSlashコマンド、イベントハンドラーなど複数の機能をパッケージ化できます。 例えば外部サービス(Googleカレンダー、GitHub、Trelloなど)から通知を受け取ることができたり、ワークフローの自動化が実現できます。 slack.com Slackbotとは? Slackbotとは、SlackAppの機能の1つで、Slack上で自動的にメッセージを投稿したり、ユーザーの指示に従って情報を提供するBotのことです。 Slackbotを使うことで、定型的な通知やリマインダーなどを自動化し、作業の効率化を図ることができます。 slack.com Slack APIとは? Slack APIとは、Slackの機能をプログラムから利用するための仕組みです。 Slack APIを使えば、外部のアプリケーションやサービスからメッセージを投稿したり、Slackのデータを取得したりすることができます。 今回はこのSlack APIを使って、スプレッドシートの情報をSlackへ送信する仕組みを作ります。 api.slack.com Slackbot作成~Token発行まで まずはSlackアプリを作成します。 Slack APIページ へアクセスして、新しいSlackアプリを作成します。 赤枠で囲った 【Create New App】 を押下します Create an App のポップアップウィンドウが表示されるので 【From scratch】 を押下します アプリ名とワークスペースを入力する画面が表示されるので、該当箇所に必要な情報を入力します。 今回は「Slackbot_test」というアプリ名で作成します。ワークスペースは各自の環境を選択してください。 入力後、 【Create App】 を押下します。 今しがた作成したアプリ名の設定画面が表示されます。 これでSlackアプリ自体の作成は完了です!簡単ですね アプリにScopeの設定を行います。左側メニューから「OAuth & Permissions」を選択し、Scopes項目の赤枠で囲った 【Add an OAuth Scope】を 押下します。 ここでどういうアクションを許可するか設定します。 今回作りたい物を分解すると ①スプシ上のセルに書き込まれているメアドを参照して、紐づいているSlackIDを取得後( users:read ) ②SlackID宛にリマインドメッセージを飛ばす( chat:write ) となるので、 chat:write 、 users:read の2つを追加します Scopeを設定したら、このアプリをワークスペースへインストールします。 インストールすることで、Slack ワークスペース上でアプリを使用できるようになります。 「OAuth & Permissions」の上の方にある「OAuth Token」項の 【Install to xxx(ワークスペース名)】 を押下します。 SlackAppが ワークスペースにアクセスする権限をリクエストしてくるので、許可してあげましょう 無事このアプリ用のOAuth Tokenが発行されました!(黒くフィルターされている箇所にTokenが表示されています) このTokenを使用することで、SlackAPIを利用することができます 動作確認 本当にAPIを使用できるかどうか、実際にテストしてみましょう テスト用データ作成 Googleスプレッドシートの準備 新しいGoogleスプレッドシートを作成し、テストデータをセルへ書き込んでいきます。 とりあえず以下画面のようなデータを作成しました。 GASの設定 GASにSlackAPP用のライブラリを設定します。 スプレッドシートからGASを開き、画面左側にあるライブラリの”+” をクリック → 以下のスプリクトIDを入力し、検索します。 1on93YOYfSmV92R5q59NpKmsyWIQD8qnoLYk-gkQBI92C58SPyA2x1-bq SlackAPPが見つかったら【追加】ボタンを押下します。 追加後、以下画面のようにライブラリ欄に「SlackAPP」が追加されている事を確認してください これでGASの設定は完了です SlackIDを取得してみる 以下のようなスクリプトを作成しましたので、このコードをGASへ張り付けます。 ※あくまでテストなのでtoken直書きになっていますがご承知おきください (後ほど隠す方法も記載します) function GetSlackUserId (){ // トークン const slack_OAuth_token = "xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; // スプレッドシートの B2 からメールアドレスを取得 const sheet = SpreadsheetApp . getActiveSpreadsheet () . getActiveSheet () ; const email = sheet . getRange ( "B2" ) . getValue () ; const url = "https://slack.com/api/users.lookupByEmail" const options = { "method" : "post" , "contentType" : "application/x-www-form-urlencoded" , "payload" : { "token" : slack_OAuth_token , "email" : email } } ; const response = UrlFetchApp . fetch ( url , options ) ; let res = JSON . parse ( response ) ; let user_id = res . user . id ; // 取得結果をシートの C2 に書き込む sheet . getRange ( 2 , 3 ) . setValue ( user_id ) ; } 貼り付けたら、赤枠の「▷実行」ボタンを押下します。 スプシの構成通りなら、実行ログに「実行完了」と表示されます。 スプシを見てみましょう。SlackID欄にIDが書き込まれているはずです [ やったぜ このSlackIDって正しいの??? という事で、ぱぱっと確認してみます。 ①まず、生成されたC2セルのSlackIDをコピーします。 ②Slackアプリ上で、新規DMを作成します。 ③送信先入力欄に先ほどコピーしたSlackIDを張り付けます 直接メアドや登録名を記入せずとも、コピペだけで宛先欄にユーザーが表示されることが確認できます DMを送信してみる 上項でメアドからSlackIDを取得できる事は確認できました。次は、SlackBotを介してメッセージを送ってみましょう。 前項のSlackIDを取得するスクリプトに手を加え、以下のようなスクリプトを作成しました。やっている事は簡単で、 ①誤実行防止用のメッセージボックスを表示後に ②B列に入っているメアド取得 ③slack_message_textで送信したい文章を作ってから ④B列のメアドをGetSlackUserId()に渡して個々のSlackIDを取得 ⑤個々のSlackIDを元に、slack_App.postMessageでSlackID宛にDMを飛ばす ⑥行数分④~⑤を繰り返す 以上です function SlackBot () { //誤実行防止 let result = Browser . msgBox ( "Slackに通知文を送信します。よろしいですか?" , Browser . Buttons . OK_CANCEL ) ; if ( result == "cancel" ){ return; } const slack_OAuth_token = "xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ; const slack_App = SlackApp . create ( slack_OAuth_token ) ; let sheet = SpreadsheetApp . getActiveSpreadsheet () . getActiveSheet () ; // スプレッドシート最終行の行番号を取得 let lastRow = sheet . getLastRow () ; // B列の2行目から最終行までを指定 let emailRange = sheet . getRange ( 2 , 2 , lastRow - 1 , 1 ) ; // (開始行, 開始列, 行数, 列数) // 空白を除く let emails = emailRange . getValues () . filter ( row => row [ 0 ]) ; // 送信文 let slack_message_text = "test" for ( let email of emails ){ let user_id = GetSlackUserId ( email [ 0 ] , slack_OAuth_token ) let slack_message = "<@" + user_id + ">" + slack_message_text slack_App . postMessage ( user_id , slack_message ) ; } } function GetSlackUserId ( email , token ){ const url = "https://slack.com/api/users.lookupByEmail" const options = { "method" : "post" , "contentType" : "application/x-www-form-urlencoded" , "payload" : { "token" : token , "email" : email } } ; const response = UrlFetchApp . fetch ( url , options ) ; let res = JSON . parse ( response ) ; return res . user . id ; } 上記スクリプトでGASを実行すると、作成したSlackアプリからメッセージが届きます こんな感じ 直書きtokenをなんとかする 先ほど上の方でも触れましたが、token丸出しは余りにもセキュリティ意識や見栄えが悪すぎるため、この丸出し状態をなんとかします。 GASでは 「プロパティ」 として公開したくない情報を管理できる方法があります。 GASの画面左側にあるメニュー一覧から、赤枠の設定アイコン(歯車マーク)を押下して、プロジェクトの設定画面を開きます プロジェクトの設定画面最下段に、 スクリプト プロパティ項 があります。 【スクリプトプロパティを追加】 ボタンを押下します プロパティ欄に分かり易い名称を付け、値欄にtoken等を格納します。 入力後、【スクリプトプロパティを保存】を押下します。 プロパティを登録して満足してはいけません(1敗) GASスクリプト内の直書きtokenにプロパティを適応する作業を行います function SlackBot () { : const slack_OAuth_token = "xoxb-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ; //直書き状態から ↓↓↓↓↓↓↓↓↓ const slack_OAuth_token = PropertiesService . getScriptProperties () . getProperty ( "SlackOAuthToken" ) ; //プロパティサービスを適用 : } プロパティサービスを使うためには、PropertiesServiceというクラスを使用します。getScriptPropertiesというメソッドを使用することで、スクリプトプロパティに登録済みのプロパティを取得することができます。 最後に、getPropaties()メソッドでプロパティ名を指定することによって、値を秘匿した状態で使用することが可能になります。 詳しくは コチラ をご参照ください developers.google.com 最終系は以下の通りです。 function SlackBot () { //誤実行防止 let result = Browser . msgBox ( "Slackに通知文を送信します。よろしいですか?" , Browser . Buttons . OK_CANCEL ) ; if ( result == "cancel" ){ return; } const slack_OAuth_token = PropertiesService . getScriptProperties () . getProperty ( "SlackOAuthToken" ) ; const slack_App = SlackApp . create ( slack_OAuth_token ) ; let sheet = SpreadsheetApp . getActiveSpreadsheet () . getActiveSheet () ; // スプレッドシート最終行の行番号を取得 let lastRow = sheet . getLastRow () ; // B列の2行目から最終行までを指定 let emailRange = sheet . getRange ( 2 , 2 , lastRow - 1 , 1 ) ; // (開始行, 開始列, 行数, 列数) // 空白を除く let emails = emailRange . getValues () . filter ( row => row [ 0 ]) ; // 送信文 let slack_message_text = "\n test" for ( let email of emails ){ let user_id = GetSlackUserId ( email [ 0 ] , slack_OAuth_token ) let slack_message = "<@" + user_id + ">" + slack_message_text slack_App . postMessage ( user_id , slack_message ) ; } } function GetSlackUserId ( email , token ){ const url = "https://slack.com/api/users.lookupByEmail" const options = { "method" : "post" , "contentType" : "application/x-www-form-urlencoded" , "payload" : { "token" : token , "email" : email } } ; const response = UrlFetchApp . fetch ( url , options ) ; let res = JSON . parse ( response ) ; return res . user . id ; } 仕上げ これで一通りの仕組みが出来上がりました。 ですが、現状わざわざGASを開いて都度「実行」を押しているので、ここをもっと簡単に実行できるようにします。 スプレッドシートの「挿入」から図形描画を選択し、任意の図形を作成します。 作成したら「保存して閉じる」を押下します。 以下のように、シート上に図形が作成されます。  図形をクリックすると、右上にメニューアイコンが出てきます。アイコンを押下し、「スクリプトを割り当て」を押下します。 この図形をクリックしたときに実行したいスクリプトを入力する画面が表示されます。  作成したスクリプトの「SlackBot」関数を指定します 「確定」を押下後、スプレッドシート画面が表示されます。 これで、図形を押下すればSlack通知を送れるようになりました。  以下画像は、実際に図形をクリックした時の画面です。 終わりに 以上で、GoogleスプレッドシートからSlackAPIとSlackApp(Slackbot)を使ってSlackに通知を送る仕組みが出来上がりました! とはいえ、上記スクリプトにはまだまだ手を加えるべきポイントがあります。 例えば・・・ メアドが存在しない場合の例外処理 その他エラーが発生した場合の例外処理 スクリプト実行時のタイムスタンプをセルに追加する 送信文がGAS内にべた書きになっている 等々 もし機会があれば、本記事を元にGoogleフォームのアンケート機能と組み合わせて、回答確認 & リマインドの発展系バージョンもご紹介できればと考えています。 本記事を参考に、日常業務の中で発生する連絡・リマインド作業がある方の助けになれば幸いです。 セーフィーではエンジニアを積極的に募集しています。どのような職種があるのか気になる方はこちらをご覧ください! safie.co.jp カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 皆様のご応募、心よりお待ちしております! 最後までお読みいただき、ありがとうございました
アバター
はじめに こんにちは。セーフィー株式会社で内定者インターンをしている戌亥です。汎用的なコンポーネントの開発をしている中で起こったトグルボタンを実装バグの紹介とその原理、解決法についてご説明しますので、ぜひ参考にしてください。 はじめに 背景 ラベルボタン 起こったバグ 検証 バグが起こっていた原理 解決法 まとめ 最後に 背景 インターンのReactを用いた個人開発課題の中で汎用的なコンポーネントを先にある程度作ってしまおうと考えました。その際にラベルボタン方式でトグルボタンを実装しようとしたのですが、clickイベントが2度実行されてしまうバグが発生しました。React側の問題かと思って調べてみるとlabel要素自体の仕様が関わっていることがわかったので、色々検証を行いました。 ラベルボタン トグルボタンを実装する方法にはJavaScriptで実装する場合とHTML/CSSのみで実装する場合の2パターンがあります。 JavaScriptで実装する場合は、ボタンにする要素がクリックされたときに<要素>.classList.toggleなどでclassをつけたり外したりすることでONかOFFかの状態を保存できます。 HTML/CSSで実装する場合は、label要素を非表示化したcheckboxに紐づけることでONかOFFかの状態を保存できます。 本来label要素はforで指定したidを持つ要素と連動しますが、forで指定していない上にlabel要素の中にcheckboxなどの紐づけられる要素がある場合、それに紐づきます。このようにlabel要素で囲んで実装するボタンを個人的にラベルボタンと呼んでいます。ラベルボタンのメリットはforやidを指定しなくていいので、複製する際などにidをわざわざ振らなくてもいいところです。 < style > input [ type = "checkbox" ] { display : none ; } label > span { padding : 10px 20px ; margin : 5px ; background-color : #357c9c ; color : white ; border : 2px solid #ed7d31 ; cursor : pointer ; display : inline-block ; } label :has(: checked ) > span { background-color : #ed7d31 ; } </ style > < label > < input type = "checkbox" > < span > ラベルボタン </ span > </ label > ソースコード1:ラベルボタンの例 起こったバグ 実際のコードは割愛しますが、先ほどのサンプルコードのような要素をReactコンポーネントで作成し、安易にonClickにイベントを紐づけました。その結果、ボタンをクリックしたときに紐づけた関数が2回実行されるということが起きました。 まずReact環境に起因するのかどうかを判断するために、純粋なhtmlファイルをエクスプローラーで開いただけの環境にしました。そこでaddEventListenerを用いてclickイベントを紐づけたところ、同じ挙動が発生したため、Reactに起因することではないことがわかりました。 またラベルボタンではなくidでlabel要素を紐づけて行ったところ、1回のみの実行となったため、ラベルボタンの時のみに起こることがわかりました。 再現できるソースコード2ではボタンのlabel要素のclickイベントに「Hello, world.」とコンソール出力するだけの設定をしています。図1はラベルボタンをクリックしただけの状態ですが、2回出力されていることがわかります。対して図2はidに紐づける方法ですが、1回のみの出力となっていることがわかります。 <!DOCTYPE html> < html > < head > < meta charset = "utf-8" > < style > input , input [ type = "checkbox" ] { display : none ; } label > span { padding : 10px 20px ; margin : 5px ; background-color : #357c9c ; color : white ; border : 2px solid #ed7d31 ; cursor : pointer ; display : inline-block ; } label :has(: checked ) > span { background-color : #ed7d31 ; } input [ type = "checkbox" ] : checked + label > span { background-color : #ed7d31 ; } </ style > </ head > < body > < div id = "test1-1" > < label id = "test1-2" > < input type = "checkbox" id = "test1-3" > < span > ラベルボタン </ span > </ label > </ div > < div id = "test2-1" > < input type = "checkbox" id = "test2-2" > < label for = "test2-2" id = "test2-3" > < span > 紐づけボタン </ span > </ label > </ div > < script > function hw () { console . log ( 'Hello, world.' ) ; } window . addEventListener ( 'load' , function () { const labels = document . querySelectorAll ( 'label' ) ; [ ... labels ] . map (( label ) => { label . addEventListener ( 'click' , hw , false ) ; }) ; } , false ) ; </ script > </ body > </ html > ソースコード2:テスト環境 図1:ラベルボタンのクリック時の実行結果 図2:紐づけボタン(idとforで紐づけたトグルボタン)のクリック時の実行結果 label要素の挙動について正確な仕様を調べたところ、「label要素がクリックされたとき、その中に含まれるフォームコントロールに対してclickイベントを発火する」[1]ということがわかりました。このフォームコントロールとはlabelable elementと呼ばれるbutton,input,meter,textarea等といったlabel要素に紐づけられる要素のことです。 検証 次にソースコード3のhtmlファイルでイベントの流れを調べていきます。 <!DOCTYPE html> < html > < head > < meta charset = "utf-8" > < style > input , input [ type = "checkbox" ] { display : none ; } label > span { padding : 10px 20px ; margin : 5px ; background-color : #357c9c ; color : white ; border : 2px solid #ed7d31 ; cursor : pointer ; display : inline-block ; } label :has(: checked ) > span { background-color : #ed7d31 ; } input [ type = "checkbox" ] : checked + label > span { background-color : #ed7d31 ; } </ style > </ head > < body > < div id = "test1-1" > < label id = "test1-2" > < input type = "checkbox" id = "test1-3" > < span > ラベルボタン </ span > </ label > </ div > < div id = "test2-1" > < input type = "checkbox" id = "test2-2" > < label for = "test2-2" id = "test2-3" > < span > 紐づけボタン </ span > </ label > </ div > < script > function returnPhase ( phase ) { switch ( phase ) { case 1 : return 'Capturing' ; case 2 : return 'Target' ; case 3 : return 'Bubbling' ; } } function sample ( event ) { const ev = { type : event . type , evented : event . target , handled : event . currentTarget , phase : returnPhase ( event . eventPhase ) , } console . log ( ev ) ; } window . addEventListener ( 'load' , function (){ const targets = document . querySelectorAll ( 'body *' ) ; [ ... targets ] . map (( target ) => { target . addEventListener ( 'click' , { handleEvent : sample // イベントに紐づけさせる関数 } , false // captureフェーズで発火させる場合はtrue ) ; }) ; } , false ) ; </ script > </ body > </ html > ソースコード3:検証用環境 こちらが今回検証するラベルボタンの部分 < div id = "test1-1" > < label id = "test1-2" > < input type = "checkbox" id = "test1-3" > < span > ラベルボタン </ span > </ label > </ div > ソースコード4:ラベルボタンの部分 < div id = "test2-1" > < input type = "checkbox" id = "test2-2" > < label for = "test2-2" id = "test2-3" > < span > 紐づけボタン </ span > </ label > </ div > ソースコード5:比較用の紐づけボタンの部分 図3:ラベルボタンのDOM要素イメージ図 こちらは今回イベントに紐づけるためのサンプルとして作った関数(sample)です。 function sample ( event ) { const ev = { type : event . type , // イベントの種類 evented : event . target , // イベントの自体の発火元 handled : event . currentTarget , // 伝播された現在の発火元 phase : returnPhase ( event . eventPhase ) , // どこのフェーズで発火したか } console . log ( ev ) ; } ソースコード6:sample関数 図4:実装したレイアウト(まだクリックしていない状況) バグが起こっていた原理 イベントの処理の伝達にはCaptureフェーズ、Targetフェーズ、Bubblingフェーズが存在します。全体の流れとしてはラベルボタンをクリックするとまず、Captureフェーズ(赤矢印)でwindow→document→body→div→label→spanとターゲットまで伝達が上がってきます。次にTargetフェーズ(緑枠)でspanタグに到達します。その後、Bubblingフェーズ(青矢印)で伝達が下りていきます。 図5:CaptureフェーズからBubblingフェーズ デフォルトだとaddEventLisetnerの第三引数がfalseになっているのでBubblingフェーズでイベントが実行されます。そのため子要素から親要素へと処理が行われていくようになります。 図6:CaptureフェーズからBubblingフェーズの詳細 sample関数を紐づけているdiv要素以下に注目すると、span要素がクリックされ、Captureフェーズでspan要素まで到達します。①Targetフェーズでspan要素がイベント発火します。②Bubblingフェーズでlabel要素がイベント発火します。③Bubblingフェーズでdiv要素がイベント発火します。 図7:label要素によるcheckboxのクリック後の流れ label要素でclickイベントが発火したため、子要素で一番最初に見つかるlabelable elementのcheckbox要素がクリックされます。このクリックにおいても①Targetフェーズでcheckbox要素がイベント発火します。②Bubblingフェーズでlabel要素がイベント発火します。③Bubblingフェーズでdiv要素がイベント発火します。これによってlabel要素と、それより親の要素は2回イベント発火することになります。 図8:ラベルボタンをクリックした図 図9:紐づけボタンをクリックした図 図8のコンソール画面を見ると、説明の通りまずspan要素のバブリングが行われています。その後checkboxがクリックされ、checkboxからバブリングが起きていることがわかります。 図9のコンソール画面を見るとspan要素がクリックされた後、先ほどと同じようにTargetフェーズでspan要素がイベント発火し、Bubblingフェーズでlabel要素、div要素とイベント発火しています。こちらはforで指定したcheckboxがクリックされ、Targetフェーズでcheckbox要素が、Bubblingフェーズで親要素であるdiv要素がイベント発火しています。 label要素はクリックされたときに、子要素またはforで指定したcheckbox等をクリックする仕様がありました。つまりcheckboxのバブリング上にlabel要素があるときは、label要素のクリックとcheckboxのクリックの2回イベント発火します。ラベルボタンはcheckboxのバブリング上にあるから2回発火し、紐づけボタンはcheckboxの兄弟要素であってバブリング上にはないために1回のみの発火になっていました。 解決法 clickイベントが2回発火する原因はバブリング上にlabel要素自身があったためなので、主に取れる手法は ①label要素をバブリング上から外す ②バブリングが伝わらないようにする ③clickイベントではなくchangeイベントを使う の3つのうちのどれかで解決できます。 ①バブリング上から外す場合、紐づけボタンのようにlabel要素の親子関係に対象のlabelable elementを置かずにidとforで紐づけます。 ②バブリングが伝わらないようにする場合、これはいくつか方法があります。 (1)stopPropagationを使う (2)preventDefaultを使う (3)targetを参照する ②(1)ソースコード7のようにsample関数を書き変えてevent.stopPropagation()を実行することで、イベントの伝播をそこで止めることができます。これは他の紐づけ関数に対しても伝播を止めてしまうことになるため、あまり推奨されていません。図10はstopPropagationを実装した後のラベルボタンをクリックしたときの図です。図6の①と図7の①だけ実行されていることがわかります。伝播は中断してもlabel要素の挙動によるcheckboxのクリックは中断されないようです。またこの場合はcheckboxにclickイベントを設定する必要があります。 function sample ( event ) { event . stopPropagation () ; const ev = { type : event . type , // イベントの種類 evented : event . target , // イベントの自体の発火元 handled : event . currentTarget , // 伝播された現在の発火元 phase : returnPhase ( event . eventPhase ) , // どこのフェーズで発火したか } console . log ( ev ) ; } ソースコード7:event.stopPrpagationの実装例 図10:②(1)実装後のラベルボタンをクリックした図 ②(2)ソースコード8のようにsample関数を書き換えてevent.preventDefault()を実行することで、label要素のクリックされたら紐づいたlabelable elementをクリックするという本来の挙動を止めることができます。それによってlabelボタンのclickイベントは発火しますが、checkboxはクリックされないため新たにclickイベントが発生しなくなり、2重発火を防ぐことができます。しかしトグルボタンとして利用する場合は、label要素がcheckboxがクリックしないため切り替えることができなくなります。 function sample ( event ) { event . preventDefault () ; const ev = { type : event . type , // イベントの種類 evented : event . target , // イベントの自体の発火元 handled : event . currentTarget , // 伝播された現在の発火元 phase : returnPhase ( event . eventPhase ) , // どこのフェーズで発火したか } console . log ( ev ) ; } ソースコード8:event.preventDefaultの実装例 図11:②(2)実装後のラベルボタンをクリックした図 ②(3)event.targetはバブリングの位置に関わらず、イベントのトリガーとなった要素を参照します。checkboxがクリックされた場合はevent.targetにcheckboxが格納されているため、checkboxのクリックによるバブリング中か、ラベルボタンのクリックによるバブリング中かが判別できます。どちらかのときだけ実行するとしておくことで2重実行を防ぐことができます。またこの場合はlabel要素ではなく、checkboxにclickイベントを設定する方がどの場合でも使えるのでお勧めです。 function sample ( event ) { if ( event . target ! == event . currentTarget ) return; const ev = { type : event . type , // イベントの種類 evented : event . target , // イベントの自体の発火元 handled : event . currentTarget , // 伝播された現在の発火元 phase : returnPhase ( event . eventPhase ) , // どこのフェーズで発火したか } console . log ( ev ) ; } ソースコード8:event.preventDefaultの実装例 図12:②(3)実装後のラベルボタンをクリックした図 ③changeイベントを使う場合、label要素自身のchangeイベントは存在しないので、必ずcheckbox等のchangeイベントがバブリングした場合のみイベントが発火します。checkboxはクリックされると状態がON・OFF切り替わるので、changeイベントにすることで同一イベントの2重発火を避けることができます。 まとめ label要素でforを指定せず、checkbox等を囲ってボタンを作るとclickイベントが2重発火します。 2重発火する原因はcheckbox等がlabel要素の親子要素にあり、ボタンのクリック時のバブリングで1回、label要素の仕様によるcheckboxのクリック時のバブリングで1回の計2回label要素でclickイベントが発火するためでした。 対策としてはイベントのトリガーになった要素を参照したり、clickイベントからchangeイベントに変えたりするとボタンに紐づいた処理の2重実行を避けることができます。 トグルボタンの実装からJavaScriptのイベントに関して掘り下げた記事でした。ぜひ参考にしてください。 最後に セーフィーではエンジニアを積極的に募集しています。どのような職種があるのか気になる方はこちらをご覧ください! safie.co.jp 皆様のご応募、心よりお待ちしております! 最後までお読みいただき、ありがとうございました。
アバター
はじめに セーフィー株式会社 で画像認識AIの開発をしているおにきです。 画像認識AIシステムを構築する際AIのコンピューティング処理を「どこで」「いつ」「どのようなデータに対して」行うか、すなわちサービングパターンを選択することは、システムの性能、コスト、拡張性に大きな影響を与えます。本記事では弊社のクラウド録画サービスを例に、画像認識AIのサービングパターンについて詳しく解説します。適切なサービングパターンを選択することで、より効率的で高性能なシステムを構築できるようになります はじめに 画像認識システムのサービングパターンのための要素 コンピューティング タイミング トリガー 入力データ形式 組み合わせのパターン 1.エッジ(カメラ内)動画認識 2.エッジ(AI Box)動画認識 3.エッジ(カメラ内)静止画認識 4.エッジ(AI Box)静止画認識 5.クラウドストリーミング動画認識 6.クラウドストリーミング静止画認識 7.クラウドストリーミング外部トリガー認識 8.クラウドバッチ外部トリガー動画認識 9.エッジ(カメラ内)・クラウドハイブリッド認識 まとめ 最後に 画像認識システムのサービングパターンのための要素 弊社のクラウド録画プラットフォームSafieでは、ネットワークに接続されたカメラが常時映像をクラウドにアップロードし、それを保存する仕組みとなっております。 コンピューティング コンピューティングとは画像認識の主たる処理で画像を入力して認識結果のメタ情報(物体位置など)を出力する処理であり、それをどこで行うかで場合分けを行いました。 エッジ(カメラ内) カメラ(イメージセンサー)と同じハードウェア内にあるCPUもしくはNPU(Neural Processing Unit)で画像認識処理を行います。スマホは防犯カメラの内部で画像認識を行うパターンです。多くの場合、カメラの電力面・コスト面の制約により計算性能が限られます。 エッジ(AI Box) カメラとネットワーク上で近い箇所に別途ハードウェア(AI Box)を設置して画像認識を行う場合です。防犯カメラと同じネットワーク内にJetsonやオンプレGPU PCなどをおいて画像認識処理を行うパターンです。通常カメラ内でのエッジ処理に比べると大きな計算性能を持っています。複数台のカメラに対して1つのAI Boxで認識を行うこともできます。 クラウド カメラから送られてきた画像をクラウド上のコンピューティング環境で画像認識処理を行うパターンです。CPU・GPUなど様々な計算性能を持つインスタンスを選択できますが、エッジ(カメラ内)、エッジ(AI Box)と異なり大きなランニングコストがかかります。 タイミング 画像認識処理を行うタイミングによって以下の3つのパターンに分類することができます ストリーミング 画像データがコンピューティングモジュールに送られたタイミングで処理するパターンです。 バッチ 画像データをストレージに保存しておいて、あるタイミングでまとめて処理を行うパターンです。本稿では触れませんが、バッチ処理は細かく分けると非同期と同期の2種類にさらに分けることができます。 トリガー 処理を行うタイミングは以下の2パターンに分類できます。 一定周期 あらかじめ定められた周期で処理を行います。 外部・UI 外部のセンサーによるイベントやUIからのリクエストをトリガーとして処理を行います。 入力データ形式 静止画 静止画を入力データとして認識処理をします 動画 動画を入力データとして認識処理をします。 ※動画は複数枚の静止画として考えることができますが、動画を入力データとして用いる場合はトラッキングなどフレーム間をまたがった処理を行います。 組み合わせのパターン 前述の要素の組み合わせからパターンを検討します。組み合わせによっては、実現性がないものもあります。以下に弊社で利用および利用を計画している代表的なパターンを記載します。 コンピューティング タイミング トリガー 形式 # パターン名 エッジ(カメラ内) エッジ(AI Box) クラウド ストリー ミング バッチ 一定 周期 外部・UI 画像 動画 1 エッジ(カメラ内)動画認識 ○ ○ ○ ○ 2 エッジ(AI Box)動画認識 ○ ○ ○ ○ 3 エッジ(カメラ内)静止画認識 ○ ○ ○ ○ 4 エッジ(AI Box)静止画認識 ○ ○ ○ ○ 5 クラウドストリーミング動画認識 ○ ○ ○ ○ 6 クラウドストリーミング静止画認識 ○ ○ ○ ○ 7 クラウドストリーミング外部トリガー認識 ○ ○ ○ * * 8 クラウドバッチ外部トリガー動画認識 ○ ○ ○ ○ 9 エッジ(カメラ内)・クラウドハイブリッド認識 ● ● ○ ○ ○ ○:利用 ●:両方同時に利用 *:どれかを利用 1.エッジ(カメラ内)動画認識 カメラ内で動画の認識を行います。カメラ内のAI処理は計算性能が小さいため、比較的小さいモデルを利用して動画認識を行います。 ・動きを伴う分析(人・車両・ベルトコンベア上のモノ) ・姿勢・アクション検出 ・属性(人・車種) ・認証(顔・ナンバープレート) 2.エッジ(AI Box)動画認識 AI Boxで動画の認識を行います。認識できるタスクとしては先述のエッジ(カメラ内)動画認識と同じになりますが、より大きいモデルを利用して精度を向上させることができます。 ・動きを伴う分析(人・車両・ベルコンベア上のモノ) ・姿勢・アクション検出ト ・属性(人・車種) ・認証(顔・ナンバープレート) 3.エッジ(カメラ内)静止画認識 認識間隔が数分など比較的長い認識のパターンです。エッジ(カメラ内)動画認識に比べて計算時間に余裕があるため、1フレームあたりの計算量がある程度大きくても実行可能です。またメーター読み取りなどカメラごとに違うカスタマイズしたモデルも実行可能です。 ・メーター読み取り 4.エッジ(AI Box)静止画認識 認識間隔が数分など比較的長い認識のパターンです。エッジ(カメラ内)静止画認識に比べてより大きなカスタマイズモデルを利用することもできます。 ・メーター読み取り 5.クラウドストリーミング動画認識 クラウドでストリーミングで動画を認識するパターンです。サーバーのインスタンスにつねに大きな負荷を掛けるため大きなランニングコストになります。ランニングコストが問題にならない高付加価値な認識機能やPoCなどでは利用できるパターンです。 ・動きを伴う分析(人・車両・ベルトコンベア上のモノ) ・姿勢・アクション検出 ・属性(車種) はできる 以下は映像の解像度が低いため精度で不利 ・属性(人) ・認証(顔・ナンバープレート) 6.クラウドストリーミング静止画認識 クラウドでストリーミングで静止画を認識するパターンです。複数のデバイスを一つのインスタンスで認識する場合にはカメラごとに認識タイミングをずらすなどの工夫は必要ですが、動画に比べると負荷が低くランニングコストはかなり低くなります。クラウドで処理する場合1つのインスタンスで多数のカメラの静止画を分析するため、カメラごとにカスタムモデルを利用する場合には認識ごとにモデルの切り替えコストが大きくなることに注意が必要です。 ・人検出 ・河川氾濫判定 ・マルチモーダルLLMによる認識 7.クラウドストリーミング外部トリガー認識 外部のセンサーなどをトリガーとして、動画もしくは静止画の認識を行うパターンです。例えば、車両を検出するセンサーが反応した際に、クラウド側で車両のナンバープレートを認識するなどのパターンがあります。 ・ナンバープレート認識(外部センサーとの組み合わせ) 8.クラウドバッチ外部トリガー動画認識 クラウド上に撮りためた動画データをUI上で認識期間を指定して認識するパターンです。ある期間における人の動きや交通量を改正したい場合に利用するパターンです。クラウドストリーミング動画認識と比べるとある一定の期間の動画データに対してのみクラウドで認識を行うので比較的コストを低く抑えることができます。 ・期間を限った人の流れ認識 ・期間を限った交通量分析 9.エッジ(カメラ内)・クラウドハイブリッド認識 カメラ内でリアルタイム性が必要な画像認識を行い、更に詳細な認識をクラウド側で行うパターンです。リアルタイム処理はカメラ内で行い、クラウドでの詳細認識は低頻度であるためランニングコストを抑えることができます。一方で、システムが複雑なため開発難易度が上がるというデメリットもあります。 ・顔認証 エッジ(カメラ内):人検出、顔切り出し クラウド:顔特徴量マッチング ・車番認識 エッジ(カメラ内):車両検出、ナンバープレート切り出し クラウド:ナンバープレート認識 まとめ 本記事では画像認識システムのサービングパターンにおける要素を示し、その組み合わせから実際に利用できる主なサービングパターンを挙げました。本記事で扱った要素の組み合わせは、主にSafieのシステムを想定しています。みなさんが扱うシステムによって事情は異なってくるとは思いますが、要素に分けて組み合わせで考えることで比較検討を行い適切なシステムを選択することができると思われます。 最後に セーフィーではエンジニアを積極的に募集しています。どのような職種があるのか気になる方はこちらをご覧ください! safie.co.jp カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 皆様のご応募、心よりお待ちしております! 最後までお読みいただき、ありがとうございました。
アバター
はじめに こんにちは。 セーフィー株式会社 先行開発Gの井上です。 今回は、タイトルの通り llama.cpp を使用して MiniCPM-o-2_6 をローカル環境で動作させる方法 について解説します。ローカルでの動作環境を簡単に構築できる手順を紹介しますので、ぜひ参考にしてください。 はじめに 用語解説 llama.cppとは? MiniCPM-o-2_6とは? CMakeとは? CMakeを使用する利点 PCスペック・環境 実装の前準備 CMakeの導入方法(Windows 11基準) MiniCPM-o-2_6の用意 llama.cppの導入方法 Windows 11環境での導入手順 実行してみる 実行結果 ① Developers Summit 2024 Summerでの集合写真 所要時間 質問文 回答 ② セーフィーが掲げる映像プラットフォームの概念図 所要時間 質問文 回答 最後に 最近のマルチモーダルAIの発展に伴い、ローカル環境でも手軽に動作させたいというニーズが増えています。MiniCPM-o-2_6 はオープンソースのマルチモーダルAIであり、ローカルでの実行も可能です。本記事では llama.cpp を使用し、Windows 11 環境で MiniCPM-o-2_6 を動かす手順をまとめました。 また、環境構築に CMake を利用することで、ビルドや依存関係の管理が簡単になり、手軽にセットアップできる点も魅力です。CMake を活用することで、複雑な設定をすることなくスムーズに環境を構築できるため、本記事の方法を採用しました。 用語解説 llama.cppとは? llama.cpp は、オープンソースの軽量な LLM(大規模言語モデル)推論フレームワークであり、Meta社が公開した LLaMA(Large Language Model Meta AI)シリーズのモデルをローカル環境で実行するために開発されました。このフレームワークは、特に低リソース環境向けに最適化されており、GPU を使用しなくても CPU 上で高いパフォーマンスを発揮できるのが特徴です。llama.cpp は C++ で実装されており、Windows、Linux、macOS などの主要なプラットフォームで利用可能です。 MiniCPM-o-2_6とは? MiniCPM-o-2_6 は、オープンソースのマルチモーダル大規模言語モデル(MLLM)であり、テキスト、画像、音声などの複数のモダリティを処理することが可能です。このモデルは、エッジデバイスでの運用にも適した軽量な設計がされており、比較的低スペックなマシンでも動作できるのが特徴です。また、OCR(光学文字認識)機能が強化されており、最大1344×1344ピクセルの画像を処理可能なため、画像解析や視覚情報の理解にも活用できます。 llama.cpp はローカルで LLM を実行する際に非常に有効なツールであり、特に 軽量・シンプルな導入が可能 な点が優れています。そのため、本記事では llama.cpp を使用して MiniCPM-o-2_6 を動作させる方法を選択しました。 CMakeとは? CMake は、プログラムのビルドを管理するためのクロスプラットフォームツールです。C++ などのプロジェクトでコンパイル・ビルドを簡単に行うために使用されます。 CMakeを使用する利点 CMake を使用する最大の利点は、クロスプラットフォーム対応であり、Windows、Linux、macOS など異なる OS でも統一的なビルド環境を提供できる点です。また、柔軟なビルド設定が可能で、CMakeLists.txt を記述することで、複雑なプロジェクトの管理が容易になります。 さらに、CMake は依存関係の管理にも優れており、外部ライブラリを簡単に導入し、統一されたビルド環境を構築することができます。これにより、異なる開発者や異なる環境でのビルドの再現性が向上し、開発の効率化が期待できます。 また、CMake を使用することで、ビルドの自動化や再利用性が高まり、同じ設定を異なる環境でも適用できるため、複数のプロジェクトや開発チーム間での統一した開発環境を維持しやすくなります。加えて、CMake は Visual Studio、Makefile、Ninja など、多くのビルドシステムとの互換性があり、環境に応じたビルドが容易に行える点も大きな利点です。 PCスペック・環境 CPU: Intel(R) Core(TM) i7-1355U 1.70 GHz メモリ: 32.0 GB OS: Windows 11 64bit エディタ:VisualStudioCode 実装の前準備 CMakeの導入方法(Windows 11基準) CMake公式サイト から最新のバージョンをダウンロード インストーラーを実行し、システム環境変数にパスを追加 ここで自分は「Add CMake to the system PATH for the All user」を選択しました。 保存先を聞かれるので、任意の好きな場所を選択 インストール完了後、ターミナルで cmake --version を実行し、正常にインストールされたことを確認 cmake version 3.31.4(筆者の環境)と出ました! これでCmakeの準備は完了です! MiniCPM-o-2_6の用意 今回必要なモデル Model-7.6B-Q4_K_M.gguf mmproj-model-f16.gguf モデルの導入手順 HuggingFace に上記MiniCPM-o-2_6モデルのggufファイルが公開されているのでダウンロードします huggingface.co 上記サイトへアクセスします 赤枠で囲ってあるFiles and versionを押下します モデル一覧が表示されます。 ここで、 今回必要なモデル 項に記載している2つのモデルをダウンロードします。 ダウンロード後、どこか分かり易い場所へ一時保管しておきます llama.cppの導入方法 下準備が全て完了したので、ここからはいよいよllama.cppの導入方法について解説していきます! github.com といっても、llama.cppのgithubからクローンしてCmakeを実行するだけで構築できちゃいますので、作業自体は下準備項よりも早く終わります Windows 11環境での導入手順 ローカルに作業用ディレクトリを作成します。 mkdir llama_test cd llama_test 作成したllama_testディレクトリ上で llama.cpp のリポジトリをクローン。 完了後、llama.cppディレクトリへ移動します。 git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp llama.cppディレクトリ上で以下コマンドを入力し、CMakeを使用してビルドを行います cmake -B build cd build cmake --build . --config Release ※cmake --build . --config Release はかなり時間がかかります 上記コマンドを上から順に実行すると、llama.cppに「build」フォルダが作成されます。 モデルの準備項で用意しておいた2つのモデルをbuildフォルダに格納します。 格納場所はどこでもOKです。自分は以下の場所にモデル格納用のディレクトリを作成し、格納しました。 llama.cpp/build/bin/ models/MiniCPM-o-2_6/ 画像保管場所を用意します。 次項にも記載していますが、画像を解析させるために画像のパスを教えてあげる必要があります。任意の場所に画像用フォルダを作成し、指定できるようにしておきましょう。 筆者は以下のように画像保管フォルダを作成しました。 llama.cpp/build/bin/ images/ これで実行準備が全て完了しました!次項から早速触ってみましょう! 実行してみる 実際にモデルを動作させるために、以下のコマンドを実行します。 ※筆者はbuildディレクトリ上で実行しているので、以下のパス構成になっています。逐次書き換えてください ./bin/Release/llama-minicpmv-cli -m **./bin/models/MiniCPM-o-2_6/Model-7.6B-Q4_K_M.gguf** --mmproj **./bin/models/MiniCPM-o-2_6/mmproj-model-f16.gguf** -c 8192 --temp 0.7 --top-p 0.8 --top-k 100 --repeat-penalty 1.05 --image **./bin/images/001.png** -p "how many people are in the image" --n_predict 8192 今回解析してもらう画像は次の2つです 実行結果 ① Developers Summit 2024 Summerでの集合写真 2024年にブース出展したDevelopers Summit 2024 Summerでの集合写真です! この時のブース出展では、 Safie One や Safie GO 360 、 Safie Pocket2 Plus をブースに展示していました。 展示したプロダクトについても是非ご確認ください! safie.jp safie.jp safie.jp 所要時間 1分以内(50秒程) 質問文 how many people are in the image 回答 There are four people in the image. They appear to be standing behind a booth, possibly promoting or demonstrating something related to 'safie'. Each person is holding up pamphlets with Japanese text and pictures of owls on them. The setting includes posters and banners about 'safie' technology, suggesting it might be an event or exhibition where they are showcasing their product or service. The individuals seem engaged in presenting information about the company's offerings, as evidenced by the materials they're holding and the visual aids set up around them. There is also a table with more pamphlets available for visitors to take. The overall atmosphere indicates a professional presentation aimed at informing attendees about 'safie' technology or services. In summary: Four people are visible in the image. They appear to be part of a promotional event for their company, possibly called 'safie'. Their actions suggest they're explaining features related to owls and video management. 画像には4人が写っています。彼らはブースの後ろに立って、おそらく「safie」に関連した何かを宣伝またはデモンストレーションしているようです。それぞれが日本語の文字とフクロウの絵が描かれたパンフレットを掲げている。この設定には「セーフィー」テクノロジーに関するポスターやバナーが含まれており、自社の製品やサービスを紹介するイベントや展示会である可能性があることを示唆しています。 彼らが手にしている資料や周囲に設置された視覚補助具からわかるように、各個人は同社の製品に関する情報の提示に熱心に取り組んでいるように見えます。訪問者が手に取れるパンフレットが置かれたテーブルもあります。全体的な雰囲気は、参加者に「安全」テクノロジーやサービスについて知らせることを目的とした専門的なプレゼンテーションを示しています。 要約すれば: 画像には 4 人が表示されます。 彼らはおそらく「safie」と呼ばれる、自社のプロモーションイベントの一部であるようです。 彼らの行動は、フクロウとビデオ管理に関連する機能を説明していることを示唆しています。 ② セーフィーが掲げる映像プラットフォームの概念図 セーフィーの事業紹介にも掲載していますので、興味があれば以下サイトも確認してみてください safie.co.jp 所要時間 2分程 質問文 describe the image 回答 The image is a detailed infographic in Japanese, featuring various applications of video data. The central element is the logo and name "safie," which appears to be a platform for integrating diverse analysis applications related to video data. Surrounding this central theme are several circular images with corresponding text labels that illustrate different use cases: Restaurants : There's an image showing tables in a dining area, possibly indicating how video data can help monitor customer behavior or optimize operations. Hospitals : An image of hospital beds suggests the application could involve patient monitoring and facility management within medical settings. Street View / Tourism Locations : This includes images that might represent tourist spots or public spaces where surveillance or crowd analysis is beneficial. Additionally, there are other applications mentioned: Business Facilities Crime Prevention Construction Sites Flood Monitoring The infographic also mentions practical uses like sending out educational videos and calculating parking fees based on footage. Each application area has a visual representation to help understand its context better within the overall platform's capabilities. Overall, it appears that "safie" is designed as an integrated solution for various industries looking to utilize video data efficiently across multiple sectors such as hospitality, healthcare, security, education, and more. 画像は、映像データのさまざまなアプリケーションを紹介する、日本語の詳細なインフォグラフィックです。中心となるのは「safie」というロゴと名前で、映像データに関連する多様な解析アプリケーションを統合するプラットフォームと思われる。 この中心テーマの周囲には、さまざまな使用例を示す対応するテキスト ラベルが付いたいくつかの円形の画像があります。 レストラン : ダイニング エリアのテーブルを示す画像があり、映像データが顧客の行動の監視や業務の最適化にどのように役立つかを示している可能性があります。 病院 : 病院のベッドの画像は、このアプリケーションに医療現場での患者の監視と施設管理が含まれる可能性があることを示唆しています。 ストリートビュー / 観光地 : これには、監視や群衆分析が有益な観光地や公共スペースを表す可能性のある画像が含まれます。 さらに、他のアプリケーションについても言及されています。 商業施設 防犯 建設現場 洪水監視 インフォグラフィックでは、教育映像の送信や映像に基づいた駐車料金の計算などの実用的な用途についても言及しています。各アプリケーション領域には視覚的な表現があり、プラットフォーム全体の機能内でそのコンテキストをよりよく理解できるようになります。 全体として、「safie」は、ホスピタリティ、ヘルスケア、セキュリティ、教育などの複数の分野にわたって映像データを効率的に活用したいと考えているさまざまな業界向けの統合ソリューションとして設計されているようです。 最後に 本記事では、ローカル環境で llama.cpp を使用して MiniCPM-o-2_6 を動作させる方法 について解説しました。 ローカルで LLM を実行する方法を探している方にとって、有益な情報になれば幸いです。 セーフィーではエンジニアを積極的に募集しています。どのような職種があるのか気になる方はこちらをご覧ください! safie.co.jp カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 皆様のご応募、心よりお待ちしております! 最後までお読みいただき、ありがとうございました
アバター
はじめに セーフィー株式会社AIソリューションプラットフォーム推進室の植松です。 2024年12月に実施した セーフィーアドベントカレンダー でCTO 森本からご紹介がありましたとおり、AIソリューションプラットフォームが 経産省プロジェクト(IR資料) として採択されたことをきっかけに、このプラットフォーム化に会社としてさらに注力して取り組むことになりました。 engineers.safie.link その注力施策の一つとして、今年の1月から新たに部署を作りプロジェクトを進めていますので、今一度全体像と、事業ロードマップ、経産省プロジェクトにおける実証実験の取り組み状況、認知施策についてお話しできればと思います。 はじめに AIソリューションプラットフォーム全体像 事業ロードマップ 経産省プロジェクトにおける実証実験について テーマ1. 鹿島建設様 テーマ2. 清水建設様 テーマ3. 慶睦会様 認知施策 生成AIの取り組みについて 最後に AIソリューションプラットフォーム全体像 こちらは 2024年12月期第3四半期決算資料 でも展開させていただきましたが、データ利用を簡単に、AI生成・再学習を簡単に、ビジネスを簡単に〜AIビジネスを量産できる仕組みを我々のプラットフォームとして提供し、データホルダーとAI開発者の連携を実現するような場の構築、提供を行うことを想定しています。 事業ロードマップ まずは国内、映像データの活用はファーストステップですが、それに留まらず海外展開や、映像に音声・センサデータなどのデータを組み合わせたマルチモーダル対応を進めていく予定です。 事業化に関するもう少し深掘りした話については、 IR noteの方でも記事にしております ので、ご興味ある方はそちらもご覧いただければと思います。 note.com 経産省プロジェクトにおける実証実験について AIソリューションプラットフォームの開発と合わせ、3つの実証実験を行うことでプラットフォームの確からしさを立証していこうと考えています。 残念ながらプロジェクト途中のため写真が公開できないのですが、どのテーマも現場の課題をデータ活用によりなんとか解決したい、という各社の熱い思いを持って取り組んでいます。 テーマ1. 鹿島建設様 クレーン作業において、荷物を持ち上げる時(玉掛け作業)の不安全行動を防ぐため、3.3.3運動(玉掛けして3秒確認、玉掛け者は3m以上離れる、荷の安定確認を30cm以内にする)ということを基本としているのですが、その状態が守られているかを検知したい、という事象に取り組もうとしています。 また、安全な状態を保つこと(囲いがあるか、開口部が開きっぱなしになっていないこと、立ち入り禁止箇所がきちんと守られていること、など)に対しても現在どの事象に対して取り組むか?を整理しています。 テーマ2. 清水建設様 トンネル工事において、コンクリートを運んでくる生コン車や、トンネルの一部を構成するセグメントと呼ばれるものを載せた車などが、いつ入ってくるか?がわからず、待機状態が長いのでそれをもっと効率化するため検知したい、という事象に取り組もうとしています。 テーマ3. 慶睦会様 介護施設で転倒してしまった場合に介助しにいく必要がありますが、特に夜間は待機しているスタッフも少なく、何度も見回りにいく負荷も高いため、転倒時に通知がくるような仕組みを作ってスタッフさんの負荷を減らしたい、という事象に取り組もうとしています。 いずれのテーマも、実際に実証実験現場に伺うことで、取り組もうとしている課題以上に様々にAIを量産できたらやれそうなことが沢山出てきます。やむなく今回はテーマを絞っていますが、プラットフォーム化でさらに課題解決に繋げられそうだと感じています。 認知施策 プラットフォームをシステムとして作ると同時に、それをプラットフォームとして使ってくださるデータホルダー(お客様)、AI開発者を集める施策が必要となってきます。 昨年から弊社主催のカンファレンス、通称レゾサミ( レポートはこちら )を行っています。 note.com 今までは主にデータホルダーであるお客様向けでしたが、今年はAIソリューションプラットフォームという軸で上記の実証実験や、AI開発者様と実施している取り組みを紹介していく登壇やデモブースを設け、さらにAI開発者の方も興味を持っていただけ楽しめるようなイベントを設計していく予定です。 また、セーフィーでは様々な業界向け展示会に出展しており、直近ですと リテールテックJAPAN にこの取り組みを展示する予定です。 messe.nikkei.co.jp それらに加え、AIの展示会への登壇や技術系の認知施策にも取り組んでいきたいと思います。セーフィーにはショールームもあるので、そこにもこのプラットフォームの内容がお目見えする予定ですので、興味のある方は是非会社の方にもお越しいただければと思います。 本Tech Blogでも今後定期的にプロジェクトのアップデートや実際の仕組み、開発状況などをお伝えしていきます。 生成AIの取り組みについて 経産省プロジェクトとしては、AIモデルを簡単に作れる、いわゆるMLOpsと呼ばれる仕組みを中心に取り組んでいるのですが、同時に生成AIを活用した仕組みに関しても合わせてプラットフォームとして提供できないか、ということを並行して検討しています。 まずはデモとしてお客様の反応を確認しつつ、ビジネスとしてうまく軌道に載せられるような探索を繰り返していきたい、と考えています。 最後に セーフィーが様々な大企業の方々にご支援いただいてここまで大きく成長できたように、今度はこのAIソリューションプラットフォームを使ってスタートアップの方の支援ができるような形を目指しています。 AIソリューションプラットフォームを構築し、またプラットフォームを使って様々なプロダクト・サービスを提供するには、AIだけでなくその他領域のエンジニアやプロダクトマネージャーも合わせて必要です。 サーバ・フロントエンドエンジニアはもちろん、映像データを取得するために欠かせないカメラやその周辺デバイスを担当するIoTエンジニア、ユーザ動向の調査に欠かせないデータ分析エンジニア、様々なオペレーションを支える業務システムエンジニア、品質を上流からもささえるクオリティマネジメント担当、各領域のプロダクトマネージャーなど、様々なポジションで採用をかけています。 この記事を読んで興味を持っていただいた方、カジュアルに話してみたい、等で結構ですので、 是非こちらの採用サイトから コンタクトいただけると幸いです! safie.co.jp
アバター
はじめに セーフィーの髙木( @hitsan8 )です。 セーフィーは2025年2月13、14日に行われたDevelopers Summit 2025(以下デブサミ)にブーススポンサーとして参加しました。 デブサミとは2003年から開催されているITエンジニアのための祭典です。今年のテーマは「 ひろがるエンジニアリング 」です。 event.shoeisha.jp 多くの方にセーフィーを知ってもらうために2023年から参加し続けて今年で3回目になりました。 この記事はデブサミのブーススポンサー参加レポートになります。 はじめに ブースのデザイン アニマルマスクのデモ 展示パネル さいごに ブースのデザイン 2024年にロゴをリニューアルしたのでブースで使用する備品、ノベルティを一新しました。 去年までは白をベースにしたデザインでしたが、今年はコーポレートカラーであるセーフィーブルーを基調としたデザインに仕上げています。 例えばバナースタンドはこのようなデザインになっています。 実際のブースに並べてみるとこんな感じになります。 全体の色味をそろえたので統一感がでたのではないでしょうか。 実際にお客さんに説明している様子はこんな感じでした。 ブースではアニマルマスクのデモとパネルの展示をしました。 こちらもそれぞれ紹介していきます。 アニマルマスクのデモ プライバシーマスクという機能があり、これは個人の特定を防ぐために映像の一部を隠す機能です。 通常は映像を隠すためにモザイク処理することが多いですが、今回は検出した人の上に動物のイラストを被せてアニマルマスクとして展示しています。 会場では三脚を使って俯瞰で撮影するようにしました。 この画角がセキュリティカメラっぽいですね。 実際にアニマルマスクを動かすとこんな感じです。 自分の顔が動物になっているのでお客さんの目を引くことができました。 こちらはお客さんや他のブースの方々から面白い、かわいいというお声をいただきました。さらに、内部のアルゴリズムやセキュリティについて興味を持たれた方も多かったです。 展示パネル 今回はアニマルマスクのパネルと技術スタックのパネルを展示しました。 どちらもコーポレートカラーを基調としたデザインにしています。 このパネルは動物たちの質問にセーフィーくんが回答してくれるという内容になってます。 楽しそうな雰囲気が伝わってきますね。 技術スタックのパネルはエンジニアの方々に好評でした。 「セーフィーではこれを使ってるけどうちの会社ではこれを使ってる」みたいな会話が生まれていました。 さいごに 2日間のブース出展を無事に終えることができました。実際にプロダクトを見てもらい、その場でフィードバックをもらえたので非常によい体験でした。 スタッフの感想としては アニマルマスクでエッジの処理にも興味を持ってもらえてよかった。 会場が広くてきれいだったのでおどろいた。 大変だったけど楽しかった。 ノベルティがかわいいと好評でよかった。 今後の改善点として挙がったのは アニマルマスクの動物を検出した人の属性で振り分けけてもよかったかもしれない 生成AIも組み合わせたら面白かったかも マスコットのぬいぐるみを置きたい ゲーム要素も入れたかった セーフィーは今後も認知拡大のため継続的にイベントに参加します。 セーフィーはエンジニアを積極的に募集しています。ご興味がある方は採用ページをご覧ください。 safie.co.jp
アバター
はじめに こんにちは!セーフィー株式会社でデバイス開発をしている杉本です。 セーフィーでは普段の業務以外にも、エンジニアのスキルアップのために様々な取り組みを行っています。その一つの取り組みとしてエンジニアの「やってみたい!」をボトムアップで実現する活動をしています。 今回はコンセプトの立案からプロトタイプのユーザー検証まで行うプロト開発WG(ワーキンググループ)の活動をご紹介します! はじめに プロト開発WGの目的 やったこと 他部署からのフィードバック やってみて 最後に プロト開発WGの目的 セーフィーはクラウドカメラサービスを提供しており、様々な現場のユーザーの課題に向き合って新プロダクトの企画・開発を進めています。 営業、企画からの提案で開発をすることが多いのですが、エンジニアだからこその気付きもあります。ユーザーが気付かないような視点からエンジニアが提案ができる組織を実現すべく、プロト開発WGを開催しています! 今回のワークショップの目的は以下のとおりです。 エンジニア自身でユーザーの課題の仮説を立てて検証し、プロト開発の知見を得ること 業務で使用する技術以外の技術領域にチャレンジしてスキルの幅を広げること やったこと プロト開発WGではコンセプト(仮説)の立案、プロト開発、ユーザー検証の大きく3つのプロセスで進めており、今回のプロト開発WGではデバイス開発部の有志メンバーにて計5か月の期間で実施しました。 コンセプト(仮説)の立案 約2カ月 ユーザーの想定課題 想定課題に対するユーザーへの提供価値 プロト開発 約2カ月 コンセプトを実現するための技術検討 コンセプトを試すためのプロトを開発 ユーザー検証 約1ヶ月 ユーザーにプロトを試してもらい、フィードバックを得る フィードバックから課題を抽出して改善の方向性を特定する コンセプトの立案ではエンジニアならではの視点でアイデアが合計12個出てきたのですが、そこからの絞り込みが苦労しました。ユーザーの課題をどう解決できるのか、どれだけユーザーにとって価値があるのか、などの深堀をしてコンセプトの絞り込みを進めました。 最終的には提供価値だけでなく、技術的に実現ができそうか、他社とは違いがありそうか、今後も活用できそうかの4つの観点で5段階で点数付けをしてコンセプトを1つに決定しました。 このような議論を通じてエンジニアが提案できる組織につながるような視点を養うことができたのではないかと思っています! また、本活動で実際に立案したコンセプトや開発プロトタイプについてはありがたいことに商品化検討のステップにつながっているので、残念ながら本ブログでご紹介することができません。 ただ、ユーザーの課題に着目してコンセプトをエンジニア自身が考えたため、大変ながらも楽しくプロトタイプの開発ができました! 他部署からのフィードバック ユーザー検証に協力いただいた各部署の方から本活動についてたくさんのフィードバックがありましたので一部抜粋して公開します! とてもいい活動だと思いました。営業側では思いつかない視点もありありがたいです。 提案内容が突拍子もないものではなく、既にある機器の拡張や改造で行けるという、夢と現実のはざまの大変バランスの良い所を攻めているので、商品化の可能性が高いと思いました! 技術オリエンテッドな提案は営業企画としても刺激されるとおもいます。なにより活動メンバーが楽しそうにしていたのが印象的でした! 非常に面白かった。今すぐ商品に繋がらないことも多いと思いますが、このようなことをやって今後の商品につながる種をいっぱい作って欲しいです。 営業、企画スタートではなく、開発から発信したほうが固定概念に縛られないモノが作れる場合があるし、営業・企画が暗黙のうちに諦めたり後回しにしたものが作られたりするので、今後も継続してほしいです。 想定機能を型にすることで、もっとイメージしやすいです。「こんなこともできることだ」を直感でき、ペーパーより商品に対する理解が深くなる。また、実現可能性も参考になります。 日々とてもお忙しい中、アイディアを形にする時間を取るのは非常に大変だったと思いますが、ここまでWowのあるものを短期間で仕上げており本当に素晴らしいと思いました。 開発側の方がお持ちのアイディアと顧客課題をうまく紐づける活動をしていきたいと考えているので、こういった事がやりたい!これってニーズありそう?といった話はお気軽にしていただければと思います! 非常に有意義な時間でした!プロトタイプと言いつつ、商品建て付け考えればそのまま売れるのではと思うくらいのクオリティだったのですぐ企画と連携して進めていきたいくらいです。 最終的にプロトタイプの実機デモと合わせて、セーフィー全社員向けのプロト開発WGの報告会を実施しました。 ありがたいことに、オフライン、オンラインのハイブリッドで開催したのですが、100名以上の方に参加いただいて報告会の後も質問が絶えないなど大盛況で終えることができました! 全社員向けのプロト開発WG報告会の様子 やってみて 参加メンバーにて本活動の振り返りを実施しましたので一部抜粋して公開します!当初の目的についてはおおむね達成できた結果となり、一安心です! 提案したプロトタイプ4つのうち、3つは具体的にユーザーの現場でのPoCにつながるなどエンジニアのボトムアップの活動から実際の商品化に向けて動き出すという予想以上の結果を出すこともできました! 反省点としては記載の通りですが、通常業務をしながら本活動の時間をなかなか割くことができずにプロトタイプの開発に苦労してしまったことです。良かった点を活かしつつ次回の活動では改善をしていきたいと思います! Keep 全員しっかり関わりながら、期間内にコンセプト立案~開発~実機デモまでやりきることができた 業務ではあまり使わないプロダクトや技術の知見が得られた 営業、企画への実機で技術提案をすることで、プロダクト化に向けた前向きな議論ができた Problem プロジェクトなどの主務の忙しい時期と重なると本活動に割り当てる時間が確保するのが難しく、進捗がない時期が発生してしまった トライ&エラーでの実装となり、予想よりも開発に時間がかかってしまった Try 開発期間は2ヶ月ではなく3~4ヶ月は確保する 新しい技術に挑戦するため、開発期間に余裕が必要なため タスクと担当を明確にして計画を立てて進捗管理する トライ&エラーでの開発が前提だが、計画を立てて進めることで効率的に開発をするため 本活動はトライアルとしてデバイス開発部にて閉じて実施したのですが、他の部署からも参加したいという声が挙がってきており、ゆくゆくは開発本部横串での活動に広げていきたいと思っています。 最後に 本記事では、プロト開発WGの活動についてご紹介させていただきました。 セーフィーではエンジニアを積極的に募集しています。どのような職種があるのか気になる方はこちらをご覧ください! safie.co.jp カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 皆様のご応募、心よりお待ちしております! 最後までお読みいただき、ありがとうございました。
アバター
はじめに こんにちは。 セーフィー株式会社でサーバーサイドの開発をしている金成です。 今回は、サーバーチームで「マイクロサービスアーキテクチャ第2版 」 を題材に、輪読会を開催したので、紹介させてください はじめに 背景 実装/実行 やってみて 終わりに 背景 セーフィーは、サービスの拡大と開発者の増加に伴い、工数の増加やリリースリスクの増大など、開発生産性の問題を抱えています。この問題の解決のために、組織の再編やコードベースの分割によるマイクロサービス化が進行しています。 私を含め開発チームのメンバーには、マイクロサービスとはなんなのか?何を目指してるのか?どういったメリット・デメリットをもたらすのか?などイメージがついていない部分がありました。 そこで、今回の「マイクロサービスアーキテクチャ第2版」を参考に輪読会を開催し、このトピックに対する理解度を深めようとしました。 実装/実行 開催の形式は、下記の通りです 週1回 30-45分で開催 1つの章を取り上げ、参加者は事前に読む 議論したい話題を取り上げ、自由に議論する 議論した内容を簡単に残し、後で見れるようにする 全16章、4ヶ月ほどかけて全ての章で実施しました。 議題は マイクロサービスにおける一番の目的は何か、どんな指標を持つべきか REST/RPC/GraphQL/メッセージキューの使い分け サーガパターンとその適用するケース コンウェイの法則と逆コンウェイ戦略について などがあり、技術的な側面から組織面まで様々な議論をしました。 弊社ではドキュメント管理にnotionを利用してるので、下記のようなテーブルを作成して、議題を管理しています。 下記のように笑顔の絶えない議論が展開され、4ヶ月間笑いが絶えることはありませんでした。 — アットホームな職場の写真サンプル(タノシイヨ, コワクナイヨ) やってみて 輪読会後にアンケートをとったところ、下記のような結果が得られました。 よかったところ マイクロサービスへの理解が深まった 参加者の実務経験から周辺知識を補うことができた 対話の機会が増えた 問題点・改善点 事前の準備の時間が足りなかった 発言の機会が足りなかった 時間が足りなかった 事前準備や議論のファシリテーションには問題が残りますが、マイクロサービス化への理解を深めるという当初の目的を達成できたこと、リモートが多い弊社で対話の機会を増やせたことは一定プラスになったかと感じてます。 終わりに 自分の思い付きから企画した輪読会でしたが、参加してくれたメンバーの満足度は概ね高く、やってよかったと感じました。 実は、私もこの秋からマイクロサービス化を進めるチームに参画しています。マイクロサービスについて知らないままだと、仕事がうまく進められません。だから「マイクロサービスアーキテクチャ」を読む必要があったんですね。 セーフィーではエンジニアを積極的に募集しています。気になる方はこちらをご覧ください! safie.co.jp この記事を読んでもし興味を持っていただけた方は、ぜひ採用サイトもご覧ください。 カジュアル面談のみでも大歓迎ですので、お気軽にご連絡ください。 最後までお読みいただき、ありがとうございました。
アバター
はじめに こんにちは!QCDグループに所属している小熊です。 Safie Viewer for PCのQA(システムテスト)を2019年9月頃から担当しているのですが、悩みのタネであった「度重なるバージョンアップによって増え続けるリグレッションテスト項目」の解消を目指してトライしてみたことについて書こうと思います。 セーフィーにおけるQCDグループはどういう事をしているのか?を紹介している記事もございますので、是非こちらもご確認ください! engineers.safie.link はじめに リグレッションテストについて 暗黒時代 「整理整頓」時代 その後(現在) まとめ リグレッションテストについて Safie Viewer for PCは月イチで新バージョンがリリースされており(過去はもっと頻繁に(2~3週間に1回程度)リリースされていた)、その度に新機能追加や不具合修正などが既存機能に悪い影響(意図していない影響)を与えていないことを確認する必要が有ります。 Safie Viewer for PCは歴史が古いこともあって機能数が多くて思わぬところに開発影響が出たりするため、範囲や対象を絞り込まずに全てのテスト項目の実施(フルリグレッションテスト)をリリース毎に行っています。 暗黒時代 リリース毎に新機能もしくは改修が入るので、リグレッションテスト項目内容もそれに合わせて更新し続ける必要があります。 具体的には新機能は一度リリースされたら既存機能に含まれるのでその内容をリグレッションテストに落とし込んでいるのですが、その度にテスト項目は増え、増え、増え、と増え続け、これまで一番ひどかった時期の項目数をピックアップしたら下記表な感じでした。 リリース月 実施項目数 2022年7月 1083 2022年12月 1230 2023年3月 1584 2023年8月 2102 1年で実施項目数が約2倍!こりゃだめだ。。 この件について頭を悩ませていたポイントを下記にまとめてみました。 テストの実施に時間がかかる 項目ごとに優先度を割り振り、優先度低い項目は毎回は実施しないようにしていたがそれでも限界 テスト項目の視認性が悪くてやりにくい テスト項目のメンテナンスに時間がかかる 1と共通点が多い問題、メンテナンス後に項目レビューするのも大変 テスト自動化対応しにくい テスト自動化対応しにくい 自動化できる/できない(しない)の精査、自動化した後の管理が大変 これとは別に新機能/改修向けのテストの準備や実施が有り、特にテスト期間中はテスト項目消化に追われていた(探索的テストなど、他のことに割く工数が無い) これらによって改善活動(テスト項目の整理整頓)にリソース割くのが困難、改善しないので更に項目が増えていき、さらにリソースが逼迫するという悪循環に陥ってました。 「整理整頓」時代 暗黒時代の中、ただ手をこまねいていたわけでは無く、課題解決のためにはどのような方針で整理整頓する必要があるのかをQAメンバー全員で議論や準備を少しずつ進めていたところ、ある転機がやってきました。 engineers.safie.link ↑の記事にあるテスト管理ツールの導入イベントです。 これまでスプレッドシートでテスト項目を作成&実施していたものを管理ツールで行うことが決定、せっかく管理ツールへの移行に工数割くなら準備してきた改善案を適用した内容でやってやろうじゃないかとなり、テスト実施にかかる以外の工数をほぼ全振りして整理整頓対応することとなりました。 その時の整理整頓の活動方針は これまでのテスト項目を優先度付けして、優先度高い内容を移行する新項目のメイン機能(骨格)とする 1で決めたメイン機能チェックは、なるべくシンプルにまとめる(肉付け) 肉付けの際は「自動化できる(自動テストのみで完結できる内容)」「できない or 困難(メンテナンスに時間かかるなど)」のように自動化可否を分けて(混在しないように)行う の3点です。 特に①と②の対応に苦心しました。 どの機能を骨にして(いわゆる優先度付け)どれだけ肉付けするかは、「これまでで実施してきた項目」から「どれだけインシデントが検知できてきたか」の分析を行ってその結果を指針にしたり(新しい項目によって実施工数は減ったけどリリース後に見つかるバグが増えた、は許されない)、①や②の対応後に「これまでのインシデントが検知できる内容になっているか」の見直しを行ったり、それをメンバー間でレビューしあったり(①②の実施は各メンバーの感覚によってバラつきが有るので)、とにかく試行錯誤の連続でした。 また、テスト項目での記載内容によってちょっとでも項目を減らすように工夫しました。たとえば「画面AでボタンAの確認」「モーダルBで機能Bの確認」とぶつ切りになっていた2つの項目を「画面AでボタンAを押下した後のモーダルBで機能Bの確認」のように一連の流れで網羅できるような1つの項目へ組み替えたりです。 その後(現在) 整理整頓を頑張り始めてから、その後項目数はどうなったでしょうか。 リリース月 実施項目数 2023年8月 2102 2024年5月 641 2024年12月 665 ピークから3分の1以下まで減りました(拍手!) ただし気を抜くとあっという間に以前に逆戻りしてしまう(実際、5月から少しずつ増えている)ので活動の継続は必要なのと、浮いた工数をどのように有効活用していくかも今後の課題のひとつです。 まとめ 今回の対応が最適解とせず、メンバー内で整理整頓(①~③)への共通認識ができている今の内に更なる改善目指し、これからも試行錯誤進めていこうと思っています。(そのことをまたお話できたら良いなぁ)
アバター
はじめに セーフィー株式会社 でサーバーサイドエンジニアをやっております石塚です。 今回はGoogle Chromeの拡張機能に入門したので備忘録として残しておこうと思います。完全にプライベートで利用するための拡張機能を実装したので、ゆるりと呼んでいただけると幸いです。 はじめに 拡張機能の作り方 インストール ローカルで実行 実装 最後に 拡張機能の作り方 今回はGoolge Chrome向けの拡張機能を実装する方法を紹介します。 Google公式の拡張機能ドキュメント があります。詳しくはこれを参照してください。基本的にはHTML, CSS, JavaScriptで実装可能なので簡単に構築することができそうです。他には他に WXT や Extention.js , plasmo などの拡張機能向けのフレームワークを使う方法もあります。 今回は plasmo を選択しました。”the all-in-one platform that makes it easy for browser extension developers to create, test, and publish amazing extensions.“とあったので全部入りかつ簡単に導入できそうという安易な理由です。 インストール 動作環境は以下の通りです。 macOS Sonoma 14.7 node v18.16.0 npm 9.5.1 まずはプロジェクトを作成します。今回はnpmを使いますが公式ではpnpmの使用が推奨されているようです。 npm create plasmo 対話形式でプロジェクト名や説明などを設定します。 しばらく待つとプロジェクトが作成され、以下のようなディレクトリ構造になっているはずです。 / < PROJECT_NAME > ├── README.md ├── assets │ └── icon.png ├── package.json ├── popup.tsx ├── tsconfig.json └── yarn-error.log 意外とシンプルな構造ですね。 ローカルで実行 まずはローカルで実行してみましょう。 # 筆者の環境では不足していたので追加 $ npm install sharp # 実行 $ npm run dev > extention-sample@ 0 . 0 . 1 dev > plasmo dev 🟣 Plasmo v0. 89 . 4 🔴 The Browser Extension Framework 🔵 INFO | Starting the extension development server... 🔵 INFO | Building for target: chrome-mv3 🔵 INFO | Loaded environment variables from: [] ( node:29924 ) [ DEP0040 ] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. ( Use `node --trace-deprecation ...` to show where the warning was created ) 🟢 DONE | Extension re-packaged in 1734ms! 🚀 ビルドされ、開発サーバーで実行可能な状態になっているのでブラウザから拡張機能を有効化して動作確認してみます。 chromeの右上の3点リーダーから「拡張機能」、「拡張機能を管理」に進みます。拡張機能のページの右上に「デベロッパーモード」があるので、このトグルボタンをおして有効化します。 「パッケージ化されていない拡張機能を取り込む」から先ほどビルドの成果物を選択します。 ./build/hrome-mv3-dev に出力されています。 取り込むと以下の画像のようになります。 ブラウザから拡張機能をピン留めしてクリックすると作成した拡張機能が確認できます。 これで最低限の開発ができるようになりました! 実装 今回はサンプルとしてページがロードされたときにコンソールにURLを出力してみます。 mkdir contents code contents/content.ts 新しくcontents.tsを編集します。今回は以下のような実装にしました。 export {} ; window . addEventListener ( "load" , () => { const url = new URL ( window . location . href ); console .log( "URL:" , url. origin + url. pathname ); } ); ホットリロードが有効なのでコンソールを表示しながらページをリロードするとアクセスしているURLが表示されます! ビルドの成果物にはHTML, JavaScript, manifest.json(それから画像ファイル)が含まれています。manifest.jsonは拡張機能に必須のファイルですが、これはplasmoが生成しています。manifest.jsonの内容はpackage.jsonで定義できます。 このままではあまり意味のある拡張機能ではないですが、ビルドしていつでも使えるようにしてみましょう。 npm run build ./build/chrome-mv3-prod/ にビルド成果物が出力されます。 これをブラウザの拡張機能のページから読み込むと開発サーバーが起動していないときでも使えます。 拡張機能を広く公開するにはパッケージ化したり、Googleウェブストアで公開したりしないといけないですが、ここでは紹介しないのでぜひ 公式のヘルプページ などを参照して公開してみてください。 最後に 簡単ですが、Google Chrome拡張機能の作り方について紹介しました。 シンプルにHTML, CSS, JavaScriptで実装する方法もありますが、フレームワークを使うとフレームワーク特有のお作法があったりと学習コストが増える分、いろいろとよしなに作ってくれます。 30分から1時間程度あれば簡単な拡張機能は作れると思いますので、年末年始に作ってみるのはいかがでしょうか? セーフィーではエンジニアを積極的に募集しています。気になる方はこちらをご覧ください! https://safie.co.jp/teams/engineering/ この記事を読んでもし興味を持っていただけた方は、ぜひ採用サイトもご覧ください。 カジュアル面談のみでも大歓迎ですので、お気軽にご連絡ください。 最後までお読みいただき、ありがとうございました。
アバター
メリー・クリスマス、セーフィーCTOの森本です。 こちらは Safie Engineers' Blog! Advent Calendar の25日目のエントリーです。 早いもので、昨年 創業以来10年の開発組織の振り返り について掲載してからもう1年が経ちます。昨年の記事の最後でも触れた通り、メンバーのお陰で事業、組織、プロダクト、内部的な仕組みなど全てにおいて様々な課題を乗り越え再成長軌道に載せると共に、その先の更なる成長へ向けての仕込みも適宜進められていると実感しています。特に大きなチャレンジであったシステム刷新も専任チームを組成し、一歩一歩ですが進められている状況となっています。 さて、今回のエントリーではシステム刷新とは別の、先々に向けた重要な取り組みの一つである AIソリューションプラットフォーム について紹介します。 はじめに AIソリューションプラットフォーム概要 実現したいこと AIソリューション開発における課題 システム構成イメージ 目指すべき姿 経産省PJへの採択 今後に向けて まとめ 最後に はじめに 図1 当社は主にB2B領域でクラウドレコーディングサービスを提供しています。 様々な業界のお客様の現場で、防犯だけでなく業務改善用途でも幅広く活用されており、既に27万台を超えるカメラがお客様環境で常時稼働しており、それらのカメラで録画された総量35PBを超える膨大な映像データが当社システム上で管理されています。 単純な録画視聴に加え、特に業務改善観点では映像データの解析なども重要で、既に複数の 汎用的用途のソリューション の提供は行っているものの、お客様からも業界、課題に特化、もしくはお客様に特化したデータ活用のご要望を幅広く頂いており、より広範囲で効率的に様々なソリューションの開発、提供を進められるような仕組みの整備が急務となっています。 尚、データ活用という観点ではAIの利用は当然考慮に入れる必要がありますので、如何にAIを活用したソリューションの提供を効率的に行えるかも重要な要素と捉えています。 AIソリューションプラットフォーム概要 実現したいこと 図2 冒頭で触れた通り、我々は映像データを活用し、我々のサービスをご利用頂いている様々な業界のお客様の現場課題の解決を進めて行きたいと考えています。ただ、様々な業界が存在し、その中の課題もまた多様な状況で、これらを我々単独で実現するのが不可能に近いのは明白です。(図2参照) だからこそ我々は我々のシステムをプラットフォームと定義し、APIやSDKを活用してデータを活用できるような機能を既に提供してきています。 developers.safie.link 今後に向けてはこれを更に発展させ、他のAI開発者も容易にお客様の課題解決につながるAIソリューションの開発と展開を行える仕組みを整備し、多面的にこれらの活動を進めていく事が必要だと感じています。 大前提として、AIソリューションの開発には適切なデータ活用が必要ですので、我々はまずはそれらを促進する為に何を実現する必要があるかの整理を行ってきました。 AIソリューション開発における課題 図3 適切なデータ活用を実現するための仕組みの整理を行っている中で、AI開発者とデータホルダー(我々に取ってのカメラをご利用頂いているお客様)がそれぞれ課題を抱えている事がわかりました。(図2参照)例えばデータホルダーで言うと、そもそもデータ提供を行うメリットが不明確だったり、いざデータを提供するにも手間がかかります。また、昨今は個人情報やプライバシーにも十分に配慮をする必要があります。一方で、AI開発者側はAIモデルを開発しようにもデータの収集に手間がかかったり、いざAIモデルを準備してもアプリケーションとして提供する環境が必要となります。 これらの課題がデータ活用の促進とAIソリューションの開発効率を高めるための課題となっている事がわかりました。 システム構成イメージ 図4 データホルダー、AI開発者の抱える課題を解決するため、我々は当社が管理する映像を教師データとして活用し、効率よく且つ一気通貫でAIソリューション開発が行える仕組みを提供すると共に、開発したAIソリューションが当社のカメラやクラウドプラットフォーム上に直ぐに展開でき、実行可能な環境を整備していきます。(※大前提として当社が管理するデータはデータホルダーに帰属していますので、その活用にはデータホルダーの合意が必須です) これによって、データホルダーはデータの取り回しやセキュリティについてあれこれケアする必要がなくなり、一方でAI開発者はAIモデル開発にできるかぎり集中し、データ収集に工数を割く必要も無ければ、アプリケーションの展開先も当社の環境を活用することにより素早く顧客に価値提供を行う事ができるようになります。 我々としては、わざわざデータ収集しなくとも、データホルダー環境に展開済みのカメラによって録画されたデータを活用できるのも非常に重要な点ですが、開発したAIソリューションを稼働中のエッジAIカメラやクラウドシステム上に展開し素早く利用して頂く状況に繋げられる点も非常に価値がある点であると捉えています。 目指すべき姿 図5 我々は、先ほどの仕組みによりデータホルダー、AI開発者の抱える課題を解決し、AIソリューションの効率的な開発、展開を可能としていく事だけでは、目標とする様々な業界の課題解決の実現には不十分だと考えています。 AI開発者がどこにどのような課題が存在するか十分に分かっていないケースがあれば、一方でデータホルダーも課題解決に向けてどのAI開発者と連携すればよいか不明確なケースがあると推測しています。 これらの状況を解決するために、上記に加え、データホルダーとAI開発者の連携を実現するような場の構築、提供を行う必要性も感じており、合わせて整備を行っていくことを考えています。 このようなデータホルダーとAI開発者間の多対多のコラボレーションの実現こそが、先程の仕組みの効果を適切に発揮させるために必要であると捉えています。 経産省PJへの採択 我々は上記で説明したAIソリューションプラットフォームについて、数年前より検討を進め少しづつ進めていましたが、この度、国立研究開発法人新エネルギー・産業技術総合開発機構(以下「NEDO」)が公募した「ポスト5G情報通信システ厶基盤強化研究開発事業/データ・生成AIの利活用に係る先進事例に関する調査(調査類型1)」に採択されることが決まりました。 safie.co.jp データ活用に対しての国としての課題感、危機感の現れでもあると捉えています。我々としてはこれはもちろん好機ではありますが、一方で国策の一貫に選定頂いた責任と自負を感じつつ、開発活動を加速して行きたいと考えています。 今後に向けて AIソリューションプラットフォームの開発を進め、2025年中には外部に公開できるよう準備を行うべく進めています。 図6 合わせて、その有効性を示すべく実際のデータホルダー、AI開発者にもご協力頂き、AIソリューションプラットフォームの効果の実証も進めていきます。 更に、有効なコラボレーションの場として成立させていく必要があり、その為にはよく知って頂く事が必須だと捉えていますので、認知向上に向けた取り組みも順次行っていきます。 テックブログでも継続的に具体的な取り組みについてアップデートする想定で検討を進めています。 まとめ 我々はお客様の様々な現場課題の解決を多面的に推し進めるため、それぞれの抱える課題の解決を行える仕組みの整備と、データホルダーとAI開発者の多対多のコラボレーションの場を提供すべくAIソリューションプラットフォームの整備を進めて行きます。 これらの仕組みを活用しデータ活用を多面的に進めて行くことにより、様々なお客様がAIの有用性を実感できるような社会の実現を目指して行きます。 最後に 今回はAIソリューションプラットフォームの紹介となりましたが、セーフィーでは他にも更なる成長へ向けて様々な開発に関わる取り組みを行っています。それらに一緒に関わってくれるエンジニアさんを絶賛募集しています!!!!
アバター
この記事は Safie Engineers' Blog! Advent Calendar 24日目の記事です。 はじめに こんにちは、セーフィー 企画本部 デザインセンターの碇石(いかりいし)です。 2024年10月30日、デザインシステムを用いたUIリニューアルがついに公開されました。対象プロダクトは、エンタープライズ向けに多台数のカメラを統合管理できるSafie Manager(セーフィーマネージャー)です。 まず、デザインシステムについては、2019年12月頃から開発を進めていました。2023年に入ってからSafie Managerを含む管理ツールの開発を行っている開発チーム内でフレームワーク移行の話が上がり、そのタイミングに乗せる形でデザインシステムを用いたUIリニューアルプロジェクトが始動。 手探りでスタートしたこのプロジェクトは約1年10ヶ月の開発期間を経て無事公開となりました。 はじめに 開発体制 戦略:手探り感満載の初動期 ターゲットユーザー 目的 UX課題 具体方針 構造:オブジェクト指向(OOUI)で再設計 サイトマップとデータ管理の流れの型化  骨格〜表層:作っては壊しを繰り返して磨き上げるUX 画面構成(ゾーニング) WF〜デザインカンプ マイクロインタラクション まとめ さいごに safie.co.jp 最適なUXを考える上で、デザインセンターでは日頃からJesse James Garrett 氏が提唱する5段階モデルを用いる事が多く、今回のプロジェクトもこちらをベースにデザイン制作までを振り返ってみようと思います。 UXの5段階モデル 開発体制 フレームワーク刷新を含む大型リニューアルはセーフィーとしても初めての試みとなるのですが、このプロダクトは初期リリースの後デザイナー不在となり開発チームのみで守っていたプロダクトなのです。 そういった背景もあり、このチームにおいてはエンジニアとデザイナーの共同開発は実質初めての状態。今回のプロジェクトのためにディレクターとデザイナーをアサインしました。 PdM 1名 フロントエンドエンジニア 9名 デザインディレクター 1名 デザイナー 2名 戦略:手探り感満載の初動期 UXの5段階モデル - 戦略 -  日頃から使い勝手に関する足元の課題感は多く、この機会にしっかり解決していきたい意志をエンジニアと共に意識を合わせ、再設計に当たり、ターゲットユーザーのすり合わせと目的、現状のUX課題の抽出を行いました。 ターゲットユーザー 企業の管理者 情報システム部など会社のシステムを管理する人物 スーパーバイザーやマネージャーなど、現場の監督責任者にあたる人物 利用用途 複数店舗を跨いで多台数カメラの設定および操作権限を管理する 目的 フレームワークの刷新(Nuxt2→React) デザインシステムの適用 【ポイント】UXの改善(あるべきを考え、使い勝手の負を解消する!) 今回のリニューアルのポイントは、最適なUXの実現です。ユーザビリティ *1 を担保することはもちろんのこと、複雑な仕様に対しユーザーがストレスなく利用できる状況を目指します。 UX課題 一貫性が無く、学習コストの高い操作体験 予測しづらい機能名称 つぎはぎ的に追加されたナビゲーションメニュー 生かされていない一等地のホーム画面 具体方針 構造の最適化 骨格の最適化(ナビゲーション) 機能名称の最適化 今回はUIのリニューアルなので、機能要件はそのままに、ユーザーニーズに則したUXの検討に向けて構造〜骨格の再設計を実施します。 構造:オブジェクト指向(OOUI)で再設計 UXの5段階モデル - 構造 -  Safie Managerでは、管理するオブジェクト(データ)が多くそれぞれを結合させることでカメラデバイスの管理や管理権限の組み合わせの自由度を実現させています。 その一方で複雑な機能にインターフェースが追いついておらず、例えばオブジェクトが選ばれていないのにタスク選択のUIが出っ放しになっており、実行ボタンはdisableになっている、、 実行不可の理由が分からないというストレスをユーザーに与える可能性のある箇所や、操作画面の初期状態から「この操作は無効です」というようなエラー表示がされているなど、散見していました。 また、一覧上の操作においては単一の場合でも一時的にオブジェクトを選択状態にしなくてはならず効率の面で課題がありました。 旧UIのオブジェクト操作のインタラクション 設計の基本思想としてオブジェクト指向(OOUI)を意識し、オブジェクトを選択→タスクの選択→結果という流れに調整しながら、インタラクションを含めた形で基本的な負を改善する事ができました。 整理したオブジェクト操作のインタラクション サイトマップとデータ管理の流れの型化  仕様理解もかねて、現行の構造も明らかにしながらデータ管理の流れを型化。全体像でBeforeAfterのイメージをすり合わせ、ナビゲーション構造はこのサイトマップを元に再設計を行いました。 サイトマップ ナビゲーション構造の再設計 データ管理の流れの型化 この辺りは、今後画面を作っていく中での指針となる部分なので細かく確認を交えながら設計していきました。 骨格〜表層:作っては壊しを繰り返して磨き上げるUX UXの5段階モデル - 骨格〜表層 - 型化したデータ管理の流れを元に各画面を制作していきます。このフェーズになってくるとイメージが具体化していくので、実装に向けた細かい調整に移っていくところではありますが、画面構成をユーザーニーズに立ち戻り一度壊して再考するといった場面も多く発生しました。 画面構成(ゾーニング) ゾーニング 画面構成はプロダクト全体で統一されるため、デザイン主体で設計しています。 ヘッダー プロダクト共通で固定化されています。デザインシステムがまだ反映されていないプロダクトにも既に展開されているUIになります。 ナビゲーションメニュー プロダクト固有のメニューが並びます。作成したデータなど可変するものをメニューに並べることはNGというルールを設けています。 コンテンツエリア 制約は特にないですが、横幅指定が画面幅100%を基準値とし、設定画面など横幅の長さによりコンテンツが見づらい、使いづらい場合に1040pxを最大値とするルールを設けています。 WF〜デザインカンプ UIComponentがある事で、より具体的な形で情報設計をする事ができます。見た目がデザインカンプなので、この段階で作って壊してっていうのは大変なのでは、、と思われるかもしれませんが、実際の作業負荷はそこまででもなかったりします。 ただ、エンジニア側からのFBや提案の際、現状の制作環境としてFigmaの編集権限をもっていないエンジニアにとっては改めてWFを作って伝えなければいけない環境だったので、画面要件を決めるまでのラリーが何度も繰り返されたのは、今回のプロジェクトの中でも一番大変だった部分なのではないかと思います、、 デザインFIXまでの流れ マイクロインタラクション disable時にリストをhoverすると、選択不可の理由がToolTipにより表示されるなどの細かいインタラクションを検討しながら最後の仕上げを行なっていきました。全ての画面に行き届いていない状況ではありますが、このToolTipが出ることで安心感が高まったように思います。 disableのオブジェクトをhoverした時 まとめ 今回はデザインを作るまでの話を5段階モデルに沿って振り返ってみました。改めて振り返ってみると、初動の「ユーザーを知る」ここの紐解きが甘すぎた事が今回の一番の反省点です。 実はこの5段階モデルの考え方もOOUIも、エンジニアには全く共有していないまま進めており、前提となる意識の部分で噛み合わない瞬間がしばしばあったように思います。 一方で、プロジェクトやデザインに対するエンジニアの思いの強さを肌で感じルことができ、プロジェクトが進行するにつれてプロジェクトメンバーの一体感が強くなったように思うのはすごく嬉しかったです。 現在、絶賛プロジェクトメンバーと振り返りを行っており、ユーザー理解が十分にできていればプロダクト特性を捉えた大胆な操作体験も実現できたかも?などなど後悔することもありますが、運用フェーズではデザイナーとエンジニアの連携強化に向けた相互理解の場や勉強会の実施など、前向きな取り組みを始めようとしています。 手探り感のある進行に全力で頑張っていただいたプロジェクトメンバーには感謝しかありません、、今後のエンハンス開発に向けて今回の反省を活かした共創体制を築いていきたいと思っています。 さいごに デザインセンターでは、今回のプロジェクトのキーとなるデザインシステム「Pantograph」を順次主要プロダクトに反映しています。 デザインシステムの全体像や、開発秘話を掲載しておりますので、よろしければ合わせてご覧ください。 note.com note.com セーフィーではデザイナー3期生を募集中です。 safie.co.jp *1 : ユーザビリティ=「使いやすさ」ではなく「利用可能性」と考え、有効さ、効率、満足度の度合いで判断しています。
アバター
この記事は Safie Engineers' Blog! Advent Calendar  23日目の記事です はじめに はじめまして、サーバーチームの城山です。 弊社の一部サービスで Kinesis Client Library(v2) for Python を利用しており、私なりに感じた躓きやすいポイントについてお話ししたいと思います。 はじめに Kinesis Client Library for Python とは 構成について 躓きやすいポイント 同一シャード内でのパーティションキー毎の処理遅延 重複レコードの制御 ECS Fargate task での RecordProcessor のログ連携 タスク終了時の MultiLangDaemon のタイムアウトエラー ローカル開発環境の構築 まとめ Kinesis Client Library for Python とは https://github.com/awslabs/amazon-kinesis-client-python Kinesis Client Library for Python(以下KCL) は、kinesis data stream(以下KDS) からデータを取得し、処理するためのライブラリです。 Kinesis Client Library は Java で実装されており、MultiLangDaemon と言われる多言語インターフェースを利用することで、他の言語から利用することができ、Pythonでも利用することができます。 構成について シャード数3、ECSタスク数1の場合の構成図です。 ※ 簡潔にするため、一部省略している箇所があります。 KDSに書き込まれたデータを、KCL の MultiLangDaemon が取得し、標準出力を介して、複数レコードが RecordProcessor(Pythonサブプロセス) に渡されます。 RecordProcessor では、主にDBへのデータ登録や、API連携等のビジネスロジックを実装しています。 また、KDSは複数のシャードに分かれており、RecordProcessor はシャード毎に1つずつ起動されます。 これにより、シャード単位での並列処理が可能となっています。 躓きやすいポイント 同一シャード内でのパーティションキー毎の処理遅延 1つのパーティションキーで大量にデータが登録された場合、同一シャード内の他のパーティションキーのデータが処理待ちとなり、処理が遅延する可能性があります。 例えば、上図のように、パーティションキーAのデータが大量に登録された場合、パーティションキーAのレコードを全て処理するまで、パーティションキーBのデータが処理待ちになるという状況です。 それに対し弊社では、RecordProcessor でパーティションキー毎に非同期処理を行うことで、特定パーティションキーの処理遅延が他のパーティションキーに影響を与えないようになっています。 また、 queue.Queue を利用して、同一パーティションキー内でデータの順序を保持しつつ処理できるようにもなっています。 以下に一部抜粋してコード例を示します。(※ 実際のコードから一部簡略化しています) def process_records (self, process_records_input: messages.ProcessRecordsInput): """ Called by a KCLProcess with a list of records to be processed and a checkpointer which accepts sequence numbers from the records to indicate where in the stream to checkpoint. Args: process_records_input (amazon_kclpy.messages.ProcessRecordsInput): the records, and metadata about the records. """ async def process_queue_record (record_queue: queue.Queue, partition_key: str ): while not record_queue.empty(): record: messages.Record = record_queue.get() # ここからrecordの処理を行う async def process_records_in_parallel (records: list [messages.Record]): # partition key毎にqueueを用意し、recordをputする queues: dict [ str , queue.Queue] = {} for r in records: if r.partition_key not in queues: queues[r.partition_key] = queue.Queue() queues[r.partition_key].put(r) # partition key毎に用意したqueueをevent loop内で並列に処理していく tasks = [] async with asyncio.TaskGroup() as tg: for key, q in queues.items(): tasks.append(tg.create_task(process_queue_record(record_queue=q, partition_key=key))) event_loop = asyncio.get_event_loop() event_loop.run_until_complete(process_records_in_parallel(process_records_input.records)) 参考: awslabs amazon-kinesis-client-python (GitHub) / Apache License 2.0 重複レコードの制御 KCLのデプロイ、エラーによる予期せぬ終了、シャードのマージ、分割 等で、処理が途中で終了した場合、重複してデータが処理される可能性があります。 これに対し、データの生成元(弊社の場合はカメラデバイス)でデータの一意性を保証するためのIDを生成し付与してもらい、DB側でそのIDを保持し、重複レコードの制御が行われています。 以下に重複レコード制御に関する流れの例を示します。 ECS Fargate task での RecordProcessor のログ連携 弊社では、アプリケーションのログドライバーとして主にAWS FireLensを利用し、ログを特定のサービスに連携しています。しかし、RecordProcessorはサブプロセスとして起動され、かつ標準出力がMultiLangDaemonとの通信に使用されるため、RecordProcessorのログを連携するには工夫が必要でした。この課題は、ログ出力先をMultiLangDaemonの標準出力先に変更することで解決されています。 具体的なコードは以下の通りです。(※ 実際の設定から一部簡略化しています) import logging.config logging.config.dictConfig( { "version" : 1 , "handlers" : { "file" : { "class" : "logging.FileHandler" , "filename" : "/proc/1/fd/1" , # MultiLangDaemonの標準出力先 }, }, "loggers" : { "app" : { "level" : "INFO" , "handlers" : [ "file" ], }, }, } ) タスク終了時の MultiLangDaemon のタイムアウトエラー 開発初期の頃、デプロイ時など古いタスクの終了時に、MultiLangDaemon 側でタイムアウトエラーが発生していました。 実際にタイムアウトが発生した際の MultiLangDaemon のログが以下です。 2024-01-25 06:28:02,986 [Thread-1] INFO s.a.k.multilang.MultiLangDaemon [NONE] - Process terminated, will initiate shutdown. 2024-01-25 06:28:02,917 [ShardRecordProcessor-0000] INFO s.a.kinesis.multilang.MessageWriter [NONE] - Writing ProcessRecordsMessage to child process for shard shardId-000000000012 at java.base/java.lang.Thread.run(Thread.java:829) at software.amazon.kinesis.multilang.MultiLangDaemon.lambda$setupShutdownHook$0(MultiLangDaemon.java:183) at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2027) at java.base/java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1892) java.util.concurrent.TimeoutException: null 2024-01-25 06:28:02,732 [Thread-1] ERROR s.a.k.multilang.MultiLangDaemon [NONE] - Encountered an error during shutdown. この問題は、MultiLangDaemon が RecordProcessor に shutdown リクエストを送信し、その終了を待っている間にタイムアウトしていることが原因でした。これはKCLの設定値 shutdownGraceMillis を指定し、タイムアウト時間を調整することで解決されています。 shutdownGraceMillis = 10000 ローカル開発環境の構築 KCLは、DynamoDBとKinesis Data Streamに依存しています。KCLの設定でAWSのエンドポイントを指定できるため、AWSサービスをローカルでエミュレートできる LocalStack を利用して、AWS上のリソースを使用せず、ローカル開発環境が構築されています。 kinesisEndpoint = http://localhost:4566 dynamoDBEndpoint = http://localhost:4566 ちなみに、port 4566 は LocalStack のデフォルトのポート番号です。 まとめ KCLを利用すること分散コンピューティングに関連する複雑なタスクを簡素化してくれるが、独自の対応が必要な部分があることがわかりました。 また、awsドキュメント等に記載がない設定値( kinesisEndpoint , dynamoDBEndpoint 等)もあり、場合によってはソースコードを読むことも必要と感じました。
アバター
この記事は Safie Engineers' Blog! Advent Calendar  22日目の記事です はじめに こんにちは、セーフィー株式会社でAI開発をしている木村です。年の暮れということで、今年チームで始めた取り組みについて共有させていただきます。 私は昨年セーフィーに入社した新米エンジニアで、日々勉強の毎日を送っていました。そんな中、技術力向上のために本を読んでインプットすることは重要だと感じていましたが、一人では継続が難しく悩んでいました。 そこで、「複数人で読み進めれば、より効果的にインプットができるのではないか」と考え、チーム内で輪読会を開催することにしました。 輪読会は「始めたものの続かない」という声をよく耳にする活動ですが、私たちは1年間継続することができています。 本記事では、”サスティナブル” な輪読会を実現するために運営として気を付けたことや、実践を通じて改善してきたことなどについてお伝えしたいと思います。 はじめに なぜ輪読会を始めたのか 実施形態 基本情報 やり方 本選び 実際にやってみて ふりかえり アンケート アンケート結果を踏まえた改善 まとめ さいごに なぜ輪読会を始めたのか 知識を増やせる、インプットを楽できるという点の他にも、輪読会には多くのメリットがあると考えています。 相互サポートによる学習効果 理解に時間がかかってしまう難しめの本でも、互いにサポートし合うことで読破が可能になります。また、本に書かれている内容だけでなく、メンバーの実務経験や関連知識も共有できるため、一人で読むよりも多くの学びを得ることができます。 チーム内の共通知識の醸成 同じ本を読み、その内容を互いに認識していることで、チーム内の共通知識が育まれます。これはコードレビューなどの実務でも活かされ、「あの本のアレ」といった共通認識をベースにしたコミュニケーションが可能になります。 コミュニケーションの活性化 普段の業務では直接関わる機会が少ないメンバーとも対話する場となり、チーム内のコミュニケーションが活性化します。技術的な議論を通じて、お互いの考え方や知見を共有する良い機会となります。 これらのメリットを活かし、個人の技術力向上とチームの成長の両方を実現できる場として、輪読会をスタートさせました。 実施形態 基本情報 参加規模:6~8名程度 実施形式:基本オフライン(オンライン併用) 参加形態:自由参加制 開催頻度:週1回 所要時間:1時間 やり方 輪読会の実施方法には、大きく分けて「当日その場で読む」パターンと「事前に読んでくる」パターンがあります。 私たちは、コミュニケーションの活性化やディスカッションによる関連知識共有をを重視し、「事前に読んでくる」パターンを採用しました。 さらに、事前に読んでくるパターンにも以下の2つの方式があります: 全員が読んできてディスカッションを行う 数人が読んできて発表する ディスカッション方式の方がコミュニケーションの活性化には効果的ですが、参加者全員に毎回の予習が必要となり負担が大きくなります。 長期的な継続を考慮し、私たちは以下のようなハイブリッドな形式を採用しました。 1. 当日の発表担当者数名が事前に読んで簡単な発表を行う 2. その後、質問や補足の時間を設け、参加者全員で対話する この方式により、発表担当者以外の参加者の負担を抑えつつ、活発な意見交換の場を作ることができました。 本選び 本は参加者全員で候補を出し合い、投票によって選びました。 最初に選んだ本は、 出版社はオーム社の 「 リファクタリング(第2版): 既存のコードを安全に改善する 」 (著者:Martin Fowler、翻訳者:児玉 公信 , 友野 晶夫 , 平澤 章, 梅澤 真史)です。 この本は、リファクタリングの本質的な考え方から具体的な手法まで、体系的に解説された名著です。 (本の内容についてはここでは詳しく触れませんが、リファクタリングの勘所やコードを修正するステップについて細かく記載されており、非常におすすめな書籍でした。) 実際にやってみて 週1回1時間の頻度で実施し、4か月ほどかけて1冊を読み終えました。 実際に輪読会を運営してみて、当初の期待通りの効果が得られただけでなく、予想以上の学びもありました。 良かった点 一番の成果は、一人では途中で挫折しそうなぶ厚めの本を最後まで読み切れたことです。メンバー同士で分からない部分を教え合い、それぞれの理解を深めながら進められたことが、完走につながりました。 予想以上だったのは、メンバーそれぞれの実務経験や関連知識の共有から得られる学びの多さでした。例えば「製品コードの○○の部分はこのようにリファクタリングするとよさそうだね」といった具体的な改善提案が出てきて、より実践的な議論に発展することができました。 課題となった点 メンバーがあまり興味を持てない章に差し掛かると、議論も盛り上がらず、一時的にモチベーションが下がってしまう時期がありました。この経験から、本の内容から重要なポイントを見極めて読み進める必要性を実感しました。 ふりかえり アンケート 1冊通してやってみた後、運営改善のためにアンケートを実施しました。5段階評価での結果は以下の通りでした。 全体的な満足度:4.0 / 5.0 本がためになったか:4.1 / 5.0 発表資料のわかりやすさ:4.0 / 5.0 ポジティブな意見 知識が増えたという意見が多かったのはもちろんのこと、 新しい視点が得られた(他の参加者の意見や考え方から新しい視点を得た) 意見交換やディスカッションが有意義だった という意見が多かった。 改善点として挙がった意見 一方で、以下のような課題も指摘されました。 本の内容にあまり興味を持てなかった 自由に参加という形と発表者分担制がかみ合ってない 発表担当が連続するとやや負担に感じる チームの状況や目的に合わせて、取り上げる章を選択的に決めても良さそう アンケート結果を踏まえた改善 アンケート結果を踏まえ、より効果的な輪読会を目指して以下の改善を実施しました。 複数の本の並行読書制の導入 読む本を2種類に増やし、参加者がより興味のある本を選べるようにしました。これにより、メンバーの学習意欲を高めることが期待できます 班制度の導入 本ごとに班分けを行い、各班で発表を担当する形式に変更しました。これにより、業務都合などで輪読会に参加できない場合でも、班内で柔軟に担当を調整できるようになり、運営の安定性が向上しました。 期間の明確化と効率化 各本に対して輪読会の回数を事前に設定し、その期間内で完結させる方式を採用しました。これにより、 複数の本を並行して進めても、読了のタイミングを揃えられる 重要度の高い章に焦点を当てた効率的な進行が可能になった これらの改善により、より柔軟で効率的な輪読会の運営が実現でき、参加者の満足度向上にもつながりました。 まとめ 本記事では、1年間継続できた輪読会の運営について紹介させていただきました。 特に、アンケートを通じて得られた意見を基に実施した改善(複数の本の並行読書制、班制度の導入、期間の明確化)は、輪読会の質を大きく向上させることができました。 輪読会は、個人の技術力向上だけでなく、チーム内のコミュニケーション活性化や知識共有の場としても非常に有効です。 本記事が、みなさんの職場での ”サスティナブル” な輪読会立ち上げと運営の参考になれば幸いです。 さいごに セーフィーではこのように学習意欲の高いエンジニアとともに開発する仲間を募集しています! この記事を読んでもし興味を持っていただけた方は、ぜひ採用サイトもご覧ください。 カジュアル面談のみでも大歓迎ですので、お気軽にご連絡ください。 https://safie.co.jp/teams/engineering/
アバター
この記事は Safie Engineers' Blog! Advent Calendar 21日目の記事です はじめに Hello~ モバイルグループのアダムです。 今年も誕生日に記事を出せて嬉しいです!🎅 最近自分がはまっているモバイルのアーキテクチャの遊びについて紹介させてください! 今回の遊びの環境: Xcode: 16.2 Minimum Target: 17.0 Swift: 6 Concurrency: Strict はじめに MVVM Environment Repository ViewModel App/Scope View 結果 まとめ おわりに MVVM 将来的に理想なアーキテクチャを行うならどんな感じで実装するのか?をやってみました。 今まで開発に参加していたアプリはほとんどMVVMだったので、最新のSwiftUIで実装した場合どんな形になるんだろう?🤔 オンメモリーのCacheを持つRepositoryがある場合どんな形で作るのか? そのために今回どんな感じで考えたかを紹介させて〜 もしこんな方法おすすめ、こちらの方がベターっていうのがあったらこちら→ @monolithic_adam or monolithic-adam.bsky.social にPOSTお願いします! Environment 一旦例のためカウントを持つRepositoryとViewModelがincrementするUseCaseを用意します Repository @Observable にして何かの変更があった場合Viewが再描画される @Observable final class TestRepository { var count : Int = 0 } ViewModel ViewModelにシンプルなUseCaseを用意、Repositoryに最新の状態をアップデートできる func を用意します struct TestViewModel { let repository : TestRepository func increment () { repository.count += 1 } } App/Scope 今回のRepositoryがアプリスコープのため、Appの方で init ・ .environment にセットする @main struct TestProjectApp : App { @State private var testRepository = TestRepository() var body : some Scene { WindowGroup { ContentView() .environment(testRepository) } } } View こちらが 迷いポイント1 です! RepositoryがEnvironmentの方で設定しているので、どうやってViewModelへ渡すのかを迷っています・・・ Environmentはinit時にまだ存在しないのでparentから渡すのか、ViewModelを .environment にセットして、observeすべきかも? ま、一旦parentから渡す感じで実装してみましょう! struct ContentView : View { @Environment ( TestRepository.self ) var repository : TestRepository let viewModel : TestViewModel init (repository : TestRepository ) { self .viewModel = TestViewModel(repository : repository ) } } 結局App側で二重に渡している感じになってしまう @main struct TestProjectApp : App { @State private var testRepository = TestRepository() var body : some Scene { WindowGroup { ContentView(repository : testRepository ) .environment(testRepository) } } } 最後に状態変化を見れるよう、シンプルにcountを表示させるTextとincrementできるボタンを用意して完成! var body : some View { VStack { Text( "Current Count \( repository.count ) " ) Button { viewModel.increment() } label : { Text( "Increment" ) } } .padding() } 結果 まとめ View -> ViewModelにRepository渡すことがEnvironmentをちゃんと活用できていないな〜 画面遷移する時にどうせ次のビューを渡さないといけなくなるので、あまりEnvironmentにセットする意味がない init 時は気にしなくて良くなるViewModelをEnvironmentにした方が綺麗になる 理想はこうなるかな〜 おわりに モバイルチームでは、このように多様な環境で世界に向けたアプリをともに開発する仲間を募集しています! open.talentio.com
アバター
こんにちは!第5開発部業務システム第1グループの常世田です。 この記事は セーフィー株式会社 Advent Calendar 2024 の12月20日の記事です! 2024年4月1日、顧客お問い合わせ対応ツールをZendeskからSalesforce Service Cloudへ移行しました。移行前は営業部がSalesforce、カスタマーサービス部(以降CS部)がZendeskと部門ごとに違うツールを利用してきたことにより、データ一元管理がされてない、非効率な業務が多々発生している状態でした。 この記事ではService Cloud導入をどのように実現したかを説明します。 1.導入前の大きな課題 2.実現方法 3.試行錯誤した点 4.取り組んだ感想 1.導入前の大きな課題 顧客対応履歴の分断 営業部はSalesforceを利用して顧客対応をし、CS部ではZendeskを利用して顧客対応をしていました。分断されることで、1顧客に対しての対応履歴がすべて網羅できない状態になり、両部署でどのようなやりとりをしているか、素早くキャッチするのが困難になっていました。 顧客情報が正しくない SalesforceとZendeskはそれぞれ内部で顧客情報を管理しています。Salesforceにすべての情報が連携されておらず、一部の顧客で情報の不足や二重管理になり、正しい情報が一元管理されていないため判定が困難な状態でした。        顧客に関連する情報がZendeskで管理されていない お問い合わせ対応のスタートはカメラのシリアルナンバーを頂いてからスタートします。シリアルナンバーはSalesforceで管理されているため、ツールを横断してSalesforceで検索し状況把握、Zendeskに戻り顧客へ返信する といった非効率な作業が発生していました。        2.実現方法 業務要件の洗い出し Zendeskで実現できていた機能がSalesforceで実現可能か否かの洗い出しを実施。 実現不可の場合は、どうやって実現するか、そもそも運用でカバーできる等も検討し、すべての機能を実現するのではなく、時には業務をシステムに合わせてもらうなど取捨選択をしつつ、一つ一つ丁寧に方針を決めていきました。 流入チャネルの整理 各流入チャネルからのお問い合わせデータ連携先がZendeskに集約されていたため、Zendeskにどういうお問い合わせが集約されているかの洗い出しを行いました。 標準とカスタムする範囲の選別( ※実装の一部分を抜粋 ) 標準機能を用いて対応 メール機能 顧客満足度アンケート チャット機能 Slack連携(Slackワークフローを利用) 電話機能(MiiTel)(※一部カスタマイズ実施) 開発して対応 お問い合わせフォームの連携(DataSpider Cloud) プラン変更/解約申請の連携(DataSpider Cloud) 業務効率化のためのSalesforce内カスタマイズ(関連情報の自動取得/他システム連携) お問い合わせ情報を正しい顧客へ紐づける運用の意識付け 一番のメリットはすべてのお問い合わせが適切な顧客に紐付けられ対応履歴が網羅できる点です。データの紐づきが汚い状態では、導入しても意味がありません。紐付けはなるべくSalesforceで自動的に行いますが、どうしても手動で対応しないといけない部分も発生しました。Zendeskではあまり意識していなかった顧客データ管理の意識付けを行い、正しい情報が正しい顧客へ紐づくようにオペレーションを見直して業務側へ周知を徹底しました。 Zendeskデータ移行 Zendesk内で保管されていたお問い合わせ履歴をすべてSalesforceへ移行しました。Zendeskの仕様上、データを抽出しただけではSalesforceへデータ移行はできず、データ成形方法から検討しつつ、移行作業を実施しました。 3.試行錯誤した点 どのような業務をしているかのキャッチアップ システム導入する前に大切なことは業務がどのような流れで進んでいるかという業務を理解することです。私は入社して早々にService Cloud導入を一任されました。入社研修でCS部がどのような業務をやっているかの座学はあったものの、実際に業務メンバーとの会話ではイメージが沸きずらい状態でした。当時業務PMを担当していた方に不明点あればすぐ確認し、どういう業務をしているかの理解から重点的に行い、周りの方にもサポートを頂きながらプロジェクトを推進していきました。特に業務PMの方にはとても助けられました(笑) Zendesk中心の機能要件 「Zendeskで出来ていたことだからSalesforceでもやってくれ」 という無理難題な要望もありました。運用でカバーするか、システム側で頑張って実装するか、実装した場合スケジュール的に間に合うか等考えた上でどこで着地させるか、という折衷案出しに苦労しました。 データ移行 Zendeskから出力されるデータはJSON形式になっており、Salesforceへ投入するにはCSV形式への変換が必要でした。 JSONデータを扱うことが私自身初めてでしたので移行に必要な項目がどのような構造となっているかから調査を始めました。変換にはCLIコマンドとPythonを用いました。 また、お問い合わせのデータだけではなく、紐づくメールデータ、添付ファイルデータも対象でしたので、3つのデータが正しく出力されるように相当苦労しました。 データ投入のリハーサルは何度もトライしながら、正しいデータでSalesforceへ移行されるよう慎重に対応をしました。またメールデータは通常のSalesforceレコードの容量(2KB)とは異なっていたため、Zendeskデータをすべて移行した際どの程度データ量が増えるかの試算も必要でした。今思い返すとでService Cloud導入プロジェクトで一番パワーを使った作業だったと思っています。 4.取り組んだ感想 今回のプロジェクトでは、要件定義〜開発、リリース、業務調整、運用定着支援まで幅広い領域に携われました。急遽発生する業務要件の変更や追加要望も多々ありましたが、業務側とシステム側がより良い関係を構築することでスムーズに推進できました。どちらかが一方的に要望を伝えるのではなく、お互いの理解があってこそ実現できたプロジェクトだと感じました。「現場を知る」という大切さをこのプロジェクトを経て学べることができたと思っています。 結果、Service Cloudを導入して良かったと思っています。Salesforceに情報が一元管理されることで、顧客対応履歴が瞬時に見れる世界です。また業務効率化という点でもService Cloudの機能が十分発揮し、業務側の力になっていると感じています。 セーフィーのビジネスが拡張していくと同時に新たなカスタマイズ要望もたくさん増えていくと思いますので、業務がスムーズにいくよう引き続き改善、サポートをしていきたいと思っています。 最後まで読んでいただきありがとうございました。
アバター
This post is for day 19th of Safie Engineers' Blog! Advent Calendar AWS Kinesis Data Stream and Apache Flink are two of the most popular tools for streaming data processing applications. In today's post, we will explore how those two tools can enhance each other and how Apache Flink compares to AWS counterpart AWS Kinesis Consumer Library (KCL) What is Apache Flink When use Apache Fink over KCL Application Code example DataStreamAPI example TableAPI example Deployment on AWS cloud Conclusion What is Apache Flink In order to understand how Apache Flink can enhance a AWS Kinesis application, firstly, we need to understand its purpose and basic characteristics. Apache Flink is an open-source stream processing framework developed to go hand in hand with data streaming processes, including, but not limited to, the open-source message broker Apache Kafka . Its goal is to be used on unbounded and bounded data streams in real-time and batch processing. One of its strengths is the ability to connect to various data sources and its final destination (downstream data sinks), including data streaming services, like AWS Kinesis Data Stream, filesystem such AWS S3 and directly to Databases, using JBDC Drivers. Fig1. Flink Application data flow Flink was developed considering native cloud deployment, providing a robust and scalable platform for building data-driven applications, offering features such as event time processing, stateful computations, fault tolerance, scalability. When use Apache Fink over KCL While KCL allows record processing checkpoint for each shard iterator for recovery, it does not support state recovery. On the other hand, besides record checkpoint recovery, Flink also allows for complex data structure state recovery. Similar to checkpoint settings, the user can define not only the frequency of state save, but also the backend location. It can be saved locally, on a filesystem, such as S3 or in a key-value store. In case of a failure, Flink restores the state from the latest checkpoint and replays the process until the current point. The number of restart attempts and delay between each attempt is also configurable. This robust state recovery process allows for a better fault tolerance and consistency (exactly-once processing). Another advantage of Flink over KCL is its ability to process records using event-time semantics. KCL does not provide event-time semantics. All the processing logic is based on arrival time. The implementation of event-time processing can be done using a custom logic for out-of-order events buffer, but its implementation can be cumbersome and error prone, besides its overhead on the processing power. Flink natively handles out-of-order records using a watermark strategy, which can be either monotonously increasing (only accept records with higher timestamp than previous) or fixed amount of lateness behind (only accept records within a defined duration behind the previous watermark). This system of event-time semantics allows for accurate window aggregation operations and time-based analysis. It also increases the scalable and resilient processing capability under unstable data flow conditions. The architecture and scalability characteristics of both Fink and KCL differ in many ways. White KCL allows for horizontal scalability, ultimately, the number of workers is limited by the number of active shards. Each KCL instance subscribes to one or more shards, and while multiple KCL can subscribe to one shard, it can lead to a record being processed multiple times. Flink uses a multiple node architecture, allowing for horizontal scalability not bounded by the number of shards. Flink's overall architecture is also more resilient and robust against failure. The process control is based on Task Manager and Job Managers instances. With the Job Manager acting as the central work coordinator handling the Task Manager, who performs the actual work. JobManager is also responsible for detecting and handling processing failures, including establishing new Task Managers nodes and retrieving the previous state and checkpoint. Task Managers can have internal sub-task slots, which with its own purpose and sharing data. Fig2. Flink internal Managers architecture One point where KCL clearly excels is in its simplicity to deploy to production. While a Flink Application has many moving parts that must be adjusted and managed, KCL is an out-of-the-box solution on the AWS environment. Although modem deployment tools, such as Amazon Kinesis Data Analytics for Apache Flink, and solution vendors can help to lower the difficulties of a Fink Application deployment, it is still necessary a high level of understanding of its inner architecture and definitions to really take the full potential of its usage. Application Code example Now, to get a feeling of how the application is written, lets try to make a simple example considering a hypothetical, but realistic, IoT data analysis and processing. One of the first choices to be made when writing an Flink Application is deciding which API to use. Both DataStreamAPI and TableAPI can be used to transform and analyse the data. DataStreamAPI is a lower level abstraction and allows a higher flexibility, while TableAPI is a higher level of abstraction based on SQL language that requires less code to write and thus quicker development cycles. For this example, let's try one example of each API to get a feeling of how each one can be used. Similar to KCL, Flink backbone is written in Java, but there is a wrapper for Python available. In this example, we will use its native Java language to show a few of its characteristics. So, now to the code: In order to connect to a AWS Kinesis Data Stream, we will first need to include its relative connector in our dependencies. The easiest way to do it is to use a Java build tool such as Maven or Gradle maven <dependency> <groupId> org.apache.flink </groupId> <artifactId> flink-connector-aws-kinesis-stream </artifactId> <version> 5.0.0-1.20 </version> </dependency> gradle implementation 'org.apache.flink:flink-connector-aws-kinesis-streams:5.0.0-1.20' While the datastream can be used unstructured and undefined, I will help a lot in the long run if we define the data schema that we expect to be receiving. For this particular case, let's assume the data is for a device that will be sending periodically a packet of data in bytes and a value related to its status. Keep in mind that Flink has a native AWS Glue Schema decoder that can also be used for bigger projects where the schema must be shared across several services. public class DeviceRecord implements Serializable{ public String device_id; public long timestamp; public double value; public String data; public DeviceRecord() { } public DeviceRecord(String device_id, long timestamp, double value, String data) { this .device_id = device_id; this .timestamp = timestamp; this .value = value; this .data = data; } public String getDeviceId() { return device_id; } public long getTimestamp() { return timestamp; } public double getValue() { return value; } public byte [] getData() { return data != null ? data.getBytes() : null ; } @Override public String toString() { return "DeviceRecord{" + "device_id='" + device_id + ' \' ' + ", timestamp=" + timestamp + ", value=" + value + ", data=" + data + '}' ; } } The next step is to define how to deserialize the data received from the stream. We can get that byte array and transform it into an DeviceRecord instance. public class DeviceRecordDeserializationSchema implements DeserializationSchema<DeviceRecord> { private static final long serialVersionUID = 1L ; @Override public DeviceRecord deserialize( byte [] message) throws IOException { String line = new String(message, StandardCharsets.UTF_8); ObjectMapper objectMapper = new ObjectMapper(); DeviceRecord deviceRecord = objectMapper.readValue(line, DeviceRecord. class ); return deviceRecord; } @Override public boolean isEndOfStream(DeviceRecord nextElement) { return false ; } @Override public TypeInformation<DeviceRecord> getProducedType() { return TypeInformation.of(DeviceRecord. class ); } } DataStreamAPI example This is the actual implementation code of the processing job that connects with the Kinesis DataStream. For now no actual processing is being done. We will implement the sink connector latter public class BasicStreamingJob { public static void main(String[] args) throws Exception { Configuration sourceConfig = new Configuration(); sourceConfig.set(KinesisSourceConfigOptions.STREAM_INITIAL_POSITION, KinesisSourceConfigOptions.InitialPosition.LATEST); // Create a new KinesisStreamsSource to read from specified Kinesis Stream. KinesisStreamsSource<DeviceRecord> kdsSource = KinesisStreamsSource.<DeviceRecord>builder() .setStreamArn( "your-stream-arn" ) .setSourceConfig(sourceConfig) .setDeserializationSchema( new DeviceRecordDeserializationSchema()) // The DeviceRecord deserializer .setKinesisShardAssigner(ShardAssignerFactory.uniformShardAssigner()) // This is optional, by default uniformShardAssigner will be used. .build(); WatermarkStrategy<DeviceRecord> watermarkStrategy = WatermarkStrategy.<DeviceRecord>forBoundedOutOfOrderness(Duration.ofSeconds( 10 )) .withTimestampAssigner((event, timestamp) -> event.getTimestamp()); final StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(); env.enableCheckpointing( 5000 ); // Define the data processing pipeline. We will create one job for each device_id KeyedStream<DeviceRecord, String> stream = env.fromSource(kdsSource, watermarkStrategy, "Kinesis Source" ) .returns(TypeInformation.of(DeviceRecord. class )) .keyBy(value -> value.getDeviceId()); // Print the records to the console stream.print(); // Execute the Flink job env.execute( "Order Records by Partition Key" ); } } In this example, we will retrieve the field “data” from the Device object and insert it in as AWS S3 bucket. The first step it to decide how to retrieve this data. This can be done with an encoder class that writes the data into an OutputStream. public class DeviceRecordEncoder implements Encoder<DeviceRecord> { @Override public void encode(DeviceRecord element, OutputStream stream) throws IOException { stream.write(element.getData()); } } After that we will need to decide where to put our objects. For this case, we will create a daily bucket, combined with the device ID. public class DeviceRecordBucketAssigner implements BucketAssigner<DeviceRecord, String> { private static final long serialVersionUID = 1L ; private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "yyyy-MM-dd" ); @Override public String getBucketId(DeviceRecord element, Context context) { LocalDate date = Instant.ofEpochSecond(element.getTimestamp()).atZone(ZoneId.of( "Z" )).toLocalDate(); return date.format(formatter) + "/" + element.getDeviceId(); } @Override public SimpleVersionedSerializer<String> getSerializer() { return SimpleVersionedStringSerializer.INSTANCE; } } Finally, now we can combine all these elements and create our processing job to insert the data into an S3 bucket, by adding the following code to the main function. // Define the S3 sink FileSink<DeviceRecord> s3Sink = FileSink .forRowFormat( new Path( "s3://testbucket/" ), new DeviceRecordEncoder()) // Use the custom encoder .withBucketAssigner( new DeviceRecordBucketAssigner()) // Use the custom bucket assigner .withRollingPolicy(DefaultRollingPolicy.builder() // Roll the file every 5 seconds .withRolloverInterval(Duration.ofSeconds( 5 )) .withInactivityInterval(Duration.ofMinutes( 1 )) .withMaxPartSize(MemorySize.ofMebiBytes( 16 )) .build()) .build(); // Add the S3 sink to the pipeline stream.sinkTo(s3Sink); With this piece of code, we can insert the "data" field from the Kinesis record into the S3 according to the rolling policy as we receive it. TableAPI example For the example with TableAPI, let’s consider a job that inserts the streaming data directly into a RDS Database. In this example, we will insert the Device field “value” into the table as we receive it. final StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); // Define the Kinesis source table tableEnv.executeSql( "CREATE TABLE kinesis_table (" + " device_id character varying(32)," + " `timestamp` NUMERIC," + " `value` NUMERIC" + ") " + "PARTITIONED BY (device_id) WITH (" + " 'connector' = 'kinesis'," + " 'stream.arn' = 'your-stream-arn'," + " 'format' = 'json'" + ")" ); // Define the PostgreSQL sink table tableEnv.executeSql( "CREATE TABLE postgres_table (" + " device_id VARCHAR(32)," + " `timestamp` NUMERIC," + " `value` NUMERIC" + ") WITH (" + " 'connector' = 'jdbc'," + " 'url' = 'jdbc:postgresql://db-host:port/dbname'," + " 'table-name' = 'yourtablename'" + ")" ); // Insert data from Kinesis source table to PostgreSQL sink table tableEnv.executeSql( "INSERT INTO postgres_table " + "SELECT device_id, `timestamp`, `value` FROM kinesis_table" ); The above TableAPI example shows how it trades granularity and control for simplicity and quick deployment. It is ideal to work with highly structured data; or for cases when it requires JOIN between several data sources. For either cases, both APIs can be mixed at will for any Flink Application. So choose the API you will be using depending on your necessity. Deployment on AWS cloud With your Flink Application ready, now you can deploy it and start analyzing the stream data. Strictly speaking, the deployment can be done even locally, but to really reap the benefits of scaling and parallel processing, deployment on the cloud is recommended. Since we will already be using AWS Kinesis Datastream, let's consider the options for deployment on AWS Cloud. The deployment can be done on several different services, each one with its own benefits and trade-offs. Amazon Kinesis Data Analytics for Apache Flink (previously called Amazon Kinesis Data Analytics) EMR (Elastic MapReduce) AWS Fargate with Kubernetes (EKS) ECS AWS Lambda For architectures based on nodes, such as EKS, another choice that must be made is to decide if the application will be run in Session mode or Application Mode. In Session mode, the resources are shared between the nodes inside the single cluster, lowering the overhead for the overall service, but miss the isolation. This mode is best suited for quick or small jobs. Some special considerations must be taken into account when defining the configuration for a Flink application on the cloud. A critical point that has a direct impact on the cost of operation is regarding resource allocation. The number of tasks should be decided considering the parallelism level that the Task Manager will use to process the job, while the instance size should be decided considering the expected load. While Flink allows for those parameters to be set in its configuration file flink.conf, most services, such as EMR and Amazon Kinesis Data Analytics for Apache Flink allow to integrate its autoscaling to AWS native metrics and setup. Conclusion In this post we showed how Apache Flink can be used to analyze and process data coming from a AWS Kinesis Data Stream. When compared to AWS native solution, KCL, Flink has a few advantages, such as better flexibility and the ability to create save states. On the flip side, it also increases the complexity of the system, and consequently, the need of inhouse skills. The choice of which stream data analysis framework to use should be taken considering your specific necessities. To have Apache Flink as one of the available tools will surely increase your possibilities to take the data analysis and transformation process even further.
アバター
こんばんは。データ分析基盤グループ所属の大室です。 この記事は Safie Engineers' Blog! Advent Calendar 18日目の記事です! はじめに 登壇内容 セッション概要 当社のご紹介 プロジェクト背景 プロジェクト始動 CData Syncの採用ポイント 今後の展望 余談 会場風景 We are hiring! はじめに 2024年9月12日、 Snowflake World Tour Tokyo 2024 にて「SnowflakeとCData Syncを採用したSafieのデータ分析基盤構築におけるポイント」と題したセッションに登壇しました。 本セッションは事前登録時点で満席の申し込みを頂き、当日も多くの方にご参加頂きました! この記事では当日の登壇資料をベースにセッション内容をご紹介します。 なお、本セッションはCData社のスポンサーセッションにご招待頂く形で実現しました。 登壇の機会を頂いたCData社とSnowflake社の皆様にはこの場をお借りして感謝申し上げます。 会場は昨年と同じく、 ANAインターコンチネンタル東京 でした。 登壇させて頂いたセッション会場はこんな感じです。 CData社の杉本さん(左)と共同で登壇しました。 登壇中はCData社の疋田社長に撮影頂きました。ありがとうございました! 登壇内容 セッション概要 まず最初に、セッションの概要についてお伝えします。 当社では複数SaaS導入によりデータのサイロ化が加速し、財務会計/管理会計におけるデータ分析上の課題が発生しました。 この課題を解決する上でデータ分析基盤が重要な役割を担うことになったのですが、その背景やポイントについてご説明したいと思います。 当社のご紹介 本題に入る前に、当社について紹介させて下さい。 当社はクラウドカメラの映像プラットフォームを提供する会社で、会社名もサービス名もセーフィー(Safie)と言います。 当社のサービスに対応するカメラをご利用頂くと、リアルタイムで映像がクラウドにアップロードされ、Webやモバイルの専用アプリから確認できる、というのが基本機能になっています。 基本機能以外にも、YouTube Liveと連携する機能や、AIで人を検知する機能など、オプション機能も充実しています。 現在、当社のサービスに対応するカメラは全体で約26万台ご契約頂いており、ユーザー様の用途は多様化しています。 やはり防犯・監視用途で利用されることが多いのですが、防犯だけではなく、建設や工事の現場における異常検知や現場監督のリモート化など、人材不足解消を目的とした用途でも広く利用されています。 当社のサービスはオプション含め、基本的にサブスクリプション型で提供しているため、売上全体に占めるリカーリング売上の割合が大きいです。 また、当社はカメラ本体の販売や設置工事も行っておりますので、一般的なSaaS企業と比較するとスポット売上の割合が大きいのが特徴的だと思います。 プロジェクト背景 当社では元々、サブスクリプションの契約管理にSalesforceを使用していました。 しかし、Salesforceはいわゆるワンショットの取引をベースに設計されているサービスなので、サブスクリプションの契約、更新管理、月単位での請求のレコード自動生成等はアドオンを開発しなければならず、オペレーションコストや開発コストが非常に多くかかっていました。 そこで、ビジネスオペレーションの最適化、オペレーションコストの削減や開発コストの抑制を目的として、サブスクリプションに特化した契約管理サービスのZuoraを導入することになりました。 しかし、Zuora導入に伴い別の課題が浮上しました。端的に申し上げると、データのサイロ化です。 Zuora導入後もSalesforceはSFAとして継続利用するため、商談や商流、各種マスタデータはそのままSalesforceで維持されます。 一方、Zuora導入後はSalesforce上のサブスクリプション契約関連オブジェクトが更新されなくなるため、元々Salesforceのレポート機能で完結していた財務会計/管理会計に関連するレポートも更新されなくなるという課題が発生しました。 プロジェクト始動 前述の課題が顕在化したのはZuora本番稼働開始の数ヶ月前。上場企業としてタイムリーに会計業務を遂行できなくなることは、経営上の大きなリスクでした。 この課題を解決するためには、サイロ化している各種データをどこかで結合し、財務会計/管理会計レポートを再構築する必要がありました。そこで声を掛けられたのが、私が管轄しているデータ分析基盤グループです。 データ分析基盤グループでは元々、 Safieプラットフォーム(サービスのバックエンドDB)のデータをSnowflake(DWH)に連携し、Tableau(BIツール)で可視化する仕組みを構築していました。 しかし、SalesforceやZuoraをSnowflakeに連携する仕組みは構築していませんでした。当初はスクラッチ開発で仕組みの構築を検討しましたが、データエンジニアのリソースが不足している事情もあり断念。 短期間で確実に開発を終えられる方法を模索した結果、Salesforceのデータ連携用に CData Sync 、Zuoraのデータ連携用に Zuora Secure Data Share for Snowflake (Zuora社からSnowflakeのデータシェアリング機能でデータを共有頂くサービス)を導入しました。 各ツールの検証には1ヶ月程度掛かりましたが、CData社やZuora社のサポート、社内のSalesforceを管轄するチームと密に連携し、何とか正式導入に漕ぎ着けることが出来ました。 ちなみに、CData SyncはAWSのEC2インスタンスでホスティングして運用しています。 そして、既存の財務会計/管理会計レポートのロジックを、各種データを掛け合わせた形で再構築し、Tableauで可視化。会計業務の継続に大きく貢献することができました。 CData Syncの採用ポイント CData Syncの導入はすんなり決まったわけではなく、OSSのツールをホスティングする方法や、各種データ連携SaaSと比較検討していました。 CData SyncはGUIベースで容易にジョブの実装が可能であり、履歴化処理やメタデータの自動検出によるスキーマ同期機能・dbt Cloudとの連携機能を有しているため、少ない開発リソースでも短期間で実装可能なイメージを持つことができました。 また、中長期的にはSalesforce以外のSaaSとの連携にも活用でき、価格帯も予算にフィットしたことから、当社のデータ分析基盤にはCData Syncが適していると判断しました。 今後の展望 今回のプロジェクトは短納期かつデータエンジニアの工数が不足していた事情から、やむを得ずSalesforceやZuora由来のデータにはデータモデリングを適用せず、Tableau側でデータの結合や変換処理の大部分を実装しました。 そのため、開発者目線だと運用・保守の負荷が高く、利用者目線だとTableauを経由しないと財務会計/管理会計に関するデータを閲覧できないという課題が生じています。 また、コスト抑制の観点から、Tableauのライセンスは定期利用の見込みがある一部の利用者にしか提供できないという社内事情も相まって、データの民主化のボトルネックになっています。 現在はこのような状態ですが、 今後はSalesforceやZuora由来のデータにもデータモデリングを適用し、Tableau側の処理依存度を低下させ、データ分析基盤全体の拡張性や保守性を向上し、データの民主化を推進できる土壌を整えていきたいと考えています。 余談 イベント登壇前後で会場を徘徊しておりましたので、当日の写真を数点共有します。会場の雰囲気が少しでも伝われば幸いです。 ちなみに、Snowflake社公式で Snowflake World Tour Tokyo 2024のハイライト動画 が公開されていますので、こちらもぜひチェックしてみて下さい。 会場風景 Keynoteセッションは満席。Snowflake社のスリダール・ラマスワミCEOが登壇していました。 Snowflake界のアイドル(?)くま太郎。可愛さが爆発していました。 CData社のブースには、 SafieのAIソリューション「AI-App人数カウント」のデータ連携にCData Syncが対応した件 のチラシを置いて頂きました。ありがとうございます! 来訪者には、Snowflakeに連携したいサービスにシールを貼ってもらっていたようです。 Safieにもシールが貼ってありますね! Snowflakeの資格を持っていると素敵なグッズが貰えます。 今年はイケてる缶バッジとTシャツでした。家宝にします。 We are hiring! 最後になりますが、データ分析基盤グループでは一緒に働いてくれる仲間を大募集中です! セーフィーのデバイスやアプリケーションからは1日あたり約80億件のログが発生し、データ量は線形的に増加しています。 このような超ビッグデータを効率的に処理し、ビジネスの意思決定に繋げるための仕組みを一緒に作ってみませんか? ご興味のある方は セーフィー採用サイト を覗いてみて下さい。 皆様のご応募、心よりお待ちしております!
アバター
この記事は Safie Engineers' Blog! Advent Calendar 17日目の記事です はじめに こんにちは、第1開発部でサーバーサイドエンジニアをしている伊東公平です。今回は、昨年の11月から今日まで1年以上に渡って、スクラムマスターとしてチームの改善のために試行錯誤してきた内容についてお伝えします。 はじめに 課題 取り組んだこと スクラムの基盤づくり チームの強化 適切なツールへの移行 プロダクトバックログ スプリントバックログ 最後に 課題 私がスクラムマスターとして就任した際にはすでにチームでスクラム開発を採用していました。しかし、その中での課題はまだ多くありました。その中でも私個人としては、以下のようなものに改善するメリットがあると感じていました。 課題①: スプリントゴールが不明瞭で、スプリントの途中で現時点の進捗を把握する手段が少ない。そのため、チームとしての目的意識が保ちづらい 課題②: タスクの背景理解ができていないままタスクをとり、実装することで仕様とのズレが生じ、手戻りが発生しやすくなる 課題③: 使用しているツールがバラバラで、ツール間の連携がうまく取れていないため、ツールの管理や移行に時間がかかる 取り組んだこと 上記のような課題を感じていた中で、これに対処するために大きな取り組みとして以下のようなことを1年に渡り行ってきました。この3つの取り組みに対し、それぞれの目的と効果について以降では説明していきたいと思います。 スクラムの基盤づくり スクラムマスターとして最初に取り組んだのは、チーム内でのスクラムに対する認識やルールを整えることでした。これは先に挙げた課題①を解決するために行いました。 スクラムイベントの開催日時を見直し固定化 当初、スクラムイベントが開催される日時が非効率な点がいくつかありました。 例えば、スプリントの切り替わりで半日時間が空いてしまうため、スプリント間の連続性がなくなっていたというようなものです。 そのため、以下のようにスプリントの振り返りとプランニング間での連続性を持たせるようにスプリントのスケジュールを見直しました。特にスプリントの振り返りとプランニングは振り返りを行ったことを即座にプランニングで反映できるようにすることを意識しました。 定量的に現在の進捗を計測 チーム内でスプリントの途中でも現在の進捗を定量的に確認できるように、以下のようなバーンダウンチャートを作成しました。これにより、進捗が遅れている場合は朝会でそれを確認できるようになり、振り返りの際にその原因をチームで会話できる体制を整えました。 上記の2点の取り組みにより、スプリントの区切りがはっきりし、進捗も明らかになり、いつまでにスプリントゴールを達成するといった意識がより強くなったと考えています。 チームの強化 次にチーム内で取り組んだのは 強いチーム 作りです。私の中の定義としては、「一人一人が自律し、スプリントでのタスクを消化できるような状態」を目指すために行いました。 これは課題②の解決にもなると考え行いました。 具体的には、プランニングの際に以下のようなユーザーストーリーを作成するようにしました。 ユーザーストーリーとは、エンドユーザー目線から必要な機能をタスクに分解していく手法です。青枠がサービスの機能でそれを黄色枠部分のユーザーストーリー、さらにその下のタスクまで分解する作業をチームメンバー全員で行うようにしました。 ユーザーストーリーを作成することで、タスクの背景を重点的に説明する機会が増え、チーム全員が以前より背景を理解した上でタスクの消化を行うことができるようになりました。また、タスクを作成する際の属人化を避けられるようになり、チームメンバーの誰でもタスクを作成できるという意識づけもできたと考えています。 適切なツールへの移行 最後に、スクラムイベントを行うにあたり、私のチームで使用しているツールを使用方法や現状のチームに合わせていくつか移行してきましたので、その紹介をしたいと思います。 これは、課題③でもあったように、複数ツールを使用することにより、ツール間の連携が取りづらく、タスク管理が二重になってしまっていたり、うまくそのツールを有効活用できていなかったため、この負を解決するために移行しました。 以下に改善前の使用ツールと改善後の使用ツールの一覧を載せます。 種類 改善前 改善後 プロダクトバックログ Miro GitHub Projects スプリントバックログ Backlog GitHub Projects 振り返り Jamboard Miro 上記の移行により、最も効果が現れたのは、プロダクトバックログとスプリントバックログ をGitHub Projectsへ移行したことでした。以下のように、同じツールを使用することで二重管理から解放され、プルリクエストとの紐付け等も行いやすくなりました。 プロダクトバックログ スプリントバックログ 最後に 今回のブログでは、この一年間に渡ってスクラムマスターとして自分のチームをどのように変革してきたのかを紹介しました。ここでは紹介しなかった変革も沢山ありますし、失敗したものもいくつもありますが、全て挑戦して良かったと思っています。 また、今回紹介した内容が全てのチームに当てはまるわけではないので、参考にされる場合も「そんな考え方があるのかと」と思うぐらいで軽く考え、自チームにあった方法に改善して活用していただけると嬉しいです。
アバター
はじめに   この記事は Safie Engineers' Blog! Advent Calendar 16日目の記事です  セーフィーのサーバーサイドエンジニアの三村です。  セーフィーのサーバーサイドでは箇所によって色々な言語(Python, Go, Java…)が用いられていますが、コードベースの大部分はPythonを用いています。本記事では、そんな普段セーフィーのエンジニアが用いているPython( *1 )のガベージコレクションの仕組みについて調べたので、まとめてみます。 はじめに 概要の説明 参照数によるガベージコレクションの実装 概要 intのオブジェクトでの例 世代別の循環参照検知のガベージコレクションの実装 世代とは オブジェクトのリストの作成 オブジェクトのリストの走査 到達不能のオブジェクトの探索 まとめ 概要の説明  多くのプログラミング言語には、ユーザーが明示的に行なわずとも自動で不要なデータをメモリから解放するガベージコレクションの仕組みがあります。こちらは各言語ごとに仕組みや詳細な実装は異なっていますが、Pythonの場合には2種類の自動のメモリ解放の仕組みがあります。  一つ目は、Pythonの全てのオブジェクトに被参照数のカウントを持つようにして、それが0になったらメモリ解放をするという単純なものです。  もう一つは、上記の手法では対応できない循環参照の検知のための仕組みです。全てのオブジェクトを「世代」に分けてトラックしておき、それらの中で使われていないものを探索し開放する手法のものです。 参照数によるガベージコレクションの実装 概要  Pythonのオブジェクト(Cのコードでは PyObject と呼ばれる)は全て、被参照数をデータとして持っています。こちらのカウンターが0になるとメモリ解放がされます。こちらの、PyObjectの被参照数を減らして数が0になれば削除をしている実装を見てみます。 static inline Py_ALWAYS_INLINE void Py_DECREF (PyObject *op) { // Non-limited C API and limited C API for Python 3.9 and older access // directly PyObject.ob_refcnt. if ( _Py_IsImmortal (op)) { return ; } _Py_DECREF_STAT_INC (); if (--op->ob_refcnt == 0 ) { _Py_Dealloc (op); } } cpython/Include/object.h at v3.12.7 · python/cpython · GitHub  引数で受け取ったPyObjectの被参照数を表す ob_refcount をデクリメントして、値が0になれば _PyDealloc というメモリ解放の関数を呼び出しています。 呼び出される _PyDealloc の関数は、以下の実装になっています。 // デバッグモードの時等の挙動が長いので、それらを省いて簡略化 void _Py_Dealloc (PyObject *op) { PyTypeObject *type = Py_TYPE (op); destructor dealloc = type->tp_dealloc; (*dealloc)(op); } cpython/Objects/object.c at v3.12.7 · python/cpython · GitHub  削除対象のオブジェクトの型( Py_TYPE )がそれぞれ持つ tp_dealloc という関数を呼び出してメモリ解放を行なっています。このように、Pythonのオブジェクトの型ごとにメモリ解放の実装は異なっています。 intのオブジェクトでの例  いくつもあるPythonの型の中で、一例としてint型のオブジェクトの参照数によるメモリ解放の例を見てみます。( *2 ) long_dealloc (PyObject *self) { /* This should never get called, but we also don't want to SEGV if * we accidentally decref small Ints out of existence. Instead, * since small Ints are immortal, re-set the reference count. */ PyLongObject *pylong = (PyLongObject*)self; if (pylong && _PyLong_IsCompact (pylong)) { stwodigits ival = medium_value (pylong); if ( IS_SMALL_INT (ival)) { PyLongObject *small_pylong = (PyLongObject *) get_small_int ((sdigit)ival); if (pylong == small_pylong) { _Py_SetImmortal (self); return ; } } } Py_TYPE (self)-> tp_free (self); } cpython/Objects/longobject.c at v3.12.7 · python/cpython · GitHub  条件分岐を経て、最後の部分でメモリ解放( tp_free の中で実際にはされています)がされています。  余談ですが、上で引用したコードの中ではこのintの値が特定の基準よりも小さいかどうかを判定( IS_SMALL_INT ) *3 して、小さければ _Py_SetImmortal をしてガベージコレクションにかけられないようにしています。こちらはPython3.12以降には、使用頻度が高いことが見込まれる小さい値のintのオブジェクトを削除しないようにした、パフォーマンス向上目的の変更です。こちらの仕様の導入には色々議論があったようで、気になった方は こちら のPRを見てみてください。 世代別の循環参照検知のガベージコレクションの実装  上記のようなシンプルな参照数によるガベージコレクションがあれば、それで足りるのではないかと思うかもしれませんが、実際はそれでは済みません。Pythonのオブジェクト同士の循環参照のケースが、上のやり方だとカバーできないためです。このケースを避けるため、Pythonにはもう一つ世代別のガベージコレクションの仕組みが存在します。 世代とは  (プログラミングの実行時の一般論として)ほとんどのオブジェクトは作成直後に使用されなくなる傾向があり、作成直後のオブジェクトのリストのほうが作成から時間の経ったオブジェクトよりも、ガベージコレクションの対象となる確率が高くなります。  そのため、循環参照の検知のような重めの処理では、「世代」の若い(作られて間もない)オブジェクトのチェックを頻繁に行い、「世代の古い」(作られて時間の経っている)オブジェクトのチェックはより稀に行うようにして、効率化を図っています。  この考え方は、Python独自のものではなくそのほかのプログラミング言語(Java等)でも一般的に用いられています。 オブジェクトのリストの作成  Pythonのこちらの方式のガベージコレクションでは、世代ごとに循環参照となり得るすべてのオブジェクト(=他のPyObjectへの参照を持っているPyObject)のリストを持ちます。その後、そのリストの内部を走査し、特定の世代内部のみで循環参照をしているものを見つけて、メモリ解放をします。  ここではまず、特定の世代のPyObjectを双方向リストとして保持する実装を見てみます。こちらの双方向リストには、オブジェクトの作成時に追加されます。以下でtupleのオブジェクトの生成時の例を挙げます。 PyObject * PyTuple_New (Py_ssize_t size) { PyTupleObject *op; if (size == 0 ) { return tuple_get_empty (); } op = tuple_alloc (size); if (op == NULL ) { return NULL ; } for (Py_ssize_t i = 0 ; i < size; i++) { op->ob_item[i] = NULL ; } _PyObject_GC_TRACK (op); // 筆者注: ここで作成したオブジェクトを、トラックできるようにしている return (PyObject *) op; } cpython/Objects/tupleobject.c at v3.12.7 · python/cpython · GitHub  引用したコードの最後の方で _PyObject_GC_TRACK 関数を呼び出して、世代別ガベージコレクションで追跡できるよう、作成したtupleのオブジェクトを双方向リスト(各要素が、自身の前と次へのリンクを持っているリスト)に追加しています。  こちらの _PyObject_GC_TRACK の処理の実装を見てみます。 static inline void _PyObject_GC_TRACK ( // The preprocessor removes _PyObject_ASSERT_FROM() calls if NDEBUG is defined #ifndef NDEBUG const char *filename, int lineno, #endif PyObject *op) { _PyObject_ASSERT_FROM (op, ! _PyObject_GC_IS_TRACKED (op), "object already tracked by the garbage collector" , filename, lineno, __func__ ); PyGC_Head *gc = _Py_AS_GC (op); _PyObject_ASSERT_FROM (op, (gc->_gc_prev & _PyGC_PREV_MASK_COLLECTING) == 0 , "object is in generation which is garbage collected" , filename, lineno, __func__ ); PyInterpreterState *interp = _PyInterpreterState_GET (); PyGC_Head *generation0 = interp->gc.generation0; PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev); _PyGCHead_SET_NEXT (last, gc); // 筆者注: この辺からリストへの追加を行なっている _PyGCHead_SET_PREV (gc, last); _PyGCHead_SET_NEXT (gc, generation0); generation0->_gc_prev = ( uintptr_t )gc; } cpython/Include/internal/pycore_object.h at v3.12.7 · python/cpython · GitHub  こちらのコード内の gc というものが、引数で受け取ったPyObjectをガベージコレクションで追跡できるような型にキャストしたものです。こちらを、現在のラインタイムにおける最も若い世代のPyObjectの双方向リスト( generation0 )の最後尾に追加しています。 オブジェクトのリストの走査  上で特定の世代のPyObjectのリストに要素を追加する実装例を見たので、次にそのリスト内を走査する実装を見て見ます。こちらは以下の gc_collect_main 関数で実行されています。 /* This is the main function. Read this to understand how the * collection process works. */ static Py_ssize_t gc_collect_main (PyThreadState *tstate, int generation, Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable, int nofail) { int i; Py_ssize_t m = 0 ; /* # objects collected */ Py_ssize_t n = 0 ; /* # unreachable objects that couldn't be collected */ PyGC_Head *young; /* the generation we are examining */ PyGC_Head *old; /* next older generation */ PyGC_Head unreachable; /* non-problematic unreachable trash */ PyGC_Head finalizers; /* objects with, & reachable from, __del__ */ PyGC_Head *gc; _PyTime_t t1 = 0 ; /* initialize to prevent a compiler warning */ GCState *gcstate = &tstate->interp->gc; // 略 // 筆者注: 1. 若い世代のマージ /* merge younger generations with one we are currently collecting */ for (i = 0 ; i < generation; i++) { gc_list_merge ( GEN_HEAD (gcstate, i), GEN_HEAD (gcstate, generation)); } /* handy references */ young = GEN_HEAD (gcstate, generation); if (generation < NUM_GENERATIONS- 1 ) old = GEN_HEAD (gcstate, generation+ 1 ); else old = young; validate_list (old, collecting_clear_unreachable_clear); // 筆者注: 2. 到達不能のオブジェクトの探索 deduce_unreachable (young, &unreachable); untrack_tuples (young); // 筆者注: 3. 到達可能のオブジェクトを古い世代に移動 /* Move reachable objects to next generation. */ if (young != old) { if (generation == NUM_GENERATIONS - 2 ) { gcstate->long_lived_pending += gc_list_size (young); } gc_list_merge (young, old); } else { /* We only un-track dicts in full collections, to avoid quadratic dict build-up. See issue #14775. */ untrack_dicts (young); gcstate->long_lived_pending = 0 ; gcstate->long_lived_total = gc_list_size (young); } // 略 // 筆者注: 4. 到達不能のオブジェクトの削除 /* Call tp_finalize on objects which have one. */ finalize_garbage (tstate, &unreachable); /* Handle any objects that may have resurrected after the call * to 'finalize_garbage' and continue the collection with the * objects that are still unreachable */ PyGC_Head final_unreachable; handle_resurrected_objects (&unreachable, &final_unreachable, old); /* Call tp_clear on objects in the final_unreachable set. This will cause * the reference cycles to be broken. It may also cause some objects * in finalizers to be freed. */ m += gc_list_size (&final_unreachable); delete_garbage (tstate, gcstate, &final_unreachable, old); // 略 } cpython/Modules/gcmodule.c at v3.12.7 · python/cpython · GitHub  上記で引用したコード( *4 )では、引数 generation で受け取った世代のリストのコレクションを実行します。以下のような手順でオブジェクトのリストを走査しています。 1.若い世代のマージ  本関数でコレクションをする対象の世代のリストに、それよりも若い世代のコレクション対象リストをマージします( gc_list_merge 関数)。  例として、第3世代のコレクションをしている場合は、第3世代のリストに第1世代と第2世代のリストをマージして、このリストをガベージコレクションのチェックの対象とします。こうすることで、若い世代のリストほど、ガベージコレクションの走査の対象となる頻度が高くなります。 2.到達不能のオブジェクトの探索   1. 若い世代のマージ で作成したリストの中から、到達不能なオブジェクトを探し出します( deduce_unreachable 関数)。この関数の中では、循環参照を検知(後述)して、参照はあるものの実際は到達不能なオブジェクト(=参照数カウントのガベージコレクションでは検知できないが、実際は削除するべきオブジェクト)を探します。 3.到達可能のオブジェクトを古い世代に移動   2. 到達不能のオブジェクトの探索 でのチェックで引っ掛からなかった、コレクションの対象とすべきでないオブジェクトを、一つ古い世代に移します。こちらは先述した通り、作成されてから時間が経ったオブジェクトほど削除対象となる確率が下がるためです。 4.到達不能のオブジェクトの削除   2. 到達不能のオブジェクトの探索 で削除が必要と判明したオブジェクトを、実際に削除します。 到達不能のオブジェクトの探索  上で、PyObjectのリストから到達不能なものを探索して削除する実装を軽くみてみました。次に、具体的にどのようなロジックで到達不能なオブジェクトを探しているのかを見てみます。上で到達不能なオブジェクトを探索するのに用いていた、 deduce_unreachable の実装を引用します。 // 筆者注: コードに元々あったコメントを削除し、筆者のコメントを追加 static inline void deduce_unreachable (PyGC_Head *base, PyGC_Head *unreachable) { validate_list (base, collecting_clear_unreachable_clear); update_refs (base); // 1. GC用のカウンターをセット subtract_refs (base); // 2. 他のオブジェクトからの参照を探して、その分カウンターを減らす gc_list_init (unreachable); move_unreachable (base, unreachable); // 3. カウンターの値を元に、削除対象のオブジェクトを探す validate_list (base, collecting_clear_unreachable_clear); validate_list (unreachable, collecting_set_unreachable_set); } cpython/Modules/gcmodule.c at v3.12.7 · python/cpython · GitHub  まず、 1. GC用のカウンターをセット の部分で、検査対象の世代の全オブジェクトのリスト(実装上は containers と呼ばれる)を走査し、参照数によるガベージコレクションに用いられている obj_refcnt とは別に本処理用に gc_refs というカウンターを設定します。新しく設定した後者のカウンターには、デフォルト値として前者と同じ値を入れます。  以下に、簡単な図解を用意します。ここでは、オブジェクト A から E までがあり、それら全てに gc_refs の値を参照数である obj_refcnt の値を設定しています。この例だと、 A , B , C はこの世代の外から参照されており削除対象とはなりませんが、 D と E はお互いの循環参照のみによって参照されていて、もう利用されておらずガベージコレクションで削除されるべきオブジェクトになります。 各オブジェクトにおいて、obj_refcntの数をgc_refsにコピーする  次に、上で引用したコードの 2. 他のオブジェクトからの参照を探して、その分カウンターを減らす の部分で、GC対象のオブジェクトのリストでイテレーションを回します。その中で、それぞれのオブジェクトが持つ他のオブジェクトへの参照を探し出して、参照先のオブジェクトの gc_refs のカウンターを1ずつ減らします。  例えばオブジェクトがdict型であれば、自身の辞書のkeyとvalueの両方のオブジェクトの gc_refs をデクリメントします。( *5 )  以下の例だと、 A は B への参照を持つため B のgc_refsを減らし、同様に B は C への参照を持つためCのgc_refsを減らします。 D と E はお互いを参照しているので、お互いのgc_refsを減らします。 各オブジェクトの参照を辿って、参照先のオブジェクトのgc_refsを1ずつ減らしていく  その後、上記引用のコードの 3. カウンターの値を元に、削除対象のオブジェクトを探す の箇所で、 gc_refs の値を元に循環参照でのみ参照されているオブジェクト(=参照数が0ではないがもう利用されていないオブジェクト)を探します。  まず、 gc_refs が0であるオブジェクトと1以上であるオブジェクトに分離します。オブジェクトに参照がある場合はそのまま containers に置かれ、なければ unreachable というリストに移されます。   gc_refs が0である場合、この世代の外側から参照されていない可能性があります。こちらが、ガベージコレクションの対象となるオブジェクトの候補のリストとなります(後述する通り、この時点ではコレクションの対象となると確定したわけではないです)。  逆に gc_refs が1以上であれば、それは現在使用されているオブジェクトであると判定できます。この場合、この世代以外の箇所からも参照されていると判断できるため、この時点でガベージコレクションの対象からは外れます。  下記の例だと、gc_refsが1以上であり確実に利用されていると言えるのは A のみです。ただし、gc_refsが0である unreachable のリストの中には、確かにコレクション対象となるべき D と E も存在しますが、 A から利用されていて消されるべきではない B と C も存在します gc_refsが1以上であれば外部からの参照が確実にあると言えて、そうでなければ到達不能な可能性がある  次に、 gc_refs > 0 のリストのオブジェクトでイテレーションを回して、それぞれのオブジェクトが持つ参照の中に、 gc_refs == 0 のリストに入れられたオブジェクトがないかを探します。  もし gc_refs が0であったとしても(=オブジェクト間の循環参照のみによって参照されていても)、消してはいけないオブジェクトによって参照されているのであれば、そのオブジェクトはまだ利用されていると判断できます。  元々 gc_refs が0である削除候補のリストに入っていたとしても、上記のチェックでまだ利用されていると判断された場合、 gc_refs が1以上のリストの方に移動されます。  下記の例だと、 A からの参照があることにより B がcontainersに戻され、同様に B からの参照があることにより C が戻されます。 object Bからobject Cにたどり着いたので、Cのgc_refsを増やしてunreachableリストから除く  上のチェックが全て終わった後も、まだ削除候補のリストに入っているオブジェクト(図の例だとオブジェクトの D と E )は、削除されてメモリ解放がされます。 まとめ  このように、Pythonのガベージコレクションは基本的には参照数を元に行なっていて、それではカバーできない循環参照を別途探して削除しており、後者は重い処理であるため世代の概念を導入しています。  セーフィーではエンジニアを積極的に募集しています。気になる方は以下をご覧ください。 カジュアル面談から受け付けておりますので、お気軽にご応募ください。 safie.co.jp *1 : この記事でPythonという時、CPythonの3.12のバージョンのことを指します *2 : Cのコード内でオブジェクト名等は Long となっていますが、これは Pythonの世界的にはintのことを指します *3 : -5から257の間であるかを判定 *4 : 引用したコードはだいぶ省略していてかなり簡略化されているので、より詳細な実装が気になる方はコードの方を見てみてください *5 : dict型オブジェクトにおける実装は こちら
アバター