aboutsummaryrefslogtreecommitdiff
path: root/attribute_stats/src/Analyzer.java
diff options
context:
space:
mode:
Diffstat (limited to 'attribute_stats/src/Analyzer.java')
-rw-r--r--attribute_stats/src/Analyzer.java663
1 files changed, 663 insertions, 0 deletions
diff --git a/attribute_stats/src/Analyzer.java b/attribute_stats/src/Analyzer.java
new file mode 100644
index 000000000..8da53ea38
--- /dev/null
+++ b/attribute_stats/src/Analyzer.java
@@ -0,0 +1,663 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * Gathers statistics about attribute usage in layout files. This is how the "topAttrs"
+ * attributes listed in ADT's extra-view-metadata.xml (which drives the common attributes
+ * listed in the top of the context menu) is determined by running this script on a body
+ * of sample layout code.
+ * <p>
+ * This program takes one or more directory paths, and then it searches all of them recursively
+ * for layout files that are not in folders containing the string "test", and computes and
+ * prints frequency statistics.
+ */
+public class Analyzer {
+ /** Number of attributes to print for each view */
+ public static final int ATTRIBUTE_COUNT = 6;
+ /** Separate out any attributes that constitute less than N percent of the total */
+ public static final int THRESHOLD = 10; // percent
+
+ private List<File> mDirectories;
+ private File mCurrentFile;
+ private boolean mListAdvanced;
+
+ /** Map from view id to map from attribute to frequency count */
+ private Map<String, Map<String, Usage>> mFrequencies =
+ new HashMap<String, Map<String, Usage>>(100);
+
+ private Map<String, Map<String, Usage>> mLayoutAttributeFrequencies =
+ new HashMap<String, Map<String, Usage>>(100);
+
+ private Map<String, String> mTopAttributes = new HashMap<String, String>(100);
+ private Map<String, String> mTopLayoutAttributes = new HashMap<String, String>(100);
+
+ private int mFileVisitCount;
+ private int mLayoutFileCount;
+ private File mXmlMetadataFile;
+
+ private Analyzer(List<File> directories, File xmlMetadataFile, boolean listAdvanced) {
+ mDirectories = directories;
+ mXmlMetadataFile = xmlMetadataFile;
+ mListAdvanced = listAdvanced;
+ }
+
+ public static void main(String[] args) {
+ if (args.length < 1) {
+ System.err.println("Usage: " + Analyzer.class.getSimpleName()
+ + " <directory1> [directory2 [directory3 ...]]\n");
+ System.err.println("Recursively scans for layouts in the given directory and");
+ System.err.println("computes statistics about attribute frequencies.");
+ System.exit(-1);
+ }
+
+ File metadataFile = null;
+ List<File> directories = new ArrayList<File>();
+ boolean listAdvanced = false;
+ for (int i = 0, n = args.length; i < n; i++) {
+ String arg = args[i];
+
+ if (arg.equals("--list")) {
+ // List ALL encountered attributes
+ listAdvanced = true;
+ continue;
+ }
+
+ // The -metadata flag takes a pointer to an ADT extra-view-metadata.xml file
+ // and attempts to insert topAttrs attributes into it (and saves it as same
+ // file +.mod as an extension). This isn't listed on the usage flag because
+ // it's pretty brittle and requires some manual fixups to the file afterwards.
+ if (arg.equals("--metadata")) {
+ i++;
+ File file = new File(args[i]);
+ if (!file.exists()) {
+ System.err.println(file.getName() + " does not exist");
+ System.exit(-5);
+ }
+ if (!file.isFile() || !file.getName().endsWith(".xml")) {
+ System.err.println(file.getName() + " must be an XML file");
+ System.exit(-4);
+ }
+ metadataFile = file;
+ continue;
+ }
+ File directory = new File(arg);
+ if (!directory.exists()) {
+ System.err.println(directory.getName() + " does not exist");
+ System.exit(-2);
+ }
+
+ if (!directory.isDirectory()) {
+ System.err.println(directory.getName() + " is not a directory");
+ System.exit(-3);
+ }
+
+ directories.add(directory);
+ }
+
+ new Analyzer(directories, metadataFile, listAdvanced).analyze();
+ }
+
+ private void analyze() {
+ for (File directory : mDirectories) {
+ scanDirectory(directory);
+ }
+
+ if (mListAdvanced) {
+ listAdvanced();
+ }
+
+ printStatistics();
+
+ if (mXmlMetadataFile != null) {
+ printMergedMetadata();
+ }
+ }
+
+ private void scanDirectory(File directory) {
+ File[] files = directory.listFiles();
+ if (files == null) {
+ return;
+ }
+
+ for (File file : files) {
+ mFileVisitCount++;
+ if (mFileVisitCount % 50000 == 0) {
+ System.out.println("Analyzed " + mFileVisitCount + " files...");
+ }
+
+ if (file.isFile()) {
+ scanFile(file);
+ } else if (file.isDirectory()) {
+ // Skip stuff related to tests
+ if (file.getName().contains("test")) {
+ continue;
+ }
+
+ // Recurse over subdirectories
+ scanDirectory(file);
+ }
+ }
+ }
+
+ private void scanFile(File file) {
+ if (file.getName().endsWith(".xml")) {
+ File parent = file.getParentFile();
+ if (parent.getName().startsWith("layout")) {
+ analyzeLayout(file);
+ }
+ }
+
+ }
+
+ private void analyzeLayout(File file) {
+ mCurrentFile = file;
+ mLayoutFileCount++;
+ Document document = null;
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ InputSource is = new InputSource(new StringReader(readFile(file)));
+ try {
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ document = builder.parse(is);
+
+ analyzeDocument(document);
+
+ } catch (ParserConfigurationException e) {
+ // pass -- ignore files we can't parse
+ } catch (SAXException e) {
+ // pass -- ignore files we can't parse
+ } catch (IOException e) {
+ // pass -- ignore files we can't parse
+ }
+ }
+
+
+ private void analyzeDocument(Document document) {
+ analyzeElement(document.getDocumentElement());
+ }
+
+ private void analyzeElement(Element element) {
+ if (element.getTagName().equals("item")) {
+ // Resource files shouldn't be in the layout/ folder but I came across
+ // some cases
+ System.out.println("Warning: found <item> tag in a layout file in "
+ + mCurrentFile.getPath());
+ return;
+ }
+
+ countAttributes(element);
+ countLayoutAttributes(element);
+
+ // Recurse over children
+ NodeList childNodes = element.getChildNodes();
+ for (int i = 0, n = childNodes.getLength(); i < n; i++) {
+ Node child = childNodes.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ analyzeElement((Element) child);
+ }
+ }
+ }
+
+ private void countAttributes(Element element) {
+ String tag = element.getTagName();
+ Map<String, Usage> attributeMap = mFrequencies.get(tag);
+ if (attributeMap == null) {
+ attributeMap = new HashMap<String, Usage>(70);
+ mFrequencies.put(tag, attributeMap);
+ }
+
+ NamedNodeMap attributes = element.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attribute = attributes.item(i);
+ String name = attribute.getNodeName();
+
+ if (name.startsWith("android:layout_")) {
+ // Skip layout attributes; they are a function of the parent layout that this
+ // view is embedded within, not the view itself.
+ // TODO: Consider whether we should incorporate this info or make statistics
+ // about that as well?
+ continue;
+ }
+
+ if (name.equals("android:id")) {
+ // Skip ids: they are (mostly) unrelated to the view type and the tool
+ // already offers id editing prominently
+ continue;
+ }
+
+ if (name.startsWith("xmlns:")) {
+ // Unrelated to frequency counts
+ continue;
+ }
+
+ Usage usage = attributeMap.get(name);
+ if (usage == null) {
+ usage = new Usage(name);
+ } else {
+ usage.incrementCount();
+ }
+ attributeMap.put(name, usage);
+ }
+ }
+
+ private void countLayoutAttributes(Element element) {
+ String parentTag = element.getParentNode().getNodeName();
+ Map<String, Usage> attributeMap = mLayoutAttributeFrequencies.get(parentTag);
+ if (attributeMap == null) {
+ attributeMap = new HashMap<String, Usage>(70);
+ mLayoutAttributeFrequencies.put(parentTag, attributeMap);
+ }
+
+ NamedNodeMap attributes = element.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attribute = attributes.item(i);
+ String name = attribute.getNodeName();
+
+ if (!name.startsWith("android:layout_")) {
+ continue;
+ }
+
+ // Skip layout_width and layout_height; they are mandatory in all but GridLayout so not
+ // very interesting
+ if (name.equals("android:layout_width") || name.equals("android:layout_height")) {
+ continue;
+ }
+
+ Usage usage = attributeMap.get(name);
+ if (usage == null) {
+ usage = new Usage(name);
+ } else {
+ usage.incrementCount();
+ }
+ attributeMap.put(name, usage);
+ }
+ }
+
+ // Copied from AdtUtils
+ private static String readFile(File file) {
+ try {
+ return readFile(new FileReader(file));
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ private static String readFile(Reader inputStream) {
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(inputStream);
+ StringBuilder sb = new StringBuilder(2000);
+ while (true) {
+ int c = reader.read();
+ if (c == -1) {
+ return sb.toString();
+ } else {
+ sb.append((char)c);
+ }
+ }
+ } catch (IOException e) {
+ // pass -- ignore files we can't read
+ } finally {
+ try {
+ if (reader != null) {
+ reader.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return null;
+ }
+
+ private void printStatistics() {
+ System.out.println("Analyzed " + mLayoutFileCount
+ + " layouts (in a directory trees containing " + mFileVisitCount + " files)");
+ System.out.println("Top " + ATTRIBUTE_COUNT
+ + " for each view (excluding layout_ attributes) :");
+ System.out.println("\n");
+ System.out.println(" Rank Count Share Attribute");
+ System.out.println("=========================================================");
+ List<String> views = new ArrayList<String>(mFrequencies.keySet());
+ Collections.sort(views);
+ for (String view : views) {
+ String top = processUageMap(view, mFrequencies.get(view));
+ if (top != null) {
+ mTopAttributes.put(view, top);
+ }
+ }
+
+ System.out.println("\n\n\nTop " + ATTRIBUTE_COUNT + " layout attributes (excluding "
+ + "mandatory layout_width and layout_height):");
+ System.out.println("\n");
+ System.out.println(" Rank Count Share Attribute");
+ System.out.println("=========================================================");
+ views = new ArrayList<String>(mLayoutAttributeFrequencies.keySet());
+ Collections.sort(views);
+ for (String view : views) {
+ String top = processUageMap(view, mLayoutAttributeFrequencies.get(view));
+ if (top != null) {
+ mTopLayoutAttributes.put(view, top);
+ }
+ }
+ }
+
+ private static String processUageMap(String view, Map<String, Usage> map) {
+ if (map == null) {
+ return null;
+ }
+
+ if (view.indexOf('.') != -1 && !view.startsWith("android.")) {
+ // Skip custom views
+ return null;
+ }
+
+ List<Usage> values = new ArrayList<Usage>(map.values());
+ if (values.size() == 0) {
+ return null;
+ }
+
+ Collections.sort(values);
+ int totalCount = 0;
+ for (Usage usage : values) {
+ totalCount += usage.count;
+ }
+
+ System.out.println("\n<" + view + ">:");
+ if (view.equals("#document")) {
+ System.out.println("(Set on root tag, probably intended for included context)");
+ }
+
+ int place = 1;
+ int count = 0;
+ int prevCount = -1;
+ float prevPercentage = 0f;
+ StringBuilder sb = new StringBuilder();
+ for (Usage usage : values) {
+ if (count++ >= ATTRIBUTE_COUNT && usage.count < prevCount) {
+ break;
+ }
+
+ float percentage = 100 * usage.count/(float)totalCount;
+ if (percentage < THRESHOLD && prevPercentage >= THRESHOLD) {
+ System.out.println(" -----Less than 10%-------------------------------------");
+ }
+ System.out.printf(" %1d. %5d %5.1f%% %s\n", place, usage.count,
+ percentage, usage.attribute);
+
+ prevPercentage = percentage;
+ if (prevCount != usage.count) {
+ prevCount = usage.count;
+ place++;
+ }
+
+ if (percentage >= THRESHOLD /*&& usage.count > 1*/) { // 1:Ignore when not enough data?
+ if (sb.length() > 0) {
+ sb.append(',');
+ }
+ String name = usage.attribute;
+ if (name.startsWith("android:")) {
+ name = name.substring("android:".length());
+ }
+ sb.append(name);
+ }
+ }
+
+ return sb.length() > 0 ? sb.toString() : null;
+ }
+
+ private void printMergedMetadata() {
+ assert mXmlMetadataFile != null;
+ String metadata = readFile(mXmlMetadataFile);
+ if (metadata == null || metadata.length() == 0) {
+ System.err.println("Invalid metadata file");
+ System.exit(-6);
+ }
+
+ System.err.flush();
+ System.out.println("\n\nUpdating layout metadata file...");
+ System.out.flush();
+
+ StringBuilder sb = new StringBuilder((int) (2 * mXmlMetadataFile.length()));
+ String[] lines = metadata.split("\n");
+ for (int i = 0; i < lines.length; i++) {
+ String line = lines[i];
+ sb.append(line).append('\n');
+ int classIndex = line.indexOf("class=\"");
+ if (classIndex != -1) {
+ int start = classIndex + "class=\"".length();
+ int end = line.indexOf('"', start + 1);
+ if (end != -1) {
+ String view = line.substring(start, end);
+ if (view.startsWith("android.widget.")) {
+ view = view.substring("android.widget.".length());
+ } else if (view.startsWith("android.view.")) {
+ view = view.substring("android.view.".length());
+ } else if (view.startsWith("android.webkit.")) {
+ view = view.substring("android.webkit.".length());
+ }
+ String top = mTopAttributes.get(view);
+ if (top == null) {
+ System.err.println("Warning: No frequency data for view " + view);
+ } else {
+ sb.append(line.substring(0, classIndex)); // Indentation
+
+ sb.append("topAttrs=\"");
+ sb.append(top);
+ sb.append("\"\n");
+ }
+
+ top = mTopLayoutAttributes.get(view);
+ if (top != null) {
+ // It's a layout attribute
+ sb.append(line.substring(0, classIndex)); // Indentation
+
+ sb.append("topLayoutAttrs=\"");
+ sb.append(top);
+ sb.append("\"\n");
+ }
+ }
+ }
+ }
+
+ System.out.println("\nTop attributes:");
+ System.out.println("--------------------------");
+ List<String> views = new ArrayList<String>(mTopAttributes.keySet());
+ Collections.sort(views);
+ for (String view : views) {
+ String top = mTopAttributes.get(view);
+ System.out.println(view + ": " + top);
+ }
+
+ System.out.println("\nTop layout attributes:");
+ System.out.println("--------------------------");
+ views = new ArrayList<String>(mTopLayoutAttributes.keySet());
+ Collections.sort(views);
+ for (String view : views) {
+ String top = mTopLayoutAttributes.get(view);
+ System.out.println(view + ": " + top);
+ }
+
+ System.out.println("\nModified XML metadata file:\n");
+ String newContent = sb.toString();
+ File output = new File(mXmlMetadataFile.getParentFile(), mXmlMetadataFile.getName() + ".mod");
+ if (output.exists()) {
+ output.delete();
+ }
+ try {
+ BufferedWriter writer = new BufferedWriter(new FileWriter(output));
+ writer.write(newContent);
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ System.out.println("Done - wrote " + output.getPath());
+ }
+
+ //private File mPublicFile = new File(location, "data/res/values/public.xml");
+ private File mPublicFile = new File("/Volumes/AndroidWork/git/frameworks/base/core/res/res/values/public.xml");
+
+ private void listAdvanced() {
+ Set<String> keys = new HashSet<String>(1000);
+
+ // Merged usages across view types
+ Map<String, Usage> mergedUsages = new HashMap<String, Usage>(100);
+
+ for (Entry<String,Map<String,Usage>> entry : mFrequencies.entrySet()) {
+ String view = entry.getKey();
+ if (view.indexOf('.') != -1 && !view.startsWith("android.")) {
+ // Skip custom views etc
+ continue;
+ }
+ Map<String, Usage> map = entry.getValue();
+ for (Usage usage : map.values()) {
+// if (usage.count == 1) {
+// System.out.println("Only found *one* usage of " + usage.attribute);
+// }
+// if (usage.count < 4) {
+// System.out.println("Only found " + usage.count + " usage of " + usage.attribute);
+// }
+
+ String attribute = usage.attribute;
+ int index = attribute.indexOf(':');
+ if (index == -1 || attribute.startsWith("android:")) {
+ Usage merged = mergedUsages.get(attribute);
+ if (merged == null) {
+ merged = new Usage(attribute);
+ merged.count = usage.count;
+ mergedUsages.put(attribute, merged);
+ } else {
+ merged.count += usage.count;
+ }
+ }
+ }
+ }
+
+ for (Usage usage : mergedUsages.values()) {
+ String attribute = usage.attribute;
+ if (usage.count < 4) {
+ System.out.println("Only found " + usage.count + " usage of " + usage.attribute);
+ continue;
+ }
+ int index = attribute.indexOf(':');
+ if (index != -1) {
+ attribute = attribute.substring(index + 1); // +1: skip ':'
+ }
+ keys.add(attribute);
+ }
+
+ List<String> sorted = new ArrayList<String>(keys);
+ Collections.sort(sorted);
+ System.out.println("\nEncountered Attributes");
+ System.out.println("-----------------------------");
+ for (String attribute : sorted) {
+ System.out.println(attribute);
+ }
+
+ System.out.println();
+ }
+
+ private static class Usage implements Comparable<Usage> {
+ public String attribute;
+ public int count;
+
+
+ public Usage(String attribute) {
+ super();
+ this.attribute = attribute;
+
+ count = 1;
+ }
+
+ public void incrementCount() {
+ count++;
+ }
+
+ @Override
+ public int compareTo(Usage o) {
+ // Sort by decreasing frequency, then sort alphabetically
+ int frequencyDelta = o.count - count;
+ if (frequencyDelta != 0) {
+ return frequencyDelta;
+ } else {
+ return attribute.compareTo(o.attribute);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return attribute + ": " + count;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((attribute == null) ? 0 : attribute.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Usage other = (Usage) obj;
+ if (attribute == null) {
+ if (other.attribute != null)
+ return false;
+ } else if (!attribute.equals(other.attribute))
+ return false;
+ return true;
+ }
+ }
+}