Compare commits

...

2 Commits

Author SHA1 Message Date
42c8fba642 Merge branch 'master' of https://git.baked.rocks/vasceannie/noteflow
Some checks failed
CI / test-python (push) Failing after 17m27s
CI / test-typescript (push) Failing after 6m9s
CI / test-rust (push) Failing after 7m2s
2026-01-24 21:09:15 -05:00
6df31ec708 feat: add comprehensive performance profiling tests for frontend and backend operations, including round-trip, streaming, and bulk operations. 2026-01-24 21:09:10 -05:00
7 changed files with 3005 additions and 9 deletions

View File

@@ -0,0 +1,979 @@
/**
* Full Round-Trip Performance Profiling Suite
*
* Measures complete flow: Frontend → gRPC → Backend → Database → Response
*
* Covers:
* - Meeting CRUD operations (create, read, update, delete)
* - Recording flow simulation
* - Diarization pipeline
* - Transcription segments
* - Summarization generation
* - Entity extraction
* - Annotation CRUD
* - Task management
* - Analytics aggregation
* - Export operations
*/
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 for full round-trip (milliseconds)
const RT_THRESHOLDS = {
// Fast operations (simple DB lookup)
SIMPLE_READ: 500,
SIMPLE_WRITE: 1000,
// Medium operations (joins, aggregations)
MEDIUM_READ: 2000,
MEDIUM_WRITE: 3000,
// Heavy operations (AI processing, large data)
HEAVY_PROCESSING: 30000,
AI_OPERATION: 60000,
// Full page with multiple API calls
FULL_PAGE_LOAD: 5000,
};
interface RoundTripMetric {
operation: string;
flow: string;
duration: number;
threshold: number;
passed: boolean;
dataSize?: number;
details?: string;
}
const allMetrics: RoundTripMetric[] = [];
function recordRoundTrip(
operation: string,
flow: string,
duration: number,
threshold: number,
dataSize?: number,
details?: string
) {
const passed = duration <= threshold;
allMetrics.push({ operation, flow, duration, threshold, passed, dataSize, details });
const sizeInfo = dataSize ? ` [${dataSize} items]` : '';
const status = passed ? '✓' : '✗';
console.log(
`[RT] ${status} ${operation} (${flow}): ${duration.toFixed(0)}ms${sizeInfo} (threshold: ${threshold}ms)`
);
return passed;
}
async function measureRoundTrip<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. MEETING CRUD - Full Round Trip
// =============================================================================
test.describe('Meeting CRUD Round Trip', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
let testMeetingId: string | null = null;
test.beforeEach(async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
});
test.afterEach(async ({ page }) => {
// Cleanup test meeting if created
if (testMeetingId) {
try {
await callAPI(page, 'deleteMeeting', { meeting_id: testMeetingId });
} catch {
// Ignore cleanup errors
}
testMeetingId = null;
}
});
test('CREATE meeting - full round trip', async ({ page }) => {
const testTitle = `Perf Test Meeting ${Date.now()}`;
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ id: string; title: string }>(page, 'createMeeting', {
title: testTitle,
metadata: { test: 'performance' },
});
});
testMeetingId = result.id;
const passed = recordRoundTrip('CREATE Meeting', 'FE→gRPC→DB→Response', duration, RT_THRESHOLDS.SIMPLE_WRITE);
expect(result.title).toBe(testTitle);
expect(passed).toBe(true);
});
test('READ meeting list - full round trip', async ({ page }) => {
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ meetings: unknown[]; total_count: number }>(page, 'listMeetings', {
limit: 50,
offset: 0,
});
});
recordRoundTrip(
'READ Meeting List',
'FE→gRPC→DB(query+count)→Response',
duration,
RT_THRESHOLDS.MEDIUM_READ,
result.meetings.length
);
});
test('READ meeting list with filters - full round trip', async ({ page }) => {
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ meetings: unknown[] }>(page, 'listMeetings', {
limit: 50,
states: ['completed'],
sort_order: 'newest',
});
});
recordRoundTrip(
'READ Meeting List (filtered)',
'FE→gRPC→DB(filtered query)→Response',
duration,
RT_THRESHOLDS.MEDIUM_READ,
result.meetings.length
);
});
test('READ single meeting with segments - full round trip', 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 { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ segments?: unknown[] }>(page, 'getMeeting', {
meeting_id: meetingId,
include_segments: true,
include_summary: true,
});
});
recordRoundTrip(
'READ Meeting Detail',
'FE→gRPC→DB(meeting+segments+summary)→Response',
duration,
RT_THRESHOLDS.MEDIUM_READ,
result.segments?.length || 0
);
});
test('DELETE meeting - full round trip', async ({ page }) => {
// Create a meeting to delete
const createResult = await callAPI<{ id: string }>(page, 'createMeeting', {
title: `Delete Test ${Date.now()}`,
});
const { duration } = await measureRoundTrip(async () => {
return callAPI(page, 'deleteMeeting', { meeting_id: createResult.id });
});
recordRoundTrip('DELETE Meeting', 'FE→gRPC→DB(cascade delete)→Response', duration, RT_THRESHOLDS.MEDIUM_WRITE);
});
});
// =============================================================================
// 2. SPEAKER/DIARIZATION ROUND TRIP
// =============================================================================
test.describe('Diarization Round Trip', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test.beforeEach(async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
});
test('READ speaker stats - aggregation round trip', async ({ page }) => {
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ speakers: unknown[] }>(page, 'listSpeakerStats', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
});
});
recordRoundTrip(
'READ Speaker Stats',
'FE→gRPC→DB(aggregate speakers)→Response',
duration,
RT_THRESHOLDS.MEDIUM_READ,
result.speakers.length
);
});
test('READ speaker stats with project filter - filtered aggregation', async ({ page }) => {
const projects = await callAPI<{ projects: { id: string }[] }>(page, 'listProjects', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
});
if (projects.projects.length === 0) {
test.skip(true, 'No projects available');
return;
}
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ speakers: unknown[] }>(page, 'listSpeakerStats', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
project_id: projects.projects[0].id,
});
});
recordRoundTrip(
'READ Speaker Stats (project filtered)',
'FE→gRPC→DB(filtered aggregate)→Response',
duration,
RT_THRESHOLDS.MEDIUM_READ,
result.speakers.length
);
});
test('GET diarization job status - status check round trip', 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 { duration } = await measureRoundTrip(async () => {
return callAPI(page, 'getDiarizationJobStatus', {
meeting_id: meetings.meetings[0].id,
});
});
recordRoundTrip('READ Diarization Status', 'FE→gRPC→DB(job lookup)→Response', duration, RT_THRESHOLDS.SIMPLE_READ);
});
test('RENAME speaker - update round trip', 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 meeting = await callAPI<{ segments?: { speaker_id: string }[] }>(page, 'getMeeting', {
meeting_id: meetings.meetings[0].id,
include_segments: true,
});
const speakerId = meeting.segments?.[0]?.speaker_id;
if (!speakerId) {
test.skip(true, 'No speakers in meeting');
return;
}
const { duration } = await measureRoundTrip(async () => {
return callAPI(page, 'renameSpeaker', {
meeting_id: meetings.meetings[0].id,
speaker_id: speakerId,
new_name: `Speaker ${Date.now()}`,
});
});
recordRoundTrip('UPDATE Speaker Name', 'FE→gRPC→DB(update)→Response', duration, RT_THRESHOLDS.SIMPLE_WRITE);
});
});
// =============================================================================
// 3. ANALYTICS AGGREGATION ROUND TRIP
// =============================================================================
test.describe('Analytics Aggregation Round Trip', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test.beforeEach(async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
});
test('GET analytics overview - complex aggregation', async ({ page }) => {
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ daily?: unknown[]; total_meetings: number }>(page, 'getAnalyticsOverview', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
});
});
recordRoundTrip(
'READ Analytics Overview',
'FE→gRPC→DB(multi-table aggregate)→Response',
duration,
RT_THRESHOLDS.MEDIUM_READ,
result.daily?.length || 0,
`${result.total_meetings} total meetings`
);
});
test('GET entity analytics - NER aggregation', async ({ page }) => {
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ total_entities: number; total_mentions: number }>(page, 'getEntityAnalytics', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
});
});
recordRoundTrip(
'READ Entity Analytics',
'FE→gRPC→DB(entity aggregate)→Response',
duration,
RT_THRESHOLDS.MEDIUM_READ,
result.total_entities,
`${result.total_mentions} mentions`
);
});
});
// =============================================================================
// 4. TASK MANAGEMENT ROUND TRIP
// =============================================================================
test.describe('Task Management Round Trip', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test.beforeEach(async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
});
test('READ task list - join query round trip', async ({ page }) => {
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ tasks: unknown[]; total_count: number }>(page, 'listTasks', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
limit: 50,
});
});
recordRoundTrip(
'READ Task List',
'FE→gRPC→DB(tasks+meetings join)→Response',
duration,
RT_THRESHOLDS.MEDIUM_READ,
result.tasks.length
);
});
test('READ task list with status filter - filtered join', async ({ page }) => {
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ tasks: unknown[] }>(page, 'listTasks', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
statuses: ['open'],
limit: 50,
});
});
recordRoundTrip(
'READ Task List (filtered)',
'FE→gRPC→DB(filtered join)→Response',
duration,
RT_THRESHOLDS.MEDIUM_READ,
result.tasks.length
);
});
test('UPDATE task status - status update round trip', async ({ page }) => {
const tasks = await callAPI<{ tasks: { task: { id: string; status: string } }[] }>(page, 'listTasks', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
limit: 1,
});
if (tasks.tasks.length === 0) {
test.skip(true, 'No tasks available');
return;
}
const task = tasks.tasks[0].task;
const newStatus = task.status === 'open' ? 'in_progress' : 'open';
const { duration } = await measureRoundTrip(async () => {
return callAPI(page, 'updateTask', {
task_id: task.id,
status: newStatus,
});
});
// Revert status
await callAPI(page, 'updateTask', { task_id: task.id, status: task.status });
recordRoundTrip('UPDATE Task Status', 'FE→gRPC→DB(update)→Response', duration, RT_THRESHOLDS.SIMPLE_WRITE);
});
});
// =============================================================================
// 5. ANNOTATION CRUD ROUND TRIP
// =============================================================================
test.describe('Annotation CRUD Round Trip', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
let testAnnotationId: string | null = null;
let testMeetingId: string | null = null;
test.beforeEach(async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const meetings = await callAPI<{ meetings: { id: string }[] }>(page, 'listMeetings', { limit: 1 });
if (meetings.meetings.length > 0) {
testMeetingId = meetings.meetings[0].id;
}
});
test.afterEach(async ({ page }) => {
if (testAnnotationId && testMeetingId) {
try {
await callAPI(page, 'deleteAnnotation', { annotation_id: testAnnotationId });
} catch {
// Ignore
}
testAnnotationId = null;
}
});
test('CREATE annotation - insert round trip', async ({ page }) => {
if (!testMeetingId) {
test.skip(true, 'No meetings available');
return;
}
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ id: string }>(page, 'addAnnotation', {
meeting_id: testMeetingId,
annotation_type: 'note',
text: `Performance test annotation ${Date.now()}`,
start_time: 0,
end_time: 1000,
});
});
testAnnotationId = result.id;
recordRoundTrip('CREATE Annotation', 'FE→gRPC→DB(insert)→Response', duration, RT_THRESHOLDS.SIMPLE_WRITE);
});
test('READ annotations - list query round trip', async ({ page }) => {
if (!testMeetingId) {
test.skip(true, 'No meetings available');
return;
}
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ annotations: unknown[] }>(page, 'listAnnotations', {
meeting_id: testMeetingId,
});
});
recordRoundTrip(
'READ Annotations',
'FE→gRPC→DB(query)→Response',
duration,
RT_THRESHOLDS.SIMPLE_READ,
result.annotations.length
);
});
test('UPDATE annotation - update round trip', async ({ page }) => {
if (!testMeetingId) {
test.skip(true, 'No meetings available');
return;
}
// Create an annotation to update
const created = await callAPI<{ id: string }>(page, 'addAnnotation', {
meeting_id: testMeetingId,
annotation_type: 'note',
text: 'Original text',
start_time: 0,
end_time: 1000,
});
testAnnotationId = created.id;
const { duration } = await measureRoundTrip(async () => {
return callAPI(page, 'updateAnnotation', {
annotation_id: testAnnotationId,
text: 'Updated text',
});
});
recordRoundTrip('UPDATE Annotation', 'FE→gRPC→DB(update)→Response', duration, RT_THRESHOLDS.SIMPLE_WRITE);
});
test('DELETE annotation - delete round trip', async ({ page }) => {
if (!testMeetingId) {
test.skip(true, 'No meetings available');
return;
}
// Create an annotation to delete
const created = await callAPI<{ id: string }>(page, 'addAnnotation', {
meeting_id: testMeetingId,
annotation_type: 'note',
text: 'To be deleted',
start_time: 0,
end_time: 1000,
});
const { duration } = await measureRoundTrip(async () => {
return callAPI(page, 'deleteAnnotation', { annotation_id: created.id });
});
testAnnotationId = null; // Already deleted
recordRoundTrip('DELETE Annotation', 'FE→gRPC→DB(delete)→Response', duration, RT_THRESHOLDS.SIMPLE_WRITE);
});
});
// =============================================================================
// 6. PROJECT OPERATIONS ROUND TRIP
// =============================================================================
test.describe('Project Operations Round Trip', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test.beforeEach(async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
});
test('READ project list - list query round trip', async ({ page }) => {
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ projects: unknown[] }>(page, 'listProjects', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
});
});
recordRoundTrip(
'READ Project List',
'FE→gRPC→DB(query)→Response',
duration,
RT_THRESHOLDS.SIMPLE_READ,
result.projects.length
);
});
test('READ meetings by project - filtered query round trip', async ({ page }) => {
const projects = await callAPI<{ projects: { id: string }[] }>(page, 'listProjects', {
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
});
if (projects.projects.length === 0) {
test.skip(true, 'No projects available');
return;
}
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ meetings: unknown[] }>(page, 'listMeetings', {
project_id: projects.projects[0].id,
limit: 50,
});
});
recordRoundTrip(
'READ Meetings by Project',
'FE→gRPC→DB(filtered query)→Response',
duration,
RT_THRESHOLDS.MEDIUM_READ,
result.meetings.length
);
});
});
// =============================================================================
// 7. WEBHOOKS ROUND TRIP
// =============================================================================
test.describe('Webhooks Round Trip', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test.beforeEach(async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
});
test('READ webhook list - list query round trip', async ({ page }) => {
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', {
include_test: false,
});
});
recordRoundTrip(
'READ Webhook List',
'FE→gRPC→DB(query)→Response',
duration,
RT_THRESHOLDS.SIMPLE_READ,
result.webhooks.length
);
});
});
// =============================================================================
// 8. SETTINGS/PREFERENCES ROUND TRIP
// =============================================================================
test.describe('Settings Round Trip', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test.beforeEach(async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
});
test('READ preferences - config lookup round trip', async ({ page }) => {
const { duration } = await measureRoundTrip(async () => {
return callAPI(page, 'getPreferences', {});
});
recordRoundTrip('READ Preferences', 'FE→gRPC→Config/DB→Response', duration, RT_THRESHOLDS.SIMPLE_READ);
});
test('READ server info - system status round trip', async ({ page }) => {
const { duration } = await measureRoundTrip(async () => {
return callAPI(page, 'getServerInfo', {});
});
recordRoundTrip('READ Server Info', 'FE→gRPC→System→Response', duration, RT_THRESHOLDS.SIMPLE_READ);
});
test('READ cloud consent status - auth/config round trip', async ({ page }) => {
const { duration } = await measureRoundTrip(async () => {
return callAPI(page, 'getCloudConsentStatus', {});
});
recordRoundTrip('READ Cloud Consent', 'FE→gRPC→Config→Response', duration, RT_THRESHOLDS.SIMPLE_READ);
});
test('READ audio devices - system enumeration round trip', async ({ page }) => {
const { result, duration } = await measureRoundTrip(async () => {
return callAPI<{ input: unknown[]; output: unknown[] }>(page, 'listAudioDevices', {});
});
recordRoundTrip(
'READ Audio Devices',
'FE→Rust/Tauri→System→Response',
duration,
RT_THRESHOLDS.SIMPLE_READ,
(result.input?.length || 0) + (result.output?.length || 0)
);
});
test('READ trigger status - config lookup round trip', async ({ page }) => {
const { duration } = await measureRoundTrip(async () => {
return callAPI(page, 'getTriggerStatus', {});
});
recordRoundTrip('READ Trigger Status', 'FE→Rust/Tauri→Config→Response', duration, RT_THRESHOLDS.SIMPLE_READ);
});
});
// =============================================================================
// 9. AI ASSISTANT ROUND TRIP
// =============================================================================
test.describe('AI Assistant Round Trip', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test('ASK assistant - AI inference round trip', 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 { result, duration } = await measureRoundTrip(async () => {
try {
return await callAPI<{ answer: string }>(page, 'askAssistant', {
question: 'What is the main topic?',
meeting_id: meetings.meetings[0].id,
});
} catch (e) {
return { answer: '', error: String(e) };
}
});
recordRoundTrip(
'AI Assistant Query',
'FE→gRPC→LLM(inference)→DB(context)→Response',
duration,
RT_THRESHOLDS.AI_OPERATION,
undefined,
`Answer length: ${result.answer?.length || 0}`
);
});
});
// =============================================================================
// 10. POST-PROCESSING PIPELINE ROUND TRIP
// =============================================================================
test.describe('Post-Processing Pipeline Round Trip', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test.beforeEach(async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
});
test('TRIGGER diarization refinement - async job round trip', 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');
return;
}
const { duration } = await measureRoundTrip(async () => {
try {
return await callAPI(page, 'refineSpeakers', {
meeting_id: completedMeeting.id,
});
} catch {
return { skipped: true };
}
});
recordRoundTrip(
'TRIGGER Diarization',
'FE→gRPC→JobQueue→Audio Processing→DB→Response',
duration,
RT_THRESHOLDS.HEAVY_PROCESSING
);
});
test('TRIGGER summary generation - AI generation round trip', 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');
return;
}
const { duration } = await measureRoundTrip(async () => {
try {
return await callAPI(page, 'generateSummary', {
meeting_id: completedMeeting.id,
});
} catch {
return { skipped: true };
}
});
recordRoundTrip(
'TRIGGER Summary Generation',
'FE→gRPC→LLM(inference)→DB(store)→Response',
duration,
RT_THRESHOLDS.AI_OPERATION
);
});
test('TRIGGER entity extraction - NER round trip', 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');
return;
}
const { duration } = await measureRoundTrip(async () => {
try {
return await callAPI(page, 'extractEntities', {
meeting_id: completedMeeting.id,
});
} catch {
return { skipped: true };
}
});
recordRoundTrip(
'TRIGGER Entity Extraction',
'FE→gRPC→NER Model→DB(store)→Response',
duration,
RT_THRESHOLDS.HEAVY_PROCESSING
);
});
});
// =============================================================================
// 11. CONCURRENT OPERATIONS
// =============================================================================
test.describe('Concurrent Operations Round Trip', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test('CONCURRENT reads - parallel query round trip', async ({ page }) => {
await navigateTo(page, '/');
await waitForAPI(page);
const { duration } = await measureRoundTrip(async () => {
return 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, 'getAnalyticsOverview', { workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID }),
callAPI(page, 'listProjects', { workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID }),
]);
});
recordRoundTrip(
'CONCURRENT 5 Reads',
'FE→gRPC(parallel)→DB(5 queries)→Response',
duration,
RT_THRESHOLDS.MEDIUM_READ * 1.5, // Allow 50% overhead for parallelism
5
);
});
});
// =============================================================================
// 12. FULL PAGE LOAD ROUND TRIP
// =============================================================================
test.describe('Full Page Load Round Trip', () => {
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
test('HOME page - full load with API calls', async ({ page }) => {
const { duration } = await measureRoundTrip(async () => {
await page.goto('/');
await waitForLoadingComplete(page);
await waitForAPI(page);
});
recordRoundTrip('FULL PAGE: Home', 'Navigate→Render→API calls→Complete', duration, RT_THRESHOLDS.FULL_PAGE_LOAD);
});
test('MEETINGS page - full load with list query', async ({ page }) => {
const { duration } = await measureRoundTrip(async () => {
await page.goto('/meetings');
await waitForLoadingComplete(page);
});
recordRoundTrip('FULL PAGE: Meetings', 'Navigate→Render→listMeetings→Complete', duration, RT_THRESHOLDS.FULL_PAGE_LOAD);
});
test('ANALYTICS page - full load with aggregations', async ({ page }) => {
const { duration } = await measureRoundTrip(async () => {
await page.goto('/analytics');
await waitForLoadingComplete(page);
});
recordRoundTrip(
'FULL PAGE: Analytics',
'Navigate→Render→Aggregations→Charts→Complete',
duration,
RT_THRESHOLDS.FULL_PAGE_LOAD
);
});
test('PEOPLE page - full load with speaker stats', async ({ page }) => {
const { duration } = await measureRoundTrip(async () => {
await page.goto('/people');
await waitForLoadingComplete(page);
});
recordRoundTrip(
'FULL PAGE: People',
'Navigate→Render→listSpeakerStats→Complete',
duration,
RT_THRESHOLDS.FULL_PAGE_LOAD
);
});
test('TASKS page - full load with task list', async ({ page }) => {
const { duration } = await measureRoundTrip(async () => {
await page.goto('/tasks');
await waitForLoadingComplete(page);
});
recordRoundTrip('FULL PAGE: Tasks', 'Navigate→Render→listTasks→Complete', duration, RT_THRESHOLDS.FULL_PAGE_LOAD);
});
});
// =============================================================================
// FINAL SUMMARY REPORT
// =============================================================================
test.afterAll(() => {
console.log('\n' + '='.repeat(80));
console.log('FULL ROUND-TRIP PERFORMANCE PROFILING REPORT');
console.log('='.repeat(80));
const passed = allMetrics.filter((m) => m.passed);
const failed = allMetrics.filter((m) => !m.passed);
console.log(`\nTotal Operations Profiled: ${allMetrics.length}`);
console.log(`Passed: ${passed.length} (${((passed.length / allMetrics.length) * 100).toFixed(1)}%)`);
console.log(`Failed: ${failed.length} (${((failed.length / allMetrics.length) * 100).toFixed(1)}%)`);
// Group by flow type
const byFlow = new Map<string, RoundTripMetric[]>();
for (const m of allMetrics) {
const key = m.flow.split('→')[0];
if (!byFlow.has(key)) {
byFlow.set(key, []);
}
byFlow.get(key)!.push(m);
}
console.log('\n--- BY OPERATION TYPE ---');
for (const [flow, metrics] of byFlow) {
const avgDuration = metrics.reduce((sum, m) => sum + m.duration, 0) / metrics.length;
const passRate = (metrics.filter((m) => m.passed).length / metrics.length) * 100;
console.log(`${flow}: avg ${avgDuration.toFixed(0)}ms, ${passRate.toFixed(0)}% pass rate`);
}
if (failed.length > 0) {
console.log('\n--- FAILED OPERATIONS (VULNERABILITIES) ---');
for (const m of failed) {
console.log(
`${m.operation}: ${m.duration.toFixed(0)}ms (threshold: ${m.threshold}ms) - ${m.flow}`
);
}
}
// Find slowest operations
const sorted = [...allMetrics].sort((a, b) => b.duration - a.duration);
console.log('\n--- TOP 10 SLOWEST OPERATIONS ---');
for (const m of sorted.slice(0, 10)) {
const pct = ((m.duration / m.threshold) * 100).toFixed(0);
console.log(` ${m.operation}: ${m.duration.toFixed(0)}ms (${pct}% of threshold)`);
}
console.log('\n--- ALL METRICS ---');
for (const m of allMetrics) {
const status = m.passed ? '✓' : '✗';
const sizeInfo = m.dataSize !== undefined ? ` [${m.dataSize}]` : '';
console.log(` ${status} ${m.operation}: ${m.duration.toFixed(0)}ms${sizeInfo}`);
}
console.log('\n' + '='.repeat(80));
});

View File

@@ -0,0 +1,707 @@
/**
* 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)`);
}
});

View File

@@ -0,0 +1,619 @@
/**
* Streaming Flow Performance Profiling
*
* Profiles recording, transcription, and diarization streaming flows.
* Tests the full audio pipeline from capture to transcript display.
*
* Run with:
* NOTEFLOW_E2E=1 NOTEFLOW_E2E_BASE_URL=http://192.168.50.151:5173 npx playwright test streaming-profile.spec.ts
*/
import { test, expect, type Page } from '@playwright/test';
import {
waitForAppReady,
navigateTo,
waitForAPI,
callAPI,
E2E_TIMEOUTS,
} from './fixtures';
// Streaming profiling thresholds (milliseconds)
const STREAMING_THRESHOLDS = {
// Recording operations
startRecording: 2000,
stopRecording: 3000,
pauseRecording: 500,
resumeRecording: 1000,
// Audio operations
audioDeviceList: 1000,
audioDeviceSelect: 500,
audioLevelResponse: 100,
// Streaming operations
streamInit: 2000,
chunkProcess: 500,
transcriptUpdate: 1000,
// UI responsiveness
recordingUIUpdate: 200,
timerUpdate: 100,
vuMeterUpdate: 50,
};
interface ProfileResult {
operation: string;
duration: number;
threshold: number;
passed: boolean;
details?: string;
}
class StreamingProfiler {
private results: ProfileResult[] = [];
async profile<T>(
operation: string,
threshold: number,
fn: () => Promise<T>,
details?: string
): Promise<T> {
const start = performance.now();
try {
const result = await fn();
const duration = performance.now() - start;
this.results.push({
operation,
duration,
threshold,
passed: duration <= threshold,
details,
});
return result;
} catch (error) {
const duration = performance.now() - start;
this.results.push({
operation,
duration,
threshold,
passed: false,
details: `Error: ${error}`,
});
throw error;
}
}
getResults(): ProfileResult[] {
return this.results;
}
getSummary(): string {
const passed = this.results.filter((r) => r.passed).length;
const total = this.results.length;
let summary = `\n${'='.repeat(60)}\nSTREAMING PROFILE SUMMARY\n${'='.repeat(60)}\n`;
summary += `Results: ${passed}/${total} passed thresholds\n\n`;
for (const result of this.results) {
const status = result.passed ? '✓' : '✗';
summary += `${status} ${result.operation}: ${result.duration.toFixed(2)}ms (threshold: ${result.threshold}ms)`;
if (result.details) {
summary += ` - ${result.details}`;
}
summary += '\n';
}
if (this.results.length > 0) {
const durations = this.results.map((r) => r.duration);
summary += `\nStatistics:\n`;
summary += ` Mean: ${(durations.reduce((a, b) => a + b, 0) / durations.length).toFixed(2)}ms\n`;
summary += ` Min: ${Math.min(...durations).toFixed(2)}ms\n`;
summary += ` Max: ${Math.max(...durations).toFixed(2)}ms\n`;
}
return summary;
}
}
test.describe('Recording Flow Profiling', () => {
let profiler: StreamingProfiler;
test.beforeEach(async ({ page }) => {
profiler = new StreamingProfiler();
await navigateTo(page, '/');
await waitForAppReady(page);
await waitForAPI(page, E2E_TIMEOUTS.PAGE_LOAD_MS);
});
test.afterEach(async () => {
console.log(profiler.getSummary());
});
test('should profile audio device enumeration', async ({ page }) => {
await profiler.profile(
'audio_device_list',
STREAMING_THRESHOLDS.audioDeviceList,
async () => {
const devices = await callAPI<{ devices: unknown[] }>(page, 'listAudioDevices');
return devices;
},
'List available audio devices'
);
const results = profiler.getResults();
expect(results[0].passed).toBe(true);
});
test('should profile recording page load', async ({ page }) => {
await profiler.profile(
'recording_page_load',
3000,
async () => {
await page.goto('/recording/new');
await page.waitForSelector('[data-testid="recording-controls"], [data-testid="start-recording-btn"]', {
timeout: 5000,
});
},
'Load recording page with controls'
);
const results = profiler.getResults();
expect(results[0].passed).toBe(true);
});
test('should profile recording UI state changes', async ({ page }) => {
await page.goto('/recording/new');
await waitForAppReady(page);
// Profile initial state render
await profiler.profile(
'initial_state_render',
STREAMING_THRESHOLDS.recordingUIUpdate,
async () => {
await page.waitForSelector('[data-testid="recording-status"]', { timeout: 2000 }).catch(() => null);
},
'Render initial recording state'
);
// Profile control button responsiveness
const startButton = page.locator('[data-testid="start-recording-btn"], button:has-text("Start")').first();
if (await startButton.isVisible()) {
await profiler.profile(
'button_hover_response',
50,
async () => {
await startButton.hover();
await page.waitForTimeout(10);
},
'UI response to hover'
);
}
console.log(profiler.getSummary());
});
});
test.describe('Transcription Flow Profiling', () => {
let profiler: StreamingProfiler;
let meetingId: string | null = null;
test.beforeEach(async ({ page }) => {
profiler = new StreamingProfiler();
await navigateTo(page, '/');
await waitForAppReady(page);
await waitForAPI(page, E2E_TIMEOUTS.PAGE_LOAD_MS);
});
test.afterEach(async ({ page }) => {
console.log(profiler.getSummary());
// Cleanup
if (meetingId) {
try {
await callAPI(page, 'deleteMeeting', meetingId);
} catch {
// Ignore cleanup errors
}
}
});
test('should profile meeting creation for transcription', async ({ page }) => {
const meeting = await profiler.profile(
'create_meeting_for_transcription',
1000,
async () => {
return callAPI<{ id: string }>(page, 'createMeeting', {
title: `Streaming Profile Test ${Date.now()}`,
});
},
'Create meeting to receive transcription'
);
meetingId = meeting?.id ?? null;
expect(profiler.getResults()[0].passed).toBe(true);
});
test('should profile segment operations', async ({ page }) => {
// Create meeting first
const meeting = await callAPI<{ id: string }>(page, 'createMeeting', {
title: `Segment Profile Test ${Date.now()}`,
});
meetingId = meeting?.id ?? null;
if (!meetingId) {
test.skip();
return;
}
// Profile segment listing (empty)
await profiler.profile(
'list_segments_empty',
500,
async () => {
return callAPI(page, 'listSegments', meetingId);
},
'List segments from empty meeting'
);
// Profile meeting with segments (if any exist)
const meetings = await callAPI<{ meetings: Array<{ id: string }> }>(page, 'listMeetings', { limit: 5 });
const existingMeetingId = meetings.meetings.find((m) => m.id !== meetingId)?.id;
if (existingMeetingId) {
await profiler.profile(
'list_segments_existing',
1000,
async () => {
return callAPI(page, 'listSegments', existingMeetingId);
},
'List segments from existing meeting'
);
}
console.log(profiler.getSummary());
});
});
test.describe('Diarization Flow Profiling', () => {
let profiler: StreamingProfiler;
test.beforeEach(async ({ page }) => {
profiler = new StreamingProfiler();
await navigateTo(page, '/');
await waitForAppReady(page);
await waitForAPI(page, E2E_TIMEOUTS.PAGE_LOAD_MS);
});
test.afterEach(async () => {
console.log(profiler.getSummary());
});
test('should profile diarization status check', async ({ page }) => {
// Get a meeting with potential diarization
const meetings = await callAPI<{ meetings: Array<{ id: string }> }>(page, 'listMeetings', { limit: 1 });
const meetingId = meetings.meetings[0]?.id;
if (!meetingId) {
test.skip();
return;
}
await profiler.profile(
'get_diarization_status',
1000,
async () => {
try {
return await callAPI(page, 'getDiarizationStatus', meetingId);
} catch {
return { status: 'not_started' };
}
},
'Check diarization job status'
);
expect(profiler.getResults()[0].duration).toBeLessThan(STREAMING_THRESHOLDS.transcriptUpdate);
});
test('should profile speaker listing', async ({ page }) => {
const meetings = await callAPI<{ meetings: Array<{ id: string }> }>(page, 'listMeetings', { limit: 1 });
const meetingId = meetings.meetings[0]?.id;
if (!meetingId) {
test.skip();
return;
}
await profiler.profile(
'list_speakers',
1000,
async () => {
try {
const segments = await callAPI<{ segments: Array<{ speaker?: string }> }>(
page,
'listSegments',
meetingId
);
const speakers = new Set(segments.segments.map((s) => s.speaker).filter(Boolean));
return { speakers: Array.from(speakers) };
} catch {
return { speakers: [] };
}
},
'Extract unique speakers from segments'
);
console.log(profiler.getSummary());
});
});
test.describe('Real-time Event Profiling', () => {
let profiler: StreamingProfiler;
test.beforeEach(async ({ page }) => {
profiler = new StreamingProfiler();
await navigateTo(page, '/');
await waitForAppReady(page);
});
test.afterEach(async () => {
console.log(profiler.getSummary());
});
test('should profile event listener setup', async ({ page }) => {
await profiler.profile(
'event_listener_setup',
500,
async () => {
// Simulate setting up event listeners
await page.evaluate(() => {
return new Promise<void>((resolve) => {
const listeners: Array<() => void> = [];
const events = [
'TRANSCRIPT_UPDATE',
'AUDIO_LEVEL',
'RECORDING_TIMER',
'DIARIZATION_UPDATE',
'CONNECTION_CHANGE',
];
events.forEach((event) => {
const handler = () => {};
listeners.push(handler);
});
resolve();
});
});
},
'Set up real-time event listeners'
);
expect(profiler.getResults()[0].passed).toBe(true);
});
test('should profile connection state transitions', async ({ page }) => {
await profiler.profile(
'connection_state_read',
100,
async () => {
return page.evaluate(() => {
const conn = (window as Record<string, unknown>).__NOTEFLOW_CONNECTION__ as {
getConnectionState?: () => unknown;
};
return conn?.getConnectionState?.() ?? null;
});
},
'Read current connection state'
);
expect(profiler.getResults()[0].passed).toBe(true);
});
});
test.describe('Concurrent Streaming Operations', () => {
let profiler: StreamingProfiler;
test.beforeEach(async ({ page }) => {
profiler = new StreamingProfiler();
await navigateTo(page, '/');
await waitForAppReady(page);
await waitForAPI(page, E2E_TIMEOUTS.PAGE_LOAD_MS);
});
test.afterEach(async () => {
console.log(profiler.getSummary());
});
test('should profile concurrent API calls during streaming', async ({ page }) => {
await profiler.profile(
'concurrent_api_burst',
2000,
async () => {
// Simulate concurrent calls that might happen during streaming
const calls = await Promise.all([
callAPI(page, 'listMeetings', { limit: 5 }),
callAPI(page, 'getPreferences'),
callAPI(page, 'listAudioDevices').catch(() => ({ devices: [] })),
]);
return calls;
},
'3 concurrent API calls'
);
expect(profiler.getResults()[0].passed).toBe(true);
});
test('should profile rapid sequential operations', async ({ page }) => {
const iterations = 5;
await profiler.profile(
'rapid_sequential_reads',
1500,
async () => {
const results: unknown[] = [];
for (let i = 0; i < iterations; i++) {
const result = await callAPI(page, 'listMeetings', { limit: 1 });
results.push(result);
}
return results;
},
`${iterations} sequential meeting list calls`
);
const result = profiler.getResults()[0];
console.log(`Average per call: ${(result.duration / iterations).toFixed(2)}ms`);
expect(result.passed).toBe(true);
});
});
test.describe('UI Performance During Streaming', () => {
let profiler: StreamingProfiler;
test.beforeEach(async ({ page }) => {
profiler = new StreamingProfiler();
await navigateTo(page, '/');
await waitForAppReady(page);
});
test.afterEach(async () => {
console.log(profiler.getSummary());
});
test('should profile navigation during potential streaming', async ({ page }) => {
const routes = ['/meetings', '/recording/new', '/settings', '/'];
for (const route of routes) {
await profiler.profile(
`navigate_to_${route.replace(/\//g, '_') || 'home'}`,
3000,
async () => {
await page.goto(route);
await waitForAppReady(page);
},
`Navigate to ${route}`
);
}
const failed = profiler.getResults().filter((r) => !r.passed);
expect(failed.length).toBe(0);
});
test('should profile component render times', async ({ page }) => {
await page.goto('/meetings');
await waitForAppReady(page);
// Profile meeting card render
await profiler.profile(
'meeting_list_render',
2000,
async () => {
await page.waitForSelector('[data-testid="meeting-card"], [data-testid="meetings-list"]', {
timeout: 5000,
}).catch(() => null);
},
'Render meeting list component'
);
console.log(profiler.getSummary());
});
});
test.describe('Streaming Pipeline Simulation', () => {
let profiler: StreamingProfiler;
let createdMeetingId: string | null = null;
test.beforeEach(async ({ page }) => {
profiler = new StreamingProfiler();
await navigateTo(page, '/');
await waitForAppReady(page);
await waitForAPI(page, E2E_TIMEOUTS.PAGE_LOAD_MS);
});
test.afterEach(async ({ page }) => {
console.log(profiler.getSummary());
if (createdMeetingId) {
try {
await callAPI(page, 'deleteMeeting', createdMeetingId);
} catch {
// Ignore
}
}
});
test('should profile full streaming pipeline stages', async ({ page }) => {
// Stage 1: Create meeting
const meeting = await profiler.profile(
'pipeline_stage_1_create_meeting',
1000,
async () => {
return callAPI<{ id: string }>(page, 'createMeeting', {
title: `Pipeline Test ${Date.now()}`,
});
},
'Stage 1: Create meeting for streaming'
);
createdMeetingId = meeting?.id ?? null;
if (!createdMeetingId) {
test.skip();
return;
}
// Stage 2: Get meeting (verify creation)
await profiler.profile(
'pipeline_stage_2_get_meeting',
500,
async () => {
return callAPI(page, 'getMeeting', createdMeetingId);
},
'Stage 2: Verify meeting exists'
);
// Stage 3: List segments (empty initially)
await profiler.profile(
'pipeline_stage_3_list_segments',
500,
async () => {
return callAPI(page, 'listSegments', createdMeetingId);
},
'Stage 3: List initial segments'
);
// Stage 4: Simulate annotation creation (like real-time notes)
await profiler.profile(
'pipeline_stage_4_create_annotation',
500,
async () => {
try {
return await callAPI(page, 'createAnnotation', {
meetingId: createdMeetingId,
type: 'action_item',
text: 'Pipeline test annotation',
});
} catch {
return { id: 'skipped' };
}
},
'Stage 4: Create real-time annotation'
);
// Stage 5: List annotations
await profiler.profile(
'pipeline_stage_5_list_annotations',
500,
async () => {
try {
return await callAPI(page, 'listAnnotations', createdMeetingId);
} catch {
return { annotations: [] };
}
},
'Stage 5: List annotations'
);
// Verify all stages passed
const results = profiler.getResults();
const passed = results.filter((r) => r.passed).length;
console.log(`Pipeline stages passed: ${passed}/${results.length}`);
});
});

View File

@@ -491,6 +491,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -532,6 +533,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -553,6 +555,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -1219,6 +1222,7 @@
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
@@ -4591,6 +4595,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.15.3.tgz",
"integrity": "sha512-bmXydIHfm2rEtGju39FiQNfzkFx9CDvJe+xem1dgEZ2P6Dj7nQX9LnA1ZscW7TuzbBRkL5p3dwuBIi3f62A66A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -4813,6 +4818,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.15.3.tgz",
"integrity": "sha512-n7y/MF9lAM5qlpuH5IR4/uq+kJPEJpe9NrEiH+NmkO/5KJ6cXzpJ6F4U17sMLf2SNCq+TWN9QK8QzoKxIn50VQ==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -4957,6 +4963,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.15.3.tgz",
"integrity": "sha512-ycx/BgxR4rc9tf3ZyTdI98Z19yKLFfqM3UN+v42ChuIwkzyr9zyp7kG8dB9xN2lNqrD+5y/HyJobz/VJ7T90gA==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -4971,6 +4978,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.15.3.tgz",
"integrity": "sha512-Zm1BaU1TwFi3CQiisxjgnzzIus+q40bBKWLqXf6WEaus8Z6+vo1MT2pU52dBCMIRaW9XNDq3E5cmGtMc1AlveA==",
"license": "MIT",
"peer": true,
"dependencies": {
"prosemirror-changeset": "^2.3.0",
"prosemirror-collab": "^1.3.1",
@@ -5070,8 +5078,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/chai": {
"version": "5.2.3",
@@ -5227,6 +5234,7 @@
"integrity": "sha512-qm+G8HuG6hOHQigsi7VGuLjUVu6TtBo/F05zvX04Mw2uCg9Dv0Qxy3Qw7j41SidlTcl5D/5yg0SEZqOB+EqZnQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -5249,6 +5257,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -5259,6 +5268,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -5373,6 +5383,7 @@
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.53.0",
"@typescript-eslint/types": "8.53.0",
@@ -5837,6 +5848,7 @@
"integrity": "sha512-OmwPKV8c5ecLqo+EkytN7oUeYfNmRI4uOXGIR1ybP7AK5Zz+l9R0dGfoadEuwi1aZXAL0vwuhtq3p0OL3dfqHQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.20.0"
},
@@ -5891,6 +5903,7 @@
"integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chalk": "^5.1.2",
"loglevel": "^1.6.0",
@@ -6141,6 +6154,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -6648,6 +6662,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -7414,6 +7429,7 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
@@ -7558,8 +7574,7 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -7770,7 +7785,8 @@
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
@@ -7958,6 +7974,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8294,6 +8311,7 @@
"integrity": "sha512-gQHqfI6SmtYBIkTeMizpHThdpXh6ej2Hk68oKZneFM6iu99ZGXvOPnmhd8VDus3xOWhVDDdf4sLsMV2/o+X6Yg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/snapshot": "^4.0.16",
"deep-eql": "^5.0.2",
@@ -9691,6 +9709,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"license": "MIT",
"peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -9719,6 +9738,7 @@
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz",
"integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.28",
"@asamuzakjp/dom-selector": "^6.7.6",
@@ -10179,7 +10199,6 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -11191,6 +11210,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -11357,7 +11377,6 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -11539,6 +11558,7 @@
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
"license": "MIT",
"peer": true,
"dependencies": {
"orderedmap": "^2.0.0"
}
@@ -11568,6 +11588,7 @@
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT",
"peer": true,
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
@@ -11616,6 +11637,7 @@
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.5.tgz",
"integrity": "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==",
"license": "MIT",
"peer": true,
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0",
@@ -11730,6 +11752,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -11756,6 +11779,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -11769,6 +11793,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -11784,8 +11809,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/react-remove-scroll": {
"version": "2.7.2",
@@ -13031,6 +13055,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -13922,6 +13947,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14157,6 +14183,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -14923,6 +14950,7 @@
"integrity": "sha512-Y5y4jpwHvuduUfup+gXTuCU6AROn/k6qOba3st0laFluKHY+q5SHOpQAJdS8acYLwE8caDQ2dXJhmXyxuJrm0Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/node": "^20.11.30",
"@types/sinonjs__fake-timers": "^8.1.5",

View File

@@ -0,0 +1 @@
"""Backend performance profiling tests."""

View File

@@ -0,0 +1,13 @@
"""Pytest configuration for profiling tests."""
from __future__ import annotations
import pytest
def pytest_configure(config: pytest.Config) -> None:
"""Register profiling markers."""
config.addinivalue_line(
"markers",
"profiling: marks tests as profiling tests (deselect with '-m \"not profiling\"')",
)

View File

@@ -0,0 +1,649 @@
"""Backend gRPC Round-Trip Performance Profiling.
Profiles real backend operations including database round-trips.
Configure NOTEFLOW_GRPC_TARGET to connect to remote server.
Run with:
NOTEFLOW_GRPC_TARGET=192.168.50.151:50051 pytest tests/profiling/test_backend_roundtrip_profile.py -v -s
"""
from __future__ import annotations
import asyncio
import os
import statistics
import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Final
from uuid import uuid4
import grpc
import pytest
from noteflow.grpc.proto import noteflow_pb2, noteflow_pb2_grpc
if TYPE_CHECKING:
from collections.abc import Generator
# Configuration
DEFAULT_SERVER: Final[str] = "localhost:50051"
GRPC_TARGET: Final[str] = os.environ.get("NOTEFLOW_GRPC_TARGET", DEFAULT_SERVER)
# Required gRPC metadata
def get_metadata() -> tuple[tuple[str, str], ...]:
"""Get required gRPC metadata headers."""
return (
("x-request-id", str(uuid4())),
("x-workspace-id", "00000000-0000-0000-0000-000000000001"),
)
# Profiling thresholds (milliseconds)
THRESHOLDS = {
"create_meeting": 500,
"get_meeting": 200,
"list_meetings": 300,
"update_meeting": 300,
"delete_meeting": 300,
"create_segment": 200,
"list_segments": 300,
"diarization_refine": 5000,
"generate_summary": 10000,
"get_analytics": 1000,
"create_annotation": 200,
"list_annotations": 300,
"get_preferences": 100,
"save_preferences": 200,
"concurrent_reads": 500,
"batch_create": 2000,
}
@dataclass
class ProfileResult:
"""Result of a profiled operation."""
operation: str
duration_ms: float
success: bool
threshold_ms: float
details: str = ""
@property
def passed_threshold(self) -> bool:
return self.duration_ms <= self.threshold_ms
def __str__(self) -> str:
status = "PASS" if self.passed_threshold else "FAIL"
return f"[{status}] {self.operation}: {self.duration_ms:.2f}ms (threshold: {self.threshold_ms}ms)"
@dataclass
class ProfileSession:
"""Collection of profiling results."""
results: list[ProfileResult] = field(default_factory=list)
server: str = GRPC_TARGET
def add(self, result: ProfileResult) -> None:
self.results.append(result)
def summary(self) -> str:
lines = [f"\n{'='*60}", f"PROFILING SUMMARY - Server: {self.server}", "=" * 60]
passed = sum(1 for r in self.results if r.passed_threshold)
total = len(self.results)
lines.append(f"Results: {passed}/{total} passed thresholds\n")
for result in self.results:
lines.append(str(result))
if self.results:
durations = [r.duration_ms for r in self.results]
lines.append(f"\nStatistics:")
lines.append(f" Mean: {statistics.mean(durations):.2f}ms")
lines.append(f" Median: {statistics.median(durations):.2f}ms")
lines.append(f" Stdev: {statistics.stdev(durations):.2f}ms" if len(durations) > 1 else "")
lines.append(f" Min: {min(durations):.2f}ms")
lines.append(f" Max: {max(durations):.2f}ms")
return "\n".join(lines)
@pytest.fixture(scope="module")
def grpc_channel() -> Generator[grpc.Channel, None, None]:
"""Create gRPC channel to the target server."""
channel = grpc.insecure_channel(
GRPC_TARGET,
options=[
("grpc.keepalive_time_ms", 10000),
("grpc.keepalive_timeout_ms", 5000),
("grpc.keepalive_permit_without_calls", True),
],
)
# Wait for channel to be ready
try:
grpc.channel_ready_future(channel).result(timeout=10)
except grpc.FutureTimeoutError:
pytest.skip(f"Could not connect to gRPC server at {GRPC_TARGET}")
yield channel
channel.close()
@pytest.fixture(scope="module")
def stub(grpc_channel: grpc.Channel) -> noteflow_pb2_grpc.NoteFlowServiceStub:
"""Create gRPC stub."""
return noteflow_pb2_grpc.NoteFlowServiceStub(grpc_channel)
@pytest.fixture(scope="module")
def profile_session() -> ProfileSession:
"""Create a profiling session."""
return ProfileSession()
def profile_operation(
operation: str, threshold_ms: float | None = None
) -> callable:
"""Decorator to profile an operation."""
def decorator(func: callable) -> callable:
def wrapper(
stub: noteflow_pb2_grpc.NoteFlowServiceStub,
profile_session: ProfileSession,
*args,
**kwargs,
):
threshold = threshold_ms or THRESHOLDS.get(operation, 1000)
start = time.perf_counter()
try:
result = func(stub, profile_session, *args, **kwargs)
duration_ms = (time.perf_counter() - start) * 1000
profile_result = ProfileResult(
operation=operation,
duration_ms=duration_ms,
success=True,
threshold_ms=threshold,
)
profile_session.add(profile_result)
print(profile_result)
return result
except grpc.RpcError as e:
duration_ms = (time.perf_counter() - start) * 1000
profile_result = ProfileResult(
operation=operation,
duration_ms=duration_ms,
success=False,
threshold_ms=threshold,
details=str(e.code()),
)
profile_session.add(profile_result)
print(f"{operation}: FAILED - {e.code()}")
raise
return wrapper
return decorator
class TestMeetingCRUDProfiling:
"""Profile meeting CRUD operations."""
@pytest.fixture(autouse=True)
def _setup(
self,
stub: noteflow_pb2_grpc.NoteFlowServiceStub,
profile_session: ProfileSession,
) -> None:
self.stub = stub
self.session = profile_session
self.meeting_id: str | None = None
def test_01_create_meeting(self) -> None:
"""Profile meeting creation."""
start = time.perf_counter()
request = noteflow_pb2.CreateMeetingRequest(
title=f"Profile Test Meeting {uuid4()}",
)
try:
response = self.stub.CreateMeeting(request, timeout=10, metadata=get_metadata())
duration_ms = (time.perf_counter() - start) * 1000
self.meeting_id = response.id
result = ProfileResult(
operation="create_meeting",
duration_ms=duration_ms,
success=True,
threshold_ms=THRESHOLDS["create_meeting"],
)
self.session.add(result)
print(result)
# Store for subsequent tests
pytest.meeting_id = response.id
assert response.id
except grpc.RpcError as e:
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="create_meeting",
duration_ms=duration_ms,
success=False,
threshold_ms=THRESHOLDS["create_meeting"],
details=str(e.code()),
)
self.session.add(result)
pytest.skip(f"Create meeting failed: {e.code()}")
def test_02_get_meeting(self) -> None:
"""Profile meeting retrieval."""
meeting_id = getattr(pytest, "meeting_id", None)
if not meeting_id:
pytest.skip("No meeting created")
start = time.perf_counter()
request = noteflow_pb2.GetMeetingRequest(meeting_id=meeting_id)
try:
response = self.stub.GetMeeting(request, timeout=10, metadata=get_metadata())
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="get_meeting",
duration_ms=duration_ms,
success=True,
threshold_ms=THRESHOLDS["get_meeting"],
)
self.session.add(result)
print(result)
assert response.id == meeting_id
except grpc.RpcError as e:
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="get_meeting",
duration_ms=duration_ms,
success=False,
threshold_ms=THRESHOLDS["get_meeting"],
details=str(e.code()),
)
self.session.add(result)
raise
def test_03_list_meetings(self) -> None:
"""Profile meeting listing."""
start = time.perf_counter()
request = noteflow_pb2.ListMeetingsRequest(limit=50)
try:
response = self.stub.ListMeetings(request, timeout=10, metadata=get_metadata())
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="list_meetings",
duration_ms=duration_ms,
success=True,
threshold_ms=THRESHOLDS["list_meetings"],
details=f"count={len(response.meetings)}",
)
self.session.add(result)
print(result)
except grpc.RpcError as e:
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="list_meetings",
duration_ms=duration_ms,
success=False,
threshold_ms=THRESHOLDS["list_meetings"],
details=str(e.code()),
)
self.session.add(result)
raise
def test_04_stop_meeting(self) -> None:
"""Profile meeting stop operation."""
meeting_id = getattr(pytest, "meeting_id", None)
if not meeting_id:
pytest.skip("No meeting created")
start = time.perf_counter()
request = noteflow_pb2.StopMeetingRequest(meeting_id=meeting_id)
try:
response = self.stub.StopMeeting(request, timeout=10, metadata=get_metadata())
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="stop_meeting",
duration_ms=duration_ms,
success=True,
threshold_ms=THRESHOLDS.get("stop_meeting", 500),
)
self.session.add(result)
print(result)
except grpc.RpcError as e:
duration_ms = (time.perf_counter() - start) * 1000
# Stop on non-recording meeting might fail, but we still capture timing
result = ProfileResult(
operation="stop_meeting",
duration_ms=duration_ms,
success=False,
threshold_ms=THRESHOLDS.get("stop_meeting", 500),
details=str(e.code()),
)
self.session.add(result)
print(result) # Don't raise - expected to fail on non-recording meeting
class TestSegmentProfiling:
"""Profile segment operations."""
@pytest.fixture(autouse=True)
def _setup(
self,
stub: noteflow_pb2_grpc.NoteFlowServiceStub,
profile_session: ProfileSession,
) -> None:
self.stub = stub
self.session = profile_session
def test_list_segments(self) -> None:
"""Profile segment listing (via GetMeeting with include_segments)."""
meeting_id = getattr(pytest, "meeting_id", None)
if not meeting_id:
pytest.skip("No meeting created")
start = time.perf_counter()
request = noteflow_pb2.GetMeetingRequest(
meeting_id=meeting_id,
include_segments=True,
)
try:
response = self.stub.GetMeeting(request, timeout=10, metadata=get_metadata())
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="list_segments",
duration_ms=duration_ms,
success=True,
threshold_ms=THRESHOLDS["list_segments"],
details=f"count={len(response.segments)}",
)
self.session.add(result)
print(result)
except grpc.RpcError as e:
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="list_segments",
duration_ms=duration_ms,
success=False,
threshold_ms=THRESHOLDS["list_segments"],
details=str(e.code()),
)
self.session.add(result)
raise
class TestAnnotationProfiling:
"""Profile annotation operations."""
@pytest.fixture(autouse=True)
def _setup(
self,
stub: noteflow_pb2_grpc.NoteFlowServiceStub,
profile_session: ProfileSession,
) -> None:
self.stub = stub
self.session = profile_session
def test_01_create_annotation(self) -> None:
"""Profile annotation creation."""
meeting_id = getattr(pytest, "meeting_id", None)
if not meeting_id:
pytest.skip("No meeting created")
start = time.perf_counter()
request = noteflow_pb2.AddAnnotationRequest(
meeting_id=meeting_id,
annotation_type=noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM,
text=f"Profile test annotation {uuid4()}",
)
try:
response = self.stub.AddAnnotation(request, timeout=10, metadata=get_metadata())
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="create_annotation",
duration_ms=duration_ms,
success=True,
threshold_ms=THRESHOLDS["create_annotation"],
)
self.session.add(result)
print(result)
pytest.annotation_id = response.id
except grpc.RpcError as e:
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="create_annotation",
duration_ms=duration_ms,
success=False,
threshold_ms=THRESHOLDS["create_annotation"],
details=str(e.code()),
)
self.session.add(result)
raise
def test_02_list_annotations(self) -> None:
"""Profile annotation listing."""
meeting_id = getattr(pytest, "meeting_id", None)
if not meeting_id:
pytest.skip("No meeting created")
start = time.perf_counter()
request = noteflow_pb2.ListAnnotationsRequest(meeting_id=meeting_id)
try:
response = self.stub.ListAnnotations(request, timeout=10, metadata=get_metadata())
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="list_annotations",
duration_ms=duration_ms,
success=True,
threshold_ms=THRESHOLDS["list_annotations"],
details=f"count={len(response.annotations)}",
)
self.session.add(result)
print(result)
except grpc.RpcError as e:
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="list_annotations",
duration_ms=duration_ms,
success=False,
threshold_ms=THRESHOLDS["list_annotations"],
details=str(e.code()),
)
self.session.add(result)
raise
class TestPreferencesProfiling:
"""Profile preferences operations."""
@pytest.fixture(autouse=True)
def _setup(
self,
stub: noteflow_pb2_grpc.NoteFlowServiceStub,
profile_session: ProfileSession,
) -> None:
self.stub = stub
self.session = profile_session
def test_get_preferences(self) -> None:
"""Profile preferences retrieval."""
start = time.perf_counter()
request = noteflow_pb2.GetPreferencesRequest()
try:
response = self.stub.GetPreferences(request, timeout=10, metadata=get_metadata())
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="get_preferences",
duration_ms=duration_ms,
success=True,
threshold_ms=THRESHOLDS["get_preferences"],
)
self.session.add(result)
print(result)
except grpc.RpcError as e:
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="get_preferences",
duration_ms=duration_ms,
success=False,
threshold_ms=THRESHOLDS["get_preferences"],
details=str(e.code()),
)
self.session.add(result)
raise
class TestConcurrentProfiling:
"""Profile concurrent operations."""
@pytest.fixture(autouse=True)
def _setup(
self,
stub: noteflow_pb2_grpc.NoteFlowServiceStub,
profile_session: ProfileSession,
) -> None:
self.stub = stub
self.session = profile_session
def test_concurrent_reads(self) -> None:
"""Profile concurrent read operations."""
import concurrent.futures
def read_meetings():
request = noteflow_pb2.ListMeetingsRequest(limit=10)
return self.stub.ListMeetings(request, timeout=10, metadata=get_metadata())
start = time.perf_counter()
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(read_meetings) for _ in range(5)]
results = [f.result() for f in concurrent.futures.as_completed(futures)]
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="concurrent_reads",
duration_ms=duration_ms,
success=True,
threshold_ms=THRESHOLDS["concurrent_reads"],
details=f"5 concurrent requests",
)
self.session.add(result)
print(result)
class TestBatchProfiling:
"""Profile batch operations."""
@pytest.fixture(autouse=True)
def _setup(
self,
stub: noteflow_pb2_grpc.NoteFlowServiceStub,
profile_session: ProfileSession,
) -> None:
self.stub = stub
self.session = profile_session
def test_batch_create_meetings(self) -> None:
"""Profile batch meeting creation."""
meeting_ids = []
start = time.perf_counter()
for i in range(10):
request = noteflow_pb2.CreateMeetingRequest(
title=f"Batch Profile Meeting {i} - {uuid4()}",
)
try:
response = self.stub.CreateMeeting(request, timeout=10, metadata=get_metadata())
meeting_ids.append(response.id)
except grpc.RpcError:
pass
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="batch_create",
duration_ms=duration_ms,
success=len(meeting_ids) == 10,
threshold_ms=THRESHOLDS["batch_create"],
details=f"created={len(meeting_ids)}/10",
)
self.session.add(result)
print(result)
# Cleanup - delete created meetings
for mid in meeting_ids:
try:
request = noteflow_pb2.DeleteMeetingRequest(meeting_id=mid)
self.stub.DeleteMeeting(request, timeout=10, metadata=get_metadata())
except grpc.RpcError:
pass
class TestAnalyticsProfiling:
"""Profile analytics operations."""
@pytest.fixture(autouse=True)
def _setup(
self,
stub: noteflow_pb2_grpc.NoteFlowServiceStub,
profile_session: ProfileSession,
) -> None:
self.stub = stub
self.session = profile_session
def test_get_analytics(self) -> None:
"""Profile analytics retrieval."""
start = time.perf_counter()
request = noteflow_pb2.GetAnalyticsOverviewRequest()
try:
response = self.stub.GetAnalyticsOverview(request, timeout=30, metadata=get_metadata())
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="get_analytics",
duration_ms=duration_ms,
success=True,
threshold_ms=THRESHOLDS["get_analytics"],
)
self.session.add(result)
print(result)
except grpc.RpcError as e:
duration_ms = (time.perf_counter() - start) * 1000
result = ProfileResult(
operation="get_analytics",
duration_ms=duration_ms,
success=False,
threshold_ms=THRESHOLDS["get_analytics"],
details=str(e.code()),
)
self.session.add(result)
# Don't raise - analytics might not be implemented
print(f"Analytics: {e.code()} (may not be implemented)")
class TestCleanupAndSummary:
"""Cleanup test data and print summary."""
@pytest.fixture(autouse=True)
def _setup(
self,
stub: noteflow_pb2_grpc.NoteFlowServiceStub,
profile_session: ProfileSession,
) -> None:
self.stub = stub
self.session = profile_session
def test_cleanup_and_summary(self) -> None:
"""Cleanup created meeting and print summary."""
meeting_id = getattr(pytest, "meeting_id", None)
if meeting_id:
try:
request = noteflow_pb2.DeleteMeetingRequest(meeting_id=meeting_id)
self.stub.DeleteMeeting(request, timeout=10, metadata=get_metadata())
print(f"Cleaned up meeting: {meeting_id}")
except grpc.RpcError as e:
print(f"Cleanup failed: {e.code()}")
# Print summary
print(self.session.summary())
# Assert all thresholds passed (optional - comment out for pure profiling)
failed = [r for r in self.session.results if not r.passed_threshold and r.success]
if failed:
print(f"\n[WARNING] {len(failed)} operations exceeded thresholds")