From f6c501030e94b397a6232d8cb8e04ec71ea413a6 Mon Sep 17 00:00:00 2001 From: oib Date: Mon, 21 Jul 2025 17:39:09 +0200 Subject: [PATCH] RC2 --- .gitignore | 64 ++++++- public_streams.txt | 2 - run-navigation-test.js | 179 ++++++++++++++++++++ static/app.js | 101 +++++++---- static/fix-nav.js | 182 ++++++++++---------- static/footer.html | 2 + static/nav.js | 11 +- tests/profile-auth.js | 374 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 789 insertions(+), 126 deletions(-) delete mode 100644 public_streams.txt create mode 100644 run-navigation-test.js create mode 100644 tests/profile-auth.js diff --git a/.gitignore b/.gitignore index 2d1218b..0f64611 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,65 @@ -# Bytecode-Dateien +# Bytecode files __pycache__/ *.py[cod] -# Virtuelle Umgebungen +# Virtual environments .venv/ venv/ -# Betriebssystem-Dateien +# System files .DS_Store Thumbs.db -# Logfiles und Dumps +# Logs and temporary files *.log *.bak *.swp *.tmp -# IDEs und Editoren +# Node.js dependencies +node_modules/ +package.json +package-lock.json +yarn.lock + +# Development documentation +PERFORMANCE-TESTING.md + +# Build and distribution +dist/ +build/ +*.min.js +*.min.css +*.map + +# Testing +coverage/ +*.test.js +*.spec.js +.nyc_output/ + +# Environment variables +.env +.env.* +!.env.example + +# Debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDEs and editors .vscode/ .idea/ +*.sublime-workspace +*.sublime-project +# Local development +.cache/ +.temp/ +.tmp/ + +# Project specific data/* !data/.gitignore @@ -28,3 +68,17 @@ log/* streams/* !streams/.gitignore + +# Test files +tests/**/*.js +!tests/*.test.js +!tests/*.spec.js +!tests/README.md +!tests/profile-auth.js + +# Performance test results +performance-results/* +!performance-results/.gitkeep + +# Legacy files +public_streams.txt diff --git a/public_streams.txt b/public_streams.txt deleted file mode 100644 index 3eb027b..0000000 --- a/public_streams.txt +++ /dev/null @@ -1,2 +0,0 @@ -{"uid":"oibchello","size":3371119,"mtime":1752994076} -{"uid":"orangeicebear","size":1734396,"mtime":1748767975} diff --git a/run-navigation-test.js b/run-navigation-test.js new file mode 100644 index 0000000..ef2d9fa --- /dev/null +++ b/run-navigation-test.js @@ -0,0 +1,179 @@ +const puppeteer = require('puppeteer'); +const fs = require('fs'); +const path = require('path'); + +// Configuration +const BASE_URL = 'http://localhost:8000'; // Update this if your app runs on a different URL +const TEST_ITERATIONS = 5; +const OUTPUT_DIR = path.join(__dirname, 'performance-results'); +const TIMESTAMP = new Date().toISOString().replace(/[:.]/g, '-'); + +// Ensure output directory exists +if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); +} + +// Helper function to save results +function saveResults(data, filename) { + const filepath = path.join(OUTPUT_DIR, `${filename}-${TIMESTAMP}.json`); + fs.writeFileSync(filepath, JSON.stringify(data, null, 2)); + console.log(`Results saved to ${filepath}`); + return filepath; +} + +// Test runner +async function runNavigationTest() { + const browser = await puppeteer.launch({ + headless: false, // Set to true for CI/CD + devtools: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-accelerated-2d-canvas', + '--no-first-run', + '--no-zygote', + '--single-process', + '--disable-gpu', + '--js-flags="--max-old-space-size=1024"' + ] + }); + + try { + const page = await browser.newPage(); + + // Enable performance metrics + await page.setViewport({ width: 1280, height: 800 }); + await page.setDefaultNavigationTimeout(60000); + + // Set up console logging + page.on('console', msg => console.log('PAGE LOG:', msg.text())); + + // Load the performance test script + const testScript = fs.readFileSync(path.join(__dirname, 'static', 'router-perf-test.js'), 'utf8'); + + // Test guest mode + console.log('Testing guest mode...'); + await page.goto(`${BASE_URL}`, { waitUntil: 'networkidle0' }); + + // Inject and run the test + const guestResults = await page.evaluate(async (script) => { + // Inject the test script + const scriptEl = document.createElement('script'); + scriptEl.textContent = script; + document.head.appendChild(scriptEl); + + // Run the test + const test = new RouterPerfTest(); + return await test.runTest('guest'); + }, testScript); + + saveResults(guestResults, 'guest-results'); + + // Test logged-in mode (if credentials are provided) + if (process.env.LOGIN_EMAIL && process.env.LOGIN_PASSWORD) { + console.log('Testing logged-in mode...'); + + // Navigate to the test page with authentication token + console.log('Authenticating with provided token...'); + await page.goto('https://dicta2stream.net/?token=d96561d7-6c95-4e10-80f7-62d5d3a5bd04', { + waitUntil: 'networkidle0', + timeout: 60000 + }); + + // Wait for authentication to complete and verify + try { + await page.waitForSelector('body.authenticated', { + timeout: 30000, + visible: true + }); + console.log('✅ Successfully authenticated'); + + // Verify user is actually logged in + const isAuthenticated = await page.evaluate(() => { + return document.body.classList.contains('authenticated'); + }); + + if (!isAuthenticated) { + throw new Error('Authentication failed - not in authenticated state'); + } + + // Force a navigation to ensure the state is stable + await page.goto('https://dicta2stream.net/#welcome-page', { + waitUntil: 'networkidle0', + timeout: 30000 + }); + + } catch (error) { + console.error('❌ Authentication failed:', error.message); + // Take a screenshot for debugging + await page.screenshot({ path: 'auth-failure.png' }); + console.log('Screenshot saved as auth-failure.png'); + throw error; + } + + // Wait for the page to fully load after login + await page.waitForTimeout(2000); + + // Run the test in logged-in mode + const loggedInResults = await page.evaluate(async (script) => { + const test = new RouterPerfTest(); + return await test.runTest('loggedIn'); + }, testScript); + + saveResults(loggedInResults, 'loggedin-results'); + + // Generate comparison report + const comparison = { + timestamp: new Date().toISOString(), + guest: { + avg: guestResults.overall.avg, + min: guestResults.overall.min, + max: guestResults.overall.max + }, + loggedIn: { + avg: loggedInResults.overall.avg, + min: loggedInResults.overall.min, + max: loggedInResults.overall.max + }, + difference: { + ms: loggedInResults.overall.avg - guestResults.overall.avg, + percent: ((loggedInResults.overall.avg - guestResults.overall.avg) / guestResults.overall.avg) * 100 + } + }; + + const reportPath = saveResults(comparison, 'performance-comparison'); + console.log(`\nPerformance comparison report generated at: ${reportPath}`); + + // Take a screenshot of the results + await page.screenshot({ path: path.join(OUTPUT_DIR, `results-${TIMESTAMP}.png`), fullPage: true }); + + return comparison; + } + + return guestResults; + + } catch (error) { + console.error('Test failed:', error); + // Take a screenshot on error + if (page) { + await page.screenshot({ path: path.join(OUTPUT_DIR, `error-${TIMESTAMP}.png`), fullPage: true }); + } + throw error; + + } finally { + await browser.close(); + } +} + +// Run the test +runNavigationTest() + .then(results => { + console.log('Test completed successfully'); + console.log('Results:', JSON.stringify(results, null, 2)); + process.exit(0); + }) + .catch(error => { + console.error('Test failed:', error); + process.exit(1); + }); diff --git a/static/app.js b/static/app.js index 0025e3a..afc81bb 100644 --- a/static/app.js +++ b/static/app.js @@ -505,10 +505,15 @@ window.clearTimeout = (id) => { originalClearTimeout(id); }; -// Track auth check calls +// Track auth check calls and cache state let lastAuthCheckTime = 0; let authCheckCounter = 0; const AUTH_CHECK_DEBOUNCE = 1000; // 1 second +let authStateCache = { + timestamp: 0, + value: null, + ttl: 5000 // Cache TTL in milliseconds +}; // Override console.log to capture all logs const originalConsoleLog = console.log; @@ -822,33 +827,46 @@ function updateAccountDeletionVisibility(isAuthenticated) { }); } -// Check authentication state and update UI -function checkAuthState() { - // Debounce rapid calls +// Check authentication state and update UI with caching and debouncing +function checkAuthState(force = false) { const now = Date.now(); - if (now - lastAuthCheckTime < AUTH_CHECK_DEBOUNCE) { + + // Return cached value if still valid and not forcing a refresh + if (!force && now - authStateCache.timestamp < authStateCache.ttl && authStateCache.value !== null) { + return authStateCache.value; + } + + // Debounce rapid calls + if (now - lastAuthCheckTime < AUTH_CHECK_DEBOUNCE && !force) { return wasAuthenticated === true; } + lastAuthCheckTime = now; authCheckCounter++; - - // Check various authentication indicators - const hasAuthCookie = document.cookie.includes('isAuthenticated=true'); - const hasUidCookie = document.cookie.includes('uid='); - const hasLocalStorageAuth = localStorage.getItem('isAuthenticated') === 'true'; - const hasAuthToken = !!localStorage.getItem('authToken'); - // User is considered authenticated if any of these are true - const isAuthenticated = hasAuthCookie || hasUidCookie || hasLocalStorageAuth || hasAuthToken; + // Use a single check for authentication state + let isAuthenticated = false; + + // Check the most likely indicators first for better performance + isAuthenticated = + document.cookie.includes('isAuthenticated=') || + document.cookie.includes('uid=') || + localStorage.getItem('isAuthenticated') === 'true' || + !!localStorage.getItem('authToken'); + + // Update cache + authStateCache = { + timestamp: now, + value: isAuthenticated, + ttl: isAuthenticated ? 30000 : 5000 // Longer TTL for authenticated users + }; - if (DEBUG_AUTH_STATE || isAuthenticated !== wasAuthenticated) { + if (DEBUG_AUTH_STATE && isAuthenticated !== wasAuthenticated) { console.log('Auth State Check:', { - hasAuthCookie, - hasUidCookie, - hasLocalStorageAuth, - hasAuthToken, isAuthenticated, - wasAuthenticated + wasAuthenticated, + cacheHit: !force && now - authStateCache.timestamp < authStateCache.ttl, + cacheAge: now - authStateCache.timestamp }); } @@ -887,19 +905,44 @@ function checkAuthState() { return isAuthenticated; } -// Periodically check authentication state +// Periodically check authentication state with optimized polling function setupAuthStatePolling() { - // Initial check - checkAuthState(); + // Initial check with force to ensure we get the latest state + checkAuthState(true); - // Check every 30 seconds instead of 2 to reduce load - setInterval(checkAuthState, 30000); + // Use a single interval for all checks + const checkAndUpdate = () => { + // Only force check if the page is visible + checkAuthState(!document.hidden); + }; - // Also check after certain events that might affect auth state - window.addEventListener('storage', checkAuthState); - document.addEventListener('visibilitychange', () => { - if (!document.hidden) checkAuthState(); - }); + // Check every 30 seconds (reduced from previous implementation) + const AUTH_CHECK_INTERVAL = 30000; + setInterval(checkAndUpdate, AUTH_CHECK_INTERVAL); + + // Listen for storage events (like login/logout from other tabs) + const handleStorageEvent = (e) => { + if (['isAuthenticated', 'authToken', 'uid'].includes(e.key)) { + checkAuthState(true); // Force check on relevant storage changes + } + }; + + window.addEventListener('storage', handleStorageEvent); + + // Check auth state when page becomes visible + const handleVisibilityChange = () => { + if (!document.hidden) { + checkAuthState(true); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + // Cleanup function + return () => { + window.removeEventListener('storage', handleStorageEvent); + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; } diff --git a/static/fix-nav.js b/static/fix-nav.js index 3ce5bc4..8c34805 100644 --- a/static/fix-nav.js +++ b/static/fix-nav.js @@ -1,50 +1,69 @@ -// Force hide guest navigation for authenticated users -function fixMobileNavigation() { - console.log('[FIX-NAV] Running navigation fix...'); +// Debounce helper function +function debounce(func, wait) { + let timeout; + return function() { + const context = this; + const args = arguments; + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(context, args), wait); + }; +} + +// Throttle helper function +function throttle(func, limit) { + let inThrottle; + return function() { + const args = arguments; + const context = this; + if (!inThrottle) { + func.apply(context, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; +} + +// Check authentication state once and cache it +function getAuthState() { + return ( + document.cookie.includes('isAuthenticated=') || + document.cookie.includes('uid=') || + localStorage.getItem('isAuthenticated') === 'true' || + !!localStorage.getItem('authToken') + ); +} + +// Update navigation based on authentication state +function updateNavigation() { + const isAuthenticated = getAuthState(); - // Check if user is authenticated - const hasAuthCookie = document.cookie.includes('isAuthenticated=true'); - const hasUidCookie = document.cookie.includes('uid='); - const hasLocalStorageAuth = localStorage.getItem('isAuthenticated') === 'true'; - const hasAuthToken = localStorage.getItem('authToken') !== null; - const isAuthenticated = hasAuthCookie || hasUidCookie || hasLocalStorageAuth || hasAuthToken; - - console.log('[FIX-NAV] Authentication state:', { - isAuthenticated, - hasAuthCookie, - hasUidCookie, - hasLocalStorageAuth, - hasAuthToken - }); + // Only proceed if the authentication state has changed + if (isAuthenticated === updateNavigation.lastState) { + return; + } + updateNavigation.lastState = isAuthenticated; if (isAuthenticated) { - // Force hide guest navigation with !important styles + // Hide guest navigation for authenticated users const guestNav = document.getElementById('guest-dashboard'); if (guestNav) { - console.log('[FIX-NAV] Hiding guest navigation'); guestNav.style.cssText = ` display: none !important; visibility: hidden !important; opacity: 0 !important; height: 0 !important; - width: 0 !important; - padding: 0 !important; - margin: 0 !important; - border: none !important; - position: absolute !important; overflow: hidden !important; + position: absolute !important; clip: rect(0, 0, 0, 0) !important; pointer-events: none !important; `; - guestNav.classList.add('force-hidden'); } - // Ensure user navigation is visible with !important styles + // Show user navigation if it exists const userNav = document.getElementById('user-dashboard'); if (userNav) { - console.log('[FIX-NAV] Showing user navigation'); userNav.style.cssText = ` - display: flex !important; + display: block !important; visibility: visible !important; opacity: 1 !important; height: auto !important; @@ -55,25 +74,9 @@ function fixMobileNavigation() { userNav.classList.add('force-visible'); } - // Add authenticated class to body + // Update body classes document.body.classList.add('authenticated'); document.body.classList.remove('guest-mode'); - - // Prevent default behavior of nav links that might cause page reloads - document.querySelectorAll('a[href^="#"]').forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - const targetId = link.getAttribute('href'); - if (targetId && targetId !== '#') { - // Use history API to update URL without full page reload - history.pushState(null, '', targetId); - // Dispatch a custom event that other scripts can listen for - window.dispatchEvent(new CustomEvent('hashchange')); - // Force re-apply our navigation fix - setTimeout(fixMobileNavigation, 0); - } - }); - }); } else { // User is not authenticated - ensure guest nav is visible const guestNav = document.getElementById('guest-dashboard'); @@ -85,50 +88,53 @@ function fixMobileNavigation() { } } -// Run on page load -document.addEventListener('DOMContentLoaded', fixMobileNavigation); +// Initialize the navigation state +updateNavigation.lastState = null; -// Also run after a short delay to catch any dynamic content -setTimeout(fixMobileNavigation, 100); -setTimeout(fixMobileNavigation, 300); -setTimeout(fixMobileNavigation, 1000); - -// Listen for hash changes (navigation) -window.addEventListener('hashchange', fixMobileNavigation); - -// Listen for pushState/replaceState (SPA navigation) -const originalPushState = history.pushState; -const originalReplaceState = history.replaceState; - -history.pushState = function() { - originalPushState.apply(this, arguments); - setTimeout(fixMobileNavigation, 0); -}; - -history.replaceState = function() { - originalReplaceState.apply(this, arguments); - setTimeout(fixMobileNavigation, 0); -}; - -// Run on any DOM mutations (for dynamically loaded content) -const observer = new MutationObserver((mutations) => { - let shouldFix = false; - mutations.forEach((mutation) => { - if (mutation.addedNodes.length || mutation.removedNodes.length) { - shouldFix = true; - } - }); - if (shouldFix) { - setTimeout(fixMobileNavigation, 0); +// Handle navigation link clicks +function handleNavLinkClick(e) { + const link = e.target.closest('a[href^="#"]'); + if (!link) return; + + e.preventDefault(); + const targetId = link.getAttribute('href'); + if (targetId && targetId !== '#') { + // Update URL without triggering full page reload + history.pushState(null, '', targetId); + // Dispatch a custom event for other scripts + window.dispatchEvent(new CustomEvent('hashchange')); } -}); +} -observer.observe(document.body, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ['class', 'style', 'id'] -}); +// Initialize the navigation system +function initNavigation() { + // Set up event delegation for navigation links + document.body.addEventListener('click', handleNavLinkClick); + + // Listen for hash changes (throttled) + window.addEventListener('hashchange', throttle(updateNavigation, 100)); + + // Listen for storage changes (like login/logout from other tabs) + window.addEventListener('storage', debounce(updateNavigation, 100)); + + // Check for authentication changes periodically (every 30 seconds) + setInterval(updateNavigation, 30000); + + // Initial update + updateNavigation(); +} -// Export for debugging -window.fixMobileNavigation = fixMobileNavigation; +// Run initialization when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initNavigation); +} else { + // DOMContentLoaded has already fired + initNavigation(); +} + +// Export for testing if needed +window.navigationUtils = { + updateNavigation, + getAuthState, + initNavigation +}; diff --git a/static/footer.html b/static/footer.html index 3a8684b..bb32a29 100644 --- a/static/footer.html +++ b/static/footer.html @@ -8,5 +8,7 @@ Privacy Imprint + + diff --git a/static/nav.js b/static/nav.js index 61d2684..ea54db3 100644 --- a/static/nav.js +++ b/static/nav.js @@ -258,8 +258,15 @@ document.addEventListener("DOMContentLoaded", () => { e.preventDefault(); const target = link.dataset.target; if (target) { - // Show the target section without updating URL hash - showSection(target); + // Update URL hash to maintain proper history state + window.location.hash = target; + // Use the router to handle the navigation + if (router && typeof router.showOnly === 'function') { + router.showOnly(target); + } else { + // Fallback to showSection if router is not available + showSection(target); + } } }); }); diff --git a/tests/profile-auth.js b/tests/profile-auth.js new file mode 100644 index 0000000..e19d536 --- /dev/null +++ b/tests/profile-auth.js @@ -0,0 +1,374 @@ +/** + * 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.'); +})();