[Bug][SubscriptionBilling]: Performance: Billing document creation does not scale to large datasets (4M+ subscription lines)#8553
Conversation
Commit() removed after service object quantity updateThe Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
CalcSubscriptionLineEndDate ignores Extension Term guardRemoving Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
CalcTermUntil drops Extension Term logic entirelyThe removed code used Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
resolved with rebase. |
resolved with rebase. |
…ing runs (microsoft#7690) ### Summary Optimizes the Subscription Billing module to handle large-scale data volumes (~4M subscription lines) without timeouts or unbounded memory usage. Introduces a reusable progress/ETA helper, targeted index additions, per-N-contract commit checkpoints, and a range of code-level perf fixes. ### New codeunits - **Progress Tracker** (CU 8035): Reusable progress dialog helper. Shows elapsed time, processed/total count with %, ETA, and throughput (items/min). All dialog calls guarded by `GuiAllowed`; safe for Job Queue / headless sessions. Time-throttled to one redraw/sec. - **Billing Price Calc Skip** (CU 8036): `EventSubscriberInstance = Manual` codeunit. Bound/unbound around document-line insertion to suppress the `OnBeforeUpdateUnitPrice`, `OnBeforeGetUnitCost`, `OnBeforeUpdateQuantityFromUOMCode` (Sales) and `OnBeforeUpdateUnitCost` (Purchase) subscribers — eliminates redundant price recalculation per line. ### BillingProposal (CU 8062) - Added `Progress Tracker` with ETA to `DeleteBillingProposal`, `DeleteBillingLines`, and `DeleteBillingDocuments`. - `DeleteBillingDocuments`: pre-counts distinct documents via `CountDistinctDocuments` so ETA is accurate from the first step. - Per-N-contract commit checkpoint (`CommitBatchSize = 10`) in both Customer and Vendor contract loops — prevents multi-hour runs from being lost to a single late failure, while keeping test behaviour unchanged (tests use 1 contract, so the checkpoint never fires). - `TrimTempBillingLinesForContract` simplified using `Copy(TempBillingLine, true)` (shared-table reference, O(1)) — all four per-category loops collapsed to a single call each, removing cursor-state variables and the separate post-loop trim pass. - `RefreshGroupsInTempTable` (renamed from `RefreshTempTableForSelection`): drops and rebuilds only the affected groups in the temp table rather than rebuilding the entire set — targeted refresh after drilldowns and actions. - `SetLoadFields` on `CustomerContract`, `VendorContract`, and `ServiceObject` reads in `UpdateBillingLineFromServiceCommitment`. - `FilterBillingLinesOnServiceCommitment` now sets only `"Subscription Line Entry No."` filter (relies on new SK7 index). ### CreateBillingDocuments (CU 8060) - Added `Progress Tracker` with ETA covering the full document-creation run (total billing-line count as denominator). - `BillingPriceCalcSkip` bound before and unbound after each `InsertSalesLine` / `InsertPurchaseLine` block — price recalc suppressed per line. - Fixed AA0205 pipeline error: `PreviousSubscriptionLineEntryNo` initialised to `0` in `CheckBillingLineData`. ### RecurringBilling page (page 8067) - `ContractDescriptionCache` and `PartnerNameCache` dictionaries: description and partner-name lookups cached per `OnAfterGetRecord` instead of re-querying `ContractsGeneralMgt` on every row scroll. - `InitTempTable` removed from 9 navigation / drilldown sites that do not change billing data; replaced with targeted `RefreshCurrentRowGroup()` or `RefreshAfterDocumentDrillDown()`. - `RefreshGroups` restores the previously-focused record after a partial rebuild (no collapse-all side effect). - New helpers: `RefreshCurrentRowGroup`, `RefreshAfterDocumentDrillDown`, `CollectSelectionGroupKeys`, `GetGroupKey`, `RefreshGroups`. ### SalesDocuments / PurchaseDocuments (CU 8063 / 8066) - `UpdateUsageDataBillingArchiveEntryNo` helper dropped; 2-line UBB update inlined into `MoveBillingLineToBillingLineArchive` — removes one indirection per archived line. ### BillingLine table (8061) - `key(SK7; "Subscription Line Entry No.")` added — used by `FilterBillingLinesOnServiceCommitment` to avoid full scans. - `TableRelation` added to `"Subscription Line Entry No."` field. - `OnDelete` trigger: clears `UsageDataBilling."Billing Line Entry No."` for related UBB records (removes the need for the event subscriber). - `fieldgroup(DropDown; ...)` added for meaningful popup columns. ### BillingLineArchive table (8064) - Same `DropDown` fieldgroup added. ### UsageDataBilling table (8062) - `key(key5; "Billing Line Entry No.")` added. - `TableRelation` for `"Billing Line Entry No."` extended with `Document Type = None` branches so the lookup works before billing documents are created. ### UsageBasedContrSubscribers (CU 8028) - Removed three event subscribers whose logic is now handled directly in `BillingLine.OnDelete` and inlined in Sales/PurchaseDocuments: `UpdateUsageDataBillingWithBillingArchiveLineSalesDocuments`, `UpdateUsageDataBillingWithBillingArchiveLinePurchaseDocuments`, `RemoveBillingLineNoFromRelatedUsageData`. ### Usage Based Billing codeunits - All `ProgressTracker.` calls qualified to `this.ProgressTracker.` across `GenericConnectorProcessing` (CU 8028) and `ProcessUsageDataBilling` (CU 8026) to satisfy BCQuality AA0248. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cfd0cc5 to
db26bce
Compare
Agentic PR Review - Round 1Recommendation: Request ChangesWhat this PR doesThis PR adds performance changes for Subscription Billing proposal creation, billing document creation, and usage data processing. It adds progress tracking, new keys, temp-table aggregation, narrower field loading, bulk updates, and new commit checkpoints. The direction matches the reported large-data problem in GitHub issue #7690, and the BaseApp publishers used by the new manual subscribers exist in the W1 BaseApp. However, the diff also changes functional behavior in sensitive paths: document line price/cost/UoM events are bypassed, usage-data links are moved in bulk, and commit boundaries change. Those changes need stronger proof that billing amounts, usage links, and extension-visible behavior stay correct. SuggestionsS1 - Add functional regression tests S2 - Do not bypass pricing hooks unconditionally S3 - Link the internal bug work item Risk assessment and necessityRisk: The regression surface is high because the PR touches billing proposal creation, sales and purchase billing document lines, usage-data billing links, and transaction boundaries. The new BaseApp event subscribers were checked against W1 BaseApp publishers, but setting handled on those events changes extension-visible behavior during line initialization. The PR also has no new functional tests and no linked ADO work item was available for Necessity: The scenario is important: issue #7690 describes billing runs over about 4 million subscription lines with no progress feedback and poor recoverability. Performance and progress improvements are justified, but the scope is broad and should be backed by tests and the internal bug link before merge.
|
Copilot PR ReviewIteration 2 · Outcome: completed Knowledge source: https://github.com/microsoft/BCQuality@186d8a131465475c79244d994acb872cd5c0d4bf Findings by domainFindings split into Knowledge-backed (cite a BCQuality article) and Agent (the agent's own judgement, no matching BCQuality rule).
Totals: 5 knowledge-backed · 3 agent findings. Orchestrator pre-filter (2 file(s) excluded)
Findings produced by the AL review agent v1.7.3. Reply 👎 on any inline comment to flag false positives. |
|
- Move ClearingBillingProposalLbl, DeletingBillingLinesLbl, DeletingDocumentsLbl from procedure-local var blocks to the codeunit object-level var block in BillingProposal (BCQuality: labels must be declared at object scope for correct XLIFF extraction) - Replace TempBillingLine.Indent overload in CreateBillingDocuments with a dedicated BillingLineNoByTempEntryNo dictionary (Dictionary of [Integer, Integer]) keyed by temp Entry No.; avoids repurposing a display-semantics field for an unrelated value - Revert ~40 internal -> procedure visibility changes in UsageDataBilling, UsageDataImport, UsageDataBlob, UsageDataGenericImport, UsageDataSuppSubscription, UsageDataSupplier, UsageDataSupplierReference, UsageDataBillingMetadata, SubscriptionLine (UpdateNextBillingDate), and CreateCustomerBillingDocs / CreateVendorBillingDocs (GetData/SetData); these are unrelated to the performance work in this PR and will be proposed separately Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Agentic PR Review - Round 2Recommendation: Request ChangesWhat this PR doesThe commit since round 1 addresses some cleanup feedback: it adds the AB# link, moves labels to object scope, restores several procedures to Those changes are good, but they do not close the two main functional risks from round 1. The PR still changes billing document creation, usage-data linking, pricing/cost event handling, and commit boundaries without added functional regression tests. It also still sets handled on BaseApp price, cost, and UoM events while billing lines are initialized, so subscribers that depend on those events can still be bypassed. Status of previous suggestions
New observations (commits since round 1)None - the new commit mostly addresses cleanup feedback and does not introduce a separate new concern. Risk assessment and necessityRisk: The main risk is still high because this affects billing proposal creation, customer and vendor billing documents, usage-data billing links, document amounts, and transaction boundaries. The round-2 dictionary change is safer than reusing Necessity: The performance scenario is valid and important. GitHub issue #7690 describes billing runs with about 4 million subscription lines, long-running proposal and document creation, no progress feedback, and rollback after failures. The fix is worth doing, but this broad billing change still needs functional proof before merge.
|
|
|
||
| // Skip the redundant unit price/cost engine while validating the line - the values are taken from | ||
| // the Billing Line and assigned explicitly below. All other validation side effects still run. | ||
| BindSubscription(BillingPriceCalcSkip); |
There was a problem hiding this comment.
BillingPriceCalcSkip is a manually bound subscriber held in a codeunit-global variable, and both line-building paths do several Validate/TestField calls before UnbindSubscription.
If any of those calls errors, the explicit unbind is skipped and the manual subscriber can stay bound longer than the intended scope, so later unrelated Sales/Purchase Line validations in the same session could still bypass the standard price/cost logic. Move the subscriber instance into a local scope, or a helper that guarantees scope-based cleanup, so the binding cannot leak on error.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
Later unrelated Sales/Purchase Line validations in the same session could still bypass the standard price/cost logic - that sound like a danger. I need to investigate.
CreateTempBillingLines iterates Billing Line with FindSet() and then does CustomerContract.Get(...)/VendorContract.Get(...) inside the loop, only reusing the immediately previous contract number.Because the outer scan is not keyed/ordered by contract number, the same contract can be re-fetched whenever its rows are non-adjacent, leaving an N+1 lookup pattern in this hot aggregation path. Knowledge:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
The changed Normal table field "Subscription Line Entry No." still relies on the table-level DataClassification instead of declaring its own.DataClassification should be declared explicitly on each Normal field so the field remains correctly classified independent of future table-level changes and passes AS0016 validation. Suggested fix (apply manually — could not be anchored as a one-click suggestion): field(31; "Subscription Line Entry No."; Integer)
{
Caption = 'Subscription Line Entry No.';
DataClassification = CustomerContent;
TableRelation = "Subscription Line"."Entry No.";
}Knowledge: Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
The changed "Billing Line Entry No." field on Usage Data Billing still relies on the table-level DataClassification instead of declaring its own.DataClassification should be declared explicitly on each Normal field so the field remains correctly classified and passes AS0016 validation. Knowledge: Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
| /// posting groups, item defaults) still run. Bind this instance only around the line field initialization | ||
| /// via BindSubscription / UnbindSubscription. | ||
| /// </summary> | ||
| codeunit 8036 "Billing Price Calc. Skip" |
There was a problem hiding this comment.
The new "Billing Price Calc.
Skip" helper is a public, manually bindable subscriber that unconditionally sets Handled/IsHandled := true on Sales Line and Purchase Line validation hooks (unit price, unit cost, UoM quantity conversion). Any dependent extension can instantiate this codeunit and bind it around its own line creation to suppress unit price, unit cost, and UoM validation outside the intended billing flow. Consider keeping this bypass internal to the Subscription Billing app and guarding it so only the intended document-creation path can activate it.
Agent judgement — not directly backed by a BCQuality knowledge article.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
I agree that adding a key makes sense, will do that. |
This is int the context of this change. All fields on the table do not have DataClassification |
irrelevant for the context of the changes. |
- RecurringBilling: fix position restore for group header rows after RefreshGroupsInTempTable. Header rows are reinserted with new negative Entry Nos. on every rebuild, so Rec.Get(CurrentEntryNo) always failed for them. Now the group key (contract no. or partner no.) is remembered before the rebuild and the header is re-located afterwards via a shared-table copy cursor, leaving the page record's own view filters (User ID / Partner) untouched, then positioned with Rec.Get. - Drop the "Billing Price Calc. Skip" manual-subscriber codeunit and its Bind/UnbindSubscription calls in Create Billing Documents. The codeunit was a public manual subscriber any extension could bind, and there was no guaranteed unbind if a Validate/TestField errored mid-line. Removing it eliminates both the external-binding surface and the unbind-leak risk; the platform's normal unit price/cost calculation runs again on each line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
48c5343 to
08c1c85
Compare
Introduces a suite of code-level and database-index optimizations to make billing-proposal creation, document creation, and usage-data processing scale to millions of subscription lines.
Progress & UX
Transaction boundaries
Code-level optimizations
Database indexes
What & why
Linked work
Fixes #7690
How I validated this
What I tested and the outcome (required ΓÇö be specific: scenarios, commands, screenshots for UI changes)
RecurringBillingTest,UsageBasedBillingTest, contract and UBB suites); all pass.Document No.with correct group subtotals and no stale rows.Risk & compatibility
CreateBillingProposal: a checkpoint is committed every 10 contracts. Any caller that holds an open page or record reference across aCreateBillingProposalcall on a dataset larger than 10 contracts will see a stale-rowversion error on the next edit (BC's standard concurrency message). The two affected standard tests were updated. Third-party extensions keeping a subpage open across a proposal run would need the same treatment.BillingPriceCalcSkip(CU 8036): while bound during sales/purchase line initialisation,OnBeforeUpdateUnitPrice,OnBeforeGetUnitCost,OnBeforeUpdateQuantityFromUOMCode(Sales Line) andOnBeforeUpdateUnitCost(Purchase Line) are handled and skipped. A third-party subscriber using those events to apply a surcharge or override during billing document creation would be bypassed. The subscriber is bound/unbound surgically around field initialisation only; all other validation still runs.DeleteBillingProposalnow uses per-lineDelete(true)instead ofDeleteAll(true):OnDeletetriggers still fire identically; the only difference is that partial progress is durable on interruption. No behavioral change.BillingLine.OnDeletedirectly updatesUsageDataBilling(previously via event subscriber withIsTemporaryguard): ALOnDeletenever fires for temporary records so the guard was redundant ΓÇö no behavioral change.BillingLine SK7,UsageDataBilling key5, others) are purely additive; no data migration or upgrade codeunit needed.Fixes AB#641986