IdentityFilePointer should be removed from myUrlToIdentity cache on dispose
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / vfs / impl / VirtualFilePointerManagerImpl.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.vfs.impl;
17
18 import com.intellij.openapi.Disposable;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.components.ApplicationComponent;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.util.*;
23 import com.intellij.openapi.util.io.FileUtil;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.openapi.vfs.*;
26 import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
27 import com.intellij.openapi.vfs.newvfs.BulkFileListener;
28 import com.intellij.openapi.vfs.newvfs.events.*;
29 import com.intellij.openapi.vfs.newvfs.persistent.PersistentFS;
30 import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
31 import com.intellij.openapi.vfs.pointers.VirtualFilePointerContainer;
32 import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
33 import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
34 import com.intellij.util.ConcurrencyUtil;
35 import com.intellij.util.SmartList;
36 import com.intellij.util.containers.ContainerUtil;
37 import com.intellij.util.io.URLUtil;
38 import com.intellij.util.messages.MessageBus;
39 import gnu.trove.THashMap;
40 import gnu.trove.TObjectIntHashMap;
41 import org.jetbrains.annotations.NonNls;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
44 import org.jetbrains.annotations.TestOnly;
45
46 import java.util.*;
47 import java.util.concurrent.ConcurrentMap;
48
49 public class VirtualFilePointerManagerImpl extends VirtualFilePointerManager implements ApplicationComponent, ModificationTracker, BulkFileListener {
50   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.impl.VirtualFilePointerManagerImpl");
51   private final TempFileSystem TEMP_FILE_SYSTEM;
52   private final LocalFileSystem LOCAL_FILE_SYSTEM;
53   private final JarFileSystem JAR_FILE_SYSTEM;
54   // guarded by this
55   private final Map<VirtualFilePointerListener, FilePointerPartNode> myPointers = new LinkedHashMap<>();
56
57   // compare by identity because VirtualFilePointerContainer has too smart equals
58   // guarded by myContainers
59   private final Set<VirtualFilePointerContainerImpl> myContainers = ContainerUtil.newIdentityTroveSet();
60   @NotNull private final VirtualFileManager myVirtualFileManager;
61   @NotNull private final MessageBus myBus;
62   private static final Comparator<String> URL_COMPARATOR = SystemInfo.isFileSystemCaseSensitive ? String::compareTo : String::compareToIgnoreCase;
63
64   VirtualFilePointerManagerImpl(@NotNull VirtualFileManager virtualFileManager,
65                                 @NotNull MessageBus bus,
66                                 @NotNull TempFileSystem tempFileSystem,
67                                 @NotNull LocalFileSystem localFileSystem,
68                                 @NotNull JarFileSystem jarFileSystem) {
69     myVirtualFileManager = virtualFileManager;
70     myBus = bus;
71     bus.connect().subscribe(VirtualFileManager.VFS_CHANGES, this);
72     TEMP_FILE_SYSTEM = tempFileSystem;
73     LOCAL_FILE_SYSTEM = localFileSystem;
74     JAR_FILE_SYSTEM = jarFileSystem;
75   }
76
77   @Override
78   public void initComponent() {
79   }
80
81   @Override
82   public void disposeComponent() {
83     assertAllPointersDisposed();
84   }
85
86   @NotNull
87   @Override
88   public String getComponentName() {
89     return "VirtualFilePointerManager";
90   }
91
92   private static class EventDescriptor {
93     @NotNull private final VirtualFilePointerListener myListener;
94     @NotNull private final VirtualFilePointer[] myPointers;
95
96     private EventDescriptor(@NotNull VirtualFilePointerListener listener, @NotNull VirtualFilePointer[] pointers) {
97       myListener = listener;
98       myPointers = pointers;
99     }
100
101     private void fireBefore() {
102       if (myPointers.length != 0) {
103         myListener.beforeValidityChanged(myPointers);
104       }
105     }
106
107     private void fireAfter() {
108       if (myPointers.length != 0) {
109         myListener.validityChanged(myPointers);
110       }
111     }
112   }
113
114   @NotNull
115   private static VirtualFilePointer[] toPointers(@NotNull List<FilePointerPartNode> nodes) {
116     if (nodes.isEmpty()) return VirtualFilePointer.EMPTY_ARRAY;
117     List<VirtualFilePointer> list = new ArrayList<>(nodes.size());
118     for (FilePointerPartNode node : nodes) {
119       node.addAllPointersTo(list);
120     }
121     return list.toArray(new VirtualFilePointer[list.size()]);
122   }
123
124   @TestOnly
125   VirtualFilePointer[] getPointersUnder(VirtualFile parent, String childName) {
126     List<FilePointerPartNode> nodes = new ArrayList<>();
127     addPointersUnder(parent, true, childName, nodes);
128     return toPointers(nodes);
129   }
130
131   private void addPointersUnder(VirtualFile parent,
132                                 boolean separator,
133                                 @NotNull CharSequence childName,
134                                 @NotNull List<FilePointerPartNode> out) {
135     for (FilePointerPartNode root : myPointers.values()) {
136       root.addPointersUnder(parent, separator, childName, out);
137     }
138   }
139
140   @Override
141   @NotNull
142   public synchronized VirtualFilePointer create(@NotNull String url, @NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
143     return create(null, url, parent, listener);
144   }
145
146   @Override
147   @NotNull
148   public synchronized VirtualFilePointer create(@NotNull VirtualFile file, @NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
149     return create(file, null, parent, listener);
150   }
151
152   @NotNull
153   private VirtualFilePointer create(@Nullable("null means the pointer will be created from the (not null) url") VirtualFile file,
154                                     @Nullable("null means url has to be computed from the (not-null) file path") String url,
155                                     @NotNull Disposable parentDisposable,
156                                     @Nullable VirtualFilePointerListener listener) {
157     VirtualFileSystem fileSystem;
158     String protocol;
159     String path;
160     if (file == null) {
161       int protocolEnd = url.indexOf(URLUtil.SCHEME_SEPARATOR);
162       if (protocolEnd == -1) {
163         protocol = null;
164         fileSystem = null;
165         path = url;
166       }
167       else {
168         protocol = url.substring(0, protocolEnd);
169         fileSystem = myVirtualFileManager.getFileSystem(protocol);
170         path = url.substring(protocolEnd + URLUtil.SCHEME_SEPARATOR.length());
171       }
172     }
173     else {
174       fileSystem = file.getFileSystem();
175       protocol = fileSystem.getProtocol();
176       path = file.getPath();
177       url = VirtualFileManager.constructUrl(protocol, path);
178     }
179
180     if (fileSystem == TEMP_FILE_SYSTEM) {
181       // for tests, recreate always
182       VirtualFile found = file == null ? VirtualFileManager.getInstance().findFileByUrl(url) : file;
183       return found == null ? new LightFilePointer(url) : new LightFilePointer(found);
184     }
185
186     boolean isJar = fileSystem == JAR_FILE_SYSTEM;
187     if (fileSystem != LOCAL_FILE_SYSTEM && !isJar) {
188       // we are unable to track alien file systems for now
189       VirtualFile found = fileSystem == null ? null : file != null ? file : VirtualFileManager.getInstance().findFileByUrl(url);
190       // if file is null, this pointer will never be alive
191       return getOrCreateIdentity(url, found, parentDisposable, listener);
192     }
193
194     if (file == null) {
195       String cleanPath = cleanupPath(path, isJar);
196       // if newly created path is the same as substringed from url one then the url did not change, we can reuse it
197       //noinspection StringEquality
198       if (cleanPath != path) {
199         url = VirtualFileManager.constructUrl(protocol, cleanPath);
200         path = cleanPath;
201       }
202       if (url.contains("..")) {
203         // the url of the form "/x/../y" should resolve to "/y" (or something else in the case of symlinks)
204         file = VirtualFileManager.getInstance().findFileByUrl(url);
205         if (file != null) {
206           url = file.getUrl();
207           path = file.getPath();
208         }
209       }
210     }
211     // else url has come from VirtualFile.getPath() and is good enough
212     VirtualFilePointerImpl pointer = getOrCreate(listener, path, Pair.create(file, url));
213     DelegatingDisposable.registerDisposable(parentDisposable, pointer);
214     return pointer;
215   }
216
217   private final Map<String, IdentityVirtualFilePointer> myUrlToIdentity = new THashMap<>();
218   @NotNull
219   private IdentityVirtualFilePointer getOrCreateIdentity(@NotNull String url,
220                                                          @Nullable VirtualFile found,
221                                                          @NotNull Disposable parentDisposable,
222                                                          VirtualFilePointerListener listener) {
223     IdentityVirtualFilePointer pointer = myUrlToIdentity.get(url);
224     if (pointer == null) {
225       pointer = new IdentityVirtualFilePointer(found, url,listener){
226         @Override
227         public void dispose() {
228           synchronized (VirtualFilePointerManagerImpl.this) {
229             super.dispose();
230             myUrlToIdentity.remove(url);
231           }
232         }
233       };
234       myUrlToIdentity.put(url, pointer);
235
236       DelegatingDisposable.registerDisposable(parentDisposable, pointer);
237     }
238     pointer.incrementUsageCount(1);
239     return pointer;
240   }
241
242   @NotNull
243   private static String cleanupPath(@NotNull String path, boolean isJar) {
244     path = FileUtil.normalize(path);
245     path = trimTrailingSeparators(path, isJar);
246     return path;
247   }
248
249   private static String trimTrailingSeparators(@NotNull String path, boolean isJar) {
250     while (StringUtil.endsWithChar(path, '/') && !(isJar && path.endsWith(JarFileSystem.JAR_SEPARATOR))) {
251       path = StringUtil.trimEnd(path, "/");
252     }
253     return path;
254   }
255
256   @NotNull
257   private VirtualFilePointerImpl getOrCreate(@Nullable VirtualFilePointerListener listener,
258                                              @NotNull String path,
259                                              @NotNull Pair<VirtualFile, String> fileAndUrl) {
260     FilePointerPartNode root = myPointers.get(listener);
261     FilePointerPartNode node;
262     if (root == null) {
263       root = new FilePointerPartNode(path, null, fileAndUrl);
264       root.pointersUnder++;
265       myPointers.put(listener, root);
266       node = root;
267     }
268     else {
269       node = root.findPointerOrCreate(path, 0, fileAndUrl, 1);
270     }
271
272     VirtualFilePointerImpl pointer = node.getAnyPointer();
273     if (pointer == null) {
274       pointer = new VirtualFilePointerImpl(listener);
275       node.associate(pointer, fileAndUrl);
276     }
277     pointer.incrementUsageCount(1);
278
279     root.checkConsistency();
280     return pointer;
281   }
282
283   @Override
284   @NotNull
285   public synchronized VirtualFilePointer duplicate(@NotNull VirtualFilePointer pointer,
286                                                    @NotNull Disposable parent,
287                                                    @Nullable VirtualFilePointerListener listener) {
288     VirtualFile file = pointer.getFile();
289     return file == null ? create(pointer.getUrl(), parent, listener) : create(file, parent, listener);
290   }
291
292   private synchronized void assertAllPointersDisposed() {
293     for (Map.Entry<VirtualFilePointerListener, FilePointerPartNode> entry : myPointers.entrySet()) {
294       FilePointerPartNode root = entry.getValue();
295       List<FilePointerPartNode> left = new ArrayList<>();
296       root.addPointersUnder(null, false, "", left);
297       List<VirtualFilePointerImpl> pointers = new ArrayList<>();
298       for (FilePointerPartNode node : left) {
299         node.addAllPointersTo(pointers);
300       }
301       if (!pointers.isEmpty()) {
302         VirtualFilePointerImpl p = pointers.get(0);
303         try {
304           p.throwDisposalError("Not disposed pointer: "+p);
305         }
306         finally {
307           for (VirtualFilePointerImpl pointer : pointers) {
308             pointer.dispose();
309           }
310         }
311       }
312     }
313
314     synchronized (myContainers) {
315       if (!myContainers.isEmpty()) {
316         VirtualFilePointerContainerImpl container = myContainers.iterator().next();
317         container.throwDisposalError("Not disposed container");
318       }
319     }
320   }
321
322   private final Set<VirtualFilePointerImpl> myStoredPointers = ContainerUtil.newIdentityTroveSet();
323
324   @TestOnly
325   public void storePointers() {
326     myStoredPointers.clear();
327     addAllPointersTo(myStoredPointers);
328   }
329
330   @TestOnly
331   public void assertPointersAreDisposed() {
332     List<VirtualFilePointerImpl> pointers = new ArrayList<>();
333     addAllPointersTo(pointers);
334     try {
335       for (VirtualFilePointerImpl pointer : pointers) {
336         if (!myStoredPointers.contains(pointer)) {
337           pointer.throwDisposalError("Virtual pointer hasn't been disposed: "+pointer);
338         }
339       }
340     }
341     finally {
342       myStoredPointers.clear();
343     }
344   }
345
346   @TestOnly
347   private void addAllPointersTo(@NotNull Collection<VirtualFilePointerImpl> pointers) {
348     List<FilePointerPartNode> out = new ArrayList<>();
349     for (FilePointerPartNode root : myPointers.values()) {
350       root.addPointersUnder(null, false, "", out);
351     }
352     for (FilePointerPartNode node : out) {
353       node.addAllPointersTo(pointers);
354     }
355   }
356
357   @Override
358   public void dispose() {
359   }
360
361   @Override
362   @NotNull
363   public VirtualFilePointerContainer createContainer(@NotNull Disposable parent) {
364     return createContainer(parent, null);
365   }
366
367   @Override
368   @NotNull
369   public synchronized VirtualFilePointerContainer createContainer(@NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
370     return registerContainer(parent, new VirtualFilePointerContainerImpl(this, parent, listener));
371   }
372
373   @NotNull
374   private VirtualFilePointerContainer registerContainer(@NotNull Disposable parent, @NotNull final VirtualFilePointerContainerImpl virtualFilePointerContainer) {
375     synchronized (myContainers) {
376       myContainers.add(virtualFilePointerContainer);
377     }
378     Disposer.register(parent, new Disposable() {
379       @Override
380       public void dispose() {
381         Disposer.dispose(virtualFilePointerContainer);
382         boolean removed;
383         synchronized (myContainers) {
384           removed = myContainers.remove(virtualFilePointerContainer);
385         }
386         if (!ApplicationManager.getApplication().isUnitTestMode()) {
387           assert removed;
388         }
389       }
390
391       @Override
392       @NonNls
393       @NotNull
394       public String toString() {
395         return "Disposing container " + virtualFilePointerContainer;
396       }
397     });
398     return virtualFilePointerContainer;
399   }
400
401   private List<EventDescriptor> myEvents = Collections.emptyList();
402   private List<FilePointerPartNode> myNodesToUpdateUrl = Collections.emptyList();
403   private List<FilePointerPartNode> myNodesToFire = Collections.emptyList();
404
405   @Override
406   public void before(@NotNull final List<? extends VFileEvent> events) {
407     List<FilePointerPartNode> toFireEvents = new ArrayList<>();
408     List<FilePointerPartNode> toUpdateUrl = new ArrayList<>();
409     VirtualFilePointer[] toFirePointers;
410
411     synchronized (this) {
412       incModificationCount();
413       for (VFileEvent event : events) {
414         if (event instanceof VFileDeleteEvent) {
415           final VFileDeleteEvent deleteEvent = (VFileDeleteEvent)event;
416           addPointersUnder(deleteEvent.getFile(), false, "", toFireEvents);
417
418         }
419         else if (event instanceof VFileCreateEvent) {
420           final VFileCreateEvent createEvent = (VFileCreateEvent)event;
421           addPointersUnder(createEvent.getParent(), true, createEvent.getChildName(), toFireEvents);
422         }
423         else if (event instanceof VFileCopyEvent) {
424           final VFileCopyEvent copyEvent = (VFileCopyEvent)event;
425           addPointersUnder(copyEvent.getNewParent(), true, copyEvent.getFile().getName(), toFireEvents);
426         }
427         else if (event instanceof VFileMoveEvent) {
428           final VFileMoveEvent moveEvent = (VFileMoveEvent)event;
429           VirtualFile eventFile = moveEvent.getFile();
430           addPointersUnder(moveEvent.getNewParent(), true, eventFile.getName(), toFireEvents);
431
432           List<FilePointerPartNode> nodes = new ArrayList<>();
433           addPointersUnder(eventFile, false, "", nodes);
434           for (FilePointerPartNode node : nodes) {
435             VirtualFilePointerImpl pointer = node.getAnyPointer();
436             VirtualFile file = pointer == null ? null : pointer.getFile();
437             if (file != null) {
438               toUpdateUrl.add(node);
439             }
440           }
441         }
442         else if (event instanceof VFilePropertyChangeEvent) {
443           final VFilePropertyChangeEvent change = (VFilePropertyChangeEvent)event;
444           if (VirtualFile.PROP_NAME.equals(change.getPropertyName())
445               && !Comparing.equal(change.getOldValue(), change.getNewValue())) {
446             VirtualFile eventFile = change.getFile();
447             VirtualFile parent = eventFile.getParent(); // e.g. for LightVirtualFiles
448             addPointersUnder(parent, true, change.getNewValue().toString(), toFireEvents);
449
450             List<FilePointerPartNode> nodes = new ArrayList<>();
451             addPointersUnder(eventFile, false, "", nodes);
452             for (FilePointerPartNode node : nodes) {
453               VirtualFilePointerImpl pointer = node.getAnyPointer();
454               VirtualFile file = pointer == null ? null : pointer.getFile();
455               if (file != null) {
456                 toUpdateUrl.add(node);
457               }
458             }
459           }
460         }
461       }
462
463       myEvents = new ArrayList<>();
464       toFirePointers = toPointers(toFireEvents);
465       for (final VirtualFilePointerListener listener : myPointers.keySet()) {
466         if (listener == null) continue;
467         List<VirtualFilePointer> filtered = ContainerUtil.filter(toFirePointers,
468                                                                  pointer -> ((VirtualFilePointerImpl)pointer).getListener() == listener);
469         if (!filtered.isEmpty()) {
470           EventDescriptor event = new EventDescriptor(listener, filtered.toArray(new VirtualFilePointer[filtered.size()]));
471           myEvents.add(event);
472         }
473       }
474     }
475
476     for (EventDescriptor descriptor : myEvents) {
477       descriptor.fireBefore();
478     }
479
480     if (!toFireEvents.isEmpty()) {
481       myBus.syncPublisher(VirtualFilePointerListener.TOPIC).beforeValidityChanged(toFirePointers);
482     }
483
484     myNodesToFire = toFireEvents;
485     myNodesToUpdateUrl = toUpdateUrl;
486
487     assertConsistency();
488   }
489
490   void assertConsistency() {
491     for (FilePointerPartNode root : myPointers.values()) {
492       root.checkConsistency();
493     }
494   }
495
496   @Override
497   public void after(@NotNull final List<? extends VFileEvent> events) {
498     incModificationCount();
499
500     for (FilePointerPartNode node : myNodesToUpdateUrl) {
501       synchronized (this) {
502         String urlBefore = node.myFileAndUrl.second;
503         Pair<VirtualFile,String> after = node.update();
504         String urlAfter = after.second;
505         if (URL_COMPARATOR.compare(urlBefore, urlAfter) != 0 || !urlAfter.endsWith(node.part)) {
506           List<VirtualFilePointerImpl> myPointers = new SmartList<>();
507           node.addAllPointersTo(myPointers);
508
509           // url has changed, reinsert
510           int useCount = node.useCount;
511           FilePointerPartNode root = node.remove();
512           FilePointerPartNode newNode = root.findPointerOrCreate(VfsUtilCore.urlToPath(urlAfter), 0, after, myPointers.size());
513           VirtualFilePointer existingPointer = newNode.getAnyPointer();
514           if (existingPointer != null) {
515             // can happen when e.g. file renamed to the existing file
516             // merge two pointers
517             for (FilePointerPartNode n = newNode; n != null; n = n.parent) {
518               n.pointersUnder += myPointers.size();
519             }
520           }
521           newNode.addAllPointersTo(myPointers);
522           VirtualFilePointerImpl[] newMyPointers = myPointers.toArray(new VirtualFilePointerImpl[myPointers.size()]);
523           newNode.associate(newMyPointers, after);
524           newNode.incrementUsageCount(useCount);
525         }
526       }
527     }
528
529     VirtualFilePointer[] pointersToFireArray = toPointers(myNodesToFire);
530     for (VirtualFilePointer pointer : pointersToFireArray) {
531       ((VirtualFilePointerImpl)pointer).myNode.update();
532     }
533
534     for (EventDescriptor event : myEvents) {
535       event.fireAfter();
536     }
537
538     if (pointersToFireArray.length != 0) {
539       myBus.syncPublisher(VirtualFilePointerListener.TOPIC).validityChanged(pointersToFireArray);
540     }
541
542     myNodesToUpdateUrl = Collections.emptyList();
543     myEvents = Collections.emptyList();
544     myNodesToFire = Collections.emptyList();
545     assertConsistency();
546   }
547
548   void removeNode(@NotNull FilePointerPartNode node, VirtualFilePointerListener listener) {
549     FilePointerPartNode root = node.remove();
550     boolean rootNodeEmpty = root.children.length == 0 ;
551     if (rootNodeEmpty) {
552       myPointers.remove(listener);
553     }
554     assertConsistency();
555   }
556
557   @Override
558   public long getModificationCount() {
559     // depend on PersistentFS.getInstance().getStructureModificationCount() because com.intellij.openapi.vfs.impl.FilePointerPartNode.update is
560     // depend on its own modcount because we need to change both before and after VFS changes
561     return super.getModificationCount() + PersistentFS.getInstance().getStructureModificationCount();
562   }
563
564   private static class DelegatingDisposable implements Disposable {
565     private static final ConcurrentMap<Disposable, DelegatingDisposable> ourInstances = ContainerUtil.newConcurrentMap(ContainerUtil.<Disposable>identityStrategy());
566     private final TObjectIntHashMap<VirtualFilePointerImpl> myCounts = new TObjectIntHashMap<>(); // guarded by this
567     private final Disposable myParent;
568
569     private DelegatingDisposable(@NotNull Disposable parent) {
570       myParent = parent;
571     }
572
573     private static void registerDisposable(@NotNull Disposable parentDisposable, @NotNull VirtualFilePointerImpl pointer) {
574       DelegatingDisposable result = ourInstances.get(parentDisposable);
575       if (result == null) {
576         DelegatingDisposable newDisposable = new DelegatingDisposable(parentDisposable);
577         result = ConcurrencyUtil.cacheOrGet(ourInstances, parentDisposable, newDisposable);
578         if (result == newDisposable) {
579           Disposer.register(parentDisposable, result);
580         }
581       }
582
583       synchronized (result) {
584         result.myCounts.put(pointer, result.myCounts.get(pointer) + 1);
585       }
586     }
587
588     @Override
589     public void dispose() {
590       ourInstances.remove(myParent);
591       synchronized (this) {
592         myCounts.forEachEntry((pointer, disposeCount) -> {
593           int after = pointer.incrementUsageCount(-disposeCount + 1);
594           LOG.assertTrue(after > 0, after);
595           pointer.dispose();
596           return true;
597         });
598       }
599     }
600   }
601
602   @TestOnly
603   int numberOfPointers() {
604     int number = 0;
605     for (FilePointerPartNode root : myPointers.values()) {
606       number = root.numberOfPointersUnder();
607     }
608     return number;
609   }
610
611   @TestOnly
612   int numberOfListeners() {
613     return myPointers.keySet().size();
614   }
615
616   @TestOnly
617   int numberOfCachedUrlToIdentity() {
618     return myUrlToIdentity.size();
619   }
620 }