From d220d078756bed63d0a89d0172f5e6e2e4c9d765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 27 Jan 2026 20:27:30 +0100 Subject: [PATCH 01/10] :wrench: Add custom domain --- .github/workflows/plugins-deploy-api-doc.yml | 28 ++++++++++++++------ plugins/wrangler-penpot-plugins-api-doc.toml | 4 +++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/workflows/plugins-deploy-api-doc.yml b/.github/workflows/plugins-deploy-api-doc.yml index f8451e9816..aaa1339c9e 100644 --- a/.github/workflows/plugins-deploy-api-doc.yml +++ b/.github/workflows/plugins-deploy-api-doc.yml @@ -7,11 +7,11 @@ on: - staging - main paths: - - "plugins/libs/plugin-types/index.d.ts" - - "plugins/libs/plugin-types/REAME.md" - - "plugins/tools/typedoc.css" - - "plugins/CHANGELOG.md" - - "plugins/wrangler-penpot-plugins-api-doc.toml" + - 'plugins/libs/plugin-types/index.d.ts' + - 'plugins/libs/plugin-types/REAME.md' + - 'plugins/tools/typedoc.css' + - 'plugins/CHANGELOG.md' + - 'plugins/wrangler-penpot-plugins-api-doc.toml' workflow_dispatch: inputs: gh_ref: @@ -86,12 +86,24 @@ jobs: run: | REF="${{ steps.vars.outputs.gh_ref }}" case "$REF" in - main) echo "WORKER_NAME=penpot-plugins-api-doc-pro" >> $GITHUB_ENV ;; - staging) echo "WORKER_NAME=penpot-plugins-api-doc-pre" >> $GITHUB_ENV ;; - develop) echo "WORKER_NAME=penpot-plugins-api-doc-hourly" >> $GITHUB_ENV ;; + main) + echo "WORKER_NAME=penpot-plugins-api-doc-pro" >> $GITHUB_ENV + echo "WORKER_URI=doc.plugins.penpot.app" >> $GITHUB_ENV ;; + staging) + echo "WORKER_NAME=penpot-plugins-api-doc-pre" >> $GITHUB_ENV + echo "WORKER_URI=doc.plugins.penpot.dev" >> $GITHUB_ENV ;; + develop) + echo "WORKER_NAME=penpot-plugins-api-doc-hourly" >> $GITHUB_ENV + echo "WORKER_URI=doc.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;; *) echo "Unsupported branch ${REF}" && exit 1 ;; esac + - name: Set the custom url + working-directory: plugins + shell: bash + run: | + sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-api-doc.toml + - name: Deploy to Cloudflare Workers uses: cloudflare/wrangler-action@v3 with: diff --git a/plugins/wrangler-penpot-plugins-api-doc.toml b/plugins/wrangler-penpot-plugins-api-doc.toml index e9535be2d8..b09adf8b1a 100644 --- a/plugins/wrangler-penpot-plugins-api-doc.toml +++ b/plugins/wrangler-penpot-plugins-api-doc.toml @@ -2,3 +2,7 @@ name = "penpot-plugins-api-doc" compatibility_date = "2025-01-01" assets = { directory = "dist/doc" } + +[[routes]] +pattern = "WORKER_URI" +custom_domain = true From 1834a18263b3d72d6cc43f2d127581a43767656c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 27 Jan 2026 20:28:30 +0100 Subject: [PATCH 02/10] :wrench: Deploy plugin styles documentation --- .../workflows/plugins-deploy-styles-doc.yml | 123 ++++++++++++++++++ .../wrangler-penpot-plugins-styles-doc.toml | 8 ++ 2 files changed, 131 insertions(+) create mode 100644 .github/workflows/plugins-deploy-styles-doc.yml create mode 100644 plugins/wrangler-penpot-plugins-styles-doc.toml diff --git a/.github/workflows/plugins-deploy-styles-doc.yml b/.github/workflows/plugins-deploy-styles-doc.yml new file mode 100644 index 0000000000..7c759a62e6 --- /dev/null +++ b/.github/workflows/plugins-deploy-styles-doc.yml @@ -0,0 +1,123 @@ +name: Plugins/styles-doc deployer + +on: + push: + branches: + - develop + - staging + - main + paths: + - 'plugins/apps/example-styles/**' + - 'plugins/libs/plugins-styles/**' + - 'plugins/wrangler-penpot-plugins-styles-doc.toml' + workflow_dispatch: + inputs: + gh_ref: + description: 'Name of the branch' + type: choice + required: true + default: 'develop' + options: + - develop + - staging + - main + +permissions: + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Extract some useful variables + id: vars + run: | + echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ steps.vars.outputs.gh_ref }} + + # START: Setup Node and PNPM enabling cache + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Enable PNPM + working-directory: ./plugins + shell: bash + run: | + corepack enable; + corepack install; + + - name: Get pnpm store path + id: pnpm-store + working-directory: ./plugins + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT + + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-store.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + # END: Setup Node and PNPM enabling cache + + - name: Install deps + working-directory: ./plugins + shell: bash + run: | + pnpm install --no-frozen-lockfile; + pnpm add -D -w wrangler@latest; + + - name: Build styles + working-directory: plugins + shell: bash + run: npx nx run example-styles:build + + - name: Select Worker name + run: | + REF="${{ steps.vars.outputs.gh_ref }}" + case "$REF" in + main) + echo "WORKER_NAME=penpot-plugins-styles-doc-pro" >> $GITHUB_ENV + echo "WORKER_URI=styles-doc.plugins.penpot.app" >> $GITHUB_ENV ;; + staging) + echo "WORKER_NAME=penpot-plugins-styles-doc-pre" >> $GITHUB_ENV + echo "WORKER_URI=styles-doc.plugins.penpot.dev" >> $GITHUB_ENV ;; + develop) + echo "WORKER_NAME=penpot-plugins-styles-doc-hourly" >> $GITHUB_ENV + echo "WORKER_URI=styles-doc.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;; + *) echo "Unsupported branch ${REF}" && exit 1 ;; + esac + + - name: Set the custom url + working-directory: plugins + shell: bash + run: | + sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-api-doc.toml + + - name: Deploy to Cloudflare Workers + uses: cloudflare/wrangler-action@v3 + with: + workingDirectory: plugins + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: deploy --config wrangler-penpot-plugins-styles-doc.toml --name ${{ env.WORKER_NAME }} + + - name: Notify Mattermost + if: failure() + uses: mattermost/action-mattermost-notify@master + with: + MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }} + MATTERMOST_CHANNEL: bot-alerts-cicd + TEXT: | + ❌ 🧩💅 *[PENPOT PLUGINS] Error deploying Styles documentation.* + 📄 Triggered from ref: `${{ inputs.gh_ref }}` + 🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + @infra diff --git a/plugins/wrangler-penpot-plugins-styles-doc.toml b/plugins/wrangler-penpot-plugins-styles-doc.toml new file mode 100644 index 0000000000..7aa86eb469 --- /dev/null +++ b/plugins/wrangler-penpot-plugins-styles-doc.toml @@ -0,0 +1,8 @@ +name = "penpot-plugins-style-doc" +compatibility_date = "2025-01-01" + +assets = { directory = "dist/apps/example-styles" } + +[[routes]] +pattern = "WORKER_URI" +custom_domain = true From c6465e27e38779ae779487e93987ca20cddac84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 27 Jan 2026 14:13:42 +0100 Subject: [PATCH 03/10] :books: Fix links related to penpot plugins --- docs/plugins/api.md | 2 +- docs/plugins/beta-changelog.md | 4 ++-- docs/plugins/create-a-plugin.md | 4 ++-- docs/plugins/examples-templates.md | 2 +- docs/plugins/faq.md | 7 ++++--- docs/plugins/getting-started.md | 13 +++++++------ plugins/CONTRIBUTING.md | 2 +- plugins/docs/api-docs.md | 2 +- plugins/libs/plugins-styles/README.md | 2 +- 9 files changed, 20 insertions(+), 18 deletions(-) diff --git a/docs/plugins/api.md b/docs/plugins/api.md index 9abf6a9fa7..d1cd0bf4c3 100644 --- a/docs/plugins/api.md +++ b/docs/plugins/api.md @@ -6,4 +6,4 @@ desc: Create, deploy, and use the Penpot plugin API with our comprehensive docum # Penpot plugins API -We've got all the documentation you need for the API right here. +We've got all the documentation you need for the API right here. diff --git a/docs/plugins/beta-changelog.md b/docs/plugins/beta-changelog.md index 1f58235752..fb8a656f97 100644 --- a/docs/plugins/beta-changelog.md +++ b/docs/plugins/beta-changelog.md @@ -9,13 +9,13 @@ desc: See the Penpot plugin API changelog for version 1.0! Find breaking changes ### boom Epics and highlights - This marks the release of version 1.0, and from this point forward, we’ll do our best to avoid making any more breaking changes (or make deprecations backward compatible). - We’ve redone the documentation. You can check the API here: -[https://penpot-plugins-api-doc.pages.dev/](https://penpot-plugins-api-doc.pages.dev/) +[https://doc.plugins.penpot.app/](https://doc.plugins.penpot.app/) - New samples repository with lots of samples to use the API: [https://github.com/penpot/penpot-plugins-samples](https://github.com/penpot/penpot-plugins-samples) ### boom Breaking changes & Deprecations -- Changed types names to remove the Penpot prefix. So for example: PenpotShape becomes Shape; PenpotFile becomes File, and so on. Check the [API documentation](https://penpot-plugins-api-doc.pages.dev/) for more details. +- Changed types names to remove the Penpot prefix. So for example: PenpotShape becomes Shape; PenpotFile becomes File, and so on. Check the [API documentation](https://doc.plugins.penpot.app/) for more details. - Changes on the penpot.on and penpot.off methods. Previously you had to send the original callback to the off method in order to remove an event listener. Now, penpot.on will return an *id* that you can pass to the penpot.off method in order to remove the listener. diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index 80c4a08df6..42fc096dbe 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -49,7 +49,7 @@ There are two libraries that can help you with your plugin's development. They a ### Plugin styles -@penpot/plugin-styles contains styles to help build the UI for Penpot plugins. To check the styles go to Plugin styles. +@penpot/plugin-styles contains styles to help build the UI for Penpot plugins. To check the styles go to Plugin styles. ```bash npm install @penpot/plugin-styles @@ -139,7 +139,7 @@ parent.postMessage(responseMessage, targetOrigin); By using these message-based events, any data retrieved through the Penpot API can be communicated to and from your plugin interface seamlessly. -For more detailed information, refer to the [Penpot Plugins API Documentation](https://penpot-plugins-api-doc.pages.dev/). +For more detailed information, refer to the [Penpot Plugins API Documentation](https://doc.plugins.penpot.app/). ## 2.5. Step 5. Build the plugin file diff --git a/docs/plugins/examples-templates.md b/docs/plugins/examples-templates.md index 1836b132c2..d50e49e460 100644 --- a/docs/plugins/examples-templates.md +++ b/docs/plugins/examples-templates.md @@ -86,7 +86,7 @@ penpot.library.local.createTypography(); Penpot has dark and light modes, and you can easily add this to your plugin so your interface adapts to both themes. When you add theme support, your plugin will automatically sync with Penpot's interface settings, so the user experience is consistent no matter which mode is selected. This makes your plugin look better and also ensures it stays in line with Penpot's overall design. -Just a heads-up: if you use the plugin-styles library, many elements will automatically adapt to dark or light mode without any extra effort from you. However, if you need to customize specific elements, be sure to use the selectors provided in the styles.css of the example. +Just a heads-up: if you use the plugin-styles library, many elements will automatically adapt to dark or light mode without any extra effort from you. However, if you need to customize specific elements, be sure to use the selectors provided in the styles.css of the example. Theme example diff --git a/docs/plugins/faq.md b/docs/plugins/faq.md index 4050358597..0d761bde10 100644 --- a/docs/plugins/faq.md +++ b/docs/plugins/faq.md @@ -40,7 +40,7 @@ The plugin PenpotFlow or PenpotInteraction interfaces. +Absolutely! You can definitely create flows and interactions in the same elements as in the interface, like frames, shapes, and groups. Just check out the API documentation for the methods: createFlow, addInteraction, or removeInteraction. And if you need more help, you can always check out the Flow or Interaction interfaces. ### Are there any security or quality criteria I should be aware of? @@ -48,7 +48,8 @@ There are no set requirements. However, we can recommend the use of https:\/\/create-palette-penpot-plugin.pages.dev/assets/manifest.json or check the code here +No, it’s completely optional, in fact, we have an example of a plugin without UI. Try the plugin using this url to install it: https:\/\/create-palette.plugins.penpot.app/assets/manifest.json or check the code here + ### Can I create components? @@ -58,7 +59,7 @@ Yes, it is possible to create components using: createComponent(shapes: Shape[]): LibraryComponent; ``` -Take a look at the Penpot Library methods in the API documentation or this simple example. +Take a look at the Penpot Library methods in the API documentation or this simple example. ### Is there a place where I can share my plugin? diff --git a/docs/plugins/getting-started.md b/docs/plugins/getting-started.md index 9765e737b0..abfbc508b1 100644 --- a/docs/plugins/getting-started.md +++ b/docs/plugins/getting-started.md @@ -69,12 +69,13 @@ You need to provide the plugin's manifest URL for the installation. If there are | Name | URL | | ------------- | ------------------------------------------------------------------- | -| Lorem Ipsum | https://lorem-ipsum-penpot-plugin.pages.dev/assets/manifest.json | -| Contrast | https://contrast-penpot-plugin.pages.dev/assets/manifest.json | -| Feather icons | https://icons-penpot-plugin.pages.dev/assets/manifest.json | -| Tables | https://table-penpot-plugin.pages.dev/assets/manifest.json | -| Color palette | https://create-palette-penpot-plugin.pages.dev/assets/manifest.json | -| Rename layers | https://rename-layers-penpot-plugin.pages.dev/assets/manifest.json | +| Color palette | https://create-palette.plugins.penpot.app/assets/manifest.json | +| Contrast | https://contrast.plugins.penpot.app/assets/manifest.json | +| Feather icons | https://icons.plugins.penpot.app/assets/manifest.json | +| Lorem ipsum | https://lorem-ipsum.plugins.penpot.app/assets/manifest.json | +| Rename layers | https://rename-layers.plugins.penpot.app/assets/manifest.json | +| Tables | https://table.plugins.penpot.app/assets/manifest.json | + ## 1.4. Plugin's basics diff --git a/plugins/CONTRIBUTING.md b/plugins/CONTRIBUTING.md index e4440cf083..10b243cd19 100644 --- a/plugins/CONTRIBUTING.md +++ b/plugins/CONTRIBUTING.md @@ -7,7 +7,7 @@ different parts of the platform, please refer to `docs/` directory. ## Reporting Bugs -We are using [GitHub Issues](https://github.com/penpot/penpot-plugins/issues) +We are using [GitHub Issues](https://github.com/penpot/penpot/issues) for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn't already exist. diff --git a/plugins/docs/api-docs.md b/plugins/docs/api-docs.md index a8cf00b157..dd38659fc9 100644 --- a/plugins/docs/api-docs.md +++ b/plugins/docs/api-docs.md @@ -19,7 +19,7 @@ the latest changes from the `main` branch. This will trigger the deployment at Cloudfare if the `libs/plugin-types/index.d.ts` or the `tools/typedoc.css` files have been updated. -Take a look at the [Penpot plugins API](https://penpot-plugins-api-doc.pages.dev/) to see what's new. +Take a look at the [Penpot plugins API](https://doc.plugins.penpot.app/) to see what's new. #### Styles diff --git a/plugins/libs/plugins-styles/README.md b/plugins/libs/plugins-styles/README.md index 6739a9c393..9291f21cfe 100644 --- a/plugins/libs/plugins-styles/README.md +++ b/plugins/libs/plugins-styles/README.md @@ -20,7 +20,7 @@ Import the CSS file into your project: For detailed examples and to see how to use the styles and components, visit the documentation at: -[Penpot Plugin Styles Documentation](https://penpot-plugins-styles.pages.dev) +[Penpot Plugin Styles Documentation](https://styles-doc.plugins.penpot.app) #### Icons From 18aca16f98d25bf3c512df42bad4c6582ddbff62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 27 Jan 2026 21:04:25 +0100 Subject: [PATCH 04/10] :wrench: Fix file name --- .github/workflows/plugins-deploy-styles-doc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugins-deploy-styles-doc.yml b/.github/workflows/plugins-deploy-styles-doc.yml index 7c759a62e6..0ab376cb6e 100644 --- a/.github/workflows/plugins-deploy-styles-doc.yml +++ b/.github/workflows/plugins-deploy-styles-doc.yml @@ -100,7 +100,7 @@ jobs: working-directory: plugins shell: bash run: | - sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-api-doc.toml + sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-styles-doc.toml - name: Deploy to Cloudflare Workers uses: cloudflare/wrangler-action@v3 From 17ffd9a5d0d1aed32cb1470ced599e5c14188065 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 28 Jan 2026 12:54:18 +0100 Subject: [PATCH 05/10] :sparkles: Backport linter fixes and config from develop --- .clj-kondo/config.edn | 9 +++++++++ backend/src/app/binfile/v3.clj | 5 +---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 1d5149d6b6..fba4cac7bc 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -45,6 +45,15 @@ :potok/reify-type {:level :error} + :redundant-primitive-coercion + {:level :off} + + :unused-excluded-var + {:level :off} + + :unresolved-excluded-var + {:level :off} + :missing-protocol-method {:level :off} diff --git a/backend/src/app/binfile/v3.clj b/backend/src/app/binfile/v3.clj index bd6c041b0a..0db826e407 100644 --- a/backend/src/app/binfile/v3.clj +++ b/backend/src/app/binfile/v3.clj @@ -873,11 +873,8 @@ (import-storage-objects cfg) (let [files (get manifest :files) - result (reduce (fn [result {:keys [id] :as file}] + result (reduce (fn [result file] (let [name' (get file :name) - name' (if (map? name) - (get name id) - name') file (assoc file :name name')] (conj result (import-file cfg file)))) [] From a9e2fc8d9498723d6b839e5d10952f96f5f605b8 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 28 Jan 2026 12:54:18 +0100 Subject: [PATCH 06/10] :sparkles: Backport linter fixes and config from develop --- .clj-kondo/config.edn | 9 +++++++++ backend/src/app/binfile/v3.clj | 5 +---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 1d5149d6b6..fba4cac7bc 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -45,6 +45,15 @@ :potok/reify-type {:level :error} + :redundant-primitive-coercion + {:level :off} + + :unused-excluded-var + {:level :off} + + :unresolved-excluded-var + {:level :off} + :missing-protocol-method {:level :off} diff --git a/backend/src/app/binfile/v3.clj b/backend/src/app/binfile/v3.clj index bd6c041b0a..0db826e407 100644 --- a/backend/src/app/binfile/v3.clj +++ b/backend/src/app/binfile/v3.clj @@ -873,11 +873,8 @@ (import-storage-objects cfg) (let [files (get manifest :files) - result (reduce (fn [result {:keys [id] :as file}] + result (reduce (fn [result file] (let [name' (get file :name) - name' (if (map? name) - (get name id) - name') file (assoc file :name name')] (conj result (import-file cfg file)))) [] From 3cb716ec30dd99b5cec0bd7c80cce364eba48c1e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 27 Jan 2026 12:47:33 +0100 Subject: [PATCH 07/10] :bug: Disable thumbnails render in wasm --- .../src/app/main/data/workspace/pages.cljs | 12 ++- .../app/main/data/workspace/thumbnails.cljs | 86 ++++++++++--------- 2 files changed, 54 insertions(+), 44 deletions(-) diff --git a/frontend/src/app/main/data/workspace/pages.cljs b/frontend/src/app/main/data/workspace/pages.cljs index 5b91d10864..5865cb969d 100644 --- a/frontend/src/app/main/data/workspace/pages.cljs +++ b/frontend/src/app/main/data/workspace/pages.cljs @@ -105,9 +105,15 @@ (if (dsh/lookup-page state file-id page-id) (rx/concat (rx/of (initialize-page* file-id page-id) - (fdf/fix-deleted-fonts-for-page file-id page-id) - (dwth/watch-state-changes file-id page-id) - (dwl/watch-component-changes)) + (fdf/fix-deleted-fonts-for-page file-id page-id)) + + ;; Disable thumbnail generation in wasm renderer + (if (features/active-feature? state "render-wasm/v1") + (rx/empty) + (rx/of (dwth/watch-state-changes file-id page-id))) + + (rx/of (dwl/watch-component-changes)) + (let [profile (:profile state) props (get profile :props)] (when (not (:workspace-visited props)) diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index a8ff8fedf9..5edab10c27 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -191,59 +191,63 @@ [page-id [event [old-data new-data]]] (let [changes (:changes event) - lookup-data-objects - (fn [data page-id] - (dm/get-in data [:pages-index page-id :objects])) + ;; cache for the get-frame-ids function + frame-id-cache (atom {})] + (letfn [(lookup-data-objects [data page-id] + (dm/get-in data [:pages-index page-id :objects])) - extract-ids - (fn [{:keys [page-id type] :as change}] - (case type - :add-obj [[page-id (:id change)]] - :mod-obj [[page-id (:id change)]] - :del-obj [[page-id (:id change)]] - :mov-objects (->> (:shapes change) (map #(vector page-id %))) - [])) + (extract-ids [{:keys [page-id type] :as change}] + (case type + :add-obj [[page-id (:id change)]] + :mod-obj [[page-id (:id change)]] + :del-obj [[page-id (:id change)]] + :mov-objects (->> (:shapes change) (map #(vector page-id %))) + [])) - get-frame-ids - (fn get-frame-ids [id] - (let [old-objects (lookup-data-objects old-data page-id) - new-objects (lookup-data-objects new-data page-id) + (get-frame-ids [id] + (let [old-objects (lookup-data-objects old-data page-id) + new-objects (lookup-data-objects new-data page-id) - new-shape (get new-objects id) - old-shape (get old-objects id) + new-shape (get new-objects id) + old-shape (get old-objects id) - old-frame-id (if (cfh/frame-shape? old-shape) id (:frame-id old-shape)) - new-frame-id (if (cfh/frame-shape? new-shape) id (:frame-id new-shape)) + old-frame-id (if (cfh/frame-shape? old-shape) id (:frame-id old-shape)) + new-frame-id (if (cfh/frame-shape? new-shape) id (:frame-id new-shape)) - root-frame-old? (cfh/root-frame? old-objects old-frame-id) - root-frame-new? (cfh/root-frame? new-objects new-frame-id) - instance-root? (ctc/instance-root? new-shape)] + root-frame-old? (cfh/root-frame? old-objects old-frame-id) + root-frame-new? (cfh/root-frame? new-objects new-frame-id) + instance-root? (ctc/instance-root? new-shape)] - (cond-> #{} - root-frame-old? - (conj ["frame" old-frame-id]) + (cond-> #{} + root-frame-old? + (conj ["frame" old-frame-id]) - root-frame-new? - (conj ["frame" new-frame-id]) + root-frame-new? + (conj ["frame" new-frame-id]) - instance-root? - (conj ["component" id]) + instance-root? + (conj ["component" id]) - (and (uuid? (:frame-id old-shape)) - (not= uuid/zero (:frame-id old-shape))) - (into (get-frame-ids (:frame-id old-shape))) + (and (uuid? (:frame-id old-shape)) + (not= uuid/zero (:frame-id old-shape))) + (into (get-frame-ids (:frame-id old-shape))) - (and (uuid? (:frame-id new-shape)) - (not= uuid/zero (:frame-id new-shape))) - (into (get-frame-ids (:frame-id new-shape))))))] + (and (uuid? (:frame-id new-shape)) + (not= uuid/zero (:frame-id new-shape))) + (into (get-frame-ids (:frame-id new-shape)))))) - (into #{} - (comp (mapcat extract-ids) - (filter (fn [[page-id']] (= page-id page-id'))) - (map (fn [[_ id]] id)) - (mapcat get-frame-ids)) - changes))) + (get-frame-ids-cached [id] + (or (get @frame-id-cache id) + (let [result (get-frame-ids id)] + (swap! frame-id-cache assoc id result) + result)))] + (into #{} + (comp (mapcat extract-ids) + (filter (fn [[page-id']] (= page-id page-id'))) + (map (fn [[_ id]] id)) + (mapcat get-frame-ids-cached)) + changes)))) (defn watch-state-changes "Watch the state for changes inside frames. If a change is detected will force a rendering From 3b86d7c1b1e78b8a023fef8e1db8906097de7acc Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Wed, 28 Jan 2026 12:25:15 +0100 Subject: [PATCH 08/10] :bug: Fix initializing rasterizer --- frontend/src/app/main.cljs | 10 ++++++++-- frontend/src/app/main/rasterizer.cljs | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index c317c8555f..a02499065f 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -16,6 +16,7 @@ [app.main.data.profile :as dp] [app.main.data.websocket :as ws] [app.main.errors] + [app.main.features :as feat] [app.main.rasterizer :as thr] [app.main.store :as st] [app.main.ui :as ui] @@ -87,7 +88,12 @@ (rx/map deref) (rx/filter dp/is-authenticated?) (rx/take 1) - (rx/map #(ws/initialize))))))) + (rx/map #(ws/initialize))))) + + ptk/EffectEvent + (effect [_ state _] + (when-not (feat/active-feature? state "render-wasm/v1") + (thr/init!))))) (defn ^:export init [options] @@ -97,7 +103,7 @@ (mw/init!) (i18n/init) (cur/init-styles) - (thr/init!) + (init-ui) (st/emit! (plugins/initialize) (initialize))) diff --git a/frontend/src/app/main/rasterizer.cljs b/frontend/src/app/main/rasterizer.cljs index 6fcb4dc8a8..3c03bede59 100644 --- a/frontend/src/app/main/rasterizer.cljs +++ b/frontend/src/app/main/rasterizer.cljs @@ -108,6 +108,7 @@ "Initializes the rasterizer." [] (let [iframe (dom/create-element "iframe")] + (dom/set-attribute! iframe "id" "rasterizer") (dom/set-attribute! iframe "src" origin) (dom/set-attribute! iframe "hidden" true) (.addEventListener js/window "message" on-message) From cc81e56d82ddd1acaa543791859f47be862923a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 28 Jan 2026 11:58:19 +0100 Subject: [PATCH 09/10] :wrench: Fix CORS error --- plugins/apps/colors-to-tokens-plugin/project.json | 1 + plugins/apps/colors-to-tokens-plugin/src/_headers | 4 ++++ plugins/apps/contrast-plugin/project.json | 1 + plugins/apps/contrast-plugin/src/_headers | 4 ++++ plugins/apps/create-palette-plugin/public/_headers | 4 ++++ plugins/apps/icons-plugin/project.json | 1 + plugins/apps/icons-plugin/src/_headers | 4 ++++ plugins/apps/lorem-ipsum-plugin/project.json | 1 + plugins/apps/lorem-ipsum-plugin/src/_headers | 4 ++++ plugins/apps/rename-layers-plugin/project.json | 1 + plugins/apps/rename-layers-plugin/src/_headers | 4 ++++ plugins/apps/table-plugin/project.json | 1 + plugins/apps/table-plugin/src/_headers | 4 ++++ 13 files changed, 34 insertions(+) create mode 100644 plugins/apps/colors-to-tokens-plugin/src/_headers create mode 100644 plugins/apps/contrast-plugin/src/_headers create mode 100644 plugins/apps/create-palette-plugin/public/_headers create mode 100644 plugins/apps/icons-plugin/src/_headers create mode 100644 plugins/apps/lorem-ipsum-plugin/src/_headers create mode 100644 plugins/apps/rename-layers-plugin/src/_headers create mode 100644 plugins/apps/table-plugin/src/_headers diff --git a/plugins/apps/colors-to-tokens-plugin/project.json b/plugins/apps/colors-to-tokens-plugin/project.json index 01ad02bbe6..87e3fb83d4 100644 --- a/plugins/apps/colors-to-tokens-plugin/project.json +++ b/plugins/apps/colors-to-tokens-plugin/project.json @@ -16,6 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/colors-to-tokens-plugin/tsconfig.app.json", "assets": [ + "apps/colors-to-tokens-plugin/src/_headers", "apps/colors-to-tokens-plugin/src/favicon.ico", "apps/colors-to-tokens-plugin/src/assets" ], diff --git a/plugins/apps/colors-to-tokens-plugin/src/_headers b/plugins/apps/colors-to-tokens-plugin/src/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/colors-to-tokens-plugin/src/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type diff --git a/plugins/apps/contrast-plugin/project.json b/plugins/apps/contrast-plugin/project.json index 6de0d44104..69f9c92766 100644 --- a/plugins/apps/contrast-plugin/project.json +++ b/plugins/apps/contrast-plugin/project.json @@ -16,6 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/contrast-plugin/tsconfig.app.json", "assets": [ + "apps/contrast-plugin/src/_headers", "apps/contrast-plugin/src/favicon.ico", "apps/contrast-plugin/src/assets" ], diff --git a/plugins/apps/contrast-plugin/src/_headers b/plugins/apps/contrast-plugin/src/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/contrast-plugin/src/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type diff --git a/plugins/apps/create-palette-plugin/public/_headers b/plugins/apps/create-palette-plugin/public/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/create-palette-plugin/public/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type diff --git a/plugins/apps/icons-plugin/project.json b/plugins/apps/icons-plugin/project.json index 6540a148e1..90f01a50c2 100644 --- a/plugins/apps/icons-plugin/project.json +++ b/plugins/apps/icons-plugin/project.json @@ -16,6 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/icons-plugin/tsconfig.app.json", "assets": [ + "apps/icons-plugin/src/_headers", "apps/icons-plugin/src/favicon.ico", "apps/icons-plugin/src/assets" ], diff --git a/plugins/apps/icons-plugin/src/_headers b/plugins/apps/icons-plugin/src/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/icons-plugin/src/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type diff --git a/plugins/apps/lorem-ipsum-plugin/project.json b/plugins/apps/lorem-ipsum-plugin/project.json index db66c3677b..1747fcf6e8 100644 --- a/plugins/apps/lorem-ipsum-plugin/project.json +++ b/plugins/apps/lorem-ipsum-plugin/project.json @@ -16,6 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/lorem-ipsum-plugin/tsconfig.app.json", "assets": [ + "apps/lorem-ipsum-plugin/src/_headers", "apps/lorem-ipsum-plugin/src/favicon.ico", "apps/lorem-ipsum-plugin/src/assets" ], diff --git a/plugins/apps/lorem-ipsum-plugin/src/_headers b/plugins/apps/lorem-ipsum-plugin/src/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/lorem-ipsum-plugin/src/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type diff --git a/plugins/apps/rename-layers-plugin/project.json b/plugins/apps/rename-layers-plugin/project.json index 9a4e67c5be..71c314986a 100644 --- a/plugins/apps/rename-layers-plugin/project.json +++ b/plugins/apps/rename-layers-plugin/project.json @@ -16,6 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/rename-layers-plugin/tsconfig.app.json", "assets": [ + "apps/rename-layers-plugin/src/_headers", "apps/rename-layers-plugin/src/favicon.ico", "apps/rename-layers-plugin/src/assets" ], diff --git a/plugins/apps/rename-layers-plugin/src/_headers b/plugins/apps/rename-layers-plugin/src/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/rename-layers-plugin/src/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type diff --git a/plugins/apps/table-plugin/project.json b/plugins/apps/table-plugin/project.json index 9ac5758270..e3cc6fc45e 100644 --- a/plugins/apps/table-plugin/project.json +++ b/plugins/apps/table-plugin/project.json @@ -16,6 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/table-plugin/tsconfig.app.json", "assets": [ + "apps/table-plugin/src/_headers", "apps/table-plugin/src/favicon.ico", "apps/table-plugin/src/assets" ], diff --git a/plugins/apps/table-plugin/src/_headers b/plugins/apps/table-plugin/src/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/table-plugin/src/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type From b40e775a7024f1eee375a120f4d6318e19e32faa Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 28 Jan 2026 20:47:14 +0100 Subject: [PATCH 10/10] :sparkles: Add minor improvements to performance events (#8217) * :sparkles: Move devtools perf logging helpers to util.perf ns * :lipstick: Move flag check to the entry point instead of initialize event * :recycle: Make performance events consistent with other events --- backend/src/app/rpc/commands/audit.clj | 2 +- frontend/src/app/main.cljs | 7 +- frontend/src/app/main/data/event.cljs | 465 +++++++++------------- frontend/src/app/main/data/workspace.cljs | 13 +- frontend/src/app/util/perf.cljs | 78 ++++ 5 files changed, 282 insertions(+), 283 deletions(-) diff --git a/backend/src/app/rpc/commands/audit.clj b/backend/src/app/rpc/commands/audit.clj index e1e5a5ef3f..754c2ce395 100644 --- a/backend/src/app/rpc/commands/audit.clj +++ b/backend/src/app/rpc/commands/audit.clj @@ -79,7 +79,7 @@ (db/insert-many! pool :audit-log event-columns events)))) (def valid-event-types - #{"action" "identify"}) + #{"action" "identify" "trigger"}) (def schema:event [:map {:title "Event"} diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index a02499065f..9994856f60 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -66,8 +66,11 @@ ptk/WatchEvent (watch [_ _ stream] (rx/merge - (rx/of (ev/initialize) - (dp/refresh-profile)) + (if (contains? cf/flags :audit-log) + (rx/of (ev/initialize)) + (rx/empty)) + + (rx/of (dp/refresh-profile)) ;; Watch for profile deletion events (->> stream diff --git a/frontend/src/app/main/data/event.cljs b/frontend/src/app/main/data/event.cljs index 7ee1d63225..cfd2cc841c 100644 --- a/frontend/src/app/main/data/event.cljs +++ b/frontend/src/app/main/data/event.cljs @@ -31,40 +31,34 @@ (l/set-level! :info) ;; Defines the maximum buffer size, after events start discarding. -(def max-buffer-size 1024) +(def ^:private ^:const max-buffer-size 1024) ;; Defines the maximum number of events that can go in a single batch. -(def max-chunk-size 100) +(def ^:private ^:const max-chunk-size 100) ;; Defines the time window (in ms) within events belong to the same session. -(def session-timeout (* 1000 60 30)) - +(def ^:private ^:const session-timeout (* 1000 60 30)) ;; Min time for a long task to be reported to telemetry -(def min-longtask-time 1000) +(def ^:private ^:const min-longtask-time 1000) ;; Min time between long task reports -(def debounce-longtask-time 1000) +(def ^:private ^:const debounce-longtask-time 1000) ;; Min time for a long task to be reported to telemetry -(def min-browser-event-time 1000) +(def ^:private ^:const min-browser-event-time 1000) ;; Min time between long task reports -(def debounce-browser-event-time 1000) +(def ^:private ^:const debounce-browser-event-time 1000) ;; Min time for a long task to be reported to telemetry -(def min-performace-event-time 1000) +(def ^:private ^:const min-performace-event-time 1000) ;; Min time between long task reports -(def debounce-performance-event-time 1000) +(def ^:private ^:const debounce-performance-event-time 1000) -;; Def micro-benchmark iterations -(def micro-benchmark-iterations 1e6) - -;; Performance logs -(defonce ^:private longtask-observer* (atom nil)) -(defonce ^:private stall-timer* (atom nil)) -(defonce ^:private current-op* (atom nil)) +;; Default micro-benchmark iterations +(def ^:private ^:const micro-benchmark-iterations 1e6) ;; --- CONTEXT @@ -142,12 +136,12 @@ data data)) -(defn add-external-context-info +(defn- add-external-context-info [context] (let [external-context-info (json/->clj (cf/external-context-info))] (merge context external-context-info))) -(defn- process-event-by-proto +(defn- make-proto-event [event] (let [data (d/deep-merge (-data event) (meta event)) type (ptk/type event) @@ -156,7 +150,6 @@ (assoc :event-origin (::origin data)) (assoc :event-namespace (namespace type)) (assoc :event-symbol ev-name) - (add-external-context-info) (d/without-nils)) props (-> data d/without-qualified simplify-props)] @@ -165,7 +158,7 @@ :context context :props props})) -(defn- process-data-event +(defn- make-data-event [event] (let [data (deref event) name (::name data)] @@ -174,7 +167,6 @@ (let [type (::type data "action") context (-> (::context data) (assoc :event-origin (::origin data)) - (add-external-context-info) (d/without-nils)) props (-> data d/without-qualified simplify-props)] {:type type @@ -182,57 +174,62 @@ :context context :props props})))) -(defn performance-payload +(defn- make-event + "Create a standard event" ([result] (let [props (aget result 0) profile-id (aget result 1)] - (performance-payload profile-id props))) + (make-event profile-id props))) + ([profile-id event] + (when-let [event (cond + (satisfies? Event event) + (make-proto-event event) + + (ptk/data-event? event) + (make-data-event event))] + (assoc event :profile-id profile-id)))) + +(defn- make-performance-event + "Create a performance trigger event" + ([result] + (let [props (aget result 0) + profile-id (aget result 1)] + (make-performance-event profile-id props))) ([profile-id props] - (let [{:keys [performance-info]} @st/state] - {:type "action" - :name "performance" - :context (merge @context performance-info) - :props props + (let [perf-info (get @st/state :performance-info) + name (get props ::name)] + {:type "trigger" + :name (str "performance-" name) + :context {:file-stats (:counters perf-info)} + :props (-> props + (dissoc ::name) + (assoc :file-id (:file-id perf-info))) :profile-id profile-id}))) (defn- process-performance-event + "Process performance sensitive events" [result] (let [event (aget result 0) profile-id (aget result 1)] - - (if (and (satisfies? PerformanceEvent event) - (exists? js/globalThis) - (exists? (.-requestAnimationFrame js/globalThis)) - (exists? (.-scheduler js/globalThis)) - (exists? (.-postTask (.-scheduler js/globalThis)))) + (if (satisfies? PerformanceEvent event) (rx/create (fn [subs] - (let [start (perf/timestamp)] + (let [start (perf/now)] (js/requestAnimationFrame - #(js/scheduler.postTask - (fn [] - (let [time (- (perf/timestamp) start)] - (when (> time min-performace-event-time) - (rx/push! - subs - (performance-payload - profile-id - {::event (str (ptk/type event)) - :time time})))) - (rx/end! subs)) - #js {"priority" "user-blocking"}))) - nil)) + #(.postTask js/scheduler + (fn [] + (let [time (- (perf/now) start)] + (when (> time min-performace-event-time) + (rx/push! subs + (make-performance-event profile-id + {::name "blocking-event" + :event-name (d/name (ptk/type event)) + :duration time}))) + (rx/end! subs))) + #js {:priority "user-blocking"})) + nil))) (rx/empty)))) -(defn- process-event - [event] - (cond - (satisfies? Event event) - (process-event-by-proto event) - - (ptk/data-event? event) - (process-data-event event))) - ;; --- MAIN LOOP (defn- append-to-buffer @@ -260,7 +257,8 @@ (rx/of nil))) -(defn performance-observer-event-stream +(defn- user-input-observer + "Create user interaction/input event observer. Returns rx stream." [] (if (and (exists? js/globalThis) (exists? (.-PerformanceObserver js/globalThis))) @@ -273,18 +271,17 @@ (fn [entry] (when (and (= "event" (.-entryType entry)) (> (.-duration entry) min-browser-event-time)) - (rx/push! - subs - {::event :observer-event - :duration (.-duration entry) - :event-name (.-name entry)}))) + (rx/push! subs {::name "user-input" + :duration (.-duration entry) + :event-name (.-name entry)}))) (.getEntries list))))] (.observe observer #js {:entryTypes #js ["event"]}) (fn [] (.disconnect observer))))) (rx/empty))) -(defn performance-observer-longtask-stream +(defn- longtask-observer + "Create a Long-Task performance observer. Returns rx stream." [] (if (and (exists? js/globalThis) (exists? (.-PerformanceObserver js/globalThis))) @@ -298,7 +295,7 @@ (when (and (= "longtask" (.-entryType entry)) (> (.-duration entry) min-longtask-time)) (rx/push! subs - {::event :observer-longtask + {::name "long-task" :duration (.-duration entry)}))) (.getEntries list))))] (.observe observer #js {:entryTypes #js ["longtask"]}) @@ -306,238 +303,156 @@ (.disconnect observer))))) (rx/empty))) -(defn- save-performance-info - [] - (ptk/reify ::save-performance-info - ptk/UpdateEvent - (update [_ state] - (letfn [(count-shapes [file] - (->> file :data :pages-index - (reduce-kv - (fn [sum _ page] - (+ sum (count (:objects page)))) - 0))) - (count-library-data [files {:keys [id]}] - (let [data (dm/get-in files [id :data])] - {:components (count (:components data)) - :colors (count (:colors data)) - :typographies (count (:typographies data))}))] - (let [file-id (get state :current-file-id) - file (get-in state [:files file-id]) - file-size (count-shapes file) +(defn- snapshot-performance-info + [{:keys [file-id]}] - libraries - (-> (refs/select-libraries (:files state) (:id file)) - (d/update-vals (partial count-library-data (:files state)))) + (letfn [(count-shapes [file] + (->> file :data :pages-index + (reduce-kv + (fn [sum _ page] + (+ sum (count (:objects page)))) + 0))) - lib-sizes - (->> libraries - (reduce-kv - (fn [acc _ {:keys [components colors typographies]}] - (-> acc - (update :components + components) - (update :colors + colors) - (update :typographies + typographies))) - {}))] - (update state :performance-info - (fn [info] - (-> info - (assoc :file-size file-size) - (assoc :library-sizes lib-sizes) - (assoc :file-start-time (perf/now)))))))))) + (add-libraries-counters [state files] + (reduce (fn [state library-id] + (let [data (dm/get-in files [library-id :data])] + (-> state + (update :total-components + (count (:components data))) + (update :total-colors + (count (:colors data))) + (update :total-typographies + (count (:typographies data)))))) + state + (refs/select-libraries files file-id)))] -(defn store-performace-info - [] - (letfn [(micro-benchmark [state] - (let [start (perf/now)] - (loop [i micro-benchmark-iterations] - (when-not (zero? i) - (* (math/sin i) (math/sqrt i)) - (recur (dec i)))) - (let [end (perf/now)] - (update state :performance-info assoc :bench-result (- end start)))))] - - (ptk/reify ::store-performace-info + (ptk/reify ::snapshot-performance-info ptk/UpdateEvent (update [_ state] - (-> state - micro-benchmark - (assoc-in [:performance-info :app-start-time] (perf/now)))) + (update state :performance-info + (fn [info] + (let [files (get state :files) + file (get files file-id)] + (-> info + (assoc :file-id file-id) + (update :counters assoc :total-shapes (count-shapes file)) + (update :counters add-libraries-counters files))))))))) - ptk/WatchEvent - (watch [_ _ stream] - (->> stream - (rx/filter (ptk/type? :app.main.data.workspace/all-libraries-resolved)) - (rx/take 1) - (rx/map save-performance-info)))))) +(defn- store-performace-info + [] + (ptk/reify ::store-performace-info + ptk/UpdateEvent + (update [_ state] + (let [start (perf/now) + _ (loop [i micro-benchmark-iterations] + (when-not (zero? i) + (* (math/sin i) (math/sqrt i)) + (recur (dec i)))) + end (perf/now)] + + (update state :performance-info assoc :bench (- end start)))) + + ptk/WatchEvent + (watch [_ _ stream] + (->> stream + (rx/filter (ptk/type? :app.main.data.workspace/all-libraries-resolved)) + (rx/take 1) + (rx/map deref) + (rx/map snapshot-performance-info))))) (defn initialize [] - (when (contains? cf/flags :audit-log) - (ptk/reify ::initialize - ptk/WatchEvent - (watch [_ _ _] - (rx/of (store-performace-info))) + (ptk/reify ::initialize + ptk/WatchEvent + (watch [_ _ _] + (rx/of (store-performace-info))) - ptk/EffectEvent - (effect [_ _ stream] - (let [session (atom nil) - stopper (rx/filter (ptk/type? ::initialize) stream) - buffer (atom #queue []) - profile (->> (rx/from-atom storage/user {:emit-current-value? true}) - (rx/map :profile) - (rx/map :id) - (rx/pipe (rxo/distinct-contiguous)))] + ptk/EffectEvent + (effect [_ _ stream] + (let [session (atom nil) + stopper (rx/filter (ptk/type? ::initialize) stream) + buffer (atom #queue []) + profile (->> (rx/from-atom storage/user {:emit-current-value? true}) + (rx/map :profile) + (rx/map :id) + (rx/pipe (rxo/distinct-contiguous)))] - (l/debug :hint "event instrumentation initialized") + (l/debug :hint "event instrumentation initialized") - (->> (rx/merge - (->> (rx/from-atom buffer) - (rx/filter #(pos? (count %))) - (rx/debounce 2000)) - (->> stream - (rx/filter (ptk/type? :app.main.data.profile/logout)) - (rx/observe-on :async))) - (rx/map (fn [_] - (into [] (take max-buffer-size) @buffer))) - (rx/with-latest-from profile) - (rx/mapcat (fn [[chunk profile-id]] - (let [events (filterv #(= profile-id (:profile-id %)) chunk)] - (->> (persist-events events) - (rx/tap (fn [_] - (l/debug :hint "events chunk persisted" :total (count chunk)))) - (rx/map (constantly chunk)))))) - (rx/take-until stopper) - (rx/subs! (fn [chunk] - (swap! buffer remove-from-buffer (count chunk))) - (fn [cause] - (l/error :hint "unexpected error on audit persistence" :cause cause)) - (fn [] - (l/debug :hint "audit persistence terminated")))) + (->> (rx/merge + (->> (rx/from-atom buffer) + (rx/filter #(pos? (count %))) + (rx/debounce 2000)) + (->> stream + (rx/filter (ptk/type? :app.main.data.profile/logout)) + (rx/observe-on :async))) + (rx/map (fn [_] + (into [] (take max-chunk-size) @buffer))) + (rx/with-latest-from profile) + (rx/mapcat (fn [[chunk profile-id]] + (let [events (filterv #(= profile-id (:profile-id %)) chunk)] + (->> (persist-events events) + (rx/tap (fn [_] + (l/debug :hint "events chunk persisted" :total (count chunk)))) + (rx/map (constantly chunk)))))) + (rx/take-until stopper) + (rx/subs! (fn [chunk] + (swap! buffer remove-from-buffer (count chunk))) + (fn [cause] + (l/error :hint "unexpected error on audit persistence" :cause cause)) + (fn [] + (l/debug :hint "audit persistence terminated")))) - (->> (rx/merge - (->> stream - (rx/with-latest-from profile) - (rx/map (fn [result] - (let [event (aget result 0) - profile-id (aget result 1)] - (some-> (process-event event) - (update :profile-id #(or % profile-id))))))) + (->> (rx/merge + (->> stream + (rx/with-latest-from profile) + (rx/map make-event)) - (->> (performance-observer-event-stream) - (rx/with-latest-from profile) - (rx/map performance-payload) - (rx/debounce debounce-browser-event-time)) + (->> (user-input-observer) + (rx/with-latest-from profile) + (rx/map make-performance-event) + (rx/debounce debounce-browser-event-time)) - (->> (performance-observer-longtask-stream) - (rx/with-latest-from profile) - (rx/map performance-payload) - (rx/debounce debounce-longtask-time)) + (->> (longtask-observer) + (rx/with-latest-from profile) + (rx/map make-performance-event) + (rx/debounce debounce-longtask-time)) + (if (and (exists? js/globalThis) + (exists? (.-requestAnimationFrame js/globalThis)) + (exists? (.-scheduler js/globalThis)) + (exists? (.-postTask (.-scheduler js/globalThis)))) (->> stream (rx/with-latest-from profile) (rx/merge-map process-performance-event) - (rx/debounce debounce-performance-event-time))) + (rx/debounce debounce-performance-event-time)) + (rx/empty))) - (rx/filter :profile-id) - (rx/map (fn [event] - (let [session* (or @session (ct/now)) - context (-> @context - (merge (:context event)) - (assoc :session session*) - (assoc :external-session-id (cf/external-session-id)) - (d/without-nils))] - (reset! session session*) - (-> event - (assoc :timestamp (ct/now)) - (assoc :context context))))) + (rx/filter :profile-id) + (rx/map (fn [event] + (let [session* (or @session (ct/now)) + context (-> @context + (merge (:context event)) + (assoc :session session*) + (assoc :external-session-id (cf/external-session-id)) + (add-external-context-info) + (d/without-nils))] + (reset! session session*) + (-> event + (assoc :timestamp (ct/now)) + (assoc :context context))))) - (rx/tap (fn [event] - (l/debug :hint "event enqueued") - (swap! buffer append-to-buffer event))) + (rx/tap (fn [event] + (l/debug :hint "event enqueued") + (swap! buffer append-to-buffer event))) - (rx/switch-map #(rx/timer session-timeout)) - (rx/take-until stopper) - (rx/subs! (fn [_] - (l/debug :hint "session reinitialized") - (reset! session nil)) - (fn [cause] - (l/error :hint "error on event batching stream" :cause cause)) - (fn [] - (l/debug :hitn "events batching stream terminated"))))))))) + (rx/switch-map #(rx/timer session-timeout)) + (rx/take-until stopper) + (rx/subs! (fn [_] + (l/debug :hint "session reinitialized") + (reset! session nil)) + (fn [cause] + (l/error :hint "error on event batching stream" :cause cause)) + (fn [] + (l/debug :hitn "events batching stream terminated")))))))) (defn event [props] (ptk/data-event ::event props)) - -;; --- DEVTOOLS PERF LOGGING - -(defn install-long-task-observer! [] - (when (and (some? (.-PerformanceObserver js/window)) (nil? @longtask-observer*)) - (let [observer (js/PerformanceObserver. - (fn [list _] - (when (contains? cf/flags :perf-logs) - (doseq [entry (.getEntries list)] - (let [dur (.-duration entry) - start (.-startTime entry) - attrib (.-attribution entry) - attrib-count (when attrib (.-length attrib)) - first-attrib (when (and attrib-count (> attrib-count 0)) (aget attrib 0)) - attrib-name (when first-attrib (.-name first-attrib)) - attrib-ctype (when first-attrib (.-containerType first-attrib)) - attrib-cid (when first-attrib (.-containerId first-attrib)) - attrib-csrc (when first-attrib (.-containerSrc first-attrib))] - - (.warn js/console (str "[perf] long task " (Math/round dur) "ms at " (Math/round start) "ms" - (when first-attrib - (str " attrib:name=" attrib-name - " ctype=" attrib-ctype - " cid=" attrib-cid - " csrc=" attrib-csrc)))))))))] - (.observe observer #js{:entryTypes #js["longtask"]}) - (reset! longtask-observer* observer)))) - -(defn start-event-loop-stall-logger! - "Log event loop stalls by measuring setInterval drift. - interval-ms: base interval - threshold-ms: drift over which we report" - [interval-ms threshold-ms] - (when (nil? @stall-timer*) - (let [last (atom (.now js/performance)) - id (js/setInterval - (fn [] - (when (contains? cf/flags :perf-logs) - (let [now (.now js/performance) - expected (+ @last interval-ms) - drift (- now expected) - current-op @current-op* - measures (.getEntriesByType js/performance "measure") - mlen (.-length measures) - last-measure (when (> mlen 0) (aget measures (dec mlen))) - meas-name (when last-measure (.-name last-measure)) - meas-detail (when last-measure (.-detail last-measure)) - meas-count (when meas-detail (unchecked-get meas-detail "count"))] - (reset! last now) - (when (> drift threshold-ms) - (.warn js/console - (str "[perf] event loop stall: " (Math/round drift) "ms" - (when current-op (str " op=" current-op)) - (when meas-name (str " last=" meas-name)) - (when meas-count (str " count=" meas-count)))))))) - interval-ms)] - (reset! stall-timer* id)))) - -(defn init! - "Install perf observers in dev builds. Safe to call multiple times. - Perf logs are disabled by default. Enable them with the :perf-logs flag in config." - [] - (when ^boolean js/goog.DEBUG - (install-long-task-observer!) - (start-event-loop-stall-logger! 50 100) - ;; Expose simple API on window for manual control in devtools - (let [api #js {:reset (fn [] - (try - (.clearMarks js/performance) - (.clearMeasures js/performance) - (catch :default _ nil)))}] - (aset js/window "PenpotPerf" api)))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 4d71f1ec46..292aef0f10 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -24,6 +24,7 @@ [app.common.types.shape :as cts] [app.common.types.variant :as ctv] [app.common.uuid :as uuid] + [app.config :as cf] [app.main.data.changes :as dch] [app.main.data.comments :as dcmt] [app.main.data.common :as dcm] @@ -75,6 +76,7 @@ [app.util.dom :as dom] [app.util.globals :as ug] [app.util.http :as http] + [app.util.perf :as perf] [app.util.storage :as storage] [app.util.timers :as tm] [app.util.webapi :as wapi] @@ -195,7 +197,7 @@ (rx/of (check-libraries-synchronization file-id libraries)))))) ;; This events marks that all the libraries have been resolved - (rx/of (ptk/data-event ::all-libraries-resolved))) + (rx/of (ptk/data-event ::all-libraries-resolved {:file-id file-id}))) (rx/take-until stopper-s)))))) (defn- workspace-initialized @@ -348,10 +350,11 @@ :file-id file-id})))))) ;; Install dev perf observers once the workspace is ready - (->> stream - (rx/filter (ptk/type? ::workspace-initialized)) - (rx/take 1) - (rx/map (fn [_] (ev/init!)))) + (when (contains? cf/flags :perf-logs) + (->> stream + (rx/filter (ptk/type? ::workspace-initialized)) + (rx/take 1) + (rx/tap (fn [_] (perf/setup))))) (->> stream (rx/filter (ptk/type? ::dps/persistence-notification)) diff --git a/frontend/src/app/util/perf.cljs b/frontend/src/app/util/perf.cljs index 868f2eb2b4..e3fb8fce47 100644 --- a/frontend/src/app/util/perf.cljs +++ b/frontend/src/app/util/perf.cljs @@ -169,3 +169,81 @@ (let [end (timestamp)] (println (str "[" event "]" (- end start))))) #js {"priority" "user-blocking"}))))) + +;; --- DEVTOOLS PERF LOGGING + +(defonce ^:private longtask-observer* (atom nil)) +(defonce ^:private stall-timer* (atom nil)) +(defonce ^:private current-op* (atom nil)) + +(defn- install-long-task-observer + [] + (when (and (some? (.-PerformanceObserver js/window)) (nil? @longtask-observer*)) + (let [observer (js/PerformanceObserver. + (fn [list _] + (doseq [entry (.getEntries list)] + (let [dur (.-duration entry) + start (.-startTime entry) + attrib (.-attribution entry) + attrib-count (when attrib (.-length attrib)) + first-attrib (when (and attrib-count (> attrib-count 0)) (aget attrib 0)) + attrib-name (when first-attrib (.-name first-attrib)) + attrib-ctype (when first-attrib (.-containerType first-attrib)) + attrib-cid (when first-attrib (.-containerId first-attrib)) + attrib-csrc (when first-attrib (.-containerSrc first-attrib))] + + (.warn js/console (str "[perf] long task " (Math/round dur) "ms at " (Math/round start) "ms" + (when first-attrib + (str " attrib:name=" attrib-name + " ctype=" attrib-ctype + " cid=" attrib-cid + " csrc=" attrib-csrc))))))))] + (.observe observer #js{:entryTypes #js["longtask"]}) + (reset! longtask-observer* observer)))) + +(defn- start-event-loop-stall-logger + "Log event loop stalls by measuring setInterval drift. + + Params: + - interval-ms: base interval + - threshold-ms: drift over which we report + " + [interval-ms threshold-ms] + (when (nil? @stall-timer*) + (let [last (atom (.now js/performance)) + id (js/setInterval + (fn [] + (let [now (.now js/performance) + expected (+ @last interval-ms) + drift (- now expected) + current-op @current-op* + measures (.getEntriesByType js/performance "measure") + mlen (.-length measures) + last-measure (when (> mlen 0) (aget measures (dec mlen))) + meas-name (when last-measure (.-name last-measure)) + meas-detail (when last-measure (.-detail last-measure)) + meas-count (when meas-detail (unchecked-get meas-detail "count"))] + (reset! last now) + (when (> drift threshold-ms) + (.warn js/console + (str "[perf] event loop stall: " (Math/round drift) "ms" + (when current-op (str " op=" current-op)) + (when meas-name (str " last=" meas-name)) + (when meas-count (str " count=" meas-count))))))) + interval-ms)] + (reset! stall-timer* id)))) + +(defn setup + "Install perf observers in dev builds. Safe to call multiple times. + Perf logs are disabled by default. Enable them with the :perf-logs + flag in config." + [] + (install-long-task-observer) + (start-event-loop-stall-logger 50 100) + ;; Expose simple API on window for manual control in devtools + (let [api #js {:reset (fn [] + (try + (.clearMarks js/performance) + (.clearMeasures js/performance) + (catch :default _ nil)))}] + (unchecked-set js/window "PenpotPerf" api)))