hacomono TECH BLOG フィットネスクラブ・スクールなど施設・店舗のための会員管理・予約・決済システム「hacomono」 開発チームの技術ブログ 2024-03-26T17:00:00+09:00 hacomono-tech Hatena::Blog hatenablog://blog/26006613682949162 SETチームのモブプロミングの手法を活用したフロー効率を最大化する取り組み hatenablog://entry/6801883189093614717 2024-03-26T17:00:00+09:00 2024-03-26T17:00:01+09:00 こんにちは、QA部SETチームのモーリーこと森島です! SETチームではペアプロミング・モブプログラミングを週に2〜3回実施しており、イテレーション内に目標が確実に達成できるように取り組んでいます。 今回はその取り組みの事例とそれによりどんな効果を得られているかを紹介しようと思います。 はじめに(対象となる読者) SETではイテレーションの目標となるトピックに紐づくタスクをペアやモブで作業しています。 一般的にペア・モブプログラミングというと複数人でコーディングをする作業を指すと思います。 SETチームでは一般的なコーディングをすることもありますが、大半はコーディング以外の作業が多い状況です。… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240326/20240326144537.png" width="1200" height="674" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは、QA部SETチームの<a href="https://twitter.com/m39ryo">モーリーこと森島</a>です!</p> <p>SETチームではペアプロミング・モブプログラミングを週に2〜3回実施しており、イテレーション内に目標が確実に達成できるように取り組んでいます。<br/> 今回はその取り組みの事例とそれによりどんな効果を得られているかを紹介しようと思います。</p> <h1 id="はじめに対象となる読者">はじめに(対象となる読者)</h1> <p>SETではイテレーションの目標となるトピックに紐づくタスクをペアやモブで作業しています。<br/> 一般的にペア・モブプログラミングというと複数人で<u>コーディングをする作業</u>を指すと思います。<br/> SETチームでは一般的なコーディングをすることもありますが、大半はコーディング以外の作業が多い状況です。</p> <p>そのため今回は記事のタイトルの通りモブプログラミングの手法を活用した内容になりますので、一般的なモブプログラミングに対するプラクティスを期待されていた方はご注意ください。<br/> 一方で、コーディング以外の作業にもモブプログラミングのエッセンスを取り入れてみたいと考えている方には参考になると思います。</p> <h1 id="ペアプログラミングモブプログラミングとは">ペアプログラミング・モブプログラミングとは</h1> <p>今回の記事では以下のように定義します。</p> <ul> <li>ペアプログラミング(以降、ペアプロと呼ぶ)=同じタスクを同期的にペア(2人)のチームで取り組むこと</li> <li>モブプログラミング(以降、モブプロと呼ぶ)=同じタスクを同期的に3人以上のチームで取り組むこと</li> </ul> <p>これ以降の説明にペアプロ・モブプロと記載しますが、前述の通り一般的に想像されるものとは違い、複数人で同期的に同じタスクを進行する呼称として使いますので、誤解なきようお願いいたします。</p> <h1 id="SETチームで実施してるペアプロモブプロ">SETチームで実施してるペアプロ・モブプロ</h1> <h4 id="SETチーム3人のメンバー構成">SETチーム(3人)のメンバー構成</h4> <p>hacomono在籍期間や製品知識もバラバラでチームとしては新しくできたばかりです。</p> <ul> <li>はま: <ul> <li>ベテランSETエンジニア</li> <li>hacomonoの機能についてなんでも知ってると言っても過言ではない</li> <li>プロダクトコードにも詳しい</li> </ul> </li> <li>モーリー(私): <ul> <li>準新入社員(4ヶ月)</li> <li>hacomonoの機能?完全に理解した(=わかった気になっている)</li> </ul> </li> <li>muga: <ul> <li>新入社員(1ヶ月)</li> <li>hacomonoの機能はほとんど知らない</li> </ul> </li> </ul> <h4 id="ペアプロモブプロで取り組んでいるタスク例">ペアプロ・モブプロで取り組んでいるタスク例</h4> <p>主にmablによるE2E自動テストの実装やテスタビリティの向上のためにプロダクトコードのフロントエンドにdata-testidの属性を追加するなどの作業でペアプロ・モブプロ活用しています。</p> <ul> <li>mabl <ul> <li>テスト作成(モブプロ)</li> <li>テストリファクタリング(ペアプロ)</li> <li>成果物ピアレビュー(ペアプロ)</li> <li>新機能を試そうワイワイ会(モブプロ)</li> </ul> </li> <li>data testid <ul> <li>コーディング(モブプロ)</li> </ul> </li> </ul> <h4 id="ペアプロモブプロ開催方法">ペアプロ・モブプロ開催方法</h4> <p>イテレーションが始まるときに<u>事前</u>に計画したり、デイリーミーティングの状況により<u>当日</u>に計画したり、ここ詰まってますといったヘルプが必要なときに<u>突発</u>的に、とタイミングは様々です。</p> <ul> <li>事前:Googleカレンダーで予定作成して開催する</li> <li>当日:朝会の相談でタスク状況に応じてカレンダーに予定作成して開催する</li> <li>突発:その場でSlackで呼びかけて開催する</li> </ul> <h4 id="ペアプロモブプロ進行方法">ペアプロ・モブプロ進行方法</h4> <p>以下のような2パターンがありますが、基本的には1のドライバーとナビゲーターに分かれる一般的なペアプロ・モブプロと同じ手法を多く使っています。</p> <ol> <li>ドライバーがナビゲーターの指示に従って作業する</li> <li>作業を細かく相談しながら手分けしてやる</li> </ol> <p>2を使うときはペアプロ・モブプロというよりはただの同期的な作業と言えます。<br/> hacomonoはフルリモート企業のため同期的な作業を実施するには、オンサイトでのコミュニケーションよりひとつ壁があると思ったためあえて列挙しました。</p> <h4 id="ペアプロモブプロ作業環境">ペアプロ・モブプロ作業環境</h4> <p>前述の通り働き方はフルリモートになるため、オンラインでペアプロ・モブプロを実施します。<br/> リモートでの作業環境は以下の通りです。</p> <ul> <li><strong>Slack ハドルミーティングで通話しながら作業者の画面を共有する</strong> <ul> <li>作業者の画面を見ながら指示者は次に書くことを丁寧に指示する</li> </ul> </li> <li>VS CodeでLiveShare機能を使ってコードを共同編集する <ul> <li>date testidはこっちを利用(mablは共同編集機能がないため)</li> </ul> </li> </ul> <p>Slack ハドルミーティングを採用していて、以下のようなメリットがあると感じています。</p> <ul> <li>共有した画面にペンで書き込みができる <ul> <li>口頭だけで伝わりづらいときに箇所を明示できるのが便利</li> </ul> </li> <li>他の人にもミーティングしていることがわかる <ul> <li>ハドルミーティングにトピックを設定できてわかりやすい</li> <li>トピックに興味があれば気軽に参加できる(Google Meetだと招待されていないと参加しづらい)</li> </ul> </li> </ul> <p>LiveShare機能は最近使ってみたばかりですが、ファイルを共同編集できるので意思疎通も早く感じましたし、繰り返しの作業が必要になったときに分担できて楽でした。</p> <h1 id="ペアプロモブプロでやると嬉しいこと">ペアプロ・モブプロでやると嬉しいこと</h1> <p>これまでペアプロ・モブプロでやってみて感じたメリットを共有します。</p> <ul> <li>一つのタスクの完了が早い(リードタイムが短い) <ul> <li>成果物完成までの不確実性が高い作業の場合、相談しながら一気に進められる</li> <li>作成と議論/相談・レビューがその場で解消するので待ち時間が発生しない <ul> <li>待ち時間に別の作業をやるとコンテキストスイッチが発生し、また思い出すのが大変</li> </ul> </li> <li>他のタスクの邪魔が入らず集中できる</li> </ul> </li> <li>各自の知識・知見・TIPSが共有される <ul> <li>作業者の画面を見ているときに「あ、ここをこうすると早くできますよ」などのTIPSや便利なツールをアドバイスしてもらえる</li> </ul> </li> <li>プロダクトの知識差があってもプロダクトに貢献しやすい <ul> <li>ドライバーを新人が担当することでOJTのようにも利用できる</li> <li>各自の得意分野やどの辺の知識の強化が必要なのか、が少し可視化される</li> </ul> </li> <li>モチベーションや勝手な優先度に左右されない <ul> <li>時間が決まっているので時間がなくてできませんでした、がない</li> <li>ちょっと方針考えたり調査するのが大変だから後回しにしよ、もない。ただやるのみ</li> <li>みんなでワイワイやるのたのしい</li> </ul> </li> </ul> <h1 id="ペアプロモブプロでやると嬉しくないこと">ペアプロ・モブプロでやると嬉しくないこと</h1> <p>もちろんメリットだけではないので、デメリットもあります。</p> <ul> <li>ずっと話しながら作業するので疲れが早い <ul> <li>25分作業、休憩5分でワンセットなど適度な休憩は必須</li> </ul> </li> <li>リソース効率はよくなさそう <ul> <li>3人でやったら3人分の時間が同じタスクに注ぎ込まれるので、アウトプット自体は少ない</li> </ul> </li> <li>一人でもくもくとやりたい人はイライラしちゃいそう <ul> <li>メリット部分を享受できない場合は効率悪いと感じて不満がでるかも</li> </ul> </li> </ul> <h1 id="まとめ">まとめ</h1> <p>コーディング以外の作業にもペアプロ・モブプロをとてもおすすめしたいですが、むやみやたらにペア・モブにするのは注意が必要です。   個人的に下記のような使い分けをするとちょうど良いのではないかと思いました。</p> <p><strong><u>ペアプロ・モブプロに向いているタスク</u></strong></p> <ul> <li>タスクの担当者が作業者とレビュワーの往復が多く待ち時間が断続的に発生する</li> <li>タスクの担当者がタスクをどう進めたらいいかイメージが湧いていない <ul> <li>原因調査など未知数な</li> <li>新人が担当するタスクも同様と言える</li> </ul> </li> <li>タスクの完了における不確実性が高い</li> <li>テスト作成のための仕様書確認 <ul> <li>文面の理解が人によって異なるため全員で認識合わせをする</li> </ul> </li> </ul> <p><strong><u>ペアプロ・モブプロに向いていないタスク</u></strong></p> <ul> <li>誰が作っても同じ成果物になる(熟練度の考慮は除く) <ul> <li>テスト実行など手順書に従って作業するタスク(ただし手順書の完成度が高い必要がある)</li> </ul> </li> </ul> <h1 id="終わりに">終わりに</h1> <p>ペアプロ・モブプロの手法を利用したチームでの作業の進め方を紹介しましたが、いかがでしたでしょうか。少しでも参考になれば幸いです。</p> <p>現在のところ一人で実施する対比としてペアプロ・モブプロを取り上げましたが、意識的にペアかモブかは分けてはいません。<br/> SETチームはこれからも拡大して人数が増えていく予定なので、今後はもう少しこの点について深堀りしてより高い成果を目指していきたいと思います。</p> <p><br> hacomono QAではQAエンジニアやSETエンジニアを積極的に採用しています。<br/> 少しでも気になったらぜひ気軽にお問い合わせやカジュアル面談を申し込みください。</p> <p><iframe src="https://open.talentio.com/r/1/c/hacomono/embed/pages/90115" width="100%" height="300" frameborder=0 title="%E6%A0%AA%E5%BC%8F%E4%BC%9A%E7%A4%BEhacomono+%7C++SET%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2"></iframe><cite class="hatena-citation"><a href="https://open.talentio.com/r/1/c/hacomono/pages/90115">open.talentio.com</a></cite> <iframe src="https://open.talentio.com/r/1/c/hacomono/embed/pages/90113" width="100%" height="300" frameborder=0 title="%E6%A0%AA%E5%BC%8F%E4%BC%9A%E7%A4%BEhacomono+%7C+QA%E3%82%B9%E3%83%9A%E3%82%B7%E3%83%A3%E3%83%AA%E3%82%B9%E3%83%88%E5%80%99%E8%A3%9C"></iframe><cite class="hatena-citation"><a href="https://open.talentio.com/r/1/c/hacomono/pages/90113">open.talentio.com</a></cite></p> hacomono-tech Polypaneとはいったい?レスポンシブデザインのためなブラウザを試してみた hatenablog://entry/6801883189092718470 2024-03-26T11:00:00+09:00 2024-03-26T11:00:00+09:00 こんにちは、稀に出没するhacomonoの門田です。 最近おやつカルパスにはまってます。 hacomonoにはサービスのUI・UXを担う部署としてUX部が存在しておりまして、自分も今はそこで日々駆けずり回っている次第です。 部のメンバーもありがたいことに徐々に増え、今後はフロントエンドの品質担保にも精力的に注力していくのでそんな内容の記事も今後出てくるかもしれないですね。 今回はレスポンシブデザインなサイトを開発する上でデベロッパー特化なブラウザであるPolypaneを触ってみた話をちょこっとさせていただければと思います。 Polypaneとはそもそも? Polypane(読みはポリペイン。お… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240322/20240322173824.png" width="1200" height="674" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは、稀に出没するhacomonoの門田です。<br/> 最近おやつカルパスにはまってます。</p> <p>hacomonoにはサービスのUI・UXを担う部署としてUX部が存在しておりまして、自分も今はそこで日々駆けずり回っている次第です。<br/> 部のメンバーもありがたいことに徐々に増え、今後はフロントエンドの品質担保にも精力的に注力していくのでそんな内容の記事も今後出てくるかもしれないですね。</p> <p>今回はレスポンシブデザインなサイトを開発する上でデベロッパー特化なブラウザである<strong>Polypane</strong>を触ってみた話をちょこっとさせていただければと思います。</p> <h1 id="Polypaneとはそもそも"><strong>Polypaneとはそもそも?</strong></h1> <p><a href="https://polypane.app/">Polypane</a>(読みはポリペイン。おそらく)はウェブ開発者とデザイナー向けに特化したブラウザで、一つのウィンドウ内で複数のビューポートを同時に表示し、異なるデバイスや解像度でのサイトの見え方をリアルタイムできるブラウザです。<br/> 開発時に都度デバイス・解像度の条件を切り替えてチェックをしていた作業を省略化し、シームレスにレスポンシブデザインをテスト・最適化することができます。</p> <p>無償利用のトライアルは2週間、有償の場合個人利用の範疇であれば月額おおよそ1500〜1800円、10人規模などのビジネス利用なら1userあたり600円規模になります。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fpolypane.app%2F" title="Polypane, The browser for ambitious web developers" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://polypane.app/">polypane.app</a></cite></p> <p><br></p> <h1 id="主な特徴">主な特徴</h1> <h4 id="-様々なサイズのビューを同時にプレビュー"><strong>① 様々なサイズのビューを同時にプレビュー</strong></h4> <p>様々な解像度とデバイスサイズに対応するウェブページの複数のビューを一つのウィンドウで同時に表示可能。<br/> 異なるデバイスやビューポートでのレイアウトの問題特定を早く行うことができます。<br/> ルーラーを表示することもできるため、要素の位置も正確にチェック可能です。</p> <p>自身でチェックするのはもちろんですが、スクリーンショットで第三者に共有する際にも一度に共有できるのは利点ですね。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240322/20240322142023.png" width="1200" height="750" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240322/20240322142119.png" width="1200" height="750" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>また、PolypaneはChromiumベースのブラウザのため、普段ChromeなどChromiumベースのブラウザで開発をしている場合は検証ツールも非常に近しい操作感を期待できると思います。</p> <p><br></p> <h4 id="同期するスクロールやクリック"><strong>②同期するスクロールやクリック</strong></h4> <p>複数のビューでスクロールを同期できるため、デバイス・解像度別でのファーストビューからのスクロール表示量やスクロールにより発火するイベントなどのチェックを複数のビューで同時にチェックすることができます。</p> <p>また、スクロールだけでなくクリックやホバー、タイピングも同期するのでそれらの動作チェックも同時に行うことができるようです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240322/20240322152825.gif" width="500" height="312" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><br></p> <h4 id="多種多様なアクセシビリティチェック"><strong>③多種多様なアクセシビリティチェック</strong></h4> <p>特に強い要素として、ウェブアクセシビリティの基準に基づいてページのアクセシビリティを評価し、改善点を提案してくれる機能が備わっています。</p> <h6 id="コントラストチェック"><strong>コントラストチェック</strong></h6> <p>コントラストチェックでは、各種要素の背景色に対してコントラスト比を出してくれますし、 どのカラーコードをあてれば最低基準である<strong>4.5</strong>をクリアできるかも示してくれます。<br/> 以下の画像では、赤字の箇所はそのままだと比率が4.26ですが<code>#d2443f</code> を指定してあげると4.5以上になることを教えてくれています。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240322/20240322163036.png" width="1092" height="462" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><br></p> <h6 id="WAI-ARIAチェック"><strong>WAI-ARIAチェック</strong></h6> <p>表示している画面内の各種要素で、<strong>WAI-ARIA</strong>要素を指定しているものがあればその要素の内容を画面上に表示してくれます。<br/> 属性の指定が正しいかどうか、指定の漏れがないかなどをチェックすることができます。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240322/20240322163106.png" width="952" height="642" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><br></p> <h6 id="親指の到達領域チェック"><strong>親指の到達領域チェック</strong></h6> <p>指定サイズのデバイスを実際に操作する際に親指がどのあたりまで届きやすいのか、操作しやすいのかというのを色で可視化してくれる機能なんかもあったりします。思わずへーとなります。<br/> いかにユーザーにストレスフリーな操作感を与えるかなどの検証にも、こういった機能は役に立ちそうですね。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240322/20240322163209.png" width="667" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><br></p> <h1 id="締め">締め</h1> <p>Polypane、少し触っただけでも一般的なブラウザでの開発体験とは違うものを感じられます。</p> <p>今回は簡易な紹介なので触れてみた機能はPolypaneの中でも主要な項目のみですが、カスタマイズ要素や便利なツールがほかにも多く存在し、より細かい検証を行うことができます。<br/> 実際に日々のサービス開発時に利用するブラウザとして採用するかはまた別ですが、<br/> エンジニア自身が開発後の通しチェックを行う際や、デザイナーが実装デザインのレビューをする際などにはより効率的にレスポンシブ・アクセシビリティを確認することができるブラウザかと思います。</p> <p>もし興味を持っていただいた方は、まずはトライアルで是非試してみてください! <br> <br></p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています。 <br>採用情報や採用ウィッシュリストもぜひご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(プロダクトチーム向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech SETチームが取り組むmablを活用したE2E自動テストのイマを大公開 hatenablog://entry/6801883189090979404 2024-03-21T11:00:00+09:00 2024-03-21T11:00:01+09:00 こんにちは、hacomono QA部SETチームのモーリーこと森島です。 SETチームでは現在E2Eテスト自動化に積極的に取り組んでおり、その中でもmablで作成・実行する自動テスト作成を最も活用しております。 hacomonoでどのようにmablを活用しているかイマの状況をお伝えしたいと思います。 mabl運用状況 実行頻度 参考:1月の合計実行数 QA部では主に2週間に1度の定期リリースタイミングに合わせて、リグレッションテストを実施しています。 このリグレッションテストの位置づけは、機能テストが完了したあとリリース前にこれだけはインシデントが起きたら困るという部分に対して全体的に実施して… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240319/20240319204747.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは、hacomono QA部SETチームの<a href="https://twitter.com/m39ryo">モーリーこと森島</a>です。</p> <p>SETチームでは現在E2Eテスト自動化に積極的に取り組んでおり、その中でもmablで作成・実行する自動テスト作成を最も活用しております。<br/> hacomonoでどのようにmablを活用しているかイマの状況をお伝えしたいと思います。</p> <h1 id="mabl運用状況">mabl運用状況</h1> <h4 id="実行頻度">実行頻度</h4> <p>参考:1月の合計実行数 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240319/20240319205207.png" width="1200" height="520" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>QA部では主に2週間に1度の定期リリースタイミングに合わせて、リグレッションテストを実施しています。<br/> このリグレッションテストの位置づけは、機能テストが完了したあとリリース前にこれだけはインシデントが起きたら困るという部分に対して全体的に実施している最後の確認です。<br/> リグレッションテストには手動テスト・自動テストがあり、自動テストはmablで実行しています。<br/> 自動テストの実行はイマはSETチームが担当しており、手動テストはQA部の各プロダクトチームのQAメンバーが実施しています。</p> <p>その他に基盤の変更など影響範囲が想定しづらい場合も、簡易かつ全体的にテストを実行できる自動テストを実行して失敗が少なければリリース判断を、失敗数が多ければ失敗傾向から追加で手動テストを実施する範囲を決定したりしています。</p> <h4 id="mablの具体的な数値">mablの具体的な数値</h4> <h5 id="プラン数">プラン数</h5> <p><strong><u>4個のプランに分けてクラウド実行しています。</u></strong> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240319/20240319205332.png" width="1200" height="494" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これは複数の目的があり、ひとつは<u>自動テスト実行環境がイマは2つ</u>という制約があるため環境ごとに分けて実行して並列実行できるようにするためです。<br/> もうひとつはhacomono内でテスト同士で同時に成立しない設定項目を検証するにあたり実行する口を分けることで間違えて実行しないようにするためです。<br/> さらにテストの種類でも分けており、hacomonoで複雑な設定をしているシナリオテストやすべての機能で画面遷移、新規作成、参照や削除ができることだけのテストなどの違いがあります。</p> <p><strong><u>最大の並列実行テスト数はイマは46です。</u></strong><br/> mablの魅力のひとつでもある大量の並列実行に強いという点を活かして、テストを作成するときに並列実行できるかどうかを判断してプランに組み込んでいます。<br/> リリースごとに機能が増えているので進行形で自動テストを増やしているため、今後はもっと並列実行数があがるかもしれません。</p> <p>プランの中でも並列実行と逐次実行の使い分けをしており、イマは画像のような状態です。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240319/20240319205425.png" width="1200" height="733" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>hacomonoでの逐次実行の使い所は、先ほど説明した「テスト同士で同時に成立しないhacomonoの設定項目」を検証するためです。<br/> 自動テストの実行の流れとして、大部分のテストに当てはまる前提条件かつ相互に影響しない設定を並列実行で実施して先に確認し、このテストにしか当てはまらない設定を逐次実行で分けて後から確認しています。<br/> これによりテスト同士の影響による正しい結果が得られないことを防いでいます。</p> <p>逐次実行はやはりテスト実行時間が長くなるため、できるだけ並列実行できるように検討していますが<br/> イマはこの逐次実行内の判断が少し曖昧であるため、本当は逐次実行でなくても問題ないテストも含まれている可能性があります。<br/> 時々(イマは気分で)逐次実行のテストの中で並列実行できるテストはないかを整理しています。</p> <p><strong><u>テスト実行の成功率は87%です。</u></strong><br/> この数値は普段から意識していないですが、mablのダッシュボードに表示されているので公開します。<br/> 87%が高いとは思いませんが、日々テスト実行してたくさんの失敗数に悩まされている我が身としては体感より上の数値だと感じました。<br/> 他にmablをご利用の企業様はどれくらいの成功率でしょうか、高い成功率の場合はぜひコツを教えてください。。。</p> <p>最近の2ヶ月は87%でした。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240319/20240319205536.png" width="852" height="478" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240319/20240319205556.png" width="1200" height="104" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>昨年末の2ヶ月は80%でした <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240319/20240319205643.png" width="860" height="586" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h5 id="フロー数">フロー数</h5> <p><strong><u>有効なフロー数は約1800個です。</u></strong><br/> これは他社の利用よりも多いのではないかと推測していますが、理由としてテストはすべてフローで構成している背景があります。<br/> すべてフローにすることでテストが操作単位で見えて可読性が高い便利な面もあれば、mablではひとつのフローを再利用する時に選択までにステップ数が多かったり、フローをコピー&ペーストができない点などフローを利用するのが不便な点もあり、各企業で方針が分かれるのではないかと思います。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240319/20240319205727.png" width="822" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>hacomonoではフロー利用のガイドラインも決めており、そのガイドラインは過去のhacomono TECH BLOGでも触れているのでよろしければご覧いただけると嬉しいです。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftechblog.hacomono.jp%2Fentry%2F2023%2F12%2F20%2F0700" title="あとから参画したメンバーが助かったmablの運用ガイドライン - hacomono TECH BLOG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://techblog.hacomono.jp/entry/2023/12/20/0700">techblog.hacomono.jp</a></cite></p> <p><br></p> <h1 id="おわりに">おわりに</h1> <p>hacomonoでE2E自動テストとして活用しているmablの内容を公開しましたが、いかがでしたでしょうか。<br/> 今回はmablについてのみ触れていますが、Playwrightの導入も進めたりとツールに縛られずに目的に沿って利用するようにしています。</p> <p>また、hacomonoのSETチームの活動内容は、E2E自動テストだけに限らないため次回はそちらで取り組んだことがあれば記事を書きたいと思います。</p> <p><br> hacomono SETチームは積極的に採用をしています。<br/> 本記事でSETチームやmablの利用方法について気になった方はお気軽にお問い合わせください、まずはカジュアル面談でお会いしましょう。</p> <p><iframe src="https://open.talentio.com/r/1/c/hacomono/embed/pages/90115" width="100%" height="300" frameborder=0 title="%E6%A0%AA%E5%BC%8F%E4%BC%9A%E7%A4%BEhacomono+%7C++SET%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2"></iframe><cite class="hatena-citation"><a href="https://open.talentio.com/r/1/c/hacomono/pages/90115">open.talentio.com</a></cite> <iframe src="https://open.talentio.com/r/1/c/hacomono/embed/pages/90113" width="100%" height="300" frameborder=0 title="%E6%A0%AA%E5%BC%8F%E4%BC%9A%E7%A4%BEhacomono+%7C+QA%E3%82%B9%E3%83%9A%E3%82%B7%E3%83%A3%E3%83%AA%E3%82%B9%E3%83%88%E5%80%99%E8%A3%9C"></iframe><cite class="hatena-citation"><a href="https://open.talentio.com/r/1/c/hacomono/pages/90113">open.talentio.com</a></cite></p> hacomono-tech マルチワークスペース下のVSCodeでShopify.ruby-lsp導入 hatenablog://entry/6801883189090978960 2024-03-19T11:00:00+09:00 2024-03-21T18:28:02+09:00 こんにちは! hacomono スクール開発チームの一員、森山です。今回は小ネタです。 導入 早速ですが、rebornix.Ruby が Deprecated になって長らく経つので、弊プロジェクトにも Shopify.ruby-lsp を導入しました。 しかしながら、バックエンドとフロントエンドが混在するリポジトリで開発していると、上手く動作してくれなかったのです。 調べるとマルチワークスペースになるとのことなので、動作するように設定しましたよという話を書いておきます。 各設定ファイル まずは前提となるディレクトリ構成ですね。下記が完成形になりました。 リポジトリ/ ├── .vscode/… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240319/20240319035159.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは! hacomono スクール開発チームの一員、森山です。今回は小ネタです。</p> <h1 id="導入">導入</h1> <p>早速ですが、rebornix.Ruby が Deprecated になって長らく経つので、弊プロジェクトにも Shopify.ruby-lsp を導入しました。<br/> しかしながら、バックエンドとフロントエンドが混在するリポジトリで開発していると、上手く動作してくれなかったのです。<br/> 調べると<a href="https://code.visualstudio.com/docs/editor/multi-root-workspaces">マルチワークスペース</a>になるとのことなので、動作するように設定しましたよという話を書いておきます。</p> <p><br></p> <h1 id="各設定ファイル">各設定ファイル</h1> <p>まずは前提となるディレクトリ構成ですね。下記が完成形になりました。</p> <pre class="code" data-lang="" data-unlink>リポジトリ/ ├── .vscode/         ├── extensions.json  推奨拡張の設定ファイル         └── settings.json vscodeの設定ファイル ├── api/ バックエンド(弊プロダクトだとRails) └── Gemfile ├── app/ フロントエンド └── hacomono.code-workspace      ワークスペースの設定ファイル </pre> <p>詳しく個別にみていきましょう。</p> <p>1.<strong>.code-workspaceファイルの作成</strong></p> <ul> <li>「hacomono」のプロダクトなので <code>hacomono.code-workspace</code> と名前をつけて作成しています。 <ul> <li>バックエンド側を個別のワークスペースとして認識させるために、apiディレクトリを個別で”path”に指定して記述しました。</li> </ul> </li> </ul> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">folders</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">path</span>&quot;: &quot;<span class="synConstant">.</span>&quot; <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">path</span>&quot;: &quot;<span class="synConstant">api</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span> </pre> <p><br> 2.<strong>VSCodeにRuby-LSPの導入</strong></p> <ul> <li><code>.vscode/extensions.json</code> に、ワークスペース内に推奨・非推奨な拡張として下記を記述します。</li> </ul> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">recommendations</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">Shopify.ruby-lsp</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">unwantedRecommendations</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">rebornix.Ruby</span>&quot; <span class="synSpecial">]</span> } </pre> <ul> <li>VSCode上で <a href="https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp">Shopify.ruby-lsp</a> をインストールします。</li> </ul> <p><br> 3.<strong>バックエンド側にruby-lspを導入</strong></p> <ul> <li><code>api/Gemfile</code> にruby-lspを指定して、開発環境のみ使うように宣言します。</li> </ul> <pre class="code" data-lang="" data-unlink>group :development do gem &#39;ruby-lsp&#39; end</pre> <ul> <li>ターミナル上から <code>bundle install</code> を実行します。</li> </ul> <p><br> 4.<strong>settings.jsonの設定</strong></p> <ul> <li><code>.vscode/settings.json</code> に バックエンド側のGemfileのパスを指定します。 <ul> <li>hacomonoではrubocopで書式を揃えているので、ついでに<code>rubyLsp.formatter</code>を指定しています。</li> </ul> </li> </ul> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">rubyLsp.bundleGemfile</span>&quot;: &quot;<span class="synConstant">api/Gemfile</span>&quot;, &quot;<span class="synStatement">rubyLsp.formatter</span>&quot;: &quot;<span class="synConstant">rubocop</span>&quot; <span class="synSpecial">}</span> </pre> <p>以上で設定ファイルの作成・配置は完了です。次は動作確認ですね!</p> <p><br></p> <h1 id="動作確認">動作確認</h1> <ol> <li><p>コマンドパレットを開き「RubyLSP」と入力して、「RubyLSP: Start」を選択します。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240319/20240319033620.png" width="892" height="218" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p></li> <li><p>「Ruby LSP: indexing files: x%」 と表示されていればOK。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240319/20240319033647.png" width="378" height="37" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p></li> <li><p>Lintを試すことで動作確認するのも良さそうですね。 <iframe width="560" height="315" src="https://www.youtube.com/embed/W4X4YsKZR6c?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="TechBlog_20240319"></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=W4X4YsKZR6c">www.youtube.com</a></cite></p></li> </ol> <p><br></p> <h1 id="まとめ">まとめ</h1> <p>以上で、バックエンドとフロントエンドが混在するリポジトリにvscodeのRuby-LSPを導入する準備が整いました。<br/> これにより、VSCode+Rubyな開発環境が最適化され、より効率的な開発が可能になると思います。是非お試しください! <br> <br></p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています。 <br>採用情報や採用ウィッシュリストもぜひご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(プロダクトチーム向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech Notionのフロー情報を行き渡るようにする hatenablog://entry/6801883189089847089 2024-03-12T11:00:00+09:00 2024-03-12T11:00:01+09:00 こんにちは、QA部SETチームのモーリーです。 hacomonoに入社して3月で5ヶ月目になります。 hacomonoではドキュメントツールとしてNotionを利用しています。 今回はhacomonoのNotionの使い方でいくつか課題を感じていたので 解決のために実行したことをシェアしようと思います。 記事の本題に進む前に、Notionで扱っている種類について触れておきます。 下記のように情報を分類したときに、今回はフロー情報にフォーカスして課題に取り組みました。 ストック情報 あとから何度も更新がされて活用する「蓄積される情報」です。 例:仕様書、マニュアル、等 フロー情報 その場限りで更… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240311/20240311155341.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは、QA部SETチームの<a href="https://twitter.com/m39ryo">モーリー</a>です。<br/> hacomonoに入社して3月で5ヶ月目になります。</p> <p>hacomonoではドキュメントツールとしてNotionを利用しています。 今回はhacomonoのNotionの使い方でいくつか課題を感じていたので 解決のために実行したことをシェアしようと思います。</p> <p>記事の本題に進む前に、Notionで扱っている種類について触れておきます。<br/> 下記のように情報を分類したときに、今回はフロー情報にフォーカスして課題に取り組みました。</p> <ul> <li>ストック情報 <ul> <li>あとから何度も更新がされて活用する「蓄積される情報」です。</li> <li>例:仕様書、マニュアル、等</li> </ul> </li> <li>フロー情報 <ul> <li>その場限りで更新がされない「流れる情報」です。</li> <li>例:議事録、メモ、等</li> </ul> </li> </ul> <p><br></p> <h1 id="hacomonoで感じたNotionの課題">hacomonoで感じたNotionの課題</h1> <p>hacomonoに入社して過ごしてみたところ、以下のような課題を感じていました。</p> <p><strong><u>階層化が進んでいて情報が階層の奥にある</u></strong><br/> hacomono の Notion は階層を使って資料が整理されています。<br/> 上位の階層には”チーム別”があり、その配下にチームごとの階層がぶら下がりチームの情報などが貯まっています。</p> <p>チームや委員会などの議事録は、それぞれのページの配下に議事録データベースを作っています。<br/> ”個人別”という階層もあり、個人的なメモを個人配下に作成している印象がありました。</p> <p>これらのデータベースやページは、Notionの通知機能を利用していないので、作成されたことに会議の参加者以外には気づきません。<br/> 別の課題として個人専用ページという建付けが勝手に覗くのも悪い気がしてしまう、という意識も働いていました。</p> <p><strong><u>チームごと・個人のフロー情報に気付けない</u></strong><br/> hacomonoはValuesに”オープン&amp;フェアネス”を掲げており、情報はほとんどオープンになっています。<br/> そのためフロー情報(議事録やメモ)は取りに行けば見ることができるのですが、階層化により情報を積極的に取りにいく必要があります。<br/> 一見自分にしか関係ないと思う有用な情報や、チームの動き、個々人の動きなどが自分からアクセスしない限りにはわからない状態になっています。</p> <p><strong><u>メモを書きたいときに場所に迷う</u></strong><br/> ちょっとした情報(メモ)を書きたいときに、どこに書くかを意識する必要もありました。<br/> 議事録やナレッジは書くところが決まっていますが、個人的なTIPSや思考の過程などは特に決まりはないため チームに共有すべき情報かどうかなどの判断に迷ってしまう場面があります。</p> <p>結果、とりあえず個人別に書き、本当は有用な情報が日の目を浴びない状態になってしまいます。</p> <p><br> という点を踏まえ、以下を解決したいと思いました。</p> <ul> <li><strong>資料作成を気付けるようにしてフロー情報をにアクセスしやくする</strong></li> <li><strong>フロー情報を書く場所に迷わないようにする</strong></li> </ul> <p>なお解決にあたりこちらの記事を参考にさせていただきました。ありがとうございます! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.layerx.co.jp%2Fentry%2F2023%2F11%2F14%2F191707" title="情報の流通性を上げコミュニケーションを活性化させるNotionデータベース #LayerXテックアドカレ - LayerX エンジニアブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.layerx.co.jp/entry/2023/11/14/191707">tech.layerx.co.jp</a></cite></p> <p><br></p> <h1 id="課題解決のためにやったこと">課題解決のためにやったこと</h1> <p>課題解決にあたり、まずは小さくやってみるべく、QA部内で検証してみました。</p> <p>QA部について簡単に説明しておきます。<br/> QA部は20人程度のメンバーが所属しており、プロダクトのチームにQAメンバーがそれぞれ配置されています。<br/> そのためQAメンバー全員が一緒の環境では仕事しておらず、プロダクトの詳しい箇所にはメンバー間で差異があります。</p> <p>以下の具体的なアクションはすべてQA部内で実施しており、まだ全社では実施していません。</p> <h4 id="統一化されたメモデータベース以下メモDBと呼ぶを作成する"><strong><u>①統一化されたメモデータベース(以下、メモDBと呼ぶ)を作成する</u></strong></h4> <p>QA内にも議事録データベース(以降DB)はありましたのでそれを流用しました。<br/> 議事録DBをメモDBとして刷新し、部内に展開しました。 <figure class="figure-image figure-image-fotolife" title="✨新メモDB✨"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240311/20240311120015.png" width="1200" height="1041" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>✨新メモDB✨</figcaption></figure></p> <h4 id="ボタンを設定してメモが作成しやすいようにする"><strong><u>②ボタンを設定してメモが作成しやすいようにする</u></strong></h4> <p>とにかくすべてのフロー情報をメモDBに集めてほしいので、メモを作りやすいようにボタンを設置しました。<br/> ボタンを押すとデータベースにメモが作成され、メモを書き始められます。 <figure class="figure-image figure-image-fotolife" title="ボタンを作成しました"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240311/20240311120105.png" width="1200" height="390" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ボタンを作成しました</figcaption></figure></p> <h4 id="メモが作成されたら通知する"><strong><u>③メモが作成されたら通知する</u></strong></h4> <p>Notionページ作成通知のためのSlackチャンネルを作り、メモDBに新規ページが作成されたら通知するようデータベースにオートメーションを設定しました。<br/> このSlackチャンネルにはQA部全員を招待して一旦強制的に参加してもらいました(ミュートなどは各自で)</p> <p>これでメモや議事録が作成されたことに気づけることができようになりました。 <figure class="figure-image figure-image-fotolife" title="Notion 通知チャンネル( #h_qa_notiton_notfiy )"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240311/20240311141025.png" width="1200" height="689" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Notion 通知チャンネル( #h_qa_notiton_notfiy )</figcaption></figure></p> <h4 id="メモDBから各用途チームに適したリンクドビューを作る"><strong><u>④メモDBから各用途・チームに適したリンクドビューを作る</u></strong></h4> <p>例えば◯◯チームの議事録をまとめて表示したい!となるときします。<br/> このときにいままでのように◯◯チームの階層に新たにDBを作成するのではなく、統一化されたメモDBのリンクドビューを作成し、タグでフィルターするようQA部には周知しました。 <figure class="figure-image figure-image-fotolife" title="SETタグがつくようにSET用の新しいボタンを設置"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240311/20240311141331.png" width="1200" height="753" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>SETタグがつくようにSET用の新しいボタンを設置</figcaption></figure></p> <p>具体的なアクションは以上です。<br/> アクションを始める前にもQAメンバーに内容をレビューしてもらいながら、実行しました。<br/> そのときにも他の課題や懸念も出てきましたが、一旦は運用してみて判断することとなりました。</p> <p><br></p> <h1 id="約1ヶ月運用してみて中間結果">約1ヶ月運用してみて中間結果</h1> <p>昨年12月にはじめて今年の2月頭に現在の状況を振り返ってみました。<br/> 振り返るにあたり、QA部内で使用具合や満足度を測るアンケートを取りました。</p> <h5 id="メモDBを使用した割合と満足度">メモDBを使用した割合と満足度</h5> <p>概ね悪い効果はでておらず、ポジティブな意見が多かった印象です。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240311/20240311141957.png" width="1200" height="825" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h5 id="フィードバックコメント抜粋">💬フィードバックコメント抜粋</h5> <ul> <li>今までは何が作られたか、見に行かないとわかりませんでしたが、 通知がくることによって、後で見たいもの、いま見るべきものが都度確認できるようになったので助かっています!</li> <li>作成時に通知が出るのは作成する側として安心感があります。自身でタスクを抱え込まない意味でも習慣づけたいと考えています</li> <li>個人からアナウンスされる前にnotionが作成されたことに気づけるので、便利と感じました。</li> <li>1つの場所に情報がまとまっているのは作る側も見る側もわかりやすくて良い</li> <li>通知を全て追えない</li> <li>個人メモ的なページには気軽にアクセスしないほうがいい?みたいな遠慮がある</li> <li>QAメンバーに共有できそうな情報がなかった</li> </ul> <h5 id="副次的な効果">副次的な効果</h5> <p>昨年12月から始めましたが、DBに作成されるページ数が圧倒的に増えました 🙌 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240311/20240311142504.png" width="600" height="371" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1 id="まとめ">まとめ</h1> <p>いかがでしたでしょうか。</p> <p>個人的には大きな反対もなく、QA部のメンバーに多く利用いただけてメリットを実感いただけてやってみてよかったと思い、継続していきたいと思います。</p> <p>というのもQA部の人数が私が参画したあとも増えていき、情報の共有やコミュニケーションが難しくなってきたと感じてた時期にこの課題に取り組みはじめたこともあり、メモや議事録を通して各メンバーが何をやっているかを知ることができています。</p> <p>今後はできればQA部からプロダクト開発全体、全社にも広げていき、hacomono全体に情報が行き渡るようになるといいなと思います。 <br> <br></p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています。 <br>採用情報や採用ウィッシュリストもぜひご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(プロダクトチーム向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech Raspberry Pi 5の基本セットアップとLLM動かしてみた hatenablog://entry/6801883189088111889 2024-03-05T11:00:00+09:00 2024-03-05T11:06:28+09:00 はじめに hacomonoのIoT部マネージャーの岩貞(さとちゃん)です。 最近hacomonoの影響もあってか、サウナにちょっとずつハマっています。 友人の結婚祝いで名古屋に行くことがあったので SENSE sauna 行ってきました。大衆浴場のサウナしか行ったことがなかったので、小スペースで友人だけと会話しながら入れるサウナはすごく良かったです。 良いサウナ場をこれからも探そうと思います笑 今回の話 技適がついに通ったラズパイ5。少し静観してたのですが、スイッチサイエンスに4GBモデルの在庫があるということでIoTのSW開発メンバーを中心に研究開発目的で購入してお配りしました。 あわせて自… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240304/20240304235928.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="はじめに">はじめに</h2> <p>hacomonoのIoT部マネージャーの<a href="https://twitter.com/iwasada_s">岩貞(さとちゃん)</a>です。<br/> 最近hacomonoの影響もあってか、サウナにちょっとずつハマっています。<br/> 友人の結婚祝いで名古屋に行くことがあったので <a href="https://sense-sauna.jp/">SENSE sauna</a> 行ってきました。大衆浴場のサウナしか行ったことがなかったので、小スペースで友人だけと会話しながら入れるサウナはすごく良かったです。<br/> 良いサウナ場をこれからも探そうと思います笑</p> <p><br></p> <h2 id="今回の話">今回の話</h2> <p>技適がついに通ったラズパイ5。少し静観してたのですが、スイッチサイエンスに4GBモデルの在庫があるということでIoTのSW開発メンバーを中心に研究開発目的で購入してお配りしました。</p> <p>あわせて自分も手元にGETしたので、眠らせるのはもったいないということで「火入れだけでも…」と思い動かしてみました。</p> <p>その時の簡単なセットアップ手順とついでに流行りっぽいもの動かしてみました。</p> <p><br></p> <h2 id="環境">環境</h2> <p>Raspberry Pi 5 4GBモデル<br/> 安定のスイッチサイエンスさんから買いました。<br/> <a href="https://www.switch-science.com/products/9249">Raspberry Pi 5 / 4GB</a><cite class="hatena-citation"><a href="https://www.switch-science.com/products/9249">www.switch-science.com</a></cite></p> <p>電源は5V3Aでも動作するということでラズパイ4のものを流用。<br/> ケースもなし、ファンやヒートシンクもなしです。</p> <p>電源はまだ専用のものが用意できていないというのと、ケースやファンも売り切れていたので致し方なし。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240304/20240304182804.jpg" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><br></p> <h2 id="イメージ書き込み">イメージ書き込み</h2> <ul> <li>適当なSD見繕う (過去使ってたものを見つけてきた)</li> <li>Raspberry Pi Imagerでラズパイ5向けのイメージを書き込み <ul> <li>昔はSDへのイメージ書き込みも面倒だったけど、今はImagerで一発。脳死で行ける(便利な世界)</li> <li>書き込み前に色々事前に設定出来るが、自宅のWiFi設定をしておくのとSSHの設定をしておくのをおすすめする。 <ul> <li>キーボードやら、マウスやら、ディスプレイやらを引っ張ってこなくてもターミナル入ってあれこれ出来る</li> <li>色々準備すると机の上がとっ散らかるのでとても大事。</li> </ul> </li> </ul> </li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240304/20240304182924.png" width="1200" height="876" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="ラズパイ5のセットアップ">ラズパイ5のセットアップ</h2> <ul> <li>SDに書き込めたらラズパイ5に挿して、電源ON</li> <li>起動したらSSHで接続</li> </ul> <pre class="code lang-sh" data-lang="sh" data-unlink>$ ssh pi@raspberrypi.<span class="synStatement">local</span> </pre> <ul> <li>ラズパイ5の証を確認しておく。</li> </ul> <pre class="code lang-sh" data-lang="sh" data-unlink>pi@raspberrypi:~ $ lscpu Architecture: aarch64 CPU op-mode<span class="synPreProc">(</span><span class="synSpecial">s</span><span class="synPreProc">)</span>: 32-bit, 64-bit Byte Order: Little Endian CPU<span class="synPreProc">(</span><span class="synSpecial">s</span><span class="synPreProc">)</span>: <span class="synConstant">4</span> On-line CPU<span class="synPreProc">(</span><span class="synSpecial">s</span><span class="synPreProc">)</span> list: 0-3 Vendor ID: ARM Model name: Cortex-A76 Model: <span class="synConstant">1</span> Thread<span class="synPreProc">(</span><span class="synSpecial">s</span><span class="synPreProc">)</span> per core: <span class="synConstant">1</span> Core<span class="synPreProc">(</span><span class="synSpecial">s</span><span class="synPreProc">)</span> per cluster: <span class="synConstant">4</span> Socket<span class="synPreProc">(</span><span class="synSpecial">s</span><span class="synPreProc">)</span>: - Cluster<span class="synPreProc">(</span><span class="synSpecial">s</span><span class="synPreProc">)</span>: <span class="synConstant">1</span> Stepping: r4p1 CPU<span class="synPreProc">(</span><span class="synSpecial">s</span><span class="synPreProc">)</span> scaling MHz: <span class="synConstant">62</span>% CPU max MHz: <span class="synConstant">2400</span>.<span class="synConstant">0000</span> CPU min MHz: <span class="synConstant">1500</span>.<span class="synConstant">0000</span> BogoMIPS: <span class="synConstant">108</span>.<span class="synConstant">00</span> Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp Vulnerabilities: Gather data sampling: Not affected Itlb multihit: Not affected L1tf: Not affected Mds: Not affected Meltdown: Not affected Mmio stale data: Not affected Retbleed: Not affected Spec rstack overflow: Not affected Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl Spectre v1: Mitigation; __user pointer sanitization Spectre v2: Mitigation; CSV2, BHB Srbds: Not affected Tsx async abort: Not affected </pre> <p>うん、Cortex-A76だ。間違いない。</p> <ul> <li>SWアップデートしておく</li> </ul> <pre class="code lang-sh" data-lang="sh" data-unlink>$ sudo apt update $ sudo apt upgrade $ reboot <span class="synComment">#念の為reboot</span> </pre> <p>以上、特にこだわらなければセットアップはこれで完了。</p> <p><br></p> <h2 id="生成AI動かしてみる">生成AI動かしてみる</h2> <p>ここまで来たら後は良しなに。</p> <p>何しようかなと思って他の方のラズパイ5の投稿見つつ最近さわれてなかったLLM動かすのが面白そうだったので本当に書いてあるとおりに動かす。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fkazuhitoyokoi%2Fitems%2F66e8c9e1b447a2850ba7" title="Raspberry Pi 5上で軽量LLM、TinyLlamaを動かしてみる - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://qiita.com/kazuhitoyokoi/items/66e8c9e1b447a2850ba7">qiita.com</a></cite></p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># Python仮想環境作る</span> $ python <span class="synSpecial">-m</span> venv llama2 $ <span class="synStatement">cd</span> llama2 $<span class="synStatement"> . </span>bin/activate <span class="synComment"># ついでに環境も切り替えておく</span> <span class="synComment"># モデルをDL</span> $ wget https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.<span class="synConstant">0</span>.Q4_K_M.gguf </pre> <p>※ここでvimもinstallしておく</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ sudo apt install neovim </pre> <p>記事記載の通りにvimとかでファイル作成する。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ vim llama2.py </pre> <p>↓中身</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> sys <span class="synPreProc">from</span> llama_cpp <span class="synPreProc">import</span> Llama llm = Llama(model_path=<span class="synConstant">&quot;tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf&quot;</span>) <span class="synIdentifier">print</span>(llm(<span class="synConstant">&quot;&lt;user&gt;</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span> + sys.argv[<span class="synConstant">1</span>] + <span class="synConstant">&quot;</span><span class="synSpecial">\n</span><span class="synConstant">&lt;assistant&gt;</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>, max_tokens=<span class="synConstant">40</span>)[<span class="synConstant">'choices'</span>][<span class="synConstant">0</span>][<span class="synConstant">&quot;text&quot;</span>] + <span class="synConstant">&quot;...&quot;</span>) </pre> <p>これだけで準備完了。<br/> 動かしてみる。</p> <pre class="code lang-sh" data-lang="sh" data-unlink> <span class="synPreProc">(</span><span class="synSpecial">llama2</span><span class="synPreProc">)</span> pi@raspberrypi:~/llama2 $ python llama2.py <span class="synStatement">&quot;</span><span class="synConstant">what is Empire State Building?</span><span class="synStatement">&quot;</span> llama_model_loader: loaded meta data with <span class="synConstant">23</span> key-value pairs and <span class="synConstant">201</span> tensors from tinyllama-1.1b-chat-v1.<span class="synConstant">0</span>.Q4_K_M.gguf <span class="synPreProc">(</span><span class="synSpecial">version GGUF V3 </span><span class="synPreProc">(</span><span class="synSpecial">latest</span><span class="synPreProc">))</span> llama_model_loader: Dumping metadata keys/values. Note: KV overrides <span class="synStatement">do</span> not apply <span class="synError">in</span> this output. llama_model_loader: - kv 0: general.architecture str <span class="synStatement">=</span> <span class="synConstant">llama</span> llama_model_loader: - kv 1: general.name str <span class="synStatement">=</span> <span class="synConstant">tinyllama_tinyllama</span><span class="synStatement">-1</span>.1b-chat-v1.<span class="synConstant">0</span> llama_model_loader: - kv 2: llama.context_length u32 <span class="synStatement">=</span> <span class="synConstant">2048</span> llama_model_loader: - kv 3: llama.embedding_length u32 <span class="synStatement">=</span> <span class="synConstant">2048</span> llama_model_loader: - kv 4: llama.block_count u32 <span class="synStatement">=</span> <span class="synConstant">22</span> llama_model_loader: - kv 5: llama.feed_forward_length u32 <span class="synStatement">=</span> <span class="synConstant">5632</span> llama_model_loader: - kv 6: llama.rope.dimension_count u32 <span class="synStatement">=</span> <span class="synConstant">64</span> llama_model_loader: - kv 7: llama.attention.head_count u32 <span class="synStatement">=</span> <span class="synConstant">32</span> llama_model_loader: - kv 8: llama.attention.head_count_kv u32 <span class="synStatement">=</span> <span class="synConstant">4</span> llama_model_loader: - kv 9: llama.attention.layer_norm_rms_epsilon f32 <span class="synStatement">=</span> <span class="synConstant">0</span>.<span class="synConstant">000010</span> llama_model_loader: - kv 10: llama.rope.freq_base f32 <span class="synStatement">=</span> <span class="synConstant">10000</span>.<span class="synConstant">000000</span> llama_model_loader: - kv 11: general.file_type u32 <span class="synStatement">=</span> <span class="synConstant">15</span> llama_model_loader: - kv 12: tokenizer.ggml.model str <span class="synStatement">=</span> <span class="synConstant">llama</span> llama_model_loader: - kv 13: tokenizer.ggml.tokens arr<span class="synStatement">[</span>str,<span class="synConstant">32000</span><span class="synStatement">]</span> <span class="synStatement">=</span> <span class="synStatement">[&quot;</span><span class="synConstant">&lt;unk&gt;</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">&lt;s&gt;</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">&lt;/s&gt;</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">&lt;0x00&gt;</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">&lt;...</span> <span class="synConstant">llama_model_loader: - kv 14: tokenizer.ggml.scores arr[f32,32000] = [0.000000, 0.000000, 0.000000, 0.0000...</span> <span class="synConstant">llama_model_loader: - kv 15: tokenizer.ggml.token_type arr[i32,32000] = [2, 3, 3, 6, 6, 6, 6, 6, 6, 6, 6, 6, ...</span> <span class="synConstant">llama_model_loader: - kv 16: tokenizer.ggml.merges arr[str,61249] = [</span><span class="synStatement">&quot;</span>▁ t<span class="synStatement">&quot;</span><span class="synConstant">, </span><span class="synStatement">&quot;</span>e r<span class="synStatement">&quot;</span><span class="synConstant">, </span><span class="synStatement">&quot;</span>i n<span class="synStatement">&quot;</span><span class="synConstant">, </span><span class="synStatement">&quot;</span>▁ a<span class="synStatement">&quot;</span><span class="synConstant">, </span><span class="synStatement">&quot;</span>e n... llama_model_loader: - kv 17: tokenizer.ggml.bos_token_id u32 <span class="synStatement">=</span> <span class="synConstant">1</span> llama_model_loader: - kv 18: tokenizer.ggml.eos_token_id u32 <span class="synStatement">=</span> <span class="synConstant">2</span> llama_model_loader: - kv 19: tokenizer.ggml.unknown_token_id u32 <span class="synStatement">=</span> <span class="synConstant">0</span> llama_model_loader: - kv 20: tokenizer.ggml.padding_token_id u32 <span class="synStatement">=</span> <span class="synConstant">2</span> llama_model_loader: - kv 21: tokenizer.chat_template str <span class="synStatement">=</span> <span class="synSpecial">{</span>% <span class="synStatement">for</span> message <span class="synStatement">in</span> messages %<span class="synSpecial">}\n{</span>% <span class="synStatement">if </span>m... llama_model_loader: - kv 22: general.quantization_version u32 <span class="synStatement">=</span> <span class="synConstant">2</span> llama_model_loader: - <span class="synStatement">type</span> f32: <span class="synConstant">45</span> tensors llama_model_loader: - <span class="synStatement">type</span> q4_K: <span class="synConstant">135</span> tensors llama_model_loader: - <span class="synStatement">type</span> q6_K: <span class="synConstant">21</span> tensors llm_load_vocab: special tokens definition check successful <span class="synPreProc">(</span> <span class="synConstant">259</span>/<span class="synConstant">32000</span> <span class="synPreProc">)</span>. llm_load_print_meta: format <span class="synStatement">=</span> <span class="synConstant">GGUF</span> V3 <span class="synPreProc">(</span>latest<span class="synPreProc">)</span> llm_load_print_meta: arch <span class="synStatement">=</span> <span class="synConstant">llama</span> llm_load_print_meta: vocab <span class="synStatement">type</span> <span class="synStatement">=</span> <span class="synConstant">SPM</span> llm_load_print_meta: n_vocab <span class="synStatement">=</span> <span class="synConstant">32000</span> llm_load_print_meta: n_merges <span class="synStatement">=</span> <span class="synConstant">0</span> llm_load_print_meta: n_ctx_train <span class="synStatement">=</span> <span class="synConstant">2048</span> llm_load_print_meta: n_embd <span class="synStatement">=</span> <span class="synConstant">2048</span> llm_load_print_meta: n_head <span class="synStatement">=</span> <span class="synConstant">32</span> llm_load_print_meta: n_head_kv <span class="synStatement">=</span> <span class="synConstant">4</span> llm_load_print_meta: n_layer <span class="synStatement">=</span> <span class="synConstant">22</span> llm_load_print_meta: n_rot <span class="synStatement">=</span> <span class="synConstant">64</span> llm_load_print_meta: n_embd_head_k <span class="synStatement">=</span> <span class="synConstant">64</span> llm_load_print_meta: n_embd_head_v <span class="synStatement">=</span> <span class="synConstant">64</span> llm_load_print_meta: n_gqa <span class="synStatement">=</span> <span class="synConstant">8</span> llm_load_print_meta: n_embd_k_gqa <span class="synStatement">=</span> <span class="synConstant">256</span> llm_load_print_meta: n_embd_v_gqa <span class="synStatement">=</span> <span class="synConstant">256</span> llm_load_print_meta: f_norm_eps <span class="synStatement">=</span> <span class="synConstant">0</span>.0e+<span class="synConstant">00</span> llm_load_print_meta: f_norm_rms_eps <span class="synStatement">=</span> <span class="synConstant">1</span>.0e-05 llm_load_print_meta: f_clamp_kqv <span class="synStatement">=</span> <span class="synConstant">0</span>.0e+<span class="synConstant">00</span> llm_load_print_meta: f_max_alibi_bias <span class="synStatement">=</span> <span class="synConstant">0</span>.0e+<span class="synConstant">00</span> llm_load_print_meta: n_ff <span class="synStatement">=</span> <span class="synConstant">5632</span> llm_load_print_meta: n_expert <span class="synStatement">=</span> <span class="synConstant">0</span> llm_load_print_meta: n_expert_used <span class="synStatement">=</span> <span class="synConstant">0</span> llm_load_print_meta: rope scaling <span class="synStatement">=</span> <span class="synConstant">linear</span> llm_load_print_meta: freq_base_train <span class="synStatement">=</span> <span class="synConstant">10000</span>.<span class="synConstant">0</span> llm_load_print_meta: freq_scale_train <span class="synStatement">=</span> <span class="synConstant">1</span> llm_load_print_meta: n_yarn_orig_ctx <span class="synStatement">=</span> <span class="synConstant">2048</span> llm_load_print_meta: rope_finetuned <span class="synStatement">=</span> <span class="synConstant">unknown</span> llm_load_print_meta: model <span class="synStatement">type</span> <span class="synStatement">=</span> <span class="synConstant">1B</span> llm_load_print_meta: model ftype <span class="synStatement">=</span> <span class="synConstant">Q4_K</span> - Medium llm_load_print_meta: model params <span class="synStatement">=</span> <span class="synConstant">1</span>.<span class="synConstant">10</span> B llm_load_print_meta: model size <span class="synStatement">=</span> <span class="synConstant">636</span>.<span class="synConstant">18</span> MiB <span class="synPreProc">(</span><span class="synConstant">4</span>.<span class="synConstant">85</span> BPW<span class="synPreProc">)</span> llm_load_print_meta: general.name <span class="synStatement">=</span> <span class="synConstant">tinyllama_tinyllama</span><span class="synStatement">-1</span>.1b-chat-v1.<span class="synConstant">0</span> llm_load_print_meta: BOS token <span class="synStatement">=</span> <span class="synConstant">1</span> <span class="synStatement">'</span><span class="synConstant">&lt;s&gt;</span><span class="synStatement">'</span> llm_load_print_meta: EOS token <span class="synStatement">=</span> <span class="synConstant">2</span> <span class="synStatement">'</span><span class="synConstant">&lt;/s&gt;</span><span class="synStatement">'</span> llm_load_print_meta: UNK token <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">'</span><span class="synConstant">&lt;unk&gt;</span><span class="synStatement">'</span> llm_load_print_meta: PAD token <span class="synStatement">=</span> <span class="synConstant">2</span> <span class="synStatement">'</span><span class="synConstant">&lt;/s&gt;</span><span class="synStatement">'</span> llm_load_print_meta: LF token <span class="synStatement">=</span> <span class="synConstant">13</span> <span class="synStatement">'</span><span class="synConstant">&lt;0x0A&gt;</span><span class="synStatement">'</span> llm_load_tensors: ggml ctx size <span class="synStatement">=</span> <span class="synConstant">0</span>.<span class="synConstant">08</span> MiB llm_load_tensors: CPU buffer size <span class="synStatement">=</span> <span class="synConstant">636</span>.<span class="synConstant">18</span> MiB .................................................................................. llama_new_context_with_model: n_ctx <span class="synStatement">=</span> <span class="synConstant">512</span> llama_new_context_with_model: freq_base <span class="synStatement">=</span> <span class="synConstant">10000</span>.<span class="synConstant">0</span> llama_new_context_with_model: freq_scale <span class="synStatement">=</span> <span class="synConstant">1</span> llama_kv_cache_init: CPU KV buffer size <span class="synStatement">=</span> <span class="synConstant">11</span>.<span class="synConstant">00</span> MiB llama_new_context_with_model: KV self size <span class="synStatement">=</span> <span class="synConstant">11</span>.<span class="synConstant">00</span> MiB, K <span class="synPreProc">(</span>f16<span class="synPreProc">)</span>: <span class="synConstant">5</span>.<span class="synConstant">50</span> MiB, V <span class="synPreProc">(</span>f16<span class="synPreProc">)</span>: <span class="synConstant">5</span>.<span class="synConstant">50</span> MiB llama_new_context_with_model: CPU input buffer size <span class="synStatement">=</span> <span class="synConstant">6</span>.<span class="synConstant">01</span> MiB llama_new_context_with_model: CPU compute buffer size <span class="synStatement">=</span> <span class="synConstant">66</span>.<span class="synConstant">50</span> MiB llama_new_context_with_model: graph splits <span class="synPreProc">(</span>measure<span class="synPreProc">)</span>: <span class="synConstant">1</span> AVX <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> AVX_VNNI <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> AVX2 <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> AVX512 <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> AVX512_VBMI <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> AVX512_VNNI <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> FMA <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> NEON <span class="synStatement">=</span> <span class="synConstant">1</span> <span class="synStatement">|</span> ARM_FMA <span class="synStatement">=</span> <span class="synConstant">1</span> <span class="synStatement">|</span> F16C <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> FP16_VA <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> WASM_SIMD <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> BLAS <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> SSE3 <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> SSSE3 <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> VSX <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> MATMUL_INT8 <span class="synStatement">=</span> <span class="synConstant">0</span> <span class="synStatement">|</span> Model <span class="synIdentifier">metadata: {</span><span class="synStatement">'</span><span class="synConstant">tokenizer.chat_template</span><span class="synStatement">'</span>: <span class="synStatement">&quot;</span><span class="synConstant">{% for message in messages %}</span><span class="synSpecial">\n</span><span class="synConstant">{% if message['role'] == 'user' %}</span><span class="synSpecial">\n</span><span class="synConstant">{{ '&lt;|user|&gt;</span><span class="synSpecial">\n</span><span class="synConstant">' + message['content'] + eos_token }}</span><span class="synSpecial">\n</span><span class="synConstant">{% elif message['role'] == 'system' %}</span><span class="synSpecial">\n</span><span class="synConstant">{{ '&lt;|system|&gt;</span><span class="synSpecial">\n</span><span class="synConstant">' + message['content'] + eos_token }}</span><span class="synSpecial">\n</span><span class="synConstant">{% elif message['role'] == 'assistant' %}</span><span class="synSpecial">\n</span><span class="synConstant">{{ '&lt;|assistant|&gt;</span><span class="synSpecial">\n</span><span class="synConstant">' + message['content'] + eos_token }}</span><span class="synSpecial">\n</span><span class="synConstant">{% endif %}</span><span class="synSpecial">\n</span><span class="synConstant">{% if loop.last and add_generation_prompt %}</span><span class="synSpecial">\n</span><span class="synConstant">{{ '&lt;|assistant|&gt;' }}</span><span class="synSpecial">\n</span><span class="synConstant">{% endif %}</span><span class="synSpecial">\n</span><span class="synConstant">{% endfor %}</span><span class="synStatement">&quot;</span>, <span class="synStatement">'</span><span class="synConstant">tokenizer.ggml.padding_token_id</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">2</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">tokenizer.ggml.unknown_token_id</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">0</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">tokenizer.ggml.eos_token_id</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">2</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">general.architecture</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">llama</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">llama.rope.freq_base</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">10000.000000</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">llama.context_length</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">2048</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">general.name</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">tinyllama_tinyllama-1.1b-chat-v1.0</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">llama.embedding_length</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">2048</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">llama.feed_forward_length</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">5632</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">llama.attention.layer_norm_rms_epsilon</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">0.000010</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">llama.rope.dimension_count</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">64</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">tokenizer.ggml.bos_token_id</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">1</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">llama.attention.head_count</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">32</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">llama.block_count</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">22</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">llama.attention.head_count_kv</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">4</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">general.quantization_version</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">2</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">tokenizer.ggml.model</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">llama</span><span class="synStatement">'</span>, <span class="synStatement">'</span><span class="synConstant">general.file_type</span><span class="synStatement">'</span>: <span class="synStatement">'</span><span class="synConstant">15</span><span class="synStatement">'</span><span class="synIdentifier">}</span> Using chat <span class="synIdentifier">template: {</span>% <span class="synStatement">for</span> message <span class="synStatement">in</span> messages %<span class="synIdentifier">}</span> <span class="synSpecial">{</span>% <span class="synStatement">if </span>message<span class="synStatement">['</span><span class="synConstant">role</span><span class="synStatement">']</span> <span class="synStatement">==</span> <span class="synStatement">'</span><span class="synConstant">user</span><span class="synStatement">'</span> %<span class="synError">}</span> <span class="synSpecial">{{</span> <span class="synStatement">'</span><span class="synConstant">&lt;|user|&gt;</span> <span class="synStatement">'</span> + message<span class="synStatement">['</span><span class="synConstant">content</span><span class="synStatement">']</span> + eos_token <span class="synSpecial">}}</span> <span class="synSpecial">{</span>% elif message<span class="synStatement">['</span><span class="synConstant">role</span><span class="synStatement">']</span> <span class="synStatement">==</span> <span class="synStatement">'</span><span class="synConstant">system</span><span class="synStatement">'</span> %<span class="synSpecial">}</span> <span class="synSpecial">{{</span> <span class="synStatement">'</span><span class="synConstant">&lt;|system|&gt;</span> <span class="synStatement">'</span> + message<span class="synStatement">['</span><span class="synConstant">content</span><span class="synStatement">']</span> + eos_token <span class="synSpecial">}}</span> <span class="synSpecial">{</span>% elif message<span class="synStatement">['</span><span class="synConstant">role</span><span class="synStatement">']</span> <span class="synStatement">==</span> <span class="synStatement">'</span><span class="synConstant">assistant</span><span class="synStatement">'</span> %<span class="synSpecial">}</span> <span class="synSpecial">{{</span> <span class="synStatement">'</span><span class="synConstant">&lt;|assistant|&gt;</span> <span class="synStatement">'</span> + message<span class="synStatement">['</span><span class="synConstant">content</span><span class="synStatement">']</span> + eos_token <span class="synSpecial">}}</span> <span class="synSpecial">{</span>% endif %<span class="synSpecial">}</span> <span class="synSpecial">{</span>% <span class="synStatement">if </span>loop.last and add_generation_prompt %<span class="synError">}</span> <span class="synSpecial">{{</span> <span class="synStatement">'</span><span class="synConstant">&lt;|assistant|&gt;</span><span class="synStatement">'</span> <span class="synSpecial">}}</span> <span class="synSpecial">{</span>% endif %<span class="synSpecial">}</span> <span class="synSpecial">{</span>% endfor %<span class="synSpecial">}</span> Using chat eos_token: Using chat bos_token: llama_print_timings: load time <span class="synStatement">=</span> <span class="synConstant">1366</span>.<span class="synConstant">39</span> ms llama_print_timings: sample time <span class="synStatement">=</span> <span class="synConstant">10</span>.<span class="synConstant">24</span> ms / <span class="synConstant">40</span> runs <span class="synPreProc">(</span> <span class="synConstant">0</span>.<span class="synConstant">26</span> ms per token, <span class="synConstant">3907</span>.<span class="synConstant">78</span> tokens per second<span class="synPreProc">)</span> llama_print_timings: prompt <span class="synStatement">eval</span> time <span class="synStatement">=</span> <span class="synConstant">1366</span>.<span class="synConstant">32</span> ms / <span class="synConstant">17</span> tokens <span class="synPreProc">(</span> <span class="synConstant">80</span>.<span class="synConstant">37</span> ms per token, <span class="synConstant">12</span>.<span class="synConstant">44</span> tokens per second<span class="synPreProc">)</span> llama_print_timings: <span class="synStatement">eval</span> time <span class="synStatement">=</span> <span class="synConstant">3326</span>.<span class="synConstant">60</span> ms / <span class="synConstant">39</span> runs <span class="synPreProc">(</span> <span class="synConstant">85</span>.<span class="synConstant">30</span> ms per token, <span class="synConstant">11</span>.<span class="synConstant">72</span> tokens per second<span class="synPreProc">)</span> llama_print_timings: total time <span class="synStatement">=</span> <span class="synConstant">4850</span>.<span class="synConstant">41</span> ms / <span class="synConstant">56</span> tokens The Empire State Building, also known as the Freedom Tower or simply <span class="synStatement">&quot;</span><span class="synConstant">the Blob,</span><span class="synStatement">&quot;</span> is a skyscraper located <span class="synError">in</span> New York City, USA. It was completed <span class="synError">in</span> <span class="synConstant">19</span>... </pre> <p>色々でますが、一番最後のところの</p> <pre class="code lang-sh" data-lang="sh" data-unlink>The Empire State Building, also known as the Freedom Tower or simply <span class="synStatement">&quot;</span><span class="synConstant">the Blob,</span><span class="synStatement">&quot;</span> is a skyscraper located <span class="synError">in</span> New York City, USA. It was completed <span class="synError">in</span> <span class="synConstant">19</span>... </pre> <p>が実行結果。</p> <p>tokenサイズを40としているので途中で切れている。<br/> 40くらいだと数秒で実行結果が出力されましたが、これを400とか10倍にすると数十秒待つ必要がありました。</p> <p>max_token増やすとすごい時間かかる。あと、ChatGPTと違って最後にバッと出力されるので、体感結構かかる気がした。途中経過を生成している様を見るのは結構体験的にも大事なのかも知れない。そこはllmの出力optionでなんとかできそうな気もしたけど一旦おいとく。</p> <p>今使ってるモデルは英語オンリーなので、日本語使えるモデルで置き換えてみた。<br/> 以下から適当なものを取得。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhuggingface.co%2Fmmnga%2FELYZA-japanese-Llama-2-7b-fast-instruct-gguf" title="mmnga/ELYZA-japanese-Llama-2-7b-fast-instruct-gguf · Hugging Face" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://huggingface.co/mmnga/ELYZA-japanese-Llama-2-7b-fast-instruct-gguf">huggingface.co</a></cite></p> <p>サンプルのggufのモデル指定部分を置き換えてみる。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ vim llama2_jp.py </pre> <p>↓中身はコピペしてモデル部分だけ置き換えた</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> sys <span class="synPreProc">from</span> llama_cpp <span class="synPreProc">import</span> Llama llm = Llama(model_path=<span class="synConstant">&quot;/home/pi/llama2/ELYZA-japanese-Llama-2-7b-fast-instruct-q2_K.gguf&quot;</span>) <span class="synIdentifier">print</span>(llm(<span class="synConstant">&quot;&lt;user&gt;</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span> + sys.argv[<span class="synConstant">1</span>] + <span class="synConstant">&quot;</span><span class="synSpecial">\n</span><span class="synConstant">&lt;assistant&gt;</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>, max_tokens=<span class="synConstant">40</span>)[<span class="synConstant">'choices'</span>][<span class="synConstant">0</span>][<span class="synConstant">&quot;text&quot;</span>] + <span class="synConstant">&quot;...&quot;</span>) </pre> <p>実行してみる。その時、親子丼食べたかったんで親子丼について聞いてみた。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synPreProc">(</span><span class="synSpecial">llama2</span><span class="synPreProc">)</span> pi@raspberrypi:~/llama2 $ python llama2_jp.py <span class="synStatement">&quot;</span><span class="synConstant">親子丼のこと教えて</span><span class="synStatement">&quot;</span> llama_model_loader: loaded meta data with <span class="synConstant">21</span> key-value pairs and <span class="synConstant">291</span> tensors from /home/pi/llama2/ELYZA-japanese-Llama-2-7b-fast-instruct-q2_K.gguf <span class="synPreProc">(</span><span class="synSpecial">version GGUF V3 </span><span class="synPreProc">(</span><span class="synSpecial">latest</span><span class="synPreProc">))</span> llama_model_loader: Dumping metadata keys/values. Note: KV overrides <span class="synStatement">do</span> not apply <span class="synError">in</span> this output. llama_model_loader: - kv 0: general.architecture str <span class="synStatement">=</span> <span class="synConstant">llama</span> llama_model_loader: - kv 1: general.name str <span class="synStatement">=</span> <span class="synConstant">ELYZA</span>-japanese-Llama-2-7b-fast-instruct llama_model_loader: - kv 2: general.<span class="synStatement">source</span>.huggingface.repository str <span class="synStatement">=</span> <span class="synConstant">elyza</span>/ELYZA-japanese-Llama-2-7b-fast-... llama_model_loader: - kv 3: llama.tensor_data_layout str <span class="synStatement">=</span> <span class="synConstant">Meta</span> AI original pth llama_model_loader: - kv 4: llama.context_length u32 <span class="synStatement">=</span> <span class="synConstant">4096</span> llama_model_loader: - kv 5: llama.embedding_length u32 <span class="synStatement">=</span> <span class="synConstant">4096</span> llama_model_loader: - kv 6: llama.block_count u32 <span class="synStatement">=</span> <span class="synConstant">32</span> llama_model_loader: - kv 7: llama.feed_forward_length u32 <span class="synStatement">=</span> <span class="synConstant">11008</span> llama_model_loader: - kv 8: llama.rope.dimension_count u32 <span class="synStatement">=</span> <span class="synConstant">128</span> llama_model_loader: - kv 9: llama.attention.head_count u32 <span class="synStatement">=</span> <span class="synConstant">32</span> llama_model_loader: - kv 10: llama.attention.head_count_kv u32 <span class="synStatement">=</span> <span class="synConstant">32</span> llama_model_loader: - kv 11: llama.attention.layer_norm_rms_epsilon f32 <span class="synStatement">=</span> <span class="synConstant">0</span>.<span class="synConstant">000001</span> llama_model_loader: - kv 12: tokenizer.ggml.model str <span class="synStatement">=</span> <span class="synConstant">llama</span> llama_model_loader: - kv 13: tokenizer.ggml.tokens arr<span class="synStatement">[</span>str,<span class="synConstant">45043</span><span class="synStatement">]</span> <span class="synStatement">=</span> <span class="synStatement">[&quot;</span><span class="synConstant">&lt;unk&gt;</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">&lt;s&gt;</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">&lt;/s&gt;</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">&lt;0x00&gt;</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">&lt;...</span> <span class="synConstant">llama_model_loader: - kv 14: tokenizer.ggml.scores arr[f32,45043] = [0.000000, 0.000000, 0.000000, 0.0000...</span> <span class="synConstant">llama_model_loader: - kv 15: tokenizer.ggml.token_type arr[i32,45043] = [2, 3, 3, 6, 6, 6, 6, 6, 6, 6, 6, 6, ...</span> <span class="synConstant">llama_model_loader: - kv 16: tokenizer.ggml.bos_token_id u32 = 1</span> <span class="synConstant">llama_model_loader: - kv 17: tokenizer.ggml.eos_token_id u32 = 2</span> <span class="synConstant">llama_model_loader: - kv 18: tokenizer.ggml.unknown_token_id u32 = 0</span> <span class="synConstant">llama_model_loader: - kv 19: general.quantization_version u32 = 2</span> <span class="synConstant">llama_model_loader: - kv 20: general.file_type u32 = 10</span> <span class="synConstant">llama_model_loader: - type f32: 65 tensors</span> <span class="synConstant">llama_model_loader: - type q2_K: 65 tensors</span> <span class="synConstant">llama_model_loader: - type q3_K: 160 tensors</span> <span class="synConstant">llama_model_loader: - type q6_K: 1 tensors</span> <span class="synConstant">llm_load_vocab: mismatch in special tokens definition ( 304/45043 vs 264/45043 ).</span> <span class="synConstant">llm_load_print_meta: format = GGUF V3 (latest)</span> <span class="synConstant">llm_load_print_meta: arch = llama</span> <span class="synConstant">llm_load_print_meta: vocab type = SPM</span> <span class="synConstant">llm_load_print_meta: n_vocab = 45043</span> <span class="synConstant">llm_load_print_meta: n_merges = 0</span> <span class="synConstant">llm_load_print_meta: n_ctx_train = 4096</span> <span class="synConstant">llm_load_print_meta: n_embd = 4096</span> <span class="synConstant">llm_load_print_meta: n_head = 32</span> <span class="synConstant">llm_load_print_meta: n_head_kv = 32</span> <span class="synConstant">llm_load_print_meta: n_layer = 32</span> <span class="synConstant">llm_load_print_meta: n_rot = 128</span> <span class="synConstant">llm_load_print_meta: n_embd_head_k = 128</span> <span class="synConstant">llm_load_print_meta: n_embd_head_v = 128</span> <span class="synConstant">llm_load_print_meta: n_gqa = 1</span> <span class="synConstant">llm_load_print_meta: n_embd_k_gqa = 4096</span> <span class="synConstant">llm_load_print_meta: n_embd_v_gqa = 4096</span> <span class="synConstant">llm_load_print_meta: f_norm_eps = 0.0e+00</span> <span class="synConstant">llm_load_print_meta: f_norm_rms_eps = 1.0e-06</span> <span class="synConstant">llm_load_print_meta: f_clamp_kqv = 0.0e+00</span> <span class="synConstant">llm_load_print_meta: f_max_alibi_bias = 0.0e+00</span> <span class="synConstant">llm_load_print_meta: n_ff = 11008</span> <span class="synConstant">llm_load_print_meta: n_expert = 0</span> <span class="synConstant">llm_load_print_meta: n_expert_used = 0</span> <span class="synConstant">llm_load_print_meta: rope scaling = linear</span> <span class="synConstant">llm_load_print_meta: freq_base_train = 10000.0</span> <span class="synConstant">llm_load_print_meta: freq_scale_train = 1</span> <span class="synConstant">llm_load_print_meta: n_yarn_orig_ctx = 4096</span> <span class="synConstant">llm_load_print_meta: rope_finetuned = unknown</span> <span class="synConstant">llm_load_print_meta: model type = 7B</span> <span class="synConstant">llm_load_print_meta: model ftype = Q2_K - Medium</span> <span class="synConstant">llm_load_print_meta: model params = 6.85 B</span> <span class="synConstant">llm_load_print_meta: model size = 2.69 GiB (3.37 BPW)</span> <span class="synConstant">llm_load_print_meta: general.name = ELYZA-japanese-Llama-2-7b-fast-instruct</span> <span class="synConstant">llm_load_print_meta: BOS token = 1 '&lt;s&gt;'</span> <span class="synConstant">llm_load_print_meta: EOS token = 2 '&lt;/s&gt;'</span> <span class="synConstant">llm_load_print_meta: UNK token = 0 '&lt;unk&gt;'</span> <span class="synConstant">llm_load_print_meta: LF token = 13 '&lt;0x0A&gt;'</span> <span class="synConstant">llm_load_tensors: ggml ctx size = 0.11 MiB</span> <span class="synConstant">llm_load_tensors: CPU buffer size = 2752.83 MiB</span> <span class="synConstant">..............................................................................................</span> <span class="synConstant">llama_new_context_with_model: n_ctx = 512</span> <span class="synConstant">llama_new_context_with_model: freq_base = 10000.0</span> <span class="synConstant">llama_new_context_with_model: freq_scale = 1</span> <span class="synConstant">llama_kv_cache_init: CPU KV buffer size = 256.00 MiB</span> <span class="synConstant">llama_new_context_with_model: KV self size = 256.00 MiB, K (f16): 128.00 MiB, V (f16): 128.00 MiB</span> <span class="synConstant">llama_new_context_with_model: CPU input buffer size = 10.01 MiB</span> <span class="synConstant">llama_new_context_with_model: CPU compute buffer size = 95.97 MiB</span> <span class="synConstant">llama_new_context_with_model: graph splits (measure): 1</span> <span class="synConstant">AVX = 0 | AVX_VNNI = 0 | AVX2 = 0 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 0 | NEON = 1 | ARM_FMA = 1 | F16C = 0 | FP16_VA = 0 | WASM_SIMD = 0 | BLAS = 0 | SSE3 = 0 | SSSE3 = 0 | VSX = 0 | MATMUL_INT8 = 0 |</span> <span class="synConstant">Model metadata: {'general.file_type': '10', 'tokenizer.ggml.unknown_token_id': '0', 'tokenizer.ggml.eos_token_id': '2', 'general.architecture': 'llama', 'llama.context_length': '4096', 'general.name': 'ELYZA-japanese-Llama-2-7b-fast-instruct', 'general.source.huggingface.repository': 'elyza/ELYZA-japanese-Llama-2-7b-fast-instruct', 'llama.embedding_length': '4096', 'llama.tensor_data_layout': 'Meta AI original pth', 'llama.feed_forward_length': '11008', 'llama.attention.layer_norm_rms_epsilon': '0.000001', 'llama.rope.dimension_count': '128', 'tokenizer.ggml.bos_token_id': '1', 'llama.attention.head_count': '32', 'llama.block_count': '32', 'llama.attention.head_count_kv': '32', 'general.quantization_version': '2', 'tokenizer.ggml.model': 'llama'}</span> <span class="synConstant">llama_print_timings: load time = 12159.14 ms</span> <span class="synConstant">llama_print_timings: sample time = 13.89 ms / 40 runs ( 0.35 ms per token, 2880.18 tokens per second)</span> <span class="synConstant">llama_print_timings: prompt eval time = 12159.06 ms / 16 tokens ( 759.94 ms per token, 1.32 tokens per second)</span> <span class="synConstant">llama_print_timings: eval time = 30298.97 ms / 39 runs ( 776.90 ms per token, 1.29 tokens per second)</span> <span class="synConstant">llama_print_timings: total time = 42718.35 ms / 55 tokens</span> <span class="synConstant">ごちそうが好きなので親子丼を食べたことがあります。</span> <span class="synConstant">赤ちゃんがまだ小学生のときに食べて、とても美味しかったですね。</span> <span class="synConstant">子供の頃は今ほど親に...</span> </pre> <p>一番下部に回答がありますね。</p> <p>ちなみにはじめ動かしていたモデルの tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf は638MBだったのですが、持ってきた日本語モデル ELYZA-japanese-Llama-2-7b-fast-instruct-q2_K.gguf は2.7GBもあるせいか、読み込みがめっちゃ遅い。SDのI/O遅いというのもあるかも。。<br/> その分実行結果もめちゃくちゃ遅いです。メモリ8GBだともう少し頑張れたのかも。</p> <p>ちなみに参考にした記事をベースにNode-RED使ったchatサーバーまでやりました。<br/> 同じようにやってみたい方はパス設定で動かなかったりしたのでそこだけ注意してください。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240304/20240304183035.png" width="955" height="575" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> 結果はまぁ… 笑</p> <p><br></p> <h2 id="さいごに">さいごに</h2> <p>ラズパイ5セットアップついでにLLM動かせたのはなんか良かった。でも4からの比較にはならなかったです。そこはやってる人多いからもういいよね…?</p> <p>ラズパイ5が技適が通ってちょうど各方面、手に入れられているようで色々と記事が増えてきて面白いです。<br/> 個人的にはSSD周りも簡単にできるようになっているので高速化のためにやってみたいですね。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Fcoderdojoseishin%2Fn%2Fn07f11bc868f9" title="Raspberry Pi 5をSSDで高速化する|CoderDojo西神" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://note.com/coderdojoseishin/n/n07f11bc868f9">note.com</a></cite></p> <p>Interfaceも特集記事上げるみたいなのでちょっと祭り的に盛り上がるの良いですね。業界盛り上げるようなデバイスが最近なかった気がして嬉しい。 <blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ラズベリーパイ5の技適申請が通ったようです.<br>Interface誌では3/25発売の5月号にてラズベリーパイ5を特集します.<br>詳細は近日中に公開します.ご期待ください. <a href="https://t.co/x91xLpT5td">pic.twitter.com/x91xLpT5td</a></p>&mdash; コンピュータ技術実験雑誌「Interface」(毎月25日発売,CQ出版社) (@If_CQ) <a href="https://twitter.com/If_CQ/status/1754276244257145212?ref_src=twsrc%5Etfw">2024年2月4日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <h2 id="宣伝">宣伝</h2> <p>最近、当社で顧問としても入っていただいているくずさんとの対談記事が公開されたので、あわせて見ていただけるとhacomonoのIoTが結構頑張ってるところがわかると思います笑 <blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">スイッチサイエンス企画の対談記事に掲載して頂きました!hacomonoのIoT事業の立ち上げでPoCは出来たものの、量産ステップで悩んでいた時に全方位助けて頂きました。<br>同じように悩んでいる方は是非くずさん <a href="https://twitter.com/qzuryu?ref_src=twsrc%5Etfw">@qzuryu</a> に相談してみてください〜!🙆‍♂️<a href="https://twitter.com/hashtag/hacomono?src=hash&amp;ref_src=twsrc%5Etfw">#hacomono</a> <a href="https://twitter.com/hashtag/%E3%82%B9%E3%82%A4%E3%83%83%E3%83%81%E3%82%B5%E3%82%A4%E3%82%A8%E3%83%B3%E3%82%B9?src=hash&amp;ref_src=twsrc%5Etfw">#スイッチサイエンス</a> <a href="https://twitter.com/hashtag/%E9%87%8F%E7%94%A3?src=hash&amp;ref_src=twsrc%5Etfw">#量産</a> <a href="https://t.co/GsloxU76Kg">https://t.co/GsloxU76Kg</a></p>&mdash; 岩貞 智 / hacomono (@iwasada_s) <a href="https://twitter.com/iwasada_s/status/1758425077564273106?ref_src=twsrc%5Etfw">2024年2月16日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p><br> <br></p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています。 <br>採用情報や採用ウィッシュリストもぜひご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(プロダクトチーム向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech 計算複雑性理論をなんとなく理解してパフォーマンスを改善する hatenablog://entry/6801883189086311413 2024-02-27T11:00:00+09:00 2024-02-28T15:55:31+09:00 どうも, みゅーとんです. 今回はパフォーマンスチューニングの総集編に当たる記事になります. 以前 OSS として公開しているライブラリ json-origami のバージョンアップで大幅にパフォーマンスを悪くしてしまい, 製品に影響が出てしまったので, その改善のための観点として, 計算量をちゃんと見積もってみようと思いました. 関連記事 以下の記事は, この本記事で取り上げる json-origami の改善まわりで実施した, 品質担保まわりの取り組みです. こちらも合わせて, 軽く読んでおくことをおすすめします. techblog.hacomono.jp techblog.hacomon… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240227/20240227040427.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>どうも, <a href="https://twitter.com/_mew_ton">みゅーとん</a>です.</p> <p>今回はパフォーマンスチューニングの総集編に当たる記事になります.</p> <p>以前 OSS として公開しているライブラリ <code>json-origami</code> のバージョンアップで大幅にパフォーマンスを悪くしてしまい, 製品に影響が出てしまったので, その改善のための観点として, 計算量をちゃんと見積もってみようと思いました.</p> <h1 id="関連記事">関連記事</h1> <p>以下の記事は, この本記事で取り上げる <code>json-origami</code> の改善まわりで実施した, 品質担保まわりの取り組みです. こちらも合わせて, 軽く読んでおくことをおすすめします. <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftechblog.hacomono.jp%2Fentry%2F2024%2F01%2F30%2F1100" title="(小ネタ) Vitest でベンチマークテストする - hacomono TECH BLOG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://techblog.hacomono.jp/entry/2024/01/30/1100">techblog.hacomono.jp</a></cite> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftechblog.hacomono.jp%2Fentry%2F2024%2F02%2F08%2F1100" title="(小ネタ) Vitest でパフォーマンス劣化を検知する - hacomono TECH BLOG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://techblog.hacomono.jp/entry/2024/02/08/1100">techblog.hacomono.jp</a></cite> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftechblog.hacomono.jp%2Fentry%2F2024%2F02%2F19%2F1100" title="(小ネタ) Vitest で Node.js の GC をテストする - hacomono TECH BLOG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://techblog.hacomono.jp/entry/2024/02/19/1100">techblog.hacomono.jp</a></cite></p> <p><br></p> <h1 id="概要">概要</h1> <h4 id="3-行でまとめ">3 行でまとめ</h4> <ul> <li>計算複雑性理論 (計算量のオーダーなどと称される) の概念は, 実際パフォーマンス改善のためにコードを良くする際に参考にはなる.</li> <li>理論をある程度理解していても, 実際のコードから厳密に計算は困難.</li> <li>計算量を上げる要因となる変数が何であるかを見立てて, 最悪のケースを算出する.</li> </ul> <h4 id="取り上げないこと">取り上げないこと</h4> <p>計算複雑性理論については軽く触れますが, 深く掘り下げると収集がつかないので, 概要にとどめます.</p> <p>まだ計算量概念については学習中であるため, 間違いは多く有りえます.</p> <p>逃げの一言になってしまいますが, 本記事で紹介するのは, あくまで “改善のために着目した観点” です. また, まさかりを受け付けてます. ぜひバッサリと切り落としてほしいです.</p> <p><br></p> <h1 id="背景">背景</h1> <p>インシデント未遂を引き起こしてしまいました.</p> <p>hacomono-lib で公開している OSS の <code>json-origami</code> にて, パッチバージョンアップとして, たった1行変更するだけのバグ修正を行いました.</p> <p>この修正が計算量を膨大に引き上げてしまい, プロダクトのコードでパフォーマンスが大幅劣化してしまいました.</p> <p>暫定対処として, 利用する <code>json-origami</code> のバージョンを下げるなどの対応はしましたが, 恒久対処として, <code>json-origami</code> のロジックを見直してみると, どうやら書き直しが必要な規模感を感じたため, 思い切ってしっかり対応してみることにしました.</p> <h4 id="json-origami-とは-">json-origami とは ?</h4> <p>今回は具体的なロジックの話になるため, 修正対象の <code>json-origami</code> ライブラリについて説明します. リポジトリはこちら. <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhacomono-lib%2Fjson-origami" title="GitHub - hacomono-lib/json-origami: Transform nested objects into a flattened key-value structure and vice-versa." class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hacomono-lib/json-origami">github.com</a></cite></p> <p>npm 向けのパッケージとして公開しているライブラリで, 深い階層のオブジェクトを, 割りと簡単に構造変更する汎用ロジックを提供しています.</p> <p>例えば, <code>{ 'a.b.c': 'x.y.z' }</code> のようなキーを元のオブジェクトのキー文字列, 値を変換後のオブジェクトのキーの文字列としたマップオブジェクトと, 変換対象のオブジェクトを渡すと写像変換をする <code>twist</code> 関数なんかがあったりします. 背景説明にて, パフォーマンスが劣化したのもこれです.</p> <p>なんとなく, おっきい形の変更をする構図が折り紙みたいだなーと思い, <code>json-origami</code> と命名しました. API 詳細は README.md を確認してください.</p> <h4 id="計算複雑性理論-とは-">計算複雑性理論 とは ?</h4> <p>wikipedia の引用になってしまいますが, 計算複雑性理論とは, 概要として</p> <blockquote><p>「あるアルゴリズムへの入力データの長さを増やしたとき、実行時間や必要な記憶量はどのように増えるか?」という問に答える理論</p></blockquote> <p>と説明されます.</p> <p>この理論では, チューリング機会やら P, NP などの概念がありますが, 我々実際にプログラムを書く者からすると, この理論の中で特に “<strong>計算量のオーダー</strong>” が最もよく聞くと思います.<br/> 今回はその計算量のオーダーについて触れていきます.</p> <h4 id="計算量-とは-">計算量 とは ?</h4> <p>入力データ長を $N$ とするとき, その結果発生する処理時間や必要な記憶量については , 一般的に $O(N)$ のように表されます.</p> <p>例えば,</p> <ul> <li>$O(1)$ .. 入力のサイズに応じず, 固定である</li> <li>$O(N)$ .. 入力サイズの大きさに比例して, 時間もしくは記憶量が増加する</li> <li>$O(N^{2})$ .. 入力サイズの大きさについて 2 乗する形で, 時間もしくは記憶量が増加する</li> </ul> <p>というような意味合いです. <br/> 入力に応じて増減する時間を <strong>時間計算量</strong>, 増減する記憶量を <strong>空間計算量</strong> と称されます.</p> <p>ロジックに書くとわかりやすいかもしれないですね. 時間計算量の目安として, 以下は $O(N)$</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> i <span class="synStatement">&lt;</span> input.length<span class="synStatement">;</span> i<span class="synStatement">++)</span> <span class="synIdentifier">{</span> <span class="synComment">// do something</span> <span class="synIdentifier">}</span> </pre> <p>以下は $O(N^{2})$ です.</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> i <span class="synStatement">&lt;</span> input.length<span class="synStatement">;</span> i<span class="synStatement">++)</span> <span class="synIdentifier">{</span> <span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> j <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> j <span class="synStatement">&lt;</span> input.length<span class="synStatement">;</span> j<span class="synStatement">++)</span> <span class="synIdentifier">{</span> <span class="synComment">// do something</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <p>また, 計算量のオーダーでは, N に対する定数倍や指数の少ない項は省略されます.<br/> 例えば, 以下は $O(2N^{2} + N)$ っぽいですが, 省略して $O(N^{2})$ が正になります.</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> i <span class="synStatement">&lt;</span> input.length<span class="synStatement">;</span> i<span class="synStatement">++)</span> <span class="synIdentifier">{</span> <span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> j <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> j <span class="synStatement">&lt;</span> input.length<span class="synStatement">;</span> j<span class="synStatement">++)</span> <span class="synIdentifier">{</span> <span class="synComment">// do something</span> <span class="synIdentifier">}</span> <span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> j <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> j <span class="synStatement">&lt;</span> input.length<span class="synStatement">;</span> j<span class="synStatement">++)</span> <span class="synIdentifier">{</span> <span class="synComment">// do something</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> j <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> j <span class="synStatement">&lt;</span> input.length<span class="synStatement">;</span> j<span class="synStatement">++)</span> <span class="synIdentifier">{</span> <span class="synComment">// do something</span> <span class="synIdentifier">}</span> </pre> <p>空間計算量のサンプルを示すと, 以下は $O(N)$</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> arr <span class="synStatement">=</span> <span class="synSpecial">Array</span><span class="synStatement">(</span>input.length<span class="synStatement">)</span> </pre> <p>以下は $O(N^{2})$ です.</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> arr <span class="synStatement">=</span> <span class="synSpecial">Array</span><span class="synStatement">(</span>input1.length<span class="synStatement">)</span> <span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> i <span class="synStatement">&lt;</span> input1.length<span class="synStatement">;</span> i<span class="synStatement">++)</span> <span class="synIdentifier">{</span> arr<span class="synIdentifier">[</span>i<span class="synIdentifier">]</span> <span class="synStatement">=</span> <span class="synSpecial">Array</span><span class="synStatement">(</span>input2.length<span class="synStatement">);</span> <span class="synIdentifier">}</span> </pre> <p>空間計算量でも同様に, 定数倍や指数の少ない項目を省略します. 以下は $O(N^{2})$ です.</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> arr1 <span class="synStatement">=</span> <span class="synIdentifier">[]</span> <span class="synType">const</span> arr2 <span class="synStatement">=</span> <span class="synIdentifier">[]</span> <span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> i <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> i <span class="synStatement">&lt;</span> input.length<span class="synStatement">;</span> i<span class="synStatement">++)</span> <span class="synIdentifier">{</span> <span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">let</span> j <span class="synStatement">=</span> <span class="synConstant">0</span><span class="synStatement">;</span> j <span class="synStatement">&lt;</span> input.length<span class="synStatement">;</span> j<span class="synStatement">++)</span> <span class="synIdentifier">{</span> arr1.push<span class="synStatement">(</span>getData1<span class="synStatement">(</span>i<span class="synStatement">,</span> j<span class="synStatement">))</span> <span class="synIdentifier">}</span> arr2.push<span class="synStatement">(</span>getData2<span class="synStatement">(</span>i<span class="synStatement">))</span> <span class="synIdentifier">}</span> </pre> <p>少ない項や定数倍を省略する理由は単純で, $N$が無限の極限をとっていく程度に極端に大きい数にしたとき, 定数倍や小さい項は計算量に大きな変化を与えなくなるためです.</p> <p>概要説明はこれくらいにしておきます. 理論の理解にあたって, 以下はだいぶ参考になった記事でした. (ありがとうございました)</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fasksaito%2Fitems%2F59e0d48408f1eab081b5" title="計算量オーダーについて - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://qiita.com/asksaito/items/59e0d48408f1eab081b5">qiita.com</a></cite> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fdrken%2Fitems%2F872ebc3a2b5caaa4a0d0%23%E4%BE%8B-7-%E3%83%8A%E3%83%99%E3%82%A2%E3%83%84-ologn" title="計算量オーダーの求め方を総整理! 〜 どこから log が出て来るか 〜 - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://qiita.com/drken/items/872ebc3a2b5caaa4a0d0#例-7-ナベアツ-ologn">qiita.com</a></cite> <iframe src="https://www.slideshare.net/slideshow/embed_code/key/bemfeEfncOiUmU" width="427" height="356" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe> <div style="margin-bottom:5px"> <strong> <a href="https://www.slideshare.net/catupper/ss-26238956" title="計算量" target="_blank">計算量</a> </strong> from <strong><a href="https://www.slideshare.net/catupper" target="_blank">Ken Ogura</a></strong> </div> <cite class="hatena-citation"><a href="https://www.slideshare.net/catupper/ss-26238956">www.slideshare.net</a></cite></p> <p><br></p> <h1 id="課題">課題</h1> <p>json-origami の実際のコードを確認して, 計算量を求めてみます.</p> <p>今回パフォーマンス劣化を起こしてしまった twist 関数のコードは以下の通り.</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> fold <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'./fold'</span> <span class="synStatement">import</span> <span class="synStatement">type</span> <span class="synIdentifier">{</span> Dictionary<span class="synStatement">,</span> MoveMap<span class="synStatement">,</span> Twist<span class="synStatement">,</span> TwistOption <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'./type'</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> unfold <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'./unfold'</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> includesKey <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'./utils'</span> <span class="synStatement">export</span> <span class="synStatement">function</span> twist<span class="synStatement">&lt;</span>D <span class="synStatement">extends</span> Dictionary<span class="synStatement">,</span> M <span class="synStatement">extends</span> MoveMap<span class="synStatement">&lt;</span>D<span class="synStatement">&gt;&gt;(</span> obj: D<span class="synStatement">,</span> moveMap: M<span class="synStatement">,</span> option?: TwistOption<span class="synStatement">,</span> <span class="synStatement">)</span>: Twist<span class="synStatement">&lt;</span>D<span class="synStatement">,</span> M<span class="synStatement">&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> folded <span class="synStatement">=</span> fold<span class="synStatement">(</span>obj<span class="synStatement">,</span> option<span class="synStatement">)</span> <span class="synType">const</span> twisted <span class="synStatement">=</span> <span class="synSpecial">Object</span>.fromEntries<span class="synStatement">(</span> <span class="synSpecial">Object</span>.entries<span class="synStatement">(</span>folded<span class="synStatement">)</span>.map<span class="synStatement">((</span><span class="synIdentifier">[</span>key<span class="synStatement">,</span> value<span class="synIdentifier">]</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> found <span class="synStatement">=</span> <span class="synSpecial">Object</span>.keys<span class="synStatement">(</span>moveMap<span class="synStatement">)</span>.find<span class="synStatement">((</span>k<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> includesKey<span class="synStatement">(</span>key<span class="synStatement">,</span> k<span class="synStatement">,</span> option<span class="synStatement">))</span> <span class="synStatement">if</span> <span class="synStatement">(</span>found<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> newKey <span class="synStatement">=</span> key.replace<span class="synStatement">(</span>found<span class="synStatement">,</span> moveMap<span class="synIdentifier">[</span>found<span class="synIdentifier">]</span><span class="synConstant">!</span><span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synIdentifier">[</span>newKey<span class="synStatement">,</span> value<span class="synIdentifier">]</span> <span class="synIdentifier">}</span> <span class="synStatement">return</span> <span class="synIdentifier">[</span>key<span class="synStatement">,</span> value<span class="synIdentifier">]</span> <span class="synIdentifier">}</span><span class="synStatement">),</span> <span class="synStatement">)</span> <span class="synStatement">return</span> unfold<span class="synStatement">(</span>twisted<span class="synStatement">,</span> option<span class="synStatement">)</span> <span class="synIdentifier">}</span> </pre> <p>twist 関数は, 第 1 引数のオブジェクトが例えば <code>{ a: { b: { c: 'd' }}}</code> だったとして, 第 2 引数で <code>{ "a.b.c": "x.y.z" }</code> を指定すると, 返り値は <code>{ x: { y: { z: 'd' }}}</code> となる, 簡単に言うとオブジェクトの写像をするような処理になっています.</p> <p>ざっくりまとめると, <code>fold</code> <code>unfold</code> 中間処理 の 3ブロックがあり, そのなかで最大の計算量を求める必要があります.</p> <p>軽く調べた感じ、それぞれ以下の通りでした</p> <h5 id="fold-の計算量">fold の計算量</h5> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">function</span> fold<span class="synStatement">&lt;</span>D <span class="synStatement">extends</span> Dictionary<span class="synStatement">&gt;(</span>obj: D<span class="synStatement">,</span> option?: FoldOption<span class="synStatement">)</span>: Folded<span class="synStatement">&lt;</span>D<span class="synStatement">&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">if</span> <span class="synStatement">(</span><span class="synSpecial">Object</span>.keys<span class="synStatement">(</span>obj<span class="synStatement">)</span>.length <span class="synStatement">&lt;=</span> <span class="synConstant">0</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{}</span> <span class="synIdentifier">}</span> <span class="synStatement">return</span> <span class="synSpecial">Object</span>.fromEntries<span class="synStatement">(</span> flatEntries<span class="synStatement">(</span>option?.keyPrefix ?? <span class="synConstant">''</span><span class="synStatement">,</span> obj<span class="synStatement">,</span> <span class="synIdentifier">{</span> ...defaultCommonOption<span class="synStatement">,</span> ...option<span class="synStatement">,</span> <span class="synIdentifier">}</span> <span class="synStatement">as</span> FixedFoldOption<span class="synStatement">),</span> <span class="synStatement">)</span> <span class="synStatement">as</span> Folded<span class="synStatement">&lt;</span>D<span class="synStatement">&gt;</span> <span class="synIdentifier">}</span> <span class="synType">const</span> arrayKeyMap <span class="synStatement">=</span> <span class="synIdentifier">{</span> dot: <span class="synStatement">(</span>prefix: <span class="synType">string</span><span class="synStatement">,</span> index: <span class="synType">number</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">(</span>prefix <span class="synStatement">===</span> <span class="synConstant">''</span> ? <span class="synConstant">`</span><span class="synSpecial">${</span>index<span class="synSpecial">}</span><span class="synConstant">`</span> : <span class="synConstant">`</span><span class="synSpecial">${</span>prefix<span class="synSpecial">}</span><span class="synConstant">.</span><span class="synSpecial">${</span>index<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synStatement">),</span> bracket: <span class="synStatement">(</span>prefix: <span class="synType">string</span><span class="synStatement">,</span> index: <span class="synType">number</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synConstant">`</span><span class="synSpecial">${</span>prefix<span class="synSpecial">}</span><span class="synConstant">[</span><span class="synSpecial">${</span>index<span class="synSpecial">}</span><span class="synConstant">]`</span><span class="synStatement">,</span> <span class="synIdentifier">}</span> satisfies Record<span class="synStatement">&lt;</span>ArrayIndex<span class="synStatement">,</span> <span class="synStatement">(</span>prefix: <span class="synType">string</span><span class="synStatement">,</span> index: <span class="synType">number</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synType">string</span><span class="synStatement">&gt;</span> <span class="synStatement">function</span> flatEntries<span class="synStatement">(</span>key: <span class="synType">string</span><span class="synStatement">,</span> value: <span class="synType">object</span><span class="synStatement">,</span> opt: FixedFoldOption<span class="synStatement">)</span>: <span class="synIdentifier">[</span><span class="synType">string</span><span class="synStatement">,</span> Primitive<span class="synIdentifier">][]</span> <span class="synIdentifier">{</span> <span class="synStatement">if</span> <span class="synStatement">(</span>value <span class="synStatement">===</span> <span class="synType">undefined</span> <span class="synConstant">||</span> value <span class="synStatement">===</span> <span class="synType">null</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">[]</span> <span class="synIdentifier">}</span> <span class="synType">const</span> appendKey <span class="synStatement">=</span> <span class="synStatement">(</span>k: <span class="synType">string</span> | <span class="synType">number</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">typeof</span> k <span class="synStatement">===</span> <span class="synConstant">'number'</span> ? arrayKeyMap<span class="synIdentifier">[</span>opt.arrayIndex<span class="synIdentifier">]</span><span class="synStatement">(</span>key<span class="synStatement">,</span> k<span class="synStatement">)</span> : key <span class="synStatement">===</span> <span class="synConstant">''</span> ? k : <span class="synConstant">`</span><span class="synSpecial">${</span>key<span class="synSpecial">}</span><span class="synConstant">.</span><span class="synSpecial">${</span>k<span class="synSpecial">}</span><span class="synConstant">`</span> <span class="synStatement">if</span> <span class="synStatement">(</span><span class="synSpecial">Array</span>.isArray<span class="synStatement">(</span>value<span class="synStatement">)</span> <span class="synConstant">&amp;&amp;</span> value.length <span class="synStatement">&gt;</span> <span class="synConstant">0</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> value.flatMap<span class="synStatement">((</span>v<span class="synStatement">,</span> i<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> flatEntries<span class="synStatement">(</span>appendKey<span class="synStatement">(</span>i<span class="synStatement">),</span> v<span class="synStatement">,</span> opt<span class="synStatement">))</span> <span class="synIdentifier">}</span> <span class="synStatement">if</span> <span class="synStatement">(typeof</span> value <span class="synStatement">===</span> <span class="synConstant">'object'</span> <span class="synConstant">&amp;&amp;</span> <span class="synSpecial">Object</span>.keys<span class="synStatement">(</span>value<span class="synStatement">)</span>.length <span class="synStatement">&gt;</span> <span class="synConstant">0</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synSpecial">Object</span>.entries<span class="synStatement">(</span>value <span class="synStatement">as</span> <span class="synType">object</span><span class="synStatement">)</span>.flatMap<span class="synStatement">((</span><span class="synIdentifier">[</span>k<span class="synStatement">,</span> v<span class="synIdentifier">]</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> flatEntries<span class="synStatement">(</span>appendKey<span class="synStatement">(</span>k<span class="synStatement">),</span> v<span class="synStatement">,</span> opt<span class="synStatement">))</span> <span class="synIdentifier">}</span> <span class="synStatement">return</span> <span class="synIdentifier">[[</span>key<span class="synStatement">,</span> value<span class="synIdentifier">]]</span> <span class="synIdentifier">}</span> </pre> <p> fold 関数は <code>{ a: { b: { c: "d" }}}</code> のようなオブジェクトを <code>{ "a.b.c": "d" }</code> のような形に直します.</p> <p>複数のループのネストはあるものの, 合計すると json object におけるプリミティブな値の数しか処理を実行していません. したがって, json object のプリミティブな値の数を $N$ とおくとして, fold 関数の時間計算量は $O(N)$ といえます.</p> <p>json object の階層の数だけ再帰処理を行っており, その度に javascript の新しいスコープが発生することから, 階層の数を $D$ とおくとして, $O(D)$ といえるでしょう.</p> <h5 id="unfold-の計算量">unfold の計算量</h5> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">function</span> unfold<span class="synStatement">&lt;</span>Kv <span class="synStatement">extends</span> Folded<span class="synStatement">&lt;</span><span class="synType">any</span><span class="synStatement">&gt;&gt;(</span>kv: Kv<span class="synStatement">,</span> option?: UnfoldOption<span class="synStatement">)</span>: Unfolded<span class="synStatement">&lt;</span>Kv<span class="synStatement">&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> fixedOpion <span class="synStatement">=</span> <span class="synIdentifier">{</span> ...defaultUnfoldOption<span class="synStatement">,</span> ...option<span class="synStatement">,</span> <span class="synIdentifier">}</span> <span class="synStatement">as</span> FixedUnfoldOption validateKeys<span class="synStatement">(</span>kv<span class="synStatement">)</span> <span class="synStatement">return</span> unfoldInternal<span class="synStatement">(</span><span class="synSpecial">Object</span>.entries<span class="synStatement">(</span>kv<span class="synStatement">),</span> fixedOpion<span class="synStatement">)</span> <span class="synStatement">as</span> Unfolded<span class="synStatement">&lt;</span>Kv<span class="synStatement">&gt;</span> <span class="synIdentifier">}</span> <span class="synStatement">function</span> unfoldInternal<span class="synStatement">(</span>entries: <span class="synIdentifier">[</span><span class="synType">string</span><span class="synStatement">,</span> <span class="synType">unknown</span><span class="synIdentifier">][]</span><span class="synStatement">,</span> opt: FixedUnfoldOption<span class="synStatement">)</span>: <span class="synType">unknown</span> <span class="synIdentifier">{</span> <span class="synStatement">if</span> <span class="synStatement">(</span>entries.length <span class="synStatement">&lt;=</span> <span class="synConstant">0</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{}</span> <span class="synIdentifier">}</span> <span class="synType">const</span> firstKey <span class="synStatement">=</span> extractHeadKey<span class="synStatement">(</span>entries<span class="synIdentifier">[</span><span class="synConstant">0</span><span class="synIdentifier">]</span><span class="synConstant">!</span><span class="synIdentifier">[</span><span class="synConstant">0</span><span class="synIdentifier">]</span><span class="synStatement">,</span> opt<span class="synStatement">)</span> <span class="synStatement">if</span> <span class="synStatement">(</span>firstKey <span class="synStatement">===</span> <span class="synConstant">''</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> entries<span class="synIdentifier">[</span>entries.length - <span class="synConstant">1</span><span class="synIdentifier">]</span><span class="synConstant">!</span><span class="synIdentifier">[</span><span class="synConstant">1</span><span class="synIdentifier">]</span> <span class="synIdentifier">}</span> <span class="synStatement">if</span> <span class="synStatement">(typeof</span> firstKey <span class="synStatement">===</span> <span class="synConstant">'number'</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> maxIndex <span class="synStatement">=</span> entries .map<span class="synStatement">((</span><span class="synIdentifier">[</span>key<span class="synIdentifier">]</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> extractHeadKey<span class="synStatement">(</span>key<span class="synStatement">,</span> opt<span class="synStatement">)</span> <span class="synStatement">as</span> <span class="synType">number</span><span class="synStatement">)</span> .reduce<span class="synStatement">((</span>acc<span class="synStatement">,</span> index<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synSpecial">Math</span>.max<span class="synStatement">(</span>acc<span class="synStatement">,</span> index<span class="synStatement">),</span> <span class="synConstant">0</span><span class="synStatement">)</span> <span class="synType">const</span> array <span class="synStatement">=</span> <span class="synStatement">new</span> <span class="synSpecial">Array</span><span class="synStatement">(</span>maxIndex + <span class="synConstant">1</span><span class="synStatement">)</span>.fill<span class="synStatement">(</span><span class="synType">undefined</span><span class="synStatement">)</span>.map<span class="synStatement">((</span>_<span class="synStatement">,</span> index<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> filteredEntries <span class="synStatement">=</span> entries.filter<span class="synStatement">((</span><span class="synIdentifier">[</span>key<span class="synIdentifier">]</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> extractHeadKey<span class="synStatement">(</span>key<span class="synStatement">,</span> opt<span class="synStatement">)</span> <span class="synStatement">===</span> index<span class="synStatement">)</span> <span class="synStatement">if</span> <span class="synStatement">(</span>filteredEntries.length <span class="synStatement">&lt;=</span> <span class="synConstant">0</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synType">undefined</span> <span class="synIdentifier">}</span> <span class="synType">const</span> unfolded <span class="synStatement">=</span> unfoldInternal<span class="synStatement">(</span> filteredEntries.map<span class="synStatement">((</span><span class="synIdentifier">[</span>key<span class="synStatement">,</span> value<span class="synIdentifier">]</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">[</span>omitHeadKey<span class="synStatement">(</span>key<span class="synStatement">,</span> opt<span class="synStatement">),</span> value<span class="synIdentifier">]</span><span class="synStatement">),</span> opt<span class="synStatement">,</span> <span class="synStatement">)</span> <span class="synStatement">return</span> unfolded <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synStatement">if</span> <span class="synStatement">(</span>opt.pruneArray<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> array.filter<span class="synStatement">((</span>v<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> v <span class="synStatement">!==</span> <span class="synType">undefined</span><span class="synStatement">)</span> <span class="synIdentifier">}</span> <span class="synStatement">return</span> array <span class="synIdentifier">}</span> <span class="synType">const</span> keys <span class="synStatement">=</span> <span class="synSpecial">Array</span>.<span class="synStatement">from(new</span> <span class="synSpecial">Set</span><span class="synStatement">(</span>entries.map<span class="synStatement">((</span><span class="synIdentifier">[</span>key<span class="synIdentifier">]</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> extractHeadKey<span class="synStatement">(</span>key<span class="synStatement">,</span> opt<span class="synStatement">)</span> <span class="synStatement">as</span> <span class="synType">string</span><span class="synStatement">)))</span> <span class="synStatement">return</span> keys.reduce<span class="synStatement">((</span>acc<span class="synStatement">,</span> key<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> filteredEntries <span class="synStatement">=</span> entries.filter<span class="synStatement">((</span><span class="synIdentifier">[</span>k<span class="synIdentifier">]</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> extractHeadKey<span class="synStatement">(</span>k<span class="synStatement">,</span> opt<span class="synStatement">)</span> <span class="synStatement">===</span> key<span class="synStatement">)</span> <span class="synType">const</span> unfolded <span class="synStatement">=</span> unfoldInternal<span class="synStatement">(</span> filteredEntries.map<span class="synStatement">((</span><span class="synIdentifier">[</span>k<span class="synStatement">,</span> value<span class="synIdentifier">]</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">[</span>omitHeadKey<span class="synStatement">(</span>k<span class="synStatement">,</span> opt<span class="synStatement">),</span> value<span class="synIdentifier">]</span><span class="synStatement">),</span> opt<span class="synStatement">,</span> <span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> ...acc<span class="synStatement">,</span> <span class="synIdentifier">[</span>key<span class="synIdentifier">]</span>: unfolded<span class="synStatement">,</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">{}</span><span class="synStatement">)</span> <span class="synIdentifier">}</span> </pre> <p>正直なんでこんなコード書いたんだろう・・っていう気がします.</p> <p>unfold 関数は <code>{ "a.b.c": "d" }</code> のようなオブジェクトを <code>{ a: { b: { c: "d" } } }</code> のような形に直す処理をしています.</p> <p>こちらも再帰処理がありますが, json object のプリミティブ値の数のループがあり, それをフィルタした数だけ再帰に突入しています.</p> <p>再帰のたびにループ回数が減少します. 具体的な計算は困難であるため, 適当に最悪のケースで時間計算量を考え, $O(N^{D})$ とみなして良さそうです.</p> <p>また, fold と同様に再帰のたびにスコープが生まれるため, 空間計算量は $O(D)$ と考えて良さそうです.</p> <h5 id="中間処理の計算量">中間処理の計算量</h5> <p>再掲ですが, twist 関数は以下のコードでした.</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> fold <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'./fold'</span> <span class="synStatement">import</span> <span class="synStatement">type</span> <span class="synIdentifier">{</span> Dictionary<span class="synStatement">,</span> MoveMap<span class="synStatement">,</span> Twist<span class="synStatement">,</span> TwistOption <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'./type'</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> unfold <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'./unfold'</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> includesKey <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'./utils'</span> <span class="synStatement">export</span> <span class="synStatement">function</span> twist<span class="synStatement">&lt;</span>D <span class="synStatement">extends</span> Dictionary<span class="synStatement">,</span> M <span class="synStatement">extends</span> MoveMap<span class="synStatement">&lt;</span>D<span class="synStatement">&gt;&gt;(</span> obj: D<span class="synStatement">,</span> moveMap: M<span class="synStatement">,</span> option?: TwistOption<span class="synStatement">,</span> <span class="synStatement">)</span>: Twist<span class="synStatement">&lt;</span>D<span class="synStatement">,</span> M<span class="synStatement">&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> folded <span class="synStatement">=</span> fold<span class="synStatement">(</span>obj<span class="synStatement">,</span> option<span class="synStatement">)</span> <span class="synComment">// ここから中間処理</span> <span class="synType">const</span> twisted <span class="synStatement">=</span> <span class="synSpecial">Object</span>.fromEntries<span class="synStatement">(</span> <span class="synSpecial">Object</span>.entries<span class="synStatement">(</span>folded<span class="synStatement">)</span>.map<span class="synStatement">((</span><span class="synIdentifier">[</span>key<span class="synStatement">,</span> value<span class="synIdentifier">]</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> found <span class="synStatement">=</span> <span class="synSpecial">Object</span>.keys<span class="synStatement">(</span>moveMap<span class="synStatement">)</span>.find<span class="synStatement">((</span>k<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> includesKey<span class="synStatement">(</span>key<span class="synStatement">,</span> k<span class="synStatement">,</span> option<span class="synStatement">))</span> <span class="synStatement">if</span> <span class="synStatement">(</span>found<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> newKey <span class="synStatement">=</span> key.replace<span class="synStatement">(</span>found<span class="synStatement">,</span> moveMap<span class="synIdentifier">[</span>found<span class="synIdentifier">]</span><span class="synConstant">!</span><span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synIdentifier">[</span>newKey<span class="synStatement">,</span> value<span class="synIdentifier">]</span> <span class="synIdentifier">}</span> <span class="synStatement">return</span> <span class="synIdentifier">[</span>key<span class="synStatement">,</span> value<span class="synIdentifier">]</span> <span class="synIdentifier">}</span><span class="synStatement">),</span> <span class="synStatement">)</span> <span class="synComment">// ここまで中間処理</span> <span class="synStatement">return</span> unfold<span class="synStatement">(</span>twisted<span class="synStatement">,</span> option<span class="synStatement">)</span> <span class="synIdentifier">}</span> <span class="synStatement">export</span> <span class="synStatement">function</span> includesKey<span class="synStatement">(</span> origin: <span class="synType">string</span><span class="synStatement">,</span> match: <span class="synType">string</span> | <span class="synSpecial">RegExp</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> arrayIndex <span class="synIdentifier">}</span>: CommonOption <span class="synStatement">=</span> defaultCommonOption<span class="synStatement">,</span> <span class="synStatement">)</span>: <span class="synType">boolean</span> <span class="synIdentifier">{</span> <span class="synStatement">if</span> <span class="synStatement">(</span>match <span class="synStatement">instanceof</span> <span class="synSpecial">RegExp</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> match.test<span class="synStatement">(</span>origin<span class="synStatement">)</span> <span class="synIdentifier">}</span> <span class="synType">const</span> split <span class="synStatement">=</span> <span class="synStatement">(</span>key: <span class="synType">string</span><span class="synStatement">)</span>: <span class="synType">string</span><span class="synIdentifier">[]</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> fixedKey <span class="synStatement">=</span> arrayIndex <span class="synStatement">===</span> <span class="synConstant">'bracket'</span> ? key.replace<span class="synStatement">(</span><span class="synConstant">/\[(\w+)\]/g</span><span class="synStatement">,</span> <span class="synConstant">'.$1'</span><span class="synStatement">)</span> : key <span class="synStatement">return</span> fixedKey.split<span class="synStatement">(</span><span class="synConstant">'.'</span><span class="synStatement">)</span> <span class="synIdentifier">}</span> <span class="synType">const</span> originKeys <span class="synStatement">=</span> split<span class="synStatement">(</span>origin<span class="synStatement">)</span> <span class="synType">const</span> keys <span class="synStatement">=</span> split<span class="synStatement">(</span>match<span class="synStatement">)</span> <span class="synStatement">if</span> <span class="synStatement">(</span>keys.length <span class="synStatement">&gt;</span> originKeys.length<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synConstant">false</span> <span class="synIdentifier">}</span> <span class="synType">const</span> targets <span class="synStatement">=</span> originKeys.slice<span class="synStatement">(</span><span class="synConstant">0</span><span class="synStatement">,</span> keys.length<span class="synStatement">)</span> <span class="synStatement">return</span> targets.join<span class="synStatement">(</span><span class="synConstant">'.'</span><span class="synStatement">)</span> <span class="synStatement">===</span> keys.join<span class="synStatement">(</span><span class="synConstant">'.'</span><span class="synStatement">)</span> <span class="synIdentifier">}</span> </pre> <p>ここの中間処理で, fold 結果のキーを写像先のキーに変換しています. この結果を unfold すればほしい結果が得られるという算段です.<br/> だいぶぐるぐる回ってるのが見て取れますね.</p> <p>includesKey は文字列をオブジェクトキーに見立てた場合の前方一致を見ています. includesKey の引数が RegExp のケースは twist 関数から呼ばれるケースでは利用されていないため無視します.</p> <p>split は key の文字列長に対して線形の時間計算量であるため, includesKey は入力の origin の文字列長を $n$ , match の文字列長を $m$ とおいた際に $O(n + m)$ と考えられます.</p> <p>ここで引数となる文字列は origin, match ともに <code>"a.b.c"</code> のような結合された文字列であるため, オブジェクトの深さ $D$ と, 変換元オブジェクトの平均的なキー文字列長 $L$ に置き換えて, $O(LD)$ としてしまったほうが, 結果がわかりやすそうです.</p> <p>中間処理では, fold 結果の値数と 写像用の key-value オブジェクトのキー数の二重ループ構造で includesKey をコールしています. fold 結果はすなわち変換対象のオブジェクトの値数に同じであるため, 写像対象の値数を $M$ とおくことで, 時間計算量を $O(NMLD)$ としてよさそうです.</p> <p>空間計算量については, 単純に変数を見れば良さそうなので, 時間計算量と同様の $O(LD)$ としました.</p> <h5 id="合算">合算</h5> <p>まとめると, まず考慮すべき変数として以下があります</p> <ul> <li>$N$ .. 変換元となるオブジェクトにおけるプリミティブな値の総数</li> <li>$M$ .. 写像対象とするキーの数</li> <li>$D$ .. 変換元となるオブジェクトのネスト数</li> <li>$L$ .. 全キーの文字列長の平均</li> </ul> <p>twist を構成する 3 つの処理における計算量は以下のとおりでした</p> <ul> <li>fold <ul> <li>時間計算量 .. $O(N)$</li> <li>空間計算量 .. $O(D)$</li> </ul> </li> <li>中間処理 <ul> <li>時間計算量 .. $O(NMLD)$</li> <li>空間計算量 .. $O(LD)$</li> </ul> </li> <li>unfold <ul> <li>時間計算量 .. $O(N^{D})$</li> <li>空間計算量 .. $O(D)$</li> </ul> </li> </ul> <p>上記より, それぞれの和から係数の少ない項を排除すると, 時間計算量は</p> <p>$$ O(N) + O(NMLD) + O(N^{D}) = O(NMLD + N^{D}) $$</p> <p>空間計算量は</p> <p>$$ O(D) + O(LD) + O(D) = O(LD) $$</p> <p>となりました. 時間計算量がとにかくヤバいですね.</p> <h5 id="なぜプロダクトのコードでパフォーマンス劣化が起きたか">なぜプロダクトのコードでパフォーマンス劣化が起きたか?</h5> <p>単純な話で, D が 10 ~ 11 くらいあるキーがありました. 探せばもっと大きいかも.</p> <p>また, 実は javascript における RegExp が, ただの String の関数を呼ぶ場合と比べると少し重めであることも最大の原因の一つでした. 中間処理では正規表現による処理を $O(NM)$ 回コールしていることになります.</p> <p><br></p> <h1 id="どう改善すべきか">どう改善すべきか?</h1> <p>基本的に, twist 関数については以下を考慮して処理を改善すべきと判断しました.</p> <ul> <li>unfold を使わない</li> <li>正規表現を使わない</li> </ul> <p>また, twist 関数に限らず, 全体的に以下を考慮して同様に修正すべきと考えました.</p> <ul> <li>ネストとループを組み合わせない</li> <li>キャッシュを考慮できるロジックを考えるべき</li> <li>空間計算量はもう少し贅沢に使って良い. ただしちゃんと開放されたか確認する.</li> </ul> <p>今回のインシデント未遂をうけて, このパフォーマンス改善のために, すべて書き直す対応を入れることにしました.</p> <p>改善に際し, ロジックの書き直しに際し, ユニットテストは元々十分にありました. また, 改善が確認できるように, パフォーマンステストを事前導入し, 改善前後での差を確認しながら取り組みました.</p> <h4 id="改善のアイデア">改善のアイデア</h4> <p>そもそも twist 関数以外にも, 以下の問題が同様にありました.</p> <ul> <li>基本的な処理は fold → 中間処理 → unfold で実現される</li> <li>処理が全般的に愚直なため, キャッシュの戦略を立てれない</li> </ul> <p>そのため, 以下の要件を満たすようなオブジェクトを作り, 各関数はこれを用いて結果のオブジェクトを作るように変更しました.</p> <ul> <li>ネストされたオブジェクトを内包する</li> <li><code>"a.b.c"</code> のようなキーに対して get, set の処理ができる</li> <li>set においては, 途中のキーに対応するオブジェクトがない場合は都度新規に作成する</li> <li>途中まで同じキーに対するアクセスをキャッシュによって省略する</li> </ul> <p>そのときの修正の PR は以下です. 説明すると膨大になるため, 省略します. <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhacomono-lib%2Fjson-origami%2Fpull%2F109" title="fix: #106 performance improvement by mew-ton · Pull Request #109 · hacomono-lib/json-origami" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hacomono-lib/json-origami/pull/109">github.com</a></cite></p> <h4 id="改善の結果">改善の結果</h4> <p>オブジェクトのプリミティブ値の数 $N$ に対して, 変更前後でのパフォーマンスを計測した結果, 以下のグラフのようになりました.</p> <p>計測には tinybench を使用しています. <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240227/20240227025045.png" width="1200" height="745" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>緑線が途中までしか記録されていないのは, 個人的に, 今後の先の向きが予想できるし, もう悪化具合が見てられなくなったためです. 中途半端なデータでごめんなさい.</p> <p>それぞれ, バージョンについては以下の通り.</p> <ul> <li>v0.4.0 .. 早かったけどバグのあるバージョン</li> <li>v0.4.1 .. バグ修正のためにパフォーマンスが劣化してしまったバージョン</li> <li>current .. 今回の修正</li> </ul> <p>また, 計測は最低 10 回実施しており, 線の色はそれぞれ以下を示しています.</p> <ul> <li>min .. 最短の結果</li> <li>max .. 最長の結果</li> <li>mean .. 結果の平均</li> </ul> <p>横軸は $N$, 縦軸は所要時間 (ms) です.<br/> 計測に際し, 値数 $N$ の完全ランダムなオブジェクトを作るようなロジックを組みました. <br/> これは単純に 実利用において一番ブレやすい変数が値数であるかなと思ったためです.</p> <p>改善結果として, min-max の分散が激しかった従来に比べ, 結果は分散が極端に少なくなり, 安定した速度を叩き出せるようになりました. また, $N=5000$ 以下までは改善前と比べて早い結果になりました.</p> <p>反面, $N=5000$ を超えると勾配が急になっていく様子が見られるため, どこかで累乗の指数が上がった, もしくは軽減できていないことが読み取れると思います.</p> <p>実際の PR において, 修正後のコードでのパフォーマンスの問題が CodeRabbit によって指摘されました. <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240227/20240227025130.png" width="1200" height="827" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>実務での利用は $N=3000 ... 4000$ 付近であるため, 一旦はこれでヨシとし, 別の issue としてこれを対処する方針としました.</p> <p>CodeRabbit はこういう所を目ざとく見つけてくれるので, 大変助かりますね.</p> <p><br></p> <h1 id="まとめ">まとめ</h1> <p>今回は, 計算複雑性理論について, 軽くではあるけどしっかり向き合い, パフォーマンス貢献のための緒をみつけるのに役立てました.</p> <p>計算だけでもだいぶしんどかった. でも, こういう計算を一度はしっかり理解しておくと, 今後パフォーマンスの悪いコードをコーディング・レビュー単位で防げるため, やってよかったと思ってます. <br> <br></p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています。 <br>採用情報や採用ウィッシュリストもぜひご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(プロダクトチーム向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech 【イベントレポート】YAPC::Hiroshima 2024 に協賛&登壇しました hatenablog://entry/6801883189083687126 2024-02-20T11:00:00+09:00 2024-02-20T16:01:36+09:00 こんにちは、DevHRのなんちな (@nanchina_nano) です! 2月10日(土)に広島国際会議場で開催されたYAPC::Hiroshima 2024 に、hacomonoも初めてゴールドスポンサー、スペシャルランチスポンサーとして参加させていただきました! yapcjapan.org この記事では、当日の登壇者と発表資料、当日のイベントの振り返りをしたいと思います。 最高に楽しいイベントだったので、少しでも雰囲気が伝われば嬉しいです! 登壇者と発表内容のご紹介 ランチセッションでは、hacomonoのCTO工藤(@macococo)が「プロダクトエンジニア」のキャリアについて発表さ… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240219/20240219202943.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは、DevHRのなんちな (<a href="https://twitter.com/nanchina_nano">@nanchina_nano</a>) です!</p> <p>2月10日(土)に<a href="https://d.hatena.ne.jp/keyword/%E5%BA%83%E5%B3%B6%E5%9B%BD%E9%9A%9B%E4%BC%9A%E8%AD%B0%E5%A0%B4">広島国際会議場</a>で開催されたYAPC::Hiroshima 2024 に、hacomonoも初めてゴールドスポンサー、スペシャルランチスポンサーとして参加させていただきました! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fyapcjapan.org%2F2024hiroshima%2F" title="YAPC::Hiroshima 2024 " class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://yapcjapan.org/2024hiroshima/">yapcjapan.org</a></cite></p> <p>この記事では、当日の登壇者と発表資料、当日のイベントの振り返りをしたいと思います。<br/> 最高に楽しいイベントだったので、少しでも雰囲気が伝われば嬉しいです! <br> <br></p> <h1 id="登壇者と発表内容のご紹介">登壇者と発表内容のご紹介</h1> <p>ランチセッションでは、hacomonoのCTO工藤(<a href="https://twitter.com/macococo">@macococo</a>)が「プロダクトエンジニア」のキャリアについて発表させていただきました。</p> <p>セッション資料は以下に公開しておりますので、是非ご覧ください! <iframe id="talk_frame_1144664" class="speakerdeck-iframe" src="//speakerdeck.com/player/bc2f753a7f804d2d9c5be13a872955ec" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/hacomono/urutoraziyanpu-nacheng-chang-wozhi-erupurodakutoenziniatoiukiyaria">speakerdeck.com</a></cite> <figure class="figure-image figure-image-fotolife" title="CTO工藤の登壇の様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240219/20240219175848.png" width="1200" height="1093" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>CTO工藤の登壇の様子</figcaption></figure></p> <p>400名ほどが入る大きい会場でした!</p> <p>とても好評だったようで、登壇後はブースにも工藤宛に足を運んでくださる方もいらっしゃいました🙌(工藤が居ない時間もあり、お手数おかけいたしました🙇🏼‍♂️) <br> <br></p> <h1 id="スポンサーブース紹介">スポンサーブース紹介</h1> <p>今回は、CTO以外にもエンジニア4名、DevHR2名と過去最多の合計7名で参加させていただきました!</p> <p>hacomonoのブースでは、ノベルティとしてストレッチボールやプリングルス、hacomonoロゴ入りの飲料水、hacomonoカレンダーをお渡ししました!</p> <p>当日ブースにお立ち寄りいただいた皆様、ありがとうございました。 <figure class="figure-image figure-image-fotolife" title="Wellnessの「W」ポーズで集合写真"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240219/20240219180029.png" width="1040" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Wellnessの「W」ポーズで集合写真</figcaption></figure></p> <p>毎回ノベルティ作りには頭を悩ませるのですが、今回一番人気だったのはこのコルクストレッチボールでした!</p> <p>普段座りっぱなしで身体が凝り固まっているエンジニアの方も多いだろうと思い、少しでもほぐしたい・・・!と、作ってみました😀<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240219/20240219180315.png" width="1200" height="1086" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>hacomono devlopersのプリングルスも X に投稿いただきました!嬉しい!<br/> <blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">そういや朝イチで hacomono さんのプリングルスもいただいてました。<a href="https://twitter.com/hashtag/yapcjapan?src=hash&amp;ref_src=twsrc%5Etfw">#yapcjapan</a> <a href="https://t.co/5hAoKKI3yD">pic.twitter.com/5hAoKKI3yD</a></p>&mdash; KOYAMA Tetsuji (こいほげ) (@koyhoge) <a href="https://twitter.com/koyhoge/status/1756159500539867583?ref_src=twsrc%5Etfw">2024年2月10日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <br> <br></p> <h1 id="各セッションの感想">各セッションの感想</h1> <p>hacomonoエンジニアが参加させていただいたセッションの中で印象に残ったものをご紹介いたします!</p> <h5 id="プロダクトエンジニア-こじこじkoji_kono">プロダクトエンジニア こじこじ(<a href="https://twitter.com/koji_kono">@koji_kono</a>)</h5> <hr /> <h6 id="VISAカードの裏側と-手が掛かる-決済システムの育て方">VISAカードの裏側と “手が掛かる” 決済システムの育て方</h6> <p><iframe id="talk_frame_1144608" class="speakerdeck-iframe" src="//speakerdeck.com/player/525267e2aa7f413e83c7cdfc95d031ce" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/shoheimitani/visakadonoli-ce-to-shou-gagua-karu-jue-ji-sisutemunoyu-tefang">speakerdeck.com</a></cite></p> <p>VisaNetの決済ネットワークと処理の流れについて大変分かりやすくまとめられたセッションだった。<br/> ブランド・アクワイアラー・イシュアーなどの登場人物の関係やオーソリ・クリアリングなどの通信と仮決済・本決済の処理の流れなどの仕組みが勉強になった。<br/> 上記にまつわるレガシーな仕組みの上での開発の苦労なども、参考になった。</p> <h6 id="経営意思エンジニアリング">経営・意思・エンジニアリング</h6> <p><iframe id="talk_frame_1144621" class="speakerdeck-iframe" src="//speakerdeck.com/player/a0c9a6d02aa244eeb5bfa0c0b28cb2e1" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/ymatsuwitter/management-conviction-and-engineering">speakerdeck.com</a></cite></p> <p>全く事象としては別だが自分の中ではインシデントの予防や事前準備などを設計する際に変数と制約は常に意識していることであったので、凄く話として理解しやすい内容だった。</p> <p>そして結局は正解なんてものはなく、自分達でバランスを取ることが大事というのは、自分の中でも共感できる部分だった。</p> <p>何故バランスを取って考えなければいけないのか?その決定を行うにはどのようなトレードオフが必要なのか?ということをチームや社内に説明する際に非常に参考になる話だった。 <br> <br></p> <h5 id="IoTチーム所属-SREエンジニア-しぐさんsakadachimaru">IoTチーム所属 SREエンジニア しぐさん(<a href="https://twitter.com/sakadachimaru">@sakadachimaru</a>)</h5> <hr /> <h6 id="理解容易性と変更容易性を支える自動テスト">理解容易性と変更容易性を支える自動テスト</h6> <p><iframe id="talk_frame_1144665" class="speakerdeck-iframe" src="//speakerdeck.com/player/a87e76a8d71f450f93fa5c326409e4b5" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/twada/automated-test-knowledge-from-savanna-202402-yapc-hiroshima-edition">speakerdeck.com</a></cite></p> <p>t-wadaさんの話は数年ぶり聞いたので、凄くテスト文化に対しての進化を感じた。</p> <p>書籍などでキャッチアップなどは定期的にしているつもりなのだが、ギャップを大きく感じたのでキャッチアップをもう少し頑張っていきたいと思えた。</p> <p>最近配属された部署にてチームの立ち上げを行っているため、今回の公演を参考に文化形成を行っていきたい。 <br> <br></p> <h5 id="プラットフォームエンジニアまこたすMaco_Tasu">プラットフォームエンジニア まこたす(<a href="https://twitter.com/Maco_Tasu">@Maco_Tasu</a>)</h5> <hr /> <h6 id="Amazon-ECSで好きなだけ検証環境を起動できるOSSの設計実装運用">Amazon ECSで好きなだけ検証環境を起動できるOSSの設計・実装・運用</h6> <p><iframe id="talk_frame_1144629" class="speakerdeck-iframe" src="//speakerdeck.com/player/705ee2271f80412a9ed71ce3a92cb682" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/fujiwara3/yapc-hiroshima-2024">speakerdeck.com</a></cite></p> <p>検証環境の問題はどの会社でも共通して直面するもので、それを解決してくれるmirage-ecsはコストバランスが良いとても便利なパッケージだと感じました。<br/> OSSになっているため、誰でも同様の検証環境を短時間で構築できるので、いちエンジニアとしてとても興味が湧きました。 <br> <br></p> <h1 id="最後に">最後に</h1> <p>hacomonoとしては、昨年10月に参加したVue Fes Japan 2023に引き続き、オフラインでは2回目の技術カンファレンスでした。<br/> 今回は運営メンバーが増えたこともあり、準備から当日の運営までみんなで楽しみながら進めることができて本当に嬉しかったです。</p> <p>Vue Fes Japan 2023のレポートはこちら <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftechblog.hacomono.jp%2Fentry%2F2023%2F10%2F31%2F192117" title="【番外編】Vue Fes Japan 2023 に協賛&登壇しました - hacomono TECH BLOG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://techblog.hacomono.jp/entry/2023/10/31/192117">techblog.hacomono.jp</a></cite></p> <p>hacomonoは全国からフルリモートで働いているエンジニアがいるため、今回は広島在住のメンバーにも運営に携わってもらいました!</p> <p>エンジニアも各社のセッションが聞けたり、エンジニア同士での情報交換ができたりと、貴重な機会になったようで楽しんでいました☺️</p> <p>お祭り感覚で盛り上がれるオフラインイベントはやっぱり楽しい!!! <br> <br></p> <p>最後に、運営していただいたYAPCのスタッフの皆様、本当にありがとうございました。<br/> ホスピタリティ溢れる運営で、当日まで不安もなく迎えることができました。<br/> 今年のYAPCは広島でしたが、来年はどこになるかな〜?来年も楽しみ!</p> <p>また参戦させていただきたいです!<br/> <figure class="figure-image figure-image-fotolife" title="運営メンバーでの打ち上げの様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240219/20240219180848.png" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>運営メンバーでの打ち上げの様子</figcaption></figure></p> <p><strong>(本当に最後)KPTでの振り返り</strong></p> <ul> <li>ブース当日の打ち合わせMTGをもっと早くセッティングする</li> <li>ノベルティキャンペーンのQR読み込みは、notionを挟むと1ステップ手間に見える</li> <li>ステッカーないですか?ってお声をかけていただいたので、作りたい</li> <li>X(旧Twitter)での投稿による認知を試してみる</li> <li>マルチDayカンファレンスへの応用</li> </ul> <p>次のイベントではもっと良い体験をしていただけるように頑張ります。 <br> <br></p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています。 <br>採用情報や採用ウィッシュリストもぜひご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(プロダクトチーム向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech (小ネタ) Vitest で Node.js の GC をテストする hatenablog://entry/6801883189083667978 2024-02-19T11:00:00+09:00 2024-02-19T11:00:02+09:00 どうもみゅーとんです. 小ネタです. 弊社は GitHub の hacomono-lib という organization にて, OSS を公開しているのですが, なんとなく作っているロジックがちゃんとメモリ解放されているかが気になったので, そのテストを書く方法を探りました. 概要 3 行でまとめ Node.js 実行時のオプションにて, ガベージコレクションを任意実行させる関数を有効化できる メモリ解放されているかどうかは WeakRef を利用すると確認できる Node.js 実行時のオプションを使って Vitest を実行できる ここで話題にしないこと メモリ管理 / ガベージコレク… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240216/20240216175250.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>どうも<a href="https://twitter.com/_mew_ton">みゅーとん</a>です.</p> <p>小ネタです.</p> <p>弊社は GitHub の <a href="https://github.com/hacomono-lib">hacomono-lib</a> という organization にて, OSS を公開しているのですが,<br/> なんとなく作っているロジックがちゃんとメモリ解放されているかが気になったので, そのテストを書く方法を探りました. <br> <br></p> <h1 id="概要">概要</h1> <h4 id="3-行でまとめ">3 行でまとめ</h4> <ul> <li>Node.js 実行時のオプションにて, ガベージコレクションを任意実行させる関数を有効化できる</li> <li>メモリ解放されているかどうかは <code>WeakRef</code> を利用すると確認できる</li> <li>Node.js 実行時のオプションを使って Vitest を実行できる</li> </ul> <h4 id="ここで話題にしないこと">ここで話題にしないこと</h4> <ul> <li>メモリ管理 / ガベージコレクタについて</li> <li>Vitest などのテスト導入について</li> </ul> <h1 id="背景">背景</h1> <p>冒頭に記載したとおりですが, OSS を作っていてメモリがちゃんと開放されているかどうかが気になった次第です.</p> <p>ライブラリのロジック内で, 複雑なロジックを実行するために必要なオブジェクトを作っており, これが処理完了後のガベージコレクタでちゃんとメモリが開放されているかどうか, 気になりませんかね.</p> <p>私は気になりました. <br> <br></p> <h1 id="実践">実践</h1> <h4 id="テストを書くためのヒント">テストを書くためのヒント</h4> <p>ここでつかえるのは <code>WeakRef</code> です. <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fja%2Fdocs%2FWeb%2FJavaScript%2FReference%2FGlobal_Objects%2FWeakRef" title="WeakRef - JavaScript | MDN" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/WeakRef">developer.mozilla.org</a></cite></p> <p><code>WeakRef</code> については, この記事が最も詳細に解説してくれているので, 詳細は省きます. <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fuhyo%2Fitems%2F5dc97667ba90ce3941cd" title="WeakRef: JavaScriptに弱参照がやってくる(ついでにFinalizationも) - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://qiita.com/uhyo/items/5dc97667ba90ce3941cd">qiita.com</a></cite></p> <p>要は, ガベージコレクタが効いた時点で参照がなければ, undefined になる値です.</p> <p>内部処理で作ったオブジェクトなどを WeakRef で扱っておき, 参照がなくなった後のガベージコレクタの実施後, これが undefined になっていることが確認できていれば OK と判断してよいでしょう.</p> <p>逆にこれが undefined になっていなければ, それは参照が残っている証拠であり, メモリリークが起きる原因になりえます.</p> <h4 id="ガベージコレクタを実施させる">ガベージコレクタを実施させる</h4> <p>基本的にガベージコレクタは内部で勝手に動くものであるため, 任意実行させることはできません.<br/> これではテストができないため, ガベージコレクションを強制的に実行させる必要があります.</p> <p>実行させるには <code>global.gc()</code> という関数をコールすれば OK です.<br/> ただ, これは基本的に隠された機能で, NodeJS 実行時に <code>--expose-gc</code> オプションを設定する必要があります.</p> <p>例:</p> <pre class="code lang-sh" data-lang="sh" data-unlink>node <span class="synSpecial">--expose-gc</span> ./index.js </pre> <h4 id="テストを書く">テストを書く</h4> <p>上記をふまえてテストを書いてみます.</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> it<span class="synStatement">,</span> expect <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'vitest'</span> it<span class="synStatement">(</span><span class="synConstant">'gc test'</span><span class="synStatement">,</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">let</span> source <span class="synStatement">=</span> <span class="synIdentifier">{}</span> <span class="synComment">// 開放されるべきオブジェクトを取得し, weakRef につっこんでおく.</span> <span class="synComment">// source = null のときに開放されることを期待する</span> <span class="synType">const</span> target <span class="synStatement">=</span> <span class="synStatement">new</span> WeakRef<span class="synStatement">(</span>getTargetObject<span class="synStatement">(</span>source<span class="synStatement">))</span> expect<span class="synStatement">(</span>target.deref<span class="synStatement">())</span>.not.toUndefined<span class="synStatement">()</span> source <span class="synStatement">=</span> <span class="synType">null</span> <span class="synComment">// ガベージコレクションを強制実行する</span> <span class="synSpecial">global</span>.gc<span class="synStatement">()</span> expect<span class="synStatement">(</span>target.deref<span class="synStatement">())</span>.toUndefined<span class="synStatement">()</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> </pre> <p>package.json も以下のようにしておきます</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">scripts</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">test</span>&quot;: &quot;<span class="synConstant">node --expose-gc ./node_modules/.bin/vitest</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <p>こんな感じ. 結構簡単に書けました.</p> <h4 id="実際のコードはこちら">実際のコードはこちら</h4> <p>上記を踏まえて実際に GC をテストしているコードはこちらです.</p> <p>テストコードでは, json の変換処理が完了した後に, 使わなくなった変数がちゃんと開放されているかどうかをテストしています. <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhacomono-lib%2Fjson-origami%2Fblob%2Fmain%2Ftest%2Flib%2Fmodifier%2Fmemory.spec.ts" title="json-origami/test/lib/modifier/memory.spec.ts at main · hacomono-lib/json-origami" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hacomono-lib/json-origami/blob/main/test/lib/modifier/memory.spec.ts">github.com</a></cite></p> <h1 id="まとめ">まとめ</h1> <p>OSS 周りだと処理時間やメモリ効率のパフォーマンスが気になると思いますが,<br/> これをテストに書けているとだいぶうれしいのではないでしょうか.</p> <p>品質の高い OSS を今後も目指して作っていきます. <br> <br></p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています。 <br>採用情報や採用ウィッシュリストもぜひご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(プロダクトチーム向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech hacomono Rails プロダクトの改善の取り組みの話 hatenablog://entry/6801883189082716921 2024-02-13T11:00:00+09:00 2024-02-13T11:00:00+09:00 hacomono インテグレーションチームに所属している西脇です。社内では htz (ひつじ) と呼ばれており、インテグレーション領域の開発とチームマネージメントを行っています。 先月まではエンタープライズ開発チームとして活動していましたが、エンタープライズのお客様のご要望に多い主に API の公開や、システム連携の仕組みを開発する事が多い事から、今月よりインテグレーションチームとチーム名称を変更し活動をしています。 普段はインテグレーションチームでの開発とチームマネージメントも行いつつも、Rails や RSpec 周りの改善についても行っています。今回は現在 hacomono で抱えている… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240213/20240213013547.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>hacomono インテグレーションチームに所属している西脇です。社内では <a href="https://twitter.com/h2z">htz (ひつじ)</a> と呼ばれており、インテグレーション領域の開発とチームマネージメントを行っています。</p> <p>先月まではエンタープライズ開発チームとして活動していましたが、エンタープライズのお客様のご要望に多い主に API の公開や、システム連携の仕組みを開発する事が多い事から、今月よりインテグレーションチームとチーム名称を変更し活動をしています。</p> <p>普段はインテグレーションチームでの開発とチームマネージメントも行いつつも、Rails や RSpec 周りの改善についても行っています。今回は現在 hacomono で抱えている問題と、その問題についての取組について紹介したいと思います。 <br> <br></p> <h1 id="はじめに">はじめに</h1> <p>hacomono のバックエンドアプリケーションは Ruby on Rails で実装されており、テストは RSpec で記述されています。Rails のアプリケーションは API もしくはバッチ処理になっており、フロントエンドは持っておらず、テストは</p> <ul> <li>単体テスト</li> <li>リクエストテスト (内部結合テスト)</li> </ul> <p>の両軸で行われており、 E2E テスト (RSpec で言うところのシステムテスト) はフロントエンドアプリ側で行っています。</p> <p>全体的なテストとしてはかなり網羅的に記述されており、テストのカバレジッジとしては実に<span style="color: #d32f2f">『94%』</span>を超えており、この規模のアプリケーションとしては非常に高い数値となっており、開発者としてもお客様としても安心感が高くなっているかと思います。 <br> <br></p> <h1 id="課題">課題</h1> <p>前述の通りカバレッジを高く保つ事はできていますが、アプリケーションの拡大と共に様々な課題が見えてきました。</p> <h3 id="テストの記述方法に統一感やルールが無くメンテナンス性が悪い">テストの記述方法に統一感やルールが無く、メンテナンス性が悪い</h3> <p>[問題1] プロダクトも開発に携わるメンバーも増えたことの裏返しとして、それまで細かなルールやスタイルガイドが存在しなかったことも影響し、RSpecの記述方法がメンバーごとにばらばらで、テストデータの作成方法自体も統一感がない RSpec が増えています。</p> <p>[問題2] また、多くのテストをリクエストテストに頼り、API のエンドポイントレベルでのホワイトボックステストで記述されており、依存する各階層の細かな仕様の違いを全てリクエストテストとして記述をしているため <code>context</code> ブロックのネストが深く仕様把握にも仕様変更時にも大きなコストがかかっています。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink>context <span class="synSpecial">'</span><span class="synConstant">所属店舗、前回予約店舗、お気に入り店舗がある場合</span><span class="synSpecial">'</span> <span class="synStatement">do</span> context <span class="synSpecial">'</span><span class="synConstant">前回予約店舗は所属店舗と同じ店舗</span><span class="synSpecial">'</span> <span class="synStatement">do</span> context <span class="synSpecial">'</span><span class="synConstant">所属店舗、前回予約店舗、お気に入り店舗(すべて同じ店舗)がメンバーサイト非表示店舗の場合</span><span class="synSpecial">'</span> <span class="synStatement">do</span> it <span class="synSpecial">'</span><span class="synConstant">所属店舗、前回予約店舗が表示されない</span><span class="synSpecial">'</span> <span class="synStatement">do</span> </pre> <p>[問題3] テストデータの作成方法が、FactoryBot の利用、モデルから作成、Usecsse 層を呼び出して作成など、さまざまです。また、 <code>shared_context</code> を利用 (悪用) した example 側で直接把握できない大量の let が生成されるような書き方が常用されており、仕様の把握が困難になり、テストのパフォーマンスも悪化させています。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink>shared_context <span class="synConstant">:init_xxxxx_members</span> <span class="synStatement">do</span> let!(<span class="synConstant">:plan1_entity</span>) { ... } let!(<span class="synConstant">:plan2_entity</span>) { ... } ... let!(<span class="synConstant">:plan40_entity</span>) { ... } let!(<span class="synConstant">:member1_entity</span>) { ... } let!(<span class="synConstant">:member2_entity</span>) { ... } .... let!(<span class="synConstant">:member40_entity</span>) { ... } <span class="synStatement">end</span> </pre> <p>[問題4] 不安定なテスト (Flaky test) が頻繁に発生 (潜在) しており、Pull Request の CI においてテストが通らない事が多発しています。</p> <h3 id="全体の-Example-のうちリクエストテストが82以上を占めている">全体の Example のうちリクエストテストが<span style="color: #d32f2f">『82%』</span>以上を占めている</h3> <p>[問題5] 全体の Example 数としてはヒミツですが、そのうち 82% がリクエストテストで構成されています。ですので、単体テストとしてはわずか 18% 程度しか記述されていません。そのため、クラス単位での問題の発見性も悪く、分岐やメソッドがあらゆる条件下において網羅されていない可能性があります。<br/> また逆に、リクエストテストで重複した仕様をテストしている場合もあり、実装レイヤーの深い場所への仕様の追加の際には影響範囲の把握が難しく、リクエストテストでの条件 (<code>context</code>) の追加が難しい (漏れる) 場合が多発しています。 (問題2 にもつながっている)</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240213/20240213001057.png" width="1200" height="726" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>実際にリクエストテストの除いたテストカバレッジを計測すると<span style="color: #d32f2f">『65%』</span>程度にまで下がってしまっていました。Rubyのカバレッジ計測では、クラスやメソッドの定義部はそのクラスやメソッドがテストで呼び出されなくても実行時に評価されるため、何もテストを書いていなくても最低でも40%程度はあります。そのため、より単体テストの記述割合が少ないことがわかるかと思います。</p> <h3 id="RSpec-のみに費やす-CI-が並列実行を行っても40分近くかかってしまっている">RSpec のみに費やす CI が並列実行を行っても<span style="color: #d32f2f">『40分』</span>近くかかってしまっている</h3> <p>[問題6] 問題5 の Example 数が相当多いということと、問題3 のテストデータが無駄に作成されているということも有り、1 回の CI における RSpec の実行時間が約 5 時間、6 並列化することにより約 40 分もの GitHub Actions 時間を費やしてしまっています。<br/> プロダクトの成長とともに並列数を増やしていけば良い話ではあるが、問題を抱えた状態で札束で殴るだけの状態ではエンジニアとしてはあまりにも不幸な状態だと思っています。 <br> <br></p> <h1 id="解決策">解決策</h1> <h3 id="RSpec-コーディングガイドの作成">RSpec コーディングガイドの作成</h3> <p>プロダクト自体の規模も大きく関わる開発者の人数も多くなってきたため、細かな規約は Rubocop や CI で機械的にチェックできるようにしているため (問題2 の対応)、基本的な RSpec の記述方針に対して足並みを揃えることを目的として作成しました ( 問題1 への対応)。<br/> また、大きなパフォーマンス Issue が起きないように、これまで繰り返されてきた良くない記述方法についての禁止事項も hacomono 独自として記述しました。 (問題3 への対応) <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240213/20240213001141.png" width="1200" height="1132" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="不安定なテスト-Flaky-test-を見て見ぬふりをしない">不安定なテスト (Flaky test) を見て見ぬふりをしない</h3> <p>ボーイスカウトルールで自身の PR で落ちたテストは出来るだけ修正し、できる限り見て見ぬふりをしないようにしています。<br/> 自身で解決できない場合も、Slack で詳しそうな人や担当者へ共有をすることで必ず解決へ向けて前進させ、他の人が同じ問題で時間を無駄にしないようにしています。 (問題4 への対応)</p> <h3 id="単体テストとリクエストテストのカバレッジ計測を分ける">単体テストとリクエストテストのカバレッジ計測を分ける</h3> <p>リクエストテストによる全体的なカバレッジが上がっていることの幻を防ぐためと、単体テストによるテストカバレッジの向上のため、 hacomono ではカバレッジの計測を単体テストとリクエストテストに分け別々に計測するようにしました。 (問題5 への対応)</p> <ul> <li>単体テストのカバレッジ・・・コントローラーを除いたコードカバレッジ</li> <li>リクエストテストのカバレッジ・・・コントローラーのみのコードカバレッジ</li> </ul> <p>これにより、単体テストを記述することの重要性を伝えるとともに、これまで単体テストが足りていなかった部分が原因で起きていたインシデントを減らすことを期待しています。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240213/20240213001238.png" width="1200" height="613" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="parallel_test-の導入と既存-RSpec-のリファクタリング">parallel_test の導入と、既存 RSpec のリファクタリング</h3> <p>GitHub Actions による Matrix による RSpec の水平分割は行っていたが、parallel_test の利用はできていませんでした。<br/> CircleCI 時代に利用していた形跡があったため、GitHub Actions でも利用するような修正を行いました。 (問題6 への対応)</p> <p>また、単体テストの追加と同時にリクエストテストを削減したり、パフォーマンスの悪い RSpec のリファクタリングを行いこまめにパフォーマンスの改善を行いました。<br/> それにより Matrix による並列数を増やすこと無く、改善の開始当初より半分近く CI 待ち時間を短縮する事ができています。 (問題2, 6 への対応) <br> <br></p> <h1 id="まとめ">まとめ</h1> <p>いかがでしたでしょうか?現在、 hacomono で抱えている課題とそれに対する解決策についてご紹介させて頂きました。<br/> 少しずつではありますが課題の改善に向けて進めることができており、引き続き楽しみながら改善も進めていこうと思っています。 <br> <br></p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています。 <br>採用情報や採用ウィッシュリストもぜひご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(プロダクトチーム向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech (小ネタ) Vitest でパフォーマンス劣化を検知する hatenablog://entry/6801883189081407721 2024-02-08T11:00:00+09:00 2024-02-08T11:00:01+09:00 どうもみゅーとんです. 最近パフォーマンス周りで問題をおこしかけてしまったので, パフォーマンスの劣化を抑制する方法を調べてみました. 概要 3 行でまとめ public repository であれば, CodSpeed を無料で利用できる main ブランチでのパフォーマンスを計測しておき, Pull Request で劣化したら警告してくれる CodSpeed から, 内部処理を詳細に追うことができる 前提知識 vitest でパフォーマンステストを行う構成ができていることが条件になります. 導入方法についてはこの記事を確認してください. techblog.hacomono.jp Cod… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240208/20240208021746.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>どうも<a href="https://twitter.com/_mew_ton">みゅーとん</a>です.<br/> 最近パフォーマンス周りで問題をおこしかけてしまったので, パフォーマンスの劣化を抑制する方法を調べてみました. <br> <br></p> <h1 id="概要">概要</h1> <h4 id="3-行でまとめ">3 行でまとめ</h4> <ul> <li>public repository であれば, CodSpeed を無料で利用できる</li> <li>main ブランチでのパフォーマンスを計測しておき, Pull Request で劣化したら警告してくれる</li> <li>CodSpeed から, 内部処理を詳細に追うことができる</li> </ul> <h4 id="前提知識">前提知識</h4> <p>vitest でパフォーマンステストを行う構成ができていることが条件になります.<br/> 導入方法についてはこの記事を確認してください. <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftechblog.hacomono.jp%2Fentry%2F2024%2F01%2F30%2F1100" title="(小ネタ) Vitest でベンチマークテストする - hacomono TECH BLOG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://techblog.hacomono.jp/entry/2024/01/30/1100">techblog.hacomono.jp</a></cite> <br></p> <h1 id="CodSpeed-とは">CodSpeed とは</h1> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.codspeed.io%2F" title="🚀 Getting Started | CodSpeed Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.codspeed.io/">docs.codspeed.io</a></cite></p> <p>なんて読むんでしょうか・・?私はコードスピードと呼んでいますが, コッドスピードのほうが正しそう・・?</p> <p>GitHub Actions で実行した Python, Rust, JavaScript のベンチマークテスト結果を使って, パフォーマンスリグレッションテストを行ってくれる</p> <p>GitHub Repository と連携するとすぐに利用可能になり, JS の場合だと tinybench の実行結果を Actions から CodSpeed に送り, 詳細を表示してくれるようになります.</p> <p>PullRequest 作成時に main ブランチとの比較でパフォーマンス劣化 (デフォルトで 10% の劣化) があれば, 教えてくれる機能があり, 大幅なパフォーマンス劣化を検知できます.</p> <p>なんと, Public Repository では無料で利用できます.</p> <h4 id="導入してみた">導入してみた</h4> <p>今パフォーマンス周りで最も気になっているのは, hacomono-lib で OSS として公開している json-origami なので, これに導入してみました.</p> <p>通常は tinybench での実行に対して手を入れるようですが, vitest の benchmark test から tinybench を使っている場合は vitest 用のプラグインを導入すれば良いようです.</p> <p>vitest を利用している場合の専用のマニュアルが存在しました. <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.codspeed.io%2Fbenchmarks%2Fnodejs%2Fvitest" title="Vitest (recommended) | CodSpeed Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.codspeed.io/benchmarks/nodejs/vitest">docs.codspeed.io</a></cite></p> <h4 id="導入方法">導入方法</h4> <h5 id="vitest-の設定を追加">vitest の設定を追加</h5> <p><code>@codspeed/vitest-plugin</code> を導入し, vitest.config.ts にプラグインとして追加します.</p> <pre class="code bash" data-lang="bash" data-unlink>yarn add -D @codspeed/vitest-plugin</pre> <pre class="code lang-diff" data-lang="diff" data-unlink><span class="synIdentifier">+import codspeed from '@codspeed/vitest-plugin'</span> import { defineConfig } from 'vitest/config' export default defineConfig({ <span class="synIdentifier">+ plugins: [codspeed()],</span> test: { benchmark: { include: ['test/**/*.bench.(js|ts)'], }, }, }) </pre> <h5 id="GitHub-Actions-を設定">GitHub Actions を設定</h5> <p>main ブランチと, current Pull Request で実行したいので, 以下のように設定しました.</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Codspeed Benchmarks <span class="synIdentifier">permissions</span><span class="synSpecial">:</span> <span class="synIdentifier">contents</span><span class="synSpecial">:</span> read <span class="synIdentifier">packages</span><span class="synSpecial">:</span> read <span class="synIdentifier">pull-requests</span><span class="synSpecial">:</span> write <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">push</span><span class="synSpecial">:</span> <span class="synIdentifier">branches</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">&quot;main&quot;</span> <span class="synIdentifier">pull_request</span><span class="synSpecial">:</span> <span class="synIdentifier">types</span><span class="synSpecial">:</span> <span class="synSpecial">[</span>opened, synchronize, reopened, ready_for_review<span class="synSpecial">]</span> <span class="synIdentifier">paths</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">&quot;.github/workflows/benchmarks.yml&quot;</span> <span class="synStatement">- </span><span class="synConstant">&quot;src&quot;</span> <span class="synStatement">- </span><span class="synConstant">&quot;package.json&quot;</span> <span class="synStatement">- </span><span class="synConstant">&quot;vitest.config.ts&quot;</span> <span class="synStatement">- </span><span class="synConstant">&quot;yarn.lock&quot;</span> <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">init__node</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v4 <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/actions/init-node <span class="synIdentifier">benchmarks</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">needs</span><span class="synSpecial">:</span> <span class="synStatement">- </span>init__node <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v4 <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/actions/init-node <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Run benchmarks <span class="synIdentifier">uses</span><span class="synSpecial">:</span> CodSpeedHQ/action@v2 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">run</span><span class="synSpecial">:</span> yarn bench <span class="synIdentifier">token</span><span class="synSpecial">:</span> ${{ secrets.CODSPEED_TOKEN }} </pre> <h5 id="GitHub-側で-Repository-の設定">GitHub 側で Repository の設定</h5> <p>Pull Request 毎に必ず実行してほしいので, Branch Protection Rule で, main ブランチに以下の設定をします. <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240208/20240208010649.png" width="1200" height="536" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><code>Require status checks to pass before merging</code> にチェックを入れ, 下の <code>Status checks that are required.</code> で <code>CodSpeed Performance Analysis</code> を追加します.</p> <p>これで, パフォーマンスの劣化を検知したときにマージをブロックできるはずです.</p> <p>CodSpeed 側でも, Thresholdを設定しておきます. デフォルトは 10%. <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240208/20240208010701.png" width="1200" height="211" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><br></p> <h1 id="結果を見てみる">結果を見てみる</h1> <h4 id="main-ブランチの計測結果を-CodSpeed-上で見てみる">main ブランチの計測結果を CodSpeed 上で見てみる</h4> <p>こんな感じになりました. <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240208/20240208010625.png" width="1200" height="884" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これは main ブランチにあるベンチマークテストのテストケース毎の実行結果の一覧です.</p> <p>時間がかかったケースが画面上部にわかりやすく表示されてます. 面積が広ければ時間がかかってる意味かな?</p> <h4 id="パフォーマンスが劣化する-PR-を作ってみる">パフォーマンスが劣化する PR を作ってみる</h4> <p>わざとパフォーマンスが落ちるようにコードを書いて, エラーを起こさせてみます. <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240208/20240208010715.png" width="985" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんな感じのレポートが Pull Request にコメントされ, テストが通過しなくなりました.<br/> CodSpeed による通過が main ブランチへのマージ条件になっているので, これでマージを防ぐことができます.</p> <p>良きですね.</p> <p>CodSpeed 側の画面で見てみると, 劣化具合がしっかりレポートされています. <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240208/20240208010746.png" width="1200" height="765" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>詳細を開いてみると, 変更前と後で関数ごとにかかった時間割合まで見れます. これ無料ってヤバいですね. <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240208/20240208010802.png" width="1200" height="551" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><br></p> <h1 id="まとめ">まとめ</h1> <p>これでパフォーマンスの劣化の軽減と, 原因の検出がこれだけでだいぶスムーズにできるのではないでしょうか.</p> <p>とても良いものを見つけた!と思いました. 今後も活用していきたいです.おわり. <br></p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています。 <br>採用情報や採用ウィッシュリストもぜひご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(プロダクトチーム向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech YAPC::Hiroshima 2024にhacomonoがゴールドスポンサーとして協賛します! hatenablog://entry/6801883189081055129 2024-02-07T11:00:00+09:00 2024-02-07T11:49:55+09:00 こんにちは! 株式会社hacomonoの Engineering Office でアシスタントをしているみーこです。 今週末2月10日に開催されるオフラインイベント、YAPC::Hiroshima 2024についてのお知らせです。 YAPC::Hiroshima 2024とは? YAPCはYet Another Perl Conferenceの略で、Perlを軸としたITに関わる全ての人のためのカンファレンスです。 Perlだけにとどまらない技術者たちが、好きな技術の話をし交流するカンファレンスで、技術者であれば誰でも楽しめるお祭りです! 今回のYAPC::Hiroshima 2024は「wh… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240206/20240206183655.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは!<br/> 株式会社hacomonoの Engineering Office でアシスタントをしているみーこです。 <br></p> <p>今週末2月10日に開催されるオフラインイベント、YAPC::Hiroshima 2024についてのお知らせです。 <br> <br></p> <h1 id="YAPCHiroshima-2024とは">YAPC::Hiroshima 2024とは?</h1> <p>YAPCはYet Another Perl Conferenceの略で、Perlを軸としたITに関わる全ての人のためのカンファレンスです。<br/> Perlだけにとどまらない技術者たちが、好きな技術の話をし交流するカンファレンスで、技術者であれば誰でも楽しめるお祭りです!</p> <p>今回のYAPC::Hiroshima 2024は「what you like」がテーマです。<br/> 職種、ロール、プログラミング言語、技術要素... あなたのさまざまな「お好み」を語ってみてください。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fyapcjapan.org%2F2024hiroshima%2F" title="YAPC::Hiroshima 2024 " class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://yapcjapan.org/2024hiroshima/">yapcjapan.org</a></cite></p> <h5 id="イベント概要">イベント概要</h5> <p>日時:2024年2月10日(土)<br/> 開催方法:オフライン<br/> 会場:広島国際会議場 <br> <br></p> <h1 id="スポンサー概要登壇">スポンサー概要・登壇</h1> <p>hacomonoはゴールドスポンサーとして初参加させていただきます!<br></p> <p>また「広島名物ランチセッションスポンサー」として、13時からのランチセッションにCTOの<a href="https://twitter.com/macococo">@macococo</a>(工藤)が登壇いたします。<br/> お話するのは…最近よく耳にするようになってきた「プロダクトエンジニア」というキャリアについて。 <br> <figure class="figure-image figure-image-fotolife" title="ウルトラジャンプのフォントがかわいい"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240206/20240206202129.png" width="1200" height="668" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ウルトラジャンプのフォントがかわいい</figcaption></figure></p> <p>現在絶賛準備中ですが、ものすごく面白い内容になりそう。必聴です! <br> <br></p> <h1 id="ブース出展">ブース出展</h1> <p>ブースも出展しますよー!hacomonoのブースの場所はこちらです。 ぜひお立ち寄りください。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240206/20240206183614.png" width="1200" height="902" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><br></p> <h1 id="参加メンバー">参加メンバー</h1> <p>hacomonoからは以下の7メンバーが参加予定です!</p> <p>CTOの <a href="https://twitter.com/macococo">@macococo</a> 、プラットフォームチームのアーキテクト <a href="https://twitter.com/Maco_Tasu">@macotasu</a> 、プロダクトエンジニアの <a href="https://twitter.com/koji_kono">@kojikoji</a> と@renren 、SRE / IoTチームのヤギマスター <a href="https://www.wantedly.com/companies/hacomono/post_articles/410101">@shigu</a>、 それからブース担当のEngineering Office <a href="https://twitter.com/rh1011_">@rh1011_</a> とHR <a href="https://twitter.com/nanchina_nano">@nanchina_nano</a>です!</p> <p>ぜひぜひ、遊びに来てくださいね🙌🏻<br/> みなさんと会場でお会いできることを楽しみにしています!</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています。 <br>採用情報や採用ウィッシュリストもぜひご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(プロダクトチーム向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech 開発のストーリーを語る PullRequest をつくる ~ レビューしやすい PR を目指して ~ hatenablog://entry/6801883189080901507 2024-02-06T11:00:00+09:00 2024-02-06T11:00:03+09:00 こんにちは。 開発本部 フィーチャー部 会計・決済グループでエンジニアをやっている ぺい です。 みなさん、PullRequest 作っていますか? 本記事では、私が普段 PullRequest を作る中で、レビュアーにとってレビューしやすく読みやすくするために意識しているコミットの積み方の工夫についてお話しします。 ※ 私の主観が多く入っている点について、予めご了承ください。 前提 以下を、本記事の前提条件とします。 GitHub を利用している PullRequest は最終的に Squash and Merge でマージ先のブランチに取り込まれる 実装者の開発ストーリーを追体験できるよう… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240206/20240206005145.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは。<br/> 開発本部 フィーチャー部 会計・決済グループでエンジニアをやっている ぺい です。</p> <p>みなさん、PullRequest 作っていますか?</p> <p>本記事では、私が普段 PullRequest を作る中で、レビュアーにとってレビューしやすく読みやすくするために意識しているコミットの積み方の工夫についてお話しします。</p> <p>※ 私の主観が多く入っている点について、予めご了承ください。 <br> <br></p> <h1 id="前提">前提</h1> <p>以下を、本記事の前提条件とします。</p> <ul> <li>GitHub を利用している</li> <li>PullRequest は最終的に <code>Squash and Merge</code> でマージ先のブランチに取り込まれる <br> <br></li> </ul> <h1 id="実装者の開発ストーリーを追体験できるようなコミットを積み重ねる">実装者の開発ストーリーを追体験できるようなコミットを積み重ねる</h1> <p>例として、Ruby on Rails を使って新しく API を追加するケースを考えてみます。User モデルがあるとして User を新規作成する API を実装する、としましょう。</p> <p>極端な例ですが、この変更内容に対して以下のように 1commit にすべての変更が詰め込まれていたとします。</p> <pre class="code" data-lang="" data-unlink>- User を新規作成する API を追加 (+300)</pre> <p>この場合、レビュアーは <code>Files changed</code> から300行の変更を自ら紐解いていかなければなりません。これは、あまりレビュアーに優しい状態とは言えないでしょう。</p> <p>一方、以下のようにコミットが分かれていたらどうでしょうか (上から下に流れるものとします)。</p> <pre class="code" data-lang="" data-unlink>- users テーブルを追加するマイグレーションの追加 (+20) - bin/rails db:migrate を実行 (+30) - ルーティングの定義を追加 (+1) - User の作成処理を実装 (+200) - users_controller を実装 (+49)</pre> <p>このような粒度でコミットが作られている場合、レビュアーはコミットを順番に読んでいくことで、まるで実装者の作業を追体験しているかのように PullRequest の内容を読み解いていけると思います。</p> <p>この例では省略していますが、コミットログに付加的な情報が添えられていれば開発時に考えたことを残しておけば、より鮮明に見えてくるでしょう。</p> <p>※ GitHub の場合、PullRequest の <code>Commits</code> タブを開き、適当なコミットの詳細ページに進んだら「&lt; Prev」と「Next >」ボタンを使って前後の Commit に移動できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240206/20240206000305.png" width="1004" height="416" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>我々開発者は、大きな課題に直面したとき、よく問題を小さな粒度に分解してから1つずつ取り掛かっていくと思います。</p> <p>PullRequest のレビューにおいても同じで、修正内容全体としては大きかったとしても、それを構成するコミットがいい感じに分解されていれば、実装者の作業の流れや意図がわかりやすくなり、レビューする側のコストは下がっていくと考えています。</p> <p>書籍において、著者の意図や考えが理解できる・想像できるときにスラスラ読んでいけると思いますが、それに近い感覚です。</p> <p>また、この方法の良いところは、しっかりとレビューすべきポイントとそうでないところを取捨選択しやすくなるという点もあります。</p> <p>上記の例で言うと <code>bin/rails db:migrate を実行</code> のコミットについては自動生成された diff となるため、その前のコミットが問題無ければじっくり見る必要性は薄いでしょう。</p> <p>一方で <code>User の作成処理を実装</code> の部分は diff も大きく、また注目すべきポイントであることが見て取れるはずです。 <br> <br></p> <h1 id="知っていると便利な-git-のテクニック3選">知っていると便利な git のテクニック3選</h1> <p>今回例にあげたようなわかりやすい開発作業であれば特別なテクニックは不要ですが、現実的には当初思っていたように作業が進まないことも多いでしょう。</p> <p>うっかり DB の制約を追加するのを忘れていたり、リンター・フォーマッターの実行を忘れていたり等、作業を進めている中で後から気がつくことも多いと思います。</p> <p>そのようなコミットは数が少なければそれはそれでリアルな作業が追体験できて良いと思うのですが、数が増えてくると整理したくなってきます。そんなときに、私がよく使う3つのgitコマンドについて紹介します。</p> <h2 id="1つ前のコミットに混ぜ込む--git-commit-amend">1つ前のコミットに混ぜ込む : <code>git commit —amend</code></h2> <p>直前のコミットに追加し忘れていた修正がある場合、</p> <pre class="code" data-lang="" data-unlink>$ git add &lt;対象のファイルパス&gt; $ git commit --amend</pre> <p>で混ぜ込むことができます。</p> <p>この機能はシンプルでわかりやすく使っている方も多いのではないかと思います。</p> <h2 id="1つ以上前のコミットに混ぜ込む--git-commit-fixup">1つ以上前のコミットに混ぜ込む : <code>git commit —fixup</code></h2> <p>直前ではないコミットに追加し忘れていた修正がある場合は <code>git commit —fixup</code> が使えます。この機能はざっくり2ステップに分かれます。</p> <p>まず <code>git commit —fixup</code> で混ぜ込みたいコミットのハッシュを渡します。</p> <pre class="code" data-lang="" data-unlink>$ git commit --fixup &lt;混ぜたいコミットのハッシュ値&gt;</pre> <p>すると <code>fixup! &lt;混ぜたいコミットのコミットメッセージ&gt;</code> というメッセージの新しいコミットが作られます。この時点ではただコミットが増えただけの状態です。</p> <p>この後に <code>git rebase -i —autosquash &lt;混ぜたいコミットの1つ前のハッシュ&gt;</code> を実行すると次のような画面でエディタが立ち上がります。</p> <pre class="code" data-lang="" data-unlink>pick 783e434 ABC fixup 676869d fixup! ABC pick 07b47f5 DEF</pre> <p>お使いのエディタが Vim であれば <code>:wq</code> などで保存して終了すれば完了です。</p> <h2 id="色々な改変ができる--git-rebase--i">色々な改変ができる : <code>git rebase -i</code></h2> <p>単に既存コミットに混ぜ込みたい以上にコミット履歴を変更したい場合は、</p> <pre class="code" data-lang="" data-unlink>$ git rebase -i &lt;変更したいコミットの1つ前のハッシュ&gt;</pre> <p>を実行します。この機能の詳細な説明は省略しますが、自分がよく使う場面としては</p> <ul> <li>既存のコミットのメッセージを変更したいとき</li> <li>コミットの順番を変更したいとき</li> <li>既存の A と B のコミットを1つにしたいとき</li> </ul> <p>などです。</p> <p>機能が豊富なので、まずは自分の置かれた状況で必要になったケースの利用方法を1つずつ覚えていくのが良いのではないかと思います。 <br> <br></p> <h1 id="おわりに">おわりに</h1> <p>以上、私が PullRequest を作るときにひっそりと行っているちょっとしたワークフローのご紹介でした。</p> <p>これは、自分自身が過去にレビュアーとしてレビューをしたときに読みやすい PullRequest に出会い、そこで感銘を受けてそのときから実践しているものでした。</p> <p>レビューしやすい PR を作ることに関心がある、どなたかの参考となれば幸いです。</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています。 <br>採用情報や採用ウィッシュリストもぜひご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(プロダクトチーム向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech (小ネタ) Vitest でベンチマークテストする hatenablog://entry/6801883189079046154 2024-01-30T11:00:00+09:00 2024-01-30T11:00:00+09:00 どうもみゅーとんです. 小ネタです. hacomonoは GitHub の hacomono-lib という organization にて, OSS を公開しているのですが, なんとなく作っているロジックが多重ループになっていて計算量が多そうだ, と思い, そのパフォーマンス計測を行うことにしました. 概要 3 行でまとめ Vitest には標準でベンチマークテストを行う仕組みがある ただし experimental (v1.2.x 時点) ベンチマークテストには tinybench を利用している ここで話題にしないこと 計算量の概念 パフォーマンス改善のためのコーディング 背景 インシデ… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240130/20240130002201.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>どうも<a href="https://twitter.com/_mew_ton">みゅーとん</a>です.</p> <p>小ネタです.</p> <p>hacomonoは GitHub の <a href="https://github.com/hacomono-lib">hacomono-lib</a> という organization にて, OSS を公開しているのですが,<br/> なんとなく作っているロジックが多重ループになっていて計算量が多そうだ, と思い, そのパフォーマンス計測を行うことにしました. <br> <br></p> <h1 id="概要">概要</h1> <h2 id="3-行でまとめ">3 行でまとめ</h2> <ul> <li>Vitest には標準でベンチマークテストを行う仕組みがある</li> <li>ただし experimental (<code>v1.2.x</code> 時点)</li> <li>ベンチマークテストには <code>tinybench</code> を利用している</li> </ul> <h2 id="ここで話題にしないこと">ここで話題にしないこと</h2> <ul> <li>計算量の概念</li> <li>パフォーマンス改善のためのコーディング</li> </ul> <p><br></p> <h1 id="背景">背景</h1> <p>インシデント未遂を引き起こしてしまいました.</p> <p>hacomono-lib で公開している OSS の <code>json-origami</code> にて, パッチバージョンアップとして, たった1行変更するだけのバグ修正を行いました.</p> <p>この修正が計算量を膨大に引き上げてしまい, プロダクトのコードでパフォーマンスが大幅劣化してしまいました.</p> <p>暫定対処として, 利用する <code>json-origami</code> のバージョンを下げるなどの対応はしました.</p> <p>恒久対処をするためには, 速度が改善されたかどうかを計測する仕組みが前提として必要でした.<br></p> <p><br></p> <h1 id="実践">実践</h1> <h2 id="Vitest-でパフォーマンステストを書く">Vitest でパフォーマンステストを書く</h2> <p>まず, <code>vitest.config.ts</code> に以下のような修正を加えます.</p> <p>今回は <code>test/*</code> フォルダ配下にある <code>*.bench.ts</code> ファイルをベンチマークテストを書くファイルとしています.</p> <pre class="code lang-sh" data-lang="sh" data-unlink>import <span class="synSpecial">{</span> defineConfig <span class="synSpecial">}</span> from <span class="synStatement">'</span><span class="synConstant">vitest/config</span><span class="synStatement">'</span> <span class="synStatement">export</span> default defineConfig<span class="synPreProc">(</span><span class="synSpecial">{</span> <span class="synSpecial"> test: {</span> <span class="synSpecial"> // これを追加</span> <span class="synSpecial"> benchmark: {</span> <span class="synSpecial"> include: [</span><span class="synStatement">'</span><span class="synConstant">test/**/*.bench.(js|ts)</span><span class="synStatement">'</span><span class="synSpecial">]</span> <span class="synSpecial"> }</span> <span class="synSpecial"> },</span> <span class="synSpecial">}</span><span class="synPreProc">)</span> </pre> <p>ベンチマークテストとして, 以下のようなコードを書きます.</p> <pre class="code lang-sh" data-lang="sh" data-unlink>import <span class="synSpecial">{</span> bench, describe <span class="synSpecial">}</span> from <span class="synStatement">'</span><span class="synConstant">vitest</span><span class="synStatement">'</span> import <span class="synSpecial">{</span> createObject <span class="synSpecial">}</span> from <span class="synStatement">'</span><span class="synConstant">./test/utils</span><span class="synStatement">'</span> import <span class="synSpecial">{</span> modifyObject <span class="synSpecial">}</span> from <span class="synStatement">'</span><span class="synConstant">./src/modifyObject</span><span class="synStatement">'</span> describe<span class="synPreProc">(</span><span class="synStatement">'</span><span class="synConstant">benchmark test: &quot;modifyObject&quot;</span><span class="synStatement">'</span><span class="synSpecial">, </span><span class="synPreProc">()</span><span class="synSpecial"> </span><span class="synStatement">=&gt;</span><span class="synSpecial"> {</span> <span class="synSpecial"> // ベンチマークテスト対象外にしたいコードは外に書く</span> <span class="synSpecial"> // createObject は例として </span><span class="synConstant">500</span><span class="synSpecial"> 個値のある json object 作る関数として想定している.</span> <span class="synSpecial"> const object500 </span><span class="synStatement">=</span><span class="synSpecial"> createObject</span><span class="synPreProc">(</span><span class="synConstant">500</span><span class="synPreProc">)</span> <span class="synSpecial"> bench</span><span class="synPreProc">(</span><span class="synStatement">'</span><span class="synConstant">when use 500 size object</span><span class="synStatement">'</span><span class="synSpecial">, </span><span class="synPreProc">()</span><span class="synSpecial"> </span><span class="synStatement">=&gt;</span><span class="synSpecial"> {</span> <span class="synSpecial"> modifyObject</span><span class="synPreProc">(</span><span class="synSpecial">object500</span><span class="synPreProc">)</span> <span class="synSpecial"> }</span><span class="synPreProc">)</span> <span class="synSpecial">}</span><span class="synPreProc">)</span> </pre> <p>Vitest の通常のテストと同時に実行できないため, 以下のコマンドを package.json に追加しておくとよいです.</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">scripts</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">bench</span>&quot;: &quot;<span class="synConstant">vitest bench</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <p><br></p> <h2 id="仕組み">仕組み</h2> <p>vitest (v1.2時点) では内部で tinybench が使われており, bench 関数の第2 引数の関数を対象に, tinybench でパフォーマンス計測を行っています.</p> <p>※ experimental なため, 変わる可能性があります.</p> <p>上記のコードでは省略していますが, bench 関数は第 3 引数で tinybench のオプションを指定できるようになってます. 詳細は以下のとおり.</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftinylibs%2Ftinybench%3Ftab%3Dreadme-ov-file%23bench" title="GitHub - tinylibs/tinybench: 🔎 A simple, tiny and lightweight benchmarking library!" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tinylibs/tinybench?tab=readme-ov-file#bench">github.com</a></cite></p> <p>基本的には, ベンチマークテストしたい最小限のコードを第 2 引数にすれば良さそうです.</p> <p>上記のコードだと, テストに関係ない “モックデータを作る処理” がbench の外に記載してあることで, 余計な処理をテスト対象に含めないようにしてます.</p> <p>第 3 引数には <code>time (default 500)</code> <code>iterations (default 10)</code> の 2 パラメータがあり, テスト開始から合計 <code>time</code> ms 経過するまでは何度も実行し計測しますが, この時間を超えて <code>iterations</code> 回数に満たない場合は, 時間超過しつつ必ず <code>iterations</code> 回実施するようになっています.</p> <h2 id="結果の読み方">結果の読み方</h2> <p>以下は <code>json-origami</code> でパフォーマンステストを実施した結果の一部です</p> <pre class="code" data-lang="" data-unlink>✓ test/twist.bench.ts (6) 1840ms ✓ twist with light object (3) 1836ms name hz min max mean p75 p99 p995 p999 rme samples · twist (complex object including 100 values, twist 10% of keys) 264.30 0.8337 13.3483 3.7835 4.3818 10.4968 13.3483 13.3483 ±7.74% 133 fastest · twist (complex object including 100 values, twist 50% of keys) 215.81 1.3680 10.4523 4.6338 5.3692 9.8662 10.4523 10.4523 ±6.44% 108 · twist (complex object including 100 values, twist 90% of keys) 162.60 2.9027 10.6133 6.1502 6.9685 10.6133 10.6133 10.6133 ±4.95% 82 slowest</pre> <p>ちょっと横に長いですが, それぞれ項目の意味は以下の通り</p> <ul> <li>hz .. 1 秒間にこの処理を実行できる回数</li> <li>min .. 計測したなかで最短だった時間 (ms)</li> <li>max .. 計測したなかで最長だった時間 (ms)</li> <li>mean .. 標本平均 (ms)</li> <li>p75 .. 75 パーセンタイル (ms)</li> <li>p99 .. 99 パーセンタイル (ms)</li> <li>p995 .. 99.5 パーセンタイル (ms)</li> <li>p999 .. 99.9 パーセンタイル (ms)</li> <li>rme .. 相対誤差 (%)</li> <li>samples .. 標本数</li> </ul> <h3 id="hz">hz?</h3> <p>周波数の Hz と同じ意味合いだと思います. 数が多いほど同じ時間での実行可能数が多いことを意味します.</p> <h3 id="標本平均">標本平均?</h3> <p>一般的に平均と呼ばれたりしますが, 統計学上では標本平均と呼ばれたりします.<br/> 全標本を足し合わせて, 標本数で割った数です.</p> <h3 id="パーセンタイル">パーセンタイル?</h3> <p>小さい方から数えて指定 % の位置にある標本の値を指しています.</p> <p>ベンチマークテストの結果においては, 例えば <code>p75</code> であれば, 実行時の 75% はその値以下の時間で処理が完了する意味合いと捉えてよさそうです.</p> <p>上記の実施結果の例をみると <code>p75</code> <code>p99</code> と差が開いていて, 結果が分散している様子が伺えるかと思います. 入力するオブジェクトをテスト毎に変更しているため, 分散する結果になったのかなと思っています.</p> <p> (後のリファクタリングで安定化しました)</p> <h3 id="相対誤差">相対誤差?</h3> <p>一般的には <code>(実測値 - 理論値) / 理論値</code> で表される, 理論値に対する誤差の割合ですが, tinybench においては理論値として標本平均が使われます.</p> <p>つまり, 平均値に対してどれだけ結果に誤差が生じるかが得られており, いいかえると計測時間がどれだけブレるかがわかります. 少ないほど <em>どんな入力値に対しても近い実行時間が得られる</em> ことがわかります.</p> <h3 id="標本数">標本数?</h3> <p>実行回数です.</p> <p>指定した <code>time</code> (ms) の時間内に実行した回数が記載されます.</p> <p><code>time</code> を超えても <code>iterations</code> 回数を実施できなかった場合は, 時間超過して <code>iterations</code> 回数だけは必ず実施します. そのため, 最低値は <code>iterations</code> と同じです.</p> <p>高ければ高いほど高い精度の結果が得られます. <br> <br></p> <h1 id="まとめ">まとめ</h1> <p>案外, しっかり理解すると, パフォーマンス改善に役立てそうです.</p> <p>今回は プロダクトのコードを切り出した OSS でのパフォーマンス検証であるため, <br/> そもそも切り出されたロジックの検証であって, 導入しやすかったかなと思います.</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています。 <br>採用情報や採用ウィッシュリストもぜひご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(プロダクトチーム向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech hacomonoでインターンを始めてから今までの振り返り hatenablog://entry/6801883189077100405 2024-01-23T11:00:00+09:00 2024-01-23T11:00:00+09:00 はじめまして!hacomono インターン生の、インフラとウマと馬が大好きなかわにーと申します。お馬さんかわいい。 2022年6月に入社してから2年弱経ち、このタイミングでやってきたことの整理として振り返り記事を書いています。 hacomono のインターンに興味を持っていただける方の参考となれば幸いです。 (※編集部注:2024年1月現在、hacomono開発チームのインターンは募集停止しております。再開次第、hacomonoプロダクト公式Xなどでお知らせいたします!) hacomono に応募したきっかけ 大学に入学してから今まで、気の赴くままに趣味として低レイヤやインフラ技術を学んでいま… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240122/20240122143802.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>はじめまして!hacomono インターン生の、インフラとウマと馬が大好きなかわにーと申します。お馬さんかわいい。</p> <p>2022年6月に入社してから2年弱経ち、このタイミングでやってきたことの整理として振り返り記事を書いています。</p> <p>hacomono のインターンに興味を持っていただける方の参考となれば幸いです。<br/> (※編集部注:2024年1月現在、hacomono開発チームのインターンは募集停止しております。再開次第、<a href="https://twitter.com/hacomono_Dev">hacomonoプロダクト公式X</a>などでお知らせいたします!) <br> <br></p> <h2 id="hacomono-に応募したきっかけ">hacomono に応募したきっかけ</h2> <hr /> <p>大学に入学してから今まで、気の赴くままに趣味として低レイヤやインフラ技術を学んでいました。何か役に立つようなプロダクトを作るわけでもなく、ただ自分の好奇心に従って。</p> <p>しかし、大学院修士1年の頃、早期選考、インターン….、就活早期化の波が押し寄せます。</p> <p>趣味ばかりで実務経験のない私は、つよつよ学生エンジニアが跋扈する就活戦争で戦っていけるのだろうかと思い、少しでも経験を積もうと考え、Wantedly でインターンを探していました。</p> <p>できれば自分の好きな分野を伸ばしたく、インフラ、なんならクラウドを触れるインターンはないだろうかと探していたところ、この募集を見つけました(<strong>現在は終了しています</strong>)。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fprojects%2F870227" title=" 学生歓迎!SaaSスタートアップでのクラウドエンジニア募集 - 株式会社hacomonoのWebエンジニアの採用 - Wantedly" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.wantedly.com/projects/870227">www.wantedly.com</a></cite></p> <p>パブリッククラウドを扱えて、IaC も実運用環境でできる!最高じゃないか!ということで早速応募し、今に至ります。<br/> <br> <br></p> <h2 id="hacomonoでやってきたこと">hacomonoでやってきたこと</h2> <hr /> <p>入社から今までの2年弱でやってきたことは、順に</p> <ol> <li>監視基盤のマネージド & IaC 化</li> <li>勤怠打刻 CLI アプリの作成</li> <li>git-flow を GitHub Actions で制御</li> <li>BigQuery のクエリ作成</li> <li>PRごとの検証環境作成 ( playground )</li> <li>Istio を使った Service Mesh の検証</li> </ol> <p>になります。</p> <p>改めて並べてみると、幅広い経験をさせてもらってますね…。</p> <p>この中で特に印象深かった、<br/> <strong>1. 監視基盤のマネージド &amp; IaC 化</strong><br/> <strong>5. PRごとの検証環境作成</strong> <br/> を振り返りたいと思います。</p> <h3 id="監視基盤のマネージド--IaC化">監視基盤のマネージド &amp; IaC化</h3> <p>hacomono に入社して初めて割り振られたタスクがこれです。</p> <p>当時の hacomono では、お客様の環境ごとに個別の環境を立ち上げるシングルテナントが大多数を占めていました。膨大な環境を一括で監視するために、Grafana と Prometheus を使い CPU、メモリ使用率や、Nginx 関連のメトリクスを取得していました。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240122/20240122135448.png" width="662" height="391" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>しかし、Grafana &amp; Prometheusを 単一の ECS クラスタ上で動作させていたため、クエリ操作とPull操作を1つのコンテナで行わなければならず、高い負荷が掛かります。</p> <p>それによりダッシュボードを開くだけでも時間が掛かってしまい、利便性を損ねていました。</p> <p>また、Prometheus が取得したメトリクスはすべてローカルに書き込まれていたため、データが吹き飛ぶリスクもあります(EFSがマウントされていたのでよっぽど大丈夫だとは思いますが)。</p> <p>これを解消するため、Prometheus &amp; Grafana のマネージド化を行いました。</p> <p><strong><a href="https://aws.amazon.com/jp/prometheus/">Amazon Managed Service for Prometheus</a>(AMP)</strong>は <strong>メトリクスの保存先を提供</strong>するマネージドサービスです。あくまでも保存先であって、AMPはメトリクスの収集を行わないため、メトリクスを書き込む Agent を用意する必要があります。</p> <p>AMP はメトリクスを 150日間保存してくれるほか、書き込みが多くなった場合は自動でスケールしてくれます。</p> <p><strong><a href="https://aws.amazon.com/jp/grafana/">Amazon Managed Service for Grafana</a>(AMG)</strong> は Grafana のマネージドサービスで、インフラを全く意識することなく通常の Grafana と同じように使うことができます。</p> <p>Cloud Watch 等もデータソースに選ぶこともでき、1つのダッシュボードにまとめられます。</p> <p>ここまでを踏まえ、下記のシステムを構築しました。</p> <ol> <li>Prometheus Agent が、Service Discovery を使って検知した App のメトリクスを取得</li> <li>取得したメトリクスは一時的に Prometheus Agent に保存され、順に AMP に書き込まれる</li> <li>AMG は ダッシュボードが開かれると、AMP に対しクエリを発行し、メトリクスの可視化を行う</li> </ol> <p>※ Cloud Watch に関しては、AMG から直接アクセスされます。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240122/20240122135425.png" width="752" height="512" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>この環境を Terraform を使って構築しました。</p> <p>Terraform も AWS も初めての経験だったため、苦労しながらもなんとか構築することができました。</p> <p>ここで得た知識(特に IAM 周り)が、今のベースになっています。</p> <h3 id="PRごとの検証環境作成">PRごとの検証環境作成</h3> <p>2023年の頭くらいに担当したタスクです。</p> <p>hacomono では、特定 PR を検証するためには、社内で既に立ち上がっている環境にブランチを手動で当てる必要がありました。</p> <p>ローカルで1人で開発する場合は問題ありませんが、PdM や QA、デザイナーの方々が先行環境を気軽に触ることができず、手間になります。</p> <p>そこで、EKS (on Fargate) を使い、PR ごとの検証環境を気軽に立ち上げられるシステムを構築しました。</p> <ol> <li>PR を作成した Branch を指定し、GitHub Actions の workflow を起動</li> <li>workflow が Docker イメージの Build を行い、ECR に Push する</li> <li>workflow が EKS に対し、テンプレートに環境独自の情報を付加した K8s manifest を Apply する</li> <li>PR のコメントに URL が通知されるので、利用者はそれにアクセスする</li> </ol> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240122/20240122135500.jpg" width="1200" height="603" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これで検証環境自体は気軽に立てられるようになったのですが、社内には人的リソース含め K8s の保守に割けるリソースがあまりなく、クラスタやアドオンのアップデートが大きな負担になっています。</p> <p>システムの設計時、Code Deploy や ECS、EKS 等様々な方法を検討しましたが、保守性がきちんと詰められていませんでした。</p> <p>失敗からの教訓にはなりますが、インターンを通じてシステム設計において考慮すべきことを身をもって体感できたと思います。</p> <p>本番環境でないからこそ、インターンだからこそできる失敗でした。(社員の皆さん本当にありがとうございます)</p> <p>現在は別の検証環境との繋ぎこみを行っている関係で、システムを丸ごと変更するのは難しくなってしまいましたが、少しでも運用負担を下げるためにドキュメントの整備や、自動化できるところは自動化していきたいと考えています。 <br> <br></p> <h2 id="おわりに">おわりに</h2> <hr /> <p>長くなりましたが、ここまでお読みいただきありがとうございました。</p> <p>hacomono は今までの経験関係なく、様々なタスクを任せていただける環境です。<br/> またとにかく物腰の柔らかい優しい方が多く、気持ちよく働くことができます。<br/> 私は、入社当初ベンチャーはイケイケでなければならないと恐れ慄いていましたが、いい意味でイメージが変わりました。</p> <p>私は4月でインターンを離れ、別の企業に就職する予定ではありますが、副業で hacomonoインターン の方々とは関わらせていただく予定なので、もしこの記事を読んでインターンに応募してくださる方がいればぜひ話しましょう!</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech 多言語対応はキーを原文にしたら見通しが良くなった hatenablog://entry/6801883189075223998 2024-01-16T11:00:00+09:00 2024-01-16T11:00:01+09:00 あけおめ一発目です. フロントエンドテックリードやってるみゅーとんです. 年末年始はひたすら Unity と Blender で遊んでました. 弊プロジェクトのフロントエンドではもともと vue-i18n を導入しており, 後に多言語対応をサポートできるようにしていました. これを, サポートできるように見直そうとしたところだったのですが, 課題が多く見つかり, 書き直すことになりました. その際に見通しのよい書き方に直せたので, これを共有します. TL;DR 文言を引き当てるキーには, 表示する文言を設定すること なるべく 1文まとめて文言を作ること コーディング時に, そこに何が書いてあ… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20240115/20240115152220.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>あけおめ一発目です. フロントエンドテックリードやってる<a href="https://twitter.com/_mew_ton">みゅーとん</a>です.<br/> 年末年始はひたすら Unity と Blender で遊んでました.</p> <p>弊プロジェクトのフロントエンドではもともと vue-i18n を導入しており, 後に多言語対応をサポートできるようにしていました.</p> <p>これを, サポートできるように見直そうとしたところだったのですが, 課題が多く見つかり, 書き直すことになりました. その際に見通しのよい書き方に直せたので, これを共有します. <br> <br></p> <h1 id="TLDR">TL;DR</h1> <ul> <li>文言を引き当てるキーには, 表示する文言を設定すること</li> <li>なるべく 1文まとめて文言を作ること</li> <li>コーディング時に, そこに何が書いてあるかわかりやすくなった</li> </ul> <h2 id="前提">前提</h2> <ul> <li>vue-i18n, nuxt-i18n の使い方は解説しません.</li> <li>同様の仕組みであれば真似できる話です.</li> </ul> <h1 id="背景">背景</h1> <p>元々以下のように単語ごとに翻訳し, 文字列結合するようなロジックになっていました.</p> <p>(極端な例なので, 実際はここまで酷くはないです)</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synError">// 言語リソース</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">label</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">this_month</span>&quot;: &quot;<span class="synConstant">今月</span>&quot;, &quot;<span class="synStatement">today</span>&quot;: &quot;<span class="synConstant">本日</span>&quot;, &quot;<span class="synStatement">more</span>&quot;: &quot;<span class="synConstant">あと</span>&quot;, &quot;<span class="synStatement">times</span>&quot;: &quot;<span class="synConstant">回</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">&lt;</span>template<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>p<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>template v-if<span class="synStatement">=&quot;</span><span class="synConstant">remainingType === 'thisMonth'</span><span class="synStatement">&quot;&gt;</span> <span class="synStatement">&lt;</span>!-- 今月あと N 回 <span class="synSpecial">--&gt;</span> <span class="synSpecial">{{</span> <span class="synPreProc">$t(</span><span class="synStatement">'</span><span class="synConstant">label.this_month</span><span class="synStatement">'</span><span class="synPreProc">)</span> <span class="synSpecial">}}</span> <span class="synSpecial">{{</span> <span class="synPreProc">$t(</span><span class="synStatement">'</span><span class="synConstant">label.more</span><span class="synStatement">'</span><span class="synPreProc">)</span> <span class="synSpecial">}}</span> <span class="synSpecial">{{</span> remain <span class="synSpecial">}}</span> <span class="synSpecial">{{</span> <span class="synPreProc">$t(</span><span class="synStatement">'</span><span class="synConstant">label.times</span><span class="synStatement">'</span><span class="synPreProc">)</span> <span class="synSpecial">}}</span> <span class="synStatement">&lt;</span>/template<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>template v-else-if<span class="synStatement">=&quot;</span><span class="synConstant">remainingType === 'today'</span><span class="synStatement">&quot;&gt;</span> <span class="synStatement">&lt;</span>!-- 本日あと N 回 <span class="synSpecial">--&gt;</span> <span class="synSpecial">{{</span> <span class="synPreProc">$t(</span><span class="synStatement">'</span><span class="synConstant">label.today</span><span class="synStatement">'</span><span class="synPreProc">)</span> <span class="synSpecial">}}</span> <span class="synSpecial">{{</span> <span class="synPreProc">$t(</span><span class="synStatement">'</span><span class="synConstant">label.more</span><span class="synStatement">'</span><span class="synPreProc">)</span> <span class="synSpecial">}}</span> <span class="synSpecial">{{</span> remain <span class="synSpecial">}}</span> <span class="synSpecial">{{</span> <span class="synPreProc">$t(</span><span class="synStatement">'</span><span class="synConstant">label.times</span><span class="synStatement">'</span><span class="synPreProc">)</span> <span class="synSpecial">}}</span> <span class="synStatement">&lt;</span>/template<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>template v-else<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>!-- あと N 回 <span class="synSpecial">--&gt;</span> <span class="synSpecial">{{</span> <span class="synPreProc">$t(</span><span class="synStatement">'</span><span class="synConstant">label.more</span><span class="synStatement">'</span><span class="synPreProc">)</span> <span class="synSpecial">}}</span> <span class="synSpecial">{{</span> remain <span class="synSpecial">}}</span> <span class="synSpecial">{{</span> <span class="synPreProc">$t(</span><span class="synStatement">'</span><span class="synConstant">label.times</span><span class="synStatement">'</span><span class="synPreProc">)</span> <span class="synSpecial">}}</span> <span class="synStatement">&lt;</span>/template<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/p<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/template<span class="synStatement">&gt;</span> </pre> <p>同じ文言で再利用したい気持ちはわかるのですが, これは翻訳が考慮されていません.</p> <ul> <li>今月あと N 回</li> <li>本日あと N 回</li> <li>あと N 回</li> </ul> <p>この 3文をとりあえず適当に DeepL などで翻訳かけてみるとこうなります.</p> <p>英語:</p> <ul> <li>N more times this month</li> <li>N more times today</li> <li>N times left</li> </ul> <p>中国語 (簡体字):</p> <ul> <li>本月 N 多次</li> <li>今天 N 多次</li> <li>N 多次</li> </ul> <p>韓国語:</p> <ul> <li>이번 달에 N 번 더</li> <li>오늘 N 번 더</li> <li>앞으로 N 번 더</li> </ul> <p>私は言語に詳しくないので, これが正しいかどうかはイマイチわかりませんが, <br/> 少なくとも今のロジックでこれに対応できないことはわかります.</p> <p>上記, や他言語も考慮すると, 以下のように組まれるべきでした.</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synError">// 言語リソース</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">label</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">n_more_times_this_month</span>&quot;: &quot;<span class="synConstant">今月あと{remain}回</span>&quot;, &quot;<span class="synStatement">n_more_times_today</span>&quot;: &quot;<span class="synConstant">本日あと{remain}回</span>&quot;, &quot;<span class="synStatement">n_times_left</span>&quot;: &quot;<span class="synConstant">あと{remain}回</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">&lt;</span>template<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>p<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>template v-if<span class="synStatement">=&quot;</span><span class="synConstant">remainingType === 'thisMonth'</span><span class="synStatement">&quot;&gt;</span> <span class="synStatement">&lt;</span>!-- 今月あと N 回 <span class="synSpecial">--&gt;</span> <span class="synSpecial">{{</span> <span class="synPreProc">$t(</span><span class="synStatement">'</span><span class="synConstant">label.n_more_times_this_month</span><span class="synStatement">'</span>, <span class="synSpecial">{</span> remain <span class="synSpecial">}</span><span class="synPreProc">)</span> <span class="synSpecial">}}</span> <span class="synStatement">&lt;</span>/template<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>template v-else-if<span class="synStatement">=&quot;</span><span class="synConstant">remainingType === 'today'</span><span class="synStatement">&quot;&gt;</span> <span class="synStatement">&lt;</span>!-- 本日あと N 回 <span class="synSpecial">--&gt;</span> <span class="synSpecial">{{</span> <span class="synPreProc">$t(</span><span class="synStatement">'</span><span class="synConstant">label.n_more_times_today</span><span class="synStatement">'</span>, <span class="synSpecial">{</span> remain <span class="synSpecial">}</span><span class="synPreProc">)</span> <span class="synSpecial">}}</span> <span class="synStatement">&lt;</span>/template<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>template v-else<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>!-- あと N 回 <span class="synSpecial">--&gt;</span> <span class="synSpecial">{{</span> <span class="synPreProc">$t(</span><span class="synStatement">'</span><span class="synConstant">label.n_times_left</span><span class="synStatement">'</span>, <span class="synSpecial">{</span> remain <span class="synSpecial">}</span><span class="synPreProc">)</span> <span class="synSpecial">}}</span> <span class="synStatement">&lt;</span>/template<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/p<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/template<span class="synStatement">&gt;</span> </pre> <p><br></p> <h1 id="本題">本題</h1> <p>さて, 多言語周りを見直す際に色々調べて回ってたところ, 以下のソースコードに行き着きました.</p> <p><a href="https://github.com/Tokyo-Metro-Gov/covid19">https://github.com/Tokyo-Metro-Gov/covid19</a></p> <p>なんでここに行き着いたんだろう・・・? 全く関係ないですね, 経緯も覚えてません.</p> <p>ただ, ここにある locale のコードが最も重要でした.</p> <p><a href="https://github.com/Tokyo-Metro-Gov/covid19/blob/development/assets/locales/ja.json">https://github.com/Tokyo-Metro-Gov/covid19/blob/development/assets/locales/ja.json</a></p> <p>一部抜粋</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">詳細を見る(東京都福祉保健局)</span>&quot;: &quot;<span class="synConstant">詳細を見る(東京都福祉保健局)</span>&quot;, &quot;<span class="synStatement">相談方法は、症状や状況別で3つに分かれます</span>&quot;: &quot;<span class="synConstant">相談方法は、症状や状況別で3つに分かれます</span>&quot;, &quot;<span class="synStatement">かかりつけ医がいて症状のある方</span>&quot;: &quot;<span class="synConstant">かかりつけ医がいて症状のある方</span>&quot;, &quot;<span class="synStatement">かかりつけ医がいない症状のある方</span>&quot;: &quot;<span class="synConstant">かかりつけ医がいない症状のある方</span>&quot;, ... </pre> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">詳細を見る(東京都福祉保健局)</span>&quot;: &quot;<span class="synConstant">Details:See the website (of Tokyo Metropolitan Government Bureau of Social Welfare and Public Health)</span>&quot;, &quot;<span class="synStatement">相談方法は、症状や状況別で3つに分かれます</span>&quot;: &quot;<span class="synConstant">Which of the following is your situation?</span>&quot;, &quot;<span class="synStatement">かかりつけ医がいて症状のある方</span>&quot;: &quot;<span class="synConstant">You are experiencing symptoms and have a primary care doctor</span>&quot;, &quot;<span class="synStatement">かかりつけ医がいない症状のある方</span>&quot;: &quot;<span class="synConstant">You are experiencing symptoms and do not have a primary care doctor</span>&quot;, ... </pre> <p>あっ・・わかりやすい!!!</p> <p>json には key に日本語を設定しちゃって良いのでした. 完全に失念していました.</p> <p>つまり, 問題としていたコードは, この方式を真似すると以下のようになります.</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synError">// 言語リソース</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">label</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">今月あと{remain}回</span>&quot;: &quot;<span class="synConstant">今月あと{remain}回</span>&quot;, &quot;<span class="synStatement">本日あと{remain}回</span>&quot;: &quot;<span class="synConstant">本日あと{remain}回</span>&quot;, &quot;<span class="synStatement">あと{remain}回</span>&quot;: &quot;<span class="synConstant">あと{remain}回</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">template</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">p</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">template</span><span class="synIdentifier"> v-if=</span><span class="synConstant">&quot;remainingType === 'thisMonth'&quot;</span><span class="synIdentifier">&gt;</span> {{ $t('今月あと{remain}回', { remain }) }} <span class="synIdentifier">&lt;/</span><span class="synStatement">template</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">template</span><span class="synIdentifier"> v-else-if=</span><span class="synConstant">&quot;remainingType === 'today'&quot;</span><span class="synIdentifier">&gt;</span> {{ $t('本日あと{remain}回', { remain }) }} <span class="synIdentifier">&lt;/</span><span class="synStatement">template</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">template</span><span class="synIdentifier"> v-else&gt;</span> {{ $t('あと{remain}回', { remain }) }} <span class="synIdentifier">&lt;/</span><span class="synStatement">template</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">p</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">template</span><span class="synIdentifier">&gt;</span> </pre> <p>可読性が格段に上がりました. 表示文言がキーになっているので, コメントで補足しなくても何が書いてあるか推測しやすいです. <br> <br></p> <h1 id="まとめ">まとめ</h1> <p>背景で説明した通り, 弊プロジェクトでは多言語対応の処理が翻訳困難なロジックになっており, 全体的に改修が必須になっていました.</p> <p>改修をどうせするなら, この方式を導入しても工数的にはなんら差はなく, 方向性だけ決めて現在はスムーズに変更ができている状況です.</p> <p>json のキーに日本語を設定できる. 仕様としては知ってましたが, こういうときに使えるとは思っておらず, 目からウロコでした.</p> <p>public repository を眺めてるだけで, こういう改善のアイデアがいっぱい拾えるのいいですね.<br/> どんどんコードを良くしていきたいです.</p> <p>おわり</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech hacomono CTOによる2023年の振り返り hatenablog://entry/6801883189069285752 2023-12-25T07:00:00+09:00 2023-12-25T11:50:56+09:00 こちらの記事はhacomono Advent Calendar 2023の25日目の記事です hacomono CTOのまこ(@macococo)です。 子供へのサンタ業を済ませてこの記事を書いています。今年のプレゼントは「マイニンテンドーストア」で買える「えらべるプレゼントセット」にしました。よく考えたらサンタ = ニンテンドーアカウントを持ってるとある人物と気付かれるかもしれませんが触れずに押し通そうと思います。 さて、今年最後のテックブログとして、2023年の hacomono のプロダクト組織を振り返りたいと思います。ちなみに昨年の振り返りはこちらです。 組織の成長 hacomono … <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231225/20231225080925.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>こちらの記事は<a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono Advent Calendar 2023</a>の25日目の記事です</p></blockquote> <p>hacomono CTOのまこ(<a href="https://twitter.com/macococo">@macococo</a>)です。</p> <p>子供へのサンタ業を済ませてこの記事を書いています。今年のプレゼントは「マイニンテンドーストア」で買える「えらべるプレゼントセット」にしました。よく考えたらサンタ = ニンテンドーアカウントを持ってるとある人物と気付かれるかもしれませんが触れずに押し通そうと思います。</p> <p>さて、今年最後のテックブログとして、2023年の hacomono のプロダクト組織を振り返りたいと思います。ちなみに昨年の振り返りは<a href="https://techblog.hacomono.jp/entry/2022/12/27/070000">こちら</a>です。</p> <h2 id="組織の成長">組織の成長</h2> <p>hacomono のプロダクト組織の社員数は2022年末で49名ほど、当時は下の組織図で説明していました。プロダクト開発を行っている4チーム + IoT チームと、それを支えたり協業する各チームという構成です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231224/20231224093559.png" width="1200" height="567" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>現在は以下のような組織図で、社員が80名ほどに増えました 🎉 全体の構成は大きく変わっていませんが、人数増加に合わせて横串組織としての CTO 室の新設、事業軸・ドメイン軸で開発組織を整理するなど、適宜チューニングがされてきています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231224/20231224093509.png" width="1070" height="934" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>この1-2年は特に全体の社員数が非常に増えた年ではありましたが、スタートアップ不況とも言われる現在の市況から、hacomono においても筋肉質な組織運営を目指す方針にシフトしていっています。ただ採用人数を追うのではなくイシュー採用を掲げ、hacomono と求職者の方との will がお互いマッチするかをしっかり見定めて採用に取り組んでいます。</p> <p>そんな中でもしっかり採用が進められているのは、普段からエンジニア採用にコミットしている人事、Engineering Office、外部発信やアトラクトに全力投球してくれているメンバーのおかげです。特に今年は外部発信に力を入れてくれたおかげで、自社イベントが実施できたり、多少無茶振りした気もしますが…こうしてアドベントカレンダー企画も遂に実施できています。本当に素晴らしい成果だと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fadvent-calendar%2F2023%2Fhacomono" title="hacomonoのカレンダー | Advent Calendar 2023 - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> <p>さて、hacomono はフルリモート x フルフレックスの会社です。最近は週N回出社にシフトしている会社も多いと思いますが、hacomono はフルリモートを継続するという前提の中で、どう成果を最大化させることができるかに引き続きチャレンジしています。</p> <p>入社いただく方の居住地もそれぞれなのですが、昨年と同じく日本地図にプロットしてみました。黄色が今年入社、緑が去年入社、灰色が一昨年入社です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231224/20231224093841.png" width="1200" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>…大都市圏にお住まいの方も既に多いので、あまり大きくは変わってないですね笑</p> <p>ただ各地域のメンバーは確実に増えていて、年明け1月に行われる全社ミーティングでは、各地域毎に集まっての新年会が企画されるほどにメンバーが増えています。新年会、個人的にも楽しみにしてます🍻</p> <h2 id="事業の成長">事業の成長</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231224/20231224093906.png" width="1200" height="674" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231224/20231224093929.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>今年はシリーズC調達プレスに合わせて作成した<a href="https://speakerdeck.com/hacomono/smart-wellness-saas">会社紹介資料</a>をベースに事業を説明していましたが、資料のキービジュアルにもある “インフラ” という言葉をよくお伝えしていました。シリーズC調達プレス時点では導入店舗数が3,000店舗ほどでしたが、現在では5,000店舗まで拡大しており、昨年にも増してインフラとしての責務がさらに高まっています。</p> <p>我々が提供するサービスに何か問題を起こることで、店舗スタッフの方の運用が回らない、エンドユーザーが入会ができない・予約できない・店舗に出入りできないといったクリティカルな問題が起きるリスクを想定する必要があります。昨年の振り返り記事でもインシデントや品質面を課題として挙げていましたが、2023年は大小問わず様々なアクションプランを動かして “品質” に取り組んできた年でした。取り組みの1つとして PSIRT を立ち上げ、品質向上からリスクマネジメントなど幅広いイシューをリードしています。</p> <p>CTO 室 EM のよこちゃんが PSIRT 含め品質向上に向けてゴリゴリリードしてくれており、LTイベントにてインシデント対応〜ポストモーテムについて発表してくれました。ぜひご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftechblog.hacomono.jp%2Fentry%2F2023%2F09%2F19%2F110000" title="ポストモーテムLunchLTに登壇しました - hacomono TECH BLOG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> <p>まだまだやりきれていないことはたくさんありますが、こういった取り組みを進めることでトップダウンだけでなくボトムアップでも品質に対する意識の向上が高まっており、チームや個人の OKR にも品質に関する目標が入ってくるなど、具体的に行動も変容してきていることを感じています。永遠のテーマとして、来年以降も継続していく取り組んでいきます。</p> <h2 id="さらなる大きな成長に向けて">さらなる大きな成長に向けて</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231224/20231224094052.png" width="1200" height="673" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>hacomono はフィットネス事業を中心に既存プロダクトで一定のトラクションを作れているものの、重要なのは事業の状態が良いうちに非連続な成長につなげる手を打っていくことです。品質・セキュリティなど守りの取り組みも進めつつ、スタートアップとしてランウェイを意識しながらスピード感を持って事業成長を牽引するプロダクトを仕込んでいくことも進めています。</p> <p>今年も既存機能の改善以外にも、そういった新規開発が複数走っていましたが、そのクオリティコントロールやプロセスに関しては課題があり、まさに今立て直しをしているところです。</p> <p>MVP (Minimum Viable Product) という言葉がありますが、こと B2B 領域においてはそもそも業務プロセスを回せるレベルのものがベースとして必要となります。ビジネスとしてコミットするためには MVP だけではなく MSP (Minimum Sellable Product) の観点が必要で、そこからさらに期待値を超える体験を加えていく。ただ言われたものを作る、誰かがこれで OK と言われたからやるのではなく、自分たち自身で仮説思考を持って議論できるプロダクト組織を目指し、ロードマップ策定のプロセスから見直しを図っています。</p> <p>自分たちが作っているものを自分ごと化できるか、作品と思えるかも重要だと考えています。組織がスケールしていくに連れて役割が細分化され、そういった感覚は薄れがちですが、チームにフォーカスして見た時にこういったマインド・熱量を持てている組織・カルチャー作り、ミッションの明文化は自分の責務として捉えています。そして hacomono のプロダクト開発チームが、プロダクト開発のスペシャリストとして客観的にも評価される未来を目指したいと思います。</p> <p>そんなことをここ最近考えていたら、アセンド CTO の丹羽さんのプロダクトエンジニアの記事が飛び込んできました。プロダクトエンジニアについて素晴らしく言語化された記事でオススメです。強く共感します。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Fniwa_takeru%2Fn%2Fn0ae4acf2964d" title="プロダクトエンジニアとは何者か|niwa_takeru" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> <h2 id="さいごに">さいごに</h2> <p>今年もお世話になった皆様、本当にありがとうございました!</p> <p>最近、hacomono は人も体制も揃ってきているという印象を持たれることも増えてきたのですが、まだまだ課題も多いのが現状ですし、恐らく来年も同じようなことを言っていると思います。ただ課題はあるのは当然で、この記事を書くにあたって自分の毎月の振り返りメモを読んでいたのですが、コミットしてきた成果も素晴らしく、率直にメンバーへの感謝の気持ちが改めて強まりました。来年も着実に前に進んでいけるチームだと思っています。</p> <p>個人的にはちょっと今年は開発の方に時間を使い過ぎてしまったので、年末年始はしっかり休んでインプットの時間に当てたいと思います。</p> <p>それでは皆様良いお年を!</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech Jamf Proのゼロタッチデプロイの中身を見直した話 hatenablog://entry/6801883189065013059 2023-12-24T07:00:00+09:00 2023-12-24T07:00:16+09:00 こちらの記事はhacomono Advent Calendar 2023の24日目の記事です こんにちは!情シスのえんどうです。 hacomono発のアドベントカレンダーということで、今回はJamf ProのPrestage Enrollment(所謂ゼロタッチデプロイ)という少しコアな話をしようと思います。 Jamf Proを運用している情シスの皆さんのためになる記事であれば嬉しいです🙇 Jamf Pro導入からもうすぐ2年 2022年4月にhacomonoがJamf Proを導入してからもうすぐ2年経とうとしています。 hacomonoの就労環境はフルリモートで私も福岡に住んでいる都合上、… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231221/20231221153402.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>こちらの記事は<a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono Advent Calendar 2023</a>の24日目の記事です</p></blockquote> <p>こんにちは!情シスのえんどうです。</p> <p>hacomono発のアドベントカレンダーということで、今回はJamf ProのPrestage Enrollment(所謂ゼロタッチデプロイ)という少しコアな話をしようと思います。</p> <p>Jamf Proを運用している情シスの皆さんのためになる記事であれば嬉しいです🙇</p> <h3 id="Jamf-Pro導入からもうすぐ2年">Jamf Pro導入からもうすぐ2年</h3> <p>2022年4月にhacomonoがJamf Proを導入してからもうすぐ2年経とうとしています。</p> <p>hacomonoの就労環境はフルリモートで私も福岡に住んでいる都合上、導入直後からPrestage Enrollmentの利用は必至で早々にゼロダッチデプロイの環境を作り上げました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftechblog.hacomono.jp%2Fentry%2F2022%2F06%2F27%2F120000" title="hacomonoのIT環境がここ半年で180度変わった話 - hacomono TECH BLOG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://techblog.hacomono.jp/entry/2022/06/27/120000">techblog.hacomono.jp</a></cite></p> <p>それから1年以上経った今もJamf Proはhacomonoのフルリモート環境を支えてくれている、重要なサービスです。</p> <p>ただ、最近ふと思ったことがあります。</p> <h3 id="Prestage-Enrollmentの中身を見直せていない">Prestage Enrollmentの中身を見直せていない…</h3> <p>正確に言うと、”<strong>問題が起きた部分だけ</strong>”都度修正して解消させていました。</p> <p>ただ、抜本的な見直しは行なっておらず、今では必要なくなったポリシーなどもそのまま流し込んでしまっていました。</p> <p>今回のテックブログでは実際に行なった”<strong>お掃除</strong>”を紹介出来ればと思います。</p> <h4 id="ポリシー">ポリシー</h4> <p>お恥ずかしながら今は既に使っていない複合機のドライバインストールが残っていました…</p> <p>また、これを機にカスタムイベント名を<code>installHogehoge</code>と統一することにしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231208/20231208084206.png" width="1200" height="108" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>また、全従業員が利用しているSlackやZoom,Google Chrome,Okta Verifyですが、これまで<a href="https://github.com/jamf/DEPNotify-Starter">DEPNotify</a>(オンボーディングを効率化するスクリプト)後にSelfServiceからインストールしてもらっていましたが、どうせ全従業員が利用するのであればDEPNotifyでインストールさせることにしました。</p> <h4 id="構成プロファイル">構成プロファイル</h4> <p>macOS 13 Venturaからバックグラウンドで実行されるアプリケーションなどがインストールされると、ユーザーに確認を求められるようになりました(面倒) <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231208/20231208084255.png" width="1200" height="363" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>この設定は通常であればユーザーもオフにすることが出来るようになっていますが、セキュリティソフトなど必須のアプリをオフにされると困ってしまいます。</p> <p>ということで、下記の知見を参考にJamf Proから設定のオンを強制させています(Jamf Pro 11.1.1現在ではGUIで設定を追加出来るようになっています) <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fzenn.dev%2Fssk_ats%2Farticles%2F0a98484ce099d6" title="Jamf Proを使ってVenturaのBackground Itemを許可する設定を配布する" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://zenn.dev/ssk_ats/articles/0a98484ce099d6">zenn.dev</a></cite></p> <p><figure class="figure-image figure-image-fotolife" title="Jamf Proから強制しているアプリとしていないアプリの図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231208/20231208084344.png" width="1200" height="266" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Jamf Proから強制しているアプリとしていないアプリの図</figcaption></figure></p> <h4 id="スクリプト">スクリプト</h4> <p>皆さんも忘れがちなスクリプトの更新、ちゃんとやっておきましょう(1敗)</p> <p>Jamf Proユーザーであれば<a href="https://github.com/Installomator/Installomator">Installomator</a>を、Netskopeも利用しているのであればNetskope公式が提供している<a href="https://docs.netskope.com/en/netskope-help/netskope-client/netskope-client-deployment-options/jamf/">JAMFScript</a>も定期的に更新していた方が良いと思います。</p> <blockquote><p>ご存じの通りDEPNotifyは2年以上更新されておらず、Jamf Pro側でmacOSオンボーディングなる機能が登場したので、皆さん今後はそちらに移っていくんですかね…?</p></blockquote> <h4 id="パッケージ">パッケージ</h4> <p>Enroll時にインストールされるアプリは要注意です。</p> <p>セキュリティソフト系は定期的に最新の安定版パッケージをJamfに登録しておくようにしましょう。</p> <h3 id="従業員が関係する変更点">従業員が関係する変更点</h3> <h4 id="インストールアプリケーションの追加">インストールアプリケーションの追加</h4> <p>前述したように、<a href="https://github.com/jamf/DEPNotify-Starter">DEPNotify</a>でインストールするアプリを増やしたことでセットアップの待ち時間が増えるようになりました。</p> <p>ただ、ここでインストールしなければ後々SelfServiceでインストールする必要があるので、待ってもらうこととしました。 <figure class="figure-image figure-image-fotolife" title="これまでは業務系のアプリはDEPNotifyではなくSelfServiceでインストールしてもらっていました"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231208/20231208084515.png" width="1200" height="748" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>これまでは業務系のアプリはDEPNotifyではなくSelfServiceでインストールしてもらっていました</figcaption></figure></p> <h4 id="DEPNotify後の再起動">DEPNotify後の再起動</h4> <p><figure class="figure-image figure-image-fotolife" title="DEPNotifyが実行している画面"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231208/20231208084541.png" width="1200" height="781" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>DEPNotifyが実行している画面</figcaption></figure></p> <p>これまで、<a href="https://github.com/jamf/DEPNotify-Starter">DEPNotify</a>が終了した後は自動ログオフ→サインインをしてもらっていました(FileVault有効化のため)</p> <p>が、稀にインベントリの収集が走らなかったりしたことがあり、ヘルプデスクから再起動してもらうことがあったので、いっそのこと<a href="https://github.com/jamf/DEPNotify-Starter">DEPNotify</a>後に再起動するように変更しました。</p> <h4 id="User-Initiated-EnrollmentでもDEPNotifyを起動">User-Initiated EnrollmentでもDEPNotifyを起動</h4> <p>後述もしますが、近々でインターネット接続直後にリモートマネジメントに入ってくれない端末が出現しており、User-Initiated Enrollment(以降UIE)を再び利用することが出始めました。</p> <p>UIEは約2年前の既存従業員のJamf Pro登録のためだけに利用していたので、これを機にUIEでも<a href="https://github.com/jamf/DEPNotify-Starter">DEPNotify</a>を起動させ、必要なソフトをインストールさせることにしました。</p> <h4 id="設定アシスタント内でFileVault有効化">設定アシスタント内でFileVault有効化</h4> <p>Jamf Pro 10.50から、FileVaultの有効化を設定アシスタント中に有効にすることができるようになりました。</p> <p>これまではセットアップ後にサインアウト or 再起動しないとFileVaultが有効にならなかったのでありがたい機能更新です。 <figure class="figure-image figure-image-fotolife" title="実際に設定アシスタント内でFileVaultを有効にしている画面"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231208/20231208084647.png" width="1200" height="781" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>実際に設定アシスタント内でFileVaultを有効にしている画面</figcaption></figure></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.jamf.com%2Fblog%2Fjamf-pro-1050-release-apple-os-compatibility-and-much-more%2F" title="Jamf Pro 10.50 Offers Compatibility with Apple&#39;s Latest macOS, iPadOS and iOS Betas" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.jamf.com/blog/jamf-pro-1050-release-apple-os-compatibility-and-much-more/">www.jamf.com</a></cite></p> <h3 id="細い問題点と課題">細い問題点と課題</h3> <h4 id="リモートマネジメントに入ってくれない">リモートマネジメントに入ってくれない</h4> <p>明確な原因は分かっていないのですが、PreStage Enrollmentとして登録しているmacであるにも関わらずリモートマネジメントに入らず、組織外のmacとしてそのままセットアップが走ってしまう現象が発生しています。</p> <p>ネットの速度が遅いと発生しているのでは…?という推測まで至ってはいますが、現状は発生したらそのままセットアップを進めてもらい、UIEでJamf Proに登録してもらっている状況です。</p> <h4 id="2台目のPCをセットアップするとDEPNotifyが終わらない">2台目のPCをセットアップするとDEPNotifyが終わらない</h4> <p>例として、AユーザーのmacがJamf Proに登録済みかつユーザーと位置に従業員情報が入っている状態で、Aユーザーが2台目のmacをセットアップすると、2台目のインベントリではユーザーと位置が空白になってしまいます(登録カスタマイゼーションでOktaを選択しセットアップ時にOktaにログインさせています) <figure class="figure-image figure-image-fotolife" title="空白のユーザと位置"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231208/20231208084802.png" width="1200" height="418" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>空白のユーザと位置</figcaption></figure></p> <p>この件をJamf Proのサポートにも問い合わせてみたところ、Jamf社側も問題を認知してくれましたが、現状では解決方法&amp;予定も無いので、途方に暮れています。</p> <p>Jamf ProでLDAPを利用している方であれば、手動で入れればOKじゃないか?と思われるかもしれませんが、セットアップ時点でユーザ名が入っていないことでNetskopeのインストール時にユーザーを認識できずコケてしまう。という問題に直面しています。</p> <h3 id="結論">結論</h3> <p>ここまでJamf Proの細かい設定等々を説明してきましたが、結論として「<strong>Enrollmentで使用している設定は定期的(最低半年に1度)は見直すようにしましょう</strong>」ということです。</p> <p>1年前には最新で最善と思っていたことが今では陳腐化してしまった。ということはよくあることです。</p> <p>SaaSは構築して終わりではなく運用も大事ですので、ぜひ皆さんの環境でも見直してみてください!</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech 急成長スタートアップで経験したスキーマ変更の壁 hatenablog://entry/6801883189068322084 2023-12-23T07:00:00+09:00 2023-12-23T07:00:03+09:00 こちらの記事はhacomono Advent Calendar 2023の23日目の記事です こんにちは、hacomonoでプロダクト開発をしております田中です! 最近は子供の保育園が始まり毎朝バタバタしていますが、hacomonoはフルフレックス制度を導入しているため、始業時間を柔軟に調整できるので助かっています。 hacomonoに入社して2年と10ヶ月目になりましたが、この数年で社員の数も約30人前後から200人を超えるまでに増え、プロダクトも加速度的に大きくなり、開発をする上でさまざまな障壁に直面してきました。 hacomonoは、24時間運営をしているお客様にも多くご利用いただいてい… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231221/20231221153520.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>こちらの記事は<a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono Advent Calendar 2023</a>の23日目の記事です</p></blockquote> <p>こんにちは、hacomonoでプロダクト開発をしております田中です!</p> <p>最近は子供の保育園が始まり毎朝バタバタしていますが、hacomonoはフルフレックス制度を導入しているため、始業時間を柔軟に調整できるので助かっています。</p> <p>hacomonoに入社して2年と10ヶ月目になりましたが、この数年で社員の数も約30人前後から200人を超えるまでに増え、プロダクトも加速度的に大きくなり、開発をする上でさまざまな障壁に直面してきました。</p> <p>hacomonoは、24時間運営をしているお客様にも多くご利用いただいているため、頻繁にメンテナンスタイムを設けることが難しく、ダウンタイム無しでプロダクトをアップデートし続けることが適宜求められます。</p> <p>今回は過去経験した中でも特に障壁に感じていた事の1つであるDBのスキーマ変更について、入社直後から現在までに直面した問題を経験ベースで書いていきたいと思います。</p> <h2 id="DDLデータ移行に時間がかかるようになる">DDL、データ移行に時間がかかるようになる</h2> <p>hacomono入社後の約3ヶ月後、新機能のリリースでスキーマ変更がタイムアウトしている事象が起きていました。自身が開発していたものではなかったので当時は原因をあまり理解できていませんでしたが、DDL先のレコード量が多い状態でテーブルコピーが発生し、Railsのmigrationが途中でタイムアウトしてしまうような問題が起きていました。</p> <p>SQL自体はタイムアウトしていましたが、Railsのmigraionのステータスを管理しているテーブル上のステータスが完了状態になっていたため、恒久対応としてDDLが失敗した環境に対して手動でSQLを流したことを今でも覚えています。</p> <p>顧客によっては膨大なデータ量になっていることを当時は改めて実感しました。</p> <h2 id="DDL中に排他ロックがかかる">DDL中に排他ロックがかかる</h2> <p>hacomono入社後の約1年半後、自身が開発していた新機能のリリースが近くなってきたこともあり、スキーマ変更の事前テストを行っていました。前述していたDDLのタイムアウトを経験していたのでデータ量の多い検証環境を作成し、そこに対して本番で想定しているDDLを実行しつつ画面を触りながら各トランザクションを走らせて検証を行いました。</p> <p>すると、DDL対象のテーブルでロック待ちが発生し、特定画面の操作でサーバーエラーが発生することがわかりました。</p> <p>当時のDBはMySQL5.7をメインで利用していたため、MySQL5.7のドキュメントを見つつ原因が判明しました。この時の問題は、外部キーのカラムを追加しようとしていたため、DDL対象のテーブルに排他ロックがかかり、並列で走るDML(INSERTやUPDATEなど)が不可になるような状態になっていることがわかりました。</p> <p>MySQLのドキュメントを漁りつつ、社内の有識者にも相談し、foreign_key_checksを一時的にOFFにしながらALTER TABLEを行うことでオンラインDDLで実行できることがわかったため、SQLを調整して再検証を行いました。オンラインDDLで実行することで並列のDMLが許可されるようになったため、本番リリースもこの方式でいくことにし、リリース日を迎えました。</p> <p>リリース日当日はテーブルをロックすることはなくなったのですが、ユニーク制約にひっかかってしまうDMLのクエリが多く走っており、結局深夜までDDLが通らずに苦い思い出となりました。</p> <p>ただ、事前に検証をしっかり行ったことで大事故にはならずにリリースを終えることができたのはとても良い経験になりました。</p> <p>以下の記事でより詳細な当時の対応を書いていますので是非見ていただければと思います!</p> <p><a href="https://techblog.hacomono.jp/entry/2023/01/24/070000">MySQL 5.7のオンラインDDLによるサービス無停止のカラム追加 - hacomono TECH BLOG</a></p> <h2 id="必須カラムの存在によって切り戻しできない">必須カラムの存在によって切り戻しできない</h2> <p>直近約1年程はモノリスなコードの肥大化に伴いインシデントの量も増加し、リリースのPRをRevertするような場面が多々発生しました。アプリケーションコードのRevertに関しては比較的容易ですが、DBを戻すのは一筋縄ではいきません。</p> <p>基本的にはアプリケーションのコードよりも先にスキーマ変更だけ実施しておき、後からコードをリリースするようなやり方でリリースしていましたが、新機能で利用する必須のカラムがある場合などは、データ移行を行ったり、アプリケーションコードと同時にリリースする時もありました。</p> <p>しかし、必須カラムの追加を行ってしまうとリリースの切り戻しの難易度があがります。必須カラム追加後にアプリケーションコードを切り戻してしまうと、古いコードでは該当テーブルに対して意図せぬデフォルト値での保存になってしまう場合があります。</p> <p>こういった問題が多く発生していたため、リリース前にスキーマ変更だけを行った状態でrspecが全て通ることを確認してからリリース判定を行うような運用になりました。</p> <p>組織が大きくなると様々なチームからスキーマ変更が実施されるため、スピードはなるべく落とさず、より安全面を意識するような運用に変わってきたなと実感しました。</p> <h2 id="オンラインDDLでもスキーマ変更ができない">オンラインDDLでもスキーマ変更ができない</h2> <p>ここ半年は顧客のデータ量増加が顕著になり、常にトランザクションが途切れないため、日中のスキーマ変更が難しくなってきました。今まではオンラインDDLで実行すれば問題なく実施できていましたが、トランザクションの数が増えてきたことで、DDL実行開始時と終了時にメタデータロックを獲得できず、他トランザクションを含めて待機状態になってしまうような傾向がでてきました。</p> <p>日中のDDL実行が難しくなってきたため、交代制で深夜にDDLを実施するような運用に変わりましたが、深夜でさえ中々1発でDDLが通らない時もあります。</p> <p>メンテナンスタイム以外の時間で安全にスキーマ変更を行う必要がある場合は、今時点ではこの方式に着地しています。</p> <h1 id="まとめ">まとめ</h1> <p>スタートアップにいると前例が役に立たず常に新しい運用方法を模索しないといけない場面が多く、試行錯誤の繰り返しだなと思うことが多々ありますが、こういった経験は本で読むだけでは体得できないモノだと思います。</p> <p>苦しい場面もありますが、とても貴重な経験でありエンジニアとしての成長を実感できています。</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech 【RailsサービスのMySQL5.7 -> 8移行: 前編】MySQLのcollation周りの話 hatenablog://entry/6801883189067772242 2023-12-20T07:00:00+09:00 2023-12-20T07:00:00+09:00 この記事は、hacomono Advent Calender 2023の20日目の記事です。 はじめに こんにちは、プラットフォームチーム所属のまこたすです。 この記事は主にMySQL5.7,MySQL8.0のcollation周りの挙動の違いについて書いています。AWS RDS MySQL5.7がEOLを迎える今、一番話したい内容はRails x MySQL5.7環境からRails x MySQL8.0環境へ移行する際にハマった話とそこからみる気をつけるべき観点という話題ではあるのですが、前提の話が長いので記事を2つに分けてお伝えします。今回はRailsの話は触れず、MySQLのcollat… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231219/20231219105027.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>この記事は、<a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono Advent Calender 2023</a>の20日目の記事です。</p></blockquote> <h1 id="はじめに">はじめに</h1> <p>こんにちは、プラットフォームチーム所属の<a href="https://twitter.com/Maco_Tasu">まこたす</a>です。</p> <p>この記事は主にMySQL5.7,MySQL8.0のcollation周りの挙動の違いについて書いています。AWS RDS MySQL5.7がEOLを迎える今、一番話したい内容はRails x MySQL5.7環境からRails x MySQL8.0環境へ移行する際にハマった話とそこからみる気をつけるべき観点という話題ではあるのですが、前提の話が長いので記事を2つに分けてお伝えします。今回はRailsの話は触れず、MySQLのcollation周りの話のみをします。</p> <h1 id="この記事で書くこと">この記事で書くこと</h1> <ul> <li>MySQL5.7, MySQL8.0でのサーバー, データベース, テーブルのcollationの決まり方と<code>SHOW CREATE (TABLE|DATABASE)</code>の結果の違い <ul> <li>記事内においては<strong>utf8mb4な設定がされているMySQLという前提で話します</strong></li> </ul> </li> </ul> <h1 id="この記事で書かないこと">この記事で書かないこと</h1> <ul> <li>character_set_*について</li> <li>Rails周りの話</li> </ul> <h1 id="TLDR">TL;DR</h1> <p>※早見表では、挙動が変わる場合と最低限の挙動を理解するために必要な組み合わせのみを載せています。</p> <h2 id="データベースのcollation決定の早見表">データベースのcollation決定の早見表</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/M/Maco_Tasu/20231219/20231219115109.png" width="1117" height="311" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul> <li>MySQL5.7の場合、COLLATE指定 > <code>collation_database</code> の優先度でcollationが決まる</li> <li>MySQL8.0の場合、COLLATE指定 > utf8mb4指定の場合のみ<code>default_collation_for_utf8mb4</code> > <code>collation_server</code> の優先度でcollationが決まる</li> </ul> <h2 id="テーブルのcollation決定の早見表">テーブルのcollation決定の早見表</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/M/Maco_Tasu/20231219/20231219115133.png" width="1117" height="311" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul> <li>MySQL5.7の場合、COLLATE指定 > <code>collation_database</code> の優先度でcollationが決まる</li> <li>MySQL8.0の場合、COLLATE指定 > utf8mb4を明示指定の場合のみ<code>default_collation_for_utf8mb4</code> > <code>collation_database</code> の優先度でcollationが決まる</li> </ul> <h1 id="MySQLでのcollation関係の設定値について">MySQLでのcollation関係の設定値について</h1> <p>まずサーバー, データベースのcollationを設定するための重要なパラメーターを見ていきます。</p> <h2 id="collation_server">collation_server</h2> <p>MySQL Serverのデフォルトcollactionの値です。サーバーの起動時にオプションで渡したり、AWS RDSだとパラメーターグループで指定ができます。 以下のような特徴があります。</p> <blockquote><p>サーバー文字セットおよび照合順序は、データベース文字セットおよび照合順序が CREATE DATABASE ステートメントで指定されていない場合にデフォルト値として使用されます。 これらにほかの用途はありません。</p> <p>現在のサーバー文字セットおよび照合順序は、character_set_server および collation_server システム変数の値で判別できます。 これらの変数は実行時に変更できます。 ~(公式ドキュメントより引用)~</p></blockquote> <p><a href="https://dev.mysql.com/doc/refman/8.0/ja/charset-server.html">https://dev.mysql.com/doc/refman/8.0/ja/charset-server.html</a></p> <h2 id="collation_database">collation_database</h2> <p>こちらはデフォルトデータベースに対するcollationの設定で、デフォルトデータベースが切り替わる度に変数に値を設定しているようです。記事内の後にでてくる検証では触れませんが、重要なパラメーターではあるので紹介しました。</p> <blockquote><p>デフォルトのデータベースに対する文字セットと照合順序は、character_set_database および collation_database システム変数の値から判別できます。 デフォルトのデータベースが変わるたびに、サーバーはこれらの変数を設定します。 デフォルトのデータベースがない場合、変数は、character_set_server および collation_server という対応するサーバーレベルのシステム変数と同値になります。 ~(公式ドキュメントより引用)~</p></blockquote> <p><a href="https://dev.mysql.com/doc/refman/8.0/ja/charset-database.html">https://dev.mysql.com/doc/refman/8.0/ja/charset-database.html</a></p> <h2 id="default_collation_for_utf8mb4MySQL8のみ">default_collation_for_utf8mb4(MySQL8のみ)</h2> <p>utf8mb4が指定された際に利用されるcollationの指定です。例えば、<code>CREATE DATABASE</code>などのDDLで明示的に <code>CHARACTER SET=utf8mb4</code>のみ指定してあった場合は、この設定値が利用されます。 <a href="https://dev.mysql.com/doc/refman/8.0/ja/server-system-variables.html">https://dev.mysql.com/doc/refman/8.0/ja/server-system-variables.html</a></p> <h2 id="MySQLのversionごとの初期値">MySQLのversionごとの初期値</h2> <p>utf8mb4の場合、以下のような初期値設定になっています。</p> <table> <thead> <tr> <th> MysqlVersion </th> <th> collation_server </th> <th> default_collation_for_utf8mb4 </th> </tr> </thead> <tbody> <tr> <td> MySQL5.7 </td> <td> utf8mb4_general_ci </td> <td>パラメーター無し </td> </tr> <tr> <td> MySQL8 </td> <td> utf8mb4_0900_as_ci </td> <td> utf8mb4_0900_ai_ci </td> </tr> </tbody> </table> <h1 id="検証">検証</h1> <p>ここまでcollationを決める値やMySQL versionごとの初期値を見てきました。ここからは実際の挙動を一つずつ検証していきます。</p> <p>利用するdocker-compose設定</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">version</span><span class="synSpecial">:</span> <span class="synConstant">'3.1'</span> <span class="synIdentifier">services</span><span class="synSpecial">:</span> <span class="synIdentifier">mysql57</span><span class="synSpecial">:</span> <span class="synIdentifier">image</span><span class="synSpecial">:</span> mysql:5.7 <span class="synIdentifier">command</span><span class="synSpecial">:</span> mysqld --character-set-server=utf8mb4 <span class="synIdentifier">ports</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">&quot;3306:3306&quot;</span> <span class="synIdentifier">environment</span><span class="synSpecial">:</span> <span class="synIdentifier">MYSQL_xxx</span><span class="synSpecial">:</span> ... <span class="synIdentifier">mysql80</span><span class="synSpecial">:</span> <span class="synIdentifier">image</span><span class="synSpecial">:</span> mysql:8.0 <span class="synIdentifier">command</span><span class="synSpecial">:</span> mysqld --character-set-server=utf8mb4 <span class="synIdentifier">ports</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">&quot;3307:3306&quot;</span> <span class="synIdentifier">environment</span><span class="synSpecial">:</span> <span class="synIdentifier">MYSQL_xxx</span><span class="synSpecial">:</span> ... </pre> <h2 id="MySQL57">MySQL5.7</h2> <p>検証version: 5.7.44</p> <p>最初にMySQL5.7の検証した内容を見ていきます。</p> <h3 id="起動時のcollationの設定">起動時のcollationの設定</h3> <p>標準である<code>utf8mb4_general_ci</code>になっています。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; show variables <span class="synStatement">like</span> <span class="synSpecial">&quot;</span><span class="synConstant">collation_%</span><span class="synSpecial">&quot;</span>; +<span class="synComment">----------------------+--------------------+</span> | Variable_name | Value | +<span class="synComment">----------------------+--------------------+</span> | collation_connection | utf8mb4_general_ci | | collation_database | utf8mb4_general_ci | | collation_server | utf8mb4_general_ci | +<span class="synComment">----------------------+--------------------+</span> <span class="synConstant">3</span> <span class="synSpecial">rows</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">06</span> sec) </pre> <h3 id="データベースを作成collationを確認">データベースを作成・collationを確認</h3> <p>データべースを作成しcollationを確認しました。<code>SHOW CREATE DATABASE</code>に表示されているDDLをみるとcollationの情報(<code>COLLATE utf8mb4_general_ci</code>)は含まれていません。 ただし、information_schemaから確認すると<code>collation_server</code>の値で設定されているようでした。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; CREATE DATABASE test_mysql57; Query OK, <span class="synConstant">1</span> <span class="synSpecial">row</span> affected (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; SHOW CREATE DATABASE test_mysql57; +<span class="synComment">--------------+--------------------------------------------------------------------------+</span> | Database | Create Database | +<span class="synComment">--------------+--------------------------------------------------------------------------+</span> | test_mysql57 | CREATE DATABASE `test_mysql57` <span class="synComment">/*!40100 DEFAULT CHARACTER SET utf8mb4 */</span> | +<span class="synComment">--------------+--------------------------------------------------------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; SELECT DEFAULT_COLLATION_NAME <span class="synSpecial">FROM</span> information_schema. schemata <span class="synSpecial">WHERE</span> SCHEMA_NAME = <span class="synSpecial">'</span><span class="synConstant">test_mysql57</span><span class="synSpecial">'</span>; +<span class="synComment">------------------------+</span> | DEFAULT_COLLATION_NAME | +<span class="synComment">------------------------+</span> | utf8mb4_general_ci | +<span class="synComment">------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">02</span> sec) </pre> <p>次に<code>collation_server</code>の値とは異なるcollationを指定し作成します。結果、指定したcollationで作成されました。また今回はcollationの情報が<code>SHOW CREATE</code>の結果に含まれています。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; CREATE DATABASE test_mysql57_2 COLLATE utf8mb4_unicode_ci; Query OK, <span class="synConstant">1</span> <span class="synSpecial">row</span> affected (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; SHOW CREATE DATABASE test_mysql57_2; +<span class="synComment">----------------+-------------------------------------------------------------------------------------------------------+</span> | Database | Create Database | +<span class="synComment">----------------+-------------------------------------------------------------------------------------------------------+</span> | test_mysql57_2 | CREATE DATABASE `test_mysql57_2` <span class="synComment">/*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */</span> | +<span class="synComment">----------------+-------------------------------------------------------------------------------------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">00</span> sec) mysql&gt; SELECT DEFAULT_COLLATION_NAME <span class="synSpecial">FROM</span> information_schema.schemata <span class="synSpecial">WHERE</span> SCHEMA_NAME = <span class="synSpecial">'</span><span class="synConstant">test_mysql57_2</span><span class="synSpecial">'</span>; +<span class="synComment">------------------------+</span> | DEFAULT_COLLATION_NAME | +<span class="synComment">------------------------+</span> | utf8mb4_unicode_ci | +<span class="synComment">------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">00</span> sec) </pre> <p>次にCREATE文で明示的に<code>collation_server</code>の値を指定して実行しました。結果、collation・<code>SHOW CREATE</code>の両方が最初と同じ結果になりました。このことからMySQL5.7では、標準である<code>utf8mb4_general_ci</code>と同じ場合は、collationの情報は省略する挙動になりそうと推測できます。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; CREATE DATABASE test_mysql57_3 COLLATE utf8mb4_general_ci; Query OK, <span class="synConstant">1</span> <span class="synSpecial">row</span> affected (<span class="synConstant">0</span>.<span class="synConstant">00</span> sec) mysql&gt; SHOW CREATE DATABASE test_mysql57_3; +<span class="synComment">----------------+----------------------------------------------------------------------------+</span> | Database | Create Database | +<span class="synComment">----------------+----------------------------------------------------------------------------+</span> | test_mysql57_3 | CREATE DATABASE `test_mysql57_3` <span class="synComment">/*!40100 DEFAULT CHARACTER SET utf8mb4 */</span> | +<span class="synComment">----------------+----------------------------------------------------------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; SELECT DEFAULT_COLLATION_NAME <span class="synSpecial">FROM</span> information_schema.schemata <span class="synSpecial">WHERE</span> SCHEMA_NAME = <span class="synSpecial">'</span><span class="synConstant">test_mysql57_3</span><span class="synSpecial">'</span>; +<span class="synComment">------------------------+</span> | DEFAULT_COLLATION_NAME | +<span class="synComment">------------------------+</span> | utf8mb4_general_ci | +<span class="synComment">------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) </pre> <p>一つ前の推測があっているか確認するために、MySQLのcollation_serverを<code>utf8mb4_unicode_ci</code>へ変更し、再起動後にテーブルを作成すると<code>utf8mb4_unicode_ci</code>のcollationの情報は<code>SHOW CREATE</code>の結果に含まれていました。推測はあってそうにみえます。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; show variables <span class="synStatement">like</span> <span class="synSpecial">&quot;</span><span class="synConstant">collation_%</span><span class="synSpecial">&quot;</span>; +<span class="synComment">----------------------+--------------------+</span> | Variable_name | Value | +<span class="synComment">----------------------+--------------------+</span> | collation_connection | utf8mb4_general_ci | | collation_database | utf8mb4_unicode_ci | | collation_server | utf8mb4_unicode_ci | +<span class="synComment">----------------------+--------------------+</span> <span class="synConstant">3</span> <span class="synSpecial">rows</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">07</span> sec) mysql&gt; CREATE DATABASE test_mysql57_4; Query OK, <span class="synConstant">1</span> <span class="synSpecial">row</span> affected (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; SHOW CREATE DATABASE test_mysql57_4; +<span class="synComment">----------------+-------------------------------------------------------------------------------------------------------+</span> | Database | Create Database | +<span class="synComment">----------------+-------------------------------------------------------------------------------------------------------+</span> | test_mysql57_4 | CREATE DATABASE `test_mysql57_4` <span class="synComment">/*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */</span> | +<span class="synComment">----------------+-------------------------------------------------------------------------------------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) </pre> <h3 id="テーブルを作成collationを確認">テーブルを作成・collationを確認</h3> <p>テーブルを作成する際に利用されるcollationは、データベースを作成した際に設定したcollationが利用されます。その前提で、前の手順で作成した<code>test_mysql57</code>データベース(utf8mb4_general_ci)を利用してテーブル作成時に設定されるcollationについてみていきます。</p> <p>まず最初にテーブルをcollation指定無しで作成します。この場合<code>collation_database</code>の値がそのまま利用されました。こちらも<code>SHOW CREATE</code>の結果にcollationの情報は含まれていません。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; use test_mysql57; Database changed mysql&gt; CREATE <span class="synSpecial">TABLE</span> test_mysql57_table1 (id <span class="synType">int</span>(<span class="synConstant">11</span>)); Query OK, <span class="synConstant">0</span> <span class="synSpecial">rows</span> affected (<span class="synConstant">0</span>.<span class="synConstant">03</span> sec) mysql&gt; SHOW CREATE <span class="synSpecial">TABLE</span> test_mysql57_table1; +<span class="synComment">---------------------+----------------------------------------------------------------------------------------------------------+</span> | <span class="synSpecial">Table</span> | Create <span class="synSpecial">Table</span> | +<span class="synComment">---------------------+----------------------------------------------------------------------------------------------------------+</span> | test_mysql57_table1 | CREATE <span class="synSpecial">TABLE</span> `test_mysql57_table1` ( `id` <span class="synType">int</span>(<span class="synConstant">11</span>) <span class="synSpecial">DEFAULT</span> <span class="synSpecial">NULL</span> ) ENGINE=InnoDB <span class="synSpecial">DEFAULT</span> CHARSET=utf8mb4 | +<span class="synComment">---------------------+----------------------------------------------------------------------------------------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; SELECT table_name, table_collation <span class="synSpecial">FROM</span> information_schema.tables <span class="synSpecial">WHERE</span> table_schema = <span class="synSpecial">'</span><span class="synConstant">test_mysql57</span><span class="synSpecial">'</span> <span class="synStatement">AND</span> table_name = <span class="synSpecial">'</span><span class="synConstant">test_mysql57_table1</span><span class="synSpecial">'</span>; +<span class="synComment">---------------------+--------------------+</span> | table_name | table_collation | +<span class="synComment">---------------------+--------------------+</span> | test_mysql57_table1 | utf8mb4_general_ci | +<span class="synComment">---------------------+--------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) </pre> <p>次に<code>collation_database</code>の値とは異なるcollation指定で実行します。結果、指定されたcollationが適用されました。またcollationの情報も結果に含まれていました。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; use test_mysql57; Database changed mysql&gt; CREATE <span class="synSpecial">TABLE</span> test_mysql57_table2 (id <span class="synType">int</span>(<span class="synConstant">11</span>)) <span class="synSpecial">DEFAULT</span> COLLATE=utf8mb4_unicode_ci; Query OK, <span class="synConstant">0</span> <span class="synSpecial">rows</span> affected (<span class="synConstant">0</span>.<span class="synConstant">03</span> sec) mysql&gt; SHOW CREATE <span class="synSpecial">TABLE</span> test_mysql57_table2; +<span class="synComment">---------------------+-------------------------------------------------------------------------------------------------------------------------------------+</span> | <span class="synSpecial">Table</span> | Create <span class="synSpecial">Table</span> | +<span class="synComment">---------------------+-------------------------------------------------------------------------------------------------------------------------------------+</span> | test_mysql57_table2 | CREATE <span class="synSpecial">TABLE</span> `test_mysql57_table2` ( `id` <span class="synType">int</span>(<span class="synConstant">11</span>) <span class="synSpecial">DEFAULT</span> <span class="synSpecial">NULL</span> ) ENGINE=InnoDB <span class="synSpecial">DEFAULT</span> CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci | +<span class="synComment">---------------------+-------------------------------------------------------------------------------------------------------------------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; SELECT table_name, table_collation <span class="synSpecial">FROM</span> information_schema.tables <span class="synSpecial">WHERE</span> table_schema = <span class="synSpecial">'</span><span class="synConstant">test_mysql57</span><span class="synSpecial">'</span> <span class="synStatement">AND</span> table_name = <span class="synSpecial">'</span><span class="synConstant">test_mysql57_table2</span><span class="synSpecial">'</span>; +<span class="synComment">---------------------+--------------------+</span> | table_name | table_collation | +<span class="synComment">---------------------+--------------------+</span> | test_mysql57_table2 | utf8mb4_unicode_ci | +<span class="synComment">---------------------+--------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) </pre> <p>MySQL5.7は<code>SHOW CREATE</code>で表示される結果が変わることを除いて、かなりシンプルな挙動で動作してくれることがわかりました。</p> <h2 id="MySQL80">MySQL8.0</h2> <p>検証version: 8.0.35</p> <p>次にMySQL8.0での挙動を見ていきます。先に一部結論になりますが、基本的な挙動はMySQL5.7と同じになるので重複する検証は省きます。ただしMySQL8.0の場合は、<code>collation_server</code>または<code>collation_database</code>の値が<code>default_collation_for_utf8mb4</code>と違う場合にMySQL5.7の場合と異なる挙動をするので、そのケースのみ検証を行っていきます。</p> <h3 id="起動時のcollationの設定-1">起動時のcollationの設定</h3> <p>標準である<code>utf8mb4_0900_ai_ci</code>で起動しています。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; show variables <span class="synStatement">like</span> <span class="synSpecial">&quot;</span><span class="synConstant">collation_%</span><span class="synSpecial">&quot;</span>; +<span class="synComment">----------------------+--------------------+</span> | Variable_name | Value | +<span class="synComment">----------------------+--------------------+</span> | collation_connection | utf8mb4_general_ci | | collation_database | utf8mb4_0900_ai_ci | | collation_server | utf8mb4_0900_ai_ci | +<span class="synComment">----------------------+--------------------+</span> <span class="synConstant">3</span> <span class="synSpecial">rows</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">03</span> sec) mysql&gt; show variables <span class="synStatement">like</span> <span class="synSpecial">&quot;</span><span class="synConstant">default_collation_%</span><span class="synSpecial">&quot;</span>; +<span class="synComment">-------------------------------+--------------------+</span> | Variable_name | Value | +<span class="synComment">-------------------------------+--------------------+</span> | default_collation_for_utf8mb4 | utf8mb4_0900_ai_ci | +<span class="synComment">-------------------------------+--------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) </pre> <h3 id="default_collation_for_utf8mb4をutf8mb4_general_ciへ変更">default_collation_for_utf8mb4をutf8mb4_general_ciへ変更</h3> <p>このパラメーターは<code>SET PERSIST</code>で設定することができ、DB再起動後も値を保持することができるパラメーターになります。</p> <p>注意したい点としてはAWS RDSを利用している場合です。RDSでは<code>SET PERSIST</code>をサポートしていません。またパラメーターグループでも<code>default_collation_for_utf8mb4</code>の変更ができないため、基本は<code>utf8mb4_0900_ai_ci</code>の値で固定になるかとおもいます。</p> <p><a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/MySQL.Concepts.FeatureSupport.html">https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/MySQL.Concepts.FeatureSupport.html</a></p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; <span class="synStatement">SET</span> PERSIST default_collation_for_utf8mb4=utf8mb4_general_ci; Query OK, <span class="synConstant">0</span> <span class="synSpecial">rows</span> affected, <span class="synConstant">1</span> warning (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; show variables <span class="synStatement">like</span> <span class="synSpecial">&quot;</span><span class="synConstant">default_collation_%</span><span class="synSpecial">&quot;</span>; +<span class="synComment">-------------------------------+--------------------+</span> | Variable_name | Value | +<span class="synComment">-------------------------------+--------------------+</span> | default_collation_for_utf8mb4 | utf8mb4_general_ci | +<span class="synComment">-------------------------------+--------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) </pre> <h3 id="データベースを作成collationを確認-1">データベースを作成・collationを確認</h3> <p>まずはcollation, charset未指定パターンです。結果としては<code>collation_server</code>の値がそのまま使われています。また<code>SHOW CREATE</code>の結果には標準のcollationであったとしても明示的に出力されるようになりました。 <code>CHARACTER SET utf8mb4</code>という記述はありますが、このケースでは<code>default_collation_for_utf8mb4</code>の値が適用されないようでした。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; CREATE DATABASE test_mysql8_1; Query OK, <span class="synConstant">1</span> <span class="synSpecial">row</span> affected (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; SHOW CREATE DATABASE test_mysql8_1; +<span class="synComment">---------------+-----------------------------------------------------------------------------------------------------------------------------------------+</span> | Database | Create Database | +<span class="synComment">---------------+-----------------------------------------------------------------------------------------------------------------------------------------+</span> | test_mysql8_1 | CREATE DATABASE `test_mysql8_1` <span class="synComment">/*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */</span> <span class="synComment">/*!80016 DEFAULT ENCRYPTION='N' */</span> | +<span class="synComment">---------------+-----------------------------------------------------------------------------------------------------------------------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; SELECT DEFAULT_COLLATION_NAME <span class="synSpecial">FROM</span> information_schema. schemata <span class="synSpecial">WHERE</span> SCHEMA_NAME = <span class="synSpecial">'</span><span class="synConstant">test_mysql8_1</span><span class="synSpecial">'</span>; +<span class="synComment">------------------------+</span> | DEFAULT_COLLATION_NAME | +<span class="synComment">------------------------+</span> | utf8mb4_0900_ai_ci | +<span class="synComment">------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) </pre> <p>次に<code>CHARACTER SET utf8mb4</code>を明示的にしてみます。結果、<code>default_collation_for_utf8mb4</code>の値が適用されるようになりました。このパラメーターは、charsetでutf8mb4を明示的に渡した場合のみ適用されるようです。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; CREATE DATABASE test_mysql8_2 <span class="synType">CHARACTER</span> <span class="synStatement">SET</span> utf8mb4; Query OK, <span class="synConstant">1</span> <span class="synSpecial">row</span> affected (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; SHOW CREATE DATABASE test_mysql8_2; +<span class="synComment">---------------+-----------------------------------------------------------------------------------------------------------------------------------------+</span> | Database | Create Database | +<span class="synComment">---------------+-----------------------------------------------------------------------------------------------------------------------------------------+</span> | test_mysql8_2 | CREATE DATABASE `test_mysql8_2` <span class="synComment">/*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */</span> <span class="synComment">/*!80016 DEFAULT ENCRYPTION='N' */</span> | +<span class="synComment">---------------+-----------------------------------------------------------------------------------------------------------------------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">00</span> sec) mysql&gt; SELECT DEFAULT_COLLATION_NAME <span class="synSpecial">FROM</span> information_schema. schemata <span class="synSpecial">WHERE</span> SCHEMA_NAME = <span class="synSpecial">'</span><span class="synConstant">test_mysql8_2</span><span class="synSpecial">'</span>; +<span class="synComment">------------------------+</span> | DEFAULT_COLLATION_NAME | +<span class="synComment">------------------------+</span> | utf8mb4_general_ci | +<span class="synComment">------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) </pre> <p>最後に<code>CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci</code>を指定してみます。結果、明示的に渡した<code>utf8mb4_0900_ai_ci</code>の値が適用されました。また今回も<code>SHOW CREATE</code>の結果にはcollationの値がでていることから、MySQL8.0では必ず含まれるようになったのかもしれません。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; CREATE DATABASE test_mysql8_3 <span class="synType">CHARACTER</span> <span class="synStatement">SET</span> utf8mb4 COLLATE utf8mb4_0900_ai_ci; Query OK, <span class="synConstant">1</span> <span class="synSpecial">row</span> affected (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; SHOW CREATE DATABASE test_mysql8_3; +<span class="synComment">---------------+-----------------------------------------------------------------------------------------------------------------------------------------+</span> | Database | Create Database | +<span class="synComment">---------------+-----------------------------------------------------------------------------------------------------------------------------------------+</span> | test_mysql8_3 | CREATE DATABASE `test_mysql8_3` <span class="synComment">/*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */</span> <span class="synComment">/*!80016 DEFAULT ENCRYPTION='N' */</span> | +<span class="synComment">---------------+-----------------------------------------------------------------------------------------------------------------------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">00</span> sec) mysql&gt; SELECT DEFAULT_COLLATION_NAME <span class="synSpecial">FROM</span> information_schema. schemata <span class="synSpecial">WHERE</span> SCHEMA_NAME = <span class="synSpecial">'</span><span class="synConstant">test_mysql8_3</span><span class="synSpecial">'</span>; +<span class="synComment">------------------------+</span> | DEFAULT_COLLATION_NAME | +<span class="synComment">------------------------+</span> | utf8mb4_0900_ai_ci | +<span class="synComment">------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) </pre> <h3 id="テーブルを作成しcollationを確認">テーブルを作成し、collationを確認</h3> <p>前の手順で作成した<code>test_mysql8_1</code>データベース(utf8mb4_0900_ai_ci)を利用してテーブル作成時に設定されるcollationについてみていきます。 <code>default_collation_for_utf8mb4</code>の値は、<code>utf8mb4_general_ci</code>のままです。 検証パターンはデータベースの時と同じでためしましたが、結果も同じになりました。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; use test_mysql8_1; Database changed mysql&gt; CREATE <span class="synSpecial">TABLE</span> test_mysql8_1_table1 (id <span class="synType">int</span>(<span class="synConstant">11</span>)); Query OK, <span class="synConstant">0</span> <span class="synSpecial">rows</span> affected, <span class="synConstant">1</span> warning (<span class="synConstant">0</span>.<span class="synConstant">03</span> sec) mysql&gt; SHOW CREATE <span class="synSpecial">TABLE</span> test_mysql8_1_table1; +<span class="synComment">----------------------+----------------------------------------------------------------------------------------------------------------------------------+</span> | <span class="synSpecial">Table</span> | Create <span class="synSpecial">Table</span> | +<span class="synComment">----------------------+----------------------------------------------------------------------------------------------------------------------------------+</span> | test_mysql8_1_table1 | CREATE <span class="synSpecial">TABLE</span> `test_mysql8_1_table1` ( `id` <span class="synType">int</span> <span class="synSpecial">DEFAULT</span> <span class="synSpecial">NULL</span> ) ENGINE=InnoDB <span class="synSpecial">DEFAULT</span> CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci | +<span class="synComment">----------------------+----------------------------------------------------------------------------------------------------------------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">02</span> sec) mysql&gt; SELECT table_name, table_collation <span class="synSpecial">FROM</span> information_schema.tables <span class="synSpecial">WHERE</span> table_schema = <span class="synSpecial">'</span><span class="synConstant">test_mysql8_1</span><span class="synSpecial">'</span> <span class="synStatement">AND</span> table_name = <span class="synSpecial">'</span><span class="synConstant">test_mysql8_1_table1</span><span class="synSpecial">'</span>; +<span class="synComment">----------------------+--------------------+</span> | TABLE_NAME | TABLE_COLLATION | +<span class="synComment">----------------------+--------------------+</span> | test_mysql8_1_table1 | utf8mb4_0900_ai_ci | +<span class="synComment">----------------------+--------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">00</span> sec) </pre> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; CREATE <span class="synSpecial">TABLE</span> test_mysql8_1_table2 (id <span class="synType">int</span>(<span class="synConstant">11</span>)) <span class="synSpecial">DEFAULT</span> <span class="synType">CHARACTER</span> <span class="synStatement">SET</span> utf8mb4; Query OK, <span class="synConstant">0</span> <span class="synSpecial">rows</span> affected, <span class="synConstant">1</span> warning (<span class="synConstant">0</span>.<span class="synConstant">03</span> sec) mysql&gt; SHOW CREATE <span class="synSpecial">TABLE</span> test_mysql8_1_table2; +<span class="synComment">----------------------+----------------------------------------------------------------------------------------------------------------------------------+</span> | <span class="synSpecial">Table</span> | Create <span class="synSpecial">Table</span> | +<span class="synComment">----------------------+----------------------------------------------------------------------------------------------------------------------------------+</span> | test_mysql8_1_table2 | CREATE <span class="synSpecial">TABLE</span> `test_mysql8_1_table2` ( `id` <span class="synType">int</span> <span class="synSpecial">DEFAULT</span> <span class="synSpecial">NULL</span> ) ENGINE=InnoDB <span class="synSpecial">DEFAULT</span> CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci | +<span class="synComment">----------------------+----------------------------------------------------------------------------------------------------------------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) mysql&gt; SELECT table_name, table_collation <span class="synSpecial">FROM</span> information_schema.tables <span class="synSpecial">WHERE</span> table_schema = <span class="synSpecial">'</span><span class="synConstant">test_mysql8_1</span><span class="synSpecial">'</span> <span class="synStatement">AND</span> table_name = <span class="synSpecial">'</span><span class="synConstant">test_mysql8_1_table2</span><span class="synSpecial">'</span>; +<span class="synComment">----------------------+--------------------+</span> | TABLE_NAME | TABLE_COLLATION | +<span class="synComment">----------------------+--------------------+</span> | test_mysql8_1_table2 | utf8mb4_general_ci | +<span class="synComment">----------------------+--------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">00</span> sec) </pre> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; CREATE <span class="synSpecial">TABLE</span> test_mysql8_1_table3 (id <span class="synType">int</span>(<span class="synConstant">11</span>)) <span class="synSpecial">DEFAULT</span> <span class="synType">CHARACTER</span> <span class="synStatement">SET</span> utf8mb4 COLLATE utf8mb4_0900_ai_ci; Query OK, <span class="synConstant">0</span> <span class="synSpecial">rows</span> affected, <span class="synConstant">1</span> warning (<span class="synConstant">0</span>.<span class="synConstant">03</span> sec) mysql&gt; SHOW CREATE <span class="synSpecial">TABLE</span> test_mysql8_1_table3; +<span class="synComment">----------------------+----------------------------------------------------------------------------------------------------------------------------------+</span> | <span class="synSpecial">Table</span> | Create <span class="synSpecial">Table</span> | +<span class="synComment">----------------------+----------------------------------------------------------------------------------------------------------------------------------+</span> | test_mysql8_1_table3 | CREATE <span class="synSpecial">TABLE</span> `test_mysql8_1_table3` ( `id` <span class="synType">int</span> <span class="synSpecial">DEFAULT</span> <span class="synSpecial">NULL</span> ) ENGINE=InnoDB <span class="synSpecial">DEFAULT</span> CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci | +<span class="synComment">----------------------+----------------------------------------------------------------------------------------------------------------------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">00</span> sec) mysql&gt; SELECT table_name, table_collation <span class="synSpecial">FROM</span> information_schema.tables <span class="synSpecial">WHERE</span> table_schema = <span class="synSpecial">'</span><span class="synConstant">test_mysql8_1</span><span class="synSpecial">'</span> <span class="synStatement">AND</span> table_name = <span class="synSpecial">'</span><span class="synConstant">test_mysql8_1_table3</span><span class="synSpecial">'</span>; +<span class="synComment">----------------------+--------------------+</span> | TABLE_NAME | TABLE_COLLATION | +<span class="synComment">----------------------+--------------------+</span> | test_mysql8_1_table3 | utf8mb4_0900_ai_ci | +<span class="synComment">----------------------+--------------------+</span> <span class="synConstant">1</span> <span class="synSpecial">row</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) </pre> <p>検証によってMySQL8.0では、<code>COLLATE utf8mb4</code>の指定がある場合のみ<code>default_collation_for_utf8mb4</code>の値に注意して見ていく必要があることがわかりました。</p> <h1 id="検証を経ての雑感">検証を経ての雑感</h1> <p>検証結果のまとめは、冒頭のTL;DRにまとめました。以下検証を通しての雑感になります。</p> <ul> <li>実際の開発/運用では、MySQL5.7(またはそれ以前)な環境においてMySQL側がもっているデフォルトの値に任せて明示的に指定しないということは往々に起こる気がしている</li> <li>MySQL5.7では<code>utf8mb4_general_ci</code>の場合はcollationが省略される挙動だったので、<code>SHOW CREATE</code>の結果に依存したなんらかの仕組みを作っていた場合には、MySQL8.0では意図しない挙動になってしまい問題が発生するなどありそう</li> <li>複数のcollationの設定が混ざってしまうとSQLが期待しない結果になることが多々あるので注意が必要になる。MySQL5.x -> 8.0への移行の際には、現在のCOLLATEの指定を確認して、移行後どんな値を選択したいのかを決め影響範囲を一つずつ確認していく必要がありそう</li> </ul> <h1 id="最後に">最後に</h1> <p>いかがでしたでしょうか。今回はcollationが決定される挙動を確認するための検証がメインの記事でした。次回は検証から得られた知識をもって、Rails x MySQL5.7 -> Rails x MySQL8.0への移行に気をつけるべき観点をみていきたいと思います。</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> Maco_Tasu あとから参画したメンバーが助かったmablの運用ガイドライン hatenablog://entry/6801883189065944132 2023-12-20T07:00:00+09:00 2023-12-20T14:00:55+09:00 はじめに こんにちは、hacomono SETチームのモーリーです。 こちらはmabl Advent Calendar 2023 20日目の記事です。 hacomonoではリグレッションテストをmablで自動化しており、mablのテスト数は200にも届きそうな状態です。この記事では、新しくhacomono SETチームに参画した私が、mablでリグレッションテストの自動化に取り組む過程で助かったmablの運用ガイドラインについてご紹介します。 hacomonoのmabl関連資料の紹介 hacomonoでは下記のmabl資料を用意しており、新しいメンバーは資料を読み進めながらキャッチアップと実際… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231218/20231218144343.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1 id="はじめに">はじめに</h1> <p>こんにちは、<a href="https://twitter.com/m39ryo">hacomono SETチームのモーリー</a>です。</p> <p>こちらは<strong><strong>mabl Advent Calendar 2023</strong></strong> 20日目の記事です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fadvent-calendar%2F2023%2Fmabl" title="mablのカレンダー | Advent Calendar 2023 - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> <p>hacomonoではリグレッションテストをmablで自動化しており、mablのテスト数は200にも届きそうな状態です。この記事では、新しくhacomono SETチームに参画した私が、mablでリグレッションテストの自動化に取り組む過程で助かったmablの運用ガイドラインについてご紹介します。</p> <h1 id="hacomonoのmabl関連資料の紹介">hacomonoのmabl関連資料の紹介</h1> <p>hacomonoでは下記のmabl資料を用意しており、新しいメンバーは資料を読み進めながらキャッチアップと実際にテストを作成するようにしています。</p> <p><figure class="figure-image figure-image-fotolife" title="mabl運用ガイドライン"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231211/20231211182414.png" width="1200" height="1026" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>mabl運用ガイドライン</figcaption></figure></p> <h2 id="1-自動テスト作成環境の構築方法"><strong>1. 自動テスト作成環境の構築方法</strong></h2> <ul> <li>開発環境の構築方法</li> <li>自動テスト用初期データのインポート方法</li> <li>ローカル実行のやり方</li> </ul> <h2 id="2-運用ガイドライン"><strong>2. 運用ガイドライン</strong></h2> <ul> <li>命名規則 <ul> <li>ブランチ名</li> <li>テスト名</li> <li>フロー名</li> </ul> </li> <li>テスト実装フロー <ul> <li>テストの新規作成方法</li> <li>フローの作成方法</li> <li>エコーの使い方</li> <li>レビュー依頼</li> <li>マージ後の作業</li> </ul> </li> </ul> <h1 id="mablのテストを動かすまでに助かったポイント">mablのテストを動かすまでに助かったポイント</h1> <h3 id="自動テスト用初期データのインポート">自動テスト用<strong>初期データのインポート</strong></h3> <p>通常の開発環境構築手順に加え、hacomonoでは自動テストのための初期データを簡単にセットアップする方法が提供されています。</p> <p>これにより、新しいメンバーでも1コマンドで環境を初期化し、自動テスト環境を容易に再現できるので、容易に環境の準備が終わりすぐに実装に取りかかれました。</p> <h3 id="ローカル実行のやり方"><strong>ローカル実行のやり方</strong></h3> <p>mablではクラウド実行とローカル実行があります。この中でローカル実行は何度でも可能ですが、クラウド実行は契約上限までとなります。</p> <p>実行の種類の情報とやり方がまとまっているおかげで、うっかりクラウド実行の実行回数を使うことなく、すぐにテストの作成が始められました。</p> <p><a href="https://help.mabl.com/docs/test-execution-ja">mablのローカル実行の詳細はこちら</a></p> <h1 id="運用ガイドラインの助かったポイント"><strong>運用ガイドラインの助かったポイント</strong></h1> <p>とてもありがたいことに複数人での自動テスト開発を想定した運用ガイドラインがあります。 ガイドラインでは、ブランチ名からテスト名、フロー名まで命名規則が決められており、単純に一覧で見やすくなるだけでなく、新しいテスト作成者にとって命名で悩む必要が少ないことでも助かっています。</p> <p>ここでは、特に助かっているポイントについて参画者視点で紹介させていただきます。</p> <h2 id="1-命名規則"><strong>1. 命名規則</strong></h2> <p><strong>ブランチ名</strong></p> <p><figure class="figure-image figure-image-fotolife" title="mabl ブランチ名命名規則"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231211/20231211182459.png" width="1200" height="471" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>mabl ブランチ名命名規則</figcaption></figure></p> <p>ブランチ名にはテスト管理番号と作業者の名前で構成するようになっています。 これにより誰が何の作業しているのかわかりやすくなっていて、ブランチの乱立を防いでいます。</p> <p>余談ですが、mablのサポートの方が調査される際にブランチを作成されることもあるみたいですが、それもひと目で判別がつきます。</p> <p><strong>フロー名</strong></p> <p><figure class="figure-image figure-image-fotolife" title="mabl フロー名命名規則"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231211/20231211182530.png" width="1200" height="1171" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>mabl フロー名命名規則</figcaption></figure></p> <p>フロー名には画面のパスを接頭辞として付与していて、フローを探すことや、重複したフローを作成してしまうことの抑制にも役立っています。 細かい書式指定もあり、たとえば画面名を表す場合は「」で囲う、画面上の文言を表す場合は””で囲う、などがあります。</p> <p>慣れるまで少し大変ですが、あとから検索したりするときにとてもわかりやすいです。</p> <h2 id="2-フローの作成方法">2. <strong>フローの作成方法</strong></h2> <p>運用ガイドラインには、基本的にテストはフローで構成する方針が掲載されています。 既にあるフローを組み合わせることでテストの実装が容易になり フローが充実しているおかげで新しいメンバーでも効率的に作業できています。</p> <h1 id="もうちょっとなポイント"><strong>もうちょっとなポイント</strong></h1> <p>助かるポイントと同時に、一部改善の余地がある点も挙げてみます。</p> <h3 id="1-目的のフローが見つかりにくい"><strong>1. 目的のフローが見つかりにくい</strong></h3> <p>運用によりフロー名が統一されていて検索しやすい状態ではありますが、残念ながらmablでフローを複数キーワードできないことと、フローの数がとても多くなったことで目的のフローを探すときに少し手間がかかります。</p> <p>画面パスで検索したあとにフローを目視で探すのがつらいところで、ある程度の慣れが必要です。 その点に関しては便利なフローの紹介資料もあり、利用頻度の高いフローはキャッチアップできるようになっています。</p> <p><figure class="figure-image figure-image-fotolife" title="mablで作成済みの便利フローについて"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231211/20231211182604.png" width="1200" height="672" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>mablで作成済みの便利フローについて</figcaption></figure></p> <h3 id="2-フローを汎用化させるコストが高い"><strong>2.</strong> フローを汎用化させるコストが高い</h3> <p>フローは1箇所だけでなく様々な箇所で利用されます。そのためフローにするだけでは、このテストではうまく動いても、あのテストではうまく動かない、というようなことがあります。</p> <p>操作の記録を利用した場合にこの問題が出ることが多く、<a href="https://help.mabl.com/docs/configuring-find-steps-ja">ステップの検索の設定から属性を選択すること</a>で解決することもありますが、そうでない場合もあります。 そのときはXPathを指定するようステップを作り直したり、javaScriptスニペットを利用したりします。この作業は容易ではないので、場合によってはもう一つフローを作ってしまうのも手かもしれません。</p> <h1 id="あるとよかったかも運用ガイドライン">あるとよかったかも運用ガイドライン</h1> <p>あとあとわかって苦労しているポイントについても紹介します。</p> <h3 id="1-フローの適切なステップ数">1. <strong>フローの適切なステップ数</strong></h3> <p>フローのステップ数が多い場合、目的のフローを選んだつもりでも目的外の操作を含んでいたり、不要な条件分岐があったり、何に使うパラメーターかわからず操作内容を確認するのに必要以上に時間がかかったりします。</p> <p>hacomonoではテスト作成後にレビューをするようにしていますが、全てのフローをチェックするのは負荷が高いので、フローのステップ数上限を決めて分割するなどのガイドラインを定めることは役立つかもしれません。</p> <h3 id="2-変数のスコープ">2. 変数のスコープ</h3> <p>mablには環境変数やテスト内の変数などがあります。ただし、mablの変数のスコープは少し自由な使い方ができてしまいます。例えば変数Aを生成したあとに、パラメーターを使わずにフローの中で直接変数Aを利用したり、上書きしたりできます。環境変数も、テスト内で上書きすることができます。</p> <p>変数の管理をしていない場合、同じ名前の変数を作ってしまうと意図せず上書きしてしまい、うまくテストが実行できないことになります。あらかじめフローで変数を利用するときはパラメーター経由で渡す、などがあると思わぬ失敗を防いだり、可読性が向上すると思います。</p> <h2 id="おわりに">おわりに</h2> <p>mablを複数人で利用するにあたり、適切なガイドラインが運用されていれば、新しいメンバーが参画してもつまづくことなく効率的にテスト自動化を進めることができています。 資料からわかりやすい運用ガイドラインとその浸透まで進めてくれていたメンバーには感謝しかありません。</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech ノーコードで在庫管理アプリを作ってみたら趣味になった話 hatenablog://entry/6801883189066673326 2023-12-19T11:00:00+09:00 2023-12-19T11:00:01+09:00 この記事は hacomono advent calendar 2023 の19日目の記事です はじめに 完成したものがこちら 本編の前にまとめ 先人の知恵を借りる 情報整理 アプリ作成 なぜアプリを作ることになったのか Appsheetとの遭遇 Appsheetと格闘する日々 それっぽいだけで使えないアプリ爆誕 改良版のリリース ノーコードアプリ開発という手段 はじめに 株式会社hacomono IoT部で生産管理をしているこんちゃんです。 生産管理の人です。 コードは書けません。 正確に言うと勉強しては挫折を何度か繰り返しています。 (前職でPythonを使って解析をする機会があり学習サイト… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231218/20231218144412.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>この記事は <a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono advent calendar 2023</a> の19日目の記事です</p></blockquote> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#完成したものがこちら">完成したものがこちら</a></li> <li><a href="#本編の前にまとめ">本編の前にまとめ</a><ul> <li><a href="#先人の知恵を借りる">先人の知恵を借りる</a></li> <li><a href="#情報整理">情報整理</a></li> <li><a href="#アプリ作成">アプリ作成</a></li> </ul> </li> <li><a href="#なぜアプリを作ることになったのか">なぜアプリを作ることになったのか</a></li> <li><a href="#Appsheetとの遭遇">Appsheetとの遭遇</a></li> <li><a href="#Appsheetと格闘する日々">Appsheetと格闘する日々</a></li> <li><a href="#それっぽいだけで使えないアプリ爆誕">それっぽいだけで使えないアプリ爆誕</a></li> <li><a href="#改良版のリリース">改良版のリリース</a></li> <li><a href="#ノーコードアプリ開発という手段">ノーコードアプリ開発という手段</a></li> </ul> <h1 id="はじめに">はじめに</h1> <p>株式会社hacomono IoT部で生産管理をしているこんちゃんです。<br> <br> 生産管理の人です。<br> コードは書けません。<br> <br> 正確に言うと勉強しては挫折を何度か繰り返しています。<br> (前職でPythonを使って解析をする機会があり学習サイトで勉強してみましたが、もうほぼ覚えてないレベル) <br> <br> そんな非SWエンジニアが、ノーコードでがんばってアプリを作ってみた話です。<br> 同じように、<span style="color: #ff5252"><strong>コードは書けないけどエクセルやスプレッドシート管理に限界を感じている</strong></span>同志たちの背中を押せればと思い、筆を取りました。 <br> <br> <br></p> <h1 id="完成したものがこちら">完成したものがこちら</h1> <p><figure class="figure-image figure-image-fotolife" title="hacomono IoT 在庫管理アプリ Dashboard画面"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231214/20231214115351.png" width="1200" height="648" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>hacomono IoT 在庫管理アプリ Dashboard画面</figcaption></figure></p> <p>発注→入庫→出庫と一連の流れで登録できるようになっています。<br> コードも書けないしAppsheetを触るのも初めての素人でしたが、ここまでのものは作れました。<br> <br> <br> 以下は作ることになった経緯や苦悩がメインです。<br> どんなところでつまづいたのかも記載していますので、同じ轍を踏まぬよう参考にしていただければ幸いです。 <br> <br> <br></p> <h1 id="本編の前にまとめ">本編の前にまとめ</h1> <p>Appsheetでアプリを作りたい!独学で!と思った時のおすすめの道のりは以下になります。<br> あくまで私の場合こうだった…という経験論なので、試して違うな・合わないなと思ったら、飛ばしたり戻ったりして試行錯誤してみてください。<br></p> <h5 id="先人の知恵を借りる">先人の知恵を借りる</h5> <ol> <li>公開されているアプリをDLして触ってみる</li> <li>上記アプリをいじっては更新し、どの項目を変えるとどういう挙動をするのか確かめる</li> </ol> <h5 id="情報整理">情報整理</h5> <ol> <li>データベースを構築する</li> </ol> <h5 id="アプリ作成">アプリ作成</h5> <ol> <li>データベースに則りアプリを作る</li> <li>不明点はネットの情報を繋ぎ合わせながらトライアンドエラー</li> <li>地道に不具合を解消する <br> <br> <br></li> </ol> <h1 id="なぜアプリを作ることになったのか">なぜアプリを作ることになったのか</h1> <p>前提として、生産管理の仕事は、数字を多く扱います。<br/> 中でも在庫管理/入出庫管理は膨大な量の数字を扱います。<br> <br> 弊社は<span style="color: #ff5252"><strong>フルリモートという特性上、各個人の家が倉庫のようなもの</strong>です。</span><br> 量産初期には個人の自宅に一旦部品を送り、加工をしてからオフィスや工場へ送付する、といった移動も発生します。<br> 更に今年外部協力工場や倉庫が増え、管理する対象はどんどん増えています。<br> <br> 対してそれらを管理する人間は何人いるでしょうか。 <br> <br> <br> <strong>ひとりです。</strong> <br> <br> <br> 弊社で生産管理は私一人なのです。<br> <br> まだプロダクト数も多くなく、トータル部品種類も100程度のため何とかなっています。<br> …とはいえ、あちこちに散らばる部品の管理はそれなりに大変です。</p> <p><figure class="figure-image figure-image-fotolife" title="あの部品今いくつあるんだっけ? オフィスへのどこでもドアが欲しい"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231214/20231214115937.png" width="700" height="612" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>あの部品今いくつあるんだっけ? オフィスへのどこでもドアが欲しい</figcaption></figure></p> <p><br></p> <p>最初は入出庫が発生したタイミングで各人にGoogleフォームから登録してもらい、自動で専用のスプレッドシートに転記される仕組みを構築しました。</p> <p><figure class="figure-image figure-image-fotolife" title="ここで登録されたものが"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231214/20231214120011.png" width="978" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ここで登録されたものが</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="このシートに自動転記されています"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231214/20231214120033.png" width="1200" height="471" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>このシートに自動転記されています</figcaption></figure></p> <p>しかし、入出庫が発生するたびに開発メンバーに登録してもらう工数は無視できないレベルでした。<br> また、誤記や表記揺れもあり、結局私が全て再チェックを実施するルーティーンとなっていました。<br> <br> <br> どうしたものかと再考していた時期に、PLMのような基幹システムを導入してはどうかと部内で話題が上がりました。<br> 在庫管理もこれに乗っかろうとしたものの、機能・価格ともに今のフェーズで導入するには重すぎることや、過不足なく機能実装しようとするとそれなりにカスタマイズが発生すること、などがあり、流れてしまったのです。<br> <br> <br> 頼みの綱が切れたそんなとき、同僚から<span style="color: #ff5252"><strong><span style="font-size: 150%">「Appsheetでノーコードアプリ開発したらいいじゃん」</span></strong></span>と言われたのです。<br> <br> Appsheet、名前は見聞きしたことがあったものの、どんなものかはさっぱり分かりませんでした。</p> <p><span style="color: #2196f3">🤔 ノーコードでアプリが作れる…? しかもGoogleならナレッジも豊富だろうしUXもきっと悪くないはず!</span></p> <p><span style="color: #2196f3">🤗 だめだったらやめて他の策を考えればいいか </span></p> <p>…と気楽に考えた私は、Appsheetという魔境へと足を踏み入れることになったのでした。 <br> <br> <br></p> <h1 id="Appsheetとの遭遇">Appsheetとの遭遇</h1> <p>Appsheetを使う手筈を整える中、一抹の不安がよぎります。</p> <p><span style="color: #2196f3">🙃 これ…全部英語じゃないか??? </span></p> <p>Appsheetには日本語verが無く、無理やりGoogle翻訳をかけると相当カオスな文になります。<br> <strong>申し遅れましたが私は英語が苦手です。</strong><br> こちらも何度も挫折しています。誰かいい勉強法を教えてください。<br> <br> <br> Appsheetの設定項目を確認する中、更に一抹の不安がよぎります。</p> <p><span style="color: #2196f3">🙃 これ…ノーコードとはいうけど式は書くな??? </span></p> <p>条件式などはこちらが指定してあげないといけません。<br> 冒頭に申しましたように、コードは書けません。<br> <br> <br> あれだ、流行りのGPTさんに聞いたらコード書いてくれるって聞いたぞ。<br> きっとAppsheetだってお手のものに違いない!</p> <p><span style="color: #2196f3">🙃 GPTめっちゃ嘘つくじゃん </span></p> <p>他の言語に比べ、ナレッジが溜まっていなかったのだと思います。<br> 何を聞いても嘘の項目、嘘の操作方法をしれっと教えてくるので、途中から頼ることをやめました。<br> <br> <br> そうなると自力で勉強して使いこなせるようにならなければいけません。<br> ひたすらにググりました。</p> <p><span style="color: #2196f3">🙃 ネットの海に…情報が…少なすぎる…… </span></p> <p>情報の絶対数が圧倒的に少ないのです。<br> また、<strong>始めたてはどんなキーワードで検索すれば欲しい情報がヒットするのかわからない</strong>ことが往々にしてあります。<br> それっぽいのは引っかかるけど欲しいのはこれじゃない…ということがスプレッドシートなどに比較して圧倒的に多かったです。<br> <br> <br> ここでやめておけば良かったのに、それでもコミュニティのQ&amp;AやYouTubeのノウハウ動画、ブログ記事などを参考にしながらどうにか勉強をスタートしました。 <br> <br> <br> 天下のGoogleサービスなのに何でこんなに情報が少ないんでしょう。 やはり言語の壁でしょうか。 <br> <br> <br></p> <h1 id="Appsheetと格闘する日々">Appsheetと格闘する日々</h1> <p>情報が少ないため、トライアンドエラーを繰り返していると時間がどんどん溶けていきます。<br> 最初の一歩として簡単なアプリを自分で作ろうとしましたが、エラーの連続で断念。<br> <br> <span style="color: #ff5252"><strong>まずは先人が公開している在庫管理系アプリをいじってみることにしました。</strong></span><br> 結果的にはこれが功を奏し、どの設定項目にどんな文言を入れたらどんな動きをするのか、というところが少しずつ見えてきました。</p> <p><figure class="figure-image figure-image-fotolife" title="あー そーゆーことね 完全に理解した(わかってない)の図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231214/20231214120504.png" width="700" height="622" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>あー そーゆーことね 完全に理解した(わかってない)の図</figcaption></figure></p> <p>その後、先人のアプリの機能を削る形で、部品を登録するだけのアプリを作ってみました。<br> どのシートを削除するとどのシートに影響が出るのか、といったところを確認しつつ修正していきました。<br> 部品登録の部分のみ残ったものを、実際に登録作業をしてみることで対応するスプレッドシートへの転記具合などを確認しました。<br> <br> <br> 上記先人のアプリを自分の業務要望に合うよう修正しつつそれっぽいものをリリースできたのは、Appsheetを本格的に勉強しはじめてから4ヶ月ほど経った頃でした。<br> <br> <br> <br></p> <h1 id="それっぽいだけで使えないアプリ爆誕">それっぽいだけで使えないアプリ爆誕</h1> <p><figure class="figure-image figure-image-fotolife" title="記念すべき最初のアプリ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231214/20231214120617.png" width="1200" height="660" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>記念すべき最初のアプリ</figcaption></figure></p> <p>やっとの思いでリリースしたアプリは、何となく使えはするものの、直感的に分かりづらかったり誤操作を誘発する動線になっていたりと、初めて使う人にはだいぶ優しくないものでした。<br> <br> 何より困ったのが、<span style="color: #ff5252"><strong>いちから自分で作っていないので、何の設定項目・式がどこに紐づいて影響しているかがわからなくなり、気軽に改修できなくなってしまったこと</strong>です。</span><br> ここが使いづらい、と思い修正すると、何をいじってもエラーが出ます。<br> そしてそのエラーがなぜ出ているのか原因が突き止められない…ということの繰り返しでした。<br> <br> <br> ここで気づいたのが、<span style="color: #ff5252"><strong>データベース化の大切さ</strong></span>です。<br> Appsheetを開いても、テーブルとテーブルがどんな関係になっているかがパッと見ても分かりません。<br> <br> <br> そこで、draw.ioという作図ツールを利用し、各テーブルの内包項目と関係性、LABELとKEYの明記、名称の定義や、ややこしい数式も記載することにしました。<br> 現状(最終形)と合わせておくだけで、次回改修をかけるときに格段にとっかかりやすくなります。</p> <p><figure class="figure-image figure-image-fotolife" title="データベース"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231214/20231214120706.png" width="1200" height="614" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>データベース</figcaption></figure></p> <p>作っている最中で追加や削除はよくあるため、最初から上図のようにきれいに作る必要はありません。<br> 私は最初はコピー用紙に手書きでああでもない、こうでもないと書き殴り、あとからdraw.ioに落とし込みました。<br> <br> <br> 一応Appsheet内にもRelationshipsが確認できるページはあるのですが、テーブルやスライスが増えて複雑化するほど見づらくなるため、上図のようにデータベースとして作成しておくほうが良いと考えます。</p> <p><figure class="figure-image figure-image-fotolife" title="Appsheet内のRelationships なにがなんだか"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231214/20231214120738.png" width="1200" height="994" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Appsheet内のRelationships なにがなんだか</figcaption></figure> <br> <br> <br></p> <h1 id="改良版のリリース">改良版のリリース</h1> <p>そんなこんなで、<span style="color: #ff5252"><strong>結局イチからアプリを再度作成</strong></span>することにしました。<br> <br> 量産用に購入した部品でも開発や保守用に払い出すことがあるため、用途の項目を追加したり、似たような名前の資材があると混乱するので画像登録できるようにしたり…と、なるべく痒い所に手が届くような修正も加えました。<br> <br> 式の過不足や設定項目の意味を今度こそしっかり理解しながら、なんちゃってではない、自社に合った在庫管理アプリ。<br> <br> 出来上がったとき、「こ、これはすごいものを作ってしまった…!」と本気で思いました。<br></p> <p><figure class="figure-image figure-image-fotolife" title="資材一覧から選択すると、詳細や入出庫履歴も見れる"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231214/20231214120808.png" width="1200" height="654" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>資材一覧から選択すると、詳細や入出庫履歴も見れる</figcaption></figure></p> <p>使い勝手は上々ですが、色々と欲しい機能を詰め込んだおかげでたまにバグが発見されます。<br> (私の能力不足も多分にある)<br> <br> それに対して別の機能を実装することで解決したり、実はあまりいらない機能だったと思い直して削除したりと、日々改良を行ないつつ愛用しています。<br> <br> <br> <br></p> <h1 id="ノーコードアプリ開発という手段">ノーコードアプリ開発という手段</h1> <p>Appsheet以外にも、ノーコードでアプリを作る手段はいくつもあります。<br> 正直、私は他のツールと比較せずにAppsheet決め打ちで開発を始めてしまいました。<br> 人によってツールの使いやすさやこだわりがあると思うので、<span style="color: #ff5252"><strong>検索して色んなツールを比較</strong></span>するのも良いと思います。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231214/20231214120842.png" width="685" height="525" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>私はAppsheetを使う中で、挫折どころかだんだん楽しくなり、休日も触るようになり<span style="color: #ff5252"><strong>結果として趣味のひとつになってきています</strong>。</span><br> ネットに情報が少ないため自分でトライアンドエラーを繰り返す必要性が高いことから、成功したときの達成感が大きいこともあるかもしれません。<br> (早くリリースしたい気持ちとの戦い) <br> <br> <br> あのとき同僚は何の気なしにAppsheetを勧めてくれたのだと思いますが、うまいことハマりました。<br> 感謝しています。 <br> <br> <br> 自社のフェーズが変わり、再び基幹システムを入れる動きが出るまでは、自前アプリを改良しながら在庫管理/入出庫管理をしていこうと思います。<br> <br> <br> そして情報が少ないとはいえ、開発をする上で先人たちの知恵には本当に助けられました。<br> 覚書もかねて、自分でもナレッジ蓄積と公開をこっそり考えています。<br> <br></p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech CodeRabbit にレビュー観点の追加設定を "日本語で" できるの知ってた? hatenablog://entry/6801883189066987429 2023-12-18T07:00:00+09:00 2023-12-18T07:00:02+09:00 こちらの記事はhacomono Advent Calendar 2023の18日目の記事です みゅーとんです.どうも. hacomono アドベントカレンダー 18 日目の記事です. なんか今月張り切って 3 記事くらい作ってしまいました. CodeRabbit 使ってますか? PR 作った際に AI がレビューしてくれる GitHub App です (GitLab もできるっぽいですね ) 弊社では, 主要な private repository にも導入しており, 開発者体験向上に一役買っています. 開発メンバーからも反応が良い感じです. さて, 実は CodeRabbit にはレビュー観… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231215/20231215120446.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>こちらの記事は<a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono Advent Calendar 2023</a>の18日目の記事です</p></blockquote> <p><a href="https://twitter.com/_mew_ton">みゅーとん</a>です.どうも.</p> <p><a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono アドベントカレンダー 18 日目</a>の記事です. なんか今月張り切って 3 記事くらい作ってしまいました.</p> <p><a href="https://coderabbit.ai/">CodeRabbit</a> 使ってますか?</p> <p>PR 作った際に AI がレビューしてくれる GitHub App です (GitLab もできるっぽいですね )</p> <p>弊社では, 主要な private repository にも導入しており, 開発者体験向上に一役買っています. 開発メンバーからも反応が<a href="https://techblog.hacomono.jp/entry/2023/11/28/110000">良い感じ</a>です.</p> <p>さて, 実は CodeRabbit にはレビュー観点のカスタマイズができるらしく, 今回はそれを試してみました.</p> <h3 id="TLDR">TL;DR</h3> <p>3行でまとめ</p> <ul> <li>リポジトリのルートに <code>.coderabbit.yaml</code> を追加する</li> <li>レビュー観点は日本語で記載できる</li> <li>レビュー観点のドキュメントを config に集約してしまえるメリットがある</li> </ul> <h4 id="解説しないこと">解説しないこと</h4> <ul> <li>CodeRabbit の解説 / 導入方法</li> </ul> <h3 id="CodeRabbit-をカスタマイズする">CodeRabbit をカスタマイズする</h3> <p>兎にも角にも, このマニュアルに従えば基本は OK ですね.</p> <p><a href="https://coderabbit.ai/docs/get-started/customize-coderabbit">CodeRabbit: AI-powered Code Reviews</a></p> <p>やることは単純で, ルートに <code>.coderabbit.yaml</code> を追加し, 上記のリンク先に従って設定項目を埋めていけば良いだけです.</p> <p>ただ, ここで注目すべきは <code>reviews.path_instructions</code> の値.</p> <p>公式のサンプルを抜粋するとこうなってますね.</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">reviews</span><span class="synSpecial">:</span> <span class="synIdentifier">path_instructions</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">path</span><span class="synSpecial">:</span> <span class="synConstant">&quot;**/*.js&quot;</span> <span class="synIdentifier">instructions</span><span class="synSpecial">:</span> <span class="synConstant">&quot;Review the JavaScript code for conformity with the Google JavaScript style guide, highlighting any deviations.&quot;</span> <span class="synStatement">- </span><span class="synIdentifier">path</span><span class="synSpecial">:</span> <span class="synConstant">&quot;tests/**/*&quot;</span> <span class="synIdentifier">instructions</span><span class="synSpecial">:</span> | <span class="synConstant">&quot;Assess the unit test code employing the Mocha testing framework. Confirm that:</span> <span class="synConstant"> - The tests adhere to Mocha's established best practices.</span> <span class="synConstant"> - Test descriptions are sufficiently detailed to clarify the purpose of each test.&quot;</span> </pre> <p><code>path</code> <code>instructions</code> の配列構造になってます.</p> <blockquote><p>Review the JavaScript code for conformity with the Google JavaScript style guide, highlighting any deviations.</p> <p>Assess the unit test code employing the Mocha testing framework. Confirm that: - The tests adhere to Mocha's established best practices. - Test descriptions are sufficiently detailed to clarify the purpose of each test.</p></blockquote> <p>この文章を見る感じ, <code>instructions</code> には自然言語を記述できて, そこにレビュー観点を追加するようです.</p> <h3 id="instructions-に日本語を設定してみる"><code>instructions</code> に日本語を設定してみる</h3> <p>これって, ChatGPT に裏側で読ませる値なのだから, 日本語でもいけるのでは・・? と思い, 以下のように設定してみました.</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synStatement">- </span><span class="synIdentifier">path</span><span class="synSpecial">:</span> <span class="synConstant">&quot;**/*.vue&quot;</span> <span class="synIdentifier">instructions</span><span class="synSpecial">:</span> <span class="synConstant">&quot;|</span> <span class="synConstant"> vue ファイルのルートタグに従って、以下の通りレビュー観点を追加してください.</span> ファイル全体 <span class="synStatement">- </span>行数が多い (コメント行を含まずに <span class="synConstant">300</span> 行を超える) 場合、分割可能かどうかの観点をレビューに含め、可能であれば、指摘してください. &lt;script&gt; タグ <span class="synStatement">- </span>Google の JavaScript style guide に従ってレビューし、不具合あれば指摘してください. <span class="synStatement">- </span>Mixin, Filter の使用を非推奨とします. <span class="synStatement">- </span>Options API のコンポーネントは、順次 Composition API に移行していきます. <span class="synStatement">- </span>SSR, CSR の両方で適切に処理されることをレビューの観点に追加してください. &lt;template&gt; タグ <span class="synStatement">- </span>適切な HTML タグの使用をレビューしてください. <span class="synStatement">- </span>div や span の深い階層構造は非推奨とし、別案があれば指摘してください. <span class="synStatement">- </span>基本的には、テンプレートに js のロジックを書かず、 computed や methods などを使うように指示してください. <span class="synStatement">- </span>v-for のネストを非推奨とします. <span class="synStatement">- </span>v-for の key に index を使用することを非推奨とします. ただし、そうせざるを得ないケースもあるため、強制はしません. <span class="synStatement">- </span>v-if と v-for を同じエレメントに同時に記載することを非推奨とします. <span class="synStatement">- </span>vue filter の使用を非推奨とします. <span class="synStatement">- </span>多言語対応のため、静的なテキストはかならず `$t` `$tc` `$d` `$n` などの vue-i18n 標準の関数を使用することを推奨します. ただし、独自に実装した多言語対応用関数 `$twr` `$tw` `$tp` `$tl` の <span class="synConstant">4</span> 種を非推奨とします. <span class="synStatement">- </span>多言語対応のため、表示テキストを結合する処理を非推奨とします。例として、`'月' + <span class="synConstant">'曜日'</span>` とすると翻訳できません。 &lt;style&gt; タグ <span class="synStatement">- </span>scoped style, moduled style の使用を推奨します. <span class="synStatement">- </span>deep, global スタイルの適用を非推奨とします. <span class="synStatement">- </span>基本的に、コンポーネント内に閉じた範囲でスタイルが適用されていることをレビューしてください。 <span class="synConstant">&quot;</span> </pre> <p>コードブロックに本文があると読みづらいので, 抜粋します</p> <blockquote><p>vue ファイルのルートタグに従って、以下の通りレビュー観点を追加してください.</p> <p>ファイル全体</p> <ul> <li>行数が多い (コメント行を含まずに 300 行を超える) 場合、分割可能かどうかの観点をレビューに含め、可能であれば、指摘してください.</li> </ul> <p><code>&lt;script&gt;</code> タグ</p> <ul> <li>Google の JavaScript style guide に従ってレビューし、不具合あれば指摘してください.</li> <li>Mixin, Filter の使用を非推奨とします.</li> <li>Options API のコンポーネントは、順次 Composition API に移行していきます.</li> <li>SSR, CSR の両方で適切に処理されることをレビューの観点に追加してください.</li> </ul> <p><code>&lt;template&gt;</code> タグ</p> <ul> <li>適切な HTML タグの使用をレビューしてください.</li> <li>div や span の深い階層構造は非推奨とし、別案があれば指摘してください.</li> <li>基本的には、テンプレートに js のロジックを書かず、 computed や methods などを使うように指示してください.</li> <li>v-for のネストを非推奨とします.</li> <li>v-for の key に index を使用することを非推奨とします. ただし、そうせざるを得ないケースもあるため、強制はしません.</li> <li>v-if と v-for を同じエレメントに同時に記載することを非推奨とします.</li> <li>vue filter の使用を非推奨とします.</li> <li>多言語対応のため、静的なテキストはかならず <code>$t</code> <code>$tc</code> <code>$d</code> <code>$n</code> などの vue-i18n 標準の関数を使用することを推奨します. ただし、独自に実装した多言語対応用関数 <code>$twr</code> <code>$tw</code> <code>$tp</code> <code>$tl</code> の 4 種を非推奨とします.</li> <li>多言語対応のため、表示テキストを結合する処理を非推奨とします。例として、<code>'月' + '曜日'</code> とすると翻訳できません。</li> </ul> <p><code>&lt;style&gt;</code> タグ</p> <ul> <li>scoped style, moduled style の使用を推奨します.</li> <li>deep, global スタイルの適用を非推奨とします.</li> <li>基本的に、コンポーネント内に閉じた範囲でスタイルが適用されていることをレビューしてください。</li> </ul> </blockquote> <p>果たして, これで問題なくレビューしてくれるのだろうか.</p> <p>半信半疑で, PR をつくってみました. すると</p> <p><figure class="figure-image figure-image-fotolife" title="カスタムレビュー観点を軸にレビューしてくれている様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231215/20231215100742.png" alt="GitHub &#x306E; PullRequest &#x306E;&#x30B3;&#x30E1;&#x30F3;&#x30C8;&#x3067;, CodeRabbit &#x306E; AI &#x306B;&#x3088;&#x308B;&#x30B3;&#x30FC;&#x30C9;&#x30EC;&#x30D3;&#x30E5;&#x30FC;&#x306E;&#x6307;&#x6458;&#x304C;&#x3055;&#x308C;&#x3066;&#x3044;&#x307E;&#x3059;. &#x5185;&#x5BB9;&#x306F; &amp;#x60;.coderabbit.yaml&amp;#x60; &#x306B;&#x6307;&#x5B9A;&#x3057;&#x305F;&#x30AB;&#x30B9;&#x30BF;&#x30E0;&#x30EC;&#x30D3;&#x30E5;&#x30FC;&#x89B3;&#x70B9;&#x306B;&#x6E96;&#x3058;&#x305F;&#x3082;&#x306E;&#x3068;&#x306A;&#x3063;&#x3066;&#x3044;&#x307E;&#x3059;." width="1200" height="793" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>カスタムレビュー観点を軸にレビューしてくれている様子</figcaption></figure></p> <p>ちゃんとレビューしてくれました. すげぇ</p> <p>指摘し返しても, 同様の観点でちゃんとコメントを返してくれます.</p> <p><figure class="figure-image figure-image-fotolife" title="レビューに対してやりとりしている様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231215/20231215100920.png" alt="CodeRabbit &#x306B;&#x3088;&#x308B;&#x6307;&#x6458;&#x306B;&#x5BFE;&#x3057;&#x3066;&#x30B3;&#x30E1;&#x30F3;&#x30C8;&#x3092;&#x8FD4;&#x3057;&#x3066;&#x3044;&#x308B;&#x69D8;&#x5B50;&#x3067;&#x3059;. &#x30AB;&#x30B9;&#x30BF;&#x30E0;&#x30EC;&#x30D3;&#x30E5;&#x30FC;&#x89B3;&#x70B9;&#x306E;&#x8EF8;&#x304B;&#x3089;&#x3076;&#x308C;&#x305A;&#x306B;&#x4F1A;&#x8A71;&#x304C;&#x3067;&#x304D;&#x3066;&#x3044;&#x307E;&#x3059;." width="1200" height="719" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>レビューに対してやりとりしている様子</figcaption></figure></p> <h3 id="日本語で-config-を書けるメリット">日本語で config を書けるメリット</h3> <p>今まで, レビュー観点を notion にまとめていました. こんな感じです.</p> <p><figure class="figure-image figure-image-fotolife" title="Notion で管理しているレビュー観点のドキュメント"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231215/20231215101053.png" alt="Notion &#x3067;&#x7BA1;&#x7406;&#x3057;&#x3066;&#x3044;&#x308B;&#x30EC;&#x30D3;&#x30E5;&#x30FC;&#x89B3;&#x70B9;&#x306E;&#x30C9;&#x30AD;&#x30E5;&#x30E1;&#x30F3;&#x30C8;&#x3067;&#x3059;. Notion &#x3092;&#x99C6;&#x4F7F;&#x3057;&#x3066;&#x304D;&#x308C;&#x3044;&#x306B;&#x307E;&#x3068;&#x307E;&#x3063;&#x3066;&#x3044;&#x308B;&#x3068;&#x306F;&#x3044;&#x3048;, &#x9805;&#x76EE;&#x6570;&#x304C;&#x591A;&#x304F;&#x3066;&#x8907;&#x96D1;&#x3067;&#x8AAD;&#x3080;&#x306E;&#x304C;&#x5927;&#x5909;&#x3067;&#x3059;." width="1200" height="430" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Notion で管理しているレビュー観点のドキュメント</figcaption></figure></p> <p>全社員が notion を参照できる体制であるため, “そもそも記事にたどり着く” ことが難しくなっており, このレビュー観点は “作ったはいいものの, 誰も見ない” 記事に成り果てていました.</p> <p>最後に更新したのはいつだったか・・・</p> <p>ドキュメントの配置を Notion から同 Repository に置くことで解消はできますが, 今度は “Repository 毎に同じ文章をまとめるコスト” が発生し, 結局しんどいだけでした.</p> <p>今回 CodeRabbit の Config にレビュー観点を日本語で記載することで, CodeRabbit のレビューをカスタムするだけでなく, 開発メンバーが閲覧するレビュー観点のドキュメントとしても役立てることができます.</p> <p>Repository ごとに同じ文章をまとめることについても, それが CodeRabbit のレビューの質に直結するため, やらざるを得なくなります.</p> <h3 id="まとめ">まとめ</h3> <p>この構成, 非常に良いです.</p> <p>早速, 開発メンバー全員に告知し, レビュー観点の追加を依頼しました.</p> <p>yaml がドキュメントになる点だけは気持ち悪いですが, レビュー観点のまとめドキュメントとしてはかなり質の良いものになることが期待できるでしょう.</p> <p>ただ, 欲を言うと, 別の repository から extend できる仕組みがあって, 中央管理とかできるとうれしいかなぁ・・</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech 「緊急で重要」なrenovate, dependabotの更新 hatenablog://entry/6801883189066811160 2023-12-17T07:00:00+09:00 2023-12-17T07:00:00+09:00 こちらの記事はhacomono Advent Calendar 2023の17日目の記事です はじめに こんにちは、hacomonoでエンジニアをやっております野崎(社内ではサイモンと呼ばれています)です。 ここ数ヶ月で作業的に運用できるようになってきた、renovate/dependabotの運用を書いてみます。 renovate/dependabot renovate / dependabot は、どちらもリポジトリのソースコードに含めている依存ライブラリ(パッケージ)を自動でスキャン、および更新PRを作成してくれるサービスです。 これらはどちらも、「自動でPRを作成してくれる」便利なサー… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231215/20231215120644.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>こちらの記事は<a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono Advent Calendar 2023</a>の17日目の記事です</p></blockquote> <h1 id="はじめに">はじめに</h1> <p>こんにちは、hacomonoでエンジニアをやっております野崎(社内ではサイモンと呼ばれています)です。</p> <p>ここ数ヶ月で作業的に運用できるようになってきた、renovate/dependabotの運用を書いてみます。</p> <h2 id="renovatedependabot">renovate/dependabot</h2> <p><a href="https://docs.renovatebot.com/">renovate</a> / <a href="https://docs.github.com/ja/code-security/dependabot">dependabot</a> は、どちらもリポジトリのソースコードに含めている依存ライブラリ(パッケージ)を自動でスキャン、および更新PRを作成してくれるサービスです。</p> <p>これらはどちらも、「自動でPRを作成してくれる」便利なサービスですがdependabotはライブラリの脆弱性監視に重心があるいっぽうでrenovateはライブラリの更新全般をサービスのスコープとしています。</p> <p>本記事ではそれらをどのように運用しているのか、またその理由を書きます。具体的な導入や設定の方法は各サービスのドキュメントをご参考ください。</p> <p>なお、renovate/dependabotは運営元の異なるサービスですが私のチームではまとめて取り扱っている(区別する理由があまりない)ので、以下断りがなければ区別せず取り扱っているものとします。</p> <h2 id="自動PRによる更新オペレーションの対象">自動PRによる更新オペレーションの対象</h2> <p>本稿が対象とするrenovate/dependabotの対象リポジトリは、私がおもに関わっているプロジェクトです。</p> <p>SaaSの本体とはアーキテクチャもリポジトリも別れているコードベースであるため、モノレポ・モノリスで運用する場合と比較すると難易度は低くなっています。</p> <h1 id="依存ライブラリも負債になる">依存ライブラリも負債になる</h1> <p>具体的な運用にふれる前に、renovate/dependabot対応の重要性認識について触れておきたいと思います。</p> <p>脆弱性への対処は常にやったほうが良いのはもちろんのこと、依存ライブラリの更新もできれば常にやっておきたい、と考えております。</p> <p>重要なライブラリ(httpクライアントやjsonパーサ、その他便利ツール)を使っているとして、あるバージョンを境に破壊的な変更が加わってしまい、その後メンテナンスが困難になってしまうケースはよくあると思います。</p> <p>最悪、メンテナンスされなくなり非推奨になってしまうことも…</p> <p>メンテナンスが困難な状況になってしまうと、最新のバージョンを導入したり互換性のあるライブラリを検証したりといった作業が発生し、製品開発を止めなければならなかったり悪いときには放置するしかない状況に追い込まれる可能性もあります。</p> <p>手遅れになる前に打てる対処をしておくのが、今の私のモチベーションになっています。</p> <h2 id="負債は常に返す">負債は常に返す</h2> <p>B/Sにおける負債のメタファーとは異なり、技術的な負債になりそうなものは常に更新しておくことで手がつけられない状態を防ぎたいですし、そういった意味でrenovate, dependabotの更新は負債になりうるものとして「やばくなったらまとめてみる」のを割けたいです。</p> <p>リファクタリング同様、ライブラリの更新は「重要かつ緊急」な課題と(私個人は)位置づけています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231215/20231215113144.jpg" width="538" height="330" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>とはいえ、製品開発を見ているチームが、リファクタリングやライブラリアップデートに十分な注意や時間を払えないのも十分に承知しています。短期的に効果が見えないものなので、作業の価値が低く見られやすいですし、直接的に換金できる作業でもありません。</p> <p>そのため依存ライブラリの更新は、ルールに則って運用的に更新できるのが望ましいと考えています。</p> <h1 id="renovatedependabotの運用">renovate/dependabotの運用</h1> <h2 id="設定を共有する">設定を共有する</h2> <p>renovateは、更新タイミングや自動マージ可否など、非常に数多くの設定をいれることができます(導入できる設定は <a href="https://docs.renovatebot.com/configuration-options/">Configuration Options</a> を参考)。</p> <p>renovateの設定を簡素かつ共通にできるよう、設定自体はリポジトリに持たず中央集権的な<a href="https://github.com/hacomono-lib/renovate-config">リポジトリ</a>を用意してそれを各プロジェクトで読み込んでいます。</p> <p>共通の設定を使う側のプロジェクトでは、 <code>renovate.json</code> を以下のように記述し、読み込む運用としています。</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> $<span class="synError">schema</span>: <span class="synError">'https://docs.renovatebot.com/renovate-schema.json'</span>, <span class="synError">extends</span>: <span class="synSpecial">[</span> <span class="synError">'github&gt;hacomono-lib/renovate-config:base'</span>, <span class="synError">'github&gt;hacomono-lib/renovate-config:npm'</span>, <span class="synError">'github&gt;hacomono-lib/renovate-config:npm-lockfile'</span>, <span class="synError">'github&gt;hacomono-lib/renovate-config:github-actions'</span>, <span class="synError">'github&gt;hacomono-lib/renovate-config:pre-commit'</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span> </pre> <h2 id="更新の運用">更新の運用</h2> <p>先述のように、renovate/dependabotの作ったPRを定期的に確認・マージしていく運用をしています。</p> <p>この運用を適用しているのはフロントエンドのプロジェクトなのですが、普段フロントエンドを触るメンバーで集まり、一つずつ見れる範囲で変更差分を確認しています。</p> <p>CIではTypeScriptの型チェックやユニットテスト、ビルド実行など複数観点から見ているため、「CIが通ればそんなにまずいことにはならないはず」という前提に立っています(ある程度CIを信頼している)。</p> <p>そのため、変更差分の確認自体はやるものの、CIが通れば比較的さくっとマージしています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231215/20231215113239.png" width="812" height="348" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>また、リリースの前後など「不用意に挙動が変わってほしくない」タイミングでは安全を取ってマージはせず、マージした後もQAプロセスに組み込みCIや目視で判断できる以外の観点からも動作を確認するようにしています。</p> <h2 id="マージしないこともある">マージしないこともある</h2> <p>CIをそれなりに信用しているため、CIが落ちるケースでは原則マージしないです(当たり前…)。わかりやすい例では、NuxtやVueなどフレームワークのマイナーバージョンがあがったタイミングで大きな変更が入ると、CIが落ちることがあります。</p> <p>CIが落ちてしまう場合は、ソースコードの変更が必要なことも多いためリリースノートも読みながらアップデートに追従する作業も入れています。</p> <p>なお、リリースノート読んでコード書き換えて…までいくと作業が結構膨れてしまうので、基盤チームも巻き込ませてもらい、作業が停滞しないよう推進しています(めちゃくちゃ感謝です)。</p> <h1 id="おわりに">おわりに</h1> <p>私個人の拙い経験の中ではありますが、定期的に負債を返すようなオペレーションができていなかったり、やろうとしたときには手遅れになったことで結局大きな困難を抱えたことが何回かあります。</p> <p>大きなリライト、リアーキテクチャは成功確度が高くないものですし、日頃からの取り組みが如実に出る領域と信じて、今後も継続的に続けていきたいです。</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech 今年振り返って失敗したこと hatenablog://entry/6801883189066667602 2023-12-15T07:00:00+09:00 2023-12-15T07:00:00+09:00 こちらの記事はhacomono Advent Calendar 2023の15日目の記事です 大阪でエンタープライズ領域の開発を担当している和田です。 ライドケミートレカの PHASE: 00 がどこにも定価で売ってないんですが、どこかリアル店舗だったら売ってるんでしょうか。 情報求む。 今年を振り返る タイトルの通り、失敗したなーということを述べていきたいと思います。 ただ、失敗したと思いつつも別のいい案があるわけでないものもございますのでご了承ください。 ちなみに自分の目下の業務は以下のようなことを行っておりますので、それを前提で眺めていただければと思います。 公開API に関する開発、お… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231214/20231214145551.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>こちらの記事は<a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono Advent Calendar 2023</a>の15日目の記事です</p></blockquote> <p>大阪でエンタープライズ領域の開発を担当している<a href="https://twitter.com/iamnatsuv2">和田</a>です。</p> <p>ライドケミートレカの PHASE: 00 がどこにも定価で売ってないんですが、どこかリアル店舗だったら売ってるんでしょうか。</p> <p>情報求む。</p> <h1 id="今年を振り返る">今年を振り返る</h1> <p>タイトルの通り、失敗したなーということを述べていきたいと思います。</p> <p>ただ、失敗したと思いつつも別のいい案があるわけでないものもございますのでご了承ください。</p> <p>ちなみに自分の目下の業務は以下のようなことを行っておりますので、それを前提で眺めていただければと思います。</p> <ul> <li>公開API に関する開発、およびそれに付随する業務</li> <li>顧客毎に作っているアドオンを動かす環境(インフラや開発基盤)の管理</li> <li>SSO などエンプラっぽい機能開発</li> </ul> <h2 id="1-Terraform-の-Workspace-を顧客毎に切って管理したこと">1. Terraform の Workspace を顧客毎に切って管理したこと</h2> <p>現在はA社のアドオン環境(例えば Fargate + RDS など)、B社のアドオン環境、C社の、、、といった環境を作る際に、Terraform の Workspace を切り替えながら作ってます。</p> <p>これをやっていると、途中で構成変更、とまでは行かなくとも命名ルールを見直すなどしただけでも、作成済み環境においてリソース再作成が必要となってしまいダウンタイムとれない環境ではコードとの乖離がでるようになってしまいました。</p> <p>変更かける際はそのあたり気にしながらやっていけばいい話ではあるんですが、後の祭りです。</p> <p>今は諸問題を回避するため、よくないのですが AWS の Terraform のモジュールをローカルに落としてきて改変して使ってます。</p> <p>どのみち環境が増えていったときに辛くなるので、Workspace は使い所を見極めて利用していきたいと感じました。</p> <h2 id="2-バッチを-ECS-の-Run-Task-で実現したこと">2. バッチを ECS の Run Task で実現したこと</h2> <p><code>Capacity is unavailable at this time. Please try again later or in a different availability zone.</code></p> <p>ECS(Fargate) で Run Task したときにこんなエラーがでて起動してくれないことがありました。</p> <p>0時頃に起動させていたバッチなんですが、ある日を境に連日このエラーが出るようになりました。</p> <p>どうも AWS 側のコンピューティングリソースがシンプルに不足しているらしく(?)、0時15分に起動するようにしたら一旦は解消してます。</p> <p>解消は難しそうで、対応策としては Step Functions など用いて、エラーハンドリングするしかないようです。うーん。</p> <p>これを大きく見直そうとすると、1 で述べた問題にぶつかるという悩ましい問題です。</p> <h2 id="3-Rails-のコードから機械的に-API-ドキュメントを起こしてること">3. Rails のコードから機械的に API ドキュメントを起こしてること</h2> <p>API を公開するとなれば API ドキュメントを起こさないといけないわけですが、スキーマ駆動で開発しておらず、更に Ruby となると中々に手間がかかる作業になります。</p> <p>そこでドキュメント生成スクリプトを組んで出力することにしました。</p> <p>path や schema の情報は、Rails の <code>Rails.application.routes</code> や <code>ActiveRecord</code>、および独自のモデルクラスから取れるので、まあまあ機械的に出力することができています。</p> <p>また、コントローラーには YARD コメントでリクエストのモデル、レスポンスのモデルを手書きしてます。</p> <p>イメージとしては以下のようなものを記載しています。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># @description メンバーを登録します</span> <span class="synComment"># @request Member::MemberEntity</span> <span class="synComment"># @response [{ &quot;name&quot;: &quot;member&quot;, &quot;type&quot;: &quot;Member::MemberViewModel&quot;, &quot;description&quot;: ... }]</span> <span class="synPreProc">class</span> <span class="synType">XxxxxController</span> ... </pre> <p>これをスクリプトで拾って OpenAPI 形式のドキュメントに、そこから redoc で html 化という流れになってます。</p> <p>と、いう感じで作ってるんですが、微妙に実際のAPI仕様と差が出てしまうところがあったりするんですよね、、、(ご迷惑おかけしている方々。申し訳ございません。)</p> <p>その微妙な問題を吸収するためのスクリプト改修を、今もまさにやってます。</p> <p>具体的な問題は今回の本質ではないので語りませんが、時間がかかってもスキーマ定義を書いていくのが実は近道なんじゃないかと思い始めてます。</p> <p>とはいっても開発全体をスキーマ駆動でやっていこうと運用変えるのはパワーのいる話ですし、そうしなければスキーマ定義をリバースでいちいちメンテしないといけなくなるので、どうするのが正解なのか、未だに悩みながらやってます。</p> <h1 id="まとめ">まとめ</h1> <p>今年は自分があまり経験したことのない領域の開発をやらせていただくことが多く、一方で、ここには書いてない細かい失敗もあったりと、学びも多い1年でした。</p> <p>この記事を読んで、私ならちょちょいのちょいと思った方は、読者になるボタンの登録、採用エントリー、よろしくお願いします!</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech プロダクトエンジニアというキャリアについて考えてみる hatenablog://entry/6801883189066312955 2023-12-14T07:00:00+09:00 2023-12-14T07:00:02+09:00 こちらの記事はhacomono Advent Calendar 2023の14日目の記事です はじめに 開発本部 フィーチャーグループのこじこじ です。 プロダクトエンジニアという職種を聞いたことがあるでしょうか? 先日、Product Engineer Night というプロダクト志向を持つエンジニアのミートアップイベントの第一回に参加してきました。 自分にとって単にフルスタックエンジニアとかという言葉では、微妙にしっくり来なかった部分がきれいにハマった感覚があり、思った事を書いてみます。 プロダクトエンジニアとは Product Engineer Nightを主催してくださった株式会社アセ… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231213/20231213135122.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>こちらの記事は<a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono Advent Calendar 2023</a>の14日目の記事です</p></blockquote> <h1 id="はじめに">はじめに</h1> <p>開発本部 フィーチャーグループの<a href="https://x.com/koji_kono">こじこじ</a> です。</p> <p>プロダクトエンジニアという職種を聞いたことがあるでしょうか?<br/> 先日、<a href="https://product-engineer.connpass.com/event/301502/">Product Engineer Night</a> というプロダクト志向を持つエンジニアのミートアップイベントの第一回に参加してきました。<br/> 自分にとって単にフルスタックエンジニアとかという言葉では、微妙にしっくり来なかった部分がきれいにハマった感覚があり、思った事を書いてみます。</p> <h1 id="プロダクトエンジニアとは">プロダクトエンジニアとは</h1> <p>Product Engineer Nightを主催してくださった株式会社アセンドのCTO、<a href="https://x.com/niwa_takeru">丹羽さん</a>の<a href="https://note.com/niwa_takeru">記事</a> がとても良くまとめられててわかりやすいです。(こちらを読めば正直この記事は読まなくてもいいくらい…)<br/> 同じ事を書いても仕方がないので、詳細は省きますが、簡単に言うと「プロダクトの価値追求を開発の中心に据えて、なんでもやるエンジニア」と自分は理解してます。</p> <h2 id="いわゆるフルスタックとの違い">いわゆるフルスタックとの違い</h2> <p>フロントエンドもバックエンドもバリバリできる人をフルスタックエンジニアというのが一般的かと思います。技術領域を問わず「なんでもやる」という点では同じですが、プロダクトエンジニアはよりプロダクトの価値追求に重きをおいているので、UXデザインや業務ドメインなど関心領域は広くなり、機能開発全体にオーナーシップを持ちます。スペシャリストというよりジェネラリストに近いニュアンスだと思います。</p> <h2 id="求められる能力や役割">求められる能力や役割</h2> <h3 id="開発設計">開発・設計</h3> <p>フルスタックエンジニアなどと比較すると、技術領域への深い専門性が必須というわけではありませんが、エンジニアなので軸足となるのは技術力です。詳細設計や実装・テストを独力でやりきれる技術力が望ましいです。</p> <h3 id="業務ドメイン理解">業務・ドメイン理解</h3> <p>求められる機能を実現し優れたユーザー体験や顧客体験を提供するために、業務への理解やドメインへの高い解像度をもつことが重要です。本質的な課題解決に向けて機能やプロダクトをあるべき形を実現するのために、hacomonoのvalueで言うところのコア・シンキングやウィズ・カスタマーを発揮するのがプロダクトエンジニアの役割だと思います。</p> <h3 id="巻き込み力">巻き込み力</h3> <p>技術・UXデザイン・ドメインと横断的にパフォーマンスを発揮する必要があるものの、一人で全てて正しい判断をすることは困難というか不可能だと思います。適切なタイミングで適切な領域のスペシャリストやステークホルダーをオーナーシップを持って巻き込んで進めて行く姿勢が大切だと考えます。</p> <h1 id="メリデメ">メリデメ</h1> <p>プロダクトの価値を中心に据えて領域をまたいで動くので、より本質的な課題と向き合ったり、全体最適を図ったりできる一方で、属人的になってしまうことや一人前に熟達する事が困難であるといった懸念もあるかと思います。スキルやコンピテンシー以前に結構なバイタリティとフィジカルを求められているのかもしれません…</p> <h1 id="キャリアについて">キャリアについて</h1> <p>深いドメイン理解がプロダクトエンジニアの特に強みだと思ってます。特定のドメインへの理解を深めていってもが付きますし、業務への高い解像度を活かしてBizサイドも視野に入るかもしれません。いろんな可能性があってロマンですね。</p> <p>toBやtoBtoCだと求められる要件は特に複雑になり、全てを完璧に言語化するのは非常に困難だと思います。今年は特にChat GPTを始め生成AIの躍進がめざましく、エンジニアの将来が脅かされる〜みたいなことが話題に上がることも多いですが、より深いドメイン理解と業務解像度をもってうまく使いこなすことが重要なのだと思います。</p> <h1 id="おわりに">おわりに</h1> <p>アーキテクチャやフレームワーク、言語仕様など技術的な関心はもちろんですが、それ以上に「よりぶっ刺さるプロダクトにするために必要なことをやりたい」というモチベーションで仕事をしています。そんな自分には”プロダクトエンジニア”という役割はしっくり来るものがありました。</p> <p>個人的な話ですが、先日10月1日でhacomonoに入社して1年が経ちました。<br/> この一年ちょっとを振り返ってみて、仕事やメンバー間の愚痴みたいなものを聞いた記憶がなく、自分も言おうとも思ったこともないことに驚きました。<br/> ホスピタリティが高くオープン・アンド・フェアネスなどのバリュー体現の高いメンバーが集まってとても良いカルチャーが築かれているんだなぁと思いちょっとした自社自慢です。</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech Arc Browser はいいぞ! hatenablog://entry/6801883189066207615 2023-12-13T07:00:00+09:00 2023-12-15T10:02:10+09:00 こちらの記事はhacomono Advent Calendar 2023の13日目の記事です どうも、みゅーとんです. 社用PC には, 常用ブラウザとして Arc Browser を, 開発検証用として Chrome 等他ブラウザをインストールして使用しています. 実は Arc Browser は Mac のみサポートしているブラウザなのですが, 12/12 の 0時頃に Windows Beta 版が利用できるようになった通知があり, 非常に嬉しくなったので, その勢いでここで紹介してみようかなと思いました. 3行でまとめ タブ管理が優れている Boost 機能で常に開くページをカスタムで… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231213/20231213090125.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>こちらの記事は<a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono Advent Calendar 2023</a>の13日目の記事です</p></blockquote> <p>どうも、<a href="https://twitter.com/_mew_ton">みゅーとん</a>です.</p> <p>社用PC には, 常用ブラウザとして <a href="https://arc.net/">Arc Browser</a> を, 開発検証用として Chrome 等他ブラウザをインストールして使用しています.</p> <p>実は Arc Browser は Mac のみサポートしているブラウザなのですが, 12/12 の 0時頃に Windows Beta 版が利用できるようになった通知があり, 非常に嬉しくなったので, その勢いでここで紹介してみようかなと思いました.</p> <h3 id="3行でまとめ">3行でまとめ</h3> <ul> <li>タブ管理が優れている</li> <li>Boost 機能で常に開くページをカスタムできる</li> <li>動画や通話画面がポップアウトされて、ながら作業が超快適</li> </ul> <h3 id="Arc-Browser-とは-">Arc Browser とは ?</h3> <p>Chromium ベースの新しいブラウザです. <br/> 現状は mac でのみ動作がサポートされています.</p> <p>メディアサイトにも掲載がされており, mac ユーザ向けに注目を集めているブラウザかなと思います.</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgigazine.net%2Fnews%2F20230727-arc-web-browser%2F" title="「ウェブの世界のOSを目指す」という壮大な目的で作成されたブラウザ「Arc」レビュー、使いやすい機能がてんこもりでサクサク快適にブラウジング可能" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://gigazine.net/news/20230727-arc-web-browser/">gigazine.net</a></cite></p> <p>Chromium ベースなので, chrome 拡張は基本的にすべて使えますが、特筆すべきは “タブ管理の概念が一新された” ことかなと思います.</p> <p>arc の推しの機能をいくつか紹介していきます.</p> <h3 id="優れたタブ管理">優れたタブ管理</h3> <p>Arc には、ほかブラウザにあるような “ブックマーク” の概念がない代わりに, タブに “Favorite” “Pin” “Today” の 3種のタイプが存在します.</p> <p>Arc ブラウザを開くとウィンドウ左にタブ一覧が表示され, 上から “Favorite” “Pin” “Today” の順に並んでいます.</p> <p><figure class="figure-image figure-image-fotolife" title="Arc ブラウザの外観、タブ配置"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231212/20231212155940.png" alt="Arc &#x30D6;&#x30E9;&#x30A6;&#x30B6;&#x306E;&#x30BF;&#x30D6;&#x914D;&#x7F6E;&#x3092;&#x8AAC;&#x660E;&#x3057;&#x3066;&#x3044;&#x307E;&#x3059;. &#x30D6;&#x30E9;&#x30A6;&#x30B6;&#x306E;&#x5DE6;&#x90E8;&#x306B;&#x30BF;&#x30D6;&#x304C;&#x7E26;&#x306B;&#x4E26;&#x3093;&#x3067;&#x304A;&#x308A;&#x3001;&#x4E0A;&#x304B;&#x3089; &amp;quot;Favorite&amp;quot; &amp;quot;Pinned&amp;quot; &amp;quot;Today&amp;quot; &#x306B;&#x30AB;&#x30C6;&#x30B4;&#x30E9;&#x30A4;&#x30BA;&#x3055;&#x308C;&#x3066;&#x3044;&#x307E;&#x3059;&#x3002;" width="1200" height="915" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Arc ブラウザの外観、タブ配置</figcaption></figure></p> <p>基本的に開いたタブは “Today” にあり, 好みで “Pinned” “Favorite” に移動できます.</p> <p>それぞれ, 意味合いが異なり, 以下のように管理されます</p> <ul> <li>Favorite .. 常にトップにおいておき、セッションを維持しておきたいものを配置する. アプリ一覧のように利用できる.</li> <li>Pinned .. 残しておきたいタブをピン止めしたもの. ピン止めしたサイトから移動しても, ワンボタンか開き直しで元のサイトに戻る.</li> <li>Today .. 新規タブのデフォルト. 12時間後に自動アーカイブされるので, 翌日ブラウザを開くと消えている.</li> </ul> <p>この管理がデフォルトでできるのが非常に優れていると思っています.</p> <p>私の場合は, GitHub の Repository ごとに Tab をピン止めしてあり, すぐにアクセスできるようにしてあります. また, TypeScript Playground を Favorite にしておくことで, いつでも手軽に型パズルを解けます.</p> <p>通常Chrome などのブラウザだと, タブを常に残してしまうと管理が大変になってしまいます. タブ管理がそもそも面倒でやらなくなるんですよね.</p> <p>Arc の場合は “Today タブは必ず消えるルール” があるため、タブのピン止め管理が必然的に必須となり, “タブを追加したときに、このタブを残すかどうかを考慮する” 手間が必ず増えます.</p> <p>言い換えると, すべてのタブが管理を強制されることで, 整理を促されるということかなと思います. おかげでブラウジングも快適な状態を維持できます.</p> <p>ちなみに, Favorite タブ一覧の上にあるのは, Pin 止めした Chrome 拡張です. 1password などを多様するのですが, ここに出てくるのはかなり良いですね.</p> <p><figure class="figure-image figure-image-fotolife" title="ピン止めされた Chrome 拡張の一覧"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231212/20231212160116.png" alt="Favorite &#x30BF;&#x30D6;&#x30AB;&#x30C6;&#x30B4;&#x30EA;&#x306E;&#x4E0A;&#x90E8;&#x3001;&#x30A2;&#x30C9;&#x30EC;&#x30B9;&#x30D0;&#x30FC;&#x3068;&#x306E;&#x9593;&#x306B;&#x3001; &#x30D4;&#x30F3;&#x6B62;&#x3081;&#x3055;&#x308C;&#x305F; Chrome &#x62E1;&#x5F35;&#x304C;&#x4E00;&#x89A7;&#x8868;&#x793A;&#x3055;&#x308C;&#x3066;&#x3044;&#x307E;&#x3059;&#x3002;" width="494" height="692" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ピン止めされた Chrome 拡張の一覧</figcaption></figure></p> <h3 id="Boost-機能でページをカスタマイズ">Boost 機能でページをカスタマイズ</h3> <p>Chrome 拡張で, X (twitter) の表示領域を小さくして, シンプルな見た目にするものをよく利用していましたが, Arc ではそれが全ページ対応標準搭載です.</p> <p>表示するページの配色や文字サイズ, 指定要素の非表示化などを自由にカスタムする Boost 機能があります.</p> <p>以下は Twitter のレイアウトを非表示している様子です.</p> <p><figure class="figure-image figure-image-fotolife" title="twitter の外観を Boost している様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231212/20231212160635.gif" alt="Twitter &#x306E;&#x30EC;&#x30A4;&#x30A2;&#x30A6;&#x30C8;&#x3092; Arc Browser &#x306E; Boost &#x6A5F;&#x80FD;&#x3092;&#x4F7F;&#x7528;&#x3057;&#x3066;&#x3001;&#x6539;&#x826F;&#x3057;&#x3066;&#x3044;&#x308B;&#x69D8;&#x5B50;&#x3067;&#x3059;&#x3002;&#x30B5;&#x30A4;&#x30C9;&#x30D0;&#x30FC;&#x306E;&#x691C;&#x7D22;&#x3084;&#x30E1;&#x30CB;&#x30E5;&#x30FC;&#x3092;&#x975E;&#x8868;&#x793A;&#x306B;&#x3057;&#x3001;&#x30BF;&#x30A4;&#x30E0;&#x30E9;&#x30A4;&#x30F3;&#x306E;&#x307F;&#x3092;&#x8868;&#x793A;&#x3059;&#x308B;&#x30EC;&#x30A4;&#x30A2;&#x30A6;&#x30C8;&#x306B;&#x5909;&#x66F4;&#x3057;&#x3066;&#x3044;&#x307E;&#x3059;&#x3002;" width="640" height="480" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>twitter の外観を Boost している様子</figcaption></figure></p> <p>Boost の手にかかれば, DeepL 翻訳ページもこんな感じで超シンプルになります. (画面下の灰色はもともとコンテンツがあったところを非表示にしています.)</p> <p><figure class="figure-image figure-image-fotolife" title="DeepL を Boost した結果"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231212/20231212161204.png" alt="DeepL &#x306E;&#x7FFB;&#x8A33;&#x6A5F;&#x80FD;&#x30DA;&#x30FC;&#x30B8;&#x3092; Arc Browser &#x3067;&#x6700;&#x9069;&#x5316;&#x3057;&#x305F;&#x7D50;&#x679C;&#x3067;&#x3059;&#x3002;&#x30D8;&#x30C3;&#x30C0;&#x30FC;&#x306E; pricing &#x306A;&#x3069;&#x306E;&#x9805;&#x76EE;&#x3084;&#x3001; &#x30D5;&#x30C3;&#x30BF;&#x30FC;&#x306E;&#x4E0D;&#x8981;&#x306A;&#x6587;&#x7AE0;&#x304C;&#x307E;&#x308B;&#x3054;&#x3068;&#x975E;&#x8868;&#x793A;&#x306B;&#x306A;&#x3063;&#x3066;&#x3044;&#x307E;&#x3059;&#x3002;" width="1200" height="1055" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>DeepL を Boost した結果</figcaption></figure></p> <p>不要な領域を削除して, 注視したい領域を広げたり, 情報のノイズを減らしたりできるので, 効率も良くなりますね!</p> <p>また, Boost 設定は共有することができ, 他者が作った Boost を利用したりもできます.</p> <p><figure class="figure-image figure-image-fotolife" title="Arc Boost Gallery"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231212/20231212161322.png" alt="Arc Boost Gallery &#x3067;&#x3059;&#x3002; Arc Browser &#x3067;&#x4ED6;&#x8005;&#x304C;&#x4F5C;&#x6210;&#x3057;&#x305F; Boost &#x8A2D;&#x5B9A;&#x306E;&#x4E00;&#x89A7;&#x304C;&#x8868;&#x793A;&#x3055;&#x308C;&#x3066;&#x3044;&#x307E;&#x3059;&#x3002;" width="1200" height="713" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Arc Boost Gallery</figcaption></figure></p> <h3 id="動画をみながらブラウジング">動画をみながらブラウジング</h3> <p>一番便利な機能かなと思います.</p> <p>youtube の動画を再生したまま別のタブを表示すると・・・</p> <p><figure class="figure-image figure-image-fotolife" title="youtube"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231212/20231212161435.png" alt="youtube &#x3067;&#x52D5;&#x753B;&#x3092;&#x958B;&#x3044;&#x3066;&#x3044;&#x307E;&#x3059;&#x3002;" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>youtube</figcaption></figure></p> <p>映像部分がポップアウトしてくれます.</p> <p><figure class="figure-image figure-image-fotolife" title="youtube を popout している様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231212/20231212161523.png" alt="Arc Browser &#x3067; youtube &#x52D5;&#x753B;&#x3092;&#x518D;&#x751F;&#x3057;&#x305F;&#x307E;&#x307E;&#x3001;&#x5225;&#x30BF;&#x30D6;&#x3092;&#x958B;&#x3044;&#x305F;&#x76F4;&#x5F8C;&#x306E;&#x69D8;&#x5B50;&#x3067;&#x3059;&#x3002; &#x958B;&#x3044;&#x3066;&#x3044;&#x308B;&#x30BF;&#x30D6;&#x306F; Google &#x306E;&#x30C8;&#x30C3;&#x30D7;&#x30DA;&#x30FC;&#x30B8;&#x3067;&#x3001;&#x4E2D;&#x592E;&#x306B;&#x691C;&#x7D22;&#x30EF;&#x30FC;&#x30C9;&#x306E;&#x5165;&#x529B;&#x6B04;&#x304C;&#x3042;&#x308A;&#x307E;&#x3059;&#x304C;&#x3001;&#x753B;&#x9762;&#x53F3;&#x4E0B;&#x306B;&#x306F;&#x518D;&#x751F;&#x4E2D;&#x306E; youtube &#x306E;&#x52D5;&#x753B;&#x304C;&#x6D41;&#x308C;&#x3066;&#x3044;&#x307E;&#x3059;&#x3002;" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>youtube を popout している様子</figcaption></figure></p> <p>Meet の通話でも同じことができます.</p> <p><figure class="figure-image figure-image-fotolife" title="Meet 通話を popout している様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231212/20231212162404.png" alt="Arc Browser &#x30C0;&#x30A6;&#x30F3;&#x30ED;&#x30FC;&#x30C9;&#x30DA;&#x30FC;&#x30B8;&#x304C;&#x8868;&#x793A;&#x3055;&#x308C;&#x3066;&#x3044;&#x307E;&#x3059;&#x304C;&#x3001;&#x53F3;&#x4E0A;&#x306B; Meet &#x901A;&#x8A71;&#x753B;&#x9762;&#x304C;&#x8868;&#x793A;&#x3055;&#x308C;&#x3066;&#x3044;&#x307E;&#x3059;&#x3002;" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Meet 通話を popout している様子</figcaption></figure></p> <p>通話しながら notion をまとめたり, ググったりするときに, タブの位置を気にすることがないのはかなり便利です.</p> <p>ウィンドウが別なので, ポップアウトを別モニターに映すとじゃまにならなくて更に良いですね.</p> <p>会議中にサボるにはかなり快適なしくみです.</p> <h3 id="まとめ">まとめ</h3> <p>まだまだ語れていない推しポイントは多いですが, 今回はその主要なところを挙げてみました.</p> <p>これで作業効率アップ間違いなしですね. 全社導入を目指して, これからも推していきます.</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech LocalStackで簡単AWS開発 hatenablog://entry/6801883189065892103 2023-12-12T11:00:00+09:00 2023-12-12T11:00:00+09:00 こちらの記事はhacomono Advent Calendar 2023の12日目の記事です 自己紹介 おはこんばんちは。 hacomono 開発部の iwazer です。 久しぶりの登場ですが、この間にティアキンも Cyberpunk2077 最終 DLC も Starfield も全部発売されました。 みなさんクリアしたでしょうか。私はクリアしました。 今回は AWS を使った開発をラクチンにしてくれる LocalStack について軽く書こうと思います。 概要 LocalStack はローカル環境でAWSクラウドサービスをエミュレートするためのフレームワークです。 AWS に実際に接続す… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231211/20231211150632.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>こちらの記事は<a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono Advent Calendar 2023</a>の12日目の記事です</p></blockquote> <h2 id="自己紹介">自己紹介</h2> <p>おはこんばんちは。 hacomono 開発部の iwazer です。</p> <p>久しぶりの登場ですが、この間にティアキンも Cyberpunk2077 最終 DLC も Starfield も全部発売されました。<br/> みなさんクリアしたでしょうか。私はクリアしました。</p> <p>今回は AWS を使った開発をラクチンにしてくれる <a href="https://docs.localstack.cloud/overview/">LocalStack</a> について軽く書こうと思います。</p> <h2 id="概要">概要</h2> <p>LocalStack はローカル環境でAWSクラウドサービスをエミュレートするためのフレームワークです。<br/> AWS に実際に接続することなく AWS を使ったアプリケーションを動かしながら開発することができます。<br/> ここ最近、利用できるようになったサービスが一気に増えたようです。<br/> そのうち高度なものは Pro 以上の有料版を必要としますが、無料のコミュニティ版でサポートされているサービスも十分役に立つラインナップになっていると感じます。</p> <p>簡単なサンプルを紹介しつつ、手軽さを実感してもらえれば嬉しいです。</p> <h2 id="動かしてみる">動かしてみる</h2> <p>docker-compose を使って起動するのが手っ取り早いと思います。</p> <p>とりあえず SystemManager と S3 サービスのみで起動してみます。<br/> <code>environment/SERVICES</code> にカンマ区切りでサービスを複数、指定できます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink>&gt; cat docker-compose-localstack.yml <span class="synIdentifier">version</span><span class="synSpecial">:</span> <span class="synConstant">&quot;3.8&quot;</span> <span class="synIdentifier">services</span><span class="synSpecial">:</span> <span class="synIdentifier">localstack</span><span class="synSpecial">:</span> <span class="synIdentifier">image</span><span class="synSpecial">:</span> localstack/localstack <span class="synIdentifier">environment</span><span class="synSpecial">:</span> <span class="synStatement">- </span>SERVICES=ssm,s3 <span class="synIdentifier">ports</span><span class="synSpecial">:</span> <span class="synStatement">- </span>4566:4566 </pre> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">&gt;</span> docker-compose <span class="synSpecial">-f</span> docker-compose-localstack.yml up <span class="synSpecial">-d</span> <span class="synComment">:</span> ✔ Container docker-localstack-1 Started </pre> <p>aws コマンドが使えるように Credential を作っておきます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">&gt;</span> aws configure AWS Access Key ID <span class="synStatement">[</span>None<span class="synStatement">]</span>: dummy AWS Secret Access Key <span class="synStatement">[</span>None<span class="synStatement">]</span>: dummy Default region name <span class="synStatement">[</span>ap-northeast<span class="synStatement">-1]</span>: Default output format <span class="synStatement">[</span>None<span class="synStatement">]</span>: <span class="synStatement">&gt;</span> <span class="synStatement">cat</span> ~/.aws/credentials <span class="synStatement">[</span>default<span class="synStatement">]</span> aws_access_key_id <span class="synStatement">=</span> dummy aws_secret_access_key <span class="synStatement">=</span> dummy </pre> <p>S3 に画像を格納してみます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">&gt;</span> <span class="synStatement">ls</span> <span class="synSpecial">-l</span> ./images -rw-r--r--@ <span class="synConstant">1</span> iwazzer staff <span class="synConstant">419138</span> <span class="synConstant">12</span> <span class="synConstant">7</span> 17:39 sample.png <span class="synStatement">&gt;</span> aws <span class="synSpecial">--endpoint-url</span> <span class="synStatement">&lt;</span>http://localhost:456<span class="synStatement">6&gt;</span> s3 mb s3://images make_bucket: images <span class="synStatement">&gt;</span> aws <span class="synSpecial">--endpoint-url</span> <span class="synStatement">&lt;</span>http://localhost:456<span class="synStatement">6&gt;</span> s3 <span class="synStatement">ls</span> 2023-12-07 17:40:58 images <span class="synStatement">&gt;</span> aws <span class="synSpecial">--endpoint-url</span> <span class="synStatement">&lt;</span>http://localhost:456<span class="synStatement">6&gt;</span> s3 <span class="synStatement">cp</span> images/sample.png s3://images/background.png upload: images/sample.png to s3://images/background.png </pre> <p>Parameter Store に S3 に格納した情報を登録してみます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">&gt;</span> aws <span class="synSpecial">--endpoint-url</span> <span class="synStatement">&lt;</span>http://localhost:456<span class="synStatement">6&gt;</span> ssm put-parameter <span class="synSpecial">\\</span> <span class="synSpecial">--name</span> <span class="synStatement">'</span><span class="synConstant">/background-image</span><span class="synStatement">'</span> <span class="synSpecial">\\</span> <span class="synSpecial">--type</span> <span class="synStatement">'</span><span class="synConstant">String</span><span class="synStatement">'</span> <span class="synSpecial">\\</span> <span class="synSpecial">--value</span> <span class="synStatement">'</span><span class="synConstant">background.png</span><span class="synStatement">'</span> <span class="synSpecial">{</span> <span class="synStatement">&quot;</span><span class="synConstant">Version</span><span class="synStatement">&quot;</span>: <span class="synConstant">1</span> <span class="synSpecial">}</span> <span class="synStatement">&gt;</span> aws <span class="synSpecial">--endpoint-url</span> <span class="synStatement">&lt;</span>http://localhost:456<span class="synStatement">6&gt;</span> ssm get-parameter <span class="synSpecial">--name</span> <span class="synStatement">'</span><span class="synConstant">/background-image</span><span class="synStatement">'</span> <span class="synSpecial">{</span> <span class="synStatement">&quot;</span><span class="synConstant">Parameter</span><span class="synStatement">&quot;</span>: <span class="synSpecial">{</span> <span class="synStatement">&quot;</span><span class="synConstant">Name</span><span class="synStatement">&quot;</span>: <span class="synStatement">&quot;</span><span class="synConstant">/background-image</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">Type</span><span class="synStatement">&quot;</span>: <span class="synStatement">&quot;</span><span class="synConstant">String</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">Value</span><span class="synStatement">&quot;</span>: <span class="synStatement">&quot;</span><span class="synConstant">background.png</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">Version</span><span class="synStatement">&quot;</span>: <span class="synConstant">1</span>, <span class="synStatement">&quot;</span><span class="synConstant">LastModifiedDate</span><span class="synStatement">&quot;</span>: <span class="synStatement">&quot;</span><span class="synConstant">2023-12-07T17:44:01.051000+09:00</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">ARN</span><span class="synStatement">&quot;</span>: <span class="synStatement">&quot;</span><span class="synConstant">arn:aws:ssm:ap-northeast-1:000000000000:parameter/background-image</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">DataType</span><span class="synStatement">&quot;</span>: <span class="synStatement">&quot;</span><span class="synConstant">text</span><span class="synStatement">&quot;</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <p>というふうに、とても簡単に利用できます。</p> <h2 id="アプリケーションから利用する">アプリケーションから利用する</h2> <p>当たり前ですが AWS が提供している SDK を使ってアクセスすることができます。</p> <p>私達は主に Ruby / Rails を使ってサービス開発しておりますので、Ruby の例で紹介します。</p> <p>使う aws-sdk をインストールします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">&gt;</span> gem install aws-sdk-s3 <span class="synStatement">&gt;</span> gem install aws-sdk-ssm </pre> <p>Parameter Store から情報を取得します。(<a href="https://www.rubydoc.info/gems/aws-sdk-ssm/Aws/SSM/Client">aws-sdk-ssm reference</a>)</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink>&gt; irb <span class="synConstant">3.0</span>.<span class="synConstant">6</span> :<span class="synConstant">001</span> &gt; <span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">aws-sdk-ssm</span><span class="synSpecial">'</span> <span class="synComment">#=&gt; true</span> <span class="synConstant">3.0</span>.<span class="synConstant">6</span> :<span class="synConstant">002</span> &gt; <span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">aws-sdk-s3</span><span class="synSpecial">'</span> <span class="synComment">#=&gt; true</span> <span class="synConstant">3.0</span>.<span class="synConstant">6</span> :<span class="synConstant">003</span> &gt; ssm_client = <span class="synType">Aws</span>::<span class="synType">SSM</span>::<span class="synType">Client</span>.new(<span class="synConstant">region</span>: <span class="synSpecial">'</span><span class="synConstant">ap-northeast-1</span><span class="synSpecial">'</span>, <span class="synConstant">access_key_id</span>: <span class="synSpecial">'</span><span class="synConstant">dummy</span><span class="synSpecial">'</span>, <span class="synConstant">secret_access_key</span>: <span class="synSpecial">'</span><span class="synConstant">dummy</span><span class="synSpecial">'</span>, <span class="synConstant">endpoint</span>: <span class="synSpecial">'</span><span class="synConstant">&lt;http://localhost:4566&gt;</span><span class="synSpecial">'</span>) <span class="synComment">#=&gt; #&lt;Aws::SSM::Client&gt;</span> <span class="synConstant">3.0</span>.<span class="synConstant">6</span> :<span class="synConstant">004</span> &gt; r = ssm_client.get_parameter(<span class="synConstant">name</span>: <span class="synSpecial">'</span><span class="synConstant">/background-image</span><span class="synSpecial">'</span>) <span class="synComment">#=&gt;</span> <span class="synComment">#&lt;struct Aws::SSM::Types::GetParameterResult</span> ... <span class="synConstant">3.0</span>.<span class="synConstant">6</span> :<span class="synConstant">005</span> &gt; file = r.parameter.value <span class="synComment">#=&gt; &quot;background.png&quot;</span> </pre> <p>続いて、S3 からイメージファイルを取得します。(<a href="https://www.rubydoc.info/gems/aws-sdk-s3/Aws/S3/Client">aws-sdk-s3 reference</a>)</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synConstant">3.0</span>.<span class="synConstant">6</span> :<span class="synConstant">006</span> &gt; s3_client = <span class="synType">Aws</span>::<span class="synType">S3</span>::<span class="synType">Client</span>.new(<span class="synConstant">region</span>: <span class="synSpecial">'</span><span class="synConstant">ap-northeast-1</span><span class="synSpecial">'</span>, <span class="synConstant">access_key_id</span>: <span class="synSpecial">'</span><span class="synConstant">dummy</span><span class="synSpecial">'</span>, <span class="synConstant">secret_access_key</span>: <span class="synSpecial">'</span><span class="synConstant">dummy</span><span class="synSpecial">'</span>, <span class="synConstant">endpoint</span>: <span class="synSpecial">'</span><span class="synConstant">&lt;http://localhost:4566&gt;</span><span class="synSpecial">'</span>, <span class="synConstant">force_path_style</span>: <span class="synConstant">true</span>) <span class="synComment">#=&gt; #&lt;Aws::S3::Client&gt;</span> <span class="synConstant">3.0</span>.<span class="synConstant">6</span> :<span class="synConstant">007</span> &gt; r = s3_client.get_object(<span class="synConstant">bucket</span>: <span class="synSpecial">'</span><span class="synConstant">images</span><span class="synSpecial">'</span>, <span class="synConstant">key</span>: file) <span class="synComment">#=&gt;</span> <span class="synComment">#&lt;struct Aws::S3::Types::GetObjectOutput</span> ... <span class="synConstant">3.0</span>.<span class="synConstant">6</span> :008 &gt; <span class="synType">File</span>.open(<span class="synSpecial">'</span><span class="synConstant">./images/downloaded.png</span><span class="synSpecial">'</span>, <span class="synSpecial">'</span><span class="synConstant">w</span><span class="synSpecial">'</span>) { |f| f.write(r.body.read) } <span class="synComment">#=&gt; 419138</span> &gt; ls -l ./images -rw-r--r-- <span class="synConstant">1</span> iwazzer staff <span class="synConstant">419138</span> <span class="synConstant">12</span> <span class="synConstant">7</span> <span class="synConstant">18</span>:<span class="synConstant">38</span> downloaded.png -rw-r--r--@ <span class="synConstant">1</span> iwazzer staff <span class="synConstant">419138</span> <span class="synConstant">12</span> <span class="synConstant">7</span> <span class="synConstant">17</span>:<span class="synConstant">39</span> sample.png </pre> <p>ローカルだけで実際に動かしてみながらコーディングできるので、スムーズに開発できます。<br/> デプロイ後に期待通りに動くかは aws-sdk のクライアントインスタンスを生成する Credential の違いだけ気にすれば良いでしょう。</p> <h2 id="LocalStackがサポートしているサービス">LocalStackがサポートしているサービス</h2> <p>サポートしているサービスを知るには公式の <a href="https://docs.localstack.cloud/user-guide/aws/feature-coverage/">AWS Service Feature Coverage</a> を参照してください。</p> <p>無料のコミュニティ版でも StepFunctions, Lambda, DynamoDB など、よく使われるサービスが含まれていて有用です。</p> <p>Pro とマークされているサービスは有償の Pro 版でのサポートです。<br/> ECS, EKS, Cognito, CloudFront など、ローカルで手軽に試せれば便利そうなので、いちど使ってみたいと考えています。どこまでできるのか気になりますね。</p> <p>その際はまたエントリーを書こうかなと考えています。</p> <h2 id="まとめ">まとめ</h2> <p>LocalStack は AWS サービスを使った開発を、とても手軽にしてくれる大変便利なプロダクトです。<br/> サポートされているサービスが増えたことで、多くの現場で使える機会が増えたと思います。<br/> AWS を使った開発のトライアンドエラーをしやすくなり、敷居を下げてくれる効果が期待できますね。<br/> 単体テストにも利用できると思いますが、そちらはモックを使ったほうがいいかなと個人的には思います(といいつつ一部使っていますが。笑</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech 少しずつ進めるドキュメント改善 hatenablog://entry/6801883189065056692 2023-12-10T07:00:00+09:00 2023-12-10T23:41:16+09:00 こちらの記事はhacomono Advent Calendar 2023の10日目の記事です。 はじめに こんにちは。今年2023年の10月に入社した@gaaamiiです。UXチームというチームに配属となり、主にウェブフロントエンドの開発やAPIの改修に関わっています。アドベントカレンダーに乗じて、入社して初のTechブログ記事を投稿させていただきます! この記事では、私がいま関わっている大きめのリポジトリのフロントエンド部分において、ドキュメントを少しずつ改善していく話を書きます。 背景 今回改善しようとしたのは、hacomonoのメンバーサイトと呼ばれるウェブアプリケーションについてのドキ… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231208/20231208150458.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>こちらの記事は<a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono Advent Calendar 2023</a>の10日目の記事です。</p></blockquote> <h2 id="はじめに">はじめに</h2> <p>こんにちは。今年2023年の10月に入社した<a href="https://twitter.com/gaaamii">@gaaamii</a>です。UXチームというチームに配属となり、主にウェブフロントエンドの開発やAPIの改修に関わっています。アドベントカレンダーに乗じて、入社して初のTechブログ記事を投稿させていただきます!</p> <p>この記事では、私がいま関わっている大きめのリポジトリのフロントエンド部分において、ドキュメントを少しずつ改善していく話を書きます。</p> <h2 id="背景">背景</h2> <p>今回改善しようとしたのは、hacomonoのメンバーサイトと呼ばれるウェブアプリケーションについてのドキュメントです。メンバーサイトはメインサービスのエンドユーザー向けの画面であり、開発にも多くのエンジニアが関わっています。メインサービスということもあり大きく昔から動いているものなので、現在もVue 2で動いており、コンポーネントの書き方も複数混在していました。</p> <p>Vueは2と3で書き方が変わっている部分がいくつかあります。たとえばメンバーサイトでいうと、Vue 2時代にTypeScriptの恩恵を受けるためにVue Class Componentというライブラリを使ってクラスベースの書き方をしていましたが、このライブラリの利用はVue 3では推奨されておらず、ライブラリ自体のメンテナンスがされていない状況です。このような状況でVue 3への移行を意識すると、少なくとも新規の実装においてはこうした書き方はされないようにしたいです。</p> <p>こういった注意が必要なコーディングがところどころにあるので、開発者で認識を合わせ、ドキュメントでも推奨の書き方と非推奨の書き方を明示しておきたいと考えました。</p> <h2 id="更新しやすく参照しやすいドキュメントにするためGitHubリポジトリに移動">更新しやすく参照しやすいドキュメントにするため、GitHubリポジトリに移動</h2> <p>メンバーサイトのドキュメントはNotionのページで管理されていましたが、ところどころ古い内容が残っており、ドキュメントの更新がしづらい状況になっているのではと感じました。また、コーディング時はいつもこのNotionページを開いているわけではないので、必要になるたびにNotionのページを検索する必要があり、参照もややしづらいと感じていました。</p> <p>そこで、GitHubリポジトリに同様の内容を書いたMarkdownファイルを置き、Notionページのほうは廃止することにしました(こういった相談をするため、隔週でフロントエンド実装に関わる人が参加する相談会を設けさせていただいています。これについても後述します)。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231208/20231208115825.png" alt="&#x30C9;&#x30AD;&#x30E5;&#x30E1;&#x30F3;&#x30C8;&#x3092;&#x30EA;&#x30DD;&#x30B8;&#x30C8;&#x30EA;&#x3078;&#x79FB;&#x52D5;&#x3057;&#x305F;&#x3068;&#x304D;&#x306E;Pull Request&#x306E;&#x30B9;&#x30AF;&#x30EA;&#x30FC;&#x30F3;&#x30B7;&#x30E7;&#x30C3;&#x30C8;" width="1200" height="637" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>現在は、開発ドキュメントは <code>docs/</code> ディレクトリにまとめ、プロジェクト直下のREADME.mdから各ドキュメントへのリンクが貼られています。個人的にはREADME.mdを開けば探しているものが見つかるというのが嬉しいところです。</p> <p><figure class="figure-image figure-image-fotolife" title="メンバーサイトのリポジトリ直下のREADME.md"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231208/20231208115933.png" alt="&#x30E1;&#x30F3;&#x30D0;&#x30FC;&#x30B5;&#x30A4;&#x30C8;&#x306E;&#x30EA;&#x30DD;&#x30B8;&#x30C8;&#x30EA;&#x76F4;&#x4E0B;&#x306E;README.md" width="1200" height="896" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>メンバーサイトのリポジトリ直下のREADME.md</figcaption></figure></p> <p>ドキュメントを管理する場所としてどこが良いかは正解がないかとは思いますが、今回のようにコーディングスタイルなどを書いた開発者向けの情報は、GitHubリポジトリでの管理にすることで以下のメリットがあると考えています。</p> <ul> <li>ソースコードと近い場所にドキュメントが置けるため、開発時に参照がしやすい</li> <li>コミットログが残るので歴史を追いやすい</li> <li>Pull Requestの仕組みがあるため、ドキュメントの修正を提案しやすい</li> </ul> <p>一方、開発者以外も閲覧できる・レビューのプロセスを経ずにすぐ更新ができるなど、NotionのようなWikiにもメリットがあるため、ものや状況によってどちらが適しているかは変わるかと思います。</p> <h2 id="推奨と非推奨を区別する">推奨と非推奨を区別する</h2> <p>このドキュメントの「アーキテクチャ・記述方針」の中身を読むと、非推奨の書き方と今後推奨される書き方が区別して記載されています。たとえば以下のような内容です。</p> <blockquote><ul> <li>BaseMixin になんでもアクセサを突っ込んでしまったが故に、全てのコンポーネントが BaseMixin に依存している現状があるが、これを非推奨とする</li> <li>Mixin はそもそも Vue2 の中期までは推奨されていたが、現在は Composition API の登場とともに非推奨となっている</li> <li>mixin は将来的には廃止予定</li> </ul> </blockquote> <p>特に新たに開発に入る開発者にとって、これは重要な情報です。Vueに詳しい開発者がこのドキュメントを読めばmixinを剥がすリファクタリングをしてもいいのだなと考えるでしょうし、Vueに疎い開発者であっても、新たに書く際はmixinの利用を避けるでしょう。</p> <h2 id="暗黙のルールを見つけたらドキュメントを更新してPull-Requestを投げる">暗黙のルールを見つけたらドキュメントを更新してPull Requestを投げる</h2> <p>ドキュメントをGitHubリポジトリに移動しただけでは、少し参照しやすくなった程度であまり効果はありません。今後はコードレビューのタイミングでドキュメントを少しずつ更新していきたいと考えています。</p> <p>コードレビューでコーディングスタイルについて指摘があるということは、開発者間でコードの書き方について認識が合っていないということです。ドキュメントに書いておくと次からそのようなレビューコストが減ります。</p> <p>たとえば、以下は、私がCSSのclass名について指摘をいただいて、ドキュメント更新をするまでの実際の流れです。</p> <p><figure class="figure-image figure-image-fotolife" title="CSSのクラス名の命名についてコードレビューを受け、ついでにドキュメントにも記載。"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231208/20231208120148.png" alt="CSS&#x306E;&#x30AF;&#x30E9;&#x30B9;&#x540D;&#x306B;&#x3064;&#x3044;&#x3066;&#x30B3;&#x30FC;&#x30C9;&#x30EC;&#x30D3;&#x30E5;&#x30FC;&#x3092;&#x53D7;&#x3051;&#x3001;&#x3064;&#x3044;&#x3067;&#x306B;&#x30C9;&#x30AD;&#x30E5;&#x30E1;&#x30F3;&#x30C8;&#x306B;&#x3082;&#x8A18;&#x8F09;&#x3002;" width="1200" height="536" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>CSSのクラス名についてコードレビューを受け、ついでにドキュメントにも記載。</figcaption></figure> <figure class="figure-image figure-image-fotolife" title="ドキュメント修正のPull Requestで、他にも気になるところが見つかり、更新を入れました。"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231208/20231208120214.png" alt="&#x30C9;&#x30AD;&#x30E5;&#x30E1;&#x30F3;&#x30C8;&#x4FEE;&#x6B63;&#x306E;Pull Request&#x3067;&#x3001;&#x4ED6;&#x306B;&#x3082;&#x6C17;&#x306B;&#x306A;&#x308B;&#x3068;&#x3053;&#x308D;&#x304C;&#x898B;&#x3064;&#x304B;&#x308A;&#x3001;&#x66F4;&#x65B0;&#x3092;&#x5165;&#x308C;&#x307E;&#x3057;&#x305F;&#x3002;" width="1200" height="820" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ドキュメント修正のPull Requestで、他にも気になるところが見つかり、更新を入れました。</figcaption></figure></p> <p>こういったことを繰り返していけば、ドキュメントが頻繁に更新され、その時々の最新のものになるのでは、と期待しています。</p> <h2 id="フロントエンド相談会">フロントエンド相談会</h2> <p>些細なことであればPull Requestベースのコミュニケーションで問題ないかと思いますが、あるとより嬉しいのが開発者同士で相談ができる場です。コーディングスタイルや実装手段は開発者ごとにこだわりがある部分もあります。人によって実装方法がぶれやすいところはある程度相談をした上でドキュメントに反映しておきたいです。</p> <p>そこで、隔週でフロントエンド相談会という会を設けるようにしました。ここで、hacomonoのフロントエンドに関わる相談などをしています。ドキュメントをGitHubリポジトリで管理する話などもこの会で相談をして進めました。</p> <h2 id="今後の課題">今後の課題</h2> <p>今後もドキュメントを改善していきたい一方、不要なドキュメントは消していきたいとも考えています。特にコーディングスタイルのようなものはLintを設定すれば不要になるものが多いため、ドキュメントはそれができるまでの一時的な対応として、Lintに寄せていくようにしたいです。</p> <h2 id="まとめ">まとめ</h2> <p>フロントエンドは周辺ツールの変化などもあり、推奨されるコーディング方法も変わることがありますが、コードベースが大きいと全てを最新の書き方にリファクタリングしながら進むのは難しいです。せめてドキュメントによって、新たに書く部分についてはどう書くのが良いのか、というのが明示された状態にしていければと思っています。</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け) | Built with Notion" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech ツーマンセル開発で開発者体験が向上した話 hatenablog://entry/6801883189064773745 2023-12-08T07:00:00+09:00 2023-12-08T07:00:02+09:00 こちらの記事は hacomono Advent Calendar 2023の8日目の記事です はじめに こんにちは。開発本部 フィーチャーグループのぺいです。 本記事では、私が所属しているフィーチャーグループで取り入れてみた「ツーマンセル開発」と呼んでいる開発スタイルについて、導入の背景や実際にやってみてどうだったかということについてご紹介したいと思います。 まずは私から導入の背景や課題についておはなしし、その後に実際にやってみてどうだったかというところについては同チームのエンジニアのれんれんにバトンタッチをしたいと思います。 導入の背景と課題 現在、hacomono の開発組織は拡大しており… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231207/20231207105428.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>こちらの記事は <a href="https://qiita.com/advent-calendar/2023/hacomono">hacomono Advent Calendar 2023</a>の8日目の記事です</p></blockquote> <h1 id="はじめに">はじめに</h1> <p>こんにちは。開発本部 フィーチャーグループの<span style="color: #2196f3">ぺい</span>です。</p> <p>本記事では、私が所属しているフィーチャーグループで取り入れてみた「ツーマンセル開発」と呼んでいる開発スタイルについて、導入の背景や実際にやってみてどうだったかということについてご紹介したいと思います。</p> <p>まずは私から導入の背景や課題についておはなしし、その後に実際にやってみてどうだったかというところについては同チームのエンジニアの<span style="color: #5ed361">れんれん</span>にバトンタッチをしたいと思います。</p> <h1 id="導入の背景と課題"><span style="color: #2196f3">導入の背景と課題</span></h1> <p>現在、hacomono の開発組織は拡大しており、以下のような組織構成になっています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hacomono-tech/20231207/20231207103140.png" width="1200" height="822" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>開発組織に属しているチームとしては、スクール・POS・フィーチャー・エンタープライズ・運用保守・Engineering Officeの6つがありますが、フィーチャーグループはこの中でも特に広範囲の開発を担当しているチームというのが特徴の1つです。担当する開発案件についてはもちろん、各所からの問い合わせや不具合の調査・修正等にも幅広く対応していく必要があります。</p> <p>このようなチームの特性もあり、フィーチャーグループではチーム全体でまとまった1案件に全員で取り組むというようなスタイルではなく、個々人がそれぞれの開発案件を持ちつつ、日々の開発・運用に取り組んでいます。</p> <h3 id="課題1-コードレビューの難しさ">課題1: コードレビューの難しさ</h3> <p>フィーチャーグループでのコードレビューは、メインレビュアー・サブレビュアーの2名をランダムにアサインする仕組みを取っていますが、先ほどのチームの特性もあり感じた課題がありました。</p> <p>まずレビュアーの視点に立つと、チームの各メンバーがどのような案件に取り組んでいるかをおおまかには知っているものの、詳細についてはあまり知らないという状況です。そのため、しっかりとコードレビューをするにはそれなりに時間がかかります。その一方で、各々締め切りのある案件も持っている人も多く、あちらを立てればこちらが立たずというような状況にもなりやすいと感じていました。</p> <p>レビュイーの視点に立つと、考慮漏れやバグを防ぐためにもちゃんとレビューをしてほしいと感じる場面が多いと思いますが、前述のとおりレビュアーとしてはなかなかそれが難しく両者がコンフリクトしているような状況になっていました。</p> <p>なおここについては、私が入社2-3ヶ月時点だったという背景もあり、コードや仕様の理解も浅かったことは関係していたと思います。しかしながら、組織が拡大しつつあることを考えると、今後も引き続き発生する課題とも感じていました。</p> <h3 id="課題2-一人で抱えやすい問題">課題2: 一人で抱えやすい問題</h3> <p>hacomono はフルリモートワークを実践しており、チームのメンバーが住んでいる地域も様々です。非常に柔軟な働き方ができ、自分自身もフル出社に戻ることはもう考えられなくなってきていますが、各所でも言われているとおり意識しないとコミュニケーション不足に陥りやすい問題もあると思います。</p> <p>フィーチャーグループでも、毎朝の朝会はもちろんオンラインで集まってわいわい開発する時間を定期的に取ったりと工夫をしています。</p> <p>ただ、各々の持っている案件の細かいコンテキストについては共有しきれていないこともありピンポイントの具体な質問が少ししづらかったり、また緊急度の高い差し込みの対応に入ることで開発案件のスケジュールが厳しくなってきた場合も個人ががんばってなんとかするような場面もちらほら見えていました。</p> <p>このような背景・課題の解決策の1案として、ツーマンセル開発をチーム内に提案し、実際にトライしてみることになりました。</p> <h1 id="ツーマンセル開発について"><span style="color: #2196f3">ツーマンセル開発について</span></h1> <p>ツーマンセル開発とは、ほぼ言葉の意味そのままにはなりますが、我々の中では次のような開発スタイルと定義しました。</p> <ul> <li>1つの開発案件に対して、2人がコミットし、品質やデリバリーまでの責任を持つ <ul> <li>上記を前提として、タスクの分担は自由。ペアで進めるもよし、設計と開発で分担するもよし</li> </ul> </li> <li><strong>ペアプロすることではない</strong></li> <li>コードレビューはペアの人を指定し、サブレビュアーとしてフィーチャーグループから1名を指定する</li> </ul> <p>ポイントとしては、常にペアで進めるというのではなく、2人が品質・デリバリーに責任を持つものとし、細かな進め方については特別なルールはほとんど設けなかったところです。ペアプロもあくまで一手段であり、使うかどうかは2人の判断次第です。</p> <p>コードレビューは変更の背景や修正の目的といったコンテキストの理解が重要になります。案件にコミットしている人であればその理解は自ずと深まっていき、短い時間で効果的なレビューができるようになると考え、ペアのメンバーをレビュアーにするという方針だけ決めていました。 ただ、逆に2人で視野が狭まってしまう可能性も考慮し、第三者的な視点で見てくれるサブレビュアーは通常通りチーム内からアサインすることにしました。</p> <h1 id="実際にやってみた"><span style="color: #5ed361">実際にやってみた</span></h1> <p>2023年の9月 ~ 11月にかけて、ツーマンセル開発で取り組むのにちょうどよい規模感の開発案件があったため、ぺい・れんれんペアで取り組みました。</p> <h3 id="課題1に対して">課題1に対して</h3> <p>お互いにどのような案件か理解し、コード理解も深まっているため素早く正確なレビューが行えました。軽微な修正などの小規模PRのマージのスピードが速くなり、修正点があれば即座に対応できました。これにより、開発の停滞を最小限に抑え、迅速かつ効率的なプロセスが確立されました。</p> <p>機能の実装が完了した後、動作確認とテストコード実装を手分けして担当できました。これにより、開発速度を維持しつつ品質の担保ができ、バグの早期発見と修正がスムーズに行えました。協力体制による分業が、開発サイクルを効果的に短縮しました。</p> <h3 id="課題2に対して">課題2に対して</h3> <p>複雑な箇所のコーディングについては、ペアプロを行うことでコミュニケーションを密にとり、お互いのコード理解度を上げることができました。また普段は1人で黙々と進めているので気分転換にもなりました。</p> <p>どちらかが多忙な時にも、もう一方が柔軟にタスクを引き受けることができました。これにより、プロジェクトの進捗が滞ることなく、常に効率的に作業を進めることが可能でした。予定変更にも対応できる柔軟性が、プロジェクトの安定性を高めました。</p> <h3 id="思わぬ収穫">思わぬ収穫</h3> <p>案件に関わったPdMやQAからのお褒めの言葉や反応を多くいただけました。</p> <p>質問をした時の回答をどちらかがすぐに対応できるため、待ち時間が少ない点や、QAで見つかった不具合の修正も分担して行うことで、すぐに修正が上がってきた点など。</p> <p>またそれぞれが複数案件を抱えている中、案件に対して全員がしっかりと向き合っており、良い議論ができました。</p> <h3 id="感想">感想</h3> <p>フィーチャーチームでは誰かと1つの案件に一緒にコミットすることは少なかったため新鮮でした。かなり複雑なロジックに手を入れる必要があったため、1人ではなく2人で進められたことは、安心感があり落ち着いて案件を進めることができました。また複雑な箇所を理解しているメンバーが増えチームとしても良かったように思います。</p> <p>結果的により良いものを提供するために一部リリースを見送った箇所がありましたが、そこの判断もツーマンセルだったためより良い判断ができた印象でした。</p> <h1 id="まとめ"><span style="color: #2196f3">まとめ</span></h1> <p>以上、フィーチャーグループで取り入れたちょっとした開発の工夫のお話でした。今回のツーマンセル開発の締めくくりとして、本ブログ記事もツーマンセルで取り込み、締切りまでに責任を持って書きました(笑)。</p> <p>実際にやってみて、当初感じていた2つの課題にダイレクトに効果があったと感じることができています。さらに、想定はしていなかったものの、ペアの2人だけではなくその周囲のメンバーにも良いフィードバックをいただくことができ、ここだけ見てもやる意義があったなと感じています。</p> <p>今後も開発案件の規模や複雑度などを考慮して、適材適所でツーマンセル開発を取り入れ、知見を溜めて改善していけたらと考えています。同じような課題を持っている方に、少しでも参考になれば幸いです。</p> <hr /> <p>株式会社hacomonoでは一緒に働く仲間を募集しています! <br>採用情報や採用ウィッシュリストも是非ご覧ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2F65f1c0a6e2574d22b2842b40a8e15358" title="採用ウィッシュリスト(エンジニア向け)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fhacomono%2Fhomes%2F4140" title="株式会社hacomono 採用情報 / 株式会社hacomono" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> hacomono-tech