センニジュウヨン

意味なんてない

Selenium の待機について誤りが蔓延している

最初、「Selenium の日本語記事はたいてい間違っている」……などと怒りに任せたタイトルにしようとしたが、大げさすぎるのでやめた。

正確には、Selenium WebDriver の Python 版の日本語での解説記事などで、待機処理に関して、ある誤りが蔓延している、ということ。

具体的には、明示的な待機の文脈でよく目にする

  • presence_of_all_elements_located
  • visibility_of_all_elements_located

の2つについて、使い方が根本的に間違っており、 正しい使い方をしたところで意図した結果にはならない。

Selenium WebDriver www.selenium.dev

間違い例

以下のように書かれたページが5個以上あった

# ページ上のすべての要素が読み込まれるまで待機
wait.until(EC.presence_of_all_elements_located)

visibility_... の方は presence_... よりは少なそうだが、こちらも複数見つけた。

# ページの全要素が見えるまで待機
wait.until(EC.visibility_of_all_elements_located)

自分もそれらのページを見て、「ふーん、そーなんだー」と、おまじないのように自分のプログラムに書いていた。

なにが間違いか

呼び方

API ドキュメントを見れば明らかだが、

selenium.webdriver.support.expected_conditions.presence_of_all_elements_located(locator)
selenium.webdriver.support.expected_conditions.visibility_of_all_elements_located(locator)

一番右側の方に書いてある通り、インターフェースとしては引数として locator が必要であり、

wait.until(EC.presence_of_all_elements_located(locator))
wait.until(EC.visibility_of_all_elements_located(locator))

というような呼び方が正しい。locator というのは、(By.ID, "ID") みたいなやつ。

誤りを流布しているサイトでも、同様のインターフェースである、all がつかないものは普通に正しい使い方で呼んでいるので、違和感持って良いところではある。

↓誤り流布サイトでの、他の正しそう*1な呼び出し (多少改変してます)

# CLASS名指定した要素が読み込まれるまで待機
wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'class_name')))
# ID指定したページ上の要素が読み込まれるまで待機
wait.until(EC.presence_of_element_located(By.ID, "ID"))

挙動の解釈

なんだ、正しく呼べばいいだけか、と思ったらそんなことはない。

正しく呼ぶとは何なのか?

ページの全要素を示すロケータってのを指定するのか?なにそれ?

→そもそもページ上の全要素なんてことはなかったです

API ドキュメントを読めば明らかだが、

presence_of_all_element_located についての記載

An expectation for checking that there is at least one element present on a web page. locator is used to find the element returns the list of WebElements once they are located

注目すべきは、"at least one element" の部分。全てとは……*2

その点、visibility_... の方は ……

visibility_of_all_elements_located についての記載

An expectation for checking that all elements are present on the DOM of a page and visible. Visibility means that the elements are not only displayed but also has a height and width that is greater than 0. locator - used to find the elements returns the list of WebElements once they are located and visible all elements

"all elements" と書いてあるから安心!

……とはおそらくならない。(ロケーターも指定して要素限定するし……)

presence_... の方も visibility_... の方も、wait の検査時点で DOM 内にあるものだけが対象で、そのなかでロケーターに合う要素だけを待つだろう。 そして、presence_... は一つでもロケーターに引っかかれば即 wait 終了。 visibility_... は検出したすべての要素が「可視」になるまでちゃんと待つが、wait を抜けた瞬間にロケーターが対象とする要素が「不可視」で DOM に追加されてるかもしれない。 *3

こういった挙動が、はじめに示した例にあるコメント……

  • ページ上のすべての要素が読み込まれるまで待機

  • ページの全要素が見えるまで待機

これら達成できるかといえば、まったくできなそうでしょ。

結局、ページの全要素の存在や「可視」を待てるわけではなさそうなので、 必要な要素を絞って、個別に wait させるしかないんでしょうね。

どう動いていたか

間違ってたのは理解した、でも今まで書いてたあれはどう動いてたのさ?

ソースを見れば明らかだが、結論から言うと、全く Wait していなかった。

presence_of_all_elements_located を含む現時点での最新ソース

↓一部だけ抜粋したもの

def presence_of_all_elements_located(locator):
    def _predicate(driver):
        return driver.find_elements(*locator)
    return _predicate

def visibility_of_all_elements_located(locator):
    def _predicate(driver):
        try:
            elements = driver.find_elements(*locator)
            for element in elements:
                if _element_if_visible(element, visibility=False):
                    return False
            return elements
        except InvalidSelectorException as e:
            raise e
        except StaleElementReferenceException:
            return False
    return _predicate

詳細はともかく、どちらも関数内で定義した _predicate() という関数を返しますね。

では、これを渡す先 である wait.until() の方のソースも見てみましょう。

until() を含む現時点での最新ソース

↓until() 部分のコードだけ抜粋したもの

def until(self, method, message=''):
    screen = None
    stacktrace = None

    end_time = time.time() + self._timeout
    while True:
        try:
            value = method(self._driver)
            if value:
                return value
        except InvalidSelectorException as e:
            raise e
        except self._ignored_exceptions as exc:
            screen = getattr(exc, 'screen', None)
            stacktrace = getattr(exc, 'stacktrace', None)
        time.sleep(self._poll)
        if time.time() > end_time:
            break
    raise TimeoutException(message, screen, stacktrace)

で、もともとの呼び出し方を思い出してみると、

wait.until(EC.presence_of_all_elements_located)

関数を返す関数である presence_... をそのまま until() に渡しています。 それが until の中でどう扱われているかというと、

value = method(self._driver)
if value:
    retu**rn value

method と名前を変えて、while ループ内で呼ばれています。 関数を返す関数ですから、value には _predicate() 関数が入ります。

そして直後の if value ……。

悪い予感しかしませんね。ここまでくれば予感でもなんでも無いですが。

一応 Python 公式ドキュメントに当たると、 **

オブジェクトは、デフォルトでは真と判定されます

とのことで、関数についての明確な言及はないけど、 関数もオブジェクトだし、特に偽ともされていないので、真なのでしょう。

念の為、REPL でも確認。

Python 3.10.1 (tags/v3.10.1:2cd268a, Dec  6 2021, 19:10:37) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> def hoge():return False
...
>>> bool(hoge)
True
>>>

やはり、関数は True のようですね。

……ということで、

until() に渡された、関数を返す関数は、until() 内で関数を返し、if で True と評価され、ループを一周も回ること無く関数を抜けるのであった。完

ちなみに、until()_predicate() を return しているので、使ってみると……

_predicate=wait.until(EC.presence_of_all_elements_located)
_predicate(driver)

怒られた

TypeError: selenium.webdriver.remote.webdriver.WebDriver.find_elements() argument after * must be an iterable, not WebDriver

driver をロケーターのつもりで展開しようとして、できないというもの。そうでしょうね。

過去には正しかったか

もしかして、昔はその表記で正しかったりしたのかな? と、10年ほど前のソースまでさかのぼってみたが、そんなことなかった。

*1:とおもったけど、下の方、タプル使わずに呼んでるけど良いのかな?

*2:名前の方を all じゃなくて、any にしろって issue 投げるべきだと思うが、色々めんどくさくて……。この記事書くのすらめんどいのに。

*3:この段落に関しては、ちょっと推測も入っている。一応 Selenium のソースも見た上で書いてるが、全部読んでるわけではないので、ページ全体の読み込みを待つマジックがある可能性も無くはない。でも今の時代、何を基準に読み込み終了を決められるだろうか。

大規模接種センターでワクチン接種を受けてきた(一回目)

7月某日、ワクチン接種を受けてきたのでその記録。

地元自治体から接種券が届いたが、自分は優先接種者でない。 となると、地元自治体ではしばらく接種を受けられないようだった。 自衛隊の大規模接種センターなら一般の接種を受け入れているみたいなので挑戦してみることにした。

まずは予約

さっそく予約してみようとしたが、あいにく枠は空いていなかった。 キャンセル待ちなら可能性があるようで、キャンセルが入るたびに随時予約ができるとのこと。 キャンセルは稀にしか発生しない上に、他の人との争奪戦が超高速で行われるなか、 色々頑張ってなんとか枠*1の確保に成功した。

接種当日

寄り道してから行く

予約していた日、早速会場に向かうが、その前にヨドバシに寄った。 家の中で体温計が行方不明だったので買っていく。 副反応で発熱があったときに分かるように買うことにした。 OMRON connect *2で管理できる体温計、超便利。

ヨドバシ.com - オムロン OMRON MC-6800B [電子体温計 音波通信 けんおんくん オムロンコネクト対応 約15秒予測検温 わき専用] 通販【全品無料配達】

その日のうちに欲しかったのでヨドバシで買ったが、¥2,700 だった。(ポイントで購入)

7月29日現在、Amazon なら ¥2,430 で買えるようなので、待てるならこっちのほうが良かった*3

バスで会場へ

会場へは東京駅から無料シャトルバスが出ているようなので、それを使ってみた。

f:id:KiKibits:20210729090018j:plain:h400
丸の内南口付近の看板
f:id:KiKibits:20210729091542j:plain
シャトルバスの列

謎の経路だったり信号に引っかかったりして、会場が近い割に到着は遅かった。 下手すれば歩きのほうが早いかもしれない。 でも暑さはだいぶ回避できるので使ったほうがマシか。 他のバス利用者を見ると、皆自分より年上、ほぼ高齢者といった感じだった。 バスから降りて少し歩くと会場に到着。

会場にて

はじめに一回目か二回目かなどによって通路が別れて会場に入っていく。

以下、会場内での流れ

1. 検温、持ち物検査

検温は表面温度を見るタイプで、即座に計測されて体温が印刷されたシールが渡される。 持ち物検査は鞄の口を開いて中を見せる程度。

2. 接種券、身分証、予診票確認

多分ここで予約の有無を確認されている。 時間がなくて予診票を書いていけなかったが、この段階で住所氏名などの部分は書かされた。 クリアファイルに必要なものを入れられ、冊子ももらって次の場所へ行くように指示された。 冊子は待機時間に読んでくれ、とのこと*4。 一回目接種者の目印となる首から下げるやつもここでもらったと思う。

3. 予診票記入&待機

予診票の未記入部分をここで記述。 クリアファイルの色ごとに分かれて待機。 使う階ごとに色分けされているみたい。 予約時に指定した会場AとかBとか*5と同じ別れ方かな? 1分も待たずに移動開始。5人くらいのまとまりで本会場へ。 ここまでは本開場前の仮設の建物。

4. 予診

本会場に入ったらエレベータで対象の階へ行く。 エレベータに乗る直前にもクリアファイルの色ごとに待機。 エレベータ内では壁側を向くように指示される。

f:id:KiKibits:20210729093248j:plain
壁側を向いてお立ちください
大部屋に入り椅子で少し待つ間に予診票に書いてあることを軽く確認される。 予診ブースへ移動して問診開始。「今日は二回目ですね?」「(おいおい)一回目です」からスタート、大丈夫か? 治療中の病気がないかとか、注射で気分が悪くなったことがないかとか聞かれた。 左肩でいいかの確認をして終了。

5. 注射

隣の大部屋へ移動し、椅子で少し待ってから注射ブースへ案内される。 三角筋への筋肉注射。皮下注射より少し痛い。 シール貼ってもらって、家に帰ったら取ってくださいとのこと。 風呂は入っていい、酒と激しい運動はだめ、などの注意事項を聞いて終了。

6. 接種済証発行 & 次回の予約

数少ない一回目だからなのか、全く並ばなかった。二回目っぽい人たちは並んでるみたいだった。

f:id:KiKibits:20210729113520j:plain
接種済証
次回はモデルナなので4週間以上間を空けてということになっている。 最速で予約可能な日に予約した。

7. 待機

接種後15分の待機を命じられる*6。 待機部屋では特殊詐欺防止の映像とか見せられる。 待機部屋を抜ける際に接種券の最終確認があるので、まだ仕舞わないほうが良い。

8. 撤退

案内に従ってエレベータを降り、建物外に。会場入ってから40分くらいで出てきた。

途中散歩したりしつつ帰宅。

気づいたことなど

会場内も年配の人が多めだったが、いくらか20~30代くらいの人も見かけた。 この日は多くの人が二回目らしく、一回目の接種は珍しいようだった。

時間はちょっとルーズに行っても問題なさそう。

予診票ぐらいは書いていこう。

行ってみて

大規模接種センターと言うだけあって、受けに来る人も対応する人も結構な数がいた。 人が大勢いるってだけで少し不安にはなったが、 対応してくれた人達に手練れ感があったのでそこは安心できた。 どこに行くにも何をするにも必ず案内の人がいて細かく指示されて、 自分は何も考えなくてもいいようになっている。 なんだか大量生産されるモノになったような不思議な気分を味わうことができた。 経験が少ない人達に打たれるよりクリティカルなミスは少なそうな気がするので、 まだ接種を受けていない人は検討してみてはどうだろうか。

副反応

発熱や倦怠感は無し。あったのは接種部位の軽い痛みと腫れ程度。

発熱と倦怠感は感染歴があると一回目から起きやすいらしい*7。 逆に言えば、無症状感染などはしていなかった可能性が高いのかな?

痛みは接種4~5時間程度後から意識し始めて、10時間後くらいにピークに。接種部位の三角筋だけしっかり筋肉痛になったような感じ。 その後、少し弱くなりつつも2日目も続く。3日目の終わりくらいにはかなり弱くなった。

腫れは1日目は意識しないとわからない程度だったが、2日目は少し大きくなった。3日目もあまり変わらず。 4日目からだいぶ目立たなくなった。

痛みも腫れも5日目には殆どない状態。

2種のワクチン、どっちがいいのかね

現在日本で使われているのはファイザーとモデルナの2種類。 自治体の集団接種やかかりつけ医の個別接種ではファイザー、大規模接種や職域接種ではモデルナが使われている。 共に mRNA ワクチンで、有効性(発症予防効果?)はほぼ同等とのこと。 また、使っている物質は違えど、化学的な組成はかなり似ているようで、添加物などの構成も近いらしい。 これらの情報だけだとどっちでも同じかなっていう気になってくる。

2回めの接種までに必要な間隔が、ファイザーは3週間、モデルナは4週間という違いはあるが、 効き方がちがうというよりは、そういう設定で治験を行ったからそうなっているだけ、という話をどこかで見た。

副反応で見ると、重篤なものを見るとどちらも稀なようなので、現時点での比較は自分にとってあまり意味はなさそう。 軽症なものについては、モデルナがやや多そう。また、モデルナアームという、モデルナ特有なものもあるみたい。 これらの軽いものについては、どうせ短期間で治るし、効き目の裏返しみたいなところもあるかな、ぐらいにいい加減に受け止めている(根拠は無い)。

モデルナのメリットとしては、長期保管時の温度がファイザーより高くてすむ(-20度、ファイザーは-60~-80度)ことや、 使用直前の保管温度帯(2~8度)での使用期限が長い(30日、ファイザーは5日)こと、希釈が必要ないことなどがある。 これらは打たれる側には直接関係ないが、取り扱いミスなどの事故が起きづらくなる可能性があるのではないか。

総合的に見て、最後のメリットを鑑みると、若干モデルナの方がいいかな、と個人的には思っている。

参考情報

youtu.be

youtu.be

mainichi-kotoba.jp

www.cov19-vaccine.mhlw.go.jp

www.mhlw.go.jp 健康観察日誌集計の中間報告(令和3年7月21日)

*1:翌日の枠!

*2:スマホのアプリ、オムロンの体重計とも連携させている

*3:ヨドバシも10%ポイント還元なら同じ値段に見えるが、ポイント使うときにはポイント還元無いので、結局Amazonが安い。Amazonならカードのポイントも貯まるし。

*4:途中はスムーズに行ったので、最後の待機時間以外に読むタイミングはなかった

*5:予約時は一択で、自分では選べなかった。年齢とか地域とかで分類されてるんだろうか

*6:二回目だと30分?

*7:参考情報にあげた「健康観察日誌集計の中間報告」に詳しい情報あり。ほかにも副反応について興味深い内容になっている

radiko の再生速度変更用 bookmarklet を作った

radiko でラジオを聞くことが多いのだが、再生速度を変えられないのがすごく不便。 アプリ版は手を出しづらいので、Web版に再生速度変更機能を追加する bookmarklet を作った。

bookmarklet を実行して radiko にボタンが追加された様子
実行結果

ついでに、指定秒ジャンプ機能もある。

コードは Gist に置いた。

radiko に再生速度変更ボタンと指定秒ジャンプボタンを追加する bookmarklet

ブックマーク作りやすいように現在のバージョンでリンクを作ってみた。
link
(terser で短縮後、はてなブログMarkdown解釈がバグっている(?)部分への対処で少し手を入れているので、そのバグが直されたらうまく動かないリンクになるかも)

PC版 Firefox & ChromeAndroidChrome にて動作確認済み。AndroidFirefox はあるバージョンから bookmarklet が起動できなくなってしまったらしいので動作未確認。

軽い解説的ななにか

短めに記述しつつ、読みやすさも残してみた。コード中にコメントがないので軽く書いておく。JavaScript は素人なのでおかしな書き方してるかもしれない。

処理の流れ

  1. 多重呼び出し防止
  2. ボタン類の追加
  3. ジャンプ用アロー関数定義
  4. ジャンプ用ボタンにイベントリスナを設定
  5. 再生速度更新用アロー関数定義
  6. 再生速度変更用ボタンにイベントリスナ設定
  7. 定期的に再生速度更新
  8. サーバー指定を置換

元々 radiko のページ中で jQuery を使っていたので、要素の指定などに利用している。

指定秒ジャンプは、radiko 側のコードとの相性などを考えると、シークバーの位置を動かした(てい)にするのが最も良さそう。

定期的に再生速度更新処理を呼び出すのは、停止・再生で audio オブジェクトが新しいものに切り替わっちゃうのに追従して速度設定するための手抜きコード。 audio オブジェクトは毎回関数で現在使われているものを引っ張ってくるようになっている。

接続先サーバの変更は、やらないと再生速度上げたときにとぎれとぎれになる。smartstream.ne.jp からは等速に近いスピードでしかダウンロードされないみたい。 このコードで指定しなくても、元々第2候補として radiko.jp は指定されている。なので、ブラウザ拡張の uMatrix 等で外部のサーバに繋がない設定になっていれば自然に radiko.jp につながる。

やってみて

いくらか改善点はあるが、だいたいやりたいことができるようになったのでほぼ満足。 radiko 側のコードを結構見たが、腐ってる部分も多そうなので、中の人はなんとかしたほうがいいと思う。

Fire HD 10 を買った

Amazonブラックフライデー&サイバーマンデーでセールになってたので、勢いで買った。

www.amazon.co.jp

2019年版(第9世代) の32GBストレージのもの、 通常価格 ¥15,980 のところ、セール価格で ¥10,980 で購入。 なんとなく Kindle Unlimited 3ヶ月分 付き(無料)にした。

後から調べると、一年前のサイバーマンデーでは ¥9,980 らしい。 少し損したような気分。

ちなみに、純正カバーは ¥4,780 もする。本体が1万円ちょっとなのにカバーに5千円弱払うのは馬鹿らしいので買わなかった。すごく重そうだし。 www.amazon.co.jp

感想など

先に感想を書いておくと、自分の使い方においては満足度はかなり高い(初タブレットが嬉しいというのも大きいかもしれないが)。 主な用途は漫画や動画の視聴で、ジップロックに入れて風呂に持っていくこともあるが、安いので雑に扱いやすいというのも良い。

ソフトウェアキーボードはカス。

WiFi(5GHz) には注意が必要

初期設定時に WiFi がつかめなかった。ぐぐってみると日本では、 5GHz 帯のうち、一部の周波数しか繋がらないようにされているらしい。 WiFi ルータの設定を変えて、WiFi が使えるようになった。

詳しくは、下の方の参考ページに。

Google Play

Google Play を入れる方法も調べたので、ここにメモしておく。(入れたとは言っていない。もしこれを見て入れようと思ってるなら、ググっていろいろ注意点を見たほうが良い。)

入れ方をぐぐってみると、APKMirror から4つの APK を落としてきて入れる事になっているようだ。 ただ、自分で調べた限り、4つのうち Google Account Manager 7.1.2 はおそらく不要。手元の実機 (Android 8 と 11)には Google Account Manager は入ってなかったし、APKMirror にある APK も、2017年に Android 6 以上向けの更新が最後のようなので、おそらく構成が変わって不要になったのだろう。

また、紹介されてる APK はちょっと古いので、新しい物を探してみた。 探すときは、Fire HD 10 (2019, 9th gen) の CPU *1 と OS *2 を考慮して、以下のように優先事項を決めた。

  • アーキテクチャとして、arm64-v8a があるものを優先。次点で armeabi-v7a。
  • 対応バージョンは Android 9 (APIレベル28) 以上を優先。
  • 対応 DPI は nodpi を優先。

これらを考慮した上で、新し目の以下の APK を選出した。

Name Architecture Minimum Version Screen DPI Date Uploaded Link
Google Services Framework noarch Android 9.0 nodpi November 29, 2019 at 11:55AM GMT+0900 Google Services Framework 9-6475783 (Android 9.0+) APK Download by Google LLC - APKMirror
Google Play services arm64-v8a + armeabi-v7a Android 9.0 nodpi November 27, 2020 at 12:58AM GMT+0900 Google Play services 20.42.65 (100400-342934990) (100400) APK Download by Google LLC - APKMirror
Google Play Store arm64-v8a + armeabi-v7a + x86 + x86_64 Android 5.0 nodpi December 4, 2020 at 8:44AM GMT+0900 Google Play Store 23.0.21-21 [0] [PR] 344908203 (nodpi) (Android 5.0+) APK Download by Google LLC - APKMirror
APK の検証

APKMirror をそのまま信用するのはどうかと思うので、APK を検証する方法を調べた。 APK には署名があるので、ツールを使えば偽造・改竄されていないか確認できる。

ググってみると、keytool や jarsigner という、Java 付属のツールを使っている記事が見つかるが、これは古いやり方。 APK の署名スキームには、現時点では v1 から v4 までのバージョンがあって、jarsigner などは JAR 由来の v1 でしか機能しない。しかし、v1 の署名を含まない APK もあるので、Java 付属のツールでは検証できない。

v2 以降の署名は apksigner で検証できる。(Android SDK Build Tools 24.0.3 以降で使用可能)

使用例

> apksigner verify --verbose --print-certs hoge.apk
Verifies
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v4 scheme (APK Signature Scheme v4): false
Verified for SourceStamp: false
Number of signers: 1
Signer #1 certificate DN: CN=Android, OU=Android, O=Google Inc., L=Mountain View, ST=California, C=US
Signer #1 certificate SHA-256 digest: f0fd6c5b410f25cb25c3b53346c8972fae30f8ee7411df910480ad6b2d60db83
Signer #1 certificate SHA-1 digest: 38918a453d07199354f8b19af05ec6562ced5788
Signer #1 certificate MD5 digest: cde9f6208d672b54b1dacc0b7029f5eb
Signer #1 key algorithm: RSA
Signer #1 key size (bits): 2048
Signer #1 public key SHA-256 digest: 2b06490d2d24305c6a90dbf74cc42f50183d207d572f8079e5d92fb2c2a0cda1
Signer #1 public key SHA-1 digest: b2da9ef7ec0f4474117fb0cba4dca3b795c0eab7
Signer #1 public key MD5 digest: a90ce510a96aa09bee6bf8d9da9b258b

WARNING が出ることもあるが、あまり気にしないことにしておく。

ダメな例(ファイルを一つ置き換えて v1 署名で検証してみたもの。sdk-version は v1 だけが機能するレベルでテキトーに古いやつ)

> apksigner verify --verbose --print-certs --min-sdk-version 20 --max-sdk-version 20 dame.apk
DOES NOT VERIFY
ERROR: SHA-1 digest of resources.arsc does not match the digest specified in META-INF/MANIFEST.MF. Expected: <sjU4IQ9shYi+BmKWlxwMR1vKvfY=>, actual: <V5w7S0MUAzmPnl6wolZlKSPRaCE=>

このようにして、署名時から改竄されていないか確認可能。

あとは、偽造ではなく、正しく Google が署名がしたものかどうかが確認できればいい。それには、上のコマンド出力内の ”certificate SHA-256 digest” 等を確認すればいい。これらの値が本物の Android から吸い出した APK と同じになるなら、本物と思って良い。

端末の認証

Google非認証端末やカスタムROMをインストールした端末で、Google Apps がブロックされる可能性があるらしい。 Google Services Framework (GSF) IDをGoogleに登録すれば回避できるっぽい。

更に心配ならログインするアカウントも専用のものを用意したほうが良いかも。

詳しくは下の参考ページで。

参考ページ

*1:4xARM Cortex-A73 (2.0 GHZ), 4xARM Cortex-A53 (2.0 GHz)。ARMv8-A 64-bit 命令セットを実装しているらしい

*2:Fire OS 7、Android 9 (API Level 28) ベース

C++においてメモリブロックのオーバーラップ判定は不可能なのか

先日 std::memcpy を使うコードを書いた。

void* memcpy( void* dest, const void* src, std::size_t count );

memcpy は src から dest へ count バイトだけコピーする簡単な関数だが、いくつか注意点があり、 その一つが、メモリの範囲がオーバーラップしていると動作が未定義という点である。

この点が気になったので assert でも書いておこうと思ったのだが*1、 ポインタ演算について調べてみると、どうもこれは(C++の規格の範囲内では)書けないんじゃないかと思い始めたりして、 色々迷走したのでそれについて調査過程を含めてここに記録しておく。

(規格文書を直接あたったわけでもない調査なので間違ったことを書いているかもしれません。ツッコミ歓迎。)

そのオーバーラップ判定大丈夫?

オーバーラップ判定コードは以下のようなものである。

// オーバーラップしていないかどうかチェック
assert(((src+count) <= dest) || (src >= dest+count));

自分で考えて判定を書くと、もっとグチャッとした処理を書きそうだったので、ググってどう書くべきか調べた。 その結果、 Linus のメールに書いてあるものがシンプルで良さそうだったのでこれを参考にした。 件のメールでは unsigned long にキャストしていて、念の為 unsigned long のオーバーフローにも配慮する場合も示してあるが、 オーバーフローのチェックとかほんとに要るのか疑問だったので、ポインタ同士の演算について調べてみることにした。

ポインタへの加算は大丈夫そう

ポインタへの加算については、cppreference の算術演算子のページ*2に記載があり、 配列の最後の要素+1までは保証されるけど、それを超えると未定義動作になると書いてある*3。 今回の事例は配列の末尾を超えることは無いものだったので、とりあえずこれは大丈夫だと分かった。

問題はポインタ同士の比較

ポインタ同士の比較については、cppreference の比較演算子のページに記載があり、 内容が多いのだが、関係しそうな部分だけ抜粋する。

2つのオブジェクトへのポインタ (の変換後) の比較の結果は、以下のように定義されます。

  1. 2つのポインタが同じ配列の異なる要素を指している場合、または同じ配列の異なる要素内の部分オブジェクトを指している場合、大きい添字を持つ要素を指しているポインタの方が他方より大きいとみなされます。 別の言い方をすると、ポインタの比較の結果は、それらが指している要素のインデックスを比較した結果と同じになります。
  2. 片方のポインタが配列の要素を指すか配列の要素の部分オブジェクトを指し、他方のポインタがその配列の最後の要素の次の要素を指す場合、後者のポインタの方が前者より大きいとみなされます。 単一のオブジェクトを指しているポインタは、サイズ1の配列を指しているとみなされます。 つまり &obj+1 は &obj より大きいとみなされます。 (C++17およびそれ以降)
  3. union でないクラス型のオブジェクト内で、2つのポインタが同じメンバアクセスを持つ異なる非静的データメンバを指している場合、またはそのようなメンバの部分オブジェクトまたは配列要素を指している場合、後に宣言されたメンバを指しているポインタの方が他方より大きいとみなされます。 別の言い方をすると、同じメンバアクセスモードを持つクラスメンバは、宣言された順番でメモリ内に配置されます。

(途中省略)

2つのポインタが、より大きいとも等しいとも規定されていない場合、比較の結果は未規定です。結果は非決定的であってもよく、プログラムの同じ実行における同じ被演算子を持つ同じ式の複数回の評価に対してであっても、一貫している必要はありません。

ここで、1 は同一配列内の要素での前後関係について、2 は配列内のものとその配列の最後の次の要素との前後関係について、3 はクラス内での前後関係について言及している。 重要なのは、同一ではない配列の比較について一切言及していないことである。 そして引用の最後の方にあるように、言及がないということは比較結果は未規定であることを意味する。

前述のオーバーラップチェックコードでは、同一配列内のポインタ同士(オーバーラップの可能性がある)では正しく動くだろうが、 別の配列同士(これは普通オーバーラップしない)では正しく動く保証がない。\(^o^)/オワタ

(非決定的でもよいという文言も気になる。はじめは、これがポインタ比較全般についての言及かと思ったが、引用先のページの作りからして未規定動作時のみへの言及だと思われる*4。 未規定動作時には既に正しく動く保証がないのでここは突っ込んで考える意味は余りなさそう。)

解決策?

なんとかならないか模索していたら、Stack Overflow のある回答が目についた。 要は、ポインタ比較がだめなら intptr_t で比較すればいいじゃない、という内容。 たしかに intptr_t 同士の比較では未規定とか未定義動作ということはありえないように思える。

前述のオーバーラップチェックコードの場合は特に signed である必要もないので、やるとしたら uintptr_t を使うだろうか。 ただ気になるのは、reinterpret_cast<uintptr_t>(...) という形でキャストが必要になるのが何となく不安になる*5のと、 uintptr_t での比較がポインタでの比較で意図していたものと同じ意味になるという保証があるのかという点。

詳しくは規格を見るのが正しいんだろうけど、そんな気力もないのでまたもやググって cpprefjp の uintptr_t のページに以下のような記述を見つけた。

この型を実装するかどうかは処理系定義。

この型は、以下の動作が保証される:

  1. 有効なvoidへのポインタからuintptr_t型への変換
  2. uintptr_t型のポインタ値からvoidへのポインタへの逆変換
  3. 変換前と逆変換のポインタ値が等値となる

実装するかどうかは処理系定義とあるが、今回は定義してある環境しか相手にしないのでとりあえず置いておく。

そして気になるのが、保証される動作が極端に少ないこと。変換と逆変換が正しくできてポインタ値が同じになるとしか書いていない。これを見る限り、規格内で意味のある比較は無理筋に思える。

真の解決策

というわけで、「無理っぽいです」という記事を書こうとしたわけだが、改めてググってみたら正解らしきものを見つけた。(なぜあのとき見つからなかったのか…)

Stack Overflow のある回答に以下のように書いてある。

int overlap_p(void *a, void *b, size_t n)
{
    char *x = a, *y =  b;
    for (i=0; i<n; i++) if (a+i==b || b+i==a) return 1;
    return 0;
}

要は比較は比較でも等値比較を使えってことである。

上の方でポインタの比較について引用したが、省略した部分に等値比較についての以下のような記述がある。

2つのポインタ (の変換後) の等値比較の結果は、以下のように定義されます。

  1. ポインタがどちらも NULL ポインタの場合、それらは等しいとみなされます。
  2. ポインタが関数ポインタで、同じ関数を指している場合、それらは等しいみなされます。
  3. ポインタがオブジェクトポインタで、同じアドレスを表している場合、それらは等しいとみなされます (これには、同じ共用体の非静的メンバを指している2つのポインタや、標準レイアウト構造体を指しているポインタとその最初のメンバを指しているポインタや、 reinterpret_cast によって関連付けられているポインタなどが含まれます)。
  4. それ以外のすべてのポインタは等しくないとみなされます。

今回の場合は 3 に当てはまりそうなので未規定ではなくなる。 これにて解決!

(でもなんか冗長っぽく見えて釈然としない。標準ライブラリとかコンパイラの組み込み関数とかでサクッと判定できないものか)

*1:結局、当該コードはオーバーラップして使われることがないと判断し、実際には assert は書かなかった

*2:日本語版はアレな機械翻訳なので英語版へリンク

*3:配列有効範囲外を指すポインタ値は存在が許されない - yohhoyの日記も参照されたし

*4:ほんとのところは規格を見てみないとわからないけど

*5:reinterpret_cast ってなんとなく不安になりますよね?

Synology DS418play のベンチマーク

先日買った NAS、 DS418play のベンチマークを取ってみた。

購入記事はこちら 1024.hateblo.jp

ベンチマークのやり方

ベンチマークソフトはちょっと探してみたが、NAS用に良さげなものを見つけられなかったので 汎用的に使える CrystalDiskMark(以下、CDM) を使って計測してみた。 最新版の 6.0.0 x64 で、なんとなく UWP 版を入れてみた。

CDM で何度か適当に測ってみると、シーケンシャルアクセスはほぼ一定だが、ランダムアクセス性能が実行のたびに結構変わることがわかった。

ベンチマーク結果1ベンチマーク結果2
ランダムライトが特に異なる結果

また、テストサイズの大小でも結果が変わってくる。さらに、テストサイズを大きくしていると、時間とともに徐々に結果が良くなっていった。

CDM のデフォルトの設定だけでは安定した結果を得られないことが分かったため、ベンチマークのやり方を以下のように決めた。

  • テストサイズは最大の 32GiB と、最小の 50MiB の2種類を指定
  • テスト回数は最大の 9 を指定
  • 同一条件で 5 回(サイズが小さいときは2回)テストし、すべての値の最大値を求める

テスト回数については、9 回でもまだバラツキがあったので、それをさらに複数回やることにした。 テストサイズが小さいときはバラツキも少ないようなので2回だけにした。 また、平均値ではなく最大値にするのは、CDM の計測方法でそもそも最大値を使っているようなので、それに合わせた。 平均や標準偏差も求めたほうが良いのかもしれないが、今回は面倒だからやっていない。

ベンチマーク対象

今回のベンチマークの趣旨の一つとして、Samba 共有の設定をどうするかを決める、というのがあり、 共有の設定を 3 種類用意してそれぞれに対してベンチマークを行った。

  • デフォルト設定
    • f:id:KiKibits:20180606213013p:plain:w215f:id:KiKibits:20180606213020p:plain:w215
      DSM6.2 で共有を設定する際の規定の設定
  • 圧縮を有効
    • f:id:KiKibits:20180606213353p:plain:w215f:id:KiKibits:20180606213358p:plain:w215
      デフォルト設定に圧縮のために必要な設定だけを足したもの
  • 暗号化を有効
    • f:id:KiKibits:20180606213759p:plain:w215f:id:KiKibits:20180606213803p:plain:w215
      デフォルト設定に暗号化のために必要な設定だけを足したもの

また、ベースとなるファイルシステムは Btrfs で、HDD を 2台接続した SHR(Synology Hybrid Raid) なので RAID-1 相当である*1。 HDD は NAS と同時に購入した 7200rpm のもの。

f:id:KiKibits:20180606235528p:plain:w400

圧縮は恐らく Btrfs の透過圧縮のものだと思われるが、調べていないので確証はない。 暗号化も具体的にどうやっているか調べてないのでわからない。

なお、リンクアグリゲーションや SMB マルチチャンネルは一切使わずに先日購入したスイッチ経由で 1000BASE-T 接続している*2

1024.hateblo.jp

SMBのバージョン(dialect) は 3.1.1(最新のはず)。 PowerShell(管理者) で以下のように確認できる。

PS C:\WINDOWS\system32> Get-SmbConnection

ServerName ShareName UserName   Credential Dialect NumOpens
---------- --------- --------   ---------- ------- --------
NASX       IPC$      PCX\userxx PCX\userxx 3.1.1   1

NAS の管理画面(DSM) では、デフォルトでは 3系 が無効になっていたので設定する必要があったはず。

ベンチマーク結果

デフォルト設定の結果圧縮設定の結果暗号化設定の結果
32GiB 9x5回 ベンチマーク結果 (※複数の結果を合成した嘘画像)

デフォルト設定の結果圧縮設定の結果暗号化設定の結果
50MiB 9x2 回ベンチマーク結果 (※複数の結果を合成した嘘画像)

シーケンシャルアクセスの性能はどれも大差なく、測定ごとのブレも少ないので見るべきはランダムアクセス性能である。

50MiB の方は、"暗号化" でランダムライト性能が落ちているの以外はほぼ同じ結果で、4KiB Q1T1 以外は 1000BASE-T の帯域を使い切っていると言って良さそう。

32GiB の方は、"暗号化" が相変わらずランダムライトで性能が落ちているが、ここにきて "圧縮" が "デフォルト" を上回ってきた*3。また、上記嘘画像では分からないが、"圧縮" だけは計測ごとのブレが少なく、かなり安定してこれに近い性能が出ていた。その点、"デフォルト" はブレが大きく、これより性能が落ちることが頻繁にあった*4

というわけで、今後は圧縮をメインに使うことにした。

ReadyNAS Ultra 4 でもやってみる

ついでに、同一のベンチマーク手法で旧 NASベンチマークを取ってみた。

こちらは HDD(5400rpmクラス) 4台構成の ext4 X-RAID2 であり、RAID-5 相当なので注意が必要。 f:id:KiKibits:20180606233730p:plain:w700 ネットワーク構成等は基本的に同じだが、SMB の dialect は 1.5 である。共有の設定に暗号化や圧縮はなかったと思う。

結果は以下

旧NASの結果
32GiB 9x5回ベンチマーク結果 (※複数の結果を合成した嘘画像)
旧NASの結果
50MiB 9x2 回ベンチマーク結果 (※複数の結果を合成した嘘画像)

シーケンシャル性能は 旧→新 でだいぶ上がったが、ランダム性能は及んでいない部分もあるということが分かった。 これが Btrfs のせいなのか、RAID-1 と RAID-5 の違いのせいなのかはわからないが、 旧NAS からのデータ移行が終わったら新NAS の方へ HDD を移植するつもりなので、 気が向いたらまた計測するかも。

*1:RAID-1 だと、1台の場合とさほど変わらないか遅くなると思うが、読み込みは速くなる場合もあるらしい

*2:そもそもこのスイッチではリンクアグリゲーション使えない気がするし、使えても単一接続では効果ない気がする

*3:ランダムリードは若干落ちているが…

*4:"暗号化" のブレは中間くらいだが、そもそも性能が微妙

Synology の NAS DS418play を購入

今まで使っていた NAS が手狭になったわけではないが、勢い余って新しい NAS を購入。 旧 NAS のほうで、時々ファイルのコピーに失敗する現象が出ていた *1 のも一因(再試行すれば問題ない)。

買ったのは Synology DS418playAmazon にて ¥49,130(税込) で購入。

NAS は NETGEAR 製のものだが、それ以外のメーカーのものに手を出してみようと思い、 軽く調べた感じでは QNAP, Synology という台湾 の2メーカーが定番らしかった。

また、ファイルシステムとして Btrfs というのがあるらしく、せっかくなので対応したものを選ぼうとしたら、 QNAP の方が Btrfs をディスっていて若干引いた。 Synology は Btrfs に肯定的。NETGEAR は Btrfs を擁護というか、むしろ QNAP をディスっている↓

「Btrfsは低速」「パフォーマンスがすべて」と言い、従来のファイルシステムEXT4にしがみついているNASメーカーがあります。

ということで、平和な Synology 製にすることにした。

Synology のNASでは Btrfs はエントリークラスの製品では使えないらしく、 若干高めのものから対応しているようだった*2

今使っている Ready NAS と同じ4ベイタイプで、Btrfs が使えるもので、以下の2機種に絞った。

  • DS918+
  • DS418play

上位機種の DS918+ の方は若干CPUが良いもの使っていて、NVMe キャッシュが使えるなど、 その他にも細かい違いがあったが、 キャッシュの効果に懐疑的なのと、ストレージ以外の用途に積極的に使う気がないので 下位機種の DS418play にしてみた*3。 (ちなみに執筆時点の Amazon で 税込み ¥6,669 の価格差)

また、これはNASキットで、HDD は付いていないので、初期装備としてHDDを購入した。 HDD は自分の中で悪いイメージがついていないHGST*4NAS用のもので1TB単価が安いものを購入した。

買ったのは HGST 0S04005-2 (HDN726040ALE614 * 2) という 4TB 7200rpm のもの。 Amazon にて ¥28,942(税込) で購入。

最近は NAS の中身は 5400rpm のものにしていたが、こちらは 7200rpm のもの。 熱とか消費電力とか音とかが若干気になったが、NAS用を謳っているので気にしないことにする。

雑感

以下、軽く使ってみた感想とかメモとかを取り留めなく列挙。

本体軽い。ReadyNAS(旧NAS) は馬鹿みたいに重い。2.5kg くらい違うみたい。

HDD 繋がないと初期設定できなかった。

HDD をネジ無しで固定できるのは非常に良い。ReadyNAS はネジ固定。

HDD つないだら Btrfs, SHR に勝手になった。

初期設定時は余計なアプリ等は入れないようにした。後から入れられるし。

起動時のビープ音は、もうちょっと優しい感じにしてほしい。

ファンの音は結構静か。いくつかモードがあって、デフォルトで低ノイズモードだった。

HGST HDD のカリカリ音は結構うるさい方。

ソフトウェアのアップデート(DSM6.2) が出ていたのでアップデートした。デフォルトの設定等が変わっている可能性を考慮してボリューム等は一回消して作り直してみた。

ベンチマークとか本格的な使用感についてはまた今度。

*1:以前ググった感じだと、sambaのバージョンを上げれば直りそうだが、 ファームウェア更新が打ち止めになっているため、やるなら不具合覚悟の手動更新のため、面倒くさい

*2:購入後に知ったのだが、5月末にリリースされた DSM6.2 からは ARM 系の安いモデルでも Btrfs に対応するようになったらしい

*3:いろいろいじった今では上位の方にしてみても良かったかなと思っているが、今だけかも

*4:Seagate, WD は過去(といってもだいぶ前だが)に壊れた経験あり。今は HGST も WD の傘下らしいのだが、気持ちの問題