Inside of LOVOT

GROOVE X 技術ブログ

GROOVE Xでは、なぜスクラムマスターが「領域人事」を兼任することになったのか

こんにちは。GROOVE Xのスクラムマスターの niwano です。
今回は何回かに分けて、スクラムマスターと「領域人事」の兼任について書きたいと思います!

スクラムマスターが人事?という違和感から始まった

「スクラムマスターが、人事をやるんですか?」

この話をすると、だいたい最初にこう聞かれます。
そしてその次に来るのが、

  • それって中立性壊れません?
  • チームから信頼されなくなりません?
  • 便利屋になりません?

という、まっとうで正しい懸念です。

結論から言うと、
この懸念は全部“正しい”です。
ただし、それは「人事」という言葉から想像される役割を前提にした場合の話です。

この記事では、

  • なぜスクラムマスターが「領域人事」と呼ばれる役割を兼任することになったのか
  • それは一般的な人事やマネージャーと何が違うのか
  • なぜ今のフェーズでは必要だと判断したのか

という背景と思想を整理します。

※ 本記事は、フラットな開発組織・アジャイル前提の話です。 ※ 評価・報酬・異動などの「人事権」をスクラムマスターが持つ話ではありません。

Nano Bananaが生成したGROOVE Xのスクラムマスター


フラット組織で起きがちな「人の問題が宙に浮く」現象

私たちの開発組織の一部はフラット組織です。

  • フラット組織は2階層しかない。
  • マネージャー的役割は全員が分担する。
  • チームが自己設計・自己管理する前提

これはアジャイルやスクラムとの相性も良く、
開発スピードや当事者意識という点では大きなメリットがあります。

一方で、必ず起きる問題があります。

それが、

「人の問題」「組織の問題」を誰が扱うのか分からなくなる

という問題です。

例えば:

  • チーム内の摩擦や対立
  • 役割の曖昧さからくる不満
  • 暗黙の期待がズレたまま進む状況
  • 仕組みの欠陥が原因なのに、個人の問題に見えてしまうケース

マネージャーが明確に存在する組織であれば、
これらは自然と「マネージャーの仕事」になります。

しかしフラット組織では、

  • チームに任せたい
  • でもチームだけでは扱いきれない
  • かといって誰かが強い権限で介入するのも違う

という 宙ぶらりんな状態 が生まれやすくなります。


「じゃあ誰がやるの?」問題への暫定解として

この状態が続くと、だいたい次のどれかが起きます。

  • 声の大きい人が非公式に仕切り始める
  • 問題が放置され、突然爆発する
  • 人事や経営に“いきなり重たい形”で上がる

どれも、フラット組織が目指している姿とはズレています。

そこで出てきた問いがこれです。

過渡期の今、誰が「人と組織の問題」を受け止めるのか?

この問いに対する暫定的な答えが、 スクラムマスターが「領域人事(組織開発ファシリテーター)」を兼任する という設計でした。


なぜスクラムマスターだったのか

これは消去法でもあり、必然でもあります。

スクラムマスターは本来、

  • チームの自己管理を促進する
  • プロセスの問題を構造として扱う
  • 個人を責めず、仕組みを改善する
  • 中立な立場でファシリテーションする

という役割を持っています。

つまり、

  • 人を評価しない
  • 指示命令しない
  • 決定権を持たない
  • でも、対話と構造には深く関わる

この性質は、

  • 人事権を持つマネージャー
  • 制度設計を主とする人事部

とは明確に異なります。

逆に言えば、
人事権を持たないからこそ、扱える「人の問題」がある
ということでもあります。


「領域人事」とは何で、何ではないのか

ここで重要なのは、
この役割が 「人事」ではない という点です。

少なくとも、一般的に想像される人事ではありません。

やらないこと(明確にやらない)

  • 評価・査定
  • 報酬・昇給の決定
  • 人事異動の意思決定
  • 労務・コンプライアンス判断

やること

  • 従業員間の紛争解決(一次受け)
  • 組織・チームのプロセス改善支援
  • 育成や学習の仕組みづくり
  • カルチャーの言語化・醸成
  • 「この問題、どこで扱うべきか」を整理する

あくまで 組織開発のファシリテーター であり、 決める人ではなく、整える人 です。

Nano Banana が生成した紛争を解決しているスクラムマスター


これは「完成形」ではなく「過渡期の設計」

とても大事なことなので強調します。

このロールは、

  • 永続的にやる前提ではありません
  • スクラムマスターがずっと抱える仕事でもありません

目指しているのは、

チーム自身が、人と組織の問題を扱えるようになること

そのために一時的に、

  • 問題を受け止め
  • 構造として整理し
  • 仕組みや役割に分解し
  • 徐々に手放していく

という役割です。

スクラムマスターも、 この「領域人事(組織開発ファシリテーター)」も、 本質的には「いずれ不要になる仕事」 だと思っています。


この記事のまとめ

  • フラット組織では「人の問題」が宙に浮きやすい
  • その過渡期の受け皿として、スクラムマスターが選ばれた
  • それは人事権を持つ役割ではない
  • 決めるのではなく、整えて手放す仕事
  • この役割を永続させないことがゴールである

次回は、 「人事権のない人事」は本当に機能するのか? という点を、もう少し踏み込んで整理します。

このシリーズについて


groovex.my.canva.site

LOVOT開発の「ストーリーテラー」になりたい

この記事は、GROOVE X Advent Calendar 2025 の25日目の記事です。

こんにちは、そしてメリークリスマス! LOVOTソフトウェアのエリアプロダクトオーナー ishimegです。

本記事は、「ストーリーテラー」という最近注目の職種があるらしい、という紹介と、それを自分たちの仕事にも活かしたいな〜というゆるい内容となっております。

AIで生成したクリスマスっぽいLOVOTの画像

「ストーリーテラー」というお仕事が注目されているらしい

最近、こんなポストを目にしました。


ストーリーテラーってなんだろう?

私自身、カスタマーサクセスチームやPRチームと協力してLOVOTにまつわる情報発信の内容を考えることも多く、なんだか素敵な響きのするこの言葉が妙に気になったのでした。

ポストにリンクされた「企業が『ストーリーテラー』を必死に求めている理由」と題された記事を読んでみると、

  • 従来のようにただ情報を発信するだけではなく、自分たちの企業が製品の価値について「自分たちの物語」として発信する人材 = 「ストーリーテラー」である
  • 顧客も従業員もただの数字や事実よりも感情的なつながりを重視している
  • そして「ストーリーテラー」という単語を含む求人は米国では前年比2倍に急増している

ということが書かれていました。(だいぶ意訳も含みます)
これは米国で起こっていることではあるものの、日本においても大事な視点のように思います。

また、実際にどんな求人があるのかも少し調べてみました。

セキュリティサービスであれば、
複雑なセキュリティの概念を、顧客が自分事として捉えられる魅力的な物語へと変換すること

ドキュメント作成ツールであれば、
実際のユーザーがどのようにツールを使って人生や仕事を変えたかという「実体験」を掘り起こす力

など。共通するのは以下のような部分です。

  • AIやセキュリティなど複雑なトピックをわかりやすく伝えること
  • 実際に社内や顧客にあった出来事を取材して、「なんだかいい話」を発掘すること
  • ブランド力や売上向上というミッションのもとそれらを行う

重要なのはきちんとした事実に基づきつつも、物語として魅力があり、さらには製品の価値向上を担わなければならない、製品やブランディングに関する知識も、語り手としてのスキルも求められそうなお仕事です。

「お客様とLOVOTの物語」だけじゃない

ここで、日々おこなっている業務をふり返ってみました。

私はLOVOTのソフトウェアを横断的に理解する立場として、カスタマーサクセスチームのメンバーと週に数回は顔を合わせながら、日々様々な情報発信に制作者やレビュワーとして関わっています。

ウェブマニュアルやFAQの見直し、アップデートのお知らせ、LOVOTの技術を紹介するブログ、外部向けのインタビューなどなど・・・

これらの仕事はどれも「LOVOTとの暮らしをサポートする」「LOVOTのことをよく知ってもらう」という目的で行っているものですが、
ストーリーテラーという概念を通して「お客様とLOVOTの物語」と「私たちとLOVOTの物語」の2つに分けることもできる、と気がつきました。

AIが生成した「LOVOT開発のストーリーテラー」のイメージ
LOVOTがお家に届いてからの主役は、オーナーさんとLOVOT。私たちは「お客様とLOVOTの物語」を応援する立場です。それについては常々意識してきました。
一方で、LOVOTがお家に届く前の開発・製造の過程は「私たち(GXメンバー)とLOVOTの物語」とも考えられるのです。

「私たち(GXメンバー)の物語」とは?

突然ですが、私はGROOVE Xで働く皆さんが大好きです。
理由のひとつは、LOVOTに皆それぞれの想いを持っていること、その熱量の高さを、仕事のアウトプットや会話の端々から日々感じているからです。

そんな素敵な皆さんのLOVOTにかける情熱を物語として届けることができ、しかもそれが製品価値の向上につながるのなら、それはきっととてもやりがいのある仕事だなと思います。
と同時に、語る価値のある物語がまだまだGXに眠っているのかもしれない・・・!という可能性を感じました。

最近それを実感する出来事もありました。1月に予定されているLOVOTの工場見学*1の打ち合わせ時のことです。
数え切れないほどの製造工程をおさらいしながら、これは確かに「LOVOTが家族(お客様)と出会う前の物語」だ!と感じました。
※ それだけLOVOTの生産は大変なのです!生産チームによる部品の品質についての記事もぜひご覧ください

けれど、私がストーリーテラーになれるのか?それは私ができる、そしてすべき仕事なのか?
それについては冒頭に上げたWSJの記事にこんな示唆がありました。※ 以下Geminiによる日本語訳

専門家は、「ストーリーテリングは、雇えば手に入る『機能』ではなく、リーダーやチーム全員が培うべき『能力』である」と述べています。もし組織に明確な「目的(パーパス)」がなければ、どれほど優れたストーリーテラーを雇ったとしても、それはただの「騒音」を大きくするメガホンにしかなりません。

結局のところ、企業が本当に必要としているのは、単に「物語を語る人」ではなく、自社の存在意義を深く理解し、それを人々の心に響く形で表現できる戦略的な人材なのです。

ならば私自身もストーリーテリングの能力も身につけるべきである、ということ。
これまでたくさんのLOVOTにまつわる文章を書いてきましたが、あくまでも「LOVOTとの暮らし」をサポートするものであり、製品価値・ブランド価値への影響までは意識できていませんでした。
2026年は、主体的にLOVOTとGROOVE Xの魅力を伝えられる語り手になりたい、そんなことを考えた2025年の年の瀬でした。

最後に

最後まで読んで頂きありがとうございます!GROOVE Xでは、様々な領域で一緒に働く仲間を募集しています。少しでも興味を持ってくださった方がいましたら、下記のリンクをご参照ください。

recruit.jobcan.jp

それでは、よいお年を!

AIが生成した「よいお年を!」なLOVOTの画像

*1:LOVOTの工場見学のお申し込みは終了しています

バイナリ改変なしに Crash Reporting 機能を追加するC/C++ライブラリを昔書いたお話

この記事は、GROOVE X Advent Calendar 2025の24日目の記事です

はじめに

こんにちは!多忙を理由に記事執筆を後ろ倒ししていたら、クリスマスイブ担当になってしまった sutetako です!🫠 なんということでしょう

LOVOT Frameworkチームという、LOVOTのOSや開発基盤自体を開発するチームに所属しています。なお、専門は音声認識です*1

さて、今回は、自社開発に用いられている、C/C++ の Crash Reporting 向けライブラリの仕組みの一端をご紹介したいと思います。

コミットログを見てみたら、作ったのが2019年と、ずいぶん昔話で、多少レガシーなお話にはなってしまいますが、、、 まぁちょっとだけ面白い仕組みなので、見てみましょう!

そもそも Crash Reportingって?

ソフトウェアはよく死にます 😇

要因は、ロジックミスはもとより、ロボットの場合はより広範です。

  • 各種組み込みデバイス・FW自体の不具合
    • 数がめっちゃある
  • 組み込みOSおよびドライバの不具合
  • IOエラー
    • メモリ、ディスク、通信経路
    • 宇宙線によるビット反転

などなど…非常に多岐にわたります。

ロボットの内部で動くソフトウェアは、「常に安定的に稼働し続けること」が求められます。

多少の例外で死んでしまうようでは、「おお 勇者よ!…」と、どこからか嘆きの声が聞こえてきそうですね。

あらゆる問題・例外に対応できるようなソフトウェアを完璧に書ければいいのですが…人間様が考えられるエラーケースなど簡単に飛び越えてくるのがロボット開発というものだったりします 🙃 まず無理っす。

そこで登場するのが、Crash Reporting という仕組みです。

クラッシュ(異常終了)時に、エラーが発生したソフトウェア情報、ログ、コールスタックなどをレポートとしてまとめることで、

  • ファイルシステムに保存しておいて、あとから効率的に解析する
  • エラー追跡用のプラットフォーム(Sentry など)と連携・レポートを送信し、集約・解析する

などなどすることが可能になります。

C/C++ だとどうやるのがいい?

「まぁ、まず core 吐かせて、後で gdb でごにょごにょ」

手元でデバッグバイナリ使ってTRY&ERRORするなら、全然これでいいと思います。

しかしね…問題が起きるケースって本番想定で組み込んで稼働・検証してる状態だったりするんですよね…したがって:

  • core のサイズが莫大になるので、ファイルシステムの容量が簡単に枯渇する*2
  • core を吐く処理で、リソースが枯渇する*3
  • core を吐いている間は対象のサービスが停止し続けてしまう*4

と、全体のシステム稼働をも阻害する致命的なことが起きます。連続して発生したら、検証どころの話ではありませんね。

そこで登場するのが、MinidumpBreakpad*5 です。

たぶん、Gemini さんに聞いたほうがしっかり解説してくれそう…というところで聞いてみた内容を抜粋紹介すると:

1. Minidump とは何か?
元々は Windows オペレーティングシステムの標準的なクラッシュダンプ形式(.dmp)です。

通常の「フルダンプ」がメモリ全体(数GBになることもある)を保存するのに対し、Minidump はデバッグに必要な最小限の情報のみを保存するため、サイズが数KB〜数MBと非常に小さいのが特徴です。
...
2. Google Breakpad とは?
Google Breakpad は、この Windows 発祥の「Minidump」というフォーマットを、macOS、Linux、Android、iOS など、あらゆるプラットフォームで統一的に扱えるようにした OSS ライブラリです。

Chrome ブラウザや Firefox などで長年採用されてきました。Breakpad が登場する前は、OS ごとに異なるクラッシュレポート形式(LinuxならCore dumpなど)を扱う必要がありましたが、Breakpad のおかげで開発者は「すべてのOSのクラッシュを Minidump 形式で収集・解析する」ことが可能になりました。
...

雑にまとめると、 「ちっちゃくて取り回しのしやすい core っぽいやつ」 です!

これなら、一つのサービスのクラッシュが、全体に与える影響を非常に小さくできます。

実装サンプル

Sentry の解説記事 にわかりやすいものがあるので、日本語コメントつけつつ引用します。

#include "client/linux/handler/exception_handler.h"
using namespace google_breakpad;
namespace {
bool callback(const MinidumpDescriptor &descriptor,
              void *context,
              bool succeeded) {
    // succeeded が真の場合, minidump ファイルへのパスが
    // descriptor.path() に格納されます。 
    // context には、exception handlrer のコンストラクタに
    // 渡したものが格納されます。
    return succeeded;
}
}
int main(int argc, char *argv[]) {
    MinidumpDescriptor descriptor("path/to/cache");
    ExceptionHandler eh(
      descriptor,
      /* filter */ nullptr,
      callback,
      /* context */ nullptr,
      /* install handler */ true,
      /* server FD */ -1
    );
    // ここに実装を書く
    return 0;
}

簡単ですね!

なお、動作させるには Breakpad をビルド・リンクさせる必要がありますので、あしからず。

どういうライブラリを作ったの?

前置きが長くなりましたが、ようやく本題です。

上に書いたとおり、MinidumpBreakpad を使えば良さそうです。

でも、思ったんですよね。

「対象バイナリのソースコードにいちいち処理差し込むの面倒すぎね?」

そう、我々の製品内は多量のOSSと自社開発サービスがわんさか立ってるわけで、、、これは布教・普及するの面倒だぞと。

というわけで、対象バイナリを改変することなしに、Minidumpを生成できるようにする 形でライブラリを構築することにしました。

LD_PRELOAD

悪名高いこれを使います。

一応軽く解説しておくと、LD_PRELOAD はLinuxの環境変数で、動的リンク実行時、本来リンク対象ではなかったライブラリにおいても、すべてのリンク対象のライブラリより「先に」読み込ませてシンボル解決させることができる、というやべーやつです。

どうして悪名高いかというと、シンボルを置き換えることができるので、クレデンシャルを受け取るような関数を乗っ取って標準出力することもできたりするわけですね*6

一方、ヒープアロケーションの仕組みを拡張して解析やエラー検出に使ったり(Valgrindさんも一部利用)、より効率的なアルゴリズムに置き換えてキャッシュヒット率を上げて処理性能を上げたり(TCMallocjemalloc など)等、正の側面も大きい仕組みです。

このように使うイメージですね。

$ LD_PRELOAD=libminidump.so foo_binary

けれど、このままだと「何のシンボルを置き換えるの?」というお話になりそうです。

また、プログラム実行前にBreakpadの設定ができないと、捕捉できる異常終了が限られてきてしまいます。

これを解決するのが関数属性*7です。

__attribute__((constructor))

詳しくはこちら

main 関数の実行「前」に処理を差し込むことができます。

実装例を参考にすると、下記のようにライブラリが書けそうです*8

#include "client/linux/handler/exception_handler.h"

using namespace google_breakpad;

static std::unique_ptr<MinidumpDescriptor> desc(nullptr);
static std::unique_ptr<ExceptionHandler> eh(nullptr);

static bool callback(const MinidumpDescriptor &descriptor, void *context,
                     bool succeeded) {
  // init 関数において context に情報を引き渡しておけば
  // ここで参照することも可能ですが、割愛
  const char *path = descriptor.path();
  if (succeeded) {
    // minidump を rotate したり情報付加して保存しおしたり、などなど
  }
  return succeeded;
}

__attribute__((constructor)) void init(void) {

  desc.reset(new MinidumpDescriptor("/tmp"));
  eh.reset(new ExceptionHandler(*(desc.get()), nullptr, callback,
                                  nullptr, true, -1));
}

簡単ですね!!

これらにより、

  • LD_PRELOAD で動的リンクを強制・先読み
  • main 関数より前に Breakpad の初期化処理を差し込む

ことができ、バイナリを変更することなく Crash Reporting 機能の追加ができました 🎉

Coffee Break ☕

せっかくなので、「引っかかった罠」についても、いくつか紹介しておきます。

Systemd Service の Environment

LD_PRELOAD は環境変数なんだし、Environment に書いておけば、ExecStart に自動適用してくれるよね!

はい、大正解です。

けれど、たとえば下記のような雑な記述の .service ファイルに適用すると…

[Service]
User=bar
Group=bar
Environment="LD_PRELOAD=libminidump.so"
ExecStartPre=+/bin/mkdir -p /var/log/foo
ExecStartPre=+/bin/chown bar:bar /var/log/foo
ExecStart=/bin/foo_service
ExecStartPost=/bin/rm -rf /var/log/foo

ExecStartPre/Post の関係ないバイナリ群、mkdir, chown, rm にも見事適用してくれます 😇

対象を絞りたい場合は、やや冗長な書き方にはなってしまいますが、下記のように書けます。

[Service]
User=bar
Group=bar
-Environment="LD_PRELOAD=libminidump.so"
ExecStartPre=+/bin/mkdir -p /var/log/foo
ExecStartPre=+/bin/chown bar:bar /var/log/foo
-ExecStart=/bin/foo_service
+ExecStart=/bin/sh -c "LD_PRELOAD=libminidump.so foo_service"
ExecStartPost=/bin/rm -rf /var/log/foo

C++の標準出力

ライブラリ内の init の処理で、下記のようなエラー出力を書いてたんですが、ここに差し掛かると abort する事象が起きました。

    std::cerr << "failed to initialize breakpad" << std::endl;

しかも、適用するバイナリによって起きたり起きなかったり。

gdb してごにょごにょ調べてみると、最終的に下記に行き当たります。

(gdb) p std::cout
$1 = {<std::basic_ios<char, std::char_traits<char> >> = <invalid address>, _vptr.basic_ostream = 0x0}

標準出力が初期化されていないという...

実は __attribute__((constructor)) のドキュメント読むとちゃんと書いてました(調べてた当時は気づいてなかったけども)

The order in which constructors for C++ objects with static storage duration are invoked relative to functions decorated with attribute constructor is normally unspecified.

しっかり順番を強制する方法もある模様ですが、こだわるところでもなかったので、下記のようにワークアラウンド。 こだわりたい方は、詳しく追ってもよさそうですね!

-    std::cerr << "failed to initialize breakpad" << std::endl;
+    fprintf(stderr, "failed to initialize breakpad\n");

Sentry SDK 使えば自作する必要なかったんじゃね?

当時の日報の抜粋貼っておきますね。

2019/11/05 の日報

まだまだ当時は黎明期だった模様で…

  • ドキュメント上は「対応してる」とあるものの、実装の中身が空 *9
  • (やたらと怒ってるのは)その他にもドキュメントと実装の乖離、ビルドの不安定さ、などなどでヘイトが溜まってた

今はそんなことないんだと思います!!

Symbolication

おまけにはなりますが、Symbolication の例についても解説しておきます。

Minidump ファイルそのものにはソースコード含むデバッグ情報は存在しません。

Symbolication は、Minidumpファイルの情報と既知のデバッグ情報群を突き合わせて、ソースコードのどこで異常終了が起きたかを明らかにする処理です。

コールスタックに含まれるアドレス・ライブラリ名や周辺情報から異常終了時の状態を推定して改善することも不可能ではない(実際、初期はそうやってました)んですが、わかりやすいに越したことはないですね!

Sentry を利用した解析

Sentry は Minidumpファイルを受理できるほか、Symbolication にも対応しています。

なお、詳細な手順(Sentryプロジェクトの準備等)を含むと長くなってしまうため、それらは公式のドキュメントに譲ります。

ここでは、Symbolicationまわりを中心としたサンプルベースの解説とさせていただきます。

サンプルコード

下記のようなクラッシュコードでテストしてみます。

void crash() {
  volatile int *i = reinterpret_cast<int *>(0x45);
  *i = 5; // crash!
}

void start() {
  crash();
}

int main(void) {
  start();
}

DIFs のアップロード

DIFs (Debug Information Files) を Sentryに事前アップロードしておくことで、Sentryが受理したMinidump のバイナリと同じビルドIDのDIFs を突き合わせて解析してくれるようになります。

-g 付きでビルドして、そのバイナリを元に DIFs を生成します。

下記のように、sentry-cli を使ったDIFsの生成とアップロード処理がとても楽なのでおすすめです。CIにも組み込みやすいです。

g++ -g onepass.cpp -o ${YOUR_BINARY}

# デバッグ情報付きのバイナリそのものを、DIFs生成先にコピー
mkdir -p /tmp/debug
cp ${YOUR_BINARY} /tmp/debug/

# ソースコードをバンドル
sentry-cli difutil bundle-sources -o /tmp/debug ${YOUR_BINARY}

# アップロード
sentry-cli --url ${SENTRY_URL} --auth-token ${SENTRY_AUTH_TOKEN} upload-dif -o ${SENTRY_ORG} -p ${SENTRY_PROJECT} --wait /tmp/debug

デバッグ情報のStrip(Optional)

デバッグ情報はバイナリをとても太らせてしまうので、下記のように取り除いてあげるといいです。

$ objcopy --strip-debug --strip-unneeded ${YOUR_BINARY}

なお、DIFs がちゃんとアップロードされていれば、実行環境で動くバイナリにデバッグ情報がなくても、Symbolication は動作します。

出力例

クラッシュコードを動作させて、 生成されたMinidumpをSentryにアップロードすると、下記のような画面を表示することができます。

Sentry による Symbolication サンプル

やったね 🎉

おわりに

むっかーしに書いた Crash Reporting 向けのライブラリのお話でした。

今でもメンテしつつなんだかんだ使われていたりしますね。 (どこかに仕組み書こうと思ってようやく書けたので、すっきり)

最後まで読んでいただきありがとうございました!

LOVOT Frameworkチームでは、現在エンジニアを募集しています!

本記事を読んで、開発基盤やOS開発にご興味が出てきた方がいらっしゃいましたら、ぜひぜひカジュアルにお声掛けください!! (音声認識エンジニアも待ってます!!)

recruit.jobcan.jp

*1:チーム内で音声認識を開発しているわけではなくて、一人チームで音声認識を開発しつつ、OSまわりも作っている、という謎スタイルで働いています

*2:組み込み製品なので、追加データを格納できるファイルシステムの容量は潤沢ではないのです…

*3:応答性を担保するためにギリギリまで最適化している関係上、余裕なリソースなどないのです…

*4:仮にモーターを制御するところだったりすると…どうなるかわかりますよね

*5:ちなみに現在巷では、Out-of-process に安定的に動作する Crashpad が使われることがほとんどのようです。まぁ昔のお話なのでご勘弁を…

*6:良い子は真似しないでね!!

*7:ここでは、gcc/g++ を使う前提で説明しています。clangなど他のコンパイラではやり方が変わるのでご注意

*8:実際に弊社で使用している実装はもう少し複雑で、対象バイナリのELFヘッダパースや周辺情報付加などなど色々やってたりします。ここでは、コア部分の説明のため簡易的なものに留めています

*9:厳密に言えば、「ToDoコメント」はあったと記憶しています

Python非同期でCPUバウンドな処理を試してみよう:FreeThreadingとInterpreterPoolExecutor編

この記事は、GROOVE Xアドベントカレンダー2025 の23日目の記事です。

こんにちは、技術組織デザインチームのふくだです。 Pythonで非同期を利用すると、CPUバウンドな処理どうしようという問題によくぶつかります(LOVOTの意思決定エンジンではPythonの非同期を利用しています。 ブログの過去記事 をご参考ください)

この記事では、最近のPythonで追加されたFreeThreading(NoGIL)な PythonやInterpreterPoolExecutorをPython非同期と合わせて使ってみて、CPUバウンドな処理にどれくらい効果があるか確認してみました。

21日目の記事のアイキャッチが素敵だったのでforkしてしまいました

はじめに I/O バウンド と CPU バウンドって

I/O バウンドとCPUバウンドについて、簡単に説明します。処理時間がかかるものたちです。

処理 説明 得意なモジュール
I/O バウンドな処理 ネットワーク通信やDBアクセスなど時間がかかる処理 マルチスレッド, 非同期(asyncioやtrio)
CPU バウンドな処理 計算やデータ処理など、CPUの処理能力によって時間がかかる処理 マルチプロセス

特にCPUバウンドな処理は、非同期プログラミングやマルチスレッドでは効率的に扱うことが難しい場合があります。 PythonにはGIL(Global Interpreter Lock)が存在するため、スレッドでのCPUバウンドな処理の並列化が制限されることがあります。

それらの制限を解決するため、 Free ThreadingSubInterpreter が実装され、Pythonの並列処理能力を向上させる試みが進められています。

Free ThreadingやSubInterpreter

それぞれ一言で言うと以下のような機能です。

  • Free Threading: PythonのGILを無効化し、スレッドでのCPUバウンドな処理の並列化を可能にするカスタムビルドのPython
  • SubInterpreter:PythonのGILの制約を回避し、CPUバウンドな処理を効率的に並列化するための新しいアプローチ

詳細については、以下を参照してください。

Free Threading

SubInterpreter

Python 3.12で追加された SubInterpreter は、各スレッドが独立したインタープリターを持つことで、GILの制約を回避し、スレッドでのCPUバウンドな処理の並列化を可能にする機能です。ですが、利用方法が限られていました。

それを解決したのが、Python 3.14で追加された concurrent.futures.interpreter モジュールと高レベルAPIである InterpreterPoolExecutor です。

下記のような流れ

  • Python 3.12: SubInterpreter 追加
  • Python 3.13: Free Threading 実験的なオプション追加
  • Python 3.14: InterpreterPoolExecutor 追加, Free Threading オプション公式サポート

どちらも並列化を可能にする機能強化で、 SubInterpreter は2017年ごろから PEP 554 にて検討されていました。アプローチの異なる2種類の手法で課題への対応が進められたようです。

InterpreterPoolExecutorとは

InterpreterPoolExecutor は Python 3.14 で追加された標準ライブラリの Executor で、 ThreadPoolExecutor の サブクラス です。 各ワーカースレッドは 独立した SubInterpreter を持ち、その中でタスクを実行します。

その結果、以下のような特徴があります。

  • プロセスを増やさない
  • GIL の制約を回避できる
  • 真のマルチスレッド並列実行が可能

ThreadPoolExecutorProcessPoolExecutor との違い

特徴から ThreadPoolExecutorProcessPoolExecutor と比較すると以下のようになります。

  • ThreadPoolExecutor より並列が可能
  • ProcessPoolExecutor より軽い

それぞれの実行イメージは以下のような感じです。

OS
└─ python script.py   ← 親プロセス
    └─ Main Interpreter(GIL A)
        ├─ ThreadPoolExecutor
        │    └─ 同一プロセス内スレッド
        │
        ├─ InterpreterPoolExecutor
        │    └─  同一プロセス内スレッド(Pool内で流用される)
        │         ├─ Sub-interpreter #1(GIL B)
        │         ├─ Sub-interpreter #2(GIL C)
        │         └─ ...
        └─ ProcessPoolExecutor
             └─ OSが新しいプロセスを作る
                  ├─ python child #1 → Interpreter(GIL)
                  ├─ python child #2 → Interpreter(GIL)
                  └─ ...

モチベーション

asyncio の公式ドキュメントに、以下のような注釈が追記されていました。

注釈 Due to the GIL, asyncio.to_thread() can typically only be used to make IO-bound functions non-blocking. However, for extension modules that release the GIL or alternative Python implementations that don't have one, asyncio.to_thread() can also be used for CPU-bound functions.

意訳すると以下のようになります。

GIL(グローバルインタープリタロック)の制限により、asyncio.to_thread() は通常、I/O バウンドな関数をノンブロッキングにするためにしか使えません。しかし、GIL を解放する拡張モジュールや、GIL が存在しない代替の Python 実装では、asyncio.to_thread() は CPU バウンドな関数にも使用できます。

「なるほど!?!?」となり、実際に試してみよう、というのが本記事のモチベーションです。

asyncio.to_thread() は内部的に thread 1 を利用しています。
モチベーションは asyncio.to_thread() の公式ドキュメント由来ですが、検証用のコードは asyncio.to_thread() ではなく ThreadPoolExecutor を利用します。 InterpreterPoolExecutor ProcessPoolExecutor ThreadPoolExecutor はほぼ同じコードで切り替えが可能なためです。

検証してみた

動作環境

今回の検証環境は次のとおりです。ローカル環境での素朴なベンチマークなので、数値そのものより傾向を見ることを目的にしています。

  • M2 Mac 8コア 24GB RAM
  • Python 3.14.2
  • Free Threading 版 CPython 3.14.2

結果

CPU バウンドな処理を10回実行したときの平均時間は次のとおりです。ローカルでの素朴なベンチマークですが、 InterpreterPoolExecutorFree Threading もすごいですね!

方法 平均時間 (秒)
ProcessPoolExecutor 1.326s
InterpreterPoolExecutor 1.049s
ThreadPoolExecutor (標準 Python) 4.565s
ThreadPoolExecutor (Free Threading Python) 1.227s

実際の検証コード

今回検証で利用したコードは以下のとおりです。

import asyncio
import time
from concurrent.futures import (
    ProcessPoolExecutor,
    InterpreterPoolExecutor,
    ThreadPoolExecutor,
)


def cpu_bound(n: int) -> int:
    """n 回ループして単純な計算を行う CPU バウンドな関数"""
    acc = 0
    for i in range(n):
        acc += i * i
        acc %= 1_000_000_007
    return acc


async def run(executor):
    """指定された executor で CPU バウンドな処理を並列実行して時間を計測する"""
    loop = asyncio.get_running_loop()

    t0 = time.perf_counter()
    with executor() as ex:
        tasks = [loop.run_in_executor(ex, cpu_bound, 10_000_000) for _ in range(10)]
        await asyncio.gather(*tasks)
    dt = time.perf_counter() - t0

    print(f"{executor.__name__}: {dt:.3f}s")


async def main(mode: str):
    match mode:
        case "process":
            await run(ProcessPoolExecutor)
        case "interpreter":
            await run(InterpreterPoolExecutor)
        case "thread":
            await run(ThreadPoolExecutor)
        case _:
            raise ValueError("mode must be one of: process | interpreter | thread")


if __name__ == "__main__":
    import sys

    asyncio.run(main(sys.argv[1]))

以下のように実行しました。

$ uv run --python 3.14 sample.py process
$ uv run --python 3.14 sample.py interpreter
$ uv run --python 3.14 sample.py thread
$ uv run --python 3.14t sample.py thread. # 3.14t がFreeThreading版です

おまけ:並列化しないループの場合

元々、Python 3.13で実験的にFreeThreadingが実装された際に、シングルスレッドでの性能低下が課題として言及されていました。 前述の結果のうち、FreeThreadingは遜色ないくらいの性能で驚きました。

試しに、並列化せずにCPUバウンドな関数をPython 3.14とPython 3.14 FreeThreading版で実行してみました。

def cpu_bound(n: int) -> int:
    """n 回ループして単純な計算を行う CPU バウンドな関数"""
    acc = 0
    for i in range(n):
        acc += i * i
        acc %= 1_000_000_007
    return acc

def main():
    t0 = time.perf_counter()
    for _ in range(10):
        cpu_bound(10_000_000)
    dt = time.perf_counter() - t0
    print(f"sync (standard): {dt:.3f}s")

結果は以下のとおりです。Python 3.13 の課題は、Python 3.14 で改善されているようでした。 2

方法 平均時間 (秒)
標準 Python 4.717s
FreeThreading 5.344s

考察

結果をまとめると、次のような印象です! Free ThreadingInterpreterPoolExecutor もプロダクションで利用するには、気にしなければならない点がいくつかありますが、CPUバウンドな処理に対して有効な選択肢が増えたことを実感できました。 処理の性質と実行環境を理解して、適切な方法を選ぶのが大事そうですね。

  • InterpreterPoolExecutor 良さそう
    • ProcessPoolExecutor よりも良い結果
    • プロセス生成コストを避けたい場面で有効
  • ThreadPoolExecutor(標準 Python) はGILの影響がやはり厳しい
  • ThreadPoolExecutor(Free Threading Python) も 良さそう

Python の課題に対して、さまざまな角度からのアプローチが進んでいます。コア開発者やコミュニティのみなさまにとても感謝です。今後も Python の進化に注目していきたいと思います。 CPU使用率もだいぶ気になるところですが、それはまた別の機会にまとめたいと思います。

GROOVE Xでは、一緒に働く仲間を募集しています。少しでも興味を持ってくださった方がいましたら、下記のリンクをご参照ください。

recruit.jobcan.jp


  1. asyncio.to_thread() はイベントループのdefaultのexecutorであるThreadPoolExecutorを利用しています。 cpython/Lib/asyncio/threads.py at main · python/cpython
  2. Python 3.13 の FreeThreadingについてまとめた記事があります。ご興味ある方はご参考ください PythonのGILと3.13の実験的な新機能「free threading」を知る | gihyo.jp

可視化ツールFoxgloveとFoxglove Extensionの紹介

この記事は、GROOVE Xアドベントカレンダー2025 の22日目の記事です。

こんにちは、ふるまいチームのきゅんどうです。 今回はLOVOTのふるまい開発のデバッグのために使っているFoxgloveというアプリとその拡張機能についてご紹介します。

Foxgloveとは

Foxglove は、「時系列のメッセージデータ」を、再生・可視化・デバッグするためのツールです。ログを開いて後から解析するのはもちろん、データソースに接続してリアルタイムに観察することもできます。 ログのデータ形式としては、ROS 1の.bagファイルや、ROS 2で採用された.db3、ROS 2 Iron以降で使われる.mcapファイルなどに対応しています。 リアルタイムデータについてはROS 1のメッセージや、Websocketなどが対応しています (ROS 2向けにはwebsocketへのブリッジが提供されており、それを使うことになります)。

これらのデータ形式の中でも、わたしたちのログシステムではmcapファイルにprotobuf schemaを使って記録するところから始めました。LOVOT内部のメッセージングのためにprotobufを管理する仕組み が社内に整備されているためです。 LOVOT内部ではROSは一部にしか使われてないことや、ROSを使うことに慣れていないメンバがいることもあるので、rosbag play を実行するハードルが高いです。Foxgloveならアプリでファイルを読み込むだけで可視化できるので、使いやすいです。 またrqtのツール群と比べると、Foxgloveは必要な画面が一つのアプリ内にデフォルトで集まっているような直感的に操作しやすい構成になっており、使いやすいとも思っています。 そういった理由でmcapとFoxgloveの組み合わせを選びました。 より詳しい比較は公式ブログにもまとまっています(Foxglove vs RViz)。

Foxgloveでの可視化の様子

ただ、Foxgloveでデフォルトで表示できるデータ形式は決まっており、カスタムデータを表示するにはデータ形式の変換が必要なケースがあります。 またFoxgloveのデフォルトの表示方法以外の表示をしたいという、RVizにおけるPluginのような機能も必要です。 そういったケースのためにFoxgloveではExtensionを設定することができます。

Foxglove Extension

Foxglove Extension は、Foxgloveに「変換」「表示」「読み込み」などの機能を追加する仕組みです。 拡張の種類として message converters / custom panels / data loaders / user-script utilities があります(Foxglove Extensions)。

ここでは、それぞれのExtensionをLOVOT開発でどう使っているかも触れながら、簡単に紹介します。

Message Converters

カスタム定義したメッセージを、Foxgloveの既存パネルが解釈しやすい形に変換したいときに使います。 わたしたちの使う限りはカスタムメッセージを、3Dパネルに表示するためのSceneUpdateメッセージに変換するという使い方が多いです。 それ以外の例ですと、数値として記録されたデータを理解しやすい文字列に変換することで読みやすくするということもやったりしています。

Message Converterには、Schema Message Converter と Topic Message Converter の2種類があります。

  • Schema Message Converter: 入力データSchema -> 出力データSchema の変換
  • Topic Message Converter: 複数トピック -> 新トピック の変換

この2つのConverterですが良し悪しがあって使い分ける必要があります。

Schema Message Converter の難しい点

Schema Message Converterでは複数入力を扱うことができません。 複数のトピックを統合して一つの出力としたい場合はTopic Message Converterを使う必要があります。

また、入力/出力Schemaのペアに対して1つしか設定できません (これは公式ドキュメントでは明確に記述がありませんが、2025/12/19時点での実動作はこうなっています)。 同じ入力を複数の形式で可視化したいということがあるのですが、そういう場合にはSchema Message Converterは使いにくいです。例としては、3Dオブジェクトとラベルの組み合わせで構成されるようなつぎのような表示があります。

複数SceneUpdateの例

このとき、ラベルだけ非表示/表示をUIから切り替えられるにしたいという場合には、ラベルと直線をそれぞれ別のSceneUpdateメッセージにする必要がありますが、入力も出力もSchemaが同じなので別のSchema Message Converterを登録することができません (2つめ以降登録しても無視されます)。 マニアックな話ですが、これに対してtopic aliases という機能を組み合わせることで同じtopicに対して複数変換するというハックができますが、今日ではこういった場合にはTopic Message Converterを使うほうがやりやすいです。

Topic Message Converter の難しい点

Topic Message Converter はトピック名を予め決める必要があるので、同じSchemaの別のトピック名のメッセージが来ても、可視化できません。 いまのところ、別のtopicを可視化したいときにはExtensionの実装を変えるということをやっているので、イレギュラーなケースではFoxglove Extentionの開発に慣れたメンバしか使いこなせていません。

以上を踏まえると、基本的にSchema Message Converterが使える場合には使ったほうがいいです。ただ使えないケースも多いので、そういうときはTopic Message Converterを使うことになります。

Custum Panel

Foxgloveの標準パネルだけでは表現しづらい表示をしたいときに、custom panel extensionでパネル自体を実装できます。 トピックを購読して独自に描画したり、必要があればpublishしたり、といったことができます。 そのため、Message Converterのような使い方もできなくはないかもしれませんが、それについては検証したことがありません。

LOVOTでは複数搭載されているToFセンサの計測値やその信頼度などを表示するパネルを独自に作って使っています。つぎの図は一部を抜粋しています。センサが色々な向きに回転して取り付けられているので、生のデータより見やすい形に回転させたりすることでログの分析がしやすくなります。

センサ値表示用パネル

User Script / User-Script Utility

User Scriptは、ログファイル全体を処理した出力を作りたい場合に使います。 例えば最もLOVOTに近づいた人の距離、のような時系列上の統計をしたい場合などがわたしたちのユースケースです。ただし、Topic Message Converter でもstatefulな変換ができるので、User Scriptじゃないとできないというものはなかなか無いと思います。User Scriptは各ユーザーが自由に自分のローカルで書いて保存できるという点が他のConverterとは少し違う点です。一方で、Foxglove Organization内での共有ができないという課題がありました。最近リリースされた、Foxglove 2.39.0 からはユーザースクリプトそのものではないですが、ユーザースクリプトから使えるutilityをExtensionに追加することでOrganization内で共有できるようになり、社内共有に少し近づきました。

Data Loader(独自ファイル形式の読み込み)

「そもそもログがFoxgloveで直接開けない形式」になっている場合は、data loaderで読み込み処理を実装して、Foxgloveにメッセージとして渡すことができます。 LOVOTでもmcapに変える前の旧形式のCSVデータなどがあるので、使ってみたいなと思うのですが、まだ試せていません。

おわりに

LOVOTのふるまい開発のための可視化ツールについてご紹介しました。 今回ご紹介できなかった様々な可視化の工夫がありますので、チャンスがあったらまた公開できればと思います。読んでくださりありがとうございました。 GROOVE Xでは、一緒に働く仲間を募集しています。少しでも興味を持ってくださった方がいましたら、下記のリンクをご参照ください。

recruit.jobcan.jp

JetsonでCUDAやるなら統合メモリが幸せかと思ったらそれは幻想だったのかもしれない

この記事はGROOVE X Advent Calendar 2025の21日目の記事です。

こんにちは、「あず」こと斎藤@aznhe21です。 肩掛けスピーカーのSRS-NB10が内部で断線したのでBravia Theatre Uに乗り換えたんですが、低音が激しくて新しい体験でした。 首の部分をぐにゃぐにゃ曲げても断線する心配がなさそうなのも良きです。

さて、LOVOT 3.0ではJetson Orinを採用しており、内部ではCUDAも積極的に使用しています。 ここでCUDAの統合メモリが便利だったのでご紹介したいと思います(罠と共に)。

JetsonでCUDAやるなら統合メモリが幸せかと思ったらそれは幻想だったのかもしれない

続きを読む

照度連携確認BOXのDIY

この記事は、GROOVE X Advent Calendar 2025の20日目の記事です。

こんにちは、SWエンジニアの aoike です。 今回は、LOVOTの照度連携機能の話と、その動作を検証するためにDIYでテスト環境を作ったお話をします。

LOVOTの「照度連携」とは?

LOVOTのホーンにはリング型のLEDがついており、LOVOTの電源状態や動作状態を表しています。

このLEDは、眩しくなりすぎないように、周囲の明るさに合わせて自動で輝度を調整しています。LOVOTの照度センサーで環境の明るさを取得し、暗い場所では眩しくないように減光したり、真っ暗になると消灯したりします。こうした機能を私たちは照度連携と呼んでいます。

課題:狙った明るさが作れない!

この照度連携のテストを行う際、これまでは以下のような方法をとっていました。

  • 調光機能付きの照明のついた部屋を使う
  • 人工太陽灯を使う

しかし、部屋全体の明るさを「5ルクス」「10ルクス」といった細かい段階で均一に調整するのは非常に難しく、再現性のあるテスト環境を作るのが課題でした。

「もっと手軽に、照度連携のテストがしたい……」

ということで、「照度連携確認BOX」を自作することにしました。

照度センシング機能付きBOXを作る

カラーボックスを改造してLOVOTが入れる小さな個室を作り、その内部に光源となるライトを置くことで、明るさを自由にコントロールできるようにします。

そして、BOX内の現在の照度が、設定通りになっているかを客観的に確認するため、Raspberry Pi Pico W と照度センサー BH1750 を使った無線照度計を組み込みました。

構成イメージ

システム構成

システム全体の構成は以下の通りです。

Raspberry Pi Pico W がBH1750から値を読み取り、MQTTでデータを飛ばします。

実装のポイント

今回は開発言語に C言語 (Pico C/C++ SDK) を使いました。

BH1750のデータシートはこちらを参考にしました。
I2C通信を使い、アドレスとして0x23を指定します。

1. モード設定(高分解能モード)

今回のBOXは睡眠時間も想定した、真っ暗な部屋の再現が必要です。

データシートには以下のような記述があります。

"Use H-resolution mode or H-resolution mode2 if dark data ( less than 10 lx ) is need." (10ルクス以下の暗いデータを扱う場合は、高分解能モードを使用してください)

暗い部屋だと、ちょうど10ルクス以下の値が必要になります。そのため、今回は高分解能モードを指定するコマンドを使用します。また、通常の低分解能モードだと約4ルクス刻みでしか値が取れませんが、高分解能モードなら1ルクス単位で取ることができます。

// H-Resolution Mode (1lx単位, 測定時間120ms)
#define BH1750_CMD_CONT_HIGH_RES 0x10

最大180msの計測時間経過後、計測結果を取得します。

2. Measurement Accuracy の補正

Measurement Accuracy の項目を確認すると、以下の数式が載っていました。

  • X: 測定時間レジスタ(MTreg)の値。デフォルトは 69

デフォルト設定で使う限り、後半の 69 / 69は 1になります。生データは実際のルクス値の約1.2倍になる仕様のため、ソフトウェア側で補正が必要になります。

static bool bh1750_read_lux(float *lux_out) {
    // ... (読み込み処理) ...
    uint16_t raw = (buf[0] << 8) | buf[1];
    
    // 1.2で割ってLuxに変換
    *lux_out = (float)raw / 1.2f;  
    return true;
}

3. データの読み取り

実装したプログラムをPico Wに書き込み、PCからMQTTでデータをサブスクライブ(受信)してみます。 以下のように mosquitto_sub コマンドを使用しました。

mosquitto_sub -h <ipアドレス> -t 'topic名'

今回は、illuminanceという名前でpublishするようにしました。

消灯した状態でBOXのドアを閉めると、以下のような値が取れました。

$ mosquitto_sub -h 172.20.10.14 -t '/illuminance'
0.83

まとめ

こうして完成した「照度連携確認BOX」のおかげで、狙った明るさの環境を作り、定量的かつ再現性高く、手軽にテストできるようになりました。 従来のテスト手法と合わせて、より信頼性の高いソフトウェアを届けていきたいと思います。

いったんサイズを測るために入ってもらったところ

最後まで読んで頂きありがとうございます!GROOVE Xでは、一緒に働く仲間を募集しています。少しでも興味を持ってくださった方がいましたら、下記のリンクをご参照ください。

recruit.jobcan.jp