From 55d5e63e1deb068b8ef9731e332dc2ada346883a Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Fri, 4 Aug 2023 16:12:57 -0400 Subject: [PATCH] Add direct client methodology and e2e results (#8) * Restructure direct-client section * Add e2e results * Update direct-client * Add more info to methods and results --- _quarto.yml | 9 +- .../{direct-client.md => direct-client.qmd} | 8 +- .../benchmarking-methodology.qmd | 41 + approaches/direct-client/benchmarks.md | 3 - approaches/direct-client/e2e-results.ipynb | 874 ++++++++++++++++++ approaches/direct-client/future-areas.md | 1 - approaches/direct-client/future-areas.qmd | 3 + .../{preprocessing.md => recommendations.qmd} | 4 +- approaches/index.qmd | 2 +- 9 files changed, 933 insertions(+), 12 deletions(-) rename approaches/{direct-client.md => direct-client.qmd} (60%) create mode 100644 approaches/direct-client/benchmarking-methodology.qmd delete mode 100644 approaches/direct-client/benchmarks.md create mode 100644 approaches/direct-client/e2e-results.ipynb delete mode 100644 approaches/direct-client/future-areas.md create mode 100644 approaches/direct-client/future-areas.qmd rename approaches/direct-client/{preprocessing.md => recommendations.qmd} (79%) diff --git a/_quarto.yml b/_quarto.yml index e5c50b2..1c6b35b 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -28,7 +28,7 @@ website: - href: index.qmd text: Welcome - section: approaches/index.qmd - contents: + contents: - section: approaches/tiling.qmd contents: - approaches/tiling/benchmarking-methodology.qmd @@ -40,7 +40,12 @@ website: - approaches/tiling/e2e-results.ipynb - approaches/tiling/recommendations.md - approaches/tiling/future-areas.md - - section: approaches/direct-client.md + - section: approaches/direct-client.qmd + contents: + - approaches/direct-client/benchmarking-methodology.qmd + - approaches/direct-client/e2e-results.ipynb + - approaches/direct-client/recommendations.qmd + - approaches/direct-client/future-areas.qmd diff --git a/approaches/direct-client.md b/approaches/direct-client.qmd similarity index 60% rename from approaches/direct-client.md rename to approaches/direct-client.qmd index 562da1d..5a6cea3 100644 --- a/approaches/direct-client.md +++ b/approaches/direct-client.qmd @@ -1,8 +1,8 @@ -# Direct Client +--- +title: "Direct Client" +--- -## Introduction - -Traditional approaches to rendering raster datasets in the browser involve the creation of tiles in a pixelated impact format like PNG or JPEG. These tiles can be pre-generated or created by a tiling server on demand, as described in the [tiling approach chapter](tiling.md). While the image tiles are fast to load and easy to render, tiling offers limited flexibility for dynamically customizing visualizations based on user input. In contrast, the direct client approach leverages Zarr to render the data directly using WebGL rather than through an intermediate layer. The Zarr format is ideal for direct rendering in the browser because the chunks of a Zarr dataset serve a similar purpose to the tiles of a web map. In additional, data can be chunked across additional dimensions which prevents the need for generating individual tiles per time step. Lastly, as a cloud-optimized data format Zarr allows for fast, parallel reading and writing from object storage. +Traditional approaches to rendering raster datasets in the browser involve the creation of tiles in a pixelated impact format like PNG or JPEG. These tiles can be pre-generated or created by a tiling server on demand, as described in the [tiling approach chapter](tiling.qmd). While the image tiles are fast to load and easy to render, tiling offers limited flexibility for dynamically customizing visualizations based on user input. In contrast, the direct client approach leverages Zarr to render the data directly using WebGL rather than through an intermediate layer. The Zarr format is ideal for direct rendering in the browser because the chunks of a Zarr dataset serve a similar purpose to the tiles of a web map. In additional, data can be chunked across additional dimensions which prevents the need for generating individual tiles per time step. Lastly, as a cloud-optimized data format Zarr allows for fast, parallel reading and writing from object storage. The direct client approach leverages pyramids created with the [ndpyramid](https://github.com/carbonplan/ndpyramid) package for performant rendering of data at multiple zoom levels. The approach loads Zarr V2 or V3 data using the [zarr-js](https://github.com/freeman-lab/zarr-js) JavaScript library and renders the fetched chunks via WebGL using the [regl](https://github.com/regl-project/regl) library. The open-source library called [@carbonplan/maps](https://github.com/carbonplan/maps) provides a small set of React components for rendering raster data using this approach and supports rendering traditional vector layers side-by-side using `mapbox-gl-js`. diff --git a/approaches/direct-client/benchmarking-methodology.qmd b/approaches/direct-client/benchmarking-methodology.qmd new file mode 100644 index 0000000..fea552f --- /dev/null +++ b/approaches/direct-client/benchmarking-methodology.qmd @@ -0,0 +1,41 @@ +--- +title: Benchmarking Methodology +--- + +## End-to-End Benchmarks + +The end-to-end benchmarks capture the user experience in response to different actions. The suite of benchmarks included in this cookbook are designed to inform how the choice of Zarr versions and chunking schemes influence the user experience. + +### End-to-End Benchmarks: Datasets + + +### End-to-End Benchmarks: Approach + +CarbonPlan's [benchmark-maps](https://github.com/carbonplan/benchmark-maps) repository leverages [Playwright](https://playwright.dev/python/) for the end-to-end performance benchmarks. By default, the benchmarks are run on [https://prototype-maps.vercel.app/](https://prototype-maps.vercel.app/) although the url is configurable. The frame below shows this domain after selecting an approach, Zarr version, and dataset. + +The benchmarking script takes the following steps: + +1. Launch chromium browser +2. Create a new page +3. Start chromium tracing +4. Select Zarr version in the dropdown +5. Select Dataset in the dropbown +6. Wait 5 seconds for the page the render +7. Zoom in a defined number of times, waiting 5 seconds after each action +8. Write out metadata about each run and the trace record + +The frame rate and request information are extracted from the resultant metadata and trace records. The completion time for each zoom level is determined by comparing the screen captures in the trace record to the expected result for each zoom level. + + +```{=html} + +``` + diff --git a/approaches/direct-client/benchmarks.md b/approaches/direct-client/benchmarks.md deleted file mode 100644 index 27515c7..0000000 --- a/approaches/direct-client/benchmarks.md +++ /dev/null @@ -1,3 +0,0 @@ -# Benchmarks - -To be merged with https://github.com/carbonplan/benchmark-maps \ No newline at end of file diff --git a/approaches/direct-client/e2e-results.ipynb b/approaches/direct-client/e2e-results.ipynb new file mode 100644 index 0000000..b236783 --- /dev/null +++ b/approaches/direct-client/e2e-results.ipynb @@ -0,0 +1,874 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "title: \"End-to-End Benchmarking\"\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Processing benchmark results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Import dependencies\n", + "\n", + "The CarbonPlan team put together some utilities for parsing, processing, and visualizing the benchmarking results in [carbonplan_benchmarks](https://github.com/carbonplan/benchmark-maps). We'll use those utilities along with the [Holoviz](https://holoviz.org/) HoloViz suite of tools for visualization and [Pandas](https://pandas.pydata.org/) as the underlying analysis tool." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import carbonplan_benchmarks.analysis as cba\n", + "import hvplot\n", + "import pandas as pd\n", + "\n", + "pd.options.plotting.backend = \"holoviews\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load benchmark results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, define the paths to the baseline images that the tests will be compared against and paths to the metadata files associated with each benchmarking run." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "baseline_fp = \"s3://carbonplan-benchmarks/benchmark-data/baselines.json\"\n", + "metadata_base_fp = \"s3://carbonplan-benchmarks/benchmark-data\"\n", + "metadata_files = [\n", + " \"data-2023-08-04T01-14-24.json\",\n", + " \"data-2023-08-04T01-15-30.json\",\n", + " \"data-2023-08-04T01-16-27.json\",\n", + " \"data-2023-08-04T01-17-25.json\",\n", + " \"data-2023-08-04T01-18-37.json\",\n", + " \"data-2023-08-04T01-19-47.json\",\n", + " \"data-2023-08-04T01-21-02.json\",\n", + " \"data-2023-08-04T01-22-08.json\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, use the utilities from `carbonplan_benchmarks` to load the metadata and baseline images into DataFrames, process those results, and create a summary DataFrame for all runs." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "snapshots = cba.load_snapshots(snapshot_path=baseline_fp)\n", + "summary_dfs = []\n", + "for file in metadata_files:\n", + " fp = f\"{metadata_base_fp}/{file}\"\n", + " metadata, trace_events = cba.load_data(metadata_path=fp, run=0)\n", + " data = cba.process_run(\n", + " metadata=metadata, trace_events=trace_events, snapshots=snapshots\n", + " )\n", + " summary_dfs.append(cba.create_summary(metadata=metadata, data=data))\n", + "summary = pd.concat(summary_dfs)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
actionbrowser_namebrowser_versionplaywright_python_versionprovidertrace_pathurlzoom_levelapproachzarr_versiondatasetchunk_sizezoomdurationfpsrequest_durationrequest_percent
0zoom_inchromium115.0.5790.751.36.0unknowns3://carbonplan-benchmarks/benchmark-data/2023...https://prototype-maps.vercel.app/direct-clien...4direct-clientv21MB-chunks10.01039.89050.005289962.68792.575849
1zoom_inchromium115.0.5790.751.36.0unknowns3://carbonplan-benchmarks/benchmark-data/2023...https://prototype-maps.vercel.app/direct-clien...4direct-clientv21MB-chunks11.01076.48254.808162738.29968.584426
2zoom_inchromium115.0.5790.751.36.0unknowns3://carbonplan-benchmarks/benchmark-data/2023...https://prototype-maps.vercel.app/direct-clien...4direct-clientv21MB-chunks12.0702.45259.790562361.90351.519962
3zoom_inchromium115.0.5790.751.36.0unknowns3://carbonplan-benchmarks/benchmark-data/2023...https://prototype-maps.vercel.app/direct-clien...4direct-clientv21MB-chunks13.0981.42239.738257646.13465.836511
4zoom_inchromium115.0.5790.751.36.0unknowns3://carbonplan-benchmarks/benchmark-data/2023...https://prototype-maps.vercel.app/direct-clien...4direct-clientv21MB-chunks14.0475.92460.9340990.0000.000000
0zoom_inchromium115.0.5790.751.36.0unknowns3://carbonplan-benchmarks/benchmark-data/2023...https://prototype-maps.vercel.app/direct-clien...4direct-clientv25MB-chunks50.01199.61534.1776321107.55392.325704
1zoom_inchromium115.0.5790.751.36.0unknowns3://carbonplan-benchmarks/benchmark-data/2023...https://prototype-maps.vercel.app/direct-clien...4direct-clientv25MB-chunks51.01362.76953.567406996.51373.124132
2zoom_inchromium115.0.5790.751.36.0unknowns3://carbonplan-benchmarks/benchmark-data/2023...https://prototype-maps.vercel.app/direct-clien...4direct-clientv25MB-chunks52.02902.87225.1475092558.56488.139057
\n", + "
" + ], + "text/plain": [ + " action browser_name browser_version playwright_python_version provider \\\n", + "0 zoom_in chromium 115.0.5790.75 1.36.0 unknown \n", + "1 zoom_in chromium 115.0.5790.75 1.36.0 unknown \n", + "2 zoom_in chromium 115.0.5790.75 1.36.0 unknown \n", + "3 zoom_in chromium 115.0.5790.75 1.36.0 unknown \n", + "4 zoom_in chromium 115.0.5790.75 1.36.0 unknown \n", + "0 zoom_in chromium 115.0.5790.75 1.36.0 unknown \n", + "1 zoom_in chromium 115.0.5790.75 1.36.0 unknown \n", + "2 zoom_in chromium 115.0.5790.75 1.36.0 unknown \n", + "\n", + " trace_path \\\n", + "0 s3://carbonplan-benchmarks/benchmark-data/2023... \n", + "1 s3://carbonplan-benchmarks/benchmark-data/2023... \n", + "2 s3://carbonplan-benchmarks/benchmark-data/2023... \n", + "3 s3://carbonplan-benchmarks/benchmark-data/2023... \n", + "4 s3://carbonplan-benchmarks/benchmark-data/2023... \n", + "0 s3://carbonplan-benchmarks/benchmark-data/2023... \n", + "1 s3://carbonplan-benchmarks/benchmark-data/2023... \n", + "2 s3://carbonplan-benchmarks/benchmark-data/2023... \n", + "\n", + " url zoom_level \\\n", + "0 https://prototype-maps.vercel.app/direct-clien... 4 \n", + "1 https://prototype-maps.vercel.app/direct-clien... 4 \n", + "2 https://prototype-maps.vercel.app/direct-clien... 4 \n", + "3 https://prototype-maps.vercel.app/direct-clien... 4 \n", + "4 https://prototype-maps.vercel.app/direct-clien... 4 \n", + "0 https://prototype-maps.vercel.app/direct-clien... 4 \n", + "1 https://prototype-maps.vercel.app/direct-clien... 4 \n", + "2 https://prototype-maps.vercel.app/direct-clien... 4 \n", + "\n", + " approach zarr_version dataset chunk_size zoom duration \\\n", + "0 direct-client v2 1MB-chunks 1 0.0 1039.890 \n", + "1 direct-client v2 1MB-chunks 1 1.0 1076.482 \n", + "2 direct-client v2 1MB-chunks 1 2.0 702.452 \n", + "3 direct-client v2 1MB-chunks 1 3.0 981.422 \n", + "4 direct-client v2 1MB-chunks 1 4.0 475.924 \n", + "0 direct-client v2 5MB-chunks 5 0.0 1199.615 \n", + "1 direct-client v2 5MB-chunks 5 1.0 1362.769 \n", + "2 direct-client v2 5MB-chunks 5 2.0 2902.872 \n", + "\n", + " fps request_duration request_percent \n", + "0 50.005289 962.687 92.575849 \n", + "1 54.808162 738.299 68.584426 \n", + "2 59.790562 361.903 51.519962 \n", + "3 39.738257 646.134 65.836511 \n", + "4 60.934099 0.000 0.000000 \n", + "0 34.177632 1107.553 92.325704 \n", + "1 53.567406 996.513 73.124132 \n", + "2 25.147509 2558.564 88.139057 " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "summary.head(n=8)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualize results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, let's see how the duration of each action changes as a function of the zoom level. An important piece of context is that the underlying dataset only has four pyramid levels, so zoom=4 does not need to fetch any new data." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.2.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var is_dev = py_version.indexOf(\"+\") !== -1 || py_version.indexOf(\"-\") !== -1;\n var reloading = false;\n var Bokeh = root.Bokeh;\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'jspanel': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel', 'jspanel-modal': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal', 'jspanel-tooltip': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip', 'jspanel-hint': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint', 'jspanel-layout': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout', 'jspanel-contextmenu': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu', 'jspanel-dock': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock', 'gridstack': 'https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all', 'notyf': 'https://cdn.jsdelivr.net/npm/notyf@3/notyf.min'}, 'shim': {'jspanel': {'exports': 'jsPanel'}, 'gridstack': {'exports': 'GridStack'}}});\n require([\"jspanel\"], function(jsPanel) {\n\twindow.jsPanel = jsPanel\n\ton_load()\n })\n require([\"jspanel-modal\"], function() {\n\ton_load()\n })\n require([\"jspanel-tooltip\"], function() {\n\ton_load()\n })\n require([\"jspanel-hint\"], function() {\n\ton_load()\n })\n require([\"jspanel-layout\"], function() {\n\ton_load()\n })\n require([\"jspanel-contextmenu\"], function() {\n\ton_load()\n })\n require([\"jspanel-dock\"], function() {\n\ton_load()\n })\n require([\"gridstack\"], function(GridStack) {\n\twindow.GridStack = GridStack\n\ton_load()\n })\n require([\"notyf\"], function() {\n\ton_load()\n })\n root._bokeh_is_loading = css_urls.length + 9;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window['jsPanel'] !== undefined) && (!(window['jsPanel'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.2.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.2.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.2.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.2.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.2.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.2.1/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['GridStack'] !== undefined) && (!(window['GridStack'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.1/dist/bundled/gridstack/gridstack@7.2.3/dist/gridstack-all.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['Notyf'] !== undefined) && (!(window['Notyf'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.2.1/dist/bundled/notificationarea/notyf@3/notyf.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.2.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.2.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.2.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.2.1.min.js\", \"https://cdn.holoviz.org/panel/1.2.1/dist/panel.min.js\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [];\n var inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n Bokeh = root.Bokeh;\n bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n if (!reloading && (!bokeh_loaded || is_dev)) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n", + "application/vnd.holoviews_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":NdOverlay [zarr_version]\n", + " :Scatter [zoom] (duration)" + ] + }, + "execution_count": 5, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1002" + } + }, + "output_type": "execute_result" + } + ], + "source": [ + "summary.plot.scatter(x=\"zoom\", y=\"duration\", by=\"zarr_version\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's instead show the duration as a function of the chunk size." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":NdOverlay [zarr_version]\n", + " :Scatter [chunk_size] (duration)" + ] + }, + "execution_count": 6, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1082" + } + }, + "output_type": "execute_result" + } + ], + "source": [ + "summary.plot.scatter(x=\"chunk_size\", y=\"duration\", by=\"zarr_version\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's look at the request duration as a funciton of the chunk size." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":NdOverlay [zarr_version]\n", + " :Scatter [chunk_size] (request_duration)" + ] + }, + "execution_count": 7, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1162" + } + }, + "output_type": "execute_result" + } + ], + "source": [ + "summary.plot.scatter(x=\"chunk_size\", y=\"request_duration\", by=\"zarr_version\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lastly, let's look at the fraction of time that's spent fetching data as a function of the chunk size." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + ":NdOverlay [zarr_version]\n", + " :Scatter [chunk_size] (request_percent)" + ] + }, + "execution_count": 8, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1242" + } + }, + "output_type": "execute_result" + } + ], + "source": [ + "summary.plot.scatter(x=\"chunk_size\", y=\"request_percent\", by=\"zarr_version\").opts(\n", + " ylim=(0, 100)\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "benchmark-maps", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/approaches/direct-client/future-areas.md b/approaches/direct-client/future-areas.md deleted file mode 100644 index acd8570..0000000 --- a/approaches/direct-client/future-areas.md +++ /dev/null @@ -1 +0,0 @@ -# Future Areas diff --git a/approaches/direct-client/future-areas.qmd b/approaches/direct-client/future-areas.qmd new file mode 100644 index 0000000..69c71ea --- /dev/null +++ b/approaches/direct-client/future-areas.qmd @@ -0,0 +1,3 @@ +--- +title: Future Areas +--- \ No newline at end of file diff --git a/approaches/direct-client/preprocessing.md b/approaches/direct-client/recommendations.qmd similarity index 79% rename from approaches/direct-client/preprocessing.md rename to approaches/direct-client/recommendations.qmd index 1473eab..1eaabb6 100644 --- a/approaches/direct-client/preprocessing.md +++ b/approaches/direct-client/recommendations.qmd @@ -1,3 +1,5 @@ -# Preprocessing +--- +title: Recommendations +--- This document details the preprocessing steps to deliver performant Zarr visualization via the direct client approach. diff --git a/approaches/index.qmd b/approaches/index.qmd index 8ea5837..9dba739 100644 --- a/approaches/index.qmd +++ b/approaches/index.qmd @@ -5,6 +5,6 @@ title: Approaches For browser-based visualization of Zarr, there are 2 approaches covered in this cookbook: 1. [Tiling](./tiling.qmd) -2. [Direct Client](./direct-client.md) +2. [Direct Client](./direct-client.qmd) The tile server provides an API which is interoperable with multiple interfaces, but requires maintaining a tile server. also the response delivered to the client is an image format, not the raw data itself. The direct client has access to the underlying data and thus maximum flexibility in rendering and analysis for the user.