emahiro/b.log

日々の勉強の記録とか育児の記録とか。

httptest で server を立てるときに Port を固定する

Overview

endpoint のテストをするときに httpserver でサーバーを立ち上げて request と response の検証をする、ということをするときに、httptest で立ち上げるサーバーに割り当てられる port はランダムに決まります。
ただ、テストによっては port を固定したい時もあるので、固定する方法を実装してみました。

port を固定する方法

func TestServer(t *testing.T) {
    listener, err := net.Listen("tcp", "127.0.0.1:9999")
    if err != nil {
        t.Fatal(err)
    }

    mux := http.NewServeMux()
    ts := httptest.NewUnstartedServer(mux)
    ts.Listener = listener
    ts.Start()
    t.Cleanup(func() {
        ts.Close()
    })
}

上記のような方法で固定できます。
固定できるというか、指定した Listener を httptest server で立ち上げた server で指定することでその port で TCP のやり取りをできるようになる、という感じです。

やってみると簡単でした。

GCP で Container Registory から Artifact Registory に移行する

Overview

GCP の Container Registory が廃止されるので Artifact Registory に移行しました。

cloud.google.com

移行手順

以下のドキュメントに沿って進めました。

cloud.google.com

なお、移行するときに Container Registory -> Artifact Registory へコピーするための権限をリソースに渡す必要があります。

  gcloud projects add-iam-policy-binding $projectID --member=user:$username --role='roles/storage.admin'
  gcloud projects add-iam-policy-binding $projectID --member='$serviceAccountName' --role='roles/storage.objectViewer'

CloudRun をデプロイするときに terminated: Application failed to start: failed to load /docker-entrypoint.sh: exec format error エラーが発生して起動しない

Mac で作ったイメージを使って CloudRun をデプロイするときに terminated: Application failed to start: failed to load /docker-entrypoint.sh: exec format error というエラーが発生してコンテナが起動しない、ということがあったのですが、これはビルドするときに platform を指定しない状態でビルドしていたためでした。

docker build --platform linux/amd64 -t $tagName:latest としないと Apple Silicon でビルドしたイメージを使ってコンテナは動きません。

zenn.dev

buf CLI を v1.32 以上に上げて設定ファイルを v2 にマイグレーションする

概要

タイトルのとおりです。
Buf CLI を最新(v1.36) にしたところ、v1.32 以上で大きな変更があった模様で、それに対応しました。

buf.build

zenn.dev

対応

github.com

ハマったところ

新しい Mac で作業していたところ、Connect (依存含め)を動かすためのライブラリを go get 経由で入れて損ねてました。

slices.SortedFunc の挙動を少し調べて疑問に思った話

Overview

Go1.23 で range-over func を取り込んだ slices package 内の SortedFuncSortedStableFunc の挙動について少し調べました。

Sort における Stable とは?

Godoc を読んでも完全には理解できなかったので ChatGPT に聞いてみました。


ソートにおける「stable(安定)」とは、ソートされた後も、元の順序が保持される性質を指します。

具体的には、ソートする際に、同じ値を持つ要素が複数ある場合、それらの要素の相対的な順序がソート後も変わらないことを意味します。例えば、リストの中に同じ値を持つ要素AとBがあり、ソート前にAがBの前にあったとします。安定なソートを行った場合、ソート後もAはBの前に残ります。

例:

以下に、安定なソートと不安定なソートの違いを示します。

ソート前

[(3, 'a'), (1, 'b'), (2, 'c'), (3, 'd')]

安定なソート後 (数値でソート)

[(1, 'b'), (2, 'c'), (3, 'a'), (3, 'd')]

この例では、3の値を持つ2つの要素 (3, 'a')(3, 'd') の相対的な順序は、ソート前と後で変わっていません。

不安定なソート後 (数値でソート)

[(1, 'b'), (2, 'c'), (3, 'd'), (3, 'a')]

不安定なソートでは、3の値を持つ要素の相対的な順序がソート前後で変わってしまっています。

安定なソートは、特に複数のキーでソートする際に重要です。たとえば、最初に名前でソートし、その後年齢でソートする場合、安定なソートを使用することで、同じ年齢の人々の名前の順序が保たれます。


Sort するときに元の並び順を保証したいケースがあり、この保証をしてくれるメソッドが SortedStableFunc になります。

実際に動かしてみた

上記の説明のとおりなので期待するふるまいは以下になります。

  • SortedFunc
    • Sort した結果、同じ値の要素があったときの、その同じ要素同士の並び順は保証されない。
  • SortedStableFunc
    • Sort した結果、同じ値の要素があったときに、元の配列の並び順の通りに Sort される(並び順が保証される)
package main

import (
    "fmt"
    "slices"
)

type taple struct {
    a int
    b string
}

func main() {
    // sort の種類を設定してみる
    taples1 := []taple{{3, "dd"}, {1, "a"}, {3, "cc"}, {3, "ff"}, {2, "b"}, {4, "ee"}}
    s1 := slices.SortedFunc(slices.Values(taples1), func(a, b taple) int {
        return a.a - b.a
    })
    fmt.Println(s1)

    taples2 := []taple{{3, "dd"}, {1, "a"}, {3, "cc"}, {3, "ff"}, {2, "b"}, {4, "ee"}}
    s2 := slices.SortedStableFunc(slices.Values(taples2), func(a, b taple) int {
        return a.a - b.a
    })
    fmt.Println(s2)
}

ref: https://go.dev/play/p/ePGk-95Z8EW

結果なんですが、 SortedFunc でも並び順が何故か固定化されました(なぜ...). 上記の Playground で実行してみると SortedFunc の結果と SortedStableFunc の結果は何度実行しても同じ結果になりました。

ただ、利用するときには元の配列の並び順を保証してくれるのは SortedStableFunc なので、ユースケースに応じて使い分けるのが良さそうです。

調べたときの PR

github.com

Go1.23 の range-over func を使って高階関数を書いてみた

Overview

Go1.23 から導入される Iterator を使って Chunk 処理を書く - emahiro/b.log を書いた当日(日本時間だと翌日だけど)Go 1.23 が正式にリリースされていたので、サンプルとして range-over func を使った高階関数のセットを書いてみた。

Repository

リポジトリは以下

github.com

Usecase

Filter や Chunk は登場シーンが多いんじゃないかと思う。

例えばフィルタリング処理であれば、ある処理を行うのにフィルタされたデータセットが必要な場合、今までは事前に for-loop でフィルタリング処理を行う or sices.DeleteFunc 等で条件にそぐわない要素を削除しておき、前処理で作られた slices に対して本処理を行っていたところ、Filter 関数を使うと for の中でフィルタされたデータを使うことができる。

list := ....

for i := range hof.Filter(list,  func(v int) bool { return v%2 == 0 }) {
    // 偶数のみが for-loop に入ってくる
}

Chunk は前述したブログに記載した通り、配列を Chunk したセットが for-loop に入ってくるので、小刻みな処理を行いたいときに使える(かつパフォーマンスも良い)

Reduce や Map/Map2 といった処理は書いては見たものの、利用したいシーンはまだ特に思いつかない。
大きなデータを扱うようなユースケースであればデータの前処理や後処理で活躍しそうな気はした。

その他

この辺で書いてみた関数の一部 (Chunk とか)は slices package に iterator として用意されています。

Go1.23 から導入される Iterator を使って Chunk 処理を書く

Overview

Go1.23 から導入される Iterator を使って同様に Chunk 処理を実装してみます。

Sample

 func chunk[T any](src []T, chunkSize int) iter.Seq[[]T] {
  return func(yeild func([]T) bool) {
      for i := 0; i < len(src); i += chunkSize {
          end := i + chunkSize
          if end > len(src) {
              end = len(src)
          }
          if !yeild(src[i:end]) {
              break
          }
      }
  }
 }

利用方法

func main() {
    arr := make([]int64, 10000)
    for i := range arr {
        arr[i] = int64(i + 1)
    }
    for chunkedIDs := range chunk(arr, 1000) {
        echo(len(chunkedIDs))
    }
}

func echo[T any](v T) {
    fmt.Println(v)
}

Benchmark

以下のような従来の一般的な方法で実装された Chunk 処理とベンチマークの比較をしてみる。

func chunkBy[T any](items []T, chunkSize int) [][]T {
    chunk := make([][]T, 0, (len(items)/chunkSize)+1)
    for chunkSize < len(items) {
        chunk, items = append(chunk, items[0:chunkSize:chunkSize]), items[chunkSize:]
    }
    return append(chunk, items)
}

Benchmark のコードは以下

func BenchmarkChunk(b *testing.B) {
    // chunk と chunkBy のベンチを取る
    arr := make([]int64, 10000)
    for i := range arr {
        arr[i] = int64(i + 1)
    }

    b.Run("chunk", func(b *testing.B) {
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            for range chunk(arr, 1000) {
            }
        }
    })

    b.Run("chunkBy", func(b *testing.B) {
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            for range chunkBy(arr, 1000) {
            }
        }
    })
}

ベンチマーク結果は以下

go test -bench=. -benchmem
goos: darwin
goarch: amd64
pkg: emahiro.dev/adhoc/go-sandbox
cpu: VirtualApple @ 2.50GHz
BenchmarkChunk/chunk-8          37948653                32.04 ns/op            0 B/op          0 allocs/op
BenchmarkChunk/chunkBy-8        16093440                69.57 ns/op          288 B/op          1 allocs/op
PASS
ok      emahiro.dev/adhoc/go-sandbox    3.678s

※ このベンチマークは Go1.23rc2 を使っています。

chunk する処理の中で chunk した結果を入れる箱を用意しない分 Allocation が発生せず iterator を使うほうがパフォーマンスが良いことがわかりました。

See Also

zenn.dev

semaphore を使って goroutine の起動数を Go から排他制御する

Overview

sync package 内にある semaphore を使って Go のアプリケーション側で goroutine の起動数を排他制御します。

Go の並行処理の起動数を制御するのは

使い方

https://pkg.go.dev/golang.org/x/sync/semaphore#section-documentation のサンプルにある実装方法に準拠するだけでOKです。

// goroutine の上限を指定する。
var maxConcurrentNum int64 = 20
sem := semaphore.NewWeighted(maxConcurrentNum)

ctx := context.Backgrdound()

for i := range 20 {
    if err := sem.Acquire(ctx, 1); err != nil {
        return err
    }

    go func(ctx context.Context) {
        defer func() {
            sem.Release(1)
            if err := recover(); err != nil {
                fmt.Errorf("err: %v\n", err)
            }
        }()
        // do concurrent proccess without cancel context
        cctx := context.WithoutCancel(ctx)
    }(ctx)
}


// 最期に全部の worker pool を取得して、意図しないプロセスがゾンビとして残らないようにする。
if err := sem.Acquire(ctx, maxConcurrentNum); err != nil {
    return err
}

注意点として semaphore を Acquire する(P操作)ときに、 TryAcquire というメソッドも利用できますが、これは「利用可能か」の結果を返すだけのメソッドなので、Godoc の使い方の通り Acquire メソッドを使いましょう。こちらは semaphore に利用可能なプロセスがあるか、が充填されるまで処理を Blocking してくれます。

See Also

wa3.i-3-i.info

soarflat777.hatenablog.com

Go の iterator を触ってみた

Overview

今月にもリリースされる予定の Go1.23 に同梱されている iterator package をだいぶ今更ながら触ってみました。
どういうものか、ということの概要は知っていましたが、まぁ一旦自分でも触ってみるか、ということで触ってみて、実際動かしながら触れることで思ったことを残しておきます。

実行方法

手元は Go1.22 なので GOEXPERIMENT=rangefunc を付けて Go のファイルを実行しました。

感想

先に感想だけ書いておくと、自分としては iterator は少し直感的じゃないなという印象を持ちました。

例えばシンプルな iterator 処理を書こうと思うと以下になりますが、

func TestSeq(t *testing.T) {
    for v := range seq {
        t.Log(v)
    }
}

func seq(yeild func(int) bool) {
    for i := range 10 {
        yeild(i)
    }
}

yeild が call されるたびに for-loop の処理に一旦処理が委譲され、再度 range over func で iterator のメソッドが実行されると前回中断した yeild の処理から再開する、というのが処理の流れになりますが、そもそもメソッドを2つ行き来しないと行けないとは、直感的に読みづらいなと感じました。
自分は Typescript も業務で利用してますが map や filter がメソッド2つに分割されていたら、やっぱり読みづらいと感じてしまいます。

この辺りは書いていくうちに脳内にマップが出来上がって多少読みやすくはなるのかもしれませんが、初見では分かりづらさが勝ります。

次に yeild での for-loop への処理の委譲についてですが、以下の2つの実行結果は実は同じです。

func TestEven(t *testing.T) {
    num := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    for v := range even(num) {
        fmt.Println(v)
    }
}

func even(num []int) Seq[int] {
    return func(yield func(int) bool) {
        for _, n := range num {
            if n%2 == 0 {
                if !yield(n) {
                    break
                }
            }
        }
    }
}
func TestEven(t *testing.T) {
    num := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    for v := range even(num) {
        fmt.Println(v)
    }
}

func even(num []int) Seq[int] {
    return func(yield func(int) bool) {
        for _, n := range num {
            if n%2 == 0 {
                yield(n)
            }
        }
    }
}

どちらも2で割り切れる値のときに for-loop の処理が走ります。違うのは yeild の評価方法です。

サンプルの実装でも if !yeild(...) { return } という loop を抜けると処理を完了する、という condition を書いてる物が多かったですが、どうして否定形で書くのかいまいちわかっていませんでした。

自分なりに納得したのは例えば以下のような呼び出す側(for-loop側)が途中で loop 終わるケースで、

func TestEven(t *testing.T) {
    num := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    for v := range even(num) {
        fmt.Println(v)
        break
    }
}

iterator 側の処理を止めていない(= yeild の評価をせずそのまま yeild を実行している)ときは、

panic: runtime error: range function continued iteration after exit

goroutine 1 [running]:
main.main.func2(0x2)
        $HOME/main.go:32 +0x30
main.main.iterFilter.func3(...)
        $HOME/main.go:24
main.main()
        $HOME/main.go:32 +0x8c
exit statu

上記のような panic が発生します。これは loop はすでに抜けているのに iterator がわの処理が続いてしまうことに起因します。

こういうケースで意図しない panic を防ぐためにも yeild を評価するときは否定形で確認する実装方法がもしかしたらいいのかもしれないな、と思いました。

まとめ

何にしても Generics 以来の Go の大きなアップデートなので触りながら慣れていきたいなと思います。

ポートフォリオサイト盆栽記録 2024年7月度

サマリ

個人で運用してるポートフォリオサイトの更新記録です。

emahiro.dev

今までなんとなく更新してましたが、更新ログをつけてみることにしました。

更新内容

依存関係をアップデートしました

毎回やってるやつですね。 emahiro.dev は next と firebase-functions で動いているのでそれぞれに npm audit fix をかけました。

これに合わせて

  • next を 13 -> 14 へアップデート
  • firebase を v2 にアップデート

をしました。

ImageResponse を next/server -> vercel/og に移管

Next13 のときは OG Image の生成に next/server の Image Response を使っていたのですが、Next14 ではこれが非推奨になっていたので vercel/og の利用に切り替えました。

vercel.com

@notionhq/client のバージョンを最新にしました

これが地味にめんどくさかったです。
依存モジュールのバージョン上げたら既存で使っていた Database の QueryResponse の中に入っていた値が取れなくなっていたので、API 書き直しでした。

具体的には Database Query で取得しても page の properties が一部入ってこないものがあった(Tag 情報とか)ので、DB を Query して Page の詳細取得する、という二段構えにする必要がありました。

developers.notion.com

Note に「しずかなインターネット」の記事を追加しました

「しずかなインターネット」に他愛もないことや、技術以外のことを書いていたんですが、自分の投稿一覧に追加しました。
このための API の利用を開始するためにスポンサーになっていたのですが、ようやく実装することができました。

sizu.me

ドキュメントも充実してていい感じです(endpoint 数も少ないし参照系しかないのでメンテしやすそう)

github.com

今は Note 一覧から一旦「しずかなインターネット」に飛ばしてますが、車輪の再発明したいので自前の markdown parser でも書こうかなと思っています。

今回やろうと思ってやらなかったこと

npm -> pnpm への移行

next と firebase functions が実質 monorepo 構成になっているのと、依存管理がしやすくなるならやろうかなーと思ったんですが、デプロイに使ってる GitHub Actions とかも変更しないといけないので、今回は pend しました。

ブログのロゴを変えてみた

ずっと自分の写真だったけど昨今のいろいろリスクも増えてきたのでブログ用のアイコンを作って favicon 等々に適用してみた。
なお、どういうロゴがいいかわからなかったのでとりあえず ChatGPT に「いい感じのロゴ作って〜」とお願いしていくつか出してもらった中で最もスッキリしたやつを選んだ。

ChatGPT 便利〜

ogen が v1 になっていたので変わったところも含めて試してみた

サマリ

現職で OpenAPI からのコード生成ツールとして ogen を採用して使っているのですが、この ogen がしばらく見ないうちに v1 (現時点の最新は v1.2.1) に進んでいたので、v1 以前との違い等々を調べてみました。

github.com

v1 以前と変わったところ

設定ファイルができた

ogen を実行するディレクトリと同階層に ogen.yml という設定ファイルが必要になりました。
この設定ファイルでは後述する生成するファイル(server や client) の対象を選択する設定や、コード生成時に発生するエラーを ignore したりすることができます。

オプションが変わった

まず最初に気づいたのはここでした。v1 以前とオプションが変わっています。

特に現職ではコード(特に API のリクエストやレスポンスの部分の)生成に ogen を利用しており、server や client といったコードの生成は利用していなかったのでのでちょっと困りました。

例. 元々使っていたオプションからなくなったのは以下

  • -no-client : Client のコードを生成しないオプション
  • -no-server : Server のコードを生成しないオプション

その他 -allow-remote-debug.noerr といったオプションもなくなっていました。

現在利用できるオプションは以下です。

flag provided but not defined: -no-client
Usage: ogen [options] <spec>
  -clean
        Clean generated files before generation
  -color
        Enable color logging (default true)
  -config string
        Path to config file
  -cpuprofile string
        Write cpu profile to file
  -loglevel value
        Zap logging level
  -memprofile string
        Write memory profile to this file
  -memprofilerate int
        If > 0, sets runtime.MemProfileRate (default -1)
  -package string
        Target package name (default "api")
  -target string
        Path to target dir (default "api")
  -v    Enable verbose logging
  -version
        Print version and exit

では従来のような Client や Server のコード生成をしない(抑制する)ための方法はどうするのか?というと、config ファイルに生成するファイルを enable する設定をかくという方式でした。
ちなみに再掲ですが、現職では Clinet や Server といった生成対象のコードをプロダクションでは利用しておらず、もっぱら Req/Resp のインターフェースの管理としての自動生成のみを使っていたので、新規で生成されるファイルは必要ありませんでしたので、最初オプションがなくなったときは困りました。

v1 以降は以下のような設定ファイルを書けばまずはデフォルトの生成対象を残した状態で、余計なコード生成はスキップさせることができます。

generator:
  features:
    disable_all: true

oneOf が正式にサポートされた

もともと oneOf はサポートされていましたが、プリミティブな型のみだったりしてたのがどうやら正式にサポートされたようです。

Sum type | ogen

ogen 拡張ができた。

Extentions というところを見るとわかりますが、OpenAPI ドキュメントを記載するときに、ogen のオプションを付けることができます。

個人的に便利だなと思ったのは以下です。

  • Custom Type Name をつけることができること。

    • ogen が生成するコードの schema を使うときに、適切にコンポーネントが切られていないと ogen 側での独自の命名規則(OperationId 等々を使っている)に沿って Struct が生成されてしまうのが少し気持ちわるかったのですが、このオプションを使うと名前を割り当てることができます。
  • Struct Field Tags で生成する schema に任意の tag を割り当てることができる。

    • 特に Go だと ORM を使うときにマッピングのための tag を打つケースが多いですが、OpenAPI の定義側に tag を指定することで、tag ごとコードを生成することができます。
    • ORM とかの定義を1から作らずともよいのでこれも良い機能だなと思いました。ORM のための Entity を Response でそのまま使うのか?という点は若干議論が分かれそうですが。

まとめ

OpenAPI を使うって言うとちょっともの好き感ありますが、実際枯れている仕様でもあるので選択肢に上がることも多いかと思います。
OpenAPI を使って自動生成を考えるときに、適切なライブラリ選定が難しい問題がありますが、その中でも ogen は注目していた一つで実際に商用環境でも使っていて十分使えそうだなと思っていたところで、 v1 になり色々便利な機能も増えていたので今回はその学習のためにブログを残しました。

また使っていく中で気づいたことがあったらまとめようと思います。

Go Conference 2024 に参加してきた

Go Conference 2024 に参加してきました。

個人としてはだいぶ久しぶりに大きなソフトウェアのカンファレンスに参加してきました。

コロナ禍でのオンライン開催での現地の熱気を感じれなくなったり、その後子供が生まれてそもそも週末に1人でなにかのイベントに行く、なんてことをあまりできなくなったのもあって久しくこういったイベントへの参加は控えていたのもあります。
そもそも共働きで、かつ奥さんが土日休みではないうちの場合、週末は原則ワンオペで、そのためにほとんどの予定を週末にいれること自体がなかなか難しく、週末や遠方のイベントというのは更に参加が難しくなってきているのもあります。

個人的に頭に残ったトピックは以下

  • イテレーターの話
    • Go を書き始めて以降、for loop で回してあれこれ処理する、っていうのが当たり前だと思っていたのでパラダイムの大きな変化を感じました。
    • 現職社内の勉強会でも何度かキャッチアップの機会はあったのですが、いまいち理解できていなかった自分がいて、そんななか tenntenn san の発表してもらったスライド (※1) を読んでようやく理解できました。(そもそもイテレーターというものへの理解が自分は浅かったのかなとも思います)

※1. 以下のスライドです。

https://audience.ahaslides.com/cl965inb88/review?lookback-tab=slides

  • slog の話

    • そろそろ使いたいなーと思っていたので、具体的な Handler の実装方法のライブコーディングは面白かったです。
    • LTSV の形式に対応した Handler を自作しないとなーと思って幾星霜。
  • swissTable の話

    • 社内でも話題に上がっていたので気になっていたトピックでした。
    • こういう深いところの知識がまだまだないのである程度自分の脳みそにインデックスを張っておける状態にしたいとも思いました。

子どものお風呂と晩ごはん準備があったのですべてのセッション、LT は聞かずに早めに離脱したのですが、結論としてだいぶ久しぶりだったけど行ってみてたくさん刺激をもらえたので頻繁に行くことはできないけど年に数回くらいであれば参加していきたいなと思いました。ブースでも何人か知り合いに挨拶できたのも良かったです。やっぱりリアルな場というのはオンラインにはない価値があって大事ですね。
こういうイベントに適度に参加すためにも、日々出かけさせてもらえる(特に週末)ように徳を積まないと行けないなとは思いますが。。。

Alfred -> Raycast に移行した

Overview

タイトルのとおりですが、PC のランチャーアプリを長年愛用していた Alfred を一旦辞めて流行りの Raycast に移行してみました。

www.raycast.com

特にこれ!というものはなかったのですが、流行りに乗ってみようという単純な動機です。

便利なところ

これはいいな!と思ったのは専用の Clipboard の History 機能と Note 機能があるところですかね。 これ自体は Alred にも合ったので別に Raycast である必要も特にないんですが。Note 機能については CotEditor を入れていてちょっとしたメモとかであれば都度開いてコピペしてたりしたのでそういう手間は省けそうでした。

あと Alfred だと有料版でフルに利用しないとできないことが Raycast だと無料版でもできる?あたりは魅力かなと思いましたが、自分の利用範囲では特に違いはないかなと思いました。

残念なところ

Alfred のときは Alfred の検索バーでスペースから始めると Finder のなかのディレクトリ、ファイル検索になる、という仕様があって、かなり重宝していたのですが、Raycast だとこの振る舞いを再現する事が自分が調べた範囲ではできませんでした。
ex. 例えば Download のディレクトリを一発で開くのに Alfred は downloadスペース + {対象のディレクトリ} と打ち込むと Finder のダウンロードディレクトリまで一発で飛べましたが Raycast では Search Files だけでディレクトリへのジャンプではなくダイレクトにファイルにジャンプしてしまいます。

その他にも Raycast ならではの便利な機能もあるので使い続けてみようかなと思いましたが、Alfred でできることはだいたい Raycast でもできると思っていただけにちょっと残念。もしかしたらやり方あるんですかね?

結果

自分の使ってる範囲で使用感は特に変わりませんでした。

See Also

zenn.dev

Prezto をやめて sheldon に移行して zsh の起動速度を改善してみた

これはなに?

zshプラグインマネージャーとして Prezito を使っていましたがこれをやめて sheldon に移行したログです。

sheldon とは?

Rust 製のシェルのプラグインマネージャー。
Rust 製だけあってそもそも高速です。

brew でインストールできますが cargo でインストールできるのもいいですね。

ドキュメントも丁寧にまとめっていて良きです。

sheldon.cli.rs

動機

zsh の起動に大体 600ms 程度かかっていたのがだいぶ気になってはいたものの、高速化するのもだいぶ手間だな〜とか思っていたときに以下のエントリを読んだのがきっかけです。

karamaru-alpha.com

実際にはここで紹介されているうちの1つ(sheldon の採用)のみしか行っていませんが、体感でもだいぶ早くなりました。

やったこと

  1. prezto を削除して sheldon を入れる。
  2. zsh-defer をいれて遅延ロード処理を追加。
  3. sheldon source 読み込み結果をキャッシュする。
  4. zshrc の設定を更新する。

これらはすべて参考にしたサイトに記載されていた内容と 公式の Docs を参考にして進めました。

結果

sheldon 周りの設定を置き換えたのみの Before/After はこんな感じでした。

# before
zsh -i -c exit 
0.11s user 0.15s system 59% cpu 0.635 total

# after
time zsh -i -c exit
zsh -i -c exit  0.07s user 0.13s system 52% cpu 0.377 total

大体 600~700ms かかっていたのが 300~400ms まで改善しました。

元記事では 20ms まで落とせたらしいですがそこまでは行きませんでした。もっと早くできそうな気もしないでもないですが、zshrc で eval $(...env init -) というやってる処理が重たく、この辺はもう少し改善の余地がありそうだなと思いました。

というのも zsh の plugin 読み込みが遅いのかなと思っていたんですが、そこだけ切り取ると 30ms 程度で終わっていたので zshrc の読み込みに時間がかかってそう、というところに気がついたのがきっかけです。

time sheldon source
source "/Users/hiromichiema/.local/share/sheldon/repos/github.com/romkatv/zsh-defer/zsh-defer.plugin.zsh"
zsh-defer source "/Users/hiromichiema/.local/share/sheldon/repos/github.com/zsh-users/zsh-autosuggestions/zsh-autosuggestions.zsh"
zsh-defer source "/Users/hiromichiema/.local/share/sheldon/repos/github.com/zsh-users/zsh-syntax-highlighting/zsh-syntax-highlighting.plugin.zsh"
sheldon source  0.01s user 0.01s system 60% cpu 0.027 total

ちなみに rbenv とかそのへんを削除してみたら 100ms 弱早くなったのでやっぱり zshrc の読み込みもある程度少なくしておく必要がありそうで、試しにいつ設定したか忘れていた pyenv の設定をまるっと削除したら 100ms 以下まで起動を高速化できました。
env 系の読み込み処理を入れてる場合は遅延実行や断捨離を検討してみてもいいかもしれません。

time zsh -i -c exit
zsh -i -c exit  0.02s user 0.03s system 76% cpu 0.077 total

はまったところ

github からの プラグインを DL を指定するときは ssh でデフォルト設定していないと Git の認証で落ちる。

.ssh/id_rsa を使っていないとこのエラーが発火します。 これの回避のためには .gitconfig にて sshCommand を指定する必要があります。以下のような設定を追記します。

[core]
    sshCommand = "ssh -i /path/to/your/private/key"

特に GitHub に登録している鍵をデフォルトの id_rsa とは別で分けてるケースで必要になります。
これに引っかかると sheldon source で設定を更新しても反映されず、 GitHub の認証で落ちるエラーが出力されます。

remote (https 経由)でプラグインを DL するときは raw の URL を指定しないといけない

これも結構ハマりました。GitHub を指定しないときは remote で repository の URL (正確には *.plugin.zsh のファイルまで)を指定すると HTTP 経由でプラグインを clone してくれるのですが、URL は GitHub のURL ではなく、raw の URL が必要でした。
これは issue にも起票してありましたけど結構罠でした。

zsh-defer を使う場合には zsh-defer を最初に読み込む

言われると当たり前ですが遅延読み込みをする場合には遅延読み込みをするライブラリを先に読み込んで後続のライブラリを遅延読み込み行う事が必要です。

shell = "zsh"
apply = ["defer"]

[templates]
defer = "{% for file in files %}zsh-defer source \"{{ file }}\"\n{% endfor %}"

[plugins.zsh-defer]
github = "romkatv/zsh-defer"
apply = ["source"]

上記のように apply=["defer"] を使う場合には zsh-defer を先に読み込んでおきます。

theme の設定

theme の設定は Examples に記載されていますが、選択肢は少ないです。 p10k だけアレばある程度色々できるので自分は p10k を利用しています。

sheldon コマンドでインストールしたら p10k configure で設定が可能です。

参考にしたサイト

zenn.dev

zenn.dev

zenn.dev

zenn.dev

github.com

github.com