Skip to content

perf: reduce memory allocation pressure in VTEX transforms and CSV#1535

Merged
aka-sacci-ccr merged 4 commits intomainfrom
sacci-mem-enhances
Mar 2, 2026
Merged

perf: reduce memory allocation pressure in VTEX transforms and CSV#1535
aka-sacci-ccr merged 4 commits intomainfrom
sacci-mem-enhances

Conversation

@aka-sacci-ccr
Copy link
Contributor

@aka-sacci-ccr aka-sacci-ccr commented Feb 25, 2026

…direct parsing

Addresses memory profiling findings — the main bottleneck is ephemeral object allocation rate (not leaks), which increases GC pressure under high traffic.

Changes:

  • Internalize schema.org URL strings as module-level constants so V8 reuses a single reference instead of allocating per-call copies
  • Extract shared buildOffer() to eliminate the spread copy in toOfferLegacy ({...toOffer(seller), teasers})
  • Reuse options object in toProduct recursive calls instead of spreading {…options, imagesByKey} per variant
  • Replace .concat() chains with push()-based array building
  • Mutate product offers in-place during simulation instead of creating spread copies of every offer/product/spec object
  • Merge 4-pass CSV parsing in redirectsFromCsv into a single loop producing Route[] directly, eliminating 3 intermediate arrays

What is this Contribution About?

Please provide a brief description of the changes or enhancements you are proposing in this pull request.

Issue Link

Please link to the relevant issue that this pull request addresses:

Loom Video

Record a quick screencast describing your changes to help the team understand and review your contribution. This will greatly assist in the review process.

Demonstration Link

Provide a link to a branch or environment where this pull request can be tested and seen in action.


Summary by cubic

Reduce transient allocations in VTEX transforms, offer simulation, and CSV redirect parsing to lower GC pressure and improve throughput. Fix zero-price handling and prevent options mutation across product recursion.

  • Refactors

    • Centralized schema.org constants (price types, availability, installment) and used in offers and inStock checks.
    • Shared buildOffer with unified teaser handling for legacy sellers.
    • toProduct: reuse options/imagesByKey across recursion and build additionalProperty via push; avoid extra arrays in legacy attachments.
    • Simulation: mutate offers/specs in place and re-aggregate only when prices change.
    • redirectsFromCsv: single-pass parser producing Route[] and skipping empty or self-referential rows.
  • Bug Fixes

    • Simulation: explicit null checks preserve zero sale/list prices.
    • toProduct: stop mutating caller options.imagesByKey.

Written for commit 74c8fb6. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed redirect parsing to drop invalid or self-referential routes.
    • Ensured simulated price updates propagate correctly across products and variants.
  • Refactor

    • Optimized price update flow to mutate offers in place and only recompute aggregates when needed.
  • Chores

    • Standardized price and availability schema values and streamlined offer construction.

…direct parsing

Addresses memory profiling findings — the main bottleneck is ephemeral
object allocation rate (not leaks), which increases GC pressure under
high traffic.

Changes:
- Internalize schema.org URL strings as module-level constants so V8
  reuses a single reference instead of allocating per-call copies
- Extract shared buildOffer() to eliminate the spread copy in
  toOfferLegacy ({...toOffer(seller), teasers})
- Reuse options object in toProduct recursive calls instead of
  spreading {…options, imagesByKey} per variant
- Replace .concat() chains with push()-based array building
- Mutate product offers in-place during simulation instead of creating
  spread copies of every offer/product/spec object
- Merge 4-pass CSV parsing in redirectsFromCsv into a single loop
  producing Route[] directly, eliminating 3 intermediate arrays
@github-actions
Copy link
Contributor

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 0.135.3 update
  • 🎉 for Minor 0.136.0 update
  • 🚀 for Major 1.0.0 update

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae18f9c and 74c8fb6.

📒 Files selected for processing (1)
  • vtex/utils/extensions/simulation.ts

📝 Walkthrough

Walkthrough

Adds schema constants and refactors offer construction; simulation now mutates product offers in place and selectively re-aggregates when prices change; CSV redirect parsing switched from functional chaining to an explicit loop with inline validation. (48 words)

Changes

Cohort / File(s) Summary
Schema & Offer refactor
vtex/utils/transform.ts
Adds SCHEMA_* constants (ListPrice, SalePrice, SRP, Installment, InStock, OutOfStock). Introduces buildOffer and refactors toOffer/toOfferLegacy. Updates inStock predicate and lazily handles imagesByKey/variant toProduct calls. Adjusts additionalProperty assembly and teaser handling.
Simulation in-place mutation
vtex/utils/extensions/simulation.ts
Changes fixOffer to mutate product.offers in place rather than returning new objects. Computes salePrice/listPrice from simulated data, updates price and priceSpecification entries, tracks a changed flag, and re-aggregates with aggregateOffers only when prices changed. Applies mutations to products and their variants.
CSV redirect parsing
website/loaders/redirectsFromCsv.ts
Replaces a functional map/filter chain with an imperative loop that parses CSV rows, applies type and discardQueryParameters logic, validates and skips invalid or self-referential routes, and accumulates Route-like objects. No external behavior change expected.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested reviewers

  • vibegui

Poem

🐇 I nibbled schemas, hopped through each line,
Mutated offers, kept aggregates fine.
CSV rows checked in a tidy loop,
I danced on code with a carrot-snoot.
Hooray — clean changes, small and divine!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main objective: reducing memory allocation pressure across VTEX transforms and CSV parsing.
Description check ✅ Passed The description exceeds template requirements with detailed technical changes, refactoring details, and bug fixes, though it leaves template placeholders unfilled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sacci-mem-enhances

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
vtex/utils/transform.ts (1)

456-467: Guards on always-truthy arrays are misleading.

toAdditionalPropertyCategories, toAdditionalPropertyClusters, and toAdditionalPropertyReferenceIds all return PropertyValue[] (possibly empty), never null or undefined. An empty array is truthy, so if (categoryAdditionalProperties) and the others are always true — the push always runs, even spreading [] as a no-op. The guards add no protection and may confuse future readers.

♻️ Simplify to unconditional pushes
-  if (categoryAdditionalProperties) {
-    additionalProperty.push(...categoryAdditionalProperties);
-  }
-  if (clusterAdditionalProperties) {
-    additionalProperty.push(...clusterAdditionalProperties);
-  }
-  if (referenceIdAdditionalProperty) {
-    additionalProperty.push(...referenceIdAdditionalProperty);
-  }
+  additionalProperty.push(...categoryAdditionalProperties);
+  additionalProperty.push(...clusterAdditionalProperties);
+  additionalProperty.push(...referenceIdAdditionalProperty);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/utils/transform.ts` around lines 456 - 467, The if-guards around pushing
categoryAdditionalProperties, clusterAdditionalProperties, and
referenceIdAdditionalProperty are misleading because
toAdditionalPropertyCategories, toAdditionalPropertyClusters, and
toAdditionalPropertyReferenceIds return PropertyValue[] (never null/undefined),
so empty arrays are truthy; remove the unnecessary conditionals and
unconditionally merge those arrays into additionalProperty (e.g.,
push(...categoryAdditionalProperties) / push(...clusterAdditionalProperties) /
push(...referenceIdAdditionalProperty) or initialize additionalProperty with all
spreads) and keep references to additionalProperty,
specificationsAdditionalProperty, categoryAdditionalProperties,
clusterAdditionalProperties, and referenceIdAdditionalProperty for locating the
change.
website/loaders/redirectsFromCsv.ts (1)

68-69: from/to variable names shadow the outer from parameter.

from declared here shadows the from parameter of getRedirectFromFile (Line 37). TypeScript scoping keeps this safe, but it can mislead readers who scan the function signature. Consider csvFrom/csvTo or sourcePath/targetPath.

♻️ Proposed rename
-    const from = parts[0];
-    const to = parts[1];
+    const csvFrom = parts[0];
+    const csvTo = parts[1];

-    if (!from || !to || from === to) continue;
+    if (!csvFrom || !csvTo || csvFrom === csvTo) continue;

     routes.push({
-      pathTemplate: from,
+      pathTemplate: csvFrom,
       isHref: true,
       handler: {
         value: {
           __resolveType: "website/handlers/redirect.ts",
-          to,
+          to: csvTo,
           type: type as Redirect["type"],
           discardQueryParameters,
         },
       },
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/loaders/redirectsFromCsv.ts` around lines 68 - 69, The local
variables assigned from parts should be renamed so they don't shadow the outer
parameter `from` of getRedirectFromFile; change the two locals (currently
declared as `from` and `to`) to clearer names like `csvFrom` and `csvTo` (or
`sourcePath`/`targetPath`) and update all subsequent uses in getRedirectFromFile
accordingly so the outer `from` parameter remains unshadowed and the intent is
clear.
vtex/utils/extensions/simulation.ts (1)

111-112: changed is set even when the simulated price equals the existing offer price.

o.price = salePrice; changed = true; unconditionally marks the product as changed whenever a matching simulated offer exists. If the simulation returns the same price that is already on the offer, aggregateOffers (which sorts in-place) is still invoked, providing no benefit. The optimisation in this PR is slightly undermined for the stable-price case.

♻️ Proposed fix — only set `changed` on an actual price difference
+      if (o.price !== salePrice) {
         o.price = salePrice;
         changed = true;
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/utils/extensions/simulation.ts` around lines 111 - 112, The code
currently sets o.price = salePrice and changed = true unconditionally; update
the block that handles the simulated offer (the variables o, salePrice, and
changed) to first compare the existing o.price to salePrice and only assign
o.price and set changed = true when they differ (use a strict/normalized numeric
comparison to avoid false positives from type/coercion). Ensure any subsequent
call that relies on changed (e.g., aggregateOffers) only runs when changed is
true so stable-price cases skip the in-place sort.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@vtex/utils/extensions/simulation.ts`:
- Around line 109-110: Replace the current falsy checks with explicit
null/undefined checks so a simulated value of 0 is preserved: change the
salePrice assignment to use simulated.price != null ? simulated.price / 100 :
o.price and change listPrice to simulated.listPrice != null ?
simulated.listPrice / 100 : undefined (or whatever the previous “no simulated
listPrice” sentinel was). Update the assignments around the salePrice and
listPrice variables in simulation.ts where those symbols are defined to ensure
both zero and non-zero simulated values are handled consistently.

In `@vtex/utils/transform.ts`:
- Around line 375-383: The code currently mutates the caller's options by
setting options.imagesByKey, causing cross-product contamination; change this to
create a local imagesByKey variable (built from items via getImageKey) instead
of assigning into options, and update any recursive or subsequent calls to
toProduct/toProductVariant to accept and propagate that local imagesByKey (pass
it through the call chain where options.imagesByKey was used) so the caller's
options object is never mutated. Ensure all references that read
options.imagesByKey use the new local imagesByKey variable or the propagated
map.

---

Nitpick comments:
In `@vtex/utils/extensions/simulation.ts`:
- Around line 111-112: The code currently sets o.price = salePrice and changed =
true unconditionally; update the block that handles the simulated offer (the
variables o, salePrice, and changed) to first compare the existing o.price to
salePrice and only assign o.price and set changed = true when they differ (use a
strict/normalized numeric comparison to avoid false positives from
type/coercion). Ensure any subsequent call that relies on changed (e.g.,
aggregateOffers) only runs when changed is true so stable-price cases skip the
in-place sort.

In `@vtex/utils/transform.ts`:
- Around line 456-467: The if-guards around pushing
categoryAdditionalProperties, clusterAdditionalProperties, and
referenceIdAdditionalProperty are misleading because
toAdditionalPropertyCategories, toAdditionalPropertyClusters, and
toAdditionalPropertyReferenceIds return PropertyValue[] (never null/undefined),
so empty arrays are truthy; remove the unnecessary conditionals and
unconditionally merge those arrays into additionalProperty (e.g.,
push(...categoryAdditionalProperties) / push(...clusterAdditionalProperties) /
push(...referenceIdAdditionalProperty) or initialize additionalProperty with all
spreads) and keep references to additionalProperty,
specificationsAdditionalProperty, categoryAdditionalProperties,
clusterAdditionalProperties, and referenceIdAdditionalProperty for locating the
change.

In `@website/loaders/redirectsFromCsv.ts`:
- Around line 68-69: The local variables assigned from parts should be renamed
so they don't shadow the outer parameter `from` of getRedirectFromFile; change
the two locals (currently declared as `from` and `to`) to clearer names like
`csvFrom` and `csvTo` (or `sourcePath`/`targetPath`) and update all subsequent
uses in getRedirectFromFile accordingly so the outer `from` parameter remains
unshadowed and the intent is clear.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf90915 and 937ee49.

📒 Files selected for processing (3)
  • vtex/utils/extensions/simulation.ts
  • vtex/utils/transform.ts
  • website/loaders/redirectsFromCsv.ts

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="vtex/utils/transform.ts">

<violation number="1" location="vtex/utils/transform.ts:375">
P2: Mutating `options.imagesByKey` makes `toProduct` reuse the first product’s image map for all subsequent products in the same request, which can mis-resolve image URLs across different products. Avoid mutating the shared options object and pass a per-product `imagesByKey` instead.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

…in toProduct

- simulation.ts: use explicit null checks (!= null) instead of truthy
  checks for simulated.price and simulated.listPrice, so a value of 0
  is preserved instead of falling back to the original price
- transform.ts: stop mutating the caller's options.imagesByKey which
  caused cross-product contamination when the same options object was
  shared across a .map(); compute imagesByKey locally and create one
  variantOptions object only when needed for recursive calls

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
vtex/utils/transform.ts (1)

459-470: Optional: the if guards on categoryAdditionalProperties, clusterAdditionalProperties, and referenceIdAdditionalProperty are always truthy.

All three are produced by .map() calls in toAdditionalPropertyCategories, toAdditionalPropertyClusters, and toAdditionalPropertyReferenceIds, so they are always PropertyValue[] (possibly empty, never null/undefined). An empty array is truthy, so the guards never short-circuit. They add a false implication that these values can be absent.

🧹 Proposed cleanup
- const additionalProperty: PropertyValue[] = [
-   ...specificationsAdditionalProperty,
- ];
- if (categoryAdditionalProperties) {
-   additionalProperty.push(...categoryAdditionalProperties);
- }
- if (clusterAdditionalProperties) {
-   additionalProperty.push(...clusterAdditionalProperties);
- }
- if (referenceIdAdditionalProperty) {
-   additionalProperty.push(...referenceIdAdditionalProperty);
- }
+ const additionalProperty: PropertyValue[] = [
+   ...specificationsAdditionalProperty,
+   ...categoryAdditionalProperties,
+   ...clusterAdditionalProperties,
+   ...referenceIdAdditionalProperty,
+ ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vtex/utils/transform.ts` around lines 459 - 470, The if guards around
categoryAdditionalProperties, clusterAdditionalProperties, and
referenceIdAdditionalProperty are redundant because those values are always
arrays (from toAdditionalPropertyCategories / toAdditionalPropertyClusters /
toAdditionalPropertyReferenceIds) and an empty array is truthy; remove the
conditional checks and either push their contents unconditionally
(push(...categoryAdditionalProperties), push(...clusterAdditionalProperties),
push(...referenceIdAdditionalProperty)) or build additionalProperty by
concatenating all arrays (e.g., [...specificationsAdditionalProperty,
...categoryAdditionalProperties, ...clusterAdditionalProperties,
...referenceIdAdditionalProperty]) to simplify the logic and avoid implying they
can be null/undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@vtex/utils/extensions/simulation.ts`:
- Around line 115-116: The code sets changed = true unconditionally after
assigning o.price = salePrice, which causes aggregateOffers to run even when
salePrice equals the existing o.price (notably when simulated.price == null and
salePrice falls back to o.price). Modify the logic in the function handling
offers (references: variables o.price, salePrice, changed and the
aggregateOffers call) so that you only set changed = true when the new salePrice
is different from the current o.price (e.g., compare salePrice !== o.price
before assignment) — update the assignment to only mutate o.price and flip
changed when the values differ to prevent unnecessary re-aggregation.

---

Nitpick comments:
In `@vtex/utils/transform.ts`:
- Around line 459-470: The if guards around categoryAdditionalProperties,
clusterAdditionalProperties, and referenceIdAdditionalProperty are redundant
because those values are always arrays (from toAdditionalPropertyCategories /
toAdditionalPropertyClusters / toAdditionalPropertyReferenceIds) and an empty
array is truthy; remove the conditional checks and either push their contents
unconditionally (push(...categoryAdditionalProperties),
push(...clusterAdditionalProperties), push(...referenceIdAdditionalProperty)) or
build additionalProperty by concatenating all arrays (e.g.,
[...specificationsAdditionalProperty, ...categoryAdditionalProperties,
...clusterAdditionalProperties, ...referenceIdAdditionalProperty]) to simplify
the logic and avoid implying they can be null/undefined.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 937ee49 and ae18f9c.

📒 Files selected for processing (2)
  • vtex/utils/extensions/simulation.ts
  • vtex/utils/transform.ts

@aka-sacci-ccr aka-sacci-ccr merged commit 713d250 into main Mar 2, 2026
4 checks passed
@aka-sacci-ccr aka-sacci-ccr deleted the sacci-mem-enhances branch March 2, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants