@vue-skuilder/db 0.2.1 → 0.2.2
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.
- package/dist/core/index.js +74 -20
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +74 -20
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.d.cts +32 -0
- package/dist/impl/couch/index.d.ts +32 -0
- package/dist/impl/couch/index.js +74 -20
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +74 -20
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +2 -4
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +2 -4
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.js +74 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +74 -20
- package/dist/index.mjs.map +1 -1
- package/docs/session-lifecycle-and-replan.md +418 -0
- package/package.json +3 -3
- package/src/core/navigators/Pipeline.ts +4 -0
- package/src/core/navigators/generators/elo.ts +19 -6
- package/src/core/navigators/generators/srs.ts +10 -0
- package/src/impl/couch/courseDB.ts +146 -17
- package/src/study/SessionController.ts +33 -0
- package/src/study/services/CardHydrationService.ts +24 -0
|
@@ -302,11 +302,19 @@ export class CourseDB implements CourseDBInterface {
|
|
|
302
302
|
elo = parseInt(elo as any);
|
|
303
303
|
const limit = cardLimit ? cardLimit : 25;
|
|
304
304
|
|
|
305
|
+
// const tQ0 = performance.now();
|
|
306
|
+
// NOTE: `stale: 'update_after'` was tried here and removed — it gave no
|
|
307
|
+
// measurable speedup (PouchDB 9 effectively ignores it for the reindex
|
|
308
|
+
// cost) AND it can return an empty result on the first query after a cold
|
|
309
|
+
// DB open (index not yet loaded), which then poisons the pool cache. The
|
|
310
|
+
// session pool cache (see getCardsCenteredAtELO) is what removes the
|
|
311
|
+
// per-run cost, so we read the view normally (always-fresh) here.
|
|
305
312
|
const below: PouchDB.Query.Response<object> = await this.db.query('elo', {
|
|
306
313
|
limit: Math.ceil(limit / 2),
|
|
307
314
|
startkey: elo,
|
|
308
315
|
descending: true,
|
|
309
316
|
});
|
|
317
|
+
// const tBelowQ = performance.now();
|
|
310
318
|
|
|
311
319
|
const aboveLimit = limit - below.rows.length;
|
|
312
320
|
|
|
@@ -314,7 +322,13 @@ export class CourseDB implements CourseDBInterface {
|
|
|
314
322
|
limit: aboveLimit,
|
|
315
323
|
startkey: elo + 1,
|
|
316
324
|
});
|
|
317
|
-
//
|
|
325
|
+
// const tAbove = performance.now();
|
|
326
|
+
// [perf] parked: getCardsByELO view-query timing (below/above split)
|
|
327
|
+
// logger.info(
|
|
328
|
+
// `[perf][getCardsByELO] reqLimit=${limit} ` +
|
|
329
|
+
// `below=${(tBelowQ - tQ0).toFixed(0)}ms(${below.rows.length}r) ` +
|
|
330
|
+
// `above=${(tAbove - tBelowQ).toFixed(0)}ms(${above.rows.length}r)`
|
|
331
|
+
// );
|
|
318
332
|
|
|
319
333
|
let cards = below.rows;
|
|
320
334
|
cards = cards.concat(above.rows);
|
|
@@ -573,6 +587,8 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
|
|
|
573
587
|
|
|
574
588
|
async addNavigationStrategy(data: ContentNavigationStrategyData): Promise<void> {
|
|
575
589
|
logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
|
|
590
|
+
// Strategy set changed — drop the cached navigator so it rebuilds.
|
|
591
|
+
this.invalidateNavigatorCache();
|
|
576
592
|
// Admin write operation — use remote DB.
|
|
577
593
|
return this.remoteDB.put(data).then(() => {});
|
|
578
594
|
}
|
|
@@ -654,6 +670,35 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
|
|
|
654
670
|
*/
|
|
655
671
|
private _pendingHints: ReplanHints | null = null;
|
|
656
672
|
|
|
673
|
+
/**
|
|
674
|
+
* Session-scoped cache of the broad ELO-neighbor pool used by
|
|
675
|
+
* getCardsCenteredAtELO. The `elo` view query re-indexes on first touch per
|
|
676
|
+
* call (PouchDB 9 ignores `stale`), so without this each plan/replan pays
|
|
677
|
+
* ~1.5-2s. The pool is fetched once and re-ranked against the live (roaming)
|
|
678
|
+
* ELO in memory on subsequent calls.
|
|
679
|
+
*/
|
|
680
|
+
private _eloPoolCache: {
|
|
681
|
+
rows: (QualifiedCardID & { elo?: number })[];
|
|
682
|
+
fetchedAt: number;
|
|
683
|
+
} | null = null;
|
|
684
|
+
private readonly _eloPoolTtlMs = 5 * 60 * 1000;
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Cached assembled navigator (Pipeline). createNavigator() reads strategy
|
|
688
|
+
* docs and builds a fresh Pipeline every call — whose internal `_tagCache`
|
|
689
|
+
* and `_cachedOrchestration` are designed to make replans cheap but never
|
|
690
|
+
* survive, because the instance is discarded each run. Caching it lets those
|
|
691
|
+
* caches persist across plan/replan within a session (SessionController holds
|
|
692
|
+
* one CourseDB instance for the session's lifetime). Rebuilt on user change,
|
|
693
|
+
* TTL expiry, or explicit invalidation after a strategy-doc write.
|
|
694
|
+
*/
|
|
695
|
+
private _cachedNavigator: {
|
|
696
|
+
navigator: ContentNavigator;
|
|
697
|
+
userId: string;
|
|
698
|
+
builtAt: number;
|
|
699
|
+
} | null = null;
|
|
700
|
+
private readonly _navigatorTtlMs = 5 * 60 * 1000;
|
|
701
|
+
|
|
657
702
|
public setEphemeralHints(hints: ReplanHints): void {
|
|
658
703
|
this._pendingHints = hints;
|
|
659
704
|
}
|
|
@@ -662,18 +707,60 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
|
|
|
662
707
|
const u = await this._getCurrentUser();
|
|
663
708
|
|
|
664
709
|
try {
|
|
665
|
-
const
|
|
710
|
+
// const tNav0 = performance.now(); // [perf] parked
|
|
711
|
+
const { navigator } = await this._getCachedNavigator(u);
|
|
712
|
+
// const tNav1 = performance.now(); // [perf] parked
|
|
666
713
|
if (this._pendingHints) {
|
|
667
714
|
navigator.setEphemeralHints(this._pendingHints);
|
|
668
715
|
this._pendingHints = null;
|
|
669
716
|
}
|
|
670
|
-
|
|
717
|
+
const result = await navigator.getWeightedCards(limit);
|
|
718
|
+
// const tRun = performance.now(); // [perf] parked
|
|
719
|
+
// [perf] parked 2026-05 (pipeline-docs-workup) — uncomment to re-measure
|
|
720
|
+
// logger.info(
|
|
721
|
+
// `[perf][courseDB] getWeightedCards(limit=${limit}): ` +
|
|
722
|
+
// `navigator=${(tNav1 - tNav0).toFixed(0)}ms(${navCache}) ` +
|
|
723
|
+
// `pipelineRun=${(tRun - tNav1).toFixed(0)}ms ` +
|
|
724
|
+
// `total=${(tRun - tNav0).toFixed(0)}ms`
|
|
725
|
+
// );
|
|
726
|
+
return result;
|
|
671
727
|
} catch (e) {
|
|
672
728
|
logger.error(`[courseDB] Error getting weighted cards: ${e}`);
|
|
673
729
|
throw e;
|
|
674
730
|
}
|
|
675
731
|
}
|
|
676
732
|
|
|
733
|
+
/**
|
|
734
|
+
* Return the assembled navigator, reusing the cached instance when possible.
|
|
735
|
+
* Reuse preserves the Pipeline's per-session caches (tags, orchestration
|
|
736
|
+
* context) across replans, which is the dominant per-replan cost once the
|
|
737
|
+
* ELO-pool cost is removed. Rebuilds on user change or TTL expiry.
|
|
738
|
+
*/
|
|
739
|
+
private async _getCachedNavigator(
|
|
740
|
+
user: UserDBInterface
|
|
741
|
+
): Promise<{ navigator: ContentNavigator; cacheStatus: 'hit' | 'miss' }> {
|
|
742
|
+
const userId = user.getUsername();
|
|
743
|
+
const now = Date.now();
|
|
744
|
+
if (
|
|
745
|
+
this._cachedNavigator &&
|
|
746
|
+
this._cachedNavigator.userId === userId &&
|
|
747
|
+
now - this._cachedNavigator.builtAt < this._navigatorTtlMs
|
|
748
|
+
) {
|
|
749
|
+
return { navigator: this._cachedNavigator.navigator, cacheStatus: 'hit' };
|
|
750
|
+
}
|
|
751
|
+
const navigator = await this.createNavigator(user);
|
|
752
|
+
this._cachedNavigator = { navigator, userId, builtAt: now };
|
|
753
|
+
return { navigator, cacheStatus: 'miss' };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Drop the cached navigator so the next getWeightedCards() rebuilds it.
|
|
758
|
+
* Call after mutating this course's navigation strategy documents.
|
|
759
|
+
*/
|
|
760
|
+
public invalidateNavigatorCache(): void {
|
|
761
|
+
this._cachedNavigator = null;
|
|
762
|
+
}
|
|
763
|
+
|
|
677
764
|
public async getCardsCenteredAtELO(
|
|
678
765
|
options: {
|
|
679
766
|
limit: number;
|
|
@@ -684,6 +771,9 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
|
|
|
684
771
|
},
|
|
685
772
|
filter?: (a: QualifiedCardID) => boolean
|
|
686
773
|
): Promise<StudySessionItem[]> {
|
|
774
|
+
// [perf] parked: getCardsCenteredAtELO rewrite banner
|
|
775
|
+
// logger.info('[perf][run] getCardsCenteredAtELO rewrite (session pool cache + in-memory recenter)');
|
|
776
|
+
// const tCelo0 = performance.now();
|
|
687
777
|
let targetElo: number;
|
|
688
778
|
|
|
689
779
|
if (options.elo === 'user') {
|
|
@@ -706,26 +796,65 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
|
|
|
706
796
|
targetElo = options.elo;
|
|
707
797
|
}
|
|
708
798
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
799
|
+
// const tReg = performance.now();
|
|
800
|
+
|
|
801
|
+
// Broad neighbor pool fetched once per session and re-used. We over-fetch
|
|
802
|
+
// (POOL_SIZE >> limit) so that the in-memory active-card filter and the
|
|
803
|
+
// slowly-roaming ELO both have ample headroom before a refetch is needed.
|
|
804
|
+
const POOL_SIZE = Math.max(2000, options.limit * 4);
|
|
805
|
+
const nowMs = Date.now();
|
|
806
|
+
let cacheStatus: 'hit' | 'miss' | 'refresh' = 'hit';
|
|
807
|
+
|
|
808
|
+
if (!this._eloPoolCache || nowMs - this._eloPoolCache.fetchedAt > this._eloPoolTtlMs) {
|
|
809
|
+
// MISS: pay the (reindexing) view query once, then cache the raw pool.
|
|
810
|
+
// Guard: never cache an EMPTY pool. A cold-DB-open or sync-race fetch can
|
|
811
|
+
// transiently return [], and caching it would starve the session for the
|
|
812
|
+
// whole TTL. Leaving the cache untouched lets the next call retry.
|
|
813
|
+
const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
|
|
814
|
+
if (fetched.length > 0) {
|
|
815
|
+
this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
|
|
816
|
+
}
|
|
817
|
+
cacheStatus = 'miss';
|
|
818
|
+
}
|
|
713
819
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
820
|
+
// Apply the (fresh) caller filter, then re-center against the *current* ELO.
|
|
821
|
+
// Returns a new array each call — the cached pool is never mutated, and the
|
|
822
|
+
// ranking reflects the live ELO even as it drifts within a session.
|
|
823
|
+
const rankAgainstCurrentElo = (): (QualifiedCardID & { elo?: number })[] => {
|
|
824
|
+
const raw = this._eloPoolCache?.rows ?? [];
|
|
825
|
+
const survivors = filter ? raw.filter((c) => filter(c)) : raw;
|
|
826
|
+
return survivors
|
|
827
|
+
.map((c) => ({ ...c }))
|
|
828
|
+
.sort(
|
|
829
|
+
(a, b) =>
|
|
830
|
+
Math.abs((a.elo ?? targetElo) - targetElo) -
|
|
831
|
+
Math.abs((b.elo ?? targetElo) - targetElo)
|
|
832
|
+
);
|
|
833
|
+
};
|
|
718
834
|
|
|
719
|
-
|
|
835
|
+
let cards = rankAgainstCurrentElo();
|
|
720
836
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
837
|
+
// Refetch once if the pool can't satisfy the limit — either the active-card
|
|
838
|
+
// filter has grown past pool coverage (hit), or the pool is missing because
|
|
839
|
+
// a prior fetch came back empty (cold open / sync race). A miss that cached
|
|
840
|
+
// a non-empty-but-small pool (genuinely small course) is left alone.
|
|
841
|
+
if (cards.length < options.limit && (cacheStatus === 'hit' || !this._eloPoolCache)) {
|
|
842
|
+
const fetched = await this.getCardsByELO(targetElo, POOL_SIZE);
|
|
843
|
+
if (fetched.length > 0) {
|
|
844
|
+
this._eloPoolCache = { rows: fetched, fetchedAt: nowMs };
|
|
724
845
|
}
|
|
725
|
-
|
|
726
|
-
|
|
846
|
+
cards = rankAgainstCurrentElo();
|
|
847
|
+
cacheStatus = 'refresh';
|
|
727
848
|
}
|
|
728
849
|
|
|
850
|
+
// [perf] parked: centeredAtELO regDoc / pool-cache timing
|
|
851
|
+
// logger.info(
|
|
852
|
+
// `[perf][centeredAtELO] regDoc=${(tReg - tCelo0).toFixed(0)}ms ` +
|
|
853
|
+
// `cache=${cacheStatus} build=${(performance.now() - tReg).toFixed(0)}ms ` +
|
|
854
|
+
// `poolRaw=${this._eloPoolCache?.rows.length ?? 0} postFilter=${cards.length} ` +
|
|
855
|
+
// `limit=${options.limit} targetElo=${targetElo}`
|
|
856
|
+
// );
|
|
857
|
+
|
|
729
858
|
const selectedCards: {
|
|
730
859
|
courseID: string;
|
|
731
860
|
cardID: string;
|
|
@@ -478,7 +478,13 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
478
478
|
this.log(`[Replan] Card guarantee set to ${this._minCardsGuarantee}`);
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
+
// [perf] parked 2026-05 (pipeline-docs-workup) — uncomment to re-measure
|
|
482
|
+
// const tReplan0 = performance.now();
|
|
481
483
|
await this._executeReplan(opts);
|
|
484
|
+
// logger.info(
|
|
485
|
+
// `[perf][SessionController] replan${labelTag} (limit=${opts.limit ?? 'default'}, ` +
|
|
486
|
+
// `mode=${opts.mode ?? 'replace'}) took ${(performance.now() - tReplan0).toFixed(0)}ms`
|
|
487
|
+
// );
|
|
482
488
|
}
|
|
483
489
|
|
|
484
490
|
/**
|
|
@@ -691,6 +697,7 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
691
697
|
additive?: boolean;
|
|
692
698
|
limit?: number;
|
|
693
699
|
}): Promise<number> {
|
|
700
|
+
// const tGwc0 = performance.now(); // [perf] parked
|
|
694
701
|
const replan = options?.replan ?? false;
|
|
695
702
|
const additive = options?.additive ?? false;
|
|
696
703
|
const newLimit = options?.limit ?? this._defaultBatchLimit;
|
|
@@ -721,6 +728,8 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
721
728
|
}
|
|
722
729
|
}
|
|
723
730
|
|
|
731
|
+
// const tSources = performance.now(); // [perf] parked
|
|
732
|
+
|
|
724
733
|
// Verify we got content from at least one source
|
|
725
734
|
if (batches.length === 0) {
|
|
726
735
|
if (replan) {
|
|
@@ -736,6 +745,7 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
736
745
|
|
|
737
746
|
// Mix weighted cards across sources using configured strategy
|
|
738
747
|
const mixedWeighted = this.mixer.mix(batches, fetchLimit * this.sources.length);
|
|
748
|
+
// const tMixed = performance.now(); // [perf] parked
|
|
739
749
|
|
|
740
750
|
// Capture mixer run for debugging - fetch course names
|
|
741
751
|
const sourceIds = batches.map((b) => {
|
|
@@ -834,6 +844,18 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
834
844
|
}
|
|
835
845
|
|
|
836
846
|
this.log(report);
|
|
847
|
+
|
|
848
|
+
// [perf] parked: getWeightedContent stage timing
|
|
849
|
+
// const tEnd = performance.now();
|
|
850
|
+
// logger.info(
|
|
851
|
+
// `[perf][SessionController] getWeightedContent(replan=${replan}): ` +
|
|
852
|
+
// `sources=${(tSources - tGwc0).toFixed(0)}ms ` +
|
|
853
|
+
// `mix=${(tMixed - tSources).toFixed(0)}ms ` +
|
|
854
|
+
// `post=${(tEnd - tMixed).toFixed(0)}ms ` +
|
|
855
|
+
// `total=${(tEnd - tGwc0).toFixed(0)}ms ` +
|
|
856
|
+
// `[sources=${this.sources.length} fetchLimit=${fetchLimit} newLimit=${newLimit}]`
|
|
857
|
+
// );
|
|
858
|
+
|
|
837
859
|
return wellIndicated;
|
|
838
860
|
}
|
|
839
861
|
|
|
@@ -938,6 +960,10 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
938
960
|
public async nextCard(
|
|
939
961
|
action: SessionAction = 'dismiss-success'
|
|
940
962
|
): Promise<HydratedCard<TView> | null> {
|
|
963
|
+
// [perf] parked: nextCard provenance/timing (awaitedReplan, wedgeRuns)
|
|
964
|
+
// const tNext0 = performance.now();
|
|
965
|
+
// let awaitedInFlightReplan = false;
|
|
966
|
+
// let wedgeRuns = 0;
|
|
941
967
|
// dismiss (or sort to failedQ) the current card
|
|
942
968
|
this.dismissCurrentCard(action);
|
|
943
969
|
|
|
@@ -959,6 +985,7 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
959
985
|
this.failedQ.length === 0
|
|
960
986
|
) {
|
|
961
987
|
this.log('nextCard: queues empty, awaiting in-flight replan before drawing');
|
|
988
|
+
// awaitedInFlightReplan = true; // [perf] parked
|
|
962
989
|
await this._replanPromise;
|
|
963
990
|
}
|
|
964
991
|
|
|
@@ -1053,6 +1080,7 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
1053
1080
|
`Running pipeline (attempt ${wedgeEmptyStreak + 1}/${WEDGE_MAX_EMPTY_STREAK}).`
|
|
1054
1081
|
);
|
|
1055
1082
|
await this._replanUncoalesced({ label: 'wedge-breaker' });
|
|
1083
|
+
// wedgeRuns++; // [perf] parked
|
|
1056
1084
|
if (
|
|
1057
1085
|
this.newQ.length === 0 &&
|
|
1058
1086
|
this.reviewQ.length === 0 &&
|
|
@@ -1115,6 +1143,11 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
1115
1143
|
// Snapshot queue state
|
|
1116
1144
|
snapshotQueues(this.reviewQ.length, this.newQ.length, this.failedQ.length);
|
|
1117
1145
|
|
|
1146
|
+
// [perf] parked: per-draw nextCard timing
|
|
1147
|
+
// logger.info(
|
|
1148
|
+
// `[perf][nextCard] -> ${card.item.cardID} in ${(performance.now() - tNext0).toFixed(0)}ms ` +
|
|
1149
|
+
// `(awaitedReplan=${awaitedInFlightReplan} wedgeRuns=${wedgeRuns})`
|
|
1150
|
+
// );
|
|
1118
1151
|
return card;
|
|
1119
1152
|
}
|
|
1120
1153
|
|
|
@@ -163,6 +163,9 @@ export class CardHydrationService<TView = unknown> {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
this.hydrationInProgress = true;
|
|
166
|
+
// [perf] parked 2026-05 (pipeline-docs-workup) — batch hydration timing
|
|
167
|
+
// const tFill0 = performance.now();
|
|
168
|
+
// let hydratedThisBatch = 0;
|
|
166
169
|
|
|
167
170
|
try {
|
|
168
171
|
const itemsToHydrate = this.getItemsToHydrate();
|
|
@@ -175,12 +178,20 @@ export class CardHydrationService<TView = unknown> {
|
|
|
175
178
|
|
|
176
179
|
try {
|
|
177
180
|
await this.hydrateCard(item);
|
|
181
|
+
// hydratedThisBatch++; // [perf] parked
|
|
178
182
|
} catch (e) {
|
|
179
183
|
logger.error(`[CardHydrationService] Error hydrating card ${item.cardID}:`, e);
|
|
180
184
|
}
|
|
181
185
|
}
|
|
182
186
|
} finally {
|
|
183
187
|
this.hydrationInProgress = false;
|
|
188
|
+
// [perf] parked: batch hydration timing
|
|
189
|
+
// if (hydratedThisBatch > 0) {
|
|
190
|
+
// logger.info(
|
|
191
|
+
// `[perf][Hydrate] batch: hydrated ${hydratedThisBatch} card(s) in ` +
|
|
192
|
+
// `${(performance.now() - tFill0).toFixed(0)}ms`
|
|
193
|
+
// );
|
|
194
|
+
// }
|
|
184
195
|
}
|
|
185
196
|
}
|
|
186
197
|
|
|
@@ -195,11 +206,13 @@ export class CardHydrationService<TView = unknown> {
|
|
|
195
206
|
this.hydrationInFlight.add(item.cardID);
|
|
196
207
|
|
|
197
208
|
try {
|
|
209
|
+
// const tH0 = performance.now(); // [perf] parked
|
|
198
210
|
const courseDB = this.getCourseDB(item.courseID);
|
|
199
211
|
const [cardData, tagsByCard] = await Promise.all([
|
|
200
212
|
courseDB.getCourseDoc<CardData>(item.cardID),
|
|
201
213
|
courseDB.getAppliedTagsBatch([item.cardID]),
|
|
202
214
|
]);
|
|
215
|
+
// const tFetch = performance.now(); // [perf] parked
|
|
203
216
|
|
|
204
217
|
if (!isCourseElo(cardData.elo)) {
|
|
205
218
|
cardData.elo = toCourseElo(cardData.elo);
|
|
@@ -214,6 +227,7 @@ export class CardHydrationService<TView = unknown> {
|
|
|
214
227
|
})
|
|
215
228
|
)
|
|
216
229
|
);
|
|
230
|
+
// const tDocs = performance.now(); // [perf] parked
|
|
217
231
|
|
|
218
232
|
// Extract audio URLs from all data fields and prefetch them
|
|
219
233
|
const audioToPrefetch: string[] = [];
|
|
@@ -224,6 +238,7 @@ export class CardHydrationService<TView = unknown> {
|
|
|
224
238
|
});
|
|
225
239
|
|
|
226
240
|
// Dedupe and prefetch, waiting for browser cache to be ready
|
|
241
|
+
// const tAudioStart = performance.now(); // [perf] parked
|
|
227
242
|
const uniqueAudioUrls = [...new Set(audioToPrefetch)];
|
|
228
243
|
if (uniqueAudioUrls.length > 0) {
|
|
229
244
|
logger.debug(
|
|
@@ -231,6 +246,7 @@ export class CardHydrationService<TView = unknown> {
|
|
|
231
246
|
);
|
|
232
247
|
await Promise.allSettled(uniqueAudioUrls.map(prefetchAudio));
|
|
233
248
|
}
|
|
249
|
+
// const tAudio = performance.now(); // [perf] parked
|
|
234
250
|
|
|
235
251
|
const data = dataDocs.map(displayableDataToViewData).reverse();
|
|
236
252
|
|
|
@@ -241,6 +257,14 @@ export class CardHydrationService<TView = unknown> {
|
|
|
241
257
|
tags: tagsByCard.get(item.cardID) ?? [],
|
|
242
258
|
});
|
|
243
259
|
|
|
260
|
+
// [perf] parked: per-card hydration timing (cardDoc+tags / dataDocs / audio)
|
|
261
|
+
// logger.info(
|
|
262
|
+
// `[perf][Hydrate] ${item.cardID}: ` +
|
|
263
|
+
// `cardDoc+tags=${(tFetch - tH0).toFixed(0)}ms ` +
|
|
264
|
+
// `dataDocs=${(tDocs - tFetch).toFixed(0)}ms ` +
|
|
265
|
+
// `audioPrefetch=${(tAudio - tAudioStart).toFixed(0)}ms(${uniqueAudioUrls.length} files) ` +
|
|
266
|
+
// `total=${(tAudio - tH0).toFixed(0)}ms`
|
|
267
|
+
// );
|
|
244
268
|
logger.debug(`[CardHydrationService] Hydrated card ${item.cardID}`);
|
|
245
269
|
} finally {
|
|
246
270
|
this.hydrationInFlight.delete(item.cardID);
|