feat: enhance team data fetching with performance logging and timeout handling
- Added performance logging to the handleGetData function, tracking the duration of team data retrieval and logging warnings for slow responses. - Implemented a timeout mechanism in the selectTeam function to prevent long-running fetch operations, improving user experience. - Enhanced the getTeamData method with detailed timing metrics for each data loading step, allowing for better performance analysis and debugging.
This commit is contained in:
parent
0f302521e4
commit
9ce6d37528
3 changed files with 104 additions and 16 deletions
|
|
@ -321,6 +321,8 @@ async function handleGetData(
|
|||
return { success: false, error: validated.error ?? 'Invalid teamName' };
|
||||
}
|
||||
const tn = validated.value!;
|
||||
const startedAt = Date.now();
|
||||
logger.info(`[teams:getData] start team=${tn}`);
|
||||
let data: TeamData;
|
||||
try {
|
||||
data = await getTeamDataService().getTeamData(tn);
|
||||
|
|
@ -335,6 +337,14 @@ async function handleGetData(
|
|||
logger.error(`[teams:getData] ${message}`);
|
||||
return { success: false, error: message };
|
||||
}
|
||||
const getDataMs = Date.now() - startedAt;
|
||||
if (getDataMs >= 1000) {
|
||||
logger.warn(
|
||||
`[teams:getData] slow team=${tn} ms=${getDataMs} tasks=${data.tasks.length} members=${data.members.length} messages=${data.messages.length}`
|
||||
);
|
||||
} else {
|
||||
logger.info(`[teams:getData] done team=${tn} ms=${getDataMs}`);
|
||||
}
|
||||
const provisioning = getTeamProvisioningService();
|
||||
const isAlive = provisioning.isTeamAlive(tn);
|
||||
|
||||
|
|
|
|||
|
|
@ -169,10 +169,21 @@ export class TeamDataService {
|
|||
}
|
||||
|
||||
async getTeamData(teamName: string): Promise<TeamData> {
|
||||
const startedAt = Date.now();
|
||||
const marks: Record<string, number> = {};
|
||||
const mark = (label: string): void => {
|
||||
marks[label] = Date.now();
|
||||
};
|
||||
const msSince = (label: string): number => {
|
||||
const t = marks[label];
|
||||
return typeof t === 'number' ? t - startedAt : -1;
|
||||
};
|
||||
|
||||
const config = await this.configReader.getConfig(teamName);
|
||||
if (!config) {
|
||||
throw new Error(`Team not found: ${teamName}`);
|
||||
}
|
||||
mark('config');
|
||||
|
||||
const warnings: string[] = [];
|
||||
|
||||
|
|
@ -184,6 +195,7 @@ export class TeamDataService {
|
|||
warnings.push('Tasks failed to load');
|
||||
tasksLoaded = false;
|
||||
}
|
||||
mark('tasks');
|
||||
|
||||
let inboxNames: string[] = [];
|
||||
try {
|
||||
|
|
@ -191,6 +203,7 @@ export class TeamDataService {
|
|||
} catch {
|
||||
warnings.push('Inboxes failed to load');
|
||||
}
|
||||
mark('inboxNames');
|
||||
|
||||
let messages: InboxMessage[] = [];
|
||||
try {
|
||||
|
|
@ -198,6 +211,7 @@ export class TeamDataService {
|
|||
} catch {
|
||||
warnings.push('Messages failed to load');
|
||||
}
|
||||
mark('messages');
|
||||
|
||||
try {
|
||||
const leadTexts = await this.extractLeadSessionTexts(config);
|
||||
|
|
@ -207,6 +221,7 @@ export class TeamDataService {
|
|||
} catch {
|
||||
warnings.push('Lead session texts failed to load');
|
||||
}
|
||||
mark('leadTexts');
|
||||
|
||||
try {
|
||||
const sentMessages = await this.sentMessagesStore.readMessages(teamName);
|
||||
|
|
@ -216,6 +231,7 @@ export class TeamDataService {
|
|||
} catch {
|
||||
warnings.push('Sent messages failed to load');
|
||||
}
|
||||
mark('sentMessages');
|
||||
|
||||
messages.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
||||
|
||||
|
|
@ -225,6 +241,7 @@ export class TeamDataService {
|
|||
} catch {
|
||||
warnings.push('Member metadata failed to load');
|
||||
}
|
||||
mark('metaMembers');
|
||||
|
||||
let kanbanState: KanbanState = {
|
||||
teamName,
|
||||
|
|
@ -238,6 +255,7 @@ export class TeamDataService {
|
|||
warnings.push('Kanban state failed to load');
|
||||
canRunKanbanGc = false;
|
||||
}
|
||||
mark('kanbanState');
|
||||
|
||||
if (canRunKanbanGc && tasksLoaded) {
|
||||
try {
|
||||
|
|
@ -247,6 +265,7 @@ export class TeamDataService {
|
|||
warnings.push('Kanban state cleanup failed');
|
||||
}
|
||||
}
|
||||
mark('kanbanGc');
|
||||
|
||||
const tasksWithKanban: TeamTaskWithKanban[] = tasks.map((task) => {
|
||||
const col = kanbanState.tasks[task.id]?.column;
|
||||
|
|
@ -261,9 +280,11 @@ export class TeamDataService {
|
|||
tasksWithKanban,
|
||||
messages
|
||||
);
|
||||
mark('resolveMembers');
|
||||
|
||||
// Enrich members with git branch when it differs from lead's branch
|
||||
await this.enrichMemberBranches(members, config);
|
||||
mark('enrichBranches');
|
||||
|
||||
// Auto-sync: create comments from task-related inbox messages
|
||||
if (tasksLoaded && messages.length > 0) {
|
||||
|
|
@ -277,6 +298,7 @@ export class TeamDataService {
|
|||
warnings.push('Comment sync from messages failed');
|
||||
}
|
||||
}
|
||||
mark('syncComments');
|
||||
|
||||
const tasksToReturn: TeamTaskWithKanban[] = tasks.map((task) => {
|
||||
const col = kanbanState.tasks[task.id]?.column;
|
||||
|
|
@ -290,6 +312,22 @@ export class TeamDataService {
|
|||
} catch {
|
||||
warnings.push('Processes failed to load');
|
||||
}
|
||||
mark('processes');
|
||||
|
||||
const totalMs = Date.now() - startedAt;
|
||||
if (totalMs >= 1500) {
|
||||
logger.warn(
|
||||
`[getTeamData] slow team=${teamName} total=${totalMs}ms config=${msSince('config')} tasks=${msSince('tasks')} inboxNames=${msSince(
|
||||
'inboxNames'
|
||||
)} messages=${msSince('messages')} leadTexts=${msSince('leadTexts')} sent=${msSince(
|
||||
'sentMessages'
|
||||
)} membersMeta=${msSince('metaMembers')} kanban=${msSince('kanbanState')} kanbanGc=${msSince(
|
||||
'kanbanGc'
|
||||
)} resolveMembers=${msSince('resolveMembers')} enrichBranches=${msSince(
|
||||
'enrichBranches'
|
||||
)} syncComments=${msSince('syncComments')} processes=${msSince('processes')}`
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-track teams with alive processes for periodic health checks
|
||||
const hasAlive = processes.some((p) => !p.stoppedAt);
|
||||
|
|
@ -484,20 +522,27 @@ export class TeamDataService {
|
|||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
members.map(async (member) => {
|
||||
if (!member.cwd || member.cwd === leadCwd) return;
|
||||
try {
|
||||
const branch = await gitIdentityResolver.getBranch(member.cwd);
|
||||
if (branch && branch !== leadBranch) {
|
||||
// eslint-disable-next-line no-param-reassign -- intentional in-place enrichment
|
||||
member.gitBranch = branch;
|
||||
const candidates = members.filter((m) => m.cwd && m.cwd !== leadCwd);
|
||||
if (candidates.length === 0) return;
|
||||
|
||||
const concurrency = process.platform === 'win32' ? 4 : 8;
|
||||
for (let i = 0; i < candidates.length; i += concurrency) {
|
||||
const batch = candidates.slice(i, i + concurrency);
|
||||
await Promise.all(
|
||||
batch.map(async (member) => {
|
||||
if (!member.cwd) return;
|
||||
try {
|
||||
const branch = await gitIdentityResolver.getBranch(member.cwd);
|
||||
if (branch && branch !== leadBranch) {
|
||||
// eslint-disable-next-line no-param-reassign -- intentional in-place enrichment
|
||||
member.gitBranch = branch;
|
||||
}
|
||||
} catch {
|
||||
// Member cwd may not be a git repo — skip silently
|
||||
}
|
||||
} catch {
|
||||
// Member cwd may not be a git repo — skip silently
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async addMember(teamName: string, request: AddMemberRequest): Promise<void> {
|
||||
|
|
@ -909,6 +954,11 @@ export class TeamDataService {
|
|||
const TASK_ID_PATTERN = /#(\d+)/g;
|
||||
let synced = false;
|
||||
|
||||
const tasksById = new Map<string, TeamTask>();
|
||||
for (const t of tasks) {
|
||||
tasksById.set(t.id, t);
|
||||
}
|
||||
|
||||
// Dedup broadcasts: same sender + same text → process only once
|
||||
const processedTexts = new Set<string>();
|
||||
|
||||
|
|
@ -927,7 +977,7 @@ export class TeamDataService {
|
|||
}
|
||||
|
||||
for (const taskId of taskIds) {
|
||||
const task = tasks.find((t) => t.id === taskId);
|
||||
const task = tasksById.get(taskId);
|
||||
if (!task) continue;
|
||||
|
||||
const commentId = `msg-${msg.messageId}`;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,20 @@ import { getWorktreeNavigationState } from '../utils/stateResetHelpers';
|
|||
|
||||
const logger = createLogger('teamSlice');
|
||||
|
||||
const TEAM_GET_DATA_TIMEOUT_MS = 30_000;
|
||||
|
||||
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeout = new Promise<T>((_resolve, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
reject(new Error(`Timeout after ${ms}ms: ${label}`));
|
||||
}, ms);
|
||||
});
|
||||
return Promise.race([promise, timeout]).finally(() => {
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
}
|
||||
|
||||
import type { AppState } from '../types';
|
||||
import type {
|
||||
AddMemberRequest,
|
||||
|
|
@ -387,8 +401,18 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
return;
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
const traceId = `${teamName}:${startedAt}`;
|
||||
logger.info(
|
||||
`[selectTeam] start trace=${traceId} skipProjectAutoSelect=${opts?.skipProjectAutoSelect === true}`
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await unwrapIpc('team:getData', () => api.teams.getData(teamName));
|
||||
const data = await withTimeout(
|
||||
unwrapIpc('team:getData', () => api.teams.getData(teamName)),
|
||||
TEAM_GET_DATA_TIMEOUT_MS,
|
||||
`team:getData(${teamName}) trace=${traceId}`
|
||||
);
|
||||
// Stale check: user may have switched to another team during the async call
|
||||
if (get().selectedTeamName !== teamName) {
|
||||
return;
|
||||
|
|
@ -400,6 +424,10 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
selectedTeamError: null,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`[selectTeam] done trace=${traceId} ms=${Date.now() - startedAt} tasks=${data.tasks.length} members=${data.members.length} messages=${data.messages.length}`
|
||||
);
|
||||
|
||||
// Sync tab label with the team's display name from config
|
||||
const displayName = data.config.name || teamName;
|
||||
const allTabs = get().getAllPaneTabs();
|
||||
|
|
@ -466,7 +494,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
: error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to fetch team data';
|
||||
logger.error(`[team:getData] ${message}`);
|
||||
logger.error(`[selectTeam] fail team=${teamName} ms=${Date.now() - startedAt} ${message}`);
|
||||
set({
|
||||
selectedTeamLoading: false,
|
||||
selectedTeamData: null,
|
||||
|
|
|
|||
Loading…
Reference in a new issue