1   
2   
3   
4   
5   
6   
7   
8   
9   
10  
11  
12  
13  
14  
15  
16  
17  package io.netty5.util;
18  
19  import io.netty5.util.internal.EmptyArrays;
20  import io.netty5.util.internal.SystemPropertyUtil;
21  import io.netty5.util.internal.logging.InternalLogger;
22  import io.netty5.util.internal.logging.InternalLoggerFactory;
23  
24  import java.lang.ref.Reference;
25  import java.lang.ref.ReferenceQueue;
26  import java.lang.ref.WeakReference;
27  import java.lang.reflect.Method;
28  import java.util.Arrays;
29  import java.util.Collections;
30  import java.util.HashSet;
31  import java.util.Set;
32  import java.util.concurrent.ConcurrentHashMap;
33  import java.util.concurrent.ThreadLocalRandom;
34  import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
35  import java.util.concurrent.atomic.AtomicReference;
36  import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
37  
38  import static io.netty5.util.internal.StringUtil.EMPTY_STRING;
39  import static io.netty5.util.internal.StringUtil.NEWLINE;
40  import static io.netty5.util.internal.StringUtil.simpleClassName;
41  import static java.util.Objects.requireNonNull;
42  
43  public class ResourceLeakDetector<T> {
44  
45      private static final String PROP_LEVEL = "io.netty5.leakDetection.level";
46      private static final Level DEFAULT_LEVEL = Level.SIMPLE;
47  
48      private static final String PROP_TARGET_RECORDS = "io.netty5.leakDetection.targetRecords";
49      private static final int DEFAULT_TARGET_RECORDS = 4;
50  
51      private static final String PROP_SAMPLING_INTERVAL = "io.netty5.leakDetection.samplingInterval";
52      
53      private static final int DEFAULT_SAMPLING_INTERVAL = 128;
54  
55      private static final int TARGET_RECORDS;
56      static final int SAMPLING_INTERVAL;
57  
58      
59  
60  
61      public enum Level {
62          
63  
64  
65          DISABLED,
66          
67  
68  
69  
70          SIMPLE,
71          
72  
73  
74  
75          ADVANCED,
76          
77  
78  
79  
80          PARANOID;
81  
82          
83  
84  
85  
86  
87  
88          static Level parseLevel(String levelStr) {
89              String trimmedLevelStr = levelStr.trim();
90              for (Level l : values()) {
91                  if (trimmedLevelStr.equalsIgnoreCase(l.name()) || trimmedLevelStr.equals(String.valueOf(l.ordinal()))) {
92                      return l;
93                  }
94              }
95              return DEFAULT_LEVEL;
96          }
97      }
98  
99      private static Level level;
100 
101     private static final InternalLogger logger = InternalLoggerFactory.getInstance(ResourceLeakDetector.class);
102 
103     static {
104         String levelStr = SystemPropertyUtil.get(PROP_LEVEL, DEFAULT_LEVEL.name());
105         Level level = Level.parseLevel(levelStr);
106 
107         TARGET_RECORDS = SystemPropertyUtil.getInt(PROP_TARGET_RECORDS, DEFAULT_TARGET_RECORDS);
108         SAMPLING_INTERVAL = SystemPropertyUtil.getInt(PROP_SAMPLING_INTERVAL, DEFAULT_SAMPLING_INTERVAL);
109 
110         ResourceLeakDetector.level = level;
111         if (logger.isDebugEnabled()) {
112             logger.debug("-D{}: {}", PROP_LEVEL, level.name().toLowerCase());
113             logger.debug("-D{}: {}", PROP_TARGET_RECORDS, TARGET_RECORDS);
114         }
115     }
116 
117     
118 
119 
120     public static boolean isEnabled() {
121         return getLevel().ordinal() > Level.DISABLED.ordinal();
122     }
123 
124     
125 
126 
127     public static void setLevel(Level level) {
128         requireNonNull(level, "level");
129         ResourceLeakDetector.level = level;
130     }
131 
132     
133 
134 
135     public static Level getLevel() {
136         return level;
137     }
138 
139     
140     private final Set<DefaultResourceLeak<?>> allLeaks = ConcurrentHashMap.newKeySet();
141 
142     private final ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
143     private final Set<String> reportedLeaks =
144             Collections.newSetFromMap(new ConcurrentHashMap<>());
145     private final String resourceType;
146     private final int samplingInterval;
147 
148     
149 
150 
151 
152 
153     public ResourceLeakDetector(Class<?> resourceType, int samplingInterval) {
154         this.resourceType = simpleClassName(resourceType);
155         this.samplingInterval = samplingInterval;
156     }
157 
158     
159 
160 
161 
162 
163 
164     @SuppressWarnings("unchecked")
165     public final ResourceLeakTracker<T> track(T obj) {
166         return track0(obj);
167     }
168 
169     @SuppressWarnings("unchecked")
170     private DefaultResourceLeak track0(T obj) {
171         Level level = ResourceLeakDetector.level;
172         if (level == Level.DISABLED) {
173             return null;
174         }
175 
176         if (level.ordinal() < Level.PARANOID.ordinal()) {
177             if (ThreadLocalRandom.current().nextInt(samplingInterval) == 0) {
178                 reportLeak();
179                 return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
180             }
181             return null;
182         }
183         reportLeak();
184         return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
185     }
186 
187     private void clearRefQueue() {
188         for (;;) {
189             DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
190             if (ref == null) {
191                 break;
192             }
193             ref.dispose();
194         }
195     }
196 
197     
198 
199 
200 
201 
202 
203     protected boolean needReport() {
204         return logger.isErrorEnabled();
205     }
206 
207     private void reportLeak() {
208         if (!needReport()) {
209             clearRefQueue();
210             return;
211         }
212 
213         
214         for (;;) {
215             DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
216             if (ref == null) {
217                 break;
218             }
219 
220             if (!ref.dispose()) {
221                 continue;
222             }
223 
224             String records = ref.getReportAndClearRecords();
225             if (reportedLeaks.add(records)) {
226                 if (records.isEmpty()) {
227                     reportUntracedLeak(resourceType);
228                 } else {
229                     reportTracedLeak(resourceType, records);
230                 }
231             }
232         }
233     }
234 
235     
236 
237 
238 
239     protected void reportTracedLeak(String resourceType, String records) {
240         logger.error(
241                 "LEAK: {}.release() was not called before it's garbage-collected. " +
242                 "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
243                 resourceType, records);
244     }
245 
246     
247 
248 
249 
250     protected void reportUntracedLeak(String resourceType) {
251         logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
252                 "Enable advanced leak reporting to find out where the leak occurred. " +
253                 "To enable advanced leak reporting, " +
254                 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
255                 "See https://netty.io/wiki/reference-counted-objects.html for more information.",
256                 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
257     }
258 
259     
260 
261 
262     @Deprecated
263     protected void reportInstancesLeak(String resourceType) {
264     }
265 
266     
267 
268 
269 
270 
271     protected Object getInitialHint(String resourceType) {
272         return null;
273     }
274 
275     private static final class DefaultResourceLeak<T>
276             extends WeakReference<Object> implements ResourceLeakTracker<T> {
277 
278         @SuppressWarnings("unchecked") 
279         private static final AtomicReferenceFieldUpdater<DefaultResourceLeak<?>, TraceRecord> headUpdater =
280                 (AtomicReferenceFieldUpdater)
281                         AtomicReferenceFieldUpdater.newUpdater(DefaultResourceLeak.class, TraceRecord.class, "head");
282 
283         @SuppressWarnings("unchecked") 
284         private static final AtomicIntegerFieldUpdater<DefaultResourceLeak<?>> droppedRecordsUpdater =
285                 (AtomicIntegerFieldUpdater)
286                         AtomicIntegerFieldUpdater.newUpdater(DefaultResourceLeak.class, "droppedRecords");
287 
288         @SuppressWarnings("unused")
289         private volatile TraceRecord head;
290         @SuppressWarnings("unused")
291         private volatile int droppedRecords;
292 
293         private final Set<DefaultResourceLeak<?>> allLeaks;
294         private final int trackedHash;
295 
296         DefaultResourceLeak(
297                 Object referent,
298                 ReferenceQueue<Object> refQueue,
299                 Set<DefaultResourceLeak<?>> allLeaks,
300                 Object initialHint) {
301             super(referent, refQueue);
302 
303             assert referent != null;
304 
305             
306             
307             
308             trackedHash = System.identityHashCode(referent);
309             allLeaks.add(this);
310             
311             headUpdater.set(this, initialHint == null ?
312                     new TraceRecord(TraceRecord.BOTTOM) : new TraceRecord(TraceRecord.BOTTOM, initialHint));
313             this.allLeaks = allLeaks;
314         }
315 
316         @Override
317         public void record() {
318             record0(null);
319         }
320 
321         @Override
322         public void record(Object hint) {
323             record0(hint);
324         }
325 
326         
327 
328 
329 
330 
331 
332 
333 
334 
335 
336 
337 
338 
339 
340 
341 
342 
343 
344 
345 
346 
347 
348 
349 
350 
351 
352         private void record0(Object hint) {
353             
354             if (TARGET_RECORDS > 0) {
355                 TraceRecord oldHead;
356                 TraceRecord prevHead;
357                 TraceRecord newHead;
358                 boolean dropped;
359                 do {
360                     if ((prevHead = oldHead = headUpdater.get(this)) == null) {
361                         
362                         return;
363                     }
364                     final int numElements = oldHead.pos + 1;
365                     if (numElements >= TARGET_RECORDS) {
366                         final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
367                         dropped = ThreadLocalRandom.current().nextInt(1 << backOffFactor) != 0;
368                         if (dropped) {
369                             prevHead = oldHead.next;
370                         }
371                     } else {
372                         dropped = false;
373                     }
374                     newHead = hint != null ? new TraceRecord(prevHead, hint) : new TraceRecord(prevHead);
375                 } while (!headUpdater.compareAndSet(this, oldHead, newHead));
376                 if (dropped) {
377                     droppedRecordsUpdater.incrementAndGet(this);
378                 }
379             }
380         }
381 
382         boolean dispose() {
383             clear();
384             return allLeaks.remove(this);
385         }
386 
387         @Override
388         public boolean close(T trackedObject) {
389             
390             assert trackedHash == System.identityHashCode(trackedObject);
391 
392             try {
393                 if (allLeaks.remove(this)) {
394                     
395                     clear();
396                     headUpdater.set(this, null);
397                     return true;
398                 }
399                 return false;
400             } finally {
401                 
402                 Reference.reachabilityFence(trackedObject);
403             }
404         }
405 
406         @Override
407         public String toString() {
408             TraceRecord oldHead = headUpdater.get(this);
409             return generateReport(oldHead);
410         }
411 
412         String getReportAndClearRecords() {
413             TraceRecord oldHead = headUpdater.getAndSet(this, null);
414             return generateReport(oldHead);
415         }
416 
417         private String generateReport(TraceRecord oldHead) {
418             if (oldHead == null) {
419                 
420                 return EMPTY_STRING;
421             }
422 
423             final int dropped = droppedRecordsUpdater.get(this);
424             int duped = 0;
425 
426             int present = oldHead.pos + 1;
427             
428             StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
429             buf.append("Recent access records: ").append(NEWLINE);
430 
431             int i = 1;
432             Set<String> seen = new HashSet<>(present);
433             for (; oldHead != TraceRecord.BOTTOM; oldHead = oldHead.next) {
434                 String s = oldHead.toString();
435                 if (seen.add(s)) {
436                     if (oldHead.next == TraceRecord.BOTTOM) {
437                         buf.append("Created at:").append(NEWLINE).append(s);
438                     } else {
439                         buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
440                     }
441                 } else {
442                     duped++;
443                 }
444             }
445 
446             if (duped > 0) {
447                 buf.append(": ")
448                         .append(duped)
449                         .append(" leak records were discarded because they were duplicates")
450                         .append(NEWLINE);
451             }
452 
453             if (dropped > 0) {
454                 buf.append(": ")
455                    .append(dropped)
456                    .append(" leak records were discarded because the leak record count is targeted to ")
457                    .append(TARGET_RECORDS)
458                    .append(". Use system property ")
459                    .append(PROP_TARGET_RECORDS)
460                    .append(" to increase the limit.")
461                    .append(NEWLINE);
462             }
463 
464             buf.setLength(buf.length() - NEWLINE.length());
465             return buf.toString();
466         }
467     }
468 
469     private static final AtomicReference<String[]> excludedMethods =
470             new AtomicReference<>(EmptyArrays.EMPTY_STRINGS);
471 
472     public static void addExclusions(Class<?> clz, String ... methodNames) {
473         Set<String> nameSet = new HashSet<>(Arrays.asList(methodNames));
474         
475         
476         for (Method method : clz.getDeclaredMethods()) {
477             if (nameSet.remove(method.getName()) && nameSet.isEmpty()) {
478                 break;
479             }
480         }
481         if (!nameSet.isEmpty()) {
482             throw new IllegalArgumentException("Can't find '" + nameSet + "' in " + clz.getName());
483         }
484         String[] oldMethods;
485         String[] newMethods;
486         do {
487             oldMethods = excludedMethods.get();
488             newMethods = Arrays.copyOf(oldMethods, oldMethods.length + 2 * methodNames.length);
489             for (int i = 0; i < methodNames.length; i++) {
490                 newMethods[oldMethods.length + i * 2] = clz.getName();
491                 newMethods[oldMethods.length + i * 2 + 1] = methodNames[i];
492             }
493         } while (!excludedMethods.compareAndSet(oldMethods, newMethods));
494     }
495 
496     private static class TraceRecord extends Throwable {
497         private static final long serialVersionUID = 6065153674892850720L;
498 
499         private static final TraceRecord BOTTOM = new TraceRecord();
500 
501         private final String hintString;
502         private final TraceRecord next;
503         private final int pos;
504 
505         TraceRecord(TraceRecord next, Object hint) {
506             
507             hintString = hint instanceof ResourceLeakHint ? ((ResourceLeakHint) hint).toHintString() : hint.toString();
508             this.next = next;
509             pos = next.pos + 1;
510         }
511 
512         TraceRecord(TraceRecord next) {
513            hintString = null;
514            this.next = next;
515            pos = next.pos + 1;
516         }
517 
518         
519         private TraceRecord() {
520             super(null, null, false, false);
521             hintString = null;
522             next = null;
523             pos = -1;
524         }
525 
526         @Override
527         public String toString() {
528             StringBuilder buf = new StringBuilder(2048);
529             if (hintString != null) {
530                 buf.append("\tHint: ").append(hintString).append(NEWLINE);
531             }
532 
533             
534             StackTraceElement[] array = getStackTrace();
535             
536             out: for (int i = 3; i < array.length; i++) {
537                 StackTraceElement element = array[i];
538                 
539                 String[] exclusions = excludedMethods.get();
540                 for (int k = 0; k < exclusions.length; k += 2) {
541                     
542                     
543                     if (exclusions[k].equals(element.getClassName())
544                             && exclusions[k + 1].equals(element.getMethodName())) { 
545                         continue out;
546                     }
547                 }
548 
549                 buf.append('\t');
550                 buf.append(element.toString());
551                 buf.append(NEWLINE);
552             }
553             return buf.toString();
554         }
555     }
556 }