The Planning Quest Parts IV–V
0 / 1,650 XP
📖 I–III ⚔ IV–V 👑 VI–IX 📘 Gr.I ⚡ Gr.II ⚙ Kernel 🏆 Engage 👥 WFP 📝 Quiz 🃏 Cards 🔬 Labs 🎯 Prep 🌐 REST ⚙ CLI 💻 Lab 🆕 New 🔍 Search
Navigation
🏠
Quest Map
Parts I–III
Part IV · 500 XP
Part IV
Planning Camps
Input Forms & Rolling Forecast Action Menus & Workflow 🪟 Forms 2.0 — The New Standard
Part V · 1,150 XP
Part V
Groovy Battle Saga
Business Rules & Groovy Primer ⚔ Battle 1 — Revenue Spread ⚔ Battle 2 — Allocations ⚔ Battle 3 — Validations ⚔ Battle 4 — Debug & Optimize
Continue
🛡 Parts VI–IX →
Practice
📝 Practice Quiz 🃏 Flashcards 🔬 Enterprise Labs
Full Quest Navigation
The Quest
🏠
Quest Map
Core Lessons
📖 Parts I–III (Fundamentals) ⚔ Parts IV–V (Forms + Groovy) 👑 Parts VI–IX (Workflow + Boss)
Groovy Mastery
📘 Groovy Chapter I (Basics) ⚡ Groovy Chapter II (EPM API) 💻 Groovy Lab (Interactive)
Deep Dives
⚙ Essbase Kernel (NODE s) 👥 Workforce Planning 🌐 REST API Cookbook ⚙ EPM Automate CLI Ref
Practice & Labs
📝 Practice Quiz 🃏 Flashcards 🔬 Enterprise Labs 🎯 Interview Prep
Tools & Reference
🏆 The Engagement 🆕 What's New in EPM 🔍 Search 📊 Capability Framework 🎮 Simulators
⛺ Part IV · Lesson 9 · 150 XP

⛺ Input Forms & Rolling Forecast

Part IV · Planning Camps · Lesson 1 of 2

The Planner's Quarters

The data flows in, the cube calculates, but Vision's planners still have no way to enter their forecasts. You must build their camp — the planning forms where hundreds of users will input their budgets and rolling forecasts. Build them wrong, and the mutiny will be swift.

Planning Form Types

Form TypeDescriptionVision Use Case
Simple FormSingle grid, one plan typeDepartment expense input by month
Composite FormMultiple grids + tabsFull P&L with driver-based inputs
Ad-Hoc GridFlexible analysis, free pivotingFP&A analyst variance exploration
Smart FormCustom HTML/JS UI overlayComplex driver input with visual business logic

Rolling 12-Month Forecast

Vision uses a 12-month rolling forecast — always forecasting 12 months ahead regardless of where in the fiscal year we are.

📊Rolling vs Static: A static forecast closes at Dec 31. In November, you have 2 months of visibility. A rolling 12-month forecast means in November 2025, you're already forecasting through October 2026. CFOs and investors strongly prefer rolling forecasts for capital allocation decisions.
Rolling Forecast Form Column Setup
// Form Page: Scenario=Forecast, Version=Working // Column range — rolls forward each month automatically &CurPeriod:&CurPeriod+11 // 12 months from current period subvar // Lock Actual months — read-only so planners can't overwrite // In form: set column property "Read Only" for Actual periods // Use: @ISANCESTOR("Actual", @CURRMBR(Scenario))

Form Design Best Practices

  • Suppress zeros: Don't show users 200 empty rows — suppress missing/zero rows
  • Use member selectors: Descendants(Total_Revenue) auto-includes new accounts as hierarchy changes
  • Data validation: Restrict input to reasonable ranges (headcount 0–10,000)
  • Supporting detail: Allow planners to add text notes explaining assumptions
⛺ Quest Tasks — Lesson 9
Create a simple department expense input form
Configure 12-month rolling column range using substitution variables
Set Actual period months to read-only in the rolling form
Apply suppress zeros and test with a planner user account
🔭
The Oracle
Forms & Planning Expert
⛺ Part IV · Lesson 10 · 150 XP

🎯 Action Menus & Planning Workflow

Part IV · Planning Camps · Lesson 2 of 2

The Warlord's Commands

Action Menus are Vision's chain of command — the right-click menus that trigger business rules, launch workflows, copy data, and execute complex operations with a single click. Build them well and planners will call you a wizard.

Action Menus

Action Menus appear when a user right-clicks on a form cell, row, or column. They trigger: business rules, navigation to other forms, data copy operations, or external URL launches.

💡FP&A Power Move: Build an "Spread Annual to Monthly" action menu item. One click triggers a Groovy rule that takes the annual total a planner entered and distributes it to 12 months using historical seasonality. This eliminates one of the most tedious manual tasks in budgeting.

Budget Approval Workflow

Draft
Planner enters
📤
Submit
Sends to mgr
🔍
Review
Manager checks
Push Back
Rejected w/ note
Approved
Finance locks

Planning Unit Hierarchy (PUH) defines the approval chain. Each Entity+Scenario combination is a Planning Unit. States flow upward through manager layers until Corporate Finance gives final approval.

⛺ Quest Tasks — Lesson 10
Create an Action Menu item "Copy Budget to Working"
Build "Spread Annual to Monthly" Action Menu item
Set up Planning Unit Hierarchy for Vision entities
Test the Draft → Submit → Approve workflow for one entity
🔭
The Oracle
Workflow Expert
⛺ Part IV · Lesson 11 · 200 XP

🪟 Forms 2.0 — The New Standard

Part IV · Planning Camps · Lesson 3 of 3 · EPB-MOD-04-L3

The Architect's Upgrade

Oracle quietly changed the rules. Forms 2.0 — released progressively from EPM 25.06 — is not just a visual refresh. It is a stateless, API-driven rendering engine that replaces the classic form framework entirely. If you deploy a composite form today without understanding Forms 2.0, you will hit walls you cannot explain. Master it, and you build forms that are faster, more flexible, and future-proof.

Classic Forms vs Forms 2.0 — What Actually Changed

Forms 2.0 is Oracle's complete rewrite of the planning form rendering layer. The business user sees a similar grid, but the architecture underneath is fundamentally different — and those differences have direct consequences for how you configure, debug, and optimize forms.

DimensionClassic FormsForms 2.0
ArchitectureStateful — server holds form session state between interactionsStateless — each interaction is an independent API call with full context
Rendering engineLegacy ADF (Oracle Application Development Framework)Oracle JET 2.0 — modern JavaScript component library
Composite formsMultiple grids share a session; context passes implicitlyEach grid is independent; context must be explicitly passed via Apply Context
Runtime PromptsEvaluated once at form load; cached in sessionEvaluated per-API-call; must be explicitly re-passed to each grid in a composite
Member formula displayDynamic Calc members shown inline, re-evaluated on each cell changeDynamic Calc members retrieved via separate API call — may require explicit refresh
Performance profileSlow initial load (full session setup); faster navigation within sessionFast initial load (stateless); each interaction is a targeted API call
Mobile supportLimited — ADF was not designed for mobile viewportsFully responsive — JET 2.0 is mobile-first
Available fromAll EPM versionsEPM 25.06+ (full Forms 2.0 with composite support)
Stateless is the key mental shift. In classic forms, when a planner changes a page member selector, the server remembers the new context for all subsequent interactions in that session. In Forms 2.0, the page member change fires an API call — and that API call must carry the full POV context every time. If your composite form has Grid A and Grid B and the planner changes the entity selector in Grid A, Grid B does NOT automatically know about it unless you have configured Apply Context.

The 12-Component Dashboard Limit — NODE Z Governor

Forms 2.0 enforces a hard limit of 12 components per dashboard. This is a system governor — not a recommendation. Exceeding it causes the dashboard to fail silently on load in some versions, or throw an error in others. Plan your dashboard layout before building.

Component TypeCounts Toward 12?Notes
Planning form grid✅ YesEach grid = 1 component regardless of size
Chart✅ YesEach chart = 1 component
Infolet✅ YesEach infolet = 1 component
Tile (static text/image)✅ YesEven static tiles count
Navigation flow shortcut❌ NoNavigation items are not dashboard components
🚨NODE Z Hard Limit: 12 components per dashboard. This is not a soft guideline — it is a system ceiling. Vision Corp's original CFO dashboard design had 16 components. Rebuilding it to fit within 12 required consolidating 4 separate entity charts into a single multi-series chart and merging 2 KPI infolets. Design to the limit from the start.

Composite Forms and Apply Context — The Critical Configuration

A composite form in Forms 2.0 contains multiple grids displayed together. The challenge: each grid is stateless and independent. When a planner selects Entity = Vision_US in Grid A's page dimension, Grid B does not automatically filter to Vision_US — unless you configure Apply Context.

How Apply Context Works

Apply Context is a Forms 2.0 configuration that creates a dependency between grids. When the page member of the source grid changes, the target grid's matching dimension is automatically updated and the grid re-queries. This replaces the implicit session state sharing of classic forms.

ScenarioClassic Forms BehaviourForms 2.0 Behaviour
Planner changes Entity page selector in Grid AAll grids in composite automatically use the new entity (shared session)Only Grid A updates. Grid B stays on previous entity unless Apply Context is configured
RTP (Runtime Prompt) changes valueNew RTP value propagates to all grids automaticallyEach grid must have Apply Context configured to receive the new RTP value
Master-Detail relationship (Grid A drives Grid B)Implicit — selection in Grid A flows to Grid BExplicit — configure "Master" grid, "Detail" grid, and the dimension that links them
💡Apply Context Setup: In the composite form designer, select a grid → Properties → Apply Context. Set: Source grid = "Master", Target dimension = Entity (or whichever dimension should sync). The target grid's Entity page selection will now mirror the master grid whenever it changes. For Vision Corp's full P&L composite: Grid A (Revenue inputs) is master, Grid B (Cost inputs) and Grid C (KPI summary) are both targets on the Entity dimension.

Runtime Prompts in Forms 2.0 — The Stateless Trap

Runtime Prompts (RTPs) in Forms 2.0 behave differently from classic forms in one critical way: because each grid interaction is an independent API call, the RTP value must be explicitly re-passed on each call. This is handled automatically by Forms 2.0 when RTPs are properly configured — but fails silently when they are not.

SituationClassic FormsForms 2.0
RTP defined at form levelValue shared across all grids automaticallyValue must be mapped to each grid that uses it via Apply Context or form-level RTP binding
RTP value not passed to a gridGrid uses cached session value (often correct)Grid uses the RTP default value — silently ignores the user's selection
Multi-grid RTP dependenciesOne RTP, all grids automatically updateConfigure RTP binding explicitly for each grid in the composite form designer
🚨Silent RTP Failure Pattern: In Forms 2.0, if an RTP is not properly bound to a grid, the grid uses the RTP default value instead of the user's selection — with no error message. A planner selects "FY2026" from the Year RTP, but Grid B (not bound) silently shows FY2025 data. The planner doesn't notice. This is the #1 Forms 2.0 support ticket in production environments. Always test every grid's RTP binding explicitly after configuring a composite form.

Classic vs Forms 2.0 Feature Gap — What's Not There Yet

As of EPM 25.11, Forms 2.0 has not fully replicated every classic form feature. Know the gaps before committing to a Forms 2.0 migration.

FeatureClassic FormsForms 2.0 (EPM 25.11)
Supporting Detail (cell-level text notes)✅ Full support⚠ Available but limited formatting options
Cell-level annotations✅ Full support✅ Supported in EPM 25.09+
Spreading (distribute values across periods)✅ All spread patterns✅ Full support
Ad hoc analysis from form✅ One-click pivot to Smart View⚠ Smart View integration requires Forms 2.0 Smart View plugin (EPM 25.10+)
Form-level member selectors for page✅ Full support✅ Supported
Dependent dropdowns (cascading member selectors)✅ Supported⚠ Partial — some dependency chains require classic
Custom JavaScript overlays (Smart Forms)✅ Supported⚠ Under review — check latest EPM release notes

Vision Corp Forms 2.0 Migration — Decision Framework

Not every form needs to be migrated to Forms 2.0 immediately. Use this decision framework to prioritise.

Migrate to Forms 2.0 FirstKeep on Classic (for now)
New forms being built from scratchExisting complex composite forms that are stable and not causing issues
Mobile-accessed forms (field finance teams)Forms using Smart Form (custom JS overlay) functionality
Forms accessed by 50+ concurrent users (performance benefit)Forms relying on complex dependent dropdown cascades
Dashboard-embedded forms (12-component limit applies)Forms where Supporting Detail formatting is critical
Any new form built after EPM 25.06Forms scheduled for decommission within 12 months
📊FP&A Production Reality: Vision Corp's migration timeline: Q1 FY2026 — all new forms built in Forms 2.0. Q2 FY2026 — simple input forms migrated. Q3 FY2026 — composite forms migrated after the July forecast cycle completes (never migrate during an active planning cycle). Q4 FY2026 — decommission all classic forms. This 12-month runway respects the budget and forecast cycle constraints that govern when form changes are safe to deploy.

Forms 2.0 in the Vision Corp CFO Dashboard

Vision Corp's CFO Dashboard uses Forms 2.0 with a 4-grid composite layout — exactly at the 12-component limit when combined with 4 charts and 4 infolets.

#ComponentTypeApply Context Role
1Entity Selector GridForm grid (1 row, Entity page)Master — drives all other grids
2P&L Summary GridForm grid (Descendants Net_Income)Detail — Entity syncs from master
3Revenue vs Budget ChartChartDetail — Entity syncs from master
4EBITDA Margin ChartChartDetail — Entity syncs from master
5–84 KPI InfoletsInfolet ×4Detail — Entity syncs from master
9Entity Waterfall ChartChartDetail — Entity syncs from master
10Headcount Summary GridForm gridDetail — Entity syncs from master
11Rolling Forecast GridForm gridDetail — Entity + Year sync from master
12Alerts TileTile (static + conditional format)N/A (static)
💡Design Trick — The Entity Selector Grid: Rather than putting the Entity page selector on every grid independently, Vision Corp uses a single 1-row grid as a dedicated "master selector". This grid has Entity in its page dimension and displays just one or two high-level KPI accounts as data. Its only purpose is to drive the Apply Context chain. When the planner changes Entity here, all 11 other components update simultaneously. This pattern solves the most common Forms 2.0 UX problem — context synchronisation across a complex dashboard.
🎯 Interview Q — Senior EPBCS Architect
A client has a composite form where changing the Entity page selector in Grid A doesn't update Grid B. They're on EPM 25.08. Diagnose and fix.
Strong answer: (1) Confirm they are using Forms 2.0 — this is a Forms 2.0 Apply Context issue, not a classic form bug. (2) Navigate to the composite form designer → Grid B properties → Apply Context tab → confirm Entity dimension is NOT configured as a target from Grid A. (3) Fix: set Grid A as Master, Grid B as Detail, dimension = Entity. (4) Test: change Entity in Grid A, verify Grid B refreshes. Strong hire adds: check RTP bindings at the same time — if any RTPs are used, verify all grids have them bound.
⛺ Quest Tasks — Lesson 11
Explain the stateless architecture of Forms 2.0 and why Apply Context is necessary
Identify what the 12-component dashboard limit covers and how to stay within it
Configure Apply Context between two grids in a composite form: entity selector → P&L grid
Test the silent RTP failure pattern — change an RTP and verify all grids respond
Apply the migration decision framework to Vision Corp's 50-form library — identify which 10 to migrate first
🔭
The Oracle
Forms 2.0 & JET Architecture Expert
⚔ Part V · Lesson 11 · 175 XP

⚔ Business Rules & Groovy Primer

Part V · Groovy Battle Saga · Lesson 1 of 5

The Sorcerer Awakens

Deep in the engine room of EPBCS lives a power most admins fear to touch: Groovy scripting. Where Essbase calc scripts end, Groovy begins — server-side Java capable of reading and writing every cell in the cube, calling REST APIs, and executing complex financial logic. Master it, and you become the most dangerous person in your finance organisation.

Groovy vs Essbase Calc Scripts

CapabilityEssbase CalcGroovy
Read/Write cube
Conditional logicLimited IF/ENDIFFull Java if/else/switch/ternary
Loops & iterationFIX/ENDFIX onlyfor, while, each, collect, find
REST API calls✓ (call any HTTP endpoint)
Dynamic member lists✓ (query at runtime from cube)
Error handling✓ try/catch/finally
String manipulation✓ full Java string operations

Essential Groovy Objects & APIs

Core Groovy Objects
// The cube — your gateway to everything def essbase = cube.getEssbaseCube() // Read a cell (one network call) def rev = getCell("Vision_US", "Total_Revenue", "Full_Year", "FY2025", "Budget", "Working") // Write a cell — use sparingly, never in loops setCell(1234567.89, "Vision_US", "Product_Revenue", "Jan", "FY2025", "Budget", "Working") // Batch write — ONE call for ALL values (critical for performance) operation.grid.setDataCellValues([["Vision_US", "Prod_Rev", "Jan", "FY2025", "Budget", "Working", 1200000.0], ["Vision_US", "Prod_Rev", "Feb", "FY2025", "Budget", "Working", 980000.0]]) // Get all leaf members under a parent def entities = essbase.getMember("Entity", "Total_Vision").getLevelZeroMembers() // Get current POV from form context def entity = operation.grid.pov.find { it.dimensionName == "Entity" }.memberName
Performance Rule #1: Never call setCell() inside a loop. Build a list of writes and call setDataCellValues() once. For 50 entities, this is the difference between 50 network round-trips (slow) and 1 (fast).
⚔ Quest Tasks — Lesson 11
Create your first Groovy business rule and attach it to a form
Use getCell() to read a value and log it with println()
Use getLevelZeroMembers() to iterate all leaf Entity members
Implement batch setDataCellValues() for efficient writes
🔭
The Oracle
Groovy Sorcerer
⚔ Part V · Battle 1 · 200 XP

⚔ Battle 1 — Seasonal Revenue Spread

Part V · Groovy Battle Saga · Battle 1 of 4

⚔ Groovy Battle — The Revenue Sorcerer
Spread Annual Revenue Using Historical Seasonality

Mission: Sales enters annual revenue targets as a single total. Your Groovy rule must distribute this total to 12 months using prior year actual seasonality weights. Without this, planners manually enter 12 months — inconsistently and slowly.

The Algorithm

  1. Read the annual budget total entered by the planner
  2. Calculate prior year monthly proportions (Jan Actual / Full Year Actual)
  3. Multiply annual budget × each month's weight
  4. Write all 12 values in one batch call
Groovy — Seasonal Revenue Spread
// Battle 1: Seasonal Revenue Spread — triggered from Action Menu def months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"] def entity = operation.grid.pov.find { it.dimensionName == "Entity" }.memberName // Step 1: Read annual budget target def annualBudget = getCell(entity, "Total_Revenue", "Full_Year", "FY2025", "Budget", "Working") if (!annualBudget || annualBudget == 0) { return } // Step 2: Read prior year full year for denominator def pyTotal = getCell(entity, "Total_Revenue", "Full_Year", "FY2024", "Actual", "Final") ?: 12.0 def writes = [] months.each { month -> def pyMth = getCell(entity, "Total_Revenue", month, "FY2024", "Actual", "Final") ?: (pyTotal / 12) def weight = pyMth / pyTotal def spread = Math.round(annualBudget * weight * 100) / 100.0 writes << [entity, "Total_Revenue", month, "FY2025", "Budget", "Working", spread] } // Rounding reconciliation — assign diff to December def sumMonths = writes.sum { it[6] } writes[11][6] += (annualBudget - sumMonths) operation.grid.setDataCellValues(writes) // 12 months, 1 round-trip println("Spread complete: ${entity} annual ${annualBudget} → 12 months")
💡Rounding Fix: Due to decimal rounding, 12 months may not sum exactly to the annual total. The last line assigns the rounding difference to December — keeping the audit clean.
⚔ Battle 1 Tasks
Implement the seasonality spread Groovy rule
Add fallback logic for entities with no prior year actuals (equal distribution)
Attach the rule to an Action Menu on the annual total form
Implement rounding reconciliation so months sum exactly to annual
🔭
The Oracle
Groovy Battle Coach
⚔ Part V · Battle 2 · 225 XP

⚔ Battle 2 — Dynamic Cost Allocations

Part V · Groovy Battle Saga · Battle 2 of 4

⚔ Groovy Battle — The Overhead Distributor
Revenue-Proportional IT Shared Cost Allocation

Mission: Corporate IT has an $8.5M shared cost pool. Finance requires this cost allocated to 8 business entities proportional to each entity's revenue. Must run dynamically — as revenue changes in Budget, allocated costs auto-update.

Groovy — Two-Pass Proportional Allocation
// Battle 2: Dynamic Overhead Allocation def essbase = cube.getEssbaseCube() def entities = essbase.getMember("Entity", "Total_Operating_Entities").getLevelZeroMembers() // Pass 1: sum total revenue — guard against zero def totalRev = 0.0 entities.each { e -> totalRev += getCell(e.getName(), "Total_Revenue", "Full_Year", "FY2025", "Budget", "Working") ?: 0 } if (totalRev == 0) { println("Guard: No revenue — abort"); return } def pool = getCell("Corporate", "IT_Shared_Cost", "Full_Year", "FY2025", "Budget", "Working") ?: 0 def writes = [] // Pass 2: each entity's proportional share entities.each { e -> def rev = getCell(e.getName(), "Total_Revenue", "Full_Year", "FY2025", "Budget", "Working") ?: 0 def share = pool * (rev / totalRev) writes << [e.getName(), "IT_Allocated", "Full_Year", "FY2025", "Budget", "Working", share] } operation.grid.setDataCellValues(writes) // 8 entities, 1 round-trip — NOT 8 println("IT allocated: pool=${pool} across ${entities.size()} entities")
💡FP&A Enterprise Pattern: Shared services allocation (HR, IT, Finance, Legal) is one of the top 5 most-automated use cases in EPBCS across Fortune 500 companies. This two-pass pattern handles all of them — just change the pool account and allocation basis.
⚔ Battle 2 Tasks
Implement two-pass proportional allocation in Groovy
Add zero-revenue guard to prevent division by zero
Extend to also allocate HR and Finance shared cost pools
Add monthly spreading so annual pool distributes to each month
🔭
The Oracle
Allocation Expert
⚔ Part V · Battle 3 · 250 XP

⚔ Battle 3 — Validation Guards

Part V · Groovy Battle Saga · Battle 3 of 4

⚔ Groovy Battle — The Gatekeeper
Business Rule Validation with Clear User Feedback

Mission: Build Groovy validations that prevent impossible plan submissions: negative headcount, margin below floor, revenue below committed backlog. Each violation must produce a clear, actionable error — not a cryptic system error.

Groovy — Collect-All-Errors Validation Pattern
import com.hyperion.calcmgr.common.groovyrules.exceptions.RuleStoppedException def entity = operation.grid.pov.find { it.dimensionName == "Entity" }.memberName def errors = [] // Collect ALL errors before throwing — better UX // Validation 1: Headcount must be positive and sane def hc = getCell(entity, "Total_Headcount", "Full_Year", "FY2025", "Budget", "Working") ?: 0 if (hc < 0) errors << "❌ Headcount cannot be negative (${entity}): ${hc}" if (hc > 10000) errors << "⚠ Headcount >10,000 — likely a data entry error: ${hc}" // Validation 2: Gross margin must be above threshold def rev = getCell(entity, "Total_Revenue", "Full_Year", "FY2025", "Budget", "Working") ?: 0 def gp = getCell(entity, "Gross_Profit", "Full_Year", "FY2025", "Budget", "Working") ?: 0 if (rev > 0 && (gp / rev) * 100 < 10) errors << "❌ Gross margin ${Math.round((gp/rev)*100)}% is below the 10% minimum for ${entity}" // Validation 3: Forecast revenue cannot be below committed backlog def backlog = getCell(entity, "Committed_Backlog", "Full_Year", "FY2025", "Actual", "Final") ?: 0 if (rev < backlog) errors << "❌ Revenue forecast (${rev}) < committed backlog (${backlog}) for ${entity}" // Throw once with ALL errors — user sees everything at once if (!errors.isEmpty()) throw new RuleStoppedException(errors.join("\n"))
💡UX Best Practice: Collect all errors before throwing. If 3 validations fail, the user sees all 3 at once and fixes them in one go. Throwing on the first error means fix-resubmit-fix-resubmit — a maddening experience that breeds distrust in the system.
⚔ Battle 3 Tasks
Implement headcount range validation with clear error messages
Add gross margin floor validation (10% minimum)
Build backlog vs forecast check to prevent impossible plans
Use collect-all-errors pattern before throwing RuleStoppedException
🔭
The Oracle
Groovy Validation Expert
⚔ Part V · Battle 4 · 300 XP

⚔ Battle 4 — Debug & Optimize

Part V · Groovy Battle Saga · Battle 4 of 4

⚔ Groovy Battle — The Performance Inquisitor
Diagnosing and Fixing a Slow Groovy Rule

Mission: A Groovy allocation rule that worked in testing is taking 45 minutes in production with full data. Diagnose the bottleneck, identify the anti-patterns, and refactor for sub-5-minute execution.

❌ The Slow Code (45 minutes)

Anti-Pattern — N network calls in a loop
// WRONG: setCell inside nested loops = hundreds of round-trips entities.each { entity -> months.each { month -> def val = computeValue(entity, month) setCell(val, entity, "IT_Alloc", month, "FY2025", "Budget", "Working") // 8 entities × 12 months = 96 individual network calls } } // WRONG: Reading inside an O(n²) nested loop entities.each { e -> entities.each { other -> // n × n reads = terrible def v = getCell(e.getName(), "Rev", ...) } }

✅ The Fixed Code (under 2 minutes)

Optimized Pattern — Pre-fetch, compute in memory, batch write
def startTime = System.currentTimeMillis() // Step 1: Pre-fetch all data into a map (reads happen BEFORE writes) def revenueMap = [:] entities.each { e -> revenueMap[e.getName()] = months.collectEntries { month -> [month, getCell(e.getName(), "Total_Revenue", month, "FY2025", "Actual", "Final") ?: 0] } } // Step 2: Compute in pure Groovy (zero Essbase calls here) def writes = [] entities.each { e -> months.each { month -> def allocated = computeShare(revenueMap, e.getName(), month) writes << [e.getName(), "IT_Alloc", month, "FY2025", "Budget", "Working", allocated] } } // Step 3: ONE batch write for all cells operation.grid.setDataCellValues(writes) println("Done in ${System.currentTimeMillis() - startTime}ms. ${writes.size()} cells written.")
💡FP&A Enterprise Impact: A 45-minute rule blocks the overnight batch — planners arrive to stale numbers every morning. A 2-minute rule runs reliably in the nightly window. This is the difference between a system Finance trusts and one they work around.
⚔ Battle 4 Tasks
Identify all anti-patterns in the slow code
Implement pre-fetch data map pattern (reads before writes)
Refactor to batch setDataCellValues for all writes
Add timing logs (System.currentTimeMillis()) to measure before/after
🔭
The Oracle
Performance Expert