[インデックス 15149] ファイルの概要
このコミットは、Go言語のランタイムにおけるデータ競合検出器(Race Detector)の内部実装に関する重要な変更です。具体的には、データ競合のコンテキストを追跡するために、従来の「goroutine ID」を使用する方式から「明示的なレースコンテキスト(explicit race context)」を使用する方式へと切り替えています。これにより、同時に存在するgoroutineの最大数に関する制限が撤廃され、パフォーマンスもわずかに向上しています。
コミット
- コミットハッシュ:
0a40cd2661a14baa9a57b4f5af84494455d83f88
- Author: Dmitriy Vyukov dvyukov@google.com
- Date: Wed Feb 6 11:40:54 2013 +0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0a40cd2661a14baa9a57b4f5af84494455d83f88
元コミット内容
runtime/race: switch to explicit race context instead of goroutine id's
Removes limit on maximum number of goroutines ever existed.
code.google.com/p/goexecutor tests now pass successfully.
Also slightly improves performance.
Before: $ time ./flate.test -test.short
real 0m9.314s
After: $ time ./flate.test -test.short
real 0m8.958s
Fixes #4286.
The runtime is built from llvm rev 174312.
R=rsc
CC=golang-dev
https://golang.org/cl/7218044
変更の背景
Go言語のデータ競合検出器(Race Detector)は、並行処理におけるデータ競合バグを特定するための強力なツールです。しかし、このツールには以前、同時に存在するgoroutineの数に制限がありました。具体的には、GoのRace Detectorは、同時にアクティブなgoroutineの数が8192を超えるとプログラムを終了させるという技術的な制限を抱えていました。この制限は、Race Detectorが内部的にgoroutine IDを使用してメモリアクセスを追跡していたことに起因します。多数のgoroutineが生成・終了を繰り返すようなアプリケーションでは、この制限に達し、Race Detectorが有効な状態でテストを実行できないという問題が発生していました。
このコミットは、この「goroutine IDの最大数制限」を解消することを目的としています。コミットメッセージにある Fixes #4286
は、この問題がGoのIssueトラッカーで追跡されていたことを示しています。また、code.google.com/p/goexecutor tests now pass successfully.
という記述から、特定の並行処理テストスイートがこの制限によって失敗していたことが伺えます。
さらに、この変更はパフォーマンスのわずかな改善ももたらしています。コミットメッセージに示されている flate.test
の実行時間の比較は、このパフォーマンス向上が実測値として確認されたことを示しています。
前提知識の解説
Go言語のGoroutine
GoroutineはGo言語における軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量であり、数千、数万といった多数のgoroutineを同時に実行することが可能です。Goランタイムがgoroutineのスケジューリングを管理し、複数のgoroutineを少数のOSスレッドに多重化して実行します。
データ競合 (Data Race)
データ競合とは、複数のgoroutineが同時に同じメモリ領域にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生するバグです。データ競合はプログラムの予測不能な動作やクラッシュを引き起こす可能性があり、デバッグが非常に困難な種類のバグです。
Go Race Detector (データ競合検出器)
Go Race Detectorは、Goプログラムの実行中にデータ競合を検出するためのツールです。プログラムを特殊な方法でコンパイルし(-race
フラグを使用)、実行時にメモリアクセスを監視することで、データ競合のパターンを特定します。Race Detectorは、Googleが開発したThreadSanitizer (TSan) という技術をベースにしています。
ThreadSanitizer (TSan)
ThreadSanitizerは、C/C++などの言語で書かれたプログラムのデータ競合を検出するためのランタイムツールです。GoのRace DetectorもこのTSanのランタイムライブラリを内部的に利用しています。TSanは、各スレッド(Goの場合はgoroutine)のメモリアクセス履歴を追跡し、競合の可能性を検出します。
goid
(Goroutine ID)
Goランタイムは、各goroutineに一意のID(goid
)を割り当てます。これは主にデバッグや内部的な追跡のために使用されます。従来のRace Detectorの実装では、このgoid
をデータ競合検出のコンテキストとして利用していました。
racectx
(Race Context)
このコミットで導入された「明示的なレースコンテキスト」は、racectx
という形で表現されます。これは、従来のgoid
に代わる、Race Detectorが内部的に使用するコンテキスト識別子です。goid
が単なる一意の数値IDであるのに対し、racectx
はRace Detectorがデータ競合検出に必要なより抽象的で柔軟なコンテキスト情報を持つポインタまたは数値であると推測されます。これにより、Race DetectorはgoroutineのライフサイクルやIDの再利用といった問題に縛られずに、より効率的かつスケーラブルに競合検出を行えるようになります。
技術的詳細
この変更の核心は、GoランタイムとThreadSanitizerライブラリ間のインターフェースの変更です。
-
goid
からracectx
への移行:- 以前は、Race Detectorの各関数(
Read
,Write
,FuncEnter
,GoStart
など)は、引数としてint32 goid
を受け取っていました。これは、どのgoroutineがメモリにアクセスしたか、あるいは関数に入ったか/出たかをTSanに伝えるためでした。 - このコミットでは、これらの関数のシグネチャが変更され、
int32 goid
の代わりにuintptr racectx
を受け取るようになりました。uintptr
はポインタを保持できる整数型であり、これによりTSanはより複雑なコンテキスト情報をポインタとして受け渡すことができるようになります。 g->goid
(現在のgoroutineのID)が直接TSanに渡されるのではなく、g->racectx
という新しいフィールドが導入され、これがTSanに渡されるようになりました。
- 以前は、Race Detectorの各関数(
-
Goroutine IDの生成方法の変更:
- 以前は、新しいgoroutineが作成される際に
runtime·xadd64((uint64*)&runtime·sched.goidgen, 1)
というアトミックな操作でgoid
を生成していました。これは、goidgen
というグローバルなカウンタをインクリメントすることで、一意のgoid
を割り当てていました。この方式では、生成されたgoroutineの総数が増え続けると、goid
の範囲が広がり、TSanが内部的に管理するデータ構造に負荷がかかる可能性がありました。 - 新しい実装では、
newg->goid = ++runtime·sched.goidgen;
となり、goid
の生成は単純なインクリメントになりました。しかし、重要なのは、このgoid
が直接Race Detectorに渡されなくなったことです。代わりに、runtime·racegostart
関数が新しいracectx
を生成し、それをnewg->racectx
に設定するようになりました。これにより、Race Detectorはgoid
の数値的な制限から解放されます。
- 以前は、新しいgoroutineが作成される際に
-
Race Detectorの初期化とGoroutineの開始/終了:
runtime·raceinit()
関数は、Race Detectorの初期化を担当します。以前はvoid
を返していましたが、変更後はuintptr
を返すようになり、初期化時にメインgoroutineのracectx
を返すようになりました。runtime·racegostart
関数は、新しいgoroutineが開始される際に呼び出されます。以前は親goroutineのIDと子goroutineのIDを受け取っていましたが、変更後は親のracectx
と子のracectx
へのポインタを受け取るようになりました。これにより、TSanはgoroutineの親子関係をracectx
ベースで追跡できます。runtime·racegoend
関数は、goroutineが終了する際に呼び出されます。以前は終了するgoroutineのIDを受け取っていましたが、変更後は現在のgoroutineのracectx
を使用するようになりました。
-
パフォーマンスの向上:
goid
の代わりにracectx
を使用することで、TSanの内部データ構造の管理がより効率的になった可能性があります。例えば、goid
が連続的でない場合や、非常に大きな値になる場合に発生する可能性のあるハッシュテーブルの衝突やメモリ割り当てのオーバーヘッドが削減されたのかもしれません。- また、
goid
の生成におけるアトミック操作の削減(runtime·xadd64
の削除)も、わずかながらパフォーマンス向上に寄与している可能性があります。
コアとなるコードの変更箇所
主に以下のファイルが変更されています。
src/pkg/runtime/proc.c
: Goランタイムのスケジューリングとgoroutine管理に関するCコード。g->racectx
フィールドの導入と、Race Detector関連関数の呼び出し箇所の変更。src/pkg/runtime/race.c
: GoランタイムとThreadSanitizerライブラリ間のCインターフェース。Race Detector関数のシグネチャ変更(goid
からracectx
へ)。src/pkg/runtime/race.h
: Race Detector関連関数のCヘッダー。シグネチャの変更を反映。src/pkg/runtime/race/race.go
: Go言語からThreadSanitizerライブラリのC関数を呼び出すためのGo側のラッパー。C関数のシグネチャ変更に合わせてGo側の関数シグネチャも変更。src/pkg/runtime/runtime.h
:G
構造体にracectx
フィールドを追加。src/pkg/runtime/race0.c
: Race Detectorが無効な場合のダミー実装。シグネチャ変更を反映。src/pkg/runtime/race/race_darwin_amd64.syso
,src/pkg/runtime/race/race_linux_amd64.syso
,src/pkg/runtime/race/race_windows_amd64.syso
: ThreadSanitizerのコンパイル済みライブラリファイル。内部実装の変更に伴いバイナリが更新されています。
src/pkg/runtime/proc.c
の変更例
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -221,7 +221,7 @@ runtime·schedinit(void)
m->nomemprof--;
if(raceenabled)
- runtime·raceinit();
+ g->racectx = runtime·raceinit();
}
extern void main·init(void);
@@ -1340,9 +1340,8 @@ runtime·newproc1(byte *fn, byte *argp, int32 narg, int32 nret, void *callerpc)
if(siz > StackMin - 1024)
runtime·throw("runtime.newproc: function arguments too large for new goroutine");
- goid = runtime·xadd64((uint64*)&runtime·sched.goidgen, 1);
if(raceenabled)
- runtime·racegostart(goid, callerpc);
+ racectx = runtime·racegostart(callerpc);
schedlock();
@@ -1374,9 +1373,11 @@ runtime·newproc1(byte *fn, byte *argp, int32 narg, int32 nret, void *callerpc)
newg->sched.g = newg;
newg->entry = fn;
newg->gopc = (uintptr)callerpc;
+ if(raceenabled)
+ newg->racectx = racectx;
runtime·sched.gcount++;
- newg->goid = goid;
+ newg->goid = ++runtime·sched.goidgen;
newprocreadylocked(newg);
schedunlock();
src/pkg/runtime/race.c
の変更例
--- a/src/pkg/runtime/race.c
+++ b/src/pkg/runtime/race.c
@@ -10,36 +10,36 @@
#include "malloc.h"
#include "race.h"
-void runtime∕race·Initialize(void);
+void runtime∕race·Initialize(uintptr *racectx);
void runtime∕race·MapShadow(void *addr, uintptr size);
void runtime∕race·Finalize(void);
-void runtime∕race·FinalizerGoroutine(int32);
-void runtime∕race·Read(int32 goid, void *addr, void *pc);
-void runtime∕race·Write(int32 goid, void *addr, void *pc);
-void runtime∕race·ReadRange(int32 goid, void *addr, uintptr sz, uintptr step, void *pc);
-void runtime∕race·WriteRange(int32 goid, void *addr, uintptr sz, uintptr step, void *pc);
-void runtime∕race·FuncEnter(int32 goid, void *pc);
-void runtime∕race·FuncExit(int32 goid);
-void runtime∕race·Malloc(int32 goid, void *p, uintptr sz, void *pc);
+void runtime∕race·FinalizerGoroutine(uintptr racectx);
+void runtime∕race·Read(uintptr racectx, void *addr, void *pc);
+void runtime∕race·Write(uintptr racectx, void *addr, void *pc);
+void runtime∕race·ReadRange(uintptr racectx, void *addr, uintptr sz, uintptr step, void *pc);
+void runtime∕race·WriteRange(uintptr racectx, void *addr, uintptr sz, uintptr step, void *pc);
+void runtime∕race·FuncEnter(uintptr racectx, void *pc);
+void runtime∕race·FuncExit(uintptr racectx);
+void runtime∕race·Malloc(uintptr racectx, void *p, uintptr sz, void *pc);
void runtime∕race·Free(void *p);
-void runtime∕race·GoStart(int32 pgoid, int32 chgoid, void *pc);
-void runtime∕race·GoEnd(int32 goid);
-void runtime∕race·Acquire(int32 goid, void *addr);
-void runtime∕race·Release(int32 goid, void *addr);
-void runtime∕race·ReleaseMerge(int32 goid, void *addr);
+void runtime∕race·GoStart(uintptr racectx, uintptr *chracectx, void *pc);
+void runtime∕race·GoEnd(uintptr racectx);
+void runtime∕race·Acquire(uintptr racectx, void *addr);
+void runtime∕race·Release(uintptr racectx, void *addr);
+void runtime∕race·ReleaseMerge(uintptr racectx, void *addr);
extern byte noptrdata[];
extern byte enoptrbss[];
static bool onstack(uintptr argp);
-void
+uintptr
runtime·raceinit(void)
{
- uintptr sz;
+ uintptr sz, racectx;
m->racecall = true;
- runtime∕race·Initialize();
+ runtime∕race·Initialize(&racectx);
sz = (byte*)&runtime·mheap - noptrdata;
if(sz)
runtime∕race·MapShadow(noptrdata, sz);
@@ -47,6 +47,7 @@ runtime·raceinit(void)
if(sz)
runtime∕race·MapShadow(&runtime·mheap+1, sz);
m->racecall = false;
+ return racectx;
}
void
@@ -73,7 +74,7 @@ runtime·racewrite(uintptr addr)
{
if(!onstack(addr)) {
m->racecall = true;
- runtime∕race·Write(g->goid-1, (void*)addr, runtime·getcallerpc(&addr));
+ runtime∕race·Write(g->racectx, (void*)addr, runtime·getcallerpc(&addr));
m->racecall = false;
}
}
@@ -86,7 +87,7 @@ runtime·raceread(uintptr addr)
{
if(!onstack(addr)) {
m->racecall = true;
- runtime∕race·Read(g->goid-1, (void*)addr, runtime·getcallerpc(&addr));
+ runtime∕race·Read(g->racectx, (void*)addr, runtime·getcallerpc(&addr));
m->racecall = false;
}
}
@@ -105,7 +106,7 @@ runtime·racefuncenter(uintptr pc)
runtime·callers(2, &pc, 1);
m->racecall = true;
- runtime∕race·FuncEnter(g->goid-1, (void*)pc);
+ runtime∕race·FuncEnter(g->racectx, (void*)pc);
m->racecall = false;
}
@@ -115,7 +116,7 @@ void
runtime·racefuncexit(void)
{
m->racecall = true;
- runtime∕race·FuncExit(g->goid-1);
+ runtime∕race·FuncExit(g->racectx);
m->racecall = false;
}
@@ -126,7 +127,7 @@ runtime·racemalloc(void *p, uintptr sz, void *pc)
if(m->curg == nil)
return;
m->racecall = true;
- runtime∕race·Malloc(m->curg->goid-1, p, sz, pc);
+ runtime∕race·Malloc(m->curg->racectx, p, sz, pc);
m->racecall = false;
}
@@ -138,42 +139,45 @@ runtime·racefree(void *p)
m->racecall = false;
}
-void
-runtime·racegostart(int32 goid, void *pc)
+uintptr
+runtime·racegostart(void *pc)
{
+ uintptr racectx;
+
m->racecall = true;
- runtime∕race·GoStart(g->goid-1, goid-1, pc);
+ runtime∕race·GoStart(g->racectx, &racectx, pc);
m->racecall = false;
+ return racectx;
}
void
-runtime·racegoend(int32 goid)
+runtime·racegoend(void)
{
m->racecall = true;
- runtime∕race·GoEnd(goid-1);
+ runtime∕race·GoEnd(g->racectx);
m->racecall = false;
}
static void
memoryaccess(void *addr, uintptr callpc, uintptr pc, bool write)
{
- int64 goid;
+ uintptr racectx;
if(!onstack((uintptr)addr)) {
m->racecall = true;
- goid = g->goid-1;
+ racectx = g->racectx;
if(callpc) {
if(callpc == (uintptr)runtime·lessstack ||
(callpc >= (uintptr)runtime·mheap.arena_start && callpc < (uintptr)runtime·mheap.arena_used))
runtime·callers(3, &callpc, 1);
- runtime∕race·FuncEnter(goid, (void*)callpc);
+ runtime∕race·FuncEnter(racectx, (void*)callpc);
}
if(write)
- runtime∕race·Write(goid, addr, (void*)pc);
+ runtime∕race·Write(racectx, addr, (void*)pc);
else
- runtime∕race·Read(goid, addr, (void*)pc);
+ runtime∕race·Read(racectx, addr, (void*)pc);
if(callpc)
- runtime∕race·FuncExit(goid);
+ runtime∕race·FuncExit(racectx);
m->racecall = false;
}
}
@@ -183,23 +187,23 @@ runtime·racereadpc(void *addr, void *callpc, void *pc)
static void
rangeaccess(void *addr, uintptr size, uintptr step, uintptr callpc, uintptr pc, bool write)
{
- int64 goid;
+ uintptr racectx;
if(!onstack((uintptr)addr)) {
m->racecall = true;
- goid = g->goid-1;
+ racectx = g->racectx;
if(callpc) {
if(callpc == (uintptr)runtime·lessstack ||
(callpc >= (uintptr)runtime·mheap.arena_start && callpc < (uintptr)runtime·mheap.arena_used))
runtime·callers(3, &callpc, 1);
- runtime∕race·FuncEnter(goid, (void*)callpc);
+ runtime∕race·FuncEnter(racectx, (void*)callpc);
}
if(write)
- runtime∕race·WriteRange(goid, addr, size, step, (void*)pc);
+ runtime∕race·WriteRange(racectx, addr, size, step, (void*)pc);
else
- runtime∕race·ReadRange(goid, addr, size, step, (void*)pc);
+ runtime∕race·ReadRange(racectx, addr, size, step, (void*)pc);
if(callpc)
- runtime∕race·FuncExit(goid);
+ runtime∕race·FuncExit(racectx);
m->racecall = false;
}
}
@@ -218,7 +222,7 @@ runtime·raceacquireg(G *gp, void *addr)
if(g->raceignore)
return;
m->racecall = true;
- runtime∕race·Acquire(gp->goid-1, addr);
+ runtime∕race·Acquire(gp->racectx, addr);
m->racecall = false;
}
@@ -234,7 +238,7 @@ runtime·racereleaseg(G *gp, void *addr)
if(g->raceignore)
return;
m->racecall = true;
- runtime∕race·Release(gp->goid-1, addr);
+ runtime∕race·Release(gp->racectx, addr);
m->racecall = false;
}
@@ -250,7 +254,7 @@ runtime·racereleasemergeg(G *gp, void *addr)
if(g->raceignore)
return;
m->racecall = true;
- runtime∕race·ReleaseMerge(gp->goid-1, addr);
+ runtime∕race·ReleaseMerge(gp->racectx, addr);
m->racecall = false;
}
@@ -258,7 +262,7 @@ void
runtime·racefingo(void)
{
m->racecall = true;
- runtime∕race·FinalizerGoroutine(g->goid - 1);
+ runtime∕race·FinalizerGoroutine(g->racectx);
m->racecall = false;
}
コアとなるコードの解説
src/pkg/runtime/proc.c
runtime·schedinit()
: ランタイム初期化時に、メインgoroutineのracectx
をg->racectx
に設定するように変更されました。runtime·raceinit()
がuintptr
を返すようになったため、その戻り値をg->racectx
に代入しています。runtime·goexit()
: goroutineが終了する際にruntime·racegoend()
を呼び出すようになりました。以前はschedule()
関数内でgoid
を渡していましたが、racectx
ベースになったため、g->racectx
を暗黙的に使用するruntime·racegoend()
が直接呼び出される形になりました。schedule()
:Gmoribund
(終了状態のgoroutine)の処理から、runtime·racegoend(gp->goid)
の呼び出しが削除されました。これは、runtime·goexit()
で処理されるようになったためです。runtime·newproc1()
:int64 goid;
がuintptr racectx;
に変更されました。goid = runtime·xadd64((uint64*)&runtime·sched.goidgen, 1);
というアトミックなgoid
生成ロジックが削除されました。runtime·racegostart(goid, callerpc);
の呼び出しがracectx = runtime·racegostart(callerpc);
に変更され、新しいracectx
が返されるようになりました。newg->racectx = racectx;
という行が追加され、新しく作成されるgoroutineのG
構造体にracectx
が設定されるようになりました。newg->goid = goid;
がnewg->goid = ++runtime·sched.goidgen;
に変更されました。これは、goid
自体は引き続き生成されますが、Race Detectorのコンテキストとしては使用されなくなったことを意味します。goid
は主にデバッグ目的で残されていると考えられます。
src/pkg/runtime/race.c
runtime∕race·Initialize
、runtime∕race·FinalizerGoroutine
、runtime∕race·Read
、runtime∕race·Write
、runtime∕race·ReadRange
、runtime∕race·WriteRange
、runtime∕race·FuncEnter
、runtime∕race·FuncExit
、runtime∕race·Malloc
、runtime∕race·GoStart
、runtime∕race·GoEnd
、runtime∕race·Acquire
、runtime∕race·Release
、runtime∕race·ReleaseMerge
といった、ThreadSanitizerのC関数を呼び出すGoランタイム側のラッパー関数のシグネチャが、int32 goid
からuintptr racectx
へと一斉に変更されています。runtime·raceinit()
: 戻り値がvoid
からuintptr
に変更され、初期化されたracectx
を返すようになりました。runtime·racegostart()
: 戻り値がvoid
からuintptr
に変更され、新しく開始されるgoroutineのracectx
を返すようになりました。また、引数も親のracectx
と子のracectx
へのポインタを受け取るように変更されました。runtime·racegoend()
: 引数からint32 goid
が削除され、現在のgoroutineのracectx
を暗黙的に使用するようになりました。memoryaccess()
およびrangeaccess()
: これらのヘルパー関数も、内部でgoid
の代わりにracectx
を使用するように変更されました。
src/pkg/runtime/runtime.h
struct G
(goroutine構造体) にuintptr racectx;
フィールドが追加されました。これにより、各goroutineが自身のRace Detectorコンテキストを保持できるようになります。
src/pkg/runtime/race/race.go
- CGOの
import "C"
ブロック内のC関数宣言が、int goid
からvoid *racectx
へと変更されています。これは、Go側からCのThreadSanitizer関数を呼び出す際の型定義を更新したものです。void *
はGoのunsafe.Pointer
に対応し、uintptr
をポインタとして扱うことを可能にします。 - Go側のラッパー関数(
Initialize
,FinalizerGoroutine
,Read
,Write
など)のシグネチャも、C側の変更に合わせてgoid int32
からracectx uintptr
へと変更されています。
これらの変更により、Race DetectorはgoroutineのIDに依存することなく、より抽象的な「レースコンテキスト」に基づいてデータ競合を検出できるようになりました。これにより、goroutineの最大数制限が解消され、大規模な並行アプリケーションでもRace Detectorをより効果的に利用できるようになります。また、コンテキスト管理の効率化により、わずかながらパフォーマンスも向上しています。
関連リンク
- Go CL 7218044: https://golang.org/cl/7218044
- Go Issue #4286: https://github.com/golang/go/issues/4286 (このコミットで修正されたIssue)
参考にした情報源リンク
- Go Race Detector Documentation: https://go.dev/blog/race-detector
- ThreadSanitizer (TSan) Overview: https://github.com/google/sanitizers/wiki/ThreadSanitizerOnePager
- Go Race Detector Goroutine Limit (Web Search Result): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF9mGAbZku-aKLd_BzDaxT7hONeUs9bjNhQd6N3zold8x74bg1vxRiUJrewH32JBvdrt59QZWn78FflqY-nwJQn3HvOsR-rP8ou59L5VC1rm5zdhuuaNQkOLsddVev9O4EDmel6
- Go Race Detector Overhead (Web Search Result): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEqg5wbTKaVKh40J3Xe0IlluPVmHMnolwDEl8-aeDFLD-mjhcCFEvF5JG3YtFLcT7iUzeOTqjWE_GYY_L_9gAmlK0INNK4fBdRlGkLUzpUURgJmYOG3ISMf-nlT3yVNVIS9KxiD
- Stack Overflow - Go Race Detector Goroutine Limit: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGn2zskP5LkVtpnfQ6l6FwaWUSthBNnWCFGlRYZkr_iZv4I3bDl0AvQVXCbP2C5OhozHOGfG3eVi8rxq0zsKS9V2weXDHOYgzrjjAd0xaV5XvAjiEM1SmQX6SVBfZ6hyTApylmbhjcdE_BV9bYfXCPlz_KX6N3q09jBTa_GgyAB