375 lines
12 KiB
JavaScript
375 lines
12 KiB
JavaScript
/**
|
|
* Authentication Profiling Tool for dicta2stream
|
|
*
|
|
* This script profiles the authentication-related operations during navigation
|
|
* to identify performance bottlenecks in the logged-in experience.
|
|
*
|
|
* Usage:
|
|
* 1. Open browser console (F12 → Console)
|
|
* 2. Copy and paste this entire script
|
|
* 3. Run: profileAuthNavigation()
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Store original methods we want to profile
|
|
const originalFetch = window.fetch;
|
|
const originalXHROpen = XMLHttpRequest.prototype.open;
|
|
const originalXHRSend = XMLHttpRequest.prototype.send;
|
|
|
|
// Track authentication-related operations with detailed metrics
|
|
const authProfile = {
|
|
// Core metrics
|
|
startTime: null,
|
|
operations: [],
|
|
navigationEvents: [],
|
|
|
|
// Counters
|
|
fetchCount: 0,
|
|
xhrCount: 0,
|
|
domUpdates: 0,
|
|
authChecks: 0,
|
|
|
|
// Performance metrics
|
|
totalTime: 0,
|
|
maxFrameTime: 0,
|
|
longTasks: [],
|
|
|
|
// Memory tracking
|
|
memorySamples: [],
|
|
maxMemory: 0,
|
|
|
|
// Navigation tracking
|
|
currentNavigation: null,
|
|
navigationStart: null
|
|
};
|
|
|
|
// Track long tasks and performance metrics
|
|
if (window.PerformanceObserver) {
|
|
// Check which entry types are supported
|
|
const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
|
|
|
|
// Only set up observers for supported types
|
|
const perfObserver = new PerformanceObserver((list) => {
|
|
const entries = list.getEntries();
|
|
for (const entry of entries) {
|
|
// Track any task longer than 50ms as a long task
|
|
if (entry.duration > 50) {
|
|
authProfile.longTasks.push({
|
|
startTime: entry.startTime,
|
|
duration: entry.duration,
|
|
name: entry.name || 'unknown',
|
|
type: entry.entryType
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Try to observe supported entry types
|
|
try {
|
|
// Check for longtask support (not available in all browsers)
|
|
if (supportedEntryTypes.includes('longtask')) {
|
|
perfObserver.observe({ entryTypes: ['longtask'] });
|
|
}
|
|
|
|
// Always try to observe paint timing
|
|
try {
|
|
if (supportedEntryTypes.includes('paint')) {
|
|
perfObserver.observe({ entryTypes: ['paint'] });
|
|
} else {
|
|
// Fallback to buffered paint observation
|
|
perfObserver.observe({ type: 'paint', buffered: true });
|
|
}
|
|
} catch (e) {
|
|
console.debug('Paint timing not supported:', e.message);
|
|
}
|
|
|
|
} catch (e) {
|
|
console.debug('Performance observation not supported:', e.message);
|
|
}
|
|
}
|
|
|
|
// Instrument fetch API
|
|
window.fetch = async function(...args) {
|
|
const url = typeof args[0] === 'string' ? args[0] : args[0].url;
|
|
const isAuthRelated = isAuthOperation(url);
|
|
|
|
if (isAuthRelated) {
|
|
const start = performance.now();
|
|
authProfile.fetchCount++;
|
|
|
|
try {
|
|
const response = await originalFetch.apply(this, args);
|
|
const duration = performance.now() - start;
|
|
|
|
authProfile.operations.push({
|
|
type: 'fetch',
|
|
url,
|
|
duration,
|
|
timestamp: new Date().toISOString(),
|
|
status: response.status,
|
|
size: response.headers.get('content-length') || 'unknown'
|
|
});
|
|
|
|
return response;
|
|
} catch (error) {
|
|
const duration = performance.now() - start;
|
|
authProfile.operations.push({
|
|
type: 'fetch',
|
|
url,
|
|
duration,
|
|
timestamp: new Date().toISOString(),
|
|
error: error.message,
|
|
status: 'error'
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
return originalFetch.apply(this, args);
|
|
};
|
|
|
|
// Instrument XHR
|
|
XMLHttpRequest.prototype.open = function(method, url) {
|
|
this._authProfile = isAuthOperation(url);
|
|
if (this._authProfile) {
|
|
this._startTime = performance.now();
|
|
this._url = url;
|
|
authProfile.xhrCount++;
|
|
}
|
|
return originalXHROpen.apply(this, arguments);
|
|
};
|
|
|
|
XMLHttpRequest.prototype.send = function(body) {
|
|
if (this._authProfile) {
|
|
this.addEventListener('load', () => {
|
|
const duration = performance.now() - this._startTime;
|
|
authProfile.operations.push({
|
|
type: 'xhr',
|
|
url: this._url,
|
|
duration,
|
|
timestamp: new Date().toISOString(),
|
|
status: this.status,
|
|
size: this.getResponseHeader('content-length') || 'unknown'
|
|
});
|
|
});
|
|
|
|
this.addEventListener('error', (error) => {
|
|
const duration = performance.now() - this._startTime;
|
|
authProfile.operations.push({
|
|
type: 'xhr',
|
|
url: this._url,
|
|
duration,
|
|
timestamp: new Date().toISOString(),
|
|
error: error.message,
|
|
status: 'error'
|
|
});
|
|
});
|
|
}
|
|
|
|
return originalXHRSend.apply(this, arguments);
|
|
};
|
|
|
|
// Track DOM updates after navigation with more details
|
|
const observer = new MutationObserver((mutations) => {
|
|
if (document.body.classList.contains('authenticated')) {
|
|
const now = performance.now();
|
|
const updateInfo = {
|
|
timestamp: now,
|
|
mutations: mutations.length,
|
|
addedNodes: 0,
|
|
removedNodes: 0,
|
|
attributeChanges: 0,
|
|
characterDataChanges: 0
|
|
};
|
|
|
|
mutations.forEach(mutation => {
|
|
updateInfo.addedNodes += mutation.addedNodes.length || 0;
|
|
updateInfo.removedNodes += mutation.removedNodes.length || 0;
|
|
updateInfo.attributeChanges += mutation.type === 'attributes' ? 1 : 0;
|
|
updateInfo.characterDataChanges += mutation.type === 'characterData' ? 1 : 0;
|
|
});
|
|
|
|
authProfile.domUpdates += mutations.length;
|
|
|
|
// Track memory usage if supported
|
|
if (window.performance && window.performance.memory) {
|
|
updateInfo.memoryUsed = performance.memory.usedJSHeapSize;
|
|
updateInfo.memoryTotal = performance.memory.totalJSHeapSize;
|
|
updateInfo.memoryLimit = performance.memory.jsHeapSizeLimit;
|
|
|
|
authProfile.memorySamples.push({
|
|
timestamp: now,
|
|
memory: performance.memory.usedJSHeapSize
|
|
});
|
|
|
|
authProfile.maxMemory = Math.max(
|
|
authProfile.maxMemory,
|
|
performance.memory.usedJSHeapSize
|
|
);
|
|
}
|
|
|
|
// Track frame time
|
|
requestAnimationFrame(() => {
|
|
const frameTime = performance.now() - now;
|
|
authProfile.maxFrameTime = Math.max(authProfile.maxFrameTime, frameTime);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Track authentication state changes
|
|
const originalAddClass = DOMTokenList.prototype.add;
|
|
const originalRemoveClass = DOMTokenList.prototype.remove;
|
|
|
|
DOMTokenList.prototype.add = function(...args) {
|
|
if (this === document.body.classList && args[0] === 'authenticated') {
|
|
authProfile.authChecks++;
|
|
if (!authProfile.startTime) {
|
|
authProfile.startTime = performance.now();
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
characterData: true
|
|
});
|
|
}
|
|
}
|
|
return originalAddClass.apply(this, args);
|
|
};
|
|
|
|
DOMTokenList.prototype.remove = function(...args) {
|
|
if (this === document.body.classList && args[0] === 'authenticated') {
|
|
authProfile.totalTime = performance.now() - (authProfile.startTime || performance.now());
|
|
observer.disconnect();
|
|
}
|
|
return originalRemoveClass.apply(this, args);
|
|
};
|
|
|
|
// Helper to identify auth-related operations
|
|
function isAuthOperation(url) {
|
|
if (!url) return false;
|
|
const authKeywords = [
|
|
'auth', 'login', 'session', 'token', 'user', 'profile',
|
|
'me', 'account', 'verify', 'validate', 'check'
|
|
];
|
|
return authKeywords.some(keyword =>
|
|
url.toLowerCase().includes(keyword)
|
|
);
|
|
}
|
|
|
|
// Main function to run the profile
|
|
window.profileAuthNavigation = async function() {
|
|
// Reset profile with all metrics
|
|
Object.assign(authProfile, {
|
|
startTime: null,
|
|
operations: [],
|
|
navigationEvents: [],
|
|
fetchCount: 0,
|
|
xhrCount: 0,
|
|
domUpdates: 0,
|
|
authChecks: 0,
|
|
totalTime: 0,
|
|
maxFrameTime: 0,
|
|
longTasks: [],
|
|
memorySamples: [],
|
|
maxMemory: 0,
|
|
currentNavigation: null,
|
|
navigationStart: null
|
|
});
|
|
|
|
// Track navigation events
|
|
if (window.performance) {
|
|
const navObserver = new PerformanceObserver((list) => {
|
|
list.getEntries().forEach(entry => {
|
|
if (entry.entryType === 'navigation') {
|
|
authProfile.navigationEvents.push({
|
|
type: 'navigation',
|
|
name: entry.name,
|
|
startTime: entry.startTime,
|
|
duration: entry.duration,
|
|
domComplete: entry.domComplete,
|
|
domContentLoaded: entry.domContentLoadedEventEnd,
|
|
load: entry.loadEventEnd
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
try {
|
|
navObserver.observe({ entryTypes: ['navigation'] });
|
|
} catch (e) {
|
|
console.warn('Navigation timing not supported:', e);
|
|
}
|
|
}
|
|
|
|
console.log('Starting authentication navigation profile...');
|
|
console.log('1. Navigate to a few pages while logged in');
|
|
console.log('2. Run getAuthProfile() to see the results');
|
|
console.log('3. Run resetAuthProfile() to start over');
|
|
|
|
// Add global accessor
|
|
window.getAuthProfile = function() {
|
|
const now = performance.now();
|
|
const duration = authProfile.startTime ? (now - authProfile.startTime) / 1000 : 0;
|
|
|
|
console.log('%c\n=== AUTHENTICATION PROFILE RESULTS ===', 'font-weight:bold;font-size:1.2em');
|
|
|
|
// Summary
|
|
console.log('\n%c--- PERFORMANCE SUMMARY ---', 'font-weight:bold');
|
|
console.log(`Total Monitoring Time: ${duration.toFixed(2)}s`);
|
|
console.log(`Authentication Checks: ${authProfile.authChecks}`);
|
|
console.log(`Fetch API Calls: ${authProfile.fetchCount}`);
|
|
console.log(`XHR Requests: ${authProfile.xhrCount}`);
|
|
console.log(`DOM Updates: ${authProfile.domUpdates}`);
|
|
console.log(`Max Frame Time: ${authProfile.maxFrameTime.toFixed(2)}ms`);
|
|
|
|
// Memory usage
|
|
if (authProfile.memorySamples.length > 0) {
|
|
const lastMem = authProfile.memorySamples[authProfile.memorySamples.length - 1];
|
|
console.log(`\n%c--- MEMORY USAGE ---`, 'font-weight:bold');
|
|
console.log(`Max Memory Used: ${(authProfile.maxMemory / 1024 / 1024).toFixed(2)} MB`);
|
|
console.log(`Current Memory: ${(lastMem.memory / 1024 / 1024).toFixed(2)} MB`);
|
|
}
|
|
|
|
// Long tasks
|
|
if (authProfile.longTasks.length > 0) {
|
|
console.log(`\n%c--- LONG TASKS (${authProfile.longTasks.length} > 50ms) ---`, 'font-weight:bold');
|
|
authProfile.longTasks
|
|
.sort((a, b) => b.duration - a.duration)
|
|
.slice(0, 5)
|
|
.forEach((task, i) => {
|
|
console.log(`#${i + 1} ${task.name}: ${task.duration.toFixed(2)}ms`);
|
|
});
|
|
}
|
|
|
|
// Slow operations
|
|
if (authProfile.operations.length > 0) {
|
|
console.log('\n%c--- SLOW OPERATIONS ---', 'font-weight:bold');
|
|
const sortedOps = [...authProfile.operations].sort((a, b) => b.duration - a.duration);
|
|
sortedOps.slice(0, 10).forEach((op, i) => {
|
|
const memUsage = op.memory ? ` | +${(op.memory / 1024).toFixed(2)}KB` : '';
|
|
console.log(`#${i + 1} [${op.type.toUpperCase()}] ${op.url || 'unknown'}: ${op.duration.toFixed(2)}ms${memUsage}`);
|
|
});
|
|
}
|
|
|
|
return authProfile;
|
|
};
|
|
|
|
window.resetAuthProfile = function() {
|
|
Object.assign(authProfile, {
|
|
startTime: null,
|
|
operations: [],
|
|
fetchCount: 0,
|
|
xhrCount: 0,
|
|
domUpdates: 0,
|
|
authChecks: 0,
|
|
totalTime: 0
|
|
});
|
|
console.log('Authentication profile has been reset');
|
|
};
|
|
};
|
|
|
|
console.log('Authentication profiler loaded. Run profileAuthNavigation() to start.');
|
|
})();
|