From 71af4e256f1917197a5eebfdcb09b8e648b0e955 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Fri, 10 Oct 2025 17:01:15 +0200 Subject: [PATCH] Correction du loggueur qui sature les CPU des browsers --- pmoapp/webapp/src/components/LogView.vue | 97 ++++++++++++++---------- pmoupnp/src/devices/device_methods.rs | 2 +- pmoupnp/src/devices/device_registry.rs | 13 ++-- 3 files changed, 66 insertions(+), 46 deletions(-) diff --git a/pmoapp/webapp/src/components/LogView.vue b/pmoapp/webapp/src/components/LogView.vue index 1f28affa..9442f921 100644 --- a/pmoapp/webapp/src/components/LogView.vue +++ b/pmoapp/webapp/src/components/LogView.vue @@ -42,13 +42,13 @@
-
+
- {{ truncateMessage(log.message) }} + {{ log.truncatedMessage }} -
+
-
+
@@ -103,11 +103,14 @@ const levelOrder = { 'TRACE': 4 } +// Pré-calculer filteredLogs de manière optimisée const filteredLogs = computed(() => { if (levelFilter.value === 'ALL') { return logs.value } - return logs.value.filter(log => log.level === levelFilter.value) + // Utiliser la référence directe pour éviter des copies inutiles + const filter = levelFilter.value + return logs.value.filter(log => log.level === filter) }) // Fonction pour mettre à jour le niveau de log côté serveur @@ -157,65 +160,62 @@ function formatTimestamp(timestamp) { }) } -function isTooLong(message) { - // Un message est trop long s'il a plus d'une ligne OU plus de 200 caractères +// Pré-traiter un log : calculer HTML, troncature, etc. UNE SEULE FOIS +function preprocessLog(message) { + // ÉTAPE 1: Déterminer si trop long const firstLineEnd = message.indexOf('\n') - return firstLineEnd !== -1 || message.length > 200 -} + const isTooLong = firstLineEnd !== -1 || message.length > 200 -function truncateMessage(message) { - // Prendre la première ligne, ou les 200 premiers caractères si pas de saut de ligne - const firstLineEnd = message.indexOf('\n') - if (firstLineEnd !== -1) { - return message.substring(0, firstLineEnd).trim() - } - return message.substring(0, 200).trim() -} + // ÉTAPE 2: Calculer le message tronqué si nécessaire + const truncatedMessage = isTooLong + ? (firstLineEnd !== -1 + ? message.substring(0, firstLineEnd).trim() + : message.substring(0, 200).trim()) + : null -function renderMarkdown(text) { - // ÉTAPE 1 : Pré-processing pour détecter et protéger le XML - let processedText = text + // ÉTAPE 3: Pré-processing pour détecter et protéger le XML + let processedText = message // Détecter si le message contient du XML - // Pattern : cherche \s]/i.test(text) + const hasXml = /<\?xml|<(scpd|root|service|device|actionList|stateVariable)[>\s]/i.test(message) if (hasXml) { - // Extraire tout ce qui ressemble à du XML (du \s][\s\S]*/) + const xmlMatch = message.match(/<([a-zA-Z][a-zA-Z0-9:-]*)[>\s][\s\S]*/) if (xmlMatch) { const xmlContent = xmlMatch[0] - const beforeXml = text.substring(0, text.indexOf(xmlContent)) + const beforeXml = message.substring(0, message.indexOf(xmlContent)) processedText = beforeXml + '\n```xml\n' + xmlContent + '\n```\n' } } } - // ÉTAPE 2 : Détecter et transformer les liens d'images - // Pattern pour détecter les URLs d'images (png, jpg, jpeg, gif, webp, svg) + // ÉTAPE 4: Détecter et transformer les liens d'images const imageUrlPattern = /(https?:\/\/[^\s]+\.(?:png|jpg|jpeg|gif|webp|svg)(?:\?[^\s]*)?)/gi processedText = processedText.replace(imageUrlPattern, (match) => { return `\n![Image](${match})\n` }) - // ÉTAPE 3 : Convertir markdown en HTML + // ÉTAPE 5: Convertir markdown en HTML const rawHtml = marked.parse(processedText, { async: false }) - // ÉTAPE 4 : Nettoyer pour la sécurité - return DOMPurify.sanitize(rawHtml, { + // ÉTAPE 6: Nettoyer pour la sécurité + const renderedHtml = DOMPurify.sanitize(rawHtml, { ALLOWED_TAGS: ['strong', 'em', 'code', 'pre', 'a', 'ul', 'ol', 'li', 'p', 'br', 'span', 'img'], ALLOWED_ATTR: ['href', 'target', 'class', 'src', 'alt', 'title'] }) + + return { + isTooLong, + truncatedMessage, + renderedHtml + } } function toggleAutoScroll() { @@ -265,6 +265,12 @@ function connectSSE() { logEntry.isHistory = true } + // PRÉ-TRAITER le log UNE SEULE FOIS à la réception + const processed = preprocessLog(logEntry.message) + logEntry.isTooLong = processed.isTooLong + logEntry.truncatedMessage = processed.truncatedMessage + logEntry.renderedHtml = processed.renderedHtml + logs.value.push(logEntry) // Limiter à 1000 logs en mémoire @@ -325,17 +331,32 @@ onUnmounted(() => { }) // Désactiver auto-scroll si l'utilisateur scroll manuellement -watch(logContainer, (container) => { +let scrollHandler = null +watch(logContainer, (container, oldContainer) => { + // Nettoyer l'ancien listener si existant + if (oldContainer && scrollHandler) { + oldContainer.removeEventListener('scroll', scrollHandler) + } + if (!container) return - container.addEventListener('scroll', () => { + scrollHandler = () => { const isAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 50 if (!isAtBottom && autoScroll.value) { autoScroll.value = false } - }) + } + + container.addEventListener('scroll', scrollHandler, { passive: true }) +}) + +// Nettoyer au démontage +onUnmounted(() => { + if (logContainer.value && scrollHandler) { + logContainer.value.removeEventListener('scroll', scrollHandler) + } }) diff --git a/pmoupnp/src/devices/device_methods.rs b/pmoupnp/src/devices/device_methods.rs index 515cf3c1..6e131fd2 100644 --- a/pmoupnp/src/devices/device_methods.rs +++ b/pmoupnp/src/devices/device_methods.rs @@ -5,7 +5,7 @@ use xmltree::{Element, XMLNode}; use crate::{ devices::{Device, DeviceInstance}, - UpnpObject, UpnpModel, UpnpInstance, UpnpTyped, + UpnpObject, UpnpModel, UpnpInstance, }; impl UpnpObject for Device { diff --git a/pmoupnp/src/devices/device_registry.rs b/pmoupnp/src/devices/device_registry.rs index 320c9800..393611d4 100644 --- a/pmoupnp/src/devices/device_registry.rs +++ b/pmoupnp/src/devices/device_registry.rs @@ -465,7 +465,6 @@ mod tests { use super::*; use crate::{ devices::Device, - services::Service, UpnpModel, }; @@ -477,13 +476,13 @@ mod tests { #[test] fn test_device_registration() { - let registry = DeviceRegistry::new(); + let mut registry = DeviceRegistry::new(); let device = Device::new( "TestDevice".to_string(), "MediaRenderer".to_string(), "Test Renderer".to_string(), ); - let instance = Arc::new(device.create_instance()); + let instance = device.create_instance(); assert!(registry.register(instance.clone()).is_ok()); assert_eq!(registry.count(), 1); @@ -494,13 +493,13 @@ mod tests { #[test] fn test_device_retrieval() { - let registry = DeviceRegistry::new(); + let mut registry = DeviceRegistry::new(); let device = Device::new( "TestDevice".to_string(), "MediaRenderer".to_string(), "Test Renderer".to_string(), ); - let instance = Arc::new(device.create_instance()); + let instance = device.create_instance(); let udn = instance.udn().to_string(); registry.register(instance.clone()).unwrap(); @@ -517,13 +516,13 @@ mod tests { #[test] fn test_device_unregistration() { - let registry = DeviceRegistry::new(); + let mut registry = DeviceRegistry::new(); let device = Device::new( "TestDevice".to_string(), "MediaRenderer".to_string(), "Test Renderer".to_string(), ); - let instance = Arc::new(device.create_instance()); + let instance = device.create_instance(); let udn = instance.udn().to_string(); registry.register(instance).unwrap();