Skip to content

Commit c907640

Browse files
committed
Send WebAssembly binary over Jupyter WebSocket
1 parent 599a13e commit c907640

File tree

5 files changed

+78
-16
lines changed

5 files changed

+78
-16
lines changed

lonboard/_map.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import sys
4+
from functools import lru_cache
45
from pathlib import Path
56
from typing import IO, TYPE_CHECKING, Optional, Sequence, TextIO, Union
67

@@ -46,6 +47,13 @@
4647
"""
4748

4849

50+
@lru_cache
51+
def _load_parquet_wasm_binary() -> bytes:
52+
"""Load the gzipped parquet-wasm binary blob"""
53+
with open(bundler_output_dir / "arrow2_bg.wasm.gz", "rb") as f:
54+
return f.read()
55+
56+
4957
class Map(BaseAnyWidget):
5058
"""
5159
The top-level class used to display a map in a Jupyter Widget.
@@ -91,10 +99,14 @@ def __init__(
9199
if isinstance(layers, BaseLayer):
92100
layers = [layers]
93101

94-
super().__init__(layers=layers, **kwargs)
102+
_parquet_wasm_content = _load_parquet_wasm_binary()
103+
super().__init__(
104+
layers=layers, _parquet_wasm_content=_parquet_wasm_content, **kwargs
105+
)
95106

96107
_esm = bundler_output_dir / "index.js"
97108
_css = bundler_output_dir / "index.css"
109+
_parquet_wasm_content = traitlets.Bytes(allow_none=False).tag(sync=True)
98110

99111
view_state = ViewStateTrait()
100112
"""

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"vitest": "^1.4.0"
3232
},
3333
"scripts": {
34-
"build": "node ./build.mjs",
34+
"build": "node ./build.mjs && gzip -c node_modules/parquet-wasm/esm/arrow2_bg.wasm > lonboard/static/arrow2_bg.wasm.gz",
3535
"build:watch": "nodemon --watch src/ --exec \"npm run build\" --ext js,json,ts,tsx,css",
3636
"fmt:check": "prettier './src/**/*.{ts,tsx,css}' --check",
3737
"fmt": "prettier './src/**/*.{ts,tsx,css}' --write",

pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ authors = ["Kyle Barron <[email protected]>"]
66
license = "MIT"
77
readme = "README.md"
88
packages = [{ include = "lonboard" }]
9-
include = ["lonboard/static/*.js", "lonboard/static/*.css", "MANIFEST.in"]
9+
include = [
10+
"lonboard/static/*.js",
11+
"lonboard/static/*.css",
12+
"lonboard/static/*.wasm",
13+
"lonboard/static/*.wasm.gz",
14+
"MANIFEST.in",
15+
]
1016

1117
[tool.poetry.dependencies]
1218
python = "^3.8"

src/index.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,14 @@ import DeckGL from "@deck.gl/react/typed";
77
import { MapViewState, type Layer } from "@deck.gl/core/typed";
88
import { BaseLayerModel, initializeLayer } from "./model/index.js";
99
import type { WidgetModel } from "@jupyter-widgets/base";
10-
import { initParquetWasm } from "./parquet.js";
10+
import { useParquetWasm } from "./parquet.js";
1111
import { getTooltip } from "./tooltip/index.js";
1212
import { isDefined, loadChildModels } from "./util.js";
1313
import { v4 as uuidv4 } from "uuid";
1414
import { Message } from "./types.js";
1515
import { flyTo } from "./actions/fly-to.js";
1616
import { useViewStateDebounced } from "./state";
1717

18-
await initParquetWasm();
19-
2018
const DEFAULT_INITIAL_VIEW_STATE = {
2119
latitude: 10,
2220
longitude: 0,
@@ -65,6 +63,10 @@ async function getChildModelState(
6563
function App() {
6664
let model = useModel();
6765

66+
let [parquetWasmBinary] = useModelState<DataView | null>(
67+
"_parquet_wasm_content",
68+
);
69+
let [parquetWasmReady] = useParquetWasm(parquetWasmBinary);
6870
let [mapStyle] = useModelState<string>("basemap_style");
6971
let [mapHeight] = useModelState<number>("_height");
7072
let [showTooltip] = useModelState<boolean>("show_tooltip");
@@ -108,7 +110,15 @@ function App() {
108110
let [stateCounter, setStateCounter] = useState<Date>(new Date());
109111

110112
useEffect(() => {
113+
if (!parquetWasmReady) {
114+
return;
115+
}
116+
111117
const callback = async () => {
118+
if (!parquetWasmReady) {
119+
throw new Error("inside callback but parquetWasm not ready!");
120+
}
121+
112122
const childModels = await loadChildModels(
113123
model.widget_manager,
114124
childLayerIds,
@@ -122,7 +132,7 @@ function App() {
122132
setSubModelState(newSubModelState);
123133
};
124134
callback().catch(console.error);
125-
}, [childLayerIds]);
135+
}, [parquetWasmReady, childLayerIds]);
126136

127137
const layers: Layer[] = [];
128138
for (const subModel of Object.values(subModelState)) {

src/parquet.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
11
import { useEffect, useState } from "react";
2-
import _initParquetWasm, { readParquet } from "parquet-wasm/esm/arrow2";
2+
import { initSync, readParquet } from "parquet-wasm/esm/arrow2";
33
import * as arrow from "apache-arrow";
44

5-
// NOTE: this version must be synced exactly with the parquet-wasm version in
6-
// use.
7-
const PARQUET_WASM_VERSION = "0.5.0";
8-
const PARQUET_WASM_CDN_URL = `https://cdn.jsdelivr.net/npm/parquet-wasm@${PARQUET_WASM_VERSION}/esm/arrow2_bg.wasm`;
95
let WASM_READY: boolean = false;
106

11-
export async function initParquetWasm() {
12-
if (WASM_READY) {
13-
return;
7+
// https://developer.mozilla.org/en-US/docs/Web/API/Compression_Streams_API
8+
async function decompressBlob(blob: Blob) {
9+
const ds = new DecompressionStream("gzip");
10+
const decompressedStream = blob.stream().pipeThrough(ds);
11+
return await new Response(decompressedStream).blob();
12+
}
13+
14+
/**
15+
* Initialize parquet-wasm from an existing WASM binary blob.
16+
* It is expected that this WASM has been gzipped
17+
*
18+
* @return Whether initialization succeeded
19+
*/
20+
export async function initParquetWasmFromBinary(
21+
view: DataView | null,
22+
): Promise<boolean> {
23+
if (!view) {
24+
return false;
1425
}
1526

16-
await _initParquetWasm(PARQUET_WASM_CDN_URL);
27+
let blob = new Blob([view]);
28+
const decompressedBlob = await decompressBlob(blob);
29+
const decompressedBuffer = await decompressedBlob.arrayBuffer();
30+
31+
initSync(decompressedBuffer);
1732
WASM_READY = true;
33+
return true;
1834
}
1935

2036
/**
@@ -58,3 +74,21 @@ export function parseParquetBuffers(dataViews: DataView[]): arrow.Table {
5874

5975
return new arrow.Table(batches);
6076
}
77+
78+
export function useParquetWasm(view: DataView | null): [boolean] {
79+
const [wasmReady, setWasmReady] = useState<boolean>(false);
80+
81+
// Init parquet wasm
82+
useEffect(() => {
83+
const callback = async () => {
84+
const succeeded = await initParquetWasmFromBinary(view);
85+
if (succeeded) {
86+
setWasmReady(true);
87+
}
88+
};
89+
90+
callback();
91+
}, []);
92+
93+
return [wasmReady];
94+
}

0 commit comments

Comments
 (0)