GDI रिसाव (या, बस बहुत अधिक GDI ऑब्जेक्ट्स का उपयोग) सबसे आम समस्याओं में से एक है। यह अंततः समस्याओं, त्रुटियों और/या प्रदर्शन समस्याओं को प्रस्तुत करने का कारण बनता है। लेख में बताया गया है कि हम इस समस्या को कैसे दूर करते हैं।
2016 में, जब अधिकांश कार्यक्रम सैंडबॉक्स में निष्पादित किए जाते हैं, जहां से सबसे अक्षम डेवलपर भी सिस्टम को नुकसान नहीं पहुंचा सकता है, मैं इस लेख में जिस समस्या के बारे में बात करूंगा, उसका सामना करने के लिए मैं चकित हूं। सच कहूं, तो मुझे उम्मीद थी कि Win32Api के साथ यह समस्या हमेशा के लिए चली जाएगी। फिर भी, मैंने इसका सामना किया। इससे पहले, मैंने इसके बारे में पुराने अधिक अनुभवी डेवलपर्स से डरावनी कहानियाँ सुनीं।
समस्या
भारी मात्रा में GDI वस्तुओं का रिसाव या उपयोग।
लक्षण
- कार्य प्रबंधक के विवरण टैब पर GDI ऑब्जेक्ट कॉलम महत्वपूर्ण 10000 दिखाता है (यदि यह कॉलम अनुपस्थित है, तो आप इसे टेबल हेडर पर राइट-क्लिक करके और कॉलम का चयन करके जोड़ सकते हैं)।
- सी# या सीएलआर द्वारा निष्पादित अन्य भाषाओं में विकसित होने पर, निम्न खराब सूचनात्मक त्रुटि उत्पन्न होती है:
संदेश:जीडीआई+ में एक सामान्य त्रुटि हुई।
स्रोत:System.Drawing
लक्ष्य साइट:IntPtr GetHbitmap(System.Drawing.Color)
प्रकार:System.Runtime.InteropServices.ExternalException
त्रुटि कुछ सेटिंग्स के साथ या कुछ सिस्टम संस्करणों में नहीं हो सकती है, लेकिन आपका एप्लिकेशन किसी एक ऑब्जेक्ट को रेंडर करने में सक्षम नहीं होगा: - С/С++ में विकास के दौरान, सभी GDI विधियाँ, जैसे Create%SOME_GDI_OBJECT%, NULL लौटाने लगीं।
क्यों?
विंडोज सिस्टम 65535 . से अधिक बनाने की अनुमति नहीं देते हैं जीडीआई ऑब्जेक्ट्स। यह संख्या, वास्तव में, प्रभावशाली है और मैं शायद ही एक सामान्य परिदृश्य की कल्पना कर सकता हूं जिसमें इतनी बड़ी मात्रा में वस्तुओं की आवश्यकता होती है। प्रक्रियाओं के लिए एक सीमा है - 10000 प्रति प्रक्रिया जिसे संशोधित किया जा सकता है (HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota को बदलकर मान 256 से 65535 की सीमा में है), लेकिन Microsoft इस सीमा को बढ़ाने की अनुशंसा नहीं करता है। यदि आप अभी भी ऐसा करते हैं, तो एक प्रक्रिया सिस्टम को फ्रीज करने में सक्षम होगी ताकि वह त्रुटि संदेश भी प्रस्तुत करने में असमर्थ हो। इस मामले में, सिस्टम को रीबूट करने के बाद ही पुनर्जीवित किया जा सकता है।
कैसे ठीक करें?
यदि आप एक आरामदायक और प्रबंधित सीएलआर दुनिया में रह रहे हैं, तो इस बात की बहुत अधिक संभावना है कि आपके आवेदन में सामान्य स्मृति रिसाव हो। समस्या अप्रिय है, लेकिन यह काफी सामान्य मामला है। इसका पता लगाने के लिए कम से कम एक दर्जन बेहतरीन उपकरण हैं। आपको यह देखने के लिए किसी भी प्रोफाइलर का उपयोग करने की आवश्यकता होगी कि क्या GDI संसाधनों को लपेटने वाली वस्तुओं की संख्या (Sytem.Drawing.Brush, Bitmap, Pen, Region, ग्राफ़िक्स) बढ़ जाती है। अगर ऐसा है, तो आप इस लेख को पढ़ना बंद कर सकते हैं। यदि आवरण वस्तुओं के रिसाव का पता नहीं चला था, तो आपका कोड सीधे GDI API का उपयोग करता है और ऐसा परिदृश्य होता है जब वे हटाए नहीं जाते हैं
दूसरों की क्या सलाह है?
इस विषय पर आधिकारिक Microsoft मार्गदर्शन या अन्य लेख आपको कुछ इस तरह की अनुशंसा करेंगे:
सभी खोजें बनाएं %SOME_GDI_OBJECT% और पता लगाएं कि क्या संबंधित DeleteObject (या एचडीसी ऑब्जेक्ट्स के लिए रिलीजडीसी) मौजूद है। अगर ऐसा है डिलीटऑब्जेक्ट मौजूद है, ऐसा परिदृश्य हो सकता है जो इसे कॉल नहीं करता है।
इस पद्धति का थोड़ा बेहतर संस्करण है जिसमें एक अतिरिक्त चरण शामिल है:
GDIView उपयोगिता डाउनलोड करें। यह प्रकार के अनुसार GDI ऑब्जेक्ट्स की सटीक संख्या दिखा सकता है। ध्यान दें कि वस्तुओं की कुल संख्या अंतिम कॉलम में मान के अनुरूप नहीं है। लेकिन हम इस पर आंखें बंद कर सकते हैं अगर यह खोज के क्षेत्र को कम करने में मदद करता है।
मैं जिस प्रोजेक्ट पर काम कर रहा हूं उसका कोड बेस 9 मिलियन रिकॉर्ड्स का है, लगभग इतनी ही मात्रा में रिकॉर्ड्स थर्ड-पार्टी लाइब्रेरी में स्थित हैं, GDI फंक्शन की सैकड़ों कॉल्स जो दर्जनों फाइलों में फैली हुई हैं। इससे पहले कि मैं यह समझ पाता कि दोषों के बिना मैन्युअल विश्लेषण असंभव है, मैंने बहुत समय और ऊर्जा बर्बाद की थी।
मैं क्या पेशकश कर सकता हूं?
यदि यह विधि आपको बहुत लंबी और थकाऊ लगती है, तो आप निराशा के सभी चरणों को पिछले एक के साथ पार नहीं कर पाए हैं। आप पिछले चरणों का पालन करने का प्रयास कर सकते हैं, लेकिन अगर यह मदद नहीं करता है, तो इस समाधान के बारे में मत भूलना।
रिसाव की खोज में, मैंने खुद से सवाल किया:रिसने वाली वस्तुएं कहां बनाई गई हैं? उन सभी जगहों पर ब्रेकप्वाइंट सेट करना असंभव था जहां एपीआई फ़ंक्शन कहा जाता है। इसके अलावा, मुझे यकीन नहीं था कि यह .NET फ्रेमवर्क में या हमारे द्वारा उपयोग किए जाने वाले किसी तीसरे पक्ष के पुस्तकालयों में नहीं होता है। गुगलिंग के कुछ मिनटों ने मुझे एपीआई मॉनिटर उपयोगिता की ओर अग्रसर किया जिसने सभी सिस्टम कार्यों में कॉल लॉग और ट्रेस करने की अनुमति दी। मुझे आसानी से उन सभी कार्यों की सूची मिल गई है जो जीडीआई ऑब्जेक्ट्स उत्पन्न करते हैं, उन्हें एपीआई मॉनिटर में स्थित और चयनित करते हैं। फिर, मैं ब्रेकप्वाइंट सेट करता हूं।
उसके बाद, मैंने डिबगिंग प्रक्रिया को इसमें चलाया विजुअल स्टूडियो और इसे प्रोसेस ट्री में चुना। पांचवें ब्रेकप्वाइंट ने तुरंत काम किया है:
मुझे एहसास हुआ कि मैं इस धार में डूब जाऊंगा और मुझे कुछ और चाहिए। मैंने कार्यों से ब्रेकप्वाइंट हटा दिए और लॉग देखने का फैसला किया। इसमें हजारों कॉल दिखाए गए। यह स्पष्ट हो गया कि मैं उनका मैन्युअल रूप से विश्लेषण नहीं कर पाऊंगा।
कार्य GDI फ़ंक्शन के उन कॉल्स को ढूंढना है जो विलोपन का कारण नहीं बनते हैं . लॉग में मुझे जो कुछ भी चाहिए था:कालानुक्रमिक क्रम में फ़ंक्शन कॉल की सूची, उनके लौटाए गए मान और पैरामीटर। इसलिए, मुझे Create%SOME_GDI_OBJECT% फ़ंक्शन का लौटाया गया मान प्राप्त करने और इस मान के साथ DeleteObject की कॉल को तर्क के रूप में ढूंढने की आवश्यकता है। मैंने एपीआई मॉनिटर में सभी रिकॉर्ड्स का चयन किया, उन्हें एक टेक्स्ट फाइल में डाला और सीएसवी जैसा कुछ टैब डिलीमीटर के साथ मिला। मैंने वीएस चलाया, जहां मेरा इरादा पार्सिंग के लिए एक छोटा प्रोग्राम लिखना था, लेकिन इससे पहले कि यह लोड हो सके, मेरे दिमाग में एक बेहतर विचार आया:डेटाबेस में डेटा निर्यात करने के लिए और मुझे जो चाहिए उसे खोजने के लिए एक प्रश्न लिखने के लिए। यह सही विकल्प था क्योंकि इसने मुझे जल्दी से प्रश्न पूछने और उत्तर प्राप्त करने की अनुमति दी।
CSV से डेटाबेस में डेटा आयात करने के लिए कई उपकरण हैं, इसलिए मैं इस विषय (mysql, mssql, sqlite) पर ध्यान नहीं दूंगा।
मेरे पास निम्न तालिका है:
CREATE TABLE apicalls ( id int(11) DEFAULT NULL, `Time of Day` datetime DEFAULT NULL, Thread int(11) DEFAULT NULL, Module varchar(50) DEFAULT NULL, API varchar(200) DEFAULT NULL, `Return Value` varchar(50) DEFAULT NULL, Error varchar(100) DEFAULT NULL, Duration varchar(50) DEFAULT NULL )
मैंने एपीआई कॉल से हटाए गए ऑब्जेक्ट का डिस्क्रिप्टर प्राप्त करने के लिए निम्नलिखित MySQL फ़ंक्शन लिखा है:
CREATE FUNCTION getHandle(api varchar(1000)) RETURNS varchar(100) CHARSET utf8 BEGIN DECLARE start int(11); DECLARE result varchar(100); SET start := INSTR(api,','); -- for ReleaseDC where HDC is second parameter. ex: 'ReleaseDC ( 0x0000000000010010, 0xffffffffd0010edf )' IF start = 0 THEN SET start := INSTR(api, '('); END IF; SET result := SUBSTRING_INDEX(SUBSTR(api, start + 1), ')', 1); RETURN TRIM(result); END
और अंत में, मैंने सभी मौजूदा वस्तुओं का पता लगाने के लिए एक प्रश्न लिखा:
SELECT creates.id, creates.handle chandle, creates.API, dels.API deletedApi FROM (SELECT a.id, a.`Return Value` handle, a.API FROM apicalls a WHERE a.API LIKE 'Create%') creates LEFT JOIN (SELECT d.id, d.API, getHandle(d.API) handle FROM apicalls d WHERE API LIKE 'DeleteObject%' OR API LIKE 'ReleaseDC%' LIMIT 0, 100) dels ON dels.handle = creates.handle WHERE creates.API LIKE 'Create%';
(मूल रूप से, यह केवल सभी कॉल बनाएं के लिए सभी हटाएं कॉल ढूंढेगा)।
जैसा कि आप ऊपर की छवि से देख सकते हैं, बिना किसी डिलीट के सभी कॉल एक ही बार में मिल गए हैं।
तो, अंतिम प्रश्न छोड़ दिया गया है:यह कैसे निर्धारित किया जाए कि मेरे कोड के संदर्भ में इन विधियों को कहां से कहा जाता है? और यहाँ एक फैंसी ट्रिक ने मेरी मदद की:
- डिबगिंग के लिए वीएस में एप्लिकेशन चलाएं
- इसे एपी मॉनिटर में ढूंढें, और इसे चुनें।
- एपीआई में एक आवश्यक फ़ंक्शन का चयन करें और एक ब्रेकपॉइंट रखें।
- 'अगला' पर तब तक क्लिक करते रहें, जब तक कि इसे विचाराधीन पैरामीटर के साथ नहीं बुलाया जाएगा (मैं वास्तव में वीएस से सशर्त ब्रेकप्वाइंट से चूक गया)
- जब आप आवश्यक कॉल पर आएं, तो CS पर स्विच करें और सभी को तोड़ें . पर क्लिक करें ।
- वीएस डीबगर को वहीं रोक दिया जाएगा जहां लीक करने वाली वस्तु बनाई गई है और आपको केवल यह पता लगाना है कि इसे क्यों नहीं हटाया गया है।
नोट:कोड उदाहरण के लिए लिखा गया है।
सारांश:
वर्णित एल्गोरिथम जटिल है और इसके लिए कई उपकरणों की आवश्यकता होती है, लेकिन इसने विशाल कोड आधार के माध्यम से एक गूंगा खोज की तुलना में बहुत तेजी से परिणाम दिया।
यहां सभी चरणों का सारांश दिया गया है:
- जीडीआई रैपर ऑब्जेक्ट की मेमोरी लीक की खोज करें।
- यदि वे मौजूद हैं, तो उन्हें हटा दें और चरण 1 दोहराएं।
- यदि कोई लीक नहीं है, तो एपीआई कार्यों के लिए स्पष्ट रूप से कॉल खोजें।
- यदि उनकी मात्रा अधिक नहीं है, तो ऐसी स्क्रिप्ट की खोज करें जहां कोई वस्तु नष्ट न हो।
- यदि उनकी मात्रा बड़ी है या उनका पता लगाना मुश्किल है, तो एपीआई मॉनिटर डाउनलोड करें और इसे जीडीआई फ़ंक्शन के लॉगिंग कॉल के लिए सेट करें।
- वीएस में डिबगिंग के लिए एप्लिकेशन चलाएं।
- रिसाव को पुन:उत्पन्न करें (यह कैश की गई वस्तुओं को छिपाने के लिए कार्यक्रम को प्रारंभ करेगा)।
- एपीआई मॉनिटर से कनेक्ट करें।
- रिसाव को पुन:उत्पन्न करें।
- लॉग को टेक्स्ट फ़ाइल में कॉपी करें, इसे किसी भी डेटाबेस में आयात करें (इस आलेख में वर्णित स्क्रिप्ट MySQL के लिए हैं, लेकिन उन्हें किसी भी रिलेशनल डेटाबेस प्रबंधन प्रणाली के लिए आसानी से अपनाया जा सकता है)।
- बनाएं और हटाएं विधियों की तुलना करें (आप ऊपर इस लेख में SQL स्क्रिप्ट पा सकते हैं), और कॉल हटाएं के बिना विधियां खोजें।
- आवश्यक विधि के कॉल पर API मॉनिटर में एक ब्रेकप्वाइंट सेट करें।
- जारी रखें पर तब तक क्लिक करते रहें जब तक कि विधि को पुनः प्राप्त किए गए मापदंडों के साथ नहीं बुलाया जाता।
- जब आवश्यक पैरामीटर के साथ विधि को कॉल किया जाता है, तो VS में ब्रेक ऑल पर क्लिक करें।
- पता लगाएं कि यह ऑब्जेक्ट क्यों नहीं हटाया जाता है।
मुझे उम्मीद है कि यह लेख उपयोगी होगा और आपको अपना समय बचाने में मदद करेगा।