[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6C0ZKJ5DXMB57SG53GAPM4M":3,"skill-same:01M6C0ZKJ5DXMB57SG53GAPM4M":53},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":25,"linkOnly":26,"markdownContent":27,"markdownBody":28,"frontMatter":29,"refName":30,"commitSha":31,"skillDirectory":32,"forksCount":33,"repositoryHtmlUrl":34,"repositoryHomepageUrl":35,"ownerHtmlUrl":36,"ownerBlogUrl":37,"ownerDescription":6,"updatedAt":38,"install":39,"resources":52},"01M6C0ZKJ5DXMB57SG53GAPM4M","pluot-memoization",null,"Use when writing layer prepare functions which load data asynchronously or otherwise need to cache or memoize results.","keller-mark\u002Fpluot","keller-mark","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F7525285?v=4","01M6C0Z7QY1MB45XYSA751H279","Mark Keller","01M6C0Z7WT9FM6H2ZFHT28C43C",31,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fkeller-mark\u002Fpluot\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fpluot-memoization\u002FSKILL.md",[],[19,20,21,22,23,24],"pluot memoization","writing","prepare","cache","data","results","2026-11-10 10:12:30",false,"---\nname: pluot-memoization\ndescription: Use when writing layer prepare functions which load data asynchronously or otherwise need to cache or memoize results.\n---\n\nPluot layers re-run `prepare()` on every render (every pan, zoom, brush move, or parameter change).\nWhile the prepare function is intended to be where data is loaded\u002Ftransformed, we must use memoization to avoid re-fetching or re-computing the same information on every rendered frame.\nTo memoize, we use `use_memo_*` functions in `crates\u002Fpluot_core\u002Fsrc\u002Fcache.rs`, modeled on React's `useMemo` hook.\nWhen the memoization logic gets complex, we can extract this logic into custom `use_*` functions, analogous to the pattern of custom hook function development in the React ecosystem.\n\n## Cache keys\n\nThink of cache keys as being analogous to queryKeys from TanStack Query \u002F React Query or as being analogous to React hook dependency arrays.\n\nTypically cache keys will contain a namespace string like \"zarr_numeric_data_arr\", then the store name, then the array paths \u002F indices \u002F parameters the value actually depends on.\n\nOmit `layer_id` when the value doesn't depend on the layer. Keying a loaded zarr array by layer means two layers reading the same column would each need to re-fetch it unnecessarily.\n\nPrefer explicitly listing properties in cache keys as opposed to converting structs to strings wholesale. When cache key arrays become unruly, extract the key construction logic into a function.\n\n\n## Eagerly split operations into independent memos when values are not dependent on one another\n\nGive each loaded data array or derived value its own memo keyed by only what it needs.\n\n### Cache the pieces, not the assembly\n\nFor example, `OmeZarrBitmapLayer::load_tile_data` caches each channel's slice under its own key\nand deliberately does **not** cache the concatenated\nmulti-channel tile. Changing one channel's index re-fetches one channel; the concatenation is\ncheap and re-running it avoids storing the same bytes twice. Prefer this shape: memoize the\nexpensive I\u002FO and GPU reductions at their natural granularity, and recompute cheap glue.\n\n\n## Leverage nested `use_memo_*` calls\n\nA memo's initializer may call other memos, including memos of the same type. This is the main\ntool for making a coarse-grained result and its expensive ingredients invalidate\nindependently.\n\nFor example, `ZarrHistogramLayer::prepare` nests three levels deep:\n\n```rust\nlet background_future = use_memo_vec_f32(async || {\n    \u002F\u002F 1. the raw column, keyed by (store, path) alone\n    let quant_arr = load_arr_as_numeric_data_memoized(\n        store.clone(), &self.store_name, &self.layer_params.data_key, quant_cache_enabled,\n    ).await?;\n\n    \u002F\u002F 2. the criteria arrays, each independently memoized by (store, path)\n    let filtering_criteria = resolve_zarr_emphasis_criteria(\n        store.clone(), &self.layer_params.filtering_criteria,\n        &self.store_name, self.view_params.cache_enabled,\n    ).await?;\n\n    \u002F\u002F Clone the Arc-backed handles the inner closure needs, before it takes them.\n    let quant_arr_for_extent = quant_arr.as_ref().clone();\n    let filtering_criteria_for_extent = filtering_criteria.clone();\n\n    \u002F\u002F 3. the extent — same memo type as the outer call, a different key\n    let extent = use_memo_vec_f32(async || {\n        let (lo, hi) = reduce_extent(gpu_context, quant_arr_for_extent, &filtering_criteria_for_extent, &[]).await.background;\n        Ok::\u003CVec\u003Cf32>, std::convert::Infallible>(vec![lo, hi])\n    }, &extent_future_deps, self.view_params.cache_enabled).await.expect(\"...\");\n\n    \u002F\u002F ... bin the column using that extent, and return [min, max, counts...]\n}, &background_future_deps, self.view_params.cache_enabled);\n```\n\nRules for nesting:\n\n- **The outer key must cover every param that reaches the outer result**, including the ones\n  the inner memos are keyed by. Above, `background_future_deps` carries the data key, the\n  filtering criteria, *and* `num_bins`. If an inner dependency were missing from the outer key,\n  the outer entry would be served stale after that dependency changed.\n- **The inner key must not pick up outer-only params.** `extent_future_deps` omits `num_bins`,\n  because the extent doesn't depend on the bin count — so changing `num_bins` re-runs the\n  binning but reuses the extent (and the loaded array).\n- **Pass `cache_enabled` down unchanged** so disabling the cache disables the whole nest. The\n  one exception is a narrowing flag like `quant_cache_enabled`\n  (`cache_enabled && layer_params.cache_data`), which lets a layer keep the small derived\n  result while declining to hold the full array.\n\nPrefer nesting because a nested call is skipped entirely on an outer hit.\n\n\n## Concurrency\n\nMemo calls return futures. Build them all first, then await them together with\n`futures::try_join!` (`ZarrPointLayer`) or `futures::future::join_all`\n(`resolve_zarr_emphasis_criteria`, the per-channel fetches), wrapping each in `maybe_timeout!`\nwith `view_params.timeout`.\n\n## Extract complex caching into its own `use_*` hook\n\nWhen a `use_memo` operation (initializer function body) is more than a few lines, or is needed in more than one place, move it out of `prepare()` into a custom hook function.\n\n## Bailing out vs. rendering partial results\n\nWhen a memo can't complete in time, decide per-value whether the layer should bail or degrade:\n\n- If the layer can render a partial result (e.g., certain sub-layers), then instantiate these sublayers and prepare them (call `sublayer.prepare()`), but still return `bailed_early: true` at the conclusion of the prepare function so that the client will know to trigger a re-render.\n- If the layer cannot render even a partial result due to the lack of a value, then we can simply return early with `bailed_early: true`\n\n\n## Prefer progressive loading via per-chunk sublayers\n\nA memo is all-or-nothing per key, so **the granularity at which data can appear on screen is\nthe granularity of the cache keys, which in turn should be the granularity of the sublayers.**\nA layer that loads its whole dataset under one key can only show everything or nothing. A layer\nthat partitions the data into independently-keyed chunks, each owned by its own sublayer, can\ndraw whatever has arrived and fill in the rest on later frames.\n\n`OmeZarrBitmapMultiscaleLayer` is the reference implementation:\n\n1. **Metadata first, memoized separately.** `load_metadata` fetches the OME-Zarr group once\n   under `(store_name, group_path, multiscale_{i})` and caches the metadata.\n2. **Sublayer construction is synchronous and loads nothing.** `build_sublayers` constructs one `OmeZarrBitmapLayer` per visible tile.\n3. **Each chunk fetches and caches itself.** Each tile sublayer's own `prepare` →\n   `load_tile_data` memoizes per `(store, array_path, slice_x, slice_y, z, t, channel)`. Pan\n   and zoom therefore reuse every tile already loaded, at every level, and only genuinely new\n   tiles hit the store.\n4. **Prepare all chunks concurrently, time out once.** Nested `join_all` (tiles within a level,\n   levels within the layer) wrapped in a single `maybe_timeout!`. Use `join_all`, not\n   `try_join_all` — one slow or failing tile must not cancel its siblings.\n5. **Parent layer aggregates sublayer bailed_early flags.** Each sublayer's `PrepareResult` is stored in\n   `LevelSublayers::prepare_results`; the parent aggregates `any_bailed` into its own\n   `bailed_early` (a \"re-render me\" signal) but keeps everything that did load. A sublayer that\n   never got data has its own internal logic to simply draw nothing.\n\nApplying the pattern elsewhere: **This generalizes past images.** Any layer whose data partitions — row groups of a dataframe, genes in a dotplot — can adopt it.\n","Pluot layers re-run `prepare()` on every render (every pan, zoom, brush move, or parameter change).\nWhile the prepare function is intended to be where data is loaded\u002Ftransformed, we must use memoization to avoid re-fetching or re-computing the same information on every rendered frame.\nTo memoize, we use `use_memo_*` functions in `crates\u002Fpluot_core\u002Fsrc\u002Fcache.rs`, modeled on React's `useMemo` hook.\nWhen the memoization logic gets complex, we can extract this logic into custom `use_*` functions, analogous to the pattern of custom hook function development in the React ecosystem.\n\n## Cache keys\n\nThink of cache keys as being analogous to queryKeys from TanStack Query \u002F React Query or as being analogous to React hook dependency arrays.\n\nTypically cache keys will contain a namespace string like \"zarr_numeric_data_arr\", then the store name, then the array paths \u002F indices \u002F parameters the value actually depends on.\n\nOmit `layer_id` when the value doesn't depend on the layer. Keying a loaded zarr array by layer means two layers reading the same column would each need to re-fetch it unnecessarily.\n\nPrefer explicitly listing properties in cache keys as opposed to converting structs to strings wholesale. When cache key arrays become unruly, extract the key construction logic into a function.\n\n\n## Eagerly split operations into independent memos when values are not dependent on one another\n\nGive each loaded data array or derived value its own memo keyed by only what it needs.\n\n### Cache the pieces, not the assembly\n\nFor example, `OmeZarrBitmapLayer::load_tile_data` caches each channel's slice under its own key\nand deliberately does **not** cache the concatenated\nmulti-channel tile. Changing one channel's index re-fetches one channel; the concatenation is\ncheap and re-running it avoids storing the same bytes twice. Prefer this shape: memoize the\nexpensive I\u002FO and GPU reductions at their natural granularity, and recompute cheap glue.\n\n\n## Leverage nested `use_memo_*` calls\n\nA memo's initializer may call other memos, including memos of the same type. This is the main\ntool for making a coarse-grained result and its expensive ingredients invalidate\nindependently.\n\nFor example, `ZarrHistogramLayer::prepare` nests three levels deep:\n\n```rust\nlet background_future = use_memo_vec_f32(async || {\n    \u002F\u002F 1. the raw column, keyed by (store, path) alone\n    let quant_arr = load_arr_as_numeric_data_memoized(\n        store.clone(), &self.store_name, &self.layer_params.data_key, quant_cache_enabled,\n    ).await?;\n\n    \u002F\u002F 2. the criteria arrays, each independently memoized by (store, path)\n    let filtering_criteria = resolve_zarr_emphasis_criteria(\n        store.clone(), &self.layer_params.filtering_criteria,\n        &self.store_name, self.view_params.cache_enabled,\n    ).await?;\n\n    \u002F\u002F Clone the Arc-backed handles the inner closure needs, before it takes them.\n    let quant_arr_for_extent = quant_arr.as_ref().clone();\n    let filtering_criteria_for_extent = filtering_criteria.clone();\n\n    \u002F\u002F 3. the extent — same memo type as the outer call, a different key\n    let extent = use_memo_vec_f32(async || {\n        let (lo, hi) = reduce_extent(gpu_context, quant_arr_for_extent, &filtering_criteria_for_extent, &[]).await.background;\n        Ok::\u003CVec\u003Cf32>, std::convert::Infallible>(vec![lo, hi])\n    }, &extent_future_deps, self.view_params.cache_enabled).await.expect(\"...\");\n\n    \u002F\u002F ... bin the column using that extent, and return [min, max, counts...]\n}, &background_future_deps, self.view_params.cache_enabled);\n```\n\nRules for nesting:\n\n- **The outer key must cover every param that reaches the outer result**, including the ones\n  the inner memos are keyed by. Above, `background_future_deps` carries the data key, the\n  filtering criteria, *and* `num_bins`. If an inner dependency were missing from the outer key,\n  the outer entry would be served stale after that dependency changed.\n- **The inner key must not pick up outer-only params.** `extent_future_deps` omits `num_bins`,\n  because the extent doesn't depend on the bin count — so changing `num_bins` re-runs the\n  binning but reuses the extent (and the loaded array).\n- **Pass `cache_enabled` down unchanged** so disabling the cache disables the whole nest. The\n  one exception is a narrowing flag like `quant_cache_enabled`\n  (`cache_enabled && layer_params.cache_data`), which lets a layer keep the small derived\n  result while declining to hold the full array.\n\nPrefer nesting because a nested call is skipped entirely on an outer hit.\n\n\n## Concurrency\n\nMemo calls return futures. Build them all first, then await them together with\n`futures::try_join!` (`ZarrPointLayer`) or `futures::future::join_all`\n(`resolve_zarr_emphasis_criteria`, the per-channel fetches), wrapping each in `maybe_timeout!`\nwith `view_params.timeout`.\n\n## Extract complex caching into its own `use_*` hook\n\nWhen a `use_memo` operation (initializer function body) is more than a few lines, or is needed in more than one place, move it out of `prepare()` into a custom hook function.\n\n## Bailing out vs. rendering partial results\n\nWhen a memo can't complete in time, decide per-value whether the layer should bail or degrade:\n\n- If the layer can render a partial result (e.g., certain sub-layers), then instantiate these sublayers and prepare them (call `sublayer.prepare()`), but still return `bailed_early: true` at the conclusion of the prepare function so that the client will know to trigger a re-render.\n- If the layer cannot render even a partial result due to the lack of a value, then we can simply return early with `bailed_early: true`\n\n\n## Prefer progressive loading via per-chunk sublayers\n\nA memo is all-or-nothing per key, so **the granularity at which data can appear on screen is\nthe granularity of the cache keys, which in turn should be the granularity of the sublayers.**\nA layer that loads its whole dataset under one key can only show everything or nothing. A layer\nthat partitions the data into independently-keyed chunks, each owned by its own sublayer, can\ndraw whatever has arrived and fill in the rest on later frames.\n\n`OmeZarrBitmapMultiscaleLayer` is the reference implementation:\n\n1. **Metadata first, memoized separately.** `load_metadata` fetches the OME-Zarr group once\n   under `(store_name, group_path, multiscale_{i})` and caches the metadata.\n2. **Sublayer construction is synchronous and loads nothing.** `build_sublayers` constructs one `OmeZarrBitmapLayer` per visible tile.\n3. **Each chunk fetches and caches itself.** Each tile sublayer's own `prepare` →\n   `load_tile_data` memoizes per `(store, array_path, slice_x, slice_y, z, t, channel)`. Pan\n   and zoom therefore reuse every tile already loaded, at every level, and only genuinely new\n   tiles hit the store.\n4. **Prepare all chunks concurrently, time out once.** Nested `join_all` (tiles within a level,\n   levels within the layer) wrapped in a single `maybe_timeout!`. Use `join_all`, not\n   `try_join_all` — one slow or failing tile must not cancel its siblings.\n5. **Parent layer aggregates sublayer bailed_early flags.** Each sublayer's `PrepareResult` is stored in\n   `LevelSublayers::prepare_results`; the parent aggregates `any_bailed` into its own\n   `bailed_early` (a \"re-render me\" signal) but keeps everything that did load. A sublayer that\n   never got data has its own internal logic to simply draw nothing.\n\nApplying the pattern elsewhere: **This generalizes past images.** Any layer whose data partitions — row groups of a dataframe, genes in a dotplot — can adopt it.\n",{"name":5,"description":7},"main","183ad01c1deac0379e3fa98720cf49c990b2a0d3",".claude\u002Fskills\u002Fpluot-memoization",3,"https:\u002F\u002Fgithub.com\u002Fkeller-mark\u002Fpluot","https:\u002F\u002Fpluot.dev","https:\u002F\u002Fgithub.com\u002Fkeller-mark","markk.co","2026-11-04 02:32:42",{"source":8,"skillName":5,"commands":40,"prompt":50,"zipAvailable":51},[41,44,47],{"tool":42,"command":43},"npx","npx skills add keller-mark\u002Fpluot --skill pluot-memoization",{"tool":45,"command":46},"bunx","bunx skills add keller-mark\u002Fpluot --skill pluot-memoization",{"tool":48,"command":49},"pnpm","pnpm dlx skills add keller-mark\u002Fpluot --skill pluot-memoization","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fkeller-mark\u002Fpluot\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fpluot-memoization\u002FSKILL.md",true,[],[54,67,76,88,100,108],{"skillNo":55,"name":56,"nameZh":6,"description":57,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":58,"categories":59,"keywords":60,"publishedAt":25},"01M6C0ZSB3MV7FRFBAEMNDSKC7","pluot-snappy-troubleshooting","Use when rust build fails due to command did not execute successfully, involving snappy or snappy_src or snappy\u002Fsnappy.cc","https:\u002F\u002Fgithub.com\u002Fkeller-mark\u002Fpluot\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fpluot-snappy-troubleshooting\u002FSKILL.md",[],[61,62,63,64,65,66],"pluot snappy troubleshooting","troubleshooting","rust","src","command","execute",{"skillNo":68,"name":69,"nameZh":6,"description":70,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":71,"categories":72,"keywords":73,"publishedAt":25},"01M6C0ZQWAKEWB2HC934DFBPP2","pluot-shader-modules","Use when you need to write or modify a WGSL shader.","https:\u002F\u002Fgithub.com\u002Fkeller-mark\u002Fpluot\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fpluot-shader-modules\u002FSKILL.md",[],[74,75],"pluot shader modules","write",{"skillNo":77,"name":78,"nameZh":6,"description":79,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":80,"categories":81,"keywords":82,"publishedAt":25},"01M6C0ZP26G5TJTFNFEK035R6Z","pluot-reference-materials","Use when the user asks you to reference specific code, documentation files, or source repositories of dependencies or related tools.","https:\u002F\u002Fgithub.com\u002Fkeller-mark\u002Fpluot\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fpluot-reference-materials\u002FSKILL.md",[],[83,84,85,86,87],"pluot reference materials","documentation","code","Tools","user",{"skillNo":89,"name":90,"nameZh":6,"description":91,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":92,"categories":93,"keywords":94,"publishedAt":25},"01M6C0ZJ279C848HRCTA1YZ454","pluot-layer-parameter-principles","Use when adding fields\u002Fproperties\u002Fmembers to layer parameter structs.","https:\u002F\u002Fgithub.com\u002Fkeller-mark\u002Fpluot\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fpluot-layer-parameter-principles\u002FSKILL.md",[],[95,96,97,98,99],"layer","principles","pluot layer parameter principles","properties","use",{"skillNo":101,"name":102,"nameZh":6,"description":103,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":104,"categories":105,"keywords":106,"publishedAt":25},"01M6C0ZGD3J2WVVE8499GW4Y47","pluot-layer-implementation","Use when implementing layers for Pluot in Rust.","https:\u002F\u002Fgithub.com\u002Fkeller-mark\u002Fpluot\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fpluot-layer-implementation\u002FSKILL.md",[],[107,63],"pluot layer implementation",{"skillNo":109,"name":110,"nameZh":6,"description":111,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":112,"categories":113,"keywords":114,"publishedAt":25},"01M6C0ZEKV8Y8N8230180FSPVH","pluot-filter-select-highlight","Use when modifying the filtering, selection, or highlighting logic in Pluot.","https:\u002F\u002Fgithub.com\u002Fkeller-mark\u002Fpluot\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fpluot-filter-select-highlight\u002FSKILL.md",[],[115,116],"pluot filter select highlight","select"]