@3dsource/angular-unreal-module 0.0.146-dev.1 → 0.0.146-dev.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -4,7 +4,7 @@ import { SourceButtonComponent, SourceLoadingComponent } from '@3dsource/source-
|
|
|
4
4
|
import { Subject, skip, share, merge, map, scan, timestamp, startWith, pairwise, concatMap, of, animationFrameScheduler, interval, Observable, timer, take, defer, switchMap, from, throwError, EMPTY, retry, timeout as timeout$1, repeat, BehaviorSubject, takeUntil as takeUntil$1, fromEvent, tap as tap$1, throttleTime, delay, concat, debounceTime, mergeMap, ignoreElements, shareReplay, NEVER, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, Subscription, bufferTime, combineLatestWith, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
|
|
5
5
|
import { createAction, props, createReducer, on, createFeature, Store, createSelector, provideState } from '@ngrx/store';
|
|
6
6
|
import { whereNot, Falsy, Logger, calculateMedian, clampf, lerp, getCanvasCached, getSnapshot, fitIntoRectangle, generateUuid, Truthy, tapLog, Semaphore, COLOR_CODES, where, KeyboardNumericCode, InvertedKeyMap, HEXtoRGB, RGBtoHSV, inverseLerp, HSVtoRGB, RGBtoHEX, fpIsASameAsB } from '@3dsource/utils';
|
|
7
|
-
import { filter, map as map$1, distinctUntilChanged, takeWhile, takeUntil, catchError, tap, timeout, finalize, first, switchMap as switchMap$1,
|
|
7
|
+
import { filter, map as map$1, distinctUntilChanged, takeWhile, takeUntil, catchError, tap, timeout, finalize, first, switchMap as switchMap$1, withLatestFrom, exhaustMap, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1 } from 'rxjs/operators';
|
|
8
8
|
import { Actions, ofType, createEffect, provideEffects } from '@ngrx/effects';
|
|
9
9
|
import { concatLatestFrom } from '@ngrx/operators';
|
|
10
10
|
import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
|
|
@@ -444,6 +444,7 @@ const initialState = {
|
|
|
444
444
|
isAfkTimerVisible: false,
|
|
445
445
|
afkCountdown: 0,
|
|
446
446
|
cirrusConnected: false,
|
|
447
|
+
adopted: false,
|
|
447
448
|
establishingConnection: false,
|
|
448
449
|
isReconnecting: false,
|
|
449
450
|
viewportReady: false,
|
|
@@ -580,7 +581,11 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
580
581
|
},
|
|
581
582
|
};
|
|
582
583
|
}), on(initSignalling, (state) => {
|
|
583
|
-
return {
|
|
584
|
+
return {
|
|
585
|
+
...state,
|
|
586
|
+
adopted: false,
|
|
587
|
+
disconnectReason: DisconnectReason.None,
|
|
588
|
+
};
|
|
584
589
|
}), on(reconnectPeerSuccess, (state) => {
|
|
585
590
|
return { ...state, isReconnecting: false };
|
|
586
591
|
}), on(setSignalingName, (state, { instanceName }) => {
|
|
@@ -647,6 +652,7 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
647
652
|
return {
|
|
648
653
|
...state,
|
|
649
654
|
cirrusConnected: true,
|
|
655
|
+
adopted: true,
|
|
650
656
|
ssData: ssData ?? state.ssData,
|
|
651
657
|
awsInstance: {
|
|
652
658
|
...state.awsInstance,
|
|
@@ -677,6 +683,7 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
677
683
|
return {
|
|
678
684
|
...state,
|
|
679
685
|
cirrusConnected: false,
|
|
686
|
+
adopted: false,
|
|
680
687
|
dataChannelConnected: false,
|
|
681
688
|
viewportReady: false,
|
|
682
689
|
isVideoPlaying: false,
|
|
@@ -1670,6 +1677,10 @@ class RegionsPingService {
|
|
|
1670
1677
|
* entry is well-formed and still within {@link REGION_PING_CACHE_TTL_MS}.
|
|
1671
1678
|
* All access is guarded — `localStorage` can throw (private mode, disabled
|
|
1672
1679
|
* storage) or be absent (non-browser).
|
|
1680
|
+
*
|
|
1681
|
+
* Public so adopted connections (which skip `getFastest`) can replay the
|
|
1682
|
+
* region-resolving telemetry phase from the same cached winner — see
|
|
1683
|
+
* `UnrealEffects.regionTelemetryOnAdopt$`.
|
|
1673
1684
|
*/
|
|
1674
1685
|
readCachedWinner() {
|
|
1675
1686
|
try {
|
|
@@ -3578,8 +3589,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
|
|
|
3578
3589
|
args: [{ providedIn: 'root' }]
|
|
3579
3590
|
}], ctorParameters: () => [] });
|
|
3580
3591
|
|
|
3581
|
-
// How often to re-check `window.__stream.state` while a prefetch is connecting.
|
|
3582
|
-
const ADOPT_POLL_MS = 100;
|
|
3583
3592
|
/**
|
|
3584
3593
|
* Adopts a WebRTC connection prefetched by the `stream-prefetch.js` head-script
|
|
3585
3594
|
* and parked on `window.__stream`, instead of running `connectToSignaling` from
|
|
@@ -3598,26 +3607,64 @@ class StreamAdoptionService {
|
|
|
3598
3607
|
*
|
|
3599
3608
|
* - nothing parked → emit `false` at once (caller connects normally).
|
|
3600
3609
|
* - already settled (`ready`/`failed`/`adopted`) → adopt-if-ready at once.
|
|
3601
|
-
* - still `connecting` →
|
|
3602
|
-
*
|
|
3610
|
+
* - still `connecting` → wait for the script's `onSettled` callback and
|
|
3611
|
+
* adopt-if-ready the instant it fires (event-driven, zero polling latency).
|
|
3603
3612
|
*
|
|
3604
|
-
*
|
|
3605
|
-
*
|
|
3606
|
-
*
|
|
3613
|
+
* Deliberately NO time budget: for DIRECT orchestration the prefetch holds
|
|
3614
|
+
* the socket open while the orchestrator provisions a machine, which can take
|
|
3615
|
+
* minutes (150–200s+). A fixed deadline would make Angular give up and open a
|
|
3616
|
+
* second connection mid-provisioning — exactly the race/orphan this avoids.
|
|
3617
|
+
* The wait is bounded by liveness instead: the script flips the parked state
|
|
3618
|
+
* to `failed` on a dead socket (`ws.onclose`/`onerror`), and the caller wraps
|
|
3619
|
+
* this in `takeUntil(abortEstablishingConnection)`, so a route change / teardown
|
|
3620
|
+
* cancels it. Emits exactly once. Cancelling runs no adoption — which is what
|
|
3621
|
+
* stops a fast route-switch from starting overlapping connections.
|
|
3622
|
+
*
|
|
3623
|
+
* Wrapped in `defer` so the `tryAdopt()` side effect (which dispatches
|
|
3624
|
+
* `streamAdopted`) runs at SUBSCRIPTION time, not when the observable is
|
|
3625
|
+
* built. The effect prepends `startWith(setEstablishingConnection(true))`,
|
|
3626
|
+
* which starts the telemetry session and subscribes its phase catalog; that
|
|
3627
|
+
* must happen BEFORE `streamAdopted` → `regionTelemetryOnAdopt$` replays
|
|
3628
|
+
* `regionResolved`, or the region phase fires into a session that doesn't
|
|
3629
|
+
* exist yet and is lost. `defer` guarantees that ordering.
|
|
3607
3630
|
*/
|
|
3608
|
-
awaitAndAdopt$(
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3631
|
+
awaitAndAdopt$() {
|
|
3632
|
+
return defer(() => {
|
|
3633
|
+
const parked = window.__stream;
|
|
3634
|
+
if (!parked) {
|
|
3635
|
+
return of(false);
|
|
3636
|
+
}
|
|
3637
|
+
if (parked.state !== 'connecting') {
|
|
3638
|
+
return of(this.tryAdopt());
|
|
3639
|
+
}
|
|
3640
|
+
Logger.info('[Adoption] Prefetch in progress — waiting before connecting');
|
|
3641
|
+
return this.awaitSettled$(parked);
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Event-driven wait: the script fires `onSettled` the instant it parks
|
|
3646
|
+
* `ready`/`failed`, so we adopt with zero polling lag. Emits exactly once.
|
|
3647
|
+
*
|
|
3648
|
+
* No timer/budget by design: an indefinitely `connecting` stream is valid
|
|
3649
|
+
* (DIRECT orchestration holds the socket open for minutes while it
|
|
3650
|
+
* provisions), and every terminal state — including a dead socket — flips to
|
|
3651
|
+
* `ready`/`failed` and fires the hook. Teardown (route change → unsubscribe)
|
|
3652
|
+
* sets `done`, so a late callback after cancel can't trigger a stray adoption.
|
|
3653
|
+
*/
|
|
3654
|
+
awaitSettled$(parked) {
|
|
3655
|
+
return new Observable((subscriber) => {
|
|
3656
|
+
let done = false;
|
|
3657
|
+
parked.onSettled(() => {
|
|
3658
|
+
if (done)
|
|
3659
|
+
return;
|
|
3660
|
+
done = true;
|
|
3661
|
+
subscriber.next(this.tryAdopt());
|
|
3662
|
+
subscriber.complete();
|
|
3663
|
+
});
|
|
3664
|
+
return () => {
|
|
3665
|
+
done = true;
|
|
3666
|
+
};
|
|
3667
|
+
});
|
|
3621
3668
|
}
|
|
3622
3669
|
/**
|
|
3623
3670
|
* @returns `true` if a ready parked stream was adopted (caller must NOT
|
|
@@ -3638,7 +3685,7 @@ class StreamAdoptionService {
|
|
|
3638
3685
|
return false;
|
|
3639
3686
|
}
|
|
3640
3687
|
Logger.info('[Adoption] Adopting prefetched stream', parked.timings);
|
|
3641
|
-
//
|
|
3688
|
+
// Clear the script's timers and mark the handoff complete.
|
|
3642
3689
|
parked.adopt();
|
|
3643
3690
|
// Flag cirrus connected (so a later `initSignalling` won't open a 2nd
|
|
3644
3691
|
// connection), record the adopted machine, and hydrate the streamer info
|
|
@@ -3649,6 +3696,7 @@ class StreamAdoptionService {
|
|
|
3649
3696
|
this.store.dispatch(streamAdopted({
|
|
3650
3697
|
machineId: parked.machineId ?? null,
|
|
3651
3698
|
ssData: parked.ssData ?? null,
|
|
3699
|
+
timings: parked.timings ?? null,
|
|
3652
3700
|
}));
|
|
3653
3701
|
// Hand the live socket over without re-sending `requestStream`, reusing the
|
|
3654
3702
|
// script's correlationId (the one `requestStream` was already sent with).
|
|
@@ -4197,9 +4245,7 @@ class UnrealEffects {
|
|
|
4197
4245
|
// connection; otherwise fall back to a fresh signalling connection.
|
|
4198
4246
|
// `takeUntil(abort)` cancels the wait on teardown / route change so a
|
|
4199
4247
|
// fast switch can't leave overlapping connections.
|
|
4200
|
-
this.streamAdoptionService
|
|
4201
|
-
.awaitAndAdopt$(this.dataChannelConnectionTimeout)
|
|
4202
|
-
.pipe(switchMap((adopted) => adopted ? EMPTY : this.signallingService.connectToSignaling(urls)), takeUntil(this.actions$.pipe(ofType(abortEstablishingConnection))),
|
|
4248
|
+
this.streamAdoptionService.awaitAndAdopt$().pipe(switchMap((adopted) => adopted ? EMPTY : this.signallingService.connectToSignaling(urls)), takeUntil(this.actions$.pipe(ofType(abortEstablishingConnection))),
|
|
4203
4249
|
// The adopt/connect stream is side-effect-only (its values aren't
|
|
4204
4250
|
// actions) — swallow them, and emit `setEstablishingConnection`
|
|
4205
4251
|
// first so the reconnect/resume popup stays hidden during the
|
|
@@ -5508,6 +5554,19 @@ const QP_RED_THRESHOLD = 35;
|
|
|
5508
5554
|
const FPS_LOW_RATIO = 0.7; // p50 fps below 70% of target
|
|
5509
5555
|
const UNLIMITED_REFERENCE_FPS = 60;
|
|
5510
5556
|
const SAMPLE_WINDOW_MS = 5000;
|
|
5557
|
+
/**
|
|
5558
|
+
* Catalog phase ids the prefetch establishes invisibly. For an adopted
|
|
5559
|
+
* connection their live closers are suppressed and the phases are emitted by
|
|
5560
|
+
* {@link StreamStatusTelemetryService2.emitAdoptedConnection} instead, timed
|
|
5561
|
+
* from the prefetch's own marks and anchored at the moment Angular reached the
|
|
5562
|
+
* connect call (session start) — so the waterfall shows the time the prefetch
|
|
5563
|
+
* actually SAVED, not the prefetch's page-load-parallel raw timeline.
|
|
5564
|
+
*/
|
|
5565
|
+
const PREFETCHED_PHASE_IDS = new Set([
|
|
5566
|
+
'region-resolving',
|
|
5567
|
+
'signaling-connection',
|
|
5568
|
+
'webrtc-negotiation',
|
|
5569
|
+
]);
|
|
5511
5570
|
// Persistent per-browser identifier — survives page reloads. Used to group
|
|
5512
5571
|
// multiple sessions of the same anonymous user for funnel / retention analysis.
|
|
5513
5572
|
const USER_ID_STORAGE_KEY = 'source3d.streamTelemetry.userId';
|
|
@@ -5538,6 +5597,17 @@ class StreamStatusTelemetryService2 {
|
|
|
5538
5597
|
this.disconnectReason$ = this.store.selectSignal(unrealFeature.selectDisconnectReason);
|
|
5539
5598
|
this.errorMessage$ = this.store.selectSignal(unrealFeature.selectErrorMessage);
|
|
5540
5599
|
this.viewPortReady$ = this.store.selectSignal(unrealFeature.selectViewportReady);
|
|
5600
|
+
/**
|
|
5601
|
+
* Synchronous adoption flag. The `streamAdopted` reducer sets it BEFORE any
|
|
5602
|
+
* action subscriber runs, so the connection-phase closers (which fire inside
|
|
5603
|
+
* the same dispatch, e.g. `ssinfo-received` off the `ssData` state change)
|
|
5604
|
+
* can read it and defer to the prefetch-timed attribution emitted by
|
|
5605
|
+
* {@link emitAdoptedConnection} instead of recording the live wall-clock.
|
|
5606
|
+
*/
|
|
5607
|
+
this.adopted$ = this.store.selectSignal(unrealFeature.selectAdopted);
|
|
5608
|
+
/** Streamer identity for the adopted signaling phase params. */
|
|
5609
|
+
this.ssData$ = this.store.selectSignal(unrealFeature.selectSsData);
|
|
5610
|
+
this.awsInstance$ = this.store.selectSignal(unrealFeature.selectAwsInstance);
|
|
5541
5611
|
this.session = null;
|
|
5542
5612
|
/**
|
|
5543
5613
|
* Whether a session has already started during the current page lifetime.
|
|
@@ -5615,7 +5685,10 @@ class StreamStatusTelemetryService2 {
|
|
|
5615
5685
|
// CLOSER — the `regionResolved` action carries the raw ping
|
|
5616
5686
|
// samples (one per probe) from `RegionsPingService`; the
|
|
5617
5687
|
// catalog flattens them to a comma-separated string since
|
|
5618
|
-
// `toPhaseParams` only accepts primitives.
|
|
5688
|
+
// `toPhaseParams` only accepts primitives. Adopted connections never
|
|
5689
|
+
// run `getRegion`, but `UnrealEffects.regionTelemetryOnAdopt$` replays
|
|
5690
|
+
// the region waterfall (start → end → resolved) from the cached winner,
|
|
5691
|
+
// so this phase still appears with its ping samples.
|
|
5619
5692
|
{
|
|
5620
5693
|
id: 'region-resolved',
|
|
5621
5694
|
event: this.actions$.pipe(ofType(regionResolved), take(1), map(({ region, pingsMs }) => ({
|
|
@@ -5927,6 +6000,13 @@ class StreamStatusTelemetryService2 {
|
|
|
5927
6000
|
if (!this.session)
|
|
5928
6001
|
return;
|
|
5929
6002
|
if (isCloser) {
|
|
6003
|
+
// Adopted connections receive the region / signaling / webrtc
|
|
6004
|
+
// phases from `emitAdoptedConnection` (timed from the prefetch's
|
|
6005
|
+
// own marks). Skip the live closer for those, so the whole
|
|
6006
|
+
// pre-adoption wait isn't dumped onto whichever connection closer
|
|
6007
|
+
// happens to fire first at the adoption instant.
|
|
6008
|
+
if (this.adopted$() && PREFETCHED_PHASE_IDS.has(bucket.id))
|
|
6009
|
+
return;
|
|
5930
6010
|
this.emitPhaseEnded(bucket, bucketIdx, value);
|
|
5931
6011
|
return;
|
|
5932
6012
|
}
|
|
@@ -5937,6 +6017,12 @@ class StreamStatusTelemetryService2 {
|
|
|
5937
6017
|
}));
|
|
5938
6018
|
});
|
|
5939
6019
|
});
|
|
6020
|
+
// Adopted connection: emit the region / signaling / webrtc phases timed
|
|
6021
|
+
// from the prefetch marks, anchored at session start (see method doc). The
|
|
6022
|
+
// live closers for these phases are suppressed (PREFETCHED_PHASE_IDS).
|
|
6023
|
+
sub.add(this.actions$
|
|
6024
|
+
.pipe(ofType(streamAdopted))
|
|
6025
|
+
.subscribe(({ timings }) => this.emitAdoptedConnection(timings)));
|
|
5940
6026
|
const bootstrapInfo = this.collectBootstrapInfo();
|
|
5941
6027
|
this.emitLifecycle('stream.session_started', {
|
|
5942
6028
|
sessionId: id,
|
|
@@ -6054,6 +6140,154 @@ class StreamStatusTelemetryService2 {
|
|
|
6054
6140
|
overlays,
|
|
6055
6141
|
});
|
|
6056
6142
|
}
|
|
6143
|
+
/**
|
|
6144
|
+
* Emit the region / signaling / webrtc phases for an ADOPTED connection.
|
|
6145
|
+
*
|
|
6146
|
+
* The prefetch script established the connection in PARALLEL with page load,
|
|
6147
|
+
* so the phases are positioned at their real (anchor-relative) spans — shifted
|
|
6148
|
+
* LEFT to overlap the HTML-loading / route-idle rows — rather than sequentially
|
|
6149
|
+
* after them. That's the whole point of the prefetch: the handshake already
|
|
6150
|
+
* happened while the page was loading. Each phase ships:
|
|
6151
|
+
* - `duration` — the real sub-step time the prefetch spent.
|
|
6152
|
+
* - `spanStartMs` — anchor-relative start, so the viewer left-shifts the bar.
|
|
6153
|
+
* - `adoptionAtMs` — where Angular took over (the next phase, Unreal
|
|
6154
|
+
* communication, resumes here on the sequential timeline).
|
|
6155
|
+
* - savings params: `prefetchConnectMs` (full handshake), `prefetchSavedMs`
|
|
6156
|
+
* (done before Angular was ready), `adoptionWaitMs` (real connect→adopted
|
|
6157
|
+
* wait), `adoptionOverheadMs` (prefetch-ready → adopted gap).
|
|
6158
|
+
*/
|
|
6159
|
+
emitAdoptedConnection(timings) {
|
|
6160
|
+
const s = this.session;
|
|
6161
|
+
if (!s)
|
|
6162
|
+
return;
|
|
6163
|
+
const t = timings ?? {};
|
|
6164
|
+
const now = performance.now();
|
|
6165
|
+
// Everything in anchor-relative ms — the same scale the viewer renders on
|
|
6166
|
+
// (and the prefetch marks share the page `performance` clock).
|
|
6167
|
+
const toMs = (perf) => Math.round(perf - s.anchorPerf);
|
|
6168
|
+
const startMs = toMs(t['prefetch_start'] ?? s.startedAtPerf);
|
|
6169
|
+
const wsOpenMs = Math.max(startMs, toMs(t['ws_open'] ?? t['prefetch_start'] ?? s.startedAtPerf));
|
|
6170
|
+
const readyMs = Math.max(wsOpenMs, toMs(t['ready'] ?? t['dc_open'] ?? t['ws_open'] ?? s.startedAtPerf));
|
|
6171
|
+
const adoptionAtMs = toMs(now);
|
|
6172
|
+
const sessionStartMs = toMs(s.startedAtPerf);
|
|
6173
|
+
const sigDur = wsOpenMs - startMs;
|
|
6174
|
+
const webDur = readyMs - wsOpenMs;
|
|
6175
|
+
const connectMs = readyMs - startMs;
|
|
6176
|
+
// Main-thread blocking from the OPTIONAL `perf-longtask.js` head-script. If
|
|
6177
|
+
// that script isn't on the page `window.__streamPerf` is undefined and
|
|
6178
|
+
// these stay 0 — nothing breaks. Tasks carry page-`performance.now()` start
|
|
6179
|
+
// times, so window them against the raw (pre-`toMs`) ready/adoption marks:
|
|
6180
|
+
// bootBlockingMs — total over [0, adoption]: overall JS cost to boot.
|
|
6181
|
+
// adoptBlockingMs — within [ready, adoption]: how much of the
|
|
6182
|
+
// "awaiting Angular code" gap was the thread actually
|
|
6183
|
+
// blocked by JS (vs idle waiting). ≈gap ⇒ JS-bound;
|
|
6184
|
+
// ≈0 ⇒ the app was just slow to reach the adopt call.
|
|
6185
|
+
const readyPerf = t['ready'] ?? t['dc_open'] ?? t['ws_open'] ?? s.startedAtPerf;
|
|
6186
|
+
const tasks = window.__streamPerf?.longTasks ?? [];
|
|
6187
|
+
let bootBlockingMs = 0;
|
|
6188
|
+
let adoptBlockingMs = 0;
|
|
6189
|
+
let maxLongTaskMs = 0;
|
|
6190
|
+
for (const lt of tasks) {
|
|
6191
|
+
if (lt.start > now)
|
|
6192
|
+
continue; // ignore tasks after adoption
|
|
6193
|
+
bootBlockingMs += lt.dur;
|
|
6194
|
+
if (lt.dur > maxLongTaskMs)
|
|
6195
|
+
maxLongTaskMs = lt.dur;
|
|
6196
|
+
if (lt.start >= readyPerf)
|
|
6197
|
+
adoptBlockingMs += lt.dur;
|
|
6198
|
+
}
|
|
6199
|
+
// Savings, anchored at the moment Angular reached the connect call.
|
|
6200
|
+
const savings = {
|
|
6201
|
+
prefetched: true,
|
|
6202
|
+
prefetchConnectMs: connectMs,
|
|
6203
|
+
// How much of the handshake finished before Angular was ready (the win).
|
|
6204
|
+
prefetchSavedMs: Math.max(0, Math.min(connectMs, sessionStartMs - startMs)),
|
|
6205
|
+
// The real wait Angular experienced (connect call → adopted).
|
|
6206
|
+
adoptionWaitMs: Math.max(0, adoptionAtMs - sessionStartMs),
|
|
6207
|
+
// Gap between the prefetch being ready and Angular adopting it.
|
|
6208
|
+
adoptionOverheadMs: Math.max(0, adoptionAtMs - readyMs),
|
|
6209
|
+
// Main-thread blocking (longtask) — diagnoses whether the gap is JS-bound.
|
|
6210
|
+
bootBlockingMs: Math.round(bootBlockingMs),
|
|
6211
|
+
adoptBlockingMs: Math.round(adoptBlockingMs),
|
|
6212
|
+
maxLongTaskMs: Math.round(maxLongTaskMs),
|
|
6213
|
+
};
|
|
6214
|
+
// Region (phaseNo 1) — served from the ping cache during prefetch (~0),
|
|
6215
|
+
// positioned at the prefetch start; ship the samples.
|
|
6216
|
+
const region = this.readRegionCache();
|
|
6217
|
+
this.emitAdoptedPhase(0, startMs, 0, adoptionAtMs, {
|
|
6218
|
+
region: region?.region_code ?? '',
|
|
6219
|
+
pingsMs: region?.result.length ? `${region.result.join(', ')} ms` : null,
|
|
6220
|
+
});
|
|
6221
|
+
// Signaling (phaseNo 2) — [prefetch_start → ws_open]; streamer identity +
|
|
6222
|
+
// savings; ws-connected marker sits at the phase's own end.
|
|
6223
|
+
this.emitAdoptedPhase(1, startMs, sigDur, adoptionAtMs, {
|
|
6224
|
+
...this.adoptedSignalingParams(),
|
|
6225
|
+
...savings,
|
|
6226
|
+
prefetchSignalingMs: sigDur,
|
|
6227
|
+
}, [{ id: 'signaling-ws-connected', time: wsOpenMs }]);
|
|
6228
|
+
// WebRTC negotiation (phaseNo 3) — [ws_open → ready].
|
|
6229
|
+
this.emitAdoptedPhase(2, wsOpenMs, webDur, adoptionAtMs, {
|
|
6230
|
+
iceCandidateCount: 0,
|
|
6231
|
+
prefetchWebrtcMs: webDur,
|
|
6232
|
+
});
|
|
6233
|
+
// Unreal communication chains from the adoption moment (real wall-clock).
|
|
6234
|
+
s.lastMilestonePerf = now;
|
|
6235
|
+
}
|
|
6236
|
+
/**
|
|
6237
|
+
* Emit one adopted phase positioned at its real `spanStartMs` (left-shifted
|
|
6238
|
+
* to overlap page load) with its real prefetch `duration`. `prefetched`,
|
|
6239
|
+
* `spanStartMs` and `adoptionAtMs` ride as top-level wire fields the viewer
|
|
6240
|
+
* uses to lay the bar out; the rest become display params.
|
|
6241
|
+
*/
|
|
6242
|
+
emitAdoptedPhase(bucketIdx, spanStartMs, durationMs, adoptionAtMs, extras, milestones = []) {
|
|
6243
|
+
const s = this.session;
|
|
6244
|
+
if (!s)
|
|
6245
|
+
return;
|
|
6246
|
+
const bucket = this.phaseCatalog[bucketIdx];
|
|
6247
|
+
this.track('stream.phase_ended', {
|
|
6248
|
+
sessionId: s.id,
|
|
6249
|
+
phaseNo: bucketIdx + 1,
|
|
6250
|
+
id: bucket.id,
|
|
6251
|
+
color: bucket.color,
|
|
6252
|
+
duration: durationMs,
|
|
6253
|
+
prefetched: true,
|
|
6254
|
+
spanStartMs,
|
|
6255
|
+
adoptionAtMs,
|
|
6256
|
+
milestones,
|
|
6257
|
+
params: toPhaseParams({ ...extras, prefetched: true }),
|
|
6258
|
+
overlays: [],
|
|
6259
|
+
});
|
|
6260
|
+
}
|
|
6261
|
+
/** Streamer identity params for the adopted signaling phase — mirrors the
|
|
6262
|
+
* `ssinfo-received` closer's shape (client / instanceName / …ssData). */
|
|
6263
|
+
adoptedSignalingParams() {
|
|
6264
|
+
const aws = this.awsInstance$();
|
|
6265
|
+
const ssData = this.ssData$();
|
|
6266
|
+
return {
|
|
6267
|
+
client: aws?.wsUrl ?? null,
|
|
6268
|
+
instanceName: aws?.instanceName ?? null,
|
|
6269
|
+
...(ssData ?? {}),
|
|
6270
|
+
};
|
|
6271
|
+
}
|
|
6272
|
+
/** Read the region-ping cache the prefetch reused (localStorage), so the
|
|
6273
|
+
* adopted region phase can still report the winning region + samples. */
|
|
6274
|
+
readRegionCache() {
|
|
6275
|
+
try {
|
|
6276
|
+
const raw = localStorage.getItem(REGION_PING_CACHE_KEY);
|
|
6277
|
+
if (!raw)
|
|
6278
|
+
return null;
|
|
6279
|
+
const parsed = JSON.parse(raw);
|
|
6280
|
+
return {
|
|
6281
|
+
region_code: typeof parsed.region_code === 'string' ? parsed.region_code : '',
|
|
6282
|
+
result: Array.isArray(parsed.result)
|
|
6283
|
+
? parsed.result.filter((n) => typeof n === 'number')
|
|
6284
|
+
: [],
|
|
6285
|
+
};
|
|
6286
|
+
}
|
|
6287
|
+
catch {
|
|
6288
|
+
return null;
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6057
6291
|
handleWindow(samples) {
|
|
6058
6292
|
if (!this.session)
|
|
6059
6293
|
return;
|