Report execution from 20min to under 2min
Quick Facts
- Industry: Global mining major (copper / gold / molybdenum producer)
- Role: Senior Consultant — forecasting workstream lead
- Timeline: ~5 months across two forecasting cycles
- Team: Solo build with the client's two senior planners pair-reviewing each iteration
- Impact: Critical reports cut from 15-20 minutes to under 2 minutes (7-10×). Multi-step upload workflow collapsed to a single dashboard interaction.
Overview
A global mining major was running their quarterly forecasting cycle on OneStream and hitting two walls: their critical management reports took 15-20 minutes to render, and the data-load workflow forced planners through a three-screen Import → Validate → Load dance every time they wanted to update their numbers and the surrounding business commentary. Over a planning cycle, planners were sinking hours into waiting and clicking rather than thinking. We rebuilt the calculation paths, introduced caching for dynamic results, and folded the entire upload flow into a single dashboard upload template. End-of-engagement: reports rendered in <2 min, and the cycle shed ~40% of its analyst-time overhead.
The Problem
The client's forecasting platform was correct but unforgiving. Three pain points compounded each other:
- Critical reports rendered in 15-20 minutes. Operations management used these reports during weekly forecast reviews; the latency made the meetings drag and forced planners to pre-run reports an hour in advance and accept stale numbers.
- Annotation and data lived in different workflows. Planners entered numbers in OneStream and then opened a separate annotation template to capture the why behind each variance. Reconciling the two at month-end took a half-day.
- The Import → Validate → Load workflow was unforgiving. Each upload required selecting the right Import workflow, running the validation step, reviewing errors, then executing the load — across multiple screens, with no in-context preview of what would actually change.
The brief: bring the critical-report latency under control without rewriting the data model, and find a way to fold annotation entry into the same upload that planners already did.
Process
Profile, then cache
The first move was an honest read of where the 15-20 minutes was going. We turned on OneStream's calculation-trace logging at the report-execution level and ran a representative report against a typical period. The trace told a familiar story: the report was re-evaluating a small number of dynamic calculations many times — once per row of the cube view, sometimes once per cell — because the calculation logic was being evaluated lazily and there was nothing in the path to memoize the result.
We didn't need to rewrite the calculations. We needed to evaluate each one once per report run and reuse the result wherever it appeared downstream. The fix was a thin caching layer between the calculation engine and the report renderer: when a dynamic calc requested a value, the cache first checked whether the same (member-set, time, scenario) tuple had already been computed in this report's execution context. If yes, return the cached value. If no, compute it, cache it, return it.
The cache was scoped per report-execution, not per session — anything stale would clear at the end of the report, so the next render started fresh and there was no risk of users seeing yesterday's numbers. The change was localised to the calculation infrastructure; no business rules were touched.
That single change took the worst reports from 20 minutes to about 4 minutes.
Restructure the heavy calc paths
The remaining 4 minutes was split across half a dozen expensive calculation paths. Each was structurally similar: a deep aggregation across several custom dimensions, followed by a comparison against a target value, followed by a derivation of a variance. Each step was correct; the chain was the problem.
We restructured each chain to compute the aggregation once at the parent level, hold it in a working member, and let the comparison and variance derivation read the working member rather than re-aggregate the leaves. The math is identical; the work performed by the engine is roughly 60-70% less. After this pass the critical reports landed at under 2 minutes consistently.
One dashboard upload, two payloads
In parallel, we rebuilt the upload flow. The insight was that planners always wanted to upload numbers and annotations together — the annotation was the story of the number, and separating them created a reconciliation burden at every cycle. We built a single Excel template with two tabs (Data and Annotations), a shared key column linking each annotation to its row in Data, and a single dashboard button that posted both tabs through one workflow.
The dashboard validated both tabs upstream of the load, surfaced any mismatches in-context (e.g., annotation pointing to a key with no data row), and only triggered the actual load once everything was clean. The Import → Validate → Load dance collapsed into a single "Upload" interaction; planners never saw the underlying workflow chain.
Solution
Four pieces, three to the report layer and one to the upload layer:
1. Per-execution calculation cache
A thin memoization layer keyed by (member-set, time, scenario, calc-name) that lives for the duration of a single report execution. The cache is invisible to the report author — they write the same dynamic calculation they always would; the layer intercepts and reuses results when the same tuple is requested twice.
2. Working-member restructuring
The six expensive calculation chains were each rewritten to materialise their aggregation step into a named working member at the parent level. The comparison and variance steps read that member instead of re-aggregating the leaves. This is the change that took the long tail from 4 min down to <2 min.
3. Annotation-aware Excel template
A two-tab Excel template plus a single dashboard upload button. Data and annotation tabs share a key column. Validation runs across both tabs before any load fires.
4. Cross-tab validation surfacing
Errors from the unified validation are surfaced inline in the dashboard — the planner sees "row 47 in Annotations references a key that doesn't exist in Data" before the load runs, with the option to fix and retry without re-uploading the file. The same surface handles missing-required-field errors, type errors, and duplicate-key errors.
Results
| Metric | Before | After | Change |
|---|---|---|---|
| Critical report runtime | 15-20 min | <2 min | 7-10× |
| Upload workflow steps | 3 screens (Import / Validate / Load) | 1 dashboard upload | 3× fewer interactions |
| Annotation reconciliation time per cycle | ~4h | ~30 min | 8× |
| Reports needing pre-warming before meetings | 6 | 0 | eliminated |
Soft outcomes:
- Forecast review meetings stopped dragging. The latency tail was the meeting's worst feature; once it landed under 2 minutes, the planners could ask "what if?" questions live and get answers in the room.
- Annotations stopped being a separate process. Planners stopped maintaining the parallel annotation spreadsheet that had grown up as a workaround.
- Validation feedback got faster. Mismatch detection moved from "load fails and someone investigates" to "validator surfaces the row at upload time" — usually within seconds of clicking.
Learnings
What worked. Reading the calculation trace as a profiler, not a debugger, was the unlock — same lesson as the metadata-automation case study. The trace tells you where the time is going if you ask the right question, and the answer was hiding in plain sight: the engine was correctly evaluating the same calculation many times because nothing was telling it not to.
What I'd do differently. Build the working-member restructuring earlier, before the cache. The cache covered for some of the structural inefficiency and made the rewrite feel less urgent than it was. In retrospect, restructuring the chains first would have made the cache's job smaller and the overall system simpler.
Skill developed. Recognising the difference between "evaluating once per cell" and "evaluating once per (member-set, scenario, time)". Many OneStream perf problems sit on that boundary; the right caching key collapses the difference instantly.