Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 12259] ファイルの概要

このコミットは、Go言語のランタイムプロファイリングツール pprof がmacOS (旧称 OS X) 上でCPUプロファイリングをサポートするための重要な変更を含んでいます。特に、macOSカーネルのバグに対する回避策の導入、addr2line および objdump というGo独自のツールの追加、そしてpprofがこれらの新しいツールを利用するように適応された点が主な内容です。これにより、macOS環境でのGoアプリケーションのパフォーマンス分析能力が向上しました。

コミット

commit 6e2ae0a12c0f73da56d4f465e68208731b4b16be
Author: Russ Cox <rsc@golang.org>
Date:   Tue Feb 28 16:18:24 2012 -0500

    runtime/pprof: support OS X CPU profiling
    
    Work around profiling kernel bug with signal masks.
    Still broken on 64-bit Snow Leopard kernel,
    but I think we can ignore that one and let people
    upgrade to Lion.
    
    Add new trivial tools addr2line and objdump to take
    the place of the GNU tools of the same name, since
    those are not installed on OS X.
    
    Adapt pprof to invoke 'go tool addr2line' and
    'go tool objdump' if the system tools do not exist.
    
    Clean up disassembly of base register on amd64.
    
    Fixes #2008.
    
    R=golang-dev, bradfitz, mikioh.mikioh, r, iant
    CC=golang-dev
    https://golang.org/cl/5697066

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/6e2ae0a12c0f73da56d4f465e68208731b4b16be

元コミット内容

このコミットは、Goのプロファイリングツール pprof がmacOS上でCPUプロファイリングを適切に機能させるための複数の変更を導入しています。

主な変更点:

  • macOSカーネルのプロファイリングバグへの対応: シグナルマスクを操作することで、プロファイリングシグナルがスリープ中のスレッドに誤って配信されるmacOSカーネルのバグ(特にSnow Leopard 64-bitカーネルで顕著)を回避します。
  • Go独自のaddr2lineobjdumpツールの追加: macOSにはGNUのaddr2lineobjdumpが標準でインストールされていないため、Goツールチェーン内にこれらの機能を提供する軽量なバージョンが追加されました。これらはgo tool addr2lineおよびgo tool objdumpとして利用されます。
  • pprofの適応: pprofスクリプトが、システムにaddr2lineobjdumpが存在しない場合に、新しく追加されたgo toolバージョンを自動的に使用するように変更されました。
  • amd64における逆アセンブルの改善: ベースレジスタの逆アセンブル表示がクリーンアップされました。
  • 関連するIssueの修正: このコミットは、GoのIssue #2008 を修正します。

ファイル変更の概要:

  • misc/pprof: addr2lineobjdumpのフォールバックロジックを追加。
  • src/cmd/addr2line/main.c: addr2lineツールのC言語実装を新規追加。
  • src/cmd/objdump/main.c: objdumpツールのC言語実装を新規追加。
  • src/cmd/dist/build.c: addr2lineobjdumpをビルドプロセスに追加。
  • src/libmach/8db.c: amd64の逆アセンブルロジックを修正。
  • src/pkg/runtime/*: プロファイリングシグナルハンドリング、特にmacOSにおけるSIGPROFの挙動を制御するための変更。runtime·setprof関数の追加と、notesleep, notetsleep, entersyscall, exitsyscall, resetcpuprofiler, semasleepなどの関数でのsetprofの呼び出し。
  • src/pkg/runtime/pprof/pprof.go: macOSのCPUプロファイリングに関する既知のバグについてのコメントを更新。
  • src/pkg/runtime/pprof/pprof_test.go: Snow Leopard 64-bitカーネルでのテストスキップロジックを追加。

変更の背景

このコミットの主要な背景には、macOS環境におけるGo言語のCPUプロファイリングの課題がありました。

  1. macOSカーネルのプロファイリングバグ: 当時のmacOS (特にSnow Leopard) のカーネルには、CPUプロファイリングに使用されるSIGPROFシグナルの配信に関するバグが存在しました。通常、SIGPROFはCPU時間を消費しているスレッドに送られるべきですが、macOSではスリープ中のスレッドにも送られてしまうことがありました。これはプロファイリングデータの精度を著しく低下させる問題でした。Goのランタイムは、このシグナルを利用してプロファイル情報を収集するため、このバグはGoアプリケーションのCPUプロファイリングを困難にしていました。コミットメッセージでは、64-bit Snow Leopardカーネルでは依然として問題が残るものの、Lionへのアップグレードを推奨することでこの問題を「無視できる」と判断しています。

  2. GNUツールの非存在: pprofツールは、プロファイルデータを人間が読める形式(関数名やソースコードの行番号など)に変換するために、通常はGNU Binutilsに含まれるaddr2lineobjdumpといった外部ツールに依存していました。しかし、macOS環境ではこれらのGNUツールが標準でインストールされておらず、ユーザーが別途インストールする必要がありました。これはGo開発者にとって不便であり、プロファイリングの利用を妨げる要因となっていました。

これらの問題を解決し、macOS上でのGoアプリケーションのプロファイリング体験を向上させることが、このコミットの目的でした。特に、Go独自のaddr2lineobjdumpを導入することで、外部依存を減らし、Goツールチェーンの自己完結性を高める狙いがありました。

前提知識の解説

このコミットを理解するためには、以下の技術的な概念について知っておく必要があります。

  1. CPUプロファイリング:

    • 目的: プロファイリングは、プログラムの実行中にどの部分が最も多くのCPU時間を消費しているかを特定する手法です。これにより、パフォーマンスのボトルネックを特定し、最適化の対象を見つけることができます。
    • サンプリングプロファイリング: GoのCPUプロファイリングは、サンプリングベースで行われます。これは、一定の間隔(例えば100Hz、つまり1秒間に100回)でプログラムの実行を一時停止し、その時点でのコールスタック(関数呼び出しの履歴)を記録する方式です。これにより、プログラムがどの関数で時間を費やしているかの統計的な情報を得ることができます。
    • SIGPROFシグナル: Unix系システムでは、ITIMER_PROFタイマーとSIGPROFシグナルがCPUプロファイリングによく利用されます。ITIMER_PROFは、プロセスが消費したCPU時間に基づいて定期的にSIGPROFシグナルを生成します。このシグナルを受け取ったプロセスは、シグナルハンドラ内で現在のコールスタックを記録します。
  2. pprofツール:

    • Go言語の標準プロファイリングツールです。Goアプリケーションから生成されたプロファイルデータ(通常はpprof形式)を解析し、テキスト、グラフ(SVG、PDFなど)、Webインターフェースなど、様々な形式で可視化します。
    • pprofは、プロファイルデータ内のメモリアドレスを実際の関数名やソースコードの行番号に変換するために、デバッグ情報(シンボル情報)を必要とします。この変換プロセスでaddr2lineobjdumpのような外部ツールが利用されます。
  3. addr2line:

    • GNU Binutilsに含まれるコマンドラインツールの一つです。実行可能ファイル内のメモリアドレスを受け取り、対応するソースファイル名と行番号を出力します。デバッグやプロファイリングにおいて、生のメモリアドレスを人間が読める情報に変換するために不可欠です。
  4. objdump:

    • GNU Binutilsに含まれるもう一つのコマンドラインツールです。実行可能ファイルやオブジェクトファイルの情報を表示します。特に、逆アセンブル機能(機械語コードをアセンブリ言語に変換する機能)は、特定のメモリアドレス範囲のコードを詳細に分析する際に使用されます。pprofでは、プロファイルされたコードの逆アセンブル表示に利用されます。
  5. シグナルとシグナルマスク:

    • シグナル: Unix系システムにおけるソフトウェア割り込みの一種で、非同期イベント(例: 外部からの終了要求、エラー発生、タイマー満了など)をプロセスに通知するメカニズムです。
    • シグナルマスク: プロセスが現在ブロックしている(つまり、受信してもすぐに処理せず保留する)シグナルのセットです。シグナルマスクを操作することで、特定のシグナルがスレッドに配信されるタイミングを制御できます。SIG_BLOCKはシグナルをブロックし、SIG_UNBLOCKはブロックを解除します。
  6. Goランタイム:

    • Goプログラムの実行を管理するシステムです。ガベージコレクション、スケジューリング、システムコールインターフェース、シグナルハンドリングなど、低レベルの機能を提供します。プロファイリング機能もランタイムの一部として実装されています。
  7. Goのビルドシステム (src/cmd/dist):

    • Goのソースコードをビルドし、Goツールチェーン全体を構築するための内部ツールです。新しいツール(addr2lineobjdump)をGoのビルドプロセスに組み込むために、この部分の変更が必要になります。

これらの知識が、コミットが解決しようとしている問題、その解決策、およびコード変更の具体的な内容を理解する上で基盤となります。

技術的詳細

このコミットの技術的詳細は、macOSにおけるCPUプロファイリングの課題を解決するための多角的なアプローチにあります。

1. macOSカーネルのプロファイリングバグへの回避策

コミットメッセージとsrc/pkg/runtime/pprof/pprof.goのコメントで言及されているように、当時のmacOSカーネル(特にSnow Leopard 64-bit)には、SIGPROFシグナルの配信に関するバグがありました。このバグは、CPU時間を消費しているスレッドではなく、スリープ中のスレッドにSIGPROFが誤って配信されるというものでした。これはプロファイリングの精度を著しく低下させます。

Goランタイムは、この問題を回避するために、シグナルマスクを動的に操作する戦略を採用しました。

  • runtime·setprof(bool on)関数の導入: この新しい関数は、現在のスレッドがSIGPROFシグナルを受信できるかどうかを制御します。
    • ontrueの場合(プロファイリングを有効にする場合)、SIGPROFがシグナルマスクからSIG_UNBLOCKされます。
    • onfalseの場合(プロファイリングを一時停止する場合)、SIGPROFがシグナルマスクにSIG_BLOCKされます。
  • ブロッキングシステムコール中のプロファイリング一時停止:
    • runtime·notesleep (セマフォ待機), runtime·notetsleep (タイムアウト付きセマフォ待機), runtime·entersyscall (システムコール開始), runtime·exitsyscall (システムコール終了), runtime·semasleep (セマフォ待機) などの関数が変更され、スレッドがブロッキング状態に入る直前にruntime·setprof(false)を呼び出してSIGPROFをブロックし、ブロッキング状態から抜けた直後にruntime·setprof(true)を呼び出してSIGPROFのブロックを解除するようにしました。
    • このアプローチにより、カーネルはSIGPROFをスリープ中のスレッドに送ることができなくなり、結果として実際にCPUを消費しているスレッドにシグナルが配信される可能性が高まります。これにより、プロファイリングデータの関連性が向上します。
  • runtime·resetcpuprofilerの変更: CPUプロファイラのリセット関数も、SIGPROFのハンドリングをruntime·setprofに委譲するように変更されました。

ただし、コミットメッセージにもあるように、この回避策は64-bit Snow Leopardカーネルでは完全ではありませんでした。そのカーネルでは、シグナルマスクに関わらずスリープ中のスレッドにSIGPROFが配信され、さらにスレッドが自発的に目覚めるまでシグナルが配信されないという問題が残っていました。このため、GoチームはユーザーにLionへのアップグレードを推奨しています。

2. Go独自のaddr2lineobjdumpツールの追加

macOSにはGNU Binutilsのaddr2lineobjdumpが標準でインストールされていないため、Goはこれらの機能を提供する独自の軽量ツールを導入しました。

  • src/cmd/addr2line/main.c:
    • このファイルは、addr2lineのGoバージョンをC言語で実装しています。
    • Goの内部ライブラリであるlibmach(Goのバイナリ形式を解析するためのライブラリ)を利用して、実行可能ファイルからシンボル情報(関数名、ファイル名、行番号)を抽出します。
    • 標準入力からメモリアドレスを読み込み、対応する関数名とファイル:行番号の形式で標準出力に出力します。これは、pprofがプロファイルデータをシンボル解決する際に必要とする最小限の機能を提供します。
  • src/cmd/objdump/main.c:
    • このファイルは、objdumpのGoバージョンをC言語で実装しています。
    • これもlibmachを利用して、指定された実行可能ファイルの特定のアドレス範囲を逆アセンブルします。
    • pprofがコードの逆アセンブル表示を行う際に利用されます。
  • src/cmd/dist/build.cの変更:
    • Goのビルドシステムに、これらの新しいツール(cmd/addr2linecmd/objdump)をビルド対象として追加しました。これにより、Goのインストール時にこれらのツールも自動的にビルドされ、go toolコマンドを通じて利用可能になります。

3. pprofツールの適応

既存のmisc/pprofスクリプト(Perlで書かれている)が、これらの新しいGoツールを認識し、利用するように変更されました。

  • フォールバックロジック: pprofスクリプトは、addr2lineobjdumpコマンドを実行する前に、システムにこれらのツールが存在するかどうかをsystem("$tool --help >/dev/null 2>&1")のようなコマンドでチェックします。
  • go toolの利用: もしシステムツールが見つからない場合、pprofは自動的にgo tool addr2linego tool objdumpにフォールバックするように設定されました。これにより、macOSユーザーは追加のツールをインストールすることなく、Goのプロファイリング機能を利用できるようになります。

4. その他の改善

  • src/libmach/8db.cの修正: amd64アーキテクチャにおける逆アセンブルの出力がクリーンアップされました。特に、ベースレジスタの表示に関する冗長な情報が削除され、より読みやすい形式になりました。
  • src/pkg/runtime/signal_darwin_386.cおよびsrc/pkg/runtime/signal_darwin_amd64.cの変更: SIGPROFハンドラ内で、gp != m->g0 && gp != m->gsignalという条件が追加されました。これは、プロファイリングシグナルがGoランタイムの内部ゴルーチン(g0gsignal)に配信された場合にはruntime·sigprofを呼び出さないようにすることで、プロファイリングデータのノイズを減らすためのものです。

これらの変更は、GoのプロファイリングエコシステムをmacOS環境に適合させ、より堅牢で使いやすいものにするための包括的な取り組みを示しています。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特に重要なのは以下のファイル群です。

  1. misc/pprof:

    • Disassemble サブルーチン内で、objdumpが見つからない場合にgo tool objdumpにフォールバックするロジックが追加されました。
    • MapToSymbols サブルーチン内で、addr2lineが見つからない場合にgo tool addr2lineにフォールバックするロジックが追加されました。
    --- a/misc/pprof
    +++ b/misc/pprof
    @@ -1234,6 +1234,13 @@ sub Disassemble {
       my $cmd = sprintf("$objdump -C -d -l --no-show-raw-insn " .
                         "--start-address=0x$start_addr " .
                         "--stop-address=0x$end_addr $prog");
    +
    +  if (system("$objdump --help >/dev/null 2>&1") != 0) {
    +    # objdump must not exist.  Fall back to go tool objdump.
    +    $objdump = "go tool objdump";
    +    $cmd = "$objdump $prog 0x$start_addr 0x$end_addr";
    +  }
    +
       open(OBJDUMP, "$cmd |") || error("$objdump: $!\\n");
       my @result = ();
       my $filename = "";
    @@ -4391,6 +4398,12 @@ sub MapToSymbols {
         $cmd = "$addr2line --demangle -f -C -e $image";
       }
     
    +  if (system("$addr2line --help >/dev/null 2>&1") != 0) {
    +    # addr2line must not exist.  Fall back to go tool addr2line.
    +    $addr2line = "go tool addr2line";
    +    $cmd = "$addr2line $image";
    +  }
    +
       # If "addr2line" isn't installed on the system at all, just use
       # nm to get what info we can (function names, but not line numbers).
       if (system("$addr2line --help >/dev/null 2>&1") != 0) {
    
  2. src/cmd/addr2line/main.c (新規ファイル):

    • Go独自のaddr2lineツールのC言語実装。crackhdr, syminit, findsym, filelineといったlibmachの関数を使用して、バイナリからシンボル情報を取得します。
    // Copyright 2012 The Go Authors. All rights reserved.
    // Use of this source code is governed by a BSD-style
    // license that can be found in the LICENSE file.
    
    /*
     * addr2line simulation - only enough to make pprof work on Macs
     */
    
    #include <u.h>
    #include <libc.h>
    #include <bio.h>
    #include <mach.h>
    
    // ... (usage function) ...
    
    void
    main(int argc, char **argv)
    {
        int fd;
        char *p;
        uvlong pc;
        Symbol s;
        Fhdr fhdr;
        Biobuf bin, bout;
        char file[1024];
    
        // ... (argument parsing) ...
    
        fd = open(argv[0], OREAD);
        if(fd < 0)
            sysfatal("open %s: %r", argv[0]);
        if(crackhdr(fd, &fhdr) <= 0)
            sysfatal("crackhdr: %r");
        machbytype(fhdr.type);
        if(syminit(fd, &fhdr) <= 0)
            sysfatal("syminit: %r");
    
        Binit(&bin, 0, OREAD);
        Binit(&bout, 1, OWRITE);
        for(;;) {
            p = Brdline(&bin, '\n');
            if(p == nil)
                break;
            p[Blinelen(&bin)-1] = '\0';
            pc = strtoull(p, 0, 16);
            if(!findsym(pc, CTEXT, &s))
                s.name = "??";
            if(!fileline(file, sizeof file, pc))
                strcpy(file, "??:0");
            Bprint(&bout, "%s\n%s\n", s.name, file);
        }
        Bflush(&bout);
        exits(0);
    }
    
  3. src/cmd/objdump/main.c (新規ファイル):

    • Go独自のobjdumpツールのC言語実装。crackhdr, syminit, loadmap, machdata->das, machdata->instsizeといったlibmachの関数を使用して、バイナリを逆アセンブルします。
    // Copyright 2012 The Go Authors. All rights reserved.
    // Use of this source code is governed by a BSD-style
    // license that can be found in the LICENSE file.
    
    /*
     * objdump simulation - only enough to make pprof work on Macs
     */
    
    #include <u.h>
    #include <libc.h>
    #include <bio.h>
    #include <mach.h>
    
    // ... (usage function) ...
    
    void
    main(int argc, char **argv)
    {
        int fd, n;
        uvlong pc, start, stop;
        Fhdr fhdr;
        Biobuf bout;
        char buf[1024];
        Map *text;
    
        // ... (argument parsing) ...
    
        fd = open(argv[0], OREAD);
        if(fd < 0)
            sysfatal("open %s: %r", argv[0]);
        if(crackhdr(fd, &fhdr) <= 0)
            sysfatal("crackhdr: %r");
        machbytype(fhdr.type);
        if(syminit(fd, &fhdr) <= 0)
            sysfatal("syminit: %r");
        text = loadmap(nil, fd, &fhdr);
        if(text == nil)
            sysfatal("loadmap: %r");
    
        Binit(&bout, 1, OWRITE);
        for(pc=start; pc<stop; ) {
            if(fileline(buf, sizeof buf, pc))
                Bprint(&bout, "%s\n", buf);
            buf[0] = '\0';
            machdata->das(text, pc, 0, buf, sizeof buf);
            Bprint(&bout, " %llx: %s\n", pc, buf);
            n = machdata->instsize(text, pc);
            if(n <= 0)
                break;
            pc += n;
        }
        Bflush(&bout);
        exits(0);
    }
    
  4. src/pkg/runtime/runtime.h:

    • runtime·setprof関数の宣言が追加されました。
    --- a/src/pkg/runtime/runtime.h
    +++ b/src/pkg/runtime/runtime.h
    @@ -730,3 +730,13 @@ bool	runtime·showframe(Func*);
     void	runtime·ifaceE2I(struct InterfaceType*, Eface, Iface*);
     
     uintptr	runtime·memlimit(void);
    +
    +// If appropriate, ask the operating system to control whether this
    +// thread should receive profiling signals.  This is only necessary on OS X.
    +// An operating system should not deliver a profiling signal to a
    +// thread that is not actually executing (what good is that?), but that's
    +// what OS X prefers to do.  When profiling is turned on, we mask
    +// away the profiling signal when threads go to sleep, so that OS X
    +// is forced to deliver the signal to a thread that's actually running.
    +// This is a no-op on other systems.
    +void	runtime·setprof(bool);
    
  5. src/pkg/runtime/thread_darwin.c:

    • runtime·setprof関数の実装が追加されました。これはSIGPROFシグナルをブロックまたはアンブロックするためにruntime·sigprocmaskを呼び出します。
    • runtime·semasleep内でruntime·setprofが呼び出され、セマフォ待機中にプロファイリングを一時停止します。
    • runtime·minit内で、プロファイリングが有効な場合にSIGPROFをアンブロックするように初期シグナルマスクを設定します。
    --- a/src/pkg/runtime/thread_darwin.c
    +++ b/src/pkg/runtime/thread_darwin.c
    @@ -11,6 +11,7 @@ extern SigTab runtime·sigtab[];
     
     static Sigset sigset_all = ~(Sigset)0;
     static Sigset sigset_none;
    +static Sigset sigset_prof = 1<<(SIGPROF-1);
     
     static void
     unimplemented(int8 *name)
    @@ -23,7 +24,14 @@ unimplemented(int8 *name)
     int32
     runtime·semasleep(int64 ns)
     {
    -\treturn runtime·mach_semacquire(m->waitsema, ns);
    +\tint32 v;
    +\n+\tif(m->profilehz > 0)\n+\t\truntime·setprof(false);\
    +\tv = runtime·mach_semacquire(m->waitsema, ns);\
    +\tif(m->profilehz > 0)\n+\t\truntime·setprof(true);\
    +\treturn v;
     }
     
     void
    @@ -98,7 +106,11 @@ runtime·minit(void)
     	// Initialize signal handling.
     	m->gsignal = runtime·malg(32*1024);\t// OS X wants >=8K, Linux >=2K
     	runtime·signalstack(m->gsignal->stackguard - StackGuard, 32*1024);\
    -\truntime·sigprocmask(SIG_SETMASK, &sigset_none, nil);
    +\n+\tif(m->profilehz > 0)\n+\t\truntime·sigprocmask(SIG_SETMASK, &sigset_none, nil);\
    +\telse
    +\t\truntime·sigprocmask(SIG_SETMASK, &sigset_prof, nil);\
     }
     
     // Mach IPC, to get at semaphores
    @@ -434,3 +446,34 @@ runtime·memlimit(void)
     	// the limit.
     	return 0;
     }
    +
    +// NOTE(rsc): On OS X, when the CPU profiling timer expires, the SIGPROF
    +// signal is not guaranteed to be sent to the thread that was executing to
    +// cause it to expire.  It can and often does go to a sleeping thread, which is
    +// not interesting for our profile.  This is filed Apple Bug Report #9177434,\n// copied to http://code.google.com/p/go/source/detail?r=35b716c94225.
    +// To work around this bug, we disable receipt of the profiling signal on
    +// a thread while in blocking system calls.  This forces the kernel to deliver
    +// the profiling signal to an executing thread.
    +//
    +// The workaround fails on OS X machines using a 64-bit Snow Leopard kernel.
    +// In that configuration, the kernel appears to want to deliver SIGPROF to the
    +// sleeping threads regardless of signal mask and, worse, does not deliver
    +// the signal until the thread wakes up on its own.
    +//
    +// If necessary, we can switch to using ITIMER_REAL for OS X and handle
    +// the kernel-generated SIGALRM by generating our own SIGALRMs to deliver
    +// to all the running threads.  SIGALRM does not appear to be affected by
    +// the 64-bit Snow Leopard bug.  However, as of this writing Mountain Lion
    +// is in preview, making Snow Leopard two versions old, so it is unclear how
    +// much effort we need to spend on one buggy kernel.
    +
    +// Control whether profiling signal can be delivered to this thread.
    +void
    +runtime·setprof(bool on)
    +{
    +\tif(on)\n+\t\truntime·sigprocmask(SIG_UNBLOCK, &sigset_prof, nil);\n+\telse
    +\t\truntime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);\n+}
    
  6. src/pkg/runtime/proc.c:

    • runtime·entersyscallruntime·exitsyscall内でruntime·setprofが呼び出され、システムコール中にプロファイリングを一時停止します。
    --- a/src/pkg/runtime/proc.c
    +++ b/src/pkg/runtime/proc.c
    @@ -916,6 +915,9 @@ runtime·entersyscall(void)
     {
     	uint32 v;
     
    +\tif(m->profilehz > 0)\n+\t\truntime·setprof(false);\n+\n     // Leave SP around for gc and traceback.
     	runtime·gosave(&g->sched);
     g->gcsp = g->sched.sp;
    @@ -979,6 +981,9 @@ runtime·exitsyscall(void)
     		// Garbage collector isn't running (since we are),
     		// so okay to clear gcstack.
     		g->gcstack = nil;
    +\n+\t\tif(m->profilehz > 0)\n+\t\t\truntime·setprof(true);\
     		return;
     	}
    

これらの変更は、macOSでのプロファイリングの信頼性を向上させるためのGoランタイムの低レベルな調整と、pprofツールが外部依存なしで機能するためのGoツールチェーンの拡張を示しています。

コアとなるコードの解説

ここでは、上記の「コアとなるコードの変更箇所」で示したコードスニペットについて、その役割と重要性を詳しく解説します。

1. misc/pprofにおけるフォールバックロジック

    if (system("$objdump --help >/dev/null 2>&1") != 0) {
      # objdump must not exist.  Fall back to go tool objdump.
      $objdump = "go tool objdump";
      $cmd = "$objdump $prog 0x$start_addr 0x$end_addr";
    }

このPerlコードスニペットは、pprofスクリプトがobjdumpコマンドを呼び出す前に実行されるチェックです。

  • system("$objdump --help >/dev/null 2>&1") != 0: これは、現在のシステムパスでobjdumpコマンドが見つかるかどうか、または実行可能であるかどうかをテストします。
    • $objdump --help: objdumpコマンドに--helpオプションを付けて実行します。これは通常、コマンドが存在し、実行可能であれば何らかのヘルプメッセージを出力します。
    • >/dev/null 2>&1: 標準出力と標準エラー出力を/dev/nullにリダイレクトします。これにより、ヘルプメッセージがコンソールに表示されるのを防ぎます。
    • system(...) != 0: system関数は、実行したコマンドの終了ステータスを返します。終了ステータスが0であれば成功(コマンドが見つかり、実行できた)、0以外であれば失敗(コマンドが見つからない、実行できないなど)を意味します。
  • # objdump must not exist. Fall back to go tool objdump.: コマンドが見つからなかった場合のコメントです。
  • $objdump = "go tool objdump";: objdumpコマンドのパスを、Goツールチェーンに含まれるgo tool objdumpに再設定します。
  • $cmd = "$objdump $prog 0x$start_addr 0x$end_addr";: 実際に実行するコマンド文字列を、新しいobjdumpパスとGoツールに合わせた引数形式で再構築します。

同様のロジックがaddr2lineに対しても適用されています。このフォールバックメカニズムにより、macOSユーザーはGNU Binutilsを別途インストールすることなく、Goのプロファイリング機能を利用できるようになり、ユーザーエクスペリエンスが大幅に向上しました。

2. src/cmd/addr2line/main.csrc/cmd/objdump/main.cの新規追加

これらのファイルは、Goツールチェーンに組み込まれる軽量なaddr2lineobjdumpの実装です。C言語で書かれており、Goの内部ライブラリであるlibmachを利用しています。

  • libmachの利用: libmachは、Goの実行可能ファイルやオブジェクトファイルの構造を解析し、シンボルテーブルやセクション情報にアクセスするためのライブラリです。
    • crackhdr(fd, &fhdr): ファイルディスクリプタfdから実行可能ファイルのヘッダ情報を解析し、fhdr構造体に格納します。
    • syminit(fd, &fhdr): シンボルテーブルを初期化し、シンボル情報をメモリにロードします。
    • findsym(pc, CTEXT, &s): 指定されたプログラムカウンタpcに対応する関数シンボルsを検索します。
    • fileline(file, sizeof file, pc): 指定されたプログラムカウンタpcに対応するソースファイル名と行番号を取得します。
    • machdata->das(text, pc, 0, buf, sizeof buf): 指定されたテキストセクションtext内のプログラムカウンタpcから命令を逆アセンブルし、結果をbufに格納します。
    • machdata->instsize(text, pc): 指定されたプログラムカウンタpcにある命令のサイズを返します。

これらのツールは、GNUの同名ツールに比べて機能は限定的ですが、pprofがプロファイルデータをシンボル解決し、逆アセンブル表示を行うために必要な最小限の機能を提供します。これにより、Goツールチェーンの自己完結性が高まり、macOS環境での外部依存が解消されました。

3. runtime·setprof(bool on)関数の導入と利用

// src/pkg/runtime/runtime.h
void	runtime·setprof(bool);

// src/pkg/runtime/thread_darwin.c
void
runtime·setprof(bool on)
{
	if(on)
		runtime·sigprocmask(SIG_UNBLOCK, &sigset_prof, nil);
	else
		runtime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);
}
  • runtime·setprofの役割: この関数は、現在のスレッドがSIGPROFシグナルを受信できるかどうかを制御します。ontrueの場合、SIGPROFはシグナルマスクから解除され、スレッドはシグナルを受信できるようになります。onfalseの場合、SIGPROFはシグナルマスクに追加され、スレッドはシグナルをブロックします。
  • sigset_prof: SIGPROFシグナルのみを含むシグナルセットです。
  • runtime·sigprocmask: Unix系システムコールsigprocmaskのGoランタイムラッパーです。スレッドのシグナルマスクを変更するために使用されます。SIG_UNBLOCKは指定されたシグナルをマスクから削除し、SIG_BLOCKは追加します。

この関数は、Goランタイムの様々な場所で呼び出されます。

  • src/pkg/runtime/lock_futex.csrc/pkg/runtime/lock_sema.c:

    • runtime·notesleep (セマフォ待機) や runtime·notetsleep (タイムアウト付きセマフォ待機) のような、スレッドが長時間ブロッキング状態に入る可能性のある関数内で、ブロッキングに入る直前にruntime·setprof(false)を呼び出し、ブロッキングから抜けた直後にruntime·setprof(true)を呼び出します。
    • これにより、スレッドがスリープしている間はSIGPROFがブロックされ、カーネルがスリープ中のスレッドにシグナルを誤って配信するのを防ぎます。
  • src/pkg/runtime/proc.c:

    • runtime·entersyscall (システムコールに入る前) と runtime·exitsyscall (システムコールから出る後) でruntime·setprofが呼び出されます。
    • システムコール中はスレッドがカーネルモードで実行されるため、この間もSIGPROFをブロックすることで、プロファイリングの精度を維持しようとします。

このシグナルマスクの動的な操作は、macOSカーネルのバグに対する主要な回避策であり、GoのCPUプロファイリングがより正確な結果を生成できるようにするための重要な変更です。

4. src/pkg/runtime/signal_darwin_386.cおよびsrc/pkg/runtime/signal_darwin_amd64.cSIGPROFハンドラ

    if(sig == SIGPROF) {
    	if(gp != m->g0 && gp != m->gsignal)
    		runtime·sigprof((uint8*)r->eip, (uint8*)r->esp, nil, gp);
    	return;
    }

このコードは、SIGPROFシグナルがGoランタイムのシグナルハンドラに到達した際に実行されます。

  • if(gp != m->g0 && gp != m->gsignal): この条件は非常に重要です。gpは現在のゴルーチン(Goの軽量スレッド)へのポインタです。
    • m->g0: Goランタイムのスケジューラやガベージコレクタなどの内部処理を実行するための特別なゴルーチンです。
    • m->gsignal: シグナルハンドラが実行される際に使用される特別なゴルーチンです。
  • この条件は、「現在のゴルーチンが、Goランタイムの内部処理やシグナルハンドリングのための特別なゴルーチンではない場合のみ、プロファイリング情報を記録する」ことを意味します。
  • 目的: Goランタイムの内部ゴルーチンがSIGPROFを受け取った場合、それはアプリケーションコードの実行とは直接関係のないCPU時間を示している可能性があります。これらのシグナルを無視することで、プロファイリングデータからノイズを取り除き、ユーザーのアプリケーションコードのパフォーマンスに焦点を当てた、よりクリーンで関連性の高いプロファイル情報を得ることができます。

これらのコアとなるコードの変更は、GoがmacOS環境でのプロファイリングの課題にどのように対処し、Goツールチェーンの独立性と堅牢性を高めたかを示しています。

関連リンク

参考にした情報源リンク

これらの情報源は、コミットの背景、技術的詳細、および関連する概念を理解するために参照されました。 I have completed the request. I have read the commit data, performed web searches for context, and generated a comprehensive technical explanation in Markdown format, adhering to all specified sections and requirements. The output is printed to standard output only, as requested.# [インデックス 12259] ファイルの概要

このコミットは、Go言語のランタイムプロファイリングツール pprof がmacOS (旧称 OS X) 上でCPUプロファイリングをサポートするための重要な変更を含んでいます。特に、macOSカーネルのバグに対する回避策の導入、addr2line および objdump というGo独自のツールの追加、そしてpprofがこれらの新しいツールを利用するように適応された点が主な内容です。これにより、macOS環境でのGoアプリケーションのパフォーマンス分析能力が向上しました。

コミット

commit 6e2ae0a12c0f73da56d4f465e68208731b4b16be
Author: Russ Cox <rsc@golang.org>
Date:   Tue Feb 28 16:18:24 2012 -0500

    runtime/pprof: support OS X CPU profiling
    
    Work around profiling kernel bug with signal masks.
    Still broken on 64-bit Snow Leopard kernel,
    but I think we can ignore that one and let people
    upgrade to Lion.
    
    Add new trivial tools addr2line and objdump to take
    the place of the GNU tools of the same name, since
    those are not installed on OS X.
    
    Adapt pprof to invoke 'go tool addr2line' and
    'go tool objdump' if the system tools do not exist.
    
    Clean up disassembly of base register on amd64.
    
    Fixes #2008.
    
    R=golang-dev, bradfitz, mikioh.mikioh, r, iant
    CC=golang-dev
    https://golang.org/cl/5697066

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/6e2ae0a12c0f73da56d4f465e68208731b4b16be

元コミット内容

このコミットは、Goのプロファイリングツール pprof がmacOS上でCPUプロファイリングを適切に機能させるための複数の変更を導入しています。

主な変更点:

  • macOSカーネルのプロファイリングバグへの対応: シグナルマスクを操作することで、プロファイリングシグナルがスリープ中のスレッドに誤って配信されるmacOSカーネルのバグ(特にSnow Leopard 64-bitカーネルで顕著)を回避します。
  • Go独自のaddr2lineobjdumpツールの追加: macOSにはGNUのaddr2lineobjdumpが標準でインストールされていないため、Goツールチェーン内にこれらの機能を提供する軽量なバージョンが追加されました。これらはgo tool addr2lineおよびgo tool objdumpとして利用されます。
  • pprofの適応: pprofスクリプトが、システムにaddr2lineobjdumpが存在しない場合に、新しく追加されたgo toolバージョンを自動的に使用するように変更されました。
  • amd64における逆アセンブルの改善: ベースレジスタの逆アセンブル表示がクリーンアップされました。
  • 関連するIssueの修正: このコミットは、GoのIssue #2008 を修正します。

ファイル変更の概要:

  • misc/pprof: addr2lineobjdumpのフォールバックロジックを追加。
  • src/cmd/addr2line/main.c: addr2lineツールのC言語実装を新規追加。
  • src/cmd/objdump/main.c: objdumpツールのC言語実装を新規追加。
  • src/cmd/dist/build.c: addr2lineobjdumpをビルドプロセスに追加。
  • src/libmach/8db.c: amd64の逆アセンブルロジックを修正。
  • src/pkg/runtime/*: プロファイリングシグナルハンドリング、特にmacOSにおけるSIGPROFの挙動を制御するための変更。runtime·setprof関数の追加と、notesleep, notetsleep, entersyscall, exitsyscall, resetcpuprofiler, semasleepなどの関数でのsetprofの呼び出し。
  • src/pkg/runtime/pprof/pprof.go: macOSのCPUプロファイリングに関する既知のバグについてのコメントを更新。
  • src/pkg/runtime/pprof/pprof_test.go: Snow Leopard 64-bitカーネルでのテストスキップロジックを追加。

変更の背景

このコミットの主要な背景には、macOS環境におけるGo言語のCPUプロファイリングの課題がありました。

  1. macOSカーネルのプロファイリングバグ: 当時のmacOS (特にSnow Leopard) のカーネルには、CPUプロファイリングに使用されるSIGPROFシグナルの配信に関するバグが存在しました。通常、SIGPROFはCPU時間を消費しているスレッドに送られるべきですが、macOSではスリープ中のスレッドにも送られてしまうことがありました。これはプロファイリングデータの精度を著しく低下させる問題でした。Goのランタイムは、このシグナルを利用してプロファイル情報を収集するため、このバグはGoアプリケーションのCPUプロファイリングを困難にしていました。コミットメッセージでは、64-bit Snow Leopardカーネルでは依然として問題が残るものの、Lionへのアップグレードを推奨することでこの問題を「無視できる」と判断しています。

  2. GNUツールの非存在: pprofツールは、プロファイルデータを人間が読める形式(関数名やソースコードの行番号など)に変換するために、通常はGNU Binutilsに含まれるaddr2lineobjdumpといった外部ツールに依存していました。しかし、macOS環境ではこれらのGNUツールが標準でインストールされておらず、ユーザーが別途インストールする必要がありました。これはGo開発者にとって不便であり、プロファイリングの利用を妨げる要因となっていました。

これらの問題を解決し、macOS上でのGoアプリケーションのプロファイリング体験を向上させることが、このコミットの目的でした。特に、Go独自のaddr2lineobjdumpを導入することで、外部依存を減らし、Goツールチェーンの自己完結性を高める狙いがありました。

前提知識の解説

このコミットを理解するためには、以下の技術的な概念について知っておく必要があります。

  1. CPUプロファイリング:

    • 目的: プロファイリングは、プログラムの実行中にどの部分が最も多くのCPU時間を消費しているかを特定する手法です。これにより、パフォーマンスのボトルネックを特定し、最適化の対象を見つけることができます。
    • サンプリングプロファイリング: GoのCPUプロファイリングは、サンプリングベースで行われます。これは、一定の間隔(例えば100Hz、つまり1秒間に100回)でプログラムの実行を一時停止し、その時点でのコールスタック(関数呼び出しの履歴)を記録する方式です。これにより、プログラムがどの関数で時間を費やしているかの統計的な情報を得ることができます。
    • SIGPROFシグナル: Unix系システムでは、ITIMER_PROFタイマーとSIGPROFシグナルがCPUプロファイリングによく利用されます。ITIMER_PROFは、プロセスが消費したCPU時間に基づいて定期的にSIGPROFシグナルを生成します。このシグナルを受け取ったプロセスは、シグナルハンドラ内で現在のコールスタックを記録します。
  2. pprofツール:

    • Go言語の標準プロファイリングツールです。Goアプリケーションから生成されたプロファイルデータ(通常はpprof形式)を解析し、テキスト、グラフ(SVG、PDFなど)、Webインターフェースなど、様々な形式で可視化します。
    • pprofは、プロファイルデータ内のメモリアドレスを実際の関数名やソースコードの行番号に変換するために、デバッグ情報(シンボル情報)を必要とします。この変換プロセスでaddr2lineobjdumpのような外部ツールが利用されます。
  3. addr2line:

    • GNU Binutilsに含まれるコマンドラインツールの一つです。実行可能ファイル内のメモリアドレスを受け取り、対応するソースファイル名と行番号を出力します。デバッグやプロファイリングにおいて、生のメモリアドレスを人間が読める情報に変換するために不可欠です。
  4. objdump:

    • GNU Binutilsに含まれるもう一つのコマンドラインツールです。実行可能ファイルやオブジェクトファイルの情報を表示します。特に、逆アセンブル機能(機械語コードをアセンブリ言語に変換する機能)は、特定のメモリアドレス範囲のコードを詳細に分析する際に使用されます。pprofでは、プロファイルされたコードの逆アセンブル表示に利用されます。
  5. シグナルとシグナルマスク:

    • シグナル: Unix系システムにおけるソフトウェア割り込みの一種で、非同期イベント(例: 外部からの終了要求、エラー発生、タイマー満了など)をプロセスに通知するメカニズムです。
    • シグナルマスク: プロセスが現在ブロックしている(つまり、受信してもすぐに処理せず保留する)シグナルのセットです。シグナルマスクを操作することで、特定のシグナルがスレッドに配信されるタイミングを制御できます。SIG_BLOCKはシグナルをブロックし、SIG_UNBLOCKはブロックを解除します。
  6. Goランタイム:

    • Goプログラムの実行を管理するシステムです。ガベージコレクション、スケジューリング、システムコールインターフェース、シグナルハンドリングなど、低レベルの機能を提供します。プロファイリング機能もランタイムの一部として実装されています。
  7. Goのビルドシステム (src/cmd/dist):

    • Goのソースコードをビルドし、Goツールチェーン全体を構築するための内部ツールです。新しいツール(addr2lineobjdump)をGoのビルドプロセスに組み込むために、この部分の変更が必要になります。

これらの知識が、コミットが解決しようとしている問題、その解決策、およびコード変更の具体的な内容を理解する上で基盤となります。

技術的詳細

このコミットの技術的詳細は、macOSにおけるCPUプロファイリングの課題を解決するための多角的なアプローチにあります。

1. macOSカーネルのプロファイリングバグへの回避策

コミットメッセージとsrc/pkg/runtime/pprof/pprof.goのコメントで言及されているように、当時のmacOSカーネル(特にSnow Leopard 64-bit)には、SIGPROFシグナルの配信に関するバグがありました。このバグは、CPU時間を消費しているスレッドではなく、スリープ中のスレッドにSIGPROFが誤って配信されるというものでした。これはプロファイリングの精度を著しく低下させます。

Goランタイムは、この問題を回避するために、シグナルマスクを動的に操作する戦略を採用しました。

  • runtime·setprof(bool on)関数の導入: この新しい関数は、現在のスレッドがSIGPROFシグナルを受信できるかどうかを制御します。
    • ontrueの場合(プロファイリングを有効にする場合)、SIGPROFがシグナルマスクからSIG_UNBLOCKされます。
    • onfalseの場合(プロファイリングを一時停止する場合)、SIGPROFがシグナルマスクにSIG_BLOCKされます。
  • ブロッキングシステムコール中のプロファイリング一時停止:
    • runtime·notesleep (セマフォ待機), runtime·notetsleep (タイムアウト付きセマフォ待機), runtime·entersyscall (システムコール開始), runtime·exitsyscall (システムコール終了), runtime·semasleep (セマフォ待機) などの関数が変更され、スレッドがブロッキング状態に入る直前にruntime·setprof(false)を呼び出してSIGPROFをブロックし、ブロッキング状態から抜けた直後にruntime·setprof(true)を呼び出してSIGPROFのブロックを解除するようにしました。
    • このアプローチにより、カーネルはSIGPROFをスリープ中のスレッドに送ることができなくなり、結果として実際にCPUを消費しているスレッドにシグナルが配信される可能性が高まります。これにより、プロファイリングデータの関連性が向上します。
  • runtime·resetcpuprofilerの変更: CPUプロファイラのリセット関数も、SIGPROFのハンドリングをruntime·setprofに委譲するように変更されました。

ただし、コミットメッセージにもあるように、この回避策は64-bit Snow Leopardカーネルでは完全ではありませんでした。そのカーネルでは、シグナルマスクに関わらずスリープ中のスレッドにSIGPROFが配信され、さらにスレッドが自発的に目覚めるまでシグナルが配信されないという問題が残っていました。このため、GoチームはユーザーにLionへのアップグレードを推奨しています。

2. Go独自のaddr2lineobjdumpツールの追加

macOSにはGNU Binutilsのaddr2lineobjdumpが標準でインストールされていないため、Goはこれらの機能を提供する独自の軽量ツールを導入しました。

  • src/cmd/addr2line/main.c:
    • このファイルは、addr2lineのGoバージョンをC言語で実装しています。
    • Goの内部ライブラリであるlibmach(Goのバイナリ形式を解析するためのライブラリ)を利用して、実行可能ファイルからシンボル情報(関数名、ファイル名、行番号)を抽出します。
    • 標準入力からメモリアドレスを読み込み、対応する関数名とファイル:行番号の形式で標準出力に出力します。これは、pprofがプロファイルデータをシンボル解決する際に必要とする最小限の機能を提供します。
  • src/cmd/objdump/main.c:
    • このファイルは、objdumpのGoバージョンをC言語で実装しています。
    • これもlibmachを利用して、指定された実行可能ファイルの特定のアドレス範囲を逆アセンブルします。
    • pprofがコードの逆アセンブル表示を行う際に利用されます。
  • src/cmd/dist/build.cの変更:
    • Goのビルドシステムに、これらの新しいツール(cmd/addr2linecmd/objdump)をビルド対象として追加しました。これにより、Goのインストール時にこれらのツールも自動的にビルドされ、go toolコマンドを通じて利用可能になります。

3. pprofツールの適応

既存のmisc/pprofスクリプト(Perlで書かれている)が、これらの新しいGoツールを認識し、利用するように変更されました。

  • フォールバックロジック: pprofスクリプトは、addr2lineobjdumpコマンドを実行する前に、システムにこれらのツールが存在するかどうかをsystem("$tool --help >/dev/null 2>&1")のようなコマンドでチェックします。
  • go toolの利用: もしシステムツールが見つからない場合、pprofは自動的にgo tool addr2linego tool objdumpにフォールバックするように設定されました。これにより、macOSユーザーは追加のツールをインストールすることなく、Goのプロファイリング機能を利用できるようになります。

4. その他の改善

  • src/libmach/8db.cの修正: amd64アーキテクチャにおける逆アセンブルの出力がクリーンアップされました。特に、ベースレジスタの表示に関する冗長な情報が削除され、より読みやすい形式になりました。
  • src/pkg/runtime/signal_darwin_386.cおよびsrc/pkg/runtime/signal_darwin_amd64.cの変更: SIGPROFハンドラ内で、gp != m->g0 && gp != m->gsignalという条件が追加されました。これは、プロファイリングシグナルがGoランタイムの内部ゴルーチン(g0gsignal)に配信された場合にはruntime·sigprofを呼び出さないようにすることで、プロファイリングデータのノイズを減らすためのものです。

これらの変更は、GoのプロファイリングエコシステムをmacOS環境に適合させ、より堅牢で使いやすいものにするための包括的な取り組みを示しています。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特に重要なのは以下のファイル群です。

  1. misc/pprof:

    • Disassemble サブルーチン内で、objdumpが見つからない場合にgo tool objdumpにフォールバックするロジックが追加されました。
    • MapToSymbols サブルーチン内で、addr2lineが見つからない場合にgo tool addr2lineにフォールバックするロジックが追加されました。
    --- a/misc/pprof
    +++ b/misc/pprof
    @@ -1234,6 +1234,13 @@ sub Disassemble {
       my $cmd = sprintf("$objdump -C -d -l --no-show-raw-insn " .
                         "--start-address=0x$start_addr " .
                         "--stop-address=0x$end_addr $prog");
    +
    +  if (system("$objdump --help >/dev/null 2>&1") != 0) {
    +    # objdump must not exist.  Fall back to go tool objdump.
    +    $objdump = "go tool objdump";
    +    $cmd = "$objdump $prog 0x$start_addr 0x$end_addr";
    +  }
    +
       open(OBJDUMP, "$cmd |") || error("$objdump: $!\\n");
       my @result = ();
       my $filename = "";
    @@ -4391,6 +4398,12 @@ sub MapToSymbols {
         $cmd = "$addr2line --demangle -f -C -e $image";
       }
     
    +  if (system("$addr2line --help >/dev/null 2>&1") != 0) {
    +    # addr2line must not exist.  Fall back to go tool addr2line.
    +    $addr2line = "go tool addr2line";
    +    $cmd = "$addr2line $image";
    +  }
    +
       # If "addr2line" isn't installed on the system at all, just use
       # nm to get what info we can (function names, but not line numbers).
       if (system("$addr2line --help >/dev/null 2>&1") != 0) {
    
  2. src/cmd/addr2line/main.c (新規ファイル):

    • Go独自のaddr2lineツールのC言語実装。crackhdr, syminit, findsym, filelineといったlibmachの関数を使用して、バイナリからシンボル情報を取得します。
    // Copyright 2012 The Go Authors. All rights reserved.
    // Use of this source code is governed by a BSD-style
    // license that can be found in the LICENSE file.
    
    /*
     * addr2line simulation - only enough to make pprof work on Macs
     */
    
    #include <u.h>
    #include <libc.h>
    #include <bio.h>
    #include <mach.h>
    
    // ... (usage function) ...
    
    void
    main(int argc, char **argv)
    {
        int fd;
        char *p;
        uvlong pc;
        Symbol s;
        Fhdr fhdr;
        Biobuf bin, bout;
        char file[1024];
    
        // ... (argument parsing) ...
    
        fd = open(argv[0], OREAD);
        if(fd < 0)
            sysfatal("open %s: %r", argv[0]);
        if(crackhdr(fd, &fhdr) <= 0)
            sysfatal("crackhdr: %r");
        machbytype(fhdr.type);
        if(syminit(fd, &fhdr) <= 0)
            sysfatal("syminit: %r");
    
        Binit(&bin, 0, OREAD);
        Binit(&bout, 1, OWRITE);
        for(;;) {
            p = Brdline(&bin, '\n');
            if(p == nil)
                break;
            p[Blinelen(&bin)-1] = '\0';
            pc = strtoull(p, 0, 16);
            if(!findsym(pc, CTEXT, &s))
                s.name = "??";
            if(!fileline(file, sizeof file, pc))
                strcpy(file, "??:0");
            Bprint(&bout, "%s\n%s\n", s.name, file);
        }
        Bflush(&bout);
        exits(0);
    }
    
  3. src/cmd/objdump/main.c (新規ファイル):

    • Go独自のobjdumpツールのC言語実装。crackhdr, syminit, loadmap, machdata->das, machdata->instsizeといったlibmachの関数を使用して、バイナリを逆アセンブルします。
    // Copyright 2012 The Go Authors. All rights reserved.
    // Use of this source code is governed by a BSD-style
    // license that can be found in the LICENSE file.
    
    /*
     * objdump simulation - only enough to make pprof work on Macs
     */
    
    #include <u.h>
    #include <libc.h>
    #include <bio.h>
    #include <mach.h>
    
    // ... (usage function) ...
    
    void
    main(int argc, char **argv)
    {
        int fd, n;
        uvlong pc, start, stop;
        Fhdr fhdr;
        Biobuf bout;
        char buf[1024];
        Map *text;
    
        // ... (argument parsing) ...
    
        fd = open(argv[0], OREAD);
        if(fd < 0)
            sysfatal("open %s: %r", argv[0]);
        if(crackhdr(fd, &fhdr) <= 0)
            sysfatal("crackhdr: %r");
        machbytype(fhdr.type);
        if(syminit(fd, &fhdr) <= 0)
            sysfatal("syminit: %r");
        text = loadmap(nil, fd, &fhdr);
        if(text == nil)
            sysfatal("loadmap: %r");
    
        Binit(&bout, 1, OWRITE);
        for(pc=start; pc<stop; ) {
            if(fileline(buf, sizeof buf, pc))
                Bprint(&bout, "%s\n", buf);
            buf[0] = '\0';
            machdata->das(text, pc, 0, buf, sizeof buf);
            Bprint(&bout, " %llx: %s\n", pc, buf);
            n = machdata->instsize(text, pc);
            if(n <= 0)
                break;
            pc += n;
        }
        Bflush(&bout);
        exits(0);
    }
    
  4. src/pkg/runtime/runtime.h:

    • runtime·setprof関数の宣言が追加されました。
    --- a/src/pkg/runtime/runtime.h
    +++ b/src/pkg/runtime/runtime.h
    @@ -730,3 +730,13 @@ bool	runtime·showframe(Func*);
     void	runtime·ifaceE2I(struct InterfaceType*, Eface, Iface*);
     
     uintptr	runtime·memlimit(void);
    +
    +// If appropriate, ask the operating system to control whether this
    +// thread should receive profiling signals.  This is only necessary on OS X.
    +// An operating system should not deliver a profiling signal to a
    +// thread that is not actually executing (what good is that?), but that's
    +// what OS X prefers to do.  When profiling is turned on, we mask
    +// away the profiling signal when threads go to sleep, so that OS X
    +// is forced to deliver the signal to a thread that's actually running.
    +// This is a no-op on other systems.
    +void	runtime·setprof(bool);
    
  5. src/pkg/runtime/thread_darwin.c:

    • runtime·setprof関数の実装が追加されました。これはSIGPROFシグナルをブロックまたはアンブロックするためにruntime·sigprocmaskを呼び出します。
    • runtime·semasleep内でruntime·setprofが呼び出され、セマフォ待機中にプロファイリングを一時停止します。
    • runtime·minit内で、プロファイリングが有効な場合にSIGPROFをアンブロックするように初期シグナルマスクを設定します。
    --- a/src/pkg/runtime/thread_darwin.c
    +++ b/src/pkg/runtime/thread_darwin.c
    @@ -11,6 +11,7 @@ extern SigTab runtime·sigtab[];
     
     static Sigset sigset_all = ~(Sigset)0;
     static Sigset sigset_none;
    +static Sigset sigset_prof = 1<<(SIGPROF-1);
     
     static void
     unimplemented(int8 *name)
    @@ -23,7 +24,14 @@ unimplemented(int8 *name)
     int32
     runtime·semasleep(int64 ns)
     {
    -\treturn runtime·mach_semacquire(m->waitsema, ns);
    +\tint32 v;
    +\n+\tif(m->profilehz > 0)\n+\t\truntime·setprof(false);\n+\tv = runtime·mach_semacquire(m->waitsema, ns);\n+\tif(m->profilehz > 0)\n+\t\truntime·setprof(true);\n+\treturn v;
     }
     
     void
    @@ -98,7 +106,11 @@ runtime·minit(void)
     	// Initialize signal handling.
     	m->gsignal = runtime·malg(32*1024);\t// OS X wants >=8K, Linux >=2K
     	runtime·signalstack(m->gsignal->stackguard - StackGuard, 32*1024);\
    -\truntime·sigprocmask(SIG_SETMASK, &sigset_none, nil);
    +\n+\tif(m->profilehz > 0)\n+\t\truntime·sigprocmask(SIG_SETMASK, &sigset_none, nil);\
    +\telse
    +\t\truntime·sigprocmask(SIG_SETMASK, &sigset_prof, nil);\
     }
     
     // Mach IPC, to get at semaphores
    @@ -434,3 +446,34 @@ runtime·memlimit(void)
     	// the limit.
     	return 0;
     }
    +
    +// NOTE(rsc): On OS X, when the CPU profiling timer expires, the SIGPROF
    +// signal is not guaranteed to be sent to the thread that was executing to
    +// cause it to expire.  It can and often does go to a sleeping thread, which is
    +// not interesting for our profile.  This is filed Apple Bug Report #9177434,\n// copied to http://code.google.com/p/go/source/detail?r=35b716c94225.
    +// To work around this bug, we disable receipt of the profiling signal on
    +// a thread while in blocking system calls.  This forces the kernel to deliver
    +// the profiling signal to an executing thread.
    +//
    +// The workaround fails on OS X machines using a 64-bit Snow Leopard kernel.
    +// In that configuration, the kernel appears to want to deliver SIGPROF to the
    +// sleeping threads regardless of signal mask and, worse, does not deliver
    +// the signal until the thread wakes up on its own.
    +//
    +// If necessary, we can switch to using ITIMER_REAL for OS X and handle
    +// the kernel-generated SIGALRM by generating our own SIGALRMs to deliver
    +// to all the running threads.  SIGALRM does not appear to be affected by
    +// the 64-bit Snow Leopard bug.  However, as of this writing Mountain Lion
    +// is in preview, making Snow Leopard two versions old, so it is unclear how
    +// much effort we need to spend on one buggy kernel.
    +
    +// Control whether profiling signal can be delivered to this thread.
    +void
    +runtime·setprof(bool on)
    +{
    +\tif(on)\n+\t\truntime·sigprocmask(SIG_UNBLOCK, &sigset_prof, nil);\n+\telse
    +\t\truntime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);\n+}
    
  6. src/pkg/runtime/proc.c:

    • runtime·entersyscallruntime·exitsyscall内でruntime·setprofが呼び出され、システムコール中にプロファイリングを一時停止します。
    --- a/src/pkg/runtime/proc.c
    +++ b/src/pkg/runtime/proc.c
    @@ -916,6 +915,9 @@ runtime·entersyscall(void)
     {
     	uint32 v;
     
    +\tif(m->profilehz > 0)\n+\t\truntime·setprof(false);\n+\n     // Leave SP around for gc and traceback.
     	runtime·gosave(&g->sched);
     g->gcsp = g->sched.sp;
    @@ -979,6 +981,9 @@ runtime·exitsyscall(void)
     		// Garbage collector isn't running (since we are),
     		// so okay to clear gcstack.
     		g->gcstack = nil;
    +\n+\t\tif(m->profilehz > 0)\n+\t\t\truntime·setprof(true);\
     		return;
     	}
    

これらの変更は、macOSでのプロファイリングの信頼性を向上させるためのGoランタイムの低レベルな調整と、pprofツールが外部依存なしで機能するためのGoツールチェーンの拡張を示しています。

コアとなるコードの解説

ここでは、上記の「コアとなるコードの変更箇所」で示したコードスニペットについて、その役割と重要性を詳しく解説します。

1. misc/pprofにおけるフォールバックロジック

    if (system("$objdump --help >/dev/null 2>&1") != 0) {
      # objdump must not exist.  Fall back to go tool objdump.
      $objdump = "go tool objdump";
      $cmd = "$objdump $prog 0x$start_addr 0x$end_addr";
    }

このPerlコードスニペットは、pprofスクリプトがobjdumpコマンドを呼び出す前に実行されるチェックです。

  • system("$objdump --help >/dev/null 2>&1") != 0: これは、現在のシステムパスでobjdumpコマンドが見つかるかどうか、または実行可能であるかどうかをテストします。
    • $objdump --help: objdumpコマンドに--helpオプションを付けて実行します。これは通常、コマンドが存在し、実行可能であれば何らかのヘルプメッセージを出力します。
    • >/dev/null 2>&1: 標準出力と標準エラー出力を/dev/nullにリダイレクトします。これにより、ヘルプメッセージがコンソールに表示されるのを防ぎます。
    • system(...) != 0: system関数は、実行したコマンドの終了ステータスを返します。終了ステータスが0であれば成功(コマンドが見つかり、実行できた)、0以外であれば失敗(コマンドが見つからない、実行できないなど)を意味します。
  • # objdump must not exist. Fall back to go tool objdump.: コマンドが見つからなかった場合のコメントです。
  • $objdump = "go tool objdump";: objdumpコマンドのパスを、Goツールチェーンに含まれるgo tool objdumpに再設定します。
  • $cmd = "$objdump $prog 0x$start_addr 0x$end_addr";: 実際に実行するコマンド文字列を、新しいobjdumpパスとGoツールに合わせた引数形式で再構築します。

同様のロジックがaddr2lineに対しても適用されています。このフォールバックメカニズムにより、macOSユーザーはGNU Binutilsを別途インストールすることなく、Goのプロファイリング機能を利用できるようになり、ユーザーエクスペリエンスが大幅に向上しました。

2. src/cmd/addr2line/main.csrc/cmd/objdump/main.cの新規追加

これらのファイルは、Goツールチェーンに組み込まれる軽量なaddr2lineobjdumpの実装です。C言語で書かれており、Goの内部ライブラリであるlibmachを利用しています。

  • libmachの利用: libmachは、Goの実行可能ファイルやオブジェクトファイルの構造を解析し、シンボルテーブルやセクション情報にアクセスするためのライブラリです。
    • crackhdr(fd, &fhdr): ファイルディスクリプタfdから実行可能ファイルのヘッダ情報を解析し、fhdr構造体に格納します。
    • syminit(fd, &fhdr): シンボルテーブルを初期化し、シンボル情報をメモリにロードします。
    • findsym(pc, CTEXT, &s): 指定されたプログラムカウンタpcに対応する関数シンボルsを検索します。
    • fileline(file, sizeof file, pc): 指定されたプログラムカウンタpcに対応するソースファイル名と行番号を取得します。
    • machdata->das(text, pc, 0, buf, sizeof buf): 指定されたテキストセクションtext内のプログラムカウンタpcから命令を逆アセンブルし、結果をbufに格納します。
    • machdata->instsize(text, pc): 指定されたプログラムカウンタpcにある命令のサイズを返します。

これらのツールは、GNUの同名ツールに比べて機能は限定的ですが、pprofがプロファイルデータをシンボル解決し、逆アセンブル表示を行うために必要な最小限の機能を提供します。これにより、Goツールチェーンの自己完結性が高まり、macOS環境での外部依存が解消されました。

3. runtime·setprof(bool on)関数の導入と利用

// src/pkg/runtime/runtime.h
void	runtime·setprof(bool);

// src/pkg/runtime/thread_darwin.c
void
runtime·setprof(bool on)
{
	if(on)
		runtime·sigprocmask(SIG_UNBLOCK, &sigset_prof, nil);
	else
		runtime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);
}
  • runtime·setprofの役割: この関数は、現在のスレッドがSIGPROFシグナルを受信できるかどうかを制御します。ontrueの場合、SIGPROFはシグナルマスクから解除され、スレッドはシグナルを受信できるようになります。onfalseの場合、SIGPROFはシグナルマスクに追加され、スレッドはシグナルをブロックします。
  • sigset_prof: SIGPROFシグナルのみを含むシグナルセットです。
  • runtime·sigprocmask: Unix系システムコールsigprocmaskのGoランタイムラッパーです。スレッドのシグナルマスクを変更するために使用されます。SIG_UNBLOCKは指定されたシグナルをマスクから削除し、SIG_BLOCKは追加します。

この関数は、Goランタイムの様々な場所で呼び出されます。

  • src/pkg/runtime/lock_futex.csrc/pkg/runtime/lock_sema.c:

    • runtime·notesleep (セマフォ待機) や runtime·notetsleep (タイムアウト付きセマフォ待機) のような、スレッドが長時間ブロッキング状態に入る可能性のある関数内で、ブロッキングに入る直前にruntime·setprof(false)を呼び出し、ブロッキングから抜けた直後にruntime·setprof(true)を呼び出します。
    • これにより、スレッドがスリープしている間はSIGPROFがブロックされ、カーネルがスリープ中のスレッドにシグナルを誤って配信するのを防ぎます。
  • src/pkg/runtime/proc.c:

    • runtime·entersyscall (システムコールに入る前) と runtime·exitsyscall (システムコールから出る後) でruntime·setprofが呼び出されます。
    • システムコール中はスレッドがカーネルモードで実行されるため、この間もSIGPROFをブロックすることで、プロファイリングの精度を維持しようとします。

このシグナルマスクの動的な操作は、macOSカーネルのバグに対する主要な回避策であり、GoのCPUプロファイリングがより正確な結果を生成できるようにするための重要な変更です。

4. src/pkg/runtime/signal_darwin_386.cおよびsrc/pkg/runtime/signal_darwin_amd64.cSIGPROFハンドラ

    if(sig == SIGPROF) {
    	if(gp != m->g0 && gp != m->gsignal)
    		runtime·sigprof((uint8*)r->eip, (uint8*)r->esp, nil, gp);
    	return;
    }

このコードは、SIGPROFシグナルがGoランタイムのシグナルハンドラに到達した際に実行されます。

  • if(gp != m->g0 && gp != m->gsignal): この条件は非常に重要です。gpは現在のゴルーチン(Goの軽量スレッド)へのポインタです。
    • m->g0: Goランタイムのスケジューラやガベージコレクタなどの内部処理を実行するための特別なゴルーチンです。
    • m->gsignal: シグナルハンドラが実行される際に使用される特別なゴルーチンです。
  • この条件は、「現在のゴルーチンが、Goランタイムの内部処理やシグナルハンドリングのための特別なゴルーチンではない場合のみ、プロファイリング情報を記録する」ことを意味します。
  • 目的: Goランタイムの内部ゴルーチンがSIGPROFを受け取った場合、それはアプリケーションコードの実行とは直接関係のないCPU時間を示している可能性があります。これらのシグナルを無視することで、プロファイリングデータからノイズを取り除き、ユーザーのアプリケーションコードのパフォーマンスに焦点を当てた、よりクリーンで関連性の高いプロファイル情報を得ることができます。

これらのコアとなるコードの変更は、GoがmacOS環境でのプロファイリングの課題にどのように対処し、Goツールチェーンの独立性と堅牢性を高めたかを示しています。

関連リンク

参考にした情報源リンク

これらの情報源は、コミットの背景、技術的詳細、および関連する概念を理解するために参照されました。