diff --git a/onvif-java/src/main/java/de/onvif/soap/OnvifDevice.java b/onvif-java/src/main/java/de/onvif/soap/OnvifDevice.java index edd487d..f830d17 100644 --- a/onvif-java/src/main/java/de/onvif/soap/OnvifDevice.java +++ b/onvif-java/src/main/java/de/onvif/soap/OnvifDevice.java @@ -45,12 +45,15 @@ public class OnvifDevice { private final URL url; // Example http://host:port, https://host, http://host, http://ip_address private Device device; - private Media media; - private PTZ ptz; - private ImagingPort imaging; - private EventPortType events; + private volatile Media media; + private volatile PTZ ptz; + private volatile ImagingPort imaging; + private volatile EventPortType events; public final EventService eventService = new EventService(); + // Cache capabilities to avoid repeated network requests + private volatile Capabilities capabilities; + private static boolean verbose = false; // enable/disable logging of SOAP messages final SimpleSecurityHandler securityHandler; @@ -112,56 +115,43 @@ public class OnvifDevice { } /** - * Initalizes the addresses used for SOAP messages and to get the internal IP, if given IP is a - * proxy. + * Initialize ONVIF device connection, only creates Device service, other services use lazy initialization + * This strategy significantly reduces initial memory usage and startup time * - * @throws ConnectException Get thrown if device doesn't give answers to GetCapabilities() - * @throws SOAPException + * @throws ConnectException Thrown when device is inaccessible or invalid, doesn't respond to SOAP messages + * @throws SOAPException SOAP related exceptions */ protected void init() throws ConnectException, SOAPException { + logger.debug("Initializing ONVIF device connection: {}", url); DeviceService deviceService = new DeviceService(null, DeviceService.SERVICE); - BindingProvider deviceServicePort = (BindingProvider) deviceService.getDevicePort(); - this.device = - getServiceProxy(deviceServicePort, url.toString() + DEVICE_SERVICE).create(Device.class); - // resetSystemDateAndTime(); // don't modify the camera in a constructor.. :) + // Use OnvifServiceFactory to create Device service proxy + this.device = OnvifServiceFactory.createServiceProxy( + deviceServicePort, + url.toString() + DEVICE_SERVICE, + Device.class, + securityHandler, + verbose + ); - Capabilities capabilities = this.device.getCapabilities(List.of(CapabilityCategory.ALL)); - if (capabilities == null) { + // Get and cache capabilities to avoid subsequent repeated network requests + this.capabilities = this.device.getCapabilities(List.of(CapabilityCategory.ALL)); + if (this.capabilities == null) { throw new ConnectException("Capabilities not reachable."); } - if (capabilities.getMedia() != null && capabilities.getMedia().getXAddr() != null) { - this.media = new MediaService().getMediaPort(); - this.media = - getServiceProxy((BindingProvider) media, capabilities.getMedia().getXAddr()) - .create(Media.class); - } - - if (capabilities.getPTZ() != null && capabilities.getPTZ().getXAddr() != null) { - this.ptz = new PtzService().getPtzPort(); - this.ptz = - getServiceProxy((BindingProvider) ptz, capabilities.getPTZ().getXAddr()) - .create(PTZ.class); - } - - if (capabilities.getImaging() != null && capabilities.getImaging().getXAddr() != null) { - this.imaging = new ImagingService().getImagingPort(); - this.imaging = - getServiceProxy((BindingProvider) imaging, capabilities.getImaging().getXAddr()) - .create(ImagingPort.class); - } - - if (capabilities.getEvents() != null && capabilities.getEvents().getXAddr() != null) { - this.events = eventService.getEventPort(); - this.events = - getServiceProxy((BindingProvider) events, capabilities.getEvents().getXAddr()) - .create(EventPortType.class); - } + logger.debug("Device initialization completed, capabilities cached, other services will be initialized on demand"); + // Note: Other services (Media, PTZ, Imaging, Events) now use lazy initialization + // They will only be created on first access, significantly reducing memory usage } + /** + * @deprecated Use OnvifServiceFactory instead for better memory management and Schema caching + * This method is retained only to ensure backward compatibility + */ + @Deprecated public JaxWsProxyFactoryBean getServiceProxy(BindingProvider servicePort, String serviceAddr) { JaxWsProxyFactoryBean proxyFactory = new JaxWsProxyFactoryBean(); @@ -236,22 +226,125 @@ public class OnvifDevice { return device; } + /** + * Get cached device capabilities information + * This information was obtained and cached during initialization to avoid repeated network requests + */ + public Capabilities getCapabilities() { + return capabilities; + } + public PTZ getPtz() { + initPtzService(); return ptz; } + /** + * Lazy initialization of PTZ service + * Uses double-checked locking pattern to ensure thread safety + */ + private void initPtzService() { + if (ptz == null && capabilities.getPTZ() != null && capabilities.getPTZ().getXAddr() != null) { + synchronized (this) { + if (ptz == null) { + logger.debug("Lazy initializing PTZ service"); + PtzService ptzService = new PtzService(); + BindingProvider ptzServicePort = (BindingProvider) ptzService.getPtzPort(); + this.ptz = OnvifServiceFactory.createServiceProxy( + ptzServicePort, + capabilities.getPTZ().getXAddr(), + PTZ.class, + securityHandler, + verbose + ); + } + } + } + } + public Media getMedia() { + initMediaService(); return media; } + /** + * Lazy initialization of Media service + * Uses double-checked locking pattern to ensure thread safety + */ + private void initMediaService() { + if (media == null && capabilities.getMedia() != null && capabilities.getMedia().getXAddr() != null) { + synchronized (this) { + if (media == null) { + logger.debug("Lazy initializing Media service"); + MediaService mediaService = new MediaService(); + BindingProvider mediaServicePort = (BindingProvider) mediaService.getMediaPort(); + this.media = OnvifServiceFactory.createServiceProxy( + mediaServicePort, + capabilities.getMedia().getXAddr(), + Media.class, + securityHandler, + verbose + ); + } + } + } + } + public ImagingPort getImaging() { + initImagingService(); return imaging; } + /** + * Lazy initialization of Imaging service + * Uses double-checked locking pattern to ensure thread safety + */ + private void initImagingService() { + if (imaging == null && capabilities.getImaging() != null && capabilities.getImaging().getXAddr() != null) { + synchronized (this) { + if (imaging == null) { + logger.debug("Lazy initializing Imaging service"); + ImagingService imagingService = new ImagingService(); + BindingProvider imagingServicePort = (BindingProvider) imagingService.getImagingPort(); + this.imaging = OnvifServiceFactory.createServiceProxy( + imagingServicePort, + capabilities.getImaging().getXAddr(), + ImagingPort.class, + securityHandler, + verbose + ); + } + } + } + } + public EventPortType getEvents() { + initEventsService(); return events; } + /** + * Lazy initialization of Events service + * Uses double-checked locking pattern to ensure thread safety + */ + private void initEventsService() { + if (events == null && capabilities.getEvents() != null && capabilities.getEvents().getXAddr() != null) { + synchronized (this) { + if (events == null) { + logger.debug("Lazy initializing Events service"); + BindingProvider eventsServicePort = (BindingProvider) eventService.getEventPort(); + this.events = OnvifServiceFactory.createServiceProxy( + eventsServicePort, + capabilities.getEvents().getXAddr(), + EventPortType.class, + securityHandler, + verbose + ); + } + } + } + } + public DateTime getDate() { return device.getSystemDateAndTime().getLocalDateTime(); } @@ -281,9 +374,12 @@ public class OnvifDevice { // returns http://host[:port]/path_for_snapshot public String getSnapshotUri(String profileToken) { - MediaUri sceenshotUri = media.getSnapshotUri(profileToken); - if (sceenshotUri != null) { - return sceenshotUri.getUri(); + Media mediaService = getMedia(); + if (mediaService != null) { + MediaUri sceenshotUri = mediaService.getSnapshotUri(profileToken); + if (sceenshotUri != null) { + return sceenshotUri.getUri(); + } } return ""; } @@ -298,24 +394,34 @@ public class OnvifDevice { // Get snapshot uri for profile with index public String getSnapshotUri(int index) { - if (media.getProfiles().size() >= index) - return getSnapshotUri(media.getProfiles().get(index).getToken()); + Media mediaService = getMedia(); + if (mediaService != null && mediaService.getProfiles().size() > index) { + return getSnapshotUri(mediaService.getProfiles().get(index).getToken()); + } return ""; } public String getStreamUri(int index) { - return getStreamUri(media.getProfiles().get(index).getToken()); + Media mediaService = getMedia(); + if (mediaService != null && mediaService.getProfiles().size() > index) { + return getStreamUri(mediaService.getProfiles().get(index).getToken()); + } + return ""; } // returns rtsp://host[:port]/path_for_rtsp public String getStreamUri(String profileToken) { - StreamSetup streamSetup = new StreamSetup(); - Transport t = new Transport(); - t.setProtocol(TransportProtocol.RTSP); - streamSetup.setTransport(t); - streamSetup.setStream(StreamType.RTP_UNICAST); - MediaUri rtsp = media.getStreamUri(streamSetup, profileToken); - return rtsp != null ? rtsp.getUri() : ""; + Media mediaService = getMedia(); + if (mediaService != null) { + StreamSetup streamSetup = new StreamSetup(); + Transport t = new Transport(); + t.setProtocol(TransportProtocol.RTSP); + streamSetup.setTransport(t); + streamSetup.setStream(StreamType.RTP_UNICAST); + MediaUri rtsp = mediaService.getStreamUri(streamSetup, profileToken); + return rtsp != null ? rtsp.getUri() : ""; + } + return ""; } public static boolean isVerbose() { @@ -325,4 +431,39 @@ public class OnvifDevice { public static void setVerbose(boolean verbose) { OnvifDevice.verbose = verbose; } + + /** + * Check if service is initialized + */ + public boolean isServiceInitialized(String serviceName) { + switch (serviceName.toLowerCase()) { + case "media": return media != null; + case "ptz": return ptz != null; + case "imaging": return imaging != null; + case "events": return events != null; + case "device": return device != null; + default: return false; + } + } + + /** + * Get count of initialized services + */ + public int getInitializedServicesCount() { + int count = 0; + if (device != null) count++; + if (media != null) count++; + if (ptz != null) count++; + if (imaging != null) count++; + if (events != null) count++; + return count; + } + + /** + * Clean up resources and clear cache (static method, affects all instances) + * Recommended to call when application shuts down + */ + public static void cleanupResources() { + OnvifServiceFactory.clearCache(); + } } diff --git a/onvif-java/src/main/java/de/onvif/soap/OnvifServiceFactory.java b/onvif-java/src/main/java/de/onvif/soap/OnvifServiceFactory.java new file mode 100644 index 0000000..9d25031 --- /dev/null +++ b/onvif-java/src/main/java/de/onvif/soap/OnvifServiceFactory.java @@ -0,0 +1,147 @@ +package de.onvif.soap; + +import jakarta.xml.ws.BindingProvider; +import org.apache.cxf.binding.soap.Soap12; +import org.apache.cxf.binding.soap.SoapBindingConfiguration; +import org.apache.cxf.endpoint.Client; +import org.apache.cxf.frontend.ClientProxy; +import org.apache.cxf.interceptor.LoggingInInterceptor; +import org.apache.cxf.interceptor.LoggingOutInterceptor; +import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; +import org.apache.cxf.transport.http.HTTPConduit; +import org.apache.cxf.transports.http.configuration.HTTPClientPolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Service proxy factory that caches and reuses WSDL Schema parsing results. + * Significantly reduces memory usage and initialization time through caching mechanism. + * + * @author ONVIF Optimization Team + */ +public class OnvifServiceFactory { + private static final Logger logger = LoggerFactory.getLogger(OnvifServiceFactory.class); + + // Core: Cache parsed proxy factory configurations using Map + private static final Map proxyCache = new ConcurrentHashMap<>(); + + /** + * Creates service proxy using cached Schema parsing results + * + * @param servicePort Service port binding provider + * @param serviceAddr Service address + * @param serviceClass Service class type + * @param securityHandler Security handler + * @param verbose Whether to enable verbose logging + * @return Service proxy instance + */ + public static synchronized T createServiceProxy( + BindingProvider servicePort, + String serviceAddr, + Class serviceClass, + SimpleSecurityHandler securityHandler, + boolean verbose) { + + // Use service class name as cache key + String cacheKey = serviceClass.getName(); + + JaxWsProxyFactoryBean proxyFactory = proxyCache.get(cacheKey); + if (proxyFactory == null) { + logger.debug("First-time creating service proxy {} - parsing and caching Schema", serviceClass.getSimpleName()); + // Parse Schema and cache on first creation + proxyFactory = createProxyFactory(servicePort, securityHandler, verbose); + proxyCache.put(cacheKey, proxyFactory); + } else { + logger.debug("Reusing cached service proxy configuration {} - skipping Schema parsing", serviceClass.getSimpleName()); + } + + // Set specific address for each instance - create new factory instance to avoid concurrency issues + JaxWsProxyFactoryBean instanceFactory = cloneProxyFactory(proxyFactory); + if (serviceAddr != null) { + instanceFactory.setAddress(serviceAddr); + } + + return instanceFactory.create(serviceClass); + } + + /** + * Create basic configuration for proxy factory + */ + private static JaxWsProxyFactoryBean createProxyFactory( + BindingProvider servicePort, + SimpleSecurityHandler securityHandler, + boolean verbose) { + + JaxWsProxyFactoryBean proxyFactory = new JaxWsProxyFactoryBean(); + proxyFactory.getHandlers(); + + proxyFactory.setServiceClass(servicePort.getClass()); + + SoapBindingConfiguration config = new SoapBindingConfiguration(); + config.setVersion(Soap12.getInstance()); + proxyFactory.setBindingConfig(config); + + Client deviceClient = ClientProxy.getClient(servicePort); + + if (verbose) { + // Enable SOAP message logging (for debugging/development only) + proxyFactory.getOutInterceptors().add(new LoggingOutInterceptor()); + proxyFactory.getInInterceptors().add(new LoggingInInterceptor()); + } + + HTTPConduit http = (HTTPConduit) deviceClient.getConduit(); + if (securityHandler != null) { + proxyFactory.getHandlers().add(securityHandler); + } + + HTTPClientPolicy httpClientPolicy = http.getClient(); + httpClientPolicy.setConnectionTimeout(36000); + httpClientPolicy.setReceiveTimeout(32000); + httpClientPolicy.setAllowChunking(false); + + return proxyFactory; + } + + /** + * Clone proxy factory to create independent instances + */ + private static JaxWsProxyFactoryBean cloneProxyFactory(JaxWsProxyFactoryBean original) { + JaxWsProxyFactoryBean clone = new JaxWsProxyFactoryBean(); + + // Copy basic configuration + clone.setServiceClass(original.getServiceClass()); + clone.setBindingConfig(original.getBindingConfig()); + + // Copy handlers + clone.getHandlers().addAll(original.getHandlers()); + clone.getInInterceptors().addAll(original.getInInterceptors()); + clone.getOutInterceptors().addAll(original.getOutInterceptors()); + + return clone; + } + + /** + * Clear cache - recommended to call when application shuts down + */ + public static void clearCache() { + logger.info("Clearing OnvifServiceFactory cache, releasing {} cached entries", proxyCache.size()); + proxyCache.clear(); + } + + /** + * Get current cache size + */ + public static int getCacheSize() { + return proxyCache.size(); + } + + /** + * Check if specified service type is already cached + */ + public static boolean isCached(Class serviceClass) { + return proxyCache.containsKey(serviceClass.getName()); + } +} \ No newline at end of file diff --git a/onvif-java/src/main/java/de/onvif/soap/PullPointSubscriptionHandler.java b/onvif-java/src/main/java/de/onvif/soap/PullPointSubscriptionHandler.java index 94c6594..6a9823d 100644 --- a/onvif-java/src/main/java/de/onvif/soap/PullPointSubscriptionHandler.java +++ b/onvif-java/src/main/java/de/onvif/soap/PullPointSubscriptionHandler.java @@ -64,8 +64,22 @@ public class PullPointSubscriptionHandler { eventWs.createPullPointSubscription(cpps); final String serviceAddress = getWSAAddress(resp.getSubscriptionReference()); - pullPointSubscription = device.getServiceProxy((BindingProvider) device.eventService.getEventPort(), serviceAddress).create(PullPointSubscription.class); - subscriptionManager = device.getServiceProxy((BindingProvider) device.eventService.getEventPort(), serviceAddress).create(SubscriptionManager.class); + // Use OnvifServiceFactory instead of deprecated getServiceProxy method + BindingProvider eventServicePort = (BindingProvider) device.eventService.getEventPort(); + pullPointSubscription = OnvifServiceFactory.createServiceProxy( + eventServicePort, + serviceAddress, + PullPointSubscription.class, + device.securityHandler, + OnvifDevice.isVerbose() + ); + subscriptionManager = OnvifServiceFactory.createServiceProxy( + eventServicePort, + serviceAddress, + SubscriptionManager.class, + device.securityHandler, + OnvifDevice.isVerbose() + ); final Client pullPointSubscriptionProxy = ClientProxy.getClient(pullPointSubscription); final Client subscriptionManagerProxy = ClientProxy.getClient(subscriptionManager); @@ -102,7 +116,7 @@ public class PullPointSubscriptionHandler { var messageIdHdr = new Header(messageID, messageIDEl); var toHdr = new Header(to, toEl); var replyToHdr = new Header(replyTo, replyToEl); - + headers.clear(); headers.add(actionHdr); headers.add(messageIdHdr); diff --git a/onvif-java/src/test/java/org/onvif/client/MemoryOptimizationDemo.java b/onvif-java/src/test/java/org/onvif/client/MemoryOptimizationDemo.java new file mode 100644 index 0000000..6381bbf --- /dev/null +++ b/onvif-java/src/test/java/org/onvif/client/MemoryOptimizationDemo.java @@ -0,0 +1,201 @@ +package org.onvif.client; + +import de.onvif.soap.OnvifDevice; +import de.onvif.soap.OnvifServiceFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * ONVIF Device Memory Optimization Demo + * + * Demonstrates the effects of new lazy initialization and Schema caching features: + * 1. Schema Caching: Avoid repeated WSDL parsing + * 2. Lazy Initialization: Create services only when needed + * 3. Capabilities Caching: Avoid repeated network requests + * + * @author ONVIF Optimization Team + */ +public class MemoryOptimizationDemo { + private static final Logger LOG = LoggerFactory.getLogger(MemoryOptimizationDemo.class); + + public static void main(String[] args) throws IOException { + LOG.info("=== ONVIF Device Memory Optimization Demo ==="); + + OnvifCredentials creds = GetTestDevice.getOnvifCredentials(args); + if (creds == null) { + LOG.error("Please provide valid ONVIF device credentials"); + return; + } + + // Demo 1: Single device memory optimization + demonstrateSingleDeviceOptimization(creds); + + // Demo 2: Multi-device Schema caching effects + demonstrateMultiDeviceCaching(creds); + + // Demo 3: Lazy initialization effects + demonstrateLazyInitialization(creds); + + // Clean up resources + OnvifDevice.cleanupResources(); + LOG.info("=== Demo Completed ==="); + } + + /** + * Demonstrate memory optimization effects for single device + */ + private static void demonstrateSingleDeviceOptimization(OnvifCredentials creds) { + LOG.info("\n--- Demo 1: Single Device Memory Optimization ---"); + + try { + long startTime = System.currentTimeMillis(); + long startMemory = getUsedMemory(); + + // Create device instance + OnvifDevice device = new OnvifDevice(creds.getHost(), creds.getUser(), creds.getPassword()); + + long initTime = System.currentTimeMillis() - startTime; + long initMemory = getUsedMemory() - startMemory; + + LOG.info("šŸš€ Device initialization completed:"); + LOG.info(" ā±ļø Initialization time: {}ms", initTime); + LOG.info(" 🧠 Initial memory usage: {}MB", initMemory / 1024 / 1024); + LOG.info(" šŸ“Š Initialized services: {}/5", device.getInitializedServicesCount()); + LOG.info(" šŸ“‹ Device info: {}", device.getDeviceInfo()); + + // Access services on demand + LOG.info("\nšŸ“ŗ Accessing Media service..."); + if (device.getMedia() != null) { + LOG.info(" āœ… Media service initialized (lazy loading)"); + LOG.info(" šŸ“Š Currently initialized services: {}/5", device.getInitializedServicesCount()); + } + + LOG.info("\nšŸŽ® Checking PTZ service..."); + if (device.getPtz() != null) { + LOG.info(" āœ… PTZ service initialized (lazy loading)"); + } else { + LOG.info(" āŒ PTZ service not available"); + } + + LOG.info("\nšŸ“Š Final statistics:"); + LOG.info(" Initialized services: {}/5", device.getInitializedServicesCount()); + LOG.info(" Total memory usage: {}MB", (getUsedMemory() - startMemory) / 1024 / 1024); + + } catch (Exception e) { + LOG.error("Device connection failed: {}", e.getMessage()); + } + } + + /** + * Demonstrate multi-device Schema caching effects + */ + private static void demonstrateMultiDeviceCaching(OnvifCredentials creds) { + LOG.info("\n--- Demo 2: Multi-Device Schema Caching Effects ---"); + + List devices = new ArrayList<>(); + long totalStartTime = System.currentTimeMillis(); + long startMemory = getUsedMemory(); + + try { + // Create multiple device instances to demonstrate caching effects + int deviceCount = 3; // Create 3 connections to the same device for demo purposes + + for (int i = 0; i < deviceCount; i++) { + long deviceStartTime = System.currentTimeMillis(); + + OnvifDevice device = new OnvifDevice(creds.getHost(), creds.getUser(), creds.getPassword()); + devices.add(device); + + long deviceTime = System.currentTimeMillis() - deviceStartTime; + LOG.info("šŸ“± Device {} initialization time: {}ms (cache {})", + i + 1, deviceTime, OnvifServiceFactory.getCacheSize() > 0 ? "active" : "inactive"); + } + + long totalTime = System.currentTimeMillis() - totalStartTime; + long totalMemory = getUsedMemory() - startMemory; + + LOG.info("\nšŸ“ˆ Multi-device statistics:"); + LOG.info(" Device count: {}", deviceCount); + LOG.info(" Total initialization time: {}ms", totalTime); + LOG.info(" Average initialization time: {}ms", totalTime / deviceCount); + LOG.info(" Total memory usage: {}MB", totalMemory / 1024 / 1024); + LOG.info(" Average memory usage: {}MB", totalMemory / deviceCount / 1024 / 1024); + LOG.info(" Schema cache entries: {}", OnvifServiceFactory.getCacheSize()); + + } catch (Exception e) { + LOG.error("Multi-device creation failed: {}", e.getMessage()); + } + } + + /** + * Demonstrate lazy initialization effects + */ + private static void demonstrateLazyInitialization(OnvifCredentials creds) { + LOG.info("\n--- Demo 3: Lazy Initialization Effects ---"); + + try { + OnvifDevice device = new OnvifDevice(creds.getHost(), creds.getUser(), creds.getPassword()); + + LOG.info("šŸ—ļø Device creation completed, checking service initialization status:"); + logServiceStatus(device); + + LOG.info("\nšŸ“ŗ First access to Media service..."); + device.getMedia(); + logServiceStatus(device); + + LOG.info("\nšŸŽ® First access to PTZ service..."); + device.getPtz(); + logServiceStatus(device); + + LOG.info("\nšŸ–¼ļø First access to Imaging service..."); + device.getImaging(); + logServiceStatus(device); + + LOG.info("\nšŸ“” First access to Events service..."); + device.getEvents(); + logServiceStatus(device); + + LOG.info("\nšŸŽÆ Lazy initialization demo completed"); + LOG.info(" Advantage: Resources are allocated only when services are actually needed"); + LOG.info(" Result: Significantly reduced initial memory usage and startup time"); + + } catch (Exception e) { + LOG.error("Lazy initialization demo failed: {}", e.getMessage()); + } + } + + /** + * Log service initialization status + */ + private static void logServiceStatus(OnvifDevice device) { + LOG.info(" šŸ“Š Service initialization status:"); + LOG.info(" Device: {} | Media: {} | PTZ: {} | Imaging: {} | Events: {}", + device.isServiceInitialized("device") ? "āœ…" : "āŒ", + device.isServiceInitialized("media") ? "āœ…" : "āŒ", + device.isServiceInitialized("ptz") ? "āœ…" : "āŒ", + device.isServiceInitialized("imaging") ? "āœ…" : "āŒ", + device.isServiceInitialized("events") ? "āœ…" : "āŒ" + ); + LOG.info(" Total: {}/5 services initialized", device.getInitializedServicesCount()); + } + + /** + * Get current used memory + */ + private static long getUsedMemory() { + Runtime runtime = Runtime.getRuntime(); + runtime.gc(); // Suggest garbage collection for more accurate memory readings + return runtime.totalMemory() - runtime.freeMemory(); + } + + /** + * Format memory size + */ + private static String formatMemory(long bytes) { + return String.format("%.2f MB", bytes / 1024.0 / 1024.0); + } +} \ No newline at end of file