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

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

このコミットは、Go言語のtimeパッケージがタイムゾーン情報(zoneinfo)を管理する方法を、個別のファイル群から非圧縮のzipファイル形式に切り替えるものです。これにより、タイムゾーンデータの配布と読み込みの効率が向上します。

コミット

  • コミットハッシュ: cb5e181fe7ba9b7412fc661e57551a0f776c294a
  • Author: Russ Cox rsc@golang.org
  • Date: Sun Feb 19 03:16:20 2012 -0500
  • コミットメッセージ:
    time: switch to using (uncompressed) zoneinfo zip file
    
    Removal of old zoneinfo files is a separate CL due to its size.
    
    R=golang-dev, bradfitz, r
    CC=golang-dev
    https://golang.org/cl/5676100
    

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

https://github.com/golang/go/commit/cb5e181fe7ba9b7412fc661e57551a0f776c294a

元コミット内容

time: switch to using (uncompressed) zoneinfo zip file

このコミットは、Go言語のtimeパッケージがタイムゾーン情報を扱う方法を、従来の個別のzoneinfoファイルから、非圧縮のzipファイル形式に移行することを目的としています。これにより、タイムゾーンデータの配布と読み込みの効率化が図られます。

変更の背景

Go言語のtimeパッケージは、正確な時刻計算のためにタイムゾーン情報(zoneinfo)を必要とします。この情報は、IANA Time Zone Database(tzdata)から提供されるもので、世界中のタイムゾーンルールが定義されています。

従来のGoのtimeパッケージでは、これらのzoneinfoファイルが個別のファイルとして$GOROOT/lib/time/zoneinfoディレクトリに配置されていました。しかし、タイムゾーンファイルの数は500を超え、個々のファイルとして配布・管理することは、以下のような課題を抱えていました。

  1. 配布の複雑さ: 多数の小さなファイルを配布することは、パッケージングやデプロイの際にオーバーヘッドが生じます。
  2. ファイルI/Oのオーバーヘッド: タイムゾーン情報を読み込む際に、多数のファイルを開閉する必要があり、I/O性能に影響を与える可能性がありました。
  3. ファイルサイズの増大: 個々のファイルとして管理すると、ファイルシステム上のメタデータなどにより、全体としてのディスク使用量が増加する傾向にありました。

これらの課題を解決するため、このコミットでは、すべてのzoneinfoファイルを単一の非圧縮zipファイルにまとめるアプローチが採用されました。zipファイルは、個々のファイルを効率的に格納できるコンテナ形式であり、特に非圧縮であれば、ファイルの内容を直接読み込むことが容易になります。また、zipファイルの集中ディレクトリ(Central Directory)を利用することで、個々のファイルへのアクセスが高速化されるという利点もあります。

この変更は、Goアプリケーションの配布サイズを最適化し、タイムゾーン情報の読み込み性能を向上させることを目的としています。

前提知識の解説

IANA Time Zone Database (tzdata)

IANA Time Zone Database(以前はtzdataまたはzoneinfoデータベースとして知られていました)は、世界中のタイムゾーンと夏時間(Daylight Saving Time, DST)の歴史的な変更を記録した協調的なデータベースです。これは、コンピュータシステムが正確な現地時間を決定するために広く使用されています。データベースは、タイムゾーンのルール、オフセット、DSTの開始・終了日などの情報を含んでおり、これらの情報はバイナリ形式の「zoneinfoファイル」として配布されます。

zoneinfoファイル

zoneinfoファイルは、IANA Time Zone Databaseの情報をバイム形式で表現したものです。各ファイルは特定のタイムゾーン(例: America/New_YorkAsia/Tokyo)に対応し、そのタイムゾーンにおける過去、現在、未来のUTCからのオフセット、夏時間の適用ルール、タイムゾーン名の変更履歴などが含まれています。Go言語のtimeパッケージは、これらのファイルを読み込むことで、指定されたタイムゾーンにおける正確な時刻変換を行います。

Go言語のtimeパッケージとタイムゾーン

Go言語のtimeパッケージは、日付と時刻を扱うための基本的な機能を提供します。time.Location型は特定のタイムゾーンを表し、time.LoadLocation関数を使用して、名前(例: "America/New_York")からLocationオブジェクトをロードします。このロード処理の際に、システムにインストールされているzoneinfoファイルを検索し、読み込む必要があります。

zipファイル形式

zipファイル形式は、データ圧縮とアーカイブのための一般的なファイル形式です。複数のファイルを単一のアーカイブファイルにまとめることができます。zipファイルは、各ファイルのデータと、アーカイブ内のすべてのファイルに関する情報(ファイル名、サイズ、圧縮方法、オフセットなど)を含む「集中ディレクトリ(Central Directory)」で構成されます。この集中ディレクトリは通常、zipファイルの末尾に配置されており、これによりアーカイブ全体をスキャンすることなく、特定のファイルの情報に素早くアクセスできます。このコミットでは、非圧縮のzipファイルが使用されるため、個々のファイルは圧縮されずに格納されますが、集中ディレクトリによる高速なファイル検索の恩恵は受けられます。

技術的詳細

このコミットの主要な技術的変更点は、Goのtimeパッケージがタイムゾーンデータを読み込む方法を、個別のzoneinfoファイルから単一の非圧縮zoneinfo.zipファイルに切り替えたことです。

  1. zoneinfo.zipの導入:

    • lib/time/zoneinfo.zipという新しいファイルが追加されました。これは、すべてのzoneinfoファイルを非圧縮でまとめたzipアーカイブです。
    • lib/time/READMEが更新され、zoneinfo.zipがタイムゾーンファイルを含むアーカイブであることが明記されました。
  2. update.bashスクリプトの変更:

    • lib/time/update.bashは、IANA Time Zone Databaseから最新のタイムゾーンデータを取得し、Goが使用するzoneinfoファイルを生成するためのスクリプトです。
    • このスクリプトが変更され、個別のzoneinfoファイルをzoneinfo/ディレクトリに生成する代わりに、それらをまとめてzoneinfo.zipという単一のファイルとして出力するようになりました。
    • 具体的には、rm -rf zoneinfo workrm -rf workに変更され、mkdir zoneinfocd workの後に移動しました。
    • make CFLAGS=-DSTD_INSPIRED AWK=awk TZDIR=../zoneinfo posix_onlymake CFLAGS=-DSTD_INSPIRED AWK=awk TZDIR=zoneinfo posix_onlyに変更され、zoneinfoディレクトリがworkディレクトリ内に作成されるようになりました。
    • 最終的に、生成されたzoneinfoディレクトリの内容をzip -0 -r ../../zoneinfo.zip *コマンドで非圧縮(-0オプション)のzipファイルとしてアーカイブし、プロジェクトルートのzoneinfo.zipとして保存するようになりました。
  3. timeパッケージのファイルI/O抽象化:

    • src/pkg/time/sys_plan9.go, src/pkg/time/sys_unix.go, src/pkg/time/sys_windows.goといったOS固有のファイルI/Oを扱うファイルに、open, closefd, preadnといった新しい関数が追加されました。
    • これらの関数は、ファイルディスクリプタ(uintptr)を直接操作し、指定されたオフセットからデータを読み込む(preadn)ための低レベルなI/O操作を提供します。これは、zipファイル内の特定のエントリにアクセスするために必要となる、ランダムアクセス読み込みを可能にするためです。
    • readFile関数も、これらの新しい低レベルI/O関数を使用するように変更されました。
  4. zoneinfo_read.goにおけるzipファイル読み込みロジックの追加:

    • src/pkg/time/zoneinfo_read.goloadZoneZipという新しい関数が追加されました。この関数は、指定されたzipファイルから特定のタイムゾーン名に対応するzoneinfoデータを読み込むためのロジックを含んでいます。
    • loadZoneZipは、zipファイルの集中ディレクトリを解析し、目的のタイムゾーンファイルのエントリを見つけ、そのオフセットとサイズに基づいてファイルの内容を直接読み込みます。
    • zipファイルのヘッダー(zecheader, zcheader, zheader)や、ファイルエントリの構造を解析するためのバイト操作(get4, get2)が実装されています。
    • 非圧縮のzipファイルのみをサポートし、圧縮されたエントリが見つかった場合はエラーを返します。
  5. LoadLocation関数の変更:

    • src/pkg/time/zoneinfo.goLoadLocation関数が変更され、ZONEINFO環境変数やデフォルトの検索パスで指定されたパスが.zipで終わる場合、新しいloadZoneZip関数を呼び出すようになりました。
    • これにより、LoadLocationは、従来のディレクトリ構造のzoneinfoファイルと、新しいzoneinfo.zipファイルの両方からタイムゾーン情報をロードできるようになります。
    • $GOROOT/lib/time/zoneinfoの代わりに$GOROOT/lib/time/zoneinfo.zipが参照されるようになりました。

この変更により、Goのtimeパッケージは、タイムゾーンデータをより効率的に配布・管理できるようになり、アプリケーションの起動時やタイムゾーンロード時のI/O性能が改善されることが期待されます。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  1. lib/time/update.bash:

    • zoneinfoファイルを生成し、zoneinfo.zipを作成するためのシェルスクリプト。
    • 個別のzoneinfoディレクトリの作成と管理から、zoneinfo.zipの生成に切り替わった。
  2. lib/time/zoneinfo.zip:

    • 新しく追加されたバイナリファイル。すべてのzoneinfoデータが非圧縮で含まれる。
  3. src/pkg/time/sys_plan9.go, src/pkg/time/sys_unix.go, src/pkg/time/sys_windows.go:

    • 各OSにおける低レベルのファイルI/O操作(open, closefd, preadn)が追加された。
    • readFile関数がこれらの新しいI/O関数を使用するように変更された。
  4. src/pkg/time/zoneinfo.go:

    • LoadLocation関数が、zoneinfo.zipファイルからの読み込みをサポートするように変更された。
    • loadZoneFile関数のシグネチャが変更され、ディレクトリとファイル名を別々に受け取るようになった。
  5. src/pkg/time/zoneinfo_read.go:

    • loadZoneZip関数が追加され、zipファイルからzoneinfoデータを解析・読み込むロジックが実装された。
    • loadZoneFile関数が、パスが.zipで終わる場合にloadZoneZipを呼び出すように変更された。
  6. src/pkg/time/zoneinfo_unix.go, src/pkg/time/zoneinfo_windows.go:

    • initTestingZoneloadLocationなどの関数が、zoneinfo.zipを参照するようにパスが変更された。

コアとなるコードの解説

lib/time/update.bash

 # ... (前略) ...
 set -e
-rm -rf zoneinfo work
-mkdir zoneinfo work
+rm -rf work
+mkdir work
 cd work
+mkdir zoneinfo # zoneinfoディレクトリをwork内に作成
 curl -O http://www.iana.org/time-zones/repository/releases/tzcode$CODE.tar.gz
 curl -O http://www.iana.org/time-zones/repository/releases/tzdata$DATA.tar.gz
 tar xzf tzcode$CODE.tar.gz
 tar xzf tzdata$DATA.tar.gz

 # ... (中略) ...

-make CFLAGS=-DSTD_INSPIRED AWK=awk TZDIR=../zoneinfo posix_only
+make CFLAGS=-DSTD_INSPIRED AWK=awk TZDIR=zoneinfo posix_only # TZDIRをwork/zoneinfoに設定
 # ... (中略) ...
-size=$(ls -l ../zoneinfo/America/Los_Angeles | awk '{print $5}')
+size=$(ls -l zoneinfo/America/Los_Angeles | awk '{print $5}') # work/zoneinfo内のファイルサイズをチェック
 # ... (中略) ...
-cd ..
-hg addremove zoneinfo
+cd zoneinfo # work/zoneinfoに移動
+rm -f ../../zoneinfo.zip # 既存のzipファイルを削除
+zip -0 -r ../../zoneinfo.zip * # 非圧縮でzipファイルを作成
+cd ../.. # プロジェクトルートに戻る
+
 echo
 if [ "$1" == "-work" ]; then
  echo Left workspace behind in work/.
 else
  rm -rf work
 fi
-echo New time zone files in zoneinfo/.
+echo New time zone files in zoneinfo.zip. # 出力メッセージの変更

このスクリプトは、IANAのタイムゾーンデータベースからソースコードとデータをダウンロードし、zicコンパイラを使用してzoneinfoファイルを生成します。変更点としては、生成された個々のzoneinfoファイルをwork/zoneinfoディレクトリに配置した後、その内容をzip -0 -r ../../zoneinfo.zip *コマンドで非圧縮のzoneinfo.zipファイルとしてアーカイブするようになりました。これにより、Goの配布物には単一のzipファイルが含まれることになります。

src/pkg/time/sys_*.go (例: sys_unix.go)

 package time

 import (
 	"errors"
 	"syscall"
 )

 // ... (readFile関数など、既存のコード) ...

 func open(name string) (uintptr, error) {
 	fd, err := syscall.Open(name, syscall.O_RDONLY, 0)
 	if err != nil {
 		return 0, err
 	}
 	return uintptr(fd), nil
 }

 func closefd(fd uintptr) {
 	syscall.Close(int(fd))
 }

 func preadn(fd uintptr, buf []byte, off int) error {
 	whence := 0
 	if off < 0 {
 		whence = 2 // SEEK_END
 	}
 	if _, err := syscall.Seek(int(fd), int64(off), whence); err != nil {
 		return err
 	}
 	for len(buf) > 0 {
 		m, err := syscall.Read(int(fd), buf)
 		if m <= 0 {
 			if err == nil {
 				return errors.New("short read")
 			}
 			return err
 		}
 		buf = buf[m:]
 	}
 	return nil
 }

これらのファイルには、OS固有の低レベルファイルI/O関数が追加されました。

  • open(name string) (uintptr, error): 指定されたファイルを読み取り専用で開き、ファイルディスクリプタ(uintptr型)を返します。
  • closefd(fd uintptr): 指定されたファイルディスクリプタを閉じます。
  • preadn(fd uintptr, buf []byte, off int) error: 指定されたファイルディスクリプタから、指定されたオフセット(off)からbufの長さ分のデータを読み込みます。これは、zipファイル内の特定のエントリに直接シークして読み込むために不可欠な機能です。

src/pkg/time/zoneinfo.go

 // ... (既存のコード) ...

 // LoadLocation looks in the directory or uncompressed zip file
 // named by the ZONEINFO environment variable, if any, then looks in
 // known installation locations on Unix systems,
 // and finally looks in $GOROOT/lib/time/zoneinfo.zip.
 func LoadLocation(name string) (*Location, error) {
 	// ... (既存のコード) ...
 	if zoneinfo != "" {
-		if z, err := loadZoneFile(zoneinfo + "/" + name); err == nil {
+		if z, err := loadZoneFile(zoneinfo, name); err == nil { // loadZoneFileの呼び出し方が変更
 			z.name = name
 			return z, nil
 		}
 	}
 	// ... (既存のコード) ...
 }

LoadLocation関数は、タイムゾーン名を元にLocationオブジェクトをロードするGoの公開APIです。このコミットでは、loadZoneFile関数の呼び出し方が変更され、zoneinfo(ディレクトリまたはzipファイルのパス)とname(タイムゾーン名)を別々の引数として渡すようになりました。また、コメントが更新され、$GOROOT/lib/time/zoneinfo.zipが最終的な検索パスとして追加されたことが示されています。

src/pkg/time/zoneinfo_read.go

 // ... (既存のコード) ...

-func loadZoneFile(name string) (l *Location, err error) {
+func loadZoneFile(dir, name string) (l *Location, err error) { // シグネチャ変更
+	if len(dir) > 4 && dir[len(dir)-4:] == ".zip" { // パスが.zipで終わるかチェック
+		return loadZoneZip(dir, name) // zipファイルの場合、loadZoneZipを呼び出す
+	}
+	if dir != "" {
+		name = dir + "/" + name // ディレクトリの場合、パスを結合
+	}
 	buf, err := readFile(name)
 	if err != nil {
 		return
 	}
 	return loadZoneData(buf)
 }

 // There are 500+ zoneinfo files.  Rather than distribute them all
 // individually, we ship them in an uncompressed zip file.
 // Used this way, the zip file format serves as a commonly readable
 // container for the individual small files.  We choose zip over tar
 // because zip files have a contiguous table of contents, making
 // individual file lookups faster, and because the per-file overhead
 // in a zip file is considerably less than tar's 512 bytes.

 // get4 returns the little-endian 32-bit value in b.
 func get4(b []byte) int {
 	if len(b) < 4 {
 		return 0
 	}
 	return int(b[0]) | int(b[1])<<8 | int(b[2])<<16 | int(b[3])<<24
 }

 // get2 returns the little-endian 16-bit value in b.
 func get2(b []byte) int {
 	if len(b) < 2 {
 		return 0
 	}
 	return int(b[0]) | int(b[1])<<8
 }

 func loadZoneZip(zipfile, name string) (l *Location, err error) {
 	fd, err := open(zipfile) // zipファイルを開く
 	if err != nil {
 		return nil, errors.New("open " + zipfile + ": " + err.Error())
 	}
 	defer closefd(fd)

 	const (
 		zecheader = 0x06054b50 // End of Central Directory Record signature
 		zcheader  = 0x02014b50 // Central File Header signature
 		ztailsize = 22         // End of Central Directory Record size

 		zheadersize = 30       // Local File Header size
 		zheader     = 0x04034b50 // Local File Header signature
 	)

 	// Read End of Central Directory Record to find Central Directory offset and size
 	buf := make([]byte, ztailsize)
 	if err := preadn(fd, buf, -ztailsize); err != nil || get4(buf) != zecheader {
 		return nil, errors.New("corrupt zip file " + zipfile)
 	}
 	n := get2(buf[10:])   // Number of entries in Central Directory
 	size := get4(buf[12:]) // Size of Central Directory
 	off := get4(buf[16:])  // Offset of Central Directory

 	// Read Central Directory
 	buf = make([]byte, size)
 	if err := preadn(fd, buf, off); err != nil {
 		return nil, errors.New("corrupt zip file " + zipfile)
 	}

 	for i := 0; i < n; i++ {
 		// Parse Central File Header
 		// ... (zipエントリの構造解析ロジック) ...
 		if get4(buf) != zcheader {
 			break
 		}
 		meth := get2(buf[10:]) // Compression method
 		// ... (他のフィールドの取得) ...
 		zname := buf[46 : 46+namelen] // File name
 		// ... (次のエントリへのポインタ更新) ...

 		if string(zname) != name { // 目的のファイル名かチェック
 			continue
 		}
 		if meth != 0 { // 非圧縮(method 0)以外はエラー
 			return nil, errors.New("unsupported compression for " + name + " in " + zipfile)
 		}

 		// Read Local File Header to get actual file data offset
 		// ... (zip per-file headerの構造解析ロジック) ...
 		buf = make([]byte, zheadersize+namelen)
 		if err := preadn(fd, buf, off); err != nil ||
 			get4(buf) != zheader ||
 			get2(buf[8:]) != meth ||
 			get2(buf[26:]) != namelen ||
 			string(buf[30:30+namelen]) != name {
 			return nil, errors.New("corrupt zip file " + zipfile)
 		}
 		xlen = get2(buf[28:])

 		// Read actual file data
 		buf = make([]byte, size)
 		if err := preadn(fd, buf, off+30+namelen+xlen); err != nil {
 			return nil, errors.New("corrupt zip file " + zipfile)
 		}

 		return loadZoneData(buf) // 読み込んだデータを解析
 	}

 	return nil, errors.New("cannot find " + name + " in zip file " + zipfile)
 }

このファイルは、タイムゾーンデータの読み込みロジックを扱います。

  • loadZoneFile関数のシグネチャが変更され、dir(ディレクトリまたはzipファイルのパス)とname(タイムゾーン名)を受け取るようになりました。dir.zipで終わる場合、新しく追加されたloadZoneZip関数を呼び出します。
  • loadZoneZip関数は、このコミットの核心部分です。
    • まず、sys_*.goで定義されたopen関数を使ってzipファイルを開きます。
    • 次に、zipファイルの末尾にある「End of Central Directory Record」をpreadnで読み込み、集中ディレクトリのオフセットとサイズを取得します。
    • 集中ディレクトリを読み込み、各ファイルエントリを解析します。目的のタイムゾーン名(name)と一致するエントリを見つけます。
    • 圧縮メソッドが0(非圧縮)であることを確認します。それ以外の場合はエラーを返します。
    • 見つかったエントリの「Local File Header」を読み込み、実際のファイルデータが始まるオフセットを特定します。
    • 最後に、preadnを使ってそのオフセットから実際のzoneinfoデータを読み込み、loadZoneData関数に渡して解析させます。
    • 目的のファイルが見つからない場合や、zipファイルが破損している場合はエラーを返します。

これらの変更により、Goのtimeパッケージは、単一のzoneinfo.zipファイルから効率的にタイムゾーン情報をロードできるようになり、配布と実行時のパフォーマンスが向上しました。

関連リンク

参考にした情報源リンク