Skip to content

feat: loading benchmark analytic - #9494

Open
lorenzo-ranciaffi wants to merge 7 commits into
devfrom
fix/loading-benchmark-analytic
Open

feat: loading benchmark analytic#9494
lorenzo-ranciaffi wants to merge 7 commits into
devfrom
fix/loading-benchmark-analytic

Conversation

@lorenzo-ranciaffi

@lorenzo-ranciaffi lorenzo-ranciaffi commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Pull Request Description

What does this PR change?

This PR adds a measure-loading-time app arg intended for CI usage only.
The CI will start the client with that flag so that it will log the startup loading times and send them to matrix in order to track how much time (and which stage) it takes for the client to land in GP (the CI will send the user to a dedicated world with a copy of GP so performance doesn't fluctuate based on users, emotes, ... ).
When the client has loaded it shuts down.

Quality Checklist

  • Changes have been tested locally
  • Documentation has been updated (if required)
  • Performance impact has been considered
  • For SDK features: Test scene is included

Code Review Reference

Please review our Branch & PR Standards before submitting. It explains the automated review flow, QA/DEV approval requirements, and what each label does — especially useful for first-time contributors.

@lorenzo-ranciaffi lorenzo-ranciaffi self-assigned this Jul 28, 2026
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

badge

New build in progress, come back later!

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

badge

Warnings not reduced: 13999 => 13999 — remove at least 1 warning to merge.

Warnings/errors in files changed by this PR (7)
Assets/DCL/PerformanceAndDiagnostics/Analytics/AnalyticsEvents.cs:28  InconsistentNaming  Name 'UI' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Ui'.
Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs:428  InconsistentNaming  Name 'badgesAPIClient' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'badgesApiClient'.
Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs:429  InconsistentNaming  Name 'marketplaceCreditsAPIClient' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'marketplaceCreditsApiClient'.
Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs:431  InconsistentNaming  Name 'marketplaceShopAPIClient' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'marketplaceShopApiClient'.
Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs:201  InconsistentNaming  Name 'nftInfoAPIClient' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'nftInfoApiClient'.
Assets/DCL/UserInAppInitializationFlow/RealUserInAppInitializationFlow.cs:380  InconsistentNaming  Name 'withUI' does not match rule 'parameters_should_be_camel_case'. Suggested name is 'withUi'.
Assets/DCL/UserInAppInitializationFlow/RealUserInAppInitializationFlow.cs:177  VariableHidesOuterVariable  Parameter 'ct' hides outer parameter with the same name

@lorenzo-ranciaffi lorenzo-ranciaffi changed the title Fix/loading benchmark analytic feat: loading benchmark analytic Aug 3, 2026
@lorenzo-ranciaffi lorenzo-ranciaffi added the no QA needed Used to tag pull requests that does not require QA validation label Aug 3, 2026
@lorenzo-ranciaffi
lorenzo-ranciaffi marked this pull request as ready for review August 3, 2026 10:01
@lorenzo-ranciaffi
lorenzo-ranciaffi requested review from a team as code owners August 3, 2026 10:01
@github-actions
github-actions Bot requested review from dalkia and lorux0 August 3, 2026 10:02
@decentraland-bot
decentraland-bot self-requested a review August 3, 2026 10:02

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review — PR #9494: feat: loading benchmark analytic

STEP 2 — Root-cause check

PASS. This PR adds a new CI-only feature for benchmarking loading times — it is solving a genuine observability gap, not patching a symptom.

STEP 3 — Design & integration

Owner search for LoadingTimes (the new orchestrator):

  • Entity managed: loading-stage measurement lifecycle.
  • Existing owners: LoadingStatus (drives stage transitions via SetCurrentStage()), RealUserInAppInitializationFlow (calls SetCurrentStage during init), ProfilingPlugin (home for profiling/CI concerns, already hosts AutoPilot).
  • Verdict: ProfilingPlugin is the correct home for this CI-only feature. The subscription to ILoadingStatus.CurrentStageMut.OnUpdate is the right integration point — it fires on each stage transition without coupling to the flow that drives them.

However, LoadingTimeSampler is a second unit that exists only as a static delegate of LoadingTimes. It has no independent consumers and no polymorphism. Per CLAUDE.md §11 ("Extracting when you should merge"), it should be merged into LoadingTimes as instance state.

Teardown trace:

  • += OnStageUpdatedLoadingTimes constructor (line 23) ✅
  • -= OnStageUpdatedLoadingTimes.Dispose() (line 28) ✅
  • loadingTimes?.Dispose()ProfilingPlugin.Dispose()
  • ⚠️ In practice, Application.Quit() fires inside the callback before Dispose() is ever reached. The teardown is technically correct but effectively dead code in the CI happy path.

Comparison with AutoPilot (existing CI feature in the same plugin):
AutoPilot uses an async UniTask pattern — it awaits loading completion, runs its work, then quits in a finally block. This gives async operations (like network I/O) time to complete. The new LoadingTimes uses a synchronous event callback that calls Track() then Application.Quit() immediately — this is a critical difference that likely causes analytics data loss (see P1 finding below).

STEP 4 — Member audit

Member Consumers Assessment
LoadingTimeSampler.Sample() 1 (LoadingTimes.OnStageUpdated) Single-use static → merge into LoadingTimes
LoadingTimeSampler.ToJObject() 1 (LoadingTimes.OnStageUpdated) Single-use static → merge into LoadingTimes
StageMeasure (struct) 1 (LoadingTimeSampler) Could be private nested type after merge

STEP 5 — Line-level findings

See inline comments below. Summary of findings:

# Severity File Issue
1 P1 LoadingTimeSampler.cs:19-36 Missing upper-bound check on array access → IndexOutOfRangeException
2 P1 LoadingTimeSampler.cs:48-54 Iterates uninitialized array entries → ArgumentException on duplicate JObject keys
3 P1 LoadingTimes.cs:39-43 Application.Quit() immediately after Track() without Flush() → analytics data likely lost
4 P2 LoadingTimeSampler.cs:6 Entirely static mutable state — merge into LoadingTimes (CLAUDE.md §11)
5 P2 LoadingTimes.cs:11 Namespace–type name collision (DCL.LoadingTimes.LoadingTimes)

STEP 5B — Design smells

  • Branch name mismatch (ADR-6): Branch is fix/loading-benchmark-analytic but PR title correctly uses feat:. The branch prefix should match — feat/loading-benchmark-analytic.

STEP 6 — Complexity

COMPLEX — introduces new assembly reference, event subscription lifecycle in ProfilingPlugin, modifies plugin constructor signature (affects all callers), and changes the authentication/initialization flow.

STEP 7 — QA

YES — modifies runtime code under Explorer/ that ships in the build. The constructor signature change to ProfilingPlugin and the auth-flow change in RealUserInAppInitializationFlow run in all builds, not just when the flag is set.

STEP 8 — Non-blocking warnings

None.

Security review

No security issues found. The measure-loading-time flag requires command-line access (not a remote attack vector). Auth-screen skip mirrors the existing AUTOPILOT pattern. No secrets or PII exposed in the analytics payload.


REVIEW_RESULT: FAIL ❌
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Introduces new assembly reference, event subscription lifecycle in ProfilingPlugin, modifies plugin constructor signature, and changes auth/init flow.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub

Comment thread Explorer/Assets/DCL/PerformanceAndDiagnostics/LoadingTimes/LoadingTimeSampler.cs Outdated
Comment thread Explorer/Assets/DCL/PerformanceAndDiagnostics/LoadingTimes/LoadingTimeSampler.cs Outdated
Comment thread Explorer/Assets/DCL/PerformanceAndDiagnostics/LoadingTimes/LoadingTimes.cs Outdated
Comment thread Explorer/Assets/DCL/PerformanceAndDiagnostics/LoadingTimes/LoadingTimeSampler.cs Outdated
Comment thread Explorer/Assets/DCL/PerformanceAndDiagnostics/LoadingTimes/LoadingTimes.cs Outdated
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

badge

All Unity tests passed ✅

TESTS SUITE Result Passed Failed Skipped
EditMode ✅ Passed 24324 0 13
PlayMode ✅ Passed 236 0 5

@lorenzo-ranciaffi lorenzo-ranciaffi added the clean-build Used to trigger clean build on PR label Aug 3, 2026

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review — PR #9494: feat: loading benchmark analytic (Re-review)

Previous review findings addressed: The author rewrote the static LoadingTimeSampler + LoadingTimes into a single instance-based LoadingTimeBenchmark, addressing all five prior findings (static mutable state, index-out-of-bounds, uninitialized array iteration, analytics delivery, namespace collision, and single-consumer merge). Well done.

STEP 2 — Root-cause check

PASS. This PR adds a new CI-only feature — a loading-time benchmark activated by the measure-loading-time app arg. It measures startup stage durations, reports them via analytics, and quits. This is genuine new functionality, not a symptom fix.

STEP 3 — Design & integration

PASS.

Owner search for LoadingTimeBenchmark:

  • Manages: observation of ILoadingStatus.CurrentStageMut stage transitions for timing measurement.
  • Existing owners searched: ILoadingStatus is created by the container infrastructure and consumed by the loading screen UI. Stage transitions are driven by RealUserInAppInitializationFlow and container initialization. No existing class measures and reports stage durations — this is a new cross-cutting CI instrumentation concern.
  • Conclusion: No existing owner can host this logic. ProfilingPlugin is the natural home for profiling/diagnostic tools, and conditional creation via the flag keeps it clean.

Teardown trace:

  • loadingStatus.CurrentStageMut.OnUpdate += OnStageUpdated (constructor, L50) → OnUpdate -= OnStageUpdated (Dispose(), L55) ✅
  • ProfilingPlugin.Dispose() calls loadingTimeBenchmark?.Dispose()
  • ReportAndQuitAsync() — detached via .Forget() with proper try/catch/finally per CLAUDE.md §9. However, UniTask.Delay has no cancellation token — if Dispose() runs during the grace period, the delay continues detached. See P2-1 below.

STEP 4 — Member audit

  • LoadingTimeBenchmark(ILoadingStatus, IAnalyticsController, IScenesCache) — 1 consumer (ProfilingPlugin constructor). ✅
  • Dispose() — 1 consumer (ProfilingPlugin.Dispose). ✅
  • All other members are private. No single-use public accessors. ✅

STEP 5 — Line-level findings

See inline comments. Summary:

# Sev Finding
1 P2 UniTask.Delay in ReportAndQuitAsync has no CancellationToken — delay continues if Dispose() runs during grace period
2 P2 Scene hash read across async boundary in ReportAndQuitAsync — capture synchronously before .Forget()
3 P2 Removed AnalyticsEvents XML comment warning about "Refresh Events" button — confirm this step is no longer required

Security: No issues found. The auth bypass mirrors the existing AUTOPILOT pattern with proper identity-expiry fallback (Application.Quit(1) if identity is null/expired). The analytics payload contains only timing floats and a scene identifier — no PII, no user-controlled input.

STEP 6 — Complexity

SIMPLE. New self-contained CI-only diagnostic class with event subscription and analytics reporting. No ECS systems, no complex async patterns, no cross-world access.

STEP 7 — QA

NO. The feature is gated behind a CI-only measure-loading-time app arg that is never passed in normal usage. The auth flow changes are purely additive flag checks that don't alter default behavior.

STEP 8 — Non-blocking warnings

None.

STEP 9 — Verdict

REVIEW_RESULT: PASS ✅
COMPLEXITY: SIMPLE
COMPLEXITY_REASON: New self-contained CI-only diagnostic class with event subscription and analytics reporting; no ECS, complex async, or architectural complexity.
QA_REQUIRED: NO


Reviewed by Jarvis 🤖 · Requested by lorenzo-ranciaffi via GitHub

Comment on lines +53 to +56
}

public void Dispose()
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] UniTask.Delay in ReportAndQuitAsync (L118) runs without a CancellationToken. If Dispose() is called during the 5-second analytics grace period, the delay and the Application.Quit() in finally continue running detached.

Add a CancellationTokenSource field, cancel it in Dispose(), and pass the token to UniTask.Delay. Also add using System.Threading; to imports.

Suggested change
}
public void Dispose()
{
public void Dispose()
{
loadingStatus.CurrentStageMut.OnUpdate -= OnStageUpdated;
cts.Cancel();
cts.Dispose();
}

Also add the field (after L35):

private readonly CancellationTokenSource cts = new ();

And update L118:

await UniTask.Delay(ANALYTICS_DELIVERY_GRACE, cancellationToken: cts.Token);

Comment on lines +66 to +69
if (stage != LoadingStatus.LoadingStage.Completed) return;

reported = true;
ReportAndQuitAsync().Forget();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Scene hash read across an async boundary. scenesCache.CurrentScene.Value?.Info.Name is currently read inside ReportAndQuitAsync (L111), after the Completed event fires and across an async transition. The reactive property could theoretically change between the event and the read.

Capture the scene hash synchronously here, before the .Forget() call:

Suggested change
if (stage != LoadingStatus.LoadingStage.Completed) return;
reported = true;
ReportAndQuitAsync().Forget();
if (stage != LoadingStatus.LoadingStage.Completed) return;
reported = true;
ReportAndQuitAsync(scenesCache.CurrentScene.Value?.Info.Name).Forget();

Then update the method signature and body:

private async UniTaskVoid ReportAndQuitAsync(string? sceneHash)
{
    // ...
    JObject payload = BuildPayload(sceneHash);

This also removes the scenesCache field dependency from the async path.

/// IMPORTANT!!
/// </summary>

public static class AnalyticsEvents

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Removed XML comment — confirm this step is obsolete. The deleted comment warned:

After doing any change to the events here, we need to hit the "Refresh Events" button on the AnalyticsConfiguration Scriptable Object so the new events are recognized!!

If this step is still required for the new LOADING_TIMES event to be recognized, the warning should be preserved (or documented elsewhere). If the analytics system no longer uses the AnalyticsConfiguration scriptable object for event registration, the removal is fine.

Suggested change
public static class AnalyticsEvents

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clean-build Used to trigger clean build on PR no QA needed Used to tag pull requests that does not require QA validation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants