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