1. 概要
duckdb: レイヤーが利用する DuckDB-Wasm について、以下を検証・修正した。
- 本当に必要時(初回クエリ時)に遅延ロードされるか
- 一度ロードした後、再ダウンロード・再初期化なしに継続利用できるか
- ブラウザ再起動・再訪問時にキャッシュが生きるか
2. 発見した問題
2.1 パネル iframe はレイヤー追加のたびに破棄される
DuckDB-Wasm は当初 layers-and-tiles-list の UI iframe 内でロードしていた。
しかし addGeoJsonValueLayer() が無条件に safeShowUI()
(reearth.ui.show(getUI(), {extended:true}))を呼ぶため、
レイヤー追加のたびに iframe が再生成され window._ddbModule
(DuckDB インスタンス)が破棄されていた。
2.2 旧構成の計測値
| 呼び出し | 所要時間 | 状態 |
|---|---|---|
| 1回目(cold) | 18.7s | wasm DL + 初期化 |
| 2回目(iframe 再生成後) | 22.1s | 再初期化発生 |
| 3回目 | 13.4s | 再初期化発生 |
jsDelivr は cache-control: immutable, max-age=31536000 を返すが、
サンドボックス iframe では HTTP キャッシュが効かず(後述)、wasm インスタンス化 +
httpfs/spatial/parquet 拡張の INSTALL/LOAD が毎回フルに走っていた。
3. 実装: 永続ホストへの移行
DuckDB の実行を basemap-widget の UI iframe に移した。
この widget は ui.show() を初期化時に一度だけ呼ぶため iframe が永続し、
パネルの再描画(safeShowUI)をまたいでインスタンスが生存する。
パネル ddbPost ── extensionMessage(ddbExec) ──> basemap 拡張
└ postToUI(ddbLoad) → 永続 iframe (wasm 実行)
<── extensionMessage(ddbAck / ddbResult) ──
↓
パネル handleDdbResult
ddbAck: ホスト iframe がクエリを受理した即時応答ddbResult: クエリ完了時の GeoJSON / エラー返送- フォールバック:
ddbAckが 3 秒以内に来なければ (widget 非表示等でホスト iframe 不在)パネル自身の iframe で実行。 パネル側にも同一のddbLoadハンドラを温存している window._ddbModuleは Promise キャッシュ。初期化失敗時は null に戻り再試行可能
変更ファイル: geo_suite/src/basemap-widget.ts(ddbExec 中継・ddbLoad 実行・ddbAck/ddbResult 返送)、
geo_suite/src/layers-and-tiles-list.ts(ddbPost ホスト優先+フォールバック、handleDdbResult、ddbAck/ddbResult 受信)
4. セッション内の実測(修正後)
| 確認項目 | 結果 |
|---|---|
| 初回クエリ前 | window._ddbModule = undefined → 遅延ロード確認 |
| 1回目(cold) | 17.7s — wasm DL + 初期化 + 拡張ロード |
| 2回目(パネル再描画後) | 9.5s — 同一インスタンス(リモート parquet 取得+クエリのみ) |
| 3回目 | 3.1s — 同上 |
- basemap iframe の CDP ターゲット ID は全工程で不変(iframe 再生成なし)
_ddbModuleにタグを付けて確認 → パネル再生成をまたいで同一オブジェクトを維持- wasm 再初期化・再ダウンロードは発生しない
5. ブラウザ再起動・再訪の検証
5.1 実測
| セッション | 所要時間 | 解釈 |
|---|---|---|
| A(新規プロファイル) | 15.3s | コールド |
| B(再起動・同一プロファイル) | 16.9s | コールドと同等 — キャッシュ未利用 |
5.2 原因の特定(CDP 実測)
- 同一 iframe・同一セッション内で同一 URL を 2 回 fetch →
2回目も
transferSize=4456(全バイト再転送)。HTTP キャッシュが機能しない caches.open()→SecurityError: Cache storage is disabled because the context is sandboxed and lacks the 'allow-same-origin' flag- iframe 属性:
sandbox="allow-scripts allow-downloads allow-popups"—allow-same-originが無く opaque origin になる
sw-duckdb.js(Service Worker + CacheStorage で wasm を永続キャッシュ)方式は
構造上移植できない。
6. 回避策の検討
| 案 | 判定 | 理由 |
|---|---|---|
| HTTP キャッシュ依存 | 不可 | opaque origin で無効(実測) |
| Service Worker + CacheStorage(KASUGAI 方式) | 不可 | sandbox で SecurityError |
拡張側 clientStorage に wasm バイナリ保存→次回注入 | 理論上可能だが非推奨 | wasm+worker+拡張で ~6–8MB → base64 ~10MB 超。localStorage 系上限・メッセージ橋のバイナリ転送制約あり |
| 永続 iframe ホスト(basemap-widget) | 採用 | セッション内の再初期化を排除(本実装) |
7. フォーマット別の使いどころ(GeoJSON / FlatGeobuf / GeoParquet / duckdb:)
GeoSuite にはベクトルデータの読込経路が4種類ある。利用者視点で 「ファイルサイズ」「ダウンロード→描画の速さ」「選び方の目安」をまとめる。
7.1 フォーマット特性
| 構文 | 実体 | 圧縮 | 読込単位 | 備考 |
|---|---|---|---|---|
geojson: | テキスト (JSON) | なし | 全量 | 標準フォーマット。どのツールでも出力できる |
flatgeobuf: / fgb: | バイナリ+空間インデックス | なし | 全量(本実装) | 本来は index による bbox 読みも可能な規格だが、現実装は全件デコード |
parquet: / geoparquet: | カラムナー (Parquet) | snappy 等 | 全行・全列(hyparquet) | Range 対応だが実質全件読み。属性列が多いほど圧縮が効く |
duckdb: | 同上 (Parquet 等) | 同上 | 行グループ単位の部分読込 | where=/limit=/columns=/bbox=auto を SQL プッシュダウン。covering.bbox で非該当行グループは転送自体が発生しない |
7.2 ファイルサイズの比較(実測 + 目安)
| データセット | GeoJSON | FlatGeobuf | GeoParquet |
|---|---|---|---|
| countries(179地物・実測) | 258 KB(100%) | 205 KB(約80%) | 〜(別データのため未計測) |
| 一般的な目安: FlatGeobuf ≈ GeoJSON の 70–90%、GeoParquet(snappy) ≈ GeoJSON の 30–60%。 属性列が多い・座標が単純(点群など)ほど Parquet の列圧縮が効く。 逆に地物数が少ない小さいファイルでは、Parquet のフッタ/メタデータの固定オーバーヘッドが効き GeoJSON より大きくなることもある(例: 5地物の example.parquet = 28KB)。 | |||
ファイルサイズが小さい順の目安: GeoParquet < FlatGeobuf < GeoJSON(中〜大ファイルの場合)
7.3 ダウンロード→描画までの速さ
小さいファイル(~数MB 以下)
| 経路 | 構成 | 実測(countries 205KB, 179地物) |
|---|---|---|
geojson: | DL + JSON.parse のみ | 最速(デコーダ不要、~0.5s) |
fgb: / parquet: | DL + デコーダ import + デコード | DL 494ms + モジュールロード 1.6s + デコード 31ms モジュールは初回のみ。以後はデコードのみで速い |
duckdb: | DL + wasm 初期化 + クエリ | wasm 初期化 ~15s(セッション1回目のみ)が支配的 → 小さいファイルでは割に合わない |
小さいファイルでは geojson: が最速。バイナリ系はモジュールロード分だけ初回に負ける。
大きいファイル(~50–100MB 以上・属性多数・表示範囲で絞れる)
| 経路 | 転送量 | 描画まで |
|---|---|---|
geojson: | 全量(100%) | 最悪 — 全量DL + JSON.parse がメインスレッドをブロック |
fgb: | ~70–90% | ストリームデコードだが全件転送は必要 |
parquet: | ~30–60% | 転送量は減るが全行デコード |
duckdb: | 該当行グループのみ(桁違いに小さいことも) | where/bbox 絞込み後の少量転送 + wasm は2回目以降 warm |
巨大ファイルかつ「表示範囲だけ欲しい」「条件で絞りたい」用途では
duckdb: が圧倒的に速い。
15s の初期化はセッションに1回だけなので、複数の duckdb レイヤーや
bbox=auto のカメラ連動再クエリを使うほど元が取れる。
7.4 選び方の目安
| ファイルサイズ / 用途 | 推奨 | 理由 |
|---|---|---|
| 〜10MB 程度 | geojson: または fgb: | デコード基盤なしで最速。手軽さ優先 |
| ~10–50MB | fgb: / parquet: | 転送量削減が効いてくる。属性が多いなら parquet |
| ~50MB 以上、または表示範囲・条件で絞って使う | duckdb: | 部分読込で転送量を最小化。15s 初期化を回収できる |
属性で検索・集計したい(where=) | duckdb: | SQL プッシュダウンは duckdb 経路のみ |
| 複数レイヤーをまとめてクエリする | duckdb: | wasm 初期化はセッション1回で使い回せる |
7.5 その他の留意点
- 変換: GeoJSON → FlatGeobuf / GeoParquet は QGIS・ogr2ogr・geopandas 等で変換可能
- 属性が多いデータ: カラムナーの Parquet が圧倒的に有利。
duckdb:のcolumns=で必要列だけ取り込める - 全量読込系はメモリ常駐: geojson/fgb/parquet 経路は FeatureCollection を丸ごとメモリに持つ。
巨大データを「一部だけ表示」するなら
duckdb:一択 - GeoParquet 1.1 の covering.bbox 付きファイルが理想:
duckdb:で自動検出され、行グループスキップ+表示範囲連動が最大効果を発揮する。 無い場合はcovering=xmin,xmax,ymin,ymax列指定か、空間フィルタ無しの where/limit のみになる
8. 関連する環境制約・今回の発見
調査過程で判明した、プラグイン開発上の重要な制約を記録しておく。
8.1 上流への機能要望(Issue 登録済み)
8.2 QuickJS 拡張コンテキストには setTimeout が無い
拡張側(QuickJS サンドボックス)で遅延処理が必要な場合は、既存のタイマー委譲機構を使うこと:
waitViaUiTimer(ms) → UI iframe がブラウザの setTimeout を実行 →
timerDone で拡張へ返送。拡張側で直接 setTimeout を書くと
ReferenceError になる(DuckDB のカメラデバウンスで実際に踏んだ)。
8.3 Workbench の resize script 注入バグ
Workbench が iframe HTML の </body> 直前に _zushi_resize スクリプトを
挿入する際、JS 文字列リテラル内の '</body></html>' に誤マッチし、
スクリプトの中間へ注入される → 注入された </script> で script 要素が早期終了し、
iframe の JS が途中で切れて無反応になる。
対策: 生成 HTML の JS 文字列リテラル内には生の </body>/</script> を
書かず '<\/body><\/html>' のようにスラッシュをエスケープする(ai-chat.ts で修正済み)。
8.4 サンドボックスでも動作するもの
- blob URL Worker は生成可能 — DuckDB-Wasm のワーカー起動が通る
crossOriginIsolated=falseでも DuckDB はeh(非スレッド)バンドルで動作。 httpfs / spatial / parquet 拡張も全て INSTALL/LOAD 成功- ESM 動的
import()(jsDelivr CDN)は制限なく使える
つまり「重い WASM が動くか」は問題ないが「永続化できない」ことが根本的な制約。
9. 結論
検証: tsc --noEmit 0 エラー / inline script チェック全通過 /
Workbench AI テスト 44/44 全パス(コンソールエラーなし)