while ループでフラグの更新を待つ時は Task.yield() が便利

Swift の Task.yield() は、非同期タスクから実行を一時停止して、他のタスクが実行されるようにするために利用するAPIです。よきタイミングでスレッドを譲ることで、CPU負荷の高い処理を効率的に実行することができたりします。

developer.apple.com

この Task.yield() はある状態が担保されているかを待つなどの while ループ でも使うことができます。

// isFoo = false になるまで Task.yield() し続ける
while isFoo {
    await Task.yield()
}

非同期処理の中の while ループで Task.yield() を使うことで、後続処理に進むのにある状態を担保しつつ不必要な sleep をしなくて済むということができるという学びでした。

FCM HTTP v1 API 移行における注意点

Firebase Cloud Messaging を利用してアプリケーションにプッシュ通知を送っている方は既にご存知かと思いますが、 2024年6月20日をもって Firebase Cloud Messaging(FCM)レガシーレジスタAPIとレガシー送信APIの数が削減されます。いくつかのAPIの廃止により、すでに廃止されたSDKや機能の一部は6月20日以降に動作しなくなるため、新しいAPIやそれに対応したSDKのメソッドに置き換えることが必要です。 詳細は2023年6月21日に "[Action Required] Update your apps to the latest Firebase Cloud Messaging APIs and SDKs" と題したメールが届いているはずなので、Deprecation の詳細と影響のあるプロジェクトをご自身で確認することをお勧めします。

Batch API Deprecation

今回の Legacy HTTP ProtocolFirebase Admin SDK Batch Send API ( sendMulticast() メソッド含む) の廃止により、Legacy API に頼らずに多くのデバイスに大規模かつ低レイテンシでマルチキャストメッセージを同時に送信することが難しくなりました。ガイドに沿って新しいAPIへ移行するだけだと、以前のパフォーマンスを維持したまま送信することができないので注意が必要という話をします。

Batch API とは何か

バッチ送信と呼ばれるFCMへの単一のHTTPリクエストに複数の送信リクエストを含めることができるAPIです。Admin SDK 経由で送信している場合、以下のメソッドが Batch API を利用しています。

  • sendAll()
  • sendMulticast()
    • 内部で sendAll() を使っています

FCM HTTP v1 API ベースの移行先メソッドは以下の通りです。利用中の各言語の Admin SDK のドキュメントコメントを確認してください。

  • sendAll()sendEach()
  • sendMulticast()sendEachMulticast()

Batch API を使うと一度のHTTPリクエストで500トークンに対して送信することができるため、低レイテンシで多くのトークンにメッセージを送ることが可能でした。多くのトークンに対してメッセージを送信する場合、トピックにメッセージを送信することも有効ですが、1つのアプリがサブスクライブできるトピック数は 2,000 トピックまでとなっており、複雑なメッセージ制御が必要なケースや、異なるメッセージを同時に送信したい場合などにこの Batch API が有用でした。

なぜ注意が必要なのか

Admin SDK を利用して移行する場合、 素直に sendEachsendEachMulticast に書き換えるだけでは Batch API 同等のパフォーマンスを発揮することはできません。 新しい FCM HTTP v1 API はリクエストごとに1つのトークンしか受け付けないため、基本的にはメッセージのトークンごとにHTTPリクエスト全体を送信する必要があります。大規模での多数のトークンへの送信は、HTTP 1.0 / 1.1で非常に遅くなる可能性があり、多くのTCP接続が必要になります。現在確認できるかぎり、Node.js の公式 Firebase Admin SDK ではこの sendEach メソッドにおいて単一のトークンごとに新しいHTTP 1.0接続を同時に開くため安易に移行するのはオススメできないという状況です...。

ではどうするのか

幸い FCM HTTP v1 API は HTTP/2 for Multiplexing に対応しています(!)。Multiplexingでは1つのHTTPコネクションの中に複数のストリーム (チャネルみたいなもの) を立てることにより、複数のHTTPリエクエストを同時に送信することができます。HTTP/2ベースで v1 API を直接利用するように実装する必要はありますが、現状の Batch API 同等かそれ以上のパフォーマンスを求める場合はこの方法しかないでしょう。

以下のFAQ*1にもこのことは記載されており、100トークン未満ならHTTP/2上のHTTP v1 APIは、マルチキャスト要求の99.9%に対して同様に機能するとのことです。(そこは500トークンであってほしかった...)

Q: Does the HTTP v1 API support sending messages to multiple tokens in one request?

A: No. This feature, called "multicast" in legacy HTTP APIs, is not supported by the HTTP v1 API, which is better designed for scalability.\n

For use cases where end-to-end latency is critical, or where total fanout size is small (fewer than 1 million), Google recommends sending multiple separate requests using the HTTP v1 API. The HTTP v1 API over HTTP/2 performs similarly for 99.9% of multicast requests (sending < 100 tokens). For outlier use cases (sending 1000 tokens), it achieves up to a third of the throughput rate, so additional concurrency is needed to optimize for this atypical use case. Users can experience more reliability and availability with the HTTP v1 API than with legacy multicast.

firebase.google.com


公式の Admin SDK が HTTP/2 for Multiplexing に対応するかは現時点では不明ですが、今の所特に動きはありません。 自分はまだ移行を完了していないので、引き続きこのトピックは注視していきたいと思いますが、6月21日の Batch API の廃止の影響を最小限に留めるためには HTTP/2 ベースで実装するしかなさそうですね、というお話でした。

*1:FAQのFCM features deprecated in June 2023の章は日本語ではまだ表示されません。Englishで確認しましょう。

コンフィグレーション生成のための静的型付き言語「Pkl」を試してみる

Vision Pro で盛り上がる最中、Apple からコンフィグレーションファイルを生成するための静的型付言語「Pkl」がオープンソースで公開されたので軽く触ってみました。 github.com

発音は "Pickle" (「ピックル?ピクルゥ?」 )だそうで、 Pickle と聞くと Python"Pickle化" が頭をよぎりますが、今回はそこには触れずに進めていきましょう!

Installation

公式ドキュメントの Installation からインストールします。今回は M1 MacBook Air から試してみるので、 Native macOS executable for amd64 (tested on macOS 10.15) を使います。

$ curl -L -o pkl https://github.com/apple/pkl/releases/download/0.25.1/pkl-macos-amd64
$ chmod +x pkl
$ ./pkl --version
Pkl 0.25.1 (macOS, native)

単に実行ファイルが降ってくるだけなので、実用を考えるなら ./.pkl/bin みたいなディレクトリを作っておいた上でダウンロードして、 .zshrc などに export PATH=$PATH:$HOME/.pkl/bin と追加しておくと任意の場所で実行できるようにできます。(Go風味)

触ってみる

コンフィグレーションファイルを生成するための言語として CUE などが比較にあがりますが、 Pkl がユニークなのはコンフィグレーションファイルの生成だけではなくアプリケーションへ埋め込み可能な言語でもあることでしょう。現状 Language Binding はすでに

  • Java
  • Kotlin
  • Swift
  • Go

がサポートされており、今後も増えていくよと語られています。Go が最初からサポートされているのははっきりとターゲットを意識しているな〜と感じました。 github.com

Swift

せっかくなので Swift で触ってみます。 Quickstart に沿ってやっていきましょう。追加でバイナリが必要なので落としてきます。

$ curl -L https://github.com/apple/pkl-swift/releases/download/0.2.1/pkl-gen-swift-macos.bin -o pkl-gen-swift
$ chmod +x pkl-gen-swift

バージョンが表示されていればOKです。

$ pkl-gen-swift --version
0.2.1

Vapor を使って、簡単なWEBアプリケーションサーバーのプロジェクト用意しておき、そこに pkl-swift を依存として追加します。

// swift-tools-version:5.9
import PackageDescription

let package = Package(
  name: "hello",
  platforms: [
    .macOS(.v13)
  ],
  dependencies: [
    .package(url: "https://github.com/vapor/vapor.git", from: "4.89.0"),
    .package(url: "https://github.com/apple/pkl-swift", from: "0.2.1"),
  ],
  targets: [
    .target(
      name: "Generated",
      dependencies: [
        .product(name: "PklSwift", package: "pkl-swift")
      ]
    ),
    .executableTarget(
      name: "App",
      dependencies: [
        "Generated",
        .product(name: "Vapor", package: "vapor"),
      ]
    ),
    .testTarget(
      name: "AppTests",
      dependencies: [
        "Generated",
        .target(name: "App"),
        .product(name: "XCTVapor", package: "vapor"),

        // Workaround for https://github.com/apple/swift-package-manager/issues/6940
        .product(name: "Vapor", package: "vapor"),
      ]),
  ]
)

今回は Example で紹介されているサーバーのホストとポートを環境ごとに用意するユースケースで確認してみましょう。まず AppConfig.pkl というファイルを作り、以下のように書きます。

// AppConfig.pkl
module AppConfig

host: String
// UInt16 は Int(isBetween(0, 65_535)) の typealias として定義されています
port: UInt16

AppConfig モジュールからローカル環境向けのコンフィグを作ります。

// pkl/Local/config.pkl
amends "../AppConfig.pkl"

host = "localhost"
port = 8080

pkl コマンドで正しく設定できているか確認できます。

$ pkl eval ./pkl/Local/config.pkl
host = "localhost"
port = 8080

今回はそのままアプリケーションから読み込んで使いますが、 JSONYAML にも当然変換できますね。

$ pkl eval -f json ./pkl/Local/config.pkl
{
  "host": "localhost",
  "port": 8080
}
$ pkl eval -f yaml ./pkl/Local/config.pkl 
host: localhost
port: 8080

実際に書き出すなら、-o オプション*1をつければいい。

$ pkl eval -f json -o config.json ./pkl/Local/config.pkl

実際にSwift アプリケーションから使えるようにするには pkl-gen-swift で Swift ファイルを生成します。

$ pkl-gen-swift pkl/AppConfig.pkl -o Sources/App/Generated/

すると、以下のようなファイルが生成されます。

// Code generated from Pkl module `AppConfig`. DO NOT EDIT.
import PklSwift

public enum AppConfig {}

extension AppConfig {
    public struct Module: PklRegisteredType, Decodable, Hashable {
        public static var registeredIdentifier: String = "AppConfig"

        public var host: String

        public var port: Int

        public init(host: String, port: Int) {
            self.host = host
            self.port = port
        }
    }

    /// Load the Pkl module at the given source and evaluate it into `AppConfig.Module`.
    ///
    /// - Parameter source: The source of the Pkl module.
    public static func loadFrom(source: ModuleSource) async throws -> AppConfig.Module {
        try await PklSwift.withEvaluator { evaluator in
            try await loadFrom(evaluator: evaluator, source: source)
        }
    }

    /// Load the Pkl module at the given source and evaluate it with the given evaluator into
    /// `AppConfig.Module`.
    ///
    /// - Parameter evaluator: The evaluator to use for evaluation.
    /// - Parameter source: The module to evaluate.
    public static func loadFrom(
        evaluator: PklSwift.Evaluator,
        source: PklSwift.ModuleSource
    ) async throws -> AppConfig.Module {
        try await evaluator.evaluateModule(source: source, as: Module.self)
    }
}

AppConfig.Module 構造体とpklモジュールから AppConfig.Module に評価・変換する関数が含まれていますね。

生成された関数 loadFrom を使って、先ほど作ったローカル環境向けの設定ファイルを読み込めば、意図した環境の AppConfig.Module が得ることができます。あとはそれらをアプリケーションに適用するだけです。

import Generated
import PklSwift
import Vapor

public func configure(_ app: Application) async throws {
  let config = try await AppConfig.loadFrom(source: ModuleSource.path("pkl/Local/config.pkl"))
  app.http.server.configuration.hostname = config.host
  app.http.server.configuration.port = config.port

  try routes(app)
}

swift run で動かせば、 Local/config.pkl で設定した値でサーバーが起動することが確認できるはずです。

[ NOTICE ] Server starting on http://localhost:8080

ざっくり触ってみるところまでやってみましたが、Pkl という言語の表現力にはあまり触れることができませんでした。それはまた別の機会にやってみたいと思います。

言語仕様については紹介しきれない箇所が沢山(throwできたり、Null Value があったりなどなど)あるので、気になる方は公式ドキュメントをご確認ください。

最後に

CUE や toml でいいという声も聞こえてきそうですが、哲学も異なりますし、Framework Integrerations のサポートは Spring (Boot) のみという状況から見ても、Apple の内部のバックエンドサーバー開発のために作ったものを公開したという感じなんですかね(?)。ちなみに公式サイトでは "Incredible IDE Integration" と謳っていますが、 Xcode への統合サポートは現状ありません。Apple 製というのもありしっかり Plist 生成のサポートもあります。今後のアップデートに期待ですね!

Simulator.app の「Stay On Top」をキーボードショートカットで切り替える

この記事は はてなエンジニア Advent Calendar 2023 の 2024年1月15日 の記事です。 developer.hatenastaff.com

Xcode を利用したアプリケーション開発では、実装の確認にシミュレータを利用することが多いかと思います。デバッグの際にはシミュレータと Xcode を交互に行き来することになりますが、コンソールの確認のために Xcode にフォーカスが移すとアプリケーションの Window Level が切り替わり、シミュレータが Xcode の裏に周ってしまうため、再度シミュレータに戻るためにはアプリケーションの切り替え操作が必要になってしまいます。

そこで便利なのが今回紹介する「Stay On Top」というシミュレータを常に最前面に固定するオプションです。

Simulator > Window > Stay On Top

ご存じの方も多いかと思いますが、このオプションを有効にするとシミュレータを起動した状態で他のアプリケーションにフォーカスを切り替えても、シミュレータを前面に配置したままにしてくれます。Breakpoint を設定し lldb を使って変数の確認や操作する際にシミュレータと Xcode の行き来がしやすくなるので大変有用なのですが、いかんせんショートカットキーが割り当てられていないためこのオプションの切り替えが少しコストを感じていました。そこで今回はショートカットキーが割り当てられていない「Stay On Top」の切り替えをキーボードショートカットでできるようにしたいと思います。

AppleScript でメニューのクリックを自動化する

まず、メニューバーにある「Stay On Top」メニューのクリックを AppleScript に担ってもらいます。AppleScriptMac OS/macOSスクリプティング機構 Open Scripting Architecture (OSA) に対応した言語(OSA言語)のひとつで、 macOS でアプリケーション操作の自動化を行うのに最適です。

developer.apple.com

support.apple.com

今回の目的は Simulator.app の「Window」メニュー内にある「Stay On Top」というメニューをクリックすることなので、AppleScript で操作する場合は以下のようになります。

tell application "System Events"
    -- Check Simulator.app is open
    if (count of (processes whose name is "Simulator")) > 0 then
        tell application "Simulator"
            -- Activate Simulator.app
            activate
            -- Toggle 'Stay On Top' from 'Window' on menu bar
            tell application "System Events"
                tell process "Simulator"
                    click menu item "Stay On Top" of menu "Window" of menu bar 1
                end tell
            end tell
        end tell
    end if
end tell

実用上、今回する設定するキーボードショートカットは Simulator.app が起動している際にしか使わないこと、起動していない状態から起動させるようなことはしないことを考慮し if (count of (processes whose name is "Simulator")) > 0 で起動状態をチェックします。

また、実際に表示されているメニューをクリックする必要があるため activate コマンドでアプリケーションにフォーカスします。そうすることでメニューバーに Simulator.app のメニューが並んでいる状態を保証することができます。

最後は Window > Stay On Top をクリックさせるという形です。スクリプトが準備できたら Script Editor.app で実際に動かしてみます。

Script Editor.app で AppleScript を動かしてみる
Script Editor.app

スクリプトを走らせてみて実際に「Stay On Top」 オプションの切り替えができていることを確認*1しましょう。

Shortcuts.app で AppleScript を走らせる

先ほどの AppleScript にショートカットキーを割り当てるために Shortcuts.app 経由で実行できるようにします。 AppleScript を動かすには事前に Settings... > Advanced > Allow Running Scripts の設定をしておきます。

Shortcuts.app で AppleScript を動かすには &#x60;Allow Running Scripts&#x60; が必要
Shortcuts.app で AppleScript を動かすには `Allow Running Scripts` が必要

あとは実行するアプリケーションに Script Editor を選択し、 Run AppleScript を選んで先ほど作ったスクリプトを貼り付けて実行*2します。

Shortcuts.app で AppleScript を実行する
Shortcuts.app で AppleScript を実行する

キーボードショートカットを設定する

最後は任意のショートカットキーを割り当てて完成です。ここで重要なので実際にこのショートカットキーを入力する際に開いているであろうアプリケーション(Xcode や Simulator.app など)が持つショートカットキーと競合させないようにすることです。競合しないものであれば好きなコマンドを割り当ててください。

任意のショートカットキーを割り当てる
任意のショートカットキーを割り当てる

最後に

このようにショートカットキーが割り当てられていないメニューにもキーボードショートカットを追加することは可能です。Xcode を用いたネイティブアプリケーション開発に限らず、シミュレータを用いた開発であれば ReactNative や Flutter などでも利用できそうですね。Xcode では Build Phase / Run Script で AppleScript を実行することもできるので、そういったアプローチもできますが、今回は手っ取り早くショートカットキーで操作できる方法を紹介しました。他にももっといい方法があるぜという方は是非教えてください!

*1:スクリプトやキーボードショートカットを動かす時にアプリケーションに Accessibility Access が必要になるので適宜設定してください

*2:1と同様。

Swift の Optional パターンを駆使する

こんにちは。2023年7月からはてなマンガアプリチームで働いています、id:fxwx23 です。

はてなに入社する前は React Native (TypeScript) や Go などを書くことが多かったため、 Swift を書くことは2021年の夏以来となります(!)。Swift を書くことを本格的に再開できることになったので、早速自分の復習も兼ねて Patterns の話を少しまとめておこうと思います。

Swift の Patterns は、 値を一致させて分解する単一の値または複合値の構造を表します。 Patterns には以下の種類があります。

今回は、 自分としても発見があった Optional Pattern に焦点をあてていきたいと思います。

Optional Pattern とは

Optional Pattern は以下のように説明されています。

An optional pattern matches values wrapped in a some(Wrapped) case of an Optional enumeration. Optional patterns consist of an identifier pattern followed immediately by a question mark

Optional Pattern は Identifier Pattern の直後に ? を置くことで表現され、 Optional<Wrapped>some(Wrapped) ケースにラップされた値と一致させることができます。(自分はSwiftは2系から書いていましたが、このパターンは知らずに使っていました...🙈)

let someOptional: Int? = 23
if case let x? = someOptional {
    print(x)
}

また、Optional Pattern は Optional の Enumeration Case Pattern のシンタックスシュガーと記載されており、

optional patterns are syntactic sugar for Optional enumeration case patterns

先ほど挙げたコード例は Enumeration Case Pattern と同等と解釈することができます。

let someOptional: Int? = 23
if case .some(let x) = someOptional {
    print(x)
}

上記の例だと、単純なので if let x = someOptional { ... } と書くことが多いと思います。今なら SE-0345: if let shorthand を使うでしょう。

if let someOption { ... }

では、Optional Pattern の使い所はどこにあるでしょうか。

Optional Pattern の使い所

まず、自分が Optional Pattern を調べるきっかけになった Associated Values を持つ Enumeration Case Pattern です。Optional Pattern を使う前は Associated Values が Optional なら、取り出した上で if let で unwrap するというようなコードを書いていましたが、

enum SomeEnum {
    case left(Int?)
}
            
let foo = SomeEnum.left(23)
if case .left(let x) = foo, let x {
    print(x)
    // 23
}

Optional Pattern は Associated Values にも有効なので、このように書くことができます。

let foo = SomeEnum.left(23)
if case .left(let x?) = foo {
    print(x)
    // 23
}

// これはつまり以下と同等
if case .left(.some(let x)) = foo {
    print(x)
}

実はこれは Patterns の説明の冒頭で説明されています。switch 文の case ラベル、do 文の catch 句、または ifwhileguard 、または for-in 文の case 条件で使用することができることがわかります。

These include enumeration case patterns, optional patterns, expression patterns, and type-casting patterns. You use these patterns in a case label of a switch statement, a catch clause of a do statement, or in the case condition of an if, while, guard, or for-in statement.

もう一つ 2つの Optional の文字列の順序が必要なシーンを考えてみます。意見が分かれるところですが、両方の文字列に値がある場合に使用される Optional Pattern は、読みやすい選択肢ではないかと感じました。

func compare(_ lhs: String?, _ rhs: String?) -> ComparisonResult {
    switch (lhs, rhs) {
    case (.none, .none): return .orderedSame
    case (.some, .none): return .orderedDescending
    case (.none, .some): return .orderedAscending
    // 以下は case (.some(let lhs), .some(let rhs)): と同等
    case (let lhs?, let rhs?):
        if lhs == rhs {
            return .orderedSame
        }
        return lhs < rhs ? .orderedAscending : .orderedDescending
    }
 }

最後に

あらためて Swift の強力な表現力を知ることができました。 Optional をサポートする他の言語ではどうなっているか気になってきましたね(?)。

Swift をこれから学ぶ方、普段から書いている人も是非 The Swift Programming Language をもう一度読んでみることをお勧めします。自分も読み直す中で面白い発見があればまた記事にしていきたいと思います。


(実は初ブログなのです)