auto import from //depot/cupcake/@135843
diff --git a/tools/idegen/src/Configuration.java b/tools/idegen/src/Configuration.java
new file mode 100644
index 0000000..392cb5d
--- /dev/null
+++ b/tools/idegen/src/Configuration.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 java.io.File;
+import java.io.IOException;
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.SortedSet;
+import java.util.regex.Pattern;
+
+/**
+ * Immutable representation of an IDE configuration. Assumes that the current
+ * directory is the project's root directory.
+ */
+public class Configuration {
+
+ /** Java source tree roots. */
+ public final SortedSet<File> sourceRoots;
+
+ /** Found .jar files (that weren't excluded). */
+ public final List<File> jarFiles;
+
+ /** Excluded directories which may or may not be under a source root. */
+ public final SortedSet<File> excludedDirs;
+
+ /** The root directory for this tool. */
+ public final File toolDirectory;
+
+ /** File name used for excluded path files. */
+ private static final String EXCLUDED_PATHS = "excluded-paths";
+
+ /**
+ * Constructs a Configuration by traversing the directory tree, looking
+ * for .java and .jar files and identifying source roots.
+ */
+ public Configuration() throws IOException {
+ this.toolDirectory = new File("development/tools/idegen");
+ if (!toolDirectory.isDirectory()) {
+ // The wrapper script should have already verified this.
+ throw new AssertionError("Not in root directory.");
+ }
+
+ Stopwatch stopwatch = new Stopwatch();
+
+ Excludes excludes = readExcludes();
+
+ stopwatch.reset("Read excludes");
+
+ List<File> jarFiles = new ArrayList<File>(500);
+ SortedSet<File> excludedDirs = new TreeSet<File>();
+ SortedSet<File> sourceRoots = new TreeSet<File>();
+
+ traverse(new File("."), sourceRoots, jarFiles, excludedDirs, excludes);
+
+ stopwatch.reset("Traversed tree");
+
+ Log.debug(sourceRoots.size() + " source roots");
+ Log.debug(jarFiles.size() + " jar files");
+ Log.debug(excludedDirs.size() + " excluded dirs");
+
+ this.sourceRoots = Collections.unmodifiableSortedSet(sourceRoots);
+ this.jarFiles = Collections.unmodifiableList(jarFiles);
+ this.excludedDirs = Collections.unmodifiableSortedSet(excludedDirs);
+ }
+
+ /**
+ * Reads excluded path files.
+ */
+ private Excludes readExcludes() throws IOException {
+ List<Pattern> patterns = new ArrayList<Pattern>();
+
+ File globalExcludes = new File(toolDirectory, EXCLUDED_PATHS);
+ parseFile(globalExcludes, patterns);
+
+ // Look for Google-specific excludes.
+ // TODO: Traverse all vendor-specific directories.
+ File googleExcludes = new File("./vendor/google/" + EXCLUDED_PATHS);
+ if (googleExcludes.exists()) {
+ parseFile(googleExcludes, patterns);
+ }
+
+ // Look for user-specific excluded-paths file in current directory.
+ File localExcludes = new File(EXCLUDED_PATHS);
+ if (localExcludes.exists()) {
+ parseFile(localExcludes, patterns);
+ }
+
+ return new Excludes(patterns);
+ }
+
+ /**
+ * Recursively finds .java source roots, .jar files, and excluded
+ * directories.
+ */
+ private static void traverse(File directory, Set<File> sourceRoots,
+ Collection<File> jarFiles, Collection<File> excludedDirs,
+ Excludes excludes) throws IOException {
+ /*
+ * Note it would be faster to stop traversing a source root as soon as
+ * we encounter the first .java file, but it appears we have nested
+ * source roots in our generated source directory (specifically,
+ * R.java files and aidl .java files don't share the same source
+ * root).
+ */
+
+ boolean firstJavaFile = true;
+ for (File file : directory.listFiles()) {
+ // Trim preceding "./" from path.
+ String path = file.getPath().substring(2);
+
+ // Keep track of source roots for .java files.
+ if (path.endsWith(".java")) {
+ if (firstJavaFile) {
+ // Only parse one .java file per directory.
+ firstJavaFile = false;
+
+ File sourceRoot = rootOf(file);
+ if (sourceRoot != null) {
+ sourceRoots.add(sourceRoot);
+ }
+ }
+
+ continue;
+ }
+
+ // Keep track of .jar files.
+ if (path.endsWith(".jar")) {
+ if (!excludes.exclude(path)) {
+ jarFiles.add(file);
+ } else {
+ Log.debug("Skipped: " + file);
+ }
+
+ continue;
+ }
+
+ // Traverse nested directories.
+ if (file.isDirectory()) {
+ if (excludes.exclude(path)) {
+ // Don't recurse into excluded dirs.
+ Log.debug("Excluding: " + path);
+ excludedDirs.add(file);
+ } else {
+ traverse(file, sourceRoots, jarFiles, excludedDirs,
+ excludes);
+ }
+ }
+ }
+ }
+
+ /**
+ * Determines the source root for a given .java file. Returns null
+ * if the file doesn't have a package or if the file isn't in the
+ * correct directory structure.
+ */
+ private static File rootOf(File javaFile) throws IOException {
+ String packageName = parsePackageName(javaFile);
+ if (packageName == null) {
+ // No package.
+ // TODO: Treat this as a source root?
+ return null;
+ }
+
+ String packagePath = packageName.replace('.', File.separatorChar);
+ File parent = javaFile.getParentFile();
+ String parentPath = parent.getPath();
+ if (!parentPath.endsWith(packagePath)) {
+ // Bad dir structure.
+ return null;
+ }
+
+ return new File(parentPath.substring(
+ 0, parentPath.length() - packagePath.length()));
+ }
+
+ /**
+ * Reads a Java file and parses out the package name. Returns null if none
+ * found.
+ */
+ private static String parsePackageName(File file) throws IOException {
+ BufferedReader in = new BufferedReader(new FileReader(file));
+ try {
+ String line;
+ while ((line = in.readLine()) != null) {
+ String trimmed = line.trim();
+ if (trimmed.startsWith("package")) {
+ // TODO: Make this more robust.
+ // Assumes there's only once space after "package" and the
+ // line ends in a ";".
+ return trimmed.substring(8, trimmed.length() - 1);
+ }
+ }
+
+ return null;
+ } finally {
+ in.close();
+ }
+ }
+
+ /**
+ * Picks out excluded directories that are under source roots.
+ */
+ public SortedSet<File> excludesUnderSourceRoots() {
+ // TODO: Refactor this to share the similar logic in
+ // Eclipse.constructExcluding().
+ SortedSet<File> picked = new TreeSet<File>();
+ for (File sourceRoot : sourceRoots) {
+ String sourcePath = sourceRoot.getPath() + "/";
+ SortedSet<File> tailSet = excludedDirs.tailSet(sourceRoot);
+ for (File file : tailSet) {
+ if (file.getPath().startsWith(sourcePath)) {
+ picked.add(file);
+ } else {
+ break;
+ }
+ }
+ }
+ return picked;
+ }
+
+ /**
+ * Reads a list of regular expressions from a file, one per line, and adds
+ * the compiled patterns to the given collection. Ignores lines starting
+ * with '#'.
+ *
+ * @param file containing regular expressions, one per line
+ * @param patterns collection to add compiled patterns from file to
+ */
+ public static void parseFile(File file, Collection<Pattern> patterns)
+ throws IOException {
+ BufferedReader in = new BufferedReader(new FileReader(file));
+ try {
+ String line;
+ while ((line = in.readLine()) != null) {
+ String trimmed = line.trim();
+ if (trimmed.length() > 0 && !trimmed.startsWith("#")) {
+ patterns.add(Pattern.compile(trimmed));
+ }
+ }
+ } finally {
+ in.close();
+ }
+ }
+}
diff --git a/tools/idegen/src/Eclipse.java b/tools/idegen/src/Eclipse.java
new file mode 100644
index 0000000..403c7d8
--- /dev/null
+++ b/tools/idegen/src/Eclipse.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 java.io.File;
+import java.io.IOException;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.Collection;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Generates an Eclipse project.
+ */
+public class Eclipse {
+
+ /**
+ * Generates an Eclipse .classpath file from the given configuration.
+ */
+ public static void generateFrom(Configuration c) throws IOException {
+ StringBuilder classpath = new StringBuilder();
+
+ classpath.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ + "<classpath>\n");
+
+ /*
+ * If the user has a file named "path-precedence" in their project's
+ * root directory, we'll order source roots based on how they match
+ * regular expressions in that file. Source roots that match earlier
+ * patterns will come sooner in configuration file.
+ */
+ List<Pattern> patterns = new ArrayList<Pattern>();
+
+ File precedence = new File("path-precedence");
+ if (precedence.exists()) {
+ Configuration.parseFile(precedence, patterns);
+ } else {
+ // Put ./out at the bottom by default.
+ patterns.add(Pattern.compile("^(?!out/)"));
+ }
+
+ // Everything not matched by the user's precedence spec.
+ patterns.add(Pattern.compile(".*"));
+
+
+ List<Bucket> buckets = new ArrayList<Bucket>(patterns.size());
+ for (Pattern pattern : patterns) {
+ buckets.add(new Bucket(pattern));
+ }
+
+ // Put source roots in respective buckets.
+ OUTER: for (File sourceRoot : c.sourceRoots) {
+ // Trim preceding "./" from path.
+ String path = sourceRoot.getPath().substring(2);
+
+ for (Bucket bucket : buckets) {
+ if (bucket.matches(path)) {
+ bucket.sourceRoots.add(sourceRoot);
+ continue OUTER;
+ }
+ }
+ }
+
+ // Output source roots to configuration file.
+ for (Bucket bucket : buckets) {
+ for (File sourceRoot : bucket.sourceRoots) {
+ classpath.append(" <classpathentry kind=\"src\"");
+ CharSequence excluding = constructExcluding(sourceRoot, c);
+ if (excluding.length() > 0) {
+ classpath.append(" excluding=\"")
+ .append(excluding).append("\"");
+ }
+ classpath.append(" path=\"")
+ .append(trimmed(sourceRoot)).append("\"/>\n");
+ }
+
+ }
+
+ // Output .jar entries.
+ for (File jar : c.jarFiles) {
+ classpath.append(" <classpathentry kind=\"lib\" path=\"")
+ .append(trimmed(jar)).append("\"/>\n");
+ }
+
+ /*
+ * Output directory. Unfortunately, Eclipse forces us to put it
+ * somewhere under the project directory.
+ */
+ classpath.append(" <classpathentry kind=\"output\" path=\""
+ + "out/eclipse\"/>\n");
+
+ classpath.append("</classpath>\n");
+
+ Files.toFile(classpath.toString(), new File(".classpath"));
+ }
+
+
+ /**
+ * Constructs the "excluding" argument for a given source root.
+ */
+ private static CharSequence constructExcluding(File sourceRoot,
+ Configuration c) {
+ StringBuilder classpath = new StringBuilder();
+ String path = sourceRoot.getPath();
+
+ // Exclude nested source roots.
+ SortedSet<File> nextRoots = c.sourceRoots.tailSet(sourceRoot);
+ int count = 0;
+ for (File nextRoot : nextRoots) {
+ // The first root is this root.
+ if (count == 0) {
+ count++;
+ continue;
+ }
+
+ String nextPath = nextRoot.getPath();
+ if (!nextPath.startsWith(path)) {
+ break;
+ }
+
+ if (count > 1) {
+ classpath.append('|');
+ }
+ classpath.append(nextPath.substring(path.length() + 1))
+ .append('/');
+
+ count++;
+ }
+
+ // Exclude excluded directories under this source root.
+ SortedSet<File> excludedDirs = c.excludedDirs.tailSet(sourceRoot);
+ for (File excludedDir : excludedDirs) {
+ String excludedPath = excludedDir.getPath();
+ if (!excludedPath.startsWith(path)) {
+ break;
+ }
+
+ if (count > 1) {
+ classpath.append('|');
+ }
+ classpath.append(excludedPath.substring(path.length() + 1))
+ .append('/');
+
+ count++;
+ }
+
+ return classpath;
+ }
+
+ /**
+ * Returns the trimmed path.
+ */
+ private static String trimmed(File file) {
+ return file.getPath().substring(2);
+ }
+
+ /**
+ * A precedence bucket for source roots.
+ */
+ private static class Bucket {
+
+ private final Pattern pattern;
+ private final List<File> sourceRoots = new ArrayList<File>();
+
+ private Bucket(Pattern pattern) {
+ this.pattern = pattern;
+ }
+
+ private boolean matches(String path) {
+ return pattern.matcher(path).find();
+ }
+ }
+}
diff --git a/tools/idegen/src/Excludes.java b/tools/idegen/src/Excludes.java
new file mode 100644
index 0000000..8531d47
--- /dev/null
+++ b/tools/idegen/src/Excludes.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 java.util.regex.Pattern;
+import java.util.List;
+
+/**
+ * Decides whether or not to exclude certain paths.
+ */
+public class Excludes {
+
+ private final List<Pattern> patterns;
+
+ /**
+ * Constructs a set of excludes matching the given patterns.
+ */
+ public Excludes(List<Pattern> patterns) {
+ this.patterns = patterns;
+ }
+
+ /**
+ * Returns true if the given path should be excluded.
+ */
+ public boolean exclude(String path) {
+ for (Pattern pattern : patterns) {
+ if (pattern.matcher(path).find()) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/tools/idegen/src/Files.java b/tools/idegen/src/Files.java
new file mode 100644
index 0000000..81176ee
--- /dev/null
+++ b/tools/idegen/src/Files.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 java.io.*;
+
+/**
+ * File utility methods.
+ */
+class Files {
+
+ /**
+ * Reads file into a string using default encoding.
+ */
+ static String toString(File file) throws IOException {
+ char[] buffer = new char[0x1000]; // 4k
+ int read;
+ Reader in = new FileReader(file);
+ StringBuilder builder = new StringBuilder();
+ while ((read = in.read(buffer)) > -1) {
+ builder.append(buffer, 0, read);
+ }
+ in.close();
+ return builder.toString();
+ }
+
+ /**
+ * Writes a string to a file using default encoding.
+ */
+ static void toFile(String contents, File file) throws IOException {
+ FileWriter out = new FileWriter(file);
+ out.write(contents);
+ out.close();
+ }
+}
\ No newline at end of file
diff --git a/tools/idegen/src/IntelliJ.java b/tools/idegen/src/IntelliJ.java
new file mode 100644
index 0000000..00d731d
--- /dev/null
+++ b/tools/idegen/src/IntelliJ.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 java.io.File;
+import java.io.IOException;
+import java.util.SortedSet;
+
+/**
+ * Generates an IntelliJ project.
+ */
+public class IntelliJ {
+
+ private static final String IDEA_IML = "android.iml";
+ private static final String IDEA_IPR = "android.ipr";
+
+ /**
+ * Generates IntelliJ configuration files from the given configuration.
+ */
+ public static void generateFrom(Configuration c) throws IOException {
+ File templatesDirectory = new File(c.toolDirectory, "templates");
+ String ipr = Files.toString(new File(templatesDirectory, IDEA_IPR));
+ Files.toFile(ipr, new File(IDEA_IPR));
+
+ String iml = Files.toString(new File(templatesDirectory, IDEA_IML));
+
+ StringBuilder sourceRootsXml = new StringBuilder();
+ for (File sourceRoot : c.sourceRoots) {
+ sourceRootsXml.append("<sourceFolder url=\"file://$MODULE_DIR$/")
+ .append(sourceRoot.getPath())
+ .append("\" isTestSource=\"").append(isTests(sourceRoot))
+ .append("\"/>\n");
+ }
+
+ /*
+ * IntelliJ excludes are module-wide. We explicitly exclude directories
+ * under source roots but leave the rest in so you can still pull
+ * up random non-Java files.
+ */
+ StringBuilder excludeXml = new StringBuilder();
+ for (File excludedDir : c.excludesUnderSourceRoots()) {
+ sourceRootsXml.append("<excludeFolder url=\"file://$MODULE_DIR$/")
+ .append(excludedDir.getPath())
+ .append("\"/>\n");
+ }
+
+ // Exclude Eclipse's output directory.
+ sourceRootsXml.append("<excludeFolder "
+ + "url=\"file://$MODULE_DIR$/out/eclipse\"/>\n");
+
+ StringBuilder jarsXml = new StringBuilder();
+ for (File jar : c.jarFiles) {
+ jarsXml.append("<orderEntry type=\"module-library\">"
+ + "<library><CLASSES><root url=\"jar://$MODULE_DIR$/")
+ .append(jar.getPath())
+ .append("!/\"/></CLASSES><JAVADOC/><SOURCES/></library>"
+ + "</orderEntry>\n");
+ }
+
+ iml = iml.replace("SOURCE_FOLDERS",
+ sourceRootsXml.toString() + excludeXml.toString());
+ iml = iml.replace("JAR_ENTRIES", jarsXml.toString());
+
+ Files.toFile(iml, new File(IDEA_IML));
+ }
+
+ private static boolean isTests(File file) {
+ String path = file.getPath();
+
+ // test-runner is testing infrastructure, not test code.
+ if (path.contains("test-runner")) {
+ return false;
+ }
+
+ return path.toUpperCase().contains("TEST");
+ }
+}
\ No newline at end of file
diff --git a/tools/idegen/src/Log.java b/tools/idegen/src/Log.java
new file mode 100644
index 0000000..e35d060
--- /dev/null
+++ b/tools/idegen/src/Log.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.
+ */
+
+/**
+ * Logs messages.
+ */
+class Log {
+
+ static final boolean DEBUG = false;
+
+ static void debug(String message) {
+ if (DEBUG) {
+ info(message);
+ }
+ }
+
+ static void info(String message) {
+ System.out.println(message);
+ }
+}
diff --git a/tools/idegen/src/Main.java b/tools/idegen/src/Main.java
new file mode 100644
index 0000000..294dbee
--- /dev/null
+++ b/tools/idegen/src/Main.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 java.io.File;
+import java.io.FileFilter;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * Generates IntelliJ and Eclipse project configurations.
+ */
+public class Main {
+
+ public static void main(String[] args) throws Exception {
+ Configuration configuration = new Configuration();
+ IntelliJ.generateFrom(configuration);
+ Eclipse.generateFrom(configuration);
+ }
+}
diff --git a/tools/idegen/src/Stopwatch.java b/tools/idegen/src/Stopwatch.java
new file mode 100644
index 0000000..4bd2ae8
--- /dev/null
+++ b/tools/idegen/src/Stopwatch.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.
+ */
+
+/**
+ * Measures passed time.
+ */
+class Stopwatch {
+
+ long last = System.currentTimeMillis();
+
+ void reset(String label) {
+ long now = System.currentTimeMillis();
+ Log.info(label + ": " + (now - last) + "ms");
+ last = now;
+ }
+}
\ No newline at end of file