├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── jarRepositories.xml
├── kotlinc.xml
├── misc.xml
└── vcs.xml
├── LICENSE
├── README.md
├── build.gradle.kts
├── docs
└── stack_chart.png
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
└── main
└── java
└── com
└── bromano
└── instrumentsgecko
├── Gecko.kt
├── GeckoCommand.kt
├── GeckoGenerator.kt
├── InstrumentsParser.kt
├── Logger.kt
└── ShellUtils.kt
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/java,gradle,intellij
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle,intellij
3 |
4 | ### Intellij ###
5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
7 |
8 | # User-specific stuff
9 | .idea/**/workspace.xml
10 | .idea/**/tasks.xml
11 | .idea/**/usage.statistics.xml
12 | .idea/**/dictionaries
13 | .idea/**/shelf
14 |
15 | # AWS User-specific
16 | .idea/**/aws.xml
17 |
18 | # Generated files
19 | .idea/**/contentModel.xml
20 |
21 | # Sensitive or high-churn files
22 | .idea/**/dataSources/
23 | .idea/**/dataSources.ids
24 | .idea/**/dataSources.local.xml
25 | .idea/**/sqlDataSources.xml
26 | .idea/**/dynamic.xml
27 | .idea/**/uiDesigner.xml
28 | .idea/**/dbnavigator.xml
29 |
30 | # Gradle
31 | .idea/**/gradle.xml
32 | .idea/**/libraries
33 |
34 | # Gradle and Maven with auto-import
35 | # When using Gradle or Maven with auto-import, you should exclude module files,
36 | # since they will be recreated, and may cause churn. Uncomment if using
37 | # auto-import.
38 | # .idea/artifacts
39 | # .idea/compiler.xml
40 | # .idea/jarRepositories.xml
41 | # .idea/modules.xml
42 | # .idea/*.iml
43 | # .idea/modules
44 | # *.iml
45 | # *.ipr
46 |
47 | # CMake
48 | cmake-build-*/
49 |
50 | # Mongo Explorer plugin
51 | .idea/**/mongoSettings.xml
52 |
53 | # File-based project format
54 | *.iws
55 |
56 | # IntelliJ
57 | out/
58 |
59 | # mpeltonen/sbt-idea plugin
60 | .idea_modules/
61 |
62 | # JIRA plugin
63 | atlassian-ide-plugin.xml
64 |
65 | # Cursive Clojure plugin
66 | .idea/replstate.xml
67 |
68 | # SonarLint plugin
69 | .idea/sonarlint/
70 |
71 | # Crashlytics plugin (for Android Studio and IntelliJ)
72 | com_crashlytics_export_strings.xml
73 | crashlytics.properties
74 | crashlytics-build.properties
75 | fabric.properties
76 |
77 | # Editor-based Rest Client
78 | .idea/httpRequests
79 |
80 | # Android studio 3.1+ serialized cache file
81 | .idea/caches/build_file_checksums.ser
82 |
83 | ### Intellij Patch ###
84 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
85 |
86 | # *.iml
87 | # modules.xml
88 | # .idea/misc.xml
89 | # *.ipr
90 |
91 | # Sonarlint plugin
92 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
93 | .idea/**/sonarlint/
94 |
95 | # SonarQube Plugin
96 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
97 | .idea/**/sonarIssues.xml
98 |
99 | # Markdown Navigator plugin
100 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
101 | .idea/**/markdown-navigator.xml
102 | .idea/**/markdown-navigator-enh.xml
103 | .idea/**/markdown-navigator/
104 |
105 | # Cache file creation bug
106 | # See https://youtrack.jetbrains.com/issue/JBR-2257
107 | .idea/$CACHE_FILE$
108 |
109 | # CodeStream plugin
110 | # https://plugins.jetbrains.com/plugin/12206-codestream
111 | .idea/codestream.xml
112 |
113 | # Azure Toolkit for IntelliJ plugin
114 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
115 | .idea/**/azureSettings.xml
116 |
117 | ### Java ###
118 | # Compiled class file
119 | *.class
120 |
121 | # Log file
122 | *.log
123 |
124 | # BlueJ files
125 | *.ctxt
126 |
127 | # Mobile Tools for Java (J2ME)
128 | .mtj.tmp/
129 |
130 | # Package Files #
131 | *.jar
132 | *.war
133 | *.nar
134 | *.ear
135 | *.zip
136 | *.tar.gz
137 | *.rar
138 |
139 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
140 | hs_err_pid*
141 | replay_pid*
142 |
143 | ### Gradle ###
144 | .gradle
145 | **/build/
146 | !src/**/build/
147 |
148 | # Ignore Gradle GUI config
149 | gradle-app.setting
150 |
151 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
152 | !gradle-wrapper.jar
153 |
154 | # Avoid ignore Gradle wrappper properties
155 | !gradle-wrapper.properties
156 |
157 | # Cache of project
158 | .gradletasknamecache
159 |
160 | # Eclipse Gradle plugin generated files
161 | # Eclipse Core
162 | .project
163 | # JDT-specific (Eclipse Java Development Tools)
164 | .classpath
165 |
166 | ### Gradle Patch ###
167 | # Java heap dump
168 | *.hprof
169 |
170 | # End of https://www.toptal.com/developers/gitignore/api/java,gradle,intellij
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Instruments To Gecko
2 |
3 | Convert an Instruments file into the Gecko Profile Format for use in Firefox Profiler ([example](https://profiler.firefox.com/public/w2teve8w5fp7tmpwycwhh6etrb3kr4f1ttyfyy8/flame-graph/?globalTrackOrder=0&hiddenLocalTracksByPid=0-7&thread=0&timelineType=category&v=10)).
4 |
5 | 
6 |
7 | ## Usage
8 |
9 | ```
10 | Usage: gecko [OPTIONS]
11 |
12 | Convert Instruments Trace to Gecko Format (Firefox Profiler)
13 |
14 | Options:
15 | -i, --input PATH Input Instruments Trace
16 | --app TEXT Name of app to match the dSyms to (e.g. YourApp)
17 | --run INT Which run within the trace file to analyze
18 | -o, --output PATH Output Path for gecko profile
19 | -h, --help Show this message and exit
20 |
21 | ```
22 |
23 | **Note: XCode 14.3 Beta or higher is required**
24 |
25 | **Example Command**
26 |
27 | ```bash
28 |
29 | # Build executable jar
30 | ./gradlew shadowJar
31 |
32 | # Convert Instruments trace to Gecko on the second run within the trace file
33 | java -jar ./build/libs/instruments-to-gecko.jar --input example.trace --run 2 --app YourApp --output examplestandalone.json.gz
34 | ```
35 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | java
3 | kotlin("jvm") version "1.8.0"
4 | id("com.github.johnrengelman.shadow") version "7.1.2"
5 | }
6 |
7 | group = "com.bromano"
8 | version = "1.0"
9 |
10 | repositories {
11 | mavenCentral()
12 | }
13 |
14 | dependencies {
15 | implementation(kotlin("stdlib"))
16 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
17 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
18 | implementation("com.github.ajalt.clikt:clikt:3.5.0")
19 | implementation("com.google.code.gson:gson:2.10")
20 | }
21 |
22 | tasks.getByName("test") {
23 | useJUnitPlatform()
24 | }
25 |
26 | tasks.withType {
27 | archiveFileName.set("instruments-to-gecko.jar")
28 | manifest {
29 | attributes(mapOf("Main-Class" to "com.bromano.instrumentsgecko.GeckoCommandKt"))
30 | }
31 | }
--------------------------------------------------------------------------------
/docs/stack_chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjaminRomano/instruments-to-gecko/3d2f13b7039512d4ef6efa92c961aec7b76e45e8/docs/stack_chart.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjaminRomano/instruments-to-gecko/3d2f13b7039512d4ef6efa92c961aec7b76e45e8/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Stop when "xargs" is not available.
209 | if ! command -v xargs >/dev/null 2>&1
210 | then
211 | die "xargs is not available"
212 | fi
213 |
214 | # Use "xargs" to parse quoted args.
215 | #
216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
217 | #
218 | # In Bash we could simply go:
219 | #
220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
221 | # set -- "${ARGS[@]}" "$@"
222 | #
223 | # but POSIX shell has neither arrays nor command substitution, so instead we
224 | # post-process each arg (as a line of input to sed) to backslash-escape any
225 | # character that might be a shell metacharacter, then use eval to reverse
226 | # that process (while maintaining the separation between arguments), and wrap
227 | # the whole thing up as a single "set" statement.
228 | #
229 | # This will of course break if any of these variables contains a newline or
230 | # an unmatched quote.
231 | #
232 |
233 | eval "set -- $(
234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
235 | xargs -n1 |
236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
237 | tr '\n' ' '
238 | )" '"$@"'
239 |
240 | exec "$JAVACMD" "$@"
241 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if %ERRORLEVEL% equ 0 goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if %ERRORLEVEL% equ 0 goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | set EXIT_CODE=%ERRORLEVEL%
84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
86 | exit /b %EXIT_CODE%
87 |
88 | :mainEnd
89 | if "%OS%"=="Windows_NT" endlocal
90 |
91 | :omega
92 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "instruments-to-gecko"
2 |
3 |
--------------------------------------------------------------------------------
/src/main/java/com/bromano/instrumentsgecko/Gecko.kt:
--------------------------------------------------------------------------------
1 | package com.bromano.instrumentsgecko
2 |
3 | import com.google.gson.Gson
4 | import java.io.FileOutputStream
5 | import java.nio.charset.StandardCharsets.UTF_8
6 | import java.nio.file.Path
7 | import java.util.zip.GZIPOutputStream
8 |
9 | const val USER_CATEGORY = 0
10 | const val FRAMEWORK_CATEGORY = 1
11 | const val LIBRARY_CATEGORY = 2
12 | const val OTHER_CATEGORY = 3
13 | const val VIRTUAL_MEMORY_CATEGORY = 4
14 |
15 | val VIRTUAL_MEMORY_ADDR = ULong.MAX_VALUE - 1UL
16 |
17 | val DEFAULT_CATEGORIES = listOf(
18 | Category("User", "yellow", listOf("Other")),
19 | Category("Framework", "green", listOf("Other")),
20 | Category("System", "orange", listOf("Other")),
21 | Category("Other", "grey", listOf("Other")),
22 | Category("Virtual Memory", "blue", listOf("Other")),
23 | )
24 |
25 | /**
26 | * Gecko Profile format
27 | *
28 | * Original types can be found here:
29 | * https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
30 | *
31 | * Example code constructing valid Gecko Profiles can be found here:
32 | * https://android.googlesource.com/platform/system/extras/+/master/simpleperf/scripts/gecko_profile_generator.py
33 | *
34 | * Note: Anything typed as List is not yet supported
35 | */
36 | data class GeckoProfile(
37 | val meta: GeckoMeta,
38 | // TODO: Look into having Gecko do desymbolication
39 | // ref: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/profile.js#L396
40 | val libs: List = emptyList(),
41 | val threads: List,
42 | val pausedRange: List = emptyList(),
43 | val processes: List = emptyList(),
44 | ) {
45 | /**
46 | * Write GeckoProfile to file as Gzipped Json
47 | */
48 | fun toFile(output: Path) {
49 | val json = Gson().toJson(this)
50 | GZIPOutputStream(FileOutputStream(output.toFile())).bufferedWriter(UTF_8).use { it.write(json) }
51 | }
52 | }
53 |
54 | data class GeckoThread(
55 | val name: String,
56 | val registerTime: Long = 0,
57 | val processType: String = "default",
58 | val processName: String? = null,
59 | val unregisterTime: Long? = null,
60 | val tid: Int,
61 | val pid: Long,
62 | val markers: GeckoMarkers = GeckoMarkers(),
63 | val samples: GeckoSamples,
64 | val frameTable: GeckoFrameTable,
65 | val stackTable: GeckoStackTable,
66 | val stringTable: List,
67 | )
68 |
69 | data class GeckoMarkers(
70 | val schema: GeckoMarkersSchema = GeckoMarkersSchema(),
71 | val data: List> = emptyList(),
72 | )
73 |
74 | data class GeckoMarkersSchema(
75 | val name: Int = 0,
76 | val startTime: Int = 1,
77 | val endTime: Int = 2,
78 | val phase: Int = 3,
79 | val category: Int = 4,
80 | val data: Int = 5,
81 | )
82 |
83 | data class GeckoMeta(
84 | val version: Long = 24,
85 | val startTime: Double,
86 | val shutdownTime: Long? = null,
87 | val categories: List = DEFAULT_CATEGORIES,
88 | val markerSchema: List = emptyList(),
89 | val interval: Int = 1,
90 | val stackwalk: Int = 1,
91 | val debug: Int = 0,
92 | val gcpoision: Int = 0,
93 | val processType: Int = 0,
94 | val presymbolicated: Boolean? = true,
95 | )
96 |
97 | data class Category(
98 | val name: String,
99 | val color: String,
100 | val subcategories: List
101 | )
102 |
103 | data class GeckoSamples(
104 | val schema: GeckoSampleSchema = GeckoSampleSchema(),
105 | val data: List>,
106 | )
107 |
108 | data class GeckoSampleSchema(
109 | val stack: Int = 0,
110 | val time: Int = 1,
111 | val eventDelay: Int = 2,
112 | )
113 |
114 | class GeckoSample(
115 | val stackId: Long?,
116 | val timeMs: Double,
117 | val eventDelay: Double = 0.0
118 | ) {
119 | fun toData() = arrayOf(stackId, timeMs, eventDelay)
120 | }
121 |
122 | data class GeckoStackTable(
123 | val schema: GeckoStackTableSchema = GeckoStackTableSchema(),
124 | val data: List>
125 | )
126 |
127 | data class GeckoStackTableSchema(
128 | val prefix: Int = 0,
129 | val frame: Int = 1,
130 | )
131 |
132 | data class GeckoStack(
133 | // Id of stack with matching prefix
134 | val prefixId: Long?,
135 | val frameId: Long,
136 | val category: Int = 0,
137 | ) {
138 | fun toData() = arrayOf(prefixId, frameId, category)
139 | }
140 |
141 | data class GeckoFrameTable(
142 | val schema: GeckoFrameTableSchema = GeckoFrameTableSchema(),
143 | val data: List>
144 | )
145 |
146 | data class GeckoFrameTableSchema(
147 | val location: Int = 0,
148 | val relevantForJS: Int = 1,
149 | val innerWindowID: Int = 2,
150 | val implementation: Int = 3,
151 | val optimizations: Int = 4,
152 | val line: Int = 5,
153 | val column: Int = 6,
154 | val category: Int = 7,
155 | val subcategory: Int = 8,
156 | )
157 |
158 | data class GeckoFrame(
159 | val stringId: Long,
160 | val relevantForJS: Boolean = false,
161 | val innerWindowID: Int = 0,
162 | val implementation: String? = null,
163 | val optimizations: String? = null,
164 | val line: String? = null,
165 | val column: String? = null,
166 | val category: Int = 0,
167 | val subcategory: Int = 0,
168 | ) {
169 | fun toData() = arrayOf(
170 | stringId,
171 | relevantForJS,
172 | innerWindowID,
173 | implementation,
174 | optimizations,
175 | line,
176 | column,
177 | category,
178 | subcategory,
179 | )
180 | }
--------------------------------------------------------------------------------
/src/main/java/com/bromano/instrumentsgecko/GeckoCommand.kt:
--------------------------------------------------------------------------------
1 | package com.bromano.instrumentsgecko
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import com.github.ajalt.clikt.parameters.options.default
5 | import com.github.ajalt.clikt.parameters.options.option
6 | import com.github.ajalt.clikt.parameters.options.required
7 | import com.github.ajalt.clikt.parameters.types.int
8 | import com.github.ajalt.clikt.parameters.types.path
9 | import kotlin.concurrent.thread
10 | import kotlin.system.exitProcess
11 |
12 | fun main(args: Array) = GeckoCommand().main(args)
13 |
14 | class GeckoCommand : CliktCommand(help = "Convert Instruments Trace to Gecko Format (Firefox Profiler)") {
15 |
16 | private val input by option(
17 | "-i", "--input",
18 | help = "Input Instruments Trace",
19 | )
20 | .path(mustExist = true, canBeDir = true)
21 | .required()
22 |
23 | private val app by option("--app", help = "Name of app (e.g. YourApp)")
24 |
25 | private val runNum by option(
26 | "--run",
27 | help = "Which run within the trace file to analyze",
28 | ).int().default(1)
29 |
30 | private val output by option(
31 | "-o", "--output",
32 | help = "Output Path for gecko profile",
33 | )
34 | .path(mustExist = false, canBeDir = false)
35 | .required()
36 |
37 | override fun run() {
38 | lateinit var samples: List
39 | lateinit var loadedImageList: List
40 |
41 | var threadIdSamples: List? = null
42 | var virtualMemorySamples: List? = null
43 | var syscallSamples: List? = null
44 |
45 | val timeProfilerSettings = InstrumentsParser.getInstrumentsSettings(input, runNum)
46 |
47 | // xctrace queries can be quite slow so parallelize them
48 | Logger.timedLog("Loading Symbols, Samples and Load Addresses...") {
49 | val thread1 = thread(start = true) {
50 | samples = InstrumentsParser.loadSamples(TIME_PROFILE_SCHEMA, SAMPLE_TIME_TAG, input, runNum)
51 | }
52 | .addUncaughtExceptionHandler()
53 |
54 | val thread2 = thread(start = true) { loadedImageList = InstrumentsParser.sortedImageList(input, runNum) }
55 | .addUncaughtExceptionHandler()
56 |
57 | val thread3: Thread? = if (timeProfilerSettings.hasThreadStates) {
58 | thread(start = true) { threadIdSamples = InstrumentsParser.loadIdleThreadSamples(input, runNum) }
59 | .addUncaughtExceptionHandler()
60 | } else null
61 |
62 | val thread4: Thread? = if (timeProfilerSettings.hasVirtualMemory) {
63 | thread(start = true) {
64 | virtualMemorySamples =
65 | InstrumentsParser.loadSamples(VIRTUAL_MEMORY_SCHEMA, START_TIME_TAG, input, runNum)
66 | }
67 | .addUncaughtExceptionHandler()
68 | } else null
69 |
70 | val thread5: Thread? = if (timeProfilerSettings.hasSyscalls) {
71 | thread(start = true) {
72 | syscallSamples = InstrumentsParser.loadSamples(SYSCALL_SCHEMA, START_TIME_TAG, input, runNum)
73 | }
74 | .addUncaughtExceptionHandler()
75 | } else null
76 |
77 | thread1.join()
78 | thread2.join()
79 | thread3?.join()
80 | thread4?.join()
81 | thread5?.join()
82 | }
83 |
84 | val concatenatedSamples =
85 | (syscallSamples ?: emptyList()) + (threadIdSamples ?: emptyList()) + (virtualMemorySamples
86 | ?: emptyList()) + samples
87 |
88 | val profile = Logger.timedLog("Converting to Gecko format") {
89 | GeckoGenerator.createGeckoProfile(app, concatenatedSamples, loadedImageList, timeProfilerSettings)
90 | }
91 |
92 | Logger.timedLog("Gzipping and writing to disk") {
93 | profile.toFile(output)
94 | }
95 | }
96 |
97 | private fun Thread.addUncaughtExceptionHandler() = also {
98 | setUncaughtExceptionHandler { _, ex ->
99 | ex.printStackTrace()
100 | exitProcess(1)
101 | }
102 | }
103 | }
--------------------------------------------------------------------------------
/src/main/java/com/bromano/instrumentsgecko/GeckoGenerator.kt:
--------------------------------------------------------------------------------
1 | package com.bromano.instrumentsgecko
2 |
3 | import java.util.*
4 |
5 | // The multiplier used for estimating whether the thread is idle instead of preempted or blocked
6 | const val THREAD_IDLE_MULTIPLIER = 5
7 |
8 | private val libraryComparator = Comparator { libA, libB ->
9 | when {
10 | (libA.loadAddress > libB.loadAddress) -> 1
11 | (libA.loadAddress < libB.loadAddress) -> -1
12 | (libA.loadAddress == libB.loadAddress) -> 0
13 | else -> 0
14 | }
15 |
16 | }
17 |
18 | /**
19 | * Generate a Gecko File
20 | */
21 | object GeckoGenerator {
22 |
23 | fun getLibraryPathForSymbol(libraryList: List, symbol: SymbolEntry): String? {
24 | val dummyLib = Library("", "", symbol.address, "")
25 | val position = Collections.binarySearch(libraryList, dummyLib, libraryComparator)
26 |
27 | /** If key is not present, Collections.binarySearch returns "(-(insertion point) - 1)".
28 | * Since we're searching through a list of loaded libraries, we don't expect
29 | * the symbol to match any of the values exactly. Instead, we'll look for the library
30 | * that contains this address.
31 | *
32 | * For example, consider the following symbol lookup on the given sorted library list:
33 | * symbol@0x1000052
34 | * |
35 | * v<<<<<<<|
36 | * +---------------+---------------+---------------+
37 | * | dyld | Foundation | UIKit |
38 | * +---------------+---------------+---------------+
39 | * 0x1000000 0x1a00000 0x1f00000
40 | *
41 | * Though it matches against insert position == 1, we should attribute this
42 | * symbol to dyld.
43 | */
44 |
45 | if (position >= 0) {
46 | // Found an exact match. This is unlikely, but should still be handled.
47 | val res = libraryList.elementAt(position).path
48 | return res
49 | }
50 |
51 | // Inverse operation of -(insertion point) - 1
52 | val insertPosition = Math.abs(position) - 1
53 | if (insertPosition == 0) {
54 | return null
55 | }
56 | val res = libraryList.elementAt(insertPosition - 1).path
57 | return res
58 |
59 | }
60 |
61 | fun createGeckoProfile(
62 | app: String?,
63 | samples: List,
64 | symbolsInfo: List,
65 | timeProfilerSettings: InstrumentsSettings,
66 | ): GeckoProfile {
67 | val interval = if (timeProfilerSettings.highFrequency) 1.0 else 5.0
68 |
69 | val threads = samples.groupBy {
70 | it.thread.tid
71 | }.map { (threadId, samples) ->
72 | val frameTable = mutableListOf()
73 | val stackTable = mutableListOf()
74 | val stringTable = mutableListOf()
75 |
76 | val frameMap = mutableMapOf()
77 | // Stored as (stackPrefixId, frameId)
78 | val stackMap = mutableMapOf, Long>()
79 | val stringMap = mutableMapOf()
80 |
81 | var priorSample: InstrumentsSample? = null
82 |
83 | val geckoSamples = samples.sortedBy { it.sampleTime }.flatMap {
84 |
85 | // Intern Frame
86 | val frameIds = it.backtrace.map { frame ->
87 | // Best effort try to find dsym string name
88 | val dsymFrame = frame.name
89 |
90 | // Intern String
91 | val stringId = stringMap.getOrPut(dsymFrame) {
92 | val stringId = stringTable.size.toLong()
93 | stringTable.add(dsymFrame)
94 | stringId
95 | }
96 |
97 | frameMap.getOrPut(dsymFrame) {
98 | val frameId = frameTable.size.toLong()
99 | frameTable.add(
100 | GeckoFrame(
101 | stringId = stringId,
102 | category = getLibraryCategory(
103 | app,
104 | frame,
105 | getLibraryPathForSymbol(symbolsInfo, frame)
106 | )
107 | )
108 | )
109 | frameId
110 | }
111 | }
112 |
113 | // Intern Stacks
114 | var prefixId: Long? = null
115 | frameIds.reversed().forEach { frameId ->
116 | prefixId = stackMap.getOrPut(Pair(prefixId, frameId)) {
117 | val stackId = stackTable.size.toLong()
118 | stackTable.add(
119 | GeckoStack(
120 | prefixId,
121 | frameId,
122 | )
123 | )
124 | stackId
125 | }
126 | }
127 |
128 | val geckoSample = GeckoSample(
129 | stackId = prefixId,
130 | timeMs = it.sampleTime
131 | )
132 |
133 | if (timeProfilerSettings.hasThreadStates) {
134 | return@flatMap listOf(geckoSample)
135 | }
136 |
137 | // Without idle thread states, we cannot differentiate idle vs. pre-emprted, runnable or blocked states.
138 | // Thus, we will over-represent the last callstack's weight when the thread transitions to idle.
139 | // To mitigate this, we use a heuristic that if we haven't received a sample in a while, thre thread is
140 | // likely idle, and we will automatically insert an idle sample.
141 | val newGeckoSamples = priorSample?.let { prior ->
142 | val priorSampleEndTime = prior.sampleTime + prior.weightMs
143 | val delta = it.sampleTime - priorSampleEndTime
144 | if (delta > interval * THREAD_IDLE_MULTIPLIER) {
145 | listOf(
146 | GeckoSample(
147 | stackId = null,
148 | timeMs = priorSampleEndTime,
149 | ), geckoSample
150 | )
151 | } else {
152 | null
153 | }
154 | } ?: listOf(geckoSample)
155 |
156 | priorSample = it
157 | newGeckoSamples
158 | }
159 |
160 | GeckoThread(
161 | name = samples.firstOrNull()?.thread?.threadName ?: "",
162 | tid = threadId,
163 | // Currently, we only support single process runs
164 | pid = 0,
165 | samples = GeckoSamples(data = geckoSamples.map { it.toData() }),
166 | stringTable = stringTable,
167 | frameTable = GeckoFrameTable(data = frameTable.map { it.toData() }),
168 | stackTable = GeckoStackTable(data = stackTable.map { it.toData() }),
169 | )
170 | }
171 |
172 | val traceStartTime = samples.map { it.sampleTime }.minOrNull() ?: 0.0
173 |
174 | return GeckoProfile(
175 | meta = GeckoMeta(startTime = traceStartTime),
176 | threads = threads,
177 | )
178 | }
179 |
180 | private fun getLibraryCategory(app: String?, frame: SymbolEntry, library: String?): Int {
181 | if (frame.address == VIRTUAL_MEMORY_ADDR) {
182 | return VIRTUAL_MEMORY_CATEGORY
183 | } else if (library == null) {
184 | return OTHER_CATEGORY
185 | }
186 | return when {
187 | app != null && library.contains(app) -> USER_CATEGORY
188 | library.contains("/System/Library/") -> FRAMEWORK_CATEGORY
189 | library.contains("/Symbols/usr/") -> LIBRARY_CATEGORY
190 | library.startsWith("/usr") -> LIBRARY_CATEGORY
191 | else -> OTHER_CATEGORY
192 | }
193 | }
194 | }
--------------------------------------------------------------------------------
/src/main/java/com/bromano/instrumentsgecko/InstrumentsParser.kt:
--------------------------------------------------------------------------------
1 | package com.bromano.instrumentsgecko
2 |
3 | import org.w3c.dom.Document
4 | import org.w3c.dom.Element
5 | import org.w3c.dom.Node
6 | import org.w3c.dom.NodeList
7 | import org.xml.sax.InputSource
8 | import java.io.StringReader
9 | import java.io.StringWriter
10 | import java.nio.file.Path
11 | import javax.xml.parsers.DocumentBuilderFactory
12 | import javax.xml.transform.OutputKeys
13 | import javax.xml.transform.TransformerFactory
14 | import javax.xml.transform.dom.DOMSource
15 | import javax.xml.transform.stream.StreamResult
16 | import javax.xml.xpath.XPathConstants
17 | import javax.xml.xpath.XPathFactory
18 |
19 | private const val THREAD_TAG = "thread"
20 | private const val THREAD_STATE_TAG = "thread-state"
21 | private const val WEIGHT_TAG = "weight"
22 | const val SAMPLE_TIME_TAG = "sample-time"
23 | private const val BACKTRACE_TAG = "backtrace"
24 | private const val TID_TAG = "tid"
25 | private const val FRAME_TAG = "frame"
26 | private const val BINARY_TAG = "binary"
27 | private const val ROW_TAG = "row"
28 | private const val RUN_TAG = "run"
29 | private const val TABLE_TAG = "table"
30 | private const val VM_OP_TAG = "vm-op"
31 | const val START_TIME_TAG = "start-time"
32 |
33 | const val TIME_PROFILE_SCHEMA = "time-profile"
34 | const val VIRTUAL_MEMORY_SCHEMA = "virtual-memory"
35 | private const val THREAD_STATE_SCHEMA = "thread-state"
36 | const val SYSCALL_SCHEMA = "syscall"
37 |
38 | private const val NUMBER_ATTR = "number"
39 | private const val SCHEMA_ATTR = "schema"
40 |
41 |
42 | /**
43 | * ThreadDescription contains a thread name and ID to identify
44 | * a given thread discovered in an Instruments documnent.
45 | */
46 | data class ThreadDescription(
47 | val threadName: String,
48 | val tid: Int
49 | ) {
50 | override fun toString(): String {
51 | return "$threadName (tid: $tid)"
52 | }
53 | }
54 |
55 | data class InstrumentsSettings(
56 | val highFrequency: Boolean,
57 | val waitingThreads: Boolean,
58 | val contextSwitches: Boolean,
59 | val kernelCallstacks: Boolean,
60 | val hasThreadStates: Boolean,
61 | val hasVirtualMemory: Boolean,
62 | val hasSyscalls: Boolean,
63 | )
64 |
65 | /**
66 | * InstrumentsSample represents a profile sample discovered in an
67 | * Instruments document. This contains information about the thread
68 | * being sampled, such as when it was sampled and a corresponding backtrace.
69 | */
70 | data class InstrumentsSample(
71 | val thread: ThreadDescription,
72 | val sampleTime: Double,
73 | val weightMs: Double,
74 | val backtrace: List
75 | ) {
76 | override fun toString(): String {
77 | return """
78 | |Time: $sampleTime
79 | |$thread:
80 | |${"\t"}${backtrace.reversed().joinToString("\n\t")}
81 | """.trimMargin()
82 | }
83 |
84 | }
85 |
86 | /**
87 | * Library represents a loaded binary image. This can be the primary executable
88 | * or any number of shared libraries / frameworks that were part of the process'
89 | * address space.
90 | */
91 | data class Library(
92 | val path: String,
93 | val uuid: String,
94 | val loadAddress: ULong,
95 | val arch: String
96 | )
97 |
98 | /**
99 | * SymbolEntry represents a symbol, such as from a backtrace.
100 | */
101 | data class SymbolEntry(
102 | val address: ULong,
103 | val name: String,
104 | ) {
105 | override fun toString(): String {
106 | return name
107 | }
108 | }
109 |
110 | /**
111 | * Utilities for parsing Instruments files
112 | */
113 | object InstrumentsParser {
114 |
115 | /**
116 | * Create a sorted list of Libraries suitable for lookup. These are sorted
117 | * so address queries can be resolved via binary search.
118 | */
119 | fun sortedImageList(input: Path, runNum: Int = 1): List {
120 | val mapped = createBinaryImageMapping(input, runNum)
121 | return mapped.values.sortedBy { it.loadAddress }
122 | }
123 |
124 | /**
125 | * Returns a mapping between a unique libraryID and the Library itself. This represents
126 | * the executable address space of the given process.
127 | *
128 | * Example: ( tag)
129 | *
130 | *
131 | * 49937041
132 | *
133 | *
134 | *
135 | *
136 | * 1127125
137 | *
138 | *
139 | *
140 | *
141 | *
142 | *
143 | */
144 | private fun createBinaryImageMapping(input: Path, runNum: Int = 1): Map {
145 | val idToLibrary = mutableMapOf()
146 | val document = queryXCTrace(input, "/trace-toc[1]/run[$runNum]/data[1]/table[@schema=\"$TIME_PROFILE_SCHEMA\"]")
147 | document.getElementsByTagName(FRAME_TAG)
148 | .asSequence()
149 | .flatMap { it.childNodesSequence() }
150 | .filter { it.nodeName == BINARY_TAG }
151 | .forEach {
152 | val binaryId = it.getIdAttrValue()
153 | val binaryPath = it.getPathAttrValue()
154 | val loadAddr = it.getLoadAddrAttrValue()
155 | val arch = it.getArchAttrValue()
156 | val uuid = it.getUUIDAttrValue()
157 | if (binaryId != null && binaryPath != null && loadAddr != null && arch != null && uuid != null) {
158 | val library = Library(binaryPath, uuid, loadAddr.removePrefix("0x").toULong(16), arch)
159 | idToLibrary[binaryId] = library
160 | }
161 | }
162 | return idToLibrary
163 | }
164 |
165 | fun getInstrumentsSettings(input: Path, runNum: Int = 1): InstrumentsSettings {
166 | val runNode = queryXCTraceTOC(input)
167 | .getElementsByTagName(RUN_TAG)
168 | .asSequence()
169 | .firstOrNull {
170 | it.getAttrValue(NUMBER_ATTR) == runNum.toString()
171 | }
172 |
173 | val runElement =
174 | runNode as? Element ?: throw IllegalStateException("Cannot find run $runNum in table of contents")
175 |
176 | val timeProfileNodes = runElement.getElementsByTagName(TABLE_TAG)
177 | .asSequence()
178 | .filter { it.getAttrValue(SCHEMA_ATTR) == TIME_PROFILE_SCHEMA }
179 | .toList()
180 |
181 | val hasThreadStates = runElement.getElementsByTagName(TABLE_TAG)
182 | .asSequence()
183 | .any { it.getAttrValue(SCHEMA_ATTR) == THREAD_STATE_SCHEMA }
184 |
185 | val hasVirtualMemory = runElement.getElementsByTagName(TABLE_TAG)
186 | .asSequence()
187 | .any { it.getAttrValue(SCHEMA_ATTR) == VIRTUAL_MEMORY_SCHEMA }
188 |
189 | val hasSyscalls = runElement.getElementsByTagName(TABLE_TAG)
190 | .asSequence()
191 | .any { it.getAttrValue(SCHEMA_ATTR) == SYSCALL_SCHEMA }
192 |
193 |
194 | return InstrumentsSettings(
195 | timeProfileNodes.any { it.getAttrValue("high-frequency-sampling") == "1" },
196 | timeProfileNodes.any { it.getAttrValue("record-waiting-threads") == "1" },
197 | timeProfileNodes.any { it.getAttrValue("context-switch-sampling") == "1" },
198 | timeProfileNodes.any { it.getAttrValue("needs-kernel-callstack") == "1" },
199 | hasThreadStates,
200 | hasVirtualMemory,
201 | hasSyscalls
202 | )
203 | }
204 |
205 | /**
206 | * Extract Instrument Samples from trace
207 | *
208 | * Note: Samples may not be in-order. In some scenarios, Instruments returns out-of-order data.
209 | *
210 | * Example:
211 | *
212 | * 178410208
213 | *
214 | *
215 | *
216 | *
217 | * 1127125
218 | *
219 | *
220 | *
221 | *
222 | *
223 | *
224 | *
225 | *
226 | *
227 | *
228 | *
229 | *
230 | *
231 | *
232 | *
233 | */
234 | fun loadSamples(schema: String, timeTag: String, input: Path, runNum: Int = 1): List {
235 | val document = queryXCTrace(input, "/trace-toc[1]/run[$runNum]/data[1]/table[@schema=\"$schema\"]")
236 |
237 | val originalNodeCache = mutableMapOf()
238 | preloadTags(
239 | document,
240 | listOf(BACKTRACE_TAG, timeTag, VM_OP_TAG, WEIGHT_TAG, THREAD_TAG, TID_TAG),
241 | originalNodeCache
242 | )
243 |
244 | val previousBacktraces = mutableMapOf()
245 | return document.getElementsByTagName(BACKTRACE_TAG)
246 | .asSequence()
247 | .map { backtraceNode ->
248 | val rowNode = backtraceNode.parentNode
249 | val originalBacktraceNode = getOriginalNode(document, backtraceNode, originalNodeCache)
250 |
251 | val sampleTime = rowNode.getFirstOriginalNodeByTag(document, timeTag, originalNodeCache)
252 | .asTimeValue()
253 | ?: throw IllegalStateException(
254 | "Cannot find $timeTag for:\n${backtraceNode.toXMLString(true)}"
255 | )
256 |
257 | val threadNode = rowNode.getFirstOriginalNodeByTag(document, THREAD_TAG, originalNodeCache)
258 |
259 | // The duration of the sample (Time Profile only)
260 | val weightMs = rowNode.getOptionalFirstOriginalNodeByTag(document, WEIGHT_TAG, originalNodeCache)
261 | ?.asTimeValue()
262 | ?: 0.0
263 |
264 | val threadName = threadNode.getFmtAttrValue() ?: ""
265 |
266 | val threadId = threadNode.getFirstOriginalNodeByTag(document, TID_TAG, originalNodeCache)
267 | .getChildValue()?.toIntOrNull() ?: -1
268 |
269 | // There can be multiple text address "fragments"
270 | // The first fragment contains addresses that are unique to this backtrace
271 | // Addresses are ordered top of stack to bottom
272 |
273 | val backtrace = originalBacktraceNode.childNodesSequence()
274 | .filter { it.nodeName == FRAME_TAG }
275 | .map {
276 | val name = it.getNameAttrValue() ?: ""
277 | val addr = it.getAddrAttrValue()
278 | val ref = it.getRefAttrValue() ?: ""
279 | val id = it.getIdAttrValue()
280 | if (name == "") {
281 | // Unnamed frame, check if this is a reference
282 | // back to an earlier frame we inspected.
283 | previousBacktraces.getOrDefault(ref, null)
284 | ?: SymbolEntry(ULong.MAX_VALUE, name)
285 | } else {
286 | if (addr != null) {
287 | // Found a full frame description. Store an ID mapping
288 | // back to this frame for future lookups.
289 | val sym = SymbolEntry(addr.removePrefix("0x").toULong(16), name)
290 | if (id != null) {
291 | previousBacktraces[id] = sym
292 | }
293 | sym
294 | } else {
295 | // Found a partial frame description (no address, but we have a symbol name).
296 | val sym = SymbolEntry(ULong.MAX_VALUE, name)
297 | if (id != null) {
298 | previousBacktraces[id] = sym
299 | }
300 | sym
301 | }
302 | }
303 | }
304 | .toMutableList()
305 |
306 | // Append virtual memory operation onto callstack if it exists (e.g. Page Fault)
307 | rowNode.getOptionalFirstOriginalNodeByTag(document, VM_OP_TAG, originalNodeCache)
308 | ?.getFmtAttrValue()
309 | ?.let { backtrace.add(0, SymbolEntry(VIRTUAL_MEMORY_ADDR, it)) }
310 |
311 | InstrumentsSample(
312 | thread = ThreadDescription(threadName, threadId),
313 | sampleTime = sampleTime,
314 | weightMs = weightMs,
315 | backtrace = backtrace,
316 | )
317 | }.toList()
318 | }
319 |
320 | /**
321 | * Convert Idle Thread State transitions into Instruments Samples
322 | *
323 | * Note: Samples may not be in-order. In some rare cases, Instruments returns out-of-order data.
324 | */
325 | fun loadIdleThreadSamples(input: Path, runNum: Int): List {
326 | val document = queryXCTrace(input, "/trace-toc[1]/run[$runNum]/data[1]/table[@schema=\"thread-state\"]")
327 | val originalNodeCache = mutableMapOf()
328 | preloadTags(document, listOf(THREAD_STATE_TAG, START_TIME_TAG, THREAD_TAG, TID_TAG), originalNodeCache)
329 |
330 | return document.getElementsByTagName(THREAD_STATE_TAG)
331 | .asSequence()
332 | .filter {
333 | it.parentNode?.nodeName == ROW_TAG &&
334 | getOriginalNode(document, it, originalNodeCache).getIdAttrValue() == "Idle"
335 | }.map { threadStateNode ->
336 | val rowNode = threadStateNode.parentNode
337 | val sampleTime = rowNode.getFirstOriginalNodeByTag(document, START_TIME_TAG, originalNodeCache)
338 | .asTimeValue()
339 | ?: throw IllegalStateException("row does not have start-time:\n${rowNode.toXMLString(true)}")
340 |
341 | val threadNode = rowNode.getFirstOriginalNodeByTag(document, THREAD_TAG, originalNodeCache)
342 | val threadName = threadNode.getFmtAttrValue() ?: ""
343 | val threadId = threadNode.getFirstOriginalNodeByTag(document, TID_TAG, originalNodeCache)
344 | .let { getOriginalNode(document, it, originalNodeCache).getChildValue()?.toIntOrNull() ?: -1 }
345 |
346 | InstrumentsSample(
347 | thread = ThreadDescription(threadName, threadId),
348 | sampleTime = sampleTime,
349 | weightMs = 0.0,
350 | backtrace = emptyList()
351 | )
352 | }.toList()
353 | }
354 |
355 | /**
356 | * Run a xpath query against a xctrace file and return an XML Document
357 | */
358 | private fun queryXCTrace(input: Path, xpath: String): Document {
359 | val xmlStr = ShellUtils.run(
360 | "xctrace export --input $input --xpath '$xpath'",
361 | redirectOutput = ProcessBuilder.Redirect.PIPE,
362 | shell = true
363 | )
364 |
365 | return processXCTraceOutput(xmlStr)
366 | }
367 |
368 | /**
369 | * Get the Table of Contents
370 | *
371 | * Note: It doesn't seem possible to use `--xpath` to query the TOC
372 | */
373 | private fun queryXCTraceTOC(input: Path): Document {
374 | val xmlStr = ShellUtils.run(
375 | "xctrace export --input $input --toc",
376 | redirectOutput = ProcessBuilder.Redirect.PIPE,
377 | shell = true
378 | )
379 |
380 | return processXCTraceOutput(xmlStr)
381 | }
382 |
383 | private fun processXCTraceOutput(xmlStr: String): Document {
384 | // Remove XML Prolog () since parser can't handle it
385 | val trimmedXmlStr = xmlStr.split("\n", limit = 2)[1]
386 |
387 | return DocumentBuilderFactory.newInstance()
388 | .newDocumentBuilder()
389 | .parse(InputSource(StringReader(trimmedXmlStr)))
390 | }
391 |
392 | /**
393 | * XCTrace XML output avoids duplicating data by having teh first node contain all the information and subsequent
394 | * nodes containing a reference to the original node.
395 | *
396 | * The original node is given a unique identifier using `id` attribute and the subsequent nodes use a
397 | * `ref` attribute with the value set to the `id` of the original node.
398 | *
399 | * Scanning the XML is expensive os to reduce that cost an originalNodeCache is expected to be provided.
400 | *
401 | * Note: This method will mutate the originalNodeCache provided.
402 | */
403 | private fun getOriginalNode(document: Document, node: Node, originalNodeCache: MutableMap): Node {
404 | // If ID attribute exists, we are already at original node
405 | if (node.getIdAttrValue() != null) {
406 | return node
407 | }
408 |
409 | val refId = node.getRefAttrValue()?.toLongOrNull()
410 | ?: throw IllegalStateException("Node with tag, ${node.nodeName} does not have id or ref attribute")
411 |
412 | val cacheKey = "${node.nodeName}:$refId"
413 |
414 | originalNodeCache[cacheKey]?.let { return it }
415 |
416 | return findNodeById(document, node.nodeName, refId).also {
417 | originalNodeCache[cacheKey] = it
418 | }
419 | }
420 |
421 | /**
422 | * Pre-compute the set of ID nodes to avoid expensive re-processing of XML Tree Nodes
423 | */
424 | private fun preloadTags(node: Document, tags: List, originalNodeCache: MutableMap) {
425 | for (tag in tags) {
426 | node.getElementsByTagName(tag)
427 | .asSequence()
428 | .forEach { it.getIdAttrValue()?.let { refId -> originalNodeCache["$tag:$refId"] = it } }
429 | }
430 | }
431 |
432 | private fun findNodeById(node: Document, tag: String, id: Long): Node {
433 | return node.getElementsByTagName(tag).asSequence()
434 | .filter { it.getIdAttrValue()?.toLongOrNull() == id }
435 | .firstOrNull()
436 | ?: throw IllegalStateException("No node with tag, $tag, and id, $id, found.")
437 | }
438 |
439 | // TODO: Remove the unnecessary utility methods?
440 | private fun Node.getAttrValue(attr: String): String? = attributes?.getNamedItem(attr)?.nodeValue
441 | private fun Node.getFmtAttrValue(): String? = getAttrValue("fmt")
442 | private fun Node.getIdAttrValue(): String? = getAttrValue("id")
443 | private fun Node.getRefAttrValue(): String? = getAttrValue("ref")
444 | private fun Node.getNameAttrValue(): String? = getAttrValue("name")
445 | private fun Node.getAddrAttrValue(): String? = getAttrValue("addr")
446 | private fun Node.getPathAttrValue(): String? = getAttrValue("path")
447 | private fun Node.getLoadAddrAttrValue(): String? = getAttrValue("load-addr")
448 | private fun Node.getUUIDAttrValue(): String? = getAttrValue("UUID")
449 | private fun Node.getArchAttrValue(): String? = getAttrValue("arch")
450 |
451 | private fun Node.getChildValue(): String? = firstChild?.nodeValue
452 |
453 | private fun Node.asTimeValue(): Double? = firstChild?.nodeValue?.toDoubleOrNull()?.let { it / 1000.0 / 1000.0 }
454 |
455 | private fun Node.childNodesSequence(): Sequence = childNodes.asSequence()
456 |
457 | private fun NodeList.asSequence(): Sequence {
458 | var i = 0
459 | return generateSequence { item(i++) }
460 | }
461 |
462 | /**
463 | * Find direct descendant with a matching tag then find it's original node if not already the original
464 | */
465 | private fun Node.getFirstOriginalNodeByTag(
466 | document: Document,
467 | tag: String,
468 | originalNodeCache: MutableMap
469 | ): Node {
470 | return getOptionalFirstOriginalNodeByTag(document, tag, originalNodeCache)
471 | ?: throw IllegalStateException("Could not find original node with tag, $tag:\n ${toXMLString(true)}")
472 | }
473 |
474 | private fun Node.getOptionalFirstOriginalNodeByTag(
475 | document: Document,
476 | tag: String,
477 | originalNodeCache: MutableMap
478 | ): Node? {
479 | return this.childNodesSequence().firstOrNull { it.nodeName == tag }
480 | ?.let { getOriginalNode(document, it, originalNodeCache) }
481 | }
482 |
483 | /**
484 | * Convert node to pretty-printed XML String
485 | *
486 | * Ref: https://stackoverflow.com/questions/33935718/save-new-xml-node-to-file
487 | */
488 | private fun Node.toXMLString(deep: Boolean = false): String {
489 | val clonedNode = this.cloneNode(true)
490 | // Remove unwanted whitespaces
491 | clonedNode.normalize()
492 | val xpath = XPathFactory.newInstance().newXPath()
493 | val expr = xpath.compile("//text()[normalize-space()='']")
494 | val nodeList = expr.evaluate(clonedNode, XPathConstants.NODESET) as NodeList
495 |
496 | if (!deep) {
497 | for (i in 0 until nodeList.length) {
498 | val node = nodeList.item(i)
499 | node.parentNode.removeChild(node)
500 | }
501 | }
502 |
503 | // Create and setup transformer
504 | val transformer = TransformerFactory.newInstance().newTransformer().apply {
505 | setOutputProperty(OutputKeys.ENCODING, "UTF-8")
506 | setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
507 | setOutputProperty(OutputKeys.INDENT, "yes")
508 | setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
509 | }
510 |
511 | // Turn the node into a string
512 | return StringWriter().use {
513 | transformer.transform(DOMSource(this), StreamResult(it))
514 | it.toString()
515 | }
516 | }
517 | }
--------------------------------------------------------------------------------
/src/main/java/com/bromano/instrumentsgecko/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.bromano.instrumentsgecko
2 |
3 | import java.util.concurrent.TimeUnit
4 |
5 | class Logger {
6 | companion object {
7 | fun timedLog(message: String, block: () -> T): T {
8 | val startTime = System.currentTimeMillis()
9 | println("$message...")
10 | val result = block()
11 | println("$message... [Done] ${humanReadableTime(System.currentTimeMillis() - startTime)}")
12 | return result
13 | }
14 |
15 | private fun humanReadableTime(millisTotal: Long): String {
16 | val minutes = TimeUnit.MILLISECONDS.toMinutes(millisTotal)
17 | val seconds = TimeUnit.MILLISECONDS.toSeconds(millisTotal) - minutes * 60
18 | val millis = millisTotal % 1000
19 | return StringBuilder().apply {
20 | if (minutes > 0) {
21 | append("${minutes}m ")
22 | }
23 | if (seconds > 0) {
24 | append("${seconds}s ")
25 | }
26 | if (minutes == 0L) {
27 | append("${millis}ms")
28 | }
29 | }.toString()
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/main/java/com/bromano/instrumentsgecko/ShellUtils.kt:
--------------------------------------------------------------------------------
1 | package com.bromano.instrumentsgecko
2 |
3 | import com.github.ajalt.clikt.core.CliktError
4 | import java.awt.Color.red
5 | import java.io.InputStream
6 |
7 | /**
8 | * Utilities for running commands
9 | */
10 | class ShellUtils {
11 | companion object {
12 | fun run(
13 | command: String,
14 | ignoreErrors: Boolean = false,
15 | shell: Boolean = true,
16 | redirectOutput: ProcessBuilder.Redirect = ProcessBuilder.Redirect.INHERIT,
17 | redirectError: ProcessBuilder.Redirect = ProcessBuilder.Redirect.INHERIT,
18 | ): String {
19 | var output = ""
20 | run(
21 | command = command,
22 | ignoreErrors = ignoreErrors,
23 | shell = shell,
24 | redirectOutput = redirectOutput,
25 | redirectError = redirectError,
26 | ) { inputStream ->
27 | output = inputStream.bufferedReader().readText().trim()
28 | }
29 |
30 | return if (redirectOutput == ProcessBuilder.Redirect.PIPE || redirectError == ProcessBuilder.Redirect.PIPE) {
31 | output
32 | } else {
33 | ""
34 | }
35 | }
36 |
37 | private fun run(
38 | command: String,
39 | ignoreErrors: Boolean = false,
40 | shell: Boolean = true,
41 | redirectOutput: ProcessBuilder.Redirect = ProcessBuilder.Redirect.INHERIT,
42 | redirectError: ProcessBuilder.Redirect = ProcessBuilder.Redirect.INHERIT,
43 | outputParser: (InputStream) -> Unit
44 | ) {
45 | val cmds = if (shell) {
46 | arrayOf("/bin/bash", "-c", command)
47 | } else {
48 | arrayOf(command)
49 | }
50 |
51 | val proc = ProcessBuilder(*cmds).apply {
52 | redirectOutput(redirectOutput)
53 | redirectError(redirectError)
54 | }
55 | .redirectInput(ProcessBuilder.Redirect.INHERIT)
56 | .start()
57 |
58 | proc.inputStream.use(outputParser)
59 | val exitCode = proc.waitFor()
60 |
61 | if (exitCode != 0 && !ignoreErrors) {
62 | val error = proc.errorStream.bufferedReader().readText().trim()
63 | throw CliktError("Command failed: ${"$command\n$error"}")
64 | }
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------