Handle leak באפליקציית מצלמה תעשייתית שקורסת אחרי ריצה ארוכה
· עודכן בתאריך: · Go Komura · פיתוח Windows, חקירת תקלות, מצלמה תעשייתית, handle leak, תכנון לוגים
היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 11 Mar 2026)
- פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173352)
מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.
Go Komura (2026). Handle leak באפליקציית מצלמה תעשייתית שקורסת אחרי ריצה ארוכה. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173352 https://comcomponent.com/he/blog/handle-leak-industrial-camera-long-run-crash-part1/
- DOI (הגרסה האחרונה)
- 10.5281/zenodo.22173352
- DOI (הגרסה הזו)
- 10.5281/zenodo.22173353
כשאפליקציית Windows קורסת פתאום אחרי ריצה ארוכה, נוטים מאוד לחשוד קודם ב-memory leak. עם זאת, לא מעטים המקרים שבהם האשם האמיתי הוא handle leak, שרק אחרי שבועות מתגלה סוף סוף כתקלה משנית.
המקרה שמוצג כאן הוא חקירה של אפליקציית Windows ששולטת במצלמה תעשייתית, וקורסת פתאום אחרי כחודש של הפעלה רציפה. אחרי פירוק, התברר שהסיבה הייתה handle leak ב-failure path סביב reconnect של המצלמה.
בפרק הראשון נסביר מה זה handle leak, איך פירקנו את המקרה הזה, ואילו לוגים כדאי להשאיר כדי שזה לא יחזור. בפרק השני נדבר על תשתית בדיקות למקרי קצה, תחת הכותרת תשתית בדיקות למקרי קצה ב-Windows עם Application Verifier.
שמות פרטיים וחלק מפריטי הלוג הוסתרו, אבל אופן החשיבה עצמו משותף במידה רבה לכלל אפליקציות בקרת ההתקנים ב-Windows.
תוכן עניינים
- השורה התחתונה
- מה זה handle leak
- 2.1. ה-handle שמדובר בו כאן
- 2.2. למה זה נוטה להתגלות רק אחרי ריצה ארוכה
- 2.3. ההבדל מ-memory leak
- מקרה: אפליקציית בקרת מצלמה תעשייתית שקורסת פתאום אחרי חודש
- 3.1. הסימפטומים שהופיעו
- 3.2. המדדים הראשונים שנבדקו
- 3.3. מקום הדליפה שהיה הגורם האמיתי
- איך פירקנו את זה
- 4.1. לקצר את הזמן בלי לחכות לשחזור בסדר גודל של חודשים
- 4.2. הסתכלות לפי ה-slope של
Handle Count - 4.3. הסתכלות על ההתאמה בין
create/openלביןclose/dispose - 4.4. ב-handle leak מחפשים את “מקום הדליפה” ולא את “מקום הקריסה”
- הלוגים שצריך כדי שזה לא יחזור
- 5.1. הסט המינימלי שכדאי להשאיר קודם
- 5.2. הלוגים שחיזקנו בפועל
- 5.3. באיזו רזולוציה לתעד
- מתי מה לבדוק, בגדול
- סיכום
- מקורות
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 18, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
1. השורה התחתונה
- באפליקציית בקרה שקורסת רק אחרי ריצה ארוכה, יש להסתכל תמיד לא רק על
Private Bytesאלא גם עלHandle Count - handle leak נוטה להסתתר לא ב-success path, אלא במסלולים של
timeout/reconnect/ כשל באמצע הפעולה /early return - השורה שבה בפועל קורסת האפליקציה היא לרוב לא המקום שבו דלף ה-handle, אלא המקום שבו כבר לא ניתן היה ליצור handle חדש
- הלוגים הראשונים שנדרשים הם: הקשר של
operation/session, ה-handle countשל ה-process, ההתאמה ביןopenל-closeשל המשאב, ושגיאות Win32 / HRESULT / SDK - עדיף להריץ בלולאה קצרה אלפי פעמים חיבור, ניתוק, reconnect ומסלולי כשל, מאשר לחכות לשחזור בסדר גודל של חודשים
- Application Verifier, שיוזכר בפרק השני, יעיל מאוד, אבל לפני כן הבסיס הוא לוודא שאפשר לעקוב אחרי קריסת lifetime באמצעות הלוגים שלכם
בקיצור, מה שצריך לעשות קודם במקרים כאלה הוא לא להסתכל על עצם העובדה ש”זה קרס אחרי זמן ארוך”, אלא להפוך את קצב הגידול של המשאבים ואת מסלולי הכשל לניתנים לתצפית.
handle leak לרוב כבר נראה כתקלה משנית ברגע שמגלים אותו. לכן, אם מסתכלים רק על ה-exception ברגע הקריסה, קל ללכת בכיוון הלא נכון.
flowchart TB
accTitle: המבנה של מה שכדאי לעשות קודם
accDescr: הסתכלות רק על ה-exception ברגע הקריסה נוטה להוביל לכיוון הלא נכון, בעוד שהפיכת קצב גידול המשאבים ומסלולי הכשל לניתנים לתצפית מובילה לזהות האמיתית של הדליפה שמתחפשת לתקלה משנית.
crash["הסתכלות רק על ה-exception ברגע הקריסה"] -.-> wrong["הליכה לכיוון הלא נכון"]
obs["תצפית על קצב הגידול ומסלולי הכשל"] --> right["הגעה לזהות האמיתית של הדליפה"]
איור 1: לפני שמסתכלים על “עובדת הקריסה”, כדאי להפוך את קצב הגידול ומסלולי הכשל לניתנים לתצפית.
2. מה זה handle leak
2.1. ה-handle שמדובר בו כאן
ה-handle שמדובר בו כאן הוא מזהה שדרכו process ב-Windows מפנה למשאב של מערכת ההפעלה. דוגמאות למשאבים כאלה:
| קטגוריה | דוגמאות |
|---|---|
| kernel objects | event, mutex, semaphore, thread, process, waitable timer |
| I/O | open עבור file, pipe, socket, device |
| דברים נפוצים בבקרת התקנים | event פנימי של ה-SDK של המצלמה, wait object שמקושר לרישום callback, handles שקשורים ל-thread הצילום |
בפרט באפליקציות בקרה, הדפוס הבעייתי ביותר הוא “לשכוח לסגור משאב שנפתח זמנית לצורך פעולה מסוימת, ב-failure path באמצע הפעולה”.
התהליך האופייני נראה כך:
- בכל reconnect נוצר event אחד
- רישום ה-callback או תחילת הצילום נכשלים באמצע
- ב-success path מתבצע close, אבל ב-failure path לא
- בבדיקות קצרות שגרתיות עוברים בעיקר ב-success path, ולכן זה מתפספס
דפוס מהסוג הזה מסתתר בקלות רבה, גם ב-code review וגם בהפעלה בפועל.
flowchart TB
accTitle: תבנית אופיינית של דליפה רק ב-failure path
accDescr: בכל reconnect נוצר event, ואם רישום ה-callback או תחילת הצילום מצליחים מתבצע close ב-success path, אך ב-failure path באמצע לא מתבצע close והדליפה מתפספסת בבדיקות קצרות שעוברות בעיקר ב-success path.
ev["בכל reconnect נוצר event"] --> q{"הרישום וההתחלה הצליחו?"}
q -->|"הצלחה"| close["close ב-success path"]
q -->|"כשל באמצע"| leak["אין close ב-failure path"]
leak -.-> unseen["בדיקות קצרות מפספסות, כי הן עוברות ב-success path"]
איור 2: שכחת סגירת משאב שנפתח זמנית ב-failure path באמצע. צורה נפוצה במיוחד באפליקציות בקרה.
2.2. למה זה נוטה להתגלות רק אחרי ריצה ארוכה
handle leak לא בהכרח שובר הכול בבת אחת בפעם הראשונה. מה שבאמת מטריד הוא דליפה עם slope קטן, למשל דליפה של יחידה אחת בלבד בכל כשל בודד.
flowchart LR
accTitle: השרשרת מהפעלה רגילה ועד לקריסה בעקבות דליפה קטנה
accDescr: בהפעלה רגילה מתרחשים מדי פעם timeout או reconnect שיוצרים Event Handle ב-failure path בלי שנקרא CloseHandle, כך ש-Handle Count גדל מעט בכל פעם, וזה חוזר על עצמו מאות פעמים עד ש-CreateEvent או פתיחת ה-SDK נכשלים וגורמים לקריסה או עצירה במקום אחר.
A["הפעלה רגילה"] --> B["מדי פעם timeout / reconnect"]
B --> C["יצירת Event Handle ב-failure path"]
C --> D["CloseHandle לא נקרא"]
D --> E["Handle Count גדל מעט"]
E --> F["חוזר על עצמו מאות פעמים"]
F --> G["CreateEvent / פתיחת ה-SDK נכשלים"]
G --> H["קריסה / עצירה במקום אחר"]
איור 3: דליפה קטנה של יחידה אחת בכל פעם, מצטברת מאות פעמים בתנאי הגבול של הפעלת 24/7, ואז מתגלה.
אם בכל reconnect דולף רק handle אחד, לא קורה שום דבר תוך כמה דקות. אבל באפליקציית בקרת התקנים שרצה 24/7, תנאי גבול כמו timeout, אתחול מחדש והתאוששות מניתוק קורים שוב ושוב. כתוצאה מכך, נוצר מראה מוזר: התקלה מתגלה רק אחרי שבועות.
החשוב כאן הוא ש-handle leak עצמו לא בהכרח הופך לשורת הקריסה. בדרך כלל האפליקציה נשברת כך:
- קריאת API שיוצרת event / file / thread חדש נכשלת
- ה-SDK לא מצליח ליצור פנימית משאב נדרש, ומחזיר רק קוד כשל כללי
- טיפול השגיאות אחרי הכשל דל, והאפליקציה דורכת על
nullאו על handle לא תקין ונופלת - מספר ה-timeout גדל, ובסופו של דבר watchdog או בקרה מסדר גבוה יותר הורגים את ה-process
כלומר, נקודת הקריסה היא ה”קורבן האחרון”, ולאו דווקא “החשוד הראשון”.
flowchart TB
accTitle: נקודת הקריסה היא הקורבן האחרון
accDescr: דליפה מתמשכת של handle במקום כלשהו מובילה בסוף לכשל של API שיוצר משאב חדש, שמתגלה כקריסה או עצירה במקום אחר עם טיפול שגיאות דל, ולכן השורה שקורסת היא רק הקורבן האחרון ולא החשוד הראשון.
leak2["דליפה מתמשכת של handle במקום כלשהו"] --> fail["כשל ב-API שיוצר משאב חדש"]
fail --> vict["קריסה או עצירה במקום אחר"]
vict -.-> note["השורה שקורסת היא רק הקורבן האחרון"]
איור 4: הדליפה עצמה לא בהכרח הופכת לשורת הקריסה. בדרך כלל השבירה היא תקלה משנית.
כאן עולה שאלה תמימה: למה זה קורס בגלל בסך הכול כמה אלפי handles?
אם מסתכלים רק על המספרים, הגבול העליון רחוק מאוד. הגבול העליון התיאורטי ל-handles של kernel objects הוא 2^24 (כ-16.77 מיליון) ל-process. אבל מכיוון שה-handles ממוקמים ב-page pool, המספר שבפועל ניתן ליצור נקבע לפי הזיכרון הזמין, וב-Windows בגרסת 32-bit המספר קטן בהרבה מהערך התיאורטי.
בקיצור, מקרים שבהם מגיעים לגבול התיאורטי הם דווקא המיעוט. מה שבפועל “נתקע בתקרה קודם” הוא בדרך כלל אחד מאלה:
| מה נתקע בתקרה קודם | סדר גודל | היכן זה משפיע |
|---|---|---|
| GDI objects | תיאורטית 65,536 לכל session, ובנוסף יש גבול ברירת מחדל ל-process שניתן לשנות בערך GDIProcessHandleQuota ב-Registry, בטווח של 256 עד 65,536 |
אפליקציות שיש בהן גם GUI. בקלות רבה נתקעים בתקרה הזו כבר בסדר גודל של אלפים |
| טבלת הניהול הפנימית של ה-SDK | תלוי ביצרן | טבלת ה-handles או המערך באורך קבוע שה-SDK של המצלמה מחזיק פנימית מתמלאים קודם |
| משאבי kernel כמו ה-page pool | משותפים לכל המכונה | כשגם משאבים אחרים חוץ מ-handles נצרכים באותו זמן |
| מרחב הכתובות הווירטואלי של process ב-32-bit | 2GB / 3GB | לא ה-handle עצמו, אלא ה-buffer שמוקצה יחד איתו הוא מה שמשפיע |
כלומר, הקריאה “עדיין יש מרווח עד הגבול, אז זה בסדר” לא תקפה. יש להסתכל לא על השאלה אם מגיעים לגבול, אלא על האם מה שאמור לחזור אכן חוזר. ברגע שנוצר slope, בטוח יותר להניח כבר שמדובר בחריגה.
flowchart TB
accTitle: מה נתקע בתקרה לפני הגבול התיאורטי
accDescr: המקרים שבהם מגיעים לגבול התיאורטי של kernel handles הם מיעוט, ובפועל נתקעים קודם בגבול GDI objects, בטבלת הניהול הפנימית של ה-SDK, או במרחב הכתובות של process ב-32-bit, ולכן יש להסתכל אם המשאב חוזר ולא על הגבול עצמו.
limit["הגבול התיאורטי של ה-kernel כ-16.77 מיליון"] -.-> rare["הגעה עד לשם היא המיעוט"]
rare -.-> first["מה שבפועל נתקע קודם"]
first --> g1["הגבול של GDI"]
first --> g2["טבלת הניהול הפנימית של ה-SDK"]
first --> g3["מרחב הכתובות של 32-bit"]
g1 --> see["שיפוט לפי האם המשאב חוזר"]
g2 --> see
g3 --> see
איור 5: “יש עוד מרווח עד הגבול” לא מרגיע. ברגע שנוצר slope, יש להתייחס לזה כאל חריגה.
2.3. ההבדל מ-memory leak
בתקלה שאחרי ריצה ארוכה, נוטים לחשוד קודם ב-memory leak. זה כמובן טבעי, אבל לעיתים מהיר יותר להסתכל על handle leak דרך ציר אחר.
| היבט | memory leak | handle leak |
|---|---|---|
| המדד הראשון שבודקים | Private Bytes, Commit, Working Set |
Handle Count |
| סימפטום אופייני | לחץ זיכרון, paging, האטה, OOM | כשל ב-Create* / Open* / אתחול פנימי של ה-SDK, תקלה משנית |
| היכן זה נוטה להסתתר | cache, החזקת הפניה, שכחת שחרור | אי-סימטריה בין create/open לבין close/dispose |
| איך זה נראה | הזיכרון גדל בהדרגה | handle count גדל בהדרגה ולא חוזר |
לכן, בפירוק תקלה אחרי ריצה ארוכה, הסתכלות רק על הזיכרון נוטה להיות כמו לראות רק חצי מהתמונה.
לפחות Handle Count ו-Thread Count כדאי להסתכל עליהם יחד, וזה מקל מאוד על הסידור.
flowchart TB
accTitle: מדדים שכדאי להסתכל עליהם יחד בפירוק ריצה ארוכה
accDescr: memory leak ו-handle leak נבדקים במדדים שונים, ולכן הסתכלות רק על מדדי זיכרון כמו Private Bytes אינה מספיקה. יש להסתכל יחד גם על Handle Count וגם על Thread Count כדי להימנע ממצב של תמונה חלקית.
watch["פירוק תקלה אחרי ריצה ארוכה"] --> m1["מדדי זיכרון (כמו Private Bytes)"]
watch --> m2["Handle Count"]
watch --> m3["Thread Count"]
m2 -.-> hint["גדל ולא חוזר — סימן ל-handle leak"]
איור 6: הסתכלות רק על הזיכרון היא תמונה חלקית. יש לעקוב באותו מסך גם אחרי מספר ה-handles וה-threads.
3. מקרה: אפליקציית בקרת מצלמה תעשייתית שקורסת פתאום אחרי חודש
3.1. הסימפטומים שהופיעו
התופעה הייתה פשוטה:
- אפליקציית Windows ששולטת במצלמה תעשייתית פועלת 24/7
- בזמן רגיל היא פועלת כרגיל
- אחרי כחודש, יום אחד היא קורסת פתאום
- אחרי restart, שוב פועלת כרגיל למשך זמן מה
מה שמקשה בהתחלה הוא “שלוקח הרבה זמן עד שזה קורס”. לחכות חודש לכל שחזור בודד זה קשה מאוד לחקירה.
מה שהקשה עוד יותר הוא שמקום הקריסה לא היה בדיוק זהה בכל פעם. לפעמים זה קרה מיד אחרי תחילת reconnect, לפעמים בתחילת הצילום, ולפעמים אחרי כשל בקריאה ל-SDK.
עם הופעה כזו, בהתחלה אפשר לחשוד בכל אחד מאלה:
- חוסר יציבות בצד ה-SDK של המצלמה
- תקלה זמנית שמקורה בתקשורת או בניתוק ההתקן
- memory leak
- race סביב threads
- כשל אתחול שלא נכנס ללוג
כלומר, המצב היה שיש יותר מדי “דברים שנראים חשודים איכשהו”.
flowchart TB
accTitle: שני הדברים שהקשו על החקירה במקרה הזה
accDescr: קריסה פתאומית אחרי כחודש של הפעלה רציפה גורמת לכך שכל שחזור לוקח חודש, ומקום הקריסה לא זהה בדיוק בכל פעם, ושני הדברים יחד יצרו מצב שבו יש יותר מדי מועמדים חשודים כמו SDK, תקשורת וזיכרון.
sym["קריסה פתאום אחרי כחודש"] --> hard1["כל שחזור לוקח חודש"]
sym --> hard2["מקום הקריסה קצת שונה בכל פעם"]
hard2 -.-> many["יותר מדי מועמדים חשודים"]
איור 7: כש”לוקח הרבה זמן עד שזה קורס” ו”מקום הקריסה נודד” מצטרפים יחד, אי אפשר להתקדם בניחוש.
3.2. המדדים הראשונים שנבדקו
לכן, הדבר הראשון שנעשה היה הסתכלות על קצב הגידול של המשאבים ב-process כולו. במקרה הזה, תוצאות התצפית הראו בערך את המגמה הבאה:
| מדד | המגמה שנצפתה | הפרשנות |
|---|---|---|
Handle Count |
גדל מעט בכל פעם אחרי reconnect או timeout, ולא חוזר | חשד ל-handle leak |
Private Bytes |
יש עליות וירידות, אבל ה-slope החד-כיווני חלש | ייתכן שהאשם אינו ה-heap |
Thread Count |
כמעט קבוע | סיכוי נמוך ל-thread leak |
| מקום הקריסה | קצת שונה בכל פעם | סביר להניח שזו תקלה משנית |
בשלב הזה, נקודת המבט כבר הצטמצמה משמעותית. מכיוון שהיה טבעי יותר להסתכל על זה לא כ”קורס אחרי חודש”, אלא כ”דולף משהו בהדרגה במהלך הדרך, ובעקבות זה קורס אחרי חודש”.
flowchart TB
accTitle: ההשערה שהצטמצמה מתצפית המדדים הראשונית
accDescr: התצפית שלפיה רק Handle Count גדל ולא חוזר, slope של Private Bytes חלש, Thread Count כמעט קבוע, ומקום הקריסה שונה בכל פעם, הובילה להצטמצמות ההשערה לכיוון של דליפה הדרגתית שמובילה לקריסה אחרי חודש.
o1["Handle Count גדל ולא חוזר"] --> narrow["נקודת המבט מצטמצמת"]
o2["slope של Private Bytes חלש"] --> narrow
o3["Thread Count כמעט קבוע"] --> narrow
narrow --> view["דליפה הדרגתית שקורסת אחרי חודש"]
איור 8: כשמסדרים יחד את הצורה של ארבעת המדדים, מתגלה לא “קורס אחרי חודש” אלא “דולף כל הזמן”.
3.3. מקום הדליפה שהיה הגורם האמיתי
בסופו של דבר, הגורם היה שכחת close של event handle שנוצר ב-failure path של האתחול בזמן reconnect של המצלמה.
אם מפשטים את התהליך, זה נראה כך:
sequenceDiagram
accTitle: התהליך שבו הדליפה ב-failure path של ה-reconnect מובילה לקריסה
accDescr: האפליקציה יוצרת Event, נכשלת ברישום ה-callback, יוצאת ב-failure path בלי לקרוא CloseHandle, ו-Handle Count גדל בהדרגה עם כל reconnect עד שקריאה חדשה ל-CreateEvent או ל-Open נכשלת וגורמת לקריסה כתקלה משנית.
participant App as אפליקציית הבקרה
participant OS as Windows
participant SDK as ה-SDK של המצלמה
App->>OS: CreateEvent
App->>SDK: רישום callback
SDK-->>App: כשל באמצע / timeout
Note over App: יציאה (return) ב-failure path
Note over App: CloseHandle לא נקרא
loop reconnect שוב ושוב
App->>OS: Handle Count גדל מעט בכל פעם
end
App->>OS: CreateEvent / Open הבא
OS-->>App: כשל
App-->>App: קריסה כתקלה משנית
איור 9: הגורם האמיתי היה שכחת close של event handle ב-failure path של ה-reconnect. אחרי הצטברות, הקריסה קורית במקום אחר.
כדי לתת מושג לגבי הקוד, זו הצורה של הדליפה:
handle = CreateEvent(...)
if (!RegisterCallback(handle))
{
return Error; // CloseHandle(handle) חסר כאן
}
if (!StartAcquisition())
{
return Error; // גם כאן חסר close
}
...
CloseHandle(handle)
גם הסיבה לכך שקל לפספס את זה בבדיקות קצרות די ברורה:
- הפעלה תקינה -> סיום תקין: מתבצע close
- הכשל קורה רק באמצע reconnect
- אין בדיקה שעוברת בכמות גדולה ב-failure path הזה
- בסביבת הייצור זה מצטבר בהדרגה במשך שבועות
כלומר, המבנה היה: “לא נראה אם מסתכלים רק על ה-success path, אבל דולף באופן שגרתי ב-failure path”.
כיוון התיקון לא מרשים במיוחד:
- לקרב את האחריות של
create/openלזו שלclose/dispose - להעביר את השחרור ל-
finally/ destructor / אובייקט ה-session, כך שהשחרור יתבצע תמיד גם בכשל באמצע - להבהיר את ה-ownership לפני ואחרי רישום ה-callback או תחילת הצילום
- לבטא את “מי סוגר” באחריות הקוד עצמו, ולא ב-comments
flowchart TB
accTitle: עיקרי כיוון התיקון
accDescr: כיוון התיקון הוא לקרב את האחריות של create לזו של close, להעביר את השחרור ל-finally, לדסטרוקטור או לאובייקט ה-session כדי שהוא יתבצע תמיד גם בכשל באמצע, ולבטא את מי שסוגר באחריות הקוד ולא בהערות.
pol["כיוון התיקון"] --> p1["קירוב האחריות של create ו-close"]
pol --> p2["העברת השחרור ל-finally או לדסטרוקטור"]
pol --> p3["ביטוי הבעלות באחריות הקוד"]
p3 -.-> nc["לא להסתמך על מוסכמה שמבוססת רק על comments"]
איור 10: זה לא תיקון מרשים. משתילים את מחזור החיים של המשאב במבנה הקוד עצמו.
מכיוון שקשה להבין רק ממילים, נשאיר כאן גם את אותו עיבוד לאחר שכתוב.
ב-C++, מכינים טיפוס RAII קטן אחד שמחזיק את ה-handle, כך שלא משאירים HANDLE גולמי בתוך הפונקציה.
// C++17 / Windows
#include <windows.h>
#include <utility>
class UniqueHandle
{
public:
UniqueHandle() noexcept = default;
explicit UniqueHandle(HANDLE h) noexcept : h_(h) {}
UniqueHandle(const UniqueHandle&) = delete;
UniqueHandle& operator=(const UniqueHandle&) = delete;
UniqueHandle(UniqueHandle&& other) noexcept
: h_(std::exchange(other.h_, nullptr)) {}
UniqueHandle& operator=(UniqueHandle&& other) noexcept
{
if (this != &other)
{
reset(std::exchange(other.h_, nullptr));
}
return *this;
}
~UniqueHandle() { reset(); }
HANDLE get() const noexcept { return h_; }
explicit operator bool() const noexcept { return h_ != nullptr; }
void reset(HANDLE h = nullptr) noexcept
{
if (h_ != nullptr)
{
::CloseHandle(h_);
}
h_ = h;
}
private:
HANDLE h_ = nullptr;
};
בעזרת זה, אין צורך להוסיף CloseHandle באופן ידני בכל failure path.
// שדה ב-CameraSession: UniqueHandle frameReady_;
bool CameraSession::Reconnect()
{
UniqueHandle frameReady{ ::CreateEventW(nullptr, TRUE, FALSE, nullptr) };
if (!frameReady)
{
return false; // היצירה עצמה נכשלה. אין מה לסגור
}
if (!RegisterCallback(frameReady.get()))
{
return false; // גם אם עושים return כאן, הדסטרוקטור יסגור
}
if (!StartAcquisition())
{
// כשל אחרי שהרישום כבר הושלם: קודם מבטלים את הרישום ורק אז סוגרים.
// אם יוצאים בלי לבטל את הרישום, הדסטרוקטור כן יקרא ל-CloseHandle,
// אבל ה-SDK עדיין מחזיק את ה-handle שהועבר אליו. בפריים הבא הוא
// עשוי לאותת למספר שכבר שוחרר, ואם אותו מספר מוקצה מחדש למשאב
// אחר, זה יתגלה כ"אירוע לא קשור שקם מעצמו"
UnregisterCallback();
return false;
}
// רק כשמצליחים, מעבירים את הבעלות לצד ה-session
frameReady_ = std::move(frameReady);
return true;
}
ב-C#, במקרים רבים לא מספיק using בודד, ולכן מציבים דגל של “האם הועברה הבעלות”, וזורקים את המשאב ב-finally רק כשהיא לא הועברה. אם כותבים סתם using var, המשאב יושמד גם כשההצלחה מתרחשת.
// C# / .NET 8
// שדה ב-CameraSession: private ManualResetEvent? _frameReady;
public bool Reconnect()
{
var frameReady = new ManualResetEvent(false);
var handedOver = false;
var registered = false;
try
{
if (!RegisterCallback(frameReady))
{
return false;
}
registered = true;
if (!StartAcquisition())
{
return false;
}
_frameReady?.Dispose();
_frameReady = frameReady;
handedOver = true;
return true;
}
finally
{
if (!handedOver)
{
// לפני שזורקים, קודם מבטלים את ההפניה שהצד החיצוני מחזיק בה.
// ה-SDK מחזיק את ה-handle שהועבר אליו בעת הרישום, ולכן אם הופכים
// את הסדר, יגישו קריאה ל-handle שכבר שוחרר
if (registered)
{
UnregisterCallback();
}
frameReady.Dispose();
}
}
}
בשני המקרים עושים את אותו הדבר. המבנה מבטיח שבכל יציאה באמצע, ללא קשר לנקודה, משאב שלא נקבעה לו בעלות תמיד נזרק. במקום שבן אדם יכתוב “אם נכשל, לסגור” בכל מקום מחדש, מטילים את זה על הטיפוס ועל ה-finally.
flowchart TB
accTitle: מי אחראי לשחרור בהתאם להעברת הבעלות
accDescr: גם אם יוצאים מהעיבוד באמצע מכל נקודה, אם הבעלות הועברה לצד ה-session הוא אחראי לשחרור מכאן ואילך, ואם לא, הטיפוס או ה-finally תמיד יזרקו, ושלפני הזריקה מבטלים תחילה את הרישום אצל ה-SDK.
exit["יציאה מהעיבוד מכל נקודה באמצע"] --> q2{"הבעלות הועברה?"}
q2 -->|"הועברה"| keep["ה-session אחראי לשחרור מכאן"]
q2 -->|"לא הועברה"| drop["הטיפוס או ה-finally תמיד יזרקו"]
drop -.-> unreg["לפני הזריקה מבטלים את הרישום"]
איור 11: לא כותבים “אם נכשל, לסגור” בכל מקום מחדש. יעד הבעלות קובע מי אחראי לשחרור.
זה לא באמת טכניקה מיוחדת, אלא סידור שמטמיע את מחזור חיי המשאב בתוך הקוד עצמו.
4. איך פירקנו את זה
מהפרק הזה מופיעים מונחים באנגלית הקשורים לחקירה כפי שהם. נשים כאן קודם הערת תרגום קצרה.
| מונח | בעברית | המשמעות במאמר הזה |
|---|---|---|
| baseline | ערך בסיס | הערך שמתייצב אחרי שה-warm-up מסתיים. משם מסתכלים על ההפרש |
| leakSlope | slope של הדליפה | כמה גדל בכל מחזור. מדד עצמאי שמייצג את קצב הגידול |
| structured log | לוג מובנה | לוג שבו הפריטים קבועים בסגנון key=value, במקום פרוזה. אפשר לרכז אותו אחר כך באופן אוטומטי |
| heartbeat | דיווח תקופתי | לוג שמוציא בקצב קבוע אישור חיים וערכי משאבים |
| harness | מסגרת בדיקה | תוכנית הרצה קטנה שמריצה שוב ושוב רק את הפעולה שרוצים לבדוק, במקום האפליקציה המלאה |
| phase | שלב | סימון כמו OpenStart או ReconnectStart, שמראה באיזה שלב של העיבוד נמצאים כרגע |
4.1. לקצר את הזמן בלי לחכות לשחזור בסדר גודל של חודשים
בחקירה כזו, לחכות חודש בכל פעם זה גישה גרועה. מה שצריך לעשות הוא להעביר את המסלול החשוד שוב ושוב, בזמן קצר.
במקרה הזה, כיווצנו את השחזור בעזרת לולאה כזו:
flowchart LR
accTitle: לולאת השחזור המכווץ
accDescr: לולאת השחזור מתחילה בהפעלה, פתיחת מצלמה והתחלת צילום, ואז מדמה timeout או ניתוק, מבצעת reconnect וחידוש צילום, וחוזרת על כך N פעמים לפני בדיקת ההפרש בסיום.
A["הפעלה"] --> B["open של המצלמה"]
B --> C["תחילת צילום"]
C --> D["דימוי timeout / ניתוק"]
D --> E["reconnect"]
E --> F["חידוש הצילום"]
F --> G{"לחזור N פעמים?"}
G -- "כן" --> D
G -- "לא" --> H["בדיקת ההפרש בסיום"]
איור 12: בלי לחכות לשחזור בסדר גודל של חודשים, מריצים אלפי פעמים בלולאה קצרה רק את הגבולות של open, ניתוק ו-reconnect.
הנקודה היא להשקיע זמן בפעולות הגבול של ה-lifetime, ולא בזמן ה”צילום הרגיל”.
התרחישים שבאמת עוזרים הם כאלה:
- להריץ
open -> start -> stop -> closeבכמות גדולה - לגרום ל-timeout בכוונה כדי להריץ reconnect
- לגרום לכשל מיד אחרי רישום ה-callback
- להכניס הפרעת ניתוק, הפרעת reconnect ו-race בזמן shutdown
אין צורך לשחזר בצורה מושלמת חודש שלם של הפעלה בפועל. דווקא לעבור אלפי פעמים על גבול lifetime חשוד מקרב הרבה יותר לגורם.
4.2. הסתכלות לפי ה-slope של Handle Count
לפני זה, נכתוב איפה בכלל מסתכלים על Handle Count. בלי זה, כל הסעיף הזה נשאר תיאורטי בלבד.
| שיטה | פעולה | מתאים למקרה |
|---|---|---|
| Task Manager | פותחים את לשונית Details, לוחצים ימני על כותרת העמודה → Select columns → מסמנים Handles | רוצים לראות מהר כמה יש כרגע |
| Process Explorer | בוחרים את ה-process, פותחים Properties, ורואים את Handle Count בלשונית Process Performance. סידור חלונית ה-Handles התחתונה לפי Type מראה גם פירוט לפי סוג | רוצים לדעת אילו handles גדלים |
handle.exe |
handle -s -p CameraApp נותן ריכוז טקסטואלי לפי סוג |
רוצים לתעד תצפית קבועה בלוג |
| PowerShell | Get-Process -Name CameraApp \| Select-Object Name, Id, HandleCount |
רוצים לאסוף באופן תקופתי בסקריפט |
typeperf |
typeperf "\Process(CameraApp)\Handle Count" -si 60 -sc 1440 -o handles.csv |
רוצים לתעד CSV לאורך זמן רב |
| האפליקציה עצמה | הטמעת GetProcessHandleCount או Process.HandleCount בתוך לוג ה-heartbeat |
רוצים לאסוף רק לוגים ממכונת הייצור |
פירוט לפי סוג ואיך לעקוב אחרי גידול של events בלי שם מסוכם כשלבים בכתובת Process Explorer / Handle / VMMap בפועל.
בחקירה של ריצה ארוכה, השיטה המרכזית היא זו האחרונה בטבלה — “האפליקציה עצמה מוציאה”. אדם שמסתכל על Task Manager לא יכול להמשיך בכך 24/7.
בחקירת handle leak, לפעמים קשה להבין רק מהערך המוחלט. החשוב הוא האם המספר חוזר אחרי הפעולה שאמורה להחזיר אותו, וכמה גדל בכל כמה פעולות.
בתור שיטת הסתכלות, הסדר הבא בדרך כלל ברור:
- קביעת
baselineאחרי ה-warm-up - תיעוד
Handle Countאחרי reconnect / start-stop / close - בדיקת ההפרש בכל מחזור
- בדיקת ה-slope המצטבר על פני כמה מחזורים
לדוגמה, כך אפשר להסתכל:
leakSlope =
(currentHandleCount - baselineHandleCount)
/ reconnectCount
האם 2000 בערך מוחלט זה הרבה או מעט תלוי באפליקציה. אבל אם בכל reconnect עולה ב-1 ולא חוזר, זה חשוד מאוד.
נכתוב גם קנה מידה לאיך המסלול התקין אמור להיראות. המספר עצמו תלוי באפליקציה, ולכן שופטים לפי הצורה.
- מיד אחרי ההפעלה יש עלייה — את זה לא קוראים
- אחרי שה-warm-up נגמר, הערך אמור לעלות ולרדת בהתאם לפעולות, אבל לנוע בתוך טווח קבוע
- אחרי מחזור אחד של
open -> start -> stop -> close, נורמלי שהערך יחזור כמעט לאותו ערך שהיה לפני המחזור - אם אחרי 100 מחזורים ההפרש מה-baseline הוא בסדר גודל של יחידות בודדות, זה בריא ברובו
- לעומת זאת, אם זה עולה בקו ישר ביחס ישר למספר המחזורים, אז זה דולף בדיוק בקצב ה-slope הזה בכל מחזור
לא מסתכלים על “הרבה או מעט”, אלא על האם זה חוזר או לא. אם מתבלבלים בנקודה הזו, מבזבזים זמן בחשד לאפליקציה תקינה.
flowchart TB
accTitle: איך קוראים את ה-slope של Handle Count
accDescr: אחרי warm-up קובעים baseline, בודקים את ההפרש בכל מחזור, ואם הערך חוזר אחרי המחזור זה בריא ברובו, ואם הוא עולה בקו ישר ביחס למספר המחזורים אז יש דליפה קבועה בכל מחזור.
base["קביעת baseline אחרי warm-up"] --> cyc["בדיקת ההפרש בכל מחזור"]
cyc --> q3{"הערך חוזר אחרי המחזור?"}
q3 -->|"חוזר"| ok["בריא ברובו"]
q3 -->|"עולה בקו ישר"| ng["דליפה קבועה בכל מחזור"]
איור 13: לא שופטים לפי הערך המוחלט, אלא לפי הצורה — “חוזר או לא חוזר”.
הטיפ כאן הוא לא להסתכל רק על Handle Count, אלא לתעד לפחות גם את הבאים:
Handle CountPrivate BytesThread CountReconnectCount- ה-
phaseהנוכחי
כך אפשר להבין הרבה יותר מהר האם “הזיכרון גדל”, “ה-threads גדלים”, או “המשאב לא חוזר בכל reconnect”.
4.3. הסתכלות על ההתאמה בין create/open לבין close/dispose
גם אם מתגלה ש-Handle Count של ה-process כולו חשוד, זה לבד לא מגיע עד מקום הדליפה.
מה שצריך אחר כך הוא לוג שמראה את מחזור חיי המשאב כזוגות.
בתור דוגמה, זה נראה כמו structured log כזה:
CameraSession session=421 cameraId=CAM01 phase=ReconnectStart reason=FrameTimeout handleCount=1824 privateBytesMB=418
CameraResource session=421 resourceId=evt-884 kind=Event name=FrameReady action=Create osHandle=0x00000ABC handleCount=1825
CameraResource session=421 resourceId=evt-884 kind=Event name=FrameReady action=Close osHandle=0x00000ABC handleCount=1824
החשוב כאן הוא לא להסתמך רק על osHandle.
מכיוון שערך ה-handle ב-Windows עשוי לעבור reuse בהמשך, קל יותר לעקוב אם מוסיפים בלוג לפחות את הבאים:
sessionIdresourceIdkindaction(Create/Open/Register/Close/Dispose/Unregister)osHandlephase
כך קל יותר למצוא זרימה חד-צדדית — יש Create, אבל אין Close.
flowchart TB
accTitle: לוג שעוקב אחרי מחזור חיי המשאב כזוגות
accDescr: רישום Create ו-Close כזוגות וקישורם באמצעות sessionId ו-resourceId מאפשר למצוא Create שלא קיבל Close, וה-osHandle עובר reuse ולכן אי אפשר לעקוב לפיו בלבד.
lg["רישום Create ו-Close כזוגות"] --> ids["קישור לפי sessionId ו-resourceId"]
ids --> find["גילוי Create שלא קיבל Close"]
lg -.-> reuse["osHandle עובר reuse, אי אפשר להסתמך עליו לבדו"]
איור 14: כדי לרדת ממספר כולל ב-process אל מקום הדליפה, נדרש לוג שמזווג את מחזור חיי המשאב.
4.4. ב-handle leak מחפשים את “מקום הדליפה” ולא את “מקום הקריסה”
הנקודה הזו חשובה מאוד.
handle leak לרוב נראה כך:
- השורה שקורסת: כשל ב-
CreateEvent - הדליפה האמיתית: כמה ימים לפני כן,
CloseHandleהיה חסר ב-failure path
כלומר, ה-API שקרס לבסוף הוא יציאת הנזק, ולא בהכרח כניסת הגורם.
לכן, סדר החקירה הוא כזה:
- לבדוק אילו משאבים ממשיכים לגדול
- לבדוק באיזה גבול פעולה הם לא חוזרים
- לחפש איפה ההתאמה בין
create/openלביןclose/disposeהתקלקלה - בסוף לקרוא את נקודת הקריסה
בסדר הזה, קל בהרבה שלא ללכת לאיבוד.
flowchart TB
accTitle: סדר החקירה שמוביל למקום הדליפה
accDescr: הסדר שמפחית את הסיכוי ללכת לאיבוד הוא לבדוק אילו משאבים ממשיכים לגדול, לבדוק באיזה גבול פעולה הם לא חוזרים, לחפש איפה ההתאמה בין create ל-close התקלקלה, ורק בסוף לקרוא את נקודת הקריסה.
s1["בדיקת המשאבים שממשיכים לגדול"] --> s2["בדיקת הגבול שבו הם לא חוזרים"]
s2 --> s3["חיפוש איפה ההתאמה בין create ל-close התקלקלה"]
s3 --> s4["בסוף, קריאת נקודת הקריסה"]
איור 15: נקודת הקריסה היא רק היציאה. יש להתחיל מ”מקום הדליפה” שהוא הכניסה.
5. הלוגים שצריך כדי שזה לא יחזור
5.1. הסט המינימלי שכדאי להשאיר קודם
מה שעזר בחקירה הזו לא היה סתם הגדלת כמות הלוגים. אלא ארגון והוספה של מידע שמאפשר להגיע לגורם בהמשך.
כמינימום, כדאי להשאיר את הבאים:
| קטגוריה | פריטים מינימליים רצויים | הסיבה |
|---|---|---|
| הקשר הפעולה | cameraId, sessionId, operationId, reconnectCount, phase |
כדי לקשר איזו פעולה ובאיזה מספר סיבוב זה קרה |
| משאבי ה-process | handleCount, privateBytes, workingSet, threadCount |
כדי לפרק קודם מה בעצם גדל |
| מחזור חיי המשאב | action, resourceId, kind, osHandle, owner |
כדי לעקוב אחרי ההתאמה בין create/open לבין close/dispose |
| תוצאת קריאות חיצוניות | win32Error, HRESULT, sdkError, timeoutMs |
כדי להשוות אחר כך בין סוגי הכשל |
| מעברי מצב | OpenStart, OpenDone, ReconnectStart, ReconnectDone, ShutdownStart וכדומה |
כדי לדעת באיזה שלב באמצע ה-phase המצב התקלקל |
| סביבת ההרצה | pid, tid, buildVersion, machineName |
כדי להתאים בין dump, symbol וחומר ההפצה |
לא נטען שזה מספיק. אבל לפחות בלי זה, קל מאוד שהלוג יישאר רק עם העובדה “זה קרס”.
5.2. הלוגים שחיזקנו בפועל
במקרה הזה, חיזקנו את הלוגים בכיוונים הבאים:
- heartbeat תקופתי
- הוצאת
Handle Count/Private Bytes/Thread Count/ReconnectCountכל 1–5 דקות
- הוצאת
- לוג גבולות לפי session של המצלמה
OpenStartCallbackRegisteredAcquisitionStartTimeoutDetectedReconnectStartReconnectDoneCloseStartCloseDone
- לוג מחזור חיי המשאב
Create/Open/Registerו-Close/Dispose/Unregisterעבור event / thread / file / timer / SDK registration token
- נרמול השגיאות
- לא להסתפק בהודעת ה-exception לבדה, אלא להוציא יחד
win32Error,HRESULT,sdkError,phase
- לא להסתפק בהודעת ה-exception לבדה, אלא להוציא יחד
החשוב הוא לא לשנות את סוג הלוג בין הצלחה לכשל. אם רק מקרי חריגה מקבלים פורמט אחר, אחר כך קשה לרכז אותם.
flowchart TB
accTitle: ארבע מערכות הלוג שחוזקו
accDescr: שילוב של heartbeat תקופתי, לוג גבולות ה-session, לוג מחזור חיי המשאב ונרמול השגיאות, בלי לשנות את הפורמט בין הצלחה לכשל, יוצר לוג שמאפשר להגיע לגורם בהמשך.
hb["heartbeat תקופתי (ערכי המשאבים)"] --> trace["לוג שמוביל לגורם"]
bd["לוג גבולות ה-session"] --> trace
rl["לוג מחזור חיי המשאב"] --> trace
er["נרמול השגיאות"] --> trace
trace -.-> same["לא לשנות את הפורמט בין הצלחה לכשל"]
איור 16: לא מגדילים את כמות הלוג, אלא מסדרים ארבע מערכות שאפשר להצליב אחר כך.
5.3. באיזו רזולוציה לתעד
מה שעושים בטעות לרוב זה “בינתיים להוציא הכול כ-INFO”. אבל אם עושים את זה, בקריאה מאוחר יותר נוצר ים של לוגים שאי אפשר לקרוא. זה די מתיש.
בתור רזולוציה, החלוקה הבאה בערך ריאלית:
- ניטור תקופתי
Handle Count,Private Bytes,Thread Count,ReconnectCount
- גבולות פעולה
- התחלה, סיום וכישלון של ה-session
- גבולות משאב
create/open/registerמולclose/dispose/unregister
- פירוט בזמן חריגה
- error code, stack, טריגר לאיסוף dump
לוג מפורט לכל פריים בדרך כלל לא נחוץ. מה שיעיל יותר לתקלה בריצה ארוכה הוא לוג שממנו אפשר לקרוא “איזו אחריות פתחה, ואיזו אחריות סגרה”.
flowchart TB
accTitle: חלוקת רזולוציית הלוג
accDescr: ניטור תקופתי מתעד ספירות, גבולות פעולה וגבולות משאב מתעדים את הזוגות של פתיחה וסגירה, ורק בזמן חריגה מתעד פירוט מלא, במקום להוציא הכול כ-INFO וליצור ים של לוגים שאי אפשר לקרוא.
lv["רזולוציית הלוג"] --> l1["ניטור תקופתי: ספירות"]
lv --> l2["גבולות פעולה וגבולות משאב"]
lv --> l3["פירוט מלא רק בזמן חריגה"]
all["הוצאת הכול כ-INFO"] -.-> wall["ים של לוגים שאי אפשר לקרוא"]
איור 17: במקום פירוט לכל פריים, מסדרים רזולוציה שממנה אפשר לקרוא “מי פתח ומי סגר”.
6. מתי מה לבדוק, בגדול
- קורס רק אחרי ימים עד שבועות
- קודם כול, להוסיף heartbeat של
Handle Count/Private Bytes/Thread Count
- קודם כול, להוסיף heartbeat של
- יש retry / reconnect / shutdown
- קודם להכין harness שמריץ בכמות גדולה רק את הגבולות האלה
- שימוש נרחב ב-native SDK / P/Invoke / Win32
- שווה מאוד להפעיל את Application Verifier מהפרק השני
- יש גם GUI
- כדאי להסתכל, מלבד
Handle Count, גם עלGDI Objects/USER Objects
- כדאי להסתכל, מלבד
- ה-exception ברגע הקריסה לבדו לא אומר כלום
- מהיר יותר לסדר קודם structured log של operation / session / מחזור חיי המשאב
הפריט האחרון חשוב מאוד. בחקירת תקלות, לעיתים קרובות האם המצב נתון לתצפית קובע את ההצלחה יותר מאשר טכניקת הניתוח עצמה.
7. סיכום
באפליקציה שקורסת רק אחרי ריצה ארוכה, יש להסתכל לא רק על הזיכרון אלא גם על Handle Count. handle leak נוטה להסתתר לא ב-success path אלא ב-failure path של המסלול החריג, ונקודת הקריסה היא לרוב יציאה של תקלה משנית ולא נקודת הדליפה עצמה. כשמסתכלים על הסימפטומים, זה בעצם מצטמצם לשלוש הנקודות האלה.
כדי שזה לא יחזור, מקרבים את האחריות של create/open לזו של close/dispose, משאירים לוג עם הקשר לפי session / operation, ומתעדים גם את משאבי ה-process וגם את מחזור חיי המשאב. בבדיקות, בלי לחכות לשחזור בסדר גודל של חודשים, מריצים timeout / reconnect / shutdown בלולאה קצרה, וקובעים כתנאי הצלחה לא רק “לא להישבר” אלא גם “אפשר לעקוב כשזה נשבר”. זה השילוב שעזר במקרה הזה. בפרק השני נשתמש ב-Application Verifier כדי לחשוף מראש שברים קשים להתגלות, כמו מחסור בזיכרון או תקלת handle.
באפליקציות בקרה, חשוב שה-success path יעבוד, אבל היכולת “לדעת מה קרה” כשזה נשבר יעילה מאוד בהפעלה ארוכת טווח.
handle leak הוא בדיוק סוג התקלה שבו ההבדל הזה משפיע. במקום להסתכל רק על רגע ההתרחשות, הסתכלות על קצב הגידול, הגבולות והזוגות של האחריות הופכת את המעקב לקל בהרבה.
פרק שני: תשתית בדיקות למקרי קצה ב-Windows עם Application Verifier
8. מקורות
- GetProcessHandleCount 関数 (processthreadsapi.h)
- Process.HandleCount プロパティ (System.Diagnostics)
- Kernel Objects - Win32 apps
- GDI Objects - Win32 apps
- typeperf - Windows Commands
- Process Explorer / Handle / VMMap בפועל — לעקוב אחרי hang, leak ו”קובץ בשימוש” מהמצב הנוכחי הזה
- פרק שני: תשתית בדיקות למקרי קצה ב-Windows עם Application Verifier
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
תשתית לבדיקות failure path ב-Windows עם Application Verifier
מה זה Application Verifier, ואיך בונים איתו תשתית לבדיקות failure path ב-Windows בעזרת Handles, Heaps, Low Resource Simulation ו-!htrace.
TCP Retransmission שעוצר תקשורת עם מצלמה תעשייתית — איך מפרקים את זה
איך מפרקים עצירה של כמה שניות בתקשורת עם מצלמה תעשייתית שנגרמת מ-TCP retransmission: packet loss, RTO, timestamps של RFC1323, ונקודות בדי...
מה Not Responding באמת אומר — איך Windows מחליט שיש hang, ואיך לתכנן אפליקציות שלא נתקעות
Windows מסמן window כ-Not Responding אחרי 5 שניות בלי שליפת message ומציג ghost window: השיפוט, סיבות ה-hang, תכנון ה-UI thread, והחקירה.
WPR/WPA בפועל — מבוא לחקירת "כל ה-PC איטי" על פני כל המערכת
חוקרים PC איטי או startup איטי ש-Task Manager לא מסביר עם ETW trace ברמת ה-OS: מצלמים ב-wpr.exe, ואז קוראים CPU, המתנות ו-I/O דיסק ב-WPA.
איך משאירים לוגים ו-crash dump כשאפליקציית Windows קורסת
איך משלבים לוג רגיל, fatal crash marker, WER LocalDumps ותהליך watchdog, כדי שאפילו כשאפליקציית Windows קורסת מ-exception לא צפוי או מ-bu...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
חקרי מקרה קשורים
חקרי המקרה האלה מציגים גישה דומה לניתוח, לתעדוף או לעיצוב מחדש.
כיצד קישרנו קריסה אחרי ריצה ארוכה לדליפת handles
חקר מקרה על האופן שבו נקודות תצפית ורישום טובות יותר הפכו קריסה חודשית לחקירה ממוקדת של דליפת handles.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
חקירת תקלות ואיתור גורמים
פירוק תקלה שקורסת רק אחרי ריצה ארוכה מתאים מאוד לחקירת באגים וניתוח סיבה.
פיתוח יישומי Windows
כשרוצים לבחון מחדש את בניית האפליקציה בצד Windows, כולל תכנון לוגים וניטור תפעולי, זה מוביל גם לייעוץ על פיתוח אפליקציות Windows.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה זה handle leak?
- מצב שבו process ב-Windows שוכח לסגור handle שמפנה למשאב של מערכת ההפעלה כמו event, mutex, file או socket, וה-Handle Count ממשיך לגדול. הדפוס הנפוץ במיוחד הוא שכחת סגירה של משאב שנפתח זמנית עבור פעולה מסוימת, ב-failure path באמצע הפעולה כמו timeout, reconnect או early return. בבדיקות קצרות שגרתיות עוברים בעיקר ב-success path, ולכן קל לפספס את זה.
- איך מבחינים בין memory leak ל-handle leak?
- המדדים שונים. ב-memory leak, Private Bytes או Commit גדלים בהדרגה. ב-handle leak, Handle Count גדל בהדרגה ולא חוזר. בפירוק תקלה אחרי ריצה ארוכה, הסתכלות רק על הזיכרון היא כמו לראות רק חצי מהתמונה, ולכן הבסיס הוא להסתכל גם על Handle Count וגם על Thread Count. אם יש גם GUI, כדאי להסתכל גם על GDI Objects / USER Objects.
- אם יש handle leak, למה זה קורס רק אחרי ריצה ארוכה?
- דליפה עם slope קטן — למשל handle אחד בודד שדולף בכל כשל — לא גורמת לשום דבר תוך כמה דקות. אבל בהפעלה 24/7, תנאי גבול כמו timeout או reconnect קורים שוב ושוב, ומצטברים במשך שבועות. בסוף, ברגע שקריאת API שיוצרת event/file/thread חדש נכשלת, זה מתגלה כתקלה משנית. חשוב גם שנקודת הקריסה לרוב אינה המקום שבו דלף ה-handle, אלא הקורבן האחרון בשרשרת.
- איך חוקרים handle leak?
- בלי לחכות לשחזור בסדר גודל של חודשים, מכווצים את זמן השחזור על ידי הרצה של אלפי איטרציות בלולאה קצרה על גבולות lifetime חשודים, כמו open -> start -> stop -> close או timeout ו-reconnect. קובעים baseline אחרי warm-up, בודקים את ההפרש וה-slope של Handle Count בכל מחזור, ומחפשים איפה ההתאמה בין create/open ל-close/dispose התקלקלה בעזרת structured log שמכיל sessionId, resourceId ו-action. קריאת נקודת הקריסה בסוף — ולא בהתחלה — מקטינה את הסיכוי ללכת לאיבוד בדרך.