Skip to content

Commit b2818c3

Browse files
Adds load times to launchpad telemetry context, and adds event for slow loads (#3339)
* Includes item load times and slow load event in Launchpad telemetry * Uses utils and improves event typing * Groups timings to prop and sends duration in slow event
1 parent eb9a3d7 commit b2818c3

File tree

3 files changed

+135
-39
lines changed

3 files changed

+135
-39
lines changed

src/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,12 @@ export type TelemetryEvents = {
13441344
'launchpad/indicator/hidden': void;
13451345
/** Sent when the launchpad indicator loads (with data) for the first time ever for this device */
13461346
'launchpad/indicator/firstLoad': void;
1347+
/** Sent when a launchpad operation is taking longer than a set timeout to complete */
1348+
'launchpad/operation/slow': {
1349+
timeout: number;
1350+
operation: 'getMyPullRequests' | 'getCodeSuggestions' | 'getEnrichedItems' | 'getCodeSuggestionCounts';
1351+
duration: number;
1352+
};
13471353

13481354
/** Sent when a PR review was started in the inspect overview */
13491355
openReviewMode: {
@@ -1469,6 +1475,9 @@ type LaunchpadEventData = LaunchpadEventDataBase &
14691475
{
14701476
'items.count': number;
14711477
'groups.count': number;
1478+
'items.timings.prs': number;
1479+
'items.timings.codeSuggestionCounts': number;
1480+
'items.timings.enrichedItems': number;
14721481
} & Record<`groups.${LaunchpadGroups}.count`, number> &
14731482
Record<`groups.${LaunchpadGroups}.collapsed`, boolean | undefined>
14741483
>;

src/plus/focus/focus.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,14 @@ import {
4646
ProviderBuildStatusState,
4747
ProviderPullRequestReviewState,
4848
} from '../integrations/providers/models';
49-
import type { FocusAction, FocusActionCategory, FocusGroup, FocusItem, FocusTargetAction } from './focusProvider';
49+
import type {
50+
FocusAction,
51+
FocusActionCategory,
52+
FocusGroup,
53+
FocusItem,
54+
FocusItemsWithDurations,
55+
FocusTargetAction,
56+
} from './focusProvider';
5057
import {
5158
countFocusItemGroups,
5259
getFocusItemIdHash,
@@ -87,7 +94,7 @@ export interface FocusItemQuickPickItem extends QuickPickItemOfT<FocusItem> {
8794
}
8895

8996
interface Context {
90-
items: FocusItem[];
97+
items: FocusItemsWithDurations;
9198
itemsError?: Error;
9299

93100
title: string;
@@ -193,7 +200,14 @@ export class FocusCommand extends QuickCommand<State> {
193200
}
194201

195202
const context: Context = {
196-
items: [],
203+
items: {
204+
items: [],
205+
timings: {
206+
prs: undefined,
207+
codeSuggestionCounts: undefined,
208+
enrichedItems: undefined,
209+
},
210+
},
197211
itemsError: undefined,
198212
title: this.title,
199213
collapsed: collapsed,
@@ -322,11 +336,11 @@ export class FocusCommand extends QuickCommand<State> {
322336
context: Context,
323337
{ picked, selectTopItem }: { picked?: string; selectTopItem?: boolean },
324338
): StepResultGenerator<GroupedFocusItem> {
325-
const getItems = (categorizedItems: FocusItem[]) => {
339+
const getItems = (categorizedItems: FocusItemsWithDurations) => {
326340
const items: (FocusItemQuickPickItem | DirectiveQuickPickItem)[] = [];
327341

328-
if (categorizedItems?.length) {
329-
const uiGroups = groupAndSortFocusItems(categorizedItems);
342+
if (categorizedItems.items?.length) {
343+
const uiGroups = groupAndSortFocusItems(categorizedItems.items);
330344
const topItem: FocusItem | undefined =
331345
!selectTopItem || picked != null
332346
? undefined
@@ -422,7 +436,7 @@ export class FocusCommand extends QuickCommand<State> {
422436
};
423437
}
424438

425-
if (!context.items.length) {
439+
if (!context.items.items.length) {
426440
return {
427441
placeholder: 'All done! Take a vacation',
428442
items: [createDirectiveQuickPickItem(Directive.Cancel, undefined, { label: 'OK' })],
@@ -768,7 +782,7 @@ export class FocusCommand extends QuickCommand<State> {
768782
break;
769783
}
770784

771-
if (item.codeSuggestions != null && item.codeSuggestions.length > 0) {
785+
if (item.codeSuggestions?.value != null && item.codeSuggestions.value.length > 0) {
772786
if (information.length > 0) {
773787
information.push(createDirectiveQuickPickItem(Directive.Noop, false, { label: '' }));
774788
}
@@ -850,17 +864,17 @@ export class FocusCommand extends QuickCommand<State> {
850864
private getFocusItemCodeSuggestionInformation(
851865
item: FocusItem,
852866
): (QuickPickItemOfT<FocusTargetAction> | DirectiveQuickPickItem)[] {
853-
if (item.codeSuggestions == null || item.codeSuggestions.length === 0) {
867+
if (item.codeSuggestions?.value == null || item.codeSuggestions.value.length === 0) {
854868
return [];
855869
}
856870

857871
const codeSuggestionInfo: (QuickPickItemOfT<FocusTargetAction> | DirectiveQuickPickItem)[] = [
858872
createDirectiveQuickPickItem(Directive.Noop, false, {
859-
label: `$(gitlens-code-suggestion) ${pluralize('code suggestion', item.codeSuggestions.length)}`,
873+
label: `$(gitlens-code-suggestion) ${pluralize('code suggestion', item.codeSuggestions.value.length)}`,
860874
}),
861875
];
862876

863-
for (const suggestion of item.codeSuggestions) {
877+
for (const suggestion of item.codeSuggestions.value) {
864878
codeSuggestionInfo.push(
865879
createQuickPickItemOfT(
866880
{
@@ -1002,7 +1016,9 @@ async function updateContextItems(container: Container, context: Context, option
10021016
context.items = await container.focus.getCategorizedItems(options);
10031017
context.itemsError = undefined;
10041018
} catch (ex) {
1005-
context.items = [];
1019+
context.items = {
1020+
items: [],
1021+
};
10061022
context.itemsError = ex;
10071023
}
10081024
if (container.telemetry.enabled) {
@@ -1013,12 +1029,15 @@ async function updateContextItems(container: Container, context: Context, option
10131029
function updateTelemetryContext(context: Context) {
10141030
if (context.telemetryContext == null) return;
10151031

1016-
const grouped = countFocusItemGroups(context.items);
1032+
const grouped = countFocusItemGroups(context.items.items);
10171033

10181034
const updatedContext: NonNullable<(typeof context)['telemetryContext']> = {
10191035
...context.telemetryContext,
1020-
'items.count': context.items.length,
1036+
'items.count': context.items.items.length,
10211037
'groups.count': grouped.size,
1038+
'items.timings.prs': context.items.timings?.prs,
1039+
'items.timings.codeSuggestionCounts': context.items.timings?.codeSuggestionCounts,
1040+
'items.timings.enrichedItems': context.items.timings?.enrichedItems,
10221041
};
10231042

10241043
for (const [group, count] of grouped) {

src/plus/focus/focusProvider.ts

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import { configuration } from '../../system/configuration';
1818
import { debug, log } from '../../system/decorators/log';
1919
import { Logger } from '../../system/logger';
2020
import { getLogScope } from '../../system/logger.scope';
21-
import { getSettledValue } from '../../system/promise';
21+
import type { TimedResult } from '../../system/promise';
22+
import { getSettledValue, timedWithSlowThreshold } from '../../system/promise';
2223
import { openUrl } from '../../system/utils';
2324
import type { UriTypes } from '../../uris/deepLinks/deepLink';
2425
import { DeepLinkActionType, DeepLinkType } from '../../uris/deepLinks/deepLink';
@@ -136,7 +137,7 @@ export type FocusPullRequest = EnrichablePullRequest & ProviderActionablePullReq
136137
export type FocusItem = FocusPullRequest & {
137138
currentViewer: Account;
138139
codeSuggestionsCount: number;
139-
codeSuggestions?: Draft[];
140+
codeSuggestions?: TimedResult<Draft[]>;
140141
isNew: boolean;
141142
actionableCategory: FocusActionCategory;
142143
suggestedActions: FocusAction[];
@@ -157,8 +158,8 @@ type CachedFocusPromise<T> = {
157158
const cacheExpiration = 1000 * 60 * 30; // 30 minutes
158159

159160
type PullRequestsWithSuggestionCounts = {
160-
prs: SearchedPullRequest[] | undefined;
161-
suggestionCounts: CodeSuggestionCounts | undefined;
161+
prs: TimedResult<SearchedPullRequest[] | undefined> | undefined;
162+
suggestionCounts: TimedResult<CodeSuggestionCounts | undefined> | undefined;
162163
};
163164

164165
export type FocusRefreshEvent =
@@ -173,6 +174,17 @@ export type FocusRefreshEvent =
173174

174175
export const supportedFocusIntegrations = [HostingIntegrationId.GitHub];
175176

177+
export interface FocusItemsWithDurations {
178+
items: FocusItem[];
179+
timings?: Timings;
180+
}
181+
182+
export interface Timings {
183+
prs: number | undefined;
184+
codeSuggestionCounts: number | undefined;
185+
enrichedItems: number | undefined;
186+
}
187+
176188
export class FocusProvider implements Disposable {
177189
private readonly _onDidChange = new EventEmitter<void>();
178190
get onDidChange() {
@@ -227,7 +239,11 @@ export class FocusProvider implements Disposable {
227239
const scope = getLogScope();
228240

229241
const [prsResult, subscriptionResult] = await Promise.allSettled([
230-
this.container.integrations.getMyPullRequests([HostingIntegrationId.GitHub], cancellation),
242+
withDurationAndSlowEventOnTimeout(
243+
this.container.integrations.getMyPullRequests([HostingIntegrationId.GitHub], cancellation),
244+
'getMyPullRequests',
245+
this.container,
246+
),
231247
this.container.subscription.getSubscription(true),
232248
]);
233249

@@ -240,9 +256,13 @@ export class FocusProvider implements Disposable {
240256
const subscription = getSettledValue(subscriptionResult);
241257

242258
let suggestionCounts;
243-
if (prs?.length && subscription?.account != null) {
259+
if (prs?.value?.length && subscription?.account != null) {
244260
try {
245-
suggestionCounts = await this.container.drafts.getCodeSuggestionCounts(prs.map(pr => pr.pullRequest));
261+
suggestionCounts = await withDurationAndSlowEventOnTimeout(
262+
this.container.drafts.getCodeSuggestionCounts(prs.value.map(pr => pr.pullRequest)),
263+
'getCodeSuggestionCounts',
264+
this.container,
265+
);
246266
} catch (ex) {
247267
Logger.error(ex, scope, 'Failed to get code suggestion counts');
248268
}
@@ -251,28 +271,32 @@ export class FocusProvider implements Disposable {
251271
return { prs: prs, suggestionCounts: suggestionCounts };
252272
}
253273

254-
private _enrichedItems: CachedFocusPromise<EnrichedItem[]> | undefined;
274+
private _enrichedItems: CachedFocusPromise<TimedResult<EnrichedItem[]>> | undefined;
255275
@debug<FocusProvider['getEnrichedItems']>({ args: { 0: o => `force=${o?.force}` } })
256276
private async getEnrichedItems(options?: { cancellation?: CancellationToken; force?: boolean }) {
257277
if (options?.force || this._enrichedItems == null || this._enrichedItems.expiresAt < Date.now()) {
258278
this._enrichedItems = {
259-
promise: this.container.enrichments.get(undefined, options?.cancellation),
279+
promise: withDurationAndSlowEventOnTimeout(
280+
this.container.enrichments.get(undefined, options?.cancellation),
281+
'getEnrichedItems',
282+
this.container,
283+
),
260284
expiresAt: Date.now() + cacheExpiration,
261285
};
262286
}
263287

264288
return this._enrichedItems?.promise;
265289
}
266290

267-
private _codeSuggestions: Map<string, CachedFocusPromise<Draft[]>> | undefined;
291+
private _codeSuggestions: Map<string, CachedFocusPromise<TimedResult<Draft[]>>> | undefined;
268292
@debug<FocusProvider['getCodeSuggestions']>({
269293
args: { 0: i => `${i.id} (${i.provider.name} ${i.type})`, 1: o => `force=${o?.force}` },
270294
})
271295
private async getCodeSuggestions(item: FocusItem, options?: { force?: boolean }) {
272296
if (item.codeSuggestionsCount < 1) return undefined;
273297

274298
if (this._codeSuggestions == null || options?.force) {
275-
this._codeSuggestions = new Map<string, CachedFocusPromise<Draft[]>>();
299+
this._codeSuggestions = new Map<string, CachedFocusPromise<TimedResult<Draft[]>>>();
276300
}
277301

278302
if (
@@ -281,9 +305,13 @@ export class FocusProvider implements Disposable {
281305
this._codeSuggestions.get(item.uuid)!.expiresAt < Date.now()
282306
) {
283307
this._codeSuggestions.set(item.uuid, {
284-
promise: this.container.drafts.getCodeSuggestions(item, HostingIntegrationId.GitHub, {
285-
includeArchived: false,
286-
}),
308+
promise: withDurationAndSlowEventOnTimeout(
309+
this.container.drafts.getCodeSuggestions(item, HostingIntegrationId.GitHub, {
310+
includeArchived: false,
311+
}),
312+
'getCodeSuggestions',
313+
this.container,
314+
),
287315
expiresAt: Date.now() + cacheExpiration,
288316
});
289317
}
@@ -372,7 +400,7 @@ export class FocusProvider implements Disposable {
372400

373401
@log<FocusProvider['openCodeSuggestion']>({ args: { 0: i => `${i.id} (${i.provider.name} ${i.type})` } })
374402
openCodeSuggestion(item: FocusItem, target: string) {
375-
const draft = item.codeSuggestions?.find(d => d.id === target);
403+
const draft = item.codeSuggestions?.value?.find(d => d.id === target);
376404
if (draft == null) return;
377405
this._codeSuggestions?.delete(item.uuid);
378406
this._prs = undefined;
@@ -516,7 +544,10 @@ export class FocusProvider implements Disposable {
516544
}
517545

518546
@log<FocusProvider['getCategorizedItems']>({ args: { 0: o => `force=${o?.force}`, 1: false } })
519-
async getCategorizedItems(options?: { force?: boolean }, cancellation?: CancellationToken): Promise<FocusItem[]> {
547+
async getCategorizedItems(
548+
options?: { force?: boolean },
549+
cancellation?: CancellationToken,
550+
): Promise<FocusItemsWithDurations> {
520551
const scope = getLogScope();
521552

522553
const ignoredRepositories = new Set(
@@ -560,14 +591,14 @@ export class FocusProvider implements Disposable {
560591
throw failedError;
561592
}
562593

594+
const enrichedItems = getSettledValue(enrichedItemsResult);
563595
const prsWithSuggestionCounts = getSettledValue(prsWithCountsResult);
564596
if (prsWithSuggestionCounts != null) {
565597
// Multiple enriched items can have the same entityId. Map by entityId to an array of enriched items.
566598
const enrichedItemsByEntityId: { [id: string]: EnrichedItem[] } = {};
567599

568-
const enrichedItems = getSettledValue(enrichedItemsResult);
569-
if (enrichedItems != null) {
570-
for (const enrichedItem of enrichedItems) {
600+
if (enrichedItems?.value != null) {
601+
for (const enrichedItem of enrichedItems.value) {
571602
if (enrichedItem.entityId in enrichedItemsByEntityId) {
572603
enrichedItemsByEntityId[enrichedItem.entityId].push(enrichedItem);
573604
} else {
@@ -577,11 +608,19 @@ export class FocusProvider implements Disposable {
577608
}
578609

579610
const { prs, suggestionCounts } = prsWithSuggestionCounts;
580-
if (prs == null) return categorized;
611+
if (prs?.value == null)
612+
return {
613+
items: categorized,
614+
timings: {
615+
prs: prs?.duration,
616+
codeSuggestionCounts: suggestionCounts?.duration,
617+
enrichedItems: enrichedItems?.duration,
618+
},
619+
};
581620

582621
const filteredPrs = !ignoredRepositories.size
583-
? prs
584-
: prs.filter(
622+
? prs.value
623+
: prs.value.filter(
585624
pr =>
586625
!ignoredRepositories.has(
587626
`${pr.pullRequest.repository.owner.toLowerCase()}/${pr.pullRequest.repository.repo.toLowerCase()}`,
@@ -640,7 +679,7 @@ export class FocusProvider implements Disposable {
640679
// Map from shared category label to local actionable category, and get suggested actions
641680
categorized = (await Promise.all(
642681
actionableItems.map(async item => {
643-
const codeSuggestionsCount = suggestionCounts?.[item.uuid]?.count ?? 0;
682+
const codeSuggestionsCount = suggestionCounts?.value?.[item.uuid]?.count ?? 0;
644683
let actionableCategory = sharedCategoryToFocusActionCategoryMap.get(
645684
item.suggestedActionCategory,
646685
)!;
@@ -669,7 +708,14 @@ export class FocusProvider implements Disposable {
669708
)) satisfies FocusItem[];
670709
}
671710

672-
return categorized;
711+
return {
712+
items: categorized,
713+
timings: {
714+
prs: prsWithSuggestionCounts?.prs?.duration,
715+
codeSuggestionCounts: prsWithSuggestionCounts?.suggestionCounts?.duration,
716+
enrichedItems: enrichedItems?.duration,
717+
},
718+
};
673719
} finally {
674720
this.updateGroupedIds(categorized);
675721
this._onDidRefresh.fire(failedError ? { error: failedError } : { items: categorized });
@@ -712,7 +758,10 @@ export class FocusProvider implements Disposable {
712758
@log<FocusProvider['ensureFocusItemCodeSuggestions']>({
713759
args: { 0: i => `${i.id} (${i.provider.name} ${i.type})`, 1: o => `force=${o?.force}` },
714760
})
715-
async ensureFocusItemCodeSuggestions(item: FocusItem, options?: { force?: boolean }): Promise<Draft[] | undefined> {
761+
async ensureFocusItemCodeSuggestions(
762+
item: FocusItem,
763+
options?: { force?: boolean },
764+
): Promise<TimedResult<Draft[]> | undefined> {
716765
item.codeSuggestions ??= await this.getCodeSuggestions(item, options);
717766
return item.codeSuggestions;
718767
}
@@ -836,3 +885,22 @@ function ensureRemoteUrl(url: string) {
836885
export function getFocusItemIdHash(item: FocusItem) {
837886
return md5(item.uuid);
838887
}
888+
889+
const slowEventTimeout = 1000 * 30; // 30 seconds
890+
891+
function withDurationAndSlowEventOnTimeout<T>(
892+
promise: Promise<T>,
893+
name: 'getMyPullRequests' | 'getCodeSuggestionCounts' | 'getCodeSuggestions' | 'getEnrichedItems',
894+
container: Container,
895+
): Promise<TimedResult<T>> {
896+
return timedWithSlowThreshold(promise, {
897+
timeout: slowEventTimeout,
898+
onSlow: (duration: number) => {
899+
container.telemetry.sendEvent('launchpad/operation/slow', {
900+
timeout: slowEventTimeout,
901+
operation: name,
902+
duration: duration,
903+
});
904+
},
905+
});
906+
}

0 commit comments

Comments
 (0)