Files
noteflow/client/e2e/performance-profile.spec.ts

708 lines
23 KiB
TypeScript

/**
* Comprehensive Performance Profiling Suite
*
* Profiles all major user flows:
* - Recording flow
* - Diarization flow
* - Transcription flow
* - Summarization flow
* - Entity extraction flow
* - Playback flow
* - Analytics computation
* - UI access paths and navigation
*/
import { expect, test, type Page } from '@playwright/test';
import { callAPI, navigateTo, TEST_DATA, waitForAPI, waitForLoadingComplete } from './fixtures';
const shouldRun = process.env.NOTEFLOW_E2E === '1';
// Performance thresholds (milliseconds)
const THRESHOLDS = {
PAGE_LOAD: 3000,
API_CALL: 2000,
NAVIGATION: 1500,
LIST_RENDER: 1000,
HEAVY_COMPUTATION: 10000,
REAL_TIME_UPDATE: 500,
};
interface PerformanceMetric {
name: string;
duration: number;
threshold: number;
passed: boolean;
details?: string;
}
const metrics: PerformanceMetric[] = [];
function recordMetric(name: string, duration: number, threshold: number, details?: string) {
const passed = duration <= threshold;
metrics.push({ name, duration, threshold, passed, details });
console.log(
`[PERF] ${passed ? '✓' : '✗'} ${name}: ${duration.toFixed(0)}ms (threshold: ${threshold}ms)${details ? ` - ${details}` : ''}`
);
return passed;
}
async function measureAsync<T>(fn: () => Promise<T>): Promise<{ result: T; duration: number }> {
const start = performance.now();
const result = await fn();
const duration = performance.now() - start;
return { result, duration };
}
// =============================================================================
// 1. PAGE LOAD PERFORMANCE
// =============================================================================
test.describe('Page Load Performance', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
const pages = [
{ path: '/', name: 'Home' },
{ path: '/meetings', name: 'Meetings List' },
{ path: '/tasks', name: 'Tasks' },
{ path: '/people', name: 'People' },
{ path: '/analytics', name: 'Analytics' },
{ path: '/settings', name: 'Settings' },
];
for (const { path, name } of pages) {
test(`${name} page load time`, async ({ page }) => {
const { duration } = await measureAsync(async () => {
await page.goto(path);
await waitForLoadingComplete(page);
});
const passed = recordMetric(`Page Load: ${name}`, duration, THRESHOLDS.PAGE_LOAD);
expect(passed).toBe(true);
});
}
});
// =============================================================================
// 2. NAVIGATION PERFORMANCE
// =============================================================================
test.describe('Navigation Performance', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test('sidebar navigation transitions', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const navPaths = [
{ selector: 'a[href="/meetings"]', name: 'Meetings' },
{ selector: 'a[href="/tasks"]', name: 'Tasks' },
{ selector: 'a[href="/people"]', name: 'People' },
{ selector: 'a[href="/analytics"]', name: 'Analytics' },
{ selector: 'a[href="/settings"]', name: 'Settings' },
{ selector: 'a[href="/"]', name: 'Home' },
];
for (const { selector, name } of navPaths) {
const link = page.locator(selector).first();
if (await link.isVisible()) {
const { duration } = await measureAsync(async () => {
await link.click();
await waitForLoadingComplete(page);
});
recordMetric(`Navigation: ${name}`, duration, THRESHOLDS.NAVIGATION);
}
}
});
});
// =============================================================================
// 3. API PERFORMANCE
// =============================================================================
test.describe('API Performance', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test.beforeEach(async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
});
test('listMeetings performance', async ({ page }) => {
const { duration } = await measureAsync(async () => {
await callAPI(page, 'listMeetings', { limit: 50 });
});
const passed = recordMetric('API: listMeetings (50)', duration, THRESHOLDS.API_CALL);
expect(passed).toBe(true);
});
test('listMeetings with pagination performance', async ({ page }) => {
const { duration } = await measureAsync(async () => {
await callAPI(page, 'listMeetings', { limit: 100, offset: 0 });
});
recordMetric('API: listMeetings (100)', duration, THRESHOLDS.API_CALL);
});
test('listSpeakerStats performance', async ({ page }) => {
const { duration } = await measureAsync(async () => {
await callAPI(page, 'listSpeakerStats', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
});
});
const passed = recordMetric('API: listSpeakerStats', duration, THRESHOLDS.API_CALL);
expect(passed).toBe(true);
});
test('getAnalyticsOverview performance', async ({ page }) => {
const { duration } = await measureAsync(async () => {
await callAPI(page, 'getAnalyticsOverview', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
});
});
recordMetric('API: getAnalyticsOverview', duration, THRESHOLDS.API_CALL);
});
test('getEntityAnalytics performance', async ({ page }) => {
const { duration } = await measureAsync(async () => {
await callAPI(page, 'getEntityAnalytics', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
});
});
recordMetric('API: getEntityAnalytics', duration, THRESHOLDS.API_CALL);
});
test('listTasks performance', async ({ page }) => {
const { duration } = await measureAsync(async () => {
await callAPI(page, 'listTasks', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
limit: 50,
});
});
recordMetric('API: listTasks', duration, THRESHOLDS.API_CALL);
});
test('listProjects performance', async ({ page }) => {
const { duration } = await measureAsync(async () => {
await callAPI(page, 'listProjects', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
});
});
recordMetric('API: listProjects', duration, THRESHOLDS.API_CALL);
});
test('getServerInfo performance', async ({ page }) => {
const { duration } = await measureAsync(async () => {
await callAPI(page, 'getServerInfo', {});
});
recordMetric('API: getServerInfo', duration, THRESHOLDS.API_CALL);
});
test('listWebhooks performance', async ({ page }) => {
const { duration } = await measureAsync(async () => {
await callAPI(page, 'listWebhooks', { include_test: false });
});
recordMetric('API: listWebhooks', duration, THRESHOLDS.API_CALL);
});
test('getPreferences performance', async ({ page }) => {
const { duration } = await measureAsync(async () => {
await callAPI(page, 'getPreferences', {});
});
recordMetric('API: getPreferences', duration, THRESHOLDS.API_CALL);
});
});
// =============================================================================
// 4. MEETING DETAIL PERFORMANCE
// =============================================================================
test.describe('Meeting Detail Performance', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test('meeting detail with segments load', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
// Get first meeting
const meetings = await callAPI<{ meetings: { id: string }[] }>(page, 'listMeetings', {
limit: 1,
});
if (meetings.meetings.length === 0) {
test.skip(true, 'No meetings available');
return;
}
const meetingId = meetings.meetings[0].id;
const { duration } = await measureAsync(async () => {
await callAPI(page, 'getMeeting', {
meeting_id: meetingId,
include_segments: true,
include_summary: true,
});
});
recordMetric('API: getMeeting (with segments)', duration, THRESHOLDS.API_CALL);
});
test('meeting detail page render', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const meetings = await callAPI<{ meetings: { id: string }[] }>(page, 'listMeetings', {
limit: 1,
});
if (meetings.meetings.length === 0) {
test.skip(true, 'No meetings available');
return;
}
const meetingId = meetings.meetings[0].id;
const { duration } = await measureAsync(async () => {
await page.goto(`/meetings/${meetingId}`);
await waitForLoadingComplete(page);
});
recordMetric('Page Load: Meeting Detail', duration, THRESHOLDS.PAGE_LOAD);
});
});
// =============================================================================
// 5. POST-PROCESSING PERFORMANCE (Diarization, Summary, Entities)
// =============================================================================
test.describe('Post-Processing Performance', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test.beforeEach(async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
});
test('getDiarizationJobStatus performance', async ({ page }) => {
const meetings = await callAPI<{ meetings: { id: string }[] }>(page, 'listMeetings', {
limit: 1,
});
if (meetings.meetings.length === 0) {
test.skip(true, 'No meetings available');
return;
}
const meetingId = meetings.meetings[0].id;
const { duration } = await measureAsync(async () => {
await callAPI(page, 'getDiarizationJobStatus', { meeting_id: meetingId });
});
recordMetric('API: getDiarizationJobStatus', duration, THRESHOLDS.API_CALL);
});
test('refineSpeakers performance (if available)', async ({ page }) => {
const meetings = await callAPI<{ meetings: { id: string; state: string }[] }>(
page,
'listMeetings',
{ limit: 10, states: ['completed'] }
);
const completedMeeting = meetings.meetings.find((m) => m.state === 'completed');
if (!completedMeeting) {
test.skip(true, 'No completed meetings available');
return;
}
const { duration } = await measureAsync(async () => {
try {
await callAPI(page, 'refineSpeakers', { meeting_id: completedMeeting.id });
} catch {
// May fail if already refined or no audio
}
});
recordMetric('API: refineSpeakers', duration, THRESHOLDS.HEAVY_COMPUTATION);
});
test('generateSummary performance (if available)', async ({ page }) => {
const meetings = await callAPI<{ meetings: { id: string; state: string }[] }>(
page,
'listMeetings',
{ limit: 10, states: ['completed'] }
);
const completedMeeting = meetings.meetings.find((m) => m.state === 'completed');
if (!completedMeeting) {
test.skip(true, 'No completed meetings available');
return;
}
const { duration } = await measureAsync(async () => {
try {
await callAPI(page, 'generateSummary', { meeting_id: completedMeeting.id });
} catch {
// May fail if already generated
}
});
recordMetric('API: generateSummary', duration, THRESHOLDS.HEAVY_COMPUTATION);
});
test('extractEntities performance (if available)', async ({ page }) => {
const meetings = await callAPI<{ meetings: { id: string; state: string }[] }>(
page,
'listMeetings',
{ limit: 10, states: ['completed'] }
);
const completedMeeting = meetings.meetings.find((m) => m.state === 'completed');
if (!completedMeeting) {
test.skip(true, 'No completed meetings available');
return;
}
const { duration } = await measureAsync(async () => {
try {
await callAPI(page, 'extractEntities', { meeting_id: completedMeeting.id });
} catch {
// May fail if already extracted
}
});
recordMetric('API: extractEntities', duration, THRESHOLDS.HEAVY_COMPUTATION);
});
});
// =============================================================================
// 6. PLAYBACK PERFORMANCE
// =============================================================================
test.describe('Playback Performance', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test('startPlayback performance', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const meetings = await callAPI<{ meetings: { id: string; state: string }[] }>(
page,
'listMeetings',
{ limit: 10, states: ['completed'] }
);
const completedMeeting = meetings.meetings.find((m) => m.state === 'completed');
if (!completedMeeting) {
test.skip(true, 'No completed meetings available');
return;
}
const { duration } = await measureAsync(async () => {
try {
await callAPI(page, 'startPlayback', {
meeting_id: completedMeeting.id,
position: 0,
});
} catch {
// May fail if no audio
}
});
recordMetric('API: startPlayback', duration, THRESHOLDS.API_CALL);
});
test('seekPlayback performance', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const meetings = await callAPI<{ meetings: { id: string; state: string }[] }>(
page,
'listMeetings',
{ limit: 10, states: ['completed'] }
);
const completedMeeting = meetings.meetings.find((m) => m.state === 'completed');
if (!completedMeeting) {
test.skip(true, 'No completed meetings available');
return;
}
// Start playback first
try {
await callAPI(page, 'startPlayback', {
meeting_id: completedMeeting.id,
position: 0,
});
} catch {
test.skip(true, 'Cannot start playback');
return;
}
const { duration } = await measureAsync(async () => {
await callAPI(page, 'seekPlayback', { position: 5000 });
});
recordMetric('API: seekPlayback', duration, THRESHOLDS.REAL_TIME_UPDATE);
});
});
// =============================================================================
// 7. ASSISTANT PERFORMANCE
// =============================================================================
test.describe('Assistant Performance', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test('askAssistant performance', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const meetings = await callAPI<{ meetings: { id: string }[] }>(page, 'listMeetings', {
limit: 1,
});
if (meetings.meetings.length === 0) {
test.skip(true, 'No meetings available');
return;
}
const meetingId = meetings.meetings[0].id;
const { duration } = await measureAsync(async () => {
try {
await callAPI(page, 'askAssistant', {
question: 'What was discussed?',
meeting_id: meetingId,
});
} catch {
// May timeout or fail
}
});
// Assistant has longer timeout - 20s
recordMetric('API: askAssistant', duration, 20000);
});
});
// =============================================================================
// 8. ANNOTATIONS PERFORMANCE
// =============================================================================
test.describe('Annotations Performance', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test('listAnnotations performance', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const meetings = await callAPI<{ meetings: { id: string }[] }>(page, 'listMeetings', {
limit: 1,
});
if (meetings.meetings.length === 0) {
test.skip(true, 'No meetings available');
return;
}
const meetingId = meetings.meetings[0].id;
const { duration } = await measureAsync(async () => {
await callAPI(page, 'listAnnotations', { meeting_id: meetingId });
});
recordMetric('API: listAnnotations', duration, THRESHOLDS.API_CALL);
});
});
// =============================================================================
// 9. EXPORT PERFORMANCE
// =============================================================================
test.describe('Export Performance', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
const formats = ['markdown', 'text', 'json'];
for (const format of formats) {
test(`exportTranscript (${format}) performance`, async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const meetings = await callAPI<{ meetings: { id: string; state: string }[] }>(
page,
'listMeetings',
{ limit: 10, states: ['completed'] }
);
const completedMeeting = meetings.meetings.find((m) => m.state === 'completed');
if (!completedMeeting) {
test.skip(true, 'No completed meetings available');
return;
}
const { duration } = await measureAsync(async () => {
await callAPI(page, 'exportTranscript', {
meeting_id: completedMeeting.id,
format,
});
});
recordMetric(`API: exportTranscript (${format})`, duration, THRESHOLDS.API_CALL);
});
}
});
// =============================================================================
// 10. SETTINGS PERFORMANCE
// =============================================================================
test.describe('Settings Performance', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test('settings tabs load performance', async ({ page }) => {
await navigateTo(page, '/settings');
await waitForLoadingComplete(page);
const tabs = ['status', 'audio', 'ai', 'integrations'];
for (const tab of tabs) {
const tabButton = page.locator(`[data-testid="settings-tab-${tab}"], button:has-text("${tab}")`).first();
if (await tabButton.isVisible()) {
const { duration } = await measureAsync(async () => {
await tabButton.click();
await page.waitForTimeout(100); // Allow tab content to render
});
recordMetric(`Settings Tab: ${tab}`, duration, THRESHOLDS.NAVIGATION);
}
}
});
test('listAudioDevices performance', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const { duration } = await measureAsync(async () => {
await callAPI(page, 'listAudioDevices', {});
});
recordMetric('API: listAudioDevices', duration, THRESHOLDS.API_CALL);
});
test('getCloudConsentStatus performance', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const { duration } = await measureAsync(async () => {
await callAPI(page, 'getCloudConsentStatus', {});
});
recordMetric('API: getCloudConsentStatus', duration, THRESHOLDS.API_CALL);
});
test('getTriggerStatus performance', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const { duration } = await measureAsync(async () => {
await callAPI(page, 'getTriggerStatus', {});
});
recordMetric('API: getTriggerStatus', duration, THRESHOLDS.API_CALL);
});
});
// =============================================================================
// 11. BULK OPERATIONS PERFORMANCE
// =============================================================================
test.describe('Bulk Operations Performance', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test('concurrent API calls performance', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const { duration } = await measureAsync(async () => {
await Promise.all([
callAPI(page, 'listMeetings', { limit: 20 }),
callAPI(page, 'listSpeakerStats', { workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID }),
callAPI(page, 'listTasks', { workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID, limit: 20 }),
callAPI(page, 'getServerInfo', {}),
]);
});
recordMetric('Concurrent: 4 API calls', duration, THRESHOLDS.API_CALL * 2);
});
test('sequential page navigation performance', async ({ page }) => {
const paths = ['/', '/meetings', '/tasks', '/people', '/analytics', '/settings'];
const { duration } = await measureAsync(async () => {
for (const path of paths) {
await page.goto(path);
await waitForLoadingComplete(page);
}
});
recordMetric(`Sequential: ${paths.length} page navigations`, duration, THRESHOLDS.PAGE_LOAD * paths.length);
});
});
// =============================================================================
// 12. MEMORY AND RESOURCE USAGE
// =============================================================================
test.describe('Memory and Resource Usage', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test('memory usage after heavy navigation', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
// Perform heavy navigation
for (let i = 0; i < 5; i++) {
await page.goto('/meetings');
await waitForLoadingComplete(page);
await page.goto('/analytics');
await waitForLoadingComplete(page);
await page.goto('/people');
await waitForLoadingComplete(page);
}
// Check for memory leaks via performance API
const metrics = await page.evaluate(() => {
const perf = performance as Performance & {
memory?: { usedJSHeapSize: number; totalJSHeapSize: number };
};
if (perf.memory) {
return {
usedJSHeapSize: perf.memory.usedJSHeapSize,
totalJSHeapSize: perf.memory.totalJSHeapSize,
};
}
return null;
});
if (metrics) {
const usedMB = metrics.usedJSHeapSize / (1024 * 1024);
const totalMB = metrics.totalJSHeapSize / (1024 * 1024);
console.log(`[PERF] Memory: ${usedMB.toFixed(1)}MB used / ${totalMB.toFixed(1)}MB total`);
// Flag if memory usage exceeds 200MB
expect(usedMB).toBeLessThan(200);
}
});
});
// =============================================================================
// FINAL SUMMARY
// =============================================================================
test.afterAll(() => {
console.log('\n========================================');
console.log('PERFORMANCE PROFILING SUMMARY');
console.log('========================================\n');
const passed = metrics.filter((m) => m.passed);
const failed = metrics.filter((m) => !m.passed);
console.log(`Total metrics: ${metrics.length}`);
console.log(`Passed: ${passed.length}`);
console.log(`Failed: ${failed.length}`);
if (failed.length > 0) {
console.log('\n--- FAILED METRICS ---');
for (const m of failed) {
console.log(`${m.name}: ${m.duration.toFixed(0)}ms (threshold: ${m.threshold}ms)`);
}
}
console.log('\n--- ALL METRICS ---');
for (const m of metrics) {
const status = m.passed ? '✓' : '✗';
console.log(` ${status} ${m.name}: ${m.duration.toFixed(0)}ms (threshold: ${m.threshold}ms)`);
}
});