• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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 
17 import org.w3c.dom.Document;
18 import org.w3c.dom.Element;
19 import org.w3c.dom.NamedNodeMap;
20 import org.w3c.dom.Node;
21 import org.w3c.dom.NodeList;
22 import org.xml.sax.InputSource;
23 import org.xml.sax.SAXException;
24 
25 import java.io.BufferedReader;
26 import java.io.BufferedWriter;
27 import java.io.File;
28 import java.io.FileNotFoundException;
29 import java.io.FileReader;
30 import java.io.FileWriter;
31 import java.io.IOException;
32 import java.io.Reader;
33 import java.io.StringReader;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Map.Entry;
41 import java.util.Set;
42 
43 import javax.xml.parsers.DocumentBuilder;
44 import javax.xml.parsers.DocumentBuilderFactory;
45 import javax.xml.parsers.ParserConfigurationException;
46 
47 /**
48  * Gathers statistics about attribute usage in layout files. This is how the "topAttrs"
49  * attributes listed in ADT's extra-view-metadata.xml (which drives the common attributes
50  * listed in the top of the context menu) is determined by running this script on a body
51  * of sample layout code.
52  * <p>
53  * This program takes one or more directory paths, and then it searches all of them recursively
54  * for layout files that are not in folders containing the string "test", and computes and
55  * prints frequency statistics.
56  */
57 public class Analyzer {
58     /** Number of attributes to print for each view */
59     public static final int ATTRIBUTE_COUNT = 6;
60     /** Separate out any attributes that constitute less than N percent of the total */
61     public static final int THRESHOLD = 10; // percent
62 
63     private List<File> mDirectories;
64     private File mCurrentFile;
65     private boolean mListAdvanced;
66 
67     /** Map from view id to map from attribute to frequency count */
68     private Map<String, Map<String, Usage>> mFrequencies =
69             new HashMap<String, Map<String, Usage>>(100);
70 
71     private Map<String, Map<String, Usage>> mLayoutAttributeFrequencies =
72             new HashMap<String, Map<String, Usage>>(100);
73 
74     private Map<String, String> mTopAttributes = new HashMap<String, String>(100);
75     private Map<String, String> mTopLayoutAttributes = new HashMap<String, String>(100);
76 
77     private int mFileVisitCount;
78     private int mLayoutFileCount;
79     private File mXmlMetadataFile;
80 
Analyzer(List<File> directories, File xmlMetadataFile, boolean listAdvanced)81     private Analyzer(List<File> directories, File xmlMetadataFile, boolean listAdvanced) {
82         mDirectories = directories;
83         mXmlMetadataFile = xmlMetadataFile;
84         mListAdvanced = listAdvanced;
85     }
86 
main(String[] args)87     public static void main(String[] args) {
88         if (args.length < 1) {
89             System.err.println("Usage: " + Analyzer.class.getSimpleName()
90                     + " <directory1> [directory2 [directory3 ...]]\n");
91             System.err.println("Recursively scans for layouts in the given directory and");
92             System.err.println("computes statistics about attribute frequencies.");
93             System.exit(-1);
94         }
95 
96         File metadataFile = null;
97         List<File> directories = new ArrayList<File>();
98         boolean listAdvanced = false;
99         for (int i = 0, n = args.length; i < n; i++) {
100             String arg = args[i];
101 
102             if (arg.equals("--list")) {
103                 // List ALL encountered attributes
104                 listAdvanced = true;
105                 continue;
106             }
107 
108             // The -metadata flag takes a pointer to an ADT extra-view-metadata.xml file
109             // and attempts to insert topAttrs attributes into it (and saves it as same
110             // file +.mod as an extension). This isn't listed on the usage flag because
111             // it's pretty brittle and requires some manual fixups to the file afterwards.
112             if (arg.equals("--metadata")) {
113                 i++;
114                 File file = new File(args[i]);
115                 if (!file.exists()) {
116                     System.err.println(file.getName() + " does not exist");
117                     System.exit(-5);
118                 }
119                 if (!file.isFile() || !file.getName().endsWith(".xml")) {
120                     System.err.println(file.getName() + " must be an XML file");
121                     System.exit(-4);
122                 }
123                 metadataFile = file;
124                 continue;
125             }
126             File directory = new File(arg);
127             if (!directory.exists()) {
128                 System.err.println(directory.getName() + " does not exist");
129                 System.exit(-2);
130             }
131 
132             if (!directory.isDirectory()) {
133                 System.err.println(directory.getName() + " is not a directory");
134                 System.exit(-3);
135             }
136 
137             directories.add(directory);
138         }
139 
140         new Analyzer(directories, metadataFile, listAdvanced).analyze();
141     }
142 
analyze()143     private void analyze() {
144         for (File directory : mDirectories) {
145             scanDirectory(directory);
146         }
147 
148         if (mListAdvanced) {
149             listAdvanced();
150         }
151 
152         printStatistics();
153 
154         if (mXmlMetadataFile != null) {
155             printMergedMetadata();
156         }
157     }
158 
scanDirectory(File directory)159     private void scanDirectory(File directory) {
160         File[] files = directory.listFiles();
161         if (files == null) {
162             return;
163         }
164 
165         for (File file : files) {
166             mFileVisitCount++;
167             if (mFileVisitCount % 50000 == 0) {
168                 System.out.println("Analyzed " + mFileVisitCount + " files...");
169             }
170 
171             if (file.isFile()) {
172                 scanFile(file);
173             } else if (file.isDirectory()) {
174                 // Skip stuff related to tests
175                 if (file.getName().contains("test")) {
176                     continue;
177                 }
178 
179                 // Recurse over subdirectories
180                 scanDirectory(file);
181             }
182         }
183     }
184 
scanFile(File file)185     private void scanFile(File file) {
186         if (file.getName().endsWith(".xml")) {
187             File parent = file.getParentFile();
188             if (parent.getName().startsWith("layout")) {
189                 analyzeLayout(file);
190             }
191         }
192 
193     }
194 
analyzeLayout(File file)195     private void analyzeLayout(File file) {
196         mCurrentFile = file;
197         mLayoutFileCount++;
198         Document document = null;
199         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
200         InputSource is = new InputSource(new StringReader(readFile(file)));
201         try {
202             factory.setNamespaceAware(true);
203             factory.setValidating(false);
204             DocumentBuilder builder = factory.newDocumentBuilder();
205             document = builder.parse(is);
206 
207             analyzeDocument(document);
208 
209         } catch (ParserConfigurationException e) {
210             // pass -- ignore files we can't parse
211         } catch (SAXException e) {
212             // pass -- ignore files we can't parse
213         } catch (IOException e) {
214             // pass -- ignore files we can't parse
215         }
216     }
217 
218 
analyzeDocument(Document document)219     private void analyzeDocument(Document document) {
220         analyzeElement(document.getDocumentElement());
221     }
222 
analyzeElement(Element element)223     private void analyzeElement(Element element) {
224         if (element.getTagName().equals("item")) {
225             // Resource files shouldn't be in the layout/ folder but I came across
226             // some cases
227             System.out.println("Warning: found <item> tag in a layout file in "
228                     + mCurrentFile.getPath());
229             return;
230         }
231 
232         countAttributes(element);
233         countLayoutAttributes(element);
234 
235         // Recurse over children
236         NodeList childNodes = element.getChildNodes();
237         for (int i = 0, n = childNodes.getLength(); i < n; i++) {
238             Node child = childNodes.item(i);
239             if (child.getNodeType() == Node.ELEMENT_NODE) {
240                 analyzeElement((Element) child);
241             }
242         }
243     }
244 
countAttributes(Element element)245     private void countAttributes(Element element) {
246         String tag = element.getTagName();
247         Map<String, Usage> attributeMap = mFrequencies.get(tag);
248         if (attributeMap == null) {
249             attributeMap = new HashMap<String, Usage>(70);
250             mFrequencies.put(tag, attributeMap);
251         }
252 
253         NamedNodeMap attributes = element.getAttributes();
254         for (int i = 0, n = attributes.getLength(); i < n; i++) {
255             Node attribute = attributes.item(i);
256             String name = attribute.getNodeName();
257 
258             if (name.startsWith("android:layout_")) {
259                 // Skip layout attributes; they are a function of the parent layout that this
260                 // view is embedded within, not the view itself.
261                 // TODO: Consider whether we should incorporate this info or make statistics
262                 // about that as well?
263                 continue;
264             }
265 
266             if (name.equals("android:id")) {
267                 // Skip ids: they are (mostly) unrelated to the view type and the tool
268                 // already offers id editing prominently
269                 continue;
270             }
271 
272             if (name.startsWith("xmlns:")) {
273                 // Unrelated to frequency counts
274                 continue;
275             }
276 
277             Usage usage = attributeMap.get(name);
278             if (usage == null) {
279                 usage = new Usage(name);
280             } else {
281                 usage.incrementCount();
282             }
283             attributeMap.put(name, usage);
284         }
285     }
286 
countLayoutAttributes(Element element)287     private void countLayoutAttributes(Element element) {
288         String parentTag = element.getParentNode().getNodeName();
289         Map<String, Usage> attributeMap = mLayoutAttributeFrequencies.get(parentTag);
290         if (attributeMap == null) {
291             attributeMap = new HashMap<String, Usage>(70);
292             mLayoutAttributeFrequencies.put(parentTag, attributeMap);
293         }
294 
295         NamedNodeMap attributes = element.getAttributes();
296         for (int i = 0, n = attributes.getLength(); i < n; i++) {
297             Node attribute = attributes.item(i);
298             String name = attribute.getNodeName();
299 
300             if (!name.startsWith("android:layout_")) {
301                 continue;
302             }
303 
304             // Skip layout_width and layout_height; they are mandatory in all but GridLayout so not
305             // very interesting
306             if (name.equals("android:layout_width") || name.equals("android:layout_height")) {
307                 continue;
308             }
309 
310             Usage usage = attributeMap.get(name);
311             if (usage == null) {
312                 usage = new Usage(name);
313             } else {
314                 usage.incrementCount();
315             }
316             attributeMap.put(name, usage);
317         }
318     }
319 
320     // Copied from AdtUtils
readFile(File file)321     private static String readFile(File file) {
322         try {
323             return readFile(new FileReader(file));
324         } catch (FileNotFoundException e) {
325             e.printStackTrace();
326         }
327 
328         return null;
329     }
330 
readFile(Reader inputStream)331     private static String readFile(Reader inputStream) {
332         BufferedReader reader = null;
333         try {
334             reader = new BufferedReader(inputStream);
335             StringBuilder sb = new StringBuilder(2000);
336             while (true) {
337                 int c = reader.read();
338                 if (c == -1) {
339                     return sb.toString();
340                 } else {
341                     sb.append((char)c);
342                 }
343             }
344         } catch (IOException e) {
345             // pass -- ignore files we can't read
346         } finally {
347             try {
348                 if (reader != null) {
349                     reader.close();
350                 }
351             } catch (IOException e) {
352                 e.printStackTrace();
353             }
354         }
355 
356         return null;
357     }
358 
printStatistics()359     private void printStatistics() {
360         System.out.println("Analyzed " + mLayoutFileCount
361                 + " layouts (in a directory trees containing " + mFileVisitCount + " files)");
362         System.out.println("Top " + ATTRIBUTE_COUNT
363                 + " for each view (excluding layout_ attributes) :");
364         System.out.println("\n");
365         System.out.println(" Rank    Count    Share  Attribute");
366         System.out.println("=========================================================");
367         List<String> views = new ArrayList<String>(mFrequencies.keySet());
368         Collections.sort(views);
369         for (String view : views) {
370             String top = processUageMap(view, mFrequencies.get(view));
371             if (top != null) {
372                 mTopAttributes.put(view,  top);
373             }
374         }
375 
376         System.out.println("\n\n\nTop " + ATTRIBUTE_COUNT + " layout attributes (excluding "
377                 + "mandatory layout_width and layout_height):");
378         System.out.println("\n");
379         System.out.println(" Rank    Count    Share  Attribute");
380         System.out.println("=========================================================");
381         views = new ArrayList<String>(mLayoutAttributeFrequencies.keySet());
382         Collections.sort(views);
383         for (String view : views) {
384             String top = processUageMap(view, mLayoutAttributeFrequencies.get(view));
385             if (top != null) {
386                 mTopLayoutAttributes.put(view,  top);
387             }
388         }
389     }
390 
processUageMap(String view, Map<String, Usage> map)391     private static String processUageMap(String view, Map<String, Usage> map) {
392         if (map == null) {
393             return null;
394         }
395 
396         if (view.indexOf('.') != -1 && !view.startsWith("android.")) {
397             // Skip custom views
398             return null;
399         }
400 
401         List<Usage> values = new ArrayList<Usage>(map.values());
402         if (values.size() == 0) {
403             return null;
404         }
405 
406         Collections.sort(values);
407         int totalCount = 0;
408         for (Usage usage : values) {
409             totalCount += usage.count;
410         }
411 
412         System.out.println("\n<" + view + ">:");
413         if (view.equals("#document")) {
414             System.out.println("(Set on root tag, probably intended for included context)");
415         }
416 
417         int place = 1;
418         int count = 0;
419         int prevCount = -1;
420         float prevPercentage = 0f;
421         StringBuilder sb = new StringBuilder();
422         for (Usage usage : values) {
423             if (count++ >= ATTRIBUTE_COUNT && usage.count < prevCount) {
424                 break;
425             }
426 
427             float percentage = 100 * usage.count/(float)totalCount;
428             if (percentage < THRESHOLD && prevPercentage >= THRESHOLD) {
429                 System.out.println("  -----Less than 10%-------------------------------------");
430             }
431             System.out.printf("  %1d.    %5d    %5.1f%%  %s\n", place, usage.count,
432                     percentage, usage.attribute);
433 
434             prevPercentage = percentage;
435             if (prevCount != usage.count) {
436                 prevCount = usage.count;
437                 place++;
438             }
439 
440             if (percentage >= THRESHOLD /*&& usage.count > 1*/) { // 1:Ignore when not enough data?
441                 if (sb.length() > 0) {
442                     sb.append(',');
443                 }
444                 String name = usage.attribute;
445                 if (name.startsWith("android:")) {
446                     name = name.substring("android:".length());
447                 }
448                 sb.append(name);
449             }
450         }
451 
452         return sb.length() > 0 ? sb.toString() : null;
453     }
454 
printMergedMetadata()455     private void printMergedMetadata() {
456         assert mXmlMetadataFile != null;
457         String metadata = readFile(mXmlMetadataFile);
458         if (metadata == null || metadata.length() == 0) {
459             System.err.println("Invalid metadata file");
460             System.exit(-6);
461         }
462 
463         System.err.flush();
464         System.out.println("\n\nUpdating layout metadata file...");
465         System.out.flush();
466 
467         StringBuilder sb = new StringBuilder((int) (2 * mXmlMetadataFile.length()));
468         String[] lines = metadata.split("\n");
469         for (int i = 0; i < lines.length; i++) {
470             String line = lines[i];
471             sb.append(line).append('\n');
472             int classIndex = line.indexOf("class=\"");
473             if (classIndex != -1) {
474                 int start = classIndex + "class=\"".length();
475                 int end = line.indexOf('"', start + 1);
476                 if (end != -1) {
477                     String view = line.substring(start, end);
478                     if (view.startsWith("android.widget.")) {
479                         view = view.substring("android.widget.".length());
480                     } else if (view.startsWith("android.view.")) {
481                         view = view.substring("android.view.".length());
482                     } else if (view.startsWith("android.webkit.")) {
483                         view = view.substring("android.webkit.".length());
484                     }
485                     String top = mTopAttributes.get(view);
486                     if (top == null) {
487                         System.err.println("Warning: No frequency data for view " + view);
488                     } else {
489                         sb.append(line.substring(0, classIndex)); // Indentation
490 
491                         sb.append("topAttrs=\"");
492                         sb.append(top);
493                         sb.append("\"\n");
494                     }
495 
496                     top = mTopLayoutAttributes.get(view);
497                     if (top != null) {
498                         // It's a layout attribute
499                         sb.append(line.substring(0, classIndex)); // Indentation
500 
501                         sb.append("topLayoutAttrs=\"");
502                         sb.append(top);
503                         sb.append("\"\n");
504                     }
505                 }
506             }
507         }
508 
509         System.out.println("\nTop attributes:");
510         System.out.println("--------------------------");
511         List<String> views = new ArrayList<String>(mTopAttributes.keySet());
512         Collections.sort(views);
513         for (String view : views) {
514             String top = mTopAttributes.get(view);
515             System.out.println(view + ": " + top);
516         }
517 
518         System.out.println("\nTop layout attributes:");
519         System.out.println("--------------------------");
520         views = new ArrayList<String>(mTopLayoutAttributes.keySet());
521         Collections.sort(views);
522         for (String view : views) {
523             String top = mTopLayoutAttributes.get(view);
524             System.out.println(view + ": " + top);
525         }
526 
527         System.out.println("\nModified XML metadata file:\n");
528         String newContent = sb.toString();
529         File output = new File(mXmlMetadataFile.getParentFile(), mXmlMetadataFile.getName() + ".mod");
530         if (output.exists()) {
531             output.delete();
532         }
533         try {
534             BufferedWriter writer = new BufferedWriter(new FileWriter(output));
535             writer.write(newContent);
536             writer.close();
537         } catch (IOException e) {
538             e.printStackTrace();
539         }
540         System.out.println("Done - wrote " + output.getPath());
541     }
542 
543     //private File mPublicFile = new File(location, "data/res/values/public.xml");
544     private File mPublicFile = new File("/Volumes/AndroidWork/git/frameworks/base/core/res/res/values/public.xml");
545 
listAdvanced()546     private void listAdvanced() {
547         Set<String> keys = new HashSet<String>(1000);
548 
549         // Merged usages across view types
550         Map<String, Usage> mergedUsages = new HashMap<String, Usage>(100);
551 
552         for (Entry<String,Map<String,Usage>> entry : mFrequencies.entrySet()) {
553             String view = entry.getKey();
554             if (view.indexOf('.') != -1 && !view.startsWith("android.")) {
555                 // Skip custom views etc
556                 continue;
557             }
558             Map<String, Usage> map = entry.getValue();
559             for (Usage usage : map.values()) {
560 //                if (usage.count == 1) {
561 //                    System.out.println("Only found *one* usage of " + usage.attribute);
562 //                }
563 //                if (usage.count < 4) {
564 //                    System.out.println("Only found " + usage.count + " usage of " + usage.attribute);
565 //                }
566 
567                 String attribute = usage.attribute;
568                 int index = attribute.indexOf(':');
569                 if (index == -1 || attribute.startsWith("android:")) {
570                     Usage merged = mergedUsages.get(attribute);
571                     if (merged == null) {
572                         merged = new Usage(attribute);
573                         merged.count = usage.count;
574                         mergedUsages.put(attribute, merged);
575                     } else {
576                         merged.count += usage.count;
577                     }
578                 }
579             }
580         }
581 
582         for (Usage usage : mergedUsages.values())  {
583             String attribute = usage.attribute;
584             if (usage.count < 4) {
585                 System.out.println("Only found " + usage.count + " usage of " + usage.attribute);
586                 continue;
587             }
588             int index = attribute.indexOf(':');
589             if (index != -1) {
590                 attribute = attribute.substring(index + 1); // +1: skip ':'
591             }
592             keys.add(attribute);
593         }
594 
595         List<String> sorted = new ArrayList<String>(keys);
596         Collections.sort(sorted);
597         System.out.println("\nEncountered Attributes");
598         System.out.println("-----------------------------");
599         for (String attribute : sorted) {
600             System.out.println(attribute);
601         }
602 
603         System.out.println();
604     }
605 
606     private static class Usage implements Comparable<Usage> {
607         public String attribute;
608         public int count;
609 
610 
Usage(String attribute)611         public Usage(String attribute) {
612             super();
613             this.attribute = attribute;
614 
615             count = 1;
616         }
617 
incrementCount()618         public void incrementCount() {
619             count++;
620         }
621 
622         @Override
compareTo(Usage o)623         public int compareTo(Usage o) {
624             // Sort by decreasing frequency, then sort alphabetically
625             int frequencyDelta = o.count - count;
626             if (frequencyDelta != 0) {
627                 return frequencyDelta;
628             } else {
629                 return attribute.compareTo(o.attribute);
630             }
631         }
632 
633         @Override
toString()634         public String toString() {
635             return attribute + ": " + count;
636         }
637 
638         @Override
hashCode()639         public int hashCode() {
640             final int prime = 31;
641             int result = 1;
642             result = prime * result + ((attribute == null) ? 0 : attribute.hashCode());
643             return result;
644         }
645 
646         @Override
equals(Object obj)647         public boolean equals(Object obj) {
648             if (this == obj)
649                 return true;
650             if (obj == null)
651                 return false;
652             if (getClass() != obj.getClass())
653                 return false;
654             Usage other = (Usage) obj;
655             if (attribute == null) {
656                 if (other.attribute != null)
657                     return false;
658             } else if (!attribute.equals(other.attribute))
659                 return false;
660             return true;
661         }
662     }
663 }
664